diff --git a/bilean/common/exception.py b/bilean/common/exception.py index 7bd3b4c..a840e3c 100644 --- a/bilean/common/exception.py +++ b/bilean/common/exception.py @@ -96,6 +96,10 @@ class InvalidParameter(BileanException): msg_fmt = _("Invalid value '%(value)s' specified for '%(name)s'") +class PluginTypeNotFound(BileanException): + msg_fmt = _("Plugin type (%(plugin_type)s) is not found.") + + class RuleTypeNotFound(BileanException): msg_fmt = _("Rule type (%(rule_type)s) is not found.") diff --git a/bilean/engine/actions/user_action.py b/bilean/engine/actions/user_action.py index dde215c..861ab83 100644 --- a/bilean/engine/actions/user_action.py +++ b/bilean/engine/actions/user_action.py @@ -21,7 +21,7 @@ from bilean.engine import event as EVENT from bilean.engine.flows import flow as bilean_flow from bilean.engine import lock as bilean_lock from bilean.engine import user as user_mod -from bilean.resources import base as resource_base +from bilean.plugins import base as plugin_base from oslo_log import log as logging @@ -57,7 +57,7 @@ class UserAction(base.Action): self.user = None def do_create_resource(self): - resource = resource_base.Resource.from_dict(self.inputs) + resource = plugin_base.Resource.from_dict(self.inputs) try: flow_engine = bilean_flow.get_create_resource_flow( self.context, self.target, resource) @@ -75,7 +75,7 @@ class UserAction(base.Action): try: values = self.inputs resource_id = values.pop('id', None) - resource = resource_base.Resource.load( + resource = plugin_base.Resource.load( self.context, resource_id=resource_id) except exception.ResourceNotFound: LOG.error(_LE('The resource(%s) trying to update not found.'), @@ -99,7 +99,7 @@ class UserAction(base.Action): def do_delete_resource(self): try: resource_id = self.inputs.get('resource_id') - resource = resource_base.Resource.load( + resource = plugin_base.Resource.load( self.context, resource_id=resource_id) except exception.ResourceNotFound: LOG.error(_LE('The resource(%s) trying to delete not found.'), diff --git a/bilean/engine/environment.py b/bilean/engine/environment.py index 45fb355..9c798b2 100644 --- a/bilean/engine/environment.py +++ b/bilean/engine/environment.py @@ -39,12 +39,12 @@ def global_env(): class Environment(object): - '''An object that contains all rules, resources and customizations.''' + '''An object that contains all plugins, drivers and customizations.''' SECTIONS = ( - PARAMETERS, CUSTOM_RULES, + PARAMETERS, CUSTOM_PLUGINS, ) = ( - 'parameters', 'custom_rules' + 'parameters', 'custom_plugins' ) def __init__(self, env=None, is_global=False): @@ -55,22 +55,19 @@ class Environment(object): ''' self.params = {} if is_global: - self.rule_registry = registry.Registry('rules') + self.plugin_registry = registry.Registry('plugins') self.driver_registry = registry.Registry('drivers') - self.resource_registry = registry.Registry('resources') else: - self.rule_registry = registry.Registry( - 'rules', global_env().rule_registry) - self.resource_registry = registry.Registry( - 'resources', global_env().resource_registry) + self.plugin_registry = registry.Registry( + 'plugins', global_env().plugin_registry) self.driver_registry = registry.Registry( 'drivers', global_env().driver_registry) if env is not None: # Merge user specified keys with current environment self.params = env.get(self.PARAMETERS, {}) - custom_rules = env.get(self.CUSTOM_RULES, {}) - self.rule_registry.load(custom_rules) + custom_plugins = env.get(self.CUSTOM_PLUGINS, {}) + self.plugin_registry.load(custom_plugins) def parse(self, env_str): '''Parse a string format environment file into a dictionary.''' @@ -97,7 +94,7 @@ class Environment(object): '''Load environment from the given dictionary.''' self.params.update(env_dict.get(self.PARAMETERS, {})) - self.rule_registry.load(env_dict.get(self.CUSTOM_RULES, {})) + self.plugin_registry.load(env_dict.get(self.CUSTOM_PLUGINS, {})) def _check_plugin_name(self, plugin_type, name): if name is None or name == "": @@ -107,33 +104,22 @@ class Environment(object): msg = _('%s type name is not a string') % plugin_type raise exception.InvalidPlugin(message=msg) - def register_rule(self, name, plugin): - self._check_plugin_name('Rule', name) - self.rule_registry.register_plugin(name, plugin) + def register_plugin(self, name, plugin): + self._check_plugin_name('Plugin', name) + self.plugin_registry.register_plugin(name, plugin) - def get_rule(self, name): - self._check_plugin_name('Rule', name) - plugin = self.rule_registry.get_plugin(name) + def get_plugin(self, name): + self._check_plugin_name('Plugin', name) + plugin = self.plugin_registry.get_plugin(name) if plugin is None: - raise exception.RuleTypeNotFound(rule_type=name) + raise exception.PluginTypeNotFound(plugin_type=name) return plugin - def get_rule_types(self): - return self.rule_registry.get_types() + def get_plugins(self): + return self.plugin_registry.get_plugins() - def register_resource(self, name, plugin): - self._check_plugin_name('Resource', name) - self.resource_registry.register_plugin(name, plugin) - - def get_resource(self, name): - self._check_plugin_name('Resource', name) - plugin = self.resource_registry.get_plugin(name) - if plugin is None: - raise exception.ResourceTypeNotFound(resource_type=name) - return plugin - - def get_resource_types(self): - return self.resource_registry.get_types() + def get_plugin_types(self): + return self.plugin_registry.get_types() def register_driver(self, name, plugin): self._check_plugin_name('Driver', name) @@ -193,13 +179,9 @@ def initialize(): env = Environment(is_global=True) # Register global plugins when initialized - entries = _get_mapping('bilean.rules') + entries = _get_mapping('bilean.plugins') for name, plugin in entries: - env.register_rule(name, plugin) - - entries = _get_mapping('bilean.resources') - for name, plugin in entries: - env.register_resource(name, plugin) + env.register_plugin(name, plugin) entries = _get_mapping('bilean.drivers') for name, plugin in entries: diff --git a/bilean/engine/flows/flow.py b/bilean/engine/flows/flow.py index 8b5cbfb..ebecc8f 100644 --- a/bilean/engine/flows/flow.py +++ b/bilean/engine/flows/flow.py @@ -24,8 +24,7 @@ from bilean.common import exception from bilean.common.i18n import _LE from bilean.engine import policy as policy_mod from bilean.engine import user as user_mod -from bilean.resources import base as resource_base -from bilean.rules import base as rule_base +from bilean.plugin import base as plugin_base from bilean import scheduler as bilean_scheduler LOG = logging.getLogger(__name__) @@ -96,7 +95,7 @@ class UpdateResourceTask(task.Task): def execute(self, context, resource, values, resource_bak, **kwargs): old_rate = resource.rate resource.properties = values.get('properties') - rule = rule_base.Rule.load(context, rule_id=resource.rule_id) + rule = plugin_base.Rule.load(context, rule_id=resource.rule_id) resource.rate = rule.get_price(resource) resource.delta_rate = resource.rate - old_rate resource.store(context) @@ -107,7 +106,7 @@ class UpdateResourceTask(task.Task): return # restore resource - res = resource_base.Resource.from_dict(resource_bak) + res = plugin_base.Resource.from_dict(resource_bak) res.store(context) diff --git a/bilean/engine/policy.py b/bilean/engine/policy.py index 254e11c..6bb57d9 100644 --- a/bilean/engine/policy.py +++ b/bilean/engine/policy.py @@ -14,7 +14,7 @@ from bilean.common import exception from bilean.common import utils from bilean.db import api as db_api -from bilean.rules import base as rule_base +from bilean.plugins import base as plugin_base class Policy(object): @@ -132,6 +132,6 @@ class Policy(object): for rule in self.rules: if rtype == rule['type'].split('-')[0]: - return rule_base.Rule.load(context, rule_id=rule['id']) + return plugin_base.Rule.load(context, rule_id=rule['id']) raise exception.RuleNotFound(rule_type=rtype) diff --git a/bilean/engine/registry.py b/bilean/engine/registry.py index d867e41..ddb4016 100644 --- a/bilean/engine/registry.py +++ b/bilean/engine/registry.py @@ -128,6 +128,9 @@ class Registry(object): infoes = sorted(matches) return infoes[0].plugin if infoes else None + def get_plugins(self): + return [p.plugin for p in six.itervalues(self._registry)] + def as_dict(self): return dict((k, v.plugin) for k, v in self._registry.items()) diff --git a/bilean/engine/service.py b/bilean/engine/service.py index 484c5b2..95fcc2e 100644 --- a/bilean/engine/service.py +++ b/bilean/engine/service.py @@ -39,8 +39,7 @@ from bilean.engine import environment from bilean.engine import event as event_mod from bilean.engine import policy as policy_mod from bilean.engine import user as user_mod -from bilean.resources import base as resource_base -from bilean.rules import base as rule_base +from bilean.plugins import base as plugin_base from bilean import scheduler as bilean_scheduler LOG = logging.getLogger(__name__) @@ -338,14 +337,14 @@ class EngineService(service.Service): @request_context def rule_create(self, cnxt, name, spec, metadata=None): - if len(rule_base.Rule.load_all(cnxt, filters={'name': name})) > 0: + if len(plugin_base.Rule.load_all(cnxt, filters={'name': name})) > 0: msg = _("The rule (%(name)s) already exists." ) % {"name": name} raise exception.BileanBadRequest(msg=msg) type_name, version = schema.get_spec_version(spec) try: - plugin = environment.global_env().get_rule(type_name) + plugin = environment.global_env().get_plugin(type_name) except exception.RuleTypeNotFound: msg = _("The specified rule type (%(type)s) is not supported." ) % {"type": type_name} @@ -353,7 +352,7 @@ class EngineService(service.Service): LOG.info(_LI("Creating rule type: %(type)s, name: %(name)s."), {'type': type_name, 'name': name}) - rule = plugin(name, spec, metadata=metadata) + rule = plugin.RuleClass(name, spec, metadata=metadata) try: rule.validate() except exception.InvalidSpec as ex: @@ -374,18 +373,18 @@ class EngineService(service.Service): if show_deleted is not None: show_deleted = utils.parse_bool_param('show_deleted', show_deleted) - rules = rule_base.Rule.load_all(cnxt, limit=limit, - marker=marker, - sort_keys=sort_keys, - sort_dir=sort_dir, - filters=filters, - show_deleted=show_deleted) + rules = plugin_base.Rule.load_all(cnxt, limit=limit, + marker=marker, + sort_keys=sort_keys, + sort_dir=sort_dir, + filters=filters, + show_deleted=show_deleted) return [rule.to_dict() for rule in rules] @request_context def rule_get(self, cnxt, rule_id): - rule = rule_base.Rule.load(cnxt, rule_id=rule_id) + rule = plugin_base.Rule.load(cnxt, rule_id=rule_id) return rule.to_dict() @request_context @@ -395,7 +394,7 @@ class EngineService(service.Service): @request_context def rule_delete(self, cnxt, rule_id): LOG.info(_LI("Deleting rule: '%s'."), rule_id) - rule_base.Rule.delete(cnxt, rule_id) + plugin_base.Rule.delete(cnxt, rule_id) @request_context def validate_creation(self, cnxt, resources): @@ -410,9 +409,9 @@ class EngineService(service.Service): total_rate = 0 for resource in resources['resources']: rule = policy.find_rule(cnxt, resource['resource_type']) - res = resource_base.Resource('FAKE_ID', user.id, - resource['resource_type'], - resource['properties']) + res = plugin_base.Resource('FAKE_ID', user.id, + resource['resource_type'], + resource['properties']) total_rate += rule.get_price(res) if count > 1: total_rate = total_rate * count @@ -426,8 +425,8 @@ class EngineService(service.Service): properties): """Create resource by given data.""" - resource = resource_base.Resource(resource_id, user_id, resource_type, - properties) + resource = plugin_base.Resource(resource_id, user_id, resource_type, + properties) params = { 'name': 'create_resource_%s' % resource_id, @@ -451,18 +450,18 @@ class EngineService(service.Service): if show_deleted is not None: show_deleted = utils.parse_bool_param('show_deleted', show_deleted) - resources = resource_base.Resource.load_all(cnxt, user_id=user_id, - limit=limit, marker=marker, - sort_keys=sort_keys, - sort_dir=sort_dir, - filters=filters, - project_safe=project_safe, - show_deleted=show_deleted) + resources = plugin_base.Resource.load_all(cnxt, user_id=user_id, + limit=limit, marker=marker, + sort_keys=sort_keys, + sort_dir=sort_dir, + filters=filters, + project_safe=project_safe, + show_deleted=show_deleted) return [r.to_dict() for r in resources] @request_context def resource_get(self, cnxt, resource_id): - resource = resource_base.Resource.load(cnxt, resource_id=resource_id) + resource = plugin_base.Resource.load(cnxt, resource_id=resource_id) return resource.to_dict() def resource_update(self, cnxt, user_id, resource): @@ -485,7 +484,7 @@ class EngineService(service.Service): """Delete a specific resource""" try: - resource_base.Resource.load(cnxt, resource_id=resource_id) + plugin_base.Resource.load(cnxt, resource_id=resource_id) except exception.ResourceNotFound: LOG.error(_LE('The resource(%s) trying to delete not found.'), resource_id) @@ -539,7 +538,7 @@ class EngineService(service.Service): type_cache = [] for rule_id in rule_ids: try: - rule = rule_base.Rule.load(cnxt, rule_id=rule_id) + rule = plugin_base.Rule.load(cnxt, rule_id=rule_id) if rule.type not in type_cache: rules.append({'id': rule_id, 'type': rule.type}) type_cache.append(rule.type) @@ -631,7 +630,7 @@ class EngineService(service.Service): not_found = [] for rule in rules: try: - db_rule = rule_base.Rule.load(cnxt, rule_id=rule) + db_rule = plugin_base.Rule.load(cnxt, rule_id=rule) append_data = {'id': db_rule.id, 'type': db_rule.type} if db_rule.type in exist_types: error_rules.append(append_data) diff --git a/bilean/engine/user.py b/bilean/engine/user.py index 8638f83..a232120 100644 --- a/bilean/engine/user.py +++ b/bilean/engine/user.py @@ -20,7 +20,7 @@ from bilean.common import utils from bilean.db import api as db_api from bilean.drivers import base as driver_base from bilean import notifier as bilean_notifier -from bilean.resources import base as resource_base +from bilean.plugins import base as plugin_base from oslo_config import cfg from oslo_log import log as logging @@ -335,7 +335,7 @@ class User(object): reason = _("Balance overdraft") LOG.info(_LI("Freeze user %(user_id)s, reason: %(reason)s"), {'user_id': self.id, 'reason': reason}) - resources = resource_base.Resource.load_all( + resources = plugin_base.Resource.load_all( context, user_id=self.id, project_safe=False) for resource in resources: resource.do_delete(context) diff --git a/bilean/resources/__init__.py b/bilean/plugins/__init__.py similarity index 100% rename from bilean/resources/__init__.py rename to bilean/plugins/__init__.py diff --git a/bilean/resources/base.py b/bilean/plugins/base.py similarity index 58% rename from bilean/resources/base.py rename to bilean/plugins/base.py index 6c015ba..53874e1 100644 --- a/bilean/resources/base.py +++ b/bilean/plugins/base.py @@ -1,23 +1,228 @@ -# -# 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 +# 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. +# 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 oslo_config import cfg +from oslo_log import log as logging +from oslo_utils import timeutils from bilean.common import exception +from bilean.common.i18n import _ +from bilean.common import schema from bilean.common import utils from bilean.db import api as db_api from bilean.engine import consumption as consumption_mod from bilean.engine import environment -from oslo_utils import timeutils +LOG = logging.getLogger(__name__) + + +resource_opts = [ + cfg.StrOpt('notifications_topic', default="notifications", + help="The default messaging notifications topic"), +] + +CONF = cfg.CONF +CONF.register_opts(resource_opts, group='resource_plugin') + + +class Plugin(object): + '''Base class for plugins.''' + + RuleClass = None + ResourceClass = None + notification_exchanges = [] + + @classmethod + def get_notification_topics_exchanges(cls): + """Returns a list of (topic,exchange), (topic,exchange)..).""" + + return [(CONF.resource_plugin.notifications_topic, exchange) + for exchange in cls.notification_exchanges] + + +class Rule(object): + '''Base class for rules.''' + + KEYS = ( + TYPE, VERSION, PROPERTIES, + ) = ( + 'type', 'version', 'properties', + ) + + spec_schema = { + TYPE: schema.String( + _('Name of the rule type.'), + required=True, + ), + VERSION: schema.String( + _('Version number of the rule type.'), + required=True, + ), + PROPERTIES: schema.Map( + _('Properties for the rule.'), + required=True, + ) + } + + properties_schema = {} + + def __new__(cls, name, spec, **kwargs): + """Create a new rule of the appropriate class. + + :param name: The name for the rule. + :param spec: A dictionary containing the spec for the rule. + :param kwargs: Keyword arguments for rule creation. + :returns: An instance of a specific sub-class of Rule. + """ + type_name, version = schema.get_spec_version(spec) + + if cls != Rule: + RuleClass = cls + else: + PluginClass = environment.global_env().get_plugin(type_name) + RuleClass = PluginClass.RuleClass + + return super(Rule, cls).__new__(RuleClass) + + def __init__(self, name, spec, **kwargs): + """Initialize a rule instance. + + :param name: A string that specifies the name for the rule. + :param spec: A dictionary containing the detailed rule spec. + :param kwargs: Keyword arguments for initializing the rule. + :returns: An instance of a specific sub-class of Rule. + """ + + type_name, version = schema.get_spec_version(spec) + + self.name = name + self.spec = spec + + self.id = kwargs.get('id') + self.type = kwargs.get('type', '%s-%s' % (type_name, version)) + + self.metadata = kwargs.get('metadata', {}) + + self.created_at = kwargs.get('created_at') + self.updated_at = kwargs.get('updated_at') + self.deleted_at = kwargs.get('deleted_at') + + self.spec_data = schema.Spec(self.spec_schema, self.spec) + self.properties = schema.Spec(self.properties_schema, + self.spec.get(self.PROPERTIES, {})) + + @classmethod + def from_db_record(cls, record): + '''Construct a rule object from database record. + + :param record: a DB Profle object that contains all required fields. + ''' + kwargs = { + 'id': record.id, + 'type': record.type, + 'metadata': record.meta_data, + 'created_at': record.created_at, + 'updated_at': record.updated_at, + 'deleted_at': record.deleted_at, + } + + return cls(record.name, record.spec, **kwargs) + + @classmethod + def load(cls, context, rule_id=None, rule=None, show_deleted=False): + '''Retrieve a rule object from database.''' + if rule is None: + rule = db_api.rule_get(context, rule_id, + show_deleted=show_deleted) + if rule is None: + raise exception.RuleNotFound(rule=rule_id) + + return cls.from_db_record(rule) + + @classmethod + def load_all(cls, context, limit=None, marker=None, sort_keys=None, + sort_dir=None, filters=None, show_deleted=False): + '''Retrieve all rules from database.''' + + records = db_api.rule_get_all(context, limit=limit, + marker=marker, + sort_keys=sort_keys, + sort_dir=sort_dir, + filters=filters, + show_deleted=show_deleted) + + return [cls.from_db_record(record) for record in records] + + @classmethod + def delete(cls, context, rule_id): + db_api.rule_delete(context, rule_id) + + def store(self, context): + '''Store the rule into database and return its ID.''' + timestamp = timeutils.utcnow() + + values = { + 'name': self.name, + 'type': self.type, + 'spec': self.spec, + 'meta_data': self.metadata, + } + + if self.id: + self.updated_at = timestamp + values['updated_at'] = timestamp + db_api.rule_update(context, self.id, values) + else: + self.created_at = timestamp + values['created_at'] = timestamp + rule = db_api.rule_create(context, values) + self.id = rule.id + + return self.id + + def validate(self): + '''Validate the schema and the data provided.''' + # general validation + self.spec_data.validate() + self.properties.validate() + + @classmethod + def get_schema(cls): + return dict((name, dict(schema)) + for name, schema in cls.properties_schema.items()) + + def get_price(self, resource): + '''For subclass to override.''' + + return NotImplemented + + def to_dict(self): + rule_dict = { + 'id': self.id, + 'name': self.name, + 'type': self.type, + 'spec': self.spec, + 'metadata': self.metadata, + 'created_at': utils.format_time(self.created_at), + 'updated_at': utils.format_time(self.updated_at), + 'deleted_at': utils.format_time(self.deleted_at), + } + return rule_dict + + @classmethod + def from_dict(cls, **kwargs): + type_name = kwargs.pop('type') + name = kwargs.pop('name') + return cls(type_name, name, kwargs) class Resource(object): @@ -41,7 +246,8 @@ class Resource(object): if cls != Resource: ResourceClass = cls else: - ResourceClass = environment.global_env().get_resource(res_type) + PluginClass = environment.global_env().get_plugin(res_type) + ResourceClass = PluginClass.ResourceClass return super(Resource, cls).__new__(ResourceClass) diff --git a/bilean/resources/os/__init__.py b/bilean/plugins/os/__init__.py similarity index 100% rename from bilean/resources/os/__init__.py rename to bilean/plugins/os/__init__.py diff --git a/bilean/resources/os/nova/__init__.py b/bilean/plugins/os/nova/__init__.py similarity index 100% rename from bilean/resources/os/nova/__init__.py rename to bilean/plugins/os/nova/__init__.py diff --git a/bilean/rules/os/nova/server.py b/bilean/plugins/os/nova/server.py similarity index 66% rename from bilean/rules/os/nova/server.py rename to bilean/plugins/os/nova/server.py index f6fdb6b..3dfb92e 100644 --- a/bilean/rules/os/nova/server.py +++ b/bilean/plugins/os/nova/server.py @@ -10,12 +10,17 @@ # License for the specific language governing permissions and limitations # under the License. +import six + from oslo_log import log as logging from bilean.common import exception from bilean.common.i18n import _ +from bilean.common.i18n import _LE from bilean.common import schema -from bilean.rules import base +from bilean.db import api as db_api +from bilean.drivers import base as driver_base +from bilean.plugins import base LOG = logging.getLogger(__name__) @@ -85,3 +90,42 @@ class ServerRule(base.Rule): if self.PER_HOUR == self.properties.get(self.UNIT) and price > 0: price = price * 1.0 / 3600 return price + + +class ServerResource(base.Resource): + '''Resource for an OpenStack Nova server.''' + + @classmethod + def do_check(context, user): + '''Communicate with other services and check user's resources. + + This would be a period job of user to check if there are any missing + actions, and then make correction. + ''' + # TODO(ldb) + return NotImplemented + + def do_delete(self, context, ignore_missing=True, timeout=None): + '''Delete resource from other services.''' + + # Delete resource from db + db_api.resource_delete(context, self.id) + + # Delete resource from nova + novaclient = driver_base.BileanDriver().compute() + try: + novaclient.server_delete(self.id, ignore_missing=ignore_missing) + novaclient.wait_for_server_delete(self.id, timeout=timeout) + except Exception as ex: + LOG.error(_LE('Error: %s'), six.text_type(ex)) + return False + + return True + + +class ServerPlugin(base.Plugin): + '''Plugin for Openstack Nova server.''' + + RuleClass = ServerRule + ResourceClass = ServerResource + notification_exchanges = ['nova', 'neutron'] diff --git a/bilean/resources/os/nova/server.py b/bilean/resources/os/nova/server.py deleted file mode 100644 index 81509c9..0000000 --- a/bilean/resources/os/nova/server.py +++ /dev/null @@ -1,54 +0,0 @@ -# -# 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 six - -from bilean.common.i18n import _LE -from bilean.db import api as db_api -from bilean.drivers import base as driver_base -from bilean.resources import base - -from oslo_log import log as logging - -LOG = logging.getLogger(__name__) - - -class ServerResource(base.Resource): - '''Resource for an OpenStack Nova server.''' - - @classmethod - def do_check(context, user): - '''Communicate with other services and check user's resources. - - This would be a period job of user to check if there are any missing - actions, and then make correction. - ''' - # TODO(ldb) - return NotImplemented - - def do_delete(self, context, ignore_missing=True, timeout=None): - '''Delete resource from other services.''' - - # Delete resource from db - db_api.resource_delete(context, self.id) - - # Delete resource from nova - novaclient = driver_base.BileanDriver().compute() - try: - novaclient.server_delete(self.id, ignore_missing=ignore_missing) - novaclient.wait_for_server_delete(self.id, timeout=timeout) - except Exception as ex: - LOG.error(_LE('Error: %s'), six.text_type(ex)) - return False - - return True diff --git a/bilean/rules/__init__.py b/bilean/rules/__init__.py deleted file mode 100644 index e69de29..0000000 diff --git a/bilean/rules/base.py b/bilean/rules/base.py deleted file mode 100644 index 83a719e..0000000 --- a/bilean/rules/base.py +++ /dev/null @@ -1,198 +0,0 @@ -# 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 oslo_log import log as logging -from oslo_utils import timeutils - -from bilean.common import exception -from bilean.common.i18n import _ -from bilean.common import schema -from bilean.common import utils -from bilean.db import api as db_api -from bilean.engine import environment - -LOG = logging.getLogger(__name__) - - -class Rule(object): - '''Base class for rules.''' - - KEYS = ( - TYPE, VERSION, PROPERTIES, - ) = ( - 'type', 'version', 'properties', - ) - - spec_schema = { - TYPE: schema.String( - _('Name of the rule type.'), - required=True, - ), - VERSION: schema.String( - _('Version number of the rule type.'), - required=True, - ), - PROPERTIES: schema.Map( - _('Properties for the rule.'), - required=True, - ) - } - - properties_schema = {} - - def __new__(cls, name, spec, **kwargs): - """Create a new rule of the appropriate class. - - :param name: The name for the rule. - :param spec: A dictionary containing the spec for the rule. - :param kwargs: Keyword arguments for rule creation. - :returns: An instance of a specific sub-class of Rule. - """ - type_name, version = schema.get_spec_version(spec) - - if cls != Rule: - RuleClass = cls - else: - RuleClass = environment.global_env().get_rule(type_name) - - return super(Rule, cls).__new__(RuleClass) - - def __init__(self, name, spec, **kwargs): - """Initialize a rule instance. - - :param name: A string that specifies the name for the rule. - :param spec: A dictionary containing the detailed rule spec. - :param kwargs: Keyword arguments for initializing the rule. - :returns: An instance of a specific sub-class of Rule. - """ - - type_name, version = schema.get_spec_version(spec) - - self.name = name - self.spec = spec - - self.id = kwargs.get('id') - self.type = kwargs.get('type', '%s-%s' % (type_name, version)) - - self.metadata = kwargs.get('metadata', {}) - - self.created_at = kwargs.get('created_at') - self.updated_at = kwargs.get('updated_at') - self.deleted_at = kwargs.get('deleted_at') - - self.spec_data = schema.Spec(self.spec_schema, self.spec) - self.properties = schema.Spec(self.properties_schema, - self.spec.get(self.PROPERTIES, {})) - - @classmethod - def from_db_record(cls, record): - '''Construct a rule object from database record. - - :param record: a DB Profle object that contains all required fields. - ''' - kwargs = { - 'id': record.id, - 'type': record.type, - 'metadata': record.meta_data, - 'created_at': record.created_at, - 'updated_at': record.updated_at, - 'deleted_at': record.deleted_at, - } - - return cls(record.name, record.spec, **kwargs) - - @classmethod - def load(cls, context, rule_id=None, rule=None, show_deleted=False): - '''Retrieve a rule object from database.''' - if rule is None: - rule = db_api.rule_get(context, rule_id, - show_deleted=show_deleted) - if rule is None: - raise exception.RuleNotFound(rule=rule_id) - - return cls.from_db_record(rule) - - @classmethod - def load_all(cls, context, limit=None, marker=None, sort_keys=None, - sort_dir=None, filters=None, show_deleted=False): - '''Retrieve all rules from database.''' - - records = db_api.rule_get_all(context, limit=limit, - marker=marker, - sort_keys=sort_keys, - sort_dir=sort_dir, - filters=filters, - show_deleted=show_deleted) - - return [cls.from_db_record(record) for record in records] - - @classmethod - def delete(cls, context, rule_id): - db_api.rule_delete(context, rule_id) - - def store(self, context): - '''Store the rule into database and return its ID.''' - timestamp = timeutils.utcnow() - - values = { - 'name': self.name, - 'type': self.type, - 'spec': self.spec, - 'meta_data': self.metadata, - } - - if self.id: - self.updated_at = timestamp - values['updated_at'] = timestamp - db_api.rule_update(context, self.id, values) - else: - self.created_at = timestamp - values['created_at'] = timestamp - rule = db_api.rule_create(context, values) - self.id = rule.id - - return self.id - - def validate(self): - '''Validate the schema and the data provided.''' - # general validation - self.spec_data.validate() - self.properties.validate() - - @classmethod - def get_schema(cls): - return dict((name, dict(schema)) - for name, schema in cls.properties_schema.items()) - - def get_price(self, resource): - '''For subclass to override.''' - - return NotImplemented - - def to_dict(self): - rule_dict = { - 'id': self.id, - 'name': self.name, - 'type': self.type, - 'spec': self.spec, - 'metadata': self.metadata, - 'created_at': utils.format_time(self.created_at), - 'updated_at': utils.format_time(self.updated_at), - 'deleted_at': utils.format_time(self.deleted_at), - } - return rule_dict - - @classmethod - def from_dict(cls, **kwargs): - type_name = kwargs.pop('type') - name = kwargs.pop('name') - return cls(type_name, name, kwargs) diff --git a/bilean/rules/os/__init__.py b/bilean/rules/os/__init__.py deleted file mode 100644 index e69de29..0000000 diff --git a/bilean/rules/os/nova/__init__.py b/bilean/rules/os/nova/__init__.py deleted file mode 100644 index e69de29..0000000 diff --git a/bilean/tests/rules/test_rule_base.py b/bilean/tests/rules/test_rule_base.py index 001ffbe..b1fd58c 100644 --- a/bilean/tests/rules/test_rule_base.py +++ b/bilean/tests/rules/test_rule_base.py @@ -18,12 +18,12 @@ from bilean.common import schema from bilean.common import utils as common_utils from bilean.db import api as db_api from bilean.engine import environment -from bilean.rules import base as rule_base +from bilean.plugins import base as plugin_base from bilean.tests.common import base from bilean.tests.common import utils -class DummyRule(rule_base.Rule): +class DummyRule(plugin_base.Rule): VERSION = '1.0' properties_schema = { @@ -41,15 +41,19 @@ class DummyRule(rule_base.Rule): super(DummyRule, self).__init__(name, spec, **kwargs) +class DummyPlugin(plugin_base.Plugin): + RuleClass = DummyRule + + class TestRuleBase(base.BileanTestCase): def setUp(self): super(TestRuleBase, self).setUp() self.context = utils.dummy_context() - environment.global_env().register_rule('bilean.rule.dummy', DummyRule) + environment.global_env().register_plugin('bilean.dummy', DummyPlugin) self.spec = { - 'type': 'bilean.rule.dummy', + 'type': 'bilean.dummy', 'version': '1.0', 'properties': { 'key1': 'value1', @@ -58,7 +62,7 @@ class TestRuleBase(base.BileanTestCase): } def _create_rule(self, rule_name, rule_id=None): - rule = rule_base.Rule(rule_name, self.spec) + rule = plugin_base.Rule(rule_name, self.spec) if rule_id: rule.id = rule_id @@ -67,7 +71,7 @@ class TestRuleBase(base.BileanTestCase): def _create_db_rule(self, **kwargs): values = { 'name': 'test-rule', - 'type': 'bilean.rule.dummy-1.0', + 'type': 'bilean.dummy-1.0', 'spec': self.spec, 'metadata': {} } @@ -81,7 +85,7 @@ class TestRuleBase(base.BileanTestCase): self.assertIsNone(rule.id) self.assertEqual(name, rule.name) - self.assertEqual('bilean.rule.dummy-1.0', rule.type) + self.assertEqual('bilean.dummy-1.0', rule.type) self.assertEqual(self.spec, rule.spec) self.assertEqual({}, rule.metadata) self.assertIsNone(rule.created_at) @@ -89,7 +93,7 @@ class TestRuleBase(base.BileanTestCase): self.assertIsNone(rule.deleted_at) spec_data = rule.spec_data - self.assertEqual('bilean.rule.dummy', spec_data['type']) + self.assertEqual('bilean.dummy', spec_data['type']) self.assertEqual('1.0', spec_data['version']) self.assertEqual({'key1': 'value1', 'key2': 2}, spec_data['properties']) @@ -102,13 +106,13 @@ class TestRuleBase(base.BileanTestCase): 'properties': '', } - self.assertRaises(exception.RuleTypeNotFound, - rule_base.Rule, + self.assertRaises(exception.PluginTypeNotFound, + plugin_base.Rule, 'test-rule', bad_spec) def test_load(self): rule = self._create_db_rule() - result = rule_base.Rule.load(self.context, rule.id) + result = plugin_base.Rule.load(self.context, rule.id) self.assertEqual(rule.id, result.id) self.assertEqual(rule.name, result.name) @@ -122,25 +126,25 @@ class TestRuleBase(base.BileanTestCase): def test_load_not_found(self): ex = self.assertRaises(exception.RuleNotFound, - rule_base.Rule.load, + plugin_base.Rule.load, self.context, 'fake-rule', None) self.assertEqual('The rule (fake-rule) could not be found.', six.text_type(ex)) ex = self.assertRaises(exception.RuleNotFound, - rule_base.Rule.load, + plugin_base.Rule.load, self.context, None, None) self.assertEqual('The rule (None) could not be found.', six.text_type(ex)) def test_load_all(self): - result = rule_base.Rule.load_all(self.context) + result = plugin_base.Rule.load_all(self.context) self.assertEqual([], list(result)) rule1 = self._create_db_rule(name='rule-1', id='ID1') rule2 = self._create_db_rule(name='rule-2', id='ID2') - result = rule_base.Rule.load_all(self.context) + result = plugin_base.Rule.load_all(self.context) rules = list(result) self.assertEqual(2, len(rules)) self.assertEqual(rule1.id, rules[0].id) @@ -150,7 +154,7 @@ class TestRuleBase(base.BileanTestCase): def test_load_all_with_params(self, mock_get_all): mock_get_all.return_value = [] - res = list(rule_base.Rule.load_all(self.context)) + res = list(plugin_base.Rule.load_all(self.context)) self.assertEqual([], res) mock_get_all.assert_called_once_with(self.context, limit=None, marker=None, sort_keys=None, @@ -158,11 +162,11 @@ class TestRuleBase(base.BileanTestCase): show_deleted=False) mock_get_all.reset_mock() - res = list(rule_base.Rule.load_all(self.context, limit=1, - marker='MARKER', - sort_keys=['K1'], - sort_dir='asc', - filters={'name': 'fake-name'})) + res = list(plugin_base.Rule.load_all(self.context, limit=1, + marker='MARKER', + sort_keys=['K1'], + sort_dir='asc', + filters={'name': 'fake-name'})) self.assertEqual([], res) mock_get_all.assert_called_once_with(self.context, limit=1, marker='MARKER', @@ -175,14 +179,14 @@ class TestRuleBase(base.BileanTestCase): rule = self._create_db_rule() rule_id = rule.id - res = rule_base.Rule.delete(self.context, rule_id) + res = plugin_base.Rule.delete(self.context, rule_id) self.assertIsNone(res) self.assertRaises(exception.RuleNotFound, - rule_base.Rule.load, + plugin_base.Rule.load, self.context, rule_id, None) def test_delete_not_found(self): - result = rule_base.Rule.delete(self.context, 'fake-rule') + result = plugin_base.Rule.delete(self.context, 'fake-rule') self.assertIsNone(result) def test_store_for_create(self): @@ -240,7 +244,7 @@ class TestRuleBase(base.BileanTestCase): 'deleted_at': None, } - result = rule_base.Rule.load(self.context, rule_id=rule.id) + result = plugin_base.Rule.load(self.context, rule_id=rule.id) self.assertEqual(expected, result.to_dict()) def test_get_schema(self): diff --git a/setup.cfg b/setup.cfg index 66a3c1f..3293f1f 100644 --- a/setup.cfg +++ b/setup.cfg @@ -40,11 +40,8 @@ oslo.config.opts = bilean.drivers = openstack = bilean.drivers.openstack -bilean.rules = - os.nova.server = bilean.rules.os.nova.server:ServerRule - -bilean.resources = - os.nova.server = bilean.resources.os.nova.server:ServerResource +bilean.plugins = + os.nova.server = bilean.plugins.os.nova.server:ServerPlugin [global] setup-hooks =