#!/usr/bin/python

import argparse
import logging
import os
import os.path
import subprocess
import sys
import tempfile
import uuid


log_name = __name__
if log_name == '__main__':
    log_name = os.path.basename(sys.argv[0])
log = logging.getLogger(log_name)


class PrepareError(Exception):
    """
    OSD preparation error
    """

    def __str__(self):
        doc = self.__doc__.strip()
        return ': '.join([doc] + [str(a) for a in self.args])


class MountError(PrepareError):
    """
    Mounting filesystem failed
    """


class UnmountError(PrepareError):
    """
    Unmounting filesystem failed
    """


def write_one_line(parent, name, text):
    """
    Write a file whose sole contents are a single line.

    Adds a newline.
    """
    path = os.path.join(parent, name)
    tmp = '{path}.{pid}.tmp'.format(path=path, pid=os.getpid())
    with file(tmp, 'wb') as f:
        f.write(text + '\n')
        os.fsync(f.fileno())
    os.rename(tmp, path)


CEPH_OSD_ONDISK_MAGIC = 'ceph osd volume v026'


def get_fsid(cluster):
    try:
        p = subprocess.Popen(
            args=[
                'ceph-conf',
                '--cluster={cluster}'.format(
                    cluster=cluster,
                    ),
                '--name=osd.',
                '--lookup',
                'fsid',
                ],
            stdout=subprocess.PIPE,
            close_fds=True,
            )
    except OSError as e:
        raise PrepareError('error executing ceph-conf', e)
    (out, _err) = p.communicate()
    ret = p.wait()
    if ret != 0:
        raise PrepareError('getting cluster uuid from configuration failed')
    fsid = out.split('\n', 1)[0]
    if not fsid:
        return None
    return fsid


MOUNT_OPTIONS = dict(
    ext4='user_xattr',
    )


def mount(
    dev,
    fstype,
    ):
    # pick best-of-breed mount options based on fs type
    options = MOUNT_OPTIONS.get(fstype, '')

    # mount
    path = tempfile.mkdtemp(
        prefix='mnt.',
        dir='/var/lib/ceph/tmp',
        )
    try:
        subprocess.check_call(
            args=[
                'mount',
                '-o', options,
                '--',
                dev,
                path,
                ],
            )
    except subprocess.CalledProcessError as e:
        try:
            os.rmdir(path)
        except (OSError, IOError):
            pass
        raise MountError(e)

    return path


def unmount(
    path,
    ):
    try:
        subprocess.check_call(
            args=[
                'umount',
                '--',
                path,
                ],
            )
    except subprocess.CalledProcessError as e:
        raise UnmountError(e)

    os.rmdir(path)


def prepare(
    disk,
    cluster_uuid,
    ):
    """
    Prepare a disk to be used as an OSD data disk.

    The ``magic`` file is written last, so it's presence is a reliable
    indicator of the whole sequence having completed.

    WARNING: This will unconditionally overwrite anything given to
    it.
    """
    try:
        subprocess.check_call(
            args=[
                'sgdisk',
                '--zap-all',
                '--clear',
                '--mbrtogpt',
                '--largest-new=1',
                '--change-name=1:ceph data',
                '--typecode=1:4fbd7e29-9d25-41b8-afd0-062c0ceff05d',
                '--',
                disk,
                ],
            )
    except subprocess.CalledProcessError as e:
        raise PrepareError(e)

    # TODO make fstype configurable; both ceph.conf and command line
    fstype = 'ext4'
    dev = '{disk}1'.format(disk=disk)
    try:
        subprocess.check_call(
            args=[
                'mkfs',
                '--type={fstype}'.format(fstype=fstype),
                '--',
                dev,
                ],
            )
    except subprocess.CalledProcessError as e:
        raise PrepareError(e)

    path = mount(dev=dev, fstype=fstype)
    try:
        write_one_line(path, 'ceph_fsid', cluster_uuid)
        osd_uuid = str(uuid.uuid4())
        write_one_line(path, 'fsid', osd_uuid)
        write_one_line(path, 'magic', CEPH_OSD_ONDISK_MAGIC)
    finally:
        unmount(path)


def parse_args():
    parser = argparse.ArgumentParser(
        description='Prepare a disk for a Ceph OSD',
        )
    parser.add_argument(
        '-v', '--verbose',
        action='store_true', default=None,
        help='be more verbose',
        )
    parser.add_argument(
        '--cluster',
        metavar='NAME',
        help='cluster name to assign this disk to',
        )
    parser.add_argument(
        '--cluster-uuid',
        metavar='UUID',
        help='cluster uuid to assign this disk to',
        )
    parser.add_argument(
        'disk',
        metavar='DISK',
        help='path to OSD data disk block device',
        )
    parser.set_defaults(
        # we want to hold on to this, for later
        prog=parser.prog,
        cluster='ceph',
        )
    args = parser.parse_args()
    return args


def main():
    args = parse_args()

    loglevel = logging.INFO
    if args.verbose:
        loglevel = logging.DEBUG

    logging.basicConfig(
        level=loglevel,
        )

    try:
        if args.cluster_uuid is None:
            args.cluster_uuid = get_fsid(cluster=args.cluster)
            if args.cluster_uuid is None:
                raise PrepareError(
                    'must have fsid in config or pass --cluster--uuid=',
                    )
        prepare(
            disk=args.disk,
            cluster_uuid=args.cluster_uuid,
            )
    except PrepareError as e:
        print >>sys.stderr, '{prog}: {msg}'.format(
            prog=args.prog,
            msg=e,
            )
        sys.exit(1)

if __name__ == '__main__':
    main()
