Add Alarm Service Configuration API 22/13822/2
authorZhang Rong(Jon) <rong.zhang@windriver.com>
Thu, 5 Dec 2024 15:32:04 +0000 (23:32 +0800)
committerZhang Rong(Jon) <rong.zhang@windriver.com>
Tue, 10 Dec 2024 03:34:02 +0000 (11:34 +0800)
This commit introduces support for the GET, PUT, and PATCH methods for
a new API URI.
The API enables clients to query and modify the Alarm Service
Configuration.

Test Plan:
- Query the Alarm Service Configuration and verify it returns the
  default values as expected.
- Use the PUT method to update the Alarm Service Configuration values.
- Use the PATCH method to update the retentionPeriod parameter of the
  Alarm Service Configuration.
- Attempt to update using a value below the minimum allowed threshold
  and confirm it fails as expected.
- Confirm the POST and DELETE methods return a 405 Method Not Allowed
  response as expected.

Issue-ID: INF-482

Change-Id: Id70d6d301ffd6f278b9989a04f45a416fbfad241
Signed-off-by: Zhang Rong(Jon) <rong.zhang@windriver.com>
13 files changed:
configs/o2app.conf
o2app/adapter/unit_of_work.py
o2app/bootstrap.py
o2common/config/config.py
o2ims/adapter/alarm_repository.py
o2ims/adapter/orm.py
o2ims/domain/alarm_obj.py
o2ims/domain/alarm_repo.py
o2ims/views/alarm_dto.py
o2ims/views/alarm_route.py
o2ims/views/alarm_view.py
o2ims/views/api_ns.py
tests/unit/test_alarm.py

index ad224bb..ad1e5da 100644 (file)
@@ -7,6 +7,9 @@ smo_token_data = smo_token_payload
 
 auth_provider = oauth2
 
+# The minimal retention period. By default is 14, minimal support is 1.
+#min_retention_period = 14
+
 [OAUTH2]
 # support OAuth2.0
 
index 57d1c00..60a94b6 100644 (file)
@@ -75,6 +75,8 @@ class SqlAlchemyUnitOfWork(AbstractUnitOfWork):
             .AlarmSubscriptionSqlAlchemyRepository(self.session)
         self.alarm_probable_causes = alarm_repository\
             .AlarmProbableCauseSqlAlchemyRepository(self.session)
+        self.alarm_service_config = alarm_repository\
+            .AlarmServiceConfigurationSqlAlchemyRepository(self.session)
 
         return super().__enter__()
 
index 7679ef3..d167091 100644 (file)
@@ -14,7 +14,7 @@
 
 from retry import retry
 import inspect
-from typing import Callable
+from typing import Callable, Optional
 
 from o2common.adapter.notifications import AbstractNotifications,\
     NoneNotifications
@@ -44,24 +44,26 @@ def wait_for_db_ready(engine):
 def bootstrap(
     start_orm: bool = True,
     uow: unit_of_work.AbstractUnitOfWork = SqlAlchemyUnitOfWork(),
-    notifications: AbstractNotifications = None,
+    notifications: Optional[AbstractNotifications] = None,
     publish: Callable = redis_eventpublisher.publish,
 ) -> messagebus.MessageBus:
-
-    if notifications is None:
-        notifications = NoneNotifications()
+    """
+    Bootstrap the application with dependencies.
+    """
+    notifications = notifications or NoneNotifications()
 
     if start_orm:
         with uow:
-            # get default engine if uow is by default
             engine = uow.session.get_bind()
-
             wait_for_db_ready(engine)
             o2ims_orm.start_o2ims_mappers(engine)
             o2dms_orm.start_o2dms_mappers(engine)
 
