Merge "add start and end time for continuous audit"

This commit is contained in:
Zuul
2018-10-31 13:05:50 +00:00
committed by Gerrit Code Review
12 changed files with 185 additions and 8 deletions

View File

@@ -0,0 +1,5 @@
---
features:
- |
Add start_time and end_time fields in audits table. User can set the start
time and/or end time when creating CONTINUOUS audit.

View File

@@ -30,11 +30,13 @@ states, visit :ref:`the Audit State machine <audit_state_machine>`.
""" """
import datetime import datetime
from dateutil import tz
import pecan import pecan
from pecan import rest from pecan import rest
import wsme import wsme
from wsme import types as wtypes from wsme import types as wtypes
from wsme import utils as wutils
import wsmeext.pecan as wsme_pecan import wsmeext.pecan as wsme_pecan
from oslo_log import log from oslo_log import log
@@ -86,6 +88,10 @@ class AuditPostType(wtypes.Base):
hostname = wtypes.wsattr(wtypes.text, readonly=True, mandatory=False) hostname = wtypes.wsattr(wtypes.text, readonly=True, mandatory=False)
start_time = wsme.wsattr(datetime.datetime, mandatory=False)
end_time = wsme.wsattr(datetime.datetime, 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:
@@ -104,6 +110,12 @@ class AuditPostType(wtypes.Base):
raise exception.Invalid('Either audit_template_uuid ' raise exception.Invalid('Either audit_template_uuid '
'or goal should be provided.') 'or goal should be provided.')
if (self.audit_type == objects.audit.AuditType.ONESHOT.value and
(self.start_time not in (wtypes.Unset, None)
or self.end_time not in (wtypes.Unset, None))):
raise exception.AuditStartEndTimeNotAllowed(
audit_type=self.audit_type)
# If audit_template_uuid was provided, we will provide any # If audit_template_uuid was provided, we will provide any
# variables not included in the request, but not override # variables not included in the request, but not override
# those variables that were included. # those variables that were included.
@@ -161,7 +173,9 @@ class AuditPostType(wtypes.Base):
strategy_id=self.strategy, strategy_id=self.strategy,
interval=self.interval, interval=self.interval,
scope=self.scope, scope=self.scope,
auto_trigger=self.auto_trigger) auto_trigger=self.auto_trigger,
start_time=self.start_time,
end_time=self.end_time)
class AuditPatchType(types.JsonPatchType): class AuditPatchType(types.JsonPatchType):
@@ -322,6 +336,12 @@ class Audit(base.APIBase):
hostname = wsme.wsattr(wtypes.text, mandatory=False) hostname = wsme.wsattr(wtypes.text, mandatory=False)
"""Hostname the audit is running on""" """Hostname the audit is running on"""
start_time = wsme.wsattr(datetime.datetime, mandatory=False)
"""The start time for continuous audit launch"""
end_time = wsme.wsattr(datetime.datetime, mandatory=False)
"""The end time that stopping continuous audit"""
def __init__(self, **kwargs): def __init__(self, **kwargs):
self.fields = [] self.fields = []
fields = list(objects.Audit.fields) fields = list(objects.Audit.fields)
@@ -382,7 +402,9 @@ class Audit(base.APIBase):
interval='7200', interval='7200',
scope=[], scope=[],
auto_trigger=False, auto_trigger=False,
next_run_time=datetime.datetime.utcnow()) next_run_time=datetime.datetime.utcnow(),
start_time=datetime.datetime.utcnow(),
end_time=datetime.datetime.utcnow())
sample.goal_id = '7ae81bb3-dec3-4289-8d6c-da80bd8001ae' sample.goal_id = '7ae81bb3-dec3-4289-8d6c-da80bd8001ae'
sample.strategy_id = '7ae81bb3-dec3-4289-8d6c-da80bd8001ff' sample.strategy_id = '7ae81bb3-dec3-4289-8d6c-da80bd8001ff'
@@ -584,6 +606,17 @@ class AuditsController(rest.RestController):
'parameter spec in predefined strategy')) 'parameter spec in predefined strategy'))
audit_dict = audit.as_dict() audit_dict = audit.as_dict()
# convert local time to UTC time
start_time_value = audit_dict.get('start_time')
end_time_value = audit_dict.get('end_time')
if start_time_value:
audit_dict['start_time'] = start_time_value.replace(
tzinfo=tz.tzlocal()).astimezone(
tz.tzutc()).replace(tzinfo=None)
if end_time_value:
audit_dict['end_time'] = end_time_value.replace(
tzinfo=tz.tzlocal()).astimezone(
tz.tzutc()).replace(tzinfo=None)
new_audit = objects.Audit(context, **audit_dict) new_audit = objects.Audit(context, **audit_dict)
new_audit.create() new_audit.create()
@@ -628,6 +661,16 @@ class AuditsController(rest.RestController):
reason=error_message % dict( reason=error_message % dict(
initial_state=initial_state, new_state=new_state)) initial_state=initial_state, new_state=new_state))
patch_path = api_utils.get_patch_key(patch, 'path')
if patch_path in ('start_time', 'end_time'):
patch_value = api_utils.get_patch_value(patch, patch_path)
# convert string format to UTC time
new_patch_value = wutils.parse_isodatetime(
patch_value).replace(
tzinfo=tz.tzlocal()).astimezone(
tz.tzutc()).replace(tzinfo=None)
api_utils.set_patch_value(patch, patch_path, new_patch_value)
audit = Audit(**api_utils.apply_jsonpatch(audit_dict, patch)) audit = Audit(**api_utils.apply_jsonpatch(audit_dict, patch))
except api_utils.JSONPATCH_EXCEPTIONS as e: except api_utils.JSONPATCH_EXCEPTIONS as e:
raise exception.PatchError(patch=patch, reason=e) raise exception.PatchError(patch=patch, reason=e)

