开始使用 v2 Tacker¶
摘要¶
本讲座将使您能够
使用 Tacker v2 API 在 OpenStack 上创建和删除示例 VNF
本讲座介绍了 Tacker 支持的两种 VNF 部署类型。
“使用 LCM 操作用户数据的 VNF 部署”是可选的。该部分将用以下标注进行说明 [这是特定于 UserData 的部分]。
下图显示了本讲座中使用的示例 VNF。
注意
您可以使用以下命令查看 Tacker 的日志
$ sudo journalctl -u devstack@tacker.service
$ sudo journalctl -u devstack@tacker-conductor.service
先决条件¶
应安装以下软件包
tacker
python-tackerclient
配置¶
加载客户端操作的凭据¶
在运行任何 Tacker 命令之前,需要先获取您的凭据。
注意
有关详细信息,请参阅 创建 OpenStack 客户端环境脚本。 在本文档中,设置如下
OS_REGION_NAME=RegionOne
OS_PROJECT_DOMAIN_ID=default
OS_CACERT=
OS_AUTH_URL=http://192.168.56.10/identity
OS_TENANT_NAME=nfv
OS_USER_DOMAIN_ID=default
OS_USERNAME=nfv_user
OS_VOLUME_API_VERSION=3
OS_AUTH_TYPE=password
OS_PROJECT_NAME=nfv
OS_PASSWORD=devstack
OS_IDENTITY_API_VERSION=3
您可以通过检查以下命令是否能正常工作来确认 Tacker 是否可用
$ openstack vim list
注意
请参阅 命令行界面参考 以查找所有可用命令。
注册 VIM¶
准备 VIM 配置文件
您可以使用设置脚本生成 VIM 配置文件,也可以按照 注册 VIM 的配置文件 中所述从头开始编辑它。 该脚本会从您的环境变量中查找配置参数,例如用户名或密码。 以下是生成 OpenStack VIM 配置文件的示例,名为
vim_config.yaml。 在本文档中,TACKER_ROOT是您服务器上 tacker 仓库的根目录。$ bash TACKER_ROOT/tools/gen_vim_config.sh -p nfv --os-user nfv_user \ --os-disable-cert-verify Config for OpenStack VIM 'vim_config.yaml' generated.
支持从命令行配置参数的几种选项。 有关详细信息,请参阅帮助信息
-h。注意
有关工具详细信息,请参阅 注册 VIM 的配置文件。
您也可以使用示例配置文件 (vim_config.yaml) 代替使用脚本。
$ cp TACKER_ROOT/doc/source/user/v2/getting_started/conf/vim_config.yaml ./ $ vi vim_config.yaml
auth_url: 'http://192.168.56.10/identity' username: "nfv_user" password: "devstack" project_name: "nfv" domain_name: "default" project_domain_name: "default" user_domain_name: "default" cert_verify: "False"
注册默认 VIM
设置好 VIM 配置文件后,您可以通过
openstack命令使用--is-default选项注册默认 VIM。$ openstack vim register --config-file ./vim_config.yaml \ --is-default --fit-width openstack-admin-vim +----------------+-----------------------------------------------------+ | Field | Value | +----------------+-----------------------------------------------------+ | auth_cred | { | | | "username": "nfv_user", | | | "user_domain_name": "default", | | | "cert_verify": "False", | | | "project_id": null, | | | "project_name": "nfv", | | | "project_domain_name": "default", | | | "auth_url": "http://192.168.56.10/identity/v3", | | | "key_type": "barbican_key", | | | "secret_uuid": "***", | | | "password": "***" | | | } | | auth_url | http://192.168.56.10/identity/v3 | | created_at | 2023-11-30 08:32:48.869451 | | description | | | extra | | | id | bff267c4-6fc9-46b5-be53-15a6a3680033 | | is_default | True | | name | openstack-admin-vim | | placement_attr | { | | | "regions": [ | | | "RegionOne" | | | ] | | | } | | project_id | ebbc6cf1a03d49918c8e408535d87268 | | status | ACTIVE | | type | openstack | | updated_at | None | | vim_project | { | | | "name": "nfv", | | | "project_domain_name": "default" | | | } | +----------------+-----------------------------------------------------+
创建并上传 VNF 包¶
准备 VNF 包¶
创建 VNF 包 CSAR 目录
$ mkdir -p ./sample_vnf_package_csar/TOSCA-Metadata \ ./sample_vnf_package_csar/Definitions \ ./sample_vnf_package_csar/BaseHOT/simple/nested \ ./sample_vnf_package_csar/Files
[这是特定于 UserData 的部分] 使用 UserData 时,创建以下目录。
$ mkdir -p ./sample_vnf_package_csar/UserData
创建一个
TOSCA.meta文件$ vi ./sample_vnf_package_csar/TOSCA-Metadata/TOSCA.meta
TOSCA-Meta-File-Version: 1.0 Created-by: Dummy User CSAR-Version: 1.1 Entry-Definitions: Definitions/sample_vnfd_top.yaml
下载 ETSI 定义文件
您应该将
${TOSCA_VERSION}设置为适当的 TOSCA 服务模板版本之一 (SOL001),例如export TOSCA_VERSION=v2.6.1。重要提示
您还应该检查 tacker 是否支持 TOSCA 服务模板的版本。 请参阅 基于 ETSI NFV-SOL001 的 VNF 描述符 (VNFD) 以获取支持的版本。
$ cd ./sample_vnf_package_csar/Definitions $ wget https://forge.etsi.org/rep/nfv/SOL001/raw/${TOSCA_VERSION}/etsi_nfv_sol001_common_types.yaml $ wget https://forge.etsi.org/rep/nfv/SOL001/raw/${TOSCA_VERSION}/etsi_nfv_sol001_vnfd_types.yaml
创建 VNFD 文件
创建
sample_vnfd_top.yaml$ vi ./sample_vnfd_top.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: b1bb0ce7-ebca-4fa7-95ed-4840d70a1177 provider: Company product_name: Sample VNF software_version: '1.0' descriptor_version: '1.0' vnfm_info: - Tacker
创建
sample_vnfd_types.yaml$ vi ./sample_vnfd_types.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: descriptor_id: type: string constraints: [ valid_values: [ b1bb0ce7-ebca-4fa7-95ed-4840d70a1177 ] ] default: b1bb0ce7-ebca-4fa7-95ed-4840d70a1177 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
注意
description_id必须全局唯一,即您不能使用相同的description_id创建多个 VNFD。创建
sample_vnfd_df_simple.yaml$ vi ./sample_vnfd_df_simple.yaml
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: 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 node_templates: VNF: type: company.provider.VNF properties: flavour_description: A simple flavour interfaces: Vnflcm: instantiate_start: [] instantiate_end: [] terminate_start: [] terminate_end: [] modify_information_start: [] modify_information_end: [] heal_start: [] heal_end: [] scale_start: [] scale_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 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: 1 GB CP1: type: tosca.nodes.nfv.VduCp properties: layer_protocols: [ ipv4 ] order: 4 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 ] - 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 ]
注意
应该根据“VNF”中的属性更新
flavour_description,但 Tacker 无法处理它。 实例化后,sample_vnfd_types.yaml中的默认值始终被使用。
创建 BaseHOT 文件
$ cd - $ vi ./sample_vnf_package_csar/BaseHOT/simple/sample_lcm_hot.yaml $ vi ./sample_vnf_package_csar/BaseHOT/simple/nested/VDU1.yaml
sample_lcm_hot.yaml
heat_template_version: 2013-05-23 description: 'imple Base HOT for Sample VNF' parameters: nfv: type: json resources: VDU1: type: VDU1.yaml properties: flavor: { get_param: [ nfv, VDU, VDU1, computeFlavourId ] } image: { get_param: [ nfv, VDU, VDU1, vcImageId ] } net: { get_resource: internalVL1 } internalVL1: type: OS::Neutron::Net internalVL1_subnet: type: OS::Neutron::Subnet properties: ip_version: 4 network: get_resource: internalVL1 cidr: 10.0.0.0/24 outputs: {}
VDU1.yaml
heat_template_version: 2013-05-23 description: 'VDU1 HOT for Sample VNF' parameters: flavor: type: string image: type: string net: type: string resources: VDU1: type: OS::Nova::Server properties: flavor: { get_param: flavor } image: { get_param: image } name: VDU1 networks: - port: get_resource: VDU1_CP1 VDU1_CP1: type: OS::Neutron::Port properties: network: { get_param: net }
[这是特定于 UserData 的部分] 创建 UserData 文件
$ cd ./sample_vnf_package_csar/UserData/ $ touch ./__init__.py $ vi ./lcm_user_data.py
注意
有关详细信息,请参阅 UserData 脚本 (VNF LCM v2)。 在本文档中,使用了以下“StandardUserData”。
# Copyright (C) 2022 Nippon Telegraph and Telephone Corporation # All Rights Reserved. # # Licensed under the Apache License, Version 2.0 (the "License"); you may # not use this file except in compliance with the License. You may obtain # a copy of the License at # # https://apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, WITHOUT # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the # License for the specific language governing permissions and limitations # under the License. import copy import yaml from tacker.sol_refactored.common import common_script_utils from tacker.sol_refactored.common import vnf_instance_utils as inst_utils from tacker.sol_refactored.infra_drivers.openstack import userdata_utils def add_idx(name, index): return f'{name}-{index}' def rm_idx(name_idx): return name_idx.rpartition('-')[0] def add_idx_to_vdu_template(vdu_template, vdu_idx): """Add index to the third element of get_param ex. input VDU template: --- VDU1: type: VDU1.yaml properties: flavor: { get_param: [ nfv, VDU, VDU1, computeFlavourId ] } image-VDU1: { get_param: [ nfv, VDU, VDU1, vcImageId ] } net1: { get_param: [ nfv, CP, VDU1_CP1, network ] } --- output VDU template: --- VDU1: type: VDU1.yaml properties: flavor: { get_param: [ nfv, VDU, VDU1-1, computeFlavourId ] } image-VDU1: { get_param: [ nfv, VDU, VDU1-1, vcImageId ] } net1: { get_param: [ nfv, CP, VDU1_CP1-1, network ] } --- """ res = copy.deepcopy(vdu_template) for prop_value in res.get('properties', {}).values(): get_param = prop_value.get('get_param') if (get_param is not None and isinstance(get_param, list) and len(get_param) >= 4): get_param[2] = add_idx(get_param[2], vdu_idx) return res def _get_new_cps_from_req(cps, req, grant): # used by change_ext_conn and change_vnfpkg new_cps = {} for cp_name_idx, cp_value in cps.items(): cp_name = rm_idx(cp_name_idx) if 'network' in cp_value: network = common_script_utils.get_param_network( cp_name, grant, req) if network is None: continue new_cps.setdefault(cp_name_idx, {}) new_cps[cp_name_idx]['network'] = network if 'fixed_ips' in cp_value: ext_fixed_ips = common_script_utils.get_param_fixed_ips( cp_name, grant, req) fixed_ips = [] for i in range(len(ext_fixed_ips)): if i not in cp_value['fixed_ips']: break ips_i = cp_value['fixed_ips'][i] if 'subnet' in ips_i: ips_i['subnet'] = ext_fixed_ips[i].get('subnet') if 'ip_address' in ips_i: ips_i['ip_address'] = ext_fixed_ips[i].get( 'ip_address') fixed_ips.append(ips_i) new_cps.setdefault(cp_name_idx, {}) new_cps[cp_name_idx]['fixed_ips'] = fixed_ips return new_cps def _merge_additional_params(nfv_dict, req, grant): if 'nfv' in req.get('additionalParams', {}): nfv_dict = inst_utils.json_merge_patch( nfv_dict, req['additionalParams']['nfv']) if 'nfv' in grant.get('additionalParams', {}): nfv_dict = inst_utils.json_merge_patch( nfv_dict, grant['additionalParams']['nfv']) return nfv_dict class StandardUserData(userdata_utils.AbstractUserData): @staticmethod def instantiate(req, inst, grant_req, grant, tmp_csar_dir): vnfd = common_script_utils.get_vnfd(inst['vnfdId'], tmp_csar_dir) flavour_id = req['flavourId'] hot_dict = vnfd.get_base_hot(flavour_id) top_hot = hot_dict['template'] # first modify VDU resources popped_vdu = {} vdu_idxes = {} for vdu_name in vnfd.get_vdu_nodes(flavour_id).keys(): popped_vdu[vdu_name] = top_hot.get('resources', {}).pop(vdu_name) vdu_idxes[vdu_name] = 0 zones = {} for res in grant_req['addResources']: if res['type'] != 'COMPUTE': continue vdu_name = res['resourceTemplateId'] if vdu_name not in popped_vdu: continue vdu_idx = vdu_idxes[vdu_name] vdu_idxes[vdu_name] += 1 zones[add_idx(vdu_name, vdu_idx)] = ( common_script_utils.get_param_zone_by_vnfc( res['id'], grant)) res = add_idx_to_vdu_template(popped_vdu[vdu_name], vdu_idx) top_hot['resources'][add_idx(vdu_name, vdu_idx)] = res nfv_dict = common_script_utils.init_nfv_dict(top_hot) vdus = nfv_dict.get('VDU', {}) for vdu_name_idx, vdu_value in vdus.items(): vdu_name = rm_idx(vdu_name_idx) if 'computeFlavourId' in vdu_value: vdu_value['computeFlavourId'] = ( common_script_utils.get_param_flavor( vdu_name, flavour_id, vnfd, grant)) if 'vcImageId' in vdu_value: vdu_value['vcImageId'] = common_script_utils.get_param_image( vdu_name, flavour_id, vnfd, grant) if 'locationConstraints' in vdu_value: vdu_value['locationConstraints'] = zones[vdu_name_idx] cps = nfv_dict.get('CP', {}) for cp_name, cp_value in cps.items(): cp_name = rm_idx(cp_name) if 'network' in cp_value: cp_value['network'] = common_script_utils.get_param_network( cp_name, grant, req) if 'fixed_ips' in cp_value: ext_fixed_ips = common_script_utils.get_param_fixed_ips( cp_name, grant, req) fixed_ips = [] for i in range(len(ext_fixed_ips)): if i not in cp_value['fixed_ips']: break ips_i = cp_value['fixed_ips'][i] if 'subnet' in ips_i: ips_i['subnet'] = ext_fixed_ips[i].get('subnet') if 'ip_address' in ips_i: ips_i['ip_address'] = ext_fixed_ips[i].get( 'ip_address') fixed_ips.append(ips_i) cp_value['fixed_ips'] = fixed_ips common_script_utils.apply_ext_managed_vls(top_hot, req, grant) nfv_dict = _merge_additional_params(nfv_dict, req, grant) fields = { 'template': yaml.safe_dump(top_hot), 'parameters': {'nfv': nfv_dict}, 'files': {} } for key, value in hot_dict.get('files', {}).items(): fields['files'][key] = yaml.safe_dump(value) return fields @staticmethod def scale(req, inst, grant_req, grant, tmp_csar_dir): if req['type'] == 'SCALE_OUT': return StandardUserData._scale_out(req, inst, grant_req, grant, tmp_csar_dir) else: return StandardUserData._scale_in(req, inst, grant_req, grant, tmp_csar_dir) @staticmethod def _scale_out(req, inst, grant_req, grant, tmp_csar_dir): vnfd = common_script_utils.get_vnfd(inst['vnfdId'], tmp_csar_dir) flavour_id = inst['instantiatedVnfInfo']['flavourId'] hot_dict = vnfd.get_base_hot(flavour_id) top_hot = hot_dict['template'] # first modify VDU resources popped_vdu = {} vdu_idxes = {} for vdu_name in vnfd.get_vdu_nodes(flavour_id).keys(): popped_vdu[vdu_name] = top_hot.get('resources', {}).pop(vdu_name) vdu_idxes[vdu_name] = common_script_utils.get_current_capacity( vdu_name, inst) zones = {} for res in grant_req['addResources']: if res['type'] != 'COMPUTE': continue vdu_name = res['resourceTemplateId'] if vdu_name not in popped_vdu: continue vdu_idx = vdu_idxes[vdu_name] vdu_idxes[vdu_name] += 1 zones[add_idx(vdu_name, vdu_idx)] = ( common_script_utils.get_param_zone_by_vnfc( res['id'], grant)) res = add_idx_to_vdu_template(popped_vdu[vdu_name], vdu_idx) top_hot['resources'][add_idx(vdu_name, vdu_idx)] = res nfv_dict = common_script_utils.init_nfv_dict(top_hot) vdus = nfv_dict.get('VDU', {}) for vdu_name_idx, vdu_value in vdus.items(): vdu_name = rm_idx(vdu_name_idx) if 'computeFlavourId' in vdu_value: vdu_value['computeFlavourId'] = ( common_script_utils.get_param_flavor( vdu_name, flavour_id, vnfd, grant)) if 'vcImageId' in vdu_value: vdu_value['vcImageId'] = common_script_utils.get_param_image( vdu_name, flavour_id, vnfd, grant) if 'locationConstraints' in vdu_value: vdu_value['locationConstraints'] = zones[vdu_name_idx] exclude_params = [param for param, value in vdu_value.items() if value is None] for exclude_param in exclude_params: del vdu_value[exclude_param] cps = nfv_dict.get('CP', {}) for cp_name, cp_value in cps.items(): cp_name = rm_idx(cp_name) if 'network' in cp_value: cp_value['network'] = ( common_script_utils.get_param_network_from_inst( cp_name, inst)) if 'fixed_ips' in cp_value: ext_fixed_ips = ( common_script_utils.get_param_fixed_ips_from_inst( cp_name, inst)) fixed_ips = [] for i in range(len(ext_fixed_ips)): if i not in cp_value['fixed_ips']: break ips_i = cp_value['fixed_ips'][i] if 'subnet' in ips_i: ips_i['subnet'] = ext_fixed_ips[i].get('subnet') if 'ip_address' in ips_i: ips_i['ip_address'] = ext_fixed_ips[i].get( 'ip_address') fixed_ips.append(ips_i) cp_value['fixed_ips'] = fixed_ips exclude_params = [param for param, value in cp_value.items() if value is None] for exclude_param in exclude_params: del cp_value[exclude_param] common_script_utils.apply_ext_managed_vls_from_inst(top_hot, inst) nfv_dict = _merge_additional_params(nfv_dict, req, grant) fields = { 'template': yaml.safe_dump(top_hot), 'parameters': {'nfv': nfv_dict} } return fields @staticmethod def _scale_in(req, inst, grant_req, grant, tmp_csar_dir): template = {'resources': {}} for res in grant_req['removeResources']: if res['type'] != 'COMPUTE': continue for inst_vnfc in inst['instantiatedVnfInfo']['vnfcResourceInfo']: if (inst_vnfc['computeResource']['resourceId'] == res['resource']['resourceId']): # must be found vdu_idx = inst_vnfc['metadata']['vdu_idx'] break vdu_name = res['resourceTemplateId'] template['resources'][add_idx(vdu_name, vdu_idx)] = None fields = { 'template': yaml.safe_dump(template), } return fields @staticmethod def scale_rollback(req, inst, grant_req, grant, tmp_csar_dir): vnfd = common_script_utils.get_vnfd(inst['vnfdId'], tmp_csar_dir) flavour_id = inst['instantiatedVnfInfo']['flavourId'] vdu_nodes = vnfd.get_vdu_nodes(flavour_id) vdu_idxes = {} for vdu_name in vdu_nodes.keys(): vdu_idxes[vdu_name] = common_script_utils.get_current_capacity( vdu_name, inst) template = {'resources': {}} for res in grant_req['addResources']: if res['type'] != 'COMPUTE': continue vdu_name = res['resourceTemplateId'] vdu_idx = vdu_idxes[vdu_name] vdu_idxes[vdu_name] += 1 template['resources'][add_idx(vdu_name, vdu_idx)] = None fields = { 'template': yaml.safe_dump(template), } return fields @staticmethod def change_ext_conn(req, inst, grant_req, grant, tmp_csar_dir): # change_ext_conn is interested in 'CP' only. # This method returns only 'CP' part in the 'nfv' dict from # ChangeExtVnfConnectivityRequest. # It is applied to json merge patch against the existing 'nfv' # dict by the caller. # NOTE: complete 'nfv' dict can not be made at the moment # since InstantiateVnfRequest is necessary to make it. vnfd = common_script_utils.get_vnfd(inst['vnfdId'], tmp_csar_dir) flavour_id = inst['instantiatedVnfInfo']['flavourId'] hot_dict = vnfd.get_base_hot(flavour_id) top_hot = hot_dict['template'] # first modify VDU resources popped_vdu = {} for vdu_name in vnfd.get_vdu_nodes(flavour_id).keys(): popped_vdu[vdu_name] = top_hot.get('resources', {}).pop(vdu_name) for inst_vnfc in inst['instantiatedVnfInfo'].get( 'vnfcResourceInfo', []): vdu_idx = inst_vnfc['metadata'].get('vdu_idx') if vdu_idx is None: continue vdu_name = inst_vnfc['vduId'] res = add_idx_to_vdu_template(popped_vdu[vdu_name], vdu_idx) top_hot['resources'][add_idx(vdu_name, vdu_idx)] = res nfv_dict = common_script_utils.init_nfv_dict(top_hot) cps = nfv_dict.get('CP', {}) new_cps = _get_new_cps_from_req(cps, req, grant) nfv_dict = _merge_additional_params({'CP': new_cps}, req, grant) fields = {'parameters': {'nfv': nfv_dict}} return fields @staticmethod def change_ext_conn_rollback(req, inst, grant_req, grant, tmp_csar_dir): fields = { 'parameters': { 'nfv': inst['instantiatedVnfInfo']['metadata']['nfv'] } } return fields @staticmethod def heal(req, inst, grant_req, grant, tmp_csar_dir): vnfd = common_script_utils.get_vnfd(inst['vnfdId'], tmp_csar_dir) flavour_id = inst['instantiatedVnfInfo']['flavourId'] vdus = inst['instantiatedVnfInfo']['metadata']['nfv'].get('VDU', {}) for res in grant_req['removeResources']: if res['type'] != 'COMPUTE': continue for inst_vnfc in inst['instantiatedVnfInfo']['vnfcResourceInfo']: if (inst_vnfc['computeResource']['resourceId'] == res['resource']['resourceId']): # must be found vdu_name = inst_vnfc['vduId'] vdu_idx = inst_vnfc['metadata']['vdu_idx'] image = common_script_utils.get_param_image( vdu_name, flavour_id, vnfd, grant, fallback_vnfd=False) if image is not None: vdus[add_idx(vdu_name, vdu_idx)]['vcImageId'] = image break nfv_dict = _merge_additional_params({'VDU': vdus}, req, grant) fields = {'parameters': {'nfv': nfv_dict}} return fields @staticmethod def change_vnfpkg(req, inst, grant_req, grant, tmp_csar_dir): vnfd = common_script_utils.get_vnfd(grant_req['dstVnfdId'], tmp_csar_dir) flavour_id = inst['instantiatedVnfInfo']['flavourId'] hot_dict = vnfd.get_base_hot(flavour_id) top_hot = hot_dict['template'] # first modify VDU resources popped_vdu = {} for vdu_name in vnfd.get_vdu_nodes(flavour_id).keys(): popped_vdu[vdu_name] = top_hot.get('resources', {}).pop(vdu_name) target_vnfc_res_ids = [ res['resource']['resourceId'] for res in grant_req['removeResources'] if res['type'] == 'COMPUTE' ] cur_hot_reses = {} new_hot_reses = {} for inst_vnfc in inst['instantiatedVnfInfo']['vnfcResourceInfo']: vdu_idx = inst_vnfc['metadata'].get('vdu_idx') if vdu_idx is None: # should not be None. just check for consistency. continue vdu_name = inst_vnfc['vduId'] vdu_name_idx = add_idx(vdu_name, vdu_idx) res = add_idx_to_vdu_template(popped_vdu[vdu_name], vdu_idx) top_hot['resources'][vdu_name_idx] = res if (inst_vnfc['computeResource']['resourceId'] in target_vnfc_res_ids): new_hot_reses[vdu_name_idx] = res else: cur_hot_reses[vdu_name_idx] = res cur_nfv_dict = common_script_utils.init_nfv_dict( {'resources': cur_hot_reses}) new_nfv_dict = common_script_utils.init_nfv_dict( {'resources': new_hot_reses}) new_vdus = new_nfv_dict.get('VDU', {}) vdus = inst['instantiatedVnfInfo']['metadata']['nfv'].get('VDU', {}) for vdu_name_idx, vdu_value in new_vdus.items(): vdu_name = rm_idx(vdu_name_idx) if 'computeFlavourId' in vdu_value: vdus[vdu_name_idx]['computeFlavourId'] = ( common_script_utils.get_param_flavor( vdu_name, flavour_id, vnfd, grant)) if 'vcImageId' in vdu_value: vdus[vdu_name_idx]['vcImageId'] = ( common_script_utils.get_param_image( vdu_name, flavour_id, vnfd, grant)) cps = cur_nfv_dict.get('CP', {}) cps.update(new_nfv_dict.get('CP', {})) # NOTE: req includes only different part. some CPs in new_nfv_dict # may be necessary to get from inst. cur_cps = inst['instantiatedVnfInfo']['metadata']['nfv'].get('CP', {}) req_cps = _get_new_cps_from_req(cps, req, grant) for cp_name in cps.keys(): if cp_name in req_cps: cps[cp_name] = req_cps[cp_name] else: cps[cp_name] = cur_cps[cp_name] common_script_utils.apply_ext_managed_vls(top_hot, req, grant) nfv_dict = _merge_additional_params({'VDU': vdus, 'CP': cps}, req, grant) fields = { 'template': yaml.safe_dump(top_hot), 'parameters': {'nfv': nfv_dict}, 'files': {}, 'existing': False } for key, value in hot_dict.get('files', {}).items(): fields['files'][key] = yaml.safe_dump(value) return fields @staticmethod def change_vnfpkg_rollback(req, inst, grant_req, grant, tmp_csar_dir): vnfd = common_script_utils.get_vnfd(inst['vnfdId'], tmp_csar_dir) flavour_id = inst['instantiatedVnfInfo']['flavourId'] hot_dict = vnfd.get_base_hot(flavour_id) top_hot = hot_dict['template'] # first modify VDU resources popped_vdu = {} for vdu_name in vnfd.get_vdu_nodes(flavour_id).keys(): popped_vdu[vdu_name] = top_hot.get('resources', {}).pop(vdu_name) for inst_vnfc in inst['instantiatedVnfInfo']['vnfcResourceInfo']: vdu_idx = inst_vnfc['metadata'].get('vdu_idx') if vdu_idx is None: # should not be None. just check for consistency. continue vdu_name = inst_vnfc['vduId'] vdu_name_idx = add_idx(vdu_name, vdu_idx) res = add_idx_to_vdu_template(popped_vdu[vdu_name], vdu_idx) top_hot['resources'][vdu_name_idx] = res common_script_utils.apply_ext_managed_vls(top_hot, req, grant) fields = { 'template': yaml.safe_dump(top_hot), 'parameters': { 'nfv': inst['instantiatedVnfInfo']['metadata']['nfv']}, 'files': {}, 'existing': False } for key, value in hot_dict.get('files', {}).items(): fields['files'][key] = yaml.safe_dump(value) return fields
将 VNF 包 CSAR 压缩为 zip
$ cd - $ cd ./sample_vnf_package_csar $ zip sample_vnf_package_csar.zip \ -r TOSCA-Metadata/ Definitions/ BaseHOT/ Files/
zip 文件中的内容应如下所示。
$ unzip -Z -1 sample_vnf_package_csar.zip TOSCA-Metadata/ TOSCA-Metadata/TOSCA.meta Definitions/ Definitions/sample_vnfd_types.yaml Definitions/etsi_nfv_sol001_vnfd_types.yaml Definitions/etsi_nfv_sol001_common_types.yaml Definitions/sample_vnfd_df_simple.yaml Definitions/sample_vnfd_top.yaml BaseHOT/ BaseHOT/simple/ BaseHOT/simple/nested/ BaseHOT/simple/nested/VDU1.yaml BaseHOT/simple/sample_lcm_hot.yaml Files/
[这是特定于 UserData 的部分] 使用 UserData 时,添加
UserData目录。$ zip sample_vnf_package_csar.zip -r UserData/
zip 文件中的内容应如下所示。
$ unzip -Z -1 sample_vnf_package_csar.zip TOSCA-Metadata/ TOSCA-Metadata/TOSCA.meta Definitions/ Definitions/sample_vnfd_types.yaml Definitions/etsi_nfv_sol001_vnfd_types.yaml Definitions/etsi_nfv_sol001_common_types.yaml Definitions/sample_vnfd_df_simple.yaml Definitions/sample_vnfd_top.yaml BaseHOT/ BaseHOT/simple/ BaseHOT/simple/nested/ BaseHOT/simple/nested/VDU1.yaml BaseHOT/simple/sample_lcm_hot.yaml Files/ UserData/ UserData/__init__.py UserData/lcm_user_data.py
在这里,您可以找到示例 VNF 包 CSAR 作为 zip 文件的结构。
创建 VNF 包¶
执行 vnfpkgm create
记下“VNF 包 ID”,因为它将在下一步中使用。
$ cd - $ openstack vnf package create +-------------------+-------------------------------------------------------------------------------------------------+ | Field | Value | +-------------------+-------------------------------------------------------------------------------------------------+ | ID | 6e6b7a6d-0ebe-4085-96c2-b34269d837f9 | | Links | { | | | "self": { | | | "href": "/vnfpkgm/v1/vnf_packages/6e6b7a6d-0ebe-4085-96c2-b34269d837f9" | | | }, | | | "packageContent": { | | | "href": "/vnfpkgm/v1/vnf_packages/6e6b7a6d-0ebe-4085-96c2-b34269d837f9/package_content" | | | } | | | } | | Onboarding State | CREATED | | Operational State | DISABLED | | Usage State | NOT_IN_USE | | User Defined Data | {} | +-------------------+-------------------------------------------------------------------------------------------------+
上传 VNF 包¶
执行 vnfpkgm upload
需要将“VNF 包 ID”
6e6b7a6d-0ebe-4085-96c2-b34269d837f9替换为从 创建 VNF 包 中获得的适当 ID。$ openstack vnf package upload \ --path ./sample_vnf_package_csar/sample_vnf_package_csar.zip \ 6e6b7a6d-0ebe-4085-96c2-b34269d837f9 Upload request for VNF package 6e6b7a6d-0ebe-4085-96c2-b34269d837f9 has been accepted.
检查创建的 VNF 包¶
确认“入职状态”为
ONBOARDED$ openstack vnf package list +--------------------------------------+------------------+------------------+-------------+-------------------+-------------------------------------------------------------------------------------------------+ | Id | Vnf Product Name | Onboarding State | Usage State | Operational State | Links | +--------------------------------------+------------------+------------------+-------------+-------------------+-------------------------------------------------------------------------------------------------+ | 6e6b7a6d-0ebe-4085-96c2-b34269d837f9 | Sample VNF | ONBOARDED | NOT_IN_USE | ENABLED | { | | | | | | | "self": { | | | | | | | "href": "/vnfpkgm/v1/vnf_packages/6e6b7a6d-0ebe-4085-96c2-b34269d837f9" | | | | | | | }, | | | | | | | "packageContent": { | | | | | | | "href": "/vnfpkgm/v1/vnf_packages/6e6b7a6d-0ebe-4085-96c2-b34269d837f9/package_content" | | | | | | | } | | | | | | | } | +--------------------------------------+------------------+------------------+-------------+-------------------+-------------------------------------------------------------------------------------------------+
创建和实例化 VNF¶
创建 VNF¶
查找创建 VNF 的“VNFD ID”
示例中可以找到“VNFD ID”为
b1bb0ce7-ebca-4fa7-95ed-4840d70a1177。$ openstack vnf package show \ 6e6b7a6d-0ebe-4085-96c2-b34269d837f9 -c 'VNFD ID' +---------+--------------------------------------+ | Field | Value | +---------+--------------------------------------+ | VNFD ID | b1bb0ce7-ebca-4fa7-95ed-4840d70a1177 | +---------+--------------------------------------+
创建 VNF
需要将“VNFD ID”
b1bb0ce7-ebca-4fa7-95ed-4840d70a1177替换为适当的 ID。$ openstack vnflcm create b1bb0ce7-ebca-4fa7-95ed-4840d70a1177 \ --os-tacker-api-version 2 +-----------------------------+------------------------------------------------------------------------------------------------------------------+ | Field | Value | +-----------------------------+------------------------------------------------------------------------------------------------------------------+ | ID | c98b05c7-bc96-43f8-a688-4d8079ffa3bf | | Instantiation State | NOT_INSTANTIATED | | Links | { | | | "self": { | | | "href": "http://127.0.0.1:9890/vnflcm/v2/vnf_instances/c98b05c7-bc96-43f8-a688-4d8079ffa3bf" | | | }, | | | "instantiate": { | | | "href": "http://127.0.0.1:9890/vnflcm/v2/vnf_instances/c98b05c7-bc96-43f8-a688-4d8079ffa3bf/instantiate" | | | } | | | } | | VNF Configurable Properties | | | VNF Instance Description | | | VNF Instance Name | | | VNF Product Name | Sample VNF | | VNF Provider | Company | | VNF Software Version | 1.0 | | VNFD ID | b1bb0ce7-ebca-4fa7-95ed-4840d70a1177 | | VNFD Version | 1.0 | +-----------------------------+------------------------------------------------------------------------------------------------------------------+
实例化 VNF¶
创建
<param-file>必需参数
flavourId
可选参数
instantiationLevelId
extVirtualLinks
extManagedVirtualLinks
vimConnectionInfo
localizationLanguage
additionalParams
extensions
vnfConfigurableProperties
注意
只有在您拥有默认 VIM 时,才能跳过
vimConnectionInfo。一个名为
sample_request.json的示例<param-file>,包含最小参数$ vi ./sample_request.json
{ "flavourId": "simple" }
[这是特定于 UserData 的部分] 使用 UserData 时,使用以下参数。
{ "flavourId":"simple", "additionalParams": { "lcm-operation-user-data": "./UserData/userdata_standard.py", "lcm-operation-user-data-class": "StandardUserData" } }
注意
userdata_standard.py应该替换为 UserData 文件名。StandardUserData应该替换为 UserData 类名。
实例化 VNF
需要“vnf 实例的 ID”和“指向 <param-file> 的路径”来实例化 vnf。
$ openstack vnflcm instantiate c98b05c7-bc96-43f8-a688-4d8079ffa3bf \ ./sample_request.json --os-tacker-api-version 2 Instantiate request for VNF Instance c98b05c7-bc96-43f8-a688-4d8079ffa3bf has been accepted.
检查实例化 vnf 的详细信息。
$ openstack vnflcm list --os-tacker-api-version 2 +--------------------------------------+-------------------+---------------------+--------------+----------------------+------------------+--------------------------------------+ | ID | VNF Instance Name | Instantiation State | VNF Provider | VNF Software Version | VNF Product Name | VNFD ID | +--------------------------------------+-------------------+---------------------+--------------+----------------------+------------------+--------------------------------------+ | c98b05c7-bc96-43f8-a688-4d8079ffa3bf | | INSTANTIATED | Company | 1.0 | Sample VNF | b1bb0ce7-ebca-4fa7-95ed-4840d70a1177 | +--------------------------------------+-------------------+---------------------+--------------+----------------------+------------------+--------------------------------------+ $ openstack vnflcm show c98b05c7-bc96-43f8-a688-4d8079ffa3bf \ --fit-width --os-tacker-api-version 2 +-----------------------------+--------------------------------------------------------------------------------------------------------------------------------+ | Field | Value | +-----------------------------+--------------------------------------------------------------------------------------------------------------------------------+ | ID | c98b05c7-bc96-43f8-a688-4d8079ffa3bf | | Instantiated Vnf Info | { | | | "flavourId": "simple", | | | "vnfState": "STARTED", | | | "scaleStatus": [ | | | { | | | "aspectId": "VDU1_scale", | | | "scaleLevel": 0 | | | } | | | ], | | | "maxScaleLevels": [ | | | { | | | "aspectId": "VDU1_scale", | | | "scaleLevel": 2 | | | } | | | ], | | | "vnfcResourceInfo": [ | | | { | | | "id": "6d01be26-f2be-421d-8c87-a4aa9d39300e", | | | "vduId": "VDU1", | | | "computeResource": { | | | "vimConnectionId": "bff267c4-6fc9-46b5-be53-15a6a3680033", | | | "resourceId": "6d01be26-f2be-421d-8c87-a4aa9d39300e", | | | "vimLevelResourceType": "OS::Nova::Server" | | | }, | | | "vnfcCpInfo": [ | | | { | | | "id": "CP1-6d01be26-f2be-421d-8c87-a4aa9d39300e", | | | "cpdId": "CP1" | | | } | | | ], | | | "metadata": { | | | "creation_time": "2023-12-01T06:57:11Z", | | | "stack_id": "vnf-c98b05c7-bc96-43f8-a688-4d8079ffa3bf-VDU1-6523jolwu66g/09019137-3b71-426e-8726-8572657999b2", | | | "vdu_idx": null, | | | "flavor": "m1.tiny", | | | "image-VDU1": "cirros-0.5.2-x86_64-disk" | | | } | | | } | | | ], | | | "vnfVirtualLinkResourceInfo": [ | | | { | | | "id": "ffa3b9cf-5135-4dc6-a7a1-dd1912d72363", | | | "vnfVirtualLinkDescId": "internalVL1", | | | "networkResource": { | | | "vimConnectionId": "bff267c4-6fc9-46b5-be53-15a6a3680033", | | | "resourceId": "ffa3b9cf-5135-4dc6-a7a1-dd1912d72363", | | | "vimLevelResourceType": "OS::Neutron::Net" | | | } | | | } | | | ], | | | "vnfcInfo": [ | | | { | | | "id": "VDU1-6d01be26-f2be-421d-8c87-a4aa9d39300e", | | | "vduId": "VDU1", | | | "vnfcResourceInfoId": "6d01be26-f2be-421d-8c87-a4aa9d39300e", | | | "vnfcState": "STARTED" | | | } | | | ], | | | "metadata": { | | | "stack_id": "0b1b274c-a493-4a2c-994f-ee8569ff111c", | | | "nfv": { | | | "VDU": { | | | "VDU1": { | | | "computeFlavourId": "m1.tiny", | | | "vcImageId": "cirros-0.5.2-x86_64-disk" | | | } | | | } | | | }, | | | "tenant": "nfv" | | | } | | | } | | Instantiation State | INSTANTIATED | | Links | { | | | "self": { | | | "href": "http://127.0.0.1:9890/vnflcm/v2/vnf_instances/c98b05c7-bc96-43f8-a688-4d8079ffa3bf" | | | }, | | | "terminate": { | | | "href": "http://127.0.0.1:9890/vnflcm/v2/vnf_instances/c98b05c7-bc96-43f8-a688-4d8079ffa3bf/terminate" | | | }, | | | "scale": { | | | "href": "http://127.0.0.1:9890/vnflcm/v2/vnf_instances/c98b05c7-bc96-43f8-a688-4d8079ffa3bf/scale" | | | }, | | | "heal": { | | | "href": "http://127.0.0.1:9890/vnflcm/v2/vnf_instances/c98b05c7-bc96-43f8-a688-4d8079ffa3bf/heal" | | | }, | | | "changeExtConn": { | | | "href": "http://127.0.0.1:9890/vnflcm/v2/vnf_instances/c98b05c7-bc96-43f8-a688-4d8079ffa3bf/change_ext_conn" | | | } | | | } | | VIM Connection Info | { | | | "default": { | | | "vimId": "bff267c4-6fc9-46b5-be53-15a6a3680033", | | | "vimType": "ETSINFV.OPENSTACK_KEYSTONE.V_3", | | | "interfaceInfo": { | | | "endpoint": "http://192.168.56.10/identity/v3", | | | "skipCertificateHostnameCheck": true, | | | "skipCertificateVerification": true | | | }, | | | "accessInfo": { | | | "username": "nfv_user", | | | "region": "RegionOne", | | | "project": "nfv", | | | "projectDomain": "default", | | | "userDomain": "default" | | | }, | | | "extra": {} | | | } | | | } | | VNF Configurable Properties | | | VNF Instance Description | | | VNF Instance Name | | | VNF Product Name | Sample VNF | | VNF Provider | Company | | VNF Software Version | 1.0 | | VNFD ID | b1bb0ce7-ebca-4fa7-95ed-4840d70a1177 | | VNFD Version | 1.0 | +-----------------------------+--------------------------------------------------------------------------------------------------------------------------------+
终止和删除 VNF¶
终止 VNF¶
检查要终止的 VNF 实例 ID
$ openstack vnflcm list --os-tacker-api-version 2 +--------------------------------------+-------------------+---------------------+--------------+----------------------+------------------+--------------------------------------+ | ID | VNF Instance Name | Instantiation State | VNF Provider | VNF Software Version | VNF Product Name | VNFD ID | +--------------------------------------+-------------------+---------------------+--------------+----------------------+------------------+--------------------------------------+ | c98b05c7-bc96-43f8-a688-4d8079ffa3bf | | INSTANTIATED | Company | 1.0 | Sample VNF | b1bb0ce7-ebca-4fa7-95ed-4840d70a1177 | +--------------------------------------+-------------------+---------------------+--------------+----------------------+------------------+--------------------------------------+
终止 VNF 实例
执行终止命令
$ openstack vnflcm terminate c98b05c7-bc96-43f8-a688-4d8079ffa3bf --os-tacker-api-version 2 Terminate request for VNF Instance 'c98b05c7-bc96-43f8-a688-4d8079ffa3bf' has been accepted.
检查 VNF 实例的状态
$ openstack vnflcm list --os-tacker-api-version 2 +--------------------------------------+-------------------+---------------------+--------------+----------------------+------------------+--------------------------------------+ | ID | VNF Instance Name | Instantiation State | VNF Provider | VNF Software Version | VNF Product Name | VNFD ID | +--------------------------------------+-------------------+---------------------+--------------+----------------------+------------------+--------------------------------------+ | c98b05c7-bc96-43f8-a688-4d8079ffa3bf | | NOT_INSTANTIATED | Company | 1.0 | Sample VNF | b1bb0ce7-ebca-4fa7-95ed-4840d70a1177 | +--------------------------------------+-------------------+---------------------+--------------+----------------------+------------------+--------------------------------------+
删除 VNF¶
删除 VNF 实例
$ openstack vnflcm delete c98b05c7-bc96-43f8-a688-4d8079ffa3bf --os-tacker-api-version 2 Vnf instance 'c98b05c7-bc96-43f8-a688-4d8079ffa3bf' is deleted successfully
删除 VNF 包¶
删除 VNF 包
检查要删除的 VNF 包 ID
$ openstack vnf package list +--------------------------------------+------------------+------------------+-------------+-------------------+-------------------------------------------------------------------------------------------------+ | Id | Vnf Product Name | Onboarding State | Usage State | Operational State | Links | +--------------------------------------+------------------+------------------+-------------+-------------------+-------------------------------------------------------------------------------------------------+ | 6e6b7a6d-0ebe-4085-96c2-b34269d837f9 | Sample VNF | ONBOARDED | NOT_IN_USE | ENABLED | { | | | | | | | "self": { | | | | | | | "href": "/vnfpkgm/v1/vnf_packages/6e6b7a6d-0ebe-4085-96c2-b34269d837f9" | | | | | | | }, | | | | | | | "packageContent": { | | | | | | | "href": "/vnfpkgm/v1/vnf_packages/6e6b7a6d-0ebe-4085-96c2-b34269d837f9/package_content" | | | | | | | } | | | | | | | } | +--------------------------------------+------------------+------------------+-------------+-------------------+-------------------------------------------------------------------------------------------------+
将运行状态更新为
DISABLED$ openstack vnf package update --operational-state 'DISABLED' \ 6e6b7a6d-0ebe-4085-96c2-b34269d837f9 +-------------------+----------+ | Field | Value | +-------------------+----------+ | Operational State | DISABLED | +-------------------+----------+
检查运行状态是否已更改
$ openstack vnf package list +--------------------------------------+------------------+------------------+-------------+-------------------+-------------------------------------------------------------------------------------------------+ | Id | Vnf Product Name | Onboarding State | Usage State | Operational State | Links | +--------------------------------------+------------------+------------------+-------------+-------------------+-------------------------------------------------------------------------------------------------+ | 6e6b7a6d-0ebe-4085-96c2-b34269d837f9 | Sample VNF | ONBOARDED | NOT_IN_USE | DISABLED | { | | | | | | | "self": { | | | | | | | "href": "/vnfpkgm/v1/vnf_packages/6e6b7a6d-0ebe-4085-96c2-b34269d837f9" | | | | | | | }, | | | | | | | "packageContent": { | | | | | | | "href": "/vnfpkgm/v1/vnf_packages/6e6b7a6d-0ebe-4085-96c2-b34269d837f9/package_content" | | | | | | | } | | | | | | | } | +--------------------------------------+------------------+------------------+-------------+-------------------+-------------------------------------------------------------------------------------------------+
删除 VNF 包
$ openstack vnf package delete 6e6b7a6d-0ebe-4085-96c2-b34269d837f9 All specified vnf-package(s) deleted successfully