Merge "Add New Relic License module driver"
This commit is contained in:
commit
6e2922bc49
|
@ -0,0 +1,6 @@
|
|||
---
|
||||
features:
|
||||
- Added a module driver for New Relics licenses.
|
||||
This allows activation of any New Relic software
|
||||
that is installed on the image. Bug 1571711
|
||||
|
|
@ -38,6 +38,7 @@ trove.api.extensions =
|
|||
|
||||
trove.guestagent.module.drivers =
|
||||
ping = trove.guestagent.module.drivers.ping_driver:PingDriver
|
||||
new_relic_license = trove.guestagent.module.drivers.new_relic_license_driver:NewRelicLicenseDriver
|
||||
|
||||
# These are for backwards compatibility with Havana notification_driver configuration values
|
||||
oslo.messaging.notify.drivers =
|
||||
|
|
|
@ -403,7 +403,7 @@ common_opts = [
|
|||
'become alive.'),
|
||||
cfg.StrOpt('module_aes_cbc_key', default='module_aes_cbc_key',
|
||||
help='OpenSSL aes_cbc key for module encryption.'),
|
||||
cfg.ListOpt('module_types', default=['ping'],
|
||||
cfg.ListOpt('module_types', default=['ping', 'new_relic_license'],
|
||||
help='A list of module types supported. A module type '
|
||||
'corresponds to the name of a ModuleDriver.'),
|
||||
cfg.StrOpt('guest_log_container_name',
|
||||
|
|
|
@ -528,6 +528,11 @@ class ModuleAccessForbidden(Forbidden):
|
|||
"options. %(options)s")
|
||||
|
||||
|
||||
class ModuleInvalid(Forbidden):
|
||||
|
||||
message = _("The module you are applying is invalid: %(reason)s")
|
||||
|
||||
|
||||
class ClusterNotFound(NotFound):
|
||||
message = _("Cluster '%(cluster)s' cannot be found.")
|
||||
|
||||
|
|
|
@ -15,9 +15,13 @@
|
|||
#
|
||||
|
||||
import abc
|
||||
import functools
|
||||
import re
|
||||
import six
|
||||
|
||||
from trove.common import cfg
|
||||
from trove.common import exception
|
||||
from trove.common.i18n import _
|
||||
|
||||
|
||||
CONF = cfg.CONF
|
||||
|
@ -28,21 +32,64 @@ class ModuleDriver(object):
|
|||
"""Base class that defines the contract for module drivers.
|
||||
|
||||
Note that you don't have to derive from this class to have a valid
|
||||
driver; it is purely a convenience.
|
||||
driver; it is purely a convenience. Any class that adheres to the
|
||||
'interface' as dictated by this class' abstractmethod decorators
|
||||
(and other methods such as get_type, get_name and configure)
|
||||
will work.
|
||||
"""
|
||||
def __init__(self):
|
||||
super(ModuleDriver, self).__init__()
|
||||
|
||||
# This is used to store any message args to be substituted by
|
||||
# the output decorator when logging/returning messages.
|
||||
self._module_message_args = {}
|
||||
self._message_args = None
|
||||
self._generated_name = None
|
||||
|
||||
@property
|
||||
def message_args(self):
|
||||
"""Return a dict of message args that can be used to enhance
|
||||
the output decorator messages. This shouldn't be overridden; use
|
||||
self.message_args = <dict> instead to append values.
|
||||
"""
|
||||
if not self._message_args:
|
||||
self._message_args = {
|
||||
'name': self.get_name(),
|
||||
'type': self.get_type()}
|
||||
self._message_args.update(self._module_message_args)
|
||||
return self._message_args
|
||||
|
||||
@message_args.setter
|
||||
def message_args(self, values):
|
||||
"""Set the message args that can be used to enhance
|
||||
the output decorator messages.
|
||||
"""
|
||||
values = values or {}
|
||||
self._module_message_args = values
|
||||
self._message_args = None
|
||||
|
||||
@property
|
||||
def generated_name(self):
|
||||
if not self._generated_name:
|
||||
# Turn class name into 'module type' format.
|
||||
# For example: DoCustomWorkDriver -> do_custom_work
|
||||
temp = re.sub('(.)[Dd]river$', r'\1', self.__class__.__name__)
|
||||
temp2 = re.sub('(.)([A-Z][a-z]+)', r'\1_\2', temp)
|
||||
temp3 = re.sub('([a-z0-9])([A-Z])', r'\1_\2', temp2)
|
||||
self._generated_name = temp3.lower()
|
||||
return self._generated_name
|
||||
|
||||
def get_type(self):
|
||||
"""This is used when setting up a module in Trove, and is here for
|
||||
code clarity. It just returns the name of the driver.
|
||||
code clarity. It just returns the name of the driver by default.
|
||||
"""
|
||||
return self.get_name()
|
||||
|
||||
def get_name(self):
|
||||
"""Attempt to generate a usable name based on the class name. If
|
||||
"""Use the generated name based on the class name. If
|
||||
overridden, must be in lower-case.
|
||||
"""
|
||||
return self.__class__.__name__.lower().replace(
|
||||
'driver', '').replace(' ', '_')
|
||||
return self.generated_name
|
||||
|
||||
@abc.abstractmethod
|
||||
def get_description(self):
|
||||
|
@ -55,15 +102,104 @@ class ModuleDriver(object):
|
|||
pass
|
||||
|
||||
@abc.abstractmethod
|
||||
def apply(self, name, datastore, ds_version, data_file):
|
||||
"""Apply the data to the guest instance. Return status and message
|
||||
as a tupple.
|
||||
def apply(self, name, datastore, ds_version, data_file, admin_module):
|
||||
"""Apply the module to the guest instance. Return status and message
|
||||
as a tuple. Passes in whether the module was created with 'admin'
|
||||
privileges. This can be used as a form of access control by having
|
||||
the driver refuse to apply a module if it wasn't created with options
|
||||
that indicate that it was done by an 'admin' user.
|
||||
"""
|
||||
return False, "Not a concrete driver"
|
||||
|
||||
@abc.abstractmethod
|
||||
def remove(self, name, datastore, ds_version, data_file):
|
||||
"""Remove the data from the guest instance. Return status and message
|
||||
as a tupple.
|
||||
"""Remove the module from the guest instance. Return
|
||||
status and message as a tuple.
|
||||
"""
|
||||
return False, "Not a concrete driver"
|
||||
|
||||
def configure(self, name, datastore, ds_version, data_file):
|
||||
"""Configure the driver. This is particularly useful for adding values
|
||||
to message_args, by having a line such as: self.message_args = <dict>.
|
||||
These values will be appended to the default ones defined
|
||||
in the message_args @property.
|
||||
"""
|
||||
pass
|
||||
|
||||
|
||||
def output(log_message=None, success_message=None,
|
||||
fail_message=None):
|
||||
"""This is a decorator to trap the typical exceptions that occur
|
||||
when applying and removing modules. It returns the proper output
|
||||
corresponding to the error messages automatically. If the function
|
||||
returns output (success_flag, message) then those are returned,
|
||||
otherwise success is assumed and the success_message returned.
|
||||
Using this removes a lot of potential boiler-plate code, however
|
||||
it is not necessary.
|
||||
Keyword arguments can be used in the message string. Default
|
||||
values can be found in the message_args @property, however a
|
||||
driver can add whatever it see fit, by setting message_args
|
||||
to a dict in the configure call (see above). Thus if you set
|
||||
self.message_args = {'my_key': 'my_key_val'} then the message
|
||||
string could look like "My key is '$(my_key)s'".
|
||||
"""
|
||||
success_message = success_message or "Success"
|
||||
fail_message = fail_message or "Fail"
|
||||
|
||||
def output_decorator(func):
|
||||
"""This is the actual decorator."""
|
||||
|
||||
@functools.wraps(func)
|
||||
def wrapper(*args, **kwargs):
|
||||
"""Here's where we handle the error messages and return values
|
||||
from the actual function.
|
||||
"""
|
||||
log_msg = log_message
|
||||
success_msg = success_message
|
||||
fail_msg = fail_message
|
||||
if isinstance(args[0], ModuleDriver):
|
||||
# Try and insert any message args if they exist in the driver
|
||||
message_args = args[0].message_args
|
||||
if message_args:
|
||||
try:
|
||||
log_msg = log_msg % message_args
|
||||
success_msg = success_msg % message_args
|
||||
fail_msg = fail_msg % message_args
|
||||
except Exception:
|
||||
# if there's a problem, just log it and drive on
|
||||
LOG.warning(_("Could not apply message args: %s") %
|
||||
message_args)
|
||||
pass
|
||||
|
||||
if log_msg:
|
||||
LOG.info(log_msg)
|
||||
success = False
|
||||
try:
|
||||
rv = func(*args, **kwargs)
|
||||
if rv:
|
||||
# Use the actual values, if there are some
|
||||
success, message = rv
|
||||
else:
|
||||
success = True
|
||||
message = success_msg
|
||||
except exception.ProcessExecutionError as ex:
|
||||
message = (_("%(msg)s: %(out)s\n%(err)s") %
|
||||
{'msg': fail_msg,
|
||||
'out': ex.stdout,
|
||||
'err': ex.stderr})
|
||||
message = message.replace(': \n', ': ')
|
||||
message = message.rstrip()
|
||||
LOG.exception(message)
|
||||
except exception.TroveError as ex:
|
||||
message = (_("%(msg)s: %(err)s") %
|
||||
{'msg': fail_msg, 'err': ex._error_string})
|
||||
LOG.exception(message)
|
||||
except Exception as ex:
|
||||
message = (_("%(msg)s: %(err)s") %
|
||||
{'msg': fail_msg, 'err': ex.message})
|
||||
LOG.exception(message)
|
||||
return success, message
|
||||
|
||||
return wrapper
|
||||
|
||||
return output_decorator
|
||||
|
|
|
@ -0,0 +1,95 @@
|
|||
# Copyright 2016 Tesora, Inc.
|
||||
# All Rights Reserved.
|
||||
#
|
||||
# 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.
|
||||
#
|
||||
|
||||
from datetime import date
|
||||
|
||||
from oslo_log import log as logging
|
||||
|
||||
from trove.common import cfg
|
||||
from trove.common.i18n import _
|
||||
from trove.common import stream_codecs
|
||||
from trove.common import utils
|
||||
from trove.guestagent.common import operating_system
|
||||
from trove.guestagent.module.drivers import module_driver
|
||||
|
||||
|
||||
LOG = logging.getLogger(__name__)
|
||||
CONF = cfg.CONF
|
||||
|
||||
|
||||
NR_ADD_LICENSE_CMD = ['nrsysmond-config', '--set', 'license_key=%s']
|
||||
NR_SRV_CONTROL_CMD = ['/etc/init.d/newrelic-sysmond']
|
||||
|
||||
|
||||
class NewRelicLicenseDriver(module_driver.ModuleDriver):
|
||||
"""Module to set up the license for the NewRelic service."""
|
||||
|
||||
def get_description(self):
|
||||
return "New Relic License Module Driver"
|
||||
|
||||
def get_updated(self):
|
||||
return date(2016, 4, 12)
|
||||
|
||||
@module_driver.output(
|
||||
log_message=_('Installing New Relic license key'),
|
||||
success_message=_('New Relic license key installed'),
|
||||
fail_message=_('New Relic license key not installed'))
|
||||
def apply(self, name, datastore, ds_version, data_file, admin_module):
|
||||
license_key = None
|
||||
data = operating_system.read_file(
|
||||
data_file, codec=stream_codecs.KeyValueCodec())
|
||||
for key, value in data.items():
|
||||
if 'license_key' == key.lower():
|
||||
license_key = value
|
||||
break
|
||||
if license_key:
|
||||
self._add_license_key(license_key)
|
||||
self._server_control('start')
|
||||
else:
|
||||
return False, "'license_key' not found in contents file"
|
||||
|
||||
def _add_license_key(self, license_key):
|
||||
try:
|
||||
exec_args = {'timeout': 10,
|
||||
'run_as_root': True,
|
||||
'root_helper': 'sudo'}
|
||||
cmd = list(NR_ADD_LICENSE_CMD)
|
||||
cmd[-1] = cmd[-1] % license_key
|
||||
utils.execute_with_timeout(*cmd, **exec_args)
|
||||
except Exception:
|
||||
LOG.exception(_("Could not install license key '%s'") %
|
||||
license_key)
|
||||
raise
|
||||
|
||||
def _server_control(self, command):
|
||||
try:
|
||||
exec_args = {'timeout': 10,
|
||||
'run_as_root': True,
|
||||
'root_helper': 'sudo'}
|
||||
cmd = list(NR_SRV_CONTROL_CMD)
|
||||
cmd.append(command)
|
||||
utils.execute_with_timeout(*cmd, **exec_args)
|
||||
except Exception:
|
||||
LOG.exception(_("Could not %s New Relic server") % command)
|
||||
raise
|
||||
|
||||
@module_driver.output(
|
||||
log_message=_('Removing New Relic license key'),
|
||||
success_message=_('New Relic license key removed'),
|
||||
fail_message=_('New Relic license key not removed'))
|
||||
def remove(self, name, datastore, ds_version, data_file):
|
||||
self._add_license_key("bad_key_that_is_exactly_40_characters_xx")
|
||||
self._server_control('stop')
|
|
@ -37,37 +37,24 @@ class PingDriver(module_driver.ModuleDriver):
|
|||
be 'Hello.'
|
||||
"""
|
||||
|
||||
def get_type(self):
|
||||
return 'ping'
|
||||
|
||||
def get_description(self):
|
||||
return "Ping Guestagent Module Driver"
|
||||
return "Ping Module Driver"
|
||||
|
||||
def get_updated(self):
|
||||
return date(2016, 3, 4)
|
||||
|
||||
def apply(self, name, datastore, ds_version, data_file):
|
||||
success = False
|
||||
message = "Message not found in contents file"
|
||||
try:
|
||||
data = operating_system.read_file(
|
||||
data_file, codec=stream_codecs.KeyValueCodec())
|
||||
for key, value in data.items():
|
||||
if 'message' == key.lower():
|
||||
success = True
|
||||
message = value
|
||||
break
|
||||
except Exception:
|
||||
# assume we couldn't read the file, because there was some
|
||||
# issue with it (for example, it's a binary file). Just log
|
||||
# it and drive on.
|
||||
LOG.error(_("Could not extract contents from '%s' - possibly "
|
||||
"a binary file?") % name)
|
||||
|
||||
return success, message
|
||||
|
||||
def _is_binary(self, data_str):
|
||||
bool(data_str.translate(None, self.TEXT_CHARS))
|
||||
@module_driver.output(
|
||||
log_message=_('Extracting %(type)s message'),
|
||||
fail_message=_('Could not extract %(type)s message'))
|
||||
def apply(self, name, datastore, ds_version, data_file, admin_module):
|
||||
data = operating_system.read_file(
|
||||
data_file, codec=stream_codecs.KeyValueCodec())
|
||||
for key, value in data.items():
|
||||
if 'message' == key.lower():
|
||||
return True, value
|
||||
return False, 'Message not found in contents file'
|
||||
|
||||
@module_driver.output(
|
||||
log_message=_('Removing %(type)s module'))
|
||||
def remove(self, name, datastore, ds_version, data_file):
|
||||
return True, ""
|
||||
|
|
|
@ -61,17 +61,17 @@ class ModuleManager(object):
|
|||
module_type, name, tenant, datastore,
|
||||
ds_version, module_id, md5, auto_apply, visible, now)
|
||||
result = cls.read_module_result(module_dir, default_result)
|
||||
admin_module = cls.is_admin_module(tenant, auto_apply, visible)
|
||||
try:
|
||||
driver.configure(name, datastore, ds_version, data_file)
|
||||
applied, message = driver.apply(
|
||||
name, datastore, ds_version, data_file)
|
||||
name, datastore, ds_version, data_file, admin_module)
|
||||
except Exception as ex:
|
||||
LOG.exception(_("Could not apply module '%s'") % name)
|
||||
applied = False
|
||||
message = ex.message
|
||||
finally:
|
||||
status = 'OK' if applied else 'ERROR'
|
||||
admin_only = (not visible or tenant == cls.MODULE_APPLY_TO_ALL or
|
||||
auto_apply)
|
||||
result['removed'] = None
|
||||
result['status'] = status
|
||||
result['message'] = message
|
||||
|
@ -81,7 +81,7 @@ class ModuleManager(object):
|
|||
result['tenant'] = tenant
|
||||
result['auto_apply'] = auto_apply
|
||||
result['visible'] = visible
|
||||
result['admin_only'] = admin_only
|
||||
result['admin_only'] = admin_module
|
||||
cls.write_module_result(module_dir, result)
|
||||
return result
|
||||
|
||||
|
@ -112,8 +112,7 @@ class ModuleManager(object):
|
|||
def build_default_result(cls, module_type, name, tenant,
|
||||
datastore, ds_version, module_id, md5,
|
||||
auto_apply, visible, now):
|
||||
admin_only = (not visible or tenant == cls.MODULE_APPLY_TO_ALL or
|
||||
auto_apply)
|
||||
admin_module = cls.is_admin_module(tenant, auto_apply, visible)
|
||||
result = {
|
||||
'type': module_type,
|
||||
'name': name,
|
||||
|
@ -129,11 +128,16 @@ class ModuleManager(object):
|
|||
'removed': None,
|
||||
'auto_apply': auto_apply,
|
||||
'visible': visible,
|
||||
'admin_only': admin_only,
|
||||
'admin_only': admin_module,
|
||||
'contents': None,
|
||||
}
|
||||
return result
|
||||
|
||||
@classmethod
|
||||
def is_admin_module(cls, tenant, auto_apply, visible):
|
||||
return (not visible or tenant == cls.MODULE_APPLY_TO_ALL or
|
||||
auto_apply)
|
||||
|
||||
@classmethod
|
||||
def read_module_result(cls, result_file, default=None):
|
||||
result_file = cls.get_result_filename(result_file)
|
||||
|
@ -203,6 +207,7 @@ class ModuleManager(object):
|
|||
raise exception.NotFound(
|
||||
_("Module '%s' has not been applied") % name)
|
||||
try:
|
||||
driver.configure(name, datastore, ds_version, contents_file)
|
||||
removed, message = driver.remove(
|
||||
name, datastore, ds_version, contents_file)
|
||||
cls.remove_module_result(module_dir)
|
||||
|
|
|
@ -16,6 +16,7 @@
|
|||
|
||||
import Crypto.Random
|
||||
from proboscis import SkipTest
|
||||
import re
|
||||
import tempfile
|
||||
|
||||
from troveclient.compat import exceptions
|
||||
|
@ -794,8 +795,10 @@ class ModuleRunner(TestRunner):
|
|||
self.assert_equal(expected_contents, module_apply.contents,
|
||||
'%s Unexpected contents' % prefix)
|
||||
if expected_message is not None:
|
||||
self.assert_equal(expected_message, module_apply.message,
|
||||
'%s Unexpected message' % prefix)
|
||||
regex = re.compile(expected_message)
|
||||
self.assert_true(regex.match(module_apply.message),
|
||||
"%s Unexpected message '%s', expected '%s'" %
|
||||
(prefix, module_apply.message, expected_message))
|
||||
if expected_status is not None:
|
||||
self.assert_equal(expected_status, module_apply.status,
|
||||
'%s Unexpected status' % prefix)
|
||||
|
@ -824,7 +827,8 @@ class ModuleRunner(TestRunner):
|
|||
% module.name)
|
||||
elif self.MODULE_BINARY_SUFFIX in module.name:
|
||||
status = 'ERROR'
|
||||
message = 'Message not found in contents file'
|
||||
message = ('^(Could not extract ping message|'
|
||||
'Message not found in contents file).*')
|
||||
contents = self.MODULE_BINARY_CONTENTS
|
||||
if self.MODULE_BINARY_SUFFIX2 in module.name:
|
||||
contents = self.MODULE_BINARY_CONTENTS2
|
||||
|
|
Loading…
Reference in New Issue