-    dependencies = {"uow": uow, "notifications": notifications,
-                    "publish": publish}
+    dependencies = {
+        "uow": uow,
+        "notifications": notifications,
+        "publish": publish
+    }
     injected_event_handlers = {
         event_type: [
             inject_dependencies(handler, dependencies)
index 2ad1da3..c82a8e0 100644 (file)
@@ -28,6 +28,9 @@ _DEFAULT_STX_URL = "http://192.168.204.1:5000/v3"
 _DCMANAGER_URL_PORT = os.environ.get("DCMANAGER_API_PORT", "8119")
 _DCMANAGER_URL_PATH = os.environ.get("DCMANAGER_API_PATH", "/v1.0")
 
+_DEFAULT_MIN_RETENTION_PERIOD = 14
+_MIN_MIN_RETENTION_PERIOD = 1
+
 
 def get_config_path():
     path = os.environ.get("O2APP_CONFIG", "/configs/o2app.conf")
@@ -392,7 +395,7 @@ def get_reviewer_token():
 
 
 def get_auth_provider():
-    return config.conf.auth_provider
+    return config.conf.DEFAULT.auth_provider
 
 
 def get_dms_support_profiles():
@@ -406,3 +409,16 @@ def get_dms_support_profiles():
     if 'native_k8sapi' not in profiles_list:
         profiles_list.append('native_k8sapi')
     return profiles_list
+
+
+def get_min_retention_period():
+    try:
+        min_retention_period_str = config.conf.DEFAULT.min_retention_period
+        if min_retention_period_str is not None:
+            min_retention_period_int = int(min_retention_period_str)
+            if min_retention_period_int >= _MIN_MIN_RETENTION_PERIOD:
+                return min_retention_period_int
+    except (ValueError, TypeError) as e:
+        logger.warning(f"Invalid min_retention_period value: {e}")
+
+    return _DEFAULT_MIN_RETENTION_PERIOD
index 5986ad5..8c4514d 100644 (file)
@@ -17,7 +17,8 @@ from typing import List, Tuple
 from o2ims.domain import alarm_obj
 from o2ims.domain.alarm_repo import AlarmDefinitionRepository, \
     AlarmEventRecordRepository, AlarmSubscriptionRepository, \
-    AlarmProbableCauseRepository, AlarmDictionaryRepository
+    AlarmProbableCauseRepository, AlarmDictionaryRepository, \
+    AlarmServiceConfigurationRepository
 from o2common.helper import o2logging
 logger = o2logging.get_logger(__name__)
 
@@ -150,3 +151,31 @@ class AlarmProbableCauseSqlAlchemyRepository(AlarmProbableCauseRepository):
     def _delete(self, probable_cause_id):
         self.session.query(alarm_obj.ProbableCause).filter_by(
             probableCauseId=probable_cause_id).delete()
+
+
+class AlarmServiceConfigurationSqlAlchemyRepository(
+        AlarmServiceConfigurationRepository):
+    def __init__(self, session):
+        super().__init__()
+        self.session = session
+
+    def _add_default(self) -> alarm_obj.AlarmServiceConfiguration:
+        query = self.session.query(
+            alarm_obj.AlarmServiceConfiguration).first()
+        if not query:
+            default_config = alarm_obj.AlarmServiceConfiguration(
+                retention_period=14
+            )
+            self.session.add(default_config)
+            self.session.commit()
+            logger.info(
+                "Inserted default AlarmServiceConfiguration record.")
+            return default_config
+        return query
+
+    def _get(self) -> alarm_obj.AlarmServiceConfiguration:
+        return self.session.query(alarm_obj.AlarmServiceConfiguration).first()
+
+    def _update(self, service_config: alarm_obj.AlarmServiceConfiguration):
+        print(service_config.retentionPeriod)
+        self.session.merge(service_config)
index e966472..d7ec289 100644 (file)
@@ -243,6 +243,16 @@ alarm_subscription = Table(
     Column("filter", String(255)),
 )
 
+alarm_service_configuration = Table(
+    "alarmServiceConfiguration",
+    metadata,
+    Column("updatetime", DateTime),
+    Column("createtime", DateTime),
+
+    Column("id", Integer, primary_key=True, autoincrement=True),
+    Column("retentionPeriod", Integer, default=15)
+)
+
 
 @retry((exc.IntegrityError), tries=3, delay=2)
 def wait_for_metadata_ready(engine):
@@ -255,6 +265,7 @@ def start_o2ims_mappers(engine=None):
     logger.info("Starting O2 IMS mappers")
 
     # IMS Infrastruture Monitoring Mappering
+    mapper(alarmModel.AlarmServiceConfiguration, alarm_service_configuration)
     mapper(alarmModel.AlarmEventRecord, alarm_event_record)
     alarmdefinition_mapper = mapper(
         alarmModel.AlarmDefinition, alarm_definition)
index c0f9f17..3c2bf06 100644 (file)
@@ -133,6 +133,12 @@ class AlarmEventRecordModifications(AgRoot):
         self.perceivedSeverity = clear
 
 
+class AlarmServiceConfiguration(AgRoot, Serializer):
+    def __init__(self, retention_period: int = None) -> None:
+        super().__init__()
+        self.retentionPeriod = retention_period
+
+
 class AlarmDefinition(AgRoot, Serializer):
     def __init__(self, id: str, name: str, change_type: AlarmChangeTypeEnum,
                  desc: str, prop_action: str, clearing_type: ClearingTypeEnum,
index ca6929e..188f7bd 100644 (file)
@@ -239,3 +239,31 @@ class AlarmProbableCauseRepository(abc.ABC):
     @abc.abstractmethod
     def _delete(self, probable_cause_id):
         raise NotImplementedError
+
+
+class AlarmServiceConfigurationRepository(abc.ABC):
+    def __init__(self):
+        self.seen = set()  # type: Set[obj.AlarmServiceConfiguration]
+
+    def get(self) -> obj.AlarmServiceConfiguration:
+        service_config = self._get()
+        if service_config:
+            self.seen.add(service_config)
+        else:
+            service_config = self._add_default()
+        return service_config
+
+    def update(self, service_config: obj.AlarmServiceConfiguration):
+        self._update(service_config)
+
+    @abc.abstractmethod
+    def _add_default(self) -> obj.AlarmServiceConfiguration:
+        raise NotImplementedError
+
+    @abc.abstractmethod
+    def _get(self) -> obj.AlarmServiceConfiguration:
+        raise NotImplementedError
+
+    @abc.abstractmethod
+    def _update(self, service_config: obj.AlarmServiceConfiguration):
+        raise NotImplementedError
index caef205..d92ef19 100644 (file)
@@ -197,3 +197,29 @@ class SubscriptionDTO:
                 'provided then all events are reported.'),
         }
     )
