This commit is contained in:
Wei Zhou 2026-01-22 12:09:18 +00:00 committed by GitHub
commit 732c232db2
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
10 changed files with 78 additions and 7 deletions

View File

@ -43,7 +43,7 @@ import java.util.List;
public class DeployVnfApplianceCmd extends DeployVMCmd implements UserCmd { public class DeployVnfApplianceCmd extends DeployVMCmd implements UserCmd {
@Parameter(name = ApiConstants.VNF_CONFIGURE_MANAGEMENT, type = CommandType.BOOLEAN, required = false, @Parameter(name = ApiConstants.VNF_CONFIGURE_MANAGEMENT, type = CommandType.BOOLEAN, required = false,
description = "True by default, security group or network rules (source nat and firewall rules) will be configured for VNF management interfaces. False otherwise. " + description = "False by default, security group or network rules (source nat and firewall rules) will be configured for VNF management interfaces. True otherwise. " +
"Network rules are configured if management network is an isolated network or shared network with security groups.") "Network rules are configured if management network is an isolated network or shared network with security groups.")
private Boolean vnfConfigureManagement; private Boolean vnfConfigureManagement;

View File

@ -29,6 +29,7 @@ import org.apache.cloudstack.api.command.user.template.UpdateVnfTemplateCmd;
import org.apache.cloudstack.api.command.user.vm.DeployVnfApplianceCmd; import org.apache.cloudstack.api.command.user.vm.DeployVnfApplianceCmd;
import org.apache.cloudstack.framework.config.ConfigKey; import org.apache.cloudstack.framework.config.ConfigKey;
import java.util.List; import java.util.List;
import java.util.Map;
public interface VnfTemplateManager { public interface VnfTemplateManager {
@ -42,11 +43,12 @@ public interface VnfTemplateManager {
void updateVnfTemplate(long templateId, UpdateVnfTemplateCmd cmd); void updateVnfTemplate(long templateId, UpdateVnfTemplateCmd cmd);
void validateVnfApplianceNics(VirtualMachineTemplate template, List<Long> networkIds); void validateVnfApplianceNics(VirtualMachineTemplate template, List<Long> networkIds, Map<Integer, Long> vmNetworkMap);
SecurityGroup createSecurityGroupForVnfAppliance(DataCenter zone, VirtualMachineTemplate template, Account owner, DeployVnfApplianceCmd cmd); SecurityGroup createSecurityGroupForVnfAppliance(DataCenter zone, VirtualMachineTemplate template, Account owner, DeployVnfApplianceCmd cmd);
void createIsolatedNetworkRulesForVnfAppliance(DataCenter zone, VirtualMachineTemplate template, Account owner, void createIsolatedNetworkRulesForVnfAppliance(DataCenter zone, VirtualMachineTemplate template, Account owner,
UserVm vm, DeployVnfApplianceCmd cmd) UserVm vm, DeployVnfApplianceCmd cmd)
throws InsufficientAddressCapacityException, ResourceAllocationException, ResourceUnavailableException; throws InsufficientAddressCapacityException, ResourceAllocationException, ResourceUnavailableException;
} }

View File

@ -16,6 +16,7 @@
// under the License. // under the License.
package org.apache.cloudstack.storage.template; package org.apache.cloudstack.storage.template;
import com.cloud.agent.api.to.deployasis.OVFNetworkTO;
import com.cloud.exception.InvalidParameterValueException; import com.cloud.exception.InvalidParameterValueException;
import com.cloud.network.VNF; import com.cloud.network.VNF;
import com.cloud.storage.Storage; import com.cloud.storage.Storage;
@ -124,6 +125,9 @@ public class VnfTemplateUtils {
public static void validateApiCommandParams(BaseCmd cmd, VirtualMachineTemplate template) { public static void validateApiCommandParams(BaseCmd cmd, VirtualMachineTemplate template) {
if (cmd instanceof RegisterVnfTemplateCmd) { if (cmd instanceof RegisterVnfTemplateCmd) {
RegisterVnfTemplateCmd registerCmd = (RegisterVnfTemplateCmd) cmd; RegisterVnfTemplateCmd registerCmd = (RegisterVnfTemplateCmd) cmd;
if (registerCmd.isDeployAsIs() && CollectionUtils.isNotEmpty(registerCmd.getVnfNics())) {
throw new InvalidParameterValueException("VNF nics cannot be specified when register a deploy-as-is Template. Please wait until Template settings are read from OVA.");
}
validateApiCommandParams(registerCmd.getVnfDetails(), registerCmd.getVnfNics(), registerCmd.getTemplateType()); validateApiCommandParams(registerCmd.getVnfDetails(), registerCmd.getVnfNics(), registerCmd.getTemplateType());
} else if (cmd instanceof UpdateVnfTemplateCmd) { } else if (cmd instanceof UpdateVnfTemplateCmd) {
UpdateVnfTemplateCmd updateCmd = (UpdateVnfTemplateCmd) cmd; UpdateVnfTemplateCmd updateCmd = (UpdateVnfTemplateCmd) cmd;
@ -149,4 +153,18 @@ public class VnfTemplateUtils {
} }
} }
} }
public static void validateDeployAsIsTemplateVnfNics(List<OVFNetworkTO> ovfNetworks, List<VNF.VnfNic> vnfNics) {
if (CollectionUtils.isEmpty(vnfNics)) {
return;
}
if (CollectionUtils.isEmpty(ovfNetworks)) {
throw new InvalidParameterValueException("The list of networks read from OVA is empty. Please wait until the template is fully downloaded and processed.");
}
for (VNF.VnfNic vnfNic : vnfNics) {
if (vnfNic.getDeviceId() < ovfNetworks.size() && !vnfNic.isRequired()) {
throw new InvalidParameterValueException(String.format("The VNF nic [device ID: %s ] is required as it is defined in the OVA template.", vnfNic.getDeviceId()));
}
}
}
} }

View File

@ -122,6 +122,7 @@ import com.cloud.agent.api.to.DatadiskTO;
import com.cloud.agent.api.to.DiskTO; import com.cloud.agent.api.to.DiskTO;
import com.cloud.agent.api.to.NfsTO; import com.cloud.agent.api.to.NfsTO;
import com.cloud.agent.api.to.VirtualMachineTO; import com.cloud.agent.api.to.VirtualMachineTO;
import com.cloud.agent.api.to.deployasis.OVFNetworkTO;
import com.cloud.api.ApiDBUtils; import com.cloud.api.ApiDBUtils;
import com.cloud.api.query.dao.UserVmJoinDao; import com.cloud.api.query.dao.UserVmJoinDao;
import com.cloud.api.query.vo.UserVmJoinVO; import com.cloud.api.query.vo.UserVmJoinVO;
@ -131,6 +132,7 @@ import com.cloud.dc.DataCenter;
import com.cloud.dc.DataCenterVO; import com.cloud.dc.DataCenterVO;
import com.cloud.dc.dao.DataCenterDao; import com.cloud.dc.dao.DataCenterDao;
import com.cloud.deploy.DeployDestination; import com.cloud.deploy.DeployDestination;
import com.cloud.deployasis.dao.TemplateDeployAsIsDetailsDao;
import com.cloud.domain.Domain; import com.cloud.domain.Domain;
import com.cloud.domain.dao.DomainDao; import com.cloud.domain.dao.DomainDao;
import com.cloud.event.ActionEvent; import com.cloud.event.ActionEvent;
@ -313,6 +315,8 @@ public class TemplateManagerImpl extends ManagerBase implements TemplateManager,
protected SnapshotHelper snapshotHelper; protected SnapshotHelper snapshotHelper;
@Inject @Inject
VnfTemplateManager vnfTemplateManager; VnfTemplateManager vnfTemplateManager;
@Inject
TemplateDeployAsIsDetailsDao templateDeployAsIsDetailsDao;
@Inject @Inject
private SecondaryStorageHeuristicDao secondaryStorageHeuristicDao; private SecondaryStorageHeuristicDao secondaryStorageHeuristicDao;
@ -2172,6 +2176,11 @@ public class TemplateManagerImpl extends ManagerBase implements TemplateManager,
templateType = validateTemplateType(cmd, isAdmin, template.isCrossZones()); templateType = validateTemplateType(cmd, isAdmin, template.isCrossZones());
if (cmd instanceof UpdateVnfTemplateCmd) { if (cmd instanceof UpdateVnfTemplateCmd) {
VnfTemplateUtils.validateApiCommandParams(cmd, template); VnfTemplateUtils.validateApiCommandParams(cmd, template);
UpdateVnfTemplateCmd updateCmd = (UpdateVnfTemplateCmd) cmd;
if (template.isDeployAsIs() && CollectionUtils.isNotEmpty(updateCmd.getVnfNics())) {
List<OVFNetworkTO> ovfNetworks = templateDeployAsIsDetailsDao.listNetworkRequirementsByTemplateId(template.getId());
VnfTemplateUtils.validateDeployAsIsTemplateVnfNics(ovfNetworks, updateCmd.getVnfNics());
}
vnfTemplateManager.updateVnfTemplate(template.getId(), (UpdateVnfTemplateCmd) cmd); vnfTemplateManager.updateVnfTemplate(template.getId(), (UpdateVnfTemplateCmd) cmd);
} }
templateTag = ((UpdateTemplateCmd)cmd).getTemplateTag(); templateTag = ((UpdateTemplateCmd)cmd).getTemplateTag();

View File

@ -6127,7 +6127,7 @@ public class UserVmManagerImpl extends ManagerBase implements UserVmManager, Vir
throw new InvalidParameterValueException("Unable to use template " + templateId); throw new InvalidParameterValueException("Unable to use template " + templateId);
} }
if (TemplateType.VNF.equals(template.getTemplateType())) { if (TemplateType.VNF.equals(template.getTemplateType())) {
vnfTemplateManager.validateVnfApplianceNics(template, cmd.getNetworkIds()); vnfTemplateManager.validateVnfApplianceNics(template, cmd.getNetworkIds(), cmd.getVmNetworkMap());
} else if (cmd instanceof DeployVnfApplianceCmd) { } else if (cmd instanceof DeployVnfApplianceCmd) {
throw new InvalidParameterValueException("Can't deploy VNF appliance from a non-VNF template"); throw new InvalidParameterValueException("Can't deploy VNF appliance from a non-VNF template");
} }

