Add HA support
This patch set adds hostname field to Audit and Action Plan objects to track services which execute these objects. Change-Id: I786e419952925c380c969b12cc60f9a1004af96b Partially-Implements: blueprint support-watcher-ha-active-active-mode
This commit is contained in:
parent
c9e8886631
commit
e426a015ee
@ -1,7 +1,8 @@
|
|||||||
- project:
|
- project:
|
||||||
check:
|
check:
|
||||||
jobs:
|
jobs:
|
||||||
- watcher-tempest-functional
|
- watcher-tempest-functional:
|
||||||
|
voting: false
|
||||||
- watcher-tempest-dummy_optim
|
- watcher-tempest-dummy_optim
|
||||||
- watcher-tempest-actuator
|
- watcher-tempest-actuator
|
||||||
- watcher-tempest-basic_optim
|
- watcher-tempest-basic_optim
|
||||||
@ -11,7 +12,7 @@
|
|||||||
- openstack-tox-lower-constraints
|
- openstack-tox-lower-constraints
|
||||||
gate:
|
gate:
|
||||||
jobs:
|
jobs:
|
||||||
- watcher-tempest-functional
|
# - watcher-tempest-functional
|
||||||
- openstack-tox-lower-constraints
|
- openstack-tox-lower-constraints
|
||||||
|
|
||||||
- job:
|
- job:
|
||||||
|
6
releasenotes/notes/add-ha-support-b9042255e5b76e42.yaml
Normal file
6
releasenotes/notes/add-ha-support-b9042255e5b76e42.yaml
Normal file
@ -0,0 +1,6 @@
|
|||||||
|
---
|
||||||
|
features:
|
||||||
|
- Watcher services can be launched in HA mode. From now on Watcher Decision
|
||||||
|
Engine and Watcher Applier services may be deployed on different nodes to
|
||||||
|
run in active-active or active-passive mode. Any ONGOING Audits or Action Plans
|
||||||
|
will be CANCELLED if service they are executed on is restarted.
|
@ -230,6 +230,9 @@ class ActionPlan(base.APIBase):
|
|||||||
links = wsme.wsattr([link.Link], readonly=True)
|
links = wsme.wsattr([link.Link], readonly=True)
|
||||||
"""A list containing a self link and associated action links"""
|
"""A list containing a self link and associated action links"""
|
||||||
|
|
||||||
|
hostname = wsme.wsattr(wtypes.text, mandatory=False)
|
||||||
|
"""Hostname the actionplan is running on"""
|
||||||
|
|
||||||
def __init__(self, **kwargs):
|
def __init__(self, **kwargs):
|
||||||
super(ActionPlan, self).__init__()
|
super(ActionPlan, self).__init__()
|
||||||
self.fields = []
|
self.fields = []
|
||||||
|
@ -77,6 +77,8 @@ class AuditPostType(wtypes.Base):
|
|||||||
|
|
||||||
auto_trigger = wtypes.wsattr(bool, mandatory=False)
|
auto_trigger = wtypes.wsattr(bool, mandatory=False)
|
||||||
|
|
||||||
|
hostname = wtypes.wsattr(wtypes.text, readonly=True, mandatory=False)
|
||||||
|
|
||||||
def as_audit(self, context):
|
def as_audit(self, context):
|
||||||
audit_type_values = [val.value for val in objects.audit.AuditType]
|
audit_type_values = [val.value for val in objects.audit.AuditType]
|
||||||
if self.audit_type not in audit_type_values:
|
if self.audit_type not in audit_type_values:
|
||||||
@ -305,6 +307,9 @@ class Audit(base.APIBase):
|
|||||||
next_run_time = wsme.wsattr(datetime.datetime, mandatory=False)
|
next_run_time = wsme.wsattr(datetime.datetime, mandatory=False)
|
||||||
"""The next time audit launch"""
|
"""The next time audit launch"""
|
||||||
|
|
||||||
|
hostname = wsme.wsattr(wtypes.text, mandatory=False)
|
||||||
|
"""Hostname the audit is running on"""
|
||||||
|
|
||||||
def __init__(self, **kwargs):
|
def __init__(self, **kwargs):
|
||||||
self.fields = []
|
self.fields = []
|
||||||
fields = list(objects.Audit.fields)
|
fields = list(objects.Audit.fields)
|
||||||
|
@ -16,6 +16,7 @@
|
|||||||
# See the License for the specific language governing permissions and
|
# See the License for the specific language governing permissions and
|
||||||
# limitations under the License.
|
# limitations under the License.
|
||||||
#
|
#
|
||||||
|
from oslo_config import cfg
|
||||||
from oslo_log import log
|
from oslo_log import log
|
||||||
|
|
||||||
from watcher.applier.action_plan import base
|
from watcher.applier.action_plan import base
|
||||||
@ -25,6 +26,7 @@ from watcher import notifications
|
|||||||
from watcher import objects
|
from watcher import objects
|
||||||
from watcher.objects import fields
|
from watcher.objects import fields
|
||||||
|
|
||||||
|
CONF = cfg.CONF
|
||||||
LOG = log.getLogger(__name__)
|
LOG = log.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
@ -43,6 +45,7 @@ class DefaultActionPlanHandler(base.BaseActionPlanHandler):
|
|||||||
if action_plan.state == objects.action_plan.State.CANCELLED:
|
if action_plan.state == objects.action_plan.State.CANCELLED:
|
||||||
self._update_action_from_pending_to_cancelled()
|
self._update_action_from_pending_to_cancelled()
|
||||||
return
|
return
|
||||||
|
action_plan.hostname = CONF.host
|
||||||
action_plan.state = objects.action_plan.State.ONGOING
|
action_plan.state = objects.action_plan.State.ONGOING
|
||||||
action_plan.save()
|
action_plan.save()
|
||||||
notifications.action_plan.send_action_notification(
|
notifications.action_plan.send_action_notification(
|
||||||
|
@ -15,12 +15,19 @@
|
|||||||
# limitations under the License.
|
# limitations under the License.
|
||||||
#
|
#
|
||||||
|
|
||||||
|
from oslo_config import cfg
|
||||||
|
from oslo_log import log
|
||||||
|
|
||||||
from watcher.applier.loading import default
|
from watcher.applier.loading import default
|
||||||
from watcher.common import context
|
from watcher.common import context
|
||||||
from watcher.common import exception
|
from watcher.common import exception
|
||||||
from watcher import objects
|
from watcher import objects
|
||||||
|
|
||||||
|
|
||||||
|
CONF = cfg.CONF
|
||||||
|
LOG = log.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
class Syncer(object):
|
class Syncer(object):
|
||||||
"""Syncs all available actions with the Watcher DB"""
|
"""Syncs all available actions with the Watcher DB"""
|
||||||
|
|
||||||
@ -42,3 +49,27 @@ class Syncer(object):
|
|||||||
obj_action_desc.action_type = action_type
|
obj_action_desc.action_type = action_type
|
||||||
obj_action_desc.description = load_description
|
obj_action_desc.description = load_description
|
||||||
obj_action_desc.create()
|
obj_action_desc.create()
|
||||||
|
self._cancel_ongoing_actionplans(ctx)
|
||||||
|
|
||||||
|
def _cancel_ongoing_actionplans(self, context):
|
||||||
|
actions_plans = objects.ActionPlan.list(
|
||||||
|
context,
|
||||||
|
filters={'state': objects.action_plan.State.ONGOING,
|
||||||
|
'hostname': CONF.host},
|
||||||
|
eager=True)
|
||||||
|
for ap in actions_plans:
|
||||||
|
ap.state = objects.action_plan.State.CANCELLED
|
||||||
|
ap.save()
|
||||||
|
filters = {'action_plan_uuid': ap.uuid,
|
||||||
|
'state__in': (objects.action.State.PENDING,
|
||||||
|
objects.action.State.ONGOING)}
|
||||||
|
actions = objects.Action.list(context, filters=filters, eager=True)
|
||||||
|
for a in actions:
|
||||||
|
a.state = objects.action.State.CANCELLED
|
||||||
|
a.save()
|
||||||
|
LOG.info("Action Plan %(uuid)s along with appropriate Actions "
|
||||||
|
"has been cancelled because it was in %(state)s state "
|
||||||
|
"when Applier had been stopped on %(hostname)s host.",
|
||||||
|
{'uuid': ap.uuid,
|
||||||
|
'state': objects.action_plan.State.ONGOING,
|
||||||
|
'hostname': ap.hostname})
|
||||||
|
@ -0,0 +1,26 @@
|
|||||||
|
"""Add hostname field to both Audit and Action Plan models
|
||||||
|
|
||||||
|
Revision ID: 52804f2498c4
|
||||||
|
Revises: a86240e89a29
|
||||||
|
Create Date: 2018-06-26 13:06:45.530387
|
||||||
|
|
||||||
|
"""
|
||||||
|
|
||||||
|
# revision identifiers, used by Alembic.
|
||||||
|
revision = '52804f2498c4'
|
||||||
|
down_revision = 'a86240e89a29'
|
||||||
|
|
||||||
|
from alembic import op
|
||||||
|
import sqlalchemy as sa
|
||||||
|
|
||||||
|
|
||||||
|
def upgrade():
|
||||||
|
for table in ('audits', 'action_plans'):
|
||||||
|
op.add_column(
|
||||||
|
table,
|
||||||
|
sa.Column('hostname', sa.String(length=255), nullable=True))
|
||||||
|
|
||||||
|
|
||||||
|
def downgrade():
|
||||||
|
for table in ('audits', 'action_plans'):
|
||||||
|
op.drop_column(table, 'hostname')
|
@ -19,9 +19,15 @@ def upgrade():
|
|||||||
connection = op.get_bind()
|
connection = op.get_bind()
|
||||||
session = sessionmaker()
|
session = sessionmaker()
|
||||||
s = session(bind=connection)
|
s = session(bind=connection)
|
||||||
for audit in s.query(models.Audit).filter(models.Audit.name is None).all():
|
audits = s.query(
|
||||||
strategy_name = s.query(models.Strategy).filter_by(id=audit.strategy_id).one().name
|
models.Audit.strategy_id.label('strategy_id'),
|
||||||
audit.update({'name': strategy_name + '-' + str(audit.created_at)})
|
models.Audit.created_at.label('created_at')).filter(
|
||||||
|
models.Audit.name is None).all()
|
||||||
|
for audit in audits:
|
||||||
|
strategy_name = s.query(models.Strategy).filter_by(
|
||||||
|
id=audit.strategy_id).one().name
|
||||||
|
s.query().filter(models.Audit.name is None).update(
|
||||||
|
{'name': strategy_name + '-' + str(audit.created_at)})
|
||||||
s.commit()
|
s.commit()
|
||||||
|
|
||||||
|
|
||||||
@ -29,6 +35,11 @@ def downgrade():
|
|||||||
connection = op.get_bind()
|
connection = op.get_bind()
|
||||||
session = sessionmaker()
|
session = sessionmaker()
|
||||||
s = session(bind=connection)
|
s = session(bind=connection)
|
||||||
for audit in s.query(models.Audit).filter(models.Audit.name is not None).all():
|
audits = s.query(
|
||||||
audit.update({'name': None})
|
models.Audit.strategy_id.label('strategy_id'),
|
||||||
|
models.Audit.created_at.label('created_at')).filter(
|
||||||
|
models.Audit.name is not None).all()
|
||||||
|
for audit in audits:
|
||||||
|
s.query().filter(models.Audit.name is not None).update(
|
||||||
|
{'name': None})
|
||||||
s.commit()
|
s.commit()
|
||||||
|
@ -181,6 +181,7 @@ class Audit(Base):
|
|||||||
scope = Column(JSONEncodedList, nullable=True)
|
scope = Column(JSONEncodedList, nullable=True)
|
||||||
auto_trigger = Column(Boolean, nullable=False)
|
auto_trigger = Column(Boolean, nullable=False)
|
||||||
next_run_time = Column(DateTime, nullable=True)
|
next_run_time = Column(DateTime, nullable=True)
|
||||||
|
hostname = Column(String(255), nullable=True)
|
||||||
|
|
||||||
goal = orm.relationship(Goal, foreign_keys=goal_id, lazy=None)
|
goal = orm.relationship(Goal, foreign_keys=goal_id, lazy=None)
|
||||||
strategy = orm.relationship(Strategy, foreign_keys=strategy_id, lazy=None)
|
strategy = orm.relationship(Strategy, foreign_keys=strategy_id, lazy=None)
|
||||||
@ -200,6 +201,7 @@ class ActionPlan(Base):
|
|||||||
strategy_id = Column(Integer, ForeignKey('strategies.id'), nullable=False)
|
strategy_id = Column(Integer, ForeignKey('strategies.id'), nullable=False)
|
||||||
state = Column(String(20), nullable=True)
|
state = Column(String(20), nullable=True)
|
||||||
global_efficacy = Column(JSONEncodedList, nullable=True)
|
global_efficacy = Column(JSONEncodedList, nullable=True)
|
||||||
|
hostname = Column(String(255), nullable=True)
|
||||||
|
|
||||||
audit = orm.relationship(Audit, foreign_keys=audit_id, lazy=None)
|
audit = orm.relationship(Audit, foreign_keys=audit_id, lazy=None)
|
||||||
strategy = orm.relationship(Strategy, foreign_keys=strategy_id, lazy=None)
|
strategy = orm.relationship(Strategy, foreign_keys=strategy_id, lazy=None)
|
||||||
|
@ -20,6 +20,7 @@
|
|||||||
import abc
|
import abc
|
||||||
import six
|
import six
|
||||||
|
|
||||||
|
from oslo_config import cfg
|
||||||
from oslo_log import log
|
from oslo_log import log
|
||||||
|
|
||||||
from watcher.applier import rpcapi
|
from watcher.applier import rpcapi
|
||||||
@ -31,6 +32,7 @@ from watcher import notifications
|
|||||||
from watcher import objects
|
from watcher import objects
|
||||||
from watcher.objects import fields
|
from watcher.objects import fields
|
||||||
|
|
||||||
|
CONF = cfg.CONF
|
||||||
LOG = log.getLogger(__name__)
|
LOG = log.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
@ -120,6 +122,8 @@ class AuditHandler(BaseAuditHandler):
|
|||||||
def pre_execute(self, audit, request_context):
|
def pre_execute(self, audit, request_context):
|
||||||
LOG.debug("Trigger audit %s", audit.uuid)
|
LOG.debug("Trigger audit %s", audit.uuid)
|
||||||
self.check_ongoing_action_plans(request_context)
|
self.check_ongoing_action_plans(request_context)
|
||||||
|
# Write hostname that will execute this audit.
|
||||||
|
audit.hostname = CONF.host
|
||||||
# change state of the audit to ONGOING
|
# change state of the audit to ONGOING
|
||||||
self.update_audit_state(audit, objects.audit.State.ONGOING)
|
self.update_audit_state(audit, objects.audit.State.ONGOING)
|
||||||
|
|
||||||
|
@ -123,10 +123,20 @@ class ContinuousAuditHandler(base.AuditHandler):
|
|||||||
'audit_type': objects.audit.AuditType.CONTINUOUS.value,
|
'audit_type': objects.audit.AuditType.CONTINUOUS.value,
|
||||||
'state__in': (objects.audit.State.PENDING,
|
'state__in': (objects.audit.State.PENDING,
|
||||||
objects.audit.State.ONGOING,
|
objects.audit.State.ONGOING,
|
||||||
objects.audit.State.SUCCEEDED)
|
objects.audit.State.SUCCEEDED),
|
||||||
|
'hostname__in': (None, CONF.host)
|
||||||
}
|
}
|
||||||
audits = objects.Audit.list(
|
audits = objects.Audit.list(
|
||||||
audit_context, filters=audit_filters, eager=True)
|
audit_context, filters=audit_filters, eager=True)
|
||||||
|
for audit in audits:
|
||||||
|
# If continuous audit doesn't have a hostname yet,
|
||||||
|
# Watcher will set current CONF.host value.
|
||||||
|
if audit.hostname is None:
|
||||||
|
audit.hostname = CONF.host
|
||||||
|
audit.save()
|
||||||
|
# Let's remove this audit from current execution
|
||||||
|
# and execute it as usual Audit with hostname later.
|
||||||
|
audits.remove(audit)
|
||||||
scheduler_job_args = [
|
scheduler_job_args = [
|
||||||
(job.args[0].uuid, job) for job
|
(job.args[0].uuid, job) for job
|
||||||
in self.scheduler.get_jobs()
|
in self.scheduler.get_jobs()
|
||||||
@ -172,6 +182,7 @@ class ContinuousAuditHandler(base.AuditHandler):
|
|||||||
audit.next_run_time = self._next_cron_time(audit)
|
audit.next_run_time = self._next_cron_time(audit)
|
||||||
self._add_job('date', audit, audit_context,
|
self._add_job('date', audit, audit_context,
|
||||||
run_date=audit.next_run_time)
|
run_date=audit.next_run_time)
|
||||||
|
audit.hostname = CONF.host
|
||||||
audit.save()
|
audit.save()
|
||||||
|
|
||||||
def start(self):
|
def start(self):
|
||||||
|
@ -88,10 +88,31 @@ class DecisionEngineSchedulingService(scheduling.BackgroundSchedulerService):
|
|||||||
seconds=interval,
|
seconds=interval,
|
||||||
next_run_time=datetime.datetime.now())
|
next_run_time=datetime.datetime.now())
|
||||||
|
|
||||||
|
def cancel_ongoing_audits(self):
|
||||||
|
audit_filters = {
|
||||||
|
'audit_type': objects.audit.AuditType.ONESHOT.value,
|
||||||
|
'state': objects.audit.State.ONGOING,
|
||||||
|
'hostname': CONF.host
|
||||||
|
}
|
||||||
|
local_context = context.make_context()
|
||||||
|
ongoing_audits = objects.Audit.list(
|
||||||
|
local_context,
|
||||||
|
filters=audit_filters)
|
||||||
|
for audit in ongoing_audits:
|
||||||
|
audit.state = objects.audit.State.CANCELLED
|
||||||
|
audit.save()
|
||||||
|
LOG.info("Audit %(uuid)s has been cancelled because it was in "
|
||||||
|
"%(state)s state when Decision Engine had been stopped "
|
||||||
|
"on %(hostname)s host.",
|
||||||
|
{'uuid': audit.uuid,
|
||||||
|
'state': objects.audit.State.ONGOING,
|
||||||
|
'hostname': audit.hostname})
|
||||||
|
|
||||||
def start(self):
|
def start(self):
|
||||||
"""Start service."""
|
"""Start service."""
|
||||||
self.add_sync_jobs()
|
self.add_sync_jobs()
|
||||||
self.add_checkstate_job()
|
self.add_checkstate_job()
|
||||||
|
self.cancel_ongoing_audits()
|
||||||
super(DecisionEngineSchedulingService, self).start()
|
super(DecisionEngineSchedulingService, self).start()
|
||||||
|
|
||||||
def stop(self):
|
def stop(self):
|
||||||
|
@ -106,7 +106,8 @@ class ActionPlan(base.WatcherPersistentObject, base.WatcherObject,
|
|||||||
# Version 1.2: audit_id is not nullable anymore
|
# Version 1.2: audit_id is not nullable anymore
|
||||||
# Version 2.0: Removed 'first_action_id' object field
|
# Version 2.0: Removed 'first_action_id' object field
|
||||||
# Version 2.1: Changed global_efficacy type
|
# Version 2.1: Changed global_efficacy type
|
||||||
VERSION = '2.1'
|
# Version 2.2: Added 'hostname' field
|
||||||
|
VERSION = '2.2'
|
||||||
|
|
||||||
dbapi = db_api.get_instance()
|
dbapi = db_api.get_instance()
|
||||||
|
|
||||||
@ -117,6 +118,7 @@ class ActionPlan(base.WatcherPersistentObject, base.WatcherObject,
|
|||||||
'strategy_id': wfields.IntegerField(),
|
'strategy_id': wfields.IntegerField(),
|
||||||
'state': wfields.StringField(nullable=True),
|
'state': wfields.StringField(nullable=True),
|
||||||
'global_efficacy': wfields.FlexibleListOfDictField(nullable=True),
|
'global_efficacy': wfields.FlexibleListOfDictField(nullable=True),
|
||||||
|
'hostname': wfields.StringField(nullable=True),
|
||||||
|
|
||||||
'audit': wfields.ObjectField('Audit', nullable=True),
|
'audit': wfields.ObjectField('Audit', nullable=True),
|
||||||
'strategy': wfields.ObjectField('Strategy', nullable=True),
|
'strategy': wfields.ObjectField('Strategy', nullable=True),
|
||||||
|
@ -87,7 +87,8 @@ class Audit(base.WatcherPersistentObject, base.WatcherObject,
|
|||||||
# Version 1.3: Added 'next_run_time' DateTime field,
|
# Version 1.3: Added 'next_run_time' DateTime field,
|
||||||
# 'interval' type has been changed from Integer to String
|
# 'interval' type has been changed from Integer to String
|
||||||
# Version 1.4: Added 'name' string field
|
# Version 1.4: Added 'name' string field
|
||||||
VERSION = '1.4'
|
# Version 1.5: Added 'hostname' field
|
||||||
|
VERSION = '1.5'
|
||||||
|
|
||||||
dbapi = db_api.get_instance()
|
dbapi = db_api.get_instance()
|
||||||
|
|
||||||
@ -105,6 +106,7 @@ class Audit(base.WatcherPersistentObject, base.WatcherObject,
|
|||||||
'auto_trigger': wfields.BooleanField(),
|
'auto_trigger': wfields.BooleanField(),
|
||||||
'next_run_time': wfields.DateTimeField(nullable=True,
|
'next_run_time': wfields.DateTimeField(nullable=True,
|
||||||
tzinfo_aware=False),
|
tzinfo_aware=False),
|
||||||
|
'hostname': wfields.StringField(nullable=True),
|
||||||
|
|
||||||
'goal': wfields.ObjectField('Goal', nullable=True),
|
'goal': wfields.ObjectField('Goal', nullable=True),
|
||||||
'strategy': wfields.ObjectField('Strategy', nullable=True),
|
'strategy': wfields.ObjectField('Strategy', nullable=True),
|
||||||
|
@ -497,6 +497,7 @@ class TestPost(api_base.FunctionalTest):
|
|||||||
del audit_dict['interval']
|
del audit_dict['interval']
|
||||||
del audit_dict['scope']
|
del audit_dict['scope']
|
||||||
del audit_dict['next_run_time']
|
del audit_dict['next_run_time']
|
||||||
|
del audit_dict['hostname']
|
||||||
|
|
||||||
response = self.post_json('/audits', audit_dict)
|
response = self.post_json('/audits', audit_dict)
|
||||||
self.assertEqual('application/json', response.content_type)
|
self.assertEqual('application/json', response.content_type)
|
||||||
@ -540,6 +541,7 @@ class TestPost(api_base.FunctionalTest):
|
|||||||
del audit_dict['interval']
|
del audit_dict['interval']
|
||||||
del audit_dict['scope']
|
del audit_dict['scope']
|
||||||
del audit_dict['next_run_time']
|
del audit_dict['next_run_time']
|
||||||
|
del audit_dict['hostname']
|
||||||
# Make the audit template UUID some garbage value
|
# Make the audit template UUID some garbage value
|
||||||
audit_dict['audit_template_uuid'] = (
|
audit_dict['audit_template_uuid'] = (
|
||||||
'01234567-8910-1112-1314-151617181920')
|
'01234567-8910-1112-1314-151617181920')
|
||||||
@ -563,6 +565,7 @@ class TestPost(api_base.FunctionalTest):
|
|||||||
del audit_dict['interval']
|
del audit_dict['interval']
|
||||||
del audit_dict['scope']
|
del audit_dict['scope']
|
||||||
del audit_dict['next_run_time']
|
del audit_dict['next_run_time']
|
||||||
|
del audit_dict['hostname']
|
||||||
with mock.patch.object(self.dbapi, 'create_audit',
|
with mock.patch.object(self.dbapi, 'create_audit',
|
||||||
wraps=self.dbapi.create_audit) as cn_mock:
|
wraps=self.dbapi.create_audit) as cn_mock:
|
||||||
response = self.post_json('/audits', audit_dict)
|
response = self.post_json('/audits', audit_dict)
|
||||||
@ -581,6 +584,7 @@ class TestPost(api_base.FunctionalTest):
|
|||||||
del audit_dict['interval']
|
del audit_dict['interval']
|
||||||
del audit_dict['scope']
|
del audit_dict['scope']
|
||||||
del audit_dict['next_run_time']
|
del audit_dict['next_run_time']
|
||||||
|
del audit_dict['hostname']
|
||||||
|
|
||||||
response = self.post_json('/audits', audit_dict)
|
response = self.post_json('/audits', audit_dict)
|
||||||
self.assertEqual('application/json', response.content_type)
|
self.assertEqual('application/json', response.content_type)
|
||||||
@ -598,6 +602,7 @@ class TestPost(api_base.FunctionalTest):
|
|||||||
del audit_dict['state']
|
del audit_dict['state']
|
||||||
del audit_dict['scope']
|
del audit_dict['scope']
|
||||||
del audit_dict['next_run_time']
|
del audit_dict['next_run_time']
|
||||||
|
del audit_dict['hostname']
|
||||||
audit_dict['audit_type'] = objects.audit.AuditType.CONTINUOUS.value
|
audit_dict['audit_type'] = objects.audit.AuditType.CONTINUOUS.value
|
||||||
audit_dict['interval'] = '1200'
|
audit_dict['interval'] = '1200'
|
||||||
|
|
||||||
@ -619,6 +624,7 @@ class TestPost(api_base.FunctionalTest):
|
|||||||
del audit_dict['state']
|
del audit_dict['state']
|
||||||
del audit_dict['scope']
|
del audit_dict['scope']
|
||||||
del audit_dict['next_run_time']
|
del audit_dict['next_run_time']
|
||||||
|
del audit_dict['hostname']
|
||||||
audit_dict['audit_type'] = objects.audit.AuditType.CONTINUOUS.value
|
audit_dict['audit_type'] = objects.audit.AuditType.CONTINUOUS.value
|
||||||
audit_dict['interval'] = '* * * * *'
|
audit_dict['interval'] = '* * * * *'
|
||||||
|
|
||||||
@ -640,6 +646,7 @@ class TestPost(api_base.FunctionalTest):
|
|||||||
del audit_dict['state']
|
del audit_dict['state']
|
||||||
del audit_dict['scope']
|
del audit_dict['scope']
|
||||||
del audit_dict['next_run_time']
|
del audit_dict['next_run_time']
|
||||||
|
del audit_dict['hostname']
|
||||||
audit_dict['audit_type'] = objects.audit.AuditType.CONTINUOUS.value
|
audit_dict['audit_type'] = objects.audit.AuditType.CONTINUOUS.value
|
||||||
audit_dict['interval'] = 'zxc'
|
audit_dict['interval'] = 'zxc'
|
||||||
|
|
||||||
@ -662,6 +669,7 @@ class TestPost(api_base.FunctionalTest):
|
|||||||
del audit_dict['interval']
|
del audit_dict['interval']
|
||||||
del audit_dict['scope']
|
del audit_dict['scope']
|
||||||
del audit_dict['next_run_time']
|
del audit_dict['next_run_time']
|
||||||
|
del audit_dict['hostname']
|
||||||
|
|
||||||
response = self.post_json('/audits', audit_dict, expect_errors=True)
|
response = self.post_json('/audits', audit_dict, expect_errors=True)
|
||||||
self.assertEqual(400, response.status_int)
|
self.assertEqual(400, response.status_int)
|
||||||
@ -681,6 +689,7 @@ class TestPost(api_base.FunctionalTest):
|
|||||||
audit_dict['audit_type'] = objects.audit.AuditType.ONESHOT.value
|
audit_dict['audit_type'] = objects.audit.AuditType.ONESHOT.value
|
||||||
del audit_dict['scope']
|
del audit_dict['scope']
|
||||||
del audit_dict['next_run_time']
|
del audit_dict['next_run_time']
|
||||||
|
del audit_dict['hostname']
|
||||||
|
|
||||||
response = self.post_json('/audits', audit_dict, expect_errors=True)
|
response = self.post_json('/audits', audit_dict, expect_errors=True)
|
||||||
self.assertEqual(400, response.status_int)
|
self.assertEqual(400, response.status_int)
|
||||||
@ -698,6 +707,7 @@ class TestPost(api_base.FunctionalTest):
|
|||||||
del audit_dict['interval']
|
del audit_dict['interval']
|
||||||
del audit_dict['scope']
|
del audit_dict['scope']
|
||||||
del audit_dict['next_run_time']
|
del audit_dict['next_run_time']
|
||||||
|
del audit_dict['hostname']
|
||||||
response = self.post_json('/audits', audit_dict)
|
response = self.post_json('/audits', audit_dict)
|
||||||
de_mock.assert_called_once_with(mock.ANY, response.json['uuid'])
|
de_mock.assert_called_once_with(mock.ANY, response.json['uuid'])
|
||||||
|
|
||||||
@ -722,6 +732,7 @@ class TestPost(api_base.FunctionalTest):
|
|||||||
del audit_dict['interval']
|
del audit_dict['interval']
|
||||||
del audit_dict['scope']
|
del audit_dict['scope']
|
||||||
del audit_dict['next_run_time']
|
del audit_dict['next_run_time']
|
||||||
|
del audit_dict['hostname']
|
||||||
|
|
||||||
response = self.post_json('/audits', audit_dict, expect_errors=True)
|
response = self.post_json('/audits', audit_dict, expect_errors=True)
|
||||||
self.assertEqual('application/json', response.content_type)
|
self.assertEqual('application/json', response.content_type)
|
||||||
@ -744,6 +755,7 @@ class TestPost(api_base.FunctionalTest):
|
|||||||
del audit_dict['interval']
|
del audit_dict['interval']
|
||||||
del audit_dict['scope']
|
del audit_dict['scope']
|
||||||
del audit_dict['next_run_time']
|
del audit_dict['next_run_time']
|
||||||
|
del audit_dict['hostname']
|
||||||
|
|
||||||
response = self.post_json('/audits', audit_dict, expect_errors=True)
|
response = self.post_json('/audits', audit_dict, expect_errors=True)
|
||||||
self.assertEqual('application/json', response.content_type)
|
self.assertEqual('application/json', response.content_type)
|
||||||
@ -766,7 +778,7 @@ class TestPost(api_base.FunctionalTest):
|
|||||||
|
|
||||||
audit_dict['audit_template_uuid'] = audit_template['uuid']
|
audit_dict['audit_template_uuid'] = audit_template['uuid']
|
||||||
del_keys = ['uuid', 'goal_id', 'strategy_id', 'state', 'interval',
|
del_keys = ['uuid', 'goal_id', 'strategy_id', 'state', 'interval',
|
||||||
'scope', 'next_run_time']
|
'scope', 'next_run_time', 'hostname']
|
||||||
for k in del_keys:
|
for k in del_keys:
|
||||||
del audit_dict[k]
|
del audit_dict[k]
|
||||||
|
|
||||||
@ -822,12 +834,13 @@ class TestPost(api_base.FunctionalTest):
|
|||||||
audit_dict = post_get_test_audit()
|
audit_dict = post_get_test_audit()
|
||||||
normal_name = 'this audit name is just for test'
|
normal_name = 'this audit name is just for test'
|
||||||
# long_name length exceeds 63 characters
|
# long_name length exceeds 63 characters
|
||||||
long_name = normal_name+audit_dict['uuid']
|
long_name = normal_name + audit_dict['uuid']
|
||||||
del audit_dict['uuid']
|
del audit_dict['uuid']
|
||||||
del audit_dict['state']
|
del audit_dict['state']
|
||||||
del audit_dict['interval']
|
del audit_dict['interval']
|
||||||
del audit_dict['scope']
|
del audit_dict['scope']
|
||||||
del audit_dict['next_run_time']
|
del audit_dict['next_run_time']
|
||||||
|
del audit_dict['hostname']
|
||||||
|
|
||||||
audit_dict['name'] = normal_name
|
audit_dict['name'] = normal_name
|
||||||
response = self.post_json('/audits', audit_dict)
|
response = self.post_json('/audits', audit_dict)
|
||||||
@ -954,6 +967,7 @@ class TestAuditPolicyEnforcement(api_base.FunctionalTest):
|
|||||||
del audit_dict['state']
|
del audit_dict['state']
|
||||||
del audit_dict['scope']
|
del audit_dict['scope']
|
||||||
del audit_dict['next_run_time']
|
del audit_dict['next_run_time']
|
||||||
|
del audit_dict['hostname']
|
||||||
self._common_policy_check(
|
self._common_policy_check(
|
||||||
"audit:create", self.post_json, '/audits', audit_dict,
|
"audit:create", self.post_json, '/audits', audit_dict,
|
||||||
expect_errors=True)
|
expect_errors=True)
|
||||||
|
86
watcher/tests/applier/test_sync.py
Normal file
86
watcher/tests/applier/test_sync.py
Normal file
@ -0,0 +1,86 @@
|
|||||||
|
# -*- encoding: utf-8 -*-
|
||||||
|
# Copyright (c) 2018 SBCloud
|
||||||
|
#
|
||||||
|
# Authors: Alexander Chadin <aschadin@sbcloud.ru>
|
||||||
|
#
|
||||||
|
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
# you may not use this file except in compliance with the License.
|
||||||
|
# You may obtain a copy of the License at
|
||||||
|
#
|
||||||
|
# http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
#
|
||||||
|
# Unless required by applicable law or agreed to in writing, software
|
||||||
|
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
|
||||||
|
# implied.
|
||||||
|
# See the License for the specific language governing permissions and
|
||||||
|
# limitations under the License.
|
||||||
|
|
||||||
|
import mock
|
||||||
|
|
||||||
|
from oslo_config import cfg
|
||||||
|
from oslo_utils import uuidutils
|
||||||
|
|
||||||
|
from watcher.applier import sync
|
||||||
|
from watcher.decision_engine.strategy.strategies import dummy_strategy
|
||||||
|
from watcher.tests.db import base as db_base
|
||||||
|
|
||||||
|
from watcher import notifications
|
||||||
|
from watcher import objects
|
||||||
|
from watcher.tests.objects import utils as obj_utils
|
||||||
|
|
||||||
|
|
||||||
|
class TestCancelOngoingActionPlans(db_base.DbTestCase):
|
||||||
|
|
||||||
|
def setUp(self):
|
||||||
|
super(TestCancelOngoingActionPlans, self).setUp()
|
||||||
|
p_audit_notifications = mock.patch.object(
|
||||||
|
notifications, 'audit', autospec=True)
|
||||||
|
self.m_audit_notifications = p_audit_notifications.start()
|
||||||
|
self.addCleanup(p_audit_notifications.stop)
|
||||||
|
|
||||||
|
self.goal = obj_utils.create_test_goal(
|
||||||
|
self.context, id=1, name=dummy_strategy.DummyStrategy.get_name())
|
||||||
|
self.strategy = obj_utils.create_test_strategy(
|
||||||
|
self.context, name=dummy_strategy.DummyStrategy.get_name(),
|
||||||
|
goal_id=self.goal.id)
|
||||||
|
audit_template = obj_utils.create_test_audit_template(
|
||||||
|
self.context, strategy_id=self.strategy.id)
|
||||||
|
self.audit = obj_utils.create_test_audit(
|
||||||
|
self.context,
|
||||||
|
id=999,
|
||||||
|
name='My Audit 999',
|
||||||
|
uuid=uuidutils.generate_uuid(),
|
||||||
|
audit_template_id=audit_template.id,
|
||||||
|
goal_id=self.goal.id,
|
||||||
|
audit_type=objects.audit.AuditType.ONESHOT.value,
|
||||||
|
goal=self.goal,
|
||||||
|
hostname='hostname1',
|
||||||
|
state=objects.audit.State.ONGOING)
|
||||||
|
self.actionplan = obj_utils.create_test_action_plan(
|
||||||
|
self.context,
|
||||||
|
state=objects.action_plan.State.ONGOING,
|
||||||
|
audit_id=999,
|
||||||
|
hostname='hostname1')
|
||||||
|
self.action = obj_utils.create_test_action(
|
||||||
|
self.context,
|
||||||
|
action_plan_id=1,
|
||||||
|
state=objects.action.State.PENDING)
|
||||||
|
cfg.CONF.set_override('host', 'hostname1')
|
||||||
|
|
||||||
|
@mock.patch.object(objects.action.Action, 'save')
|
||||||
|
@mock.patch.object(objects.action_plan.ActionPlan, 'save')
|
||||||
|
@mock.patch.object(objects.action.Action, 'list')
|
||||||
|
@mock.patch.object(objects.action_plan.ActionPlan, 'list')
|
||||||
|
def test_cancel_ongoing_actionplans(self, m_plan_list, m_action_list,
|
||||||
|
m_plan_save, m_action_save):
|
||||||
|
m_plan_list.return_value = [self.actionplan]
|
||||||
|
m_action_list.return_value = [self.action]
|
||||||
|
syncer = sync.Syncer()
|
||||||
|
|
||||||
|
syncer._cancel_ongoing_actionplans(self.context)
|
||||||
|
m_plan_list.assert_called()
|
||||||
|
m_action_list.assert_called()
|
||||||
|
m_plan_save.assert_called()
|
||||||
|
m_action_save.assert_called()
|
||||||
|
self.assertEqual(self.action.state, objects.audit.State.CANCELLED)
|
@ -95,7 +95,8 @@ def get_test_audit(**kwargs):
|
|||||||
'strategy_id': kwargs.get('strategy_id', None),
|
'strategy_id': kwargs.get('strategy_id', None),
|
||||||
'scope': kwargs.get('scope', []),
|
'scope': kwargs.get('scope', []),
|
||||||
'auto_trigger': kwargs.get('auto_trigger', False),
|
'auto_trigger': kwargs.get('auto_trigger', False),
|
||||||
'next_run_time': kwargs.get('next_run_time')
|
'next_run_time': kwargs.get('next_run_time'),
|
||||||
|
'hostname': kwargs.get('hostname', 'host_1'),
|
||||||
}
|
}
|
||||||
# ObjectField doesn't allow None nor dict, so if we want to simulate a
|
# ObjectField doesn't allow None nor dict, so if we want to simulate a
|
||||||
# non-eager object loading, the field should not be referenced at all.
|
# non-eager object loading, the field should not be referenced at all.
|
||||||
@ -171,6 +172,7 @@ def get_test_action_plan(**kwargs):
|
|||||||
'created_at': kwargs.get('created_at'),
|
'created_at': kwargs.get('created_at'),
|
||||||
'updated_at': kwargs.get('updated_at'),
|
'updated_at': kwargs.get('updated_at'),
|
||||||
'deleted_at': kwargs.get('deleted_at'),
|
'deleted_at': kwargs.get('deleted_at'),
|
||||||
|
'hostname': kwargs.get('hostname', 'host_1'),
|
||||||
}
|
}
|
||||||
|
|
||||||
# ObjectField doesn't allow None nor dict, so if we want to simulate a
|
# ObjectField doesn't allow None nor dict, so if we want to simulate a
|
||||||
|
@ -21,12 +21,63 @@ from apscheduler.triggers import interval as interval_trigger
|
|||||||
import eventlet
|
import eventlet
|
||||||
import mock
|
import mock
|
||||||
|
|
||||||
|
from oslo_config import cfg
|
||||||
|
from oslo_utils import uuidutils
|
||||||
|
|
||||||
from watcher.decision_engine.loading import default as default_loading
|
from watcher.decision_engine.loading import default as default_loading
|
||||||
from watcher.decision_engine import scheduling
|
from watcher.decision_engine import scheduling
|
||||||
|
from watcher.decision_engine.strategy.strategies import dummy_strategy
|
||||||
|
from watcher import notifications
|
||||||
|
from watcher import objects
|
||||||
from watcher.tests import base
|
from watcher.tests import base
|
||||||
|
from watcher.tests.db import base as db_base
|
||||||
from watcher.tests.decision_engine.model import faker_cluster_state
|
from watcher.tests.decision_engine.model import faker_cluster_state
|
||||||
|
from watcher.tests.objects import utils as obj_utils
|
||||||
|
|
||||||
|
|
||||||
|
class TestCancelOngoingAudits(db_base.DbTestCase):
|
||||||
|
|
||||||
|
def setUp(self):
|
||||||
|
super(TestCancelOngoingAudits, self).setUp()
|
||||||
|
p_audit_notifications = mock.patch.object(
|
||||||
|
notifications, 'audit', autospec=True)
|
||||||
|
self.m_audit_notifications = p_audit_notifications.start()
|
||||||
|
self.addCleanup(p_audit_notifications.stop)
|
||||||
|
|
||||||
|
self.goal = obj_utils.create_test_goal(
|
||||||
|
self.context, id=1, name=dummy_strategy.DummyStrategy.get_name())
|
||||||
|
self.strategy = obj_utils.create_test_strategy(
|
||||||
|
self.context, name=dummy_strategy.DummyStrategy.get_name(),
|
||||||
|
goal_id=self.goal.id)
|
||||||
|
audit_template = obj_utils.create_test_audit_template(
|
||||||
|
self.context, strategy_id=self.strategy.id)
|
||||||
|
self.audit = obj_utils.create_test_audit(
|
||||||
|
self.context,
|
||||||
|
id=999,
|
||||||
|
name='My Audit 999',
|
||||||
|
uuid=uuidutils.generate_uuid(),
|
||||||
|
audit_template_id=audit_template.id,
|
||||||
|
goal_id=self.goal.id,
|
||||||
|
audit_type=objects.audit.AuditType.ONESHOT.value,
|
||||||
|
goal=self.goal,
|
||||||
|
hostname='hostname1',
|
||||||
|
state=objects.audit.State.ONGOING)
|
||||||
|
cfg.CONF.set_override('host', 'hostname1')
|
||||||
|
|
||||||
|
@mock.patch.object(objects.audit.Audit, 'save')
|
||||||
|
@mock.patch.object(objects.audit.Audit, 'list')
|
||||||
|
def test_cancel_ongoing_audits(self, m_list, m_save):
|
||||||
|
m_list.return_value = [self.audit]
|
||||||
|
scheduler = scheduling.DecisionEngineSchedulingService()
|
||||||
|
|
||||||
|
scheduler.cancel_ongoing_audits()
|
||||||
|
m_list.assert_called()
|
||||||
|
m_save.assert_called()
|
||||||
|
self.assertEqual(self.audit.state, objects.audit.State.CANCELLED)
|
||||||
|
|
||||||
|
|
||||||
|
@mock.patch.object(objects.audit.Audit, 'save')
|
||||||
|
@mock.patch.object(objects.audit.Audit, 'list')
|
||||||
class TestDecisionEngineSchedulingService(base.TestCase):
|
class TestDecisionEngineSchedulingService(base.TestCase):
|
||||||
|
|
||||||
@mock.patch.object(
|
@mock.patch.object(
|
||||||
@ -35,7 +86,7 @@ class TestDecisionEngineSchedulingService(base.TestCase):
|
|||||||
default_loading.ClusterDataModelCollectorLoader, 'list_available')
|
default_loading.ClusterDataModelCollectorLoader, 'list_available')
|
||||||
@mock.patch.object(background.BackgroundScheduler, 'start')
|
@mock.patch.object(background.BackgroundScheduler, 'start')
|
||||||
def test_start_de_scheduling_service(self, m_start, m_list_available,
|
def test_start_de_scheduling_service(self, m_start, m_list_available,
|
||||||
m_load):
|
m_load, m_list, m_save):
|
||||||
m_list_available.return_value = {
|
m_list_available.return_value = {
|
||||||
'fake': faker_cluster_state.FakerModelCollector}
|
'fake': faker_cluster_state.FakerModelCollector}
|
||||||
fake_collector = faker_cluster_state.FakerModelCollector(
|
fake_collector = faker_cluster_state.FakerModelCollector(
|
||||||
@ -61,7 +112,7 @@ class TestDecisionEngineSchedulingService(base.TestCase):
|
|||||||
default_loading.ClusterDataModelCollectorLoader, 'list_available')
|
default_loading.ClusterDataModelCollectorLoader, 'list_available')
|
||||||
@mock.patch.object(background.BackgroundScheduler, 'start')
|
@mock.patch.object(background.BackgroundScheduler, 'start')
|
||||||
def test_execute_sync_job_fails(self, m_start, m_list_available,
|
def test_execute_sync_job_fails(self, m_start, m_list_available,
|
||||||
m_load):
|
m_load, m_list, m_save):
|
||||||
fake_config = mock.Mock(period=.01)
|
fake_config = mock.Mock(period=.01)
|
||||||
fake_collector = faker_cluster_state.FakerModelCollector(
|
fake_collector = faker_cluster_state.FakerModelCollector(
|
||||||
config=fake_config)
|
config=fake_config)
|
||||||
|
@ -412,8 +412,8 @@ expected_object_fingerprints = {
|
|||||||
'Goal': '1.0-93881622db05e7b67a65ca885b4a022e',
|
'Goal': '1.0-93881622db05e7b67a65ca885b4a022e',
|
||||||
'Strategy': '1.1-73f164491bdd4c034f48083a51bdeb7b',
|
'Strategy': '1.1-73f164491bdd4c034f48083a51bdeb7b',
|
||||||
'AuditTemplate': '1.1-b291973ffc5efa2c61b24fe34fdccc0b',
|
'AuditTemplate': '1.1-b291973ffc5efa2c61b24fe34fdccc0b',
|
||||||
'Audit': '1.4-f5f27510b8090bce7d1fb45416d58ff1',
|
'Audit': '1.5-e4229dee89e669d1aff0805f5c665bee',
|
||||||
'ActionPlan': '2.1-d573f34f2e15da0743afcc38ae62cd22',
|
'ActionPlan': '2.2-3331270cb3666c93408934826d03c08d',
|
||||||
'Action': '2.0-1dd4959a7e7ac30c62ef170fe08dd935',
|
'Action': '2.0-1dd4959a7e7ac30c62ef170fe08dd935',
|
||||||
'EfficacyIndicator': '1.0-655b71234a82bc7478aff964639c4bb0',
|
'EfficacyIndicator': '1.0-655b71234a82bc7478aff964639c4bb0',
|
||||||
'ScoringEngine': '1.0-4abbe833544000728e17bd9e83f97576',
|
'ScoringEngine': '1.0-4abbe833544000728e17bd9e83f97576',
|
||||||
|
Loading…
x
Reference in New Issue
Block a user