Merge "OAuth2 support" master
authorJon Zhang <rong.zhang@windriver.com>
Thu, 30 May 2024 08:44:55 +0000 (08:44 +0000)
committerGerrit Code Review <gerrit@o-ran-sc.org>
Thu, 30 May 2024 08:44:55 +0000 (08:44 +0000)
25 files changed:
Dockerfile.localtest
charts/resources/scripts/init/o2api_start.sh
charts/templates/deployment.yaml
charts/values.yaml
configs/o2app.conf
docker-compose.yml
o2app/entrypoints/flask_application.py
o2app/entrypoints/redis_eventconsumer.py
o2common/authmw/authmiddleware.py
o2common/authmw/authprov.py
o2common/authmw/exceptions.py [new file with mode: 0644]
o2common/config/config.py
o2ims/adapter/alarm_repository.py
o2ims/adapter/clients/fault_client.py
o2ims/adapter/clients/ocloud_client.py
o2ims/domain/alarm_obj.py
o2ims/domain/alarm_repo.py
o2ims/service/auditor/alarm_handler.py
o2ims/service/command/purge_alarm_handler.py
o2ims/service/event/alarm_event.py
o2ims/views/alarm_dto.py
o2ims/views/alarm_route.py
o2ims/views/alarm_view.py
requirements-stx.txt
requirements.txt

index 33fcaa8..7ced03b 100644 (file)
@@ -1,7 +1,27 @@
-FROM python:3.11-slim-buster
-
-RUN apt-get update && apt-get install -y git gcc \
-    vim curl procps ssh
+FROM nexus3.onap.org:10001/onap/integration-python:12.0.0
+# https://nexus3.onap.org/#browse/search=keyword%3Dintegration-python:d406d405e4cfbf1186265b01088caf9a
+# https://git.onap.org/integration/docker/onap-python/tree/Dockerfile
+
+USER root
+
+ARG user=orano2
+ARG group=orano2
+
+# Create a group and user
+RUN addgroup -S $group && adduser -S -D -h /home/$user $user $group && \
+    chown -R $user:$group /home/$user &&  \
+    mkdir /var/log/$user && \
+    mkdir -p /src && \
+    mkdir -p /configs/ && \
+    mkdir -p /src/o2app/ && \
+    mkdir -p /src/helm_sdk/ && \
+    mkdir -p /etc/o2/ && \
+    chown -R $user:$group /var/log/$user && \
+    chown -R $user:$group /src && \
+    chown -R $user:$group /configs && \
+    chown -R $user:$group /etc/o2/
+
+COPY requirements.txt requirements-test.txt requirements-stx.txt constraints.txt /tmp/
 
 # in case git repo is not accessable
 RUN mkdir -p /cgtsclient && mkdir -p /distcloud-client
@@ -9,49 +29,61 @@ COPY temp/config /cgtsclient/
 COPY temp/distcloud-client /distcloud-client/
 COPY temp/fault /faultclient/
 
-RUN pip install -e cgtsclient/sysinv/cgts-client/cgts-client/ \
-    && pip install -e /distcloud-client/distributedcloud-client \
-    && pip install -e /faultclient/python-fmclient/fmclient/
-# in case git repo is not accessable
-
-COPY requirements.txt constraints.txt requirements-test.txt /tmp/
-
-RUN pip install -r /tmp/requirements.txt -c /tmp/constraints.txt \
-    && pip install -r /tmp/requirements-test.txt
-
-
-RUN mkdir -p /src
 COPY o2ims/ /src/o2ims/
 COPY o2dms/ /src/o2dms/
 COPY o2common/ /src/o2common/
-
-RUN mkdir -p /src/o2app/
 COPY o2app/ /src/o2app/
+COPY setup.py /src/
 
-RUN mkdir -p /src/helm_sdk/
 COPY helm_sdk/ /src/helm_sdk/
 
-COPY setup.py /src/
-
 COPY configs/ /etc/o2/
+COPY configs/ /configs/
+
+RUN apk add --no-cache \
+    git \
+    curl \
+    bluez-dev \
+    bzip2-dev \
+    dpkg-dev dpkg \
+    expat-dev \
+    gcc \
+    libc-dev \
+    libffi-dev \
+    libnsl-dev \
+    libtirpc-dev \
+    linux-headers \
+    make \
+    ncurses-dev \
+    openssl-dev \
+    pax-utils \
+    sqlite-dev \
+    tcl-dev \
+    tk \
+    tk-dev \
+    util-linux-dev \
+    xz-dev \
+    zlib-dev
+
+RUN set -ex \
+    && apk add --no-cache bash \
+        && apk add --no-cache --virtual .fetch2-deps \
+    && pip install -r /tmp/requirements.txt -c /tmp/constraints.txt \
+    && pip install -r /tmp/requirements-test.txt \
+    && pip install -e /cgtsclient/sysinv/cgts-client/cgts-client/ \
+    && pip install -e /distcloud-client/distributedcloud-client \
+    && pip install -e /faultclient/python-fmclient/fmclient/ \
+    && pip install -e /src \
+    && apk del --no-network .fetch2-deps
 
-# RUN mkdir -p /helmsdk
-# COPY temp/helmsdk /helmsdk/
-# # RUN git clone --depth 1 --branch master https://github.com/cloudify-incubator/cloudify-helm-plugin.git helmsdk
-# COPY /helmsdk/helm_sdk /src/helm_sdk
-
-# RUN pip install -e /src
 COPY tests/ /tests/
 
-#RUN apt-get install -y procps vim
-
-#RUN apt-get install -y curl
-RUN curl -O https://get.helm.sh/helm-v3.3.1-linux-amd64.tar.gz;
-RUN tar -zxvf helm-v3.3.1-linux-amd64.tar.gz; cp linux-amd64/helm /usr/local/bin
-
 RUN mkdir -p /etc/kubeconfig/
 # COPY temp/kubeconfig/config /etc/kubeconfig/
 
 RUN mkdir -p /var/log/orano2
 
 WORKDIR /src
+
+# USER $user
+ENV PYTHONHASHSEED=0
index 4581db5..e790a6f 100644 (file)
 #!/bin/bash
 
 # The gunicorn start with [::] to listen on both IPv4 and IPv6
-gunicorn -b [::]:80 o2app.entrypoints.flask_application:app --certfile /configs/server.crt  --keyfile /configs/server.key
+gunicorn -b [::]:80 o2app.entrypoints.flask_application:app \
+--certfile /configs/server.crt \
+--keyfile /configs/server.key \
+--ca-certs /configs/smoca.crt \
+--cert-reqs 2
 
 sleep infinity
index 72e3580..19d7431 100644 (file)
@@ -64,8 +64,10 @@ spec:
           volumeMounts:
             - name: scripts
               mountPath: /opt
