From bc49c51e7da3517a665b038ccb9b1e1797c9c8dc Mon Sep 17 00:00:00 2001 From: Conrado Costa Date: Thu, 18 Sep 2025 08:24:32 -0400 Subject: [PATCH 1/2] generalize sla framework for other metrics --- apps/sla/README.md | 2 +- apps/sla/exceptions.py | 8 +- apps/sla/framework.py | 21 -- .../0005_replace_sla_with_temporalpolicy.py | 33 ++ apps/sla/migrations/0006_delete_slapolicy.py | 19 ++ apps/sla/migrations/0007_create_slopolicy.py | 29 ++ apps/sla/migrations/0008_create_slapolicy.py | 29 ++ apps/sla/models.py | 287 +++++++++++------ apps/sla/tests/conftest.py | 5 +- apps/sla/tests/test_framework.py | 214 ++++++------ apps/sla/tests/test_models.py | 304 +++++++++--------- apps/trackers/bugzilla/query.py | 10 +- apps/trackers/jira/query.py | 56 +++- apps/trackers/tests/conftest.py | 6 +- apps/trackers/tests/test_bugzilla.py | 9 +- apps/trackers/tests/test_common.py | 125 ++++++- apps/trackers/tests/test_jira.py | 98 ++---- collectors/ps_constants/core.py | 23 +- collectors/ps_constants/tasks.py | 10 + conftest.py | 31 ++ docs/CHANGELOG.md | 2 + .../management/commands/sync_ps_constants.py | 2 + osidb/serializer.py | 2 +- osidb/signals.py | 2 +- 24 files changed, 836 insertions(+), 491 deletions(-) delete mode 100644 apps/sla/framework.py create mode 100644 apps/sla/migrations/0005_replace_sla_with_temporalpolicy.py create mode 100644 apps/sla/migrations/0006_delete_slapolicy.py create mode 100644 apps/sla/migrations/0007_create_slopolicy.py create mode 100644 apps/sla/migrations/0008_create_slapolicy.py diff --git a/apps/sla/README.md b/apps/sla/README.md index 2735bfa78..24e258da4 100644 --- a/apps/sla/README.md +++ b/apps/sla/README.md @@ -1,3 +1,3 @@ # SLA -Application module for tracker SLA deadline computation. +Application module for tracker temporal metrics (SLA/SLO deadlines) computation. diff --git a/apps/sla/exceptions.py b/apps/sla/exceptions.py index cf205965a..707200e3a 100644 --- a/apps/sla/exceptions.py +++ b/apps/sla/exceptions.py @@ -3,13 +3,13 @@ """ -class SLAException(Exception): +class TemporalPolicyException(Exception): """ - base exception class for SLA specific exceptions + base exception class for TemporalPolicy specific exceptions """ -class SLAExecutionError(SLAException): +class TemporalPolicyExecutionError(TemporalPolicyException): """ - exception class for SLA execution errors + exception class for TemporalPolicy execution errors """ diff --git a/apps/sla/framework.py b/apps/sla/framework.py deleted file mode 100644 index 42812cd68..000000000 --- a/apps/sla/framework.py +++ /dev/null @@ -1,21 +0,0 @@ -""" -SLA Framework -""" - -from django.db.models import Model - -from .models import SLAContext, SLAPolicy - - -def sla_classify(instance: Model) -> SLAContext: - """ - Classifies the instance into the proper SLA context - with the proper SLA instance assigned which is the one - resulting in the earliest SLA end under the given context. - - Returns empty SLA context if the instance is not bound by SLA. - """ - policies = SLAPolicy.objects.all() - if not policies.exists(): - return SLAContext() - return min(policy.context(instance) for policy in policies) diff --git a/apps/sla/migrations/0005_replace_sla_with_temporalpolicy.py b/apps/sla/migrations/0005_replace_sla_with_temporalpolicy.py new file mode 100644 index 000000000..a3a718e9c --- /dev/null +++ b/apps/sla/migrations/0005_replace_sla_with_temporalpolicy.py @@ -0,0 +1,33 @@ +# Generated by Django 4.2.22 on 2025-10-01 13:41 + +from django.db import migrations, models +import django.db.models.deletion + + +class Migration(migrations.Migration): + + dependencies = [ + ('sla', '0004_make_sla_nullable'), + ] + + operations = [ + migrations.DeleteModel( + name='SLA', + ), + migrations.CreateModel( + name='TemporalPolicy', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('duration', models.IntegerField()), + ('duration_type', models.CharField(choices=[('Business Days', 'Business Days'), ('Calendar Days', 'Calendar Days')], max_length=20)), + ('ending', models.CharField(choices=[('any day', 'Any Day'), ('no week ending', 'No Week Ending')], default='any day', max_length=20)), + ('start_criteria', models.CharField(choices=[('Earliest', 'Earliest'), ('Latest', 'Latest')], max_length=20)), + ('start_dates', models.JSONField(default=dict)), + ], + ), + migrations.AlterField( + model_name='slapolicy', + name='sla', + field=models.ForeignKey(null=True, on_delete=django.db.models.deletion.CASCADE, related_name='sla_policies', to='sla.temporalpolicy'), + ), + ] diff --git a/apps/sla/migrations/0006_delete_slapolicy.py b/apps/sla/migrations/0006_delete_slapolicy.py new file mode 100644 index 000000000..e408cc599 --- /dev/null +++ b/apps/sla/migrations/0006_delete_slapolicy.py @@ -0,0 +1,19 @@ +# Needed as django migration renaming operation does not +# changes database constraints name creating conflicts. +# Also this is safe since we recreate policies every collection. + +from django.db import migrations, models +import django.db.models.deletion + + +class Migration(migrations.Migration): + + dependencies = [ + ('sla', '0005_replace_sla_with_temporalpolicy'), + ] + + operations = [ + migrations.DeleteModel( + name='SLAPolicy', + ), + ] diff --git a/apps/sla/migrations/0007_create_slopolicy.py b/apps/sla/migrations/0007_create_slopolicy.py new file mode 100644 index 000000000..29dd4f7ac --- /dev/null +++ b/apps/sla/migrations/0007_create_slopolicy.py @@ -0,0 +1,29 @@ +# Generated by Django 4.2.22 on 2025-10-01 14:10 + +from django.db import migrations, models +import django.db.models.deletion + + +class Migration(migrations.Migration): + + dependencies = [ + ('sla', '0006_delete_slapolicy'), + ] + + operations = [ + migrations.CreateModel( + name='SLOPolicy', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('name', models.CharField(max_length=100, unique=True)), + ('description', models.TextField()), + ('condition_descriptions', models.JSONField(default=dict)), + ('order', models.IntegerField(unique=True)), + ('slo', models.ForeignKey(null=True, on_delete=django.db.models.deletion.CASCADE, related_name='slo_policies', to='sla.temporalpolicy')), + ], + options={ + 'ordering': ['order'], + 'abstract': False, + }, + ), + ] diff --git a/apps/sla/migrations/0008_create_slapolicy.py b/apps/sla/migrations/0008_create_slapolicy.py new file mode 100644 index 000000000..b23973af1 --- /dev/null +++ b/apps/sla/migrations/0008_create_slapolicy.py @@ -0,0 +1,29 @@ +# Generated by Django 4.2.22 on 2025-10-01 14:38 + +from django.db import migrations, models +import django.db.models.deletion + + +class Migration(migrations.Migration): + + dependencies = [ + ('sla', '0007_create_slopolicy'), + ] + + operations = [ + migrations.CreateModel( + name='SLAPolicy', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('name', models.CharField(max_length=100, unique=True)), + ('description', models.TextField()), + ('condition_descriptions', models.JSONField(default=dict)), + ('order', models.IntegerField(unique=True)), + ('sla', models.ForeignKey(null=True, on_delete=django.db.models.deletion.CASCADE, related_name='sla_policies', to='sla.temporalpolicy')), + ], + options={ + 'ordering': ['order'], + 'abstract': False, + }, + ), + ] diff --git a/apps/sla/models.py b/apps/sla/models.py index 1435deeb4..6d614481a 100644 --- a/apps/sla/models.py +++ b/apps/sla/models.py @@ -2,6 +2,7 @@ SLA policy model definitions """ +import abc from functools import cached_property from django.db import models @@ -9,13 +10,13 @@ from apps.workflows.models import Check from osidb.models import Affect, Flaw, PsUpdateStream, Tracker -from .exceptions import SLAExecutionError +from .exceptions import TemporalPolicyExecutionError from .time import add_business_days, add_days, skip_week_ending -class SLA(models.Model): +class TemporalPolicy(models.Model): """ - SLA definition and computation model + Temporal policy definition and computation model """ class DurationTypes(models.TextChoices): @@ -58,7 +59,7 @@ class StartCriteria(models.TextChoices): start_dates = models.JSONField(default=dict) @classmethod - def create_from_description(cls, sla_desc): + def create_from_description(cls, desc): def parse_date(date_desc): """ translate human-readable date description into the attribute name @@ -67,8 +68,8 @@ def parse_date(date_desc): def parse_type(type_desc): """ - translate human-readable SLA type description - into the actual SLA type and its optional ending + translate human-readable description into the actual + TemporalPolicy type and its optional ending """ ending = cls.EndingTypes.ANY_DAY for ending_type, _ in cls.EndingTypes.choices: @@ -79,13 +80,13 @@ def parse_type(type_desc): return type_desc, ending - if sla_desc is None: + if desc is None: return None - duration = int(sla_desc["duration"]) - sla_type, ending = parse_type(sla_desc["type"]) + duration = int(desc["duration"]) + duration_type, ending = parse_type(desc["type"]) - start_desc = sla_desc["start"] + start_desc = desc["start"] if isinstance(start_desc, str): start_desc = {"latest": [start_desc]} @@ -100,31 +101,27 @@ def parse_type(type_desc): start_dates = {} for date_source, date_desc_list in date_source_desc.items(): if date_source not in cls.VALID_DATE_SOURCES: - raise SLAExecutionError( - f"SLA contains an invalid start date source. Valid sources: {', '.join(cls.VALID_DATE_SOURCES)}" + raise TemporalPolicyExecutionError( + "Policy contains an invalid start date source. " + f"Valid sources: {', '.join(cls.VALID_DATE_SOURCES)}" ) - start_dates[date_source] = [ - parse_date(date_desc) for date_desc in date_desc_list - ] + start_dates[date_source] = [parse_date(d) for d in date_desc_list] - sla = SLA( + return TemporalPolicy( duration=duration, - duration_type=sla_type, + duration_type=duration_type, ending=ending, start_criteria=start_criteria, start_dates=start_dates, ) - return sla - - def start(self, sla_context): + def start(self, context): """ - compute SLA start moment for the given instance + compute start moment for the given instance """ - # Populate with the actual dates start_dates = [] for model, dates in self.start_dates.items(): - instance = sla_context.get(model, None) + instance = context.get(model, None) start_dates += [ getattr(instance, date) for date in dates if instance is not None ] @@ -134,15 +131,12 @@ def start(self, sla_context): return self.get_start(start_dates) - def end(self, sla_context): + def end(self, context): """ - compute SLA end moment for the given instance + compute end moment for the given instance """ return self.SET_ENDING[self.ending]( - self.add_days( - self.start(sla_context), - self.duration, - ) + self.add_days(self.start(context), self.duration) ) @property @@ -154,9 +148,9 @@ def add_days(self): return self.ADD_DAYS[self.duration_type] -class SLAContext(dict): +class TemporalContext(dict): """ - SLA context holder + temporal policy context holder """ def __init__(self, **kwargs): @@ -164,30 +158,30 @@ def __init__(self, **kwargs): initialize the context based on given entities keyword arguments should comply with ENTITY2CLASS - mapping defined within the SLAPolicy class + mapping defined within the TemporalPolicy class """ for name, obj in kwargs.items(): self[name] = obj - # empty initial SLA - self.sla = None + # empty initial policy + self.policy = None # this flag determines if this should take priority over other - # SLAs as it's used to exclude certain trackers from SLA + # policies as it's used to exclude certain trackers from SLA/SLO self.is_exclusion = False def __eq__(self, other): """ - empty SLA contexts are not equal + empty policies contexts are not equal otherwise compare the end dates """ - if self.sla is None or other.sla is None: + if self.policy is None or other.policy is None: return False return self.end == other.end def __lt__(self, other): """ - empty SLA context is greater, - exclusion SLA is smaller, + empty policy context is greater, + exclusion policy is smaller, otherwise compare the end dates """ # Exclusion takes priority @@ -195,51 +189,55 @@ def __lt__(self, other): return True if other.is_exclusion: return False - # SLAs that didn't match but are not exclusion SLAs - if self.sla is None: + # Policies that didn't match but are not exclusion policy + if self.policy is None: return False - if other.sla is None: + if other.policy is None: return True return self.end < other.end @property def start(self): """ - compute SLA start for the given instance + compute policy start for the given instance - returns None if there is no SLA policy - assigned possibly meaning that this SLA - context is accepted by no SLA policy + returns None if there is no policy + assigned possibly meaning that this + context is accepted by no policy """ - if self.sla is None: + if self.policy is None: return None - return self.sla.start(self) + return self.policy.start(self) @property def end(self): """ - compute SLA end for the given instance + compute policy end for the given instance - returns None if there is no SLA policy - assigned possibly meaning that this SLA - context is accepted by no SLA policy + returns None if there is no policy + assigned possibly meaning that this + context is accepted by no policy """ - if self.sla is None: + if self.policy is None: return None - return self.sla.end(self) + return self.policy.end(self) -class SLAPolicy(models.Model): +class AbstractPolicyMeta(abc.ABCMeta, type(models.Model)): + pass + + +class AbstractPolicy(models.Model, metaclass=AbstractPolicyMeta): """ - SLA policy model + Generic temporal policy has name and description has conditions which is a list of checks - has SLA definition + has policy definition - provides SLA start and end computation + provides policy start and end computation """ ENTITY2CLASS = { @@ -250,40 +248,43 @@ class SLAPolicy(models.Model): name = models.CharField(max_length=100, unique=True) description = models.TextField() - sla = models.ForeignKey( - SLA, on_delete=models.CASCADE, null=True, related_name="policies" - ) condition_descriptions = models.JSONField(default=dict) order = models.IntegerField(unique=True) class Meta: - # Order of SLA is important, so by default retrieve them using the order field + # Order of the policy is important, so by default retrieve them using the order field ordering = ["order"] + abstract = True def __str__(self): return self.name @classmethod - def create_from_description(self, policy_desc, order=None): + @abc.abstractmethod + def create_from_description(cls, policy_desc, order=None): """Creates an SLA policy from a YAML description.""" - name = policy_desc["name"] - description = policy_desc["description"] - sla = SLA.create_from_description(policy_desc["sla"]) - if sla is not None: - sla.save() + pass - if order is None: - # Order is implied by the number of already existing SLA policies - order = SLAPolicy.objects.count() + @abc.abstractmethod + def context(self, instance) -> TemporalContext: + """ + find the right context as there may be multiple ones + which is the one resulting in the earliest deadline + """ + pass - policy = SLAPolicy( - name=name, - description=description, - condition_descriptions=policy_desc["conditions"], - sla=sla, - order=order, - ) - return policy + @classmethod + def classify(cls, instance: models.Model): + """ + Evaluate all policies of this concrete subclass against the instance + and return the TemporalContext that yields the earliest end. + Returns an empty TemporalContext if there are no policies. + """ + + policies = cls.objects.all() + if not policies.exists(): + return TemporalContext() + return min(policy.context(instance) for policy in policies) @cached_property def conditions(self): @@ -296,23 +297,53 @@ def conditions(self): ] return conditions - def accepts(self, sla_context): + def accepts(self, context): """ - accepts the SLA context if it contains all the entities required - by the SLA policy and each of them meets all the defined conditions + accepts the context if it contains all the entities required + by the policy and each of them meets all the defined conditions """ for entity, conditions in self.conditions.items(): - if entity not in sla_context: + if entity not in context: return False - if not all(condition(sla_context[entity]) for condition in conditions): + if not all(condition(context[entity]) for condition in conditions): return False else: # all conditions were met - # SLA context is accepted + # context is accepted return True + +class SLAPolicy(AbstractPolicy): + """ + SLA policy model responsible for interpreting description fields and context + """ + + sla = models.ForeignKey( + TemporalPolicy, on_delete=models.CASCADE, null=True, related_name="sla_policies" + ) + + @classmethod + def create_from_description(cls, policy_desc, order=None): + """Creates an SLA policy from a YAML description.""" + name = policy_desc["name"] + description = policy_desc["description"] + sla = TemporalPolicy.create_from_description(policy_desc["sla"]) + if sla is not None: + sla.save() + + if order is None: + order = SLAPolicy.objects.count() + + return SLAPolicy( + name=name, + description=description, + condition_descriptions=policy_desc["conditions"], + sla=sla, + order=order, + ) + def context(self, instance): """ find the right SLA context as there may be multiple ones @@ -320,11 +351,13 @@ def context(self, instance): """ # for now we only support Tracker SLAs if not isinstance(instance, Tracker): - raise SLAExecutionError(f"Unsupported SLA instance type: {type(instance)}") + raise TemporalPolicyExecutionError( + f"Unsupported SLA instance type: {type(instance)}" + ) ps_update_stream = PsUpdateStream.objects.get(name=instance.ps_update_stream) if not ps_update_stream.rhsa_sla_applicable: - return SLAContext() + return TemporalContext() # computing the SLA is not simple as we have to consider multi-flaw trackers where # the SLA start must be computed for the flaw which results in the earlist SLA end @@ -334,7 +367,7 @@ def context(self, instance): # incomplete data from the tracker which may be being saved affect = Affect.objects.get(uuid=affect.uuid) sla_contexts.append( - SLAContext(affect=affect, flaw=affect.flaw, tracker=instance) + TemporalContext(affect=affect, flaw=affect.flaw, tracker=instance) ) # filter out the SLA contexts not accepted by this SLA policy @@ -342,11 +375,11 @@ def context(self, instance): if not sla_contexts: # return an empty context # if none is accepted - return SLAContext() + return TemporalContext() # assign SLA policies for context in sla_contexts: - context.sla = self.sla + context.policy = self.sla if self.sla is None: # Exclusion SLA is defined as null in the policy context.is_exclusion = True @@ -354,3 +387,77 @@ def context(self, instance): # return the context resulting # in the earliest deadline return min(sla_contexts) + + +class SLOPolicy(AbstractPolicy): + """ + SLO policy model responsible for interpreting description fields and context + """ + + slo = models.ForeignKey( + TemporalPolicy, on_delete=models.CASCADE, null=True, related_name="slo_policies" + ) + + @classmethod + def create_from_description(cls, policy_desc, order=None): + """Creates an SLO policy from a YAML description.""" + name = policy_desc["name"] + description = policy_desc["description"] + slo = TemporalPolicy.create_from_description(policy_desc["slo"]) + if slo is not None: + slo.save() + + if order is None: + order = SLOPolicy.objects.count() + + return SLOPolicy( + name=name, + description=description, + condition_descriptions=policy_desc["conditions"], + slo=slo, + order=order, + ) + + def context(self, instance): + """ + find the right SLO context as there may be multiple ones + which is the one resulting in the earliest deadline + """ + # for now we only support Tracker SLOs + if not isinstance(instance, Tracker): + raise TemporalPolicyExecutionError( + f"Unsupported SLO instance type: {type(instance)}" + ) + + ps_update_stream = PsUpdateStream.objects.get(name=instance.ps_update_stream) + if not ps_update_stream.rhsa_sla_applicable: + return TemporalContext() + + # computing the SLO is not simple as we have to consider multi-flaw trackers where + # the SLO start must be computed for the flaw which results in the earlist SLO end + slo_contexts = [] + for affect in instance.affects.all(): + # Make sure we are getting the latest data from the database and not the possibly + # incomplete data from the tracker which may be being saved + affect = Affect.objects.get(uuid=affect.uuid) + slo_contexts.append( + TemporalContext(affect=affect, flaw=affect.flaw, tracker=instance) + ) + + # filter out the SLO contexts not accepted by this SLO policy + slo_contexts = [context for context in slo_contexts if self.accepts(context)] + if not slo_contexts: + # return an empty context + # if none is accepted + return TemporalContext() + + # assign SLO policies + for context in slo_contexts: + context.policy = self.slo + if self.slo is None: + # Exclusion SLO is defined as null in the policy + context.is_exclusion = True + + # return the context resulting + # in the earliest deadline + return min(slo_contexts) diff --git a/apps/sla/tests/conftest.py b/apps/sla/tests/conftest.py index bd23ad002..cff6c1faa 100644 --- a/apps/sla/tests/conftest.py +++ b/apps/sla/tests/conftest.py @@ -1,6 +1,6 @@ import pytest -from apps.sla.models import SLAPolicy +from apps.sla.models import SLAPolicy, SLOPolicy @pytest.fixture(autouse=True) @@ -11,7 +11,7 @@ def enable_db_access_for_all_tests(db): @pytest.fixture(autouse=True) def clean_policies(): """ - Clean SLA policies before and after every test + Clean SLA/SLO policies before and after every test * before so it is not mixed with some leftovers * after so we do not leave any leftovers @@ -20,3 +20,4 @@ def clean_policies(): when run in batch than when run alone so better to be safe then sorry """ SLAPolicy.objects.all().delete() + SLOPolicy.objects.all().delete() diff --git a/apps/sla/tests/test_framework.py b/apps/sla/tests/test_framework.py index 7ef1f5aa6..1f00c0580 100644 --- a/apps/sla/tests/test_framework.py +++ b/apps/sla/tests/test_framework.py @@ -2,7 +2,7 @@ import yaml from django.utils.timezone import datetime, make_aware, timedelta -from apps.sla.framework import SLAContext, SLAPolicy, sla_classify +from apps.sla.models import SLOPolicy, TemporalContext from osidb.models import Affect, Flaw, Impact, PsUpdateStream, Tracker from osidb.tests.factories import ( AffectFactory, @@ -15,35 +15,35 @@ pytestmark = pytest.mark.unit -def load_sla_policies(sla_file): +def load_policies(policy_cls, slo_file): """ - Helper function to load SLA policies into the database using a - string representing the contents of an SLA file. + Helper function to load policies into the database using a + string representing the contents of a policy file. """ - for order, policy_desc in enumerate(yaml.safe_load_all(sla_file)): - policy = SLAPolicy.create_from_description(policy_desc, order) + for order, policy_desc in enumerate(yaml.safe_load_all(slo_file)): + policy = policy_cls.create_from_description(policy_desc, order) policy.save() class TestSLAFramework: """ - test SLA framework functionality + test SLA/SLO framework functionality """ class TestLoad: """ - test SLA policies loading + test SLO policies loading """ def test_single(self): """ test that a policy definition is loaded properly """ - sla_file = """ + slo_file = """ # some comment --- name: Low -description: SLA policy applied to low impact +description: SLO policy applied to low impact conditions: affect: - aggregated impact is low @@ -51,7 +51,7 @@ def test_single(self): flaw: - is not embargoed - state is not triage -sla: +slo: duration: 180 start: latest: @@ -60,25 +60,25 @@ def test_single(self): type: calendar days """ - load_sla_policies(sla_file) + load_policies(SLOPolicy, slo_file) - assert SLAPolicy.objects.count() == 1 - policy = SLAPolicy.objects.first() + assert SLOPolicy.objects.count() == 1 + policy = SLOPolicy.objects.first() assert policy.name == "Low" - assert policy.description == "SLA policy applied to low impact" + assert policy.description == "SLO policy applied to low impact" assert policy.conditions - assert policy.sla - # more details are tested as part of SLA model tests + assert policy.slo + # more details are tested as part of SLO model tests def test_multiple(self): """ test that multiple policy definitions are loaded properly """ - sla_file = """ + slo_file = """ --- name: Critical description: > - SLA policy applied to critical impact + SLO policy applied to critical impact conditions: affect: - aggregated impact is critical @@ -86,7 +86,7 @@ def test_multiple(self): flaw: - is not embargoed - state is not triage -sla: +slo: duration: 50 start: latest: @@ -96,7 +96,7 @@ def test_multiple(self): --- name: Moderate -description: SLA policy applied to moderate impact +description: SLO policy applied to moderate impact conditions: affect: - aggregated impact is moderate @@ -104,7 +104,7 @@ def test_multiple(self): flaw: - is not embargoed - state is not triage -sla: +slo: duration: 90 start: latest: @@ -114,7 +114,7 @@ def test_multiple(self): --- name: Low -description: SLA policy applied to low impact +description: SLO policy applied to low impact conditions: affect: - aggregated impact is low @@ -122,7 +122,7 @@ def test_multiple(self): flaw: - is not embargoed - state is not triage -sla: +slo: duration: 180 start: latest: @@ -131,29 +131,29 @@ def test_multiple(self): type: calendar days """ - load_sla_policies(sla_file) - assert SLAPolicy.objects.count() == 3 - sla_policies = SLAPolicy.objects.all() + load_policies(SLOPolicy, slo_file) + assert SLOPolicy.objects.count() == 3 + slo_policies = SLOPolicy.objects.all() # the order matters so check that it is preserved - assert sla_policies[0].name == "Critical" - assert sla_policies[1].name == "Moderate" - assert sla_policies[2].name == "Low" + assert slo_policies[0].name == "Critical" + assert slo_policies[1].name == "Moderate" + assert slo_policies[2].name == "Low" def test_date_sources(self): """ test that a policy definition with multiple date sources is loaded correctly """ - sla_file = """ + slo_file = """ # some comment --- name: Low -description: SLA policy applied to low impact +description: SLO policy applied to low impact conditions: affect: - aggregated impact is low flaw: - is not embargoed -sla: +slo: duration: 180 start: latest: @@ -165,14 +165,14 @@ def test_date_sources(self): type: calendar days """ - load_sla_policies(sla_file) + load_policies(SLOPolicy, slo_file) - assert SLAPolicy.objects.count() == 1 - policy = SLAPolicy.objects.first() + assert SLOPolicy.objects.count() == 1 + policy = SLOPolicy.objects.first() assert policy.name == "Low" - assert policy.description == "SLA policy applied to low impact" + assert policy.description == "SLO policy applied to low impact" assert policy.conditions - assert policy.sla + assert policy.slo @pytest.mark.parametrize( "type_desc,expected_ending,expected_type", @@ -198,17 +198,17 @@ def test_ending(self, type_desc, expected_ending, expected_type): """ test that a policy ending is correctly loaded from the definition """ - sla_file = f""" + slo_file = f""" # some comment --- name: Low -description: SLA policy applied to low impact +description: SLO policy applied to low impact conditions: affect: - aggregated impact is low flaw: - is not embargoed -sla: +slo: duration: 180 start: latest: @@ -220,37 +220,37 @@ def test_ending(self, type_desc, expected_ending, expected_type): type: {type_desc} """ - load_sla_policies(sla_file) + load_policies(SLOPolicy, slo_file) - assert SLAPolicy.objects.count() == 1 - policy = SLAPolicy.objects.first() - assert policy.sla - assert policy.sla.duration_type == expected_type - assert policy.sla.ending == expected_ending + assert SLOPolicy.objects.count() == 1 + policy = SLOPolicy.objects.first() + assert policy.slo + assert policy.slo.duration_type == expected_type + assert policy.slo.ending == expected_ending - def test_null_sla(self): + def test_null_slo(self): """ - Test that a policy with a null SLA is correctly loaded from the definition + Test that a policy with a null SLO is correctly loaded from the definition """ - sla_file = """ + slo_file = """ --- -name: Null SLA -description: Null SLA +name: Null SLO +description: Null SLO conditions: affect: - aggregated impact is low -sla: null +slo: null """ - load_sla_policies(sla_file) + load_policies(SLOPolicy, slo_file) - assert SLAPolicy.objects.count() == 1 - policy = SLAPolicy.objects.first() - assert policy.sla is None + assert SLOPolicy.objects.count() == 1 + policy = SLOPolicy.objects.first() + assert policy.slo is None class TestClassify: """ test that a model instance is properly - classified to the correct SLA context + classified to the correct SLO context """ def test_single(self): @@ -273,25 +273,25 @@ def test_single(self): type=Tracker.BTS2TYPE[ps_module.bts_name], ) - sla_file = """ + slo_file = """ --- -name: fantastic SLA policy +name: fantastic SLO policy description: there is no better conditions: flaw: - is not embargoed -sla: +slo: duration: 5 start: created date type: calendar days """ - load_sla_policies(sla_file) - sla_context = sla_classify(tracker) + load_policies(SLOPolicy, slo_file) + slo_context = SLOPolicy.classify(tracker) - assert sla_context.sla - assert sla_context.start == flaw.created_dt - assert sla_context.end == flaw.created_dt + timedelta(days=5) + assert slo_context.policy + assert slo_context.start == flaw.created_dt + assert slo_context.end == flaw.created_dt + timedelta(days=5) @pytest.mark.parametrize( "reported_dt1,reported_dt2,mi_duration,ne_duration", @@ -353,14 +353,14 @@ def test_multiple(self, reported_dt1, reported_dt2, mi_duration, ne_duration): type=Tracker.BTS2TYPE[ps_module.bts_name], ) - sla_file = f""" + slo_file = f""" --- name: Major Incident description: only for very serious cases conditions: flaw: - major incident state is approved -sla: +slo: duration: {mi_duration} start: reported date type: calendar days @@ -371,43 +371,43 @@ def test_multiple(self, reported_dt1, reported_dt2, mi_duration, ne_duration): conditions: flaw: - is not embargoed -sla: +slo: duration: {ne_duration} start: reported date type: calendar days """ - load_sla_policies(sla_file) + load_policies(SLOPolicy, slo_file) - policies = SLAPolicy.objects.all() + policies = SLOPolicy.objects.all() assert policies[0].accepts( - SLAContext( + TemporalContext( affect=affect1, flaw=flaw1, tracker=tracker, ) ) assert policies[1].accepts( - SLAContext( + TemporalContext( affect=affect2, flaw=flaw2, tracker=tracker, ) ) - sla_context = sla_classify(tracker) + slo_context = SLOPolicy.classify(tracker) - # the first context is the one resulting in the earlist SLA + # the first context is the one resulting in the earlist SLO # end so it should be the outcome of the classification - assert "flaw" in sla_context - assert sla_context["flaw"] == flaw1 - assert "affect" in sla_context - assert sla_context["affect"] == affect1 - assert "tracker" in sla_context - assert sla_context["tracker"] == tracker - assert sla_context.sla - assert sla_context.start == flaw1.reported_dt - assert sla_context.end == flaw1.reported_dt + timedelta(days=mi_duration) + assert "flaw" in slo_context + assert slo_context["flaw"] == flaw1 + assert "affect" in slo_context + assert slo_context["affect"] == affect1 + assert "tracker" in slo_context + assert slo_context["tracker"] == tracker + assert slo_context.policy + assert slo_context.start == flaw1.reported_dt + assert slo_context.end == flaw1.reported_dt + timedelta(days=mi_duration) @pytest.mark.parametrize( "is_closed", @@ -416,9 +416,9 @@ def test_multiple(self, reported_dt1, reported_dt2, mi_duration, ne_duration): (False), ], ) - def test_tracker_sla(self, is_closed): + def test_tracker_slo(self, is_closed): """ - test that classification work also with an SLA property belonging to the tracker + test that classification work also with an SLO property belonging to the tracker """ flaw = FlawFactory( embargoed=False, @@ -450,7 +450,7 @@ def test_tracker_sla(self, is_closed): ) assert tracker.is_closed is is_closed - sla_file = """ + slo_file = """ name: policy used for closed trackers description: > this is actually an unrealistic scenario @@ -462,21 +462,21 @@ def test_tracker_sla(self, is_closed): - is not community flaw: - is not embargoed -sla: +slo: duration: 5 start: created date type: calendar days """ - load_sla_policies(sla_file) - sla_context = sla_classify(tracker) + load_policies(SLOPolicy, slo_file) + slo_context = SLOPolicy.classify(tracker) if is_closed: - assert sla_context.sla - assert sla_context.start == flaw.created_dt - assert sla_context.end == flaw.created_dt + timedelta(days=5) + assert slo_context.policy + assert slo_context.start == flaw.created_dt + assert slo_context.end == flaw.created_dt + timedelta(days=5) else: - assert not sla_context.sla + assert not slo_context.policy @pytest.mark.parametrize( "component,excluded", @@ -488,26 +488,26 @@ def test_tracker_sla(self, is_closed): def test_exclusion(self, component, excluded): """ Test that an exclusion policy placed as the first one using the 'in' condition - works for not applying SLA when these conditions are met + works for not applying SLO when these conditions are met """ - sla_file = """ + slo_file = """ --- name: Excluded components -description: Components which do not need SLA +description: Components which do not need SLO conditions: affect: - PS component in: - kpatch - kudos - kratos -sla: null +slo: null --- name: Low -description: SLA policy applied to low impact +description: SLO policy applied to low impact conditions: affect: - aggregated impact is low -sla: +slo: duration: 5 start: reported date type: calendar days @@ -517,7 +517,7 @@ def test_exclusion(self, component, excluded): conditions: flaw: - major incident state is approved -sla: +slo: duration: 2 start: reported date type: calendar days @@ -544,11 +544,11 @@ def test_exclusion(self, component, excluded): type=Tracker.BTS2TYPE[ps_module.bts_name], ) - load_sla_policies(sla_file) - sla_context = sla_classify(tracker) + load_policies(SLOPolicy, slo_file) + slo_context = SLOPolicy.classify(tracker) if excluded: - assert not sla_context.sla + assert not slo_context.policy else: - assert sla_context.sla - assert sla_context.start == flaw.reported_dt - assert sla_context.end == flaw.created_dt + timedelta(days=5) + assert slo_context.policy + assert slo_context.start == flaw.reported_dt + assert slo_context.end == flaw.created_dt + timedelta(days=5) diff --git a/apps/sla/tests/test_models.py b/apps/sla/tests/test_models.py index 1764475da..e9ab1f97a 100644 --- a/apps/sla/tests/test_models.py +++ b/apps/sla/tests/test_models.py @@ -1,7 +1,7 @@ import pytest from django.utils.timezone import datetime, make_aware, timedelta -from apps.sla.models import SLA, SLAContext, SLAPolicy +from apps.sla.models import SLOPolicy, TemporalContext, TemporalPolicy from apps.sla.time import add_business_days, add_days from osidb.models import Affect, Flaw, Impact, Tracker from osidb.tests.factories import ( @@ -16,14 +16,14 @@ pytestmark = pytest.mark.unit -class TestSLA: +class TestSLO: """ - test SLA parsing and computation + test SLO parsing and computation """ class TestParsing: """ - test SLA definition parsing + test SLO definition parsing """ @pytest.mark.parametrize( @@ -32,16 +32,16 @@ class TestParsing: ) def test_duration(self, definition, expected): """ - test parsing of SLA duration + test parsing of SLO duration """ - sla_desc = { + slo_desc = { "duration": definition, "start": "unembargo date", "type": "business days", } - sla = SLA.create_from_description(sla_desc) + slo = TemporalPolicy.create_from_description(slo_desc) - assert sla.duration == expected + assert slo.duration == expected @pytest.mark.parametrize( "definition,expected_func,expected_dates", @@ -70,18 +70,18 @@ def test_duration(self, definition, expected): ) def test_start(self, definition, expected_func, expected_dates): """ - test parsing of SLA start + test parsing of SLO start """ - sla_desc = { + slo_desc = { "duration": 5, "start": definition, "type": "business days", } - sla = SLA.create_from_description(sla_desc) + slo = TemporalPolicy.create_from_description(slo_desc) - assert sla.get_start == expected_func + assert slo.get_start == expected_func # No source is specified so by default it's flaw - assert sla.start_dates["flaw"] == expected_dates + assert slo.start_dates["flaw"] == expected_dates @pytest.mark.parametrize( "definition,expected", @@ -89,25 +89,25 @@ def test_start(self, definition, expected_func, expected_dates): ) def test_type(self, definition, expected): """ - test parsing of SLA type + test parsing of SLO type """ - sla_desc = { + slo_desc = { "duration": 5, "start": "unembargo date", "type": definition, } - sla = SLA.create_from_description(sla_desc) + slo = TemporalPolicy.create_from_description(slo_desc) - assert sla.add_days == expected + assert slo.add_days == expected def test_null(self): - """Test parsing a null SLA""" - sla = SLA.create_from_description(None) - assert sla is None + """Test parsing a null SLO""" + slo = TemporalPolicy.create_from_description(None) + assert slo is None class TestStart: """ - test SLA start determination + test SLO start determination """ @pytest.mark.parametrize( @@ -125,20 +125,20 @@ class TestStart: ) def test_start_simple(self, definition, attribute, value): """ - test determination of SLA start defined by short definition + test determination of SLO start defined by short definition """ - sla_desc = { + slo_desc = { "duration": 5, "start": definition, "type": "business days", } - sla = SLA.create_from_description(sla_desc) + slo = TemporalPolicy.create_from_description(slo_desc) flaw = FlawFactory() setattr(flaw, attribute, value) - sla_context = SLAContext(flaw=flaw) - assert sla.start(sla_context) == value + slo_context = TemporalContext(flaw=flaw) + assert slo.start(slo_context) == value @pytest.mark.parametrize( "definition,context,expected", @@ -193,23 +193,23 @@ def test_start_simple(self, definition, attribute, value): ) def test_start_earliest(self, definition, context, expected): """ - test determination of SLA start defined by earliest definition + test determination of SLO start defined by earliest definition """ - sla_desc = { + slo_desc = { "duration": 5, "start": { "earliest": definition, }, "type": "business days", } - sla = SLA.create_from_description(sla_desc) + slo = TemporalPolicy.create_from_description(slo_desc) flaw = FlawFactory() for attribute, value in context: setattr(flaw, attribute, make_aware(value)) - sla_context = SLAContext(flaw=flaw) + slo_context = TemporalContext(flaw=flaw) - assert sla.start(sla_context) == make_aware(expected) + assert slo.start(slo_context) == make_aware(expected) @pytest.mark.parametrize( "definition,context,expected", @@ -264,23 +264,23 @@ def test_start_earliest(self, definition, context, expected): ) def test_start_latest(self, definition, context, expected): """ - test determination of SLA start defined by latest definition + test determination of SLO start defined by latest definition """ - sla_desc = { + slo_desc = { "duration": 5, "start": { "latest": definition, }, "type": "business days", } - sla = SLA.create_from_description(sla_desc) + slo = TemporalPolicy.create_from_description(slo_desc) flaw = FlawFactory() for attribute, value in context: setattr(flaw, attribute, make_aware(value)) - sla_context = SLAContext(flaw=flaw) + slo_context = TemporalContext(flaw=flaw) - assert sla.start(sla_context) == make_aware(expected) + assert slo.start(slo_context) == make_aware(expected) @pytest.mark.parametrize( "definition,context,expected", @@ -313,14 +313,14 @@ def test_start_latest(self, definition, context, expected): ], ) def test_date_source(self, definition, context, expected): - sla_desc = { + slo_desc = { "duration": 5, "start": { "latest": definition, }, "type": "calendar days", } - sla = SLA.create_from_description(sla_desc) + slo = TemporalPolicy.create_from_description(slo_desc) flaw = FlawFactory() for attribute, value in context.get("flaw", {}): @@ -342,13 +342,13 @@ def test_date_source(self, definition, context, expected): for attribute, value in context.get("tracker", {}): setattr(tracker, attribute, make_aware(value)) - sla_context = SLAContext(flaw=flaw, affect=affect, tracker=tracker) + slo_context = TemporalContext(flaw=flaw, affect=affect, tracker=tracker) - assert sla.start(sla_context) == make_aware(expected) + assert slo.start(slo_context) == make_aware(expected) class TestEnd: """ - test SLA end computation + test SLO end computation """ @pytest.mark.parametrize( @@ -366,18 +366,18 @@ class TestEnd: ) def test_end_business(self, definition, expected): """ - test computation of SLA end in business days + test computation of SLO end in business days """ - sla_desc = { + slo_desc = { "duration": definition, "start": "reported date", "type": "business days", } - sla = SLA.create_from_description(sla_desc) + slo = TemporalPolicy.create_from_description(slo_desc) flaw = FlawFactory(reported_dt=make_aware(datetime(2023, 11, 13, 1, 1, 1))) - sla_context = SLAContext(flaw=flaw) + slo_context = TemporalContext(flaw=flaw) - assert sla.end(sla_context) == make_aware(expected) + assert slo.end(slo_context) == make_aware(expected) @pytest.mark.parametrize( "definition,expected", @@ -390,18 +390,18 @@ def test_end_business(self, definition, expected): ) def test_end_calendar(self, definition, expected): """ - test computation of SLA end in calendar days + test computation of SLO end in calendar days """ - sla_desc = { + slo_desc = { "duration": definition, "start": "reported date", "type": "calendar days", } - sla = SLA.create_from_description(sla_desc) + slo = TemporalPolicy.create_from_description(slo_desc) flaw = FlawFactory(reported_dt=make_aware(datetime(2023, 11, 13, 5, 5, 5))) - sla_context = SLAContext(flaw=flaw) + slo_context = TemporalContext(flaw=flaw) - assert sla.end(sla_context) == make_aware(expected) + assert slo.end(slo_context) == make_aware(expected) @pytest.mark.parametrize( "definition,expected", @@ -416,29 +416,29 @@ def test_end_calendar(self, definition, expected): ) def test_no_week_ending(self, definition, expected): """ - test computation of SLA end when no-week-ending is specified + test computation of SLO end when no-week-ending is specified """ - sla_desc = { + slo_desc = { "duration": definition, "start": "reported date", "type": "no week ending calendar days", } - sla = SLA.create_from_description(sla_desc) + slo = TemporalPolicy.create_from_description(slo_desc) # Wednesday flaw = FlawFactory(reported_dt=make_aware(datetime(2024, 11, 13, 5, 5, 5))) - sla_context = SLAContext(flaw=flaw) + slo_context = TemporalContext(flaw=flaw) - assert sla.end(sla_context) == make_aware(expected) + assert slo.end(slo_context) == make_aware(expected) -class TestSLAContext: +class TestTemporalContext: """ - test SLAContext functionality + test TemporalContext functionality """ class TestMin: """ - test minimal SLAContext determination + test minimal TemporalContext determination """ @pytest.mark.parametrize( @@ -515,107 +515,107 @@ def test_min( ): """ test that the minimal context which is the one - resulting in earlies SLA end is correctly determined + resulting in earlies SLO end is correctly determined """ policy_desc1 = { - "name": "fantastic SLA policy", + "name": "fantastic SLO policy", "description": "there is no better", "conditions": {}, # this is not valid but OK for this test case - "sla": definition1, + "slo": definition1, } - policy1 = SLAPolicy.create_from_description(policy_desc1) + policy1 = SLOPolicy.create_from_description(policy_desc1) flaw1 = FlawFactory() setattr(flaw1, attribute1, value1) - sla_context1 = SLAContext(flaw=flaw1) - sla_context1.sla = policy1.sla + slo_context1 = TemporalContext(flaw=flaw1) + slo_context1.policy = policy1.slo policy_desc2 = { - "name": "fantastic SLA policy", + "name": "fantastic SLO policy", "description": "there is no better", "conditions": {}, # this is not valid but OK for this test case - "sla": definition2, + "slo": definition2, } - policy2 = SLAPolicy.create_from_description(policy_desc2) + policy2 = SLOPolicy.create_from_description(policy_desc2) flaw2 = FlawFactory() setattr(flaw2, attribute2, value1) - sla_context2 = SLAContext(flaw=flaw2) - sla_context2.sla = policy2.sla + slo_context2 = TemporalContext(flaw=flaw2) + slo_context2.policy = policy2.slo - assert min(sla_context1, sla_context2) is sla_context1 + assert min(slo_context1, slo_context2) is slo_context1 def test_min_empty(self): """ test that an empty context is never minimal """ policy_desc = { - "name": "fantastic SLA policy", + "name": "fantastic SLO policy", "description": "there is no better", "conditions": {}, # this is not valid but OK for this test case - "sla": { + "slo": { "duration": 5, "start": "creation date", "type": "calendar days", }, } - policy = SLAPolicy.create_from_description(policy_desc) + policy = SLOPolicy.create_from_description(policy_desc) flaw = FlawFactory() - sla_context1 = SLAContext(flaw=flaw) - sla_context1.sla = policy.sla + slo_context1 = TemporalContext(flaw=flaw) + slo_context1.policy = policy.slo - # an empty SLA context - sla_context2 = SLAContext() + # an empty SLO context + slo_context2 = TemporalContext() - assert min(sla_context1, sla_context2) is sla_context1 + assert min(slo_context1, slo_context2) is slo_context1 class TestTimestamps: """ - test SLAContext timestamps computation + test TemporalContext timestamps computation """ def test_empty(self): """ test that an empty context has None timestamps """ - # an empty SLA context + # an empty SLO context # with fake flaw entity - sla_context = SLAContext(flaw="fake") - assert sla_context.start is None - assert sla_context.end is None + slo_context = TemporalContext(flaw="fake") + assert slo_context.start is None + assert slo_context.end is None -class TestSLAPolicy: +class TestSLOPolicy: """ - test SLAPolicy parsing and computation + test SLOPolicy parsing and computation """ class TestParsing: """ - test SLAPolicy definition parsing + test SLOPolicy definition parsing - does not include the SLA model parsing test + does not include the SLO model parsing test which is included in a dedicated test class """ def test_basics(self): """ - test parsing of SLAPolicy basics + test parsing of SLOPolicy basics """ policy_desc = { - "name": "fantastic SLA policy", + "name": "fantastic SLO policy", "description": "there is no better", "conditions": {}, # this is not valid but OK for this test case - "sla": { + "slo": { "duration": 5, "start": "unembargo date", "type": "business days", }, } - policy = SLAPolicy.create_from_description(policy_desc) + policy = SLOPolicy.create_from_description(policy_desc) - assert policy.name == "fantastic SLA policy" + assert policy.name == "fantastic SLO policy" assert policy.description == "there is no better" - # also check that there is some SLA inside - assert policy.sla.duration == 5 + # also check that there is some SLO inside + assert policy.slo.duration == 5 @pytest.mark.parametrize( "definition,expected_affect,expected_flaw,expected_tracker", @@ -691,19 +691,19 @@ def test_conditions( self, definition, expected_affect, expected_flaw, expected_tracker ): """ - test parsing of SLAPolicy conditions + test parsing of SLOPolicy conditions """ policy_desc = { - "name": "fantastic SLA policy", + "name": "fantastic SLO policy", "description": "there is no better", "conditions": definition, - "sla": { + "slo": { "duration": 5, "start": "unembargo date", "type": "business days", }, } - policy = SLAPolicy.create_from_description(policy_desc) + policy = SLOPolicy.create_from_description(policy_desc) # let us only check the conditions by name here # checking the whole functionality will be done in other tests @@ -719,15 +719,15 @@ def test_conditions( class TestConditions: """ - test SLAPolicy conditions eveluation + test SLOPolicy conditions eveluation """ def test_conditions(self): """ - test evaluating of SLAPolicy conditions + test evaluating of SLOPolicy conditions """ policy_desc = { - "name": "fantastic SLA policy", + "name": "fantastic SLO policy", "description": "there is no better", "conditions": { "affect": [ @@ -741,13 +741,13 @@ def test_conditions(self): "aggregated impact is moderate", ], }, - "sla": { + "slo": { "duration": 5, "start": "unembargo date", "type": "business days", }, } - policy = SLAPolicy.create_from_description(policy_desc) + policy = SLOPolicy.create_from_description(policy_desc) flaw = FlawFactory( components=["dnf"], @@ -772,9 +772,9 @@ def test_conditions(self): type=Tracker.BTS2TYPE[ps_module.bts_name], ) - # provide SLA context + # provide SLO context assert policy.accepts( - SLAContext( + TemporalContext( flaw=flaw, affect=affect, tracker=tracker, @@ -783,10 +783,10 @@ def test_conditions(self): def test_conditions_multiple_flaws(self): """ - test evaluating of SLAPolicy conditions when there are multiple flaws associated + test evaluating of SLOPolicy conditions when there are multiple flaws associated """ policy_desc = { - "name": "fantastic SLA policy", + "name": "fantastic SLO policy", "description": "there is no better", "conditions": { "affect": [ @@ -799,13 +799,13 @@ def test_conditions_multiple_flaws(self): "is not embargoed", ], }, - "sla": { + "slo": { "duration": 5, "start": "unembargo date", "type": "business days", }, } - policy = SLAPolicy.create_from_description(policy_desc) + policy = SLOPolicy.create_from_description(policy_desc) flaw1 = FlawFactory( components=["dnf"], @@ -850,7 +850,7 @@ def test_conditions_multiple_flaws(self): # first flaw context should be accepted # because it meets the conditions assert policy.accepts( - SLAContext( + TemporalContext( flaw=flaw1, affect=affect1, tracker=tracker, @@ -859,7 +859,7 @@ def test_conditions_multiple_flaws(self): # second flaw context should not be accepted # because it does not meet the conditions assert not policy.accepts( - SLAContext( + TemporalContext( flaw=flaw2, affect=affect2, tracker=tracker, @@ -877,23 +877,23 @@ def test_conditions_multiple_flaws(self): ) def test_in_condition(self, operator, component, accepted): """ - Test that the 'in' and 'not in' operators in an SLA condition works as expected + Test that the 'in' and 'not in' operators in an SLO condition works as expected """ policy_desc = { - "name": "SLA in/not in operator", + "name": "SLO in/not in operator", "description": "Test for the in/not in operator", "conditions": { "affect": [ {f"PS component {operator}": ["kpatch", "kmatch", "kcatch"]} ], }, - "sla": { + "slo": { "duration": 3, "start": "reported date", "type": "calendar days", }, } - policy = SLAPolicy.create_from_description(policy_desc) + policy = SLOPolicy.create_from_description(policy_desc) flaw = FlawFactory() ps_module = PsModuleFactory() @@ -914,34 +914,36 @@ def test_in_condition(self, operator, component, accepted): ) assert ( - policy.accepts(SLAContext(flaw=flaw, affect=affect, tracker=tracker)) + policy.accepts( + TemporalContext(flaw=flaw, affect=affect, tracker=tracker) + ) == accepted ) class TestContext: """ - test SLAPolicy context determination + test SLOPolicy context determination """ def test_context(self): """ - test simple determination of SLAPolicy context + test simple determination of SLOPolicy context """ policy_desc = { - "name": "fantastic SLA policy", + "name": "fantastic SLO policy", "description": "there is no better", "conditions": { "flaw": [ "is not embargoed", ], }, - "sla": { + "slo": { "duration": 5, "start": "unembargo date", "type": "calendar days", }, } - policy = SLAPolicy.create_from_description(policy_desc) + policy = SLOPolicy.create_from_description(policy_desc) flaw = FlawFactory( components=["dnf"], @@ -962,22 +964,22 @@ def test_context(self): type=Tracker.BTS2TYPE[ps_module.bts_name], ) - sla_context = policy.context(tracker) - assert sla_context - assert sla_context["affect"] == affect - assert sla_context["flaw"] == flaw - assert sla_context["tracker"] == tracker - assert sla_context.sla == policy.sla - assert sla_context.start == flaw.unembargo_dt - assert sla_context.end == flaw.unembargo_dt + timedelta(days=5) + slo_context = policy.context(tracker) + assert slo_context + assert slo_context["affect"] == affect + assert slo_context["flaw"] == flaw + assert slo_context["tracker"] == tracker + assert slo_context.policy == policy.slo + assert slo_context.start == flaw.unembargo_dt + assert slo_context.end == flaw.unembargo_dt + timedelta(days=5) def test_context_multiple_flaws(self): """ - test evaluating of SLAPolicy conditions when + test evaluating of SLOPolicy conditions when there are multiple accepted flaws associated """ policy_desc = { - "name": "fantastic SLA policy", + "name": "fantastic SLO policy", "description": "there is no better", "conditions": { "affect": [ @@ -987,13 +989,13 @@ def test_context_multiple_flaws(self): "is not embargoed", ], }, - "sla": { + "slo": { "duration": 5, "start": "unembargo date", "type": "calendar days", }, } - policy = SLAPolicy.create_from_description(policy_desc) + policy = SLOPolicy.create_from_description(policy_desc) flaw1 = FlawFactory( components=["dnf"], @@ -1030,50 +1032,50 @@ def test_context_multiple_flaws(self): # both context are accepted assert policy.accepts( - SLAContext( + TemporalContext( flaw=flaw1, affect=affect1, tracker=tracker, ) ) assert policy.accepts( - SLAContext( + TemporalContext( flaw=flaw2, affect=affect2, tracker=tracker, ) ) - # but the first one resutls in earliest SLA end - sla_context = policy.context(tracker) - assert sla_context - assert sla_context["affect"] == affect1 - assert sla_context["flaw"] == flaw1 - assert sla_context["tracker"] == tracker - assert sla_context.sla == policy.sla - assert sla_context.start == flaw1.unembargo_dt - assert sla_context.end == flaw1.unembargo_dt + timedelta(days=5) + # but the first one resutls in earliest SLO end + slo_context = policy.context(tracker) + assert slo_context + assert slo_context["affect"] == affect1 + assert slo_context["flaw"] == flaw1 + assert slo_context["tracker"] == tracker + assert slo_context.policy == policy.slo + assert slo_context.start == flaw1.unembargo_dt + assert slo_context.end == flaw1.unembargo_dt + timedelta(days=5) def test_policy_not_applicable(self): """ - Test that SLA policy does not apply when rhsa_sla_applicable is false + Test that SLO policy does not apply when rhsa_sla_applicable is false in the PS update stream. """ policy_desc = { - "name": "fantastic SLA policy", + "name": "fantastic SLO policy", "description": "there is no better", "conditions": { "flaw": [ "is not embargoed", ], }, - "sla": { + "slo": { "duration": 5, "start": "unembargo date", "type": "business days", }, } - policy = SLAPolicy.create_from_description(policy_desc) + policy = SLOPolicy.create_from_description(policy_desc) flaw = FlawFactory( embargoed=False, @@ -1095,5 +1097,5 @@ def test_policy_not_applicable(self): ps_update_stream=ps_update_stream.name, ) - sla_context = policy.context(tracker) - assert not sla_context + slo_context = policy.context(tracker) + assert not slo_context diff --git a/apps/trackers/bugzilla/query.py b/apps/trackers/bugzilla/query.py index 614eccaa8..886c5b489 100644 --- a/apps/trackers/bugzilla/query.py +++ b/apps/trackers/bugzilla/query.py @@ -6,7 +6,7 @@ from apps.bbsync.exceptions import ProductDataError from apps.bbsync.models import BugzillaComponent from apps.bbsync.query import BugzillaQueryBuilder -from apps.sla.framework import sla_classify +from apps.sla.models import SLOPolicy from apps.trackers.common import TrackerQueryBuilder from osidb.cc import BugzillaAffectCCBuilder from osidb.models import Flaw @@ -200,10 +200,12 @@ def generate_deadline(self): """ generate query for Bugzilla deadline """ - sla_context = sla_classify(self.tracker) - # the tracker may or may not be under SLA + slo_context = SLOPolicy.classify(self.tracker) + # the tracker may or may not be under SLO self._query["deadline"] = ( - sla_context.end.strftime(DATE_FMT) if sla_context.sla is not None else None + slo_context.end.strftime(DATE_FMT) + if slo_context.policy is not None + else None ) def generate_description(self): diff --git a/apps/trackers/jira/query.py b/apps/trackers/jira/query.py index 0f4492637..582816ccb 100644 --- a/apps/trackers/jira/query.py +++ b/apps/trackers/jira/query.py @@ -11,7 +11,7 @@ from django.utils.timezone import make_aware -from apps.sla.framework import sla_classify +from apps.sla.models import SLAPolicy, SLOPolicy from apps.trackers.common import TrackerQueryBuilder from apps.trackers.exceptions import ( ComponentUnavailableError, @@ -189,6 +189,7 @@ def generate(self): self.generate_description() self.generate_labels() self.generate_sla() + self.generate_slo() self.generate_summary() self.generate_versions() self.generate_additional_fields() @@ -351,7 +352,7 @@ def generate_labels(self): def generate_sla(self): """ - generate query for Jira SLA timestamps + generate query for Jira SLO timestamps """ # Tracker has a manually defined due date if "nonstandard-sla" in self._query["fields"]["labels"]: @@ -359,9 +360,43 @@ def generate_sla(self): if not self.tracker.external_system_id: # Workaround for when a new tracker is filed. At this point in the code it - # has not been fully saved so created_dt is not a valid date, but the SLAs + # has not been fully saved so created_dt is not a valid date, but the SLOs # use the tracker's created date. Since we only care about the date and not the - # time for the SLA computation, we temporarily set a created_dt of now, which + # time for the SLO computation, we temporarily set a created_dt of now, which + # will be replaced later by the TrackingMixin, and this way we do not have to change + # the entire logic of the code for this to work. + self.tracker.created_dt = make_aware(datetime.now()) + + sla_date_field = JiraProjectFields.objects.filter( + project_key=self.ps_module.bts_key, field_name="SLA Date" + ).first() + + sla_context = SLAPolicy.classify(self.tracker) + # the tracker may or may not be under SLA + if sla_context.policy is not None: + if sla_date_field: + self._query["fields"][sla_date_field.field_id] = ( + sla_context.end.strftime("%Y-%m-%dT%H:%M:%S.%f")[:-3] + "+0000" + ) + else: + # explicitly set the empty dates so they are cleared + # out in case of falling out of SLA later on update + if sla_date_field: + self._query["fields"][sla_date_field.field_id] = None + + def generate_slo(self): + """ + generate query for Jira SLO timestamps + """ + # Tracker has a manually defined due date + if "nonstandard-sla" in self._query["fields"]["labels"]: + return + + if not self.tracker.external_system_id: + # Workaround for when a new tracker is filed. At this point in the code it + # has not been fully saved so created_dt is not a valid date, but the SLOs + # use the tracker's created date. Since we only care about the date and not the + # time for the SLO computation, we temporarily set a created_dt of now, which # will be replaced later by the TrackingMixin, and this way we do not have to change # the entire logic of the code for this to work. self.tracker.created_dt = make_aware(datetime.now()) @@ -372,17 +407,17 @@ def generate_sla(self): project_key=self.ps_module.bts_key, field_name="Target start" ) - sla_context = sla_classify(self.tracker) - # the tracker may or may not be under SLA - if sla_context.sla is not None: - self._query["fields"]["duedate"] = sla_context.end.isoformat() + slo_context = SLOPolicy.classify(self.tracker) + # the tracker may or may not be under SLO + if slo_context.policy is not None: + self._query["fields"]["duedate"] = slo_context.end.isoformat() if target_start.exists(): self._query["fields"][target_start.first().field_id] = ( - sla_context.start.isoformat() + slo_context.start.isoformat() ) else: # explicitly set the empty dates so they are cleared - # out in case of falling out of SLA later on update + # out in case of falling out of SLO later on update self._query["fields"]["duedate"] = None if target_start.exists(): self._query["fields"][target_start.first().field_id] = None @@ -596,6 +631,7 @@ def generate(self): self.generate_description() self.generate_labels() self.generate_sla() + self.generate_slo() self.generate_summary() self.generate_versions() self.generate_additional_fields() diff --git a/apps/trackers/tests/conftest.py b/apps/trackers/tests/conftest.py index 2260666b2..6626dcd01 100644 --- a/apps/trackers/tests/conftest.py +++ b/apps/trackers/tests/conftest.py @@ -1,6 +1,6 @@ import pytest -from apps.sla.framework import SLAPolicy +from apps.sla.models import SLAPolicy, SLOPolicy from apps.trackers.constants import TRACKERS_API_VERSION from apps.trackers.models import JiraProjectFields from apps.trackers.tests.factories import JiraProjectFieldsFactory @@ -60,7 +60,7 @@ def test_app_api_uri(test_app_scheme_host, api_version) -> str: @pytest.fixture() def clean_policies(): """ - clean SLA framework before and after every test + clean SLA/SLO policies before and after every test * before so it is not mixed with some leftovers * after so we do not leave any leftovers @@ -69,8 +69,10 @@ def clean_policies(): when run in batch than when run alone so better to be safe then sorry """ SLAPolicy.objects.all().delete() + SLOPolicy.objects.all().delete() yield # run test here SLAPolicy.objects.all().delete() + SLOPolicy.objects.all().delete() def jira_vulnissuetype_fields_setup_without_versions(): diff --git a/apps/trackers/tests/test_bugzilla.py b/apps/trackers/tests/test_bugzilla.py index 1621df852..4ca7f771e 100644 --- a/apps/trackers/tests/test_bugzilla.py +++ b/apps/trackers/tests/test_bugzilla.py @@ -10,7 +10,8 @@ from apps.bbsync.constants import RHSCL_BTS_KEY from apps.bbsync.exceptions import ProductDataError from apps.bbsync.tests.factories import BugzillaComponentFactory, BugzillaProductFactory -from apps.sla.tests.test_framework import load_sla_policies +from apps.sla.models import SLOPolicy +from apps.sla.tests.test_framework import load_policies from apps.trackers.bugzilla.query import TrackerBugzillaQueryBuilder from osidb.models import Affect, Impact, Tracker from osidb.tests.factories import ( @@ -118,20 +119,20 @@ def test_generate_deadline(self, clean_policies): type=Tracker.TrackerType.BUGZILLA, ) - sla_file = """ + slo_file = """ --- name: Not Embargoed description: suitable for whatever we find on the street conditions: flaw: - is not embargoed -sla: +slo: duration: 10 start: reported date type: calendar days """ - load_sla_policies(sla_file) + load_policies(SLOPolicy, slo_file) query = TrackerBugzillaQueryBuilder(tracker).query assert "deadline" in query diff --git a/apps/trackers/tests/test_common.py b/apps/trackers/tests/test_common.py index 8c4f7de0e..d16a951ee 100644 --- a/apps/trackers/tests/test_common.py +++ b/apps/trackers/tests/test_common.py @@ -7,7 +7,8 @@ import pytest -from apps.sla.tests.test_framework import load_sla_policies +from apps.sla.models import SLAPolicy, SLOPolicy +from apps.sla.tests.test_framework import load_policies from apps.trackers.bugzilla.query import TrackerBugzillaQueryBuilder from apps.trackers.common import TrackerQueryBuilder from apps.trackers.jira.query import TrackerJiraQueryBuilder @@ -719,13 +720,13 @@ def test_triage(self): assert "Triage" not in tqb.description -class TestTrackerQueryBuilderSLA: +class TestTrackerQueryBuilderSLO: """ TrackerQueryBuilder deadline/duedate and related stuff test cases """ @pytest.mark.parametrize( - "impact,under_sla", + "impact,under_slo", [ (Impact.LOW, False), (Impact.MODERATE, False), @@ -738,11 +739,11 @@ def test_explicit_duedate( clean_policies, setup_vulnerability_issue_type_fields, impact, - under_sla, + under_slo, ): """ test that every tracker gets a deadline/duedate - even if the value was empty because of no SLA + even if the value was empty because of no SLO """ flaw = FlawFactory( embargoed=False, @@ -799,6 +800,106 @@ def test_explicit_duedate( field_name="Target start", ).save() + # simplified impact policies effective today + slo_file = """ +--- +name: Critical +description: SLO policy applied to critical impact +conditions: + affect: + - aggregated impact is critical +slo: + duration: 7 + start: + latest: + flaw: + - unembargo date + type: calendar days +--- +name: Important +description: SLO policy applied to important impact +conditions: + affect: + - aggregated impact is important +slo: + duration: 21 + start: + latest: + flaw: + - unembargo date + type: calendar days +""" + + load_policies(SLOPolicy, slo_file) + + # check that the SLO fields are always in the query + # but has a non-empty value only when SLO is applied + + query = TrackerBugzillaQueryBuilder(tracker1).query + assert "deadline" in query + assert bool(query["deadline"]) is under_slo + + query = TrackerJiraQueryBuilder(tracker2).query + assert "duedate" in query["fields"] + assert bool(query["fields"]["duedate"]) is under_slo + assert target_start_id in query["fields"] + assert bool(query["fields"][target_start_id]) is under_slo + + +class TestTrackerQueryBuilderSLA: + """ + TrackerQueryBuilder SLA Date and related stuff test cases + """ + + @pytest.mark.parametrize( + "impact,under_sla", + [ + (Impact.LOW, False), + (Impact.MODERATE, False), + (Impact.IMPORTANT, True), + (Impact.CRITICAL, True), + ], + ) + def test_explicit_duedate( + self, + clean_policies, + setup_sample_external_resources, + setup_vulnerability_issue_type_fields, + impact, + under_sla, + ): + """ + test that every tracker gets a deadline/duedate + even if the value was empty because of no SLA + """ + flaw = FlawFactory( + embargoed=False, + impact=impact, + reported_dt=datetime(2020, 1, 1, tzinfo=timezone.utc), + source="REDHAT", + ) + + # Jira tracker context + ps_module = PsModuleFactory( + bts_key="RHEL", + bts_name="jboss", + private_trackers_allowed=False, + ) + affect = AffectFactory( + flaw=flaw, + impact=Impact.NOVALUE, + affectedness=Affect.AffectAffectedness.AFFECTED, + resolution=Affect.AffectResolution.DELEGATED, + ps_module=ps_module.name, + ) + ps_update_stream = PsUpdateStreamFactory(ps_module=ps_module) + tracker = TrackerFactory( + affects=[affect], + embargoed=flaw.embargoed, + ps_update_stream=ps_update_stream.name, + type=Tracker.TrackerType.JIRA, + ) + # simplified impact policies effective today sla_file = """ --- @@ -829,17 +930,11 @@ def test_explicit_duedate( type: calendar days """ - load_sla_policies(sla_file) + load_policies(SLAPolicy, sla_file) # check that the SLA fields are always in the query # but has a non-empty value only when SLA is applied - query = TrackerBugzillaQueryBuilder(tracker1).query - assert "deadline" in query - assert bool(query["deadline"]) is under_sla - - query = TrackerJiraQueryBuilder(tracker2).query - assert "duedate" in query["fields"] - assert bool(query["fields"]["duedate"]) is under_sla - assert target_start_id in query["fields"] - assert bool(query["fields"][target_start_id]) is under_sla + query = TrackerJiraQueryBuilder(tracker).query + assert "customfield_12326740" in query["fields"] + assert bool(query["fields"]["customfield_12326740"]) is under_sla diff --git a/apps/trackers/tests/test_jira.py b/apps/trackers/tests/test_jira.py index 4e74c0c77..a62d24dc3 100644 --- a/apps/trackers/tests/test_jira.py +++ b/apps/trackers/tests/test_jira.py @@ -9,7 +9,8 @@ import pytest from django.utils.timezone import make_aware -from apps.sla.tests.test_framework import load_sla_policies +from apps.sla.models import SLAPolicy, SLOPolicy +from apps.sla.tests.test_framework import load_policies from apps.trackers.exceptions import ( ComponentUnavailableError, MissingEmbargoStatusError, @@ -2468,23 +2469,23 @@ def test_generate_cvss_non_rh_mod7_lt_rh( ) -class TestOldTrackerJiraQueryBuilderSla: +class TestOldTrackerJiraQueryBuilderSlo: """ - Test Jira tracker SLA query building for Bug issuetype. + Test Jira tracker SLO query building for Bug issuetype. Not in the other classes, because for reasons unknown, the tests mysteriously breaks when parametrized like others in TestBothNewOldTrackerJiraQueryBuilder are. This isn't - the first mystery for SLAs, as clean_policies looks suspect too. - (Tests should rollback everything when run, so why is SLA + the first mystery for SLOs, as clean_policies looks suspect too. + (Tests should rollback everything when run, so why is SLO cleanup necessary? Something evades pytest cleanup, and @pytest.mark.django_db(transaction=True) doesn't help.) Not enough resources to investigate deeper. """ - def test_generate_sla(self, clean_policies): + def test_generate_slo(self, clean_policies, setup_sample_external_resources): return """ - test that the query for the Jira SLA timestamps is generated correctly + test that the query for the Jira SLO timestamps is generated correctly """ flaw = FlawFactory( embargoed=False, @@ -2505,67 +2506,32 @@ def test_generate_sla(self, clean_policies): type=Tracker.TrackerType.BUGZILLA, ) - JiraProjectFields( - project_key=ps_module.bts_key, - field_id="priority", - field_name="Priority", - allowed_values=[ - "Blocker", - "Critical", - "Major", - "Normal", - "Minor", - "Undefined", - ], - ).save() - # this value is used in RH instance of Jira however - # it is always fetched from project meta anyway - target_start_id = "customfield_12313941" - JiraProjectFields( - project_key=ps_module.bts_key, - field_id=target_start_id, - field_name="Target start", - ).save() - JiraProjectFieldsFactory( - project_key=ps_module.bts_key, - field_id="security", - field_name="Security Level", - allowed_values=[ - "Embargoed Security Issue", - "Red Hat Employee", - "Red Hat Engineering Authorized", - "Red Hat Partner", - "Restricted", - "Team", - ], - ) - - sla_file = """ + slo_file = """ --- name: Not Embargoed description: suitable for whatever we find on the street conditions: flaw: - is not embargoed -sla: +slo: duration: 10 start: reported date type: calendar days """ - load_sla_policies(sla_file) + load_policies(SLOPolicy, slo_file) query = OldTrackerJiraQueryBuilder(tracker).query - assert target_start_id in query["fields"] - assert query["fields"][target_start_id] == "2000-01-01T00:00:00+00:00" + assert "customfield_12313941" in query["fields"] + assert query["fields"]["customfield_12313941"] == "2000-01-01T00:00:00+00:00" assert "duedate" in query["fields"] assert query["fields"]["duedate"] == "2000-01-11T00:00:00+00:00" - # SLA was manually marked to not be calculated + # SLO was manually marked to not be calculated tracker.meta_attr["labels"] = json.dumps(["nonstandard-sla"]) query = OldTrackerJiraQueryBuilder(tracker).query - assert target_start_id not in query["fields"] + assert "customfield_12313941" not in query["fields"] assert "duedate" not in query["fields"] @@ -2582,7 +2548,7 @@ class TestTrackerJiraQueryBuilderSla: Not enough resources to investigate deeper. """ - def test_generate_sla(self, clean_policies): + def test_generate_sla(self, clean_policies, setup_sample_external_resources): """ test that the query for the Jira SLA timestamps is generated correctly """ @@ -2592,8 +2558,8 @@ def test_generate_sla(self, clean_policies): source="REDHAT", ) ps_module = PsModuleFactory( - bts_key="FOOPROJECT", - bts_name="bugzilla", + bts_key="RHEL", + bts_name="jboss", private_trackers_allowed=False, ) affect = AffectFactory( @@ -2607,27 +2573,11 @@ def test_generate_sla(self, clean_policies): affects=[affect], embargoed=flaw.embargoed, ps_update_stream=ps_update_stream.name, - type=Tracker.TrackerType.BUGZILLA, + type=Tracker.TrackerType.JIRA, ) jira_vulnissuetype_fields_setup_without_versions() - JiraProjectFields( - project_key="FOOPROJECT", - field_id="versions", - field_name="Affects Version/s", - allowed_values=["1.2.3"], - ).save() - - # this value is used in RH instance of Jira however - # it is always fetched from project meta anyway - target_start_id = "customfield_12313941" - JiraProjectFields( - project_key=ps_module.bts_key, - field_id=target_start_id, - field_name="Target start", - ).save() - sla_file = """ --- name: Not Embargoed @@ -2641,17 +2591,13 @@ def test_generate_sla(self, clean_policies): type: calendar days """ - load_sla_policies(sla_file) + load_policies(SLAPolicy, sla_file) query = TrackerJiraQueryBuilder(tracker).query - assert target_start_id in query["fields"] - assert query["fields"][target_start_id] == "2000-01-01T00:00:00+00:00" - assert "duedate" in query["fields"] - assert query["fields"]["duedate"] == "2000-01-11T00:00:00+00:00" + assert "customfield_12326740" in query["fields"] + assert query["fields"]["customfield_12326740"] == "2000-01-11T00:00:00.000+0000" # SLA was manually marked to not be calculated tracker.meta_attr["labels"] = json.dumps(["nonstandard-sla"]) query = TrackerJiraQueryBuilder(tracker).query - assert target_start_id not in query["fields"] - assert "duedate" not in query["fields"] diff --git a/collectors/ps_constants/core.py b/collectors/ps_constants/core.py index adf7a6ec3..fc2a2cdcd 100644 --- a/collectors/ps_constants/core.py +++ b/collectors/ps_constants/core.py @@ -6,7 +6,7 @@ from django.db import transaction from requests_gssapi import HTTPSPNEGOAuth -from apps.sla.models import SLA, SLAPolicy +from apps.sla.models import SLAPolicy, SLOPolicy, TemporalPolicy from apps.trackers.models import JiraBugIssuetype from collectors.cveorg.models import Keyword from osidb.models import SpecialConsiderationPackage @@ -52,14 +52,33 @@ def sync_sla_policies(sla_policies): """ Sync SLA policy data """ - SLA.objects.all().delete() SLAPolicy.objects.all().delete() + TemporalPolicy.objects.filter( + sla_policies__isnull=True, slo_policies__isnull=True + ).delete() + for order, policy_desc in enumerate(sla_policies): # In SLA policies order is important so it is passed down to the model policy = SLAPolicy.create_from_description(policy_desc, order) policy.save() +@transaction.atomic +def sync_slo_policies(slo_policies): + """ + Sync SLO policy data + """ + SLOPolicy.objects.all().delete() + TemporalPolicy.objects.filter( + sla_policies__isnull=True, slo_policies__isnull=True + ).delete() + + for order, policy_desc in enumerate(slo_policies): + # In SLO policies order is important so it is passed down to the model + policy = SLOPolicy.create_from_description(policy_desc, order) + policy.save() + + @transaction.atomic def sync_jira_bug_issuetype(source_dict): """ diff --git a/collectors/ps_constants/tasks.py b/collectors/ps_constants/tasks.py index c9477ba8f..ab5a1dab8 100644 --- a/collectors/ps_constants/tasks.py +++ b/collectors/ps_constants/tasks.py @@ -15,6 +15,7 @@ sync_cveorg_keywords, sync_jira_bug_issuetype, sync_sla_policies, + sync_slo_policies, sync_special_consideration_packages, ) @@ -43,6 +44,10 @@ def collect_step_1_fetch(): logger.info(f"Fetching PS Constants (SLA Policies) from '{url}'") sla_policies = fetch_ps_constants(url, multi=True) + url = "/".join((PS_CONSTANTS_BASE_URL, "slo_policies.yml")) + logger.info(f"Fetching PS Constants (SLO Policies) from '{url}'") + slo_policies = fetch_ps_constants(url, multi=True) + url = "/".join((PS_CONSTANTS_BASE_URL, "jira_bug_issuetype.yml")) logger.info(f"Fetching PS Constants (Jira Bug issuetype) from '{url}'") jira_bug_issuetype = fetch_ps_constants(url) @@ -55,6 +60,7 @@ def collect_step_1_fetch(): cveorg_keywords, sc_packages, sla_policies, + slo_policies, jira_bug_issuetype, ) @@ -63,11 +69,13 @@ def collect_step_2_sync( cveorg_keywords, sc_packages, sla_policies, + slo_policies, jira_bug_issuetype, ): sync_cveorg_keywords(cveorg_keywords) sync_special_consideration_packages(sc_packages) sync_sla_policies(sla_policies) + sync_slo_policies(slo_policies) sync_jira_bug_issuetype(jira_bug_issuetype) @@ -94,6 +102,7 @@ def ps_constants_collector(collector_obj) -> str: cveorg_keywords, sc_packages, sla_policies, + slo_policies, jira_bug_issuetype, ) = collect_step_1_fetch() @@ -105,6 +114,7 @@ def ps_constants_collector(collector_obj) -> str: cveorg_keywords, sc_packages, sla_policies, + slo_policies, jira_bug_issuetype, ) diff --git a/conftest.py b/conftest.py index eb65619d7..4d804d278 100644 --- a/conftest.py +++ b/conftest.py @@ -603,6 +603,37 @@ def setup_sample_external_resources(): "KEV (active exploit case)", ], ).save() + JiraProjectFields( + project_key=ps_module.bts_key, + field_id="versions", + field_name="Affects Version/s", + allowed_values=["1.2.3"], + ).save() + JiraProjectFields( + project_key=ps_module.bts_key, + field_id="customfield_12313941", + field_name="Target start", + ).save() + JiraProjectFields( + project_key=ps_module.bts_key, + field_id="customfield_12316142", + field_name="Severity", + allowed_values=[ + "Critical", + "Important", + "Moderate", + "Low", + "unexpected mess here", + "Informational", + "None", + ], + ).save() + JiraProjectFields( + project_key=ps_module.bts_key, + field_id="customfield_12326740", + field_name="SLA Date", + allowed_values=[], + ).save() JiraBugIssuetype(project=ps_module.bts_key).save() # 4) list some valid components accepeted for the diff --git a/docs/CHANGELOG.md b/docs/CHANGELOG.md index 1ab3541ed..a9045a21d 100644 --- a/docs/CHANGELOG.md +++ b/docs/CHANGELOG.md @@ -5,6 +5,8 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). ## Unreleased +### Added +- Add SLA policies in the new Jira field (OSIDB-4404) ## [4.16.0] - 2025-09-16 ### Fixed diff --git a/osidb/management/commands/sync_ps_constants.py b/osidb/management/commands/sync_ps_constants.py index 2b3d739bc..5fec49748 100644 --- a/osidb/management/commands/sync_ps_constants.py +++ b/osidb/management/commands/sync_ps_constants.py @@ -15,6 +15,7 @@ def handle(self, *args, **options): cveorg_keywords, sc_packages, sla_policies, + slo_policies, jira_bug_issuetype, ) = collect_step_1_fetch() @@ -22,6 +23,7 @@ def handle(self, *args, **options): cveorg_keywords, sc_packages, sla_policies, + slo_policies, jira_bug_issuetype, ) diff --git a/osidb/serializer.py b/osidb/serializer.py index c700e8b0a..d0cf993a1 100644 --- a/osidb/serializer.py +++ b/osidb/serializer.py @@ -2105,7 +2105,7 @@ def mi_differ(flaw1, flaw2): # we only need to sync the trackers when crucial attributes change # plus in the case of the MI we care for specific changes only # - # the crucial attributes are those influencing the SLA deadline plus the CVE ID + # the crucial attributes are those influencing the SLO deadline plus the CVE ID # # in the case of impact we should ideally check whether the change actually # changes the tracker aggregated impact (in cases of multi-flaw trackers) diff --git a/osidb/signals.py b/osidb/signals.py index 7b44ed683..94589b428 100644 --- a/osidb/signals.py +++ b/osidb/signals.py @@ -82,7 +82,7 @@ def update_major_incident_start_dt(flaw: Flaw) -> None: Flaw.FlawMajorIncident.APPROVED, Flaw.FlawMajorIncident.CISA_APPROVED, # Flaw.FlawMajorIncident.MINOR is not - # included as it has no impact on the SLA + # included as it has no impact on the SLO Flaw.FlawMajorIncident.ZERO_DAY, } if is_major_incident and flaw.major_incident_start_dt is None: From bf25a27a3f636b7cefd62093b255f81b6934e234 Mon Sep 17 00:00:00 2001 From: Conrado Costa Date: Wed, 1 Oct 2025 13:39:19 -0400 Subject: [PATCH 2/2] Update version to 4.16.1 --- config/settings.py | 2 +- docs/CHANGELOG.md | 2 ++ openapi.yml | 2 +- osidb/__init__.py | 2 +- pyproject.toml | 2 +- 5 files changed, 6 insertions(+), 4 deletions(-) diff --git a/config/settings.py b/config/settings.py index 098b5c111..f2498af27 100644 --- a/config/settings.py +++ b/config/settings.py @@ -280,7 +280,7 @@ SPECTACULAR_SETTINGS = { "TITLE": "OSIDB API", "DESCRIPTION": "REST API autogenerated docs for the OSIDB and its components", - "VERSION": "4.16.0", + "VERSION": "4.16.1", "SWAGGER_UI_SETTINGS": {"supportedSubmitMethods": []}, "SERVE_AUTHENTICATION": [ "kaminarimon.auth.KerberosAuthentication", diff --git a/docs/CHANGELOG.md b/docs/CHANGELOG.md index a9045a21d..a9da5856a 100644 --- a/docs/CHANGELOG.md +++ b/docs/CHANGELOG.md @@ -5,6 +5,8 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). ## Unreleased + +## [4.16.1] - 2025-10-01 ### Added - Add SLA policies in the new Jira field (OSIDB-4404) diff --git a/openapi.yml b/openapi.yml index 9f792e151..2c2f976d6 100644 --- a/openapi.yml +++ b/openapi.yml @@ -1,7 +1,7 @@ openapi: 3.0.3 info: title: OSIDB API - version: 4.16.0 + version: 4.16.1 description: REST API autogenerated docs for the OSIDB and its components paths: /auth/token: diff --git a/osidb/__init__.py b/osidb/__init__.py index 6a2f9cf67..0159f55ea 100644 --- a/osidb/__init__.py +++ b/osidb/__init__.py @@ -2,5 +2,5 @@ osidb version """ -__version__ = "4.16.0" +__version__ = "4.16.1" VERSION = __version__ diff --git a/pyproject.toml b/pyproject.toml index b71ce0d72..7b21b21f7 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [project] name = "osidb" -version = "4.16.0" +version = "4.16.1" requires-python = ">=3.12,<3.13" dependencies = [ "backoff>=1.11.1",