When updating a Fuel plugin which is the new format, the json response contains 'id' at the root of the json response. To support the old format I've updated code to retrieve 'id' from the 'message' object. Closes-Bug: #1565883 Change-Id: Ie411ef322fa9ebc5887fef9db53d4318b99ae857
523 lines
16 KiB
Python
523 lines
16 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 json
|
|
import os
|
|
import shutil
|
|
import subprocess
|
|
import sys
|
|
import tarfile
|
|
|
|
from distutils.version import StrictVersion
|
|
|
|
import six
|
|
import yaml
|
|
|
|
from fuelclient.cli import error
|
|
from fuelclient.objects import base
|
|
from fuelclient import utils
|
|
|
|
IS_MASTER = None
|
|
FUEL_PACKAGE = 'fuel'
|
|
PLUGINS_PATH = '/var/www/nailgun/plugins/'
|
|
METADATA_MASK = '/var/www/nailgun/plugins/*/metadata.yaml'
|
|
|
|
|
|
def raise_error_if_not_master():
|
|
"""Raises error if it's not Fuel master
|
|
|
|
:raises: error.WrongEnvironmentError
|
|
"""
|
|
msg_tail = 'Action can be performed from Fuel master node only.'
|
|
global IS_MASTER
|
|
if IS_MASTER is None:
|
|
IS_MASTER = False
|
|
rpm_exec = utils.find_exec('rpm')
|
|
if not rpm_exec:
|
|
msg = 'Command "rpm" not found. ' + msg_tail
|
|
raise error.WrongEnvironmentError(msg)
|
|
command = [rpm_exec, '-q', FUEL_PACKAGE]
|
|
p = subprocess.Popen(
|
|
command,
|
|
stdout=subprocess.PIPE,
|
|
stderr=subprocess.PIPE)
|
|
p.communicate()
|
|
if p.poll() == 0:
|
|
IS_MASTER = True
|
|
if not IS_MASTER:
|
|
msg = 'Package "fuel" is not installed. ' + msg_tail
|
|
raise error.WrongEnvironmentError(msg)
|
|
|
|
|
|
def master_only(f):
|
|
"""Decorator for the method, which raises error, if method
|
|
is called on the node which is not Fuel master
|
|
"""
|
|
@six.wraps(f)
|
|
def print_message(*args, **kwargs):
|
|
raise_error_if_not_master()
|
|
return f(*args, **kwargs)
|
|
|
|
return print_message
|
|
|
|
|
|
@six.add_metaclass(abc.ABCMeta)
|
|
class BasePlugin(object):
|
|
|
|
@abc.abstractmethod
|
|
def install(cls, plugin_path, force=False):
|
|
"""Installs plugin package
|
|
"""
|
|
|
|
@abc.abstractmethod
|
|
def update(cls, plugin_path):
|
|
"""Updates the plugin
|
|
"""
|
|
|
|
@abc.abstractmethod
|
|
def remove(cls, plugin_name, plugin_version):
|
|
"""Removes the plugin from file system
|
|
"""
|
|
|
|
@abc.abstractmethod
|
|
def downgrade(cls, plugin_path):
|
|
"""Downgrades the plugin
|
|
"""
|
|
|
|
@abc.abstractmethod
|
|
def name_from_file(cls, file_path):
|
|
"""Retrieves name from plugin package
|
|
"""
|
|
|
|
@abc.abstractmethod
|
|
def version_from_file(cls, file_path):
|
|
"""Retrieves version from plugin package
|
|
"""
|
|
|
|
|
|
class PluginV1(BasePlugin):
|
|
|
|
metadata_config = 'metadata.yaml'
|
|
|
|
def deprecated(f):
|
|
"""Prints deprecation warning for old plugins
|
|
"""
|
|
@six.wraps(f)
|
|
def print_message(*args, **kwargs):
|
|
six.print_(
|
|
'DEPRECATION WARNING: The plugin has old 1.0 package format, '
|
|
'this format does not support many features, such as '
|
|
'plugins updates, find plugin in new format or migrate '
|
|
'and rebuild this one.', file=sys.stderr)
|
|
return f(*args, **kwargs)
|
|
|
|
return print_message
|
|
|
|
@classmethod
|
|
@master_only
|
|
@deprecated
|
|
def install(cls, plugin_path, force=False):
|
|
plugin_tar = tarfile.open(plugin_path, 'r')
|
|
try:
|
|
plugin_tar.extractall(PLUGINS_PATH)
|
|
finally:
|
|
plugin_tar.close()
|
|
|
|
@classmethod
|
|
@master_only
|
|
@deprecated
|
|
def remove(cls, plugin_name, plugin_version):
|
|
plugin_path = os.path.join(
|
|
PLUGINS_PATH, '{0}-{1}'.format(plugin_name, plugin_version))
|
|
shutil.rmtree(plugin_path)
|
|
|
|
@classmethod
|
|
def update(cls, _):
|
|
raise error.BadDataException(
|
|
'Update action is not supported for old plugins with '
|
|
'package version "1.0.0", you can install your plugin '
|
|
'or use newer plugin format.')
|
|
|
|
@classmethod
|
|
def downgrade(cls, _):
|
|
raise error.BadDataException(
|
|
'Downgrade action is not supported for old plugins with '
|
|
'package version "1.0.0", you can install your plugin '
|
|
'or use newer plugin format.')
|
|
|
|
@classmethod
|
|
def name_from_file(cls, file_path):
|
|
"""Retrieves plugin name from plugin archive.
|
|
|
|
:param str plugin_path: path to the plugin
|
|
:returns: plugin name
|
|
"""
|
|
return cls._get_metadata(file_path)['name']
|
|
|
|
@classmethod
|
|
def version_from_file(cls, file_path):
|
|
"""Retrieves plugin version from plugin archive.
|
|
|
|
:param str plugin_path: path to the plugin
|
|
:returns: plugin version
|
|
"""
|
|
return cls._get_metadata(file_path)['version']
|
|
|
|
@classmethod
|
|
def _get_metadata(cls, plugin_path):
|
|
"""Retrieves metadata from plugin archive
|
|
|
|
:param str plugin_path: path to the plugin
|
|
:returns: metadata from the plugin
|
|
"""
|
|
plugin_tar = tarfile.open(plugin_path, 'r')
|
|
|
|
try:
|
|
for member_name in plugin_tar.getnames():
|
|
if cls.metadata_config in member_name:
|
|
return yaml.load(
|
|
plugin_tar.extractfile(member_name).read())
|
|
finally:
|
|
plugin_tar.close()
|
|
|
|
|
|
class PluginV2(BasePlugin):
|
|
|
|
@classmethod
|
|
@master_only
|
|
def install(cls, plugin_path, force=False):
|
|
if force:
|
|
utils.exec_cmd(
|
|
'yum -y install --disablerepo=\'*\' {0} || '
|
|
'yum -y reinstall --disablerepo=\'*\' {0}'
|
|
.format(plugin_path))
|
|
else:
|
|
utils.exec_cmd('yum -y install --disablerepo=\'*\' {0}'
|
|
.format(plugin_path))
|
|
|
|
@classmethod
|
|
@master_only
|
|
def remove(cls, name, version):
|
|
rpm_name = '{0}-{1}'.format(name, utils.major_plugin_version(version))
|
|
utils.exec_cmd('yum -y remove {0}'.format(rpm_name))
|
|
|
|
@classmethod
|
|
@master_only
|
|
def update(cls, plugin_path):
|
|
utils.exec_cmd('yum -y update {0}'.format(plugin_path))
|
|
|
|
@classmethod
|
|
@master_only
|
|
def downgrade(cls, plugin_path):
|
|
utils.exec_cmd('yum -y downgrade {0}'.format(plugin_path))
|
|
|
|
@classmethod
|
|
def name_from_file(cls, file_path):
|
|
"""Retrieves plugin name from RPM. RPM name contains
|
|
the version of the plugin, which should be removed.
|
|
|
|
:param str file_path: path to rpm file
|
|
:returns: name of the plugin
|
|
"""
|
|
for line in utils.exec_cmd_iterator(
|
|
"rpm -qp --queryformat '%{{name}}' {0}".format(file_path)):
|
|
name = line
|
|
break
|
|
|
|
return cls._remove_major_plugin_version(name)
|
|
|
|
@classmethod
|
|
def version_from_file(cls, file_path):
|
|
"""Retrieves plugin version from RPM.
|
|
|
|
:param str file_path: path to rpm file
|
|
:returns: version of the plugin
|
|
"""
|
|
for line in utils.exec_cmd_iterator(
|
|
"rpm -qp --queryformat '%{{version}}' {0}".format(file_path)):
|
|
version = line
|
|
break
|
|
|
|
return version
|
|
|
|
@classmethod
|
|
def _remove_major_plugin_version(cls, name):
|
|
"""Removes the version from plugin name.
|
|
Here is an example: "name-1.0" -> "name"
|
|
|
|
:param str name: plugin name
|
|
:returns: the name withot version
|
|
"""
|
|
name_wo_version = name
|
|
|
|
if '-' in name_wo_version:
|
|
name_wo_version = '-'.join(name.split('-')[:-1])
|
|
|
|
return name_wo_version
|
|
|
|
|
|
class Plugins(base.BaseObject):
|
|
|
|
class_api_path = 'plugins/'
|
|
class_instance_path = 'plugins/{id}'
|
|
|
|
@classmethod
|
|
def register(cls, name, version, force=False):
|
|
"""Tries to find plugin on file system, creates
|
|
it in API service if it exists.
|
|
|
|
:param str name: plugin name
|
|
:param str version: plugin version
|
|
:param bool force: if True updates meta information
|
|
about the plugin even it does not
|
|
support updates
|
|
"""
|
|
metadata = None
|
|
for m in utils.glob_and_parse_yaml(METADATA_MASK):
|
|
if m.get('version') == version and \
|
|
m.get('name') == name:
|
|
metadata = m
|
|
break
|
|
|
|
if not metadata:
|
|
raise error.BadDataException(
|
|
'Plugin {0} with version {1} does '
|
|
'not exist, install it and try again'.format(
|
|
name, version))
|
|
|
|
return cls.update_or_create(metadata, force=force)
|
|
|
|
@classmethod
|
|
def sync(cls, plugin_ids=None):
|
|
"""Checks all of the plugins on file systems,
|
|
and makes sure that they have consistent information
|
|
in API service.
|
|
|
|
:params plugin_ids: list of ids for plugins which should be synced
|
|
:type plugin_ids: list
|
|
:returns: None
|
|
"""
|
|
post_data = None
|
|
if plugin_ids is not None:
|
|
post_data = {'ids': plugin_ids}
|
|
|
|
cls.connection.post_request(
|
|
api='plugins/sync/', data=post_data)
|
|
|
|
@classmethod
|
|
def unregister(cls, name, version):
|
|
"""Removes the plugin from API service
|
|
|
|
:param str name: plugin name
|
|
:param str version: plugin version
|
|
"""
|
|
plugin = cls.get_plugin(name, version)
|
|
return cls.connection.delete_request(
|
|
cls.class_instance_path.format(**plugin))
|
|
|
|
@classmethod
|
|
def install(cls, plugin_path, force=False):
|
|
"""Installs the package, and creates data in API service
|
|
|
|
:param str plugin_path: Name of plugin file
|
|
:param bool force: Updates existent plugin even if it is not updatable
|
|
:return: Plugins information
|
|
:rtype: dict
|
|
"""
|
|
plugin = cls.make_obj_by_file(plugin_path)
|
|
|
|
name = plugin.name_from_file(plugin_path)
|
|
version = plugin.version_from_file(plugin_path)
|
|
|
|
plugin.install(plugin_path, force=force)
|
|
response = cls.register(name, version, force=force)
|
|
|
|
return response
|
|
|
|
@classmethod
|
|
def remove(cls, plugin_name, plugin_version):
|
|
"""Removes the package, and updates data in API service
|
|
|
|
:param str name: plugin name
|
|
:param str version: plugin version
|
|
"""
|
|
plugin = cls.make_obj_by_name(plugin_name, plugin_version)
|
|
cls.unregister(plugin_name, plugin_version)
|
|
return plugin.remove(plugin_name, plugin_version)
|
|
|
|
@classmethod
|
|
def update(cls, plugin_path):
|
|
"""Updates the package, and updates data in API service
|
|
|
|
:param str plugin_path: path to the plugin
|
|
"""
|
|
plugin = cls.make_obj_by_file(plugin_path)
|
|
|
|
name = plugin.name_from_file(plugin_path)
|
|
version = plugin.version_from_file(plugin_path)
|
|
|
|
plugin.update(plugin_path)
|
|
return cls.register(name, version)
|
|
|
|
@classmethod
|
|
def downgrade(cls, plugin_path):
|
|
"""Downgrades the package, and updates data in API service
|
|
|
|
:param str plugin_path: path to the plugin
|
|
"""
|
|
plugin = cls.make_obj_by_file(plugin_path)
|
|
|
|
name = plugin.name_from_file(plugin_path)
|
|
version = plugin.version_from_file(plugin_path)
|
|
|
|
plugin.downgrade(plugin_path)
|
|
return cls.register(name, version)
|
|
|
|
@classmethod
|
|
def make_obj_by_name(cls, name, version):
|
|
"""Finds appropriate plugin class version,
|
|
by plugin version and name.
|
|
|
|
:param str name:
|
|
:param str version:
|
|
:returns: plugin class
|
|
:raises: error.BadDataException unsupported package version
|
|
"""
|
|
plugin = cls.get_plugin(name, version)
|
|
package_version = plugin['package_version']
|
|
|
|
if StrictVersion('1.0.0') <= \
|
|
StrictVersion(package_version) < \
|
|
StrictVersion('2.0.0'):
|
|
return PluginV1
|
|
elif StrictVersion('2.0.0') <= StrictVersion(package_version):
|
|
return PluginV2
|
|
|
|
raise error.BadDataException(
|
|
'Plugin {0}=={1} has unsupported package version {2}'.format(
|
|
name, version, package_version))
|
|
|
|
@classmethod
|
|
def make_obj_by_file(cls, file_path):
|
|
"""Finds appropriate plugin class version,
|
|
by plugin file.
|
|
|
|
:param str file_path: plugin path
|
|
:returns: plugin class
|
|
:raises: error.BadDataException unsupported package version
|
|
"""
|
|
_, ext = os.path.splitext(file_path)
|
|
|
|
if ext == '.fp':
|
|
return PluginV1
|
|
elif ext == '.rpm':
|
|
return PluginV2
|
|
|
|
raise error.BadDataException(
|
|
'Plugin {0} has unsupported format {1}'.format(
|
|
file_path, ext))
|
|
|
|
@classmethod
|
|
def update_or_create(cls, metadata, force=False):
|
|
"""Try to update existent plugin or create new one.
|
|
|
|
:param dict metadata: plugin information
|
|
:param bool force: updates existent plugin even if
|
|
it is not updatable
|
|
"""
|
|
# Try to update plugin
|
|
plugin_for_update = cls.get_plugin_for_update(metadata)
|
|
if plugin_for_update:
|
|
url = cls.class_instance_path.format(id=plugin_for_update['id'])
|
|
resp = cls.connection.put_request(url, metadata)
|
|
return resp
|
|
|
|
# If plugin is not updatable it means that we should
|
|
# create new instance in Nailgun
|
|
resp_raw = cls.connection.post_request_raw(
|
|
cls.class_api_path, metadata)
|
|
resp = resp_raw.json()
|
|
|
|
if resp_raw.status_code == 409 and force:
|
|
# Replace plugin information
|
|
message = json.loads(resp['message'])
|
|
url = cls.class_instance_path.format(id=message['id'])
|
|
resp = cls.connection.put_request(url, metadata)
|
|
elif resp_raw.status_code == 409:
|
|
error.exit_with_error(
|
|
"Nothing to do: %(title)s, version "
|
|
"%(package_version)s, does not update "
|
|
"installed plugin." % metadata)
|
|
else:
|
|
resp_raw.raise_for_status()
|
|
|
|
return resp
|
|
|
|
@classmethod
|
|
def get_plugin_for_update(cls, metadata):
|
|
"""Retrieves plugins which can be updated
|
|
|
|
:param dict metadata: plugin metadata
|
|
:returns: dict with plugin which can be updated or None
|
|
"""
|
|
if not cls.is_updatable(metadata['package_version']):
|
|
return
|
|
|
|
plugins = [p for p in cls.get_all_data()
|
|
if (p['name'] == metadata['name'] and
|
|
cls.is_updatable(p['package_version']) and
|
|
utils.major_plugin_version(metadata['version']) ==
|
|
utils.major_plugin_version(p['version']))]
|
|
|
|
plugin = None
|
|
if plugins:
|
|
# List should contain only one plugin, but just
|
|
# in case we make sure that we get plugin with
|
|
# higher version
|
|
plugin = sorted(
|
|
plugins,
|
|
key=lambda p: StrictVersion(p['version']))[0]
|
|
|
|
return plugin
|
|
|
|
@classmethod
|
|
def is_updatable(cls, package_version):
|
|
"""Checks if plugin's package version supports updates.
|
|
|
|
:param str package_version: package version of the plugin
|
|
:returns: True if plugin can be updated
|
|
False if plugin cannot be updated
|
|
"""
|
|
return StrictVersion('2.0.0') <= StrictVersion(package_version)
|
|
|
|
@classmethod
|
|
def get_plugin(cls, name, version):
|
|
"""Returns plugin fetched by name and version.
|
|
|
|
:param str name: plugin name
|
|
:param str version: plugin version
|
|
:returns: dictionary with plugin data
|
|
:raises: error.BadDataException if no plugin was found
|
|
"""
|
|
plugins = [p for p in cls.get_all_data()
|
|
if (p['name'], p['version']) == (name, version)]
|
|
|
|
if not plugins:
|
|
raise error.BadDataException(
|
|
'Plugin "{name}" with version {version}, does '
|
|
'not exist'.format(name=name, version=version))
|
|
|
|
return plugins[0]
|