+            {{- if .Values.db.persistence }}
             - name: db-pv
               mountPath: /var/lib/postgresql/data
+            {{- end }}
         - name: redis
           image: "{{ .Values.o2ims.images.tags.redis }}"
           ports:
@@ -124,6 +126,12 @@ spec:
               value: "1"
             - name: REDIS_HOST
               value: localhost
+            {{- if default false .Values.o2ims.useHostCert }}
+            - name: REQUESTS_CA_BUNDLE
+              value: /etc/ssl/custom-cert.pem
+            {{- end }}
+            - name: CGTS_INSECURE_SSL
+              value: {{ ternary "1" "0" (default false .Values.o2ims.cgtsInsecureSSL) | quote }}
           volumeMounts:
             - name: scripts
               mountPath: /opt
@@ -131,6 +139,11 @@ spec:
               mountPath: /configs/o2app.conf
               subPath: config.json
               readOnly: true
+            {{- if default false .Values.o2ims.useHostCert }}
+            - name: ca-certs
+              mountPath: /etc/ssl/custom-cert.pem
+              readOnly: true
+            {{- end }}
         - name: o2api
           image: "{{ .Values.o2ims.images.tags.o2service }}"
           ports:
@@ -179,6 +192,9 @@ spec:
               mountPath: /configs/server.key
               subPath: config.json
               readOnly: true
+            - name: smocacrt
+              mountPath: /configs/smoca.crt
+              subPath: config.json
         {{- if .Values.o2dms.helm_cli_enable }}
         - name: helmcli
           image: "{{ .Values.o2ims.images.tags.o2service }}"
@@ -216,7 +232,15 @@ spec:
         - configMap:
             name: {{ .Chart.Name }}-smocacrt
           name: smocacrt
+        {{- if .Values.db.persistence }}
         - name: db-pv
           persistentVolumeClaim:
             claimName: {{ .Chart.Name }}-db-pv
+        {{- end }}
+        {{- if default false .Values.o2ims.useHostCert }}
+        - name: ca-certs
+          hostPath:
+            path: {{ .Values.o2ims.hostCertPath | quote }}
+            type: File
+        {{- end }}
 ---
index 0ab6f73..83629c4 100644 (file)
@@ -1,4 +1,5 @@
-# Copyright (C) 2021-2023 Wind River Systems, Inc.
+---
+# Copyright (C) 2021-2024 Wind River Systems, Inc.
 #
 #  Licensed under the Apache License, Version 2.0 (the "License");
 #  you may not use this file except in compliance with the License.
@@ -42,6 +43,7 @@ global:
   namespace: oran-o2
 
 db:
+  persistence: true
   storageSize: 10Gi
 
 # ImagePullSecrets for operator ServiceAccount, list of secrets in the same
@@ -60,6 +62,14 @@ o2ims:
     pullPolicy: IfNotPresent
   logginglevel: "WARNING"
 
+  # Mount certs from host system.
+  # Normally required to use CGTS client with SSL.
+  useHostCert: false
+  hostCertPath: /etc/ssl/certs/ca-certificates.crt
+
+  # Skip SSL verification when using CGTS client.
+  cgtsInsecureSSL: false
+
 o2dms:
   helm_cli_enable: false
 
index cf6bdfe..ad224bb 100644 (file)
@@ -5,16 +5,35 @@ ocloud_global_id = 4e24b97c-8c49-4c4f-b53e-3de5235a4e37
 smo_register_url = http://127.0.0.1:8090/register
 smo_token_data = smo_token_payload
 
+auth_provider = oauth2
+
+[OAUTH2]
+# support OAuth2.0
+
+# oauth2 token verify type: jwt or introspection
+oauth2_verify_type =
+# oauth2 public key
+oauth2_public_key =
+# oauth2 encryption asymmetric algorithm
+oauth2_algorithm =
+
+# oauth2 jwt token introspection endpoint, required if oauth2_verify_type = introspection
+oauth2_introspection_endpoint =
+# required if oauth2_verify_type = introspection
+oauth2_client_id =
+# required if oauth2_verify_type = introspection
+oauth2_client_secret =
+
 [OCLOUD]
-OS_AUTH_URL = 
-OS_USERNAME = 
-OS_PASSWORD = 
-API_HOST_EXTERNAL_FLOATING = 
+OS_AUTH_URL =
+OS_USERNAME =
+OS_PASSWORD =
+API_HOST_EXTERNAL_FLOATING =
 
 [API]
 # support native_k8sapi,sol018,sol018_helmcli
 # if the value is black, then native_k8sapi will set by default
-DMS_SUPPORT_PROFILES = 
+DMS_SUPPORT_PROFILES =
 
 [WATCHER]
 
index a3bf256..a624b56 100644 (file)
@@ -20,6 +20,7 @@ services:
       - OS_USERNAME=${OS_USERNAME}
       - OS_PASSWORD=${OS_PASSWORD}
       - LOGGING_CONFIG_LEVEL=DEBUG
+      - CGTS_INSECURE_SSL=1
     volumes:
       - ./configs:/configs
       - ./o2ims:/o2ims
@@ -67,6 +68,7 @@ services:
       - OS_PASSWORD=${OS_PASSWORD}
       - LOGGING_CONFIG_LEVEL=DEBUG
       - HELM_USER_PASSWD=St8rlingX*
+      - CGTS_INSECURE_SSL=1
     volumes:
       - ./configs:/configs
       - ./share:/share
@@ -83,9 +85,6 @@ services:
       - "5005:80"
 
   watcher:
-    build:
-      context: .
-      dockerfile: Dockerfile.localtest
     image: o2imsdms
     depends_on:
       - redis_pubsub
@@ -99,6 +98,7 @@ services:
       - OS_USERNAME=${OS_USERNAME}
       - OS_PASSWORD=${OS_PASSWORD}
       - LOGGING_CONFIG_LEVEL=DEBUG
+      - CGTS_INSECURE_SSL=1
     volumes:
       - ./configs:/configs
       - ./o2ims:/o2ims
index c7e0ef9..fb4b6a2 100644 (file)
@@ -22,28 +22,17 @@ from o2common.views.route_exception import configure_exception
 
 from o2common.authmw import authmiddleware
 from o2common.authmw import authprov
-from o2common.config.config import get_review_url
 from o2common.helper import o2logging
 
+AUTH_ENABLED = True
+FLASK_API_VERSION = '1.0.0'
+
 # apibase = config.get_o2ims_api_base()
-auth = True
 app = Flask(__name__)
 logger = o2logging.get_logger(__name__)
 
