Пример #1
0
class HTTPRequestDecorators(HTTPRequestFlaskDecorators,
                            HTTPRequestGenericDecorators):
    """
    Class with decorator functionality for HTTP requests
    """
    app = app
    logger = Logger('flask')
    version = 3

    def __init__(self):
        """
        Dummy init method
        """

    @classmethod
    def authorized(cls):
        """
        Indicates whether a call is authenticated
        """
        # For backwards compatibility we first try to retrieve the node ID by using the bootstrap file
        try:
            with open(BOOTSTRAP_FILE) as bstr_file:
                node_id = json.load(bstr_file)['node_id']
        except:
            node_id = SettingList.get_setting_by_code(code='node_id').value

        node_config = Configuration.get(
            ASD_NODE_CONFIG_MAIN_LOCATION.format(node_id))
        username = node_config['username']
        password = node_config['password']
        auth = request.authorization
        return auth and auth.username == username and auth.password == password
Пример #2
0
    def add_maintenance_service(name,
                                alba_backend_guid,
                                abm_name,
                                read_preferences=None):
        """
        Add a maintenance service with a specific name
        :param name: Name of the maintenance service to add
        :type name: str
        :param alba_backend_guid: ALBA Backend GUID for which the maintenance service needs to run
        :type alba_backend_guid: str
        :param abm_name: Name of the ABM cluster
        :type abm_name: str
        :param read_preferences: List of ALBA Node IDs (LOCAL) or ALBA IDs of linked ALBA Backends (GLOBAL) for the maintenance services where they should prioritize the READ actions
        :type read_preferences: list[str]
        :return: None
        :rtype: NoneType
        """
        if MaintenanceController._service_manager.has_service(
                name, MaintenanceController._local_client) is False:
            alba_pkg_name, alba_version_cmd = PackageFactory.get_package_and_version_cmd_for(
                component=PackageFactory.COMP_ALBA)
            config_location = '{0}/config'.format(
                MaintenanceController.MAINTENANCE_KEY.format(
                    alba_backend_guid, name))
            params = {
                'LOG_SINK':
                Logger.get_sink_path('alba_maintenance'),
                'ALBA_CONFIG':
                Configuration.get_configuration_path(config_location),
                'ALBA_PKG_NAME':
                alba_pkg_name,
                'ALBA_VERSION_CMD':
                alba_version_cmd
            }
            Configuration.set(
                key=config_location,
                value={
                    'log_level':
                    'info',
                    'albamgr_cfg_url':
                    Configuration.get_configuration_path(
                        '/ovs/arakoon/{0}/config'.format(abm_name)),
                    'read_preference':
                    [] if read_preferences is None else read_preferences,
                    'multicast_discover_osds':
                    False
                })

            MaintenanceController._service_manager.add_service(
                name=MaintenanceController.MAINTENANCE_PREFIX,
                client=MaintenanceController._local_client,
                params=params,
                target_name=name)
        MaintenanceController._service_manager.start_service(
            name, MaintenanceController._local_client)
Пример #3
0
class PostUpdate(object):

    logger = Logger('update')
    service_manager = ServiceFactory.get_manager()

    CURRENT_VERSION = 7

    @staticmethod
    def ensure_directory(file_path):
        """
        Ensures the directory for a given file
        :param file_path: Path to the file
        :return: None
        :rtype: NoneType
        """
        directory = os.path.dirname(file_path)
        try:
            # Safer to capture the exception than to check if the directory exists (which can have race condition problems).
            os.makedirs(directory)
        except OSError as e:
            if e.errno != errno.EEXIST:
                raise

    @classmethod
    def update(cls):
        """
        Perform an update
        :return: None
        :rtype: NoneType
        """
        try:
            cls.migrate_arakoon_cacc()
            cls.migrate()
        except Exception:
            log_path = cls.logger.get_sink_path('update',
                                                forced_target_type='file')
            msg = 'Exception while updating the package. Please check the logging within {0}'.format(
                log_path)
            print msg  # Print to stdout
            cls.logger.exception(msg)
            exit(1)

    @classmethod
    def migrate_arakoon_cacc(cls):
        # type: () -> None
        """
        Migrate the Arakoon config file. It was moved away from /opt/asd-manager in order to unify all paths
        This is done during the post install as updating the code will point the new configuration to /opt/OpenvStorage/
        If the setup did not run, there is no problem
        :return: None
        :rtype: NoneType
        :raises: AssertionException if the file under /opt/OpenvStorage would not match the one under /opt/asd-manager
        This means that the asd-manager was added to another cluster different to the one that the framework/other components are on
        A machine can only be part of one cluster now.
        """
        if CACC_LOCATION_OLD == CACC_LOCATION:
            # Manual intervention changed the constants
            return
        if os.path.exists(CACC_LOCATION_OLD):
            if os.path.exists(CACC_LOCATION):
                if filecmp.cmp(CACC_LOCATION_OLD, CACC_LOCATION):
                    os.remove(CACC_LOCATION_OLD)
                else:
                    raise AssertionError(
                        'File {0} and {1} are not identical.'
                        'The ASD-manager belongs to a different cluster than the other component on this host.'
                        'This ASD-Manager won\'t be able to resolve Arakoon URL generated by any other component'
                        'If you wish to continue using the file in this location, '
                        'update the /opt/asd-manager/constants/asd.py and change `CACC_LOCATION=CACC_LOCATION_OLD`'
                        'and run '.format(CACC_LOCATION_OLD, CACC_LOCATION))
            else:
                cls.ensure_directory(CACC_LOCATION)
                shutil.copyfile(CACC_LOCATION_OLD, CACC_LOCATION)
                os.remove(CACC_LOCATION_OLD)

    @classmethod
    def migrate(cls):
        # type: () -> None
        """
        Execute the migration logic.
        :return: None
        :rtype: NoneType
        """
        with file_mutex('package_update_pu'):
            local_client = SSHClient(endpoint='127.0.0.1', username='******')

            # Override the created openvstorage_sdm_id during package install, with currently available SDM ID
            if local_client.file_exists(BOOTSTRAP_FILE):
                with open(BOOTSTRAP_FILE) as bstr_file:
                    node_id = json.load(bstr_file)['node_id']
                local_client.file_write(filename='/etc/openvstorage_sdm_id',
                                        contents=node_id + '\n')
            else:
                with open('/etc/openvstorage_sdm_id', 'r') as id_file:
                    node_id = id_file.read().strip()

            key = '{0}/versions'.format(
                ASD_NODE_CONFIG_LOCATION.format(node_id))
            version = Configuration.get(key) if Configuration.exists(
                key) else 0

            asd_manager_service_name = 'asd-manager'
            if cls.service_manager.has_service(
                    asd_manager_service_name,
                    local_client) and cls.service_manager.get_service_status(
                        asd_manager_service_name, local_client) == 'active':
                cls.logger.info('Stopping asd-manager service')
                cls.service_manager.stop_service(asd_manager_service_name,
                                                 local_client)

            # @TODO: Move these migrations to alba_node.client.update_execute_migration_code()
            if version < cls.CURRENT_VERSION:
                try:
                    # DB migrations
                    from source.controllers.asd import ASDController
                    from source.controllers.disk import DiskController
                    from source.dal.asdbase import ASDBase
                    from source.dal.lists.asdlist import ASDList
                    from source.dal.lists.disklist import DiskList
                    from source.dal.objects.asd import ASD

                    if not local_client.file_exists('{0}/main.db'.format(
                            ASDBase.DATABASE_FOLDER)):
                        local_client.dir_create([ASDBase.DATABASE_FOLDER])

                    asd_map = dict(
                        (asd.asd_id, asd) for asd in ASDList.get_asds())
                    DiskController.sync_disks()
                    for disk in DiskList.get_usable_disks():
                        if disk.state == 'MISSING' or disk.mountpoint is None:
                            continue
                        for asd_id in local_client.dir_list(disk.mountpoint):
                            if asd_id in asd_map:
                                asd = asd_map[asd_id]
                            else:
                                asd = ASD()

                            asd.disk = disk
                            asd.asd_id = asd_id
                            asd.folder = asd_id
                            if asd.has_config:
                                if asd.port is None or asd.hosts is None:
                                    config = Configuration.get(
                                        key=asd.config_key)
                                    asd.port = config['port']
                                    asd.hosts = config.get('ips', [])
                                asd.save()

                    # Adjustment of open file descriptors for ASD/maintenance services to 8192
                    asd_service_names = list(ASDController.list_asd_services())
                    maintenance_service_names = list(
                        MaintenanceController.get_services())
                    for service_name in asd_service_names + maintenance_service_names:
                        if cls.service_manager.has_service(
                                name=service_name, client=local_client):
                            if cls.service_manager.__class__ == Systemd:
                                path = '/lib/systemd/system/{0}.service'.format(
                                    service_name)
                                check = 'LimitNOFILE=8192'
                            else:
                                path = '/etc/init/{0}.conf'.format(
                                    service_name)
                                check = 'limit nofile 8192 8192'

                            restart_required = False
                            if os.path.exists(path):
                                with open(path, 'r') as system_file:
                                    if check not in system_file.read():
                                        restart_required = True

                            if restart_required is False:
                                continue

                            configuration_key = ServiceFactory.SERVICE_CONFIG_KEY.format(
                                node_id, service_name)
                            if Configuration.exists(configuration_key):
                                # Rewrite the service file
                                cls.service_manager.add_service(
                                    name=ASDController.ASD_PREFIX
                                    if service_name in asd_service_names else
                                    MaintenanceController.MAINTENANCE_PREFIX,
                                    client=local_client,
                                    params=Configuration.get(
                                        configuration_key),
                                    target_name=service_name)

                                # Let the update know that the ASD / maintenance services need to be restarted
                                # Inside `if Configuration.exists`, because useless to rapport restart if we haven't rewritten service file
                                ExtensionsToolbox.edit_version_file(
                                    client=local_client,
                                    package_name='alba',
                                    old_run_file='{0}/{1}.version'.format(
                                        ServiceFactory.RUN_FILE_DIR,
                                        service_name))
                        if cls.service_manager.__class__ == Systemd:
                            local_client.run(['systemctl', 'daemon-reload'])

                    # Version 3: Addition of 'ExecReload' for ASD/maintenance SystemD services
                    if cls.service_manager.__class__ == Systemd:  # Upstart does not have functionality to reload a process' configuration
                        reload_daemon = False
                        asd_service_names = list(
                            ASDController.list_asd_services())
                        maintenance_service_names = list(
                            MaintenanceController.get_services())
                        for service_name in asd_service_names + maintenance_service_names:
                            if not cls.service_manager.has_service(
                                    name=service_name, client=local_client):
                                continue

                            path = '/lib/systemd/system/{0}.service'.format(
                                service_name)
                            if os.path.exists(path):
                                with open(path, 'r') as system_file:
                                    if 'ExecReload' not in system_file.read():
                                        reload_daemon = True
                                        configuration_key = ServiceFactory.SERVICE_CONFIG_KEY.format(
                                            node_id, service_name)
                                        if Configuration.exists(
                                                configuration_key):
                                            # No need to edit the service version file, since this change only requires a daemon-reload
                                            cls.service_manager.add_service(
                                                name=ASDController.ASD_PREFIX
                                                if service_name
                                                in asd_service_names else
                                                MaintenanceController.
                                                MAINTENANCE_PREFIX,
                                                client=local_client,
                                                params=Configuration.get(
                                                    configuration_key),
                                                target_name=service_name)
                        if reload_daemon is True:
                            local_client.run(['systemctl', 'daemon-reload'])

                    # Version 6: Introduction of Active Drive
                    all_local_ips = OSFactory.get_manager().get_ip_addresses(
                        client=local_client)
                    for asd in ASDList.get_asds():
                        if asd.has_config:
                            asd_config = Configuration.get(asd.config_key)
                            if 'multicast' not in asd_config:
                                asd_config['multicast'] = None
                            if 'ips' in asd_config:
                                asd_ips = asd_config['ips'] or all_local_ips
                            else:
                                asd_ips = all_local_ips
                            asd.hosts = asd_ips
                            asd_config['ips'] = asd_ips
                            Configuration.set(asd.config_key, asd_config)
                            asd.save()

                    # Version 7: Moving flask certificate files to config dir
                    for file_name in [
                            'passphrase', 'server.crt', 'server.csr',
                            'server.key'
                    ]:
                        if local_client.file_exists(
                                '/opt/asd-manager/source/{0}'.format(
                                    file_name)):
                            local_client.file_move(
                                source_file_name='/opt/asd-manager/source/{0}'.
                                format(file_name),
                                destination_file_name=
                                '/opt/asd-manager/config/{0}'.format(
                                    file_name))
                except:
                    cls.logger.exception(
                        'Error while executing post-update code on node {0}'.
                        format(node_id))
            Configuration.set(key, cls.CURRENT_VERSION)

            if cls.service_manager.has_service(
                    asd_manager_service_name,
                    local_client) and cls.service_manager.get_service_status(
                        asd_manager_service_name, local_client) != 'active':
                cls.logger.info('Starting asd-manager service')
                cls.service_manager.start_service(asd_manager_service_name,
                                                  local_client)

        cls.logger.info('Post-update logic executed')