+
+
+class AlarmServiceConfigurationDTO:
+
+    alarm_service_configuration_get = api_monitoring_v1.model(
+        "AlarmServiceConfigurationDto",
+        {
+            'retentionPeriod': fields.Integer(
+                required=True,
+                example=14,
+                description='Number of days for alarm history to be retained.'
+                ),
+            'extensions': Json2Dict(attribute='extensions')
+        }
+    )
+
+    alarm_service_configuration_expect = api_monitoring_v1.model(
+        "AlarmServiceConfigurationDto",
+        {
+            'retentionPeriod': fields.Integer(
+                required=True,
+                example=14,
+                description='Number of days for alarm history to be retained.'
+                )
+        }
+    )
index 7b0c03e..c1c4129 100644 (file)
@@ -15,6 +15,7 @@
 from flask import request
 from flask_restx import Resource, reqparse
 
+from o2common.config import config
 from o2common.service.messagebus import MessageBus
 from o2common.views.pagination_route import link_header, PAGE_PARAM
 from o2common.views.route_exception import NotFoundException, \
@@ -23,7 +24,7 @@ from o2ims.domain.alarm_obj import PerceivedSeverityEnum
 from o2ims.views import alarm_view
 from o2ims.views.api_ns import api_ims_monitoring as api_monitoring_v1
 from o2ims.views.alarm_dto import AlarmDTO, SubscriptionDTO, \
-    MonitoringApiV1DTO
+    MonitoringApiV1DTO, AlarmServiceConfigurationDTO
 
 from o2common.helper import o2logging
 logger = o2logging.get_logger(__name__)
