#!/usr/bin/python
#pylint: disable-msg=C0301
"""
Purpose: Manager of custom packages for Blik CloudManager
Created: 31.12.2011
Author:  Konstantin Andrusenko
"""
import os
import sys
import shutil
import tarfile
import tempfile
from optparse import OptionParser

import yaml


SPEC_DIR = '/opt/blik/develop/spec/'
NODES_TYPES = ['mgmt', 'node']
NODE_TYPE_DIRS = {'mgmt': ['clusters_spec', 'nodes_spec', 'operations_spec', 'dictionaries'
                            'nodes_manager_plugins', 'console_ext', 'console_ext'],
                 'node': ['node_agent_plugins']}

CLUSTER_SPEC_STRUCT = '''
cluster_info:
    type: dict
    structure:
        type_name:
            type: str
        description:
            type: str
parameters:
    type: list
    structure:
        name:
            type: str
        type:
            type: str
        posible_params:
            type: list
            optional: True
        default:
            type: str
            optional: True
'''

NODES_SPEC_STRUCT = '''
node_info:
    type: dict
    structure:
        type_name:
            type: str
        description:
            type: str
        rootfs_mode:
            type: str
            posible_values: [memorized, networked]
        packages:
            type: list
        supported_archs:
            type: list
            posible_values: [x86, x86_64]
kernels:
    type: dict
    optional: True
    structure:
        kernel_url:
            type: str
        arch:
            type: str
            posible_values: [x86, x86_64]
parameters:
    type: list
    structure:
        name:
            type: str
        type:
            type: str
        posible_params:
            type: list
            optional: True
        default:
            type: str
            optional: True
'''

OPERATIONS_SPEC_STRUCT = '''
operations:
    type: list
    structure:
        sid:
            type: str
        label:
            type: str
        node_type:
            type: str
            optional: True
        timeout:
            type: int
            optional: True
        description:
            type: str
'''

DICTIONARIES_STRUCT = '''
dictionaries:
    type: list
    structure:
        table_name:
            type: str
        dict_fields:
            type: list
        unique_field:
            type: str
        data:
            type: list
            structure:
                row:
                    type: list
'''

class OptionError(Exception):
    '''Exception class for invalid command line parameters'''
    pass

def new_package(package_name, path='.'):
    '''Creates new package development structure'''
    print('Creating project "%s" at %s...'%(package_name, path))
    package_dir = os.path.join(path, package_name)
    if os.path.exists(package_dir):
        raise Exception('Directory %s already exists!'%package_dir)

    os.makedirs(package_dir)

    subdirs = ['clusters_spec', 'nodes_spec', 'operations_spec',
            'nodes_manager_plugins', 'node_agent_plugins', 'dictionaries',
            'console_ext/menu', 'console_ext/applications', 'tests', 'spec']

    for subdir in subdirs:
        os.makedirs(os.path.join(package_dir, subdir))

    for f_name in os.listdir(SPEC_DIR):
        path = os.path.join(SPEC_DIR, f_name)

        dst =  os.path.join(package_dir, 'spec/')
        ret = os.system('cp -r %s %s'%(path, dst))
        if ret:
            raise Exception('Copying failed!')

    print ('Project is created successfull!')

#---------------------------------------------------------------------------

def _check_element_type(element, type_name):
    '''checks element type
        Returns True if element has valid type
    '''
    if element.__class__.__name__ == type_name:
        return True

    if type_name == 'int':
        try:
            int(element)
            return True
        except ValueError:
            return False
    elif type_name == 'str':
        try:
            str(element)
            return True
        except ValueError:
            return False
    return False

def validate_spec(spec_name, spec_obj, structure):
    '''Validate yaml config structure'''
    for key in spec_obj:
        if key not in structure:
            raise Exception('Key "%s" is not supported'%key)

        e_type = structure[key]['type']
        is_matched = _check_element_type(spec_obj[key], e_type)
        if not is_matched:
            raise Exception('Value of "%s" key should has "%s" type. But: "%s"'%(key, e_type, spec_obj[key]))

        posible_values = structure[key].get('posible_values', None)
        if posible_values:
            if e_type == 'list':
                obj_list = spec_obj[key]
            else:
                obj_list = [spec_obj[key]]

            for item in obj_list:
                if item not in posible_values:
                    raise Exception('Value "%s" is not supported! Expected values: %s'%(item, posible_values))

        if structure[key].get('structure', None):
            if e_type == 'list':
                objects_list = spec_obj[key]
            else:
                objects_list = [spec_obj[key]]

            for item in objects_list:
                validate_spec(spec_name, item, structure[key]['structure'])

    for key, value in structure.items():
        if value.get('optional', False):
            continue
        if key not in spec_obj:
            raise Exception('Key "%s" is not found in specification %s'%(key, spec_name))