Пример #4
0
class SDMUpdateController(object):
    """
    Update Controller class for SDM package
    """
    _local_client = SSHClient(endpoint='127.0.0.1', username='******')
    _logger = Logger(name='update', forced_target_type='file')
    _package_manager = PackageFactory.get_manager()
    _service_manager = ServiceFactory.get_manager()

    @classmethod
    def get_package_information(cls):
        """
        Retrieve the installed and candidate versions of all packages relevant for this repository (See PackageFactory.get_package_info)
        If installed version is lower than candidate version, this information is stored
        If installed version is equal or higher than candidate version we verify whether all relevant services have the correct binary active
        Whether a service has the correct binary version in use, we use the ServiceFactory.get_service_update_versions functionality

        In this function the services for each component / package combination are defined
        This service information consists out of:
            * Services to stop (before update) and start (after update of packages) -> 'services_stop_start'
            * Services to restart after update (post-update logic)                  -> 'services_post_update'
            * Down-times which will be caused due to service restarts               -> 'downtime'
            * Prerequisites that have not been met                                  -> 'prerequisites'

        The installed vs candidate version which is displayed always gives priority to the versions effectively installed on the system
        and not the versions as reported by the service files

        This combined information is then stored in the 'package_information' of the ALBA Node DAL object
        :return: Update information
        :rtype: dict
        """
        cls._logger.info('Refreshing update information')

        binaries = cls._package_manager.get_binary_versions(client=cls._local_client)
        update_info = {}
        package_info = PackageFactory.get_packages_to_update(client=cls._local_client)  # {'alba': {'openvstorage-sdm': {'installed': 'ee-1.6.1', 'candidate': 'ee-1.6.2'}}}
        cls._logger.debug('Binary versions found: {0}'.format(binaries))
        cls._logger.debug('Package info found: {0}'.format(package_info))
        for component, package_names in PackageFactory.get_package_info()['names'].iteritems():
            package_names = sorted(package_names)
            cls._logger.debug('Validating component {0} and related packages: {1}'.format(component, package_names))
            if component not in update_info:
                update_info[component] = copy.deepcopy(ServiceFactory.DEFAULT_UPDATE_ENTRY)
            svc_component_info = update_info[component]
            pkg_component_info = package_info.get(component, {})

            for package_name in package_names:
                cls._logger.debug('Validating package {0}'.format(package_name))
                if package_name in [PackageFactory.PKG_ALBA, PackageFactory.PKG_ALBA_EE]:
                    for service_name in sorted(list(ASDController.list_asd_services())) + sorted(list(MaintenanceController.get_services())):
                        service_version = ServiceFactory.get_service_update_versions(client=cls._local_client, service_name=service_name, binary_versions=binaries)
                        cls._logger.debug('Service {0} has version: {1}'.format(service_name, service_version))
                        # If package_name in pkg_component_info --> update available (installed <--> candidate)
                        # If service_version is not None --> service is running an older binary version
                        if package_name in pkg_component_info or service_version is not None:
                            svc_component_info['services_post_update'][20].append(service_name)
                            if service_version is not None and package_name not in svc_component_info['packages']:
                                svc_component_info['packages'][package_name] = service_version

                # Extend the service information with the package information related to this repository for current ALBA Node
                if package_name in pkg_component_info and package_name not in svc_component_info['packages']:
                    cls._logger.debug('Adding package {0} because it has an update available'.format(package_name))
                    svc_component_info['packages'][package_name] = pkg_component_info[package_name]
        cls._logger.info('Refreshed update information')
        return update_info

    @classmethod
    def update(cls, package_name):
        """
        Update the package on the local node
        :return: None
        :rtype: NoneType
        """
        cls._logger.info('Installing package {0}'.format(package_name))
        cls._package_manager.install(package_name=package_name, client=cls._local_client)
        cls._logger.info('Installed package {0}'.format(package_name))

    @classmethod
    def get_installed_version_for_package(cls, package_name):
        """
        Retrieve the currently installed package version
        :param package_name: Name of the package to retrieve the version for
        :type package_name: str
        :return: Version of the currently installed package
        :rtype: str
        """
        installed_version = cls._package_manager.get_installed_versions(client=None, package_names=[package_name])
        if package_name in installed_version:
            return str(installed_version[package_name])

    @classmethod
    def restart_services(cls, service_names):
        """
        Restart the services specified
        :param service_names: Names of the services to restart
        :type service_names: list[str]
        :return: None
        :rtype: NoneType
        """
        if len(service_names) == 0:
            service_names = [service_name for service_name in ASDController.list_asd_services()]
            service_names.extend([service_name for service_name in MaintenanceController.get_services()])

        for service_name in service_names:
            cls._logger.warning('Verifying whether service {0} needs to be restarted'.format(service_name))
            if cls._service_manager.get_service_status(service_name, cls._local_client) != 'active':
                cls._logger.warning('Found stopped service {0}. Will not start it.'.format(service_name))
                continue

            cls._logger.info('Restarting service {0}'.format(service_name))
            try:
                cls._service_manager.restart_service(service_name, cls._local_client)
            except CalledProcessError:
                cls._logger.exception('Failed to restart service {0}'.format(service_name))

    @classmethod
    def execute_migration_code(cls):
        # type: () -> None
        """
        Run some migration code after an update has been done
        :return: None
        :rtype: NoneType
        """
        cls._logger.info('Starting out of band migrations for SDM nodes')

        ###########################
        # Start crucial migration #
        ###########################

        # Removal of bootstrap file and store API IP, API port and node ID in SQLite DB
        try:
            if cls._local_client.file_exists(BOOTSTRAP_FILE):
                cls._logger.info('Bootstrap file still exists. Retrieving node ID')
                with open(BOOTSTRAP_FILE) as bstr_file:
                    node_id = json.load(bstr_file)['node_id']
            else:
                node_id = SettingList.get_setting_by_code(code='node_id').value
        except Exception:
            cls._logger.exception('Unable to determine the node ID, cannot migrate')
            raise

        try:
            api_settings_map = {'api_ip': 'ip', 'api_port': 'port'}  # Map settings code to keys in the Config management
            required_settings = ['node_id', 'migration_version'] + api_settings_map.keys()
            for settings_code in required_settings:
                try:
                    _ = SettingList.get_setting_by_code(settings_code)
                except ObjectNotFoundException:
                    cls._logger.info('Missing required settings: {0}'.format(settings_code))
                    if settings_code == 'node_id':
                        value = node_id
                    elif settings_code in api_settings_map.keys():
                        # Information must be extracted from Configuration
                        main_config = Configuration.get(ASD_NODE_CONFIG_MAIN_LOCATION.format(node_id))
                        value = main_config[api_settings_map[settings_code]]
                    elif settings_code == 'migration_version':
                        # Introduce version for ASD Manager migration code
                        value = 0
                    else:
                        raise NotImplementedError('No action implemented for setting {0}'.format(settings_code))

                    cls._logger.info('Modeling Setting with code {0} and value {1}'.format(settings_code, value))
                    setting = Setting()
                    setting.code = settings_code
                    setting.value = value
                    setting.save()

            if cls._local_client.file_exists(BOOTSTRAP_FILE):
                cls._logger.info('Removing the bootstrap file')
                cls._local_client.file_delete(BOOTSTRAP_FILE)
        except Exception:
            cls._logger.exception('Error during migration of code settings. Unable to proceed')
            raise

        ###############################
        # Start non-crucial migration #
        ###############################

        errors = []
        migration_setting = SettingList.get_setting_by_code(code='migration_version')
        # Add installed package_name in version files and additional string replacements in service files
        try:
            if migration_setting.value < 1:
                cls._logger.info('Adding additional information to service files')
                edition = Configuration.get_edition()
                if edition == PackageFactory.EDITION_ENTERPRISE:
                    for version_file_name in cls._local_client.file_list(directory=ServiceFactory.RUN_FILE_DIR):
                        version_file_path = '{0}/{1}'.format(ServiceFactory.RUN_FILE_DIR, version_file_name)
                        contents = cls._local_client.file_read(filename=version_file_path)
                        if '{0}='.format(PackageFactory.PKG_ALBA) in contents:
                            contents = contents.replace(PackageFactory.PKG_ALBA, PackageFactory.PKG_ALBA_EE)
                            cls._local_client.file_write(filename=version_file_path, contents=contents)

                    node_id = SettingList.get_setting_by_code(code='node_id').value
                    asd_services = list(ASDController.list_asd_services())
                    maint_services = list(MaintenanceController.get_services())
                    for service_name in asd_services + maint_services:
                        config_key = ServiceFactory.SERVICE_CONFIG_KEY.format(node_id, service_name)
                        if Configuration.exists(key=config_key):
                            config = Configuration.get(key=config_key)
                            if 'RUN_FILE_DIR' in config:
                                continue
                            config['RUN_FILE_DIR'] = ServiceFactory.RUN_FILE_DIR
                            config['ALBA_PKG_NAME'] = PackageFactory.PKG_ALBA_EE
                            config['ALBA_VERSION_CMD'] = PackageFactory.VERSION_CMD_ALBA
                            Configuration.set(key=config_key, value=config)
                            cls._service_manager.regenerate_service(name=ASDController.ASD_PREFIX if service_name in asd_services else MaintenanceController.MAINTENANCE_PREFIX,
                                                                    client=cls._local_client,
                                                                    target_name=service_name)
        except Exception as ex:
            cls._logger.exception('Failed to regenerate the ASD and Maintenance services')
            errors.append(ex)

        try:
            if migration_setting.value < 2:
                if System.get_component_identifier() not in Configuration.get(Configuration.get_registration_key(), default=[]):
                    Configuration.register_usage(System.get_component_identifier())
        except Exception as ex:
            cls._logger.exception('Failed to register the asd-manager')
            errors.append(ex)

        if len(errors) == 0:
            cls._logger.info('No errors during non-crucial migration. Saving the migration setting')
            # Save migration settings when no errors occurred
            migration_setting = SettingList.get_setting_by_code(code='migration_version')
            migration_setting.value = 2
            migration_setting.save()

        cls._logger.info('Finished out of band migrations for SDM nodes')