@@ -46,7 +47,7 @@ class VersionRouter(Resource):
         return {
             'uriPrefix': request.base_url.rsplit('/', 1)[0],
             'apiVersions': [{
-                'version': '1.0.0',
+                'version': '1.1.0',
                 # 'isDeprecated': 'False',
                 # 'retirementDate': ''
             }]
@@ -285,3 +286,88 @@ class SubscriptionGetDelRouter(Resource):
     def delete(self, alarmSubscriptionID):
         result = alarm_view.subscription_delete(alarmSubscriptionID, bus.uow)
         return result, 200
+
+
+# ----------  Alarm Event Record ---------- #
+@api_monitoring_v1.route("/v1/alarmServiceConfiguration")
+@api_monitoring_v1.param(
+    'all_fields',
+    'Set any value for show all fields. This value will cover "fields" ' +
+    'and "all_fields".',
+    _in='query')
+@api_monitoring_v1.param(
+    'fields',
+    'Set fields to show, split by comma, "/" for parent and children.' +
+    ' Like "name,parent/children". This value will cover' +
+    ' "exculde_fields".',
+    _in='query')
+@api_monitoring_v1.param(
+    'exclude_fields',
+    'Set fields to exclude showing, split by comma, "/" for parent and ' +
+    'children. Like "name,parent/children". This value will cover ' +
+    '"exclude_default".',
+    _in='query')
+@api_monitoring_v1.param(
+    'exclude_default',
+    'Exclude showing all default fields, Set "true" to enable.',
+    _in='query')
+class AlarmServiceConfigurationRouter(Resource):
+
+    model = AlarmServiceConfigurationDTO.alarm_service_configuration_get
+    expect = AlarmServiceConfigurationDTO.alarm_service_configuration_expect
+
+    @api_monitoring_v1.doc('Get Alarm Service Configuration')
+    @api_monitoring_v1.marshal_with(model)
+    def get(self):
+        result = alarm_view.alarm_service_configuration(bus.uow)
+        if result is not None:
+            return result
+
+    @api_monitoring_v1.doc('Patch Alarm Service Configuration')
+    @api_monitoring_v1.expect(expect)
+    @api_monitoring_v1.marshal_with(model)
+    def patch(self):
+        data = api_monitoring_v1.payload
+        retention_period = data.get('retentionPeriod', None)
+
+        min_retention_period = config.get_min_retention_period()
+        print(min_retention_period)
+
+        if retention_period is None:
+            raise BadRequestException(
+                'The "retentionPeriod" parameter is required')
+        elif retention_period < min_retention_period:
+            raise BadRequestException(
+                f'The "retentionPeriod" parameter shall more than '
+                f'{min_retention_period} days')
+
+        result = alarm_view.alarm_service_configuration_update(data, bus.uow)
+        if result is not None:
+            return result
+
+        raise BadRequestException(
+            'Failed to update alarm service configuration')
+
+    @api_monitoring_v1.doc('Update Alarm Service Configuration')
+    @api_monitoring_v1.expect(expect)
+    @api_monitoring_v1.marshal_with(model)
+    def put(self):
+        data = api_monitoring_v1.payload
+        retention_period = data.get('retentionPeriod', None)
+
+        min_retention_period = config.get_min_retention_period()
+
+        if retention_period is None:
+            raise BadRequestException(
+                'The "retentionPeriod" parameter is required')
+        elif retention_period < min_retention_period:
+            raise BadRequestException(
+                f'The "retentionPeriod" parameter shall more than '
+                f'{min_retention_period} days')
+
+        result = alarm_view.alarm_service_configuration_update(data, bus.uow)
+        if result is not None:
+            return result
+
+        raise BadRequestException(
+            'Failed to update alarm service configuration')
index 00d4a20..486a35a 100644 (file)
@@ -155,3 +155,19 @@ def subscription_delete(subscriptionId: str,
         uow.alarm_subscriptions.delete(subscriptionId)
         uow.commit()
     return True
+
+
+def alarm_service_configuration(uow: unit_of_work.AbstractUnitOfWork):
+    with uow:
+        first = uow.alarm_service_config.get()
+        return first.serialize() if first is not None else None
+
+
+def alarm_service_configuration_update(data: dict,
+                                       uow: unit_of_work.AbstractUnitOfWork):
+    with uow:
+        first = uow.alarm_service_config.get()
+        first.retentionPeriod = data.get('retentionPeriod')
+        uow.alarm_service_config.update(first)
+        uow.commit()
+        return first.serialize()
index 5f3cf23..3008e0c 100644 (file)
@@ -110,7 +110,7 @@ class MonitoringVersion(Resource):
         return {
             'uriPrefix': request.base_url.rsplit('/', 1)[0],
             'apiVersions': [{
-                'version': '1.0.0',
+                'version': '1.1.0',
                 # 'isDeprecated': 'False',
                 # 'retirementDate': ''
             }]
index 1503d09..e1a606b 100644 (file)
@@ -221,6 +221,14 @@ def test_flask_not_allowed(mock_flask_uow):
         resp = client.delete(uri)
         assert resp.status == '405 METHOD NOT ALLOWED'
 
+        # Testing alarm service configuration not support method
+        ##########################
+        uri = apibase + "/alarmServiceConfiguration"
+        resp = client.post(uri)
+        assert resp.status == '405 METHOD NOT ALLOWED'
+        resp = client.delete(uri)
+        assert resp.status == '405 METHOD NOT ALLOWED'
+
 
 class FakeAlarmClient(BaseClient):
     def __init__(self):
@@ -371,3 +379,117 @@ def test_watchers_worker():
     count3 = fakewatcher.fakeOcloudWatcherCounter
     time.sleep(3)
     assert fakewatcher.fakeOcloudWatcherCounter == count3
+
+
+def test_view_alarm_service_configuration(mock_uow):
+    session, uow = mock_uow
+
+    # Test the first time call the view will generate the default data
+    session.return_value.query.return_value.first.return_value = None
+    result = alarm_view.alarm_service_configuration(uow)
+    assert result is not None
+    assert result.get("retentionPeriod") == 14
+
+    config1 = MagicMock()
+    config1.serialize.return_value = {
+        "id": 1,
+        "retentionPeriod": 30
+    }
+    session.return_value.query.return_value.first.return_value = config1
+
+    result = alarm_view.alarm_service_configuration(uow)
+    assert result is not None
+    assert result.get("id") == 1
+    assert result.get("retentionPeriod") == 30
+
+
+def test_view_alarm_service_configuration_update(mock_uow):
+    session, uow = mock_uow
+
+    config1 = MagicMock()
+    config1.serialize.return_value = {
+        "id": 1,
+        "retentionPeriod": 60
+    }
+    session.return_value.query.return_value.first.return_value = config1
+
+    # Test update the config
+    update_data = {
+        "retentionPeriod": 60
+    }
+    result = alarm_view.alarm_service_configuration_update(update_data, uow)
+
+    assert result is not None
+    assert result.get("id") == 1
+    assert result.get("retentionPeriod") == 60
+    # Verify the update and commit is called
+    session.return_value.query.return_value.first.return_value.\
+        retentionPeriod = 60
+    assert session.return_value.commit.called
+
+
+def test_flask_get_alarm_config(mock_flask_uow):
+    session, app = mock_flask_uow
+    apibase = config.get_o2ims_monitoring_api_base() + '/v1'
+
+    config1 = MagicMock()
+    config1.serialize.return_value = {
+        "retentionPeriod": 30
+    }
+
+    session.return_value.query.return_value.first.return_value = config1
+
+    with app.test_client() as client:
+        resp = client.get(apibase+"/alarmServiceConfiguration")
+        assert resp.status_code == 200
+        data = resp.get_json()
+        assert data["retentionPeriod"] == 30
+
+
+def test_flask_patch_alarm_config(mock_flask_uow):
+    session, app = mock_flask_uow
+    apibase = config.get_o2ims_monitoring_api_base() + '/v1'
+
+    config1 = MagicMock()
+    config1.serialize.return_value = {
+        "retentionPeriod": 60
+    }
+    session.return_value.query.return_value.first.return_value = config1
+
+    with app.test_client() as client:
+        # Test updating retention period
+        resp = client.patch(apibase+"/alarmServiceConfiguration", json={
+            "retentionPeriod": 60
+        })
+        assert resp.status_code == 200
+
+        # Test invalid retention period
+        resp = client.patch(apibase+"/alarmServiceConfiguration", json={
+            "retentionPeriod": -1
+        })
+        assert resp.status_code == 400
+
+
+def test_flask_put_alarm_config(mock_flask_uow):
+    session, app = mock_flask_uow
+    apibase = config.get_o2ims_monitoring_api_base() + '/v1'
+
+    config1 = MagicMock()
+    config1.serialize.return_value = {
+        "alarmServiceId": "alarm-service-1",
+        "retentionPeriod": 60
+    }
+    session.return_value.query.return_value.first.return_value = config1
+
+    with app.test_client() as client:
+        # Test updating retention period
+        resp = client.put(apibase+"/alarmServiceConfiguration", json={
+            "retentionPeriod": 60
+        })
+        assert resp.status_code == 200
+
+        # Test invalid retention period
+        resp = client.put(apibase+"/alarmServiceConfiguration", json={
+            "retentionPeriod": -1
+        })
+        assert resp.status_code == 400