View File

@@ -101,6 +101,18 @@ def get_patch_value(patch, key):
return p['value'] return p['value']
def set_patch_value(patch, key, value):
for p in patch:
if p['op'] == 'replace' and p['path'] == '/%s' % key:
p['value'] = value
def get_patch_key(patch, key):
for p in patch:
if p['op'] == 'replace' and key in p.keys():
return p[key][1:]
def check_audit_state_transition(patch, initial): def check_audit_state_transition(patch, initial):
is_transition_valid = True is_transition_valid = True
state_value = get_patch_value(patch, "state") state_value = get_patch_value(patch, "state")

View File

@@ -260,6 +260,11 @@ class AuditIntervalNotAllowed(Invalid):
msg_fmt = _("Interval of audit must not be set for %(audit_type)s.") msg_fmt = _("Interval of audit must not be set for %(audit_type)s.")
class AuditStartEndTimeNotAllowed(Invalid):
msg_fmt = _("Start or End time of audit must not be set for "
"%(audit_type)s.")
class AuditReferenced(Invalid): class AuditReferenced(Invalid):
msg_fmt = _("Audit %(audit)s is referenced by one or multiple action " msg_fmt = _("Audit %(audit)s is referenced by one or multiple action "
"plans") "plans")

View File

@@ -0,0 +1,24 @@
"""add_start_end_time
Revision ID: 4b16194c56bc
Revises: 52804f2498c4
Create Date: 2018-03-23 00:36:29.031259
"""
# revision identifiers, used by Alembic.
revision = '4b16194c56bc'
down_revision = '52804f2498c4'
from alembic import op
import sqlalchemy as sa
def upgrade():
op.add_column('audits', sa.Column('start_time', sa.DateTime(), nullable=True))
op.add_column('audits', sa.Column('end_time', sa.DateTime(), nullable=True))
def downgrade():
op.drop_column('audits', 'start_time')
op.drop_column('audits', 'end_time')

View File

