Extensions Framework & Orchestrate Anything (#9752)

The Extensions Framework in Apache CloudStack is designed to provide a flexible and standardised mechanism for integrating external systems and custom workflows into CloudStack’s orchestration process. By defining structured hook points during key operations—such as virtual machine deployment, resource preparation, and lifecycle events—the framework allows administrators and developers to extend CloudStack’s behaviour without modifying its core codebase.
This commit is contained in:
Harikrishna 2025-07-28 10:41:17 +05:30 committed by GitHub
parent 217ff27650
commit cca8b2fef9
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
259 changed files with 22845 additions and 1187 deletions

View File

@ -89,7 +89,10 @@ jobs:
smoke/test_nested_virtualization
smoke/test_set_sourcenat
smoke/test_webhook_lifecycle
smoke/test_purge_expunged_vms",
smoke/test_purge_expunged_vms
smoke/test_extension_lifecycle
smoke/test_extension_custom_action_lifecycle
smoke/test_extension_custom",
"smoke/test_network
smoke/test_network_acl
smoke/test_network_ipv6

View File

@ -19,9 +19,10 @@ package com.cloud.agent.api;
import java.util.HashMap;
import java.util.Map;
import com.cloud.agent.api.LogLevel.Log4jLevel;
import org.apache.logging.log4j.Logger;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
import com.cloud.agent.api.LogLevel.Log4jLevel;
/**
* implemented by classes that extends the Command class. Command specifies
@ -60,6 +61,7 @@ public abstract class Command {
private int wait; //in second
private boolean bypassHostMaintenance = false;
private transient long requestSequence = 0L;
protected Map<String, Map<String, String>> externalDetails;
protected Command() {
this.wait = 0;
@ -128,6 +130,14 @@ public abstract class Command {
this.requestSequence = requestSequence;
}
public void setExternalDetails(Map<String, Map<String, String>> externalDetails) {
this.externalDetails = externalDetails;
}
public Map<String, Map<String, String>> getExternalDetails() {
return externalDetails;
}
@Override
public boolean equals(Object o) {
if (this == o) return true;

View File

@ -19,12 +19,14 @@ package com.cloud.agent.api.to;
import java.util.List;
import java.util.Map;
import java.util.HashMap;
import java.util.stream.Collectors;
import com.cloud.agent.api.LogLevel;
import com.cloud.network.element.NetworkElement;
import com.cloud.template.VirtualMachineTemplate.BootloaderType;
import com.cloud.vm.VirtualMachine;
import com.cloud.vm.VirtualMachine.Type;
import com.cloud.vm.VmDetailConstants;
public class VirtualMachineTO {
private long id;
@ -496,4 +498,16 @@ public class VirtualMachineTO {
public String toString() {
return String.format("VM {id: \"%s\", name: \"%s\", uuid: \"%s\", type: \"%s\"}", id, name, uuid, type);
}
public Map<String, String> getExternalDetails() {
if (details == null) {
return new HashMap<>();
}
return details.entrySet().stream()
.filter(entry -> entry.getKey().startsWith(VmDetailConstants.EXTERNAL_DETAIL_PREFIX))
.collect(Collectors.toMap(
entry -> entry.getKey().substring(VmDetailConstants.EXTERNAL_DETAIL_PREFIX.length()),
Map.Entry::getValue
));
}
}

View File

@ -29,13 +29,15 @@ import org.apache.cloudstack.api.response.PodResponse;
import org.apache.cloudstack.api.response.ZoneResponse;
import org.apache.cloudstack.config.Configuration;
import org.apache.cloudstack.datacenter.DataCenterIpv4GuestSubnet;
import org.apache.cloudstack.extension.Extension;
import org.apache.cloudstack.extension.ExtensionCustomAction;
import org.apache.cloudstack.ha.HAConfig;
import org.apache.cloudstack.network.BgpPeer;
import org.apache.cloudstack.network.Ipv4GuestSubnetNetworkMap;
import org.apache.cloudstack.quota.QuotaTariff;
import org.apache.cloudstack.storage.sharedfs.SharedFS;
import org.apache.cloudstack.storage.object.Bucket;
import org.apache.cloudstack.storage.object.ObjectStore;
import org.apache.cloudstack.storage.sharedfs.SharedFS;
import org.apache.cloudstack.usage.Usage;
import org.apache.cloudstack.vm.schedule.VMSchedule;
@ -806,6 +808,7 @@ public class EventTypes {
// Management Server
public static final String EVENT_MANAGEMENT_SERVER_REMOVE = "MANAGEMENT.SERVER.REMOVE";
// VM Lease
public static final String VM_LEASE_EXPIRED = "VM.LEASE.EXPIRED";
public static final String VM_LEASE_DISABLED = "VM.LEASE.DISABLED";
public static final String VM_LEASE_CANCELLED = "VM.LEASE.CANCELLED";
@ -816,6 +819,19 @@ public class EventTypes {
public static final String EVENT_GUI_THEME_REMOVE = "GUI.THEME.REMOVE";
public static final String EVENT_GUI_THEME_UPDATE = "GUI.THEME.UPDATE";
// Extension
public static final String EVENT_EXTENSION_CREATE = "EXTENSION.CREATE";
public static final String EVENT_EXTENSION_UPDATE = "EXTENSION.UPDATE";
public static final String EVENT_EXTENSION_DELETE = "EXTENSION.DELETE";
public static final String EVENT_EXTENSION_RESOURCE_REGISTER = "EXTENSION.RESOURCE.REGISTER";
public static final String EVENT_EXTENSION_RESOURCE_UNREGISTER = "EXTENSION.RESOURCE.UNREGISTER";
public static final String EVENT_EXTENSION_CUSTOM_ACTION_ADD = "EXTENSION.CUSTOM.ACTION.ADD";
public static final String EVENT_EXTENSION_CUSTOM_ACTION_UPDATE = "EXTENSION.CUSTOM.ACTION.UPDATE";
public static final String EVENT_EXTENSION_CUSTOM_ACTION_DELETE = "EXTENSION.CUSTOM.ACTION.DELETE";
// Custom Action
public static final String EVENT_CUSTOM_ACTION = "CUSTOM.ACTION";
static {
// TODO: need a way to force author adding event types to declare the entity details as well, with out braking
@ -1324,6 +1340,16 @@ public class EventTypes {
entityEventDetails.put(EVENT_GUI_THEME_CREATE, "GuiTheme");
entityEventDetails.put(EVENT_GUI_THEME_REMOVE, "GuiTheme");
entityEventDetails.put(EVENT_GUI_THEME_UPDATE, "GuiTheme");
// Extension
entityEventDetails.put(EVENT_EXTENSION_CREATE, Extension.class);
entityEventDetails.put(EVENT_EXTENSION_UPDATE, Extension.class);
entityEventDetails.put(EVENT_EXTENSION_DELETE, Extension.class);
entityEventDetails.put(EVENT_EXTENSION_RESOURCE_REGISTER, Extension.class);
entityEventDetails.put(EVENT_EXTENSION_RESOURCE_UNREGISTER, Extension.class);
entityEventDetails.put(EVENT_EXTENSION_CUSTOM_ACTION_ADD, ExtensionCustomAction.class);
entityEventDetails.put(EVENT_EXTENSION_CUSTOM_ACTION_UPDATE, ExtensionCustomAction.class);
entityEventDetails.put(EVENT_EXTENSION_CUSTOM_ACTION_DELETE, ExtensionCustomAction.class);
}
public static boolean isNetworkEvent(String eventType) {

View File

@ -54,6 +54,7 @@ public class Hypervisor {
public static final HypervisorType Ovm3 = new HypervisorType("Ovm3", ImageFormat.RAW);
public static final HypervisorType LXC = new HypervisorType("LXC");
public static final HypervisorType Custom = new HypervisorType("Custom", null, EnumSet.of(RootDiskSizeOverride));
public static final HypervisorType External = new HypervisorType("External", null, EnumSet.of(RootDiskSizeOverride));
public static final HypervisorType Any = new HypervisorType("Any"); /*If you don't care about the hypervisor type*/
private final String name;
private final ImageFormat imageFormat;

View File

@ -305,6 +305,8 @@ public interface NetworkModel {
NicProfile getNicProfile(VirtualMachine vm, long networkId, String broadcastUri);
NicProfile getNicProfile(VirtualMachine vm, Nic nic, DataCenter dataCenter);
Set<Long> getAvailableIps(Network network, String requestedIp);
String getDomainNetworkDomain(long domainId, long zoneId);

View File

@ -19,7 +19,6 @@ package com.cloud.network;
import java.util.List;
import java.util.Map;
import com.cloud.dc.DataCenter;
import org.apache.cloudstack.acl.ControlledEntity;
import org.apache.cloudstack.api.command.admin.address.ReleasePodIpCmdByAdmin;
import org.apache.cloudstack.api.command.admin.network.DedicateGuestVlanRangeCmd;
@ -39,13 +38,16 @@ import org.apache.cloudstack.api.command.user.network.UpdateNetworkCmd;
import org.apache.cloudstack.api.command.user.vm.ListNicsCmd;
import org.apache.cloudstack.api.response.AcquirePodIpCmdResponse;
import org.apache.cloudstack.framework.config.ConfigKey;
import org.apache.cloudstack.network.element.InternalLoadBalancerElementService;
import com.cloud.agent.api.to.NicTO;
import com.cloud.dc.DataCenter;
import com.cloud.exception.ConcurrentOperationException;
import com.cloud.exception.InsufficientAddressCapacityException;
import com.cloud.exception.InsufficientCapacityException;
import com.cloud.exception.InvalidParameterValueException;
import com.cloud.exception.ResourceAllocationException;
import com.cloud.exception.ResourceUnavailableException;
import com.cloud.exception.InvalidParameterValueException;
import com.cloud.network.Network.IpAddresses;
import com.cloud.network.Network.Service;
import com.cloud.network.Networks.TrafficType;
@ -57,7 +59,6 @@ import com.cloud.utils.Pair;
import com.cloud.utils.exception.CloudRuntimeException;
import com.cloud.vm.Nic;
import com.cloud.vm.NicSecondaryIp;
import org.apache.cloudstack.network.element.InternalLoadBalancerElementService;
/**
* The NetworkService interface is the "public" api to entities that make requests to the orchestration engine
@ -270,4 +271,6 @@ public interface NetworkService {
List<InternalLoadBalancerElementService> getInternalLoadBalancerElements();
boolean handleCksIsoOnNetworkVirtualRouter(Long virtualRouterId, boolean mount) throws ResourceUnavailableException;
String getNicVlanValueForExternalVm(NicTO nic);
}

View File

@ -16,9 +16,10 @@
// under the License.
package com.cloud.network.nsx;
import org.apache.cloudstack.framework.config.ConfigKey;
import com.cloud.network.IpAddress;
import com.cloud.network.vpc.Vpc;
import org.apache.cloudstack.framework.config.ConfigKey;
public interface NsxService {
@ -33,4 +34,5 @@ public interface NsxService {
boolean createVpcNetwork(Long zoneId, long accountId, long domainId, Long vpcId, String vpcName, boolean sourceNatEnabled);
boolean updateVpcSourceNatIp(Vpc vpc, IpAddress address);
String getSegmentId(long domainId, long accountId, long zoneId, Long vpcId, long networkId);
}

View File

@ -30,6 +30,7 @@ public class Storage {
OVA(true, true, true, "ova"),
VHDX(true, true, true, "vhdx"),
BAREMETAL(false, false, false, "BAREMETAL"),
EXTERNAL(false, false, false, "EXTERNAL"),
VMDK(true, true, false, "vmdk"),
VDI(true, true, false, "vdi"),
TAR(false, false, false, "tar"),

View File

@ -153,4 +153,6 @@ public interface VirtualMachineTemplate extends ControlledEntity, Identity, Inte
CPU.CPUArch getArch();
Long getExtensionId();
}

View File

@ -114,7 +114,15 @@ public interface VmDetailConstants {
String GUEST_CPU_MODE = "guest.cpu.mode";
String GUEST_CPU_MODEL = "guest.cpu.model";
// Lease related
String INSTANCE_LEASE_EXPIRY_DATE = "leaseexpirydate";
String INSTANCE_LEASE_EXPIRY_ACTION = "leaseexpiryaction";
String INSTANCE_LEASE_EXECUTION = "leaseactionexecution";
// External orchestrator related
String MAC_ADDRESS = "mac_address";
String EXPUNGE_EXTERNAL_VM = "expunge.external.vm";
String EXTERNAL_DETAIL_PREFIX = "External:";
String CLOUDSTACK_VM_DETAILS = "cloudstack.vm.details";
String CLOUDSTACK_VLAN = "cloudstack.vlan";
}

View File

@ -23,8 +23,11 @@ import com.google.common.base.Enums;
import org.apache.logging.log4j.Logger;
import org.apache.logging.log4j.LogManager;
import java.util.Collection;
import java.util.EnumSet;
import java.util.HashMap;
import java.util.Map;
import java.util.Set;
// Enum for default roles in CloudStack
public enum RoleType {
@ -100,6 +103,30 @@ public enum RoleType {
return roleId;
}
public static int toCombinedMask(Collection<RoleType> roles) {
int combinedMask = 0;
if (roles != null) {
for (RoleType role : roles) {
combinedMask |= role.getMask();
}
}
return combinedMask;
}
public static Set<RoleType> fromCombinedMask(int combinedMask) {
Set<RoleType> roles = EnumSet.noneOf(RoleType.class);
for (RoleType roleType : RoleType.values()) {
if ((combinedMask & roleType.getMask()) != 0) {
roles.add(roleType);
}
}
if (roles.isEmpty()) {
roles.add(Unknown);
}
return roles;
}
/**
* This method returns the role account type if the role isn't null, else it returns the default account type.
* */

View File

@ -73,6 +73,7 @@ public interface AlertService {
public static final AlertType ALERT_TYPE_VM_SNAPSHOT = new AlertType((short)32, "ALERT.VM.SNAPSHOT", true);
public static final AlertType ALERT_TYPE_VR_PUBLIC_IFACE_MTU = new AlertType((short)32, "ALERT.VR.PUBLIC.IFACE.MTU", true);
public static final AlertType ALERT_TYPE_VR_PRIVATE_IFACE_MTU = new AlertType((short)32, "ALERT.VR.PRIVATE.IFACE.MTU", true);
public static final AlertType ALERT_TYPE_EXTENSION_PATH_NOT_READY = new AlertType((short)33, "ALERT.TYPE.EXTENSION.PATH.NOT.READY", true);
public short getType() {
return type;

View File

@ -87,7 +87,9 @@ public enum ApiCommandResourceType {
QuotaTariff(org.apache.cloudstack.quota.QuotaTariff.class),
KubernetesCluster(com.cloud.kubernetes.cluster.KubernetesCluster.class),
KubernetesSupportedVersion(null),
SharedFS(org.apache.cloudstack.storage.sharedfs.SharedFS.class);
SharedFS(org.apache.cloudstack.storage.sharedfs.SharedFS.class),
Extension(org.apache.cloudstack.extension.Extension.class),
ExtensionCustomAction(org.apache.cloudstack.extension.ExtensionCustomAction.class);
private final Class<?> clazz;

View File

@ -32,6 +32,7 @@ public class ApiConstants {
public static final String ALLOCATED_DATE = "allocateddate";
public static final String ALLOCATED_ONLY = "allocatedonly";
public static final String ALLOCATED_TIME = "allocated";
public static final String ALLOWED_ROLE_TYPES = "allowedroletypes";
public static final String ALLOW_USER_FORCE_STOP_VM = "allowuserforcestopvm";
public static final String ANNOTATION = "annotation";
public static final String API_KEY = "apikey";
@ -140,6 +141,7 @@ public class ApiConstants {
public static final String CUSTOMIZED = "customized";
public static final String CUSTOMIZED_IOPS = "customizediops";
public static final String CUSTOM_ID = "customid";
public static final String CUSTOM_ACTION_ID = "customactionid";
public static final String CUSTOM_JOB_ID = "customjobid";
public static final String CURRENT_START_IP = "currentstartip";
public static final String CURRENT_END_IP = "currentendip";
@ -164,6 +166,7 @@ public class ApiConstants {
public static final String DISK = "disk";
public static final String DISK_OFFERING_ID = "diskofferingid";
public static final String NEW_DISK_OFFERING_ID = "newdiskofferingid";
public static final String ORCHESTRATOR_REQUIRES_PREPARE_VM = "orchestratorrequirespreparevm";
public static final String OVERRIDE_DISK_OFFERING_ID = "overridediskofferingid";
public static final String DISK_KBS_READ = "diskkbsread";
public static final String DISK_KBS_WRITE = "diskkbswrite";
@ -205,6 +208,7 @@ public class ApiConstants {
public static final String END_IPV6 = "endipv6";
public static final String END_PORT = "endport";
public static final String ENTRY_TIME = "entrytime";
public static final String ERROR_MESSAGE = "errormessage";
public static final String EVENT_ID = "eventid";
public static final String EVENT_TYPE = "eventtype";
public static final String EXPIRES = "expires";
@ -215,6 +219,12 @@ public class ApiConstants {
public static final String EXTRA_DHCP_OPTION_VALUE = "extradhcpvalue";
public static final String EXTERNAL = "external";
public static final String EXTERNAL_UUID = "externaluuid";
public static final String EXTERNAL_DETAILS = "externaldetails";
public static final String PARAMETERS = "parameters";
public static final String EXTENSION = "extension";
public static final String EXTENSION_ID = "extensionid";
public static final String EXTENSION_NAME = "extensionname";
public static final String EXTENSIONS_PATH = "extensionspath";
public static final String FENCE = "fence";
public static final String FETCH_LATEST = "fetchlatest";
public static final String FILESYSTEM = "filesystem";
@ -351,6 +361,7 @@ public class ApiConstants {
public static final String MAX_CPU_NUMBER = "maxcpunumber";
public static final String MAX_MEMORY = "maxmemory";
public static final String MEMORY_OVERCOMMIT_RATIO = "memoryOvercommitRatio";
public static final String MESSAGE = "message";
public static final String MIN_CPU_NUMBER = "mincpunumber";
public static final String MIN_MEMORY = "minmemory";
public static final String MIGRATION_TYPE = "migrationtype";
@ -412,6 +423,7 @@ public class ApiConstants {
public static final String PASSWORD_ENABLED = "passwordenabled";
public static final String SSHKEY_ENABLED = "sshkeyenabled";
public static final String PATH = "path";
public static final String PATH_READY = "pathready";
public static final String PAYLOAD = "payload";
public static final String PAYLOAD_URL = "payloadurl";
public static final String PEERS = "peers";
@ -434,6 +446,7 @@ public class ApiConstants {
public static final String POST_URL = "postURL";
public static final String POWER_STATE = "powerstate";
public static final String PRECEDENCE = "precedence";
public static final String PREPARE_VM = "preparevm";
public static final String PRIVATE_INTERFACE = "privateinterface";
public static final String PRIVATE_IP = "privateip";
public static final String PRIVATE_PORT = "privateport";
@ -457,6 +470,7 @@ public class ApiConstants {
public static final String RECOVER = "recover";
public static final String REPAIR = "repair";
public static final String REQUIRES_HVM = "requireshvm";
public static final String RESOURCES = "resources";
public static final String RESOURCE_COUNT = "resourcecount";
public static final String RESOURCE_NAME = "resourcename";
public static final String RESOURCE_TYPE = "resourcetype";
@ -529,6 +543,7 @@ public class ApiConstants {
public static final String POD_STORAGE_ACCESS_GROUPS = "podstorageaccessgroups";
public static final String ZONE_STORAGE_ACCESS_GROUPS = "zonestorageaccessgroups";
public static final String SUCCESS = "success";
public static final String SUCCESS_MESSAGE = "successmessage";
public static final String SUITABLE_FOR_VM = "suitableforvirtualmachine";
public static final String SUPPORTS_STORAGE_SNAPSHOT = "supportsstoragesnapshot";
public static final String TARGET_IQN = "targetiqn";
@ -570,7 +585,10 @@ public class ApiConstants {
public static final String USE_VIRTUAL_NETWORK = "usevirtualnetwork";
public static final String USE_VIRTUAL_ROUTER_IP_RESOLVER = "userouteripresolver";
public static final String UPDATE_IN_SEQUENCE = "updateinsequence";
public static final String VALIDATION_FORMAT = "validationformat";
public static final String VALUE = "value";
public static final String VALUE_OPTIONS = "valueoptions";
public static final String VIRTUAL_MACHINE = "virtualmachine";
public static final String VIRTUAL_MACHINE_ID = "virtualmachineid";
public static final String VIRTUAL_MACHINE_IDS = "virtualmachineids";
public static final String VIRTUAL_MACHINE_NAME = "virtualmachinename";
@ -669,7 +687,7 @@ public class ApiConstants {
public static final String NETWORK_DEVICE_PARAMETER_LIST = "networkdeviceparameterlist";
public static final String ZONE_TOKEN = "zonetoken";
public static final String DHCP_PROVIDER = "dhcpprovider";
public static final String RESULT = "success";
public static final String RESULT = "result";
public static final String RESUME = "resume";
public static final String LUN_ID = "lunId";
public static final String IQN = "iqn";
@ -1063,6 +1081,7 @@ public class ApiConstants {
public static final String RESOURCE_DETAILS = "resourcedetails";
public static final String RESOURCE_ICON = "icon";
public static final String RESOURCE_MAP = "resourcemap";
public static final String EXPUNGE = "expunge";
public static final String FOR_DISPLAY = "fordisplay";
public static final String PASSIVE = "passive";
@ -1098,6 +1117,7 @@ public class ApiConstants {
public static final String OVM3_CLUSTER = "ovm3cluster";
public static final String OVM3_VIP = "ovm3vip";
public static final String CLEAN_UP_DETAILS = "cleanupdetails";
public static final String CLEAN_UP_PARAMETERS = "cleanupparameters";
public static final String VIRTUAL_SIZE = "virtualsize";
public static final String NETSCALER_CONTROLCENTER_ID = "netscalercontrolcenterid";
public static final String NETSCALER_SERVICEPACKAGE_ID = "netscalerservicepackageid";
@ -1341,6 +1361,10 @@ public class ApiConstants {
all, resource, min;
}
public enum ExtensionDetails {
all, resource, external, min;
}
public enum ApiKeyAccess {
DISABLED(false),
ENABLED(true),

View File

@ -94,6 +94,7 @@ import com.cloud.utils.ReflectUtil;
import com.cloud.utils.db.EntityManager;
import com.cloud.utils.db.UUIDManager;
import com.cloud.vm.UserVmService;
import com.cloud.vm.VmDetailConstants;
import com.cloud.vm.snapshot.VMSnapshotService;
public abstract class BaseCmd {
@ -484,4 +485,14 @@ public abstract class BaseCmd {
}
return detailsMap;
}
public Map<String, String> convertExternalDetailsToMap(Map externalDetails) {
Map<String, String> customparameterMap = convertDetailsToMap(externalDetails);
Map<String, String> details = new HashMap<>();
for (String key : customparameterMap.keySet()) {
String value = customparameterMap.get(key);
details.put(VmDetailConstants.EXTERNAL_DETAIL_PREFIX + key, value);
}
return details;
}
}

View File

@ -19,21 +19,22 @@ package org.apache.cloudstack.api.command.admin.cluster;
import java.util.ArrayList;
import java.util.List;
import com.cloud.cpu.CPU;
import org.apache.cloudstack.api.ApiCommandResourceType;
import java.util.Map;
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.BaseCmd;
import org.apache.cloudstack.api.Parameter;
import org.apache.cloudstack.api.ServerApiException;
import org.apache.cloudstack.api.response.ClusterResponse;
import org.apache.cloudstack.api.response.ExtensionResponse;
import org.apache.cloudstack.api.response.ListResponse;
import org.apache.cloudstack.api.response.PodResponse;
import org.apache.cloudstack.api.response.ZoneResponse;
import com.cloud.cpu.CPU;
import com.cloud.exception.DiscoveryException;
import com.cloud.exception.ResourceInUseException;
import com.cloud.org.Cluster;
@ -43,7 +44,6 @@ import com.cloud.user.Account;
requestHasSensitiveInfo = true, responseHasSensitiveInfo = false)
public class AddClusterCmd extends BaseCmd {
@Parameter(name = ApiConstants.CLUSTER_NAME, type = CommandType.STRING, required = true, description = "the cluster name")
private String clusterName;
@ -65,7 +65,7 @@ public class AddClusterCmd extends BaseCmd {
@Parameter(name = ApiConstants.HYPERVISOR,
type = CommandType.STRING,
required = true,
description = "hypervisor type of the cluster: XenServer,KVM,VMware,Hyperv,BareMetal,Simulator,Ovm3")
description = "hypervisor type of the cluster: XenServer,KVM,VMware,Hyperv,BareMetal,Simulator,Ovm3,External")
private String hypervisor;
@Parameter(name = ApiConstants.ARCH, type = CommandType.STRING,
@ -118,12 +118,26 @@ public class AddClusterCmd extends BaseCmd {
private String ovm3cluster;
@Parameter(name = ApiConstants.OVM3_VIP, type = CommandType.STRING, required = false, description = "Ovm3 vip to use for pool (and cluster)")
private String ovm3vip;
@Parameter(name = ApiConstants.STORAGE_ACCESS_GROUPS,
type = CommandType.LIST, collectionType = CommandType.STRING,
description = "comma separated list of storage access groups for the hosts in the cluster",
since = "4.21.0")
private List<String> storageAccessGroups;
@Parameter(name = ApiConstants.EXTENSION_ID,
type = CommandType.UUID, entityType = ExtensionResponse.class,
description = "UUID of the extension",
since = "4.21.0")
private Long extensionId;
@Parameter(name = ApiConstants.EXTERNAL_DETAILS,
type = CommandType.MAP,
description = "Details in key/value pairs to be added to the extension-resource mapping. Use the format externaldetails[i].<key>=<value>. Example: externaldetails[0].endpoint.url=https://example.com",
since = "4.21.0")
protected Map externalDetails;
public String getOvm3Pool() {
return ovm3pool;
}
@ -190,6 +204,10 @@ public class AddClusterCmd extends BaseCmd {
return hypervisor;
}
public Long getExtensionId() {
return extensionId;
}
public String getClusterType() {
return clusterType;
}
@ -224,6 +242,10 @@ public class AddClusterCmd extends BaseCmd {
return CPU.CPUArch.fromType(arch);
}
public Map<String, String> getExternalDetails() {
return convertDetailsToMap(externalDetails);
}
@Override
public void execute() {
try {

View File

@ -17,7 +17,11 @@
package org.apache.cloudstack.api.command.admin.cluster;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import javax.inject.Inject;
import org.apache.cloudstack.api.APICommand;
import org.apache.cloudstack.api.ApiConstants;
@ -27,9 +31,13 @@ import org.apache.cloudstack.api.response.ClusterResponse;
import org.apache.cloudstack.api.response.ListResponse;
import org.apache.cloudstack.api.response.PodResponse;
import org.apache.cloudstack.api.response.ZoneResponse;
import org.apache.cloudstack.extension.Extension;
import org.apache.cloudstack.extension.ExtensionHelper;
import org.apache.commons.collections.CollectionUtils;
import org.apache.commons.lang3.StringUtils;
import com.cloud.cpu.CPU;
import com.cloud.hypervisor.Hypervisor;
import com.cloud.org.Cluster;
import com.cloud.utils.Pair;
@ -37,6 +45,8 @@ import com.cloud.utils.Pair;
requestHasSensitiveInfo = false, responseHasSensitiveInfo = false)
public class ListClustersCmd extends BaseListCmd {
@Inject
ExtensionHelper extensionHelper;
/////////////////////////////////////////////////////
//////////////// API parameters /////////////////////
@ -143,15 +153,38 @@ public class ListClustersCmd extends BaseListCmd {
/////////////// API Implementation///////////////////
/////////////////////////////////////////////////////
protected void updateClustersExtensions(final List<ClusterResponse> clusterResponses) {
if (CollectionUtils.isEmpty(clusterResponses)) {
return;
}
Map<Long, Extension> idExtensionMap = new HashMap<>();
for (ClusterResponse response : clusterResponses) {
if (!Hypervisor.HypervisorType.External.getHypervisorDisplayName().equals(response.getHypervisorType())) {
continue;
}
Long extensionId = extensionHelper.getExtensionIdForCluster(response.getInternalId());
if (extensionId == null) {
continue;
}
Extension extension = idExtensionMap.computeIfAbsent(extensionId, id -> extensionHelper.getExtension(id));
if (extension == null) {
continue;
}
response.setExtensionId(extension.getUuid());
response.setExtensionName(extension.getName());
}
}
protected Pair<List<ClusterResponse>, Integer> getClusterResponses() {
Pair<List<? extends Cluster>, Integer> result = _mgr.searchForClusters(this);
List<ClusterResponse> clusterResponses = new ArrayList<ClusterResponse>();
List<ClusterResponse> clusterResponses = new ArrayList<>();
for (Cluster cluster : result.first()) {
ClusterResponse clusterResponse = _responseGenerator.createClusterResponse(cluster, showCapacities);
clusterResponse.setObjectName("cluster");
clusterResponses.add(clusterResponse);
}
return new Pair<List<ClusterResponse>, Integer>(clusterResponses, result.second());
updateClustersExtensions(clusterResponses);
return new Pair<>(clusterResponses, result.second());
}
@Override

View File

@ -18,7 +18,7 @@ package org.apache.cloudstack.api.command.admin.host;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import org.apache.cloudstack.api.APICommand;
import org.apache.cloudstack.api.ApiConstants;
@ -81,6 +81,12 @@ public class AddHostCmd extends BaseCmd {
since = "4.21.0")
private List<String> storageAccessGroups;
@Parameter(name = ApiConstants.EXTERNAL_DETAILS,
type = CommandType.MAP,
description = "Details in key/value pairs using format externaldetails[i].keyname=keyvalue. Example: externaldetails[0].endpoint.url=urlvalue",
since = "4.21.0")
protected Map externalDetails;
/////////////////////////////////////////////////////
/////////////////// Accessors ///////////////////////
/////////////////////////////////////////////////////
@ -129,6 +135,10 @@ public class AddHostCmd extends BaseCmd {
return allocationState;
}
public Map<String, String> getExternalDetails() {
return convertExternalDetailsToMap(externalDetails);
}
/////////////////////////////////////////////////////
/////////////// API Implementation///////////////////
/////////////////////////////////////////////////////

View File

@ -16,8 +16,9 @@
// under the License.
package org.apache.cloudstack.api.command.admin.host;
import com.cloud.host.Host;
import com.cloud.user.Account;
import java.util.List;
import java.util.Map;
import org.apache.cloudstack.acl.RoleType;
import org.apache.cloudstack.api.APICommand;
import org.apache.cloudstack.api.ApiConstants;
@ -28,7 +29,8 @@ import org.apache.cloudstack.api.ServerApiException;
import org.apache.cloudstack.api.response.GuestOSCategoryResponse;
import org.apache.cloudstack.api.response.HostResponse;
import java.util.List;
import com.cloud.host.Host;
import com.cloud.user.Account;
@APICommand(name = "updateHost", description = "Updates a host.", responseObject = HostResponse.class,
requestHasSensitiveInfo = false, responseHasSensitiveInfo = false)
@ -67,6 +69,9 @@ public class UpdateHostCmd extends BaseCmd {
@Parameter(name = ApiConstants.ANNOTATION, type = CommandType.STRING, description = "Add an annotation to this host", since = "4.11", authorized = {RoleType.Admin})
private String annotation;
@Parameter(name = ApiConstants.EXTERNAL_DETAILS, type = CommandType.MAP, description = "Details in key/value pairs using format externaldetails[i].keyname=keyvalue. Example: externaldetails[0].endpoint.url=urlvalue", since = "4.21.0")
protected Map externalDetails;
/////////////////////////////////////////////////////
/////////////////// Accessors ///////////////////////
/////////////////////////////////////////////////////
@ -103,6 +108,10 @@ public class UpdateHostCmd extends BaseCmd {
return annotation;
}
public Map<String, String> getExternalDetails() {
return convertExternalDetailsToMap(externalDetails);
}
/////////////////////////////////////////////////////
/////////////// API Implementation///////////////////
/////////////////////////////////////////////////////

View File

@ -29,16 +29,16 @@ import org.apache.cloudstack.api.ApiErrorCode;
import org.apache.cloudstack.api.BaseCmd;
import org.apache.cloudstack.api.Parameter;
import org.apache.cloudstack.api.ServerApiException;
import org.apache.cloudstack.api.response.DiskOfferingResponse;
import org.apache.cloudstack.api.response.DomainResponse;
import org.apache.cloudstack.api.response.ServiceOfferingResponse;
import org.apache.cloudstack.api.response.VsphereStoragePoliciesResponse;
import org.apache.cloudstack.api.response.ZoneResponse;
import org.apache.cloudstack.api.response.DiskOfferingResponse;
import org.apache.cloudstack.vm.lease.VMLeaseManager;
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 org.apache.commons.collections.CollectionUtils;
import com.cloud.exception.InvalidParameterValueException;
import com.cloud.offering.ServiceOffering;
@ -263,6 +263,12 @@ public class CreateServiceOfferingCmd extends BaseCmd {
description = "Lease expiry action, valid values are STOP and DESTROY")
private String leaseExpiryAction;
@Parameter(name = ApiConstants.EXTERNAL_DETAILS,
type = CommandType.MAP,
description = "Details in key/value pairs using format externaldetails[i].keyname=keyvalue. Example: externaldetails[0].endpoint.url=urlvalue",
since = "4.21.0")
private Map externalDetails;
/////////////////////////////////////////////////////
/////////////////// Accessors ///////////////////////
/////////////////////////////////////////////////////
@ -369,9 +375,15 @@ public class CreateServiceOfferingCmd extends BaseCmd {
}
}
}
detailsMap.putAll(getExternalDetails());
return detailsMap;
}
public Map<String, String> getExternalDetails() {
return convertExternalDetailsToMap(externalDetails);
}
public Long getRootDiskSize() {
return rootDiskSize;
}

View File

@ -18,6 +18,7 @@ package org.apache.cloudstack.api.command.admin.offering;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import com.cloud.offering.ServiceOffering.State;
import org.apache.cloudstack.api.APICommand;
@ -94,6 +95,12 @@ public class UpdateServiceOfferingCmd extends BaseCmd {
since="4.20")
private Boolean purgeResources;
@Parameter(name = ApiConstants.EXTERNAL_DETAILS,
type = CommandType.MAP,
description = "Details in key/value pairs using format externaldetails[i].keyname=keyvalue. Example: externaldetails[0].endpoint.url=urlvalue",
since = "4.21.0")
private Map externalDetails;
/////////////////////////////////////////////////////
/////////////////// Accessors ///////////////////////
/////////////////////////////////////////////////////
@ -194,6 +201,10 @@ public class UpdateServiceOfferingCmd extends BaseCmd {
return Boolean.TRUE.equals(purgeResources);
}
public Map<String, String> getExternalDetails() {
return convertExternalDetailsToMap(externalDetails);
}
/////////////////////////////////////////////////////
/////////////// API Implementation///////////////////
/////////////////////////////////////////////////////

View File

@ -16,6 +16,7 @@
// under the License.
package org.apache.cloudstack.api.command.admin.vm;
import com.cloud.hypervisor.Hypervisor;
import org.apache.cloudstack.api.ApiCommandResourceType;
import org.apache.cloudstack.api.APICommand;
@ -145,6 +146,10 @@ public class MigrateVMCmd extends BaseAsyncCmd {
throw new InvalidParameterValueException("Unable to find the VM by id=" + getVirtualMachineId());
}
if (Hypervisor.HypervisorType.External.equals(userVm.getHypervisorType())) {
throw new InvalidParameterValueException("Migrate VM instance operation is not allowed for External hypervisor type");
}
Host destinationHost = null;
// OfflineMigration performed when this parameter is specified
StoragePool destStoragePool = null;

View File

@ -73,6 +73,7 @@ public class ListCapabilitiesCmd extends BaseCmd {
response.setSharedFsVmMinCpuCount((Integer)capabilities.get(ApiConstants.SHAREDFSVM_MIN_CPU_COUNT));
response.setSharedFsVmMinRamSize((Integer)capabilities.get(ApiConstants.SHAREDFSVM_MIN_RAM_SIZE));
response.setInstanceLeaseEnabled((Boolean) capabilities.get(ApiConstants.INSTANCE_LEASE_ENABLED));
response.setExtensionsPath((String)capabilities.get(ApiConstants.EXTENSIONS_PATH));
response.setObjectName("capability");
response.setResponseName(getCommandName());
this.setResponseObject(response);

View File

@ -28,6 +28,7 @@ import org.apache.cloudstack.api.BaseListTaggedResourcesCmd;
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.ExtensionResponse;
import org.apache.cloudstack.api.response.GuestOSCategoryResponse;
import org.apache.cloudstack.api.response.ListResponse;
import org.apache.cloudstack.api.response.TemplateResponse;
@ -115,11 +116,16 @@ public class ListTemplatesCmd extends BaseListTaggedResourcesCmd implements User
since = "4.20")
private String arch;
@Parameter(name = ApiConstants.OS_CATEGORY_ID, type = CommandType.UUID, entityType= GuestOSCategoryResponse.class,
@Parameter(name = ApiConstants.OS_CATEGORY_ID, type = CommandType.UUID, entityType = GuestOSCategoryResponse.class,
description = "the ID of the OS category for the template",
since = "4.21.0")
private Long osCategoryId;
@Parameter(name = ApiConstants.EXTENSION_ID, type = CommandType.UUID, entityType = ExtensionResponse.class,
description = "ID of the extension for the template",
since = "4.21.0")
private Long extensionId;
/////////////////////////////////////////////////////
/////////////////// Accessors ///////////////////////
/////////////////////////////////////////////////////
@ -220,6 +226,10 @@ public class ListTemplatesCmd extends BaseListTaggedResourcesCmd implements User
return osCategoryId;
}
public Long getExtensionId() {
return extensionId;
}
@Override
public String getCommandName() {
return s_name;

View File

@ -16,15 +16,12 @@
// under the License.
package org.apache.cloudstack.api.command.user.template;
import com.cloud.cpu.CPU;
import com.cloud.hypervisor.Hypervisor;
import java.net.URISyntaxException;
import java.util.ArrayList;
import java.util.Collection;
import java.util.List;
import java.util.Map;
import com.cloud.hypervisor.HypervisorGuru;
import org.apache.cloudstack.api.APICommand;
import org.apache.cloudstack.api.ApiCommandResourceType;
import org.apache.cloudstack.api.ApiConstants;
@ -35,6 +32,7 @@ import org.apache.cloudstack.api.ResponseObject.ResponseView;
import org.apache.cloudstack.api.ServerApiException;
import org.apache.cloudstack.api.command.user.UserCmd;
import org.apache.cloudstack.api.response.DomainResponse;
import org.apache.cloudstack.api.response.ExtensionResponse;
import org.apache.cloudstack.api.response.GuestOSResponse;
import org.apache.cloudstack.api.response.ListResponse;
import org.apache.cloudstack.api.response.ProjectResponse;
@ -43,7 +41,10 @@ import org.apache.cloudstack.api.response.ZoneResponse;
import org.apache.cloudstack.context.CallContext;
import org.apache.commons.lang3.StringUtils;
import com.cloud.cpu.CPU;
import com.cloud.exception.ResourceAllocationException;
import com.cloud.hypervisor.Hypervisor;
import com.cloud.hypervisor.HypervisorGuru;
import com.cloud.template.VirtualMachineTemplate;
@APICommand(name = "registerTemplate", description = "Registers an existing template into the CloudStack cloud. ", responseObject = TemplateResponse.class, responseView = ResponseView.Restricted,
@ -183,6 +184,14 @@ public class RegisterTemplateCmd extends BaseCmd implements UserCmd {
since = "4.20")
private String arch;
@Parameter(name = ApiConstants.EXTENSION_ID, type = CommandType.UUID, entityType = ExtensionResponse.class,
description = "ID of the extension",
since = "4.21.0")
private Long extensionId;
@Parameter(name = ApiConstants.EXTERNAL_DETAILS, type = CommandType.MAP, description = "Details in key/value pairs using format externaldetails[i].keyname=keyvalue. Example: externaldetails[0].endpoint.url=urlvalue", since = "4.21.0")
protected Map externalDetails;
/////////////////////////////////////////////////////
/////////////////// Accessors ///////////////////////
/////////////////////////////////////////////////////
@ -312,6 +321,14 @@ public class RegisterTemplateCmd extends BaseCmd implements UserCmd {
return CPU.CPUArch.fromType(arch);
}
public Long getExtensionId() {
return extensionId;
}
public Map<String, String> getExternalDetails() {
return convertExternalDetailsToMap(externalDetails);
}
/////////////////////////////////////////////////////
/////////////// API Implementation///////////////////
/////////////////////////////////////////////////////

View File

@ -16,28 +16,19 @@
// under the License.
package org.apache.cloudstack.api.command.user.vm;
import com.cloud.agent.api.LogLevel;
import com.cloud.event.EventTypes;
import com.cloud.exception.ConcurrentOperationException;
import com.cloud.exception.InsufficientCapacityException;
import com.cloud.exception.InsufficientServerCapacityException;
import com.cloud.exception.InvalidParameterValueException;
import com.cloud.exception.ResourceAllocationException;
import com.cloud.exception.ResourceUnavailableException;
import com.cloud.hypervisor.Hypervisor.HypervisorType;
import com.cloud.network.Network;
import com.cloud.network.Network.IpAddresses;
import com.cloud.offering.DiskOffering;
import com.cloud.template.VirtualMachineTemplate;
import com.cloud.uservm.UserVm;
import com.cloud.utils.exception.CloudRuntimeException;
import com.cloud.utils.net.Dhcp;
import com.cloud.utils.net.NetUtils;
import com.cloud.vm.VirtualMachine;
import com.cloud.vm.VmDetailConstants;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.HashMap;
import java.util.Iterator;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.stream.Stream;
import javax.annotation.Nonnull;
import org.apache.cloudstack.acl.RoleType;
import org.apache.cloudstack.affinity.AffinityGroupResponse;
import org.apache.cloudstack.api.ACL;
@ -73,15 +64,25 @@ import org.apache.commons.lang3.BooleanUtils;
import org.apache.commons.lang3.EnumUtils;
import org.apache.commons.lang3.StringUtils;
import javax.annotation.Nonnull;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.HashMap;
import java.util.Iterator;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import com.cloud.agent.api.LogLevel;
import com.cloud.event.EventTypes;
import com.cloud.exception.ConcurrentOperationException;
import com.cloud.exception.InsufficientCapacityException;
import com.cloud.exception.InsufficientServerCapacityException;
import com.cloud.exception.InvalidParameterValueException;
import com.cloud.exception.ResourceAllocationException;
import com.cloud.exception.ResourceUnavailableException;
import com.cloud.hypervisor.Hypervisor.HypervisorType;
import com.cloud.network.Network;
import com.cloud.network.Network.IpAddresses;
import com.cloud.offering.DiskOffering;
import com.cloud.template.VirtualMachineTemplate;
import com.cloud.uservm.UserVm;
import com.cloud.utils.exception.CloudRuntimeException;
import com.cloud.utils.net.Dhcp;
import com.cloud.utils.net.NetUtils;
import com.cloud.vm.VirtualMachine;
import com.cloud.vm.VmDetailConstants;
@APICommand(name = "deployVirtualMachine", description = "Creates and automatically starts a virtual machine based on a service offering, disk offering, and template.", responseObject = UserVmResponse.class, responseView = ResponseView.Restricted, entityType = {VirtualMachine.class},
requestHasSensitiveInfo = false, responseHasSensitiveInfo = true)
@ -297,6 +298,13 @@ public class DeployVMCmd extends BaseAsyncCreateCustomIdCmd implements SecurityG
@Parameter(name = ApiConstants.SNAPSHOT_ID, type = CommandType.UUID, entityType = SnapshotResponse.class, since = "4.21")
private Long snapshotId;
@Parameter(name = ApiConstants.EXTERNAL_DETAILS,
type = CommandType.MAP,
description = "Details in key/value pairs using format externaldetails[i].keyname=keyvalue. Example: externaldetails[0].server.type=typevalue",
since = "4.21.0")
protected Map externalDetails;
/////////////////////////////////////////////////////
/////////////////// Accessors ///////////////////////
/////////////////////////////////////////////////////
@ -370,9 +378,16 @@ public class DeployVMCmd extends BaseAsyncCreateCustomIdCmd implements SecurityG
customparameterMap.put(VmDetailConstants.NIC_PACKED_VIRTQUEUES_ENABLED, BooleanUtils.toStringTrueFalse(nicPackedVirtQueues));
}
if (MapUtils.isNotEmpty(externalDetails)) {
customparameterMap.putAll(getExternalDetails());
}
return customparameterMap;
}
public Map<String, String> getExternalDetails() {
return convertExternalDetailsToMap(externalDetails);
}
public ApiConstants.BootMode getBootMode() {
if (StringUtils.isNotBlank(bootMode)) {

View File

@ -38,6 +38,7 @@ import org.apache.cloudstack.api.ResponseObject.ResponseView;
import org.apache.cloudstack.api.command.user.UserCmd;
import org.apache.cloudstack.api.response.AutoScaleVmGroupResponse;
import org.apache.cloudstack.api.response.BackupOfferingResponse;
import org.apache.cloudstack.api.response.ExtensionResponse;
import org.apache.cloudstack.api.response.InstanceGroupResponse;
import org.apache.cloudstack.api.response.IsoVmResponse;
import org.apache.cloudstack.api.response.ListResponse;
@ -171,6 +172,11 @@ public class ListVMsCmd extends BaseListRetrieveOnlyResourceCountCmd implements
since = "4.21.0")
private Boolean onlyLeasedInstances = false;
@Parameter(name = ApiConstants.EXTENSION_ID, type = CommandType.UUID,
entityType = ExtensionResponse.class, description = "The ID of the Orchestrator extension for the VM",
since = "4.21.0")
private Long extensionId;
/////////////////////////////////////////////////////
/////////////////// Accessors ///////////////////////
/////////////////////////////////////////////////////
@ -318,6 +324,10 @@ public class ListVMsCmd extends BaseListRetrieveOnlyResourceCountCmd implements
return BooleanUtils.toBoolean(onlyLeasedInstances);
}
public Long getExtensionId() {
return extensionId;
}
/////////////////////////////////////////////////////
/////////////// API Implementation///////////////////
/////////////////////////////////////////////////////

View File

@ -16,6 +16,7 @@
// under the License.
package org.apache.cloudstack.api.response;
import org.apache.cloudstack.acl.RoleType;
import org.apache.cloudstack.api.ApiConstants;
import org.apache.cloudstack.api.BaseResponse;
@ -140,6 +141,10 @@ public class CapabilitiesResponse extends BaseResponse {
@Param(description = "true if instance lease feature is enabled", since = "4.21.0")
private Boolean instanceLeaseEnabled;
@SerializedName(ApiConstants.EXTENSIONS_PATH)
@Param(description = "The path of the extensions directory", since = "4.21.0", authorized = {RoleType.Admin})
private String extensionsPath;
public void setSecurityGroupsEnabled(boolean securityGroupsEnabled) {
this.securityGroupsEnabled = securityGroupsEnabled;
}
@ -255,4 +260,8 @@ public class CapabilitiesResponse extends BaseResponse {
public void setInstanceLeaseEnabled(Boolean instanceLeaseEnabled) {
this.instanceLeaseEnabled = instanceLeaseEnabled;
}
public void setExtensionsPath(String extensionsPath) {
this.extensionsPath = extensionsPath;
}
}

View File

@ -31,6 +31,8 @@ import com.google.gson.annotations.SerializedName;
@EntityReference(value = Cluster.class)
public class ClusterResponse extends BaseResponseWithAnnotations {
private transient long internalId;
@SerializedName(ApiConstants.ID)
@Param(description = "the cluster ID")
private String id;
@ -107,6 +109,22 @@ public class ClusterResponse extends BaseResponseWithAnnotations {
@Param(description = "comma-separated list of storage access groups on the zone", since = "4.21.0")
private String zoneStorageAccessGroups;
@SerializedName(ApiConstants.EXTENSION_ID)
@Param(description="The ID of extension for this cluster", since = "4.21.0")
private String extensionId;
@SerializedName(ApiConstants.EXTENSION_NAME)
@Param(description="The name of extension for this cluster", since = "4.21.0")
private String extensionName;
public void setInternalId(long internalId) {
this.internalId = internalId;
}
public long getInternalId() {
return internalId;
}
public String getId() {
return id;
}
@ -295,4 +313,20 @@ public class ClusterResponse extends BaseResponseWithAnnotations {
public void setZoneStorageAccessGroups(String zoneStorageAccessGroups) {
this.zoneStorageAccessGroups = zoneStorageAccessGroups;
}
public void setExtensionId(String extensionId) {
this.extensionId = extensionId;
}
public String getExtensionId() {
return extensionId;
}
public void setExtensionName(String extensionName) {
this.extensionName = extensionName;
}
public String getExtensionName() {
return extensionName;
}
}

View File

@ -26,7 +26,7 @@ public class CreateConsoleEndpointResponse extends BaseResponse {
public CreateConsoleEndpointResponse() {
}
@SerializedName(ApiConstants.RESULT)
@SerializedName(ApiConstants.SUCCESS)
@Param(description = "true if the console endpoint is generated properly")
private Boolean result;

View File

@ -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 org.apache.cloudstack.api.response;
import java.util.List;
import org.apache.cloudstack.api.ApiConstants;
import org.apache.cloudstack.api.BaseResponse;
import com.cloud.serializer.Param;
import com.google.gson.annotations.SerializedName;
public class ExtensionCustomActionParameterResponse extends BaseResponse {
@SerializedName(ApiConstants.NAME)
@Param(description = "Name of the parameter")
private String name;
@SerializedName(ApiConstants.TYPE)
@Param(description = "Type of the parameter")
private String type;
@SerializedName(ApiConstants.VALIDATION_FORMAT)
@Param(description = "Validation format for value of the parameter. Available for specific types")
private String validationFormat;
@SerializedName(ApiConstants.VALUE_OPTIONS)
@Param(description = "Comma-separated list of options for value of the parameter")
private List<Object> valueOptions;
@SerializedName(ApiConstants.REQUIRED)
@Param(description = "Whether the parameter is required or not")
private Boolean required;
public ExtensionCustomActionParameterResponse(String name, String type, String validationFormat, List<Object> valueOptions,
boolean required) {
this.name = name;
this.type = type;
this.validationFormat = validationFormat;
this.valueOptions = valueOptions;
this.required = required;
}
}

View File

@ -0,0 +1,184 @@
// 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 java.util.Date;
import java.util.List;
import java.util.Map;
import org.apache.cloudstack.api.ApiConstants;
import org.apache.cloudstack.api.BaseResponse;
import org.apache.cloudstack.api.EntityReference;
import org.apache.cloudstack.extension.ExtensionCustomAction;
import com.cloud.serializer.Param;
import com.google.gson.annotations.SerializedName;
@EntityReference(value = ExtensionCustomAction.class)
public class ExtensionCustomActionResponse extends BaseResponse {
@SerializedName(ApiConstants.ID)
@Param(description = "ID of the custom action")
private String id;
@SerializedName(ApiConstants.NAME)
@Param(description = "Name of the custom action")
private String name;
@SerializedName(ApiConstants.DESCRIPTION)
@Param(description = "Description of the custom action")
private String description;
@SerializedName(ApiConstants.EXTENSION_ID)
@Param(description = "ID of the extension that this custom action belongs to")
private String extensionId;
@SerializedName(ApiConstants.EXTENSION_NAME)
@Param(description = "Name of the extension that this custom action belongs to")
private String extensionName;
@SerializedName(ApiConstants.RESOURCE_TYPE)
@Param(description = "Resource type for which the action is available")
private String resourceType;
@SerializedName(ApiConstants.ALLOWED_ROLE_TYPES)
@Param(description = "List of role types allowed for the custom action")
private List<String> allowedRoleTypes;
@SerializedName(ApiConstants.SUCCESS_MESSAGE)
@Param(description = "Message that will be used on successful execution of the action")
private String successMessage;
@SerializedName(ApiConstants.ERROR_MESSAGE)
@Param(description = "Message that will be used on failure during execution of the action")
private String errorMessage;
@SerializedName(ApiConstants.TIMEOUT)
@Param(description = "Specifies the timeout in seconds to wait for the action to complete before failing")
private Integer timeout;
@SerializedName(ApiConstants.ENABLED)
@Param(description = "Whether the custom action is enabled or not")
private Boolean enabled;
@SerializedName(ApiConstants.DETAILS)
@Param(description = "Details of the custom action")
private Map<String, String> details;
@SerializedName(ApiConstants.PARAMETERS)
@Param(description = "List of the parameters for the action", responseObject = ExtensionCustomActionParameterResponse.class)
private List<ExtensionCustomActionParameterResponse> parameters;
@SerializedName(ApiConstants.CREATED)
@Param(description = "Creation timestamp of the custom action")
private Date created;
public ExtensionCustomActionResponse(String id, String name, String description) {
this.id = id;
this.name = name;
this.description = description;
}
public String getId() {
return id;
}
public void setId(String id) {
this.id = id;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public String getDescription() {
return description;
}
public void setDescription(String description) {
this.description = description;
}
public void setExtensionId(String extensionId) {
this.extensionId = extensionId;
}
public void setExtensionName(String extensionName) {
this.extensionName = extensionName;
}
public String getResourceType() {
return resourceType;
}
public void setResourceType(String resourceType) {
this.resourceType = resourceType;
}
public List<String> getAllowedRoleTypes() {
return allowedRoleTypes;
}
public void setAllowedRoleTypes(List<String> allowedRoleTypes) {
this.allowedRoleTypes = allowedRoleTypes;
}
public void setSuccessMessage(String successMessage) {
this.successMessage = successMessage;
}
public void setErrorMessage(String errorMessage) {
this.errorMessage = errorMessage;
}
public void setTimeout(Integer timeout) {
this.timeout = timeout;
}
public void setEnabled(Boolean enabled) {
this.enabled = enabled;
}
public void setParameters(List<ExtensionCustomActionParameterResponse> parameters) {
this.parameters = parameters;
}
public List<ExtensionCustomActionParameterResponse> getParameters() {
return parameters;
}
public void setDetails(Map<String, String> details) {
this.details = details;
}
public Map<String, String> getDetails() {
return details;
}
public Date getCreated() {
return created;
}
public void setCreated(Date created) {
this.created = created;
}
}

View File

@ -0,0 +1,95 @@
// 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.apache.cloudstack.extension.ExtensionResourceMap;
import com.cloud.serializer.Param;
import com.google.gson.annotations.SerializedName;
import org.apache.cloudstack.api.ApiConstants;
import org.apache.cloudstack.api.BaseResponse;
import org.apache.cloudstack.api.EntityReference;
import java.util.Date;
import java.util.Map;
@EntityReference(value = ExtensionResourceMap.class)
public class ExtensionResourceResponse extends BaseResponse {
@SerializedName(ApiConstants.ID)
@Param(description = "ID of the resource associated with the extension")
private String id;
@SerializedName(ApiConstants.NAME)
@Param(description = "Name of the resource associated with this mapping")
private String name;
@SerializedName(ApiConstants.TYPE)
@Param(description = "Type of the resource")
private String type;
@SerializedName(ApiConstants.DETAILS)
@Param(description = "the details of the resource map")
private Map<String, String> details;
@SerializedName(ApiConstants.CREATED)
@Param(description = "Creation timestamp of the mapping")
private Date created;
public ExtensionResourceResponse() {
}
public String getId() {
return id;
}
public void setId(String id) {
this.id = id;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public String getType() {
return type;
}
public void setType(String type) {
this.type = type;
}
public Map<String, String> getDetails() {
return details;
}
public void setDetails(Map<String, String> details) {
this.details = details;
}
public Date getCreated() {
return created;
}
public void setCreated(Date created) {
this.created = created;
}
}

View File

@ -0,0 +1,182 @@
// 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 java.util.Date;
import java.util.List;
import java.util.Map;
import org.apache.cloudstack.api.ApiConstants;
import org.apache.cloudstack.api.BaseResponse;
import org.apache.cloudstack.api.EntityReference;
import org.apache.cloudstack.api.Parameter;
import org.apache.cloudstack.extension.Extension;
import com.cloud.serializer.Param;
import com.google.gson.annotations.SerializedName;
@EntityReference(value = Extension.class)
public class ExtensionResponse extends BaseResponse {
@SerializedName(ApiConstants.ID)
@Param(description = "ID of the extension")
private String id;
@SerializedName(ApiConstants.NAME)
@Param(description = "Name of the extension")
private String name;
@SerializedName(ApiConstants.DESCRIPTION)
@Param(description = "Description of the extension")
private String description;
@SerializedName(ApiConstants.TYPE)
@Param(description = "Type of the extension")
private String type;
@SerializedName(ApiConstants.PATH)
@Param(description = "The path of the entry point fo the extension")
private String path;
@SerializedName(ApiConstants.PATH_READY)
@Param(description = "True if the extension path is in ready state across management servers")
private Boolean pathReady;
@SerializedName(ApiConstants.IS_USER_DEFINED)
@Param(description = "True if the extension is added by admin")
private Boolean userDefined;
@SerializedName(ApiConstants.ORCHESTRATOR_REQUIRES_PREPARE_VM)
@Parameter(description = "Only honored when type is Orchestrator. Whether prepare VM is needed or not")
private Boolean orchestratorRequiresPrepareVm;
@SerializedName(ApiConstants.STATE)
@Param(description = "The state of the extension")
private String state;
@SerializedName(ApiConstants.DETAILS)
@Param(description = "The details of the extension")
private Map<String, String> details;
@SerializedName(ApiConstants.RESOURCES)
@Param(description = "List of resources to which extension is registered to", responseObject = ExtensionResourceResponse.class)
private List<ExtensionResourceResponse> resources;
@SerializedName(ApiConstants.CREATED)
@Param(description = "Creation timestamp of the extension")
private Date created;
@SerializedName(ApiConstants.REMOVED)
@Param(description = "Removal timestamp of the extension, if applicable")
private Date removed;
public ExtensionResponse(String id, String name, String description, String type) {
this.id = id;
this.name = name;
this.description = description;
this.type = type;
}
public String getId() {
return id;
}
public String getName() {
return name;
}
public String getDescription() {
return description;
}
public String getType() {
return type;
}
public String getPath() {
return path;
}
public Boolean isPathReady() {
return pathReady;
}
public Boolean isUserDefined() {
return userDefined;
}
public Boolean isOrchestratorRequiresPrepareVm() {
return orchestratorRequiresPrepareVm;
}
public String getState() {
return state;
}
public Map<String, String> getDetails() {
return details;
}
public void setPath(String path) {
this.path = path;
}
public void setPathReady(Boolean pathReady) {
this.pathReady = pathReady;
}
public void setUserDefined(Boolean userDefined) {
this.userDefined = userDefined;
}
public void setOrchestratorRequiresPrepareVm(Boolean orchestratorRequiresPrepareVm) {
this.orchestratorRequiresPrepareVm = orchestratorRequiresPrepareVm;
}
public void setState(String state) {
this.state = state;
}
public void setDetails(Map<String, String> details) {
this.details = details;
}
public List<ExtensionResourceResponse> getResources() {
return resources;
}
public void setResources(List<ExtensionResourceResponse> resources) {
this.resources = resources;
}
public Date getCreated() {
return created;
}
public void setCreated(Date created) {
this.created = created;
}
public Date getRemoved() {
return removed;
}
public void setRemoved(Date removed) {
this.removed = removed;
}
}

View File

@ -90,7 +90,6 @@ public class HostResponse extends BaseResponseWithAnnotations {
@SerializedName(ApiConstants.HYPERVISOR)
@Param(description = "the host hypervisor")
private String hypervisor;
@SerializedName("cpusockets")
@Param(description = "the number of CPU sockets on the host")
private Integer cpuSockets;
@ -198,6 +197,8 @@ public class HostResponse extends BaseResponseWithAnnotations {
@Param(description = "the management server name of the host", since = "4.21.0")
private String managementServerName;
private transient long clusterInternalId;
@SerializedName("clusterid")
@Param(description = "the cluster ID of the host")
private String clusterId;
@ -318,6 +319,14 @@ public class HostResponse extends BaseResponseWithAnnotations {
@Param(description = "comma-separated list of storage access groups on the zone", since = "4.21.0")
private String zoneStorageAccessGroups;
@SerializedName(ApiConstants.EXTENSION_ID)
@Param(description="The ID of extension for this cluster", since = "4.21.0")
private String extensionId;
@SerializedName(ApiConstants.EXTENSION_NAME)
@Param(description="The name of extension for this cluster", since = "4.21.0")
private String extensionName;
@Override
public String getObjectId() {
return this.getId();
@ -471,6 +480,14 @@ public class HostResponse extends BaseResponseWithAnnotations {
this.managementServerName = managementServerName;
}
public long getClusterInternalId() {
return clusterInternalId;
}
public void setClusterInternalId(long clusterInternalId) {
this.clusterInternalId = clusterInternalId;
}
public void setClusterId(String clusterId) {
this.clusterId = clusterId;
}
@ -942,4 +959,20 @@ public class HostResponse extends BaseResponseWithAnnotations {
public Boolean getInstanceConversionSupported() {
return instanceConversionSupported;
}
public void setExtensionId(String extensionId) {
this.extensionId = extensionId;
}
public String getExtensionId() {
return extensionId;
}
public void setExtensionName(String extensionName) {
this.extensionName = extensionName;
}
public String getExtensionName() {
return extensionName;
}
}

View File

@ -34,7 +34,7 @@ public class RouterHealthCheckResultResponse extends BaseResponse {
@Param(description = "the type of the health check - basic or advanced")
private String checkType;
@SerializedName(ApiConstants.RESULT)
@SerializedName(ApiConstants.SUCCESS)
@Param(description = "result of the health check")
private boolean result;

View File

@ -254,6 +254,12 @@ public class TemplateResponse extends BaseResponseWithTagInformation implements
@SerializedName(ApiConstants.USER_DATA_PARAMS) @Param(description="list of parameters which contains the list of keys or string parameters that are needed to be passed for any variables declared in userdata", since = "4.18.0")
private String userDataParams;
@SerializedName(ApiConstants.EXTENSION_ID) @Param(description="The ID of extension linked to this template", since = "4.21.0")
private String extensionId;
@SerializedName(ApiConstants.EXTENSION_NAME) @Param(description="The name of extension linked to this template", since = "4.21.0")
private String extensionName;
public TemplateResponse() {
tags = new LinkedHashSet<>();
}
@ -547,4 +553,20 @@ public class TemplateResponse extends BaseResponseWithTagInformation implements
public void setArch(String arch) {
this.arch = arch;
}
public String getExtensionId() {
return extensionId;
}
public void setExtensionId(String extensionId) {
this.extensionId = extensionId;
}
public String getExtensionName() {
return extensionName;
}
public void setExtensionName(String extensionName) {
this.extensionName = extensionName;
}
}

View File

@ -24,7 +24,7 @@ import org.apache.cloudstack.api.BaseResponse;
public class UnmanageVMInstanceResponse extends BaseResponse {
@SerializedName(ApiConstants.RESULT)
@SerializedName(ApiConstants.SUCCESS)
@Param(description = "result of the unmanage VM operation")
private boolean success;

View File

@ -0,0 +1,65 @@
// 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.extension;
import java.util.Map;
import org.apache.cloudstack.api.ApiConstants;
import org.apache.cloudstack.api.BaseResponse;
import com.cloud.serializer.Param;
import com.google.gson.annotations.SerializedName;
public class CustomActionResultResponse extends BaseResponse {
@SerializedName(ApiConstants.ID)
@Param(description = "ID of the action")
private String id;
@SerializedName(ApiConstants.NAME)
@Param(description = "Name of the action")
private String name;
@SerializedName(ApiConstants.SUCCESS)
@Param(description = "Whether custom action succeed or not")
private Boolean success;
@SerializedName(ApiConstants.RESULT)
@Param(description = "Result of the action execution")
private Map<String, String> result;
public void setId(String id) {
this.id = id;
}
public void setName(String name) {
this.name = name;
}
public void setSuccess(Boolean success) {
this.success = success;
}
public Boolean getSuccess() {
return success;
}
public void setResult(Map<String, String> result) {
this.result = result;
}
}

View File

@ -0,0 +1,44 @@
//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.extension;
import java.util.Date;
import org.apache.cloudstack.api.Identity;
import org.apache.cloudstack.api.InternalIdentity;
public interface Extension extends InternalIdentity, Identity {
enum Type {
Orchestrator
}
enum State {
Enabled, Disabled;
};
String getName();
String getDescription();
Type getType();
String getRelativePath();
boolean isPathReady();
boolean isUserDefined();
State getState();
Date getCreated();
static String getDirectoryName(String name) {
return name.replaceAll("[^a-zA-Z0-9._-]", "_").toLowerCase();
}
}

View File

@ -0,0 +1,386 @@
//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.extension;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Date;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.UUID;
import java.util.stream.Collectors;
import org.apache.cloudstack.api.ApiConstants;
import org.apache.cloudstack.api.Identity;
import org.apache.cloudstack.api.InternalIdentity;
import org.apache.cloudstack.utils.reflectiontostringbuilderutils.ReflectionToStringBuilderUtils;
import org.apache.commons.collections.CollectionUtils;
import org.apache.commons.lang3.EnumUtils;
import org.apache.commons.lang3.StringUtils;
import com.cloud.exception.InvalidParameterValueException;
import com.cloud.utils.DateUtil;
import com.google.gson.Gson;
import com.google.gson.GsonBuilder;
import com.google.gson.JsonArray;
import com.google.gson.JsonDeserializationContext;
import com.google.gson.JsonDeserializer;
import com.google.gson.JsonElement;
import com.google.gson.JsonObject;
import com.google.gson.JsonParseException;
import com.google.gson.reflect.TypeToken;
public interface ExtensionCustomAction extends InternalIdentity, Identity {
enum ResourceType {
VirtualMachine(com.cloud.vm.VirtualMachine.class);
private final Class<?> clazz;
ResourceType(Class<?> clazz) {
this.clazz = clazz;
}
public Class<?> getAssociatedClass() {
return this.clazz;
}
}
String getName();
String getDescription();
long getExtensionId();
ResourceType getResourceType();
Integer getAllowedRoleTypes();
String getSuccessMessage();
String getErrorMessage();
int getTimeout();
boolean isEnabled();
Date getCreated();
class Parameter {
public enum Type {
STRING(true),
NUMBER(true),
BOOLEAN(false),
DATE(false);
private final boolean supportsOptions;
Type(boolean supportsOptions) {
this.supportsOptions = supportsOptions;
}
public boolean canSupportsOptions() {
return supportsOptions;
}
}
public enum ValidationFormat {
// Universal default format
NONE(null),
// String formats
UUID(Type.STRING),
EMAIL(Type.STRING),
PASSWORD(Type.STRING),
URL(Type.STRING),
// Number formats
DECIMAL(Type.NUMBER);
private final Type baseType;
ValidationFormat(Type baseType) {
this.baseType = baseType;
}
public Type getBaseType() {
return baseType;
}
}
private static final Gson gson = new GsonBuilder()
.registerTypeAdapter(Parameter.class, new ParameterDeserializer())
.setPrettyPrinting()
.create();
private final String name;
private final Type type;
private final ValidationFormat validationformat;
private final List<Object> valueoptions;
private final boolean required;
public Parameter(String name, Type type, ValidationFormat validationformat, List<Object> valueoptions, boolean required) {
this.name = name;
this.type = type;
this.validationformat = validationformat;
this.valueoptions = valueoptions;
this.required = required;
}
/**
* Parses a CSV string into a list of validated options.
*/
private static List<Object> parseValueOptions(String name, String csv, Type parsedType, ValidationFormat parsedFormat) {
if (StringUtils.isBlank(csv)) {
return null;
}
List<String> values = Arrays.stream(csv.split(","))
.map(String::trim)
.filter(s -> !s.isEmpty())
.collect(Collectors.toList());
switch (parsedType) {
case STRING:
if (parsedFormat != null && parsedFormat != ValidationFormat.NONE) {
for (String value : values) {
if (!isValidStringValue(value, parsedFormat)) {
throw new InvalidParameterValueException(String.format("Invalid value options with validation format: %s for parameter: %s", parsedFormat.name(), name));
}
}
}
return new ArrayList<>(values);
case NUMBER:
try {
return values.stream()
.map(v -> parseNumber(v, parsedFormat))
.collect(Collectors.toList());
} catch (NumberFormatException ignored) {
throw new InvalidParameterValueException(String.format("Invalid value options with validation format: %s for parameter: %s", parsedFormat.name(), name));
}
default:
throw new InvalidParameterValueException(String.format("Options not supported for type: %s for parameter: %s", parsedType, name));
}
}
private static Object parseNumber(String value, ValidationFormat parsedFormat) {
if (parsedFormat == ValidationFormat.DECIMAL) {
return Float.parseFloat(value);
}
return Integer.parseInt(value);
}
private static boolean isValidStringValue(String value, ValidationFormat validationFormat ) {
switch (validationFormat) {
case NONE:
return true;
case UUID:
try {
UUID.fromString(value);
return true;
} catch (Exception ignored) {
return false;
}
case EMAIL:
return value.matches("^[A-Za-z0-9+_.-]+@[A-Za-z0-9.-]+$");
case PASSWORD:
return !value.trim().isEmpty();
case URL:
try {
new java.net.URL(value);
return true;
} catch (Exception ignored) {
return false;
}
default:
return false;
}
}
public static Parameter fromMap(Map<String, String> map) throws InvalidParameterValueException {
final String name = map.get(ApiConstants.NAME);
final String typeStr = map.get(ApiConstants.TYPE);
final String validationFormatStr = map.get(ApiConstants.VALIDATION_FORMAT);
final String required = map.get(ApiConstants.REQUIRED);
final String valueOptionsStr = map.get(ApiConstants.VALUE_OPTIONS);
if (StringUtils.isBlank(name)) {
throw new InvalidParameterValueException("Invalid parameter specified with empty name");
}
if (StringUtils.isBlank(typeStr)) {
throw new InvalidParameterValueException(String.format("No type specified for parameter: %s", name));
}
Type parsedType = EnumUtils.getEnumIgnoreCase(Type.class, typeStr);
if (parsedType == null) {
throw new InvalidParameterValueException(String.format("Invalid type: %s specified for parameter: %s",
typeStr, name));
}
ValidationFormat parsedFormat = StringUtils.isBlank(validationFormatStr) ?
ValidationFormat.NONE : EnumUtils.getEnumIgnoreCase(ValidationFormat.class, validationFormatStr);
if (parsedFormat == null || (!ValidationFormat.NONE.equals(parsedFormat) &&
parsedFormat.getBaseType() != parsedType)) {
throw new InvalidParameterValueException(
String.format("Invalid validation format: %s specified for type: %s",
parsedFormat, parsedType.name()));
}
List<Object> valueOptions = parseValueOptions(name, valueOptionsStr, parsedType, parsedFormat);
return new Parameter(name, parsedType, parsedFormat, valueOptions, Boolean.parseBoolean(required));
}
public String getName() {
return name;
}
public Type getType() {
return type;
}
public ValidationFormat getValidationFormat() {
return validationformat;
}
public List<Object> getValueOptions() {
return valueoptions;
}
public boolean isRequired() {
return required;
}
@Override
public String toString() {
return String.format("Parameter %s", ReflectionToStringBuilderUtils.reflectOnlySelectedFields(this,
ApiConstants.NAME, ApiConstants.TYPE, ApiConstants.REQUIRED));
}
public static String toJsonFromList(List<Parameter> parameters) {
return gson.toJson(parameters);
}
public static List<Parameter> toListFromJson(String json) {
java.lang.reflect.Type listType = new TypeToken<List<Parameter>>() {}.getType();
return gson.fromJson(json, listType);
}
private void validateValueInOptions(Object value) {
if (CollectionUtils.isNotEmpty(valueoptions) && !valueoptions.contains(value)) {
throw new InvalidParameterValueException("Invalid value for parameter '" + name + "': " + value +
". Valid options are: " + valueoptions.stream().map(Object::toString).collect(Collectors.joining(", ")));
}
}
public Object validatedValue(String value) {
if (StringUtils.isBlank(value)) {
throw new InvalidParameterValueException("Empty value for parameter '" + name + "': " + value);
}
try {
switch (type) {
case BOOLEAN:
if (!Arrays.asList("true", "false").contains(value)) {
throw new IllegalArgumentException();
}
return Boolean.parseBoolean(value);
case DATE:
return DateUtil.parseTZDateString(value);
case NUMBER:
Object obj = parseNumber(value, validationformat);
validateValueInOptions(obj);
return obj;
default:
if (!isValidStringValue(value, validationformat)) {
throw new IllegalArgumentException();
}
validateValueInOptions(value);
return value;
}
} catch (Exception e) {
throw new InvalidParameterValueException("Invalid value for parameter '" + name + "': " + value);
}
}
public static Map<String, Object> validateParameterValues(List<Parameter> parameterDefinitions,
Map<String, String> suppliedValues) throws InvalidParameterValueException {
if (suppliedValues == null) {
suppliedValues = new HashMap<>();
}
for (Parameter param : parameterDefinitions) {
String value = suppliedValues.get(param.getName());
if (param.isRequired()) {
if (value == null || value.trim().isEmpty()) {
throw new InvalidParameterValueException("Missing or empty required parameter: " + param.getName());
}
}
}
Map<String, Object> validatedParams = new HashMap<>();
for (Map.Entry<String, String> entry : suppliedValues.entrySet()) {
String name = entry.getKey();
String value = entry.getValue();
Parameter param = parameterDefinitions.stream()
.filter(p -> p.getName().equals(name))
.findFirst()
.orElse(null);
if (param != null) {
validatedParams.put(name, param.validatedValue(value));
} else {
validatedParams.put(name, value);
}
}
return validatedParams;
}
static class ParameterDeserializer implements JsonDeserializer<Parameter> {
@Override
public Parameter deserialize(JsonElement json, java.lang.reflect.Type typeOfT, JsonDeserializationContext context) throws JsonParseException {
JsonObject obj = json.getAsJsonObject();
String name = obj.get(ApiConstants.NAME).getAsString();
String typeStr = obj.get(ApiConstants.TYPE).getAsString();
String validationFormatStr = obj.has(ApiConstants.VALIDATION_FORMAT) ? obj.get(ApiConstants.VALIDATION_FORMAT).getAsString() : null;
boolean required = obj.has(ApiConstants.REQUIRED) && obj.get(ApiConstants.REQUIRED).getAsBoolean();
Parameter.Type typeEnum = Parameter.Type.valueOf(typeStr);
Parameter.ValidationFormat validationFormatEnum = (validationFormatStr != null)
? Parameter.ValidationFormat.valueOf(validationFormatStr)
: Parameter.ValidationFormat.NONE;
List<Object> valueOptions = null;
if (obj.has(ApiConstants.VALUE_OPTIONS) && obj.get(ApiConstants.VALUE_OPTIONS).isJsonArray()) {
JsonArray valueOptionsArray = obj.getAsJsonArray(ApiConstants.VALUE_OPTIONS);
valueOptions = new ArrayList<>();
for (JsonElement el : valueOptionsArray) {
switch (typeEnum) {
case STRING:
valueOptions.add(el.getAsString());
break;
case NUMBER:
if (validationFormatEnum == Parameter.ValidationFormat.DECIMAL) {
valueOptions.add(el.getAsFloat());
} else {
valueOptions.add(el.getAsInt());
}
break;
default:
throw new JsonParseException("Unsupported type for value options");
}
}
}
return new Parameter(name, typeEnum, validationFormatEnum, valueOptions, required);
}
}
}
}

View File

@ -0,0 +1,24 @@
// 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.extension;
public interface ExtensionHelper {
Long getExtensionIdForCluster(long clusterId);
Extension getExtension(long id);
Extension getExtensionForCluster(long clusterId);
}

View File

@ -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.extension;
import java.util.Date;
import org.apache.cloudstack.api.Identity;
import org.apache.cloudstack.api.InternalIdentity;
public interface ExtensionResourceMap extends InternalIdentity, Identity {
enum ResourceType {
Cluster
}
long getExtensionId();
long getResourceId();
ResourceType getResourceType();
Date getCreated();
}

View File

@ -0,0 +1,83 @@
// 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.cluster;
import static org.mockito.ArgumentMatchers.anyLong;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;
import java.util.Arrays;
import java.util.Collections;
import java.util.List;
import org.apache.cloudstack.api.response.ClusterResponse;
import org.apache.cloudstack.extension.Extension;
import org.apache.cloudstack.extension.ExtensionHelper;
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.mockito.junit.MockitoJUnitRunner;
import com.cloud.hypervisor.Hypervisor;
@RunWith(MockitoJUnitRunner.class)
public class ListClustersCmdTest {
ExtensionHelper extensionHelper;
ListClustersCmd listClustersCmd = new ListClustersCmd();
@Before
public void setUp() {
extensionHelper = mock(ExtensionHelper.class);
listClustersCmd.extensionHelper = extensionHelper;
}
@Test
public void updateClustersExtensions_emptyList_noAction() {
listClustersCmd.updateClustersExtensions(Collections.emptyList());
// No exception, nothing to verify
}
@Test
public void updateClustersExtensions_nullList_noAction() {
listClustersCmd.updateClustersExtensions(null);
// No exception, nothing to verify
}
@Test
public void updateClustersExtensions_withClusterResponses_setsExtension() {
ClusterResponse cluster1 = mock(ClusterResponse.class);
ClusterResponse cluster2 = mock(ClusterResponse.class);
when(cluster1.getInternalId()).thenReturn(1L);
when(cluster1.getHypervisorType()).thenReturn(Hypervisor.HypervisorType.External.name());
when(cluster2.getInternalId()).thenReturn(2L);
when(cluster2.getHypervisorType()).thenReturn(Hypervisor.HypervisorType.External.name());
Extension ext1 = mock(Extension.class);
when(ext1.getUuid()).thenReturn("a");
Extension ext2 = mock(Extension.class);
when(ext2.getUuid()).thenReturn("b");
when(extensionHelper.getExtensionIdForCluster(anyLong())).thenAnswer(invocation -> invocation.getArguments()[0]);
when(extensionHelper.getExtension(1L)).thenReturn(ext1);
when(extensionHelper.getExtension(2L)).thenReturn(ext2);
List<ClusterResponse> clusters = Arrays.asList(cluster1, cluster2);
listClustersCmd.updateClustersExtensions(clusters);
verify(cluster1).setExtensionId("a");
verify(cluster2).setExtensionId("b");
}
}

View File

@ -0,0 +1,484 @@
//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.extension;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertNull;
import static org.junit.Assert.assertTrue;
import java.util.Arrays;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import org.apache.cloudstack.api.ApiConstants;
import org.junit.Test;
import com.cloud.exception.InvalidParameterValueException;
public class ExtensionCustomActionTest {
@Test
public void testResourceType() {
ExtensionCustomAction.ResourceType vmType = ExtensionCustomAction.ResourceType.VirtualMachine;
assertEquals(com.cloud.vm.VirtualMachine.class, vmType.getAssociatedClass());
}
@Test
public void testParameterTypeSupportsOptions() {
assertTrue(ExtensionCustomAction.Parameter.Type.STRING.canSupportsOptions());
assertTrue(ExtensionCustomAction.Parameter.Type.NUMBER.canSupportsOptions());
assertFalse(ExtensionCustomAction.Parameter.Type.BOOLEAN.canSupportsOptions());
assertFalse(ExtensionCustomAction.Parameter.Type.DATE.canSupportsOptions());
}
@Test
public void testValidationFormatBaseType() {
assertEquals(ExtensionCustomAction.Parameter.Type.STRING,
ExtensionCustomAction.Parameter.ValidationFormat.UUID.getBaseType());
assertEquals(ExtensionCustomAction.Parameter.Type.STRING,
ExtensionCustomAction.Parameter.ValidationFormat.EMAIL.getBaseType());
assertEquals(ExtensionCustomAction.Parameter.Type.STRING,
ExtensionCustomAction.Parameter.ValidationFormat.PASSWORD.getBaseType());
assertEquals(ExtensionCustomAction.Parameter.Type.STRING,
ExtensionCustomAction.Parameter.ValidationFormat.URL.getBaseType());
assertEquals(ExtensionCustomAction.Parameter.Type.NUMBER,
ExtensionCustomAction.Parameter.ValidationFormat.DECIMAL.getBaseType());
assertNull(ExtensionCustomAction.Parameter.ValidationFormat.NONE.getBaseType());
}
@Test
public void testParameterFromMapValid() {
Map<String, String> map = new HashMap<>();
map.put(ApiConstants.NAME, "testParam");
map.put(ApiConstants.TYPE, "STRING");
map.put(ApiConstants.VALIDATION_FORMAT, "EMAIL");
map.put(ApiConstants.REQUIRED, "true");
map.put(ApiConstants.VALUE_OPTIONS, "test@example.com,another@test.com");
ExtensionCustomAction.Parameter param = ExtensionCustomAction.Parameter.fromMap(map);
assertEquals("testParam", param.getName());
assertEquals(ExtensionCustomAction.Parameter.Type.STRING, param.getType());
assertEquals(ExtensionCustomAction.Parameter.ValidationFormat.EMAIL, param.getValidationFormat());
assertTrue(param.isRequired());
assertEquals(2, param.getValueOptions().size());
assertTrue(param.getValueOptions().contains("test@example.com"));
assertTrue(param.getValueOptions().contains("another@test.com"));
}
@Test(expected = InvalidParameterValueException.class)
public void testParameterFromMapEmptyName() {
Map<String, String> map = new HashMap<>();
map.put(ApiConstants.NAME, "");
map.put(ApiConstants.TYPE, "STRING");
ExtensionCustomAction.Parameter.fromMap(map);
}
@Test(expected = InvalidParameterValueException.class)
public void testParameterFromMapNoType() {
Map<String, String> map = new HashMap<>();
map.put(ApiConstants.NAME, "testParam");
ExtensionCustomAction.Parameter.fromMap(map);
}
@Test(expected = InvalidParameterValueException.class)
public void testParameterFromMapInvalidType() {
Map<String, String> map = new HashMap<>();
map.put(ApiConstants.NAME, "testParam");
map.put(ApiConstants.TYPE, "INVALID_TYPE");
ExtensionCustomAction.Parameter.fromMap(map);
}
@Test(expected = InvalidParameterValueException.class)
public void testParameterFromMapInvalidValidationFormat() {
Map<String, String> map = new HashMap<>();
map.put(ApiConstants.NAME, "testParam");
map.put(ApiConstants.TYPE, "STRING");
map.put(ApiConstants.VALIDATION_FORMAT, "INVALID_FORMAT");
ExtensionCustomAction.Parameter.fromMap(map);
}
@Test(expected = InvalidParameterValueException.class)
public void testParameterFromMapMismatchedTypeAndFormat() {
Map<String, String> map = new HashMap<>();
map.put(ApiConstants.NAME, "testParam");
map.put(ApiConstants.TYPE, "STRING");
map.put(ApiConstants.VALIDATION_FORMAT, "DECIMAL");
ExtensionCustomAction.Parameter.fromMap(map);
}
@Test
public void testParameterFromMapWithNumberOptions() {
Map<String, String> map = new HashMap<>();
map.put(ApiConstants.NAME, "testParam");
map.put(ApiConstants.TYPE, "NUMBER");
map.put(ApiConstants.VALIDATION_FORMAT, "DECIMAL");
map.put(ApiConstants.VALUE_OPTIONS, "1.5,2.7,3.0");
ExtensionCustomAction.Parameter param = ExtensionCustomAction.Parameter.fromMap(map);
assertEquals(ExtensionCustomAction.Parameter.Type.NUMBER, param.getType());
assertEquals(ExtensionCustomAction.Parameter.ValidationFormat.DECIMAL, param.getValidationFormat());
assertEquals(3, param.getValueOptions().size());
assertTrue(param.getValueOptions().contains(1.5f));
assertTrue(param.getValueOptions().contains(2.7f));
assertTrue(param.getValueOptions().contains(3.0f));
}
@Test(expected = InvalidParameterValueException.class)
public void testParameterFromMapInvalidNumberOptions() {
Map<String, String> map = new HashMap<>();
map.put(ApiConstants.NAME, "testParam");
map.put(ApiConstants.TYPE, "NUMBER");
map.put(ApiConstants.VALUE_OPTIONS, "1.5,invalid,3.0");
ExtensionCustomAction.Parameter.fromMap(map);
}
@Test(expected = InvalidParameterValueException.class)
public void testParameterFromMapInvalidEmailOptions() {
Map<String, String> map = new HashMap<>();
map.put(ApiConstants.NAME, "testParam");
map.put(ApiConstants.TYPE, "STRING");
map.put(ApiConstants.VALIDATION_FORMAT, "EMAIL");
map.put(ApiConstants.VALUE_OPTIONS, "valid@email.com,invalid-email");
ExtensionCustomAction.Parameter.fromMap(map);
}
@Test
public void testValidatedValueString() {
ExtensionCustomAction.Parameter param = new ExtensionCustomAction.Parameter(
"testParam",
ExtensionCustomAction.Parameter.Type.STRING,
ExtensionCustomAction.Parameter.ValidationFormat.EMAIL,
null,
false
);
Object result = param.validatedValue("test@example.com");
assertEquals("test@example.com", result);
}
@Test(expected = InvalidParameterValueException.class)
public void testValidatedValueInvalidEmail() {
ExtensionCustomAction.Parameter param = new ExtensionCustomAction.Parameter(
"testParam",
ExtensionCustomAction.Parameter.Type.STRING,
ExtensionCustomAction.Parameter.ValidationFormat.EMAIL,
null,
false
);
param.validatedValue("invalid-email");
}
@Test
public void testValidatedValueUUID() {
ExtensionCustomAction.Parameter param = new ExtensionCustomAction.Parameter(
"testParam",
ExtensionCustomAction.Parameter.Type.STRING,
ExtensionCustomAction.Parameter.ValidationFormat.UUID,
null,
false
);
String validUUID = "550e8400-e29b-41d4-a716-446655440000";
Object result = param.validatedValue(validUUID);
assertEquals(validUUID, result);
}
@Test(expected = InvalidParameterValueException.class)
public void testValidatedValueInvalidUUID() {
ExtensionCustomAction.Parameter param = new ExtensionCustomAction.Parameter(
"testParam",
ExtensionCustomAction.Parameter.Type.STRING,
ExtensionCustomAction.Parameter.ValidationFormat.UUID,
null,
false
);
param.validatedValue("invalid-uuid");
}
@Test
public void testValidatedValueURL() {
ExtensionCustomAction.Parameter param = new ExtensionCustomAction.Parameter(
"testParam",
ExtensionCustomAction.Parameter.Type.STRING,
ExtensionCustomAction.Parameter.ValidationFormat.URL,
null,
false
);
Object result = param.validatedValue("https://example.com");
assertEquals("https://example.com", result);
}
@Test(expected = InvalidParameterValueException.class)
public void testValidatedValueInvalidURL() {
ExtensionCustomAction.Parameter param = new ExtensionCustomAction.Parameter(
"testParam",
ExtensionCustomAction.Parameter.Type.STRING,
ExtensionCustomAction.Parameter.ValidationFormat.URL,
null,
false
);
param.validatedValue("not-a-url");
}
@Test
public void testValidatedValuePassword() {
ExtensionCustomAction.Parameter param = new ExtensionCustomAction.Parameter(
"testParam",
ExtensionCustomAction.Parameter.Type.STRING,
ExtensionCustomAction.Parameter.ValidationFormat.PASSWORD,
null,
false
);
Object result = param.validatedValue("mypassword");
assertEquals("mypassword", result);
}
@Test(expected = InvalidParameterValueException.class)
public void testValidatedValueEmptyPassword() {
ExtensionCustomAction.Parameter param = new ExtensionCustomAction.Parameter(
"testParam",
ExtensionCustomAction.Parameter.Type.STRING,
ExtensionCustomAction.Parameter.ValidationFormat.PASSWORD,
null,
false
);
param.validatedValue(" ");
}
@Test
public void testValidatedValueNumber() {
ExtensionCustomAction.Parameter param = new ExtensionCustomAction.Parameter(
"testParam",
ExtensionCustomAction.Parameter.Type.NUMBER,
ExtensionCustomAction.Parameter.ValidationFormat.NONE,
null,
false
);
Object result = param.validatedValue("42");
assertEquals(42, result);
}
@Test
public void testValidatedValueDecimal() {
ExtensionCustomAction.Parameter param = new ExtensionCustomAction.Parameter(
"testParam",
ExtensionCustomAction.Parameter.Type.NUMBER,
ExtensionCustomAction.Parameter.ValidationFormat.DECIMAL,
null,
false
);
Object result = param.validatedValue("3.14");
assertEquals(3.14f, result);
}
@Test(expected = InvalidParameterValueException.class)
public void testValidatedValueInvalidNumber() {
ExtensionCustomAction.Parameter param = new ExtensionCustomAction.Parameter(
"testParam",
ExtensionCustomAction.Parameter.Type.NUMBER,
ExtensionCustomAction.Parameter.ValidationFormat.NONE,
null,
false
);
param.validatedValue("not-a-number");
}
@Test
public void testValidatedValueBoolean() {
ExtensionCustomAction.Parameter param = new ExtensionCustomAction.Parameter(
"testParam",
ExtensionCustomAction.Parameter.Type.BOOLEAN,
ExtensionCustomAction.Parameter.ValidationFormat.NONE,
null,
false
);
Object result = param.validatedValue("true");
assertEquals(true, result);
}
@Test(expected = InvalidParameterValueException.class)
public void testValidatedValueInvalidBoolean() {
ExtensionCustomAction.Parameter param = new ExtensionCustomAction.Parameter(
"testParam",
ExtensionCustomAction.Parameter.Type.BOOLEAN,
ExtensionCustomAction.Parameter.ValidationFormat.NONE,
null,
false
);
Object result = param.validatedValue("maybe");
}
@Test
public void testValidatedValueWithOptions() {
List<Object> options = Arrays.asList("option1", "option2", "option3");
ExtensionCustomAction.Parameter param = new ExtensionCustomAction.Parameter(
"testParam",
ExtensionCustomAction.Parameter.Type.STRING,
ExtensionCustomAction.Parameter.ValidationFormat.NONE,
options,
false
);
Object result = param.validatedValue("option2");
assertEquals("option2", result);
}
@Test(expected = InvalidParameterValueException.class)
public void testValidatedValueNotInOptions() {
List<Object> options = Arrays.asList("option1", "option2", "option3");
ExtensionCustomAction.Parameter param = new ExtensionCustomAction.Parameter(
"testParam",
ExtensionCustomAction.Parameter.Type.STRING,
ExtensionCustomAction.Parameter.ValidationFormat.NONE,
options,
false
);
param.validatedValue("option4");
}
@Test(expected = InvalidParameterValueException.class)
public void testValidatedValueEmpty() {
ExtensionCustomAction.Parameter param = new ExtensionCustomAction.Parameter(
"testParam",
ExtensionCustomAction.Parameter.Type.STRING,
ExtensionCustomAction.Parameter.ValidationFormat.NONE,
null,
false
);
param.validatedValue("");
}
@Test
public void testValidateParameterValues() {
List<ExtensionCustomAction.Parameter> paramDefs = Arrays.asList(
new ExtensionCustomAction.Parameter("required1", ExtensionCustomAction.Parameter.Type.STRING,
ExtensionCustomAction.Parameter.ValidationFormat.NONE, null, true),
new ExtensionCustomAction.Parameter("required2", ExtensionCustomAction.Parameter.Type.NUMBER,
ExtensionCustomAction.Parameter.ValidationFormat.NONE, null, true),
new ExtensionCustomAction.Parameter("optional", ExtensionCustomAction.Parameter.Type.STRING,
ExtensionCustomAction.Parameter.ValidationFormat.NONE, null, false)
);
Map<String, String> suppliedValues = new HashMap<>();
suppliedValues.put("required1", "value1");
suppliedValues.put("required2", "42");
suppliedValues.put("optional", "optionalValue");
Map<String, Object> result = ExtensionCustomAction.Parameter.validateParameterValues(paramDefs, suppliedValues);
assertEquals("value1", result.get("required1"));
assertEquals(42, result.get("required2"));
assertEquals("optionalValue", result.get("optional"));
}
@Test(expected = InvalidParameterValueException.class)
public void testValidateParameterValuesMissingRequired() {
List<ExtensionCustomAction.Parameter> paramDefs = Arrays.asList(
new ExtensionCustomAction.Parameter("required1", ExtensionCustomAction.Parameter.Type.STRING,
ExtensionCustomAction.Parameter.ValidationFormat.NONE, null, true)
);
Map<String, String> suppliedValues = new HashMap<>();
ExtensionCustomAction.Parameter.validateParameterValues(paramDefs, suppliedValues);
}
@Test(expected = InvalidParameterValueException.class)
public void testValidateParameterValuesEmptyRequired() {
List<ExtensionCustomAction.Parameter> paramDefs = Arrays.asList(
new ExtensionCustomAction.Parameter("required1", ExtensionCustomAction.Parameter.Type.STRING,
ExtensionCustomAction.Parameter.ValidationFormat.NONE, null, true)
);
Map<String, String> suppliedValues = new HashMap<>();
suppliedValues.put("required1", " ");
ExtensionCustomAction.Parameter.validateParameterValues(paramDefs, suppliedValues);
}
@Test
public void testValidateParameterValuesNullSupplied() {
List<ExtensionCustomAction.Parameter> paramDefs = Arrays.asList(
new ExtensionCustomAction.Parameter("optional", ExtensionCustomAction.Parameter.Type.STRING,
ExtensionCustomAction.Parameter.ValidationFormat.NONE, null, false)
);
Map<String, Object> result = ExtensionCustomAction.Parameter.validateParameterValues(paramDefs, null);
assertTrue(result.isEmpty());
}
@Test
public void testJsonSerializationDeserialization() {
List<ExtensionCustomAction.Parameter> originalParams = Arrays.asList(
new ExtensionCustomAction.Parameter("param1", ExtensionCustomAction.Parameter.Type.STRING,
ExtensionCustomAction.Parameter.ValidationFormat.EMAIL, Arrays.asList("test@example.com"), true),
new ExtensionCustomAction.Parameter("param2", ExtensionCustomAction.Parameter.Type.NUMBER,
ExtensionCustomAction.Parameter.ValidationFormat.DECIMAL, Arrays.asList(1.5f, 2.7f), false)
);
String json = ExtensionCustomAction.Parameter.toJsonFromList(originalParams);
List<ExtensionCustomAction.Parameter> deserializedParams = ExtensionCustomAction.Parameter.toListFromJson(json);
assertEquals(originalParams.size(), deserializedParams.size());
assertEquals(originalParams.get(0).getName(), deserializedParams.get(0).getName());
assertEquals(originalParams.get(0).getType(), deserializedParams.get(0).getType());
assertEquals(originalParams.get(0).getValidationFormat(), deserializedParams.get(0).getValidationFormat());
assertEquals(originalParams.get(0).isRequired(), deserializedParams.get(0).isRequired());
assertEquals(originalParams.get(0).getValueOptions(), deserializedParams.get(0).getValueOptions());
}
@Test
public void testToString() {
ExtensionCustomAction.Parameter param = new ExtensionCustomAction.Parameter(
"testParam",
ExtensionCustomAction.Parameter.Type.STRING,
ExtensionCustomAction.Parameter.ValidationFormat.EMAIL,
null,
true
);
String result = param.toString();
assertTrue(result.contains("testParam"));
assertTrue(result.contains("STRING"));
assertTrue(result.contains("true"));
}
}

View File

@ -28,3 +28,4 @@ MSMNTDIR=/mnt
COMPONENTS-SPEC=components.xml
REMOTEHOST=localhost
COMMONLIBDIR=client/target/common/
EXTENSIONSDEPLOYMENTMODE=developer

View File

@ -55,3 +55,6 @@ webapp.dir=/usr/share/cloudstack-management/webapp
# The path to access log file
access.log=/var/log/cloudstack/management/access.log
# The deployment mode for the extensions
extensions.deployment.mode=@EXTENSIONSDEPLOYMENTMODE@

View File

@ -337,6 +337,11 @@
<artifactId>cloud-plugin-hypervisor-hyperv</artifactId>
<version>${project.version}</version>
</dependency>
<dependency>
<groupId>org.apache.cloudstack</groupId>
<artifactId>cloud-plugin-hypervisor-external</artifactId>
<version>${project.version}</version>
</dependency>
<dependency>
<groupId>org.apache.cloudstack</groupId>
<artifactId>cloud-plugin-storage-allocator-random</artifactId>

View File

@ -0,0 +1,51 @@
//
// 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.agent.api;
import java.util.Map;
import com.cloud.agent.api.to.VirtualMachineTO;
public class PrepareExternalProvisioningAnswer extends Answer {
Map<String, String> serverDetails;
VirtualMachineTO virtualMachineTO;
public PrepareExternalProvisioningAnswer() {
super();
}
public PrepareExternalProvisioningAnswer(PrepareExternalProvisioningCommand cmd, Map<String, String> externalDetails, VirtualMachineTO virtualMachineTO, String details) {
super(cmd, true, details);
this.serverDetails = externalDetails;
this.virtualMachineTO = virtualMachineTO;
}
public PrepareExternalProvisioningAnswer(PrepareExternalProvisioningCommand cmd, boolean success, String details) {
super(cmd, success, details);
}
public Map<String, String> getServerDetails() {
return serverDetails;
}
public VirtualMachineTO getVirtualMachineTO() {
return virtualMachineTO;
}
}

View File

@ -0,0 +1,39 @@
//
// 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.agent.api;
import com.cloud.agent.api.to.VirtualMachineTO;
public class PrepareExternalProvisioningCommand extends Command {
VirtualMachineTO virtualMachineTO;
public PrepareExternalProvisioningCommand(VirtualMachineTO vmTO) {
this.virtualMachineTO = vmTO;
}
public VirtualMachineTO getVirtualMachineTO() {
return virtualMachineTO;
}
@Override
public boolean executeInSequence() {
return false;
}
}

View File

@ -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 com.cloud.agent.api;
public class RunCustomActionAnswer extends Answer {
public RunCustomActionAnswer(RunCustomActionCommand cmd, boolean success, String details) {
super(cmd, success, details);
}
@Override
public boolean executeInSequence() {
return false;
}
}

View File

@ -0,0 +1,59 @@
//
// 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.agent.api;
import java.util.Map;
public class RunCustomActionCommand extends Command {
String actionName;
Long vmId;
Map<String, Object> parameters;
public RunCustomActionCommand(String actionName) {
this.actionName = actionName;
this.setWait(5);
}
public String getActionName() {
return actionName;
}
public Long getVmId() {
return vmId;
}
public void setVmId(Long vmId) {
this.vmId = vmId;
}
public Map<String, Object> getParameters() {
return parameters;
}
public void setParameters(Map<String, Object> parameters) {
this.parameters = parameters;
}
@Override
public boolean executeInSequence() {
return false;
}
}

View File

@ -19,14 +19,14 @@
package com.cloud.agent.api;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import com.cloud.agent.api.to.DpdkTO;
import com.cloud.agent.api.to.GPUDeviceTO;
import com.cloud.vm.VirtualMachine;
import java.util.ArrayList;
import java.util.Map;
import java.util.List;
public class StopCommand extends RebootCommand {
private boolean isProxy = false;
private String urlPort = null;
@ -37,6 +37,7 @@ public class StopCommand extends RebootCommand {
boolean forceStop = false;
private Map<String, DpdkTO> dpdkInterfaceMapping;
Map<String, Boolean> vlanToPersistenceMap;
boolean expungeVM = false;
public Map<String, DpdkTO> getDpdkInterfaceMapping() {
return dpdkInterfaceMapping;
@ -138,4 +139,12 @@ public class StopCommand extends RebootCommand {
public void setVlanToPersistenceMap(Map<String, Boolean> vlanToPersistenceMap) {
this.vlanToPersistenceMap = vlanToPersistenceMap;
}
public boolean isExpungeVM() {
return expungeVM;
}
public void setExpungeVM(boolean expungeVM) {
this.expungeVM = expungeVM;
}
}

View File

@ -27,6 +27,7 @@
/usr/share/cloudstack-common/scripts/vm/hypervisor/versions.sh
/usr/share/cloudstack-common/scripts/vm/hypervisor/vmware/*
/usr/share/cloudstack-common/scripts/vm/hypervisor/xenserver/*
/usr/share/cloudstack-common/scripts/vm/hypervisor/external/provisioner/*
/usr/share/cloudstack-common/lib/*
/usr/share/cloudstack-common/vms/*
/usr/bin/cloudstack-set-guest-password

View File

@ -22,6 +22,8 @@
/etc/cloudstack/management/java.security.ciphers
/etc/cloudstack/management/log4j-cloud.xml
/etc/cloudstack/management/config.json
/etc/cloudstack/extensions/Proxmox/proxmox.sh
/etc/cloudstack/extensions/HyperV/hyperv.py
/etc/default/cloudstack-management
/etc/security/limits.d/cloudstack-limits.conf
/etc/sudoers.d/cloudstack

View File

@ -59,6 +59,9 @@ if [ "$1" = configure ]; then
chown -R cloud:cloud /usr/share/cloudstack-management/templates
find /usr/share/cloudstack-management/templates -type d -exec chmod 0770 {} \;
chmod -R 0755 /etc/cloudstack/extensions
chown -R cloud:cloud /etc/cloudstack/extensions
ln -sf ${CONFDIR}/log4j-cloud.xml ${CONFDIR}/log4j2.xml
# Add jdbc MySQL driver settings to db.properties if not present

3
debian/rules vendored
View File

@ -64,6 +64,7 @@ override_dh_auto_install:
# cloudstack-management
mkdir $(DESTDIR)/$(SYSCONFDIR)/$(PACKAGE)/server
mkdir $(DESTDIR)/$(SYSCONFDIR)/$(PACKAGE)/management
mkdir -p $(DESTDIR)/$(SYSCONFDIR)/$(PACKAGE)/extensions
mkdir -p $(DESTDIR)/$(SYSCONFDIR)/security/limits.d/
mkdir -p $(DESTDIR)/$(SYSCONFDIR)/sudoers.d/
mkdir -p $(DESTDIR)/usr/share/$(PACKAGE)-management
@ -81,6 +82,7 @@ override_dh_auto_install:
cp -r client/target/classes/META-INF/webapp $(DESTDIR)/usr/share/$(PACKAGE)-management/webapp
cp server/target/conf/* $(DESTDIR)/$(SYSCONFDIR)/$(PACKAGE)/server/
cp client/target/conf/* $(DESTDIR)/$(SYSCONFDIR)/$(PACKAGE)/management/
cp -r extensions/* $(DESTDIR)/$(SYSCONFDIR)/$(PACKAGE)/extensions/
cp client/target/cloud-client-ui-$(VERSION).jar $(DESTDIR)/usr/share/$(PACKAGE)-management/lib/cloudstack-$(VERSION).jar
cp client/target/lib/*jar $(DESTDIR)/usr/share/$(PACKAGE)-management/lib/
cp -r engine/schema/dist/systemvm-templates/* $(DESTDIR)/usr/share/$(PACKAGE)-management/templates/systemvm/
@ -106,6 +108,7 @@ override_dh_auto_install:
# Remove configuration in /ur/share/cloudstack-management/webapps/client/WEB-INF
# This should all be in /etc/cloudstack/management
ln -s ../../..$(SYSCONFDIR)/$(PACKAGE)/management $(DESTDIR)/usr/share/$(PACKAGE)-management/conf
ln -s ../../..$(SYSCONFDIR)/$(PACKAGE)/extensions $(DESTDIR)/usr/share/$(PACKAGE)-management/extensions
ln -s ../../../var/log/$(PACKAGE)/management $(DESTDIR)/usr/share/$(PACKAGE)-management/logs
install -d -m0755 debian/$(PACKAGE)-management/lib/systemd/system

View File

@ -0,0 +1,61 @@
// 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.hypervisor;
import java.util.Map;
import com.cloud.agent.api.HostVmStateReportEntry;
import com.cloud.agent.api.PrepareExternalProvisioningAnswer;
import com.cloud.agent.api.PrepareExternalProvisioningCommand;
import com.cloud.agent.api.RebootAnswer;
import com.cloud.agent.api.RebootCommand;
import com.cloud.agent.api.RunCustomActionAnswer;
import com.cloud.agent.api.RunCustomActionCommand;
import com.cloud.agent.api.StartAnswer;
import com.cloud.agent.api.StartCommand;
import com.cloud.agent.api.StopAnswer;
import com.cloud.agent.api.StopCommand;
import com.cloud.utils.component.Manager;
public interface ExternalProvisioner extends Manager {
String getExtensionsPath();
String getExtensionPath(String relativePath);
String getChecksumForExtensionPath(String extensionName, String relativePath);
void prepareExtensionPath(String extensionName, boolean userDefined, String extensionRelativePath);
void cleanupExtensionPath(String extensionName, String extensionRelativePath);
void cleanupExtensionData(String extensionName, int olderThanDays, boolean cleanupDirectory);
PrepareExternalProvisioningAnswer prepareExternalProvisioning(String hostGuid, String extensionName, String extensionRelativePath, PrepareExternalProvisioningCommand cmd);
StartAnswer startInstance(String hostGuid, String extensionName, String extensionRelativePath, StartCommand cmd);
StopAnswer stopInstance(String hostGuid, String extensionName, String extensionRelativePath, StopCommand cmd);
RebootAnswer rebootInstance(String hostGuid, String extensionName, String extensionRelativePath, RebootCommand cmd);
StopAnswer expungeInstance(String hostGuid, String extensionName, String extensionRelativePath, StopCommand cmd);
Map<String, HostVmStateReportEntry> getHostVmStateReport(long hostId, String extensionName, String extensionRelativePath);
RunCustomActionAnswer runCustomAction(String hostGuid, String extensionName, String extensionRelativePath, RunCustomActionCommand cmd);
}

View File

@ -29,6 +29,7 @@ import com.cloud.dc.DataCenterVO;
import com.cloud.deploy.DeployDestination;
import com.cloud.exception.ResourceAllocationException;
import com.cloud.exception.StorageUnavailableException;
import com.cloud.hypervisor.Hypervisor;
import com.cloud.storage.DataStoreRole;
import com.cloud.storage.Storage.TemplateType;
import com.cloud.storage.StoragePool;
@ -141,7 +142,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);
TemplateType validateTemplateType(BaseCmd cmd, boolean isAdmin, boolean isCrossZones, Hypervisor.HypervisorType hypervisorType);
List<DatadiskTO> getTemplateDisksOnImageStore(VirtualMachineTemplate template, DataStoreRole role, String configurationId);

View File

@ -73,6 +73,11 @@
<artifactId>cloud-plugin-maintenance</artifactId>
<version>${project.version}</version>
</dependency>
<dependency>
<groupId>org.apache.cloudstack</groupId>
<artifactId>cloud-plugin-hypervisor-external</artifactId>
<version>${project.version}</version>
</dependency>
</dependencies>
<build>
<plugins>

View File

@ -676,17 +676,17 @@ public class AgentManagerImpl extends ManagerBase implements AgentManager, Handl
protected Status investigate(final AgentAttache agent) {
final Long hostId = agent.getId();
final HostVO host = _hostDao.findById(hostId);
if (host != null && host.getType() != null && !host.getType().isVirtual()) {
logger.debug("Checking if agent ({}) is alive", host);
final Answer answer = easySend(hostId, new CheckHealthCommand());
if (answer != null && answer.getResult()) {
final Status status = Status.Up;
logger.debug("Agent ({}) responded to checkHealthCommand, reporting that agent is {}", host, status);
return status;
}
return _haMgr.investigate(hostId);
if (host == null || host.getType() == null || host.getType().isVirtual()) {
return Status.Alert;
}
return Status.Alert;
logger.debug("Checking if agent ({}) is alive", host);
final Answer answer = easySend(hostId, new CheckHealthCommand());
if (answer != null && answer.getResult()) {
final Status status = Status.Up;
logger.debug("Agent ({}) responded to checkHealthCommand, reporting that agent is {}", host, status);
return status;
}
return _haMgr.investigate(hostId);
}
protected AgentAttache getAttache(final Long hostId) throws AgentUnavailableException {

View File

@ -47,6 +47,8 @@ import org.apache.cloudstack.ca.CAManager;
import org.apache.cloudstack.framework.config.ConfigDepot;
import org.apache.cloudstack.framework.config.ConfigKey;
import org.apache.cloudstack.framework.config.dao.ConfigurationDao;
import org.apache.cloudstack.framework.extensions.command.ExtensionServerActionBaseCommand;
import org.apache.cloudstack.framework.extensions.manager.ExtensionsManager;
import org.apache.cloudstack.ha.dao.HAConfigDao;
import org.apache.cloudstack.maintenance.ManagementServerMaintenanceManager;
import org.apache.cloudstack.maintenance.command.BaseShutdownManagementServerHostCommand;
@ -61,6 +63,7 @@ import org.apache.cloudstack.management.ManagementServerHost;
import org.apache.cloudstack.outofbandmanagement.dao.OutOfBandManagementDao;
import org.apache.cloudstack.utils.identity.ManagementServerNode;
import org.apache.cloudstack.utils.security.SSLUtils;
import org.apache.commons.collections.CollectionUtils;
import com.cloud.agent.api.Answer;
import com.cloud.agent.api.CancelCommand;
@ -106,8 +109,6 @@ import com.cloud.utils.nio.Link;
import com.cloud.utils.nio.Task;
import com.google.gson.Gson;
import org.apache.commons.collections.CollectionUtils;
public class ClusteredAgentManagerImpl extends AgentManagerImpl implements ClusterManagerListener, ClusteredAgentRebalanceService {
private static ScheduledExecutorService s_transferExecutor = Executors.newScheduledThreadPool(2, new NamedThreadFactory("Cluster-AgentRebalancingExecutor"));
private final long rebalanceTimeOut = 300000; // 5 mins - after this time remove the agent from the transfer list
@ -147,6 +148,8 @@ public class ClusteredAgentManagerImpl extends AgentManagerImpl implements Clust
private ManagementServerMaintenanceManager managementServerMaintenanceManager;
@Inject
private DataCenterDao dcDao;
@Inject
ExtensionsManager extensionsManager;
protected ClusteredAgentManagerImpl() {
super();
@ -1320,6 +1323,8 @@ public class ClusteredAgentManagerImpl extends AgentManagerImpl implements Clust
} else if (cmds.length == 1 && cmds[0] instanceof BaseShutdownManagementServerHostCommand) {
final BaseShutdownManagementServerHostCommand cmd = (BaseShutdownManagementServerHostCommand) cmds[0];
return handleShutdownManagementServerHostCommand(cmd);
} else if (cmds.length == 1 && cmds[0] instanceof ExtensionServerActionBaseCommand) {
return extensionsManager.handleExtensionServerCommands((ExtensionServerActionBaseCommand)cmds[0]);
}
try {

View File

@ -72,6 +72,9 @@ import org.apache.cloudstack.engine.subsystem.api.storage.VolumeDataFactory;
import org.apache.cloudstack.framework.ca.Certificate;
import org.apache.cloudstack.framework.config.ConfigKey;
import org.apache.cloudstack.framework.config.Configurable;
import org.apache.cloudstack.framework.extensions.dao.ExtensionDetailsDao;
import org.apache.cloudstack.framework.extensions.manager.ExtensionsManager;
import org.apache.cloudstack.framework.extensions.vo.ExtensionDetailsVO;
import org.apache.cloudstack.framework.jobs.AsyncJob;
import org.apache.cloudstack.framework.jobs.AsyncJobExecutionContext;
import org.apache.cloudstack.framework.jobs.AsyncJobManager;
@ -99,6 +102,7 @@ import org.apache.cloudstack.vm.UnmanagedVMsManager;
import org.apache.commons.collections.CollectionUtils;
import org.apache.commons.collections.MapUtils;
import org.apache.commons.lang.BooleanUtils;
import org.apache.commons.lang3.ObjectUtils;
import com.cloud.agent.AgentManager;
import com.cloud.agent.Listener;
@ -123,6 +127,8 @@ import com.cloud.agent.api.ModifyTargetsCommand;
import com.cloud.agent.api.PingRoutingCommand;
import com.cloud.agent.api.PlugNicAnswer;
import com.cloud.agent.api.PlugNicCommand;
import com.cloud.agent.api.PrepareExternalProvisioningAnswer;
import com.cloud.agent.api.PrepareExternalProvisioningCommand;
import com.cloud.agent.api.PrepareForMigrationAnswer;
import com.cloud.agent.api.PrepareForMigrationCommand;
import com.cloud.agent.api.RebootAnswer;
@ -203,12 +209,14 @@ import com.cloud.host.Host;
import com.cloud.host.HostVO;
import com.cloud.host.Status;
import com.cloud.host.dao.HostDao;
import com.cloud.host.dao.HostDetailsDao;
import com.cloud.hypervisor.Hypervisor.HypervisorType;
import com.cloud.hypervisor.HypervisorGuru;
import com.cloud.hypervisor.HypervisorGuruBase;
import com.cloud.hypervisor.HypervisorGuruManager;
import com.cloud.network.Network;
import com.cloud.network.NetworkModel;
import com.cloud.network.NetworkService;
import com.cloud.network.Networks;
import com.cloud.network.dao.NetworkDao;
import com.cloud.network.dao.NetworkDetailVO;
@ -335,6 +343,8 @@ public class VirtualMachineManagerImpl extends ManagerBase implements VirtualMac
@Inject
private HostDao _hostDao;
@Inject
private HostDetailsDao hostDetailsDao;
@Inject
private AlertManager _alertMgr;
@Inject
private GuestOSCategoryDao _guestOsCategoryDao;
@ -417,6 +427,8 @@ public class VirtualMachineManagerImpl extends ManagerBase implements VirtualMac
@Inject
private DomainDao domainDao;
@Inject
public NetworkService networkService;
@Inject
ResourceCleanupService resourceCleanupService;
@Inject
VmWorkJobDao vmWorkJobDao;
@ -433,6 +445,10 @@ public class VirtualMachineManagerImpl extends ManagerBase implements VirtualMac
@Inject
private VolumeDataFactory volumeDataFactory;
@Inject
ExtensionsManager extensionsManager;
@Inject
ExtensionDetailsDao extensionDetailsDao;
VmWorkJobHandlerProxy _jobHandlerProxy = new VmWorkJobHandlerProxy(this);
@ -594,8 +610,8 @@ public class VirtualMachineManagerImpl extends ManagerBase implements VirtualMac
if (template.getFormat() == ImageFormat.ISO) {
volumeMgr.allocateRawVolume(Type.ROOT, rootVolumeName, rootDiskOfferingInfo.getDiskOffering(), rootDiskOfferingInfo.getSize(),
rootDiskOfferingInfo.getMinIops(), rootDiskOfferingInfo.getMaxIops(), vm, template, owner, null);
} else if (template.getFormat() == ImageFormat.BAREMETAL) {
logger.debug("%s has format [{}]. Skipping ROOT volume [{}] allocation.", template.toString(), ImageFormat.BAREMETAL, rootVolumeName);
} else if (Arrays.asList(ImageFormat.BAREMETAL, ImageFormat.EXTERNAL).contains(template.getFormat())) {
logger.debug("{} has format [{}]. Skipping ROOT volume [{}] allocation.", template, template.getFormat(), rootVolumeName);
} else {
volumeMgr.allocateTemplatedVolumes(Type.ROOT, rootVolumeName, rootDiskOfferingInfo.getDiskOffering(), rootDiskSizeFinal,
rootDiskOfferingInfo.getMinIops(), rootDiskOfferingInfo.getMaxIops(), template, vm, owner, volume, snapshot);
@ -655,6 +671,13 @@ public class VirtualMachineManagerImpl extends ManagerBase implements VirtualMac
return;
}
if (HypervisorType.External.equals(vm.getHypervisorType())) {
UserVmVO userVM = _userVmDao.findById(vm.getId());
_userVmDao.loadDetails(userVM);
userVM.setDetail(VmDetailConstants.EXPUNGE_EXTERNAL_VM, Boolean.TRUE.toString());
_userVmDao.saveDetails(userVM);
}
advanceStop(vm.getUuid(), VmDestroyForcestop.value());
vm = _vmDao.findByUuid(vm.getUuid());
@ -1147,6 +1170,141 @@ public class VirtualMachineManagerImpl extends ManagerBase implements VirtualMac
vmTO.setMetadataProductName(metadataProduct);
}
protected void updateExternalVmDetailsFromPrepareAnswer(VirtualMachineTO vmTO, UserVmVO userVmVO,
Map<String, String> newDetails) {
if (newDetails == null || newDetails.equals(vmTO.getDetails())) {
return;
}
vmTO.setDetails(newDetails);
userVmVO.setDetails(newDetails);
_userVmDao.saveDetails(userVmVO);
}
protected void updateExternalVmDataFromPrepareAnswer(VirtualMachineTO vmTO, VirtualMachineTO updatedTO) {
final String vncPassword = updatedTO.getVncPassword();
final Map<String, String> details = updatedTO.getDetails();
if ((vncPassword == null || vncPassword.equals(vmTO.getVncPassword())) &&
(details == null || details.equals(vmTO.getDetails()))) {
return;
}
UserVmVO userVmVO = _userVmDao.findById(vmTO.getId());
if (userVmVO == null) {
return;
}
if (vncPassword != null && !vncPassword.equals(userVmVO.getPassword())) {
userVmVO.setVncPassword(vncPassword);
vmTO.setVncPassword(vncPassword);
}
updateExternalVmDetailsFromPrepareAnswer(vmTO, userVmVO, updatedTO.getDetails());
}
protected void updateExternalVmNicsFromPrepareAnswer(VirtualMachineTO vmTO, VirtualMachineTO updatedTO) {
if (ObjectUtils.anyNull(vmTO.getNics(), updatedTO.getNics())) {
return;
}
Map<String, NicTO> originalNicsByUuid = new HashMap<>();
for (NicTO nic : vmTO.getNics()) {
originalNicsByUuid.put(nic.getNicUuid(), nic);
}
for (NicTO updatedNicTO : updatedTO.getNics()) {
final String nicUuid = updatedNicTO.getNicUuid();
NicTO originalNicTO = originalNicsByUuid.get(nicUuid);
if (originalNicTO == null) {
continue;
}
final String mac = updatedNicTO.getMac();
final String ip4 = updatedNicTO.getIp();
final String ip6 = updatedNicTO.getIp6Address();
if (Objects.equals(mac, originalNicTO.getMac()) &&
Objects.equals(ip4, originalNicTO.getIp()) &&
Objects.equals(ip6, originalNicTO.getIp6Address())) {
continue;
}
NicVO nicVO = _nicsDao.findByUuid(nicUuid);
if (nicVO == null) {
continue;
}
logger.debug("Updating {} during External VM preparation", nicVO);
if (ip4 != null && !ip4.equals(nicVO.getIPv4Address())) {
nicVO.setIPv4Address(ip4);
originalNicTO.setIp(ip4);
}
if (ip6 != null && !ip6.equals(nicVO.getIPv6Address())) {
nicVO.setIPv6Address(ip6);
originalNicTO.setIp6Address(ip6);
}
if (mac != null && !mac.equals(nicVO.getMacAddress())) {
nicVO.setMacAddress(mac);
originalNicTO.setMac(mac);
}
_nicsDao.update(nicVO.getId(), nicVO);
}
}
protected void updateExternalVmFromPrepareAnswer(VirtualMachineTO vmTO, VirtualMachineTO updatedTO) {
if (updatedTO == null) {
return;
}
updateExternalVmDataFromPrepareAnswer(vmTO, updatedTO);
updateExternalVmNicsFromPrepareAnswer(vmTO, updatedTO);
return;
}
protected void processPrepareExternalProvisioning(boolean firstStart, Host host,
VirtualMachineProfile vmProfile, DataCenter dataCenter) throws CloudRuntimeException {
VirtualMachineTemplate template = vmProfile.getTemplate();
if (!firstStart || host == null || !HypervisorType.External.equals(host.getHypervisorType()) ||
template.getExtensionId() == null) {
return;
}
ExtensionDetailsVO detailsVO = extensionDetailsDao.findDetail(template.getExtensionId(),
ApiConstants.ORCHESTRATOR_REQUIRES_PREPARE_VM);
if (detailsVO == null || !Boolean.parseBoolean(detailsVO.getValue())) {
return;
}
logger.debug("Sending PrepareExternalProvisioningCommand for {}", vmProfile);
VirtualMachineTO virtualMachineTO = toVmTO(vmProfile);
if (virtualMachineTO.getNics() == null || virtualMachineTO.getNics().length == 0) {
List<NicVO> nics = _nicsDao.listByVmId(vmProfile.getId());
NicTO[] nicTOs = new NicTO[nics.size()];
nics.forEach(nicVO -> {
NicTO nicTO = toNicTO(_networkModel.getNicProfile(vmProfile.getVirtualMachine(), nicVO, dataCenter),
HypervisorType.External);
nicTOs[nicTO.getDeviceId()] = nicTO;
});
virtualMachineTO.setNics(nicTOs);
}
Map<String, String> vmDetails = virtualMachineTO.getExternalDetails();
Map<String, Map<String, String>> externalDetails = extensionsManager.getExternalAccessDetails(host,
vmDetails);
PrepareExternalProvisioningCommand cmd = new PrepareExternalProvisioningCommand(virtualMachineTO);
cmd.setExternalDetails(externalDetails);
Answer answer = null;
CloudRuntimeException cre = new CloudRuntimeException("Failed to prepare VM");
try {
answer = _agentMgr.send(host.getId(), cmd);
} catch (AgentUnavailableException | OperationTimedoutException e) {
logger.error("Failed PrepareExternalProvisioningCommand due to : {}", e.getMessage(), e);
throw cre;
}
if (answer == null) {
logger.error("Invalid answer received for PrepareExternalProvisioningCommand");
throw cre;
}
if (!(answer instanceof PrepareExternalProvisioningAnswer)) {
logger.error("Unexpected answer received for PrepareExternalProvisioningCommand: [result: {}, details: {}]",
answer.getResult(), answer.getDetails());
throw cre;
}
PrepareExternalProvisioningAnswer prepareAnswer = (PrepareExternalProvisioningAnswer)answer;
if (!prepareAnswer.getResult()) {
logger.error("Unexpected answer received for PrepareExternalProvisioningCommand: [result: {}, details: {}]",
answer.getResult(), answer.getDetails());
throw cre;
}
updateExternalVmFromPrepareAnswer(virtualMachineTO, prepareAnswer.getVirtualMachineTO());
}
@Override
public void orchestrateStart(final String vmUuid, final Map<VirtualMachineProfile.Param, Object> params, final DeploymentPlan planToDeploy, final DeploymentPlanner planner)
throws InsufficientCapacityException, ConcurrentOperationException, ResourceUnavailableException {
@ -1158,6 +1316,8 @@ public class VirtualMachineManagerImpl extends ManagerBase implements VirtualMac
VMInstanceVO vm = _vmDao.findByUuid(vmUuid);
final boolean firstStart = vm.getUpdated() == 0;
final VirtualMachineGuru vmGuru = getVmGuru(vm);
final Account owner = _entityMgr.findById(Account.class, vm.getAccountId());
@ -1257,7 +1417,7 @@ public class VirtualMachineManagerImpl extends ManagerBase implements VirtualMac
}
}
final VirtualMachineProfileImpl vmProfile = new VirtualMachineProfileImpl(vm, template, offering, owner, params);
VirtualMachineProfileImpl vmProfile = new VirtualMachineProfileImpl(vm, template, offering, owner, params);
logBootModeParameters(params);
DeployDestination dest = null;
try {
@ -1300,8 +1460,11 @@ public class VirtualMachineManagerImpl extends ManagerBase implements VirtualMac
try {
resetVmNicsDeviceId(vm.getId());
processPrepareExternalProvisioning(firstStart, dest.getHost(), vmProfile, dest.getDataCenter());
_networkMgr.prepare(vmProfile, dest, ctx);
if (vm.getHypervisorType() != HypervisorType.BareMetal) {
if (vm.getHypervisorType() != HypervisorType.BareMetal && vm.getHypervisorType() != HypervisorType.External) {
checkAndAttemptMigrateVmAcrossCluster(vm, clusterId, dest.getStorageForDisks());
volumeMgr.prepare(vmProfile, dest);
}
@ -1320,13 +1483,13 @@ public class VirtualMachineManagerImpl extends ManagerBase implements VirtualMac
handlePath(vmTO.getDisks(), vm.getHypervisorType());
setVmNetworkDetails(vm, vmTO);
Commands cmds = new Commands(Command.OnError.Stop);
final Map<String, String> sshAccessDetails = _networkMgr.getSystemVMAccessDetails(vm);
final Map<String, String> ipAddressDetails = new HashMap<>(sshAccessDetails);
ipAddressDetails.remove(NetworkElementCommand.ROUTER_NAME);
StartCommand command = new StartCommand(vmTO, dest.getHost(), getExecuteInSequence(vm.getHypervisorType()));
updateStartCommandWithExternalDetails(dest.getHost(), vmTO, command);
cmds.addCommand(command);
vmGuru.finalizeDeployment(cmds, vmProfile, dest, ctx);
@ -1499,6 +1662,53 @@ public class VirtualMachineManagerImpl extends ManagerBase implements VirtualMac
}
}
protected void updateStartCommandWithExternalDetails(Host host, VirtualMachineTO vmTO, StartCommand command) {
if (!HypervisorType.External.equals(host.getHypervisorType())) {
return;
}
Map<String, String> vmExternalDetails = vmTO.getExternalDetails();
for (NicTO nic : vmTO.getNics()) {
if (!nic.isDefaultNic()) {
continue;
}
vmExternalDetails.put(VmDetailConstants.CLOUDSTACK_VLAN, networkService.getNicVlanValueForExternalVm(nic));
}
Map<String, Map<String, String>> externalDetails = extensionsManager.getExternalAccessDetails(host, vmExternalDetails);
command.setExternalDetails(externalDetails);
}
protected void updateStopCommandForExternalHypervisorType(final HypervisorType hypervisorType,
final VirtualMachineProfile vmProfile, final StopCommand stopCommand) {
if (!HypervisorType.External.equals(hypervisorType) || vmProfile.getHostId() == null) {
return;
}
Host host = _hostDao.findById(vmProfile.getHostId());
if (host == null) {
return;
}
VirtualMachineTO vmTO = ObjectUtils.defaultIfNull(stopCommand.getVirtualMachine(), toVmTO(vmProfile));
if (MapUtils.isEmpty(vmTO.getGuestOsDetails())) {
vmTO.setGuestOsDetails(null);
}
if (MapUtils.isEmpty(vmTO.getExtraConfig())) {
vmTO.setExtraConfig(null);
}
if (MapUtils.isEmpty(vmTO.getNetworkIdToNetworkNameMap())) {
vmTO.setNetworkIdToNetworkNameMap(null);
}
Map<String, Map<String, String>> externalDetails = extensionsManager.getExternalAccessDetails(host, vmTO.getExternalDetails());
stopCommand.setVirtualMachine(vmTO);
stopCommand.setExternalDetails(externalDetails);
}
protected void updateRebootCommandWithExternalDetails(Host host, VirtualMachineTO vmTO, RebootCommand rebootCmd) {
if (!HypervisorType.External.equals(host.getHypervisorType())) {
return;
}
Map<String, Map<String, String>> externalDetails = extensionsManager.getExternalAccessDetails(host, vmTO.getExternalDetails());
rebootCmd.setExternalDetails(externalDetails);
}
public void setVmNetworkDetails(VMInstanceVO vm, VirtualMachineTO vmTO) {
Map<Long, String> networkToNetworkNameMap = new HashMap<>();
if (VirtualMachine.Type.User.equals(vm.getType())) {
@ -1886,7 +2096,9 @@ public class VirtualMachineManagerImpl extends ManagerBase implements VirtualMac
protected boolean sendStop(final VirtualMachineGuru guru, final VirtualMachineProfile profile, final boolean force, final boolean checkBeforeCleanup) {
final VirtualMachine vm = profile.getVirtualMachine();
Map<String, Boolean> vlanToPersistenceMap = getVlanToPersistenceMapForVM(vm.getId());
StopCommand stpCmd = new StopCommand(vm, getExecuteInSequence(vm.getHypervisorType()), checkBeforeCleanup);
updateStopCommandForExternalHypervisorType(vm.getHypervisorType(), profile, stpCmd);
if (MapUtils.isNotEmpty(vlanToPersistenceMap)) {
stpCmd.setVlanToPersistenceMap(vlanToPersistenceMap);
}
@ -2017,7 +2229,7 @@ public class VirtualMachineManagerImpl extends ManagerBase implements VirtualMac
}
try {
if (vm.getHypervisorType() != HypervisorType.BareMetal) {
if (vm.getHypervisorType() != HypervisorType.BareMetal && vm.getHypervisorType() != HypervisorType.External) {
volumeMgr.release(profile);
logger.debug("Successfully released storage resources for the VM {} in {} state", vm, state);
}
@ -2214,6 +2426,7 @@ public class VirtualMachineManagerImpl extends ManagerBase implements VirtualMac
Map<String, Boolean> vlanToPersistenceMap = getVlanToPersistenceMapForVM(vm.getId());
final StopCommand stop = new StopCommand(vm, getExecuteInSequence(vm.getHypervisorType()), false, cleanUpEvenIfUnableToStop);
stop.setControlIp(getControlNicIpForVM(vm));
updateStopCommandForExternalHypervisorType(vm.getHypervisorType(), profile, stop);
if (MapUtils.isNotEmpty(vlanToPersistenceMap)) {
stop.setVlanToPersistenceMap(vlanToPersistenceMap);
}
@ -2263,6 +2476,14 @@ public class VirtualMachineManagerImpl extends ManagerBase implements VirtualMac
} else {
logger.warn("Unable to actually stop {} but continue with release because it's a force stop", vm);
vmGuru.finalizeStop(profile, answer);
if (HypervisorType.External.equals(profile.getHypervisorType())) {
try {
stateTransitTo(vm, VirtualMachine.Event.OperationSucceeded, null);
} catch (final NoTransitionException e) {
logger.warn("Unable to transition the state " + vm, e);
}
}
}
} else {
if (VirtualMachine.systemVMs.contains(vm.getType())) {
@ -3730,6 +3951,7 @@ public class VirtualMachineManagerImpl extends ManagerBase implements VirtualMac
VirtualMachineTO vmTo = getVmTO(vm.getId());
checkAndSetEnterSetupMode(vmTo, params);
rebootCmd.setVirtualMachine(vmTo);
updateRebootCommandWithExternalDetails(host, vmTo, rebootCmd);
cmds.addCommand(rebootCmd);
_agentMgr.send(host.getId(), cmds);

View File

@ -16,6 +16,8 @@
// under the License.
package org.apache.cloudstack.engine.orchestration;
import static com.cloud.configuration.ConfigurationManager.MESSAGE_DELETE_VLAN_IP_RANGE_EVENT;
import java.net.URI;
import java.util.ArrayList;
import java.util.Arrays;
@ -38,14 +40,6 @@ import java.util.stream.Collectors;
import javax.inject.Inject;
import javax.naming.ConfigurationException;
import com.cloud.dc.ASNumberVO;
import com.cloud.bgp.BGPService;
import com.cloud.dc.VlanDetailsVO;
import com.cloud.dc.dao.ASNumberDao;
import com.cloud.dc.dao.VlanDetailsDao;
import com.cloud.network.dao.Ipv6GuestPrefixSubnetNetworkMapDao;
import com.cloud.network.dao.NetrisProviderDao;
import com.cloud.network.dao.NsxProviderDao;
import org.apache.cloudstack.acl.ControlledEntity.ACLType;
import org.apache.cloudstack.annotation.AnnotationService;
import org.apache.cloudstack.annotation.dao.AnnotationDao;
@ -67,6 +61,7 @@ import org.apache.commons.collections.CollectionUtils;
import org.apache.commons.lang3.BooleanUtils;
import org.apache.commons.lang3.ObjectUtils;
import org.apache.commons.lang3.StringUtils;
import org.jetbrains.annotations.NotNull;
import com.cloud.agent.AgentManager;
import com.cloud.agent.Listener;
@ -88,8 +83,10 @@ import com.cloud.agent.api.to.deployasis.OVFNetworkTO;
import com.cloud.alert.AlertManager;
import com.cloud.api.query.dao.DomainRouterJoinDao;
import com.cloud.api.query.vo.DomainRouterJoinVO;
import com.cloud.bgp.BGPService;
import com.cloud.configuration.ConfigurationManager;
import com.cloud.configuration.Resource.ResourceType;
import com.cloud.dc.ASNumberVO;
import com.cloud.dc.ClusterVO;
import com.cloud.dc.DataCenter;
import com.cloud.dc.DataCenter.NetworkType;
@ -97,12 +94,15 @@ import com.cloud.dc.DataCenterVO;
import com.cloud.dc.DataCenterVnetVO;
import com.cloud.dc.PodVlanMapVO;
import com.cloud.dc.Vlan;
import com.cloud.dc.VlanDetailsVO;
import com.cloud.dc.VlanVO;
import com.cloud.dc.dao.ASNumberDao;
import com.cloud.dc.dao.ClusterDao;
import com.cloud.dc.dao.DataCenterDao;
import com.cloud.dc.dao.DataCenterVnetDao;
import com.cloud.dc.dao.PodVlanMapDao;
import com.cloud.dc.dao.VlanDao;
import com.cloud.dc.dao.VlanDetailsDao;
import com.cloud.deploy.DataCenterDeployment;
import com.cloud.deploy.DeployDestination;
import com.cloud.deploy.DeploymentPlan;
@ -153,6 +153,8 @@ import com.cloud.network.dao.AccountGuestVlanMapVO;
import com.cloud.network.dao.FirewallRulesDao;
import com.cloud.network.dao.IPAddressDao;
import com.cloud.network.dao.IPAddressVO;
import com.cloud.network.dao.Ipv6GuestPrefixSubnetNetworkMapDao;
import com.cloud.network.dao.NetrisProviderDao;
import com.cloud.network.dao.NetworkAccountDao;
import com.cloud.network.dao.NetworkAccountVO;
import com.cloud.network.dao.NetworkDao;
@ -163,6 +165,7 @@ import com.cloud.network.dao.NetworkDomainVO;
import com.cloud.network.dao.NetworkServiceMapDao;
import com.cloud.network.dao.NetworkServiceMapVO;
import com.cloud.network.dao.NetworkVO;
import com.cloud.network.dao.NsxProviderDao;
import com.cloud.network.dao.PhysicalNetworkDao;
import com.cloud.network.dao.PhysicalNetworkServiceProviderDao;
import com.cloud.network.dao.PhysicalNetworkTrafficTypeDao;
@ -250,8 +253,8 @@ import com.cloud.vm.UserVmManager;
import com.cloud.vm.UserVmVO;
import com.cloud.vm.VMInstanceVO;
import com.cloud.vm.VirtualMachine;
import com.cloud.vm.VirtualMachineManager;
import com.cloud.vm.VirtualMachine.Type;
import com.cloud.vm.VirtualMachineManager;
import com.cloud.vm.VirtualMachineProfile;
import com.cloud.vm.dao.DomainRouterDao;
import com.cloud.vm.dao.NicDao;
@ -263,9 +266,6 @@ import com.cloud.vm.dao.NicSecondaryIpVO;
import com.cloud.vm.dao.UserVmDao;
import com.cloud.vm.dao.VMInstanceDao;
import com.googlecode.ipv6.IPv6Address;
import org.jetbrains.annotations.NotNull;
import static com.cloud.configuration.ConfigurationManager.MESSAGE_DELETE_VLAN_IP_RANGE_EVENT;
/**
* NetworkManagerImpl implements NetworkManager.

View File

@ -23,11 +23,14 @@ import static org.junit.Assert.assertNull;
import static org.junit.Assert.assertTrue;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.anyLong;
import static org.mockito.ArgumentMatchers.anyString;
import static org.mockito.ArgumentMatchers.eq;
import static org.mockito.Mockito.doNothing;
import static org.mockito.Mockito.doReturn;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.never;
import static org.mockito.Mockito.times;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;
import java.lang.reflect.Field;
@ -40,10 +43,14 @@ import java.util.Random;
import java.util.UUID;
import java.util.stream.Collectors;
import org.apache.cloudstack.api.ApiConstants;
import org.apache.cloudstack.context.CallContext;
import org.apache.cloudstack.engine.subsystem.api.storage.StoragePoolAllocator;
import org.apache.cloudstack.framework.config.ConfigKey;
import org.apache.cloudstack.framework.config.impl.ConfigDepotImpl;
import org.apache.cloudstack.framework.extensions.dao.ExtensionDetailsDao;
import org.apache.cloudstack.framework.extensions.manager.ExtensionsManager;
import org.apache.cloudstack.framework.extensions.vo.ExtensionDetailsVO;
import org.apache.cloudstack.storage.datastore.db.PrimaryDataStoreDao;
import org.apache.cloudstack.storage.datastore.db.StoragePoolVO;
import org.apache.cloudstack.storage.to.VolumeObjectTO;
@ -65,15 +72,20 @@ import org.springframework.test.util.ReflectionTestUtils;
import com.cloud.agent.AgentManager;
import com.cloud.agent.api.Command;
import com.cloud.agent.api.PrepareExternalProvisioningAnswer;
import com.cloud.agent.api.RebootCommand;
import com.cloud.agent.api.StartCommand;
import com.cloud.agent.api.StopAnswer;
import com.cloud.agent.api.StopCommand;
import com.cloud.agent.api.routing.NetworkElementCommand;
import com.cloud.agent.api.to.NicTO;
import com.cloud.agent.api.to.VirtualMachineTO;
import com.cloud.api.query.dao.UserVmJoinDao;
import com.cloud.api.query.vo.UserVmJoinVO;
import com.cloud.dc.ClusterDetailsDao;
import com.cloud.dc.ClusterDetailsVO;
import com.cloud.dc.ClusterVO;
import com.cloud.dc.DataCenter;
import com.cloud.dc.DataCenterVO;
import com.cloud.dc.Pod;
import com.cloud.dc.dao.ClusterDao;
@ -94,6 +106,7 @@ import com.cloud.host.HostVO;
import com.cloud.host.dao.HostDao;
import com.cloud.hypervisor.Hypervisor.HypervisorType;
import com.cloud.hypervisor.HypervisorGuruManager;
import com.cloud.network.NetworkService;
import com.cloud.network.dao.NetworkDao;
import com.cloud.network.dao.NetworkVO;
import com.cloud.network.vpc.VpcVO;
@ -130,6 +143,7 @@ import com.cloud.utils.db.EntityManager;
import com.cloud.utils.exception.CloudRuntimeException;
import com.cloud.utils.fsm.StateMachine2;
import com.cloud.vm.VirtualMachine.State;
import com.cloud.vm.dao.NicDao;
import com.cloud.vm.dao.UserVmDao;
import com.cloud.vm.dao.VMInstanceDetailsDao;
import com.cloud.vm.dao.VMInstanceDao;
@ -229,6 +243,14 @@ public class VirtualMachineManagerImplTest {
private ItWorkDao _workDao;
@Mock
protected StateMachine2<State, VirtualMachine.Event, VirtualMachine> _stateMachine;
@Mock
ExtensionsManager extensionsManager;
@Mock
ExtensionDetailsDao extensionDetailsDao;
@Mock
NicDao _nicsDao;
@Mock
NetworkService networkService;
private ConfigDepotImpl configDepotImpl;
private boolean updatedConfigKeyDepot = false;
@ -458,8 +480,8 @@ public class VirtualMachineManagerImplTest {
virtualMachineManagerImpl.executeManagedStorageChecksWhenTargetStoragePoolProvided(storagePoolVoMock, volumeVoMock, Mockito.mock(StoragePoolVO.class));
Mockito.verify(storagePoolVoMock).isManaged();
Mockito.verify(storagePoolVoMock, Mockito.times(0)).getId();
verify(storagePoolVoMock).isManaged();
verify(storagePoolVoMock, Mockito.times(0)).getId();
}
@Test
@ -469,8 +491,8 @@ public class VirtualMachineManagerImplTest {
virtualMachineManagerImpl.executeManagedStorageChecksWhenTargetStoragePoolProvided(storagePoolVoMock, volumeVoMock, Mockito.mock(StoragePoolVO.class));
Mockito.verify(storagePoolVoMock).isManaged();
Mockito.verify(storagePoolVoMock, Mockito.times(0)).getId();
verify(storagePoolVoMock).isManaged();
verify(storagePoolVoMock, Mockito.times(0)).getId();
}
@Test
@ -485,8 +507,8 @@ public class VirtualMachineManagerImplTest {
virtualMachineManagerImpl.executeManagedStorageChecksWhenTargetStoragePoolProvided(storagePoolVoMock, volumeVoMock, storagePoolVoMock);
Mockito.verify(storagePoolVoMock).isManaged();
Mockito.verify(storagePoolVoMock, Mockito.times(2)).getId();
verify(storagePoolVoMock).isManaged();
verify(storagePoolVoMock, Mockito.times(2)).getId();
}
@Test(expected = CloudRuntimeException.class)
@ -510,7 +532,7 @@ public class VirtualMachineManagerImplTest {
Assert.assertTrue(volumeToPoolObjectMap.isEmpty());
Mockito.verify(userDefinedVolumeToStoragePoolMap, times(0)).keySet();
verify(userDefinedVolumeToStoragePoolMap, times(0)).keySet();
}
@Test(expected = CloudRuntimeException.class)
@ -539,7 +561,7 @@ public class VirtualMachineManagerImplTest {
assertFalse(volumeToPoolObjectMap.isEmpty());
assertEquals(storagePoolVoMock, volumeToPoolObjectMap.get(volumeVoMock));
Mockito.verify(userDefinedVolumeToStoragePoolMap, times(1)).keySet();
verify(userDefinedVolumeToStoragePoolMap, times(1)).keySet();
}
@Test
@ -566,8 +588,8 @@ public class VirtualMachineManagerImplTest {
virtualMachineManagerImpl.executeManagedStorageChecksWhenTargetStoragePoolNotProvided(hostMock, storagePoolVoMock, volumeVoMock);
Mockito.verify(storagePoolVoMock).isManaged();
Mockito.verify(storagePoolHostDaoMock, Mockito.times(0)).findByPoolHost(anyLong(), anyLong());
verify(storagePoolVoMock).isManaged();
verify(storagePoolHostDaoMock, Mockito.times(0)).findByPoolHost(anyLong(), anyLong());
}
@Test
@ -577,8 +599,8 @@ public class VirtualMachineManagerImplTest {
virtualMachineManagerImpl.executeManagedStorageChecksWhenTargetStoragePoolNotProvided(hostMock, storagePoolVoMock, volumeVoMock);
Mockito.verify(storagePoolVoMock).isManaged();
Mockito.verify(storagePoolHostDaoMock, Mockito.times(1)).findByPoolHost(storagePoolVoMockId, hostMockId);
verify(storagePoolVoMock).isManaged();
verify(storagePoolHostDaoMock, Mockito.times(1)).findByPoolHost(storagePoolVoMockId, hostMockId);
}
@Test(expected = CloudRuntimeException.class)
@ -677,11 +699,11 @@ public class VirtualMachineManagerImplTest {
Assert.assertTrue(poolList.isEmpty());
Mockito.verify(storagePoolAllocatorMock).allocateToPool(any(DiskProfile.class), any(VirtualMachineProfile.class), any(DeploymentPlan.class),
verify(storagePoolAllocatorMock).allocateToPool(any(DiskProfile.class), any(VirtualMachineProfile.class), any(DeploymentPlan.class),
any(ExcludeList.class), Mockito.eq(StoragePoolAllocator.RETURN_UPTO_ALL));
Mockito.verify(storagePoolAllocatorMock2).allocateToPool(any(DiskProfile.class), any(VirtualMachineProfile.class), any(DeploymentPlan.class),
verify(storagePoolAllocatorMock2).allocateToPool(any(DiskProfile.class), any(VirtualMachineProfile.class), any(DeploymentPlan.class),
any(ExcludeList.class), Mockito.eq(StoragePoolAllocator.RETURN_UPTO_ALL));
Mockito.verify(storagePoolAllocatorMock3).allocateToPool(any(DiskProfile.class), any(VirtualMachineProfile.class), any(DeploymentPlan.class),
verify(storagePoolAllocatorMock3).allocateToPool(any(DiskProfile.class), any(VirtualMachineProfile.class), any(DeploymentPlan.class),
any(ExcludeList.class), Mockito.eq(StoragePoolAllocator.RETURN_UPTO_ALL));
}
@ -739,8 +761,8 @@ public class VirtualMachineManagerImplTest {
virtualMachineManagerImpl.createStoragePoolMappingsForVolumes(virtualMachineProfileMock, dataCenterDeploymentMock, volumeToPoolObjectMap, allVolumes);
Assert.assertTrue(volumeToPoolObjectMap.isEmpty());
Mockito.verify(virtualMachineManagerImpl).executeManagedStorageChecksWhenTargetStoragePoolNotProvided(hostMock, storagePoolVoMock, volumeVoMock);
Mockito.verify(virtualMachineManagerImpl).createVolumeToStoragePoolMappingIfPossible(virtualMachineProfileMock, dataCenterDeploymentMock, volumeToPoolObjectMap, volumeVoMock, storagePoolVoMock);
verify(virtualMachineManagerImpl).executeManagedStorageChecksWhenTargetStoragePoolNotProvided(hostMock, storagePoolVoMock, volumeVoMock);
verify(virtualMachineManagerImpl).createVolumeToStoragePoolMappingIfPossible(virtualMachineProfileMock, dataCenterDeploymentMock, volumeToPoolObjectMap, volumeVoMock, storagePoolVoMock);
}
@Test
@ -758,9 +780,9 @@ public class VirtualMachineManagerImplTest {
virtualMachineManagerImpl.createStoragePoolMappingsForVolumes(virtualMachineProfileMock, dataCenterDeploymentMock, volumeToPoolObjectMap, allVolumes);
Assert.assertTrue(volumeToPoolObjectMap.isEmpty());
Mockito.verify(virtualMachineManagerImpl).executeManagedStorageChecksWhenTargetStoragePoolNotProvided(hostMock, storagePoolVoMock, volumeVoMock);
Mockito.verify(virtualMachineManagerImpl).createVolumeToStoragePoolMappingIfPossible(virtualMachineProfileMock, dataCenterDeploymentMock, volumeToPoolObjectMap, volumeVoMock, storagePoolVoMock);
Mockito.verify(virtualMachineManagerImpl).isStorageCrossClusterMigration(clusterMockId, storagePoolVoMock);
verify(virtualMachineManagerImpl).executeManagedStorageChecksWhenTargetStoragePoolNotProvided(hostMock, storagePoolVoMock, volumeVoMock);
verify(virtualMachineManagerImpl).createVolumeToStoragePoolMappingIfPossible(virtualMachineProfileMock, dataCenterDeploymentMock, volumeToPoolObjectMap, volumeVoMock, storagePoolVoMock);
verify(virtualMachineManagerImpl).isStorageCrossClusterMigration(clusterMockId, storagePoolVoMock);
}
@Test
@ -779,9 +801,9 @@ public class VirtualMachineManagerImplTest {
assertFalse(volumeToPoolObjectMap.isEmpty());
assertEquals(storagePoolVoMock, volumeToPoolObjectMap.get(volumeVoMock));
Mockito.verify(virtualMachineManagerImpl).executeManagedStorageChecksWhenTargetStoragePoolNotProvided(hostMock, storagePoolVoMock, volumeVoMock);
Mockito.verify(virtualMachineManagerImpl).isStorageCrossClusterMigration(clusterMockId, storagePoolVoMock);
Mockito.verify(virtualMachineManagerImpl, Mockito.times(0)).createVolumeToStoragePoolMappingIfPossible(virtualMachineProfileMock, dataCenterDeploymentMock, volumeToPoolObjectMap, volumeVoMock,
verify(virtualMachineManagerImpl).executeManagedStorageChecksWhenTargetStoragePoolNotProvided(hostMock, storagePoolVoMock, volumeVoMock);
verify(virtualMachineManagerImpl).isStorageCrossClusterMigration(clusterMockId, storagePoolVoMock);
verify(virtualMachineManagerImpl, Mockito.times(0)).createVolumeToStoragePoolMappingIfPossible(virtualMachineProfileMock, dataCenterDeploymentMock, volumeToPoolObjectMap, volumeVoMock,
storagePoolVoMock);
}
@ -1318,7 +1340,7 @@ public class VirtualMachineManagerImplTest {
virtualMachineManagerImpl.recreateCheckpointsKvmOnVmAfterMigration(vmInstanceMock, 0);
Mockito.verify(volumeDaoMock, Mockito.never()).findByInstance(Mockito.anyLong());
verify(volumeDaoMock, never()).findByInstance(Mockito.anyLong());
}
@Test
@ -1328,7 +1350,7 @@ public class VirtualMachineManagerImplTest {
virtualMachineManagerImpl.recreateCheckpointsKvmOnVmAfterMigration(vmInstanceMock, 0);
Mockito.verify(agentManagerMock, Mockito.never()).send(Mockito.anyLong(), (Command) any());
verify(agentManagerMock, never()).send(Mockito.anyLong(), (Command) any());
}
@Test (expected = CloudRuntimeException.class)
@ -1341,7 +1363,7 @@ public class VirtualMachineManagerImplTest {
virtualMachineManagerImpl.recreateCheckpointsKvmOnVmAfterMigration(vmInstanceMock, 0);
Mockito.verify(snapshotManagerMock, Mockito.times(1)).endSnapshotChainForVolume(Mockito.anyLong(),any());
verify(snapshotManagerMock, Mockito.times(1)).endSnapshotChainForVolume(Mockito.anyLong(),any());
}
@Test (expected = CloudRuntimeException.class)
@ -1354,7 +1376,7 @@ public class VirtualMachineManagerImplTest {
virtualMachineManagerImpl.recreateCheckpointsKvmOnVmAfterMigration(vmInstanceMock, 0);
Mockito.verify(snapshotManagerMock, Mockito.times(1)).endSnapshotChainForVolume(Mockito.anyLong(),any());
verify(snapshotManagerMock, Mockito.times(1)).endSnapshotChainForVolume(Mockito.anyLong(),any());
}
@Test
@ -1367,7 +1389,7 @@ public class VirtualMachineManagerImplTest {
virtualMachineManagerImpl.recreateCheckpointsKvmOnVmAfterMigration(vmInstanceMock, 0);
Mockito.verify(snapshotManagerMock, Mockito.times(1)).endSnapshotChainForVolume(Mockito.anyLong(),any());
verify(snapshotManagerMock, Mockito.times(1)).endSnapshotChainForVolume(Mockito.anyLong(),any());
}
@Test
@ -1379,6 +1401,241 @@ public class VirtualMachineManagerImplTest {
virtualMachineManagerImpl.recreateCheckpointsKvmOnVmAfterMigration(vmInstanceMock, 0);
Mockito.verify(snapshotManagerMock, Mockito.never()).endSnapshotChainForVolume(Mockito.anyLong(),any());
verify(snapshotManagerMock, never()).endSnapshotChainForVolume(Mockito.anyLong(),any());
}
@Test
public void updateStartCommandWithExternalDetails_nonExternalHypervisor_noAction() {
Host host = mock(Host.class);
VirtualMachineTO vmTO = mock(VirtualMachineTO.class);
StartCommand command = mock(StartCommand.class);
when(host.getHypervisorType()).thenReturn(HypervisorType.KVM);
virtualMachineManagerImpl.updateStartCommandWithExternalDetails(host, vmTO, command);
verify(command, never()).setExternalDetails(any());
}
@Test
public void updateStartCommandWithExternalDetails_externalHypervisor_setsExternalDetails() {
Host host = mock(Host.class);
VirtualMachineTO vmTO = mock(VirtualMachineTO.class);
StartCommand command = mock(StartCommand.class);
NicTO nic = mock(NicTO.class);
when(host.getHypervisorType()).thenReturn(HypervisorType.External);
when(vmTO.getExternalDetails()).thenReturn(new HashMap<>());
when(vmTO.getNics()).thenReturn(new NicTO[]{nic});
when(nic.isDefaultNic()).thenReturn(true);
when(networkService.getNicVlanValueForExternalVm(nic)).thenReturn("segmentName");
when(extensionsManager.getExternalAccessDetails(eq(host), any())).thenReturn(new HashMap<>());
virtualMachineManagerImpl.updateStartCommandWithExternalDetails(host, vmTO, command);
verify(command).setExternalDetails(any());
}
@Test
public void updateStopCommandForExternalHypervisorType_nonExternalHypervisor_noAction() {
VirtualMachineProfile vmProfile = mock(VirtualMachineProfile.class);
StopCommand stopCommand = mock(StopCommand.class);
virtualMachineManagerImpl.updateStopCommandForExternalHypervisorType(HypervisorType.KVM, vmProfile, stopCommand);
verify(stopCommand, never()).setExternalDetails(any());
}
@Test
public void updateStopCommandForExternalHypervisorType_externalHypervisor_setsExternalDetails() {
VirtualMachineProfile vmProfile = mock(VirtualMachineProfile.class);
StopCommand stopCommand = mock(StopCommand.class);
HostVO host = mock(HostVO.class);
VirtualMachineTO vmTO = mock(VirtualMachineTO.class);
when(vmProfile.getHostId()).thenReturn(1L);
when(hostDaoMock.findById(1L)).thenReturn(host);
when(stopCommand.getVirtualMachine()).thenReturn(vmTO);
when(vmTO.getExternalDetails()).thenReturn(new HashMap<>());
when(extensionsManager.getExternalAccessDetails(eq(host), any())).thenReturn(new HashMap<>());
doReturn(mock(VirtualMachineTO.class)).when(virtualMachineManagerImpl).toVmTO(any());
virtualMachineManagerImpl.updateStopCommandForExternalHypervisorType(HypervisorType.External, vmProfile, stopCommand);
verify(stopCommand).setExternalDetails(any());
}
@Test
public void updateRebootCommandWithExternalDetails_nonExternalHypervisor_noAction() {
Host host = mock(Host.class);
VirtualMachineTO vmTO = mock(VirtualMachineTO.class);
RebootCommand rebootCmd = mock(RebootCommand.class);
when(host.getHypervisorType()).thenReturn(HypervisorType.KVM);
virtualMachineManagerImpl.updateRebootCommandWithExternalDetails(host, vmTO, rebootCmd);
verify(rebootCmd, never()).setExternalDetails(any());
}
@Test
public void updateRebootCommandWithExternalDetails_externalHypervisor_setsExternalDetails() {
Host host = mock(Host.class);
VirtualMachineTO vmTO = mock(VirtualMachineTO.class);
RebootCommand rebootCmd = mock(RebootCommand.class);
when(host.getHypervisorType()).thenReturn(HypervisorType.External);
when(vmTO.getExternalDetails()).thenReturn(new HashMap<>());
when(extensionsManager.getExternalAccessDetails(eq(host), any())).thenReturn(new HashMap<>());
virtualMachineManagerImpl.updateRebootCommandWithExternalDetails(host, vmTO, rebootCmd);
verify(rebootCmd).setExternalDetails(any());
}
@Test
public void updateExternalVmDetailsFromPrepareAnswer_nullDetails_noAction() {
VirtualMachineTO vmTO = mock(VirtualMachineTO.class);
UserVmVO userVmVO = mock(UserVmVO.class);
virtualMachineManagerImpl.updateExternalVmDetailsFromPrepareAnswer(vmTO, userVmVO, null);
verify(vmTO, never()).setDetails(any());
verify(userVmVO, never()).setDetails(any());
verify(userVmDaoMock, never()).saveDetails(any());
}
@Test
public void updateExternalVmDetailsFromPrepareAnswer_sameDetails_noAction() {
VirtualMachineTO vmTO = mock(VirtualMachineTO.class);
UserVmVO userVmVO = mock(UserVmVO.class);
Map<String, String> details = new HashMap<>();
when(vmTO.getDetails()).thenReturn(details);
virtualMachineManagerImpl.updateExternalVmDetailsFromPrepareAnswer(vmTO, userVmVO, details);
verify(vmTO, never()).setDetails(any());
verify(userVmVO, never()).setDetails(any());
verify(userVmDaoMock, never()).saveDetails(any());
}
@Test
public void updateExternalVmDataFromPrepareAnswer_vncPasswordUpdated_updatesPassword() {
VirtualMachineTO vmTO = mock(VirtualMachineTO.class);
VirtualMachineTO updatedTO = mock(VirtualMachineTO.class);
UserVmVO userVmVO = mock(UserVmVO.class);
when(updatedTO.getVncPassword()).thenReturn("newPassword");
when(vmTO.getVncPassword()).thenReturn("oldPassword");
when(userVmDaoMock.findById(anyLong())).thenReturn(userVmVO);
virtualMachineManagerImpl.updateExternalVmDataFromPrepareAnswer(vmTO, updatedTO);
verify(userVmVO).setVncPassword("newPassword");
verify(vmTO).setVncPassword("newPassword");
}
@Test
public void updateExternalVmNicsFromPrepareAnswer_nullNics_noAction() {
VirtualMachineTO vmTO = mock(VirtualMachineTO.class);
VirtualMachineTO updatedTO = mock(VirtualMachineTO.class);
when(vmTO.getNics()).thenReturn(null);
virtualMachineManagerImpl.updateExternalVmNicsFromPrepareAnswer(vmTO, updatedTO);
verify(_nicsDao, never()).findByUuid(anyString());
}
@Test
public void updateExternalVmNicsFromPrepareAnswer_updatesNicsSuccessfully() {
VirtualMachineTO vmTO = mock(VirtualMachineTO.class);
VirtualMachineTO updatedTO = mock(VirtualMachineTO.class);
NicTO nicTO = mock(NicTO.class);
NicTO updatedNicTO = mock(NicTO.class);
when(vmTO.getNics()).thenReturn(new NicTO[]{nicTO});
when(updatedTO.getNics()).thenReturn(new NicTO[]{updatedNicTO});
when(nicTO.getNicUuid()).thenReturn("nic-uuid");
when(nicTO.getMac()).thenReturn("mac-a");
when(updatedNicTO.getNicUuid()).thenReturn("nic-uuid");
when(updatedNicTO.getMac()).thenReturn("mac-b");
when(_nicsDao.findByUuid("nic-uuid")).thenReturn(mock(NicVO.class));
virtualMachineManagerImpl.updateExternalVmNicsFromPrepareAnswer(vmTO, updatedTO);
verify(_nicsDao).findByUuid("nic-uuid");
verify(_nicsDao).update(anyLong(), any(NicVO.class));
}
@Test
public void updateExternalVmNicsFromPrepareAnswer_noMatchingNicUuid_noAction() {
VirtualMachineTO vmTO = mock(VirtualMachineTO.class);
VirtualMachineTO updatedTO = mock(VirtualMachineTO.class);
NicTO nicTO = mock(NicTO.class);
NicTO updatedNicTO = mock(NicTO.class);
when(vmTO.getNics()).thenReturn(new NicTO[]{nicTO});
when(updatedTO.getNics()).thenReturn(new NicTO[]{updatedNicTO});
when(nicTO.getNicUuid()).thenReturn("nic-uuid");
when(updatedNicTO.getNicUuid()).thenReturn("different-uuid");
virtualMachineManagerImpl.updateExternalVmNicsFromPrepareAnswer(vmTO, updatedTO);
verify(_nicsDao, never()).findByUuid(anyString());
}
@Test
public void updateExternalVmNicsFromPrepareAnswer_nullUpdatedNics_noAction() {
VirtualMachineTO vmTO = mock(VirtualMachineTO.class);
VirtualMachineTO updatedTO = mock(VirtualMachineTO.class);
when(vmTO.getNics()).thenReturn(new NicTO[]{mock(NicTO.class)});
when(updatedTO.getNics()).thenReturn(null);
virtualMachineManagerImpl.updateExternalVmNicsFromPrepareAnswer(vmTO, updatedTO);
verify(_nicsDao, never()).findByUuid(anyString());
}
@Test
public void updateExternalVmNicsFromPrepareAnswer_nullVmNics_noAction() {
VirtualMachineTO vmTO = mock(VirtualMachineTO.class);
VirtualMachineTO updatedTO = mock(VirtualMachineTO.class);
when(vmTO.getNics()).thenReturn(null);
when(updatedTO.getNics()).thenReturn(new NicTO[]{mock(NicTO.class)});
virtualMachineManagerImpl.updateExternalVmNicsFromPrepareAnswer(vmTO, updatedTO);
verify(_nicsDao, never()).findByUuid(anyString());
}
@Test
public void updateExternalVmNicsFromPrepareAnswer_emptyNics_noAction() {
VirtualMachineTO vmTO = mock(VirtualMachineTO.class);
VirtualMachineTO updatedTO = mock(VirtualMachineTO.class);
when(vmTO.getNics()).thenReturn(new NicTO[]{});
when(updatedTO.getNics()).thenReturn(new NicTO[]{});
virtualMachineManagerImpl.updateExternalVmNicsFromPrepareAnswer(vmTO, updatedTO);
verify(_nicsDao, never()).findByUuid(anyString());
}
@Test
public void processPrepareExternalProvisioning_nonExternalHypervisor_noAction() throws OperationTimedoutException, AgentUnavailableException {
Host host = mock(Host.class);
VirtualMachineProfile vmProfile = mock(VirtualMachineProfile.class);
VirtualMachineTemplate template = mock(VirtualMachineTemplate.class);
when(vmProfile.getTemplate()).thenReturn(template);
when(host.getHypervisorType()).thenReturn(HypervisorType.KVM);
virtualMachineManagerImpl.processPrepareExternalProvisioning(true, host, vmProfile, mock(DataCenter.class));
verify(agentManagerMock, never()).send(anyLong(), any(Command.class));
}
@Test
public void processPrepareExternalProvisioning_externalHypervisor_sendsCommand() throws OperationTimedoutException, AgentUnavailableException {
Host host = mock(Host.class);
VirtualMachineProfile vmProfile = mock(VirtualMachineProfile.class);
VirtualMachineTemplate template = mock(VirtualMachineTemplate.class);
when(vmProfile.getTemplate()).thenReturn(template);
NicTO[] nics = new NicTO[]{mock(NicTO.class)};
VirtualMachineTO vmTO = mock(VirtualMachineTO.class);
when(vmTO.getNics()).thenReturn(nics);
doReturn(vmTO).when(virtualMachineManagerImpl).toVmTO(vmProfile);
ExtensionDetailsVO detailsVO = mock(ExtensionDetailsVO.class);
when(host.getHypervisorType()).thenReturn(HypervisorType.External);
when(template.getExtensionId()).thenReturn(1L);
when(extensionDetailsDao.findDetail(eq(1L), eq(ApiConstants.ORCHESTRATOR_REQUIRES_PREPARE_VM))).thenReturn(detailsVO);
when(detailsVO.getValue()).thenReturn("true");
PrepareExternalProvisioningAnswer answer = mock(PrepareExternalProvisioningAnswer.class);
when(answer.getResult()).thenReturn(true);
when(answer.getVirtualMachineTO()).thenReturn(vmTO);
when(agentManagerMock.send(anyLong(), any(Command.class))).thenReturn(answer);
virtualMachineManagerImpl.processPrepareExternalProvisioning(true, host, vmProfile, mock(DataCenter.class));
verify(agentManagerMock).send(anyLong(), any(Command.class));
}
}

View File

@ -59,4 +59,6 @@ public interface ClusterDao extends GenericDao<ClusterVO, Long> {
List<ClusterVO> listClustersByArchAndZoneId(long zoneId, CPU.CPUArch arch);
List<String> listDistinctStorageAccessGroups(String name, String keyword);
List<Long> listEnabledClusterIdsByZoneHypervisorArch(Long zoneId, HypervisorType hypervisorType, CPU.CPUArch arch);
}

View File

@ -378,4 +378,29 @@ public class ClusterDaoImpl extends GenericDaoBase<ClusterVO, Long> implements C
return customSearch(sc, null);
}
@Override
public List<Long> listEnabledClusterIdsByZoneHypervisorArch(Long zoneId, HypervisorType hypervisorType, CPU.CPUArch arch) {
GenericSearchBuilder<ClusterVO, Long> sb = createSearchBuilder(Long.class);
sb.selectFields(sb.entity().getId());
sb.and("zoneId", sb.entity().getDataCenterId(), SearchCriteria.Op.EQ);
sb.and("allocationState", sb.entity().getAllocationState(), Op.EQ);
sb.and("managedState", sb.entity().getManagedState(), Op.EQ);
sb.and("hypervisor", sb.entity().getHypervisorType(), Op.EQ);
sb.and("arch", sb.entity().getArch(), Op.EQ);
sb.done();
SearchCriteria<Long> sc = sb.create();
sc.setParameters("allocationState", Grouping.AllocationState.Enabled);
sc.setParameters("managedState", Managed.ManagedState.Managed);
if (zoneId != null) {
sc.setParameters("zoneId", zoneId);
}
if (hypervisorType != null) {
sc.setParameters("hypervisor", hypervisorType);
}
if (arch != null) {
sc.setParameters("arch", arch);
}
return customSearch(sc, null);
}
}

View File

@ -32,4 +32,7 @@ public interface HostDetailsDao extends GenericDao<DetailVO, Long> {
void deleteDetails(long hostId);
List<DetailVO> findByName(String name);
void replaceExternalDetails(long hostId, Map<String, String> details);
}

View File

@ -18,6 +18,7 @@ package com.cloud.host.dao;
import java.sql.PreparedStatement;
import java.sql.SQLException;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
@ -31,6 +32,7 @@ import com.cloud.utils.db.SearchBuilder;
import com.cloud.utils.db.SearchCriteria;
import com.cloud.utils.db.TransactionLegacy;
import com.cloud.utils.exception.CloudRuntimeException;
import com.cloud.vm.VmDetailConstants;
@Component
public class HostDetailsDaoImpl extends GenericDaoBase<DetailVO, Long> implements HostDetailsDao {
@ -130,4 +132,34 @@ public class HostDetailsDaoImpl extends GenericDaoBase<DetailVO, Long> implement
sc.setParameters("name", name);
return listBy(sc);
}
@Override
public void replaceExternalDetails(long hostId, Map<String, String> details) {
if (details.isEmpty()) {
return;
}
TransactionLegacy txn = TransactionLegacy.currentTxn();
txn.start();
List<DetailVO> detailVOs = new ArrayList<>();
for (Map.Entry<String, String> entry : details.entrySet()) {
String name = entry.getKey();
String value = entry.getValue();
if ("password".equals(entry.getKey())) {
value = DBEncryptionUtil.encrypt(value);
}
detailVOs.add(new DetailVO(hostId, name, value));
}
SearchBuilder<DetailVO> sb = createSearchBuilder();
sb.and("hostId", sb.entity().getHostId(), SearchCriteria.Op.EQ);
sb.and("name", sb.entity().getName(), SearchCriteria.Op.LIKE);
sb.done();
SearchCriteria<DetailVO> sc = sb.create();
sc.setParameters("hostId", hostId);
sc.setParameters("name", VmDetailConstants.EXTERNAL_DETAIL_PREFIX + "%");
remove(sc);
for (DetailVO detail : detailVOs) {
persist(detail);
}
txn.commit();
}
}

View File

@ -125,7 +125,7 @@ public class PhysicalNetworkTrafficTypeDaoImpl extends GenericDaoBase<PhysicalNe
sc = simulatorAllFieldsSearch.create();
} else if (hType == HypervisorType.Ovm) {
sc = ovmAllFieldsSearch.create();
} else if (hType == HypervisorType.BareMetal) {
} else if (hType == HypervisorType.BareMetal || hType == HypervisorType.External) {
return null;
} else if (hType == HypervisorType.Hyperv) {
sc = hypervAllFieldsSearch.create();

View File

@ -176,6 +176,9 @@ public class VMTemplateVO implements VirtualMachineTemplate {
@Convert(converter = CPUArchConverter.class)
private CPU.CPUArch arch;
@Column(name = "extension_id")
private Long extensionId;
@Override
public String getUniqueName() {
return uniqueName;
@ -218,7 +221,7 @@ public class VMTemplateVO implements VirtualMachineTemplate {
public VMTemplateVO(long id, String name, ImageFormat format, boolean isPublic, boolean featured, boolean isExtractable, TemplateType type, String url, boolean requiresHvm, int bits, long accountId, String cksum, String displayText, boolean enablePassword, long guestOSId, boolean bootable,
HypervisorType hyperType, String templateTag, Map<String, String> details, boolean sshKeyEnabled, boolean isDynamicallyScalable, boolean directDownload,
boolean deployAsIs, CPU.CPUArch arch) {
boolean deployAsIs, CPU.CPUArch arch, Long extensionId) {
this(id,
name,
format,
@ -245,6 +248,7 @@ public class VMTemplateVO implements VirtualMachineTemplate {
this.directDownload = directDownload;
this.deployAsIs = deployAsIs;
this.arch = arch;
this.extensionId = extensionId;
}
public static VMTemplateVO createPreHostIso(Long id, String uniqueName, String name, ImageFormat format, boolean isPublic, boolean featured, TemplateType type,
@ -702,4 +706,11 @@ public class VMTemplateVO implements VirtualMachineTemplate {
this.arch = arch;
}
public Long getExtensionId() {
return extensionId;
}
public void setExtensionId(Long extensionId) {
this.extensionId = extensionId;
}
}

View File

@ -101,4 +101,6 @@ public interface VMTemplateDao extends GenericDao<VMTemplateVO, Long>, StateDao<
List<VMTemplateVO> listByIds(List<Long> ids);
List<Long> listIdsByTemplateTag(String tag);
List<Long> listIdsByExtensionId(long extensionId);
}

View File

@ -837,6 +837,17 @@ public class VMTemplateDaoImpl extends GenericDaoBase<VMTemplateVO, Long> implem
return customSearchIncludingRemoved(sc, null);
}
@Override
public List<Long> listIdsByExtensionId(long extensionId) {
GenericSearchBuilder<VMTemplateVO, Long> sb = createSearchBuilder(Long.class);
sb.selectFields(sb.entity().getId());
sb.and("extensionId", sb.entity().getExtensionId(), SearchCriteria.Op.EQ);
sb.done();
SearchCriteria<Long> sc = sb.create();
sc.setParameters("extensionId", extensionId);
return customSearch(sc, null);
}
@Override
public boolean updateState(
com.cloud.template.VirtualMachineTemplate.State currentState,

View File

@ -21,6 +21,7 @@ import java.util.Map;
import org.apache.cloudstack.api.ResourceDetail;
import com.cloud.utils.Pair;
import com.cloud.utils.db.GenericDao;
public interface ResourceDetailsDao<R extends ResourceDetail> extends GenericDao<R, Long> {
@ -94,6 +95,8 @@ public interface ResourceDetailsDao<R extends ResourceDetail> extends GenericDao
Map<String, Boolean> listDetailsVisibility(long resourceId);
Pair<Map<String, String>, Map<String, String>> listDetailsKeyPairsWithVisibility(long resourceId);
void saveDetails(List<R> details);
void addDetail(long resourceId, String key, String value, boolean display);

View File

@ -23,6 +23,7 @@ import java.util.stream.Collectors;
import org.apache.commons.collections.CollectionUtils;
import com.cloud.utils.Pair;
import com.cloud.utils.crypt.DBEncryptionUtil;
import com.cloud.utils.db.GenericDaoBase;
import com.cloud.utils.db.GenericSearchBuilder;
@ -127,6 +128,19 @@ public abstract class ResourceDetailsDaoBase<R extends ResourceDetail> extends G
return details;
}
@Override
public Pair<Map<String, String>, Map<String, String>> listDetailsKeyPairsWithVisibility(long resourceId) {
SearchCriteria<R> sc = AllFieldsSearch.create();
sc.setParameters("resourceId", resourceId);
List<R> results = search(sc, null);
Map<Boolean, Map<String, String>> partitioned = results.stream()
.collect(Collectors.partitioningBy(
R::isDisplay,
Collectors.toMap(R::getName, R::getValue)
));
return new Pair<>(partitioned.get(true), partitioned.get(false));
}
public List<R> listDetails(long resourceId) {
SearchCriteria<R> sc = AllFieldsSearch.create();
sc.setParameters("resourceId", resourceId);

View File

@ -282,3 +282,318 @@ ALTER TABLE `cloud`.`vm_instance_details` ADD CONSTRAINT `fk_vm_instance_details
CALL `cloud`.`IDEMPOTENT_ADD_COLUMN`('cloud.backup_schedule', 'uuid', 'VARCHAR(40) NOT NULL');
UPDATE `cloud`.`backup_schedule` SET uuid = UUID();
-- Extension framework
UPDATE `cloud`.`configuration` SET value = CONCAT(value, ',External') WHERE name = 'hypervisor.list';
CREATE TABLE IF NOT EXISTS `cloud`.`extension` (
`id` bigint(20) unsigned NOT NULL AUTO_INCREMENT,
`uuid` varchar(40) NOT NULL UNIQUE,
`name` varchar(255) NOT NULL,
`description` varchar(4096),
`type` varchar(255) NOT NULL COMMENT 'Type of the extension: Orchestrator, etc',
`relative_path` varchar(2048) NOT NULL COMMENT 'Path for the extension relative to the root extensions directory',
`path_ready` tinyint(1) DEFAULT '0' COMMENT 'True if the extension path is in ready state across management servers',
`is_user_defined` tinyint(1) DEFAULT '0' COMMENT 'True if the extension is added by admin',
`state` char(32) NOT NULL COMMENT 'State of the extension - Enabled or Disabled',
`created` datetime NOT NULL,
`removed` datetime DEFAULT NULL,
PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8;
CREATE TABLE IF NOT EXISTS `cloud`.`extension_details` (
`id` bigint unsigned UNIQUE NOT NULL AUTO_INCREMENT COMMENT 'id',
`extension_id` bigint unsigned NOT NULL COMMENT 'extension to which the detail is related to',
`name` varchar(255) NOT NULL COMMENT 'name of the detail',
`value` varchar(255) NOT NULL COMMENT 'value of the detail',
`display` tinyint(1) NOT NULL DEFAULT '1' COMMENT 'True if the detail can be displayed to the end user',
PRIMARY KEY (`id`),
CONSTRAINT `fk_extension_details__extension_id` FOREIGN KEY (`extension_id`)
REFERENCES `extension` (`id`) ON DELETE CASCADE
) ENGINE=InnoDB DEFAULT CHARSET=utf8;
CREATE TABLE IF NOT EXISTS `cloud`.`extension_resource_map` (
`id` bigint(20) unsigned NOT NULL AUTO_INCREMENT,
`extension_id` bigint(20) unsigned NOT NULL,
`resource_id` bigint(20) unsigned NOT NULL,
`resource_type` char(255) NOT NULL,
`created` datetime NOT NULL,
`removed` datetime DEFAULT NULL,
PRIMARY KEY (`id`),
CONSTRAINT `fk_extension_resource_map__extension_id` FOREIGN KEY (`extension_id`)
REFERENCES `cloud`.`extension`(`id`) ON DELETE CASCADE,
INDEX `idx_extension_resource` (`resource_id`, `resource_type`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8;
CREATE TABLE IF NOT EXISTS `cloud`.`extension_resource_map_details` (
`id` bigint unsigned UNIQUE NOT NULL AUTO_INCREMENT COMMENT 'id',
`extension_resource_map_id` bigint unsigned NOT NULL COMMENT 'mapping to which the detail is related',
`name` varchar(255) NOT NULL COMMENT 'name of the detail',
`value` varchar(255) NOT NULL COMMENT 'value of the detail',
`display` tinyint(1) NOT NULL DEFAULT '1' COMMENT 'True if the detail can be displayed to the end user',
PRIMARY KEY (`id`),
CONSTRAINT `fk_extension_resource_map_details__map_id` FOREIGN KEY (`extension_resource_map_id`)
REFERENCES `extension_resource_map` (`id`) ON DELETE CASCADE
) ENGINE=InnoDB DEFAULT CHARSET=utf8;
CREATE TABLE IF NOT EXISTS `cloud`.`extension_custom_action` (
`id` bigint(20) unsigned NOT NULL AUTO_INCREMENT,
`uuid` varchar(255) NOT NULL UNIQUE,
`name` varchar(255) NOT NULL,
`description` varchar(4096),
`extension_id` bigint(20) unsigned NOT NULL,
`resource_type` varchar(255),
`allowed_role_types` int unsigned NOT NULL DEFAULT '1',
`success_message` varchar(4096),
`error_message` varchar(4096),
`enabled` boolean DEFAULT true,
`timeout` int unsigned NOT NULL DEFAULT '5' COMMENT 'The timeout in seconds to wait for the action to complete before failing',
`created` datetime NOT NULL,
`removed` datetime DEFAULT NULL,
PRIMARY KEY (`id`),
CONSTRAINT `fk_extension_custom_action__extension_id` FOREIGN KEY (`extension_id`)
REFERENCES `cloud`.`extension`(`id`) ON DELETE CASCADE
) ENGINE=InnoDB DEFAULT CHARSET=utf8;
CREATE TABLE IF NOT EXISTS `cloud`.`extension_custom_action_details` (
`id` bigint(20) unsigned NOT NULL AUTO_INCREMENT,
`extension_custom_action_id` bigint(20) unsigned NOT NULL,
`name` varchar(255) NOT NULL,
`value` TEXT NOT NULL,
`display` tinyint(1) NOT NULL DEFAULT 1,
PRIMARY KEY (`id`),
CONSTRAINT `fk_custom_action_details__action_id` FOREIGN KEY (`extension_custom_action_id`)
REFERENCES `cloud`.`extension_custom_action`(`id`) ON DELETE CASCADE
) ENGINE=InnoDB DEFAULT CHARSET=utf8;
CALL `cloud`.`IDEMPOTENT_ADD_COLUMN`('cloud.vm_template', 'extension_id', 'bigint unsigned DEFAULT NULL COMMENT "id of the extension"');
-- Add built-in Extensions and Custom Actions
DROP PROCEDURE IF EXISTS `cloud`.`INSERT_EXTENSION_IF_NOT_EXISTS`;
CREATE PROCEDURE `cloud`.`INSERT_EXTENSION_IF_NOT_EXISTS`(
IN ext_name VARCHAR(255),
IN ext_desc VARCHAR(255),
IN ext_path VARCHAR(255)
)
BEGIN
IF NOT EXISTS (
SELECT 1 FROM `cloud`.`extension` WHERE `name` = ext_name
) THEN
INSERT INTO `cloud`.`extension` (
`uuid`, `name`, `description`, `type`,
`relative_path`, `path_ready`,
`is_user_defined`, `state`, `created`, `removed`
)
VALUES (
UUID(), ext_name, ext_desc, 'Orchestrator',
ext_path, 1, 0, 'Enabled', NOW(), NULL
)
; END IF
;END;
DROP PROCEDURE IF EXISTS `cloud`.`INSERT_EXTENSION_DETAIL_IF_NOT_EXISTS`;
CREATE PROCEDURE `cloud`.`INSERT_EXTENSION_DETAIL_IF_NOT_EXISTS`(
IN ext_name VARCHAR(255),
IN detail_key VARCHAR(255),
IN detail_value TEXT,
IN display TINYINT(1)
)
BEGIN
DECLARE ext_id BIGINT
; SELECT `id` INTO ext_id FROM `cloud`.`extension` WHERE `name` = ext_name LIMIT 1
; IF NOT EXISTS (
SELECT 1 FROM `cloud`.`extension_details`
WHERE `extension_id` = ext_id AND `name` = detail_key
) THEN
INSERT INTO `cloud`.`extension_details` (
`extension_id`, `name`, `value`, `display`
)
VALUES (
ext_id, detail_key, detail_value, display
)
; END IF
;END;
CALL `cloud`.`INSERT_EXTENSION_IF_NOT_EXISTS`('Proxmox', 'Sample extension for Proxmox written in bash', 'Proxmox/proxmox.sh');
CALL `cloud`.`INSERT_EXTENSION_DETAIL_IF_NOT_EXISTS`('Proxmox', 'orchestratorrequirespreparevm', 'true', 0);
CALL `cloud`.`INSERT_EXTENSION_IF_NOT_EXISTS`('HyperV', 'Sample extension for HyperV written in python', 'HyperV/hyperv.py');
DROP PROCEDURE IF EXISTS `cloud`.`INSERT_EXTENSION_CUSTOM_ACTION_IF_NOT_EXISTS`;
CREATE PROCEDURE `cloud`.`INSERT_EXTENSION_CUSTOM_ACTION_IF_NOT_EXISTS`(
IN ext_name VARCHAR(255),
IN action_name VARCHAR(255),
IN action_desc VARCHAR(4096),
IN resource_type VARCHAR(255),
IN allowed_roles INT UNSIGNED,
IN success_msg VARCHAR(4096),
IN error_msg VARCHAR(4096),
IN timeout_seconds INT UNSIGNED
)
BEGIN
DECLARE ext_id BIGINT
; SELECT `id` INTO ext_id FROM `cloud`.`extension` WHERE `name` = ext_name LIMIT 1
; IF NOT EXISTS (
SELECT 1 FROM `cloud`.`extension_custom_action` WHERE `name` = action_name AND `extension_id` = ext_id
) THEN
INSERT INTO `cloud`.`extension_custom_action` (
`uuid`, `name`, `description`, `extension_id`, `resource_type`,
`allowed_role_types`, `success_message`, `error_message`,
`enabled`, `timeout`, `created`, `removed`
)
VALUES (
UUID(), action_name, action_desc, ext_id, resource_type,
allowed_roles, success_msg, error_msg,
1, timeout_seconds, NOW(), NULL
)
; END IF
;END;
DROP PROCEDURE IF EXISTS `cloud`.`INSERT_EXTENSION_CUSTOM_ACTION_DETAILS_IF_NOT_EXISTS`;
CREATE PROCEDURE `cloud`.`INSERT_EXTENSION_CUSTOM_ACTION_DETAILS_IF_NOT_EXISTS` (
IN ext_name VARCHAR(255),
IN action_name VARCHAR(255),
IN param_json TEXT
)
BEGIN
DECLARE action_id BIGINT UNSIGNED
; SELECT `eca`.`id` INTO action_id FROM `cloud`.`extension_custom_action` `eca`
JOIN `cloud`.`extension` `e` ON `e`.`id` = `eca`.`extension_id`
WHERE `eca`.`name` = action_name AND `e`.`name` = ext_name LIMIT 1
; IF NOT EXISTS (
SELECT 1 FROM `cloud`.`extension_custom_action_details`
WHERE `extension_custom_action_id` = action_id
AND `name` = 'parameters'
) THEN
INSERT INTO `cloud`.`extension_custom_action_details` (
`extension_custom_action_id`,
`name`,
`value`,
`display`
) VALUES (
action_id,
'parameters',
param_json,
0
)
; END IF
;END;
CALL `cloud`.`INSERT_EXTENSION_CUSTOM_ACTION_IF_NOT_EXISTS`('Proxmox', 'ListSnapshots', 'List Instance snapshots', 'VirtualMachine', 15, 'Snapshots fetched for {{resourceName}} in {{extensionName}}', 'List Snapshots failed for {{resourceName}}', 60);
CALL `cloud`.`INSERT_EXTENSION_CUSTOM_ACTION_IF_NOT_EXISTS`('Proxmox', 'CreateSnapshot', 'Create an Instance snapshot', 'VirtualMachine', 15, 'Snapshot created for {{resourceName}} in {{extensionName}}', 'Snapshot creation failed for {{resourceName}}', 60);
CALL `cloud`.`INSERT_EXTENSION_CUSTOM_ACTION_IF_NOT_EXISTS`('Proxmox', 'RestoreSnapshot', 'Restore Instance to the specific snapshot', 'VirtualMachine', 15, 'Successfully restored snapshot for {{resourceName}} in {{extensionName}}', 'Restore snapshot failed for {{resourceName}}', 60);
CALL `cloud`.`INSERT_EXTENSION_CUSTOM_ACTION_IF_NOT_EXISTS`('Proxmox', 'DeleteSnapshot', 'Delete the specified snapshot', 'VirtualMachine', 15, 'Successfully deleted snapshot for {{resourceName}} in {{extensionName}}', 'Delete snapshot failed for {{resourceName}}', 60);
CALL `cloud`.`INSERT_EXTENSION_CUSTOM_ACTION_DETAILS_IF_NOT_EXISTS`(
'Proxmox',
'ListSnapshots',
'[]'
);
CALL `cloud`.`INSERT_EXTENSION_CUSTOM_ACTION_DETAILS_IF_NOT_EXISTS`(
'Proxmox',
'CreateSnapshot',
'[
{
"name": "snap_name",
"type": "STRING",
"validationformat": "NONE",
"required": true
},
{
"name": "snap_description",
"type": "STRING",
"validationformat": "NONE",
"required": false
},
{
"name": "snap_save_memory",
"type": "BOOLEAN",
"validationformat": "NONE",
"required": false
}
]'
);
CALL `cloud`.`INSERT_EXTENSION_CUSTOM_ACTION_DETAILS_IF_NOT_EXISTS`(
'Proxmox',
'RestoreSnapshot',
'[
{
"name": "snap_name",
"type": "STRING",
"validationformat": "NONE",
"required": true
}
]'
);
CALL `cloud`.`INSERT_EXTENSION_CUSTOM_ACTION_DETAILS_IF_NOT_EXISTS`(
'Proxmox',
'DeleteSnapshot',
'[
{
"name": "snap_name",
"type": "STRING",
"validationformat": "NONE",
"required": true
}
]'
);
CALL `cloud`.`INSERT_EXTENSION_CUSTOM_ACTION_IF_NOT_EXISTS`('HyperV', 'ListSnapshots', 'List checkpoints/snapshots for the Instance', 'VirtualMachine', 15, 'Snapshots fetched for {{resourceName}} in {{extensionName}}', 'List Snapshots failed for {{resourceName}}', 60);
CALL `cloud`.`INSERT_EXTENSION_CUSTOM_ACTION_IF_NOT_EXISTS`('HyperV', 'CreateSnapshot', 'Create a checkpoint/snapshot for the Instance', 'VirtualMachine', 15, 'Snapshot created for {{resourceName}} in {{extensionName}}', 'Snapshot creation failed for {{resourceName}}', 60);
CALL `cloud`.`INSERT_EXTENSION_CUSTOM_ACTION_IF_NOT_EXISTS`('HyperV', 'RestoreSnapshot', 'Restore Instance to the specified snapshot', 'VirtualMachine', 15, 'Successfully restored snapshot for {{resourceName}} in {{extensionName}}', 'Restore snapshot failed for {{resourceName}}', 60);
CALL `cloud`.`INSERT_EXTENSION_CUSTOM_ACTION_IF_NOT_EXISTS`('HyperV', 'DeleteSnapshot', 'Delete the specified snapshot', 'VirtualMachine', 15, 'Successfully deleted snapshot for {{resourceName}} in {{extensionName}}', 'Delete snapshot failed for {{resourceName}}', 60);
CALL `cloud`.`INSERT_EXTENSION_CUSTOM_ACTION_IF_NOT_EXISTS`('HyperV', 'Suspend', 'Suspend the Instance by freezing its current state in RAM', 'VirtualMachine', 15, 'Successfully suspended {{resourceName}} in {{extensionName}}', 'Suspend failed for {{resourceName}}', 60);
CALL `cloud`.`INSERT_EXTENSION_CUSTOM_ACTION_IF_NOT_EXISTS`('HyperV', 'Resume', 'Resumes a suspended Instance, restoring CPU execution from memory.', 'VirtualMachine', 15, 'Successfully resumed {{resourceName}} in {{extensionName}}', 'Resume failed for {{resourceName}}', 60);
CALL `cloud`.`INSERT_EXTENSION_CUSTOM_ACTION_DETAILS_IF_NOT_EXISTS`(
'HyperV',
'ListSnapshots',
'[]'
);
CALL `cloud`.`INSERT_EXTENSION_CUSTOM_ACTION_DETAILS_IF_NOT_EXISTS`(
'HyperV',
'CreateSnapshot',
'[
{
"name": "snapshot_name",
"type": "STRING",
"validationformat": "NONE",
"required": true
}
]'
);
CALL `cloud`.`INSERT_EXTENSION_CUSTOM_ACTION_DETAILS_IF_NOT_EXISTS`(
'HyperV',
'RestoreSnapshot',
'[
{
"name": "snapshot_name",
"type": "STRING",
"validationformat": "NONE",
"required": true
}
]'
);
CALL `cloud`.`INSERT_EXTENSION_CUSTOM_ACTION_DETAILS_IF_NOT_EXISTS`(
'HyperV',
'DeleteSnapshot',
'[
{
"name": "snapshot_name",
"type": "STRING",
"validationformat": "NONE",
"required": true
}
]'
);
CALL `cloud`.`INSERT_EXTENSION_CUSTOM_ACTION_DETAILS_IF_NOT_EXISTS`(
'HyperV',
'Suspend',
'[]'
);
CALL `cloud`.`INSERT_EXTENSION_CUSTOM_ACTION_DETAILS_IF_NOT_EXISTS`(
'HyperV',
'Resume',
'[]'
);

View File

@ -106,7 +106,10 @@ SELECT
`user_data`.`uuid` AS `user_data_uuid`,
`user_data`.`name` AS `user_data_name`,
`user_data`.`params` AS `user_data_params`,
`vm_template`.`user_data_link_policy` AS `user_data_policy`
`vm_template`.`user_data_link_policy` AS `user_data_policy`,
`extension`.`id` AS `extension_id`,
`extension`.`uuid` AS `extension_uuid`,
`extension`.`name` AS `extension_name`
FROM
(((((((((((((`vm_template`
JOIN `guest_os` ON ((`guest_os`.`id` = `vm_template`.`guest_os_id`)))
@ -129,6 +132,7 @@ FROM
OR (`template_zone_ref`.`zone_id` = `data_center`.`id`))))
LEFT JOIN `launch_permission` ON ((`launch_permission`.`template_id` = `vm_template`.`id`)))
LEFT JOIN `user_data` ON ((`user_data`.`id` = `vm_template`.`user_data_id`))
LEFT JOIN `extension` ON ((`extension`.`id` = `vm_template`.`extension_id`))
LEFT JOIN `resource_tags` ON (((`resource_tags`.`resource_id` = `vm_template`.`id`)
AND ((`resource_tags`.`resource_type` = 'Template')
OR (`resource_tags`.`resource_type` = 'ISO')))));

View File

@ -20,6 +20,7 @@ import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertNotNull;
import static org.junit.Assert.assertNull;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.isNull;
import static org.mockito.Mockito.doReturn;
import static org.mockito.Mockito.eq;
import static org.mockito.Mockito.mock;
@ -299,4 +300,20 @@ public class VMTemplateDaoImplTest {
verify(searchBuilder, times(1)).join(eq("templateZoneSearch"), any(), any(), any(), eq(JoinBuilder.JoinType.INNER));
verify(templateDao, times(1)).customSearch(searchCriteria, null);
}
@Test
public void testListIdsByExtensionId_ReturnsIds() {
long extensionId = 42L;
List<Long> expectedIds = Arrays.asList(1L, 2L, 3L);
GenericSearchBuilder<VMTemplateVO, Long> searchBuilder = mock(GenericSearchBuilder.class);
SearchCriteria<Long> searchCriteria = mock(SearchCriteria.class);
when(templateDao.createSearchBuilder(Long.class)).thenReturn(searchBuilder);
when(searchBuilder.entity()).thenReturn(mock(VMTemplateVO.class));
when(searchBuilder.create()).thenReturn(searchCriteria);
doReturn(expectedIds).when(templateDao).customSearchIncludingRemoved(eq(searchCriteria), isNull());
List<Long> result = templateDao.listIdsByExtensionId(extensionId);
assertEquals(expectedIds, result);
verify(searchCriteria).setParameters("extensionId", extensionId);
verify(templateDao).customSearchIncludingRemoved(eq(searchCriteria), isNull());
}
}

View File

@ -236,6 +236,7 @@ public class TemplateServiceImpl implements TemplateService {
}
/* Baremetal need not to download any template */
availHypers.remove(HypervisorType.BareMetal);
availHypers.remove(HypervisorType.External);
availHypers.add(HypervisorType.None); // bug 9809: resume ISO
// download.
@ -526,6 +527,7 @@ public class TemplateServiceImpl implements TemplateService {
}
/* Baremetal need not to download any template */
availHypers.remove(HypervisorType.BareMetal);
availHypers.remove(HypervisorType.External);
availHypers.add(HypervisorType.None); // bug 9809: resume ISO
// download.
for (VMTemplateVO tmplt : toBeDownloaded) {
@ -817,7 +819,7 @@ public class TemplateServiceImpl implements TemplateService {
String templateName = dataDiskTemplate.isIso() ? dataDiskTemplate.getPath().substring(dataDiskTemplate.getPath().lastIndexOf(File.separator) + 1) : template.getName() + suffix + diskCount;
VMTemplateVO templateVO = new VMTemplateVO(templateId, templateName, format, false, false, false, ttype, template.getUrl(),
template.requiresHvm(), template.getBits(), template.getAccountId(), null, templateName, false, guestOsId, false, template.getHypervisorType(), null,
null, false, false, false, false, template.getArch());
null, false, false, false, false, template.getArch(), template.getExtensionId());
if (dataDiskTemplate.isIso()){
templateVO.setUniqueName(templateName);
}

View File

@ -358,6 +358,11 @@ public class TemplateObject implements TemplateInfo {
return imageVO.getArch();
}
@Override
public Long getExtensionId() {
return imageVO.getExtensionId();
}
@Override
public DataTO getTO() {
DataTO to = null;

302
extensions/HyperV/hyperv.py Executable file
View File

@ -0,0 +1,302 @@
#!/usr/bin/env python3
# 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.
import json
import sys
import winrm
def fail(message):
print(json.dumps({"error": message}))
sys.exit(1)
def succeed(data):
print(json.dumps(data))
sys.exit(0)
class HyperVManager:
def __init__(self, config_path):
self.config_path = config_path
self.data = self.parse_json()
self.session = self.init_winrm_session()
def parse_json(self):
try:
with open(self.config_path, 'r') as f:
json_data = json.load(f)
external_host_details = json_data["externaldetails"].get("host", [])
data = {
"url": external_host_details["url"],
"username": external_host_details["username"],
"password": external_host_details["password"],
"network_switch": external_host_details["network_switch"],
"vhd_path": external_host_details["vhd_path"],
"vm_path": external_host_details["vm_path"],
"cert_validation": "validate" if external_host_details.get("verify_tls_certificate", "true").lower() == "true" else "ignore"
}
external_vm_details = json_data["externaldetails"].get("virtualmachine", [])
if external_vm_details:
data["template_type"] = external_vm_details["template_type"]
data["generation"] = external_vm_details.get("generation", 1)
data["template_path"] = external_vm_details.get("template_path", "")
data["iso_path"] = external_vm_details.get("iso_path", "")
data["vhd_size_gb"] = external_vm_details.get("vhd_size_gb", "")
data["cpus"] = json_data["cloudstack.vm.details"]["cpus"]
data["memory"] = json_data["cloudstack.vm.details"]["minRam"]
data["vmname"] = json_data["cloudstack.vm.details"]["name"]
nics = json_data["cloudstack.vm.details"].get("nics", [])
data["nics"] = []
for nic in nics:
data["nics"].append({
"mac": nic["mac"],
"vlan": nic["broadcastUri"].replace("vlan://", "")
})
parameters = json_data.get("parameters", [])
if parameters:
data["snapshot_name"] = parameters.get("snapshot_name", "")
return data
except KeyError as e:
fail(f"Missing required field in JSON: {str(e)}")
except Exception as e:
fail(f"Error parsing JSON: {str(e)}")
def init_winrm_session(self):
return winrm.Session(
f"https://{self.data['url']}:5986/wsman",
auth=(self.data["username"], self.data["password"]),
transport='ntlm',
server_cert_validation=self.data["cert_validation"]
)
def run_ps_int(self, command):
r = self.session.run_ps(command)
if r.status_code != 0:
raise Exception(r.std_err.decode())
return r.std_out.decode()
def run_ps(self, command):
try:
output = self.run_ps_int(command)
return output
except Exception as e:
fail(str(e))
def vm_not_present(self, exception):
vm_not_present_str = f'Hyper-V was unable to find a virtual machine with name "{self.data["vmname"]}"'
return vm_not_present_str in str(exception)
def create(self):
vm_name = self.data["vmname"]
cpus = self.data["cpus"]
memory = self.data["memory"]
memory_mb = int(memory) / 1024 / 1024
template_path = self.data["template_path"]
vhd_path = self.data["vhd_path"] + "\\" + vm_name + ".vhdx"
vhd_size_gb = self.data["vhd_size_gb"]
generation = self.data["generation"]
iso_path = self.data["iso_path"]
network_switch = self.data["network_switch"]
vm_path = self.data["vm_path"]
template_type = self.data.get("template_type", "template")
vhd_created = False
vm_created = False
vm_started = False
try:
command = (
f'New-VM -Name "{vm_name}" -MemoryStartupBytes {memory_mb}MB '
f'-Generation {generation} -Path "{vm_path}" '
)
if template_type == "iso":
if (iso_path == ""):
fail("Missing required field in JSON: iso_path")
if (vhd_size_gb == ""):
fail("Missing required field in JSON: vhd_size_gb")
command += (
f'-NewVHDPath "{vhd_path}" -NewVHDSizeBytes {vhd_size_gb}GB; '
f'Add-VMDvdDrive -VMName "{vm_name}" -Path "{iso_path}"; '
)
else:
if (template_path == ""):
fail("Missing required field in JSON: template_path")
self.run_ps_int(f'Copy-Item "{template_path}" "{vhd_path}"')
vhd_created = True
command += f'-VHDPath "{vhd_path}"; '
self.run_ps_int(command)
vm_created = True
command = f'Remove-VMNetworkAdapter -VMName "{vm_name}" -Name "Network Adapter" -ErrorAction SilentlyContinue; '
self.run_ps_int(command)
command = f'Set-VMProcessor -VMName "{vm_name}" -Count "{cpus}"; '
if (generation == 2):
command += f'Set-VMFirmware -VMName "{vm_name}" -EnableSecureBoot Off; '
self.run_ps_int(command)
for idx, nic in enumerate(self.data["nics"]):
adapter_name = f"NIC{idx+1}"
self.run_ps_int(f'Add-VMNetworkAdapter -VMName "{vm_name}" -SwitchName "{network_switch}" -Name "{adapter_name}"')
self.run_ps_int(f'Set-VMNetworkAdapter -VMName "{vm_name}" -Name "{adapter_name}" -StaticMacAddress "{nic["mac"]}"')
self.run_ps_int(f'Set-VMNetworkAdapterVlan -VMName "{vm_name}" -VMNetworkAdapterName "{adapter_name}" -Access -VlanId "{nic["vlan"]}"')
self.run_ps_int(f'Start-VM -Name "{vm_name}"')
vm_started = True
succeed({"status": "success", "message": "Instance created"})
except Exception as e:
if vm_started:
self.run_ps_int(f'Stop-VM -Name "{vm_name}" -Force -TurnOff')
if vm_created:
self.run_ps_int(f'Remove-VM -Name "{vm_name}" -Force')
if vhd_created:
self.run_ps_int(f'Remove-Item -Path "{vhd_path}" -Force')
fail(str(e))
def start(self):
self.run_ps(f'Start-VM -Name "{self.data["vmname"]}"')
succeed({"status": "success", "message": "Instance started"})
def stop(self):
try:
self.run_ps_int(f'Stop-VM -Name "{self.data["vmname"]}" -Force')
except Exception as e:
if self.vm_not_present(e):
succeed({"status": "success", "message": "Instance stopped"})
else:
fail(str(e))
succeed({"status": "success", "message": "Instance stopped"})
def reboot(self):
self.run_ps(f'Restart-VM -Name "{self.data["vmname"]}" -Force')
succeed({"status": "success", "message": "Instance rebooted"})
def status(self):
command = f'(Get-VM -Name "{self.data["vmname"]}").State'
state = self.run_ps(command)
if state.lower() == "running":
power_state = "poweron"
elif state.lower() == "off":
power_state = "poweroff"
else:
power_state = "unknown"
succeed({"status": "success", "power_state": power_state})
def delete(self):
try:
self.run_ps_int(f'Remove-VM -Name "{self.data["vmname"]}" -Force')
except Exception as e:
if self.vm_not_present(e):
succeed({"status": "success", "message": "Instance deleted"})
else:
fail(str(e))
succeed({"status": "success", "message": "Instance deleted"})
def suspend(self):
self.run_ps(f'Suspend-VM -Name "{self.data["vmname"]}"')
succeed({"status": "success", "message": "Instance suspended"})
def resume(self):
self.run_ps(f'Resume-VM -Name "{self.data["vmname"]}"')
succeed({"status": "success", "message": "Instance resumed"})
def create_snapshot(self):
snapshot_name = self.data["snapshot_name"]
if snapshot_name == "":
fail("Missing required field in JSON: snapshot_name")
command = f'Checkpoint-VM -VMName "{self.data["vmname"]}" -SnapshotName "{snapshot_name}"'
self.run_ps(command)
succeed({"status": "success", "message": f"Snapshot '{snapshot_name}' created"})
def list_snapshots(self):
command = (
f'Get-VMSnapshot -VMName "{self.data["vmname"]}" '
'| Select-Object Name, @{Name="CreationTime";Expression={$_.CreationTime.ToString("s")}} '
'| ConvertTo-Json'
)
snapshots = json.loads(self.run_ps(command))
succeed({"status": "success", "printmessage": "true", "message": snapshots})
def restore_snapshot(self):
snapshot_name = self.data["snapshot_name"]
if snapshot_name == "":
fail("Missing required field in JSON: snapshot_name")
command = f'Restore-VMSnapshot -VMName "{self.data["vmname"]}" -Name "{snapshot_name}" -Confirm:$false'
self.run_ps(command)
succeed({"status": "success", "message": f"Snapshot '{snapshot_name}' restored"})
def delete_snapshot(self):
snapshot_name = self.data["snapshot_name"]
if snapshot_name == "":
fail("Missing required field in JSON: snapshot_name")
command = f'Remove-VMSnapshot -VMName "{self.data["vmname"]}" -Name "{snapshot_name}" -Confirm:$false'
self.run_ps(command)
succeed({"status": "success", "message": f"Snapshot '{snapshot_name}' deleted"})
def main():
if len(sys.argv) < 3:
fail("Usage: script.py <operation> '<json-file-path>'")
operation = sys.argv[1].lower()
json_file_path = sys.argv[2]
try:
manager = HyperVManager(json_file_path)
except FileNotFoundError:
fail(f"JSON file not found: {json_file_path}")
except json.JSONDecodeError:
fail("Invalid JSON in file")
operations = {
"create": manager.create,
"start": manager.start,
"stop": manager.stop,
"reboot": manager.reboot,
"delete": manager.delete,
"status": manager.status,
"suspend": manager.suspend,
"resume": manager.resume,
"listsnapshots": manager.list_snapshots,
"createsnapshot": manager.create_snapshot,
"restoresnapshot": manager.restore_snapshot,
"deletesnapshot": manager.delete_snapshot
}
if operation not in operations:
fail("Invalid action")
try:
operations[operation]()
except Exception as e:
fail(str(e))
if __name__ == "__main__":
main()

413
extensions/Proxmox/proxmox.sh Executable file
View File

@ -0,0 +1,413 @@
#!/usr/bin/env bash
# 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.
parse_json() {
local json_string="$1"
echo "$json_string" | jq '.' > /dev/null || { echo '{"error":"Invalid JSON input"}'; exit 1; }
local -A details
while IFS="=" read -r key value; do
details[$key]="$value"
done < <(echo "$json_string" | jq -r '{
"extension_url": (.externaldetails.extension.url // ""),
"extension_user": (.externaldetails.extension.user // ""),
"extension_token": (.externaldetails.extension.token // ""),
"extension_secret": (.externaldetails.extension.secret // ""),
"host_url": (.externaldetails.host.url // ""),
"host_user": (.externaldetails.host.user // ""),
"host_token": (.externaldetails.host.token // ""),
"host_secret": (.externaldetails.host.secret // ""),
"node": (.externaldetails.host.node // ""),
"network_bridge": (.externaldetails.host.network_bridge // ""),
"verify_tls_certificate": (.externaldetails.host.verify_tls_certificate // "true"),
"vm_name": (.externaldetails.virtualmachine.vm_name // ""),
"template_id": (.externaldetails.virtualmachine.template_id // ""),
"template_type": (.externaldetails.virtualmachine.template_type // ""),
"iso_path": (.externaldetails.virtualmachine.iso_path // ""),
"snap_name": (.parameters.snap_name // ""),
"snap_description": (.parameters.snap_description // ""),
"snap_save_memory": (.parameters.snap_save_memory // ""),
"vmid": (."cloudstack.vm.details".details.proxmox_vmid // ""),
"vm_internal_name": (."cloudstack.vm.details".name // ""),
"vmmemory": (."cloudstack.vm.details".minRam // ""),
"vmcpus": (."cloudstack.vm.details".cpus // ""),
"vlans": ([."cloudstack.vm.details".nics[]?.broadcastUri // "" | sub("vlan://"; "")] | join(",")),
"mac_addresses": ([."cloudstack.vm.details".nics[]?.mac // ""] | join(","))
} | to_entries | .[] | "\(.key)=\(.value)"')
for key in "${!details[@]}"; do
declare -g "$key=${details[$key]}"
done
# set url, user, token, secret to host values if present, otherwise use extension values
url="${host_url:-$extension_url}"
user="${host_user:-$extension_user}"
token="${host_token:-$extension_token}"
secret="${host_secret:-$extension_secret}"
check_required_fields vm_internal_name url user token secret node
}
urlencode() {
encoded_data=$(python3 -c "import urllib.parse; print(urllib.parse.quote('''$1'''))")
echo "$encoded_data"
}
check_required_fields() {
local missing=()
for varname in "$@"; do
local value="${!varname}"
if [[ -z "$value" ]]; then
missing+=("$varname")
fi
done
if [[ ${#missing[@]} -gt 0 ]]; then
echo "{\"error\":\"Missing required fields: ${missing[*]}\"}"
exit 1
fi
}
validate_name() {
local entity="$1"
local name="$2"
if [[ ! "$name" =~ ^[a-zA-Z0-9-]+$ ]]; then
echo "{\"error\":\"Invalid $entity name '$name'. Only alphanumeric characters and dashes (-) are allowed.\"}"
exit 1
fi
}
call_proxmox_api() {
local method=$1
local path=$2
local data=$3
curl_opts=(
-s
--fail
-X "$method"
-H "Authorization: PVEAPIToken=${user}!${token}=${secret}"
)
if [[ "$verify_tls_certificate" == "false" ]]; then
curl_opts+=(-k)
fi
if [[ -n "$data" ]]; then
curl_opts+=(-d "$data")
fi
#echo curl "${curl_opts[@]}" "https://${url}:8006/api2/json${path}" >&2
response=$(curl "${curl_opts[@]}" "https://${url}:8006/api2/json${path}")
echo "$response"
}
wait_for_proxmox_task() {
local upid="$1"
local timeout="${2:-$wait_time}"
local interval="${3:-1}"
local start_time
start_time=$(date +%s)
while true; do
local now
now=$(date +%s)
if (( now - start_time > timeout )); then
echo '{"error":"Timeout while waiting for async task"}'
exit 1
fi
local status_response
status_response=$(call_proxmox_api GET "/nodes/${node}/tasks/$(urlencode "$upid")/status")
if [[ -z "$status_response" || "$status_response" == *'"errors":'* ]]; then
local msg
msg=$(echo "$status_response" | jq -r '.message // "Unknown error"')
echo "{\"error\":\"$msg\"}"
exit 1
fi
local task_status
task_status=$(echo "$status_response" | jq -r '.data.status')
if [[ "$task_status" == "stopped" ]]; then
local exit_status
exit_status=$(echo "$status_response" | jq -r '.data.exitstatus')
if [[ "$exit_status" != "OK" ]]; then
echo "{\"error\":\"Task failed with exit status: $exit_status\"}"
exit 1
fi
return 0
fi
sleep "$interval"
done
}
execute_and_wait() {
local method="$1"
local path="$2"
local data="$3"
local response upid msg
response=$(call_proxmox_api "$method" "$path" "$data")
upid=$(echo "$response" | jq -r '.data // ""')
if [[ -z "$upid" ]]; then
msg=$(echo "$response" | jq -r '.message // "Unknown error"')
echo "{\"error\":\"Failed to execute API or retrieve UPID. Message: $msg\"}"
exit 1
fi
wait_for_proxmox_task "$upid"
}
vm_not_present() {
response=$(call_proxmox_api GET "/cluster/nextid?vmid=$vmid")
vmid_result=$(echo "$response" | jq -r '.data // empty')
if [[ "$vmid_result" == "$vmid" ]]; then
return 0
else
return 1
fi
}
prepare() {
response=$(call_proxmox_api GET "/cluster/nextid")
vmid=$(echo "$response" | jq -r '.data // ""')
echo "{\"details\":{\"proxmox_vmid\": \"$vmid\"}}"
}
create() {
if [[ -z "$vm_name" ]]; then
vm_name="$vm_internal_name"
fi
validate_name "VM" "$vm_name"
check_required_fields vmid network_bridge vmcpus vmmemory
if [[ "${template_type^^}" == "ISO" ]]; then
check_required_fields iso_path
local data="vmid=$vmid"
data+="&name=$vm_name"
data+="&ide2=$(urlencode "$iso_path,media=cdrom")"
data+="&ostype=l26"
data+="&scsihw=virtio-scsi-single"
data+="&scsi0=$(urlencode "local-lvm:64,iothread=on")"
data+="&sockets=1"
data+="&cores=$vmcpus"
data+="&numa=0"
data+="&cpu=x86-64-v2-AES"
data+="&memory=$((vmmemory / 1024 / 1024))"
execute_and_wait POST "/nodes/${node}/qemu/" "$data"
cleanup_vm=1
else
check_required_fields template_id
local data="newid=$vmid"
data+="&name=$vm_name"
execute_and_wait POST "/nodes/${node}/qemu/${template_id}/clone" "$data"
cleanup_vm=1
data="cores=$vmcpus"
data+="&memory=$((vmmemory / 1024 / 1024))"
execute_and_wait POST "/nodes/${node}/qemu/${vmid}/config" "$data"
fi
IFS=',' read -ra vlan_array <<< "$vlans"
IFS=',' read -ra mac_array <<< "$mac_addresses"
for i in "${!vlan_array[@]}"; do
network="net${i}=$(urlencode "virtio=${mac_array[i]},bridge=${network_bridge},tag=${vlan_array[i]},firewall=0")"
call_proxmox_api PUT "/nodes/${node}/qemu/${vmid}/config/" "$network" > /dev/null
done
execute_and_wait POST "/nodes/${node}/qemu/${vmid}/status/start"
cleanup_vm=0
echo '{"status": "success", "message": "Instance created"}'
}
start() {
execute_and_wait POST "/nodes/${node}/qemu/${vmid}/status/start"
echo '{"status": "success", "message": "Instance started"}'
}
delete() {
if vm_not_present; then
echo '{"status": "success", "message": "Instance deleted"}'
return 0
fi
execute_and_wait DELETE "/nodes/${node}/qemu/${vmid}"
echo '{"status": "success", "message": "Instance deleted"}'
}
stop() {
if vm_not_present; then
echo '{"status": "success", "message": "Instance stopped"}'
return 0
fi
execute_and_wait POST "/nodes/${node}/qemu/${vmid}/status/stop"
echo '{"status": "success", "message": "Instance stopped"}'
}
reboot() {
execute_and_wait POST "/nodes/${node}/qemu/${vmid}/status/reboot"
echo '{"status": "success", "message": "Instance rebooted"}'
}
status() {
local status_response vm_status powerstate
status_response=$(call_proxmox_api GET "/nodes/${node}/qemu/${vmid}/status/current")
vm_status=$(echo "$status_response" | jq -r '.data.status')
case "$vm_status" in
running) powerstate="poweron" ;;
stopped) powerstate="poweroff" ;;
*) powerstate="unknown" ;;
esac
echo "{\"status\": \"success\", \"power_state\": \"$powerstate\"}"
}
list_snapshots() {
snapshot_response=$(call_proxmox_api GET "/nodes/${node}/qemu/${vmid}/snapshot")
echo "$snapshot_response" | jq '
def to_date:
if . == "-" then "-"
elif . == null then "-"
else (. | tonumber | strftime("%Y-%m-%d %H:%M:%S"))
end;
{
status: "success",
printmessage: "true",
message: [.data[] | {
name: .name,
snaptime: ((.snaptime // "-") | to_date),
description: .description,
parent: (.parent // "-"),
vmstate: (.vmstate // "-")
}]
}
'
}
create_snapshot() {
check_required_fields snap_name
validate_name "Snapshot" "$snap_name"
local data vmstate
data="snapname=$snap_name"
if [[ -n "$snap_description" ]]; then
data+="&description=$snap_description"
fi
if [[ -n "$snap_save_memory" && "$snap_save_memory" == "true" ]]; then
vmstate="1"
else
vmstate="0"
fi
data+="&vmstate=$vmstate"
execute_and_wait POST "/nodes/${node}/qemu/${vmid}/snapshot" "$data"
echo '{"status": "success", "message": "Instance Snapshot created"}'
}
restore_snapshot() {
check_required_fields snap_name
validate_name "Snapshot" "$snap_name"
execute_and_wait POST "/nodes/${node}/qemu/${vmid}/snapshot/${snap_name}/rollback"
execute_and_wait POST "/nodes/${node}/qemu/${vmid}/status/start"
echo '{"status": "success", "message": "Instance Snapshot restored"}'
}
delete_snapshot() {
check_required_fields snap_name
validate_name "Snapshot" "$snap_name"
execute_and_wait DELETE "/nodes/${node}/qemu/${vmid}/snapshot/${snap_name}"
echo '{"status": "success", "message": "Instance Snapshot deleted"}'
}
action=$1
parameters_file="$2"
wait_time=$3
if [[ -z "$action" || -z "$parameters_file" ]]; then
echo '{"error":"Missing required arguments"}'
exit 1
fi
# Read file content as parameters (assumes space-separated arguments)
parameters=$(<"$parameters_file")
parse_json "$parameters" || exit 1
cleanup_vm=0
cleanup() {
if (( cleanup_vm == 1 )); then
execute_and_wait DELETE "/nodes/${node}/qemu/${vmid}"
fi
}
trap cleanup EXIT
case $action in
prepare)
prepare
;;
create)
create
;;
delete)
delete
;;
start)
start
;;
stop)
stop
;;
reboot)
reboot
;;
status)
status
;;
ListSnapshots)
list_snapshots
;;
CreateSnapshot)
create_snapshot
;;
RestoreSnapshot)
restore_snapshot
;;
DeleteSnapshot)
delete_snapshot
;;
*)
echo '{"error":"Invalid action"}'
exit 1
;;
esac
exit 0

View File

@ -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.
-->
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<artifactId>cloud-framework-extensions</artifactId>
<name>Apache CloudStack Framework - Extensions</name>
<parent>
<groupId>org.apache.cloudstack</groupId>
<artifactId>cloudstack-framework</artifactId>
<version>4.21.0.0-SNAPSHOT</version>
<relativePath>../pom.xml</relativePath>
</parent>
<dependencies>
<dependency>
<groupId>org.apache.cloudstack</groupId>
<artifactId>cloud-utils</artifactId>
<version>${project.version}</version>
</dependency>
<dependency>
<groupId>org.apache.cloudstack</groupId>
<artifactId>cloud-api</artifactId>
<version>${project.version}</version>
</dependency>
<dependency>
<groupId>org.apache.cloudstack</groupId>
<artifactId>cloud-engine-schema</artifactId>
<version>4.21.0.0-SNAPSHOT</version>
<scope>compile</scope>
</dependency>
<dependency>
<groupId>org.apache.cloudstack</groupId>
<artifactId>cloud-engine-components-api</artifactId>
<version>4.21.0.0-SNAPSHOT</version>
<scope>compile</scope>
</dependency>
</dependencies>
</project>

View File

@ -0,0 +1,175 @@
// 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.framework.extensions.api;
import java.util.List;
import java.util.Map;
import javax.inject.Inject;
import org.apache.cloudstack.acl.RoleType;
import org.apache.cloudstack.api.APICommand;
import org.apache.cloudstack.api.ApiCommandResourceType;
import org.apache.cloudstack.api.ApiConstants;
import org.apache.cloudstack.api.BaseCmd;
import org.apache.cloudstack.api.Parameter;
import org.apache.cloudstack.api.response.ExtensionCustomActionResponse;
import org.apache.cloudstack.api.response.ExtensionResponse;
import org.apache.cloudstack.extension.ExtensionCustomAction;
import org.apache.cloudstack.framework.extensions.manager.ExtensionsManager;
import com.cloud.user.Account;
@APICommand(name = "addCustomAction",
description = "Add a custom action for an extension",
responseObject = ExtensionCustomActionResponse.class,
responseHasSensitiveInfo = false,
entityType = {ExtensionCustomAction.class},
authorized = {RoleType.Admin},
since = "4.21.0")
public class AddCustomActionCmd extends BaseCmd {
@Inject
ExtensionsManager extensionsManager;
@Parameter(name = ApiConstants.EXTENSION_ID, type = CommandType.UUID, required = true,
entityType = ExtensionResponse.class, description = "The ID of the extension to associate the action with")
private Long extensionId;
@Parameter(name = ApiConstants.NAME, type = CommandType.STRING, required = true, description = "Name of the action")
private String name;
@Parameter(name = ApiConstants.DESCRIPTION, type = CommandType.STRING, description = "Description of the action")
private String description;
@Parameter(name = ApiConstants.RESOURCE_TYPE,
type = CommandType.STRING,
description = "Resource type for which the action is available")
private String resourceType;
@Parameter(name = ApiConstants.ALLOWED_ROLE_TYPES,
type = CommandType.LIST,
collectionType = CommandType.STRING,
description = "List of role types allowed for the action")
private List<String> allowedRoleTypes;
@Parameter(name = ApiConstants.PARAMETERS, type = CommandType.MAP,
description = "Parameters mapping for the action using keys - name, type, required. " +
"'name' is mandatory. If 'type' is not specified then STRING will be used. " +
"If 'required' is not specified then false will be used. "
+ "Example: parameters[0].name=xxx&parameters[0].type=BOOLEAN&parameters[0].required=true")
protected Map parameters;
@Parameter(name = ApiConstants.SUCCESS_MESSAGE, type = CommandType.STRING,
description = "Success message that will be used on successful execution of the action. " +
"Name of the action, extension, resource can be used as - actionName, extensionName, resourceName. "
+ "Example: Successfully complete {{actionName}} for {{resourceName}} with {{extensionName}}")
protected String successMessage;
@Parameter(name = ApiConstants.ERROR_MESSAGE, type = CommandType.STRING,
description = "Error message that will be used on failure during execution of the action. " +
"Name of the action, extension, resource can be used as - actionName, extensionName, resourceName. "
+ "Example: Failed to complete {{actionName}} for {{resourceName}} with {{extensionName}}")
protected String errorMessage;
@Parameter(name = ApiConstants.TIMEOUT,
type = CommandType.INTEGER,
description = "Specifies the timeout in seconds to wait for the action to complete before failing. Default value is 5 seconds")
private Integer timeout;
@Parameter(name = ApiConstants.ENABLED,
type = CommandType.BOOLEAN,
description = "Whether the action is enabled or not. Default is disabled.")
private Boolean enabled;
@Parameter(name = ApiConstants.DETAILS,
type = CommandType.MAP,
description = "Details in key/value pairs using format details[i].keyname=keyvalue. "
+ "Example: details[0].vendor=xxx&&details[0].version=2.0")
protected Map details;
/////////////////////////////////////////////////////
/////////////////// Accessors ///////////////////////
/////////////////////////////////////////////////////
public Long getExtensionId() {
return extensionId;
}
public String getName() {
return name;
}
public String getDescription() {
return description;
}
public String getResourceType() {
return resourceType;
}
public List<String> getAllowedRoleTypes() {
return allowedRoleTypes;
}
public Map getParametersMap() {
return parameters;
}
public String getSuccessMessage() {
return successMessage;
}
public String getErrorMessage() {
return errorMessage;
}
public Integer getTimeout() {
return timeout;
}
public boolean isEnabled() {
return Boolean.TRUE.equals(enabled);
}
public Map<String, String> getDetails() {
return convertDetailsToMap(details);
}
/////////////////////////////////////////////////////
/////////////// API Implementation///////////////////
/////////////////////////////////////////////////////
@Override
public void execute() {
ExtensionCustomAction extensionCustomAction = extensionsManager.addCustomAction(this);
ExtensionCustomActionResponse response = extensionsManager.createCustomActionResponse(extensionCustomAction);
response.setResponseName(getCommandName());
setResponseObject(response);
}
@Override
public long getEntityOwnerId() {
return Account.ACCOUNT_ID_SYSTEM;
}
@Override
public ApiCommandResourceType getApiResourceType() {
return ApiCommandResourceType.ExtensionCustomAction;
}
}

View File

@ -0,0 +1,140 @@
// 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.framework.extensions.api;
import java.util.EnumSet;
import java.util.Map;
import javax.inject.Inject;
import org.apache.cloudstack.acl.RoleType;
import org.apache.cloudstack.api.APICommand;
import org.apache.cloudstack.api.ApiCommandResourceType;
import org.apache.cloudstack.api.ApiConstants;
import org.apache.cloudstack.api.BaseCmd;
import org.apache.cloudstack.api.Parameter;
import org.apache.cloudstack.api.ServerApiException;
import org.apache.cloudstack.api.response.ExtensionResponse;
import org.apache.cloudstack.extension.Extension;
import org.apache.cloudstack.framework.extensions.manager.ExtensionsManager;
import com.cloud.exception.ConcurrentOperationException;
import com.cloud.exception.NetworkRuleConflictException;
import com.cloud.exception.ResourceAllocationException;
import com.cloud.user.Account;
@APICommand(name = "createExtension",
description = "Create an extension",
responseObject = ExtensionResponse.class,
responseHasSensitiveInfo = false,
entityType = {Extension.class},
authorized = {RoleType.Admin},
since = "4.21.0")
public class CreateExtensionCmd extends BaseCmd {
@Inject
ExtensionsManager extensionsManager;
/////////////////////////////////////////////////////
//////////////// API parameters /////////////////////
/////////////////////////////////////////////////////
@Parameter(name = ApiConstants.NAME, type = CommandType.STRING, required = true,
description = "Name of the extension")
private String name;
@Parameter(name = ApiConstants.DESCRIPTION, type = CommandType.STRING,
description = "Description of the extension")
private String description;
@Parameter(name = ApiConstants.TYPE, type = CommandType.STRING, required = true,
description = "Type of the extension")
private String type;
@Parameter(name = ApiConstants.PATH, type = CommandType.STRING,
description = "Relative path for the extension")
private String path;
@Parameter(name = ApiConstants.ORCHESTRATOR_REQUIRES_PREPARE_VM,
type = CommandType.BOOLEAN,
description = "Only honored when type is Orchestrator. Whether prepare VM is needed or not")
private Boolean orchestratorRequiresPrepareVm;
@Parameter(name = ApiConstants.STATE, type = CommandType.STRING,
description = "State of the extension")
private String state;
@Parameter(name = ApiConstants.DETAILS, type = CommandType.MAP,
description = "Details in key/value pairs using format details[i].keyname=keyvalue. Example: details[0].endpoint.url=urlvalue")
protected Map details;
/////////////////////////////////////////////////////
/////////////////// Accessors ///////////////////////
/////////////////////////////////////////////////////
public String getName() {
return name;
}
public String getDescription() {
return description;
}
public String getType() {
return type;
}
public String getPath() {
return path;
}
public Boolean isOrchestratorRequiresPrepareVm() {
return orchestratorRequiresPrepareVm;
}
public String getState() {
return state;
}
public Map<String, String> getDetails() {
return convertDetailsToMap(details);
}
/////////////////////////////////////////////////////
/////////////// API Implementation///////////////////
/////////////////////////////////////////////////////
@Override
public void execute() throws ServerApiException, ConcurrentOperationException, ResourceAllocationException, NetworkRuleConflictException {
Extension extension = extensionsManager.createExtension(this);
ExtensionResponse response = extensionsManager.createExtensionResponse(extension,
EnumSet.of(ApiConstants.ExtensionDetails.all));
response.setResponseName(getCommandName());
setResponseObject(response);
}
@Override
public long getEntityOwnerId() {
return Account.ACCOUNT_ID_SYSTEM;
}
@Override
public ApiCommandResourceType getApiResourceType() {
return ApiCommandResourceType.Extension;
}
}

View File

@ -0,0 +1,96 @@
// 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.framework.extensions.api;
import javax.inject.Inject;
import org.apache.cloudstack.acl.RoleType;
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.BaseCmd;
import org.apache.cloudstack.api.Parameter;
import org.apache.cloudstack.api.ServerApiException;
import org.apache.cloudstack.api.response.ExtensionCustomActionResponse;
import org.apache.cloudstack.api.response.SuccessResponse;
import org.apache.cloudstack.extension.ExtensionCustomAction;
import org.apache.cloudstack.framework.extensions.manager.ExtensionsManager;
import com.cloud.exception.ConcurrentOperationException;
import com.cloud.user.Account;
@APICommand(name = "deleteCustomAction",
description = "Delete the custom action",
responseObject = SuccessResponse.class,
responseHasSensitiveInfo = false,
entityType = {ExtensionCustomAction.class},
authorized = {RoleType.Admin},
since = "4.21.0")
public class DeleteCustomActionCmd extends BaseCmd {
@Inject
ExtensionsManager extensionsManager;
/////////////////////////////////////////////////////
//////////////// API parameters /////////////////////
/////////////////////////////////////////////////////
@Parameter(name = ApiConstants.ID, type = CommandType.UUID,
entityType = ExtensionCustomActionResponse.class, description = "uuid of the custom action")
private Long id;
/////////////////////////////////////////////////////
/////////////////// Accessors ///////////////////////
/////////////////////////////////////////////////////
public Long getId() {
return id;
}
/////////////////////////////////////////////////////
/////////////// API Implementation///////////////////
/////////////////////////////////////////////////////
@Override
public void execute() throws ServerApiException, ConcurrentOperationException {
boolean result = extensionsManager.deleteCustomAction(this);
if (result) {
SuccessResponse response = new SuccessResponse(getCommandName());
response.setSuccess(result);
setResponseObject(response);
} else {
throw new ServerApiException(ApiErrorCode.INTERNAL_ERROR, "Failed to delete extension custom action");
}
}
@Override
public long getEntityOwnerId() {
return Account.ACCOUNT_ID_SYSTEM;
}
@Override
public ApiCommandResourceType getApiResourceType() {
return ApiCommandResourceType.ExtensionCustomAction;
}
@Override
public Long getApiResourceId() {
return getId();
}
}

View File

@ -0,0 +1,104 @@
// 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.framework.extensions.api;
import javax.inject.Inject;
import org.apache.cloudstack.acl.RoleType;
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.BaseCmd;
import org.apache.cloudstack.api.Parameter;
import org.apache.cloudstack.api.ServerApiException;
import org.apache.cloudstack.api.response.ExtensionResponse;
import org.apache.cloudstack.api.response.SuccessResponse;
import org.apache.cloudstack.extension.Extension;
import org.apache.cloudstack.framework.extensions.manager.ExtensionsManager;
import com.cloud.exception.ConcurrentOperationException;
import com.cloud.user.Account;
@APICommand(name = "deleteExtension",
description = "Delete the extensions",
responseObject = ExtensionResponse.class,
responseHasSensitiveInfo = false,
entityType = {Extension.class},
authorized = {RoleType.Admin},
since = "4.21.0")
public class DeleteExtensionCmd extends BaseCmd {
@Inject
ExtensionsManager extensionsManager;
/////////////////////////////////////////////////////
//////////////// API parameters /////////////////////
/////////////////////////////////////////////////////
@Parameter(name = ApiConstants.ID, type = CommandType.UUID,
entityType = ExtensionResponse.class, description = "ID of the extension")
private Long id;
@Parameter(name = ApiConstants.CLEANUP, type = CommandType.BOOLEAN,
entityType = ExtensionResponse.class, description = "Whether cleanup entry-point files for the extension")
private Boolean cleanup;
/////////////////////////////////////////////////////
/////////////////// Accessors ///////////////////////
/////////////////////////////////////////////////////
public Long getId() {
return id;
}
public boolean isCleanup() {
return Boolean.TRUE.equals(cleanup);
}
/////////////////////////////////////////////////////
/////////////// API Implementation///////////////////
/////////////////////////////////////////////////////
@Override
public void execute() throws ServerApiException, ConcurrentOperationException {
boolean result = extensionsManager.deleteExtension(this);
if (result) {
SuccessResponse response = new SuccessResponse(getCommandName());
response.setSuccess(result);
setResponseObject(response);
} else {
throw new ServerApiException(ApiErrorCode.INTERNAL_ERROR, "Failed to delete extension");
}
}
@Override
public long getEntityOwnerId() {
return Account.ACCOUNT_ID_SYSTEM;
}
@Override
public ApiCommandResourceType getApiResourceType() {
return ApiCommandResourceType.Extension;
}
@Override
public Long getApiResourceId() {
return getId();
}
}

View File

@ -0,0 +1,110 @@
// 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.framework.extensions.api;
import java.util.List;
import javax.inject.Inject;
import org.apache.cloudstack.acl.RoleType;
import org.apache.cloudstack.api.APICommand;
import org.apache.cloudstack.api.ApiConstants;
import org.apache.cloudstack.api.BaseListCmd;
import org.apache.cloudstack.api.Parameter;
import org.apache.cloudstack.api.ServerApiException;
import org.apache.cloudstack.api.response.ExtensionCustomActionResponse;
import org.apache.cloudstack.api.response.ExtensionResponse;
import org.apache.cloudstack.api.response.ListResponse;
import org.apache.cloudstack.extension.ExtensionCustomAction;
import org.apache.cloudstack.framework.extensions.manager.ExtensionsManager;
@APICommand(name = "listCustomActions",
description = "Lists the custom actions",
responseObject = ExtensionCustomActionResponse.class,
responseHasSensitiveInfo = false,
entityType = {ExtensionCustomAction.class},
authorized = {RoleType.Admin, RoleType.DomainAdmin, RoleType.ResourceAdmin, RoleType.User},
since = "4.21.0")
public class ListCustomActionCmd extends BaseListCmd {
@Inject
ExtensionsManager extensionsManager;
@Parameter(name = ApiConstants.ID, type = CommandType.UUID,
entityType = ExtensionCustomActionResponse.class, description = "uuid of the custom action")
private Long id;
@Parameter(name = ApiConstants.NAME, type = CommandType.STRING, description = "Name of the custom action")
private String name;
@Parameter(name = ApiConstants.EXTENSION_ID, type = CommandType.UUID,
entityType = ExtensionResponse.class, description = "uuid of the extension")
private Long extensionId;
@Parameter(name = ApiConstants.RESOURCE_TYPE,
type = CommandType.STRING,
description = "Type of the resource for actions")
private String resourceType;
@Parameter(name = ApiConstants.RESOURCE_ID,
type = CommandType.STRING,
description = "ID of a resource for actions")
private String resourceId;
@Parameter(name = ApiConstants.ENABLED,
type = CommandType.BOOLEAN,
description = "List actions whether they are enabled or not")
private Boolean enabled;
/////////////////////////////////////////////////////
/////////////////// Accessors ///////////////////////
/////////////////////////////////////////////////////
public Long getId() {
return id;
}
public String getName() {
return name;
}
public Long getExtensionId() {
return extensionId;
}
public String getResourceType() {
return resourceType;
}
public String getResourceId() {
return resourceId;
}
public Boolean isEnabled() {
return enabled;
}
@Override
public void execute() throws ServerApiException {
List<ExtensionCustomActionResponse> responses = extensionsManager.listCustomActions(this);
ListResponse<ExtensionCustomActionResponse> response = new ListResponse<>();
response.setResponses(responses, responses.size());
response.setResponseName(getCommandName());
setResponseObject(response);
}
}

View File

@ -0,0 +1,114 @@
// 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.framework.extensions.api;
import java.util.EnumSet;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
import javax.inject.Inject;
import org.apache.cloudstack.acl.RoleType;
import org.apache.cloudstack.api.APICommand;
import org.apache.cloudstack.api.ApiConstants;
import org.apache.cloudstack.api.BaseListCmd;
import org.apache.cloudstack.api.Parameter;
import org.apache.cloudstack.api.ServerApiException;
import org.apache.cloudstack.api.response.ExtensionResponse;
import org.apache.cloudstack.api.response.ListResponse;
import org.apache.cloudstack.extension.Extension;
import org.apache.cloudstack.framework.extensions.manager.ExtensionsManager;
import org.apache.commons.collections.CollectionUtils;
import com.cloud.exception.ConcurrentOperationException;
import com.cloud.exception.InvalidParameterValueException;
@APICommand(name = "listExtensions",
description = "Lists extensions",
responseObject = ExtensionResponse.class,
responseHasSensitiveInfo = false,
entityType = {Extension.class},
authorized = {RoleType.Admin},
since = "4.21.0")
public class ListExtensionsCmd extends BaseListCmd {
@Inject
ExtensionsManager extensionsManager;
/////////////////////////////////////////////////////
//////////////// API parameters /////////////////////
/////////////////////////////////////////////////////
@Parameter(name = ApiConstants.NAME, type = CommandType.STRING, description = "Name of the extension")
private String name;
@Parameter(name = ApiConstants.ID, type = CommandType.UUID,
entityType = ExtensionResponse.class, description = "uuid of the extension")
private Long extensionId;
@Parameter(name = ApiConstants.DETAILS,
type = CommandType.LIST,
collectionType = CommandType.STRING,
description = "comma separated list of extension details requested, "
+ "value can be a list of [all, resources, external, min]."
+ " When no parameters are passed, all the details are returned.")
private List<String> details;
/////////////////////////////////////////////////////
/////////////////// Accessors ///////////////////////
/////////////////////////////////////////////////////
public String getName() {
return name;
}
public Long getExtensionId() {
return extensionId;
}
public EnumSet<ApiConstants.ExtensionDetails> getDetails() throws InvalidParameterValueException {
if (CollectionUtils.isEmpty(details)) {
return EnumSet.of(ApiConstants.ExtensionDetails.all);
}
try {
Set<ApiConstants.ExtensionDetails> detailsSet = new HashSet<>();
for (String detail : details) {
detailsSet.add(ApiConstants.ExtensionDetails.valueOf(detail));
}
return EnumSet.copyOf(detailsSet);
} catch (IllegalArgumentException e) {
throw new InvalidParameterValueException("The details parameter contains a non permitted value." +
"The allowed values are " + EnumSet.allOf(ApiConstants.ExtensionDetails.class));
}
}
/////////////////////////////////////////////////////
/////////////// API Implementation///////////////////
/////////////////////////////////////////////////////
@Override
public void execute() throws ServerApiException, ConcurrentOperationException {
List<ExtensionResponse> responses = extensionsManager.listExtensions(this);
ListResponse<ExtensionResponse> response = new ListResponse<>();
response.setResponses(responses, responses.size());
response.setResponseName(getCommandName());
setResponseObject(response);
}
}

View File

@ -0,0 +1,118 @@
// 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.framework.extensions.api;
import java.util.EnumSet;
import java.util.Map;
import javax.inject.Inject;
import org.apache.cloudstack.acl.RoleType;
import org.apache.cloudstack.api.APICommand;
import org.apache.cloudstack.api.ApiCommandResourceType;
import org.apache.cloudstack.api.ApiConstants;
import org.apache.cloudstack.api.BaseCmd;
import org.apache.cloudstack.api.Parameter;
import org.apache.cloudstack.api.ServerApiException;
import org.apache.cloudstack.api.response.ExtensionResponse;
import org.apache.cloudstack.extension.Extension;
import org.apache.cloudstack.framework.extensions.manager.ExtensionsManager;
import com.cloud.exception.ConcurrentOperationException;
import com.cloud.user.Account;
@APICommand(name = "registerExtension",
description = "Register an extension with a resource",
responseObject = ExtensionResponse.class,
responseHasSensitiveInfo = false,
entityType = {Extension.class},
authorized = {RoleType.Admin},
since = "4.21.0")
public class RegisterExtensionCmd extends BaseCmd {
@Inject
ExtensionsManager extensionsManager;
/////////////////////////////////////////////////////
//////////////// API parameters /////////////////////
/////////////////////////////////////////////////////
@Parameter(name = ApiConstants.EXTENSION_ID, type = CommandType.UUID, required = true,
entityType = ExtensionResponse.class, description = "ID of the extension")
private Long extensionId;
@Parameter(name = ApiConstants.RESOURCE_ID, type = CommandType.STRING, required = true,
description = "ID of the resource to register the extension with")
private String resourceId;
@Parameter(name = ApiConstants.RESOURCE_TYPE, type = CommandType.STRING, required = true,
description = "Type of the resource")
private String resourceType;
@Parameter(name = ApiConstants.DETAILS, type = CommandType.MAP,
description = "Details in key/value pairs using format details[i].keyname=keyvalue. Example: details[0].endpoint.url=urlvalue")
protected Map details;
/////////////////////////////////////////////////////
/////////////////// Accessors ///////////////////////
/////////////////////////////////////////////////////
public Long getExtensionId() {
return extensionId;
}
public String getResourceId() {
return resourceId;
}
public String getResourceType() {
return resourceType;
}
public Map<String, String> getDetails() {
return convertDetailsToMap(details);
}
/////////////////////////////////////////////////////
/////////////// API Implementation///////////////////
/////////////////////////////////////////////////////
@Override
public void execute() throws ServerApiException, ConcurrentOperationException {
Extension extension = extensionsManager.registerExtensionWithResource(this);
ExtensionResponse response = extensionsManager.createExtensionResponse(extension,
EnumSet.of(ApiConstants.ExtensionDetails.all));
response.setResponseName(getCommandName());
setResponseObject(response);
}
@Override
public long getEntityOwnerId() {
return Account.ACCOUNT_ID_SYSTEM;
}
@Override
public ApiCommandResourceType getApiResourceType() {
return ApiCommandResourceType.Extension;
}
@Override
public Long getApiResourceId() {
return getExtensionId();
}
}

View File

@ -0,0 +1,121 @@
// 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.framework.extensions.api;
import java.util.Map;
import javax.inject.Inject;
import org.apache.cloudstack.acl.RoleType;
import org.apache.cloudstack.api.APICommand;
import org.apache.cloudstack.api.ApiConstants;
import org.apache.cloudstack.api.ApiErrorCode;
import org.apache.cloudstack.api.BaseAsyncCmd;
import org.apache.cloudstack.api.Parameter;
import org.apache.cloudstack.api.ServerApiException;
import org.apache.cloudstack.api.response.ExtensionCustomActionResponse;
import org.apache.cloudstack.extension.CustomActionResultResponse;
import org.apache.cloudstack.extension.ExtensionCustomAction;
import org.apache.cloudstack.framework.extensions.manager.ExtensionsManager;
import com.cloud.event.EventTypes;
import com.cloud.exception.ConcurrentOperationException;
import com.cloud.user.Account;
@APICommand(name = "runCustomAction",
description = "Run the custom action",
responseObject = CustomActionResultResponse.class,
responseHasSensitiveInfo = false,
entityType = {ExtensionCustomAction.class},
authorized = {RoleType.Admin, RoleType.DomainAdmin, RoleType.ResourceAdmin, RoleType.User},
since = "4.21.0")
public class RunCustomActionCmd extends BaseAsyncCmd {
@Inject
ExtensionsManager extensionsManager;
/////////////////////////////////////////////////////
//////////////// API parameters /////////////////////
/////////////////////////////////////////////////////
@Parameter(name = ApiConstants.CUSTOM_ACTION_ID, type = CommandType.UUID, required = true,
entityType = ExtensionCustomActionResponse.class, description = "ID of the custom action")
private Long customActionId;
@Parameter(name = ApiConstants.RESOURCE_TYPE, type = CommandType.STRING,
description = "Type of the resource")
private String resourceType;
@Parameter(name = ApiConstants.RESOURCE_ID, type = CommandType.STRING, required = true,
description = "ID of the instance")
private String resourceId;
@Parameter(name = ApiConstants.PARAMETERS, type = CommandType.MAP,
description = "Parameters in key/value pairs using format parameters[i].keyname=keyvalue. Example: parameters[0].endpoint.url=urlvalue")
protected Map parameters;
/////////////////////////////////////////////////////
/////////////////// Accessors ///////////////////////
/////////////////////////////////////////////////////
public Long getCustomActionId() {
return customActionId;
}
public String getResourceType() {
return resourceType;
}
public String getResourceId() {
return resourceId;
}
public Map<String, String> getParameters() {
return convertDetailsToMap(parameters);
}
/////////////////////////////////////////////////////
/////////////// API Implementation///////////////////
/////////////////////////////////////////////////////
@Override
public void execute() throws ServerApiException, ConcurrentOperationException {
CustomActionResultResponse response = extensionsManager.runCustomAction(this);
if (response != null) {
response.setResponseName(getCommandName());
setResponseObject(response);
} else {
throw new ServerApiException(ApiErrorCode.INTERNAL_ERROR, "Failed to run custom action");
}
}
@Override
public long getEntityOwnerId() {
return Account.ACCOUNT_ID_SYSTEM;
}
@Override
public String getEventType() {
return EventTypes.EVENT_CUSTOM_ACTION;
}
@Override
public String getEventDescription() {
return "Running custom action";
}
}

View File

@ -0,0 +1,109 @@
// 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.framework.extensions.api;
import java.util.EnumSet;
import javax.inject.Inject;
import org.apache.cloudstack.acl.RoleType;
import org.apache.cloudstack.api.APICommand;
import org.apache.cloudstack.api.ApiCommandResourceType;
import org.apache.cloudstack.api.ApiConstants;
import org.apache.cloudstack.api.BaseCmd;
import org.apache.cloudstack.api.Parameter;
import org.apache.cloudstack.api.ServerApiException;
import org.apache.cloudstack.api.response.ExtensionResponse;
import org.apache.cloudstack.extension.Extension;
import org.apache.cloudstack.framework.extensions.manager.ExtensionsManager;
import com.cloud.exception.ConcurrentOperationException;
import com.cloud.user.Account;
@APICommand(name = "unregisterExtension",
description = "Unregister an extension with a resource",
responseObject = ExtensionResponse.class,
responseHasSensitiveInfo = false,
entityType = {Extension.class},
authorized = {RoleType.Admin},
since = "4.21.0")
public class UnregisterExtensionCmd extends BaseCmd {
@Inject
ExtensionsManager extensionsManager;
/////////////////////////////////////////////////////
//////////////// API parameters /////////////////////
/////////////////////////////////////////////////////
@Parameter(name = ApiConstants.EXTENSION_ID, type = CommandType.UUID, required = true,
entityType = ExtensionResponse.class, description = "ID of the extension")
private Long extensionId;
@Parameter(name = ApiConstants.RESOURCE_ID, type = CommandType.STRING, required = true,
description = "ID of the resource to register the extension with")
private String resourceId;
@Parameter(name = ApiConstants.RESOURCE_TYPE, type = CommandType.STRING, required = true,
description = "Type of the resource")
private String resourceType;
/////////////////////////////////////////////////////
/////////////////// Accessors ///////////////////////
/////////////////////////////////////////////////////
public Long getExtensionId() {
return extensionId;
}
public String getResourceId() {
return resourceId;
}
public String getResourceType() {
return resourceType;
}
/////////////////////////////////////////////////////
/////////////// API Implementation///////////////////
/////////////////////////////////////////////////////
@Override
public void execute() throws ServerApiException, ConcurrentOperationException {
Extension extension = extensionsManager.unregisterExtensionWithResource(this);
ExtensionResponse response = extensionsManager.createExtensionResponse(extension,
EnumSet.of(ApiConstants.ExtensionDetails.all));
response.setResponseName(getCommandName());
setResponseObject(response);
}
@Override
public long getEntityOwnerId() {
return Account.ACCOUNT_ID_SYSTEM;
}
@Override
public ApiCommandResourceType getApiResourceType() {
return ApiCommandResourceType.Extension;
}
@Override
public Long getApiResourceId() {
return getExtensionId();
}
}

View File

@ -0,0 +1,197 @@
// 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.framework.extensions.api;
import java.util.List;
import java.util.Map;
import javax.inject.Inject;
import org.apache.cloudstack.api.APICommand;
import org.apache.cloudstack.api.ApiCommandResourceType;
import org.apache.cloudstack.api.ApiConstants;
import org.apache.cloudstack.api.BaseCmd;
import org.apache.cloudstack.api.Parameter;
import org.apache.cloudstack.api.ServerApiException;
import org.apache.cloudstack.api.response.ExtensionCustomActionResponse;
import org.apache.cloudstack.api.response.SuccessResponse;
import org.apache.cloudstack.extension.ExtensionCustomAction;
import org.apache.cloudstack.framework.extensions.manager.ExtensionsManager;
import com.cloud.user.Account;
@APICommand(name = "updateCustomAction",
description = "Update the custom action",
responseObject = SuccessResponse.class,
responseHasSensitiveInfo = false, since = "4.21.0")
public class UpdateCustomActionCmd extends BaseCmd {
@Inject
ExtensionsManager extensionsManager;
@Parameter(name = ApiConstants.ID,
type = CommandType.UUID,
required = true,
entityType = ExtensionCustomActionResponse.class,
description = "ID of the custom action")
private Long id;
@Parameter(name = ApiConstants.DESCRIPTION,
type = CommandType.STRING,
description = "The description of the command")
private String description;
@Parameter(name = ApiConstants.RESOURCE_TYPE,
type = CommandType.STRING,
description = "Type of the resource for actions")
private String resourceType;
@Parameter(name = ApiConstants.ALLOWED_ROLE_TYPES,
type = CommandType.LIST,
collectionType = CommandType.STRING,
description = "List of role types allowed for the action")
private List<String> allowedRoleTypes;
@Parameter(name = ApiConstants.ENABLED,
type = CommandType.BOOLEAN,
description = "Whether the action is enabled or not")
private Boolean enabled;
@Parameter(name = ApiConstants.PARAMETERS, type = CommandType.MAP,
description = "Parameters mapping for the action using keys - name, type, required. " +
"'name' is mandatory. If 'type' is not specified then STRING will be used. " +
"If 'required' is not specified then false will be used. "
+ "Example: parameters[0].name=xxx&parameters[0].type=BOOLEAN&parameters[0].required=true")
protected Map parameters;
@Parameter(name = ApiConstants.CLEAN_UP_PARAMETERS,
type = CommandType.BOOLEAN,
description = "Optional boolean field, which indicates if parameters should be cleaned up or not " +
"(If set to true, parameters will be removed for this action, parameters field ignored; " +
"if false or not set, no action)")
private Boolean cleanupParameters;
@Parameter(name = ApiConstants.SUCCESS_MESSAGE, type = CommandType.STRING,
description = "Success message that will be used on successful execution of the action. " +
"Name of the action and and extension can be used in the - actionName, extensionName. "
+ "Example: Successfully complete {{actionName}} for {{extensionName")
protected String successMessage;
@Parameter(name = ApiConstants.ERROR_MESSAGE, type = CommandType.STRING,
description = "Error message that will be used on failure during execution of the action. " +
"Name of the action and and extension can be used in the - actionName, extensionName. "
+ "Example: Failed to complete {{actionName}} for {{extensionName")
protected String errorMessage;
@Parameter(name = ApiConstants.TIMEOUT,
type = CommandType.INTEGER,
description = "Specifies the timeout in seconds to wait for the action to complete before failing. Default value is 3 seconds")
private Integer timeout;
@Parameter(name = ApiConstants.DETAILS,
type = CommandType.MAP,
description = "Details in key/value pairs using format details[i].keyname=keyvalue. "
+ "Example: details[0].vendor=xxx&&details[0].version=2.0")
protected Map details;
@Parameter(name = ApiConstants.CLEAN_UP_DETAILS,
type = CommandType.BOOLEAN,
description = "Optional boolean field, which indicates if details should be cleaned up or not " +
"(If set to true, details removed for this action, details field ignored; " +
"if false or not set, no action)")
private Boolean cleanupDetails;
/////////////////////////////////////////////////////
/////////////////// Accessors ///////////////////////
/////////////////////////////////////////////////////
public long getId() {
return id;
}
public String getDescription() {
return description;
}
public String getResourceType() {
return resourceType;
}
public List<String> getAllowedRoleTypes() {
return allowedRoleTypes;
}
public Map getParametersMap() {
return parameters;
}
public Boolean isCleanupParameters() {
return cleanupParameters;
}
public String getSuccessMessage() {
return successMessage;
}
public String getErrorMessage() {
return errorMessage;
}
public Integer getTimeout() {
return timeout;
}
public Boolean isEnabled() {
return enabled;
}
public Map<String, String> getDetails() {
return convertDetailsToMap(details);
}
public Boolean isCleanupDetails() {
return cleanupDetails;
}
/////////////////////////////////////////////////////
/////////////// API Implementation///////////////////
/////////////////////////////////////////////////////
@Override
public void execute() throws ServerApiException {
ExtensionCustomAction extensionCustomAction = extensionsManager.updateCustomAction(this);
ExtensionCustomActionResponse response = extensionsManager.createCustomActionResponse(extensionCustomAction);
response.setResponseName(getCommandName());
setResponseObject(response);
}
@Override
public long getEntityOwnerId() {
return Account.ACCOUNT_ID_SYSTEM;
}
@Override
public ApiCommandResourceType getApiResourceType() {
return ApiCommandResourceType.ExtensionCustomAction;
}
@Override
public Long getApiResourceId() {
return getId();
}
}

View File

@ -0,0 +1,136 @@
// 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.framework.extensions.api;
import java.util.EnumSet;
import java.util.Map;
import javax.inject.Inject;
import org.apache.cloudstack.api.APICommand;
import org.apache.cloudstack.api.ApiCommandResourceType;
import org.apache.cloudstack.api.ApiConstants;
import org.apache.cloudstack.api.BaseCmd;
import org.apache.cloudstack.api.Parameter;
import org.apache.cloudstack.api.ServerApiException;
import org.apache.cloudstack.api.response.ExtensionResponse;
import org.apache.cloudstack.extension.Extension;
import org.apache.cloudstack.framework.extensions.manager.ExtensionsManager;
import com.cloud.user.Account;
@APICommand(name = "updateExtension",
description = "Update the extension",
responseObject = ExtensionResponse.class,
responseHasSensitiveInfo = false,
since = "4.21.0")
public class UpdateExtensionCmd extends BaseCmd {
@Inject
ExtensionsManager extensionsManager;
/////////////////////////////////////////////////////
//////////////// API parameters /////////////////////
/////////////////////////////////////////////////////
@Parameter(name = ApiConstants.ID, type = CommandType.UUID,
entityType = ExtensionResponse.class,
required = true,
description = "The ID of the extension")
private Long id;
@Parameter(name = ApiConstants.DESCRIPTION, type = CommandType.STRING,
description = "Description of the extension")
private String description;
@Parameter(name = ApiConstants.ORCHESTRATOR_REQUIRES_PREPARE_VM,
type = CommandType.BOOLEAN,
description = "Only honored when type is Orchestrator. Whether prepare VM is needed or not")
private Boolean orchestratorRequiresPrepareVm;
@Parameter(name = ApiConstants.STATE, type = CommandType.STRING,
description = "State of the extension")
private String state;
@Parameter(name = ApiConstants.DETAILS, type = CommandType.MAP,
description = "Details in key/value pairs using format details[i].keyname=keyvalue. Example: details[0].endpoint.url=urlvalue")
protected Map details;
@Parameter(name = ApiConstants.CLEAN_UP_DETAILS,
type = CommandType.BOOLEAN,
description = "Optional boolean field, which indicates if details should be cleaned up or not " +
"(If set to true, details removed for this action, details field ignored; " +
"if false or not set, no action)")
private Boolean cleanupDetails;
/////////////////////////////////////////////////////
/////////////////// Accessors ///////////////////////
/////////////////////////////////////////////////////
public Long getId() {
return id;
}
public String getDescription() {
return description;
}
public Boolean isOrchestratorRequiresPrepareVm() {
return orchestratorRequiresPrepareVm;
}
public String getState() {
return state;
}
public Map<String, String> getDetails() {
return convertDetailsToMap(details);
}
public Boolean isCleanupDetails() {
return cleanupDetails;
}
/////////////////////////////////////////////////////
/////////////// API Implementation///////////////////
/////////////////////////////////////////////////////
@Override
public void execute() throws ServerApiException {
Extension extension = extensionsManager.updateExtension(this);
ExtensionResponse response = extensionsManager.createExtensionResponse(extension,
EnumSet.of(ApiConstants.ExtensionDetails.all));
response.setResponseName(getCommandName());
setResponseObject(response);
}
@Override
public long getEntityOwnerId() {
return Account.ACCOUNT_ID_SYSTEM;
}
@Override
public ApiCommandResourceType getApiResourceType() {
return ApiCommandResourceType.Extension;
}
@Override
public Long getApiResourceId() {
return getId();
}
}

View File

@ -0,0 +1,27 @@
// 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.framework.extensions.command;
import org.apache.cloudstack.extension.Extension;
public class CleanupExtensionFilesCommand extends ExtensionServerActionBaseCommand {
public CleanupExtensionFilesCommand(long msId, Extension extension) {
super(msId, extension);
}
}

View File

@ -0,0 +1,63 @@
// 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.framework.extensions.command;
import org.apache.cloudstack.extension.Extension;
import com.cloud.agent.api.Command;
public class ExtensionBaseCommand extends Command {
private final long extensionId;
private final String extensionName;
private final boolean extensionUserDefined;
private final String extensionRelativePath;
private final Extension.State extensionState;
protected ExtensionBaseCommand(Extension extension) {
this.extensionId = extension.getId();
this.extensionName = extension.getName();
this.extensionUserDefined = extension.isUserDefined();
this.extensionRelativePath = extension.getRelativePath();
this.extensionState = extension.getState();
}
public long getExtensionId() {
return extensionId;
}
public String getExtensionName() {
return extensionName;
}
public boolean isExtensionUserDefined() {
return extensionUserDefined;
}
public String getExtensionRelativePath() {
return extensionRelativePath;
}
public Extension.State getExtensionState() {
return extensionState;
}
@Override
public boolean executeInSequence() {
return false;
}
}

View File

@ -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.framework.extensions.command;
import org.apache.cloudstack.extension.Extension;
public class ExtensionRoutingUpdateCommand extends ExtensionBaseCommand {
final boolean removed;
public ExtensionRoutingUpdateCommand(Extension extension, boolean removed) {
super(extension);
this.removed = removed;
}
public boolean isRemoved() {
return removed;
}
}

View File

@ -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.framework.extensions.command;
import org.apache.cloudstack.extension.Extension;
public class ExtensionServerActionBaseCommand extends ExtensionBaseCommand {
private final long msId;
protected ExtensionServerActionBaseCommand(long msId, Extension extension) {
super(extension);
this.msId = msId;
}
public long getMsId() {
return msId;
}
}

View File

@ -0,0 +1,27 @@
// 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.framework.extensions.command;
import org.apache.cloudstack.extension.Extension;
public class GetExtensionPathChecksumCommand extends ExtensionServerActionBaseCommand {
public GetExtensionPathChecksumCommand(long msId, Extension extension) {
super(msId, extension);
}
}

Some files were not shown because too many files have changed in this diff Show More