From: Zhang Rong(Jon) Date: Thu, 5 Dec 2024 15:32:04 +0000 (+0800) Subject: Add Alarm Service Configuration API X-Git-Tag: k-release~1 X-Git-Url: https://gerrit.o-ran-sc.org/r/gitweb?a=commitdiff_plain;h=26e9f039966cdd7bc62b36155cbbd8f883fad8dc;p=pti%2Fo2.git Add Alarm Service Configuration API 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) --- diff --git a/configs/o2app.conf b/configs/o2app.conf index ad224bb..ad1e5da 100644 --- a/configs/o2app.conf +++ b/configs/o2app.conf @@ -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 diff --git a/o2app/adapter/unit_of_work.py b/o2app/adapter/unit_of_work.py index 57d1c00..60a94b6 100644 --- a/o2app/adapter/unit_of_work.py +++ b/o2app/adapter/unit_of_work.py @@ -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__() diff --git a/o2app/bootstrap.py b/o2app/bootstrap.py index 7679ef3..d167091 100644 --- a/o2app/bootstrap.py +++ b/o2app/bootstrap.py @@ -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) diff --git a/o2common/config/config.py b/o2common/config/config.py index 2ad1da3..c82a8e0 100644 --- a/o2common/config/config.py +++ b/o2common/config/config.py @@ -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 diff --git a/o2ims/adapter/alarm_repository.py b/o2ims/adapter/alarm_repository.py index 5986ad5..8c4514d 100644 --- a/o2ims/adapter/alarm_repository.py +++ b/o2ims/adapter/alarm_repository.py @@ -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) diff --git a/o2ims/adapter/orm.py b/o2ims/adapter/orm.py index e966472..d7ec289 100644 --- a/o2ims/adapter/orm.py +++ b/o2ims/adapter/orm.py @@ -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) diff --git a/o2ims/domain/alarm_obj.py b/o2ims/domain/alarm_obj.py index c0f9f17..3c2bf06 100644 --- a/o2ims/domain/alarm_obj.py +++ b/o2ims/domain/alarm_obj.py @@ -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, diff --git a/o2ims/domain/alarm_repo.py b/o2ims/domain/alarm_repo.py index ca6929e..188f7bd 100644 --- a/o2ims/domain/alarm_repo.py +++ b/o2ims/domain/alarm_repo.py @@ -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 diff --git a/o2ims/views/alarm_dto.py b/o2ims/views/alarm_dto.py index caef205..d92ef19 100644 --- a/o2ims/views/alarm_dto.py +++ b/o2ims/views/alarm_dto.py @@ -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.' + ) + } + ) diff --git a/o2ims/views/alarm_route.py b/o2ims/views/alarm_route.py index 7b0c03e..c1c4129 100644 --- a/o2ims/views/alarm_route.py +++ b/o2ims/views/alarm_route.py @@ -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') diff --git a/o2ims/views/alarm_view.py b/o2ims/views/alarm_view.py index 00d4a20..486a35a 100644 --- a/o2ims/views/alarm_view.py +++ b/o2ims/views/alarm_view.py @@ -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() diff --git a/o2ims/views/api_ns.py b/o2ims/views/api_ns.py index 5f3cf23..3008e0c 100644 --- a/o2ims/views/api_ns.py +++ b/o2ims/views/api_ns.py @@ -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': '' }] diff --git a/tests/unit/test_alarm.py b/tests/unit/test_alarm.py index 1503d09..e1a606b 100644 --- a/tests/unit/test_alarm.py +++ b/tests/unit/test_alarm.py @@ -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