Пример #5
0
 def __init__(self):
     """
     Dummy init method
     """
     self._logger = Logger('tools')
Пример #6
0
class Watcher(object):
    """
    Watcher class
    """

    LOG_CONTENTS = None
    INTERNAL_CONFIG_KEY = '__ovs_config'

    def __init__(self):
        """
        Dummy init method
        """
        self._logger = Logger('tools')

    def log_message(self, log_target, entry, level):
        """
        Logs an entry
        """
        if level > 0:  # 0 = debug, 1 = info, 2 = error
            self._logger.debug('[{0}] {1}'.format(log_target, entry))

    def services_running(self, target):
        """
        Check all services are running
        :param target: Target to check
        :return: Boolean
        """
        try:
            if target == 'config':
                self.log_message(target, 'Testing configuration store...', 0)
                from ovs_extensions.db.arakoon.pyrakoon.pyrakoon.compat import NoGuarantee
                from source.tools.arakooninstaller import ArakoonInstaller, ArakoonClusterConfig
                from source.tools.configuration import Configuration

                try:
                    Configuration.list('/')
                except Exception as ex:
                    self.log_message(
                        target,
                        '  Error during configuration store test: {0}'.format(
                            ex), 2)
                    return False

                with open(CACC_LOCATION) as config_file:
                    contents = config_file.read()
                config = ArakoonClusterConfig(cluster_id='cacc',
                                              load_config=False)
                config.read_config(contents=contents)
                client = ArakoonInstaller.build_client(config)
                contents = client.get(Watcher.INTERNAL_CONFIG_KEY,
                                      consistency=NoGuarantee())
                if Watcher.LOG_CONTENTS != contents:
                    try:
                        # Validate whether the contents are not corrupt
                        parser = RawConfigParser()
                        parser.readfp(StringIO(contents))
                    except Exception as ex:
                        self.log_message(
                            target,
                            '  Configuration stored in configuration store seems to be corrupt: {0}'
                            .format(ex), 2)
                        return False
                    temp_filename = '{0}~'.format(CACC_LOCATION)
                    with open(temp_filename, 'w') as config_file:
                        config_file.write(contents)
                        config_file.flush()
                        os.fsync(config_file)
                    os.rename(temp_filename, CACC_LOCATION)
                    if Watcher.LOG_CONTENTS is not None:
                        self.log_message(
                            target, '  Configuration changed, trigger restart',
                            1)
                        sys.exit(1)
                    Watcher.LOG_CONTENTS = contents
                self.log_message(target, '  Configuration store OK', 0)
                return True
        except Exception as ex:
            self.log_message(target, 'Unexpected exception: {0}'.format(ex), 2)
            return False
Пример #7
0
 def _get_logger_instance(cls):
     return Logger('tools')
