diff --git a/.zuul.yaml b/.zuul.yaml index 201151245..69be24ff7 100644 --- a/.zuul.yaml +++ b/.zuul.yaml @@ -683,6 +683,38 @@ setup_multi_az: true controller_tacker_hostname: "{{ hostvars['controller-tacker']['ansible_hostname'] }}" +- job: + name: tacker-functional-devstack-enhanced-policy-sol + parent: tacker-functional-devstack-multinode-legacy + description: | + Enhanced policy job for SOL devstack-based functional tests + host-vars: + controller-tacker: + tox_envlist: dsvm-functional-enhanced-policy-sol + devstack_local_conf: + post-config: + $TACKER_CONF: + oslo_policy: + enhanced_tacker_policy: True + vars: + config_enhanced_policy: true + +- job: + name: tacker-functional-devstack-enhanced-policy-sol-kubernetes + parent: tacker-functional-devstack-multinode-sol-kubernetes-v2 + description: | + Enhanced policy job for SOL Kubernetes devstack-based functional tests + host-vars: + controller-tacker: + tox_envlist: dsvm-functional-enhanced-policy-sol-kubernetes + devstack_local_conf: + post-config: + $TACKER_CONF: + oslo_policy: + enhanced_tacker_policy: True + vars: + config_enhanced_policy: true + - job: name: tacker-compliance-devstack-multinode-sol parent: tacker-functional-devstack-multinode-legacy @@ -720,4 +752,6 @@ - tacker-functional-devstack-multinode-sol-kubernetes-multi-tenant - tacker-functional-devstack-kubernetes-oidc-auth - tacker-functional-devstack-multinode-sol-v2-az-retry + - tacker-functional-devstack-enhanced-policy-sol + - tacker-functional-devstack-enhanced-policy-sol-kubernetes - tacker-compliance-devstack-multinode-sol diff --git a/doc/source/user/enhanced_tacker_policy_usage_guide.rst b/doc/source/user/enhanced_tacker_policy_usage_guide.rst new file mode 100644 index 000000000..cdeac085f --- /dev/null +++ b/doc/source/user/enhanced_tacker_policy_usage_guide.rst @@ -0,0 +1,1436 @@ +================================== +Enhanced Tacker Policy Usage Guide +================================== + +Overview +-------- + +The default Tacker API policy only supports whether the user can access the +API, but does not determine whether the users can access the resource on which +the API call operates. + +Enhanced Tacker Policy enables Users to get finer-grained access control based +on user and VNF information for API resources. + +This document describes how to use Enhanced Tacker Policy in Tacker. + +Introduction to enhanced tacker attributes +------------------------------------------ + +Enhanced Tacker Policy function currently supports three enhanced attributes: +area, vendor, and tenant. + +* area: Area attribute is an area-region pair. The value of this attribute is a + string in the format of "area@region". This attribute describes the area + where VIM or VNF is located. +* vendor: Vendor attribute is the name of the vendor. It is defined in the + definition file of VNF package. VNF obtains this attribute from VNF package. +* tenant: Tenant attribute is the name of the tenant. Tacker Antelope version + only supports the namespace of CNF. The tenant of VNF will be supported in + future releases. + +Enable Enhanced Tacker Policy +----------------------------- + +Enhanced Tacker Policy is disabled by default in Tacker. +For it to work, user needs to find ``enhanced_tacker_policy`` in +``tacker.conf`` and change its value to ``True``. If not found, please add it +yourself. + +.. code-block:: console + + $ vi /etc/tacker/tacker.conf + ... + [oslo_policy] + enhanced_tacker_policy = True + ... + +Configure Enhanced Policy Rules +------------------------------- + +The oslo.policy [#oslo.policy]_ supports the function to compare API attributes +to object attributes. +Based on this function, Enhanced Tacker Policy function currently supports +three enhanced tacker attributes: area, vendor, and tenant. + +.. code-block:: yaml + + "get_vim" : "area:%(area)s" + "os_nfv_orchestration_api:vnf_packages:show" : "vendor:%(vendor)s" + "os_nfv_orchestration_api:vnf_instances:show" : "tenant:%(tenant)s" + +Take the area attribute as an example, the area string before the colon is an +API attribute, namely the area of the API user. It is compared with the area of +the object (in this case, a VNF instance). More precisely, it is compared with +the area field of that object in the database. If the two values are equal, +permission is granted. + +For the policy rule configuration used in this usage guide, please refer to the +`Sample policy.yaml file`_ chapter in the Appendix. + +Create user with special roles +------------------------------ + +Users need to define special roles with the following naming rules to represent +users with access rights to corresponding resources. For example, a user with a +role of ``VENDOR_company-a`` has permission to access resources whose vendor +attribute is ``company-a``. This is because in implementation, Tacker will +convert special roles into user attributes according to the following +conversion rules for attribute comparison. + +Special Roles' Naming Rules +~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +Enhanced Tacker Policy defines the following naming rules for special roles. +Admin user need to create special roles according to these naming rules, +otherwise Tacker will not correctly convert these special roles into user +attributes. + +#. The role name consists of three parts: prefix + "_" + [attribute + value/special value] +#. Supported prefixes, attribute values and special values are shown in the + following table: + + .. list-table:: + :widths: 10 14 12 50 + :header-rows: 1 + + * - Prefix + - Attribute value + - Special value + - Sample + * - AREA + - area value + - all@all, all@{region_value} + - AREA_tokyo@japan, AREA_all@all, AREA_all@japan + * - VENDOR + - vendor value + - all + - VENDOR_company_a, VENDOR_all + * - TENANT + - tenant value + - all + - TENANT_default, TENANT_all + + .. note:: + + As "all" is treated as a special value, the above attribute of resource + cannot use "all" as the attribute value. + +Conversion rules +~~~~~~~~~~~~~~~~ + +In Tacker implementation, Tacker converts these special roles into API +attributes and provide them to Tacker policy. The conversion follows the +following rules: + +#. For ordinary attribute values, they will be directly converted to user + attribute values. + + .. list-table:: + :widths: 10 14 50 + :header-rows: 1 + + * - Prefix + - Attribute Name + - Sample (special role -> user attribute value) + * - AREA + - area + - AREA_tokyo@japan -> {"area": ["tokyo@japan"]} + * - VENDOR + - vendor + - VENDOR_company-a -> {"vendor": ["company-a"]} + * - TENANT + - tenant value + - TENANT_default -> {"tenant": ["default"]} + +#. For special value in Enhanced Tacker Policy, the corresponding attribute + value of resource will be assigned to user. + + .. list-table:: + :widths: 10 14 14 50 + :header-rows: 1 + + * - Prefix + - Attribute Name + - Special Value + - Sample (resource attribute -> user attribute) + * - AREA + - area + - all@all + - {"area": "tokyo@japan"} -> {"area": ["tokyo@japan"]} + * - AREA + - area + - all@{region_value} + - same region value: + + .. code-block:: console + + {"area": "tokyo@japan"} -> {"area": ["tokyo@japan"]} + + different region value: + + .. code-block:: console + + any -> {"area": []} + + * - VENDOR + - vendor + - all + - {"vendor": "vendor_company-a"} -> {"vendor": ["company-a"]} + * - TENANT + - tenant value + - all + - {"tenant": "default"} -> {"tenant": ["default"]} + +Create users with special roles +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +In this user guide, user creates three end-users with admin user: + +* ``user-a`` with special roles is used as the experimental group. +* ``user-b`` without special roles is used as the control group. +* ``user-manager`` with special roles and ``manager`` role as manager. + +Create users +^^^^^^^^^^^^ + +#. Create the ``user-a`` user: + + .. code-block:: console + + $ openstack user create --project nfv --password devstack user-a + +---------------------+----------------------------------+ + | Field | Value | + +---------------------+----------------------------------+ + | default_project_id | 4cdc4e2efe144f87812677cfe224fffb | + | domain_id | default | + | enabled | True | + | id | 57dacae03a1a41eeb0eacf481863697a | + | name | user-a | + | options | {} | + | password_expires_at | None | + +---------------------+----------------------------------+ + +#. Create the ``user-b`` user: + + .. code-block:: console + + $ openstack user create --project nfv --password devstack user-b + +---------------------+----------------------------------+ + | Field | Value | + +---------------------+----------------------------------+ + | default_project_id | 4cdc4e2efe144f87812677cfe224fffb | + | domain_id | default | + | enabled | True | + | id | 3ce9d137090943e1a006392274d92f8a | + | name | user-b | + | options | {} | + | password_expires_at | None | + +---------------------+----------------------------------+ + +#. Create the ``user-manager`` user: + + .. code-block:: console + + $ openstack user create --project nfv --password devstack user-manager + +---------------------+----------------------------------+ + | Field | Value | + +---------------------+----------------------------------+ + | default_project_id | 4cdc4e2efe144f87812677cfe224fffb | + | domain_id | default | + | enabled | True | + | id | 53f085076d9d4324bbe8498d92aa5292 | + | name | user-manager | + | options | {} | + | password_expires_at | None | + +---------------------+----------------------------------+ + +Create roles +^^^^^^^^^^^^ + +#. Create the ``AREA_tokyo@japan`` role: + + .. code-block:: console + + $ openstack role create AREA_tokyo@japan + +-------------+----------------------------------+ + | Field | Value | + +-------------+----------------------------------+ + | description | None | + | domain_id | None | + | id | b059c62190b34877a9e6f649108161e7 | + | name | AREA_tokyo@japan | + | options | {} | + +-------------+----------------------------------+ + +#. Create the ``AREA_all@all`` role: + + .. code-block:: console + + $ openstack role create AREA_all@all + +-------------+----------------------------------+ + | Field | Value | + +-------------+----------------------------------+ + | description | None | + | domain_id | None | + | id | b7c1f766a0884064aac130807844d429 | + | name | AREA_all@all | + | options | {} | + +-------------+----------------------------------+ + +#. Create the ``VENDOR_company-a`` role: + + .. code-block:: console + + $ openstack role create VENDOR_company-a + +-------------+----------------------------------+ + | Field | Value | + +-------------+----------------------------------+ + | description | None | + | domain_id | None | + | id | 9bdacbbb96c14a849e4e0e86cd627845 | + | name | VENDOR_company-a | + | options | {} | + +-------------+----------------------------------+ + +#. Create the ``VENDOR_all`` role: + + .. code-block:: console + + $ openstack role create VENDOR_all + +-------------+----------------------------------+ + | Field | Value | + +-------------+----------------------------------+ + | description | None | + | domain_id | None | + | id | 28f0bb8a07bb44c3973e2922c8380ef2 | + | name | VENDOR_all | + | options | {} | + +-------------+----------------------------------+ + +#. Create the ``TENANT_curry`` role: + + .. code-block:: console + + $ openstack role create TENANT_curry + +-------------+----------------------------------+ + | Field | Value | + +-------------+----------------------------------+ + | description | None | + | domain_id | None | + | id | cb98edb048ad49399701d4397708f397 | + | name | TENANT_curry | + | options | {} | + +-------------+----------------------------------+ + +#. Create the ``TENANT_all`` role: + + .. code-block:: console + + $ openstack role create TENANT_all + +-------------+----------------------------------+ + | Field | Value | + +-------------+----------------------------------+ + | description | None | + | domain_id | None | + | id | b2ab9d7eec8c4b978417aff45206a7e0 | + | name | TENANT_all | + | options | {} | + +-------------+----------------------------------+ + +#. Create the ``manager`` role: + + .. code-block:: console + + $ openstack role create manager + +-------------+----------------------------------+ + | Field | Value | + +-------------+----------------------------------+ + | description | None | + | domain_id | None | + | id | d4bc947fca0f41368ade7173a9f0f9cb | + | name | manager | + | options | {} | + +-------------+----------------------------------+ + +Assign roles to user-project pairs +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +#. Assign ``AREA_tokyo@japan``, ``VENDOR_company-a`` and ``TENANT_curry`` + to ``user-a``: + + .. code-block:: console + + $ openstack role add --user user-a --project nfv AREA_tokyo@japan + $ openstack role add --user user-a --project nfv VENDOR_company-a + $ openstack role add --user user-a --project nfv TENANT_curry + + Verify the role assignment of ``user-a``: + + .. code-block:: console + + $ openstack role assignment list --user user-a --project nfv --names + +------------------+----------------+-------+-------------+--------+--------+-----------+ + | Role | User | Group | Project | Domain | System | Inherited | + +------------------+----------------+-------+-------------+--------+--------+-----------+ + | VENDOR_company-a | user-a@Default | | nfv@Default | | | False | + | AREA_tokyo@japan | user-a@Default | | nfv@Default | | | False | + | TENANT_curry | user-a@Default | | nfv@Default | | | False | + +------------------+----------------+-------+-------------+--------+--------+-----------+ + +#. Assign reader to ``user-b``: + + .. code-block:: console + + $ openstack role add --user user-b --project nfv reader + + Verify the role assignment of ``user-b``: + + .. code-block:: console + + $ openstack role assignment list --user user-b --project nfv --names + +--------+----------------+-------+-------------+--------+--------+-----------+ + | Role | User | Group | Project | Domain | System | Inherited | + +--------+----------------+-------+-------------+--------+--------+-----------+ + | reader | user-b@Default | | nfv@Default | | | False | + +--------+----------------+-------+-------------+--------+--------+-----------+ + +#. Assign ``AREA_all@all``, ``VENDOR_all`` and ``TENANT_all`` to + ``user-manager``: + + .. code-block:: console + + $ openstack role add --user user-manager --project nfv AREA_all@all + $ openstack role add --user user-manager --project nfv VENDOR_all + $ openstack role add --user user-manager --project nfv TENANT_all + $ openstack role add --user user-manager --project nfv manager + + Verify the role assignment of ``user-manager``: + + .. code-block:: console + + $ openstack role assignment list --user user-manager --project nfv --names + +---------------+----------------------+-------+-------------+--------+--------+-----------+ + | Role | User | Group | Project | Domain | System | Inherited | + +---------------+----------------------+-------+-------------+--------+--------+-----------+ + | VENDOR_all | user-manager@Default | | nfv@Default | | | False | + | TENANT_all | user-manager@Default | | nfv@Default | | | False | + | AREA_all@all | user-manager@Default | | nfv@Default | | | False | + | manager | user-manager@Default | | nfv@Default | | | False | + +---------------+----------------------+-------+-------------+--------+--------+-----------+ + +Create resources with enhanced tacker attributes +------------------------------------------------ + +This section describes how to create resources with enhanced tacker +attributes, using examples. + +Register vim with area attribute +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +When registering a vim, users can specify area attribute for the vim. This is +achieved by putting the area attribute into the extra field of the vim +configuration file. Please refer to VIM Management [#VIM_Management]_ for how +to register a vim. + +.. warning:: + It is highly recommended that users who performs the VIM registration is + isolated from users who call VNF LCM APIs. Otherwise, users can ignore + area attribute if VIM by overwriting. + +#. Register an OpenStack VIM with area attribute ``tokyo@japan``. + + Sample ``vim_config.yaml`` file for OpenStack VIM: + + .. code-block:: yaml + + auth_url: 'http://192.168.10.115/identity/v3' + username: 'nfv_user' + password: 'devstack' + project_name: 'nfv' + project_domain_name: 'default' + user_domain_name: 'default' + cert_verify: 'True' + extra: + area: tokyo@japan + + Register OpenStack VIM: + + .. code-block:: console + + $ openstack vim register --config-file ./vim_config.yaml \ + --description 'openstack vim in nfv' \ + openstack-tokyo@japan + +----------------+------------------------------------------------------+ + | Field | Value | + +----------------+------------------------------------------------------+ + | auth_cred | { | + | | "username": "nfv_user", | + | | "user_domain_name": "default", | + | | "cert_verify": "True", | + | | "project_id": null, | + | | "project_name": "nfv", | + | | "project_domain_name": "default", | + | | "auth_url": "http://192.168.10.115/identity/v3", | + | | "key_type": "barbican_key", | + | | "secret_uuid": "***", | + | | "password": "***" | + | | } | + | auth_url | http://192.168.10.115/identity/v3 | + | created_at | 2023-02-14 07:05:26.234729 | + | description | openstack vim in nfv | + | extra | area=tokyo@japan | + | id | 95f633de-d2d1-4d90-90f7-0f3839369ff2 | + | is_default | False | + | name | openstack-tokyo@japan | + | placement_attr | { | + | | "regions": [ | + | | "RegionOne" | + | | ] | + | | } | + | project_id | 4cdc4e2efe144f87812677cfe224fffb | + | status | PENDING | + | type | openstack | + | updated_at | None | + | vim_project | { | + | | "name": "nfv", | + | | "project_domain_name": "default" | + | | } | + +----------------+------------------------------------------------------+ + +#. Register a OpenStack VIM with area attribute ``osaka@japan``. + + Sample ``vim_config.yaml`` file for OpenStack VIM: + + .. code-block:: yaml + + auth_url: 'http://192.168.10.115/identity/v3' + username: 'nfv_user' + password: 'devstack' + project_name: 'nfv' + project_domain_name: 'default' + user_domain_name: 'default' + cert_verify: 'True' + extra: + area: osaka@japan + + Register OpenStack VIM: + + .. code-block:: console + + $ openstack vim register --config-file ./vim_config.yaml \ + --description 'openstack vim in nfv' \ + openstack-osaka@japan + +----------------+------------------------------------------------------+ + | Field | Value | + +----------------+------------------------------------------------------+ + | auth_cred | { | + | | "username": "nfv_user", | + | | "user_domain_name": "default", | + | | "cert_verify": "True", | + | | "project_id": null, | + | | "project_name": "nfv", | + | | "project_domain_name": "default", | + | | "auth_url": "http://192.168.10.115/identity/v3", | + | | "key_type": "barbican_key", | + | | "secret_uuid": "***", | + | | "password": "***" | + | | } | + | auth_url | http://192.168.10.115/identity/v3 | + | created_at | 2023-02-14 07:07:36.934208 | + | description | openstack vim in nfv | + | extra | area=osaka@japan | + | id | cd63517f-95c2-4088-ab67-36420ab87ed7 | + | is_default | False | + | name | openstack-osaka@japan | + | placement_attr | { | + | | "regions": [ | + | | "RegionOne" | + | | ] | + | | } | + | project_id | 4cdc4e2efe144f87812677cfe224fffb | + | status | PENDING | + | type | openstack | + | updated_at | None | + | vim_project | { | + | | "name": "nfv", | + | | "project_domain_name": "default" | + | | } | + +----------------+------------------------------------------------------+ + +#. Register a Kubernetes VIM with area attribute ``tokyo@japan``. + + Sample ``vim_config_k8s.yaml`` file for Kubernetes VIM: + + .. code-block:: yaml + + auth_url: "https://kubernetes.default.svc:6443" + bearer_token: "eyJhbGciOiJSUzI1NiIsImtpZCI6IjlrcFJlLVBRREoxZDRHVVRFS1g4eHBFQzFqRWpqOWNhSmRkbDVtY0tqWW8ifQ.eyJpc3MiOiJrdWJlcm5ldGVzL3NlcnZpY2VhY2NvdW50Iiwia3ViZXJuZXRlcy5pby9zZXJ2aWNlYWNjb3VudC9uYW1lc3BhY2UiOiJrdWJlLXN5c3RlbSIsImt1YmVybmV0ZXMuaW8vc2VydmljZWFjY291bnQvc2VjcmV0Lm5hbWUiOiJhZG1pbi10b2tlbi1rOHN2aW0iLCJrdWJlcm5ldGVzLmlvL3NlcnZpY2VhY2NvdW50L3NlcnZpY2UtYWNjb3VudC5uYW1lIjoiYWRtaW4iLCJrdWJlcm5ldGVzLmlvL3NlcnZpY2VhY2NvdW50L3NlcnZpY2UtYWNjb3VudC51aWQiOiIwM2M3ODRjNi0yNjhkLTQ5ZTgtYjU1Yi0zNDJhMmFiMjM1ZDUiLCJzdWIiOiJzeXN0ZW06c2VydmljZWFjY291bnQ6a3ViZS1zeXN0ZW06YWRtaW4ifQ.KgqK6nBOnJaJ6uxmbLilYjenUbEwVvJ3-Ynbulw2GjGgMfbhO4lXR57nVdA9LkM17NyiUnP01t7b6BzzUPELQA03q5ufGkZns9d7xzlmb6SAKzTXh2rdh3skUDtv4dMHTqf7-e6K9VWtQPRo9qfCgRR_nrU4ED9ycjE707kcopbrOTk_EEZ-roPBTWZl5OgFQrTl5y-xVPcHNqF2vN-l6t8M_3g6PZV6yQl0ul4iBrfGpnMQyvJcgUBmFe2o1L3ey4VXC9aR0FLz--vi9K7TntBrm5pipAJIrLdDImws00P5hylyAavY8OACInyNWPkHeVK0D49koV9J1J5kR_paxQ" + project_name: "default" + ssl_ca_cert: "-----BEGIN CERTIFICATE----- + MIIC/jCCAeagAwIBAgIBADANBgkqhkiG9w0BAQsFADAVMRMwEQYDVQQDEwprdWJl + cm5ldGVzMB4XDTIyMTIwNTA1MzIwOFoXDTMyMTIwMjA1MzIwOFowFTETMBEGA1UE + AxMKa3ViZXJuZXRlczCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBAMHS + 8d5g+5Qxhw5ViKwQo8kAWf/wxmfrT5sPQZfCfb/lnxt2kdiUkbhPlM2f4SLSz0CF + 3QM+6rFCP3ajOKyVF4/Gom/qwNseGrxKW3PxzWqA4PBgXk4wiv/VVXiAm0TS6fMd + GnuVqebV7Q8lue6YnuK1ttDsYSpHiEaiWW1g9eBV4/BRHUFHKRHO4b1u2c+s3MSV + NpPGgyVEqwAk76kDNJGcrihP7Ze3TRWHY52VCjsZZ4x4zQrAtsCYL9to6PcOxz7z + ZFL4SKOlOXdQieIZ47DTHWKlB6gshxQiWd74AEzzVH3jMSRPnSTipcFzwL9z7AY6 + r1SzLb9TngxrjiNdndcCAwEAAaNZMFcwDgYDVR0PAQH/BAQDAgKkMA8GA1UdEwEB + /wQFMAMBAf8wHQYDVR0OBBYEFAHIWsqm7ffo4EF4dDbUWFHQAZjLMBUGA1UdEQQO + MAyCCmt1YmVybmV0ZXMwDQYJKoZIhvcNAQELBQADggEBAEQuTS5F6g/XRnjF9C1v + umi1ZphbOUVcrAifRMVFV1fLqa9kgKH/mgl0JN04CE2fCErJYxBmlGlCJQcihlkk + sZu6/dQ1hgI4pS891kCpSu5RExDNj5fm7X3s/OuxqIBsOr2CayEhWyKkqXyT9CoR + jsfyfq/WQYxbhq92l7sB+tWI0/sVHWVaouY7QzXdP6d6LkC58f7t5d0p46X3sECK + jPUFktAZb2axK4ipHRYxYzB7n7RB6K+nNsaaZOhWMxCa4835yleuRp6Caq9TnV75 + YW8MCUN6YNEQ1PYpGMuguAkAsS62QeL7/1VCUzYo/Rxu7l5sErjcKtq6bWWJ3eM6 + Yx4= + -----END CERTIFICATE-----" + type: "kubernetes" + extra: + use_helm: true + area: tokyo@japan + + Register Kubernetes VIM: + + .. code-block:: console + + $ openstack vim register --config-file ./vim_k8s_config.yaml \ + --description 'kubernetes vim in nfv' \ + kubernetes-tokyo@japan --fit-width + +----------------+----------------------------------------------------------------------------------------------------------------------------------------------------------------+ + | Field | Value | + +----------------+----------------------------------------------------------------------------------------------------------------------------------------------------------------+ + | auth_cred | { | + | | "bearer_token": "***", | + | | "ssl_ca_cert": "b'gAAAAABj6zRs0_iM8WzO3BPSdafLsnunr8jA1Kx-9V3ITS-m_2W7_Z76hmH3zXHSOn8zmwwUbKoWBl74QTHOne- | + | | z_Uwuc5tJuaKaEgfOUX8UkifhVhA2-V6zZyQ5nFoLVLmzwN1oSUmFaT-yli9Noel0oi4xK03WxvnOj01F_LcGFXnJahSg74FYCoRRQpSng9IBYy_jCIeLCPtoeWpxx1Yske-2nyOCXmcxnEgbOw79s-8qL07iu | + | | Ln2nYBS7buGTZ64nmCPNiUWsFeccOCX8rdoTSOiKaeSVFQYlfos5eUA8V-6RKJECEtmbjUWyoc1084uTukS_0-o--rLsnOklAfkDuArC2l_w62iMxLGmsQQO3dD5C9nImbdKkR0xRRTayRiCKDFqCua9Ny2UIy | + | | WdsNYziX1oxhYeeguHkL1b9hIzlU3pStuEkcqhmX1R9-E4Heh0h4uJb_2wc_R540-Wq0x7faKhPbWx4kuyr4S1S4KplDuyWCpQEWTlZ957-S5n50pnsTRTrjPi25VvYtQ3c7yyZjQaNIf0bJ9eXVbG2OhnZOWC | + | | hOX-_B_CsqP27_zdQ5etxj- | + | | Dct9VGHpD29Sra_bLUw3_EUHxvB4J-Rb0FyinKzOuLJSB52kPZ5Ay2k8_jwEQyrXK6ZqJYAP24XtQFrFl7FMCiYoep_6cYrB2WX8nCwQW4BqLDV4astJEGHRSVwEiJklF3BBNCeuJ-MhMmtm- | + | | hulnqw7tSHYYUHKzKcU30_XZKHdp5dqtsmGJvbuvfwbbKX0fjHasi8PQrI_7982OLF55JkXsmdu-vemoJe8RQZC2KX9nZI8qOVTqmmqkAo3GupcCgLMPItaLjogeEkLmqy30L7WOrXU9em3oq7828gufPS6JmG | + | | FpqridqJf8onbljrayND2050-XmgzaEOLtzah9YZaIo4_97Ki4QyCtVk2uauP0_po3jcGnU1qklOZyqdRAkZ9sWTiWUdfmmxjpE_JHgwu8VeFJFVB1y5is7O7Ww4YNvVCGjZYPzQVzomssjmWgtAqzu_biDrv0 | + | | JvAz4OjV3VgRUhSW_VDIUmCnlx6zWWoPckOVuGAIAX0Q93afUqdOPsLB9QU0J4D1eTTQcjCOMWROMSDvsi11KN6ejmp7fiki5KRQX8hZGmo2d71OmkPhap8KSW2hcV2EfZlLOT0i5RQZNpfuc3BWUnKgeSW1OM | + | | FssJLiDScFry-7VvMjQCIVYWH7amMubSNGVuPvN5dFhUk6CukjAmW82VZ9pvz1XymohS48FSYBfRiTgO6m4BMkxhVb1sMOMCBSysi8NIc75YHh4vwvTkmt5pu6wZcIisdGgwTgkOoYezmBex2agQXJ2vCiO- | + | | bLAISkCyl-Bs0dF-TI3HEVv0twRXBZsSJiaF8lmk_lOlqP5rFLwk37JCvFC-MMRWHl9pBcvr-FLWyUGVVL1WDaWnW6o3cEz0iwRw5wFmy7kqZw_3FAj0qguCq6UQ- | + | | NDBaePMyv9vJJJrXVInsJA6J17b9iA97UqNlzydb07Ag4TBgSipsC1NZwbMJR87D'", | + | | "auth_url": "https://kubernetes.default.svc:6443", | + | | "username": "None", | + | | "key_type": "barbican_key", | + | | "secret_uuid": "***" | + | | } | + | auth_url | https://kubernetes.default.svc:6443 | + | created_at | 2023-02-14 07:12:45.027780 | + | description | kubernetes vim in nfv | + | extra | area=tokyo@japan, use_helm=True | + | id | 705f23fc-054f-46c4-bcfe-922d11d85b27 | + | is_default | False | + | name | kubernetes-tokyo@japan | + | placement_attr | { | + | | "regions": [ | + | | "default", | + | | "kube-node-lease", | + | | "kube-public", | + | | "kube-system", | + | | "syg" | + | | ] | + | | } | + | project_id | 4cdc4e2efe144f87812677cfe224fffb | + | status | PENDING | + | type | kubernetes | + | updated_at | None | + | vim_project | { | + | | "name": "default" | + | | } | + +----------------+----------------------------------------------------------------------------------------------------------------------------------------------------------------+ + +Create VNF package with vendor attribute +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +The vendor attribute of the VNF package resource comes from the provider field +defined in ``Definitions/vnfd_top.yaml`` of the VNF package. To create a VNF +package with a specified vendor attribute, users need to modify the provider +attribute to vendor. Please refer to VNF Package [#VNF_Package]_ for how to +make zip file and create VNF packages. This chapter only gives a sample of +configuration files that need to be modified in VNF package. + +#. Set the provider in ``Definitions/vnfd_top.yaml`` to ``company-a``. + + .. code-block:: yaml + + tosca_definitions_version: tosca_simple_yaml_1_2 + + description: Sample VNF + + imports: + - etsi_nfv_sol001_common_types.yaml + - etsi_nfv_sol001_vnfd_types.yaml + - sample_vnfd_types.yaml + - sample_vnfd_df_simple.yaml + + topology_template: + inputs: + selected_flavour: + type: string + description: VNF deployment flavour selected by the consumer. It is provided in the API + + node_templates: + VNF: + type: company.provider.VNF + properties: + flavour_id: { get_input: selected_flavour } + descriptor_id: c1bb0ce7-ebca-4fa7-95ed-4840d70a1175 + provider: company-a + product_name: Sample VNF + software_version: "1.0" + descriptor_version: "1.0" + vnfm_info: + - Tacker + requirements: + #- virtual_link_external # mapped in lower-level templates + #- virtual_link_internal # mapped in lower-level templates + +#. Set the provider in ``Definitions/vnfd_types.yaml`` to ``company-a``. + + .. code-block:: yaml + + tosca_definitions_version: tosca_simple_yaml_1_2 + + description: VNF type definition + + imports: + - etsi_nfv_sol001_common_types.yaml + - etsi_nfv_sol001_vnfd_types.yaml + + node_types: + company.provider.VNF: + derived_from: tosca.nodes.nfv.VNF + properties: + ... + provider: + type: string + constraints: [valid_values: ["company-a"]] + default: "company-a" + ... + +Create & Instantiate VNF with vendor, area and tenant attributes +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +Create VNF with vendor attribute +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +The vendor attribute of the VNF comes from the provider attribute of the VNF +package. Therefore, users only need to use the VNF package with the specified +provider attribute to create a VNF with the specified vendor attribute. + +The following is an example of creating a VNF with the specified vendor +attribute. + +Create a VNF with vnfd_id: + +.. code-block:: console + + $ openstack vnflcm create + +Instantiate VNF on VIM with area attributes +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +The area attribute of the VNF comes from the used vim. In other +words, you need to specify a VIM in the area where you want to +instantiate a VNF. + +For VNF LCM API version 1, please refer to [#VNF_Lifecycle_Management]_ to +instantiate VNF. Below are two samples of . + +#. If contains the ``vimConnectionInfo`` parameter, the area + attribute comes from vim in it. + + .. code-block:: json + + { + "flavourId": "simple", + "extVirtualLinks": [ + { + "id": "net0", + "resourceId": "1d868d02-ecd4-4402-8e6b-54e77ebdcc28", + "extCps": [ + { + "cpdId": "CP1", + "cpConfig": [ + { + "cpProtocolData": [ + { + "layerProtocol": "IP_OVER_ETHERNET", + "ipOverEthernet": { + "ipAddresses": [ + { + "type": "IPV4", + "numDynamicAddresses": 1, + "subnetId": "109f5049-b51e-409a-9a99-d740ba5f3acb" + } + ] + } + } + ] + } + ] + } + ] + } + ], + "vimConnectionInfo": [ + { + "id": "e24f9796-a8e9-4cb0-85ce-5920dcddafa1", + "vimId": "991a1e07-e8a2-4e1b-b77d-3937177a5b7f", + "vimType": "ETSINFV.OPENSTACK_KEYSTONE.v_2" + } + ], + "additionalParams": { + "lcm-operation-user-data": "./UserData/lcm_user_data.py", + "lcm-operation-user-data-class": "SampleUserData" + } + } + + +#. If doesn't contains the ``vimConnectionInfo`` parameter, the + default vim is used and area attribute comes from it. + + .. code-block:: json + + { + "flavourId": "simple" + } + +For VNF LCM API version 2, please refer to [#VNF_Lifecycle_Management]_ to +instantiate VNF. Below are two samples of . + +#. If the vim in the ``vimConnectionInfo`` parameter of is an + existing vim in the DB, the vendor attribute of the instantiated VNF + comes from this vim. + + .. code-block:: json + + { + "extManagedVirtualLinks": [ + { + "id": "7a6fe192-c34b-4029-937d-f1a2e7a00f5a", + "resourceId": "11f8a056-0495-4ca6-8de9-94402604663f", + "vnfVirtualLinkDescId": "internalVL1" + } + ], + "extVirtualLinks": [ + { + "extCps": [ + { + "cpConfig": { + "VDU1_CP1_1": { + "cpProtocolData": [ + { + "ipOverEthernet": { + "ipAddresses": [ + { + "numDynamicAddresses": 1, + "type": "IPV4" + } + ] + }, + "layerProtocol": "IP_OVER_ETHERNET" + } + ] + } + }, + "cpdId": "VDU1_CP1" + }, + { + "cpConfig": { + "VDU2_CP1_1": { + "cpProtocolData": [ + { + "ipOverEthernet": { + "ipAddresses": [ + { + "fixedAddresses": [ + "10.10.0.101" + ], + "type": "IPV4" + } + ] + }, + "layerProtocol": "IP_OVER_ETHERNET" + } + ] + } + }, + "cpdId": "VDU2_CP1" + } + ], + "id": "b0b2f836-a275-4374-834e-ed336a563b1e", + "resourceId": "1948231e-bbf0-4ff9-a692-40f8d6d5c90d" + }, + { + "extCps": [ + { + "cpConfig": { + "VDU1_CP2_1": { + "cpProtocolData": [ + { + "ipOverEthernet": { + "ipAddresses": [ + { + "numDynamicAddresses": 1, + "subnetId": "1d4877ea-b810-4093-95de-bee62b2363f1", + "type": "IPV4" + } + ] + }, + "layerProtocol": "IP_OVER_ETHERNET" + } + ] + } + }, + "cpdId": "VDU1_CP2" + }, + { + "cpConfig": { + "VDU2_CP2_1": { + "cpProtocolData": [ + { + "ipOverEthernet": { + "ipAddresses": [ + { + "fixedAddresses": [ + "10.10.1.101" + ], + "subnetId": "1d4877ea-b810-4093-95de-bee62b2363f1", + "type": "IPV4" + } + ] + }, + "layerProtocol": "IP_OVER_ETHERNET" + } + ] + } + }, + "cpdId": "VDU2_CP2" + } + ], + "id": "6766a8d4-cad1-43f1-b0cb-ce0ef9267661", + "resourceId": "5af7e28a-e744-4b4f-a1a4-c7d0f7d93cd7" + } + ], + "flavourId": "simple", + "instantiationLevelId": "instantiation_level_1", + "vimConnectionInfo": { + "vim1": { + "id": "725f625e-f6b7-4bcd-b1b7-7184039fde45" + "vimId": "03e608b2-e7d4-44fa-bd84-74fb24be3ed5", + "vimType": "ETSINFV.OPENSTACK_KEYSTONE.V_3" + } + } + } + +#. If the vim in the ``vimConnectionInfo`` parameter of is not + existed in the DB, the vendor attribute of the instantiated VNF comes from + this vim. Users need to specify the area attribute in the + ``vimConnectionInfo`` parameter. + + .. code-block:: json + + { + "extManagedVirtualLinks": [ + { + "id": "7a6fe192-c34b-4029-937d-f1a2e7a00f5a", + "resourceId": "11f8a056-0495-4ca6-8de9-94402604663f", + "vnfVirtualLinkDescId": "internalVL1" + } + ], + "extVirtualLinks": [ + { + "extCps": [ + { + "cpConfig": { + "VDU1_CP1_1": { + "cpProtocolData": [ + { + "ipOverEthernet": { + "ipAddresses": [ + { + "numDynamicAddresses": 1, + "type": "IPV4" + } + ] + }, + "layerProtocol": "IP_OVER_ETHERNET" + } + ] + } + }, + "cpdId": "VDU1_CP1" + }, + { + "cpConfig": { + "VDU2_CP1_1": { + "cpProtocolData": [ + { + "ipOverEthernet": { + "ipAddresses": [ + { + "fixedAddresses": [ + "10.10.0.101" + ], + "type": "IPV4" + } + ] + }, + "layerProtocol": "IP_OVER_ETHERNET" + } + ] + } + }, + "cpdId": "VDU2_CP1" + } + ], + "id": "b0b2f836-a275-4374-834e-ed336a563b1e", + "resourceId": "1948231e-bbf0-4ff9-a692-40f8d6d5c90d" + }, + { + "extCps": [ + { + "cpConfig": { + "VDU1_CP2_1": { + "cpProtocolData": [ + { + "ipOverEthernet": { + "ipAddresses": [ + { + "numDynamicAddresses": 1, + "subnetId": "1d4877ea-b810-4093-95de-bee62b2363f1", + "type": "IPV4" + } + ] + }, + "layerProtocol": "IP_OVER_ETHERNET" + } + ] + } + }, + "cpdId": "VDU1_CP2" + }, + { + "cpConfig": { + "VDU2_CP2_1": { + "cpProtocolData": [ + { + "ipOverEthernet": { + "ipAddresses": [ + { + "fixedAddresses": [ + "10.10.1.101" + ], + "subnetId": "1d4877ea-b810-4093-95de-bee62b2363f1", + "type": "IPV4" + } + ] + }, + "layerProtocol": "IP_OVER_ETHERNET" + } + ] + } + }, + "cpdId": "VDU2_CP2" + } + ], + "id": "6766a8d4-cad1-43f1-b0cb-ce0ef9267661", + "resourceId": "5af7e28a-e744-4b4f-a1a4-c7d0f7d93cd7" + } + ], + "flavourId": "simple", + "instantiationLevelId": "instantiation_level_1", + "vimConnectionInfo": { + "vim1": { + "accessInfo": { + "password": "devstack", + "project": "nfv", + "projectDomain": "Default", + "region": "RegionOne", + "userDomain": "Default", + "username": "nfv_user" + }, + "interfaceInfo": { + "endpoint": "http://localhost/identity/v3" + }, + "vimId": "03e608b2-e7d4-44fa-bd84-74fb24be3ed5", + "vimType": "ETSINFV.OPENSTACK_KEYSTONE.V_3", + "extra": {"area": "tokyo@japan"} + } + } + } + +Instantiate CNF with tenant attribute +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +In Tacker Antelope verison, only CNF has the tenant attribute. When +instantiating CNF, the tenant attribute of CNF is specified by the namespace in +the additionalParams field of . + +.. code-block:: json + + { + "flavourId": "simple", + "vimConnectionInfo": [ + { + "id": "b1bb0ce7-ebca-4fa7-95ed-4840d70a1177", + "vimId": "725f625e-f6b7-4bcd-b1b7-7184039fde45", + "vimType": "kubernetes" + } + ], + "additionalParams": { + "lcm-kubernetes-def-files": [ + "Files/kubernetes/deployment.yaml", + "Files/kubernetes/namespace.yaml" + ], + "namespace": "curry" + } + } + +Usage of APIs supporting Enhanced Tacker Policy +----------------------------------------------- + +This section takes the VIM Management API as an example to introduce the API +calls that support Enhanced Tacker Policy. You can find a list of APIs that +support Enhanced Tacker Policy and the enhanced tacker attributes supported by +each API in `Tacker APIs that support Enhanced Tacker Policy`_. + +GET individual resources +~~~~~~~~~~~~~~~~~~~~~~~~ + +Users with special roles have permission to access corresponding resources. For +example, ``user-a`` who has the role of ``AREA_tokyo@japan`` has permission +to access the VIM with area attribute ``tokyo@japan``. ``user-b`` who does +not have the role of ``AREA_tokyo@japan`` does not have permission to +access the VIM with area attribute ``tokyo@japan``. Here take Show VIM as +an example. + +``user-a`` shows VIM whose area attribute is ``tokyo@japan``, and it succeeds. + +.. code-block:: console + + $ openstack vim show 95f633de-d2d1-4d90-90f7-0f3839369ff2 + +----------------+------------------------------------------------------+ + | Field | Value | + +----------------+------------------------------------------------------+ + | auth_cred | { | + | | "username": "nfv_user", | + | | "user_domain_name": "default", | + | | "cert_verify": "True", | + | | "project_id": null, | + | | "project_name": "nfv", | + | | "project_domain_name": "default", | + | | "auth_url": "http://192.168.10.115/identity/v3", | + | | "key_type": "barbican_key", | + | | "secret_uuid": "***", | + | | "password": "***" | + | | } | + | auth_url | http://192.168.10.115/identity/v3 | + | created_at | 2023-02-14 07:05:26 | + | description | openstack vim in nfv | + | extra | area=tokyo@japan | + | id | 95f633de-d2d1-4d90-90f7-0f3839369ff2 | + | is_default | False | + | name | openstack-tokyo@japan | + | placement_attr | { | + | | "regions": [ | + | | "RegionOne" | + | | ] | + | | } | + | project_id | 4cdc4e2efe144f87812677cfe224fffb | + | status | REACHABLE | + | type | openstack | + | updated_at | 2023-02-14 07:05:28 | + | vim_project | { | + | | "name": "nfv", | + | | "project_domain_name": "default" | + | | } | + +----------------+------------------------------------------------------+ + +``user-b`` shows VIM whose area attribute is ``tokyo@japan``, and it fails. + +.. code-block:: console + + $ openstack vim show 95f633de-d2d1-4d90-90f7-0f3839369ff2 + The request you have made requires authentication. (HTTP 401) (Request-ID: req-4875a3b8-b553-439b-a837-5940737f672a) + +Users can use the ``manager`` role to distinguish between reference APIs and +operating APIs. This is an existing function of oslo.policy, and here is just a +suggested usage scenario. + +``user-a`` has no ``manager`` role and cannot delete VIM. + +.. code-block:: console + + $ openstack vim delete 95f633de-d2d1-4d90-90f7-0f3839369ff2 + Unable to delete the below vim(s): + Cannot delete 95f633de-d2d1-4d90-90f7-0f3839369ff2: The resource could not be found. + +``user-manager`` has the role of ``manager`` and can delete VIM. + +.. code-block:: console + + $ openstack vim delete 95f633de-d2d1-4d90-90f7-0f3839369ff2 + All specified vim(s) deleted successfully + +LIST resources +~~~~~~~~~~~~~~ + +For APIs that list resources, Enhanced Tacker Policy acts as a filter. That is, +the list operation only lists resources that the user has permission. +Here take List VIM as an example. + +The ``user-a`` only has permission to access the VIM with area attribute +``tokyo@japan``. Therefore, ``user-a`` can only list the VIM with area +attribute ``tokyo@japan``. + +.. code-block:: console + + $ openstack vim list + +--------------------------------------+------------------------+----------------------------------+------------+------------+-----------+ + | ID | Name | Tenant_id | Type | Is Default | Status | + +--------------------------------------+------------------------+----------------------------------+------------+------------+-----------+ + | 705f23fc-054f-46c4-bcfe-922d11d85b27 | kubernetes-tokyo@japan | 4cdc4e2efe144f87812677cfe224fffb | kubernetes | False | REACHABLE | + | c5e82dd6-c5bc-4018-8415-ee5d53df5203 | default_for_nfv | 4cdc4e2efe144f87812677cfe224fffb | openstack | True | REACHABLE | + +--------------------------------------+------------------------+----------------------------------+------------+------------+-----------+ + +The ``user-b`` does not have access to any VIM with the area attribute, so +the list is empty when performing the List VIM operation. + +.. code-block:: console + + $ openstack vim list + (No output.) + + +The ``user-manager`` has access rights to all resources. It can list all +resources. + +.. code-block:: console + + $ openstack vim list + +--------------------------------------+------------------------+----------------------------------+------------+------------+-----------+ + | ID | Name | Tenant_id | Type | Is Default | Status | + +--------------------------------------+------------------------+----------------------------------+------------+------------+-----------+ + | 705f23fc-054f-46c4-bcfe-922d11d85b27 | kubernetes-tokyo@japan | 4cdc4e2efe144f87812677cfe224fffb | kubernetes | False | REACHABLE | + | c5e82dd6-c5bc-4018-8415-ee5d53df5203 | default_for_nfv | 4cdc4e2efe144f87812677cfe224fffb | openstack | True | REACHABLE | + | cd63517f-95c2-4088-ab67-36420ab87ed7 | openstack-osaka@japan | 4cdc4e2efe144f87812677cfe224fffb | openstack | False | REACHABLE | + +--------------------------------------+------------------------+----------------------------------+------------+------------+-----------+ + +Limitations +----------- +* As the resources created in the previous version of Tacker may not have + enhanced policy attributes, if the enhanced policy attributes are used as + comparison attributes in the policy rule, this rule will prevent users from + accessing those resources without these attributes as the comparison result + is always false. + +Appendix +-------- + +Tacker APIs that support Enhanced Tacker Policy +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +The table below lists the APIs that support Enhanced Tacker Policy, and the +enhanced tacker attributes supported by each API. + +.. list-table:: + :widths: 28 28 16 16 16 + :header-rows: 1 + + * - Operation + - API + - Supports area + - Supports vendor + - Supports tenant + * - VIM-List + - **GET** /v1.0/vims + - Yes + - No + - No + * - VIM-Show + - **GET** /v1.0/vims/{vim_id} + - Yes + - No + - No + * - VIM-Update + - **PUT** /v1.0/vims/{vim_id} + - Yes + - No + - No + * - VIM-Delete + - **Delete** /v1.0/vims/{vim_id} + - Yes + - No + - No + * - PKG-List + - **GET** /vnfpkgm/v1/vnf_packages + - No + - Yes(1) + - No + * - PKG-Show + - **GET** /vnfpkgm/v1/vnf_packages/{vnfPkgId} + - No + - Yes(1) + - No + * - PKG-Update + - **PATCH** /vnfpkgm/v1/vnf_packages/{vnfPkgId} + - No + - Yes + - No + * - PKG-Delete + - **DELETE** /vnfpkgm/v1/vnf_packages/{vnfPkgId} + - No + - Yes + - No + * - PKG-Read-vnfd + - **GET** /vnfpkgm/v1/vnf_packages/{vnfPkgId}/vnfd + - No + - Yes + - No + * - PKG-Fetch + - **GET** /vnfpkgm/v1/vnf_packages/{vnfPkgId}/package_content + - No + - Yes + - No + * - PKG-Upload-content + - **PUT** /vnfpkgm/v1/vnf_packages/{vnfPkgId}/package_content + - No + - Yes + - No + * - PKG-Artifacts + - **GET** /vnfpkgm/v1/vnf_packages/{vnfPkgId}/artifacts/{artifactPath} + - No + - Yes + - No + * - LCM-List + - **GET** /vnflcm/v1/vnf_instances + - Yes(3) + - Yes + - Yes(2) + * - LCM-Create + - **POST** /vnflcm/v1/vnf_instances + - No + - Yes + - No + * - LCM-Show + - **GET** /vnflcm/v1/vnf_instances/{vnfInstanceId} + - Yes(3) + - Yes + - Yes(2) + * - LCM-Update + - **PATCH** /vnflcm/v1/vnf_instances/{vnfInstanceId} + - Yes + - Yes + - Yes(2) + * - LCM-Delete + - **DELETE** /vnflcm/v1/vnf_instances/{vnfInstanceId} + - No + - Yes + - No + * - LCM-Instantiate + - **POST** /vnflcm/v1/vnf_instances/{vnfInstanceId}/instantiate + - Yes + - Yes + - Yes(2) + * - LCM-Scale + - **POST** /vnflcm/v1/vnf_instances/{vnfInstanceId}/scale + - Yes + - Yes + - Yes(2) + * - LCM-Terminate + - **POST** /vnflcm/v1/vnf_instances/{vnfInstanceId}/terminate + - Yes + - Yes + - Yes(2) + * - LCM-Heal + - **POST** /vnflcm/v1/vnf_instances/{vnfInstanceId}/heal + - Yes + - Yes + - Yes(2) + * - LCM-Change-Connectivity + - **POST** /vnflcm/v1/vnf_instances/{vnfInstanceId}/change_ext_conn + - Yes + - Yes + - Yes(2) + * - LCM-ListV2 + - **GET** /vnflcm/v2/vnf_instances + - Yes(4) + - Yes + - Yes(2) + * - LCM-CreateV2 + - **POST** /vnflcm/v2/vnf_instances + - No + - Yes + - No + * - LCM-ShowV2 + - **GET** /vnflcm/v2/vnf_instances/{vnfInstanceId} + - Yes(4) + - Yes + - Yes(2) + * - LCM-UpdateV2 + - **PATCH** /vnflcm/v2/vnf_instances/{vnfInstanceId} + - Yes + - Yes + - Yes(2) + * - LCM-DeleteV2 + - **DELETE** /vnflcm/v2/vnf_instances/{vnfInstanceId} + - No + - Yes + - No + * - LCM-InstantiateV2 + - **POST** /vnflcm/v2/vnf_instances/{vnfInstanceId}/instantiate + - Yes + - Yes + - Yes(2) + * - LCM-ScaleV2 + - **POST** /vnflcm/v2/vnf_instances/{vnfInstanceId}/scale + - Yes + - Yes + - Yes(2) + * - LCM-TerminateV2 + - **POST** /vnflcm/v2/vnf_instances/{vnfInstanceId}/terminate + - Yes + - Yes + - Yes(2) + * - LCM-HealV2 + - **POST** /vnflcm/v2/vnf_instances/{vnfInstanceId}/heal + - Yes + - Yes + - Yes(2) + * - LCM-Change-ConnectivityV2 + - **POST** /vnflcm/v2/vnf_instances/{vnfInstanceId}/change_ext_conn + - Yes + - Yes + - Yes(2) + * - LCM-Change-VnfPkgV2 + - **POST** /vnflcm/v2/vnf_instances/{vnfInstanceId}/change_vnfpkg + - Yes + - Yes + - Yes(2) + +(1) This is ignored when the state is not `ONBOARDED`. +(2) This is ignored when the instance is vnf(not cnf). +(3) Default vim is used when the state is `NOT_INSTANTIATED`. +(4) This is ignored when the state is not `NOT_INSTANTIATED`. + +Sample policy.yaml file +~~~~~~~~~~~~~~~~~~~~~~~ + +.. literalinclude:: ../../../etc/tacker/enhanced_tacker_policy.yaml.sample + +References +---------- + +.. [#oslo.policy] https://docs.openstack.org/oslo.policy/latest/ +.. [#VIM_Management] https://docs.openstack.org/tacker/latest/cli/cli-legacy-vim.html +.. [#VNF_Package] https://docs.openstack.org/tacker/latest/user/vnf-package.html +.. [#VNF_Lifecycle_Management] https://docs.openstack.org/tacker/latest/cli/cli-etsi-vnflcm.html diff --git a/doc/source/user/index.rst b/doc/source/user/index.rst index a0e84fd26..9dd1f4da4 100644 --- a/doc/source/user/index.rst +++ b/doc/source/user/index.rst @@ -60,3 +60,4 @@ Use Case Guide fault_notification_use_case_guide prometheus_plugin_use_case_guide db_migration_tool_usage_guide + enhanced_tacker_policy_usage_guide diff --git a/etc/tacker/enhanced_tacker_policy.yaml.sample b/etc/tacker/enhanced_tacker_policy.yaml.sample new file mode 100644 index 000000000..97d31c2d5 --- /dev/null +++ b/etc/tacker/enhanced_tacker_policy.yaml.sample @@ -0,0 +1,251 @@ +# Decides what is required for the 'is_admin:True' check to succeed. +"context_is_admin": "role:admin" + +# Default rule for most non-Admin APIs. +"admin_or_owner": "is_admin:True or project_id:%(project_id)s" + +# Default rule for most Admin APIs. +"admin_only": "is_admin:True" + +# Default rule for sharing vims. +"shared": "field:vims:shared=True" + +# Default rule for most non-Admin APIs. +"default": "rule:admin_or_owner" + +# For manager +"manager_and_owner": "role:manager and project_id:%(project_id)s" + +# For user +"owner": "project_id:%(project_id)s" + +# VIM resource attributes compare rule. +"vim_attrs_cmp": "area:%(area)s" + +# Register a VIM. +# Post /v1.0/vims +"create_vim": "rule:manager_and_owner or role:admin" + +# List VIMs or show a VIM. +# GET /v1.0/vims +# GET /v1.0/vims/{vim_id} +"get_vim": "rule:vim_attrs_cmp or role:admin" + +# Update a VIM. +# PUT /v1.0/vims/{vim_id} +"update_vim": "rule:vim_attrs_cmp and rule:manager_and_owner or role:admin" + +# Delete a VIM. +# DELETE /v1.0/vims/{vim_id} +"delete_vim": "rule:vim_attrs_cmp and rule:manager_and_owner or role:admin" + +# vnf_packages resource attributes compare rule. +"vnf_pkg_attrs_cmp": "vendor:%(vendor)s" + +# Creates a VNF package. +# POST /vnf_packages +"os_nfv_orchestration_api:vnf_packages:create": "rule:admin_or_owner" + +# Show a VNF package. +# GET /vnf_packages/{vnf_package_id} +"os_nfv_orchestration_api:vnf_packages:show": "rule:vnf_pkg_attrs_cmp and rule:owner or role:admin" + +# List all VNF packages. +# GET /vnf_packages/ +"os_nfv_orchestration_api:vnf_packages:index": "rule:vnf_pkg_attrs_cmp and rule:owner or role:admin" + +# Delete a VNF package. +# DELETE /vnf_packages/{vnf_package_id} +"os_nfv_orchestration_api:vnf_packages:delete": "rule:vnf_pkg_attrs_cmp and rule:manager_and_owner or role:admin" + +# Fetch the contents of an on-boarded VNF Package. +# GET /vnf_packages/{vnf_package_id}/package_content +"os_nfv_orchestration_api:vnf_packages:fetch_package_content": "rule:vnf_pkg_attrs_cmp and rule:owner or role:admin" + +# Upload a VNF package content. +# PUT /vnf_packages/{vnf_package_id}/package_content +"os_nfv_orchestration_api:vnf_packages:upload_package_content": "rule:vnf_pkg_attrs_cmp or role:admin" + +# Upload a VNF package content from URI. +# POST /vnf_packages/{vnf_package_id}/package_content/upload_from_uri +"os_nfv_orchestration_api:vnf_packages:upload_from_uri": "rule:admin_or_owner" + +# Update information of VNF package. +# PATCH /vnf_packages/{vnf_package_id} +"os_nfv_orchestration_api:vnf_packages:patch": "rule:vnf_pkg_attrs_cmp and rule:manager_and_owner or role:admin" + +# Read the content of the VNFD within a VNF package. +# GET /vnf_packages/{vnf_package_id}/vnfd +"os_nfv_orchestration_api:vnf_packages:get_vnf_package_vnfd": "rule:vnf_pkg_attrs_cmp and rule:owner or role:admin" + +# Read the content of the artifact within a VNF package. +# GET /vnf_packages/{vnfPkgId}/artifacts/{artifactPath} +"os_nfv_orchestration_api:vnf_packages:fetch_artifact": "rule:vnf_pkg_attrs_cmp and rule:owner or role:admin" + +# vnflcm create attributes compare rule. +"vnflcm_create_attrs_cmp": "vendor:%(vendor)s" + +# vnflcm instantiate attributes compare rule. +"vnflcm_inst_attrs_cmp": "vendor:%(vendor)s" + +# vnflcm delete attributes compare rule. +"vnflcm_delete_attrs_cmp": "vendor:%(vendor)s" + +# vnflcm resource attributes compare rule. +"vnflcm_attrs_cmp": "area:%(area)s and vendor:%(vendor)s and tenant:%(tenant)s" + +# Get API Versions. +# GET /vnflcm/v1/api_versions +"os_nfv_orchestration_api:vnf_instances:api_versions": "@" + +# Create VNF instance. +# POST /vnflcm/v1/vnf_instances +"os_nfv_orchestration_api:vnf_instances:create": "rule:vnflcm_create_attrs_cmp and rule:manager_and_owner or role:admin" + +# Instantiate VNF instance. +# POST /vnflcm/v1/vnf_instances/{vnfInstanceId}/instantiate +"os_nfv_orchestration_api:vnf_instances:instantiate": "rule:vnflcm_inst_attrs_cmp and rule:manager_and_owner or role:admin" + +# Query an Individual VNF instance. +# GET /vnflcm/v1/vnf_instances/{vnfInstanceId} +"os_nfv_orchestration_api:vnf_instances:show": "rule:vnflcm_attrs_cmp and rule:owner or role:admin" + +# Terminate a VNF instance. +# POST /vnflcm/v1/vnf_instances/{vnfInstanceId}/terminate +"os_nfv_orchestration_api:vnf_instances:terminate": "rule:vnflcm_attrs_cmp and rule:manager_and_owner or role:admin" + +# Heal a VNF instance. +# POST /vnflcm/v1/vnf_instances/{vnfInstanceId}/heal +"os_nfv_orchestration_api:vnf_instances:heal": "rule:vnflcm_attrs_cmp and rule:manager_and_owner or role:admin" + +# Scale a VNF instance. +# POST /vnflcm/v1/vnf_instances/{vnfInstanceId}/scale +"os_nfv_orchestration_api:vnf_instances:scale": "rule:vnflcm_attrs_cmp and rule:manager_and_owner or role:admin" + +# Query an Individual VNF LCM operation occurrence. +# GET /vnflcm/v1/vnf_lcm_op_occs/{vnfLcmOpOccId} +"os_nfv_orchestration_api:vnf_instances:show_lcm_op_occs": "rule:admin_or_owner" + +# Query VNF LCM operation occurrence. +# GET /vnflcm/v1/vnf_lcm_op_occs +"os_nfv_orchestration_api:vnf_instances:list_lcm_op_occs": "rule:admin_or_owner" + +# Query VNF instances. +# GET /vnflcm/v1/vnf_instances +"os_nfv_orchestration_api:vnf_instances:index": "rule:vnflcm_attrs_cmp and rule:owner or role:admin" + +# Delete an Individual VNF instance. +# DELETE /vnflcm/v1/vnf_instances/{vnfInstanceId} +"os_nfv_orchestration_api:vnf_instances:delete": "rule:vnflcm_delete_attrs_cmp and rule:manager_and_owner or role:admin" + +# Update an Individual VNF instance. +# PATCH /vnflcm/v1/vnf_instances/{vnfInstanceId} +"os_nfv_orchestration_api:vnf_instances:update_vnf": "rule:vnflcm_attrs_cmp and rule:manager_and_owner or role:admin" + +# Rollback a VNF instance. +# POST /vnflcm/v1/vnf_lcm_op_occs/{vnfLcmOpOccId}/rollback +"os_nfv_orchestration_api:vnf_instances:rollback": "rule:admin_or_owner" + +# Cancel a VNF instance. +# POST /vnflcm/v1/vnf_lcm_op_occs/{vnfLcmOpOccId}/cancel +"os_nfv_orchestration_api:vnf_instances:cancel": "rule:admin_or_owner" + +# Fail a VNF instance. +# POST /vnflcm/v1/vnf_lcm_op_occs/{vnfLcmOpOccId}/fail +"os_nfv_orchestration_api:vnf_instances:fail": "rule:admin_or_owner" + +# Retry a VNF instance. +# POST /vnflcm/v1/vnf_lcm_op_occs/{vnfLcmOpOccId}/retry +"os_nfv_orchestration_api:vnf_instances:retry": "rule:admin_or_owner" + +# Change external VNF connectivity. +# POST /vnflcm/v1/vnf_instances/{vnfInstanceId}/change_ext_conn +"os_nfv_orchestration_api:vnf_instances:change_ext_conn": "rule:vnflcm_attrs_cmp and rule:manager_and_owner or role:admin" + +# Get API Versions. +# GET /vnflcm/v2/api_versions +"os_nfv_orchestration_api_v2:vnf_instances:api_versions": "@" + +# Creates VNF instance. +# POST /vnflcm/v2/vnf_instances +"os_nfv_orchestration_api_v2:vnf_instances:create": "rule:vnflcm_create_attrs_cmp and rule:manager_and_owner or role:admin" + +# Query VNF instances. +# GET /vnflcm/v2/vnf_instances +"os_nfv_orchestration_api_v2:vnf_instances:index": "rule:vnflcm_attrs_cmp and rule:owner or role:admin" + +# Query an Individual VNF instance. +# GET /vnflcm/v2/vnf_instances/{vnfInstanceId} +"os_nfv_orchestration_api_v2:vnf_instances:show": "rule:vnflcm_attrs_cmp and rule:owner or role:admin" + +# Delete an Individual VNF instance. +# DELETE /vnflcm/v2/vnf_instances/{vnfInstanceId} +"os_nfv_orchestration_api_v2:vnf_instances:delete": "rule:vnflcm_delete_attrs_cmp and rule:manager_and_owner or role:admin" + +# Modify VNF instance information. +# PATCH /vnflcm/v2/vnf_instances/{vnfInstanceId} +"os_nfv_orchestration_api_v2:vnf_instances:update": "rule:vnflcm_attrs_cmp and rule:manager_and_owner or role:admin" + +# Instantiate VNF instance. +# POST /vnflcm/v2/vnf_instances/{vnfInstanceId}/instantiate +"os_nfv_orchestration_api_v2:vnf_instances:instantiate": "rule:vnflcm_inst_attrs_cmp and rule:manager_and_owner or role:admin" + +# Terminate VNF instance. +# POST /vnflcm/v2/vnf_instances/{vnfInstanceId}/terminate +"os_nfv_orchestration_api_v2:vnf_instances:terminate": "rule:vnflcm_attrs_cmp and rule:manager_and_owner or role:admin" + +# Scale VNF instance. +# POST /vnflcm/v2/vnf_instances/{vnfInstanceId}/scale +"os_nfv_orchestration_api_v2:vnf_instances:scale": "rule:vnflcm_attrs_cmp and rule:manager_and_owner or role:admin" + +# Heal VNF instance. +# POST /vnflcm/v2/vnf_instances/{vnfInstanceId}/heal +"os_nfv_orchestration_api_v2:vnf_instances:heal": "rule:vnflcm_attrs_cmp and rule:manager_and_owner or role:admin" + +# Change external VNF connectivity. +# POST /vnflcm/v2/vnf_instances/{vnfInstanceId}/change_ext_conn +"os_nfv_orchestration_api_v2:vnf_instances:change_ext_conn": "rule:vnflcm_attrs_cmp and rule:manager_and_owner or role:admin" + +# Change VNF package. +# POST /vnflcm/v2/vnf_instances/{vnfInstanceId}/change_vnfpkg +"os_nfv_orchestration_api_v2:vnf_instances:change_vnfpkg": "rule:vnflcm_attrs_cmp and rule:manager_and_owner or role:admin" + +# Create subscription. +# POST /vnflcm/v2/subscriptions +"os_nfv_orchestration_api_v2:vnf_instances:subscription_create": "@" + +# List subscription. +# GET /vnflcm/v2/subscriptions +"os_nfv_orchestration_api_v2:vnf_instances:subscription_list": "@" + +# Show subscription. +# GET /vnflcm/v2/vnf_instances/{subscriptionId} +"os_nfv_orchestration_api_v2:vnf_instances:subscription_show": "@" + +# Delete subscription. +# DELETE /vnflcm/v2/vnf_instances/{subscriptionId} +"os_nfv_orchestration_api_v2:vnf_instances:subscription_delete": "@" + +# List VnfLcmOpOcc. +# GET /vnflcm/v2/vnf_lcm_op_occs +"os_nfv_orchestration_api_v2:vnf_instances:lcm_op_occ_list": "@" + +# Show VnfLcmOpOcc. +# GET /vnflcm/v2/vnf_lcm_op_occs/{vnfLcmOpOccId} +"os_nfv_orchestration_api_v2:vnf_instances:lcm_op_occ_show": "@" + +# Retry VnfLcmOpOcc. +# POST /vnflcm/v2/vnf_lcm_op_occs/{vnfLcmOpOccId}/retry +"os_nfv_orchestration_api_v2:vnf_instances:lcm_op_occ_retry": "@" + +# Rollback VnfLcmOpOcc. +# POST /vnflcm/v2/vnf_lcm_op_occs/{vnfLcmOpOccId}/rollback +"os_nfv_orchestration_api_v2:vnf_instances:lcm_op_occ_rollback": "@" + +# Fail VnfLcmOpOcc. +# POST /vnflcm/v2/vnf_lcm_op_occs/{vnfLcmOpOccId}/fail +"os_nfv_orchestration_api_v2:vnf_instances:lcm_op_occ_fail": "@" + +# Delete VnfLcmOpOcc. +# DELETE /vnflcm/v2/vnf_lcm_op_occs/{vnfLcmOpOccId} +"os_nfv_orchestration_api_v2:vnf_instances:lcm_op_occ_delete": "@" diff --git a/playbooks/devstack/pre.yaml b/playbooks/devstack/pre.yaml index 67453bd79..003f68f51 100644 --- a/playbooks/devstack/pre.yaml +++ b/playbooks/devstack/pre.yaml @@ -15,6 +15,8 @@ when: setup_multi_az is defined and setup_multi_az | bool - role: setup-fake-https-server when: https_setup is defined and https_setup | bool + - role: config-enhanced-policy + when: config_enhanced_policy is defined and config_enhanced_policy | bool - role: bindep bindep_profile: test bindep_dir: "{{ zuul_work_dir }}" diff --git a/releasenotes/notes/enhanced-tacker-policy-f477637776771294.yaml b/releasenotes/notes/enhanced-tacker-policy-f477637776771294.yaml new file mode 100644 index 000000000..1d7c167d2 --- /dev/null +++ b/releasenotes/notes/enhanced-tacker-policy-f477637776771294.yaml @@ -0,0 +1,6 @@ +--- +features: + - | + Enhance the existing Tacker policy function so that users can obtain more + fine-grained access control based on user roles and VNF information for + API resources. diff --git a/roles/config-enhanced-policy/tasks/main.yaml b/roles/config-enhanced-policy/tasks/main.yaml new file mode 100644 index 000000000..94de38865 --- /dev/null +++ b/roles/config-enhanced-policy/tasks/main.yaml @@ -0,0 +1,12 @@ +- block: + - name: Copy policy.yaml + copy: + src: "{{ devstack_base_dir }}/tacker/etc/tacker/enhanced_tacker_policy.yaml.sample" + dest: "/etc/tacker/policy.yaml" + remote_src: true + mode: 0644 + owner: stack + group: stack + become: yes + when: + - inventory_hostname == 'controller-tacker' diff --git a/tacker/api/vnflcm/v1/controller.py b/tacker/api/vnflcm/v1/controller.py index 54e319e02..1d39d1ebb 100644 --- a/tacker/api/vnflcm/v1/controller.py +++ b/tacker/api/vnflcm/v1/controller.py @@ -288,7 +288,7 @@ class VnfLcmController(wsgi.Controller): vim_client_obj = vim_client.VimClient() try: - vim_client_obj.get_vim( + return vim_client_obj.get_vim( context, vim_id, region_name=region_name) except nfvo.VimDefaultNotDefined as exp: raise webob.exc.HTTPBadRequest(explanation=exp.message) @@ -439,7 +439,8 @@ class VnfLcmController(wsgi.Controller): @validation.schema(vnf_lcm.create) def create(self, request, body): context = request.environ['tacker.context'] - context.can(vnf_lcm_policies.VNFLCM % 'create') + if not CONF.oslo_policy.enhanced_tacker_policy: + context.can(vnf_lcm_policies.VNFLCM % 'create') try: req_body = utils.convert_camelcase_to_snakecase(body) vnfd_id = req_body.get('vnfd_id') @@ -466,6 +467,10 @@ class VnfLcmController(wsgi.Controller): vnfd_id) return self._make_problem_detail( msg, 500, 'Internal Server Error') + if CONF.oslo_policy.enhanced_tacker_policy: + context.can(vnf_lcm_policies.VNFLCM % 'create', + target={'vendor': vnfd.vnf_provider}) + try: # get default vim information vim_client_obj = vim_client.VimClient() @@ -489,6 +494,17 @@ class VnfLcmController(wsgi.Controller): try: vnf_instance.create() + vim_conn = objects.VimConnectionInfo( + id=uuidutils.generate_uuid(), + vim_id=default_vim.get('vim_id'), + vim_type=default_vim.get('vim_type'), + access_info={}, + interface_info={}, + extra=default_vim.get('extra', {}) + ) + vnf_instance.vim_connection_info = [vim_conn] + vnf_instance.save() + # create entry to 'vnf' table and 'vnf_attribute' table attributes = {'placement_attr': default_vim. get('placement_attr', {})} @@ -560,12 +576,52 @@ class VnfLcmController(wsgi.Controller): return vnf_package_info[0] + def _get_policy_target(self, vnf_instance): + vendor = vnf_instance.vnf_provider + + area = None + if vnf_instance.vim_connection_info: + for connection_info in vnf_instance.vim_connection_info: + area = connection_info.extra.get('area') + if area: + break + + tenant = None + if (vnf_instance.instantiation_state == + fields.VnfInstanceState.INSTANTIATED): + if vnf_instance.vnf_metadata: + tenant = vnf_instance.vnf_metadata.get('namespace') + + # TODO(kexuesheng): Add steps to get tenant of VNFs deployed + # in OpenStack VIM. This is a temporary workaround until that + # information is available. + if vnf_instance.vim_connection_info: + vim_type = vnf_instance.vim_connection_info[0].vim_type + if vim_type in ["openstack", "ETSINFV.OPENSTACK_KEYSTONE.v_2"]: + tenant = '*' + else: + tenant = '*' + + target = { + 'vendor': vendor, + 'area': area, + 'tenant': tenant + } + + target = {k: v for k, v in target.items() if v is not None} + + return target + @wsgi.response(http_client.OK) @wsgi.expected_errors((http_client.FORBIDDEN, http_client.NOT_FOUND)) def show(self, request, id): context = request.environ['tacker.context'] - context.can(vnf_lcm_policies.VNFLCM % 'show') + if not CONF.oslo_policy.enhanced_tacker_policy: + context.can(vnf_lcm_policies.VNFLCM % 'show') vnf_instance = self._get_vnf_instance(context, id) + if CONF.oslo_policy.enhanced_tacker_policy: + context.can(vnf_lcm_policies.VNFLCM % 'show', + target=self._get_policy_target(vnf_instance)) return self._view_builder.show(vnf_instance) @@ -579,8 +635,8 @@ class VnfLcmController(wsgi.Controller): @api_common.validate_supported_params({'filter', 'nextpage_opaque_marker', 'all_records'}) def index(self, request): - if 'tacker.context' in request.environ: - context = request.environ['tacker.context'] + context = request.environ['tacker.context'] + if not CONF.oslo_policy.enhanced_tacker_policy: context.can(vnf_lcm_policies.VNFLCM % 'index') filters = request.GET.get('filter') @@ -611,6 +667,13 @@ class VnfLcmController(wsgi.Controller): return self._make_problem_detail( str(e), 500, title='Internal Server Error') + if CONF.oslo_policy.enhanced_tacker_policy: + result = [vnf_instance for vnf_instance in result + if context.can( + vnf_lcm_policies.VNFLCM % 'index', + target=self._get_policy_target(vnf_instance), + fatal=False)] + result = self._view_builder.index(result) res = webob.Response(content_type='application/json') @@ -650,6 +713,10 @@ class VnfLcmController(wsgi.Controller): context = request.environ['tacker.context'] vnf_instance = self._get_vnf_instance(context, id) + if CONF.oslo_policy.enhanced_tacker_policy: + context.can(vnf_lcm_policies.VNFLCM % 'delete', + target=self._get_policy_target(vnf_instance)) + vnf = self._get_vnf(context, id) self._delete(context, vnf_instance, vnf) @@ -683,7 +750,12 @@ class VnfLcmController(wsgi.Controller): req_body, context=context) # validate the vim connection id passed through request is exist or not - self._validate_vim_connection(context, instantiate_vnf_request) + vim_res = self._validate_vim_connection( + context, instantiate_vnf_request) + + if instantiate_vnf_request.vim_connection_info: + instantiate_vnf_request.vim_connection_info[0].extra = vim_res.get( + 'extra', {}) vnf_instance.task_state = fields.VnfInstanceTaskState.INSTANTIATING vnf_instance.save() @@ -715,10 +787,14 @@ class VnfLcmController(wsgi.Controller): @validation.schema(vnf_lcm.instantiate) def instantiate(self, request, id, body): context = request.environ['tacker.context'] - context.can(vnf_lcm_policies.VNFLCM % 'instantiate') + if not CONF.oslo_policy.enhanced_tacker_policy: + context.can(vnf_lcm_policies.VNFLCM % 'instantiate') vnf = self._get_vnf(context, id) vnf_instance = self._get_vnf_instance(context, id) + if CONF.oslo_policy.enhanced_tacker_policy: + context.can(vnf_lcm_policies.VNFLCM % 'instantiate', + target=self._get_policy_target(vnf_instance)) return self._instantiate(context, vnf_instance, vnf, body) @@ -766,10 +842,14 @@ class VnfLcmController(wsgi.Controller): @validation.schema(vnf_lcm.terminate) def terminate(self, request, id, body): context = request.environ['tacker.context'] - context.can(vnf_lcm_policies.VNFLCM % 'terminate') + if not CONF.oslo_policy.enhanced_tacker_policy: + context.can(vnf_lcm_policies.VNFLCM % 'terminate') vnf = self._get_vnf(context, id) vnf_instance = self._get_vnf_instance(context, id) + if CONF.oslo_policy.enhanced_tacker_policy: + context.can(vnf_lcm_policies.VNFLCM % 'terminate', + target=self._get_policy_target(vnf_instance)) return self._terminate(context, vnf_instance, vnf, body) @check_vnf_status_and_error_point(action="heal", @@ -822,10 +902,14 @@ class VnfLcmController(wsgi.Controller): @validation.schema(vnf_lcm.heal) def heal(self, request, id, body): context = request.environ['tacker.context'] - context.can(vnf_lcm_policies.VNFLCM % 'heal') + if not CONF.oslo_policy.enhanced_tacker_policy: + context.can(vnf_lcm_policies.VNFLCM % 'heal') vnf = self._get_vnf(context, id) vnf_instance = self._get_vnf_instance(context, id) + if CONF.oslo_policy.enhanced_tacker_policy: + context.can(vnf_lcm_policies.VNFLCM % 'heal', + target=self._get_policy_target(vnf_instance)) if vnf_instance.instantiation_state not in \ [fields.VnfInstanceState.INSTANTIATED]: @@ -865,7 +949,12 @@ class VnfLcmController(wsgi.Controller): @wsgi.expected_errors((http_client.FORBIDDEN, http_client.NOT_FOUND)) def update(self, request, id, body): context = request.environ['tacker.context'] - context.can(vnf_lcm_policies.VNFLCM % 'update_vnf') + if CONF.oslo_policy.enhanced_tacker_policy: + vnf_instance = self._get_vnf_instance(context, id) + context.can(vnf_lcm_policies.VNFLCM % 'update_vnf', + target=self._get_policy_target(vnf_instance)) + else: + context.can(vnf_lcm_policies.VNFLCM % 'update_vnf') # get body body_data = {} @@ -1343,7 +1432,8 @@ class VnfLcmController(wsgi.Controller): http_client.NOT_FOUND, http_client.CONFLICT)) def scale(self, request, id, body): context = request.environ['tacker.context'] - context.can(vnf_lcm_policies.VNFLCM % 'scale') + if not CONF.oslo_policy.enhanced_tacker_policy: + context.can(vnf_lcm_policies.VNFLCM % 'scale') try: vnf_info = self._vnfm_plugin.get_vnf(context, id) @@ -1351,6 +1441,9 @@ class VnfLcmController(wsgi.Controller): return self._make_problem_detail( 'VNF IS NOT ACTIVE', 409, title='VNF IS NOT ACTIVE') vnf_instance = self._get_vnf_instance(context, id) + if CONF.oslo_policy.enhanced_tacker_policy: + context.can(vnf_lcm_policies.VNFLCM % 'scale', + target=self._get_policy_target(vnf_instance)) if not vnf_instance.instantiated_vnf_info.scale_status: return self._make_problem_detail( 'NOT SCALE VNF', 409, title='NOT SCALE VNF') @@ -1361,6 +1454,8 @@ class VnfLcmController(wsgi.Controller): except webob.exc.HTTPNotFound as inst_e: return self._make_problem_detail( str(inst_e), 404, title='VNF NOT FOUND') + except exceptions.PolicyNotAuthorized: + raise except Exception as e: LOG.error(traceback.format_exc()) return self._make_problem_detail( @@ -1819,10 +1914,14 @@ class VnfLcmController(wsgi.Controller): @validation.schema(vnf_lcm.change_ext_conn) def change_ext_conn(self, request, id, body): context = request.environ['tacker.context'] - context.can(vnf_lcm_policies.VNFLCM % 'change_ext_conn') + if not CONF.oslo_policy.enhanced_tacker_policy: + context.can(vnf_lcm_policies.VNFLCM % 'change_ext_conn') vnf = self._get_vnf(context, id) vnf_instance = self._get_vnf_instance(context, id) + if CONF.oslo_policy.enhanced_tacker_policy: + context.can(vnf_lcm_policies.VNFLCM % 'change_ext_conn', + target=self._get_policy_target(vnf_instance)) if (vnf_instance.instantiation_state != fields.VnfInstanceState.INSTANTIATED): return self._make_problem_detail( diff --git a/tacker/api/vnfpkgm/v1/controller.py b/tacker/api/vnfpkgm/v1/controller.py index ec292afc1..0b0bfcb2f 100644 --- a/tacker/api/vnfpkgm/v1/controller.py +++ b/tacker/api/vnfpkgm/v1/controller.py @@ -104,7 +104,8 @@ class VnfPkgmController(wsgi.Controller): @wsgi.expected_errors((http_client.FORBIDDEN, http_client.NOT_FOUND)) def show(self, request, id): context = request.environ['tacker.context'] - context.can(vnf_package_policies.VNFPKGM % 'show') + if not CONF.oslo_policy.enhanced_tacker_policy: + context.can(vnf_package_policies.VNFPKGM % 'show') # check if id is of type uuid format if not uuidutils.is_uuid_like(id): @@ -115,18 +116,33 @@ class VnfPkgmController(wsgi.Controller): vnf_package = vnf_package_obj.VnfPackage.get_by_id( request.context, id, expected_attrs=[ "vnf_deployment_flavours", "vnfd", "vnf_artifacts"]) + if CONF.oslo_policy.enhanced_tacker_policy: + context.can(vnf_package_policies.VNFPKGM % 'show', + target=self._get_policy_target(vnf_package)) except exceptions.VnfPackageNotFound: msg = _("Can not find requested vnf package: %s") % id raise webob.exc.HTTPNotFound(explanation=msg) return self._view_builder.show(vnf_package) + def _get_policy_target(self, vnf_package): + if vnf_package.onboarding_state == \ + fields.PackageOnboardingStateType.ONBOARDED: + + vendor = (vnf_package.vnfd.get('vnf_provider') + if vnf_package.vnfd is not None else None) + + return {'vendor': vendor} if vendor else {} + + else: + return {'vendor': '*'} + @wsgi.response(http_client.OK) @wsgi.expected_errors((http_client.BAD_REQUEST, http_client.FORBIDDEN)) @validation.query_schema(vnf_packages.query_params_v1) def index(self, request): - if 'tacker.context' in request.environ: - context = request.environ['tacker.context'] + context = request.environ['tacker.context'] + if not CONF.oslo_policy.enhanced_tacker_policy: context.can(vnf_package_policies.VNFPKGM % 'index') search_opts = {} @@ -178,6 +194,13 @@ class VnfPkgmController(wsgi.Controller): return self._make_problem_detail( str(e), 500, title='Internal Server Error') + if CONF.oslo_policy.enhanced_tacker_policy: + result = [vnf_package for vnf_package in result + if context.can( + vnf_package_policies.VNFPKGM % 'index', + target=self._get_policy_target(vnf_package), + fatal=False)] + results = self._view_builder.index(result, all_fields=all_fields, exclude_fields=exclude_fields, @@ -206,9 +229,13 @@ class VnfPkgmController(wsgi.Controller): http_client.CONFLICT)) def delete(self, request, id): context = request.environ['tacker.context'] - context.can(vnf_package_policies.VNFPKGM % 'delete') + if not CONF.oslo_policy.enhanced_tacker_policy: + context.can(vnf_package_policies.VNFPKGM % 'delete') vnf_package = self._get_vnf_package(id, request) + if CONF.oslo_policy.enhanced_tacker_policy: + context.can(vnf_package_policies.VNFPKGM % 'delete', + target=self._get_policy_target(vnf_package)) if (vnf_package.operational_state == fields.PackageOperationalStateType.ENABLED or @@ -244,9 +271,13 @@ class VnfPkgmController(wsgi.Controller): http_client.REQUESTED_RANGE_NOT_SATISFIABLE)) def fetch_vnf_package_content(self, request, id): context = request.environ['tacker.context'] - context.can(vnf_package_policies.VNFPKGM % 'fetch_package_content') + if not CONF.oslo_policy.enhanced_tacker_policy: + context.can(vnf_package_policies.VNFPKGM % 'fetch_package_content') vnf_package = self._get_vnf_package(id, request) + if CONF.oslo_policy.enhanced_tacker_policy: + context.can(vnf_package_policies.VNFPKGM % 'fetch_package_content', + target=self._get_policy_target(vnf_package)) if vnf_package.onboarding_state != \ fields.PackageOnboardingStateType.ONBOARDED: @@ -367,7 +398,9 @@ class VnfPkgmController(wsgi.Controller): http_client.CONFLICT)) def upload_vnf_package_content(self, request, id, body): context = request.environ['tacker.context'] - context.can(vnf_package_policies.VNFPKGM % 'upload_package_content') + if not CONF.oslo_policy.enhanced_tacker_policy: + context.can( + vnf_package_policies.VNFPKGM % 'upload_package_content') # check if id is of type uuid format if not uuidutils.is_uuid_like(id): @@ -404,6 +437,13 @@ class VnfPkgmController(wsgi.Controller): try: (location, size, checksum, multihash, loc_meta) = glance_store.store_csar(context, id, body) + if CONF.oslo_policy.enhanced_tacker_policy: + zip_path = glance_store.load_csar(vnf_package.id, location) + vnf_data, flavours, vnf_artifacts = csar_utils.load_csar_data( + context.elevated(), vnf_package.id, zip_path) + context.can( + vnf_package_policies.VNFPKGM % 'upload_package_content', + target={'vendor': vnf_data.get('provider')}) except exceptions.UploadFailedToGlanceStore: with excutils.save_and_reraise_exception(): vnf_package.onboarding_state = ( @@ -413,6 +453,19 @@ class VnfPkgmController(wsgi.Controller): except Exception as e: return self._make_problem_detail( 'Internal Server Error', str(e), 500) + except exceptions.Forbidden: + with excutils.save_and_reraise_exception(): + vnf_package.onboarding_state = ( + fields.PackageOnboardingStateType.CREATED) + try: + # Delete from glance store + glance_store.delete_csar(context, vnf_package.id, + location) + csar_utils.delete_csar_data(vnf_package.id) + vnf_package.save() + except Exception as e: + return self._make_problem_detail( + 'Internal Server Error', str(e), 500) vnf_package.algorithm = CONF.vnf_package.hashing_algorithm vnf_package.hash = multihash @@ -475,9 +528,13 @@ class VnfPkgmController(wsgi.Controller): @validation.schema(vnf_packages.patch) def patch(self, request, id, body): context = request.environ['tacker.context'] - context.can(vnf_package_policies.VNFPKGM % 'patch') + if not CONF.oslo_policy.enhanced_tacker_policy: + context.can(vnf_package_policies.VNFPKGM % 'patch') old_vnf_package = self._get_vnf_package(id, request) + if CONF.oslo_policy.enhanced_tacker_policy: + context.can(vnf_package_policies.VNFPKGM % 'patch', + target=self._get_policy_target(old_vnf_package)) vnf_package = old_vnf_package.obj_clone() user_data = body.get('userDefinedData') @@ -529,7 +586,8 @@ class VnfPkgmController(wsgi.Controller): http_client.INTERNAL_SERVER_ERROR)) def get_vnf_package_vnfd(self, request, id): context = request.environ['tacker.context'] - context.can(vnf_package_policies.VNFPKGM % 'get_vnf_package_vnfd') + if not CONF.oslo_policy.enhanced_tacker_policy: + context.can(vnf_package_policies.VNFPKGM % 'get_vnf_package_vnfd') valid_accept_headers = ['application/zip', 'text/plain'] accept_headers = request.headers['Accept'].split(',') @@ -543,6 +601,9 @@ class VnfPkgmController(wsgi.Controller): valid_accept_headers)}) vnf_package = self._get_vnf_package(id, request) + if CONF.oslo_policy.enhanced_tacker_policy: + context.can(vnf_package_policies.VNFPKGM % 'get_vnf_package_vnfd', + target=self._get_policy_target(vnf_package)) if vnf_package.onboarding_state != \ fields.PackageOnboardingStateType.ONBOARDED: @@ -586,7 +647,8 @@ class VnfPkgmController(wsgi.Controller): def fetch_vnf_package_artifacts(self, request, id, artifact_path): context = request.environ['tacker.context'] # get policy - context.can(vnf_package_policies.VNFPKGM % 'fetch_artifact') + if not CONF.oslo_policy.enhanced_tacker_policy: + context.can(vnf_package_policies.VNFPKGM % 'fetch_artifact') # get vnf_package if not uuidutils.is_uuid_like(id): @@ -597,6 +659,10 @@ class VnfPkgmController(wsgi.Controller): vnf_package = vnf_package_obj.VnfPackage.get_by_id( request.context, id, expected_attrs=["vnf_artifacts"]) + if CONF.oslo_policy.enhanced_tacker_policy: + # get policy + context.can(vnf_package_policies.VNFPKGM % 'fetch_artifact', + target=self._get_policy_target(vnf_package)) except exceptions.VnfPackageNotFound: msg = _("Can not find requested vnf package: %s") % id raise webob.exc.HTTPNotFound(explanation=msg) diff --git a/tacker/common/utils.py b/tacker/common/utils.py index f8a32dcee..2eee42193 100644 --- a/tacker/common/utils.py +++ b/tacker/common/utils.py @@ -174,6 +174,18 @@ def is_valid_ipv4(address): return False +def is_valid_area(area): + """Verify that the area attribute is valid. + + Area attribute is an area-region pair. The value of this attribute should + be a string in the format of "area@region". + """ + try: + return re.match(r'\w+@\w+', area) is not None + except Exception: + return False + + def change_memory_unit(mem, to): """Change the memory value(mem) based on the unit('to') specified. diff --git a/tacker/conf/__init__.py b/tacker/conf/__init__.py index e5afcc6ee..cb6e5954c 100644 --- a/tacker/conf/__init__.py +++ b/tacker/conf/__init__.py @@ -18,6 +18,7 @@ from oslo_config import cfg from tacker.conf import conductor from tacker.conf import coordination +from tacker.conf import policy from tacker.conf import vnf_lcm from tacker.conf import vnf_package @@ -29,3 +30,4 @@ vnf_lcm.register_opts(CONF) conductor.register_opts(CONF) coordination.register_opts(CONF) glance_store.register_opts(CONF) +policy.register_opts(CONF) diff --git a/tacker/conf/policy.py b/tacker/conf/policy.py new file mode 100644 index 000000000..4850ff3ea --- /dev/null +++ b/tacker/conf/policy.py @@ -0,0 +1,33 @@ +# 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_config import cfg + + +CONF = cfg.CONF + +policy_opts = [ + cfg.BoolOpt('enhanced_tacker_policy', + default=False, + help=_('Enable enhanced tacker policy')), +] + + +def register_opts(conf): + conf.register_opts(policy_opts, group='oslo_policy') + + +def list_opts(): + return {'oslo_policy': policy_opts} diff --git a/tacker/context.py b/tacker/context.py index b5746ac7e..971b55fac 100644 --- a/tacker/context.py +++ b/tacker/context.py @@ -148,11 +148,14 @@ class ContextBase(oslo_context.RequestContext): :return: returns a non-False value (not necessarily "True") if authorized and False if not authorized and fatal is False. """ - if target is None: - target = {'project_id': self.tenant_id, - 'user_id': self.user_id} + tgt = { + 'project_id': self.tenant_id, + 'user_id': self.user_id + } + if target is not None: + tgt.update(target) try: - return policy.authorize(self, action, target) + return policy.authorize(self, action, tgt) except exceptions.Forbidden: if fatal: raise diff --git a/tacker/policy.py b/tacker/policy.py index e7d72642f..970435f43 100644 --- a/tacker/policy.py +++ b/tacker/policy.py @@ -14,6 +14,7 @@ # under the License. from collections import abc +import copy import re import sys @@ -28,6 +29,7 @@ from oslo_utils import importutils from tacker._i18n import _ from tacker.api.v1 import attributes from tacker.common import exceptions +from tacker.common.utils import is_valid_area from tacker import policies @@ -60,10 +62,133 @@ def init(conf=cfg.CONF, policy_file=None): _ENFORCER.load_rules() +def _pre_enhanced_policy_check(target, credentials): + """Preprocesses target and credentials for enhanced tacker policy. + + This method does the following things: + 1) Convert special roles to enhanced policy attributes in credentials. + Note: Special roles are roles that have prefixes 'AREA_', 'VENDOR_', + or 'TENANT_'. + Example:: + + Before conversion: + credentials = { + 'roles': [ + 'AREA_area_A@region_A', + 'VENDOR_company_A', + 'TENANT_default' + ] + } + After conversion: + credentials = { + 'roles': [ + 'AREA_area_A@region_A', + 'VENDOR_company_A', + 'TENANT_default' + ], + 'area': ['*', 'area_A@region_A'], + 'vendor': ['*', 'company_A'], + 'tenant: ['*', 'default'] + } + 2) Convert special value `all` to the corresponding attribute value in + target. + + :param target: a dictionary of the attributes of the object + being accessed. + :param credentials: The information about the user performing the action. + :return tgt: The preprocessed target is returned. + :return user_attrs: The preprocessed credentials is returned. + """ + if not cfg.CONF.oslo_policy.enhanced_tacker_policy: + return target, credentials + + if target is None: + tgt = {} + else: + tgt = copy.copy(target) + + LOG.debug(f'target: {target}') + + convert_map = { + 'area_': 'area', + 'vendor_': 'vendor', + 'tenant_': 'tenant' + } + user_attrs = { + 'area': ['*'], + 'vendor': ['*'], + 'tenant': ['*'] + } + # Convert special roles to enhanced policy attributes in credentials. + for role in credentials.get('roles'): + role = role.lower() + for prefix, key in convert_map.items(): + if role.startswith(prefix): + attr = role[len(prefix):] + if attr: + user_attrs[key].append(attr) + + common_keys = user_attrs.keys() & tgt.keys() + + # Convert special value `all` to the corresponding attribute value in + # target. + for key in common_keys: + tgt[key] = tgt[key].lower() + attrs = user_attrs.get(key) + if tgt.get(key) == '*': + continue + to_remove = [] + if 'area' == key: + if not is_valid_area(tgt.get(key)): + continue + for attr in attrs: + if not is_valid_area(attr): + continue + if 'all@all' == attr: + # example: + # target = {'area': 'area_A@region_A'} + # then: + # 'all@all' -> 'area_A@region_A' + to_remove.append('all@all') + attrs.append(tgt.get(key)) + elif attr.startswith('all@'): + to_remove.append(attr) + area, region = attr.split('@', 1) + t_area, t_region = tgt.get(key).split('@', 1) + if region == t_region: + # example: + # target = {'area': 'area_A@region_A'} + # then: + # 'all@region_A' -> 'area_A@region_A' + attrs.append(f'{t_area}@{region}') + # else: + # example: + # target = {'area': 'area_A@region_B'} + # then: + # 'all@region_A' -> to be removed. + + else: + for attr in attrs: + if 'all' == attr: + # example: + # target = {'vendor': 'company_A'} + # then: + # 'all' -> 'company_A' + to_remove.append('all') + attrs.append(tgt.get(key)) + for item in to_remove: + attrs.remove(item) + + user_attrs.update(credentials) + + return tgt, user_attrs + + def authorize(context, action, target, do_raise=True, exc=None): init() credentials = context.to_policy_values() + target, credentials = _pre_enhanced_policy_check(target, credentials) if not exc: exc = exceptions.PolicyNotAuthorized try: @@ -387,6 +512,15 @@ def check(context, action, target, plugin=None, might_not_exist=False, action, target, pluralized) + target = copy.copy(target) + if 'area' not in target: + area = target.get('extra', {}).get('area') + if area: + target.update({'area': area}) + if 'tenant_id' in target: + target['project_id'] = target['tenant_id'] + target, credentials = _pre_enhanced_policy_check(target, credentials) + result = _ENFORCER.enforce(match_rule, target, credentials, @@ -397,7 +531,8 @@ def check(context, action, target, plugin=None, might_not_exist=False, return result -def enforce(context, action, target, plugin=None, pluralized=None): +def enforce(context, action, target, plugin=None, pluralized=None, + exc=exceptions.PolicyNotAuthorized): """Verifies that the action is valid on the target in this context. :param context: tacker context @@ -410,9 +545,12 @@ def enforce(context, action, target, plugin=None, pluralized=None): Kept for backward compatibility. :param pluralized: pluralized case of resource e.g. firewall_policy -> pluralized = "firewall_policies" + :param exc: Class of the exception to raise if the check fails. + If not specified, :class:`PolicyNotAuthorized` will be used. - :raises oslo_policy.policy.PolicyNotAuthorized: - if verification fails. + :raises tacker.common.exceptions.PolicyNotAuthorized or exc specified by + caller: + if verification fails. """ # If we already know the context has admin rights do not perform an # additional check and authorize the operation @@ -422,10 +560,19 @@ def enforce(context, action, target, plugin=None, pluralized=None): action, target, pluralized) + target = copy.copy(target) + if 'area' not in target: + area = target.get('extra', {}).get('area') + if area: + target.update({'area': area}) + if 'tenant_id' in target: + target['project_id'] = target['tenant_id'] + target, credentials = _pre_enhanced_policy_check(target, credentials) + try: result = _ENFORCER.enforce(rule, target, credentials, action=action, - do_raise=True) - except policy.PolicyNotAuthorized: + do_raise=True, exc=exc) + except Exception: with excutils.save_and_reraise_exception(): log_rule_list(rule) LOG.error("Failed policy check for '%s'", action) diff --git a/tacker/sol_refactored/api/policies/vnflcm_v2.py b/tacker/sol_refactored/api/policies/vnflcm_v2.py index 504fffba7..741650c0e 100644 --- a/tacker/sol_refactored/api/policies/vnflcm_v2.py +++ b/tacker/sol_refactored/api/policies/vnflcm_v2.py @@ -36,6 +36,20 @@ VNF_LCM_OP_OCCS_PATH = V2_PATH + '/vnf_lcm_op_occs' VNF_LCM_OP_OCCS_ID_PATH = VNF_LCM_OP_OCCS_PATH + '/{vnfLcmOpOccId}' SERVER_NOTIFICATION_PATH = CONF.server_notification.uri_path_prefix +ENHANCED_POLICY_ACTIONS = ( + POLICY_NAME.format('create'), + POLICY_NAME.format('index'), + POLICY_NAME.format('show'), + POLICY_NAME.format('delete'), + POLICY_NAME.format('update'), + POLICY_NAME.format('instantiate'), + POLICY_NAME.format('terminate'), + POLICY_NAME.format('scale'), + POLICY_NAME.format('heal'), + POLICY_NAME.format('change_ext_conn'), + POLICY_NAME.format('change_vnfpkg'), +) + rules = [ policy.DocumentedRuleDefault( name=POLICY_NAME.format('api_versions'), diff --git a/tacker/sol_refactored/api/wsgi.py b/tacker/sol_refactored/api/wsgi.py index c5713e53d..8a6ea6bf7 100644 --- a/tacker/sol_refactored/api/wsgi.py +++ b/tacker/sol_refactored/api/wsgi.py @@ -24,6 +24,8 @@ from tacker.common import exceptions as common_ex from tacker import context from tacker.sol_refactored.api import api_version +from tacker.sol_refactored.api.policies.vnflcm_v2 import ( + ENHANCED_POLICY_ACTIONS) from tacker.sol_refactored.common import config from tacker.sol_refactored.common import exceptions as sol_ex @@ -159,7 +161,9 @@ class SolResource(object): return if action == 'reject': return - request.context.can(self.policy_name.format(action)) + if not (self.policy_name.format(action) in ENHANCED_POLICY_ACTIONS + and config.CONF.oslo_policy.enhanced_tacker_policy): + request.context.can(self.policy_name.format(action)) def _dispatch(self, request, action, action_args): controller_method = getattr(self.controller, action) diff --git a/tacker/sol_refactored/common/vim_utils.py b/tacker/sol_refactored/common/vim_utils.py index 98986140c..4fcd8a999 100644 --- a/tacker/sol_refactored/common/vim_utils.py +++ b/tacker/sol_refactored/common/vim_utils.py @@ -48,6 +48,7 @@ def vim_to_conn_info(vim): region = vim['placement_attr']['regions'][0] vim_auth = vim['vim_auth'] + extra = vim.get('extra', {}) if vim['vim_type'] == "openstack": # see. https://nfvwiki.etsi.org/index.php @@ -73,7 +74,8 @@ def vim_to_conn_info(vim): vimId=vim['vim_id'], vimType='ETSINFV.OPENSTACK_KEYSTONE.V_3', interfaceInfo=interface_info, - accessInfo=access_info + accessInfo=access_info, + extra=extra ) if vim['vim_type'] == "kubernetes": # When vimType is kubernetes, it will be converted to the vimType name @@ -116,7 +118,8 @@ def vim_to_conn_info(vim): vimId=vim['vim_id'], vimType=vim_type, interfaceInfo=interface_info, - accessInfo=access_info + accessInfo=access_info, + extra=extra ) raise sol_ex.SolException(sol_detail='not support vim type') diff --git a/tacker/sol_refactored/common/vnflcm_utils.py b/tacker/sol_refactored/common/vnflcm_utils.py index c809ea0bc..7ebef7670 100644 --- a/tacker/sol_refactored/common/vnflcm_utils.py +++ b/tacker/sol_refactored/common/vnflcm_utils.py @@ -33,8 +33,9 @@ from tacker.sol_refactored.objects.v2 import fields as v2fields # and the creation of lcmocc and the call to start_lcm_op are # all executed by the controller, notification driver, etc. @coordinate.lock_vnf_instance('{vnf_instance_id}') -def heal(context, vnf_instance_id, body): - inst = inst_utils.get_inst(context, vnf_instance_id) +def heal(context, vnf_instance_id, body, inst=None): + if not inst: + inst = inst_utils.get_inst(context, vnf_instance_id) if inst.instantiationState != 'INSTANTIATED': raise sol_ex.VnfInstanceIsNotInstantiated(inst_id=vnf_instance_id) @@ -73,8 +74,9 @@ def heal(context, vnf_instance_id, body): # and the creation of lcmocc and the call to start_lcm_op are # all executed by the controller, notification driver, etc. @coordinate.lock_vnf_instance('{vnf_instance_id}') -def scale(context, vnf_instance_id, body): - inst = inst_utils.get_inst(context, vnf_instance_id) +def scale(context, vnf_instance_id, body, inst=None): + if not inst: + inst = inst_utils.get_inst(context, vnf_instance_id) if inst.instantiationState != 'INSTANTIATED': raise sol_ex.VnfInstanceIsNotInstantiated(inst_id=vnf_instance_id) diff --git a/tacker/sol_refactored/controller/vnflcm_v2.py b/tacker/sol_refactored/controller/vnflcm_v2.py index f003f596c..b38074a5d 100644 --- a/tacker/sol_refactored/controller/vnflcm_v2.py +++ b/tacker/sol_refactored/controller/vnflcm_v2.py @@ -18,6 +18,7 @@ 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.policies.vnflcm_v2 import POLICY_NAME from tacker.sol_refactored.api.schemas import vnflcm_v2 as schema from tacker.sol_refactored.api import validator from tacker.sol_refactored.api import wsgi as sol_wsgi @@ -62,6 +63,10 @@ class VnfLcmControllerV2(sol_wsgi.SolAPIController): pkg_info = self.nfvo_client.get_vnf_package_info_vnfd( context, vnfd_id) + if config.CONF.oslo_policy.enhanced_tacker_policy: + context.can(POLICY_NAME.format('create'), + target={'vendor': pkg_info.vnfProvider}) + if pkg_info.operationalState != "ENABLED": raise sol_ex.VnfdIdNotEnabled(vnfd_id=vnfd_id) @@ -122,6 +127,11 @@ class VnfLcmControllerV2(sol_wsgi.SolAPIController): insts = inst_utils.get_inst_all(request.context, marker=pager.marker) + if config.CONF.oslo_policy.enhanced_tacker_policy: + insts = [inst for inst in insts if request.context.can( + POLICY_NAME.format('index'), + target=self._get_policy_target(inst), + fatal=False)] resp_body = self._inst_view.detail_list(insts, filters, selector, pager) @@ -130,7 +140,9 @@ class VnfLcmControllerV2(sol_wsgi.SolAPIController): def show(self, request, id): inst = inst_utils.get_inst(request.context, id) - + if config.CONF.oslo_policy.enhanced_tacker_policy: + request.context.can(POLICY_NAME.format('show'), + target=self._get_policy_target(inst)) resp_body = self._inst_view.detail(inst) return sol_wsgi.SolResponse(200, resp_body) @@ -139,7 +151,9 @@ class VnfLcmControllerV2(sol_wsgi.SolAPIController): def delete(self, request, id): context = request.context inst = inst_utils.get_inst(context, id) - + if config.CONF.oslo_policy.enhanced_tacker_policy: + context.can(POLICY_NAME.format('delete'), + target=self._get_policy_target(inst)) if inst.instantiationState != 'NOT_INSTANTIATED': raise sol_ex.VnfInstanceIsInstantiated(inst_id=id) @@ -158,7 +172,9 @@ class VnfLcmControllerV2(sol_wsgi.SolAPIController): def update(self, request, id, body): context = request.context inst = inst_utils.get_inst(context, id) - + if config.CONF.oslo_policy.enhanced_tacker_policy: + context.can(POLICY_NAME.format('update'), + target=self._get_policy_target(inst)) lcmocc_utils.check_lcmocc_in_progress(context, id) if (inst.instantiationState == 'NOT_INSTANTIATED' and 'vimConnectionInfo' in body): @@ -200,11 +216,54 @@ class VnfLcmControllerV2(sol_wsgi.SolAPIController): return sol_wsgi.SolResponse(202, None, location=location) + def _get_policy_target(self, vnf_instance): + vendor = vnf_instance.vnfProvider + + if vnf_instance.instantiationState == 'NOT_INSTANTIATED': + area = '*' + tenant = '*' + else: + vim_type = None + area = None + if vnf_instance.obj_attr_is_set('vimConnectionInfo'): + for _, vim_conn_info in vnf_instance.vimConnectionInfo.items(): + area = vim_conn_info.get('extra', {}).get('area') + vim_type = vim_conn_info.vimType + if area and vim_type: + break + + tenant = None + if (vnf_instance.obj_attr_is_set('instantiatedVnfInfo') and + vnf_instance.instantiatedVnfInfo.obj_attr_is_set( + 'metadata')): + + tenant = (vnf_instance.instantiatedVnfInfo + .metadata.get('namespace')) + + # TODO(kexuesheng): Add steps to get tenant of VNFs deployed + # in OpenStack VIM. This is a temporary workaround until that + # information is available. + if vim_type == "ETSINFV.OPENSTACK_KEYSTONE.V_3": + tenant = '*' + + target = { + 'vendor': vendor, + 'area': area, + 'tenant': tenant + } + + target = {k: v for k, v in target.items() if v is not None} + + return target + @validator.schema(schema.InstantiateVnfRequest_V200, '2.0.0') @coordinate.lock_vnf_instance('{id}') def instantiate(self, request, id, body): context = request.context inst = inst_utils.get_inst(context, id) + if config.CONF.oslo_policy.enhanced_tacker_policy: + context.can(POLICY_NAME.format('instantiate'), + target=self._get_policy_target(inst)) if inst.instantiationState != 'NOT_INSTANTIATED': raise sol_ex.VnfInstanceIsInstantiated(inst_id=id) @@ -239,6 +298,9 @@ class VnfLcmControllerV2(sol_wsgi.SolAPIController): def terminate(self, request, id, body): context = request.context inst = inst_utils.get_inst(context, id) + if config.CONF.oslo_policy.enhanced_tacker_policy: + context.can(POLICY_NAME.format('terminate'), + target=self._get_policy_target(inst)) if inst.instantiationState != 'INSTANTIATED': raise sol_ex.VnfInstanceIsNotInstantiated(inst_id=id) @@ -258,7 +320,12 @@ class VnfLcmControllerV2(sol_wsgi.SolAPIController): @validator.schema(schema.ScaleVnfRequest_V200, '2.0.0') def scale(self, request, id, body): context = request.context - lcmocc = vnflcm_utils.scale(context, id, body) + inst = inst_utils.get_inst(context, id) + if config.CONF.oslo_policy.enhanced_tacker_policy: + context.can(POLICY_NAME.format('scale'), + target=self._get_policy_target(inst)) + + lcmocc = vnflcm_utils.scale(context, id, body, inst=inst) location = lcmocc_utils.lcmocc_href(lcmocc.id, self.endpoint) @@ -267,7 +334,12 @@ class VnfLcmControllerV2(sol_wsgi.SolAPIController): @validator.schema(schema.HealVnfRequest_V200, '2.0.0') def heal(self, request, id, body): context = request.context - lcmocc = vnflcm_utils.heal(context, id, body) + inst = inst_utils.get_inst(context, id) + if config.CONF.oslo_policy.enhanced_tacker_policy: + context.can(POLICY_NAME.format('heal'), + target=self._get_policy_target(inst)) + + lcmocc = vnflcm_utils.heal(context, id, body, inst=inst) location = lcmocc_utils.lcmocc_href(lcmocc.id, self.endpoint) @@ -278,6 +350,9 @@ class VnfLcmControllerV2(sol_wsgi.SolAPIController): def change_ext_conn(self, request, id, body): context = request.context inst = inst_utils.get_inst(context, id) + if config.CONF.oslo_policy.enhanced_tacker_policy: + context.can(POLICY_NAME.format('change_ext_conn'), + target=self._get_policy_target(inst)) if inst.instantiationState != 'INSTANTIATED': raise sol_ex.VnfInstanceIsNotInstantiated(inst_id=id) @@ -336,6 +411,9 @@ class VnfLcmControllerV2(sol_wsgi.SolAPIController): def change_vnfpkg(self, request, id, body): context = request.context inst = inst_utils.get_inst(context, id) + if config.CONF.oslo_policy.enhanced_tacker_policy: + context.can(POLICY_NAME.format('change_vnfpkg'), + target=self._get_policy_target(inst)) vnfd_id = body['vnfdId'] if inst.instantiationState != 'INSTANTIATED': diff --git a/tacker/tests/etc/samples/etsi/nfv/test_enhanced_policy/BaseHOT/simple/base_hot_top.yaml b/tacker/tests/etc/samples/etsi/nfv/test_enhanced_policy/BaseHOT/simple/base_hot_top.yaml new file mode 100644 index 000000000..814b4574f --- /dev/null +++ b/tacker/tests/etc/samples/etsi/nfv/test_enhanced_policy/BaseHOT/simple/base_hot_top.yaml @@ -0,0 +1,38 @@ +heat_template_version: 2013-05-23 +description: 'Simple Base HOT for Sample VNF' + +parameters: + nfv: + type: json + +resources: + VDU1_scale: + type: OS::Heat::AutoScalingGroup + properties: + min_size: 1 + max_size: 2 + desired_capacity: 1 + resource: + type: vdu1.yaml + properties: + flavor: { get_param: [ nfv, VDU, VDU1, flavor ] } + image: { get_param: [ nfv, VDU, VDU1, image ] } + net1: { get_param: [ nfv, CP, CP1, network ] } + + VDU1_scale_scale_out: + type: OS::Heat::ScalingPolicy + properties: + scaling_adjustment: 1 + auto_scaling_group_id: + get_resource: VDU1_scale + adjustment_type: change_in_capacity + + VDU1_scale_scale_in: + type: OS::Heat::ScalingPolicy + properties: + scaling_adjustment: -1 + auto_scaling_group_id: + get_resource: VDU1_scale + adjustment_type: change_in_capacity + +outputs: {} diff --git a/tacker/tests/etc/samples/etsi/nfv/test_enhanced_policy/BaseHOT/simple/nested/vdu1.yaml b/tacker/tests/etc/samples/etsi/nfv/test_enhanced_policy/BaseHOT/simple/nested/vdu1.yaml new file mode 100644 index 000000000..2dbe84981 --- /dev/null +++ b/tacker/tests/etc/samples/etsi/nfv/test_enhanced_policy/BaseHOT/simple/nested/vdu1.yaml @@ -0,0 +1,28 @@ +heat_template_version: 2013-05-23 +description: "Template for test _generate_hot_from_tosca()." + +parameters: + flavor: + type: string + image: + type: string + net1: + type: string + +resources: + VDU1: + type: OS::Nova::Server + properties: + flavor: { get_param: flavor } + name: VDU1 + image: { get_param: image } + networks: + - port: + get_resource: CP1 + + CP1: + type: OS::Neutron::Port + properties: + network: { get_param: net1 } + +outputs: {} diff --git a/tacker/tests/etc/samples/etsi/nfv/test_enhanced_policy/Definitions/sample_vnfd_df_simple.yaml b/tacker/tests/etc/samples/etsi/nfv/test_enhanced_policy/Definitions/sample_vnfd_df_simple.yaml new file mode 100644 index 000000000..1d59e2ed8 --- /dev/null +++ b/tacker/tests/etc/samples/etsi/nfv/test_enhanced_policy/Definitions/sample_vnfd_df_simple.yaml @@ -0,0 +1,195 @@ +tosca_definitions_version: tosca_simple_yaml_1_2 + +description: Simple deployment flavour for Sample VNF + +imports: + - etsi_nfv_sol001_common_types.yaml + - etsi_nfv_sol001_vnfd_types.yaml + - sample_vnfd_types.yaml + +topology_template: + inputs: + id: + type: string + vendor: + type: string + version: + type: version + descriptor_id: + type: string + descriptor_version: + type: string + provider: + type: string + product_name: + type: string + software_version: + type: string + vnfm_info: + type: list + entry_schema: + type: string + flavour_id: + type: string + flavour_description: + type: string + + substitution_mappings: + node_type: company.provider.VNF + properties: + flavour_id: simple + requirements: + virtual_link_external: [] + + node_templates: + VNF: + type: company.provider.VNF + properties: + flavour_description: A simple flavour + interfaces: + Vnflcm: + # supporting only 'instantiate', 'terminate', 'modify', + # and 'heal' + # not supporting LCM script, supporting only default LCM + instantiate: [] + # instantiate_start: [] + # instantiate_end: [] + terminate: [] + # terminate_start: [] + # terminate_end: [] + modify_information: [] + # modify_information_start: [] + # modify_information_end: [] + # change_flavour: [] + # change_flavour_start: [] + # change_flavour_end: [] + # change_external_connectivity: [] + # change_external_connectivity_start: [] + # change_external_connectivity_end: [] + # operate: [] + # operate_start: [] + # operate_end: [] + heal: [] + # heal_start: [] + # heal_end: [] + scale: [] + # scale_start: [] + # scale_end: [] + # scale_to_level: [] + # scale_to_level_start: [] + # scale_to_level_end: [] + + VDU1: + type: tosca.nodes.nfv.Vdu.Compute + properties: + name: VDU1 + description: VDU1 compute node + vdu_profile: + min_number_of_instances: 1 + max_number_of_instances: 1 + sw_image_data: + name: cirros-0.5.2-x86_64-disk + version: "0.5.2" + checksum: + algorithm: sha-256 + hash: 932fcae93574e242dc3d772d5235061747dfe537668443a1f0567d893614b464 + container_format: bare + disk_format: qcow2 + min_disk: 1 GB + size: 1 GB + + capabilities: + virtual_compute: + properties: + requested_additional_capabilities: + properties: + requested_additional_capability_name: m1.tiny + support_mandatory: true + target_performance_parameters: + entry_schema: test + virtual_memory: + virtual_mem_size: 512 MB + virtual_cpu: + num_virtual_cpu: 1 + virtual_local_storage: + - size_of_storage: 3 GB + + CP1: + type: tosca.nodes.nfv.VduCp + properties: + layer_protocols: [ipv4] + requirements: + - virtual_binding: VDU1 + - virtual_link: internalVL1 + + internalVL1: + type: tosca.nodes.nfv.VnfVirtualLink + properties: + connectivity_type: + layer_protocols: [ipv4] + description: Internal Virtual link in the VNF + vl_profile: + max_bitrate_requirements: + root: 1048576 + leaf: 1048576 + min_bitrate_requirements: + root: 1048576 + leaf: 1048576 + virtual_link_protocol_data: + - associated_layer_protocol: ipv4 + l3_protocol_data: + ip_version: ipv4 + cidr: 10.0.0.0/24 + policies: + - scaling_aspects: + type: tosca.policies.nfv.ScalingAspects + properties: + aspects: + VDU1_scale: + name: VDU1_scale + description: VDU1 scaling aspect + max_scale_level: 2 + step_deltas: + - delta_1 + + - VDU1_initial_delta: + type: tosca.policies.nfv.VduInitialDelta + properties: + initial_delta: + number_of_instances: 1 + targets: [VDU1] + + - VDU1_scaling_aspect_deltas: + type: tosca.policies.nfv.VduScalingAspectDeltas + properties: + aspect: VDU1_scale + deltas: + delta_1: + number_of_instances: 1 + targets: [VDU1] + + - instantiation_levels: + type: tosca.policies.nfv.InstantiationLevels + properties: + levels: + instantiation_level_1: + description: Smallest size + scale_info: + VDU1_scale: + scale_level: 0 + instantiation_level_2: + description: Largest size + scale_info: + VDU1_scale: + scale_level: 2 + default_level: instantiation_level_1 + + - VDU1_instantiation_levels: + type: tosca.policies.nfv.VduInstantiationLevels + properties: + levels: + instantiation_level_1: + number_of_instances: 1 + instantiation_level_2: + number_of_instances: 3 + targets: [VDU1] diff --git a/tacker/tests/etc/samples/etsi/nfv/test_enhanced_policy/Definitions/sample_vnfd_top.yaml b/tacker/tests/etc/samples/etsi/nfv/test_enhanced_policy/Definitions/sample_vnfd_top.yaml new file mode 100644 index 000000000..f8906a9ed --- /dev/null +++ b/tacker/tests/etc/samples/etsi/nfv/test_enhanced_policy/Definitions/sample_vnfd_top.yaml @@ -0,0 +1,31 @@ +tosca_definitions_version: tosca_simple_yaml_1_2 + +description: Sample VNF + +imports: + - etsi_nfv_sol001_common_types.yaml + - etsi_nfv_sol001_vnfd_types.yaml + - sample_vnfd_types.yaml + - sample_vnfd_df_simple.yaml + +topology_template: + inputs: + selected_flavour: + type: string + description: VNF deployment flavour selected by the consumer. It is provided in the API + + node_templates: + VNF: + type: company.provider.VNF + properties: + flavour_id: { get_input: selected_flavour } + descriptor_id: c1bb0ce7-ebca-4fa7-95ed-4840d70a1175 + provider: Company + product_name: Sample VNF + software_version: "1.0" + descriptor_version: "1.0" + vnfm_info: + - Tacker + requirements: + #- virtual_link_external # mapped in lower-level templates + #- virtual_link_internal # mapped in lower-level templates diff --git a/tacker/tests/etc/samples/etsi/nfv/test_enhanced_policy/Definitions/sample_vnfd_types.yaml b/tacker/tests/etc/samples/etsi/nfv/test_enhanced_policy/Definitions/sample_vnfd_types.yaml new file mode 100644 index 000000000..905012e14 --- /dev/null +++ b/tacker/tests/etc/samples/etsi/nfv/test_enhanced_policy/Definitions/sample_vnfd_types.yaml @@ -0,0 +1,63 @@ +tosca_definitions_version: tosca_simple_yaml_1_2 + +description: VNF type definition + +imports: + - etsi_nfv_sol001_common_types.yaml + - etsi_nfv_sol001_vnfd_types.yaml + +node_types: + company.provider.VNF: + derived_from: tosca.nodes.nfv.VNF + properties: + id: + type: string + description: ID of this VNF + default: vnf_id + vendor: + type: string + description: name of the vendor who generate this VNF + default: vendor + version: + type: version + description: version of the software for this VNF + default: 1.0 + descriptor_id: + type: string + constraints: [valid_values: [c1bb0ce7-ebca-4fa7-95ed-4840d70a1175]] + default: c1bb0ce7-ebca-4fa7-95ed-4840d70a1175 + descriptor_version: + type: string + constraints: [valid_values: ["1.0"]] + default: "1.0" + provider: + type: string + constraints: [valid_values: ["Company"]] + default: "Company" + product_name: + type: string + constraints: [valid_values: ["Sample VNF"]] + default: "Sample VNF" + software_version: + type: string + constraints: [valid_values: ["1.0"]] + default: "1.0" + vnfm_info: + type: list + entry_schema: + type: string + constraints: [valid_values: [Tacker]] + default: [Tacker] + flavour_id: + type: string + constraints: [valid_values: [simple]] + default: simple + flavour_description: + type: string + default: This is the default flavour description + requirements: + - virtual_link_internal: + capability: tosca.capabilities.nfv.VirtualLinkable + interfaces: + Vnflcm: + type: tosca.interfaces.nfv.Vnflcm diff --git a/tacker/tests/etc/samples/etsi/nfv/test_enhanced_policy/TOSCA-Metadata/TOSCA.meta b/tacker/tests/etc/samples/etsi/nfv/test_enhanced_policy/TOSCA-Metadata/TOSCA.meta new file mode 100644 index 000000000..4c10d980d --- /dev/null +++ b/tacker/tests/etc/samples/etsi/nfv/test_enhanced_policy/TOSCA-Metadata/TOSCA.meta @@ -0,0 +1,4 @@ +TOSCA-Meta-File-Version: 1.0 +Created-by: Dummy User +CSAR-Version: 1.1 +Entry-Definitions: Definitions/sample_vnfd_top.yaml diff --git a/tacker/tests/etc/samples/etsi/nfv/test_enhanced_policy/UserData/__init__.py b/tacker/tests/etc/samples/etsi/nfv/test_enhanced_policy/UserData/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/tacker/tests/etc/samples/etsi/nfv/test_enhanced_policy/UserData/lcm_user_data.py b/tacker/tests/etc/samples/etsi/nfv/test_enhanced_policy/UserData/lcm_user_data.py new file mode 100644 index 000000000..725c5430a --- /dev/null +++ b/tacker/tests/etc/samples/etsi/nfv/test_enhanced_policy/UserData/lcm_user_data.py @@ -0,0 +1,35 @@ +# +# 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.vnfm.lcm_user_data.abstract_user_data import AbstractUserData +import tacker.vnfm.lcm_user_data.utils as UserDataUtil + + +class SampleUserData(AbstractUserData): + @staticmethod + def instantiate(base_hot_dict=None, + vnfd_dict=None, + inst_req_info=None, + grant_info=None): + api_param = UserDataUtil.get_diff_base_hot_param_from_api( + base_hot_dict, inst_req_info) + initial_param_dict = \ + UserDataUtil.create_initial_param_server_port_dict( + base_hot_dict) + vdu_flavor_dict = \ + UserDataUtil.create_vdu_flavor_capability_name_dict(vnfd_dict) + vdu_image_dict = UserDataUtil.create_sw_image_dict(vnfd_dict) + cpd_vl_dict = UserDataUtil.create_network_dict( + inst_req_info, initial_param_dict) + final_param_dict = UserDataUtil.create_final_param_dict( + initial_param_dict, vdu_flavor_dict, vdu_image_dict, cpd_vl_dict) + return {**final_param_dict, **api_param} diff --git a/tacker/tests/functional/base.py b/tacker/tests/functional/base.py index e16c2ad64..3b4bd560f 100644 --- a/tacker/tests/functional/base.py +++ b/tacker/tests/functional/base.py @@ -78,7 +78,8 @@ class SessionClient(adapter.Adapter): def do_request(self, url, method, **kwargs): kwargs.setdefault('authenticated', True) resp = self.request(url, method, **kwargs) - if resp.headers['Content-Type'] == 'application/zip': + if ('Content-Type' not in resp.headers or + resp.headers['Content-Type'] == 'application/zip'): return resp, resp.content body = self._decode_json(resp) return resp, body diff --git a/tacker/tests/functional/sol_enhanced_policy/__init__.py b/tacker/tests/functional/sol_enhanced_policy/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/tacker/tests/functional/sol_enhanced_policy/base.py b/tacker/tests/functional/sol_enhanced_policy/base.py new file mode 100644 index 000000000..235d68223 --- /dev/null +++ b/tacker/tests/functional/sol_enhanced_policy/base.py @@ -0,0 +1,1107 @@ +# +# 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 hashlib +from itertools import chain +import os +import re +import tempfile +import time +import yaml +import zipfile + +from keystoneauth1.identity import v3 +from keystoneauth1 import session +from keystoneclient.v3 import client as ks_client +from oslo_serialization import jsonutils +from oslo_utils import uuidutils +from tackerclient.v1_0 import client as tacker_client + +from tacker.common import utils +from tacker.objects import fields +from tacker.tests.functional import base +from tacker.tests.functional.base import BaseTackerTest +from tacker.tests.functional.sol.vnflcm import base as vnflcm_base +from tacker.tests.functional.sol.vnflcm import fake_vnflcm +from tacker.tests.utils import _update_unique_id_in_yaml +from tacker.tests.utils import read_file + + +class BaseEnhancedPolicyTest(object): + + TK_CLIENT_NAME = 'tk_client_%(username)s' + TK_HTTP_CLIENT_NAME = 'tk_http_client_%(username)s' + local_vim_conf_file = 'local-vim.yaml' + base_subscriptions_url = "/vnflcm/v1/subscriptions" + vim_base_url = "/v1.0/vims" + pkg_base_url = '/vnfpkgm/v1/vnf_packages' + user_role_map = {} + + @classmethod + def setUpClass(cls, subclass_with_user_role_map): + cls.user_role_map = subclass_with_user_role_map.user_role_map + cls.ks_client = cls.keystone_client() + cls.project = cls._get_project() + cls._create_user_role() + cls.create_tacker_http_client_for_user() + cls.cleanup_list = [] + + @classmethod + def create_tacker_http_client_for_user(cls): + for user in cls.users: + client = cls.get_tacker_client_for_user(user) + setattr(cls, + cls.TK_CLIENT_NAME % {'username': user.name}, client) + http_client = cls.get_tacker_http_client_for_user(user) + setattr(cls, + cls.TK_HTTP_CLIENT_NAME % {'username': user.name}, + http_client) + + @classmethod + def tearDownClass(cls): + cls._delete_user_role() + + @classmethod + def keystone_client(cls): + auth_session = base.BaseTackerTest.get_auth_session( + vim_conf_file=cls.local_vim_conf_file) + keystone = ks_client.Client(session=auth_session) + return keystone + + @classmethod + def _step_pkg_create(cls, username): + client = cls.get_tk_http_client_by_user(username) + resp, pkg = client.do_request( + cls.pkg_base_url, 'POST', + body=jsonutils.dumps({"userDefinedData": {"foo": "bar"}})) + if resp.status_code == 201: + return pkg + else: + raise Exception('Failed to create package.') + + @classmethod + def _wait_for_onboard(cls, client, package_uuid): + show_url = os.path.join(cls.pkg_base_url, package_uuid) + timeout = vnflcm_base.VNF_PACKAGE_UPLOAD_TIMEOUT + start_time = int(time.time()) + while True: + resp, body = client.do_request(show_url, "GET") + + if body['onboardingState'] == "ONBOARDED": + break + + if (int(time.time()) - start_time) > timeout: + raise Exception("Failed to onboard vnf package") + + time.sleep(1) + + @classmethod + def _step_pkg_upload_content(cls, username, pkg, csar_name, provider, + namespace=None): + client = cls.get_tk_http_client_by_user(username) + csar_dir = cls._get_csar_dir_path(csar_name) + + file_path, vnfd_id = cls.custom_csar(csar_dir, provider, + namespace=namespace) + cls.addClassCleanup(os.remove, file_path) + + with open(file_path, 'rb') as file_object: + resp, _ = client.do_request( + '{base_path}/{id}/package_content'.format( + id=pkg['id'], + base_path=cls.pkg_base_url), + "PUT", body=file_object, content_type='application/zip') + if resp.status_code == 202: + cls._wait_for_onboard(client, pkg['id']) + else: + raise Exception('Failed to upload content.') + + return vnfd_id + + @classmethod + def _step_pkg_disable(cls, username, pkg): + client = cls.get_tk_http_client_by_user(username) + update_req_body = jsonutils.dumps({ + "operationalState": "DISABLED"}) + resp, _ = client.do_request( + '{base_path}/{id}'.format(id=pkg['id'], + base_path=cls.pkg_base_url), + "PATCH", content_type='application/json', body=update_req_body) + if resp.status_code != 200: + raise Exception('Failed to disable package.') + + @classmethod + def _step_pkg_delete(cls, username, pkg): + client = cls.get_tk_http_client_by_user(username) + resp, _ = client.do_request( + os.path.join(cls.pkg_base_url, pkg.get('id')), + 'DELETE') + if resp.status_code != 204: + raise Exception('Failed to delete package.') + + @classmethod + def _create_project(cls): + project_name = 'policy-test' + projects = cls.ks_client.projects.list() + project_exists = False + for project in projects: + if project_name == project.name: + project_exists = True + cls.project_id = project.id + cls.project = project + break + if not project_exists: + project = cls.ks_client.projects.create(project_name, 'default') + cls.project_id = project.id + cls.project = project + + @classmethod + def _get_project(cls): + vim_params = base.BaseTackerTest.get_credentials( + cls.local_vim_conf_file) + project_name = vim_params['project_name'] + projects = cls.ks_client.projects.list() + for project in projects: + if project.name == project_name: + return project + else: + raise Exception('project not found.') + + @classmethod + def _delete_project(cls): + if hasattr(cls, 'project_id') and cls.project_id: + if hasattr(cls, 'ks_client') and cls.ks_client: + cls.ks_client.projects.delete(cls.project_id) + + @classmethod + def _get_user_by_name(cls, name): + for user in cls.users: + if user.name == name: + return user + else: + raise Exception(f'user {name} not found.') + + @classmethod + def _create_user_role(cls): + if cls.user_role_map: + user_role_map = cls.user_role_map + else: + raise Exception('user_role_map is needed.') + # create user + users_to_create = user_role_map.keys() + users = cls.ks_client.users.list() + usernames = [user.name for user in users] + users_to_create = set(users_to_create) - set(usernames) + project_id = cls.project.id + password = 'devstack' + cls.users = [] + for username in users_to_create: + user = cls.ks_client.users.create( + username, + project=project_id, + password=password + ) + cls.users.append(user) + username_exists = set(usernames) & user_role_map.keys() + for user in users: + if user.name in username_exists: + cls.users.append(user) + + # create roles + roles_to_create = set(chain(*user_role_map.values())) + cls._create_roles(roles_to_create) + + for username, roles in user_role_map.items(): + for role in roles: + cls.ks_client.roles.grant( + cls.role_map.get(role.lower()), + user=cls._get_user_by_name(username), + project=cls.project.id + ) + + @classmethod + def _create_role(cls, role_name): + role = cls.ks_client.roles.create(role_name) + return role + + @classmethod + def _create_roles(cls, role_names): + roles = cls.ks_client.roles.list() + role_map = {role.name.lower(): role for role in roles} + + for role_name in role_names: + if role_name.lower() not in role_map: + role = cls._create_role(role_name) + role_map[role_name.lower()] = role + + cls.role_map = role_map + cls.roles_to_delete = [ + role_map[role_name.lower()] for role_name in role_names] + + @classmethod + def _delete_user_role(cls): + for user in cls.users: + cls.ks_client.users.delete(user) + + @classmethod + def _get_auth_session_for_user(cls, user): + vim_params = base.BaseTackerTest.get_credentials( + cls.local_vim_conf_file) + auth = v3.Password( + auth_url=vim_params['auth_url'], + username=user.name, + password='devstack', + project_name=cls.project.name, + user_domain_name=vim_params['user_domain_name'], + project_domain_name=vim_params['project_domain_name']) + verify = utils.str_to_bool(vim_params.pop('cert_verify', 'False')) + auth_ses = session.Session(auth=auth, verify=verify) + return auth_ses + + @classmethod + def get_tacker_client_for_user(cls, user): + return tacker_client.Client( + session=cls._get_auth_session_for_user(user), retries=5) + + @classmethod + def get_tacker_http_client_for_user(cls, user): + return base.SessionClient( + session=cls._get_auth_session_for_user(user), + service_type='nfv-orchestration', + region_name='RegionOne') + + @classmethod + def get_tk_http_client_by_user(cls, username): + return getattr(cls, cls.TK_HTTP_CLIENT_NAME % {'username': username}) + + @classmethod + def register_vim(cls, client, url, vim_file, name, description, vim_type, + extra, is_default=False): + base_data = yaml.safe_load(read_file(vim_file)) + if vim_type == 'openstack': + auth_cred = { + 'username': base_data['username'], + 'password': base_data['password'], + 'user_domain_name': base_data['user_domain_name'] + } + elif vim_type == 'kubernetes': + auth_cred = {'bearer_token': base_data['bearer_token']} + else: + raise Exception(f'unknown vim type: {vim_type}.') + vim_project = { + 'name': base_data['project_name'], + } + if 'project_domain_name' in base_data: + vim_project['project_domain_name'] = ( + base_data['project_domain_name']) + vim_req = { + 'vim': { + 'name': name, + 'description': description, + 'type': vim_type, + 'auth_url': base_data['auth_url'], + 'auth_cred': auth_cred, + 'vim_project': vim_project, + 'is_default': is_default + } + } + if extra: + vim_req['vim'].update({'extra': extra}) + resp, body = client.do_request( + url, 'POST', body=jsonutils.dumps(vim_req) + ) + return resp, body + + @classmethod + def _update_provider_in_yaml(cls, data, provider): + try: + prop = data['topology_template']['node_templates']['VNF'][ + 'properties'] + if prop.get('provider', None): + prop['provider'] = provider + except KeyError: + # Let's check for 'node_types' + pass + + if not data.get('node_types', None): + return + + for ntype in data['node_types'].values(): + if ntype['derived_from'] != 'tosca.nodes.nfv.VNF': + continue + try: + desc_id = ntype['properties']['provider'] + if desc_id.get('constraints', None): + for constraint in desc_id.get('constraints'): + if constraint.get('valid_values', None): + constraint['valid_values'] = [provider] + if desc_id.get('default', None): + desc_id['default'] = provider + except KeyError: + # Let's check next node_type + pass + + @classmethod + def custom_csar(cls, csar_dir, provider, namespace=None): + current_dir = os.path.dirname(os.path.abspath(__file__)) + csar_dir = os.path.join(current_dir, "../../", csar_dir) + unique_id = uuidutils.generate_uuid() + tempfd, tempname = tempfile.mkstemp(suffix=".zip", + dir=os.path.dirname(csar_dir)) + os.close(tempfd) + common_dir = os.path.join(csar_dir, "../common/") + zcsar = zipfile.ZipFile(tempname, 'w') + + namespace_file = None + if namespace: + src_file = os.path.join( + csar_dir, "Files", "kubernetes", "namespace.yaml") + dst_file = os.path.join("Files", "kubernetes", "namespace.yaml") + namespace_file = dst_file + with open(src_file, 'r') as f: + data = yaml.safe_load(f) + data["metadata"]["name"] = namespace + ns_str = yaml.dump( + data, default_flow_style=False, + allow_unicode=True) + hash_value = hashlib.sha256(ns_str.encode()).hexdigest() + zcsar.writestr(dst_file, ns_str) + + tosca_file = None + artifact_files = [] + for (dpath, _, fnames) in os.walk(csar_dir): + if not fnames: + continue + for fname in fnames: + if fname == 'TOSCA.meta' or fname.endswith('.mf'): + src_file = os.path.join(dpath, fname) + tosca_file = src_file + with open(src_file, 'rb') as f: + artifacts_data = f.read() + artifacts_data_split = re.split(b'\n\n+', artifacts_data) + artifact_data_strs = [] + for data in artifacts_data_split: + + artifact_data_dict = yaml.safe_load(data) + if re.findall(b'.?Algorithm:.?|.?Hash:.?', data): + artifact_file = (artifact_data_dict['Source'] + if 'Source' in artifact_data_dict.keys() + else artifact_data_dict['Name']) + if (namespace_file and + artifact_file.endswith('namespace.yaml')): + artifact_data_dict['Hash'] = hash_value + artifact_files.append(artifact_file) + artifact_data_strs.append( + yaml.dump(artifact_data_dict, + default_flow_style=False, + allow_unicode=True)) + + if namespace_file: + dst_file = os.path.relpath( + os.path.join(dpath, fname), csar_dir) + zcsar.writestr(dst_file, '\n'.join(artifact_data_strs)) + + artifact_files = list(set(artifact_files)) + + for (dpath, _, fnames) in os.walk(csar_dir): + if not fnames: + continue + for fname in fnames: + src_file = os.path.join(dpath, fname) + dst_file = os.path.relpath( + os.path.join(dpath, fname), csar_dir) + + if (namespace_file and tosca_file and + src_file.endswith(tosca_file)): + continue + if fname.endswith('.yaml') or fname.endswith('.yml'): + if dst_file not in artifact_files: + with open(src_file, 'rb') as yfile: + data = yaml.safe_load(yfile) + _update_unique_id_in_yaml(data, unique_id) + cls._update_provider_in_yaml(data, provider) + zcsar.writestr(dst_file, yaml.dump( + data, default_flow_style=False, + allow_unicode=True)) + else: + if not dst_file.endswith('namespace.yaml'): + zcsar.write(src_file, dst_file) + else: + zcsar.write(src_file, dst_file) + + for (dpath, _, fnames) in os.walk(common_dir): + if not fnames: + continue + if ('test_cnf' in csar_dir and + re.search('images|kubernetes|Scripts', dpath)): + continue + for fname in fnames: + src_file = os.path.join(dpath, fname) + dst_file = os.path.relpath( + os.path.join(dpath, fname), common_dir) + zcsar.write(src_file, dst_file) + + zcsar.close() + return tempname, unique_id + + @classmethod + def _step_vim_register( + cls, username, vim_type, local_vim, vim_name, area, + is_default=False): + extra = {} + if area: + extra = {'area': area} + resp, body = cls.register_vim( + cls.get_tk_http_client_by_user(username), cls.vim_base_url, + local_vim, vim_name, + "{}-{}".format(vim_name, uuidutils.generate_uuid()), + vim_type, extra, is_default=is_default) + if resp.status_code == 201: + return body.get('vim') + else: + raise Exception('Failed to create vim.') + + @classmethod + def _step_vim_delete(cls, username, vim): + client = cls.get_tk_http_client_by_user(username) + resp, _ = client.do_request( + os.path.join(cls.vim_base_url, vim.get('id')), 'DELETE') + if resp.status_code != 204: + raise Exception('Failed to delete vim.') + + @classmethod + def _get_csar_dir_path(cls, csar_name): + csar_dir = os.path.abspath( + os.path.join(os.path.dirname(__file__), + "../../etc/samples/etsi/nfv", + csar_name)) + + return csar_dir + + def _register_subscription(self, request_body, http_client=None): + if http_client is None: + http_client = self.http_client + resp, response_body = http_client.do_request( + self.base_subscriptions_url, + "POST", + body=jsonutils.dumps(request_body)) + return resp, response_body + + def register_subscription(self): + client = self.get_tk_http_client_by_user('user_all') + + callback_url = os.path.join(vnflcm_base.MOCK_NOTIFY_CALLBACK_URL, + self._testMethodName) + request_body = fake_vnflcm.Subscription.make_create_request_body( + 'http://localhost:{}{}'.format( + vnflcm_base.FAKE_SERVER_MANAGER.SERVER_PORT, + callback_url)) + resp, response_body = self._register_subscription( + request_body, http_client=client) + self.assertEqual(201, resp.status_code) + self.assert_http_header_location_for_subscription(resp.headers) + self.assert_notification_get(callback_url) + subscription_id = response_body.get('id') + self.addCleanup( + self._delete_subscription, subscription_id, tacker_client=client) + + def create_port(self, neutron_client, network_id): + body = {'port': {'network_id': network_id}} + port = neutron_client.create_port(body=body)["port"] + self.addCleanup(neutron_client.delete_port, port['id']) + return port['id'] + + def create_network(self, neutron_client, network_name): + net = neutron_client.create_network( + {'network': {'name': "network-%s" % uuidutils.generate_uuid()}}) + net_id = net['network']['id'] + self.addCleanup(neutron_client.delete_network, net_id) + return net_id + + +class VimAPIsTest(BaseTackerTest, BaseEnhancedPolicyTest): + + base_url = "/v1.0/vims" + user_role_map = { + 'user_a': ['AREA_area_A@region_A', 'manager'], + 'user_b': ['AREA_area_B@region_B', 'manager'], + 'user_c': ['AREA_all@all', 'manager'], + 'user_all': ['AREA_all@all', 'manager'], + 'user_admin': ['admin'] + } + + @classmethod + def setUpClass(cls): + BaseTackerTest.setUpClass() + BaseEnhancedPolicyTest.setUpClass(cls) + + @classmethod + def tearDownClass(cls): + BaseEnhancedPolicyTest.tearDownClass() + BaseTackerTest.tearDownClass() + + def _step_vim_register(self, username, vim_type, local_vim, vim_name, area, + expected_status_code): + extra = {} + if area: + extra = {'area': area} + resp, body = self.register_vim( + self.get_tk_http_client_by_user(username), self.base_url, + local_vim, vim_name, + "{}-{}".format(vim_name, uuidutils.generate_uuid()), + vim_type, extra) + vim = body.get('vim') + self.assertEqual(expected_status_code, resp.status_code) + self.assertEqual(extra.get('area'), vim.get('extra', {}).get('area')) + return vim + + def _step_vim_delete(self, username, vim, expected_status_code): + client = self.get_tk_http_client_by_user(username) + resp, _ = client.do_request( + os.path.join(self.base_url, vim.get('id')), 'DELETE') + self.assertEqual(expected_status_code, resp.status_code) + + def _step_vim_show(self, username, vim, expected_status_code): + client = self.get_tk_http_client_by_user(username) + resp, body = client.do_request( + os.path.join(self.base_url, vim.get('id')), 'GET') + self.assertEqual(expected_status_code, resp.status_code) + if expected_status_code != 404: + self.assertEqual(vim.get('extra', {}).get('area'), + body.get('vim', {}).get('extra', {}).get('area')) + + def _step_vim_list(self, username, expected_vim_list): + client = self.get_tk_http_client_by_user(username) + resp, body = client.do_request(self.base_url, 'GET') + self.assertEqual(200, resp.status_code) + vim_ids = [vim.get('id') for vim in body.get('vims')] + for vim in expected_vim_list: + self.assertIn(vim['id'], vim_ids) + + def _step_vim_update(self, username, vim, expected_status_code): + client = self.get_tk_http_client_by_user(username) + req_body = {'vim': {'description': 'vim update'}} + resp, body = client.do_request( + os.path.join(self.base_url, vim.get('id')), 'PUT', + body=jsonutils.dumps(req_body) + ) + self.assertEqual(expected_status_code, resp.status_code) + if expected_status_code != 404: + self.assertEqual(vim.get('extra', {}).get('area'), + body.get('vim', {}).get('extra', {}).get('area')) + + def _test_vim_apis_enhanced_policy(self, vim_type, local_vim): + # step 1 VIM-Register, Resource Group A / User Group A + vim_a = self._step_vim_register( + 'user_a', vim_type, local_vim, 'vim_a', 'area_A@region_A', 201) + + # step 2 VIM-Register, Resource Group B / User Group all + vim_b = self._step_vim_register( + 'user_all', vim_type, local_vim, 'vim_b', 'area_B@region_B', 201) + + # step 3 VIM-Register, Resource Group C / User Group A + vim_c = self._step_vim_register( + 'user_a', vim_type, local_vim, 'vim_c', None, 201) + + # step 4 VIM-Show Resource Group A / User Group A + self._step_vim_show('user_a', vim_a, 200) + + # step 5 VIM-Show Resource Group B / User Group A + self._step_vim_show('user_a', vim_b, 404) + + # step 6 VIM-Show Resource Group A / User Group all + self._step_vim_show('user_all', vim_b, 200) + + # step 7 VIM-List Resource Group - / User Group A + self._step_vim_list('user_a', [vim_a]) + + # step 8 VIM-List Resource Group - / User Group B + self._step_vim_list('user_b', [vim_b]) + + # step 9 VIM-List Resource Group - / User Group all + self._step_vim_list('user_all', [vim_a, vim_b]) + + # step 10 VIM-Update Resource Group A - User Group A + self._step_vim_update('user_a', vim_a, 200) + + # step 11 VIM-Update Resource Group B - User Group A + self._step_vim_update('user_a', vim_b, 404) + + # step 12 VIM-Update Resource Group B - User Group all + self._step_vim_update('user_all', vim_b, 200) + + # step 13 VIM-Delete, Resource Group A / User Group A + self._step_vim_delete('user_a', vim_a, 204) + + # step 14 VIM-Delete, Resource Group B / User Group A + self._step_vim_delete('user_a', vim_b, 404) + + # step 15 VIM-Delete, Resource Group B / User Group all + self._step_vim_delete('user_all', vim_b, 204) + + # step 16 VIM-Delete, Resource Group C / User Group admin + self._step_vim_delete('user_admin', vim_c, 204) + + def _test_vim_apis_vim_without_area_attribute(self, vim_type, local_vim): + # step 1 VIM-Register, Resource Group C / User Group C + vim_c = self._step_vim_register( + 'user_c', vim_type, local_vim, 'vim_c', None, 201) + + # step 2 VIM-Show Resource Group C / User Group C + self._step_vim_show('user_c', vim_c, 404) + + # step 3 VIM-Show Resource Group C / User Group admin + self._step_vim_show('user_admin', vim_c, 200) + + # step 4 VIM-List Resource Group - / User Group C + self._step_vim_list('user_c', []) + + # step 5 VIM-List Resource Group - / User Group admin + self._step_vim_list('user_admin', [vim_c]) + + # step 6 VIM-Update Resource Group C - User Group C + self._step_vim_update('user_c', vim_c, 404) + + # step 7 VIM-Update Resource Group C - User Group admin + self._step_vim_update('user_admin', vim_c, 200) + + # step 8 VIM-Delete, Resource Group C / User Group C + self._step_vim_delete('user_c', vim_c, 404) + + # step 9 VIM-Delete, Resource Group C / User Group admin + self._step_vim_delete('user_admin', vim_c, 204) + + +class VnflcmAPIsV1Base(vnflcm_base.BaseVnfLcmTest, BaseEnhancedPolicyTest): + + user_role_map = { + 'user_a': ['VENDOR_company_A', 'AREA_area_A@region_A', + 'TENANT_namespace-a', 'manager'], + 'user_a_1': ['VENDOR_company_A', 'manager'], + 'user_b': ['VENDOR_company_B', 'AREA_area_B@region_B', + 'TENANT_namespace-b', 'manager'], + 'user_c': ['VENDOR_company_C', 'AREA_area_C@region_C', + 'TENANT_namespace-c', 'manager'], + 'user_all': ['VENDOR_all', 'AREA_all@all', 'TENANT_all', 'manager'], + 'user_admin': ['admin'] + } + + @classmethod + def setUpClass(cls): + vnflcm_base.BaseVnfLcmTest.setUpClass() + BaseEnhancedPolicyTest.setUpClass(cls) + + def setUp(self): + super().setUp() + + def _step_lcm_create(self, username, vnfd_id, vnf_instance_name, + expected_status_code): + client = self.get_tk_http_client_by_user(username) + + request_body = { + 'vnfdId': vnfd_id, + 'vnfInstanceDescription': 'Sample VNF for LCM Testing', + 'vnfInstanceName': + "{}-{}".format(self._testMethodName, vnf_instance_name) + } + resp, response_body = client.do_request( + self.base_vnf_instances_url, + "POST", + body=jsonutils.dumps(request_body)) + self.assertEqual(expected_status_code, resp.status_code) + return response_body.get('id') + + def _step_lcm_show(self, username, inst_id, expected_status_code): + client = self.get_tk_http_client_by_user(username) + show_url = os.path.join(self.base_vnf_instances_url, inst_id) + resp, vnf_instance = client.do_request(show_url, "GET") + self.assertEqual(expected_status_code, resp.status_code) + + def _step_lcm_list(self, username, expected_inst_list): + client = self.get_tk_http_client_by_user(username) + + resp, vnf_instances = client.do_request( + self.base_vnf_instances_url, "GET") + + self.assertEqual(200, resp.status_code) + inst_ids = set([inst.get('id') for inst in vnf_instances]) + for inst_id in expected_inst_list: + self.assertIn(inst_id, inst_ids) + + def _step_lcm_terminate(self, username, inst_id, expected_status_code): + client = self.get_tk_http_client_by_user(username) + + terminate_req_body = { + "terminationType": fields.VnfInstanceTerminationType.GRACEFUL, + 'gracefulTerminationTimeout': 60 + } + url = os.path.join(self.base_vnf_instances_url, inst_id, "terminate") + resp, body = client.do_request(url, "POST", + body=jsonutils.dumps(terminate_req_body)) + self.assertEqual(expected_status_code, resp.status_code) + if expected_status_code == 202: + lcmocc_id = os.path.basename(resp.headers['Location']) + self.wait_lcmocc_complete(username, lcmocc_id) + + def _step_lcm_delete(self, username, inst_id, expected_status_code): + client = self.get_tk_http_client_by_user(username) + url = os.path.join(self.base_vnf_instances_url, inst_id) + resp, _ = client.do_request(url, "DELETE") + self.assertEqual(expected_status_code, resp.status_code) + + def _step_lcm_heal(self, username, inst_id, expected_status_code): + client = self.get_tk_http_client_by_user(username) + heal_req = {} + url = os.path.join(self.base_vnf_instances_url, inst_id, "heal") + resp, _ = client.do_request( + url, "POST", body=jsonutils.dumps(heal_req)) + self.assertEqual(expected_status_code, resp.status_code) + if expected_status_code == 202: + lcmocc_id = os.path.basename(resp.headers['Location']) + self.wait_lcmocc_complete(username, lcmocc_id) + + def _step_lcm_scale(self, username, inst_id, request_body, + expected_status_code): + client = self.get_tk_http_client_by_user(username) + url = os.path.join(self.base_vnf_instances_url, inst_id, "scale") + resp, _ = client.do_request( + url, "POST", body=jsonutils.dumps(request_body)) + self.assertEqual(expected_status_code, resp.status_code) + if expected_status_code == 202: + lcmocc_id = os.path.basename(resp.headers['Location']) + self.wait_lcmocc_complete(username, lcmocc_id) + + def _lcm_instantiate( + self, username, inst_id, request_body, expected_status_code): + client = self.get_tk_http_client_by_user(username) + url = os.path.join(self.base_vnf_instances_url, inst_id, "instantiate") + resp, _ = client.do_request( + url, "POST", body=jsonutils.dumps(request_body)) + self.assertEqual(expected_status_code, resp.status_code) + if expected_status_code == 202: + lcmocc_id = os.path.basename(resp.headers['Location']) + self.wait_lcmocc_complete(username, lcmocc_id) + + def wait_lcmocc_complete(self, username, lcmocc_id): + # NOTE: It is not necessary to set timeout because the operation + # itself set timeout and the state will become 'FAILED_TEMP'. + client = self.get_tk_http_client_by_user(username) + path = "/vnflcm/v1/vnf_lcm_op_occs/{}".format(lcmocc_id) + while True: + time.sleep(vnflcm_base.RETRY_WAIT_TIME) + resp, body = client.do_request( + path, "GET") + self.assertEqual(200, resp.status_code) + + state = body['operationState'] + if state == 'COMPLETED': + return + elif state in ['STARTING', 'PROCESSING']: + continue + else: # FAILED_TEMP or ROLLED_BACK + raise Exception("Operation failed. state: %s" % state) + + def _wait_lcm_done(self, + operation=None, + vnf_lcm_op_occs=None, + expected_operation_status=None, + vnf_instance_id=None, + fake_server_manager=None): + if fake_server_manager is None: + fake_server_manager = vnflcm_base.FAKE_SERVER_MANAGER + start_time = int(time.time()) + callback_url = os.path.join( + vnflcm_base.MOCK_NOTIFY_CALLBACK_URL, + self._testMethodName) + + while True: + actual_status = None + vnf_lcm_op_occ_id = None + notify_mock_responses = fake_server_manager.get_history( + callback_url) + print( + ("Wait:callback_url=<%s>, " + "wait_operation=<%s>, " + "wait_status=<%s>, " + "vnf_instance_id=<%s>") % + (callback_url, operation, expected_operation_status, + vnf_instance_id), + flush=True) + + for res in notify_mock_responses: + if (vnf_instance_id != res.request_body.get('vnfInstanceId') + and operation != res.request_body.get('operation')): + continue + + if expected_operation_status is None: + return + actual_status = res.request_body.get('operationState', '') + vnf_lcm_op_occ_id = res.request_body.get('vnfLcmOpOccId', '') + if actual_status == expected_operation_status: + if operation: + if operation != res.request_body.get('operation'): + continue + if vnf_lcm_op_occs: + if (vnf_lcm_op_occs != + res.request_body.get('vnfLcmOpOccId')): + continue + return + elif actual_status == 'FAILED_TEMP': + error = ( + "LCM incomplete timeout, %(vnf_lcm_op_occ_id)s" + " is %(actual)s," + " expected status should be %(expected)s") + self.fail( + error % { + "vnf_lcm_op_occ_id": vnf_lcm_op_occ_id, + "expected": expected_operation_status, + "actual": actual_status}) + + if ((int(time.time()) - start_time) > + vnflcm_base.VNF_LCM_DONE_TIMEOUT): + if actual_status: + error = ( + "LCM incomplete timeout, %(vnf_lcm_op_occ_id)s" + " is %(actual)s," + " expected status should be %(expected)s") + self.fail( + error % { + "vnf_lcm_op_occ_id": vnf_lcm_op_occ_id, + "expected": expected_operation_status, + "actual": actual_status}) + else: + self.fail("LCM incomplete timeout") + + time.sleep(vnflcm_base.RETRY_WAIT_TIME) + + def _step_lcm_modify(self, username, inst_id, expected_status_code): + client = self.get_tk_http_client_by_user(username) + request_body = { + "vnfInstanceName": "modify_{}".format(inst_id) + } + url = os.path.join(self.base_vnf_instances_url, inst_id) + resp, body = client.do_request(url, "PATCH", + body=jsonutils.dumps(request_body)) + self.assertEqual(expected_status_code, resp.status_code) + if expected_status_code == 202: + lcmocc_id = os.path.basename(resp.headers['Location']) + self.wait_lcmocc_complete(username, lcmocc_id) + + def steps_lcm_create_and_get_with_area(self): + # step 1 LCM-Create, Resource Group A / User Group A + inst_id_a = self._step_lcm_create( + 'user_a', self.vnfd_id_a, 'vnf_instance_a', 201) + + # step 2 LCM-Create, Resource Group B / User Group A + self._step_lcm_create( + 'user_a', self.vnfd_id_b, 'vnf_instance_b', 403) + + # step 3 LCM-Create, Resource Group B / User Group all + inst_id_b = self._step_lcm_create( + 'user_all', self.vnfd_id_b, 'vnf_instance_b', 201) + + # step 4 LCM-Show, Resource Group A / User Group A + self._step_lcm_show('user_a', inst_id_a, 403) + + # step 5 LCM-Show, Resource Group A / User Group A-1 + self._step_lcm_show('user_a_1', inst_id_a, 403) + + # step 6 LCM-Show, Resource Group B / User Group A + self._step_lcm_show('user_a', inst_id_b, 403) + + # step 7 LCM-Show, Resource Group B / User Group all + self._step_lcm_show('user_all', inst_id_b, 403) + + # step 8 LCM-List, Resource Group - / User Group A + self._step_lcm_list('user_a', []) + + # step 9 LCM-List, Resource Group - / User Group A-1 + self._step_lcm_list('user_a_1', []) + + # step 10 LCM-List, Resource Group - / User Group B + self._step_lcm_list('user_b', []) + + # step 11 LCM-List, Resource Group - / User Group all + self._step_lcm_list('user_all', []) + + return inst_id_a, inst_id_b + + def steps_lcm_get_scale_heal_modify_with_area(self, inst_id_a, inst_id_b): + # step 15 LCM-Show, Resource Group A / User Group A + self._step_lcm_show('user_a', inst_id_a, 200) + + # step 16 LCM-Show, Resource Group B / User Group A + self._step_lcm_show('user_a', inst_id_b, 403) + + # step 17 LCM-Show, Resource Group B / User Group all + self._step_lcm_show('user_all', inst_id_b, 200) + + # step 18 LCM-List, Resource Group - / User Group A + self._step_lcm_list('user_a', [inst_id_a]) + + # step 19 LCM-List, Resource Group - / User Group A-1 + self._step_lcm_list('user_a_1', []) + + # step 20 LCM-List, Resource Group - / User Group B + self._step_lcm_list('user_b', [inst_id_b]) + + # step 21 LCM-List, Resource Group - / User Group all + self._step_lcm_list('user_all', [inst_id_a, inst_id_b]) + + # step 22 LCM-Scale(out), Resource Group A / User Group A + self._step_lcm_scale_out('user_a', inst_id_a, 202) + + # step 23 LCM-Scale(out), Resource Group B / User Group A + self._step_lcm_scale_out('user_a', inst_id_b, 403) + + # step 24 LCM-Scale(out), Resource Group B / User Group all + self._step_lcm_scale_out('user_all', inst_id_b, 202) + + # step 25 LCM-Scale(in), Resource Group A / User Group A + self._step_lcm_scale_in('user_a', inst_id_a, 202) + + # step 26 LCM-Scale(in), Resource Group B / User Group A + self._step_lcm_scale_in('user_a', inst_id_b, 403) + + # step 27 LCM-Scale(in), Resource Group B / User Group all + self._step_lcm_scale_in('user_all', inst_id_b, 202) + + # step 28 LCM-Heal, Resource Group A / User Group A + self._step_lcm_heal('user_a', inst_id_a, 202) + + # step 29 LCM-Heal, Resource Group B / User Group A + self._step_lcm_heal('user_a', inst_id_b, 403) + + # step 30 LCM-Heal, Resource Group B / User Group all + self._step_lcm_heal('user_all', inst_id_b, 202) + + # step 31 LCM-Modify, Resource Group A / User Group A + self._step_lcm_modify('user_a', inst_id_a, 202) + + # step 32 LCM-Modify, Resource Group b / User Group A + self._step_lcm_modify('user_a', inst_id_b, 403) + + # step 33 LCM-Modify, Resource Group B / User Group all + self._step_lcm_modify('user_all', inst_id_b, 202) + + def steps_lcm_terminate_delete_with_area(self, inst_id_a, inst_id_b): + # step 37 LCM-Terminate, Resource Group A / User Group A + self._step_lcm_terminate('user_a', inst_id_a, 202) + + # step 38 LCM-Terminate, Resource Group B / User Group A + self._step_lcm_terminate('user_a', inst_id_b, 403) + + # step 39 LCM-Terminate, Resource Group B / User Group A + self._step_lcm_terminate('user_all', inst_id_b, 202) + + # step 40 LCM-Delete, Resource Group A / User Group A + self._step_lcm_delete('user_a', inst_id_a, 204) + + # step 41 LCM-Delete, Resource Group B / User Group A + self._step_lcm_delete('user_a', inst_id_b, 403) + + # step 42 LCM-Delete, Resource Group B / User Group A + self._step_lcm_delete('user_all', inst_id_b, 204) + + def steps_lcm_create_and_get_without_area(self): + # step 1 LCM-Create, Resource Group C / User Group C + inst_id_c = self._step_lcm_create( + 'user_c', self.vnfd_id_c, 'vnf_instance_c', 201) + + # step 2 LCM-Show, Resource Group C / User Group C + self._step_lcm_show('user_c', inst_id_c, 403) + + # step 3 LCM-Show, Resource Group C / User Group all + self._step_lcm_show('user_all', inst_id_c, 403) + + # step 4 LCM-Show, Resource Group C / User Group admin + self._step_lcm_show('user_admin', inst_id_c, 200) + + # step 5 LCM-List, Resource Group - / User Group C + self._step_lcm_list('user_c', []) + + # step 6 LCM-List, Resource Group - / User Group all + self._step_lcm_list('user_all', []) + + # step 7 LCM-List, Resource Group - / User Group admin + self._step_lcm_list('user_admin', [inst_id_c]) + + return inst_id_c + + def steps_lcm_get_scale_heal_modify_without_area(self, inst_id_c): + # step 9 LCM-Show, Resource Group C / User Group C + self._step_lcm_show('user_c', inst_id_c, 403) + + # step 10 LCM-Show, Resource Group C / User Group all + self._step_lcm_show('user_all', inst_id_c, 403) + + # step 11 LCM-Show, Resource Group C / User Group admin + self._step_lcm_show('user_admin', inst_id_c, 200) + + # step 12 LCM-List, Resource Group - / User Group C + self._step_lcm_list('user_c', []) + + # step 13 LCM-List, Resource Group - / User Group all + self._step_lcm_list('user_all', []) + + # step 14 LCM-List, Resource Group - / User Group admin + self._step_lcm_list('user_admin', [inst_id_c]) + + # step 15 LCM-Scale(out), Resource Group C / User Group C + self._step_lcm_scale_out('user_c', inst_id_c, 403) + + # step 16 LCM-Scale(out), Resource Group C / User Group all + self._step_lcm_scale_out('user_all', inst_id_c, 403) + + # step 17 LCM-Scale(out), Resource Group C / User Group admin + self._step_lcm_scale_out('user_admin', inst_id_c, 202) + + # step 18 LCM-Scale(in), Resource Group C / User Group C + self._step_lcm_scale_in('user_c', inst_id_c, 403) + + # step 19 LCM-Scale(in), Resource Group C / User Group all + self._step_lcm_scale_in('user_all', inst_id_c, 403) + + # step 20 LCM-Scale(in), Resource Group C / User Group admin + self._step_lcm_scale_in('user_admin', inst_id_c, 202) + + # step 21 LCM-Heal, Resource Group C / User Group C + self._step_lcm_heal('user_c', inst_id_c, 403) + + # step 22 LCM-Heal, Resource Group C / User Group all + self._step_lcm_heal('user_all', inst_id_c, 403) + + # step 23 LCM-Heal, Resource Group C / User Group admin + self._step_lcm_heal('user_admin', inst_id_c, 202) + + # step 24 LCM-Modify, Resource Group C / User Group C + self._step_lcm_modify('user_c', inst_id_c, 403) + + # step 25 LCM-Modify, Resource Group C / User Group all + self._step_lcm_modify('user_all', inst_id_c, 403) + + # step 26 LCM-Modify, Resource Group C / User Group admin + self._step_lcm_modify('user_admin', inst_id_c, 202) + + def steps_lcm_terminate_delete_without_area(self, inst_id_c): + # step 30 LCM-Terminate, Resource Group C / User Group C + self._step_lcm_terminate('user_c', inst_id_c, 403) + + # step 31 LCM-Terminate, Resource Group C / User Group all + self._step_lcm_terminate('user_all', inst_id_c, 403) + + # step 32 LCM-Terminate, Resource Group C / User Group admin + self._step_lcm_terminate('user_admin', inst_id_c, 202) + + # step 33 LCM-Delete, Resource Group C / User Group C + self._step_lcm_delete('user_c', inst_id_c, 204) diff --git a/tacker/tests/functional/sol_enhanced_policy/sol/__init__.py b/tacker/tests/functional/sol_enhanced_policy/sol/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/tacker/tests/functional/sol_enhanced_policy/sol/test_policy_vim_apis_openstack.py b/tacker/tests/functional/sol_enhanced_policy/sol/test_policy_vim_apis_openstack.py new file mode 100644 index 000000000..68768d13d --- /dev/null +++ b/tacker/tests/functional/sol_enhanced_policy/sol/test_policy_vim_apis_openstack.py @@ -0,0 +1,25 @@ +# +# 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.tests.functional.sol_enhanced_policy.base import ( + VimAPIsTest) + + +class VimAPIsOpenstackTest(VimAPIsTest): + + def test_vim_apis_vim_with_area_openstack(self): + self._test_vim_apis_enhanced_policy('openstack', 'local-vim.yaml') + + def test_vim_apis_vim_without_area_openstack(self): + self._test_vim_apis_vim_without_area_attribute( + 'openstack', 'local-vim.yaml') diff --git a/tacker/tests/functional/sol_enhanced_policy/sol/test_policy_vnf_package_apis.py b/tacker/tests/functional/sol_enhanced_policy/sol/test_policy_vnf_package_apis.py new file mode 100644 index 000000000..12143683c --- /dev/null +++ b/tacker/tests/functional/sol_enhanced_policy/sol/test_policy_vnf_package_apis.py @@ -0,0 +1,244 @@ +# +# 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 os.path +import time + +from oslo_serialization import jsonutils + +from tacker.tests.functional.base import BaseTackerTest +from tacker.tests.functional.sol_enhanced_policy.base import ( + BaseEnhancedPolicyTest) + + +class BaseVnfPackageAPIsTest(BaseTackerTest, BaseEnhancedPolicyTest): + + VNF_PACKAGE_UPLOAD_TIMEOUT = 300 + base_url = '/vnfpkgm/v1/vnf_packages' + user_role_map = { + 'user_a': ['VENDOR_company_A', 'manager'], + 'user_b': ['VENDOR_company_B', 'manager'], + 'user_all': ['VENDOR_all', 'manager'], + } + + @classmethod + def setUpClass(cls): + BaseTackerTest.setUpClass() + BaseEnhancedPolicyTest.setUpClass(cls) + + @classmethod + def tearDownClass(cls): + BaseEnhancedPolicyTest.tearDownClass() + BaseTackerTest.tearDownClass() + + def _step_pkg_create(self, username): + client = self.get_tk_http_client_by_user(username) + + resp, pkg = client.do_request( + self.base_url, 'POST', + body=jsonutils.dumps({"userDefinedData": {"foo": "bar"}})) + + self.assertEqual(201, resp.status_code) + return pkg + + def _step_pkg_show(self, username, pkg, expected_status_code, + expected_vendor=None): + client = self.get_tk_http_client_by_user(username) + resp, pkg = client.do_request( + os.path.join(self.base_url, pkg.get('id')), + 'GET') + self.assertEqual(expected_status_code, resp.status_code) + if expected_status_code == 200 and expected_vendor: + self.assertEqual(expected_vendor, pkg.get('vnfProvider')) + + def _step_pkg_list(self, username, expected_pkg_list): + client = self.get_tk_http_client_by_user(username) + resp, pkgs = client.do_request(self.base_url, 'GET') + self.assertEqual(200, resp.status_code) + pkg_ids = set([pkg.get('id') for pkg in pkgs]) + for pkg in expected_pkg_list: + self.assertIn(pkg.get('id'), pkg_ids) + + def _get_csar_dir_path(self, csar_name): + csar_dir = os.path.abspath( + os.path.join(os.path.dirname(__file__), + "../../../etc/samples/etsi/nfv", csar_name)) + + return csar_dir + + def _wait_for_onboard(self, client, package_uuid): + show_url = os.path.join(self.base_url, package_uuid) + timeout = self.VNF_PACKAGE_UPLOAD_TIMEOUT + start_time = int(time.time()) + while True: + resp, body = client.do_request(show_url, "GET") + if body['onboardingState'] == "ONBOARDED": + break + + if (int(time.time()) - start_time) > timeout: + raise Exception("Failed to onboard vnf package") + + time.sleep(1) + + def _step_pkg_upload_content(self, username, pkg, csar_name, provider, + expected_status_code): + client = self.get_tk_http_client_by_user(username) + csar_dir = self._get_csar_dir_path(csar_name) + + file_path, vnfd_id = self.custom_csar(csar_dir, provider) + self.addCleanup(os.remove, file_path) + with open(file_path, 'rb') as file_object: + resp, resp_body = client.do_request( + '{base_path}/{id}/package_content'.format( + id=pkg['id'], + base_path=self.base_url), + "PUT", body=file_object, content_type='application/zip') + self.assertEqual(expected_status_code, resp.status_code) + if expected_status_code == 202: + self._wait_for_onboard(client, pkg['id']) + + def _step_pkg_read_vnfd(self, username, pkg, expected_status_code): + client = self.get_tk_http_client_by_user(username) + resp, resp_body = client.do_request( + '{base_path}/{id}/vnfd'.format(id=pkg['id'], + base_path=self.base_url), + "GET", content_type='application/zip') + self.assertEqual(expected_status_code, resp.status_code) + if expected_status_code == 200: + self.assertEqual('application/zip', resp.headers['Content-Type']) + self.assertIsNotNone(resp.text) + + def _step_pkg_fetch(self, username, pkg, expected_status_code): + client = self.get_tk_http_client_by_user(username) + response = client.do_request( + '{base_path}/{id}/package_content'.format( + id=pkg['id'], base_path=self.base_url), + "GET", body={}, headers={}) + self.assertEqual(expected_status_code, response[0].status_code) + + def _step_pkg_update(self, username, pkg, expected_status_code): + client = self.get_tk_http_client_by_user(username) + update_req_body = jsonutils.dumps({ + "operationalState": "DISABLED"}) + + resp, _ = client.do_request( + '{base_path}/{id}'.format(id=pkg['id'], + base_path=self.base_url), + "PATCH", content_type='application/json', body=update_req_body) + self.assertEqual(expected_status_code, resp.status_code) + + def _step_pkg_delete(self, username, pkg, expected_status_code): + client = self.get_tk_http_client_by_user(username) + resp, _ = client.do_request(os.path.join(self.base_url, pkg.get('id')), + 'DELETE') + self.assertEqual(expected_status_code, resp.status_code) + + def _test_vnf_package_apis_enhanced_policy(self, csar_name): + # step 1 PKG-Create, Resource Group A / User Group A + pkg_a = self._step_pkg_create('user_a') + + # step 2 PKG-Create, Resource Group B / User Group all + pkg_b = self._step_pkg_create('user_all') + + # step 3 PKG-Show, Resource Group A / User Group A + self._step_pkg_show('user_a', pkg_a, 200) + + # step 4 PKG-Show, Resource Group B / User Group A + self._step_pkg_show('user_a', pkg_b, 200) + + # step 5 PKG-Show, Resource Group A / User Group A + self._step_pkg_show('user_all', pkg_b, 200) + + # step 6 PKG-List, Resource Group - / User Group A + self._step_pkg_list('user_a', [pkg_a, pkg_b]) + + # step 7 PKG-List, Resource Group - / User Group B + self._step_pkg_list('user_b', [pkg_a, pkg_b]) + + # step 8 PKG-List, Resource Group - / User Group all + self._step_pkg_list('user_all', [pkg_a, pkg_b]) + + # step 9 PKG-Upload-content, Resource Group B / User Group A + self._step_pkg_upload_content( + 'user_a', pkg_a, csar_name, 'company_B', 403) + + # step 10 PKG-Upload-content, Resource Group A / User Group A + self._step_pkg_upload_content( + 'user_a', pkg_a, csar_name, 'company_A', 202) + + # step 11 PKG-Upload-content, Resource Group B / User Group all + self._step_pkg_upload_content( + 'user_all', pkg_b, csar_name, 'company_B', 202) + + # step 12 PKG-Show, Resource Group A / User Group A + self._step_pkg_show('user_a', pkg_a, 200) + + # step 13 PKG-Show, Resource Group B / User Group A + self._step_pkg_show('user_a', pkg_b, 403) + + # step 14 PKG-Show, Resource Group A / User Group A + self._step_pkg_show('user_all', pkg_b, 200) + + # step 15 PKG-List, Resource Group - / User Group A + self._step_pkg_list('user_a', [pkg_a]) + + # step 16 PKG-List, Resource Group - / User Group B + self._step_pkg_list('user_b', [pkg_b]) + + # step 17 PKG-List, Resource Group - / User Group all + self._step_pkg_list('user_all', [pkg_a, pkg_b]) + + # step 18 PKG-Read-vnfd, Resource Group A / User Group A + self._step_pkg_read_vnfd('user_a', pkg_a, 200) + + # step 19 PKG-Read-vnfd, Resource Group B / User Group A + self._step_pkg_read_vnfd('user_a', pkg_b, 403) + + # step 20 PKG-Read-vnfd, Resource Group B / User Group all + self._step_pkg_read_vnfd('user_all', pkg_b, 200) + + # step 21 PKG-Read-vnfd, Resource Group A / User Group A + self._step_pkg_fetch('user_a', pkg_a, 200) + + # step 22 PKG-Read-vnfd, Resource Group B / User Group A + self._step_pkg_fetch('user_a', pkg_b, 403) + + # step 23 PKG-Read-vnfd, Resource Group B / User Group all + self._step_pkg_fetch('user_all', pkg_b, 200) + + # step 24 PKG-Update, Resource Group B / User Group A + self._step_pkg_update('user_a', pkg_b, 403) + + # step 25 PKG-Update, Resource Group A / User Group A + self._step_pkg_update('user_a', pkg_a, 200) + + # step 26 PKG-Update, Resource Group B / User Group all + self._step_pkg_update('user_all', pkg_b, 200) + + # step 27 PKG-Delete, Resource Group A / User Group A + self._step_pkg_delete('user_a', pkg_a, 204) + + # step 29 PKG-Delete, Resource Group B / User Group all + self._step_pkg_delete('user_all', pkg_b, 204) + + +class VnfPackageAPIsTest(BaseVnfPackageAPIsTest): + + def test_vnf_package_apis_enhanced_policy_vnf(self): + self._test_vnf_package_apis_enhanced_policy('test_enhanced_policy') + + +class CnfPackageAPIsTest(BaseVnfPackageAPIsTest): + + def test_vnf_package_apis_enhanced_policy_cnf(self): + self._test_vnf_package_apis_enhanced_policy('test_cnf_scale') diff --git a/tacker/tests/functional/sol_enhanced_policy/sol/test_policy_vnflcm_apis_v1.py b/tacker/tests/functional/sol_enhanced_policy/sol/test_policy_vnflcm_apis_v1.py new file mode 100644 index 000000000..1a92ad32a --- /dev/null +++ b/tacker/tests/functional/sol_enhanced_policy/sol/test_policy_vnflcm_apis_v1.py @@ -0,0 +1,253 @@ +# +# 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 os + +from oslo_serialization import jsonutils +from oslo_utils import uuidutils + +from tacker.tests.functional.sol.vnflcm import fake_vnflcm +from tacker.tests.functional.sol.vnflcm.test_vnf_instance import ( + get_external_virtual_links) +from tacker.tests.functional.sol_enhanced_policy.base import ( + VnflcmAPIsV1Base) + + +class VnflcmAPIsV1Test(VnflcmAPIsV1Base): + + @classmethod + def setUpClass(cls): + super().setUpClass() + + vim_type = 'openstack' + + local_vim = 'local-vim.yaml' + + cls.vim_a = cls._step_vim_register( + 'user_a', vim_type, local_vim, 'vim_a', 'area_A@region_A') + + cls.vim_a_1 = cls._step_vim_register( + 'user_a', vim_type, local_vim, 'vim_a_1', 'area_A@region_A') + + cls.vim_b = cls._step_vim_register( + 'user_b', vim_type, local_vim, 'vim_b', 'area_B@region_B') + + cls.vim_b_1 = cls._step_vim_register( + 'user_b', vim_type, local_vim, 'vim_b_1', 'area_B@region_B') + + cls.vim_c = cls._step_vim_register( + 'user_b', vim_type, local_vim, 'vim_c', None) + + cls.vim_c_1 = cls._step_vim_register( + 'user_b', vim_type, local_vim, 'vim_c_1', None) + + cls.pkg_a = cls._step_pkg_create('user_a') + + cls.vnfd_id_a = cls._step_pkg_upload_content( + 'user_a', cls.pkg_a, 'test_enhanced_policy', 'company_A') + + cls.pkg_b = cls._step_pkg_create('user_b') + + cls.vnfd_id_b = cls._step_pkg_upload_content( + 'user_b', cls.pkg_b, 'test_enhanced_policy', 'company_B') + + cls.pkg_c = cls._step_pkg_create('user_c') + + cls.vnfd_id_c = cls._step_pkg_upload_content( + 'user_c', cls.pkg_c, 'test_enhanced_policy', 'company_C') + + @classmethod + def tearDownClass(cls): + + cls._step_pkg_disable('user_a', cls.pkg_a,) + cls._step_pkg_disable('user_b', cls.pkg_b,) + cls._step_pkg_disable('user_c', cls.pkg_c,) + cls._step_pkg_delete('user_a', cls.pkg_a) + cls._step_pkg_delete('user_b', cls.pkg_b) + cls._step_pkg_delete('user_c', cls.pkg_c) + + cls._step_vim_delete('user_a', cls.vim_a) + cls._step_vim_delete('user_a', cls.vim_a_1) + cls._step_vim_delete('user_b', cls.vim_b) + cls._step_vim_delete('user_b', cls.vim_b_1) + cls._step_vim_delete('user_admin', cls.vim_c) + cls._step_vim_delete('user_admin', cls.vim_c_1) + + super().tearDownClass() + + def _instantiate_vnf_request(self, flavour_id, + instantiation_level_id=None, vim_id=None, ext_vl=None, + ext_managed_vl=None): + request_body = { + "flavourId": flavour_id, + "additionalParams": { + "lcm-operation-user-data": "./UserData/lcm_user_data.py", + "lcm-operation-user-data-class": "SampleUserData" + } + } + + if instantiation_level_id: + request_body["instantiationLevelId"] = instantiation_level_id + + if ext_managed_vl: + request_body["extManagedVirtualLinks"] = ext_managed_vl + + if ext_vl: + request_body["extVirtualLinks"] = ext_vl + + if vim_id: + request_body["vimConnectionInfo"] = [ + {"id": uuidutils.generate_uuid(), + "vimId": vim_id, + "vimType": "ETSINFV.OPENSTACK_KEYSTONE.v_2"}] + + return request_body + + def _step_lcm_instantiate( + self, username, inst_id, vim_id, expected_status_code): + neutron_client = self.neutronclient() + net = neutron_client.list_networks() + networks = {} + for network in net['networks']: + networks[network['name']] = network['id'] + net0_id = networks.get('net0') + if not net0_id: + self.fail("net0 network is not available") + net_mgmt_id = networks.get('net_mgmt') + if not net_mgmt_id: + self.fail("net_mgmt network is not available") + network_uuid = self.create_network(neutron_client, + "external_network") + port_uuid = self.create_port(neutron_client, network_uuid) + ext_vl = get_external_virtual_links(net0_id, net_mgmt_id, + port_uuid) + request_body = self._instantiate_vnf_request( + "simple", vim_id=vim_id, ext_vl=ext_vl) + + self._lcm_instantiate( + username, inst_id, request_body, expected_status_code) + + def _step_lcm_scale_out(self, username, inst_id, expected_status_code): + request_body = fake_vnflcm.VnfInstances.make_scale_request_body( + 'SCALE_OUT') + self._step_lcm_scale( + username, inst_id, request_body, expected_status_code) + + def _step_lcm_scale_in(self, username, inst_id, expected_status_code): + request_body = fake_vnflcm.VnfInstances.make_scale_request_body( + 'SCALE_IN') + self._step_lcm_scale( + username, inst_id, request_body, expected_status_code) + + def _change_ext_conn_vnf_request(self, vim_id=None, ext_vl=None, + vim_type="ETSINFV.OPENSTACK_KEYSTONE.v_2"): + request_body = {} + if ext_vl: + request_body["extVirtualLinks"] = ext_vl + + if vim_id: + request_body["vimConnectionInfo"] = [ + {"id": uuidutils.generate_uuid(), + "vimId": vim_id, + "vimType": vim_type}] + + return request_body + + def _step_lcm_change_connectivity(self, username, inst_id, new_vim_id, + expected_status_code): + client = self.get_tk_http_client_by_user(username) + + neutron_client = self.neutronclient() + net = neutron_client.list_networks() + networks = {} + for network in net['networks']: + networks[network['name']] = network['id'] + net0_id = networks.get('net0') + if not net0_id: + self.fail("net0 network is not available") + net_mgmt_id = networks.get('net_mgmt') + if not net_mgmt_id: + self.fail("net_mgmt network is not available") + network_uuid = self.create_network(neutron_client, + "external_network") + port_uuid = self.create_port(neutron_client, network_uuid) + ext_vl = get_external_virtual_links(net0_id, net_mgmt_id, + port_uuid) + change_ext_conn_req_body = self._change_ext_conn_vnf_request( + vim_id=new_vim_id, ext_vl=ext_vl) + url = os.path.join( + self.base_vnf_instances_url, + inst_id, + "change_ext_conn") + resp, body = client.do_request(url, "POST", + body=jsonutils.dumps(change_ext_conn_req_body)) + self.assertEqual(expected_status_code, resp.status_code) + if expected_status_code == 202: + self._wait_lcm_done( + operation='CHANGE_EXT_CONN', + expected_operation_status='COMPLETED', + vnf_instance_id=inst_id) + + def test_vnflcm_apis_vnf_instance_with_area_vnf(self): + self.register_subscription() + + inst_id_a, inst_id_b = self.steps_lcm_create_and_get_with_area() + + # step 12 LCM-Instantiate, Resource Group A / User Group A + self._step_lcm_instantiate('user_a', inst_id_a, self.vim_a['id'], 202) + + # step 13 LCM-Instantiate, Resource Group B / User Group A + self._step_lcm_instantiate('user_a', inst_id_b, self.vim_b['id'], 403) + + # step 14 LCM-Instantiate, Resource Group B / User Group all + self._step_lcm_instantiate( + 'user_all', inst_id_b, self.vim_b['id'], 202) + + self.steps_lcm_get_scale_heal_modify_with_area(inst_id_a, inst_id_b) + + # step 34 LCM-Change-Connectivity, Resource Group A / User Group A + self._step_lcm_change_connectivity( + 'user_a', inst_id_a, self.vim_a_1['id'], 202) + + # step 35 LCM-Change-Connectivity, Resource Group B / User Group A + self._step_lcm_change_connectivity( + 'user_a', inst_id_b, self.vim_b_1['id'], 403) + + # step 36 LCM-Change-Connectivity, Resource Group B / User Group all + self._step_lcm_change_connectivity( + 'user_all', inst_id_b, self.vim_b_1['id'], 202) + + self.steps_lcm_terminate_delete_with_area(inst_id_a, inst_id_b) + + def test_vnflcm_apis_vnf_instance_without_area_vnf(self): + self.register_subscription() + + inst_id_c = self.steps_lcm_create_and_get_without_area() + + # step 8 LCM-Instantiate, Resource Group C / User Group C + self._step_lcm_instantiate('user_c', inst_id_c, self.vim_c['id'], 202) + + self.steps_lcm_get_scale_heal_modify_without_area(inst_id_c) + + # step 27 LCM-Change-Connectivity, Resource Group C / User Group C + self._step_lcm_change_connectivity( + 'user_c', inst_id_c, self.vim_c_1['id'], 403) + + # step 28 LCM-Change-Connectivity, Resource Group C / User Group all + self._step_lcm_change_connectivity( + 'user_all', inst_id_c, self.vim_c_1['id'], 403) + + # step 29 LCM-Change-Connectivity, Resource Group C / User Group admin + self._step_lcm_change_connectivity( + 'user_admin', inst_id_c, self.vim_c_1['id'], 202) + + self.steps_lcm_terminate_delete_without_area(inst_id_c) diff --git a/tacker/tests/functional/sol_enhanced_policy/sol/test_policy_vnflcm_apis_v2.py b/tacker/tests/functional/sol_enhanced_policy/sol/test_policy_vnflcm_apis_v2.py new file mode 100644 index 000000000..6c3a2509f --- /dev/null +++ b/tacker/tests/functional/sol_enhanced_policy/sol/test_policy_vnflcm_apis_v2.py @@ -0,0 +1,974 @@ +# +# 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 os +import time +import yaml + +from oslo_utils import uuidutils + +from tacker.sol_refactored.common import http_client +from tacker.sol_refactored import objects +from tacker.tests.functional.sol_enhanced_policy.base import ( + BaseEnhancedPolicyTest) +from tacker.tests.functional.sol_v2_common import base_v2 +from tacker.tests.functional.sol_v2_common import paramgen +from tacker.tests.functional.sol_v2_common.test_vnflcm_basic_common import ( + CommonVnfLcmTest) +from tacker.tests import utils as base_utils + + +class VnflcmAPIsV2VNFBase(CommonVnfLcmTest, BaseEnhancedPolicyTest): + + user_role_map = { + 'user_a': ['VENDOR_company_A', 'AREA_area_A@region_A', + 'TENANT_namespace_A', 'manager'], + 'user_a_1': ['VENDOR_company_A', 'manager'], + 'user_b': ['VENDOR_company_B', 'AREA_area_B@region_B', + 'TENANT_namespace_B', 'manager'], + 'user_c': ['VENDOR_company_C', 'AREA_area_C@region_C', + 'TENANT_namespace-c', 'manager'], + 'user_all': ['VENDOR_all', 'AREA_all@all', 'TENANT_all', 'manager'], + 'user_admin': ['admin'] + } + + @classmethod + def setUpClass(cls): + CommonVnfLcmTest.setUpClass() + BaseEnhancedPolicyTest.setUpClass(cls) + + for user in cls.users: + client = cls.get_local_tacker_http_client(user.name) + setattr(cls, + cls.TK_HTTP_CLIENT_NAME % {'username': user.name}, client) + + cls.tacker_client = cls.get_local_tacker_http_client('user_all') + + cur_dir = os.path.dirname(__file__) + image_dir = os.path.join( + cur_dir, "../../../etc/samples/etsi/nfv/common/Files/images") + + image_file = "cirros-0.5.2-x86_64-disk.img" + image_path = os.path.abspath(os.path.join(image_dir, image_file)) + + # for basic lcms tests min pattern + basic_lcms_min_path = os.path.join(cur_dir, + "../../sol_v2_common/samples/basic_lcms_min") + + # for update vnf test + update_vnf_path = os.path.join(cur_dir, + "../../sol_v2_common/samples/update_vnf") + # for change ext conn + change_vnfpkg_from_image_to_image_path_2 = os.path.join(cur_dir, + "../../sol_v2_common/samples/test_change_vnf_pkg_with_new_image") + + # for user_a + cls.vnf_pkg_a, cls.vnfd_id_a = cls.create_vnf_package( + basic_lcms_min_path, image_path=image_path, provider='company_A') + + cls.vnf_pkg_a_1, cls.vnfd_id_a_1 = cls.create_vnf_package( + update_vnf_path, provider='company_A') + + cls.vnf_pkg_a_2, cls.vnfd_id_a_2 = cls.create_vnf_package( + change_vnfpkg_from_image_to_image_path_2, image_path=image_path, + provider='company_A') + + # for user_b + cls.vnf_pkg_b, cls.vnfd_id_b = cls.create_vnf_package( + basic_lcms_min_path, image_path=image_path, provider='company_B') + + cls.vnf_pkg_b_1, cls.vnfd_id_b_1 = cls.create_vnf_package( + update_vnf_path, provider='company_B') + + cls.vnf_pkg_b_2, cls.vnfd_id_b_2 = cls.create_vnf_package( + change_vnfpkg_from_image_to_image_path_2, image_path=image_path, + provider='company_B') + + # for user_c + cls.vnf_pkg_c, cls.vnfd_id_c = cls.create_vnf_package( + basic_lcms_min_path, image_path=image_path, provider='company_C') + + cls.vnf_pkg_c_1, cls.vnfd_id_c_1 = cls.create_vnf_package( + update_vnf_path, provider='company_C') + + cls.vnf_pkg_c_2, cls.vnfd_id_c_2 = cls.create_vnf_package( + change_vnfpkg_from_image_to_image_path_2, image_path=image_path, + provider='company_C') + + @classmethod + def tearDownClass(cls): + cls.delete_vnf_package(cls.vnf_pkg_a) + cls.delete_vnf_package(cls.vnf_pkg_a_1) + cls.delete_vnf_package(cls.vnf_pkg_a_2) + cls.delete_vnf_package(cls.vnf_pkg_b) + cls.delete_vnf_package(cls.vnf_pkg_b_1) + cls.delete_vnf_package(cls.vnf_pkg_b_2) + cls.delete_vnf_package(cls.vnf_pkg_c) + cls.delete_vnf_package(cls.vnf_pkg_c_1) + cls.delete_vnf_package(cls.vnf_pkg_c_2) + BaseEnhancedPolicyTest.tearDownClass() + super(VnflcmAPIsV2VNFBase, cls).tearDownClass() + + @classmethod + def get_vim_info(cls, vim_conf='local-vim.yaml'): + vim_params = yaml.safe_load(base_utils.read_file(vim_conf)) + vim_params['auth_url'] += '/v3' + + vim_info = objects.VimConnectionInfo( + interfaceInfo={'endpoint': vim_params['auth_url']}, + accessInfo={ + 'region': 'RegionOne', + 'project': vim_params['project_name'], + 'username': vim_params['username'], + 'password': vim_params['password'], + 'userDomain': vim_params['user_domain_name'], + 'projectDomain': vim_params['project_domain_name'] + } + ) + + return vim_info + + @classmethod + def get_local_tacker_http_client(cls, username): + vim_info = cls.get_vim_info(vim_conf=cls.local_vim_conf_file) + + auth = http_client.KeystonePasswordAuthHandle( + auth_url=vim_info.interfaceInfo['endpoint'], + username=username, + password='devstack', + project_name=vim_info.accessInfo['project'], + user_domain_name=vim_info.accessInfo['userDomain'], + project_domain_name=vim_info.accessInfo['projectDomain'] + ) + return http_client.HttpClient(auth) + + def change_ext_conn_max(self, net_ids, subnets, auth_url, area): + vim_id_1 = uuidutils.generate_uuid() + vim_id_2 = uuidutils.generate_uuid() + + ext_vl_1 = { + "id": uuidutils.generate_uuid(), + "vimConnectionId": vim_id_1, + "resourceProviderId": uuidutils.generate_uuid(), + "resourceId": net_ids['ft-net1'], + "extCps": [ + { + "cpdId": "VDU1_CP1", + "cpConfig": { + "VDU1_CP1": { + "cpProtocolData": [{ + "layerProtocol": "IP_OVER_ETHERNET", + "ipOverEthernet": { + # "macAddress": omitted, + # "segmentationId": omitted, + "ipAddresses": [{ + "type": "IPV4", + # "fixedAddresses": omitted, + "numDynamicAddresses": 1, + # "addressRange": omitted, + "subnetId": subnets[ + 'ft-ipv4-subnet1']}] + } + }]} + } + }, + { + "cpdId": "VDU2_CP2", + "cpConfig": { + "VDU2_CP2": { + "cpProtocolData": [{ + "layerProtocol": "IP_OVER_ETHERNET", + "ipOverEthernet": { + # "macAddress": omitted, + # "segmentationId": omitted, + "ipAddresses": [{ + "type": "IPV4", + "fixedAddresses": [ + "22.22.22.101" + ], + # "numDynamicAddresses": omitted + # "addressRange": omitted, + "subnetId": subnets['ft-ipv4-subnet1'] + }, { + "type": "IPV6", + # "fixedAddresses": omitted, + # "numDynamicAddresses": omitted, + "numDynamicAddresses": 1, + # "addressRange": omitted, + "subnetId": subnets['ft-ipv6-subnet1'] + }] + } + }] + }} + } + ] + } + vim_1 = { + "vimId": vim_id_1, + "vimType": "ETSINFV.OPENSTACK_KEYSTONE.V_3", + "interfaceInfo": {"endpoint": auth_url}, + "accessInfo": { + "username": "nfv_user", + "region": "RegionOne", + "password": "devstack", + "project": "nfv", + "projectDomain": "Default", + "userDomain": "Default" + }, + "extra": {"area": area} + } + vim_2 = { + "vimId": vim_id_2, + "vimType": "ETSINFV.OPENSTACK_KEYSTONE.V_3", + "interfaceInfo": {"endpoint": auth_url}, + "accessInfo": { + "username": "dummy_user", + "region": "RegionOne", + "password": "dummy_password", + "project": "dummy_project", + "projectDomain": "Default", + "userDomain": "Default" + }, + "extra": {"area": area} + } + if not area: + vim_1.pop('extra') + vim_2.pop('extra') + return { + "extVirtualLinks": [ + ext_vl_1 + ], + "vimConnectionInfo": { + "vim1": vim_1, + "vim2": vim_2 + }, + "additionalParams": {"dummy-key": "dummy-val"} + } + + def _step_lcm_create(self, username, vnfd_id, expected_status_code): + self.tacker_client = self.get_tk_http_client_by_user(username) + create_req = paramgen.create_vnf_min(vnfd_id) + resp, body = self.create_vnf_instance(create_req) + self.assertEqual(expected_status_code, resp.status_code) + if expected_status_code == 201: + return body['id'] + else: + return None + + def instantiate_vnf(self, area=None, vim_id=None): + # Omit except for required attributes + # NOTE: Only the following cardinality attributes are set. + # - 1 + # - 1..N (1) + vim_id_1 = uuidutils.generate_uuid() + vim_id_2 = uuidutils.generate_uuid() + if area: + vim_1 = { + "vimId": vim_id_1, + "vimType": "ETSINFV.OPENSTACK_KEYSTONE.V_3", + "interfaceInfo": {"endpoint": self.auth_url}, + "accessInfo": { + "username": "nfv_user", + "region": "RegionOne", + "password": "devstack", + "project": "nfv", + "projectDomain": "Default", + "userDomain": "Default" + }, + "extra": {"area": area} + } + vim_2 = { + "vimId": vim_id_2, + "vimType": "ETSINFV.OPENSTACK_KEYSTONE.V_3", + "interfaceInfo": {"endpoint": self.auth_url}, + "accessInfo": { + "username": "dummy_user", + "region": "RegionOne", + "password": "dummy_password", + "project": "dummy_project", + "projectDomain": "Default", + "userDomain": "Default" + }, + "extra": {"area": area} + } + if vim_id: + vim_1 = { + "vimId": vim_id, + "vimType": "ETSINFV.OPENSTACK_KEYSTONE.V_3" + } + vim_2 = { + "vimId": vim_id, + "vimType": "ETSINFV.OPENSTACK_KEYSTONE.V_3" + } + + return { + "flavourId": "simple", + "vimConnectionInfo": { + "vim1": vim_1, + "vim2": vim_2 + } + } + + def _step_lcm_instantiate(self, username, inst_id, glance_image, + flavour_vdu_dict, zone_name_list, expected_status_code, + area=None, vim_id=None): + self.tacker_client = self.get_tk_http_client_by_user(username) + self._set_grant_response( + False, 'INSTANTIATE', glance_image=glance_image, + flavour_vdu_dict=flavour_vdu_dict, zone_name_list=zone_name_list) + instantiate_req = self.instantiate_vnf(area, vim_id) + resp, body = self.instantiate_vnf_instance(inst_id, instantiate_req) + self.assertEqual(expected_status_code, resp.status_code) + if expected_status_code == 202: + 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 instantiate completion. + time.sleep(3) + + def _step_lcm_show(self, username, inst_id, expected_status_code): + self.tacker_client = self.get_tk_http_client_by_user(username) + resp, body = self.show_vnf_instance(inst_id) + self.assertEqual(expected_status_code, resp.status_code) + + def _step_lcm_list(self, username, expected_inst_list): + self.tacker_client = self.get_tk_http_client_by_user(username) + resp, vnf_instances = self.list_vnf_instance() + self.assertEqual(200, resp.status_code) + inst_ids = set([inst.get('id') for inst in vnf_instances]) + for inst_id in expected_inst_list: + self.assertIn(inst_id, inst_ids) + + def _step_lcm_heal(self, username, inst_id, glance_image, flavour_vdu_dict, + zone_name_list, expected_status_code): + self.tacker_client = self.get_tk_http_client_by_user(username) + self._set_grant_response( + False, 'HEAL', glance_image=glance_image, + flavour_vdu_dict=flavour_vdu_dict, zone_name_list=zone_name_list) + + heal_req = paramgen.heal_vnf_all_min() + resp, body = self.heal_vnf_instance(inst_id, heal_req) + self.assertEqual(expected_status_code, resp.status_code) + if expected_status_code == 202: + lcmocc_id = os.path.basename(resp.headers['Location']) + self.wait_lcmocc_complete(lcmocc_id) + time.sleep(3) + + def _step_lcm_update(self, username, inst_id, update_vnfd_id, + expected_status_code): + self.tacker_client = self.get_tk_http_client_by_user(username) + update_req = paramgen.update_vnf_min_with_parameter(update_vnfd_id) + resp, body = self.update_vnf_instance(inst_id, update_req) + self.assertEqual(expected_status_code, resp.status_code) + if expected_status_code == 202: + lcmocc_id = os.path.basename(resp.headers['Location']) + self.wait_lcmocc_complete(lcmocc_id) + + def _step_lcm_scale_out(self, username, inst_id, glance_image, + flavour_vdu_dict, zone_name_list, + expected_status_code): + self.tacker_client = self.get_tk_http_client_by_user(username) + self._set_grant_response( + False, 'SCALE', glance_image=glance_image, + flavour_vdu_dict=flavour_vdu_dict, zone_name_list=zone_name_list) + scaleout_req = paramgen.scaleout_vnf_min() + resp, body = self.scale_vnf_instance(inst_id, scaleout_req) + self.assertEqual(expected_status_code, resp.status_code) + if expected_status_code == 202: + lcmocc_id = os.path.basename(resp.headers['Location']) + self.wait_lcmocc_complete(lcmocc_id) + + def _step_lcm_scale_in(self, username, inst_id, glance_image, + flavour_vdu_dict, zone_name_list, expected_status_code): + self.tacker_client = self.get_tk_http_client_by_user(username) + self._set_grant_response( + False, 'SCALE', glance_image=glance_image, + flavour_vdu_dict=flavour_vdu_dict, zone_name_list=zone_name_list) + scalein_req = paramgen.scalein_vnf_min() + resp, body = self.scale_vnf_instance(inst_id, scalein_req) + self.assertEqual(expected_status_code, resp.status_code) + if expected_status_code == 202: + lcmocc_id = os.path.basename(resp.headers['Location']) + self.wait_lcmocc_complete(lcmocc_id) + + def _step_lcm_change_vnfpkg(self, username, inst_id, change_vnfd_id, + glance_image, flavour_vdu_dict, expected_status_code): + self.tacker_client = self.get_tk_http_client_by_user(username) + change_vnfpkg_req = paramgen.change_vnfpkg_with_ext_vl( + change_vnfd_id, self.get_network_ids(['net1'])) + + del change_vnfpkg_req[ + "additionalParams"]["lcm-operation-coordinate-old-vnf"] + del change_vnfpkg_req[ + "additionalParams"]["lcm-operation-coordinate-new-vnf"] + self._set_grant_response(False, 'CHANGE_VNFPKG', + glance_image=glance_image, + flavour_vdu_dict=flavour_vdu_dict) + + resp, body = self.change_vnfpkg(inst_id, change_vnfpkg_req) + self.assertEqual(expected_status_code, resp.status_code) + if expected_status_code == 202: + 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 change_vnfpkg completion. + time.sleep(3) + + def _step_lcm_change_ext_conn(self, username, inst_id, area, + zone_name_list, expected_status_code): + self.tacker_client = self.get_tk_http_client_by_user(username) + self._set_grant_response( + False, 'CHANGE_EXT_CONN', zone_name_list=zone_name_list) + + # Create a new network and subnet to check the IP allocation of + # IPv4 and IPv6 + ft_net0_name = 'ft-net0' + ft_net0_subs = { + 'ft-ipv4-subnet0': { + 'range': '100.100.100.0/24', + 'ip_version': 4 + }, + 'ft-ipv6-subnet0': { + 'range': '1111:2222:3333::/64', + 'ip_version': 6 + } + } + ft_net0_id = self.create_network(ft_net0_name) + self.addCleanup(self.delete_network, ft_net0_id) + for sub_name, val in ft_net0_subs.items(): + # subnet is automatically deleted with network deletion + self.create_subnet( + ft_net0_id, sub_name, val['range'], val['ip_version']) + + # Create a new network for change external connectivity + ft_net1_name = 'ft-net1' + ft_net1_subs = { + 'ft-ipv4-subnet1': { + 'range': '22.22.22.0/24', + 'ip_version': 4 + }, + 'ft-ipv6-subnet1': { + 'range': '1111:2222:4444::/64', + 'ip_version': 6 + } + } + ft_net1_id = self.create_network(ft_net1_name) + self.addCleanup(self.delete_network, ft_net1_id) + for sub_name, val in ft_net1_subs.items(): + # subnet is automatically deleted with network deletion + self.create_subnet( + ft_net1_id, sub_name, val['range'], val['ip_version']) + + net_ids = self.get_network_ids( + ['net0', 'net1', 'net_mgmt', 'ft-net0', 'ft-net1']) + subnet_ids = self.get_subnet_ids( + ['subnet0', 'subnet1', 'ft-ipv4-subnet0', 'ft-ipv6-subnet0', + 'ft-ipv4-subnet1', 'ft-ipv6-subnet1']) + port_names = ['VDU2_CP1-1', 'VDU2_CP1-2'] + port_ids = {} + for port_name in port_names: + port_id = self.create_port(net_ids['net0'], port_name) + port_ids[port_name] = port_id + self.addCleanup(self.delete_port, port_id) + + change_ext_conn_req = self.change_ext_conn_max( + net_ids, subnet_ids, self.auth_url, area) + resp, body = self.change_ext_conn(inst_id, change_ext_conn_req) + self.assertEqual(expected_status_code, resp.status_code) + if expected_status_code == 202: + lcmocc_id = os.path.basename(resp.headers['Location']) + self.wait_lcmocc_complete(lcmocc_id) + time.sleep(3) + + def _step_lcm_terminate(self, username, inst_id, expected_status_code): + self.tacker_client = self.get_tk_http_client_by_user(username) + self._set_grant_response(False, 'TERMINATE') + terminate_req = paramgen.terminate_vnf_min() + resp, body = self.terminate_vnf_instance(inst_id, terminate_req) + self.assertEqual(expected_status_code, resp.status_code) + if expected_status_code == 202: + 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(3) + + def _step_lcm_delete(self, username, inst_id, expected_status_code): + self.tacker_client = self.get_tk_http_client_by_user(username) + resp, body = self.delete_vnf_instance(inst_id) + self.assertEqual(expected_status_code, resp.status_code) + + def vnflcm_apis_v2_vnf_test_before_instantiate(self): + # Create subscription + self.tacker_client = self.get_tk_http_client_by_user('user_all') + + 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'] + + # Test notification + self.assert_notification_get(callback_url) + # check usageState of VNF Package + self._check_package_usage(False, self.vnf_pkg_a) + + # step 1 LCM-CreateV2, Resource Group A / User Group A + inst_id_a = self._step_lcm_create('user_a', self.vnfd_id_a, 201) + + # step 2 LCM-CreateV2, Resource Group B / User Group A + self._step_lcm_create('user_a', self.vnfd_id_b, 403) + + # step 3 LCM-CreateV2, Resource Group B / User Group all + inst_id_b = self._step_lcm_create('user_all', self.vnfd_id_b, 201) + + # step 4 LCM-ShowV2, Resource Group A / User Group A + self._step_lcm_show('user_a', inst_id_a, 200) + + # step 5 LCM-ShowV2, Resource Group A / User Group A-1 + self._step_lcm_show('user_a_1', inst_id_a, 200) + + # step 6 LCM-ShowV2, Resource Group B / User Group A + self._step_lcm_show('user_a', inst_id_b, 403) + + # step 7 LCM-ShowV2, Resource Group B / User Group all + self._step_lcm_show('user_all', inst_id_b, 200) + + # step 8 LCM-ListV2, Resource Group A / User Group A + self._step_lcm_list('user_a', [inst_id_a]) + + # step 9 LCM-ListV2, Resource Group - / User Group A-1 + self._step_lcm_list('user_a_1', [inst_id_a]) + + # step 10 LCM-ListV2, Resource Group - / User Group B + self._step_lcm_list('user_b', [inst_id_b]) + + # step 11 LCM-ListV2, Resource Group - / User Group all + self._step_lcm_list('user_all', [inst_id_a, inst_id_b]) + + return sub_id, inst_id_a, inst_id_b + + def vnflcm_apis_v2_vnf_test_after_instantiate( + self, sub_id, inst_id_a, inst_id_b, zone_name_list, glance_image, + flavour_vdu_dict): + + # step 15 LCM-ShowV2, Resource Group A / User Group A + self._step_lcm_show('user_a', inst_id_a, 200) + + # step 16 LCM-ShowV2, Resource Group A / User Group A-1 + self._step_lcm_show('user_a_1', inst_id_a, 403) + + # step 17 LCM-ShowV2, Resource Group B / User Group A + self._step_lcm_show('user_a', inst_id_b, 403) + + # step 18 LCM-ShowV2, Resource Group B / User Group all + self._step_lcm_show('user_all', inst_id_b, 200) + + # step 19 LCM-ListV2, Resource Group - / User Group A + self._step_lcm_list('user_a', [inst_id_a]) + + # step 20 LCM-ListV2, Resource Group - / User Group A-1 + self._step_lcm_list('user_a_1', []) + + # step 21 LCM-ListV2, Resource Group - / User Group B + self._step_lcm_list('user_b', [inst_id_b]) + + # step 22 LCM-ListV2, Resource Group - / User Group all + self._step_lcm_list('user_all', [inst_id_a, inst_id_b]) + + # step 23 LCM-ScaleV2(out), Resource Group A / User Group A + self._step_lcm_scale_out('user_a', inst_id_a, glance_image, + flavour_vdu_dict, zone_name_list, 202) + + # step 24 LCM-ScaleV2(out), Resource Group B / User Group A + self._step_lcm_scale_out('user_a', inst_id_b, glance_image, + flavour_vdu_dict, zone_name_list, 403) + + # step 25 LCM-ScaleV2(out), Resource Group B / User Group all + self._step_lcm_scale_out('user_all', inst_id_b, glance_image, + flavour_vdu_dict, zone_name_list, 202) + + # step 26 LCM-ScaleV2(in), Resource Group A / User Group A + self._step_lcm_scale_in('user_a', inst_id_a, glance_image, + flavour_vdu_dict, zone_name_list, 202) + + # step 27 LCM-ScaleV2(in), Resource Group B / User Group A + self._step_lcm_scale_in('user_a', inst_id_b, glance_image, + flavour_vdu_dict, zone_name_list, 403) + + # step 28 LCM-ScaleV2(in), Resource Group B / User Group all + self._step_lcm_scale_in('user_all', inst_id_b, glance_image, + flavour_vdu_dict, zone_name_list, 202) + + # step 29 LCM-HealV2, Resource Group A / User Group A + self._step_lcm_heal('user_a', inst_id_a, glance_image, + flavour_vdu_dict, zone_name_list, 202) + + # step 30 LCM-HealV2, Resource Group B / User Group A + self._step_lcm_heal('user_a', inst_id_b, glance_image, + flavour_vdu_dict, zone_name_list, 403) + + # step 31 LCM-HealV2, Resource Group B / User Group all + self._step_lcm_heal('user_all', inst_id_b, glance_image, + flavour_vdu_dict, zone_name_list, 202) + + # step 32 LCM-ModifyV2, Resource Group A / User Group A + self._step_lcm_update('user_a', inst_id_a, self.vnfd_id_a_1, 202) + + # step 33 LCM-ModifyV2, Resource Group b / User Group A + self._step_lcm_update('user_a', inst_id_b, self.vnfd_id_b_1, 403) + + # step 34 LCM-ModifyV2, Resource Group B / User Group all + self._step_lcm_update('user_all', inst_id_b, self.vnfd_id_b_1, 202) + + # step 35 LCM-Change-ConnectivityV2, Resource Group A / User Group A + self._step_lcm_change_ext_conn( + 'user_a', inst_id_a, 'area_A@region_A', zone_name_list, 202) + + # step 36 LCM-Change-ConnectivityV2, Resource Group B / User Group A + self._step_lcm_change_ext_conn( + 'user_a', inst_id_b, 'area_B@region_B', zone_name_list, 403) + + # step 37 LCM-Change-ConnectivityV2, Resource Group B / User Group all + self._step_lcm_change_ext_conn( + 'user_all', inst_id_b, 'area_B@region_B', zone_name_list, 202) + + # step 38 LCM-Change-VnfPkgV2, Resource Group A / User Group A + self._step_lcm_update('user_a', inst_id_a, self.vnfd_id_a, 202) + self._step_lcm_change_vnfpkg('user_a', inst_id_a, self.vnfd_id_a_2, + glance_image, flavour_vdu_dict, 202) + + # step 39 LCM-Change-VnfPkgV2, Resource Group B / User Group A + self._step_lcm_change_vnfpkg('user_a', inst_id_b, self.vnfd_id_b_2, + glance_image, flavour_vdu_dict, 403) + + # step 40 LCM-Change-VnfPkgV2, Resource Group B / User Group all + self._step_lcm_update('user_all', inst_id_b, self.vnfd_id_b, 202) + self._step_lcm_change_vnfpkg('user_all', inst_id_b, self.vnfd_id_b_2, + glance_image, flavour_vdu_dict, 202) + + # step 41 LCM-TerminateV2, Resource Group A / User Group A + self._step_lcm_terminate('user_a', inst_id_a, 202) + + # step 42 LCM-TerminateV2, Resource Group B / User Group A + self._step_lcm_terminate('user_a', inst_id_b, 403) + + # step 43 LCM-TerminateV2, Resource Group B / User Group all + self._step_lcm_terminate('user_all', inst_id_b, 202) + + # step 44 LCM-DeleteV2, Resource Group A / User Group A + self._step_lcm_delete('user_a', inst_id_a, 204) + + # step 45 LCM-DeleteV2, Resource Group B / User Group A + self._step_lcm_delete('user_a', inst_id_b, 403) + + # step 46 LCM-DeleteV2, Resource Group B / User Group all + self._step_lcm_delete('user_all', inst_id_b, 204) + + # Delete subscription + resp, body = self.delete_subscription(sub_id) + self.assertEqual(204, resp.status_code) + self.check_resp_headers_in_delete(resp) + + # Show subscription + resp, body = self.show_subscription(sub_id) + self.assertEqual(404, resp.status_code) + + +class VnflcmAPIsV2VNFInstantiateWithArea(VnflcmAPIsV2VNFBase): + + def test_vnflcm_apis_v2_vnf_with_area_in_vim_conn_info(self): + glance_image = None + flavour_vdu_dict = None + zone_name_list = None + + sub_id, inst_id_a, inst_id_b = ( + self.vnflcm_apis_v2_vnf_test_before_instantiate()) + + # step 12 LCM-InstantiateV2, Resource Group A / User Group A + self._step_lcm_instantiate('user_a', inst_id_a, glance_image, + flavour_vdu_dict, zone_name_list, 202, area='area_A@region_A') + + # step 13 LCM-InstantiateV2, Resource Group B / User Group A + self._step_lcm_instantiate('user_a', inst_id_b, glance_image, + flavour_vdu_dict, zone_name_list, 403, + area='area_B@region_B') + + # step 14 LCM-InstantiateV2, Resource Group B / User Group all + self._step_lcm_instantiate('user_all', inst_id_b, glance_image, + flavour_vdu_dict, zone_name_list, 202, + area='area_B@region_B') + + self.vnflcm_apis_v2_vnf_test_after_instantiate( + sub_id, inst_id_a, inst_id_b, zone_name_list, glance_image, + flavour_vdu_dict) + + +class VnflcmAPIsV2VNFInstantiateWithoutArea(VnflcmAPIsV2VNFBase): + + @classmethod + def setUpClass(cls): + super().setUpClass() + + vim_type = 'openstack' + + local_vim = 'local-vim.yaml' + + cls.vim_a = cls._step_vim_register( + 'user_a', vim_type, local_vim, 'vim_a', 'area_A@region_A') + + cls.vim_a_1 = cls._step_vim_register( + 'user_a', vim_type, local_vim, 'vim_a_1', 'area_A@region_A') + + cls.vim_b = cls._step_vim_register( + 'user_b', vim_type, local_vim, 'vim_b', 'area_B@region_B') + + cls.vim_b_1 = cls._step_vim_register( + 'user_b', vim_type, local_vim, 'vim_b_1', 'area_B@region_B') + + @classmethod + def tearDownClass(cls): + + cls._step_vim_delete('user_a', cls.vim_a) + cls._step_vim_delete('user_a', cls.vim_a_1) + cls._step_vim_delete('user_b', cls.vim_b) + cls._step_vim_delete('user_b', cls.vim_b_1) + + super().tearDownClass() + + def test_vnflcm_apis_v2_vnf_without_area_in_vim_conn_info(self): + + glance_image = None + flavour_vdu_dict = None + zone_name_list = None + + sub_id, inst_id_a, inst_id_b = ( + self.vnflcm_apis_v2_vnf_test_before_instantiate()) + + # step 12 LCM-InstantiateV2, Resource Group A / User Group A + self._step_lcm_instantiate('user_a', inst_id_a, + glance_image, + flavour_vdu_dict, zone_name_list, 202, + vim_id=self.vim_a['id']) + + # step 13 LCM-InstantiateV2, Resource Group B / User Group A + self._step_lcm_instantiate('user_a', inst_id_b, + glance_image, + flavour_vdu_dict, zone_name_list, 403, + vim_id=self.vim_b['id']) + + # step 14 LCM-InstantiateV2, Resource Group B / User Group all + self._step_lcm_instantiate('user_all', inst_id_b, + glance_image, + flavour_vdu_dict, zone_name_list, 202, + vim_id=self.vim_b['id']) + + self.vnflcm_apis_v2_vnf_test_after_instantiate( + sub_id, inst_id_a, inst_id_b, zone_name_list, glance_image, + flavour_vdu_dict) + + +class VnflcmAPIsV2VNFInstanceWithoutArea(VnflcmAPIsV2VNFBase): + + @classmethod + def setUpClass(cls): + super().setUpClass() + + vim_type = 'openstack' + + local_vim = 'local-vim.yaml' + + cls.vim_c = cls._step_vim_register( + 'user_c', vim_type, local_vim, 'vim_c', None) + + cls.vim_c_1 = cls._step_vim_register( + 'user_c', vim_type, local_vim, 'vim_c_1', None) + + @classmethod + def tearDownClass(cls): + + cls._step_vim_delete('user_admin', cls.vim_c) + cls._step_vim_delete('user_admin', cls.vim_c_1) + + super().tearDownClass() + + def test_vnflcm_apis_v2_vnf_instance_without_area(self): + + glance_image = None + flavour_vdu_dict = None + zone_name_list = None + + # Create subscription + self.tacker_client = self.get_tk_http_client_by_user('user_all') + + 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'] + + # Test notification + self.assert_notification_get(callback_url) + # check usageState of VNF Package + self._check_package_usage(False, self.vnf_pkg_c) + + # step 1 LCM-CreateV2, Resource Group C / User Group C + inst_id_c = self._step_lcm_create('user_c', self.vnfd_id_c, 201) + + # step 2 LCM-ShowV2, Resource Group C / User Group C + self._step_lcm_show('user_c', inst_id_c, 200) + + # step 3 LCM-ShowV2, Resource Group C / User Group all + self._step_lcm_show('user_all', inst_id_c, 200) + + # step 4 LCM-ShowV2, Resource Group C / User Group admin + self._step_lcm_show('user_admin', inst_id_c, 200) + + # step 5 LCM-ListV2, Resource Group - / User Group C + self._step_lcm_list('user_c', [inst_id_c]) + + # step 6 LCM-ListV2, Resource Group - / User Group all + self._step_lcm_list('user_all', [inst_id_c]) + + # step 7 LCM-ListV2, Resource Group - / User Group admin + self._step_lcm_list('user_admin', [inst_id_c]) + + # step 8 LCM-InstantiateV2, Resource Group C / User Group C + self._step_lcm_instantiate('user_c', inst_id_c, + glance_image, + flavour_vdu_dict, zone_name_list, 202, + vim_id=self.vim_c['id']) + + # step 9 LCM-ShowV2, Resource Group C / User Group C + self._step_lcm_show('user_c', inst_id_c, 403) + + # step 10 LCM-ShowV2, Resource Group C / User Group all + self._step_lcm_show('user_all', inst_id_c, 403) + + # step 11 LCM-ShowV2, Resource Group C / User Group admin + self._step_lcm_show('user_admin', inst_id_c, 200) + + # step 12 LCM-ListV2, Resource Group - / User Group C + self._step_lcm_list('user_c', []) + + # step 13 LCM-ListV2, Resource Group - / User Group all + self._step_lcm_list('user_all', []) + + # step 14 LCM-ListV2, Resource Group - / User Group admin + self._step_lcm_list('user_admin', [inst_id_c]) + + # step 15 LCM-ScaleV2(out), Resource Group C / User Group C + self._step_lcm_scale_out('user_c', inst_id_c, glance_image, + flavour_vdu_dict, zone_name_list, 403) + + # step 16 LCM-ScaleV2(out), Resource Group C / User Group all + self._step_lcm_scale_out('user_all', inst_id_c, glance_image, + flavour_vdu_dict, zone_name_list, 403) + + # step 17 LCM-ScaleV2(out), Resource Group C / User Group admin + self._step_lcm_scale_out('user_admin', inst_id_c, glance_image, + flavour_vdu_dict, zone_name_list, 202) + + # step 18 LCM-ScaleV2(in), Resource Group C / User Group C + self._step_lcm_scale_in('user_c', inst_id_c, glance_image, + flavour_vdu_dict, zone_name_list, 403) + + # step 19 LCM-ScaleV2(in), Resource Group C / User Group A + self._step_lcm_scale_in('user_all', inst_id_c, glance_image, + flavour_vdu_dict, zone_name_list, 403) + + # step 20 LCM-ScaleV2(in), Resource Group C / User Group all + self._step_lcm_scale_in('user_admin', inst_id_c, glance_image, + flavour_vdu_dict, zone_name_list, 202) + + # step 21 LCM-HealV2, Resource Group C / User Group C + self._step_lcm_heal('user_c', inst_id_c, glance_image, + flavour_vdu_dict, zone_name_list, 403) + + # step 22 LCM-HealV2, Resource Group C / User Group A + self._step_lcm_heal('user_all', inst_id_c, glance_image, + flavour_vdu_dict, zone_name_list, 403) + + # step 23 LCM-HealV2, Resource Group C / User Group all + self._step_lcm_heal('user_admin', inst_id_c, glance_image, + flavour_vdu_dict, zone_name_list, 202) + + # step 24 LCM-ModifyV2, Resource Group C / User Group C + self._step_lcm_update('user_c', inst_id_c, self.vnfd_id_c_1, 403) + + # step 25 LCM-ModifyV2, Resource Group C / User Group A + self._step_lcm_update('user_all', inst_id_c, self.vnfd_id_c_1, 403) + + # step 26 LCM-ModifyV2, Resource Group C / User Group all + self._step_lcm_update('user_admin', inst_id_c, self.vnfd_id_c_1, 202) + + # step 27 LCM-Change-ConnectivityV2, Resource Group C / User Group C + self._step_lcm_change_ext_conn( + 'user_c', inst_id_c, None, zone_name_list, 403) + + # step 28 LCM-Change-ConnectivityV2, Resource Group C / User Group A + self._step_lcm_change_ext_conn( + 'user_all', inst_id_c, None, zone_name_list, 403) + + # step 29 LCM-Change-ConnectivityV2, Resource Group C / User Group all + self._step_lcm_change_ext_conn( + 'user_admin', inst_id_c, None, zone_name_list, 202) + + # step 30 LCM-Change-VnfPkgV2, Resource Group C / User Group C + self._step_lcm_change_vnfpkg('user_c', inst_id_c, self.vnfd_id_c_2, + glance_image, flavour_vdu_dict, 403) + + # step 31 LCM-Change-VnfPkgV2, Resource Group C / User Group A + self._step_lcm_change_vnfpkg('user_all', inst_id_c, self.vnfd_id_c_2, + glance_image, flavour_vdu_dict, 403) + + # step 32 LCM-Change-VnfPkgV2, Resource Group C / User Group all + self._step_lcm_update('user_admin', inst_id_c, self.vnfd_id_c, 202) + self._step_lcm_change_vnfpkg('user_admin', inst_id_c, self.vnfd_id_c_2, + glance_image, flavour_vdu_dict, 202) + + # step 33 LCM-TerminateV2, Resource Group C / User Group C + self._step_lcm_terminate('user_c', inst_id_c, 403) + + # step 34 LCM-TerminateV2, Resource Group C / User Group A + self._step_lcm_terminate('user_all', inst_id_c, 403) + + # step 35 LCM-TerminateV2, Resource Group C / User Group all + self._step_lcm_terminate('user_admin', inst_id_c, 202) + + # step 36 LCM-DeleteV2, Resource Group C / User Group C + self._step_lcm_delete('user_c', inst_id_c, 204) + + # Delete subscription + resp, body = self.delete_subscription(sub_id) + self.assertEqual(204, resp.status_code) + self.check_resp_headers_in_delete(resp) + + # Show subscription + resp, body = self.show_subscription(sub_id) + self.assertEqual(404, resp.status_code) diff --git a/tacker/tests/functional/sol_enhanced_policy/sol_kubernetes/__init__.py b/tacker/tests/functional/sol_enhanced_policy/sol_kubernetes/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/tacker/tests/functional/sol_enhanced_policy/sol_kubernetes/test_policy_cnflcm_apis_v1.py b/tacker/tests/functional/sol_enhanced_policy/sol_kubernetes/test_policy_cnflcm_apis_v1.py new file mode 100644 index 000000000..cfd143670 --- /dev/null +++ b/tacker/tests/functional/sol_enhanced_policy/sol_kubernetes/test_policy_cnflcm_apis_v1.py @@ -0,0 +1,220 @@ +# +# 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 tacker import context +from tacker.tests.functional.sol_enhanced_policy.base import ( + VnflcmAPIsV1Base) + + +class VnflcmAPIsV1CNFTest(VnflcmAPIsV1Base): + + @classmethod + def setUpClass(cls): + super().setUpClass() + + cls.client = cls.tackerclient(cls.local_vim_conf_file) + + # In the setUp of the parent class, it will be confirmed whether there + # is a default vim named "VIM0". Here, it is confirmed in advance. If + # it does not exist, it will be created to avoid the parent class + # throwing an exception. + cls.if_not_vim_create() + + local_k8s_vim = 'local-k8s-vim.yaml' + vim_type = 'kubernetes' + + cls.vim_a = cls._step_vim_register( + 'user_a', vim_type, local_k8s_vim, 'vim_a', 'area_A@region_A') + + cls.vim_a_1 = cls._step_vim_register( + 'user_a', vim_type, local_k8s_vim, 'vim_a_1', 'area_A@region_A') + + cls.vim_b = cls._step_vim_register( + 'user_b', vim_type, local_k8s_vim, 'vim_b', 'area_B@region_B') + + cls.vim_b_1 = cls._step_vim_register( + 'user_b', vim_type, local_k8s_vim, 'vim_b_1', 'area_B@region_B') + + cls.vim_c = cls._step_vim_register( + 'user_c', vim_type, local_k8s_vim, 'vim_c', None) + + cls.vim_c_1 = cls._step_vim_register( + 'user_c', vim_type, local_k8s_vim, 'vim_c_1', None) + + cls.pkg_a = cls._step_pkg_create('user_a') + + cls.vnfd_id_a = cls._step_pkg_upload_content( + 'user_a', cls.pkg_a, 'test_cnf', 'company_A', + namespace='namespace-a') + + cls.pkg_b = cls._step_pkg_create('user_b') + + cls.vnfd_id_b = cls._step_pkg_upload_content( + 'user_b', cls.pkg_b, 'test_cnf', 'company_B', + namespace='namespace-b') + + cls.pkg_c = cls._step_pkg_create('user_c') + + cls.vnfd_id_c = cls._step_pkg_upload_content( + 'user_c', cls.pkg_c, 'test_cnf', 'company_C', + namespace='namespace-c') + + @classmethod + def tearDownClass(cls): + cls._step_pkg_disable('user_a', cls.pkg_a,) + cls._step_pkg_disable('user_b', cls.pkg_b,) + cls._step_pkg_disable('user_c', cls.pkg_c,) + cls._step_pkg_delete('user_a', cls.pkg_a) + cls._step_pkg_delete('user_b', cls.pkg_b) + cls._step_pkg_delete('user_c', cls.pkg_c) + + cls._step_vim_delete('user_a', cls.vim_a) + cls._step_vim_delete('user_a', cls.vim_a_1) + cls._step_vim_delete('user_b', cls.vim_b) + cls._step_vim_delete('user_b', cls.vim_b_1) + cls._step_vim_delete('user_admin', cls.vim_c) + cls._step_vim_delete('user_admin', cls.vim_c_1) + + super().tearDownClass() + + @classmethod + def get_vim(cls, vim_list, vim_name): + if len(vim_list.values()) == 0: + assert False, "vim_list is Empty: Default VIM is missing" + + for vim_list in vim_list.values(): + for vim in vim_list: + if vim['name'] == vim_name: + return vim + return None + + @classmethod + def if_not_vim_create(cls): + cls.context = context.get_admin_context() + vim_list = cls.client.list_vims() + vim_name = 'VIM0' + if not vim_list: + cls.vim_k8s = cls._step_vim_register( + 'user_all', 'kubernetes', 'local-k8s-vim.yaml', + vim_name, None) + + vim = cls.get_vim(vim_list, vim_name) + if not vim: + + vim_k8s = cls._step_vim_register( + 'user_all', 'kubernetes', 'local-k8s-vim.yaml', + vim_name, None) + + cls.vim_k8s_id = vim_k8s.get('id') + else: + cls.vim_k8s_id = vim.get('id') + + @classmethod + def _instantiate_vnf_instance_request( + cls, flavour_id, vim_id=None, additional_param=None, + extra_param=None): + request_body = {"flavourId": flavour_id} + + if vim_id: + request_body["vimConnectionInfo"] = [ + {"id": uuidutils.generate_uuid(), + "vimId": vim_id, + "vimType": "kubernetes"}] + + if extra_param: + request_body["vimConnectionInfo"][0]["extra"] = extra_param + + if additional_param: + request_body["additionalParams"] = additional_param + + return request_body + + def _step_lcm_instantiate(self, username, inst_id, vim_id, namespace, + expected_status_code): + additional_param = { + "lcm-kubernetes-def-files": [ + "Files/kubernetes/deployment.yaml", + "Files/kubernetes/namespace.yaml" + ], + "namespace": namespace + } + request_body = self._instantiate_vnf_instance_request( + "simple", vim_id, additional_param=additional_param) + self._lcm_instantiate( + username, inst_id, request_body, expected_status_code) + + def _step_lcm_scale_out(self, username, inst_id, expected_status_code): + scale_out_req = { + "type": "SCALE_OUT", + "aspectId": "vdu2_aspect", + "numberOfSteps": 1 + } + self._step_lcm_scale( + username, inst_id, scale_out_req, expected_status_code) + + def _step_lcm_scale_in(self, username, inst_id, expected_status_code): + scale_in_req = { + "type": "SCALE_IN", + "aspectId": "vdu2_aspect", + "numberOfSteps": 1 + } + self._step_lcm_scale( + username, inst_id, scale_in_req, expected_status_code) + + def test_vnflcm_apis_vnf_instance_with_area_cnf(self): + + self.register_subscription() + + inst_id_a, inst_id_b = self.steps_lcm_create_and_get_with_area() + + # step 12 LCM-Instantiate, Resource Group A / User Group A + self._step_lcm_instantiate( + 'user_a', inst_id_a, self.vim_a['id'], 'namespace-a', 202) + + # step 13 LCM-Instantiate, Resource Group B / User Group A + self._step_lcm_instantiate( + 'user_a', inst_id_b, self.vim_b['id'], 'namespace-b', 403) + + # step 14 LCM-Instantiate, Resource Group B / User Group all + self._step_lcm_instantiate( + 'user_all', inst_id_b, self.vim_b['id'], 'namespace-b', 202) + + self.steps_lcm_get_scale_heal_modify_with_area(inst_id_a, inst_id_b) + + # NOTE: CNF has no LCM-Change-Connectivity + # step 34 LCM-Change-Connectivity, Resource Group A / User Group A + # step 35 LCM-Change-Connectivity, Resource Group b / User Group A + # step 36 LCM-Change-Connectivity, Resource Group B / User Group all + + self.steps_lcm_terminate_delete_with_area(inst_id_a, inst_id_b) + + def test_vnflcm_apis_vnf_instance_without_area_cnf(self): + + self.register_subscription() + + inst_id_c = self.steps_lcm_create_and_get_without_area() + + # step 8 LCM-Instantiate, Resource Group C / User Group C + self._step_lcm_instantiate( + 'user_c', inst_id_c, self.vim_c['id'], 'namespace-c', 202) + + self.steps_lcm_get_scale_heal_modify_without_area(inst_id_c) + + # NOTE: CNF has no LCM-Change-Connectivity + # step 27 LCM-Change-Connectivity, Resource Group C / User Group C + # step 28 LCM-Change-Connectivity, Resource Group C / User Group all + # step 29 LCM-Change-Connectivity, Resource Group C / User Group admin + + self.steps_lcm_terminate_delete_without_area(inst_id_c) diff --git a/tacker/tests/functional/sol_enhanced_policy/sol_kubernetes/test_policy_cnflcm_apis_v2.py b/tacker/tests/functional/sol_enhanced_policy/sol_kubernetes/test_policy_cnflcm_apis_v2.py new file mode 100644 index 000000000..fb3bb59ab --- /dev/null +++ b/tacker/tests/functional/sol_enhanced_policy/sol_kubernetes/test_policy_cnflcm_apis_v2.py @@ -0,0 +1,797 @@ +# +# 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 os +import time +import yaml + +from oslo_utils import uuidutils + +from tacker.sol_refactored.common import http_client +from tacker.sol_refactored import objects +from tacker.tests.functional.sol_enhanced_policy.base import ( + BaseEnhancedPolicyTest) +from tacker.tests.functional.sol_kubernetes_v2.base_v2 import ( + BaseVnfLcmKubernetesV2Test) +from tacker.tests.functional.sol_kubernetes_v2 import paramgen +from tacker.tests import utils as base_utils + + +class VnflcmAPIsV2CNFBase(BaseVnfLcmKubernetesV2Test, BaseEnhancedPolicyTest): + + user_role_map = { + 'user_a': ['VENDOR_company_A', 'AREA_area_A@region_A', + 'TENANT_namespace-a', 'manager'], + 'user_a_1': ['VENDOR_company_A', 'manager'], + 'user_b': ['VENDOR_company_B', 'AREA_area_B@region_B', + 'TENANT_namespace-b', 'manager'], + 'user_c': ['VENDOR_company_C', 'AREA_area_C@region_C', + 'TENANT_namespace-c', 'manager'], + 'user_all': ['VENDOR_all', 'AREA_all@all', 'TENANT_all', 'manager'], + 'user_admin': ['admin'] + } + + @classmethod + def setUpClass(cls): + BaseVnfLcmKubernetesV2Test.setUpClass() + BaseEnhancedPolicyTest.setUpClass(cls) + + for user in cls.users: + client = cls.get_local_tacker_http_client(user.name) + setattr( + cls, cls.TK_HTTP_CLIENT_NAME % {'username': user.name}, client) + + cls.tacker_client = cls.get_local_tacker_http_client('user_all') + + cur_dir = os.path.dirname(__file__) + + test_instantiate_cnf_resources_path = os.path.join( + cur_dir, + "../../sol_kubernetes_v2/samples/test_instantiate_cnf_resources") + test_change_vnf_pkg_with_deployment_path = os.path.join( + cur_dir, + "../../sol_kubernetes_v2/samples/" + "test_change_vnf_pkg_with_deployment") + + cls.vnf_pkg_a, cls.vnfd_id_a = cls.create_vnf_package( + test_instantiate_cnf_resources_path, provider='company_A', + namespace='namespace-a') + + cls.vnf_pkg_a_1, cls.vnfd_id_a_1 = cls.create_vnf_package( + test_change_vnf_pkg_with_deployment_path, provider='company_A', + namespace='namespace-a') + + cls.vnf_pkg_b, cls.vnfd_id_b = cls.create_vnf_package( + test_instantiate_cnf_resources_path, provider='company_B', + namespace='namespace-b') + + cls.vnf_pkg_b_1, cls.vnfd_id_b_1 = cls.create_vnf_package( + test_change_vnf_pkg_with_deployment_path, provider='company_B', + namespace='namespace-b') + + cls.vnf_pkg_c, cls.vnfd_id_c = cls.create_vnf_package( + test_instantiate_cnf_resources_path, provider='company_C', + namespace='namespace-c') + + cls.vnf_pkg_c_1, cls.vnfd_id_c_1 = cls.create_vnf_package( + test_change_vnf_pkg_with_deployment_path, provider='company_C', + namespace='namespace-c') + + @classmethod + def tearDownClass(cls): + super(VnflcmAPIsV2CNFBase, cls).tearDownClass() + + cls.delete_vnf_package(cls.vnf_pkg_a) + cls.delete_vnf_package(cls.vnf_pkg_a_1) + cls.delete_vnf_package(cls.vnf_pkg_b) + cls.delete_vnf_package(cls.vnf_pkg_b_1) + cls.delete_vnf_package(cls.vnf_pkg_c) + cls.delete_vnf_package(cls.vnf_pkg_c_1) + + @classmethod + def get_vim_info(cls, vim_conf='local-vim.yaml'): + vim_params = yaml.safe_load(base_utils.read_file(vim_conf)) + vim_params['auth_url'] += '/v3' + + vim_info = objects.VimConnectionInfo( + interfaceInfo={'endpoint': vim_params['auth_url']}, + accessInfo={ + 'region': 'RegionOne', + 'project': vim_params['project_name'], + 'username': vim_params['username'], + 'password': vim_params['password'], + 'userDomain': vim_params['user_domain_name'], + 'projectDomain': vim_params['project_domain_name'] + } + ) + + return vim_info + + @classmethod + def get_local_tacker_http_client(cls, username): + vim_info = cls.get_vim_info(vim_conf=cls.local_vim_conf_file) + + auth = http_client.KeystonePasswordAuthHandle( + auth_url=vim_info.interfaceInfo['endpoint'], + username=username, + password='devstack', + project_name=vim_info.accessInfo['project'], + user_domain_name=vim_info.accessInfo['userDomain'], + project_domain_name=vim_info.accessInfo['projectDomain'] + ) + + return http_client.HttpClient(auth) + + def _step_lcm_create(self, username, vnfd_id, expected_status_code): + self.tacker_client = self.get_tk_http_client_by_user(username) + + expected_inst_attrs = [ + 'id', + 'vnfInstanceName', + 'vnfInstanceDescription', + 'vnfdId', + 'vnfProvider', + 'vnfProductName', + 'vnfSoftwareVersion', + 'vnfdVersion', + # 'vnfConfigurableProperties', # omitted + # 'vimConnectionInfo', # omitted + 'instantiationState', + # 'instantiatedVnfInfo', # omitted + 'metadata', + # 'extensions', # omitted + '_links' + ] + create_req = paramgen.test_instantiate_cnf_resources_create( + vnfd_id) + resp, body = self.create_vnf_instance(create_req) + self.assertEqual(expected_status_code, resp.status_code) + if expected_status_code == 201: + self.check_resp_headers_in_create(resp) + self.check_resp_body(body, expected_inst_attrs) + inst_id = body['id'] + return inst_id + else: + return None + + def _sample_instantiate(self, auth_url, bearer_token, ssl_ca_cert=None, + namespace='default', area=None, vim_id=None): + vim_id_1 = uuidutils.generate_uuid() + vim_id_2 = uuidutils.generate_uuid() + vim_1 = { + "vimId": vim_id_1, + "vimType": "kubernetes", + "interfaceInfo": {"endpoint": auth_url}, + "accessInfo": { + "bearer_token": bearer_token, + }, + "extra": {"dummy-key": "dummy-val"} + } + vim_2 = { + "vimId": vim_id_2, + "vimType": "kubernetes", + "interfaceInfo": {"endpoint": auth_url}, + "accessInfo": { + "username": "dummy_user", + "password": "dummy_password", + }, + "extra": {"dummy-key": "dummy-val"} + } + + if ssl_ca_cert: + vim_1["interfaceInfo"]["ssl_ca_cert"] = ssl_ca_cert + vim_2["interfaceInfo"]["ssl_ca_cert"] = ssl_ca_cert + + if area: + vim_1.update({'extra': {'area': area}}) + vim_2.update({'extra': {'area': area}}) + + if vim_id: + vim_1 = { + "vimId": vim_id, + "vimType": "kubernetes" + } + vim_2 = { + "vimId": vim_id, + "vimType": "kubernetes" + } + + return { + "flavourId": "simple", + "vimConnectionInfo": { + "vim1": vim_1, + "vim2": vim_2 + }, + "additionalParams": { + "lcm-kubernetes-def-files": [ + "Files/kubernetes/namespace.yaml", + "Files/kubernetes/deployment.yaml", + ], + "namespace": namespace + } + } + + def _step_lcm_instantiate(self, username, inst_id, namespace, + expected_status_code, area=None, vim_id=None): + self.tacker_client = self.get_tk_http_client_by_user(username) + # Instantiate a VNF instance + instantiate_req = self._sample_instantiate( + self.auth_url, self.bearer_token, ssl_ca_cert=self.ssl_ca_cert, + namespace=namespace, area=area, vim_id=vim_id + ) + resp, body = self.instantiate_vnf_instance(inst_id, instantiate_req) + self.assertEqual(expected_status_code, resp.status_code) + if expected_status_code == 202: + self.check_resp_headers_in_operation_task(resp) + lcmocc_id = os.path.basename(resp.headers['Location']) + self.wait_lcmocc_complete(lcmocc_id) + + def _step_lcm_show(self, username, inst_id, expected_status_code): + self.tacker_client = self.get_tk_http_client_by_user(username) + resp, body = self.show_vnf_instance(inst_id) + self.assertEqual(expected_status_code, resp.status_code) + + def _step_lcm_list(self, username, expected_inst_list): + self.tacker_client = self.get_tk_http_client_by_user(username) + path = "/vnflcm/v2/vnf_instances" + resp, vnf_instances = self.tacker_client.do_request( + path, "GET", version="2.0.0") + self.assertEqual(200, resp.status_code) + inst_ids = set([inst.get('id') for inst in vnf_instances]) + for inst_id in expected_inst_list: + self.assertIn(inst_id, inst_ids) + + def _step_lcm_scale_out(self, username, inst_id, expected_status_code): + self.tacker_client = self.get_tk_http_client_by_user(username) + scale_out_req = { + "type": "SCALE_OUT", + "aspectId": "vdu2_aspect", + "numberOfSteps": 1 + } + resp, body = self.scale_vnf_instance(inst_id, scale_out_req) + self.assertEqual(expected_status_code, resp.status_code) + if expected_status_code == 202: + lcmocc_id = os.path.basename(resp.headers['Location']) + self.wait_lcmocc_complete(lcmocc_id) + + def change_ext_conn_max(self, net_ids, subnets, auth_url, area): + vim_id_1 = uuidutils.generate_uuid() + vim_id_2 = uuidutils.generate_uuid() + + ext_vl_1 = { + "id": uuidutils.generate_uuid(), + "vimConnectionId": vim_id_1, + "resourceProviderId": uuidutils.generate_uuid(), + "resourceId": net_ids['ft-net1'], + "extCps": [ + { + "cpdId": "VDU1_CP1", + "cpConfig": { + "VDU1_CP1": { + "cpProtocolData": [{ + "layerProtocol": "IP_OVER_ETHERNET", + "ipOverEthernet": { + # "macAddress": omitted, + # "segmentationId": omitted, + "ipAddresses": [{ + "type": "IPV4", + # "fixedAddresses": omitted, + "numDynamicAddresses": 1, + # "addressRange": omitted, + "subnetId": subnets[ + 'ft-ipv4-subnet1']}] + } + }]} + } + }, + { + "cpdId": "VDU2_CP2", + "cpConfig": { + "VDU2_CP2": { + "cpProtocolData": [{ + "layerProtocol": "IP_OVER_ETHERNET", + "ipOverEthernet": { + # "macAddress": omitted, + # "segmentationId": omitted, + "ipAddresses": [{ + "type": "IPV4", + "fixedAddresses": [ + "22.22.22.101" + ], + # "numDynamicAddresses": omitted + # "addressRange": omitted, + "subnetId": subnets['ft-ipv4-subnet1'] + }, { + "type": "IPV6", + # "fixedAddresses": omitted, + # "numDynamicAddresses": omitted, + "numDynamicAddresses": 1, + # "addressRange": omitted, + "subnetId": subnets['ft-ipv6-subnet1'] + }] + } + }] + }} + } + ] + } + vim_1 = { + "vimId": vim_id_1, + "vimType": "ETSINFV.OPENSTACK_KEYSTONE.V_3", + "interfaceInfo": {"endpoint": auth_url}, + "accessInfo": { + "username": "nfv_user", + "region": "RegionOne", + "password": "devstack", + "project": "nfv", + "projectDomain": "Default", + "userDomain": "Default" + }, + "extra": {"area": area} + } + vim_2 = { + "vimId": vim_id_2, + "vimType": "ETSINFV.OPENSTACK_KEYSTONE.V_3", + "interfaceInfo": {"endpoint": auth_url}, + "accessInfo": { + "username": "dummy_user", + "region": "RegionOne", + "password": "dummy_password", + "project": "dummy_project", + "projectDomain": "Default", + "userDomain": "Default" + }, + "extra": {"area": area} + } + + return { + "extVirtualLinks": [ + ext_vl_1 + ], + "vimConnectionInfo": { + "vim1": vim_1, + "vim2": vim_2 + }, + "additionalParams": {"dummy-key": "dummy-val"} + } + + def _step_lcm_change_vnfpkg(self, username, inst_id, new_vnfd_id, + expected_status_code): + self.tacker_client = self.get_tk_http_client_by_user(username) + change_vnfpkg_req = paramgen.change_vnfpkg(new_vnfd_id) + resp, body = self.change_vnfpkg(inst_id, change_vnfpkg_req) + self.assertEqual(expected_status_code, resp.status_code) + if expected_status_code == 202: + 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 change_vnfpkg completion. + time.sleep(3) + + def _step_lcm_update(self, username, inst_id, + expected_status_code): + self.tacker_client = self.get_tk_http_client_by_user(username) + update_req = { + "vnfInstanceName": "modify_{}".format(inst_id) + } + path = f"/vnflcm/v2/vnf_instances/{inst_id}" + resp, body = self.tacker_client.do_request( + path, "PATCH", body=update_req, version="2.0.0") + self.assertEqual(expected_status_code, resp.status_code) + if expected_status_code == 202: + lcmocc_id = os.path.basename(resp.headers['Location']) + self.wait_lcmocc_complete(lcmocc_id) + + def _step_lcm_scale_in(self, username, inst_id, expected_status_code): + self.tacker_client = self.get_tk_http_client_by_user(username) + scale_in_req = { + "type": "SCALE_IN", + "aspectId": "vdu2_aspect", + "numberOfSteps": 1 + } + resp, body = self.scale_vnf_instance(inst_id, scale_in_req) + self.assertEqual(expected_status_code, resp.status_code) + if expected_status_code == 202: + lcmocc_id = os.path.basename(resp.headers['Location']) + self.wait_lcmocc_complete(lcmocc_id) + + def _step_lcm_heal(self, username, inst_id, expected_status_code): + self.tacker_client = self.get_tk_http_client_by_user('user_admin') + resp, body = self.show_vnf_instance(inst_id) + self.assertEqual(200, resp.status_code) + self.check_resp_headers_in_get(resp) + + # check vnfc_resource_info + vnfc_infos = body['instantiatedVnfInfo']['vnfcInfo'] + vdu2_ids = [vnfc_info['id'] for vnfc_info in vnfc_infos + if vnfc_info['vduId'] == 'VDU2'] + target = [vdu2_ids[0]] + + heal_req = paramgen.max_sample_heal(target) + + self.tacker_client = self.get_tk_http_client_by_user(username) + + resp, body = self.heal_vnf_instance(inst_id, heal_req) + self.assertEqual(expected_status_code, resp.status_code) + if expected_status_code == 202: + lcmocc_id = os.path.basename(resp.headers['Location']) + self.wait_lcmocc_complete(lcmocc_id) + + def _step_lcm_terminate(self, username, inst_id, expected_status_code): + self.tacker_client = self.get_tk_http_client_by_user(username) + terminate_req = paramgen.max_sample_terminate() + resp, body = self.terminate_vnf_instance(inst_id, terminate_req) + self.assertEqual(expected_status_code, resp.status_code) + if expected_status_code == 202: + self.check_resp_headers_in_operation_task(resp) + + 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(3) + + def _step_lcm_delete(self, username, inst_id, expected_status_code): + self.tacker_client = self.get_tk_http_client_by_user(username) + # Delete a VNF instance + resp, body = self.delete_vnf_instance(inst_id) + self.assertEqual(expected_status_code, resp.status_code) + if expected_status_code == 204: + # check deletion of VNF instance + resp, body = self.show_vnf_instance(inst_id) + self.assertEqual(404, resp.status_code) + + def vnflcm_apis_v2_cnf_test_before_instantiate(self): + + # step 1 LCM-CreateV2, Resource Group A / User Group A + inst_id_a = self._step_lcm_create('user_a', self.vnfd_id_a, 201) + + # step 2 LCM-CreateV2, Resource Group B / User Group A + self._step_lcm_create('user_a', self.vnfd_id_b, 403) + + # step 3 LCM-CreateV2, Resource Group B / User Group all + inst_id_b = self._step_lcm_create('user_all', self.vnfd_id_b, 201) + + # step 4 LCM-ShowV2, Resource Group A / User Group A + self._step_lcm_show('user_a', inst_id_a, 200) + + # step 5 LCM-ShowV2, Resource Group A / User Group A-1 + self._step_lcm_show('user_a_1', inst_id_a, 200) + + # step 6 LCM-ShowV2, Resource Group B / User Group A + self._step_lcm_show('user_a', inst_id_b, 403) + + # step 7 LCM-ShowV2, Resource Group B / User Group all + self._step_lcm_show('user_all', inst_id_b, 200) + + # step 8 LCM-ListV2, Resource Group A / User Group A + self._step_lcm_list('user_a', [inst_id_a]) + + # step 9 LCM-ListV2, Resource Group - / User Group A-1 + self._step_lcm_list('user_a_1', [inst_id_a]) + + # step 10 LCM-ListV2, Resource Group - / User Group B + self._step_lcm_list('user_b', [inst_id_b]) + + # step 11 LCM-ListV2, Resource Group - / User Group all + self._step_lcm_list('user_all', [inst_id_a, inst_id_b]) + + return inst_id_a, inst_id_b + + def vnflcm_apis_v2_cnf_test_after_instantiate(self, inst_id_a, inst_id_b): + + # step 15 LCM-ShowV2, Resource Group A / User Group A + self._step_lcm_show('user_a', inst_id_a, 200) + + # step 16 LCM-Show, Resource Group A / User Group A-1 + self._step_lcm_show('user_a_1', inst_id_a, 403) + + # step 17 LCM-ShowV2, Resource Group B / User Group A + self._step_lcm_show('user_a', inst_id_b, 403) + + # step 18 LCM-ShowV2, Resource Group B / User Group all + self._step_lcm_show('user_all', inst_id_b, 200) + + # step 19 LCM-ListV2, Resource Group - / User Group A + self._step_lcm_list('user_a', [inst_id_a]) + + # step 20 LCM-ListV2, Resource Group - / User Group A-1 + self._step_lcm_list('user_a_1', []) + + # step 21 LCM-ListV2, Resource Group - / User Group B + self._step_lcm_list('user_b', [inst_id_b]) + + # step 22 LCM-ListV2, Resource Group - / User Group all + self._step_lcm_list('user_all', [inst_id_a, inst_id_b]) + + # step 23 LCM-ScaleV2(out), Resource Group A / User Group A + self._step_lcm_scale_out('user_a', inst_id_a, 202) + + # step 24 LCM-ScaleV2(out), Resource Group B / User Group A + self._step_lcm_scale_out('user_a', inst_id_b, 403) + + # step 25 LCM-ScaleV2(out), Resource Group B / User Group all + self._step_lcm_scale_out('user_all', inst_id_b, 202) + + # step 26 LCM-ScaleV2(in), Resource Group A / User Group A + self._step_lcm_scale_in('user_a', inst_id_a, 202) + + # step 27 LCM-ScaleV2(in), Resource Group B / User Group A + self._step_lcm_scale_in('user_a', inst_id_b, 403) + + # step 28 LCM-ScaleV2(in), Resource Group B / User Group all + self._step_lcm_scale_in('user_all', inst_id_b, 202) + + # step 29 LCM-HealV2, Resource Group A / User Group A + self._step_lcm_heal('user_a', inst_id_a, 202) + + # step 30 LCM-HealV2, Resource Group B / User Group A + self._step_lcm_heal('user_a', inst_id_b, 403) + + # step 31 LCM-HealV2, Resource Group B / User Group all + self._step_lcm_heal('user_all', inst_id_b, 202) + + # step 32 LCM-ModifyV2, Resource Group A / User Group A + self._step_lcm_update('user_a', inst_id_a, 202) + + # step 33 LCM-ModifyV2, Resource Group b / User Group A + self._step_lcm_update('user_a', inst_id_b, 403) + + # step 34 LCM-ModifyV2, Resource Group B / User Group all + self._step_lcm_update('user_all', inst_id_b, 202) + + # NOTE: CNF has no LCM-Change-Connectivity + # step 34 LCM-Change-ConnectivityV2, Resource Group A / User Group A + # step 35 LCM-Change-ConnectivityV2, Resource Group b / User Group A + # step 36 LCM-Change-ConnectivityV2, Resource Group B / User Group all + + # step 38 LCM-Change-VnfPkgV2, Resource Group A / User Group A + self._step_lcm_change_vnfpkg( + 'user_a', inst_id_a, self.vnfd_id_a_1, 202) + + # step 39 LCM-Change-VnfPkgV2, Resource Group B / User Group A + self._step_lcm_change_vnfpkg( + 'user_a', inst_id_b, self.vnfd_id_b_1, 403) + + # step 40 LCM-Change-VnfPkgV2, Resource Group B / User Group all + self._step_lcm_change_vnfpkg( + 'user_all', inst_id_b, self.vnfd_id_b_1, 202) + + # step 41 LCM-TerminateV2, Resource Group A / User Group A + self._step_lcm_terminate('user_a', inst_id_a, 202) + + # step 42 LCM-TerminateV2, Resource Group B / User Group A + self._step_lcm_terminate('user_a', inst_id_b, 403) + + # step 43 LCM-TerminateV2, Resource Group B / User Group all + self._step_lcm_terminate('user_all', inst_id_b, 202) + + # step 44 LCM-DeleteV2, Resource Group A / User Group A + self._step_lcm_delete('user_a', inst_id_a, 204) + + # step 45 LCM-DeleteV2, Resource Group B / User Group A + self._step_lcm_delete('user_a', inst_id_b, 403) + + # step 46 LCM-DeleteV2, Resource Group B / User Group all + self._step_lcm_delete('user_all', inst_id_b, 204) + + +class VnflcmAPIsV2VNFInstantiateWithArea(VnflcmAPIsV2CNFBase): + + def test_vnflcm_apis_v2_cnf_without_area_in_vim_conn_info(self): + + inst_id_a, inst_id_b = ( + self.vnflcm_apis_v2_cnf_test_before_instantiate()) + + # step 12 LCM-InstantiateV2, Resource Group A / User Group A + self._step_lcm_instantiate( + 'user_a', inst_id_a, 'namespace-a', 202, area='area_A@region_A') + + # step 13 LCM-InstantiateV2, Resource Group B / User Group A + self._step_lcm_instantiate( + 'user_a', inst_id_b, 'namespace-b', 403, area='area_B@region_B') + + # step 14 LCM-InstantiateV2, Resource Group B / User Group all + self._step_lcm_instantiate( + 'user_all', inst_id_b, 'namespace-b', 202, area='area_B@region_B') + + self.vnflcm_apis_v2_cnf_test_after_instantiate(inst_id_a, inst_id_b) + + +class VnflcmAPIsV2CNFInstantiateWithoutArea(VnflcmAPIsV2CNFBase): + + @classmethod + def setUpClass(cls): + super().setUpClass() + + vim_type = 'kubernetes' + + local_k8s_vim = 'local-k8s-vim.yaml' + + cls.vim_a = cls._step_vim_register( + 'user_a', vim_type, local_k8s_vim, 'vim_a', 'area_A@region_A') + + cls.vim_a_1 = cls._step_vim_register( + 'user_a', vim_type, local_k8s_vim, 'vim_a_1', 'area_A@region_A') + + cls.vim_b = cls._step_vim_register( + 'user_b', vim_type, local_k8s_vim, 'vim_b', 'area_B@region_B') + + cls.vim_b_1 = cls._step_vim_register( + 'user_b', vim_type, local_k8s_vim, 'vim_b_1', 'area_B@region_B') + + @classmethod + def tearDownClass(cls): + + cls._step_vim_delete('user_a', cls.vim_a) + cls._step_vim_delete('user_a', cls.vim_a_1) + cls._step_vim_delete('user_b', cls.vim_b) + cls._step_vim_delete('user_b', cls.vim_b_1) + + super().tearDownClass() + + def test_vnflcm_apis_v2_cnf_with_area_in_vim_conn_info(self): + inst_id_a, inst_id_b = ( + self.vnflcm_apis_v2_cnf_test_before_instantiate()) + + # step 12 LCM-InstantiateV2, Resource Group A / User Group A + self._step_lcm_instantiate( + 'user_a', inst_id_a, 'namespace-a', 202, vim_id=self.vim_a['id']) + + # step 13 LCM-InstantiateV2, Resource Group B / User Group A + self._step_lcm_instantiate( + 'user_a', inst_id_b, 'namespace-b', 403, vim_id=self.vim_b['id']) + + # step 14 LCM-InstantiateV2, Resource Group B / User Group all + self._step_lcm_instantiate( + 'user_all', inst_id_b, 'namespace-b', 202, vim_id=self.vim_b['id']) + + self.vnflcm_apis_v2_cnf_test_after_instantiate(inst_id_a, inst_id_b) + + +class VnflcmAPIsV2CNFInstanceWithoutArea(VnflcmAPIsV2CNFBase): + + @classmethod + def setUpClass(cls): + super().setUpClass() + + vim_type = 'kubernetes' + + local_k8s_vim = 'local-k8s-vim.yaml' + + cls.vim_c = cls._step_vim_register( + 'user_c', vim_type, local_k8s_vim, 'vim_c', None) + + cls.vim_c_1 = cls._step_vim_register( + 'user_c', vim_type, local_k8s_vim, 'vim_c_1', None) + + @classmethod + def tearDownClass(cls): + + cls._step_vim_delete('user_admin', cls.vim_c) + cls._step_vim_delete('user_admin', cls.vim_c_1) + + super().tearDownClass() + + def test_vnflcm_apis_v2_cnf_instance_without_area(self): + + # step 1 LCM-CreateV2, Resource Group C / User Group C + inst_id_c = self._step_lcm_create('user_c', self.vnfd_id_c, 201) + + # step 2 LCM-ShowV2, Resource Group C / User Group C + self._step_lcm_show('user_c', inst_id_c, 200) + + # step 3 LCM-ShowV2, Resource Group C / User Group all + self._step_lcm_show('user_all', inst_id_c, 200) + + # step 4 LCM-ShowV2, Resource Group C / User Group admin + self._step_lcm_show('user_admin', inst_id_c, 200) + + # step 5 LCM-ListV2, Resource Group - / User Group C + self._step_lcm_list('user_c', [inst_id_c]) + + # step 6 LCM-ListV2, Resource Group - / User Group all + self._step_lcm_list('user_all', [inst_id_c]) + + # step 7 LCM-ListV2, Resource Group - / User Group admin + self._step_lcm_list('user_admin', [inst_id_c]) + + # step 8 LCM-InstantiateV2, Resource Group C / User Group C + self._step_lcm_instantiate('user_c', inst_id_c, 'namespace-c', 202, + vim_id=self.vim_c['id']) + + # step 9 LCM-ShowV2, Resource Group C / User Group C + self._step_lcm_show('user_c', inst_id_c, 403) + + # step 10 LCM-ShowV2, Resource Group C / User Group all + self._step_lcm_show('user_all', inst_id_c, 403) + + # step 11 LCM-ShowV2, Resource Group C / User Group admin + self._step_lcm_show('user_admin', inst_id_c, 200) + + # step 12 LCM-ListV2, Resource Group - / User Group C + self._step_lcm_list('user_c', []) + + # step 13 LCM-ListV2, Resource Group - / User Group all + self._step_lcm_list('user_all', []) + + # step 14 LCM-ListV2, Resource Group - / User Group admin + self._step_lcm_list('user_admin', [inst_id_c]) + + # step 15 LCM-ScaleV2(out), Resource Group C / User Group C + self._step_lcm_scale_out('user_c', inst_id_c, 403) + + # step 16 LCM-ScaleV2(out), Resource Group C / User Group all + self._step_lcm_scale_out('user_all', inst_id_c, 403) + + # step 17 LCM-ScaleV2(out), Resource Group C / User Group admin + self._step_lcm_scale_out('user_admin', inst_id_c, 202) + + # step 18 LCM-ScaleV2(in), Resource Group C / User Group C + self._step_lcm_scale_in('user_c', inst_id_c, 403) + + # step 19 LCM-ScaleV2(in), Resource Group C / User Group A + self._step_lcm_scale_in('user_all', inst_id_c, 403) + + # step 20 LCM-ScaleV2(in), Resource Group C / User Group all + self._step_lcm_scale_in('user_admin', inst_id_c, 202) + + # step 21 LCM-HealV2, Resource Group C / User Group C + self._step_lcm_heal('user_c', inst_id_c, 403) + + # step 22 LCM-HealV2, Resource Group C / User Group A + self._step_lcm_heal('user_all', inst_id_c, 403) + + # step 23 LCM-HealV2, Resource Group C / User Group all + self._step_lcm_heal('user_admin', inst_id_c, 202) + + # step 24 LCM-ModifyV2, Resource Group C / User Group C + self._step_lcm_update('user_c', inst_id_c, 403) + + # step 25 LCM-ModifyV2, Resource Group C / User Group A + self._step_lcm_update('user_all', inst_id_c, 403) + + # step 26 LCM-ModifyV2, Resource Group C / User Group all + self._step_lcm_update('user_admin', inst_id_c, 202) + + # NOTE: CNF has no LCM-Change-Connectivity + # step 27 LCM-Change-ConnectivityV2, Resource Group C / User Group C + # step 28 LCM-Change-ConnectivityV2, Resource Group C / User Group all + # step 29 LCM-Change-ConnectivityV2, + # Resource Group C / User Group admin + + # step 30 LCM-Change-VnfPkgV2, Resource Group C / User Group C + self._step_lcm_change_vnfpkg( + 'user_c', inst_id_c, self.vnfd_id_c_1, 403) + + # step 31 LCM-Change-VnfPkgV2, Resource Group C / User Group A + self._step_lcm_change_vnfpkg( + 'user_all', inst_id_c, self.vnfd_id_c_1, 403) + + # step 32 LCM-Change-VnfPkgV2, Resource Group C / User Group all + self._step_lcm_change_vnfpkg( + 'user_admin', inst_id_c, self.vnfd_id_c_1, 202) + + # step 33 LCM-TerminateV2, Resource Group C / User Group C + self._step_lcm_terminate('user_c', inst_id_c, 403) + + # step 34 LCM-TerminateV2, Resource Group C / User Group A + self._step_lcm_terminate('user_all', inst_id_c, 403) + + # step 35 LCM-TerminateV2, Resource Group C / User Group all + self._step_lcm_terminate('user_admin', inst_id_c, 202) + + # step 36 LCM-DeleteV2, Resource Group C / User Group C + self._step_lcm_delete('user_c', inst_id_c, 204) diff --git a/tacker/tests/functional/sol_enhanced_policy/sol_kubernetes/test_policy_vim_apis_kubernetes.py b/tacker/tests/functional/sol_enhanced_policy/sol_kubernetes/test_policy_vim_apis_kubernetes.py new file mode 100644 index 000000000..dc14b04c7 --- /dev/null +++ b/tacker/tests/functional/sol_enhanced_policy/sol_kubernetes/test_policy_vim_apis_kubernetes.py @@ -0,0 +1,25 @@ +# +# 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.tests.functional.sol_enhanced_policy.base import ( + VimAPIsTest) + + +class VimAPIsOpenstackTest(VimAPIsTest): + + def test_vim_apis_vim_with_area_kubernetes(self): + self._test_vim_apis_enhanced_policy('kubernetes', 'local-k8s-vim.yaml') + + def test_vim_apis_vim_without_area_kubernetes(self): + self._test_vim_apis_vim_without_area_attribute( + 'kubernetes', 'local-k8s-vim.yaml') diff --git a/tacker/tests/functional/sol_kubernetes_v2/base_v2.py b/tacker/tests/functional/sol_kubernetes_v2/base_v2.py index 8f83b2d24..881acc106 100644 --- a/tacker/tests/functional/sol_kubernetes_v2/base_v2.py +++ b/tacker/tests/functional/sol_kubernetes_v2/base_v2.py @@ -136,11 +136,13 @@ class BaseVnfLcmKubernetesV2Test(base.BaseTestCase): return None @classmethod - def create_vnf_package(cls, sample_path, user_data={}, image_path=None): + def create_vnf_package(cls, sample_path, user_data={}, image_path=None, + provider=None, namespace=None): vnfd_id = uuidutils.generate_uuid() tmp_dir = tempfile.mkdtemp() - utils.make_zip(sample_path, tmp_dir, vnfd_id, image_path) + utils.make_zip(sample_path, tmp_dir, vnfd_id, image_path, + provider=provider, namespace=namespace) zip_file_name = os.path.basename(os.path.abspath(sample_path)) + ".zip" zip_file_path = os.path.join(tmp_dir, zip_file_name) diff --git a/tacker/tests/functional/sol_v2_common/base_v2.py b/tacker/tests/functional/sol_v2_common/base_v2.py index 08c387c7d..fdc9c409f 100644 --- a/tacker/tests/functional/sol_v2_common/base_v2.py +++ b/tacker/tests/functional/sol_v2_common/base_v2.py @@ -109,12 +109,13 @@ class BaseSolV2Test(base.BaseTestCase): @classmethod def create_vnf_package(cls, sample_path, user_data={}, - image_path=None, nfvo=False, userdata_path=None): + image_path=None, nfvo=False, userdata_path=None, + provider=None): vnfd_id = uuidutils.generate_uuid() tmp_dir = tempfile.mkdtemp() utils.make_zip(sample_path, tmp_dir, vnfd_id, image_path, - userdata_path) + userdata_path, provider) zip_file_name = os.path.basename(os.path.abspath(sample_path)) + ".zip" zip_file_path = os.path.join(tmp_dir, zip_file_name) diff --git a/tacker/tests/functional/sol_v2_common/utils.py b/tacker/tests/functional/sol_v2_common/utils.py index 2d5e602bb..03a524e5f 100644 --- a/tacker/tests/functional/sol_v2_common/utils.py +++ b/tacker/tests/functional/sol_v2_common/utils.py @@ -13,10 +13,13 @@ # License for the specific language governing permissions and limitations # under the License. +import hashlib import json import os +import re import shutil import subprocess +import yaml # vnfdId of samples must be this. @@ -24,7 +27,7 @@ SAMPLE_VNFD_ID = "b1bb0ce7-ebca-4fa7-95ed-4840d7000000" def make_zip(sample_dir, tmp_dir, vnfd_id, image_path=None, - userdata_path=None): + userdata_path=None, provider=None, namespace=None): # NOTE: '.zip' will be added by shutil.make_archive zip_file_name = os.path.basename(os.path.abspath(sample_dir)) zip_file_path = os.path.join(tmp_dir, zip_file_name) @@ -60,9 +63,83 @@ def make_zip(sample_dir, tmp_dir, vnfd_id, image_path=None, os.makedirs(file_path) shutil.copy(userdata_path, file_path) + if provider: + # replace provider + def_path = os.path.join(tmp_contents, "Definitions") + for entry in os.listdir(def_path): + entry_path = os.path.join(def_path, entry) + with open(entry_path, 'r') as f: + data = yaml.safe_load(f) + _update_provider_in_yaml(data, provider) + with open(entry_path, 'w') as f: + yaml.dump(data, f, default_flow_style=False, + allow_unicode=True) + if namespace: + file_path = os.path.join( + tmp_contents, "Files", "kubernetes", "namespace.yaml") + with open(file_path, 'r') as f: + data = yaml.safe_load(f) + data["metadata"]["name"] = namespace + with open(file_path, 'w') as f: + yaml.dump(data, f, default_flow_style=False, + allow_unicode=True) + with open(file_path, 'r') as f: + content = f.read() + hash_value = hashlib.sha256(content.encode()).hexdigest() + tosca_file = os.path.join(tmp_contents, "TOSCA-Metadata", "TOSCA.meta") + with open(tosca_file, 'rb') as f: + artifacts_data = f.read() + artifacts_data_split = re.split(b'\n\n+', artifacts_data) + artifact_data_strs = [] + for data in artifacts_data_split: + artifact_data_dict = yaml.safe_load(data) + if re.findall(b'.?Algorithm:.?|.?Hash:.?', data): + artifact_file = (artifact_data_dict['Source'] + if 'Source' in artifact_data_dict.keys() + else artifact_data_dict['Name']) + if artifact_file.endswith('namespace.yaml'): + artifact_data_dict['Hash'] = hash_value + if artifact_data_dict: + artifact_data_strs.append( + yaml.dump( + artifact_data_dict, + default_flow_style=False, + allow_unicode=True)) + with open(tosca_file, 'w') as f: + f.write('\n'.join(artifact_data_strs)) + shutil.make_archive(zip_file_path, "zip", tmp_contents) +def _update_provider_in_yaml(data, provider): + try: + prop = data['topology_template']['node_templates']['VNF'][ + 'properties'] + if prop.get('provider', None): + prop['provider'] = provider + except KeyError: + # Let's check for 'node_types' + pass + + if not data.get('node_types', None): + return + + for ntype in data['node_types'].values(): + if ntype['derived_from'] != 'tosca.nodes.nfv.VNF': + continue + try: + desc_id = ntype['properties']['provider'] + if desc_id.get('constraints', None): + for constraint in desc_id.get('constraints'): + if constraint.get('valid_values', None): + constraint['valid_values'] = [provider] + if desc_id.get('default', None): + desc_id['default'] = provider + except KeyError: + # Let's check next node_type + pass + + def create_network(network): # assume OS_* environment variables are already set subprocess.run( diff --git a/tacker/tests/unit/sol_refactored/api/test_wsgi.py b/tacker/tests/unit/sol_refactored/api/test_wsgi.py index bab9aec95..4f9fd2bb6 100644 --- a/tacker/tests/unit/sol_refactored/api/test_wsgi.py +++ b/tacker/tests/unit/sol_refactored/api/test_wsgi.py @@ -13,13 +13,18 @@ # License for the specific language governing permissions and limitations # under the License. +import ddt +from oslo_config import cfg from unittest import mock +from tacker import context +from tacker.sol_refactored.api.policies.vnflcm_v2 import POLICY_NAME from tacker.sol_refactored.api import wsgi as sol_wsgi from tacker.sol_refactored.common import exceptions as sol_ex from tacker.tests.unit import base +@ddt.ddt class TestWsgi(base.TestCase): def test_response_too_big(self): @@ -46,3 +51,88 @@ class TestWsgi(base.TestCase): request.headers = {} self.assertRaises(sol_ex.APIVersionMissing, resource._check_api_version, request, 'action') + + @mock.patch.object(context.Context, 'can') + def test_enhanced_policy_action(self, mock_can): + cfg.CONF.set_override( + "enhanced_tacker_policy", True, group='oslo_policy') + resource = sol_wsgi.SolResource(sol_wsgi.SolAPIController(), + policy_name=POLICY_NAME) + request = mock.Mock() + request.context = context.Context() + request.headers = {} + enhanced_policy_actions = [ + 'create', + 'index', + 'show', + 'delete', + 'update', + 'instantiate', + 'terminate', + 'scale', + 'heal', + 'change_ext_conn', + 'change_vnfpkg' + ] + for action in enhanced_policy_actions: + resource._check_policy(request, action) + mock_can.assert_not_called() + + @ddt.data('api_versions', + 'subscription_create', + 'subscription_list', + 'subscription_show', + 'subscription_delete', + 'lcm_op_occ_list', + 'lcm_op_occ_show', + 'lcm_op_occ_retry', + 'lcm_op_occ_rollback', + 'lcm_op_occ_fail', + 'lcm_op_occ_delete') + @mock.patch.object(context.Context, 'can') + def test_not_enhanced_policy_action(self, action, mock_can): + cfg.CONF.set_override( + "enhanced_tacker_policy", True, group='oslo_policy') + resource = sol_wsgi.SolResource(sol_wsgi.SolAPIController(), + policy_name=POLICY_NAME) + request = mock.Mock() + request.context = context.Context() + request.headers = {} + + resource._check_policy(request, action) + mock_can.assert_called_once() + + @ddt.data('create', + 'index', + 'show', + 'delete', + 'update', + 'instantiate', + 'terminate', + 'scale', + 'heal', + 'change_ext_conn', + 'change_vnfpkg', + 'api_versions', + 'subscription_create', + 'subscription_list', + 'subscription_show', + 'subscription_delete', + 'lcm_op_occ_list', + 'lcm_op_occ_show', + 'lcm_op_occ_retry', + 'lcm_op_occ_rollback', + 'lcm_op_occ_fail', + 'lcm_op_occ_delete') + @mock.patch.object(context.Context, 'can') + def test_enhanced_policy_is_false(self, action, mock_can): + cfg.CONF.set_override( + "enhanced_tacker_policy", False, group='oslo_policy') + resource = sol_wsgi.SolResource(sol_wsgi.SolAPIController(), + policy_name=POLICY_NAME) + request = mock.Mock() + request.context = context.Context() + request.headers = {} + + resource._check_policy(request, action) + mock_can.assert_called_once() diff --git a/tacker/tests/unit/sol_refactored/controller/test_vnflcm_v2.py b/tacker/tests/unit/sol_refactored/controller/test_vnflcm_v2.py index 219ede1ef..b8a7828ab 100644 --- a/tacker/tests/unit/sol_refactored/controller/test_vnflcm_v2.py +++ b/tacker/tests/unit/sol_refactored/controller/test_vnflcm_v2.py @@ -12,14 +12,21 @@ # 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 datetime import datetime +import ddt +from http import client as http_client import requests from unittest import mock +from oslo_policy import policy as oslo_policy from oslo_utils import uuidutils +from tacker.common.exceptions import PolicyNotAuthorized from tacker import context +from tacker import policy from tacker.sol_refactored.api import api_version +from tacker.sol_refactored.api.policies.vnflcm_v2 import POLICY_NAME from tacker.sol_refactored.common import common_script_utils from tacker.sol_refactored.common import config from tacker.sol_refactored.common import exceptions as sol_ex @@ -36,6 +43,9 @@ from tacker.sol_refactored.objects.v2 import fields from tacker.tests.unit.db import base as db_base +objects.register_all() + + _change_ext_conn_req_example = { "extVirtualLinks": [ { @@ -112,6 +122,494 @@ SAMPLE_VNFD_ID = "b1bb0ce7-ebca-4fa7-95ed-4840d7000000" CONF = config.CONF +def get_test_data_policy_instantiate(): + rules = {POLICY_NAME.format('instantiate'): "vendor:%(vendor)s"} + test_data = [ + # 'expected_status_code': http_client.ACCEPTED + { + 'vnf_instance_updates': {'vnfProvider': 'provider_A'}, + 'rules': rules, + 'roles': ['VENDOR_provider_A'], + 'expected_status_code': http_client.ACCEPTED + }, + { + 'vnf_instance_updates': {'vnfProvider': 'provider_A'}, + 'rules': rules, + 'roles': ['VENDOR_all'], + 'expected_status_code': http_client.ACCEPTED + }, + # 'expected_status_code': http_client.FORBIDDEN + { + 'vnf_instance_updates': {'vnfProvider': 'provider_A'}, + 'rules': rules, + 'roles': [], + 'expected_status_code': http_client.FORBIDDEN + }, + { + 'vnf_instance_updates': {'vnfProvider': 'provider_A'}, + 'rules': rules, + 'roles': ['VENDOR_provider_B'], + 'expected_status_code': http_client.FORBIDDEN + }, + ] + return test_data + + +def get_test_data_policy_vnf_instantiated(action, success_status_code): + vim_connection_info_area_a_region_a = objects.VimConnectionInfo( + id='f8c35bd0-4d67-4436-9f11-14b8a84c92aa', + vimId='f8c35bd0-4d67-4436-9f11-14b8a84c92aa', + vimType='openstack', + access_info={"key1": 'value1', "key2": 'value2'}, + extra={'area': 'area_A@region_A'} + ) + vim_connection_info_without_area = objects.VimConnectionInfo( + id='f8c35bd0-4d67-4436-9f11-14b8a84c92aa', + vimId='f8c35bd0-4d67-4436-9f11-14b8a84c92aa', + vimType='openstack', + access_info={"key1": 'value1', "key2": 'value2'} + ) + vnf_instance_updates = { + 'vnfProvider': 'provider_A', + 'vimConnectionInfo': {'vim1': vim_connection_info_area_a_region_a} + } + vnf_instance_updates_without_area = { + 'vnfProvider': 'provider_A', + 'vimConnectionInfo': {'vim1': vim_connection_info_without_area} + } + rule_area_vendor_tenant = { + POLICY_NAME.format(action): + "area:%(area)s and vendor:%(vendor)s and " + "tenant:%(tenant)s" + } + rule_vendor = { + POLICY_NAME.format(action): "vendor:%(vendor)s" + } + test_data = [ + # 'expected_status_code': success_status_code + { + 'vnf_instance_updates': vnf_instance_updates, + 'tenant': 'namespace_A', + 'rules': rule_area_vendor_tenant, + 'roles': [ + 'AREA_area_A@region_A', + 'VENDOR_provider_A', + 'TENANT_namespace_A' + ], + 'expected_status_code': success_status_code + }, + { + 'vnf_instance_updates': vnf_instance_updates, + 'tenant': 'namespace_A', + 'rules': rule_area_vendor_tenant, + 'roles': [ + 'AREA_all@all', 'VENDOR_all', 'TENANT_all' + ], + 'expected_status_code': success_status_code + }, + { + 'vnf_instance_updates': vnf_instance_updates, + 'tenant': 'namespace_A', + 'rules': rule_area_vendor_tenant, + 'roles': [ + 'AREA_all@region_A', 'VENDOR_all', 'TENANT_all' + ], + 'expected_status_code': success_status_code + }, + { + 'vnf_instance_updates': vnf_instance_updates, + 'tenant': 'namespace_A', + 'rules': rule_vendor, + 'roles': [ + 'AREA_area_A@region_A', + 'VENDOR_provider_A', + 'TENANT_namespace_A' + ], + 'expected_status_code': success_status_code + }, + # 'expected_status_code': http_client.FORBIDDEN + { + 'vnf_instance_updates': vnf_instance_updates, + 'tenant': None, + 'rules': rule_area_vendor_tenant, + 'roles': [ + 'AREA_area_A@region_A', + 'VENDOR_provider_A', + 'TENANT_namespace_A' + ], + 'expected_status_code': http_client.FORBIDDEN + }, + { + 'vnf_instance_updates': vnf_instance_updates_without_area, + 'tenant': 'namespace_A', + 'rules': rule_area_vendor_tenant, + 'roles': [ + 'AREA_area_A@region_A', + 'VENDOR_provider_A', + 'TENANT_namespace_A' + ], + 'expected_status_code': http_client.FORBIDDEN + }, + { + 'vnf_instance_updates': + vnf_instance_updates_without_area, + 'tenant': None, + 'rules': rule_area_vendor_tenant, + 'roles': [ + 'AREA_area_A@region_A', + 'VENDOR_provider_A', + 'TENANT_namespace_A' + ], + 'expected_status_code': http_client.FORBIDDEN + }, + { + 'vnf_instance_updates': vnf_instance_updates, + 'tenant': 'namespace_A', + 'rules': rule_area_vendor_tenant, + 'roles': [ + 'AREA_area_B@region_A', + 'VENDOR_provider_A', + 'TENANT_namespace_A' + ], + 'expected_status_code': http_client.FORBIDDEN + }, + { + 'vnf_instance_updates': vnf_instance_updates, + 'tenant': 'namespace_A', + 'rules': rule_area_vendor_tenant, + 'roles': [ + 'AREA_area_A@region_A', + 'VENDOR_provider_B', + 'TENANT_namespace_A' + ], + 'expected_status_code': http_client.FORBIDDEN + }, + { + 'vnf_instance_updates': vnf_instance_updates, + 'tenant': 'namespace_A', + 'rules': rule_area_vendor_tenant, + 'roles': [ + 'AREA_area_A@region_A', + 'VENDOR_provider_A', + 'TENANT_namespace_B' + ], + 'expected_status_code': http_client.FORBIDDEN + }, + { + 'vnf_instance_updates': vnf_instance_updates, + 'tenant': 'namespace_A', + 'rules': rule_area_vendor_tenant, + 'roles': [ + 'VENDOR_provider_A', + 'TENANT_namespace_A' + ], + 'expected_status_code': http_client.FORBIDDEN + }, + { + 'vnf_instance_updates': vnf_instance_updates, + 'tenant': 'namespace_A', + 'rules': rule_area_vendor_tenant, + 'roles': [ + 'AREA_area_A@region_A', + 'TENANT_namespace_A' + ], + 'expected_status_code': http_client.FORBIDDEN + }, + { + 'vnf_instance_updates': vnf_instance_updates, + 'tenant': 'namespace_A', + 'rules': rule_area_vendor_tenant, + 'roles': [ + 'AREA_area_A@region_A', + 'VENDOR_provider_A', + ], + 'expected_status_code': http_client.FORBIDDEN + }, + { + 'vnf_instance_updates': vnf_instance_updates, + 'tenant': 'namespace_A', + 'rules': rule_area_vendor_tenant, + 'roles': [], + 'expected_status_code': http_client.FORBIDDEN + }, + { + 'vnf_instance_updates': vnf_instance_updates, + 'tenant': 'namespace_A', + 'rules': rule_area_vendor_tenant, + 'roles': [ + 'AREA_all@region_B', + 'VENDOR_provider_A', + 'TENANT_namespace_A' + ], + 'expected_status_code': http_client.FORBIDDEN + }, + { + 'vnf_instance_updates': vnf_instance_updates, + 'tenant': 'namespace_A', + 'rules': rule_vendor, + 'roles': [ + 'AREA_area_A@region_A', + 'VENDOR_provider_B', + 'TENANT_namespace_A' + ], + 'expected_status_code': http_client.FORBIDDEN + }, + { + 'vnf_instance_updates': vnf_instance_updates, + 'tenant': 'namespace_A', + 'rules': rule_vendor, + 'roles': [ + 'AREA_area_A@region_A', + 'TENANT_namespace_A' + ], + 'expected_status_code': http_client.FORBIDDEN + }, + { + 'vnf_instance_updates': + vnf_instance_updates_without_area, + 'tenant': None, + 'rules': rule_area_vendor_tenant, + 'roles': [ + 'AREA_area_B@region_A', + 'VENDOR_provider_B', + 'TENANT_namespace_A' + ], + 'expected_status_code': http_client.FORBIDDEN + }, + { + 'vnf_instance_updates': + vnf_instance_updates_without_area, + 'tenant': None, + 'rules': rule_area_vendor_tenant, + 'roles': [ + 'VENDOR_provider_B', + ], + 'expected_status_code': http_client.FORBIDDEN + }, + { + 'vnf_instance_updates': + vnf_instance_updates_without_area, + 'tenant': None, + 'rules': rule_vendor, + 'roles': [ + 'VENDOR_provider_B', + ], + 'expected_status_code': http_client.FORBIDDEN + } + ] + return test_data + + +def make_vnf_instance( + vim_type, instantiation_state, vendor, area=None, tenant=None): + vim_connection_info = objects.VimConnectionInfo( + id='f8c35bd0-4d67-4436-9f11-14b8a84c92aa', + vimId='f8c35bd0-4d67-4436-9f11-14b8a84c92aa', + vimType=vim_type, + access_info={"key1": 'value1', "key2": 'value2'}, + ) + + if area: + vim_connection_info.update({'extra': {'area': area}}) + + vnf_instance_updates = { + 'id': uuidutils.generate_uuid(), + 'vnfProvider': vendor, + 'vimConnectionInfo': {'vim1': vim_connection_info} + } + vnf_inst_fields = { + 'id': uuidutils.generate_uuid(), + 'vnfdId': uuidutils.generate_uuid(), + 'vnfProductName': 'product name', + 'vnfSoftwareVersion': 'software version', + 'vnfdVersion': 'vnfd version', + 'instantiationState': instantiation_state + } + + vnf_instance_updates.update(vnf_inst_fields) + + vnf_instance = objects.VnfInstanceV2(**vnf_instance_updates) + + if tenant: + vnf_instance.instantiatedVnfInfo = ( + objects.VnfInstanceV2_InstantiatedVnfInfo( + flavourId='fake_flavour_id', + vnfState='STARTED', + metadata={'namespace': tenant})) + vnf_inst_fields.update({'metadata': {'namespace': tenant}}) + + return vnf_instance + + +def get_test_data_policy_index(): + rule_area_vendor_tenant = { + POLICY_NAME.format('index'): + "area:%(area)s and vendor:%(vendor)s and " + "tenant:%(tenant)s" + } + + test_data = [] + + inst_1 = make_vnf_instance( + 'ETSINFV.OPENSTACK_KEYSTONE.V_3', 'INSTANTIATED', 'provider_A', + area='area_A@region_A') + # OK + test_data.append({ + 'vnf_instance_list': [inst_1], + 'rules': rule_area_vendor_tenant, + 'roles': ['AREA_area_A@region_A', 'VENDOR_provider_A'], + 'expected_vnf_inst_ids': [inst_1.id] + }) + test_data.append({ + 'vnf_instance_list': [inst_1], + 'rules': rule_area_vendor_tenant, + 'roles': ['AREA_all@all', 'VENDOR_all'], + 'expected_vnf_inst_ids': [inst_1.id] + }) + test_data.append({ + 'vnf_instance_list': [inst_1], + 'rules': rule_area_vendor_tenant, + 'roles': ['AREA_all@region_A', 'VENDOR_all'], + 'expected_vnf_inst_ids': [inst_1.id] + }) + # wrong region role + test_data.append({ + 'vnf_instance_list': [inst_1], + 'rules': rule_area_vendor_tenant, + 'roles': ['AREA_all@region_B', 'VENDOR_all'], + 'expected_vnf_inst_ids': [] + }) + # wrong area role + test_data.append({ + 'vnf_instance_list': [inst_1], + 'rules': rule_area_vendor_tenant, + 'roles': ['AREA_area_B@region_A', 'VENDOR_provider_A'], + 'expected_vnf_inst_ids': [] + }) + # wrong vendor role + test_data.append({ + 'vnf_instance_list': [inst_1], + 'rules': rule_area_vendor_tenant, + 'roles': ['AREA_area_A@region_A', 'VENDOR_provider_B'], + 'expected_vnf_inst_ids': [] + }) + # without area + inst_2 = make_vnf_instance( + 'ETSINFV.OPENSTACK_KEYSTONE.V_3', 'INSTANTIATED', 'provider_A') + test_data.append({ + 'vnf_instance_list': [inst_2], + 'rules': rule_area_vendor_tenant, + 'roles': ['AREA_area_A@region_A', 'VENDOR_provider_A'], + 'expected_vnf_inst_ids': [] + }) + # NOT_INSTANTIATED + inst_3 = make_vnf_instance( + 'ETSINFV.OPENSTACK_KEYSTONE.V_3', 'NOT_INSTANTIATED', 'provider_A') + test_data.append({ + 'vnf_instance_list': [inst_3], + 'rules': rule_area_vendor_tenant, + 'roles': ['VENDOR_provider_A'], + 'expected_vnf_inst_ids': [inst_3.id] + }) + + inst_4 = make_vnf_instance( + 'kubernetes', 'INSTANTIATED', 'provider_A', area='area_A@region_A', + tenant='namespace_A') + # OK + test_data.append({ + 'vnf_instance_list': [inst_4], + 'rules': rule_area_vendor_tenant, + 'roles': [ + 'AREA_area_A@region_A', + 'VENDOR_provider_A', + 'TENANT_namespace_A'], + 'expected_vnf_inst_ids': [inst_4.id] + }) + test_data.append({ + 'vnf_instance_list': [inst_4], + 'rules': rule_area_vendor_tenant, + 'roles': [ + 'AREA_all@region_A', + 'VENDOR_all', + 'TENANT_all'], + 'expected_vnf_inst_ids': [inst_4.id] + }) + test_data.append({ + 'vnf_instance_list': [inst_4], + 'rules': rule_area_vendor_tenant, + 'roles': [ + 'AREA_all@all', + 'VENDOR_all', + 'TENANT_all'], + 'expected_vnf_inst_ids': [inst_4.id] + }) + # wrong region role + test_data.append({ + 'vnf_instance_list': [inst_4], + 'rules': rule_area_vendor_tenant, + 'roles': [ + 'AREA_all@region_B', + 'VENDOR_all', + 'TENANT_all'], + 'expected_vnf_inst_ids': [] + }) + # wrong region role + test_data.append({ + 'vnf_instance_list': [inst_4], + 'rules': rule_area_vendor_tenant, + 'roles': [ + 'AREA_area_B@region_A', + 'VENDOR_all', + 'TENANT_all'], + 'expected_vnf_inst_ids': [] + }) + # wrong vendor role + test_data.append({ + 'vnf_instance_list': [inst_4], + 'rules': rule_area_vendor_tenant, + 'roles': [ + 'AREA_area_A@region_A', + 'VENDOR_provider_B', + 'TENANT_all'], + 'expected_vnf_inst_ids': [] + }) + # wrong namespace + test_data.append({ + 'vnf_instance_list': [inst_4], + 'rules': rule_area_vendor_tenant, + 'roles': [ + 'AREA_all@all', + 'VENDOR_all', + 'TENANT_namespace_b'], + 'expected_vnf_inst_ids': [] + }) + # without namespace + inst_5 = make_vnf_instance( + 'kubernetes', 'INSTANTIATED', 'provider_A', area='area_A@region_A') + test_data.append({ + 'vnf_instance_list': [inst_5], + 'rules': rule_area_vendor_tenant, + 'roles': [ + 'AREA_area_A@region_A', + 'VENDOR_provider_A', + 'TENANT_all'], + 'expected_vnf_inst_ids': [] + }) + inst_6 = make_vnf_instance( + 'kubernetes', 'NOT_INSTANTIATED', 'provider_A') + # OK + test_data.append({ + 'vnf_instance_list': [inst_6], + 'rules': rule_area_vendor_tenant, + 'roles': [ + 'VENDOR_provider_A', + ], + 'expected_vnf_inst_ids': [inst_6.id] + }) + + return test_data + + class TestVnflcmV2(db_base.SqlTestCase): def setUp(self): @@ -1311,3 +1809,501 @@ class TestVnflcmV2(db_base.SqlTestCase): self.assertRaises(sol_ex.SolValidationError, self.controller.heal, request=self.request, id=inst_id, body=body) + + +@ddt.ddt +class TestVnflcmV2EnhancedPolicy(TestVnflcmV2): + + def setUp(self): + super(TestVnflcmV2EnhancedPolicy, self).setUp() + CONF.set_override( + "enhanced_tacker_policy", True, group='oslo_policy') + + def _create_inst_and_lcmocc(self, inst_state, op_state, + vnf_inst_updates=None, + tenant=None): + inst, lcmocc = self._set_inst_and_lcmocc( + inst_state, op_state, vnf_inst_updates=vnf_inst_updates) + if tenant: + inst.instantiatedVnfInfo = ( + objects.VnfInstanceV2_InstantiatedVnfInfo( + flavourId='fake_flavour_id', + vnfState='STARTED', + metadata={'namespace': 'namespace_A'})) + inst.create(self.context) + lcmocc.create(self.context) + + return inst.id, lcmocc.id + + def _set_inst_and_lcmocc( + self, inst_state, op_state, vnf_inst_updates=None): + vnf_inst_fields = { + # required fields + 'id': uuidutils.generate_uuid(), + 'vnfdId': uuidutils.generate_uuid(), + 'vnfProvider': 'provider', + 'vnfProductName': 'product name', + 'vnfSoftwareVersion': 'software version', + 'vnfdVersion': 'vnfd version', + 'instantiationState': inst_state + } + if vnf_inst_updates: + vnf_inst_fields.update(vnf_inst_updates) + inst = objects.VnfInstanceV2(**vnf_inst_fields) + + req = {"flavourId": "simple"} # instantiate request + lcmocc = objects.VnfLcmOpOccV2( + # required fields + id=uuidutils.generate_uuid(), + operationState=op_state, + stateEnteredTime=datetime.utcnow(), + startTime=datetime.utcnow(), + vnfInstanceId=inst.id, + operation=fields.LcmOperationType.INSTANTIATE, + isAutomaticInvocation=False, + isCancelPending=False, + operationParams=req + ) + + return inst, lcmocc + + def _prepare_db_for_scale_param_check(self, scale_status, + max_scale_levels, vnf_inst_updates=None, tenant=None): + vnf_inst_fields = { + # required fields + 'id': uuidutils.generate_uuid(), + 'vnfdId': uuidutils.generate_uuid(), + 'vnfProvider': 'provider', + 'vnfProductName': 'product name', + 'vnfSoftwareVersion': 'software version', + 'vnfdVersion': 'vnfd version', + 'instantiationState': 'INSTANTIATED' + } + if vnf_inst_updates: + vnf_inst_fields.update(vnf_inst_updates) + inst = objects.VnfInstanceV2(**vnf_inst_fields) + instantiated_vnf_info = { + 'flavourId': 'small', + 'vnfState': 'STARTED', + 'scaleStatus': scale_status, + 'maxScaleLevels': max_scale_levels, + } + if tenant: + instantiated_vnf_info.update({'metadata': {'namespace': tenant}}) + inst.instantiatedVnfInfo = objects.VnfInstanceV2_InstantiatedVnfInfo( + **instantiated_vnf_info + ) + inst.create(self.context) + + return inst.id + + def _prepare_db_for_change_vnfpkg_param( + self, vnf_inst_updates=None, tenant=None): + vnf_inst_fields = { + # required fields + 'id': uuidutils.generate_uuid(), + 'vnfdId': uuidutils.generate_uuid(), + 'vnfProvider': 'provider', + 'vnfProductName': 'product name', + 'vnfSoftwareVersion': 'software version', + 'vnfdVersion': 'vnfd version', + 'instantiationState': 'INSTANTIATED' + } + if vnf_inst_updates: + vnf_inst_fields.update(vnf_inst_updates) + inst = objects.VnfInstanceV2(**vnf_inst_fields) + inst.instantiatedVnfInfo = objects.VnfInstanceV2_InstantiatedVnfInfo( + flavourId='small', + vnfState='STARTED', + vnfcResourceInfo=[ + objects.VnfcResourceInfoV2( + id="VDU1-vnfc_res_info_id_VDU1", + vduId="VDU1" + ) + ] + ) + if tenant: + inst.instantiatedVnfInfo.metadata = {'namespace': tenant} + if not vnf_inst_updates: + inst.vimConnectionInfo = { + "vim1": objects.VimConnectionInfo.from_dict( + _vim_connection_info_example)} + inst.create(self.context) + return inst.id + + def _fake_request(self, roles): + request = requests.Request() + ctx = context.Context('fake', 'fake', roles=roles) + ctx.api_version = api_version.APIVersion("2.0.0") + request.context = ctx + return request + + def _overwrite_policy(self, rules): + policy.set_rules(oslo_policy.Rules.from_dict(rules), overwrite=True) + + @mock.patch.object(inst_utils, 'get_inst_all') + def test_index(self, mock_inst): + request = requests.Request() + request.context = self.context + request.GET = {'filter': f'(eq,vnfdId,{SAMPLE_VNFD_ID})'} + mock_inst.return_value = [objects.VnfInstanceV2( + id='inst-1', vnfdId=SAMPLE_VNFD_ID, vnfProvider='Company', + instantiationState='NOT_INSTANTIATED')] + + result = self.controller.index(request) + self.assertEqual(200, result.status) + + # no filter + request.GET = {} + result = self.controller.index(request) + self.assertEqual(200, result.status) + + @mock.patch.object(inst_utils, 'get_inst') + def test_show(self, mock_inst): + request = requests.Request() + request.context = self.context + mock_inst.return_value = objects.VnfInstanceV2( + id='inst-1', vnfdId=SAMPLE_VNFD_ID, vnfProvider='Company', + instantiationState='NOT_INSTANTIATED') + result = self.controller.show(request, 'inst-1') + self.assertEqual(200, result.status) + + @ddt.data(*get_test_data_policy_instantiate()) + @ddt.unpack + @mock.patch.object(vim_utils, 'get_vim') + @mock.patch.object(conductor_rpc_v2.VnfLcmRpcApiV2, 'start_lcm_op') + def test_instantiate_enhanced_policy(self, mock_start, + mock_vim, vnf_instance_updates, rules, roles, + expected_status_code): + self._overwrite_policy(rules) + inst_id, _ = self._create_inst_and_lcmocc('NOT_INSTANTIATED', + fields.LcmOperationStateType.COMPLETED, + vnf_inst_updates=vnf_instance_updates) + body = { + "flavourId": "small", + "vimConnectionInfo": { + "vim1": { + "vimId": "vim_id_1", + "vimType": "ETSINFV.OPENSTACK_KEYSTONE.V_3" + } + } + } + mock_vim.return_value = objects.VimConnectionInfo.from_dict( + _vim_connection_info_example) + try: + result = self.controller.instantiate( + request=self._fake_request(roles), id=inst_id, body=body) + self.assertEqual(expected_status_code, result.status) + except PolicyNotAuthorized: + if expected_status_code != http_client.FORBIDDEN: + raise + + @mock.patch.object(vim_utils, 'get_vim') + @mock.patch.object(conductor_rpc_v2.VnfLcmRpcApiV2, 'start_lcm_op') + def test_instantiate_add_area(self, mock_start, mock_vim): + inst_id, _ = self._create_inst_and_lcmocc( + 'NOT_INSTANTIATED', fields.LcmOperationStateType.COMPLETED) + body = { + "flavourId": "small", + "vimConnectionInfo": { + "vim1": { + "vimId": "vim_id_1", + "vimType": "ETSINFV.OPENSTACK_KEYSTONE.V_3" + } + } + } + vim = copy.copy(_vim_connection_info_example) + vim.update({'extra': {'area': 'area_A@region_A'}}) + mock_vim.return_value = objects.VimConnectionInfo.from_dict(vim) + result = self.controller.instantiate( + request=self.request, id=inst_id, body=body) + self.assertEqual(202, result.status) + + @mock.patch.object(vim_utils, 'get_vim') + @mock.patch.object(conductor_rpc_v2.VnfLcmRpcApiV2, 'start_lcm_op') + def test_instantiate_with_area_in_vim_conn_info( + self, mock_start, mock_vim): + inst_id, _ = self._create_inst_and_lcmocc( + 'NOT_INSTANTIATED', fields.LcmOperationStateType.COMPLETED) + body = { + "flavourId": "small", + "vimConnectionInfo": { + "vim1": { + "vimId": "vim_id_1", + "vimType": "ETSINFV.OPENSTACK_KEYSTONE.V_3", + "interfaceInfo": {"endpoint": "http://127.0.0.1/identity"}, + "accessInfo": { + "username": "nfv_user", + "region": "RegionOne", + "password": "devstack", + "project": "nfv", + "projectDomain": "Default", + "userDomain": "Default" + }, + "extra": {"area": "area_A@region_A"} + } + } + } + vim = copy.copy(_vim_connection_info_example) + mock_vim.return_value = objects.VimConnectionInfo.from_dict(vim) + result = self.controller.instantiate( + request=self.request, id=inst_id, body=body) + self.assertEqual(202, result.status) + + @ddt.data(*get_test_data_policy_vnf_instantiated( + 'terminate', http_client.ACCEPTED)) + @ddt.unpack + @mock.patch.object(conductor_rpc_v2.VnfLcmRpcApiV2, 'start_lcm_op') + def test_terminate_enhanced_policy(self, mock_start, vnf_instance_updates, + tenant, rules, roles, expected_status_code): + self._overwrite_policy(rules) + inst_id, _ = self._create_inst_and_lcmocc( + 'INSTANTIATED', fields.LcmOperationStateType.COMPLETED, + vnf_inst_updates=vnf_instance_updates, + tenant=tenant) + body = {"terminationType": "FORCEFUL"} + try: + result = self.controller.terminate( + request=self._fake_request(roles), id=inst_id, + body=body) + self.assertEqual(202, result.status) + except PolicyNotAuthorized: + if expected_status_code != http_client.FORBIDDEN: + raise + + @ddt.data(*get_test_data_policy_vnf_instantiated( + 'heal', http_client.ACCEPTED)) + @ddt.unpack + @mock.patch.object(conductor_rpc_v2.VnfLcmRpcApiV2, 'start_lcm_op') + def test_heal_enhanced_policy(self, mock_start, vnf_instance_updates, + tenant, rules, roles, expected_status_code): + self._overwrite_policy(rules) + inst_id, _ = self._create_inst_and_lcmocc('INSTANTIATED', + fields.LcmOperationStateType.COMPLETED, + vnf_inst_updates=vnf_instance_updates, + tenant=tenant) + body = {"cause": "Healing VNF instance"} + try: + result = self.controller.heal( + request=self._fake_request(roles), id=inst_id, + body=body) + self.assertEqual(http_client.ACCEPTED, result.status) + except PolicyNotAuthorized: + if expected_status_code != http_client.FORBIDDEN: + raise + + @ddt.data(*get_test_data_policy_vnf_instantiated( + 'delete', http_client.NO_CONTENT)) + @ddt.unpack + @mock.patch.object(nfvo_client.NfvoClient, 'send_inst_delete_notification') + def test_delete_enhanced_policy(self, mock_delete, vnf_instance_updates, + tenant, rules, roles, expected_status_code): + self._overwrite_policy(rules) + inst_id, _ = self._create_inst_and_lcmocc('NOT_INSTANTIATED', + fields.LcmOperationStateType.COMPLETED, + vnf_inst_updates=vnf_instance_updates, + tenant=tenant) + try: + result = self.controller.delete( + self._fake_request(roles), id=inst_id) + self.assertEqual(http_client.NO_CONTENT, result.status) + except PolicyNotAuthorized: + if expected_status_code != http_client.FORBIDDEN: + raise + + @ddt.data(*get_test_data_policy_vnf_instantiated( + 'show', http_client.OK)) + @ddt.unpack + @mock.patch.object(inst_utils, 'get_inst') + def test_show_enhanced_policy(self, mock_inst, vnf_instance_updates, + tenant, rules, roles, expected_status_code): + self._overwrite_policy(rules) + mock_inst.return_value = objects.VnfInstanceV2( + id='inst-1', vnfdId=SAMPLE_VNFD_ID, + instantiationState='NOT_INSTANTIATED', **vnf_instance_updates) + try: + result = self.controller.show(self._fake_request(roles), 'inst-1') + self.assertEqual(200, result.status) + except PolicyNotAuthorized: + if expected_status_code != http_client.FORBIDDEN: + raise + + @ddt.data(*get_test_data_policy_vnf_instantiated( + 'scale', http_client.ACCEPTED)) + @ddt.unpack + @mock.patch.object(conductor_rpc_v2.VnfLcmRpcApiV2, 'start_lcm_op') + def test_scale_enhanced_policy(self, mock_start, vnf_instance_updates, + tenant, rules, roles, expected_status_code): + self._overwrite_policy(rules) + scale_status = [ + objects.ScaleInfoV2( + aspectId="aspect_1", + scaleLevel=1 + ) + ] + max_scale_levels = [ + objects.ScaleInfoV2( + aspectId="aspect_1", + scaleLevel=3 + ) + ] + inst_id = self._prepare_db_for_scale_param_check( + scale_status, max_scale_levels, + vnf_inst_updates=vnf_instance_updates, + tenant=tenant) + body = {"aspectId": "aspect_1", "type": "SCALE_OUT", + "numberOfSteps": 1} + + try: + result = self.controller.scale( + request=self._fake_request(roles), + id=inst_id, + body=body) + self.assertEqual(http_client.ACCEPTED, result.status) + except PolicyNotAuthorized: + if expected_status_code != http_client.FORBIDDEN: + raise + + @ddt.data(*get_test_data_policy_vnf_instantiated( + 'update', http_client.ACCEPTED)) + @ddt.unpack + @mock.patch.object(conductor_rpc_v2.VnfLcmRpcApiV2, 'modify_vnfinfo') + def test_update_enhanced_policy(self, mock_modify, vnf_instance_updates, + tenant, rules, roles, expected_status_code): + self._overwrite_policy(rules) + inst_fields = { + # required fields + 'id': uuidutils.generate_uuid(), + 'vnfdId': uuidutils.generate_uuid(), + 'vnfProvider': 'provider', + 'vnfProductName': 'product name', + 'vnfSoftwareVersion': 'software version', + 'vnfdVersion': 'vnfd version', + 'instantiationState': 'INSTANTIATED' + } + if vnf_instance_updates: + inst_fields.update(vnf_instance_updates) + inst = objects.VnfInstanceV2(**inst_fields) + inst.instantiatedVnfInfo = objects.VnfInstanceV2_InstantiatedVnfInfo( + flavourId='small', + vnfState='STARTED', + vnfcInfo=[ + objects.VnfcInfoV2( + id="VDU1-vnfc_res_info_id_VDU1", + vduId="VDU1", + vnfcResourceInfoId="vnfc_res_info_id_VDU1", + vnfcState="STARTED" + ) + ] + ) + if tenant: + inst.instantiatedVnfInfo.metadata = {'namespace': tenant} + # inst.instantiatedVnfInfo = objects.VnfInstanceV2_InstantiatedVnfInfo( + # flavourId='fake_flavour_id', + # vnfState='STARTED', + # metadata={'namespace': tenant} + # ) + inst.create(self.context) + + body = { + "vnfcInfoModifications": [ + { + "id": "VDU1-vnfc_res_info_id_VDU1", + "vnfcConfigurableProperties": {"key": "value"} + } + ] + } + try: + result = self.controller.update( + request=self._fake_request(roles), id=inst.id, body=body) + self.assertEqual(202, result.status) + except PolicyNotAuthorized: + if expected_status_code != http_client.FORBIDDEN: + raise + + @ddt.data(*get_test_data_policy_vnf_instantiated( + 'change_ext_conn', http_client.ACCEPTED)) + @ddt.unpack + @mock.patch.object(conductor_rpc_v2.VnfLcmRpcApiV2, 'start_lcm_op') + def test_change_ext_conn_enhanced_policy(self, mock_start, + vnf_instance_updates, tenant, rules, roles, + expected_status_code): + self._overwrite_policy(rules) + inst_id, _ = self._create_inst_and_lcmocc('INSTANTIATED', + fields.LcmOperationStateType.COMPLETED, + vnf_inst_updates=vnf_instance_updates, + tenant=tenant) + try: + result = self.controller.change_ext_conn( + request=self._fake_request(roles), id=inst_id, + body=_change_ext_conn_req_example) + self.assertEqual(202, result.status) + except PolicyNotAuthorized: + if expected_status_code != http_client.FORBIDDEN: + raise + + @ddt.data(*get_test_data_policy_vnf_instantiated( + 'change_vnfpkg', http_client.ACCEPTED)) + @ddt.unpack + @mock.patch.object(nfvo_client.NfvoClient, 'get_vnf_package_info_vnfd') + @mock.patch.object(conductor_rpc_v2.VnfLcmRpcApiV2, 'start_lcm_op') + def test_change_vnfpkg_pkg_enhanced_policy( + self, mock_start, mocked_get_vnf_package_info_vnfd, + vnf_instance_updates, tenant, rules, roles, + expected_status_code): + self._overwrite_policy(rules) + vnfd_id = uuidutils.generate_uuid() + inst_id = self._prepare_db_for_change_vnfpkg_param( + vnf_inst_updates=vnf_instance_updates, tenant=tenant) + body = { + "vnfdId": vnfd_id, + "additionalParams": { + "upgrade_type": "RollingUpdate", + "vdu_params": [{ + "vdu_id": "VDU1", + "new_vnfc_param": { + "username": "test", + "password": "test", + "cp_name": "VDU1_CP1" + }, + "old_vnfc_param": { + "username": "test", + "password": "test", + "cp_name": "VDU1_CP1" + } + }] + } + } + pkg_info = objects.VnfPkgInfoV2( + id=uuidutils.generate_uuid(), + vnfdId=vnfd_id, + vnfProvider="provider", + vnfProductName="product", + vnfSoftwareVersion="software version", + vnfdVersion="vnfd version", + operationalState="ENABLED" + ) + mocked_get_vnf_package_info_vnfd.return_value = pkg_info + try: + result = self.controller.change_vnfpkg( + request=self._fake_request(roles), id=inst_id, body=body) + self.assertEqual(202, result.status) + except PolicyNotAuthorized: + if expected_status_code != http_client.FORBIDDEN: + raise + + @ddt.data(*get_test_data_policy_index()) + @ddt.unpack + @mock.patch.object(inst_utils, 'get_inst_all') + def test_index_enhanced_policy(self, mock_inst, vnf_instance_list, + rules, roles, expected_vnf_inst_ids): + self._overwrite_policy(rules) + mock_inst.return_value = vnf_instance_list + request = self._fake_request(roles) + request.GET = {} + + result = self.controller.index(request) + self.assertEqual(200, result.status) + result = result.body + self.assertEqual( + expected_vnf_inst_ids, [inst.get('id') for inst in result]) diff --git a/tacker/tests/unit/vnflcm/fakes.py b/tacker/tests/unit/vnflcm/fakes.py index 384c35853..95245fbb7 100644 --- a/tacker/tests/unit/vnflcm/fakes.py +++ b/tacker/tests/unit/vnflcm/fakes.py @@ -15,6 +15,7 @@ from copy import deepcopy import datetime +from http import client as http_client import iso8601 import json import os @@ -32,6 +33,7 @@ from tacker.objects.instantiate_vnf_req import ExtVirtualLinkData from tacker.objects.instantiate_vnf_req import InstantiateVnfRequest from tacker.objects import scale_vnf_request from tacker.objects.vim_connection import VimConnectionInfo +from tacker.policies import vnf_lcm as vnf_lcm_policies from tacker.tests import constants from tacker.tests import uuidsentinel from tacker import wsgi @@ -106,9 +108,9 @@ def fake_vnf_package_vnfd_model_dict(**updates): return vnfd -def return_vnf_package_vnfd(): +def return_vnf_package_vnfd(**updates): model_obj = models.VnfPackageVnfd() - model_obj.update(fake_vnf_package_vnfd_model_dict()) + model_obj.update(fake_vnf_package_vnfd_model_dict(**updates)) return model_obj @@ -1904,3 +1906,510 @@ def fake_subscription_response(**updates): data = _subscription_links(data) return data + + +def get_test_data_policy_create(): + rules = {vnf_lcm_policies.VNFLCM % 'create': "vendor:%(vendor)s"} + test_data = [ + # 'expected_status_code': http_client.OK + { + 'vnfd_updates': {'vnf_provider': 'provider_A'}, + 'rules': rules, + 'roles': ['VENDOR_provider_A'], + 'expected_status_code': http_client.CREATED + }, + { + 'vnfd_updates': {'vnf_provider': 'provider_A'}, + 'rules': rules, + 'roles': ['VENDOR_all'], + 'expected_status_code': http_client.CREATED + }, + { + 'vnfd_updates': {'vnf_provider': 'provider_B'}, + 'rules': rules, + 'roles': ['VENDOR_provider_B'], + 'expected_status_code': http_client.CREATED + }, + { + 'vnfd_updates': {'vnf_provider': 'provider_B'}, + 'rules': rules, + 'roles': ['VENDOR_all'], + 'expected_status_code': http_client.CREATED + }, + { + 'vnfd_updates': {'vnf_provider': 'provider_B'}, + 'rules': {vnf_lcm_policies.VNFLCM % 'create': "@"}, + 'roles': [], + 'expected_status_code': http_client.CREATED + }, + # 'expected_status_code': http_client.FORBIDDEN + { + 'vnfd_updates': {'vnf_provider': ''}, + 'rules': rules, + 'roles': ['VENDOR_'], + 'expected_status_code': http_client.FORBIDDEN + }, + { + 'vnfd_updates': {'vnf_provider': 'provider_A'}, + 'rules': rules, + 'roles': ['VENDOR_B'], + 'expected_status_code': http_client.FORBIDDEN + }, + { + 'vnfd_updates': {'vnf_provider': 'provider_B'}, + 'rules': rules, + 'roles': ['VENDOR_A'], + 'expected_status_code': http_client.FORBIDDEN + }, + { + 'vnfd_updates': {'vnf_provider': 'provider_A'}, + 'rules': rules, + 'roles': [], + 'expected_status_code': http_client.FORBIDDEN + }, + ] + return test_data + + +def make_vnf_instance(vim_type, vendor, area=None, tenant=None, + instantiated_state=fields.VnfInstanceState.NOT_INSTANTIATED): + vnf_instance_updates = make_vnf_instance_updates( + vim_type, vendor, area=area, tenant=tenant) + return return_vnf_instance(instantiated_state, **vnf_instance_updates) + + +def get_test_data_policy_index(): + rule_area_vendor_tenant = { + vnf_lcm_policies.VNFLCM % 'index': + "area:%(area)s and vendor:%(vendor)s and " + "tenant:%(tenant)s" + } + test_data = [] + inst_1 = make_vnf_instance('openstack', 'provider_A', + area='area_A@region_A') + test_data.append({ + 'vnf_instance_list': [inst_1], + 'rules': rule_area_vendor_tenant, + 'roles': ['AREA_area_A@region_A', 'VENDOR_provider_A'], + 'expected_vnf_inst_ids': [inst_1.id] + }) + test_data.append({ + 'vnf_instance_list': [inst_1], + 'rules': rule_area_vendor_tenant, + 'roles': ['AREA_all@region_A', 'VENDOR_provider_A'], + 'expected_vnf_inst_ids': [inst_1.id] + }) + test_data.append({ + 'vnf_instance_list': [inst_1], + 'rules': rule_area_vendor_tenant, + 'roles': ['AREA_all@all', 'VENDOR_all'], + 'expected_vnf_inst_ids': [inst_1.id] + }) + inst_2 = make_vnf_instance( + 'openstack', 'provider_A', area='area_A@region_A', + instantiated_state=fields.VnfInstanceState.INSTANTIATED) + test_data.append({ + 'vnf_instance_list': [inst_2], + 'rules': rule_area_vendor_tenant, + 'roles': ['AREA_area_A@region_A', 'VENDOR_provider_A'], + 'expected_vnf_inst_ids': [inst_2.id] + }) + test_data.append({ + 'vnf_instance_list': [inst_2], + 'rules': rule_area_vendor_tenant, + 'roles': ['AREA_all@region_A', 'VENDOR_provider_A'], + 'expected_vnf_inst_ids': [inst_2.id] + }) + test_data.append({ + 'vnf_instance_list': [inst_2], + 'rules': rule_area_vendor_tenant, + 'roles': ['AREA_all@all', 'VENDOR_all'], + 'expected_vnf_inst_ids': [inst_2.id] + }) + inst_3 = make_vnf_instance( + 'openstack', 'provider_A', + instantiated_state=fields.VnfInstanceState.INSTANTIATED) + test_data.append({ + 'vnf_instance_list': [inst_3], + 'rules': rule_area_vendor_tenant, + 'roles': ['AREA_area_A@region_A', 'VENDOR_provider_A'], + 'expected_vnf_inst_ids': [] + }) + test_data.append({ + 'vnf_instance_list': [inst_3], + 'rules': rule_area_vendor_tenant, + 'roles': ['AREA_all@region_A', 'VENDOR_provider_A'], + 'expected_vnf_inst_ids': [] + }) + test_data.append({ + 'vnf_instance_list': [inst_3], + 'rules': rule_area_vendor_tenant, + 'roles': ['AREA_all@all', 'VENDOR_all'], + 'expected_vnf_inst_ids': [] + }) + inst_4 = make_vnf_instance( + 'kubernetes', 'provider_A', + tenant='namespace_A', + area='area_A@region_A', + instantiated_state=fields.VnfInstanceState.INSTANTIATED) + test_data.append({ + 'vnf_instance_list': [inst_4], + 'rules': rule_area_vendor_tenant, + 'roles': [ + 'AREA_area_A@region_A', + 'VENDOR_provider_A', + 'TENANT_namespace_A' + ], + 'expected_vnf_inst_ids': [inst_4.id] + }) + test_data.append({ + 'vnf_instance_list': [inst_4], + 'rules': rule_area_vendor_tenant, + 'roles': [ + 'AREA_all@region_A', + 'VENDOR_provider_A', + 'TENANT_namespace_A' + ], + 'expected_vnf_inst_ids': [inst_4.id] + }) + test_data.append({ + 'vnf_instance_list': [inst_4], + 'rules': rule_area_vendor_tenant, + 'roles': [ + 'AREA_all@all', + 'VENDOR_all', + 'TENANT_namespace_A' + ], + 'expected_vnf_inst_ids': [inst_4.id] + }) + + return test_data + + +def get_test_data_policy_instantiate(): + test_data = [ + # 'expected_status_code': http_client.ACCEPTED + { + 'vnf_instance_updates': {'vnf_provider': 'provider_A'}, + 'rules': { + "os_nfv_orchestration_api:vnf_instances:instantiate": + "vendor:%(vendor)s" + }, + 'roles': ['VENDOR_provider_A'], + 'expected_status_code': http_client.ACCEPTED + }, + { + 'vnf_instance_updates': {'vnf_provider': 'provider_A'}, + 'rules': { + "os_nfv_orchestration_api:vnf_instances:instantiate": + "vendor:%(vendor)s" + }, + 'roles': ['VENDOR_all'], + 'expected_status_code': http_client.ACCEPTED + }, + # 'expected_status_code': http_client.FORBIDDEN + { + 'vnf_instance_updates': {'vnf_provider': 'provider_A'}, + 'rules': { + "os_nfv_orchestration_api:vnf_instances:instantiate": + "vendor:%(vendor)s" + }, + 'roles': [], + 'expected_status_code': http_client.FORBIDDEN + }, + { + 'vnf_instance_updates': {'vnf_provider': 'provider_A'}, + 'rules': { + "os_nfv_orchestration_api:vnf_instances:instantiate": + "vendor:%(vendor)s" + }, + 'roles': ['VENDOR_provider_B'], + 'expected_status_code': http_client.FORBIDDEN + }, + ] + return test_data + + +def get_test_data_policy_vnf_not_instantiated( + action, success_status_code=http_client.OK): + # openstack + vim_connection_info = { + "id": 'f8c35bd0-4d67-4436-9f11-14b8a84c92aa', + "vim_id": 'f8c35bd0-4d67-4436-9f11-14b8a84c92aa', + "vim_type": 'openstack', + "access_info": {"key1": 'value1', "key2": 'value2'}, + 'extra': {'area': 'area_A@region_A'} + } + vim_connection_info_wrong_area = { + "id": 'f8c35bd0-4d67-4436-9f11-14b8a84c92aa', + "vim_id": 'f8c35bd0-4d67-4436-9f11-14b8a84c92aa', + "vim_type": 'openstack', + "access_info": {"key1": 'value1', "key2": 'value2'}, + 'extra': {'area': 'area_A'} + } + vnf_instance_updates = { + 'vnf_provider': 'provider_A', + 'vim_connection_info': [vim_connection_info] + } + vnf_instance_updates_with_wrong_area = { + 'vnf_provider': 'provider_A', + 'vim_connection_info': [vim_connection_info_wrong_area] + } + rule_area_vendor_tenant = { + vnf_lcm_policies.VNFLCM % action: + "area:%(area)s and vendor:%(vendor)s and " + "tenant:%(tenant)s" + } + rule_vendor = { + vnf_lcm_policies.VNFLCM % action: "vendor:%(vendor)s" + } + test_data = [ + # 'expected_status_code': http_client.OK + { + 'vnf_instance_updates': vnf_instance_updates, + 'rules': rule_area_vendor_tenant, + 'roles': ['AREA_area_A@region_A', 'VENDOR_provider_A'], + 'expected_status_code': success_status_code + }, + { + 'vnf_instance_updates': vnf_instance_updates, + 'rules': rule_area_vendor_tenant, + 'roles': ['AREA_all@all', 'VENDOR_all', 'TENANT_all'], + 'expected_status_code': success_status_code + }, + { + 'vnf_instance_updates': vnf_instance_updates, + 'rules': rule_area_vendor_tenant, + 'roles': ['AREA_all@region_A', 'VENDOR_all', 'TENANT_all'], + 'expected_status_code': success_status_code + }, + { + 'vnf_instance_updates': vnf_instance_updates, + 'rules': rule_area_vendor_tenant, + 'roles': [ + 'AREA_all@region_A', 'AREA_all@all', 'VENDOR_all', + 'TENANT_all'], + 'expected_status_code': success_status_code + }, + { + 'vnf_instance_updates': vnf_instance_updates, + 'rules': rule_vendor, + 'roles': ['AREA_area_A@region_A', 'VENDOR_provider_A'], + 'expected_status_code': success_status_code + }, + { + 'vnf_instance_updates': vnf_instance_updates, + 'rules': rule_vendor, + 'roles': ['AREA_all@all', 'VENDOR_all', 'TENANT_all'], + 'expected_status_code': success_status_code + }, + { + 'vnf_instance_updates': vnf_instance_updates, + 'rules': rule_vendor, + 'roles': ['AREA_all@region_A', 'VENDOR_all', 'TENANT_all'], + 'expected_status_code': success_status_code + }, + # 'expected_status_code': http_client.FORBIDDEN + { + 'vnf_instance_updates': vnf_instance_updates, + 'rules': rule_area_vendor_tenant, + 'roles': [], + 'expected_status_code': http_client.FORBIDDEN + }, + { + 'vnf_instance_updates': vnf_instance_updates, + 'rules': rule_area_vendor_tenant, + 'roles': ['AREA_area_A@region_A'], + 'expected_status_code': http_client.FORBIDDEN + }, + { + 'vnf_instance_updates': vnf_instance_updates, + 'rules': rule_area_vendor_tenant, + 'roles': ['VENDOR_provider_A'], + 'expected_status_code': http_client.FORBIDDEN + }, + { + 'vnf_instance_updates': vnf_instance_updates, + 'rules': rule_area_vendor_tenant, + 'roles': ['AREA_area_A@region_A', 'VENDOR_provider_B'], + 'expected_status_code': http_client.FORBIDDEN + }, + { + 'vnf_instance_updates': vnf_instance_updates, + 'rules': rule_area_vendor_tenant, + 'roles': ['AREA_area_B@region_A', 'VENDOR_provider_A'], + 'expected_status_code': http_client.FORBIDDEN + }, + { + 'vnf_instance_updates': vnf_instance_updates, + 'rules': rule_area_vendor_tenant, + 'roles': ['AREA_all@region_B', 'VENDOR_provider_A'], + 'expected_status_code': http_client.FORBIDDEN + }, + { + 'vnf_instance_updates': vnf_instance_updates, + 'rules': rule_area_vendor_tenant, + 'roles': ['AREA_all', 'VENDOR_provider_A'], + 'expected_status_code': http_client.FORBIDDEN + }, + { + 'vnf_instance_updates': vnf_instance_updates, + 'rules': rule_area_vendor_tenant, + 'roles': ['AREA_all@', 'VENDOR_provider_A'], + 'expected_status_code': http_client.FORBIDDEN + }, + { + 'vnf_instance_updates': vnf_instance_updates_with_wrong_area, + 'rules': rule_area_vendor_tenant, + 'roles': ['AREA_all@', 'VENDOR_provider_A'], + 'expected_status_code': http_client.FORBIDDEN + }, + ] + return test_data + + +def make_vnf_instance_updates( + vim_type, vendor, area=None, tenant=None): + vim_connection_info = VimConnectionInfo( + id='f8c35bd0-4d67-4436-9f11-14b8a84c92aa', + vim_id='f8c35bd0-4d67-4436-9f11-14b8a84c92aa', + vim_type=vim_type, + access_info={"key1": 'value1', "key2": 'value2'}, + ) + if area: + vim_connection_info.extra = {'area': area} + vnf_instance_updates = { + 'vnf_provider': vendor, + 'vim_connection_info': [vim_connection_info] + } + if tenant: + vnf_instance_updates.update({'vnf_metadata': {"namespace": tenant}}) + + return vnf_instance_updates + + +def get_test_data_policy_vnf_instantiated(action, success_status_code): + rule_area_vendor_tenant = { + vnf_lcm_policies.VNFLCM % action: + "area:%(area)s and vendor:%(vendor)s and " + "tenant:%(tenant)s" + } + + test_data = [] + # openstack + vnf_instance_updates_1 = make_vnf_instance_updates( + 'openstack', 'provider_A', area='area_A@region_A') + test_data.append({ + 'vnf_instance_updates': vnf_instance_updates_1, + 'rules': rule_area_vendor_tenant, + 'roles': [ + 'AREA_area_A@region_A', + 'VENDOR_provider_A' + ], + 'expected_status_code': success_status_code + }) + test_data.append({ + 'vnf_instance_updates': vnf_instance_updates_1, + 'rules': rule_area_vendor_tenant, + 'roles': [ + 'AREA_all@region_A', + 'VENDOR_provider_A' + ], + 'expected_status_code': success_status_code + }) + test_data.append({ + 'vnf_instance_updates': vnf_instance_updates_1, + 'rules': rule_area_vendor_tenant, + 'roles': [ + 'AREA_all@all', + 'VENDOR_provider_A' + ], + 'expected_status_code': success_status_code + }) + test_data.append({ + 'vnf_instance_updates': vnf_instance_updates_1, + 'rules': rule_area_vendor_tenant, + 'roles': [ + 'AREA_all@all', + 'VENDOR_all' + ], + 'expected_status_code': success_status_code + }) + test_data.append({ + 'vnf_instance_updates': vnf_instance_updates_1, + 'rules': rule_area_vendor_tenant, + 'roles': [ + 'AREA_all@all', + 'VENDOR_all' + ], + 'expected_status_code': success_status_code + }) + # wrong area role + test_data.append({ + 'vnf_instance_updates': vnf_instance_updates_1, + 'rules': rule_area_vendor_tenant, + 'roles': [ + 'AREA_area_B@region_A', + 'VENDOR_provider_A' + ], + 'expected_status_code': http_client.FORBIDDEN + }) + # wrong region role + test_data.append({ + 'vnf_instance_updates': vnf_instance_updates_1, + 'rules': rule_area_vendor_tenant, + 'roles': [ + 'AREA_area_A@region_B', + 'VENDOR_provider_A' + ], + 'expected_status_code': http_client.FORBIDDEN + }) + # wrong vendor role + test_data.append({ + 'vnf_instance_updates': vnf_instance_updates_1, + 'rules': rule_area_vendor_tenant, + 'roles': [ + 'AREA_area_A@region_A', + 'VENDOR_provider_B' + ], + 'expected_status_code': http_client.FORBIDDEN + }) + + vnf_instance_updates_2 = make_vnf_instance_updates( + 'openstack', 'provider_A') + test_data.append({ + 'vnf_instance_updates': vnf_instance_updates_2, + 'rules': rule_area_vendor_tenant, + 'roles': [ + 'AREA_area_A@region_A', + 'VENDOR_provider_A' + ], + 'expected_status_code': http_client.FORBIDDEN + }) + + # kubernetes + vnf_instance_updates_3 = make_vnf_instance_updates( + 'kubernetes', 'provider_A', area='area_A@region_A', + tenant='namespace_A') + test_data.append({ + 'vnf_instance_updates': vnf_instance_updates_3, + 'rules': rule_area_vendor_tenant, + 'roles': [ + 'AREA_area_A@region_A', + 'VENDOR_provider_A', + 'TENANT_namespace_A' + ], + 'expected_status_code': success_status_code + }) + test_data.append({ + 'vnf_instance_updates': vnf_instance_updates_3, + 'rules': rule_area_vendor_tenant, + 'roles': [ + 'AREA_all@all', + 'VENDOR_all', + 'TENANT_all' + ], + 'expected_status_code': success_status_code + }) + + return test_data diff --git a/tacker/tests/unit/vnflcm/test_controller.py b/tacker/tests/unit/vnflcm/test_controller.py index cf594693c..a5f9f4c2b 100644 --- a/tacker/tests/unit/vnflcm/test_controller.py +++ b/tacker/tests/unit/vnflcm/test_controller.py @@ -26,6 +26,7 @@ import webob from webob import exc from oslo_config import cfg +from oslo_policy import policy as oslo_policy from oslo_serialization import jsonutils from tacker.api.views import vnf_subscriptions as vnf_subscription_view @@ -42,6 +43,7 @@ from tacker.manager import TackerManager from tacker import objects from tacker.objects import fields from tacker.objects import vnf_lcm_subscriptions as subscription_obj +from tacker import policy from tacker.tests import constants from tacker.tests.unit import base from tacker.tests.unit.db import utils @@ -261,11 +263,13 @@ class TestController(base.TestCase): @mock.patch.object(TackerManager, 'get_service_plugins', return_value={'VNFM': test_nfvo_plugin.FakeVNFMPlugin()}) + @mock.patch.object(objects.vnf_instance, '_vnf_instance_update') @mock.patch.object(objects.vnf_instance, '_vnf_instance_create') @mock.patch.object(objects.vnf_package_vnfd.VnfPackageVnfd, 'get_by_id') def test_create_without_name_and_description( self, mock_get_by_id, mock_vnf_instance_create, + mock_vnf_instance_update, mock_get_service_plugins, mock_package_save, mock_private_create_vnf, @@ -285,6 +289,8 @@ class TestController(base.TestCase): mock_vnf_instance_create.return_value =\ fakes.return_vnf_instance_model(**updates) + mock_vnf_instance_update.return_value = \ + fakes.return_vnf_instance_model(**updates) body = {'vnfdId': uuidsentinel.vnfd_id, "vnfInstanceName": "SampleVnf", @@ -396,17 +402,20 @@ class TestController(base.TestCase): return_value={'VNFM': test_nfvo_plugin.FakeVNFMPlugin()}) @mock.patch.object(sync_resource.SyncVnfPackage, 'create_package') @mock.patch.object(nfvo_client.VnfPackageRequest, "index") + @mock.patch.object(objects.vnf_instance, '_vnf_instance_update') @mock.patch.object(objects.vnf_instance, '_vnf_instance_create') @mock.patch.object(objects.vnf_package_vnfd.VnfPackageVnfd, 'get_by_id') def test_create_vnf_package_not_found( self, mock_get_by_id_package_vnfd, mock_vnf_instance_create, + mock_vnf_instance_update, mock_index, mock_create_pkg, mock_get_service_plugins, mock_private_create_vnf, mock_vnf_package_get_by_id, mock_update_package_usage_state, mock_get_vim): + mock_get_vim.return_value = self.vim_info mock_get_by_id_package_vnfd.side_effect =\ exceptions.VnfPackageVnfdNotFound @@ -421,6 +430,8 @@ class TestController(base.TestCase): updates = {'vnfd_id': uuidsentinel.vnfd_id} mock_vnf_instance_create.return_value =\ fakes.return_vnf_instance_model(**updates) + mock_vnf_instance_update.return_value =\ + fakes.return_vnf_instance_model(**updates) body = {'vnfdId': uuidsentinel.vnfd_id} req = fake_request.HTTPRequest.blank('/vnf_instances') @@ -584,6 +595,9 @@ class TestController(base.TestCase): mock_get_vnf, mock_insta_notfi_process, mock_get_service_plugins): + vim = fakes.return_default_vim() + vim.update({'extra': {"area": "area_A@region_A"}}) + mock_get_vim.return_value = vim mock_vnf_instance_get_by_id.return_value =\ fakes.return_vnf_instance_model() mock_vnf_package_vnfd_get_by_id.return_value = \ @@ -595,7 +609,12 @@ class TestController(base.TestCase): vnf_id=mock_vnf_instance_get_by_id.return_value.id, status='INACTIVE') - body = {"flavourId": "simple"} + body = {"flavourId": "simple", + "vimConnectionInfo": [ + {"id": uuidsentinel.vim_connection_id, + "vimId": uuidsentinel.vim_id, + "vimType": 'openstack'} + ]} body.update({"additionalParams": {"foo_number": 12}}) req = fake_request.HTTPRequest.blank( '/vnf_instances/%s/instantiate' % uuidsentinel.vnf_instance_id) @@ -673,6 +692,9 @@ class TestController(base.TestCase): mock_get_vnf, mock_insta_notif_process, mock_get_service_plugins): + vim = fakes.return_default_vim() + vim.update({'extra': {"area": "area_A@region_A"}}) + mock_get_vim.return_value = vim mock_vnf_instance_get_by_id.return_value =\ fakes.return_vnf_instance_model() mock_vnf_package_vnfd_get_by_id.return_value = \ @@ -815,6 +837,9 @@ class TestController(base.TestCase): mock_get_vnf, mock_insta_notif_process, mock_get_service_plugins): + vim = fakes.return_default_vim() + vim.update({'extra': {"area": "area_A@region_A"}}) + mock_get_vim.return_value = vim mock_vnf_instance_get_by_id.return_value =\ fakes.return_vnf_instance_model() mock_vnf_package_vnfd_get_by_id.return_value = \ @@ -4814,3 +4839,790 @@ class TestController(base.TestCase): ""}} expected_msg = expected_vnf['tackerFault']['message'] self.assertEqual(expected_msg, resp.json['detail']) + + +@ddt.ddt +class TestControllerEnhancedPolicy(TestController): + + def setUp(self): + super(TestControllerEnhancedPolicy, self).setUp() + cfg.CONF.set_override( + "enhanced_tacker_policy", True, group='oslo_policy') + + @mock.patch.object(objects.VnfInstance, "get_by_id") + @mock.patch.object(TackerManager, 'get_service_plugins', + return_value={'VNFM': + test_nfvo_plugin.FakeVNFMPlugin()}) + @mock.patch.object(objects.VNF, "vnf_index_list") + @mock.patch.object(objects.VnfInstanceList, "vnf_instance_list") + @mock.patch.object(objects.VnfPackageVnfd, 'get_vnf_package_vnfd') + @mock.patch.object(vnf_lcm_rpc.VNFLcmRPCAPI, "update") + def test_update_vnf_with_pkg_id( + self, mock_update, + mock_vnf_package_vnf_get_vnf_package_vnfd, + mock_vnf_instance_list, mock_vnf_index_list, + mock_get_service_plugins, mock_vnf_by_id): + mock_vnf_by_id.return_value = fakes.return_vnf_instance() + mock_vnf_index_list.return_value = fakes._get_vnf() + mock_vnf_instance_list.return_value = fakes.return_vnf_instance( + fields.VnfInstanceState.INSTANTIATED) + mock_vnf_package_vnf_get_vnf_package_vnfd.return_value =\ + fakes.return_vnf_package_vnfd() + + body = {"vnfInstanceName": "new_instance_name", + "vnfInstanceDescription": "new_instance_discription", + "vnfPkgId": "2c69a161-0000-4b0f-bcf8-391f8fc76600", + "vnfConfigurableProperties": {"test": "test_value"}, + "vnfcInfoModificationsDeleteIds": ["test1"]} + req = fake_request.HTTPRequest.blank( + '/vnf_instances/%s' % constants.UUID) + req.body = jsonutils.dump_as_bytes(body) + req.headers['Content-Type'] = 'application/json' + req.method = 'PATCH' + + # Call Instantiate API + resp = req.get_response(self.app) + self.assertEqual(http_client.ACCEPTED, resp.status_code) + mock_update.assert_called_once() + + @mock.patch.object(objects.VnfInstance, "get_by_id") + @mock.patch.object(TackerManager, 'get_service_plugins', + return_value={'VNFM': + test_nfvo_plugin.FakeVNFMPlugin()}) + @mock.patch.object(objects.VNF, "vnf_index_list") + def test_update_vnf_status_err( + self, + mock_vnf_index_list, + mock_get_service_plugins, + mock_vnf_by_id): + mock_vnf_by_id.return_value = fakes.return_vnf_instance() + updates = {'status': 'ERROR'} + mock_vnf_index_list.return_value = fakes._get_vnf(**updates) + + body = {"vnfInstanceName": "new_instance_name", + "vnfInstanceDescription": "new_instance_discription", + "vnfdId": "2c69a161-0000-4b0f-bcf8-391f8fc76600", + "vnfConfigurableProperties": { + "test": "test_value" + }, + "vnfcInfoModificationsDeleteIds": ["test1"], + "metadata": {"testkey": "test_value"}, + "vimConnectionInfo": {"id": "testid"}} + req = fake_request.HTTPRequest.blank( + '/vnf_instances/%s' % constants.UUID) + req.body = jsonutils.dump_as_bytes(body) + + req.headers['Content-Type'] = 'application/json' + req.method = 'PATCH' + + msg = _("VNF %(id)s status is %(state)s") % { + "id": constants.UUID, "state": "ERROR"} + res = self._make_problem_detail(msg % + {"state": "ERROR"}, 409, 'Conflict') + + resp = req.get_response(self.app) + self.assertEqual(res.text, resp.text) + + @mock.patch.object(objects.VnfInstance, "get_by_id") + @mock.patch.object(TackerManager, 'get_service_plugins', + return_value={'VNFM': + test_nfvo_plugin.FakeVNFMPlugin()}) + @mock.patch.object(sync_resource.SyncVnfPackage, 'create_package') + @mock.patch.object(objects.vnf_package_vnfd.VnfPackageVnfd, + "get_vnf_package_vnfd") + @mock.patch.object(nfvo_client.VnfPackageRequest, "index") + @mock.patch.object(objects.VNF, "vnf_index_list") + @mock.patch.object(objects.VnfInstanceList, "vnf_instance_list") + @mock.patch.object(objects.VnfPackageVnfd, 'get_vnf_package_vnfd') + @mock.patch.object(vnf_lcm_rpc.VNFLcmRPCAPI, "update") + def test_update_vnf_none_vnfd( + self, + mock_update, + mock_vnf_package_vnf_get_vnf_package_vnfd, + mock_vnf_instance_list, + mock_vnf_index_list, + mock_index, + mock_get_vnf_package_vnfd, + mock_create_package, + mock_get_service_plugins, + mock_vnf_by_id): + + mock_vnf_by_id.return_value = fakes.return_vnf_instance() + mock_vnf_index_list.return_value = fakes._get_vnf() + mock_vnf_instance_list.return_value = fakes.return_vnf_instance( + fields.VnfInstanceState.INSTANTIATED) + mock_vnf_package_vnf_get_vnf_package_vnfd.return_value = "" + mock_get_vnf_package_vnfd.side_effect =\ + exceptions.VnfPackageVnfdNotFound + mock_create_package.return_value = fakes.return_vnf_package_vnfd() + mock_response = mock.MagicMock() + mock_response.ok = True + mock_response.json = mock.MagicMock() + mock_response.json.return_value = ['aaa', 'bbb', 'ccc'] + + mock_index.return_value = mock_response + + body = { + "vnfInstanceName": "new_instance_name", + "vnfInstanceDescription": "new_instance_discription", + "vnfPkgId": "2c69a161-0000-4b0f-bcf8-391f8fc76600", + "vnfConfigurableProperties": {"test": "test_value"}, + "vnfcInfoModificationsDeleteIds": ["test1"] + } + req = fake_request.HTTPRequest.blank( + '/vnf_instances/%s' % constants.UUID) + req.body = jsonutils.dump_as_bytes(body) + req.headers['Content-Type'] = 'application/json' + req.method = 'PATCH' + + resp = req.get_response(self.app) + self.assertEqual(http_client.ACCEPTED, resp.status_code) + mock_update.assert_called_once() + + @mock.patch.object(objects.VnfInstance, "get_by_id") + @mock.patch.object(TackerManager, 'get_service_plugins', + return_value={'VNFM': + test_nfvo_plugin.FakeVNFMPlugin()}) + @mock.patch.object(objects.VNF, "vnf_index_list") + def test_update_vnf_none_vnf_data( + self, + mock_vnf_index_list, + mock_get_service_plugins, + mock_vnf_by_id): + + mock_vnf_by_id.return_value = fakes.return_vnf_instance() + mock_vnf_index_list.return_value = None + + body = {"vnfInstanceName": "new_instance_name", + "vnfInstanceDescription": "new_instance_discription", + "vnfdId": "2c69a161-0000-4b0f-bcf8-391f8fc76600", + "vnfConfigurableProperties": { + "test": "test_value" + }, + "vnfcInfoModificationsDeleteIds": ["test1"], + "metadata": {"testkey": "test_value"}, + "vimConnectionInfo": {"id": "testid"}} + req = fake_request.HTTPRequest.blank( + '/vnf_instances/%s' % constants.UUID) + req.body = jsonutils.dump_as_bytes(body) + req.headers['Content-Type'] = 'application/json' + req.method = 'PATCH' + + msg = _("Can not find requested vnf data: %s") % constants.UUID + res = self._make_problem_detail(msg, 404, title='Not Found') + + resp = req.get_response(self.app) + self.assertEqual(res.text, resp.text) + + @mock.patch.object(objects.VnfInstance, "get_by_id") + @mock.patch.object(TackerManager, 'get_service_plugins', + return_value={'VNFM': + test_nfvo_plugin.FakeVNFMPlugin()}) + @mock.patch.object(objects.VNF, "vnf_index_list") + @mock.patch.object(objects.VnfInstanceList, "vnf_instance_list") + def test_update_vnf_none_instance_data( + self, + mock_vnf_instance_list, + mock_vnf_index_list, + mock_get_service_plugins, + mock_vnf_by_id): + + mock_vnf_by_id.return_value = fakes.return_vnf_instance() + mock_vnf_index_list.return_value = fakes._get_vnf() + mock_vnf_instance_list.return_value = "" + + body = {"vnfInstanceName": "new_instance_name", + "vnfInstanceDescription": "new_instance_discription", + "vnfdId": "2c69a161-0000-4b0f-bcf8-391f8fc76600", + "vnfConfigurableProperties": { + "test": "test_value" + }, + "vnfcInfoModificationsDeleteIds": ["test1"], + "metadata": {"testkey": "test_value"}, + "vimConnectionInfo": {"id": "testid"}} + req = fake_request.HTTPRequest.blank( + '/vnf_instances/%s' % constants.UUID) + req.body = jsonutils.dump_as_bytes(body) + req.headers['Content-Type'] = 'application/json' + req.method = 'PATCH' + + vnf_data = fakes._get_vnf() + msg = ("Can not find requested vnf instance data: %s") % vnf_data.get( + 'vnfd_id') + res = self._make_problem_detail(msg, 404, title='Not Found') + + resp = req.get_response(self.app) + self.assertEqual(res.text, resp.text) + + @mock.patch.object(objects.VnfInstance, "get_by_id") + @mock.patch.object(TackerManager, 'get_service_plugins', + return_value={'VNFM': + test_nfvo_plugin.FakeVNFMPlugin()}) + @mock.patch.object(objects.VNF, "vnf_index_list") + @mock.patch.object(objects.VnfInstanceList, "vnf_instance_list") + @mock.patch.object(objects.VnfPackageVnfd, 'get_vnf_package_vnfd') + @mock.patch.object(vnf_lcm_rpc.VNFLcmRPCAPI, "update") + def test_update_vnf( + self, + mock_update, + mock_vnf_package_vnf_get_vnf_package_vnfd, + mock_vnf_instance_list, + mock_vnf_index_list, + mock_get_service_plugins, + mock_vnf_by_id): + + mock_vnf_by_id.return_value = fakes.return_vnf_instance() + mock_vnf_index_list.return_value = fakes._get_vnf() + mock_vnf_instance_list.return_value = fakes.return_vnf_instance( + fields.VnfInstanceState.INSTANTIATED) + mock_vnf_package_vnf_get_vnf_package_vnfd.return_value =\ + fakes.return_vnf_package_vnfd() + + body = {"vnfInstanceName": "new_instance_name", + "vnfInstanceDescription": "new_instance_discription", + "vnfdId": "2c69a161-0000-4b0f-bcf8-391f8fc76600", + "vnfConfigurableProperties": { + "test": "test_value" + }, + "vnfcInfoModificationsDeleteIds": ["test1"], + "metadata": {"testkey": "test_value"}, + "vimConnectionInfo": {"id": "testid"}} + req = fake_request.HTTPRequest.blank( + '/vnf_instances/%s' % constants.UUID) + req.body = jsonutils.dump_as_bytes(body) + req.headers['Content-Type'] = 'application/json' + req.method = 'PATCH' + + # Call Instantiate API + resp = req.get_response(self.app) + self.assertEqual(http_client.ACCEPTED, resp.status_code) + mock_update.assert_called_once() + + @ddt.data("vnfdId", "vnfPkgId") + @mock.patch.object(objects.VnfInstance, "get_by_id") + @mock.patch.object(TackerManager, 'get_service_plugins', + return_value={'VNFM': + test_nfvo_plugin.FakeVNFMPlugin()}) + @mock.patch.object(sync_resource.SyncVnfPackage, 'create_package') + @mock.patch.object(objects.vnf_package_vnfd.VnfPackageVnfd, + "get_vnf_package_vnfd") + @mock.patch.object(nfvo_client.VnfPackageRequest, "index") + @mock.patch.object(objects.VNF, "vnf_index_list") + @mock.patch.object(objects.VnfInstanceList, "vnf_instance_list") + @mock.patch.object(objects.VnfPackageVnfd, 'get_vnf_package_vnfd') + @mock.patch.object(vnf_lcm_rpc.VNFLcmRPCAPI, "update") + def test_update_none_vnf_package_info( + self, input_id, + mock_update, + mock_vnf_package_vnf_get_vnf_package_vnfd, + mock_vnf_instance_list, + mock_vnf_index_list, + mock_index, + mock_get_vnf_package_vnfd, + mock_create_package, + mock_get_service_plugins, + mock_vnf_by_id): + mock_vnf_by_id.return_value = fakes.return_vnf_instance() + mock_vnf_index_list.return_value = fakes._get_vnf() + mock_vnf_instance_list.return_value = fakes.return_vnf_instance( + fields.VnfInstanceState.INSTANTIATED) + mock_vnf_package_vnf_get_vnf_package_vnfd.return_value = "" + mock_get_vnf_package_vnfd.side_effect = \ + exceptions.VnfPackageVnfdNotFound + mock_create_package.return_value = fakes.return_vnf_package_vnfd() + mock_response = mock.MagicMock() + mock_response.ok = False + mock_response.json = mock.MagicMock() + mock_response.json.return_value = ['aaa', 'bbb', 'ccc'] + + mock_index.return_value = mock_response + + body = {"vnfInstanceName": "new_instance_name", + "vnfInstanceDescription": "new_instance_discription", + input_id: "2c69a161-0000-4b0f-bcf8-391f8fc76600", + "vnfConfigurableProperties": {"test": "test_value"}, + "vnfcInfoModificationsDeleteIds": ["test1"]} + req = fake_request.HTTPRequest.blank( + '/vnf_instances/%s' % constants.UUID) + req.body = jsonutils.dump_as_bytes(body) + req.headers['Content-Type'] = 'application/json' + req.method = 'PATCH' + + resp = req.get_response(self.app) + self.assertEqual(http_client.BAD_REQUEST, resp.status_code) + + @ddt.data(*fakes.get_test_data_policy_vnf_not_instantiated( + 'show')) + @ddt.unpack + @mock.patch.object(objects.vnf_instance, "_vnf_instance_get_by_id") + def test_show_enhanced_policy_vnf_not_instantiated( + self, mock_vnf_by_id, vnf_instance_updates, + rules, roles, expected_status_code): + mock_vnf_by_id.return_value = fakes.return_vnf_instance_model( + **vnf_instance_updates) + + req = fake_request.HTTPRequest.blank( + '/vnf_instances/%s' % uuidsentinel.instance_id) + req.method = 'GET' + policy.set_rules(oslo_policy.Rules.from_dict(rules), overwrite=True) + ctx = context.Context( + 'fake', 'fake', roles=roles) + resp = req.get_response(fakes.wsgi_app_v1(fake_auth_context=ctx)) + self.assertEqual(expected_status_code, resp.status_code) + + @ddt.data(*fakes.get_test_data_policy_vnf_instantiated( + 'show', http_client.OK)) + @ddt.unpack + @mock.patch.object(objects.VnfInstance, "get_by_id") + def test_show_enhanced_policy_vnf_instantiated( + self, mock_vnf_by_id, vnf_instance_updates, + rules, roles, expected_status_code): + mock_vnf_by_id.return_value = fakes.return_vnf_instance( + instantiated_state=fields.VnfInstanceState.INSTANTIATED, + **vnf_instance_updates) + + req = fake_request.HTTPRequest.blank( + '/vnf_instances/%s' % uuidsentinel.instance_id) + req.method = 'GET' + policy.set_rules(oslo_policy.Rules.from_dict(rules), overwrite=True) + ctx = context.Context( + 'fake', 'fake', roles=roles) + resp = req.get_response(fakes.wsgi_app_v1(fake_auth_context=ctx)) + self.assertEqual(expected_status_code, resp.status_code) + + @ddt.data(*fakes.get_test_data_policy_create()) + @ddt.unpack + @mock.patch.object(objects.VnfPackage, 'get_by_id') + @mock.patch('tacker.api.vnflcm.v1.controller.' + 'VnfLcmController._create_vnf') + @mock.patch.object(objects.vnf_instance, '_vnf_instance_update') + @mock.patch.object(objects.vnf_instance, '_vnf_instance_create') + @mock.patch.object(vim_client.VimClient, "get_vim") + @mock.patch.object(objects.VnfPackageVnfd, 'get_by_id') + def test_create_enhanced_policy( + self, + mock_vnf_package_vnfd_get_by_id, + mock_get_vim, + mock_vnf_instance_create, + mock_vnf_instance_update, + mock_private_create_vnf, + mock_vnf_package_get_by_id, + vnfd_updates, + rules, + roles, + expected_status_code): + + mock_vnf_package_vnfd_get_by_id.return_value = \ + fakes.return_vnf_package_vnfd( + **vnfd_updates) + updates = {'vnfd_id': uuidsentinel.vnfd_id} + mock_vnf_instance_create.return_value = \ + fakes.return_vnf_instance_model(**updates) + mock_vnf_instance_update.return_value = \ + fakes.return_vnf_instance_model(**updates) + mock_get_vim.return_value = self.vim_info + + body = {'vnfdId': uuidsentinel.vnfd_id, + 'metadata': {"key": "value"}} + req = fake_request.HTTPRequest.blank('/vnf_instances') + req.body = jsonutils.dump_as_bytes(body) + req.headers['Content-Type'] = 'application/json' + req.method = 'POST' + + policy.set_rules(oslo_policy.Rules.from_dict(rules), overwrite=True) + ctx = context.Context( + 'fake', 'fake', roles=roles) + resp = req.get_response(fakes.wsgi_app_v1(fake_auth_context=ctx)) + self.assertEqual(expected_status_code, resp.status_code) + + @ddt.data(*fakes.get_test_data_policy_vnf_instantiated( + 'terminate', http_client.ACCEPTED)) + @ddt.unpack + @mock.patch.object(vnf_lcm_rpc.VNFLcmRPCAPI, "terminate") + @mock.patch('tacker.api.vnflcm.v1.controller.' + 'VnfLcmController._notification_process') + @mock.patch.object(objects.VnfInstance, "save") + @mock.patch('tacker.api.vnflcm.v1.controller.' + 'VnfLcmController._get_vnf') + @mock.patch.object(objects.VnfInstance, "get_by_id") + def test_terminate_enhanced_policy( + self, mock_vnf_by_id, mock_get_vnf, mock_save, + mock_notification_process, mock_terminate, + vnf_instance_updates, + rules, roles, expected_status_code): + vnf_instance_obj = fakes.return_vnf_instance( + instantiated_state=fields.VnfInstanceState.INSTANTIATED, + **vnf_instance_updates) + mock_vnf_by_id.return_value = vnf_instance_obj + mock_get_vnf.return_value = \ + self._get_dummy_vnf(vnf_id=vnf_instance_obj.id, status='ACTIVE') + + req = fake_request.HTTPRequest.blank( + '/vnf_instances/%s/terminate' % uuidsentinel.instance_id) + body = {'terminationType': 'FORCEFUL'} + req.body = jsonutils.dump_as_bytes(body) + req.headers['Content-Type'] = 'application/json' + req.method = 'POST' + policy.set_rules(oslo_policy.Rules.from_dict(rules), overwrite=True) + ctx = context.Context( + 'fake', 'fake', roles=roles) + resp = req.get_response(fakes.wsgi_app_v1(fake_auth_context=ctx)) + self.assertEqual(expected_status_code, resp.status_code) + + @ddt.data(*fakes.get_test_data_policy_vnf_instantiated( + 'heal', http_client.ACCEPTED)) + @ddt.unpack + @mock.patch('tacker.api.vnflcm.v1.controller.' + 'VnfLcmController._notification_process') + @mock.patch.object(objects.VnfInstance, "save") + @mock.patch('tacker.api.vnflcm.v1.controller.' + 'VnfLcmController._get_vnf') + @mock.patch.object(objects.VnfInstance, "get_by_id") + def test_heal_enhanced_policy( + self, mock_vnf_by_id, mock_get_vnf, mock_save, + mock_notification_process, + vnf_instance_updates, + rules, roles, expected_status_code): + vnf_instance_obj = fakes.return_vnf_instance( + instantiated_state=fields.VnfInstanceState.INSTANTIATED, + **vnf_instance_updates) + mock_vnf_by_id.return_value = vnf_instance_obj + mock_get_vnf.return_value = \ + self._get_dummy_vnf(vnf_id=vnf_instance_obj.id, status='ACTIVE') + + req = fake_request.HTTPRequest.blank( + '/vnf_instances/%s/heal' % uuidsentinel.instance_id) + body = {} + req.body = jsonutils.dump_as_bytes(body) + req.headers['Content-Type'] = 'application/json' + req.method = 'POST' + policy.set_rules(oslo_policy.Rules.from_dict(rules), overwrite=True) + ctx = context.Context( + 'fake', 'fake', roles=roles) + resp = req.get_response(fakes.wsgi_app_v1(fake_auth_context=ctx)) + self.assertEqual(expected_status_code, resp.status_code) + + @ddt.data(*fakes.get_test_data_policy_vnf_instantiated( + 'delete', http_client.NO_CONTENT)) + @ddt.unpack + @mock.patch('tacker.api.vnflcm.v1.controller.' + 'VnfLcmController._delete') + @mock.patch('tacker.api.vnflcm.v1.controller.' + 'VnfLcmController._notification_process') + @mock.patch('tacker.api.vnflcm.v1.controller.' + 'VnfLcmController._get_vnf') + @mock.patch.object(objects.VnfInstance, "get_by_id") + def test_delete_enhanced_policy( + self, + mock_vnf_by_id, + mock_get_vnf, + mock_notification_process, + mock_private_delete, + vnf_instance_updates, + rules, roles, expected_status_code): + vnf_instance_obj = fakes.return_vnf_instance_delete( + **vnf_instance_updates) + mock_vnf_by_id.return_value = vnf_instance_obj + mock_get_vnf.return_value = \ + self._get_dummy_vnf(vnf_id=vnf_instance_obj.id, status='ACTIVE') + + req = fake_request.HTTPRequest.blank( + '/vnf_instances/%s' % uuidsentinel.vnf_instance_id) + req.headers['Content-Type'] = 'application/json' + req.method = 'DELETE' + policy.set_rules(oslo_policy.Rules.from_dict(rules), overwrite=True) + ctx = context.Context( + 'fake', 'fake', roles=roles) + resp = req.get_response(fakes.wsgi_app_v1(fake_auth_context=ctx)) + self.assertEqual(expected_status_code, resp.status_code) + + @ddt.data(*fakes.get_test_data_policy_vnf_instantiated( + 'scale', http_client.ACCEPTED)) + @ddt.unpack + @mock.patch.object(objects.VnfLcmOpOcc, "create") + @mock.patch.object(TackerManager, 'get_service_plugins', + return_value={'VNFM': FakeVNFMPlugin()}) + @mock.patch.object(tacker.db.vnfm.vnfm_db.VNFMPluginDb, "get_vnf") + @mock.patch('tacker.api.vnflcm.v1.controller.' + 'VnfLcmController._notification_process') + @mock.patch.object(objects.VnfInstance, "save") + @mock.patch.object(objects.VnfInstance, "get_by_id") + # @mock.patch.object(vnf_lcm_rpc.VNFLcmRPCAPI, "_scale") + @mock.patch('tacker.api.vnflcm.v1.controller.' + 'VnfLcmController._scale') + def test_scale_enhanced_policy( + self, mock_scale, mock_vnf_by_id, + mock_save, + mock_notification_process, + mock_get_vnf, + mock_get_service_plugins, + mock_opocc_create, + vnf_instance_updates, + rules, roles, expected_status_code): + res = webob.Response() + res.status_int = 202 + location = ('Location', 'http://mock.mock') + res.headerlist.append(location) + mock_scale.return_value = res + vnf_instance_obj = fakes.return_vnf_instance( + instantiated_state=fields.VnfInstanceState.INSTANTIATED, + scale_status="scale_status", + **vnf_instance_updates) + mock_vnf_by_id.return_value = vnf_instance_obj + mock_get_vnf.return_value = fakes._get_vnf() + + req = fake_request.HTTPRequest.blank( + '/vnf_instances/%s/scale' % uuidsentinel.instance_id) + body = {"type": "SCALE_OUT", + "aspectId": "SP1", + "numberOfSteps": 1, + "additionalParams": { + "test": "test_value"}} + req.body = jsonutils.dump_as_bytes(body) + req.headers['Content-Type'] = 'application/json' + req.method = 'POST' + policy.set_rules(oslo_policy.Rules.from_dict(rules), overwrite=True) + ctx = context.Context( + 'fake', 'fake', roles=roles) + resp = req.get_response(fakes.wsgi_app_v1(fake_auth_context=ctx)) + self.assertEqual(expected_status_code, resp.status_code) + + @ddt.data(*fakes.get_test_data_policy_vnf_instantiated( + 'update_vnf', http_client.ACCEPTED)) + @ddt.unpack + @mock.patch.object(TackerManager, 'get_service_plugins', + return_value={'VNFM': + test_nfvo_plugin.FakeVNFMPlugin()}) + @mock.patch.object(objects.VNF, "vnf_index_list") + @mock.patch.object(objects.VnfInstanceList, "vnf_instance_list") + @mock.patch.object(objects.VnfPackageVnfd, 'get_vnf_package_vnfd') + @mock.patch.object(vnf_lcm_rpc.VNFLcmRPCAPI, "update") + @mock.patch.object(objects.VnfInstance, "get_by_id") + def test_update_vnf_enhanced_policy( + self, + mock_vnf_by_id, + mock_update, + mock_vnf_package_vnf_get_vnf_package_vnfd, + mock_vnf_instance_list, + mock_vnf_index_list, + mock_get_service_plugins, + vnf_instance_updates, + rules, roles, expected_status_code): + vnf_instance_obj = fakes.return_vnf_instance( + instantiated_state=fields.VnfInstanceState.INSTANTIATED, + scale_status="scale_status", + **vnf_instance_updates) + mock_vnf_by_id.return_value = vnf_instance_obj + mock_vnf_index_list.return_value = fakes._get_vnf() + mock_vnf_instance_list.return_value = fakes.return_vnf_instance( + fields.VnfInstanceState.INSTANTIATED, **vnf_instance_updates) + mock_vnf_package_vnf_get_vnf_package_vnfd.return_value =\ + fakes.return_vnf_package_vnfd() + + body = {"vnfInstanceName": "new_instance_name", + "vnfInstanceDescription": "new_instance_discription", + "vnfdId": "2c69a161-0000-4b0f-bcf8-391f8fc76600", + "vnfConfigurableProperties": { + "test": "test_value" + }, + "vnfcInfoModificationsDeleteIds": ["test1"], + "metadata": {"testkey": "test_value"}, + "vimConnectionInfo": {"id": "testid"}} + req = fake_request.HTTPRequest.blank( + '/vnf_instances/%s' % constants.UUID) + req.body = jsonutils.dump_as_bytes(body) + req.headers['Content-Type'] = 'application/json' + req.method = 'PATCH' + policy.set_rules(oslo_policy.Rules.from_dict(rules), overwrite=True) + ctx = context.Context( + 'fake', 'fake', roles=roles) + # Call Instantiate API + resp = req.get_response(fakes.wsgi_app_v1(fake_auth_context=ctx)) + self.assertEqual(expected_status_code, resp.status_code) + if expected_status_code != http_client.FORBIDDEN: + mock_update.assert_called_once() + + @ddt.data(*fakes.get_test_data_policy_vnf_instantiated( + 'change_ext_conn', http_client.ACCEPTED)) + @ddt.unpack + @mock.patch.object(TackerManager, 'get_service_plugins', + return_value={'VNFM': + test_nfvo_plugin.FakeVNFMPlugin()}) + @mock.patch('tacker.api.vnflcm.v1.controller.' + 'VnfLcmController._notification_process') + @mock.patch('tacker.api.vnflcm.v1.controller.' + 'VnfLcmController._get_vnf') + @mock.patch.object(objects.VnfInstance, "get_by_id") + @mock.patch.object(objects.VnfInstance, "save") + @mock.patch.object(vnf_lcm_rpc.VNFLcmRPCAPI, "change_ext_conn") + def test_change_ext_conn_enhanced_policy( + self, mock_rpc, mock_save, + mock_vnf_by_id, mock_get_vnf, + mock_notification_process, + mock_get_service_plugins, + vnf_instance_updates, + rules, roles, expected_status_code): + + vnf_instance_obj = fakes.return_vnf_instance( + fields.VnfInstanceState.INSTANTIATED, + **vnf_instance_updates + ) + mock_vnf_by_id.return_value = vnf_instance_obj + mock_get_vnf.return_value = \ + self._get_dummy_vnf(vnf_id=vnf_instance_obj.id, status='ACTIVE') + + body = fakes.get_change_ext_conn_request_body() + req = fake_request.HTTPRequest.blank( + '/vnf_instances/%s/change_ext_conn' % uuidsentinel.vnf_instance_id) + req.body = jsonutils.dump_as_bytes(body) + req.headers['Content-Type'] = 'application/json' + req.method = 'POST' + + policy.set_rules(oslo_policy.Rules.from_dict(rules), overwrite=True) + ctx = context.Context( + 'fake', 'fake', roles=roles) + resp = req.get_response(fakes.wsgi_app_v1(fake_auth_context=ctx)) + + self.assertEqual(expected_status_code, resp.status_code) + if expected_status_code != http_client.FORBIDDEN: + mock_rpc.assert_called_once() + + @ddt.data(*fakes.get_test_data_policy_instantiate()) + @ddt.unpack + @mock.patch.object(TackerManager, 'get_service_plugins', + return_value={'VNFM': + test_nfvo_plugin.FakeVNFMPlugin()}) + @mock.patch('tacker.api.vnflcm.v1.controller.VnfLcmController.' + '_notification_process') + @mock.patch('tacker.api.vnflcm.v1.controller.' + 'VnfLcmController._get_vnf') + @mock.patch.object(vim_client.VimClient, "get_vim") + @mock.patch.object(objects.vnf_instance, "_vnf_instance_get_by_id") + @mock.patch.object(objects.VnfInstance, "save") + @mock.patch.object(objects.VnfPackageVnfd, 'get_by_id') + @mock.patch.object(objects.VnfPackage, "get_by_id") + @mock.patch.object(vnf_lcm_rpc.VNFLcmRPCAPI, "instantiate") + def test_instantiate_with_vim_connection_enhanced_policy( + self, mock_instantiate, mock_vnf_package_get_by_id, + mock_vnf_package_vnfd_get_by_id, mock_save, + mock_vnf_instance_get_by_id, mock_get_vim, + mock_get_vnf, mock_insta_notif_process, + mock_get_service_plugins, vnf_instance_updates, + rules, roles, expected_status_code): + + vim = fakes.return_default_vim() + vim.update({'extra': {"area": "area_A@region_A"}}) + mock_get_vim.return_value = vim + mock_vnf_instance_get_by_id.return_value =\ + fakes.return_vnf_instance_model(**vnf_instance_updates) + mock_vnf_package_vnfd_get_by_id.return_value = \ + fakes.return_vnf_package_vnfd() + mock_vnf_package_get_by_id.return_value = \ + fakes.return_vnf_package_with_deployment_flavour() + mock_get_vnf.return_value = \ + self._get_dummy_vnf( + vnf_id=mock_vnf_instance_get_by_id.return_value.id, + status='INACTIVE') + + body = {"flavourId": "simple", + "vimConnectionInfo": [ + {"id": uuidsentinel.vim_connection_id, + "vimId": uuidsentinel.vim_id, + "vimType": 'openstack'} + ]} + + req = fake_request.HTTPRequest.blank( + '/vnf_instances/%s/instantiate' % uuidsentinel.vnf_instance_id) + req.body = jsonutils.dump_as_bytes(body) + req.headers['Content-Type'] = 'application/json' + req.method = 'POST' + policy.set_rules(oslo_policy.Rules.from_dict(rules), overwrite=True) + ctx = context.Context( + 'fake', 'fake', roles=roles) + # Call Instantiate API + resp = req.get_response(fakes.wsgi_app_v1(fake_auth_context=ctx)) + self.assertEqual(expected_status_code, resp.status_code) + if expected_status_code != http_client.FORBIDDEN: + self.assertTrue('Location' in resp.headers.keys()) + expected_location = (self.expected_location_prefix + + str(mock_insta_notif_process.return_value)) + self.assertEqual(expected_location, resp.headers['location']) + mock_instantiate.assert_called_once() + mock_get_vnf.assert_called_once() + mock_insta_notif_process.assert_called_once() + + @ddt.data(*fakes.get_test_data_policy_index()) + @ddt.unpack + @mock.patch.object(objects.VnfInstanceList, "get_by_marker_filter") + def test_index_enhanced_policy(self, mock_vnf_list, vnf_instance_list, + rules, roles, expected_vnf_inst_ids): + + mock_vnf_list.return_value = vnf_instance_list + + req = fake_request.HTTPRequest.blank('/vnf_instances') + req.headers['Content-Type'] = 'application/json' + req.method = 'GET' + policy.set_rules(oslo_policy.Rules.from_dict(rules), overwrite=True) + ctx = context.Context( + 'fake', 'fake', roles=roles) + resp = req.get_response(fakes.wsgi_app_v1(fake_auth_context=ctx)) + self.assertEqual(http_client.OK, resp.status_code) + self.assertEqual( + expected_vnf_inst_ids, [inst.get('id') for inst in resp.json]) + + @mock.patch.object(TackerManager, 'get_service_plugins', + return_value={'VNFM': + test_nfvo_plugin.FakeVNFMPlugin()}) + @mock.patch('tacker.api.vnflcm.v1.controller.VnfLcmController.' + '_notification_process') + @mock.patch('tacker.api.vnflcm.v1.controller.' + 'VnfLcmController._get_vnf') + @mock.patch.object(vim_client.VimClient, "get_vim") + @mock.patch.object(objects.vnf_instance, "_vnf_instance_get_by_id") + @mock.patch.object(objects.VnfInstance, "save") + @mock.patch.object(objects.VnfPackageVnfd, 'get_by_id') + @mock.patch.object(objects.VnfPackage, "get_by_id") + @mock.patch.object(vnf_lcm_rpc.VNFLcmRPCAPI, "instantiate") + def test_instantiate_add_area( + self, mock_instantiate, mock_vnf_package_get_by_id, + mock_vnf_package_vnfd_get_by_id, mock_save, + mock_vnf_instance_get_by_id, mock_get_vim, + mock_get_vnf, mock_insta_notif_process, + mock_get_service_plugins): + vim = fakes.return_default_vim() + vim.update({'extra': {"area": "area_A@region_A"}}) + mock_get_vim.return_value = vim + mock_vnf_instance_get_by_id.return_value =\ + fakes.return_vnf_instance_model() + mock_vnf_package_vnfd_get_by_id.return_value = \ + fakes.return_vnf_package_vnfd() + mock_vnf_package_get_by_id.return_value = \ + fakes.return_vnf_package_with_deployment_flavour() + mock_get_vnf.return_value = \ + self._get_dummy_vnf( + vnf_id=mock_vnf_instance_get_by_id.return_value.id, + status='INACTIVE') + + body = {"flavourId": "simple", + "vimConnectionInfo": [ + {"id": uuidsentinel.vim_connection_id, + "vimId": uuidsentinel.vim_id, + "vimType": 'openstack'} + ]} + + req = fake_request.HTTPRequest.blank( + '/vnf_instances/%s/instantiate' % uuidsentinel.vnf_instance_id) + req.body = jsonutils.dump_as_bytes(body) + req.headers['Content-Type'] = 'application/json' + req.method = 'POST' + + # Call Instantiate API + resp = req.get_response(self.app) + self.assertTrue('Location' in resp.headers.keys()) + expected_location = (self.expected_location_prefix + + str(mock_insta_notif_process.return_value)) + self.assertEqual(expected_location, resp.headers['location']) + self.assertEqual(http_client.ACCEPTED, resp.status_code) + mock_instantiate.assert_called_once() + mock_get_vnf.assert_called_once() + mock_insta_notif_process.assert_called_once() diff --git a/tacker/tests/unit/vnfpkgm/fakes.py b/tacker/tests/unit/vnfpkgm/fakes.py index 14e4c1d3b..28a9f9e6c 100644 --- a/tacker/tests/unit/vnfpkgm/fakes.py +++ b/tacker/tests/unit/vnfpkgm/fakes.py @@ -16,8 +16,10 @@ from copy import deepcopy import datetime +from http import client as http_client import iso8601 import os +from oslo_utils import uuidutils import shutil import uuid import webob @@ -30,6 +32,7 @@ from tacker.objects import vnf_deployment_flavour as vnf_deployment_flavour_obj from tacker.objects import vnf_package as vnf_package_obj from tacker.objects import vnf_package_vnfd as vnf_package_vnfd_obj from tacker.objects import vnf_software_image as vnf_software_image_obj +from tacker.policies import vnf_package as vnf_package_policies from tacker.tests import constants from tacker.tests import utils from tacker.tests import uuidsentinel @@ -296,3 +299,128 @@ def return_vnfd_data(csar_without_tosca_meta=False): shutil.rmtree(csar_temp_dir) return file_path_and_data + + +def get_test_data_pkg_index(): + rules = { + vnf_package_policies.VNFPKGM % 'index': + "vendor:%(vendor)s" + } + id_a = uuidutils.generate_uuid() + pkg_provider_a = return_vnfpkg_obj( + vnf_package_updates={'id': uuidutils.generate_uuid()}, + vnfd_updates={ + 'vnf_provider': 'provider_A', + 'id': id_a, + }) + id_b = uuidutils.generate_uuid() + pkg_provider_b = return_vnfpkg_obj( + vnf_package_updates={'id': uuidutils.generate_uuid()}, + vnfd_updates={ + 'vnf_provider': 'provider_B', + 'id': id_b, + }) + # id_c = uuidutils.generate_uuid() + updates = {'id': uuidutils.generate_uuid(), + 'onboarding_state': 'CREATED', + 'operational_state': 'DISABLED'} + pkg_provider_c = return_vnfpkg_obj( + vnf_package_updates=updates + ) + test_data = [ + { + 'pkg_list': [pkg_provider_a], + 'rules': rules, + 'roles': ['VENDOR_provider_A'], + 'expected_pkg_ids': [pkg_provider_a.id] + }, + { + 'pkg_list': [pkg_provider_b], + 'rules': rules, + 'roles': ['VENDOR_provider_B'], + 'expected_pkg_ids': [pkg_provider_b.id] + }, + { + 'pkg_list': [pkg_provider_a, pkg_provider_b], + 'rules': rules, + 'roles': ['VENDOR_all'], + 'expected_pkg_ids': [pkg_provider_a.id, pkg_provider_b.id] + }, + { + 'pkg_list': [pkg_provider_c], + 'rules': rules, + 'roles': [], + 'expected_pkg_ids': [pkg_provider_c.id] + }, + { + 'pkg_list': [pkg_provider_c], + 'rules': rules, + 'roles': ['VENDOR_provider_A'], + 'expected_pkg_ids': [pkg_provider_c.id] + }, + { + 'pkg_list': [pkg_provider_c], + 'rules': rules, + 'roles': ['VENDOR_all'], + 'expected_pkg_ids': [pkg_provider_c.id] + }, + ] + return test_data + + +def get_test_data_pkg_uploaded(action, success_status_code): + rules = { + vnf_package_policies.VNFPKGM % action: + "vendor:%(vendor)s" + } + test_data = [ + # 'expected_status_code': success_status_code + { + 'vnfd_updates': { + 'vnf_provider': 'provider_A', + }, + 'rules': rules, + 'roles': [ + 'VENDOR_provider_A', + ], + 'expected_status_code': success_status_code + }, + { + 'vnfd_updates': { + 'vnf_provider': 'provider_A', + }, + 'rules': rules, + 'roles': [ + 'VENDOR_all', + ], + 'expected_status_code': success_status_code + }, + # 'expected_status_code': http_client.FORBIDDEN + { + 'vnfd_updates': { + 'vnf_provider': 'provider_A', + }, + 'rules': rules, + 'roles': [ + 'VENDOR_provider_B', + ], + 'expected_status_code': http_client.FORBIDDEN + }, + { + 'vnfd_updates': { + 'vnf_provider': 'provider_A', + }, + 'rules': rules, + 'roles': [], + 'expected_status_code': http_client.FORBIDDEN + }, + ] + return test_data + + +def get_test_data_pkg_to_upload(action, success_status_code): + test_data = get_test_data_pkg_uploaded(action, success_status_code) + for item in test_data: + item['vnf_data'] = item.pop('vnfd_updates') + item['vnf_data']['provider'] = item['vnf_data'].pop('vnf_provider') + return test_data diff --git a/tacker/tests/unit/vnfpkgm/test_controller.py b/tacker/tests/unit/vnfpkgm/test_controller.py index e2676adb6..365d22b01 100644 --- a/tacker/tests/unit/vnfpkgm/test_controller.py +++ b/tacker/tests/unit/vnfpkgm/test_controller.py @@ -24,17 +24,22 @@ import urllib from webob import exc from oslo_config import cfg +from oslo_policy import policy as oslo_policy from oslo_serialization import jsonutils from tacker.api.vnfpkgm.v1 import controller +from tacker.common import csar_utils from tacker.common import exceptions as tacker_exc from tacker.conductor.conductorrpc.vnf_pkgm_rpc import VNFPackageRPCAPI +from tacker import context from tacker.glance_store import store as glance_store from tacker import objects from tacker.objects import fields from tacker.objects import vnf_package from tacker.objects.vnf_package import VnfPackagesList from tacker.objects.vnf_software_image import VnfSoftwareImage +from tacker.policies import vnf_package as vnf_package_policies +from tacker import policy from tacker.tests import constants from tacker.tests.unit import base from tacker.tests.unit import fake_request @@ -679,25 +684,25 @@ class TestController(base.TestCase): @mock.patch.object(objects.VnfPackage, "get_by_id") def test_delete_with_operational_state_enabled(self, mock_vnf_by_id): + vnfpkg_updates = { + 'operational_state': fields.PackageOperationalStateType.ENABLED} + mock_vnf_by_id.return_value = fakes.return_vnfpkg_obj( + vnf_package_updates=vnfpkg_updates) req = fake_request.HTTPRequest.blank( '/vnfpkgm/v1/vnf_packages/%s' % constants.UUID) - vnf_package_dict = fakes.fake_vnf_package() - vnf_package_dict['operational_state'] = \ - fields.PackageOperationalStateType.ENABLED - vnf_package = objects.VnfPackage(**vnf_package_dict) - mock_vnf_by_id.return_value = vnf_package + self.assertRaises(exc.HTTPConflict, self.controller.delete, req, constants.UUID) @mock.patch.object(vnf_package.VnfPackage, "get_by_id") def test_delete_with_usage_state_in_use(self, mock_vnf_by_id): + vnfpkg_updates = { + 'usage_state': fields.PackageUsageStateType.IN_USE} + mock_vnf_by_id.return_value = fakes.return_vnfpkg_obj( + vnf_package_updates=vnfpkg_updates) req = fake_request.HTTPRequest.blank( '/vnfpkgm/v1/vnf_packages/%s' % constants.UUID) - vnf_package_dict = fakes.fake_vnf_package() - vnf_package_dict['usage_state'] = \ - fields.PackageUsageStateType.IN_USE - vnf_package = objects.VnfPackage(**vnf_package_dict) - mock_vnf_by_id.return_value = vnf_package + self.assertRaises(exc.HTTPConflict, self.controller.delete, req, constants.UUID) @@ -1370,3 +1375,297 @@ class TestController(base.TestCase): self.controller.fetch_vnf_package_artifacts, req, constants.UUID, constants.ARTIFACT_PATH) + + +@ddt.ddt +class TestControllerEnhancedPolicy(TestController): + + def setUp(self): + super(TestControllerEnhancedPolicy, self).setUp() + cfg.CONF.set_override( + "enhanced_tacker_policy", True, group='oslo_policy') + + @mock.patch.object(csar_utils, 'load_csar_data') + @mock.patch.object(glance_store, 'load_csar') + @mock.patch.object(glance_store, 'store_csar') + @mock.patch.object(VNFPackageRPCAPI, "upload_vnf_package_content") + @mock.patch.object(vnf_package.VnfPackage, "get_by_id") + @mock.patch.object(vnf_package.VnfPackage, "save") + def test_upload_vnf_package_content(self, mock_vnf_pack_save, + mock_vnf_by_id, + mock_upload_vnf_package_content, + mock_glance_store, + mock_load_csar, + mock_load_csar_data): + updates = {'onboarding_state': 'CREATED', + 'operational_state': 'DISABLED'} + vnf_package_dict = fakes.fake_vnf_package(updates) + vnf_package_obj = objects.VnfPackage(**vnf_package_dict) + mock_vnf_by_id.return_value = vnf_package_obj + mock_vnf_pack_save.return_value = vnf_package_obj + mock_glance_store.return_value = 'location', 0, 'checksum', \ + 'multihash', 'loc_meta' + mock_load_csar.return_value = '/var/lib/tacker/5f5d99c6-844a-4c3' \ + '1-9e6d-ab21b87dcfff.zip' + mock_load_csar_data.return_value = ( + {'provider': 'company'}, mock.ANY, mock.ANY) + req = fake_request.HTTPRequest.blank( + '/vnf_packages/%s/package_content' + % constants.UUID) + req.headers['Content-Type'] = 'application/zip' + req.method = 'PUT' + req.body = jsonutils.dump_as_bytes(mock.mock_open()) + resp = req.get_response(self.app) + mock_glance_store.assert_called() + self.assertEqual(http_client.ACCEPTED, resp.status_code) + + @ddt.data(*fakes.get_test_data_pkg_to_upload('upload_package_content', + http_client.ACCEPTED)) + @ddt.unpack + @mock.patch.object(glance_store, 'delete_csar') + @mock.patch.object(csar_utils, 'load_csar_data') + @mock.patch.object(glance_store, 'load_csar') + @mock.patch.object(glance_store, 'store_csar') + @mock.patch.object(VNFPackageRPCAPI, "upload_vnf_package_content") + @mock.patch.object(vnf_package.VnfPackage, "get_by_id") + @mock.patch.object(vnf_package.VnfPackage, "save") + def test_upload_vnf_package_content_enhanced_policy( + self, mock_vnf_pack_save, + mock_vnf_by_id, + mock_upload_vnf_package_content, + mock_glance_store, + mock_load_csar, + mock_load_csar_data, + mock_delete_csar, + vnf_data, rules, roles, expected_status_code): + updates = {'onboarding_state': 'CREATED', + 'operational_state': 'DISABLED'} + vnf_package_dict = fakes.fake_vnf_package(updates) + vnf_package_obj = objects.VnfPackage(**vnf_package_dict) + mock_vnf_by_id.return_value = vnf_package_obj + mock_vnf_pack_save.return_value = vnf_package_obj + mock_glance_store.return_value = 'location', 0, 'checksum', \ + 'multihash', 'loc_meta' + mock_load_csar.return_value = '/var/lib/tacker/5f5d99c6-844a-4c3' \ + '1-9e6d-ab21b87dcfff.zip' + mock_load_csar_data.return_value = ( + vnf_data, mock.ANY, mock.ANY) + policy.set_rules(oslo_policy.Rules.from_dict(rules), overwrite=True) + ctx = context.Context( + 'fake', 'fake', roles=roles) + req = fake_request.HTTPRequest.blank( + '/vnf_packages/%s/package_content' + % constants.UUID) + req.headers['Content-Type'] = 'application/zip' + req.method = 'PUT' + req.body = jsonutils.dump_as_bytes(mock.mock_open()) + resp = req.get_response(fakes.wsgi_app_v1(fake_auth_context=ctx)) + mock_glance_store.assert_called() + self.assertEqual(expected_status_code, resp.status_code) + if expected_status_code == http_client.FORBIDDEN: + mock_delete_csar.assert_called() + + @ddt.data(*fakes.get_test_data_pkg_uploaded('show', http_client.OK)) + @ddt.unpack + @mock.patch.object(VnfSoftwareImage, 'get_by_id') + @mock.patch.object(vnf_package.VnfPackage, "get_by_id") + def test_show_enhanced_policy( + self, mock_vnf_by_id, mock_sw_image_by_id, + vnfd_updates, rules, roles, expected_status_code): + + mock_vnf_by_id.return_value = fakes.return_vnfpkg_obj( + vnfd_updates=vnfd_updates) + mock_sw_image_by_id.return_value = fakes.return_software_image() + req = fake_request.HTTPRequest.blank( + '/vnf_packages/%s' % constants.UUID) + req.method = 'GET' + policy.set_rules(oslo_policy.Rules.from_dict(rules), overwrite=True) + ctx = context.Context( + 'fake', 'fake', roles=roles) + + resp = req.get_response(fakes.wsgi_app_v1(fake_auth_context=ctx)) + self.assertEqual(expected_status_code, resp.status_code) + + @mock.patch.object(VnfSoftwareImage, 'get_by_id') + @mock.patch.object(vnf_package.VnfPackage, "get_by_id") + @ddt.data(['VENDOR_provider_A'], []) + def test_show_enhanced_policy_created( + self, roles, mock_vnf_by_id, mock_sw_image_by_id): + updates = {'onboarding_state': 'CREATED', + 'operational_state': 'DISABLED'} + mock_vnf_by_id.return_value = fakes.return_vnfpkg_obj( + vnf_package_updates=updates) + mock_sw_image_by_id.return_value = fakes.return_software_image() + req = fake_request.HTTPRequest.blank( + '/vnf_packages/%s' % constants.UUID) + req.method = 'GET' + rules = { + vnf_package_policies.VNFPKGM % 'show': + "vendor:%(vendor)s" + } + policy.set_rules(oslo_policy.Rules.from_dict(rules), overwrite=True) + ctx = context.Context( + 'fake', 'fake', roles=roles) + + resp = req.get_response(fakes.wsgi_app_v1(fake_auth_context=ctx)) + self.assertEqual(http_client.OK, resp.status_code) + + @ddt.data( + *fakes.get_test_data_pkg_uploaded('delete', http_client.NO_CONTENT)) + @ddt.unpack + @mock.patch.object(vnf_package.VnfPackage, "destroy") + @mock.patch.object(vnf_package.VnfPackage, "get_by_id") + @mock.patch.object(VNFPackageRPCAPI, "delete_vnf_package") + def test_delete_enhanced_policy( + self, mock_delete_rpc, mock_vnf_by_id, mock_vnf_pack_destroy, + vnfd_updates, rules, roles, expected_status_code): + vnfpkg_updates = {'operational_state': 'DISABLED'} + mock_vnf_by_id.return_value = fakes.return_vnfpkg_obj( + vnf_package_updates=vnfpkg_updates, vnfd_updates=vnfd_updates) + req = fake_request.HTTPRequest.blank( + '/vnf_packages/%s' % constants.UUID) + req.headers['Content-Type'] = 'application/json' + req.method = 'DELETE' + policy.set_rules(oslo_policy.Rules.from_dict(rules), overwrite=True) + ctx = context.Context( + 'fake', 'fake', roles=roles) + + resp = req.get_response(fakes.wsgi_app_v1(fake_auth_context=ctx)) + self.assertEqual(expected_status_code, resp.status_code) + + @ddt.data(*fakes.get_test_data_pkg_uploaded('patch', http_client.OK)) + @ddt.unpack + @mock.patch.object(vnf_package.VnfPackage, "get_by_id") + @mock.patch.object(vnf_package.VnfPackage, "save") + def test_patch_enhanced_policy(self, mock_save, mock_vnf_by_id, + vnfd_updates, rules, roles, expected_status_code): + vnf_package_updates = {'operational_state': 'DISABLED'} + mock_vnf_by_id.return_value = \ + fakes.return_vnfpkg_obj( + vnf_package_updates=vnf_package_updates, + vnfd_updates=vnfd_updates + ) + + req_body = {"operationalState": "ENABLED", + "userDefinedData": {"testKey1": "val01", + "testKey2": "val02", "testkey3": "val03"}} + + req = fake_request.HTTPRequest.blank( + '/vnf_packages/%s' + % constants.UUID) + req.headers['Content-Type'] = 'application/json' + req.method = 'PATCH' + req.body = jsonutils.dump_as_bytes(req_body) + policy.set_rules(oslo_policy.Rules.from_dict(rules), overwrite=True) + ctx = context.Context( + 'fake', 'fake', roles=roles) + + resp = req.get_response(fakes.wsgi_app_v1(fake_auth_context=ctx)) + + self.assertEqual(expected_status_code, resp.status_code) + if expected_status_code != http_client.FORBIDDEN: + self.assertEqual(req_body, jsonutils.loads(resp.body)) + + @ddt.data(*fakes.get_test_data_pkg_uploaded( + 'get_vnf_package_vnfd', http_client.OK)) + @ddt.unpack + @mock.patch.object(VNFPackageRPCAPI, "get_vnf_package_vnfd") + @mock.patch.object(vnf_package.VnfPackage, "get_by_id") + def test_get_vnf_package_vnfd_enhanced_policy( + self, mock_vnf_by_id, mock_get_vnf_package_vnfd, + vnfd_updates, rules, roles, expected_status_code): + + mock_vnf_by_id.return_value = fakes.return_vnfpkg_obj( + vnfd_updates=vnfd_updates) + mock_get_vnf_package_vnfd.return_value = fakes.return_vnfd_data() + req = fake_request.HTTPRequest.blank( + '/vnf_packages/%s/vnfd' + % constants.UUID) + req.headers['Accept'] = 'application/zip' + req.method = 'GET' + + policy.set_rules(oslo_policy.Rules.from_dict(rules), overwrite=True) + ctx = context.Context('fake', 'fake', roles=roles) + + resp = req.get_response(fakes.wsgi_app_v1(fake_auth_context=ctx)) + self.assertEqual(expected_status_code, resp.status_code) + + @ddt.data(*fakes.get_test_data_pkg_uploaded('fetch_package_content', + http_client.ACCEPTED)) + @ddt.unpack + @mock.patch.object(controller.VnfPkgmController, "_download") + @mock.patch.object(controller.VnfPkgmController, "_get_range_from_request") + @mock.patch.object(glance_store, 'get_csar_size') + @mock.patch.object(controller.VnfPkgmController, '_get_vnf_package') + @mock.patch.object(vnf_package.VnfPackage, 'save') + def test_fetch_vnf_package_content_enhanced_policy( + self, + mock_save, + mock_get_vnf_package, + mock_get_csar_size, + mock_get_range, + mock_download, + vnfd_updates, rules, roles, expected_status_code): + + mock_get_vnf_package.return_value = fakes.return_vnfpkg_obj( + vnfd_updates=vnfd_updates) + mock_get_csar_size.return_value = 1000 + mock_get_range.return_value = "10-20, 21-30" + mock_download.return_value = "Response" + + request = fake_request.HTTPRequest.blank( + '/vnf_packages/%s/package_content' % constants.UUID) + request.method = 'GET' + + request.headers["Range"] = 'bytes=10-20,21-30' + request.response = "" + + policy.set_rules(oslo_policy.Rules.from_dict(rules), overwrite=True) + ctx = context.Context('fake', 'fake', roles=roles) + + resp = request.get_response(fakes.wsgi_app_v1(fake_auth_context=ctx)) + + self.assertEqual(expected_status_code, resp.status_code) + + @ddt.data(*fakes.get_test_data_pkg_uploaded('fetch_artifact', + http_client.OK)) + @ddt.unpack + @mock.patch.object(controller.VnfPkgmController, "_get_csar_path") + @mock.patch.object(vnf_package.VnfPackage, "get_by_id") + def test_fetch_vnf_package_artifacts_enhanced_policy( + self, mock_vnf_by_id, mock_get_csar_path, + vnfd_updates, rules, roles, expected_status_code): + mock_vnf_by_id.return_value = fakes.return_vnfpkg_obj( + vnfd_updates=vnfd_updates) + base_path = os.path.dirname(os.path.abspath(__file__)) + extract_path = os.path.join(base_path, '../../etc/samples/' + 'sample_vnf_package_csar_in_meta_and_manifest') + mock_get_csar_path.return_value = extract_path + req = fake_request.HTTPRequest.blank( + '/vnf_packages/%s/artifacts/%s' + % (constants.UUID, constants.ARTIFACT_PATH)) + req.method = 'GET' + + policy.set_rules(oslo_policy.Rules.from_dict(rules), overwrite=True) + ctx = context.Context('fake', 'fake', roles=roles) + + resp = req.get_response(fakes.wsgi_app_v1(fake_auth_context=ctx)) + + self.assertEqual(expected_status_code, resp.status_code) + + @ddt.data(*fakes.get_test_data_pkg_index()) + @ddt.unpack + @mock.patch.object(VnfPackagesList, "get_by_marker_filter") + def test_index_enhanced_policy( + self, mock_vnf_list, pkg_list, rules, roles, expected_pkg_ids): + mock_vnf_list.return_value = pkg_list + req = fake_request.HTTPRequest.blank('/vnf_packages') + req.headers['Content-Type'] = 'application/json' + req.method = 'GET' + policy.set_rules(oslo_policy.Rules.from_dict(rules), overwrite=True) + ctx = context.Context('fake', 'fake', roles=roles) + + resp = req.get_response(fakes.wsgi_app_v1(fake_auth_context=ctx)) + self.assertEqual(http_client.OK, resp.status_code) + self.assertEqual( + expected_pkg_ids, [pkg.get('id') for pkg in resp.json]) diff --git a/tox.ini b/tox.ini index 98febbe83..22c158c01 100644 --- a/tox.ini +++ b/tox.ini @@ -101,6 +101,17 @@ setenv = {[testenv]setenv} commands = stestr --test-path=./tacker/tests/functional/sol_v2_az_retry run --slowest --concurrency 1 {posargs} +[testenv:dsvm-functional-enhanced-policy-sol] +setenv = {[testenv]setenv} + +commands = + stestr --test-path=./tacker/tests/functional/sol_enhanced_policy/sol run --slowest --concurrency 1 {posargs} + +[testenv:dsvm-functional-enhanced-policy-sol-kubernetes] +setenv = {[testenv]setenv} + +commands = + stestr --test-path=./tacker/tests/functional/sol_enhanced_policy/sol_kubernetes run --slowest --concurrency 1 {posargs} [testenv:dsvm-functional-sol-https-v2] setenv = {[testenv]setenv}