347 lines
11 KiB
Python
347 lines
11 KiB
Python
# Copyright 2014 Mirantis, Inc.
|
|
#
|
|
# Licensed under the Apache License, Version 2.0 (the "License"); you may
|
|
# not use this file except in compliance with the License. You may obtain
|
|
# a copy of the License at
|
|
#
|
|
# http://www.apache.org/licenses/LICENSE-2.0
|
|
#
|
|
# Unless required by applicable law or agreed to in writing, software
|
|
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
|
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
|
# License for the specific language governing permissions and limitations
|
|
# under the License.
|
|
|
|
import abc
|
|
import copy
|
|
import glob
|
|
import os
|
|
|
|
from distutils.version import StrictVersion
|
|
from urlparse import urljoin
|
|
|
|
import six
|
|
import yaml
|
|
|
|
from nailgun.errors import errors
|
|
from nailgun.logger import logger
|
|
from nailgun.objects.plugin import ClusterPlugins
|
|
from nailgun.objects.plugin import Plugin
|
|
from nailgun.settings import settings
|
|
|
|
|
|
@six.add_metaclass(abc.ABCMeta)
|
|
class PluginAdapterBase(object):
|
|
"""Implements wrapper for plugin db model configuration files logic
|
|
|
|
1. Uploading plugin provided cluster attributes
|
|
2. Uploading tasks
|
|
3. Enabling/Disabling of plugin based on cluster attributes
|
|
4. Providing repositories/deployment scripts related info to clients
|
|
"""
|
|
|
|
environment_config_name = 'environment_config.yaml'
|
|
plugin_metadata = 'metadata.yaml'
|
|
task_config_name = 'tasks.yaml'
|
|
|
|
def __init__(self, plugin):
|
|
self.plugin = plugin
|
|
self.plugin_path = os.path.join(
|
|
settings.PLUGINS_PATH,
|
|
self.path_name)
|
|
self.tasks = []
|
|
|
|
@abc.abstractmethod
|
|
def path_name(self):
|
|
"""A name which is used to create path to plugin scripts and repos"""
|
|
|
|
def sync_metadata_to_db(self):
|
|
"""Sync metadata from config yaml files into DB"""
|
|
metadata_file_path = os.path.join(
|
|
self.plugin_path, self.plugin_metadata)
|
|
|
|
metadata = self._load_config(metadata_file_path) or {}
|
|
Plugin.update(self.plugin, metadata)
|
|
|
|
db_config_metadata_mapping = {
|
|
'attributes_metadata': self.environment_config_name,
|
|
'tasks': self.task_config_name
|
|
}
|
|
|
|
self._update_plugin(db_config_metadata_mapping)
|
|
|
|
def _load_config(self, config):
|
|
if os.access(config, os.R_OK):
|
|
with open(config, "r") as conf:
|
|
try:
|
|
return yaml.safe_load(conf.read())
|
|
except yaml.YAMLError as exc:
|
|
logger.warning(exc)
|
|
raise errors.ParseError(
|
|
'Problem with loading YAML file {0}'.format(config))
|
|
else:
|
|
logger.warning("Config {0} is not readable.".format(config))
|
|
|
|
def _update_plugin(self, mapping):
|
|
data_to_update = {}
|
|
|
|
for attribute, config in six.iteritems(mapping):
|
|
config_file_path = os.path.join(self.plugin_path, config)
|
|
attribute_data = self._load_config(config_file_path)
|
|
# Plugin columns have constraints for nullable data, so
|
|
# we need to check it
|
|
if attribute_data:
|
|
if attribute == 'attributes_metadata':
|
|
attribute_data = attribute_data['attributes']
|
|
data_to_update[attribute] = attribute_data
|
|
|
|
Plugin.update(self.plugin, data_to_update)
|
|
|
|
def _load_tasks(self, config):
|
|
data = self._load_config(config)
|
|
for item in data:
|
|
# backward compatibility for plugins added in version 6.0,
|
|
# and it is expected that task with role: [controller]
|
|
# will be executed on all controllers
|
|
|
|
if (StrictVersion(self.plugin.package_version)
|
|
== StrictVersion('1.0')
|
|
and isinstance(item['role'], list)
|
|
and 'controller' in item['role']):
|
|
item['role'].append('primary-controller')
|
|
return data
|
|
|
|
def set_cluster_tasks(self):
|
|
"""Load plugins provided tasks and set them to instance tasks variable
|
|
|
|
Provided tasks are loaded from tasks config file.
|
|
"""
|
|
task_yaml = os.path.join(
|
|
self.plugin_path, self.task_config_name)
|
|
if os.path.exists(task_yaml):
|
|
self.tasks = self._load_tasks(task_yaml)
|
|
|
|
def filter_tasks(self, tasks, stage):
|
|
filtered = []
|
|
for task in tasks:
|
|
if stage and stage == task.get('stage'):
|
|
filtered.append(task)
|
|
return filtered
|
|
|
|
@property
|
|
def plugin_release_versions(self):
|
|
if not self.plugin.releases:
|
|
return set()
|
|
return set([rel['version'] for rel in self.plugin.releases])
|
|
|
|
@property
|
|
def name(self):
|
|
return self.plugin.name
|
|
|
|
@property
|
|
def full_name(self):
|
|
return u'{0}-{1}'.format(self.plugin.name, self.plugin.version)
|
|
|
|
@property
|
|
def slaves_scripts_path(self):
|
|
return settings.PLUGINS_SLAVES_SCRIPTS_PATH.format(
|
|
plugin_name=self.path_name)
|
|
|
|
@property
|
|
def deployment_tasks(self):
|
|
deployment_tasks = []
|
|
for task in self.plugin.deployment_tasks:
|
|
if task.get('parameters'):
|
|
task['parameters'].setdefault('cwd', self.slaves_scripts_path)
|
|
deployment_tasks.append(task)
|
|
return deployment_tasks
|
|
|
|
@property
|
|
def volumes_metadata(self):
|
|
return self.plugin.volumes_metadata
|
|
|
|
@property
|
|
def components_metadata(self):
|
|
return self.plugin.components_metadata
|
|
|
|
@property
|
|
def releases(self):
|
|
return self.plugin.releases
|
|
|
|
@property
|
|
def normalized_roles_metadata(self):
|
|
"""Block plugin disabling if nodes with plugin-provided roles exist"""
|
|
result = {}
|
|
for role, meta in six.iteritems(self.plugin.roles_metadata):
|
|
condition = "settings:{0}.metadata.enabled == false".format(
|
|
self.plugin.name)
|
|
meta = copy.copy(meta)
|
|
meta['restrictions'] = [condition] + meta.get('restrictions', [])
|
|
result[role] = meta
|
|
|
|
return result
|
|
|
|
def get_release_info(self, release):
|
|
"""Get plugin release information which corresponds to given release"""
|
|
rel_os = release.operating_system.lower()
|
|
version = release.version
|
|
|
|
release_info = filter(
|
|
lambda r: (
|
|
r['os'] == rel_os and
|
|
ClusterPlugins.is_release_version_compatible(version,
|
|
r['version'])),
|
|
self.plugin.releases)
|
|
|
|
return release_info[0]
|
|
|
|
def repo_files(self, cluster):
|
|
release_info = self.get_release_info(cluster.release)
|
|
repo_path = os.path.join(
|
|
settings.PLUGINS_PATH,
|
|
self.path_name,
|
|
release_info['repository_path'],
|
|
'*')
|
|
return glob.glob(repo_path)
|
|
|
|
def repo_url(self, cluster):
|
|
release_info = self.get_release_info(cluster.release)
|
|
repo_base = settings.PLUGINS_REPO_URL.format(
|
|
master_ip=settings.MASTER_IP,
|
|
plugin_name=self.path_name)
|
|
|
|
return urljoin(repo_base, release_info['repository_path'])
|
|
|
|
def master_scripts_path(self, cluster):
|
|
release_info = self.get_release_info(cluster.release)
|
|
# NOTE(eli): we cannot user urljoin here, because it
|
|
# works wrong, if protocol is rsync
|
|
base_url = settings.PLUGINS_SLAVES_RSYNC.format(
|
|
master_ip=settings.MASTER_IP,
|
|
plugin_name=self.path_name)
|
|
return '{0}{1}'.format(
|
|
base_url,
|
|
release_info['deployment_scripts_path'])
|
|
|
|
|
|
class PluginAdapterV1(PluginAdapterBase):
|
|
"""Plugins attributes class for package version 1.0.0"""
|
|
|
|
@property
|
|
def path_name(self):
|
|
"""Returns a name and full version
|
|
|
|
e.g. if there is a plugin with name "plugin_name" and version
|
|
is "1.0.0", the method returns "plugin_name-1.0.0"
|
|
"""
|
|
return self.full_name
|
|
|
|
|
|
class PluginAdapterV2(PluginAdapterBase):
|
|
"""Plugins attributes class for package version 2.0.0"""
|
|
|
|
@property
|
|
def path_name(self):
|
|
"""Returns a name and major version of the plugin
|
|
|
|
e.g. if there is a plugin with name "plugin_name" and version
|
|
is "1.0.0", the method returns "plugin_name-1.0".
|
|
|
|
It's different from previous version because in previous
|
|
version we did not have plugin updates, in 2.0.0 version
|
|
we should expect different plugin path.
|
|
|
|
See blueprint: https://blueprints.launchpad.net/fuel/+spec
|
|
/plugins-security-fixes-delivery
|
|
"""
|
|
return u'{0}-{1}'.format(self.plugin.name, self._major_version)
|
|
|
|
@property
|
|
def _major_version(self):
|
|
"""Returns major version of plugin's version
|
|
|
|
e.g. if plugin has 1.2.3 version, the method returns 1.2
|
|
"""
|
|
version_tuple = StrictVersion(self.plugin.version).version
|
|
major = '.'.join(map(str, version_tuple[:2]))
|
|
|
|
return major
|
|
|
|
|
|
class PluginAdapterV3(PluginAdapterV2):
|
|
"""Plugin wrapper class for package version 3.0.0"""
|
|
|
|
node_roles_config_name = 'node_roles.yaml'
|
|
volumes_config_name = 'volumes.yaml'
|
|
deployment_tasks_config_name = 'deployment_tasks.yaml'
|
|
network_roles_config_name = 'network_roles.yaml'
|
|
|
|
def sync_metadata_to_db(self):
|
|
"""Sync metadata from all config yaml files to DB"""
|
|
super(PluginAdapterV3, self).sync_metadata_to_db()
|
|
|
|
db_config_metadata_mapping = {
|
|
'roles_metadata': self.node_roles_config_name,
|
|
'volumes_metadata': self.volumes_config_name,
|
|
'network_roles_metadata': self.network_roles_config_name,
|
|
'deployment_tasks': self.deployment_tasks_config_name,
|
|
}
|
|
|
|
self._update_plugin(db_config_metadata_mapping)
|
|
|
|
|
|
class PluginAdapterV4(PluginAdapterV3):
|
|
"""Plugin wrapper class for package version 4.0.0"""
|
|
|
|
components = 'components.yaml'
|
|
|
|
def sync_metadata_to_db(self):
|
|
super(PluginAdapterV4, self).sync_metadata_to_db()
|
|
|
|
db_config_metadata_mapping = {
|
|
'components_metadata': self.components
|
|
}
|
|
|
|
self._update_plugin(db_config_metadata_mapping)
|
|
|
|
|
|
class PluginAdapterV5(PluginAdapterV4):
|
|
"""Plugin wrapper class for package version 5.0.0"""
|
|
|
|
|
|
__version_mapping = {
|
|
'1.0.': PluginAdapterV1,
|
|
'2.0.': PluginAdapterV2,
|
|
'3.0.': PluginAdapterV3,
|
|
'4.0.': PluginAdapterV4,
|
|
'5.0.': PluginAdapterV5,
|
|
}
|
|
|
|
|
|
def wrap_plugin(plugin):
|
|
"""Creates plugin object with specific class version
|
|
|
|
:param plugin: plugin db object
|
|
:returns: cluster attribute object
|
|
"""
|
|
package_version = plugin.package_version
|
|
|
|
attr_class = None
|
|
|
|
# Filter by major version
|
|
for version, klass in six.iteritems(__version_mapping):
|
|
if package_version.startswith(version):
|
|
attr_class = klass
|
|
break
|
|
|
|
if not attr_class:
|
|
supported_versions = ', '.join(__version_mapping.keys())
|
|
|
|
raise errors.PackageVersionIsNotCompatible(
|
|
'Plugin id={0} package_version={1} '
|
|
'is not supported by Nailgun, currently '
|
|
'supported versions {2}'.format(
|
|
plugin.id, package_version, supported_versions))
|
|
|
|
return attr_class(plugin)
|