-
-def _get_k8s_url():
-    try:
-        token_review_url = get_review_url()
-        return token_review_url
-    except Exception:
-        raise Exception('Get k8s token review url failed')
-
-
-FLASK_API_VERSION = '1.0.0'
-
-if auth:
+if AUTH_ENABLED:
     # perform service account identity&privilege check.
-    _get_k8s_url()
     ad = authprov.auth_definer('ad')
     ad.sanity_check()
     app.wsgi_app = authmiddleware.authmiddleware(app.wsgi_app)
index 439619b..db39daa 100644 (file)
 
 import redis
 import json
+
 from o2app import bootstrap
-from o2common.config import config
 from o2common.adapter.notifications import SmoNotifications
+from o2common.config import config
+from o2common.helper import o2logging
 from o2dms.domain import commands
 from o2ims.domain import commands as imscmd
 from o2ims.domain.subscription_obj import Message2SMO, RegistrationMessage
 from o2ims.domain.alarm_obj import AlarmEvent2SMO
 
-from o2common.helper import o2logging
 logger = o2logging.get_logger(__name__)
 
 r = redis.Redis(**config.get_redis_host_and_port())
@@ -143,11 +144,8 @@ def handle_changed(m, bus):
         datastr = m['data']
         data = json.loads(datastr)
         logger.info('AlarmEventPurged with cmd:{}'.format(data))
-        ref = api_monitoring_base + \
-            monitor_api_version + '/alarms/' + data['id']
         cmd = imscmd.PurgeAlarmEvent(data=AlarmEvent2SMO(
-            id=data['id'], ref=ref,
-            eventtype=data['notificationEventType'],
+            id=data['id'], eventtype=data['notificationEventType'],
             updatetime=data['updatetime']))
         bus.handle(cmd)
     else:
index 3141263..a5193fc 100644 (file)
 #  See the License for the specific language governing permissions and
 #  limitations under the License.
 
+import json
+from flask_restx._http import HTTPStatus
 from werkzeug.wrappers import Request, Response
-from o2common.helper import o2logging
+
 from o2common.authmw.authprov import auth_definer
-from flask_restx._http import HTTPStatus
-import json
+from o2common.authmw.exceptions import AuthRequiredExp
+from o2common.authmw.exceptions import AuthFailureExp
+from o2common.helper import o2logging
 
 logger = o2logging.get_logger(__name__)
 
 
-class AuthRequiredExp(Exception):
-    def __init__(self, value):
-        self.value = value
-
-    def dictize(self):
-        return {
-            'WWW-Authenticate': '{}'.format(self.value)}
-
-
 class AuthProblemDetails():
     def __init__(self, code: int, detail: str, path: str,
                  title=None, instance=None
@@ -54,15 +48,6 @@ class AuthProblemDetails():
         return json.dumps(details, indent=True)
 
 
-class AuthFailureExp(Exception):
-    def __init__(self, value):
-        self.value = value
-
-    def dictize(self):
-        return {
-            'WWW-Authenticate': '{}'.format(self.value)}
-
-
 def _response_wrapper(environ, start_response, header, detail):
     res = Response(headers=header,
                    mimetype='application/json', status=401, response=detail)
@@ -75,7 +60,6 @@ def _internal_err_response_wrapper(environ, start_response, detail):
 
 
 class authmiddleware():
-
     '''
     Auth WSGI middleware
     '''
index c6f5646..87bbc4e 100644 (file)
 #  limitations under the License.
 
 import ssl
-from o2common.helper import o2logging
 import urllib.request
 import urllib.parse
 import json
-
+from http import HTTPStatus
+from requests import post as requests_post
+from requests.auth import HTTPBasicAuth
+from requests.exceptions import HTTPError
+from jwt import decode as jwt_decode
+from jwt.exceptions import ExpiredSignatureError, InvalidTokenError
+
+from o2common.authmw.exceptions import AuthRequiredExp
+from o2common.authmw.exceptions import AuthFailureExp
 from o2common.config.config import get_auth_provider, get_review_url
 from o2common.config.config import get_reviewer_token
+from o2common.config import conf
+from o2common.helper import o2logging
 
 ssl._create_default_https_context = ssl._create_unverified_context
 logger = o2logging.get_logger(__name__)
 
-# read the conf from config file
-auth_prv_conf = get_auth_provider()
 
-try:
-    token_review_url = get_review_url()
-except Exception:
-    raise Exception('Get k8s token review url failed')
+class OAuthAuthenticationException(Exception):
+    def __init__(self, value):
+        self.value = value
 
 
 class K8SAuthenticaException(Exception):
@@ -48,10 +54,12 @@ class auth_definer():
     def __init__(self, name):
         super().__init__()
         self.name = name
+        # read the conf from config file
+        auth_prv_conf = get_auth_provider()
         if auth_prv_conf == 'k8s':
             self.obj = k8s_auth_provider('k8s')
         else:
-            self.obj = keystone_auth_provider('keystone')
+            self.obj = oauth2_auth_provider('oauth2')
 
     def tokenissue(self):
         return self.obj.tokenissue()
@@ -59,7 +67,6 @@ class auth_definer():
     def sanity_check(self):
         return self.obj.sanity_check()
 
-    # call k8s api
     def authenticate(self, token):
         return self.obj.authenticate(token)
 
@@ -71,6 +78,10 @@ class k8s_auth_provider(auth_definer):
 
     def __init__(self, name):
         self.name = name
+        try:
+            self.token_review_url = get_review_url()
+        except Exception:
+            raise Exception('Failed to get k8s token review url.')
 
     def tokenissue(self, **args2):
         pass
@@ -84,6 +95,7 @@ class k8s_auth_provider(auth_definer):
             raise Exception(str(ex))
 
     def authenticate(self, token):
+        ''' Call Kubenetes API to authenticate '''
         reviewer_token = get_reviewer_token()
         tokenreview = {
             "kind": "TokenReview",
@@ -105,7 +117,7 @@ class k8s_auth_provider(auth_definer):
                   'Content-Type': 'application/json'}
         try:
             req = urllib.request.Request(
-                token_review_url, data=binary_data, headers=header)
+                self.token_review_url, data=binary_data, headers=header)
             response = urllib.request.urlopen(req)
             data = json.load(response)
             if data['status']['authenticated'] is True:
@@ -127,18 +139,70 @@ class k8s_auth_provider(auth_definer):
         return True
 
 
-class keystone_auth_provider(auth_definer):
+class oauth2_auth_provider(auth_definer):
     def __init__(self, name):
         self.name = name
 
