From 8b45c8082b297ffb0aafc6575440747c1382158d Mon Sep 17 00:00:00 2001
From: Yi Feng <fengyi@fujitsu.com>
Date: Tue, 7 Feb 2023 10:15:51 +0900
Subject: [PATCH] Add support cnf auto scale via threshold interface

Add the Performance Management Threshold CLI to support AutoScale.
The Performance Management Threshold API is based on ETSI NFV-SOL
002 v3.3.1 and ETSI NFV-SOL 003 v3.3.1, which is Version "2.0.0"
API of Tacker.

Implements: blueprint support-auto-lcm
Change-Id: Idd313d6abe47dfa41fc86ddc614d00f99f3fc3b2
---
 doc/source/cli/commands.rst                   |   5 +
 setup.cfg                                     |   5 +
 tackerclient/osc/utils.py                     |  29 ++
 tackerclient/osc/v1/vnflcm/vnflcm.py          |  41 +-
 tackerclient/osc/v1/vnflcm/vnflcm_subsc.py    |  27 +-
 tackerclient/osc/v2/vnffm/vnffm_sub.py        |  30 +-
 .../create_vnf_pm_threshold_param_sample.json |  40 ++
 .../update_vnf_pm_threshold_param_sample.json |  27 ++
 tackerclient/osc/v2/vnfpm/vnfpm_job.py        |  32 +-
 tackerclient/osc/v2/vnfpm/vnfpm_threshold.py  | 216 ++++++++++
 tackerclient/tests/unit/osc/v1/test_vnflcm.py |  24 +-
 tackerclient/tests/unit/osc/v2/test_vnflcm.py |   6 +-
 .../tests/unit/osc/v2/test_vnfpm_threshold.py | 398 ++++++++++++++++++
 .../unit/osc/v2/vnfpm_threshold_fakes.py      | 111 +++++
 tackerclient/v1_0/client.py                   |  48 +++
 15 files changed, 907 insertions(+), 132 deletions(-)
 create mode 100644 tackerclient/osc/v2/vnfpm/samples/create_vnf_pm_threshold_param_sample.json
 create mode 100644 tackerclient/osc/v2/vnfpm/samples/update_vnf_pm_threshold_param_sample.json
 create mode 100644 tackerclient/osc/v2/vnfpm/vnfpm_threshold.py
 create mode 100644 tackerclient/tests/unit/osc/v2/test_vnfpm_threshold.py
 create mode 100644 tackerclient/tests/unit/osc/v2/vnfpm_threshold_fakes.py

diff --git a/doc/source/cli/commands.rst b/doc/source/cli/commands.rst
index 4d6aba49..e3c41f5e 100644
--- a/doc/source/cli/commands.rst
+++ b/doc/source/cli/commands.rst
@@ -110,3 +110,8 @@ of individual command can be referred by **openstack help <command-name>**.
    openstack vnfpm job update                      Update PM job.
    openstack vnfpm job delete                      Delete PM job.
    openstack vnfpm report show                     Show PM report.
+   openstack vnfpm threshold create                Create PM threshold.
+   openstack vnfpm threshold list                  List PM threshold.
+   openstack vnfpm threshold show                  Show PM threshold.
+   openstack vnfpm threshold update                Update PM threshold.
+   openstack vnfpm threshold delete                Delete PM threshold.
diff --git a/setup.cfg b/setup.cfg
index 015e0bbc..a5bc1b3e 100644
--- a/setup.cfg
+++ b/setup.cfg
@@ -137,6 +137,11 @@ openstack.tackerclient.v2 =
      vnfpm_job_update = tackerclient.osc.v2.vnfpm.vnfpm_job:UpdateVnfPmJob
      vnfpm_job_delete = tackerclient.osc.v2.vnfpm.vnfpm_job:DeleteVnfPmJob
      vnfpm_report_show = tackerclient.osc.v2.vnfpm.vnfpm_report:ShowVnfPmReport
+     vnfpm_threshold_create = tackerclient.osc.v2.vnfpm.vnfpm_threshold:CreateVnfPmThreshold
+     vnfpm_threshold_list = tackerclient.osc.v2.vnfpm.vnfpm_threshold:ListVnfPmThreshold
+     vnfpm_threshold_show = tackerclient.osc.v2.vnfpm.vnfpm_threshold:ShowVnfPmThreshold
+     vnfpm_threshold_update = tackerclient.osc.v2.vnfpm.vnfpm_threshold:UpdateVnfPmThreshold
+     vnfpm_threshold_delete = tackerclient.osc.v2.vnfpm.vnfpm_threshold:DeleteVnfPmThreshold
      vnffm_alarm_list = tackerclient.osc.v2.vnffm.vnffm_alarm:ListVnfFmAlarm
      vnffm_alarm_show = tackerclient.osc.v2.vnffm.vnffm_alarm:ShowVnfFmAlarm
      vnffm_alarm_update = tackerclient.osc.v2.vnffm.vnffm_alarm:UpdateVnfFmAlarm