def __check_spec(path, yaml_structure):
    '''Validate all YAML files at @path directory'''
    for f_name in os.listdir(path):
        if not f_name.endswith('.yaml'):
            continue

        f_path = os.path.join(path, f_name)
        file_obj = None
        try:
            file_obj = open(f_path)
            yaml_obj = yaml.load(file_obj.read())
        except Exception, err:
            raise Exception('File %s has invalid format! Details: %s'%(f_path, err))
        finally:
            if file_obj:
                file_obj.close()

        print('Validating %s specification structure...'%f_path)
        validate_spec(f_name, yaml_obj, yaml.load(yaml_structure))
        return yaml_obj

def __validate_operations_plugins(path, plugin_dir):
    '''Static validation of operations plugins directories'''
    nmp_path = os.path.join(path, plugin_dir)
    if not os.path.exists(nmp_path):
        raise Exception('Directory %s is not found!'%nmp_path)

    items = os.listdir(nmp_path)
    for item in items:
        path = os.path.join(nmp_path, item)
        if os.path.isfile(path):
            raise Exception('%s contain file %s. This directory should contain subdirectories only!'%(plugin_dir, item))

        init_file = os.path.join(path, '__init__.py')
        if not os.path.exists(init_file):
            raise Exception('Plugin at %s is incorrent! It should contain __init__.py file'% path)

        init_content = open(init_file).read()
        if init_content.find('OPERATIONS_PLUGINS') < 0:
            raise Exception('OPERATIONS_PLUGINS variable should be defined in %s'%init_file)

def __validate_console_applications(app_path):
    '''Validate cloud manager console applications'''
    for item in os.listdir(app_path):
        path = os.path.join(app_path, item)
        if os.path.isdir(path):
            if not os.path.exists(os.path.join(path, '__init__.py')):
                raise Exception('Console application "%s" should contain __init__.py file!'%item)
        else:
            raise Exception('console_ext/applications contain file %s. This directory should contain directories only!'%item)

def test_package(package_path):
    '''Testing package at path @package_path'''
    package_path = os.path.abspath(package_path)
    print('Start testing process of project at %s'%package_path)

    __check_spec(os.path.join(package_path, 'clusters_spec'), CLUSTER_SPEC_STRUCT)
    __check_spec(os.path.join(package_path, 'nodes_spec'), NODES_SPEC_STRUCT)
    __check_spec(os.path.join(package_path, 'operations_spec'), OPERATIONS_SPEC_STRUCT)
    __check_spec(os.path.join(package_path, 'dictionaries'), DICTIONARIES_STRUCT)

    __validate_operations_plugins(package_path, 'nodes_manager_plugins')
    __validate_operations_plugins(package_path, 'node_agent_plugins')

    __validate_console_applications(os.path.join(package_path, 'console_ext/applications'))

    tests_dir = os.path.join(package_path, 'tests')
    for f_name in os.listdir(tests_dir):
        ret = os.system(os.path.join(tests_dir, f_name))
        if ret:
            raise Exception('===== Tests failed! =====')
    print('Project is tested successful!')

#---------------------------------------------------------------------------

def build_package(package_path, version):
    '''Build package at path @package_path with version @version'''
    #testing package before build
    test_package(package_path)

    package_path = os.path.abspath(package_path)
    print ('Building packages with versiion %s for project at %s'%(version, package_path))

    if not os.path.exists( os.path.join(package_path, 'spec') ):
        raise Exception('Project directory is invalid!')

    build_dir = os.path.join(package_path, 'dist')
    if not os.path.exists(build_dir):
        os.mkdir(build_dir)

    package_name = os.path.basename(os.path.normpath(package_path))
    for node_type in NODES_TYPES:
        out_file = '%s-%s-%s.tar'% (package_name, node_type, version)
        out_file = os.path.join(build_dir, out_file)

        tar = tarfile.TarFile(out_file,'w')

        cur_dir = os.path.abspath('.')
        os.chdir(package_path)
        content = os.listdir('.')
        for item in content:
            if item in NODE_TYPE_DIRS[node_type]:
                tar.add(item)

        tar.close()
        os.chdir(cur_dir)
        print('Build saved at %s'%out_file)
    print('Project is builded successfull!')

#---------------------------------------------------------------------------