Пример #8
0
    def create_asd(disk):
        """
        Creates and starts an ASD on a given disk
        :param disk: Disk on which to create an ASD
        :type disk: source.dal.objects.disk.Disk
        :return: None
        :rtype: NoneType
        """
        # Validations
        if disk.state == 'MISSING':
            raise RuntimeError(
                'Cannot create an ASD on missing disk {0}'.format(disk.name))

        _node_id = SettingList.get_setting_by_code(code='node_id').value
        ipaddresses = Configuration.get('{0}|ips'.format(
            ASD_NODE_CONFIG_NETWORK_LOCATION.format(_node_id)))
        if len(ipaddresses) == 0:
            ipaddresses = OSFactory.get_manager().get_ip_addresses(
                client=ASDController._local_client)
            if len(ipaddresses) == 0:
                raise RuntimeError('Could not find any IP on the local node')

        alba_pkg_name, alba_version_cmd = PackageFactory.get_package_and_version_cmd_for(
            component='alba'
        )  # Call here, because this potentially raises error, which should happen before actually making changes

        # Fetch disk information
        disk_size = int(
            ASDController._local_client.run(
                ['df', '-B', '1', '--output=size', disk.mountpoint],
                timeout=5).splitlines()[1])

        # Find out appropriate disk size
        asd_size = int(math.floor(disk_size / (len(disk.asds) + 1)))
        for asd in disk.asds:
            if asd.has_config:
                config = Configuration.get(asd.config_key)
                config['capacity'] = asd_size
                cache_size = ASDController.calculate_rocksdb_cache_size(
                    is_ssd=disk.is_ssd)
                if cache_size:
                    config.update({'rocksdb_block_cache_size': cache_size})
                Configuration.set(asd.config_key, config)
                try:
                    ASDController._service_manager.send_signal(
                        asd.service_name, signal.SIGUSR1,
                        ASDController._local_client)
                except Exception as ex:
                    ASDController._logger.info(
                        'Could not send signal to ASD for reloading the quota: {0}'
                        .format(ex))

        used_ports = []
        for asd in ASDList.get_asds():
            if asd.has_config:
                config = Configuration.get(asd.config_key)
                used_ports.append(config['port'])
                if 'rora_port' in config:
                    used_ports.append(config['rora_port'])

        # Prepare & start service
        ASDController._logger.info('Setting up service for disk {0}'.format(
            disk.name))
        asd_id = ''.join(
            random.choice(string.ascii_letters + string.digits)
            for _ in range(32))
        homedir = '{0}/{1}'.format(disk.mountpoint, asd_id)
        base_port = Configuration.get('{0}|port'.format(
            ASD_NODE_CONFIG_NETWORK_LOCATION.format(_node_id)))

        asd_port = base_port
        rora_port = base_port + 1
        while asd_port in used_ports:
            asd_port += 1
        used_ports.append(asd_port)
        while rora_port in used_ports:
            rora_port += 1

        asd_config = {
            'ips': ipaddresses,
            'home': homedir,
            'port': asd_port,
            'asd_id': asd_id,
            'node_id': _node_id,
            'capacity': asd_size,
            'multicast': None,
            'transport': 'tcp',
            'log_level': 'info'
        }
        cache_size = ASDController.calculate_rocksdb_cache_size(
            is_ssd=disk.is_ssd)
        if cache_size:
            asd_config.update({'rocksdb_block_cache_size': cache_size})
        if Configuration.get('/ovs/framework/rdma'):
            asd_config['rora_port'] = rora_port
            asd_config['rora_transport'] = 'rdma'

        if Configuration.exists('{0}/extra'.format(
                ASD_NODE_CONFIG_LOCATION.format(_node_id))):
            data = Configuration.get('{0}/extra'.format(
                ASD_NODE_CONFIG_LOCATION.format(_node_id)))
            asd_config.update(data)

        asd = ASD()
        asd.disk = disk
        asd.port = asd_port
        asd.hosts = ipaddresses
        asd.asd_id = asd_id
        asd.folder = asd_id
        asd.save()

        Configuration.set(asd.config_key, asd_config)
        params = {
            'LOG_SINK': Logger.get_sink_path('alba-asd_{0}'.format(asd_id)),
            'CONFIG_PATH':
            Configuration.get_configuration_path(asd.config_key),
            'SERVICE_NAME': asd.service_name,
            'ALBA_PKG_NAME': alba_pkg_name,
            'ALBA_VERSION_CMD': alba_version_cmd
        }
        os.mkdir(homedir)
        ASDController._local_client.run(['chown', '-R', 'alba:alba', homedir])
        ASDController._service_manager.add_service(
            name=ASDController.ASD_PREFIX,
            client=ASDController._local_client,
            params=params,
            target_name=asd.service_name)
        ASDController.start_asd(asd)
