diff --git a/api/src/main/java/com/cloud/network/VNF.java b/api/src/main/java/com/cloud/network/VNF.java new file mode 100644 index 00000000000..ebc11c1f39b --- /dev/null +++ b/api/src/main/java/com/cloud/network/VNF.java @@ -0,0 +1,112 @@ +// Licensed to the Apache Software Foundation (ASF) under one +// or more contributor license agreements. See the NOTICE file +// distributed with this work for additional information +// regarding copyright ownership. The ASF licenses this file +// to you 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. +package com.cloud.network; + +import org.apache.commons.lang3.StringUtils; + +public class VNF { + + public enum AccessMethod { + SSH_WITH_PASSWORD("ssh-password"), + SSH_WITH_KEY("ssh-key"), + HTTP("http"), + HTTPS("https"), + CONSOLE("console"); + + String _method; + + AccessMethod(String method) { + _method = method; + } + + @Override + public String toString() { + return _method; + } + + public static AccessMethod fromValue(String method) { + if (StringUtils.isBlank(method)) { + return null; + } else { + for (AccessMethod accessMethod : AccessMethod.values()) { + if (accessMethod.toString().equalsIgnoreCase(method)) { + return accessMethod; + } + } + } + return null; + } + } + + public enum AccessDetail { + ACCESS_METHODS, + USERNAME, + PASSWORD, + SSH_USER, + SSH_PASSWORD, + SSH_PORT, + WEB_USER, + WEB_PASSWORD, + HTTP_PATH, + HTTP_PORT, + HTTPS_PATH, + HTTPS_PORT + } + + public enum VnfDetail { + ICON, + VERSION, + VENDOR, + MAINTAINER + } + + public static class VnfNic { + long deviceId; + String name; + boolean required; + boolean management; + String description; + + public VnfNic(long deviceId, String nicName, boolean required, boolean management, String nicDescription) { + this.deviceId = deviceId; + this.name = nicName; + this.required = required; + this.management = management; + this.description = nicDescription; + } + + public long getDeviceId() { + return deviceId; + } + + public String getName() { + return name; + } + + public boolean isRequired() { + return required; + } + + public boolean isManagement() { + return management; + } + + public String getDescription() { + return description; + } + } +} diff --git a/api/src/main/java/com/cloud/server/ResourceTag.java b/api/src/main/java/com/cloud/server/ResourceTag.java index 6288446cbdd..89ec5b905c9 100644 --- a/api/src/main/java/com/cloud/server/ResourceTag.java +++ b/api/src/main/java/com/cloud/server/ResourceTag.java @@ -30,6 +30,7 @@ public interface ResourceTag extends ControlledEntity, Identity, InternalIdentit public enum ResourceObjectType { UserVm(true, true, true), Template(true, true, true), + VnfTemplate(true, true, true), ISO(true, false, true), Volume(true, true), Snapshot(true, false), diff --git a/api/src/main/java/com/cloud/storage/Storage.java b/api/src/main/java/com/cloud/storage/Storage.java index c6dee56fa22..1ee7200a313 100644 --- a/api/src/main/java/com/cloud/storage/Storage.java +++ b/api/src/main/java/com/cloud/storage/Storage.java @@ -125,6 +125,7 @@ public class Storage { BUILTIN, /* buildin template */ PERHOST, /* every host has this template, don't need to install it in secondary storage */ USER, /* User supplied template/iso */ + VNF, /* VNFs (virtual network functions) template */ DATADISK, /* Template corresponding to a datadisk(non root disk) present in an OVA */ ISODISK /* Template corresponding to a iso (non root disk) present in an OVA */ } diff --git a/api/src/main/java/org/apache/cloudstack/api/ApiConstants.java b/api/src/main/java/org/apache/cloudstack/api/ApiConstants.java index 3fbd43b143a..c95e5c8bddb 100644 --- a/api/src/main/java/org/apache/cloudstack/api/ApiConstants.java +++ b/api/src/main/java/org/apache/cloudstack/api/ApiConstants.java @@ -436,6 +436,7 @@ public class ApiConstants { public static final String TEMPLATE_ID = "templateid"; public static final String TEMPLATE_IDS = "templateids"; public static final String TEMPLATE_NAME = "templatename"; + public static final String TEMPLATE_TYPE = "templatetype"; public static final String TIMEOUT = "timeout"; public static final String TIMEZONE = "timezone"; public static final String TIMEZONEOFFSET = "timezoneoffset"; @@ -1013,7 +1014,6 @@ public class ApiConstants { public static final String DEPLOY_AS_IS = "deployasis"; public static final String DEPLOY_AS_IS_DETAILS = "deployasisdetails"; public static final String CROSS_ZONES = "crossZones"; - public static final String TEMPLATETYPE = "templatetype"; public static final String SOURCETEMPLATEID = "sourcetemplateid"; public static final String DYNAMIC_SCALING_ENABLED = "dynamicscalingenabled"; public static final String IOTHREADS_ENABLED = "iothreadsenabled"; @@ -1047,6 +1047,15 @@ public class ApiConstants { public static final String SOURCE_NAT_IP_ID = "sourcenatipaddressid"; public static final String HAS_RULES = "hasrules"; + public static final String MANAGEMENT = "management"; + public static final String IS_VNF = "isvnf"; + public static final String VNF_NICS = "vnfnics"; + public static final String VNF_DETAILS = "vnfdetails"; + public static final String CLEAN_UP_VNF_DETAILS = "cleanupvnfdetails"; + public static final String CLEAN_UP_VNF_NICS = "cleanupvnfnics"; + public static final String VNF_CONFIGURE_MANAGEMENT = "vnfconfiguremanagement"; + public static final String VNF_CIDR_LIST = "vnfcidrlist"; + /** * This enum specifies IO Drivers, each option controls specific policies on I/O. * Qemu guests support "threads" and "native" options Since 0.8.8 ; "io_uring" is supported Since 6.3.0 (QEMU 5.0). @@ -1092,7 +1101,7 @@ public class ApiConstants { } public enum VMDetails { - all, group, nics, stats, secgrp, tmpl, servoff, diskoff, backoff, iso, volume, min, affgrp; + all, group, nics, stats, secgrp, tmpl, servoff, diskoff, backoff, iso, volume, min, affgrp, vnfnics; } public enum DomainDetails { diff --git a/api/src/main/java/org/apache/cloudstack/api/BaseCmd.java b/api/src/main/java/org/apache/cloudstack/api/BaseCmd.java index 79e103a291d..0b80cfc8229 100644 --- a/api/src/main/java/org/apache/cloudstack/api/BaseCmd.java +++ b/api/src/main/java/org/apache/cloudstack/api/BaseCmd.java @@ -43,6 +43,7 @@ import org.apache.cloudstack.network.lb.ApplicationLoadBalancerService; import org.apache.cloudstack.network.lb.InternalLoadBalancerVMService; import org.apache.cloudstack.query.QueryService; import org.apache.cloudstack.storage.ImageStoreService; +import org.apache.cloudstack.storage.template.VnfTemplateManager; import org.apache.cloudstack.usage.UsageService; import org.apache.commons.collections.MapUtils; import org.apache.log4j.Logger; @@ -213,6 +214,8 @@ public abstract class BaseCmd { public ResourceIconManager resourceIconManager; @Inject public Ipv6Service ipv6Service; + @Inject + public VnfTemplateManager vnfTemplateManager; public abstract void execute() throws ResourceUnavailableException, InsufficientCapacityException, ServerApiException, ConcurrentOperationException, ResourceAllocationException, NetworkRuleConflictException; diff --git a/api/src/main/java/org/apache/cloudstack/api/command/admin/template/ListVnfTemplatesCmdByAdmin.java b/api/src/main/java/org/apache/cloudstack/api/command/admin/template/ListVnfTemplatesCmdByAdmin.java new file mode 100644 index 00000000000..f231036bbc6 --- /dev/null +++ b/api/src/main/java/org/apache/cloudstack/api/command/admin/template/ListVnfTemplatesCmdByAdmin.java @@ -0,0 +1,32 @@ +// Licensed to the Apache Software Foundation (ASF) under one +// or more contributor license agreements. See the NOTICE file +// distributed with this work for additional information +// regarding copyright ownership. The ASF licenses this file +// to you 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. +package org.apache.cloudstack.api.command.admin.template; + +import org.apache.cloudstack.api.APICommand; +import org.apache.cloudstack.api.ResponseObject.ResponseView; +import org.apache.cloudstack.api.command.admin.AdminCmd; +import org.apache.cloudstack.api.command.user.template.ListVnfTemplatesCmd; +import org.apache.cloudstack.api.response.TemplateResponse; + +import com.cloud.template.VirtualMachineTemplate; + +@APICommand(name = "listVnfTemplates", description = "List all public, private, and privileged VNF templates.", + responseObject = TemplateResponse.class, entityType = {VirtualMachineTemplate.class}, responseView = ResponseView.Full, + requestHasSensitiveInfo = false, responseHasSensitiveInfo = false, + since = "4.19.0") +public class ListVnfTemplatesCmdByAdmin extends ListVnfTemplatesCmd implements AdminCmd { +} diff --git a/api/src/main/java/org/apache/cloudstack/api/command/admin/template/RegisterTemplateCmdByAdmin.java b/api/src/main/java/org/apache/cloudstack/api/command/admin/template/RegisterTemplateCmdByAdmin.java index 28593755c11..91c0dd50e8e 100644 --- a/api/src/main/java/org/apache/cloudstack/api/command/admin/template/RegisterTemplateCmdByAdmin.java +++ b/api/src/main/java/org/apache/cloudstack/api/command/admin/template/RegisterTemplateCmdByAdmin.java @@ -18,9 +18,11 @@ package org.apache.cloudstack.api.command.admin.template; import org.apache.cloudstack.api.APICommand; import org.apache.cloudstack.api.ResponseObject.ResponseView; +import org.apache.cloudstack.api.command.admin.AdminCmd; import org.apache.cloudstack.api.command.user.template.RegisterTemplateCmd; import org.apache.cloudstack.api.response.TemplateResponse; @APICommand(name = "registerTemplate", description = "Registers an existing template into the CloudStack cloud.", responseObject = TemplateResponse.class, responseView = ResponseView.Full, requestHasSensitiveInfo = false, responseHasSensitiveInfo = false) -public class RegisterTemplateCmdByAdmin extends RegisterTemplateCmd {} +public class RegisterTemplateCmdByAdmin extends RegisterTemplateCmd implements AdminCmd { +} diff --git a/api/src/main/java/org/apache/cloudstack/api/command/admin/template/RegisterVnfTemplateCmdByAdmin.java b/api/src/main/java/org/apache/cloudstack/api/command/admin/template/RegisterVnfTemplateCmdByAdmin.java new file mode 100644 index 00000000000..45b40bfebb1 --- /dev/null +++ b/api/src/main/java/org/apache/cloudstack/api/command/admin/template/RegisterVnfTemplateCmdByAdmin.java @@ -0,0 +1,31 @@ +// Licensed to the Apache Software Foundation (ASF) under one +// or more contributor license agreements. See the NOTICE file +// distributed with this work for additional information +// regarding copyright ownership. The ASF licenses this file +// to you 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. +package org.apache.cloudstack.api.command.admin.template; + +import org.apache.cloudstack.api.APICommand; +import org.apache.cloudstack.api.ResponseObject.ResponseView; +import org.apache.cloudstack.api.command.admin.AdminCmd; +import org.apache.cloudstack.api.command.user.template.RegisterVnfTemplateCmd; +import org.apache.cloudstack.api.response.TemplateResponse; + +@APICommand(name = "registerVnfTemplate", + description = "Registers an existing VNF template into the CloudStack cloud. ", + responseObject = TemplateResponse.class, responseView = ResponseView.Full, + requestHasSensitiveInfo = false, responseHasSensitiveInfo = false, + since = "4.19.0") +public class RegisterVnfTemplateCmdByAdmin extends RegisterVnfTemplateCmd implements AdminCmd { +} diff --git a/api/src/main/java/org/apache/cloudstack/api/command/admin/template/UpdateTemplateCmdByAdmin.java b/api/src/main/java/org/apache/cloudstack/api/command/admin/template/UpdateTemplateCmdByAdmin.java index 09591c809b1..b1dfae3ed83 100644 --- a/api/src/main/java/org/apache/cloudstack/api/command/admin/template/UpdateTemplateCmdByAdmin.java +++ b/api/src/main/java/org/apache/cloudstack/api/command/admin/template/UpdateTemplateCmdByAdmin.java @@ -24,4 +24,5 @@ import org.apache.cloudstack.api.response.TemplateResponse; @APICommand(name = "updateTemplate", description = "Updates attributes of a template.", responseObject = TemplateResponse.class, responseView = ResponseView.Full, requestHasSensitiveInfo = false, responseHasSensitiveInfo = false) -public class UpdateTemplateCmdByAdmin extends UpdateTemplateCmd implements AdminCmd {} +public class UpdateTemplateCmdByAdmin extends UpdateTemplateCmd implements AdminCmd { +} diff --git a/api/src/main/java/org/apache/cloudstack/api/command/admin/template/UpdateVnfTemplateCmdByAdmin.java b/api/src/main/java/org/apache/cloudstack/api/command/admin/template/UpdateVnfTemplateCmdByAdmin.java new file mode 100644 index 00000000000..102a4703d4c --- /dev/null +++ b/api/src/main/java/org/apache/cloudstack/api/command/admin/template/UpdateVnfTemplateCmdByAdmin.java @@ -0,0 +1,31 @@ +// Licensed to the Apache Software Foundation (ASF) under one +// or more contributor license agreements. See the NOTICE file +// distributed with this work for additional information +// regarding copyright ownership. The ASF licenses this file +// to you 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. +package org.apache.cloudstack.api.command.admin.template; + +import org.apache.cloudstack.api.APICommand; +import org.apache.cloudstack.api.ResponseObject.ResponseView; +import org.apache.cloudstack.api.command.admin.AdminCmd; +import org.apache.cloudstack.api.command.user.template.UpdateVnfTemplateCmd; +import org.apache.cloudstack.api.response.TemplateResponse; + +@APICommand(name = "updateVnfTemplate", + description = "Updates a template to VNF template or attributes of a VNF template.", + responseObject = TemplateResponse.class, responseView = ResponseView.Full, + requestHasSensitiveInfo = false, responseHasSensitiveInfo = false, + since = "4.19.0") +public class UpdateVnfTemplateCmdByAdmin extends UpdateVnfTemplateCmd implements AdminCmd { +} diff --git a/api/src/main/java/org/apache/cloudstack/api/command/admin/vm/DeployVnfApplianceCmdByAdmin.java b/api/src/main/java/org/apache/cloudstack/api/command/admin/vm/DeployVnfApplianceCmdByAdmin.java new file mode 100644 index 00000000000..5d2756c82fc --- /dev/null +++ b/api/src/main/java/org/apache/cloudstack/api/command/admin/vm/DeployVnfApplianceCmdByAdmin.java @@ -0,0 +1,34 @@ +// Licensed to the Apache Software Foundation (ASF) under one +// or more contributor license agreements. See the NOTICE file +// distributed with this work for additional information +// regarding copyright ownership. The ASF licenses this file +// to you 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. +package org.apache.cloudstack.api.command.admin.vm; + +import com.cloud.vm.VirtualMachine; +import org.apache.cloudstack.api.APICommand; +import org.apache.cloudstack.api.ResponseObject; +import org.apache.cloudstack.api.command.admin.AdminCmd; +import org.apache.cloudstack.api.command.user.vm.DeployVnfApplianceCmd; +import org.apache.cloudstack.api.response.UserVmResponse; + +@APICommand(name = "deployVnfAppliance", + description = "Creates and automatically starts a VNF appliance based on a service offering, disk offering, and template.", + responseObject = UserVmResponse.class, + responseView = ResponseObject.ResponseView.Full, + entityType = {VirtualMachine.class}, + requestHasSensitiveInfo = false, responseHasSensitiveInfo = true, + since = "4.19.0") +public class DeployVnfApplianceCmdByAdmin extends DeployVnfApplianceCmd implements AdminCmd { +} diff --git a/api/src/main/java/org/apache/cloudstack/api/command/user/template/DeleteVnfTemplateCmd.java b/api/src/main/java/org/apache/cloudstack/api/command/user/template/DeleteVnfTemplateCmd.java new file mode 100755 index 00000000000..c2c712cd2bb --- /dev/null +++ b/api/src/main/java/org/apache/cloudstack/api/command/user/template/DeleteVnfTemplateCmd.java @@ -0,0 +1,47 @@ +// Licensed to the Apache Software Foundation (ASF) under one +// or more contributor license agreements. See the NOTICE file +// distributed with this work for additional information +// regarding copyright ownership. The ASF licenses this file +// to you 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. +package org.apache.cloudstack.api.command.user.template; + +import org.apache.cloudstack.acl.RoleType; +import org.apache.cloudstack.api.APICommand; +import org.apache.cloudstack.api.response.SuccessResponse; + +import com.cloud.template.VirtualMachineTemplate; +import com.cloud.user.Account; + +@APICommand(name = "deleteVnfTemplate", + responseObject = SuccessResponse.class, + description = "Deletes a VNF template from the system. All virtual machines using the deleted template will not be affected.", + requestHasSensitiveInfo = false, responseHasSensitiveInfo = false, + authorized = {RoleType.Admin, RoleType.ResourceAdmin, RoleType.DomainAdmin, RoleType.User}, + since = "4.19.0") +public class DeleteVnfTemplateCmd extends DeleteTemplateCmd { + + ///////////////////////////////////////////////////// + /////////////// API Implementation/////////////////// + ///////////////////////////////////////////////////// + + @Override + public long getEntityOwnerId() { + VirtualMachineTemplate template = _entityMgr.findById(VirtualMachineTemplate.class, getId()); + if (template != null) { + return template.getAccountId(); + } + + return Account.ACCOUNT_ID_SYSTEM; + } +} diff --git a/api/src/main/java/org/apache/cloudstack/api/command/user/template/ListTemplatesCmd.java b/api/src/main/java/org/apache/cloudstack/api/command/user/template/ListTemplatesCmd.java index a64ce195c0f..26d79084531 100644 --- a/api/src/main/java/org/apache/cloudstack/api/command/user/template/ListTemplatesCmd.java +++ b/api/src/main/java/org/apache/cloudstack/api/command/user/template/ListTemplatesCmd.java @@ -96,6 +96,15 @@ public class ListTemplatesCmd extends BaseListTaggedResourcesCmd implements User description = "comma separated list of template details requested, value can be a list of [ all, min]") private List viewDetails; + @Parameter(name = ApiConstants.TEMPLATE_TYPE, type = CommandType.STRING, + description = "the type of the template", since = "4.19.0") + private String templateType; + + @Parameter(name = ApiConstants.IS_VNF, type = CommandType.BOOLEAN, + description = "flag to list VNF templates or not; true if need to list VNF templates, false otherwise.", + since = "4.19.0") + private Boolean isVnf; + ///////////////////////////////////////////////////// /////////////////// Accessors /////////////////////// ///////////////////////////////////////////////////// @@ -151,6 +160,10 @@ public class ListTemplatesCmd extends BaseListTaggedResourcesCmd implements User return parentTemplateId; } + public String getTemplateType() { + return templateType; + } + public boolean listInReadyState() { Account account = CallContext.current().getCallingAccount(); @@ -175,6 +188,10 @@ public class ListTemplatesCmd extends BaseListTaggedResourcesCmd implements User return showIcon != null ? showIcon : false; } + public Boolean getVnf() { + return isVnf; + } + @Override public String getCommandName() { return s_name; diff --git a/api/src/main/java/org/apache/cloudstack/api/command/user/template/ListVnfTemplatesCmd.java b/api/src/main/java/org/apache/cloudstack/api/command/user/template/ListVnfTemplatesCmd.java new file mode 100644 index 00000000000..0a98a159527 --- /dev/null +++ b/api/src/main/java/org/apache/cloudstack/api/command/user/template/ListVnfTemplatesCmd.java @@ -0,0 +1,33 @@ +// Licensed to the Apache Software Foundation (ASF) under one +// or more contributor license agreements. See the NOTICE file +// distributed with this work for additional information +// regarding copyright ownership. The ASF licenses this file +// to you 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. +package org.apache.cloudstack.api.command.user.template; + +import org.apache.cloudstack.acl.RoleType; +import org.apache.cloudstack.api.APICommand; +import org.apache.cloudstack.api.ResponseObject.ResponseView; +import org.apache.cloudstack.api.command.user.UserCmd; +import org.apache.cloudstack.api.response.TemplateResponse; + +import com.cloud.template.VirtualMachineTemplate; + +@APICommand(name = "listVnfTemplates", description = "List all public, private, and privileged VNF templates.", + responseObject = TemplateResponse.class, entityType = {VirtualMachineTemplate.class}, responseView = ResponseView.Restricted, + requestHasSensitiveInfo = false, responseHasSensitiveInfo = false, + authorized = {RoleType.Admin, RoleType.ResourceAdmin, RoleType.DomainAdmin, RoleType.User}, + since = "4.19.0") +public class ListVnfTemplatesCmd extends ListTemplatesCmd implements UserCmd { +} diff --git a/api/src/main/java/org/apache/cloudstack/api/command/user/template/RegisterTemplateCmd.java b/api/src/main/java/org/apache/cloudstack/api/command/user/template/RegisterTemplateCmd.java index 5709e3ed900..0a087888d52 100644 --- a/api/src/main/java/org/apache/cloudstack/api/command/user/template/RegisterTemplateCmd.java +++ b/api/src/main/java/org/apache/cloudstack/api/command/user/template/RegisterTemplateCmd.java @@ -143,6 +143,7 @@ public class RegisterTemplateCmd extends BaseCmd implements UserCmd { description = "true if template contains XS/VMWare tools inorder to support dynamic scaling of VM cpu/memory") protected Boolean isDynamicallyScalable; + @Deprecated @Parameter(name = ApiConstants.ROUTING, type = CommandType.BOOLEAN, description = "true if the template type is routing i.e., if template is used to deploy router") protected Boolean isRoutingType; @@ -168,6 +169,11 @@ public class RegisterTemplateCmd extends BaseCmd implements UserCmd { description = "(VMware only) true if VM deployments should preserve all the configurations defined for this template", since = "4.15.1") protected Boolean deployAsIs; + @Parameter(name = ApiConstants.TEMPLATE_TYPE, type = CommandType.STRING, + description = "the type of the template. Valid options are: USER/VNF (for all users) and SYSTEM/ROUTING/BUILTIN (for admins only).", + since = "4.19.0") + private String templateType; + ///////////////////////////////////////////////////// /////////////////// Accessors /////////////////////// ///////////////////////////////////////////////////// @@ -285,6 +291,10 @@ public class RegisterTemplateCmd extends BaseCmd implements UserCmd { Boolean.TRUE.equals(deployAsIs); } + public String getTemplateType() { + return templateType; + } + ///////////////////////////////////////////////////// /////////////// API Implementation/////////////////// ///////////////////////////////////////////////////// @@ -315,7 +325,7 @@ public class RegisterTemplateCmd extends BaseCmd implements UserCmd { VirtualMachineTemplate template = _templateService.registerTemplate(this); if (template != null) { - ListResponse response = new ListResponse(); + ListResponse response = new ListResponse<>(); List templateResponses = _responseGenerator.createTemplateResponses(getResponseView(), template, getZoneIds(), false); response.setResponses(templateResponses); diff --git a/api/src/main/java/org/apache/cloudstack/api/command/user/template/RegisterVnfTemplateCmd.java b/api/src/main/java/org/apache/cloudstack/api/command/user/template/RegisterVnfTemplateCmd.java new file mode 100644 index 00000000000..c3e99d5791f --- /dev/null +++ b/api/src/main/java/org/apache/cloudstack/api/command/user/template/RegisterVnfTemplateCmd.java @@ -0,0 +1,79 @@ +// Licensed to the Apache Software Foundation (ASF) under one +// or more contributor license agreements. See the NOTICE file +// distributed with this work for additional information +// regarding copyright ownership. The ASF licenses this file +// to you 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. +package org.apache.cloudstack.api.command.user.template; + +import java.util.List; +import java.util.Map; + +import com.cloud.network.VNF; +import org.apache.cloudstack.acl.RoleType; +import org.apache.cloudstack.api.APICommand; +import org.apache.cloudstack.api.ApiConstants; +import org.apache.cloudstack.api.Parameter; +import org.apache.cloudstack.api.ResponseObject.ResponseView; +import org.apache.cloudstack.api.command.user.UserCmd; +import org.apache.cloudstack.api.response.TemplateResponse; +import org.apache.cloudstack.storage.template.VnfTemplateUtils; + +@APICommand(name = "registerVnfTemplate", + description = "Registers an existing VNF template into the CloudStack cloud. ", + responseObject = TemplateResponse.class, responseView = ResponseView.Restricted, + requestHasSensitiveInfo = false, responseHasSensitiveInfo = false, + authorized = {RoleType.Admin, RoleType.ResourceAdmin, RoleType.DomainAdmin, RoleType.User}, + since = "4.19.0") +public class RegisterVnfTemplateCmd extends RegisterTemplateCmd implements UserCmd { + + ///////////////////////////////////////////////////// + //////////////// API parameters ///////////////////// + ///////////////////////////////////////////////////// + + @Parameter(name = ApiConstants.VNF_NICS, + type = CommandType.MAP, + description = "VNF nics in key/value pairs using format vnfnics[i].keyname=keyvalue. " + + " Example: vnfnics[0].deviceid=0&&vnfnics[0].name=FirstNIC&&vnfnics[0].required=true" + + "&&vnfnics[1].deviceid=1&&vnfnics[1].name=SecondNIC") + protected Map vnfNics; + + @Parameter(name = ApiConstants.VNF_DETAILS, + type = CommandType.MAP, + description = "VNF details in key/value pairs using format vnfdetails[i].keyname=keyvalue. " + + "Example: vnfdetails[0].vendor=xxx&&vnfdetails[0].version=2.0") + protected Map vnfDetails; + + ///////////////////////////////////////////////////// + /////////////////// Accessors /////////////////////// + ///////////////////////////////////////////////////// + + public List getVnfNics() { + return VnfTemplateUtils.getVnfNicsList(this.vnfNics); + } + + public Map getVnfDetails() { + return convertDetailsToMap(vnfDetails); + } + + ///////////////////////////////////////////////////// + /////////////// API Implementation/////////////////// + ///////////////////////////////////////////////////// + + @Override + protected void validateParameters() { + super.validateParameters(); + + VnfTemplateUtils.validateApiCommandParams(this, null); + } +} diff --git a/api/src/main/java/org/apache/cloudstack/api/command/user/template/UpdateTemplateCmd.java b/api/src/main/java/org/apache/cloudstack/api/command/user/template/UpdateTemplateCmd.java index 28ecd453d26..2afa6a98b13 100644 --- a/api/src/main/java/org/apache/cloudstack/api/command/user/template/UpdateTemplateCmd.java +++ b/api/src/main/java/org/apache/cloudstack/api/command/user/template/UpdateTemplateCmd.java @@ -16,10 +16,11 @@ // under the License. package org.apache.cloudstack.api.command.user.template; -import org.apache.cloudstack.api.ApiCommandResourceType; import org.apache.log4j.Logger; import org.apache.cloudstack.api.APICommand; +import org.apache.cloudstack.api.ApiCommandResourceType; +import org.apache.cloudstack.api.ApiConstants; import org.apache.cloudstack.api.ApiErrorCode; import org.apache.cloudstack.api.BaseUpdateTemplateOrIsoCmd; import org.apache.cloudstack.api.Parameter; @@ -41,7 +42,8 @@ public class UpdateTemplateCmd extends BaseUpdateTemplateOrIsoCmd implements Use //////////////// API parameters ///////////////////// ///////////////////////////////////////////////////// - @Parameter(name = "templatetype", type = CommandType.STRING, description = "the type of the template") + @Parameter(name = ApiConstants.TEMPLATE_TYPE, type = CommandType.STRING, + description = "the type of the template. Valid options are: USER/VNF (for all users) and SYSTEM/ROUTING/BUILTIN (for admins only).") private String templateType; ///////////////////////////////////////////////////// diff --git a/api/src/main/java/org/apache/cloudstack/api/command/user/template/UpdateVnfTemplateCmd.java b/api/src/main/java/org/apache/cloudstack/api/command/user/template/UpdateVnfTemplateCmd.java new file mode 100644 index 00000000000..7479dd3be09 --- /dev/null +++ b/api/src/main/java/org/apache/cloudstack/api/command/user/template/UpdateVnfTemplateCmd.java @@ -0,0 +1,87 @@ +// Licensed to the Apache Software Foundation (ASF) under one +// or more contributor license agreements. See the NOTICE file +// distributed with this work for additional information +// regarding copyright ownership. The ASF licenses this file +// to you 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. +package org.apache.cloudstack.api.command.user.template; + +import com.cloud.network.VNF; + +import org.apache.cloudstack.acl.RoleType; +import org.apache.cloudstack.api.APICommand; +import org.apache.cloudstack.api.ApiConstants; +import org.apache.cloudstack.api.Parameter; +import org.apache.cloudstack.api.ResponseObject.ResponseView; +import org.apache.cloudstack.api.command.user.UserCmd; +import org.apache.cloudstack.api.response.TemplateResponse; +import org.apache.cloudstack.storage.template.VnfTemplateUtils; + +import java.util.List; +import java.util.Map; + +@APICommand(name = "updateVnfTemplate", description = "Updates a template to VNF template or attributes of a VNF template.", + responseObject = TemplateResponse.class, responseView = ResponseView.Restricted, + requestHasSensitiveInfo = false, responseHasSensitiveInfo = false, + authorized = {RoleType.Admin, RoleType.ResourceAdmin, RoleType.DomainAdmin, RoleType.User}, + since = "4.19.0") +public class UpdateVnfTemplateCmd extends UpdateTemplateCmd implements UserCmd { + + ///////////////////////////////////////////////////// + //////////////// API parameters ///////////////////// + ///////////////////////////////////////////////////// + + @Parameter(name = ApiConstants.VNF_NICS, + type = CommandType.MAP, + description = "VNF nics in key/value pairs using format vnfnics[i].keyname=keyvalue. " + + " Example: vnfnics[0].deviceid=0&&vnfnics[0].name=FirstNIC&&vnfnics[0].required=true" + + "&&vnfnics[1].deviceid=1&&vnfnics[1].name=SecondNIC") + protected Map vnfNics; + + @Parameter(name = ApiConstants.VNF_DETAILS, + type = CommandType.MAP, + description = "VNF details in key/value pairs using format vnfdetails[i].keyname=keyvalue. " + + "Example: vnfdetails[0].vendor=xxx&&vnfdetails[0].version=2.0") + protected Map vnfDetails; + + @Parameter(name = ApiConstants.CLEAN_UP_VNF_DETAILS, + type = CommandType.BOOLEAN, + description = "optional boolean field, which indicates if VNF details will be cleaned up or not") + private Boolean cleanupVnfDetails = null; + + @Parameter(name = ApiConstants.CLEAN_UP_VNF_NICS, + type = CommandType.BOOLEAN, + description = "optional boolean field, which indicates if VNF nics will be cleaned up or not") + private Boolean cleanupVnfNics = null; + + ///////////////////////////////////////////////////// + /////////////////// Accessors /////////////////////// + ///////////////////////////////////////////////////// + + + public List getVnfNics() { + return VnfTemplateUtils.getVnfNicsList(this.vnfNics); + } + + public Map getVnfDetails() { + return convertDetailsToMap(vnfDetails); + } + + public boolean isCleanupVnfDetails(){ + return cleanupVnfDetails == null ? false : cleanupVnfDetails.booleanValue(); + } + + public boolean isCleanupVnfNics(){ + return cleanupVnfNics == null ? false : cleanupVnfNics.booleanValue(); + } +} diff --git a/api/src/main/java/org/apache/cloudstack/api/command/user/vm/DeployVnfApplianceCmd.java b/api/src/main/java/org/apache/cloudstack/api/command/user/vm/DeployVnfApplianceCmd.java new file mode 100644 index 00000000000..4d50dd9c39b --- /dev/null +++ b/api/src/main/java/org/apache/cloudstack/api/command/user/vm/DeployVnfApplianceCmd.java @@ -0,0 +1,74 @@ +// Licensed to the Apache Software Foundation (ASF) under one +// or more contributor license agreements. See the NOTICE file +// distributed with this work for additional information +// regarding copyright ownership. The ASF licenses this file +// to you 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. +package org.apache.cloudstack.api.command.user.vm; + +import com.cloud.exception.ResourceAllocationException; +import com.cloud.utils.net.NetUtils; +import com.cloud.vm.VirtualMachine; +import org.apache.cloudstack.acl.RoleType; +import org.apache.cloudstack.api.APICommand; +import org.apache.cloudstack.api.ApiConstants; +import org.apache.cloudstack.api.Parameter; +import org.apache.cloudstack.api.ResponseObject; +import org.apache.cloudstack.api.command.user.UserCmd; +import org.apache.cloudstack.api.response.UserVmResponse; +import org.apache.cloudstack.storage.template.VnfTemplateUtils; +import org.apache.commons.collections.CollectionUtils; + +import java.util.ArrayList; +import java.util.List; + +@APICommand(name = "deployVnfAppliance", + description = "Creates and automatically starts a VNF appliance based on a service offering, disk offering, and template.", + responseObject = UserVmResponse.class, + responseView = ResponseObject.ResponseView.Restricted, + entityType = {VirtualMachine.class}, + requestHasSensitiveInfo = false, responseHasSensitiveInfo = true, + authorized = {RoleType.Admin, RoleType.ResourceAdmin, RoleType.DomainAdmin, RoleType.User}, + since = "4.19.0") +public class DeployVnfApplianceCmd extends DeployVMCmd implements UserCmd { + + @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. " + + "Network rules are configured if management network is an isolated network or shared network with security groups.") + private Boolean vnfConfigureManagement; + + @Parameter(name = ApiConstants.VNF_CIDR_LIST, type = CommandType.LIST, collectionType = CommandType.STRING, + description = "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.") + private List vnfCidrlist; + + public Boolean getVnfConfigureManagement() { + return vnfConfigureManagement != null && vnfConfigureManagement; + } + + public List getVnfCidrlist() { + if (CollectionUtils.isNotEmpty(vnfCidrlist)) { + return vnfCidrlist; + } else { + List defaultCidrList = new ArrayList(); + defaultCidrList.add(NetUtils.ALL_IP4_CIDRS); + return defaultCidrList; + } + } + + @Override + public void create() throws ResourceAllocationException { + VnfTemplateUtils.validateVnfCidrList(this.getVnfCidrlist()); + + super.create(); + } +} diff --git a/api/src/main/java/org/apache/cloudstack/api/command/user/vm/ListVMsCmd.java b/api/src/main/java/org/apache/cloudstack/api/command/user/vm/ListVMsCmd.java index 07d83b47d3c..9ec625cf3cf 100644 --- a/api/src/main/java/org/apache/cloudstack/api/command/user/vm/ListVMsCmd.java +++ b/api/src/main/java/org/apache/cloudstack/api/command/user/vm/ListVMsCmd.java @@ -189,6 +189,10 @@ public class ListVMsCmd extends BaseListRetrieveOnlyResourceCountCmd implements return zoneId; } + @Parameter(name = ApiConstants.IS_VNF, type = CommandType.BOOLEAN, + description = "flag to list vms created from VNF templates (as known as VNF appliances) or not; true if need to list VNF appliances, false otherwise.", + since = "4.19.0") + private Boolean isVnf; public Long getNetworkId() { return networkId; @@ -266,6 +270,10 @@ public class ListVMsCmd extends BaseListRetrieveOnlyResourceCountCmd implements return accumulate; } + public Boolean getVnf() { + return isVnf; + } + ///////////////////////////////////////////////////// /////////////// API Implementation/////////////////// ///////////////////////////////////////////////////// diff --git a/api/src/main/java/org/apache/cloudstack/api/response/ChildTemplateResponse.java b/api/src/main/java/org/apache/cloudstack/api/response/ChildTemplateResponse.java index b036cd48e87..8f5b5de2919 100644 --- a/api/src/main/java/org/apache/cloudstack/api/response/ChildTemplateResponse.java +++ b/api/src/main/java/org/apache/cloudstack/api/response/ChildTemplateResponse.java @@ -39,7 +39,7 @@ public class ChildTemplateResponse extends BaseResponse { @Param(description = "the size of the template") private Integer size; - @SerializedName("templatetype") + @SerializedName(ApiConstants.TEMPLATE_TYPE) @Param(description = "the type of the template") private String templateType; diff --git a/api/src/main/java/org/apache/cloudstack/api/response/NicResponse.java b/api/src/main/java/org/apache/cloudstack/api/response/NicResponse.java index c267323cb7a..65e126de545 100644 --- a/api/src/main/java/org/apache/cloudstack/api/response/NicResponse.java +++ b/api/src/main/java/org/apache/cloudstack/api/response/NicResponse.java @@ -138,6 +138,14 @@ public class NicResponse extends BaseResponse { @Param(description = "MTU configured on the NIC", since="4.18.0") private Integer mtu; + @SerializedName(ApiConstants.PUBLIC_IP_ID) + @Param(description = "public IP address id associated with this nic via Static nat rule") + private String publicIpId; + + @SerializedName(ApiConstants.PUBLIC_IP) + @Param(description = "public IP address associated with this nic via Static nat rule") + private String publicIp; + public void setVmId(String vmId) { this.vmId = vmId; } @@ -400,4 +408,12 @@ public class NicResponse extends BaseResponse { public String getVpcId() { return vpcId; } + + public void setPublicIpId(String publicIpId) { + this.publicIpId = publicIpId; + } + + public void setPublicIp(String publicIp) { + this.publicIp = publicIp; + } } diff --git a/api/src/main/java/org/apache/cloudstack/api/response/TemplateResponse.java b/api/src/main/java/org/apache/cloudstack/api/response/TemplateResponse.java index bd09d098708..3abd44941d9 100644 --- a/api/src/main/java/org/apache/cloudstack/api/response/TemplateResponse.java +++ b/api/src/main/java/org/apache/cloudstack/api/response/TemplateResponse.java @@ -123,7 +123,7 @@ public class TemplateResponse extends BaseResponseWithTagInformation implements @Param(description = "the physical size of the template") private Long physicalSize; - @SerializedName(ApiConstants.TEMPLATETYPE) + @SerializedName(ApiConstants.TEMPLATE_TYPE) @Param(description = "the type of the template") private String templateType; diff --git a/api/src/main/java/org/apache/cloudstack/api/response/UserVmResponse.java b/api/src/main/java/org/apache/cloudstack/api/response/UserVmResponse.java index 114403da7bc..906529c13ab 100644 --- a/api/src/main/java/org/apache/cloudstack/api/response/UserVmResponse.java +++ b/api/src/main/java/org/apache/cloudstack/api/response/UserVmResponse.java @@ -16,9 +16,12 @@ // under the License. package org.apache.cloudstack.api.response; +import java.util.ArrayList; import java.util.Comparator; import java.util.Date; +import java.util.LinkedHashMap; import java.util.LinkedHashSet; +import java.util.List; import java.util.Map; import java.util.Set; import java.util.TreeSet; @@ -130,6 +133,10 @@ public class UserVmResponse extends BaseResponseWithTagInformation implements Co @Param(description = "the name of the template for the virtual machine") private String templateName; + @SerializedName(ApiConstants.TEMPLATE_TYPE) + @Param(description = "the type of the template for the virtual machine", since = "4.19.0") + private String templateType; + @SerializedName("templatedisplaytext") @Param(description = " an alternate display text of the template for the virtual machine") private String templateDisplayText; @@ -360,6 +367,14 @@ public class UserVmResponse extends BaseResponseWithTagInformation implements Co @SerializedName(ApiConstants.USER_DATA_DETAILS) @Param(description="list of variables and values for the variables declared in userdata", since = "4.18.0") private String userDataDetails; + @SerializedName(ApiConstants.VNF_NICS) + @Param(description = "NICs of the VNF appliance", since = "4.19.0") + private List vnfNics; + + @SerializedName(ApiConstants.VNF_DETAILS) + @Param(description = "VNF details", since = "4.19.0") + private Map vnfDetails; + public UserVmResponse() { securityGroupList = new LinkedHashSet<>(); nics = new TreeSet<>(Comparator.comparingInt(x -> Integer.parseInt(x.getDeviceId()))); @@ -1045,4 +1060,49 @@ public class UserVmResponse extends BaseResponseWithTagInformation implements Co this.userDataDetails = userDataDetails; } + public Long getBytesReceived() { + return bytesReceived; + } + + public Long getBytesSent() { + return bytesSent; + } + + public String getTemplateType() { + return templateType; + } + + public void setTemplateType(String templateType) { + this.templateType = templateType; + } + + public List getVnfNics() { + return vnfNics; + } + + public void setVnfNics(List vnfNics) { + this.vnfNics = vnfNics; + } + + public Map getVnfDetails() { + return vnfDetails; + } + + public void setVnfDetails(Map vnfDetails) { + this.vnfDetails = vnfDetails; + } + + public void addVnfNic(VnfNicResponse vnfNic) { + if (this.vnfNics == null) { + this.vnfNics = new ArrayList<>(); + } + this.vnfNics.add(vnfNic); + } + + public void addVnfDetail(String key, String value) { + if (this.vnfDetails == null) { + this.vnfDetails = new LinkedHashMap<>(); + } + this.vnfDetails.put(key,value); + } } diff --git a/api/src/main/java/org/apache/cloudstack/api/response/VnfNicResponse.java b/api/src/main/java/org/apache/cloudstack/api/response/VnfNicResponse.java new file mode 100644 index 00000000000..af238c5a330 --- /dev/null +++ b/api/src/main/java/org/apache/cloudstack/api/response/VnfNicResponse.java @@ -0,0 +1,119 @@ +// Licensed to the Apache Software Foundation (ASF) under one +// or more contributor license agreements. See the NOTICE file +// distributed with this work for additional information +// regarding copyright ownership. The ASF licenses this file +// to you 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. +package org.apache.cloudstack.api.response; + +import com.cloud.serializer.Param; +import com.google.gson.annotations.SerializedName; +import org.apache.cloudstack.api.ApiConstants; + +@SuppressWarnings("unused") +public class VnfNicResponse { + @SerializedName(ApiConstants.DEVICE_ID) + @Param(description = "Device id of the NIC") + private long deviceId; + + @SerializedName(ApiConstants.NAME) + @Param(description = "Name of the NIC") + private String name; + + @SerializedName(ApiConstants.REQUIRED) + @Param(description = "True if the NIC is required. False if optional") + private Boolean required; + + @SerializedName(ApiConstants.MANAGEMENT) + @Param(description = "True if the NIC is a management interface. False otherwise") + private Boolean management; + + @SerializedName(ApiConstants.DESCRIPTION) + @Param(description = "Description of the NIC") + private String description; + + @SerializedName(ApiConstants.NETWORK_ID) + @Param(description = "Network id of the NIC") + private String networkId; + + @SerializedName(ApiConstants.NETWORK_NAME) + @Param(description = "Network name of the NIC") + private String networkName; + + public void setDeviceId(long deviceId) { + this.deviceId = deviceId; + } + + public void setName(String name) { + this.name = name; + } + + public void setRequired(Boolean required) { + this.required = required; + } + + public void setManagement(Boolean management) { + this.management = management; + } + + public void setDescription(String description) { + this.description = description; + } + + public void setNetworkId(String networkId) { + this.networkId = networkId; + } + + public void setNetworkName(String networkName) { + this.networkName = networkName; + } + + public VnfNicResponse() { + } + + public VnfNicResponse(long deviceId, String name, Boolean required, Boolean management, String description) { + this.deviceId = deviceId; + this.name = name; + this.required = required; + this.management = management; + this.description = description; + } + + public long getDeviceId() { + return deviceId; + } + + public String getName() { + return name; + } + + public Boolean isRequired() { + return required; + } + + public Boolean isManagement() { + return management; + } + + public String getDescription() { + return description; + } + + public String getNetworkId() { + return networkId; + } + + public String getNetworkName() { + return networkName; + } +} diff --git a/api/src/main/java/org/apache/cloudstack/api/response/VnfTemplateResponse.java b/api/src/main/java/org/apache/cloudstack/api/response/VnfTemplateResponse.java new file mode 100644 index 00000000000..5fd17efca88 --- /dev/null +++ b/api/src/main/java/org/apache/cloudstack/api/response/VnfTemplateResponse.java @@ -0,0 +1,60 @@ +// Licensed to the Apache Software Foundation (ASF) under one +// or more contributor license agreements. See the NOTICE file +// distributed with this work for additional information +// regarding copyright ownership. The ASF licenses this file +// to you 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. +package org.apache.cloudstack.api.response; + +import com.cloud.serializer.Param; +import com.google.gson.annotations.SerializedName; +import org.apache.cloudstack.api.ApiConstants; + +import java.util.ArrayList; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; + +@SuppressWarnings("unused") +public class VnfTemplateResponse extends TemplateResponse { + + @SerializedName(ApiConstants.VNF_NICS) + @Param(description = "NICs of the VNF template") + private List vnfNics; + + @SerializedName(ApiConstants.VNF_DETAILS) + @Param(description = "VNF details") + private Map vnfDetails; + + public void addVnfNic(VnfNicResponse vnfNic) { + if (this.vnfNics == null) { + this.vnfNics = new ArrayList<>(); + } + this.vnfNics.add(vnfNic); + } + + public void addVnfDetail(String key, String value) { + if (this.vnfDetails == null) { + this.vnfDetails = new LinkedHashMap<>(); + } + this.vnfDetails.put(key,value); + } + + public List getVnfNics() { + return vnfNics; + } + + public Map getVnfDetails() { + return vnfDetails; + } +} diff --git a/api/src/main/java/org/apache/cloudstack/storage/template/VnfTemplateManager.java b/api/src/main/java/org/apache/cloudstack/storage/template/VnfTemplateManager.java new file mode 100644 index 00000000000..6571346ad65 --- /dev/null +++ b/api/src/main/java/org/apache/cloudstack/storage/template/VnfTemplateManager.java @@ -0,0 +1,52 @@ +// Licensed to the Apache Software Foundation (ASF) under one +// or more contributor license agreements. See the NOTICE file +// distributed with this work for additional information +// regarding copyright ownership. The ASF licenses this file +// to you 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. +package org.apache.cloudstack.storage.template; + +import com.cloud.dc.DataCenter; +import com.cloud.exception.InsufficientAddressCapacityException; +import com.cloud.exception.ResourceAllocationException; +import com.cloud.exception.ResourceUnavailableException; +import com.cloud.network.security.SecurityGroup; +import com.cloud.template.VirtualMachineTemplate; +import com.cloud.user.Account; +import com.cloud.uservm.UserVm; +import org.apache.cloudstack.api.command.user.template.RegisterVnfTemplateCmd; +import org.apache.cloudstack.api.command.user.template.UpdateVnfTemplateCmd; +import org.apache.cloudstack.api.command.user.vm.DeployVnfApplianceCmd; +import org.apache.cloudstack.framework.config.ConfigKey; +import java.util.List; + +public interface VnfTemplateManager { + + ConfigKey VnfTemplateAndApplianceEnabled = new ConfigKey("Advanced", Boolean.class, + "vnf.template.appliance.enabled", + "true", + "Indicates whether the creation of VNF templates and VNF appliances is enabled or not.", + false); + + void persistVnfTemplate(long templateId, RegisterVnfTemplateCmd cmd); + + void updateVnfTemplate(long templateId, UpdateVnfTemplateCmd cmd); + + void validateVnfApplianceNics(VirtualMachineTemplate template, List networkIds); + + SecurityGroup createSecurityGroupForVnfAppliance(DataCenter zone, VirtualMachineTemplate template, Account owner, DeployVnfApplianceCmd cmd); + + void createIsolatedNetworkRulesForVnfAppliance(DataCenter zone, VirtualMachineTemplate template, Account owner, + UserVm vm, DeployVnfApplianceCmd cmd) + throws InsufficientAddressCapacityException, ResourceAllocationException, ResourceUnavailableException; +} diff --git a/api/src/main/java/org/apache/cloudstack/storage/template/VnfTemplateUtils.java b/api/src/main/java/org/apache/cloudstack/storage/template/VnfTemplateUtils.java new file mode 100644 index 00000000000..e997a50cec0 --- /dev/null +++ b/api/src/main/java/org/apache/cloudstack/storage/template/VnfTemplateUtils.java @@ -0,0 +1,152 @@ +// Licensed to the Apache Software Foundation (ASF) under one +// or more contributor license agreements. See the NOTICE file +// distributed with this work for additional information +// regarding copyright ownership. The ASF licenses this file +// to you 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. +package org.apache.cloudstack.storage.template; + +import com.cloud.exception.InvalidParameterValueException; +import com.cloud.network.VNF; +import com.cloud.storage.Storage; +import com.cloud.template.VirtualMachineTemplate; +import com.cloud.utils.net.NetUtils; +import org.apache.cloudstack.api.BaseCmd; +import org.apache.cloudstack.api.command.user.template.DeleteVnfTemplateCmd; +import org.apache.cloudstack.api.command.user.template.RegisterVnfTemplateCmd; +import org.apache.cloudstack.api.command.user.template.UpdateVnfTemplateCmd; +import org.apache.commons.collections.CollectionUtils; +import org.apache.commons.collections.MapUtils; +import org.apache.commons.lang3.EnumUtils; +import org.apache.commons.lang3.StringUtils; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collection; +import java.util.Collections; +import java.util.Comparator; +import java.util.HashMap; +import java.util.Iterator; +import java.util.List; +import java.util.Map; +import java.util.stream.Collectors; + +public class VnfTemplateUtils { + private VnfTemplateUtils() { + } + + public static List getVnfNicsList(Map vnfNics) { + List nicsList = new ArrayList<>(); + if (MapUtils.isNotEmpty(vnfNics)) { + Collection nicsCollection = vnfNics.values(); + Iterator iter = nicsCollection.iterator(); + while (iter.hasNext()) { + HashMap nicDetails = (HashMap)iter.next(); + String deviceIdString = nicDetails.get("deviceid"); + String name = nicDetails.get("name"); + String requiredString = nicDetails.get("required"); + String managementString = nicDetails.get("management"); + String description = nicDetails.get("description"); + Integer deviceId = null; + if (StringUtils.isAnyBlank(name, deviceIdString)) { + throw new InvalidParameterValueException("VNF nic name and deviceid cannot be null"); + } + try { + deviceId = Integer.parseInt(deviceIdString); + } catch (NumberFormatException e) { + throw new InvalidParameterValueException("Unable to parse VNF nic deviceId to Integer: " + deviceId); + } + boolean required = StringUtils.isBlank(requiredString) || Boolean.parseBoolean(requiredString); + boolean management = StringUtils.isBlank(managementString) || Boolean.parseBoolean(managementString); + nicsList.add(new VNF.VnfNic(deviceId, name, required, management, description)); + } + Collections.sort(nicsList, Comparator.comparing(VNF.VnfNic::getDeviceId)); + } + return nicsList; + } + + public static void validateApiCommandParams(Map vnfDetails, List vnfNics, String templateType) { + if (templateType != null && !Storage.TemplateType.VNF.name().equals(templateType)) { + throw new InvalidParameterValueException("The template type must be VNF for VNF templates."); + } + + if (vnfDetails != null) { + for (String vnfDetail : vnfDetails.keySet()) { + if (!EnumUtils.isValidEnumIgnoreCase(VNF.VnfDetail.class, vnfDetail) && + !EnumUtils.isValidEnumIgnoreCase(VNF.AccessDetail.class, vnfDetail)) { + throw new InvalidParameterValueException(String.format("Invalid VNF detail found: %s. Valid values are %s and %s", vnfDetail, + Arrays.stream(VNF.AccessDetail.values()).map(method -> method.toString()).collect(Collectors.joining(", ")), + Arrays.stream(VNF.VnfDetail.values()).map(method -> method.toString()).collect(Collectors.joining(", ")))); + } + if (vnfDetails.get(vnfDetail) == null) { + throw new InvalidParameterValueException("Empty value found for VNF detail: " + vnfDetail); + } + if (VNF.AccessDetail.ACCESS_METHODS.name().equalsIgnoreCase(vnfDetail)) { + String[] accessMethods = vnfDetails.get(vnfDetail).split(","); + for (String accessMethod : accessMethods) { + if (VNF.AccessMethod.fromValue(accessMethod.trim()) == null) { + throw new InvalidParameterValueException(String.format("Invalid VNF access method found: %s. Valid values are %s", accessMethod, + Arrays.stream(VNF.AccessMethod.values()).map(method -> method.toString()).sorted().collect(Collectors.joining(", ")))); + } + } + } + } + } + + validateVnfNics(vnfNics); + } + + public static void validateVnfNics(List nicsList) { + long deviceId = 0L; + boolean required = true; + for (VNF.VnfNic nic : nicsList) { + if (nic.getDeviceId() != deviceId) { + throw new InvalidParameterValueException(String.format("deviceid must be consecutive and start from 0. Nic deviceid should be %s but actual is %s.", deviceId, nic.getDeviceId())); + } + if (!required && nic.isRequired()) { + throw new InvalidParameterValueException(String.format("required cannot be true if a preceding nic is optional. Nic with deviceid %s should be required but actual is optional.", deviceId)); + } + deviceId ++; + required = nic.isRequired(); + } + } + + public static void validateApiCommandParams(BaseCmd cmd, VirtualMachineTemplate template) { + if (cmd instanceof RegisterVnfTemplateCmd) { + RegisterVnfTemplateCmd registerCmd = (RegisterVnfTemplateCmd) cmd; + validateApiCommandParams(registerCmd.getVnfDetails(), registerCmd.getVnfNics(), registerCmd.getTemplateType()); + } else if (cmd instanceof UpdateVnfTemplateCmd) { + UpdateVnfTemplateCmd updateCmd = (UpdateVnfTemplateCmd) cmd; + if (!Storage.TemplateType.VNF.equals(template.getTemplateType())) { + throw new InvalidParameterValueException(String.format("Cannot update as template %s is not a VNF template. The template type is %s.", updateCmd.getId(), template.getTemplateType())); + } + validateApiCommandParams(updateCmd.getVnfDetails(), updateCmd.getVnfNics(), updateCmd.getTemplateType()); + } else if (cmd instanceof DeleteVnfTemplateCmd) { + if (!Storage.TemplateType.VNF.equals(template.getTemplateType())) { + DeleteVnfTemplateCmd deleteCmd = (DeleteVnfTemplateCmd) cmd; + throw new InvalidParameterValueException(String.format("Cannot delete as Template %s is not a VNF template. The template type is %s.", deleteCmd.getId(), template.getTemplateType())); + } + } + } + + public static void validateVnfCidrList(List cidrList) { + if (CollectionUtils.isEmpty(cidrList)) { + return; + } + for (String cidr : cidrList) { + if (!NetUtils.isValidIp4Cidr(cidr)) { + throw new InvalidParameterValueException(String.format("Invalid cidr for VNF appliance: %s", cidr)); + } + } + } +} diff --git a/api/src/test/java/com/cloud/network/VNFTest.java b/api/src/test/java/com/cloud/network/VNFTest.java new file mode 100644 index 00000000000..d38e9cc1b93 --- /dev/null +++ b/api/src/test/java/com/cloud/network/VNFTest.java @@ -0,0 +1,58 @@ +// Licensed to the Apache Software Foundation (ASF) under one +// or more contributor license agreements. See the NOTICE file +// distributed with this work for additional information +// regarding copyright ownership. The ASF licenses this file +// to you 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. +package com.cloud.network; + +import org.junit.Assert; +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.junit.MockitoJUnitRunner; + + +@RunWith(MockitoJUnitRunner.class) +public class VNFTest { + + static long deviceId = 0L; + static String deviceName = "eth0"; + static boolean required = true; + static boolean management = false; + static String description = "description of vnf nic"; + + @Before + public void setUp() { + } + + @Test + public void testAccessMethods() { + Assert.assertEquals(VNF.AccessMethod.CONSOLE, VNF.AccessMethod.fromValue("console")); + Assert.assertEquals(VNF.AccessMethod.HTTP, VNF.AccessMethod.fromValue("http")); + Assert.assertEquals(VNF.AccessMethod.HTTPS, VNF.AccessMethod.fromValue("https")); + Assert.assertEquals(VNF.AccessMethod.SSH_WITH_KEY, VNF.AccessMethod.fromValue("ssh-key")); + Assert.assertEquals(VNF.AccessMethod.SSH_WITH_PASSWORD, VNF.AccessMethod.fromValue("ssh-password")); + } + + @Test + public void testVnfNic() { + VNF.VnfNic vnfNic = new VNF.VnfNic(deviceId, deviceName, required, management, description); + + Assert.assertEquals(deviceId, vnfNic.getDeviceId()); + Assert.assertEquals(deviceName, vnfNic.getName()); + Assert.assertEquals(required, vnfNic.isRequired()); + Assert.assertEquals(management, vnfNic.isManagement()); + Assert.assertEquals(description, vnfNic.getDescription()); + } +} diff --git a/api/src/test/java/org/apache/cloudstack/api/response/VnfNicResponseTest.java b/api/src/test/java/org/apache/cloudstack/api/response/VnfNicResponseTest.java new file mode 100644 index 00000000000..cdfbf451e86 --- /dev/null +++ b/api/src/test/java/org/apache/cloudstack/api/response/VnfNicResponseTest.java @@ -0,0 +1,54 @@ +// Licensed to the Apache Software Foundation (ASF) under one +// or more contributor license agreements. See the NOTICE file +// distributed with this work for additional information +// regarding copyright ownership. The ASF licenses this file +// to you 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. +package org.apache.cloudstack.api.response; + +import org.junit.Assert; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.junit.MockitoJUnitRunner; + +@RunWith(MockitoJUnitRunner.class) +public final class VnfNicResponseTest { + + static long deviceId = 0L; + static String deviceName = "eth0"; + static boolean required = true; + static boolean management = false; + static String description = "description of vnf nic"; + + static String networkUuid = "networkuuid"; + static String networkName = "networkname"; + + @Test + public void testNewVnfNicResponse() { + final VnfNicResponse response = new VnfNicResponse(deviceId, deviceName, required, management, description); + Assert.assertEquals(deviceId, response.getDeviceId()); + Assert.assertEquals(deviceName, response.getName()); + Assert.assertEquals(required, response.isRequired()); + Assert.assertEquals(management, response.isManagement()); + Assert.assertEquals(description, response.getDescription()); + } + + @Test + public void testSetVnfNicResponse() { + final VnfNicResponse response = new VnfNicResponse(); + response.setNetworkId(networkUuid); + response.setNetworkName(networkName); + Assert.assertEquals(networkUuid, response.getNetworkId()); + Assert.assertEquals(networkName, response.getNetworkName()); + } +} diff --git a/api/src/test/java/org/apache/cloudstack/api/response/VnfTemplateResponseTest.java b/api/src/test/java/org/apache/cloudstack/api/response/VnfTemplateResponseTest.java new file mode 100644 index 00000000000..38875c07713 --- /dev/null +++ b/api/src/test/java/org/apache/cloudstack/api/response/VnfTemplateResponseTest.java @@ -0,0 +1,47 @@ +// Licensed to the Apache Software Foundation (ASF) under one +// or more contributor license agreements. See the NOTICE file +// distributed with this work for additional information +// regarding copyright ownership. The ASF licenses this file +// to you 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. +package org.apache.cloudstack.api.response; + +import org.junit.Assert; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.junit.MockitoJUnitRunner; + +@RunWith(MockitoJUnitRunner.class) +public final class VnfTemplateResponseTest { + + @Test + public void testAddVnfNicToResponse() { + final VnfTemplateResponse response = new VnfTemplateResponse(); + + response.addVnfNic(new VnfNicResponse()); + response.addVnfNic(new VnfNicResponse()); + + Assert.assertEquals(2, response.getVnfNics().size()); + } + + @Test + public void testAddVnfDetailToResponse() { + final VnfTemplateResponse response = new VnfTemplateResponse(); + + response.addVnfDetail("key1", "value1"); + response.addVnfDetail("key2", "value2"); + response.addVnfDetail("key3", "value3"); + + Assert.assertEquals(3, response.getVnfDetails().size()); + } +} diff --git a/api/src/test/java/org/apache/cloudstack/storage/template/VnfTemplateUtilsTest.java b/api/src/test/java/org/apache/cloudstack/storage/template/VnfTemplateUtilsTest.java new file mode 100644 index 00000000000..7ba8564faf4 --- /dev/null +++ b/api/src/test/java/org/apache/cloudstack/storage/template/VnfTemplateUtilsTest.java @@ -0,0 +1,261 @@ +// Licensed to the Apache Software Foundation (ASF) under one +// or more contributor license agreements. See the NOTICE file +// distributed with this work for additional information +// regarding copyright ownership. The ASF licenses this file +// to you 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. +package org.apache.cloudstack.storage.template; + +import com.cloud.exception.InvalidParameterValueException; +import com.cloud.network.VNF.VnfNic; +import com.cloud.storage.Storage; +import com.cloud.template.VirtualMachineTemplate; +import org.apache.cloudstack.api.command.user.template.RegisterVnfTemplateCmd; +import org.apache.cloudstack.api.command.user.template.UpdateVnfTemplateCmd; + +import org.junit.Assert; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.Mockito; +import org.mockito.junit.MockitoJUnitRunner; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +@RunWith(MockitoJUnitRunner.class) +public class VnfTemplateUtilsTest { + + @Test + public void testGetVnfNicsListAllGood() { + final Map vnfNics = new HashMap<>(); + vnfNics.put("0", new HashMap<>(Map.ofEntries( + Map.entry("deviceid", "1"), + Map.entry("name", "eth1"), + Map.entry("required", "true"), + Map.entry("description", "The second NIC of VNF appliance") + ))); + vnfNics.put("1", new HashMap<>(Map.ofEntries( + Map.entry("deviceid", "2"), + Map.entry("name", "eth2"), + Map.entry("required", "false"), + Map.entry("description", "The third NIC of VNF appliance") + ))); + vnfNics.put("2", new HashMap<>(Map.ofEntries( + Map.entry("deviceid", "0"), + Map.entry("name", "eth0"), + Map.entry("description", "The first NIC of VNF appliance") + ))); + + Map vnfNicsMock = Mockito.mock(Map.class); + Mockito.when(vnfNicsMock.values()).thenReturn(vnfNics.values()); + + List nicsList = VnfTemplateUtils.getVnfNicsList(vnfNicsMock); + Mockito.verify(vnfNicsMock).values(); + + Assert.assertEquals(3, nicsList.size()); + Assert.assertEquals(0, nicsList.get(0).getDeviceId()); + Assert.assertEquals("eth0", nicsList.get(0).getName()); + Assert.assertTrue(nicsList.get(0).isRequired()); + Assert.assertEquals(1, nicsList.get(1).getDeviceId()); + Assert.assertEquals("eth1", nicsList.get(1).getName()); + Assert.assertTrue(nicsList.get(1).isRequired()); + Assert.assertEquals(2, nicsList.get(2).getDeviceId()); + Assert.assertEquals("eth2", nicsList.get(2).getName()); + Assert.assertFalse(nicsList.get(2).isRequired()); + } + + @Test(expected = InvalidParameterValueException.class) + public void testGetVnfNicsListWithEmptyName() { + final Map vnfNics = new HashMap<>(); + vnfNics.put("0", new HashMap<>(Map.ofEntries( + Map.entry("deviceid", "1"), + Map.entry("required", "true"), + Map.entry("description", "The second NIC of VNF appliance") + ))); + + Map vnfNicsMock = Mockito.mock(Map.class); + Mockito.when(vnfNicsMock.values()).thenReturn(vnfNics.values()); + + List nicsList = VnfTemplateUtils.getVnfNicsList(vnfNicsMock); + } + + @Test(expected = InvalidParameterValueException.class) + public void testGetVnfNicsListWithEmptyDeviceId() { + final Map vnfNics = new HashMap<>(); + vnfNics.put("0", new HashMap<>(Map.ofEntries( + Map.entry("name", "eth1"), + Map.entry("required", "true"), + Map.entry("description", "The second NIC of VNF appliance") + ))); + + Map vnfNicsMock = Mockito.mock(Map.class); + Mockito.when(vnfNicsMock.values()).thenReturn(vnfNics.values()); + + List nicsList = VnfTemplateUtils.getVnfNicsList(vnfNicsMock); + } + + @Test(expected = InvalidParameterValueException.class) + public void testGetVnfNicsListWithInvalidDeviceId() { + final Map vnfNics = new HashMap<>(); + vnfNics.put("0", new HashMap<>(Map.ofEntries( + Map.entry("deviceid", "invalid"), + Map.entry("name", "eth1"), + Map.entry("required", "true"), + Map.entry("description", "The second NIC of VNF appliance") + ))); + + Map vnfNicsMock = Mockito.mock(Map.class); + Mockito.when(vnfNicsMock.values()).thenReturn(vnfNics.values()); + + List nicsList = VnfTemplateUtils.getVnfNicsList(vnfNicsMock); + } + + @Test + public void testValidateVnfNicsAllGood() { + VnfNic nic1 = Mockito.mock(VnfNic.class); + Mockito.when(nic1.getDeviceId()).thenReturn(0L); + Mockito.when(nic1.isRequired()).thenReturn(true); + + VnfNic nic2 = Mockito.mock(VnfNic.class); + Mockito.when(nic2.getDeviceId()).thenReturn(1L); + Mockito.when(nic2.isRequired()).thenReturn(true); + + VnfNic nic3 = Mockito.mock(VnfNic.class); + Mockito.when(nic3.getDeviceId()).thenReturn(2L); + Mockito.when(nic3.isRequired()).thenReturn(false); + + List nicsList = Arrays.asList(nic1, nic2, nic3); + + VnfTemplateUtils.validateVnfNics(nicsList); + } + + @Test(expected = InvalidParameterValueException.class) + public void testValidateVnfNicsStartWithNonzero() { + VnfNic nic1 = Mockito.mock(VnfNic.class); + Mockito.when(nic1.getDeviceId()).thenReturn(1L); + + VnfNic nic2 = Mockito.mock(VnfNic.class); + + VnfNic nic3 = Mockito.mock(VnfNic.class); + + List nicsList = Arrays.asList(nic1, nic2, nic3); + + VnfTemplateUtils.validateVnfNics(nicsList); + } + + @Test(expected = InvalidParameterValueException.class) + public void testValidateVnfNicsWithNonConstantDeviceIds() { + VnfNic nic1 = Mockito.mock(VnfNic.class); + Mockito.when(nic1.getDeviceId()).thenReturn(0L); + Mockito.when(nic1.isRequired()).thenReturn(true); + + VnfNic nic2 = Mockito.mock(VnfNic.class); + Mockito.when(nic2.getDeviceId()).thenReturn(2L); + + VnfNic nic3 = Mockito.mock(VnfNic.class); + + List nicsList = Arrays.asList(nic1, nic2, nic3); + + VnfTemplateUtils.validateVnfNics(nicsList); + } + + @Test(expected = InvalidParameterValueException.class) + public void testValidateVnfNicsWithInvalidRequired() { + VnfNic nic1 = Mockito.mock(VnfNic.class); + Mockito.when(nic1.getDeviceId()).thenReturn(0L); + Mockito.when(nic1.isRequired()).thenReturn(true); + + VnfNic nic2 = Mockito.mock(VnfNic.class); + Mockito.when(nic2.getDeviceId()).thenReturn(1L); + Mockito.when(nic2.isRequired()).thenReturn(false); + + VnfNic nic3 = Mockito.mock(VnfNic.class); + Mockito.when(nic3.getDeviceId()).thenReturn(2L); + Mockito.when(nic3.isRequired()).thenReturn(true); + + List nicsList = Arrays.asList(nic1, nic2, nic3); + + VnfTemplateUtils.validateVnfNics(nicsList); + } + + @Test + public void testValidateApiCommandParamsAllGood() { + VirtualMachineTemplate template = Mockito.mock(VirtualMachineTemplate.class); + RegisterVnfTemplateCmd cmd = Mockito.mock(RegisterVnfTemplateCmd.class); + Map vnfDetails = Mockito.spy(new HashMap<>()); + vnfDetails.put("username", "admin"); + vnfDetails.put("password", "password"); + vnfDetails.put("version", "4.19.0"); + vnfDetails.put("vendor", "cloudstack"); + Mockito.when(cmd.getVnfDetails()).thenReturn(vnfDetails); + + VnfTemplateUtils.validateApiCommandParams(cmd, template); + } + + @Test(expected = InvalidParameterValueException.class) + public void testValidateApiCommandParamsInvalidAccessMethods() { + VirtualMachineTemplate template = Mockito.mock(VirtualMachineTemplate.class); + Mockito.when(template.getTemplateType()).thenReturn(Storage.TemplateType.VNF); + UpdateVnfTemplateCmd cmd = Mockito.mock(UpdateVnfTemplateCmd.class); + Map vnfDetails = Mockito.spy(new HashMap<>()); + vnfDetails.put("access_methods", "invalid"); + Mockito.when(cmd.getVnfDetails()).thenReturn(vnfDetails); + + VnfTemplateUtils.validateApiCommandParams(cmd, template); + } + + @Test(expected = InvalidParameterValueException.class) + public void testValidateApiCommandParamsInvalidAccessDetails() { + VirtualMachineTemplate template = Mockito.mock(VirtualMachineTemplate.class); + Mockito.when(template.getTemplateType()).thenReturn(Storage.TemplateType.VNF); + UpdateVnfTemplateCmd cmd = Mockito.mock(UpdateVnfTemplateCmd.class); + Map vnfDetails = Mockito.spy(new HashMap<>()); + vnfDetails.put("invalid", "value"); + Mockito.when(cmd.getVnfDetails()).thenReturn(vnfDetails); + + VnfTemplateUtils.validateApiCommandParams(cmd, template); + } + + @Test(expected = InvalidParameterValueException.class) + public void testValidateApiCommandParamsInvalidTemplateType() { + VirtualMachineTemplate template = Mockito.mock(VirtualMachineTemplate.class); + Mockito.when(template.getTemplateType()).thenReturn(Storage.TemplateType.USER); + UpdateVnfTemplateCmd cmd = Mockito.mock(UpdateVnfTemplateCmd.class); + + VnfTemplateUtils.validateApiCommandParams(cmd, template); + } + + @Test + public void testValidateVnfCidrList() { + List cidrList = new ArrayList<>(); + cidrList.add("10.10.10.0/24"); + VnfTemplateUtils.validateVnfCidrList(cidrList); + } + + @Test + public void testValidateVnfCidrListWithEmptyList() { + List cidrList = new ArrayList<>(); + VnfTemplateUtils.validateVnfCidrList(cidrList); + } + + @Test(expected = InvalidParameterValueException.class) + public void testValidateVnfCidrListwithInvalidList() { + List cidrList = new ArrayList<>(); + cidrList.add("10.10.10.0/24"); + cidrList.add("10.10.10.0/33"); + VnfTemplateUtils.validateVnfCidrList(cidrList); + } +} diff --git a/engine/components-api/src/main/java/com/cloud/template/TemplateManager.java b/engine/components-api/src/main/java/com/cloud/template/TemplateManager.java index a3d9c1b79f1..2c129dfd6a5 100644 --- a/engine/components-api/src/main/java/com/cloud/template/TemplateManager.java +++ b/engine/components-api/src/main/java/com/cloud/template/TemplateManager.java @@ -18,6 +18,7 @@ package com.cloud.template; import java.util.List; +import org.apache.cloudstack.api.BaseCmd; import org.apache.cloudstack.engine.subsystem.api.storage.DataStore; import org.apache.cloudstack.engine.subsystem.api.storage.TemplateInfo; import org.apache.cloudstack.framework.config.ConfigKey; @@ -29,6 +30,7 @@ import com.cloud.deploy.DeployDestination; import com.cloud.exception.ResourceAllocationException; import com.cloud.exception.StorageUnavailableException; import com.cloud.storage.DataStoreRole; +import com.cloud.storage.Storage.TemplateType; import com.cloud.storage.StoragePool; import com.cloud.storage.VMTemplateStoragePoolVO; import com.cloud.storage.VMTemplateVO; @@ -133,5 +135,7 @@ public interface TemplateManager { public static final String MESSAGE_REGISTER_PUBLIC_TEMPLATE_EVENT = "Message.RegisterPublicTemplate.Event"; public static final String MESSAGE_RESET_TEMPLATE_PERMISSION_EVENT = "Message.ResetTemplatePermission.Event"; + TemplateType validateTemplateType(BaseCmd cmd, boolean isAdmin, boolean isCrossZones); + List getTemplateDisksOnImageStore(Long templateId, DataStoreRole role, String configurationId); } diff --git a/engine/schema/src/main/java/com/cloud/storage/VnfTemplateDetailVO.java b/engine/schema/src/main/java/com/cloud/storage/VnfTemplateDetailVO.java new file mode 100644 index 00000000000..24d8191fa04 --- /dev/null +++ b/engine/schema/src/main/java/com/cloud/storage/VnfTemplateDetailVO.java @@ -0,0 +1,101 @@ +// Licensed to the Apache Software Foundation (ASF) under one +// or more contributor license agreements. See the NOTICE file +// distributed with this work for additional information +// regarding copyright ownership. The ASF licenses this file +// to you 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. +package com.cloud.storage; + +import javax.persistence.Column; +import javax.persistence.Entity; +import javax.persistence.GeneratedValue; +import javax.persistence.GenerationType; +import javax.persistence.Id; +import javax.persistence.Lob; +import javax.persistence.Table; + +import org.apache.cloudstack.api.ResourceDetail; + +@Entity +@Table(name = "vnf_template_details") +public class VnfTemplateDetailVO implements ResourceDetail { + + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + @Column(name = "id") + private long id; + + @Column(name = "template_id") + private long resourceId; + + @Column(name = "name") + private String name; + + @Lob + @Column(name = "value", length = 65535) + private String value; + + @Column(name = "display") + private boolean display = true; + + public VnfTemplateDetailVO() { + } + + public VnfTemplateDetailVO(long templateId, String name, String value, boolean display) { + this.resourceId = templateId; + this.name = name; + this.value = value; + this.display = display; + } + + @Override + public long getId() { + return id; + } + + @Override + public long getResourceId() { + return resourceId; + } + + @Override + public String getName() { + return name; + } + + @Override + public String getValue() { + return value; + } + + @Override + public boolean isDisplay() { + return display; + } + + public void setId(long id) { + this.id = id; + } + + public void setResourceId(long resourceId) { + this.resourceId = resourceId; + } + + public void setName(String name) { + this.name = name; + } + + public void setValue(String value) { + this.value = value; + } +} diff --git a/engine/schema/src/main/java/com/cloud/storage/VnfTemplateNicVO.java b/engine/schema/src/main/java/com/cloud/storage/VnfTemplateNicVO.java new file mode 100644 index 00000000000..1f5054c0cd8 --- /dev/null +++ b/engine/schema/src/main/java/com/cloud/storage/VnfTemplateNicVO.java @@ -0,0 +1,101 @@ +// Licensed to the Apache Software Foundation (ASF) under one +// or more contributor license agreements. See the NOTICE file +// distributed with this work for additional information +// regarding copyright ownership. The ASF licenses this file +// to you 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. +package com.cloud.storage; + +import org.apache.cloudstack.api.InternalIdentity; +import org.apache.cloudstack.utils.reflectiontostringbuilderutils.ReflectionToStringBuilderUtils; + +import javax.persistence.Column; +import javax.persistence.Entity; +import javax.persistence.GeneratedValue; +import javax.persistence.GenerationType; +import javax.persistence.Id; +import javax.persistence.Table; + +@Entity +@Table(name = "vnf_template_nics") +public class VnfTemplateNicVO implements InternalIdentity { + + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + @Column(name = "id") + private long id; + + @Column(name = "template_id") + private long templateId; + + @Column(name = "device_id") + private long deviceId; + + @Column(name = "device_name") + private String deviceName; + + @Column(name = "required") + private boolean required = true; + + @Column(name = "management") + private boolean management = true; + + @Column(name = "description") + private String description; + + public VnfTemplateNicVO() { + } + + public VnfTemplateNicVO(long templateId, long deviceId, String deviceName, boolean required, boolean management, String description) { + this.templateId = templateId; + this.deviceId = deviceId; + this.deviceName = deviceName; + this.required = required; + this.management = management; + this.description = description; + } + + @Override + public String toString() { + return String.format("Template %s", ReflectionToStringBuilderUtils.reflectOnlySelectedFields(this, "id", "templateId", "deviceId", "required")); + } + + @Override + public long getId() { + return id; + } + + public long getTemplateId() { + return templateId; + } + + public long getDeviceId() { + return deviceId; + } + + public String getDeviceName() { + return deviceName; + } + + public boolean isRequired() { + return required; + } + + public boolean isManagement() { + return management; + } + + public String getDescription() { + return description; + } +} diff --git a/engine/schema/src/main/java/com/cloud/storage/dao/VnfTemplateDetailsDao.java b/engine/schema/src/main/java/com/cloud/storage/dao/VnfTemplateDetailsDao.java new file mode 100644 index 00000000000..c492240bf0f --- /dev/null +++ b/engine/schema/src/main/java/com/cloud/storage/dao/VnfTemplateDetailsDao.java @@ -0,0 +1,26 @@ +// Licensed to the Apache Software Foundation (ASF) under one +// or more contributor license agreements. See the NOTICE file +// distributed with this work for additional information +// regarding copyright ownership. The ASF licenses this file +// to you 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. +package com.cloud.storage.dao; + +import com.cloud.storage.VnfTemplateDetailVO; +import com.cloud.utils.db.GenericDao; + +import org.apache.cloudstack.resourcedetail.ResourceDetailsDao; + +public interface VnfTemplateDetailsDao extends GenericDao, ResourceDetailsDao { + +} diff --git a/engine/schema/src/main/java/com/cloud/storage/dao/VnfTemplateDetailsDaoImpl.java b/engine/schema/src/main/java/com/cloud/storage/dao/VnfTemplateDetailsDaoImpl.java new file mode 100644 index 00000000000..a4cbfa09fe6 --- /dev/null +++ b/engine/schema/src/main/java/com/cloud/storage/dao/VnfTemplateDetailsDaoImpl.java @@ -0,0 +1,31 @@ +// Licensed to the Apache Software Foundation (ASF) under one +// or more contributor license agreements. See the NOTICE file +// distributed with this work for additional information +// regarding copyright ownership. The ASF licenses this file +// to you 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. +package com.cloud.storage.dao; + +import com.cloud.storage.VnfTemplateDetailVO; + +import org.apache.cloudstack.resourcedetail.ResourceDetailsDaoBase; +import org.springframework.stereotype.Component; + +@Component +public class VnfTemplateDetailsDaoImpl extends ResourceDetailsDaoBase implements VnfTemplateDetailsDao { + + @Override + public void addDetail(long resourceId, String key, String value, boolean display) { + super.addDetail(new VnfTemplateDetailVO(resourceId, key, value, display)); + } +} diff --git a/engine/schema/src/main/java/com/cloud/storage/dao/VnfTemplateNicDao.java b/engine/schema/src/main/java/com/cloud/storage/dao/VnfTemplateNicDao.java new file mode 100644 index 00000000000..b076f14abc9 --- /dev/null +++ b/engine/schema/src/main/java/com/cloud/storage/dao/VnfTemplateNicDao.java @@ -0,0 +1,29 @@ +// Licensed to the Apache Software Foundation (ASF) under one +// or more contributor license agreements. See the NOTICE file +// distributed with this work for additional information +// regarding copyright ownership. The ASF licenses this file +// to you 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. +package com.cloud.storage.dao; + +import java.util.List; + +import com.cloud.storage.VnfTemplateNicVO; +import com.cloud.utils.db.GenericDao; + +public interface VnfTemplateNicDao extends GenericDao { + + List listByTemplateId(long templateId); + + void deleteByTemplateId(long templateId); +} diff --git a/engine/schema/src/main/java/com/cloud/storage/dao/VnfTemplateNicDaoImpl.java b/engine/schema/src/main/java/com/cloud/storage/dao/VnfTemplateNicDaoImpl.java new file mode 100644 index 00000000000..990ef446b47 --- /dev/null +++ b/engine/schema/src/main/java/com/cloud/storage/dao/VnfTemplateNicDaoImpl.java @@ -0,0 +1,53 @@ +// Licensed to the Apache Software Foundation (ASF) under one +// or more contributor license agreements. See the NOTICE file +// distributed with this work for additional information +// regarding copyright ownership. The ASF licenses this file +// to you 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. +package com.cloud.storage.dao; + +import java.util.List; + +import com.cloud.storage.VnfTemplateNicVO; +import com.cloud.utils.db.GenericDaoBase; +import com.cloud.utils.db.SearchBuilder; +import com.cloud.utils.db.SearchCriteria; +import com.cloud.utils.db.TransactionLegacy; + +public class VnfTemplateNicDaoImpl extends GenericDaoBase implements VnfTemplateNicDao { + + protected SearchBuilder TemplateSearch; + + public VnfTemplateNicDaoImpl() { + TemplateSearch = createSearchBuilder(); + TemplateSearch.and("templateId", TemplateSearch.entity().getTemplateId(), SearchCriteria.Op.EQ); + TemplateSearch.done(); + } + + @Override + public List listByTemplateId(long templateId) { + SearchCriteria sc = TemplateSearch.create(); + sc.setParameters("templateId", templateId); + return listBy(sc); + } + + @Override + public void deleteByTemplateId(long templateId) { + SearchCriteria sc = TemplateSearch.create(); + sc.setParameters("templateId", templateId); + TransactionLegacy txn = TransactionLegacy.currentTxn(); + txn.start(); + remove(sc); + txn.commit(); + } +} diff --git a/engine/schema/src/main/java/com/cloud/vm/NicVO.java b/engine/schema/src/main/java/com/cloud/vm/NicVO.java index fba7c966c44..a32a943ea58 100644 --- a/engine/schema/src/main/java/com/cloud/vm/NicVO.java +++ b/engine/schema/src/main/java/com/cloud/vm/NicVO.java @@ -333,6 +333,8 @@ public class NicVO implements Nic { .append("-") .append(instanceId) .append("-") + .append(deviceId) + .append("-") .append(reservationId) .append("-") .append(iPv4Address) diff --git a/engine/schema/src/main/resources/META-INF/cloudstack/core/spring-engine-schema-core-daos-context.xml b/engine/schema/src/main/resources/META-INF/cloudstack/core/spring-engine-schema-core-daos-context.xml index 647be81098c..2a0597ce9b5 100644 --- a/engine/schema/src/main/resources/META-INF/cloudstack/core/spring-engine-schema-core-daos-context.xml +++ b/engine/schema/src/main/resources/META-INF/cloudstack/core/spring-engine-schema-core-daos-context.xml @@ -278,6 +278,8 @@ + + diff --git a/engine/schema/src/main/resources/META-INF/db/schema-41810to41900.sql b/engine/schema/src/main/resources/META-INF/db/schema-41810to41900.sql index c14730adb86..88ff76f0c8c 100644 --- a/engine/schema/src/main/resources/META-INF/db/schema-41810to41900.sql +++ b/engine/schema/src/main/resources/META-INF/db/schema-41810to41900.sql @@ -184,6 +184,230 @@ ALTER TABLE `cloud`.`kubernetes_cluster` MODIFY COLUMN `kubernetes_version_id` b -- Set removed state for all removed accounts UPDATE `cloud`.`account` SET state='removed' WHERE `removed` IS NOT NULL; + +-- New tables for VNF +CREATE TABLE IF NOT EXISTS `cloud`.`vnf_template_nics` ( + `id` bigint unsigned NOT NULL AUTO_INCREMENT, + `template_id` bigint unsigned NOT NULL COMMENT 'id of the VNF template', + `device_id` bigint unsigned NOT NULL COMMENT 'Device id of the NIC when plugged into the VNF appliances', + `device_name` varchar(1024) NOT NULL COMMENT 'Name of the NIC', + `required` tinyint NOT NULL DEFAULT '1' COMMENT 'True if the NIC is required. False if optional', + `management` tinyint NOT NULL DEFAULT '1' COMMENT 'True if the NIC is a management interface', + `description` varchar(1024) COMMENT 'Description of the NIC', + PRIMARY KEY (`id`), + UNIQUE KEY `uk_template_id_device_id` (`template_id`, `device_id`), + KEY `fk_vnf_template_nics__template_id` (`template_id`), + CONSTRAINT `fk_vnf_template_nics__template_id` FOREIGN KEY (`template_id`) REFERENCES `vm_template` (`id`) ON DELETE CASCADE + ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; + +CREATE TABLE IF NOT EXISTS `cloud`.`vnf_template_details` ( + `id` bigint unsigned NOT NULL AUTO_INCREMENT, + `template_id` bigint unsigned NOT NULL COMMENT 'id of the VNF template', + `name` varchar(255) NOT NULL, + `value` varchar(1024) NOT NULL, + `display` tinyint(1) NOT NULL DEFAULT '1' COMMENT 'True if the detail can be displayed to the end user', + PRIMARY KEY (`id`), + KEY `fk_vnf_template_details__template_id` (`template_id`), + CONSTRAINT `fk_vnf_template_details__template_id` FOREIGN KEY (`template_id`) REFERENCES `vm_template` (`id`) ON DELETE CASCADE + ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; + +DROP VIEW IF EXISTS `cloud`.`user_vm_view`; +CREATE + VIEW `user_vm_view` AS +SELECT + `vm_instance`.`id` AS `id`, + `vm_instance`.`name` AS `name`, + `user_vm`.`display_name` AS `display_name`, + `user_vm`.`user_data` AS `user_data`, + `account`.`id` AS `account_id`, + `account`.`uuid` AS `account_uuid`, + `account`.`account_name` AS `account_name`, + `account`.`type` AS `account_type`, + `domain`.`id` AS `domain_id`, + `domain`.`uuid` AS `domain_uuid`, + `domain`.`name` AS `domain_name`, + `domain`.`path` AS `domain_path`, + `projects`.`id` AS `project_id`, + `projects`.`uuid` AS `project_uuid`, + `projects`.`name` AS `project_name`, + `instance_group`.`id` AS `instance_group_id`, + `instance_group`.`uuid` AS `instance_group_uuid`, + `instance_group`.`name` AS `instance_group_name`, + `vm_instance`.`uuid` AS `uuid`, + `vm_instance`.`user_id` AS `user_id`, + `vm_instance`.`last_host_id` AS `last_host_id`, + `vm_instance`.`vm_type` AS `type`, + `vm_instance`.`limit_cpu_use` AS `limit_cpu_use`, + `vm_instance`.`created` AS `created`, + `vm_instance`.`state` AS `state`, + `vm_instance`.`update_time` AS `update_time`, + `vm_instance`.`removed` AS `removed`, + `vm_instance`.`ha_enabled` AS `ha_enabled`, + `vm_instance`.`hypervisor_type` AS `hypervisor_type`, + `vm_instance`.`instance_name` AS `instance_name`, + `vm_instance`.`guest_os_id` AS `guest_os_id`, + `vm_instance`.`display_vm` AS `display_vm`, + `guest_os`.`uuid` AS `guest_os_uuid`, + `vm_instance`.`pod_id` AS `pod_id`, + `host_pod_ref`.`uuid` AS `pod_uuid`, + `vm_instance`.`private_ip_address` AS `private_ip_address`, + `vm_instance`.`private_mac_address` AS `private_mac_address`, + `vm_instance`.`vm_type` AS `vm_type`, + `data_center`.`id` AS `data_center_id`, + `data_center`.`uuid` AS `data_center_uuid`, + `data_center`.`name` AS `data_center_name`, + `data_center`.`is_security_group_enabled` AS `security_group_enabled`, + `data_center`.`networktype` AS `data_center_network_type`, + `host`.`id` AS `host_id`, + `host`.`uuid` AS `host_uuid`, + `host`.`name` AS `host_name`, + `host`.`cluster_id` AS `cluster_id`, + `host`.`status` AS `host_status`, + `host`.`resource_state` AS `host_resource_state`, + `vm_template`.`id` AS `template_id`, + `vm_template`.`uuid` AS `template_uuid`, + `vm_template`.`name` AS `template_name`, + `vm_template`.`type` AS `template_type`, + `vm_template`.`display_text` AS `template_display_text`, + `vm_template`.`enable_password` AS `password_enabled`, + `iso`.`id` AS `iso_id`, + `iso`.`uuid` AS `iso_uuid`, + `iso`.`name` AS `iso_name`, + `iso`.`display_text` AS `iso_display_text`, + `service_offering`.`id` AS `service_offering_id`, + `service_offering`.`uuid` AS `service_offering_uuid`, + `disk_offering`.`uuid` AS `disk_offering_uuid`, + `disk_offering`.`id` AS `disk_offering_id`, + (CASE + WHEN ISNULL(`service_offering`.`cpu`) THEN `custom_cpu`.`value` + ELSE `service_offering`.`cpu` + END) AS `cpu`, + (CASE + WHEN ISNULL(`service_offering`.`speed`) THEN `custom_speed`.`value` + ELSE `service_offering`.`speed` + END) AS `speed`, + (CASE + WHEN ISNULL(`service_offering`.`ram_size`) THEN `custom_ram_size`.`value` + ELSE `service_offering`.`ram_size` + END) AS `ram_size`, + `backup_offering`.`uuid` AS `backup_offering_uuid`, + `backup_offering`.`id` AS `backup_offering_id`, + `service_offering`.`name` AS `service_offering_name`, + `disk_offering`.`name` AS `disk_offering_name`, + `backup_offering`.`name` AS `backup_offering_name`, + `storage_pool`.`id` AS `pool_id`, + `storage_pool`.`uuid` AS `pool_uuid`, + `storage_pool`.`pool_type` AS `pool_type`, + `volumes`.`id` AS `volume_id`, + `volumes`.`uuid` AS `volume_uuid`, + `volumes`.`device_id` AS `volume_device_id`, + `volumes`.`volume_type` AS `volume_type`, + `security_group`.`id` AS `security_group_id`, + `security_group`.`uuid` AS `security_group_uuid`, + `security_group`.`name` AS `security_group_name`, + `security_group`.`description` AS `security_group_description`, + `nics`.`id` AS `nic_id`, + `nics`.`uuid` AS `nic_uuid`, + `nics`.`device_id` AS `nic_device_id`, + `nics`.`network_id` AS `network_id`, + `nics`.`ip4_address` AS `ip_address`, + `nics`.`ip6_address` AS `ip6_address`, + `nics`.`ip6_gateway` AS `ip6_gateway`, + `nics`.`ip6_cidr` AS `ip6_cidr`, + `nics`.`default_nic` AS `is_default_nic`, + `nics`.`gateway` AS `gateway`, + `nics`.`netmask` AS `netmask`, + `nics`.`mac_address` AS `mac_address`, + `nics`.`broadcast_uri` AS `broadcast_uri`, + `nics`.`isolation_uri` AS `isolation_uri`, + `vpc`.`id` AS `vpc_id`, + `vpc`.`uuid` AS `vpc_uuid`, + `networks`.`uuid` AS `network_uuid`, + `networks`.`name` AS `network_name`, + `networks`.`traffic_type` AS `traffic_type`, + `networks`.`guest_type` AS `guest_type`, + `user_ip_address`.`id` AS `public_ip_id`, + `user_ip_address`.`uuid` AS `public_ip_uuid`, + `user_ip_address`.`public_ip_address` AS `public_ip_address`, + `ssh_details`.`value` AS `keypair_names`, + `resource_tags`.`id` AS `tag_id`, + `resource_tags`.`uuid` AS `tag_uuid`, + `resource_tags`.`key` AS `tag_key`, + `resource_tags`.`value` AS `tag_value`, + `resource_tags`.`domain_id` AS `tag_domain_id`, + `domain`.`uuid` AS `tag_domain_uuid`, + `domain`.`name` AS `tag_domain_name`, + `resource_tags`.`account_id` AS `tag_account_id`, + `account`.`account_name` AS `tag_account_name`, + `resource_tags`.`resource_id` AS `tag_resource_id`, + `resource_tags`.`resource_uuid` AS `tag_resource_uuid`, + `resource_tags`.`resource_type` AS `tag_resource_type`, + `resource_tags`.`customer` AS `tag_customer`, + `async_job`.`id` AS `job_id`, + `async_job`.`uuid` AS `job_uuid`, + `async_job`.`job_status` AS `job_status`, + `async_job`.`account_id` AS `job_account_id`, + `affinity_group`.`id` AS `affinity_group_id`, + `affinity_group`.`uuid` AS `affinity_group_uuid`, + `affinity_group`.`name` AS `affinity_group_name`, + `affinity_group`.`description` AS `affinity_group_description`, + `autoscale_vmgroups`.`id` AS `autoscale_vmgroup_id`, + `autoscale_vmgroups`.`uuid` AS `autoscale_vmgroup_uuid`, + `autoscale_vmgroups`.`name` AS `autoscale_vmgroup_name`, + `vm_instance`.`dynamically_scalable` AS `dynamically_scalable`, + `user_data`.`id` AS `user_data_id`, + `user_data`.`uuid` AS `user_data_uuid`, + `user_data`.`name` AS `user_data_name`, + `user_vm`.`user_data_details` AS `user_data_details`, + `vm_template`.`user_data_link_policy` AS `user_data_policy` +FROM + (((((((((((((((((((((((((((((((((((`user_vm` + JOIN `vm_instance` ON (((`vm_instance`.`id` = `user_vm`.`id`) + AND ISNULL(`vm_instance`.`removed`)))) + JOIN `account` ON ((`vm_instance`.`account_id` = `account`.`id`))) + JOIN `domain` ON ((`vm_instance`.`domain_id` = `domain`.`id`))) + LEFT JOIN `guest_os` ON ((`vm_instance`.`guest_os_id` = `guest_os`.`id`))) + LEFT JOIN `host_pod_ref` ON ((`vm_instance`.`pod_id` = `host_pod_ref`.`id`))) + LEFT JOIN `projects` ON ((`projects`.`project_account_id` = `account`.`id`))) + LEFT JOIN `instance_group_vm_map` ON ((`vm_instance`.`id` = `instance_group_vm_map`.`instance_id`))) + LEFT JOIN `instance_group` ON ((`instance_group_vm_map`.`group_id` = `instance_group`.`id`))) + LEFT JOIN `data_center` ON ((`vm_instance`.`data_center_id` = `data_center`.`id`))) + LEFT JOIN `host` ON ((`vm_instance`.`host_id` = `host`.`id`))) + LEFT JOIN `vm_template` ON ((`vm_instance`.`vm_template_id` = `vm_template`.`id`))) + LEFT JOIN `vm_template` `iso` ON ((`iso`.`id` = `user_vm`.`iso_id`))) + LEFT JOIN `volumes` ON ((`vm_instance`.`id` = `volumes`.`instance_id`))) + LEFT JOIN `service_offering` ON ((`vm_instance`.`service_offering_id` = `service_offering`.`id`))) + LEFT JOIN `disk_offering` `svc_disk_offering` ON ((`volumes`.`disk_offering_id` = `svc_disk_offering`.`id`))) + LEFT JOIN `disk_offering` ON ((`volumes`.`disk_offering_id` = `disk_offering`.`id`))) + LEFT JOIN `backup_offering` ON ((`vm_instance`.`backup_offering_id` = `backup_offering`.`id`))) + LEFT JOIN `storage_pool` ON ((`volumes`.`pool_id` = `storage_pool`.`id`))) + LEFT JOIN `security_group_vm_map` ON ((`vm_instance`.`id` = `security_group_vm_map`.`instance_id`))) + LEFT JOIN `security_group` ON ((`security_group_vm_map`.`security_group_id` = `security_group`.`id`))) + LEFT JOIN `user_data` ON ((`user_data`.`id` = `user_vm`.`user_data_id`))) + LEFT JOIN `nics` ON (((`vm_instance`.`id` = `nics`.`instance_id`) + AND ISNULL(`nics`.`removed`)))) + LEFT JOIN `networks` ON ((`nics`.`network_id` = `networks`.`id`))) + LEFT JOIN `vpc` ON (((`networks`.`vpc_id` = `vpc`.`id`) + AND ISNULL(`vpc`.`removed`)))) + LEFT JOIN `user_ip_address` ON ((`user_ip_address`.`vm_id` = `vm_instance`.`id`))) + LEFT JOIN `user_vm_details` `ssh_details` ON (((`ssh_details`.`vm_id` = `vm_instance`.`id`) + AND (`ssh_details`.`name` = 'SSH.KeyPairNames')))) + LEFT JOIN `resource_tags` ON (((`resource_tags`.`resource_id` = `vm_instance`.`id`) + AND (`resource_tags`.`resource_type` = 'UserVm')))) + LEFT JOIN `async_job` ON (((`async_job`.`instance_id` = `vm_instance`.`id`) + AND (`async_job`.`instance_type` = 'VirtualMachine') + AND (`async_job`.`job_status` = 0)))) + LEFT JOIN `affinity_group_vm_map` ON ((`vm_instance`.`id` = `affinity_group_vm_map`.`instance_id`))) + LEFT JOIN `affinity_group` ON ((`affinity_group_vm_map`.`affinity_group_id` = `affinity_group`.`id`))) + LEFT JOIN `autoscale_vmgroup_vm_map` ON ((`autoscale_vmgroup_vm_map`.`instance_id` = `vm_instance`.`id`))) + LEFT JOIN `autoscale_vmgroups` ON ((`autoscale_vmgroup_vm_map`.`vmgroup_id` = `autoscale_vmgroups`.`id`))) + LEFT JOIN `user_vm_details` `custom_cpu` ON (((`custom_cpu`.`vm_id` = `vm_instance`.`id`) + AND (`custom_cpu`.`name` = 'CpuNumber')))) + LEFT JOIN `user_vm_details` `custom_speed` ON (((`custom_speed`.`vm_id` = `vm_instance`.`id`) + AND (`custom_speed`.`name` = 'CpuSpeed')))) + LEFT JOIN `user_vm_details` `custom_ram_size` ON (((`custom_ram_size`.`vm_id` = `vm_instance`.`id`) + AND (`custom_ram_size`.`name` = 'memory')))); + -- Add tables for Cluster DRS DROP TABLE IF EXISTS `cloud`.`cluster_drs_plan`; CREATE TABLE `cloud`.`cluster_drs_plan` ( diff --git a/engine/schema/src/test/java/com/cloud/storage/VnfTemplateDetailVOTest.java b/engine/schema/src/test/java/com/cloud/storage/VnfTemplateDetailVOTest.java new file mode 100755 index 00000000000..99edbc46c42 --- /dev/null +++ b/engine/schema/src/test/java/com/cloud/storage/VnfTemplateDetailVOTest.java @@ -0,0 +1,38 @@ +// Licensed to the Apache Software Foundation (ASF) under one +// or more contributor license agreements. See the NOTICE file +// distributed with this work for additional information +// regarding copyright ownership. The ASF licenses this file +// to you 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. +package com.cloud.storage; + +import org.junit.Assert; +import org.junit.Test; + +public class VnfTemplateDetailVOTest { + + static long templateId = 100L; + static String name = "key1"; + static String value = "value1"; + static boolean display = true; + + @Test + public void testVnfTemplateNicVOProperties() { + VnfTemplateDetailVO detailVO = new VnfTemplateDetailVO(templateId, name, value, display); + + Assert.assertEquals(templateId, detailVO.getResourceId()); + Assert.assertEquals(name, detailVO.getName()); + Assert.assertEquals(value, detailVO.getValue()); + Assert.assertEquals(display, detailVO.isDisplay()); + } +} diff --git a/engine/schema/src/test/java/com/cloud/storage/VnfTemplateNicVOTest.java b/engine/schema/src/test/java/com/cloud/storage/VnfTemplateNicVOTest.java new file mode 100755 index 00000000000..96ebd4ed171 --- /dev/null +++ b/engine/schema/src/test/java/com/cloud/storage/VnfTemplateNicVOTest.java @@ -0,0 +1,46 @@ +// Licensed to the Apache Software Foundation (ASF) under one +// or more contributor license agreements. See the NOTICE file +// distributed with this work for additional information +// regarding copyright ownership. The ASF licenses this file +// to you 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. +package com.cloud.storage; + +import org.junit.Assert; +import org.junit.Test; + +public class VnfTemplateNicVOTest { + + static long templateId = 100L; + static long deviceId = 0L; + static String deviceName = "eth0"; + static boolean required = true; + static boolean management = false; + static String description = "description of vnf nic"; + + + @Test + public void testVnfTemplateNicVOProperties() { + VnfTemplateNicVO nicVO = new VnfTemplateNicVO(templateId, deviceId, deviceName, required, management, description); + + Assert.assertEquals(templateId, nicVO.getTemplateId()); + Assert.assertEquals(deviceId, nicVO.getDeviceId()); + Assert.assertEquals(deviceName, nicVO.getDeviceName()); + Assert.assertEquals(required, nicVO.isRequired()); + Assert.assertEquals(management, nicVO.isManagement()); + Assert.assertEquals(description, nicVO.getDescription()); + + String expected = String.format("Template {\"deviceId\":%d,\"id\":0,\"required\":%s,\"templateId\":%d}", deviceId, required, templateId); + Assert.assertEquals(expected, nicVO.toString()); + } +} diff --git a/engine/schema/src/test/java/com/cloud/storage/dao/VnfTemplateNicDaoImplTest.java b/engine/schema/src/test/java/com/cloud/storage/dao/VnfTemplateNicDaoImplTest.java new file mode 100644 index 00000000000..6a574fc81ee --- /dev/null +++ b/engine/schema/src/test/java/com/cloud/storage/dao/VnfTemplateNicDaoImplTest.java @@ -0,0 +1,88 @@ +// Licensed to the Apache Software Foundation (ASF) under one +// or more contributor license agreements. See the NOTICE file +// distributed with this work for additional information +// regarding copyright ownership. The ASF licenses this file +// to you 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. +package com.cloud.storage.dao; + +import com.cloud.storage.VnfTemplateNicVO; +import com.cloud.utils.db.SearchBuilder; +import com.cloud.utils.db.SearchCriteria; +import com.cloud.utils.db.TransactionLegacy; +import org.junit.Assert; +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.Mock; +import org.mockito.MockedStatic; +import org.mockito.Mockito; +import org.mockito.Spy; +import org.mockito.junit.MockitoJUnitRunner; + +import java.util.List; + +@RunWith(MockitoJUnitRunner.class) +public class VnfTemplateNicDaoImplTest { + + @Mock + SearchBuilder searchBuilderVnfTemplateNicVOMock; + + @Mock + SearchCriteria searchCriteriaVnfTemplateNicVOMock; + + @Mock + List listVnfTemplateNicVOMock; + + @Mock + private TransactionLegacy transactionMock; + + @Spy + VnfTemplateNicDaoImpl vnfTemplateNicDaoImplSpy; + + @Before + public void setUp() { + vnfTemplateNicDaoImplSpy.TemplateSearch = searchBuilderVnfTemplateNicVOMock; + Mockito.doReturn(searchCriteriaVnfTemplateNicVOMock).when(searchBuilderVnfTemplateNicVOMock).create(); + Mockito.doNothing().when(searchCriteriaVnfTemplateNicVOMock).setParameters(Mockito.anyString(), Mockito.any()); + } + + @Test + public void testListByTemplateId() { + Mockito.doReturn(listVnfTemplateNicVOMock).when(vnfTemplateNicDaoImplSpy).listBy(Mockito.any(SearchCriteria.class)); + long templateId = 100L; + + List result = vnfTemplateNicDaoImplSpy.listByTemplateId(templateId); + + Assert.assertEquals(listVnfTemplateNicVOMock, result); + Mockito.verify(searchCriteriaVnfTemplateNicVOMock).setParameters("templateId", templateId); + } + + @Test + public void testDeleteByTemplateId() { + Mockito.doReturn(0).when(vnfTemplateNicDaoImplSpy).remove(searchCriteriaVnfTemplateNicVOMock); + long templateId = 100L; + + try (MockedStatic ignore = Mockito.mockStatic(TransactionLegacy.class)) { + Mockito.when(TransactionLegacy.currentTxn()).thenReturn(transactionMock); + Mockito.doNothing().when(transactionMock).start(); + Mockito.doReturn(true).when(transactionMock).commit(); + + vnfTemplateNicDaoImplSpy.deleteByTemplateId(templateId); + + Mockito.verify(transactionMock, Mockito.times(1)).start(); + Mockito.verify(vnfTemplateNicDaoImplSpy, Mockito.times(1)).remove(searchCriteriaVnfTemplateNicVOMock); + Mockito.verify(transactionMock, Mockito.times(1)).commit(); + } + } +} diff --git a/server/src/main/java/com/cloud/api/ApiDBUtils.java b/server/src/main/java/com/cloud/api/ApiDBUtils.java index ddfd0671820..5d821d38f2e 100644 --- a/server/src/main/java/com/cloud/api/ApiDBUtils.java +++ b/server/src/main/java/com/cloud/api/ApiDBUtils.java @@ -1533,6 +1533,10 @@ public class ApiDBUtils { return s_ipAddressDao.findByAssociatedVmId(vmId); } + public static IpAddress findIpByAssociatedVmIdAndNetworkId(long vmId, long networkId) { + return s_ipAddressDao.findByVmIdAndNetworkId(networkId, vmId); + } + public static String getHaTag() { return s_haMgr.getHaTag(); } diff --git a/server/src/main/java/com/cloud/api/query/QueryManagerImpl.java b/server/src/main/java/com/cloud/api/query/QueryManagerImpl.java index 946b48cf480..93b858077f6 100644 --- a/server/src/main/java/com/cloud/api/query/QueryManagerImpl.java +++ b/server/src/main/java/com/cloud/api/query/QueryManagerImpl.java @@ -99,6 +99,7 @@ import org.apache.cloudstack.api.command.user.snapshot.CopySnapshotCmd; import org.apache.cloudstack.api.command.user.snapshot.ListSnapshotsCmd; import org.apache.cloudstack.api.command.user.tag.ListTagsCmd; import org.apache.cloudstack.api.command.user.template.ListTemplatesCmd; +import org.apache.cloudstack.api.command.user.template.ListVnfTemplatesCmd; import org.apache.cloudstack.api.command.user.vm.ListVMsCmd; import org.apache.cloudstack.api.command.user.vmgroup.ListVMGroupsCmd; import org.apache.cloudstack.api.command.user.volume.ListResourceDetailsCmd; @@ -222,6 +223,7 @@ import com.cloud.ha.HighAvailabilityManager; import com.cloud.hypervisor.Hypervisor; import com.cloud.hypervisor.Hypervisor.HypervisorType; import com.cloud.network.RouterHealthCheckResult; +import com.cloud.network.VNF; import com.cloud.network.VpcVirtualNetworkApplianceService; import com.cloud.network.dao.RouterHealthCheckResultDao; import com.cloud.network.dao.RouterHealthCheckResultVO; @@ -1315,6 +1317,15 @@ public class QueryManagerImpl extends MutualExclusiveIdsManagerBase implements Q userVmSearchBuilder.join("autoScaleVmGroup", autoScaleMapSearch, autoScaleMapSearch.entity().getInstanceId(), userVmSearchBuilder.entity().getId(), JoinBuilder.JoinType.INNER); } + Boolean isVnf = cmd.getVnf(); + if (isVnf != null) { + SearchBuilder templateSearch = _templateDao.createSearchBuilder(); + templateSearch.and("templateTypeEQ", templateSearch.entity().getTemplateType(), Op.EQ); + templateSearch.and("templateTypeNEQ", templateSearch.entity().getTemplateType(), Op.NEQ); + + userVmSearchBuilder.join("vmTemplate", templateSearch, templateSearch.entity().getId(), userVmSearchBuilder.entity().getTemplateId(), JoinBuilder.JoinType.INNER); + } + SearchCriteria userVmSearchCriteria = userVmSearchBuilder.create(); accountMgr.buildACLSearchCriteria(userVmSearchCriteria, domainId, isRecursive, permittedAccounts, listProjectResourcesCriteria); @@ -1423,6 +1434,14 @@ public class QueryManagerImpl extends MutualExclusiveIdsManagerBase implements Q userVmSearchCriteria.setJoinParameters("autoScaleVmGroup", "autoScaleVmGroupId", autoScaleVmGroupId); } + if (isVnf != null) { + if (isVnf) { + userVmSearchCriteria.setJoinParameters("vmTemplate", "templateTypeEQ", TemplateType.VNF); + } else { + userVmSearchCriteria.setJoinParameters("vmTemplate", "templateTypeNEQ", TemplateType.VNF); + } + } + if (isRootAdmin) { if (podId != null) { userVmSearchCriteria.setParameters("podId", podId); @@ -3791,13 +3810,24 @@ public class QueryManagerImpl extends MutualExclusiveIdsManagerBase implements Q boolean showDomr = ((templateFilter != TemplateFilter.selfexecutable) && (templateFilter != TemplateFilter.featured)); HypervisorType hypervisorType = HypervisorType.getType(cmd.getHypervisor()); + String templateType = cmd.getTemplateType(); + if (cmd instanceof ListVnfTemplatesCmd) { + if (templateType == null) { + templateType = TemplateType.VNF.name(); + } else if (!TemplateType.VNF.name().equals(templateType)) { + throw new InvalidParameterValueException("Template type must be VNF when list VNF templates"); + } + } + Boolean isVnf = cmd.getVnf(); + return searchForTemplatesInternal(id, cmd.getTemplateName(), cmd.getKeyword(), templateFilter, false, null, cmd.getPageSizeVal(), cmd.getStartIndex(), cmd.getZoneId(), hypervisorType, - showDomr, cmd.listInReadyState(), permittedAccounts, caller, listProjectResourcesCriteria, tags, showRemovedTmpl, cmd.getIds(), parentTemplateId, cmd.getShowUnique()); + showDomr, cmd.listInReadyState(), permittedAccounts, caller, listProjectResourcesCriteria, tags, showRemovedTmpl, cmd.getIds(), parentTemplateId, cmd.getShowUnique(), templateType, isVnf); } private Pair, Integer> searchForTemplatesInternal(Long templateId, String name, String keyword, TemplateFilter templateFilter, boolean isIso, Boolean bootable, Long pageSize, Long startIndex, Long zoneId, HypervisorType hyperType, boolean showDomr, boolean onlyReady, List permittedAccounts, Account caller, - ListProjectResourcesCriteria listProjectResourcesCriteria, Map tags, boolean showRemovedTmpl, List ids, Long parentTemplateId, Boolean showUnique) { + ListProjectResourcesCriteria listProjectResourcesCriteria, Map tags, boolean showRemovedTmpl, List ids, Long parentTemplateId, Boolean showUnique, String templateType, + Boolean isVnf) { // check if zone is configured, if not, just return empty list List hypers = null; @@ -3964,7 +3994,7 @@ public class QueryManagerImpl extends MutualExclusiveIdsManagerBase implements Q applyPublicTemplateSharingRestrictions(sc, caller); return templateChecks(isIso, hypers, tags, name, keyword, hyperType, onlyReady, bootable, zoneId, showDomr, caller, - showRemovedTmpl, parentTemplateId, showUnique, searchFilter, sc); + showRemovedTmpl, parentTemplateId, showUnique, templateType, isVnf, searchFilter, sc); } @@ -4019,7 +4049,7 @@ public class QueryManagerImpl extends MutualExclusiveIdsManagerBase implements Q private Pair, Integer> templateChecks(boolean isIso, List hypers, Map tags, String name, String keyword, HypervisorType hyperType, boolean onlyReady, Boolean bootable, Long zoneId, boolean showDomr, Account caller, - boolean showRemovedTmpl, Long parentTemplateId, Boolean showUnique, + boolean showRemovedTmpl, Long parentTemplateId, Boolean showUnique, String templateType, Boolean isVnf, Filter searchFilter, SearchCriteria sc) { if (!isIso) { // add hypervisor criteria for template case @@ -4101,6 +4131,18 @@ public class QueryManagerImpl extends MutualExclusiveIdsManagerBase implements Q sc.addAnd("parentTemplateId", SearchCriteria.Op.EQ, parentTemplateId); } + if (templateType != null) { + sc.addAnd("templateType", SearchCriteria.Op.EQ, templateType); + } + + if (isVnf != null) { + if (isVnf) { + sc.addAnd("templateType", SearchCriteria.Op.EQ, TemplateType.VNF); + } else { + sc.addAnd("templateType", SearchCriteria.Op.NEQ, TemplateType.VNF); + } + } + // don't return removed template, this should not be needed since we // changed annotation for removed field in TemplateJoinVO. // sc.addAnd("removed", SearchCriteria.Op.NULL); @@ -4192,7 +4234,7 @@ public class QueryManagerImpl extends MutualExclusiveIdsManagerBase implements Q HypervisorType hypervisorType = HypervisorType.getType(cmd.getHypervisor()); return searchForTemplatesInternal(cmd.getId(), cmd.getIsoName(), cmd.getKeyword(), isoFilter, true, cmd.isBootable(), cmd.getPageSizeVal(), cmd.getStartIndex(), cmd.getZoneId(), - hypervisorType, true, cmd.listInReadyState(), permittedAccounts, caller, listProjectResourcesCriteria, tags, showRemovedISO, null, null, cmd.getShowUnique()); + hypervisorType, true, cmd.listInReadyState(), permittedAccounts, caller, listProjectResourcesCriteria, tags, showRemovedISO, null, null, cmd.getShowUnique(), null, null); } @Override @@ -4212,6 +4254,9 @@ public class QueryManagerImpl extends MutualExclusiveIdsManagerBase implements Q } fillVMOrTemplateDetailOptions(options, hypervisorType); break; + case VnfTemplate: + fillVnfTemplateDetailOptions(options); + return new DetailOptionsResponse(options); default: throw new CloudRuntimeException("Resource type not supported."); } @@ -4235,6 +4280,19 @@ public class QueryManagerImpl extends MutualExclusiveIdsManagerBase implements Q return responses; } + private void fillVnfTemplateDetailOptions(final Map> options) { + for (VNF.AccessDetail detail : VNF.AccessDetail.values()) { + if (VNF.AccessDetail.ACCESS_METHODS.equals(detail)) { + options.put(detail.name().toLowerCase(), Arrays.stream(VNF.AccessMethod.values()).map(method -> method.toString()).sorted().collect(Collectors.toList())); + } else { + options.put(detail.name().toLowerCase(), Collections.emptyList()); + } + } + for (VNF.VnfDetail detail : VNF.VnfDetail.values()) { + options.put(detail.name().toLowerCase(), Collections.emptyList()); + } + } + private void fillVMOrTemplateDetailOptions(final Map> options, final HypervisorType hypervisorType) { if (options == null) { throw new CloudRuntimeException("Invalid/null detail-options response object passed"); diff --git a/server/src/main/java/com/cloud/api/query/dao/TemplateJoinDaoImpl.java b/server/src/main/java/com/cloud/api/query/dao/TemplateJoinDaoImpl.java index 4fe0f200741..b9dcad98f22 100644 --- a/server/src/main/java/com/cloud/api/query/dao/TemplateJoinDaoImpl.java +++ b/server/src/main/java/com/cloud/api/query/dao/TemplateJoinDaoImpl.java @@ -17,6 +17,7 @@ package com.cloud.api.query.dao; import java.util.ArrayList; +import java.util.Collections; import java.util.EnumSet; import java.util.HashMap; import java.util.HashSet; @@ -29,10 +30,16 @@ import javax.inject.Inject; import com.cloud.deployasis.DeployAsIsConstants; import com.cloud.deployasis.TemplateDeployAsIsDetailVO; import com.cloud.deployasis.dao.TemplateDeployAsIsDetailsDao; +import com.cloud.storage.VnfTemplateDetailVO; +import com.cloud.storage.VnfTemplateNicVO; +import com.cloud.storage.dao.VnfTemplateDetailsDao; +import com.cloud.storage.dao.VnfTemplateNicDao; import com.cloud.user.dao.UserDataDao; import org.apache.cloudstack.annotation.AnnotationService; import org.apache.cloudstack.annotation.dao.AnnotationDao; import org.apache.cloudstack.api.ApiConstants; +import org.apache.cloudstack.api.response.VnfNicResponse; +import org.apache.cloudstack.api.response.VnfTemplateResponse; import org.apache.cloudstack.storage.datastore.db.ImageStoreVO; import org.apache.cloudstack.utils.security.DigestHelper; import org.apache.log4j.Logger; @@ -93,6 +100,10 @@ public class TemplateJoinDaoImpl extends GenericDaoBaseWithTagInformation tmpltIdPairSearch; @@ -190,7 +201,7 @@ public class TemplateJoinDaoImpl extends GenericDaoBaseWithTagInformation nics = vnfTemplateNicDao.listByTemplateId(template.getId()); + for (VnfTemplateNicVO nic : nics) { + vnfTemplateResponse.addVnfNic(new VnfNicResponse(nic.getDeviceId(), nic.getDeviceName(), nic.isRequired(), nic.isManagement(), nic.getDescription())); + } + List details = vnfTemplateDetailsDao.listDetails(template.getId()); + Collections.sort(details, (v1, v2) -> v1.getName().compareToIgnoreCase(v2.getName())); + for (VnfTemplateDetailVO detail : details) { + vnfTemplateResponse.addVnfDetail(detail.getName(), detail.getValue()); + } + templateResponse = vnfTemplateResponse; + } + return templateResponse; + } + private void setDeployAsIsDetails(TemplateJoinVO template, TemplateResponse templateResponse) { if (template.isDeployAsIs()) { List deployAsIsDetails = templateDeployAsIsDetailsDao.listDetails(template.getId()); @@ -326,7 +356,7 @@ public class TemplateJoinDaoImpl extends GenericDaoBaseWithTagInformation VmDetailSearch; private final SearchBuilder activeVmByIsoSearch; @@ -183,6 +195,7 @@ public class UserVmJoinDaoImpl extends GenericDaoBaseWithTagInformation nicExtraDhcpOptionResponses = _nicExtraDhcpOptionDao.listByNicId(nic_id).stream() @@ -419,9 +438,25 @@ public class UserVmJoinDaoImpl extends GenericDaoBaseWithTagInformation vnfNics = vnfTemplateNicDao.listByTemplateId(userVm.getTemplateId()); + for (VnfTemplateNicVO nic : vnfNics) { + userVmResponse.addVnfNic(new VnfNicResponse(nic.getDeviceId(), nic.getDeviceName(), nic.isRequired(), nic.isManagement(), nic.getDescription())); + } + List vnfDetails = vnfTemplateDetailsDao.listDetails(userVm.getTemplateId()); + Collections.sort(vnfDetails, (v1, v2) -> v1.getName().compareToIgnoreCase(v2.getName())); + for (VnfTemplateDetailVO detail : vnfDetails) { + userVmResponse.addVnfDetail(detail.getName(), detail.getValue()); + } + } + private void addVmRxTxDataToResponse(UserVmJoinVO userVm, UserVmResponse userVmResponse) { Long bytesReceived = 0L; Long bytesSent = 0L; @@ -519,6 +554,11 @@ public class UserVmJoinDaoImpl extends GenericDaoBaseWithTagInformation zoneId = cmd.getZoneIds(); // ignore passed zoneId if we are using region wide image store @@ -305,7 +306,7 @@ public abstract class TemplateAdapterBase extends AdapterBase implements Templat } return prepare(false, CallContext.current().getCallingUserId(), cmd.getTemplateName(), cmd.getDisplayText(), cmd.getBits(), cmd.isPasswordEnabled(), cmd.getRequiresHvm(), cmd.getUrl(), cmd.isPublic(), cmd.isFeatured(), cmd.isExtractable(), cmd.getFormat(), cmd.getOsTypeId(), zoneId, hypervisorType, cmd.getChecksum(), true, - cmd.getTemplateTag(), owner, details, cmd.isSshKeyEnabled(), null, cmd.isDynamicallyScalable(), isRouting ? TemplateType.ROUTING : TemplateType.USER, + cmd.getTemplateTag(), owner, details, cmd.isSshKeyEnabled(), null, cmd.isDynamicallyScalable(), templateType, cmd.isDirectDownload(), cmd.isDeployAsIs()); } diff --git a/server/src/main/java/com/cloud/template/TemplateManagerImpl.java b/server/src/main/java/com/cloud/template/TemplateManagerImpl.java index 43a3de2078d..f5d385b4e10 100755 --- a/server/src/main/java/com/cloud/template/TemplateManagerImpl.java +++ b/server/src/main/java/com/cloud/template/TemplateManagerImpl.java @@ -36,6 +36,7 @@ import javax.naming.ConfigurationException; import org.apache.cloudstack.acl.SecurityChecker.AccessType; import org.apache.cloudstack.api.ApiConstants; +import org.apache.cloudstack.api.BaseCmd; import org.apache.cloudstack.api.BaseListTemplateOrIsoPermissionsCmd; import org.apache.cloudstack.api.BaseUpdateTemplateOrIsoCmd; import org.apache.cloudstack.api.BaseUpdateTemplateOrIsoPermissionsCmd; @@ -53,8 +54,10 @@ import org.apache.cloudstack.api.command.user.template.ExtractTemplateCmd; import org.apache.cloudstack.api.command.user.template.GetUploadParamsForTemplateCmd; import org.apache.cloudstack.api.command.user.template.ListTemplatePermissionsCmd; import org.apache.cloudstack.api.command.user.template.RegisterTemplateCmd; +import org.apache.cloudstack.api.command.user.template.RegisterVnfTemplateCmd; import org.apache.cloudstack.api.command.user.template.UpdateTemplateCmd; import org.apache.cloudstack.api.command.user.template.UpdateTemplatePermissionsCmd; +import org.apache.cloudstack.api.command.user.template.UpdateVnfTemplateCmd; import org.apache.cloudstack.api.command.user.userdata.LinkUserDataToTemplateCmd; import org.apache.cloudstack.api.response.GetUploadParamsResponse; import org.apache.cloudstack.context.CallContext; @@ -96,6 +99,8 @@ import org.apache.cloudstack.storage.datastore.db.StoragePoolVO; import org.apache.cloudstack.storage.datastore.db.TemplateDataStoreDao; import org.apache.cloudstack.storage.datastore.db.TemplateDataStoreVO; import org.apache.cloudstack.storage.image.datastore.ImageStoreEntity; +import org.apache.cloudstack.storage.template.VnfTemplateManager; +import org.apache.cloudstack.storage.template.VnfTemplateUtils; import org.apache.cloudstack.storage.to.TemplateObjectTO; import org.apache.cloudstack.utils.imagestore.ImageStoreUtil; import org.apache.commons.collections.CollectionUtils; @@ -303,6 +308,8 @@ public class TemplateManagerImpl extends ManagerBase implements TemplateManager, @Inject protected SnapshotHelper snapshotHelper; + @Inject + VnfTemplateManager vnfTemplateManager; private TemplateAdapter getAdapter(HypervisorType type) { TemplateAdapter adapter = null; @@ -360,6 +367,9 @@ public class TemplateManagerImpl extends ManagerBase implements TemplateManager, if (template != null) { CallContext.current().putContextParameter(VirtualMachineTemplate.class, template.getUuid()); + if (cmd instanceof RegisterVnfTemplateCmd) { + vnfTemplateManager.persistVnfTemplate(template.getId(), (RegisterVnfTemplateCmd) cmd); + } return template; } else { throw new CloudRuntimeException("Failed to create a template"); @@ -1329,6 +1339,8 @@ public class TemplateManagerImpl extends ManagerBase implements TemplateManager, throw new InvalidParameterValueException("Please specify a valid template."); } + VnfTemplateUtils.validateApiCommandParams(cmd, template); + TemplateAdapter adapter = getAdapter(template.getHypervisorType()); TemplateProfile profile = adapter.prepareDelete(cmd); return adapter.delete(profile); @@ -2113,22 +2125,11 @@ public class TemplateManagerImpl extends ManagerBase implements TemplateManager, // update template type TemplateType templateType = null; if (cmd instanceof UpdateTemplateCmd) { - String newType = ((UpdateTemplateCmd)cmd).getTemplateType(); - if (newType != null) { - if (!_accountService.isRootAdmin(account.getId())) { - throw new PermissionDeniedException("Parameter templatetype can only be specified by a Root Admin, permission denied"); - } - try { - templateType = TemplateType.valueOf(newType.toUpperCase()); - } catch (IllegalArgumentException ex) { - throw new InvalidParameterValueException("Please specify a valid templatetype: ROUTING / SYSTEM / USER / BUILTIN / PERHOST"); - } - } - if (templateType != null && cmd.isRoutingType() != null && (TemplateType.ROUTING.equals(templateType) != cmd.isRoutingType())) { - throw new InvalidParameterValueException("Please specify a valid templatetype (consistent with isrouting parameter)."); - } - if (templateType != null && (templateType == TemplateType.SYSTEM || templateType == TemplateType.BUILTIN) && !template.isCrossZones()) { - throw new InvalidParameterValueException("System and Builtin templates must be cross zone"); + boolean isAdmin = _accountMgr.isAdmin(account.getId()); + templateType = validateTemplateType(cmd, isAdmin, template.isCrossZones()); + if (cmd instanceof UpdateVnfTemplateCmd) { + VnfTemplateUtils.validateApiCommandParams(cmd, template); + vnfTemplateManager.updateVnfTemplate(template.getId(), (UpdateVnfTemplateCmd) cmd); } } @@ -2248,6 +2249,51 @@ public class TemplateManagerImpl extends ManagerBase implements TemplateManager, return _tmpltDao.findById(id); } + @Override + public TemplateType validateTemplateType(BaseCmd cmd, boolean isAdmin, boolean isCrossZones) { + if (!(cmd instanceof UpdateTemplateCmd) && !(cmd instanceof RegisterTemplateCmd)) { + return null; + } + TemplateType templateType = null; + String newType = null; + Boolean isRoutingType = null; + if (cmd instanceof UpdateTemplateCmd) { + newType = ((UpdateTemplateCmd)cmd).getTemplateType(); + isRoutingType = ((UpdateTemplateCmd)cmd).isRoutingType(); + } else if (cmd instanceof RegisterTemplateCmd) { + newType = ((RegisterTemplateCmd)cmd).getTemplateType(); + isRoutingType = ((RegisterTemplateCmd)cmd).isRoutingType(); + } + if (newType != null) { + try { + templateType = TemplateType.valueOf(newType.toUpperCase()); + } catch (IllegalArgumentException ex) { + throw new InvalidParameterValueException(String.format("Please specify a valid templatetype: %s", + org.apache.commons.lang3.StringUtils.join(",", TemplateType.values()))); + } + } + if (templateType != null) { + if (isRoutingType != null && (TemplateType.ROUTING.equals(templateType) != isRoutingType)) { + throw new InvalidParameterValueException("Please specify a valid templatetype (consistent with isrouting parameter)."); + } else if ((templateType == TemplateType.SYSTEM || templateType == TemplateType.BUILTIN) && !isCrossZones) { + throw new InvalidParameterValueException("System and Builtin templates must be cross zone."); + } else if ((cmd instanceof RegisterVnfTemplateCmd || cmd instanceof UpdateVnfTemplateCmd) && !TemplateType.VNF.equals(templateType)) { + throw new InvalidParameterValueException("The template type must be VNF for VNF templates, but the actual type is " + templateType); + } + } else if (cmd instanceof RegisterTemplateCmd) { + boolean isRouting = Boolean.TRUE.equals(isRoutingType); + templateType = (cmd instanceof RegisterVnfTemplateCmd) ? TemplateType.VNF : (isRouting ? TemplateType.ROUTING : TemplateType.USER); + } + if (templateType != null && !isAdmin && !Arrays.asList(TemplateType.USER, TemplateType.VNF).contains(templateType)) { + if (cmd instanceof RegisterTemplateCmd) { + throw new InvalidParameterValueException(String.format("Users can not register template with template type %s.", templateType)); + } else if (cmd instanceof UpdateTemplateCmd) { + throw new InvalidParameterValueException(String.format("Users can not update template to template type %s.", templateType)); + } + } + return templateType; + } + void validateDetails(VMTemplateVO template, Map details) { if (MapUtils.isEmpty(details)) { return; @@ -2270,7 +2316,8 @@ public class TemplateManagerImpl extends ManagerBase implements TemplateManager, String msg = String.format("Invalid %s: %s specified. Valid values are: %s", ApiConstants.BOOT_MODE, bootMode, Arrays.toString(ApiConstants.BootMode.values())); s_logger.error(msg); - throw new InvalidParameterValueException(msg); } + throw new InvalidParameterValueException(msg); + } } void verifyTemplateId(Long id) { diff --git a/server/src/main/java/com/cloud/vm/UserVmManagerImpl.java b/server/src/main/java/com/cloud/vm/UserVmManagerImpl.java index 90f1dde96e7..1f8332d6f02 100644 --- a/server/src/main/java/com/cloud/vm/UserVmManagerImpl.java +++ b/server/src/main/java/com/cloud/vm/UserVmManagerImpl.java @@ -69,6 +69,7 @@ import org.apache.cloudstack.api.command.admin.vm.DeployVMCmdByAdmin; import org.apache.cloudstack.api.command.admin.vm.RecoverVMCmd; import org.apache.cloudstack.api.command.user.vm.AddNicToVMCmd; import org.apache.cloudstack.api.command.user.vm.DeployVMCmd; +import org.apache.cloudstack.api.command.user.vm.DeployVnfApplianceCmd; import org.apache.cloudstack.api.command.user.vm.DestroyVMCmd; import org.apache.cloudstack.api.command.user.vm.RebootVMCmd; import org.apache.cloudstack.api.command.user.vm.RemoveNicFromVMCmd; @@ -123,6 +124,7 @@ import org.apache.cloudstack.storage.datastore.db.PrimaryDataStoreDao; import org.apache.cloudstack.storage.datastore.db.StoragePoolVO; import org.apache.cloudstack.storage.datastore.db.TemplateDataStoreDao; import org.apache.cloudstack.storage.datastore.db.TemplateDataStoreVO; +import org.apache.cloudstack.storage.template.VnfTemplateManager; import org.apache.cloudstack.userdata.UserDataManager; import org.apache.cloudstack.utils.bytescale.ByteScaleUtils; import org.apache.cloudstack.utils.security.ParserUtils; @@ -620,6 +622,9 @@ public class UserVmManagerImpl extends ManagerBase implements UserVmManager, Vir @Inject private UserDataManager userDataManager; + @Inject + VnfTemplateManager vnfTemplateManager; + private static final ConfigKey VmIpFetchWaitInterval = new ConfigKey("Advanced", Integer.class, "externaldhcp.vmip.retrieval.interval", "180", "Wait Interval (in seconds) for shared network vm dhcp ip addr fetch for next iteration ", true); @@ -5901,6 +5906,11 @@ public class UserVmManagerImpl extends ManagerBase implements UserVmManager, Vir if (template == null) { throw new InvalidParameterValueException("Unable to use template " + templateId); } + if (TemplateType.VNF.equals(template.getTemplateType())) { + vnfTemplateManager.validateVnfApplianceNics(template, cmd.getNetworkIds()); + } else if (cmd instanceof DeployVnfApplianceCmd) { + throw new InvalidParameterValueException("Can't deploy VNF appliance from a non-VNF template"); + } ServiceOfferingJoinVO svcOffering = serviceOfferingJoinDao.findById(serviceOfferingId); @@ -5982,14 +5992,14 @@ public class UserVmManagerImpl extends ManagerBase implements UserVmManager, Vir if (networkIds != null) { throw new InvalidParameterValueException("Can't specify network Ids in Basic zone"); } else { - vm = createBasicSecurityGroupVirtualMachine(zone, serviceOffering, template, getSecurityGroupIdList(cmd), owner, name, displayName, diskOfferingId, + vm = createBasicSecurityGroupVirtualMachine(zone, serviceOffering, template, getSecurityGroupIdList(cmd, zone, template, owner), owner, name, displayName, diskOfferingId, size , group , cmd.getHypervisor(), cmd.getHttpMethod(), userData, userDataId, userDataDetails, sshKeyPairNames, cmd.getIpToNetworkMap(), addrs, displayVm , keyboard , cmd.getAffinityGroupIdList(), cmd.getDetails(), cmd.getCustomId(), cmd.getDhcpOptionsMap(), dataDiskTemplateToDiskOfferingMap, userVmOVFProperties, dynamicScalingEnabled, overrideDiskOfferingId); } } else { if (zone.isSecurityGroupEnabled()) { - vm = createAdvancedSecurityGroupVirtualMachine(zone, serviceOffering, template, networkIds, getSecurityGroupIdList(cmd), owner, name, + vm = createAdvancedSecurityGroupVirtualMachine(zone, serviceOffering, template, networkIds, getSecurityGroupIdList(cmd, zone, template, owner), owner, name, displayName, diskOfferingId, size, group, cmd.getHypervisor(), cmd.getHttpMethod(), userData, userDataId, userDataDetails, sshKeyPairNames, cmd.getIpToNetworkMap(), addrs, displayVm, keyboard, cmd.getAffinityGroupIdList(), cmd.getDetails(), cmd.getCustomId(), cmd.getDhcpOptionsMap(), dataDiskTemplateToDiskOfferingMap, userVmOVFProperties, dynamicScalingEnabled, overrideDiskOfferingId, null); @@ -6001,6 +6011,9 @@ public class UserVmManagerImpl extends ManagerBase implements UserVmManager, Vir vm = createAdvancedVirtualMachine(zone, serviceOffering, template, networkIds, owner, name, displayName, diskOfferingId, size, group, cmd.getHypervisor(), cmd.getHttpMethod(), userData, userDataId, userDataDetails, sshKeyPairNames, cmd.getIpToNetworkMap(), addrs, displayVm, keyboard, cmd.getAffinityGroupIdList(), cmd.getDetails(), cmd.getCustomId(), cmd.getDhcpOptionsMap(), dataDiskTemplateToDiskOfferingMap, userVmOVFProperties, dynamicScalingEnabled, null, overrideDiskOfferingId); + if (cmd instanceof DeployVnfApplianceCmd) { + vnfTemplateManager.createIsolatedNetworkRulesForVnfAppliance(zone, template, owner, vm, (DeployVnfApplianceCmd) cmd); + } } } @@ -6267,6 +6280,20 @@ public class UserVmManagerImpl extends ManagerBase implements UserVmManager, Vir } } + protected List getSecurityGroupIdList(SecurityGroupAction cmd, DataCenter zone, VirtualMachineTemplate template, Account owner) { + List securityGroupIdList = getSecurityGroupIdList(cmd); + if (cmd instanceof DeployVnfApplianceCmd) { + SecurityGroup securityGroup = vnfTemplateManager.createSecurityGroupForVnfAppliance(zone, template, owner, (DeployVnfApplianceCmd) cmd); + if (securityGroup != null) { + if (securityGroupIdList == null) { + securityGroupIdList = new ArrayList<>(); + } + securityGroupIdList.add(securityGroup.getId()); + } + } + return securityGroupIdList; + } + // this is an opportunity to verify that parameters that came in via the Details Map are OK // for example, minIops and maxIops should either both be specified or neither be specified and, // if specified, minIops should be <= maxIops diff --git a/server/src/main/java/org/apache/cloudstack/storage/template/VnfTemplateManagerImpl.java b/server/src/main/java/org/apache/cloudstack/storage/template/VnfTemplateManagerImpl.java new file mode 100644 index 00000000000..0371be86448 --- /dev/null +++ b/server/src/main/java/org/apache/cloudstack/storage/template/VnfTemplateManagerImpl.java @@ -0,0 +1,372 @@ +// Licensed to the Apache Software Foundation (ASF) under one +// or more contributor license agreements. See the NOTICE file +// distributed with this work for additional information +// regarding copyright ownership. The ASF licenses this file +// to you 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. +package org.apache.cloudstack.storage.template; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.HashMap; +import java.util.HashSet; +import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.stream.Collectors; + +import javax.inject.Inject; + +import com.cloud.dc.DataCenter; +import com.cloud.exception.InsufficientAddressCapacityException; +import com.cloud.exception.InvalidParameterValueException; +import com.cloud.exception.NetworkRuleConflictException; +import com.cloud.exception.ResourceAllocationException; +import com.cloud.exception.ResourceUnavailableException; +import com.cloud.network.IpAddress; +import com.cloud.network.IpAddressManager; +import com.cloud.network.Network; +import com.cloud.network.NetworkModel; +import com.cloud.network.NetworkService; +import com.cloud.network.VNF; +import com.cloud.network.dao.FirewallRulesDao; +import com.cloud.network.dao.NetworkDao; +import com.cloud.network.firewall.FirewallService; +import com.cloud.network.rules.FirewallRule; +import com.cloud.network.rules.FirewallRuleVO; +import com.cloud.network.rules.RulesService; +import com.cloud.network.security.SecurityGroup; +import com.cloud.network.security.SecurityGroupManager; +import com.cloud.network.security.SecurityGroupService; +import com.cloud.network.security.SecurityGroupVO; +import com.cloud.network.security.SecurityRule; +import com.cloud.storage.VnfTemplateDetailVO; +import com.cloud.storage.VnfTemplateNicVO; +import com.cloud.storage.dao.VnfTemplateDetailsDao; +import com.cloud.storage.dao.VnfTemplateNicDao; +import com.cloud.template.VirtualMachineTemplate; +import com.cloud.user.Account; +import com.cloud.uservm.UserVm; +import com.cloud.utils.NumbersUtil; +import com.cloud.utils.component.ManagerBase; +import com.cloud.utils.component.PluggableService; +import com.cloud.utils.db.Transaction; +import com.cloud.utils.db.TransactionCallbackWithExceptionNoReturn; +import com.cloud.utils.db.TransactionStatus; +import com.cloud.utils.exception.CloudRuntimeException; +import com.cloud.utils.net.NetUtils; +import com.cloud.vm.NicVO; +import com.cloud.vm.dao.NicDao; +import org.apache.cloudstack.api.command.admin.template.ListVnfTemplatesCmdByAdmin; +import org.apache.cloudstack.api.command.admin.template.RegisterVnfTemplateCmdByAdmin; +import org.apache.cloudstack.api.command.admin.template.UpdateVnfTemplateCmdByAdmin; +import org.apache.cloudstack.api.command.admin.vm.DeployVnfApplianceCmdByAdmin; +import org.apache.cloudstack.api.command.user.template.DeleteVnfTemplateCmd; +import org.apache.cloudstack.api.command.user.template.ListVnfTemplatesCmd; +import org.apache.cloudstack.api.command.user.template.RegisterVnfTemplateCmd; +import org.apache.cloudstack.api.command.user.template.UpdateVnfTemplateCmd; +import org.apache.cloudstack.api.command.user.vm.DeployVnfApplianceCmd; +import org.apache.cloudstack.framework.config.ConfigKey; +import org.apache.cloudstack.framework.config.Configurable; +import org.apache.commons.collections.CollectionUtils; +import org.apache.commons.collections.MapUtils; +import org.apache.log4j.Logger; + + +public class VnfTemplateManagerImpl extends ManagerBase implements VnfTemplateManager, PluggableService, Configurable { + + static final Logger LOGGER = Logger.getLogger(VnfTemplateManagerImpl.class); + + public static final String VNF_SECURITY_GROUP_NAME = "VNF_SecurityGroup_"; + public static final String ACCESS_METHOD_SEPARATOR = ","; + public static final Integer ACCESS_DEFAULT_SSH_PORT = 22; + public static final Integer ACCESS_DEFAULT_HTTP_PORT = 80; + public static final Integer ACCESS_DEFAULT_HTTPS_PORT = 443; + + @Inject + VnfTemplateDetailsDao vnfTemplateDetailsDao; + @Inject + VnfTemplateNicDao vnfTemplateNicDao; + @Inject + SecurityGroupManager securityGroupManager; + @Inject + SecurityGroupService securityGroupService; + @Inject + NetworkModel networkModel; + @Inject + IpAddressManager ipAddressManager; + @Inject + NicDao nicDao; + @Inject + NetworkDao networkDao; + @Inject + NetworkService networkService; + @Inject + RulesService rulesService; + @Inject + FirewallRulesDao firewallRulesDao; + @Inject + FirewallService firewallService; + + @Override + public List> getCommands() { + final List> cmdList = new ArrayList<>(); + if (!VnfTemplateAndApplianceEnabled.value()) { + return cmdList; + } + cmdList.add(RegisterVnfTemplateCmd.class); + cmdList.add(RegisterVnfTemplateCmdByAdmin.class); + cmdList.add(ListVnfTemplatesCmd.class); + cmdList.add(ListVnfTemplatesCmdByAdmin.class); + cmdList.add(UpdateVnfTemplateCmd.class); + cmdList.add(UpdateVnfTemplateCmdByAdmin.class); + cmdList.add(DeleteVnfTemplateCmd.class); + cmdList.add(DeployVnfApplianceCmd.class); + cmdList.add(DeployVnfApplianceCmdByAdmin.class); + return cmdList; + } + + @Override + public void persistVnfTemplate(long templateId, RegisterVnfTemplateCmd cmd) { + persistVnfTemplateNics(templateId, cmd.getVnfNics()); + persistVnfTemplateDetails(templateId, cmd); + } + + private void persistVnfTemplateNics(long templateId, List nics) { + for (VNF.VnfNic nic : nics) { + VnfTemplateNicVO vnfTemplateNicVO = new VnfTemplateNicVO(templateId, nic.getDeviceId(), nic.getName(), nic.isRequired(), nic.isManagement(), nic.getDescription()); + vnfTemplateNicDao.persist(vnfTemplateNicVO); + } + } + + private void persistVnfTemplateDetails(long templateId, RegisterVnfTemplateCmd cmd) { + persistVnfTemplateDetails(templateId, cmd.getVnfDetails()); + } + + private void persistVnfTemplateDetails(long templateId, Map vnfDetails) { + for (Map.Entry entry: vnfDetails.entrySet()) { + String value = entry.getValue(); + if (VNF.AccessDetail.ACCESS_METHODS.name().equalsIgnoreCase(entry.getKey())) { + value = Arrays.stream(value.split(ACCESS_METHOD_SEPARATOR)).sorted().collect(Collectors.joining(ACCESS_METHOD_SEPARATOR)); + } + vnfTemplateDetailsDao.addDetail(templateId, entry.getKey().toLowerCase(), value, true); + } + } + + @Override + public void updateVnfTemplate(long templateId, UpdateVnfTemplateCmd cmd) { + updateVnfTemplateDetails(templateId, cmd); + updateVnfTemplateNics(templateId, cmd); + } + + private void updateVnfTemplateDetails(long templateId, UpdateVnfTemplateCmd cmd) { + boolean cleanupVnfDetails = cmd.isCleanupVnfDetails(); + if (cleanupVnfDetails) { + vnfTemplateDetailsDao.removeDetails(templateId); + } else if (MapUtils.isNotEmpty(cmd.getVnfDetails())) { + vnfTemplateDetailsDao.removeDetails(templateId); + persistVnfTemplateDetails(templateId, cmd.getVnfDetails()); + } + } + + private void updateVnfTemplateNics(long templateId, UpdateVnfTemplateCmd cmd) { + boolean cleanupVnfNics = cmd.isCleanupVnfNics(); + if (cleanupVnfNics) { + vnfTemplateNicDao.deleteByTemplateId(templateId); + } else if (CollectionUtils.isNotEmpty(cmd.getVnfNics())) { + vnfTemplateNicDao.deleteByTemplateId(templateId); + persistVnfTemplateNics(templateId, cmd.getVnfNics()); + } + } + + @Override + public String getConfigComponentName() { + return VnfTemplateManager.class.getSimpleName(); + } + + @Override + public ConfigKey[] getConfigKeys() { + return new ConfigKey[] { VnfTemplateAndApplianceEnabled }; + } + + @Override + public void validateVnfApplianceNics(VirtualMachineTemplate template, List networkIds) { + if (CollectionUtils.isEmpty(networkIds)) { + throw new InvalidParameterValueException("VNF nics list is empty"); + } + List vnfNics = vnfTemplateNicDao.listByTemplateId(template.getId()); + for (VnfTemplateNicVO vnfNic : vnfNics) { + if (vnfNic.isRequired() && networkIds.size() <= vnfNic.getDeviceId()) { + throw new InvalidParameterValueException("VNF nic is required but not found: " + vnfNic); + } + } + } + + protected Set getOpenPortsForVnfAppliance(VirtualMachineTemplate template) { + Set ports = new HashSet<>(); + VnfTemplateDetailVO accessMethodsDetail = vnfTemplateDetailsDao.findDetail(template.getId(), VNF.AccessDetail.ACCESS_METHODS.name().toLowerCase()); + if (accessMethodsDetail == null || accessMethodsDetail.getValue() == null) { + return ports; + } + String[] accessMethods = accessMethodsDetail.getValue().split(ACCESS_METHOD_SEPARATOR); + for (String accessMethod : accessMethods) { + if (VNF.AccessMethod.SSH_WITH_KEY.toString().equalsIgnoreCase(accessMethod) + || VNF.AccessMethod.SSH_WITH_PASSWORD.toString().equalsIgnoreCase(accessMethod)) { + VnfTemplateDetailVO accessDetail = vnfTemplateDetailsDao.findDetail(template.getId(), VNF.AccessDetail.SSH_PORT.name().toLowerCase()); + if (accessDetail == null) { + ports.add(ACCESS_DEFAULT_SSH_PORT); + } else { + ports.add(NumbersUtil.parseInt(accessDetail.getValue(), ACCESS_DEFAULT_SSH_PORT)); + } + } else if (VNF.AccessMethod.HTTP.toString().equalsIgnoreCase(accessMethod)) { + VnfTemplateDetailVO accessDetail = vnfTemplateDetailsDao.findDetail(template.getId(), VNF.AccessDetail.HTTP_PORT.name().toLowerCase()); + if (accessDetail == null) { + ports.add(ACCESS_DEFAULT_HTTP_PORT); + } else { + ports.add(NumbersUtil.parseInt(accessDetail.getValue(), ACCESS_DEFAULT_HTTP_PORT)); + } + } else if (VNF.AccessMethod.HTTPS.toString().equalsIgnoreCase(accessMethod)) { + VnfTemplateDetailVO accessDetail = vnfTemplateDetailsDao.findDetail(template.getId(), VNF.AccessDetail.HTTPS_PORT.name().toLowerCase()); + if (accessDetail == null) { + ports.add(ACCESS_DEFAULT_HTTPS_PORT); + } else { + ports.add(NumbersUtil.parseInt(accessDetail.getValue(), ACCESS_DEFAULT_HTTPS_PORT)); + } + } + } + return ports; + } + + private Set getDeviceIdsOfVnfManagementNics(VirtualMachineTemplate template) { + Set deviceIds = new HashSet<>(); + for (VnfTemplateNicVO nic : vnfTemplateNicDao.listByTemplateId(template.getId())) { + if (nic.isManagement()) { + deviceIds.add(nic.getDeviceId()); + } + } + return deviceIds; + } + + protected Map getManagementNetworkAndIp(VirtualMachineTemplate template, UserVm vm) { + Map networkAndIpMap = new HashMap<>(); + Set managementDeviceIds = getDeviceIdsOfVnfManagementNics(template); + for (NicVO nic : nicDao.listByVmId(vm.getId())) { + if (managementDeviceIds.contains((long) nic.getDeviceId()) && nic.getIPv4Address() != null) { + Network network = networkDao.findById(nic.getNetworkId()); + if (network == null || !Network.GuestType.Isolated.equals(network.getGuestType())) { + continue; + } + if (!networkModel.areServicesSupportedInNetwork(network.getId(), Network.Service.StaticNat)) { + LOGGER.info(String.format("Network ID: %s does not support static nat, " + + "skipping this network configuration for VNF appliance", network.getUuid())); + continue; + } + if (network.getVpcId() != null) { + LOGGER.info(String.format("Network ID: %s is a VPC tier, " + + "skipping this network configuration for VNF appliance", network.getUuid())); + continue; + } + if (!networkModel.areServicesSupportedInNetwork(network.getId(), Network.Service.Firewall)) { + LOGGER.info(String.format("Network ID: %s does not support firewall, " + + "skipping this network configuration for VNF appliance", network.getUuid())); + continue; + } + networkAndIpMap.put(network, nic.getIPv4Address()); + } + } + return networkAndIpMap; + } + + @Override + public SecurityGroup createSecurityGroupForVnfAppliance(DataCenter zone, VirtualMachineTemplate template, Account owner, + DeployVnfApplianceCmd cmd) { + if (zone == null || !zone.isSecurityGroupEnabled()) { + return null; + } + if (!cmd.getVnfConfigureManagement()) { + return null; + } + LOGGER.debug("Creating security group and rules for VNF appliance"); + Set ports = getOpenPortsForVnfAppliance(template); + if (ports.size() == 0) { + LOGGER.debug("No need to create security group and rules for VNF appliance as there is no ports to be open"); + return null; + } + String securityGroupName = VNF_SECURITY_GROUP_NAME.concat(Long.toHexString(System.currentTimeMillis())); + SecurityGroupVO securityGroupVO = securityGroupManager.createSecurityGroup(securityGroupName, + "Security group for VNF appliance", owner.getDomainId(), owner.getId(), owner.getAccountName()); + if (securityGroupVO == null) { + throw new CloudRuntimeException(String.format("Failed to create security group: %s", securityGroupName)); + } + List cidrList = cmd.getVnfCidrlist(); + for (Integer port : ports) { + securityGroupService.authorizeSecurityGroupRule(securityGroupVO.getId(), NetUtils.TCP_PROTO, port, port, + null, null, cidrList, null, SecurityRule.SecurityRuleType.IngressRule); + } + return securityGroupVO; + } + + @Override + public void createIsolatedNetworkRulesForVnfAppliance(DataCenter zone, VirtualMachineTemplate template, Account owner, + UserVm vm, DeployVnfApplianceCmd cmd) + throws InsufficientAddressCapacityException, ResourceAllocationException, ResourceUnavailableException { + + Map networkAndIpMap = getManagementNetworkAndIp(template, vm); + Set ports = getOpenPortsForVnfAppliance(template); + for (Map.Entry entry : networkAndIpMap.entrySet()) { + Network network = entry.getKey(); + LOGGER.debug("Creating network rules for VNF appliance on isolated network " + network.getUuid()); + String ip = entry.getValue(); + IpAddress publicIp = networkService.allocateIP(owner, zone.getId(), network.getId(), null, null); + if (publicIp == null) { + continue; + } + publicIp = ipAddressManager.associateIPToGuestNetwork(publicIp.getId(), network.getId(), false); + if (publicIp.isSourceNat()) { + // If isolated network is not implemented, the first acquired Public IP will be Source NAT IP + publicIp = networkService.allocateIP(owner, zone.getId(), network.getId(), null, null); + if (publicIp == null) { + continue; + } + publicIp = ipAddressManager.associateIPToGuestNetwork(publicIp.getId(), network.getId(), false); + } + final IpAddress publicIpFinal = publicIp; + final List cidrList = cmd.getVnfCidrlist(); + try { + boolean result = rulesService.enableStaticNat(publicIp.getId(), vm.getId(), network.getId(), ip); + if (!result) { + throw new CloudRuntimeException(String.format("Failed to create static nat for vm: %s", vm.getUuid())); + } + } catch (NetworkRuleConflictException e) { + throw new CloudRuntimeException(String.format("Failed to create static nat for vm %s due to %s", vm.getUuid(), e.getMessage())); + } + if (network.getVpcId() == null) { + Transaction.execute(new TransactionCallbackWithExceptionNoReturn<>() { + @Override + public void doInTransactionWithoutResult(final TransactionStatus status) throws CloudRuntimeException { + for (Integer port : ports) { + FirewallRuleVO newFirewallRule = new FirewallRuleVO(null, publicIpFinal.getId(), port, port, NetUtils.TCP_PROTO, + network.getId(), owner.getAccountId(), owner.getDomainId(), FirewallRule.Purpose.Firewall, + cidrList, null, null, null, FirewallRule.TrafficType.Ingress); + newFirewallRule.setDisplay(true); + newFirewallRule.setState(FirewallRule.State.Add); + firewallRulesDao.persist(newFirewallRule); + } + } + }); + firewallService.applyIngressFwRules(publicIp.getId(), owner); + } + LOGGER.debug("Created network rules for VNF appliance on isolated network " + network.getUuid()); + } + } +} diff --git a/server/src/main/resources/META-INF/cloudstack/core/spring-server-core-managers-context.xml b/server/src/main/resources/META-INF/cloudstack/core/spring-server-core-managers-context.xml index b976ed6a329..3443e2403dd 100644 --- a/server/src/main/resources/META-INF/cloudstack/core/spring-server-core-managers-context.xml +++ b/server/src/main/resources/META-INF/cloudstack/core/spring-server-core-managers-context.xml @@ -356,4 +356,5 @@ + diff --git a/server/src/test/java/com/cloud/api/query/QueryManagerImplTest.java b/server/src/test/java/com/cloud/api/query/QueryManagerImplTest.java index 29361bfeeae..08ba0955f49 100644 --- a/server/src/test/java/com/cloud/api/query/QueryManagerImplTest.java +++ b/server/src/test/java/com/cloud/api/query/QueryManagerImplTest.java @@ -24,7 +24,9 @@ import com.cloud.event.dao.EventJoinDao; import com.cloud.exception.InvalidParameterValueException; import com.cloud.exception.PermissionDeniedException; import com.cloud.network.Network; +import com.cloud.network.VNF; import com.cloud.network.dao.NetworkVO; +import com.cloud.server.ResourceTag; import com.cloud.user.Account; import com.cloud.user.AccountManager; import com.cloud.user.AccountVO; @@ -39,6 +41,8 @@ import com.cloud.vm.VirtualMachine; import org.apache.cloudstack.acl.SecurityChecker; import org.apache.cloudstack.api.ApiCommandResourceType; import org.apache.cloudstack.api.command.user.event.ListEventsCmd; +import org.apache.cloudstack.api.command.user.resource.ListDetailOptionsCmd; +import org.apache.cloudstack.api.response.DetailOptionsResponse; import org.apache.cloudstack.api.response.EventResponse; import org.apache.cloudstack.api.response.ListResponse; import org.apache.cloudstack.context.CallContext; @@ -54,10 +58,13 @@ import org.mockito.Spy; import org.mockito.junit.MockitoJUnitRunner; import java.util.ArrayList; +import java.util.Arrays; import java.util.HashSet; import java.util.List; +import java.util.Map; import java.util.Set; import java.util.UUID; +import java.util.stream.Collectors; import static org.mockito.Mockito.when; @@ -197,6 +204,27 @@ public class QueryManagerImplTest { queryManager.searchForEvents(cmd); } + @Test + public void listVnfDetailOptionsCmd() { + ListDetailOptionsCmd cmd = Mockito.mock(ListDetailOptionsCmd.class); + when(cmd.getResourceType()).thenReturn(ResourceTag.ResourceObjectType.VnfTemplate); + + DetailOptionsResponse response = queryManager.listDetailOptions(cmd); + Map> options = response.getDetails(); + + int expectedLength = VNF.AccessDetail.values().length + VNF.VnfDetail.values().length; + Assert.assertEquals(expectedLength, options.size()); + Set keys = options.keySet(); + for (VNF.AccessDetail detail : VNF.AccessDetail.values()) { + Assert.assertTrue(keys.contains(detail.name().toLowerCase())); + } + for (VNF.VnfDetail detail : VNF.VnfDetail.values()) { + Assert.assertTrue(keys.contains(detail.name().toLowerCase())); + } + List expectedAccessMethods = Arrays.stream(VNF.AccessMethod.values()).map(method -> method.toString()).sorted().collect(Collectors.toList()); + Assert.assertEquals(expectedAccessMethods, options.get(VNF.AccessDetail.ACCESS_METHODS.name().toLowerCase())); + + } @Test public void applyPublicTemplateRestrictionsTestDoesNotApplyRestrictionsWhenCallerIsRootAdmin() { Mockito.when(accountMock.getType()).thenReturn(Account.Type.ADMIN); diff --git a/server/src/test/java/com/cloud/api/query/dao/TemplateJoinDaoImplTest.java b/server/src/test/java/com/cloud/api/query/dao/TemplateJoinDaoImplTest.java index df25eb88332..94d6722a7c5 100755 --- a/server/src/test/java/com/cloud/api/query/dao/TemplateJoinDaoImplTest.java +++ b/server/src/test/java/com/cloud/api/query/dao/TemplateJoinDaoImplTest.java @@ -19,17 +19,25 @@ package com.cloud.api.query.dao; import com.cloud.api.query.vo.TemplateJoinVO; import com.cloud.hypervisor.Hypervisor; import com.cloud.storage.Storage; +import com.cloud.storage.VnfTemplateDetailVO; +import com.cloud.storage.VnfTemplateNicVO; +import com.cloud.storage.dao.VnfTemplateDetailsDao; +import com.cloud.storage.dao.VnfTemplateNicDao; import com.cloud.template.TemplateManager; import com.cloud.user.Account; import org.apache.cloudstack.api.response.TemplateResponse; +import org.apache.cloudstack.api.response.VnfTemplateResponse; import org.junit.Assert; import org.junit.Before; import org.junit.Test; import org.junit.runner.RunWith; import org.mockito.InjectMocks; +import org.mockito.Mock; +import org.mockito.Mockito; import org.mockito.junit.MockitoJUnitRunner; import org.springframework.test.util.ReflectionTestUtils; +import java.util.Arrays; import java.util.Date; import java.util.Map; @@ -39,6 +47,12 @@ public class TemplateJoinDaoImplTest extends GenericDaoBaseWithTagInformationBas @InjectMocks private TemplateJoinDaoImpl _templateJoinDaoImpl; + @Mock + private VnfTemplateNicDao vnfTemplateNicDao; + + @Mock + private VnfTemplateDetailsDao vnfTemplateDetailsDao; + private TemplateJoinVO template = new TemplateJoinVO(); private TemplateResponse templateResponse = new TemplateResponse(); @@ -60,6 +74,8 @@ public class TemplateJoinDaoImplTest extends GenericDaoBaseWithTagInformationBas private String domainName = "ROOT"; private String detailName = "detail_name1"; private String detailValue = "detail_val"; + private Storage.TemplateType templateType = Storage.TemplateType.VNF; + private Long templateId = 101L; @Before public void setup() { @@ -68,7 +84,7 @@ public class TemplateJoinDaoImplTest extends GenericDaoBaseWithTagInformationBas } @Test - public void testUpdateTemplateTagInfo(){ + public void testUpdateTemplateTagInfo() { testUpdateTagInformation(_templateJoinDaoImpl, template, templateResponse); } @@ -89,8 +105,8 @@ public class TemplateJoinDaoImplTest extends GenericDaoBaseWithTagInformationBas Assert.assertEquals(accountName, ReflectionTestUtils.getField(response, "account")); Assert.assertEquals(domainUuid, ReflectionTestUtils.getField(response, "domainId")); Assert.assertEquals(domainName, ReflectionTestUtils.getField(response, "domainName")); - Assert.assertTrue(((Map)ReflectionTestUtils.getField(response, "details")).containsKey(detailName)); - Assert.assertEquals(detailValue, ((Map)ReflectionTestUtils.getField(response, "details")).get(detailName)); + Assert.assertTrue(((Map) ReflectionTestUtils.getField(response, "details")).containsKey(detailName)); + Assert.assertEquals(detailValue, ((Map) ReflectionTestUtils.getField(response, "details")).get(detailName)); } private void populateTemplateJoinVO() { @@ -111,5 +127,27 @@ public class TemplateJoinDaoImplTest extends GenericDaoBaseWithTagInformationBas ReflectionTestUtils.setField(template, "domainName", domainName); ReflectionTestUtils.setField(template, "detailName", detailName); ReflectionTestUtils.setField(template, "detailValue", detailValue); + ReflectionTestUtils.setField(template, "templateType", templateType); + } + + @Test + public void testNewUpdateResponseForVnf() { + ReflectionTestUtils.setField(template, "id", templateId); + ReflectionTestUtils.setField(template, "templateType", templateType); + + VnfTemplateNicVO vnfNic1 = new VnfTemplateNicVO(templateId, 0L, "eth0", true, true, "first"); + VnfTemplateNicVO vnfNic2 = new VnfTemplateNicVO(templateId, 1L, "eth1", true, true, "second"); + Mockito.doReturn(Arrays.asList(vnfNic1, vnfNic2)).when(vnfTemplateNicDao).listByTemplateId(templateId); + + VnfTemplateDetailVO detail1 = new VnfTemplateDetailVO(templateId, "name1", "value1", true); + VnfTemplateDetailVO detail2 = new VnfTemplateDetailVO(templateId, "name2", "value2", true); + VnfTemplateDetailVO detail3 = new VnfTemplateDetailVO(templateId, "name3", "value3", true); + Mockito.doReturn(Arrays.asList(detail1, detail2, detail3)).when(vnfTemplateDetailsDao).listDetails(templateId); + + final TemplateResponse response = _templateJoinDaoImpl.newUpdateResponse(template); + Assert.assertTrue(response instanceof VnfTemplateResponse); + Assert.assertEquals(2, ((VnfTemplateResponse)response).getVnfNics().size()); + Assert.assertEquals(3, ((VnfTemplateResponse)response).getVnfDetails().size()); + } } diff --git a/server/src/test/java/com/cloud/api/query/dao/UserVmJoinDaoImplTest.java b/server/src/test/java/com/cloud/api/query/dao/UserVmJoinDaoImplTest.java index a4f28c9dc7e..320c556fc51 100755 --- a/server/src/test/java/com/cloud/api/query/dao/UserVmJoinDaoImplTest.java +++ b/server/src/test/java/com/cloud/api/query/dao/UserVmJoinDaoImplTest.java @@ -17,23 +17,78 @@ package com.cloud.api.query.dao; import com.cloud.api.query.vo.UserVmJoinVO; +import com.cloud.storage.Storage; +import com.cloud.storage.VnfTemplateDetailVO; +import com.cloud.storage.VnfTemplateNicVO; +import com.cloud.storage.dao.VnfTemplateDetailsDao; +import com.cloud.storage.dao.VnfTemplateNicDao; +import com.cloud.user.Account; +import com.cloud.user.AccountManager; +import com.cloud.user.UserStatisticsVO; +import com.cloud.user.dao.UserDao; +import com.cloud.user.dao.UserStatisticsDao; +import com.cloud.utils.db.SearchBuilder; +import com.cloud.utils.db.SearchCriteria; +import com.cloud.vm.dao.UserVmDetailsDao; +import org.apache.cloudstack.annotation.dao.AnnotationDao; +import org.apache.cloudstack.api.ApiConstants; +import org.apache.cloudstack.api.ResponseObject; import org.apache.cloudstack.api.response.UserVmResponse; import org.junit.After; +import org.junit.Assert; import org.junit.Before; import org.junit.Test; import org.junit.runner.RunWith; import org.mockito.InjectMocks; +import org.mockito.Mock; +import org.mockito.Mockito; import org.mockito.junit.MockitoJUnitRunner; +import java.util.Arrays; +import java.util.EnumSet; + +import static org.mockito.ArgumentMatchers.nullable; + @RunWith(MockitoJUnitRunner.class) public class UserVmJoinDaoImplTest extends GenericDaoBaseWithTagInformationBaseTest { @InjectMocks private UserVmJoinDaoImpl _userVmJoinDaoImpl; + @Mock + private UserDao userDao; + + @Mock + private AnnotationDao annotationDao; + + @Mock + private AccountManager accountMgr; + + @Mock + private UserVmDetailsDao _userVmDetailsDao; + + @Mock + private UserStatisticsDao userStatsDao; + + @Mock + private VnfTemplateNicDao vnfTemplateNicDao; + + @Mock + private VnfTemplateDetailsDao vnfTemplateDetailsDao; + private UserVmJoinVO userVm = new UserVmJoinVO(); private UserVmResponse userVmResponse = new UserVmResponse(); + @Mock + Account caller; + + @Mock + UserVmJoinVO userVmMock; + + private Long vmId = 100L; + + private Long templateId = 101L; + @Before public void setup() { prepareSetup(); @@ -50,4 +105,51 @@ public class UserVmJoinDaoImplTest extends GenericDaoBaseWithTagInformationBaseT testUpdateTagInformation(_userVmJoinDaoImpl, userVm, userVmResponse); } + private void prepareNewUserVmResponseForVnfAppliance() { + Mockito.when(userVmMock.getId()).thenReturn(vmId); + Mockito.when(userVmMock.getTemplateId()).thenReturn(templateId); + Mockito.when(userVmMock.getTemplateType()).thenReturn(Storage.TemplateType.VNF); + + Mockito.when(caller.getId()).thenReturn(2L); + Mockito.when(accountMgr.isRootAdmin(nullable(Long.class))).thenReturn(true); + + SearchBuilder searchBuilderMock = Mockito.mock(SearchBuilder.class); + Mockito.doReturn(searchBuilderMock).when(userStatsDao).createSearchBuilder(); + UserStatisticsVO userStatisticsVOMock = Mockito.mock(UserStatisticsVO.class); + Mockito.when(searchBuilderMock.entity()).thenReturn(userStatisticsVOMock); + SearchCriteria searchCriteriaMock = Mockito.mock(SearchCriteria.class); + Mockito.doReturn(searchCriteriaMock).when(searchBuilderMock).create(); + Mockito.doReturn(Arrays.asList()).when(userStatsDao).search(searchCriteriaMock, null); + + VnfTemplateNicVO vnfNic1 = new VnfTemplateNicVO(templateId, 0L, "eth0", true, true, "first"); + VnfTemplateNicVO vnfNic2 = new VnfTemplateNicVO(templateId, 1L, "eth1", true, true, "second"); + Mockito.doReturn(Arrays.asList(vnfNic1, vnfNic2)).when(vnfTemplateNicDao).listByTemplateId(templateId); + + VnfTemplateDetailVO detail1 = new VnfTemplateDetailVO(templateId, "name1", "value1", true); + VnfTemplateDetailVO detail2 = new VnfTemplateDetailVO(templateId, "name2", "value2", true); + VnfTemplateDetailVO detail3 = new VnfTemplateDetailVO(templateId, "name3", "value3", true); + Mockito.doReturn(Arrays.asList(detail1, detail2, detail3)).when(vnfTemplateDetailsDao).listDetails(templateId); + } + + @Test + public void testNewUserVmResponseForVnfAppliance() { + prepareNewUserVmResponseForVnfAppliance(); + + UserVmResponse response = _userVmJoinDaoImpl.newUserVmResponse(ResponseObject.ResponseView.Full, "virtualmachine", userVmMock, + EnumSet.of(ApiConstants.VMDetails.all), null, null, caller); + + Assert.assertEquals(2, response.getVnfNics().size()); + Assert.assertEquals(3, response.getVnfDetails().size()); + } + + @Test + public void testNewUserVmResponseForVnfApplianceVnfNics() { + prepareNewUserVmResponseForVnfAppliance(); + + UserVmResponse response = _userVmJoinDaoImpl.newUserVmResponse(ResponseObject.ResponseView.Full, "virtualmachine", userVmMock, + EnumSet.of(ApiConstants.VMDetails.vnfnics), null, null, caller); + + Assert.assertEquals(2, response.getVnfNics().size()); + Assert.assertEquals(3, response.getVnfDetails().size()); + } } diff --git a/server/src/test/java/com/cloud/template/TemplateManagerImplTest.java b/server/src/test/java/com/cloud/template/TemplateManagerImplTest.java index 43c3b3f25c0..cb4d701d8a5 100755 --- a/server/src/test/java/com/cloud/template/TemplateManagerImplTest.java +++ b/server/src/test/java/com/cloud/template/TemplateManagerImplTest.java @@ -45,6 +45,10 @@ import javax.inject.Inject; import org.apache.cloudstack.api.command.user.template.CreateTemplateCmd; import org.apache.cloudstack.api.command.user.template.DeleteTemplateCmd; +import org.apache.cloudstack.api.command.user.template.RegisterTemplateCmd; +import org.apache.cloudstack.api.command.user.template.RegisterVnfTemplateCmd; +import org.apache.cloudstack.api.command.user.template.UpdateTemplateCmd; +import org.apache.cloudstack.api.command.user.template.UpdateVnfTemplateCmd; import org.apache.cloudstack.api.command.user.userdata.LinkUserDataToTemplateCmd; import org.apache.cloudstack.context.CallContext; import org.apache.cloudstack.engine.orchestration.service.VolumeOrchestrationService; @@ -68,6 +72,7 @@ import org.apache.cloudstack.storage.datastore.db.SnapshotDataStoreDao; import org.apache.cloudstack.storage.datastore.db.StoragePoolVO; import org.apache.cloudstack.storage.datastore.db.TemplateDataStoreDao; import org.apache.cloudstack.storage.datastore.db.TemplateDataStoreVO; +import org.apache.cloudstack.storage.template.VnfTemplateManager; import org.apache.cloudstack.test.utils.SpringUtils; import org.junit.After; import org.junit.Assert; @@ -193,6 +198,8 @@ public class TemplateManagerImplTest { @Inject AccountManager _accountMgr; + @Inject + VnfTemplateManager vnfTemplateManager; public class CustomThreadPoolExecutor extends ThreadPoolExecutor { AtomicInteger ai = new AtomicInteger(0); @@ -589,6 +596,109 @@ public class TemplateManagerImplTest { Assert.assertEquals(template, resultTemplate); } + @Test + public void testRegisterTemplateWithTemplateType() { + RegisterTemplateCmd cmd = Mockito.mock(RegisterTemplateCmd.class); + when(cmd.getTemplateType()).thenReturn(Storage.TemplateType.SYSTEM.toString()); + Storage.TemplateType type = templateManager.validateTemplateType(cmd, true, true); + Assert.assertEquals(Storage.TemplateType.SYSTEM, type); + } + + @Test + public void testRegisterTemplateWithoutTemplateType() { + RegisterTemplateCmd cmd = Mockito.mock(RegisterTemplateCmd.class); + Storage.TemplateType type = templateManager.validateTemplateType(cmd, true, true); + Assert.assertEquals(Storage.TemplateType.USER, type); + } + + @Test(expected = InvalidParameterValueException.class) + public void testRegisterTemplateWithSystemTemplateTypeByUser() { + RegisterVnfTemplateCmd cmd = Mockito.mock(RegisterVnfTemplateCmd.class); + when(cmd.getTemplateType()).thenReturn(Storage.TemplateType.SYSTEM.toString()); + Storage.TemplateType type = templateManager.validateTemplateType(cmd, false, true); + } + + @Test(expected = InvalidParameterValueException.class) + public void testRegisterVnfTemplateWithTemplateType() { + RegisterVnfTemplateCmd cmd = Mockito.mock(RegisterVnfTemplateCmd.class); + when(cmd.getTemplateType()).thenReturn(Storage.TemplateType.SYSTEM.toString()); + Storage.TemplateType type = templateManager.validateTemplateType(cmd, false, true); + Assert.assertEquals(Storage.TemplateType.VNF, type); + } + + @Test + public void testRegisterVnfTemplateWithoutTemplateType() { + RegisterVnfTemplateCmd cmd = Mockito.mock(RegisterVnfTemplateCmd.class); + Storage.TemplateType type = templateManager.validateTemplateType(cmd, false, true); + Assert.assertEquals(Storage.TemplateType.VNF, type); + } + + @Test + public void testUpdateTemplateWithTemplateType() { + UpdateTemplateCmd cmd = Mockito.mock(UpdateTemplateCmd.class); + when(cmd.getTemplateType()).thenReturn(Storage.TemplateType.SYSTEM.toString()); + Storage.TemplateType type = templateManager.validateTemplateType(cmd, true, true); + Assert.assertEquals(Storage.TemplateType.SYSTEM, type); + } + + @Test + public void testUpdateTemplateWithoutTemplateType() { + UpdateTemplateCmd cmd = Mockito.mock(UpdateTemplateCmd.class); + Storage.TemplateType type = templateManager.validateTemplateType(cmd, true, true); + Assert.assertNull(type); + } + + @Test(expected = InvalidParameterValueException.class) + public void testUpdateTemplateWithInvalidTemplateType() { + UpdateTemplateCmd cmd = Mockito.mock(UpdateTemplateCmd.class); + when(cmd.getTemplateType()).thenReturn("invalidtype"); + Storage.TemplateType type = templateManager.validateTemplateType(cmd, true, true); + } + + @Test(expected = InvalidParameterValueException.class) + public void testUpdateTemplateWithInvalidTemplateTypeForRouting() { + UpdateTemplateCmd cmd = Mockito.mock(UpdateTemplateCmd.class); + when(cmd.getTemplateType()).thenReturn(Storage.TemplateType.USER.toString()); + when(cmd.isRoutingType()).thenReturn(true); + Storage.TemplateType type = templateManager.validateTemplateType(cmd, true, true); + } + + @Test(expected = InvalidParameterValueException.class) + public void testUpdateTemplateWithInvalidCrossZonesForSystem() { + UpdateTemplateCmd cmd = Mockito.mock(UpdateTemplateCmd.class); + when(cmd.getTemplateType()).thenReturn(Storage.TemplateType.SYSTEM.toString()); + Storage.TemplateType type = templateManager.validateTemplateType(cmd, true, false); + } + + @Test(expected = InvalidParameterValueException.class) + public void testUpdateTemplateWithSystemTemplateTypeByUser() { + UpdateVnfTemplateCmd cmd = Mockito.mock(UpdateVnfTemplateCmd.class); + when(cmd.getTemplateType()).thenReturn(Storage.TemplateType.SYSTEM.toString()); + Storage.TemplateType type = templateManager.validateTemplateType(cmd, false, true); + } + + @Test(expected = InvalidParameterValueException.class) + public void testUpdateVnfTemplateWithTemplateType() { + UpdateVnfTemplateCmd cmd = Mockito.mock(UpdateVnfTemplateCmd.class); + when(cmd.getTemplateType()).thenReturn(Storage.TemplateType.SYSTEM.toString()); + Storage.TemplateType type = templateManager.validateTemplateType(cmd, false, true); + Assert.assertEquals(Storage.TemplateType.VNF, type); + } + + @Test + public void testUpdateVnfTemplateWithoutTemplateType() { + UpdateVnfTemplateCmd cmd = Mockito.mock(UpdateVnfTemplateCmd.class); + Storage.TemplateType type = templateManager.validateTemplateType(cmd, false, true); + Assert.assertNull(type); + } + + @Test + public void testDeleteTemplateWithTemplateType() { + DeleteTemplateCmd cmd = new DeleteTemplateCmd(); + Storage.TemplateType type = templateManager.validateTemplateType(cmd, true, true); + Assert.assertNull(type); + } + @Configuration @ComponentScan(basePackageClasses = {TemplateManagerImpl.class}, includeFilters = {@ComponentScan.Filter(value = TestConfiguration.Library.class, type = FilterType.CUSTOM)}, @@ -790,6 +900,11 @@ public class TemplateManagerImplTest { return Mockito.mock(HypervisorGuruManager.class); } + @Bean + public VnfTemplateManager vnfTemplateManager() { + return Mockito.mock(VnfTemplateManager.class); + } + @Bean public SnapshotHelper snapshotHelper() { return Mockito.mock(SnapshotHelper.class); diff --git a/server/src/test/java/com/cloud/vm/UserVmManagerImplTest.java b/server/src/test/java/com/cloud/vm/UserVmManagerImplTest.java index 7886e70920c..7acd347d7fe 100644 --- a/server/src/test/java/com/cloud/vm/UserVmManagerImplTest.java +++ b/server/src/test/java/com/cloud/vm/UserVmManagerImplTest.java @@ -16,6 +16,8 @@ // under the License. package com.cloud.vm; +import com.cloud.api.query.dao.ServiceOfferingJoinDao; +import com.cloud.api.query.vo.ServiceOfferingJoinVO; import com.cloud.configuration.Resource; import com.cloud.dc.DataCenter; import com.cloud.dc.DataCenterVO; @@ -37,6 +39,7 @@ import com.cloud.hypervisor.Hypervisor; import com.cloud.network.NetworkModel; import com.cloud.network.dao.NetworkDao; import com.cloud.network.dao.NetworkVO; +import com.cloud.network.security.SecurityGroupVO; import com.cloud.offering.ServiceOffering; import com.cloud.server.ManagementService; import com.cloud.service.ServiceOfferingVO; @@ -44,6 +47,7 @@ import com.cloud.service.dao.ServiceOfferingDao; import com.cloud.storage.DiskOfferingVO; import com.cloud.storage.GuestOSVO; import com.cloud.storage.ScopeType; +import com.cloud.storage.Storage; import com.cloud.storage.VMTemplateVO; import com.cloud.storage.Volume; import com.cloud.storage.VolumeApiService; @@ -72,6 +76,7 @@ import com.cloud.vm.dao.UserVmDao; import com.cloud.vm.dao.UserVmDetailsDao; import org.apache.cloudstack.api.BaseCmd.HTTPMethod; import org.apache.cloudstack.api.command.user.vm.DeployVMCmd; +import org.apache.cloudstack.api.command.user.vm.DeployVnfApplianceCmd; import org.apache.cloudstack.api.command.user.vm.ResetVMUserDataCmd; import org.apache.cloudstack.api.command.user.vm.UpdateVMCmd; import org.apache.cloudstack.api.command.user.volume.ResizeVolumeCmd; @@ -79,6 +84,7 @@ import org.apache.cloudstack.context.CallContext; import org.apache.cloudstack.engine.orchestration.service.NetworkOrchestrationService; import org.apache.cloudstack.storage.datastore.db.PrimaryDataStoreDao; import org.apache.cloudstack.storage.datastore.db.StoragePoolVO; +import org.apache.cloudstack.storage.template.VnfTemplateManager; import org.apache.cloudstack.userdata.UserDataManager; import org.junit.After; import org.junit.Assert; @@ -229,10 +235,20 @@ public class UserVmManagerImplTest { @Mock VirtualMachineProfile virtualMachineProfile; + @Mock + VirtualMachineTemplate templateMock; + + @Mock + VnfTemplateManager vnfTemplateManager; + + @Mock + ServiceOfferingJoinDao serviceOfferingJoinDao; + private static final long vmId = 1l; private static final long zoneId = 2L; private static final long accountId = 3L; private static final long serviceOfferingId = 10L; + private static final long templateId = 11L; private static final long GiB_TO_BYTES = 1024 * 1024 * 1024; @@ -941,6 +957,43 @@ public class UserVmManagerImplTest { userVmManagerImpl.createVirtualMachine(deployVMCmd); } + @Test + public void createVirtualMachine() throws ResourceUnavailableException, InsufficientCapacityException, ResourceAllocationException { + DeployVMCmd deployVMCmd = new DeployVMCmd(); + ReflectionTestUtils.setField(deployVMCmd, "zoneId", zoneId); + ReflectionTestUtils.setField(deployVMCmd, "templateId", templateId); + ReflectionTestUtils.setField(deployVMCmd, "serviceOfferingId", serviceOfferingId); + deployVMCmd._accountService = accountService; + + when(accountService.finalyzeAccountId(nullable(String.class), nullable(Long.class), nullable(Long.class), eq(true))).thenReturn(accountId); + when(accountService.getActiveAccountById(accountId)).thenReturn(account); + when(entityManager.findById(DataCenter.class, zoneId)).thenReturn(_dcMock); + when(entityManager.findById(ServiceOffering.class, serviceOfferingId)).thenReturn(serviceOffering); + when(serviceOffering.getState()).thenReturn(ServiceOffering.State.Active); + + when(entityManager.findById(VirtualMachineTemplate.class, templateId)).thenReturn(templateMock); + when(templateMock.getTemplateType()).thenReturn(Storage.TemplateType.VNF); + when(templateMock.isDeployAsIs()).thenReturn(false); + when(templateMock.getFormat()).thenReturn(Storage.ImageFormat.QCOW2); + when(templateMock.getUserDataId()).thenReturn(null); + Mockito.doNothing().when(vnfTemplateManager).validateVnfApplianceNics(any(), nullable(List.class)); + + ServiceOfferingJoinVO svcOfferingMock = Mockito.mock(ServiceOfferingJoinVO.class); + when(serviceOfferingJoinDao.findById(anyLong())).thenReturn(svcOfferingMock); + when(_dcMock.isLocalStorageEnabled()).thenReturn(true); + when(_dcMock.getNetworkType()).thenReturn(DataCenter.NetworkType.Basic); + Mockito.doReturn(userVmVoMock).when(userVmManagerImpl).createBasicSecurityGroupVirtualMachine(any(), any(), any(), any(), any(), any(), any(), + any(), any(), any(), any(), any(), any(), any(), any(), any(), any(), any(), nullable(Boolean.class), any(), any(), any(), + any(), any(), any(), any(), eq(true), any()); + + UserVm result = userVmManagerImpl.createVirtualMachine(deployVMCmd); + assertEquals(userVmVoMock, result); + Mockito.verify(vnfTemplateManager).validateVnfApplianceNics(templateMock, null); + Mockito.verify(userVmManagerImpl).createBasicSecurityGroupVirtualMachine(any(), any(), any(), any(), any(), any(), any(), + any(), any(), any(), any(), any(), any(), any(), any(), any(), any(), any(), nullable(Boolean.class), any(), any(), any(), + any(), any(), any(), any(), eq(true), any()); + } + private List mockVolumesForIsAnyVmVolumeUsingLocalStorageTest(int localVolumes, int nonLocalVolumes) { List volumes = new ArrayList<>(); for (int i=0; i< localVolumes + nonLocalVolumes; ++i) { @@ -1076,6 +1129,24 @@ public class UserVmManagerImplTest { Mockito.verify(userVmDao).update(vmId, userVmVoMock); } + @Test + public void testGetSecurityGroupIdList() { + DeployVnfApplianceCmd cmd = Mockito.mock(DeployVnfApplianceCmd.class); + Mockito.doReturn(new ArrayList()).when(userVmManagerImpl).getSecurityGroupIdList(cmd); + SecurityGroupVO securityGroupVO = Mockito.mock(SecurityGroupVO.class); + long securityGroupId = 100L; + when(securityGroupVO.getId()).thenReturn(securityGroupId); + Mockito.doReturn(securityGroupVO).when(vnfTemplateManager).createSecurityGroupForVnfAppliance(any(), any(), any(), any(DeployVnfApplianceCmd.class)); + + List securityGroupIds = userVmManagerImpl.getSecurityGroupIdList(cmd, null, null, null); + + Assert.assertEquals(1, securityGroupIds.size()); + Assert.assertEquals(securityGroupId, securityGroupIds.get(0).longValue()); + + Mockito.verify(userVmManagerImpl).getSecurityGroupIdList(cmd); + Mockito.verify(vnfTemplateManager).createSecurityGroupForVnfAppliance(any(), any(), any(), any(DeployVnfApplianceCmd.class)); + } + @Test public void getCurrentVmPasswordOrDefineNewPasswordTestTemplateIsNotPasswordEnabledReturnPreDefinedString() { String expected = "saved_password"; diff --git a/server/src/test/java/org/apache/cloudstack/storage/template/VnfTemplateManagerImplTest.java b/server/src/test/java/org/apache/cloudstack/storage/template/VnfTemplateManagerImplTest.java new file mode 100644 index 00000000000..c3fa0d62604 --- /dev/null +++ b/server/src/test/java/org/apache/cloudstack/storage/template/VnfTemplateManagerImplTest.java @@ -0,0 +1,389 @@ +// Licensed to the Apache Software Foundation (ASF) under one +// or more contributor license agreements. See the NOTICE file +// distributed with this work for additional information +// regarding copyright ownership. The ASF licenses this file +// to you 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. +package org.apache.cloudstack.storage.template; + +import com.cloud.dc.DataCenter; +import com.cloud.exception.InsufficientAddressCapacityException; +import com.cloud.exception.InvalidParameterValueException; +import com.cloud.exception.NetworkRuleConflictException; +import com.cloud.exception.ResourceAllocationException; +import com.cloud.exception.ResourceUnavailableException; +import com.cloud.network.IpAddressManager; +import com.cloud.network.Network; +import com.cloud.network.NetworkModel; +import com.cloud.network.NetworkService; +import com.cloud.network.VNF; +import com.cloud.network.dao.FirewallRulesDao; +import com.cloud.network.dao.IPAddressVO; +import com.cloud.network.dao.NetworkDao; +import com.cloud.network.dao.NetworkVO; +import com.cloud.network.firewall.FirewallService; +import com.cloud.network.rules.FirewallRuleVO; +import com.cloud.network.rules.RulesService; +import com.cloud.network.security.SecurityGroup; +import com.cloud.network.security.SecurityGroupManager; +import com.cloud.network.security.SecurityGroupRuleVO; +import com.cloud.network.security.SecurityGroupService; +import com.cloud.network.security.SecurityGroupVO; +import com.cloud.storage.VnfTemplateDetailVO; +import com.cloud.storage.VnfTemplateNicVO; +import com.cloud.storage.dao.VnfTemplateDetailsDao; +import com.cloud.storage.dao.VnfTemplateNicDao; +import com.cloud.template.VirtualMachineTemplate; +import com.cloud.user.Account; +import com.cloud.uservm.UserVm; +import com.cloud.vm.NicVO; +import com.cloud.vm.dao.NicDao; +import org.apache.cloudstack.api.command.user.template.RegisterVnfTemplateCmd; +import org.apache.cloudstack.api.command.user.template.UpdateVnfTemplateCmd; + +import org.apache.cloudstack.api.command.user.vm.DeployVnfApplianceCmd; +import org.junit.Assert; +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.InjectMocks; +import org.mockito.Mock; +import org.mockito.Mockito; +import org.mockito.Spy; +import org.mockito.junit.MockitoJUnitRunner; +import org.springframework.test.util.ReflectionTestUtils; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.HashMap; +import java.util.HashSet; +import java.util.List; +import java.util.Map; +import java.util.Set; + +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.anyInt; +import static org.mockito.ArgumentMatchers.anyLong; +import static org.mockito.ArgumentMatchers.anyString; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.Mockito.when; + +@RunWith(MockitoJUnitRunner.class) +public class VnfTemplateManagerImplTest { + + @Spy + @InjectMocks + VnfTemplateManagerImpl vnfTemplateManagerImpl; + + @Mock + VnfTemplateDetailsDao vnfTemplateDetailsDao; + @Mock + VnfTemplateNicDao vnfTemplateNicDao; + + @Mock + VirtualMachineTemplate template; + + @Mock + NicDao nicDao; + + @Mock + NetworkDao networkDao; + + @Mock + NetworkModel networkModel; + + @Mock + SecurityGroupManager securityGroupManager; + + @Mock + SecurityGroupService securityGroupService; + + @Mock + NetworkService networkService; + + @Mock + IpAddressManager ipAddressManager; + + @Mock + RulesService rulesService; + + @Mock + FirewallRulesDao firewallRulesDao; + + @Mock + FirewallService firewallService; + + final static long templateId = 100L; + final static long vmId = 101L; + final static long networkId = 101L; + final static long securityGroupId = 102L; + final static long zoneId = 103L; + final static long publicIpId = 104L; + final static String ipAddress = "10.10.10.10"; + final static Integer sshPort = 2222; + final static Integer httpPort = 8080; + final static Integer httpsPort = 8443; + final Map vnfNics = new HashMap<>(); + final Map vnfDetails = new HashMap<>(); + + @Before + public void setUp() { + vnfNics.put("0", new HashMap<>(Map.ofEntries( + Map.entry("deviceid", "1"), + Map.entry("name", "eth1"), + Map.entry("required", "true"), + Map.entry("description", "The second NIC of VNF appliance") + ))); + vnfNics.put("1", new HashMap<>(Map.ofEntries( + Map.entry("deviceid", "2"), + Map.entry("name", "eth2"), + Map.entry("required", "false"), + Map.entry("description", "The third NIC of VNF appliance") + ))); + vnfNics.put("2", new HashMap<>(Map.ofEntries( + Map.entry("deviceid", "0"), + Map.entry("name", "eth0"), + Map.entry("description", "The first NIC of VNF appliance") + ))); + + vnfDetails.put("0", new HashMap<>(Map.ofEntries( + Map.entry("accessMethods", "console,http,https"), + Map.entry("username", "admin"), + Map.entry("password", "password"), + Map.entry("version", "4.19.0"), + Map.entry("vendor", "cloudstack") + ))); + + VnfTemplateNicVO vnfNic1 = new VnfTemplateNicVO(templateId, 0L, "eth0", true, true, "first"); + VnfTemplateNicVO vnfNic2 = new VnfTemplateNicVO(templateId, 1L, "eth1", true, true, "second"); + VnfTemplateNicVO vnfNic3 = new VnfTemplateNicVO(templateId, 2L, "eth2", false, true, "third"); + Mockito.doReturn(Arrays.asList(vnfNic1, vnfNic2, vnfNic3)).when(vnfTemplateNicDao).listByTemplateId(templateId); + + when(template.getId()).thenReturn(templateId); + } + + @Test + public void testPersistVnfTemplateRegister() { + RegisterVnfTemplateCmd cmd = new RegisterVnfTemplateCmd(); + ReflectionTestUtils.setField(cmd,"vnfNics", vnfNics); + ReflectionTestUtils.setField(cmd,"vnfDetails", vnfDetails); + + vnfTemplateManagerImpl.persistVnfTemplate(templateId, cmd); + + Mockito.verify(vnfTemplateNicDao, Mockito.times(vnfNics.size())).persist(any(VnfTemplateNicVO.class)); + Mockito.verify(vnfTemplateDetailsDao, Mockito.times(0)).removeDetails(templateId); + Mockito.verify(vnfTemplateDetailsDao, Mockito.times(5)).addDetail(eq(templateId), anyString(), anyString(), eq(true)); + } + + @Test + public void testPersistVnfTemplateUpdate() { + UpdateVnfTemplateCmd cmd = new UpdateVnfTemplateCmd(); + ReflectionTestUtils.setField(cmd,"vnfNics", vnfNics); + ReflectionTestUtils.setField(cmd,"vnfDetails", vnfDetails); + + vnfTemplateManagerImpl.updateVnfTemplate(templateId, cmd); + + Mockito.verify(vnfTemplateNicDao, Mockito.times(vnfNics.size())).persist(any(VnfTemplateNicVO.class)); + Mockito.verify(vnfTemplateDetailsDao, Mockito.times(1)).removeDetails(templateId); + Mockito.verify(vnfTemplateDetailsDao, Mockito.times(5)).addDetail(eq(templateId), anyString(), anyString(), eq(true)); + } + + @Test + public void testPersistVnfTemplateUpdateWithoutNics() { + UpdateVnfTemplateCmd cmd = new UpdateVnfTemplateCmd(); + ReflectionTestUtils.setField(cmd,"vnfDetails", vnfDetails); + ReflectionTestUtils.setField(cmd,"cleanupVnfNics", true); + + vnfTemplateManagerImpl.updateVnfTemplate(templateId, cmd); + + Mockito.verify(vnfTemplateNicDao, Mockito.times(1)).deleteByTemplateId(templateId); + Mockito.verify(vnfTemplateNicDao, Mockito.times(0)).persist(any(VnfTemplateNicVO.class)); + Mockito.verify(vnfTemplateDetailsDao, Mockito.times(1)).removeDetails(templateId); + Mockito.verify(vnfTemplateDetailsDao, Mockito.times(5)).addDetail(eq(templateId), anyString(), anyString(), eq(true)); + } + + @Test + public void testPersistVnfTemplateUpdateWithoutDetails() { + UpdateVnfTemplateCmd cmd = new UpdateVnfTemplateCmd(); + ReflectionTestUtils.setField(cmd,"vnfNics", vnfNics); + ReflectionTestUtils.setField(cmd,"cleanupVnfDetails", true); + + vnfTemplateManagerImpl.updateVnfTemplate(templateId, cmd); + + Mockito.verify(vnfTemplateNicDao, Mockito.times(vnfNics.size())).persist(any(VnfTemplateNicVO.class)); + Mockito.verify(vnfTemplateDetailsDao, Mockito.times(1)).removeDetails(templateId); + Mockito.verify(vnfTemplateDetailsDao, Mockito.times(0)).addDetail(eq(templateId), anyString(), anyString(), eq(true)); + } + + @Test + public void testValidateVnfApplianceNicsWithRequiredNics() { + List networkIds = Arrays.asList(200L, 201L); + vnfTemplateManagerImpl.validateVnfApplianceNics(template, networkIds); + } + + @Test + public void testValidateVnfApplianceNicsWithAllNics() { + List networkIds = Arrays.asList(200L, 201L, 202L); + vnfTemplateManagerImpl.validateVnfApplianceNics(template, networkIds); + } + + @Test(expected = InvalidParameterValueException.class) + public void testValidateVnfApplianceNicsWithEmptyList() { + List networkIds = new ArrayList<>(); + vnfTemplateManagerImpl.validateVnfApplianceNics(template, networkIds); + } + + @Test(expected = InvalidParameterValueException.class) + public void testValidateVnfApplianceNicsWithMissingNetworkId() { + List networkIds = Arrays.asList(200L); + vnfTemplateManagerImpl.validateVnfApplianceNics(template, networkIds); + } + + @Test + public void testGetManagementNetworkAndIp() { + when(template.getId()).thenReturn(templateId); + VnfTemplateNicVO vnfNic1 = new VnfTemplateNicVO(templateId, 0L, "eth0", true, true, "first"); + VnfTemplateNicVO vnfNic2 = new VnfTemplateNicVO(templateId, 1L, "eth1", true, false, "second"); + VnfTemplateNicVO vnfNic3 = new VnfTemplateNicVO(templateId, 2L, "eth2", false, false, "third"); + Mockito.doReturn(Arrays.asList(vnfNic1, vnfNic2, vnfNic3)).when(vnfTemplateNicDao).listByTemplateId(templateId); + + UserVm vm = Mockito.mock(UserVm.class); + when(vm.getId()).thenReturn(vmId); + NicVO nic1 = Mockito.mock(NicVO.class); + NicVO nic2 = Mockito.mock(NicVO.class); + NicVO nic3 = Mockito.mock(NicVO.class); + when(nic1.getDeviceId()).thenReturn(0); + when(nic1.getIPv4Address()).thenReturn(ipAddress); + when(nic1.getNetworkId()).thenReturn(networkId); + when(nic2.getDeviceId()).thenReturn(1); + when(nic3.getDeviceId()).thenReturn(2); + Mockito.doReturn(Arrays.asList(nic1, nic2, nic3)).when(nicDao).listByVmId(vmId); + + NetworkVO network = Mockito.mock(NetworkVO.class); + when(network.getId()).thenReturn(networkId); + when(network.getGuestType()).thenReturn(Network.GuestType.Isolated); + when(network.getVpcId()).thenReturn(null); + Mockito.doReturn(network).when(networkDao).findById(networkId); + when(networkModel.areServicesSupportedInNetwork(networkId, Network.Service.StaticNat)).thenReturn(true); + when(networkModel.areServicesSupportedInNetwork(networkId, Network.Service.Firewall)).thenReturn(true); + + Map networkAndIpMap = vnfTemplateManagerImpl.getManagementNetworkAndIp(template, vm); + + Assert.assertEquals(1, networkAndIpMap.size()); + Assert.assertTrue(networkAndIpMap.containsKey(network)); + Assert.assertTrue(networkAndIpMap.containsValue(ipAddress)); + } + + @Test + public void testGetOpenPortsForVnfAppliance() { + when(template.getId()).thenReturn(templateId); + VnfTemplateDetailVO accessMethodsDetail = Mockito.mock(VnfTemplateDetailVO.class); + when(accessMethodsDetail.getValue()).thenReturn("console,ssh-password,http,https"); + when(vnfTemplateDetailsDao.findDetail(templateId, VNF.AccessDetail.ACCESS_METHODS.name().toLowerCase())).thenReturn(accessMethodsDetail); + + VnfTemplateDetailVO sshPortDetail = Mockito.mock(VnfTemplateDetailVO.class); + when(sshPortDetail.getValue()).thenReturn(String.valueOf(sshPort)); + when(vnfTemplateDetailsDao.findDetail(templateId, VNF.AccessDetail.SSH_PORT.name().toLowerCase())).thenReturn(sshPortDetail); + + VnfTemplateDetailVO httpPortDetail = Mockito.mock(VnfTemplateDetailVO.class); + when(httpPortDetail.getValue()).thenReturn(String.valueOf(httpPort)); + when(vnfTemplateDetailsDao.findDetail(templateId, VNF.AccessDetail.HTTP_PORT.name().toLowerCase())).thenReturn(httpPortDetail); + + VnfTemplateDetailVO httpsPortDetail = Mockito.mock(VnfTemplateDetailVO.class); + when(httpsPortDetail.getValue()).thenReturn(String.valueOf(httpsPort)); + when(vnfTemplateDetailsDao.findDetail(templateId, VNF.AccessDetail.HTTPS_PORT.name().toLowerCase())).thenReturn(httpsPortDetail); + + Set ports = vnfTemplateManagerImpl.getOpenPortsForVnfAppliance(template); + + Assert.assertEquals(3, ports.size()); + Assert.assertTrue(ports.contains(sshPort)); + Assert.assertTrue(ports.contains(httpPort)); + Assert.assertTrue(ports.contains(httpsPort)); + } + + @Test + public void testCreateSecurityGroupForVnfAppliance() { + DataCenter zone = Mockito.mock(DataCenter.class); + when(zone.isSecurityGroupEnabled()).thenReturn(true); + + DeployVnfApplianceCmd cmd = Mockito.mock(DeployVnfApplianceCmd.class); + when(cmd.getVnfConfigureManagement()).thenReturn(true); + when(cmd.getVnfCidrlist()).thenReturn(Arrays.asList("0.0.0.0/0")); + + Set ports = new HashSet<>(); + ports.add(sshPort); + ports.add(httpPort); + ports.add(httpsPort); + Mockito.doReturn(ports).when(vnfTemplateManagerImpl).getOpenPortsForVnfAppliance(template); + + Account owner = Mockito.mock(Account.class); + when(owner.getDomainId()).thenReturn(1L); + when(owner.getAccountName()).thenReturn("admin"); + + SecurityGroupVO securityGroupVO = Mockito.mock(SecurityGroupVO.class); + when(securityGroupVO.getId()).thenReturn(securityGroupId); + Mockito.doReturn(securityGroupVO).when(securityGroupManager).createSecurityGroup(anyString(), anyString(), anyLong(), anyLong(), anyString()); + SecurityGroupRuleVO securityGroupRuleVO = Mockito.mock(SecurityGroupRuleVO.class); + Mockito.doReturn(Arrays.asList(securityGroupRuleVO)).when(securityGroupService).authorizeSecurityGroupRule(anyLong(), anyString(), anyInt(), anyInt(), + any(), any(), any(), any(), any()); + + SecurityGroup result = vnfTemplateManagerImpl.createSecurityGroupForVnfAppliance(zone, template, owner, cmd); + + Assert.assertEquals(result, securityGroupVO); + Mockito.verify(securityGroupService, Mockito.times(3)).authorizeSecurityGroupRule(anyLong(), anyString(), anyInt(), anyInt(), + any(), any(), any(), any(), any()); + } + + @Test + public void testCreateIsolatedNetworkRulesForVnfAppliance() throws InsufficientAddressCapacityException, ResourceUnavailableException, + ResourceAllocationException, NetworkRuleConflictException { + DataCenter zone = Mockito.mock(DataCenter.class); + when(zone.getId()).thenReturn(zoneId); + Account owner = Mockito.mock(Account.class); + UserVm vm = Mockito.mock(UserVm.class); + when(vm.getId()).thenReturn(vmId); + DeployVnfApplianceCmd cmd = Mockito.mock(DeployVnfApplianceCmd.class); + + Map networkAndIpMap = new HashMap<>(); + NetworkVO network = Mockito.mock(NetworkVO.class); + when(network.getId()).thenReturn(networkId); + when(network.getVpcId()).thenReturn(null); + networkAndIpMap.put(network, ipAddress); + Mockito.doReturn(networkAndIpMap).when(vnfTemplateManagerImpl).getManagementNetworkAndIp(template, vm); + + Set ports = new HashSet<>(); + ports.add(sshPort); + ports.add(httpPort); + ports.add(httpsPort); + Mockito.doReturn(ports).when(vnfTemplateManagerImpl).getOpenPortsForVnfAppliance(template); + + FirewallRuleVO firewallRuleVO = Mockito.mock(FirewallRuleVO.class); + + IPAddressVO publicIp = Mockito.mock(IPAddressVO.class); + when(publicIp.getId()).thenReturn(publicIpId); + when(publicIp.isSourceNat()).thenReturn(true).thenReturn(false); + Mockito.doReturn(publicIp).when(networkService).allocateIP(owner, zoneId, networkId, null, null); + Mockito.doReturn(publicIp).when(ipAddressManager).associateIPToGuestNetwork(publicIpId, networkId, false); + Mockito.doReturn(true).when(rulesService).enableStaticNat(publicIpId, vmId, networkId, ipAddress); + when(firewallRulesDao.persist(any())).thenReturn(firewallRuleVO); + Mockito.doReturn(true).when(firewallService).applyIngressFwRules(publicIpId, owner); + + vnfTemplateManagerImpl.createIsolatedNetworkRulesForVnfAppliance(zone, template, owner, vm, cmd); + + Mockito.verify(networkService, Mockito.times(2)).allocateIP(owner, zoneId, networkId, null, null); + Mockito.verify(ipAddressManager, Mockito.times(2)).associateIPToGuestNetwork(publicIpId, networkId, false); + Mockito.verify(rulesService, Mockito.times(1)).enableStaticNat(publicIpId, vmId, networkId, ipAddress); + Mockito.verify(firewallRulesDao, Mockito.times(3)).persist(any()); + Mockito.verify(firewallService, Mockito.times(1)).applyIngressFwRules(publicIpId, owner); + } +} diff --git a/test/integration/smoke/test_vnf_templates.py b/test/integration/smoke/test_vnf_templates.py new file mode 100644 index 00000000000..f963ce41b38 --- /dev/null +++ b/test/integration/smoke/test_vnf_templates.py @@ -0,0 +1,341 @@ +# Licensed to the Apache Software Foundation (ASF) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The ASF licenses this file +# to you 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. + +""" Smoke tests for VNF templates/appliances +""" +from marvin.cloudstackTestCase import cloudstackTestCase +from marvin.lib.base import (Account, + Domain, + Configurations, + ServiceOffering, + VirtualMachine, + Network, + NetworkOffering, + VnfAppliance, + VnfTemplate, + Zone) +from marvin.lib.common import get_zone, get_template +from nose.plugins.attrib import attr + +import time + +VNF_NICS = [{"deviceid": "0", "name": "WAN", "required": "true", "description": "Public WAN"}, + {"deviceid": "1", "name": "LAN-1", "required": "true", "description": "Private LAN-1"}] +NEW_VNF_NICS = [{"deviceid": "0", "name": "WAN", "required": "true", "description": "Public WAN"}, + {"deviceid": "1", "name": "LAN-1", "required": "true", "description": "Private LAN-1"}, + {"deviceid": "2", "name": "LAN-2", "required": "false", "description": "Private LAN-2"}] +VNF_DETAILS = [{"access_methods": "console,https,http", "username": "root"}] +NEW_VNF_DETAILS = [{"access_methods": "console,https,http", "username": "root", "password": "cloudstack"}] + +class TestVnfTemplates(cloudstackTestCase): + + @classmethod + def setUpClass(cls): + + testClient = super(TestVnfTemplates, cls).getClsTestClient() + cls.apiclient = testClient.getApiClient() + cls._cleanup = [] + cls.services = testClient.getParsedTestDataConfig() + + # Get Zone, Domain and templates + cls.hypervisor = cls.testClient.getHypervisorInfo() + zone = get_zone(cls.apiclient, cls.testClient.getZoneForTests()) + cls.zone = Zone(zone.__dict__) + cls.template = get_template(cls.apiclient, cls.zone.id) + + cls.domain = Domain.create( + cls.apiclient, + cls.services["domain"] + ) + cls._cleanup.append(cls.domain) + + cls.account = Account.create( + cls.apiclient, + cls.services["account"], + admin=True, + domainid=cls.domain.id + ) + cls._cleanup.append(cls.account) + + cls.user = cls.account.user[0] + cls.user_apiclient = cls.testClient.getUserApiClient( + cls.user.username, cls.domain.name + ) + + cls.service_offering = ServiceOffering.create( + cls.apiclient, + cls.services["service_offerings"]["big"] + ) + cls._cleanup.append(cls.service_offering) + + cls.vnf_template_config = { + "name": "pfsense", + "displaytext": "pfsense", + "format": cls.template.format, + "url": cls.template.url, + "requireshvm": "True", + "ispublic": "True", + "isextractable": "True", + "hypervisor": cls.hypervisor, + "zoneid": cls.zone.id, + "ostype": "FreeBSD 12 (64-bit)", + "directdownload": False + } + + cls.initial_setting = Configurations.list( + cls.apiclient, + name="vnf.template.appliance.enabled")[0].value + + Configurations.update(cls.apiclient, "vnf.template.appliance.enabled", "true") + + cls.vnf_templates = [] + + @classmethod + def tearDownClass(cls): + Configurations.update(cls.apiclient, "vnf.template.appliance.enabled", cls.initial_setting) + if len(cls.vnf_templates) > 0: + for vnf_template in cls.vnf_templates: + vnf_template.delete(cls.user_apiclient) + super(TestVnfTemplates, cls).tearDownClass() + + def setUp(self): + self.apiclient = self.testClient.getApiClient() + self.dbclient = self.testClient.getDbConnection() + self.cleanup = [] + + def tearDown(self): + super(TestVnfTemplates, self).tearDown() + + def ensureVnfTemplateExists(self): + if len(self.vnf_templates) == 0: + self.vnf_template = VnfTemplate.register(self.user_apiclient, + self.vnf_template_config, + zoneid=self.zone.id, + hypervisor=self.hypervisor, + vnfnics=VNF_NICS, + vnfdetails=VNF_DETAILS) + self.vnf_templates.append(self.vnf_template) + else: + self.vnf_template = self.vnf_templates[0] + + def ensureVnfTemplateDownloaded(self): + """Check if template download will finish in 5 minutes""" + retries = 30 + interval = 10 + while retries > -1: + time.sleep(interval) + templates_response = VnfTemplate.list( + self.user_apiclient, + id=self.vnf_template.id, + zoneid=self.zone.id, + templatefilter='self' + ) + template = templates_response[0] + + if not hasattr(template, 'status') or not template or not template.status: + retries = retries - 1 + continue + + if 'Failed' in template.status: + raise Exception( + "Failed to download template: status - %s" % + template.status) + elif template.status == 'Download Complete' and template.isready: + return + elif 'Downloaded' in template.status: + retries = retries - 1 + continue + elif 'Installing' not in template.status: + if retries >= 0: + retries = retries - 1 + continue + raise Exception( + "Error in downloading template: status - %s" % + template.status) + else: + retries = retries - 1 + raise Exception("Template download failed exception.") + + @attr(tags=["advanced"], required_hardware="false") + def test_01_register_vnf_template(self): + """Test register VNF template + """ + self.ensureVnfTemplateExists() + + @attr(tags=["advanced"], required_hardware="false") + def test_02_list_vnf_template(self): + """Test list VNF template + """ + self.ensureVnfTemplateExists() + + templates_response = VnfTemplate.list( + self.user_apiclient, + id=self.vnf_template.id, + zoneid=self.zone.id, + templatefilter='self' + ) + + if isinstance(templates_response, list) and len(templates_response) > 0: + template = templates_response[0] + self.assertEqual("VNF", template.templatetype, + "The template type of VNF template should be VNF but actually it is %s" % template.templatetype) + self.assertTrue(isinstance(template.vnfnics, list), "The template vnfnics must be a list") + self.assertEqual(2, len(template.vnfnics), "The VNF template should have 2 VNF nics") + self.assertEqual(2, len(template.vnfdetails.__dict__), "The VNF template should have 2 VNF details") + else: + self.fail("Failed to get VNF templates by listVnfTemplates API") + + @attr(tags=["advanced"], required_hardware="false") + def test_03_edit_vnf_template(self): + """Test edit VNF template + """ + self.ensureVnfTemplateExists() + + self.vnf_template.update( + self.user_apiclient, + id=self.vnf_template.id, + vnfnics=NEW_VNF_NICS, + vnfdetails=NEW_VNF_DETAILS + ) + + templates_response = VnfTemplate.list( + self.user_apiclient, + id=self.vnf_template.id, + zoneid=self.zone.id, + templatefilter='self' + ) + + if isinstance(templates_response, list) and len(templates_response) > 0: + template = templates_response[0] + self.assertEqual("VNF", template.templatetype, + "The template type of VNF template should be VNF but actually it is %s" % template.templatetype) + self.assertEqual(3, len(template.vnfnics), "The VNF template should have 2 VNF nics") + self.assertEqual(3, len(template.vnfdetails.__dict__), "The VNF template should have 3 VNF details") + else: + self.fail("Failed to get VNF templates by listVnfTemplates API") + + @attr(tags=["advanced"], required_hardware="false") + def test_04_deploy_vnf_appliance(self): + """Test deploy VNF appliance + """ + self.ensureVnfTemplateExists() + self.ensureVnfTemplateDownloaded() + + templates_response = VnfTemplate.list( + self.user_apiclient, + id=self.vnf_template.id, + zoneid=self.zone.id, + templatefilter='self' + ) + + if isinstance(templates_response, list) and len(templates_response) > 0: + template = templates_response[0] + if not template.isready: + self.fail("VNF template is not Ready") + else: + self.fail("Failed to find VNF template") + + # Create network offerings + self.isolated_network_offering = NetworkOffering.create( + self.apiclient, + self.services["isolated_network_offering"]) + self.cleanup.append(self.isolated_network_offering) + self.isolated_network_offering.update( + self.apiclient, + state='Enabled') + + self.l2_network_offering = NetworkOffering.create( + self.apiclient, + self.services["l2-network_offering"]) + self.cleanup.append(self.l2_network_offering) + self.l2_network_offering.update( + self.apiclient, + state='Enabled') + + # Create networks + isolated_network = Network.create( + self.user_apiclient, + self.services["network"], + networkofferingid=self.isolated_network_offering.id, + zoneid=self.zone.id + ) + self.cleanup.append(isolated_network) + + l2_network_1 = Network.create( + self.user_apiclient, + self.services["l2-network"], + networkofferingid=self.l2_network_offering.id, + zoneid=self.zone.id + ) + self.cleanup.append(l2_network_1) + + l2_network_2 = Network.create( + self.user_apiclient, + self.services["l2-network"], + networkofferingid=self.l2_network_offering.id, + zoneid=self.zone.id + ) + self.cleanup.append(l2_network_2) + + # failed deployment + try: + self.virtual_machine = VirtualMachine.create( + self.user_apiclient, + self.services["virtual_machine"], + zoneid=self.zone.id, + templateid=self.vnf_template.id, + accountid=self.account.name, + domainid=self.account.domainid, + serviceofferingid=self.service_offering.id, + networkids=[isolated_network.id] + ) + self.cleanup.append(self.virtual_machine) + self.fail("The deployment should fail") + except Exception as e: + pass + + # success deployment + self.vnf_appliance = VnfAppliance.create( + self.user_apiclient, + self.services["virtual_machine"], + zoneid=self.zone.id, + templateid=self.vnf_template.id, + accountid=self.account.name, + domainid=self.account.domainid, + serviceofferingid=self.service_offering.id, + networkids=[isolated_network.id, l2_network_1.id, l2_network_2.id], + vnfconfiguremanagement='true' + ) + self.cleanup.append(self.vnf_appliance) + + @attr(tags=["advanced"], required_hardware="false") + def test_05_delete_vnf_template(self): + """Test delete VNF template + """ + self.ensureVnfTemplateExists() + + self.vnf_template.delete(self.user_apiclient) + + templates_response = VnfTemplate.list( + self.user_apiclient, + id=self.vnf_template.id, + zoneid=self.zone.id, + templatefilter='self' + ) + self.assertIsNone(templates_response, "The VNF template should be removed") + + self.vnf_templates.remove(self.vnf_template) diff --git a/tools/apidoc/gen_toc.py b/tools/apidoc/gen_toc.py index bd102e9c7cc..75ff7e1d29a 100644 --- a/tools/apidoc/gen_toc.py +++ b/tools/apidoc/gen_toc.py @@ -50,6 +50,7 @@ known_categories = { 'SystemVm': 'System VM', 'VirtualMachine': 'Virtual Machine', 'VM': 'Virtual Machine', + 'Vnf': 'Virtual Network Functions', 'Domain': 'Domain', 'Template': 'Template', 'Iso': 'ISO', diff --git a/tools/marvin/marvin/lib/base.py b/tools/marvin/marvin/lib/base.py index d57f1a7e552..fd4b39a0ee9 100755 --- a/tools/marvin/marvin/lib/base.py +++ b/tools/marvin/marvin/lib/base.py @@ -6731,3 +6731,315 @@ class VMSchedule: cmd.id = self.id cmd.virtualmachineid = self.virtualmachineid return (apiclient.deleteVMSchedule(cmd)) + +class VnfTemplate: + """Manage VNF template life cycle""" + + def __init__(self, items): + self.__dict__.update(items) + + @classmethod + def register(cls, apiclient, services, zoneid=None, + account=None, domainid=None, hypervisor=None, + projectid=None, details=None, randomize_name=True, + vnfnics=None, vnfdetails=None): + """Create VNF template from URL""" + + # Create template from Virtual machine and Volume ID + cmd = registerVnfTemplate.registerVnfTemplateCmd() + cmd.displaytext = services["displaytext"] + if randomize_name: + cmd.name = "-".join([services["name"], random_gen()]) + else: + cmd.name = services["name"] + cmd.format = services["format"] + if hypervisor: + cmd.hypervisor = hypervisor + elif "hypervisor" in services: + cmd.hypervisor = services["hypervisor"] + + if "ostypeid" in services: + cmd.ostypeid = services["ostypeid"] + elif "ostype" in services: + # Find OSTypeId from Os type + sub_cmd = listOsTypes.listOsTypesCmd() + sub_cmd.description = services["ostype"] + ostypes = apiclient.listOsTypes(sub_cmd) + + if not isinstance(ostypes, list): + raise Exception( + "Unable to find Ostype id with desc: %s" % + services["ostype"]) + cmd.ostypeid = ostypes[0].id + else: + raise Exception( + "Unable to find Ostype is required for registering template") + + cmd.url = services["url"] + + if zoneid: + cmd.zoneid = zoneid + else: + cmd.zoneid = services["zoneid"] + + cmd.isfeatured = services[ + "isfeatured"] if "isfeatured" in services else False + cmd.ispublic = services[ + "ispublic"] if "ispublic" in services else False + cmd.isextractable = services[ + "isextractable"] if "isextractable" in services else False + cmd.isdynamicallyscalable = services["isdynamicallyscalable"] if "isdynamicallyscalable" in services else False + cmd.passwordenabled = services[ + "passwordenabled"] if "passwordenabled" in services else False + cmd.deployasis = services["deployasis"] if "deployasis" in services else False + + if account: + cmd.account = account + + if domainid: + cmd.domainid = domainid + + if projectid: + cmd.projectid = projectid + elif "projectid" in services: + cmd.projectid = services["projectid"] + + if details: + cmd.details = details + + if "directdownload" in services: + cmd.directdownload = services["directdownload"] + + if vnfnics: + cmd.vnfnics = vnfnics + + if vnfdetails: + cmd.vnfdetails = vnfdetails + + # Register Template + template = apiclient.registerVnfTemplate(cmd) + + if isinstance(template, list): + return VnfTemplate(template[0].__dict__) + + def delete(self, apiclient, zoneid=None): + """Delete VNF Template""" + + cmd = deleteVnfTemplate.deleteVnfTemplateCmd() + cmd.id = self.id + if zoneid: + cmd.zoneid = zoneid + apiclient.deleteVnfTemplate(cmd) + + def update(self, apiclient, **kwargs): + """Updates the template details""" + + cmd = updateVnfTemplate.updateVnfTemplateCmd() + cmd.id = self.id + [setattr(cmd, k, v) for k, v in list(kwargs.items())] + return (apiclient.updateVnfTemplate(cmd)) + + @classmethod + def list(cls, apiclient, **kwargs): + """List all templates matching criteria""" + + cmd = listVnfTemplates.listVnfTemplatesCmd() + [setattr(cmd, k, v) for k, v in list(kwargs.items())] + if 'account' in list(kwargs.keys()) and 'domainid' in list(kwargs.keys()): + cmd.listall = True + return (apiclient.listVnfTemplates(cmd)) + +class VnfAppliance: + """Manage VNF Appliance life cycle""" + + def __init__(self, items): + self.__dict__.update(items) + + @classmethod + def create(cls, apiclient, services, templateid=None, accountid=None, + domainid=None, zoneid=None, networkids=None, + serviceofferingid=None, securitygroupids=None, + projectid=None, startvm=None, diskofferingid=None, + affinitygroupnames=None, affinitygroupids=None, group=None, + hostid=None, clusterid=None, keypair=None, ipaddress=None, mode='default', + method='GET', hypervisor=None, customcpunumber=None, + customcpuspeed=None, custommemory=None, rootdisksize=None, + rootdiskcontroller=None, vpcid=None, macaddress=None, datadisktemplate_diskoffering_list={}, + properties=None, nicnetworklist=None, bootmode=None, boottype=None, dynamicscalingenabled=None, + userdataid=None, userdatadetails=None, extraconfig=None, + vnfconfiguremanagement=None, vnfcidrlist=None): + """Create the VNF appliance""" + + cmd = deployVnfAppliance.deployVnfApplianceCmd() + + if serviceofferingid: + cmd.serviceofferingid = serviceofferingid + elif "serviceoffering" in services: + cmd.serviceofferingid = services["serviceoffering"] + + if zoneid: + cmd.zoneid = zoneid + elif "zoneid" in services: + cmd.zoneid = services["zoneid"] + + if hypervisor: + cmd.hypervisor = hypervisor + + if "displayname" in services: + cmd.displayname = services["displayname"] + + if "name" in services: + cmd.name = services["name"] + + if accountid: + cmd.account = accountid + elif "account" in services: + cmd.account = services["account"] + + if domainid: + cmd.domainid = domainid + elif "domainid" in services: + cmd.domainid = services["domainid"] + + if networkids: + cmd.networkids = networkids + allow_egress = False + elif "networkids" in services: + cmd.networkids = services["networkids"] + allow_egress = False + else: + # When no networkids are passed, network + # is created using the "defaultOfferingWithSourceNAT" + # which has an egress policy of DENY. But guests in tests + # need access to test network connectivity + allow_egress = True + + if templateid: + cmd.templateid = templateid + elif "template" in services: + cmd.templateid = services["template"] + + if diskofferingid: + cmd.diskofferingid = diskofferingid + elif "diskoffering" in services: + cmd.diskofferingid = services["diskoffering"] + + if keypair: + cmd.keypair = keypair + elif "keypair" in services: + cmd.keypair = services["keypair"] + + if ipaddress: + cmd.ipaddress = ipaddress + elif "ipaddress" in services: + cmd.ipaddress = services["ipaddress"] + + if securitygroupids: + cmd.securitygroupids = [str(sg_id) for sg_id in securitygroupids] + + if "affinitygroupnames" in services: + cmd.affinitygroupnames = services["affinitygroupnames"] + elif affinitygroupnames: + cmd.affinitygroupnames = affinitygroupnames + + if affinitygroupids: + cmd.affinitygroupids = affinitygroupids + + if projectid: + cmd.projectid = projectid + + if startvm is not None: + cmd.startvm = startvm + + if hostid: + cmd.hostid = hostid + + if clusterid: + cmd.clusterid = clusterid + + if "userdata" in services: + cmd.userdata = base64.urlsafe_b64encode(services["userdata"].encode()).decode() + + if userdataid is not None: + cmd.userdataid = userdataid + + if userdatadetails is not None: + cmd.userdatadetails = userdatadetails + + if "dhcpoptionsnetworklist" in services: + cmd.dhcpoptionsnetworklist = services["dhcpoptionsnetworklist"] + + if dynamicscalingenabled is not None: + cmd.dynamicscalingenabled = dynamicscalingenabled + + cmd.details = [{}] + + if customcpunumber: + cmd.details[0]["cpuNumber"] = customcpunumber + + if customcpuspeed: + cmd.details[0]["cpuSpeed"] = customcpuspeed + + if custommemory: + cmd.details[0]["memory"] = custommemory + + if not rootdisksize is None and rootdisksize >= 0: + cmd.details[0]["rootdisksize"] = rootdisksize + + if rootdiskcontroller: + cmd.details[0]["rootDiskController"] = rootdiskcontroller + + if "size" in services: + cmd.size = services["size"] + + if group: + cmd.group = group + + cmd.datadisktemplatetodiskofferinglist = [] + for datadisktemplate, diskoffering in list(datadisktemplate_diskoffering_list.items()): + cmd.datadisktemplatetodiskofferinglist.append({ + 'datadisktemplateid': datadisktemplate, + 'diskofferingid': diskoffering + }) + + # program default access to ssh + if mode.lower() == 'basic': + cls.ssh_access_group(apiclient, cmd) + + if macaddress: + cmd.macaddress = macaddress + elif macaddress in services: + cmd.macaddress = services["macaddress"] + + if properties: + cmd.properties = properties + + if nicnetworklist: + cmd.nicnetworklist = nicnetworklist + + if bootmode: + cmd.bootmode = bootmode + + if boottype: + cmd.boottype = boottype + + if extraconfig: + cmd.extraconfig = extraconfig + + if vnfconfiguremanagement: + cmd.vnfconfiguremanagement = vnfconfiguremanagement + + if vnfcidrlist: + cmd.vnfcidrlist = vnfcidrlist + + vnf_app = apiclient.deployVnfAppliance(cmd, method=method) + + return VnfAppliance(vnf_app.__dict__) + + def delete(self, apiclient, expunge=True, **kwargs): + """Destroy an VNF appliance""" + cmd = destroyVirtualMachine.destroyVirtualMachineCmd() + cmd.id = self.id + cmd.expunge = expunge + [setattr(cmd, k, v) for k, v in list(kwargs.items())] + apiclient.destroyVirtualMachine(cmd) diff --git a/ui/public/locales/en.json b/ui/public/locales/en.json index a10ed69320a..2118fa2e525 100644 --- a/ui/public/locales/en.json +++ b/ui/public/locales/en.json @@ -372,6 +372,7 @@ "label.back": "Back", "label.backup": "Backups", "label.backup.attach.restore": "Restore and attach backup volume", +"label.backup.configure.schedule": "Configure Backup Schedule", "label.backup.offering.assign": "Assign VM to backup offering", "label.backup.offering.remove": "Remove VM from backup offering", "label.backup.offerings": "Backup offerings", @@ -1151,6 +1152,8 @@ "label.launch": "Launch", "label.launch.vm": "Launch instance", "label.launch.vm.and.stay": "Launch instance & stay on this page", +"label.launch.vnf.appliance": "Launch VNF appliance", +"label.launch.vnf.appliance.and.stay": "Launch VNF appliance & stay on this page", "label.launch.zone": "Launch zone", "label.lb.algorithm.leastconn": "Least connections", "label.lb.algorithm.roundrobin": "Round-robin", @@ -1460,6 +1463,7 @@ "label.parentname": "Parent", "label.passive": "Passive", "label.password": "Password", +"label.password.default": "Default Password", "label.password.reset.confirm": "Password has been reset to ", "label.passwordenabled": "Password enabled", "label.path": "Path", @@ -2191,6 +2195,42 @@ "label.vmwaredcname": "VMware datacenter name", "label.vmwaredcvcenter": "VMware datacenter vCenter", "label.vmwarenetworklabel": "VMware traffic label", +"label.vnf.appliance": "VNF Appliance", +"label.vnf.appliances": "VNF appliances", +"label.vnf.appliance.add": "Add VNF Appliance", +"label.vnf.appliance.access.methods": "Management access information of this VNF appliance", +"label.vnf.app.action.destroy": "Destroy VNF appliance", +"label.vnf.app.action.edit": "Edit VNF appliance", +"label.vnf.app.action.expunge": "Expunge VNF appliance", +"label.vnf.app.action.migrate.to.host": "Migrate VNF appliance to another host", +"label.vnf.app.action.migrate.to.ps": "Migrate VNF appliance to another primary storage", +"label.vnf.app.action.recover": "Recover VNF appliance", +"label.vnf.app.action.scale": "Scale VNF appliance", +"label.vnf.app.action.start": "Start VNF appliance", +"label.vnf.app.action.stop": "Stop VNF appliance", +"label.vnf.app.action.reboot": "Reboot VNF appliance", +"label.vnf.app.action.reinstall": "Reinstall VNF appliance", +"label.vnf.cidr.list": "Source cidr list of rules", +"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 rules for VNF 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.", +"label.vnf.detail.add": "Add VNF detail", +"label.vnf.detail.remove": "Remove VNF detail", +"label.vnf.details": "VNF details", +"label.vnf.nic.add": "Add VNF nic", +"label.vnf.nic.delete": "Delete VNF nic", +"label.vnf.nic.description": "Description of VNF nic", +"label.vnf.nic.deviceid": "Device ID of VNF nic. It starts with 0", +"label.vnf.nic.edit": "Edit VNF nic", +"label.vnf.nic.management": "Management NIC", +"label.vnf.nic.management.description": "True if the VNF nic is a management interface. False otherwise", +"label.vnf.nic.name": "Name of VNF nic", +"label.vnf.nic.remove": "Remove VNF nic", +"label.vnf.nic.required": "True if VNF nic is required. Otherwise optional", +"label.vnf.nics": "VNF nics", +"label.vnf.settings": "VNF settings", +"label.vnf.templates": "VNF templates", +"label.vnf.template.register": "Register VNF template", "label.vnmc": "VNMC", "label.volgroup": "Volume group", "label.volume": "Volume", @@ -3145,6 +3185,17 @@ "message.vm.state.stopped": "VM is stopped.", "message.vm.state.stopping": "VM is being stopped.", "message.vm.state.unknown": "VM state is unknown.", +"message.vnf.appliance.networks": "Please select networks for the new VNF appliance.", +"message.vnf.credentials.change": "Please change the password(s) of the VNF appliance immediately.", +"message.vnf.credentials.default": "The default credentials(s) of the VNF appliance", +"message.vnf.credentials.in.template.vnf.details": "Please find the default credentials for this VNF in the details of the VNF template.", +"message.vnf.error.deviceid.should.be.continuous": "The deviceid of selected VNF nics should be continuous.", +"message.vnf.error.network.is.already.used": "Network has been used by multiple nics of the new VNF appliance.", +"message.vnf.error.no.networks": "Please select networks for nics of the new VNF appliance.", +"message.vnf.error.no.network.for.required.deviceid": "Please select a network for required nic of the new VNF appliance.", +"message.vnf.nic.move.up.fail": "Failed to move up this NIC", +"message.vnf.nic.move.down.fail": "Failed to move down this NIC", +"message.vnf.select.networks": "Please select a network for each VNF nic. ", "message.volume.state.allocated": "The volume is allocated but has not been created yet.", "message.volume.state.attaching": "The volume is attaching to a volume from Ready state.", "message.volume.state.copying": "The volume is being copied from the image store to primary storage, in case it's an uploaded volume.", diff --git a/ui/src/components/view/DetailsTab.vue b/ui/src/components/view/DetailsTab.vue index 301686c3433..a6f77eacdd0 100644 --- a/ui/src/components/view/DetailsTab.vue +++ b/ui/src/components/view/DetailsTab.vue @@ -28,6 +28,11 @@