diff --git a/tackerclient/osc/utils.py b/tackerclient/osc/utils.py
index 205e271b..8ebf3248 100644
--- a/tackerclient/osc/utils.py
+++ b/tackerclient/osc/utils.py
@@ -20,7 +20,9 @@ Stuffs specific to tackerclient OSC plugin should not be added
 to this module. They should go to tackerclient.osc.v1.utils.
 """
 
+import json
 import operator
+import os
 
 from cliff import columns as cliff_columns
 from keystoneclient import exceptions as identity_exc
@@ -29,6 +31,7 @@ from keystoneclient.v3 import projects
 from osc_lib import utils
 from oslo_serialization import jsonutils
 
+from tackerclient.common import exceptions
 from tackerclient.i18n import _
 
 
@@ -212,3 +215,29 @@ class FormatComplexDataColumn(cliff_columns.FormattableColumn):
 
     def human_readable(self):
         return format_dict_with_indention(self._value)
+
+
+def jsonfile2body(file_path):
+
+    if file_path is None:
+        msg = _("File %s does not exist")
+        reason = msg % file_path
+        raise exceptions.InvalidInput(reason=reason)
+
+    if os.access(file_path, os.R_OK) is False:
+        msg = _("User does not have read privileges to it")
+        raise exceptions.InvalidInput(reason=msg)
+
+    try:
+        with open(file_path) as f:
+            body = json.load(f)
+    except (IOError, ValueError) as ex:
+        msg = _("Failed to load parameter file. Error: %s")
+        reason = msg % ex
+        raise exceptions.InvalidInput(reason=reason)
+
+    if not body:
+        reason = _('The parameter file is empty')
+        raise exceptions.EmptyInput(reason=reason)
+
+    return body
diff --git a/tackerclient/osc/v1/vnflcm/vnflcm.py b/tackerclient/osc/v1/vnflcm/vnflcm.py
index 2517860e..53e6053d 100644
--- a/tackerclient/osc/v1/vnflcm/vnflcm.py
+++ b/tackerclient/osc/v1/vnflcm/vnflcm.py
@@ -13,9 +13,7 @@
 #    License for the specific language governing permissions and limitations
 #    under the License.
 
-import json
 import logging
-import os
 import time
 
 from osc_lib.command import command
@@ -113,7 +111,7 @@ class CreateVnfLcm(command.ShowOne):
         body = {}
 
         if file_path:
-            return jsonfile2body(file_path)
+            return tacker_osc_utils.jsonfile2body(file_path)
 
         body['vnfdId'] = parsed_args.vnfd_id
 
@@ -184,29 +182,6 @@ class ListVnfLcm(command.Lister):
                 ) for s in vnf_instances))
 
 
-def jsonfile2body(file_path):
-
-    if file_path is not None and os.access(file_path, os.R_OK) is False:
-        msg = _("File %s does not exist or user does not have read "
-                "privileges to it")
-        reason = msg % file_path
-        raise exceptions.InvalidInput(reason=reason)
-
-    try:
-        with open(file_path) as f:
-            body = json.load(f)
-    except (IOError, ValueError) as ex:
-        msg = _("Failed to load parameter file. Error: %s")
-        reason = msg % ex
-        raise exceptions.InvalidInput(reason=reason)
-
-    if not body:
-        reason = _('The parameter file is empty')
-        raise exceptions.InvalidInput(reason=reason)
-
-    return body
-
-
 class InstantiateVnfLcm(command.Command):
     _description = _("Instantiate a VNF Instance")
 
@@ -226,7 +201,7 @@ class InstantiateVnfLcm(command.Command):
     def take_action(self, parsed_args):
         client = self.app.client_manager.tackerclient
         result = client.instantiate_vnf_instance(
-            parsed_args.vnf_instance, jsonfile2body(
+            parsed_args.vnf_instance, tacker_osc_utils.jsonfile2body(
                 parsed_args.instantiation_request_file))
         if not result:
             print((_('Instantiate request for VNF Instance %(id)s has been'
@@ -271,7 +246,8 @@ class HealVnfLcm(command.Command):
         if parsed_args.vnfc_instance:
             body['vnfcInstanceId'] = parsed_args.vnfc_instance
         if parsed_args.additional_param_file:
-            body.update(jsonfile2body(parsed_args.additional_param_file))
+            body.update(tacker_osc_utils.jsonfile2body(
+                parsed_args.additional_param_file))
 
         return body
 
@@ -461,7 +437,7 @@ class UpdateVnfLcm(command.Command):
         body = {}
 
         if file_path:
-            return jsonfile2body(file_path)
+            return tacker_osc_utils.jsonfile2body(file_path)
 
         return body
 
@@ -535,7 +511,8 @@ class ScaleVnfLcm(command.Command):
             body['numberOfSteps'] = parsed_args.number_of_steps
 
         if parsed_args.additional_param_file:
-            body.update(jsonfile2body(parsed_args.additional_param_file))
+            body.update(tacker_osc_utils.jsonfile2body(
+                parsed_args.additional_param_file))
 
         return body
 
@@ -574,7 +551,7 @@ class ChangeExtConnVnfLcm(command.Command):
     def take_action(self, parsed_args):
         client = self.app.client_manager.tackerclient
         result = client.change_ext_conn_vnf_instance(
-            parsed_args.vnf_instance, jsonfile2body(
+            parsed_args.vnf_instance, tacker_osc_utils.jsonfile2body(
                 parsed_args.request_file))
         if not result:
             print((_('Change External VNF Connectivity for VNF Instance %s '
@@ -601,7 +578,7 @@ class ChangeVnfPkgVnfLcm(command.Command):
     def take_action(self, parsed_args):
         client = self.app.client_manager.tackerclient
         result = client.change_vnfpkg_vnf_instance(
-            parsed_args.vnf_instance, jsonfile2body(
+            parsed_args.vnf_instance, tacker_osc_utils.jsonfile2body(
                 parsed_args.request_file))
         if not result:
             print((_('Change Current VNF Package for VNF Instance %s '
diff --git a/tackerclient/osc/v1/vnflcm/vnflcm_subsc.py b/tackerclient/osc/v1/vnflcm/vnflcm_subsc.py
index d429141f..847bac74 100644
--- a/tackerclient/osc/v1/vnflcm/vnflcm_subsc.py
+++ b/tackerclient/osc/v1/vnflcm/vnflcm_subsc.py
@@ -13,9 +13,7 @@
 #    License for the specific language governing permissions and limitations
 #    under the License.
 
-import json
 import logging
-import os
 
 from osc_lib.command import command
 from osc_lib import utils
@@ -49,29 +47,6 @@ def _get_columns(lccn_subsc_obj):
                                                            column_map)
 
 
-def jsonfile2body(file_path):
-
-    if file_path is not None and os.access(file_path, os.R_OK) is False:
-        msg = _("File %s does not exist or user does not have read "
-                "privileges to it")
-        reason = msg % file_path
-        raise exceptions.InvalidInput(reason=reason)
-
-    try:
-        with open(file_path) as f:
-            body = json.load(f)
-    except (IOError, ValueError) as ex:
-        msg = _("Failed to load parameter file. Error: %s")
-        reason = msg % ex
-        raise exceptions.InvalidInput(reason=reason)
-
-    if not body:
-        reason = _('The parameter file is empty')
-        raise exceptions.InvalidInput(reason=reason)
-
-    return body
-
-
 class CreateLccnSubscription(command.ShowOne):
     _description = _("Create a new Lccn Subscription")
 
@@ -86,7 +61,7 @@ class CreateLccnSubscription(command.ShowOne):
     def take_action(self, parsed_args):
         client = self.app.client_manager.tackerclient
         subsc = client.create_lccn_subscription(
-            jsonfile2body(parsed_args.create_request_file))
+            tacker_osc_utils.jsonfile2body(parsed_args.create_request_file))
         display_columns, columns = _get_columns(subsc)
         data = utils.get_item_properties(sdk_utils.DictModel(subsc),
                                          columns, formatters=_FORMATTERS,
diff --git a/tackerclient/osc/v2/vnffm/vnffm_sub.py b/tackerclient/osc/v2/vnffm/vnffm_sub.py
index 5fcb9bb0..bc2e18a6 100644
--- a/tackerclient/osc/v2/vnffm/vnffm_sub.py
+++ b/tackerclient/osc/v2/vnffm/vnffm_sub.py
@@ -13,9 +13,7 @@
 #    License for the specific language governing permissions and limitations
 #    under the License.
 
-import json
 import logging
-import os
 
 from osc_lib.command import command
 from osc_lib import utils
@@ -56,32 +54,6 @@ def _get_columns(vnffm_sub_obj):
         vnffm_sub_obj, column_map)
 
 
-def jsonfile2body(file_path):
-
-    if file_path is None:
-        msg = _("File %s does not exist")
-        reason = msg % file_path
-        raise exceptions.InvalidInput(reason=reason)
-
-    if os.access(file_path, os.R_OK) is False:
-        msg = _("User does not have read privileges to it")
-        raise exceptions.InvalidInput(reason=msg)
-
-    try:
-        with open(file_path) as f:
-            body = json.load(f)
-    except (IOError, ValueError) as ex:
-        msg = _("Failed to load parameter file. Error: %s")
-        reason = msg % ex
-        raise exceptions.InvalidInput(reason=reason)
-
-    if not body:
-        reason = _('The parameter file is empty')
-        raise exceptions.EmptyInput(reason=reason)
-
-    return body
-
-
 class CreateVnfFmSub(command.ShowOne):
     _description = _("Create a new VNF FM subscription")
 
@@ -97,7 +69,7 @@ class CreateVnfFmSub(command.ShowOne):
     def take_action(self, parsed_args):
         client = self.app.client_manager.tackerclient
         vnf_fm_sub = client.create_vnf_fm_sub(
-            jsonfile2body(parsed_args.request_file))
+            tacker_osc_utils.jsonfile2body(parsed_args.request_file))
         display_columns, columns = _get_columns(vnf_fm_sub)
         data = utils.get_item_properties(
             sdk_utils.DictModel(vnf_fm_sub), columns,
diff --git a/tackerclient/osc/v2/vnfpm/samples/create_vnf_pm_threshold_param_sample.json b/tackerclient/osc/v2/vnfpm/samples/create_vnf_pm_threshold_param_sample.json
new file mode 100644
index 00000000..2a227b0c
--- /dev/null
+++ b/tackerclient/osc/v2/vnfpm/samples/create_vnf_pm_threshold_param_sample.json
@@ -0,0 +1,40 @@
+{
+    "objectType": "Vnfc",
+    "objectInstanceId": "object-instance-id-1",
+    "subObjectInstanceIds": [
+        "sub-object-instance-id-2"
+    ],
+    "criteria": {
+        "performanceMetric": "VCpuUsageMeanVnf.object-instance-id-1",
+        "thresholdType": "SIMPLE",
+        "simpleThresholdDetails": {
+            "thresholdValue": 400.5,
+            "hysteresis": 10.3
+        }
+    },
+    "callbackUri": "/nfvo/notify/threshold",
+    "authentication": {
+        "authType": [
+            "BASIC",
+            "OAUTH2_CLIENT_CREDENTIALS",
+            "OAUTH2_CLIENT_CERT"
+        ],
+        "paramsBasic": {
+            "userName": "nfvo",
+            "password": "nfvopwd"
+        },
+        "paramsOauth2ClientCredentials": {
+            "clientId": "auth_user_name",
+            "clientPassword": "auth_password",
+            "tokenEndpoint": "token_endpoint"
+        },
+        "paramsOauth2ClientCert": {
+            "clientId": "test",
+            "certificateRef": {
+                "type": "x5t#256",
+                "value": "03c6e188d1fe5d3da8c9bc9a8dc531a2b3e"
+            },
+            "tokenEndpoint": "http://127.0.0.1/token"
+        }
+    }
+}
diff --git a/tackerclient/osc/v2/vnfpm/samples/update_vnf_pm_threshold_param_sample.json b/tackerclient/osc/v2/vnfpm/samples/update_vnf_pm_threshold_param_sample.json
new file mode 100644
index 00000000..81ddd682
--- /dev/null
+++ b/tackerclient/osc/v2/vnfpm/samples/update_vnf_pm_threshold_param_sample.json
@@ -0,0 +1,27 @@
+{
+    "callbackUri": "/nfvo/notify/threshold",
+    "authentication": {
+        "authType": [
+            "BASIC",
+            "OAUTH2_CLIENT_CREDENTIALS",
+            "OAUTH2_CLIENT_CERT"
+        ],
+        "paramsBasic": {
+            "userName": "nfvo",
+            "password": "nfvopwd"
+        },
+        "paramsOauth2ClientCredentials": {
+            "clientId": "auth_user_name",
+            "clientPassword": "auth_password",
+            "tokenEndpoint": "token_endpoint"
+        },
+        "paramsOauth2ClientCert": {
+            "clientId": "test",
+            "certificateRef": {
+                "type": "x5t#256",
+                "value": "03c6e188d1fe5d3da8c9bc9a8dc531a2b3e"
+            },
+            "tokenEndpoint": "http://127.0.0.1/token"
+        }
+    }
+}
diff --git a/tackerclient/osc/v2/vnfpm/vnfpm_job.py b/tackerclient/osc/v2/vnfpm/vnfpm_job.py
index ff9c8334..d7eae498 100644
--- a/tackerclient/osc/v2/vnfpm/vnfpm_job.py
+++ b/tackerclient/osc/v2/vnfpm/vnfpm_job.py
@@ -13,9 +13,7 @@
 #    License for the specific language governing permissions and limitations
 #    under the License.
 
-import json
 import logging
-import os
 
 from functools import reduce
 from osc_lib.command import command
@@ -78,32 +76,6 @@ def _get_columns(vnfpm_job_obj, action=None):
         vnfpm_job_obj, column_map)
 
 
-def jsonfile2body(file_path):
-
-    if file_path is None:
-        msg = _("File %s does not exist")
-        reason = msg % file_path
-        raise exceptions.InvalidInput(reason=reason)
-
-    if os.access(file_path, os.R_OK) is False:
-        msg = _("User does not have read privileges to it")
-        raise exceptions.InvalidInput(reason=msg)
-
-    try:
-        with open(file_path) as f:
-            body = json.load(f)
-    except (IOError, ValueError) as ex:
-        msg = _("Failed to load parameter file. Error: %s")
-        reason = msg % ex
-        raise exceptions.InvalidInput(reason=reason)
-
-    if not body:
-        reason = _('The parameter file is empty')
-        raise exceptions.EmptyInput(reason=reason)
-
-    return body
-
-
 class CreateVnfPmJob(command.ShowOne):
     _description = _("Create a new VNF PM job")
 
@@ -119,7 +91,7 @@ class CreateVnfPmJob(command.ShowOne):
     def take_action(self, parsed_args):
         client = self.app.client_manager.tackerclient
         vnf_pm_job = client.create_vnf_pm_job(
-            jsonfile2body(parsed_args.request_file))
+            tacker_osc_utils.jsonfile2body(parsed_args.request_file))
         display_columns, columns = _get_columns(vnf_pm_job)
         data = utils.get_item_properties(
             sdk_utils.DictModel(vnf_pm_job), columns,
@@ -284,7 +256,7 @@ class UpdateVnfPmJob(command.ShowOne):
         client = self.app.client_manager.tackerclient
         updated_values = client.update_vnf_pm_job(
             parsed_args.vnf_pm_job_id,
-            jsonfile2body(parsed_args.request_file))
+            tacker_osc_utils.jsonfile2body(parsed_args.request_file))
         display_columns, columns = _get_columns(updated_values, 'update')
         data = utils.get_item_properties(
             sdk_utils.DictModel(updated_values),
diff --git a/tackerclient/osc/v2/vnfpm/vnfpm_threshold.py b/tackerclient/osc/v2/vnfpm/vnfpm_threshold.py
new file mode 100644
index 00000000..57c71a8e
--- /dev/null
+++ b/tackerclient/osc/v2/vnfpm/vnfpm_threshold.py
@@ -0,0 +1,216 @@
+# Copyright (C) 2023 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 logging
+
+from osc_lib.command import command
+from osc_lib import utils
+
+from tackerclient.common import exceptions
+from tackerclient.i18n import _
+from tackerclient.osc import sdk_utils
+from tackerclient.osc import utils as tacker_osc_utils
+
+LOG = logging.getLogger(__name__)
+
+_ATTR_MAP = (
+    ('id', 'ID', tacker_osc_utils.LIST_BOTH),
+    ('objectType', 'Object Type', tacker_osc_utils.LIST_BOTH),
+    ('_links', 'Links', tacker_osc_utils.LIST_BOTH)
+)
+
+_FORMATTERS = {
+    'subObjectInstanceIds': tacker_osc_utils.FormatComplexDataColumn,
+    'criteria': tacker_osc_utils.FormatComplexDataColumn,
+    '_links': tacker_osc_utils.FormatComplexDataColumn
+}
+
+_MIXED_CASE_FIELDS = (
+    'objectType', 'objectInstanceId', 'subObjectInstanceIds', 'callbackUri'
+)
+
+_MIXED_CASE_FIELDS_UPDATE = (
+    'callbackUri'
+)
+
+_VNF_PM_THRESHOLD_ID = 'vnf_pm_threshold_id'
+
+
+def _get_columns(vnf_pm_threshold, action=None):
+    if action == 'update':
+        column_map = {
+            'callbackUri': 'Callback Uri'
+        }
+    else:
+        column_map = {
+            'id': 'ID',
+            'objectType': 'Object Type',
+            'objectInstanceId': 'Object Instance Id',
+            'subObjectInstanceIds': 'Sub Object Instance Ids',
+            'criteria': 'Criteria',
+            'callbackUri': 'Callback Uri',
+            '_links': 'Links'
+        }
+    return sdk_utils.get_osc_show_columns_for_sdk_resource(
+        vnf_pm_threshold, column_map)
+
+
+class CreateVnfPmThreshold(command.ShowOne):
+    _description = _("Create a new VNF PM threshold")
+
+    def get_parser(self, prog_name):
+        LOG.debug('get_parser(%s)', prog_name)
+        parser = super(CreateVnfPmThreshold, self).get_parser(prog_name)
+        parser.add_argument(
+            'request_file',
+            metavar="<param-file>",
+            help=_('Specify create VNF PM threshold request '
+                   'parameters in a json file.'))
+        return parser
+
+    def take_action(self, parsed_args):
+        client = self.app.client_manager.tackerclient
+        vnf_pm_threshold = client.create_vnf_pm_threshold(
+            tacker_osc_utils.jsonfile2body(parsed_args.request_file))
+        display_columns, columns = _get_columns(vnf_pm_threshold)
+        data = utils.get_item_properties(
+            sdk_utils.DictModel(vnf_pm_threshold), columns,
+            formatters=_FORMATTERS, mixed_case_fields=_MIXED_CASE_FIELDS)
+        return (display_columns, data)
+
+
+class ListVnfPmThreshold(command.Lister):
+    _description = _("List VNF PM thresholds")
+
+    def get_parser(self, prog_name):
+        LOG.debug('get_parser(%s)', prog_name)
+        parser = super(ListVnfPmThreshold, self).get_parser(prog_name)
+        parser.add_argument(
+            "--filter",
+            metavar="<filter>",
+            help=_("Attribute-based-filtering parameters"),
+        )
+        return parser
+
+    def take_action(self, parsed_args):
+        _params = {}
+
+        if parsed_args.filter:
+            _params['filter'] = parsed_args.filter
+
+        client = self.app.client_manager.tackerclient
+        data = client.list_vnf_pm_thresholds(**_params)
+        headers, columns = tacker_osc_utils.get_column_definitions(
+            _ATTR_MAP, long_listing=True)
+        return (headers,
+                (utils.get_dict_properties(
+                    s, columns, formatters=_FORMATTERS,
+                    mixed_case_fields=_MIXED_CASE_FIELDS,
+                ) for s in data['vnf_pm_thresholds']))
+
+
+class ShowVnfPmThreshold(command.ShowOne):
+    _description = _("Display VNF PM threshold details")
+
+    def get_parser(self, prog_name):
+        LOG.debug('get_parser(%s)', prog_name)
+        parser = super(ShowVnfPmThreshold, self).get_parser(prog_name)
+        parser.add_argument(
+            _VNF_PM_THRESHOLD_ID,
+            metavar="<vnf-pm-threshold-id>",
+            help=_("VNF PM threshold ID to display"))
+        return parser
+
+    def take_action(self, parsed_args):
+        client = self.app.client_manager.tackerclient
+        obj = client.show_vnf_pm_threshold(parsed_args.vnf_pm_threshold_id)
+        display_columns, columns = _get_columns(obj)
+        data = utils.get_item_properties(
+            sdk_utils.DictModel(obj), columns,
+            mixed_case_fields=_MIXED_CASE_FIELDS,
+            formatters=_FORMATTERS)
+        return (display_columns, data)
+
+
+class UpdateVnfPmThreshold(command.ShowOne):
+    _description = _("Update information about an individual VNF PM threshold")
+
+    def get_parser(self, prog_name):
+        LOG.debug('get_parser(%s)', prog_name)
+        parser = super(UpdateVnfPmThreshold, self).get_parser(prog_name)
+        parser.add_argument(
+            _VNF_PM_THRESHOLD_ID,
+            metavar="<vnf-pm-threshold-id>",
+            help=_("VNF PM threshold ID to update.")
+        )
+        parser.add_argument(
+            'request_file',
+            metavar="<param-file>",
+            help=_('Specify update PM threshold request '
+                   'parameters in a json file.'))
+        return parser
+
+    def take_action(self, parsed_args):
+        client = self.app.client_manager.tackerclient
+        updated_values = client.update_vnf_pm_threshold(
+            parsed_args.vnf_pm_threshold_id,
+            tacker_osc_utils.jsonfile2body(parsed_args.request_file))
+        display_columns, columns = _get_columns(updated_values, 'update')
+        data = utils.get_item_properties(
+            sdk_utils.DictModel(updated_values), columns,
+            mixed_case_fields=_MIXED_CASE_FIELDS_UPDATE)
+        return (display_columns, data)
+
+
+class DeleteVnfPmThreshold(command.Command):
+    _description = _("Delete VNF PM threshold")
+
+    def get_parser(self, prog_name):
+        LOG.debug('get_parser(%s)', prog_name)
+        parser = super(DeleteVnfPmThreshold, self).get_parser(prog_name)
+        parser.add_argument(
+            _VNF_PM_THRESHOLD_ID,
+            metavar="<vnf-pm-threshold-id>",
+            nargs="+",
+            help=_("VNF PM threshold ID(s) to delete"))
+        return parser
+
+    def take_action(self, parsed_args):
+        error_count = 0
+        client = self.app.client_manager.tackerclient
+        vnf_pm_threshold_ids = parsed_args.vnf_pm_threshold_id
+
+        for threshold_id in vnf_pm_threshold_ids:
+            try:
+                client.delete_vnf_pm_threshold(threshold_id)
+            except Exception as e:
+                error_count += 1
+                LOG.error(_("Failed to delete VNF PM threshold with "
+                            "ID '%(threshold_id)s': %(e)s"),
+                          {'threshold_id': threshold_id, 'e': e})
+
+        total = len(vnf_pm_threshold_ids)
+        if error_count > 0:
+            msg = (_("Failed to delete %(error_count)s of %(total)s "
+                     "VNF PM thresholds.") %
+                   {'error_count': error_count, 'total': total})
+            raise exceptions.CommandError(message=msg)
+
+        if total > 1:
+            print(_('All specified VNF PM thresholds are deleted '
+                    'successfully'))
+            return
+        print(_("VNF PM threshold '%s' deleted "
+                "successfully") % vnf_pm_threshold_ids[0])
diff --git a/tackerclient/tests/unit/osc/v1/test_vnflcm.py b/tackerclient/tests/unit/osc/v1/test_vnflcm.py
index b08dbb1b..d5798fed 100644
--- a/tackerclient/tests/unit/osc/v1/test_vnflcm.py
+++ b/tackerclient/tests/unit/osc/v1/test_vnflcm.py
@@ -297,9 +297,9 @@ class TestInstantiateVnfLcm(TestVnfLcm):
                                self.instantiate_vnf_lcm.take_action,
                                parsed_args)
 
-        expected_msg = ("Invalid input: File %s does not exist "
-                        "or user does not have read privileges to it")
-        self.assertEqual(expected_msg % sample_param_file, str(ex))
+        expected_msg = ("Invalid input: "
+                        "User does not have read privileges to it")
+        self.assertEqual(expected_msg, str(ex))
 
     @mock.patch("os.open")
     @mock.patch("os.access")
@@ -403,9 +403,9 @@ class TestHealVnfLcm(TestVnfLcm):
         ex = self.assertRaises(exceptions.InvalidInput,
                                self.heal_vnf_lcm.take_action, parsed_args)
 
-        expected_msg = ("Invalid input: File %s does not exist "
-                        "or user does not have read privileges to it")
-        self.assertEqual(expected_msg % sample_param_file, str(ex))
+        expected_msg = ("Invalid input: "
+                        "User does not have read privileges to it")
+        self.assertEqual(expected_msg, str(ex))
 
 
 @ddt.ddt
@@ -774,9 +774,9 @@ class TestScaleVnfLcm(TestVnfLcm):
         ex = self.assertRaises(exceptions.InvalidInput,
                                self.scale_vnf_lcm.take_action, parsed_args)
 
-        expected_msg = ("Invalid input: File %s does not exist "
-                        "or user does not have read privileges to it")
-        self.assertEqual(expected_msg % sample_param_file, str(ex))
+        expected_msg = ("Invalid input: "
+                        "User does not have read privileges to it")
+        self.assertEqual(expected_msg, str(ex))
 
     @ddt.data('SCALE_IN', 'SCALE_OUT')
     def test_take_action_vnf_instance_not_found(self, scale_type):
@@ -888,9 +888,9 @@ class TestChangeExtConnVnfLcm(TestVnfLcm):
             self.change_ext_conn_vnf_lcm.take_action,
             parsed_args)
 
-        expected_msg = ("Invalid input: File %s does not exist "
-                        "or user does not have read privileges to it")
-        self.assertEqual(expected_msg % sample_param_file, str(ex))
+        expected_msg = ("Invalid input: "
+                        "User does not have read privileges to it")
+        self.assertEqual(expected_msg, str(ex))
 
     @mock.patch("os.open")
     @mock.patch("os.access")
diff --git a/tackerclient/tests/unit/osc/v2/test_vnflcm.py b/tackerclient/tests/unit/osc/v2/test_vnflcm.py
index 825b9b37..f19d31e0 100644
--- a/tackerclient/tests/unit/osc/v2/test_vnflcm.py
+++ b/tackerclient/tests/unit/osc/v2/test_vnflcm.py
@@ -121,9 +121,9 @@ class TestChangeVnfPkgVnfLcm(test_vnflcm.TestVnfLcm):
             self.change_vnfpkg_vnf_lcm.take_action,
             parsed_args)
 
-        expected_msg = ("Invalid input: File %s does not exist "
-                        "or user does not have read privileges to it")
-        self.assertEqual(expected_msg % sample_param_file, str(ex))
+        expected_msg = ("Invalid input: "
+                        "User does not have read privileges to it")
+        self.assertEqual(expected_msg, str(ex))
 
     @mock.patch("os.open")
     @mock.patch("os.access")
diff --git a/tackerclient/tests/unit/osc/v2/test_vnfpm_threshold.py b/tackerclient/tests/unit/osc/v2/test_vnfpm_threshold.py
new file mode 100644
index 00000000..53cc8270
--- /dev/null
+++ b/tackerclient/tests/unit/osc/v2/test_vnfpm_threshold.py
@@ -0,0 +1,398 @@
+# Copyright (C) 2023 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 sys
+
+from io import StringIO
+from oslo_utils.fixture import uuidsentinel
+from unittest import mock
+
+from tackerclient.common import exceptions
+from tackerclient.osc import utils as tacker_osc_utils
+from tackerclient.osc.v2.vnfpm import vnfpm_threshold
+from tackerclient.tests.unit.osc import base
+from tackerclient.tests.unit.osc.v1.fixture_data import client
+from tackerclient.tests.unit.osc.v2 import vnfpm_threshold_fakes
+
+
+class TestVnfPmThreshold(base.FixturedTestCase):
+    client_fixture_class = client.ClientFixture
+
+    def setUp(self):
+        super(TestVnfPmThreshold, self).setUp()
+        self.url = client.TACKER_URL
+        self.header = {'content-type': 'application/json'}
+        self.app = mock.Mock()
+        self.app_args = mock.Mock()
+        self.client_manager = self.cs
+        self.app.client_manager.tackerclient = self.client_manager
+
+
+def _get_columns_vnfpm_threshold(action=None):
+    if action == 'update':
+        columns = ['Callback Uri']
+    else:
+        columns = ['ID', 'Object Type', 'Object Instance Id',
+                   'Sub Object Instance Ids', 'Criteria', 'Callback Uri',
+                   'Links']
+    if action == 'list':
+        columns = [
+            'ID', 'Object Type', 'Links'
+        ]
+    return columns
+
+
+@ddt.ddt
+class TestCreateVnfPmThreshold(TestVnfPmThreshold):
+
+    def setUp(self):
+        super(TestCreateVnfPmThreshold, self).setUp()
+        self.create_vnf_pm_threshold = vnfpm_threshold.CreateVnfPmThreshold(
+            self.app, self.app_args, cmd_name='vnfpm threshold create')
+
+    def test_create_no_args(self):
+        self.assertRaises(base.ParserException, self.check_parser,
+                          self.create_vnf_pm_threshold, [], [])
+
+    @ddt.unpack
+    def test_take_action(self):
+        param_file = ("./tackerclient/osc/v2/vnfpm/samples/"
+                      "create_vnf_pm_threshold_param_sample.json")
+
+        arg_list = [param_file]
+        verify_list = [('request_file', param_file)]
+
+        parsed_args = self.check_parser(self.create_vnf_pm_threshold, arg_list,
+                                        verify_list)
+
+        response_json = vnfpm_threshold_fakes.vnf_pm_threshold_response()
+        self.requests_mock.register_uri(
+            'POST', os.path.join(self.url, 'vnfpm/v2/thresholds'),
+            json=response_json, headers=self.header)
+
+        actual_columns, data = (
+            self.create_vnf_pm_threshold.take_action(parsed_args))
+        self.assertCountEqual(_get_columns_vnfpm_threshold(),
+                              actual_columns)
+
+        _, attributes = vnfpm_threshold._get_columns(response_json)
+        expected_data = vnfpm_threshold_fakes.get_vnfpm_threshold_data(
+            response_json, columns=attributes)
+        self.assertListItemsEqual(expected_data, data)
+
+
+@ddt.ddt
+class TestListVnfPmThreshold(TestVnfPmThreshold):
+
+    def setUp(self):
+        super(TestListVnfPmThreshold, self).setUp()
+        self.list_vnf_pm_thresholds = vnfpm_threshold.ListVnfPmThreshold(
+            self.app, self.app_args, cmd_name='vnfpm threshold list')
+
+    def test_take_action(self):
+        vnf_pm_threshold_objs = vnfpm_threshold_fakes.create_vnf_pm_thresholds(
+            count=3)
+        parsed_args = self.check_parser(self.list_vnf_pm_thresholds, [], [])
+        self.requests_mock.register_uri(
+            'GET',
+            os.path.join(self.url, 'vnfpm/v2/thresholds'),
+            json=vnf_pm_threshold_objs, headers=self.header)
+
+        actual_columns, data = self.list_vnf_pm_thresholds.take_action(
+            parsed_args)
+
+        _, columns = tacker_osc_utils.get_column_definitions(
+            vnfpm_threshold._ATTR_MAP, long_listing=True)
+
+        expected_data = []
+        for vnf_pm_threshold_obj_idx in vnf_pm_threshold_objs:
+            expected_data.append(
+                vnfpm_threshold_fakes.get_vnfpm_threshold_data(
+                    vnf_pm_threshold_obj_idx, columns=columns))
+
+        self.assertCountEqual(_get_columns_vnfpm_threshold(action='list'),
+                              actual_columns)
+        self.assertCountEqual(expected_data, list(data))
+
+    def test_take_action_with_filter(self):
+        vnf_pm_threshold_objs = vnfpm_threshold_fakes.create_vnf_pm_thresholds(
+            count=3)
+        parsed_args = self.check_parser(
+            self.list_vnf_pm_thresholds,
+            ["--filter", '(eq,perceivedSeverity,WARNING)'],
+            [('filter', '(eq,perceivedSeverity,WARNING)')])
+        self.requests_mock.register_uri(
+            'GET', os.path.join(
+                self.url,
+                'vnfpm/v2/thresholds?filter=(eq,perceivedSeverity,WARNING)'),
+            json=vnf_pm_threshold_objs, headers=self.header)
+
+        actual_columns, data = self.list_vnf_pm_thresholds.take_action(
+            parsed_args)
+
+        _, columns = tacker_osc_utils.get_column_definitions(
+            vnfpm_threshold._ATTR_MAP, long_listing=True)
+
+        expected_data = []
+        for vnf_pm_threshold_obj_idx in vnf_pm_threshold_objs:
+            expected_data.append(
+                vnfpm_threshold_fakes.get_vnfpm_threshold_data(
+                    vnf_pm_threshold_obj_idx, columns=columns))
+
+        self.assertCountEqual(_get_columns_vnfpm_threshold(action='list'),
+                              actual_columns)
+        self.assertListItemsEqual(expected_data, list(data))
+
+    def test_take_action_with_incorrect_filter(self):
+        parsed_args = self.check_parser(
+            self.list_vnf_pm_thresholds,
+            ["--filter", '(perceivedSeverity)'],
+            [('filter', '(perceivedSeverity)')])
+
+        url = os.path.join(
+            self.url, 'vnfpm/v2/thresholds?filter=(perceivedSeverity)')
+
+        self.requests_mock.register_uri(
+            'GET', url, headers=self.header, status_code=400, json={})
+
+        self.assertRaises(exceptions.TackerClientException,
+                          self.list_vnf_pm_thresholds.take_action,
+                          parsed_args)
+
+    def test_take_action_internal_server_error(self):
+        parsed_args = self.check_parser(
+            self.list_vnf_pm_thresholds,
+            ["--filter", '(eq,perceivedSeverity,WARNING)'],
+            [('filter', '(eq,perceivedSeverity,WARNING)')])
+
+        url = os.path.join(
+            self.url,
+            'vnfpm/v2/thresholds?filter=(eq,perceivedSeverity,WARNING)')
+
+        self.requests_mock.register_uri(
+            'GET', url, headers=self.header, status_code=500, json={})
+
+        self.assertRaises(exceptions.TackerClientException,
+                          self.list_vnf_pm_thresholds.take_action,
+                          parsed_args)
+
+
+class TestShowVnfPmThreshold(TestVnfPmThreshold):
+
+    def setUp(self):
+        super(TestShowVnfPmThreshold, self).setUp()
+        self.show_vnf_pm_thresholds = vnfpm_threshold.ShowVnfPmThreshold(
+            self.app, self.app_args, cmd_name='vnfpm threshold show')
+
+    def test_take_action(self):
+        vnfpm_threshold_obj = vnfpm_threshold_fakes.vnf_pm_threshold_response()
+
+        arg_list = [vnfpm_threshold_obj['id']]
+        verify_list = [('vnf_pm_threshold_id', vnfpm_threshold_obj['id'])]
+
+        # command param
+        parsed_args = self.check_parser(
+            self.show_vnf_pm_thresholds, arg_list, verify_list)
+        url = os.path.join(
+            self.url, 'vnfpm/v2/thresholds', vnfpm_threshold_obj['id'])
+
+        self.requests_mock.register_uri(
+            'GET', url, headers=self.header, json=vnfpm_threshold_obj)
+
+        columns, data = (self.show_vnf_pm_thresholds.take_action(parsed_args))
+
+        self.assertCountEqual(_get_columns_vnfpm_threshold('show'), columns)
+
+        _, attributes = vnfpm_threshold._get_columns(vnfpm_threshold_obj)
+        self.assertListItemsEqual(
+            vnfpm_threshold_fakes.get_vnfpm_threshold_data(
+                vnfpm_threshold_obj, columns=attributes), data)
+
+    def test_take_action_vnf_pm_threshold_id_not_found(self):
+        arg_list = [uuidsentinel.vnf_pm_threshold_id]
+        verify_list = [('vnf_pm_threshold_id',
+                        uuidsentinel.vnf_pm_threshold_id)]
+
+        # command param
+        parsed_args = self.check_parser(
+            self.show_vnf_pm_thresholds, arg_list, verify_list)
+
+        url = os.path.join(
+            self.url, 'vnfpm/v2/thresholds',
+            uuidsentinel.vnf_pm_threshold_id)
+        self.requests_mock.register_uri(
+            'GET', url, headers=self.header, status_code=404, json={})
+
+        self.assertRaises(exceptions.TackerClientException,
+                          self.show_vnf_pm_thresholds.take_action,
+                          parsed_args)
+
+    def test_take_action_internal_server_error(self):
+        arg_list = [uuidsentinel.vnf_pm_threshold_id]
+        verify_list = [('vnf_pm_threshold_id',
+                        uuidsentinel.vnf_pm_threshold_id)]
+
+        # command param
+        parsed_args = self.check_parser(
+            self.show_vnf_pm_thresholds, arg_list, verify_list)
+
+        url = os.path.join(
+            self.url, 'vnfpm/v2/thresholds',
+            uuidsentinel.vnf_pm_threshold_id)
+        self.requests_mock.register_uri(
+            'GET', url, headers=self.header, status_code=500, json={})
+
+        self.assertRaises(exceptions.TackerClientException,
+                          self.show_vnf_pm_thresholds.take_action,
+                          parsed_args)
+
+
+@ddt.ddt
+class TestUpdateVnfPmThreshold(TestVnfPmThreshold):
+
+    def setUp(self):
+        super(TestUpdateVnfPmThreshold, self).setUp()
+        self.update_vnf_pm_threshold = vnfpm_threshold.UpdateVnfPmThreshold(
+            self.app, self.app_args, cmd_name='vnfpm threshold update')
+
+    def test_take_action(self):
+        param_file = ("./tackerclient/osc/v2/vnfpm/samples/"
+                      "update_vnf_pm_threshold_param_sample.json")
+        arg_list = [uuidsentinel.vnf_pm_threshold_id, param_file]
+        verify_list = [
+            ('vnf_pm_threshold_id', uuidsentinel.vnf_pm_threshold_id),
+            ('request_file', param_file)
+        ]
+        vnfpm_threshold_obj = vnfpm_threshold_fakes.vnf_pm_threshold_response(
+            None, 'update')
+
+        # command param
+        parsed_args = self.check_parser(
+            self.update_vnf_pm_threshold, arg_list, verify_list)
+        url = os.path.join(
+            self.url, 'vnfpm/v2/thresholds',
+            uuidsentinel.vnf_pm_threshold_id)
+        self.requests_mock.register_uri(
+            'PATCH', url, headers=self.header, json=vnfpm_threshold_obj)
+
+        actual_columns, data = (
+            self.update_vnf_pm_threshold.take_action(parsed_args))
+        expected_columns = _get_columns_vnfpm_threshold(action='update')
+        self.assertCountEqual(expected_columns, actual_columns)
+
+        _, columns = vnfpm_threshold._get_columns(
+            vnfpm_threshold_obj, action='update')
+        expected_data = vnfpm_threshold_fakes.get_vnfpm_threshold_data(
+            vnfpm_threshold_obj, columns=columns)
+        self.assertEqual(expected_data, data)
+
+    def test_take_action_vnf_pm_threshold_id_not_found(self):
+        param_file = ("./tackerclient/osc/v2/vnfpm/samples/"
+                      "update_vnf_pm_threshold_param_sample.json")
+        arg_list = [uuidsentinel.vnf_pm_threshold_id, param_file]
+        verify_list = [
+            ('vnf_pm_threshold_id', uuidsentinel.vnf_pm_threshold_id),
+            ('request_file', param_file)
+        ]
+
+        # command param
+        parsed_args = self.check_parser(
+            self.update_vnf_pm_threshold, arg_list, verify_list)
+        url = os.path.join(
+            self.url, 'vnfpm/v2/thresholds',
+            uuidsentinel.vnf_pm_threshold_id)
+        self.requests_mock.register_uri(
+            'PATCH', url, headers=self.header, status_code=404, json={})
+        self.assertRaises(exceptions.TackerClientException,
+                          self.update_vnf_pm_threshold.take_action,
+                          parsed_args)
+
+
+class TestDeleteVnfPmThreshold(TestVnfPmThreshold):
+
+    def setUp(self):
+        super(TestDeleteVnfPmThreshold, self).setUp()
+        self.delete_vnf_pm_threshold = vnfpm_threshold.DeleteVnfPmThreshold(
+            self.app, self.app_args, cmd_name='vnfpm threshold delete')
+
+        # Vnf Pm threshold to delete
+        self.vnf_pm_thresholds = (
+            vnfpm_threshold_fakes.create_vnf_pm_thresholds(count=3))
+
+    def _mock_request_url_for_delete(self, index):
+        url = os.path.join(self.url, 'vnfpm/v2/thresholds',
+                           self.vnf_pm_thresholds[index]['id'])
+
+        self.requests_mock.register_uri('DELETE', url,
+                                        headers=self.header, json={})
+
+    def test_delete_one_vnf_pm_threshold(self):
+        arg_list = [self.vnf_pm_thresholds[0]['id']]
+        verify_list = [('vnf_pm_threshold_id',
+                       [self.vnf_pm_thresholds[0]['id']])]
+
+        parsed_args = self.check_parser(self.delete_vnf_pm_threshold, arg_list,
+                                        verify_list)
+
+        self._mock_request_url_for_delete(0)
+        sys.stdout = buffer = StringIO()
+        result = self.delete_vnf_pm_threshold.take_action(parsed_args)
+        self.assertIsNone(result)
+        self.assertEqual(
+            (f"VNF PM threshold '{self.vnf_pm_thresholds[0]['id']}' "
+             f"deleted successfully"), buffer.getvalue().strip())
+
+    def test_delete_multiple_vnf_pm_threshold(self):
+        arg_list = []
+        for obj in self.vnf_pm_thresholds:
+            arg_list.append(obj['id'])
+        verify_list = [('vnf_pm_threshold_id', arg_list)]
+        parsed_args = self.check_parser(self.delete_vnf_pm_threshold, arg_list,
+                                        verify_list)
+        for i in range(0, len(self.vnf_pm_thresholds)):
+            self._mock_request_url_for_delete(i)
+        sys.stdout = buffer = StringIO()
+        result = self.delete_vnf_pm_threshold.take_action(parsed_args)
+        self.assertIsNone(result)
+        self.assertEqual('All specified VNF PM thresholds are deleted '
+                         'successfully', buffer.getvalue().strip())
+
+    def test_delete_multiple_vnf_pm_threshold_exception(self):
+        arg_list = [
+            self.vnf_pm_thresholds[0]['id'],
+            'xxxx-yyyy-zzzz',
+            self.vnf_pm_thresholds[1]['id'],
+        ]
+        verify_list = [('vnf_pm_threshold_id', arg_list)]
+        parsed_args = self.check_parser(self.delete_vnf_pm_threshold,
+                                        arg_list, verify_list)
+
+        self._mock_request_url_for_delete(0)
+
+        url = os.path.join(self.url, 'vnfpm/v2/thresholds',
+                           'xxxx-yyyy-zzzz')
+        self.requests_mock.register_uri(
+            'GET', url, exc=exceptions.ConnectionFailed)
+
+        self._mock_request_url_for_delete(1)
+        exception = self.assertRaises(exceptions.CommandError,
+                                      self.delete_vnf_pm_threshold.take_action,
+                                      parsed_args)
+
+        self.assertEqual(
+            f'Failed to delete 1 of {len(self.vnf_pm_thresholds)} '
+            'VNF PM thresholds.', exception.message)
diff --git a/tackerclient/tests/unit/osc/v2/vnfpm_threshold_fakes.py b/tackerclient/tests/unit/osc/v2/vnfpm_threshold_fakes.py
new file mode 100644
index 00000000..ac2567ea
--- /dev/null
+++ b/tackerclient/tests/unit/osc/v2/vnfpm_threshold_fakes.py
@@ -0,0 +1,111 @@
+# Copyright (C) 2023 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
+
+from tackerclient.osc import utils as tacker_osc_utils
+
+
+def create_vnf_pm_thresholds(count=2):
+    """Create multiple fake vnf pm thresholds.
+
+    :param int count:
+        The number of vnf_pm_thresholds to fake
+    :return:
+        A list of fake vnf pm thresholds dictionary
+    """
+    vnf_pm_thresholds = []
+    for _ in range(0, count):
+        unique_id = uuidutils.generate_uuid()
+        vnf_pm_thresholds.append(vnf_pm_threshold_response(
+            attrs={'id': unique_id}))
+    return vnf_pm_thresholds
+
+
+def vnf_pm_threshold_response(attrs=None, action=None):
+    """Create a fake vnf pm threshold.
+
+    :param Dictionary attrs:
+        A dictionary with all attributes
+    :param String action:
+        The operation performed on threshold
+    :return:
+        A pm threshold dict
+    """
+    if action == 'update':
+        fake_vnf_pm_threshold = {
+            "callbackUri": "/nfvo/notify/threshold",
+        }
+        return fake_vnf_pm_threshold
+
+    attrs = attrs or {}
+    # Set default attributes.
+    fake_vnf_pm_threshold = {
+        "id": "2bb72d78-b1d9-48fe-8c64-332654ffeb5d",
+        "objectType": "Vnfc",
+        "objectInstanceId": "object-instance-id-1",
+        "subObjectInstanceIds": [
+            "sub-object-instance-id-2"
+        ],
+        "criteria": {
+            "performanceMetric": "VCpuUsageMeanVnf.object-instance-id-1",
+            "thresholdType": "SIMPLE",
+            "simpleThresholdDetails": {
+                "thresholdValue": 500.5,
+                "hysteresis": 10.5
+            }
+        },
+        "callbackUri": "/nfvo/notify/threshold",
+        "_links": {
+            "self": {
+                "href": "/vnfpm/v2/thresholds/"
+                        "78a39661-60a8-4824-b989-88c1b0c3534a"
+            },
+            "object": {
+                "href": "/vnflcm/v1/vnf_instances/"
+                        "0e5f3086-4e79-47ed-a694-54c29155fa26"
+            }
+        }
+    }
+
+    # Overwrite default attributes.
+    fake_vnf_pm_threshold.update(attrs)
+
+    return fake_vnf_pm_threshold
+
+
+def get_vnfpm_threshold_data(vnf_pm_threshold, columns=None):
+    """Get the vnfpm threshold.
+
+    :param Dictionary vnf_pm_threshold:
+        A dictionary with vnf_pm_threshold
+    :param List columns:
+        A list of column names
+    :return:
+        A tuple object sorted based on the name of the columns.
+    """
+    complex_attributes = ['subObjectInstanceIds',
+                          'criteria', '_links', 'authentication']
+
+    for attribute in complex_attributes:
+        if vnf_pm_threshold.get(attribute):
+            vnf_pm_threshold.update(
+                {attribute: tacker_osc_utils.FormatComplexDataColumn(
+                    vnf_pm_threshold[attribute])})
+
+    # return the list of data as per column order
+    if columns is None:
+        columns = sorted(vnf_pm_threshold.keys())
+    return tuple([vnf_pm_threshold[key] for key in columns])
diff --git a/tackerclient/v1_0/client.py b/tackerclient/v1_0/client.py
index 289359ae..77a384d1 100644
--- a/tackerclient/v1_0/client.py
+++ b/tackerclient/v1_0/client.py
@@ -1104,6 +1104,8 @@ class VnfPMClient(ClientBase):
     vnf_pm_jobs_path = '/vnfpm/v2/pm_jobs'
     vnf_pm_job_path = '/vnfpm/v2/pm_jobs/%s'
     vnf_pm_reports_path = '/vnfpm/v2/pm_jobs/%(job_id)s/reports/%(report_id)s'
+    vnf_pm_thresholds_path = '/vnfpm/v2/thresholds'
+    vnf_pm_threshold_path = '/vnfpm/v2/thresholds/%s'
 
     def build_action(self, action):
         return action
@@ -1143,6 +1145,35 @@ class VnfPMClient(ClientBase):
                 'job_id': vnf_pm_job_id, 'report_id': vnf_pm_report_id
             }, headers=self.headers)
 
+    @APIParamsCall
+    def create_vnf_pm_threshold(self, body):
+        return self.post(
+            self.vnf_pm_thresholds_path, body=body, headers=self.headers)
+
+    @APIParamsCall
+    def list_vnf_pm_thresholds(self, retrieve_all=True, **_params):
+        return self.list(
+            "vnf_pm_thresholds", self.vnf_pm_thresholds_path, retrieve_all,
+            headers=self.headers, **_params)
+
+    @APIParamsCall
+    def show_vnf_pm_threshold(self, vnf_pm_threshold_id):
+        return self.get(
+            self.vnf_pm_threshold_path % vnf_pm_threshold_id,
+            headers=self.headers)
+
+    @APIParamsCall
+    def update_vnf_pm_threshold(self, vnf_pm_threshold_id, body):
+        return self.patch(
+            self.vnf_pm_threshold_path % vnf_pm_threshold_id, body=body,
+            headers=self.headers)
+
+    @APIParamsCall
+    def delete_vnf_pm_threshold(self, vnf_pm_threshold_id):
+        return self.delete(
+            self.vnf_pm_threshold_path % vnf_pm_threshold_id,
+            headers=self.headers)
+
 
 class Client(object):
     """Unified interface to interact with multiple applications of tacker service.
@@ -1527,3 +1558,20 @@ class Client(object):
     def show_vnf_pm_report(self, vnf_pm_job_id, vnf_pm_report_id):
         return self.vnf_pm_client.show_vnf_pm_report(
             vnf_pm_job_id, vnf_pm_report_id)
+
+    def create_vnf_pm_threshold(self, body):
+        return self.vnf_pm_client.create_vnf_pm_threshold(body)
+
+    def list_vnf_pm_thresholds(self, retrieve_all=True, **_params):
+        return self.vnf_pm_client.list_vnf_pm_thresholds(
+            retrieve_all=retrieve_all, **_params)
+
+    def show_vnf_pm_threshold(self, vnf_pm_threshold_id):
+        return self.vnf_pm_client.show_vnf_pm_threshold(vnf_pm_threshold_id)
+
+    def update_vnf_pm_threshold(self, vnf_pm_threshold_id, body):
+        return self.vnf_pm_client.update_vnf_pm_threshold(
+            vnf_pm_threshold_id, body)
+
+    def delete_vnf_pm_threshold(self, vnf_pm_threshold_id):
+        return self.vnf_pm_client.delete_vnf_pm_threshold(vnf_pm_threshold_id)