Пример #9
0
class ASDController(object):
    """
    ASD Controller class
    """
    ASD_PREFIX = 'alba-asd'
    _logger = Logger('controllers')
    _local_client = SSHClient(endpoint='127.0.0.1', username='******')
    _service_manager = ServiceFactory.get_manager()

    @staticmethod
    def calculate_rocksdb_cache_size(is_ssd):
        """
        Calculate the cache size for the RocksDB
        :param disk: disk on which the asd is running
        :type disk: source.dal.objects.disk.Disk
        :return: None or int
        """
        if is_ssd:  # No cache size is required to be specified for ASDs
            return None
        else:
            return 128 * 1024 * 1024  # 128 MiB

    @staticmethod
    def create_asd(disk):
        """
        Creates and starts an ASD on a given disk
        :param disk: Disk on which to create an ASD
        :type disk: source.dal.objects.disk.Disk
        :return: None
        :rtype: NoneType
        """
        # Validations
        if disk.state == 'MISSING':
            raise RuntimeError(
                'Cannot create an ASD on missing disk {0}'.format(disk.name))

        _node_id = SettingList.get_setting_by_code(code='node_id').value
        ipaddresses = Configuration.get('{0}|ips'.format(
            ASD_NODE_CONFIG_NETWORK_LOCATION.format(_node_id)))
        if len(ipaddresses) == 0:
            ipaddresses = OSFactory.get_manager().get_ip_addresses(
                client=ASDController._local_client)
            if len(ipaddresses) == 0:
                raise RuntimeError('Could not find any IP on the local node')

        alba_pkg_name, alba_version_cmd = PackageFactory.get_package_and_version_cmd_for(
            component='alba'
        )  # Call here, because this potentially raises error, which should happen before actually making changes

        # Fetch disk information
        disk_size = int(
            ASDController._local_client.run(
                ['df', '-B', '1', '--output=size', disk.mountpoint],
                timeout=5).splitlines()[1])

        # Find out appropriate disk size
        asd_size = int(math.floor(disk_size / (len(disk.asds) + 1)))
        for asd in disk.asds:
            if asd.has_config:
                config = Configuration.get(asd.config_key)
                config['capacity'] = asd_size
                cache_size = ASDController.calculate_rocksdb_cache_size(
                    is_ssd=disk.is_ssd)
                if cache_size:
                    config.update({'rocksdb_block_cache_size': cache_size})
                Configuration.set(asd.config_key, config)
                try:
                    ASDController._service_manager.send_signal(
                        asd.service_name, signal.SIGUSR1,
                        ASDController._local_client)
                except Exception as ex:
                    ASDController._logger.info(
                        'Could not send signal to ASD for reloading the quota: {0}'
                        .format(ex))

        used_ports = []
        for asd in ASDList.get_asds():
            if asd.has_config:
                config = Configuration.get(asd.config_key)
                used_ports.append(config['port'])
                if 'rora_port' in config:
                    used_ports.append(config['rora_port'])

        # Prepare & start service
        ASDController._logger.info('Setting up service for disk {0}'.format(
            disk.name))
        asd_id = ''.join(
            random.choice(string.ascii_letters + string.digits)
            for _ in range(32))
        homedir = '{0}/{1}'.format(disk.mountpoint, asd_id)
        base_port = Configuration.get('{0}|port'.format(
            ASD_NODE_CONFIG_NETWORK_LOCATION.format(_node_id)))

        asd_port = base_port
        rora_port = base_port + 1
        while asd_port in used_ports:
            asd_port += 1
        used_ports.append(asd_port)
        while rora_port in used_ports:
            rora_port += 1

        asd_config = {
            'ips': ipaddresses,
            'home': homedir,
            'port': asd_port,
            'asd_id': asd_id,
            'node_id': _node_id,
            'capacity': asd_size,
            'multicast': None,
            'transport': 'tcp',
            'log_level': 'info'
        }
        cache_size = ASDController.calculate_rocksdb_cache_size(
            is_ssd=disk.is_ssd)
        if cache_size:
            asd_config.update({'rocksdb_block_cache_size': cache_size})
        if Configuration.get('/ovs/framework/rdma'):
            asd_config['rora_port'] = rora_port
            asd_config['rora_transport'] = 'rdma'

        if Configuration.exists('{0}/extra'.format(
                ASD_NODE_CONFIG_LOCATION.format(_node_id))):
            data = Configuration.get('{0}/extra'.format(
                ASD_NODE_CONFIG_LOCATION.format(_node_id)))
            asd_config.update(data)

        asd = ASD()
        asd.disk = disk
        asd.port = asd_port
        asd.hosts = ipaddresses
        asd.asd_id = asd_id
        asd.folder = asd_id
        asd.save()

        Configuration.set(asd.config_key, asd_config)
        params = {
            'LOG_SINK': Logger.get_sink_path('alba-asd_{0}'.format(asd_id)),
            'CONFIG_PATH':
            Configuration.get_configuration_path(asd.config_key),
            'SERVICE_NAME': asd.service_name,
            'ALBA_PKG_NAME': alba_pkg_name,
            'ALBA_VERSION_CMD': alba_version_cmd
        }
        os.mkdir(homedir)
        ASDController._local_client.run(['chown', '-R', 'alba:alba', homedir])
        ASDController._service_manager.add_service(
            name=ASDController.ASD_PREFIX,
            client=ASDController._local_client,
            params=params,
            target_name=asd.service_name)
        ASDController.start_asd(asd)

    @staticmethod
    def update_asd(asd, update_data):
        """
        Updates an ASD with the 'update_data' provided
        :param asd: ASD to update
        :type asd: source.dal.objects.asd.ASD
        :param update_data: Data to update
        :type update_data: dict
        :raises ValueError: - When ASD configuration key is not present
                            - When an unsupported key is passed in via 'update_data'
        :return: None
        :rtype: NoneType
        """
        key_map = {'ips': 'hosts'}
        if not Configuration.exists(asd.config_key):
            raise ValueError(
                'Failed to the configuration at location {0}'.format(
                    asd.config_key))

        config = Configuration.get(asd.config_key)
        for key, value in update_data.iteritems():
            if key not in key_map:  # Only updating IPs is supported for now
                raise ValueError(
                    'Unsupported property provided: {0}. Only IPs can be updated for now'
                    .format(key))
            setattr(asd, key_map[key], value)
            config[key] = value
        asd.save()
        Configuration.set(key=asd.config_key, value=config)

    @staticmethod
    def remove_asd(asd):
        """
        Remove an ASD
        :param asd: ASD to remove
        :type asd: source.dal.objects.asd.ASD
        :return: None
        :rtype: NoneType
        """
        if ASDController._service_manager.has_service(
                asd.service_name, ASDController._local_client):
            ASDController._service_manager.stop_service(
                asd.service_name, ASDController._local_client)
            ASDController._service_manager.remove_service(
                asd.service_name, ASDController._local_client)
        try:
            ASDController._local_client.dir_delete('{0}/{1}'.format(
                asd.disk.mountpoint, asd.asd_id))
        except Exception:
            ASDController._logger.exception('Could not clean ASD data')
        Configuration.delete(asd.config_key)
        asd.delete()

    @staticmethod
    def start_asd(asd):
        """
        Start an ASD
        :param asd: ASD to start
        :type asd: source.dal.objects.asd.ASD
        :return: None
        :rtype: NoneType
        """
        if ASDController._service_manager.has_service(
                asd.service_name, ASDController._local_client):
            ASDController._service_manager.start_service(
                asd.service_name, ASDController._local_client)

    @staticmethod
    def stop_asd(asd):
        """
        Stop an ASD
        :param asd: ASD to stop
        :type asd: source.dal.objects.asd.ASD
        :return: None
        :rtype: NoneType
        """
        if ASDController._service_manager.has_service(
                asd.service_name, ASDController._local_client):
            ASDController._service_manager.stop_service(
                asd.service_name, ASDController._local_client)

    @staticmethod
    def restart_asd(asd):
        """
        Restart an ASD
        :param asd: ASD to restart
        :type asd: source.dal.objects.asd.ASD
        :return: None
        :rtype: NoneType
        """
        if ASDController._service_manager.has_service(
                asd.service_name, ASDController._local_client):
            ASDController._service_manager.restart_service(
                asd.service_name, ASDController._local_client)

    @staticmethod
    def list_asd_services():
        """
        Retrieve all ASD services
        :return: The ASD Services present on this ALBA Node
        :rtype: generator
        """
        for service_name in ASDController._service_manager.list_services(
                ASDController._local_client):
            if service_name.startswith(ASD.ASD_SERVICE_PREFIX.format('')):
                yield service_name
Пример #10
0
from source.dal.objects.setting import Setting
from source.constants.asd import ASD_NODE_CONFIG_MAIN_LOCATION, CACC_LOCATION, CONFIG_STORE_LOCATION
from source.controllers.asd import ASDController
from source.controllers.disk import DiskController
from source.controllers.maintenance import MaintenanceController
from source.tools.configuration import Configuration
from source.tools.logger import Logger
from source.tools.osfactory import OSFactory
from source.tools.servicefactory import ServiceFactory

PRECONFIG_FILE = '/opt/asd-manager/config/preconfig.json'
BOOTSTRAP_FILE = '/opt/asd-manager/config/bootstrap.json'
MANAGER_SERVICE = 'asd-manager'
WATCHER_SERVICE = 'asd-watcher'

asd_manager_logger = Logger('asd_manager')


def setup():
    """
    Interactive setup part for initial asd manager configuration
    """
    _print_and_log(message=Interactive.boxed_message(['ASD Manager setup']))

    # Gather information
    ipaddresses = OSFactory.get_manager().get_ip_addresses()
    if not ipaddresses:
        _print_and_log(
            level='error',
            message='\n' + Interactive.boxed_message(
                ['Could not retrieve IP information on local node']))
