FT for multi tenant policy in LCM
This patch adds functional test cases to validate the subscription and vnf package functionality in a multi-tenant environment. Validates VNF instantiation is only allowed when VNF and VIM belongs to same tenants. The patch covers only positive test cases of the feature. Validation of negative test cases would require design changes in Fake NFVO server, which could be implemented in the upcoming cycle. Additionally, add missing "domain-name" field while creating VIM config file in tools/gen_vim_config.sh. Implement: blueprint multi-tenant-policy Change-Id: I57d2ec780bd65423820c291bc67d1328bcf9f620
This commit is contained in:
parent
1cb068c74a
commit
a039fa329e
10
.zuul.yaml
10
.zuul.yaml
@ -275,6 +275,15 @@
|
||||
controller-tacker:
|
||||
tox_envlist: dsvm-functional-sol-v2
|
||||
|
||||
- job:
|
||||
name: tacker-functional-devstack-multinode-sol-multi-tenant
|
||||
parent: tacker-functional-devstack-multinode-sol
|
||||
description: |
|
||||
Multinodes job for SOL Multi tenant devstack-based functional tests
|
||||
host-vars:
|
||||
controller-tacker:
|
||||
tox_envlist: dsvm-functional-sol-multi-tenant
|
||||
|
||||
- job:
|
||||
name: tacker-functional-devstack-multinode-sol-separated-nfvo
|
||||
parent: tacker-functional-devstack-multinode-sol
|
||||
@ -522,3 +531,4 @@
|
||||
- tacker-functional-devstack-multinode-sol-kubernetes
|
||||
- tacker-functional-devstack-multinode-libs-master
|
||||
- tacker-functional-devstack-multinode-sol-v2
|
||||
- tacker-functional-devstack-multinode-sol-multi-tenant
|
||||
|
@ -163,7 +163,7 @@ def _vnf_lcm_subscriptions_show(context, subscriptionId):
|
||||
|
||||
sql = text(
|
||||
"select "
|
||||
"t1.id,t1.callback_uri,t2.filter "
|
||||
"t1.id,t1.callback_uri,t1.tenant_id,t2.filter "
|
||||
"from vnf_lcm_subscriptions t1, "
|
||||
"(select distinct subscription_uuid,filter from vnf_lcm_filters) t2 "
|
||||
"where t1.id = t2.subscription_uuid "
|
||||
|
@ -0,0 +1,99 @@
|
||||
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: 3
|
||||
desired_capacity: 1
|
||||
resource:
|
||||
type: VDU1.yaml
|
||||
properties:
|
||||
flavor: { get_param: [ nfv, VDU, VDU1, flavor ] }
|
||||
image: { get_param: [ nfv, VDU, VirtualStorage, image ] }
|
||||
zone: { get_param: [ nfv, vdu, VDU1, zone ] }
|
||||
net1: { get_param: [ nfv, CP, VDU1_CP1, network ] }
|
||||
net2: { get_param: [ nfv, CP, VDU1_CP2, network ] }
|
||||
net3: { get_resource: extmanageNW_1 }
|
||||
net4: { get_resource: extmanageNW_2 }
|
||||
net5: { get_resource: internalNW_1 }
|
||||
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
|
||||
VDU2_scale:
|
||||
type: OS::Heat::AutoScalingGroup
|
||||
depends_on: VDU1_scale
|
||||
properties:
|
||||
min_size: 2
|
||||
max_size: 2
|
||||
desired_capacity: 2
|
||||
resource:
|
||||
type: VDU2.yaml
|
||||
properties:
|
||||
flavor: { get_param: [ nfv, VDU, VDU2, flavor ] }
|
||||
image: { get_param: [ nfv, VDU, VDU2, image ] }
|
||||
zone: { get_param: [ nfv, vdu, VDU2, zone ] }
|
||||
net1: { get_param: [ nfv, CP, VDU2_CP1, network ] }
|
||||
net2: { get_param: [ nfv, CP, VDU2_CP2, network ] }
|
||||
net3: { get_resource: extmanageNW_1 }
|
||||
net4: { get_resource: extmanageNW_2 }
|
||||
net5: { get_resource: internalNW_1 }
|
||||
VDU2_scale_scale_out:
|
||||
type: OS::Heat::ScalingPolicy
|
||||
properties:
|
||||
scaling_adjustment: 1
|
||||
auto_scaling_group_id:
|
||||
get_resource: VDU2_scale
|
||||
adjustment_type: change_in_capacity
|
||||
VDU2_scale_scale_in:
|
||||
type: OS::Heat::ScalingPolicy
|
||||
properties:
|
||||
scaling_adjustment: -1
|
||||
auto_scaling_group_id:
|
||||
get_resource: VDU2_scale
|
||||
adjustment_type: change_in_capacity
|
||||
extmanageNW_1:
|
||||
type: OS::Neutron::Net
|
||||
extmanageNW_2:
|
||||
type: OS::Neutron::Net
|
||||
internalNW_1:
|
||||
type: OS::Neutron::Net
|
||||
extmanageNW_1_subnet:
|
||||
type: OS::Neutron::Subnet
|
||||
properties:
|
||||
ip_version: 4
|
||||
network:
|
||||
get_resource: extmanageNW_1
|
||||
cidr: 192.168.3.0/24
|
||||
extmanageNW_2_subnet:
|
||||
type: OS::Neutron::Subnet
|
||||
properties:
|
||||
ip_version: 4
|
||||
network:
|
||||
get_resource: extmanageNW_2
|
||||
cidr: 192.168.4.0/24
|
||||
internalNW_1_subnet:
|
||||
type: OS::Neutron::Subnet
|
||||
properties:
|
||||
ip_version: 4
|
||||
network:
|
||||
get_resource: internalNW_1
|
||||
cidr: 192.168.5.0/24
|
||||
outputs: {}
|
@ -0,0 +1,72 @@
|
||||
heat_template_version: 2013-05-23
|
||||
description: 'VDU1 HOT for Sample VNF'
|
||||
|
||||
parameters:
|
||||
flavor:
|
||||
type: string
|
||||
image:
|
||||
type: string
|
||||
zone:
|
||||
type: string
|
||||
net1:
|
||||
type: string
|
||||
net2:
|
||||
type: string
|
||||
net3:
|
||||
type: string
|
||||
net4:
|
||||
type: string
|
||||
net5:
|
||||
type: string
|
||||
|
||||
resources:
|
||||
VDU1:
|
||||
type: OS::Nova::Server
|
||||
properties:
|
||||
flavor: { get_param: flavor }
|
||||
name: VDU1
|
||||
block_device_mapping_v2: [{"volume_id": { get_resource: VirtualStorage }}]
|
||||
networks:
|
||||
- port:
|
||||
get_resource: VDU1_CP1
|
||||
- port:
|
||||
get_resource: VDU1_CP2
|
||||
- port:
|
||||
get_resource: VDU1_CP3
|
||||
- port:
|
||||
get_resource: VDU1_CP4
|
||||
- port:
|
||||
get_resource: VDU1_CP5
|
||||
availability_zone: { get_param: zone }
|
||||
|
||||
VirtualStorage:
|
||||
type: OS::Cinder::Volume
|
||||
properties:
|
||||
image: { get_param: image }
|
||||
size: 1
|
||||
volume_type: { get_resource: multi }
|
||||
multi:
|
||||
type: OS::Cinder::VolumeType
|
||||
properties:
|
||||
name: { get_resource: VDU1_CP1 }
|
||||
metadata: { multiattach: "<is> True" }
|
||||
VDU1_CP1:
|
||||
type: OS::Neutron::Port
|
||||
properties:
|
||||
network: { get_param: net1 }
|
||||
VDU1_CP2:
|
||||
type: OS::Neutron::Port
|
||||
properties:
|
||||
network: { get_param: net2 }
|
||||
VDU1_CP3:
|
||||
type: OS::Neutron::Port
|
||||
properties:
|
||||
network: { get_param: net3 }
|
||||
VDU1_CP4:
|
||||
type: OS::Neutron::Port
|
||||
properties:
|
||||
network: { get_param: net4 }
|
||||
VDU1_CP5:
|
||||
type: OS::Neutron::Port
|
||||
properties:
|
||||
network: { get_param: net5 }
|
@ -0,0 +1,61 @@
|
||||
heat_template_version: 2013-05-23
|
||||
description: 'VDU2 HOT for Sample VNF'
|
||||
|
||||
parameters:
|
||||
flavor:
|
||||
type: string
|
||||
image:
|
||||
type: string
|
||||
zone:
|
||||
type: string
|
||||
net1:
|
||||
type: string
|
||||
net2:
|
||||
type: string
|
||||
net3:
|
||||
type: string
|
||||
net4:
|
||||
type: string
|
||||
net5:
|
||||
type: string
|
||||
|
||||
resources:
|
||||
VDU2:
|
||||
type: OS::Nova::Server
|
||||
properties:
|
||||
flavor: { get_param: flavor }
|
||||
name: VDU2
|
||||
image: { get_param: image }
|
||||
networks:
|
||||
- port:
|
||||
get_resource: VDU2_CP1
|
||||
- port:
|
||||
get_resource: VDU2_CP2
|
||||
- port:
|
||||
get_resource: VDU2_CP3
|
||||
- port:
|
||||
get_resource: VDU2_CP4
|
||||
- port:
|
||||
get_resource: VDU2_CP5
|
||||
availability_zone: { get_param: zone }
|
||||
|
||||
VDU2_CP1:
|
||||
type: OS::Neutron::Port
|
||||
properties:
|
||||
network: { get_param: net1 }
|
||||
VDU2_CP2:
|
||||
type: OS::Neutron::Port
|
||||
properties:
|
||||
network: { get_param: net2 }
|
||||
VDU2_CP3:
|
||||
type: OS::Neutron::Port
|
||||
properties:
|
||||
network: { get_param: net3 }
|
||||
VDU2_CP4:
|
||||
type: OS::Neutron::Port
|
||||
properties:
|
||||
network: errornetwork
|
||||
VDU2_CP5:
|
||||
type: OS::Neutron::Port
|
||||
properties:
|
||||
network: errornetwork
|
@ -0,0 +1,403 @@
|
||||
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
|
||||
- helloworld3_types.yaml
|
||||
|
||||
topology_template:
|
||||
inputs:
|
||||
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_external1_1: [ VDU1_CP1, virtual_link ]
|
||||
virtual_link_external1_2: [ VDU2_CP1, virtual_link ]
|
||||
virtual_link_external2_1: [ VDU1_CP2, virtual_link ]
|
||||
virtual_link_external2_2: [ VDU2_CP2, virtual_link ]
|
||||
|
||||
node_templates:
|
||||
VNF:
|
||||
type: company.provider.VNF
|
||||
properties:
|
||||
flavour_description: A simple flavour
|
||||
interfaces:
|
||||
Vnflcm:
|
||||
instantiate: []
|
||||
instantiate_start: []
|
||||
instantiate_end: []
|
||||
terminate: []
|
||||
terminate_start: []
|
||||
terminate_end: []
|
||||
modify_information: []
|
||||
modify_information_start: []
|
||||
modify_information_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: 3
|
||||
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
|
||||
requirements:
|
||||
- virtual_storage: VirtualStorage
|
||||
|
||||
VDU2:
|
||||
type: tosca.nodes.nfv.Vdu.Compute
|
||||
properties:
|
||||
name: VDU2
|
||||
description: VDU2 compute node
|
||||
vdu_profile:
|
||||
min_number_of_instances: 2
|
||||
max_number_of_instances: 2
|
||||
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: 0 GB
|
||||
min_ram: 256 MB
|
||||
size: 12 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
|
||||
|
||||
VirtualStorage:
|
||||
type: tosca.nodes.nfv.Vdu.VirtualBlockStorage
|
||||
properties:
|
||||
virtual_block_storage_data:
|
||||
size_of_storage: 1 GB
|
||||
rdma_enabled: true
|
||||
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: 0 GB
|
||||
min_ram: 256 MB
|
||||
size: 12 GB
|
||||
|
||||
VDU1_CP1:
|
||||
type: tosca.nodes.nfv.VduCp
|
||||
properties:
|
||||
layer_protocols: [ ipv4 ]
|
||||
order: 0
|
||||
requirements:
|
||||
- virtual_binding: VDU1
|
||||
|
||||
VDU1_CP2:
|
||||
type: tosca.nodes.nfv.VduCp
|
||||
properties:
|
||||
layer_protocols: [ ipv4 ]
|
||||
order: 1
|
||||
requirements:
|
||||
- virtual_binding: VDU1
|
||||
|
||||
VDU1_CP3:
|
||||
type: tosca.nodes.nfv.VduCp
|
||||
properties:
|
||||
layer_protocols: [ ipv4 ]
|
||||
order: 2
|
||||
requirements:
|
||||
- virtual_binding: VDU1
|
||||
- virtual_link: internalVL1
|
||||
|
||||
VDU1_CP4:
|
||||
type: tosca.nodes.nfv.VduCp
|
||||
properties:
|
||||
layer_protocols: [ ipv4 ]
|
||||
order: 3
|
||||
requirements:
|
||||
- virtual_binding: VDU1
|
||||
- virtual_link: internalVL2
|
||||
|
||||
VDU1_CP5:
|
||||
type: tosca.nodes.nfv.VduCp
|
||||
properties:
|
||||
layer_protocols: [ ipv4 ]
|
||||
order: 4
|
||||
requirements:
|
||||
- virtual_binding: VDU1
|
||||
- virtual_link: internalVL3
|
||||
|
||||
VDU2_CP1:
|
||||
type: tosca.nodes.nfv.VduCp
|
||||
properties:
|
||||
layer_protocols: [ ipv4 ]
|
||||
order: 0
|
||||
requirements:
|
||||
- virtual_binding: VDU2
|
||||
|
||||
VDU2_CP2:
|
||||
type: tosca.nodes.nfv.VduCp
|
||||
properties:
|
||||
layer_protocols: [ ipv4 ]
|
||||
order: 1
|
||||
requirements:
|
||||
- virtual_binding: VDU2
|
||||
|
||||
VDU2_CP3:
|
||||
type: tosca.nodes.nfv.VduCp
|
||||
properties:
|
||||
layer_protocols: [ ipv4 ]
|
||||
order: 2
|
||||
requirements:
|
||||
- virtual_binding: VDU2
|
||||
- virtual_link: internalVL1
|
||||
|
||||
VDU2_CP4:
|
||||
type: tosca.nodes.nfv.VduCp
|
||||
properties:
|
||||
layer_protocols: [ ipv4 ]
|
||||
order: 3
|
||||
requirements:
|
||||
- virtual_binding: VDU2
|
||||
- virtual_link: internalVL2
|
||||
|
||||
VDU2_CP5:
|
||||
type: tosca.nodes.nfv.VduCp
|
||||
properties:
|
||||
layer_protocols: [ ipv4 ]
|
||||
order: 4
|
||||
requirements:
|
||||
- virtual_binding: VDU2
|
||||
- virtual_link: internalVL3
|
||||
|
||||
internalVL1:
|
||||
type: tosca.nodes.nfv.VnfVirtualLink
|
||||
properties:
|
||||
connectivity_type:
|
||||
layer_protocols: [ ipv4 ]
|
||||
description: External Managed 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: 33.33.0.0/24
|
||||
|
||||
internalVL2:
|
||||
type: tosca.nodes.nfv.VnfVirtualLink
|
||||
properties:
|
||||
connectivity_type:
|
||||
layer_protocols: [ ipv4 ]
|
||||
description: External Managed 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: 33.34.0.0/24
|
||||
|
||||
internalVL3:
|
||||
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: 33.35.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 ]
|
||||
|
||||
- VDU2_initial_delta:
|
||||
type: tosca.policies.nfv.VduInitialDelta
|
||||
properties:
|
||||
initial_delta:
|
||||
number_of_instances: 2
|
||||
targets: [ VDU2 ]
|
||||
|
||||
- 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 ]
|
||||
|
||||
- VDU2_instantiation_levels:
|
||||
type: tosca.policies.nfv.VduInstantiationLevels
|
||||
properties:
|
||||
levels:
|
||||
instantiation_level_1:
|
||||
number_of_instances: 2
|
||||
instantiation_level_2:
|
||||
number_of_instances: 2
|
||||
targets: [ VDU2 ]
|
||||
|
||||
- internalVL1_instantiation_levels:
|
||||
type: tosca.policies.nfv.VirtualLinkInstantiationLevels
|
||||
properties:
|
||||
levels:
|
||||
instantiation_level_1:
|
||||
bitrate_requirements:
|
||||
root: 1048576
|
||||
leaf: 1048576
|
||||
instantiation_level_2:
|
||||
bitrate_requirements:
|
||||
root: 1048576
|
||||
leaf: 1048576
|
||||
targets: [ internalVL1 ]
|
||||
|
||||
- internalVL2_instantiation_levels:
|
||||
type: tosca.policies.nfv.VirtualLinkInstantiationLevels
|
||||
properties:
|
||||
levels:
|
||||
instantiation_level_1:
|
||||
bitrate_requirements:
|
||||
root: 1048576
|
||||
leaf: 1048576
|
||||
instantiation_level_2:
|
||||
bitrate_requirements:
|
||||
root: 1048576
|
||||
leaf: 1048576
|
||||
targets: [ internalVL2 ]
|
||||
|
||||
- internalVL3_instantiation_levels:
|
||||
type: tosca.policies.nfv.VirtualLinkInstantiationLevels
|
||||
properties:
|
||||
levels:
|
||||
instantiation_level_1:
|
||||
bitrate_requirements:
|
||||
root: 1048576
|
||||
leaf: 1048576
|
||||
instantiation_level_2:
|
||||
bitrate_requirements:
|
||||
root: 1048576
|
||||
leaf: 1048576
|
||||
targets: [ internalVL3 ]
|
||||
|
||||
- policy_antiaffinity_vdu1:
|
||||
type: tosca.policies.nfv.AntiAffinityRule
|
||||
targets: [ VDU1 ]
|
||||
properties:
|
||||
scope: zone
|
||||
|
||||
- policy_antiaffinity_vdu2:
|
||||
type: tosca.policies.nfv.AntiAffinityRule
|
||||
targets: [ VDU2 ]
|
||||
properties:
|
||||
scope: zone
|
@ -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
|
||||
- helloworld3_types.yaml
|
||||
- helloworld3_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: b1bb0ce7-ebca-4fa7-95ed-4840d7000000
|
||||
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
|
@ -0,0 +1,55 @@
|
||||
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:
|
||||
descriptor_id:
|
||||
type: string
|
||||
constraints: [ valid_values: [ b1bb0ce7-ebca-4fa7-95ed-4840d7000000 ] ]
|
||||
default: b1bb0ce7-ebca-4fa7-95ed-4840d7000000
|
||||
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: "falvour"
|
||||
requirements:
|
||||
- virtual_link_external1:
|
||||
capability: tosca.capabilities.nfv.VirtualLinkable
|
||||
- virtual_link_external2:
|
||||
capability: tosca.capabilities.nfv.VirtualLinkable
|
||||
- virtual_link_internal:
|
||||
capability: tosca.capabilities.nfv.VirtualLinkable
|
||||
interfaces:
|
||||
Vnflcm:
|
||||
type: tosca.interfaces.nfv.Vnflcm
|
@ -0,0 +1,4 @@
|
||||
TOSCA-Meta-File-Version: 1.0
|
||||
CSAR-Version: 1.1
|
||||
Created-by: Onboarding portal
|
||||
Entry-Definitions: Definitions/helloworld3_top.vnfd.yaml
|
@ -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}
|
@ -454,10 +454,12 @@ class BaseTackerTest(base.BaseTestCase):
|
||||
|
||||
return vnf_instance, tosca_dict
|
||||
|
||||
def _list_op_occs(self, filter_string=''):
|
||||
def _list_op_occs(self, filter_string='', http_client=None):
|
||||
if http_client is None:
|
||||
http_client = self.http_client
|
||||
show_url = os.path.join(
|
||||
self.base_vnf_lcm_op_occs_url)
|
||||
resp, response_body = self.http_client.do_request(
|
||||
resp, response_body = http_client.do_request(
|
||||
show_url + filter_string, "GET")
|
||||
return resp, response_body
|
||||
|
||||
|
@ -206,6 +206,8 @@ class FakeServerManager(object):
|
||||
"""Manager class to manage dummy server setting and control"""
|
||||
|
||||
SERVER_PORT = 9990
|
||||
SERVER_PORT_T1 = 9995
|
||||
SERVER_PORT_T2 = 9996
|
||||
SERVER_INVOKE_CHECK_INTERVAL = 10
|
||||
|
||||
def __init__(self):
|
||||
|
@ -209,6 +209,12 @@ def _create_instantiate_vnf_request_body(flavour_id,
|
||||
class BaseVnfLcmTest(base.BaseTackerTest):
|
||||
|
||||
is_setup_error = False
|
||||
# NOTE: If prepare_fake_server is set(by default) then this base
|
||||
# class will prepare(create and start) the fake http server which
|
||||
# takes time and can add up more time in gate CI. Any child class
|
||||
# can set it false. For example, BaseVnfLcmMultiTenantTest which
|
||||
# create their own servers for two different tenants.
|
||||
prepare_fake_server = True
|
||||
|
||||
@classmethod
|
||||
def setUpClass(cls):
|
||||
@ -217,13 +223,15 @@ class BaseVnfLcmTest(base.BaseTackerTest):
|
||||
we set up fake NFVO server for test at here.
|
||||
'''
|
||||
super(BaseVnfLcmTest, cls).setUpClass()
|
||||
cls._prepare_start_fake_server(FAKE_SERVER_MANAGER,
|
||||
if cls.prepare_fake_server:
|
||||
cls._prepare_start_fake_server(FAKE_SERVER_MANAGER,
|
||||
FAKE_SERVER_PORT)
|
||||
|
||||
@classmethod
|
||||
def tearDownClass(cls):
|
||||
super(BaseVnfLcmTest, cls).tearDownClass()
|
||||
FAKE_SERVER_MANAGER.stop_server()
|
||||
if cls.prepare_fake_server:
|
||||
FAKE_SERVER_MANAGER.stop_server()
|
||||
|
||||
def setUp(self):
|
||||
super(BaseVnfLcmTest, self).setUp()
|
||||
@ -231,10 +239,10 @@ class BaseVnfLcmTest(base.BaseTackerTest):
|
||||
if self.is_setup_error:
|
||||
self.fail("Faild, not exists pre-registered image.")
|
||||
|
||||
callback_url = os.path.join(
|
||||
MOCK_NOTIFY_CALLBACK_URL,
|
||||
self._testMethodName)
|
||||
self._clear_history_and_set_callback(FAKE_SERVER_MANAGER,
|
||||
if self.prepare_fake_server:
|
||||
callback_url = os.path.join(MOCK_NOTIFY_CALLBACK_URL,
|
||||
self._testMethodName)
|
||||
self._clear_history_and_set_callback(FAKE_SERVER_MANAGER,
|
||||
callback_url)
|
||||
|
||||
self.tacker_client = base.BaseTackerTest.tacker_http_client()
|
||||
|
118
tacker/tests/functional/sol_multi_tenant/vnflcm/base.py
Normal file
118
tacker/tests/functional/sol_multi_tenant/vnflcm/base.py
Normal file
@ -0,0 +1,118 @@
|
||||
#
|
||||
# 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 tacker.tests.functional import base
|
||||
from tacker.tests.functional.common.fake_server import FakeServerManager
|
||||
from tacker.tests.functional.sol.vnflcm import base as vnflcm_base
|
||||
|
||||
|
||||
FAKE_SERVER_MANAGER_T1 = FakeServerManager()
|
||||
FAKE_SERVER_PORT_T1 = 9995
|
||||
FAKE_SERVER_MANAGER_T2 = FakeServerManager()
|
||||
FAKE_SERVER_PORT_T2 = 9996
|
||||
|
||||
|
||||
class BaseVnfLcmMultiTenantTest(vnflcm_base.BaseVnfLcmTest):
|
||||
|
||||
prepare_fake_server = False
|
||||
|
||||
@classmethod
|
||||
def setUpClass(cls):
|
||||
super(BaseVnfLcmMultiTenantTest, cls).setUpClass()
|
||||
|
||||
result = cls.get_openstack_client_session(
|
||||
vim_conf_file='local-tenant1-vim.yaml')
|
||||
cls.client_tenant1 = result.get('client')
|
||||
cls.http_client_tenant1 = result.get('http_client')
|
||||
cls.h_client_tenant1 = result.get('h_client')
|
||||
cls.glance_client_tenant1 = result.get('glance_client')
|
||||
|
||||
result = cls.get_openstack_client_session(
|
||||
vim_conf_file='local-tenant2-vim.yaml')
|
||||
cls.client_tenant2 = result.get('client')
|
||||
cls.http_client_tenant2 = result.get('http_client')
|
||||
cls.h_client_tenant2 = result.get('h_client')
|
||||
cls.glance_client_tenant2 = result.get('glance_client')
|
||||
|
||||
cls.tacker_client_t1 = base.BaseTackerTest.tacker_http_client(
|
||||
'local-tenant1-vim.yaml')
|
||||
cls.tacker_client_t2 = base.BaseTackerTest.tacker_http_client(
|
||||
'local-tenant2-vim.yaml')
|
||||
|
||||
# Set up fake NFVO server for tenant1 and tenant2
|
||||
cls.servers = {FAKE_SERVER_PORT_T1: FAKE_SERVER_MANAGER_T1,
|
||||
FAKE_SERVER_PORT_T2: FAKE_SERVER_MANAGER_T2}
|
||||
# NOTE: Create both server in parallel, otherwise they can
|
||||
# cause (especially server start) job timeout.
|
||||
for port, manager in cls.servers.items():
|
||||
cls._prepare_start_fake_server(manager, port)
|
||||
|
||||
@classmethod
|
||||
def tearDownClass(cls):
|
||||
super(BaseVnfLcmMultiTenantTest, cls).tearDownClass()
|
||||
for _, manager in cls.servers.items():
|
||||
manager.stop_server()
|
||||
|
||||
def setUp(self):
|
||||
super(BaseVnfLcmMultiTenantTest, self).setUp()
|
||||
self.base_url = "/vnfpkgm/v1/vnf_packages"
|
||||
|
||||
callback_url = os.path.join(
|
||||
vnflcm_base.MOCK_NOTIFY_CALLBACK_URL,
|
||||
self._testMethodName)
|
||||
self._clear_history_and_set_callback(FAKE_SERVER_MANAGER_T1,
|
||||
callback_url)
|
||||
self._clear_history_and_set_callback(FAKE_SERVER_MANAGER_T2,
|
||||
callback_url)
|
||||
|
||||
vim_list = self.client.list_vims()
|
||||
self.vim_tenant1 = self.get_vim(vim_list, 'VIM_TEST')
|
||||
if not self.vim_tenant1:
|
||||
assert False, "vim_list is Empty: Tenant VIM is missing"
|
||||
self.vim_tenant2 = self.get_vim(vim_list, 'VIM_DEMO')
|
||||
if not self.vim_tenant2:
|
||||
assert False, "vim_list is Empty: Tenant VIM is missing"
|
||||
|
||||
result = self._create_network_settings()
|
||||
self.ext_networks_tenant1 = result.get('ext_networks')
|
||||
self.ext_vl_tenant1 = result.get('ext_vl')
|
||||
self.ext_mngd_networks_tenant1 = result.get('ext_mngd_networks')
|
||||
self.ext_link_ports_tenant1 = result.get('ext_link_ports')
|
||||
self.ext_subnets_tenant1 = result.get('ext_subnets')
|
||||
self.changed_ext_networks_tenant1 = result.get(
|
||||
'changed_ext_networks')
|
||||
self.changed_ext_subnets_tenant1 = result.get(
|
||||
'changed_ext_subnets')
|
||||
|
||||
result = self._create_network_settings()
|
||||
self.ext_networks_tenant2 = result.get('ext_networks')
|
||||
self.ext_vl_tenant2 = result.get('ext_vl')
|
||||
self.ext_mngd_networks_tenant2 = result.get('ext_mngd_networks')
|
||||
self.ext_link_ports_tenant2 = result.get('ext_link_ports')
|
||||
self.ext_subnets_tenant2 = result.get('ext_subnets')
|
||||
self.changed_ext_networks_tenant2 = result.get(
|
||||
'changed_ext_networks')
|
||||
self.changed_ext_subnets_tenant2 = result.get(
|
||||
'changed_ext_subnets')
|
||||
|
||||
@classmethod
|
||||
def get_openstack_client_session(cls, vim_conf_file):
|
||||
client = base.BaseTackerTest.tackerclient(vim_conf_file)
|
||||
http_client = base.BaseTackerTest.tacker_http_client(vim_conf_file)
|
||||
h_client = base.BaseTackerTest.heatclient(vim_conf_file)
|
||||
glance_client = base.BaseTackerTest.glanceclient(vim_conf_file)
|
||||
return {'client': client,
|
||||
'http_client': http_client,
|
||||
'h_client': h_client,
|
||||
'glance_client': glance_client}
|
252
tacker/tests/functional/sol_multi_tenant/vnflcm/fake_vnflcm.py
Normal file
252
tacker/tests/functional/sol_multi_tenant/vnflcm/fake_vnflcm.py
Normal file
@ -0,0 +1,252 @@
|
||||
#
|
||||
# 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 import uuidsentinel
|
||||
|
||||
|
||||
class Subscription:
|
||||
|
||||
@staticmethod
|
||||
def make_create_request_body(callback_uri):
|
||||
"""Parameter selection policy.
|
||||
|
||||
Set all Notification types and all life cycle types for filter.
|
||||
Specify OAuth2 for authentication → do not set authentication.
|
||||
|
||||
Args:
|
||||
callback_uri (str): Notification URI.
|
||||
|
||||
Returns:
|
||||
dict: Request body
|
||||
"""
|
||||
return {
|
||||
"filter": {
|
||||
"vnfInstanceSubscriptionFilter": {
|
||||
"vnfdIds": ["b1bb0ce7-ebca-4fa7-95ed-4840d7000000"],
|
||||
"vnfProductsFromProviders": [{
|
||||
"vnfProvider": "Company",
|
||||
"vnfProducts": [
|
||||
{
|
||||
"vnfProductName": "Sample VNF",
|
||||
"versions": [
|
||||
{
|
||||
"vnfSoftwareVersion": "1.0",
|
||||
"vnfdVersions": ["1.0"]
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}]
|
||||
},
|
||||
"notificationTypes": [
|
||||
"VnfLcmOperationOccurrenceNotification",
|
||||
"VnfIdentifierCreationNotification",
|
||||
"VnfIdentifierDeletionNotification"
|
||||
],
|
||||
"operationTypes": [
|
||||
"INSTANTIATE",
|
||||
"SCALE",
|
||||
"TERMINATE",
|
||||
"HEAL",
|
||||
"MODIFY_INFO",
|
||||
"CHANGE_EXT_CONN"
|
||||
],
|
||||
"operationStates": ["STARTING"]
|
||||
},
|
||||
"callbackUri": callback_uri
|
||||
}
|
||||
|
||||
|
||||
ext_vdu1_cp1 = {
|
||||
"cpdId": "VDU1_CP1",
|
||||
"cpConfig": [{
|
||||
"linkPortId": uuidsentinel.elp1_id
|
||||
}],
|
||||
}
|
||||
ext_vdu2_cp1 = {
|
||||
"cpdId": "VDU2_CP1",
|
||||
"cpConfig": [{
|
||||
"linkPortId": uuidsentinel.elp2_id
|
||||
}]
|
||||
}
|
||||
|
||||
|
||||
def _set_ext_link_port1(external_ports_id):
|
||||
ext_link_port1 = {
|
||||
"id": uuidsentinel.elp1_id,
|
||||
"resourceHandle": {
|
||||
"vimConnectionId": uuidsentinel.vim_connection_id,
|
||||
"resourceId": external_ports_id[0]
|
||||
}
|
||||
}
|
||||
return ext_link_port1
|
||||
|
||||
|
||||
def _set_ext_link_port2(external_ports_id):
|
||||
ext_link_port2 = {
|
||||
"id": uuidsentinel.elp2_id,
|
||||
"resourceHandle": {
|
||||
"vimConnectionId": uuidsentinel.vim_connection_id,
|
||||
"resourceId": external_ports_id[1]
|
||||
}
|
||||
}
|
||||
return ext_link_port2
|
||||
|
||||
|
||||
def _set_ext_virtual_link_cp1(networks_id, external_ports_id):
|
||||
ext_virtual_link_cp1 = {
|
||||
"id": uuidsentinel.evl1_id,
|
||||
"resourceId": networks_id[0],
|
||||
"vimConnectionId": uuidsentinel.vim_connection_id,
|
||||
"extCps": [ext_vdu1_cp1, ext_vdu2_cp1],
|
||||
"extLinkPorts": [
|
||||
_set_ext_link_port1(external_ports_id),
|
||||
_set_ext_link_port2(external_ports_id)]
|
||||
}
|
||||
return ext_virtual_link_cp1
|
||||
|
||||
|
||||
def _set_ext_cps_vdu1_cp2(external_subnets_id):
|
||||
ext_cps_vdu1_cp2 = {
|
||||
"cpdId": "VDU1_CP2",
|
||||
"cpConfig": [{
|
||||
"cpProtocolData": [{
|
||||
"layerProtocol": "IP_OVER_ETHERNET",
|
||||
"ipOverEthernet": {
|
||||
"ipAddresses": [{
|
||||
"type": "IPV4",
|
||||
"fixedAddresses": ["22.22.1.10"],
|
||||
"subnetId": external_subnets_id[1]
|
||||
}]
|
||||
}
|
||||
}]
|
||||
}]
|
||||
}
|
||||
return ext_cps_vdu1_cp2
|
||||
|
||||
|
||||
def _set_ext_cps_vdu2_cp2(external_subnets_id):
|
||||
ext_cps_vdu2_cp2 = {
|
||||
"cpdId": "VDU2_CP2",
|
||||
"cpConfig": [{
|
||||
"cpProtocolData": [{
|
||||
"layerProtocol": "IP_OVER_ETHERNET",
|
||||
"ipOverEthernet": {
|
||||
"ipAddresses": [{
|
||||
"type": "IPV4",
|
||||
"fixedAddresses": ["22.22.1.20"],
|
||||
"subnetId": external_subnets_id[1]
|
||||
}]
|
||||
}
|
||||
}]
|
||||
}]
|
||||
}
|
||||
return ext_cps_vdu2_cp2
|
||||
|
||||
|
||||
def _set_ext_virtual_link_cp2(networks_id, external_subnets_id):
|
||||
ext_virtual_link_cp2 = {
|
||||
"id": uuidsentinel.evl2_id,
|
||||
"resourceId": networks_id[1],
|
||||
"vimConnectionId": uuidsentinel.vim_connection_id,
|
||||
"extCps": [
|
||||
_set_ext_cps_vdu1_cp2(external_subnets_id),
|
||||
_set_ext_cps_vdu2_cp2(external_subnets_id)
|
||||
]
|
||||
}
|
||||
return ext_virtual_link_cp2
|
||||
|
||||
|
||||
def _set_ext_mng_vtl_lnks(ext_mngd_networks_id):
|
||||
ext_mng_vtl_lnks = [{
|
||||
"id": uuidsentinel.emvl1_id,
|
||||
"vnfVirtualLinkDescId": "internalVL1",
|
||||
"resourceId": ext_mngd_networks_id[0]
|
||||
}, {
|
||||
"id": uuidsentinel.emvl2_id,
|
||||
"vnfVirtualLinkDescId": "internalVL2",
|
||||
"resourceId": ext_mngd_networks_id[1]
|
||||
}]
|
||||
return ext_mng_vtl_lnks
|
||||
|
||||
|
||||
class VnfInstances:
|
||||
|
||||
@staticmethod
|
||||
def make_create_request_body(vnfd_id):
|
||||
return {
|
||||
"vnfdId": vnfd_id,
|
||||
"vnfInstanceName": "",
|
||||
"vnfInstanceDescription": "Sample VNF",
|
||||
"metadata": {
|
||||
"samplekey": "samplevalue"
|
||||
}
|
||||
}
|
||||
|
||||
@staticmethod
|
||||
def make_inst_request_body(
|
||||
user_name,
|
||||
tenant_id,
|
||||
networks_id,
|
||||
ext_mngd_networks_id,
|
||||
external_ports_id,
|
||||
external_subnets_id):
|
||||
data = {
|
||||
"flavourId": "simple",
|
||||
"instantiationLevelId": "instantiation_level_1",
|
||||
"extVirtualLinks": [
|
||||
_set_ext_virtual_link_cp1(
|
||||
networks_id, external_ports_id),
|
||||
_set_ext_virtual_link_cp2(
|
||||
networks_id, external_subnets_id)
|
||||
],
|
||||
"extManagedVirtualLinks": _set_ext_mng_vtl_lnks(
|
||||
ext_mngd_networks_id),
|
||||
"vimConnectionInfo": [{
|
||||
"id": uuidsentinel.vim_connection_id,
|
||||
"vimType": "ETSINFV.OPENSTACK_KEYSTONE.v_2",
|
||||
"vimConnectionId": uuidsentinel.vim_connection_id,
|
||||
"interfaceInfo": {
|
||||
"endpoint": "http://127.0.0.1/identity"
|
||||
},
|
||||
"accessInfo": {
|
||||
"username": user_name,
|
||||
"region": "RegionOne",
|
||||
"password": "devstack",
|
||||
"tenant": tenant_id
|
||||
}
|
||||
}],
|
||||
"additionalParams": {
|
||||
"lcm-operation-user-data": "./UserData/lcm_user_data.py",
|
||||
"lcm-operation-user-data-class": "SampleUserData"
|
||||
}
|
||||
}
|
||||
|
||||
return data
|
||||
|
||||
@staticmethod
|
||||
def make_term_request_body():
|
||||
"""Parameter selection policy.
|
||||
|
||||
As all parameters are set, GRACEFUL is specified for terminationType.
|
||||
(to specify gracefulTerminationTimeout)
|
||||
|
||||
Returns:
|
||||
dict: Request body
|
||||
"""
|
||||
return {
|
||||
"terminationType": "GRACEFUL",
|
||||
"gracefulTerminationTimeout": 1,
|
||||
"additionalParams": {
|
||||
"samplekey": "samplevalue"
|
||||
}
|
||||
}
|
@ -0,0 +1,455 @@
|
||||
#
|
||||
# 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 tacker.objects import fields
|
||||
from tacker.tests.functional.sol.vnflcm import base as vnflcm_base
|
||||
from tacker.tests.functional.sol_multi_tenant.vnflcm import base
|
||||
from tacker.tests.functional.sol_multi_tenant.vnflcm import fake_vnflcm
|
||||
import tempfile
|
||||
import time
|
||||
|
||||
|
||||
class VnfLcmWithMultiTenant(base.BaseVnfLcmMultiTenantTest):
|
||||
|
||||
VNF_PACKAGE_DELETE_TIMEOUT = 120
|
||||
|
||||
@classmethod
|
||||
def setUpClass(cls):
|
||||
super(VnfLcmWithMultiTenant, cls).setUpClass()
|
||||
|
||||
# ModifyVNF specific image create.
|
||||
is_setup_error_tenant1 = cls._modify_vnf_specific_image_create(
|
||||
cls.glance_client_tenant1)
|
||||
if is_setup_error_tenant1:
|
||||
cls.is_setup_error = True
|
||||
return
|
||||
|
||||
is_setup_error_tenant2 = cls._modify_vnf_specific_image_create(
|
||||
cls.glance_client_tenant2)
|
||||
if is_setup_error_tenant2:
|
||||
cls.is_setup_error = True
|
||||
return
|
||||
|
||||
@classmethod
|
||||
def _modify_vnf_specific_image_create(cls, glance_clt):
|
||||
is_setup_error = False
|
||||
images = cls._list_glance_image()
|
||||
if len(images) == 0:
|
||||
is_setup_error = True
|
||||
return is_setup_error
|
||||
|
||||
for image in images:
|
||||
specific_image_name = f'{image.name}{2}'
|
||||
image_data = {
|
||||
"min_disk": image.min_disk,
|
||||
"min_ram": image.min_ram,
|
||||
"disk_format": image.disk_format,
|
||||
"container_format": image.container_format,
|
||||
"visibility": image.visibility,
|
||||
"name": specific_image_name}
|
||||
|
||||
try:
|
||||
images = cls._list_glance_image(specific_image_name)
|
||||
if len(images) == 1:
|
||||
break
|
||||
|
||||
_, body = glance_clt.http_client.get(
|
||||
f'{glance_clt.http_client.get_endpoint()}{image.file}')
|
||||
|
||||
with tempfile.TemporaryFile('w+b') as f:
|
||||
for content in body:
|
||||
f.write(content)
|
||||
cls._create_glance_image(image_data, f.read())
|
||||
except Exception as e:
|
||||
print("Fail, Modify-VNF specific image create.", e, flush=True)
|
||||
is_setup_error = True
|
||||
return is_setup_error
|
||||
|
||||
return is_setup_error
|
||||
|
||||
def _wait_show_subscription(self, subscription_id, tacker_client):
|
||||
# wait for subscription creation
|
||||
timeout = vnflcm_base.VNF_SUBSCRIPTION_TIMEOUT
|
||||
start_time = int(time.time())
|
||||
while True:
|
||||
resp, body = self._show_subscription(subscription_id,
|
||||
tacker_client)
|
||||
if resp.ok or resp.status_code == 404:
|
||||
return resp, body
|
||||
|
||||
if ((int(time.time()) - start_time) > timeout):
|
||||
if resp:
|
||||
resp.raise_for_status()
|
||||
raise TimeoutError("Failed to show_subscription")
|
||||
|
||||
time.sleep(1)
|
||||
|
||||
def _delete_vnf_package(self, package_uuid, http_client):
|
||||
url = os.path.join(self.base_url, package_uuid)
|
||||
resp, _ = http_client.do_request(url, "DELETE")
|
||||
self.assertEqual(204, resp.status_code)
|
||||
|
||||
def _wait_for_delete(self, package_uuid, http_client):
|
||||
show_url = os.path.join(self.base_url, package_uuid)
|
||||
timeout = self.VNF_PACKAGE_DELETE_TIMEOUT
|
||||
start_time = int(time.time())
|
||||
while True:
|
||||
resp, body = http_client.do_request(show_url, "GET")
|
||||
if (404 == resp.status_code):
|
||||
return resp, body
|
||||
|
||||
if (int(time.time()) - start_time) > timeout:
|
||||
raise Exception("Failed to delete package")
|
||||
time.sleep(1)
|
||||
|
||||
def _disable_operational_state(self, package_uuid, http_client):
|
||||
update_req_body = jsonutils.dumps({
|
||||
"operationalState": "DISABLED"})
|
||||
|
||||
resp, _ = http_client.do_request(
|
||||
'{base_path}/{id}'.format(id=package_uuid,
|
||||
base_path=self.base_url),
|
||||
"PATCH", content_type='application/json',
|
||||
body=update_req_body)
|
||||
self.assertEqual(200, resp.status_code)
|
||||
|
||||
def assert_vnf_package_usage_state(
|
||||
self,
|
||||
vnf_package_info,
|
||||
expected_usage_state=fields.PackageUsageStateType.IN_USE):
|
||||
self.assertEqual(
|
||||
expected_usage_state,
|
||||
vnf_package_info['usageState'])
|
||||
|
||||
def assert_create_vnf(self, resp, vnf_instance, vnf_pkg_id,
|
||||
tacker_client, fake_server_manager):
|
||||
super().assert_create_vnf(resp, vnf_instance,
|
||||
fake_server_manager)
|
||||
|
||||
resp, vnf_pkg_info = vnflcm_base._show_vnf_package(
|
||||
tacker_client, vnf_pkg_id)
|
||||
self.assert_vnf_package_usage_state(vnf_pkg_info)
|
||||
|
||||
def _vnf_instance_wait_until_fail_detected(self, id,
|
||||
instantiation_state=fields.VnfInstanceState.NOT_INSTANTIATED,
|
||||
timeout=vnflcm_base.VNF_INSTANTIATE_ERROR_WAIT):
|
||||
time.sleep(timeout)
|
||||
_, body = self._show_vnf_instance(id)
|
||||
if body['instantiationState'] != instantiation_state:
|
||||
error = ("Vnf instance %(id)s status is %(current)s, "
|
||||
"expected status should be %(expected)s")
|
||||
self.fail(error % {"id": id,
|
||||
"current": body['instantiationState'],
|
||||
"expected": instantiation_state})
|
||||
|
||||
def test_subscription_functionality(self):
|
||||
"""Test subscription operations with member role users.
|
||||
|
||||
In this test case, we do following steps.
|
||||
Note: User A belongs to Tenant 1(t1).
|
||||
User B belongs to Tenant 2(t2).
|
||||
- Create subscription.
|
||||
- User A registers Subscription A(Notification Server A).
|
||||
- User B registers Subscription B(Notification Server B).
|
||||
- Show Subscription
|
||||
- User A only gets information about Subscription A.
|
||||
- User B only gets information about Subscription B.
|
||||
- List Subscription
|
||||
- User A gets subscription list and confirms only
|
||||
Subscription A is output.
|
||||
- User B gets subscription list and confirms only
|
||||
Subscription B is output.
|
||||
- Delete Subscription
|
||||
- User A deletes Subscription A.
|
||||
- User B deletes Subscription B.
|
||||
TODO(manpreetk): Only positive test cases are validated in
|
||||
Y-release.
|
||||
Negative test cases
|
||||
- User A fails to delete Subscription B.
|
||||
- User B fails to delete Subscription A.
|
||||
Validation of negative test cases would require design changes
|
||||
in Fake NFVO server, which could be implemented in the upcoming
|
||||
cycle.
|
||||
"""
|
||||
# Create subscription
|
||||
# User A registers Subscription A.
|
||||
callback_url = os.path.join(vnflcm_base.MOCK_NOTIFY_CALLBACK_URL,
|
||||
self._testMethodName)
|
||||
req_body = fake_vnflcm.Subscription.make_create_request_body(
|
||||
'http://localhost:{}{}'.format(
|
||||
base.FAKE_SERVER_MANAGER_T1.SERVER_PORT_T1,
|
||||
callback_url))
|
||||
resp_t1, resp_body_t1 = self._register_subscription(req_body,
|
||||
self.http_client_tenant1)
|
||||
self.assertEqual(201, resp_t1.status_code)
|
||||
self.assert_http_header_location_for_subscription(
|
||||
resp_t1.headers)
|
||||
self.assert_notification_get(callback_url,
|
||||
base.FAKE_SERVER_MANAGER_T1)
|
||||
subscription_id_t1 = resp_body_t1.get('id')
|
||||
|
||||
# User B registers Subscription B
|
||||
callback_url = os.path.join(vnflcm_base.MOCK_NOTIFY_CALLBACK_URL,
|
||||
self._testMethodName)
|
||||
req_body_t2 = fake_vnflcm.Subscription.make_create_request_body(
|
||||
'http://localhost:{}{}'.format(
|
||||
base.FAKE_SERVER_MANAGER_T2.SERVER_PORT_T2,
|
||||
callback_url))
|
||||
resp_t2, resp_body_t2 = self._register_subscription(
|
||||
req_body_t2, self.http_client_tenant2)
|
||||
self.assertEqual(201, resp_t2.status_code)
|
||||
self.assert_http_header_location_for_subscription(
|
||||
resp_t2.headers)
|
||||
self.assert_notification_get(callback_url,
|
||||
base.FAKE_SERVER_MANAGER_T2)
|
||||
subscription_id_t2 = resp_body_t2.get('id')
|
||||
|
||||
# Show Subscription
|
||||
# User A gets information for Subscription A
|
||||
resp_t1, resp_body_show_t1 = self._wait_show_subscription(
|
||||
subscription_id_t1, self.tacker_client_t1)
|
||||
self.assert_subscription_show(resp_t1, resp_body_show_t1)
|
||||
|
||||
# User B gets information for Subscription B
|
||||
resp_t2, resp_body_show_t2 = self._wait_show_subscription(
|
||||
subscription_id_t2, self.tacker_client_t2)
|
||||
self.assert_subscription_show(resp_t2, resp_body_show_t2)
|
||||
|
||||
# List Subscription
|
||||
# User A gets subscription list
|
||||
resp, _ = self._list_subscription(self.tacker_client_t1)
|
||||
self.assertEqual(200, resp_t1.status_code)
|
||||
|
||||
# Confirm subscription A
|
||||
filter_expr = {
|
||||
'filter': "filter=(eq,id,{})".format(
|
||||
resp_body_show_t1.get('id'))}
|
||||
resp, subscription_body_t1 = self._list_subscription_filter(
|
||||
self.http_client_tenant1,
|
||||
params=filter_expr)
|
||||
self.assertEqual(200, resp.status_code)
|
||||
self.assertEqual(1, len(subscription_body_t1))
|
||||
|
||||
# User B gets subscription list
|
||||
resp_t2, _ = self._list_subscription(
|
||||
self.tacker_client_t2)
|
||||
self.assertEqual(200, resp_t2.status_code)
|
||||
|
||||
# Confirm subscription B
|
||||
filter_expr = {
|
||||
'filter': "filter=(eq,id,{})".format(
|
||||
resp_body_show_t2.get('id'))}
|
||||
resp, subscription_body_t2 = self._list_subscription_filter(
|
||||
self.http_client_tenant2,
|
||||
params=filter_expr)
|
||||
self.assertEqual(200, resp.status_code)
|
||||
self.assertEqual(1, len(subscription_body_t2))
|
||||
|
||||
# Delete subscription
|
||||
# User A deletes Subscription A
|
||||
self.addCleanup(self._delete_subscription,
|
||||
subscription_id_t1, self.tacker_client_t1)
|
||||
|
||||
# User B deletes Subscription B
|
||||
self.addCleanup(self._delete_subscription,
|
||||
subscription_id_t2, self.tacker_client_t2)
|
||||
|
||||
def test_vnf_package_functionality(self):
|
||||
"""Test VNF package operations with member role users.
|
||||
|
||||
In this test case, we do following steps.
|
||||
Note: User A belongs to Tenant 1.
|
||||
User B belongs to Tenant 2.
|
||||
- Create and Upload VNF Package
|
||||
- User A creates VNF Package A.
|
||||
- User A uploads VNF Package A.
|
||||
- User B creates VNF Package B.
|
||||
- User B uploads VNF Package B.
|
||||
- List VNF Package
|
||||
- User A gets VNF package list and confirms only
|
||||
VNF Package A is output.
|
||||
- User B gets VNF package list and confirms only
|
||||
VNF Package B is output.
|
||||
- Show VNF Package
|
||||
- User A only gets information about VNF Package A.
|
||||
- User B only gets information about VNF Package B.
|
||||
- Delete VNF Package
|
||||
- User A deletes VNF Package A.
|
||||
- User B deletes VNF Package B.
|
||||
TODO(manpreetk): Only positive test cases are validated in
|
||||
Y-release.
|
||||
Negative test cases
|
||||
- User A fails to delete VNF Package B.
|
||||
- User B fails to delete VNF Package A.
|
||||
Validation of negative test cases would require design changes
|
||||
in Fake NFVO server, which could be implemented in the upcoming
|
||||
cycle.
|
||||
"""
|
||||
# Create and Upload VNF Package
|
||||
# User A creates VNF Package A
|
||||
sample_name = 'mt_functional1'
|
||||
csar_package_path = os.path.abspath(
|
||||
os.path.join(
|
||||
os.path.dirname(__file__),
|
||||
"../../../etc/samples/etsi/nfv",
|
||||
sample_name))
|
||||
tempname, _ = vnflcm_base._create_csar_with_unique_vnfd_id(
|
||||
csar_package_path)
|
||||
|
||||
# User A uploads VNF Package A
|
||||
vnf_pkg_id, vnfd_id = vnflcm_base._create_and_upload_vnf_package(
|
||||
self.tacker_client_t1, user_defined_data={
|
||||
"key": sample_name}, temp_csar_path=tempname)
|
||||
|
||||
# User B creates VNF Package B
|
||||
sample_name_t2 = 'mt_functional1'
|
||||
csar_package_path_t2 = os.path.abspath(
|
||||
os.path.join(
|
||||
os.path.dirname(__file__),
|
||||
"../../../etc/samples/etsi/nfv",
|
||||
sample_name_t2))
|
||||
tempname_t2, _ = vnflcm_base._create_csar_with_unique_vnfd_id(
|
||||
csar_package_path_t2)
|
||||
|
||||
# User B uploads VNF Package B
|
||||
vnf_pkg_id2, vnfd_id2 = vnflcm_base._create_and_upload_vnf_package(
|
||||
self.tacker_client_t2, user_defined_data={
|
||||
"key": sample_name_t2}, temp_csar_path=tempname_t2)
|
||||
|
||||
# List VNF Package
|
||||
# User A gets VNF package list and confirms only VNF Package A
|
||||
# is output.
|
||||
resp, body = self.http_client_tenant1.do_request(
|
||||
self.base_url, "GET")
|
||||
self.assertEqual(200, resp.status_code)
|
||||
|
||||
package_id = [obj['id'] for obj in body]
|
||||
self.assertIn(vnf_pkg_id, package_id)
|
||||
|
||||
# User B gets VNF package list and confirms only VNF Package B
|
||||
# is output.
|
||||
resp_t2, body_t2 = self.http_client_tenant2.do_request(
|
||||
self.base_url, "GET")
|
||||
self.assertEqual(200, resp_t2.status_code)
|
||||
|
||||
package_id = [obj['id'] for obj in body_t2]
|
||||
self.assertIn(vnf_pkg_id2, package_id)
|
||||
|
||||
# Show VNF Package
|
||||
# User A only gets information about VNF Package A
|
||||
show_url = os.path.join(self.base_url, vnf_pkg_id)
|
||||
resp, body = self.http_client_tenant1.do_request(
|
||||
show_url, "GET")
|
||||
self.assertEqual(200, resp.status_code)
|
||||
|
||||
# User B only gets information about VNF Package B
|
||||
show_url_t2 = os.path.join(self.base_url, vnf_pkg_id2)
|
||||
resp_t2, body_t2 = self.http_client_tenant2.do_request(
|
||||
show_url_t2, "GET")
|
||||
self.assertEqual(200, resp_t2.status_code)
|
||||
|
||||
# Delete VNF Package
|
||||
# User A deletes VNF Package A
|
||||
self._disable_operational_state(vnf_pkg_id,
|
||||
self.http_client_tenant1)
|
||||
self._delete_vnf_package(vnf_pkg_id, self.http_client_tenant1)
|
||||
self._wait_for_delete(vnf_pkg_id, self.http_client_tenant1)
|
||||
|
||||
# User B deletes VNF Package B
|
||||
self._disable_operational_state(vnf_pkg_id2,
|
||||
self.http_client_tenant2)
|
||||
self._delete_vnf_package(vnf_pkg_id2, self.http_client_tenant2)
|
||||
self._wait_for_delete(vnf_pkg_id2, self.http_client_tenant2)
|
||||
|
||||
def test_vnf_instantiation_by_vim_of_different_tenant_and_role(self):
|
||||
"""Test VNF instantiation by VIM of differnt tenant.
|
||||
|
||||
In this test case, we do following steps.
|
||||
Note: User A is an admin role user belongs to Tenant 1.
|
||||
User B is a non-admin role user belongs to Tenant 2.
|
||||
- Create VNF Instance
|
||||
- User B creates VNF Instance B using VNF Package B.
|
||||
- Instantiate VNF
|
||||
- User A fails to instantiates VNF Instance B, both
|
||||
VNF and VIM belong to different tenant.
|
||||
"""
|
||||
# Pre-Setting
|
||||
# User B registers Subscription B.
|
||||
callback_url = os.path.join(vnflcm_base.MOCK_NOTIFY_CALLBACK_URL,
|
||||
self._testMethodName)
|
||||
req_body_t2 = fake_vnflcm.Subscription.make_create_request_body(
|
||||
'http://localhost:{}{}'.format(
|
||||
base.FAKE_SERVER_MANAGER_T2.SERVER_PORT_T2,
|
||||
callback_url))
|
||||
resp_t2, resp_body_t2 = self._register_subscription(
|
||||
req_body_t2, self.http_client_tenant2)
|
||||
self.assertEqual(201, resp_t2.status_code)
|
||||
self.assert_http_header_location_for_subscription(
|
||||
resp_t2.headers)
|
||||
self.assert_notification_get(callback_url,
|
||||
base.FAKE_SERVER_MANAGER_T2)
|
||||
subscription_id_t2 = resp_body_t2.get('id')
|
||||
self.addCleanup(
|
||||
self._delete_subscription,
|
||||
subscription_id_t2,
|
||||
self.tacker_client_t2)
|
||||
|
||||
# Create and Upload VNF Package
|
||||
# User B creates VNF Package B
|
||||
sample_name_t2 = 'mt_functional1'
|
||||
csar_package_path_t2 = os.path.abspath(
|
||||
os.path.join(
|
||||
os.path.dirname(__file__),
|
||||
"../../../etc/samples/etsi/nfv",
|
||||
sample_name_t2))
|
||||
tempname_t2, _ = vnflcm_base._create_csar_with_unique_vnfd_id(
|
||||
csar_package_path_t2)
|
||||
|
||||
# User B uploads VNF Package B
|
||||
vnf_pkg_id, vnfd_id = vnflcm_base._create_and_upload_vnf_package(
|
||||
self.tacker_client_t2, user_defined_data={
|
||||
"key": sample_name_t2}, temp_csar_path=tempname_t2)
|
||||
|
||||
# Post Setting: Reserve deleting VNF package.
|
||||
self.addCleanup(vnflcm_base._delete_vnf_package,
|
||||
self.tacker_client_t2, vnf_pkg_id)
|
||||
|
||||
# Create VNF Instance
|
||||
# User B creates VNF Instance B using VNF Package B
|
||||
resp_t2, vnf_instance_t2 = self._create_vnf_instance_from_body(
|
||||
fake_vnflcm.VnfInstances.make_create_request_body(vnfd_id),
|
||||
self.http_client_tenant2)
|
||||
vnf_instance_id_t2 = vnf_instance_t2.get('id')
|
||||
self._wait_lcm_done(vnf_instance_id=vnf_instance_id_t2,
|
||||
fake_server_manager=base.FAKE_SERVER_MANAGER_T2)
|
||||
self.assert_create_vnf(resp_t2, vnf_instance_t2,
|
||||
vnf_pkg_id,
|
||||
self.tacker_client_t2,
|
||||
base.FAKE_SERVER_MANAGER_T2)
|
||||
|
||||
# Instantiate VNF instance
|
||||
# User A fails to instantiate VNF Instance B as both VIM and VNF
|
||||
# belongs to differernt tenants
|
||||
request_body = fake_vnflcm.VnfInstances.make_inst_request_body(
|
||||
'nfv_user',
|
||||
self.vim['tenant_id'], self.ext_networks,
|
||||
self.ext_mngd_networks,
|
||||
self.ext_link_ports, self.ext_subnets)
|
||||
resp, _ = self._instantiate_vnf_instance(vnf_instance_id_t2,
|
||||
request_body, self.http_client)
|
||||
self.assertEqual(202, resp.status_code)
|
||||
self._vnf_instance_wait_until_fail_detected(vnf_instance_id_t2)
|
@ -88,6 +88,7 @@ auth_url: "${VIMC_ENDPOINT}"
|
||||
username: "${VIMC_OS_USER}"
|
||||
password: "${VIMC_OS_PASSWORD}"
|
||||
project_name: "${VIMC_PROJ}"
|
||||
domain_name: "${VIMC_OS_PROJ_DOMAIN}"
|
||||
project_domain_name: "${VIMC_OS_PROJ_DOMAIN}"
|
||||
user_domain_name: "${VIMC_OS_USER_DOMAIN}"
|
||||
cert_verify: "${_cert_verify}"
|
||||
|
6
tox.ini
6
tox.ini
@ -61,6 +61,12 @@ setenv = {[testenv]setenv}
|
||||
commands =
|
||||
stestr --test-path=./tacker/tests/functional/sol_v2 run --slowest --concurrency 1 {posargs}
|
||||
|
||||
[testenv:dsvm-functional-sol-multi-tenant]
|
||||
setenv = {[testenv]setenv}
|
||||
|
||||
commands =
|
||||
stestr --test-path=./tacker/tests/functional/sol_multi_tenant run --slowest --concurrency 1 {posargs}
|
||||
|
||||
[testenv:debug]
|
||||
commands = oslo_debug_helper {posargs}
|
||||
|
||||
|
Loading…
Reference in New Issue
Block a user