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
This commit is contained in:
Vincent Françoise
2016-08-22 11:15:13 +02:00
parent ed95d621f4
commit fc31dae7f2
36 changed files with 1109 additions and 1492 deletions

View File

@@ -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

View File

@@ -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

View File

@@ -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)

View File

@@ -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)

View File

@@ -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])

View File

@@ -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

View File

@@ -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):

View File

@@ -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)

View File

@@ -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)

View File

@@ -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)

View File

@@ -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:

View File

@@ -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):

View File

@@ -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:

View File

@@ -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 = [

View File

@@ -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')

View File

@@ -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()

View File

@@ -45,8 +45,9 @@ composed of two types of Action Item(s):
An :ref:`Action Plan <action_plan_definition>` may be described using
standard workflow model description formats such as
`Business Process Model and Notation 2.0 (BPMN 2.0) <http://www.omg.org/spec/BPMN/2.0/>`_
or `Unified Modeling Language (UML) <http://www.uml.org/>`_.
`Business Process Model and Notation 2.0 (BPMN 2.0)
<http://www.omg.org/spec/BPMN/2.0/>`_ or `Unified Modeling Language (UML)
<http://www.uml.org/>`_.
An :ref:`Action Plan <action_plan_definition>` 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 <action_plan_definition>` was in
**PENDING** or **ONGOING** state and was cancelled by the
:ref:`Administrator <administrator_definition>`
""" # 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})

View File

@@ -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()

View File

@@ -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)

View File

@@ -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

View File

@@ -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.

89
watcher/objects/fields.py Normal file
View File

@@ -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)

View File

@@ -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)

View File

@@ -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)

View File

@@ -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)

View File

@@ -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.

View File

@@ -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()

View File

@@ -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': {

View File

@@ -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,

View File

@@ -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

View File

@@ -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.

View File

@@ -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()

View File

@@ -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'),

View File

@@ -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))

View File

@@ -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)

View File

@@ -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)