View File

@ -201,7 +201,14 @@ public class VnfTemplateManagerImpl extends ManagerBase implements VnfTemplateMa
} }
@Override @Override
public void validateVnfApplianceNics(VirtualMachineTemplate template, List<Long> networkIds) { public void validateVnfApplianceNics(VirtualMachineTemplate template, List<Long> networkIds, Map<Integer, Long> vmNetworkMap) {
if (template.isDeployAsIs()) {
if (CollectionUtils.isNotEmpty(networkIds)) {
throw new InvalidParameterValueException("VNF nics mappings should be empty for deploy-as-is templates");
}
validateVnfApplianceNetworksMap(template, vmNetworkMap);
return;
}
if (CollectionUtils.isEmpty(networkIds)) { if (CollectionUtils.isEmpty(networkIds)) {
throw new InvalidParameterValueException("VNF nics list is empty"); throw new InvalidParameterValueException("VNF nics list is empty");
} }
@ -213,6 +220,18 @@ public class VnfTemplateManagerImpl extends ManagerBase implements VnfTemplateMa
} }
} }
private void validateVnfApplianceNetworksMap(VirtualMachineTemplate template, Map<Integer, Long> vmNetworkMap) {
if (MapUtils.isEmpty(vmNetworkMap)) {
throw new InvalidParameterValueException("VNF networks map is empty");
}
List<VnfTemplateNicVO> vnfNics = vnfTemplateNicDao.listByTemplateId(template.getId());
for (VnfTemplateNicVO vnfNic : vnfNics) {
if (vnfNic.isRequired() && vmNetworkMap.size() <= vnfNic.getDeviceId()) {
throw new InvalidParameterValueException("VNF nic is required but not found: " + vnfNic);
}
}
}
protected Set<Integer> getOpenPortsForVnfAppliance(VirtualMachineTemplate template) { protected Set<Integer> getOpenPortsForVnfAppliance(VirtualMachineTemplate template) {
Set<Integer> ports = new HashSet<>(); Set<Integer> ports = new HashSet<>();
VnfTemplateDetailVO accessMethodsDetail = vnfTemplateDetailsDao.findDetail(template.getId(), VNF.AccessDetail.ACCESS_METHODS.name().toLowerCase()); VnfTemplateDetailVO accessMethodsDetail = vnfTemplateDetailsDao.findDetail(template.getId(), VNF.AccessDetail.ACCESS_METHODS.name().toLowerCase());

View File

@ -23,6 +23,7 @@ import com.cloud.agent.AgentManager;
import com.cloud.api.query.dao.UserVmJoinDao; import com.cloud.api.query.dao.UserVmJoinDao;
import com.cloud.configuration.Resource; import com.cloud.configuration.Resource;
import com.cloud.dc.dao.DataCenterDao; import com.cloud.dc.dao.DataCenterDao;
import com.cloud.deployasis.dao.TemplateDeployAsIsDetailsDao;
import com.cloud.domain.dao.DomainDao; import com.cloud.domain.dao.DomainDao;
import com.cloud.event.dao.UsageEventDao; import com.cloud.event.dao.UsageEventDao;
import com.cloud.exception.InvalidParameterValueException; import com.cloud.exception.InvalidParameterValueException;
@ -204,6 +205,8 @@ public class TemplateManagerImplTest {
AccountManager _accountMgr; AccountManager _accountMgr;
@Inject @Inject
VnfTemplateManager vnfTemplateManager; VnfTemplateManager vnfTemplateManager;
@Inject
TemplateDeployAsIsDetailsDao templateDeployAsIsDetailsDao;
@Inject @Inject
HeuristicRuleHelper heuristicRuleHelperMock; HeuristicRuleHelper heuristicRuleHelperMock;
@ -956,6 +959,11 @@ public class TemplateManagerImplTest {
return Mockito.mock(VnfTemplateManager.class); return Mockito.mock(VnfTemplateManager.class);
} }
@Bean
public TemplateDeployAsIsDetailsDao templateDeployAsIsDetailsDao() {
return Mockito.mock(TemplateDeployAsIsDetailsDao.class);
}
@Bean @Bean
public SnapshotHelper snapshotHelper() { public SnapshotHelper snapshotHelper() {
return Mockito.mock(SnapshotHelper.class); return Mockito.mock(SnapshotHelper.class);

View File

@ -2528,7 +2528,7 @@
"label.vnf.cidr.list": "CIDR from which access to the VNF appliance's Management interface should be allowed from", "label.vnf.cidr.list": "CIDR from which access to the VNF appliance's Management interface should be allowed from",
"label.vnf.cidr.list.tooltip": "the CIDR list to forward traffic from to the VNF management interface. Multiple entries must be separated by a single comma character (,). The default value is 0.0.0.0/0.", "label.vnf.cidr.list.tooltip": "the CIDR list to forward traffic from to the VNF management interface. Multiple entries must be separated by a single comma character (,). The default value is 0.0.0.0/0.",
"label.vnf.configure.management": "Configure network rules for VNF's management interfaces", "label.vnf.configure.management": "Configure network rules for VNF's management interfaces",
"label.vnf.configure.management.tooltip": "True by default, security group or network rules (source nat and firewall rules) will be configured for VNF management interfaces. False otherwise. Learn what rules are configured at http://docs.cloudstack.apache.org/en/latest/adminguide/networking/vnf_templates_appliances.html#deploying-vnf-appliances", "label.vnf.configure.management.tooltip": "False by default, security group or network rules (source nat and firewall rules) will be configured for VNF management interfaces. True otherwise. Learn what rules are configured at http://docs.cloudstack.apache.org/en/latest/adminguide/networking/vnf_templates_appliances.html#deploying-vnf-appliances",
"label.vnf.detail.add": "Add VNF detail", "label.vnf.detail.add": "Add VNF detail",
"label.vnf.detail.remove": "Remove VNF detail", "label.vnf.detail.remove": "Remove VNF detail",
"label.vnf.details": "VNF Details", "label.vnf.details": "VNF Details",

View File

@ -372,6 +372,7 @@
<div> <div>
<vnf-nics-selection <vnf-nics-selection
:items="templateVnfNics" :items="templateVnfNics"
:templateNics="templateNics"
:networks="networks" :networks="networks"
@update-vnf-nic-networks="($event) => updateVnfNicNetworks($event)" /> @update-vnf-nic-networks="($event) => updateVnfNicNetworks($event)" />
</div> </div>
@ -1293,7 +1294,8 @@ export default {
return tabList return tabList
}, },
showVnfNicsSection () { showVnfNicsSection () {
return this.networks && this.networks.length > 0 && this.vm.templateid && this.templateVnfNics && this.templateVnfNics.length > 0 return ((this.networks && this.networks.length > 0) || (this.templateNics && this.templateNics.length > 0)) &&
this.vm.templateid && this.templateVnfNics && this.templateVnfNics.length > 0
}, },
showVnfConfigureManagement () { showVnfConfigureManagement () {
const managementDeviceIds = [] const managementDeviceIds = []
@ -1303,6 +1305,11 @@ export default {
} }
} }
for (const deviceId of managementDeviceIds) { for (const deviceId of managementDeviceIds) {
if (this.templateNics && this.templateNics[deviceId] &&
((this.templateNics[deviceId].selectednetworktype === 'Isolated' && this.templateNics[deviceId].selectednetworkvpcid === undefined) ||
(this.templateNics[deviceId].selectednetworktype === 'Shared' && this.templateNics[deviceId].selectednetworkwithsg))) {
return true
}
if (this.vnfNicNetworks && this.vnfNicNetworks[deviceId] && if (this.vnfNicNetworks && this.vnfNicNetworks[deviceId] &&
((this.vnfNicNetworks[deviceId].type === 'Isolated' && this.vnfNicNetworks[deviceId].vpcid === undefined) || ((this.vnfNicNetworks[deviceId].type === 'Isolated' && this.vnfNicNetworks[deviceId].vpcid === undefined) ||
(this.vnfNicNetworks[deviceId].type === 'Shared' && this.vnfNicNetworks[deviceId].service.filter(svc => svc.name === 'SecurityGroupProvider')))) { (this.vnfNicNetworks[deviceId].type === 'Shared' && this.vnfNicNetworks[deviceId].service.filter(svc => svc.name === 'SecurityGroupProvider')))) {
@ -2005,7 +2012,7 @@ export default {
// All checked networks should be used and only once. // All checked networks should be used and only once.
// Required NIC must be associated to a network // Required NIC must be associated to a network
// DeviceID must be consequent // DeviceID must be consequent
if (this.templateVnfNics && this.templateVnfNics.length > 0) { if (this.templateVnfNics && this.templateVnfNics.length > 0 && (!this.templateNics || this.templateNics.length === 0)) {
let nextDeviceId = 0 let nextDeviceId = 0
const usedNetworkIds = [] const usedNetworkIds = []
const keys = Object.keys(this.vnfNicNetworks) const keys = Object.keys(this.vnfNicNetworks)
@ -2629,6 +2636,9 @@ export default {
var network = this.options.networks[Math.min(i, this.options.networks.length - 1)] var network = this.options.networks[Math.min(i, this.options.networks.length - 1)]
nic.selectednetworkid = network.id nic.selectednetworkid = network.id
nic.selectednetworkname = network.name nic.selectednetworkname = network.name
nic.selectednetworktype = network.type
nic.selectednetworkvpcid = network.vpcid
nic.selectednetworkwithsg = network.service.filter(svc => svc.name === 'SecurityGroupProvider').length > 0
this.nicToNetworkSelection.push({ nic: nic.id, network: network.id }) this.nicToNetworkSelection.push({ nic: nic.id, network: network.id })
} }
} }

View File

@ -50,6 +50,7 @@
<template #network="{ record }"> <template #network="{ record }">
<a-form-item style="display: block" :name="'nic-' + record.deviceid"> <a-form-item style="display: block" :name="'nic-' + record.deviceid">
<a-select <a-select
disabled="templateNics && templateNics.length > 0"
@change="updateNicNetworkValue($event, record.deviceid)" @change="updateNicNetworkValue($event, record.deviceid)"
optionFilterProp="label" optionFilterProp="label"
:filterOption="(input, option) => { :filterOption="(input, option) => {
@ -75,6 +76,10 @@ export default {
type: Array, type: Array,
default: () => [] default: () => []
}, },
templateNics: {
type: Array,
default: () => []
},
networks: { networks: {
type: Array, type: Array,
default: () => [] default: () => []