From fc31dae7f26c8c846321351717c4bab837b09215 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Vincent=20Fran=C3=A7oise?= Date: Mon, 22 Aug 2016 11:15:13 +0200 Subject: [PATCH] Refactored Watcher objects to use OVO In this changeset, I modified all existing Watcher objects to now rely on oslo.versionedobjects as a base. Change-Id: I3c9b1ca6da529d128743b99020350f28926ea1a2 Partially-Implements: blueprint watcher-versioned-objects --- doc/source/conf.py | 3 + requirements.txt | 1 + watcher/api/controllers/v1/action.py | 2 +- watcher/api/controllers/v1/audit.py | 2 +- watcher/api/controllers/v1/audit_template.py | 6 +- watcher/applier/action_plan/default.py | 10 +- watcher/applier/workflow_engine/default.py | 17 +- watcher/common/service.py | 7 +- watcher/db/sqlalchemy/api.py | 14 +- watcher/decision_engine/audit/base.py | 8 +- watcher/decision_engine/audit/continuous.py | 35 +- .../messaging/audit_endpoint.py | 4 +- watcher/decision_engine/planner/default.py | 6 +- watcher/decision_engine/sync.py | 29 +- watcher/objects/__init__.py | 60 +- watcher/objects/action.py | 114 +-- watcher/objects/action_plan.py | 117 +-- watcher/objects/audit.py | 115 +-- watcher/objects/audit_template.py | 121 +--- watcher/objects/base.py | 583 +++------------ watcher/objects/efficacy_indicator.py | 62 +- watcher/objects/fields.py | 89 +++ watcher/objects/goal.py | 65 +- watcher/objects/scoring_engine.py | 123 +--- watcher/objects/service.py | 91 +-- watcher/objects/strategy.py | 55 +- watcher/tests/__init__.py | 22 + watcher/tests/api/base.py | 2 +- watcher/tests/api/v1/test_audits.py | 8 +- .../test_default_workflow_engine.py | 2 +- watcher/tests/base.py | 28 +- watcher/tests/db/test_purge.py | 108 ++- watcher/tests/db/utils.py | 2 +- watcher/tests/decision_engine/test_sync.py | 17 +- watcher/tests/objects/test_objects.py | 670 ++++++++++++------ watcher/tests/objects/test_service.py | 3 +- 36 files changed, 1109 insertions(+), 1492 deletions(-) create mode 100644 watcher/objects/fields.py diff --git a/doc/source/conf.py b/doc/source/conf.py index f08e445fa..33fb6937b 100755 --- a/doc/source/conf.py +++ b/doc/source/conf.py @@ -16,6 +16,9 @@ import sys import os from watcher import version as watcher_version +from watcher import objects + +objects.register_all() # If extensions (or modules to document with autodoc) are in another directory, # add these directories to sys.path here. If the directory is relative to the diff --git a/requirements.txt b/requirements.txt index 9ba76097a..7756aa5e1 100644 --- a/requirements.txt +++ b/requirements.txt @@ -21,6 +21,7 @@ oslo.reports>=0.6.0 # Apache-2.0 oslo.serialization>=1.10.0 # Apache-2.0 oslo.service>=1.10.0 # Apache-2.0 oslo.utils>=3.16.0 # Apache-2.0 +oslo.versionedobjects>=1.13.0 # Apache-2.0 PasteDeploy>=1.5.0 # MIT pbr>=1.6 # Apache-2.0 pecan!=1.0.2,!=1.0.3,!=1.0.4,!=1.2,>=1.0.0 # BSD diff --git a/watcher/api/controllers/v1/action.py b/watcher/api/controllers/v1/action.py index 0b7275f28..a93591099 100644 --- a/watcher/api/controllers/v1/action.py +++ b/watcher/api/controllers/v1/action.py @@ -379,7 +379,7 @@ class ActionsController(rest.RestController): action_dict = action.as_dict() context = pecan.request.context new_action = objects.Action(context, **action_dict) - new_action.create(context) + new_action.create() # Set the HTTP Location Header pecan.response.location = link.build_url('actions', new_action.uuid) diff --git a/watcher/api/controllers/v1/audit.py b/watcher/api/controllers/v1/audit.py index 95e25e901..e457f84b8 100644 --- a/watcher/api/controllers/v1/audit.py +++ b/watcher/api/controllers/v1/audit.py @@ -519,7 +519,7 @@ class AuditsController(rest.RestController): audit_dict = audit.as_dict() new_audit = objects.Audit(context, **audit_dict) - new_audit.create(context) + new_audit.create() # Set the HTTP Location Header pecan.response.location = link.build_url('audits', new_audit.uuid) diff --git a/watcher/api/controllers/v1/audit_template.py b/watcher/api/controllers/v1/audit_template.py index b99658284..87365b449 100644 --- a/watcher/api/controllers/v1/audit_template.py +++ b/watcher/api/controllers/v1/audit_template.py @@ -568,11 +568,11 @@ class AuditTemplatesController(rest.RestController): audit_template_dict = audit_template.as_dict() new_audit_template = objects.AuditTemplate(context, **audit_template_dict) - new_audit_template.create(context) + new_audit_template.create() # Set the HTTP Location Header - pecan.response.location = link.build_url('audit_templates', - new_audit_template.uuid) + pecan.response.location = link.build_url( + 'audit_templates', new_audit_template.uuid) return AuditTemplate.convert_with_links(new_audit_template) @wsme.validate(types.uuid, [AuditTemplatePatchType]) diff --git a/watcher/applier/action_plan/default.py b/watcher/applier/action_plan/default.py index 54919c3d7..c9f4dcf93 100644 --- a/watcher/applier/action_plan/default.py +++ b/watcher/applier/action_plan/default.py @@ -22,7 +22,7 @@ from watcher.applier.action_plan import base from watcher.applier import default from watcher.applier.messaging import event_types from watcher.common.messaging.events import event -from watcher.objects import action_plan as ap_objects +from watcher import objects LOG = log.getLogger(__name__) @@ -35,7 +35,7 @@ class DefaultActionPlanHandler(base.BaseActionPlanHandler): self.action_plan_uuid = action_plan_uuid def notify(self, uuid, event_type, state): - action_plan = ap_objects.ActionPlan.get_by_uuid(self.ctx, uuid) + action_plan = objects.ActionPlan.get_by_uuid(self.ctx, uuid) action_plan.state = state action_plan.save() ev = event.Event() @@ -50,13 +50,13 @@ class DefaultActionPlanHandler(base.BaseActionPlanHandler): # update state self.notify(self.action_plan_uuid, event_types.EventTypes.LAUNCH_ACTION_PLAN, - ap_objects.State.ONGOING) + objects.action_plan.State.ONGOING) applier = default.DefaultApplier(self.ctx, self.service) applier.execute(self.action_plan_uuid) - state = ap_objects.State.SUCCEEDED + state = objects.action_plan.State.SUCCEEDED except Exception as e: LOG.exception(e) - state = ap_objects.State.FAILED + state = objects.action_plan.State.FAILED finally: # update state diff --git a/watcher/applier/workflow_engine/default.py b/watcher/applier/workflow_engine/default.py index e89ce1e35..bebe4bd52 100644 --- a/watcher/applier/workflow_engine/default.py +++ b/watcher/applier/workflow_engine/default.py @@ -23,7 +23,7 @@ from taskflow import task from watcher._i18n import _LE, _LW, _LC from watcher.applier.workflow_engine import base from watcher.common import exception -from watcher.objects import action as obj_action +from watcher import objects LOG = log.getLogger(__name__) @@ -107,14 +107,12 @@ class TaskFlowActionContainer(task.Task): def pre_execute(self): try: - self.engine.notify(self._db_action, - obj_action.State.ONGOING) + self.engine.notify(self._db_action, objects.action.State.ONGOING) LOG.debug("Pre-condition action: %s", self.name) self.action.pre_condition() except Exception as e: LOG.exception(e) - self.engine.notify(self._db_action, - obj_action.State.FAILED) + self.engine.notify(self._db_action, objects.action.State.FAILED) raise def execute(self, *args, **kwargs): @@ -122,15 +120,13 @@ class TaskFlowActionContainer(task.Task): LOG.debug("Running action: %s", self.name) self.action.execute() - self.engine.notify(self._db_action, - obj_action.State.SUCCEEDED) + self.engine.notify(self._db_action, objects.action.State.SUCCEEDED) except Exception as e: LOG.exception(e) LOG.error(_LE('The workflow engine has failed ' 'to execute the action: %s'), self.name) - self.engine.notify(self._db_action, - obj_action.State.FAILED) + self.engine.notify(self._db_action, objects.action.State.FAILED) raise def post_execute(self): @@ -139,8 +135,7 @@ class TaskFlowActionContainer(task.Task): self.action.post_condition() except Exception as e: LOG.exception(e) - self.engine.notify(self._db_action, - obj_action.State.FAILED) + self.engine.notify(self._db_action, objects.action.State.FAILED) raise def revert(self, *args, **kwargs): diff --git a/watcher/common/service.py b/watcher/common/service.py index 20c44e5b7..051b900a8 100644 --- a/watcher/common/service.py +++ b/watcher/common/service.py @@ -36,8 +36,8 @@ from watcher.common.messaging.events import event_dispatcher as dispatcher from watcher.common.messaging import messaging_handler from watcher.common import rpc from watcher.common import scheduling +from watcher import objects from watcher.objects import base -from watcher.objects import service as service_object from watcher import opts from watcher import version @@ -118,7 +118,7 @@ class ServiceHeartbeat(scheduling.BackgroundSchedulerService): def send_beat(self): host = CONF.host - watcher_list = service_object.Service.list( + watcher_list = objects.Service.list( self.context, filters={'name': self.service_name, 'host': host}) if watcher_list: @@ -126,7 +126,7 @@ class ServiceHeartbeat(scheduling.BackgroundSchedulerService): watcher_service.last_seen_up = datetime.datetime.utcnow() watcher_service.save() else: - watcher_service = service_object.Service(self.context) + watcher_service = objects.Service(self.context) watcher_service.name = self.service_name watcher_service.host = host watcher_service.create() @@ -333,6 +333,7 @@ def prepare_service(argv=(), conf=cfg.CONF): default_log_levels=_DEFAULT_LOG_LEVELS) log.setup(conf, 'python-watcher') conf.log_opt_values(LOG, logging.DEBUG) + objects.register_all() gmr.TextGuruMeditation.register_section(_('Plugins'), opts.show_plugins) gmr.TextGuruMeditation.setup_autorun(version, conf=conf) diff --git a/watcher/db/sqlalchemy/api.py b/watcher/db/sqlalchemy/api.py index cf5e0a973..86c2661ec 100644 --- a/watcher/db/sqlalchemy/api.py +++ b/watcher/db/sqlalchemy/api.py @@ -34,9 +34,7 @@ from watcher.common import exception from watcher.common import utils from watcher.db import api from watcher.db.sqlalchemy import models -from watcher.objects import action as action_objects -from watcher.objects import action_plan as ap_objects -from watcher.objects import audit as audit_objects +from watcher import objects CONF = cfg.CONF @@ -70,7 +68,6 @@ def model_query(model, *args, **kwargs): :param session: if present, the session to use """ - session = kwargs.get('session') or get_session() query = session.query(model, *args) return query @@ -647,7 +644,7 @@ class Connection(api.BaseConnection): query = self._add_audits_filters(query, filters) if not context.show_deleted: query = query.filter( - ~(models.Audit.state == audit_objects.State.DELETED)) + ~(models.Audit.state == objects.audit.State.DELETED)) return _paginate_query(models.Audit, limit, marker, sort_key, sort_dir, query) @@ -658,7 +655,7 @@ class Connection(api.BaseConnection): values['uuid'] = utils.generate_uuid() if values.get('state') is None: - values['state'] = audit_objects.State.PENDING + values['state'] = objects.audit.State.PENDING audit = models.Audit() audit.update(values) @@ -734,7 +731,7 @@ class Connection(api.BaseConnection): query = self._add_actions_filters(query, filters) if not context.show_deleted: query = query.filter( - ~(models.Action.state == action_objects.State.DELETED)) + ~(models.Action.state == objects.action.State.DELETED)) return _paginate_query(models.Action, limit, marker, sort_key, sort_dir, query) @@ -821,7 +818,8 @@ class Connection(api.BaseConnection): query = self._add_action_plans_filters(query, filters) if not context.show_deleted: query = query.filter( - ~(models.ActionPlan.state == ap_objects.State.DELETED)) + ~(models.ActionPlan.state == + objects.action_plan.State.DELETED)) return _paginate_query(models.ActionPlan, limit, marker, sort_key, sort_dir, query) diff --git a/watcher/decision_engine/audit/base.py b/watcher/decision_engine/audit/base.py index cb6a109d5..7357cfaf5 100644 --- a/watcher/decision_engine/audit/base.py +++ b/watcher/decision_engine/audit/base.py @@ -26,7 +26,7 @@ from watcher.common.messaging.events import event as watcher_event from watcher.decision_engine.messaging import events as de_events from watcher.decision_engine.planner import manager as planner_manager from watcher.decision_engine.strategy.context import default as default_context -from watcher.objects import audit as audit_objects +from watcher import objects LOG = log.getLogger(__name__) @@ -89,13 +89,13 @@ class AuditHandler(BaseAuditHandler): def pre_execute(self, audit, request_context): LOG.debug("Trigger audit %s", audit.uuid) # change state of the audit to ONGOING - self.update_audit_state(audit, audit_objects.State.ONGOING) + self.update_audit_state(audit, objects.audit.State.ONGOING) def post_execute(self, audit, solution, request_context): self.planner.schedule(request_context, audit.id, solution) # change state of the audit to SUCCEEDED - self.update_audit_state(audit, audit_objects.State.SUCCEEDED) + self.update_audit_state(audit, objects.audit.State.SUCCEEDED) def execute(self, audit, request_context): try: @@ -104,4 +104,4 @@ class AuditHandler(BaseAuditHandler): self.post_execute(audit, solution, request_context) except Exception as e: LOG.exception(e) - self.update_audit_state(audit, audit_objects.State.FAILED) + self.update_audit_state(audit, objects.audit.State.FAILED) diff --git a/watcher/decision_engine/audit/continuous.py b/watcher/decision_engine/audit/continuous.py index be2cc2d44..f0f8ae2a1 100644 --- a/watcher/decision_engine/audit/continuous.py +++ b/watcher/decision_engine/audit/continuous.py @@ -25,8 +25,7 @@ from oslo_config import cfg from watcher.common import context from watcher.decision_engine.audit import base -from watcher.objects import action_plan as action_objects -from watcher.objects import audit as audit_objects +from watcher import objects CONF = cfg.CONF @@ -56,11 +55,11 @@ class ContinuousAuditHandler(base.AuditHandler): return self._scheduler def _is_audit_inactive(self, audit): - audit = audit_objects.Audit.get_by_uuid(self.context_show_deleted, - audit.uuid) - if audit.state in (audit_objects.State.CANCELLED, - audit_objects.State.DELETED, - audit_objects.State.FAILED): + audit = objects.Audit.get_by_uuid( + self.context_show_deleted, audit.uuid) + if audit.state in (objects.audit.State.CANCELLED, + objects.audit.State.DELETED, + objects.audit.State.FAILED): # if audit isn't in active states, audit's job must be removed to # prevent using of inactive audit in future. job_to_delete = [job for job in self.jobs @@ -77,14 +76,13 @@ class ContinuousAuditHandler(base.AuditHandler): solution = self.strategy_context.execute_strategy( audit, request_context) - if audit.audit_type == audit_objects.AuditType.CONTINUOUS.value: + if audit.audit_type == objects.audit.AuditType.CONTINUOUS.value: a_plan_filters = {'audit_uuid': audit.uuid, - 'state': action_objects.State.RECOMMENDED} - action_plans = action_objects.ActionPlan.list( - request_context, - filters=a_plan_filters) + 'state': objects.action_plan.State.RECOMMENDED} + action_plans = objects.ActionPlan.list( + request_context, filters=a_plan_filters) for plan in action_plans: - plan.state = action_objects.State.CANCELLED + plan.state = objects.action_plan.State.CANCELLED plan.save() return solution @@ -98,13 +96,12 @@ class ContinuousAuditHandler(base.AuditHandler): def launch_audits_periodically(self): audit_context = context.RequestContext(is_admin=True) audit_filters = { - 'audit_type': audit_objects.AuditType.CONTINUOUS.value, - 'state__in': (audit_objects.State.PENDING, - audit_objects.State.ONGOING, - audit_objects.State.SUCCEEDED) + 'audit_type': objects.audit.AuditType.CONTINUOUS.value, + 'state__in': (objects.audit.State.PENDING, + objects.audit.State.ONGOING, + objects.audit.State.SUCCEEDED) } - audits = audit_objects.Audit.list(audit_context, - filters=audit_filters) + audits = objects.Audit.list(audit_context, filters=audit_filters) scheduler_job_args = [job.args for job in self.scheduler.get_jobs() if job.name == 'execute_audit'] for audit in audits: diff --git a/watcher/decision_engine/messaging/audit_endpoint.py b/watcher/decision_engine/messaging/audit_endpoint.py index d6cab9354..0d19600bf 100644 --- a/watcher/decision_engine/messaging/audit_endpoint.py +++ b/watcher/decision_engine/messaging/audit_endpoint.py @@ -23,7 +23,7 @@ from oslo_log import log from watcher.decision_engine.audit import continuous as continuous_handler from watcher.decision_engine.audit import oneshot as oneshot_handler -from watcher.objects import audit as audit_objects +from watcher import objects CONF = cfg.CONF LOG = log.getLogger(__name__) @@ -49,7 +49,7 @@ class AuditEndpoint(object): return self._messaging def do_trigger_audit(self, context, audit_uuid): - audit = audit_objects.Audit.get_by_uuid(context, audit_uuid) + audit = objects.Audit.get_by_uuid(context, audit_uuid) self._oneshot_handler.execute(audit, context) def trigger_audit(self, context, audit_uuid): diff --git a/watcher/decision_engine/planner/default.py b/watcher/decision_engine/planner/default.py index ac384661e..35a8aeee1 100644 --- a/watcher/decision_engine/planner/default.py +++ b/watcher/decision_engine/planner/default.py @@ -128,7 +128,7 @@ class DefaultPlanner(base.BasePlanner): } new_action_plan = objects.ActionPlan(context, **action_plan_dict) - new_action_plan.create(context) + new_action_plan.create() return new_action_plan @@ -145,7 +145,7 @@ class DefaultPlanner(base.BasePlanner): } new_efficacy_indicator = objects.EfficacyIndicator( context, **efficacy_indicator_dict) - new_efficacy_indicator.create(context) + new_efficacy_indicator.create() efficacy_indicators.append(new_efficacy_indicator) return efficacy_indicators @@ -156,7 +156,7 @@ class DefaultPlanner(base.BasePlanner): _action.get("action_type")) new_action = objects.Action(context, **_action) - new_action.create(context) + new_action.create() new_action.save() if parent_action: diff --git a/watcher/decision_engine/sync.py b/watcher/decision_engine/sync.py index ce7be78b9..989bf01cf 100644 --- a/watcher/decision_engine/sync.py +++ b/watcher/decision_engine/sync.py @@ -23,8 +23,6 @@ from watcher.common import context from watcher.decision_engine.loading import default from watcher.decision_engine.scoring import scoring_factory from watcher import objects -from watcher.objects import action_plan as apobjects -from watcher.objects import audit as auditobjects LOG = log.getLogger(__name__) @@ -338,13 +336,13 @@ class Syncer(object): for audit in stale_audits: if audit.id not in self.stale_audits_map: audit.strategy_id = synced_strategy.id - audit.state = auditobjects.State.CANCELLED + audit.state = objects.audit.State.CANCELLED self.stale_audits_map[audit.id] = audit else: self.stale_audits_map[ audit.id].strategy_id = synced_strategy.id self.stale_audits_map[ - audit.id].state = auditobjects.State.CANCELLED + audit.id].state = objects.audit.State.CANCELLED def _find_stale_action_plans_due_to_strategy(self): for strategy_id, synced_strategy in self.strategy_mapping.items(): @@ -356,13 +354,14 @@ class Syncer(object): for action_plan in stale_action_plans: if action_plan.id not in self.stale_action_plans_map: action_plan.strategy_id = synced_strategy.id - action_plan.state = apobjects.State.CANCELLED + action_plan.state = objects.action_plan.State.CANCELLED self.stale_action_plans_map[action_plan.id] = action_plan else: self.stale_action_plans_map[ action_plan.id].strategy_id = synced_strategy.id self.stale_action_plans_map[ - action_plan.id].state = apobjects.State.CANCELLED + action_plan.id].state = ( + objects.action_plan.State.CANCELLED) def _find_stale_action_plans_due_to_audit(self): for audit_id, synced_audit in self.stale_audits_map.items(): @@ -374,13 +373,14 @@ class Syncer(object): for action_plan in stale_action_plans: if action_plan.id not in self.stale_action_plans_map: action_plan.audit_id = synced_audit.id - action_plan.state = apobjects.State.CANCELLED + action_plan.state = objects.action_plan.State.CANCELLED self.stale_action_plans_map[action_plan.id] = action_plan else: self.stale_action_plans_map[ action_plan.id].audit_id = synced_audit.id self.stale_action_plans_map[ - action_plan.id].state = apobjects.State.CANCELLED + action_plan.id].state = ( + objects.action_plan.State.CANCELLED) def _soft_delete_removed_goals(self): removed_goals = [ @@ -402,11 +402,11 @@ class Syncer(object): _LW("Audit '%(audit)s' references a " "goal that does not exist"), audit=audit.uuid) if audit.id not in self.stale_audits_map: - audit.state = auditobjects.State.CANCELLED + audit.state = objects.audit.State.CANCELLED self.stale_audits_map[audit.id] = audit else: self.stale_audits_map[ - audit.id].state = auditobjects.State.CANCELLED + audit.id].state = objects.audit.State.CANCELLED def _soft_delete_removed_strategies(self): removed_strategies = [ @@ -437,11 +437,11 @@ class Syncer(object): _LW("Audit '%(audit)s' references a " "strategy that does not exist"), audit=audit.uuid) if audit.id not in self.stale_audits_map: - audit.state = auditobjects.State.CANCELLED + audit.state = objects.audit.State.CANCELLED self.stale_audits_map[audit.id] = audit else: self.stale_audits_map[ - audit.id].state = auditobjects.State.CANCELLED + audit.id].state = objects.audit.State.CANCELLED stale_action_plans = objects.ActionPlan.list( self.ctx, filters=filters) @@ -451,11 +451,12 @@ class Syncer(object): "strategy that does not exist"), action_plan=action_plan.uuid) if action_plan.id not in self.stale_action_plans_map: - action_plan.state = apobjects.State.CANCELLED + action_plan.state = objects.action_plan.State.CANCELLED self.stale_action_plans_map[action_plan.id] = action_plan else: self.stale_action_plans_map[ - action_plan.id].state = apobjects.State.CANCELLED + action_plan.id].state = ( + objects.action_plan.State.CANCELLED) def _soft_delete_removed_scoringengines(self): removed_se = [ diff --git a/watcher/objects/__init__.py b/watcher/objects/__init__.py index 3c56f8efa..11c8a862e 100644 --- a/watcher/objects/__init__.py +++ b/watcher/objects/__init__.py @@ -1,37 +1,35 @@ -# -*- encoding: utf-8 -*- +# Copyright 2013 IBM Corp. # -# 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 +# 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 watcher.objects import action -from watcher.objects import action_plan -from watcher.objects import audit -from watcher.objects import audit_template -from watcher.objects import efficacy_indicator -from watcher.objects import goal -from watcher.objects import scoring_engine -from watcher.objects import service -from watcher.objects import strategy +# NOTE(comstud): You may scratch your head as you see code that imports +# this module and then accesses attributes for objects such as Node, +# etc, yet you do not see these attributes in here. Never fear, there is +# a little bit of magic. When objects are registered, an attribute is set +# on this module automatically, pointing to the newest/latest version of +# the object. -Audit = audit.Audit -AuditTemplate = audit_template.AuditTemplate -Action = action.Action -ActionPlan = action_plan.ActionPlan -Goal = goal.Goal -ScoringEngine = scoring_engine.ScoringEngine -Strategy = strategy.Strategy -EfficacyIndicator = efficacy_indicator.EfficacyIndicator -Service = service.Service -__all__ = ("Audit", "AuditTemplate", "Action", "ActionPlan", "Goal", - "ScoringEngine", "Strategy", "EfficacyIndicator", "Service") +def register_all(): + # NOTE(danms): You must make sure your object gets imported in this + # function in order for it to be registered by services that may + # need to receive it via RPC. + __import__('watcher.objects.goal') + __import__('watcher.objects.strategy') + __import__('watcher.objects.audit_template') + __import__('watcher.objects.audit') + __import__('watcher.objects.action_plan') + __import__('watcher.objects.action') + __import__('watcher.objects.efficacy_indicator') + __import__('watcher.objects.scoring_engine') + __import__('watcher.objects.service') diff --git a/watcher/objects/action.py b/watcher/objects/action.py index 76075a384..42c41fe6e 100644 --- a/watcher/objects/action.py +++ b/watcher/objects/action.py @@ -14,12 +14,11 @@ # See the License for the specific language governing permissions and # limitations under the License. - from watcher.common import exception from watcher.common import utils from watcher.db import api as dbapi from watcher.objects import base -from watcher.objects import utils as obj_utils +from watcher.objects import fields as wfields class State(object): @@ -31,38 +30,26 @@ class State(object): CANCELLED = 'CANCELLED' -class Action(base.WatcherObject): +@base.WatcherObjectRegistry.register +class Action(base.WatcherPersistentObject, base.WatcherObject, + base.WatcherObjectDictCompat): + # Version 1.0: Initial version VERSION = '1.0' dbapi = dbapi.get_instance() fields = { - 'id': int, - 'uuid': obj_utils.str_or_none, - 'action_plan_id': obj_utils.int_or_none, - 'action_type': obj_utils.str_or_none, - 'input_parameters': obj_utils.dict_or_none, - 'state': obj_utils.str_or_none, - 'next': obj_utils.int_or_none, + 'id': wfields.IntegerField(), + 'uuid': wfields.UUIDField(), + 'action_plan_id': wfields.IntegerField(nullable=True), + 'action_type': wfields.StringField(nullable=True), + 'input_parameters': wfields.DictField(nullable=True), + 'state': wfields.StringField(nullable=True), + 'next': wfields.IntegerField(nullable=True), } - @staticmethod - def _from_db_object(action, db_action): - """Converts a database entity to a formal object.""" - for field in action.fields: - action[field] = db_action[field] - - action.obj_reset_changes() - return action - - @staticmethod - def _from_db_object_list(db_objects, cls, context): - """Converts a list of database entities to a list of formal objects.""" - return \ - [Action._from_db_object(cls(context), obj) for obj in db_objects] - - @classmethod + @base.remotable_classmethod def get(cls, context, action_id): """Find a action based on its id or uuid and return a Action object. @@ -76,7 +63,7 @@ class Action(base.WatcherObject): else: raise exception.InvalidIdentity(identity=action_id) - @classmethod + @base.remotable_classmethod def get_by_id(cls, context, action_id): """Find a action based on its integer id and return a Action object. @@ -87,7 +74,7 @@ class Action(base.WatcherObject): action = Action._from_db_object(cls(context), db_action) return action - @classmethod + @base.remotable_classmethod def get_by_uuid(cls, context, uuid): """Find a action based on uuid and return a :class:`Action` object. @@ -99,7 +86,7 @@ class Action(base.WatcherObject): action = Action._from_db_object(cls(context), db_action) return action - @classmethod + @base.remotable_classmethod def list(cls, context, limit=None, marker=None, filters=None, sort_key=None, sort_dir=None): """Return a list of Action objects. @@ -111,7 +98,6 @@ class Action(base.WatcherObject): :param sort_key: column to sort results by. :param sort_dir: direction to sort. "asc" or "desc". :returns: a list of :class:`Action` object. - """ db_actions = cls.dbapi.get_action_list(context, limit=limit, @@ -119,84 +105,48 @@ class Action(base.WatcherObject): filters=filters, sort_key=sort_key, sort_dir=sort_dir) - return Action._from_db_object_list(db_actions, cls, context) - def create(self, context=None): - """Create a Action record in the DB. + return [cls._from_db_object(cls(context), obj) + for obj in db_actions] - :param context: Security context. NOTE: This should only - be used internally by the indirection_api. - Unfortunately, RPC requires context as the first - argument, even though we don't use it. - A context should be set when instantiating the - object, e.g.: Action(context) - - """ + @base.remotable + def create(self): + """Create a Action record in the DB""" values = self.obj_get_changes() db_action = self.dbapi.create_action(values) self._from_db_object(self, db_action) - def destroy(self, context=None): - """Delete the Action from the DB. - - :param context: Security context. NOTE: This should only - be used internally by the indirection_api. - Unfortunately, RPC requires context as the first - argument, even though we don't use it. - A context should be set when instantiating the - object, e.g.: Action(context) - """ + def destroy(self): + """Delete the Action from the DB""" self.dbapi.destroy_action(self.uuid) self.obj_reset_changes() - def save(self, context=None): + @base.remotable + def save(self): """Save updates to this Action. Updates will be made column by column based on the result of self.what_changed(). - - :param context: Security context. NOTE: This should only - be used internally by the indirection_api. - Unfortunately, RPC requires context as the first - argument, even though we don't use it. - A context should be set when instantiating the - object, e.g.: Action(context) """ updates = self.obj_get_changes() self.dbapi.update_action(self.uuid, updates) self.obj_reset_changes() - def refresh(self, context=None): + @base.remotable + def refresh(self): """Loads updates for this Action. Loads a action with the same uuid from the database and checks for updated attributes. Updates are applied from the loaded action column by column, if there are any updates. - - :param context: Security context. NOTE: This should only - be used internally by the indirection_api. - Unfortunately, RPC requires context as the first - argument, even though we don't use it. - A context should be set when instantiating the - object, e.g.: Action(context) """ current = self.__class__.get_by_uuid(self._context, uuid=self.uuid) - for field in self.fields: - if (hasattr(self, base.get_attrname(field)) and - self[field] != current[field]): - self[field] = current[field] + self.obj_refresh(current) - def soft_delete(self, context=None): - """soft Delete the Audit from the DB. - - :param context: Security context. NOTE: This should only - be used internally by the indirection_api. - Unfortunately, RPC requires context as the first - argument, even though we don't use it. - A context should be set when instantiating the - object, e.g.: Audit(context) - """ + @base.remotable + def soft_delete(self): + """Soft Delete the Audit from the DB""" self.dbapi.soft_delete_action(self.uuid) - self.state = "DELETED" + self.state = State.DELETED self.save() diff --git a/watcher/objects/action_plan.py b/watcher/objects/action_plan.py index 1286f5d1f..6d2e4c04e 100644 --- a/watcher/objects/action_plan.py +++ b/watcher/objects/action_plan.py @@ -45,8 +45,9 @@ composed of two types of Action Item(s): An :ref:`Action Plan ` may be described using standard workflow model description formats such as -`Business Process Model and Notation 2.0 (BPMN 2.0) `_ -or `Unified Modeling Language (UML) `_. +`Business Process Model and Notation 2.0 (BPMN 2.0) +`_ or `Unified Modeling Language (UML) +`_. An :ref:`Action Plan ` has a life-cycle and its current state may be one of the following: @@ -66,7 +67,7 @@ state may be one of the following: - **CANCELLED** : the :ref:`Action Plan ` was in **PENDING** or **ONGOING** state and was cancelled by the :ref:`Administrator ` -""" # noqa +""" from watcher.common import exception from watcher.common import utils @@ -74,7 +75,7 @@ from watcher.db import api as dbapi from watcher.objects import action as action_objects from watcher.objects import base from watcher.objects import efficacy_indicator as indicator_objects -from watcher.objects import utils as obj_utils +from watcher.objects import fields as wfields class State(object): @@ -87,38 +88,25 @@ class State(object): CANCELLED = 'CANCELLED' -class ActionPlan(base.WatcherObject): +@base.WatcherObjectRegistry.register +class ActionPlan(base.WatcherPersistentObject, base.WatcherObject, + base.WatcherObjectDictCompat): # Version 1.0: Initial version VERSION = '1.0' dbapi = dbapi.get_instance() fields = { - 'id': int, - 'uuid': obj_utils.str_or_none, - 'audit_id': obj_utils.int_or_none, - 'strategy_id': obj_utils.int_or_none, - 'first_action_id': obj_utils.int_or_none, - 'state': obj_utils.str_or_none, - 'global_efficacy': obj_utils.dict_or_none, + 'id': wfields.IntegerField(), + 'uuid': wfields.UUIDField(), + 'audit_id': wfields.IntegerField(nullable=True), + 'strategy_id': wfields.IntegerField(), + 'first_action_id': wfields.IntegerField(nullable=True), + 'state': wfields.StringField(nullable=True), + 'global_efficacy': wfields.FlexibleDictField(nullable=True), } - @staticmethod - def _from_db_object(action_plan, db_action_plan): - """Converts a database entity to a formal object.""" - for field in action_plan.fields: - action_plan[field] = db_action_plan[field] - - action_plan.obj_reset_changes() - return action_plan - - @staticmethod - def _from_db_object_list(db_objects, cls, context): - """Converts a list of database entities to a list of formal objects.""" - return [ActionPlan._from_db_object( - cls(context), obj) for obj in db_objects] - - @classmethod + @base.remotable_classmethod def get(cls, context, action_plan_id): """Find a action_plan based on its id or uuid and return a Action object. @@ -132,7 +120,7 @@ class ActionPlan(base.WatcherObject): else: raise exception.InvalidIdentity(identity=action_plan_id) - @classmethod + @base.remotable_classmethod def get_by_id(cls, context, action_plan_id): """Find a action_plan based on its integer id and return a Action object. @@ -145,7 +133,7 @@ class ActionPlan(base.WatcherObject): cls(context), db_action_plan) return action_plan - @classmethod + @base.remotable_classmethod def get_by_uuid(cls, context, uuid): """Find a action_plan based on uuid and return a :class:`Action` object. @@ -157,7 +145,7 @@ class ActionPlan(base.WatcherObject): action_plan = ActionPlan._from_db_object(cls(context), db_action_plan) return action_plan - @classmethod + @base.remotable_classmethod def list(cls, context, limit=None, marker=None, filters=None, sort_key=None, sort_dir=None): """Return a list of Action objects. @@ -177,33 +165,20 @@ class ActionPlan(base.WatcherObject): filters=filters, sort_key=sort_key, sort_dir=sort_dir) - return ActionPlan._from_db_object_list(db_action_plans, cls, context) - def create(self, context=None): - """Create a Action record in the DB. + return [cls._from_db_object(cls(context), obj) + for obj in db_action_plans] - :param context: Security context. NOTE: This should only - be used internally by the indirection_api. - Unfortunately, RPC requires context as the first - argument, even though we don't use it. - A context should be set when instantiating the - object, e.g.: Action(context) - - """ + @base.remotable + def create(self): + """Create a Action record in the DB""" values = self.obj_get_changes() db_action_plan = self.dbapi.create_action_plan(values) self._from_db_object(self, db_action_plan) - def destroy(self, context=None): - """Delete the action plan from the DB. - - :param context: Security context. NOTE: This should only - be used internally by the indirection_api. - Unfortunately, RPC requires context as the first - argument, even though we don't use it. - A context should be set when instantiating the - object, e.g.: Action(context) - """ + @base.remotable + def destroy(self): + """Delete the action plan from the DB""" related_efficacy_indicators = indicator_objects.EfficacyIndicator.list( context=self._context, filters={"action_plan_uuid": self.uuid}) @@ -215,54 +190,32 @@ class ActionPlan(base.WatcherObject): self.dbapi.destroy_action_plan(self.uuid) self.obj_reset_changes() - def save(self, context=None): + @base.remotable + def save(self): """Save updates to this Action plan. Updates will be made column by column based on the result of self.what_changed(). - - :param context: Security context. NOTE: This should only - be used internally by the indirection_api. - Unfortunately, RPC requires context as the first - argument, even though we don't use it. - A context should be set when instantiating the - object, e.g.: Action(context) """ updates = self.obj_get_changes() self.dbapi.update_action_plan(self.uuid, updates) self.obj_reset_changes() - def refresh(self, context=None): + @base.remotable + def refresh(self): """Loads updates for this Action plan. Loads a action_plan with the same uuid from the database and checks for updated attributes. Updates are applied from the loaded action_plan column by column, if there are any updates. - - :param context: Security context. NOTE: This should only - be used internally by the indirection_api. - Unfortunately, RPC requires context as the first - argument, even though we don't use it. - A context should be set when instantiating the - object, e.g.: Action(context) """ current = self.__class__.get_by_uuid(self._context, uuid=self.uuid) - for field in self.fields: - if (hasattr(self, base.get_attrname(field)) and - self[field] != current[field]): - self[field] = current[field] + self.obj_refresh(current) - def soft_delete(self, context=None): - """Soft Delete the Action plan from the DB. - - :param context: Security context. NOTE: This should only - be used internally by the indirection_api. - Unfortunately, RPC requires context as the first - argument, even though we don't use it. - A context should be set when instantiating the - object, e.g.: Audit(context) - """ + @base.remotable + def soft_delete(self): + """Soft Delete the Action plan from the DB""" related_actions = action_objects.Action.list( context=self._context, filters={"action_plan_uuid": self.uuid}) diff --git a/watcher/objects/audit.py b/watcher/objects/audit.py index 14a7cfc61..c0a3994b5 100644 --- a/watcher/objects/audit.py +++ b/watcher/objects/audit.py @@ -54,7 +54,7 @@ from watcher.common import exception from watcher.common import utils from watcher.db import api as dbapi from watcher.objects import base -from watcher.objects import utils as obj_utils +from watcher.objects import fields as wfields class State(object): @@ -72,40 +72,27 @@ class AuditType(enum.Enum): CONTINUOUS = 'CONTINUOUS' -class Audit(base.WatcherObject): +@base.WatcherObjectRegistry.register +class Audit(base.WatcherPersistentObject, base.WatcherObject, + base.WatcherObjectDictCompat): # Version 1.0: Initial version VERSION = '1.0' dbapi = dbapi.get_instance() fields = { - 'id': int, - 'uuid': obj_utils.str_or_none, - 'audit_type': obj_utils.str_or_none, - 'state': obj_utils.str_or_none, - 'parameters': obj_utils.dict_or_none, - 'interval': obj_utils.int_or_none, - 'goal_id': obj_utils.int_or_none, - 'strategy_id': obj_utils.int_or_none, - 'scope': obj_utils.list_or_none, + 'id': wfields.IntegerField(), + 'uuid': wfields.UUIDField(), + 'audit_type': wfields.StringField(), + 'state': wfields.StringField(), + 'parameters': wfields.FlexibleDictField(nullable=True), + 'interval': wfields.IntegerField(nullable=True), + 'scope': wfields.FlexibleListOfDictField(nullable=True), + 'goal_id': wfields.IntegerField(), + 'strategy_id': wfields.IntegerField(nullable=True), } - @staticmethod - def _from_db_object(audit, db_audit): - """Converts a database entity to a formal object.""" - for field in audit.fields: - audit[field] = db_audit[field] - - audit.obj_reset_changes() - return audit - - @staticmethod - def _from_db_object_list(db_objects, cls, context): - """Converts a list of database entities to a list of formal objects.""" - return \ - [Audit._from_db_object(cls(context), obj) for obj in db_objects] - - @classmethod + @base.remotable_classmethod def get(cls, context, audit_id): """Find a audit based on its id or uuid and return a Audit object. @@ -125,7 +112,7 @@ class Audit(base.WatcherObject): else: raise exception.InvalidIdentity(identity=audit_id) - @classmethod + @base.remotable_classmethod def get_by_id(cls, context, audit_id): """Find a audit based on its integer id and return a Audit object. @@ -142,7 +129,7 @@ class Audit(base.WatcherObject): audit = Audit._from_db_object(cls(context), db_audit) return audit - @classmethod + @base.remotable_classmethod def get_by_uuid(cls, context, uuid): """Find a audit based on uuid and return a :class:`Audit` object. @@ -160,7 +147,7 @@ class Audit(base.WatcherObject): audit = Audit._from_db_object(cls(context), db_audit) return audit - @classmethod + @base.remotable_classmethod def list(cls, context, limit=None, marker=None, filters=None, sort_key=None, sort_dir=None): """Return a list of Audit objects. @@ -185,84 +172,46 @@ class Audit(base.WatcherObject): filters=filters, sort_key=sort_key, sort_dir=sort_dir) - return Audit._from_db_object_list(db_audits, cls, context) + return [cls._from_db_object(cls(context), obj) for obj in db_audits] - def create(self, context=None): - """Create a Audit record in the DB. - - :param context: Security context. NOTE: This should only - be used internally by the indirection_api. - Unfortunately, RPC requires context as the first - argument, even though we don't use it. - A context should be set when instantiating the - object, e.g.: Audit(context) - - """ + @base.remotable + def create(self): + """Create a Audit record in the DB.""" values = self.obj_get_changes() db_audit = self.dbapi.create_audit(values) self._from_db_object(self, db_audit) - def destroy(self, context=None): - """Delete the Audit from the DB. - - :param context: Security context. NOTE: This should only - be used internally by the indirection_api. - Unfortunately, RPC requires context as the first - argument, even though we don't use it. - A context should be set when instantiating the - object, e.g.: Audit(context) - """ + def destroy(self): + """Delete the Audit from the DB.""" self.dbapi.destroy_audit(self.uuid) self.obj_reset_changes() - def save(self, context=None): + @base.remotable + def save(self): """Save updates to this Audit. Updates will be made column by column based on the result of self.what_changed(). - - :param context: Security context. NOTE: This should only - be used internally by the indirection_api. - Unfortunately, RPC requires context as the first - argument, even though we don't use it. - A context should be set when instantiating the - object, e.g.: Audit(context) """ updates = self.obj_get_changes() self.dbapi.update_audit(self.uuid, updates) self.obj_reset_changes() - def refresh(self, context=None): + @base.remotable + def refresh(self): """Loads updates for this Audit. Loads a audit with the same uuid from the database and checks for updated attributes. Updates are applied from the loaded audit column by column, if there are any updates. - - :param context: Security context. NOTE: This should only - be used internally by the indirection_api. - Unfortunately, RPC requires context as the first - argument, even though we don't use it. - A context should be set when instantiating the - object, e.g.: Audit(context) """ current = self.__class__.get_by_uuid(self._context, uuid=self.uuid) - for field in self.fields: - if (hasattr(self, base.get_attrname(field)) and - self[field] != current[field]): - self[field] = current[field] + self.obj_refresh(current) - def soft_delete(self, context=None): - """soft Delete the Audit from the DB. - - :param context: Security context. NOTE: This should only - be used internally by the indirection_api. - Unfortunately, RPC requires context as the first - argument, even though we don't use it. - A context should be set when instantiating the - object, e.g.: Audit(context) - """ + @base.remotable + def soft_delete(self): + """Soft Delete the Audit from the DB.""" self.dbapi.soft_delete_audit(self.uuid) - self.state = "DELETED" + self.state = State.DELETED self.save() diff --git a/watcher/objects/audit_template.py b/watcher/objects/audit_template.py index 24c7cc1c9..842c556d8 100644 --- a/watcher/objects/audit_template.py +++ b/watcher/objects/audit_template.py @@ -51,41 +51,28 @@ from watcher.common import exception from watcher.common import utils from watcher.db import api as dbapi from watcher.objects import base -from watcher.objects import utils as obj_utils +from watcher.objects import fields as wfields -class AuditTemplate(base.WatcherObject): +@base.WatcherObjectRegistry.register +class AuditTemplate(base.WatcherPersistentObject, base.WatcherObject, + base.WatcherObjectDictCompat): # Version 1.0: Initial version VERSION = '1.0' dbapi = dbapi.get_instance() fields = { - 'id': int, - 'uuid': obj_utils.str_or_none, - 'name': obj_utils.str_or_none, - 'description': obj_utils.str_or_none, - 'goal_id': obj_utils.int_or_none, - 'strategy_id': obj_utils.int_or_none, - 'scope': obj_utils.list_or_none, + 'id': wfields.IntegerField(), + 'uuid': wfields.UUIDField(), + 'name': wfields.StringField(), + 'description': wfields.StringField(nullable=True), + 'scope': wfields.FlexibleListOfDictField(nullable=True), + 'goal_id': wfields.IntegerField(), + 'strategy_id': wfields.IntegerField(nullable=True), } - @staticmethod - def _from_db_object(audit_template, db_audit_template): - """Converts a database entity to a formal object.""" - for field in audit_template.fields: - audit_template[field] = db_audit_template[field] - - audit_template.obj_reset_changes() - return audit_template - - @staticmethod - def _from_db_object_list(db_objects, cls, context): - """Converts a list of database entities to a list of formal objects.""" - return [AuditTemplate._from_db_object(cls(context), obj) - for obj in db_objects] - - @classmethod + @base.remotable_classmethod def get(cls, context, audit_template_id): """Find an audit template based on its id or uuid @@ -98,7 +85,6 @@ class AuditTemplate(base.WatcherObject): :param audit_template_id: the id *or* uuid of a audit_template. :returns: a :class:`AuditTemplate` object. """ - if utils.is_int_like(audit_template_id): return cls.get_by_id(context, audit_template_id) elif utils.is_uuid_like(audit_template_id): @@ -106,7 +92,7 @@ class AuditTemplate(base.WatcherObject): else: raise exception.InvalidIdentity(identity=audit_template_id) - @classmethod + @base.remotable_classmethod def get_by_id(cls, context, audit_template_id): """Find an audit template based on its integer id @@ -119,7 +105,6 @@ class AuditTemplate(base.WatcherObject): :param audit_template_id: the id of a audit_template. :returns: a :class:`AuditTemplate` object. """ - db_audit_template = cls.dbapi.get_audit_template_by_id( context, audit_template_id) @@ -127,7 +112,7 @@ class AuditTemplate(base.WatcherObject): db_audit_template) return audit_template - @classmethod + @base.remotable_classmethod def get_by_uuid(cls, context, uuid): """Find an audit template based on uuid @@ -140,13 +125,12 @@ class AuditTemplate(base.WatcherObject): :param uuid: the uuid of a audit_template. :returns: a :class:`AuditTemplate` object. """ - db_audit_template = cls.dbapi.get_audit_template_by_uuid(context, uuid) audit_template = AuditTemplate._from_db_object(cls(context), db_audit_template) return audit_template - @classmethod + @base.remotable_classmethod def get_by_name(cls, context, name): """Find an audit template based on name @@ -154,13 +138,12 @@ class AuditTemplate(base.WatcherObject): :param context: Security context :returns: a :class:`AuditTemplate` object. """ - db_audit_template = cls.dbapi.get_audit_template_by_name(context, name) audit_template = AuditTemplate._from_db_object(cls(context), db_audit_template) return audit_template - @classmethod + @base.remotable_classmethod def list(cls, context, filters=None, limit=None, marker=None, sort_key=None, sort_dir=None): """Return a list of :class:`AuditTemplate` objects. @@ -178,7 +161,6 @@ class AuditTemplate(base.WatcherObject): :param sort_dir: direction to sort. "asc" or "desc". :returns: a list of :class:`AuditTemplate` object. """ - db_audit_templates = cls.dbapi.get_audit_template_list( context, filters=filters, @@ -186,87 +168,46 @@ class AuditTemplate(base.WatcherObject): marker=marker, sort_key=sort_key, sort_dir=sort_dir) - return AuditTemplate._from_db_object_list(db_audit_templates, - cls, context) - def create(self, context=None): - """Create a :class:`AuditTemplate` record in the DB. - - :param context: Security context. NOTE: This should only - be used internally by the indirection_api. - Unfortunately, RPC requires context as the first - argument, even though we don't use it. - A context should be set when instantiating the - object, e.g.: AuditTemplate(context) - """ + return [cls._from_db_object(cls(context), obj) + for obj in db_audit_templates] + @base.remotable + def create(self): + """Create a :class:`AuditTemplate` record in the DB""" values = self.obj_get_changes() db_audit_template = self.dbapi.create_audit_template(values) self._from_db_object(self, db_audit_template) - def destroy(self, context=None): - """Delete the :class:`AuditTemplate` from the DB. - - :param context: Security context. NOTE: This should only - be used internally by the indirection_api. - Unfortunately, RPC requires context as the first - argument, even though we don't use it. - A context should be set when instantiating the - object, e.g.: AuditTemplate(context) - """ - + def destroy(self): + """Delete the :class:`AuditTemplate` from the DB""" self.dbapi.destroy_audit_template(self.uuid) self.obj_reset_changes() - def save(self, context=None): + @base.remotable + def save(self): """Save updates to this :class:`AuditTemplate`. Updates will be made column by column based on the result of self.what_changed(). - - :param context: Security context. NOTE: This should only - be used internally by the indirection_api. - Unfortunately, RPC requires context as the first - argument, even though we don't use it. - A context should be set when instantiating the - object, e.g.: AuditTemplate(context) """ - updates = self.obj_get_changes() self.dbapi.update_audit_template(self.uuid, updates) self.obj_reset_changes() - def refresh(self, context=None): + @base.remotable + def refresh(self): """Loads updates for this :class:`AuditTemplate`. Loads a audit_template with the same uuid from the database and checks for updated attributes. Updates are applied from the loaded audit_template column by column, if there are any updates. - - :param context: Security context. NOTE: This should only - be used internally by the indirection_api. - Unfortunately, RPC requires context as the first - argument, even though we don't use it. - A context should be set when instantiating the - object, e.g.: AuditTemplate(context) """ - current = self.__class__.get_by_uuid(self._context, uuid=self.uuid) - for field in self.fields: - if (hasattr(self, base.get_attrname(field)) and - self[field] != current[field]): - self[field] = current[field] - - def soft_delete(self, context=None): - """soft Delete the :class:`AuditTemplate` from the DB. - - :param context: Security context. NOTE: This should only - be used internally by the indirection_api. - Unfortunately, RPC requires context as the first - argument, even though we don't use it. - A context should be set when instantiating the - object, e.g.: AuditTemplate(context) - """ + self.obj_refresh(current) + @base.remotable + def soft_delete(self): + """Soft Delete the :class:`AuditTemplate` from the DB""" self.dbapi.soft_delete_audit_template(self.uuid) diff --git a/watcher/objects/base.py b/watcher/objects/base.py index 67f1c73d8..d23e3fd48 100644 --- a/watcher/objects/base.py +++ b/watcher/objects/base.py @@ -14,109 +14,62 @@ """Watcher common internal object model""" -import collections -import copy - -from oslo_log import log as logging -import oslo_messaging as messaging from oslo_utils import versionutils -import six +from oslo_versionedobjects import base as ovo_base +from oslo_versionedobjects import fields as ovo_fields -from watcher._i18n import _ -from watcher._i18n import _LE -from watcher.common import exception -from watcher.objects import utils as obj_utils +from watcher import objects - -LOG = logging.getLogger('object') - - -class NotSpecifiedSentinel(object): - pass +remotable_classmethod = ovo_base.remotable_classmethod +remotable = ovo_base.remotable def get_attrname(name): """Return the mangled name of the attribute's underlying storage.""" - return '_%s' % name + # FIXME(danms): This is just until we use o.vo's class properties + # and object base. + return '_obj_' + name -def make_class_properties(cls): - # NOTE(danms/comstud): Inherit fields from super classes. - # mro() returns the current class first and returns 'object' last, so - # those can be skipped. Also be careful to not overwrite any fields - # that already exist. And make sure each cls has its own copy of - # fields and that it is not sharing the dict with a super class. - cls.fields = dict(cls.fields) - for supercls in cls.mro()[1:-1]: - if not hasattr(supercls, 'fields'): - continue - for name, field in supercls.fields.items(): - if name not in cls.fields: - cls.fields[name] = field - for name, typefn in cls.fields.items(): +class WatcherObjectRegistry(ovo_base.VersionedObjectRegistry): + notification_classes = [] - def getter(self, name=name): - attrname = get_attrname(name) - if not hasattr(self, attrname): - self.obj_load_attr(name) - return getattr(self, attrname) - - def setter(self, value, name=name, typefn=typefn): - self._changed_fields.add(name) - try: - return setattr(self, get_attrname(name), typefn(value)) - except Exception: - attr = "%s.%s" % (self.obj_name(), name) - LOG.exception(_LE('Error setting %(attr)s'), - {'attr': attr}) - raise - - setattr(cls, name, property(getter, setter)) - - -class WatcherObjectMetaclass(type): - """Metaclass that allows tracking of object classes.""" - - # NOTE(danms): This is what controls whether object operations are - # remoted. If this is not None, use it to remote things over RPC. - indirection_api = None - - def __init__(cls, names, bases, dict_): - if not hasattr(cls, '_obj_classes'): - # This will be set in the 'WatcherObject' class. - cls._obj_classes = collections.defaultdict(list) + def registration_hook(self, cls, index): + # NOTE(danms): This is called when an object is registered, + # and is responsible for maintaining watcher.objects.$OBJECT + # as the highest-versioned implementation of a given object. + version = versionutils.convert_version_to_tuple(cls.VERSION) + if not hasattr(objects, cls.obj_name()): + setattr(objects, cls.obj_name(), cls) else: - # Add the subclass to WatcherObject._obj_classes - make_class_properties(cls) - cls._obj_classes[cls.obj_name()].append(cls) + cur_version = versionutils.convert_version_to_tuple( + getattr(objects, cls.obj_name()).VERSION) + if version >= cur_version: + setattr(objects, cls.obj_name(), cls) + + @classmethod + def register_notification(cls, notification_cls): + """Register a class as notification. + + Use only to register concrete notification or payload classes, + do not register base classes intended for inheritance only. + """ + cls.register_if(False)(notification_cls) + cls.notification_classes.append(notification_cls) + return notification_cls + + @classmethod + def register_notification_objects(cls): + """Register previously decorated notification as normal ovos. + + This is not intended for production use but only for testing and + document generation purposes. + """ + for notification_cls in cls.notification_classes: + cls.register(notification_cls) -# Object versioning rules -# -# Each service has its set of objects, each with a version attached. When -# a client attempts to call an object method, the server checks to see if -# the version of that object matches (in a compatible way) its object -# implementation. If so, cool, and if not, fail. -def check_object_version(server, client): - try: - client_major, _client_minor = client.split('.') - server_major, _server_minor = server.split('.') - client_minor = int(_client_minor) - server_minor = int(_server_minor) - except ValueError: - raise exception.IncompatibleObjectVersion( - _('Invalid version string')) - - if client_major != server_major: - raise exception.IncompatibleObjectVersion( - dict(client=client_major, server=server_major)) - if client_minor > server_minor: - raise exception.IncompatibleObjectVersion( - dict(client=client_minor, server=server_minor)) - - -@six.add_metaclass(WatcherObjectMetaclass) -class WatcherObject(object): +class WatcherObject(ovo_base.VersionedObject): """Base class and object factory. This forms the base of all objects that can be remoted or instantiated @@ -126,431 +79,59 @@ class WatcherObject(object): as appropriate. """ - # Version of this object (see rules above check_object_version()) - VERSION = '1.0' - - # The fields present in this object as key:typefn pairs. For example: - # - # fields = { 'foo': int, - # 'bar': str, - # 'baz': lambda x: str(x).ljust(8), - # } - # - # NOTE(danms): The base WatcherObject class' fields will be inherited - # by subclasses, but that is a special case. Objects inheriting from - # other objects will not receive this merging of fields contents. - fields = { - 'created_at': obj_utils.datetime_or_str_or_none, - 'updated_at': obj_utils.datetime_or_str_or_none, - 'deleted_at': obj_utils.datetime_or_str_or_none, - } - obj_extra_fields = [] - - _attr_created_at_from_primitive = obj_utils.dt_deserializer - _attr_updated_at_from_primitive = obj_utils.dt_deserializer - _attr_created_at_to_primitive = obj_utils.dt_serializer('created_at') - _attr_updated_at_to_primitive = obj_utils.dt_serializer('updated_at') - _attr_deleted_at_to_primitive = obj_utils.dt_serializer('deleted_at') - - def __init__(self, context, **kwargs): - self._changed_fields = set() - self._context = context - self.update(kwargs) - - @classmethod - def obj_name(cls): - """Get canonical object name. - - This object name will be used over the wire for remote hydration. - """ - return cls.__name__ - - @classmethod - def obj_class_from_name(cls, objname, objver): - """Returns a class from the registry based on a name and version.""" - if objname not in cls._obj_classes: - LOG.error(_LE('Unable to instantiate unregistered object type ' - '%(objtype)s'), dict(objtype=objname)) - raise exception.UnsupportedObjectError(objtype=objname) - - latest = None - compatible_match = None - for objclass in cls._obj_classes[objname]: - if objclass.VERSION == objver: - return objclass - - version_bits = tuple([int(x) for x in objclass.VERSION.split(".")]) - if latest is None: - latest = version_bits - elif latest < version_bits: - latest = version_bits - - if versionutils.is_compatible(objver, objclass.VERSION): - compatible_match = objclass - - if compatible_match: - return compatible_match - - latest_ver = '%i.%i' % latest - raise exception.IncompatibleObjectVersion(objname=objname, - objver=objver, - supported=latest_ver) - - def _attr_from_primitive(self, attribute, value): - """Attribute deserialization dispatcher. - - This calls self._attr_foo_from_primitive(value) for an attribute - foo with value, if it exists, otherwise it assumes the value - is suitable for the attribute's setter method. - """ - handler = '_attr_%s_from_primitive' % attribute - if hasattr(self, handler): - return getattr(self, handler)(value) - return value - - @classmethod - def _obj_from_primitive(cls, context, objver, primitive): - self = cls(context) - self.VERSION = objver - objdata = primitive['watcher_object.data'] - changes = primitive.get('watcher_object.changes', []) - for name in self.fields: - if name in objdata: - setattr(self, name, - self._attr_from_primitive(name, objdata[name])) - self._changed_fields = set([x for x in changes if x in self.fields]) - return self - - @classmethod - def obj_from_primitive(cls, primitive, context=None): - """Simple base-case hydration. - - This calls self._attr_from_primitive() for each item in fields. - """ - if primitive['watcher_object.namespace'] != 'watcher': - # NOTE(danms): We don't do anything with this now, but it's - # there for "the future" - raise exception.UnsupportedObjectError( - objtype='%s.%s' % (primitive['watcher_object.namespace'], - primitive['watcher_object.name'])) - objname = primitive['watcher_object.name'] - objver = primitive['watcher_object.version'] - objclass = cls.obj_class_from_name(objname, objver) - return objclass._obj_from_primitive(context, objver, primitive) - - def __deepcopy__(self, memo): - """Efficiently make a deep copy of this object.""" - - # NOTE(danms): A naive deepcopy would copy more than we need, - # and since we have knowledge of the volatile bits of the - # object, we can be smarter here. Also, nested entities within - # some objects may be uncopyable, so we can avoid those sorts - # of issues by copying only our field data. - - nobj = self.__class__(self._context) - for name in self.fields: - if self.obj_attr_is_set(name): - nval = copy.deepcopy(getattr(self, name), memo) - setattr(nobj, name, nval) - nobj._changed_fields = set(self._changed_fields) - return nobj - - def obj_clone(self): - """Create a copy.""" - return copy.deepcopy(self) - - def _attr_to_primitive(self, attribute): - """Attribute serialization dispatcher. - - This calls self._attr_foo_to_primitive() for an attribute foo, - if it exists, otherwise it assumes the attribute itself is - primitive-enough to be sent over the RPC wire. - """ - handler = '_attr_%s_to_primitive' % attribute - if hasattr(self, handler): - return getattr(self, handler)() - else: - return getattr(self, attribute) - - def obj_to_primitive(self): - """Simple base-case dehydration. - - This calls self._attr_to_primitive() for each item in fields. - """ - primitive = dict() - for name in self.fields: - if hasattr(self, get_attrname(name)): - primitive[name] = self._attr_to_primitive(name) - obj = {'watcher_object.name': self.obj_name(), - 'watcher_object.namespace': 'watcher', - 'watcher_object.version': self.VERSION, - 'watcher_object.data': primitive} - if self.obj_what_changed(): - obj['watcher_object.changes'] = list(self.obj_what_changed()) - return obj - - def obj_load_attr(self, attrname): - """Load an additional attribute from the real object. - - This should use self._conductor, and cache any data that might - be useful for future load operations. - """ - raise NotImplementedError( - _("Cannot load '%(attrname)s' in the base class") % - {'attrname': attrname}) - - def save(self, context): - """Save the changed fields back to the store. - - This is optional for subclasses, but is presented here in the base - class for consistency among those that do. - """ - raise NotImplementedError(_("Cannot save anything in the base class")) - - def obj_get_changes(self): - """Returns a dict of changed fields and their new values.""" - changes = {} - for key in self.obj_what_changed(): - changes[key] = self._attr_to_primitive(key) - return changes - - def obj_what_changed(self): - """Returns a set of fields that have been modified.""" - return self._changed_fields - - def obj_reset_changes(self, fields=None): - """Reset the list of fields that have been changed. - - Note that this is NOT "revert to previous values" - """ - if fields: - self._changed_fields -= set(fields) - else: - self._changed_fields.clear() - - def obj_attr_is_set(self, attrname): - """Test object to see if attrname is present. - - Returns True if the named attribute has a value set, or - False if not. Raises AttributeError if attrname is not - a valid attribute for this object. - """ - if attrname not in self.obj_fields: - raise AttributeError( - _("%(objname)s object has no attribute '%(attrname)s'") % - {'objname': self.obj_name(), 'attrname': attrname}) - return hasattr(self, get_attrname(attrname)) - - @property - def obj_fields(self): - return list(self.fields.keys()) + self.obj_extra_fields - - # dictish syntactic sugar - def iteritems(self): - """For backwards-compatibility with dict-based objects. - - NOTE(danms): May be removed in the future. - """ - return self._iteritems() - - # dictish syntactic sugar, internal to pass hacking checks - def _iteritems(self): - """For backwards-compatibility with dict-based objects. - - NOTE(danms): May be removed in the future. - """ - for name in list(self.fields.keys()) + self.obj_extra_fields: - if (hasattr(self, get_attrname(name)) or - name in self.obj_extra_fields): - yield name, getattr(self, name) - - def items(self): - return list(self._iteritems()) - - def __getitem__(self, name): - """For backwards-compatibility with dict-based objects. - - NOTE(danms): May be removed in the future. - """ - return getattr(self, name) - - def __setitem__(self, name, value): - """For backwards-compatibility with dict-based objects. - - NOTE(danms): May be removed in the future. - """ - setattr(self, name, value) - - def __contains__(self, name): - """For backwards-compatibility with dict-based objects. - - NOTE(danms): May be removed in the future. - """ - return hasattr(self, get_attrname(name)) - - def get(self, key, value=NotSpecifiedSentinel): - """For backwards-compatibility with dict-based objects. - - NOTE(danms): May be removed in the future. - """ - if key not in self.obj_fields: - raise AttributeError( - _("'%(objclass)s' object has no attribute '%(attrname)s'") % - {'objclass': self.__class__, 'attrname': key}) - if value != NotSpecifiedSentinel and not self.obj_attr_is_set(key): - return value - else: - return self[key] - - def update(self, updates): - """For backwards-compatibility with dict-base objects. - - NOTE(danms): May be removed in the future. - """ - for key, value in updates.items(): - self[key] = value + OBJ_SERIAL_NAMESPACE = 'watcher_object' + OBJ_PROJECT_NAMESPACE = 'watcher' def as_dict(self): - return dict((k, getattr(self, k)) - for k in self.fields - if hasattr(self, k)) + return { + k: getattr(self, k) for k in self.fields + if self.obj_attr_is_set(k)} -class ObjectListBase(object): - """Mixin class for lists of objects. +class WatcherObjectDictCompat(ovo_base.VersionedObjectDictCompat): + pass - This mixin class can be added as a base class for an object that - is implementing a list of objects. It adds a single field of 'objects', - which is the list store, and behaves like a list itself. It supports - serialization of the list of objects automatically. + +class WatcherPersistentObject(object): + """Mixin class for Persistent objects. + + This adds the fields that we use in common for all persistent objects. """ fields = { - 'objects': list, + 'created_at': ovo_fields.DateTimeField(nullable=True), + 'updated_at': ovo_fields.DateTimeField(nullable=True), + 'deleted_at': ovo_fields.DateTimeField(nullable=True), } - # This is a dictionary of my_version:child_version mappings so that - # we can support backleveling our contents based on the version - # requested of the list object. - child_versions = {} + def obj_refresh(self, loaded_object): + """Applies updates for objects that inherit from base.WatcherObject. - def __iter__(self): - """List iterator interface.""" - return iter(self.objects) - - def __len__(self): - """List length.""" - return len(self.objects) - - def __getitem__(self, index): - """List index access.""" - if isinstance(index, slice): - new_obj = self.__class__(self._context) - new_obj.objects = self.objects[index] - # NOTE(danms): We must be mixed in with an WatcherObject! - new_obj.obj_reset_changes() - return new_obj - return self.objects[index] - - def __contains__(self, value): - """List membership test.""" - return value in self.objects - - def count(self, value): - """List count of value occurrences.""" - return self.objects.count(value) - - def index(self, value): - """List index of value.""" - return self.objects.index(value) - - def _attr_objects_to_primitive(self): - """Serialization of object list.""" - return [x.obj_to_primitive() for x in self.objects] - - def _attr_objects_from_primitive(self, value): - """Deserialization of object list.""" - objects = [] - for entity in value: - obj = WatcherObject.obj_from_primitive( - entity, - context=self._context) - objects.append(obj) - return objects - - def obj_make_compatible(self, primitive, target_version): - primitives = primitive['objects'] - child_target_version = self.child_versions.get(target_version, '1.0') - for index, item in enumerate(self.objects): - self.objects[index].obj_make_compatible( - primitives[index]['watcher_object.data'], - child_target_version) - primitives[index]['watcher_object.version'] = child_target_version - - def obj_what_changed(self): - changes = set(self._changed_fields) - for child in self.objects: - if child.obj_what_changed(): - changes.add('objects') - return changes - - -class WatcherObjectSerializer(messaging.NoOpSerializer): - """A WatcherObject-aware Serializer. - - This implements the Oslo Serializer interface and provides the - ability to serialize and deserialize WatcherObject entities. Any service - that needs to accept or return WatcherObjects as arguments or result values - should pass this to its RpcProxy and RpcDispatcher objects. - """ - - def _process_iterable(self, context, action_fn, values): - """Process an iterable, taking an action on each value. - - :param:context: Request context - :param:action_fn: Action to take on each item in values - :param:values: Iterable container of things to take action on - :returns: A new container of the same type (except set) with - items from values having had action applied. + Checks for updated attributes in an object. Updates are applied from + the loaded object column by column in comparison with the current + object. """ - iterable = values.__class__ - if iterable == set: - # NOTE(danms): A set can't have an unhashable value inside, such as - # a dict. Convert sets to tuples, which is fine, since we can't - # send them over RPC anyway. - iterable = tuple - return iterable([action_fn(context, value) for value in values]) + for field in self.fields: + if (self.obj_attr_is_set(field) and + self[field] != loaded_object[field]): + self[field] = loaded_object[field] - def serialize_entity(self, context, entity): - if isinstance(entity, (tuple, list, set)): - entity = self._process_iterable(context, self.serialize_entity, - entity) - elif (hasattr(entity, 'obj_to_primitive') and - callable(entity.obj_to_primitive)): - entity = entity.obj_to_primitive() - return entity + @staticmethod + def _from_db_object(obj, db_object): + """Converts a database entity to a formal object. - def deserialize_entity(self, context, entity): - if isinstance(entity, dict) and 'watcher_object.name' in entity: - entity = WatcherObject.obj_from_primitive(entity, context=context) - elif isinstance(entity, (tuple, list, set)): - entity = self._process_iterable(context, self.deserialize_entity, - entity) - return entity + :param obj: An object of the class. + :param db_object: A DB model of the object + :return: The object of the class with the database entity added + """ -def obj_to_primitive(obj): - """Recursively turn an object into a python primitive. + for field in obj.fields: + obj[field] = db_object[field] - An WatcherObject becomes a dict, and anything that implements - ObjectListBase becomes a list. - """ - - if isinstance(obj, ObjectListBase): - return [obj_to_primitive(x) for x in obj] - elif isinstance(obj, WatcherObject): - result = {} - for key, value in obj.items(): - result[key] = obj_to_primitive(value) - return result - else: + obj.obj_reset_changes() return obj + + +class WatcherObjectSerializer(ovo_base.VersionedObjectSerializer): + # Base class to use for object hydration + OBJ_BASE_CLASS = WatcherObject diff --git a/watcher/objects/efficacy_indicator.py b/watcher/objects/efficacy_indicator.py index ad36a596e..13027e1c7 100644 --- a/watcher/objects/efficacy_indicator.py +++ b/watcher/objects/efficacy_indicator.py @@ -14,46 +14,32 @@ # See the License for the specific language governing permissions and # limitations under the License. - from watcher.common import exception from watcher.common import utils -from watcher.db import api as dbapi +from watcher.db import api as db_api from watcher.objects import base -from watcher.objects import utils as obj_utils +from watcher.objects import fields as wfields -class EfficacyIndicator(base.WatcherObject): +@base.WatcherObjectRegistry.register +class EfficacyIndicator(base.WatcherPersistentObject, base.WatcherObject, + base.WatcherObjectDictCompat): # Version 1.0: Initial version VERSION = '1.0' - dbapi = dbapi.get_instance() + dbapi = db_api.get_instance() fields = { - 'id': int, - 'uuid': obj_utils.str_or_none, - 'action_plan_id': obj_utils.int_or_none, - 'name': obj_utils.str_or_none, - 'description': obj_utils.str_or_none, - 'unit': obj_utils.str_or_none, - 'value': obj_utils.numeric_or_none, + 'id': wfields.IntegerField(), + 'uuid': wfields.UUIDField(), + 'action_plan_id': wfields.IntegerField(), + 'name': wfields.StringField(), + 'description': wfields.StringField(nullable=True), + 'unit': wfields.StringField(nullable=True), + 'value': wfields.NumericField(), } - @staticmethod - def _from_db_object(efficacy_indicator, db_efficacy_indicator): - """Converts a database entity to a formal object.""" - for field in efficacy_indicator.fields: - efficacy_indicator[field] = db_efficacy_indicator[field] - - efficacy_indicator.obj_reset_changes() - return efficacy_indicator - - @staticmethod - def _from_db_object_list(db_objects, cls, context): - """Converts a list of database entities to a list of formal objects.""" - return [EfficacyIndicator._from_db_object(cls(context), obj) - for obj in db_objects] - - @classmethod + @base.remotable_classmethod def get(cls, context, efficacy_indicator_id): """Find an efficacy indicator object given its ID or UUID @@ -67,7 +53,7 @@ class EfficacyIndicator(base.WatcherObject): else: raise exception.InvalidIdentity(identity=efficacy_indicator_id) - @classmethod + @base.remotable_classmethod def get_by_id(cls, context, efficacy_indicator_id): """Find an efficacy indicator given its integer ID @@ -80,7 +66,7 @@ class EfficacyIndicator(base.WatcherObject): cls(context), db_efficacy_indicator) return efficacy_indicator - @classmethod + @base.remotable_classmethod def get_by_uuid(cls, context, uuid): """Find an efficacy indicator given its UUID @@ -94,7 +80,7 @@ class EfficacyIndicator(base.WatcherObject): cls(context), db_efficacy_indicator) return efficacy_indicator - @classmethod + @base.remotable_classmethod def list(cls, context, limit=None, marker=None, filters=None, sort_key=None, sort_dir=None): """Return a list of EfficacyIndicator objects. @@ -115,9 +101,11 @@ class EfficacyIndicator(base.WatcherObject): filters=filters, sort_key=sort_key, sort_dir=sort_dir) - return EfficacyIndicator._from_db_object_list( - db_efficacy_indicators, cls, context) + return [cls._from_db_object(cls(context), obj) + for obj in db_efficacy_indicators] + + @base.remotable def create(self, context=None): """Create a EfficacyIndicator record in the DB. @@ -146,6 +134,7 @@ class EfficacyIndicator(base.WatcherObject): self.dbapi.destroy_efficacy_indicator(self.uuid) self.obj_reset_changes() + @base.remotable def save(self, context=None): """Save updates to this EfficacyIndicator. @@ -164,6 +153,7 @@ class EfficacyIndicator(base.WatcherObject): self.obj_reset_changes() + @base.remotable def refresh(self, context=None): """Loads updates for this EfficacyIndicator. @@ -179,11 +169,9 @@ class EfficacyIndicator(base.WatcherObject): object, e.g.: EfficacyIndicator(context) """ current = self.__class__.get_by_uuid(self._context, uuid=self.uuid) - for field in self.fields: - if (hasattr(self, base.get_attrname(field)) and - self[field] != current[field]): - self[field] = current[field] + self.obj_refresh(current) + @base.remotable def soft_delete(self, context=None): """Soft Delete the efficacy indicator from the DB. diff --git a/watcher/objects/fields.py b/watcher/objects/fields.py new file mode 100644 index 000000000..c1fe2061f --- /dev/null +++ b/watcher/objects/fields.py @@ -0,0 +1,89 @@ +# Copyright 2013 IBM Corp. +# +# 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. + +"""Utility methods for objects""" + +import ast +import six + +from oslo_log import log +from oslo_versionedobjects import fields + + +LOG = log.getLogger(__name__) + + +IntegerField = fields.IntegerField +UUIDField = fields.UUIDField +StringField = fields.StringField +DateTimeField = fields.DateTimeField +BooleanField = fields.BooleanField +ListOfStringsField = fields.ListOfStringsField + + +class Numeric(fields.FieldType): + @staticmethod + def coerce(obj, attr, value): + if value is None: + return value + f_value = float(value) + return f_value if not f_value.is_integer() else value + + +class NumericField(fields.AutoTypedField): + AUTO_TYPE = Numeric() + + +class DictField(fields.AutoTypedField): + AUTO_TYPE = fields.Dict(fields.FieldType()) + + +class FlexibleDict(fields.FieldType): + @staticmethod + def coerce(obj, attr, value): + if isinstance(value, six.string_types): + value = ast.literal_eval(value) + return dict(value) + + +class FlexibleDictField(fields.AutoTypedField): + AUTO_TYPE = FlexibleDict() + + # TODO(lucasagomes): In our code we've always translated None to {}, + # this method makes this field to work like this. But probably won't + # be accepted as-is in the oslo_versionedobjects library + def _null(self, obj, attr): + if self.nullable: + return {} + super(FlexibleDictField, self)._null(obj, attr) + + +class FlexibleListOfDict(fields.FieldType): + @staticmethod + def coerce(obj, attr, value): + if isinstance(value, six.string_types): + value = ast.literal_eval(value) + return list(value) + + +class FlexibleListOfDictField(fields.AutoTypedField): + AUTO_TYPE = FlexibleListOfDict() + + # TODO(lucasagomes): In our code we've always translated None to {}, + # this method makes this field to work like this. But probably won't + # be accepted as-is in the oslo_versionedobjects library + def _null(self, obj, attr): + if self.nullable: + return [] + super(FlexibleListOfDictField, self)._null(obj, attr) diff --git a/watcher/objects/goal.py b/watcher/objects/goal.py index 630c62337..b089ab281 100644 --- a/watcher/objects/goal.py +++ b/watcher/objects/goal.py @@ -16,40 +16,28 @@ from watcher.common import exception from watcher.common import utils -from watcher.db import api as dbapi +from watcher.db import api as db_api from watcher.objects import base -from watcher.objects import utils as obj_utils +from watcher.objects import fields as wfields -class Goal(base.WatcherObject): +@base.WatcherObjectRegistry.register +class Goal(base.WatcherPersistentObject, base.WatcherObject, + base.WatcherObjectDictCompat): # Version 1.0: Initial version VERSION = '1.0' - dbapi = dbapi.get_instance() + dbapi = db_api.get_instance() fields = { - 'id': int, - 'uuid': obj_utils.str_or_none, - 'name': obj_utils.str_or_none, - 'display_name': obj_utils.str_or_none, - 'efficacy_specification': obj_utils.list_or_none, + 'id': wfields.IntegerField(), + 'uuid': wfields.UUIDField(), + 'name': wfields.StringField(), + 'display_name': wfields.StringField(), + 'efficacy_specification': wfields.FlexibleListOfDictField(), } - @staticmethod - def _from_db_object(goal, db_goal): - """Converts a database entity to a formal object.""" - for field in goal.fields: - goal[field] = db_goal[field] - - goal.obj_reset_changes() - return goal - - @staticmethod - def _from_db_object_list(db_objects, cls, context): - """Converts a list of database entities to a list of formal objects.""" - return [cls._from_db_object(cls(context), obj) for obj in db_objects] - - @classmethod + @base.remotable_classmethod def get(cls, context, goal_id): """Find a goal based on its id or uuid @@ -69,7 +57,7 @@ class Goal(base.WatcherObject): else: raise exception.InvalidIdentity(identity=goal_id) - @classmethod + @base.remotable_classmethod def get_by_id(cls, context, goal_id): """Find a goal based on its integer id @@ -86,7 +74,7 @@ class Goal(base.WatcherObject): goal = cls._from_db_object(cls(context), db_goal) return goal - @classmethod + @base.remotable_classmethod def get_by_uuid(cls, context, uuid): """Find a goal based on uuid @@ -99,12 +87,11 @@ class Goal(base.WatcherObject): :param uuid: the uuid of a goal. :returns: a :class:`Goal` object. """ - db_goal = cls.dbapi.get_goal_by_uuid(context, uuid) goal = cls._from_db_object(cls(context), db_goal) return goal - @classmethod + @base.remotable_classmethod def get_by_name(cls, context, name): """Find a goal based on name @@ -112,12 +99,11 @@ class Goal(base.WatcherObject): :param context: Security context :returns: a :class:`Goal` object. """ - db_goal = cls.dbapi.get_goal_by_name(context, name) goal = cls._from_db_object(cls(context), db_goal) return goal - @classmethod + @base.remotable_classmethod def list(cls, context, limit=None, marker=None, filters=None, sort_key=None, sort_dir=None): """Return a list of :class:`Goal` objects. @@ -142,20 +128,22 @@ class Goal(base.WatcherObject): marker=marker, sort_key=sort_key, sort_dir=sort_dir) - return cls._from_db_object_list(db_goals, cls, context) + return [cls._from_db_object(cls(context), obj) for obj in db_goals] + + @base.remotable def create(self): - """Create a :class:`Goal` record in the DB.""" - + """Create a :class:`Goal` record in the DB""" values = self.obj_get_changes() db_goal = self.dbapi.create_goal(values) self._from_db_object(self, db_goal) def destroy(self): - """Delete the :class:`Goal` from the DB.""" + """Delete the :class:`Goal` from the DB""" self.dbapi.destroy_goal(self.id) self.obj_reset_changes() + @base.remotable def save(self): """Save updates to this :class:`Goal`. @@ -167,6 +155,7 @@ class Goal(base.WatcherObject): self.obj_reset_changes() + @base.remotable def refresh(self): """Loads updates for this :class:`Goal`. @@ -175,11 +164,9 @@ class Goal(base.WatcherObject): the loaded goal column by column, if there are any updates. """ current = self.get_by_uuid(self._context, uuid=self.uuid) - for field in self.fields: - if (hasattr(self, base.get_attrname(field)) and - self[field] != current[field]): - self[field] = current[field] + self.obj_refresh(current) + @base.remotable def soft_delete(self): - """Soft Delete the :class:`Goal` from the DB.""" + """Soft Delete the :class:`Goal` from the DB""" self.dbapi.soft_delete_goal(self.uuid) diff --git a/watcher/objects/scoring_engine.py b/watcher/objects/scoring_engine.py index 6f76f82c5..dbf0484aa 100644 --- a/watcher/objects/scoring_engine.py +++ b/watcher/objects/scoring_engine.py @@ -26,42 +26,28 @@ be needed by the user of a given scoring engine. from watcher.common import exception from watcher.common import utils -from watcher.db import api as dbapi +from watcher.db import api as db_api from watcher.objects import base -from watcher.objects import utils as obj_utils +from watcher.objects import fields as wfields -class ScoringEngine(base.WatcherObject): +@base.WatcherObjectRegistry.register +class ScoringEngine(base.WatcherPersistentObject, base.WatcherObject, + base.WatcherObjectDictCompat): # Version 1.0: Initial version VERSION = '1.0' - dbapi = dbapi.get_instance() + dbapi = db_api.get_instance() fields = { - 'id': int, - 'uuid': obj_utils.str_or_none, - 'name': obj_utils.str_or_none, - 'description': obj_utils.str_or_none, - 'metainfo': obj_utils.str_or_none, + 'id': wfields.IntegerField(), + 'uuid': wfields.UUIDField(), + 'name': wfields.StringField(), + 'description': wfields.StringField(nullable=True), + 'metainfo': wfields.StringField(nullable=True), } - @staticmethod - def _from_db_object(scoring_engine, db_scoring_engine): - """Converts a database entity to a formal object.""" - for field in scoring_engine.fields: - scoring_engine[field] = db_scoring_engine[field] - - scoring_engine.obj_reset_changes() - return scoring_engine - - @staticmethod - def _from_db_object_list(db_objects, cls, context): - """Converts a list of database entities to a list of formal objects.""" - return \ - [ScoringEngine._from_db_object(cls(context), obj) - for obj in db_objects] - - @classmethod + @base.remotable_classmethod def get(cls, context, scoring_engine_id): """Find a scoring engine based on its id or uuid @@ -81,7 +67,7 @@ class ScoringEngine(base.WatcherObject): else: raise exception.InvalidIdentity(identity=scoring_engine_id) - @classmethod + @base.remotable_classmethod def get_by_id(cls, context, scoring_engine_id): """Find a scoring engine based on its id @@ -94,7 +80,6 @@ class ScoringEngine(base.WatcherObject): :param scoring_engine_id: the id of a scoring_engine. :returns: a :class:`ScoringEngine` object. """ - db_scoring_engine = cls.dbapi.get_scoring_engine_by_id( context, scoring_engine_id) @@ -102,7 +87,7 @@ class ScoringEngine(base.WatcherObject): db_scoring_engine) return scoring_engine - @classmethod + @base.remotable_classmethod def get_by_uuid(cls, context, scoring_engine_uuid): """Find a scoring engine based on its uuid @@ -115,7 +100,6 @@ class ScoringEngine(base.WatcherObject): :param scoring_engine_uuid: the uuid of a scoring_engine. :returns: a :class:`ScoringEngine` object. """ - db_scoring_engine = cls.dbapi.get_scoring_engine_by_uuid( context, scoring_engine_uuid) @@ -123,7 +107,7 @@ class ScoringEngine(base.WatcherObject): db_scoring_engine) return scoring_engine - @classmethod + @base.remotable_classmethod def get_by_name(cls, context, scoring_engine_name): """Find a scoring engine based on its name @@ -136,7 +120,6 @@ class ScoringEngine(base.WatcherObject): :param scoring_engine_name: the name of a scoring_engine. :returns: a :class:`ScoringEngine` object. """ - db_scoring_engine = cls.dbapi.get_scoring_engine_by_name( context, scoring_engine_name) @@ -144,7 +127,7 @@ class ScoringEngine(base.WatcherObject): db_scoring_engine) return scoring_engine - @classmethod + @base.remotable_classmethod def list(cls, context, filters=None, limit=None, marker=None, sort_key=None, sort_dir=None): """Return a list of :class:`ScoringEngine` objects. @@ -162,7 +145,6 @@ class ScoringEngine(base.WatcherObject): :param sort_dir: direction to sort. "asc" or "desc". :returns: a list of :class:`ScoringEngine` objects. """ - db_scoring_engines = cls.dbapi.get_scoring_engine_list( context, filters=filters, @@ -170,88 +152,43 @@ class ScoringEngine(base.WatcherObject): marker=marker, sort_key=sort_key, sort_dir=sort_dir) - return ScoringEngine._from_db_object_list(db_scoring_engines, - cls, context) - - def create(self, context=None): - """Create a :class:`ScoringEngine` record in the DB. - - :param context: Security context. NOTE: This should only - be used internally by the indirection_api. - Unfortunately, RPC requires context as the first - argument, even though we don't use it. - A context should be set when instantiating the - object, e.g.: ScoringEngine(context) - """ + return [cls._from_db_object(cls(context), obj) + for obj in db_scoring_engines] + @base.remotable + def create(self): + """Create a :class:`ScoringEngine` record in the DB.""" values = self.obj_get_changes() db_scoring_engine = self.dbapi.create_scoring_engine(values) self._from_db_object(self, db_scoring_engine) - def destroy(self, context=None): - """Delete the :class:`ScoringEngine` from the DB. - - :param context: Security context. NOTE: This should only - be used internally by the indirection_api. - Unfortunately, RPC requires context as the first - argument, even though we don't use it. - A context should be set when instantiating the - object, e.g.: ScoringEngine(context) - """ - + def destroy(self): + """Delete the :class:`ScoringEngine` from the DB""" self.dbapi.destroy_scoring_engine(self.id) self.obj_reset_changes() - def save(self, context=None): + @base.remotable + def save(self): """Save updates to this :class:`ScoringEngine`. Updates will be made column by column based on the result of self.what_changed(). - - :param context: Security context. NOTE: This should only - be used internally by the indirection_api. - Unfortunately, RPC requires context as the first - argument, even though we don't use it. - A context should be set when instantiating the - object, e.g.: ScoringEngine(context) """ - updates = self.obj_get_changes() self.dbapi.update_scoring_engine(self.id, updates) self.obj_reset_changes() - def refresh(self, context=None): + def refresh(self): """Loads updates for this :class:`ScoringEngine`. Loads a scoring_engine with the same id from the database and checks for updated attributes. Updates are applied from the loaded scoring_engine column by column, if there are any updates. - - :param context: Security context. NOTE: This should only - be used internally by the indirection_api. - Unfortunately, RPC requires context as the first - argument, even though we don't use it. - A context should be set when instantiating the - object, e.g.: ScoringEngine(context) - """ - - current = self.__class__.get_by_id(self._context, - scoring_engine_id=self.id) - for field in self.fields: - if (hasattr(self, base.get_attrname(field)) and - self[field] != current[field]): - self[field] = current[field] - - def soft_delete(self, context=None): - """soft Delete the :class:`ScoringEngine` from the DB. - - :param context: Security context. NOTE: This should only - be used internally by the indirection_api. - Unfortunately, RPC requires context as the first - argument, even though we don't use it. - A context should be set when instantiating the - object, e.g.: ScoringEngine(context) """ + current = self.get_by_id(self._context, scoring_engine_id=self.id) + self.obj_refresh(current) + def soft_delete(self): + """Soft Delete the :class:`ScoringEngine` from the DB""" self.dbapi.soft_delete_scoring_engine(self.id) diff --git a/watcher/objects/service.py b/watcher/objects/service.py index c5c4ee9d2..c215a114f 100644 --- a/watcher/objects/service.py +++ b/watcher/objects/service.py @@ -16,9 +16,9 @@ from watcher.common import exception from watcher.common import utils -from watcher.db import api as dbapi +from watcher.db import api as db_api from watcher.objects import base -from watcher.objects import utils as obj_utils +from watcher.objects import fields as wfields class ServiceStatus(object): @@ -26,33 +26,24 @@ class ServiceStatus(object): FAILED = 'FAILED' -class Service(base.WatcherObject): +@base.WatcherObjectRegistry.register +class Service(base.WatcherPersistentObject, base.WatcherObject, + base.WatcherObjectDictCompat): - dbapi = dbapi.get_instance() + # Version 1.0: Initial version + VERSION = '1.0' + + dbapi = db_api.get_instance() fields = { - 'id': int, - 'name': obj_utils.str_or_none, - 'host': obj_utils.str_or_none, - 'last_seen_up': obj_utils.datetime_or_str_or_none + 'id': wfields.IntegerField(), + 'name': wfields.StringField(), + 'host': wfields.StringField(), + 'last_seen_up': wfields.DateTimeField( + tzinfo_aware=False, nullable=True), } - @staticmethod - def _from_db_object(service, db_service): - """Converts a database entity to a formal object.""" - for field in service.fields: - service[field] = db_service[field] - - service.obj_reset_changes() - return service - - @staticmethod - def _from_db_object_list(db_objects, cls, context): - """Converts a list of database entities to a list of formal objects.""" - return [Service._from_db_object(cls(context), obj) - for obj in db_objects] - - @classmethod + @base.remotable_classmethod def get(cls, context, service_id): """Find a service based on its id @@ -72,7 +63,7 @@ class Service(base.WatcherObject): else: raise exception.InvalidIdentity(identity=service_id) - @classmethod + @base.remotable_classmethod def get_by_name(cls, context, name): """Find a service based on name @@ -85,7 +76,7 @@ class Service(base.WatcherObject): service = cls._from_db_object(cls(context), db_service) return service - @classmethod + @base.remotable_classmethod def list(cls, context, limit=None, marker=None, filters=None, sort_key=None, sort_dir=None): """Return a list of :class:`Service` objects. @@ -110,69 +101,41 @@ class Service(base.WatcherObject): marker=marker, sort_key=sort_key, sort_dir=sort_dir) - return Service._from_db_object_list(db_services, cls, context) - def create(self, context=None): - """Create a :class:`Service` record in the DB. - - :param context: Security context. NOTE: This should only - be used internally by the indirection_api. - Unfortunately, RPC requires context as the first - argument, even though we don't use it. - A context should be set when instantiating the - object, e.g.: Service(context) - """ + return [cls._from_db_object(cls(context), obj) for obj in db_services] + @base.remotable + def create(self): + """Create a :class:`Service` record in the DB.""" values = self.obj_get_changes() db_service = self.dbapi.create_service(values) self._from_db_object(self, db_service) - def save(self, context=None): + @base.remotable + def save(self): """Save updates to this :class:`Service`. Updates will be made column by column based on the result of self.what_changed(). - - :param context: Security context. NOTE: This should only - be used internally by the indirection_api. - Unfortunately, RPC requires context as the first - argument, even though we don't use it. - A context should be set when instantiating the - object, e.g.: Service(context) """ updates = self.obj_get_changes() self.dbapi.update_service(self.id, updates) self.obj_reset_changes() - def refresh(self, context=None): + def refresh(self): """Loads updates for this :class:`Service`. Loads a service with the same id from the database and checks for updated attributes. Updates are applied from the loaded service column by column, if there are any updates. - - :param context: Security context. NOTE: This should only - be used internally by the indirection_api. - Unfortunately, RPC requires context as the first - argument, even though we don't use it. - A context should be set when instantiating the - object, e.g.: Service(context) """ - current = self.__class__.get(self._context, service_id=self.id) + current = self.get(self._context, service_id=self.id) for field in self.fields: if (hasattr(self, base.get_attrname(field)) and self[field] != current[field]): self[field] = current[field] - def soft_delete(self, context=None): - """Soft Delete the :class:`Service` from the DB. - - :param context: Security context. NOTE: This should only - be used internally by the indirection_api. - Unfortunately, RPC requires context as the first - argument, even though we don't use it. - A context should be set when instantiating the - object, e.g.: Service(context) - """ + def soft_delete(self): + """Soft Delete the :class:`Service` from the DB.""" self.dbapi.soft_delete_service(self.id) diff --git a/watcher/objects/strategy.py b/watcher/objects/strategy.py index 60b35701c..6f95a103e 100644 --- a/watcher/objects/strategy.py +++ b/watcher/objects/strategy.py @@ -16,40 +16,27 @@ from watcher.common import exception from watcher.common import utils -from watcher.db import api as dbapi +from watcher.db import api as db_api from watcher.objects import base -from watcher.objects import utils as obj_utils +from watcher.objects import fields as wfields -class Strategy(base.WatcherObject): +@base.WatcherObjectRegistry.register +class Strategy(base.WatcherPersistentObject, base.WatcherObject, + base.WatcherObjectDictCompat): - dbapi = dbapi.get_instance() + dbapi = db_api.get_instance() fields = { - 'id': int, - 'uuid': obj_utils.str_or_none, - 'name': obj_utils.str_or_none, - 'display_name': obj_utils.str_or_none, - 'goal_id': obj_utils.int_or_none, - 'parameters_spec': obj_utils.dict_or_none, + 'id': wfields.IntegerField(), + 'uuid': wfields.UUIDField(), + 'name': wfields.StringField(), + 'display_name': wfields.StringField(), + 'goal_id': wfields.IntegerField(), + 'parameters_spec': wfields.FlexibleDictField(nullable=True), } - @staticmethod - def _from_db_object(strategy, db_strategy): - """Converts a database entity to a formal object.""" - for field in strategy.fields: - strategy[field] = db_strategy[field] - - strategy.obj_reset_changes() - return strategy - - @staticmethod - def _from_db_object_list(db_objects, cls, context): - """Converts a list of database entities to a list of formal objects.""" - return [Strategy._from_db_object(cls(context), obj) - for obj in db_objects] - - @classmethod + @base.remotable_classmethod def get(cls, context, strategy_id): """Find a strategy based on its id or uuid @@ -69,7 +56,7 @@ class Strategy(base.WatcherObject): else: raise exception.InvalidIdentity(identity=strategy_id) - @classmethod + @base.remotable_classmethod def get_by_id(cls, context, strategy_id): """Find a strategy based on its integer id @@ -86,7 +73,7 @@ class Strategy(base.WatcherObject): strategy = Strategy._from_db_object(cls(context), db_strategy) return strategy - @classmethod + @base.remotable_classmethod def get_by_uuid(cls, context, uuid): """Find a strategy based on uuid @@ -104,7 +91,7 @@ class Strategy(base.WatcherObject): strategy = cls._from_db_object(cls(context), db_strategy) return strategy - @classmethod + @base.remotable_classmethod def get_by_name(cls, context, name): """Find a strategy based on name @@ -117,7 +104,7 @@ class Strategy(base.WatcherObject): strategy = cls._from_db_object(cls(context), db_strategy) return strategy - @classmethod + @base.remotable_classmethod def list(cls, context, limit=None, marker=None, filters=None, sort_key=None, sort_dir=None): """Return a list of :class:`Strategy` objects. @@ -142,8 +129,11 @@ class Strategy(base.WatcherObject): marker=marker, sort_key=sort_key, sort_dir=sort_dir) - return Strategy._from_db_object_list(db_strategies, cls, context) + return [cls._from_db_object(cls(context), obj) + for obj in db_strategies] + + @base.remotable def create(self, context=None): """Create a :class:`Strategy` record in the DB. @@ -172,6 +162,7 @@ class Strategy(base.WatcherObject): self.dbapi.destroy_strategy(self.id) self.obj_reset_changes() + @base.remotable def save(self, context=None): """Save updates to this :class:`Strategy`. @@ -190,6 +181,7 @@ class Strategy(base.WatcherObject): self.obj_reset_changes() + @base.remotable def refresh(self, context=None): """Loads updates for this :class:`Strategy`. @@ -210,6 +202,7 @@ class Strategy(base.WatcherObject): self[field] != current[field]): self[field] = current[field] + @base.remotable def soft_delete(self, context=None): """Soft Delete the :class:`Strategy` from the DB. diff --git a/watcher/tests/__init__.py b/watcher/tests/__init__.py index e69de29bb..cdc336c8a 100644 --- a/watcher/tests/__init__.py +++ b/watcher/tests/__init__.py @@ -0,0 +1,22 @@ +# Copyright 2010 United States Government as represented by the +# Administrator of the National Aeronautics and Space Administration. +# 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 watcher import objects + +# NOTE(comstud): Make sure we have all of the objects loaded. We do this +# at module import time, because we may be using mock decorators in our +# tests that run at import time. +objects.register_all() diff --git a/watcher/tests/api/base.py b/watcher/tests/api/base.py index 4e4698aa6..b5eb158ad 100644 --- a/watcher/tests/api/base.py +++ b/watcher/tests/api/base.py @@ -64,7 +64,7 @@ class FunctionalTest(base.DbTestCase): def _make_app(self, enable_acl=False): # Determine where we are so we can set up paths in the config - root_dir = self.path_get() + root_dir = self.get_path() self.config = { 'app': { diff --git a/watcher/tests/api/v1/test_audits.py b/watcher/tests/api/v1/test_audits.py index 65771a94a..e5cb056f0 100644 --- a/watcher/tests/api/v1/test_audits.py +++ b/watcher/tests/api/v1/test_audits.py @@ -250,7 +250,7 @@ class TestPatch(api_base.FunctionalTest): def setUp(self): super(TestPatch, self).setUp() obj_utils.create_test_audit_template(self.context) - self.audit = obj_utils.create_test_audit(self.context) + self.audit = obj_utils.create_test_audit(self.context, ) p = mock.patch.object(db_api.BaseConnection, 'update_audit') self.mock_audit_update = p.start() self.mock_audit_update.side_effect = self._simulate_rpc_audit_update @@ -313,15 +313,15 @@ class TestPatch(api_base.FunctionalTest): def test_remove_ok(self): response = self.get_json('/audits/%s' % self.audit.uuid) - self.assertIsNotNone(response['state']) + self.assertIsNotNone(response['interval']) response = self.patch_json('/audits/%s' % self.audit.uuid, - [{'path': '/state', 'op': 'remove'}]) + [{'path': '/interval', 'op': 'remove'}]) self.assertEqual('application/json', response.content_type) self.assertEqual(200, response.status_code) response = self.get_json('/audits/%s' % self.audit.uuid) - self.assertIsNone(response['state']) + self.assertIsNone(response['interval']) def test_remove_uuid(self): response = self.patch_json('/audits/%s' % self.audit.uuid, diff --git a/watcher/tests/applier/workflow_engine/test_default_workflow_engine.py b/watcher/tests/applier/workflow_engine/test_default_workflow_engine.py index b647bb20c..1983639d9 100644 --- a/watcher/tests/applier/workflow_engine/test_default_workflow_engine.py +++ b/watcher/tests/applier/workflow_engine/test_default_workflow_engine.py @@ -80,7 +80,7 @@ class TestDefaultWorkFlowEngine(base.DbTestCase): 'next': next, } new_action = objects.Action(self.context, **action) - new_action.create(self.context) + new_action.create() new_action.save() return new_action diff --git a/watcher/tests/base.py b/watcher/tests/base.py index b882c2687..6a23a6f3b 100644 --- a/watcher/tests/base.py +++ b/watcher/tests/base.py @@ -1,5 +1,3 @@ -# -*- coding: utf-8 -*- - # Copyright 2010-2011 OpenStack Foundation # Copyright (c) 2013 Hewlett-Packard Development Company, L.P. # @@ -35,8 +33,11 @@ from watcher.tests import policy_fixture CONF = cfg.CONF -log.register_options(CONF) -CONF.set_override('use_stderr', False, enforce_type=True) +try: + log.register_options(CONF) +except cfg.ArgsAlreadyParsedError: + pass +CONF.set_override('use_stderr', False) class BaseTestCase(testscenarios.WithScenarios, base.BaseTestCase): @@ -76,6 +77,9 @@ class TestCase(BaseTestCase): } } } + + objects_base.WatcherObject.indirection_api = None + self.context = watcher_context.RequestContext( auth_token_info=self.token_info, project_id='fake_project', @@ -104,19 +108,21 @@ class TestCase(BaseTestCase): self._reset_singletons() self._base_test_obj_backup = copy.copy( - objects_base.WatcherObject._obj_classes) + objects_base.WatcherObjectRegistry._registry._obj_classes) self.addCleanup(self._restore_obj_registry) self.addCleanup(self._reset_singletons) def _reset_singletons(self): service.Singleton._instances.clear() - def _restore_obj_registry(self): - objects_base.WatcherObject._obj_classes = self._base_test_obj_backup + def reset_pecan(): + pecan.set_config({}, overwrite=True) - def tearDown(self): - super(TestCase, self).tearDown() - pecan.set_config({}, overwrite=True) + self.addCleanup(reset_pecan) + + def _restore_obj_registry(self): + objects_base.WatcherObjectRegistry._registry._obj_classes = ( + self._base_test_obj_backup) def config(self, **kw): """Override config options for a test.""" @@ -124,7 +130,7 @@ class TestCase(BaseTestCase): for k, v in kw.items(): CONF.set_override(k, v, group, enforce_type=True) - def path_get(self, project_file=None): + def get_path(self, project_file=None): """Get the absolute path to a file. Used for testing the API. :param project_file: File whose path to return. Default: None. diff --git a/watcher/tests/db/test_purge.py b/watcher/tests/db/test_purge.py index dfa128198..6a86b2684 100644 --- a/watcher/tests/db/test_purge.py +++ b/watcher/tests/db/test_purge.py @@ -20,6 +20,7 @@ import freezegun import mock from watcher.common import context as watcher_context +from watcher.common import utils from watcher.db import purge from watcher.db.sqlalchemy import api as dbapi from watcher.tests.db import base @@ -101,27 +102,33 @@ class TestPurgeCommand(base.DbTestCase): with freezegun.freeze_time(self.expired_date): self.goal1 = obj_utils.create_test_goal( - self.context, id=self._generate_id(), uuid=None, + self.context, id=self._generate_id(), + uuid=utils.generate_uuid(), name=goal1_name, display_name=goal1_name.lower()) self.goal2 = obj_utils.create_test_goal( - self.context, id=self._generate_id(), uuid=None, + self.context, id=self._generate_id(), + uuid=utils.generate_uuid(), name=goal2_name, display_name=goal2_name.lower()) self.goal3 = obj_utils.create_test_goal( - self.context, id=self._generate_id(), uuid=None, + self.context, id=self._generate_id(), + uuid=utils.generate_uuid(), name=goal3_name, display_name=goal3_name.lower()) self.goal1.soft_delete() with freezegun.freeze_time(self.expired_date): self.strategy1 = obj_utils.create_test_strategy( - self.context, id=self._generate_id(), uuid=None, + self.context, id=self._generate_id(), + uuid=utils.generate_uuid(), name=strategy1_name, display_name=strategy1_name.lower(), goal_id=self.goal1.id) self.strategy2 = obj_utils.create_test_strategy( - self.context, id=self._generate_id(), uuid=None, + self.context, id=self._generate_id(), + uuid=utils.generate_uuid(), name=strategy2_name, display_name=strategy2_name.lower(), goal_id=self.goal2.id) self.strategy3 = obj_utils.create_test_strategy( - self.context, id=self._generate_id(), uuid=None, + self.context, id=self._generate_id(), + uuid=utils.generate_uuid(), name=strategy3_name, display_name=strategy3_name.lower(), goal_id=self.goal3.id) self.strategy1.soft_delete() @@ -129,50 +136,61 @@ class TestPurgeCommand(base.DbTestCase): with freezegun.freeze_time(self.expired_date): self.audit_template1 = obj_utils.create_test_audit_template( self.context, name=self.audit_template1_name, - id=self._generate_id(), uuid=None, goal_id=self.goal1.id, + id=self._generate_id(), + uuid=utils.generate_uuid(), goal_id=self.goal1.id, strategy_id=self.strategy1.id) self.audit_template2 = obj_utils.create_test_audit_template( self.context, name=self.audit_template2_name, - id=self._generate_id(), uuid=None, goal_id=self.goal2.id, + id=self._generate_id(), + uuid=utils.generate_uuid(), goal_id=self.goal2.id, strategy_id=self.strategy2.id) self.audit_template3 = obj_utils.create_test_audit_template( self.context, name=self.audit_template3_name, - id=self._generate_id(), uuid=None, goal_id=self.goal3.id, + id=self._generate_id(), + uuid=utils.generate_uuid(), goal_id=self.goal3.id, strategy_id=self.strategy3.id) self.audit_template1.soft_delete() with freezegun.freeze_time(self.expired_date): self.audit1 = obj_utils.create_test_audit( - self.context, id=self._generate_id(), uuid=None, + self.context, id=self._generate_id(), + uuid=utils.generate_uuid(), goal_id=self.goal1.id, strategy_id=self.strategy1.id) self.audit2 = obj_utils.create_test_audit( - self.context, id=self._generate_id(), uuid=None, + self.context, id=self._generate_id(), + uuid=utils.generate_uuid(), goal_id=self.goal2.id, strategy_id=self.strategy2.id) self.audit3 = obj_utils.create_test_audit( - self.context, id=self._generate_id(), uuid=None, + self.context, id=self._generate_id(), + uuid=utils.generate_uuid(), goal_id=self.goal3.id, strategy_id=self.strategy3.id) self.audit1.soft_delete() with freezegun.freeze_time(self.expired_date): self.action_plan1 = obj_utils.create_test_action_plan( - self.context, id=self._generate_id(), uuid=None, - audit_id=self.audit1.id, strategy_id=self.strategy1.id) + self.context, audit_id=self.audit1.id, + id=self._generate_id(), uuid=utils.generate_uuid(), + strategy_id=self.strategy1.id) self.action_plan2 = obj_utils.create_test_action_plan( - self.context, id=self._generate_id(), uuid=None, - audit_id=self.audit2.id, strategy_id=self.strategy2.id) + self.context, audit_id=self.audit2.id, + id=self._generate_id(), + strategy_id=self.strategy2.id, + uuid=utils.generate_uuid()) self.action_plan3 = obj_utils.create_test_action_plan( - self.context, id=self._generate_id(), uuid=None, - audit_id=self.audit3.id, strategy_id=self.strategy3.id) + self.context, audit_id=self.audit3.id, + id=self._generate_id(), uuid=utils.generate_uuid(), + strategy_id=self.strategy3.id) self.action1 = obj_utils.create_test_action( self.context, action_plan_id=self.action_plan1.id, - id=self._generate_id(), uuid=None) + id=self._generate_id(), + uuid=utils.generate_uuid()) self.action2 = obj_utils.create_test_action( self.context, action_plan_id=self.action_plan2.id, - id=self._generate_id(), uuid=None) + id=self._generate_id(), uuid=utils.generate_uuid()) self.action3 = obj_utils.create_test_action( self.context, action_plan_id=self.action_plan3.id, - id=self._generate_id(), uuid=None) + id=self._generate_id(), uuid=utils.generate_uuid()) self.action_plan1.soft_delete() @mock.patch.object(dbapi.Connection, "destroy_action") @@ -249,30 +267,38 @@ class TestPurgeCommand(base.DbTestCase): audit_template4 = obj_utils.create_test_audit_template( self.context, goal_id=404, # Does not exist name=self.generate_unique_name(prefix="Audit Template 4 "), - strategy_id=None, id=self._generate_id(), uuid=None) + strategy_id=None, id=self._generate_id(), + uuid=utils.generate_uuid()) audit4 = obj_utils.create_test_audit( self.context, audit_template_id=audit_template4.id, - id=self._generate_id(), uuid=None) + id=self._generate_id(), + uuid=utils.generate_uuid()) action_plan4 = obj_utils.create_test_action_plan( self.context, audit_id=audit4.id, - id=self._generate_id(), uuid=None) + id=self._generate_id(), + uuid=utils.generate_uuid()) action4 = obj_utils.create_test_action( self.context, action_plan_id=action_plan4.id, - id=self._generate_id(), uuid=None) + id=self._generate_id(), + uuid=utils.generate_uuid()) audit_template5 = obj_utils.create_test_audit_template( self.context, goal_id=self.goal1.id, name=self.generate_unique_name(prefix="Audit Template 5 "), - strategy_id=None, id=self._generate_id(), uuid=None) + strategy_id=None, id=self._generate_id(), + uuid=utils.generate_uuid()) audit5 = obj_utils.create_test_audit( self.context, audit_template_id=audit_template5.id, - id=self._generate_id(), uuid=None) + id=self._generate_id(), + uuid=utils.generate_uuid()) action_plan5 = obj_utils.create_test_action_plan( self.context, audit_id=audit5.id, - id=self._generate_id(), uuid=None) + id=self._generate_id(), + uuid=utils.generate_uuid()) action5 = obj_utils.create_test_action( self.context, action_plan_id=action_plan5.id, - id=self._generate_id(), uuid=None) + id=self._generate_id(), + uuid=utils.generate_uuid()) self.goal2.soft_delete() self.strategy2.soft_delete() @@ -338,30 +364,38 @@ class TestPurgeCommand(base.DbTestCase): audit_template4 = obj_utils.create_test_audit_template( self.context, goal_id=404, # Does not exist name=self.generate_unique_name(prefix="Audit Template 4 "), - strategy_id=None, id=self._generate_id(), uuid=None) + strategy_id=None, id=self._generate_id(), + uuid=utils.generate_uuid()) audit4 = obj_utils.create_test_audit( self.context, audit_template_id=audit_template4.id, - id=self._generate_id(), uuid=None) + id=self._generate_id(), + uuid=utils.generate_uuid()) action_plan4 = obj_utils.create_test_action_plan( self.context, audit_id=audit4.id, - id=self._generate_id(), uuid=None) + id=self._generate_id(), + uuid=utils.generate_uuid()) action4 = obj_utils.create_test_action( self.context, action_plan_id=action_plan4.id, - id=self._generate_id(), uuid=None) + id=self._generate_id(), + uuid=utils.generate_uuid()) audit_template5 = obj_utils.create_test_audit_template( self.context, goal_id=self.goal1.id, name=self.generate_unique_name(prefix="Audit Template 5 "), - strategy_id=None, id=self._generate_id(), uuid=None) + strategy_id=None, id=self._generate_id(), + uuid=utils.generate_uuid()) audit5 = obj_utils.create_test_audit( self.context, audit_template_id=audit_template5.id, - id=self._generate_id(), uuid=None) + id=self._generate_id(), + uuid=utils.generate_uuid()) action_plan5 = obj_utils.create_test_action_plan( self.context, audit_id=audit5.id, - id=self._generate_id(), uuid=None) + id=self._generate_id(), + uuid=utils.generate_uuid()) action5 = obj_utils.create_test_action( self.context, action_plan_id=action_plan5.id, - id=self._generate_id(), uuid=None) + id=self._generate_id(), + uuid=utils.generate_uuid()) self.goal2.soft_delete() self.strategy2.soft_delete() diff --git a/watcher/tests/db/utils.py b/watcher/tests/db/utils.py index 1d106b312..ca5ef2dd9 100644 --- a/watcher/tests/db/utils.py +++ b/watcher/tests/db/utils.py @@ -56,7 +56,7 @@ def get_test_audit(**kwargs): 'id': kwargs.get('id', 1), 'uuid': kwargs.get('uuid', '10a47dd1-4874-4298-91cf-eff046dbdb8d'), 'audit_type': kwargs.get('audit_type', 'ONESHOT'), - 'state': kwargs.get('state'), + 'state': kwargs.get('state', objects.audit.State.PENDING), 'created_at': kwargs.get('created_at'), 'updated_at': kwargs.get('updated_at'), 'deleted_at': kwargs.get('deleted_at'), diff --git a/watcher/tests/decision_engine/test_sync.py b/watcher/tests/decision_engine/test_sync.py index 704fd7205..5771f5e8d 100644 --- a/watcher/tests/decision_engine/test_sync.py +++ b/watcher/tests/decision_engine/test_sync.py @@ -21,7 +21,6 @@ from watcher.common import utils from watcher.decision_engine.loading import default from watcher.decision_engine import sync from watcher import objects -from watcher.objects import action_plan as ap_objects from watcher.tests.db import base from watcher.tests.decision_engine import fake_goals from watcher.tests.decision_engine import fake_strategies @@ -329,22 +328,30 @@ class TestSyncer(base.DbTestCase): # Should stay unmodified after sync() audit1 = objects.Audit( self.ctx, id=1, uuid=utils.generate_uuid(), + audit_type=objects.audit.AuditType.ONESHOT.value, + state=objects.audit.State.PENDING, goal_id=goal1.id, strategy_id=strategy1.id) # Should be modified by the sync() because its associated goal # has been modified (compared to the defined fake goals) audit2 = objects.Audit( self.ctx, id=2, uuid=utils.generate_uuid(), + audit_type=objects.audit.AuditType.ONESHOT.value, + state=objects.audit.State.PENDING, goal_id=goal2.id, strategy_id=strategy2.id) # Should be modified by the sync() because its associated strategy # has been modified (compared to the defined fake strategies) audit3 = objects.Audit( self.ctx, id=3, uuid=utils.generate_uuid(), + audit_type=objects.audit.AuditType.ONESHOT.value, + state=objects.audit.State.PENDING, goal_id=goal1.id, strategy_id=strategy3.id) # Modified because of both because its associated goal and associated # strategy should be modified (compared to the defined fake # goals/strategies) audit4 = objects.Audit( self.ctx, id=4, uuid=utils.generate_uuid(), + audit_type=objects.audit.AuditType.ONESHOT.value, + state=objects.audit.State.PENDING, goal_id=goal2.id, strategy_id=strategy4.id) audit1.create() @@ -483,7 +490,7 @@ class TestSyncer(base.DbTestCase): set([action_plan2.id, action_plan3.id, action_plan4.id]), set(modified_action_plans)) self.assertTrue( - all(ap.state == ap_objects.State.CANCELLED + all(ap.state == objects.action_plan.State.CANCELLED for ap in modified_action_plans.values())) self.assertEqual(set([action_plan1.id]), set(unmodified_action_plans)) @@ -551,10 +558,14 @@ class TestSyncer(base.DbTestCase): # Should stay unmodified after sync() audit1 = objects.Audit( self.ctx, id=1, uuid=utils.generate_uuid(), + audit_type=objects.audit.AuditType.ONESHOT.value, + state=objects.audit.State.PENDING, goal_id=goal1.id, strategy_id=strategy1.id) # Stale after syncing because the goal has been soft deleted audit2 = objects.Audit( self.ctx, id=2, uuid=utils.generate_uuid(), + audit_type=objects.audit.AuditType.ONESHOT.value, + state=objects.audit.State.PENDING, goal_id=goal2.id, strategy_id=strategy2.id) audit1.create() audit2.create() @@ -651,6 +662,6 @@ class TestSyncer(base.DbTestCase): self.assertEqual(set([action_plan2.id]), set(modified_action_plans)) self.assertTrue( - all(ap.state == ap_objects.State.CANCELLED + all(ap.state == objects.action_plan.State.CANCELLED for ap in modified_action_plans.values())) self.assertEqual(set([action_plan1.id]), set(unmodified_action_plans)) diff --git a/watcher/tests/objects/test_objects.py b/watcher/tests/objects/test_objects.py index 4e7be9e76..165b71bdc 100644 --- a/watcher/tests/objects/test_objects.py +++ b/watcher/tests/objects/test_objects.py @@ -1,4 +1,4 @@ -# Copyright 2015 IBM Corp. +# Copyright 2013 IBM Corp. # # 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 @@ -12,32 +12,38 @@ # License for the specific language governing permissions and limitations # under the License. +import contextlib import datetime import gettext - import iso8601 -import netaddr -from oslo_utils import timeutils + +import mock +from oslo_versionedobjects import base as object_base +from oslo_versionedobjects import exception as object_exception +from oslo_versionedobjects import fixture as object_fixture import six +from watcher.common import context from watcher.objects import base -from watcher.objects import utils +from watcher.objects import fields from watcher.tests import base as test_base gettext.install('watcher') -class MyObj(base.WatcherObject): - VERSION = '1.0' +@base.WatcherObjectRegistry.register +class MyObj(base.WatcherPersistentObject, base.WatcherObject, + base.WatcherObjectDictCompat): + VERSION = '1.5' - fields = {'foo': int, - 'bar': str, - 'missing': str, - } + fields = {'foo': fields.IntegerField(), + 'bar': fields.StringField(), + 'missing': fields.StringField()} def obj_load_attr(self, attrname): setattr(self, attrname, 'loaded!') + @object_base.remotable_classmethod def query(cls, context): obj = cls(context) obj.foo = 1 @@ -45,24 +51,29 @@ class MyObj(base.WatcherObject): obj.obj_reset_changes() return obj - def marco(self, context): + @object_base.remotable + def marco(self, context=None): return 'polo' - def update_test(self, context): - if context.project_id == 'alternate': + @object_base.remotable + def update_test(self, context=None): + if context and context.user == 'alternate': self.bar = 'alternate-context' else: self.bar = 'updated' - def save(self, context): + @object_base.remotable + def save(self, context=None): self.obj_reset_changes() - def refresh(self, context): + @object_base.remotable + def refresh(self, context=None): self.foo = 321 self.bar = 'refreshed' self.obj_reset_changes() - def modify_save_modify(self, context): + @object_base.remotable + def modify_save_modify(self, context=None): self.bar = 'meow' self.save() self.foo = 42 @@ -73,232 +84,361 @@ class MyObj2(object): def obj_name(cls): return 'MyObj' + @object_base.remotable_classmethod def get(cls, *args, **kwargs): pass -class DummySubclassedObject(MyObj): - fields = {'new_field': str} +@base.WatcherObjectRegistry.register_if(False) +class WatcherTestSubclassedObject(MyObj): + fields = {'new_field': fields.StringField()} -class TestMetaclass(test_base.TestCase): - def test_obj_tracking(self): - - @six.add_metaclass(base.WatcherObjectMetaclass) - class NewBaseClass(object): - fields = {} - - @classmethod - def obj_name(cls): - return cls.__name__ - - class Test1(NewBaseClass): - @staticmethod - def obj_name(): - return 'fake1' - - class Test2(NewBaseClass): - pass - - class Test2v2(NewBaseClass): - @staticmethod - def obj_name(): - return 'Test2' - - expected = {'fake1': [Test1], 'Test2': [Test2, Test2v2]} - - self.assertEqual(expected, NewBaseClass._obj_classes) - # The following should work, also. - self.assertEqual(expected, Test1._obj_classes) - self.assertEqual(expected, Test2._obj_classes) +class _LocalTest(test_base.TestCase): + def setUp(self): + super(_LocalTest, self).setUp() + # Just in case + base.WatcherObject.indirection_api = None -class TestUtils(test_base.TestCase): - - def test_datetime_or_none(self): - naive_dt = datetime.datetime.now() - dt = timeutils.parse_isotime(timeutils.isotime(naive_dt)) - self.assertEqual(dt, utils.datetime_or_none(dt, tzinfo_aware=True)) - self.assertEqual(naive_dt.replace(tzinfo=iso8601.iso8601.Utc(), - microsecond=0), - utils.datetime_or_none(dt, tzinfo_aware=True)) - self.assertIsNone(utils.datetime_or_none(None)) - self.assertRaises(ValueError, utils.datetime_or_none, 'foo') - - def test_datetime_or_none_tzinfo_naive(self): - naive_dt = datetime.datetime.utcnow() - self.assertEqual(naive_dt, utils.datetime_or_none(naive_dt, - tzinfo_aware=False)) - self.assertIsNone(utils.datetime_or_none(None)) - self.assertRaises(ValueError, utils.datetime_or_none, 'foo') - - def test_datetime_or_str_or_none(self): - dts = timeutils.isotime() - dt = timeutils.parse_isotime(dts) - self.assertEqual(dt, utils.datetime_or_str_or_none(dt, - tzinfo_aware=True)) - self.assertIsNone(utils.datetime_or_str_or_none(None, - tzinfo_aware=True)) - self.assertEqual(dt, utils.datetime_or_str_or_none(dts, - tzinfo_aware=True)) - self.assertRaises(ValueError, utils.datetime_or_str_or_none, 'foo') - - def test_int_or_none(self): - self.assertEqual(1, utils.int_or_none(1)) - self.assertEqual(1, utils.int_or_none('1')) - self.assertIsNone(utils.int_or_none(None)) - self.assertRaises(ValueError, utils.int_or_none, 'foo') - - def test_str_or_none(self): - class Obj(object): - pass - self.assertEqual('foo', utils.str_or_none('foo')) - self.assertEqual('1', utils.str_or_none(1)) - self.assertIsNone(utils.str_or_none(None)) - - def test_ip_or_none(self): - ip4 = netaddr.IPAddress('1.2.3.4', 4) - ip6 = netaddr.IPAddress('1::2', 6) - self.assertEqual(ip4, utils.ip_or_none(4)('1.2.3.4')) - self.assertEqual(ip6, utils.ip_or_none(6)('1::2')) - self.assertIsNone(utils.ip_or_none(4)(None)) - self.assertIsNone(utils.ip_or_none(6)(None)) - self.assertRaises(netaddr.AddrFormatError, utils.ip_or_none(4), 'foo') - self.assertRaises(netaddr.AddrFormatError, utils.ip_or_none(6), 'foo') - - def test_dt_serializer(self): - class Obj(object): - foo = utils.dt_serializer('bar') - - obj = Obj() - obj.bar = timeutils.parse_isotime('1955-11-05T00:00:00Z') - self.assertEqual('1955-11-05T00:00:00Z', obj.foo()) - obj.bar = None - self.assertIsNone(obj.foo()) - obj.bar = 'foo' - self.assertRaises(AttributeError, obj.foo) - - def test_dt_deserializer(self): - dt = timeutils.parse_isotime('1955-11-05T00:00:00Z') - self.assertEqual(dt, utils.dt_deserializer(timeutils.isotime(dt))) - self.assertIsNone(utils.dt_deserializer(None)) - self.assertRaises(ValueError, utils.dt_deserializer, 'foo') - - def test_obj_to_primitive_list(self): - class MyList(base.ObjectListBase, base.WatcherObject): - pass - mylist = MyList(self.context) - mylist.objects = [1, 2, 3] - self.assertEqual([1, 2, 3], base.obj_to_primitive(mylist)) - - def test_obj_to_primitive_dict(self): - myobj = MyObj(self.context) - myobj.foo = 1 - myobj.bar = 'foo' - self.assertEqual({'foo': 1, 'bar': 'foo'}, - base.obj_to_primitive(myobj)) - - def test_obj_to_primitive_recursive(self): - class MyList(base.ObjectListBase, base.WatcherObject): - pass - - mylist = MyList(self.context) - mylist.objects = [MyObj(self.context), MyObj(self.context)] - for i, value in enumerate(mylist): - value.foo = i - self.assertEqual([{'foo': 0}, {'foo': 1}], - base.obj_to_primitive(mylist)) +@contextlib.contextmanager +def things_temporarily_local(): + # Temporarily go non-remote so the conductor handles + # this request directly + _api = base.WatcherObject.indirection_api + base.WatcherObject.indirection_api = None + yield + base.WatcherObject.indirection_api = _api -class TestObjectListBase(test_base.TestCase): +class _TestObject(object): + def test_hydration_type_error(self): + primitive = {'watcher_object.name': 'MyObj', + 'watcher_object.namespace': 'watcher', + 'watcher_object.version': '1.5', + 'watcher_object.data': {'foo': 'a'}} + self.assertRaises(ValueError, MyObj.obj_from_primitive, primitive) - def test_list_like_operations(self): - class Foo(base.ObjectListBase, base.WatcherObject): - pass + def test_hydration(self): + primitive = {'watcher_object.name': 'MyObj', + 'watcher_object.namespace': 'watcher', + 'watcher_object.version': '1.5', + 'watcher_object.data': {'foo': 1}} + obj = MyObj.obj_from_primitive(primitive) + self.assertEqual(1, obj.foo) - objlist = Foo(self.context) - objlist._context = 'foo' - objlist.objects = [1, 2, 3] - self.assertEqual(list(objlist), objlist.objects) - self.assertEqual(3, len(objlist)) - self.assertIn(2, objlist) - self.assertEqual([1], list(objlist[:1])) - self.assertEqual('foo', objlist[:1]._context) - self.assertEqual(3, objlist[2]) - self.assertEqual(1, objlist.count(1)) - self.assertEqual(1, objlist.index(2)) + def test_hydration_bad_ns(self): + primitive = {'watcher_object.name': 'MyObj', + 'watcher_object.namespace': 'foo', + 'watcher_object.version': '1.5', + 'watcher_object.data': {'foo': 1}} + self.assertRaises(object_exception.UnsupportedObjectError, + MyObj.obj_from_primitive, primitive) - def test_serialization(self): - class Foo(base.ObjectListBase, base.WatcherObject): - pass - - class Bar(base.WatcherObject): - fields = {'foo': str} - - obj = Foo(self.context) - obj.objects = [] - for i in 'abc': - bar = Bar(self.context) - bar.foo = i - obj.objects.append(bar) - - obj2 = base.WatcherObject.obj_from_primitive(obj.obj_to_primitive()) - self.assertFalse(obj is obj2) - self.assertEqual([x.foo for x in obj], - [y.foo for y in obj2]) - - def _test_object_list_version_mappings(self, list_obj_class): - # Figure out what sort of object this list is for - list_field = list_obj_class.fields['objects'] - item_obj_field = list_field._type._element_type - item_obj_name = item_obj_field._type._obj_name - - # Look through all object classes of this type and make sure that - # the versions we find are covered by the parent list class - for item_class in base.WatcherObject._obj_classes[item_obj_name]: - self.assertIn( - item_class.VERSION, - list_obj_class.child_versions.values()) - - def test_object_version_mappings(self): - # Find all object list classes and make sure that they at least handle - # all the current object versions - for obj_classes in base.WatcherObject._obj_classes.values(): - for obj_class in obj_classes: - if issubclass(obj_class, base.ObjectListBase): - self._test_object_list_version_mappings(obj_class) - - def test_list_changes(self): - class Foo(base.ObjectListBase, base.WatcherObject): - pass - - class Bar(base.WatcherObject): - fields = {'foo': str} - - obj = Foo(self.context, objects=[]) - self.assertEqual(set(['objects']), obj.obj_what_changed()) - obj.objects.append(Bar(self.context, foo='test')) - self.assertEqual(set(['objects']), obj.obj_what_changed()) + def test_dehydration(self): + expected = {'watcher_object.name': 'MyObj', + 'watcher_object.namespace': 'watcher', + 'watcher_object.version': '1.5', + 'watcher_object.data': {'foo': 1}} + obj = MyObj(self.context) + obj.foo = 1 obj.obj_reset_changes() - # This should still look dirty because the child is dirty - self.assertEqual(set(['objects']), obj.obj_what_changed()) - obj.objects[0].obj_reset_changes() - # This should now look clean because the child is clean - self.assertEqual(set(), obj.obj_what_changed()) + self.assertEqual(expected, obj.obj_to_primitive()) + + def test_get_updates(self): + obj = MyObj(self.context) + self.assertEqual({}, obj.obj_get_changes()) + obj.foo = 123 + self.assertEqual({'foo': 123}, obj.obj_get_changes()) + obj.bar = 'test' + self.assertEqual({'foo': 123, 'bar': 'test'}, obj.obj_get_changes()) + obj.obj_reset_changes() + self.assertEqual({}, obj.obj_get_changes()) + + def test_object_property(self): + obj = MyObj(self.context, foo=1) + self.assertEqual(1, obj.foo) + + def test_object_property_type_error(self): + obj = MyObj(self.context) + + def fail(): + obj.foo = 'a' + self.assertRaises(ValueError, fail) + + def test_load(self): + obj = MyObj(self.context) + self.assertEqual('loaded!', obj.bar) + + def test_load_in_base(self): + @base.WatcherObjectRegistry.register_if(False) + class Foo(base.WatcherPersistentObject, base.WatcherObject, + base.WatcherObjectDictCompat): + fields = {'foobar': fields.IntegerField()} + obj = Foo(self.context) + + self.assertRaisesRegex( + NotImplementedError, "Cannot load 'foobar' in the base class", + getattr, obj, 'foobar') + + def test_loaded_in_primitive(self): + obj = MyObj(self.context) + obj.foo = 1 + obj.obj_reset_changes() + self.assertEqual('loaded!', obj.bar) + expected = {'watcher_object.name': 'MyObj', + 'watcher_object.namespace': 'watcher', + 'watcher_object.version': '1.5', + 'watcher_object.changes': ['bar'], + 'watcher_object.data': {'foo': 1, + 'bar': 'loaded!'}} + self.assertEqual(expected, obj.obj_to_primitive()) + + def test_changes_in_primitive(self): + obj = MyObj(self.context) + obj.foo = 123 + self.assertEqual(set(['foo']), obj.obj_what_changed()) + primitive = obj.obj_to_primitive() + self.assertIn('watcher_object.changes', primitive) + obj2 = MyObj.obj_from_primitive(primitive) + self.assertEqual(set(['foo']), obj2.obj_what_changed()) + obj2.obj_reset_changes() + self.assertEqual(set(), obj2.obj_what_changed()) + + def test_unknown_objtype(self): + self.assertRaises(object_exception.UnsupportedObjectError, + base.WatcherObject.obj_class_from_name, 'foo', '1.0') + + def test_with_alternate_context(self): + ctxt1 = context.RequestContext('foo', 'foo') + ctxt2 = context.RequestContext(user='alternate') + obj = MyObj.query(ctxt1) + obj.update_test(ctxt2) + self.assertEqual('alternate-context', obj.bar) + + def test_orphaned_object(self): + obj = MyObj.query(self.context) + obj._context = None + self.assertRaises(object_exception.OrphanedObjectError, + obj.update_test) + + def test_changed_1(self): + obj = MyObj.query(self.context) + obj.foo = 123 + self.assertEqual(set(['foo']), obj.obj_what_changed()) + obj.update_test(self.context) + self.assertEqual(set(['foo', 'bar']), obj.obj_what_changed()) + self.assertEqual(123, obj.foo) + + def test_changed_2(self): + obj = MyObj.query(self.context) + obj.foo = 123 + self.assertEqual(set(['foo']), obj.obj_what_changed()) + obj.save() + self.assertEqual(set([]), obj.obj_what_changed()) + self.assertEqual(123, obj.foo) + + def test_changed_3(self): + obj = MyObj.query(self.context) + obj.foo = 123 + self.assertEqual(set(['foo']), obj.obj_what_changed()) + obj.refresh() + self.assertEqual(set([]), obj.obj_what_changed()) + self.assertEqual(321, obj.foo) + self.assertEqual('refreshed', obj.bar) + + def test_changed_4(self): + obj = MyObj.query(self.context) + obj.bar = 'something' + self.assertEqual(set(['bar']), obj.obj_what_changed()) + obj.modify_save_modify(self.context) + self.assertEqual(set(['foo']), obj.obj_what_changed()) + self.assertEqual(42, obj.foo) + self.assertEqual('meow', obj.bar) + + def test_static_result(self): + obj = MyObj.query(self.context) + self.assertEqual('bar', obj.bar) + result = obj.marco() + self.assertEqual('polo', result) + + def test_updates(self): + obj = MyObj.query(self.context) + self.assertEqual(1, obj.foo) + obj.update_test() + self.assertEqual('updated', obj.bar) + + def test_base_attributes(self): + dt = datetime.datetime(1955, 11, 5, 0, 0, tzinfo=iso8601.iso8601.Utc()) + datatime = fields.DateTimeField() + obj = MyObj(self.context) + obj.created_at = dt + obj.updated_at = dt + expected = {'watcher_object.name': 'MyObj', + 'watcher_object.namespace': 'watcher', + 'watcher_object.version': '1.5', + 'watcher_object.changes': + ['created_at', 'updated_at'], + 'watcher_object.data': + {'created_at': datatime.stringify(dt), + 'updated_at': datatime.stringify(dt), + } + } + actual = obj.obj_to_primitive() + # watcher_object.changes is built from a set and order is undefined + self.assertEqual(sorted(expected['watcher_object.changes']), + sorted(actual['watcher_object.changes'])) + del expected[ + 'watcher_object.changes'], actual['watcher_object.changes'] + self.assertEqual(expected, actual) + + def test_contains(self): + obj = MyObj(self.context) + self.assertNotIn('foo', obj) + obj.foo = 1 + self.assertIn('foo', obj) + self.assertNotIn('does_not_exist', obj) + + def test_obj_attr_is_set(self): + obj = MyObj(self.context, foo=1) + self.assertTrue(obj.obj_attr_is_set('foo')) + self.assertFalse(obj.obj_attr_is_set('bar')) + self.assertRaises(AttributeError, obj.obj_attr_is_set, 'bang') + + def test_get(self): + obj = MyObj(self.context, foo=1) + # Foo has value, should not get the default + self.assertEqual(obj.get('foo', 2), 1) + # Foo has value, should return the value without error + self.assertEqual(obj.get('foo'), 1) + # Bar is not loaded, so we should get the default + self.assertEqual(obj.get('bar', 'not-loaded'), 'not-loaded') + # Bar without a default should lazy-load + self.assertEqual(obj.get('bar'), 'loaded!') + # Bar now has a default, but loaded value should be returned + self.assertEqual(obj.get('bar', 'not-loaded'), 'loaded!') + # Invalid attribute should raise AttributeError + self.assertRaises(AttributeError, obj.get, 'nothing') + # ...even with a default + self.assertRaises(AttributeError, obj.get, 'nothing', 3) + + def test_object_inheritance(self): + base_fields = ( + list(base.WatcherObject.fields) + + list(base.WatcherPersistentObject.fields)) + myobj_fields = ['foo', 'bar', 'missing'] + base_fields + myobj3_fields = ['new_field'] + self.assertTrue(issubclass(WatcherTestSubclassedObject, MyObj)) + self.assertEqual(len(myobj_fields), len(MyObj.fields)) + self.assertEqual(set(myobj_fields), set(MyObj.fields.keys())) + self.assertEqual(len(myobj_fields) + len(myobj3_fields), + len(WatcherTestSubclassedObject.fields)) + self.assertEqual(set(myobj_fields) | set(myobj3_fields), + set(WatcherTestSubclassedObject.fields.keys())) + + def test_get_changes(self): + obj = MyObj(self.context) + self.assertEqual({}, obj.obj_get_changes()) + obj.foo = 123 + self.assertEqual({'foo': 123}, obj.obj_get_changes()) + obj.bar = 'test' + self.assertEqual({'foo': 123, 'bar': 'test'}, obj.obj_get_changes()) + obj.obj_reset_changes() + self.assertEqual({}, obj.obj_get_changes()) + + def test_obj_fields(self): + @base.WatcherObjectRegistry.register_if(False) + class TestObj(base.WatcherPersistentObject, base.WatcherObject, + base.WatcherObjectDictCompat): + fields = {'foo': fields.IntegerField()} + obj_extra_fields = ['bar'] + + @property + def bar(self): + return 'this is bar' + + obj = TestObj(self.context) + self.assertEqual(set(['created_at', 'updated_at', 'deleted_at', + 'foo', 'bar']), + set(obj.obj_fields)) + + def test_refresh_object(self): + @base.WatcherObjectRegistry.register_if(False) + class TestObj(base.WatcherPersistentObject, base.WatcherObject, + base.WatcherObjectDictCompat): + fields = {'foo': fields.IntegerField(), + 'bar': fields.StringField()} + + obj = TestObj(self.context) + current_obj = TestObj(self.context) + obj.foo = 10 + obj.bar = 'obj.bar' + current_obj.foo = 2 + current_obj.bar = 'current.bar' + obj.obj_refresh(current_obj) + self.assertEqual(obj.foo, 2) + self.assertEqual(obj.bar, 'current.bar') + + def test_obj_constructor(self): + obj = MyObj(self.context, foo=123, bar='abc') + self.assertEqual(123, obj.foo) + self.assertEqual('abc', obj.bar) + self.assertEqual(set(['foo', 'bar']), obj.obj_what_changed()) + + def test_assign_value_without_DictCompat(self): + class TestObj(base.WatcherObject): + fields = {'foo': fields.IntegerField(), + 'bar': fields.StringField()} + obj = TestObj(self.context) + obj.foo = 10 + err_message = '' + try: + obj['bar'] = 'value' + except TypeError as e: + err_message = six.text_type(e) + finally: + self.assertIn("'TestObj' object does not support item assignment", + err_message) + + +class TestObject(_LocalTest, _TestObject): + pass + + +# The hashes are help developers to check if the change of objects need a +# version bump. It is md5 hash of object fields and remotable methods. +# The fingerprint values should only be changed if there is a version bump. +expected_object_fingerprints = { + 'Goal': '1.0-93881622db05e7b67a65ca885b4a022e', + 'Strategy': '1.0-e60f62cc854c6e63fb1c3befbfc8629e', + 'AuditTemplate': '1.0-7432ee4d3ce0c7cbb9d11a4565ee8eb6', + 'Audit': '1.0-ebfc5360d019baf583a10a8a27071c97', + 'ActionPlan': '1.0-cc76fd7f0e8479aeff817dd266341de4', + 'Action': '1.0-a78f69c0da98e13e601f9646f6b2f883', + 'EfficacyIndicator': '1.0-655b71234a82bc7478aff964639c4bb0', + 'ScoringEngine': '1.0-4abbe833544000728e17bd9e83f97576', + 'Service': '1.0-4b35b99ada9677a882c9de2b30212f35', + 'MyObj': '1.5-23c516d1e842f365f694e688d34e47c3', +} + + +class TestObjectVersions(test_base.TestCase): + + def test_object_version_check(self): + classes = base.WatcherObjectRegistry.obj_classes() + checker = object_fixture.ObjectVersionChecker(obj_classes=classes) + # Compute the difference between actual fingerprints and + # expect fingerprints. expect = actual = {} if there is no change. + expect, actual = checker.test_hashes(expected_object_fingerprints) + self.assertEqual(expect, actual, + "Some objects fields or remotable methods have been " + "modified. Please make sure the version of those " + "objects have been bumped and then update " + "expected_object_fingerprints with the new hashes. ") class TestObjectSerializer(test_base.TestCase): - def test_serialize_entity_primitive(self): - ser = base.WatcherObjectSerializer() - for thing in (1, 'foo', [1, 2], {'foo': 'bar'}): - self.assertEqual(thing, ser.serialize_entity(None, thing)) - - def test_deserialize_entity_primitive(self): - ser = base.WatcherObjectSerializer() - for thing in (1, 'foo', [1, 2], {'foo': 'bar'}): - self.assertEqual(thing, ser.deserialize_entity(None, thing)) - def test_object_serialization(self): ser = base.WatcherObjectSerializer() obj = MyObj(self.context) @@ -321,3 +461,83 @@ class TestObjectSerializer(test_base.TestCase): self.assertEqual(1, len(thing2)) for item in thing2: self.assertIsInstance(item, MyObj) + + @mock.patch('watcher.objects.base.WatcherObject.indirection_api') + def _test_deserialize_entity_newer(self, obj_version, backported_to, + mock_indirection_api, + my_version='1.6'): + ser = base.WatcherObjectSerializer() + mock_indirection_api.object_backport_versions.return_value \ + = 'backported' + + @base.WatcherObjectRegistry.register + class MyTestObj(MyObj): + VERSION = my_version + + obj = MyTestObj(self.context) + obj.VERSION = obj_version + primitive = obj.obj_to_primitive() + result = ser.deserialize_entity(self.context, primitive) + if backported_to is None: + self.assertFalse( + mock_indirection_api.object_backport_versions.called) + else: + self.assertEqual('backported', result) + versions = object_base.obj_tree_get_versions('MyTestObj') + mock_indirection_api.object_backport_versions.assert_called_with( + self.context, primitive, versions) + + def test_deserialize_entity_newer_version_backports(self): + "Test object with unsupported (newer) version" + self._test_deserialize_entity_newer('1.25', '1.6') + + def test_deserialize_entity_same_revision_does_not_backport(self): + "Test object with supported revision" + self._test_deserialize_entity_newer('1.6', None) + + def test_deserialize_entity_newer_revision_does_not_backport_zero(self): + "Test object with supported revision" + self._test_deserialize_entity_newer('1.6.0', None) + + def test_deserialize_entity_newer_revision_does_not_backport(self): + "Test object with supported (newer) revision" + self._test_deserialize_entity_newer('1.6.1', None) + + def test_deserialize_entity_newer_version_passes_revision(self): + "Test object with unsupported (newer) version and revision" + self._test_deserialize_entity_newer('1.7', '1.6.1', my_version='1.6.1') + + +class TestRegistry(test_base.TestCase): + + @mock.patch('watcher.objects.base.objects') + def test_hook_chooses_newer_properly(self, mock_objects): + reg = base.WatcherObjectRegistry() + reg.registration_hook(MyObj, 0) + + class MyNewerObj(object): + VERSION = '1.123' + + @classmethod + def obj_name(cls): + return 'MyObj' + + self.assertEqual(MyObj, mock_objects.MyObj) + reg.registration_hook(MyNewerObj, 0) + self.assertEqual(MyNewerObj, mock_objects.MyObj) + + @mock.patch('watcher.objects.base.objects') + def test_hook_keeps_newer_properly(self, mock_objects): + reg = base.WatcherObjectRegistry() + reg.registration_hook(MyObj, 0) + + class MyOlderObj(object): + VERSION = '1.1' + + @classmethod + def obj_name(cls): + return 'MyObj' + + self.assertEqual(MyObj, mock_objects.MyObj) + reg.registration_hook(MyOlderObj, 0) + self.assertEqual(MyObj, mock_objects.MyObj) diff --git a/watcher/tests/objects/test_service.py b/watcher/tests/objects/test_service.py index 5e5b6d354..2832fceb2 100644 --- a/watcher/tests/objects/test_service.py +++ b/watcher/tests/objects/test_service.py @@ -14,7 +14,6 @@ # under the License. import mock -from testtools import matchers from watcher import objects from watcher.tests.db import base @@ -43,7 +42,7 @@ class TestServiceObject(base.DbTestCase): mock_get_list.return_value = [self.fake_service] services = objects.Service.list(self.context) self.assertEqual(1, mock_get_list.call_count, 1) - self.assertThat(services, matchers.HasLength(1)) + self.assertEqual(1, len(services)) self.assertIsInstance(services[0], objects.Service) self.assertEqual(self.context, services[0]._context)