

from __future__ import absolute_import
from __future__ import division

import collections
import errno
import logging
import os
import time

from vdsm.common import constants
from vdsm.common import properties
from vdsm.common import supervdsm
from vdsm.common import systemctl
from vdsm.common import systemd
from vdsm.common.time import monotonic_time

from . import constants as sc
from . import exception as se
from . import fileUtils
from . sdc import sdCache

QEMU_NBD = "/usr/bin/qemu-nbd"
RUN_DIR = os.path.join(constants.P_VDSM_RUN, "nbd")

log = logging.getLogger("storage.nbd")


class Error(Exception):
    """ Base class for nbd errors """


class Timeout(Error):
    """ Timeout starting nbd server """


class InvalidPath(Error):
    """ Path is not a valid volume path """


class ServerConfig(properties.Owner):

    sd_id = properties.UUID(required=True)
    img_id = properties.UUID(required=True)
    vol_id = properties.UUID(required=True)
    readonly = properties.Boolean(default=False)
    discard = properties.Boolean(default=False)

    def __init__(self, config):
        self.sd_id = config.get("sd_id")
        self.img_id = config.get("img_id")
        self.vol_id = config.get("vol_id")
        self.readonly = config.get("readonly")
        self.discard = config.get("discard")


QemuNBDConfig = collections.namedtuple(
    "QemuNBDConfig", "format,readonly,discard,path")


def start_server(server_id, config):
    cfg = ServerConfig(config)
    dom = sdCache.produce_manifest(cfg.sd_id)
    vol = dom.produceVolume(cfg.img_id, cfg.vol_id)

    if vol.isShared() and not cfg.readonly:
        raise se.SharedVolumeNonWritable(vol)

    _create_rundir()

    sock = _socket_path(server_id)

    log.info("Starting transient service %s, serving volume %s/%s via unix "
             "socket %s",
             _service_name(server_id), cfg.sd_id, cfg.vol_id, sock)

    qemu_nbd_config = QemuNBDConfig(
        format=sc.fmt2str(vol.getFormat()),
        readonly=cfg.readonly,
        discard=cfg.discard,
        path=vol.getVolumePath())

    start_transient_service(server_id, qemu_nbd_config)

    if not _wait_for_socket(sock, 1.0):
        raise Timeout("Timeout starting NBD server {}: {}"
                      .format(server_id, config))

    return "nbd:unix:" + sock


def stop_server(server_id):
    service = _service_name(server_id)


    info = systemctl.show(service, properties=("LoadState",))
    if info and info[0]["LoadState"] == "not-found":
        log.info("Transient service %s is not running", service)
        return

    log.info("Stopping transient service %s", service)
    systemctl.stop(service)


def start_transient_service(server_id, config):
    if os.geteuid() != 0:
        return supervdsm.getProxy().nbd_start_transient_service(
            server_id, config)

    _verify_path(config.path)

    cmd = [
        QEMU_NBD,
        "--socket", _socket_path(server_id),
        "--format", config.format,
        "--persistent",

        "--export-name=",

        "--cache=none",
        "--aio=native",
    ]

    if config.readonly:
        cmd.append("--read-only")
    elif config.discard:
        cmd.append("--discard=unmap")

    cmd.append(config.path)

    systemd.run(
        cmd,
        unit=_service_name(server_id),
        uid=fileUtils.resolveUid(constants.VDSM_USER),
        gid=fileUtils.resolveGid(constants.VDSM_GROUP))


def _verify_path(path):
    norm_path = os.path.normpath(path)

    if not norm_path.startswith(sc.REPO_MOUNT_DIR):
        raise InvalidPath(
            "Path {!r} is outside storage repository {!r}"
            .format(path, sc.REPO_MOUNT_DIR))


def _service_name(server_id):
    return "vdsm-nbd-{}.service".format(server_id)


def _socket_path(server_id):
    return os.path.join(RUN_DIR, server_id + ".sock")


def _wait_for_socket(sock, timeout):
    start = monotonic_time()
    elapsed = 0.0

    while elapsed < timeout:
        if os.path.exists(sock):
            log.debug("Waited for socket %.3f seconds", elapsed)
            return True
        time.sleep(0.02)
        elapsed = monotonic_time() - start

    return False


def _create_rundir():
    try:
        os.mkdir(RUN_DIR)
    except OSError as e:
        if e.errno != errno.EEXIST:
            raise
    else:
        log.info("Created %s", RUN_DIR)
