From d1a23a3c286be30f84e30e7a47dc48ddce0628f0 Mon Sep 17 00:00:00 2001 From: Koji Shimizu Date: Wed, 24 Aug 2022 08:10:22 +0000 Subject: [PATCH] Add support cnf auto heal and scale Support container based VNF AutoHeal and AutoScale operation with External Monitoring Tools. Add the Fault Management interfaces and CLI to support AutoHeal. Add the Performance Management interfaces and CLI to support AutoScale. The Fault Management and Performance Management interfaces are based on ETSI NFV-SOL 002 v3.3.1 and ETSI NFV-SOL 003 v3.3.1, which are Version "2.0.0" API of Tacker. Add the Prometheus Plugin that has a interface between tacker and Prometheus as a sample of External Monitoring Tool. Implements: blueprint support-auto-lcm Change-Id: Ib74305f0b1da4eb8d996ebae400e75902aaa1321 --- .zuul.yaml | 6 + etc/tacker/api-paste.ini | 30 + playbooks/devstack/pre.yaml | 2 + ...eal-scale-with-tools-96ff8266ae9d31af.yaml | 17 + .../tasks/main.yaml | 39 + .../alembic_migrations/versions/HEAD | 2 +- .../de8d835ae776_add_pm_and_fm_table.py | 92 ++ tacker/policies/__init__.py | 4 + tacker/sol_refactored/api/api_version.py | 24 + .../sol_refactored/api/policies/vnffm_v1.py | 110 +++ .../sol_refactored/api/policies/vnfpm_v2.py | 132 +++ .../api/prometheus_plugin_router.py | 43 + .../api/prometheus_plugin_validator.py | 46 + .../api/prometheus_plugin_wsgi.py | 68 ++ tacker/sol_refactored/api/router.py | 28 + .../api/schemas/common_types.py | 35 +- .../api/schemas/prometheus_plugin_schemas.py | 87 ++ tacker/sol_refactored/api/schemas/vnffm_v1.py | 173 ++++ tacker/sol_refactored/api/schemas/vnfpm_v2.py | 86 ++ tacker/sol_refactored/api/wsgi.py | 4 + tacker/sol_refactored/common/config.py | 31 +- tacker/sol_refactored/common/coordinate.py | 33 + tacker/sol_refactored/common/exceptions.py | 55 ++ .../sol_refactored/common/fm_alarm_utils.py | 86 ++ .../common/fm_subscription_utils.py | 174 ++++ .../common/monitoring_plugin_base.py | 70 ++ tacker/sol_refactored/common/pm_job_utils.py | 175 ++++ .../common/prometheus_plugin.py | 869 ++++++++++++++++++ .../common/subscription_utils.py | 19 +- .../conductor/conductor_rpc_v2.py | 28 + .../sol_refactored/conductor/conductor_v2.py | 17 + .../conductor/prometheus_plugin_driver.py | 63 ++ tacker/sol_refactored/conductor/v2_hook.py | 3 + .../conductor/vnffm_driver_v1.py | 50 + .../conductor/vnfpm_driver_v2.py | 58 ++ .../prometheus_plugin_controller.py | 57 ++ tacker/sol_refactored/controller/vnffm_v1.py | 200 ++++ .../sol_refactored/controller/vnffm_view.py | 65 ++ tacker/sol_refactored/controller/vnfpm_v2.py | 297 ++++++ .../sol_refactored/controller/vnfpm_view.py | 54 ++ tacker/sol_refactored/db/sqlalchemy/models.py | 85 ++ tacker/sol_refactored/nfvo/nfvo_client.py | 24 + tacker/sol_refactored/objects/__init__.py | 12 + tacker/sol_refactored/objects/v1/alarm.py | 84 ++ .../objects/v1/alarm_cleared_notification.py | 50 + .../objects/v1/alarm_notification.py | 47 + tacker/sol_refactored/objects/v1/fields.py | 57 ++ .../objects/v1/fm_notifications_filter.py | 47 + .../objects/v1/fm_subscription.py | 52 ++ .../objects/v1/fm_subscription_request.py | 34 + .../objects/v2/create_pm_job_request.py | 36 + ...ance_information_available_notification.py | 62 ++ tacker/sol_refactored/objects/v2/pm_job.py | 75 ++ .../objects/v2/pm_job_criteria.py | 34 + .../objects/v2/pm_job_modification.py | 32 + tacker/sol_refactored/objects/v2/pm_report.py | 65 ++ tacker/tests/functional/common/fake_server.py | 23 +- .../vnflcm_v2/base_v2.py | 4 + .../functional/sol_kubernetes_v2/base_v2.py | 139 +++ .../functional/sol_kubernetes_v2/paramgen.py | 325 +++++++ .../samples/tacker-monitoring-test.zip | Bin 0 -> 4873 bytes .../test_prometheus_auto_scaling.py | 115 +++ .../sol_kubernetes_v2/test_vnffm_basic.py | 440 +++++++++ .../sol_kubernetes_v2/test_vnfpm_basic.py | 345 +++++++ .../sol_refactored/api/test_api_version.py | 25 +- .../api/test_prometheus_plugin.py | 88 ++ .../common/test_fm_alarm_utils.py | 102 ++ .../common/test_fm_subscription_utils.py | 266 ++++++ .../common/test_pm_job_utils.py | 318 +++++++ .../common/test_prometheus_plugin.py | 774 ++++++++++++++++ .../common/test_subscription_utils.py | 18 +- .../conductor/test_prometheus_plugin.py | 162 ++++ .../conductor/test_vnffm_driver_v1.py | 69 ++ .../conductor/test_vnfpm_driver_v2.py | 113 +++ .../controller/test_prometheus_plugin.py | 380 ++++++++ .../controller/test_vnffm_v1.py | 205 +++++ .../controller/test_vnfpm_v2.py | 433 +++++++++ .../controller/test_vnfpm_view.py | 73 ++ .../sol_refactored/nfvo/test_nfvo_client.py | 20 + .../sol_refactored/samples/fakes_for_fm.py | 112 +++ test-requirements.txt | 1 + tools/test-setup-fake-prometheus-server.sh | 14 + 82 files changed, 8771 insertions(+), 21 deletions(-) create mode 100644 releasenotes/notes/support-cnf-auto-heal-scale-with-tools-96ff8266ae9d31af.yaml create mode 100644 roles/setup-fake-prometheus-server/tasks/main.yaml create mode 100644 tacker/db/migration/alembic_migrations/versions/de8d835ae776_add_pm_and_fm_table.py create mode 100644 tacker/sol_refactored/api/policies/vnffm_v1.py create mode 100644 tacker/sol_refactored/api/policies/vnfpm_v2.py create mode 100644 tacker/sol_refactored/api/prometheus_plugin_router.py create mode 100644 tacker/sol_refactored/api/prometheus_plugin_validator.py create mode 100644 tacker/sol_refactored/api/prometheus_plugin_wsgi.py create mode 100644 tacker/sol_refactored/api/schemas/prometheus_plugin_schemas.py create mode 100644 tacker/sol_refactored/api/schemas/vnffm_v1.py create mode 100644 tacker/sol_refactored/api/schemas/vnfpm_v2.py create mode 100644 tacker/sol_refactored/common/fm_alarm_utils.py create mode 100644 tacker/sol_refactored/common/fm_subscription_utils.py create mode 100644 tacker/sol_refactored/common/monitoring_plugin_base.py create mode 100644 tacker/sol_refactored/common/pm_job_utils.py create mode 100644 tacker/sol_refactored/common/prometheus_plugin.py create mode 100644 tacker/sol_refactored/conductor/prometheus_plugin_driver.py create mode 100644 tacker/sol_refactored/conductor/vnffm_driver_v1.py create mode 100644 tacker/sol_refactored/conductor/vnfpm_driver_v2.py create mode 100644 tacker/sol_refactored/controller/prometheus_plugin_controller.py create mode 100644 tacker/sol_refactored/controller/vnffm_v1.py create mode 100644 tacker/sol_refactored/controller/vnffm_view.py create mode 100644 tacker/sol_refactored/controller/vnfpm_v2.py create mode 100644 tacker/sol_refactored/controller/vnfpm_view.py create mode 100644 tacker/sol_refactored/objects/v1/alarm.py create mode 100644 tacker/sol_refactored/objects/v1/alarm_cleared_notification.py create mode 100644 tacker/sol_refactored/objects/v1/alarm_notification.py create mode 100644 tacker/sol_refactored/objects/v1/fm_notifications_filter.py create mode 100644 tacker/sol_refactored/objects/v1/fm_subscription.py create mode 100644 tacker/sol_refactored/objects/v1/fm_subscription_request.py create mode 100644 tacker/sol_refactored/objects/v2/create_pm_job_request.py create mode 100644 tacker/sol_refactored/objects/v2/performance_information_available_notification.py create mode 100644 tacker/sol_refactored/objects/v2/pm_job.py create mode 100644 tacker/sol_refactored/objects/v2/pm_job_criteria.py create mode 100644 tacker/sol_refactored/objects/v2/pm_job_modification.py create mode 100644 tacker/sol_refactored/objects/v2/pm_report.py create mode 100644 tacker/tests/functional/sol_kubernetes_v2/samples/tacker-monitoring-test.zip create mode 100644 tacker/tests/functional/sol_kubernetes_v2/test_prometheus_auto_scaling.py create mode 100644 tacker/tests/functional/sol_kubernetes_v2/test_vnffm_basic.py create mode 100644 tacker/tests/functional/sol_kubernetes_v2/test_vnfpm_basic.py create mode 100644 tacker/tests/unit/sol_refactored/api/test_prometheus_plugin.py create mode 100644 tacker/tests/unit/sol_refactored/common/test_fm_alarm_utils.py create mode 100644 tacker/tests/unit/sol_refactored/common/test_fm_subscription_utils.py create mode 100644 tacker/tests/unit/sol_refactored/common/test_pm_job_utils.py create mode 100644 tacker/tests/unit/sol_refactored/common/test_prometheus_plugin.py create mode 100644 tacker/tests/unit/sol_refactored/conductor/test_prometheus_plugin.py create mode 100644 tacker/tests/unit/sol_refactored/conductor/test_vnffm_driver_v1.py create mode 100644 tacker/tests/unit/sol_refactored/conductor/test_vnfpm_driver_v2.py create mode 100644 tacker/tests/unit/sol_refactored/controller/test_prometheus_plugin.py create mode 100644 tacker/tests/unit/sol_refactored/controller/test_vnffm_v1.py create mode 100644 tacker/tests/unit/sol_refactored/controller/test_vnfpm_v2.py create mode 100644 tacker/tests/unit/sol_refactored/controller/test_vnfpm_view.py create mode 100644 tacker/tests/unit/sol_refactored/samples/fakes_for_fm.py create mode 100644 tools/test-setup-fake-prometheus-server.sh diff --git a/.zuul.yaml b/.zuul.yaml index 8a8e3c70f..e45de86e9 100644 --- a/.zuul.yaml +++ b/.zuul.yaml @@ -561,7 +561,13 @@ $TACKER_CONF: v2_vnfm: kubernetes_vim_rsc_wait_timeout: 800 + prometheus_plugin: + fault_management: True + performance_management: True + auto_scaling: True tox_envlist: dsvm-functional-sol-kubernetes-v2 + vars: + prometheus_setup: true - job: name: tacker-functional-devstack-kubernetes-oidc-auth diff --git a/etc/tacker/api-paste.ini b/etc/tacker/api-paste.ini index 1a89a24b5..eab8ca381 100644 --- a/etc/tacker/api-paste.ini +++ b/etc/tacker/api-paste.ini @@ -6,6 +6,11 @@ use = egg:Paste#urlmap /vnflcm: vnflcm_versions /vnflcm/v1: vnflcm_v1 /vnflcm/v2: vnflcm_v2 +/vnffm/v1: vnffm_v1 +/vnfpm/v2: vnfpm_v2 +/alert/vnf_instances: prometheus_auto_scaling +/alert: prometheus_fm +/pm_event: prometheus_pm [composite:tackerapi_v1_0] use = call:tacker.auth:pipeline_factory @@ -27,11 +32,21 @@ use = call:tacker.auth:pipeline_factory noauth = request_id catch_errors vnflcmaapp_v2 keystone = request_id catch_errors authtoken keystonecontext vnflcmaapp_v2 +[composite:vnfpm_v2] +use = call:tacker.auth:pipeline_factory +noauth = request_id catch_errors vnfpmaapp_v2 +keystone = request_id catch_errors authtoken keystonecontext vnfpmaapp_v2 + [composite:vnflcm_versions] use = call:tacker.auth:pipeline_factory noauth = request_id catch_errors vnflcm_api_versions keystone = request_id catch_errors authtoken keystonecontext vnflcm_api_versions +[composite:vnffm_v1] +use = call:tacker.auth:pipeline_factory +noauth = request_id catch_errors vnffmaapp_v1 +keystone = request_id catch_errors authtoken keystonecontext vnffmaapp_v1 + [filter:request_id] paste.filter_factory = oslo_middleware:RequestId.factory @@ -65,5 +80,20 @@ paste.app_factory = tacker.api.vnflcm.v1.router:VnflcmAPIRouter.factory [app:vnflcmaapp_v2] paste.app_factory = tacker.sol_refactored.api.router:VnflcmAPIRouterV2.factory +[app:vnfpmaapp_v2] +paste.app_factory = tacker.sol_refactored.api.router:VnfPmAPIRouterV2.factory + [app:vnflcm_api_versions] paste.app_factory = tacker.sol_refactored.api.router:VnflcmVersions.factory + +[app:vnffmaapp_v1] +paste.app_factory = tacker.sol_refactored.api.router:VnffmAPIRouterV1.factory + +[app:prometheus_auto_scaling] +paste.app_factory = tacker.sol_refactored.api.prometheus_plugin_router:AutoScalingRouter.factory + +[app:prometheus_fm] +paste.app_factory = tacker.sol_refactored.api.prometheus_plugin_router:FmAlertRouter.factory + +[app:prometheus_pm] +paste.app_factory = tacker.sol_refactored.api.prometheus_plugin_router:PmEventRouter.factory diff --git a/playbooks/devstack/pre.yaml b/playbooks/devstack/pre.yaml index cb8626bb8..20a8abbbd 100644 --- a/playbooks/devstack/pre.yaml +++ b/playbooks/devstack/pre.yaml @@ -6,6 +6,8 @@ - setup-k8s-oidc - setup-default-vim - setup-helm + - role: setup-fake-prometheus-server + when: prometheus_setup is defined and prometheus_setup | bool - role: setup-multi-tenant-vim when: setup_multi_tenant is defined and setup_multi_tenant | bool - role: bindep diff --git a/releasenotes/notes/support-cnf-auto-heal-scale-with-tools-96ff8266ae9d31af.yaml b/releasenotes/notes/support-cnf-auto-heal-scale-with-tools-96ff8266ae9d31af.yaml new file mode 100644 index 000000000..cc874da96 --- /dev/null +++ b/releasenotes/notes/support-cnf-auto-heal-scale-with-tools-96ff8266ae9d31af.yaml @@ -0,0 +1,17 @@ +--- +features: + - | + Support container based VNF AutoHeal and AutoScale operation with External + Monitoring Tools. + Add the Fault Management interfaces and CLI to support AutoHeal. AutoHeal + supports two modes, Polling Mode and Notification Mode. + Add the Performance Management interfaces and CLI to support AutoScale. + The Fault Management and Performance Management interfaces are based on + ETSI NFV-SOL 002 v3.3.1 and ETSI NFV-SOL 003 v3.3.1, which are Version + "2.0.0" API of Tacker. + Add the Prometheus Plugin that has a interface between tacker and + the External Monitoring Tool. Prometheus Plugin supports data model + conversion from Prometheus format data to SOL based FM/PM schema, + and vice versa. + At the same time, a user guide is also made to help users understand the + function. diff --git a/roles/setup-fake-prometheus-server/tasks/main.yaml b/roles/setup-fake-prometheus-server/tasks/main.yaml new file mode 100644 index 000000000..e3c77a77b --- /dev/null +++ b/roles/setup-fake-prometheus-server/tasks/main.yaml @@ -0,0 +1,39 @@ +- block: + - name: Copy tools/test-setup-fake-prometheus-server.sh + copy: + remote_src=True + src={{ devstack_base_dir }}/tacker/tools/test-setup-fake-prometheus-server.sh + dest={{ zuul_work_dir }}/tools/test-setup-fake-prometheus-server.sh + mode=0755 + + - name: Get stackenv from devstack environment + slurp: + src: "{{ devstack_base_dir }}/devstack/.stackenv" + register: stackenv + + - name: Install docker + package: + name: docker.io + state: present + become: yes + + - name: Replace prometheus host ip + replace: + path: "{{ item }}" + regexp: "0.0.0.0" + replace: "{{ hostvars['controller-tacker']['nodepool']['private_ipv4'] }}" + with_items: + - "{{ zuul_work_dir }}/tools/test-setup-fake-prometheus-server.sh" + when: + - p.stat.exists + + - name: Run tools/test-setup-fake-prometheus-server.sh + command: tools/test-setup-fake-prometheus-server.sh + args: + chdir: "{{ zuul_work_dir }}" + when: + - p.stat.exists + - p.stat.executable + + when: + - inventory_hostname == 'controller-tacker' \ No newline at end of file diff --git a/tacker/db/migration/alembic_migrations/versions/HEAD b/tacker/db/migration/alembic_migrations/versions/HEAD index 5b6351bf0..5e3156ff5 100644 --- a/tacker/db/migration/alembic_migrations/versions/HEAD +++ b/tacker/db/migration/alembic_migrations/versions/HEAD @@ -1 +1 @@ -85c06a0714b7 +de8d835ae776 diff --git a/tacker/db/migration/alembic_migrations/versions/de8d835ae776_add_pm_and_fm_table.py b/tacker/db/migration/alembic_migrations/versions/de8d835ae776_add_pm_and_fm_table.py new file mode 100644 index 000000000..a3e8621b8 --- /dev/null +++ b/tacker/db/migration/alembic_migrations/versions/de8d835ae776_add_pm_and_fm_table.py @@ -0,0 +1,92 @@ +# Copyright 2022 OpenStack Foundation +# +# 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. +# + +"""add_pm_and_fm_table + +Revision ID: de8d835ae776 +Revises: 85c06a0714b7 +Create Date: 2022-07-21 17:34:45.675428 + +""" + +# flake8: noqa: E402 + +# revision identifiers, used by Alembic. +revision = 'de8d835ae776' +down_revision = '85c06a0714b7' + +from alembic import op +import sqlalchemy as sa + + +def upgrade(active_plugins=None, options=None): + op.create_table('AlarmV1', + sa.Column('id', sa.String(length=255), nullable=False), + sa.Column('managedObjectId', sa.String(length=255), nullable=False), + sa.Column('vnfcInstanceIds', sa.JSON(), nullable=True), + sa.Column('rootCauseFaultyResource', sa.JSON(), nullable=True), + sa.Column('alarmRaisedTime', sa.DateTime(), nullable=False), + sa.Column('alarmChangedTime', sa.DateTime(), nullable=True), + sa.Column('alarmClearedTime', sa.DateTime(), nullable=True), + sa.Column('alarmAcknowledgedTime', sa.DateTime(), nullable=True), + sa.Column('ackState', sa.Enum( + 'UNACKNOWLEDGED', 'ACKNOWLEDGED'), nullable=False), + sa.Column('perceivedSeverity', sa.Enum( + 'CRITICAL', 'MAJOR', 'MINOR', 'WARNING', + 'INDETERMINATE', 'CLEARED'), nullable=False), + sa.Column('eventTime', sa.DateTime(), nullable=False), + sa.Column('eventType', sa.Enum( + 'COMMUNICATIONS_ALARM', 'PROCESSING_ERROR_ALARM', + 'ENVIRONMENTAL_ALARM', 'QOS_ALARM', + 'EQUIPMENT_ALARM'), nullable=False), + sa.Column('faultType', sa.String(length=255), nullable=True), + sa.Column('probableCause', sa.String(length=255), nullable=False), + sa.Column('isRootCause', sa.Boolean(), nullable=False), + sa.Column('correlatedAlarmIds', sa.JSON(), nullable=True), + sa.Column('faultDetails', sa.JSON(), nullable=True), + sa.PrimaryKeyConstraint('id'), + mysql_engine='InnoDB' + ) + + op.create_table('FmSubscriptionV1', + sa.Column('id', sa.String(length=255), nullable=False), + sa.Column('filter', sa.JSON(), nullable=True), + sa.Column('callbackUri', sa.String(length=255), nullable=False), + sa.Column('authentication', sa.JSON(), nullable=True), + sa.PrimaryKeyConstraint('id'), + mysql_engine='InnoDB' + ) + + op.create_table('PmJobV2', + sa.Column('id', sa.String(length=255), nullable=False), + sa.Column('objectType', sa.String(length=32), nullable=False), + sa.Column('objectInstanceIds', sa.JSON(), nullable=False), + sa.Column('subObjectInstanceIds', sa.JSON(), nullable=True), + sa.Column('criteria', sa.JSON(), nullable=False), + sa.Column('callbackUri', sa.String(length=255), nullable=False), + sa.Column('reports', sa.JSON(), nullable=True), + sa.Column('authentication', sa.JSON(), nullable=True), + sa.Column('metadata', sa.JSON(), nullable=True), + sa.PrimaryKeyConstraint('id'), + mysql_engine='InnoDB' + ) + + op.create_table('PerformanceReportV2', + sa.Column('id', sa.String(length=255), nullable=False), + sa.Column('jobId', sa.String(length=255), nullable=False), + sa.Column('entries', sa.JSON(), nullable=False), + sa.PrimaryKeyConstraint('id'), + mysql_engine='InnoDB' + ) diff --git a/tacker/policies/__init__.py b/tacker/policies/__init__.py index 7960069d2..e414664f2 100644 --- a/tacker/policies/__init__.py +++ b/tacker/policies/__init__.py @@ -19,7 +19,9 @@ import itertools from tacker.policies import base from tacker.policies import vnf_lcm from tacker.policies import vnf_package +from tacker.sol_refactored.api.policies import vnffm_v1 from tacker.sol_refactored.api.policies import vnflcm_v2 +from tacker.sol_refactored.api.policies import vnfpm_v2 def list_rules(): @@ -28,4 +30,6 @@ def list_rules(): vnf_package.list_rules(), vnf_lcm.list_rules(), vnflcm_v2.list_rules(), + vnffm_v1.list_rules(), + vnfpm_v2.list_rules(), ) diff --git a/tacker/sol_refactored/api/api_version.py b/tacker/sol_refactored/api/api_version.py index 6ae08e72b..9bb2cf72c 100644 --- a/tacker/sol_refactored/api/api_version.py +++ b/tacker/sol_refactored/api/api_version.py @@ -33,7 +33,23 @@ supported_versions_v2 = { ] } +supported_fm_versions_v1 = { + 'uriPrefix': '/vnffm/v1', + 'apiVersions': [ + {'version': '1.3.0', 'isDeprecated': False} + ] +} + +supported_pm_versions_v2 = { + 'uriPrefix': '/vnfpm/v2', + 'apiVersions': [ + {'version': '2.1.0', 'isDeprecated': False} + ] +} + CURRENT_VERSION = '2.0.0' +CURRENT_FM_VERSION = '1.3.0' +CURRENT_PM_VERSION = '2.1.0' v1_versions = [ item['version'] for item in supported_versions_v1['apiVersions'] @@ -43,6 +59,14 @@ v2_versions = [ item['version'] for item in supported_versions_v2['apiVersions'] ] +v1_fm_versions = [ + item['version'] for item in supported_fm_versions_v1['apiVersions'] +] + +v2_pm_versions = [ + item['version'] for item in supported_pm_versions_v2['apiVersions'] +] + class APIVersion(object): diff --git a/tacker/sol_refactored/api/policies/vnffm_v1.py b/tacker/sol_refactored/api/policies/vnffm_v1.py new file mode 100644 index 000000000..1017d16f7 --- /dev/null +++ b/tacker/sol_refactored/api/policies/vnffm_v1.py @@ -0,0 +1,110 @@ +# Copyright (C) 2022 Fujitsu +# All Rights Reserved. +# +# 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. + + +from oslo_policy import policy + + +POLICY_NAME = 'os_nfv_orchestration_api_v2:vnf_fault_monitor:{}' +RULE_ANY = '@' + +V1_PATH = '/vnffm/v1' +ALARMS_PATH = V1_PATH + '/alarms' +ALARMS_ID_PATH = ALARMS_PATH + '/{alarmId}' +SUBSCRIPTIONS_PATH = V1_PATH + '/subscriptions' +SUBSCRIPTIONS_ID_PATH = SUBSCRIPTIONS_PATH + '/{subscriptionId}' + +POLICY_NAME_PROM_PLUGIN = 'tacker_PROM_PLUGIN_api:PROM_PLUGIN:{}' +PROM_PLUGIN_FM_PATH = '/alert' + +rules = [ + policy.DocumentedRuleDefault( + name=POLICY_NAME.format('index'), + check_str=RULE_ANY, + description="Query FM alarms.", + operations=[ + {'method': 'GET', + 'path': ALARMS_PATH} + ] + ), + policy.DocumentedRuleDefault( + name=POLICY_NAME.format('show'), + check_str=RULE_ANY, + description="Query an Individual FM alarm.", + operations=[ + {'method': 'GET', + 'path': ALARMS_ID_PATH} + ] + ), + policy.DocumentedRuleDefault( + name=POLICY_NAME.format('update'), + check_str=RULE_ANY, + description="Modify FM alarm information.", + operations=[ + {'method': 'PATCH', + 'path': ALARMS_ID_PATH} + ] + ), + # NOTE: add when the operation supported + policy.DocumentedRuleDefault( + name=POLICY_NAME.format('subscription_create'), + check_str=RULE_ANY, + description="Create subscription.", + operations=[ + {'method': 'POST', + 'path': SUBSCRIPTIONS_PATH} + ] + ), + policy.DocumentedRuleDefault( + name=POLICY_NAME.format('subscription_list'), + check_str=RULE_ANY, + description="List subscription.", + operations=[ + {'method': 'GET', + 'path': SUBSCRIPTIONS_PATH} + ] + ), + policy.DocumentedRuleDefault( + name=POLICY_NAME.format('subscription_show'), + check_str=RULE_ANY, + description="Show subscription.", + operations=[ + {'method': 'GET', + 'path': SUBSCRIPTIONS_ID_PATH} + ] + ), + policy.DocumentedRuleDefault( + name=POLICY_NAME.format('subscription_delete'), + check_str=RULE_ANY, + description="Delete subscription.", + operations=[ + {'method': 'DELETE', + 'path': SUBSCRIPTIONS_ID_PATH} + ] + ), + policy.DocumentedRuleDefault( + name=POLICY_NAME_PROM_PLUGIN.format('alert'), + check_str=RULE_ANY, + description="Receive the alert sent from External Monitoring Tool", + operations=[ + {'method': 'POST', + 'path': PROM_PLUGIN_FM_PATH} + ] + ) +] + + +def list_rules(): + return rules diff --git a/tacker/sol_refactored/api/policies/vnfpm_v2.py b/tacker/sol_refactored/api/policies/vnfpm_v2.py new file mode 100644 index 000000000..a81d1d269 --- /dev/null +++ b/tacker/sol_refactored/api/policies/vnfpm_v2.py @@ -0,0 +1,132 @@ +# Copyright (C) 2022 Fujitsu +# All Rights Reserved. +# +# 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. + + +from oslo_policy import policy + + +POLICY_NAME = 'os_nfv_orchestration_api_v2:vnf_performance_management:{}' +RULE_ANY = '@' + +V2_PATH = '/vnfpm/v2' +PM_JOB_PATH = V2_PATH + '/pm_jobs' +PM_JOB_ID_PATH = PM_JOB_PATH + '/{pmJobId}' +REPORT_GET = '/vnfpm/v2/pm_jobs/{id}/reports/{report_id}' + +POLICY_NAME_PROM_PLUGIN = 'tacker_PROM_PLUGIN_api:PROM_PLUGIN:{}' +PROM_PLUGIN_PM_PATH = '/pm_event' +PROM_PLUGIN_AUTO_SCALING_PATH = '/alert/vnf_instances' + +rules = [ + policy.DocumentedRuleDefault( + name=POLICY_NAME.format('create'), + check_str=RULE_ANY, + description="Create a PM job.", + operations=[ + { + 'method': 'POST', + 'path': PM_JOB_PATH + } + ] + ), + policy.DocumentedRuleDefault( + name=POLICY_NAME.format('index'), + check_str=RULE_ANY, + description="Query PM jobs.", + operations=[ + { + 'method': 'GET', + 'path': PM_JOB_PATH + } + ] + ), + policy.DocumentedRuleDefault( + name=POLICY_NAME.format('update'), + check_str=RULE_ANY, + description="Update a PM job.", + operations=[ + { + 'method': 'PATCH', + 'path': PM_JOB_ID_PATH + } + ] + ), + policy.DocumentedRuleDefault( + name=POLICY_NAME.format('show'), + check_str=RULE_ANY, + description="Get an individual PM job.", + operations=[ + { + 'method': 'GET', + 'path': PM_JOB_ID_PATH + } + ] + ), + policy.DocumentedRuleDefault( + name=POLICY_NAME.format('delete'), + check_str=RULE_ANY, + description="Delete a PM job.", + operations=[ + { + 'method': 'DELETE', + 'path': PM_JOB_ID_PATH + } + ] + ), + # Add new Rest API GET /vnfpm/v2/pm_jobs/{id}/reports/{report_id} to + # get the specified PM report. + policy.DocumentedRuleDefault( + name=POLICY_NAME.format('report_get'), + check_str=RULE_ANY, + description="Get an individual performance report.", + operations=[ + { + 'method': 'GET', + 'path': REPORT_GET + } + ] + ), + policy.DocumentedRuleDefault( + name=POLICY_NAME_PROM_PLUGIN.format('pm_event'), + check_str=RULE_ANY, + description="Receive the PM event sent from External Monitoring Tool", + operations=[ + {'method': 'POST', + 'path': PROM_PLUGIN_PM_PATH} + ] + ), + policy.DocumentedRuleDefault( + name=POLICY_NAME_PROM_PLUGIN.format('auto_scaling'), + check_str=RULE_ANY, + description="auto_scaling", + operations=[ + {'method': 'POST', + 'path': PROM_PLUGIN_AUTO_SCALING_PATH} + ] + ), + policy.DocumentedRuleDefault( + name=POLICY_NAME_PROM_PLUGIN.format('auto_scaling_id'), + check_str=RULE_ANY, + description="auto_scaling_id", + operations=[ + {'method': 'POST', + 'path': PROM_PLUGIN_AUTO_SCALING_PATH + '/{vnfInstanceId}'} + ] + ) +] + + +def list_rules(): + return rules diff --git a/tacker/sol_refactored/api/prometheus_plugin_router.py b/tacker/sol_refactored/api/prometheus_plugin_router.py new file mode 100644 index 000000000..6a44fe5e4 --- /dev/null +++ b/tacker/sol_refactored/api/prometheus_plugin_router.py @@ -0,0 +1,43 @@ +# Copyright (C) 2022 Fujitsu +# All Rights Reserved. +# +# 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. + +from tacker.sol_refactored.api.policies import vnffm_v1 as vnffm_policy_v1 +from tacker.sol_refactored.api.policies import vnfpm_v2 as vnfpm_policy_v2 +from tacker.sol_refactored.api import prometheus_plugin_wsgi as prom_wsgi +from tacker.sol_refactored.controller import prometheus_plugin_controller + + +class PmEventRouter(prom_wsgi.PrometheusPluginAPIRouter): + controller = prom_wsgi.PrometheusPluginResource( + prometheus_plugin_controller.PmEventController(), + policy_name=vnfpm_policy_v2.POLICY_NAME_PROM_PLUGIN) + route_list = [("", {"POST": "pm_event"})] + + +class FmAlertRouter(prom_wsgi.PrometheusPluginAPIRouter): + controller = prom_wsgi.PrometheusPluginResource( + prometheus_plugin_controller.FmAlertController(), + policy_name=vnffm_policy_v1.POLICY_NAME_PROM_PLUGIN) + route_list = [("", {"POST": "alert"})] + + +class AutoScalingRouter(prom_wsgi.PrometheusPluginAPIRouter): + controller = prom_wsgi.PrometheusPluginResource( + prometheus_plugin_controller.AutoScalingController(), + policy_name=vnfpm_policy_v2.POLICY_NAME_PROM_PLUGIN) + route_list = [ + ("", {"POST": "auto_scaling"}), + ("/{id}", {"POST": "auto_scaling"}) + ] diff --git a/tacker/sol_refactored/api/prometheus_plugin_validator.py b/tacker/sol_refactored/api/prometheus_plugin_validator.py new file mode 100644 index 000000000..4b2863859 --- /dev/null +++ b/tacker/sol_refactored/api/prometheus_plugin_validator.py @@ -0,0 +1,46 @@ +# Copyright (C) 2022 Fujitsu +# All Rights Reserved. +# +# 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. + +import functools + +from tacker.api.validation import validators +from tacker.common import exceptions as tacker_ex + +from tacker.sol_refactored.common import exceptions as sol_ex + + +class PrometheusPluginSchemaValidator(validators._SchemaValidator): + def validate(self, *args, **kwargs): + try: + super(PrometheusPluginSchemaValidator, self).validate( + *args, **kwargs) + except tacker_ex.ValidationError as ex: + raise sol_ex.PrometheusPluginValidationError(detail=str(ex)) + + +def schema(request_body_schema): + def add_validator(func): + @functools.wraps(func) + def wrapper(*args, **kwargs): + if 'body' not in kwargs: + raise sol_ex.PrometheusPluginValidationError( + detail="body is missing.") + schema_validator = PrometheusPluginSchemaValidator( + request_body_schema) + schema_validator.validate(kwargs['body']) + + return func(*args, **kwargs) + return wrapper + return add_validator diff --git a/tacker/sol_refactored/api/prometheus_plugin_wsgi.py b/tacker/sol_refactored/api/prometheus_plugin_wsgi.py new file mode 100644 index 000000000..e64f08f9f --- /dev/null +++ b/tacker/sol_refactored/api/prometheus_plugin_wsgi.py @@ -0,0 +1,68 @@ +# Copyright (C) 2022 Fujitsu +# All Rights Reserved. +# +# 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. + +from oslo_log import log as logging +from tacker.sol_refactored.api import wsgi as sol_wsgi +import webob + +LOG = logging.getLogger(__name__) + + +class PrometheusPluginResponse(sol_wsgi.SolResponse): + allowed_headers = ['content_type'] + + def __init__(self, status, body, **kwargs): + self.status = status + self.body = body + self.headers = {} + for hdr in self.allowed_headers: + if hdr in kwargs: + self.headers[hdr] = kwargs[hdr] + + +class PrometheusPluginErrorResponse(sol_wsgi.SolErrorResponse): + pass + + +class PrometheusPluginResource(sol_wsgi.SolResource): + @webob.dec.wsgify(RequestClass=sol_wsgi.SolRequest) + def __call__(self, request): + LOG.info("%(method)s %(url)s", {"method": request.method, + "url": request.url}) + try: + action, args, accept = self._deserialize_request(request) + self._check_policy(request, action) + result = self._dispatch(request, action, args) + response = result.serialize(accept) + except Exception as ex: + result = PrometheusPluginErrorResponse(ex, request) + try: + response = result.serialize('application/problem+json') + except Exception: + LOG.exception("Unknown error") + return webob.exc.HTTPBadRequest(explanation="Unknown error") + + LOG.info("%(url)s returned with HTTP %(status)d", + {"url": request.url, "status": response.status_int}) + + return response + + +class PrometheusPluginAPIRouter(sol_wsgi.SolAPIRouter): + pass + + +class PrometheusPluginAPIController(sol_wsgi.SolAPIController): + pass diff --git a/tacker/sol_refactored/api/router.py b/tacker/sol_refactored/api/router.py index 36ae18502..3ac28e47c 100644 --- a/tacker/sol_refactored/api/router.py +++ b/tacker/sol_refactored/api/router.py @@ -14,10 +14,14 @@ # under the License. +from tacker.sol_refactored.api.policies import vnffm_v1 as vnffm_policy_v1 from tacker.sol_refactored.api.policies import vnflcm_v2 as vnflcm_policy_v2 +from tacker.sol_refactored.api.policies import vnfpm_v2 as vnfpm_policy_v2 from tacker.sol_refactored.api import wsgi as sol_wsgi +from tacker.sol_refactored.controller import vnffm_v1 from tacker.sol_refactored.controller import vnflcm_v2 from tacker.sol_refactored.controller import vnflcm_versions +from tacker.sol_refactored.controller import vnfpm_v2 class VnflcmVersions(sol_wsgi.SolAPIRouter): @@ -57,3 +61,27 @@ class VnflcmAPIRouterV2(sol_wsgi.SolAPIRouter): ("/vnf_lcm_op_occs/{id}", {"GET": "lcm_op_occ_show", "DELETE": "lcm_op_occ_delete"}) ] + + +class VnffmAPIRouterV1(sol_wsgi.SolAPIRouter): + controller = sol_wsgi.SolResource(vnffm_v1.VnfFmControllerV1(), + policy_name=vnffm_policy_v1.POLICY_NAME) + route_list = [ + ("/alarms", {"GET": "index"}), + ("/alarms/{id}", {"GET": "show", "PATCH": "update"}), + ("/subscriptions", {"GET": "subscription_list", + "POST": "subscription_create"}), + ("/subscriptions/{id}", {"GET": "subscription_show", + "DELETE": "subscription_delete"}) + ] + + +class VnfPmAPIRouterV2(sol_wsgi.SolAPIRouter): + controller = sol_wsgi.SolResource(vnfpm_v2.VnfPmControllerV2(), + policy_name=vnfpm_policy_v2.POLICY_NAME) + route_list = [ + ("/pm_jobs", {"POST": "create", "GET": "index"}), + ("/pm_jobs/{id}", { + "PATCH": "update", "GET": "show", "DELETE": "delete"}), + ("/pm_jobs/{id}/reports/{report_id}", {"GET": "report_get"}), + ] diff --git a/tacker/sol_refactored/api/schemas/common_types.py b/tacker/sol_refactored/api/schemas/common_types.py index 67523df92..0db86b656 100644 --- a/tacker/sol_refactored/api/schemas/common_types.py +++ b/tacker/sol_refactored/api/schemas/common_types.py @@ -16,7 +16,6 @@ from tacker.api.validation import parameter_types - # SOL013 7.2.2 Identifier = { 'type': 'string', 'minLength': 1, 'maxLength': 255 @@ -123,6 +122,40 @@ _IpAddresses = { 'additionalProperties': True } +# SOL013 8.3.4 +SubscriptionAuthentication = { + 'type': 'object', + 'properties': { + 'authType': { + 'type': 'array', + 'items': { + 'type': 'string', + 'enum': [ + 'BASIC', + 'OAUTH2_CLIENT_CREDENTIALS', + 'TLS_CERT'] + } + }, + 'paramsBasic': { + 'type': 'object', + 'properties': { + 'userName': {'type': 'string'}, + 'password': {'type': 'string'} + } + }, + 'paramsOauth2ClientCredentials': { + 'type': 'object', + 'properties': { + 'clientId': {'type': 'string'}, + 'clientPassword': {'type': 'string'}, + 'tokenEndpoint': {'type': 'string'} + } + } + }, + 'required': ['authType'], + 'additionalProperties': True, +} + # SOL003 4.4.1.10c IpOverEthernetAddressData = { 'type': 'object', diff --git a/tacker/sol_refactored/api/schemas/prometheus_plugin_schemas.py b/tacker/sol_refactored/api/schemas/prometheus_plugin_schemas.py new file mode 100644 index 000000000..f683409bb --- /dev/null +++ b/tacker/sol_refactored/api/schemas/prometheus_plugin_schemas.py @@ -0,0 +1,87 @@ +# Copyright (C) 2022 Fujitsu +# All Rights Reserved. +# +# 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. + + +Alert = { + 'type': 'object', + 'status': { + 'type': 'string', + 'enum': ['firing', 'resolved'] + }, + 'properties': { + 'status': { + 'type': 'string', + 'enum': ['firing', 'resolved'] + }, + 'labels': { + 'type': 'object', + 'properties': { + 'receiver_type': { + 'type': 'string', + 'enum': ['tacker'] + }, + 'function_type': { + 'type': 'string', + 'enum': ['vnffm', 'vnfpm', 'auto_scale'] + }, + 'job_id': {'type': 'string'}, + 'object_instance_id': {'type': 'string'}, + 'vnf_instance_id': {'type': 'string'}, + 'node': {'type': 'string'}, + 'perceived_severity': { + 'type': 'string', + 'enum': ['CRITICAL', 'MAJOR', 'MINOR', 'WARNING', + 'INDETERMINATE', 'CLEARED'] + }, + 'event_type': {'type': 'string'}, + 'auto_scale_type': { + 'type': 'string', + 'enum': ['SCALE_OUT', 'SCALE_IN'] + }, + 'aspect_id': {'type': 'string'} + }, + 'required': ['receiver_type', 'function_type'], + 'additionalProperties': True + }, + 'annotations': { + 'type': 'object', + 'properties': { + 'value': {'type': 'number'}, + 'probable_cause': {'type': 'string'}, + 'fault_type': {'type': 'string'}, + 'fault_details': {'type': 'string'} + }, + 'required': [], + 'additionalProperties': True + }, + 'startsAt': {'type': 'string'}, + 'endsAt': {'type': 'string'}, + 'fingerprint': {'type': 'string'} + }, + 'required': ['status', 'labels', 'annotations', 'startsAt', + 'fingerprint'], + 'additionalProperties': True +} + +AlertMessage = { + 'type': 'object', + 'properties': { + 'alerts': { + 'type': 'array', + 'items': Alert + } + }, + 'required': ['alerts'] +} diff --git a/tacker/sol_refactored/api/schemas/vnffm_v1.py b/tacker/sol_refactored/api/schemas/vnffm_v1.py new file mode 100644 index 000000000..a36c4dbba --- /dev/null +++ b/tacker/sol_refactored/api/schemas/vnffm_v1.py @@ -0,0 +1,173 @@ +# Copyright (C) 2022 Fujitsu +# All Rights Reserved. +# +# 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. + +from tacker.sol_refactored.api.schemas import common_types + + +# SOL003 7.5.2.8 +AlarmModifications_V130 = { + 'type': 'object', + 'properties': { + 'ackState': { + 'type': 'string', + 'enum': ['ACKNOWLEDGED', 'UNACKNOWLEDGED'] + } + }, + 'required': ['ackState'], + 'additionalProperties': True, +} + +# SOL003 4.4.1.5 inner +_VnfProductVersions = { + 'type': 'array', + 'items': { + 'type': 'objects', + 'properties': { + 'vnfSoftwareVersion': {'type': 'string'}, + 'vnfdVersions': { + 'type': 'array', + 'items': {'type': 'string'} + } + }, + 'required': ['vnfSoftwareVersion'], + 'additionalProperties': True, + } +} + +# SOL003 4.4.1.5 inner +_VnfProducts = { + 'type': 'array', + 'items': { + 'type': 'object', + 'properties': { + 'vnfProductName': {'type': 'string'}, + 'versions': _VnfProductVersions + }, + 'required': ['vnfProductName'], + 'additionalProperties': True, + } +} + +# SOL003 4.4.1.5 inner +_VnfProductsFromProviders = { + 'type': 'array', + 'items': { + 'type': 'object', + 'properties': { + 'type': 'object', + 'properties': { + 'vnfProvider': {'type': 'string'}, + 'vnfProducts': _VnfProducts + } + }, + 'required': ['vnfProvider'], + 'additionalProperties': True, + } +} + +# SOL003 4.4.1.5 +_VnfInstanceSubscriptionFilter = { + 'type': 'object', + 'properties': { + 'vnfdIds': { + 'type': 'array', + 'items': common_types.Identifier + }, + 'vnfProductsFromProviders': _VnfProductsFromProviders, + 'vnfInstanceIds': { + 'type': 'array', + 'items': common_types.Identifier + }, + 'vnfInstanceNames': { + 'type': 'array', + 'items': {'type': 'string'} + } + }, + 'additionalProperties': True, +} + +# SOL003 7.5.3.2 +_FmNotificationsFilter = { + 'type': 'object', + 'properties': { + 'vnfInstanceSubscriptionFilter': _VnfInstanceSubscriptionFilter, + 'notificationTypes': { + 'type': 'array', + 'items': { + 'type': 'string', + 'enum': [ + 'AlarmNotification', + 'AlarmClearedNotification', + 'AlarmListRebuiltNotification'] + } + }, + 'faultyResourceTypes': { + 'type': 'array', + 'items': { + 'type': 'string', + 'enum': [ + 'COMPUTE', + 'STORAGE', + 'NETWORK'] + } + }, + 'perceivedSeverities': { + 'type': 'array', + 'items': { + 'type': 'string', + 'enum': [ + 'CRITICAL', + 'MAJOR', + 'MINOR', + 'WARNING', + 'INDETERMINATE', + 'CLEARED'] + } + }, + 'eventTypes': { + 'type': 'array', + 'items': { + 'type': 'string', + 'enum': [ + 'COMMUNICATIONS_ALARM', + 'PROCESSING_ERROR_ALARM', + 'ENVIRONMENTAL_ALARM', + 'QOS_ALARM', + 'EQUIPMENT_ALARM'] + } + }, + 'probableCauses': { + 'type': 'array', + 'items': {'type': 'string'} + } + }, + 'additionalProperties': True, +} + +# SOL003 7.5.2.2 +FmSubscriptionRequest_V130 = { + 'type': 'object', + 'properties': { + 'filter': _FmNotificationsFilter, + 'callbackUri': {'type': 'string', 'maxLength': 255}, + 'authentication': common_types.SubscriptionAuthentication, + 'verbosity': { + 'type': 'string', + 'enum': ['FULL', 'SHORT'] + } + }, + 'required': ['callbackUri'], + 'additionalProperties': True, +} diff --git a/tacker/sol_refactored/api/schemas/vnfpm_v2.py b/tacker/sol_refactored/api/schemas/vnfpm_v2.py new file mode 100644 index 000000000..84322055f --- /dev/null +++ b/tacker/sol_refactored/api/schemas/vnfpm_v2.py @@ -0,0 +1,86 @@ +# Copyright (C) 2022 Fujitsu +# All Rights Reserved. +# +# 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. + +from tacker.sol_refactored.api.schemas import common_types + +# SOL003 6.5.3.3 +_PmJobCriteria_V210 = { + 'type': 'object', + 'properties': { + 'performanceMetric': { + 'type': 'array', + 'items': {'type:': 'string'} + }, + 'performanceMetricGroup': { + 'type': 'array', + 'items': {'type:': 'string'} + }, + 'collectionPeriod': {'type': 'integer'}, + 'reportingPeriod': {'type': 'integer'}, + 'reportingBoundary': {'type': 'string'} + }, + 'anyOf': [ + {'required': ['performanceMetric']}, + {'required': ['performanceMetricGroup']} + ], + 'required': ['collectionPeriod', 'reportingPeriod'], + 'additionalProperties': True, +} + +# SOL003 6.5.2.6 +CreatePmJobRequest_V210 = { + 'type': 'object', + 'properties': { + 'objectType': { + 'type': 'string', + 'enum': [ + # TODO(YiFeng): Currently, this API only supports CNF, and + # supports the following types. When VNF is supported, + # the types can be extended. + 'Vnf', + 'Vnfc', + 'VnfIntCP', + 'VnfExtCP'] + }, + 'objectInstanceIds': { + 'type': 'array', + 'items': common_types.Identifier + }, + 'subObjectInstanceIds': { + 'type': 'array', + 'items': common_types.IdentifierInVnf + }, + 'criteria': _PmJobCriteria_V210, + 'callbackUri': {'type': 'string'}, + 'authentication': common_types.SubscriptionAuthentication, + }, + 'required': ['objectType', 'objectInstanceIds', 'criteria', 'callbackUri'], + 'additionalProperties': True, +} + +# SOL003 6.5.2.12 +PmJobModificationsRequest_V210 = { + 'type': 'object', + 'properties': { + 'callbackUri': {'type': 'string'}, + 'authentication': common_types.SubscriptionAuthentication + }, + 'anyOf': [ + {'required': ['callbackUri']}, + {'required': ['authentication']} + ], + 'required': [], + 'additionalProperties': True, +} diff --git a/tacker/sol_refactored/api/wsgi.py b/tacker/sol_refactored/api/wsgi.py index 6d0cd985a..c5713e53d 100644 --- a/tacker/sol_refactored/api/wsgi.py +++ b/tacker/sol_refactored/api/wsgi.py @@ -133,6 +133,10 @@ class SolResource(object): response = result.serialize(accept) except Exception as ex: result = SolErrorResponse(ex, request.best_match_language()) + if type(self.controller).__name__ == 'VnfFmControllerV1': + result.headers['version'] = api_version.CURRENT_FM_VERSION + if type(self.controller).__name__ == 'VnfPmControllerV2': + result.headers['version'] = api_version.CURRENT_PM_VERSION try: response = result.serialize('application/problem+json') except Exception: diff --git a/tacker/sol_refactored/common/config.py b/tacker/sol_refactored/common/config.py index 49b26f891..6ad675a07 100644 --- a/tacker/sol_refactored/common/config.py +++ b/tacker/sol_refactored/common/config.py @@ -56,6 +56,14 @@ VNFM_OPTS = [ 'connection error when sending a notification. ' 'Period between retries is exponential starting ' '0.5 seconds up to a maximum of 60 seconds.')), + cfg.IntOpt('vnffm_alarm_page_size', + default=0, # 0 means no paging + help=_('Paged response size of the query result ' + 'for VNF Fault Management alarm.')), + cfg.IntOpt('vnfpm_pmjob_page_size', + default=0, # 0 means no paging + help=_('Paged response size of the query result for ' + 'VNF PM job.')), # NOTE: This is for test use since it is convenient to be able to delete # under development. cfg.BoolOpt('test_enable_lcm_op_occ_delete', @@ -104,7 +112,28 @@ NFVO_OPTS = [ CONF.register_opts(NFVO_OPTS, 'v2_nfvo') +PROMETHEUS_PLUGIN_OPTS = [ + cfg.BoolOpt('performance_management', + default=False, + help=_('Enable prometheus plugin performance management')), + + cfg.IntOpt('reporting_period_margin', + default=1, + help=_('Some margin time for PM jos\'s reportingPeriod')), + + cfg.BoolOpt('fault_management', + default=False, + help=_('Enable prometheus plugin fault management')), + + cfg.BoolOpt('auto_scaling', + default=False, + help=_('Enable prometheus plugin autoscaling')), +] + +CONF.register_opts(PROMETHEUS_PLUGIN_OPTS, 'prometheus_plugin') + def config_opts(): return [('v2_nfvo', NFVO_OPTS), - ('v2_vnfm', VNFM_OPTS)] + ('v2_vnfm', VNFM_OPTS), + ('prometheus_plugin', PROMETHEUS_PLUGIN_OPTS)] diff --git a/tacker/sol_refactored/common/coordinate.py b/tacker/sol_refactored/common/coordinate.py index d0eaee361..83d527ba7 100644 --- a/tacker/sol_refactored/common/coordinate.py +++ b/tacker/sol_refactored/common/coordinate.py @@ -67,3 +67,36 @@ def lock_vnf_instance(inst_arg, delay=False): return wrapper return operation_lock + + +def lock_resources(res_arg, delay=False): + + def operation_lock(func): + @functools.wraps(func) + def wrapper(*args, **kwargs): + coord = coordination.COORDINATOR + # ensure coordination start + # NOTE: it is noop if already started. + coord.start() + + sig = inspect.signature(func) + call_args = sig.bind(*args, **kwargs).arguments + res_id = res_arg.format(**call_args) + lock = coord.get_lock(res_id) + + blocking = False if not delay else 10 + # NOTE: 'with lock' is not used since it can't handle + # lock failed exception well. + if not lock.acquire(blocking=blocking): + LOG.debug("Locking resources %s failed.", res_id) + raise sol_ex.ResourcesOtherOperationInProgress(inst_id=res_id) + + try: + LOG.debug("resources %s locked.", res_id) + return func(*args, **kwargs) + finally: + lock.release() + + return wrapper + + return operation_lock diff --git a/tacker/sol_refactored/common/exceptions.py b/tacker/sol_refactored/common/exceptions.py index fa5fb8f44..95385fae8 100644 --- a/tacker/sol_refactored/common/exceptions.py +++ b/tacker/sol_refactored/common/exceptions.py @@ -92,6 +92,11 @@ class SolHttpError422(SolException): title = 'Unprocessable Entity' +class SolHttpError503(SolException): + status = 503 + title = 'Service Unavailable' + + class MethodNotAllowed(SolHttpError405): message = _("Method %(method)s is not supported.") @@ -372,3 +377,53 @@ class HelmOperationFailed(SolHttpError422): class HelmParameterNotFound(SolHttpError400): message = _("Helm parameter for scale vdu %(vdu_name)s is not found.") + + +class AlarmNotFound(SolHttpError404): + message = _("Alarm %(alarm_id)s not found.") + + +class AckStateInvalid(SolHttpError409): + message = _("The ackState of alarm cannot specify the same value.") + + +class FmSubscriptionNotFound(SolHttpError404): + message = _("FmSubscription %(subsc_id)s not found.") + + +class PMJobNotExist(SolHttpError404): + message = _("The specified PM job does not exist.") + + +class PMReportNotExist(SolHttpError404): + message = _("The specified Performance Report does not exist.") + + +class PMJobInvalidRequest(SolHttpError400): + message = _("Invalid request") + + +class ResourcesOtherOperationInProgress(SolHttpError409): + message = _("Other LCM operation of resources %(inst_id)s " + "is in progress.") + + +# prometheus plugin +class PrometheusPluginNotEnabled(SolHttpError404): + message = _("%(name)s API is not enabled.") + + +class PrometheusPluginError(Exception): + pass + + +class PrometheusPluginSkipped(Exception): + pass + + +class PrometheusPluginValidationError(SolValidationError): + pass + + +class PrometheusSettingFailed(SolHttpError503): + message = _("Setting PM job on External Monitoring Tool failed.") diff --git a/tacker/sol_refactored/common/fm_alarm_utils.py b/tacker/sol_refactored/common/fm_alarm_utils.py new file mode 100644 index 000000000..01a59ac83 --- /dev/null +++ b/tacker/sol_refactored/common/fm_alarm_utils.py @@ -0,0 +1,86 @@ +# Copyright (C) 2022 Fujitsu +# All Rights Reserved. +# +# 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. + +from datetime import datetime + +from oslo_log import log as logging +from oslo_utils import uuidutils + +from tacker.sol_refactored.common import exceptions as sol_ex +from tacker.sol_refactored.common import fm_subscription_utils as subsc_utils +from tacker.sol_refactored.common import vnf_instance_utils as inst_utils +from tacker.sol_refactored import objects + + +LOG = logging.getLogger(__name__) # not used at the moment + + +def get_alarm(context, alarm_id): + alarm = objects.AlarmV1.get_by_id(context, alarm_id) + if alarm is None: + raise sol_ex.AlarmNotFound(alarm_id=alarm_id) + return alarm + + +def get_alarms_all(context, marker=None): + return objects.AlarmV1.get_all(context, marker) + + +def get_not_cleared_alarms(context, inst_id): + return objects.AlarmV1.get_by_filter(context, managedObjectId=inst_id) + + +def alarm_href(alarm_id, endpoint): + return f"{endpoint}/vnffm/v1/alarms/{alarm_id}" + + +def make_alarm_links(alarm, endpoint): + links = objects.AlarmV1_Links() + links.self = objects.Link(href=alarm_href(alarm.id, endpoint)) + links.objectInstance = objects.Link( + href=inst_utils.inst_href(alarm.managedObjectId, endpoint)) + + return links + + +def make_alarm_notif_data(subsc, alarm, endpoint): + if alarm.obj_attr_is_set('alarmClearedTime'): + notif_data = objects.AlarmClearedNotificationV1( + id=uuidutils.generate_uuid(), + notificationType="AlarmClearedNotification", + subscriptionId=subsc.id, + timeStamp=datetime.utcnow(), + alarmId=alarm.id, + alarmClearedTime=alarm.alarmClearedTime, + _links=objects.AlarmClearedNotificationV1_Links( + alarm=objects.NotificationLink( + href=alarm_href(alarm.id, endpoint)), + subscription=objects.NotificationLink( + href=subsc_utils.subsc_href(subsc.id, endpoint)) + ) + ) + else: + notif_data = objects.AlarmNotificationV1( + id=uuidutils.generate_uuid(), + notificationType="AlarmNotification", + subscriptionId=subsc.id, + timeStamp=datetime.utcnow(), + alarm=alarm, + _links=objects.AlarmNotificationV1_Links( + subscription=objects.NotificationLink( + href=subsc_utils.subsc_href(subsc.id, endpoint)) + ) + ) + return notif_data diff --git a/tacker/sol_refactored/common/fm_subscription_utils.py b/tacker/sol_refactored/common/fm_subscription_utils.py new file mode 100644 index 000000000..0845388e3 --- /dev/null +++ b/tacker/sol_refactored/common/fm_subscription_utils.py @@ -0,0 +1,174 @@ +# Copyright (C) 2022 Fujitsu +# All Rights Reserved. +# +# 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. + + +import threading + +from oslo_log import log as logging + +from tacker.sol_refactored.api import api_version +from tacker.sol_refactored.common import config +from tacker.sol_refactored.common import exceptions as sol_ex +from tacker.sol_refactored.common import http_client +from tacker.sol_refactored.common import subscription_utils as subsc_utils +from tacker.sol_refactored import objects + + +LOG = logging.getLogger(__name__) + +CONF = config.CONF + +TEST_NOTIFICATION_TIMEOUT = 20 # seconds + + +def get_subsc(context, subsc_id): + subsc = objects.FmSubscriptionV1.get_by_id(context, subsc_id) + if subsc is None: + raise sol_ex.FmSubscriptionNotFound(subsc_id=subsc_id) + return subsc + + +def get_subsc_all(context, marker=None): + return objects.FmSubscriptionV1.get_all(context, marker) + + +def subsc_href(subsc_id, endpoint): + return f"{endpoint}/vnffm/v1/subscriptions/{subsc_id}" + + +def _get_notification_auth_handle(subsc): + if not subsc.obj_attr_is_set('authentication'): + return http_client.NoAuthHandle() + + if subsc.authentication.obj_attr_is_set('paramsBasic'): + param = subsc.authentication.paramsBasic + return http_client.BasicAuthHandle(param.userName, param.password) + + if subsc.authentication.obj_attr_is_set( + 'paramsOauth2ClientCredentials'): + param = subsc.authentication.paramsOauth2ClientCredentials + return http_client.OAuth2AuthHandle(None, + param.tokenEndpoint, param.clientId, param.clientPassword) + + # not reach here + + +def async_call(func): + def inner(*args, **kwargs): + th = threading.Thread(target=func, args=args, + kwargs=kwargs, daemon=True) + th.start() + return inner + + +@async_call +def send_notification(subsc, notif_data): + auth_handle = _get_notification_auth_handle(subsc) + client = http_client.HttpClient(auth_handle, + version=api_version.CURRENT_FM_VERSION) + + url = subsc.callbackUri + try: + resp, _ = client.do_request( + url, "POST", expected_status=[204], body=notif_data) + except sol_ex.SolException: + # it may occur if test_notification was not executed. + LOG.exception("send_notification failed") + + if resp.status_code != 204: + LOG.error(f"send_notification failed: {resp.status_code}") + + +def test_notification(subsc): + auth_handle = _get_notification_auth_handle(subsc) + client = http_client.HttpClient(auth_handle, + version=api_version.CURRENT_FM_VERSION, + timeout=TEST_NOTIFICATION_TIMEOUT) + + url = subsc.callbackUri + try: + resp, _ = client.do_request(url, "GET", expected_status=[204]) + except sol_ex.SolException as e: + # any sort of error is considered. avoid 500 error. + raise sol_ex.TestNotificationFailed() from e + + if resp.status_code != 204: + raise sol_ex.TestNotificationFailed() + + +def get_matched_subscs(context, inst, notif_type, alarm): + subscs = [] + for subsc in get_subsc_all(context): + # subsc: FmSubscription + + if not subsc.obj_attr_is_set('filter'): + # no filter. get it. + subscs.append(subsc) + continue + + # subsc.filter: FmNotificationsFilter + # - vnfInstanceSubscriptionFilter 0..1 + # - notificationTypes 0..N + # - faultyResourceTypes 0..N + # - perceivedSeverities 0..N + # - eventTypes 0..N + # - probableCauses 0..N + if alarm.obj_attr_is_set('rootCauseFaultyResource'): + alarm_faulty_res_type = ( + alarm.rootCauseFaultyResource.faultyResourceType) + else: + alarm_faulty_res_type = None + + if subsc.filter.obj_attr_is_set('vnfInstanceSubscriptionFilter'): + inst_filter = subsc.filter.vnfInstanceSubscriptionFilter + if not subsc_utils.match_inst_subsc_filter(inst_filter, inst): + continue + + if subsc.filter.obj_attr_is_set('notificationTypes'): + if notif_type not in subsc.filter.notificationTypes: + continue + + if (alarm_faulty_res_type is not None and + subsc.filter.obj_attr_is_set('faultyResourceTypes')): + if alarm_faulty_res_type not in subsc.filter.faultyResourceTypes: + continue + + if (alarm.perceivedSeverity is not None and + subsc.filter.obj_attr_is_set('perceivedSeverities')): + if alarm.perceivedSeverity not in subsc.filter.perceivedSeverities: + continue + + if (alarm.eventType is not None and + subsc.filter.obj_attr_is_set('eventTypes')): + if alarm.eventType not in subsc.filter.eventTypes: + continue + + if (alarm.probableCause is not None and + subsc.filter.obj_attr_is_set('probableCauses')): + if alarm.probableCause not in subsc.filter.probableCauses: + continue + # OK, matched + subscs.append(subsc) + + return subscs + + +def get_alarm_subscs(context, alarm, inst): + if alarm.obj_attr_is_set('alarmClearedTime'): + return get_matched_subscs( + context, inst, 'AlarmClearedNotification', alarm) + + return get_matched_subscs( + context, inst, 'AlarmNotification', alarm) diff --git a/tacker/sol_refactored/common/monitoring_plugin_base.py b/tacker/sol_refactored/common/monitoring_plugin_base.py new file mode 100644 index 000000000..cc1cab71f --- /dev/null +++ b/tacker/sol_refactored/common/monitoring_plugin_base.py @@ -0,0 +1,70 @@ +# Copyright (C) 2022 Fujitsu +# All Rights Reserved. +# +# 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. + +from importlib import import_module + +module_and_class = { + 'stub': + ('tacker.sol_refactored.common.monitoring_plugin_base', + 'MonitoringPluginStub'), + 'pm_event': + ('tacker.sol_refactored.common.prometheus_plugin', + 'PrometheusPluginPm'), + 'alert': + ('tacker.sol_refactored.common.prometheus_plugin', + 'PrometheusPluginFm'), + 'auto_healing': + ('tacker.sol_refactored.common.prometheus_plugin', + 'PrometheusPluginAutoScaling'), +} + + +def get_class(short_name): + module = import_module(module_and_class[short_name][0]) + return getattr(module, module_and_class[short_name][1]) + + +class MonitoringPlugin(): + @staticmethod + def get_instance(_class): + return _class.instance() + + def set_callback(self, notification_callback): + pass + + def create_job(self, **kwargs): + pass + + def delete_job(self, **kwargs): + pass + + def alert(self, **kwargs): + pass + + +class MonitoringPluginStub(MonitoringPlugin): + _instance = None + + @staticmethod + def instance(): + if not MonitoringPluginStub._instance: + MonitoringPluginStub() + return MonitoringPluginStub._instance + + def __init__(self): + if MonitoringPluginStub._instance: + raise SystemError( + "Not constructor but instance() should be used.") + MonitoringPluginStub._instance = self diff --git a/tacker/sol_refactored/common/pm_job_utils.py b/tacker/sol_refactored/common/pm_job_utils.py new file mode 100644 index 000000000..ae021c989 --- /dev/null +++ b/tacker/sol_refactored/common/pm_job_utils.py @@ -0,0 +1,175 @@ +# Copyright (C) 2022 Fujitsu +# All Rights Reserved. +# +# 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. + +import threading + +from oslo_log import log as logging +from oslo_utils import uuidutils + +from tacker.sol_refactored.api import api_version +from tacker.sol_refactored.common import config +from tacker.sol_refactored.common import exceptions as sol_ex +from tacker.sol_refactored.common import http_client +from tacker.sol_refactored.common import vnf_instance_utils as inst_utils +from tacker.sol_refactored import objects + +LOG = logging.getLogger(__name__) +CONF = config.CONF +TEST_NOTIFICATION_TIMEOUT = 20 # seconds + + +def update_report(context, job_id, report, timestamp): + # update reports in the pmJob + pm_job = get_pm_job(context, job_id) + + job_report = _gen_job_report(job_id, report, timestamp) + + if pm_job.obj_attr_is_set('reports'): + pm_job.reports.append(job_report) + else: + pm_job.reports = [job_report] + + return pm_job + + +def _gen_job_report(job_id, report, timestamp): + return objects.VnfPmJobV2_Reports( + href=f'/vnfpm/v2/pm_jobs/{job_id}/reports/{report.id}', + readyTime=timestamp + ) + + +def get_pm_job_all(context, marker=None): + # get all pm-job + return objects.PmJobV2.get_all(context, marker) + + +def get_pm_job(context, pm_job_id): + # get the PM job from DB. + pm_job = objects.PmJobV2.get_by_id(context, pm_job_id) + if pm_job is None: + raise sol_ex.PMJobNotExist() + return pm_job + + +def get_pm_report(context, pm_job_id, report_id=None): + if report_id: + pm_report = objects.PerformanceReportV2.get_by_filter( + context, id=report_id, jobId=pm_job_id) + if not pm_report: + raise sol_ex.PMReportNotExist() + return pm_report[0] + + pm_reports = objects.PerformanceReportV2.get_by_filter( + context, jobId=pm_job_id) + return pm_reports + + +def pm_job_href(pm_job_id, endpoint): + return f"{endpoint}/vnfpm/v2/pm_jobs/{pm_job_id}" + + +def make_pm_job_links(pm_job, endpoint): + links = objects.VnfPmJobV2_Links() + links.self = objects.Link(href=pm_job_href(pm_job.id, endpoint)) + links_objects = [] + for objects_id in pm_job.objectInstanceIds: + links_objects.append(objects.Link( + href=inst_utils.inst_href(objects_id, endpoint))) + links.objects = links_objects + return links + + +def _get_notification_auth_handle(pm_job): + if not pm_job.obj_attr_is_set('authentication'): + return http_client.NoAuthHandle() + if pm_job.authentication.obj_attr_is_set('paramsBasic'): + param = pm_job.authentication.paramsBasic + return http_client.BasicAuthHandle(param.userName, param.password) + if pm_job.authentication.obj_attr_is_set( + 'paramsOauth2ClientCredentials'): + param = pm_job.authentication.paramsOauth2ClientCredentials + return http_client.OAuth2AuthHandle( + None, param.tokenEndpoint, param.clientId, param.clientPassword) + return None + + +def test_notification(pm_job): + auth_handle = _get_notification_auth_handle(pm_job) + client = http_client.HttpClient(auth_handle, + version=api_version.CURRENT_PM_VERSION, + timeout=TEST_NOTIFICATION_TIMEOUT) + + url = pm_job.callbackUri + try: + resp, _ = client.do_request(url, "GET", expected_status=[204]) + except sol_ex.SolException as e: + # any sort of error is considered. avoid 500 error. + raise sol_ex.TestNotificationFailed() from e + + if resp.status_code != 204: + raise sol_ex.TestNotificationFailed() + + +def make_pm_notif_data(instance_id, sub_instance_ids, report_id, + pm_job, timestamp, endpoint): + notif_data = objects.PerformanceInformationAvailableNotificationV2( + id=uuidutils.generate_uuid(), + notificationType="PerformanceInformationAvailableNotification", + timeStamp=timestamp, + pmJobId=pm_job.id, + objectType=pm_job.objectType, + objectInstanceId=instance_id, + _links=objects.PerformanceInformationAvailableNotificationV2_Links( + objectInstance=objects.NotificationLink( + href=inst_utils.inst_href(instance_id, endpoint)), + pmJob=objects.NotificationLink( + href=pm_job_href(pm_job.id, endpoint)), + performanceReport=objects.NotificationLink( + href=f"{endpoint}/vnfpm/v2/pm_jobs/{pm_job.id}/" + f"reports/{report_id}" + ) + ) + ) + if sub_instance_ids: + notif_data.subObjectInstanceIds = sub_instance_ids + return notif_data + + +def async_call(func): + def inner(*args, **kwargs): + th = threading.Thread(target=func, args=args, + kwargs=kwargs, daemon=True) + th.start() + + return inner + + +@async_call +def send_notification(pm_job, notif_data): + auth_handle = _get_notification_auth_handle(pm_job) + client = http_client.HttpClient(auth_handle, + version=api_version.CURRENT_PM_VERSION) + + url = pm_job.callbackUri + try: + resp, _ = client.do_request( + url, "POST", expected_status=[204], body=notif_data) + except sol_ex.SolException: + # it may occur if test_notification was not executed. + LOG.exception("send_notification failed") + + if resp.status_code != 204: + LOG.error(f'send_notification failed: {resp.status_code}') diff --git a/tacker/sol_refactored/common/prometheus_plugin.py b/tacker/sol_refactored/common/prometheus_plugin.py new file mode 100644 index 000000000..f01fa895c --- /dev/null +++ b/tacker/sol_refactored/common/prometheus_plugin.py @@ -0,0 +1,869 @@ +# Copyright (C) 2022 Fujitsu +# All Rights Reserved. +# +# 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. + +import datetime +import json +import os +import paramiko +import re +import tempfile + +from oslo_log import log as logging +from oslo_utils import uuidutils +from tacker.sol_refactored.api import prometheus_plugin_validator as validator +from tacker.sol_refactored.api.schemas import prometheus_plugin_schemas +from tacker.sol_refactored.common import config as cfg +from tacker.sol_refactored.common import exceptions as sol_ex +from tacker.sol_refactored.common import fm_alarm_utils +from tacker.sol_refactored.common import http_client +from tacker.sol_refactored.common import monitoring_plugin_base as mon_base +from tacker.sol_refactored.common import pm_job_utils +from tacker.sol_refactored.common import vnf_instance_utils as inst_utils +from tacker.sol_refactored.conductor import conductor_rpc_v2 as rpc +from tacker.sol_refactored import objects + + +LOG = logging.getLogger(__name__) + +CONF = cfg.CONF + + +class PrometheusPlugin(): + def __init__(self): + self.rpc = rpc.PrometheusPluginConductor() + + def parse_datetime(self, isodate): + t = (isodate if isinstance(isodate, datetime.datetime) + else datetime.datetime.fromisoformat( + isodate.replace('Z', '+00:00'))) + return t if t.tzinfo else t.astimezone() + + +class PrometheusPluginPm(PrometheusPlugin, mon_base.MonitoringPlugin): + _instance = None + + @staticmethod + def instance(): + if PrometheusPluginPm._instance is None: + if not CONF.prometheus_plugin.performance_management: + stub = mon_base.MonitoringPluginStub.instance() + PrometheusPluginPm._instance = stub + else: + PrometheusPluginPm() + return PrometheusPluginPm._instance + + def __init__(self): + if PrometheusPluginPm._instance: + raise SystemError( + "Not constructor but instance() should be used.") + super(PrometheusPluginPm, self).__init__() + self.notification_callback = None + auth_handle = http_client.NoAuthHandle() + self.client = http_client.HttpClient(auth_handle) + self.reporting_period_margin = ( + CONF.prometheus_plugin.reporting_period_margin) + self.notification_callback = self.default_callback + self.sol_exp_map = { + 'VCpuUsageMeanVnf': + 'avg(sum(rate(pod_cpu_usage_seconds_total' + '{{pod=~"{pod}"}}[{reporting_period}s]))', + 'VCpuUsagePeakVnf': + 'max(sum(rate(pod_cpu_usage_seconds_total' + '{{pod=~"{pod}"}}[{reporting_period}s]))', + 'VMemoryUsageMeanVnf': + 'avg(pod_memory_working_set_bytes{{pod=~"{pod}"}} / ' + 'on(pod) (kube_node_status_capacity{{resource="memory"}} * ' + 'on(node) group_right kube_pod_info))', + 'VMemoryUsagePeakVnf': + 'max(pod_memory_working_set_bytes{{pod=~"{pod}"}} / ' + 'on(pod) (kube_node_status_capacity{{resource="memory"}} * ' + 'on(node) group_right kube_pod_info))', + 'VDiskUsageMeanVnf': + 'avg(container_fs_usage_bytes{{container="{container}",' + 'pod=~"{pod}"}}/container_fs_limit_bytes{{container=' + '"{container}",pod=~"{pod}"}}))', + 'VDiskUsagePeakVnf': + 'max(container_fs_usage_bytes{{container="{container}",' + 'pod=~"{pod}"}}/container_fs_limit_bytes{{container=' + '"{container}",pod=~"{pod}"}}))', + 'ByteIncomingVnfIntCp': + 'sum(container_network_receive_bytes_total' + '{{interface="{sub_object_instance_id}",pod=~"{pod}"}})', + 'PacketIncomingVnfIntCp': + 'sum(container_network_receive_packets_total' + '{{interface="{sub_object_instance_id}",pod=~"{pod}"}})', + 'ByteOutgoingVnfIntCp': + 'sum(container_network_transmit_bytes_total' + '{{interface="{sub_object_instance_id}",pod=~"{pod}"}})', + 'PacketOutgoingVnfIntCp': + 'sum(container_network_transmit_packets_total' + '{{interface="{sub_object_instance_id}",pod=~"{pod}"}})', + 'ByteIncomingVnfExtCp': + 'sum(container_network_receive_bytes_total' + '{{interface="{sub_object_instance_id}",pod=~"{pod}"}})', + 'PacketIncomingVnfExtCp': + 'sum(container_network_receive_packets_total' + '{{interface="{sub_object_instance_id}",pod=~"{pod}"}})', + 'ByteOutgoingVnfExtCp': + 'sum(container_network_transmit_bytes_total' + '{{interface="{sub_object_instance_id}",pod=~"{pod}"}})', + 'PacketOutgoingVnfExtCp': + 'sum(container_network_transmit_packets_total' + '{{interface="{sub_object_instance_id}",pod=~"{pod}"}})', + } + PrometheusPluginPm._instance = self + + def set_callback(self, notification_callback): + self.notification_callback = notification_callback + + def create_job(self, **kwargs): + return self.make_rules(kwargs['context'], kwargs['pm_job']) + + def delete_job(self, **kwargs): + self.delete_rules(kwargs['context'], kwargs['pm_job']) + + def alert(self, **kwargs): + try: + self._alert(kwargs['request'], body=kwargs['body']) + except Exception as e: + # All exceptions is ignored here and 204 response will always + # be returned. Because when tacker responds error to alertmanager, + # alertmanager may repeat the same reports. + LOG.error("%s: %s", e.__class__.__name__, e.args[0]) + + def default_callback(self, context, entries): + self.rpc.store_job_info(context, entries) + + def convert_measurement_unit(self, metric, value): + if re.match(r'^V(Cpu|Memory|Disk)Usage(Mean|Peak)Vnf\..+', metric): + value = float(value) + elif re.match(r'^(Byte|Packet)(Incoming|Outgoing)Vnf(IntCp|ExtCp)', + metric): + value = int(value) + else: + raise sol_ex.PrometheusPluginError( + "Failed to convert annotations.value to measurement unit.") + return value + + def get_datetime_of_latest_report( + self, context, pm_job, object_instance_id, + sub_object_instance_id, metric): + report = pm_job_utils.get_pm_report(context, pm_job.id) + if not report: + return None + + entries_of_same_object = list( + filter( + lambda x: ( + x.objectInstanceId == object_instance_id and + (not x.obj_attr_is_set('subObjectInstanceId') or + x.subObjectInstanceId == sub_object_instance_id) and + x.performanceMetric == metric), + report.entries)) + if len(entries_of_same_object) == 0: + return None + values = sum(list(map( + lambda x: x.performanceValues, entries_of_same_object)), []) + return max(values, key=lambda value: + self.parse_datetime(value.timeStamp)).timeStamp + + def filter_alert_by_time( + self, context, pm_job, datetime_now, + object_instance_id, sub_object_instance_id, metric): + # Ignore expired alert + reporting_boundary = pm_job.criteria.reportingBoundary\ + if (pm_job.criteria.obj_attr_is_set('reportingBoundary') and + pm_job.criteria.reportingBoundary) else None + if (reporting_boundary and + datetime_now > self.parse_datetime(reporting_boundary)): + raise sol_ex.PrometheusPluginSkipped() + + # Ignore short period alert + report_date = self.get_datetime_of_latest_report( + context, pm_job, object_instance_id, sub_object_instance_id, + metric) + + # reporting_period_margin is some margin for timing inconsistency + # between prometheus and tacker. + if (report_date and report_date + datetime.timedelta( + seconds=(pm_job.criteria.reportingPeriod - + self.reporting_period_margin)) >= datetime_now): + raise sol_ex.PrometheusPluginSkipped() + + def valid_alert(self, pm_job, object_instance_id, sub_object_instance_id): + object_instance_ids = ( + pm_job.objectInstanceIds + if (pm_job.obj_attr_is_set('objectInstanceIds') and + pm_job.objectInstanceIds) else []) + if object_instance_id not in object_instance_ids: + LOG.error( + f"labels.object_instance_id {object_instance_id} " + f"doesn't match pmJob.") + raise sol_ex.PrometheusPluginSkipped() + sub_object_instance_ids = ( + pm_job.subObjectInstanceIds + if (pm_job.obj_attr_is_set('subObjectInstanceIds') and + pm_job.subObjectInstanceIds) else []) + if (sub_object_instance_id and + (not sub_object_instance_ids or + sub_object_instance_id not in sub_object_instance_ids)): + LOG.error( + f"labels.sub_object_instance_id {sub_object_instance_id} " + f"doesn't match pmJob.") + raise sol_ex.PrometheusPluginSkipped() + + @validator.schema(prometheus_plugin_schemas.AlertMessage) + def _alert(self, request, body): + result = [] + context = request.context + datetime_now = datetime.datetime.now(datetime.timezone.utc) + for alt in body['alerts']: + if alt['labels']['function_type'] != 'vnfpm': + continue + try: + pm_job_id = alt['labels']['job_id'] + object_instance_id = alt['labels']['object_instance_id'] + metric = alt['labels']['metric'] + sub_object_instance_id = alt['labels'].get( + 'sub_object_instance_id') + value = alt['annotations']['value'] + + pm_job = pm_job_utils.get_pm_job(context, pm_job_id) + self.filter_alert_by_time(context, pm_job, datetime_now, + object_instance_id, + sub_object_instance_id, metric) + self.valid_alert( + pm_job, object_instance_id, sub_object_instance_id) + value = self.convert_measurement_unit(metric, value) + + result.append({ + 'objectType': pm_job.objectType, + 'objectInstanceId': object_instance_id, + 'subObjectInstanceId': sub_object_instance_id, + 'performanceMetric': metric, + 'performanceValues': [{ + 'timeStamp': datetime_now, + 'value': value + }] + }) + except sol_ex.PrometheusPluginSkipped: + pass + if len(result) > 0: + if self.notification_callback: + # every job_id in body['alerts'] has same id + self.notification_callback(context, { + 'id': uuidutils.generate_uuid(), + 'jobId': pm_job_id, + 'entries': result, + }) + return result + + def decompose_metrics_vnfc(self, pm_job): + metrics = pm_job.criteria.performanceMetric\ + if pm_job.criteria.obj_attr_is_set('performanceMetric') else None + metrics = (list(filter(lambda x: ( + re.match(r'^V(Cpu|Memory|Disk)Usage(Mean|Peak)Vnf\..+', x) and + re.sub(r'^V(Cpu|Memory|Disk)Usage(Mean|Peak)Vnf\.', '', + x) in pm_job.objectInstanceIds), metrics)) + if metrics else []) + metric_grps = pm_job.criteria.performanceMetricGroup\ + if (pm_job.criteria.obj_attr_is_set('performanceMetricGroup') and + pm_job.criteria.performanceMetricGroup) else [] + for obj in pm_job.objectInstanceIds: + for grp in metric_grps: + if grp == 'VirtualizedComputeResource': + metrics.append(f'VCpuUsageMeanVnf.{obj}') + metrics.append(f'VCpuUsagePeakVnf.{obj}') + metrics.append(f'VMemoryUsageMeanVnf.{obj}') + metrics.append(f'VMemoryUsagePeakVnf.{obj}') + metrics.append(f'VDiskUsageMeanVnf.{obj}') + metrics.append(f'VDiskUsagePeakVnf.{obj}') + metrics = list(set(metrics)) + if len(metrics) == 0: + raise sol_ex.PrometheusPluginError( + "Invalid performanceMetric or performanceMetricGroup." + ) + return metrics + + def make_prom_ql(self, target, pod, container='', collection_period=30, + reporting_period=60, sub_object_instance_id='*'): + reporting_period = max(reporting_period, 30) + expr = self.sol_exp_map[target].format( + pod=pod, + container=container, + collection_period=collection_period, + reporting_period=reporting_period, + sub_object_instance_id=sub_object_instance_id + ) + return expr + + def make_rule(self, pm_job, object_instance_id, sub_object_instance_id, + metric, expression, collection_period): + labels = { + 'alertname': '', + 'receiver_type': 'tacker', + 'function_type': 'vnfpm', + 'job_id': pm_job.id, + 'object_instance_id': object_instance_id, + 'sub_object_instance_id': sub_object_instance_id, + 'metric': metric + } + labels = {k: v for k, v in labels.items() if v is not None} + annotations = { + 'value': r'{{$value}}' + } + rule = { + 'alert': uuidutils.generate_uuid(), + 'expr': expression, + 'for': f'{collection_period}s', + 'labels': labels, + 'annotations': annotations + } + return rule + + def get_vnfc_resource_info(self, _, vnf_instance_id, inst_map): + inst = inst_map[vnf_instance_id] + if not inst.obj_attr_is_set('instantiatedVnfInfo') or\ + not inst.instantiatedVnfInfo.obj_attr_is_set( + 'vnfcResourceInfo'): + return None + return inst.instantiatedVnfInfo.vnfcResourceInfo + + def get_pod_regexp(self, resource_info): + # resource ids are like: + # ['test-test1-756757f8f-xcwmt', + # 'test-test2-756757f8f-kmghr', ...] + # convert them to a regex string such as: + # '(test\-test1\-[0-9a-f]{1,10}-[0-9a-z]{5}$| + # test\-test2\-[0-9a-f]{1,10}-[0-9a-z]{5}$|...)' + deployments = list(filter( + lambda r: + r.computeResource.obj_attr_is_set( + 'vimLevelResourceType') + and r.computeResource.obj_attr_is_set( + 'resourceId' + ) + and r.computeResource.vimLevelResourceType == + 'Deployment', resource_info + )) + deployments = list(set(list(map( + lambda d: re.escape(re.sub( + r'\-[0-9a-f]{1,10}-[0-9a-z]{5}$', '', + d.computeResource.resourceId)) + + r'\-[0-9a-f]{1,10}-[0-9a-z]{5}$', + deployments + )))) + pods_regexp = '(' + '|'.join(deployments) + ')' + return deployments, pods_regexp + + def _make_rules_for_each_obj(self, context, pm_job, inst_map, metric): + target = re.sub(r'\..+$', '', metric) + objs = pm_job.objectInstanceIds + collection_period = pm_job.criteria.collectionPeriod + reporting_period = pm_job.criteria.reportingPeriod + rules = [] + for obj in objs: + # resource ids are like: + # ['test-test1-756757f8f-xcwmt', + # 'test-test2-756757f8f-kmghr', ...] + # convert them to a regex string such as: + # '(test\-test1\-[0-9a-f]{1,10}-[0-9a-z]{5}$| + # test\-test2\-[0-9a-f]{1,10}-[0-9a-z]{5}$|...)' + resource_info = self.get_vnfc_resource_info(context, obj, inst_map) + if not resource_info: + continue + deployments, pods_regexp = self.get_pod_regexp(resource_info) + if len(deployments) == 0: + continue + expr = self.make_prom_ql( + target, pods_regexp, collection_period=collection_period, + reporting_period=reporting_period) + rules.append(self.make_rule( + pm_job, obj, None, metric, expr, + collection_period)) + return rules + + def get_compute_resouce_by_sub_obj(self, vnf_instance, sub_obj): + inst = vnf_instance + if (not inst.obj_attr_is_set('instantiatedVnfInfo') or + not inst.instantiatedVnfInfo.obj_attr_is_set( + 'vnfcResourceInfo') or + not inst.instantiatedVnfInfo.obj_attr_is_set('vnfcInfo')): + return None + vnfc_info = list(filter( + lambda x: (x.obj_attr_is_set('vnfcResourceInfoId') and + x.id == sub_obj), + inst.instantiatedVnfInfo.vnfcInfo)) + if len(vnfc_info) == 0: + return None + resources = list(filter( + lambda x: (vnfc_info[0].obj_attr_is_set('vnfcResourceInfoId') and + x.id == vnfc_info[0].vnfcResourceInfoId and + x.computeResource.obj_attr_is_set('vimLevelResourceType') and + x.computeResource.vimLevelResourceType == 'Deployment' and + x.computeResource.obj_attr_is_set('resourceId')), + inst.instantiatedVnfInfo.vnfcResourceInfo)) + if len(resources) == 0: + return None + return resources[0].computeResource + + def _make_rules_for_each_sub_obj(self, context, pm_job, inst_map, metric): + target = re.sub(r'\..+$', '', metric) + objs = pm_job.objectInstanceIds + sub_objs = pm_job.subObjectInstanceIds\ + if (pm_job.obj_attr_is_set('subObjectInstanceIds') and + pm_job.subObjectInstanceIds) else [] + collection_period = pm_job.criteria.collectionPeriod + reporting_period = pm_job.criteria.reportingPeriod + rules = [] + resource_info = self.get_vnfc_resource_info(context, objs[0], inst_map) + if not resource_info: + return [] + if pm_job.objectType in {'Vnf', 'Vnfc'}: + inst = inst_map[objs[0]] + for sub_obj in sub_objs: + # resource id is like 'test-test1-756757f8f-xcwmt' + # obtain 'test-test1' as deployment + # obtain 'test' as container + compute_resource = self.get_compute_resouce_by_sub_obj( + inst, sub_obj) + if not compute_resource: + continue + resource_id = compute_resource.resourceId + deployment = re.sub( + r'\-[0-9a-f]{1,10}-[0-9a-z]{5}$', '', resource_id) + g = re.match(r'^(.+)\-\1{1,}[0-9]+', deployment) + if not g: + continue + container = g.group(1) + resource_id = re.escape(resource_id) + expr = self.make_prom_ql( + target, resource_id, container=container, + collection_period=collection_period, + reporting_period=reporting_period) + rules.append(self.make_rule( + pm_job, objs[0], sub_obj, metric, expr, + collection_period)) + else: + deployments, pods_regexp = self.get_pod_regexp(resource_info) + if len(deployments) == 0: + return [] + for sub_obj in sub_objs: + expr = self.make_prom_ql( + target, pods_regexp, collection_period=collection_period, + reporting_period=reporting_period, + sub_object_instance_id=sub_obj) + rules.append(self.make_rule( + pm_job, objs[0], sub_obj, metric, expr, + collection_period)) + return rules + + def _make_rules(self, context, pm_job, metric, inst_map): + sub_objs = pm_job.subObjectInstanceIds\ + if (pm_job.obj_attr_is_set('subObjectInstanceIds') and + pm_job.subObjectInstanceIds) else [] + # Cardinality of objectInstanceIds and subObjectInstanceIds + # is N:0 or 1:N. + if len(sub_objs) > 0: + return self._make_rules_for_each_sub_obj( + context, pm_job, inst_map, metric) + return self._make_rules_for_each_obj( + context, pm_job, inst_map, metric) + + def decompose_metrics_vnfintextcp(self, pm_job): + group_name = 'VnfInternalCp'\ + if pm_job.objectType == 'VnfIntCp' else 'VnfExternalCp' + metrics = pm_job.criteria.performanceMetric\ + if pm_job.criteria.obj_attr_is_set('performanceMetric') else None + metrics = list(filter(lambda x: ( + re.match(r'^(Byte|Packet)(Incoming|Outgoing)' + pm_job.objectType, + x)), + metrics)) if metrics else [] + metric_grps = pm_job.criteria.performanceMetricGroup\ + if (pm_job.criteria.obj_attr_is_set('performanceMetricGroup') and + pm_job.criteria.performanceMetricGroup) else [] + for grp in metric_grps: + if grp == group_name: + metrics.append(f'ByteIncoming{pm_job.objectType}') + metrics.append(f'ByteOutgoing{pm_job.objectType}') + metrics.append(f'PacketIncoming{pm_job.objectType}') + metrics.append(f'PacketOutgoing{pm_job.objectType}') + metrics = list(set(metrics)) + if len(metrics) == 0: + raise sol_ex.PrometheusPluginError( + "Invalid performanceMetric or performanceMetricGroup." + ) + return metrics + + def _delete_rule(self, host, port, user, password, path, pm_job_id): + with paramiko.Transport(sock=(host, port)) as client: + client.connect(username=user, password=password) + sftp = paramiko.SFTPClient.from_transport(client) + sftp.remove(f'{path}/{pm_job_id}.json') + + def delete_rules(self, context, pm_job): + target_list, reload_list = self.get_access_info(pm_job) + for info in target_list: + self._delete_rule( + info['host'], info['port'], info['user'], + info['password'], info['path'], pm_job.id) + for uri in reload_list: + self.reload_prom_server(context, uri) + + def decompose_metrics(self, pm_job): + if pm_job.objectType in {'Vnf', 'Vnfc'}: + return self.decompose_metrics_vnfc(pm_job) + if pm_job.objectType in {'VnfIntCp', 'VnfExtCp'}: + return self.decompose_metrics_vnfintextcp(pm_job) + raise sol_ex.PrometheusPluginError( + f"Invalid objectType: {pm_job.objectType}.") + + def reload_prom_server(self, context, reload_uri): + resp, _ = self.client.do_request( + reload_uri, "PUT", context=context) + if resp.status_code != 202: + LOG.error("reloading request to prometheus is failed: %d.", + resp.status_code) + + def _upload_rule(self, rule_group, host, port, user, password, path, + pm_job_id): + with tempfile.TemporaryDirectory() as tmpdir: + with open(os.path.join(tmpdir, 'rule.json'), + 'w+', encoding="utf-8") as fp: + json.dump(rule_group, fp, indent=4, ensure_ascii=False) + filename = fp.name + with paramiko.Transport(sock=(host, port)) as client: + LOG.info("Upload rule files to prometheus server: %s.", host) + client.connect(username=user, password=password) + sftp = paramiko.SFTPClient.from_transport(client) + sftp.put(filename, f'{path}/{pm_job_id}.json') + + def get_access_info(self, pm_job): + target_list = [] + reload_list = [] + if (not pm_job.obj_attr_is_set('metadata') + or 'monitoring' not in pm_job.metadata): + raise sol_ex.PrometheusPluginError( + "monitoring info is missing at metadata field.") + access_info = pm_job.metadata['monitoring'] + if (access_info.get('monitorName') != 'prometheus' or + access_info.get('driverType') != 'external'): + raise sol_ex.PrometheusPluginError( + "prometheus info is missing at metadata field.") + for info in access_info.get('targetsInfo', []): + host = info.get('prometheusHost', '') + port = info.get('prometheusHostPort', 22) + auth = info.get('authInfo', {}) + user = auth.get('ssh_username', '') + password = auth.get('ssh_password', '') + path = info.get('alertRuleConfigPath', '') + uri = info.get('prometheusReloadApiEndpoint', '') + if not (host and user and path and uri): + continue + target_list.append({ + 'host': host, + 'port': port, + 'user': user, + 'password': password, + 'path': path + }) + reload_list.append(uri) + return target_list, list(set(reload_list)) + + def upload_rules( + self, context, target_list, reload_list, rule_group, pm_job): + for info in target_list: + self._upload_rule( + rule_group, info['host'], info['port'], info['user'], + info['password'], info['path'], pm_job.id) + for uri in reload_list: + self.reload_prom_server(context, uri) + + def get_vnf_instances(self, context, pm_job): + object_instance_ids = list(set(pm_job.objectInstanceIds)) + return dict(zip( + object_instance_ids, + list(map( + lambda inst: inst_utils.get_inst(context, inst), + object_instance_ids)))) + + def make_rules(self, context, pm_job): + target_list, reload_list = self.get_access_info(pm_job) + metrics = self.decompose_metrics(pm_job) + inst_map = self.get_vnf_instances(context, pm_job) + rules = sum([self._make_rules(context, pm_job, metric, inst_map) + for metric in metrics], []) + if len(rules) == 0: + raise sol_ex.PrometheusPluginError( + f"Converting from a PM job to alert rules is failed." + f" PM job id: {pm_job.id}") + rule_group = { + 'groups': [ + { + 'name': f'tacker_{pm_job.id}', + 'rules': rules + } + ] + } + self.upload_rules( + context, target_list, reload_list, rule_group, pm_job) + return rule_group + + +class PrometheusPluginFm(PrometheusPlugin, mon_base.MonitoringPlugin): + _instance = None + + @staticmethod + def instance(): + if PrometheusPluginFm._instance is None: + if not CONF.prometheus_plugin.fault_management: + stub = mon_base.MonitoringPluginStub.instance() + PrometheusPluginFm._instance = stub + else: + PrometheusPluginFm() + return PrometheusPluginFm._instance + + def __init__(self): + if PrometheusPluginFm._instance: + raise SystemError( + "Not constructor but instance() should be used.") + super(PrometheusPluginFm, self).__init__() + self.notification_callback = self.default_callback + self.endpoint = CONF.v2_vnfm.endpoint + PrometheusPluginFm._instance = self + + def set_callback(self, notification_callback): + self.notification_callback = notification_callback + + def alert(self, **kwargs): + try: + self._alert(kwargs['request'], body=kwargs['body']) + except Exception as e: + # All exceptions is ignored here and 204 response will always + # be returned. Because when tacker responds error to alertmanager, + # alertmanager may repeat the same reports. + LOG.error("%s: %s", e.__class__.__name__, e.args[0]) + + def default_callback(self, context, alarm): + self.rpc.store_alarm_info(context, alarm) + + def vnfc_instance_ids( + self, context, vnf_instance_id, alert_entry): + inst = inst_utils.get_inst(context, vnf_instance_id) + resources = (inst.instantiatedVnfInfo.vnfcResourceInfo + if inst.obj_attr_is_set('instantiatedVnfInfo') and + inst.instantiatedVnfInfo.obj_attr_is_set( + 'vnfcResourceInfo') else []) + vnfc_info = (inst.instantiatedVnfInfo.vnfcInfo + if inst.obj_attr_is_set('instantiatedVnfInfo') and + inst.instantiatedVnfInfo.obj_attr_is_set( + 'vnfcInfo') else []) + if 'pod' not in alert_entry['labels']: + return [] + pod = alert_entry['labels']['pod'] + + deployments = list(filter( + lambda r: ( + r.computeResource.obj_attr_is_set('vimLevelResourceType') and + r.computeResource.obj_attr_is_set('resourceId') and + (r.computeResource.vimLevelResourceType in + {'Deployment', 'Pod'}) and + re.match(pod, r.computeResource.resourceId)), + resources + )) + vnfc_res_info_ids = list(map( + lambda res: res.id, deployments + )) + vnfc_info = list(filter( + lambda info: ( + info.obj_attr_is_set('vnfcResourceInfoId') and + info.vnfcResourceInfoId in vnfc_res_info_ids), + vnfc_info + )) + vnfc_info = list(map(lambda info: info.id, vnfc_info)) + return vnfc_info + + def update_alarm(self, context, not_cleared, ends_at, datetime_now): + for alm in not_cleared: + alm.alarmClearedTime = ends_at + alm.alarmChangedTime = datetime_now + if self.notification_callback: + self.notification_callback(context, alm) + + def create_new_alarm(self, context, alert_entry, datetime_now): + vnf_instance_id = alert_entry['labels']['vnf_instance_id'] + fingerprint = alert_entry['fingerprint'] + perceived_severity = alert_entry['labels']['perceived_severity'] + + fault_details = [ + f"fingerprint: {fingerprint}", + f"detail: {alert_entry['annotations'].get('fault_details')}" + ] + + vnfc_instance_ids = self.vnfc_instance_ids( + context, vnf_instance_id, alert_entry) + if len(vnfc_instance_ids) == 0: + LOG.error("failed to specify vnfc_instance for the alert.") + raise sol_ex.PrometheusPluginSkipped() + + new_alarm = objects.AlarmV1.from_dict({ + 'id': + uuidutils.generate_uuid(), + 'managedObjectId': + vnf_instance_id, + 'vnfcInstanceIds': + vnfc_instance_ids, + 'alarmRaisedTime': + datetime_now.isoformat(), + 'ackState': + 'UNACKNOWLEDGED', + 'perceivedSeverity': + perceived_severity, + 'eventTime': + alert_entry['startsAt'], + 'eventType': + alert_entry['labels'].get('event_type', ''), + 'faultType': + alert_entry['annotations'].get('fault_type', ''), + 'probableCause': + alert_entry['annotations'].get('probable_cause', ''), + 'isRootCause': + False, + 'faultDetails': + fault_details, + '_links': {} + }) + + _links = fm_alarm_utils.make_alarm_links(new_alarm, self.endpoint) + new_alarm._links = _links + if self.notification_callback: + self.notification_callback(context, new_alarm) + return new_alarm + + def get_not_cleared_alarms(self, context, vnf_instance_id, fingerprint): + alms = fm_alarm_utils.get_not_cleared_alarms(context, vnf_instance_id) + fpstr = f'fingerprint: {fingerprint}' + return list(filter( + lambda x: (not x.obj_attr_is_set('alarmClearedTime') and + x.obj_attr_is_set('faultDetails') and + fpstr in x.faultDetails), alms)) + + def create_or_update_alarm( + self, context, alert_entry, datetime_now): + status = alert_entry['status'] + vnf_instance_id = alert_entry['labels']['vnf_instance_id'] + fingerprint = alert_entry['fingerprint'] + not_cleared = self.get_not_cleared_alarms( + context, vnf_instance_id, fingerprint) + + if status == 'resolved' and len(not_cleared) > 0: + ends_at = alert_entry['endsAt'] + self.update_alarm( + context, not_cleared, ends_at, datetime_now) + return not_cleared + if status == 'firing' and len(not_cleared) == 0: + new_alarm = self.create_new_alarm( + context, alert_entry, datetime_now) + return [new_alarm] + raise sol_ex.PrometheusPluginSkipped() + + @validator.schema(prometheus_plugin_schemas.AlertMessage) + def _alert(self, request, body): + now = datetime.datetime.now(datetime.timezone.utc) + result = [] + for alt in body['alerts']: + if alt['labels']['function_type'] != 'vnffm': + continue + try: + alarms = self.create_or_update_alarm( + request.context, alt, now) + result.extend(alarms) + except sol_ex.PrometheusPluginSkipped: + pass + return result + + +class PrometheusPluginAutoScaling(PrometheusPlugin, mon_base.MonitoringPlugin): + _instance = None + + @staticmethod + def instance(): + if PrometheusPluginAutoScaling._instance is None: + if not CONF.prometheus_plugin.auto_scaling: + stub = mon_base.MonitoringPluginStub.instance() + PrometheusPluginAutoScaling._instance = stub + else: + PrometheusPluginAutoScaling() + return PrometheusPluginAutoScaling._instance + + def __init__(self): + if PrometheusPluginAutoScaling._instance: + raise SystemError( + "Not constructor but instance() should be used.") + super(PrometheusPluginAutoScaling, self).__init__() + self.notification_callback = self.default_callback + PrometheusPluginAutoScaling._instance = self + + def set_callback(self, notification_callback): + self.notification_callback = notification_callback + + def alert(self, **kwargs): + try: + self._alert(kwargs['request'], body=kwargs['body']) + except Exception as e: + # All exceptions is ignored here and 204 response will always + # be returned. Because when tacker responds error to alertmanager, + # alertmanager may repeat the same reports. + LOG.error("%s: %s", e.__class__.__name__, e.args[0]) + + def default_callback(self, context, vnf_instance_id, scaling_param): + self.rpc.request_scale(context, vnf_instance_id, scaling_param) + + def skip_if_auto_scale_not_enabled(self, vnf_instance): + if (not vnf_instance.obj_attr_is_set('vnfConfigurableProperties') or + not vnf_instance.vnfConfigurableProperties.get( + 'isAutoscaleEnabled')): + raise sol_ex.PrometheusPluginSkipped() + + def process_auto_scale(self, request, vnf_instance_id, auto_scale_type, + aspect_id): + scaling_param = { + 'type': auto_scale_type, + 'aspectId': aspect_id, + } + context = request.context + if self.notification_callback: + self.notification_callback(context, vnf_instance_id, scaling_param) + + @validator.schema(prometheus_plugin_schemas.AlertMessage) + def _alert(self, request, body): + result = [] + for alt in body['alerts']: + if alt['labels']['function_type'] != 'auto_scale': + continue + try: + vnf_instance_id = alt['labels']['vnf_instance_id'] + auto_scale_type = alt['labels']['auto_scale_type'] + aspect_id = alt['labels']['aspect_id'] + context = request.context + + inst = inst_utils.get_inst(context, vnf_instance_id) + self.skip_if_auto_scale_not_enabled(inst) + self.process_auto_scale( + request, vnf_instance_id, auto_scale_type, aspect_id) + result.append((vnf_instance_id, auto_scale_type, aspect_id)) + except sol_ex.PrometheusPluginSkipped: + pass + return result diff --git a/tacker/sol_refactored/common/subscription_utils.py b/tacker/sol_refactored/common/subscription_utils.py index 02ed203bb..31b8c8442 100644 --- a/tacker/sol_refactored/common/subscription_utils.py +++ b/tacker/sol_refactored/common/subscription_utils.py @@ -84,13 +84,15 @@ def send_notification(subsc, notif_data): url = subsc.callbackUri try: - resp, body = client.do_request(url, "POST", body=notif_data) - if resp.status_code != 204: - LOG.error("send_notification failed: %d" % resp.status_code) - except Exception: + resp, body = client.do_request( + url, "POST", expected_status=[204], body=notif_data) + except sol_ex.SolException: # it may occur if test_notification was not executed. LOG.exception("send_notification failed") + if resp.status_code != 204: + LOG.error("send_notification failed: %d" % resp.status_code) + def test_notification(subsc): auth_handle = _get_notification_auth_handle(subsc) @@ -100,11 +102,12 @@ def test_notification(subsc): url = subsc.callbackUri try: - resp, _ = client.do_request(url, "GET") - if resp.status_code != 204: - raise sol_ex.TestNotificationFailed() - except Exception: + resp, _ = client.do_request(url, "GET", expected_status=[204]) + except sol_ex.SolException as e: # any sort of error is considered. avoid 500 error. + raise sol_ex.TestNotificationFailed() from e + + if resp.status_code != 204: raise sol_ex.TestNotificationFailed() diff --git a/tacker/sol_refactored/conductor/conductor_rpc_v2.py b/tacker/sol_refactored/conductor/conductor_rpc_v2.py index 7a80eb725..d41a77740 100644 --- a/tacker/sol_refactored/conductor/conductor_rpc_v2.py +++ b/tacker/sol_refactored/conductor/conductor_rpc_v2.py @@ -50,3 +50,31 @@ class VnfLcmRpcApiV2(object): def modify_vnfinfo(self, context, lcmocc_id): self._cast_lcm_op(context, lcmocc_id, 'modify_vnfinfo') + + +TOPIC_PROMETHEUS_PLUGIN = 'TACKER_PROMETHEUS_PLUGIN' + + +class PrometheusPluginConductor(object): + + target = oslo_messaging.Target( + exchange='tacker', + topic=TOPIC_PROMETHEUS_PLUGIN, + fanout=False, + version='1.0') + + def cast(self, context, method, **kwargs): + serializer = objects_base.TackerObjectSerializer() + client = rpc.get_client( + self.target, version_cap=None, serializer=serializer) + cctxt = client.prepare() + cctxt.cast(context, method, **kwargs) + + def store_alarm_info(self, context, alarm): + self.cast(context, 'store_alarm_info', alarm=alarm) + + def store_job_info(self, context, report): + self.cast(context, 'store_job_info', report=report) + + def request_scale(self, context, id, scale_req): + self.cast(context, 'request_scale', id=id, scale_req=scale_req) diff --git a/tacker/sol_refactored/conductor/conductor_v2.py b/tacker/sol_refactored/conductor/conductor_v2.py index 5a407ef64..5207be8a8 100644 --- a/tacker/sol_refactored/conductor/conductor_v2.py +++ b/tacker/sol_refactored/conductor/conductor_v2.py @@ -22,7 +22,10 @@ from tacker.sol_refactored.common import coordinate from tacker.sol_refactored.common import exceptions as sol_ex from tacker.sol_refactored.common import lcm_op_occ_utils as lcmocc_utils from tacker.sol_refactored.common import vnf_instance_utils as inst_utils +from tacker.sol_refactored.conductor import prometheus_plugin_driver as pp_drv +from tacker.sol_refactored.conductor import vnffm_driver_v1 from tacker.sol_refactored.conductor import vnflcm_driver_v2 +from tacker.sol_refactored.conductor import vnfpm_driver_v2 from tacker.sol_refactored.nfvo import nfvo_client from tacker.sol_refactored import objects from tacker.sol_refactored.objects.v2 import fields @@ -37,8 +40,11 @@ class ConductorV2(object): def __init__(self): self.vnflcm_driver = vnflcm_driver_v2.VnfLcmDriverV2() + self.vnffm_driver = vnffm_driver_v1.VnfFmDriverV1() + self.vnfpm_driver = vnfpm_driver_v2.VnfPmDriverV2() self.endpoint = CONF.v2_vnfm.endpoint self.nfvo_client = nfvo_client.NfvoClient() + self.prom_driver = pp_drv.PrometheusPluginDriver.instance() self._change_lcm_op_state() @@ -313,3 +319,14 @@ class ConductorV2(object): # send notification COMPLETED or FAILED_TEMP self.nfvo_client.send_lcmocc_notification(context, lcmocc, inst, self.endpoint) + + def store_alarm_info(self, context, alarm): + self.vnffm_driver.store_alarm_info(context, alarm) + + def store_job_info(self, context, report): + # call pm_driver + self.vnfpm_driver.store_job_info(context, report) + + @log.log + def request_scale(self, context, id, scale_req): + self.prom_driver.request_scale(context, id, scale_req) diff --git a/tacker/sol_refactored/conductor/prometheus_plugin_driver.py b/tacker/sol_refactored/conductor/prometheus_plugin_driver.py new file mode 100644 index 000000000..2688bf711 --- /dev/null +++ b/tacker/sol_refactored/conductor/prometheus_plugin_driver.py @@ -0,0 +1,63 @@ +# Copyright (C) 2022 Fujitsu +# All Rights Reserved. +# +# 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. + +from oslo_log import log as logging +from tacker.sol_refactored.common import config as cfg +from tacker.sol_refactored.common import http_client + +LOG = logging.getLogger(__name__) + +CONF = cfg.CONF + + +class PrometheusPluginDriverStub(): + def request_scale(self, context, vnf_instance_id, scale_req): + pass + + +class PrometheusPluginDriver(): + _instance = None + + @staticmethod + def instance(): + if PrometheusPluginDriver._instance is None: + if (CONF.prometheus_plugin.auto_scaling or + CONF.prometheus_plugin.fault_management or + CONF.prometheus_plugin.performance_management): + PrometheusPluginDriver() + else: + stub = PrometheusPluginDriverStub() + PrometheusPluginDriver._instance = stub + return PrometheusPluginDriver._instance + + def __init__(self): + if PrometheusPluginDriver._instance: + raise SystemError("Not constructor but instance() should be used.") + auth_handle = http_client.KeystonePasswordAuthHandle( + auth_url=CONF.keystone_authtoken.auth_url, + username=CONF.keystone_authtoken.username, + password=CONF.keystone_authtoken.password, + project_name=CONF.keystone_authtoken.project_name, + user_domain_name=CONF.keystone_authtoken.user_domain_name, + project_domain_name=CONF.keystone_authtoken.project_domain_name) + self.client = http_client.HttpClient(auth_handle) + PrometheusPluginDriver._instance = self + + def request_scale(self, context, vnf_instance_id, scale_req): + ep = CONF.v2_vnfm.endpoint + url = f'{ep}/vnflcm/v2/vnf_instances/{vnf_instance_id}/scale' + resp, _ = self.client.do_request( + url, "POST", context=context, body=scale_req, version="2.0.0") + LOG.info("AutoHealing request is processed: %d.", resp.status_code) diff --git a/tacker/sol_refactored/conductor/v2_hook.py b/tacker/sol_refactored/conductor/v2_hook.py index 408e36c59..43cb18acf 100644 --- a/tacker/sol_refactored/conductor/v2_hook.py +++ b/tacker/sol_refactored/conductor/v2_hook.py @@ -27,3 +27,6 @@ class ConductorV2Hook(object): service.conn.create_consumer( conductor_rpc_v2.TOPIC_CONDUCTOR_V2, endpoints, serializer=serializer) + service.conn.create_consumer( + conductor_rpc_v2.TOPIC_PROMETHEUS_PLUGIN, endpoints, + serializer=serializer) diff --git a/tacker/sol_refactored/conductor/vnffm_driver_v1.py b/tacker/sol_refactored/conductor/vnffm_driver_v1.py new file mode 100644 index 000000000..ceee939ab --- /dev/null +++ b/tacker/sol_refactored/conductor/vnffm_driver_v1.py @@ -0,0 +1,50 @@ +# Copyright (C) 2022 Fujitsu +# All Rights Reserved. +# +# 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. + +from oslo_log import log as logging + +from tacker.sol_refactored.common import config +from tacker.sol_refactored.common import exceptions as sol_ex +from tacker.sol_refactored.common import fm_alarm_utils as alarm_utils +from tacker.sol_refactored.common import vnf_instance_utils as inst_utils +from tacker.sol_refactored.nfvo import nfvo_client + + +LOG = logging.getLogger(__name__) + +CONF = config.CONF + + +class VnfFmDriverV1(): + def __init__(self): + self.endpoint = CONF.v2_vnfm.endpoint + self.nfvo_client = nfvo_client.NfvoClient() + + def store_alarm_info(self, context, alarm): + # store alarm into DB + try: + alarm_utils.get_alarm(context, alarm.id) + with context.session.begin(subtransactions=True): + alarm.update(context) + except sol_ex.AlarmNotFound: + with context.session.begin(subtransactions=True): + alarm.create(context) + + # get inst + inst = inst_utils.get_inst(context, alarm.managedObjectId) + + # send notification + self.nfvo_client.send_alarm_notification( + context, alarm, inst, self.endpoint) diff --git a/tacker/sol_refactored/conductor/vnfpm_driver_v2.py b/tacker/sol_refactored/conductor/vnfpm_driver_v2.py new file mode 100644 index 000000000..6d11185a9 --- /dev/null +++ b/tacker/sol_refactored/conductor/vnfpm_driver_v2.py @@ -0,0 +1,58 @@ +# Copyright (C) 2022 Fujitsu +# All Rights Reserved. +# +# 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. + +from tacker.sol_refactored.common import config +from tacker.sol_refactored.common import pm_job_utils +from tacker.sol_refactored.nfvo import nfvo_client +from tacker.sol_refactored import objects + + +CONF = config.CONF + + +class VnfPmDriverV2(): + + def __init__(self): + self.endpoint = CONF.v2_vnfm.endpoint + self.nfvo_client = nfvo_client.NfvoClient() + + def store_job_info(self, context, report): + # store report into db + + report = self._store_report(context, report) + + # update job reports + job_id = report.jobId + timestamp = report.entries[0].performanceValues[0].timeStamp + pm_job = self._update_job_reports( + context, job_id, report, timestamp) + + # Send a notify pm job request to the NFVO client. + # POST /{pmjob.callbackUri} + self.nfvo_client.send_pm_job_notification( + report, pm_job, timestamp, self.endpoint) + + def _store_report(self, context, report): + report = objects.PerformanceReportV2.from_dict(report) + report.create(context) + return report + + def _update_job_reports(self, context, job_id, report, timestamp): + # update reports in the pmJob + update_job = pm_job_utils.update_report( + context, job_id, report, timestamp) + with context.session.begin(subtransactions=True): + update_job.update(context) + return update_job diff --git a/tacker/sol_refactored/controller/prometheus_plugin_controller.py b/tacker/sol_refactored/controller/prometheus_plugin_controller.py new file mode 100644 index 000000000..f2df3c9aa --- /dev/null +++ b/tacker/sol_refactored/controller/prometheus_plugin_controller.py @@ -0,0 +1,57 @@ +# Copyright (C) 2022 Fujitsu +# All Rights Reserved. +# +# 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. + +from tacker.sol_refactored.api import prometheus_plugin_wsgi as prom_wsgi +from tacker.sol_refactored.common import config as cfg +from tacker.sol_refactored.common import exceptions as sol_ex +from tacker.sol_refactored.common import monitoring_plugin_base as mon_base + +CONF = cfg.CONF + + +class PmEventController(prom_wsgi.PrometheusPluginAPIController): + def pm_event(self, request, body): + if not CONF.prometheus_plugin.performance_management: + raise sol_ex.PrometheusPluginNotEnabled( + name='Performance management') + cls = mon_base.get_class('pm_event') + mon_base.MonitoringPlugin.get_instance(cls).alert( + request=request, body=body) + return prom_wsgi.PrometheusPluginResponse(204, None) + + +class FmAlertController(prom_wsgi.PrometheusPluginAPIController): + def alert(self, request, body): + if not CONF.prometheus_plugin.fault_management: + raise sol_ex.PrometheusPluginNotEnabled( + name='Fault management') + cls = mon_base.get_class('alert') + mon_base.MonitoringPlugin.get_instance(cls).alert( + request=request, body=body) + return prom_wsgi.PrometheusPluginResponse(204, None) + + +class AutoScalingController(prom_wsgi.PrometheusPluginAPIController): + def auto_scaling(self, request, body): + if not CONF.prometheus_plugin.auto_scaling: + raise sol_ex.PrometheusPluginNotEnabled( + name='Auto scaling') + cls = mon_base.get_class('auto_healing') + mon_base.MonitoringPlugin.get_instance(cls).alert( + request=request, body=body) + return prom_wsgi.PrometheusPluginResponse(204, None) + + def auto_scaling_id(self, request, _, body): + return self.auto_scaling(request, body) diff --git a/tacker/sol_refactored/controller/vnffm_v1.py b/tacker/sol_refactored/controller/vnffm_v1.py new file mode 100644 index 000000000..de4ad7dae --- /dev/null +++ b/tacker/sol_refactored/controller/vnffm_v1.py @@ -0,0 +1,200 @@ +# Copyright (C) 2022 Fujitsu +# All Rights Reserved. +# +# 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. + +from oslo_log import log as logging +from oslo_utils import uuidutils + +from tacker.sol_refactored.api import api_version +from tacker.sol_refactored.api.schemas import vnffm_v1 as schema +from tacker.sol_refactored.api import validator +from tacker.sol_refactored.api import wsgi as sol_wsgi +from tacker.sol_refactored.common import config +from tacker.sol_refactored.common import coordinate +from tacker.sol_refactored.common import exceptions as sol_ex +from tacker.sol_refactored.common import fm_alarm_utils +from tacker.sol_refactored.common import fm_subscription_utils as subsc_utils +from tacker.sol_refactored.controller import vnffm_view +from tacker.sol_refactored.nfvo import nfvo_client +from tacker.sol_refactored import objects + + +LOG = logging.getLogger(__name__) # NOTE: unused at the moment + +CONF = config.CONF + + +class VnfFmControllerV1(sol_wsgi.SolAPIController): + + def __init__(self): + self.nfvo_client = nfvo_client.NfvoClient() + self.endpoint = CONF.v2_vnfm.endpoint + self._fm_view = vnffm_view.AlarmViewBuilder(self.endpoint) + self._subsc_view = vnffm_view.FmSubscriptionViewBuilder(self.endpoint) + + def supported_api_versions(self, action): + return api_version.v1_fm_versions + + def allowed_content_types(self, action): + if action == 'update': + # Content-Type of Modify request shall be + # 'application/mergepatch+json' according to SOL spec. + # But 'application/json' and 'text/plain' is OK for backward + # compatibility. + return ['application/mergepatch+json', 'application/json', + 'text/plain'] + return ['application/json', 'text/plain'] + + def index(self, request): + filter_param = request.GET.get('filter') + if filter_param is not None: + filters = self._fm_view.parse_filter(filter_param) + else: + filters = None + + page_size = CONF.v2_vnfm.vnffm_alarm_page_size + pager = self._fm_view.parse_pager(request, page_size) + + alarms = fm_alarm_utils.get_alarms_all(request.context, + marker=pager.marker) + + resp_body = self._fm_view.detail_list(alarms, filters, None, pager) + + return sol_wsgi.SolResponse( + 200, resp_body, version=api_version.CURRENT_FM_VERSION, + link=pager.get_link()) + + def show(self, request, id): + alarm = fm_alarm_utils.get_alarm(request.context, id) + + resp_body = self._fm_view.detail(alarm) + + return sol_wsgi.SolResponse(200, resp_body, + version=api_version.CURRENT_FM_VERSION) + + @validator.schema(schema.AlarmModifications_V130, '1.3.0') + @coordinate.lock_resources('{id}') + def update(self, request, id, body): + context = request.context + alarm = fm_alarm_utils.get_alarm(context, id) + + ack_state = body['ackState'] + + if alarm.ackState == ack_state: + raise sol_ex.AckStateInvalid() + + alarm.ackState = ack_state + with context.session.begin(subtransactions=True): + alarm.update(context) + + return sol_wsgi.SolResponse(200, body, + version=api_version.CURRENT_FM_VERSION) + + @validator.schema(schema.FmSubscriptionRequest_V130, '1.3.0') + def subscription_create(self, request, body): + context = request.context + subsc = objects.FmSubscriptionV1( + id=uuidutils.generate_uuid(), + callbackUri=body['callbackUri'] + ) + + if body.get('filter'): + subsc.filter = ( + objects.FmNotificationsFilterV1.from_dict( + body['filter']) + ) + + auth_req = body.get('authentication') + if auth_req: + auth = objects.SubscriptionAuthentication( + authType=auth_req['authType'] + ) + if 'BASIC' in auth.authType: + basic_req = auth_req.get('paramsBasic') + if basic_req is None: + msg = "ParamsBasic must be specified." + raise sol_ex.InvalidSubscription(sol_detail=msg) + auth.paramsBasic = ( + objects.SubscriptionAuthentication_ParamsBasic( + userName=basic_req.get('userName'), + password=basic_req.get('password') + ) + ) + + if 'OAUTH2_CLIENT_CREDENTIALS' in auth.authType: + oauth2_req = auth_req.get('paramsOauth2ClientCredentials') + if oauth2_req is None: + msg = "paramsOauth2ClientCredentials must be specified." + raise sol_ex.InvalidSubscription(sol_detail=msg) + auth.paramsOauth2ClientCredentials = ( + objects.SubscriptionAuthentication_ParamsOauth2( + clientId=oauth2_req.get('clientId'), + clientPassword=oauth2_req.get('clientPassword'), + tokenEndpoint=oauth2_req.get('tokenEndpoint') + ) + ) + + if 'TLS_CERT' in auth.authType: + msg = "'TLS_CERT' is not supported at the moment." + raise sol_ex.InvalidSubscription(sol_detail=msg) + + subsc.authentication = auth + + if CONF.v2_nfvo.test_callback_uri: + subsc_utils.test_notification(subsc) + + subsc.create(context) + + resp_body = self._subsc_view.detail(subsc) + self_href = subsc_utils.subsc_href(subsc.id, self.endpoint) + + return sol_wsgi.SolResponse( + 201, resp_body, version=api_version.CURRENT_FM_VERSION, + location=self_href) + + def subscription_list(self, request): + filter_param = request.GET.get('filter') + if filter_param is not None: + filters = self._subsc_view.parse_filter(filter_param) + else: + filters = None + + page_size = CONF.v2_vnfm.subscription_page_size + pager = self._subsc_view.parse_pager(request, page_size) + + subscs = subsc_utils.get_subsc_all(request.context, + marker=pager.marker) + + resp_body = self._subsc_view.detail_list(subscs, filters, None, pager) + + return sol_wsgi.SolResponse( + 200, resp_body, version=api_version.CURRENT_FM_VERSION, + link=pager.get_link()) + + def subscription_show(self, request, id): + subsc = subsc_utils.get_subsc(request.context, id) + + resp_body = self._subsc_view.detail(subsc) + + return sol_wsgi.SolResponse(200, resp_body, + version=api_version.CURRENT_FM_VERSION) + + def subscription_delete(self, request, id): + context = request.context + subsc = subsc_utils.get_subsc(request.context, id) + + subsc.delete(context) + + return sol_wsgi.SolResponse(204, None, + version=api_version.CURRENT_FM_VERSION) diff --git a/tacker/sol_refactored/controller/vnffm_view.py b/tacker/sol_refactored/controller/vnffm_view.py new file mode 100644 index 000000000..047e0395e --- /dev/null +++ b/tacker/sol_refactored/controller/vnffm_view.py @@ -0,0 +1,65 @@ +# Copyright (C) 2022 Fujitsu +# All Rights Reserved. +# +# 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. + +from oslo_log import log as logging + +from tacker.sol_refactored.common import config +from tacker.sol_refactored.common import fm_alarm_utils as alarm_utils +from tacker.sol_refactored.common import fm_subscription_utils as subsc_utils +from tacker.sol_refactored.controller import vnflcm_view as base_view +from tacker.sol_refactored import objects + + +LOG = logging.getLogger(__name__) +CONF = config.CONF + + +class AlarmViewBuilder(base_view.BaseViewBuilder): + _EXCLUDE_DEFAULT = [] + + def __init__(self, endpoint): + self.endpoint = endpoint + + def detail(self, alarm, selector=None): + # NOTE: _links is not saved in DB. create when it is necessary. + if not alarm.obj_attr_is_set('_links'): + alarm._links = alarm_utils.make_alarm_links(alarm, self.endpoint) + + resp = alarm.to_dict() + + if selector is not None: + resp = selector.filter(alarm, resp) + return resp + + +class FmSubscriptionViewBuilder(base_view.BaseViewBuilder): + def __init__(self, endpoint): + self.endpoint = endpoint + + def detail(self, subsc, selector=None): + # NOTE: _links is not saved in DB. create when it is necessary. + if not subsc.obj_attr_is_set('_links'): + self_href = subsc_utils.subsc_href(subsc.id, self.endpoint) + subsc._links = objects.FmSubscriptionV1_Links() + subsc._links.self = objects.Link(href=self_href) + + resp = subsc.to_dict() + + # NOTE: authentication is not included in FmSubscriptionV1 + resp.pop('authentication', None) + + if selector is not None: + resp = selector.filter(subsc, resp) + return resp diff --git a/tacker/sol_refactored/controller/vnfpm_v2.py b/tacker/sol_refactored/controller/vnfpm_v2.py new file mode 100644 index 000000000..e14e146cf --- /dev/null +++ b/tacker/sol_refactored/controller/vnfpm_v2.py @@ -0,0 +1,297 @@ +# Copyright (C) 2022 Fujitsu +# All Rights Reserved. +# +# 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. + +import copy +import datetime + +from oslo_log import log as logging +from oslo_utils import uuidutils + +from tacker.sol_refactored.api import api_version +from tacker.sol_refactored.api.schemas import vnfpm_v2 as schema +from tacker.sol_refactored.api import validator +from tacker.sol_refactored.api import wsgi as sol_wsgi +from tacker.sol_refactored.common import config +from tacker.sol_refactored.common import coordinate +from tacker.sol_refactored.common import exceptions as sol_ex +from tacker.sol_refactored.common import monitoring_plugin_base as plugin +from tacker.sol_refactored.common import pm_job_utils +from tacker.sol_refactored.common import vnf_instance_utils as inst_utils +from tacker.sol_refactored.controller import vnfpm_view +from tacker.sol_refactored.nfvo import nfvo_client +from tacker.sol_refactored import objects + +LOG = logging.getLogger(__name__) # NOTE: unused at the moment + +CONF = config.CONF + +OBJ_TYPE_TO_GROUP_TYPE = { + 'Vnf': 'VirtualisedComputeResource', + 'Vnfc': 'VirtualisedComputeResource', + 'VnfIntCP': 'VnfInternalCP', + 'VnfExtCP': 'VnfExternalCP' +} + +OBJ_TYPE_TO_METRIC_LISt = { + 'Vnf': {'VCpuUsageMeanVnf', 'VCpuUsagePeakVnf', + 'VMemoryUsageMeanVnf', 'VMemoryUsagePeakVnf', + 'VDiskUsageMeanVnf', 'VDiskUsagePeakVnf'}, + 'Vnfc': {'VCpuUsageMeanVnf', 'VCpuUsagePeakVnf', + 'VMemoryUsageMeanVnf', 'VMemoryUsagePeakVnf', + 'VDiskUsageMeanVnf', 'VDiskUsagePeakVnf'}, + 'VnfIntCP': {'ByteIncomingVnfIntCp', 'ByteOutgoingVnfIntCp', + 'PacketIncomingVnfIntCp', 'PacketOutgoingVnfIntCp'}, + 'VnfExtCP': {'ByteIncomingVnfExtCp', 'ByteOutgoingVnfExtCp', + 'PacketIncomingVnfExtCp', 'PacketOutgoingVnfExtCp'} +} + + +def _check_http_client_auth(auth_req): + auth = objects.SubscriptionAuthentication( + authType=auth_req['authType'] + ) + + if 'BASIC' in auth.authType: + basic_req = auth_req.get('paramsBasic') + if basic_req is None: + msg = "ParamsBasic must be specified." + raise sol_ex.InvalidSubscription(sol_detail=msg) + auth.paramsBasic = ( + objects.SubscriptionAuthentication_ParamsBasic( + userName=basic_req.get('userName'), + password=basic_req.get('password') + ) + ) + + if 'OAUTH2_CLIENT_CREDENTIALS' in auth.authType: + oauth2_req = auth_req.get('paramsOauth2ClientCredentials') + if oauth2_req is None: + msg = "paramsOauth2ClientCredentials must be specified." + raise sol_ex.InvalidSubscription(sol_detail=msg) + auth.paramsOauth2ClientCredentials = ( + objects.SubscriptionAuthentication_ParamsOauth2( + clientId=oauth2_req.get('clientId'), + clientPassword=oauth2_req.get('clientPassword'), + tokenEndpoint=oauth2_req.get('tokenEndpoint') + ) + ) + + if 'TLS_CERT' in auth.authType: + msg = "'TLS_CERT' is not supported at the moment." + raise sol_ex.InvalidSubscription(sol_detail=msg) + return auth + + +def _check_performance_metric_or_group( + obj_type, metric_group, performance_metric): + # Check whether the object_type is consistent with the corresponding + # group name + if metric_group and ( + len(metric_group) != 1 or + metric_group[0] != OBJ_TYPE_TO_GROUP_TYPE[obj_type]): + raise sol_ex.PMJobInvalidRequest + + # Check if the type in performance metric matches the standard type. + if performance_metric: + metric_types = {metric.split('.')[0] for metric in performance_metric} + if not metric_types.issubset(OBJ_TYPE_TO_METRIC_LISt[obj_type]): + raise sol_ex.PMJobInvalidRequest + + +class VnfPmControllerV2(sol_wsgi.SolAPIController): + + def __init__(self): + self.nfvo_client = nfvo_client.NfvoClient() + self.endpoint = CONF.v2_vnfm.endpoint + self._pm_job_view = vnfpm_view.PmJobViewBuilder(self.endpoint) + cls = plugin.get_class('pm_event') + self.plugin = plugin.MonitoringPlugin.get_instance(cls) + + @validator.schema(schema.CreatePmJobRequest_V210, '2.1.0') + def create(self, request, body): + context = request.context + + # check request body + # If this `subObjectInstanceIds` is present, the cardinality of the + # `objectInstanceIds` attribute shall be 1. + if (body.get("subObjectInstanceIds") and + len(body.get("objectInstanceIds")) > 1): + raise sol_ex.PMJobInvalidRequest + + # At least one of the two attributes (performance + # metric or group) shall be present. + metric_group = body["criteria"].get('performanceMetricGroup') + performance_metric = body["criteria"].get('performanceMetric') + if not metric_group and not performance_metric: + raise sol_ex.PMJobInvalidRequest + + # check the value of group or performance_metric + _check_performance_metric_or_group( + body['objectType'], metric_group, performance_metric) + + # check vnf instance status + inst_ids = body["objectInstanceIds"] + for inst_id in inst_ids: + inst = inst_utils.get_inst(context, inst_id) + if inst.instantiationState == 'NOT_INSTANTIATED': + raise sol_ex.VnfInstanceIsNotInstantiated(inst_id=inst_id) + + # pm_job.criteria + pm_job_criteria = objects.VnfPmJobCriteriaV2( + collectionPeriod=body["criteria"]['collectionPeriod'], + reportingPeriod=body["criteria"]['reportingPeriod'] + ) + criteria = body["criteria"] + if performance_metric: + pm_job_criteria.performanceMetric = criteria['performanceMetric'] + if metric_group: + pm_job_criteria.performanceMetricGroup = criteria[ + 'performanceMetricGroup'] + if criteria.get('reportingBoundary'): + try: + dt = copy.deepcopy(criteria['reportingBoundary']) + datetime.datetime.fromisoformat(dt.replace('Z', '+00:00')) + except ValueError as ex: + raise sol_ex.SolValidationError( + detail="invalid date format.") from ex + pm_job_criteria.reportingBoundary = criteria['reportingBoundary'] + + # pm_job + pm_job_id = uuidutils.generate_uuid() + pm_job = objects.PmJobV2( + id=pm_job_id, + objectType=body["objectType"], + objectInstanceIds=body["objectInstanceIds"], + criteria=pm_job_criteria, + callbackUri=body["callbackUri"], + reports=[], + ) + if body.get("subObjectInstanceIds"): + pm_job.subObjectInstanceIds = body["subObjectInstanceIds"] + + # authentication + auth_req = body.get('authentication') + if auth_req: + pm_job.authentication = _check_http_client_auth(auth_req) + + # metadata + metadata = body.get('metadata') + if metadata: + pm_job.metadata = metadata + + if CONF.v2_nfvo.test_callback_uri: + pm_job_utils.test_notification(pm_job) + + try: + self.plugin.create_job(context=context, pm_job=pm_job) + except sol_ex.PrometheusPluginError as e: + raise sol_ex.PrometheusSettingFailed from e + + pm_job.create(context) + + location = pm_job_utils.pm_job_href(pm_job.id, self.endpoint) + resp_body = self._pm_job_view.detail(pm_job) + + return sol_wsgi.SolResponse(201, resp_body, + version=api_version.CURRENT_PM_VERSION, + location=location) + + def index(self, request): + filter_param = request.GET.get('filter') + if filter_param is not None: + filters = self._pm_job_view.parse_filter(filter_param) + else: + filters = None + + # validate_filter + selector = self._pm_job_view.parse_selector(request.GET) + page_size = CONF.v2_vnfm.vnfpm_pmjob_page_size + pager = self._pm_job_view.parse_pager(request, page_size) + pm_job = pm_job_utils.get_pm_job_all(request.context, + marker=pager.marker) + resp_body = self._pm_job_view.detail_list(pm_job, filters, + selector, pager) + + return sol_wsgi.SolResponse(200, resp_body, + version=api_version.CURRENT_PM_VERSION, + link=pager.get_link()) + + def show(self, request, id): + pm_job = pm_job_utils.get_pm_job(request.context, id) + pm_job_resp = self._pm_job_view.detail(pm_job) + return sol_wsgi.SolResponse(200, pm_job_resp, + version=api_version.CURRENT_PM_VERSION) + + @validator.schema(schema.PmJobModificationsRequest_V210, '2.1.0') + @coordinate.lock_resources('{id}') + def update(self, request, id, body): + context = request.context + + pm_job = pm_job_utils.get_pm_job(context, id) + if body.get("callbackUri"): + pm_job.callbackUri = body.get("callbackUri") + if body.get("authentication"): + pm_job.authentication = _check_http_client_auth( + body.get("authentication")) + + if CONF.v2_nfvo.test_callback_uri: + pm_job_utils.test_notification(pm_job) + + with context.session.begin(subtransactions=True): + pm_job.update(context) + + pm_job_modifications = objects.PmJobModificationsV2( + callbackUri=pm_job.callbackUri, + ) + resp = pm_job_modifications.to_dict() + + return sol_wsgi.SolResponse(200, resp, + version=api_version.CURRENT_PM_VERSION) + + @coordinate.lock_resources('{id}') + def delete(self, request, id): + context = request.context + pm_job = pm_job_utils.get_pm_job(context, id) + + self.plugin.delete_job(context=context, pm_job=pm_job) + + reports = objects.PerformanceReportV2.get_by_filter(context, + jobId=pm_job.id) + for report in reports: + report.delete(context) + pm_job.delete(context) + + return sol_wsgi.SolResponse(204, None, + version=api_version.CURRENT_PM_VERSION) + + def report_get(self, request, id, report_id): + pm_report = pm_job_utils.get_pm_report( + request.context, id, report_id) + pm_report_resp = self._pm_job_view.report_detail(pm_report) + return sol_wsgi.SolResponse(200, pm_report_resp, + version=api_version.CURRENT_PM_VERSION) + + def allowed_content_types(self, action): + if action == 'update': + # Content-Type of Modify request shall be + # 'application/mergepatch+json' according to SOL spec. + # But 'application/json' and 'text/plain' is OK for backward + # compatibility. + return ['application/mergepatch+json', 'application/json', + 'text/plain'] + return ['application/json', 'text/plain'] + + def supported_api_versions(self, action): + return api_version.v2_pm_versions diff --git a/tacker/sol_refactored/controller/vnfpm_view.py b/tacker/sol_refactored/controller/vnfpm_view.py new file mode 100644 index 000000000..4c24ee50e --- /dev/null +++ b/tacker/sol_refactored/controller/vnfpm_view.py @@ -0,0 +1,54 @@ +# Copyright (C) 2022 Fujitsu +# All Rights Reserved. +# +# 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. + +from oslo_log import log as logging + +from tacker.sol_refactored.common import config +from tacker.sol_refactored.common import pm_job_utils +from tacker.sol_refactored.controller import vnflcm_view as base_view + + +LOG = logging.getLogger(__name__) +CONF = config.CONF + + +class PmJobViewBuilder(base_view.BaseViewBuilder): + _EXCLUDE_DEFAULT = [] + + def __init__(self, endpoint): + self.endpoint = endpoint + + def detail(self, pm_job, selector=None): + # NOTE: _links is not saved in DB. create when it is necessary. + if not pm_job.obj_attr_is_set('_links'): + pm_job._links = pm_job_utils.make_pm_job_links( + pm_job, self.endpoint) + + resp = pm_job.to_dict() + if resp.get('authentication'): + resp.pop('authentication', None) + if resp.get('metadata'): + resp.pop('metadata', None) + if selector is not None: + resp = selector.filter(pm_job, resp) + return resp + + def report_detail(self, pm_report): + resp = pm_report.to_dict() + if resp.get('id'): + resp.pop('id') + if resp.get('jobId'): + resp.pop('jobId') + return resp diff --git a/tacker/sol_refactored/db/sqlalchemy/models.py b/tacker/sol_refactored/db/sqlalchemy/models.py index 331496699..a37f6a10c 100644 --- a/tacker/sol_refactored/db/sqlalchemy/models.py +++ b/tacker/sol_refactored/db/sqlalchemy/models.py @@ -152,3 +152,88 @@ class GrantRequestV1(model_base.BASE): placementConstraints = sa.Column(sa.JSON(), nullable=True) vimConstraints = sa.Column(sa.JSON(), nullable=True) additionalParams = sa.Column(sa.JSON(), nullable=True) + + +class AlarmV1(model_base.BASE): + """Type: Alarm + + NFV-SOL 003 + - v3.3.1 7.5.2.4 (API version: 1.3.0) + """ + + __tablename__ = 'AlarmV1' + id = sa.Column(sa.String(255), nullable=False, primary_key=True) + managedObjectId = sa.Column(sa.String(255), nullable=False) + vnfcInstanceIds = sa.Column(sa.JSON(), nullable=True) + rootCauseFaultyResource = sa.Column(sa.JSON(), nullable=True) + alarmRaisedTime = sa.Column(sa.DateTime(), nullable=False) + alarmChangedTime = sa.Column(sa.DateTime(), nullable=True) + alarmClearedTime = sa.Column(sa.DateTime(), nullable=True) + alarmAcknowledgedTime = sa.Column(sa.DateTime(), nullable=True) + ackState = sa.Column(sa.Enum( + 'UNACKNOWLEDGED', 'ACKNOWLEDGED', create_constraint=True, + validate_strings=True), nullable=False) + perceivedSeverity = sa.Column(sa.Enum( + 'CRITICAL', 'MAJOR', 'MINOR', 'WARNING', 'INDETERMINATE', 'CLEARED', + create_constraint=True, validate_strings=True), nullable=False) + eventTime = sa.Column(sa.DateTime(), nullable=False) + eventType = sa.Column(sa.Enum( + 'COMMUNICATIONS_ALARM', 'PROCESSING_ERROR_ALARM', + 'ENVIRONMENTAL_ALARM', 'QOS_ALARM', 'EQUIPMENT_ALARM', + create_constraint=True, validate_strings=True), nullable=False) + faultType = sa.Column(sa.String(255), nullable=True) + probableCause = sa.Column(sa.String(255), nullable=False) + isRootCause = sa.Column(sa.Boolean, nullable=False) + correlatedAlarmIds = sa.Column(sa.JSON(), nullable=True) + faultDetails = sa.Column(sa.JSON(), nullable=True) + + +class FmSubscriptionV1(model_base.BASE): + """Type: FmSubscription + + NFV-SOL 003 + - v3.3.1 7.5.2.3 (API version: 1.3.0) + """ + + __tablename__ = 'FmSubscriptionV1' + id = sa.Column(sa.String(255), nullable=False, primary_key=True) + filter = sa.Column(sa.JSON(), nullable=True) + callbackUri = sa.Column(sa.String(255), nullable=False) + # NOTE: 'authentication' attribute is not included in the + # original 'FmSubscription' data type definition. + authentication = sa.Column(sa.JSON(), nullable=True) + + +class PmJobV2(model_base.BASE): + """Type: PmJob + + NFV-SOL 003 + - v3.3.1 6.5.2.7 (API version: 2.1.0) + """ + + __tablename__ = 'PmJobV2' + id = sa.Column(sa.String(255), nullable=False, primary_key=True) + objectType = sa.Column(sa.String(32), nullable=False) + objectInstanceIds = sa.Column(sa.JSON(), nullable=False) + subObjectInstanceIds = sa.Column(sa.JSON(), nullable=True) + criteria = sa.Column(sa.JSON(), nullable=False) + callbackUri = sa.Column(sa.String(255), nullable=False) + reports = sa.Column(sa.JSON(), nullable=True) + # NOTE: 'authentication' attribute is not included in the + # original 'PmJob' data type definition. + authentication = sa.Column(sa.JSON(), nullable=True) + # NOTE: 'metadata' attribute is not included in the + # original 'PmJob' data type definition. + metadata__ = sa.Column("metadata", sa.JSON(), nullable=True) + + +class PerformanceReportV2(model_base.BASE): + """Type: Report + + NFV-SOL 003 + - v3.3.1 6.5.2.10 (API version: 2.1.0) + """ + __tablename__ = 'PerformanceReportV2' + id = sa.Column(sa.String(255), nullable=False, primary_key=True) + jobId = sa.Column(sa.String(255), nullable=False, primary_key=False) + entries = sa.Column(sa.JSON(), nullable=False) diff --git a/tacker/sol_refactored/nfvo/nfvo_client.py b/tacker/sol_refactored/nfvo/nfvo_client.py index e68bac2d0..5b7094433 100644 --- a/tacker/sol_refactored/nfvo/nfvo_client.py +++ b/tacker/sol_refactored/nfvo/nfvo_client.py @@ -17,8 +17,11 @@ from oslo_log import log as logging from tacker.sol_refactored.common import config +from tacker.sol_refactored.common import fm_alarm_utils as alarm_utils +from tacker.sol_refactored.common import fm_subscription_utils as fm_utils from tacker.sol_refactored.common import http_client from tacker.sol_refactored.common import lcm_op_occ_utils as lcmocc_utils +from tacker.sol_refactored.common import pm_job_utils from tacker.sol_refactored.common import subscription_utils as subsc_utils from tacker.sol_refactored.common import vnfd_utils from tacker.sol_refactored.nfvo import local_nfvo @@ -144,3 +147,24 @@ class NfvoClient(object): if self.is_local: self.nfvo.recv_lcmocc_notification(context, lcmocc, inst) + + def send_alarm_notification(self, context, alarm, inst, endpoint): + subscs = fm_utils.get_alarm_subscs(context, alarm, inst) + for subsc in subscs: + notif_data = alarm_utils.make_alarm_notif_data( + subsc, alarm, endpoint) + fm_utils.send_notification(subsc, notif_data) + + def send_pm_job_notification(self, report, pm_job, timestamp, endpoint): + report_object_instance_id = {entry.objectInstanceId + for entry in report.entries} + for instance_id in report_object_instance_id: + sub_instance_ids = [ + entry.subObjectInstanceId for entry in report.entries + if (entry.objectInstanceId == instance_id and + entry.obj_attr_is_set('subObjectInstanceId')) + ] + notif_data = pm_job_utils.make_pm_notif_data( + instance_id, sub_instance_ids, report.id, + pm_job, timestamp, endpoint) + pm_job_utils.send_notification(pm_job, notif_data) diff --git a/tacker/sol_refactored/objects/__init__.py b/tacker/sol_refactored/objects/__init__.py index 07017c593..0595fbd49 100644 --- a/tacker/sol_refactored/objects/__init__.py +++ b/tacker/sol_refactored/objects/__init__.py @@ -42,8 +42,14 @@ def register_all(): __import__(objects_root + '.common.vnf_ext_cp_config') __import__(objects_root + '.common.vnf_ext_cp_data') __import__(objects_root + '.common.vnf_instance_subscription_filter') + __import__(objects_root + '.v1.alarm') + __import__(objects_root + '.v1.alarm_cleared_notification') + __import__(objects_root + '.v1.alarm_notification') __import__(objects_root + '.v1.constraint_resource_ref') __import__(objects_root + '.v1.fields') + __import__(objects_root + '.v1.fm_notifications_filter') + __import__(objects_root + '.v1.fm_subscription') + __import__(objects_root + '.v1.fm_subscription_request') __import__(objects_root + '.v1.grant_info') __import__(objects_root + '.v1.grant') __import__(objects_root + '.v1.grant_request') @@ -84,9 +90,15 @@ def register_all(): __import__(objects_root + '.v2.modifications_triggered_by_vnf_pkg_change') __import__(objects_root + '.v2.monitoring_parameter') __import__(objects_root + '.v2.operate_vnf_request') + __import__(objects_root + + '.v2.performance_information_available_notification') __import__(objects_root + '.v2.pkgm_links') __import__(objects_root + '.v2.pkgm_notification_filter') __import__(objects_root + '.v2.pkgm_subscription_request') + __import__(objects_root + '.v2.pm_job') + __import__(objects_root + '.v2.pm_job_criteria') + __import__(objects_root + '.v2.pm_job_modification') + __import__(objects_root + '.v2.pm_report') __import__(objects_root + '.v2.revert_to_vnf_snapshot_request') __import__(objects_root + '.v2.scale_info') __import__(objects_root + '.v2.scale_vnf_request') diff --git a/tacker/sol_refactored/objects/v1/alarm.py b/tacker/sol_refactored/objects/v1/alarm.py new file mode 100644 index 000000000..8a5f8717d --- /dev/null +++ b/tacker/sol_refactored/objects/v1/alarm.py @@ -0,0 +1,84 @@ +# Copyright (C) 2022 Fujitsu +# All Rights Reserved. +# +# 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. + +from tacker.sol_refactored.objects import base +from tacker.sol_refactored.objects import fields +from tacker.sol_refactored.objects.v1 import fields as v1fields + + +# NFV-SOL 003 +# - v3.3.1 7.5.2.4 (API version: 1.3.0) +@base.TackerObjectRegistry.register +class AlarmV1(base.TackerPersistentObject, base.TackerObjectDictCompat): + + # Version 1.0: Initial version + VERSION = '1.0' + + fields = { + 'id': fields.StringField(nullable=False), + 'managedObjectId': fields.StringField(nullable=False), + # NOTE: vnfcInstanceIds is defined in NFV-SOL 002 v3.3.1 7.5.2.4 + 'vnfcInstanceIds': fields.ListOfStringsField(nullable=True), + 'rootCauseFaultyResource': fields.ObjectField( + 'AlarmV1_FaultyResourceInfo', nullable=True), + 'alarmRaisedTime': fields.DateTimeField(nullable=False), + 'alarmChangedTime': fields.DateTimeField(nullable=True), + 'alarmClearedTime': fields.DateTimeField(nullable=True), + 'alarmAcknowledgedTime': fields.DateTimeField(nullable=True), + 'ackState': fields.EnumField( + valid_values=[ + 'UNACKNOWLEDGED', + 'ACKNOWLEDGED', + ], + nullable=False), + 'perceivedSeverity': v1fields.PerceivedSeverityTypeField( + nullable=False), + 'eventTime': fields.DateTimeField(nullable=False), + 'eventType': v1fields.EventTypeField(nullable=False), + 'faultType': fields.StringField(nullable=True), + 'probableCause': fields.StringField(nullable=False), + 'isRootCause': fields.BooleanField(nullable=False), + 'correlatedAlarmIds': fields.ListOfStringsField(nullable=True), + 'faultDetails': fields.ListOfStringsField(nullable=True), + '_links': fields.ObjectField('AlarmV1_Links', nullable=False), + + } + + +@base.TackerObjectRegistry.register +class AlarmV1_FaultyResourceInfo(base.TackerObject, + base.TackerObjectDictCompat): + + # Version 1.0: Initial version + VERSION = '1.0' + + fields = { + 'faultyResource': fields.ObjectField( + 'ResourceHandle', nullable=False), + 'faultyResourceType': v1fields.FaultyResourceTypeField( + nullable=False) + } + + +@base.TackerObjectRegistry.register +class AlarmV1_Links(base.TackerObject): + + # Version 1.0: Initial version + VERSION = '1.0' + + fields = { + 'self': fields.ObjectField('Link', nullable=False), + 'objectInstance': fields.ObjectField('Link', nullable=True) + } diff --git a/tacker/sol_refactored/objects/v1/alarm_cleared_notification.py b/tacker/sol_refactored/objects/v1/alarm_cleared_notification.py new file mode 100644 index 000000000..fbccb0559 --- /dev/null +++ b/tacker/sol_refactored/objects/v1/alarm_cleared_notification.py @@ -0,0 +1,50 @@ +# Copyright (C) 2022 Fujitsu +# All Rights Reserved. +# +# 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. + +from tacker.sol_refactored.objects import base +from tacker.sol_refactored.objects import fields + + +# NFV-SOL 003 +# - v3.3.1 7.5.2.6 (API version: 1.3.0) +@base.TackerObjectRegistry.register +class AlarmClearedNotificationV1(base.TackerObject, + base.TackerObjectDictCompat): + + # Version 1.0: Initial version + VERSION = '1.0' + + fields = { + 'id': fields.StringField(nullable=False), + 'notificationType': fields.StringField(nullable=False), + 'subscriptionId': fields.StringField(nullable=False), + 'timeStamp': fields.DateTimeField(nullable=False), + 'alarmId': fields.StringField(nullable=False), + 'alarmClearedTime': fields.DateTimeField(nullable=False), + '_links': fields.ObjectField( + 'AlarmClearedNotificationV1_Links', nullable=False) + } + + +@base.TackerObjectRegistry.register +class AlarmClearedNotificationV1_Links(base.TackerObject): + + # Version 1.0: Initial version + VERSION = '1.0' + + fields = { + 'subscription': fields.ObjectField('NotificationLink', nullable=False), + 'alarm': fields.ObjectField('NotificationLink', nullable=True) + } diff --git a/tacker/sol_refactored/objects/v1/alarm_notification.py b/tacker/sol_refactored/objects/v1/alarm_notification.py new file mode 100644 index 000000000..cc3ce7997 --- /dev/null +++ b/tacker/sol_refactored/objects/v1/alarm_notification.py @@ -0,0 +1,47 @@ +# Copyright (C) 2022 Fujitsu +# All Rights Reserved. +# +# 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. + +from tacker.sol_refactored.objects import base +from tacker.sol_refactored.objects import fields + + +# NFV-SOL 003 +# - v3.3.1 7.5.2.5 (API version: 1.3.0) +@base.TackerObjectRegistry.register +class AlarmNotificationV1(base.TackerObject, base.TackerObjectDictCompat): + + # Version 1.0: Initial version + VERSION = '1.0' + + fields = { + 'id': fields.StringField(nullable=False), + 'notificationType': fields.StringField(nullable=False), + 'subscriptionId': fields.StringField(nullable=False), + 'timeStamp': fields.DateTimeField(nullable=False), + 'alarm': fields.ObjectField('AlarmV1', nullable=False), + '_links': fields.ObjectField( + 'AlarmNotificationV1_Links', nullable=False) + } + + +@base.TackerObjectRegistry.register +class AlarmNotificationV1_Links(base.TackerObject): + + # Version 1.0: Initial version + VERSION = '1.0' + + fields = { + 'subscription': fields.ObjectField('NotificationLink', nullable=False) + } diff --git a/tacker/sol_refactored/objects/v1/fields.py b/tacker/sol_refactored/objects/v1/fields.py index 1ce34e42f..3397393e2 100644 --- a/tacker/sol_refactored/objects/v1/fields.py +++ b/tacker/sol_refactored/objects/v1/fields.py @@ -41,3 +41,60 @@ class GrantedLcmOperationType(fields.BaseTackerEnum): class GrantedLcmOperationTypeField(fields.BaseEnumField): AUTO_TYPE = GrantedLcmOperationType() + + +# NFV-SOL 003 +# - v2.6.1 7.5.4.3 (API version: 1.2.0) +# - v2.7.1 7.5.4.3 (API version: 1.3.0) +# - v2.8.1 7.5.4.3 (API version: 1.3.0) +# - v3.3.1 7.5.4.3 (API version: 1.3.0) +class PerceivedSeverityType(fields.BaseTackerEnum): + CRITICAL = 'CRITICAL' + MAJOR = 'MAJOR' + MINOR = 'MINOR' + WARNING = 'WARNING' + INDETERMINATE = 'INDETERMINATE' + CLEARED = 'CLEARED' + + ALL = (CRITICAL, MAJOR, MINOR, WARNING, INDETERMINATE, CLEARED) + + +class PerceivedSeverityTypeField(fields.BaseEnumField): + AUTO_TYPE = PerceivedSeverityType() + + +# NFV-SOL 003 +# - v2.6.1 7.5.4.5 (API version: 1.2.0) +# - v2.7.1 7.5.4.5 (API version: 1.3.0) +# - v2.8.1 7.5.4.5 (API version: 1.3.0) +# - v3.3.1 7.5.4.5 (API version: 1.3.0) +class EventType(fields.BaseTackerEnum): + COMMUNICATIONS_ALARM = 'COMMUNICATIONS_ALARM' + PROCESSING_ERROR_ALARM = 'PROCESSING_ERROR_ALARM' + ENVIRONMENTAL_ALARM = 'ENVIRONMENTAL_ALARM' + QOS_ALARM = 'QOS_ALARM' + EQUIPMENT_ALARM = 'EQUIPMENT_ALARM' + + ALL = (COMMUNICATIONS_ALARM, PROCESSING_ERROR_ALARM, + ENVIRONMENTAL_ALARM, QOS_ALARM, EQUIPMENT_ALARM) + + +class EventTypeField(fields.BaseEnumField): + AUTO_TYPE = EventType() + + +# NFV-SOL 003 +# - v2.6.1 7.5.4.5 (API version: 1.2.0) +# - v2.7.1 7.5.4.5 (API version: 1.3.0) +# - v2.8.1 7.5.4.5 (API version: 1.3.0) +# - v3.3.1 7.5.4.5 (API version: 1.3.0) +class FaultyResourceType(fields.BaseTackerEnum): + COMPUTE = 'COMPUTE' + STORAGE = 'STORAGE' + NETWORK = 'NETWORK' + + ALL = (COMPUTE, STORAGE, NETWORK) + + +class FaultyResourceTypeField(fields.BaseEnumField): + AUTO_TYPE = FaultyResourceType() diff --git a/tacker/sol_refactored/objects/v1/fm_notifications_filter.py b/tacker/sol_refactored/objects/v1/fm_notifications_filter.py new file mode 100644 index 000000000..713e24f3b --- /dev/null +++ b/tacker/sol_refactored/objects/v1/fm_notifications_filter.py @@ -0,0 +1,47 @@ +# Copyright (C) 2022 Fujitsu +# All Rights Reserved. +# +# 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. + +from tacker.sol_refactored.objects import base +from tacker.sol_refactored.objects import fields +from tacker.sol_refactored.objects.v1 import fields as v1fields + + +# NFV-SOL 003 +# - v3.3.1 7.5.3.2 (API version: 1.3.0) +@base.TackerObjectRegistry.register +class FmNotificationsFilterV1(base.TackerObject, base.TackerObjectDictCompat): + + # Version 1.0: Initial version + VERSION = '1.0' + + fields = { + 'vnfInstanceSubscriptionFilter': fields.ObjectField( + 'VnfInstanceSubscriptionFilter', nullable=True), + 'notificationTypes': fields.ListOfEnumField( + valid_values=[ + 'AlarmNotification', + 'AlarmClearedNotification', + 'AlarmListRebuiltNotification', + ], + nullable=True, + ), + 'faultyResourceTypes': fields.Field(fields.List( + v1fields.FaultyResourceTypeField(), nullable=True)), + 'perceivedSeverities': fields.Field(fields.List( + v1fields.PerceivedSeverityTypeField(), nullable=True)), + 'eventTypes': fields.Field(fields.List( + v1fields.EventTypeField(), nullable=True)), + 'probableCauses': fields.ListOfStringsField(nullable=True) + } diff --git a/tacker/sol_refactored/objects/v1/fm_subscription.py b/tacker/sol_refactored/objects/v1/fm_subscription.py new file mode 100644 index 000000000..3a71e2dc8 --- /dev/null +++ b/tacker/sol_refactored/objects/v1/fm_subscription.py @@ -0,0 +1,52 @@ +# Copyright (C) 2022 Fujitsu +# All Rights Reserved. +# +# 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. + +from tacker.sol_refactored.objects import base +from tacker.sol_refactored.objects import fields + + +# NFV-SOL 003 +# - v3.3.1 7.5.2.3 (API version: 1.3.0) +@base.TackerObjectRegistry.register +class FmSubscriptionV1(base.TackerPersistentObject, + base.TackerObjectDictCompat): + + # Version 1.0: Initial version + VERSION = '1.0' + + fields = { + 'id': fields.StringField(nullable=False), + 'filter': fields.ObjectField( + 'FmNotificationsFilterV1', nullable=True), + 'callbackUri': fields.UriField(nullable=False), + # NOTE: 'authentication' attribute is not included in the + # original 'FmSubscription' data type definition. + # It is necessary to keep this to be used at sending + # notifications. Note that it is dropped at GET subscription. + 'authentication': fields.ObjectField( + 'SubscriptionAuthentication', nullable=True), + '_links': fields.ObjectField( + 'FmSubscriptionV1_Links', nullable=False), + } + + +@base.TackerObjectRegistry.register +class FmSubscriptionV1_Links(base.TackerObject, base.TackerObjectDictCompat): + # Version 1.0: Initial version + VERSION = '1.0' + + fields = { + 'self': fields.ObjectField('Link', nullable=False), + } diff --git a/tacker/sol_refactored/objects/v1/fm_subscription_request.py b/tacker/sol_refactored/objects/v1/fm_subscription_request.py new file mode 100644 index 000000000..991529da6 --- /dev/null +++ b/tacker/sol_refactored/objects/v1/fm_subscription_request.py @@ -0,0 +1,34 @@ +# Copyright (C) 2022 Fujitsu +# All Rights Reserved. +# +# 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. + +from tacker.sol_refactored.objects import base +from tacker.sol_refactored.objects import fields + + +# NFV-SOL 003 +# - v3.3.1 7.5.2.2 (API version: 1.3.0) +@base.TackerObjectRegistry.register +class FmSubscriptionRequestV1(base.TackerObject, base.TackerObjectDictCompat): + + # Version 1.0: Initial version + VERSION = '1.0' + + fields = { + 'filter': fields.ObjectField( + 'FmNotificationsFilterV1', nullable=True), + 'callbackUri': fields.UriField(nullable=False), + 'authentication': fields.ObjectField( + 'SubscriptionAuthentication', nullable=True) + } diff --git a/tacker/sol_refactored/objects/v2/create_pm_job_request.py b/tacker/sol_refactored/objects/v2/create_pm_job_request.py new file mode 100644 index 000000000..8a7622fbd --- /dev/null +++ b/tacker/sol_refactored/objects/v2/create_pm_job_request.py @@ -0,0 +1,36 @@ +# Copyright (C) 2022 Fujitsu +# All Rights Reserved. +# +# 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. + +from tacker.sol_refactored.objects import base +from tacker.sol_refactored.objects import fields + + +# NFV-SOL 003 +# - v3.3.1 6.5.2.6 (API version: 2.1.0) +@base.TackerObjectRegistry.register +class CreatePmJobRequestV2(base.TackerObject, + base.TackerObjectDictCompat): + # Version 1.0: Initial version + VERSION = '1.0' + + fields = { + 'objectType': fields.StringField(nullable=False), + 'objectInstanceIds': fields.ListOfStringsField(nullable=False), + 'subObjectInstanceIds': fields.ListOfStringsField(nullable=True), + 'criteria': fields.ObjectField('VnfPmJobCriteriaV2', nullable=False), + 'callbackUri': fields.UriField(nullable=False), + 'authentication': fields.ObjectField( + 'SubscriptionAuthentication', nullable=True), + } diff --git a/tacker/sol_refactored/objects/v2/performance_information_available_notification.py b/tacker/sol_refactored/objects/v2/performance_information_available_notification.py new file mode 100644 index 000000000..474b9562a --- /dev/null +++ b/tacker/sol_refactored/objects/v2/performance_information_available_notification.py @@ -0,0 +1,62 @@ +# Copyright (C) 2022 Fujitsu +# All Rights Reserved. +# +# 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. + + +from tacker.sol_refactored.objects import base +from tacker.sol_refactored.objects import fields + + +# NFV-SOL 003 +# - v3.3.1 6.5.2.5 (API version: 2.1.0) +@base.TackerObjectRegistry.register +class PerformanceInformationAvailableNotificationV2( + base.TackerObject, + base.TackerObjectDictCompat +): + + # Version 1.0: Initial version + VERSION = '1.0' + + fields = { + 'id': fields.StringField(nullable=False), + 'notificationType': fields.StringField(nullable=False), + 'timeStamp': fields.DateTimeField(nullable=False), + 'pmJobId': fields.StringField(nullable=False), + 'objectType': fields.StringField(nullable=False), + 'objectInstanceId': fields.StringField(nullable=False), + 'subObjectInstanceIds': fields.ListOfStringsField(nullable=True), + '_links': fields.ObjectField( + 'PerformanceInformationAvailableNotificationV2_Links', + nullable=False), + } + + +@base.TackerObjectRegistry.register +class PerformanceInformationAvailableNotificationV2_Links( + base.TackerObject, + base.TackerObjectDictCompat +): + + # Version 1.0: Initial version + VERSION = '1.0' + + fields = { + 'objectInstance': fields.ObjectField( + 'NotificationLink', nullable=True), + 'pmJob': fields.ObjectField( + 'NotificationLink', nullable=False), + 'performanceReport': fields.ObjectField( + 'NotificationLink', nullable=False), + } diff --git a/tacker/sol_refactored/objects/v2/pm_job.py b/tacker/sol_refactored/objects/v2/pm_job.py new file mode 100644 index 000000000..647e66a68 --- /dev/null +++ b/tacker/sol_refactored/objects/v2/pm_job.py @@ -0,0 +1,75 @@ +# Copyright (C) 2022 Fujitsu +# All Rights Reserved. +# +# 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. + +from tacker.sol_refactored.objects import base +from tacker.sol_refactored.objects import fields + + +# NFV-SOL 003 +# - v3.3.1 6.5.2.7 (API version: 2.1.0) +@base.TackerObjectRegistry.register +class PmJobV2(base.TackerPersistentObject, base.TackerObjectDictCompat): + + # Version 1.0: Initial version + VERSION = '1.0' + + fields = { + 'id': fields.StringField(nullable=False), + 'objectType': fields.StringField(nullable=False), + 'objectInstanceIds': fields.ListOfStringsField(nullable=False), + 'subObjectInstanceIds': fields.ListOfStringsField(nullable=True), + 'criteria': fields.ObjectField('VnfPmJobCriteriaV2', nullable=False), + 'callbackUri': fields.UriField(nullable=False), + 'reports': fields.ListOfObjectsField( + 'VnfPmJobV2_Reports', nullable=False), + '_links': fields.ObjectField( + 'VnfPmJobV2_Links', nullable=False), + # NOTE: 'authentication' attribute is not included in the + # original 'PmJob' data type definition. + # It is necessary to keep this to be used at sending + # notifications. Note that it is dropped at GET subscription. + 'authentication': fields.ObjectField( + 'SubscriptionAuthentication', nullable=True), + # NOTE: 'metadata' attribute is not included in the + # original 'PmJob' data type definition. + # It is necessary to keep this to be used at setting prometheus config. + 'metadata': fields.KeyValuePairsField(nullable=True), + } + + +@base.TackerObjectRegistry.register +class VnfPmJobV2_Reports(base.TackerObject, base.TackerObjectDictCompat): + + # Version 1.0: Initial version + VERSION = '1.0' + + fields = { + 'href': fields.UriField(nullable=False), + 'readyTime': fields.DateTimeField(nullable=False), + 'expiryTime': fields.DateTimeField(nullable=True), + 'fileSize': fields.IntegerField(nullable=True), + } + + +@base.TackerObjectRegistry.register +class VnfPmJobV2_Links(base.TackerObject, base.TackerObjectDictCompat): + + # Version 1.0: Initial version + VERSION = '1.0' + + fields = { + 'self': fields.ObjectField('Link', nullable=False), + 'objects': fields.ListOfObjectsField('Link', nullable=True), + } diff --git a/tacker/sol_refactored/objects/v2/pm_job_criteria.py b/tacker/sol_refactored/objects/v2/pm_job_criteria.py new file mode 100644 index 000000000..ec7f03f70 --- /dev/null +++ b/tacker/sol_refactored/objects/v2/pm_job_criteria.py @@ -0,0 +1,34 @@ +# Copyright (C) 2022 Fujitsu +# All Rights Reserved. +# +# 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. + +from tacker.sol_refactored.objects import base +from tacker.sol_refactored.objects import fields + + +# NFV-SOL 003 +# - v3.3.1 6.5.3.3 (API version: 2.0.0) +@base.TackerObjectRegistry.register +class VnfPmJobCriteriaV2(base.TackerObject, base.TackerObjectDictCompat): + + # Version 1.0: Initial version + VERSION = '1.0' + + fields = { + 'performanceMetric': fields.ListOfStringsField(nullable=True), + 'performanceMetricGroup': fields.ListOfStringsField(nullable=True), + 'collectionPeriod': fields.IntegerField(nullable=False), + 'reportingPeriod': fields.IntegerField(nullable=False), + 'reportingBoundary': fields.DateTimeField(nullable=True), + } diff --git a/tacker/sol_refactored/objects/v2/pm_job_modification.py b/tacker/sol_refactored/objects/v2/pm_job_modification.py new file mode 100644 index 000000000..16e4ac20a --- /dev/null +++ b/tacker/sol_refactored/objects/v2/pm_job_modification.py @@ -0,0 +1,32 @@ +# Copyright (C) 2022 Fujitsu +# All Rights Reserved. +# +# 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. + +from tacker.sol_refactored.objects import base +from tacker.sol_refactored.objects import fields + + +# NFV-SOL 003 +# - v3.3.1 6.5.2.12 (API version: 2.1.0) +@base.TackerObjectRegistry.register +class PmJobModificationsV2(base.TackerObject, base.TackerObjectDictCompat): + + # Version 1.0: Initial version + VERSION = '1.0' + + fields = { + 'callbackUri': fields.StringField(nullable=False), + 'authentication': fields.ObjectField( + 'SubscriptionAuthentication', nullable=False), + } diff --git a/tacker/sol_refactored/objects/v2/pm_report.py b/tacker/sol_refactored/objects/v2/pm_report.py new file mode 100644 index 000000000..0a0c37ffb --- /dev/null +++ b/tacker/sol_refactored/objects/v2/pm_report.py @@ -0,0 +1,65 @@ +# Copyright (C) 2022 Fujitsu +# All Rights Reserved. +# +# 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. + +from tacker.sol_refactored.objects import base +from tacker.sol_refactored.objects import fields + + +# NFV-SOL 003 +# - v3.3.1 6.5.2.10 (API version: 2.1.0) +@base.TackerObjectRegistry.register +class PerformanceReportV2(base.TackerPersistentObject, + base.TackerObjectDictCompat): + + # Version 1.0: Initial version + VERSION = '1.0' + + # PerformanceReportV2 need 'id' and 'jobId' + fields = { + 'id': fields.StringField(nullable=False), + 'jobId': fields.StringField(nullable=False), + 'entries': fields.ListOfObjectsField( + 'VnfPmReportV2_Entries', nullable=False), + } + + +@base.TackerObjectRegistry.register +class VnfPmReportV2_Entries(base.TackerObject, base.TackerObjectDictCompat): + + # Version 1.0: Initial version + VERSION = '1.0' + + fields = { + 'objectType': fields.StringField(nullable=False), + 'objectInstanceId': fields.StringField(nullable=False), + 'subObjectInstanceId': fields.StringField(nullable=True), + 'performanceMetric': fields.StringField(nullable=False), + 'performanceValues': fields.ListOfObjectsField( + 'VnfPmReportV2_Entries_PerformanceValues', nullable=False), + } + + +@base.TackerObjectRegistry.register +class VnfPmReportV2_Entries_PerformanceValues(base.TackerObject, + base.TackerObjectDictCompat): + + # Version 1.0: Initial version + VERSION = '1.0' + + fields = { + 'timeStamp': fields.DateTimeField(nullable=False), + 'value': fields.StringField(nullable=False), + 'context': fields.KeyValuePairsField(nullable=True), + } diff --git a/tacker/tests/functional/common/fake_server.py b/tacker/tests/functional/common/fake_server.py index ee9374e15..32f18a5c2 100644 --- a/tacker/tests/functional/common/fake_server.py +++ b/tacker/tests/functional/common/fake_server.py @@ -189,7 +189,28 @@ def PrepareRequestHandler(manager): inspect.currentframe().f_code.co_name)) def do_PUT(self): - raise NotImplementedError + """Process PUT request""" + LOG.debug( + '[Start] %s.%s()' % + (self.__class__.__name__, + inspect.currentframe().f_code.co_name)) + + # URI might have trailing uuid or not. + if self._is_match_with_list(): + # Request is registered in our list. + tplUri = urlparse(self.path) + self._returned_callback(tplUri.path, + manager._funcs_puts[tplUri.path]) + else: + # Unregistered URI is requested + LOG.debug('PUT Recv. Unknown URL: "%s"' % self.path) + self.send_response(http.HTTPStatus.BAD_REQUEST) + self.end_headers() + + LOG.debug( + '[ End ] %s.%s()' % + (self.__class__.__name__, + inspect.currentframe().f_code.co_name)) return DummyRequestHandler diff --git a/tacker/tests/functional/sol_kubernetes_oidc_auth/vnflcm_v2/base_v2.py b/tacker/tests/functional/sol_kubernetes_oidc_auth/vnflcm_v2/base_v2.py index a974f1b60..6bb6ad8a8 100644 --- a/tacker/tests/functional/sol_kubernetes_oidc_auth/vnflcm_v2/base_v2.py +++ b/tacker/tests/functional/sol_kubernetes_oidc_auth/vnflcm_v2/base_v2.py @@ -53,6 +53,10 @@ class BaseVnfLcmKubernetesV2OidcTest(base_v2.BaseVnfLcmKubernetesV2Test): ) cls.tacker_client = http_client.HttpClient(auth) + @classmethod + def tearDownClass(cls): + super(base_v2.BaseVnfLcmKubernetesV2Test, cls).tearDownClass() + @classmethod def get_k8s_vim_info(cls): vim_params = yaml.safe_load( diff --git a/tacker/tests/functional/sol_kubernetes_v2/base_v2.py b/tacker/tests/functional/sol_kubernetes_v2/base_v2.py index 659ce2bae..ad45a66f5 100644 --- a/tacker/tests/functional/sol_kubernetes_v2/base_v2.py +++ b/tacker/tests/functional/sol_kubernetes_v2/base_v2.py @@ -16,6 +16,7 @@ import os import shutil import tempfile import time +import urllib from oslo_config import cfg from oslo_utils import uuidutils @@ -24,10 +25,14 @@ import yaml from tacker.sol_refactored.common import http_client from tacker.sol_refactored import objects +from tacker.tests.functional.common.fake_server import FakeServerManager from tacker.tests.functional.sol_v2_common import utils from tacker.tests import utils as base_utils from tacker import version +FAKE_SERVER_MANAGER = FakeServerManager() +MOCK_NOTIFY_CALLBACK_URL = '/notification/callback' + VNF_PACKAGE_UPLOAD_TIMEOUT = 300 VNF_INSTANTIATE_TIMEOUT = 600 VNF_TERMINATE_TIMEOUT = 600 @@ -41,6 +46,9 @@ class BaseVnfLcmKubernetesV2Test(base.BaseTestCase): super(BaseVnfLcmKubernetesV2Test, cls).setUpClass() """Base test case class for SOL v2 kubernetes functional tests.""" + FAKE_SERVER_MANAGER.prepare_http_server() + FAKE_SERVER_MANAGER.start_server() + cfg.CONF(args=['--config-file', '/etc/tacker/tacker.conf'], project='tacker', version='%%prog %s' % version.version_info.release_string()) @@ -61,6 +69,22 @@ class BaseVnfLcmKubernetesV2Test(base.BaseTestCase): project_domain_name=vim_info.accessInfo['projectDomain'] ) cls.tacker_client = http_client.HttpClient(auth) + cls.fake_prometheus_ip = cls.get_controller_tacker_ip() + + @classmethod + def tearDownClass(cls): + super(BaseVnfLcmKubernetesV2Test, cls).tearDownClass() + FAKE_SERVER_MANAGER.stop_server() + + def setUp(self): + super().setUp() + + callback_url = os.path.join( + MOCK_NOTIFY_CALLBACK_URL, + self._testMethodName) + FAKE_SERVER_MANAGER.clear_history(callback_url) + FAKE_SERVER_MANAGER.set_callback('POST', callback_url, status_code=204) + FAKE_SERVER_MANAGER.set_callback('GET', callback_url, status_code=204) @classmethod def get_vim_info(cls): @@ -151,6 +175,35 @@ class BaseVnfLcmKubernetesV2Test(base.BaseTestCase): return pkg_id, vnfd_id + @classmethod + def get_controller_tacker_ip(cls): + cur_dir = os.path.dirname(__file__) + script_path = os.path.join( + cur_dir, "../../../../tools/test-setup-fake-prometheus-server.sh") + with open(script_path, 'r') as f_obj: + content = f_obj.read() + ip = content.split('TEST_REMOTE_URI')[1].split( + 'http://')[1].split('"')[0] + return ip + + def assert_notification_get(self, callback_url): + notify_mock_responses = FAKE_SERVER_MANAGER.get_history( + callback_url) + FAKE_SERVER_MANAGER.clear_history( + callback_url) + self.assertEqual(1, len(notify_mock_responses)) + self.assertEqual(204, notify_mock_responses[0].status_code) + + def _check_notification(self, callback_url, notify_type): + notify_mock_responses = FAKE_SERVER_MANAGER.get_history( + callback_url) + FAKE_SERVER_MANAGER.clear_history( + callback_url) + self.assertEqual(1, len(notify_mock_responses)) + self.assertEqual(204, notify_mock_responses[0].status_code) + self.assertEqual(notify_type, notify_mock_responses[0].request_body[ + 'notificationType']) + @classmethod def delete_vnf_package(cls, pkg_id): path = f"/vnfpkgm/v1/vnf_packages/{pkg_id}" @@ -233,6 +286,92 @@ class BaseVnfLcmKubernetesV2Test(base.BaseTestCase): return self.tacker_client.do_request( path, "GET", version="2.0.0") + def create_subscription(self, req_body): + path = "/vnffm/v1/subscriptions" + return self.tacker_client.do_request( + path, "POST", body=req_body, version="1.3.0") + + def list_subscriptions(self, filter_expr=None): + path = "/vnffm/v1/subscriptions" + if filter_expr: + path = "{}?{}".format(path, urllib.parse.urlencode(filter_expr)) + return self.tacker_client.do_request( + path, "GET", version="1.3.0") + + def show_subscription(self, subscription_id): + path = f"/vnffm/v1/subscriptions/{subscription_id}" + return self.tacker_client.do_request( + path, "GET", version="1.3.0") + + def delete_subscription(self, subscription_id): + path = f"/vnffm/v1/subscriptions/{subscription_id}" + return self.tacker_client.do_request( + path, "DELETE", version="1.3.0") + + def create_fm_alarm(self, req_body): + path = "/alert" + return self.tacker_client.do_request( + path, "POST", body=req_body, version="1.3.0") + + def list_fm_alarm(self, filter_expr=None): + path = "/vnffm/v1/alarms" + if filter_expr: + path = "{}?{}".format(path, urllib.parse.urlencode(filter_expr)) + return self.tacker_client.do_request( + path, "GET", version="1.3.0") + + def show_fm_alarm(self, alarm_id): + path = f"/vnffm/v1/alarms/{alarm_id}" + return self.tacker_client.do_request( + path, "GET", version="1.3.0") + + def update_fm_alarm(self, alarm_id, req_body): + path = f"/vnffm/v1/alarms/{alarm_id}" + return self.tacker_client.do_request( + path, "PATCH", body=req_body, version="1.3.0") + + def create_pm_job(self, req_body): + path = "/vnfpm/v2/pm_jobs" + return self.tacker_client.do_request( + path, "POST", body=req_body, version="2.1.0") + + def update_pm_job(self, pm_job_id, req_body): + path = f"/vnfpm/v2/pm_jobs/{pm_job_id}" + return self.tacker_client.do_request( + path, "PATCH", body=req_body, version="2.1.0") + + def create_pm_event(self, req_body): + path = "/pm_event" + return self.tacker_client.do_request( + path, "POST", body=req_body, version="2.1.0") + + def list_pm_job(self, filter_expr=None): + path = "/vnfpm/v2/pm_jobs" + if filter_expr: + path = "{}?{}".format(path, urllib.parse.urlencode(filter_expr)) + return self.tacker_client.do_request( + path, "GET", version="2.1.0") + + def show_pm_job(self, pm_job_id): + path = f"/vnfpm/v2/pm_jobs/{pm_job_id}" + return self.tacker_client.do_request( + path, "GET", version="2.1.0") + + def show_pm_job_report(self, pm_job_id, report_id): + path = f"/vnfpm/v2/pm_jobs/{pm_job_id}/reports/{report_id}" + return self.tacker_client.do_request( + path, "GET", version="2.1.0") + + def delete_pm_job(self, pm_job_id): + path = f"/vnfpm/v2/pm_jobs/{pm_job_id}" + return self.tacker_client.do_request( + path, "DELETE", version="2.1.0") + + def prometheus_auto_scaling_alert(self, req_body): + path = "/alert/vnf_instances" + return self.tacker_client.do_request( + path, "POST", body=req_body) + def _check_resp_headers(self, resp, supported_headers): unsupported_headers = ['Link', 'Retry-After', 'Content-Range', 'WWW-Authenticate'] diff --git a/tacker/tests/functional/sol_kubernetes_v2/paramgen.py b/tacker/tests/functional/sol_kubernetes_v2/paramgen.py index ef0928181..7b3e227d6 100644 --- a/tacker/tests/functional/sol_kubernetes_v2/paramgen.py +++ b/tacker/tests/functional/sol_kubernetes_v2/paramgen.py @@ -407,3 +407,328 @@ def helm_error_handling_change_vnfpkg(vnfd_id): }] } } + + +def instantiate_cnf_resources_create(vnfd_id): + return { + "vnfdId": vnfd_id, + "vnfInstanceName": "test", + "vnfInstanceDescription": "test", + "metadata": {"dummy-key": "dummy-val"} + } + + +def pm_instantiate_cnf_resources_create(vnfd_id): + return { + "vnfdId": vnfd_id, + "vnfInstanceName": "test", + "vnfInstanceDescription": "test" + } + + +def instantiate_vnf_min(): + # Omit except for required attributes + # NOTE: Only the following cardinality attributes are set. + # - 1 + # - 1..N (1) + return { + "flavourId": "simple" + } + + +def sub_create_min(callback_uri): + # Omit except for required attributes + # NOTE: Only the following cardinality attributes are set. + # - 1 + # - 1..N (1) + return { + "callbackUri": callback_uri + } + + +def sub_create_max(callback_uri, vnfd_id, inst_id): + return { + "filter": { + "vnfInstanceSubscriptionFilter": { + "vnfdIds": [vnfd_id], + "vnfProductsFromProviders": [ + { + "vnfProvider": "Company", + "vnfProducts": [ + { + "vnfProductName": "Sample VNF", + "versions": [ + { + "vnfSoftwareVersion": "1.0", + "vnfdVersions": ["1.0"] + } + ] + } + ] + }, + ], + "vnfInstanceIds": [inst_id], + "vnfInstanceNames": ["test"], + }, + "notificationTypes": ["AlarmNotification", + "AlarmClearedNotification"], + "faultyResourceTypes": ["COMPUTE"], + "perceivedSeverities": ["WARNING"], + "eventTypes": ["PROCESSING_ERROR_ALARM"], + "probableCauses": ["Process Terminated"] + }, + "callbackUri": callback_uri + } + + +def alert_event_firing(inst_id, pod_name): + return { + "receiver": "receiver", + "status": "firing", + "alerts": [ + { + "status": "firing", + "labels": { + "receiver_type": "tacker", + "function_type": "vnffm", + "vnf_instance_id": inst_id, + "pod": pod_name, + "perceived_severity": "WARNING", + "event_type": "PROCESSING_ERROR_ALARM" + }, + "annotations": { + "fault_type": "Server Down", + "probable_cause": "Process Terminated", + "fault_details": "pid 12345" + }, + "startsAt": "2022-06-21T23:47:36.453Z", + "endsAt": "0001-01-01T00:00:00Z", + "generatorURL": "http://controller147:9090/graph?g0.expr=" + "up%7Bjob%3D%22node%22%7D+%3D%3D+0&g0.tab=1", + "fingerprint": "5ef77f1f8a3ecb8d" + } + ], + "groupLabels": {}, + "commonLabels": { + "alertname": "NodeInstanceDown", + "job": "node" + }, + "commonAnnotations": { + "description": "sample" + }, + "externalURL": "http://controller147:9093", + "version": "4", + "groupKey": "{}:{}", + "truncatedAlerts": 0 + } + + +def alert_event_resolved(inst_id, pod_name): + return { + "receiver": "receiver", + "status": "resolved", + "alerts": [ + { + "status": "resolved", + "labels": { + "receiver_type": "tacker", + "function_type": "vnffm", + "vnf_instance_id": inst_id, + "pod": pod_name, + "perceived_severity": "WARNING", + "event_type": "PROCESSING_ERROR_ALARM" + }, + "annotations": { + "fault_type": "Server Down", + "probable_cause": "Process Terminated", + "fault_details": "pid 12345" + }, + "startsAt": "2022-06-21T23:47:36.453Z", + "endsAt": "2022-06-22T23:47:36.453Z", + "generatorURL": "http://controller147:9090/graph?g0.expr=up%7B" + "job%3D%22node%22%7D+%3D%3D+0&g0.tab=1", + "fingerprint": "5ef77f1f8a3ecb8d" + } + ], + "groupLabels": {}, + "commonLabels": { + "alertname": "NodeInstanceDown", + "job": "node" + }, + "commonAnnotations": { + "description": "sample" + }, + "externalURL": "http://controller147:9093", + "version": "4", + "groupKey": "{}:{}", + "truncatedAlerts": 0 + } + + +def update_alarm(): + return { + "ackState": "ACKNOWLEDGED" + } + + +def terminate_vnf_min(): + # Omit except for required attributes + # NOTE: Only the following cardinality attributes are set. + # - 1 + # - 1..N (1) + return { + "terminationType": "FORCEFUL" + } + + +def pm_job_min(callback_uri, inst_id, host_ip): + return { + "objectType": "Vnf", + "objectInstanceIds": [inst_id], + "criteria": { + "performanceMetric": [ + f"VCpuUsageMeanVnf.{inst_id}"], + "collectionPeriod": 5, + "reportingPeriod": 10 + }, + "callbackUri": callback_uri, + "metadata": { + "monitoring": { + "monitorName": "prometheus", + "driverType": "external", + "targetsInfo": [ + { + "prometheusHost": host_ip, + "prometheusHostPort": 50022, + "authInfo": { + "ssh_username": "root", + "ssh_password": "root" + }, + "alertRuleConfigPath": + "/tmp", + "prometheusReloadApiEndpoint": + "http://localhost:9990/-/reload", + } + ] + } + } + + } + + +def pm_job_max(callback_uri, inst_id, host_ip): + return { + "objectType": "Vnf", + "objectInstanceIds": [inst_id], + "subObjectInstanceIds": [], + "criteria": { + "performanceMetric": [ + f"VCpuUsageMeanVnf.{inst_id}"], + "performanceMetricGroup": ["VirtualisedComputeResource"], + "collectionPeriod": 5, + "reportingPeriod": 10, + "reportingBoundary": "2099-08-05T02:24:46Z" + }, + "callbackUri": callback_uri, + "metadata": { + "monitoring": { + "monitorName": "prometheus", + "driverType": "external", + "targetsInfo": [ + { + "prometheusHost": host_ip, + "prometheusHostPort": 50022, + "authInfo": { + "ssh_username": "root", + "ssh_password": "root" + }, + "alertRuleConfigPath": + "/tmp", + "prometheusReloadApiEndpoint": + "http://localhost:9990/-/reload" + } + ] + } + } + } + + +def update_pm_job(callback_uri): + return { + "callbackUri": callback_uri + } + + +def pm_event(job_id, inst_id): + return { + "receiver": "receiver", + "status": "firing", + "alerts": [ + { + "status": "firing", + "labels": { + "receiver_type": "tacker", + "function_type": "vnfpm", + "job_id": job_id, + "metric": f"VCpuUsageMeanVnf.{inst_id}", + "object_instance_id": inst_id + }, + "annotations": { + "value": 99, + }, + "startsAt": "2022-06-21T23:47:36.453Z", + "endsAt": "0001-01-01T00:00:00Z", + "generatorURL": "http://controller147:9090/graph?g0.expr=up%7B" + "job%3D%22node%22%7D+%3D%3D+0&g0.tab=1", + "fingerprint": "5ef77f1f8a3ecb8d" + } + ], + "groupLabels": {}, + "commonLabels": { + "alertname": "NodeInstanceDown", + "job": "node" + }, + "commonAnnotations": { + "description": "sample" + }, + "externalURL": "http://controller147:9093", + "version": "4", + "groupKey": "{}:{}", + "truncatedAlerts": 0 + } + + +def prometheus_auto_scaling_alert(inst_id): + return { + "receiver": "receiver", + "status": "firing", + "alerts": [{ + "status": "firing", + "labels": { + "receiver_type": "tacker", + "function_type": "auto_scale", + "vnf_instance_id": inst_id, + "auto_scale_type": "SCALE_OUT", + "aspect_id": "vdu2_aspect" + }, + "annotations": { + }, + "startsAt": "2022-06-21T23:47:36.453Z", + "endsAt": "0001-01-01T00:00:00Z", + "generatorURL": "http://controller147:9090/graph?g0.expr=" + "up%7Bjob%3D%22node%22%7D+%3D%3D+0&g0.tab=1", + "fingerprint": "5ef77f1f8a3ecb8d" + }], + "groupLabels": {}, + "commonLabels": { + "alertname": "NodeInstanceDown", + "job": "node" + }, + "commonAnnotations": { + "description": "sample" + }, + "externalURL": "http://controller147:9093", + "version": "4", + "groupKey": "{}:{}", + "truncatedAlerts": 0 + } diff --git a/tacker/tests/functional/sol_kubernetes_v2/samples/tacker-monitoring-test.zip b/tacker/tests/functional/sol_kubernetes_v2/samples/tacker-monitoring-test.zip new file mode 100644 index 0000000000000000000000000000000000000000..94b4370c0e31badddc6615abbf95727c29db4a56 GIT binary patch literal 4873 zcmai&bzD<@`^QI1hjfFCP6Z_;1aBoLGD@U}OkjgCMoEZDN)M3+>CO=n0us{h29<7v zTRH?qgdgzS{KQA`IeUH2we!dKdY{j?Uf1V+DXd@S|KdP84g7n*cP;MW{?dYxou1(UJHnprmFPDbT^8GZP909NI*nnZbZ1XG$mzq zd~9f+9@&?c_(_j;+6KL@VKdQm9n`}ag}1vkuj0ICLjQlQxa7-hI+VGWi5M$_hruP<;vN@5-9c*ggtk?YS zO=ydVRz|sJ$L8(NNmSpaRLzfpqBY(Sw&G>2fB;h#D zSG<+&es@LIWR#6zpRJ3mP00uQ%Tx3Xdx*)JDICFqOp{bB3B84I=?m8s;S z2lQikttnK)e6_i6Uh@76%d;P&zx6$;Wm?bIj%FdQ_sJ`&Y)HnywKC2$Q?j5(`3w4# zc(bxfpW+xdaZ7v!Ud_Z0DyZ-xlO!O)FU`e(d6H%{Z7&$tORX+}HV{m}+)fGo2&Ms< zmF*(svI)9avTN`1(m13*NwG+_F>?WnZEmU_XI?=sYZBdg@vv!ha#v<~@ z9|Z^6_sdxJ77__D@I{9FJ&>=RzCwQu{P88%gDI!}-F0`O;;3&PtxraLe(Z=~FIw=% zSJ@!N!=0dX*m%y~5fB&>;qC~B!jPiQwsf5mB#oS81aB3xZ~QY(rVy#lsSE0$q=Sn! zd>xCO9p>R+|xsq4C;Wqjf8u#7})c0S3*lYY~2SyO+V*w2`l>i0j!n?ecPEeAME? zmwt1GB95>I18qk&@+w*8=ZYzC^5a zPdJNGYq_ujiS#!U2`&fQ+PuH3-NjTl>FMUGhJ>eDpC#TfielcguJ`_~uWLlSDMIx$ z<>GkiXtp2MusW%i%BHl{17DbvUc}JS=lpl^qg)@>9tHWUwBK-bqP28 zwNZ;Mrwe_OtC`RF&KrAC8LaZdjRJ$*)D5>5DW$$VX8?aqWG^Lac-X?sN;l#V$x`jd zd{t)bBJFxv;7~uS>PKo%!v`J8h9O@{rB%?ZAXWm5`$Sh04C1V#)UDdmPxFeiK6T{c zqLaB9a7LzQyfFC!39NT|x;5twQ0(H{)eTHS$Ve>A%)y>$Coq0YD%E}F+5C`Gw~ ztDabMsfyWKaQRvB8hL+`U7Qg%u-1BEQ+GvtVZ3F4wRRd1|p8@eZp zsH*Y_`!|v}uZ4{q@RRmVgW8pz+q*bohVN9Q^S(inM*6h#+%LHv==n9On7R=LFH374 zFt)aOq-nIa%pgVXJHu)2#62YuRl+H6#x0wXK#3UFQ}RVmsDsT!%@DlUKAV7RI-Is? zgiENdN8AB*XomL~N8@Kk%b6jA1*|?Q)BKFa41wj}3_(@&A3$VRh7FP=0`naNOhz@u z%-!+911^GbCe?Fp-d5pPI_EV6b$0zMjL;EeCEMrdQj-|38yD{aD6nHKv|PRf%pZrE zhduHHoJUEQwJ=by`MSc!-sy(2Toa)LxXJYEV5wb4FJ9y=vw}E8A^N z1h&Qr7JIAK9{gOOR)EoYoYH8$8o`h}H^Y=GQlV2e?5xax*I6p0&!*~WQW(@{M`9L& z*qbt>aWUY*TWkd5u!*ZI%Gye-GlGc#qVwnGCjbYvAGf2{3+5}51zAQ67%8YIv)y%J zY|ClMmoBq`sYmoHGa0QRN*esKrKG}j4eE_>M*2Y-O7LYF6QlfGq`_6E)D>3jh38(} z#>8K)`f3ix8g73N?$QFv4xn#B$b>XG|li)z%8;&4wk5m-)59 zEUibLz31bpeT#b8seIX0J`7wBDaUq?1j}dQa#t~P5{2$-$WA^|4#Z}27eUK!rxQ`I z-F+okbo;%R8o?D&5=YTzBN0isz_9zU7fOt)9);FbsUe__W<8FyCcR?Rn~K|dW!_^Z zyOYYqP@2zGiIx=PpEh^`nS|C`r3@&?O{cZIno>Jy^FHV<#kKE*k7a$-Vh<&pU)oH4 z4M~5U!t(ZDHg!CKI#NvzYUl62QcSk1D7SWi4dQ^2wQ20ud3L;bTj#zCv)G_X3%C+F zR9mbaUz;ptirf4ys$tf-y_hI_#rFXsY{Lr2r`K!O4?e~clZWqZ`j%!3R5%hPPhml+ zUd?MIHDkkMSrC`m`JP(fGHJ-*_M^4|>c!rKiWVoxO0>J8$b0~m;=Qk>@;DnGFShAd z`UF4>1NYgznktzdOgWDATUjR3F-*(A>$azD-Fj(_9fhgFW(S1}hiHn})CQa#h!3e#?=K%E&M&8ARX1v<+vOj#87DJduc&e1Zb?3!#;i zM%5Xzc#0QQ=t9}J-)X_8B}XkoOU6x#!Z{2Q>vIgG@=(=-;XZd~@HMQ3yS*}=;U2Il=S0_No+#;&QLKFCz7!8P@DNHd7V5_6JU=f@tAa285c~fq= zip}=P$h)RZE;Y*>Ro9|Pem=oo6~Pyh_35H0dH7?y1KY7x&*!eL+pLYZE^}(xO0_JH z=i{>NEx%a0Y1~N2-TIfEY>r*1Y@3-nxxBm*lqC~qOIuUkP-L|*meMO>E|qt`+g4n3 zVZT0LE#yst>snVJ{KM3qTOGw}o?Z{>)AZ{uZ>E$rDdVqauoyniB_x!!wB{#%shG>* zWRiWoHWizwP3yR;_BAI-3w+Sbp<|NZs_{3Ej@EmGuls13o3PJ*xvYm3y!6`#7i3wO z5ncDR=iHhFQb-kGug zzUfhDs?bu~o2m5uQNH3Hc6`&*%KI99jv^Q9GE;%?2qj1Z2Uihrmg_t!n2#>1|AezV zD9LeQ3jp8%7y-+hb|Fp;gE$6yQH-w|2XUAs{`33;^gE||0^&RbNgcK`%TK}J>zB!xv z|8C$k(U9kohr=O>ku~I)h?8}i`1_KW@7VMlO&{lB%f>%b=YJRIr@_9p+H^*TQcr*< z@yDG0u8cpCdbD_!{s2BM>QCdg@Nqy#I`OMWk9B_O{@*%*Kk4K<{QPmo5<(o~PRjAq z*b|i}EBGXyR{5)4IIZ&j+1MlVa2f_kh}Jhg9MThwKf``MmluB0nEq$jpA=3q+aK@I zllVj7?`+A5!lMN=`)BA+Cgmr&ky+T4L%AlrJRC=xe7p8gLx CrK&>! literal 0 HcmV?d00001 diff --git a/tacker/tests/functional/sol_kubernetes_v2/test_prometheus_auto_scaling.py b/tacker/tests/functional/sol_kubernetes_v2/test_prometheus_auto_scaling.py new file mode 100644 index 000000000..784fbf86a --- /dev/null +++ b/tacker/tests/functional/sol_kubernetes_v2/test_prometheus_auto_scaling.py @@ -0,0 +1,115 @@ +# Copyright (C) 2022 Fujitsu +# All Rights Reserved. +# +# 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. +import ddt +import os +import time + +from tacker.objects import fields +from tacker.tests.functional.sol_kubernetes_v2 import base_v2 +from tacker.tests.functional.sol_kubernetes_v2 import paramgen + + +@ddt.ddt +class PrometheusAutoScalingTest(base_v2.BaseVnfLcmKubernetesV2Test): + + @classmethod + def setUpClass(cls): + super(PrometheusAutoScalingTest, cls).setUpClass() + + cur_dir = os.path.dirname(__file__) + + test_instantiate_cnf_resources_path = os.path.join( + cur_dir, "samples/test_instantiate_cnf_resources") + cls.vnf_pkg_1, cls.vnfd_id_1 = cls.create_vnf_package( + test_instantiate_cnf_resources_path) + + @classmethod + def tearDownClass(cls): + super(PrometheusAutoScalingTest, cls).tearDownClass() + + cls.delete_vnf_package(cls.vnf_pkg_1) + + def setUp(self): + super(PrometheusAutoScalingTest, self).setUp() + + def test_prometheus_auto_scaling_basic(self): + """Test Prometheus Auto Scaling operations with all attributes set + + * About LCM operations: + This test includes the following operations. + - 1. Create a new VNF instance resource + - 2. Instantiate a VNF instance + - 3. Prometheus Auto Scaling alert. + - 4. Terminate a VNF instance + - 5. Delete a VNF instance + """ + + # 1. LCM-Create: Create a new VNF instance resource + # NOTE: extensions and vnfConfigurableProperties are omitted + # because they are commented out in etsi_nfv_sol001. + create_req = paramgen.instantiate_cnf_resources_create(self.vnfd_id_1) + resp, body = self.create_vnf_instance(create_req) + self.assertEqual(201, resp.status_code) + inst_id = body['id'] + + # 2. LCM-Instantiate: Instantiate a VNF instance + vim_id = self.get_k8s_vim_id() + instantiate_req = paramgen.min_sample_instantiate(vim_id) + instantiate_req['additionalParams'][ + 'lcm-kubernetes-def-files'] = ['Files/kubernetes/deployment.yaml'] + instantiate_req['vnfConfigurableProperties'] = { + 'isAutoscaleEnabled': True} + resp, body = self.instantiate_vnf_instance(inst_id, instantiate_req) + self.assertEqual(202, resp.status_code) + + lcmocc_id = os.path.basename(resp.headers['Location']) + self.wait_lcmocc_complete(lcmocc_id) + + resp, body = self.show_vnf_instance(inst_id) + self.assertEqual(200, resp.status_code) + + # 3. Send Auto-Healing alert + alert = paramgen.prometheus_auto_scaling_alert(inst_id) + # CNF scale is not integrated yet. use this value for now. + alert['alerts'][0]['labels']['aspect_id'] = 'invalid_id' + resp, body = self.prometheus_auto_scaling_alert(alert) + self.assertEqual(204, resp.status_code) + time.sleep(5) + + # 4. LCM-Terminate: Terminate VNF + terminate_req = paramgen.terminate_vnf_min() + resp, body = self.terminate_vnf_instance(inst_id, terminate_req) + self.assertEqual(202, resp.status_code) + + lcmocc_id = os.path.basename(resp.headers['Location']) + self.wait_lcmocc_complete(lcmocc_id) + + # wait a bit because there is a bit time lag between lcmocc DB + # update and terminate completion. + time.sleep(10) + + # check instantiationState of VNF + resp, body = self.show_vnf_instance(inst_id) + self.assertEqual(200, resp.status_code) + self.assertEqual(fields.VnfInstanceState.NOT_INSTANTIATED, + body['instantiationState']) + + # 5. LCM-Delete: Delete a VNF instance + resp, body = self.delete_vnf_instance(inst_id) + self.assertEqual(204, resp.status_code) + + # check deletion of VNF instance + resp, body = self.show_vnf_instance(inst_id) + self.assertEqual(404, resp.status_code) diff --git a/tacker/tests/functional/sol_kubernetes_v2/test_vnffm_basic.py b/tacker/tests/functional/sol_kubernetes_v2/test_vnffm_basic.py new file mode 100644 index 000000000..810faeaff --- /dev/null +++ b/tacker/tests/functional/sol_kubernetes_v2/test_vnffm_basic.py @@ -0,0 +1,440 @@ +# Copyright (C) 2022 Fujitsu +# All Rights Reserved. +# +# 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. +import ddt +import os +import time + +from tacker.objects import fields +from tacker.tests.functional.sol_kubernetes_v2 import base_v2 +from tacker.tests.functional.sol_kubernetes_v2 import paramgen + + +@ddt.ddt +class VnfFmTest(base_v2.BaseVnfLcmKubernetesV2Test): + + @classmethod + def setUpClass(cls): + super(VnfFmTest, cls).setUpClass() + + cur_dir = os.path.dirname(__file__) + + test_instantiate_cnf_resources_path = os.path.join( + cur_dir, "samples/test_instantiate_cnf_resources") + cls.vnf_pkg_1, cls.vnfd_id_1 = cls.create_vnf_package( + test_instantiate_cnf_resources_path) + + @classmethod + def tearDownClass(cls): + super(VnfFmTest, cls).tearDownClass() + + cls.delete_vnf_package(cls.vnf_pkg_1) + + def setUp(self): + super(VnfFmTest, self).setUp() + + def test_faultmanagement_interface_min(self): + """Test FM operations with all attributes set + + * About attributes: + All of the following cardinality attributes are set. + In addition, 0..N or 1..N attributes are set to 2 or more. + - 0..1 (1) + - 0..N (2 or more) + - 1 + - 1..N (2 or more) + + * About LCM operations: + This test includes the following operations. + - 1. Create a new VNF instance resource + - 2. Instantiate a VNF instance + - 3. Create a new subscription + - 4. List subscription with attribute-based + - 5. Show subscription + - 6. Alert-Event (firing) + - 7. FM-List-Alarm + - 8. FM-Show-Alarm + - 9. FM-Update-Alarm + - 10. FM-Show-Alarm + - 11. Alert-Event (resolved) + - 12. FM-Show-Alarm + - 13. FM-Delete-Subscription: Delete subscription + - 14. Terminate a VNF instance + - 15. Delete a VNF instance + """ + + # 1. LCM-Create: Create a new VNF instance resource + # NOTE: extensions and vnfConfigurableProperties are omitted + # because they are commented out in etsi_nfv_sol001. + create_req = paramgen.instantiate_cnf_resources_create(self.vnfd_id_1) + resp, body = self.create_vnf_instance(create_req) + self.assertEqual(201, resp.status_code) + inst_id = body['id'] + + # 2. LCM-Instantiate: Instantiate a VNF instance + vim_id = self.get_k8s_vim_id() + instantiate_req = paramgen.min_sample_instantiate(vim_id) + instantiate_req['additionalParams'][ + 'lcm-kubernetes-def-files'] = ['Files/kubernetes/deployment.yaml'] + resp, body = self.instantiate_vnf_instance(inst_id, instantiate_req) + self.assertEqual(202, resp.status_code) + + lcmocc_id = os.path.basename(resp.headers['Location']) + self.wait_lcmocc_complete(lcmocc_id) + + resp, body = self.show_vnf_instance(inst_id) + self.assertEqual(200, resp.status_code) + vnfc_resource_infos = body['instantiatedVnfInfo'].get( + 'vnfcResourceInfo') + pod_name = [vnfc_info['computeResource']['resourceId'] + for vnfc_info in vnfc_resource_infos + if vnfc_info['vduId'] == 'VDU2'][0] + + # 3. FM-Create-Subscription: Create a new subscription + expected_inst_attrs = ['id', 'callbackUri', '_links'] + callback_url = os.path.join(base_v2.MOCK_NOTIFY_CALLBACK_URL, + self._testMethodName) + callback_uri = ('http://localhost:' + f'{base_v2.FAKE_SERVER_MANAGER.SERVER_PORT}' + f'{callback_url}') + + sub_req = paramgen.sub_create_min(callback_uri) + resp, body = self.create_subscription(sub_req) + self.assertEqual(201, resp.status_code) + self.check_resp_headers_in_create(resp) + sub_id = body['id'] + self.check_resp_body(body, expected_inst_attrs) + # Test notification + self.assert_notification_get(callback_url) + self.addCleanup(self.delete_subscription, sub_id) + + # 4. FM-List-Subscription: List subscription with attribute-based + # filtering + expected_attrs = ['id', 'callbackUri', '_links'] + resp, body = self.list_subscriptions() + self.assertEqual(200, resp.status_code) + self.check_resp_headers_in_get(resp) + for sbsc in body: + self.check_resp_body(sbsc, expected_attrs) + + # 5. FM-Show-Subscription: Show subscription + resp, body = self.show_subscription(sub_id) + self.assertEqual(200, resp.status_code) + self.check_resp_headers_in_get(resp) + self.check_resp_body(body, expected_attrs) + + # 6. Alert-Event (firing) + alert = paramgen.alert_event_firing(inst_id, pod_name) + resp, body = self.create_fm_alarm(alert) + self.assertEqual(204, resp.status_code) + time.sleep(5) + self._check_notification(callback_url, 'AlarmNotification') + + # 7. FM-List-Alarm + alarm_expected_attrs = [ + 'id', + 'managedObjectId', + 'alarmRaisedTime', + 'ackState', + 'perceivedSeverity', + 'eventTime', + 'eventType', + 'probableCause', + 'isRootCause', + '_links' + ] + resp, body = self.list_fm_alarm() + self.assertEqual(200, resp.status_code) + self.check_resp_headers_in_get(resp) + for sbsc in body: + self.check_resp_body(sbsc, alarm_expected_attrs) + + # 8. FM-Show-Alarm + filter_expr = {'filter': f'(eq,managedObjectId,{inst_id})'} + resp, body = self.list_fm_alarm(filter_expr) + alarm_id = body[0]['id'] + resp, body = self.show_fm_alarm(alarm_id) + self.assertEqual(200, resp.status_code) + self.check_resp_headers_in_get(resp) + self.check_resp_body(body, alarm_expected_attrs) + + # 9. FM-Update-Alarm + expected_attrs = [ + 'ackState' + ] + update_req = paramgen.update_alarm() + resp, body = self.update_fm_alarm(alarm_id, update_req) + self.assertEqual(200, resp.status_code) + self.check_resp_headers_in_delete(resp) + self.check_resp_body(body, expected_attrs) + + # 10. FM-Show-Alarm + expected_attrs = [ + 'id', + 'managedObjectId', + 'alarmRaisedTime', + 'ackState', + 'perceivedSeverity', + 'eventTime', + 'eventType', + 'probableCause', + 'isRootCause', + '_links' + ] + resp, body = self.show_fm_alarm(alarm_id) + self.assertEqual(200, resp.status_code) + self.check_resp_headers_in_get(resp) + self.check_resp_body(body, expected_attrs) + + # 11. Alert-Event (resolved) + alert = paramgen.alert_event_resolved(inst_id, pod_name) + resp, body = self.create_fm_alarm(alert) + self.assertEqual(204, resp.status_code) + time.sleep(5) + self._check_notification(callback_url, 'AlarmClearedNotification') + + # 12. FM-Show-Alarm + resp, body = self.show_fm_alarm(alarm_id) + self.assertEqual(200, resp.status_code) + self.check_resp_headers_in_get(resp) + self.check_resp_body(body, alarm_expected_attrs) + + # 13. FM-Delete-Subscription: Delete subscription + resp, body = self.delete_subscription(sub_id) + self.assertEqual(204, resp.status_code) + self.check_resp_headers_in_delete(resp) + + # 14. LCM-Terminate: Terminate VNF + terminate_req = paramgen.terminate_vnf_min() + resp, body = self.terminate_vnf_instance(inst_id, terminate_req) + self.assertEqual(202, resp.status_code) + + lcmocc_id = os.path.basename(resp.headers['Location']) + self.wait_lcmocc_complete(lcmocc_id) + + # wait a bit because there is a bit time lag between lcmocc DB + # update and terminate completion. + time.sleep(10) + + # check instantiationState of VNF + resp, body = self.show_vnf_instance(inst_id) + self.assertEqual(200, resp.status_code) + self.assertEqual(fields.VnfInstanceState.NOT_INSTANTIATED, + body['instantiationState']) + + # 15. LCM-Delete: Delete a VNF instance + resp, body = self.delete_vnf_instance(inst_id) + self.assertEqual(204, resp.status_code) + + # check deletion of VNF instance + resp, body = self.show_vnf_instance(inst_id) + self.assertEqual(404, resp.status_code) + + def test_faultmanagement_interface_max(self): + """Test FM operations with all attributes set + + * About attributes: + All of the following cardinality attributes are set. + In addition, 0..N or 1..N attributes are set to 2 or more. + - 0..1 (1) + - 0..N (2 or more) + - 1 + - 1..N (2 or more) + + * About LCM operations: + This test includes the following operations. + - 1. Create a new VNF instance resource + - 2. Instantiate a VNF instance + - 3. Create a new subscription + - 4. List subscription with attribute-based + - 5. Show subscription + - 6. Alert-Event (firing) + - 7. FM-List-Alarm + - 8. FM-Show-Alarm + - 9. FM-Update-Alarm + - 10. FM-Show-Alarm + - 11. Alert-Event (resolved) + - 12. FM-Show-Alarm + - 13. FM-Delete-Subscription: Delete subscription + - 14. Terminate a VNF instance + - 15. Delete a VNF instance + """ + + # 1. LCM-Create: Create a new VNF instance resource + # NOTE: extensions and vnfConfigurableProperties are omitted + # because they are commented out in etsi_nfv_sol001. + create_req = paramgen.instantiate_cnf_resources_create(self.vnfd_id_1) + resp, body = self.create_vnf_instance(create_req) + self.assertEqual(201, resp.status_code) + inst_id = body['id'] + + # 2. LCM-Instantiate: Instantiate a VNF instance + vim_id = self.get_k8s_vim_id() + instantiate_req = paramgen.min_sample_instantiate(vim_id) + instantiate_req['additionalParams'][ + 'lcm-kubernetes-def-files'] = ['Files/kubernetes/deployment.yaml'] + resp, body = self.instantiate_vnf_instance(inst_id, instantiate_req) + self.assertEqual(202, resp.status_code) + + lcmocc_id = os.path.basename(resp.headers['Location']) + self.wait_lcmocc_complete(lcmocc_id) + + resp, body = self.show_vnf_instance(inst_id) + self.assertEqual(200, resp.status_code) + vnfc_resource_infos = body['instantiatedVnfInfo'].get( + 'vnfcResourceInfo') + pod_name = [vnfc_info['computeResource']['resourceId'] + for vnfc_info in vnfc_resource_infos + if vnfc_info['vduId'] == 'VDU2'][0] + + # 3. FM-Create-Subscription: Create a new subscription + expected_inst_attrs = ['id', 'callbackUri', '_links', 'filter'] + callback_url = os.path.join(base_v2.MOCK_NOTIFY_CALLBACK_URL, + self._testMethodName) + callback_uri = ('http://localhost:' + f'{base_v2.FAKE_SERVER_MANAGER.SERVER_PORT}' + f'{callback_url}') + sub_req = paramgen.sub_create_max( + callback_uri, self.vnfd_id_1, inst_id) + resp, body = self.create_subscription(sub_req) + self.assertEqual(201, resp.status_code) + self.check_resp_headers_in_create(resp) + sub_id = body['id'] + self.check_resp_body(body, expected_inst_attrs) + # Test notification + self.assert_notification_get(callback_url) + + # 4. FM-List-Subscription: List subscription with attribute-based + # filtering + expected_attrs = ['id', 'callbackUri', '_links', 'filter'] + filter_expr = { + 'filter': f'(eq,id,{sub_id})' + } + resp, body = self.list_subscriptions(filter_expr) + self.assertEqual(200, resp.status_code) + self.check_resp_headers_in_get(resp) + for sbsc in body: + self.check_resp_body(sbsc, expected_attrs) + + # 5. FM-Show-Subscription: Show subscription + resp, body = self.show_subscription(sub_id) + self.assertEqual(200, resp.status_code) + self.check_resp_headers_in_get(resp) + self.check_resp_body(body, expected_attrs) + + # 6. Alert-Event (firing) + alert = paramgen.alert_event_firing(inst_id, pod_name) + resp, body = self.create_fm_alarm(alert) + self.assertEqual(204, resp.status_code) + time.sleep(5) + self._check_notification(callback_url, 'AlarmNotification') + + # 7. FM-List-Alarm + alarm_expected_attrs = [ + 'id', + 'managedObjectId', + 'alarmRaisedTime', + 'ackState', + 'perceivedSeverity', + 'eventTime', + 'eventType', + 'probableCause', + 'isRootCause', + '_links' + ] + filter_expr = {'filter': f'(eq,managedObjectId,{inst_id})'} + resp, body = self.list_fm_alarm(filter_expr) + self.assertEqual(200, resp.status_code) + alarm_id = body[0]['id'] + self.check_resp_headers_in_get(resp) + for sbsc in body: + self.check_resp_body(sbsc, alarm_expected_attrs) + + # 8. FM-Show-Alarm + resp, body = self.show_fm_alarm(alarm_id) + self.assertEqual(200, resp.status_code) + self.check_resp_headers_in_get(resp) + self.check_resp_body(body, alarm_expected_attrs) + + # 9. FM-Update-Alarm + expected_attrs = [ + 'ackState' + ] + update_req = paramgen.update_alarm() + resp, body = self.update_fm_alarm(alarm_id, update_req) + self.assertEqual(200, resp.status_code) + self.check_resp_headers_in_delete(resp) + self.check_resp_body(body, expected_attrs) + + # 10. FM-Show-Alarm + expected_attrs = [ + 'id', + 'managedObjectId', + 'alarmRaisedTime', + 'ackState', + 'perceivedSeverity', + 'eventTime', + 'eventType', + 'probableCause', + 'isRootCause', + '_links' + ] + resp, body = self.show_fm_alarm(alarm_id) + self.assertEqual(200, resp.status_code) + self.check_resp_headers_in_get(resp) + self.check_resp_body(body, expected_attrs) + + # 11. Alert-Event (resolved) + alert = paramgen.alert_event_resolved(inst_id, pod_name) + resp, body = self.create_fm_alarm(alert) + self.assertEqual(204, resp.status_code) + time.sleep(5) + self._check_notification(callback_url, 'AlarmClearedNotification') + + # 12. FM-Show-Alarm + resp, body = self.show_fm_alarm(alarm_id) + self.assertEqual(200, resp.status_code) + self.check_resp_headers_in_get(resp) + self.check_resp_body(body, alarm_expected_attrs) + + # 13. FM-Delete-Subscription: Delete subscription + resp, body = self.delete_subscription(sub_id) + self.assertEqual(204, resp.status_code) + self.check_resp_headers_in_delete(resp) + + # 14. LCM-Terminate: Terminate VNF + terminate_req = paramgen.terminate_vnf_min() + resp, body = self.terminate_vnf_instance(inst_id, terminate_req) + self.assertEqual(202, resp.status_code) + + lcmocc_id = os.path.basename(resp.headers['Location']) + self.wait_lcmocc_complete(lcmocc_id) + + # wait a bit because there is a bit time lag between lcmocc DB + # update and terminate completion. + time.sleep(10) + + # check instantiationState of VNF + resp, body = self.show_vnf_instance(inst_id) + self.assertEqual(200, resp.status_code) + self.assertEqual(fields.VnfInstanceState.NOT_INSTANTIATED, + body['instantiationState']) + + # 15. LCM-Delete: Delete a VNF instance + resp, body = self.delete_vnf_instance(inst_id) + self.assertEqual(204, resp.status_code) + + # check deletion of VNF instance + resp, body = self.show_vnf_instance(inst_id) + self.assertEqual(404, resp.status_code) diff --git a/tacker/tests/functional/sol_kubernetes_v2/test_vnfpm_basic.py b/tacker/tests/functional/sol_kubernetes_v2/test_vnfpm_basic.py new file mode 100644 index 000000000..6fb98c4ac --- /dev/null +++ b/tacker/tests/functional/sol_kubernetes_v2/test_vnfpm_basic.py @@ -0,0 +1,345 @@ +# Copyright (C) 2022 Fujitsu +# All Rights Reserved. +# +# 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. +import ddt +import os +import time + +from tacker.objects import fields +from tacker.tests.functional.sol_kubernetes_v2 import base_v2 +from tacker.tests.functional.sol_kubernetes_v2 import paramgen + + +@ddt.ddt +class VnfPmTest(base_v2.BaseVnfLcmKubernetesV2Test): + + @classmethod + def setUpClass(cls): + super(VnfPmTest, cls).setUpClass() + + cur_dir = os.path.dirname(__file__) + + test_instantiate_cnf_resources_path = os.path.join( + cur_dir, "samples/test_instantiate_cnf_resources") + cls.vnf_pkg_1, cls.vnfd_id_1 = cls.create_vnf_package( + test_instantiate_cnf_resources_path) + + @classmethod + def tearDownClass(cls): + super(VnfPmTest, cls).tearDownClass() + cls.delete_vnf_package(cls.vnf_pkg_1) + + def setUp(self): + super(VnfPmTest, self).setUp() + base_v2.FAKE_SERVER_MANAGER.set_callback( + 'PUT', "/-/reload", status_code=202, + response_headers={"Content-Type": "text/plain"}) + + def test_performancemanagement_interface_min(self): + """Test PM operations with all attributes set + + * About attributes: + All of the following cardinality attributes are set. + In addition, 0..N or 1..N attributes are set to 2 or more. + - 0..1 (1) + - 0..N (2 or more) + - 1 + - 1..N (2 or more) + + * About LCM operations: + This test includes the following operations. + - 1. Create a new VNF instance resource + - 2. Instantiate a VNF instance + - 3. PMJob-Create + - 4. PMJob-Update + - 5. PM-Event + - 6. PMJob-List + - 7. PMJob-Show + - 8. PMJob-Report-Show + - 9. PMJob-Delete + - 10. Terminate a VNF instance + - 11. Delete a VNF instance + """ + # 1. LCM-Create: Create a new VNF instance resource + # NOTE: extensions and vnfConfigurableProperties are omitted + # because they are commented out in etsi_nfv_sol001. + create_req = paramgen.pm_instantiate_cnf_resources_create( + self.vnfd_id_1) + resp, body = self.create_vnf_instance(create_req) + self.assertEqual(201, resp.status_code) + inst_id = body['id'] + + # 2. LCM-Instantiate: Instantiate a VNF instance + vim_id = self.get_k8s_vim_id() + instantiate_req = paramgen.min_sample_instantiate(vim_id) + instantiate_req['additionalParams'][ + 'lcm-kubernetes-def-files'] = ['Files/kubernetes/deployment.yaml'] + resp, body = self.instantiate_vnf_instance(inst_id, instantiate_req) + self.assertEqual(202, resp.status_code) + + lcmocc_id = os.path.basename(resp.headers['Location']) + self.wait_lcmocc_complete(lcmocc_id) + + # 3. PMJob-Create + pm_expected_attrs = [ + 'id', + 'objectType', + 'objectInstanceIds', + 'criteria', + 'callbackUri', + '_links' + ] + callback_url = os.path.join(base_v2.MOCK_NOTIFY_CALLBACK_URL, + self._testMethodName) + callback_uri = ('http://localhost:' + f'{base_v2.FAKE_SERVER_MANAGER.SERVER_PORT}' + f'{callback_url}') + sub_req = paramgen.pm_job_min( + callback_uri, inst_id, self.fake_prometheus_ip) + resp, body = self.create_pm_job(sub_req) + self.assertEqual(201, resp.status_code) + self.check_resp_headers_in_create(resp) + self.check_resp_body(body, pm_expected_attrs) + # Test notification + self.assert_notification_get(callback_url) + pm_job_id = body.get('id') + + # 4. PMJob-Update + callback_url = os.path.join(base_v2.MOCK_NOTIFY_CALLBACK_URL, + self._testMethodName) + callback_url = callback_url + '_1' + callback_uri = ('http://localhost:' + f'{base_v2.FAKE_SERVER_MANAGER.SERVER_PORT}' + f'{callback_url}') + base_v2.FAKE_SERVER_MANAGER.set_callback( + 'GET', callback_url, status_code=204) + base_v2.FAKE_SERVER_MANAGER.set_callback( + 'POST', callback_url, status_code=204) + update_req = paramgen.update_pm_job(callback_uri) + resp, body = self.update_pm_job(pm_job_id, update_req) + self.assertEqual(200, resp.status_code) + self.check_resp_headers_in_delete(resp) + # Test notification + self.assert_notification_get(callback_url) + + # 5. PMJob-Event + sub_req = paramgen.pm_event(pm_job_id, inst_id) + resp, body = self.create_pm_event(sub_req) + self.assertEqual(204, resp.status_code) + time.sleep(5) + self._check_notification( + callback_url, 'PerformanceInformationAvailableNotification') + + # 6. PMJob-List + resp, body = self.list_pm_job() + self.assertEqual(200, resp.status_code) + self.check_resp_headers_in_get(resp) + for sbsc in body: + self.check_resp_body(sbsc, pm_expected_attrs) + + # 7. PMJob-Show + resp, body = self.show_pm_job(pm_job_id) + self.assertEqual(200, resp.status_code) + self.check_resp_headers_in_get(resp) + self.check_resp_body(body, pm_expected_attrs) + reports = body['reports'] + href = reports[0]['href'] + report_id = href.split('/')[-1] + + # 8. PMJob-Report-Show + expected_attrs = ['entries'] + resp, body = self.show_pm_job_report(pm_job_id, report_id) + self.assertEqual(200, resp.status_code) + self.check_resp_headers_in_get(resp) + self.check_resp_body(body, expected_attrs) + + # 9. PMJob-Delete + resp, body = self.delete_pm_job(pm_job_id) + self.assertEqual(204, resp.status_code) + self.check_resp_headers_in_delete(resp) + + # 10. LCM-Terminate: Terminate VNF + terminate_req = paramgen.terminate_vnf_min() + resp, body = self.terminate_vnf_instance(inst_id, terminate_req) + self.assertEqual(202, resp.status_code) + + lcmocc_id = os.path.basename(resp.headers['Location']) + self.wait_lcmocc_complete(lcmocc_id) + + # wait a bit because there is a bit time lag between lcmocc DB + # update and terminate completion. + time.sleep(10) + + # check instantiationState of VNF + resp, body = self.show_vnf_instance(inst_id) + self.assertEqual(200, resp.status_code) + self.assertEqual(fields.VnfInstanceState.NOT_INSTANTIATED, + body['instantiationState']) + + # 11. LCM-Delete: Delete a VNF instance + resp, body = self.delete_vnf_instance(inst_id) + self.assertEqual(204, resp.status_code) + + # check deletion of VNF instance + resp, body = self.show_vnf_instance(inst_id) + self.assertEqual(404, resp.status_code) + + def test_performancemanagement_interface_max(self): + """Test PM operations with all attributes set + + * About attributes: + All of the following cardinality attributes are set. + In addition, 0..N or 1..N attributes are set to 2 or more. + - 0..1 (1) + - 0..N (2 or more) + - 1 + - 1..N (2 or more) + + * About LCM operations: + This test includes the following operations. + - 1. Create a new VNF instance resource + - 2. Instantiate a VNF instance + - 3. PMJob-Create + - 4. PMJob-Update + - 5. PM-Event + - 6. PMJob-List + - 7. PMJob-Show + - 8. PMJob-Report-Show + - 9. PMJob-Delete + - 10. Terminate a VNF instance + - 11. Delete a VNF instance + """ + # 1. LCM-Create: Create a new VNF instance resource + # NOTE: extensions and vnfConfigurableProperties are omitted + # because they are commented out in etsi_nfv_sol001. + create_req = paramgen.instantiate_cnf_resources_create(self.vnfd_id_1) + resp, body = self.create_vnf_instance(create_req) + self.assertEqual(201, resp.status_code) + inst_id = body['id'] + + # 2. LCM-Instantiate: Instantiate a VNF instance + vim_id = self.get_k8s_vim_id() + instantiate_req = paramgen.min_sample_instantiate(vim_id) + instantiate_req['additionalParams'][ + 'lcm-kubernetes-def-files'] = ['Files/kubernetes/deployment.yaml'] + resp, body = self.instantiate_vnf_instance(inst_id, instantiate_req) + self.assertEqual(202, resp.status_code) + + lcmocc_id = os.path.basename(resp.headers['Location']) + self.wait_lcmocc_complete(lcmocc_id) + + # 3. PMJob-Create + pm_expected_attrs = [ + 'id', + 'objectType', + 'objectInstanceIds', + 'criteria', + 'callbackUri', + '_links' + ] + callback_url = os.path.join(base_v2.MOCK_NOTIFY_CALLBACK_URL, + self._testMethodName) + callback_uri = ('http://localhost:' + f'{base_v2.FAKE_SERVER_MANAGER.SERVER_PORT}' + f'{callback_url}') + sub_req = paramgen.pm_job_max( + callback_uri, inst_id, self.fake_prometheus_ip) + resp, body = self.create_pm_job(sub_req) + self.assertEqual(201, resp.status_code) + self.check_resp_headers_in_create(resp) + self.check_resp_body(body, pm_expected_attrs) + # Test notification + self.assert_notification_get(callback_url) + pm_job_id = body.get('id') + + # 4. PMJob-Update + callback_url = os.path.join(base_v2.MOCK_NOTIFY_CALLBACK_URL, + self._testMethodName) + callback_url = callback_url + '_1' + callback_uri = ('http://localhost:' + f'{base_v2.FAKE_SERVER_MANAGER.SERVER_PORT}' + f'{callback_url}') + base_v2.FAKE_SERVER_MANAGER.set_callback( + 'GET', callback_url, status_code=204) + base_v2.FAKE_SERVER_MANAGER.set_callback( + 'POST', callback_url, status_code=204) + update_req = paramgen.update_pm_job(callback_uri) + resp, body = self.update_pm_job(pm_job_id, update_req) + self.assertEqual(200, resp.status_code) + self.check_resp_headers_in_delete(resp) + # Test notification + self.assert_notification_get(callback_url) + + # 5. PMJob-Event + sub_req = paramgen.pm_event(pm_job_id, inst_id) + resp, body = self.create_pm_event(sub_req) + self.assertEqual(204, resp.status_code) + time.sleep(5) + self._check_notification( + callback_url, 'PerformanceInformationAvailableNotification') + + # 6. PMJob-List + filter_expr = {'filter': '(eq,objectType,VirtualCompute)'} + resp, body = self.list_pm_job(filter_expr) + self.assertEqual(200, resp.status_code) + self.check_resp_headers_in_get(resp) + for sbsc in body: + self.check_resp_body(sbsc, pm_expected_attrs) + + # 7. PMJob-Show + resp, body = self.show_pm_job(pm_job_id) + self.assertEqual(200, resp.status_code) + self.check_resp_headers_in_get(resp) + self.check_resp_body(body, pm_expected_attrs) + reports = body['reports'] + href = reports[0]['href'] + report_id = href.split('/')[-1] + + # 8. PMJob-Show-Report + expected_attrs = ['entries'] + resp, body = self.show_pm_job_report(pm_job_id, report_id) + self.assertEqual(200, resp.status_code) + self.check_resp_headers_in_get(resp) + self.check_resp_body(body, expected_attrs) + + # 9. PMJob-Delete + resp, body = self.delete_pm_job(pm_job_id) + self.assertEqual(204, resp.status_code) + self.check_resp_headers_in_delete(resp) + + # 10. LCM-Terminate: Terminate VNF + terminate_req = paramgen.terminate_vnf_min() + resp, body = self.terminate_vnf_instance(inst_id, terminate_req) + self.assertEqual(202, resp.status_code) + + lcmocc_id = os.path.basename(resp.headers['Location']) + self.wait_lcmocc_complete(lcmocc_id) + + # wait a bit because there is a bit time lag between lcmocc DB + # update and terminate completion. + time.sleep(10) + + # check instantiationState of VNF + resp, body = self.show_vnf_instance(inst_id) + self.assertEqual(200, resp.status_code) + self.assertEqual(fields.VnfInstanceState.NOT_INSTANTIATED, + body['instantiationState']) + + # 11. LCM-Delete: Delete a VNF instance + resp, body = self.delete_vnf_instance(inst_id) + self.assertEqual(204, resp.status_code) + + # check deletion of VNF instance + resp, body = self.show_vnf_instance(inst_id) + self.assertEqual(404, resp.status_code) diff --git a/tacker/tests/unit/sol_refactored/api/test_api_version.py b/tacker/tests/unit/sol_refactored/api/test_api_version.py index 9cba7b5f6..3a96e869f 100644 --- a/tacker/tests/unit/sol_refactored/api/test_api_version.py +++ b/tacker/tests/unit/sol_refactored/api/test_api_version.py @@ -25,8 +25,9 @@ class TestAPIVersion(base.BaseTestCase): self.assertTrue(vers.is_null()) def test_init(self): - supported_versions = ["3.1.4159", "2.0.0"] + supported_versions = ["3.1.4159", "2.0.0", "2.1.0"] for vers, vers_str in [("2.0.0", "2.0.0"), + ("2.1.0", "2.1.0"), ("3.1.4159", "3.1.4159"), ("2.0.0-impl:foobar", "2.0.0")]: v = api_version.APIVersion(vers, supported_versions) @@ -34,6 +35,14 @@ class TestAPIVersion(base.BaseTestCase): def test_init_exceptions(self): supported_versions = ["2.0.0"] + self.assertRaises(sol_ex.APIVersionMissing, + api_version.APIVersion, None, supported_versions) + + self.assertRaises(sol_ex.InvalidAPIVersionString, + api_version.APIVersion, + "2.0.0-abc:foobar", + ["2.0.0"]) + self.assertRaises(sol_ex.InvalidAPIVersionString, api_version.APIVersion, "0.1.2", supported_versions) @@ -50,11 +59,19 @@ class TestAPIVersion(base.BaseTestCase): def test_matches(self): supported_versions = ["1.3.0", "1.3.1", "2.0.0"] - vers = api_version.APIVersion("2.0.0") + vers = api_version.APIVersion("1.3.1") + self.assertTrue( + vers.matches(api_version.APIVersion(), api_version.APIVersion()) + ) + self.assertTrue( vers.matches(api_version.APIVersion("1.3.0", supported_versions), api_version.APIVersion())) - self.assertFalse( + self.assertTrue( vers.matches(api_version.APIVersion(), - api_version.APIVersion("1.3.1", supported_versions))) + api_version.APIVersion("2.0.0", supported_versions))) + + self.assertTrue( + vers.matches(api_version.APIVersion("1.3.0", supported_versions), + api_version.APIVersion("2.0.0", supported_versions))) diff --git a/tacker/tests/unit/sol_refactored/api/test_prometheus_plugin.py b/tacker/tests/unit/sol_refactored/api/test_prometheus_plugin.py new file mode 100644 index 000000000..17e8586ee --- /dev/null +++ b/tacker/tests/unit/sol_refactored/api/test_prometheus_plugin.py @@ -0,0 +1,88 @@ +# Copyright (C) 2022 Fujitsu +# All Rights Reserved. +# +# 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. + +from tacker import context +from tacker.sol_refactored.api import prometheus_plugin_wsgi as pp_wsgi +from tacker.sol_refactored.common import exceptions as sol_ex +from tacker.sol_refactored import objects +from tacker.tests.unit import base + +from unittest import mock + + +class TestPrometheusPlugin(base.TestCase): + def setUp(self): + super(TestPrometheusPlugin, self).setUp() + objects.register_all() + self.context = context.get_admin_context() + self.request = mock.Mock() + self.request.context = self.context + + @mock.patch.object(pp_wsgi.PrometheusPluginErrorResponse, 'serialize') + def test_response(self, mock_serialize_pp): + class _Test(): + def __init__(self, ctx, title): + self.status = 200 + self.detail = 'detail' + self.title = title + self.method = 'GET' + self.url = 'url' + self.environ = None + self.body = {} + self.context = ctx + self.status_int = 200 + + def best_match_content_type(self): + return 'application/json' + + def serialize(self, accept): + if self.title == 'error': + raise sol_ex.SolValidationError( + detail='test error') + return self + + def test(*args, **kwargs): + return (None, None, None) + + def test2(*args, **kwargs): + return _Test(None, None) + + def test3(*args, **kwargs): + return _Test(None, 'error') + + # make responses + pp_wsgi.PrometheusPluginResponse( + 200, {}, content_type='content_type') + pp_wsgi.PrometheusPluginErrorResponse( + _Test(self.context, None), None) + pp_wsgi.PrometheusPluginErrorResponse( + _Test(self.context, 'title'), None) + + # no error + p = pp_wsgi.PrometheusPluginResource( + None, 'tacker_prometheus_plugin_api:prometheus_plugin:alert') + p(_Test(self.context, None)) + + # raise unknown error + p = pp_wsgi.PrometheusPluginResource( + None, 'tacker_prometheus_plugin_api:prometheus_plugin:alert') + p._deserialize_request = test + p._check_policy = test + p._dispatch = test2 + p(_Test(self.context, None)) + + mock_serialize_pp.side_effect = _Test(self.context, 'error') + p._dispatch = test3 + p(_Test(self.context, 'error')) diff --git a/tacker/tests/unit/sol_refactored/common/test_fm_alarm_utils.py b/tacker/tests/unit/sol_refactored/common/test_fm_alarm_utils.py new file mode 100644 index 000000000..29e2c7d73 --- /dev/null +++ b/tacker/tests/unit/sol_refactored/common/test_fm_alarm_utils.py @@ -0,0 +1,102 @@ +# Copyright (C) 2022 Fujitsu +# All Rights Reserved. +# +# 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. +import copy +from unittest import mock + +from tacker import context +from tacker.sol_refactored.common import exceptions as sol_ex +from tacker.sol_refactored.common import fm_alarm_utils as alarm_utils +from tacker.sol_refactored import objects +from tacker.tests import base +from tacker.tests.unit.sol_refactored.samples import fakes_for_fm + + +class TestFmAlarmUtils(base.BaseTestCase): + def setUp(self): + super(TestFmAlarmUtils, self).setUp() + objects.register_all() + self.context = context.get_admin_context() + + @mock.patch.object(objects.base.TackerPersistentObject, 'get_by_id') + def test_get_alarm(self, mock_alarm): + mock_alarm.return_value = objects.AlarmV1.from_dict( + fakes_for_fm.alarm_example) + + result = alarm_utils.get_alarm( + context, fakes_for_fm.alarm_example['id']) + self.assertEqual(fakes_for_fm.alarm_example['id'], result.id) + + mock_alarm.return_value = None + self.assertRaises( + sol_ex.AlarmNotFound, + alarm_utils.get_alarm, context, fakes_for_fm.alarm_example['id']) + + @mock.patch.object(objects.base.TackerPersistentObject, 'get_all') + def test_get_alarms_all(self, mock_alarms): + mock_alarms.return_value = [objects.AlarmV1.from_dict( + fakes_for_fm.alarm_example)] + + result = alarm_utils.get_alarms_all(context) + self.assertEqual(fakes_for_fm.alarm_example['id'], result[0].id) + + @mock.patch.object(objects.base.TackerPersistentObject, 'get_by_filter') + def test_get_not_cleared_alarms(self, mock_alarms): + mock_alarms.return_value = [objects.AlarmV1.from_dict( + fakes_for_fm.alarm_example)] + + result = alarm_utils.get_not_cleared_alarms( + context, fakes_for_fm.alarm_example['managedObjectId']) + self.assertEqual(fakes_for_fm.alarm_example['id'], result[0].id) + + def test_make_alarm_links(self): + alarm = objects.AlarmV1.from_dict(fakes_for_fm.alarm_example) + endpoint = 'http://127.0.0.1:9890' + + expected_result = objects.AlarmV1_Links() + expected_result.self = objects.Link( + href=f'{endpoint}/vnffm/v1/alarms/{alarm.id}') + expected_result.objectInstance = objects.Link( + href=f'{endpoint}/vnflcm/v2/vnf_instances/{alarm.managedObjectId}') + + result = alarm_utils.make_alarm_links(alarm, endpoint) + self.assertEqual(expected_result.self.href, result.self.href) + self.assertEqual(expected_result.objectInstance.href, + result.objectInstance.href) + + def test_make_alarm_notif_data(self): + subsc = objects.FmSubscriptionV1.from_dict( + fakes_for_fm.fm_subsc_example) + alarm = objects.AlarmV1.from_dict(fakes_for_fm.alarm_example) + endpoint = 'http://127.0.0.1:9890' + + # execute alarm_cleared + alarm_cleared_result = alarm_utils.make_alarm_notif_data( + subsc, alarm, endpoint) + + # execute alarm + alarm_clear = copy.deepcopy(fakes_for_fm.alarm_example) + del alarm_clear['alarmClearedTime'] + alarm = objects.AlarmV1.from_dict(alarm_clear) + alarm_result = alarm_utils.make_alarm_notif_data( + subsc, alarm, endpoint) + + self.assertEqual('AlarmClearedNotificationV1', + type(alarm_cleared_result).__name__) + self.assertEqual('AlarmClearedNotification', + alarm_cleared_result.notificationType) + self.assertEqual('AlarmNotificationV1', type(alarm_result).__name__) + self.assertEqual('AlarmNotification', + alarm_result.notificationType) + self.assertEqual(alarm_clear, alarm_result.alarm.to_dict()) diff --git a/tacker/tests/unit/sol_refactored/common/test_fm_subscription_utils.py b/tacker/tests/unit/sol_refactored/common/test_fm_subscription_utils.py new file mode 100644 index 000000000..6cbc9b716 --- /dev/null +++ b/tacker/tests/unit/sol_refactored/common/test_fm_subscription_utils.py @@ -0,0 +1,266 @@ +# Copyright (C) 2022 Fujitsu +# All Rights Reserved. +# +# 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. +import copy +import requests +from unittest import mock + +from oslo_log import log as logging + +from tacker import context +from tacker.sol_refactored.common import exceptions as sol_ex +from tacker.sol_refactored.common import fm_alarm_utils as alarm_utils +from tacker.sol_refactored.common import fm_subscription_utils as subsc_utils +from tacker.sol_refactored.common import http_client +from tacker.sol_refactored import objects +from tacker.tests import base +from tacker.tests.unit.sol_refactored.samples import fakes_for_fm + + +LOG = logging.getLogger(__name__) + + +class TestFmSubscriptionUtils(base.BaseTestCase): + def setUp(self): + super(TestFmSubscriptionUtils, self).setUp() + objects.register_all() + self.context = context.get_admin_context() + + @mock.patch.object(objects.base.TackerPersistentObject, 'get_by_id') + def test_get_subsc(self, mock_subsc): + mock_subsc.return_value = objects.FmSubscriptionV1.from_dict( + fakes_for_fm.fm_subsc_example) + + result = subsc_utils.get_subsc( + context, fakes_for_fm.fm_subsc_example['id']) + self.assertEqual(fakes_for_fm.fm_subsc_example['id'], result.id) + + mock_subsc.return_value = None + self.assertRaises( + sol_ex.FmSubscriptionNotFound, + subsc_utils.get_subsc, context, 'subsc-1') + + @mock.patch.object(objects.base.TackerPersistentObject, 'get_all') + def test_get_subsc_all(self, mock_subsc): + mock_subsc.return_value = [objects.FmSubscriptionV1.from_dict( + fakes_for_fm.fm_subsc_example)] + + result = subsc_utils.get_subsc_all(context) + self.assertEqual(fakes_for_fm.fm_subsc_example['id'], result[0].id) + + @mock.patch.object(http_client.HttpClient, 'do_request') + def test_send_notification(self, mock_resp): + subsc_no_auth = objects.FmSubscriptionV1.from_dict( + fakes_for_fm.fm_subsc_example) + alarm = objects.AlarmV1.from_dict(fakes_for_fm.alarm_example) + notif_data_no_auth = alarm_utils.make_alarm_notif_data( + subsc_no_auth, alarm, 'http://127.0.0.1:9890') + resp_no_auth = requests.Response() + resp_no_auth.status_code = 204 + mock_resp.return_value = (resp_no_auth, None) + + # execute no_auth + subsc_utils.send_notification(subsc_no_auth, notif_data_no_auth) + + subsc_basic_auth = copy.deepcopy(subsc_no_auth) + subsc_basic_auth.authentication = objects.SubscriptionAuthentication( + paramsBasic=objects.SubscriptionAuthentication_ParamsBasic( + userName='test', password='test')) + + # execute basic_auth + subsc_utils.send_notification(subsc_basic_auth, notif_data_no_auth) + + subsc_oauth2 = copy.deepcopy(subsc_no_auth) + subsc_oauth2.authentication = objects.SubscriptionAuthentication( + paramsOauth2ClientCredentials=( + objects.SubscriptionAuthentication_ParamsOauth2( + clientId='test', clientPassword='test', + tokenEndpoint='http://127.0.0.1/token'))) + + # execute oauth2 + subsc_utils.send_notification(subsc_oauth2, notif_data_no_auth) + + self.assertEqual(3, mock_resp.call_count) + + @mock.patch.object(http_client.HttpClient, 'do_request') + def test_send_notification_error_code(self, mock_resp): + subsc_no_auth = objects.FmSubscriptionV1.from_dict( + fakes_for_fm.fm_subsc_example) + alarm = objects.AlarmV1.from_dict(fakes_for_fm.alarm_example) + notif_data_no_auth = alarm_utils.make_alarm_notif_data( + subsc_no_auth, alarm, 'http://127.0.0.1:9890') + resp_no_auth = requests.Response() + resp_no_auth.status_code = 200 + mock_resp.return_value = (resp_no_auth, None) + + # execute no_auth + subsc_utils.send_notification(subsc_no_auth, notif_data_no_auth) + self.assertLogs(LOG, 'ERROR') + + def test_send_notification_error(self): + subsc_no_auth = objects.FmSubscriptionV1.from_dict( + fakes_for_fm.fm_subsc_example) + alarm = objects.AlarmV1.from_dict(fakes_for_fm.alarm_example) + notif_data_no_auth = alarm_utils.make_alarm_notif_data( + subsc_no_auth, alarm, 'http://127.0.0.1:9890') + + # execute no_auth + subsc_utils.send_notification(subsc_no_auth, notif_data_no_auth) + self.assertLogs(LOG, 'EXCEPTION') + + @mock.patch.object(http_client.HttpClient, 'do_request') + def test_test_notification(self, mock_resp): + subsc_no_auth = objects.FmSubscriptionV1.from_dict( + fakes_for_fm.fm_subsc_example) + + resp_no_auth = requests.Response() + resp_no_auth.status_code = 204 + mock_resp.return_value = (resp_no_auth, None) + + # execute no_auth + subsc_utils.test_notification(subsc_no_auth) + + @mock.patch.object(http_client.HttpClient, 'do_request') + def test_test_notification_error_code(self, mock_resp): + subsc_no_auth = objects.FmSubscriptionV1.from_dict( + fakes_for_fm.fm_subsc_example) + resp_no_auth = requests.Response() + resp_no_auth.status_code = 200 + mock_resp.return_value = (resp_no_auth, None) + + # execute no_auth + self.assertRaises(sol_ex.TestNotificationFailed, + subsc_utils.test_notification, subsc_no_auth) + + class mock_session(): + + def request(url, method, raise_exc=False, **kwargs): + resp = requests.Response() + resp.status_code = 400 + resp.headers['Content-Type'] = 'application/zip' + return resp + + @mock.patch.object(http_client.HttpClient, '_decode_body') + @mock.patch.object(http_client.NoAuthHandle, 'get_session') + def test_test_notification_error(self, mock_session, mock_decode_body): + subsc_no_auth = objects.FmSubscriptionV1.from_dict( + fakes_for_fm.fm_subsc_example) + + mock_session.return_value = self.mock_session + mock_decode_body.return_value = None + + self.assertRaises(sol_ex.TestNotificationFailed, + subsc_utils.test_notification, subsc_no_auth) + + @mock.patch.object(objects.base.TackerPersistentObject, 'get_all') + def test_get_matched_subscs(self, mock_subscs): + inst = objects.VnfInstanceV2(id='test-instance', vnfProvider='company') + notif_type = 'AlarmClearedNotification' + new_alarm_example = copy.deepcopy(fakes_for_fm.alarm_example) + new_alarm_example['perceivedSeverity'] = 'CRITICAL' + new_alarm_example['eventType'] = 'COMMUNICATIONS_ALARM' + alarm = objects.AlarmV1.from_dict(new_alarm_example) + + subscs_no_fileter = objects.FmSubscriptionV1(id='subsc-1') + + products_vnfproducts_no_exist = objects._VnfProductsFromProviders( + vnfProvider='company') + inst_filter_match_products = objects.VnfInstanceSubscriptionFilter( + vnfProductsFromProviders=[products_vnfproducts_no_exist]) + subscs_filter_match = objects.FmSubscriptionV1( + id='subsc-2', + filter=objects.FmNotificationsFilterV1( + vnfInstanceSubscriptionFilter=inst_filter_match_products)) + + products_mismatch = objects._VnfProductsFromProviders( + vnfProvider='test') + inst_filter_mismatch_products = objects.VnfInstanceSubscriptionFilter( + vnfProductsFromProviders=[products_mismatch]) + subscs_filter_mismatch = objects.FmSubscriptionV1( + id='subsc-3', + filter=objects.FmNotificationsFilterV1( + vnfInstanceSubscriptionFilter=inst_filter_mismatch_products)) + + subscs_noti_type_match = objects.FmSubscriptionV1( + id='subsc-4', filter=objects.FmNotificationsFilterV1( + notificationTypes=['AlarmClearedNotification'])) + subscs_noti_type_mismatch = objects.FmSubscriptionV1( + id='subsc-5', filter=objects.FmNotificationsFilterV1( + notificationTypes=['AlarmNotification'])) + + subscs_faulty_res_type_match = objects.FmSubscriptionV1( + id='subsc-6', filter=objects.FmNotificationsFilterV1( + faultyResourceTypes=['COMPUTE'])) + subscs_faulty_res_type_mismatch = objects.FmSubscriptionV1( + id='subsc-7', filter=objects.FmNotificationsFilterV1( + faultyResourceTypes=['STORAGE'])) + + subscs_per_sev_match = objects.FmSubscriptionV1( + id='subsc-8', filter=objects.FmNotificationsFilterV1( + perceivedSeverities=['CRITICAL'])) + subscs_per_sev_mismatch = objects.FmSubscriptionV1( + id='subsc-9', filter=objects.FmNotificationsFilterV1( + perceivedSeverities=['MAJOR'])) + + subscs_event_type_match = objects.FmSubscriptionV1( + id='subsc-10', filter=objects.FmNotificationsFilterV1( + eventTypes=['COMMUNICATIONS_ALARM'])) + subscs_event_type_mismatch = objects.FmSubscriptionV1( + id='subsc-11', filter=objects.FmNotificationsFilterV1( + eventTypes=['PROCESSING_ERROR_ALARM'])) + + subscs_probable_cause_match = objects.FmSubscriptionV1( + id='subsc-12', filter=objects.FmNotificationsFilterV1( + probableCauses=['The server cannot be connected.'])) + subscs_probable_cause_mismatch = objects.FmSubscriptionV1( + id='subsc-13', filter=objects.FmNotificationsFilterV1( + probableCauses=['The server is invalid.'])) + + mock_subscs.return_value = [ + subscs_no_fileter, subscs_filter_match, subscs_filter_mismatch, + subscs_noti_type_match, subscs_noti_type_mismatch, + subscs_faulty_res_type_match, subscs_faulty_res_type_mismatch, + subscs_per_sev_match, subscs_per_sev_mismatch, + subscs_event_type_match, subscs_event_type_mismatch, + subscs_probable_cause_match, subscs_probable_cause_mismatch] + + result = subsc_utils.get_matched_subscs( + context, inst, notif_type, alarm) + + expected_ids = ['subsc-1', 'subsc-2', 'subsc-4', 'subsc-6', + 'subsc-8', 'subsc-10', 'subsc-12'] + + result_ids = [sub.id for sub in result] + self.assertEqual(expected_ids, result_ids) + + @mock.patch.object(objects.base.TackerPersistentObject, 'get_all') + def test_get_alarm_subscs(self, mock_subscs): + inst = objects.VnfInstanceV2( + id='dummy-vnfInstanceId-1', vnfdId='dummy-vnfdId-1', + vnfProvider='dummy-vnfProvider-1', + vnfProductName='dummy-vnfProductName-1-1', + vnfSoftwareVersion='1.0', vnfdVersion='1.0', + vnfInstanceName='dummy-vnfInstanceName-1') + alarm = objects.AlarmV1.from_dict(fakes_for_fm.alarm_example) + mock_subscs.return_value = [objects.FmSubscriptionV1.from_dict( + fakes_for_fm.fm_subsc_example)] + result = subsc_utils.get_alarm_subscs(context, alarm, inst) + self.assertEqual(fakes_for_fm.fm_subsc_example['id'], result[0].id) + + alarm_clear = copy.deepcopy(fakes_for_fm.alarm_example) + del alarm_clear['alarmClearedTime'] + alarm = objects.AlarmV1.from_dict(alarm_clear) + result = subsc_utils.get_alarm_subscs(context, alarm, inst) + + self.assertEqual(fakes_for_fm.fm_subsc_example['id'], result[0].id) diff --git a/tacker/tests/unit/sol_refactored/common/test_pm_job_utils.py b/tacker/tests/unit/sol_refactored/common/test_pm_job_utils.py new file mode 100644 index 000000000..b916b6315 --- /dev/null +++ b/tacker/tests/unit/sol_refactored/common/test_pm_job_utils.py @@ -0,0 +1,318 @@ +# Copyright (C) 2022 FUJITSU +# All Rights Reserved. +# +# 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. +from oslo_utils import uuidutils +import requests +from unittest import mock + +from tacker import context +from tacker.sol_refactored.api import api_version +from tacker.sol_refactored.common import exceptions as sol_ex +from tacker.sol_refactored.common import http_client +from tacker.sol_refactored.common import pm_job_utils +from tacker.sol_refactored import objects +from tacker.tests import base + + +class TestPmJobUtils(base.BaseTestCase): + + def setUp(self): + super(TestPmJobUtils, self).setUp() + objects.register_all() + self.context = context.get_admin_context() + self.context.api_version = api_version.APIVersion('2.1.0') + + @mock.patch.object(objects.base.TackerPersistentObject, 'get_by_id') + def test_update_report(self, mock_pm): + _PmJobCriteriaV2 = objects.VnfPmJobCriteriaV2( + performanceMetric=['VCpuUsageMeanVnf.VNF'], + performanceMetricGroup=['VirtualisedComputeResource'], + collectionPeriod=10, + reportingPeriod=11, + reportingBoundary='2000-05-23', + ) + _SubscriptionAuthentication = objects.SubscriptionAuthentication( + authType=['BASIC'], + paramsBasic=objects.SubscriptionAuthentication_ParamsBasic( + username='test_name', + password='test_pwd' + ) + ) + mock_pm.return_value = objects.PmJobV2( + id='pm_job_1', + objectType='VNF', + objectInstanceIds=['id_1'], + subObjectInstanceIds=['sub_id_1', 'sub_id_2'], + criteria=_PmJobCriteriaV2, + callbackUri='callbackuri', + authentication=_SubscriptionAuthentication + ) + report = objects.PerformanceReportV2( + id=uuidutils.generate_uuid(), + jobId='pm_job_1', + ) + + result = pm_job_utils.update_report(self.context, 'pm_job_1', + report, '2008-01-03 08:04:34') + self.assertEqual('pm_job_1', result.id) + + @mock.patch.object(objects.base.TackerPersistentObject, 'get_all') + def test_get_pm_job_all(self, mock_pm): + mock_pm.return_value = [objects.PmJobV2(id='pm_job_1')] + + result = pm_job_utils.get_pm_job_all(context) + self.assertEqual('pm_job_1', result[0].id) + + @mock.patch.object(objects.base.TackerPersistentObject, 'get_by_id') + def test_get_pm_job(self, mock_pm): + mock_pm.return_value = objects.PmJobV2(id='pm_job_1') + + result = pm_job_utils.get_pm_job(context, 'pm_job_1') + self.assertEqual('pm_job_1', result.id) + + @mock.patch.object(objects.base.TackerPersistentObject, 'get_by_id') + def test_get_pm_job_error(self, mock_pm): + mock_pm.return_value = None + self.assertRaises( + sol_ex.PMJobNotExist, + pm_job_utils.get_pm_job, context=context, pm_job_id='pm_job-1' + ) + + @mock.patch.object(objects.base.TackerPersistentObject, 'get_by_filter') + def test_get_pm_report(self, mock_pm): + mock_pm.return_value = [objects.PerformanceReportV2(id='report_1', + jobId='pm_job_1')] + + result = pm_job_utils.get_pm_report(context, 'pm_job_1', 'report_1') + self.assertEqual('pm_job_1', result.jobId) + + result = pm_job_utils.get_pm_report(context, 'pm_job_1') + self.assertEqual('pm_job_1', result[0].jobId) + + @mock.patch.object(objects.base.TackerPersistentObject, 'get_by_filter') + def test_get_pm_report_error(self, mock_pm): + mock_pm.return_value = None + self.assertRaises( + sol_ex.PMReportNotExist, + pm_job_utils.get_pm_report, context=context, + pm_job_id='pm_job_1', report_id='report_1' + ) + + def test_pm_job_href(self): + result = pm_job_utils.pm_job_href('pm_job_1', 'endpoint') + self.assertEqual('endpoint/vnfpm/v2/pm_jobs/pm_job_1', result) + + def test_pm_job_links(self): + pm_job = objects.PmJobV2(id='pm_job_1', objectInstanceIds=["id_1"]) + result = pm_job_utils.make_pm_job_links(pm_job, 'endpoint') + href = result.self.href + self.assertEqual('endpoint/vnfpm/v2/pm_jobs/pm_job_1', href) + + def test_get_notification_auth_handle(self): + pm_job = objects.PmJobV2(id='pm_job_1') + result = pm_job_utils._get_notification_auth_handle(pm_job) + res = type(result).__name__ + name = type(http_client.NoAuthHandle()).__name__ + self.assertEqual(name, res) + pm_job_1_auth = objects.SubscriptionAuthentication( + authType=["BASIC"], + paramsBasic=objects.SubscriptionAuthentication_ParamsBasic( + userName='test', + password='test', + ) + ) + pm_job_1 = objects.PmJobV2( + id='pm_job_1', + authentication=pm_job_1_auth) + result = pm_job_utils._get_notification_auth_handle(pm_job_1) + res = type(result).__name__ + name = type(http_client.BasicAuthHandle('test', 'test')).__name__ + self.assertEqual(name, res) + + pm_job_2 = objects.PmJobV2( + id='pm_job_2', + authentication=objects.SubscriptionAuthentication( + authType=["OAUTH2_CLIENT_CREDENTIALS"], + paramsOauth2ClientCredentials=( + objects.SubscriptionAuthentication_ParamsOauth2( + clientId='test', + clientPassword='test', + tokenEndpoint='http://127.0.0.1/token' + )) + ) + ) + result = pm_job_utils._get_notification_auth_handle(pm_job_2) + res = type(result).__name__ + name = type(http_client.OAuth2AuthHandle( + None, 'tokenEndpoint', 'test', 'test')).__name__ + self.assertEqual(name, res) + + pm_job_3 = objects.PmJobV2(id='pm_job_3', + authentication=( + objects.SubscriptionAuthentication( + authType=["TLS_CERT"], + )) + ) + result = pm_job_utils._get_notification_auth_handle(pm_job_3) + self.assertEqual(None, result) + + @mock.patch.object(http_client.HttpClient, 'do_request') + def test_test_notification(self, mock_do_request): + resp_no_auth = requests.Response() + resp_no_auth.status_code = 204 + mock_do_request.return_value = (resp_no_auth, None) + pm_job = objects.PmJobV2( + id='pm_job_1', + authentication=objects.SubscriptionAuthentication( + authType=["BASIC"], + paramsBasic=objects.SubscriptionAuthentication_ParamsBasic( + userName='test', + password='test' + ) + ), + callbackUri='http://127.0.0.1/callback' + ) + pm_job_utils.test_notification(pm_job) + + @mock.patch.object(http_client.HttpClient, 'do_request') + def test_test_notification_error_code(self, mock_do_request): + # execute not 204 + resp_no_auth = requests.Response() + resp_no_auth.status_code = 500 + mock_do_request.return_value = (resp_no_auth, None) + pm_job = objects.PmJobV2( + id='pm_job_1', + authentication=objects.SubscriptionAuthentication( + authType=["BASIC"], + paramsBasic=objects.SubscriptionAuthentication_ParamsBasic( + userName='test', + password='test' + ) + ), + callbackUri='http://127.0.0.1/callback' + ) + self.assertRaises(sol_ex.TestNotificationFailed, + pm_job_utils.test_notification, pm_job=pm_job) + + class mock_session(): + + def request(url, method, raise_exc=False, **kwargs): + resp = requests.Response() + resp.status_code = 400 + resp.headers['Content-Type'] = 'application/zip' + return resp + + @mock.patch.object(http_client.HttpClient, '_decode_body') + @mock.patch.object(http_client.BasicAuthHandle, 'get_session') + def test_test_notification_error(self, mock_session, mock_decode_body): + # execute not 204 + mock_session.return_value = self.mock_session + mock_decode_body.return_value = None + pm_job = objects.PmJobV2( + id='pm_job_1', + authentication=objects.SubscriptionAuthentication( + authType=["BASIC"], + paramsBasic=objects.SubscriptionAuthentication_ParamsBasic( + userName='test', + password='test' + ), + ), + callbackUri='http://127.0.0.1/callback' + ) + self.assertRaises(sol_ex.TestNotificationFailed, + pm_job_utils.test_notification, pm_job=pm_job) + + def test_make_pm_notif_data(self): + sub_instance_ids = ['1', '2', '3', '4'] + pm_job = objects.PmJobV2(id='pm_job_1', + objectType='VNF' + ) + result = pm_job_utils.make_pm_notif_data('instance_id', + sub_instance_ids, + 'report_id', + pm_job, + '2008-01-03 08:04:34', + 'endpoint') + self.assertEqual('instance_id', result.objectInstanceId) + + @mock.patch.object(http_client.HttpClient, 'do_request') + def test_send_notification(self, mock_resp): + pm_job = objects.PmJobV2(id='pm_job_1', + objectType='VNF', + callbackUri='http://127.0.0.1/callback' + ) + sub_instance_ids = ['1', '2', '3', '4'] + notif_data = pm_job_utils.make_pm_notif_data('instance_id', + sub_instance_ids, + 'report_id', + pm_job, + '2008-01-03 08:04:34', + 'endpoint') + resp_no_auth = requests.Response() + resp_no_auth.status_code = 204 + mock_resp.return_value = (resp_no_auth, None) + # execute no_auth + pm_job_utils.send_notification(pm_job, notif_data) + + pm_job = objects.PmJobV2( + id='pm_job_1', + objectType='VNF', + authentication=objects.SubscriptionAuthentication( + authType=["BASIC"], + paramsBasic=objects.SubscriptionAuthentication_ParamsBasic( + userName='test', + password='test' + ), + ), + callbackUri='http://127.0.0.1/callback' + ) + sub_instance_ids = ['1', '2', '3', '4'] + notif_data = pm_job_utils.make_pm_notif_data('instance_id', + sub_instance_ids, + 'report_id', + pm_job, + '2008-01-03 08:04:34', + 'endpoint') + resp_no_auth = requests.Response() + resp_no_auth.status_code = 204 + mock_resp.return_value = (resp_no_auth, None) + # execute basic_auth + pm_job_utils.send_notification(pm_job, notif_data) + + @mock.patch.object(http_client.HttpClient, 'do_request') + def test_send_notification_error(self, mock_resp): + pm_job = objects.PmJobV2( + id='pm_job_1', + objectType='VNF', + authentication=objects.SubscriptionAuthentication( + authType=["BASIC"], + paramsBasic=objects.SubscriptionAuthentication_ParamsBasic( + userName='test', + password='test' + ), + ), + callbackUri='http://127.0.0.1/callback' + ) + sub_instance_ids = ['1', '2', '3', '4'] + notif_data = pm_job_utils.make_pm_notif_data('instance_id', + sub_instance_ids, + 'report_id', + pm_job, + '2008-01-03 08:04:34', + 'endpoint') + resp_no_auth = requests.Response() + resp_no_auth.status_code = Exception() + mock_resp.return_value = (resp_no_auth, None) + # execute basic_auth + pm_job_utils.send_notification(pm_job, notif_data) diff --git a/tacker/tests/unit/sol_refactored/common/test_prometheus_plugin.py b/tacker/tests/unit/sol_refactored/common/test_prometheus_plugin.py new file mode 100644 index 000000000..80e9579f7 --- /dev/null +++ b/tacker/tests/unit/sol_refactored/common/test_prometheus_plugin.py @@ -0,0 +1,774 @@ +# Copyright (C) 2022 Fujitsu +# All Rights Reserved. +# +# 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. + +import copy +import datetime +import freezegun +import paramiko +import sys +import webob + +from tacker import context +from tacker.sol_refactored.common import exceptions as sol_ex +from tacker.sol_refactored.common import http_client +from tacker.sol_refactored.common import monitoring_plugin_base as mon_base +from tacker.sol_refactored.common import pm_job_utils +from tacker.sol_refactored.common import prometheus_plugin +from tacker.sol_refactored.common import vnf_instance_utils as inst_utils +from tacker.sol_refactored import objects +from tacker.tests.unit import base + +from unittest import mock + +_body_base = { + 'receiver': 'receiver', + 'status': 'firing', + 'alerts': [ + ], + 'groupLabels': {}, + 'commonLabels': { + 'alertname': 'NodeInstanceDown', + 'job': 'node' + }, + 'commonAnnotations': { + 'description': 'sample' + }, + 'externalURL': 'http://controller147:9093', + 'version': '4', + 'groupKey': '{}:{}', + 'truncatedAlerts': 0 +} + +_body_pm_alert1 = { + 'status': 'firing', + 'labels': { + 'receiver_type': 'tacker', + 'function_type': 'vnfpm', + 'job_id': '64e46b0e-887a-4691-8d2b-aa3d7b157e2c', + 'metric': 'VCpuUsageMeanVnf.' + '25b9b9d0-2461-4109-866e-a7767375415b', + 'object_instance_id': '25b9b9d0-2461-4109-866e-a7767375415b' + }, + 'annotations': { + 'value': 99, + }, + 'startsAt': '2022-06-21T23:47:36.453Z', + 'endsAt': '0001-01-01T00:00:00Z', + 'generatorURL': 'http://controller147:9090/graph?g0.expr=' + 'up%7Bjob%3D%22node%22%7D+%3D%3D+0&g0.tab=1', + 'fingerprint': '5ef77f1f8a3ecb8d' +} + +# function_type mismatch +_body_pm_alert2 = copy.deepcopy(_body_pm_alert1) +_body_pm_alert2['labels']['function_type'] = 'vnffm' + +# object_instance_id mismatch +_body_pm_alert3 = copy.deepcopy(_body_pm_alert1) +_body_pm_alert3['labels']['object_instance_id'] = 'obj_instance_mismatch' + +# object_instance_id mismatch +_body_pm_alert4 = copy.deepcopy(_body_pm_alert1) +_body_pm_alert4['labels']['sub_object_instance_id'] = 'sub_object_mismatch' + +_body_pm_alert5 = copy.deepcopy(_body_pm_alert1) +_body_pm_alert5['labels']['metric'] = 'ByteIncomingVnfIntCp' + +_body_pm_alert6 = copy.deepcopy(_body_pm_alert1) +_body_pm_alert6['labels']['metric'] = 'InvalidMetric' + +_body_pm1 = copy.copy(_body_base) +_body_pm1.update({ + 'alerts': [ + _body_pm_alert1, _body_pm_alert2, _body_pm_alert3, _body_pm_alert4] +}) + +_body_pm2 = copy.copy(_body_base) +_body_pm2.update({ + 'alerts': [_body_pm_alert5, _body_pm_alert6] +}) + +_pm_job = { + 'id': 'job_id', + 'objectType': 'Vnf', + 'objectInstanceIds': ['25b9b9d0-2461-4109-866e-a7767375415b'], + 'subObjectInstanceIds': [], + 'criteria': { + 'performanceMetric': [ + 'VcpuUsageMeanVnf.25b9b9d0-2461-4109-866e-a7767375415b' + ], + 'performanceMetricGroup': [ + 'VirtualizedComputeResource', + 'InvalidGroupName' + ], + 'collectionPeriod': 15, + 'reportingPeriod': 30, + 'reportingBoundary': '2022-06-23T04:56:00.910Z' + }, + 'callbackUri': '', + 'reports': [], + 'metadata': { + 'monitoring': { + 'monitorName': 'prometheus', + 'driverType': 'external', + 'targetsInfo': [ + { + 'prometheusHost': + 'prometheusHost', + 'prometheusHostPort': '22', + 'authInfo': { + 'ssh_username': 'ssh_username', + 'ssh_password': 'ssh_password' + }, + 'alertRuleConfigPath': + 'alertRuleConfigPath', + 'prometheusReloadApiEndpoint': + 'prometheusReloadApiEndpoint' + }, + { + # invalid access info + 'prometheusHost': + 'prometheusHost', + } + ] + } + } +} + +_pm_job2 = copy.deepcopy(_pm_job) +_pm_job2['objectType'] = 'VnfIntCp' +_pm_job2['criteria']['performanceMetric'] = ['ByteIncomingVnfIntCp'] +_pm_job2['criteria']['performanceMetricGroup'] = [ + 'VnfInternalCp', 'VnfExternalCp'] + +_pm_report = { + 'id': 'report_id', + 'jobId': 'pm_job_id', + 'entries': [{ + # objectType, InstanceId, Metric match the test + # condition. + 'objectType': 'Vnf', + 'objectInstanceId': + '25b9b9d0-2461-4109-866e-a7767375415b', + 'performanceMetric': + 'VCpuUsageMeanVnf.' + '25b9b9d0-2461-4109-866e-a7767375415b', + 'performanceValues': [{ + # current_time - 60sec + 'timeStamp': '2022-06-22T01:22:45.678Z', + 'value': 12.3 + }, { + # current_time - 30sec + 'timeStamp': '2022-06-22T01:23:15.678Z', + 'value': 45.6 + }] + }, { + # objectType, InstanceId, Metric do + # not match the test condition. + 'objectType': 'Vnf', + 'objectInstanceId': + '25b9b9d0-2461-4109-866e-a7767375415b', + 'subObjectInstanceId': + 'ebd40865-e3d9-4ac6-b7f0-0a8d2791d07f', + 'performanceMetric': + 'VCpuUsageMeanVnf.' + '25b9b9d0-2461-4109-866e-a7767375415b', + 'performanceValues': [{ + # current_time - 30sec + 'timeStamp': '2022-06-22T01:23:15.678Z', + 'value': 45.6 + }] + }, { + # objectType, InstanceId, Metric do + # not match the test condition. + 'objectType': 'Vnf', + 'objectInstanceId': + '25b9b9d0-2461-4109-866e-a7767375415b', + 'performanceMetric': + 'VMemoryUsageMeanVnf.' + '25b9b9d0-2461-4109-866e-a7767375415b', + 'performanceValues': [{ + # current_time - 5sec + 'timeStamp': '2022-06-22T01:23:40.678Z', + 'value': 78.9 + }] + }, { + # objectType, InstanceId, Metric do + # not match the test condition. + 'objectType': 'Vnf', + 'objectInstanceId': + 'test_id', + 'performanceMetric': + 'VCpuUsageMeanVnf.test_id', + 'performanceValues': [{ + # current_time + 5sec + 'timeStamp': '2022-06-22T01:23:50.678Z', + 'value': 0.1 + }] + }] +} + +_pm_report2 = { + 'id': 'report_id', + 'jobId': 'pm_job_id', + 'entries': [] +} + +_inst_base = { + 'id': '25b9b9d0-2461-4109-866e-a7767375415b', + 'vnfdId': 'vnfdId', + 'vnfProvider': 'vnfProvider', + 'vnfProductName': 'vnfProductName', + 'vnfSoftwareVersion': 'vnfSoftwareVersion', + 'vnfdVersion': 'vnfdVersion', + 'instantiationState': 'NOT_INSTANTIATED', +} + +_inst1 = copy.copy(_inst_base) +_inst1.update({ + 'instantiatedVnfInfo': { + 'id': 'id', + 'vduId': 'vduId', + 'vnfcResourceInfo': [{ + 'id': 'id2', + 'vduId': 'vduId2', + 'computeResource': { + 'vimLevelResourceType': 'Deployment', + 'resourceId': 'pod-pod1' + }, + 'metadata': { + 'hostname': 'node2', + } + }], + 'vnfcInfo': [{ + 'id': 'vnfc_info1', + 'vduId': 'vdu_id', + 'vnfcResourceInfoId': 'id2', + 'vnfcState': 'STARTED' + }] + } +}) + +datetime_test = datetime.datetime.fromisoformat( + '2022-06-22T01:23:45.678Z'.replace('Z', '+00:00')) + + +def unload_uuidsentinel(): + # Unload uuidsentinel module because it is conflict + # with the freezegun module. + if "tacker.tests.uuidsentinel" in sys.modules: + del sys.modules["tacker.tests.uuidsentinel"] + + +class _ParamikoTest(): + def __init__(self): + pass + + def connect(self, **kwargs): + pass + + def remove(self, arg1): + pass + + def put(self, a1, a2): + pass + + def __enter__(self): + return self + + def __exit__(self, exc_type, exc_value, traceback): + pass + + +class TestPrometheusPluginPm(base.TestCase): + def setUp(self): + super(TestPrometheusPluginPm, self).setUp() + objects.register_all() + self.context = context.get_admin_context() + self.request = mock.Mock() + self.request.context = self.context + prometheus_plugin.PrometheusPluginPm._instance = None + + def tearDown(self): + super(TestPrometheusPluginPm, self).tearDown() + # delete singleton object + prometheus_plugin.PrometheusPluginPm._instance = None + + def test_constructor_error(self): + self.config_fixture.config( + group='prometheus_plugin', auto_scaling=False) + mon_base.MonitoringPlugin.get_instance( + prometheus_plugin.PrometheusPluginPm) + self.assertRaises( + SystemError, + prometheus_plugin.PrometheusPluginPm) + + def test_constructor_stub(self): + self.config_fixture.config( + group='prometheus_plugin', auto_scaling=False) + pp = mon_base.MonitoringPlugin.get_instance( + prometheus_plugin.PrometheusPluginPm) + self.assertIsInstance(pp._instance, mon_base.MonitoringPluginStub) + pp = mon_base.MonitoringPlugin.get_instance( + prometheus_plugin.PrometheusPluginPm) + self.assertIsInstance(pp._instance, mon_base.MonitoringPluginStub) + + @mock.patch.object(pm_job_utils, 'get_pm_report') + @mock.patch.object(pm_job_utils, 'get_pm_job') + def test_pm(self, mock_pm_job, mock_pm_report): + self.config_fixture.config( + group='prometheus_plugin', performance_management=True) + mock_pm_job.return_value = objects.PmJobV2.from_dict(_pm_job) + mock_pm_report.return_value = objects.PerformanceReportV2.from_dict( + _pm_report) + pp = mon_base.MonitoringPlugin.get_instance( + prometheus_plugin.PrometheusPluginPm) + + unload_uuidsentinel() + with freezegun.freeze_time(datetime_test): + result = pp._alert(self.request, body=_body_pm1) + self.assertTrue(len(result) > 0) + self.assertEqual( + result[0]['objectInstanceId'], + '25b9b9d0-2461-4109-866e-a7767375415b') + + @mock.patch.object(pm_job_utils, 'get_pm_report') + @mock.patch.object(pm_job_utils, 'get_pm_job') + def test_pm_metrics(self, mock_pm_job, mock_pm_report): + self.config_fixture.config( + group='prometheus_plugin', performance_management=True) + mock_pm_job.return_value = objects.PmJobV2.from_dict(_pm_job) + mock_pm_report.return_value = objects.PerformanceReportV2.from_dict( + _pm_report) + pp = mon_base.MonitoringPlugin.get_instance( + prometheus_plugin.PrometheusPluginPm) + unload_uuidsentinel() + with freezegun.freeze_time(datetime_test): + self.assertRaises( + sol_ex.PrometheusPluginError, + pp._alert, self.request, body=_body_pm2 + ) + + @mock.patch.object(pm_job_utils, 'get_pm_report') + @mock.patch.object(pm_job_utils, 'get_pm_job') + def test_pm_report(self, mock_pm_job, mock_pm_report): + self.config_fixture.config( + group='prometheus_plugin', performance_management=True) + mock_pm_job.return_value = objects.PmJobV2.from_dict(_pm_job) + mock_pm_report.return_value = objects.PerformanceReportV2.from_dict( + _pm_report2) + pp = mon_base.MonitoringPlugin.get_instance( + prometheus_plugin.PrometheusPluginPm) + unload_uuidsentinel() + with freezegun.freeze_time(datetime_test): + result = pp._alert(self.request, body=_body_pm1) + self.assertTrue(len(result) > 0) + self.assertEqual( + result[0]['objectInstanceId'], + '25b9b9d0-2461-4109-866e-a7767375415b') + mock_pm_report.return_value = None + unload_uuidsentinel() + with freezegun.freeze_time(datetime_test): + result = pp._alert(self.request, body=_body_pm1) + self.assertTrue(len(result) > 0) + self.assertEqual( + result[0]['objectInstanceId'], + '25b9b9d0-2461-4109-866e-a7767375415b') + + @mock.patch.object(pm_job_utils, 'get_pm_report') + @mock.patch.object(pm_job_utils, 'get_pm_job') + def test_pm_datetime(self, mock_pm_job, mock_pm_report): + self.config_fixture.config( + group='prometheus_plugin', performance_management=True) + mock_pm_job.return_value = objects.PmJobV2.from_dict(_pm_job) + mock_pm_report.return_value = objects.PerformanceReportV2.from_dict( + _pm_report) + pp = mon_base.MonitoringPlugin.get_instance( + prometheus_plugin.PrometheusPluginPm) + unload_uuidsentinel() + # a time pm job is already expired. + datetime_now = datetime.datetime.fromisoformat( + '2023-06-23T04:56:00.910+00:00') + with freezegun.freeze_time(datetime_now): + result = pp._alert(self.request, body=_body_pm1) + self.assertTrue(len(result) == 0) + # now < latest reporting time + reportingPeriod + datetime_now = datetime.datetime.fromisoformat( + '2022-06-22T01:23:25.678+00:00') + with freezegun.freeze_time(datetime_now): + result = pp._alert(self.request, body=_body_pm1) + self.assertTrue(len(result) == 0) + + @mock.patch.object(pm_job_utils, 'get_pm_report') + @mock.patch.object(pm_job_utils, 'get_pm_job') + def test_pm_set_callback(self, mock_pm_job, mock_pm_report): + self.config_fixture.config( + group='prometheus_plugin', performance_management=True) + mock_pm_job.return_value = objects.PmJobV2.from_dict(_pm_job) + mock_pm_report.return_value = objects.PerformanceReportV2.from_dict( + _pm_report) + pp = mon_base.MonitoringPlugin.get_instance( + prometheus_plugin.PrometheusPluginPm) + pp.set_callback(None) + unload_uuidsentinel() + with freezegun.freeze_time(datetime_test): + result = pp._alert(self.request, body=_body_pm1) + self.assertTrue(len(result) > 0) + self.assertEqual( + result[0]['objectInstanceId'], + '25b9b9d0-2461-4109-866e-a7767375415b') + + def test_pm_error_access_info(self): + self.config_fixture.config( + group='prometheus_plugin', performance_management=True) + pp = mon_base.MonitoringPlugin.get_instance( + prometheus_plugin.PrometheusPluginPm) + job = copy.deepcopy(_pm_job) + del job['metadata'] + job = objects.PmJobV2.from_dict(job) + self.assertRaises( + sol_ex.PrometheusPluginError, + pp.delete_job, context=self.context, pm_job=job + ) + job2 = copy.deepcopy(_pm_job) + job2['metadata'] = {'monitoring': {}} + job2 = objects.PmJobV2.from_dict(job2) + self.assertRaises( + sol_ex.PrometheusPluginError, + pp.delete_job, context=self.context, pm_job=job2 + ) + + @mock.patch.object(http_client.HttpClient, 'do_request') + @mock.patch.object(paramiko.SFTPClient, 'from_transport') + @mock.patch.object(paramiko, 'Transport') + def test_delete_job(self, mock_paramiko, mock_sftp, mock_do_request): + mock_paramiko.return_value = _ParamikoTest() + mock_sftp.return_value = _ParamikoTest() + resp = webob.Response() + resp.status_code = 202 + mock_do_request.return_value = resp, {} + self.config_fixture.config( + group='prometheus_plugin', performance_management=True) + pp = mon_base.MonitoringPlugin.get_instance( + prometheus_plugin.PrometheusPluginPm) + # noromal + job = objects.PmJobV2.from_dict(_pm_job) + pp.delete_job(context=self.context, pm_job=job) + # error + resp.status_code = 503 + pp.delete_job(context=self.context, pm_job=job) + + @mock.patch.object(http_client.HttpClient, 'do_request') + @mock.patch.object(paramiko.SFTPClient, 'from_transport') + @mock.patch.object(paramiko, 'Transport') + @mock.patch.object(inst_utils, 'get_inst') + def test_create_job( + self, mock_inst, mock_paramiko, mock_sftp, mock_do_request): + mock_paramiko.return_value = _ParamikoTest() + mock_sftp.return_value = _ParamikoTest() + resp = webob.Response() + resp.status_code = 202 + mock_do_request.return_value = resp, {} + mock_inst.return_value = objects.VnfInstanceV2.from_dict(_inst1) + + self.config_fixture.config( + group='prometheus_plugin', performance_management=True) + pp = mon_base.MonitoringPlugin.get_instance( + prometheus_plugin.PrometheusPluginPm) + # VirtualizedComputeResource + job = objects.PmJobV2.from_dict(_pm_job) + rule = pp.create_job(context=self.context, pm_job=job) + self.assertTrue(len(rule['groups'][0]['rules']) > 0) + # VnfInternalCp + job = objects.PmJobV2.from_dict(_pm_job2) + rule = pp.create_job(context=self.context, pm_job=job) + self.assertTrue(len(rule['groups'][0]['rules']) > 0) + self.assertTrue('interface="*"' in str(rule)) + + @mock.patch.object(http_client.HttpClient, 'do_request') + @mock.patch.object(paramiko.SFTPClient, 'from_transport') + @mock.patch.object(paramiko, 'Transport') + @mock.patch.object(inst_utils, 'get_inst') + def test_create_job_subobj( + self, mock_inst, mock_paramiko, mock_sftp, mock_do_request): + mock_paramiko.return_value = _ParamikoTest() + mock_sftp.return_value = _ParamikoTest() + resp = webob.Response() + resp.status_code = 202 + mock_do_request.return_value = resp, {} + inst = objects.VnfInstanceV2.from_dict(_inst1) + mock_inst.return_value = inst + + self.config_fixture.config( + group='prometheus_plugin', performance_management=True) + pp = mon_base.MonitoringPlugin.get_instance( + prometheus_plugin.PrometheusPluginPm) + # VirtualizedComputeResource + job = copy.deepcopy(_pm_job) + job['subObjectInstanceIds'] = ['vnfc_info1'] + job = objects.PmJobV2.from_dict(job) + rule = pp.create_job(context=self.context, pm_job=job) + self.assertTrue(len(rule['groups'][0]['rules']) > 0) + self.assertEqual( + rule['groups'][0]['rules'][0]['labels']['sub_object_instance_id'], + job['subObjectInstanceIds'][0]) + # VnfInternalCp + job = copy.deepcopy(_pm_job2) + job['subObjectInstanceIds'] = ['test_if0'] + job = objects.PmJobV2.from_dict(job) + rule = pp.create_job(context=self.context, pm_job=job) + self.assertTrue(len(rule['groups'][0]['rules']) > 0) + self.assertTrue('interface="test_if0"' in str(rule)) + + @mock.patch.object(http_client.HttpClient, 'do_request') + @mock.patch.object(paramiko.SFTPClient, 'from_transport') + @mock.patch.object(paramiko, 'Transport') + @mock.patch.object(inst_utils, 'get_inst') + def test_create_job_error( + self, mock_inst, mock_paramiko, mock_sftp, mock_do_request): + mock_paramiko.return_value = _ParamikoTest() + mock_sftp.return_value = _ParamikoTest() + resp = webob.Response() + resp.status_code = 202 + mock_do_request.return_value = resp, {} + mock_inst.return_value = objects.VnfInstanceV2.from_dict(_inst1) + + self.config_fixture.config( + group='prometheus_plugin', performance_management=True) + pp = mon_base.MonitoringPlugin.get_instance( + prometheus_plugin.PrometheusPluginPm) + + # invalid object type + job = copy.deepcopy(_pm_job) + job['objectType'] = 'invalid_type' + job = objects.PmJobV2.from_dict(job) + self.assertRaises( + sol_ex.PrometheusPluginError, + pp.create_job, context=self.context, pm_job=job + ) + # invalid performanceMetric or performanceMetricGroup. + job = copy.deepcopy(_pm_job) + job['criteria']['performanceMetric'] = [] + job['criteria']['performanceMetricGroup'] = [] + job = objects.PmJobV2.from_dict(job) + self.assertRaises( + sol_ex.PrometheusPluginError, + pp.create_job, context=self.context, pm_job=job + ) + # Invalid performanceMetric or performanceMetricGroup. + job = copy.deepcopy(_pm_job2) + job['criteria']['performanceMetric'] = [] + job['criteria']['performanceMetricGroup'] = ['VnfExternalCp'] + job = objects.PmJobV2.from_dict(job) + self.assertRaises( + sol_ex.PrometheusPluginError, + pp.create_job, context=self.context, pm_job=job + ) + # no instantiatedVnfInfo + mock_inst.return_value = objects.VnfInstanceV2.from_dict(_inst_base) + job = objects.PmJobV2.from_dict(_pm_job) + self.assertRaises( + sol_ex.PrometheusPluginError, + pp.create_job, context=self.context, pm_job=job + ) + # no instantiatedVnfInfo with subObjectInstanceIds + job = copy.deepcopy(_pm_job2) + job['subObjectInstanceIds'] = ['test_if0'] + job = objects.PmJobV2.from_dict(job) + self.assertRaises( + sol_ex.PrometheusPluginError, + pp.create_job, context=self.context, pm_job=job + ) + # no valid computeResource + ins = copy.deepcopy(_inst1) + _ = ins['instantiatedVnfInfo']['vnfcResourceInfo'][0] + _['computeResource'] = {} + mock_inst.return_value = objects.VnfInstanceV2.from_dict(ins) + job = objects.PmJobV2.from_dict(_pm_job) + self.assertRaises( + sol_ex.PrometheusPluginError, + pp.create_job, context=self.context, pm_job=job + ) + + # no vnfcInfo + ins = copy.deepcopy(_inst1) + del ins['instantiatedVnfInfo']['vnfcInfo'] + mock_inst.return_value = objects.VnfInstanceV2.from_dict(ins) + job = copy.deepcopy(_pm_job) + job['subObjectInstanceIds'] = ['vnfc_info1'] + job = objects.PmJobV2.from_dict(job) + self.assertRaises( + sol_ex.PrometheusPluginError, + pp.create_job, context=self.context, pm_job=job + ) + + # vnfcInfo mismatch + ins = copy.deepcopy(_inst1) + ins['instantiatedVnfInfo']['vnfcInfo'][0]['vnfcResourceInfoId'] = 'ng' + mock_inst.return_value = objects.VnfInstanceV2.from_dict(ins) + job = copy.deepcopy(_pm_job) + job['subObjectInstanceIds'] = ['vnfc_info1'] + job = objects.PmJobV2.from_dict(job) + self.assertRaises( + sol_ex.PrometheusPluginError, + pp.create_job, context=self.context, pm_job=job + ) + + # vnfcInfo mismatch + ins = copy.deepcopy(_inst1) + del ins['instantiatedVnfInfo']['vnfcInfo'][0]['vnfcResourceInfoId'] + mock_inst.return_value = objects.VnfInstanceV2.from_dict(ins) + job = copy.deepcopy(_pm_job) + job['subObjectInstanceIds'] = ['vnfc_info1'] + job = objects.PmJobV2.from_dict(job) + self.assertRaises( + sol_ex.PrometheusPluginError, + pp.create_job, context=self.context, pm_job=job + ) + + # resourcename mismatch: VirtualizedComputeResource + ins = copy.deepcopy(_inst1) + _ = ins['instantiatedVnfInfo']['vnfcResourceInfo'] + del _[0]['computeResource']['resourceId'] + mock_inst.return_value = objects.VnfInstanceV2.from_dict(ins) + job = copy.deepcopy(_pm_job) + job['subObjectInstanceIds'] = ['vnfc_info1'] + job = objects.PmJobV2.from_dict(job) + self.assertRaises( + sol_ex.PrometheusPluginError, + pp.create_job, context=self.context, pm_job=job + ) + # resourcename mismatch: VirtualizedComputeResource + ins = copy.deepcopy(_inst1) + _ = ins['instantiatedVnfInfo']['vnfcResourceInfo'][0] + _['computeResource']['resourceId'] = 'test-xxx1-756757f8f-xcwmt' + mock_inst.return_value = objects.VnfInstanceV2.from_dict(ins) + job = copy.deepcopy(_pm_job) + job['subObjectInstanceIds'] = ['vnfc_info1'] + job = objects.PmJobV2.from_dict(job) + self.assertRaises( + sol_ex.PrometheusPluginError, + pp.create_job, context=self.context, pm_job=job + ) + + ins = copy.deepcopy(_inst1) + _ = ins['instantiatedVnfInfo']['vnfcResourceInfo'][0] + _['computeResource']['vimLevelResourceType'] = 'ng' + mock_inst.return_value = objects.VnfInstanceV2.from_dict(ins) + job = copy.deepcopy(_pm_job2) + job['subObjectInstanceIds'] = ['test_if0'] + job = objects.PmJobV2.from_dict(job) + self.assertRaises( + sol_ex.PrometheusPluginError, + pp.create_job, context=self.context, pm_job=job + ) + + +class TestPrometheusPluginFm(base.TestCase): + def setUp(self): + super(TestPrometheusPluginFm, self).setUp() + objects.register_all() + self.context = context.get_admin_context() + self.request = mock.Mock() + self.request.context = self.context + prometheus_plugin.PrometheusPluginFm._instance = None + + def tearDown(self): + super(TestPrometheusPluginFm, self).tearDown() + # delete singleton object + prometheus_plugin.PrometheusPluginFm._instance = None + + def test_constructor_error(self): + self.config_fixture.config( + group='prometheus_plugin', auto_scaling=False) + mon_base.MonitoringPlugin.get_instance( + prometheus_plugin.PrometheusPluginFm) + self.assertRaises( + SystemError, + prometheus_plugin.PrometheusPluginFm) + + def test_constructor_stub(self): + self.config_fixture.config( + group='prometheus_plugin', auto_scaling=False) + pp = mon_base.MonitoringPlugin.get_instance( + prometheus_plugin.PrometheusPluginFm) + self.assertIsInstance(pp._instance, mon_base.MonitoringPluginStub) + pp = mon_base.MonitoringPlugin.get_instance( + prometheus_plugin.PrometheusPluginFm) + self.assertIsInstance(pp._instance, mon_base.MonitoringPluginStub) + + def test_pm_no_body(self): + self.config_fixture.config( + group='prometheus_plugin', performance_management=True) + pp = mon_base.MonitoringPlugin.get_instance( + prometheus_plugin.PrometheusPluginPm) + self.assertRaises( + sol_ex.PrometheusPluginValidationError, + pp._alert, self.request) + + +class TestPrometheusPluginAutoScaling(base.TestCase): + def setUp(self): + super(TestPrometheusPluginAutoScaling, self).setUp() + objects.register_all() + self.context = context.get_admin_context() + self.request = mock.Mock() + self.request.context = self.context + prometheus_plugin.PrometheusPluginAutoScaling._instance = None + + def tearDown(self): + super(TestPrometheusPluginAutoScaling, self).tearDown() + # delete singleton object + prometheus_plugin.PrometheusPluginAutoScaling._instance = None + + def test_constructor_error(self): + self.config_fixture.config( + group='prometheus_plugin', auto_scaling=False) + mon_base.MonitoringPlugin.get_instance( + prometheus_plugin.PrometheusPluginAutoScaling) + self.assertRaises( + SystemError, + prometheus_plugin.PrometheusPluginAutoScaling) + + def test_constructor_stub(self): + self.config_fixture.config( + group='prometheus_plugin', auto_scaling=False) + pp = mon_base.MonitoringPlugin.get_instance( + prometheus_plugin.PrometheusPluginAutoScaling) + self.assertIsInstance(pp._instance, mon_base.MonitoringPluginStub) + pp = mon_base.MonitoringPlugin.get_instance( + prometheus_plugin.PrometheusPluginAutoScaling) + self.assertIsInstance(pp._instance, mon_base.MonitoringPluginStub) + + def test_monitoring_plugin(self): + mon = mon_base.MonitoringPlugin.get_instance( + mon_base.MonitoringPluginStub) + mon.set_callback(None) + mon.create_job() + mon.delete_job() + mon.alert() + + def test_monitoring_plugin_stub(self): + mon_base.MonitoringPlugin.get_instance( + mon_base.MonitoringPluginStub) + mon = mon_base.MonitoringPlugin.get_instance( + mon_base.MonitoringPluginStub) + mon.set_callback(None) + mon.create_job() + mon.delete_job() + mon.alert() + self.assertRaises( + SystemError, + mon_base.MonitoringPluginStub) diff --git a/tacker/tests/unit/sol_refactored/common/test_subscription_utils.py b/tacker/tests/unit/sol_refactored/common/test_subscription_utils.py index 7db76f3ae..79c0647c9 100644 --- a/tacker/tests/unit/sol_refactored/common/test_subscription_utils.py +++ b/tacker/tests/unit/sol_refactored/common/test_subscription_utils.py @@ -147,15 +147,23 @@ class TestSubscriptionUtils(base.BaseTestCase): self.assertRaises(sol_ex.TestNotificationFailed, subsc_utils.test_notification, subsc_no_auth) - @mock.patch.object(http_client.HttpClient, 'do_request') - def test_test_notification_error(self, mock_resp): + class mock_session(): + + def request(url, method, raise_exc=False, **kwargs): + resp = requests.Response() + resp.status_code = 400 + resp.headers['Content-Type'] = 'application/zip' + return resp + + @mock.patch.object(http_client.HttpClient, '_decode_body') + @mock.patch.object(http_client.NoAuthHandle, 'get_session') + def test_test_notification_error(self, mock_session, mock_decode_body): subsc_no_auth = objects.LccnSubscriptionV2( id='sub-1', verbosity='SHORT', callbackUri='http://127.0.0.1/callback') - resp_no_auth = Exception() - mock_resp.return_value = (resp_no_auth, None) + mock_session.return_value = self.mock_session + mock_decode_body.return_value = None - # execute no_auth self.assertRaises(sol_ex.TestNotificationFailed, subsc_utils.test_notification, subsc_no_auth) diff --git a/tacker/tests/unit/sol_refactored/conductor/test_prometheus_plugin.py b/tacker/tests/unit/sol_refactored/conductor/test_prometheus_plugin.py new file mode 100644 index 000000000..942674a32 --- /dev/null +++ b/tacker/tests/unit/sol_refactored/conductor/test_prometheus_plugin.py @@ -0,0 +1,162 @@ +# Copyright (C) 2022 Fujitsu +# All Rights Reserved. +# +# 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. + +import webob + +from tacker import context +from tacker.sol_refactored.common import http_client +from tacker.sol_refactored.conductor import conductor_v2 +from tacker.sol_refactored.conductor import prometheus_plugin_driver as pp_drv +from tacker.sol_refactored import objects +from tacker.tests.unit.db import base as db_base + +from unittest import mock + + +_req1 = { + 'flavourId': 'flavour' +} + +_req2 = { + 'flavourId': 'flavour', + 'additionalParams': {}, +} + +_req3 = { + 'flavourId': 'flavour', + 'additionalParams': { + 'monitoring': { + 'monitorName': 'prometheus', + 'driverType': 'external', + 'targetsInfo': [ + { + 'prometheusHost': + 'prometheusHost', + 'prometheusHostPort': '22', + 'authInfo': { + 'ssh_username': 'ssh_username', + 'ssh_password': 'ssh_password' + }, + 'alertRuleConfigPath': + 'alertRuleConfigPath', + 'prometheusReloadApiEndpoint': + 'prometheusReloadApiEndpoint' + } + ] + } + }, +} + +_inst1 = { + 'id': '25b9b9d0-2461-4109-866e-a7767375415b', + 'vnfdId': 'vnfdId', + 'vnfProvider': 'vnfProvider', + 'vnfProductName': 'vnfProductName', + 'vnfSoftwareVersion': 'vnfSoftwareVersion', + 'vnfdVersion': 'vnfdVersion', + 'instantiationState': 'NOT_INSTANTIATED', +} + +_inst2 = { + 'id': '25b9b9d0-2461-4109-866e-a7767375415b', + 'vnfdId': 'vnfdId', + 'vnfProvider': 'vnfProvider', + 'vnfProductName': 'vnfProductName', + 'vnfSoftwareVersion': 'vnfSoftwareVersion', + 'vnfdVersion': 'vnfdVersion', + 'instantiationState': 'NOT_INSTANTIATED', + 'metadata': {} +} + +_inst3 = { + 'id': '25b9b9d0-2461-4109-866e-a7767375415b', + 'vnfdId': 'vnfdId', + 'vnfProvider': 'vnfProvider', + 'vnfProductName': 'vnfProductName', + 'vnfSoftwareVersion': 'vnfSoftwareVersion', + 'vnfdVersion': 'vnfdVersion', + 'instantiationState': 'NOT_INSTANTIATED', + 'metadata': { + 'monitoring': { + 'monitorName': 'prometheus', + 'driverType': 'external', + 'targetsInfo': [ + { + 'prometheusHost': + 'prometheusHost', + 'prometheusHostPort': '22', + 'authInfo': { + 'ssh_username': 'ssh_username', + 'ssh_password': 'ssh_password' + }, + 'alertRuleConfigPath': + 'alertRuleConfigPath', + 'prometheusReloadApiEndpoint': + 'prometheusReloadApiEndpoint' + } + ] + } + } +} + + +class TestPrometheusPlugin(db_base.SqlTestCase): + def setUp(self): + super(TestPrometheusPlugin, self).setUp() + objects.register_all() + self.context = context.get_admin_context() + self.request = mock.Mock() + self.request.context = self.context + self.config_fixture.config( + group='prometheus_plugin', performance_management=True) + self.conductor = conductor_v2.ConductorV2() + + @mock.patch.object(http_client.HttpClient, 'do_request') + def test_requst_scale(self, mock_do_request): + resp = webob.Response() + resp.status_code = 202 + mock_do_request.return_value = resp, {} + scale_req = { + 'type': 'SCALE_OUT', + 'aspect_id': 'vdu', + } + self.conductor.request_scale( + self.context, 'vnf_instance_id', scale_req) + + def test_constructor(self): + self.config_fixture.config( + group='prometheus_plugin', performance_management=True) + pp_drv.PrometheusPluginDriver._instance = None + + def test_driver_stub(self): + self.config_fixture.config( + group='prometheus_plugin', performance_management=False) + pp_drv.PrometheusPluginDriver._instance = None + drv = pp_drv.PrometheusPluginDriver.instance() + drv = pp_drv.PrometheusPluginDriver.instance() + drv.request_scale(None, None, None) + self.config_fixture.config( + group='prometheus_plugin', performance_management=True) + pp_drv.PrometheusPluginDriver._instance = None + drv = pp_drv.PrometheusPluginDriver.instance() + + def test_driver_constructor(self): + self.config_fixture.config( + group='prometheus_plugin', performance_management=True) + pp_drv.PrometheusPluginDriver._instance = None + pp_drv.PrometheusPluginDriver.instance() + self.assertRaises( + SystemError, + pp_drv.PrometheusPluginDriver) diff --git a/tacker/tests/unit/sol_refactored/conductor/test_vnffm_driver_v1.py b/tacker/tests/unit/sol_refactored/conductor/test_vnffm_driver_v1.py new file mode 100644 index 000000000..bd205097f --- /dev/null +++ b/tacker/tests/unit/sol_refactored/conductor/test_vnffm_driver_v1.py @@ -0,0 +1,69 @@ +# Copyright (C) 2022 Fujitsu +# All Rights Reserved. +# +# 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. + +from unittest import mock + +from oslo_utils import uuidutils + +from tacker import context +from tacker.sol_refactored.common import fm_subscription_utils +from tacker.sol_refactored.common import vnf_instance_utils as inst_utils +from tacker.sol_refactored.conductor import vnffm_driver_v1 +from tacker.sol_refactored import objects +from tacker.tests import base +from tacker.tests.unit.sol_refactored.samples import fakes_for_fm + + +class TestVnffmDriverV1(base.BaseTestCase): + + def setUp(self): + super(TestVnffmDriverV1, self).setUp() + objects.register_all() + self.driver = vnffm_driver_v1.VnfFmDriverV1() + self.context = context.get_admin_context() + + @mock.patch.object(fm_subscription_utils, 'send_notification') + @mock.patch.object(fm_subscription_utils, 'get_alarm_subscs') + @mock.patch.object(objects.base.TackerPersistentObject, 'create') + @mock.patch.object(objects.base.TackerPersistentObject, 'update') + @mock.patch.object(inst_utils, 'get_inst') + @mock.patch.object(objects.base.TackerPersistentObject, 'get_by_id') + def test_store_alarm_info(self, mock_alarm, mock_inst, mock_update, + mock_create, mock_subscs, mock_send_notif): + alarm = objects.AlarmV1.from_dict(fakes_for_fm.alarm_example) + mock_alarm.return_value = objects.AlarmV1.from_dict( + fakes_for_fm.alarm_example) + mock_inst.return_value = objects.VnfInstanceV2( + # required fields + id=fakes_for_fm.alarm_example['managedObjectId'], + vnfdId=uuidutils.generate_uuid(), + vnfProvider='provider', + vnfProductName='product name', + vnfSoftwareVersion='software version', + vnfdVersion='vnfd version', + instantiationState='INSTANTIATED' + ) + mock_subscs.return_value = [objects.FmSubscriptionV1.from_dict( + fakes_for_fm.fm_subsc_example)] + self.driver.store_alarm_info(self.context, alarm) + self.assertEqual(0, mock_create.call_count) + self.assertEqual(1, mock_update.call_count) + self.assertEqual(1, mock_send_notif.call_count) + + mock_alarm.return_value = None + self.driver.store_alarm_info(self.context, alarm) + self.assertEqual(1, mock_create.call_count) + self.assertEqual(1, mock_update.call_count) + self.assertEqual(2, mock_send_notif.call_count) diff --git a/tacker/tests/unit/sol_refactored/conductor/test_vnfpm_driver_v2.py b/tacker/tests/unit/sol_refactored/conductor/test_vnfpm_driver_v2.py new file mode 100644 index 000000000..eb896027e --- /dev/null +++ b/tacker/tests/unit/sol_refactored/conductor/test_vnfpm_driver_v2.py @@ -0,0 +1,113 @@ +# Copyright (C) 2022 FUJITSU +# All Rights Reserved. +# +# 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. + +from unittest import mock + +from tacker.tests import base + +from tacker import context +from tacker.sol_refactored.common import pm_job_utils +from tacker.sol_refactored.conductor.vnfpm_driver_v2 import VnfPmDriverV2 +from tacker.sol_refactored.nfvo.nfvo_client import NfvoClient +from tacker.sol_refactored import objects + + +class TestVnfPmDriverV2(base.BaseTestCase): + + def setUp(self): + super(TestVnfPmDriverV2, self).setUp() + objects.register_all() + self.context = context.get_admin_context() + + @mock.patch.object(NfvoClient, 'send_pm_job_notification') + @mock.patch.object(pm_job_utils, 'update_report') + @mock.patch.object(objects.base.TackerPersistentObject, 'update') + @mock.patch.object(objects.base.TackerPersistentObject, 'create') + def test_store_job_info(self, mock_create, mock_update, mock_update_report, + mock_send): + mock_create.return_value = None + mock_update.return_value = None + pm_job = objects.PmJobV2( + id='pm_job_1', + objectTtype='VNF', + authentication=objects.SubscriptionAuthentication( + authType=["BASIC"], + paramsBasic=objects.SubscriptionAuthentication_ParamsBasic( + userName='test', + password='test' + ), + ), + callbackUri='http://127.0.0.1/callback' + ) + report = { + "id": "fake_id", + "jobId": "fake_job_id", + "entries": [{ + "objectType": "VNF", + "objectInstanceId": "instance_id_1", + "subObjectInstanceId": "subObjectInstanceId_1", + 'performanceValues': [{ + 'timeStamp': "2022-06-21T23:47:36.453Z", + 'value': "99.0" + }] + }] + } + + mock_update_report.return_value = pm_job + mock_send.return_value = None + VnfPmDriverV2().store_job_info(context=self.context, + report=report) + + @mock.patch.object(objects.base.TackerPersistentObject, 'create') + def test_store_report(self, mock_create): + mock_create.return_value = None + report = { + "id": "fake_id", + "jobId": "fake_job_id", + "entries": [{ + "objectType": "VNF", + "objectInstanceId": "instance_id_1", + "subObjectInstanceId": "subObjectInstanceId_1", + 'performanceValues': [{ + 'timeStamp': "2022-06-21T23:47:36.453Z", + 'value': "99.0" + }] + }] + } + result = VnfPmDriverV2()._store_report(context=self.context, + report=report) + self.assertEqual('fake_job_id', result.jobId) + + @mock.patch.object(objects.base.TackerPersistentObject, 'update') + @mock.patch.object(pm_job_utils, 'update_report') + def test_update_job_reports(self, mock_update_report, mock_update): + pm_job = objects.PmJobV2( + id='pm_job_1', + objectTtype='VNF', + authentication=objects.SubscriptionAuthentication( + authType=["BASIC"], + paramsBasic=objects.SubscriptionAuthentication_ParamsBasic( + userName='test', + password='test' + ), + ), + callbackUri='http://127.0.0.1/callback' + ) + mock_update_report.return_value = pm_job + mock_update.return_value = None + result = VnfPmDriverV2()._update_job_reports( + context=self.context, job_id='pm_job_1', report='report', + timestamp='timestamp') + self.assertEqual('pm_job_1', result.id) diff --git a/tacker/tests/unit/sol_refactored/controller/test_prometheus_plugin.py b/tacker/tests/unit/sol_refactored/controller/test_prometheus_plugin.py new file mode 100644 index 000000000..555000c58 --- /dev/null +++ b/tacker/tests/unit/sol_refactored/controller/test_prometheus_plugin.py @@ -0,0 +1,380 @@ +# Copyright (C) 2022 Fujitsu +# All Rights Reserved. +# +# 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. + +import copy +import datetime +from unittest import mock + +from tacker import context +from tacker.sol_refactored.common import exceptions as sol_ex +from tacker.sol_refactored.common import fm_alarm_utils +from tacker.sol_refactored.common import prometheus_plugin as plugin +from tacker.sol_refactored.common import vnf_instance_utils as inst_utils +from tacker.sol_refactored.controller import prometheus_plugin_controller +from tacker.sol_refactored import objects +from tacker.tests.unit import base + + +_body_base = { + 'receiver': 'receiver', + 'status': 'firing', + 'alerts': [ + ], + 'groupLabels': {}, + 'commonLabels': { + 'alertname': 'NodeInstanceDown', + 'job': 'node' + }, + 'commonAnnotations': { + 'description': 'sample' + }, + 'externalURL': 'http://controller147:9093', + 'version': '4', + 'groupKey': '{}:{}', + 'truncatedAlerts': 0 +} + +_body_fm_alert1 = { + 'status': 'firing', + 'labels': { + 'receiver_type': 'tacker', + 'function_type': 'vnffm', + 'vnf_instance_id': 'vnf_instance_id', + 'pod': r'test\-test1\-[0-9a-f]{1,10}-[0-9a-z]{5}$', + 'perceived_severity': 'CRITICAL', + 'event_type': 'PROCESSING_ERROR_ALARM' + }, + 'annotations': { + 'probable_cause': '', + 'fault_type': '', + 'fault_details': '' + }, + 'startsAt': '2022-06-21T23:47:36.453Z', + 'endsAt': '0001-01-01T00:00:00Z', + 'generatorURL': 'http://controller147:9090/graph?g0.expr=' + 'up%7Bjob%3D%22node%22%7D+%3D%3D+0&g0.tab=1', + 'fingerprint': '5ef77f1f8a3ecb8d' +} + +# function_type mismatch +_body_fm_alert2 = copy.deepcopy(_body_fm_alert1) +_body_fm_alert2['labels']['function_type'] = 'vnfpm' + +# status resolved +_body_fm_alert3 = copy.deepcopy(_body_fm_alert1) +_body_fm_alert3['status'] = 'resolved' + +# pod mismatch +_body_fm_alert4 = copy.deepcopy(_body_fm_alert1) +_body_fm_alert4['labels']['pod'] = 'mismatch_node' + +# pod does not exist +_body_fm_alert5 = copy.deepcopy(_body_fm_alert1) +del _body_fm_alert5['labels']['pod'] + +_body_fm1 = copy.copy(_body_base) +_body_fm1.update({ + 'alerts': [_body_fm_alert1, _body_fm_alert2] +}) + +_body_fm2 = copy.copy(_body_base) +_body_fm2.update({ + 'alerts': [_body_fm_alert3] +}) + +_body_fm3 = copy.copy(_body_base) +_body_fm3.update({ + 'alerts': [_body_fm_alert4] +}) + +_body_fm4 = copy.copy(_body_base) +_body_fm4.update({ + 'alerts': [_body_fm_alert5] +}) + +_not_cleared_alarms = { + 'id': 'id', + 'managedObjectId': 'managedObjectId', + 'rootCauseFaultyResource': { + 'faultyResource': { + 'resourceId': 'resourceId', + 'vimConnectionId': 'vimConnectionId', + 'vimLevelResourceType': 'vimLevelResourceType' + }, + 'faultyResourceType': 'COMPUTE' + }, + 'faultDetails': [ + 'fingerprint: 5ef77f1f8a3ecb8d' + ], + 'alarmRaisedTime': '2022-06-23T04:56:00.910Z', + 'ackState': 'UNACKNOWLEDGED', + 'perceivedSeverity': 'WARNING', + 'eventTime': '2022-06-23T04:56:00.910Z', + 'eventType': 'PROCESSING_ERROR_ALARM', + 'probableCause': 'problemCause', + 'isRootCause': False +} + +_body_scale_alert1 = { + 'status': 'firing', + 'labels': { + 'receiver_type': 'tacker', + 'function_type': 'auto_scale', + 'vnf_instance_id': 'vnf instance id', + 'auto_scale_type': 'SCALE_OUT', + 'aspect_id': 'aspect' + }, + 'annotations': { + }, + 'startsAt': '2022-06-21T23:47:36.453Z', + 'endsAt': '0001-01-01T00:00:00Z', + 'generatorURL': 'http://controller147:9090/graph?g0.expr=' + 'up%7Bjob%3D%22node%22%7D+%3D%3D+0&g0.tab=1', + 'fingerprint': '5ef77f1f8a3ecb8d' +} + +# fuction_type mismatch +_body_scale_alert2 = copy.deepcopy(_body_scale_alert1) +_body_scale_alert2['labels']['function_type'] = 'vnffm' + +_body_scale = copy.deepcopy(_body_base) +_body_scale.update({ + 'alerts': [_body_scale_alert1, _body_scale_alert2] +}) + +_inst1 = { + 'id': 'test_id', + 'vnfdId': 'vnfdId', + 'vnfProvider': 'vnfProvider', + 'vnfProductName': 'vnfProductName', + 'vnfSoftwareVersion': 'vnfSoftwareVersion', + 'vnfdVersion': 'vnfdVersion', + 'instantiationState': 'NOT_INSTANTIATED', + 'instantiatedVnfInfo': { + 'id': 'id', + 'vduId': 'vduId', + 'vnfcResourceInfo': [ + { + 'id': 'id', + 'vduId': 'vduId', + 'computeResource': {}, + 'metadata': { + 'hostname': 'node1', + } + }, { + 'id': 'id2', + 'vduId': 'vduId2', + 'computeResource': { + 'vimLevelResourceType': 'Deployment', + 'resourceId': 'test-test1-756757f8f-xcwmt' + } + } + ], + 'vnfcInfo': [{ + 'id': 'vnfc_info1', + 'vduId': 'vdu_id', + 'vnfcResourceInfoId': 'id2', + 'vnfcState': 'STARTED' + }] + }, + 'metadata': { + } +} + +_inst2 = copy.deepcopy(_inst1) +_inst2.update({ + 'vnfConfigurableProperties': { + 'isAutoscaleEnabled': True + }, +}) + +datetime_test = datetime.datetime.fromisoformat( + '2022-06-22T01:23:45.678Z'.replace('Z', '+00:00')) + + +class TestPrometheusPluginPm(base.TestCase): + def setUp(self): + super(TestPrometheusPluginPm, self).setUp() + objects.register_all() + self.context = context.get_admin_context() + self.request = mock.Mock() + self.request.context = self.context + self.controller = prometheus_plugin_controller.PmEventController() + plugin.PrometheusPluginPm._instance = None + + def tearDown(self): + super(TestPrometheusPluginPm, self).tearDown() + # delete singleton object + plugin.PrometheusPluginPm._instance = None + + def test_pm_event_config_false(self): + self.config_fixture.config( + group='prometheus_plugin', performance_management=False) + self.assertRaises( + sol_ex.PrometheusPluginNotEnabled, + self.controller.pm_event, self.request, {}) + + def test_pm_exception(self): + self.config_fixture.config( + group='prometheus_plugin', performance_management=True) + result = self.controller.pm_event(self.request, {}) + self.assertEqual(204, result.status) + + +class TestPrometheusPluginFm(base.TestCase): + def setUp(self): + super(TestPrometheusPluginFm, self).setUp() + objects.register_all() + self.context = context.get_admin_context() + self.request = mock.Mock() + self.request.context = self.context + self.controller = prometheus_plugin_controller.FmAlertController() + plugin.PrometheusPluginFm._instance = None + + def tearDown(self): + super(TestPrometheusPluginFm, self).tearDown() + # delete singleton object + plugin.PrometheusPluginFm._instance = None + + def test_fm_config_false(self): + self.config_fixture.config( + group='prometheus_plugin', fault_management=False) + self.assertRaises( + sol_ex.PrometheusPluginNotEnabled, + self.controller.alert, self.request, {}) + + @mock.patch.object(fm_alarm_utils, 'get_not_cleared_alarms') + @mock.patch.object(inst_utils, 'get_inst') + def test_fm_firing(self, mock_inst, mock_alarms): + self.config_fixture.config( + group='prometheus_plugin', fault_management=True) + mock_alarms.return_value = [] + mock_inst.return_value = objects.VnfInstanceV2.from_dict(_inst1) + result = self.controller.alert(self.request, _body_fm1) + self.assertEqual(204, result.status) + + @mock.patch.object(fm_alarm_utils, 'get_not_cleared_alarms') + def test_fm_firing_exception(self, mock_alarms): + self.config_fixture.config( + group='prometheus_plugin', fault_management=True) + mock_alarms.side_effect = Exception("test exception") + result = self.controller.alert(self.request, _body_fm1) + self.assertEqual(204, result.status) + + @mock.patch.object(fm_alarm_utils, 'get_not_cleared_alarms') + @mock.patch.object(inst_utils, 'get_inst') + def test_fm_firing_mismatch(self, mock_inst, mock_alarms): + self.config_fixture.config( + group='prometheus_plugin', fault_management=True) + mock_alarms.return_value = [] + mock_inst.return_value = objects.VnfInstanceV2.from_dict(_inst1) + result = self.controller.alert(self.request, _body_fm3) + self.assertEqual(204, result.status) + result = self.controller.alert(self.request, _body_fm4) + self.assertEqual(204, result.status) + + @mock.patch.object(fm_alarm_utils, 'get_not_cleared_alarms') + @mock.patch.object(inst_utils, 'get_inst') + def test_fm_already_firing(self, mock_inst, mock_alarms): + self.config_fixture.config( + group='prometheus_plugin', fault_management=True) + mock_alarms.return_value = [ + objects.AlarmV1.from_dict(_not_cleared_alarms)] + mock_inst.return_value = objects.VnfInstanceV2.from_dict(_inst1) + result = self.controller.alert(self.request, _body_fm1) + self.assertEqual(204, result.status) + + @mock.patch.object(fm_alarm_utils, 'get_not_cleared_alarms') + @mock.patch.object(inst_utils, 'get_inst') + def test_fm_resolved(self, mock_inst, mock_alarms): + self.config_fixture.config( + group='prometheus_plugin', fault_management=True) + mock_alarms.return_value = [ + objects.AlarmV1.from_dict(_not_cleared_alarms)] + mock_inst.return_value = objects.VnfInstanceV2.from_dict(_inst1) + result = self.controller.alert(self.request, _body_fm2) + self.assertEqual(204, result.status) + + @mock.patch.object(fm_alarm_utils, 'get_not_cleared_alarms') + @mock.patch.object(inst_utils, 'get_inst') + def test_fm_set_callback(self, mock_inst, mock_alarms): + self.config_fixture.config( + group='prometheus_plugin', fault_management=True) + mock_alarms.return_value = [ + objects.AlarmV1.from_dict(_not_cleared_alarms)] + mock_inst.return_value = objects.VnfInstanceV2.from_dict(_inst1) + _plugin = plugin.PrometheusPluginFm.instance() + _plugin.set_callback(None) + result = self.controller.alert(self.request, _body_fm2) + self.assertEqual(204, result.status) + mock_alarms.return_value = [] + result = self.controller.alert(self.request, _body_fm1) + self.assertEqual(204, result.status) + + +class TestPrometheusPluginAutoScaling(base.TestCase): + def setUp(self): + super(TestPrometheusPluginAutoScaling, self).setUp() + objects.register_all() + self.context = context.get_admin_context() + self.request = mock.Mock() + self.request.context = self.context + self.controller = prometheus_plugin_controller.AutoScalingController() + plugin.PrometheusPluginAutoScaling._instance = None + + def tearDown(self): + super(TestPrometheusPluginAutoScaling, self).tearDown() + # delete singleton object + plugin.PrometheusPluginAutoScaling._instance = None + + def test_auto_scaling_config_false(self): + self.config_fixture.config( + group='prometheus_plugin', auto_scaling=False) + self.assertRaises( + sol_ex.PrometheusPluginNotEnabled, + self.controller.auto_scaling_id, self.request, 'id', {}) + + @mock.patch.object(inst_utils, 'get_inst') + def test_auto_scaling_no_autoscale_enabled(self, mock_inst): + self.config_fixture.config( + group='prometheus_plugin', auto_scaling=True) + mock_inst.return_value = objects.VnfInstanceV2.from_dict(_inst1) + result = self.controller.auto_scaling_id( + self.request, 'id', _body_scale) + self.assertEqual(204, result.status) + + @mock.patch.object(inst_utils, 'get_inst') + def test_auto_scaling_is_autoscale_enabled(self, mock_inst): + self.config_fixture.config( + group='prometheus_plugin', auto_scaling=True) + mock_inst.return_value = objects.VnfInstanceV2.from_dict(_inst2) + result = self.controller.auto_scaling(self.request, _body_scale) + self.assertEqual(204, result.status) + + @mock.patch.object(inst_utils, 'get_inst') + def test_auto_scaling_set_callback(self, mock_inst): + self.config_fixture.config( + group='prometheus_plugin', auto_scaling=True) + _plugin = plugin.PrometheusPluginAutoScaling.instance() + _plugin.set_callback(None) + mock_inst.return_value = objects.VnfInstanceV2.from_dict(_inst2) + result = self.controller.auto_scaling(self.request, _body_scale) + self.assertEqual(204, result.status) + + def test_auto_scaling_error_body(self): + self.config_fixture.config( + group='prometheus_plugin', auto_scaling=True) + result = self.controller.auto_scaling(self.request, {}) + self.assertEqual(204, result.status) diff --git a/tacker/tests/unit/sol_refactored/controller/test_vnffm_v1.py b/tacker/tests/unit/sol_refactored/controller/test_vnffm_v1.py new file mode 100644 index 000000000..a3230c380 --- /dev/null +++ b/tacker/tests/unit/sol_refactored/controller/test_vnffm_v1.py @@ -0,0 +1,205 @@ +# Copyright (C) 2022 Fujitsu +# All Rights Reserved. +# +# 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. + + +import requests +from unittest import mock + +from tacker import context +from tacker.sol_refactored.api import api_version +from tacker.sol_refactored.common import exceptions as sol_ex +from tacker.sol_refactored.common import fm_alarm_utils as alarm_utils +from tacker.sol_refactored.common import fm_subscription_utils as subsc_utils +from tacker.sol_refactored.controller import vnffm_v1 +from tacker.sol_refactored import objects +from tacker.tests import base +from tacker.tests.unit.sol_refactored.samples import fakes_for_fm + +SAMPLE_INST_ID = 'c61314d0-f583-4ab3-a457-46426bce02d3' +SAMPLE_ALARM_ID = '78a39661-60a8-4824-b989-88c1b0c3534a' +SAMPLE_SUBSC_ID = '78a39661-60a8-4824-b989-88c1b0c3534a' + + +class TestVnffmV1(base.BaseTestCase): + + def setUp(self): + super(TestVnffmV1, self).setUp() + objects.register_all() + self.controller = vnffm_v1.VnfFmControllerV1() + self.context = context.get_admin_context() + self.context.api_version = api_version.APIVersion("1.3.0") + self.request = mock.Mock() + self.request.context = self.context + + def test_supported_api_versions(self): + result = self.controller.supported_api_versions('show') + + self.assertEqual(['1.3.0'], result) + + def test_allowed_content_types(self): + result = self.controller.allowed_content_types('show') + self.assertEqual(['application/json', 'text/plain'], result) + + result = self.controller.allowed_content_types('update') + self.assertEqual(['application/mergepatch+json', 'application/json', + 'text/plain'], result) + + @mock.patch.object(alarm_utils, 'get_alarms_all') + def test_index(self, mock_alarms): + request = requests.Request() + request.context = self.context + request.GET = {'filter': f'(eq,managedObjectId,{SAMPLE_INST_ID})'} + mock_alarms.return_value = [objects.AlarmV1.from_dict( + fakes_for_fm.alarm_example), objects.AlarmV1( + id='test-1', managedObjectId='inst-1')] + + result = self.controller.index(request) + self.assertEqual(200, result.status) + self.assertEqual([fakes_for_fm.alarm_example], result.body) + self.assertEqual('1.3.0', result.headers['version']) + + # no filter + request.GET = {} + result = self.controller.index(request) + self.assertEqual(200, result.status) + self.assertEqual(2, len(result.body)) + self.assertEqual('1.3.0', result.headers['version']) + + @mock.patch.object(alarm_utils, 'get_alarm') + def test_show(self, mock_alarm): + request = requests.Request() + request.context = self.context + mock_alarm.return_value = objects.AlarmV1.from_dict( + fakes_for_fm.alarm_example) + result = self.controller.show(request, SAMPLE_ALARM_ID) + self.assertEqual(200, result.status) + self.assertEqual(fakes_for_fm.alarm_example, result.body) + self.assertEqual('1.3.0', result.headers['version']) + + @mock.patch.object(objects.base.TackerPersistentObject, 'update') + @mock.patch.object(alarm_utils, 'get_alarm') + def test_update(self, mock_alarm, mock_update): + mock_alarm.return_value = objects.AlarmV1.from_dict( + fakes_for_fm.alarm_example) + body = {"ackState": "ACKNOWLEDGED"} + result = self.controller.update( + request=self.request, id=SAMPLE_ALARM_ID, body=body) + self.assertEqual(200, result.status) + self.assertEqual('1.3.0', result.headers['version']) + self.assertEqual(body, result.body) + + @mock.patch.object(alarm_utils, 'get_alarm') + def test_update_invalid_body(self, mock_alarm): + mock_alarm.return_value = objects.AlarmV1.from_dict( + fakes_for_fm.alarm_example) + body = {"ackState": "UNACKNOWLEDGED"} + self.assertRaises(sol_ex.AckStateInvalid, self.controller.update, + request=self.request, id=SAMPLE_ALARM_ID, body=body) + + @mock.patch.object(objects.base.TackerPersistentObject, 'create') + @mock.patch.object(subsc_utils, 'test_notification') + def test_subscription_create(self, mock_test, mock_create): + body = { + "callbackUri": "http://127.0.0.1:6789/notification", + "authentication": { + "authType": ["BASIC", "OAUTH2_CLIENT_CREDENTIALS"], + "paramsBasic": { + "userName": "test", + "password": "test" + }, + "paramsOauth2ClientCredentials": { + "clientId": "test", + "clientPassword": "test", + "tokenEndpoint": "https://127.0.0.1/token" + } + }, + "filter": fakes_for_fm.fm_subsc_example['filter'] + } + result = self.controller.subscription_create( + request=self.request, body=body) + self.assertEqual(201, result.status) + self.assertEqual(body['callbackUri'], result.body['callbackUri']) + self.assertEqual(body['filter'], result.body['filter']) + self.assertIsNone(result.body.get('authentication')) + + def test_invalid_subscripion(self): + body = { + "callbackUri": "http://127.0.0.1:6789/notification", + "authentication": { + "authType": ["BASIC"] + } + } + ex = self.assertRaises(sol_ex.InvalidSubscription, + self.controller.subscription_create, request=self.request, + body=body) + self.assertEqual("ParamsBasic must be specified.", ex.detail) + + body = { + "callbackUri": "http://127.0.0.1:6789/notification", + "authentication": { + "authType": ["OAUTH2_CLIENT_CREDENTIALS"] + } + } + ex = self.assertRaises(sol_ex.InvalidSubscription, + self.controller.subscription_create, request=self.request, + body=body) + self.assertEqual("paramsOauth2ClientCredentials must be specified.", + ex.detail) + + body = { + "callbackUri": "http://127.0.0.1:6789/notification", + "authentication": { + "authType": ["TLS_CERT"] + } + } + ex = self.assertRaises(sol_ex.InvalidSubscription, + self.controller.subscription_create, request=self.request, + body=body) + self.assertEqual("'TLS_CERT' is not supported at the moment.", + ex.detail) + + @mock.patch.object(subsc_utils, 'get_subsc_all') + def test_subscription_list(self, mock_subsc): + request = requests.Request() + request.context = self.context + request.GET = { + 'filter': '(eq,callbackUri,/nfvo/notify/alarm)'} + mock_subsc.return_value = [ + objects.FmSubscriptionV1.from_dict(fakes_for_fm.fm_subsc_example)] + + result = self.controller.subscription_list(request) + self.assertEqual(200, result.status) + + # no filter + request.GET = {} + result = self.controller.subscription_list(request) + self.assertEqual(200, result.status) + + @mock.patch.object(subsc_utils, 'get_subsc') + def test_subscription_show(self, mock_subsc): + mock_subsc.return_value = objects.FmSubscriptionV1.from_dict( + fakes_for_fm.fm_subsc_example) + result = self.controller.subscription_show( + request=self.request, id=SAMPLE_SUBSC_ID) + self.assertEqual(200, result.status) + + @mock.patch.object(subsc_utils, 'get_subsc') + @mock.patch.object(objects.base.TackerPersistentObject, 'delete') + def test_subscription_delete(self, mock_delete, mock_subsc): + mock_subsc.return_value = objects.FmSubscriptionV1.from_dict( + fakes_for_fm.fm_subsc_example) + result = self.controller.subscription_delete( + request=self.request, id=SAMPLE_SUBSC_ID) + self.assertEqual(204, result.status) diff --git a/tacker/tests/unit/sol_refactored/controller/test_vnfpm_v2.py b/tacker/tests/unit/sol_refactored/controller/test_vnfpm_v2.py new file mode 100644 index 000000000..06f9a415f --- /dev/null +++ b/tacker/tests/unit/sol_refactored/controller/test_vnfpm_v2.py @@ -0,0 +1,433 @@ +# Copyright (C) 2022 FUJITSU +# All Rights Reserved. +# +# 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. +import requests + +from unittest import mock + +from tacker import context +from tacker.sol_refactored.api import api_version +from tacker.sol_refactored.common import config +from tacker.sol_refactored.common import exceptions as sol_ex +from tacker.sol_refactored.common import pm_job_utils +from tacker.sol_refactored.controller.vnflcm_view import BaseViewBuilder +from tacker.sol_refactored.controller.vnflcm_view import Pager +from tacker.sol_refactored.controller import vnfpm_v2 +from tacker.sol_refactored.controller import vnfpm_view +from tacker.sol_refactored.controller.vnfpm_view import PmJobViewBuilder +from tacker.sol_refactored import objects +from tacker.tests import base + +CONF = config.CONF + + +class TestVnfpmV2(base.BaseTestCase): + + def setUp(self): + super(TestVnfpmV2, self).setUp() + objects.register_all() + self.context = context.get_admin_context() + self.context.api_version = api_version.APIVersion("2.1.0") + self.request = mock.Mock() + self.request.context = self.context + self.controller = vnfpm_v2.VnfPmControllerV2() + self.endpoint = CONF.v2_vnfm.endpoint + self._pm_job_view = vnfpm_view.PmJobViewBuilder(self.endpoint) + + def test_check_http_client_auth(self): + auth_req = { + 'authType': ['BASIC'], + 'paramsBasic': None + } + self.assertRaises(sol_ex.InvalidSubscription, + vnfpm_v2._check_http_client_auth, + auth_req=auth_req) + + auth_req_1 = { + 'authType': ['BASIC'], + 'paramsBasic': { + 'userName': 'test', + 'password': 'test' + }, + } + result = vnfpm_v2._check_http_client_auth(auth_req_1) + self.assertEqual(['BASIC'], result.authType) + + auth_req_2 = { + 'authType': ['OAUTH2_CLIENT_CREDENTIALS'], + 'paramsOauth2ClientCredentials': { + 'clientId': 'test', + 'clientPassword': 'test', + 'tokenEndpoint': + 'http://127.0.0.1/token' + } + } + result = vnfpm_v2._check_http_client_auth(auth_req_2) + self.assertEqual(['OAUTH2_CLIENT_CREDENTIALS'], result.authType) + + auth_req_3 = { + 'authType': ['OAUTH2_CLIENT_CREDENTIALS'], + } + self.assertRaises(sol_ex.InvalidSubscription, + vnfpm_v2._check_http_client_auth, + auth_req=auth_req_3) + + auth_req_4 = { + 'authType': ['TLS_CERT'] + } + self.assertRaises(sol_ex.InvalidSubscription, + vnfpm_v2._check_http_client_auth, + auth_req=auth_req_4) + + def test_check_performance_metric_or_group(self): + vnfpm_v2._check_performance_metric_or_group( + obj_type='Vnf', + metric_group=['VirtualisedComputeResource'], + performance_metric=['VCpuUsageMeanVnf.VNF']) + + self.assertRaises(sol_ex.PMJobInvalidRequest, + vnfpm_v2._check_performance_metric_or_group, + obj_type='Vnf', + metric_group=['VirtualisedComputeResource', + 'VnfInternalCP'], + performance_metric=['VCpuUsageMeanVnf.VNF']) + + self.assertRaises(sol_ex.PMJobInvalidRequest, + vnfpm_v2._check_performance_metric_or_group, + obj_type='Vnf', + metric_group=['VirtualisedComputeResource'], + performance_metric=['ByteIncomingVnfExtCp.VNF']) + + def test_create_error_1(self): + _PmJobCriteriaV2 = { + 'performanceMetric': ['VCpuUsageMeanVnf.VNF'], + 'performanceMetricGroup': ['VirtualisedComputeResource'], + 'collectionPeriod': 10, + 'reportingPeriod': 11, + 'reportingBoundary': '2022-08-05T02:24:46Z', + } + _SubscriptionAuthentication = { + 'authType': ['BASIC'], + 'paramsBasic': { + 'userName': 'test_name', + 'password': 'test_pwd' + } + } + body = { + "objectType": "Vnf", + "objectInstanceIds": ["id_1", "id_2", "id_3"], + "subObjectInstanceIds": ["sub_id_1", "sub_id_2"], + "criteria": _PmJobCriteriaV2, + "authentication": _SubscriptionAuthentication, + "callbackUri": 'callbackuri', + } + self.assertRaises(sol_ex.PMJobInvalidRequest, + self.controller.create, + request=self.request, body=body) + + def test_create_error_2(self): + _PmJobCriteriaV2 = { + 'performanceMetric': [], + 'performanceMetricGroup': [], + 'collectionPeriod': 10, + 'reportingPeriod': 11, + 'reportingBoundary': '2022-08-05T02:24:46Z', + } + _SubscriptionAuthentication = { + 'authType': ['BASIC'], + 'paramsBasic': { + 'userName': 'test_name', + 'password': 'test_pwd' + } + } + body = { + 'objectType': 'Vnf', + 'objectInstanceIds': ['id_1'], + 'subObjectInstanceIds': ['sub_id_1', 'sub_id_2'], + 'criteria': _PmJobCriteriaV2, + 'callbackUri': 'callbackuri', + 'authentication': _SubscriptionAuthentication + } + self.assertRaises(sol_ex.PMJobInvalidRequest, + self.controller.create, + request=self.request, body=body) + + def test_create_error_3(self): + _PmJobCriteriaV2 = { + 'performanceMetric': ['VCpuUsageMeanVnf.VNF'], + 'performanceMetricGroup': ['error-test'], + 'collectionPeriod': 10, + 'reportingPeriod': 11, + 'reportingBoundary': '2022-08-05T02:24:46Z', + } + _SubscriptionAuthentication = { + 'authType': ['BASIC'], + 'paramsBasic': { + 'userName': 'test_name', + 'password': 'test_pwd' + } + } + body = { + 'objectType': 'Vnf', + 'objectInstanceIds': ['id_1'], + 'subObjectInstanceIds': ['sub_id_1', 'sub_id_2'], + 'criteria': _PmJobCriteriaV2, + 'callbackUri': 'callbackuri', + 'authentication': _SubscriptionAuthentication + } + self.assertRaises(sol_ex.PMJobInvalidRequest, + self.controller.create, + request=self.request, body=body) + + def test_create_error_4(self): + _PmJobCriteriaV2 = { + 'performanceMetric': ['error.VNF'], + 'performanceMetricGroup': ['VirtualisedComputeResource'], + 'collectionPeriod': 10, + 'reportingPeriod': 11, + 'reportingBoundary': '2022-08-05T02:24:46Z', + } + _SubscriptionAuthentication = { + 'authType': ['BASIC'], + 'paramsBasic': { + 'userName': 'test_name', + 'password': 'test_pwd' + } + } + body = { + 'objectType': 'Vnf', + 'objectInstanceIds': ['id_1'], + 'subObjectInstanceIds': ['sub_id_1', 'sub_id_2'], + 'criteria': _PmJobCriteriaV2, + 'callbackUri': 'callbackuri', + 'authentication': _SubscriptionAuthentication + } + self.assertRaises(sol_ex.PMJobInvalidRequest, + self.controller.create, + request=self.request, body=body) + + @mock.patch.object(objects.base.TackerPersistentObject, 'get_by_id') + def test_create_error_5(self, mock_inst): + mock_inst.return_value = objects.VnfInstanceV2( + id='dummy-vnfInstanceId-1', vnfdId='dummy-vnfdId-1', + vnfProvider='dummy-vnfProvider-1', + instantiationState='NOT_INSTANTIATED', + vnfProductName='dummy-vnfProductName-1-1', + vnfSoftwareVersion='1.0', vnfdVersion='1.0', + vnfInstanceName='dummy-vnfInstanceName-1') + _PmJobCriteriaV2 = { + 'performanceMetric': ['VCpuUsageMeanVnf.VNF'], + 'performanceMetricGroup': ['VirtualisedComputeResource'], + 'collectionPeriod': 10, + 'reportingPeriod': 11, + 'reportingBoundary': '2022-08-05T02:24:46Z', + } + _SubscriptionAuthentication = { + 'authType': ['BASIC'], + 'paramsBasic': { + 'userName': 'test_name', + 'password': 'test_pwd' + } + } + body = { + 'objectType': 'Vnf', + 'objectInstanceIds': ['id_1'], + 'subObjectInstanceIds': ['sub_id_1', 'sub_id_2'], + 'criteria': _PmJobCriteriaV2, + 'callbackUri': 'callbackuri', + 'authentication': _SubscriptionAuthentication + } + self.assertRaises(sol_ex.VnfInstanceIsNotInstantiated, + self.controller.create, + request=self.request, body=body) + + @mock.patch.object(objects.base.TackerPersistentObject, 'get_by_id') + def test_create_error6(self, mock_inst): + mock_inst.return_value = objects.VnfInstanceV2( + id='dummy-vnfInstanceId-1', vnfdId='dummy-vnfdId-1', + vnfProvider='dummy-vnfProvider-1', + instantiationState='INSTANTIATED', + vnfProductName='dummy-vnfProductName-1-1', + vnfSoftwareVersion='1.0', vnfdVersion='1.0', + vnfInstanceName='dummy-vnfInstanceName-1') + _PmJobCriteriaV2 = { + "performanceMetric": ["VCpuUsageMeanVnf.VNF"], + "performanceMetricGroup": ["VirtualisedComputeResource"], + "collectionPeriod": 10, + "reportingPeriod": 11, + "reportingBoundary": "invalid datetime format", + } + _SubscriptionAuthentication = { + 'authType': ['BASIC'], + 'paramsBasic': { + 'userName': 'test_name', + 'password': 'test_pwd' + } + } + body = { + "objectType": "Vnf", + "objectInstanceIds": ["id_1"], + "subObjectInstanceIds": ["sub_id_1", "sub_id_2"], + "criteria": _PmJobCriteriaV2, + "authentication": _SubscriptionAuthentication, + "callbackUri": "http://127.0.0.1:6789/notification", + 'metadata': {"metadata": "example"} + } + self.assertRaises( + sol_ex.SolValidationError, + self.controller.create, request=self.request, body=body) + + @mock.patch.object(objects.base.TackerPersistentObject, 'create') + @mock.patch.object(pm_job_utils, 'test_notification') + @mock.patch.object(objects.base.TackerPersistentObject, 'get_by_id') + def test_create_201(self, mock_inst, mock_notifi, mock_create): + mock_inst.return_value = objects.VnfInstanceV2( + id='dummy-vnfInstanceId-1', vnfdId='dummy-vnfdId-1', + vnfProvider='dummy-vnfProvider-1', + instantiationState='INSTANTIATED', + vnfProductName='dummy-vnfProductName-1-1', + vnfSoftwareVersion='1.0', vnfdVersion='1.0', + vnfInstanceName='dummy-vnfInstanceName-1') + mock_notifi.return_value = None + mock_create.return_value = None + _PmJobCriteriaV2 = { + "performanceMetric": ["VCpuUsageMeanVnf.VNF"], + "performanceMetricGroup": ["VirtualisedComputeResource"], + "collectionPeriod": 10, + "reportingPeriod": 11, + "reportingBoundary": "2022-08-05T02:24:46Z", + } + _SubscriptionAuthentication = { + 'authType': ['BASIC'], + 'paramsBasic': { + 'userName': 'test_name', + 'password': 'test_pwd' + } + } + body = { + "objectType": "Vnf", + "objectInstanceIds": ["id_1"], + "subObjectInstanceIds": ["sub_id_1", "sub_id_2"], + "criteria": _PmJobCriteriaV2, + "authentication": _SubscriptionAuthentication, + "callbackUri": "http://127.0.0.1:6789/notification", + 'metadata': {"metadata": "example"} + } + + result = self.controller.create(request=self.request, body=body) + self.assertEqual(201, result.status) + + @mock.patch.object(Pager, 'get_link') + @mock.patch.object(BaseViewBuilder, 'detail_list') + @mock.patch.object(objects.base.TackerPersistentObject, 'get_all') + @mock.patch.object(vnfpm_view.PmJobViewBuilder, 'parse_pager') + @mock.patch.object(vnfpm_view.PmJobViewBuilder, 'parse_filter') + @mock.patch.object(vnfpm_view.PmJobViewBuilder, 'parse_selector') + def test_index(self, mock_parse_selector, mock_parse_filter, + mock_parse_pager, + mock_pm, + mock_detail_list, + mock_get_link): + mock_parse_selector.return_value = 'selector' + mock_parse_filter.return_value = 'filter' + + request = requests.Request() + request.GET = { + 'filter': 'pm_job_id', 'nextpage_opaque_marker': 'marker'} + request.url = 'url' + page_size = CONF.v2_vnfm.vnf_instance_page_size + pager = Pager(request.GET.get('nextpage_opaque_marker'), + request.url, + page_size) + mock_parse_pager.return_value = pager + + mock_pm.return_value = [objects.PmJobV2(id='pm_job_1')] + mock_detail_list.return_value = 1 + mock_get_link.return_value = 'url' + + result = self.controller.index(self.request) + self.assertEqual(200, result.status) + + @mock.patch.object(objects.base.TackerPersistentObject, 'get_by_id') + def test_show(self, mock_pm): + mock_pm.return_value = objects.PmJobV2( + id='pm_job_1', + objectInstanceIds=["id_1"], + authentication=objects.SubscriptionAuthentication( + authType=["BASIC"], + paramsBasic=objects.SubscriptionAuthentication_ParamsBasic( + userName='test', + password='test' + ), + ) + ) + result = self.controller.show(self.request, 'pm_job_1') + self.assertEqual(200, result.status) + + @mock.patch.object(objects.base.TackerPersistentObject, 'update') + @mock.patch.object(objects.base.TackerPersistentObject, 'get_by_id') + @mock.patch.object(pm_job_utils, 'test_notification') + def test_update(self, mock_notifi, mock_pm, mock_update): + mock_notifi.return_value = None + mock_pm.return_value = objects.PmJobV2(id='pm_job_1') + mock_update.return_value = None + + _SubscriptionAuthentication = { + 'authType': ['BASIC'], + 'paramsBasic': { + 'userName': 'test_name', + 'password': 'test_pwd' + } + } + body = { + 'objectType': 'Vnf', + 'callbackUri': 'callbackuri', + 'authentication': _SubscriptionAuthentication + } + + result = self.controller.update(request=self.request, id='id', + body=body) + self.assertEqual(200, result.status) + + @mock.patch.object(objects.base.TackerPersistentObject, 'get_by_filter') + @mock.patch.object(objects.base.TackerPersistentObject, 'get_by_id') + def test_delete(self, mock_pm, mock_report): + mock_pm.return_value = objects.PmJobV2(id='pm_job_1') + mock_report.return_value = [objects.PerformanceReportV2( + id='report_id', + jobId='pm_job_1')] + result = self.controller.delete(self.request, 'pm_job_1') + self.assertEqual(204, result.status) + + @mock.patch.object(PmJobViewBuilder, 'report_detail') + @mock.patch.object(pm_job_utils, 'get_pm_report') + def test_report_get(self, mock_get, mock_report): + mock_get.return_value = 'pm_report' + mock_report.return_value = 'pm_report_resp' + result = self.controller.report_get( + self.request, 'pm_job_id', 'report_id') + self.assertEqual(200, result.status) + + def test_allowed_content_types(self): + result = self.controller.allowed_content_types('update') + top = ['application/mergepatch+json', 'application/json', 'text/plain'] + self.assertEqual(top, result) + + result = self.controller.allowed_content_types('create') + top = ['application/json', 'text/plain'] + self.assertEqual(top, result) + + def test_supported_api_version(self): + result = self.controller.supported_api_versions('create') + self.assertEqual(['2.1.0'], result) diff --git a/tacker/tests/unit/sol_refactored/controller/test_vnfpm_view.py b/tacker/tests/unit/sol_refactored/controller/test_vnfpm_view.py new file mode 100644 index 000000000..cb7738386 --- /dev/null +++ b/tacker/tests/unit/sol_refactored/controller/test_vnfpm_view.py @@ -0,0 +1,73 @@ +# Copyright (C) 2021 Nippon Telegraph and Telephone Corporation +# All Rights Reserved. +# +# 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. + +from tacker import context +from tacker.tests import base +from unittest import mock + +from tacker.sol_refactored.common import config +from tacker.sol_refactored.controller.vnflcm_view import BaseViewBuilder +from tacker.sol_refactored.controller.vnfpm_view import PmJobViewBuilder +from tacker.sol_refactored import objects + + +CONF = config.CONF + + +class TestPmJobViewBuilder(base.BaseTestCase): + + def setUp(self): + super(TestPmJobViewBuilder, self).setUp() + objects.register_all() + self.context = context.get_admin_context() + self.request = mock.Mock() + self.request.context = self.context + self.endpoint = CONF.v2_vnfm.endpoint + + @mock.patch.object(BaseViewBuilder, 'parse_filter') + def test_parse_filter(self, mock_parse_filter): + mock_parse_filter.return_value = 1 + result = PmJobViewBuilder(self.endpoint).parse_filter('filter_param') + self.assertEqual(1, result) + + @mock.patch.object(BaseViewBuilder, 'parse_pager') + def test_parse_pager(self, mock_parse_pager): + mock_parse_pager.return_value = 1 + page_size = CONF.v2_vnfm.vnfpm_pmjob_page_size + result = PmJobViewBuilder(self.endpoint).parse_pager( + self.request, page_size) + self.assertEqual(1, result) + + def test_detail(self): + pm_job = objects.PmJobV2( + id='pm_job_1', + objectInstanceIds=["id_1"], + authentication=objects.SubscriptionAuthentication( + authType=["BASIC"], + paramsBasic=objects.SubscriptionAuthentication_ParamsBasic( + userName='test', + password='test' + ), + ) + ) + result = PmJobViewBuilder(self.endpoint).detail(pm_job) + self.assertEqual('pm_job_1', result.get('id')) + + @mock.patch.object(BaseViewBuilder, 'detail_list') + def test_report_detail(self, mock_detail_list): + mock_detail_list.return_value = 1 + result = PmJobViewBuilder(self.endpoint).detail_list( + 'pm_jobs', 'filters', 'selector', 'pager') + self.assertEqual(1, result) diff --git a/tacker/tests/unit/sol_refactored/nfvo/test_nfvo_client.py b/tacker/tests/unit/sol_refactored/nfvo/test_nfvo_client.py index 4d68303e0..fdee2388f 100644 --- a/tacker/tests/unit/sol_refactored/nfvo/test_nfvo_client.py +++ b/tacker/tests/unit/sol_refactored/nfvo/test_nfvo_client.py @@ -21,6 +21,7 @@ from tacker import context from tacker.sol_refactored.api import api_version from tacker.sol_refactored.common import config from tacker.sol_refactored.common import http_client +from tacker.sol_refactored.common import pm_job_utils from tacker.sol_refactored.common import subscription_utils as subsc_utils from tacker.sol_refactored.common import vnfd_utils from tacker.sol_refactored.nfvo import local_nfvo @@ -419,3 +420,22 @@ class TestNfvoClient(base.BaseTestCase): self.context, lcmocc, inst, 'http://127.0.0.1:9890') self.assertEqual(1, mock_recv.call_count) self.assertEqual(1, mock_send.call_count) + + @mock.patch.object(pm_job_utils, 'send_notification') + @mock.patch.object(pm_job_utils, 'make_pm_notif_data') + def test_send_pm_job_notification(self, mock_notif, mock_send): + mock_notif.return_value = 'mock_notif' + mock_send.return_value = None + entries = { + 'objectType': "VNF", + 'objectInstanceId': "instance_id_1", + 'subObjectInstanceId': "subObjectInstanceId_1" + } + report = objects.PerformanceReportV2( + id=uuidutils.generate_uuid(), + jobId='pm_job_id', + entries=[objects.VnfPmReportV2_Entries.from_dict(entries)] + ) + self.nfvo_client.send_pm_job_notification( + report, "pm_job", 'timestamp', self.nfvo_client.endpoint + ) diff --git a/tacker/tests/unit/sol_refactored/samples/fakes_for_fm.py b/tacker/tests/unit/sol_refactored/samples/fakes_for_fm.py new file mode 100644 index 000000000..1fb0fdb75 --- /dev/null +++ b/tacker/tests/unit/sol_refactored/samples/fakes_for_fm.py @@ -0,0 +1,112 @@ +# Copyright (C) 2022 Fujitsu +# All Rights Reserved. +# +# 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. + + +alarm_example = { + "id": "78a39661-60a8-4824-b989-88c1b0c3534a", + "managedObjectId": "c61314d0-f583-4ab3-a457-46426bce02d3", + "rootCauseFaultyResource": { + "faultyResource": { + "vimConnectionId": "0d57e928-86a4-4445-a4bd-1634edae73f3", + "resourceId": "4e6ccbe1-38ec-4b1b-a278-64de09ba01b3", + "vimLevelResourceType": "OS::Nova::Server" + }, + "faultyResourceType": "COMPUTE" + }, + "alarmRaisedTime": "2021-09-06T10:21:03Z", + "alarmChangedTime": "2021-09-06T10:21:03Z", + "alarmClearedTime": "2021-09-06T10:21:03Z", + "alarmAcknowledgedTime": "2021-09-06T10:21:03Z", + "ackState": "UNACKNOWLEDGED", + "perceivedSeverity": "WARNING", + "eventTime": "2021-09-06T10:21:03Z", + "eventType": "EQUIPMENT_ALARM", + "faultType": "Fault Type", + "probableCause": "The server cannot be connected.", + "isRootCause": False, + "correlatedAlarmIds": [ + "c88b624e-e997-4b17-b674-10ca2bab62e0", + "c16d41fd-12e2-49a6-bb17-72faf702353f" + ], + "faultDetails": [ + "Fault", + "Details" + ], + "_links": { + "self": { + "href": "/vnffm/v1/alarms/78a39661-60a8-4824-b989-88c1b0c3534a" + }, + "objectInstance": { + "href": "/vnflcm/v1/vnf_instances/" + "0e5f3086-4e79-47ed-a694-54c29155fa26" + } + } +} + +fm_subsc_example = { + "id": "78a39661-60a8-4824-b989-88c1b0c3534a", + "filter": { + "vnfInstanceSubscriptionFilter": { + "vnfdIds": [ + "dummy-vnfdId-1" + ], + "vnfProductsFromProviders": [ + { + "vnfProvider": "dummy-vnfProvider-1", + "vnfProducts": [ + { + "vnfProductName": "dummy-vnfProductName-1-1", + "versions": [ + { + "vnfSoftwareVersion": '1.0', + "vnfdVersions": ['1.0', '2.0'] + } + ] + } + ] + } + ], + "vnfInstanceIds": [ + "dummy-vnfInstanceId-1" + ], + "vnfInstanceNames": [ + "dummy-vnfInstanceName-1" + ] + }, + "notificationTypes": [ + "AlarmNotification", + "AlarmClearedNotification" + ], + "faultyResourceTypes": [ + "COMPUTE" + ], + "perceivedSeverities": [ + "WARNING" + ], + "eventTypes": [ + "EQUIPMENT_ALARM" + ], + "probableCauses": [ + "The server cannot be connected." + ] + }, + "callbackUri": "/nfvo/notify/alarm", + "_links": { + "self": { + "href": "/vnffm/v1/subscriptions/" + "78a39661-60a8-4824-b989-88c1b0c3534a" + } + } +} diff --git a/test-requirements.txt b/test-requirements.txt index 6ae7245b3..8cbdd8831 100644 --- a/test-requirements.txt +++ b/test-requirements.txt @@ -21,3 +21,4 @@ python-barbicanclient>=4.5.2 # Apache-2.0 python-blazarclient>=1.0.1 # Apache-2.0 requests-mock>=1.2.0 # Apache-2.0 PyMySQL>=0.10.1 # MIT +freezegun>=1.2.2 # Apache-2.0 diff --git a/tools/test-setup-fake-prometheus-server.sh b/tools/test-setup-fake-prometheus-server.sh new file mode 100644 index 000000000..583c5f52e --- /dev/null +++ b/tools/test-setup-fake-prometheus-server.sh @@ -0,0 +1,14 @@ +#!/bin/bash -xe + +# This script is used to set up a fake prometheus server +# for functional testing. +# + +cd /opt/stack/tacker/tacker/tests/functional/sol_kubernetes_v2/samples/ +sudo unzip tacker-monitoring-test.zip +cd ./tacker-monitoring-test + +sudo docker build -t tacker-monitoring-test . +sudo docker run -v ${PWD}/src:/work/src -v ${PWD}/rules:/etc/prometheus/rules \ + -p 55555:55555 -p 50022:22 -e TEST_REMOTE_URI="http://0.0.0.0" -d \ + -it tacker-monitoring-test