-    def tokenissue(self, *args1, **args2):
-        pass
+    def _format_public_key(self):
+        public_key_string = """-----BEGIN PUBLIC KEY----- \
+        %s \
+        -----END PUBLIC KEY-----""" % conf.OAUTH2.oauth2_public_key
+        return public_key_string
+
+    def _verify_jwt_token_introspect(self, token):
+        introspect_endpoint = conf.OAUTH2.oauth2_introspection_endpoint
+        client_id = conf.OAUTH2.oauth2_client_id
+        client_secret = conf.OAUTH2.oauth2_client_secret
+        try:
+            response = requests_post(
+                introspect_endpoint,
+                data={'token': token, 'client_id': client_id},
+                auth=HTTPBasicAuth(client_id, client_secret)
+            )
+        except HTTPError as e:
+            logger.error('OAuth2 jwt token introspect verify failed.')
+            raise Exception(str(e))
+        if response.status_code == HTTPStatus.OK:
+            introspection_data = response.json()
+            if introspection_data.get('active'):
+                logger.info('OAuth2 jwt token introspect result active.')
+                return True
+        logger.info('OAuth2 jwt token introspect verify failed.')
+        return False
+
+    def _verify_jwt_token(self, token):
+        algorithm = conf.OAUTH2.oauth2_algorithm
+        public_key_string = self._format_public_key()
+        try:
+            options = {"verify_signature": True, "verify_aud": False,
+                       "exp": True}
+            decoded_token = jwt_decode(token, public_key_string,
+                                       algorithms=[algorithm], options=options)
+            logger.info(
+                'Verified Token from client: %s' %
+                decoded_token.get("clientHost"))
+            return True
+        except (ExpiredSignatureError,
+                InvalidTokenError) as e:
+            logger.error(f'OAuth2 jwt token validation failed: {e}')
+            raise AuthFailureExp(
+                'OAuth2 JWT Token Authentication failure.')
+        except Exception as e:
+            raise AuthRequiredExp(str(e))
 
-    def authenticate(self, *args1, **args2):
+    def authenticate(self, token):
+        ''' Call the JWT to authenticate
+
+        If the verify type is introspection, call introspection endpoint to
+        verify the token.
+        If the verify type is jwt, call JWT SDK to verify the token.
+        '''
+        oauth2_verify_type = conf.OAUTH2.oauth2_verify_type
+        if oauth2_verify_type == 'introspection':
+            return self._verify_jwt_token_introspect(token)
+        elif oauth2_verify_type == 'jwt':
+            return self._verify_jwt_token(token)
         return False
 
     def sanity_check(self):
         pass
-
-    def tokenrevoke(self, *args1, **args2):
-        return False
diff --git a/o2common/authmw/exceptions.py b/o2common/authmw/exceptions.py
new file mode 100644 (file)
index 0000000..5d1b1a6
--- /dev/null
@@ -0,0 +1,32 @@
+# Copyright (C) 2024 Wind River Systems, Inc.
+#
+#  Licensed under the Apache License, Version 2.0 (the "License");
+#  you may not use this file except in compliance with the License.
+#  You may obtain a copy of the License at
+#
+#      http://www.apache.org/licenses/LICENSE-2.0
+#
+#  Unless required by applicable law or agreed to in writing, software
+#  distributed under the License is distributed on an "AS IS" BASIS,
+#  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+#  See the License for the specific language governing permissions and
+#  limitations under the License.
+
+# pylint: disable=too-few-public-methods
+
+
+class AuthGeneralException(Exception):
+    def __init__(self, value):
+        self.value = value
+
+    def dictize(self):
+        return {
+            'WWW-Authenticate': '{}'.format(self.value)}
+
+
+class AuthRequiredExp(AuthGeneralException):
+    pass
+
+
+class AuthFailureExp(AuthGeneralException):
+    pass
index 61c8c69..f8eee94 100644 (file)
@@ -22,8 +22,11 @@ from o2common.helper import o2logging
 logger = o2logging.get_logger(__name__)
 
 
-_DEFAULT_DCMANAGER_URL = "http://192.168.204.1:8119/v1.0"
+CGTS_INSECURE_SSL = os.environ.get("CGTS_INSECURE_SSL", "0") == "1"
+
 _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")
 
 
 def get_config_path():
@@ -60,6 +63,26 @@ def get_api_url():
     return f"https://{host}:{port}"
 
 
+def get_stx_url():
+    try:
+        return get_stx_client_args()["auth_url"]
+    except KeyError:
+        logger.error('Please source your RC file before execution, '
+                     'e.g.: `source ~/downloads/admin-rc.sh`')
+        sys.exit(1)
+
+
+def get_dc_manager_url():
+    auth_url = os.environ.get("DCMANAGER_OS_AUTH_URL", None)
+    if auth_url is None:
+        temp_url = get_stx_url()
+        u = urlparse(temp_url)
+        u = u._replace(netloc=f"{u.hostname}:{_DCMANAGER_URL_PORT}")
+        u = u._replace(path=_DCMANAGER_URL_PATH)
+        auth_url = u.geturl()
+    return auth_url
+
+
 def get_root_api_base():
     return "/"
 
@@ -129,17 +152,7 @@ def is_ipv6(address):
 
 def get_stx_access_info(region_name="RegionOne", subcloud_hostname: str = "",
                         sub_is_https: bool = False):
-    # authurl = os.environ.get("STX_AUTH_URL", "http://192.168.204.1:5000/v3")
-    # username = os.environ.get("STX_USERNAME", "admin")
-    # pswd = os.environ.get("STX_PASSWORD", "passwd1")
-    # stx_access_info = (authurl, username, pswd)
     try:
