from __future__ import print_function
import json
import logging
import os
from textwrap import dedent
from ceph_volume.util import prepare as prepare_utils
from ceph_volume.util import encryption as encryption_utils
from ceph_volume.util import disk
from ceph_volume.util import system
from ceph_volume import decorators, terminal
from ceph_volume.devices.lvm.common import rollback_osd
from .common import create_parser

logger = logging.getLogger(__name__)

def prepare_dmcrypt(key, device, device_type, fsid):
    """
    Helper for devices that are encrypted. The operations needed for
    block, db, wal, or data/journal devices are all the same
    """
    if not device:
        return ''
    kname = disk.lsblk(device)['KNAME']
    mapping = 'ceph-{}-{}-{}-dmcrypt'.format(fsid, kname, device_type)
    # format data device
    encryption_utils.luks_format(
        key,
        device
    )
    encryption_utils.luks_open(
        key,
        device,
        mapping
    )

    return '/dev/mapper/{}'.format(mapping)

def prepare_bluestore(block, wal, db, secrets, osd_id, fsid, tmpfs):
    """
    :param block: The name of the logical volume for the bluestore data
    :param wal: a regular/plain disk or logical volume, to be used for block.wal
    :param db: a regular/plain disk or logical volume, to be used for block.db
    :param secrets: A dict with the secrets needed to create the osd (e.g. cephx)
    :param id_: The OSD id
    :param fsid: The OSD fsid, also known as the OSD UUID
    """
    cephx_secret = secrets.get('cephx_secret', prepare_utils.create_key())

    if secrets.get('dmcrypt_key'):
        key = secrets['dmcrypt_key']
        block = prepare_dmcrypt(key, block, 'block', fsid)
        wal = prepare_dmcrypt(key, wal, 'wal', fsid)
        db = prepare_dmcrypt(key, db, 'db', fsid)

    # create the directory
    prepare_utils.create_osd_path(osd_id, tmpfs=tmpfs)
    # symlink the block
    prepare_utils.link_block(block, osd_id)
    # get the latest monmap
    prepare_utils.get_monmap(osd_id)
    # write the OSD keyring if it doesn't exist already
    prepare_utils.write_keyring(osd_id, cephx_secret)
    # prepare the osd filesystem
    prepare_utils.osd_mkfs_bluestore(
        osd_id, fsid,
        keyring=cephx_secret,
        wal=wal,
        db=db
    )


class Prepare(object):

    help = 'Format a raw device and associate it with a (BlueStore) OSD'

    def __init__(self, argv):
        self.argv = argv
        self.osd_id = None

    def safe_prepare(self, args=None):
        """
        An intermediate step between `main()` and `prepare()` so that we can
        capture the `self.osd_id` in case we need to rollback

        :param args: Injected args, usually from `raw create` which compounds
                     both `prepare` and `create`
        """
        if args is not None:
            self.args = args
        try:
            self.prepare()
        except Exception:
            logger.exception('raw prepare was unable to complete')
            logger.info('will rollback OSD ID creation')
            rollback_osd(self.args, self.osd_id)
            raise
        dmcrypt_log = 'dmcrypt' if args.dmcrypt else 'clear'
        terminal.success("ceph-volume raw {} prepare successful for: {}".format(dmcrypt_log, self.args.data))


    @decorators.needs_root
    def prepare(self):
        secrets = {'cephx_secret': prepare_utils.create_key()}
        encrypted = 1 if self.args.dmcrypt else 0
        cephx_lockbox_secret = '' if not encrypted else prepare_utils.create_key()

        if encrypted:
            secrets['dmcrypt_key'] = os.getenv('CEPH_VOLUME_DMCRYPT_SECRET')
            secrets['cephx_lockbox_secret'] = cephx_lockbox_secret # dummy value to make `ceph osd new` not complaining

        osd_fsid = system.generate_uuid()
        crush_device_class = self.args.crush_device_class
        if crush_device_class:
            secrets['crush_device_class'] = crush_device_class
        tmpfs = not self.args.no_tmpfs
        wal = ""
        db = ""
        if self.args.block_wal:
            wal = self.args.block_wal
        if self.args.block_db:
            db = self.args.block_db

        # reuse a given ID if it exists, otherwise create a new ID
        self.osd_id = prepare_utils.create_id(
            osd_fsid, json.dumps(secrets))

        prepare_bluestore(
            self.args.data,
            wal,
            db,
            secrets,
            self.osd_id,
            osd_fsid,
            tmpfs,
        )

    def main(self):
        sub_command_help = dedent("""
        Prepare an OSD by assigning an ID and FSID, registering them with the
        cluster with an ID and FSID, formatting the volume.

        Once the OSD is ready, an ad-hoc systemd unit will be enabled so that
        it can later get activated and the OSD daemon can get started.

            ceph-volume raw prepare --bluestore --data {device}

        DB and WAL devices are supported.

            ceph-volume raw prepare --bluestore --data {device} --block.db {device} --block.wal {device}

        """)
        parser = create_parser(
            prog='ceph-volume raw prepare',
            description=sub_command_help,
        )
        if not self.argv:
            print(sub_command_help)
            return
        self.args = parser.parse_args(self.argv)
        if not self.args.bluestore:
            terminal.error('must specify --bluestore (currently the only supported backend)')
            raise SystemExit(1)
        if self.args.dmcrypt and not os.getenv('CEPH_VOLUME_DMCRYPT_SECRET'):
            terminal.error('encryption was requested (--dmcrypt) but environment variable ' \
                           'CEPH_VOLUME_DMCRYPT_SECRET is not set, you must set ' \
                           'this variable to provide a dmcrypt secret.')
            raise SystemExit(1)

        self.safe_prepare(self.args)