@@ -184,6 +184,8 @@ class Audit(Base):
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) hostname = Column(String(255), nullable=True)
start_time = Column(DateTime, nullable=True)
end_time = Column(DateTime, 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)

View File

@@ -58,9 +58,10 @@ class ContinuousAuditHandler(base.AuditHandler):
def _is_audit_inactive(self, audit): def _is_audit_inactive(self, audit):
audit = objects.Audit.get_by_uuid( audit = objects.Audit.get_by_uuid(
self.context_show_deleted, audit.uuid) self.context_show_deleted, audit.uuid, eager=True)
if (objects.audit.AuditStateTransitionManager().is_inactive(audit) or if (objects.audit.AuditStateTransitionManager().is_inactive(audit) or
audit.hostname != CONF.host): (audit.hostname != CONF.host) or
(self.check_audit_expired(audit))):
# if audit isn't in active states, audit's job must be removed to # if audit isn't in active states, audit's job must be removed to
# prevent using of inactive audit in future. # prevent using of inactive audit in future.
jobs = [job for job in self.scheduler.get_jobs() jobs = [job for job in self.scheduler.get_jobs()
@@ -119,13 +120,26 @@ class ContinuousAuditHandler(base.AuditHandler):
name='execute_audit', name='execute_audit',
**trigger_args) **trigger_args)
def check_audit_expired(self, audit):
current = datetime.datetime.utcnow()
# Note: if audit still didn't get into the timeframe,
# skip it
if audit.start_time and audit.start_time > current:
return True
if audit.end_time and audit.end_time < current:
if audit.state != objects.audit.State.SUCCEEDED:
audit.state = objects.audit.State.SUCCEEDED
audit.save()
return True
return False
def launch_audits_periodically(self): def launch_audits_periodically(self):
audit_context = context.RequestContext(is_admin=True) audit_context = context.RequestContext(is_admin=True)
audit_filters = { audit_filters = {
'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),
} }
audit_filters['hostname'] = None audit_filters['hostname'] = None
unscheduled_audits = objects.Audit.list( unscheduled_audits = objects.Audit.list(
@@ -152,6 +166,8 @@ class ContinuousAuditHandler(base.AuditHandler):
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: for audit in audits:
if self.check_audit_expired(audit):
continue
existing_job = scheduler_jobs.get(audit.uuid, None) existing_job = scheduler_jobs.get(audit.uuid, None)
# if audit is not presented in scheduled audits yet, # if audit is not presented in scheduled audits yet,
# just add a new audit job. # just add a new audit job.

View File

@@ -88,7 +88,8 @@ class Audit(base.WatcherPersistentObject, base.WatcherObject,
# '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.5: Added 'hostname' field # Version 1.5: Added 'hostname' field
VERSION = '1.5' # Version 1.6: Added 'start_time' and 'end_time' DateTime fields
VERSION = '1.6'
dbapi = db_api.get_instance() dbapi = db_api.get_instance()
@@ -107,6 +108,8 @@ class Audit(base.WatcherPersistentObject, base.WatcherObject,
'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), 'hostname': wfields.StringField(nullable=True),
'start_time': wfields.DateTimeField(nullable=True, tzinfo_aware=False),
'end_time': wfields.DateTimeField(nullable=True, tzinfo_aware=False),
'goal': wfields.ObjectField('Goal', nullable=True), 'goal': wfields.ObjectField('Goal', nullable=True),
'strategy': wfields.ObjectField('Strategy', nullable=True), 'strategy': wfields.ObjectField('Strategy', nullable=True),

View File

@@ -11,6 +11,7 @@
# limitations under the License. # limitations under the License.
import datetime import datetime
from dateutil import tz
import itertools import itertools
import mock import mock
@@ -883,6 +884,41 @@ class TestPost(api_base.FunctionalTest):
self.assertEqual(201, response.status_int) self.assertEqual(201, response.status_int)
self.assertNotEqual(long_name, response.json['name']) self.assertNotEqual(long_name, response.json['name'])
@mock.patch.object(deapi.DecisionEngineAPI, 'trigger_audit')
def test_create_continuous_audit_with_start_end_time(
self, mock_trigger_audit):
mock_trigger_audit.return_value = mock.ANY
start_time = datetime.datetime(2018, 3, 1, 0, 0)
end_time = datetime.datetime(2018, 4, 1, 0, 0)
audit_dict = post_get_test_audit(
params_to_exclude=['uuid', 'state', 'scope',
'next_run_time', 'hostname', 'goal']
)
audit_dict['audit_type'] = objects.audit.AuditType.CONTINUOUS.value
audit_dict['interval'] = '1200'
audit_dict['start_time'] = str(start_time)
audit_dict['end_time'] = str(end_time)
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']))
return_start_time = timeutils.parse_isotime(
response.json['start_time'])
return_end_time = timeutils.parse_isotime(
response.json['end_time'])
iso_start_time = start_time.replace(
tzinfo=tz.tzlocal()).astimezone(tz.tzutc())
iso_end_time = end_time.replace(
tzinfo=tz.tzlocal()).astimezone(tz.tzutc())
self.assertEqual(iso_start_time, return_start_time)
self.assertEqual(iso_end_time, return_end_time)
class TestDelete(api_base.FunctionalTest): class TestDelete(api_base.FunctionalTest):

View File

@@ -97,6 +97,9 @@ def get_test_audit(**kwargs):
'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'), 'hostname': kwargs.get('hostname', 'host_1'),
'start_time': kwargs.get('start_time'),
'end_time': kwargs.get('end_time')
} }
# 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.

View File

@@ -468,3 +468,31 @@ class TestContinuousAuditHandler(base.DbTestCase):
self.assertTrue(is_inactive) self.assertTrue(is_inactive)
is_inactive = audit_handler._is_audit_inactive(self.audits[0]) is_inactive = audit_handler._is_audit_inactive(self.audits[0])
self.assertFalse(is_inactive) self.assertFalse(is_inactive)
def test_check_audit_expired(self):
current = datetime.datetime.utcnow()
# start_time and end_time are None
audit_handler = continuous.ContinuousAuditHandler()
result = audit_handler.check_audit_expired(self.audits[0])
self.assertFalse(result)
self.assertIsNone(self.audits[0].start_time)
self.assertIsNone(self.audits[0].end_time)
# current time < start_time and end_time is None
self.audits[0].start_time = current+datetime.timedelta(days=1)
result = audit_handler.check_audit_expired(self.audits[0])
self.assertTrue(result)
self.assertIsNone(self.audits[0].end_time)
# current time is between start_time and end_time
self.audits[0].start_time = current-datetime.timedelta(days=1)
self.audits[0].end_time = current+datetime.timedelta(days=1)
result = audit_handler.check_audit_expired(self.audits[0])
self.assertFalse(result)
# current time > end_time
self.audits[0].end_time = current-datetime.timedelta(days=1)
result = audit_handler.check_audit_expired(self.audits[0])
self.assertTrue(result)
self.assertEqual(objects.audit.State.SUCCEEDED, self.audits[0].state)

View File

@@ -412,7 +412,7 @@ 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.5-e4229dee89e669d1aff0805f5c665bee', 'Audit': '1.6-fc4abd6f133a8b419e42e05729ed0f8b',
'ActionPlan': '2.2-3331270cb3666c93408934826d03c08d', 'ActionPlan': '2.2-3331270cb3666c93408934826d03c08d',
'Action': '2.0-1dd4959a7e7ac30c62ef170fe08dd935', 'Action': '2.0-1dd4959a7e7ac30c62ef170fe08dd935',
'EfficacyIndicator': '1.0-655b71234a82bc7478aff964639c4bb0', 'EfficacyIndicator': '1.0-655b71234a82bc7478aff964639c4bb0',