def distribute_ebuilds(pkg_name, version, distribution_server):
    '''Distribute Gentoo ebuilds to distribution server'''
    tmp_path = tempfile.mkdtemp(suffix='blikdevel')

    remote_path = '%s:/opt/blik/portage/blik-products/'% distribution_server

    for node_type in NODES_TYPES:
        ebuild = 'spec/%s/gentoo_dist/package.ebuild'% node_type
        metadata = 'spec/%s/gentoo_dist/metadata.xml'% node_type
        changelog = 'spec/%s/gentoo_dist/ChangeLog'% node_type

        out_path = os.path.join(tmp_path, '%s-%s'%(pkg_name, node_type))
        os.mkdir(out_path)

        ret = os.system('cp %s %s/%s-%s-%s.ebuild' % (ebuild, out_path, pkg_name, node_type, version))
        if ret:
            raise Exception('Copying ebuilds to temporary directory failed')

        if os.path.exists(metadata):
            os.system('cp %s %s/metadata.xml'%(metadata, out_path))

        if os.path.exists(changelog):
            os.system('cp %s %s/ChangeLog'%(changelog, out_path))

    os.chdir(tmp_path)
    print('Copying portage ebuilds files to %s...'%(remote_path))
    for node_type in NODES_TYPES:
        ret = os.system('scp -r %s-%s %s' % (pkg_name, node_type, remote_path))
        if ret:
            raise Exception('Ebuilds distribution for package %s is failed!'%pkg_name)
    os.chdir('/tmp')
    shutil.rmtree(tmp_path)


def distribute_package(package_path, version, distribution_server):
    '''Distribure package with version @version to distribution server'''
    print('Distributing packages with version %s to %s'%(version, distribution_server))
    package_path = os.path.abspath(package_path)
    os.chdir(package_path)
    package_name = os.path.basename(os.path.normpath(package_path))
    remote_path = '%s:/usr/portage/distfiles' % distribution_server

    distribute_ebuilds(package_name, version, distribution_server)

    for node_type in NODES_TYPES:
        tar_file = '%s-%s-%s.tar'% (package_name, node_type, version)
        local_path = os.path.join(package_path, 'dist', tar_file)
        if not os.path.exists(local_path):
            raise Exception('Build is not found at %s'%local_path)

        print('Copying %s to %s over ssh'%(local_path, remote_path))
        ret = os.system('scp %s %s'%(local_path, remote_path))
        if ret:
            raise Exception('Distribution package %s failed!'%package_name)

        mirror_updater = '/opt/blik/sbin/portage-mirror-update'
        ebuild_name = '%s-%s'%(package_name, node_type)
        ret = os.system('ssh %s %s /usr/portage/blik-products/%s/%s-%s.ebuild'%(distribution_server, \
                                                mirror_updater, ebuild_name, ebuild_name, version))
        if ret:
            raise Exception('Distribution server is not updated on distribution server')

    print('Package %s is distributed successfully!'%package_name)

#----------------------------------------------------------------------------------------------------------------

def get_usage():
    '''Returns usage information'''
    s_usage = 'Usage: cm-package <options>\n'
    s_usage += 'Options:\n'
    s_usage += '*  create [--path|-p <home dir path>] --name|-n <package name>\n'
    s_usage += '      creates new project with name <package> name in <home dir path> directory or in current directory\n'
    s_usage += '*  test [--path|-p <package path>]\n'
    s_usage += '      test project at path <package path> or at current directory\n'
    s_usage += '*  build [--path|-p <package path>] --ver|-v <version>\n'
    s_usage += '      build project at path <package path> or at current directory with version <version>\n'
    s_usage += '*  distribute [--path|-p <package path>] --ver|-v <version> --dist-server|-d <dist server>\n'
    s_usage += '      distribute project at path <package path> or at current directory with version <version> to server <dist server>\n'
    s_usage += '      <dist server> should has format: <login>@<server hostname>\n'

    return s_usage


def main():
    '''Main routine'''
    if (len(sys.argv) < 2) or ('--help' in sys.argv) or ('-h' in sys.argv):
        print(get_usage())
        return 1

    parser = OptionParser(get_usage(), version="%prog 1.0")
    parser.add_option("-p", "--path", dest="path", default='.')
    parser.add_option("-n", "--name", dest="p_name")
    parser.add_option("-v", "--ver", dest="version")
    parser.add_option("-d", "--dist-server", dest="dist_server")

    try:
        options = parser.parse_args(sys.argv[2:])[0]

        command = sys.argv[1]
        if command == 'create':
            if options.p_name is None:
                raise OptionError('Option --name is not found!')

            new_package(options.p_name, options.path)
        elif command == 'test':
            test_package(options.path)
        elif command == 'build':
            if options.version is None:
                raise OptionError('Option --ver is not found!')

            build_package(options.path, options.version)
        elif command == 'distribute':
            if options.version is None:
                raise OptionError('Option --ver is not found!')
            if options.dist_server is None:
                raise OptionError('Option --dist-server is not found!')

            distribute_package(options.path, options.version, options.dist_server)
        else:
            raise OptionError('Command "%s" is not supported'%command)
    except OptionError, err:
        sys.stderr.write('%s\nPlease, run "cm-package --help" for printing usage\n'%err)
        return 1
    except Exception, err:
        sys.stderr.write('ERROR: %s\n'%err)
        return 2

    return 0


if __name__ == '__main__':
    sys.exit(main())