Пример #11
0
class API(object):
    """ ALBA API """
    _logger = Logger('flask')

    get = HTTPRequestDecorators.get
    post = HTTPRequestDecorators.post
    delete = HTTPRequestDecorators.delete
    wrap = HTTPRequestDecorators.wrap_data
    provide_request_data = HTTPRequestDecorators.provide_request_data

    ###########
    # GENERIC #
    ###########
    @staticmethod
    @get('/')
    @wrap('node_id')
    def index():
        # type: () -> dict
        """
        Retrieve the local node ID
        :return: Node ID
        :rtype: dict
        """
        return SettingList.get_setting_by_code(code='node_id').value

    @staticmethod
    @get('/net', authenticate=False)
    @wrap('ips')
    def net():
        # type: () -> dict
        """
        Retrieve IP information
        :return: IPs found on the local system (excluding the loop-back IPs)
        :rtype: dict
        """
        return OSFactory.get_manager().get_ip_addresses()

    @staticmethod
    @post('/net')
    @provide_request_data
    def set_net(request_data):
        # type: (dict) -> None
        """
        Set IP information
        :param request_data: Data about the request (given by the decorator)
        :type request_data: dict
        :return: None
        :rtype: NoneType
        """
        node_id = SettingList.get_setting_by_code(code='node_id').value
        Configuration.set(
            '{0}|ips'.format(ASD_NODE_CONFIG_NETWORK_LOCATION.format(node_id)),
            request_data['ips'])

    @staticmethod
    @get('/collect_logs')
    @wrap('filename')
    def collect_logs():
        # type: () -> dict
        """
        Collect the logs
        :return: The location where the file containing the logs was stored
        :rtype: dict
        """
        return GenericController.collect_logs()

    @staticmethod
    @app.route('/downloads/<filename>')
    def download_logs(filename):
        # type: (str) -> any
        """
        Download the tgz containing the logs
        :param filename: Name of the file to make available for download
        :type filename: str
        :return: Flask response
        :rtype: Flask response
        """
        filename = filename.split('/')[-1]
        API._logger.info('Uploading file {0}'.format(filename))
        return send_from_directory(directory='/opt/asd-manager/downloads',
                                   filename=filename)

    #################
    # STACK / SLOTS #
    #################

    @staticmethod
    @get('/slots')
    @wrap()
    def get_slots():
        # type: () -> dict
        """
        Gets the current stack (slot based)
        :return: Stack information
        :rtype: dict
        """
        stack = {}
        for disk in DiskList.get_usable_disks():
            slot_id = disk.aliases[0].split('/')[-1]
            stack[slot_id] = disk.export()
            stack[slot_id].update({
                'osds':
                dict((asd.asd_id, asd.export()) for asd in disk.asds)
            })
        return stack

    @staticmethod
    @post('/slots/<slot_id>/asds')
    def asd_add(slot_id):
        # type: (str) -> None
        """
        Add an ASD to the slot specified
        :param slot_id: Identifier of the slot
        :type slot_id: str
        :return: None
        :rtype: NoneType
        """
        disk = DiskList.get_by_alias(slot_id)
        if disk.available is True:
            with file_mutex('add_disk'), file_mutex(
                    'disk_{0}'.format(slot_id)):
                DiskController.prepare_disk(disk=disk)
                disk = Disk(disk.id)
        with file_mutex('add_asd'):
            ASDController.create_asd(disk)

    @staticmethod
    @delete('/slots/<slot_id>/asds/<asd_id>')
    def asd_delete_by_slot(slot_id, asd_id):
        # type: (str, str) -> None
        """
        Delete an ASD from the slot specified
        :param slot_id: Identifier of the slot
        :type slot_id: str
        :param asd_id: Identifier of the ASD  (eg: bnAWEXuPHN5YJceCeZo7KxaQW86ixXd4, found under /mnt/alba-asd/WDCztMxmRqi6Hx21/)
        :type asd_id: str
        :return: None
        :rtype: NoneType
        """
        # If the disk would be missing, this will still return a disk object and the asds should be able to be found
        # Sync disk will only remove disks once they have no more asds linked to them
        disk = DiskList.get_by_alias(slot_id)
        asds = [asd for asd in disk.asds if asd.asd_id == asd_id]
        if len(asds) != 1:
            raise HttpNotFoundException(
                error='asd_not_found',
                error_description='Could not find ASD {0} on Slot {1}'.format(
                    asd_id, slot_id))
        ASDController.remove_asd(asds[0])

    @staticmethod
    @post('/slots/<slot_id>/asds/<asd_id>/restart')
    def asd_restart(slot_id, asd_id):
        # type: (str, str) -> None
        """
        Restart an ASD
        :param slot_id: Identifier of the slot
        :type slot_id: str
        :param asd_id: Identifier of the ASD  (eg: bnAWEXuPHN5YJceCeZo7KxaQW86ixXd4, found under /mnt/alba-asd/WDCztMxmRqi6Hx21/)
        :type asd_id: str
        :return: None
        :rtype: NoneType
        """
        disk = DiskList.get_by_alias(slot_id)
        asds = [asd for asd in disk.asds if asd.asd_id == asd_id]
        if len(asds) != 1:
            raise HttpNotFoundException(
                error='asd_not_found',
                error_description='Could not find ASD {0} on Slot {1}'.format(
                    asd_id, slot_id))
        ASDController.restart_asd(asds[0])

    @staticmethod
    @post('/slots/<slot_id>/asds/<asd_id>/update')
    @provide_request_data
    def asd_update(slot_id, asd_id, request_data):
        # type: (str, str, dict) -> None
        """
        Restart an ASD
        :param slot_id: Identifier of the slot
        :type slot_id: str
        :param asd_id: Identifier of the ASD  (eg: bnAWEXuPHN5YJceCeZo7KxaQW86ixXd4, found under /mnt/alba-asd/WDCztMxmRqi6Hx21/)
        :type asd_id: str
        :param request_data: Data about the request (given by the decorator)
        :type request_data: dict
        :return: None
        :rtype: NoneType
        """
        disk = DiskList.get_by_alias(slot_id)
        asds = [asd for asd in disk.asds if asd.asd_id == asd_id]
        if len(asds) != 1:
            raise HttpNotFoundException(
                error='asd_not_found',
                error_description='Could not find ASD {0} on Slot {1}'.format(
                    asd_id, slot_id))
        ASDController.update_asd(asd=asds[0],
                                 update_data=request_data['update_data'])

    @staticmethod
    @post('/slots/<slot_id>/restart')
    def slot_restart(slot_id):
        # type: (str) -> None
        """
       Restart a slot
       :param slot_id: Identifier of the slot  (eg: 'pci-0000:03:00.0-sas-0x5000c29f4cf04566-lun-0')
       :type slot_id: str
       :return: None
       """
        disk = DiskList.get_by_alias(slot_id)
        with file_mutex('slot_{0}'.format(slot_id)):
            API._logger.info(
                'Got lock for restarting slot {0}'.format(slot_id))
            for asd in disk.asds:
                ASDController.stop_asd(asd=asd)
            DiskController.remount_disk(disk=disk)
            for asd in disk.asds:
                ASDController.start_asd(asd=asd)

    @staticmethod
    @delete('/slots/<slot_id>')
    def clear_slot(slot_id):
        # type: (str) -> None
        """
        Clears a slot
        :param slot_id: Identifier of the slot
        :type slot_id: str
        :return: None
        :rtype: NoneType
        """
        try:
            disk = DiskList.get_by_alias(slot_id)
        except ObjectNotFoundException:
            API._logger.warning(
                'Disk with ID {0} is no longer present (or cannot be managed)'.
                format(slot_id))
            return None

        if disk.available is True:
            raise HttpNotAcceptableException(
                error='disk_not_configured',
                error_description='Disk not yet configured')

        with file_mutex('disk_{0}'.format(slot_id)):
            last_exception = None
            for asd in disk.asds:
                try:
                    ASDController.remove_asd(asd=asd)
                except Exception as ex:
                    last_exception = ex
            disk = Disk(disk.id)
            if len(disk.asds) == 0:
                DiskController.clean_disk(disk=disk)
            elif last_exception is not None:
                raise last_exception
            else:
                raise RuntimeError(
                    'Still some ASDs configured on Disk {0}'.format(slot_id))

    #########
    # DISKS #
    #########

    @staticmethod
    @get('/disks')
    def list_disks():
        """ Obsolete """
        return {}

    @staticmethod
    @get('/disks/<disk_id>')
    def index_disk(disk_id):
        """ Obsolete """
        raise RuntimeError('Disk {0} not found.'.format(disk_id))

    @staticmethod
    @post('/disks/<disk_id>/add')
    def add_disk(disk_id):
        """ Obsolete """
        raise RuntimeError('Disk {0} not found'.format(disk_id))

    @staticmethod
    @post('/disks/<disk_id>/delete')
    def delete_disk(disk_id):
        """ Obsolete """
        raise RuntimeError('Disk {0} not found'.format(disk_id))

    @staticmethod
    @post('/disks/<disk_id>/restart')
    def restart_disk(disk_id):
        """ Obsolete """
        raise RuntimeError('Disk {0} not found'.format(disk_id))

    ########
    # ASDS #
    ########

    @staticmethod
    @get('/asds')
    def list_asds():
        """ Obsolete """
        return {}

    @staticmethod
    @get('/asds/services')
    def list_asd_services():
        """ Obsolete """
        return {'services': []}

    @staticmethod
    @get('/disks/<disk_id>/asds')
    def list_asds_disk(disk_id):
        """ Obsolete """
        raise RuntimeError('Disk {0} not found'.format(disk_id))

    @staticmethod
    @get('/disks/<disk_id>/get_claimed_asds')
    def get_claimed_asds(disk_id):
        """ Obsolete """
        raise RuntimeError('Disk {0} not found'.format(disk_id))

    @staticmethod
    @post('/disks/<disk_id>/asds')
    def add_asd_disk(disk_id):
        """ Obsolete """
        raise RuntimeError('Disk {0} not found'.format(disk_id))

    @staticmethod
    @get('/disks/<disk_id>/asds/<asd_id>')
    def get_asd(disk_id, asd_id):
        """ Obsolete """
        raise RuntimeError('Disk {0} not found'.format(disk_id))

    @staticmethod
    @post('/disks/<disk_id>/asds/<asd_id>/restart')
    def restart_asd(disk_id, asd_id):
        """ Obsolete """
        raise RuntimeError('Disk {0} not found'.format(disk_id))

    @staticmethod
    @post('/disks/<disk_id>/asds/<asd_id>/delete')
    def asd_delete(disk_id, asd_id):
        """ Obsolete """
        raise RuntimeError('Disk {0} not found'.format(disk_id))

    ##########
    # UPDATE #
    ##########

    @staticmethod
    @get('/update/package_information')
    def get_package_information_new():
        # type: () -> dict
        """
        Retrieve update information
        This call is used by the new framework code (as off 30 Nov 2016)
        In case framework has new code, but SDM runs old code, the asdmanager.py plugin will adjust the old format to the new format
        :return: Installed and candidate for install version for all SDM related packages
        :rtype: dict
        """
        with file_mutex('package_update'):
            API._logger.info('Locking in place for package update')
            return SDMUpdateController.get_package_information()

    @staticmethod
    @get('/update/information')
    def get_package_information_old():
        # type: () -> dict
        """
        Retrieve update information
        This call is required when framework has old code and SDM has been updated (as off 30 Nov 2016)
        Old code tries to call /update/information and expects data formatted in the old style
        :return: Installed and candidate for install version for all SDM related packages
        :rtype: dict
        """
        return_value = {'version': '', 'installed': ''}
        with file_mutex('package_update'):
            API._logger.info('Locking in place for package update')
            update_info = SDMUpdateController.get_package_information().get(
                'alba', {})
            if 'openvstorage-sdm' in update_info:
                return_value['version'] = update_info['openvstorage-sdm'][
                    'candidate']
                return_value['installed'] = update_info['openvstorage-sdm'][
                    'installed']
            elif 'alba' in update_info:
                return_value['version'] = update_info['alba']['candidate']
                return_value['installed'] = update_info['alba']['installed']
        return return_value

    @staticmethod
    @post('/update/install/<package_name>')
    def update(package_name):
        # type: (str) -> None
        """
        Install the specified package
        :param package_name: Name of the package to update
        :type package_name: str
        :return: None
        :rtype: NoneType
        """
        with file_mutex('package_update'):
            SDMUpdateController.update(package_name=package_name)

    @staticmethod
    @post('/update/execute/<status>')
    @wrap('status')
    def execute_update(status):
        # type: (str) -> str
        """
        This call is required when framework has old code and SDM has been updated (as off 30 Nov 2016)
        Old code tries to initiate update providing a status, while new code no longer requires this status
        :param status: Unused
        :type status: str
        :return: The status of the ongoing update
        :rtype: str
        """
        _ = status
        with file_mutex('package_update'):
            SDMUpdateController.update(package_name='alba')
            SDMUpdateController.update(package_name='openvstorage-sdm')
            return 'done'

    @staticmethod
    @get('/update/installed_version_package/<package_name>')
    @wrap('version')
    def update_installed_version_package(package_name):
        # type: (str) -> str
        """
        Retrieve the currently installed package version
        :param package_name: Name of the package to retrieve the version for
        :type package_name: str
        :return: Version of the currently installed package
        :rtype: str
        """
        return SDMUpdateController.get_installed_version_for_package(
            package_name=package_name)

    @staticmethod
    @post('/update/execute_migration_code')
    def update_execute_migration_code():
        # type: () -> None
        """
        Run some migration code after an update has been done
        :return: None
        :rtype: NoneType
        """
        with file_mutex('post_update'):
            SDMUpdateController.execute_migration_code()

    ####################
    # GENERIC SERVICES #
    ####################

    @staticmethod
    @post('/update/restart_services')
    @provide_request_data
    def restart_services(request_data):
        # type: (dict) -> None
        """
        Restart services
        :param request_data: Data about the request (given by the decorator)
        :type request_data: dict
        :return: None
        :rtype: NoneType
        """
        with file_mutex('package_update'):
            SDMUpdateController.restart_services(service_names=json.loads(
                request_data.get('service_names', [])))

    @staticmethod
    @get('/service_status/<name>')
    @wrap('status')
    def get_service_status(name):
        # type: (str) -> Union[Tuple[bool, str], None]
        """
        Retrieve the status of the service specified
        :param name: Name of the service to check
        :type name: str
        :return: Status of the service
        :rtype: dict
        """
        client = SSHClient(endpoint='127.0.0.1', username='******')
        service_manager = ServiceFactory.get_manager()
        if service_manager.has_service(name=name, client=client):
            status = service_manager.get_service_status(name=name,
                                                        client=client)
            return status == 'active', status
        return None

    ########################
    # MAINTENANCE SERVICES #
    ########################

    @staticmethod
    @get('/maintenance')
    @wrap('services')
    def list_maintenance_services():
        # type: () -> List[str]
        """
        List all maintenance information
        :return: The names of all maintenance services found on the system
        :rtype: list
        """
        return list(MaintenanceController.get_services())

    @staticmethod
    @post('/maintenance/<name>/add')
    @provide_request_data
    def add_maintenance_service(name, request_data):
        # type: (str, dict) -> None
        """
        Add a maintenance service with a specific name
        :param name: Name of the maintenance service
        :type name: str
        :param request_data: Data about the request (given by the decorator)
        :type request_data: dict
        :return: None
        :rtype: NoneType
        """
        MaintenanceController.add_maintenance_service(
            name=name,
            abm_name=request_data['abm_name'],
            alba_backend_guid=request_data['alba_backend_guid'],
            read_preferences=request_data.get('read_preferences', []))

    @staticmethod
    @post('/maintenance/<name>/remove')
    @provide_request_data
    def remove_maintenance_service(name, request_data):
        # type: (str, dict) -> None
        """
        Remove a maintenance service with a specific name
        :param name: Name of the maintenance service
        :type name: str
        :param request_data: Data about the request (given by the decorator)
        :type request_data: dict
        :return: None
        :rtype: NoneType
        """
        MaintenanceController.remove_maintenance_service(
            name=name, alba_backend_guid=request_data.get('alba_backend_guid'))