-        # client_args = dict(
-        #     auth_url=os.environ.get('OS_AUTH_URL', _DEFAULT_STX_URL),
-        #     username=os.environ.get('OS_USERNAME', "admin"),
-        #     api_key=os.environ.get('OS_PASSWORD', "fakepasswd1"),
-        #     project_name=os.environ.get('OS_PROJECT_NAME', "admin"),
-        # )
         client_args = get_stx_client_args()
     except KeyError:
         logger.error('Please source your RC file before execution, '
@@ -152,7 +165,7 @@ def get_stx_access_info(region_name="RegionOne", subcloud_hostname: str = "",
     if "" != subcloud_hostname:
         if is_ipv6(subcloud_hostname):
             subcloud_hostname = "[" + subcloud_hostname + "]"
-        orig_auth_url = urlparse(_DEFAULT_STX_URL)
+        orig_auth_url = urlparse(get_stx_url())
         new_auth_url = orig_auth_url._replace(
             netloc=orig_auth_url.netloc.replace(
                 orig_auth_url.hostname, subcloud_hostname))
@@ -163,7 +176,7 @@ def get_stx_access_info(region_name="RegionOne", subcloud_hostname: str = "",
             new_auth_url = new_auth_url._replace(
                 scheme=new_auth_url.scheme.
                 replace(new_auth_url.scheme, 'https'))
-            os_client_args['insecure'] = True
+            os_client_args['insecure'] = CGTS_INSECURE_SSL
         os_client_args['os_auth_url'] = new_auth_url.geturl()
         os_client_args['os_endpoint_type'] = 'public'
     # os_client_args['system_url'] = os_client_args['os_auth_url']
@@ -177,12 +190,6 @@ def get_stx_access_info(region_name="RegionOne", subcloud_hostname: str = "",
 
 def get_dc_access_info():
     try:
-        # client_args = dict(
-        #     auth_url=os.environ.get('OS_AUTH_URL', _DEFAULT_STX_URL),
-        #     username=os.environ.get('OS_USERNAME', "admin"),
-        #     api_key=os.environ.get('OS_PASSWORD', "fakepasswd1"),
-        #     project_name=os.environ.get('OS_PROJECT_NAME', "admin"),
-        # )
         client_args = get_stx_client_args()
     except KeyError:
         logger.error('Please source your RC file before execution, '
@@ -195,7 +202,7 @@ def get_dc_access_info():
     auth_url = urlparse(os_client_args.pop('os_auth_url'))
     hostname = f"[{auth_url.hostname}]" if is_ipv6(auth_url.hostname) \
         else auth_url.hostname
-    dcmanager_url = urlparse(_DEFAULT_DCMANAGER_URL)
+    dcmanager_url = urlparse(get_dc_manager_url())
     dcmanager_url = dcmanager_url._replace(netloc=dcmanager_url.netloc.replace(
         dcmanager_url.hostname, hostname))
 
@@ -213,12 +220,6 @@ def get_dc_access_info():
 def get_fm_access_info(subcloud_hostname: str = "",
                        sub_is_https: bool = False):
     try:
-        # client_args = dict(
-        #     auth_url=os.environ.get('OS_AUTH_URL', _DEFAULT_STX_URL),
-        #     username=os.environ.get('OS_USERNAME', "admin"),
-        #     api_key=os.environ.get('OS_PASSWORD', "fakepasswd1"),
-        #     project_name=os.environ.get('OS_PROJECT_NAME', "admin"),
-        # )
         client_args = get_stx_client_args()
     except KeyError:
         logger.error('Please source your RC file before execution, '
@@ -235,7 +236,7 @@ def get_fm_access_info(subcloud_hostname: str = "",
     if "" != subcloud_hostname:
         subcloud_hostname = f"[{subcloud_hostname}]" if \
             is_ipv6(subcloud_hostname) else subcloud_hostname
-        orig_auth_url = urlparse(_DEFAULT_STX_URL)
+        orig_auth_url = urlparse(get_stx_url())
         new_auth_url = orig_auth_url._replace(
             netloc=orig_auth_url.netloc.replace(
                 orig_auth_url.hostname, subcloud_hostname))
@@ -246,7 +247,7 @@ def get_fm_access_info(subcloud_hostname: str = "",
         os_client_args['auth_url'] = new_auth_url.geturl()
         os_client_args['endpoint_type'] = 'publicURL'
 
-    os_client_args['insecure'] = True
+    os_client_args['insecure'] = CGTS_INSECURE_SSL
 
     os_client_args['username'] = os_client_args.pop('os_username')
     os_client_args['password'] = os_client_args.pop('os_api_key')
@@ -385,7 +386,7 @@ def get_reviewer_token():
 
 
 def get_auth_provider():
-    return 'k8s'
+    return config.conf.auth_provider
 
 
 def get_dms_support_profiles():
index ebfe816..5986ad5 100644 (file)
@@ -1,4 +1,4 @@
-# Copyright (C) 2022 Wind River Systems, Inc.
+# Copyright (C) 2022-2024 Wind River Systems, Inc.
 #
 #  Licensed under the Apache License, Version 2.0 (the "License");
 #  you may not use this file except in compliance with the License.
index c9e1e95..74abe57 100644 (file)
@@ -21,7 +21,7 @@ from cgtsclient.client import get_client as get_stx_client
 from cgtsclient.exc import EndpointException
 from dcmanagerclient.api.client import client as get_dc_client
 from fmclient.client import get_client as get_fm_client
-from fmclient.common.exceptions import HTTPNotFound
+from fmclient.common.exceptions import HTTPNotFound, HttpServerError
 
 from o2app.adapter import unit_of_work
 from o2common.config import config
@@ -85,6 +85,11 @@ class StxAlarmClient(BaseClient):
                 logger.debug('alarm {} not in this resource pool {}'
                              .format(alarm, self._pool_id))
                 continue
+            except HttpServerError:
+                # TODO(jon): This exception needs to be removed when the
+                # INF-457 related FM client upgrade and issue fix occur.
+                logger.debug('alarm {} query failed'.format(alarm))
+                continue
             ret.append(event)
 
         return ret
@@ -92,6 +97,9 @@ class StxAlarmClient(BaseClient):
     def _set_stx_client(self):
         self.driver.setFaultClient(self._pool_id)
 
+    def delete(self, id) -> alarmModel.FaultGenericModel:
+        return self.driver.deleteAlarm(id)
+
 
 class StxEventClient(BaseClient):
     def __init__(self, uow: unit_of_work.AbstractUnitOfWork, driver=None):
@@ -152,7 +160,7 @@ class StxFaultClientImp(object):
         try:
             sub_is_https = False
             os_client_args = config.get_stx_access_info(
-                region_name=subcloud[0].name,
+                region_name=subcloud[0].region_name,
                 subcloud_hostname=subcloud[0].oam_floating_ip)
             stx_client = get_stx_client(**os_client_args)
         except EndpointException as e:
@@ -160,7 +168,8 @@ class StxFaultClientImp(object):
             if CGTSCLIENT_ENDPOINT_ERROR_MSG in msg:
                 sub_is_https = True
                 os_client_args = config.get_stx_access_info(
-                    region_name=subcloud[0].name, sub_is_https=sub_is_https,
+                    region_name=subcloud[0].region_name,
+                    sub_is_https=sub_is_https,
                     subcloud_hostname=subcloud[0].oam_floating_ip)
                 stx_client = get_stx_client(**os_client_args)
             else:
@@ -194,8 +203,8 @@ class StxFaultClientImp(object):
         alarms = self.fmclient.alarm.list(expand=True)
         if len(alarms) == 0:
             return []
-        logger.debug('alarm 1:' + str(alarms[0].to_dict()))
-        # [print('alarm:' + str(alarm.to_dict())) for alarm in alarms if alarm]
+        [logger.debug(
+            'alarm:' + str(alarm.to_dict())) for alarm in alarms if alarm]
         return [alarmModel.FaultGenericModel(
             alarmModel.EventTypeEnum.ALARM, self._alarmconverter(alarm))
             for alarm in alarms if alarm]
@@ -203,7 +212,8 @@ class StxFaultClientImp(object):
     def getAlarmInfo(self, id) -> alarmModel.FaultGenericModel:
         try:
             alarm = self.fmclient.alarm.get(id)
-            logger.debug('get alarm id ' + id + ':' + str(alarm.to_dict()))
+            logger.debug(
+                'get alarm id: ' + id + ', result:' + str(alarm.to_dict()))
         except HTTPNotFound:
             event = self.fmclient.event_log.get(id)
             return alarmModel.FaultGenericModel(
@@ -212,10 +222,16 @@ class StxFaultClientImp(object):
         return alarmModel.FaultGenericModel(
             alarmModel.EventTypeEnum.ALARM, self._alarmconverter(alarm))
 
+    def deleteAlarm(self, id) -> alarmModel.FaultGenericModel:
+        alarm = self.fmclient.alarm.delete(id)
+        logger.debug('delete alarm id ' + id + ':' + str(alarm.to_dict()))
+        return alarmModel.FaultGenericModel(
+            alarmModel.EventTypeEnum.ALARM, self._alarmconverter(alarm))
+
     def getEventList(self, **filters) -> List[alarmModel.FaultGenericModel]:
         events = self.fmclient.event_log.list(alarms=True, expand=True)
-        logger.debug('event 1:' + str(events[0].to_dict()))
-        # [print('alarm:' + str(event.to_dict())) for event in events if event]
+        [logger.debug(
+            'alarm:' + str(event.to_dict())) for event in events if event]
         return [alarmModel.FaultGenericModel(
             alarmModel.EventTypeEnum.EVENT, self._eventconverter(event))
             for event in events if event]
@@ -236,8 +252,8 @@ class StxFaultClientImp(object):
 
     def getSuppressionList(self, alarm_id) -> alarmModel.FaultGenericModel:
         suppression_list = []
-        queryAsArray = []
-        events = self.fmclient.event_suppression.list(q=queryAsArray)
+        query_as_array = []
+        events = self.fmclient.event_suppression.list(q=query_as_array)
         for event in events:
             if event.alarm_id == alarm_id:
                 # logger.debug('suppression event:' + str(event.to_dict()))
index 4e8430f..5a7b3fb 100644 (file)
@@ -301,7 +301,7 @@ class StxClientImp(object):
                 subcloud_stxclient = self.getSubcloudClient(
                     subcloud.subcloud_id)
                 systems = subcloud_stxclient.isystem.list()
-                logger.debug('systems:' + str(systems[0].to_dict()))
+                logger.debug('subcloud system:' + str(systems[0].to_dict()))
                 pools.append(systems[0])
             except Exception as ex:
                 logger.warning('Failed get cgstclient of subcloud %s: %s' %
index 050e7b0..800c6d7 100644 (file)
@@ -125,6 +125,14 @@ class ClearingTypeEnum(str, Enum):
     MANUAL = 'MANUAL'
 
 
+class AlarmEventRecordModifications(AgRoot):
+    def __init__(self, ack: bool = None,
+                 clear: PerceivedSeverityEnum = None) -> None:
+        super().__init__()
+        self.alarmAcknowledged = ack
+        self.perceivedSeverity = clear
+
+
 class AlarmDefinition(AgRoot, Serializer):
     def __init__(self, id: str, name: str, change_type: AlarmChangeTypeEnum,
                  desc: str, prop_action: str, clearing_type: ClearingTypeEnum,
@@ -170,9 +178,8 @@ class AlarmNotificationEventEnum(str, Enum):
 
 class AlarmEvent2SMO(Serializer):
     def __init__(self, eventtype: AlarmNotificationEventEnum,
-                 id: str, ref: str, updatetime: str) -> None:
+                 id: str, updatetime: str) -> None:
         self.notificationEventType = eventtype
-        self.objectRef = ref
         self.id = id
         self.updatetime = updatetime
 
index 1ef92a4..ca6929e 100644 (file)
@@ -1,4 +1,4 @@
-# Copyright (C) 2022-2024 Wind River Systems, Inc.
+# Copyright (C) 2022 Wind River Systems, Inc.
 #
 #  Licensed under the Apache License, Version 2.0 (the "License");
 #  you may not use this file except in compliance with the License.
index 2847a85..5f6ed35 100644 (file)
@@ -41,10 +41,6 @@ def update_alarm(
 
         alarm_event_record = uow.alarm_event_records.get(fmobj.id)
         if not alarm_event_record:
-            logger.info("add alarm event record:" + fmobj.name
-                        + " update_at: " + str(fmobj.updatetime)
-                        + " id: " + str(fmobj.id)
-                        + " hash: " + str(fmobj.hash))
             localmodel = create_by(fmobj)
             content = json.loads(fmobj.content)
             entity_type_id = content['entity_type_id']
@@ -65,6 +61,14 @@ def update_alarm(
                     extensions = json.loads(host.extensions)
                     if extensions['hostname'] == hostname:
                         localmodel.resourceId = host.resourceId
+                        break
+                else:
+                    # Example would be when alarm has host=controller
+                    # TODO: Handle host=controller better
+                    logger.warning(
+                        'Couldnt match alarm event '
+                        f'to hostname for: {content}')
+                    return
                 uow.alarm_event_records.add(localmodel)
                 logger.info("Add the alarm event record: " + fmobj.id
                             + ", name: " + fmobj.name)
index 390cfe2..335488a 100644 (file)
 
 import json
 
-from o2common.service.unit_of_work import AbstractUnitOfWork
 from o2common.adapter.notifications import AbstractNotifications
-
-from o2ims.adapter.clients.fault_client import StxEventClient
-from o2ims.domain import commands, alarm_obj
-
+from o2common.config import conf
+from o2common.domain.filter import gen_orm_filter
 from o2common.helper import o2logging
+from o2common.service.unit_of_work import AbstractUnitOfWork
+from o2ims.adapter.clients.fault_client import StxAlarmClient
+from o2ims.domain import commands, alarm_obj
 logger = o2logging.get_logger(__name__)
 
 
@@ -29,23 +29,104 @@ def purge_alarm_event(
     uow: AbstractUnitOfWork,
     notifications: AbstractNotifications,
 ):
-    logger.debug('In purge_alarm_event')
-    fault_client = StxEventClient(uow)
+    """
+    Purges an alarm event and notifies relevant subscribers.
+
+    This method performs the following steps:
+    1. Retrieves data from the command object and initializes the fault client.
+    2. Uses the Unit of Work pattern to find and delete the corresponding
+       alarm event record.
+    3. Updates the alarm event record's hash, extensions, changed time,
+       and perceived severity.
+    4. Commits the changes to the database.
+    5. Finds and processes all alarm subscriptions, deciding whether to
+       send notifications based on subscription filters.
+
+    Parameters:
+    - cmd (commands.PubAlarm2SMO): Command object containing the alarm
+      event data.
+    - uow (AbstractUnitOfWork): Unit of Work object for managing
+      database transactions.
+    - notifications (AbstractNotifications): Abstract notifications
+      object for sending notifications.
+
+    Exceptions:
+    - Any exceptions that might occur during database operations or
+      notification sending.
+    """
+    fault_client = StxAlarmClient(uow)
     data = cmd.data
     with uow:
         alarm_event_record = uow.alarm_event_records.get(data.id)
-        alarm_id = json.loads(alarm_event_record.extensions).get('alarm_id')
-
-        events = fault_client.suppression_list(alarm_id)
-        for event_id in events:
-            event = fault_client.suppress(event_id.id)
-            alarm_event_record.hash = event.hash
-            alarm_event_record.extensions = json.dumps(event.filtered)
-            alarm_event_record.alarmChangedTime = event.updatetime.\
-                strftime("%Y-%m-%dT%H:%M:%S")
-            alarm_event_record.perceivedSeverity = \
-                alarm_obj.PerceivedSeverityEnum.CLEARED
-
-            uow.alarm_event_records.update(alarm_event_record)
-            break
+        alarm = fault_client.delete(alarm_event_record.alarmEventRecordId)
+        alarm_event_record.hash = alarm.hash
+        alarm_event_record.extensions = json.dumps(alarm.filtered)
+        alarm_event_record.alarmChangedTime = alarm.updatetime.\
+            strftime("%Y-%m-%dT%H:%M:%S")
+        alarm_event_record.perceivedSeverity = \
+            alarm_obj.PerceivedSeverityEnum.CLEARED
+
+        uow.alarm_event_records.update(alarm_event_record)
+
         uow.commit()
+
+        alarm = uow.alarm_event_records.get(data.id)
+        subs = uow.alarm_subscriptions.list()
+        for sub in subs:
+            sub_data = sub.serialize()
+            logger.debug('Alarm Subscription: {}'.format(
+                sub_data['alarmSubscriptionId']))
+
+            if not sub_data.get('filter', None):
+                callback_smo(notifications, sub, data, alarm)
+                continue
+            try:
+                args = gen_orm_filter(alarm_obj.AlarmEventRecord,
+                                      sub_data['filter'])
+            except KeyError:
+                logger.warning(
+                    'Alarm Subscription {} filter {} has wrong attribute '
+                    'name or value. Ignore the filter'.format(
+                        sub_data['alarmSubscriptionId'],
+                        sub_data['filter']))
+                callback_smo(notifications, sub, data, alarm)
+                continue
+            args.append(alarm_obj.AlarmEventRecord.
+                        alarmEventRecordId == data.id)
+            count, _ = uow.alarm_event_records.list_with_count(*args)
+            if count != 0:
+                logger.debug(
+                    'Alarm Event {} skip for subscription {} because of '
+                    'the filter.'
+                    .format(data.id, sub_data['alarmSubscriptionId']))
+                continue
+            callback_smo(notifications, sub, data, alarm)
+
+
+def callback_smo(notifications: AbstractNotifications,
+                 sub: alarm_obj.AlarmSubscription,
+                 msg: alarm_obj.AlarmEvent2SMO,
+                 alarm: alarm_obj.AlarmEventRecord):
+    sub_data = sub.serialize()
+    alarm_data = alarm.serialize()
+    callback = {
+        'globalCloudID': conf.DEFAULT.ocloud_global_id,
+        'consumerSubscriptionId': sub_data['consumerSubscriptionId'],
+        'notificationEventType': msg.notificationEventType,
+        'objectRef': msg.objectRef,
+        'alarmEventRecordId': alarm_data['alarmEventRecordId'],
+        'resourceTypeID': alarm_data['resourceTypeId'],
+        'resourceID': alarm_data['resourceId'],
+        'alarmDefinitionID': alarm_data['alarmDefinitionId'],
+        'probableCauseID': alarm_data['probableCauseId'],
+        'alarmRaisedTime': alarm_data['alarmRaisedTime'],
+        'alarmChangedTime': alarm_data['alarmChangedTime'],
+        'alarmAcknowledgeTime': alarm_data['alarmAcknowledgeTime'],
+        'alarmAcknowledged': alarm_data['alarmAcknowledged'],
+        'perceivedSeverity': alarm_data['perceivedSeverity'],
+        'extensions': json.loads(alarm_data['extensions'])
+    }
+    logger.info('callback URL: {}'.format(sub_data['callback']))
+    logger.debug('callback data: {}'.format(json.dumps(callback)))
+
+    return notifications.send(sub_data['callback'], callback)
index ff005d6..78807c7 100644 (file)
@@ -24,7 +24,6 @@ def notify_alarm_event_change(
     event: events.AlarmEventChanged,
     publish: Callable,
 ):
-    logger.debug('In notify_alarm_event_change')
     publish("AlarmEventChanged", event)
     logger.debug("published Alarm Event Changed: {}".format(
         event.id))
@@ -34,7 +33,6 @@ def notify_alarm_event_purge(
     event: events.AlarmEventPurged,
     publish: Callable,
 ):
-    logger.debug('In notify_alarm_event_purge')
     publish("AlarmEventPurged", event)
     logger.debug("published Alarm Event Purged: {}".format(
         event.id))
index 3028a84..caef205 100644 (file)
@@ -129,6 +129,22 @@ class AlarmDTO:
         # 'alarmAcknowledgeTime,alarmAcknowledged,extensions}'
     )
 
+    alarm_event_record_patch = api_monitoring_v1.model(
+        "AlarmPatchDto",
+        {
+            'alarmAcknowledged': fields.Boolean(
+                example=True,
+                description='Boolean value indication of a management ' +
+                'system has acknowledged the alarm.'),
+            'perceivedSeverity': fields.String(
+                example='5',
+                description='indicate that the alarm record is requested ' +
+                'to be cleared. Only the value "5" for "CLEARED" is ' +
+                'permitted in a request message content. ')
+        },
+        mask='{alarmAcknowledged,}'
+    )
+
 
 class SubscriptionDTO:
 
index 8b0af04..7b0c03e 100644 (file)
@@ -19,6 +19,7 @@ from o2common.service.messagebus import MessageBus
 from o2common.views.pagination_route import link_header, PAGE_PARAM
 from o2common.views.route_exception import NotFoundException, \
     BadRequestException
+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, \
@@ -130,6 +131,7 @@ class AlarmListRouter(Resource):
 class AlarmGetRouter(Resource):
 
     model = AlarmDTO.alarm_event_record_get
+    patch = AlarmDTO.alarm_event_record_patch
 
     @api_monitoring_v1.doc('Get Alarm Event Record Information')
     @api_monitoring_v1.marshal_with(model)
@@ -141,11 +143,35 @@ class AlarmGetRouter(Resource):
             "Alarm Event Record {} doesn't exist".format(alarmEventRecordId))
 
     @api_monitoring_v1.doc('Patch Alarm Event Record Information')
-    @api_monitoring_v1.marshal_with(model)
+    @api_monitoring_v1.expect(patch)
+    @api_monitoring_v1.marshal_with(patch)
     def patch(self, alarmEventRecordId):
-        result = alarm_view.alarm_event_record_ack(alarmEventRecordId, bus.uow)
-        if result is not None:
-            return result
+        data = api_monitoring_v1.payload
+        ack_action = data.get('alarmAcknowledged', None)
+        clear_action = data.get('perceivedSeverity', None)
+
+        ack_is_none = ack_action is None
+        clear_is_none = clear_action is None
+        if (ack_is_none and clear_is_none) or (not ack_is_none and
+                                               not clear_is_none):
+            raise BadRequestException('Either "alarmAcknowledged" or '
+                                      '"perceivedSeverity" shall be included '
+                                      'in a request, but not both.')
+        if ack_action:
+            result = alarm_view.alarm_event_record_ack(alarmEventRecordId,
+                                                       bus.uow)
+            if result is not None:
+                return result
+        elif clear_action:
+            if clear_action != PerceivedSeverityEnum.CLEARED:
+                raise BadRequestException(
+                    'Only the value "5" for "CLEARED" is permitted of '
+                    '"perceivedSeverity".')
+
+            result = alarm_view.alarm_event_record_clear(alarmEventRecordId,
+                                                         bus.uow)
+            if result is not None:
+                return result
         raise NotFoundException(
             "Alarm Event Record {} doesn't exist".format(alarmEventRecordId))
 
index 9f7af04..00d4a20 100644 (file)
@@ -24,7 +24,8 @@ from o2common.views.route_exception import BadRequestException, \
 from o2ims.domain import events
 from o2ims.views.alarm_dto import SubscriptionDTO
 from o2ims.domain.alarm_obj import AlarmSubscription, AlarmEventRecord, \
-    AlarmNotificationEventEnum
+    AlarmNotificationEventEnum, AlarmEventRecordModifications, \
+    PerceivedSeverityEnum
 
 from o2common.helper import o2logging
 # from o2common.config import config
@@ -52,26 +53,43 @@ def alarm_event_record_ack(alarmEventRecordId: str,
                            uow: unit_of_work.AbstractUnitOfWork):
     with uow:
         alarm_event_record = uow.alarm_event_records.get(alarmEventRecordId)
+        # Check the record does not exist, return None. Otherwise, the
+        # acknowledge request will update the record even if it is
+        # acknowledged.
         if alarm_event_record is None:
             return None
-        elif alarm_event_record.alarmAcknowledged == 'true':
-            raise BadRequestException(
-                "Alarm Event Record {} has already been acknowledged."
-                .format(alarmEventRecordId))
         alarm_event_record.alarmAcknowledged = True
         alarm_event_record.alarmAcknowledgeTime = datetime.\
             now().strftime("%Y-%m-%dT%H:%M:%S")
-        bus = messagebus.MessageBus.get_instance()
+        uow.alarm_event_records.update(alarm_event_record)
+        uow.commit()
+
+        result = AlarmEventRecordModifications(True)
+    return result
+
+
+def alarm_event_record_clear(alarmEventRecordId: str,
+                             uow: unit_of_work.AbstractUnitOfWork):
+    with uow:
+        alarm_event_record = uow.alarm_event_records.get(alarmEventRecordId)
+        if alarm_event_record is None:
+            return None
+        elif alarm_event_record.perceivedSeverity == \
+                PerceivedSeverityEnum.CLEARED:
+            raise BadRequestException(
+                "Alarm Event Record {} has already been marked as CLEARED."
+                .format(alarmEventRecordId))
         alarm_event_record.events.append(events.AlarmEventPurged(
             id=alarm_event_record.alarmEventRecordId,
-            notificationEventType=AlarmNotificationEventEnum.ACKNOWLEDGE,
+            notificationEventType=AlarmNotificationEventEnum.CLEAR,
             updatetime=alarm_event_record.alarmAcknowledgeTime))
 
         uow.alarm_event_records.update(alarm_event_record)
         uow.commit()
 
-        result = alarm_event_record.serialize()
-    _handle_events(bus)
+        result = AlarmEventRecordModifications(
+            clear=PerceivedSeverityEnum.CLEARED)
+    _handle_events(messagebus.MessageBus.get_instance())
     return result
 
 
index 88effb5..9f4f9f6 100644 (file)
@@ -2,6 +2,7 @@
 # -e git+https://opendev.org/starlingx/config.git@master#egg=cgtsclient&subdirectory=sysinv/cgts-client/cgts-client
 # -e git+https://opendev.org/starlingx/fault.git@master#egg=fmclient&subdirectory=python-fmclient/fmclient
 
--e git+https://opendev.org/starlingx/distcloud-client.git@eb4e7eeeb09bdf2e1b80984b378c5a8ea9930f04#egg=distributedcloud-client&subdirectory=distributedcloud-client
--e git+https://opendev.org/starlingx/config.git@r/stx.7.0#egg=cgtsclient&subdirectory=sysinv/cgts-client/cgts-client
--e git+https://opendev.org/starlingx/fault.git@r/stx.7.0#egg=fmclient&subdirectory=python-fmclient/fmclient
+-e git+https://opendev.org/starlingx/distcloud-client.git@b4a8ec19dc6078952a3762d7eee8d426d520a1f0#egg=distributedcloud-client&subdirectory=distributedcloud-client
+# Updated to lastest commit at May 24th 2024
+-e git+https://opendev.org/starlingx/config.git@04271215320b8ab9824be837ac5aae07883d363b#egg=cgtsclient&subdirectory=sysinv/cgts-client/cgts-client
+-e git+https://opendev.org/starlingx/fault.git@b213c9155b2323831143977447f41efc2f84b76a#egg=fmclient&subdirectory=python-fmclient/fmclient
\ No newline at end of file
index 3462bae..5d34a0c 100644 (file)
@@ -24,3 +24,6 @@ ruamel.yaml==0.17.17
 pyOpenSSL
 
 gunicorn
+
+# Import JWT to support OAuth2
+pyjwt==2.6.0