Merge "New cron type for audit interval"
This commit is contained in:
commit
2266e2baa3
@ -9,6 +9,7 @@ keystoneauth1>=2.21.0 # Apache-2.0
|
||||
jsonschema!=2.5.0,<3.0.0,>=2.0.0 # MIT
|
||||
keystonemiddleware>=4.12.0 # Apache-2.0
|
||||
lxml!=3.7.0,>=2.3 # BSD
|
||||
croniter>=0.3.4 # MIT License
|
||||
oslo.concurrency>=3.8.0 # Apache-2.0
|
||||
oslo.cache>=1.5.0 # Apache-2.0
|
||||
oslo.config!=4.3.0,!=4.4.0,>=4.0.0 # Apache-2.0
|
||||
|
@ -65,7 +65,7 @@ class AuditPostType(wtypes.Base):
|
||||
|
||||
parameters = wtypes.wsattr({wtypes.text: types.jsontype}, mandatory=False,
|
||||
default={})
|
||||
interval = wsme.wsattr(int, mandatory=False)
|
||||
interval = wsme.wsattr(types.interval_or_cron, mandatory=False)
|
||||
|
||||
scope = wtypes.wsattr(types.jsontype, readonly=True)
|
||||
|
||||
@ -261,7 +261,7 @@ class Audit(base.APIBase):
|
||||
links = wsme.wsattr([link.Link], readonly=True)
|
||||
"""A list containing a self link and associated audit links"""
|
||||
|
||||
interval = wsme.wsattr(int, mandatory=False)
|
||||
interval = wsme.wsattr(wtypes.text, mandatory=False)
|
||||
"""Launch audit periodically (in seconds)"""
|
||||
|
||||
scope = wsme.wsattr(types.jsontype, mandatory=False)
|
||||
@ -270,6 +270,9 @@ class Audit(base.APIBase):
|
||||
auto_trigger = wsme.wsattr(bool, mandatory=False, default=False)
|
||||
"""Autoexecute action plan once audit is succeeded"""
|
||||
|
||||
next_run_time = wsme.wsattr(datetime.datetime, mandatory=False)
|
||||
"""The next time audit launch"""
|
||||
|
||||
def __init__(self, **kwargs):
|
||||
self.fields = []
|
||||
fields = list(objects.Audit.fields)
|
||||
@ -301,7 +304,8 @@ class Audit(base.APIBase):
|
||||
audit.unset_fields_except(['uuid', 'audit_type', 'state',
|
||||
'goal_uuid', 'interval', 'scope',
|
||||
'strategy_uuid', 'goal_name',
|
||||
'strategy_name', 'auto_trigger'])
|
||||
'strategy_name', 'auto_trigger',
|
||||
'next_run_time'])
|
||||
|
||||
audit.links = [link.Link.make_link('self', url,
|
||||
'audits', audit.uuid),
|
||||
@ -325,9 +329,10 @@ class Audit(base.APIBase):
|
||||
created_at=datetime.datetime.utcnow(),
|
||||
deleted_at=None,
|
||||
updated_at=datetime.datetime.utcnow(),
|
||||
interval=7200,
|
||||
interval='7200',
|
||||
scope=[],
|
||||
auto_trigger=False)
|
||||
auto_trigger=False,
|
||||
next_run_time=datetime.datetime.utcnow())
|
||||
|
||||
sample.goal_id = '7ae81bb3-dec3-4289-8d6c-da80bd8001ae'
|
||||
sample.strategy_id = '7ae81bb3-dec3-4289-8d6c-da80bd8001ff'
|
||||
|
@ -43,6 +43,28 @@ class UuidOrNameType(wtypes.UserType):
|
||||
return UuidOrNameType.validate(value)
|
||||
|
||||
|
||||
class IntervalOrCron(wtypes.UserType):
|
||||
"""A simple int value or cron syntax type"""
|
||||
|
||||
basetype = wtypes.text
|
||||
name = 'interval_or_cron'
|
||||
|
||||
@staticmethod
|
||||
def validate(value):
|
||||
if not (utils.is_int_like(value) or utils.is_cron_like(value)):
|
||||
raise exception.InvalidIntervalOrCron(name=value)
|
||||
return value
|
||||
|
||||
@staticmethod
|
||||
def frombasetype(value):
|
||||
if value is None:
|
||||
return None
|
||||
return IntervalOrCron.validate(value)
|
||||
|
||||
|
||||
interval_or_cron = IntervalOrCron()
|
||||
|
||||
|
||||
class NameType(wtypes.UserType):
|
||||
"""A simple logical name type."""
|
||||
|
||||
|
@ -202,6 +202,10 @@ class InvalidUuidOrName(Invalid):
|
||||
msg_fmt = _("Expected a logical name or uuid but received %(name)s")
|
||||
|
||||
|
||||
class InvalidIntervalOrCron(Invalid):
|
||||
msg_fmt = _("Expected an interval or cron syntax but received %(name)s")
|
||||
|
||||
|
||||
class GoalNotFound(ResourceNotFound):
|
||||
msg_fmt = _("Goal %(goal)s could not be found")
|
||||
|
||||
@ -418,6 +422,10 @@ class WildcardCharacterIsUsed(WatcherException):
|
||||
"wildcard character.")
|
||||
|
||||
|
||||
class CronFormatIsInvalid(WatcherException):
|
||||
msg_fmt = _("Provided cron is invalid: %(message)s")
|
||||
|
||||
|
||||
# Model
|
||||
|
||||
class ComputeResourceNotFound(WatcherException):
|
||||
|
@ -16,8 +16,11 @@
|
||||
|
||||
"""Utilities and helper functions."""
|
||||
|
||||
import datetime
|
||||
import re
|
||||
|
||||
from croniter import croniter
|
||||
|
||||
from jsonschema import validators
|
||||
from oslo_log import log as logging
|
||||
from oslo_utils import strutils
|
||||
@ -63,6 +66,15 @@ is_int_like = strutils.is_int_like
|
||||
strtime = timeutils.strtime
|
||||
|
||||
|
||||
def is_cron_like(value):
|
||||
"""Return True is submitted value is like cron syntax"""
|
||||
try:
|
||||
croniter(value, datetime.datetime.now())
|
||||
except Exception as e:
|
||||
raise exception.CronFormatIsInvalid(message=str(e))
|
||||
return True
|
||||
|
||||
|
||||
def safe_rstrip(value, chars=None):
|
||||
"""Removes trailing characters from a string if that does not make it empty
|
||||
|
||||
|
@ -0,0 +1,26 @@
|
||||
"""Add cron support for audit table
|
||||
|
||||
Revision ID: d098df6021e2
|
||||
Revises: 0f6042416884
|
||||
Create Date: 2017-06-08 16:21:35.746752
|
||||
|
||||
"""
|
||||
from alembic import op
|
||||
import sqlalchemy as sa
|
||||
|
||||
# revision identifiers, used by Alembic.
|
||||
revision = 'd098df6021e2'
|
||||
down_revision = '0f6042416884'
|
||||
|
||||
|
||||
def upgrade():
|
||||
op.alter_column('audits', 'interval', existing_type=sa.String(36),
|
||||
nullable=True)
|
||||
op.add_column('audits',
|
||||
sa.Column('next_run_time', sa.DateTime(), nullable=True))
|
||||
|
||||
|
||||
def downgrade():
|
||||
op.alter_column('audits', 'interval', existing_type=sa.Integer(),
|
||||
nullable=True)
|
||||
op.drop_column('audits', 'next_run_time')
|
@ -173,11 +173,12 @@ class Audit(Base):
|
||||
audit_type = Column(String(20))
|
||||
state = Column(String(20), nullable=True)
|
||||
parameters = Column(JSONEncodedDict, nullable=True)
|
||||
interval = Column(Integer, nullable=True)
|
||||
interval = Column(String(36), nullable=True)
|
||||
goal_id = Column(Integer, ForeignKey('goals.id'), nullable=False)
|
||||
strategy_id = Column(Integer, ForeignKey('strategies.id'), nullable=True)
|
||||
scope = Column(JSONEncodedList, nullable=True)
|
||||
auto_trigger = Column(Boolean, nullable=False)
|
||||
next_run_time = Column(DateTime, nullable=True)
|
||||
|
||||
goal = orm.relationship(Goal, foreign_keys=goal_id, lazy=None)
|
||||
strategy = orm.relationship(Strategy, foreign_keys=strategy_id, lazy=None)
|
||||
|
@ -19,11 +19,14 @@
|
||||
|
||||
|
||||
import datetime
|
||||
from dateutil import tz
|
||||
|
||||
from apscheduler.jobstores import memory
|
||||
from croniter import croniter
|
||||
|
||||
from watcher.common import context
|
||||
from watcher.common import scheduling
|
||||
from watcher.common import utils
|
||||
from watcher import conf
|
||||
from watcher.db.sqlalchemy import api as sq_api
|
||||
from watcher.db.sqlalchemy import job_store
|
||||
@ -81,11 +84,38 @@ class ContinuousAuditHandler(base.AuditHandler):
|
||||
plan.save()
|
||||
return solution
|
||||
|
||||
def _next_cron_time(self, audit):
|
||||
if utils.is_cron_like(audit.interval):
|
||||
return croniter(audit.interval, datetime.datetime.utcnow()
|
||||
).get_next(datetime.datetime)
|
||||
|
||||
@classmethod
|
||||
def execute_audit(cls, audit, request_context):
|
||||
self = cls()
|
||||
if not self._is_audit_inactive(audit):
|
||||
self.execute(audit, request_context)
|
||||
try:
|
||||
self.execute(audit, request_context)
|
||||
except Exception:
|
||||
raise
|
||||
finally:
|
||||
if utils.is_int_like(audit.interval):
|
||||
audit.next_run_time = (
|
||||
datetime.datetime.utcnow() +
|
||||
datetime.timedelta(seconds=int(audit.interval)))
|
||||
else:
|
||||
audit.next_run_time = self._next_cron_time(audit)
|
||||
audit.save()
|
||||
|
||||
def _add_job(self, trigger, audit, audit_context, **trigger_args):
|
||||
time_var = 'next_run_time' if trigger_args.get(
|
||||
'next_run_time') else 'run_date'
|
||||
# We should convert UTC time to local time without tzinfo
|
||||
trigger_args[time_var] = trigger_args[time_var].replace(
|
||||
tzinfo=tz.tzutc()).astimezone(tz.tzlocal()).replace(tzinfo=None)
|
||||
self.scheduler.add_job(self.execute_audit, trigger,
|
||||
args=[audit, audit_context],
|
||||
name='execute_audit',
|
||||
**trigger_args)
|
||||
|
||||
def launch_audits_periodically(self):
|
||||
audit_context = context.RequestContext(is_admin=True)
|
||||
@ -101,13 +131,34 @@ class ContinuousAuditHandler(base.AuditHandler):
|
||||
job.args for job in self.scheduler.get_jobs()
|
||||
if job.name == 'execute_audit']
|
||||
for audit in audits:
|
||||
# if audit is not presented in scheduled audits yet.
|
||||
if audit.uuid not in [arg[0].uuid for arg in scheduler_job_args]:
|
||||
self.scheduler.add_job(
|
||||
self.execute_audit, 'interval',
|
||||
args=[audit, audit_context],
|
||||
seconds=audit.interval,
|
||||
name='execute_audit',
|
||||
next_run_time=datetime.datetime.now())
|
||||
# if interval is provided with seconds
|
||||
if utils.is_int_like(audit.interval):
|
||||
# if audit has already been provided and we need
|
||||
# to restore it after shutdown
|
||||
if audit.next_run_time is not None:
|
||||
old_run_time = audit.next_run_time
|
||||
current = datetime.datetime.utcnow()
|
||||
if old_run_time < current:
|
||||
delta = datetime.timedelta(
|
||||
seconds=(int(audit.interval) - (
|
||||
current - old_run_time).seconds %
|
||||
int(audit.interval)))
|
||||
audit.next_run_time = current + delta
|
||||
next_run_time = audit.next_run_time
|
||||
# if audit is new one
|
||||
else:
|
||||
next_run_time = datetime.datetime.utcnow()
|
||||
self._add_job('interval', audit, audit_context,
|
||||
seconds=int(audit.interval),
|
||||
next_run_time=next_run_time)
|
||||
|
||||
else:
|
||||
audit.next_run_time = self._next_cron_time(audit)
|
||||
self._add_job('date', audit, audit_context,
|
||||
run_date=audit.next_run_time)
|
||||
audit.save()
|
||||
|
||||
def start(self):
|
||||
self.scheduler.add_job(
|
||||
|
@ -39,6 +39,8 @@ class TerseAuditPayload(notificationbase.NotificationPayloadBase):
|
||||
'parameters': ('audit', 'parameters'),
|
||||
'interval': ('audit', 'interval'),
|
||||
'scope': ('audit', 'scope'),
|
||||
'auto_trigger': ('audit', 'auto_trigger'),
|
||||
'next_run_time': ('audit', 'next_run_time'),
|
||||
|
||||
'created_at': ('audit', 'created_at'),
|
||||
'updated_at': ('audit', 'updated_at'),
|
||||
@ -46,17 +48,22 @@ class TerseAuditPayload(notificationbase.NotificationPayloadBase):
|
||||
}
|
||||
|
||||
# Version 1.0: Initial version
|
||||
VERSION = '1.0'
|
||||
# Version 1.1: Added 'auto_trigger' boolean field,
|
||||
# Added 'next_run_time' DateTime field,
|
||||
# 'interval' type has been changed from Integer to String
|
||||
VERSION = '1.1'
|
||||
|
||||
fields = {
|
||||
'uuid': wfields.UUIDField(),
|
||||
'audit_type': wfields.StringField(),
|
||||
'state': wfields.StringField(),
|
||||
'parameters': wfields.FlexibleDictField(nullable=True),
|
||||
'interval': wfields.IntegerField(nullable=True),
|
||||
'interval': wfields.StringField(nullable=True),
|
||||
'scope': wfields.FlexibleListOfDictField(nullable=True),
|
||||
'goal_uuid': wfields.UUIDField(),
|
||||
'strategy_uuid': wfields.UUIDField(nullable=True),
|
||||
'auto_trigger': wfields.BooleanField(),
|
||||
'next_run_time': wfields.DateTimeField(nullable=True),
|
||||
|
||||
'created_at': wfields.DateTimeField(nullable=True),
|
||||
'updated_at': wfields.DateTimeField(nullable=True),
|
||||
@ -79,6 +86,8 @@ class AuditPayload(TerseAuditPayload):
|
||||
'parameters': ('audit', 'parameters'),
|
||||
'interval': ('audit', 'interval'),
|
||||
'scope': ('audit', 'scope'),
|
||||
'auto_trigger': ('audit', 'auto_trigger'),
|
||||
'next_run_time': ('audit', 'next_run_time'),
|
||||
|
||||
'created_at': ('audit', 'created_at'),
|
||||
'updated_at': ('audit', 'updated_at'),
|
||||
@ -86,7 +95,9 @@ class AuditPayload(TerseAuditPayload):
|
||||
}
|
||||
|
||||
# Version 1.0: Initial version
|
||||
VERSION = '1.0'
|
||||
# Version 1.1: Added 'auto_trigger' field,
|
||||
# Added 'next_run_time' field
|
||||
VERSION = '1.1'
|
||||
|
||||
fields = {
|
||||
'goal': wfields.ObjectField('GoalPayload'),
|
||||
@ -119,7 +130,9 @@ class AuditStateUpdatePayload(notificationbase.NotificationPayloadBase):
|
||||
@base.WatcherObjectRegistry.register_notification
|
||||
class AuditCreatePayload(AuditPayload):
|
||||
# Version 1.0: Initial version
|
||||
VERSION = '1.0'
|
||||
# Version 1.1: Added 'auto_trigger' field,
|
||||
# Added 'next_run_time' field
|
||||
VERSION = '1.1'
|
||||
fields = {}
|
||||
|
||||
def __init__(self, audit, goal, strategy):
|
||||
@ -133,7 +146,9 @@ class AuditCreatePayload(AuditPayload):
|
||||
@base.WatcherObjectRegistry.register_notification
|
||||
class AuditUpdatePayload(AuditPayload):
|
||||
# Version 1.0: Initial version
|
||||
VERSION = '1.0'
|
||||
# Version 1.1: Added 'auto_trigger' field,
|
||||
# Added 'next_run_time' field
|
||||
VERSION = '1.1'
|
||||
fields = {
|
||||
'state_update': wfields.ObjectField('AuditStateUpdatePayload'),
|
||||
}
|
||||
@ -150,7 +165,9 @@ class AuditUpdatePayload(AuditPayload):
|
||||
@base.WatcherObjectRegistry.register_notification
|
||||
class AuditActionPayload(AuditPayload):
|
||||
# Version 1.0: Initial version
|
||||
VERSION = '1.0'
|
||||
# Version 1.1: Added 'auto_trigger' field,
|
||||
# Added 'next_run_time' field
|
||||
VERSION = '1.1'
|
||||
fields = {
|
||||
'fault': wfields.ObjectField('ExceptionPayload', nullable=True),
|
||||
}
|
||||
@ -167,7 +184,9 @@ class AuditActionPayload(AuditPayload):
|
||||
@base.WatcherObjectRegistry.register_notification
|
||||
class AuditDeletePayload(AuditPayload):
|
||||
# Version 1.0: Initial version
|
||||
VERSION = '1.0'
|
||||
# Version 1.1: Added 'auto_trigger' field,
|
||||
# Added 'next_run_time' field
|
||||
VERSION = '1.1'
|
||||
fields = {}
|
||||
|
||||
def __init__(self, audit, goal, strategy):
|
||||
|
@ -83,8 +83,10 @@ class Audit(base.WatcherPersistentObject, base.WatcherObject,
|
||||
|
||||
# Version 1.0: Initial version
|
||||
# Version 1.1: Added 'goal' and 'strategy' object field
|
||||
# Version 1.2 Added 'auto_trigger' boolean field
|
||||
VERSION = '1.2'
|
||||
# Version 1.2: Added 'auto_trigger' boolean field
|
||||
# Version 1.3: Added 'next_run_time' DateTime field,
|
||||
# 'interval' type has been changed from Integer to String
|
||||
VERSION = '1.3'
|
||||
|
||||
dbapi = db_api.get_instance()
|
||||
|
||||
@ -94,11 +96,13 @@ class Audit(base.WatcherPersistentObject, base.WatcherObject,
|
||||
'audit_type': wfields.StringField(),
|
||||
'state': wfields.StringField(),
|
||||
'parameters': wfields.FlexibleDictField(nullable=True),
|
||||
'interval': wfields.IntegerField(nullable=True),
|
||||
'interval': wfields.StringField(nullable=True),
|
||||
'scope': wfields.FlexibleListOfDictField(nullable=True),
|
||||
'goal_id': wfields.IntegerField(),
|
||||
'strategy_id': wfields.IntegerField(nullable=True),
|
||||
'auto_trigger': wfields.BooleanField(),
|
||||
'next_run_time': wfields.DateTimeField(nullable=True,
|
||||
tzinfo_aware=False),
|
||||
|
||||
'goal': wfields.ObjectField('Goal', nullable=True),
|
||||
'strategy': wfields.ObjectField('Strategy', nullable=True),
|
||||
|
@ -481,6 +481,7 @@ class TestPost(api_base.FunctionalTest):
|
||||
del audit_dict['state']
|
||||
del audit_dict['interval']
|
||||
del audit_dict['scope']
|
||||
del audit_dict['next_run_time']
|
||||
|
||||
response = self.post_json('/audits', audit_dict)
|
||||
self.assertEqual('application/json', response.content_type)
|
||||
@ -523,6 +524,7 @@ class TestPost(api_base.FunctionalTest):
|
||||
del audit_dict['state']
|
||||
del audit_dict['interval']
|
||||
del audit_dict['scope']
|
||||
del audit_dict['next_run_time']
|
||||
# Make the audit template UUID some garbage value
|
||||
audit_dict['audit_template_uuid'] = (
|
||||
'01234567-8910-1112-1314-151617181920')
|
||||
@ -545,6 +547,7 @@ class TestPost(api_base.FunctionalTest):
|
||||
del audit_dict['state']
|
||||
del audit_dict['interval']
|
||||
del audit_dict['scope']
|
||||
del audit_dict['next_run_time']
|
||||
with mock.patch.object(self.dbapi, 'create_audit',
|
||||
wraps=self.dbapi.create_audit) as cn_mock:
|
||||
response = self.post_json('/audits', audit_dict)
|
||||
@ -562,6 +565,7 @@ class TestPost(api_base.FunctionalTest):
|
||||
del audit_dict['state']
|
||||
del audit_dict['interval']
|
||||
del audit_dict['scope']
|
||||
del audit_dict['next_run_time']
|
||||
|
||||
response = self.post_json('/audits', audit_dict)
|
||||
self.assertEqual('application/json', response.content_type)
|
||||
@ -571,15 +575,16 @@ class TestPost(api_base.FunctionalTest):
|
||||
self.assertTrue(utils.is_uuid_like(response.json['uuid']))
|
||||
|
||||
@mock.patch.object(deapi.DecisionEngineAPI, 'trigger_audit')
|
||||
def test_create_continuous_audit_with_period(self, mock_trigger_audit):
|
||||
def test_create_continuous_audit_with_interval(self, mock_trigger_audit):
|
||||
mock_trigger_audit.return_value = mock.ANY
|
||||
|
||||
audit_dict = post_get_test_audit()
|
||||
del audit_dict['uuid']
|
||||
del audit_dict['state']
|
||||
del audit_dict['scope']
|
||||
del audit_dict['next_run_time']
|
||||
audit_dict['audit_type'] = objects.audit.AuditType.CONTINUOUS.value
|
||||
audit_dict['interval'] = 1200
|
||||
audit_dict['interval'] = '1200'
|
||||
|
||||
response = self.post_json('/audits', audit_dict)
|
||||
self.assertEqual('application/json', response.content_type)
|
||||
@ -589,6 +594,48 @@ class TestPost(api_base.FunctionalTest):
|
||||
self.assertEqual(audit_dict['interval'], response.json['interval'])
|
||||
self.assertTrue(utils.is_uuid_like(response.json['uuid']))
|
||||
|
||||
@mock.patch.object(deapi.DecisionEngineAPI, 'trigger_audit')
|
||||
def test_create_continuous_audit_with_cron_interval(self,
|
||||
mock_trigger_audit):
|
||||
mock_trigger_audit.return_value = mock.ANY
|
||||
|
||||
audit_dict = post_get_test_audit()
|
||||
del audit_dict['uuid']
|
||||
del audit_dict['state']
|
||||
del audit_dict['scope']
|
||||
del audit_dict['next_run_time']
|
||||
audit_dict['audit_type'] = objects.audit.AuditType.CONTINUOUS.value
|
||||
audit_dict['interval'] = '* * * * *'
|
||||
|
||||
response = self.post_json('/audits', audit_dict)
|
||||
self.assertEqual('application/json', response.content_type)
|
||||
self.assertEqual(201, response.status_int)
|
||||
self.assertEqual(objects.audit.State.PENDING,
|
||||
response.json['state'])
|
||||
self.assertEqual(audit_dict['interval'], response.json['interval'])
|
||||
self.assertTrue(utils.is_uuid_like(response.json['uuid']))
|
||||
|
||||
@mock.patch.object(deapi.DecisionEngineAPI, 'trigger_audit')
|
||||
def test_create_continuous_audit_with_wrong_interval(self,
|
||||
mock_trigger_audit):
|
||||
mock_trigger_audit.return_value = mock.ANY
|
||||
|
||||
audit_dict = post_get_test_audit()
|
||||
del audit_dict['uuid']
|
||||
del audit_dict['state']
|
||||
del audit_dict['scope']
|
||||
del audit_dict['next_run_time']
|
||||
audit_dict['audit_type'] = objects.audit.AuditType.CONTINUOUS.value
|
||||
audit_dict['interval'] = 'zxc'
|
||||
|
||||
response = self.post_json('/audits', audit_dict, expect_errors=True)
|
||||
self.assertEqual('application/json', response.content_type)
|
||||
self.assertEqual(500, response.status_int)
|
||||
expected_error_msg = ('Exactly 5 or 6 columns has to be '
|
||||
'specified for iteratorexpression.')
|
||||
self.assertTrue(response.json['error_message'])
|
||||
self.assertIn(expected_error_msg, response.json['error_message'])
|
||||
|
||||
@mock.patch.object(deapi.DecisionEngineAPI, 'trigger_audit')
|
||||
def test_create_continuous_audit_without_period(self, mock_trigger_audit):
|
||||
mock_trigger_audit.return_value = mock.ANY
|
||||
@ -599,6 +646,7 @@ class TestPost(api_base.FunctionalTest):
|
||||
audit_dict['audit_type'] = objects.audit.AuditType.CONTINUOUS.value
|
||||
del audit_dict['interval']
|
||||
del audit_dict['scope']
|
||||
del audit_dict['next_run_time']
|
||||
|
||||
response = self.post_json('/audits', audit_dict, expect_errors=True)
|
||||
self.assertEqual(400, response.status_int)
|
||||
@ -616,8 +664,8 @@ class TestPost(api_base.FunctionalTest):
|
||||
del audit_dict['uuid']
|
||||
del audit_dict['state']
|
||||
audit_dict['audit_type'] = objects.audit.AuditType.ONESHOT.value
|
||||
audit_dict['interval'] = 1200
|
||||
del audit_dict['scope']
|
||||
del audit_dict['next_run_time']
|
||||
|
||||
response = self.post_json('/audits', audit_dict, expect_errors=True)
|
||||
self.assertEqual(400, response.status_int)
|
||||
@ -634,6 +682,7 @@ class TestPost(api_base.FunctionalTest):
|
||||
del audit_dict['state']
|
||||
del audit_dict['interval']
|
||||
del audit_dict['scope']
|
||||
del audit_dict['next_run_time']
|
||||
response = self.post_json('/audits', audit_dict)
|
||||
de_mock.assert_called_once_with(mock.ANY, response.json['uuid'])
|
||||
|
||||
@ -657,6 +706,7 @@ class TestPost(api_base.FunctionalTest):
|
||||
del audit_dict['state']
|
||||
del audit_dict['interval']
|
||||
del audit_dict['scope']
|
||||
del audit_dict['next_run_time']
|
||||
|
||||
response = self.post_json('/audits', audit_dict, expect_errors=True)
|
||||
self.assertEqual('application/json', response.content_type)
|
||||
@ -678,6 +728,7 @@ class TestPost(api_base.FunctionalTest):
|
||||
del audit_dict['state']
|
||||
del audit_dict['interval']
|
||||
del audit_dict['scope']
|
||||
del audit_dict['next_run_time']
|
||||
|
||||
response = self.post_json('/audits', audit_dict, expect_errors=True)
|
||||
self.assertEqual('application/json', response.content_type)
|
||||
@ -700,7 +751,7 @@ class TestPost(api_base.FunctionalTest):
|
||||
|
||||
audit_dict['audit_template_uuid'] = audit_template['uuid']
|
||||
del_keys = ['uuid', 'goal_id', 'strategy_id', 'state', 'interval',
|
||||
'scope']
|
||||
'scope', 'next_run_time']
|
||||
for k in del_keys:
|
||||
del audit_dict[k]
|
||||
|
||||
@ -839,6 +890,7 @@ class TestAuditPolicyEnforcement(api_base.FunctionalTest):
|
||||
del audit_dict['uuid']
|
||||
del audit_dict['state']
|
||||
del audit_dict['scope']
|
||||
del audit_dict['next_run_time']
|
||||
self._common_policy_check(
|
||||
"audit:create", self.post_json, '/audits', audit_dict,
|
||||
expect_errors=True)
|
||||
|
@ -89,11 +89,12 @@ def get_test_audit(**kwargs):
|
||||
'updated_at': kwargs.get('updated_at'),
|
||||
'deleted_at': kwargs.get('deleted_at'),
|
||||
'parameters': kwargs.get('parameters', {}),
|
||||
'interval': kwargs.get('interval', 3600),
|
||||
'interval': kwargs.get('interval', '3600'),
|
||||
'goal_id': kwargs.get('goal_id', 1),
|
||||
'strategy_id': kwargs.get('strategy_id', None),
|
||||
'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')
|
||||
}
|
||||
# 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.
|
||||
|
@ -14,12 +14,15 @@
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
|
||||
import datetime
|
||||
|
||||
import mock
|
||||
from oslo_utils import uuidutils
|
||||
|
||||
from apscheduler import job
|
||||
|
||||
from watcher.applier import rpcapi
|
||||
from watcher.common import exception
|
||||
from watcher.common import scheduling
|
||||
from watcher.db.sqlalchemy import api as sq_api
|
||||
from watcher.decision_engine.audit import continuous
|
||||
@ -241,20 +244,65 @@ class TestContinuousAuditHandler(base.DbTestCase):
|
||||
@mock.patch.object(scheduling.BackgroundSchedulerService, 'add_job')
|
||||
@mock.patch.object(scheduling.BackgroundSchedulerService, 'get_jobs')
|
||||
@mock.patch.object(objects.audit.Audit, 'list')
|
||||
def test_launch_audits_periodically(self, mock_list, mock_jobs,
|
||||
m_add_job, m_engine, m_service):
|
||||
def test_launch_audits_periodically_with_interval(
|
||||
self, mock_list, mock_jobs, m_add_job, m_engine, m_service):
|
||||
audit_handler = continuous.ContinuousAuditHandler()
|
||||
mock_list.return_value = self.audits
|
||||
self.audits[0].next_run_time = (datetime.datetime.now() -
|
||||
datetime.timedelta(seconds=1800))
|
||||
mock_jobs.return_value = mock.MagicMock()
|
||||
m_engine.return_value = mock.MagicMock()
|
||||
m_add_job.return_value = audit_handler.execute_audit(
|
||||
self.audits[0], self.context)
|
||||
m_add_job.return_value = mock.MagicMock()
|
||||
|
||||
audit_handler.launch_audits_periodically()
|
||||
m_service.assert_called()
|
||||
m_engine.assert_called()
|
||||
m_add_job.assert_called()
|
||||
mock_jobs.assert_called()
|
||||
self.assertIsNotNone(self.audits[0].next_run_time)
|
||||
self.assertIsNone(self.audits[1].next_run_time)
|
||||
|
||||
@mock.patch.object(objects.service.Service, 'list')
|
||||
@mock.patch.object(sq_api, 'get_engine')
|
||||
@mock.patch.object(scheduling.BackgroundSchedulerService, 'add_job')
|
||||
@mock.patch.object(scheduling.BackgroundSchedulerService, 'get_jobs')
|
||||
@mock.patch.object(objects.audit.Audit, 'list')
|
||||
def test_launch_audits_periodically_with_cron(
|
||||
self, mock_list, mock_jobs, m_add_job, m_engine, m_service):
|
||||
audit_handler = continuous.ContinuousAuditHandler()
|
||||
mock_list.return_value = self.audits
|
||||
self.audits[0].interval = "*/5 * * * *"
|
||||
mock_jobs.return_value = mock.MagicMock()
|
||||
m_engine.return_value = mock.MagicMock()
|
||||
m_add_job.return_value = mock.MagicMock()
|
||||
|
||||
audit_handler.launch_audits_periodically()
|
||||
m_service.assert_called()
|
||||
m_engine.assert_called()
|
||||
m_add_job.assert_called()
|
||||
mock_jobs.assert_called()
|
||||
self.assertIsNotNone(self.audits[0].next_run_time)
|
||||
self.assertIsNone(self.audits[1].next_run_time)
|
||||
|
||||
@mock.patch.object(continuous.ContinuousAuditHandler, '_next_cron_time')
|
||||
@mock.patch.object(objects.service.Service, 'list')
|
||||
@mock.patch.object(sq_api, 'get_engine')
|
||||
@mock.patch.object(scheduling.BackgroundSchedulerService, 'add_job')
|
||||
@mock.patch.object(scheduling.BackgroundSchedulerService, 'get_jobs')
|
||||
@mock.patch.object(objects.audit.Audit, 'list')
|
||||
def test_launch_audits_periodically_with_invalid_cron(
|
||||
self, mock_list, mock_jobs, m_add_job, m_engine, m_service,
|
||||
mock_cron):
|
||||
audit_handler = continuous.ContinuousAuditHandler()
|
||||
mock_list.return_value = self.audits
|
||||
self.audits[0].interval = "*/5* * * *"
|
||||
mock_cron.side_effect = exception.CronFormatIsInvalid
|
||||
mock_jobs.return_value = mock.MagicMock()
|
||||
m_engine.return_value = mock.MagicMock()
|
||||
m_add_job.return_value = mock.MagicMock()
|
||||
|
||||
self.assertRaises(exception.CronFormatIsInvalid,
|
||||
audit_handler.launch_audits_periodically)
|
||||
|
||||
@mock.patch.object(objects.service.Service, 'list')
|
||||
@mock.patch.object(sq_api, 'get_engine')
|
||||
@ -273,7 +321,7 @@ class TestContinuousAuditHandler(base.DbTestCase):
|
||||
args=[mock.ANY, mock.ANY],
|
||||
seconds=3600,
|
||||
name='execute_audit',
|
||||
next_run_time=mock.ANY) for audit in self.audits]
|
||||
next_run_time=mock.ANY) for _ in self.audits]
|
||||
audit_handler.launch_audits_periodically()
|
||||
m_add_job.assert_has_calls(calls)
|
||||
|
||||
|
@ -94,6 +94,8 @@ class TestActionPlanNotification(base.DbTestCase):
|
||||
"audit": {
|
||||
"watcher_object.data": {
|
||||
"interval": None,
|
||||
"next_run_time": None,
|
||||
"auto_trigger": False,
|
||||
"parameters": {},
|
||||
"uuid": "10a47dd1-4874-4298-91cf-eff046dbdb8d",
|
||||
"strategy_uuid": None,
|
||||
@ -108,7 +110,7 @@ class TestActionPlanNotification(base.DbTestCase):
|
||||
},
|
||||
"watcher_object.name": "TerseAuditPayload",
|
||||
"watcher_object.namespace": "watcher",
|
||||
"watcher_object.version": "1.0"
|
||||
"watcher_object.version": "1.1"
|
||||
},
|
||||
"deleted_at": None,
|
||||
"state": "ONGOING",
|
||||
@ -168,6 +170,8 @@ class TestActionPlanNotification(base.DbTestCase):
|
||||
"audit": {
|
||||
"watcher_object.data": {
|
||||
"interval": None,
|
||||
"next_run_time": None,
|
||||
"auto_trigger": False,
|
||||
"parameters": {},
|
||||
"uuid": "10a47dd1-4874-4298-91cf-eff046dbdb8d",
|
||||
"strategy_uuid": None,
|
||||
@ -182,7 +186,7 @@ class TestActionPlanNotification(base.DbTestCase):
|
||||
},
|
||||
"watcher_object.name": "TerseAuditPayload",
|
||||
"watcher_object.namespace": "watcher",
|
||||
"watcher_object.version": "1.0"
|
||||
"watcher_object.version": "1.1"
|
||||
},
|
||||
"deleted_at": None,
|
||||
"state": "PENDING",
|
||||
@ -234,6 +238,8 @@ class TestActionPlanNotification(base.DbTestCase):
|
||||
"audit": {
|
||||
"watcher_object.data": {
|
||||
"interval": None,
|
||||
"next_run_time": None,
|
||||
"auto_trigger": False,
|
||||
"parameters": {},
|
||||
"uuid": "10a47dd1-4874-4298-91cf-eff046dbdb8d",
|
||||
"strategy_uuid": None,
|
||||
@ -248,7 +254,7 @@ class TestActionPlanNotification(base.DbTestCase):
|
||||
},
|
||||
"watcher_object.name": "TerseAuditPayload",
|
||||
"watcher_object.namespace": "watcher",
|
||||
"watcher_object.version": "1.0"
|
||||
"watcher_object.version": "1.1"
|
||||
},
|
||||
"deleted_at": None,
|
||||
"state": "DELETED",
|
||||
@ -287,9 +293,11 @@ class TestActionPlanNotification(base.DbTestCase):
|
||||
"audit": {
|
||||
"watcher_object.namespace": "watcher",
|
||||
"watcher_object.name": "TerseAuditPayload",
|
||||
"watcher_object.version": "1.0",
|
||||
"watcher_object.version": "1.1",
|
||||
"watcher_object.data": {
|
||||
"interval": None,
|
||||
"next_run_time": None,
|
||||
"auto_trigger": False,
|
||||
"parameters": {},
|
||||
"uuid": "10a47dd1-4874-4298-91cf-eff046dbdb8d",
|
||||
"strategy_uuid": None,
|
||||
@ -373,6 +381,8 @@ class TestActionPlanNotification(base.DbTestCase):
|
||||
"audit": {
|
||||
"watcher_object.data": {
|
||||
"interval": None,
|
||||
"next_run_time": None,
|
||||
"auto_trigger": False,
|
||||
"parameters": {},
|
||||
"uuid": "10a47dd1-4874-4298-91cf-eff046dbdb8d",
|
||||
"strategy_uuid": None,
|
||||
@ -387,7 +397,7 @@ class TestActionPlanNotification(base.DbTestCase):
|
||||
},
|
||||
"watcher_object.name": "TerseAuditPayload",
|
||||
"watcher_object.namespace": "watcher",
|
||||
"watcher_object.version": "1.0"
|
||||
"watcher_object.version": "1.1"
|
||||
},
|
||||
"global_efficacy": {},
|
||||
"state": "ONGOING",
|
||||
|
@ -69,9 +69,11 @@ class TestAuditNotification(base.DbTestCase):
|
||||
self.assertDictEqual(
|
||||
{
|
||||
"watcher_object.namespace": "watcher",
|
||||
"watcher_object.version": "1.0",
|
||||
"watcher_object.version": "1.1",
|
||||
"watcher_object.data": {
|
||||
"interval": None,
|
||||
"next_run_time": None,
|
||||
"auto_trigger": False,
|
||||
"strategy_uuid": "cb3d0b58-4415-4d90-b75b-1e96878730e3",
|
||||
"strategy": {
|
||||
"watcher_object.namespace": "watcher",
|
||||
@ -141,9 +143,11 @@ class TestAuditNotification(base.DbTestCase):
|
||||
self.assertDictEqual(
|
||||
{
|
||||
"watcher_object.namespace": "watcher",
|
||||
"watcher_object.version": "1.0",
|
||||
"watcher_object.version": "1.1",
|
||||
"watcher_object.data": {
|
||||
"interval": None,
|
||||
"next_run_time": None,
|
||||
"auto_trigger": False,
|
||||
"parameters": {},
|
||||
"uuid": "10a47dd1-4874-4298-91cf-eff046dbdb8d",
|
||||
"goal_uuid": "f7ad87ae-4298-91cf-93a0-f35a852e3652",
|
||||
@ -200,9 +204,11 @@ class TestAuditNotification(base.DbTestCase):
|
||||
self.assertDictEqual(
|
||||
{
|
||||
"watcher_object.namespace": "watcher",
|
||||
"watcher_object.version": "1.0",
|
||||
"watcher_object.version": "1.1",
|
||||
"watcher_object.data": {
|
||||
"interval": None,
|
||||
"next_run_time": None,
|
||||
"auto_trigger": False,
|
||||
"strategy_uuid": "cb3d0b58-4415-4d90-b75b-1e96878730e3",
|
||||
"strategy": {
|
||||
"watcher_object.namespace": "watcher",
|
||||
@ -263,9 +269,11 @@ class TestAuditNotification(base.DbTestCase):
|
||||
self.assertDictEqual(
|
||||
{
|
||||
"watcher_object.namespace": "watcher",
|
||||
"watcher_object.version": "1.0",
|
||||
"watcher_object.version": "1.1",
|
||||
"watcher_object.data": {
|
||||
"interval": None,
|
||||
"next_run_time": None,
|
||||
"auto_trigger": False,
|
||||
"strategy_uuid": "cb3d0b58-4415-4d90-b75b-1e96878730e3",
|
||||
"strategy": {
|
||||
"watcher_object.namespace": "watcher",
|
||||
@ -350,6 +358,8 @@ class TestAuditNotification(base.DbTestCase):
|
||||
"watcher_object.version": "1.0"
|
||||
},
|
||||
"interval": None,
|
||||
"next_run_time": None,
|
||||
"auto_trigger": False,
|
||||
"parameters": {},
|
||||
"scope": [],
|
||||
"state": "ONGOING",
|
||||
@ -374,7 +384,7 @@ class TestAuditNotification(base.DbTestCase):
|
||||
},
|
||||
"watcher_object.name": "AuditActionPayload",
|
||||
"watcher_object.namespace": "watcher",
|
||||
"watcher_object.version": "1.0"
|
||||
"watcher_object.version": "1.1"
|
||||
}
|
||||
},
|
||||
notification
|
||||
@ -434,6 +444,8 @@ class TestAuditNotification(base.DbTestCase):
|
||||
"watcher_object.version": "1.0"
|
||||
},
|
||||
"interval": None,
|
||||
"next_run_time": None,
|
||||
"auto_trigger": False,
|
||||
"parameters": {},
|
||||
"scope": [],
|
||||
"state": "ONGOING",
|
||||
@ -458,7 +470,7 @@ class TestAuditNotification(base.DbTestCase):
|
||||
},
|
||||
"watcher_object.name": "AuditActionPayload",
|
||||
"watcher_object.namespace": "watcher",
|
||||
"watcher_object.version": "1.0"
|
||||
"watcher_object.version": "1.1"
|
||||
}
|
||||
},
|
||||
notification
|
||||
|
@ -254,17 +254,17 @@ expected_notification_fingerprints = {
|
||||
'ExceptionNotification': '1.0-9b69de0724fda8310d05e18418178866',
|
||||
'ExceptionPayload': '1.0-4516ae282a55fe2fd5c754967ee6248b',
|
||||
'NotificationPublisher': '1.0-bbbc1402fb0e443a3eb227cc52b61545',
|
||||
'TerseAuditPayload': '1.0-aaf31166b8698f08d12cae98c380b8e0',
|
||||
'AuditPayload': '1.0-30c85c834648c8ca11f54fc5e084d86b',
|
||||
'TerseAuditPayload': '1.1-19b0e9224c0953366418a30ed785f267',
|
||||
'AuditPayload': '1.1-4c59e0cc5d30c42d3b842ce0332709d5',
|
||||
'AuditStateUpdatePayload': '1.0-1a1b606bf14a2c468800c2b010801ce5',
|
||||
'AuditUpdateNotification': '1.0-9b69de0724fda8310d05e18418178866',
|
||||
'AuditUpdatePayload': '1.0-d3aace28d9eb978c1ecf833e108f61f7',
|
||||
'AuditUpdatePayload': '1.1-9b1f725e736051b976571701e5cc1e55',
|
||||
'AuditCreateNotification': '1.0-9b69de0724fda8310d05e18418178866',
|
||||
'AuditCreatePayload': '1.0-30c85c834648c8ca11f54fc5e084d86b',
|
||||
'AuditCreatePayload': '1.1-4c59e0cc5d30c42d3b842ce0332709d5',
|
||||
'AuditDeleteNotification': '1.0-9b69de0724fda8310d05e18418178866',
|
||||
'AuditDeletePayload': '1.0-30c85c834648c8ca11f54fc5e084d86b',
|
||||
'AuditDeletePayload': '1.1-4c59e0cc5d30c42d3b842ce0332709d5',
|
||||
'AuditActionNotification': '1.0-9b69de0724fda8310d05e18418178866',
|
||||
'AuditActionPayload': '1.0-09f5d005f94ba9e5f6b9200170332c52',
|
||||
'AuditActionPayload': '1.1-5a43e7321495c19f98ef5663efa0a821',
|
||||
'GoalPayload': '1.0-fa1fecb8b01dd047eef808ded4d50d1a',
|
||||
'StrategyPayload': '1.0-94f01c137b083ac236ae82573c1fcfc1',
|
||||
'ActionPlanActionPayload': '1.0-d9f134708e06cf2ff2d3b8d522ac2aa8',
|
||||
|
@ -412,7 +412,7 @@ expected_object_fingerprints = {
|
||||
'Goal': '1.0-93881622db05e7b67a65ca885b4a022e',
|
||||
'Strategy': '1.1-73f164491bdd4c034f48083a51bdeb7b',
|
||||
'AuditTemplate': '1.1-b291973ffc5efa2c61b24fe34fdccc0b',
|
||||
'Audit': '1.2-910522db78b7b1cb59df614754656db4',
|
||||
'Audit': '1.3-f47ffb1ee79d8248eb991674bda565ce',
|
||||
'ActionPlan': '2.0-394f1abbf5d73d7b6675a118fe1a0284',
|
||||
'Action': '2.0-1dd4959a7e7ac30c62ef170fe08dd935',
|
||||
'EfficacyIndicator': '1.0-655b71234a82bc7478aff964639c4bb0',
|
||||
|
@ -171,7 +171,7 @@ class BaseInfraOptimTest(test.BaseTestCase):
|
||||
:param audit_template_uuid: Audit Template UUID this audit will use
|
||||
:param audit_type: Audit type (either ONESHOT or CONTINUOUS)
|
||||
:param state: Audit state (str)
|
||||
:param interval: Audit interval in seconds (int)
|
||||
:param interval: Audit interval in seconds or cron syntax (str)
|
||||
:return: A tuple with The HTTP response and its body
|
||||
"""
|
||||
resp, body = cls.client.create_audit(
|
||||
|
@ -63,7 +63,7 @@ class TestCreateUpdateDeleteAudit(base.BaseInfraOptimTest):
|
||||
audit_params = dict(
|
||||
audit_template_uuid=audit_template['uuid'],
|
||||
audit_type='CONTINUOUS',
|
||||
interval=7200,
|
||||
interval='7200',
|
||||
)
|
||||
|
||||
_, body = self.create_audit(**audit_params)
|
||||
|
Loading…
Reference in New Issue
Block a user