#!/usr/env/python
# author    guojy8993@163.com
# date      2017/02/04
# version   0.1
# desc      docker volume plugin

# Please visit the follwing website to know more about
# volume plugin protocol between docker daemon and plugins.
# https://docs.docker.com/engine/extend/plugins_volume/

from wsgiref.simple_server import make_server
from oslo.config import cfg
from subprocess import Popen,PIPE
import copy
import json
import os
import shutil

opts = [
    cfg.StrOpt('ceph_pool',
               default='volumes',
               help='Ceph pool in which volumes are created'),
    cfg.StrOpt('auth_user',
               default='cinder',
               help='Ceph user that can RWX ${ceph_pool}'),
    cfg.IntOpt('default_volume_size',
               default=1,
               help='Default volume size, unit.GB'),
    cfg.IntOpt('plugin_url_port',
               default=8080,
               help='Port that volume plugin listens to'),
    cfg.StrOpt('plugin_url_ip',
               default='0.0.0.0',
               help='Ip address that volume plugin daemon locates at'),
    cfg.StrOpt('local_dir',
               default='/data/docker-volumes',
               help='Local folder in which mountpoints files are created'),
    cfg.StrOpt('default_file_system',
               default='xfs',
               help='File system used to format rbd'),
    cfg.StrOpt('volume_metadata',
               default='/var/lib/doceph/metadata',
               help='Local folder used to store volume metadata'),
    cfg.StrOpt('daemon_log',
               default='/var/log/doceph.log',
               help='Daemon log'),
    ]

CONF = cfg.CONF
CONF.register_opts(opts)

PATH_INFO_MAPPING_HANDLERS = {
    "Plugin.Activate": "activate_plugin",
    "VolumeDriver.Create": "create_volume",
    "VolumeDriver.Remove": "remove_volume",
    "VolumeDriver.Mount": "mount",
    "VolumeDriver.Path": "volume_local_path",
    "VolumeDriver.Unmount": "umount",
    "VolumeDriver.Get": "volume_details",
    "VolumeDriver.List": "volumes"
}

def util_log(msg):
    with open(CONF.daemon_log, "a") as f:
        f.write("%s\n" % msg if msg else "") 

def util_exec(cmd):
    pipe = Popen(cmd,
                 shell=True,
                 stdout=PIPE,
                 stderr=PIPE)
    return pipe.stdout.read(), pipe.stderr.read()

def mount_point_busy(mp):
    out, err = util_exec("df")
    return mp in out

def activate_plugin(**request):
    result = {
        "Implements": ["VolumeDriver"]
    }
    return json.dumps(result)

def create_volume(**request):

    name = request.get('Name')
    opts = request.get('Opts', {})

    files = [CONF.local_dir, name]
    target = os.path.join(*files)
    if not os.path.exists(target):
        os.makedirs(target)
    elif mount_point_busy(target):
        return json.dumps({"Err":"Volume exists and device busy !"})

    pool = opts.get("pool", CONF.ceph_pool)
    fs = opts.get("fs", CONF.default_file_system)
    size = opts.get("size", CONF.default_volume_size)
    user = opts.get("auth_user", CONF.auth_user)

    prepare_volume = "/usr/bin/rbdmap2local %s \
        %s %s %s %s %s" % (pool, name, size*1024, user, fs, target)
    out, err = util_exec(prepare_volume)

    util_log(err)

    if len(err.strip()) > 0:
        return json.dumps({"Err":"Fail to create volume: %s" % err})
    
    content = { "Mountpoint": target }
    content["Name"] = name
    content["Opts"] = opts
    meta_files = [CONF.volume_metadata, name]
    target = os.path.join(*meta_files)
   
    if not os.path.exists(target):
        with open(target, 'a') as writer:
            writer.write("%s" % json.dumps(content))
   
    # Lzying creating actual volume
    result = { "Err": "" }
    return json.dumps(result)

def remove_volume(**request):

    name = request.get('Name')
    files = [CONF.local_dir, name]
    target = os.path.join(*files)
    if os.path.exists(target):
        if mount_point_busy(target):
            meta_files = [CONF.volume_metadata, name]
            meta_file = os.path.join(*meta_files)
            meta = {}
            with open(meta_file) as f:
                meta = json.loads(f.read())

            revoke_rbdmap = "/usr/bin/rbdmap_revoke %s %s %s\
                                %s %s" % (meta["Opts"]["pool"],
                                          meta["Name"],
                                          meta["Opts"]["auth_user"],
                                          meta["Opts"]["fs"],
                                          meta["Mountpoint"])
            out, err = util_exec(revoke_rbdmap)
            os.remove(meta_file)
    
    result = { "Err": "" }
    return json.dumps(result)

def mount(**request):
    
    name = request.get('Name')
    files = [CONF.local_dir, name]
    target = os.path.join(*files)

    result = { "Err": "" }
    result["Mountpoint"] = target 
    return json.dumps(result) 

def volume_local_path(**request):

    name = request.get('Name')
    files = [CONF.local_dir, name]
    target = os.path.join(*files)
    
    result = { "Err": "" }
    result["Mountpoint"] = target
    return json.dumps(result)    

def umount(**request):
    name = request.get('Name')
    container_id = request.get('ID')
    files = [CONF.local_dir, name]
    target = os.path.join(*files)
    
    result = { "Err": "" }
    return json.dumps(result)

def volume_details(**request):
    name = request.get('Name')
    files = [CONF.volume_metadata, name]
    target = os.path.join(*files)

    result = {}   
    if not os.path.exists(target):
        result["Volume"] = {}
        result["Err"] = "Volume %s not found" % name

    else:
        with open(target) as reader:
            result["Volume"] = json.loads(reader.read())
        result["Err"] = ""
    return json.dumps(result)

def volumes(**request):
    managed_volumes = []
    if not os.path.exists(CONF.volume_metadata):
        result = {
            "Volumes": managed_volumes,
            "Err": ""
        }
        return json.dumps(result)
    
    volume_metadatas_iter = os.walk(CONF.volume_metadata)
    for volume_meta in volume_metadatas_iter.next()[-1]:
        target = os.path.join(*[CONF.volume_metadata, volume_meta])
        with open(target) as reader:
            managed_volumes.append(json.loads(reader.read()))
    
    result = {
            "Volumes": managed_volumes,
            "Err": ""
    }
    return json.dumps(result)

def application(environ, start_response):
     
    headers = [('Content-type', 'application/json')]
    status = '200 OK'
    start_response(status, headers)
    
    http_method = environ.get('REQUEST_METHOD')
    if "post" != http_method.lower():
        error = "Only POST Request Allowed"
        return [ error ]

    path = environ.get('PATH_INFO', '')
    if "/" not in path:
        error = "What can i do for you ?"
        return [ error ]

    _, stipulated_function = path.rsplit("/", 1)
    if stipulated_function.strip() not in PATH_INFO_MAPPING_HANDLERS:
        error = "Function %s not implemented yet" % stipulated_function
        return [ error ]
    handler = eval(PATH_INFO_MAPPING_HANDLERS.get(stipulated_function))

    request_body_size = int(environ.get('CONTENT_LENGTH', 0))
    if request_body_size > 0:
        request_body = environ['wsgi.input'].read(request_body_size)
        request_body = json.loads(request_body)
    else:
        request_body = {}

    result = handler(**request_body)
    return [ result ]

if __name__ == "__main__":
    httpd = make_server('', CONF.plugin_url_port, application)
    httpd.serve_forever()