Пример #12
0
class DiskController(object):
    """
    Disk helper methods
    """
    controllers = {}
    _local_client = SSHClient(endpoint='127.0.0.1', username='******')
    _logger = Logger('controllers')

    @staticmethod
    def sync_disks():
        # type: () -> None
        """
        Syncs the disks
        Changes made to this code should be reflected in the framework DiskController.sync_with_reality call.
        :return: None
        :rtype: NoneType
        """
        node_id = SettingList.get_setting_by_code(code='node_id').value
        s3 = Configuration.get(
            ASD_NODE_CONFIG_MAIN_LOCATION_S3.format(node_id), default=False)
        disks, name_alias_mapping = DiskTools.model_devices(s3=s3)
        disks_by_name = dict((disk.name, disk) for disk in disks)
        alias_name_mapping = name_alias_mapping.reverse_mapping()
        # Specific for the asd-manager: handle unique constraint exception
        DiskController._prepare_for_name_switch(disks)
        # Sync the model
        for disk in DiskList.get_disks():
            generic_disk_model = None  # type: GenericDisk
            for alias in disk.aliases:
                # IBS wont have alias
                if alias in alias_name_mapping:
                    name = alias_name_mapping[alias].replace('/dev/', '')
                    if name in disks_by_name:
                        generic_disk_model = disks_by_name.pop(name)
                        break
            # Partitioned loop, nvme devices no longer show up in alias_name_mapping
            if generic_disk_model is None and disk.name in disks_by_name and (
                    disk.name.startswith(tuple(['fio', 'loop', 'nvme']))):
                generic_disk_model = disks_by_name.pop(disk.name)

            if not generic_disk_model:
                # Remove disk / partitions if not reported by 'lsblk'
                DiskController._remove_disk_model(disk)
            else:
                # Update existing disks and their partitions
                DiskController._sync_disk_with_model(disk, generic_disk_model)
        # Create all disks and their partitions not yet modeled
        for disk_name, generic_disk_model in disks_by_name.iteritems():
            DiskController._model_disk(generic_disk_model)

    @classmethod
    def _remove_disk_model(cls, modeled_disk):
        # type: (Disk) -> None
        """
        Remove the modeled disk
        :param modeled_disk: The modeled disk
        :type modeled_disk: Disk
        :return: None
        :rtype: NoneType
        """
        cls._logger.info('Disk {0} - No longer found'.format(
            modeled_disk.name))
        if len(modeled_disk.asds) == 0:
            modeled_disk.delete()
            cls._logger.info('Disk {0} - Deleted (no ASDs)'.format(
                modeled_disk.name))
        else:
            if modeled_disk.state != 'MISSING':
                for partition in modeled_disk.partitions:
                    cls._logger.warning(
                        'Disk {0} - Partition with offset {1} - Updated status to MISSING'
                        .format(modeled_disk.name, partition['offset']))
                modeled_disk.state = 'MISSING'
                modeled_disk.save()
                cls._logger.warning(
                    'Disk {0} - Updated status to MISSING'.format(
                        modeled_disk.name))

    @classmethod
    def _sync_disk_with_model(cls, modeled_disk, generic_modeled_disk):
        # type: (Disk, GenericDisk) -> None
        """
        Sync a generic disk with the modeled disk
        :param modeled_disk: The modeled disk
        :type modeled_disk: Disk
        :param generic_modeled_disk: The generic modeled disk (returned by Disktools)
        :type generic_modeled_disk: GenericDisk
        :return: None
        :rtype NoneType
        """
        cls._logger.info('Disk {0} - Found, updating'.format(
            modeled_disk.name))
        cls._update_disk(modeled_disk, generic_modeled_disk)

    @classmethod
    def _model_disk(cls, generic_disk_model):
        # type: (GenericDisk) -> Disk
        """
        Models a disk
        :param generic_disk_model: The generic modeled disk (returned by Disktools)
        :type generic_disk_model: GenericDisk
        :return: The newly modeled disk
        :rtype: Disk
        """
        cls._logger.info('Disk {0} - Creating disk - {1}'.format(
            generic_disk_model.name, generic_disk_model.__dict__))
        disk = Disk()
        disk.name = generic_disk_model.name
        cls._update_disk(disk, generic_disk_model)
        return disk

    @staticmethod
    def _update_disk(modeled_disk, generic_disk_model):
        # type: (Disk, GenericDisk) -> None
        """
        Updates a disk
        Copies all properties from the generic modeled disk to the own model
        :param modeled_disk: The modeled disk
        :type modeled_disk: Disk
        :param generic_disk_model: The generic modeled disk (returned by Disktools)
        :type generic_disk_model: GenericDisk
        :return: None
        :rtype NoneType
        """
        for prop in [
                'state', 'aliases', 'is_ssd', 'model', 'size', 'name',
                'serial', 'partitions'
        ]:
            if hasattr(generic_disk_model, prop):
                if prop == 'partitions':
                    # Update partition info
                    partitions_as_dicts = [
                        partition.__dict__
                        for partition in generic_disk_model.partitions
                    ]
                    modeled_disk.partitions = partitions_as_dicts
                else:
                    setattr(modeled_disk, prop,
                            getattr(generic_disk_model, prop))
        modeled_disk.save()

    @classmethod
    def _prepare_for_name_switch(cls, generic_disks):
        # type: (List[GenericDisk]) -> None
        """
        This manager has a unique constraint on the disk name
        It could happen that a disk switched drive letter.
        To avoid any issues while syncing the disk, the name is temporarily changed
        :param generic_disks: List of the disks currently found by the system
        :type generic_disks: list
        :return: None
        :rtype: NoneType
        """
        # Check names to avoid a unique constraint exception
        for generic_disk in generic_disks:  # type: GenericDisk
            if len(generic_disk.aliases) >= 1:
                disk_alias = generic_disk.aliases[0]
                try:
                    disk = DiskList.get_by_alias(disk_alias)
                    if generic_disk.name != generic_disk.name:
                        cls._logger.info(
                            'Disk with alias {0} its name has changed from {1} to {2},'
                            ' changing disk names to circumvent unique constraints'
                            .format(disk_alias, disk.name, generic_disk.name))
                        disk.name = str(uuid.uuid4())
                        disk.save()
                except ObjectNotFoundException:
                    # No disk with such an alias. Will be caught later in the sync disk by adding the left-over models
                    pass

    @classmethod
    def prepare_disk(cls, disk):
        """
        Prepare a disk for use with ALBA
        :param disk: Disk object to prepare
        :type disk: source.dal.objects.disk.Disk
        :return: None
        """
        if disk.usable is False:
            raise RuntimeError('Cannot prepare disk {0}'.format(disk.name))
        cls._logger.info('Preparing disk {0}'.format(disk.name))

        # Create partition
        mountpoint = '/mnt/alba-asd/{0}'.format(''.join(
            random.choice(string.ascii_letters + string.digits)
            for _ in range(16)))
        alias = disk.aliases[0]
        cls._locate(device_alias=alias, start=False)
        cls._local_client.run(['umount', disk.mountpoint], allow_nonzero=True)
        cls._local_client.run(['parted', alias, '-s', 'mklabel', 'gpt'])
        cls._local_client.run([
            'parted', alias, '-s', 'mkpart',
            alias.split('/')[-1], '2MB', '100%'
        ])
        cls._local_client.run(['udevadm', 'settle'
                               ])  # Waits for all udev rules to have finished

        # Wait for partition to be ready by attempting to add filesystem
        counter = 0
        already_mounted = False
        while True:
            disk = Disk(disk.id)
            if len(disk.partitions) == 1:
                try:
                    cls._local_client.run(
                        ['mkfs.xfs', '-qf', disk.partition_aliases[0]])
                    break
                except CalledProcessError:
                    mountpoint = disk.mountpoint
                    if mountpoint and mountpoint in cls._local_client.run(
                        ['mount']):
                        # Some OSes have auto-mount functionality making mkfs.xfs to fail when the mountpoint has already been mounted
                        # This can occur when the exact same partition gets created on the device
                        already_mounted = True
                        if mountpoint.startswith('/mnt/alba-asd'):
                            cls._local_client.run(
                                'rm -rf {0}/*'.format(mountpoint),
                                allow_insecure=True)
                        cls._logger.warning(
                            'Device has already been used by ALBA, re-using mountpoint {0}'
                            .format(mountpoint))
                        break
            cls._logger.info('Partition for disk {0} not ready yet'.format(
                disk.name))
            cls.sync_disks()
            time.sleep(0.2)
            counter += 1
            if counter > 10:
                raise RuntimeError(
                    'Partition for disk {0} not ready in 2 seconds'.format(
                        disk.name))

        # Create mountpoint and mount
        cls._local_client.run(['mkdir', '-p', mountpoint])
        FSTab.add(partition_aliases=[disk.partition_aliases[0]],
                  mountpoint=mountpoint)
        if already_mounted is False:
            cls._local_client.run(['mount', mountpoint])
        cls.sync_disks()
        cls._local_client.run(['chown', '-R', 'alba:alba', mountpoint])
        cls._logger.info('Prepare disk {0} complete'.format(disk.name))

    @classmethod
    def clean_disk(cls, disk):
        """
        Removes the given disk
        :param disk: Disk object to clean
        :type disk: source.dal.objects.disk.Disk
        :return: None
        """
        if disk.usable is False:
            raise RuntimeError('Cannot clean disk {0}'.format(disk.name))
        cls._logger.info('Cleaning disk {0}'.format(disk.name))

        FSTab.remove(disk.partition_aliases)
        if disk.mountpoint is not None:
            umount_cmd = ['umount', disk.mountpoint]
            try:
                cls._local_client.run(umount_cmd)
                cls._local_client.dir_delete(disk.mountpoint)
            except Exception:
                cls._logger.exception(
                    'Failure to umount or delete the mountpoint')
                raise
        try:
            cls._local_client.run(
                ['parted', disk.aliases[0], '-s', 'mklabel', 'gpt'])
        except CalledProcessError:
            # Wiping the partition is a nice-to-have and might fail when a disk is e.g. unavailable
            pass
        cls.sync_disks()
        cls._locate(device_alias=disk.aliases[0], start=True)
        cls._logger.info('Clean disk {0} complete'.format(disk.name))

    @classmethod
    def remount_disk(cls, disk):
        """
        Remount the disk
        :param disk: Disk object to remount
        :type disk: source.dal.objects.disk.Disk
        :return: None
        """
        if disk.usable is False:
            raise RuntimeError('Cannot remount disk {0}'.format(disk.name))

        cls._logger.info('Remounting disk {0}'.format(disk.name))
        cls._local_client.run(['umount', '-l', disk.mountpoint],
                              timeout=10,
                              allow_nonzero=True)
        cls._local_client.run(['mount', disk.mountpoint],
                              timeout=10,
                              allow_nonzero=True)
        cls._logger.info('Remounting disk {0} complete'.format(disk.name))

    @classmethod
    def scan_controllers(cls):
        """
        Scan the disk controller(s)
        :return: None
        """
        cls._logger.info('Scanning controllers')
        controllers = {}
        has_storecli = cls._local_client.run(['which', 'storcli64'],
                                             allow_nonzero=True).strip() != ''
        if has_storecli is True:
            controller_info = json.loads(
                cls._local_client.run(
                    ['storcli64', '/call/eall/sall', 'show', 'all', 'J']))
            for controller in controller_info['Controllers']:
                if controller['Command Status']['Status'] == 'Failure':
                    continue
                data = controller['Response Data']
                drive_locations = set(
                    drive.split(' ')[1] for drive in data.keys())
                for location in drive_locations:
                    if data['Drive {0}'.format(
                            location)][0]['State'] == 'JBOD':
                        wwn = data['Drive {0} - Detailed Information'.format(
                            location)]['Drive {0} Device attributes'.format(
                                location)]['WWN']
                        controllers[wwn] = ('storcli64', location)
        cls.controllers = controllers
        cls._logger.info('Scan complete')

    @classmethod
    def _locate(cls, device_alias, start):
        """
        Locate the disk on the controller
        :param device_alias: Alias for the device  (eg: '/dev/disk/by-path/pci-0000:03:00.0-sas-0x5000c29f4cf04566-lun-0' or 'pci-0000:03:00.0-sas-0x5000c29f4cf04566-lun-0')
        :type device_alias: str
        :param start: True to start locating, False otherwise
        :type start: bool
        :return: None
        """
        if cls.controllers == {}:
            cls.scan_controllers()
        for wwn in cls.controllers:
            if device_alias and device_alias.endswith(wwn):
                controller_type, location = cls.controllers[wwn]
                if controller_type == 'storcli64':
                    cls._logger.info('Location {0} for {1}'.format(
                        'start' if start is True else 'stop', location))
                    cls._local_client.run([
                        'storcli64', location,
                        'start' if start is True else 'stop', 'locate'
                    ])