+ + + @@ -159,11 +164,99 @@ export default { customDisplayItems () { return ['ip6routes', 'privatemtu', 'publicmtu'] }, + vnfAccessMethods () { + if (this.resource.templatetype === 'VNF' && ['vm', 'vnfapp'].includes(this.$route.meta.name)) { + const accessMethodsDescription = [] + const accessMethods = this.resource.vnfdetails?.access_methods || null + const username = this.resource.vnfdetails?.username || null + const password = this.resource.vnfdetails?.password || null + const sshPort = this.resource.vnfdetails?.ssh_port || 22 + const sshUsername = this.resource.vnfdetails?.ssh_user || null + const sshPassword = this.resource.vnfdetails?.ssh_password || null + let httpPath = this.resource.vnfdetails?.http_path || '' + if (!httpPath.startsWith('/')) { + httpPath = '/' + httpPath + } + const httpPort = this.resource.vnfdetails?.http_port || null + let httpsPath = this.resource.vnfdetails?.https_path || '' + if (!httpsPath.startsWith('/')) { + httpsPath = '/' + httpsPath + } + const httpsPort = this.resource.vnfdetails?.https_port || null + const webUsername = this.resource.vnfdetails?.web_user || null + const webPassword = this.resource.vnfdetails?.web_password || null + + const credentials = [] + if (username) { + credentials.push(this.$t('label.username') + ' : ' + username) + } + if (password) { + credentials.push(this.$t('label.password.default') + ' : ' + password) + } + if (webUsername) { + credentials.push('Web ' + this.$t('label.username') + ' : ' + webUsername) + } + if (webPassword) { + credentials.push('Web ' + this.$t('label.password.default') + ' : ' + webPassword) + } + if (sshUsername) { + credentials.push('SSH ' + this.$t('label.username') + ' : ' + sshUsername) + } + if (sshPassword) { + credentials.push('SSH ' + this.$t('label.password.default') + ' : ' + sshPassword) + } + + const managementDeviceIds = [] + for (const vnfnic of this.resource.vnfnics) { + if (vnfnic.management) { + managementDeviceIds.push(vnfnic.deviceid) + } + } + const managementIps = [] + for (const nic of this.resource.nic) { + if (managementDeviceIds.includes(parseInt(nic.deviceid)) && nic.ipaddress) { + managementIps.push(nic.ipaddress) + if (nic.publicip) { + managementIps.push(nic.publicip) + } + } + } + + if (accessMethods) { + const accessMethodsArray = accessMethods.split(',') + for (const accessMethod of accessMethodsArray) { + if (accessMethod === 'console') { + accessMethodsDescription.push('- VM Console.') + } else if (accessMethod === 'ssh-password') { + accessMethodsDescription.push('- SSH with password' + (sshPort ? ' (SSH port is ' + sshPort + ').' : '.')) + } else if (accessMethod === 'ssh-key') { + accessMethodsDescription.push('- SSH with key' + (sshPort ? ' (SSH port is ' + sshPort + ').' : '.')) + } else if (accessMethod === 'http') { + for (const managementIp of managementIps) { + const url = 'http://' + managementIp + (httpPort ? ':' + httpPort : '') + httpPath + accessMethodsDescription.push('- Webpage: ' + url + '') + } + } else if (accessMethod === 'https') { + for (const managementIp of managementIps) { + const url = 'https://' + managementIp + (httpsPort ? ':' + httpsPort : '') + httpsPath + accessMethodsDescription.push('- Webpage: ' + url + '') + } + } + } + } else { + accessMethodsDescription.push('- VM Console.') + } + if (credentials) { + accessMethodsDescription.push('
' + this.$t('message.vnf.credentials.in.template.vnf.details')) + } + return accessMethodsDescription.join('
') + } + return null + }, ipV6Address () { if (this.dataResource.nic && this.dataResource.nic.length > 0) { return this.dataResource.nic.filter(e => { return e.ip6address }).map(e => { return e.ip6address }).join(', ') } - return null }, ip6routes () { diff --git a/ui/src/components/view/ListView.vue b/ui/src/components/view/ListView.vue index b3405d8eda1..6ab6967cc6f 100644 --- a/ui/src/components/view/ListView.vue +++ b/ui/src/components/view/ListView.vue @@ -39,7 +39,7 @@