diff --git a/.asf.yaml b/.asf.yaml index 0603bf8e487..be818fda4d3 100644 --- a/.asf.yaml +++ b/.asf.yaml @@ -53,13 +53,12 @@ github: - acs-robot - gpordeus - hsato03 - - bernardodemarco - FelipeM525 - lucas-a-martins - nicoschmdt - abh1sar - - sudo87 - rosi-shapeblue + - sudo87 protected_branches: ~ diff --git a/.github/linters/.markdown-lint.yml b/.github/linters/.markdown-lint.yml index 5e8d65905eb..531c26ace44 100644 --- a/.github/linters/.markdown-lint.yml +++ b/.github/linters/.markdown-lint.yml @@ -86,3 +86,6 @@ MD046: false # MD052/reference-links-images Reference links and images should use a label that is defined MD052: false + +# MD059/descriptive-link-text Link text should be descriptive +MD059: false diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 0d101777c70..8c61ca8e4dd 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -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 @@ -137,6 +140,7 @@ jobs: smoke/test_vm_deployment_planner smoke/test_vm_strict_host_tags smoke/test_vm_schedule + smoke/test_deploy_vgpu_enabled_vm smoke/test_vm_life_cycle smoke/test_vm_lifecycle_unmanage_import smoke/test_vm_snapshot_kvm diff --git a/.github/workflows/ui.yml b/.github/workflows/ui.yml index 56f757133b7..88d4a70e4c2 100644 --- a/.github/workflows/ui.yml +++ b/.github/workflows/ui.yml @@ -56,6 +56,7 @@ jobs: npm run test:unit - uses: codecov/codecov-action@v4 + if: github.repository == 'apache/cloudstack' with: working-directory: ui files: ./coverage/lcov.info diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index a9bc87454f1..32b70145e59 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -15,18 +15,24 @@ # specific language governing permissions and limitations # under the License. --- -default_stages: [commit, push] +default_stages: [pre-commit, pre-push] default_language_version: # force all unspecified Python hooks to run python3 python: python3 -minimum_pre_commit_version: "2.17.0" +minimum_pre_commit_version: "3.2.0" repos: - repo: meta hooks: - id: identity - id: check-hooks-apply + - repo: https://github.com/gitleaks/gitleaks + rev: v8.27.2 + hooks: + - id: gitleaks + name: run gitleaks + description: detect hardcoded secrets - repo: https://github.com/pre-commit/pre-commit-hooks - rev: v4.6.0 + rev: v5.0.0 hooks: #- id: check-added-large-files - id: check-case-conflict @@ -69,7 +75,7 @@ repos: name: run codespell description: Check spelling with codespell args: [--ignore-words=.github/linters/codespell.txt] - exclude: ^ui/package\.json$|^ui/package-lock\.json$|^ui/public/js/less\.min\.js$|^ui/public/locales/.*[^n].*\.json$ + exclude: ^systemvm/agent/noVNC/|^ui/package\.json$|^ui/package-lock\.json$|^ui/public/js/less\.min\.js$|^ui/public/locales/.*[^n].*\.json$ - repo: https://github.com/pycqa/flake8 rev: 7.0.0 hooks: @@ -87,7 +93,7 @@ repos: ^setup/bindir/cloud-setup-encryption\.in$| ^venv/.*$ - repo: https://github.com/igorshubovych/markdownlint-cli - rev: v0.43.0 + rev: v0.45.0 hooks: - id: markdownlint name: run markdownlint diff --git a/README.md b/README.md index f66a4dc6f97..28d7d7396f4 100644 --- a/README.md +++ b/README.md @@ -160,3 +160,11 @@ The following provides more details on the included cryptographic software: * CloudStack makes use of the Bouncy Castle general-purpose encryption library. * CloudStack can optionally interact with and control OpenSwan-based VPNs. * CloudStack has a dependency on and makes use of JSch - a java SSH2 implementation. + +## Star History + +[![Apache CloudStack Star History](https://api.star-history.com/svg?repos=apache/cloudstack&type=Date)](https://www.star-history.com/#apache/cloudstack&Date) + +## Contributors + +[![Apache CloudStack Contributors](https://contrib.rocks/image?repo=apache/cloudstack&anon=0&max=500)](https://github.com/apache/cloudstack/graphs/contributors) diff --git a/agent/src/main/java/com/cloud/agent/properties/AgentProperties.java b/agent/src/main/java/com/cloud/agent/properties/AgentProperties.java index 69537621673..47255762a05 100644 --- a/agent/src/main/java/com/cloud/agent/properties/AgentProperties.java +++ b/agent/src/main/java/com/cloud/agent/properties/AgentProperties.java @@ -155,6 +155,14 @@ public class AgentProperties{ */ public static final Property CMDS_TIMEOUT = new Property<>("cmds.timeout", 7200); + /** + * The timeout (in seconds) for the snapshot merge operation, mainly used for classic volume snapshots and disk-only VM snapshots on file-based storage.
+ * This configuration is only considered if libvirt.events.enabled is also true.
+ * Data type: Integer.
+ * Default value: 259200 + */ + public static final Property QCOW2_DELTA_MERGE_TIMEOUT = new Property<>("qcow2.delta.merge.timeout", 60 * 60 * 72); + /** * This parameter sets the VM migration speed (in mbps). The default value is -1,
* which means that the agent will try to guess the speed of the guest network and consume all possible bandwidth.
@@ -213,6 +221,15 @@ public class AgentProperties{ */ public static final Property AGENT_HOOKS_LIBVIRT_VM_XML_TRANSFORMER_SCRIPT = new Property<>("agent.hooks.libvirt_vm_xml_transformer.script", "libvirt-vm-xml-transformer.groovy"); + /** + * This property is used with the agent.hooks.basedir property to define the Libvirt VM XML transformer shell script.
+ * The shell script is used to execute the Libvirt VM XML transformer script.
+ * For more information see the agent.properties file.
+ * Data type: String.
+ * Default value: libvirt-vm-xml-transformer.sh + */ + public static final Property AGENT_HOOKS_LIBVIRT_VM_XML_TRANSFORMER_SHELL_SCRIPT = new Property<>("agent.hooks.libvirt_vm_xml_transformer.shell_script", "libvirt-vm-xml-transformer.sh"); + /** * This property is used with the agent.hooks.basedir and agent.hooks.libvirt_vm_xml_transformer.script properties to define the Libvirt VM XML transformer method.
* Libvirt XML transformer hook does XML-to-XML transformation.
@@ -233,6 +250,15 @@ public class AgentProperties{ */ public static final Property AGENT_HOOKS_LIBVIRT_VM_ON_START_SCRIPT = new Property<>("agent.hooks.libvirt_vm_on_start.script", "libvirt-vm-state-change.groovy"); + /** + * This property is used with the agent.hooks.basedir property to define the Libvirt VM on start shell script.
+ * The shell script is used to execute the Libvirt VM on start script.
+ * For more information see the agent.properties file.
+ * Data type: String.
+ * Default value: libvirt-vm-state-change.sh + */ + public static final Property AGENT_HOOKS_LIBVIRT_VM_ON_START_SHELL_SCRIPT = new Property<>("agent.hooks.libvirt_vm_on_start.shell_script", "libvirt-vm-state-change.sh"); + /** * This property is used with the agent.hooks.basedir and agent.hooks.libvirt_vm_on_start.script properties to define the Libvirt VM on start method.
* The hook is called right after Libvirt successfully launched the VM.
@@ -252,6 +278,15 @@ public class AgentProperties{ */ public static final Property AGENT_HOOKS_LIBVIRT_VM_ON_STOP_SCRIPT = new Property<>("agent.hooks.libvirt_vm_on_stop.script", "libvirt-vm-state-change.groovy"); + /** + * This property is used with the agent.hooks.basedir property to define the Libvirt VM on stop shell script.
+ * The shell script is used to execute the Libvirt VM on stop script.
+ * For more information see the agent.properties file.
+ * Data type: String.
+ * Default value: libvirt-vm-state-change.sh + */ + public static final Property AGENT_HOOKS_LIBVIRT_VM_ON_STOP_SHELL_SCRIPT = new Property<>("agent.hooks.libvirt_vm_on_stop.shell_script", "libvirt-vm-state-change.sh"); + /** * This property is used with the agent.hooks.basedir and agent.hooks.libvirt_vm_on_stop.script properties to define the Libvirt VM on stop method.
* The hook is called right after libvirt successfully stopped the VM.
@@ -833,7 +868,7 @@ public class AgentProperties{ private T defaultValue; private Class typeClass; - Property(String name, T value) { + public Property(String name, T value) { init(name, value); } diff --git a/api/src/main/java/com/cloud/agent/api/Command.java b/api/src/main/java/com/cloud/agent/api/Command.java index 4766c30ead2..c4e99cb4170 100644 --- a/api/src/main/java/com/cloud/agent/api/Command.java +++ b/api/src/main/java/com/cloud/agent/api/Command.java @@ -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> externalDetails; protected Command() { this.wait = 0; @@ -128,6 +130,14 @@ public abstract class Command { this.requestSequence = requestSequence; } + public void setExternalDetails(Map> externalDetails) { + this.externalDetails = externalDetails; + } + + public Map> getExternalDetails() { + return externalDetails; + } + @Override public boolean equals(Object o) { if (this == o) return true; diff --git a/api/src/main/java/com/cloud/agent/api/VgpuTypesInfo.java b/api/src/main/java/com/cloud/agent/api/VgpuTypesInfo.java index 85ffc189820..5515a9c48bc 100644 --- a/api/src/main/java/com/cloud/agent/api/VgpuTypesInfo.java +++ b/api/src/main/java/com/cloud/agent/api/VgpuTypesInfo.java @@ -15,10 +15,24 @@ // specific language governing permissions and limitations // under the License. package com.cloud.agent.api; + +import org.apache.cloudstack.gpu.GpuDevice; + public class VgpuTypesInfo { + private boolean passthroughEnabled = true; + private GpuDevice.DeviceType deviceType; + private String parentBusAddress; + private String busAddress; + private String numaNode; + private String pciRoot; + private String deviceId; + private String deviceName; + private String vendorId; + private String vendorName; private String modelName; private String groupName; + private String vmName; private Long maxHeads; private Long videoRam; private Long maxResolutionX; @@ -26,6 +40,7 @@ public class VgpuTypesInfo { private Long maxVgpuPerGpu; private Long remainingCapacity; private Long maxCapacity; + private boolean display = false; public String getModelName() { return modelName; @@ -39,22 +54,42 @@ public class VgpuTypesInfo { return videoRam; } + public void setVideoRam(Long videoRam) { + this.videoRam = videoRam; + } + public Long getMaxHeads() { return maxHeads; } + public void setMaxHeads(Long maxHeads) { + this.maxHeads = maxHeads; + } + public Long getMaxResolutionX() { return maxResolutionX; } + public void setMaxResolutionX(Long maxResolutionX) { + this.maxResolutionX = maxResolutionX; + } + public Long getMaxResolutionY() { return maxResolutionY; } + public void setMaxResolutionY(Long maxResolutionY) { + this.maxResolutionY = maxResolutionY; + } + public Long getMaxVpuPerGpu() { return maxVgpuPerGpu; } + public void setMaxVgpuPerGpu(Long maxVgpuPerGpu) { + this.maxVgpuPerGpu = maxVgpuPerGpu; + } + public Long getRemainingCapacity() { return remainingCapacity; } @@ -71,8 +106,133 @@ public class VgpuTypesInfo { this.maxCapacity = maxCapacity; } - public VgpuTypesInfo(String groupName, String modelName, Long videoRam, Long maxHeads, Long maxResolutionX, Long maxResolutionY, Long maxVgpuPerGpu, - Long remainingCapacity, Long maxCapacity) { + public boolean isPassthroughEnabled() { + return passthroughEnabled; + } + + public void setPassthroughEnabled(boolean passthroughEnabled) { + this.passthroughEnabled = passthroughEnabled; + } + + public GpuDevice.DeviceType getDeviceType() { + return deviceType; + } + + public void setDeviceType(GpuDevice.DeviceType deviceType) { + this.deviceType = deviceType; + } + + public String getParentBusAddress() { + return parentBusAddress; + } + + public void setParentBusAddress(String parentBusAddress) { + this.parentBusAddress = parentBusAddress; + } + + public String getBusAddress() { + return busAddress; + } + + public void setBusAddress(String busAddress) { + this.busAddress = busAddress; + } + + public String getNumaNode() { + return numaNode; + } + + public void setNumaNode(String numaNode) { + this.numaNode = numaNode; + } + + public String getPciRoot() { + return pciRoot; + } + + public void setPciRoot(String pciRoot) { + this.pciRoot = pciRoot; + } + + public String getDeviceId() { + return deviceId; + } + + public void setDeviceId(String deviceId) { + this.deviceId = deviceId; + } + + public String getDeviceName() { + return deviceName; + } + + public void setDeviceName(String deviceName) { + this.deviceName = deviceName; + } + + public String getVendorId() { + return vendorId; + } + + public void setVendorId(String vendorId) { + this.vendorId = vendorId; + } + + public String getVendorName() { + return vendorName; + } + + public void setVendorName(String vendorName) { + this.vendorName = vendorName; + } + + public String getVmName() { + return vmName; + } + + public void setVmName(String vmName) { + this.vmName = vmName; + } + + public boolean isDisplay() { + return display; + } + + public void setDisplay(boolean display) { + this.display = display; + } + + public VgpuTypesInfo(GpuDevice.DeviceType deviceType, String groupName, String modelName, String busAddress, + String vendorId, String vendorName, String deviceId, String deviceName, String numaNode, String pciRoot + ) { + this.deviceType = deviceType; + this.groupName = groupName; + this.modelName = modelName; + this.busAddress = busAddress; + this.deviceId = deviceId; + this.deviceName = deviceName; + this.vendorId = vendorId; + this.vendorName = vendorName; + this.numaNode = numaNode; + this.pciRoot = pciRoot; + } + + public VgpuTypesInfo(GpuDevice.DeviceType deviceType, String groupName, String modelName, String busAddress, + String vendorId, String vendorName, String deviceId, String deviceName + ) { + this.deviceType = deviceType; + this.groupName = groupName; + this.modelName = modelName; + this.busAddress = busAddress; + this.deviceId = deviceId; + this.deviceName = deviceName; + this.vendorId = vendorId; + this.vendorName = vendorName; + } + + public VgpuTypesInfo(String groupName, String modelName, Long videoRam, Long maxHeads, Long maxResolutionX, + Long maxResolutionY, Long maxVgpuPerGpu, Long remainingCapacity, Long maxCapacity + ) { this.groupName = groupName; this.modelName = modelName; this.videoRam = videoRam; diff --git a/api/src/main/java/com/cloud/agent/api/to/FirewallRuleTO.java b/api/src/main/java/com/cloud/agent/api/to/FirewallRuleTO.java index 25c75001a3c..69350815be3 100644 --- a/api/src/main/java/com/cloud/agent/api/to/FirewallRuleTO.java +++ b/api/src/main/java/com/cloud/agent/api/to/FirewallRuleTO.java @@ -47,7 +47,7 @@ public class FirewallRuleTO implements InternalIdentity { int[] srcPortRange; boolean revoked; boolean alreadyAdded; - private List sourceCidrList; + protected List sourceCidrList; private List destCidrList; FirewallRule.Purpose purpose; private Integer icmpType; diff --git a/api/src/main/java/com/cloud/agent/api/to/GPUDeviceTO.java b/api/src/main/java/com/cloud/agent/api/to/GPUDeviceTO.java index 4afe080477b..6e9cee06dd3 100644 --- a/api/src/main/java/com/cloud/agent/api/to/GPUDeviceTO.java +++ b/api/src/main/java/com/cloud/agent/api/to/GPUDeviceTO.java @@ -16,7 +16,9 @@ // under the License. package com.cloud.agent.api.to; +import java.util.ArrayList; import java.util.HashMap; +import java.util.List; import com.cloud.agent.api.VgpuTypesInfo; @@ -24,9 +26,23 @@ public class GPUDeviceTO { private String gpuGroup; private String vgpuType; + private int gpuCount; private HashMap> groupDetails = new HashMap>(); + private List gpuDevices = new ArrayList<>(); - public GPUDeviceTO( String gpuGroup, String vgpuType, HashMap> groupDetails) { + public GPUDeviceTO(String gpuGroup, String vgpuType, int gpuCount, + HashMap> groupDetails, + List gpuDevices) { + this.gpuGroup = gpuGroup; + this.vgpuType = vgpuType; + this.groupDetails = groupDetails; + this.gpuCount = gpuCount; + this.gpuDevices = gpuDevices; + + } + + public GPUDeviceTO(String gpuGroup, String vgpuType, + HashMap> groupDetails) { this.gpuGroup = gpuGroup; this.vgpuType = vgpuType; this.groupDetails = groupDetails; @@ -48,6 +64,14 @@ public class GPUDeviceTO { this.vgpuType = vgpuType; } + public int getGpuCount() { + return gpuCount; + } + + public void setGpuCount(int gpuCount) { + this.gpuCount = gpuCount; + } + public HashMap> getGroupDetails() { return groupDetails; } @@ -56,4 +80,11 @@ public class GPUDeviceTO { this.groupDetails = groupDetails; } + public List getGpuDevices() { + return gpuDevices; + } + + public void setGpuDevices(List gpuDevices) { + this.gpuDevices = gpuDevices; + } } diff --git a/api/src/main/java/com/cloud/agent/api/to/PortForwardingRuleTO.java b/api/src/main/java/com/cloud/agent/api/to/PortForwardingRuleTO.java index d43625c09a9..91f337c5f55 100644 --- a/api/src/main/java/com/cloud/agent/api/to/PortForwardingRuleTO.java +++ b/api/src/main/java/com/cloud/agent/api/to/PortForwardingRuleTO.java @@ -21,8 +21,6 @@ import com.cloud.network.rules.PortForwardingRule; import com.cloud.utils.net.NetUtils; import org.apache.commons.lang3.StringUtils; -import java.util.List; - /** * PortForwardingRuleTO specifies one port forwarding rule. * @@ -32,8 +30,6 @@ public class PortForwardingRuleTO extends FirewallRuleTO { String dstIp; int[] dstPortRange; - List sourceCidrList; - protected PortForwardingRuleTO() { super(); } diff --git a/api/src/main/java/com/cloud/agent/api/to/VirtualMachineTO.java b/api/src/main/java/com/cloud/agent/api/to/VirtualMachineTO.java index d67ce679684..cffb9874080 100644 --- a/api/src/main/java/com/cloud/agent/api/to/VirtualMachineTO.java +++ b/api/src/main/java/com/cloud/agent/api/to/VirtualMachineTO.java @@ -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 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 + )); + } } diff --git a/api/src/main/java/com/cloud/configuration/ConfigurationService.java b/api/src/main/java/com/cloud/configuration/ConfigurationService.java index 13a44ef05b0..32e31519ea7 100644 --- a/api/src/main/java/com/cloud/configuration/ConfigurationService.java +++ b/api/src/main/java/com/cloud/configuration/ConfigurationService.java @@ -17,7 +17,11 @@ package com.cloud.configuration; import java.util.List; +import java.util.Map; +import java.util.Objects; +import com.cloud.network.Network; +import org.apache.cloudstack.api.ApiConstants; import org.apache.cloudstack.api.command.admin.config.ResetCfgCmd; import org.apache.cloudstack.api.command.admin.config.UpdateCfgCmd; import org.apache.cloudstack.api.command.admin.network.CreateGuestNetworkIpv6PrefixCmd; @@ -373,4 +377,16 @@ public interface ConfigurationService { List listPortableIps(long id); Boolean isAccountAllowedToCreateOfferingsWithTags(IsAccountAllowedToCreateOfferingsWithTagsCmd cmd); + + public static final Map ProviderDetailKeyMap = Map.of( + Network.Provider.Nsx.getName(), ApiConstants.NSX_DETAIL_KEY, + Network.Provider.Netris.getName(), ApiConstants.NETRIS_DETAIL_KEY + ); + + public static boolean IsIpRangeForProvider(Network.Provider provider) { + if (Objects.isNull(provider)) { + return false; + } + return ProviderDetailKeyMap.containsKey(provider.getName()); + } } diff --git a/api/src/main/java/com/cloud/configuration/Resource.java b/api/src/main/java/com/cloud/configuration/Resource.java index c7bf44de76c..97be7f9d64c 100644 --- a/api/src/main/java/com/cloud/configuration/Resource.java +++ b/api/src/main/java/com/cloud/configuration/Resource.java @@ -37,7 +37,8 @@ public interface Resource { backup("backup", 12), backup_storage("backup_storage", 13), bucket("bucket", 14), - object_storage("object_storage", 15); + object_storage("object_storage", 15), + gpu("gpu", 16); private String name; private int ordinal; diff --git a/api/src/main/java/com/cloud/event/EventTypes.java b/api/src/main/java/com/cloud/event/EventTypes.java index c8694e076b8..a8777d5c75b 100644 --- a/api/src/main/java/com/cloud/event/EventTypes.java +++ b/api/src/main/java/com/cloud/event/EventTypes.java @@ -29,13 +29,18 @@ 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.gpu.GpuCard; +import org.apache.cloudstack.gpu.GpuDevice; +import org.apache.cloudstack.gpu.VgpuProfile; 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; @@ -376,6 +381,21 @@ public class EventTypes { public static final String EVENT_DISK_OFFERING_EDIT = "DISK.OFFERING.EDIT"; public static final String EVENT_DISK_OFFERING_DELETE = "DISK.OFFERING.DELETE"; + // GPU Cards + public static final String EVENT_GPU_CARD_CREATE = "GPU.CARD.CREATE"; + public static final String EVENT_GPU_CARD_EDIT = "GPU.CARD.EDIT"; + public static final String EVENT_GPU_CARD_DELETE = "GPU.CARD.DELETE"; + + // vGPU Profile + public static final String EVENT_VGPU_PROFILE_CREATE = "VGPU.PROFILE.CREATE"; + public static final String EVENT_VGPU_PROFILE_EDIT = "VGPU.PROFILE.EDIT"; + public static final String EVENT_VGPU_PROFILE_DELETE = "VGPU.PROFILE.DELETE"; + + // GPU Devices + public static final String EVENT_GPU_DEVICE_CREATE = "GPU.DEVICE.CREATE"; + public static final String EVENT_GPU_DEVICE_EDIT = "GPU.DEVICE.EDIT"; + public static final String EVENT_GPU_DEVICE_DELETE = "GPU.DEVICE.DELETE"; + // Network offerings public static final String EVENT_NETWORK_OFFERING_CREATE = "NETWORK.OFFERING.CREATE"; public static final String EVENT_NETWORK_OFFERING_ASSIGN = "NETWORK.OFFERING.ASSIGN"; @@ -499,6 +519,8 @@ public class EventTypes { public static final String EVENT_ZONE_VLAN_ASSIGN = "ZONE.VLAN.ASSIGN"; public static final String EVENT_ZONE_VLAN_RELEASE = "ZONE.VLAN.RELEASE"; + public static final String EVENT_ZONE_VXLAN_ASSIGN = "ZONE.VXLAN.ASSIGN"; + public static final String EVENT_ZONE_VXLAN_RELEASE = "ZONE.VXLAN.RELEASE"; // Projects public static final String EVENT_PROJECT_CREATE = "PROJECT.CREATE"; @@ -804,11 +826,30 @@ 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"; public static final String VM_LEASE_EXPIRING = "VM.LEASE.EXPIRING"; + // GUI Theme + public static final String EVENT_GUI_THEME_CREATE = "GUI.THEME.CREATE"; + 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 @@ -1003,6 +1044,21 @@ public class EventTypes { entityEventDetails.put(EVENT_DISK_OFFERING_EDIT, DiskOffering.class); entityEventDetails.put(EVENT_DISK_OFFERING_DELETE, DiskOffering.class); + // GPU Cards + entityEventDetails.put(EVENT_GPU_CARD_CREATE, GpuCard.class); + entityEventDetails.put(EVENT_GPU_CARD_EDIT, GpuCard.class); + entityEventDetails.put(EVENT_GPU_CARD_DELETE, GpuCard.class); + + // vGPU Profiles + entityEventDetails.put(EVENT_VGPU_PROFILE_CREATE, VgpuProfile.class); + entityEventDetails.put(EVENT_VGPU_PROFILE_EDIT, VgpuProfile.class); + entityEventDetails.put(EVENT_VGPU_PROFILE_DELETE, VgpuProfile.class); + + // GPU Devices + entityEventDetails.put(EVENT_GPU_DEVICE_CREATE, GpuDevice.class); + entityEventDetails.put(EVENT_GPU_DEVICE_EDIT, GpuDevice.class); + entityEventDetails.put(EVENT_GPU_DEVICE_DELETE, GpuDevice.class); + // Network offerings entityEventDetails.put(EVENT_NETWORK_OFFERING_CREATE, NetworkOffering.class); entityEventDetails.put(EVENT_NETWORK_OFFERING_ASSIGN, NetworkOffering.class); @@ -1312,6 +1368,21 @@ public class EventTypes { entityEventDetails.put(VM_LEASE_EXPIRING, VirtualMachine.class); entityEventDetails.put(VM_LEASE_DISABLED, VirtualMachine.class); entityEventDetails.put(VM_LEASE_CANCELLED, VirtualMachine.class); + + // GUI theme + 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) { diff --git a/api/src/main/java/com/cloud/hypervisor/Hypervisor.java b/api/src/main/java/com/cloud/hypervisor/Hypervisor.java index 27ffef1c370..5baac484772 100644 --- a/api/src/main/java/com/cloud/hypervisor/Hypervisor.java +++ b/api/src/main/java/com/cloud/hypervisor/Hypervisor.java @@ -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; diff --git a/api/src/main/java/com/cloud/network/IpAddress.java b/api/src/main/java/com/cloud/network/IpAddress.java index ae1af450577..70d652b54e9 100644 --- a/api/src/main/java/com/cloud/network/IpAddress.java +++ b/api/src/main/java/com/cloud/network/IpAddress.java @@ -99,4 +99,5 @@ public interface IpAddress extends ControlledEntity, Identity, InternalIdentity, boolean isForSystemVms(); + boolean isForRouter(); } diff --git a/api/src/main/java/com/cloud/network/Network.java b/api/src/main/java/com/cloud/network/Network.java index d3bc5005cb7..dc94932e31f 100644 --- a/api/src/main/java/com/cloud/network/Network.java +++ b/api/src/main/java/com/cloud/network/Network.java @@ -206,6 +206,7 @@ public interface Network extends ControlledEntity, StateObject, I public static final Provider Tungsten = new Provider("Tungsten", false); public static final Provider Nsx = new Provider("Nsx", false); + public static final Provider Netris = new Provider("Netris", false); private final String name; private final boolean isExternal; diff --git a/api/src/main/java/com/cloud/network/NetworkModel.java b/api/src/main/java/com/cloud/network/NetworkModel.java index a4cd87af008..eb496ac4e0b 100644 --- a/api/src/main/java/com/cloud/network/NetworkModel.java +++ b/api/src/main/java/com/cloud/network/NetworkModel.java @@ -305,6 +305,8 @@ public interface NetworkModel { NicProfile getNicProfile(VirtualMachine vm, long networkId, String broadcastUri); + NicProfile getNicProfile(VirtualMachine vm, Nic nic, DataCenter dataCenter); + Set getAvailableIps(Network network, String requestedIp); String getDomainNetworkDomain(long domainId, long zoneId); diff --git a/api/src/main/java/com/cloud/network/NetworkService.java b/api/src/main/java/com/cloud/network/NetworkService.java index 36d58c737cc..fd51cbfa774 100644 --- a/api/src/main/java/com/cloud/network/NetworkService.java +++ b/api/src/main/java/com/cloud/network/NetworkService.java @@ -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 getInternalLoadBalancerElements(); boolean handleCksIsoOnNetworkVirtualRouter(Long virtualRouterId, boolean mount) throws ResourceUnavailableException; + + String getNicVlanValueForExternalVm(NicTO nic); } diff --git a/api/src/main/java/com/cloud/network/Networks.java b/api/src/main/java/com/cloud/network/Networks.java index dfa0ddb84ca..9f06a044111 100644 --- a/api/src/main/java/com/cloud/network/Networks.java +++ b/api/src/main/java/com/cloud/network/Networks.java @@ -129,7 +129,8 @@ public class Networks { UnDecided(null, null), OpenDaylight("opendaylight", String.class), TUNGSTEN("tf", String.class), - NSX("nsx", String.class); + NSX("nsx", String.class), + Netris("netris", String.class); private final String scheme; private final Class type; diff --git a/api/src/main/java/com/cloud/network/SDNProviderNetworkRule.java b/api/src/main/java/com/cloud/network/SDNProviderNetworkRule.java new file mode 100644 index 00000000000..a22db4287dc --- /dev/null +++ b/api/src/main/java/com/cloud/network/SDNProviderNetworkRule.java @@ -0,0 +1,358 @@ +// Licensed to the Apache Software Foundation (ASF) under one +// or more contributor license agreements. See the NOTICE file +// distributed with this work for additional information +// regarding copyright ownership. The ASF licenses this file +// to you under the Apache License, Version 2.0 (the +// "License"); you may not use this file except in compliance +// with the License. You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, +// software distributed under the License is distributed on an +// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +// KIND, either express or implied. See the License for the +// specific language governing permissions and limitations +// under the License. +package com.cloud.network; + +import java.util.List; + +public class SDNProviderNetworkRule { + + protected long domainId; + protected long accountId; + protected long zoneId; + protected Long networkResourceId; + protected String networkResourceName; + protected boolean isVpcResource; + protected long vmId; + protected long ruleId; + protected String publicIp; + protected String vmIp; + protected String publicPort; + protected String privatePort; + protected String protocol; + protected String algorithm; + protected List sourceCidrList; + protected List destinationCidrList; + protected Integer icmpCode; + + protected Integer icmpType; + protected String trafficType; + protected Network.Service service; + + public long getDomainId() { + return domainId; + } + + public void setDomainId(long domainId) { + this.domainId = domainId; + } + + public long getAccountId() { + return accountId; + } + + public void setAccountId(long accountId) { + this.accountId = accountId; + } + + public long getZoneId() { + return zoneId; + } + + public void setZoneId(long zoneId) { + this.zoneId = zoneId; + } + + public Long getNetworkResourceId() { + return networkResourceId; + } + + public void setNetworkResourceId(Long networkResourceId) { + this.networkResourceId = networkResourceId; + } + + public String getNetworkResourceName() { + return networkResourceName; + } + + public void setNetworkResourceName(String networkResourceName) { + this.networkResourceName = networkResourceName; + } + + public boolean isVpcResource() { + return isVpcResource; + } + + public void setVpcResource(boolean vpcResource) { + isVpcResource = vpcResource; + } + + public long getVmId() { + return vmId; + } + + public void setVmId(long vmId) { + this.vmId = vmId; + } + + public long getRuleId() { + return ruleId; + } + + public void setRuleId(long ruleId) { + this.ruleId = ruleId; + } + + public String getPublicIp() { + return publicIp; + } + + public void setPublicIp(String publicIp) { + this.publicIp = publicIp; + } + + public String getVmIp() { + return vmIp; + } + + public void setVmIp(String vmIp) { + this.vmIp = vmIp; + } + + public String getPublicPort() { + return publicPort; + } + + public void setPublicPort(String publicPort) { + this.publicPort = publicPort; + } + + public String getPrivatePort() { + return privatePort; + } + + public void setPrivatePort(String privatePort) { + this.privatePort = privatePort; + } + + public String getProtocol() { + return protocol; + } + + public void setProtocol(String protocol) { + this.protocol = protocol; + } + + public void setAlgorithm(String algorithm) { + this.algorithm = algorithm; + } + + public String getAlgorithm() { + return algorithm; + } + + public Network.Service getService() { + return service; + } + + public void setService(Network.Service service) { + this.service = service; + } + + public Integer getIcmpCode() { + return icmpCode; + } + + public void setIcmpCode(Integer icmpCode) { + this.icmpCode = icmpCode; + } + + public Integer getIcmpType() { + return icmpType; + } + + public void setIcmpType(Integer icmpType) { + this.icmpType = icmpType; + } + + public List getSourceCidrList() { + return sourceCidrList; + } + + public void setSourceCidrList(List sourceCidrList) { + this.sourceCidrList = sourceCidrList; + } + + public List getDestinationCidrList() { + return destinationCidrList; + } + + public void setDestinationCidrList(List destinationCidrList) { + this.destinationCidrList = destinationCidrList; + } + + public String getTrafficType() { + return trafficType; + } + + public void setTrafficType(String trafficType) { + this.trafficType = trafficType; + } + + public static class Builder { + public long domainId; + public long accountId; + public long zoneId; + public Long networkResourceId; + public String networkResourceName; + public boolean isVpcResource; + public long vmId; + + public long ruleId; + public String publicIp; + public String vmIp; + public String publicPort; + public String privatePort; + public String protocol; + public String algorithm; + public List sourceCidrList; + public List destinationCidrList; + public String trafficType; + public Integer icmpType; + public Integer icmpCode; + public Network.Service service; + + public Builder() { + // Default constructor + } + + public Builder setDomainId(long domainId) { + this.domainId = domainId; + return this; + } + + public Builder setAccountId(long accountId) { + this.accountId = accountId; + return this; + } + + public Builder setZoneId(long zoneId) { + this.zoneId = zoneId; + return this; + } + + public Builder setNetworkResourceId(Long networkResourceId) { + this.networkResourceId = networkResourceId; + return this; + } + + public Builder setNetworkResourceName(String networkResourceName) { + this.networkResourceName = networkResourceName; + return this; + } + + public Builder setVpcResource(boolean isVpcResource) { + this.isVpcResource = isVpcResource; + return this; + } + + + public Builder setVmId(long vmId) { + this.vmId = vmId; + return this; + } + + public Builder setRuleId(long ruleId) { + this.ruleId = ruleId; + return this; + } + + public Builder setPublicIp(String publicIp) { + this.publicIp = publicIp; + return this; + } + + public Builder setVmIp(String vmIp) { + this.vmIp = vmIp; + return this; + } + + public Builder setPublicPort(String publicPort) { + this.publicPort = publicPort; + return this; + } + + public Builder setPrivatePort(String privatePort) { + this.privatePort = privatePort; + return this; + } + + public Builder setProtocol(String protocol) { + this.protocol = protocol; + return this; + } + + public Builder setAlgorithm(String algorithm) { + this.algorithm = algorithm; + return this; + } + + public Builder setTrafficType(String trafficType) { + this.trafficType = trafficType; + return this; + } + + public Builder setIcmpType(Integer icmpType) { + this.icmpType = icmpType; + return this; + } + + public Builder setIcmpCode(Integer icmpCode) { + this.icmpCode = icmpCode; + return this; + } + + public Builder setSourceCidrList(List sourceCidrList) { + this.sourceCidrList = sourceCidrList; + return this; + } + + public Builder setDestinationCidrList(List destinationCidrList) { + this.destinationCidrList = destinationCidrList; + return this; + } + + public Builder setService(Network.Service service) { + this.service = service; + return this; + } + + public SDNProviderNetworkRule build() { + SDNProviderNetworkRule rule = new SDNProviderNetworkRule(); + rule.setDomainId(this.domainId); + rule.setAccountId(this.accountId); + rule.setZoneId(this.zoneId); + rule.setNetworkResourceId(this.networkResourceId); + rule.setNetworkResourceName(this.networkResourceName); + rule.setVpcResource(this.isVpcResource); + rule.setVmId(this.vmId); + rule.setVmIp(this.vmIp); + rule.setPublicIp(this.publicIp); + rule.setPublicPort(this.publicPort); + rule.setPrivatePort(this.privatePort); + rule.setProtocol(this.protocol); + rule.setRuleId(this.ruleId); + rule.setAlgorithm(this.algorithm); + rule.setIcmpType(this.icmpType); + rule.setIcmpCode(this.icmpCode); + rule.setSourceCidrList(this.sourceCidrList); + rule.setDestinationCidrList(this.destinationCidrList); + rule.setTrafficType(this.trafficType); + rule.setService(service); + return rule; + } + } +} diff --git a/api/src/main/java/com/cloud/network/Site2SiteVpnConnection.java b/api/src/main/java/com/cloud/network/Site2SiteVpnConnection.java index 994df875f7d..51036abe060 100644 --- a/api/src/main/java/com/cloud/network/Site2SiteVpnConnection.java +++ b/api/src/main/java/com/cloud/network/Site2SiteVpnConnection.java @@ -24,7 +24,7 @@ import org.apache.cloudstack.api.InternalIdentity; public interface Site2SiteVpnConnection extends ControlledEntity, InternalIdentity, Displayable { enum State { - Pending, Connecting, Connected, Disconnected, Error, + Pending, Connecting, Connected, Disconnected, Error, Removed } @Override diff --git a/api/src/main/java/com/cloud/network/element/NetworkElement.java b/api/src/main/java/com/cloud/network/element/NetworkElement.java index fa67575edd3..cb0fc2fca98 100644 --- a/api/src/main/java/com/cloud/network/element/NetworkElement.java +++ b/api/src/main/java/com/cloud/network/element/NetworkElement.java @@ -23,6 +23,7 @@ import com.cloud.deploy.DeployDestination; import com.cloud.exception.ConcurrentOperationException; import com.cloud.exception.InsufficientCapacityException; import com.cloud.exception.ResourceUnavailableException; +import com.cloud.network.IpAddress; import com.cloud.network.Network; import com.cloud.network.Network.Capability; import com.cloud.network.Network.Provider; @@ -87,6 +88,14 @@ public interface NetworkElement extends Adapter { boolean release(Network network, NicProfile nic, VirtualMachineProfile vm, ReservationContext context) throws ConcurrentOperationException, ResourceUnavailableException; + /** + * Release IP from the network provider if reserved + * @param ipAddress + */ + default boolean releaseIp(IpAddress ipAddress) { + return true; + } + /** * The network is being shutdown. * @param network diff --git a/api/src/main/java/com/cloud/network/element/PortForwardingServiceProvider.java b/api/src/main/java/com/cloud/network/element/PortForwardingServiceProvider.java index e99bc2fd416..8dcc8b6d0a4 100644 --- a/api/src/main/java/com/cloud/network/element/PortForwardingServiceProvider.java +++ b/api/src/main/java/com/cloud/network/element/PortForwardingServiceProvider.java @@ -17,12 +17,40 @@ package com.cloud.network.element; import java.util.List; +import java.util.Objects; import com.cloud.exception.ResourceUnavailableException; import com.cloud.network.Network; +import com.cloud.network.rules.FirewallRule; import com.cloud.network.rules.PortForwardingRule; +import com.cloud.network.vpc.NetworkACLItem; public interface PortForwardingServiceProvider extends NetworkElement, IpDeployingRequester { + + static String getPublicPortRange(PortForwardingRule rule) { + return Objects.equals(rule.getSourcePortStart(), rule.getSourcePortEnd()) ? + String.valueOf(rule.getSourcePortStart()) : + String.valueOf(rule.getSourcePortStart()).concat("-").concat(String.valueOf(rule.getSourcePortEnd())); + } + + static String getPrivatePFPortRange(PortForwardingRule rule) { + return rule.getDestinationPortStart() == rule.getDestinationPortEnd() ? + String.valueOf(rule.getDestinationPortStart()) : + String.valueOf(rule.getDestinationPortStart()).concat("-").concat(String.valueOf(rule.getDestinationPortEnd())); + } + + static String getPrivatePortRange(FirewallRule rule) { + return Objects.equals(rule.getSourcePortStart(), rule.getSourcePortEnd()) ? + String.valueOf(rule.getSourcePortStart()) : + String.valueOf(rule.getSourcePortStart()).concat("-").concat(String.valueOf(rule.getSourcePortEnd())); + } + + static String getPrivatePortRangeForACLRule(NetworkACLItem rule) { + return Objects.equals(rule.getSourcePortStart(), rule.getSourcePortEnd()) ? + String.valueOf(rule.getSourcePortStart()) : + String.valueOf(rule.getSourcePortStart()).concat("-").concat(String.valueOf(rule.getSourcePortEnd())); + } + /** * Apply rules * @param network diff --git a/api/src/main/java/com/cloud/network/element/VpcProvider.java b/api/src/main/java/com/cloud/network/element/VpcProvider.java index 6debd1fbc2d..fe8c8f8612f 100644 --- a/api/src/main/java/com/cloud/network/element/VpcProvider.java +++ b/api/src/main/java/com/cloud/network/element/VpcProvider.java @@ -55,4 +55,8 @@ public interface VpcProvider extends NetworkElement { boolean applyACLItemsToPrivateGw(PrivateGateway gateway, List rules) throws ResourceUnavailableException; boolean updateVpcSourceNatIp(Vpc vpc, IpAddress address); + + default boolean updateVpc(Vpc vpc, String previousVpcName) { + return true; + } } diff --git a/api/src/main/java/com/cloud/network/guru/NetworkGuru.java b/api/src/main/java/com/cloud/network/guru/NetworkGuru.java index 7b81c75ed84..ced664e54a9 100644 --- a/api/src/main/java/com/cloud/network/guru/NetworkGuru.java +++ b/api/src/main/java/com/cloud/network/guru/NetworkGuru.java @@ -215,4 +215,8 @@ public interface NetworkGuru extends Adapter { default boolean isSlaacV6Only() { return true; } + + default boolean update(Network network, String prevNetworkName) { + return true; + } } diff --git a/api/src/main/java/com/cloud/network/netris/NetrisLbBackend.java b/api/src/main/java/com/cloud/network/netris/NetrisLbBackend.java new file mode 100644 index 00000000000..afc21f7f511 --- /dev/null +++ b/api/src/main/java/com/cloud/network/netris/NetrisLbBackend.java @@ -0,0 +1,41 @@ +// Licensed to the Apache Software Foundation (ASF) under one +// or more contributor license agreements. See the NOTICE file +// distributed with this work for additional information +// regarding copyright ownership. The ASF licenses this file +// to you under the Apache License, Version 2.0 (the +// "License"); you may not use this file except in compliance +// with the License. You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, +// software distributed under the License is distributed on an +// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +// KIND, either express or implied. See the License for the +// specific language governing permissions and limitations +// under the License. +package com.cloud.network.netris; + +public class NetrisLbBackend { + private long vmId; + private String vmIp; + private int port; + + public NetrisLbBackend(long vmId, String vmIp, int port) { + this.vmId = vmId; + this.vmIp = vmIp; + this.port = port; + } + + public long getVmId() { + return vmId; + } + + public String getVmIp() { + return vmIp; + } + + public int getPort() { + return port; + } +} diff --git a/api/src/main/java/com/cloud/network/netris/NetrisNetworkRule.java b/api/src/main/java/com/cloud/network/netris/NetrisNetworkRule.java new file mode 100644 index 00000000000..211517ead49 --- /dev/null +++ b/api/src/main/java/com/cloud/network/netris/NetrisNetworkRule.java @@ -0,0 +1,108 @@ +// Licensed to the Apache Software Foundation (ASF) under one +// or more contributor license agreements. See the NOTICE file +// distributed with this work for additional information +// regarding copyright ownership. The ASF licenses this file +// to you under the Apache License, Version 2.0 (the +// "License"); you may not use this file except in compliance +// with the License. You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, +// software distributed under the License is distributed on an +// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +// KIND, either express or implied. See the License for the +// specific language governing permissions and limitations +// under the License. +package com.cloud.network.netris; + +import com.cloud.network.SDNProviderNetworkRule; + + +import java.util.List; + +public class NetrisNetworkRule { + public enum NetrisRuleAction { + PERMIT, DENY + } + + private SDNProviderNetworkRule baseRule; + private NetrisRuleAction aclAction; + private List lbBackends; + private String lbRuleName; + private String lbCidrList; + private String reason; + + public NetrisNetworkRule(Builder builder) { + this.baseRule = builder.baseRule; + this.aclAction = builder.aclAction; + this.lbBackends = builder.lbBackends; + this.reason = builder.reason; + this.lbCidrList = builder.lbCidrList; + this.lbRuleName = builder.lbRuleName; + } + + public NetrisRuleAction getAclAction() { + return aclAction; + } + + public List getLbBackends() { + return lbBackends; + } + + public String getReason() { + return reason; + } + + public String getLbCidrList() {return lbCidrList; } + + public String getLbRuleName() { return lbRuleName; } + + public SDNProviderNetworkRule getBaseRule() { + return baseRule; + } + + // Builder class extending the parent builder + public static class Builder { + private SDNProviderNetworkRule baseRule; + private NetrisRuleAction aclAction; + private List lbBackends; + private String reason; + private String lbCidrList; + private String lbRuleName; + + public Builder baseRule(SDNProviderNetworkRule baseRule) { + this.baseRule = baseRule; + return this; + } + + public Builder aclAction(NetrisRuleAction aclAction) { + this.aclAction = aclAction; + return this; + } + + public Builder lbBackends(List lbBackends) { + this.lbBackends = lbBackends; + return this; + } + + public Builder reason(String reason) { + this.reason = reason; + return this; + } + + public Builder lbCidrList(String lbCidrList) { + this.lbCidrList = lbCidrList; + return this; + } + + public Builder lbRuleName(String lbRuleName) { + this.lbRuleName = lbRuleName; + return this; + } + + public NetrisNetworkRule build() { + return new NetrisNetworkRule(this); + } + } +} diff --git a/api/src/main/java/com/cloud/network/netris/NetrisProvider.java b/api/src/main/java/com/cloud/network/netris/NetrisProvider.java new file mode 100644 index 00000000000..fccf2930e97 --- /dev/null +++ b/api/src/main/java/com/cloud/network/netris/NetrisProvider.java @@ -0,0 +1,30 @@ +// Licensed to the Apache Software Foundation (ASF) under one +// or more contributor license agreements. See the NOTICE file +// distributed with this work for additional information +// regarding copyright ownership. The ASF licenses this file +// to you under the Apache License, Version 2.0 (the +// "License"); you may not use this file except in compliance +// with the License. You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, +// software distributed under the License is distributed on an +// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +// KIND, either express or implied. See the License for the +// specific language governing permissions and limitations +// under the License. +package com.cloud.network.netris; + +import org.apache.cloudstack.api.Identity; +import org.apache.cloudstack.api.InternalIdentity; + +public interface NetrisProvider extends InternalIdentity, Identity { + long getZoneId(); + String getName(); + String getUrl(); + String getUsername(); + String getSiteName(); + String getTenantName(); + String getNetrisTag(); +} diff --git a/api/src/main/java/com/cloud/network/netris/NetrisService.java b/api/src/main/java/com/cloud/network/netris/NetrisService.java new file mode 100644 index 00000000000..110e9f07105 --- /dev/null +++ b/api/src/main/java/com/cloud/network/netris/NetrisService.java @@ -0,0 +1,310 @@ +// Licensed to the Apache Software Foundation (ASF) under one +// or more contributor license agreements. See the NOTICE file +// distributed with this work for additional information +// regarding copyright ownership. The ASF licenses this file +// to you under the Apache License, Version 2.0 (the +// "License"); you may not use this file except in compliance +// with the License. You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, +// software distributed under the License is distributed on an +// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +// KIND, either express or implied. See the License for the +// specific language governing permissions and limitations +// under the License. +package com.cloud.network.netris; + +import com.cloud.network.IpAddress; +import com.cloud.network.Network; +import com.cloud.network.SDNProviderNetworkRule; +import com.cloud.network.vpc.StaticRoute; +import com.cloud.network.vpc.Vpc; + +import java.util.List; + +/** + * Interface for Netris Services that provides methods to manage VPCs, networks, + * NAT rules, network rules, and static routes in an SDN (Software Defined Networking) environment. + */ + +public interface NetrisService { + + /** + * Creates IPAM (IP Address Management) allocations for zone-level public ranges. + * + * @param zoneId the ID of the zone + * @return true if the operation is successful, false otherwise + */ + boolean createIPAMAllocationsForZoneLevelPublicRanges(long zoneId); + + /** + * Creates a VPC (Virtual Private Cloud) resource. + * + * @param zoneId the ID of the zone + * @param accountId the ID of the account + * @param domainId the ID of the domain + * @param vpcId the ID of the VPC + * @param vpcName the name of the VPC + * @param sourceNatEnabled true if source NAT is enabled + * @param cidr the CIDR of the VPC + * @param isVpcNetwork true if it is a VPC network + * @return true if the operation is successful, false otherwise + */ + boolean createVpcResource(long zoneId, long accountId, long domainId, Long vpcId, String vpcName, boolean sourceNatEnabled, String cidr, boolean isVpcNetwork); + + /** + * Updates an existing VPC resource. + * + * @param zoneId the ID of the zone + * @param accountId the ID of the account + * @param domainId the ID of the domain + * @param vpcId the ID of the VPC + * @param vpcName the new name of the VPC + * @param previousVpcName the previous name of the VPC + * @return true if the operation is successful, false otherwise + */ + boolean updateVpcResource(long zoneId, long accountId, long domainId, Long vpcId, String vpcName, String previousVpcName); + + /** + * Deletes a VPC resource. + * + * @param zoneId the ID of the zone + * @param accountId the ID of the account + * @param domainId the ID of the domain + * @param vpc the VPC to delete + * @return true if the operation is successful, false otherwise + */ + boolean deleteVpcResource(long zoneId, long accountId, long domainId, Vpc vpc); + + /** + * Creates a virtual network (vNet) resource. + * + * @param zoneId the ID of the zone + * @param accountId the ID of the account + * @param domainId the ID of the domain + * @param vpcName the name of the VPC + * @param vpcId the ID of the VPC + * @param networkName the name of the network + * @param networkId the ID of the network + * @param cidr the CIDR of the network + * @param globalRouting true if global routing is enabled + * @return true if the operation is successful, false otherwise + */ + boolean createVnetResource(Long zoneId, long accountId, long domainId, String vpcName, Long vpcId, String networkName, Long networkId, String cidr, Boolean globalRouting); + + /** + * Updates an existing vNet resource. + * + * @param zoneId the ID of the zone + * @param accountId the ID of the account + * @param domainId the ID of the domain + * @param vpcName the name of the VPC + * @param vpcId the ID of the VPC + * @param networkName the new name of the network + * @param networkId the ID of the network + * @param prevNetworkName the previous name of the network + * @return true if the operation is successful, false otherwise + */ + boolean updateVnetResource(Long zoneId, long accountId, long domainId, String vpcName, Long vpcId, String networkName, Long networkId, String prevNetworkName); + + /** + * Deletes an existing vNet resource. + * + * @param zoneId the ID of the zone + * @param accountId the ID of the account + * @param domainId the ID of the domain + * @param vpcName the name of the VPC + * @param vpcId the ID of the VPC + * @param networkName the name of the network + * @param networkId the ID of the network + * @param cidr the CIDR of the network + * @return true if the operation is successful, false otherwise + */ + boolean deleteVnetResource(long zoneId, long accountId, long domainId, String vpcName, Long vpcId, String networkName, Long networkId, String cidr); + + /** + * Creates a source NAT rule for a VPC or network. + * + * @param zoneId the ID of the zone + * @param accountId the ID of the account + * @param domainId the ID of the domain + * @param vpcName the name of the VPC + * @param vpcId the ID of the VPC + * @param networkName the name of the network + * @param networkId the ID of the network + * @param isForVpc true if the rule applies to a VPC + * @param vpcCidr the VPC CIDR + * @param sourceNatIp the source NAT IP + * @return true if the operation is successful, false otherwise + */ + boolean createSnatRule(long zoneId, long accountId, long domainId, String vpcName, long vpcId, String networkName, long networkId, boolean isForVpc, String vpcCidr, String sourceNatIp); + + /** + * Creates a port forwarding rule for a VPC or network. + * + * @param zoneId the ID of the zone + * @param accountId the ID of the account + * @param domainId the ID of the domain + * @param vpcName the name of the VPC + * @param vpcId the ID of the VPC + * @param networkName the name of the network + * @param networkId the ID of the network + * @param isForVpc true if the rule applies to a VPC + * @param vpcCidr the VPC CIDR + * @param networkRule the network rule to forward + * @return true if the operation is successful, false otherwise + */ + boolean createPortForwardingRule(long zoneId, long accountId, long domainId, String vpcName, long vpcId, String networkName, Long networkId, boolean isForVpc, String vpcCidr, SDNProviderNetworkRule networkRule); + + /** + * Deletes a port forwarding rule for a VPC or network. + * + * @param zoneId the ID of the zone + * @param accountId the ID of the account + * @param domainId the ID of the domain + * @param vpcName the name of the VPC + * @param vpcId the ID of the VPC + * @param networkName the name of the network + * @param networkId the ID of the network + * @param isForVpc true if the rule applies to a VPC + * @param vpcCidr the VPC CIDR + * @param networkRule the network rule to remove + * @return true if the operation is successful, false otherwise + */ + boolean deletePortForwardingRule(long zoneId, long accountId, long domainId, String vpcName, Long vpcId, String networkName, Long networkId, boolean isForVpc, String vpcCidr, SDNProviderNetworkRule networkRule); + + /** + * Updates the source NAT IP for a specified VPC. + * + * @param vpc the VPC to updates + * @param address the new source NAT IP address + * @return true if the operation is successful, false otherwise + */ + boolean updateVpcSourceNatIp(Vpc vpc, IpAddress address); + + /** + * Creates a static NAT rule for a specific VM. + * + * @param zoneId the ID of the zone + * @param accountId the ID of the account + * @param domainId the ID of the domain + * @param networkResourceName the name of the network resource + * @param networkResourceId the ID of the network resource + * @param isForVpc true if the rule applies to a VPC + * @param vpcCidr the VPC CIDR + * @param staticNatIp the static NAT IP + * @param vmIp the VM's IP address + * @param vmId the ID of the VM + * @return true if the operation is successful, false otherwise + */ + boolean createStaticNatRule(long zoneId, long accountId, long domainId, String networkResourceName, Long networkResourceId, boolean isForVpc, String vpcCidr, String staticNatIp, String vmIp, long vmId); + + /** + * Deletes a static NAT rule for a specific VM. + * + * @param zoneId the ID of the zone + * @param accountId the ID of the account + * @param domainId the ID of the domain + * @param networkResourceName the name of the network resource + * @param networkResourceId the ID of the network resource + * @param isForVpc true if the rule applies to a VPC + * @param staticNatIp the static NAT IP + * @param vmId the ID of the VM + * @return true if the operation is successful, false otherwise + */ + boolean deleteStaticNatRule(long zoneId, long accountId, long domainId, String networkResourceName, Long networkResourceId, boolean isForVpc, String staticNatIp, long vmId); + + /** + * Adds firewall rules to a specific network. + * + * @param network the target network + * @param firewallRules the list of firewall rules to add + * @return true if the operation is successful, false otherwise + */ + boolean addFirewallRules(Network network, List firewallRules); + + /** + * Deletes firewall rules from a specific network. + * + * @param network the target network + * @param firewallRules the list of firewall rules to delete + * @return true if the operation is successful, false otherwise + */ + boolean deleteFirewallRules(Network network, List firewallRules); + + /** + * Adds or updates a static route for a specific network or VPC. + * + * @param zoneId the ID of the zone + * @param accountId the ID of the account + * @param domainId the ID of the domain + * @param networkResourceName the name of the network resource + * @param networkResourceId the ID of the network resource + * @param isForVpc true if it is for a VPC + * @param prefix the IP prefix of the route + * @param nextHop the next hop address + * @param routeId the ID of the route + * @param updateRoute true if the route should be updated + * @return true if the operation is successful, false otherwise + */ + boolean addOrUpdateStaticRoute(long zoneId, long accountId, long domainId, String networkResourceName, Long networkResourceId, boolean isForVpc, String prefix, String nextHop, Long routeId, boolean updateRoute); + + /** + * Deletes a specific static route for a network or VPC. + * + * @param zoneId the ID of the zone + * @param accountId the ID of the account + * @param domainId the ID of the domain + * @param networkResourceName the name of the network resource + * @param networkResourceId the ID of the network resource + * @param isForVpc true if it is for a VPC + * @param prefix the IP prefix of the route + * @param nextHop the next hop address + * @param routeId the ID of the route + * @return true if the operation is successful, false otherwise + */ + boolean deleteStaticRoute(long zoneId, long accountId, long domainId, String networkResourceName, Long networkResourceId, boolean isForVpc, String prefix, String nextHop, Long routeId); + + /** + * Lists static routes for a specific network or VPC. + * + * @param zoneId the ID of the zone + * @param accountId the ID of the account + * @param domainId the ID of the domain + * @param networkResourceName the name of the network resource + * @param networkResourceId the ID of the network resource + * @param isForVpc true if it is for a VPC + * @param prefix the IP prefix of the route + * @param nextHop the next hop address + * @param routeId the ID of the route + * @return a list of static routes + */ + List listStaticRoutes(long zoneId, long accountId, long domainId, String networkResourceName, Long networkResourceId, boolean isForVpc, String prefix, String nextHop, Long routeId); + + /** + * Releases a NAT IP address. + * + * @param zoneId the ID of the zone + * @param publicIp the public NAT IP to release + * @return true if the operation is successful, false otherwise + */ + boolean releaseNatIp(long zoneId, String publicIp); + + /** + * Creates or updates a load balancer (LB) rule. + * + * @param rule the network rule for the load balancer + * @return true if the operation is successful, false otherwise + */ + boolean createOrUpdateLbRule(NetrisNetworkRule rule); + + /** + * Deletes a load balancer (LB) rule. + * + * @param rule the network rule to delete + * @return true if the operation is successful, false otherwise + */ + boolean deleteLbRule(NetrisNetworkRule rule); +} diff --git a/api/src/main/java/com/cloud/network/nsx/NsxService.java b/api/src/main/java/com/cloud/network/nsx/NsxService.java index bc4e6aafbfe..1adb7461cc0 100644 --- a/api/src/main/java/com/cloud/network/nsx/NsxService.java +++ b/api/src/main/java/com/cloud/network/nsx/NsxService.java @@ -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); } diff --git a/api/src/main/java/com/cloud/network/vpc/StaticRoute.java b/api/src/main/java/com/cloud/network/vpc/StaticRoute.java index 5707ca14024..739fca328b8 100644 --- a/api/src/main/java/com/cloud/network/vpc/StaticRoute.java +++ b/api/src/main/java/com/cloud/network/vpc/StaticRoute.java @@ -25,6 +25,7 @@ public interface StaticRoute extends ControlledEntity, Identity, InternalIdentit Staged, // route been created but has never got through network rule conflict detection. Routes in this state can not be sent to VPC virtual router. Add, // Add means the route has been created and has gone through network rule conflict detection. Active, // Route has been sent to the VPC router and reported to be active. + Update, Revoke, // Revoke means this route has been revoked. If this route has been sent to the VPC router, the route will be deleted from database. Deleting // rule has been revoked and is scheduled for deletion } @@ -32,7 +33,9 @@ public interface StaticRoute extends ControlledEntity, Identity, InternalIdentit /** * @return */ - long getVpcGatewayId(); + Long getVpcGatewayId(); + + String getNextHop(); /** * @return diff --git a/api/src/main/java/com/cloud/network/vpc/StaticRouteProfile.java b/api/src/main/java/com/cloud/network/vpc/StaticRouteProfile.java index cb4849f1f7b..c8fc073911f 100644 --- a/api/src/main/java/com/cloud/network/vpc/StaticRouteProfile.java +++ b/api/src/main/java/com/cloud/network/vpc/StaticRouteProfile.java @@ -23,7 +23,8 @@ public class StaticRouteProfile implements StaticRoute { private String targetCidr; private long accountId; private long domainId; - private long gatewayId; + private Long gatewayId; + private String nextHop; private StaticRoute.State state; private long vpcId; String vlanTag; @@ -46,6 +47,18 @@ public class StaticRouteProfile implements StaticRoute { ipAddress = gateway.getIp4Address(); } + public StaticRouteProfile(StaticRoute staticRoute) { + id = staticRoute.getId(); + uuid = staticRoute.getUuid(); + targetCidr = staticRoute.getCidr(); + accountId = staticRoute.getAccountId(); + domainId = staticRoute.getDomainId(); + gatewayId = staticRoute.getVpcGatewayId(); + state = staticRoute.getState(); + vpcId = staticRoute.getVpcId(); + gateway = staticRoute.getNextHop(); + } + @Override public long getAccountId() { return accountId; @@ -57,10 +70,15 @@ public class StaticRouteProfile implements StaticRoute { } @Override - public long getVpcGatewayId() { + public Long getVpcGatewayId() { return gatewayId; } + @Override + public String getNextHop() { + return nextHop; + } + @Override public String getCidr() { return targetCidr; diff --git a/api/src/main/java/com/cloud/network/vpc/VpcOffering.java b/api/src/main/java/com/cloud/network/vpc/VpcOffering.java index 38263f59667..17f49bb3652 100644 --- a/api/src/main/java/com/cloud/network/vpc/VpcOffering.java +++ b/api/src/main/java/com/cloud/network/vpc/VpcOffering.java @@ -32,6 +32,8 @@ public interface VpcOffering extends InternalIdentity, Identity { public static final String redundantVPCOfferingName = "Redundant VPC offering"; public static final String DEFAULT_VPC_NAT_NSX_OFFERING_NAME = "VPC offering with NSX - NAT Mode"; public static final String DEFAULT_VPC_ROUTE_NSX_OFFERING_NAME = "VPC offering with NSX - Route Mode"; + public static final String DEFAULT_VPC_ROUTE_NETRIS_OFFERING_NAME = "VPC offering with Netris - Route Mode"; + public static final String DEFAULT_VPC_NAT_NETRIS_OFFERING_NAME = "VPC offering with Netris - NAT Mode"; /** * @@ -56,8 +58,6 @@ public interface VpcOffering extends InternalIdentity, Identity { */ boolean isDefault(); - boolean isForNsx(); - NetworkOffering.NetworkMode getNetworkMode(); /** diff --git a/api/src/main/java/com/cloud/network/vpc/VpcProvisioningService.java b/api/src/main/java/com/cloud/network/vpc/VpcProvisioningService.java index 10f1ddcc12d..97b95339ecf 100644 --- a/api/src/main/java/com/cloud/network/vpc/VpcProvisioningService.java +++ b/api/src/main/java/com/cloud/network/vpc/VpcProvisioningService.java @@ -37,7 +37,7 @@ public interface VpcProvisioningService { VpcOffering createVpcOffering(String name, String displayText, List supportedServices, Map> serviceProviders, Map serviceCapabilitystList, NetUtils.InternetProtocol internetProtocol, - Long serviceOfferingId, Boolean forNsx, NetworkOffering.NetworkMode networkMode, + Long serviceOfferingId, String externalProvider, NetworkOffering.NetworkMode networkMode, List domainIds, List zoneIds, VpcOffering.State state, NetworkOffering.RoutingMode routingMode, boolean specifyAsNumber); diff --git a/api/src/main/java/com/cloud/network/vpc/VpcService.java b/api/src/main/java/com/cloud/network/vpc/VpcService.java index 9b7a83c29c1..c1546609d2b 100644 --- a/api/src/main/java/com/cloud/network/vpc/VpcService.java +++ b/api/src/main/java/com/cloud/network/vpc/VpcService.java @@ -238,7 +238,7 @@ public interface VpcService { * @param cidr * @return */ - StaticRoute createStaticRoute(long gatewayId, String cidr) throws NetworkRuleConflictException; + StaticRoute createStaticRoute(Long gatewayId, Long vpcId, String nextHop, String cidr) throws NetworkRuleConflictException; /** * Lists static routes based on parameters passed to the call diff --git a/api/src/main/java/com/cloud/offering/NetworkOffering.java b/api/src/main/java/com/cloud/offering/NetworkOffering.java index 7011aea679e..5000a4f8c62 100644 --- a/api/src/main/java/com/cloud/offering/NetworkOffering.java +++ b/api/src/main/java/com/cloud/offering/NetworkOffering.java @@ -64,6 +64,8 @@ public interface NetworkOffering extends InfrastructureEntity, InternalIdentity, public static final String DEFAULT_NAT_NSX_OFFERING_FOR_VPC = "DefaultNATNSXNetworkOfferingForVpc"; public static final String DEFAULT_NAT_NSX_OFFERING_FOR_VPC_WITH_ILB = "DefaultNATNSXNetworkOfferingForVpcWithInternalLB"; public static final String DEFAULT_ROUTED_NSX_OFFERING_FOR_VPC = "DefaultRoutedNSXNetworkOfferingForVpc"; + public static final String DEFAULT_ROUTED_NETRIS_OFFERING_FOR_VPC = "DefaultRoutedNetrisNetworkOfferingForVpc"; + public static final String DEFAULT_NAT_NETRIS_OFFERING_FOR_VPC = "DefaultNATNetrisNetworkOfferingForVpc"; public static final String DEFAULT_NAT_NSX_OFFERING = "DefaultNATNSXNetworkOffering"; public static final String DEFAULT_ROUTED_NSX_OFFERING = "DefaultRoutedNSXNetworkOffering"; public final static String QuickCloudNoServices = "QuickCloudNoServices"; @@ -102,10 +104,6 @@ public interface NetworkOffering extends InfrastructureEntity, InternalIdentity, boolean isForVpc(); - boolean isForTungsten(); - - boolean isForNsx(); - NetworkMode getNetworkMode(); TrafficType getTrafficType(); diff --git a/api/src/main/java/com/cloud/offering/ServiceOffering.java b/api/src/main/java/com/cloud/offering/ServiceOffering.java index acb7a9f1cf9..532123e4373 100644 --- a/api/src/main/java/com/cloud/offering/ServiceOffering.java +++ b/api/src/main/java/com/cloud/offering/ServiceOffering.java @@ -142,4 +142,8 @@ public interface ServiceOffering extends InfrastructureEntity, InternalIdentity, Boolean getDiskOfferingStrictness(); void setDiskOfferingStrictness(boolean diskOfferingStrictness); + + Long getVgpuProfileId(); + + Integer getGpuCount(); } diff --git a/api/src/main/java/com/cloud/storage/Storage.java b/api/src/main/java/com/cloud/storage/Storage.java index 05b8b3ab7a8..1ad3731b9ea 100644 --- a/api/src/main/java/com/cloud/storage/Storage.java +++ b/api/src/main/java/com/cloud/storage/Storage.java @@ -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"), diff --git a/api/src/main/java/com/cloud/storage/VolumeApiService.java b/api/src/main/java/com/cloud/storage/VolumeApiService.java index b7b5423244c..dd7341da1b5 100644 --- a/api/src/main/java/com/cloud/storage/VolumeApiService.java +++ b/api/src/main/java/com/cloud/storage/VolumeApiService.java @@ -137,7 +137,7 @@ public interface VolumeApiService { void updateDisplay(Volume volume, Boolean displayVolume); - Snapshot allocSnapshotForVm(Long vmId, Long volumeId, String snapshotName) throws ResourceAllocationException; + Snapshot allocSnapshotForVm(Long vmId, Long volumeId, String snapshotName, Long vmSnapshotId) throws ResourceAllocationException; /** * Checks if the storage pool supports the disk offering tags. @@ -171,6 +171,13 @@ public interface VolumeApiService { * */ boolean doesStoragePoolSupportDiskOffering(StoragePool destPool, DiskOffering diskOffering); + + /** + * Checks if the storage pool supports the required disk offering tags + * destPool the storage pool to check the disk offering tags + * diskOfferingTags the tags that should be supported + * return whether the tags are supported in the storage pool + */ boolean doesStoragePoolSupportDiskOfferingTags(StoragePool destPool, String diskOfferingTags); Volume destroyVolume(long volumeId, Account caller, boolean expunge, boolean forceExpunge); diff --git a/api/src/main/java/com/cloud/template/VirtualMachineTemplate.java b/api/src/main/java/com/cloud/template/VirtualMachineTemplate.java index 89953d225a0..b8c646048b9 100644 --- a/api/src/main/java/com/cloud/template/VirtualMachineTemplate.java +++ b/api/src/main/java/com/cloud/template/VirtualMachineTemplate.java @@ -153,4 +153,6 @@ public interface VirtualMachineTemplate extends ControlledEntity, Identity, Inte CPU.CPUArch getArch(); + Long getExtensionId(); + } diff --git a/api/src/main/java/com/cloud/user/AccountService.java b/api/src/main/java/com/cloud/user/AccountService.java index e2c3bed0c29..c0ebcf09f59 100644 --- a/api/src/main/java/com/cloud/user/AccountService.java +++ b/api/src/main/java/com/cloud/user/AccountService.java @@ -87,6 +87,8 @@ public interface AccountService { boolean isDomainAdmin(Long accountId); + boolean isResourceDomainAdmin(Long accountId); + boolean isNormalUser(long accountId); User getActiveUserByRegistrationToken(String registrationToken); diff --git a/api/src/main/java/com/cloud/user/ResourceLimitService.java b/api/src/main/java/com/cloud/user/ResourceLimitService.java index 2f4ad1347be..49b20fe2fef 100644 --- a/api/src/main/java/com/cloud/user/ResourceLimitService.java +++ b/api/src/main/java/com/cloud/user/ResourceLimitService.java @@ -50,8 +50,14 @@ public interface ResourceLimitService { "The default maximum number of projects that can be created for an account",false); static final ConfigKey DefaultMaxDomainProjects = new ConfigKey<>("Domain Defaults",Long.class,"max.domain.projects","50", "The default maximum number of projects that can be created for a domain",false); + static final ConfigKey DefaultMaxAccountGpus = new ConfigKey<>("Account Defaults",Long.class,"max.account.gpus","20", + "The default maximum number of GPU devices that can be used for an account", false); + static final ConfigKey DefaultMaxDomainGpus = new ConfigKey<>("Domain Defaults",Long.class,"max.domain.gpus","20", + "The default maximum number of GPU devices that can be used for a domain", false); + static final ConfigKey DefaultMaxProjectGpus = new ConfigKey<>("Project Defaults",Long.class,"max.project.gpus","20", + "The default maximum number of GPU devices that can be used for a project", false); - static final List HostTagsSupportingTypes = List.of(ResourceType.user_vm, ResourceType.cpu, ResourceType.memory); + static final List HostTagsSupportingTypes = List.of(ResourceType.user_vm, ResourceType.cpu, ResourceType.memory, ResourceType.gpu); static final List StorageTagsSupportingTypes = List.of(ResourceType.volume, ResourceType.primary_storage); /** @@ -284,4 +290,8 @@ public interface ResourceLimitService { void incrementVmMemoryResourceCount(long accountId, Boolean display, ServiceOffering serviceOffering, VirtualMachineTemplate template, Long memory); void decrementVmMemoryResourceCount(long accountId, Boolean display, ServiceOffering serviceOffering, VirtualMachineTemplate template, Long memory); + void checkVmGpuResourceLimit(Account owner, Boolean display, ServiceOffering serviceOffering, VirtualMachineTemplate template, Long gpu) throws ResourceAllocationException; + void incrementVmGpuResourceCount(long accountId, Boolean display, ServiceOffering serviceOffering, VirtualMachineTemplate template, Long gpu); + void decrementVmGpuResourceCount(long accountId, Boolean display, ServiceOffering serviceOffering, VirtualMachineTemplate template, Long gpu); + } diff --git a/api/src/main/java/com/cloud/vm/UserVmService.java b/api/src/main/java/com/cloud/vm/UserVmService.java index 5c1c2f9a2e5..a8ed62fb6b9 100644 --- a/api/src/main/java/com/cloud/vm/UserVmService.java +++ b/api/src/main/java/com/cloud/vm/UserVmService.java @@ -16,6 +16,8 @@ // under the License. package com.cloud.vm; +import com.cloud.storage.Snapshot; +import com.cloud.storage.Volume; import java.util.LinkedHashMap; import java.util.List; import java.util.Map; @@ -222,7 +224,7 @@ public interface UserVmService { String userData, Long userDataId, String userDataDetails, List sshKeyPairs, Map requestedIps, IpAddresses defaultIp, Boolean displayVm, String keyboard, List affinityGroupIdList, Map customParameter, String customId, Map> dhcpOptionMap, Map dataDiskTemplateToDiskOfferingMap, - Map userVmOVFProperties, boolean dynamicScalingEnabled, Long overrideDiskOfferingId) throws InsufficientCapacityException, + Map userVmOVFProperties, boolean dynamicScalingEnabled, Long overrideDiskOfferingId, Volume volume, Snapshot snapshot) throws InsufficientCapacityException, ConcurrentOperationException, ResourceUnavailableException, StorageUnavailableException, ResourceAllocationException; /** @@ -298,7 +300,7 @@ public interface UserVmService { List securityGroupIdList, Account owner, String hostName, String displayName, Long diskOfferingId, Long diskSize, String group, HypervisorType hypervisor, HTTPMethod httpmethod, String userData, Long userDataId, String userDataDetails, List sshKeyPairs, Map requestedIps, IpAddresses defaultIps, Boolean displayVm, String keyboard, List affinityGroupIdList, Map customParameters, String customId, Map> dhcpOptionMap, - Map dataDiskTemplateToDiskOfferingMap, Map userVmOVFProperties, boolean dynamicScalingEnabled, Long overrideDiskOfferingId, String vmType) throws InsufficientCapacityException, ConcurrentOperationException, ResourceUnavailableException, StorageUnavailableException, ResourceAllocationException; + Map dataDiskTemplateToDiskOfferingMap, Map userVmOVFProperties, boolean dynamicScalingEnabled, Long overrideDiskOfferingId, String vmType, Volume volume, Snapshot snapshot) throws InsufficientCapacityException, ConcurrentOperationException, ResourceUnavailableException, StorageUnavailableException, ResourceAllocationException; /** * Creates a User VM in Advanced Zone (Security Group feature is disabled) @@ -370,7 +372,7 @@ public interface UserVmService { String hostName, String displayName, Long diskOfferingId, Long diskSize, String group, HypervisorType hypervisor, HTTPMethod httpmethod, String userData, Long userDataId, String userDataDetails, List sshKeyPairs, Map requestedIps, IpAddresses defaultIps, Boolean displayVm, String keyboard, List affinityGroupIdList, Map customParameters, String customId, Map> dhcpOptionMap, Map dataDiskTemplateToDiskOfferingMap, - Map templateOvfPropertiesMap, boolean dynamicScalingEnabled, String vmType, Long overrideDiskOfferingId) + Map templateOvfPropertiesMap, boolean dynamicScalingEnabled, String vmType, Long overrideDiskOfferingId, Volume volume, Snapshot snapshot) throws InsufficientCapacityException, ConcurrentOperationException, ResourceUnavailableException, StorageUnavailableException, ResourceAllocationException; diff --git a/api/src/main/java/com/cloud/vm/VmDetailConstants.java b/api/src/main/java/com/cloud/vm/VmDetailConstants.java index 14f15c9fd0f..ea5d209a5d4 100644 --- a/api/src/main/java/com/cloud/vm/VmDetailConstants.java +++ b/api/src/main/java/com/cloud/vm/VmDetailConstants.java @@ -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"; } diff --git a/api/src/main/java/com/cloud/vm/snapshot/VMSnapshot.java b/api/src/main/java/com/cloud/vm/snapshot/VMSnapshot.java index 3897df2d5e6..24e93af1562 100644 --- a/api/src/main/java/com/cloud/vm/snapshot/VMSnapshot.java +++ b/api/src/main/java/com/cloud/vm/snapshot/VMSnapshot.java @@ -31,7 +31,8 @@ public interface VMSnapshot extends ControlledEntity, Identity, InternalIdentity enum State { Allocated("The VM snapshot is allocated but has not been created yet."), Creating("The VM snapshot is being created."), Ready( "The VM snapshot is ready to be used."), Reverting("The VM snapshot is being used to revert"), Expunging("The volume is being expunging"), Removed( - "The volume is destroyed, and can't be recovered."), Error("The volume is in error state, and can't be recovered"); + "The volume is destroyed, and can't be recovered."), Error("The volume is in error state, and can't be recovered"), + Hidden("The VM snapshot is hidden from the user and cannot be recovered."); String _description; @@ -60,6 +61,8 @@ public interface VMSnapshot extends ControlledEntity, Identity, InternalIdentity s_fsm.addTransition(Expunging, Event.ExpungeRequested, Expunging); s_fsm.addTransition(Expunging, Event.OperationSucceeded, Removed); s_fsm.addTransition(Expunging, Event.OperationFailed, Error); + s_fsm.addTransition(Expunging, Event.Hide, Hidden); + s_fsm.addTransition(Hidden, Event.ExpungeRequested, Expunging); } } @@ -68,7 +71,7 @@ public interface VMSnapshot extends ControlledEntity, Identity, InternalIdentity } enum Event { - CreateRequested, OperationFailed, OperationSucceeded, RevertRequested, ExpungeRequested, + CreateRequested, OperationFailed, OperationSucceeded, RevertRequested, ExpungeRequested, Hide, } @Override diff --git a/api/src/main/java/org/apache/cloudstack/acl/RoleType.java b/api/src/main/java/org/apache/cloudstack/acl/RoleType.java index 005d47c85bc..46e4f1bc510 100644 --- a/api/src/main/java/org/apache/cloudstack/acl/RoleType.java +++ b/api/src/main/java/org/apache/cloudstack/acl/RoleType.java @@ -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 roles) { + int combinedMask = 0; + if (roles != null) { + for (RoleType role : roles) { + combinedMask |= role.getMask(); + } + } + return combinedMask; + } + + public static Set fromCombinedMask(int combinedMask) { + Set 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. * */ diff --git a/api/src/main/java/org/apache/cloudstack/alert/AlertService.java b/api/src/main/java/org/apache/cloudstack/alert/AlertService.java index 1250284b5c2..5146e5c38e8 100644 --- a/api/src/main/java/org/apache/cloudstack/alert/AlertService.java +++ b/api/src/main/java/org/apache/cloudstack/alert/AlertService.java @@ -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; diff --git a/api/src/main/java/org/apache/cloudstack/api/ApiCommandResourceType.java b/api/src/main/java/org/apache/cloudstack/api/ApiCommandResourceType.java index 4cd89c877b2..4d33ba859a5 100644 --- a/api/src/main/java/org/apache/cloudstack/api/ApiCommandResourceType.java +++ b/api/src/main/java/org/apache/cloudstack/api/ApiCommandResourceType.java @@ -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; diff --git a/api/src/main/java/org/apache/cloudstack/api/ApiConstants.java b/api/src/main/java/org/apache/cloudstack/api/ApiConstants.java index 22e7e807502..4fef598d311 100644 --- a/api/src/main/java/org/apache/cloudstack/api/ApiConstants.java +++ b/api/src/main/java/org/apache/cloudstack/api/ApiConstants.java @@ -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"; @@ -68,6 +69,7 @@ public class ApiConstants { public static final String BOOTABLE = "bootable"; public static final String BIND_DN = "binddn"; public static final String BIND_PASSWORD = "bindpass"; + public static final String BUS_ADDRESS = "busaddress"; public static final String BYTES_READ_RATE = "bytesreadrate"; public static final String BYTES_READ_RATE_MAX = "bytesreadratemax"; public static final String BYTES_READ_RATE_MAX_LENGTH = "bytesreadratemaxlength"; @@ -90,9 +92,11 @@ public class ApiConstants { public static final String CONVERT_INSTANCE_HOST_ID = "convertinstancehostid"; public static final String CONVERT_INSTANCE_STORAGE_POOL_ID = "convertinstancepoolid"; public static final String ENABLED_REVOCATION_CHECK = "enabledrevocationcheck"; + public static final String COMBINED_CAPACITY_ORDERING = "COMBINED"; public static final String CONTROLLER = "controller"; public static final String CONTROLLER_UNIT = "controllerunit"; public static final String COPY_IMAGE_TAGS = "copyimagetags"; + public static final String CPU_OVERCOMMIT_RATIO = "cpuOvercommitRatio"; public static final String CSR = "csr"; public static final String PRIVATE_KEY = "privatekey"; public static final String DATASTORE_HOST = "datastorehost"; @@ -124,6 +128,7 @@ public class ApiConstants { public static final String CNI_CONFIG_DETAILS = "cniconfigdetails"; public static final String CNI_CONFIG_NAME = "cniconfigname"; public static final String COMPONENT = "component"; + public static final String CPU = "CPU"; public static final String CPU_CORE_PER_SOCKET = "cpucorepersocket"; public static final String CPU_NUMBER = "cpunumber"; public static final String CPU_SPEED = "cpuspeed"; @@ -137,6 +142,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"; @@ -157,10 +163,12 @@ public class ApiConstants { public static final String DESTINATION_ZONE_ID = "destzoneid"; public static final String DETAILS = "details"; public static final String DEVICE_ID = "deviceid"; + public static final String DEVICE_NAME = "devicename"; public static final String DIRECT_DOWNLOAD = "directdownload"; 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"; @@ -202,6 +210,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"; @@ -212,6 +221,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"; @@ -220,9 +235,11 @@ public class ApiConstants { public static final String FORCED_DESTROY_LOCAL_STORAGE = "forcedestroylocalstorage"; public static final String FORCE_DELETE_HOST = "forcedeletehost"; public static final String FORCE_MS_TO_IMPORT_VM_FILES = "forcemstoimportvmfiles"; + public static final String FORCE_UPDATE_OS_TYPE = "forceupdateostype"; public static final String FORMAT = "format"; public static final String FOR_VIRTUAL_NETWORK = "forvirtualnetwork"; public static final String FOR_SYSTEM_VMS = "forsystemvms"; + public static final String FOR_PROVIDER = "forprovider"; public static final String FULL_PATH = "fullpath"; public static final String GATEWAY = "gateway"; public static final String IP6_GATEWAY = "ip6gateway"; @@ -267,6 +284,7 @@ public class ApiConstants { public static final String PREVIOUS_OWNER_ID = "previousownerid"; public static final String PREVIOUS_OWNER_NAME = "previousownername"; public static final String NEXT_ACL_RULE_ID = "nextaclruleid"; + public static final String NEXT_HOP = "nexthop"; public static final String MOVE_ACL_CONSISTENCY_HASH = "aclconsistencyhash"; public static final String IMAGE_PATH = "imagepath"; public static final String INSTANCE_CONVERSION_SUPPORTED = "instanceconversionsupported"; @@ -344,9 +362,13 @@ public class ApiConstants { public static final String MAX_BACKUPS = "maxbackups"; 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"; + public static final String MIGRATION_JOB_ID = "migrationjobid"; + public static final String MIGRATION_JOB_STATUS = "migrationjobstatus"; public static final String MIGRATIONS = "migrations"; public static final String MEMORY = "memory"; public static final String MODE = "mode"; @@ -368,6 +390,7 @@ public class ApiConstants { public static final String NEW_START_IP = "newstartip"; public static final String NEW_END_IP = "newendip"; public static final String KUBERNETES_NODE_VERSION = "kubernetesnodeversion"; + public static final String NUMA_NODE = "numanode"; public static final String NUM_RETRIES = "numretries"; public static final String OFFER_HA = "offerha"; public static final String OS_DISTRIBUTION = "osdistribution"; @@ -384,6 +407,13 @@ public class ApiConstants { public static final String OS_TYPE_ID = "ostypeid"; public static final String OS_DISPLAY_NAME = "osdisplayname"; public static final String OS_NAME_FOR_HYPERVISOR = "osnameforhypervisor"; + public static final String GPU_CARD_ID = "gpucardid"; + public static final String GPU_CARD_NAME = "gpucardname"; + public static final String GPU_COUNT = "gpucount"; + public static final String GPU_DISPLAY = "gpudisplay"; + public static final String GPU_DEVICE_TYPE = "gpudevicetype"; + public static final String GPU_ENABLED = "gpuenabled"; + public static final String MAX_VGPU_PER_PHYSICAL_GPU = "maxvgpuperphysicalgpu"; public static final String GUEST_OS_LIST = "guestoslist"; public static final String GUEST_OS_COUNT = "guestoscount"; public static final String OS_MAPPING_CHECK_ENABLED = "osmappingcheckenabled"; @@ -395,14 +425,17 @@ public class ApiConstants { public static final String PARENT = "parent"; public static final String PARENT_ID = "parentid"; public static final String PARENT_DOMAIN_ID = "parentdomainid"; + public static final String PARENT_GPU_DEVICE_ID = "parentgpudeviceid"; public static final String PARENT_SUBNET = "parentsubnet"; public static final String PARENT_TEMPLATE_ID = "parenttemplateid"; public static final String PASSWORD = "password"; + public static final String PCI_ROOT = "pciroot"; public static final String CURRENT_PASSWORD = "currentpassword"; public static final String SHOULD_UPDATE_PASSWORD = "update_passwd_on_host"; 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"; @@ -425,6 +458,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"; @@ -441,12 +475,14 @@ public class ApiConstants { public static final String PUBLIC_END_PORT = "publicendport"; public static final String PUBLIC_ZONE = "publiczone"; public static final String PURGE_RESOURCES = "purgeresources"; + public static final String RAM = "RAM"; public static final String REBALANCE = "rebalance"; public static final String RECEIVED_BYTES = "receivedbytes"; public static final String RECONNECT = "reconnect"; 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"; @@ -482,6 +518,7 @@ public class ApiConstants { public static final String SIGNATURE = "signature"; public static final String SIGNATURE_VERSION = "signatureversion"; public static final String SINCE = "since"; + public static final String SITE_NAME = "sitename"; public static final String SIZE = "size"; public static final String SIZEGB = "sizegb"; public static final String SNAPSHOT = "snapshot"; @@ -518,6 +555,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"; @@ -530,6 +568,7 @@ public class ApiConstants { public static final String TIMEOUT = "timeout"; public static final String TIMEZONE = "timezone"; public static final String TIMEZONEOFFSET = "timezoneoffset"; + public static final String TENANT_NAME = "tenantname"; public static final String TOTAL = "total"; public static final String TOTAL_SUBNETS = "totalsubnets"; public static final String TYPE = "type"; @@ -558,7 +597,14 @@ 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 VENDOR_ID = "vendorid"; + public static final String VENDOR_NAME = "vendorname"; + public static final String VGPU_PROFILE_ID = "vgpuprofileid"; + public static final String VGPU_PROFILE_NAME = "vgpuprofilename"; + 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"; @@ -657,7 +703,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"; @@ -906,6 +952,8 @@ public class ApiConstants { public static final String NETWORK = "network"; public static final String VPC_ID = "vpcid"; public static final String VPC_NAME = "vpcname"; + public static final String VPC_GATEWAY_ID = "vpcgatewayid"; + public static final String VPC_GATEWAY_IP = "vpcgatewayip"; public static final String GATEWAY_ID = "gatewayid"; public static final String CAN_USE_FOR_DEPLOY = "canusefordeploy"; public static final String RESOURCE_IDS = "resourceids"; @@ -1049,6 +1097,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"; @@ -1084,6 +1133,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"; @@ -1201,6 +1251,9 @@ public class ApiConstants { public static final String SOURCE_NAT_IP_ID = "sourcenatipaddressid"; public static final String HAS_RULES = "hasrules"; public static final String NSX_DETAIL_KEY = "forNsx"; + public static final String NETRIS_DETAIL_KEY = "forNetris"; + public static final String NETRIS_TAG = "netristag"; + public static final String NETRIS_VXLAN_ID = "netrisvxlanid"; public static final String DISK_PATH = "diskpath"; public static final String IMPORT_SOURCE = "importsource"; public static final String TEMP_PATH = "temppath"; @@ -1256,6 +1309,22 @@ public class ApiConstants { public static final String VMWARE_DC = "vmwaredc"; + public static final String CSS = "css"; + + public static final String JSON_CONFIGURATION = "jsonconfiguration"; + + public static final String COMMON_NAMES = "commonnames"; + + public static final String COMMON_NAME = "commonname"; + + public static final String DOMAIN_IDS = "domainids"; + + public static final String SHOW_PUBLIC = "showpublic"; + + public static final String LIST_ONLY_DEFAULT_THEME = "listonlydefaulttheme"; + + public static final String RECURSIVE_DOMAINS = "recursivedomains"; + /** * This enum specifies IO Drivers, each option controls specific policies on I/O. * Qemu guests support "threads" and "native" options Since 0.8.8 ; "io_uring" is supported Since 6.3.0 (QEMU 5.0). @@ -1308,6 +1377,10 @@ public class ApiConstants { all, resource, min; } + public enum ExtensionDetails { + all, resource, external, min; + } + public enum ApiKeyAccess { DISABLED(false), ENABLED(true), diff --git a/api/src/main/java/org/apache/cloudstack/api/BaseCmd.java b/api/src/main/java/org/apache/cloudstack/api/BaseCmd.java index 457afdc8847..8f47d51b19d 100644 --- a/api/src/main/java/org/apache/cloudstack/api/BaseCmd.java +++ b/api/src/main/java/org/apache/cloudstack/api/BaseCmd.java @@ -39,6 +39,7 @@ import org.apache.cloudstack.affinity.AffinityGroupService; import org.apache.cloudstack.alert.AlertService; import org.apache.cloudstack.annotation.AnnotationService; import org.apache.cloudstack.context.CallContext; +import org.apache.cloudstack.gpu.GpuService; import org.apache.cloudstack.network.RoutedIpv4Manager; import org.apache.cloudstack.network.lb.ApplicationLoadBalancerService; import org.apache.cloudstack.network.lb.InternalLoadBalancerVMService; @@ -94,6 +95,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 { @@ -130,6 +132,8 @@ public abstract class BaseCmd { @Inject public UserVmService _userVmService; @Inject + public GpuService gpuService; + @Inject public ManagementService _mgr; @Inject public StorageService _storageService; @@ -484,4 +488,14 @@ public abstract class BaseCmd { } return detailsMap; } + + public Map convertExternalDetailsToMap(Map externalDetails) { + Map customparameterMap = convertDetailsToMap(externalDetails); + Map details = new HashMap<>(); + for (String key : customparameterMap.keySet()) { + String value = customparameterMap.get(key); + details.put(VmDetailConstants.EXTERNAL_DETAIL_PREFIX + key, value); + } + return details; + } } diff --git a/api/src/main/java/org/apache/cloudstack/api/BaseUpdateTemplateOrIsoCmd.java b/api/src/main/java/org/apache/cloudstack/api/BaseUpdateTemplateOrIsoCmd.java index 9a8282df112..8489bf05ec7 100644 --- a/api/src/main/java/org/apache/cloudstack/api/BaseUpdateTemplateOrIsoCmd.java +++ b/api/src/main/java/org/apache/cloudstack/api/BaseUpdateTemplateOrIsoCmd.java @@ -51,6 +51,10 @@ public abstract class BaseUpdateTemplateOrIsoCmd extends BaseCmd { description = "the ID of the OS type that best represents the OS of this image.") private Long osTypeId; + @Parameter(name = ApiConstants.FORCE_UPDATE_OS_TYPE, type = CommandType.BOOLEAN, since = "4.21", description = "Force OS type update. Warning: Updating OS type will " + + "update the guest OS configuration for all the existing Instances deployed with this template/iso, which may affect their behavior.") + private Boolean forceUpdateOsType; + @Parameter(name = ApiConstants.FORMAT, type = CommandType.STRING, description = "the format for the image") private String format; @@ -112,6 +116,10 @@ public abstract class BaseUpdateTemplateOrIsoCmd extends BaseCmd { return osTypeId; } + public Boolean getForceUpdateOsType() { + return forceUpdateOsType; + } + public Boolean getPasswordEnabled() { return passwordEnabled; } diff --git a/api/src/main/java/org/apache/cloudstack/api/ResponseGenerator.java b/api/src/main/java/org/apache/cloudstack/api/ResponseGenerator.java index 3288eb58d75..d0683299e73 100644 --- a/api/src/main/java/org/apache/cloudstack/api/ResponseGenerator.java +++ b/api/src/main/java/org/apache/cloudstack/api/ResponseGenerator.java @@ -64,6 +64,7 @@ import org.apache.cloudstack.api.response.GuestOSResponse; import org.apache.cloudstack.api.response.GuestOsMappingResponse; import org.apache.cloudstack.api.response.GuestVlanRangeResponse; import org.apache.cloudstack.api.response.GuestVlanResponse; +import org.apache.cloudstack.api.response.GuiThemeResponse; import org.apache.cloudstack.api.response.HostForMigrationResponse; import org.apache.cloudstack.api.response.HostResponse; import org.apache.cloudstack.api.response.HypervisorCapabilitiesResponse; @@ -150,6 +151,7 @@ import org.apache.cloudstack.config.ConfigurationGroup; import org.apache.cloudstack.direct.download.DirectDownloadCertificate; import org.apache.cloudstack.direct.download.DirectDownloadCertificateHostMap; import org.apache.cloudstack.direct.download.DirectDownloadManager; +import org.apache.cloudstack.gui.theme.GuiThemeJoin; import org.apache.cloudstack.management.ManagementServerHost; import org.apache.cloudstack.network.lb.ApplicationLoadBalancerRule; import org.apache.cloudstack.region.PortableIp; @@ -579,4 +581,6 @@ public interface ResponseGenerator { SharedFSResponse createSharedFSResponse(ResponseView view, SharedFS sharedFS); void updateTemplateIsoResponsesForIcons(List responses, ResourceTag.ResourceObjectType type); + + GuiThemeResponse createGuiThemeResponse(GuiThemeJoin guiThemeJoin); } diff --git a/api/src/main/java/org/apache/cloudstack/api/command/admin/cluster/AddClusterCmd.java b/api/src/main/java/org/apache/cloudstack/api/command/admin/cluster/AddClusterCmd.java index 15265f561e7..3aef11b92e9 100644 --- a/api/src/main/java/org/apache/cloudstack/api/command/admin/cluster/AddClusterCmd.java +++ b/api/src/main/java/org/apache/cloudstack/api/command/admin/cluster/AddClusterCmd.java @@ -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 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].=. 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 getExternalDetails() { + return convertDetailsToMap(externalDetails); + } + @Override public void execute() { try { diff --git a/api/src/main/java/org/apache/cloudstack/api/command/admin/cluster/ListClustersCmd.java b/api/src/main/java/org/apache/cloudstack/api/command/admin/cluster/ListClustersCmd.java index 9cc39503fbf..b38278e3946 100644 --- a/api/src/main/java/org/apache/cloudstack/api/command/admin/cluster/ListClustersCmd.java +++ b/api/src/main/java/org/apache/cloudstack/api/command/admin/cluster/ListClustersCmd.java @@ -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,21 +153,44 @@ public class ListClustersCmd extends BaseListCmd { /////////////// API Implementation/////////////////// ///////////////////////////////////////////////////// + protected void updateClustersExtensions(final List clusterResponses) { + if (CollectionUtils.isEmpty(clusterResponses)) { + return; + } + Map 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, Integer> getClusterResponses() { Pair, Integer> result = _mgr.searchForClusters(this); - List clusterResponses = new ArrayList(); + List clusterResponses = new ArrayList<>(); for (Cluster cluster : result.first()) { ClusterResponse clusterResponse = _responseGenerator.createClusterResponse(cluster, showCapacities); clusterResponse.setObjectName("cluster"); clusterResponses.add(clusterResponse); } - return new Pair, Integer>(clusterResponses, result.second()); + updateClustersExtensions(clusterResponses); + return new Pair<>(clusterResponses, result.second()); } @Override public void execute() { Pair, Integer> clusterResponses = getClusterResponses(); - ListResponse response = new ListResponse(); + ListResponse response = new ListResponse<>(); response.setResponses(clusterResponses.first(), clusterResponses.second()); response.setResponseName(getCommandName()); this.setResponseObject(response); diff --git a/api/src/main/java/org/apache/cloudstack/api/command/admin/cluster/UpdateClusterCmd.java b/api/src/main/java/org/apache/cloudstack/api/command/admin/cluster/UpdateClusterCmd.java index 816285e3430..c160cfd2e03 100644 --- a/api/src/main/java/org/apache/cloudstack/api/command/admin/cluster/UpdateClusterCmd.java +++ b/api/src/main/java/org/apache/cloudstack/api/command/admin/cluster/UpdateClusterCmd.java @@ -16,6 +16,8 @@ // under the License. package org.apache.cloudstack.api.command.admin.cluster; +import java.util.Map; + import com.cloud.cpu.CPU; import org.apache.cloudstack.api.ApiCommandResourceType; @@ -60,6 +62,12 @@ public class UpdateClusterCmd extends BaseCmd { since = "4.20") private String arch; + @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].=. Example: externaldetails[0].endpoint.url=https://example.com", + since = "4.21.0") + protected Map externalDetails; + public String getClusterName() { return clusterName; } @@ -122,6 +130,10 @@ public class UpdateClusterCmd extends BaseCmd { return CPU.CPUArch.fromType(arch); } + public Map getExternalDetails() { + return convertDetailsToMap(externalDetails); + } + @Override public void execute() { Cluster cluster = _resourceService.getCluster(getId()); diff --git a/api/src/main/java/org/apache/cloudstack/api/command/admin/gpu/CreateGpuCardCmd.java b/api/src/main/java/org/apache/cloudstack/api/command/admin/gpu/CreateGpuCardCmd.java new file mode 100644 index 00000000000..2faad89bf67 --- /dev/null +++ b/api/src/main/java/org/apache/cloudstack/api/command/admin/gpu/CreateGpuCardCmd.java @@ -0,0 +1,122 @@ +// 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.gpu; + +import com.cloud.exception.ConcurrentOperationException; +import com.cloud.exception.InsufficientCapacityException; +import com.cloud.exception.NetworkRuleConflictException; +import com.cloud.exception.ResourceAllocationException; +import com.cloud.exception.ResourceUnavailableException; +import com.cloud.user.Account; +import org.apache.cloudstack.api.APICommand; +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.GpuCardResponse; +import org.apache.cloudstack.gpu.GpuCard; + + +@APICommand(name = "createGpuCard", description = "Creates a GPU card definition in the system", + responseObject = GpuCardResponse.class, requestHasSensitiveInfo = false, responseHasSensitiveInfo = false, + since = "4.21.0") +public class CreateGpuCardCmd extends BaseCmd { + + /// ////////////////////////////////////////////////// + /// ///////////// API parameters ///////////////////// + /// ////////////////////////////////////////////////// + + @Parameter(name = ApiConstants.DEVICE_ID, type = CommandType.STRING, required = true, + description = "the device ID of the GPU card") + private String deviceId; + + @Parameter(name = ApiConstants.DEVICE_NAME, type = CommandType.STRING, required = true, + description = "the device name of the GPU card") + private String deviceName; + + @Parameter(name = ApiConstants.NAME, type = CommandType.STRING, required = true, + description = "the display name of the GPU card") + private String name; + + @Parameter(name = ApiConstants.VENDOR_NAME, type = CommandType.STRING, required = true, + description = "the vendor name of the GPU card") + private String vendorName; + + @Parameter(name = ApiConstants.VENDOR_ID, type = CommandType.STRING, required = true, + description = "the vendor ID of the GPU card") + private String vendorId; + + // Optional parameters for the passthrough vGPU profile display properties + @Parameter(name = ApiConstants.VIDEORAM, type = CommandType.LONG, + description = "the video RAM size in MB for the passthrough vGPU profile") + private Long videoRam; + + /// ////////////////////////////////////////////////// + /// //////////////// Accessors /////////////////////// + /// ////////////////////////////////////////////////// + + public String getDeviceId() { + return deviceId; + } + + public String getDeviceName() { + return deviceName; + } + + public String getName() { + return name; + } + + public String getVendorName() { + return vendorName; + } + + public String getVendorId() { + return vendorId; + } + + public Long getVideoRam() { + return videoRam; + } + + @Override + public void execute() throws ResourceUnavailableException, InsufficientCapacityException, ServerApiException, + ConcurrentOperationException, ResourceAllocationException, NetworkRuleConflictException { + try { + GpuCard gpuCard = gpuService.createGpuCard(this); + if (gpuCard != null) { + GpuCardResponse response = new GpuCardResponse(gpuCard); + response.setResponseName(getCommandName()); + setResponseObject(response); + } else { + throw new ServerApiException(ApiErrorCode.INTERNAL_ERROR, "Failed to create GPU card"); + } + } catch (Exception e) { + throw new ServerApiException(ApiErrorCode.INTERNAL_ERROR, "Failed to create GPU card: " + e.getMessage()); + } + } + + /// ////////////////////////////////////////////////// + /// //////////// API Implementation/////////////////// + /// ////////////////////////////////////////////////// + + @Override + public long getEntityOwnerId() { + return Account.ACCOUNT_ID_SYSTEM; + } +} diff --git a/api/src/main/java/org/apache/cloudstack/api/command/admin/gpu/CreateGpuDeviceCmd.java b/api/src/main/java/org/apache/cloudstack/api/command/admin/gpu/CreateGpuDeviceCmd.java new file mode 100644 index 00000000000..e6386082a44 --- /dev/null +++ b/api/src/main/java/org/apache/cloudstack/api/command/admin/gpu/CreateGpuDeviceCmd.java @@ -0,0 +1,123 @@ +// 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.gpu; + +import com.cloud.user.Account; +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.BaseCmd; +import org.apache.cloudstack.api.Parameter; +import org.apache.cloudstack.api.ServerApiException; +import org.apache.cloudstack.api.response.GpuCardResponse; +import org.apache.cloudstack.api.response.GpuDeviceResponse; +import org.apache.cloudstack.api.response.HostResponse; +import org.apache.cloudstack.api.response.VgpuProfileResponse; +import org.apache.cloudstack.gpu.GpuDevice; +import org.apache.commons.lang3.EnumUtils; +import org.apache.commons.lang3.StringUtils; + + +@APICommand(name = "createGpuDevice", description = "Creates a GPU device manually on a host", + responseObject = GpuDeviceResponse.class, since = "4.21.0", requestHasSensitiveInfo = false, + responseHasSensitiveInfo = false, authorized = {RoleType.Admin}) +public class CreateGpuDeviceCmd extends BaseCmd { + + @Parameter(name = ApiConstants.HOST_ID, type = CommandType.UUID, entityType = HostResponse.class, required = true, + description = "ID of the host where the GPU device is located") + private Long hostId; + + @Parameter(name = ApiConstants.BUS_ADDRESS, type = CommandType.STRING, required = true, + description = "PCI bus address of the GPU device (e.g., 0000:01:00.0) or UUID for MDEV devices.") + private String busAddress; + + @Parameter(name = ApiConstants.GPU_CARD_ID, type = CommandType.UUID, entityType = GpuCardResponse.class, + required = true, description = "ID of the GPU card type") + private Long gpuCardId; + + @Parameter(name = ApiConstants.VGPU_PROFILE_ID, type = CommandType.UUID, entityType = VgpuProfileResponse.class, + required = true, description = "ID of the vGPU profile") + private Long vgpuProfileId; + + @Parameter(name = ApiConstants.TYPE, type = CommandType.STRING, + description = "Type of GPU device (PCI, MDEV, VGPUOnly). Defaults to PCI.") + private String type; + + @Parameter(name = ApiConstants.PARENT_GPU_DEVICE_ID, type = CommandType.UUID, entityType = GpuDeviceResponse.class, + description = "ID of the parent GPU device (for virtual GPU devices)") + private Long parentGpuDeviceId; + + @Parameter(name = ApiConstants.NUMA_NODE, type = CommandType.STRING, + description = "NUMA node of the GPU device (e.g., 0, 1, etc.). This is optional and can be used to " + + "specify the NUMA node for the GPU device which is used during allocation. Defaults to -1") + private String numaNode; + + public Long getHostId() { + return hostId; + } + + public String getBusAddress() { + return busAddress; + } + + public Long getGpuCardId() { + return gpuCardId; + } + + public Long getVgpuProfileId() { + return vgpuProfileId; + } + + public GpuDevice.DeviceType getType() { + GpuDevice.DeviceType deviceType = GpuDevice.DeviceType.PCI; + if (StringUtils.isNotBlank(type)) { + deviceType = EnumUtils.getEnumIgnoreCase(GpuDevice.DeviceType.class, type); + if (deviceType == null) { + throw new ServerApiException(ApiErrorCode.PARAM_ERROR, "Invalid GPU device type: " + type); + } + } + return deviceType; + } + + public Long getParentGpuDeviceId() { + return parentGpuDeviceId; + } + + public String getNumaNode() { + if (StringUtils.isBlank(numaNode)) { + return "-1"; // Default value for NUMA node + } + return numaNode; + } + + @Override + public void execute() { + try { + GpuDeviceResponse response = gpuService.createGpuDevice(this); + response.setResponseName(getCommandName()); + setResponseObject(response); + } catch (Exception e) { + throw new ServerApiException(ApiErrorCode.INTERNAL_ERROR, e.getMessage()); + } + } + + @Override + public long getEntityOwnerId() { + return Account.ACCOUNT_ID_SYSTEM; + } +} diff --git a/api/src/main/java/org/apache/cloudstack/api/command/admin/gpu/CreateVgpuProfileCmd.java b/api/src/main/java/org/apache/cloudstack/api/command/admin/gpu/CreateVgpuProfileCmd.java new file mode 100644 index 00000000000..3210773b6f4 --- /dev/null +++ b/api/src/main/java/org/apache/cloudstack/api/command/admin/gpu/CreateVgpuProfileCmd.java @@ -0,0 +1,131 @@ +// 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.gpu; + +import com.cloud.user.Account; +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.BaseCmd; +import org.apache.cloudstack.api.Parameter; +import org.apache.cloudstack.api.ServerApiException; +import org.apache.cloudstack.api.response.GpuCardResponse; +import org.apache.cloudstack.api.response.VgpuProfileResponse; + + +@APICommand(name = "createVgpuProfile", description = "Creates a vGPU profile in the system", + responseObject = VgpuProfileResponse.class, requestHasSensitiveInfo = false, + responseHasSensitiveInfo = false, since = "4.21.0", authorized = {RoleType.Admin}) +public class CreateVgpuProfileCmd extends BaseCmd { + + /// ////////////////////////////////////////////////// + /// ///////////// API parameters ///////////////////// + /// ////////////////////////////////////////////////// + + @Parameter(name = ApiConstants.NAME, type = CommandType.STRING, required = true, + description = "the name of the vGPU profile") + private String name; + + @Parameter(name = ApiConstants.DESCRIPTION, type = CommandType.STRING, + description = "the description of the vGPU profile") + private String description; + + @Parameter(name = ApiConstants.GPU_CARD_ID, type = CommandType.UUID, entityType = GpuCardResponse.class, + required = true, description = "the GPU card ID associated with this GPU device") + private Long cardId; + + @Parameter(name = ApiConstants.MAX_VGPU_PER_PHYSICAL_GPU, type = CommandType.LONG, + description = "Max vGPU per physical GPU. This is used to calculate capacity.") + private Long maxVgpuPerPgpu; + + @Parameter(name = ApiConstants.VIDEORAM, type = CommandType.LONG, + description = "the video RAM size in MB") + private Long videoRam; + + @Parameter(name = ApiConstants.MAXHEADS, type = CommandType.LONG, + description = "the maximum number of display heads") + private Long maxHeads; + + @Parameter(name = ApiConstants.MAXRESOLUTIONX, type = CommandType.LONG, + description = "the maximum X resolution") + private Long maxResolutionX; + + @Parameter(name = ApiConstants.MAXRESOLUTIONY, type = CommandType.LONG, + description = "the maximum Y resolution") + private Long maxResolutionY; + + /// ////////////////////////////////////////////////// + /// //////////////// Accessors /////////////////////// + /// ////////////////////////////////////////////////// + + public String getName() { + return name; + } + + public String getDescription() { + return description; + } + + public Long getCardId() { + return cardId; + } + + public Long getMaxVgpuPerPgpu() { + return maxVgpuPerPgpu; + } + + public Long getVideoRam() { + return videoRam; + } + + public Long getMaxHeads() { + return maxHeads; + } + + public Long getMaxResolutionX() { + return maxResolutionX; + } + + public Long getMaxResolutionY() { + return maxResolutionY; + } + + @Override + public void execute() { + try { + VgpuProfileResponse response = gpuService.createVgpuProfile(this); + if (response != null) { + setResponseObject(response); + } else { + throw new ServerApiException(ApiErrorCode.INTERNAL_ERROR, "Failed to create vGPU profile"); + } + } catch (Exception e) { + throw new ServerApiException(ApiErrorCode.INTERNAL_ERROR, + "Failed to create vGPU profile: " + e.getMessage()); + } + } + + /// ////////////////////////////////////////////////// + /// //////////// API Implementation/////////////////// + /// ////////////////////////////////////////////////// + + @Override + public long getEntityOwnerId() { + return Account.ACCOUNT_ID_SYSTEM; + } +} diff --git a/api/src/main/java/org/apache/cloudstack/api/command/admin/gpu/DeleteGpuCardCmd.java b/api/src/main/java/org/apache/cloudstack/api/command/admin/gpu/DeleteGpuCardCmd.java new file mode 100644 index 00000000000..9a510ecdead --- /dev/null +++ b/api/src/main/java/org/apache/cloudstack/api/command/admin/gpu/DeleteGpuCardCmd.java @@ -0,0 +1,75 @@ +// 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.gpu; + +import com.cloud.user.Account; +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.BaseCmd; +import org.apache.cloudstack.api.Parameter; +import org.apache.cloudstack.api.ServerApiException; +import org.apache.cloudstack.api.response.GpuCardResponse; +import org.apache.cloudstack.api.response.SuccessResponse; + + +@APICommand(name = "deleteGpuCard", description = "Deletes a GPU card definition from the system", + responseObject = SuccessResponse.class, requestHasSensitiveInfo = false, responseHasSensitiveInfo = false, + since = "4.21.0", authorized = {RoleType.Admin}) +public class DeleteGpuCardCmd extends BaseCmd { + + /// ////////////////////////////////////////////////// + /// ///////////// API parameters ///////////////////// + /// ////////////////////////////////////////////////// + + @Parameter(name = ApiConstants.ID, type = CommandType.UUID, entityType = GpuCardResponse.class, required = true, + description = "the ID of the GPU card") + private Long id; + + /// ////////////////////////////////////////////////// + /// //////////////// Accessors /////////////////////// + /// ////////////////////////////////////////////////// + + public Long getId() { + return id; + } + + @Override + public void execute() { + try { + boolean success = gpuService.deleteGpuCard(this); + if (success) { + SuccessResponse response = new SuccessResponse(getCommandName()); + setResponseObject(response); + } else { + throw new ServerApiException(ApiErrorCode.INTERNAL_ERROR, "Failed to delete GPU card"); + } + } catch (Exception e) { + throw new ServerApiException(ApiErrorCode.INTERNAL_ERROR, "Failed to delete GPU card: " + e.getMessage()); + } + } + + /// ////////////////////////////////////////////////// + /// //////////// API Implementation/////////////////// + /// ////////////////////////////////////////////////// + + @Override + public long getEntityOwnerId() { + return Account.ACCOUNT_ID_SYSTEM; + } +} diff --git a/api/src/main/java/org/apache/cloudstack/api/command/admin/gpu/DeleteGpuDeviceCmd.java b/api/src/main/java/org/apache/cloudstack/api/command/admin/gpu/DeleteGpuDeviceCmd.java new file mode 100644 index 00000000000..9224afc66ec --- /dev/null +++ b/api/src/main/java/org/apache/cloudstack/api/command/admin/gpu/DeleteGpuDeviceCmd.java @@ -0,0 +1,78 @@ +// 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.gpu; + +import com.cloud.user.Account; +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.BaseCmd; +import org.apache.cloudstack.api.Parameter; +import org.apache.cloudstack.api.ServerApiException; +import org.apache.cloudstack.api.response.GpuDeviceResponse; +import org.apache.cloudstack.api.response.SuccessResponse; + +import java.util.List; + +@APICommand(name = "deleteGpuDevice", description = "Deletes a vGPU profile from the system", + responseObject = SuccessResponse.class, requestHasSensitiveInfo = false, responseHasSensitiveInfo = false, + since = "4.21.0", authorized = {RoleType.Admin}) +public class DeleteGpuDeviceCmd extends BaseCmd { + + /// ////////////////////////////////////////////////// + /// ///////////// API parameters ///////////////////// + /// ////////////////////////////////////////////////// + + @Parameter(name = ApiConstants.IDS, type = CommandType.LIST, collectionType = CommandType.UUID, + entityType = GpuDeviceResponse.class, required = true, + description = "comma separated list of IDs of the GPU device") + private List ids; + + /// ////////////////////////////////////////////////// + /// //////////////// Accessors /////////////////////// + /// ////////////////////////////////////////////////// + + public List getIds() { + return ids; + } + + @Override + public void execute() { + try { + boolean success = gpuService.deleteGpuDevices(this); + if (success) { + SuccessResponse response = new SuccessResponse(getCommandName()); + setResponseObject(response); + } else { + throw new ServerApiException(ApiErrorCode.INTERNAL_ERROR, "Failed to delete vGPU profile"); + } + } catch (Exception e) { + throw new ServerApiException(ApiErrorCode.INTERNAL_ERROR, + "Failed to delete vGPU profile: " + e.getMessage()); + } + } + + /// ////////////////////////////////////////////////// + /// //////////// API Implementation/////////////////// + /// ////////////////////////////////////////////////// + + @Override + public long getEntityOwnerId() { + return Account.ACCOUNT_ID_SYSTEM; + } +} diff --git a/api/src/main/java/org/apache/cloudstack/api/command/admin/gpu/DeleteVgpuProfileCmd.java b/api/src/main/java/org/apache/cloudstack/api/command/admin/gpu/DeleteVgpuProfileCmd.java new file mode 100644 index 00000000000..a09a04199f5 --- /dev/null +++ b/api/src/main/java/org/apache/cloudstack/api/command/admin/gpu/DeleteVgpuProfileCmd.java @@ -0,0 +1,76 @@ +// 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.gpu; + +import com.cloud.user.Account; +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.BaseCmd; +import org.apache.cloudstack.api.Parameter; +import org.apache.cloudstack.api.ServerApiException; +import org.apache.cloudstack.api.response.SuccessResponse; +import org.apache.cloudstack.api.response.VgpuProfileResponse; + + +@APICommand(name = "deleteVgpuProfile", description = "Deletes a vGPU profile from the system", + responseObject = SuccessResponse.class, requestHasSensitiveInfo = false, responseHasSensitiveInfo = false, + since = "4.21.0", authorized = {RoleType.Admin}) +public class DeleteVgpuProfileCmd extends BaseCmd { + + /// ////////////////////////////////////////////////// + /// ///////////// API parameters ///////////////////// + /// ////////////////////////////////////////////////// + + @Parameter(name = ApiConstants.ID, type = CommandType.UUID, entityType = VgpuProfileResponse.class, required = true, + description = "the ID of the vGPU profile") + private Long id; + + /// ////////////////////////////////////////////////// + /// //////////////// Accessors /////////////////////// + /// ////////////////////////////////////////////////// + + public Long getId() { + return id; + } + + @Override + public void execute() { + try { + boolean success = gpuService.deleteVgpuProfile(this); + if (success) { + SuccessResponse response = new SuccessResponse(getCommandName()); + setResponseObject(response); + } else { + throw new ServerApiException(ApiErrorCode.INTERNAL_ERROR, "Failed to delete vGPU profile"); + } + } catch (Exception e) { + throw new ServerApiException(ApiErrorCode.INTERNAL_ERROR, + "Failed to delete vGPU profile: " + e.getMessage()); + } + } + + /// ////////////////////////////////////////////////// + /// //////////// API Implementation/////////////////// + /// ////////////////////////////////////////////////// + + @Override + public long getEntityOwnerId() { + return Account.ACCOUNT_ID_SYSTEM; + } +} diff --git a/api/src/main/java/org/apache/cloudstack/api/command/admin/gpu/DiscoverGpuDevicesCmd.java b/api/src/main/java/org/apache/cloudstack/api/command/admin/gpu/DiscoverGpuDevicesCmd.java new file mode 100644 index 00000000000..2ac07a9fb3a --- /dev/null +++ b/api/src/main/java/org/apache/cloudstack/api/command/admin/gpu/DiscoverGpuDevicesCmd.java @@ -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.api.command.admin.gpu; + +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.response.GpuDeviceResponse; +import org.apache.cloudstack.api.response.HostResponse; +import org.apache.cloudstack.api.response.ListResponse; +import org.apache.cloudstack.context.CallContext; + + +@APICommand(name = "discoverGpuDevices", description = "Discovers available GPU devices on a host", + responseObject = GpuDeviceResponse.class, requestHasSensitiveInfo = false, responseHasSensitiveInfo = false, + since = "4.21.0", authorized = {RoleType.Admin}) +public class DiscoverGpuDevicesCmd extends BaseListCmd { + + /// ////////////////////////////////////////////////// + /// ///////////// API parameters ///////////////////// + /// ////////////////////////////////////////////////// + + @Parameter(name = ApiConstants.ID, type = CommandType.UUID, entityType = HostResponse.class, required = true, + description = "ID of the host") + private Long id; + + /// ////////////////////////////////////////////////// + /// //////////// API Implementation ////////////////// + /// ////////////////////////////////////////////////// + + @Override + public void execute() { + CallContext.current().setEventDetails("Discovering GPU Devices on host id: " + getId()); + ListResponse response = gpuService.discoverGpuDevices(this); + response.setResponseName(getCommandName()); + setResponseObject(response); + } + + /// ////////////////////////////////////////////////// + /// //////////////// Accessors /////////////////////// + /// ////////////////////////////////////////////////// + + public Long getId() { + return id; + } + +} diff --git a/api/src/main/java/org/apache/cloudstack/api/command/admin/gpu/ListGpuDevicesCmdByAdmin.java b/api/src/main/java/org/apache/cloudstack/api/command/admin/gpu/ListGpuDevicesCmdByAdmin.java new file mode 100644 index 00000000000..b3c57713fcd --- /dev/null +++ b/api/src/main/java/org/apache/cloudstack/api/command/admin/gpu/ListGpuDevicesCmdByAdmin.java @@ -0,0 +1,74 @@ +// Licensed to the Apache Software Foundation (ASF) under one +// or more contributor license agreements. See the NOTICE file +// distributed with this work for additional information +// regarding copyright ownership. The ASF licenses this file +// to you under the Apache License, Version 2.0 (the +// "License"); you may not use this file except in compliance +// with the License. You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, +// software distributed under the License is distributed on an +// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +// KIND, either express or implied. See the License for the +// specific language governing permissions and limitations +// under the License. +package org.apache.cloudstack.api.command.admin.gpu; + +import org.apache.cloudstack.api.APICommand; +import org.apache.cloudstack.api.ApiConstants; +import org.apache.cloudstack.api.Parameter; +import org.apache.cloudstack.api.ResponseObject; +import org.apache.cloudstack.api.command.admin.AdminCmd; +import org.apache.cloudstack.api.command.user.gpu.ListGpuDevicesCmd; +import org.apache.cloudstack.api.response.GpuCardResponse; +import org.apache.cloudstack.api.response.GpuDeviceResponse; +import org.apache.cloudstack.api.response.HostResponse; +import org.apache.cloudstack.api.response.VgpuProfileResponse; + + +@APICommand(name = "listGpuDevices", description = "Lists all available GPU devices", + responseView = ResponseObject.ResponseView.Full, + responseObject = GpuDeviceResponse.class, requestHasSensitiveInfo = false, responseHasSensitiveInfo = false, + since = "4.21.0") +public class ListGpuDevicesCmdByAdmin extends ListGpuDevicesCmd implements AdminCmd { + + @Parameter(name = ApiConstants.ID, type = CommandType.UUID, entityType = GpuDeviceResponse.class, + description = "ID of the GPU device") + private Long id; + + @Parameter(name = ApiConstants.HOST_ID, type = CommandType.UUID, entityType = HostResponse.class, + description = "the host ID where the GPU device is attached") + private Long hostId; + + @Parameter(name = ApiConstants.GPU_CARD_ID, type = CommandType.UUID, entityType = GpuCardResponse.class, + description = "the GPU card ID associated with the GPU device") + private Long gpuCardId; + + @Parameter(name = ApiConstants.VGPU_PROFILE_ID, type = CommandType.UUID, entityType = VgpuProfileResponse.class, + description = "the vGPU profile ID assigned to the GPU device") + private Long vgpuProfileId; + + + /// ////////////////////////////////////////////////// + /// //////////////// Accessors /////////////////////// + /// ////////////////////////////////////////////////// + + public Long getId() { + return id; + } + + public Long getHostId() { + return hostId; + } + + public Long getGpuCardId() { + return gpuCardId; + } + + public Long getVgpuProfileId() { + return vgpuProfileId; + } + +} diff --git a/api/src/main/java/org/apache/cloudstack/api/command/admin/gpu/ManageGpuDeviceCmd.java b/api/src/main/java/org/apache/cloudstack/api/command/admin/gpu/ManageGpuDeviceCmd.java new file mode 100644 index 00000000000..5dfe6c3deee --- /dev/null +++ b/api/src/main/java/org/apache/cloudstack/api/command/admin/gpu/ManageGpuDeviceCmd.java @@ -0,0 +1,78 @@ +// 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.gpu; + +import com.cloud.user.Account; +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.BaseCmd; +import org.apache.cloudstack.api.Parameter; +import org.apache.cloudstack.api.ServerApiException; +import org.apache.cloudstack.api.response.GpuDeviceResponse; +import org.apache.cloudstack.api.response.SuccessResponse; + +import java.util.List; + +@APICommand(name = "manageGpuDevice", description = "Manages a GPU device", responseObject = SuccessResponse.class, + requestHasSensitiveInfo = false, responseHasSensitiveInfo = false, since = "4.21.0", + authorized = {RoleType.Admin}) +public class ManageGpuDeviceCmd extends BaseCmd { + + /// ////////////////////////////////////////////////// + /// ///////////// API parameters ///////////////////// + /// ////////////////////////////////////////////////// + + @Parameter(name = ApiConstants.IDS, type = CommandType.LIST, collectionType = CommandType.UUID, + entityType = GpuDeviceResponse.class, required = true, + description = "comma separated list of IDs of the GPU device") + private List ids; + + + /// ////////////////////////////////////////////////// + /// //////////////// Accessors /////////////////////// + /// ////////////////////////////////////////////////// + + public List getIds() { + return ids; + } + + @Override + public void execute() { + try { + if (gpuService.enableGpuDevice(this)) { + SuccessResponse response = new SuccessResponse(); + response.setResponseName(getCommandName()); + setResponseObject(response); + } else { + throw new ServerApiException(ApiErrorCode.INTERNAL_ERROR, "Failed to enable GPU device"); + } + } catch (Exception e) { + throw new ServerApiException(ApiErrorCode.INTERNAL_ERROR, "Failed to enable GPU device: " + e.getMessage()); + } + } + + /// ////////////////////////////////////////////////// + /// //////////// API Implementation/////////////////// + /// ////////////////////////////////////////////////// + + @Override + public long getEntityOwnerId() { + return Account.ACCOUNT_ID_SYSTEM; + } +} diff --git a/api/src/main/java/org/apache/cloudstack/api/command/admin/gpu/UnmanageGpuDeviceCmd.java b/api/src/main/java/org/apache/cloudstack/api/command/admin/gpu/UnmanageGpuDeviceCmd.java new file mode 100644 index 00000000000..46de23ec44b --- /dev/null +++ b/api/src/main/java/org/apache/cloudstack/api/command/admin/gpu/UnmanageGpuDeviceCmd.java @@ -0,0 +1,79 @@ +// Licensed to the Apache Software Foundation (ASF) under one +// or more contributor license agreements. See the NOTICE file +// distributed with this work for additional information +// regarding copyright ownership. The ASF licenses this file +// to you under the Apache License, Version 2.0 (the +// "License"); you may not use this file except in compliance +// with the License. You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, +// software distributed under the License is distributed on an +// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +// KIND, either express or implied. See the License for the +// specific language governing permissions and limitations +// under the License. +package org.apache.cloudstack.api.command.admin.gpu; + +import com.cloud.user.Account; +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.BaseCmd; +import org.apache.cloudstack.api.Parameter; +import org.apache.cloudstack.api.ServerApiException; +import org.apache.cloudstack.api.response.GpuDeviceResponse; +import org.apache.cloudstack.api.response.SuccessResponse; + +import java.util.List; + +@APICommand(name = "unmanageGpuDevice", description = "Unmanage a GPU device", responseObject = SuccessResponse.class, + requestHasSensitiveInfo = false, responseHasSensitiveInfo = false, since = "4.21.0", + authorized = {RoleType.Admin}) +public class UnmanageGpuDeviceCmd extends BaseCmd { + + /// ////////////////////////////////////////////////// + /// ///////////// API parameters ///////////////////// + /// ////////////////////////////////////////////////// + + @Parameter(name = ApiConstants.IDS, type = CommandType.LIST, collectionType = CommandType.UUID, + entityType = GpuDeviceResponse.class, required = true, + description = "comma separated list of IDs of the GPU device") + private List ids; + + + /// ////////////////////////////////////////////////// + /// //////////////// Accessors /////////////////////// + /// ////////////////////////////////////////////////// + + public List getIds() { + return ids; + } + + @Override + public void execute() { + try { + if (gpuService.disableGpuDevice(this)) { + SuccessResponse response = new SuccessResponse(); + response.setResponseName(getCommandName()); + setResponseObject(response); + } else { + throw new ServerApiException(ApiErrorCode.INTERNAL_ERROR, "Failed to disable GPU device"); + } + } catch (Exception e) { + throw new ServerApiException(ApiErrorCode.INTERNAL_ERROR, + "Failed to disable GPU device: " + e.getMessage()); + } + } + + /// ////////////////////////////////////////////////// + /// //////////// API Implementation/////////////////// + /// ////////////////////////////////////////////////// + + @Override + public long getEntityOwnerId() { + return Account.ACCOUNT_ID_SYSTEM; + } +} diff --git a/api/src/main/java/org/apache/cloudstack/api/command/admin/gpu/UpdateGpuCardCmd.java b/api/src/main/java/org/apache/cloudstack/api/command/admin/gpu/UpdateGpuCardCmd.java new file mode 100644 index 00000000000..0061149a985 --- /dev/null +++ b/api/src/main/java/org/apache/cloudstack/api/command/admin/gpu/UpdateGpuCardCmd.java @@ -0,0 +1,99 @@ +// 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.gpu; + +import com.cloud.user.Account; +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.BaseCmd; +import org.apache.cloudstack.api.Parameter; +import org.apache.cloudstack.api.ServerApiException; +import org.apache.cloudstack.api.response.GpuCardResponse; +import org.apache.cloudstack.gpu.GpuCard; + + +@APICommand(name = "updateGpuCard", description = "Updates a GPU card definition in the system", + responseObject = GpuCardResponse.class, requestHasSensitiveInfo = false, responseHasSensitiveInfo = false, + since = "4.21.0", authorized = {RoleType.Admin}) +public class UpdateGpuCardCmd extends BaseCmd { + + /// ////////////////////////////////////////////////// + /// ///////////// API parameters ///////////////////// + /// ////////////////////////////////////////////////// + + @Parameter(name = ApiConstants.ID, type = CommandType.UUID, entityType = GpuCardResponse.class, required = true, + description = "the ID of the GPU card") + private Long id; + + @Parameter(name = ApiConstants.DEVICE_NAME, type = CommandType.STRING, + description = "the device name of the GPU card") + private String deviceName; + + @Parameter(name = ApiConstants.NAME, type = CommandType.STRING, description = "the display name of the GPU card") + private String name; + + @Parameter(name = ApiConstants.VENDOR_NAME, type = CommandType.STRING, + description = "the vendor name of the GPU card") + private String vendorName; + + /// ////////////////////////////////////////////////// + /// //////////////// Accessors /////////////////////// + /// ////////////////////////////////////////////////// + + public Long getId() { + return id; + } + + public String getDeviceName() { + return deviceName; + } + + public String getName() { + return name; + } + + public String getVendorName() { + return vendorName; + } + + @Override + public void execute() { + try { + GpuCard gpuCard = gpuService.updateGpuCard(this); + if (gpuCard != null) { + GpuCardResponse response = new GpuCardResponse(gpuCard); + response.setResponseName(getCommandName()); + setResponseObject(response); + } else { + throw new ServerApiException(ApiErrorCode.INTERNAL_ERROR, "Failed to update GPU card"); + } + } catch (Exception e) { + throw new ServerApiException(ApiErrorCode.INTERNAL_ERROR, "Failed to update GPU card: " + e.getMessage()); + } + } + + /// ////////////////////////////////////////////////// + /// //////////// API Implementation/////////////////// + /// ////////////////////////////////////////////////// + + @Override + public long getEntityOwnerId() { + return Account.ACCOUNT_ID_SYSTEM; + } +} diff --git a/api/src/main/java/org/apache/cloudstack/api/command/admin/gpu/UpdateGpuDeviceCmd.java b/api/src/main/java/org/apache/cloudstack/api/command/admin/gpu/UpdateGpuDeviceCmd.java new file mode 100644 index 00000000000..5ad6e6e1a6f --- /dev/null +++ b/api/src/main/java/org/apache/cloudstack/api/command/admin/gpu/UpdateGpuDeviceCmd.java @@ -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.api.command.admin.gpu; + +import com.cloud.user.Account; +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.BaseCmd; +import org.apache.cloudstack.api.Parameter; +import org.apache.cloudstack.api.ServerApiException; +import org.apache.cloudstack.api.response.GpuCardResponse; +import org.apache.cloudstack.api.response.GpuDeviceResponse; +import org.apache.cloudstack.api.response.VgpuProfileResponse; +import org.apache.cloudstack.gpu.GpuDevice; +import org.apache.commons.lang3.EnumUtils; +import org.apache.commons.lang3.StringUtils; + + +@APICommand(name = "updateGpuDevice", description = "Updates an existing GPU device", + responseObject = GpuDeviceResponse.class, since = "4.21.0", requestHasSensitiveInfo = false, + responseHasSensitiveInfo = false, authorized = {RoleType.Admin}) +public class UpdateGpuDeviceCmd extends BaseCmd { + + @Parameter(name = ApiConstants.ID, type = CommandType.UUID, entityType = GpuDeviceResponse.class, required = true, + description = "ID of the GPU device to update") + private Long id; + + @Parameter(name = ApiConstants.GPU_CARD_ID, type = CommandType.UUID, entityType = GpuCardResponse.class, + description = "New GPU card ID") + private Long gpuCardId; + + @Parameter(name = ApiConstants.VGPU_PROFILE_ID, type = CommandType.UUID, entityType = VgpuProfileResponse.class, + description = "New vGPU profile ID") + private Long vgpuProfileId; + + @Parameter(name = "type", type = CommandType.STRING, description = "New type of GPU device (PCI, MDEV, VGPUOnly)") + private String type; + + @Parameter(name = "parentgpudeviceid", type = CommandType.UUID, entityType = GpuDeviceResponse.class, + description = "New parent GPU device ID (for virtual GPU devices)") + private Long parentGpuDeviceId; + + @Parameter(name = ApiConstants.NUMA_NODE, type = CommandType.STRING, + description = "New NUMA node of the GPU device") + private String numaNode; + + public Long getId() { + return id; + } + + public Long getGpuCardId() { + return gpuCardId; + } + + public Long getVgpuProfileId() { + return vgpuProfileId; + } + + public GpuDevice.DeviceType getType() { + GpuDevice.DeviceType deviceType = null; + if (StringUtils.isNotBlank(type)) { + deviceType = EnumUtils.getEnumIgnoreCase(GpuDevice.DeviceType.class, type); + if (deviceType == null) { + throw new ServerApiException(ApiErrorCode.PARAM_ERROR, "Invalid GPU device type: " + type); + } + } + return deviceType; + } + + public Long getParentGpuDeviceId() { + return parentGpuDeviceId; + } + + public String getNumaNode() { + return numaNode; + } + + @Override + public void execute() { + try { + GpuDeviceResponse response = gpuService.updateGpuDevice(this); + response.setResponseName(getCommandName()); + setResponseObject(response); + } catch (Exception e) { + throw new ServerApiException(ApiErrorCode.INTERNAL_ERROR, e.getMessage()); + } + } + + @Override + public long getEntityOwnerId() { + return Account.ACCOUNT_ID_SYSTEM; + } +} diff --git a/api/src/main/java/org/apache/cloudstack/api/command/admin/gpu/UpdateVgpuProfileCmd.java b/api/src/main/java/org/apache/cloudstack/api/command/admin/gpu/UpdateVgpuProfileCmd.java new file mode 100644 index 00000000000..c8d60739bd4 --- /dev/null +++ b/api/src/main/java/org/apache/cloudstack/api/command/admin/gpu/UpdateVgpuProfileCmd.java @@ -0,0 +1,129 @@ +// 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.gpu; + +import com.cloud.user.Account; +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.BaseCmd; +import org.apache.cloudstack.api.Parameter; +import org.apache.cloudstack.api.ServerApiException; +import org.apache.cloudstack.api.response.VgpuProfileResponse; + + +@APICommand(name = "updateVgpuProfile", description = "Updates a vGPU profile in the system", + responseObject = VgpuProfileResponse.class, requestHasSensitiveInfo = false, + responseHasSensitiveInfo = false, since = "4.21.0", authorized = {RoleType.Admin}) +public class UpdateVgpuProfileCmd extends BaseCmd { + + /// ////////////////////////////////////////////////// + /// ///////////// API parameters ///////////////////// + /// ////////////////////////////////////////////////// + + @Parameter(name = ApiConstants.ID, type = CommandType.UUID, entityType = VgpuProfileResponse.class, required = true, + description = "the ID of the vGPU profile") + private Long id; + + @Parameter(name = ApiConstants.NAME, type = CommandType.STRING, description = "the name of the vGPU profile") + private String profileName; + + @Parameter(name = ApiConstants.DESCRIPTION, type = CommandType.STRING, + description = "the description of the vGPU profile") + private String description; + + @Parameter(name = ApiConstants.MAX_VGPU_PER_PHYSICAL_GPU, type = CommandType.LONG, + description = "the maximum number of vGPUs per physical GPU") + private Long maxVgpuPerPgpu; + + @Parameter(name = ApiConstants.VIDEORAM, type = CommandType.LONG, + description = "the video RAM size in MB") + private Long videoRam; + + @Parameter(name = ApiConstants.MAXHEADS, type = CommandType.LONG, + description = "the maximum number of display heads") + private Long maxHeads; + + @Parameter(name = ApiConstants.MAXRESOLUTIONX, type = CommandType.LONG, + description = "the maximum X resolution") + private Long maxResolutionX; + + @Parameter(name = ApiConstants.MAXRESOLUTIONY, type = CommandType.LONG, + description = "the maximum Y resolution") + private Long maxResolutionY; + + /// ////////////////////////////////////////////////// + /// //////////////// Accessors /////////////////////// + /// ////////////////////////////////////////////////// + + public Long getId() { + return id; + } + + public String getProfileName() { + return profileName; + } + + public String getDescription() { + return description; + } + + public Long getMaxVgpuPerPgpu() { + return maxVgpuPerPgpu; + } + + public Long getVideoRam() { + return videoRam; + } + + public Long getMaxHeads() { + return maxHeads; + } + + public Long getMaxResolutionX() { + return maxResolutionX; + } + + public Long getMaxResolutionY() { + return maxResolutionY; + } + + @Override + public void execute() { + try { + VgpuProfileResponse response = gpuService.updateVgpuProfile(this); + if (response != null) { + setResponseObject(response); + } else { + throw new ServerApiException(ApiErrorCode.INTERNAL_ERROR, "Failed to update vGPU profile"); + } + } catch (Exception e) { + throw new ServerApiException(ApiErrorCode.INTERNAL_ERROR, + "Failed to update vGPU profile: " + e.getMessage()); + } + } + + /// ////////////////////////////////////////////////// + /// //////////// API Implementation/////////////////// + /// ////////////////////////////////////////////////// + + @Override + public long getEntityOwnerId() { + return Account.ACCOUNT_ID_SYSTEM; + } +} diff --git a/api/src/main/java/org/apache/cloudstack/api/command/admin/host/AddHostCmd.java b/api/src/main/java/org/apache/cloudstack/api/command/admin/host/AddHostCmd.java index 6531444b52e..cc124ab8106 100644 --- a/api/src/main/java/org/apache/cloudstack/api/command/admin/host/AddHostCmd.java +++ b/api/src/main/java/org/apache/cloudstack/api/command/admin/host/AddHostCmd.java @@ -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 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 getExternalDetails() { + return convertExternalDetailsToMap(externalDetails); + } + ///////////////////////////////////////////////////// /////////////// API Implementation/////////////////// ///////////////////////////////////////////////////// diff --git a/api/src/main/java/org/apache/cloudstack/api/command/admin/host/UpdateHostCmd.java b/api/src/main/java/org/apache/cloudstack/api/command/admin/host/UpdateHostCmd.java index 397f9c80735..9baea58b7b5 100644 --- a/api/src/main/java/org/apache/cloudstack/api/command/admin/host/UpdateHostCmd.java +++ b/api/src/main/java/org/apache/cloudstack/api/command/admin/host/UpdateHostCmd.java @@ -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 getExternalDetails() { + return convertExternalDetailsToMap(externalDetails); + } + ///////////////////////////////////////////////////// /////////////// API Implementation/////////////////// ///////////////////////////////////////////////////// diff --git a/api/src/main/java/org/apache/cloudstack/api/command/admin/network/CreateNetworkOfferingCmd.java b/api/src/main/java/org/apache/cloudstack/api/command/admin/network/CreateNetworkOfferingCmd.java index af3db374a7c..51ba6ec02ea 100644 --- a/api/src/main/java/org/apache/cloudstack/api/command/admin/network/CreateNetworkOfferingCmd.java +++ b/api/src/main/java/org/apache/cloudstack/api/command/admin/network/CreateNetworkOfferingCmd.java @@ -17,6 +17,7 @@ package org.apache.cloudstack.api.command.admin.network; import java.util.ArrayList; +import java.util.Arrays; import java.util.Collection; import java.util.HashMap; import java.util.Iterator; @@ -140,12 +141,19 @@ public class CreateNetworkOfferingCmd extends BaseCmd { description = "true if network offering is meant to be used for VPC, false otherwise.") private Boolean forVpc; + @Deprecated @Parameter(name = ApiConstants.FOR_NSX, type = CommandType.BOOLEAN, description = "true if network offering is meant to be used for NSX, false otherwise.", since = "4.20.0") private Boolean forNsx; + @Parameter(name = ApiConstants.PROVIDER, + type = CommandType.STRING, + description = "Name of the provider providing the service", + since = "4.21.0") + private String provider; + @Parameter(name = ApiConstants.NSX_SUPPORT_LB, type = CommandType.BOOLEAN, description = "true if network offering for NSX network offering supports Load balancer service.", @@ -257,18 +265,38 @@ public class CreateNetworkOfferingCmd extends BaseCmd { return serviceOfferingId; } + public boolean isExternalNetworkProvider() { + return Arrays.asList("NSX", "Netris").stream() + .anyMatch(s -> provider != null && s.equalsIgnoreCase(provider)); + } + + public boolean isForNsx() { + return provider != null && provider.equalsIgnoreCase("NSX"); + } + + public boolean isForNetris() { + return provider != null && provider.equalsIgnoreCase("Netris"); + } + + public String getProvider() { + return provider; + } + public List getSupportedServices() { - if (!isForNsx()) { + if (!isExternalNetworkProvider()) { return supportedServices == null ? new ArrayList() : supportedServices; } else { List services = new ArrayList<>(List.of( Dhcp.getName(), Dns.getName(), - StaticNat.getName(), - SourceNat.getName(), - PortForwarding.getName(), UserData.getName() )); + if (NetworkOffering.NetworkMode.NATTED.name().equalsIgnoreCase(getNetworkMode())) { + services.addAll(Arrays.asList( + StaticNat.getName(), + SourceNat.getName(), + PortForwarding.getName())); + } if (getNsxSupportsLbService()) { services.add(Lb.getName()); } @@ -308,10 +336,6 @@ public class CreateNetworkOfferingCmd extends BaseCmd { return forVpc; } - public boolean isForNsx() { - return BooleanUtils.isTrue(forNsx); - } - public String getNetworkMode() { return networkMode; } @@ -345,7 +369,7 @@ public class CreateNetworkOfferingCmd extends BaseCmd { public Map> getServiceProviders() { Map> serviceProviderMap = new HashMap<>(); - if (serviceProviderList != null && !serviceProviderList.isEmpty() && !isForNsx()) { + if (serviceProviderList != null && !serviceProviderList.isEmpty() && !isExternalNetworkProvider()) { Collection servicesCollection = serviceProviderList.values(); Iterator iter = servicesCollection.iterator(); while (iter.hasNext()) { @@ -361,17 +385,16 @@ public class CreateNetworkOfferingCmd extends BaseCmd { providerList.add(provider); serviceProviderMap.put(service, providerList); } - } else if (Boolean.TRUE.equals(forNsx)) { - getServiceProviderMapForNsx(serviceProviderMap); + } else if (isExternalNetworkProvider()) { + getServiceProviderMapForExternalProvider(serviceProviderMap, Network.Provider.getProvider(provider).getName()); } return serviceProviderMap; } - private void getServiceProviderMapForNsx(Map> serviceProviderMap) { + private void getServiceProviderMapForExternalProvider(Map> serviceProviderMap, String provider) { String routerProvider = Boolean.TRUE.equals(getForVpc()) ? VirtualRouterProvider.Type.VPCVirtualRouter.name() : VirtualRouterProvider.Type.VirtualRouter.name(); - List unsupportedServices = new ArrayList<>(List.of("Vpn", "SecurityGroup", "Connectivity", - "Gateway", "BaremetalPxeService")); + List unsupportedServices = new ArrayList<>(List.of("Vpn", "Gateway", "SecurityGroup", "Connectivity", "BaremetalPxeService")); List routerSupported = List.of("Dhcp", "Dns", "UserData"); List allServices = Service.listAllServices().stream().map(Service::getName).collect(Collectors.toList()); if (routerProvider.equals(VirtualRouterProvider.Type.VPCVirtualRouter.name())) { @@ -384,8 +407,9 @@ public class CreateNetworkOfferingCmd extends BaseCmd { continue; if (routerSupported.contains(service)) serviceProviderMap.put(service, List.of(routerProvider)); - else - serviceProviderMap.put(service, List.of(Network.Provider.Nsx.getName())); + else if (NetworkOffering.NetworkMode.NATTED.name().equalsIgnoreCase(getNetworkMode()) || NetworkACL.getName().equalsIgnoreCase(service)) { + serviceProviderMap.put(service, List.of(provider)); + } if (!getNsxSupportsLbService()) { serviceProviderMap.remove(Lb.getName()); } diff --git a/api/src/main/java/org/apache/cloudstack/api/command/admin/offering/CreateServiceOfferingCmd.java b/api/src/main/java/org/apache/cloudstack/api/command/admin/offering/CreateServiceOfferingCmd.java index 4cadaad0e47..3d20ed50a5d 100644 --- a/api/src/main/java/org/apache/cloudstack/api/command/admin/offering/CreateServiceOfferingCmd.java +++ b/api/src/main/java/org/apache/cloudstack/api/command/admin/offering/CreateServiceOfferingCmd.java @@ -29,16 +29,17 @@ 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.VgpuProfileResponse; 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 +264,31 @@ public class CreateServiceOfferingCmd extends BaseCmd { description = "Lease expiry action, valid values are STOP and DESTROY") private String leaseExpiryAction; + @Parameter(name = ApiConstants.VGPU_PROFILE_ID, + type = CommandType.UUID, + entityType = VgpuProfileResponse.class, + description = "the ID of the vGPU profile to which service offering should be mapped", + since = "4.21") + private Long vgpuProfileId; + + @Parameter(name = ApiConstants.GPU_COUNT, + type = CommandType.INTEGER, + description = "Count of GPUs to be used with this service offering. This is applicable only when passed with vGPU profile.", + since = "4.21") + private Integer gpuCount; + + @Parameter(name = ApiConstants.GPU_DISPLAY, + type = CommandType.BOOLEAN, + description = "Whether to enable GPU display for this service offering. This is applicable only when passed with vGPU profile. Defaults to false.", + since = "4.21") + private Boolean gpuDisplay; + + @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 +395,15 @@ public class CreateServiceOfferingCmd extends BaseCmd { } } } + + detailsMap.putAll(getExternalDetails()); return detailsMap; } + public Map getExternalDetails() { + return convertExternalDetailsToMap(externalDetails); + } + public Long getRootDiskSize() { return rootDiskSize; } @@ -517,6 +549,18 @@ public class CreateServiceOfferingCmd extends BaseCmd { return Boolean.TRUE.equals(purgeResources); } + public Long getVgpuProfileId() { + return vgpuProfileId; + } + + public Integer getGpuCount() { + return gpuCount; + } + + public Boolean getGpuDisplay() { + return Boolean.TRUE.equals(gpuDisplay); + } + ///////////////////////////////////////////////////// /////////////// API Implementation/////////////////// ///////////////////////////////////////////////////// diff --git a/api/src/main/java/org/apache/cloudstack/api/command/admin/offering/UpdateServiceOfferingCmd.java b/api/src/main/java/org/apache/cloudstack/api/command/admin/offering/UpdateServiceOfferingCmd.java index e580f0d9f41..7f66ac7058a 100644 --- a/api/src/main/java/org/apache/cloudstack/api/command/admin/offering/UpdateServiceOfferingCmd.java +++ b/api/src/main/java/org/apache/cloudstack/api/command/admin/offering/UpdateServiceOfferingCmd.java @@ -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 getExternalDetails() { + return convertExternalDetailsToMap(externalDetails); + } + ///////////////////////////////////////////////////// /////////////// API Implementation/////////////////// ///////////////////////////////////////////////////// diff --git a/api/src/main/java/org/apache/cloudstack/api/command/admin/pod/ListPodsByCmd.java b/api/src/main/java/org/apache/cloudstack/api/command/admin/pod/ListPodsByCmd.java index ca5635d4fe4..264e5263c4d 100644 --- a/api/src/main/java/org/apache/cloudstack/api/command/admin/pod/ListPodsByCmd.java +++ b/api/src/main/java/org/apache/cloudstack/api/command/admin/pod/ListPodsByCmd.java @@ -103,8 +103,8 @@ public class ListPodsByCmd extends BaseListCmd { @Override public void execute() { Pair, Integer> result = _mgr.searchForPods(this); - ListResponse response = new ListResponse(); - List podResponses = new ArrayList(); + ListResponse response = new ListResponse<>(); + List podResponses = new ArrayList<>(); for (Pod pod : result.first()) { PodResponse podResponse = _responseGenerator.createPodResponse(pod, showCapacities); podResponse.setObjectName("pod"); diff --git a/api/src/main/java/org/apache/cloudstack/api/command/admin/vlan/CreateVlanIpRangeCmd.java b/api/src/main/java/org/apache/cloudstack/api/command/admin/vlan/CreateVlanIpRangeCmd.java index c0ba99a8233..5cb384925e8 100644 --- a/api/src/main/java/org/apache/cloudstack/api/command/admin/vlan/CreateVlanIpRangeCmd.java +++ b/api/src/main/java/org/apache/cloudstack/api/command/admin/vlan/CreateVlanIpRangeCmd.java @@ -16,6 +16,8 @@ // under the License. package org.apache.cloudstack.api.command.admin.vlan; +import com.cloud.configuration.ConfigurationService; +import com.cloud.network.Network; import com.cloud.utils.net.NetUtils; import org.apache.cloudstack.api.APICommand; @@ -39,7 +41,6 @@ import com.cloud.exception.ResourceAllocationException; import com.cloud.exception.ResourceUnavailableException; import com.cloud.user.Account; -import java.util.Objects; @APICommand(name = "createVlanIpRange", description = "Creates a VLAN IP range.", responseObject = VlanIpRangeResponse.class, requestHasSensitiveInfo = false, responseHasSensitiveInfo = false) @@ -114,8 +115,8 @@ public class CreateVlanIpRangeCmd extends BaseCmd { @Parameter(name = ApiConstants.FOR_SYSTEM_VMS, type = CommandType.BOOLEAN, description = "true if IP range is set to system vms, false if not") private Boolean forSystemVms; - @Parameter(name = ApiConstants.FOR_NSX, type = CommandType.BOOLEAN, description = "true if the IP range is used for NSX resource", since = "4.20.0") - private boolean forNsx; + @Parameter(name = ApiConstants.PROVIDER, type = CommandType.STRING, description = "Provider name for which the IP range is reserved for", since = "4.21.0") + private String provider; ///////////////////////////////////////////////////// /////////////////// Accessors /////////////////////// @@ -157,12 +158,12 @@ public class CreateVlanIpRangeCmd extends BaseCmd { return startIp; } - public boolean isForNsx() { - return !Objects.isNull(forNsx) && forNsx; + public Network.Provider getProvider() { + return Network.Provider.getProvider(provider); } public String getVlan() { - if ((vlan == null || vlan.isEmpty()) && !isForNsx()) { + if ((vlan == null || vlan.isEmpty()) && !ConfigurationService.IsIpRangeForProvider(getProvider())) { vlan = "untagged"; } return vlan; diff --git a/api/src/main/java/org/apache/cloudstack/api/command/admin/vm/MigrateVMCmd.java b/api/src/main/java/org/apache/cloudstack/api/command/admin/vm/MigrateVMCmd.java index 8881a2bc354..0bc993ef1f7 100644 --- a/api/src/main/java/org/apache/cloudstack/api/command/admin/vm/MigrateVMCmd.java +++ b/api/src/main/java/org/apache/cloudstack/api/command/admin/vm/MigrateVMCmd.java @@ -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; diff --git a/api/src/main/java/org/apache/cloudstack/api/command/admin/vpc/CreateVPCOfferingCmd.java b/api/src/main/java/org/apache/cloudstack/api/command/admin/vpc/CreateVPCOfferingCmd.java index 73b4f5df196..84f4d451460 100644 --- a/api/src/main/java/org/apache/cloudstack/api/command/admin/vpc/CreateVPCOfferingCmd.java +++ b/api/src/main/java/org/apache/cloudstack/api/command/admin/vpc/CreateVPCOfferingCmd.java @@ -17,6 +17,7 @@ package org.apache.cloudstack.api.command.admin.vpc; import java.util.ArrayList; +import java.util.Arrays; import java.util.Collection; import java.util.HashMap; import java.util.Iterator; @@ -25,10 +26,12 @@ import java.util.List; import java.util.Map; import java.util.Set; import java.util.stream.Collectors; +import java.util.stream.Stream; import com.cloud.exception.InvalidParameterValueException; import com.cloud.network.Network; import com.cloud.network.VirtualRouterProvider; +import com.cloud.offering.NetworkOffering; import org.apache.cloudstack.api.response.DomainResponse; import org.apache.cloudstack.api.response.ZoneResponse; import org.apache.commons.collections.CollectionUtils; @@ -57,6 +60,7 @@ import static com.cloud.network.Network.Service.SourceNat; import static com.cloud.network.Network.Service.PortForwarding; import static com.cloud.network.Network.Service.NetworkACL; import static com.cloud.network.Network.Service.UserData; +import static com.cloud.network.Network.Service.Gateway; @APICommand(name = "createVPCOffering", description = "Creates VPC offering", responseObject = VpcOfferingResponse.class, requestHasSensitiveInfo = false, responseHasSensitiveInfo = false) @@ -112,12 +116,19 @@ public class CreateVPCOfferingCmd extends BaseAsyncCreateCmd { since = "4.13") private List zoneIds; + @Deprecated @Parameter(name = ApiConstants.FOR_NSX, type = CommandType.BOOLEAN, description = "true if network offering is meant to be used for NSX, false otherwise.", since = "4.20.0") private Boolean forNsx; + @Parameter(name = ApiConstants.PROVIDER, + type = CommandType.STRING, + description = "Name of the provider providing the service", + since = "4.21.0") + private String provider; + @Parameter(name = ApiConstants.NSX_SUPPORT_LB, type = CommandType.BOOLEAN, description = "true if network offering for NSX VPC offering supports Load balancer service.", @@ -158,20 +169,31 @@ public class CreateVPCOfferingCmd extends BaseAsyncCreateCmd { return StringUtils.isEmpty(displayText) ? vpcOfferingName : displayText; } + public boolean isExternalNetworkProvider() { + return Arrays.asList("NSX", "Netris").stream() + .anyMatch(s -> provider != null && s.equalsIgnoreCase(provider)); + } + public List getSupportedServices() { - if (!isForNsx() && CollectionUtils.isEmpty(supportedServices)) { + if (!isExternalNetworkProvider() && CollectionUtils.isEmpty(supportedServices)) { throw new InvalidParameterValueException("Supported services needs to be provided"); } - if (isForNsx()) { + if (isExternalNetworkProvider()) { supportedServices = new ArrayList<>(List.of( Dhcp.getName(), Dns.getName(), - StaticNat.getName(), - SourceNat.getName(), NetworkACL.getName(), - PortForwarding.getName(), UserData.getName() )); + if (NetworkOffering.NetworkMode.NATTED.name().equalsIgnoreCase(getNetworkMode())) { + supportedServices.addAll(Arrays.asList( + StaticNat.getName(), + SourceNat.getName(), + PortForwarding.getName())); + } + if (NetworkOffering.NetworkMode.ROUTED.name().equalsIgnoreCase(getNetworkMode())) { + supportedServices.add(Gateway.getName()); + } if (getNsxSupportsLbService()) { supportedServices.add(Lb.getName()); } @@ -179,8 +201,8 @@ public class CreateVPCOfferingCmd extends BaseAsyncCreateCmd { return supportedServices; } - public boolean isForNsx() { - return BooleanUtils.isTrue(forNsx); + public String getProvider() { + return provider; } public String getNetworkMode() { @@ -193,7 +215,7 @@ public class CreateVPCOfferingCmd extends BaseAsyncCreateCmd { public Map> getServiceProviders() { Map> serviceProviderMap = new HashMap<>(); - if (serviceProviderList != null && !serviceProviderList.isEmpty() && !isForNsx()) { + if (serviceProviderList != null && !serviceProviderList.isEmpty() && !isExternalNetworkProvider()) { Collection> servicesCollection = serviceProviderList.values(); Iterator> iter = servicesCollection.iterator(); while (iter.hasNext()) { @@ -213,16 +235,18 @@ public class CreateVPCOfferingCmd extends BaseAsyncCreateCmd { providerList.add(provider); serviceProviderMap.put(service, providerList); } - } else if (Boolean.TRUE.equals(forNsx)) { - getServiceProviderMapForNsx(serviceProviderMap); + } else if (isExternalNetworkProvider()) { + getServiceProviderMapForExternalProvider(serviceProviderMap, Network.Provider.getProvider(provider).getName()); } return serviceProviderMap; } - private void getServiceProviderMapForNsx(Map> serviceProviderMap) { - List unsupportedServices = List.of("Vpn", "BaremetalPxeService", "SecurityGroup", "Connectivity", - "Gateway", "Firewall"); + private void getServiceProviderMapForExternalProvider(Map> serviceProviderMap, String provider) { + List unsupportedServices = new ArrayList<>(List.of("Vpn", "BaremetalPxeService", "SecurityGroup", "Connectivity", "Firewall")); + if (NetworkOffering.NetworkMode.NATTED.name().equalsIgnoreCase(getNetworkMode())) { + unsupportedServices.add("Gateway"); + } List routerSupported = List.of("Dhcp", "Dns", "UserData"); List allServices = Network.Service.listAllServices().stream().map(Network.Service::getName).collect(Collectors.toList()); for (String service : allServices) { @@ -230,8 +254,10 @@ public class CreateVPCOfferingCmd extends BaseAsyncCreateCmd { continue; if (routerSupported.contains(service)) serviceProviderMap.put(service, List.of(VirtualRouterProvider.Type.VPCVirtualRouter.name())); - else - serviceProviderMap.put(service, List.of(Network.Provider.Nsx.getName())); + else if (NetworkOffering.NetworkMode.NATTED.name().equalsIgnoreCase(getNetworkMode()) || + Stream.of(NetworkACL.getName(), Gateway.getName()).anyMatch(s -> s.equalsIgnoreCase(service))) { + serviceProviderMap.put(service, List.of(provider)); + } } if (!getNsxSupportsLbService()) { serviceProviderMap.remove(Lb.getName()); diff --git a/api/src/main/java/org/apache/cloudstack/api/command/user/address/ListPublicIpAddressesCmd.java b/api/src/main/java/org/apache/cloudstack/api/command/user/address/ListPublicIpAddressesCmd.java index 5760ca3ba1c..357f0c83ed7 100644 --- a/api/src/main/java/org/apache/cloudstack/api/command/user/address/ListPublicIpAddressesCmd.java +++ b/api/src/main/java/org/apache/cloudstack/api/command/user/address/ListPublicIpAddressesCmd.java @@ -108,6 +108,9 @@ public class ListPublicIpAddressesCmd extends BaseListRetrieveOnlyResourceCountC @Parameter(name = ApiConstants.FOR_SYSTEM_VMS, type = CommandType.BOOLEAN, description = "true if range is dedicated for system VMs", since = "4.20.0") private Boolean forSystemVMs; + @Parameter(name = ApiConstants.FOR_PROVIDER, type = CommandType.BOOLEAN, description = "true if range is dedicated for external network provider", since = "4.21.0") + private Boolean forProvider; + ///////////////////////////////////////////////////// /////////////////// Accessors /////////////////////// ///////////////////////////////////////////////////// @@ -183,6 +186,10 @@ public class ListPublicIpAddressesCmd extends BaseListRetrieveOnlyResourceCountC return BooleanUtils.isTrue(forSystemVMs); } + public boolean isForProvider() { + return BooleanUtils.isTrue(forProvider); + } + ///////////////////////////////////////////////////// /////////////// API Implementation/////////////////// ///////////////////////////////////////////////////// diff --git a/api/src/main/java/org/apache/cloudstack/api/command/user/backup/DeleteBackupScheduleCmd.java b/api/src/main/java/org/apache/cloudstack/api/command/user/backup/DeleteBackupScheduleCmd.java index 548f4d67b23..dbfd21d5a56 100644 --- a/api/src/main/java/org/apache/cloudstack/api/command/user/backup/DeleteBackupScheduleCmd.java +++ b/api/src/main/java/org/apache/cloudstack/api/command/user/backup/DeleteBackupScheduleCmd.java @@ -43,7 +43,7 @@ import com.cloud.utils.exception.CloudRuntimeException; description = "Deletes the backup schedule of a VM", responseObject = SuccessResponse.class, since = "4.14.0", authorized = {RoleType.Admin, RoleType.ResourceAdmin, RoleType.DomainAdmin, RoleType.User}) -public class DeleteBackupScheduleCmd extends BaseCmd { +public class DeleteBackupScheduleCmd extends BaseCmd { @Inject private BackupManager backupManager; @@ -52,17 +52,13 @@ public class DeleteBackupScheduleCmd extends BaseCmd { //////////////// API parameters ///////////////////// ///////////////////////////////////////////////////// - @Parameter(name = ApiConstants.VIRTUAL_MACHINE_ID, - type = CommandType.UUID, - entityType = UserVmResponse.class, - description = "ID of the VM") + @Parameter(name = ApiConstants.VIRTUAL_MACHINE_ID, type = CommandType.UUID, entityType = UserVmResponse.class, + description = "ID of the VM from which all backup schedules will be deleted.") private Long vmId; - @Parameter(name = ApiConstants.ID, - type = CommandType.UUID, - entityType = BackupScheduleResponse.class, - description = "ID of the schedule", - since = "4.20.1") + @Parameter(name = ApiConstants.ID, type = CommandType.UUID, entityType = BackupScheduleResponse.class, + since = "4.20.1", description = "ID of the backup schedule to be deleted. It has precedence over the 'virtualmachineid' parameter, " + + "i.e., when the 'id' parameter is specified, the 'virtualmachineid' parameter will be ignored.") private Long id; ///////////////////////////////////////////////////// diff --git a/api/src/main/java/org/apache/cloudstack/api/command/user/config/ListCapabilitiesCmd.java b/api/src/main/java/org/apache/cloudstack/api/command/user/config/ListCapabilitiesCmd.java index 77a7a7fd8ea..e73bb97a21b 100644 --- a/api/src/main/java/org/apache/cloudstack/api/command/user/config/ListCapabilitiesCmd.java +++ b/api/src/main/java/org/apache/cloudstack/api/command/user/config/ListCapabilitiesCmd.java @@ -73,6 +73,8 @@ 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.setDynamicScalingEnabled((Boolean) capabilities.get(ApiConstants.DYNAMIC_SCALING_ENABLED)); response.setObjectName("capability"); response.setResponseName(getCommandName()); this.setResponseObject(response); diff --git a/api/src/main/java/org/apache/cloudstack/api/command/user/gpu/ListGpuCardsCmd.java b/api/src/main/java/org/apache/cloudstack/api/command/user/gpu/ListGpuCardsCmd.java new file mode 100644 index 00000000000..b035ecbe71b --- /dev/null +++ b/api/src/main/java/org/apache/cloudstack/api/command/user/gpu/ListGpuCardsCmd.java @@ -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.api.command.user.gpu; + +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.response.GpuCardResponse; +import org.apache.cloudstack.api.response.ListResponse; + +@APICommand(name = "listGpuCards", description = "Lists all available GPU cards", + responseObject = GpuCardResponse.class, requestHasSensitiveInfo = false, responseHasSensitiveInfo = false, + since = "4.21.0") +public class ListGpuCardsCmd extends BaseListCmd { + + /// ////////////////////////////////////////////////// + /// ///////////// API parameters ///////////////////// + /// ////////////////////////////////////////////////// + + @Parameter(name = ApiConstants.ID, type = CommandType.UUID, entityType = GpuCardResponse.class, + description = "ID of the GPU card") + private Long id; + + @Parameter(name = ApiConstants.VENDOR_NAME, type = CommandType.STRING, + description = "vendor name of the GPU card") + private String vendorName; + + @Parameter(name = ApiConstants.VENDOR_ID, type = CommandType.STRING, + description = "vendor ID of the GPU card") + private String vendorId; + + @Parameter(name = ApiConstants.DEVICE_ID, type = CommandType.STRING, + description = "device ID of the GPU card") + private String deviceId; + + @Parameter(name = ApiConstants.DEVICE_NAME, type = CommandType.STRING, + description = "device name of the GPU card") + private String deviceName; + + @Parameter(name = ApiConstants.ACTIVE_ONLY, type = CommandType.BOOLEAN, + description = "If true, only GPU cards which have a device will be listed. If false, all GPU cards will be listed.") + private Boolean activeOnly; + + /// ////////////////////////////////////////////////// + /// //////////////// Accessors /////////////////////// + /// ////////////////////////////////////////////////// + + public Long getId() { + return id; + } + + public String getVendorName() { + return vendorName; + } + + public String getVendorId() { + return vendorId; + } + + public String getDeviceId() { + return deviceId; + } + + public String getDeviceName() { + return deviceName; + } + + public boolean getActiveOnly() { + return Boolean.TRUE.equals(activeOnly); + } + + /// ////////////////////////////////////////////////// + /// //////////// API Implementation/////////////////// + /// ////////////////////////////////////////////////// + + @Override public void execute() { + ListResponse response = gpuService.listGpuCards(this); + response.setResponseName(getCommandName()); + setResponseObject(response); + } +} diff --git a/api/src/main/java/org/apache/cloudstack/api/command/user/gpu/ListGpuDevicesCmd.java b/api/src/main/java/org/apache/cloudstack/api/command/user/gpu/ListGpuDevicesCmd.java new file mode 100644 index 00000000000..19c92062851 --- /dev/null +++ b/api/src/main/java/org/apache/cloudstack/api/command/user/gpu/ListGpuDevicesCmd.java @@ -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.api.command.user.gpu; + +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.ResponseObject; +import org.apache.cloudstack.api.command.user.UserCmd; +import org.apache.cloudstack.api.response.GpuDeviceResponse; +import org.apache.cloudstack.api.response.ListResponse; +import org.apache.cloudstack.api.response.UserVmResponse; +import org.apache.cloudstack.context.CallContext; + +@APICommand(name = "listGpuDevices", description = "Lists all available GPU devices", + responseView = ResponseObject.ResponseView.Restricted, + responseObject = GpuDeviceResponse.class, requestHasSensitiveInfo = false, responseHasSensitiveInfo = false, + since = "4.21.0", authorized = {RoleType.Admin, RoleType.ResourceAdmin, RoleType.DomainAdmin, RoleType.User}) +public class ListGpuDevicesCmd extends BaseListCmd implements UserCmd { + + /// ////////////////////////////////////////////////// + /// ///////////// API parameters ///////////////////// + /// ////////////////////////////////////////////////// + + @Parameter(name = ApiConstants.VIRTUAL_MACHINE_ID, type = CommandType.UUID, entityType = UserVmResponse.class, + description = "the virtual machine ID to which the GPU device is allocated") + private Long vmId; + + /// ////////////////////////////////////////////////// + /// //////////////// Accessors /////////////////////// + /// ////////////////////////////////////////////////// + + public Long getVmId() { + return vmId; + } + + /// ////////////////////////////////////////////////// + /// //////////// API Implementation ////////////////// + /// ////////////////////////////////////////////////// + + @Override + public void execute() { + CallContext.current().setEventDetails("Listing GPU devices"); + ListResponse response = gpuService.listGpuDevices(this); + response.setResponseName(getCommandName()); + setResponseObject(response); + } + +} diff --git a/api/src/main/java/org/apache/cloudstack/api/command/user/gpu/ListVgpuProfilesCmd.java b/api/src/main/java/org/apache/cloudstack/api/command/user/gpu/ListVgpuProfilesCmd.java new file mode 100644 index 00000000000..85bf91d7aee --- /dev/null +++ b/api/src/main/java/org/apache/cloudstack/api/command/user/gpu/ListVgpuProfilesCmd.java @@ -0,0 +1,80 @@ +// Licensed to the Apache Software Foundation (ASF) under one +// or more contributor license agreements. See the NOTICE file +// distributed with this work for additional information +// regarding copyright ownership. The ASF licenses this file +// to you under the Apache License, Version 2.0 (the +// "License"); you may not use this file except in compliance +// with the License. You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, +// software distributed under the License is distributed on an +// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +// KIND, either express or implied. See the License for the +// specific language governing permissions and limitations +// under the License. +package org.apache.cloudstack.api.command.user.gpu; + +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.response.GpuCardResponse; +import org.apache.cloudstack.api.response.ListResponse; +import org.apache.cloudstack.api.response.VgpuProfileResponse; + +@APICommand(name = "listVgpuProfiles", description = "Lists all available vGPU profiles", + responseObject = VgpuProfileResponse.class, requestHasSensitiveInfo = false, responseHasSensitiveInfo = false, + since = "4.21.0") +public class ListVgpuProfilesCmd extends BaseListCmd { + + /// ////////////////////////////////////////////////// + /// ///////////// API parameters ///////////////////// + /// ////////////////////////////////////////////////// + + @Parameter(name = ApiConstants.ID, type = CommandType.UUID, entityType = VgpuProfileResponse.class, + description = "ID of the vGPU profile") + private Long id; + + @Parameter(name = ApiConstants.NAME, type = CommandType.STRING, description = "name of the vGPU profile") + private String name; + + @Parameter(name = ApiConstants.GPU_CARD_ID, type = CommandType.UUID, entityType = GpuCardResponse.class, + description = "the GPU card ID associated with this GPU device") + private Long cardId; + + @Parameter(name = ApiConstants.ACTIVE_ONLY, type = CommandType.BOOLEAN, + description = "If true, only vGPU profiles which have a device will be listed. If false, all vGPU profiles will be listed.") + private Boolean activeOnly; + + /// ////////////////////////////////////////////////// + /// //////////////// Accessors /////////////////////// + /// ////////////////////////////////////////////////// + + public Long getId() { + return id; + } + + public String getName() { + return name; + } + + public Long getCardId() { + return cardId; + } + + public boolean getActiveOnly() { + return Boolean.TRUE.equals(activeOnly); + } + + /// ////////////////////////////////////////////////// + /// //////////// API Implementation/////////////////// + /// ////////////////////////////////////////////////// + + @Override public void execute() { + ListResponse response = gpuService.listVgpuProfiles(this); + response.setResponseName(getCommandName()); + setResponseObject(response); + } +} diff --git a/api/src/main/java/org/apache/cloudstack/api/command/user/gui/theme/CreateGuiThemeCmd.java b/api/src/main/java/org/apache/cloudstack/api/command/user/gui/theme/CreateGuiThemeCmd.java new file mode 100644 index 00000000000..8566b413cc1 --- /dev/null +++ b/api/src/main/java/org/apache/cloudstack/api/command/user/gui/theme/CreateGuiThemeCmd.java @@ -0,0 +1,129 @@ +// Licensed to the Apache Software Foundation (ASF) under one +// or more contributor license agreements. See the NOTICE file +// distributed with this work for additional information +// regarding copyright ownership. The ASF licenses this file +// to you under the Apache License, Version 2.0 (the +// "License"); you may not use this file except in compliance +// with the License. You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, +// software distributed under the License is distributed on an +// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +// KIND, either express or implied. See the License for the +// specific language governing permissions and limitations +// under the License. +package org.apache.cloudstack.api.command.user.gui.theme; + +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.BaseCmd; +import org.apache.cloudstack.api.Parameter; +import org.apache.cloudstack.api.ServerApiException; +import org.apache.cloudstack.api.response.GuiThemeResponse; +import org.apache.cloudstack.context.CallContext; +import org.apache.cloudstack.gui.theme.GuiTheme; +import org.apache.cloudstack.gui.theme.GuiThemeJoin; +import org.apache.cloudstack.gui.theme.GuiThemeService; + +import javax.inject.Inject; + +@APICommand(name = "createGuiTheme", description = "Creates a customized GUI theme for a set of Common Names (fixed or wildcard), a set of domain UUIDs, and/or a set of " + + "account UUIDs.", responseObject = GuiThemeResponse.class, entityType = {GuiTheme.class}, requestHasSensitiveInfo = false, responseHasSensitiveInfo = false, + since = "4.21.0.0", authorized = {RoleType.Admin}) +public class CreateGuiThemeCmd extends BaseCmd { + + @Inject + GuiThemeService guiThemeService; + + @Parameter(name = ApiConstants.NAME, required = true, type = CommandType.STRING, length = 2048, description = "A name to identify the theme.") + private String name; + + @Parameter(name = ApiConstants.DESCRIPTION, type = CommandType.STRING, length = 4096, description = "A description for the theme.") + private String description; + + @Parameter(name = ApiConstants.CSS, type = CommandType.STRING, length = 65535, description = "The CSS to be retrieved and imported into the GUI " + + "when matching the theme access configurations.") + private String css; + + @Parameter(name = ApiConstants.JSON_CONFIGURATION, type = CommandType.STRING, length = 65535, description = "The JSON with the configurations to be " + + "retrieved and imported into the GUI when matching the theme access configurations.") + private String jsonConfiguration; + + @Parameter(name = ApiConstants.COMMON_NAMES, type = CommandType.STRING, length = 65535, description = "A set of Common Names (CN) (fixed or " + + "wildcard) separated by comma that can retrieve the theme; e.g.: *acme.com,acme2.com") + private String commonNames; + + @Parameter(name = ApiConstants.DOMAIN_IDS, type = CommandType.STRING, length = 65535, description = "A set of domain UUIDs (also known as ID for " + + "the end-user) separated by comma that can retrieve the theme.") + private String domainIds; + + @Parameter(name = ApiConstants.ACCOUNT_IDS, type = CommandType.STRING, length = 65535, description = "A set of account UUIDs (also known as ID for" + + " the end-user) separated by comma that can retrieve the theme.") + private String accountIds; + + @Parameter(name = ApiConstants.IS_PUBLIC, type = CommandType.BOOLEAN, description = "Defines whether a theme can be retrieved by anyone when only " + + "the `commonNames` is informed. If the `domainIds` or `accountIds` is informed, it is considered as `false`.") + private Boolean isPublic = true; + + @Parameter(name = ApiConstants.RECURSIVE_DOMAINS, type = CommandType.BOOLEAN, description = "Defines whether the subdomains of the informed domains are considered. Default " + + "value is false.") + private Boolean recursiveDomains = false; + + public String getName() { + return name; + } + + public String getDescription() { + return description; + } + + public String getCss() { + return css; + } + + public String getJsonConfiguration() { + return jsonConfiguration; + } + + public String getCommonNames() { + return commonNames; + } + + public String getDomainIds() { + return domainIds; + } + + public String getAccountIds() { + return accountIds; + } + + public Boolean getPublic() { + return isPublic; + } + + public Boolean getRecursiveDomains() { + return recursiveDomains; + } + + @Override + public void execute() { + GuiThemeJoin guiThemeJoin = guiThemeService.createGuiTheme(this); + + if (guiThemeJoin == null) { + throw new ServerApiException(ApiErrorCode.INTERNAL_ERROR, "Failed to create the GUI theme."); + } + + GuiThemeResponse response = _responseGenerator.createGuiThemeResponse(guiThemeJoin); + response.setResponseName(getCommandName()); + this.setResponseObject(response); + } + + @Override + public long getEntityOwnerId() { + return CallContext.current().getCallingAccountId(); + } +} diff --git a/api/src/main/java/org/apache/cloudstack/api/command/user/gui/theme/ListGuiThemesCmd.java b/api/src/main/java/org/apache/cloudstack/api/command/user/gui/theme/ListGuiThemesCmd.java new file mode 100644 index 00000000000..35a0a749aa9 --- /dev/null +++ b/api/src/main/java/org/apache/cloudstack/api/command/user/gui/theme/ListGuiThemesCmd.java @@ -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.api.command.user.gui.theme; + +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.response.AccountResponse; +import org.apache.cloudstack.api.response.DomainResponse; +import org.apache.cloudstack.api.response.GuiThemeResponse; +import org.apache.cloudstack.api.response.ListResponse; +import org.apache.cloudstack.gui.theme.GuiTheme; +import org.apache.cloudstack.gui.theme.GuiThemeService; + +import javax.inject.Inject; + +@APICommand(name = "listGuiThemes", description = "Lists GUI themes.", responseObject = GuiThemeResponse.class, entityType = {GuiTheme.class}, + since = "4.21.0.0", requestHasSensitiveInfo = false, responseHasSensitiveInfo = false, authorized = {RoleType.Admin, RoleType.User, RoleType.DomainAdmin, + RoleType.ResourceAdmin}) +public class ListGuiThemesCmd extends BaseListCmd { + + @Inject + GuiThemeService guiThemeService; + + @Parameter(name = ApiConstants.ID, type = CommandType.UUID, description = "The theme ID.", entityType = GuiThemeResponse.class) + private Long id; + + @Parameter(name = ApiConstants.NAME, type = CommandType.STRING, description = "The name of the theme.") + private String name; + + @Parameter(name = ApiConstants.COMMON_NAME, type = CommandType.STRING, description = "The internet Common Name (CN) to be filtered.") + private String commonName; + + @Parameter(name = ApiConstants.DOMAIN_ID, type = CommandType.UUID, entityType = DomainResponse.class, description = "The ID of the domain to be filtered.") + private Long domainId; + + @Parameter(name = ApiConstants.ACCOUNT_ID, type = CommandType.UUID, entityType = AccountResponse.class, description = "The ID of the account to be filtered.") + private Long accountId; + + @Parameter(name = ApiConstants.LIST_ALL, type = CommandType.BOOLEAN, description = "Whether to list all themes.") + private boolean listAll = false; + + @Parameter(name = ApiConstants.SHOW_REMOVED, type = CommandType.BOOLEAN, description = "Whether to list removed themes.") + private boolean showRemoved = false; + + @Parameter(name = ApiConstants.SHOW_PUBLIC, type = CommandType.BOOLEAN, description = "Whether to list public themes.") + private Boolean showPublic; + + @Parameter(name = ApiConstants.LIST_ONLY_DEFAULT_THEME, type = CommandType.BOOLEAN, description = "Whether to only list the default theme.") + private boolean listOnlyDefaultTheme = false; + + public Long getId() { + return id; + } + + public String getName() { + return name; + } + + public String getCommonName() { + return commonName; + } + + public Long getDomainId() { + return domainId; + } + + public Long getAccountId() { + return accountId; + } + + public boolean getListAll() { + return listAll; + } + + public boolean getShowRemoved() { + return showRemoved; + } + + public Boolean getShowPublic() { + return showPublic; + } + + public boolean getListOnlyDefaultTheme() { + return listOnlyDefaultTheme; + } + + @Override + public void execute() { + ListResponse response = guiThemeService.listGuiThemes(this); + response.setResponseName(getCommandName()); + this.setResponseObject(response); + } +} diff --git a/api/src/main/java/org/apache/cloudstack/api/command/user/gui/theme/RemoveGuiThemeCmd.java b/api/src/main/java/org/apache/cloudstack/api/command/user/gui/theme/RemoveGuiThemeCmd.java new file mode 100644 index 00000000000..64164838eba --- /dev/null +++ b/api/src/main/java/org/apache/cloudstack/api/command/user/gui/theme/RemoveGuiThemeCmd.java @@ -0,0 +1,60 @@ +// Licensed to the Apache Software Foundation (ASF) under one +// or more contributor license agreements. See the NOTICE file +// distributed with this work for additional information +// regarding copyright ownership. The ASF licenses this file +// to you under the Apache License, Version 2.0 (the +// "License"); you may not use this file except in compliance +// with the License. You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, +// software distributed under the License is distributed on an +// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +// KIND, either express or implied. See the License for the +// specific language governing permissions and limitations +// under the License. +package org.apache.cloudstack.api.command.user.gui.theme; + +import org.apache.cloudstack.acl.RoleType; +import org.apache.cloudstack.api.APICommand; +import org.apache.cloudstack.api.ApiConstants; +import org.apache.cloudstack.api.BaseCmd; +import org.apache.cloudstack.api.Parameter; +import org.apache.cloudstack.api.response.GuiThemeResponse; +import org.apache.cloudstack.api.response.SuccessResponse; +import org.apache.cloudstack.context.CallContext; +import org.apache.cloudstack.gui.theme.GuiTheme; +import org.apache.cloudstack.gui.theme.GuiThemeService; + +import javax.inject.Inject; + +@APICommand(name = "removeGuiTheme", description = "Removes an existing GUI theme.", responseObject = GuiThemeResponse.class, entityType = {GuiTheme.class}, + since = "4.21.0.0", requestHasSensitiveInfo = false, responseHasSensitiveInfo = false, authorized = {RoleType.Admin}) +public class RemoveGuiThemeCmd extends BaseCmd { + + @Inject + GuiThemeService guiThemeService; + + @Parameter(name = ApiConstants.ID, type = CommandType.UUID, entityType = GuiThemeResponse.class, required = true, + description = "The unique identifier of the GUI theme to be removed.") + private Long id; + + public Long getId() { + return id; + } + + @Override + public void execute() { + guiThemeService.removeGuiTheme(this); + final SuccessResponse response = new SuccessResponse(); + response.setResponseName(getCommandName()); + response.setSuccess(true); + setResponseObject(response); + } + + @Override + public long getEntityOwnerId() { + return CallContext.current().getCallingAccountId(); + } +} diff --git a/api/src/main/java/org/apache/cloudstack/api/command/user/gui/theme/UpdateGuiThemeCmd.java b/api/src/main/java/org/apache/cloudstack/api/command/user/gui/theme/UpdateGuiThemeCmd.java new file mode 100644 index 00000000000..daef2235ce8 --- /dev/null +++ b/api/src/main/java/org/apache/cloudstack/api/command/user/gui/theme/UpdateGuiThemeCmd.java @@ -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.api.command.user.gui.theme; + +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.BaseCmd; +import org.apache.cloudstack.api.Parameter; +import org.apache.cloudstack.api.ServerApiException; +import org.apache.cloudstack.api.response.GuiThemeResponse; +import org.apache.cloudstack.context.CallContext; +import org.apache.cloudstack.gui.theme.GuiTheme; +import org.apache.cloudstack.gui.theme.GuiThemeJoin; +import org.apache.cloudstack.gui.theme.GuiThemeService; + +import javax.inject.Inject; + + +@APICommand(name = "updateGuiTheme", description = "Updates an existing GUI theme.", responseObject = GuiThemeResponse.class, entityType = {GuiTheme.class}, + since = "4.21.0.0", requestHasSensitiveInfo = false, responseHasSensitiveInfo = false, authorized = {RoleType.Admin}) +public class UpdateGuiThemeCmd extends BaseCmd { + + @Inject + GuiThemeService guiThemeService; + + @Parameter(name = ApiConstants.ID, required = true, type = CommandType.UUID, entityType = GuiThemeResponse.class, description = "The ID of the theme to be updated.") + private Long id; + + @Parameter(name = ApiConstants.NAME, type = CommandType.STRING, length = 2048, description = "A name to identify the theme.") + private String name; + + @Parameter(name = ApiConstants.DESCRIPTION, type = CommandType.STRING, length = 4096, description = "A description for the theme.") + private String description; + + @Parameter(name = ApiConstants.CSS, type = CommandType.STRING, length = 65535, description = "The CSS to be retrieved and imported into the GUI " + + "when matching the theme access configurations.") + private String css; + + @Parameter(name = ApiConstants.JSON_CONFIGURATION, type = CommandType.STRING, length = 65535, description = "The JSON with the configurations to be " + + "retrieved and imported into the GUI when matching the theme access configurations.") + private String jsonConfiguration; + + @Parameter(name = ApiConstants.COMMON_NAMES, type = CommandType.STRING, length = 65535, description = "A set of Common Names (CN) (fixed or " + + "wildcard) separated by comma that can retrieve the theme; e.g.: *acme.com,acme2.com") + private String commonNames; + + @Parameter(name = ApiConstants.DOMAIN_IDS, type = CommandType.STRING, length = 65535, description = "A set of domain UUIDs (also known as ID for " + + "the end-user) separated by comma that can retrieve the theme.") + private String domainIds; + + @Parameter(name = ApiConstants.RECURSIVE_DOMAINS, type = CommandType.BOOLEAN, description = "Defines whether the subdomains of the informed domains are considered. Default " + + "value is false.") + private Boolean recursiveDomains = false; + + @Parameter(name = ApiConstants.ACCOUNT_IDS, type = CommandType.STRING, length = 65535, description = "A set of account UUIDs (also known as ID for" + + " the end-user) separated by comma that can retrieve the theme.") + private String accountIds; + + @Parameter(name = ApiConstants.IS_PUBLIC, type = CommandType.BOOLEAN, description = "Defines whether a theme can be retrieved by anyone when only " + + "the `commonNames` is informed. If the `domainIds` or `accountIds` is informed, it is considered as `false`.") + private Boolean isPublic = true; + + public Long getId() { + return id; + } + + public String getName() { + return name; + } + + public String getDescription() { + return description; + } + + public String getCss() { + return css; + } + + public String getJsonConfiguration() { + return jsonConfiguration; + } + + public String getCommonNames() { + return commonNames; + } + + public String getDomainIds() { + return domainIds; + } + + public String getAccountIds() { + return accountIds; + } + + public Boolean getRecursiveDomains() { + return recursiveDomains; + } + + public Boolean getIsPublic() { + return isPublic; + } + + @Override + public void execute() { + GuiThemeJoin guiThemeJoin = guiThemeService.updateGuiTheme(this); + + if (guiThemeJoin == null) { + throw new ServerApiException(ApiErrorCode.INTERNAL_ERROR, "Failed to update the GUI theme."); + } + + GuiThemeResponse response = _responseGenerator.createGuiThemeResponse(guiThemeJoin); + response.setResponseName(getCommandName()); + this.setResponseObject(response); + } + + @Override + public long getEntityOwnerId() { + return CallContext.current().getCallingAccountId(); + } +} diff --git a/api/src/main/java/org/apache/cloudstack/api/command/user/offering/ListServiceOfferingsCmd.java b/api/src/main/java/org/apache/cloudstack/api/command/user/offering/ListServiceOfferingsCmd.java index 1b3f531e370..3b693fe57b7 100644 --- a/api/src/main/java/org/apache/cloudstack/api/command/user/offering/ListServiceOfferingsCmd.java +++ b/api/src/main/java/org/apache/cloudstack/api/command/user/offering/ListServiceOfferingsCmd.java @@ -26,6 +26,7 @@ import org.apache.cloudstack.api.response.ListResponse; import org.apache.cloudstack.api.response.ServiceOfferingResponse; import org.apache.cloudstack.api.response.TemplateResponse; import org.apache.cloudstack.api.response.UserVmResponse; +import org.apache.cloudstack.api.response.VgpuProfileResponse; import org.apache.cloudstack.api.response.ZoneResponse; import org.apache.commons.lang3.EnumUtils; import org.apache.commons.lang3.StringUtils; @@ -110,6 +111,19 @@ public class ListServiceOfferingsCmd extends BaseListProjectAndAccountResourcesC since = "4.20.0") private Long templateId; + @Parameter(name = ApiConstants.VGPU_PROFILE_ID, + type = CommandType.UUID, + entityType = VgpuProfileResponse.class, + description = "The ID of the vGPU profile that listed offerings must support", + since = "4.21.0") + private Long vgpuProfileId; + + @Parameter(name = ApiConstants.GPU_ENABLED, + type = CommandType.BOOLEAN, + description = "Flag to indicate if the service offering supports GPU. If set to true, only service offerings that support GPU will be returned.", + since = "4.21.0") + private Boolean gpuEnabled; + ///////////////////////////////////////////////////// /////////////////// Accessors /////////////////////// ///////////////////////////////////////////////////// @@ -171,6 +185,14 @@ public class ListServiceOfferingsCmd extends BaseListProjectAndAccountResourcesC return templateId; } + public Long getVgpuProfileId() { + return vgpuProfileId; + } + + public Boolean getGpuEnabled() { + return gpuEnabled; + } + ///////////////////////////////////////////////////// /////////////// API Implementation/////////////////// ///////////////////////////////////////////////////// diff --git a/api/src/main/java/org/apache/cloudstack/api/command/user/snapshot/CreateSnapshotFromVMSnapshotCmd.java b/api/src/main/java/org/apache/cloudstack/api/command/user/snapshot/CreateSnapshotFromVMSnapshotCmd.java index cdd908dfb87..75089cece8e 100644 --- a/api/src/main/java/org/apache/cloudstack/api/command/user/snapshot/CreateSnapshotFromVMSnapshotCmd.java +++ b/api/src/main/java/org/apache/cloudstack/api/command/user/snapshot/CreateSnapshotFromVMSnapshotCmd.java @@ -153,7 +153,7 @@ public class CreateSnapshotFromVMSnapshotCmd extends BaseAsyncCreateCmd { @Override public void create() throws ResourceAllocationException { - Snapshot snapshot = this._volumeService.allocSnapshotForVm(getVmId(), getVolumeId(), getSnapshotName()); + Snapshot snapshot = this._volumeService.allocSnapshotForVm(getVmId(), getVolumeId(), getSnapshotName(), getVMSnapshotId()); if (snapshot != null) { this.setEntityId(snapshot.getId()); this.setEntityUuid(snapshot.getUuid()); diff --git a/api/src/main/java/org/apache/cloudstack/api/command/user/template/CreateTemplateCmd.java b/api/src/main/java/org/apache/cloudstack/api/command/user/template/CreateTemplateCmd.java index 0a7bf291843..5f09ac6698d 100644 --- a/api/src/main/java/org/apache/cloudstack/api/command/user/template/CreateTemplateCmd.java +++ b/api/src/main/java/org/apache/cloudstack/api/command/user/template/CreateTemplateCmd.java @@ -20,6 +20,7 @@ import java.util.Collection; import java.util.List; import java.util.Map; +import com.cloud.cpu.CPU; import org.apache.cloudstack.acl.SecurityChecker; import org.apache.cloudstack.api.APICommand; import org.apache.cloudstack.api.ApiCommandResourceType; @@ -148,6 +149,11 @@ public class CreateTemplateCmd extends BaseAsyncCreateCmd implements UserCmd { since = "4.19.0") private String accountName; + @Parameter(name = ApiConstants.ARCH, type = CommandType.STRING, + description = "the CPU arch of the template. Valid options are: x86_64, aarch64. Defaults to x86_64", + since = "4.20.2") + private String arch; + // /////////////////////////////////////////////////// // ///////////////// Accessors /////////////////////// // /////////////////////////////////////////////////// @@ -234,6 +240,10 @@ public class CreateTemplateCmd extends BaseAsyncCreateCmd implements UserCmd { return accountName; } + public CPU.CPUArch getArch() { + return CPU.CPUArch.fromType(arch); + } + // /////////////////////////////////////////////////// // ///////////// API Implementation/////////////////// // /////////////////////////////////////////////////// diff --git a/api/src/main/java/org/apache/cloudstack/api/command/user/template/ListTemplatesCmd.java b/api/src/main/java/org/apache/cloudstack/api/command/user/template/ListTemplatesCmd.java index 585114e07ad..4727e395c41 100644 --- a/api/src/main/java/org/apache/cloudstack/api/command/user/template/ListTemplatesCmd.java +++ b/api/src/main/java/org/apache/cloudstack/api/command/user/template/ListTemplatesCmd.java @@ -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; diff --git a/api/src/main/java/org/apache/cloudstack/api/command/user/template/RegisterTemplateCmd.java b/api/src/main/java/org/apache/cloudstack/api/command/user/template/RegisterTemplateCmd.java index 6ea149fd90d..5d5cab219c1 100644 --- a/api/src/main/java/org/apache/cloudstack/api/command/user/template/RegisterTemplateCmd.java +++ b/api/src/main/java/org/apache/cloudstack/api/command/user/template/RegisterTemplateCmd.java @@ -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 getExternalDetails() { + return convertExternalDetailsToMap(externalDetails); + } + ///////////////////////////////////////////////////// /////////////// API Implementation/////////////////// ///////////////////////////////////////////////////// diff --git a/api/src/main/java/org/apache/cloudstack/api/command/user/userdata/RegisterUserDataCmd.java b/api/src/main/java/org/apache/cloudstack/api/command/user/userdata/RegisterUserDataCmd.java index fabf8827796..185c9c05b1c 100644 --- a/api/src/main/java/org/apache/cloudstack/api/command/user/userdata/RegisterUserDataCmd.java +++ b/api/src/main/java/org/apache/cloudstack/api/command/user/userdata/RegisterUserDataCmd.java @@ -33,7 +33,7 @@ import com.cloud.exception.ResourceUnavailableException; import com.cloud.user.UserData; @APICommand(name = "registerUserData", - description = "Register a new userdata.", + description = "Register a new User Data.", since = "4.18", responseObject = SuccessResponse.class, requestHasSensitiveInfo = false, @@ -49,7 +49,6 @@ public class RegisterUserDataCmd extends BaseRegisterUserDataCmd { @Parameter(name = ApiConstants.USER_DATA, type = CommandType.STRING, required = true, description = "User data content", length = 1048576) protected String userData; - public String getUserData() { return userData; } diff --git a/api/src/main/java/org/apache/cloudstack/api/command/user/vm/DeployVMCmd.java b/api/src/main/java/org/apache/cloudstack/api/command/user/vm/DeployVMCmd.java index bf6e41e6d59..afd23cfd871 100644 --- a/api/src/main/java/org/apache/cloudstack/api/command/user/vm/DeployVMCmd.java +++ b/api/src/main/java/org/apache/cloudstack/api/command/user/vm/DeployVMCmd.java @@ -16,24 +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.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; @@ -55,9 +50,11 @@ import org.apache.cloudstack.api.response.NetworkResponse; import org.apache.cloudstack.api.response.ProjectResponse; import org.apache.cloudstack.api.response.SecurityGroupResponse; import org.apache.cloudstack.api.response.ServiceOfferingResponse; +import org.apache.cloudstack.api.response.SnapshotResponse; import org.apache.cloudstack.api.response.TemplateResponse; import org.apache.cloudstack.api.response.UserDataResponse; import org.apache.cloudstack.api.response.UserVmResponse; +import org.apache.cloudstack.api.response.VolumeResponse; import org.apache.cloudstack.api.response.ZoneResponse; import org.apache.cloudstack.context.CallContext; import org.apache.cloudstack.vm.lease.VMLeaseManager; @@ -67,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) @@ -95,7 +102,7 @@ public class DeployVMCmd extends BaseAsyncCreateCustomIdCmd implements SecurityG private Long serviceOfferingId; @ACL - @Parameter(name = ApiConstants.TEMPLATE_ID, type = CommandType.UUID, entityType = TemplateResponse.class, required = true, description = "the ID of the template for the virtual machine") + @Parameter(name = ApiConstants.TEMPLATE_ID, type = CommandType.UUID, entityType = TemplateResponse.class, description = "the ID of the template for the virtual machine") private Long templateId; @Parameter(name = ApiConstants.NAME, type = CommandType.STRING, description = "host name for the virtual machine", validations = {ApiArgValidator.RFCComplianceDomainName}) @@ -286,6 +293,18 @@ public class DeployVMCmd extends BaseAsyncCreateCustomIdCmd implements SecurityG description = "Lease expiry action, valid values are STOP and DESTROY") private String leaseExpiryAction; + @Parameter(name = ApiConstants.VOLUME_ID, type = CommandType.UUID, entityType = VolumeResponse.class, since = "4.21") + private Long volumeId; + + @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 /////////////////////// ///////////////////////////////////////////////////// @@ -359,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 getExternalDetails() { + return convertExternalDetailsToMap(externalDetails); + } public ApiConstants.BootMode getBootMode() { if (StringUtils.isNotBlank(bootMode)) { @@ -744,6 +770,18 @@ public class DeployVMCmd extends BaseAsyncCreateCustomIdCmd implements SecurityG } return null; } + + public Long getVolumeId() { + return volumeId; + } + + public Long getSnapshotId() { + return snapshotId; + } + + public boolean isVolumeOrSnapshotProvided() { + return volumeId != null || snapshotId != null; + } ///////////////////////////////////////////////////// /////////////// API Implementation/////////////////// ///////////////////////////////////////////////////// @@ -840,6 +878,10 @@ public class DeployVMCmd extends BaseAsyncCreateCustomIdCmd implements SecurityG @Override public void create() throws ResourceAllocationException { + if (Stream.of(templateId, snapshotId, volumeId).filter(Objects::nonNull).count() != 1) { + throw new CloudRuntimeException("Please provide only one of the following parameters - template ID, volume ID or snapshot ID"); + } + try { UserVm vm = _userVmService.createVirtualMachine(this); diff --git a/api/src/main/java/org/apache/cloudstack/api/command/user/vm/ListVMsCmd.java b/api/src/main/java/org/apache/cloudstack/api/command/user/vm/ListVMsCmd.java index 9fc4258c6d9..a561b6fd24f 100644 --- a/api/src/main/java/org/apache/cloudstack/api/command/user/vm/ListVMsCmd.java +++ b/api/src/main/java/org/apache/cloudstack/api/command/user/vm/ListVMsCmd.java @@ -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,17 @@ public class ListVMsCmd extends BaseListRetrieveOnlyResourceCountCmd implements since = "4.21.0") private Boolean onlyLeasedInstances = false; + @Parameter(name = ApiConstants.GPU_ENABLED, + type = CommandType.BOOLEAN, + description = "Flag to indicate if the VMs should be filtered by GPU support. If set to true, only VMs that support GPU will be returned.", + since = "4.21.0") + private Boolean gpuEnabled; + + @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 +330,14 @@ public class ListVMsCmd extends BaseListRetrieveOnlyResourceCountCmd implements return BooleanUtils.toBoolean(onlyLeasedInstances); } + public Boolean getGpuEnabled() { + return gpuEnabled; + } + + public Long getExtensionId() { + return extensionId; + } + ///////////////////////////////////////////////////// /////////////// API Implementation/////////////////// ///////////////////////////////////////////////////// diff --git a/api/src/main/java/org/apache/cloudstack/api/command/user/vpc/CreateStaticRouteCmd.java b/api/src/main/java/org/apache/cloudstack/api/command/user/vpc/CreateStaticRouteCmd.java index b28c02cb800..c4b5e159b5c 100644 --- a/api/src/main/java/org/apache/cloudstack/api/command/user/vpc/CreateStaticRouteCmd.java +++ b/api/src/main/java/org/apache/cloudstack/api/command/user/vpc/CreateStaticRouteCmd.java @@ -27,6 +27,7 @@ import org.apache.cloudstack.api.Parameter; import org.apache.cloudstack.api.ServerApiException; import org.apache.cloudstack.api.response.PrivateGatewayResponse; import org.apache.cloudstack.api.response.StaticRouteResponse; +import org.apache.cloudstack.api.response.VpcResponse; import org.apache.cloudstack.context.CallContext; import com.cloud.event.EventTypes; @@ -45,20 +46,40 @@ public class CreateStaticRouteCmd extends BaseAsyncCreateCmd { @Parameter(name = ApiConstants.GATEWAY_ID, type = CommandType.UUID, entityType = PrivateGatewayResponse.class, - required = true, - description = "the gateway id we are creating static route for") + description = "the gateway id we are creating static route for. Mutually exclusive with the nexthop parameter") private Long gatewayId; + @Parameter(name = ApiConstants.VPC_ID, + type = CommandType.UUID, + entityType = VpcResponse.class, + description = "the vpc id for which the static route is created. This is required for nexthop parameter", + since = "4.21.0") + private Long vpcId; + + @Parameter(name = ApiConstants.NEXT_HOP, + type = CommandType.STRING, + description = "the next hop of static route. Mutually exclusive with the gatewayid parameter", + since = "4.21.0") + private String nextHop; + @Parameter(name = ApiConstants.CIDR, required = true, type = CommandType.STRING, description = "static route cidr") private String cidr; ///////////////////////////////////////////////////// /////////////////// Accessors /////////////////////// ///////////////////////////////////////////////////// - public long getGatewayId() { + public Long getGatewayId() { return gatewayId; } + public Long getVpcId() { + return vpcId; + } + + public String getNextHop() { + return nextHop; + } + public String getCidr() { return cidr; } @@ -69,7 +90,7 @@ public class CreateStaticRouteCmd extends BaseAsyncCreateCmd { @Override public void create() throws ResourceAllocationException { try { - StaticRoute result = _vpcService.createStaticRoute(getGatewayId(), getCidr()); + StaticRoute result = _vpcService.createStaticRoute(getGatewayId(), getVpcId(), getNextHop(), getCidr()); setEntityId(result.getId()); setEntityUuid(result.getUuid()); } catch (NetworkRuleConflictException ex) { @@ -114,11 +135,8 @@ public class CreateStaticRouteCmd extends BaseAsyncCreateCmd { @Override public long getEntityOwnerId() { - VpcGateway gateway = _entityMgr.findById(VpcGateway.class, gatewayId); - if (gateway == null) { - throw new InvalidParameterValueException("Invalid gateway id is specified"); - } - return _entityMgr.findById(Vpc.class, gateway.getVpcId()).getAccountId(); + Long vpcId = getSyncObjId(); + return _entityMgr.findById(Vpc.class, vpcId).getAccountId(); } @Override @@ -128,11 +146,20 @@ public class CreateStaticRouteCmd extends BaseAsyncCreateCmd { @Override public Long getSyncObjId() { - VpcGateway gateway = _entityMgr.findById(VpcGateway.class, gatewayId); - if (gateway == null) { - throw new InvalidParameterValueException("Invalid id is specified for the gateway"); + if (gatewayId != null) { + VpcGateway gateway = _entityMgr.findById(VpcGateway.class, gatewayId); + if (gateway == null) { + throw new InvalidParameterValueException("Invalid id is specified for the gateway"); + } + return gateway.getVpcId(); + } else if (vpcId != null) { + Vpc vpc = _entityMgr.findById(Vpc.class, vpcId); + if (vpc == null) { + throw new InvalidParameterValueException("Invalid vpc id is specified"); + } + return vpc.getId(); } - return gateway.getVpcId(); + throw new InvalidParameterValueException("One of vpcId or gatewayId must be specified"); } @Override diff --git a/api/src/main/java/org/apache/cloudstack/api/command/user/vpn/CreateVpnGatewayCmd.java b/api/src/main/java/org/apache/cloudstack/api/command/user/vpn/CreateVpnGatewayCmd.java index 6f31176c4ff..6c3f57b3dfa 100644 --- a/api/src/main/java/org/apache/cloudstack/api/command/user/vpn/CreateVpnGatewayCmd.java +++ b/api/src/main/java/org/apache/cloudstack/api/command/user/vpn/CreateVpnGatewayCmd.java @@ -28,6 +28,7 @@ import org.apache.cloudstack.api.BaseAsyncCmd; import org.apache.cloudstack.api.BaseAsyncCreateCmd; import org.apache.cloudstack.api.Parameter; import org.apache.cloudstack.api.ServerApiException; +import org.apache.cloudstack.api.response.IPAddressResponse; import org.apache.cloudstack.api.response.Site2SiteVpnGatewayResponse; import org.apache.cloudstack.api.response.VpcResponse; import org.apache.cloudstack.context.CallContext; @@ -44,9 +45,16 @@ public class CreateVpnGatewayCmd extends BaseAsyncCreateCmd { type = CommandType.UUID, entityType = VpcResponse.class, required = true, - description = "public ip address id of the vpn gateway") + description = "id of the vpc") private Long vpcId; + @Parameter(name = ApiConstants.IP_ADDRESS_ID, + type = CommandType.UUID, + entityType = IPAddressResponse.class, + description = "the public IP address ID for which VPN gateway is being enabled. By default the source NAT IP or router IP will be used.", + since = "4.21.0") + private Long ipAddressId; + @Parameter(name = ApiConstants.FOR_DISPLAY, type = CommandType.BOOLEAN, description = "an optional field, whether to the display the vpn to the end user or not", since = "4.4", authorized = {RoleType.Admin}) private Boolean display; @@ -58,6 +66,10 @@ public class CreateVpnGatewayCmd extends BaseAsyncCreateCmd { return vpcId; } + public Long getIpAddressId() { + return ipAddressId; + } + @Deprecated public Boolean getDisplay() { return display; diff --git a/api/src/main/java/org/apache/cloudstack/api/command/user/zone/ListZonesCmd.java b/api/src/main/java/org/apache/cloudstack/api/command/user/zone/ListZonesCmd.java index a5e26f30dfb..36ce23cbc6e 100644 --- a/api/src/main/java/org/apache/cloudstack/api/command/user/zone/ListZonesCmd.java +++ b/api/src/main/java/org/apache/cloudstack/api/command/user/zone/ListZonesCmd.java @@ -34,8 +34,6 @@ import org.apache.cloudstack.api.response.ZoneResponse; requestHasSensitiveInfo = false, responseHasSensitiveInfo = false) public class ListZonesCmd extends BaseListCmd implements UserCmd { - private static final String s_name = "listzonesresponse"; - ///////////////////////////////////////////////////// //////////////// API parameters ///////////////////// ///////////////////////////////////////////////////// @@ -130,11 +128,6 @@ public class ListZonesCmd extends BaseListCmd implements UserCmd { /////////////// API Implementation/////////////////// ///////////////////////////////////////////////////// - @Override - public String getCommandName() { - return s_name; - } - @Override public void execute() { ListResponse response = _queryService.listDataCenters(this); diff --git a/api/src/main/java/org/apache/cloudstack/api/response/AccountResponse.java b/api/src/main/java/org/apache/cloudstack/api/response/AccountResponse.java index aaad7f985fc..3c99e2cbec6 100644 --- a/api/src/main/java/org/apache/cloudstack/api/response/AccountResponse.java +++ b/api/src/main/java/org/apache/cloudstack/api/response/AccountResponse.java @@ -231,6 +231,18 @@ public class AccountResponse extends BaseResponse implements ResourceLimitAndCou @Param(description = "the total memory (in MB) available to be created for this account", since = "4.2.0") private String memoryAvailable; + @SerializedName("gpulimit") + @Param(description = "the total number of gpus the account can own", since = "4.21.0") + private String gpuLimit; + + @SerializedName("gputotal") + @Param(description = "the total number of gpus owned by account", since = "4.21.0") + private Long gpuTotal; + + @SerializedName("gpuavailable") + @Param(description = "the total number of gpus available to be created for this account", since = "4.21.0") + private String gpuAvailable; + @SerializedName("primarystoragelimit") @Param(description = "the total primary storage space (in GiB) the account can own", since = "4.2.0") private String primaryStorageLimit; @@ -489,6 +501,21 @@ public class AccountResponse extends BaseResponse implements ResourceLimitAndCou this.vmRunning = vmRunning; } + @Override + public void setGpuLimit(String gpuLimit) { + this.gpuLimit = gpuLimit; + } + + @Override + public void setGpuTotal(Long gpuTotal) { + this.gpuTotal = gpuTotal; + } + + @Override + public void setGpuAvailable(String gpuAvailable) { + this.gpuAvailable = gpuAvailable; + } + public void setState(String state) { this.state = state; } diff --git a/api/src/main/java/org/apache/cloudstack/api/response/AsyncJobResponse.java b/api/src/main/java/org/apache/cloudstack/api/response/AsyncJobResponse.java index 5b47a7a06e4..edbc94bc498 100644 --- a/api/src/main/java/org/apache/cloudstack/api/response/AsyncJobResponse.java +++ b/api/src/main/java/org/apache/cloudstack/api/response/AsyncJobResponse.java @@ -55,10 +55,6 @@ public class AsyncJobResponse extends BaseResponse { @Param(description = "the async command executed") private String cmd; - @SerializedName("jobstatus") - @Param(description = "the current job status-should be 0 for PENDING") - private Integer jobStatus; - @SerializedName("jobprocstatus") @Param(description = "the progress information of the PENDING job") private Integer jobProcStatus; @@ -123,11 +119,6 @@ public class AsyncJobResponse extends BaseResponse { this.cmd = cmd; } - @Override - public void setJobStatus(Integer jobStatus) { - this.jobStatus = jobStatus; - } - public void setJobProcStatus(Integer jobProcStatus) { this.jobProcStatus = jobProcStatus; } diff --git a/api/src/main/java/org/apache/cloudstack/api/response/BackupScheduleResponse.java b/api/src/main/java/org/apache/cloudstack/api/response/BackupScheduleResponse.java index d7c6f96add5..9584223563f 100644 --- a/api/src/main/java/org/apache/cloudstack/api/response/BackupScheduleResponse.java +++ b/api/src/main/java/org/apache/cloudstack/api/response/BackupScheduleResponse.java @@ -28,6 +28,9 @@ import com.google.gson.annotations.SerializedName; @EntityReference(value = BackupSchedule.class) public class BackupScheduleResponse extends BaseResponse { + @SerializedName(ApiConstants.ID) + @Param(description = "ID of the backup schedule.", since = "4.21.0") + private String id; @SerializedName(ApiConstants.VIRTUAL_MACHINE_NAME) @Param(description = "name of the VM") @@ -51,7 +54,11 @@ public class BackupScheduleResponse extends BaseResponse { @SerializedName(ApiConstants.MAX_BACKUPS) @Param(description = "maximum number of backups retained") - private Integer maxBakups; + private Integer maxBackups; + + public void setId(String id) { + this.id = id; + } public String getVmName() { return vmName; @@ -93,7 +100,7 @@ public class BackupScheduleResponse extends BaseResponse { this.timezone = timezone; } - public void setMaxBakups(Integer maxBakups) { - this.maxBakups = maxBakups; + public void setMaxBackups(Integer maxBackups) { + this.maxBackups = maxBackups; } } diff --git a/api/src/main/java/org/apache/cloudstack/api/response/CapabilitiesResponse.java b/api/src/main/java/org/apache/cloudstack/api/response/CapabilitiesResponse.java index 74dbfa15a43..eb0daf75148 100644 --- a/api/src/main/java/org/apache/cloudstack/api/response/CapabilitiesResponse.java +++ b/api/src/main/java/org/apache/cloudstack/api/response/CapabilitiesResponse.java @@ -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,14 @@ 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; + + @SerializedName(ApiConstants.DYNAMIC_SCALING_ENABLED) + @Param(description = "true if dynamically scaling for instances is enabled", since = "4.21.0") + private Boolean dynamicScalingEnabled; + public void setSecurityGroupsEnabled(boolean securityGroupsEnabled) { this.securityGroupsEnabled = securityGroupsEnabled; } @@ -255,4 +264,12 @@ public class CapabilitiesResponse extends BaseResponse { public void setInstanceLeaseEnabled(Boolean instanceLeaseEnabled) { this.instanceLeaseEnabled = instanceLeaseEnabled; } + + public void setExtensionsPath(String extensionsPath) { + this.extensionsPath = extensionsPath; + } + + public void setDynamicScalingEnabled(Boolean dynamicScalingEnabled) { + this.dynamicScalingEnabled = dynamicScalingEnabled; + } } diff --git a/api/src/main/java/org/apache/cloudstack/api/response/ClusterDrsPlanMigrationResponse.java b/api/src/main/java/org/apache/cloudstack/api/response/ClusterDrsPlanMigrationResponse.java index 4114c228e26..f399c1260f7 100644 --- a/api/src/main/java/org/apache/cloudstack/api/response/ClusterDrsPlanMigrationResponse.java +++ b/api/src/main/java/org/apache/cloudstack/api/response/ClusterDrsPlanMigrationResponse.java @@ -50,13 +50,13 @@ public class ClusterDrsPlanMigrationResponse extends BaseResponse { @Param(description = "Destination host for VM migration") String destHostName; - @SerializedName(ApiConstants.JOB_ID) + @SerializedName(ApiConstants.MIGRATION_JOB_ID) @Param(description = "id of VM migration async job") - private Long jobId; + private Long migrationJobId; - @SerializedName(ApiConstants.JOB_STATUS) + @SerializedName(ApiConstants.MIGRATION_JOB_STATUS) @Param(description = "Job status of VM migration async job") - private JobInfo.Status jobStatus; + private JobInfo.Status migrationJobStatus; public ClusterDrsPlanMigrationResponse(String vmId, String vmName, String srcHostId, String srcHostName, @@ -68,8 +68,8 @@ public class ClusterDrsPlanMigrationResponse extends BaseResponse { this.srcHostName = srcHostName; this.destHostId = destHostId; this.destHostName = destHostName; - this.jobId = jobId; - this.jobStatus = jobStatus; + this.migrationJobId = jobId; + this.migrationJobStatus = jobStatus; this.setObjectName(ApiConstants.MIGRATIONS); } } diff --git a/api/src/main/java/org/apache/cloudstack/api/response/ClusterResponse.java b/api/src/main/java/org/apache/cloudstack/api/response/ClusterResponse.java index 17c86072b98..202ff4bd870 100644 --- a/api/src/main/java/org/apache/cloudstack/api/response/ClusterResponse.java +++ b/api/src/main/java/org/apache/cloudstack/api/response/ClusterResponse.java @@ -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; + } } diff --git a/api/src/main/java/org/apache/cloudstack/api/response/CreateConsoleEndpointResponse.java b/api/src/main/java/org/apache/cloudstack/api/response/CreateConsoleEndpointResponse.java index c60917bbe7a..c1e04beee73 100644 --- a/api/src/main/java/org/apache/cloudstack/api/response/CreateConsoleEndpointResponse.java +++ b/api/src/main/java/org/apache/cloudstack/api/response/CreateConsoleEndpointResponse.java @@ -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; diff --git a/api/src/main/java/org/apache/cloudstack/api/response/DomainResponse.java b/api/src/main/java/org/apache/cloudstack/api/response/DomainResponse.java index 74fa2cbb1e4..e2246a1d531 100644 --- a/api/src/main/java/org/apache/cloudstack/api/response/DomainResponse.java +++ b/api/src/main/java/org/apache/cloudstack/api/response/DomainResponse.java @@ -183,6 +183,15 @@ public class DomainResponse extends BaseResponseWithAnnotations implements Resou @SerializedName("memoryavailable") @Param(description="the total memory (in MB) available to be created for this domain", since="4.2.0") private String memoryAvailable; + @SerializedName("gpulimit") @Param(description="the total number of gpus the domain can own", since="4.21.0") + private String gpuLimit; + + @SerializedName("gputotal") @Param(description="the total number of gpus owned by domain", since="4.21.0") + private Long gpuTotal; + + @SerializedName("gpuavailable") @Param(description="the total number of gpus available to be created for this domain", since="4.21.0") + private String gpuAvailable; + @SerializedName("primarystoragelimit") @Param(description="the total primary storage space (in GiB) the domain can own", since="4.2.0") private String primaryStorageLimit; @@ -478,6 +487,21 @@ public class DomainResponse extends BaseResponseWithAnnotations implements Resou this.memoryAvailable = memoryAvailable; } + @Override + public void setGpuLimit(String gpuLimit) { + this.gpuLimit = gpuLimit; + } + + @Override + public void setGpuTotal(Long gpuTotal) { + this.gpuTotal = gpuTotal; + } + + @Override + public void setGpuAvailable(String gpuAvailable) { + this.gpuAvailable = gpuAvailable; + } + @Override public void setPrimaryStorageLimit(String primaryStorageLimit) { this.primaryStorageLimit = primaryStorageLimit; diff --git a/api/src/main/java/org/apache/cloudstack/api/response/ExtensionCustomActionParameterResponse.java b/api/src/main/java/org/apache/cloudstack/api/response/ExtensionCustomActionParameterResponse.java new file mode 100644 index 00000000000..d627f8077dc --- /dev/null +++ b/api/src/main/java/org/apache/cloudstack/api/response/ExtensionCustomActionParameterResponse.java @@ -0,0 +1,58 @@ +// Licensed to the Apache Software Foundation (ASF) under one +// or more contributor license agreements. See the NOTICE file +// distributed with this work for additional information +// regarding copyright ownership. The ASF licenses this file +// to you under the Apache License, Version 2.0 (the +// "License"); you may not use this file except in compliance +// with the License. You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, +// software distributed under the License is distributed on an +// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +// KIND, either express or implied. See the License for the +// specific language governing permissions and limitations +// under the License. + +package 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 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 valueOptions, + boolean required) { + this.name = name; + this.type = type; + this.validationFormat = validationFormat; + this.valueOptions = valueOptions; + this.required = required; + } +} diff --git a/api/src/main/java/org/apache/cloudstack/api/response/ExtensionCustomActionResponse.java b/api/src/main/java/org/apache/cloudstack/api/response/ExtensionCustomActionResponse.java new file mode 100644 index 00000000000..96edf6d2fd8 --- /dev/null +++ b/api/src/main/java/org/apache/cloudstack/api/response/ExtensionCustomActionResponse.java @@ -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 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 details; + + @SerializedName(ApiConstants.PARAMETERS) + @Param(description = "List of the parameters for the action", responseObject = ExtensionCustomActionParameterResponse.class) + private List 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 getAllowedRoleTypes() { + return allowedRoleTypes; + } + + public void setAllowedRoleTypes(List 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 parameters) { + this.parameters = parameters; + } + + public List getParameters() { + return parameters; + } + + public void setDetails(Map details) { + this.details = details; + } + + public Map getDetails() { + return details; + } + + public Date getCreated() { + return created; + } + + public void setCreated(Date created) { + this.created = created; + } +} diff --git a/api/src/main/java/org/apache/cloudstack/api/response/ExtensionResourceResponse.java b/api/src/main/java/org/apache/cloudstack/api/response/ExtensionResourceResponse.java new file mode 100644 index 00000000000..aa370887b74 --- /dev/null +++ b/api/src/main/java/org/apache/cloudstack/api/response/ExtensionResourceResponse.java @@ -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 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 getDetails() { + return details; + } + + public void setDetails(Map details) { + this.details = details; + } + + public Date getCreated() { + return created; + } + + public void setCreated(Date created) { + this.created = created; + } +} diff --git a/api/src/main/java/org/apache/cloudstack/api/response/ExtensionResponse.java b/api/src/main/java/org/apache/cloudstack/api/response/ExtensionResponse.java new file mode 100644 index 00000000000..fdf1e87df50 --- /dev/null +++ b/api/src/main/java/org/apache/cloudstack/api/response/ExtensionResponse.java @@ -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 details; + + @SerializedName(ApiConstants.RESOURCES) + @Param(description = "List of resources to which extension is registered to", responseObject = ExtensionResourceResponse.class) + private List 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 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 details) { + this.details = details; + } + + public List getResources() { + return resources; + } + + public void setResources(List 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; + } +} diff --git a/api/src/main/java/org/apache/cloudstack/api/response/GpuCardResponse.java b/api/src/main/java/org/apache/cloudstack/api/response/GpuCardResponse.java new file mode 100644 index 00000000000..ad91b3490ef --- /dev/null +++ b/api/src/main/java/org/apache/cloudstack/api/response/GpuCardResponse.java @@ -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.api.response; + +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 org.apache.cloudstack.gpu.GpuCard; + +@EntityReference(value = GpuCard.class) +public class GpuCardResponse extends BaseResponse { + @SerializedName(ApiConstants.ID) + @Param(description = "the ID of the GPU card") + protected String id; + + @SerializedName("deviceid") + @Param(description = "the device ID of the GPU card") + protected String deviceId; + + @SerializedName("devicename") + @Param(description = "the device name of the GPU card") + protected String deviceName; + + @SerializedName("name") + @Param(description = "the display name of the GPU card") + protected String name; + + @SerializedName("vendorname") + @Param(description = "the vendor name of the GPU card") + protected String vendorName; + + @SerializedName("vendorid") + @Param(description = "the vendor ID of the GPU card") + protected String vendorId; + + public GpuCardResponse(GpuCard gpuCard) { + super("gpucard"); + id = gpuCard.getUuid(); + deviceId = gpuCard.getDeviceId(); + deviceName = gpuCard.getDeviceName(); + name = gpuCard.getName(); + vendorName = gpuCard.getVendorName(); + vendorId = gpuCard.getVendorId(); + } + + public String getId() { + return id; + } + + public void setId(String id) { + this.id = id; + } + + public String getDeviceId() { + return deviceId; + } + + public void setDeviceId(String deviceId) { + this.deviceId = deviceId; + } + + public String getDeviceName() { + return deviceName; + } + + public void setDeviceName(String deviceName) { + this.deviceName = deviceName; + } + + public String getName() { + return name; + } + + public void setName(String name) { + this.name = name; + } + + public String getVendorName() { + return vendorName; + } + + public void setVendorName(String vendorName) { + this.vendorName = vendorName; + } + + public String getVendorId() { + return vendorId; + } + + public void setVendorId(String vendorId) { + this.vendorId = vendorId; + } +} diff --git a/api/src/main/java/org/apache/cloudstack/api/response/GpuDeviceResponse.java b/api/src/main/java/org/apache/cloudstack/api/response/GpuDeviceResponse.java new file mode 100644 index 00000000000..09e98b54eaa --- /dev/null +++ b/api/src/main/java/org/apache/cloudstack/api/response/GpuDeviceResponse.java @@ -0,0 +1,227 @@ +// Licensed to the Apache Software Foundation (ASF) under one +// or more contributor license agreements. See the NOTICE file +// distributed with this work for additional information +// regarding copyright ownership. The ASF licenses this file +// to you under the Apache License, Version 2.0 (the +// "License"); you may not use this file except in compliance +// with the License. You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, +// software distributed under the License is distributed on an +// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +// KIND, either express or implied. See the License for the +// specific language governing permissions and limitations +// under the License. +package org.apache.cloudstack.api.response; + +import com.cloud.serializer.Param; +import com.cloud.vm.VirtualMachine; +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 org.apache.cloudstack.gpu.GpuDevice; + +@EntityReference(value = GpuDevice.class) +public class GpuDeviceResponse extends BaseResponse { + + @SerializedName(ApiConstants.ID) + @Param(description = "the ID of the GPU device") + private String id; + + @SerializedName(ApiConstants.BUS_ADDRESS) + @Param(description = "bus address of the GPU device or MDEV UUID for vGPU devices") + private String bussAddress; + + @SerializedName(ApiConstants.GPU_DEVICE_TYPE) + @Param(description = "bus address of the GPU device") + private GpuDevice.DeviceType type; + + @SerializedName(ApiConstants.HOST_ID) + @Param(description = "the host ID where the GPU device is attached") + private String hostId; + + @SerializedName(ApiConstants.HOST_NAME) + @Param(description = "the host name where the GPU device is attached") + private String hostName; + + @SerializedName(ApiConstants.GPU_CARD_ID) + @Param(description = "the GPU card ID associated with this GPU device") + private String gpuCardId; + + @SerializedName(ApiConstants.GPU_CARD_NAME) + @Param(description = "the GPU card name associated with this GPU device") + private String gpuCardName; + + @SerializedName(ApiConstants.VGPU_PROFILE_ID) + @Param(description = "the vGPU profile ID assigned to this GPU device") + private String vgpuProfileId; + + @SerializedName(ApiConstants.VGPU_PROFILE_NAME) + @Param(description = "the vGPU profile name assigned to this GPU device") + private String vgpuProfileName; + + @SerializedName(ApiConstants.VIRTUAL_MACHINE_ID) + @Param(description = "the vGPU profile ID assigned to this GPU device") + private String vmId; + + @SerializedName(ApiConstants.VIRTUAL_MACHINE_NAME) + @Param(description = "the vGPU profile name assigned to this GPU device") + private String vmName; + + @SerializedName(ApiConstants.VIRTUAL_MACHINE_STATE) + @Param(description = "the state of the virtual machine to which this GPU device is allocated") + private VirtualMachine.State vmState; + + @SerializedName(ApiConstants.STATE) + @Param(description = "the vGPU profile name assigned to this GPU device") + private GpuDevice.State state; + + @SerializedName(ApiConstants.MANAGED_STATE) + @Param(description = "the managed state of the GPU device (Enabled/Disabled)") + private GpuDevice.ManagedState managedState; + + @SerializedName(ApiConstants.PARENT_GPU_DEVICE_ID) + @Param(description = "the ID of the parent GPU device, if this is a vGPU") + private String parentGpuDeviceId; + + @SerializedName(ApiConstants.NUMA_NODE) + @Param(description = "the NUMA node where the GPU device is located") + private String numaNode; + + + public GpuDeviceResponse() { + // Empty constructor for serialization + super("gpudevice"); + } + + public String getId() { + return id; + } + + public void setId(String id) { + this.id = id; + } + + public String getBussAddress() { + return bussAddress; + } + + public void setBussAddress(String bussAddress) { + this.bussAddress = bussAddress; + } + + public GpuDevice.DeviceType getType() { + return type; + } + + public void setType(GpuDevice.DeviceType type) { + this.type = type; + } + + public String getHostId() { + return hostId; + } + + public void setHostId(String hostId) { + this.hostId = hostId; + } + + public String getHostName() { + return hostName; + } + + public void setHostName(String hostName) { + this.hostName = hostName; + } + + public String getGpuCardId() { + return gpuCardId; + } + + public void setGpuCardId(String gpuCardId) { + this.gpuCardId = gpuCardId; + } + + public String getGpuCardName() { + return gpuCardName; + } + + public void setGpuCardName(String gpuCardName) { + this.gpuCardName = gpuCardName; + } + + public String getVgpuProfileId() { + return vgpuProfileId; + } + + public void setVgpuProfileId(String vgpuProfileId) { + this.vgpuProfileId = vgpuProfileId; + } + + public String getVgpuProfileName() { + return vgpuProfileName; + } + + public void setVgpuProfileName(String vgpuProfileName) { + this.vgpuProfileName = vgpuProfileName; + } + + public String getVmId() { + return vmId; + } + + public void setVmId(String vmId) { + this.vmId = vmId; + } + + public String getVmName() { + return vmName; + } + + public void setVmName(String vmName) { + this.vmName = vmName; + } + + public VirtualMachine.State getVmState() { + return vmState; + } + + public void setVmState(VirtualMachine.State vmState) { + this.vmState = vmState; + } + + public GpuDevice.State getState() { + return state; + } + + public void setState(GpuDevice.State state) { + this.state = state; + } + + public GpuDevice.ManagedState getManagedState() { + return managedState; + } + + public void setManagedState(GpuDevice.ManagedState managedState) { + this.managedState = managedState; + } + + public String getParentGpuDeviceId() { + return parentGpuDeviceId; + } + + public void setParentGpuDeviceId(String parentGpuDeviceId) { + this.parentGpuDeviceId = parentGpuDeviceId; + } + + public String getNumaNode() { + return numaNode; + } + + public void setNumaNode(String numaNode) { + this.numaNode = numaNode; + } +} diff --git a/api/src/main/java/org/apache/cloudstack/api/response/GuiThemeResponse.java b/api/src/main/java/org/apache/cloudstack/api/response/GuiThemeResponse.java new file mode 100644 index 00000000000..fe8a85b4176 --- /dev/null +++ b/api/src/main/java/org/apache/cloudstack/api/response/GuiThemeResponse.java @@ -0,0 +1,179 @@ +//Licensed to the Apache Software Foundation (ASF) under one +//or more contributor license agreements. See the NOTICE file +//distributed with this work for additional information +//regarding copyright ownership. The ASF licenses this file +//to you under the Apache License, Version 2.0 (the +//"License"); you may not use this file except in compliance +//with the License. You may obtain a copy of the License at +// +//http://www.apache.org/licenses/LICENSE-2.0 +// +//Unless required by applicable law or agreed to in writing, +//software distributed under the License is distributed on an +//"AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +//KIND, either express or implied. See the License for the +//specific language governing permissions and limitations +//under the License. +package org.apache.cloudstack.api.response; + +import com.cloud.serializer.Param; +import com.google.gson.annotations.SerializedName; +import org.apache.cloudstack.api.ApiConstants; +import org.apache.cloudstack.api.BaseResponse; +import org.apache.cloudstack.api.EntityReference; +import org.apache.cloudstack.gui.theme.GuiThemeJoin; + +import java.util.Date; + +@EntityReference(value = {GuiThemeJoin.class}) +public class GuiThemeResponse extends BaseResponse { + + @SerializedName(ApiConstants.ID) + @Param(description = "ID of the custom GUI theme.") + private String id; + + @SerializedName(ApiConstants.NAME) + @Param(description = "Name of the GUI theme.") + private String name; + + @SerializedName(ApiConstants.DESCRIPTION) + @Param(description = "Description of the GUI theme.") + private String description; + + @SerializedName(ApiConstants.CSS) + @Param(description = "The CSS to be retrieved and imported into the GUI when matching the theme access configurations.") + private String css; + + @SerializedName(ApiConstants.JSON_CONFIGURATION) + @Param(description = "The JSON with the configurations to be retrieved and imported into the GUI when matching the theme access configurations.") + private String jsonConfiguration; + + @SerializedName(ApiConstants.COMMON_NAMES) + @Param(description = "A set of Common Names (CN) (fixed or wildcard) separated by comma that can retrieve the theme; e.g.: *acme.com,acme2.com") + private String commonNames; + + @SerializedName(ApiConstants.DOMAIN_IDS) + @Param(description = "A set of domain UUIDs (also known as ID for the end-user) separated by comma that can retrieve the theme.") + private String domainIds; + + @SerializedName(ApiConstants.RECURSIVE_DOMAINS) + @Param(description = "Whether to consider the subdomains of the informed domain IDs.") + private Boolean recursiveDomains; + + @SerializedName(ApiConstants.ACCOUNT_IDS) + @Param(description = "A set of account UUIDs (also known as ID for the end-user) separated by comma that can retrieve the theme.") + private String accountIds; + + @SerializedName(ApiConstants.IS_PUBLIC) + @Param(description = "Defines whether a theme can be retrieved by anyone when only the `commonNames` is informed. If the `domainIds` or `accountIds` is informed, it is " + + "considered as `false`.") + private Boolean isPublic; + + @SerializedName(ApiConstants.CREATED) + @Param(description = "When the GUI theme was created.") + private Date created; + + @SerializedName(ApiConstants.REMOVED) + @Param(description = "When the GUI theme was removed.") + private Date removed; + + public GuiThemeResponse() { + super("guiThemes"); + } + + 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 String getCss() { + return css; + } + + public void setCss(String css) { + this.css = css; + } + + public String getJsonConfiguration() { + return jsonConfiguration; + } + + public void setJsonConfiguration(String jsonConfiguration) { + this.jsonConfiguration = jsonConfiguration; + } + + public String getCommonNames() { + return commonNames; + } + + public void setCommonNames(String commonNames) { + this.commonNames = commonNames; + } + + public String getDomainIds() { + return domainIds; + } + + public void setDomainIds(String domainIds) { + this.domainIds = domainIds; + } + + public String getAccountIds() { + return accountIds; + } + + public void setAccountIds(String accountIds) { + this.accountIds = accountIds; + } + + public Boolean getPublic() { + return isPublic; + } + + public void setPublic(Boolean aPublic) { + isPublic = aPublic; + } + + public Date getCreated() { + return created; + } + + public void setCreated(Date created) { + this.created = created; + } + + public Date getRemoved() { + return removed; + } + + public Boolean getRecursiveDomains() { + return recursiveDomains; + } + + public void setRecursiveDomains(Boolean recursiveDomains) { + this.recursiveDomains = recursiveDomains; + } + + public void setRemoved(Date removed) { + this.removed = removed; + } +} diff --git a/api/src/main/java/org/apache/cloudstack/api/response/HostResponse.java b/api/src/main/java/org/apache/cloudstack/api/response/HostResponse.java index 342a1eb7df3..ca31bd8b155 100644 --- a/api/src/main/java/org/apache/cloudstack/api/response/HostResponse.java +++ b/api/src/main/java/org/apache/cloudstack/api/response/HostResponse.java @@ -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; @@ -166,6 +165,14 @@ public class HostResponse extends BaseResponseWithAnnotations { @Param(description = "the amount of the host's memory currently used") private Long memoryUsed; + @SerializedName("gputotal") + @Param(description = "Total GPUs on the Host", responseObject = Long.class, since = "4.21") + private Long gpuTotal; + + @SerializedName("gpuused") + @Param(description = "Used GPUs on the Host", responseObject = Long.class, since = "4.21") + private Long gpuUsed; + @SerializedName(ApiConstants.GPUGROUP) @Param(description = "GPU cards present in the host", responseObject = GpuResponse.class, since = "4.4") private List gpuGroup; @@ -198,6 +205,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 +327,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(); @@ -439,6 +456,14 @@ public class HostResponse extends BaseResponseWithAnnotations { this.memoryUsed = memoryUsed; } + public void setGpuTotal(Long gpuTotal) { + this.gpuTotal = gpuTotal; + } + + public void setGpuUsed(Long gpuUsed) { + this.gpuUsed = gpuUsed; + } + public void setGpuGroup(List gpuGroup) { this.gpuGroup = gpuGroup; } @@ -471,6 +496,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; } @@ -903,6 +936,14 @@ public class HostResponse extends BaseResponseWithAnnotations { return memoryAllocatedBytes; } + public Long getGpuTotal() { + return gpuTotal; + } + + public Long getGpuUsed() { + return gpuUsed; + } + public Boolean getTagARule() { return isTagARule; } @@ -942,4 +983,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; + } } diff --git a/api/src/main/java/org/apache/cloudstack/api/response/IPAddressResponse.java b/api/src/main/java/org/apache/cloudstack/api/response/IPAddressResponse.java index 0018edc8638..8b0e3cc0201 100644 --- a/api/src/main/java/org/apache/cloudstack/api/response/IPAddressResponse.java +++ b/api/src/main/java/org/apache/cloudstack/api/response/IPAddressResponse.java @@ -175,6 +175,10 @@ public class IPAddressResponse extends BaseResponseWithAnnotations implements Co @Param(description="true if range is dedicated for System VMs") private boolean forSystemVms; + @SerializedName(ApiConstants.FOR_PROVIDER) + @Param(description="true if range is dedicated for external network providers", since = "4.21.0") + private boolean forProvider; + public void setIpAddress(String ipAddress) { this.ipAddress = ipAddress; } @@ -332,4 +336,8 @@ public class IPAddressResponse extends BaseResponseWithAnnotations implements Co public void setForSystemVms(boolean forSystemVms) { this.forSystemVms = forSystemVms; } + + public void setForProvider(boolean forProvider) { + this.forProvider = forProvider; + } } diff --git a/api/src/main/java/org/apache/cloudstack/api/response/NetworkResponse.java b/api/src/main/java/org/apache/cloudstack/api/response/NetworkResponse.java index a1ffda72234..d5db8b4dabb 100644 --- a/api/src/main/java/org/apache/cloudstack/api/response/NetworkResponse.java +++ b/api/src/main/java/org/apache/cloudstack/api/response/NetworkResponse.java @@ -211,14 +211,6 @@ public class NetworkResponse extends BaseResponseWithAssociatedNetwork implement @Param(description = "Name of the VPC to which this network belongs", since = "4.15") private String vpcName; - @SerializedName(ApiConstants.ASSOCIATED_NETWORK_ID) - @Param(description = "the ID of the Network associated with this network") - private String associatedNetworkId; - - @SerializedName(ApiConstants.ASSOCIATED_NETWORK) - @Param(description = "the name of the Network associated with this network") - private String associatedNetworkName; - @SerializedName(ApiConstants.TUNGSTEN_VIRTUAL_ROUTER_UUID) @Param(description = "Tungsten-Fabric virtual router the network belongs to") private String tungstenVirtualRouterUuid; @@ -619,14 +611,6 @@ public class NetworkResponse extends BaseResponseWithAssociatedNetwork implement this.vpcName = vpcName; } - public void setAssociatedNetworkId(String associatedNetworkId) { - this.associatedNetworkId = associatedNetworkId; - } - - public void setAssociatedNetworkName(String associatedNetworkName) { - this.associatedNetworkName = associatedNetworkName; - } - @Override public void setResourceIconResponse(ResourceIconResponse icon) { this.icon = icon; diff --git a/api/src/main/java/org/apache/cloudstack/api/response/ProjectResponse.java b/api/src/main/java/org/apache/cloudstack/api/response/ProjectResponse.java index 8bdf042add0..e72f6b86036 100644 --- a/api/src/main/java/org/apache/cloudstack/api/response/ProjectResponse.java +++ b/api/src/main/java/org/apache/cloudstack/api/response/ProjectResponse.java @@ -116,6 +116,18 @@ public class ProjectResponse extends BaseResponse implements ResourceLimitAndCou @Param(description = "the total memory (in MB) available to be created for this project", since = "4.2.0") private String memoryAvailable; + @SerializedName("gpulimit") + @Param(description = "the total number of gpus the project can own", since = "4.21.0") + private String gpuLimit; + + @SerializedName("gputotal") + @Param(description = "the total number of gpus owned by project", since = "4.21.0") + private Long gpuTotal; + + @SerializedName("gpuavailable") + @Param(description = "the total number of gpus available to be created for this project", since = "4.21.0") + private String gpuAvailable; + @SerializedName("primarystoragelimit") @Param(description = "the total primary storage space (in GiB) the project can own", since = "4.2.0") private String primaryStorageLimit; @@ -483,6 +495,21 @@ public class ProjectResponse extends BaseResponse implements ResourceLimitAndCou this.memoryAvailable = memoryAvailable; } + @Override + public void setGpuLimit(String gpuLimit) { + this.gpuLimit = gpuLimit; + } + + @Override + public void setGpuTotal(Long gpuTotal) { + this.gpuTotal = gpuTotal; + } + + @Override + public void setGpuAvailable(String gpuAvailable) { + this.gpuAvailable = gpuAvailable; + } + @Override public void setPrimaryStorageLimit(String primaryStorageLimit) { this.primaryStorageLimit = primaryStorageLimit; diff --git a/api/src/main/java/org/apache/cloudstack/api/response/ResourceLimitAndCountResponse.java b/api/src/main/java/org/apache/cloudstack/api/response/ResourceLimitAndCountResponse.java index b86723b36c4..66de71dd763 100644 --- a/api/src/main/java/org/apache/cloudstack/api/response/ResourceLimitAndCountResponse.java +++ b/api/src/main/java/org/apache/cloudstack/api/response/ResourceLimitAndCountResponse.java @@ -48,6 +48,12 @@ public interface ResourceLimitAndCountResponse { public void setMemoryAvailable(String memoryAvailable); + public void setGpuLimit(String gpuLimit); + + public void setGpuTotal(Long gpuTotal); + + public void setGpuAvailable(String gpuAvailable); + public void setPrimaryStorageLimit(String primaryStorageLimit); public void setPrimaryStorageTotal(Long primaryStorageTotal); diff --git a/api/src/main/java/org/apache/cloudstack/api/response/RouterHealthCheckResultResponse.java b/api/src/main/java/org/apache/cloudstack/api/response/RouterHealthCheckResultResponse.java index f98cf0acd5d..00f1e4e3bb0 100644 --- a/api/src/main/java/org/apache/cloudstack/api/response/RouterHealthCheckResultResponse.java +++ b/api/src/main/java/org/apache/cloudstack/api/response/RouterHealthCheckResultResponse.java @@ -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; diff --git a/api/src/main/java/org/apache/cloudstack/api/response/ServiceOfferingResponse.java b/api/src/main/java/org/apache/cloudstack/api/response/ServiceOfferingResponse.java index ca9358e2e5e..4565a878b34 100644 --- a/api/src/main/java/org/apache/cloudstack/api/response/ServiceOfferingResponse.java +++ b/api/src/main/java/org/apache/cloudstack/api/response/ServiceOfferingResponse.java @@ -234,6 +234,46 @@ public class ServiceOfferingResponse extends BaseResponseWithAnnotations { @Param(description = "true if virtual machine root disk will be encrypted on storage", since = "4.18") private Boolean encryptRoot; + @SerializedName(ApiConstants.GPU_CARD_ID) + @Param(description = "the ID of the gpu card to which service offering is linked", since = "4.21") + private String gpuCardId; + + @SerializedName(ApiConstants.GPU_CARD_NAME) + @Param(description = "the name of the gpu card to which service offering is linked", since = "4.21") + private String gpuCardName; + + @SerializedName(ApiConstants.VGPU_PROFILE_ID) + @Param(description = "the ID of the vgpu profile to which service offering is linked", since = "4.21") + private String vgpuProfileId; + + @SerializedName(ApiConstants.VGPU_PROFILE_NAME) + @Param(description = "the name of the vgpu profile to which service offering is linked", since = "4.21") + private String vgpuProfileName; + + @SerializedName(ApiConstants.VIDEORAM) + @Param(description = "the video RAM size in MB") + private Long videoRam; + + @SerializedName(ApiConstants.MAXHEADS) + @Param(description = "the maximum number of display heads") + private Long maxHeads; + + @SerializedName(ApiConstants.MAXRESOLUTIONX) + @Param(description = "the maximum X resolution") + private Long maxResolutionX; + + @SerializedName(ApiConstants.MAXRESOLUTIONY) + @Param(description = "the maximum Y resolution") + private Long maxResolutionY; + + @SerializedName(ApiConstants.GPU_COUNT) + @Param(description = "the count of GPUs to attach ", since = "4.21") + private Integer gpuCount; + + @SerializedName(ApiConstants.GPU_DISPLAY) + @Param(description = "whether GPU device is used for display or not ", since = "4.21") + private Boolean gpuDisplay; + @SerializedName(ApiConstants.PURGE_RESOURCES) @Param(description = "Whether to cleanup VM and its associated resource upon expunge", since = "4.20") private Boolean purgeResources; @@ -584,6 +624,86 @@ public class ServiceOfferingResponse extends BaseResponseWithAnnotations { public void setEncryptRoot(Boolean encrypt) { this.encryptRoot = encrypt; } + public String getVgpuProfileName() { + return vgpuProfileName; + } + + public void setVgpuProfileName(String vgpuProfileName) { + this.vgpuProfileName = vgpuProfileName; + } + + public Long getVideoRam() { + return videoRam; + } + + public void setVideoRam(Long videoRam) { + this.videoRam = videoRam; + } + + public Long getMaxHeads() { + return maxHeads; + } + + public void setMaxHeads(Long maxHeads) { + this.maxHeads = maxHeads; + } + + public Long getMaxResolutionX() { + return maxResolutionX; + } + + public void setMaxResolutionX(Long maxResolutionX) { + this.maxResolutionX = maxResolutionX; + } + + public Long getMaxResolutionY() { + return maxResolutionY; + } + + public void setMaxResolutionY(Long maxResolutionY) { + this.maxResolutionY = maxResolutionY; + } + + public String getVgpuProfileId() { + return vgpuProfileId; + } + + public void setVgpuProfileId(String vgpuProfileId) { + this.vgpuProfileId = vgpuProfileId; + } + + public String getGpuCardName() { + return gpuCardName; + } + + public void setGpuCardName(String gpuCardName) { + this.gpuCardName = gpuCardName; + } + + public String getGpuCardId() { + return gpuCardId; + } + + public void setGpuCardId(String gpuCardId) { + this.gpuCardId = gpuCardId; + } + + public Integer getGpuCount() { + return gpuCount; + } + + public void setGpuCount(Integer gpuCount) { + this.gpuCount = gpuCount; + } + + public Boolean getGpuDisplay() { + return gpuDisplay; + } + + public void setGpuDisplay(Boolean gpuDisplay) { + this.gpuDisplay = gpuDisplay; + } + public void setPurgeResources(Boolean purgeResources) { this.purgeResources = purgeResources; } diff --git a/api/src/main/java/org/apache/cloudstack/api/response/StaticRouteResponse.java b/api/src/main/java/org/apache/cloudstack/api/response/StaticRouteResponse.java index 51f8a130383..9008fce2392 100644 --- a/api/src/main/java/org/apache/cloudstack/api/response/StaticRouteResponse.java +++ b/api/src/main/java/org/apache/cloudstack/api/response/StaticRouteResponse.java @@ -42,9 +42,17 @@ public class StaticRouteResponse extends BaseResponse implements ControlledEntit @Param(description = "VPC the static route belongs to") private String vpcId; - @SerializedName(ApiConstants.GATEWAY_ID) + @SerializedName(ApiConstants.VPC_GATEWAY_ID) @Param(description = "VPC gateway the route is created for") - private String gatewayId; + private String vpcGatewayId; + + @SerializedName(ApiConstants.VPC_GATEWAY_IP) + @Param(description = "IP of VPC gateway the route is created for", since = "4.21.0") + private String vpcGatewayIp; + + @SerializedName(ApiConstants.NEXT_HOP) + @Param(description = "Next hop of the static route", since = "4.21.0") + private String nextHop; @SerializedName(ApiConstants.CIDR) @Param(description = "static route CIDR") @@ -95,8 +103,16 @@ public class StaticRouteResponse extends BaseResponse implements ControlledEntit this.vpcId = vpcId; } - public void setGatewayId(String gatewayId) { - this.gatewayId = gatewayId; + public void setVpcGatewayId(String vpcGatewayId) { + this.vpcGatewayId = vpcGatewayId; + } + + public void setVpcGatewayIp(String vpcGatewayIp) { + this.vpcGatewayIp = vpcGatewayIp; + } + + public void setNextHop(String nextHop) { + this.nextHop = nextHop; } public void setCidr(String cidr) { diff --git a/api/src/main/java/org/apache/cloudstack/api/response/SystemVmResponse.java b/api/src/main/java/org/apache/cloudstack/api/response/SystemVmResponse.java index be9f14f5060..7b1784fc767 100644 --- a/api/src/main/java/org/apache/cloudstack/api/response/SystemVmResponse.java +++ b/api/src/main/java/org/apache/cloudstack/api/response/SystemVmResponse.java @@ -38,14 +38,6 @@ public class SystemVmResponse extends BaseResponseWithAnnotations { @Param(description = "the system VM type") private String systemVmType; - @SerializedName("jobid") - @Param(description = "the job ID associated with the system VM. This is only displayed if the router listed is part of a currently running asynchronous job.") - private String jobId; - - @SerializedName("jobstatus") - @Param(description = "the job status associated with the system VM. This is only displayed if the router listed is part of a currently running asynchronous job.") - private Integer jobStatus; - @SerializedName("zoneid") @Param(description = "the Zone ID for the system VM") private String zoneId; diff --git a/api/src/main/java/org/apache/cloudstack/api/response/TemplateResponse.java b/api/src/main/java/org/apache/cloudstack/api/response/TemplateResponse.java index 57970368d7e..a94dbd95a56 100644 --- a/api/src/main/java/org/apache/cloudstack/api/response/TemplateResponse.java +++ b/api/src/main/java/org/apache/cloudstack/api/response/TemplateResponse.java @@ -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; + } } diff --git a/api/src/main/java/org/apache/cloudstack/api/response/UnmanageVMInstanceResponse.java b/api/src/main/java/org/apache/cloudstack/api/response/UnmanageVMInstanceResponse.java index cec70f20cff..e9d45cb506a 100644 --- a/api/src/main/java/org/apache/cloudstack/api/response/UnmanageVMInstanceResponse.java +++ b/api/src/main/java/org/apache/cloudstack/api/response/UnmanageVMInstanceResponse.java @@ -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; diff --git a/api/src/main/java/org/apache/cloudstack/api/response/UserDataResponse.java b/api/src/main/java/org/apache/cloudstack/api/response/UserDataResponse.java index 2dfc66fa7d5..ce344596aeb 100644 --- a/api/src/main/java/org/apache/cloudstack/api/response/UserDataResponse.java +++ b/api/src/main/java/org/apache/cloudstack/api/response/UserDataResponse.java @@ -27,41 +27,41 @@ import org.apache.cloudstack.api.EntityReference; public class UserDataResponse extends BaseResponseWithAnnotations implements ControlledEntityResponse { @SerializedName(ApiConstants.ID) - @Param(description = "ID of the ssh keypair") + @Param(description = "ID of the User Data") private String id; @SerializedName(ApiConstants.NAME) - @Param(description = "Name of the userdata") + @Param(description = "Name of the User Data") private String name; - @SerializedName(ApiConstants.ACCOUNT_ID) @Param(description="the owner id of the userdata") + @SerializedName(ApiConstants.ACCOUNT_ID) @Param(description="the owner id of the User Data") private String accountId; - @SerializedName(ApiConstants.ACCOUNT) @Param(description="the owner of the userdata") + @SerializedName(ApiConstants.ACCOUNT) @Param(description="the owner of the User Data") private String accountName; @SerializedName(ApiConstants.PROJECT_ID) - @Param(description = "the project id of the userdata", since = "4.19.1") + @Param(description = "the project id of the User Data", since = "4.19.1") private String projectId; @SerializedName(ApiConstants.PROJECT) - @Param(description = "the project name of the userdata", since = "4.19.1") + @Param(description = "the project name of the User Data", since = "4.19.1") private String projectName; - @SerializedName(ApiConstants.DOMAIN_ID) @Param(description="the domain id of the userdata owner") + @SerializedName(ApiConstants.DOMAIN_ID) @Param(description="the domain id of the User Data owner") private String domainId; - @SerializedName(ApiConstants.DOMAIN) @Param(description="the domain name of the userdata owner") + @SerializedName(ApiConstants.DOMAIN) @Param(description="the domain name of the User Data owner") private String domain; @SerializedName(ApiConstants.DOMAIN_PATH) - @Param(description = "path of the domain to which the userdata owner belongs", since = "4.19.2.0") + @Param(description = "path of the domain to which the User Data owner belongs", since = "4.19.2.0") private String domainPath; - @SerializedName(ApiConstants.USER_DATA) @Param(description="base64 encoded userdata content") + @SerializedName(ApiConstants.USER_DATA) @Param(description="base64 encoded User Data content") private String userData; - @SerializedName(ApiConstants.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") + @SerializedName(ApiConstants.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 the User Data") private String params; public UserDataResponse() { diff --git a/api/src/main/java/org/apache/cloudstack/api/response/UserVmResponse.java b/api/src/main/java/org/apache/cloudstack/api/response/UserVmResponse.java index d873bc65709..ca5bd09a9aa 100644 --- a/api/src/main/java/org/apache/cloudstack/api/response/UserVmResponse.java +++ b/api/src/main/java/org/apache/cloudstack/api/response/UserVmResponse.java @@ -182,6 +182,42 @@ public class UserVmResponse extends BaseResponseWithTagInformation implements Co @Param(description = "the name of the disk offering of the virtual machine. This parameter should not be used for retrieving disk offering details of DATA volumes. Use listVolumes API instead", since = "4.4") private String diskOfferingName; + @SerializedName(ApiConstants.GPU_CARD_ID) + @Param(description = "the ID of the gpu card to which service offering is linked", since = "4.21") + private String gpuCardId; + + @SerializedName(ApiConstants.GPU_CARD_NAME) + @Param(description = "the name of the gpu card to which service offering is linked", since = "4.21") + private String gpuCardName; + + @SerializedName(ApiConstants.VGPU_PROFILE_ID) + @Param(description = "the ID of the vgpu profile to which service offering is linked", since = "4.21") + private String vgpuProfileId; + + @SerializedName(ApiConstants.VGPU_PROFILE_NAME) + @Param(description = "the name of the vgpu profile to which service offering is linked", since = "4.21") + private String vgpuProfileName; + + @SerializedName(ApiConstants.VIDEORAM) + @Param(description = "the video RAM size in MB") + private Long videoRam; + + @SerializedName(ApiConstants.MAXHEADS) + @Param(description = "the maximum number of display heads") + private Long maxHeads; + + @SerializedName(ApiConstants.MAXRESOLUTIONX) + @Param(description = "the maximum X resolution") + private Long maxResolutionX; + + @SerializedName(ApiConstants.MAXRESOLUTIONY) + @Param(description = "the maximum Y resolution") + private Long maxResolutionY; + + @SerializedName(ApiConstants.GPU_COUNT) + @Param(description = "the count of GPUs on the virtual machine", since = "4.21") + private Integer gpuCount; + @SerializedName(ApiConstants.BACKUP_OFFERING_ID) @Param(description = "the ID of the backup offering of the virtual machine", since = "4.14") private String backupOfferingId; @@ -565,6 +601,42 @@ public class UserVmResponse extends BaseResponseWithTagInformation implements Co return diskOfferingName; } + public String getGpuCardId() { + return gpuCardId; + } + + public String getGpuCardName() { + return gpuCardName; + } + + public String getVgpuProfileId() { + return vgpuProfileId; + } + + public String getVgpuProfileName() { + return vgpuProfileName; + } + + public Long getVideoRam() { + return videoRam; + } + + public Long getMaxHeads() { + return maxHeads; + } + + public Long getMaxResolutionX() { + return maxResolutionX; + } + + public Long getMaxResolutionY() { + return maxResolutionY; + } + + public Integer getGpuCount() { + return gpuCount; + } + public String getBackupOfferingId() { return backupOfferingId; } @@ -847,6 +919,42 @@ public class UserVmResponse extends BaseResponseWithTagInformation implements Co this.diskOfferingName = diskOfferingName; } + public void setGpuCardId(String gpuCardId) { + this.gpuCardId = gpuCardId; + } + + public void setGpuCardName(String gpuCardName) { + this.gpuCardName = gpuCardName; + } + + public void setVgpuProfileId(String vgpuProfileId) { + this.vgpuProfileId = vgpuProfileId; + } + + public void setVgpuProfileName(String vgpuProfileName) { + this.vgpuProfileName = vgpuProfileName; + } + + public void setVideoRam(Long videoRam) { + this.videoRam = videoRam; + } + + public void setMaxHeads(Long maxHeads) { + this.maxHeads = maxHeads; + } + + public void setMaxResolutionX(Long maxResolutionX) { + this.maxResolutionX = maxResolutionX; + } + + public void setMaxResolutionY(Long maxResolutionY) { + this.maxResolutionY = maxResolutionY; + } + + public void setGpuCount(Integer gpuCount) { + this.gpuCount = gpuCount; + } + public void setBackupOfferingId(String backupOfferingId) { this.backupOfferingId = backupOfferingId; } diff --git a/api/src/main/java/org/apache/cloudstack/api/response/VgpuProfileResponse.java b/api/src/main/java/org/apache/cloudstack/api/response/VgpuProfileResponse.java new file mode 100644 index 00000000000..382b391ef59 --- /dev/null +++ b/api/src/main/java/org/apache/cloudstack/api/response/VgpuProfileResponse.java @@ -0,0 +1,135 @@ +// Licensed to the Apache Software Foundation (ASF) under one +// or more contributor license agreements. See the NOTICE file +// distributed with this work for additional information +// regarding copyright ownership. The ASF licenses this file +// to you under the Apache License, Version 2.0 (the +// "License"); you may not use this file except in compliance +// with the License. You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, +// software distributed under the License is distributed on an +// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +// KIND, either express or implied. See the License for the +// specific language governing permissions and limitations +// under the License. +package org.apache.cloudstack.api.response; + +import com.cloud.serializer.Param; +import com.google.gson.annotations.SerializedName; +import org.apache.cloudstack.api.ApiConstants; +import org.apache.cloudstack.api.EntityReference; +import org.apache.cloudstack.gpu.GpuCard; +import org.apache.cloudstack.gpu.VgpuProfile; + +@EntityReference(value = VgpuProfile.class) +public class VgpuProfileResponse extends GpuCardResponse { + + @SerializedName(ApiConstants.DESCRIPTION) + @Param(description = "the description of the vGPU profile") + private String description; + + @SerializedName(ApiConstants.GPU_CARD_ID) + @Param(description = "the ID of the GPU card associated with this vGPU profile") + private String gpuCardId; + + @SerializedName(ApiConstants.GPU_CARD_NAME) + @Param(description = "the name of the vGPU profile") + private String gpuCardName; + + @SerializedName(ApiConstants.MAX_VGPU_PER_PHYSICAL_GPU) + @Param(description = "the maximum number of vGPUs per physical GPU") + private Long maxVgpuPerPgpu; + + @SerializedName(ApiConstants.VIDEORAM) + @Param(description = "the video RAM size in MB") + private Long videoRam; + + @SerializedName(ApiConstants.MAXHEADS) + @Param(description = "the maximum number of display heads") + private Long maxHeads; + + @SerializedName(ApiConstants.MAXRESOLUTIONX) + @Param(description = "the maximum X resolution") + private Long maxResolutionX; + + @SerializedName(ApiConstants.MAXRESOLUTIONY) + @Param(description = "the maximum Y resolution") + private Long maxResolutionY; + + public VgpuProfileResponse(VgpuProfile vgpuProfile, GpuCard gpuCard) { + super(gpuCard); + id = vgpuProfile.getUuid(); + name = vgpuProfile.getName(); + description = vgpuProfile.getDescription(); + gpuCardId = gpuCard.getUuid(); + gpuCardName = gpuCard.getName(); + maxVgpuPerPgpu = vgpuProfile.getMaxVgpuPerPgpu(); + videoRam = vgpuProfile.getVideoRam(); + maxHeads = vgpuProfile.getMaxHeads(); + maxResolutionX = vgpuProfile.getMaxResolutionX(); + maxResolutionY = vgpuProfile.getMaxResolutionY(); + setObjectName("vgpuprofile"); + } + + public String getName() { + return name; + } + + public void setName(String name) { + this.name = name; + } + + public String getDescription() { + return description; + } + + public String getGpuCardId() { + return gpuCardId; + } + + public String getGpuCardName() { + return gpuCardName; + } + + public Long getMaxVgpuPerPgpu() { + return maxVgpuPerPgpu; + } + + public void setDescription(String description) { + this.description = description; + } + + public Long getVideoRam() { + return videoRam; + } + + public void setVideoRam(Long videoRam) { + this.videoRam = videoRam; + } + + public Long getMaxHeads() { + return maxHeads; + } + + public void setMaxHeads(Long maxHeads) { + this.maxHeads = maxHeads; + } + + public Long getMaxResolutionX() { + return maxResolutionX; + } + + public void setMaxResolutionX(Long maxResolutionX) { + this.maxResolutionX = maxResolutionX; + } + + public Long getMaxResolutionY() { + return maxResolutionY; + } + + public void setMaxResolutionY(Long maxResolutionY) { + this.maxResolutionY = maxResolutionY; + } +} diff --git a/api/src/main/java/org/apache/cloudstack/api/response/VlanIpRangeResponse.java b/api/src/main/java/org/apache/cloudstack/api/response/VlanIpRangeResponse.java index 1492c23e882..2be08bb7fcb 100644 --- a/api/src/main/java/org/apache/cloudstack/api/response/VlanIpRangeResponse.java +++ b/api/src/main/java/org/apache/cloudstack/api/response/VlanIpRangeResponse.java @@ -127,9 +127,9 @@ public class VlanIpRangeResponse extends BaseResponse implements ControlledEntit @Param(description = "indicates whether VLAN IP range is dedicated to system vms or not") private Boolean forSystemVms; - @SerializedName(ApiConstants.FOR_NSX) - @Param(description = "indicates whether IP range is dedicated to NSX resources or not") - private Boolean forNsx; + @SerializedName(ApiConstants.PROVIDER) + @Param(description = "indicates to which provider the IP range is dedicated to", since = "4.21.0") + private String provider; public void setId(String id) { this.id = id; @@ -249,7 +249,7 @@ public class VlanIpRangeResponse extends BaseResponse implements ControlledEntit this.ip6Cidr = ip6Cidr; } - public void setForNsx(Boolean forNsx) { - this.forNsx = forNsx; + public void setProvider(String provider) { + this.provider = provider; } } diff --git a/api/src/main/java/org/apache/cloudstack/api/response/ZoneResponse.java b/api/src/main/java/org/apache/cloudstack/api/response/ZoneResponse.java index 8e9a993bac6..09e53dbb146 100644 --- a/api/src/main/java/org/apache/cloudstack/api/response/ZoneResponse.java +++ b/api/src/main/java/org/apache/cloudstack/api/response/ZoneResponse.java @@ -97,6 +97,14 @@ public class ZoneResponse extends BaseResponseWithAnnotations implements SetReso @Param(description = "true if security groups support is enabled, false otherwise") private Boolean securityGroupsEnabled; + @SerializedName("gputotal") + @Param(description = "Total GPUs in the Zone", responseObject = Long.class, since = "4.21") + private Long gpuTotal; + + @SerializedName("gpuused") + @Param(description = "Used GPUs in the Zone", responseObject = Long.class, since = "4.21") + private Long gpuUsed; + @SerializedName("allocationstate") @Param(description = "the allocation state of the cluster") private String allocationState; @@ -145,10 +153,15 @@ public class ZoneResponse extends BaseResponseWithAnnotations implements SetReso @Param(description = "the type of the zone - core or edge", since = "4.18.0") String type; + @Deprecated(since = "4.21.0") @SerializedName(ApiConstants.NSX_ENABLED) @Param(description = "true, if zone is NSX enabled", since = "4.20.0") private boolean nsxEnabled = false; + @SerializedName(ApiConstants.PROVIDER) + @Param(description = "External network provider if any", since = "4.21.0") + private String provider = null; + @SerializedName(ApiConstants.MULTI_ARCH) @Param(description = "true, if zone contains clusters and hosts from different CPU architectures", since = "4.20") private boolean multiArch; @@ -226,6 +239,14 @@ public class ZoneResponse extends BaseResponseWithAnnotations implements SetReso this.securityGroupsEnabled = securityGroupsEnabled; } + public void setGpuTotal(Long gpuTotal) { + this.gpuTotal = gpuTotal; + } + + public void setGpuUsed(Long gpuUsed) { + this.gpuUsed = gpuUsed; + } + public void setAllocationState(String allocationState) { this.allocationState = allocationState; } @@ -361,6 +382,14 @@ public class ZoneResponse extends BaseResponseWithAnnotations implements SetReso return securityGroupsEnabled; } + public Long getGpuUsed() { + return gpuUsed; + } + + public Long getGpuTotal() { + return gpuTotal; + } + public boolean isLocalStorageEnabled() { return localStorageEnabled; } @@ -381,6 +410,14 @@ public class ZoneResponse extends BaseResponseWithAnnotations implements SetReso return nsxEnabled; } + public String getProvider() { + return provider; + } + + public void setProvider(String provider) { + this.provider = provider; + } + @Override public void setResourceIconResponse(ResourceIconResponse resourceIconResponse) { this.resourceIconResponse = resourceIconResponse; diff --git a/api/src/main/java/org/apache/cloudstack/backup/BackupSchedule.java b/api/src/main/java/org/apache/cloudstack/backup/BackupSchedule.java index 4ff946be9cd..f439b3a9139 100644 --- a/api/src/main/java/org/apache/cloudstack/backup/BackupSchedule.java +++ b/api/src/main/java/org/apache/cloudstack/backup/BackupSchedule.java @@ -31,4 +31,5 @@ public interface BackupSchedule extends InternalIdentity { Date getScheduledTimestamp(); Long getAsyncJobId(); Integer getMaxBackups(); + String getUuid(); } diff --git a/api/src/main/java/org/apache/cloudstack/extension/CustomActionResultResponse.java b/api/src/main/java/org/apache/cloudstack/extension/CustomActionResultResponse.java new file mode 100644 index 00000000000..33ff70fcace --- /dev/null +++ b/api/src/main/java/org/apache/cloudstack/extension/CustomActionResultResponse.java @@ -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 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 result) { + this.result = result; + } +} diff --git a/api/src/main/java/org/apache/cloudstack/extension/Extension.java b/api/src/main/java/org/apache/cloudstack/extension/Extension.java new file mode 100644 index 00000000000..3068612ed6f --- /dev/null +++ b/api/src/main/java/org/apache/cloudstack/extension/Extension.java @@ -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(); + } +} diff --git a/api/src/main/java/org/apache/cloudstack/extension/ExtensionCustomAction.java b/api/src/main/java/org/apache/cloudstack/extension/ExtensionCustomAction.java new file mode 100644 index 00000000000..776b42f671b --- /dev/null +++ b/api/src/main/java/org/apache/cloudstack/extension/ExtensionCustomAction.java @@ -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 valueoptions; + private final boolean required; + + public Parameter(String name, Type type, ValidationFormat validationformat, List 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 parseValueOptions(String name, String csv, Type parsedType, ValidationFormat parsedFormat) { + if (StringUtils.isBlank(csv)) { + return null; + } + List 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 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 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 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 parameters) { + return gson.toJson(parameters); + } + + public static List toListFromJson(String json) { + java.lang.reflect.Type listType = new TypeToken>() {}.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 validateParameterValues(List parameterDefinitions, + Map 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 validatedParams = new HashMap<>(); + for (Map.Entry 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 { + + @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 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); + } + } + } +} diff --git a/api/src/main/java/org/apache/cloudstack/extension/ExtensionHelper.java b/api/src/main/java/org/apache/cloudstack/extension/ExtensionHelper.java new file mode 100644 index 00000000000..f50f841ed74 --- /dev/null +++ b/api/src/main/java/org/apache/cloudstack/extension/ExtensionHelper.java @@ -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); +} diff --git a/api/src/main/java/org/apache/cloudstack/extension/ExtensionResourceMap.java b/api/src/main/java/org/apache/cloudstack/extension/ExtensionResourceMap.java new file mode 100644 index 00000000000..40ebc19eb5e --- /dev/null +++ b/api/src/main/java/org/apache/cloudstack/extension/ExtensionResourceMap.java @@ -0,0 +1,34 @@ +//Licensed to the Apache Software Foundation (ASF) under one +//or more contributor license agreements. See the NOTICE file +//distributed with this work for additional information +//regarding copyright ownership. The ASF licenses this file +//to you under the Apache License, Version 2.0 (the +//"License"); you may not use this file except in compliance +//with the License. You may obtain a copy of the License at +// +//http://www.apache.org/licenses/LICENSE-2.0 +// +//Unless required by applicable law or agreed to in writing, +//software distributed under the License is distributed on an +//"AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +//KIND, either express or implied. See the License for the +//specific language governing permissions and limitations +//under the License. + +package org.apache.cloudstack.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(); +} diff --git a/api/src/main/java/org/apache/cloudstack/gpu/GpuCard.java b/api/src/main/java/org/apache/cloudstack/gpu/GpuCard.java new file mode 100644 index 00000000000..2c02a0e30c2 --- /dev/null +++ b/api/src/main/java/org/apache/cloudstack/gpu/GpuCard.java @@ -0,0 +1,69 @@ +// 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.gpu; + +import org.apache.cloudstack.api.Identity; +import org.apache.cloudstack.api.InternalIdentity; + +import java.util.Date; + +/** + * GPU card interface representing a physical GPU card model + */ +public interface GpuCard extends InternalIdentity, Identity { + /** + * @return the UUID of the GPU card + */ + String getUuid(); + + /** + * @return the device ID of the GPU card + */ + String getDeviceId(); + + /** + * @return the device name of the GPU card + */ + String getDeviceName(); + + /** + * @return the name of the GPU card + */ + String getName(); + + /** + * @return the vendor name of the GPU card + */ + String getVendorName(); + + /** + * @return the vendor ID of the GPU card + */ + String getVendorId(); + + /** + * @return the date when the GPU card was created + */ + Date getCreated(); + + + /** + * @return the group name of the GPU card based on how the XenServer expects it. + */ + String getGroupName(); + +} diff --git a/api/src/main/java/org/apache/cloudstack/gpu/GpuDevice.java b/api/src/main/java/org/apache/cloudstack/gpu/GpuDevice.java new file mode 100644 index 00000000000..22adda46477 --- /dev/null +++ b/api/src/main/java/org/apache/cloudstack/gpu/GpuDevice.java @@ -0,0 +1,42 @@ +// 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.gpu; + +import org.apache.cloudstack.api.Identity; +import org.apache.cloudstack.api.InternalIdentity; + +/** + * GPU device interface representing a physical GPU device + */ +public interface GpuDevice extends InternalIdentity, Identity { + + enum State { + Allocated, Free, Error, PartiallyAllocated, + } + + enum ManagedState { + Managed, Unmanaged, + } + + enum DeviceType { + PCI, MDEV, VGPUOnly, + } + + long getHostId(); + + State getState(); +} diff --git a/api/src/main/java/org/apache/cloudstack/gpu/GpuService.java b/api/src/main/java/org/apache/cloudstack/gpu/GpuService.java new file mode 100644 index 00000000000..1e7928a319f --- /dev/null +++ b/api/src/main/java/org/apache/cloudstack/gpu/GpuService.java @@ -0,0 +1,157 @@ +// 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.gpu; + +import com.cloud.agent.api.VgpuTypesInfo; +import com.cloud.agent.api.to.GPUDeviceTO; +import com.cloud.host.Host; +import com.cloud.utils.component.Manager; +import com.cloud.vm.VirtualMachine; +import org.apache.cloudstack.api.command.admin.gpu.CreateGpuCardCmd; +import org.apache.cloudstack.api.command.admin.gpu.CreateGpuDeviceCmd; +import org.apache.cloudstack.api.command.admin.gpu.CreateVgpuProfileCmd; +import org.apache.cloudstack.api.command.admin.gpu.DeleteGpuCardCmd; +import org.apache.cloudstack.api.command.admin.gpu.DeleteGpuDeviceCmd; +import org.apache.cloudstack.api.command.admin.gpu.DeleteVgpuProfileCmd; +import org.apache.cloudstack.api.command.admin.gpu.UnmanageGpuDeviceCmd; +import org.apache.cloudstack.api.command.admin.gpu.DiscoverGpuDevicesCmd; +import org.apache.cloudstack.api.command.admin.gpu.ManageGpuDeviceCmd; +import org.apache.cloudstack.api.command.user.gpu.ListGpuDevicesCmd; +import org.apache.cloudstack.api.command.admin.gpu.UpdateGpuCardCmd; +import org.apache.cloudstack.api.command.admin.gpu.UpdateGpuDeviceCmd; +import org.apache.cloudstack.api.command.admin.gpu.UpdateVgpuProfileCmd; +import org.apache.cloudstack.api.command.user.gpu.ListGpuCardsCmd; +import org.apache.cloudstack.api.command.user.gpu.ListVgpuProfilesCmd; +import org.apache.cloudstack.api.response.GpuCardResponse; +import org.apache.cloudstack.api.response.GpuDeviceResponse; +import org.apache.cloudstack.api.response.ListResponse; +import org.apache.cloudstack.api.response.VgpuProfileResponse; +import org.apache.cloudstack.framework.config.ConfigKey; + +import java.util.HashMap; +import java.util.List; + +public interface GpuService extends Manager { + + ConfigKey GpuDetachOnStop = new ConfigKey<>(Boolean.class, "gpu.detach.on.stop", "Advanced", "false", + "Whether to detach GPU devices from VM on stop or keep them allocated", true, ConfigKey.Scope.Domain, null); + + GpuCard createGpuCard(CreateGpuCardCmd cmd); + + GpuCard updateGpuCard(UpdateGpuCardCmd cmd); + + boolean deleteGpuCard(DeleteGpuCardCmd cmd); + + VgpuProfileResponse createVgpuProfile(CreateVgpuProfileCmd cmd); + + VgpuProfileResponse updateVgpuProfile(UpdateVgpuProfileCmd cmd); + + boolean deleteVgpuProfile(DeleteVgpuProfileCmd cmd); + + ListResponse listGpuCards(ListGpuCardsCmd cmd); + + ListResponse listVgpuProfiles(ListVgpuProfilesCmd cmd); + + GpuDeviceResponse createGpuDevice(CreateGpuDeviceCmd cmd); + + GpuDeviceResponse updateGpuDevice(UpdateGpuDeviceCmd cmd); + + ListResponse listGpuDevices(ListGpuDevicesCmd cmd); + + boolean disableGpuDevice(UnmanageGpuDeviceCmd cmd); + + boolean enableGpuDevice(ManageGpuDeviceCmd cmd); + + /** + * Deallocate GPU devices for a VM on a host. + * + * @param vmId The ID of the VM to deallocate GPU devices for. + */ + void deallocateAllGpuDevicesForVm(long vmId); + + + /** + * Deallocate GPU devices for a VM on a host. + * + * @param vmId The ID of the VM to deallocate GPU devices for. + */ + void deallocateGpuDevicesForVmOnHost(long vmId, long hostId); + + /** + * Deallocate existing GPU devices for a VM on a host and allocate new GPU devices to the VM. + * + * @param vmId The ID of the VM to allocate GPU devices to. + * @param hostId The ID of the host to allocate GPU devices to. + * @param gpuDevices The list of GPU devices to allocate to the VM. + */ + void allocateGpuDevicesToVmOnHost(long vmId, long hostId, List gpuDevices); + + /** + * Discover GPU devices on a host by using the getGPUStatistics command and updating the GPU details for the host. + * + * @param cmd The command to discover GPU devices. + * @return The list of GPU devices. + */ + ListResponse discoverGpuDevices(DiscoverGpuDevicesCmd cmd); + + /** + * Check if GPU devices are available for a VM on a host by checking the number of available GPU devices for the + * vGPU profile. + * + * @param host The host to check GPU devices for. + * @param vmId The ID of the VM to check GPU devices for. + * @param vgpuProfile The vGPU profile to check GPU devices for. + * @param gpuCount The number of GPU devices to check for. + * @return True if GPU devices are available, false otherwise. + */ + boolean isGPUDeviceAvailable(Host host, Long vmId, VgpuProfile vgpuProfile, int gpuCount); + + /** + * Get GPU devices for a VM on a host by checking the number of available GPU devices for the vGPU profile. + * If the VM already has GPU devices assigned, deallocate them and allocate new GPU devices to the VM. + * The new GPU devices are allocated optimally to the VM. + * + * @param vm The VM to get GPU devices for. + * @param vgpuProfile The vGPU profile to get GPU devices for. + * @param gpuCount The number of GPU devices to get. + * @return The GPU devices. + */ + GPUDeviceTO getGPUDevice(VirtualMachine vm, long hostId, VgpuProfile vgpuProfile, int gpuCount); + + /** + * Gets the GPU group details from the GPU devices on a host. + * This fetches the GPU devices from the host and prepares the GPU group details for the host. + * The GPU group details are a map of GPU group name (Card's device name) to a map of vGPU profile name to + * VgpuTypesInfo. + * The VgpuTypesInfo contains the information about the GPU device. + * + * @param hostId The host ID to get GPU group details for. + * @return The GPU group details. + */ + HashMap> getGpuGroupDetailsFromGpuDevicesOnHost(long hostId); + + /** + * This method is used to add the GPU devices to the host when the host is discovered or when the GPU devices are + * updated. + * + * @param host The host to add the GPU devices to. + * @param newGpuDevicesInfo The list of GPU devices to add to the host. + */ + void addGpuDevicesToHost(Host host, List newGpuDevicesInfo); + + boolean deleteGpuDevices(DeleteGpuDeviceCmd deleteGpuDeviceCmd); +} diff --git a/api/src/main/java/org/apache/cloudstack/gpu/VgpuProfile.java b/api/src/main/java/org/apache/cloudstack/gpu/VgpuProfile.java new file mode 100644 index 00000000000..8cfac2a20de --- /dev/null +++ b/api/src/main/java/org/apache/cloudstack/gpu/VgpuProfile.java @@ -0,0 +1,74 @@ +// Licensed to the Apache Software Foundation (ASF) under one +// or more contributor license agreements. See the NOTICE file +// distributed with this work for additional information +// regarding copyright ownership. The ASF licenses this file +// to you under the Apache License, Version 2.0 (the +// "License"); you may not use this file except in compliance +// with the License. You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, +// software distributed under the License is distributed on an +// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +// KIND, either express or implied. See the License for the +// specific language governing permissions and limitations +// under the License. +package org.apache.cloudstack.gpu; + +import org.apache.cloudstack.api.Identity; +import org.apache.cloudstack.api.InternalIdentity; + +import java.util.Date; + +/** + * vGPU profile interface representing a virtualized GPU profile + */ +public interface VgpuProfile extends InternalIdentity, Identity { + /** + * @return the UUID of the vGPU profile + */ + String getUuid(); + + /** + * @return the name of the vGPU profile + */ + String getName(); + + /** + * @return the description of the vGPU profile + */ + String getDescription(); + + /** + * @return the date when the vGPU profile was created + */ + Date getCreated(); + + Long getCardId(); + + /** + * @return the maximum number of vGPUs per physical GPU + */ + Long getMaxVgpuPerPgpu(); + + /** + * @return the video RAM size in MB + */ + Long getVideoRam(); + + /** + * @return the maximum number of display heads + */ + Long getMaxHeads(); + + /** + * @return the maximum X resolution + */ + Long getMaxResolutionX(); + + /** + * @return the maximum Y resolution + */ + Long getMaxResolutionY(); +} diff --git a/api/src/main/java/org/apache/cloudstack/gui/theme/GuiTheme.java b/api/src/main/java/org/apache/cloudstack/gui/theme/GuiTheme.java new file mode 100644 index 00000000000..0cca8bc2d7d --- /dev/null +++ b/api/src/main/java/org/apache/cloudstack/gui/theme/GuiTheme.java @@ -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 org.apache.cloudstack.gui.theme; + +import org.apache.cloudstack.api.Identity; +import org.apache.cloudstack.api.InternalIdentity; + +import java.util.Date; + +public interface GuiTheme extends InternalIdentity, Identity { + + String getName(); + + String getDescription(); + + String getCss(); + + String getJsonConfiguration(); + + Date getCreated(); + + Date getRemoved(); + + boolean getIsPublic(); + + void setId(Long id); + + void setUuid(String uuid); + + void setName(String name); + + void setDescription(String description); + + void setCss(String css); + + void setJsonConfiguration(String jsonConfiguration); + + void setCreated(Date created); + + void setRemoved(Date removed); + + void setIsPublic(boolean isPublic); + + boolean isRecursiveDomains(); + + void setRecursiveDomains(boolean recursiveDomains); +} diff --git a/api/src/main/java/org/apache/cloudstack/gui/theme/GuiThemeDetails.java b/api/src/main/java/org/apache/cloudstack/gui/theme/GuiThemeDetails.java new file mode 100644 index 00000000000..164535033d9 --- /dev/null +++ b/api/src/main/java/org/apache/cloudstack/gui/theme/GuiThemeDetails.java @@ -0,0 +1,36 @@ +// 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.gui.theme; + +import org.apache.cloudstack.api.InternalIdentity; + +public interface GuiThemeDetails extends InternalIdentity { + + void setId(Long id); + + Long getGuiThemeId(); + + void setGuiThemeId(Long guiThemeId); + + String getType(); + + void setType(String type); + + String getValue(); + + void setValue(String value); +} diff --git a/api/src/main/java/org/apache/cloudstack/gui/theme/GuiThemeJoin.java b/api/src/main/java/org/apache/cloudstack/gui/theme/GuiThemeJoin.java new file mode 100644 index 00000000000..e54d53138ef --- /dev/null +++ b/api/src/main/java/org/apache/cloudstack/gui/theme/GuiThemeJoin.java @@ -0,0 +1,47 @@ +// Licensed to the Apache Software Foundation (ASF) under one +// or more contributor license agreements. See the NOTICE file +// distributed with this work for additional information +// regarding copyright ownership. The ASF licenses this file +// to you under the Apache License, Version 2.0 (the +// "License"); you may not use this file except in compliance +// with the License. You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, +// software distributed under the License is distributed on an +// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +// KIND, either express or implied. See the License for the +// specific language governing permissions and limitations +// under the License. +package org.apache.cloudstack.gui.theme; + +import org.apache.cloudstack.api.Identity; +import org.apache.cloudstack.api.InternalIdentity; + +import java.util.Date; + +public interface GuiThemeJoin extends InternalIdentity, Identity { + + String getName(); + + String getDescription(); + + String getCss(); + + String getJsonConfiguration(); + + String getCommonNames(); + + String getDomains(); + + String getAccounts(); + + boolean isRecursiveDomains(); + + boolean getIsPublic(); + + Date getCreated(); + + Date getRemoved(); +} diff --git a/api/src/main/java/org/apache/cloudstack/gui/theme/GuiThemeService.java b/api/src/main/java/org/apache/cloudstack/gui/theme/GuiThemeService.java new file mode 100644 index 00000000000..8a066d3ffec --- /dev/null +++ b/api/src/main/java/org/apache/cloudstack/gui/theme/GuiThemeService.java @@ -0,0 +1,35 @@ +// 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.gui.theme; + +import org.apache.cloudstack.api.command.user.gui.theme.CreateGuiThemeCmd; +import org.apache.cloudstack.api.command.user.gui.theme.ListGuiThemesCmd; +import org.apache.cloudstack.api.command.user.gui.theme.RemoveGuiThemeCmd; +import org.apache.cloudstack.api.command.user.gui.theme.UpdateGuiThemeCmd; +import org.apache.cloudstack.api.response.GuiThemeResponse; +import org.apache.cloudstack.api.response.ListResponse; + +public interface GuiThemeService { + + ListResponse listGuiThemes(ListGuiThemesCmd cmd); + + GuiThemeJoin createGuiTheme(CreateGuiThemeCmd cmd); + + GuiThemeJoin updateGuiTheme(UpdateGuiThemeCmd cmd); + + void removeGuiTheme(RemoveGuiThemeCmd cmd); +} diff --git a/api/src/main/java/org/apache/cloudstack/query/QueryService.java b/api/src/main/java/org/apache/cloudstack/query/QueryService.java index 4278c9217b5..6a20c2fa248 100644 --- a/api/src/main/java/org/apache/cloudstack/query/QueryService.java +++ b/api/src/main/java/org/apache/cloudstack/query/QueryService.java @@ -114,11 +114,11 @@ public interface QueryService { ConfigKey AllowUserViewDestroyedVM = new ConfigKey<>("Advanced", Boolean.class, "allow.user.view.destroyed.vm", "false", "Determines whether users can view their destroyed or expunging vm ", true, ConfigKey.Scope.Account); - static final ConfigKey UserVMDeniedDetails = new ConfigKey<>(String.class, + ConfigKey UserVMDeniedDetails = new ConfigKey<>(String.class, "user.vm.denied.details", "Advanced", "rootdisksize, cpuOvercommitRatio, memoryOvercommitRatio, Message.ReservedCapacityFreed.Flag", "Determines whether users can view certain VM settings. When set to empty, default value used is: rootdisksize, cpuOvercommitRatio, memoryOvercommitRatio, Message.ReservedCapacityFreed.Flag.", true, ConfigKey.Scope.Global, null, null, null, null, null, ConfigKey.Kind.CSV, null); - static final ConfigKey UserVMReadOnlyDetails = new ConfigKey<>(String.class, + ConfigKey UserVMReadOnlyDetails = new ConfigKey<>(String.class, "user.vm.readonly.details", "Advanced", "dataDiskController, rootDiskController", "List of read-only VM settings/details as comma separated string", true, ConfigKey.Scope.Global, null, null, null, null, null, ConfigKey.Kind.CSV, null); @@ -127,16 +127,20 @@ public interface QueryService { "network offering, zones), we use the flag to determine if the entities should be sorted ascending (when flag is true) " + "or descending (when flag is false). Within the scope of the config all users see the same result.", true, ConfigKey.Scope.Global); - public static final ConfigKey AllowUserViewAllDomainAccounts = new ConfigKey<>("Advanced", Boolean.class, + ConfigKey AllowUserViewAllDomainAccounts = new ConfigKey<>("Advanced", Boolean.class, "allow.user.view.all.domain.accounts", "false", "Determines whether users can view all user accounts within the same domain", true, ConfigKey.Scope.Domain); - static final ConfigKey SharePublicTemplatesWithOtherDomains = new ConfigKey<>("Advanced", Boolean.class, "share.public.templates.with.other.domains", "true", + ConfigKey AllowUserViewAllDataCenters = new ConfigKey<>("Advanced", Boolean.class, "allow.user.view.all.zones", "true", + "Determines whether for instance a Resource Admin can view zones that are not dedicated to them.", true, ConfigKey.Scope.Domain); + + ConfigKey SharePublicTemplatesWithOtherDomains = new ConfigKey<>("Advanced", Boolean.class, "share.public.templates.with.other.domains", "true", "If false, templates of this domain will not show up in the list templates of other domains.", true, ConfigKey.Scope.Domain); ConfigKey ReturnVmStatsOnVmList = new ConfigKey<>("Advanced", Boolean.class, "list.vm.default.details.stats", "true", "Determines whether VM stats should be returned when details are not explicitly specified in listVirtualMachines API request. When false, details default to [group, nics, secgrp, tmpl, servoff, diskoff, backoff, iso, volume, min, affgrp]. When true, all details are returned including 'stats'.", true, ConfigKey.Scope.Global); + ListResponse searchForUsers(ResponseObject.ResponseView responseView, ListUsersCmd cmd) throws PermissionDeniedException; ListResponse searchForUsers(Long domainId, boolean recursive) throws PermissionDeniedException; diff --git a/api/src/test/java/org/apache/cloudstack/api/command/admin/cluster/ListClustersCmdTest.java b/api/src/test/java/org/apache/cloudstack/api/command/admin/cluster/ListClustersCmdTest.java new file mode 100644 index 00000000000..af53a539e67 --- /dev/null +++ b/api/src/test/java/org/apache/cloudstack/api/command/admin/cluster/ListClustersCmdTest.java @@ -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 clusters = Arrays.asList(cluster1, cluster2); + listClustersCmd.updateClustersExtensions(clusters); + verify(cluster1).setExtensionId("a"); + verify(cluster2).setExtensionId("b"); + } +} diff --git a/api/src/test/java/org/apache/cloudstack/api/command/admin/gpu/CreateGpuCardCmdTest.java b/api/src/test/java/org/apache/cloudstack/api/command/admin/gpu/CreateGpuCardCmdTest.java new file mode 100644 index 00000000000..be21384109c --- /dev/null +++ b/api/src/test/java/org/apache/cloudstack/api/command/admin/gpu/CreateGpuCardCmdTest.java @@ -0,0 +1,90 @@ +/* + * 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.gpu; + +import com.cloud.user.Account; +import org.junit.Test; +import org.springframework.test.util.ReflectionTestUtils; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNull; + +public class CreateGpuCardCmdTest { + + @Test + public void getDeviceId() { + CreateGpuCardCmd cmd = new CreateGpuCardCmd(); + assertNull(cmd.getDeviceId()); + String deviceId = "0000:00:1f.6"; + ReflectionTestUtils.setField(cmd, "deviceId", deviceId); + assertEquals(deviceId, cmd.getDeviceId()); + } + + @Test + public void getDeviceName() { + CreateGpuCardCmd cmd = new CreateGpuCardCmd(); + assertNull(cmd.getDeviceName()); + String deviceName = "NVIDIA GeForce GTX 1080"; + ReflectionTestUtils.setField(cmd, "deviceName", deviceName); + assertEquals(deviceName, cmd.getDeviceName()); + } + + @Test + public void getName() { + CreateGpuCardCmd cmd = new CreateGpuCardCmd(); + assertNull(cmd.getName()); + String name = "Test GPU Card"; + ReflectionTestUtils.setField(cmd, "name", name); + assertEquals(name, cmd.getName()); + } + + @Test + public void getVendorName() { + CreateGpuCardCmd cmd = new CreateGpuCardCmd(); + assertNull(cmd.getVendorName()); + String vendorName = "NVIDIA"; + ReflectionTestUtils.setField(cmd, "vendorName", vendorName); + assertEquals(vendorName, cmd.getVendorName()); + } + + @Test + public void getVendorId() { + CreateGpuCardCmd cmd = new CreateGpuCardCmd(); + assertNull(cmd.getVendorId()); + String vendorId = "10de"; // NVIDIA vendor ID + ReflectionTestUtils.setField(cmd, "vendorId", vendorId); + assertEquals(vendorId, cmd.getVendorId()); + } + + @Test + public void getVideoRam() { + CreateGpuCardCmd cmd = new CreateGpuCardCmd(); + assertNull(cmd.getVideoRam()); + Long videoRam = 8192L; // 8 GB + ReflectionTestUtils.setField(cmd, "videoRam", videoRam); + assertEquals(videoRam, cmd.getVideoRam()); + } + + @Test + public void getEntityOwnerId() { + CreateGpuCardCmd cmd = new CreateGpuCardCmd(); + assertEquals(Account.ACCOUNT_ID_SYSTEM, cmd.getEntityOwnerId()); + } +} diff --git a/api/src/test/java/org/apache/cloudstack/api/command/admin/gpu/CreateGpuDeviceCmdTest.java b/api/src/test/java/org/apache/cloudstack/api/command/admin/gpu/CreateGpuDeviceCmdTest.java new file mode 100644 index 00000000000..fd5c568d5bc --- /dev/null +++ b/api/src/test/java/org/apache/cloudstack/api/command/admin/gpu/CreateGpuDeviceCmdTest.java @@ -0,0 +1,100 @@ +/* + * 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.gpu; + +import com.cloud.user.Account; +import org.apache.cloudstack.gpu.GpuDevice; +import org.junit.Test; +import org.springframework.test.util.ReflectionTestUtils; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNull; + +public class CreateGpuDeviceCmdTest { + + @Test + public void getHostId() { + CreateGpuDeviceCmd cmd = new CreateGpuDeviceCmd(); + assertNull(cmd.getHostId()); + Long hostId = 1L; + ReflectionTestUtils.setField(cmd, "hostId", hostId); + assertEquals(hostId, cmd.getHostId()); + } + + @Test + public void getBusAddress() { + CreateGpuDeviceCmd cmd = new CreateGpuDeviceCmd(); + assertNull(cmd.getBusAddress()); + String busAddress = "0000:00:1f.6"; + ReflectionTestUtils.setField(cmd, "busAddress", busAddress); + assertEquals(busAddress, cmd.getBusAddress()); + } + + @Test + public void getGpuCardId() { + CreateGpuDeviceCmd cmd = new CreateGpuDeviceCmd(); + assertNull(cmd.getGpuCardId()); + Long gpuCardId = 1L; + ReflectionTestUtils.setField(cmd, "gpuCardId", gpuCardId); + assertEquals(gpuCardId, cmd.getGpuCardId()); + } + + @Test + public void getVgpuProfileId() { + CreateGpuDeviceCmd cmd = new CreateGpuDeviceCmd(); + assertNull(cmd.getVgpuProfileId()); + Long vgpuProfileId = 1L; + ReflectionTestUtils.setField(cmd, "vgpuProfileId", vgpuProfileId); + assertEquals(vgpuProfileId, cmd.getVgpuProfileId()); + } + + @Test + public void getType() { + CreateGpuDeviceCmd cmd = new CreateGpuDeviceCmd(); + assertEquals(GpuDevice.DeviceType.PCI, cmd.getType()); + String type = "MDEV"; + ReflectionTestUtils.setField(cmd, "type", type); + assertEquals(GpuDevice.DeviceType.MDEV, cmd.getType()); + } + + @Test + public void getParentGpuDeviceId() { + CreateGpuDeviceCmd cmd = new CreateGpuDeviceCmd(); + assertNull(cmd.getParentGpuDeviceId()); + Long parentGpuDeviceId = 1L; + ReflectionTestUtils.setField(cmd, "parentGpuDeviceId", parentGpuDeviceId); + assertEquals(parentGpuDeviceId, cmd.getParentGpuDeviceId()); + } + + @Test + public void getNumaNode() { + CreateGpuDeviceCmd cmd = new CreateGpuDeviceCmd(); + assertEquals("-1", cmd.getNumaNode()); + String numaNode = "0"; + ReflectionTestUtils.setField(cmd, "numaNode", numaNode); + assertEquals(numaNode, cmd.getNumaNode()); + } + + @Test + public void getEntityOwnerId() { + CreateGpuDeviceCmd cmd = new CreateGpuDeviceCmd(); + assertEquals(Account.ACCOUNT_ID_SYSTEM, cmd.getEntityOwnerId()); + } +} diff --git a/api/src/test/java/org/apache/cloudstack/api/command/admin/gpu/CreateVgpuProfileCmdTest.java b/api/src/test/java/org/apache/cloudstack/api/command/admin/gpu/CreateVgpuProfileCmdTest.java new file mode 100644 index 00000000000..c71286bda65 --- /dev/null +++ b/api/src/test/java/org/apache/cloudstack/api/command/admin/gpu/CreateVgpuProfileCmdTest.java @@ -0,0 +1,108 @@ +/* + * 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.gpu; + +import com.cloud.user.Account; +import org.junit.Test; +import org.springframework.test.util.ReflectionTestUtils; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNull; + +public class CreateVgpuProfileCmdTest { + + @Test + public void getName() { + CreateVgpuProfileCmd cmd = new CreateVgpuProfileCmd(); + assertNull(cmd.getName()); + String name = "Test VGPU Profile"; + ReflectionTestUtils.setField(cmd, "name", name); + assertEquals(name, cmd.getName()); + } + + @Test + public void getDescription() { + CreateVgpuProfileCmd cmd = new CreateVgpuProfileCmd(); + assertNull(cmd.getDescription()); + String description = "Test VGPU Profile Description"; + ReflectionTestUtils.setField(cmd, "description", description); + assertEquals(description, cmd.getDescription()); + } + + @Test + public void getCardId() { + CreateVgpuProfileCmd cmd = new CreateVgpuProfileCmd(); + assertNull(cmd.getCardId()); + Long cardId = 1L; + ReflectionTestUtils.setField(cmd, "cardId", cardId); + assertEquals(cardId, cmd.getCardId()); + } + + @Test + public void getMaxVgpuPerPgpu() { + CreateVgpuProfileCmd cmd = new CreateVgpuProfileCmd(); + assertNull(cmd.getMaxVgpuPerPgpu()); + Long maxVgpuPerPgpu = 8L; + ReflectionTestUtils.setField(cmd, "maxVgpuPerPgpu", maxVgpuPerPgpu); + assertEquals(maxVgpuPerPgpu, cmd.getMaxVgpuPerPgpu()); + } + + @Test + public void getVideoRam() { + CreateVgpuProfileCmd cmd = new CreateVgpuProfileCmd(); + assertNull(cmd.getVideoRam()); + Long videoRam = 8192L; // 8 GB + ReflectionTestUtils.setField(cmd, "videoRam", videoRam); + assertEquals(videoRam, cmd.getVideoRam()); + } + + @Test + public void getMaxHeads() { + CreateVgpuProfileCmd cmd = new CreateVgpuProfileCmd(); + assertNull(cmd.getMaxHeads()); + Long maxHeads = 2L; + ReflectionTestUtils.setField(cmd, "maxHeads", maxHeads); + assertEquals(maxHeads, cmd.getMaxHeads()); + } + + @Test + public void getMaxResolutionX() { + CreateVgpuProfileCmd cmd = new CreateVgpuProfileCmd(); + assertNull(cmd.getMaxResolutionX()); + Long maxResolutionX = 1920L; // 1920 pixels + ReflectionTestUtils.setField(cmd, "maxResolutionX", maxResolutionX); + assertEquals(maxResolutionX, cmd.getMaxResolutionX()); + } + + @Test + public void getMaxResolutionY() { + CreateVgpuProfileCmd cmd = new CreateVgpuProfileCmd(); + assertNull(cmd.getMaxResolutionY()); + Long maxResolutionY = 1080L; // 1080 pixels + ReflectionTestUtils.setField(cmd, "maxResolutionY", maxResolutionY); + assertEquals(maxResolutionY, cmd.getMaxResolutionY()); + } + + @Test + public void getEntityOwnerId() { + CreateVgpuProfileCmd cmd = new CreateVgpuProfileCmd(); + assertEquals(Account.ACCOUNT_ID_SYSTEM, cmd.getEntityOwnerId()); + } +} diff --git a/api/src/test/java/org/apache/cloudstack/api/command/admin/gpu/DeleteGpuCardCmdTest.java b/api/src/test/java/org/apache/cloudstack/api/command/admin/gpu/DeleteGpuCardCmdTest.java new file mode 100644 index 00000000000..21df915b420 --- /dev/null +++ b/api/src/test/java/org/apache/cloudstack/api/command/admin/gpu/DeleteGpuCardCmdTest.java @@ -0,0 +1,45 @@ +/* + * 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.gpu; + +import com.cloud.user.Account; +import org.junit.Test; +import org.springframework.test.util.ReflectionTestUtils; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNull; + +public class DeleteGpuCardCmdTest { + + @Test + public void getId() { + DeleteGpuCardCmd cmd = new DeleteGpuCardCmd(); + assertNull(cmd.getId()); + Long id = 1L; + ReflectionTestUtils.setField(cmd, "id", id); + assertEquals(id, cmd.getId()); + } + + @Test + public void getEntityOwnerId() { + CreateVgpuProfileCmd cmd = new CreateVgpuProfileCmd(); + assertEquals(Account.ACCOUNT_ID_SYSTEM, cmd.getEntityOwnerId()); + } +} diff --git a/api/src/test/java/org/apache/cloudstack/api/command/admin/gpu/DeleteGpuDeviceCmdTest.java b/api/src/test/java/org/apache/cloudstack/api/command/admin/gpu/DeleteGpuDeviceCmdTest.java new file mode 100644 index 00000000000..02b04dd307a --- /dev/null +++ b/api/src/test/java/org/apache/cloudstack/api/command/admin/gpu/DeleteGpuDeviceCmdTest.java @@ -0,0 +1,47 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +package org.apache.cloudstack.api.command.admin.gpu; + +import com.cloud.user.Account; +import org.junit.Test; +import org.springframework.test.util.ReflectionTestUtils; + +import java.util.List; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNull; + +public class DeleteGpuDeviceCmdTest { + + @Test + public void getIds() { + DeleteGpuDeviceCmd cmd = new DeleteGpuDeviceCmd(); + assertNull(cmd.getIds()); + List ids = List.of(1L, 2L, 3L); + ReflectionTestUtils.setField(cmd, "ids", ids); + assertEquals(ids, cmd.getIds()); + } + + @Test + public void getEntityOwnerId() { + DeleteGpuDeviceCmd cmd = new DeleteGpuDeviceCmd(); + assertEquals(Account.ACCOUNT_ID_SYSTEM, cmd.getEntityOwnerId()); + } +} diff --git a/api/src/test/java/org/apache/cloudstack/api/command/admin/gpu/DeleteVgpuProfileCmdTest.java b/api/src/test/java/org/apache/cloudstack/api/command/admin/gpu/DeleteVgpuProfileCmdTest.java new file mode 100644 index 00000000000..ecd43810e65 --- /dev/null +++ b/api/src/test/java/org/apache/cloudstack/api/command/admin/gpu/DeleteVgpuProfileCmdTest.java @@ -0,0 +1,45 @@ +/* + * 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.gpu; + +import com.cloud.user.Account; +import org.junit.Test; +import org.springframework.test.util.ReflectionTestUtils; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNull; + +public class DeleteVgpuProfileCmdTest { + + @Test + public void getId() { + DeleteVgpuProfileCmd cmd = new DeleteVgpuProfileCmd(); + assertNull(cmd.getId()); + Long id = 1L; + ReflectionTestUtils.setField(cmd, "id", id); + assertEquals(id, cmd.getId()); + } + + @Test + public void getEntityOwnerId() { + DeleteVgpuProfileCmd cmd = new DeleteVgpuProfileCmd(); + assertEquals(Account.ACCOUNT_ID_SYSTEM, cmd.getEntityOwnerId()); + } +} diff --git a/api/src/test/java/org/apache/cloudstack/api/command/admin/gpu/DiscoverGpuDevicesCmdTest.java b/api/src/test/java/org/apache/cloudstack/api/command/admin/gpu/DiscoverGpuDevicesCmdTest.java new file mode 100644 index 00000000000..8295e06e0d5 --- /dev/null +++ b/api/src/test/java/org/apache/cloudstack/api/command/admin/gpu/DiscoverGpuDevicesCmdTest.java @@ -0,0 +1,38 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +package org.apache.cloudstack.api.command.admin.gpu; + +import org.junit.Test; +import org.springframework.test.util.ReflectionTestUtils; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNull; + +public class DiscoverGpuDevicesCmdTest { + + @Test + public void getId() { + DiscoverGpuDevicesCmd cmd = new DiscoverGpuDevicesCmd(); + assertNull(cmd.getId()); + Long id = 1L; + ReflectionTestUtils.setField(cmd, "id", id); + assertEquals(id, cmd.getId()); + } +} diff --git a/api/src/test/java/org/apache/cloudstack/api/command/admin/gpu/ListGpuDevicesCmdByAdminTest.java b/api/src/test/java/org/apache/cloudstack/api/command/admin/gpu/ListGpuDevicesCmdByAdminTest.java new file mode 100644 index 00000000000..200bce76933 --- /dev/null +++ b/api/src/test/java/org/apache/cloudstack/api/command/admin/gpu/ListGpuDevicesCmdByAdminTest.java @@ -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.api.command.admin.gpu; + +import org.junit.Test; +import org.springframework.test.util.ReflectionTestUtils; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNull; + +public class ListGpuDevicesCmdByAdminTest { + + @Test + public void getId() { + ListGpuDevicesCmdByAdmin cmd = new ListGpuDevicesCmdByAdmin(); + assertNull(cmd.getId()); + Long id = 1L; + ReflectionTestUtils.setField(cmd, "id", id); + assertEquals(id, cmd.getId()); + } + + @Test + public void getHostId() { + ListGpuDevicesCmdByAdmin cmd = new ListGpuDevicesCmdByAdmin(); + assertNull(cmd.getHostId()); + Long hostId = 1L; + ReflectionTestUtils.setField(cmd, "hostId", hostId); + assertEquals(hostId, cmd.getHostId()); + } + + @Test + public void getGpuCardId() { + ListGpuDevicesCmdByAdmin cmd = new ListGpuDevicesCmdByAdmin(); + assertNull(cmd.getGpuCardId()); + Long gpuCardId = 1L; + ReflectionTestUtils.setField(cmd, "gpuCardId", gpuCardId); + assertEquals(gpuCardId, cmd.getGpuCardId()); + } + + @Test + public void getVgpuProfileId() { + ListGpuDevicesCmdByAdmin cmd = new ListGpuDevicesCmdByAdmin(); + assertNull(cmd.getVgpuProfileId()); + Long vgpuProfileId = 1L; + ReflectionTestUtils.setField(cmd, "vgpuProfileId", vgpuProfileId); + assertEquals(vgpuProfileId, cmd.getVgpuProfileId()); + } +} diff --git a/api/src/test/java/org/apache/cloudstack/api/command/admin/gpu/ManageGpuDeviceCmdTest.java b/api/src/test/java/org/apache/cloudstack/api/command/admin/gpu/ManageGpuDeviceCmdTest.java new file mode 100644 index 00000000000..ee862409a93 --- /dev/null +++ b/api/src/test/java/org/apache/cloudstack/api/command/admin/gpu/ManageGpuDeviceCmdTest.java @@ -0,0 +1,47 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +package org.apache.cloudstack.api.command.admin.gpu; + +import com.cloud.user.Account; +import org.junit.Test; +import org.springframework.test.util.ReflectionTestUtils; + +import java.util.List; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNull; + +public class ManageGpuDeviceCmdTest { + + @Test + public void getIds() { + ManageGpuDeviceCmd cmd = new ManageGpuDeviceCmd(); + assertNull(cmd.getIds()); + List ids = List.of(1L, 2L, 3L); + ReflectionTestUtils.setField(cmd, "ids", ids); + assertEquals(ids, cmd.getIds()); + } + + @Test + public void getEntityOwnerId() { + ManageGpuDeviceCmd cmd = new ManageGpuDeviceCmd(); + assertEquals(Account.ACCOUNT_ID_SYSTEM, cmd.getEntityOwnerId()); + } +} diff --git a/api/src/test/java/org/apache/cloudstack/api/command/admin/gpu/UnmanageGpuDeviceCmdTest.java b/api/src/test/java/org/apache/cloudstack/api/command/admin/gpu/UnmanageGpuDeviceCmdTest.java new file mode 100644 index 00000000000..63700d9e908 --- /dev/null +++ b/api/src/test/java/org/apache/cloudstack/api/command/admin/gpu/UnmanageGpuDeviceCmdTest.java @@ -0,0 +1,47 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +package org.apache.cloudstack.api.command.admin.gpu; + +import com.cloud.user.Account; +import org.junit.Test; +import org.springframework.test.util.ReflectionTestUtils; + +import java.util.List; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNull; + +public class UnmanageGpuDeviceCmdTest { + + @Test + public void getIds() { + UnmanageGpuDeviceCmd cmd = new UnmanageGpuDeviceCmd(); + assertNull(cmd.getIds()); + List ids = List.of(1L, 2L, 3L); + ReflectionTestUtils.setField(cmd, "ids", ids); + assertEquals(ids, cmd.getIds()); + } + + @Test + public void getEntityOwnerId() { + UnmanageGpuDeviceCmd cmd = new UnmanageGpuDeviceCmd(); + assertEquals(Account.ACCOUNT_ID_SYSTEM, cmd.getEntityOwnerId()); + } +} diff --git a/api/src/test/java/org/apache/cloudstack/api/command/admin/gpu/UpdateGpuCardCmdTest.java b/api/src/test/java/org/apache/cloudstack/api/command/admin/gpu/UpdateGpuCardCmdTest.java new file mode 100644 index 00000000000..ead7ab9d3d3 --- /dev/null +++ b/api/src/test/java/org/apache/cloudstack/api/command/admin/gpu/UpdateGpuCardCmdTest.java @@ -0,0 +1,72 @@ +/* + * 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.gpu; + +import com.cloud.user.Account; +import org.junit.Test; +import org.springframework.test.util.ReflectionTestUtils; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNull; + +public class UpdateGpuCardCmdTest { + + @Test + public void getId() { + UpdateGpuCardCmd cmd = new UpdateGpuCardCmd(); + assertNull(cmd.getId()); + Long id = 1L; + ReflectionTestUtils.setField(cmd, "id", id); + assertEquals(id, cmd.getId()); + } + + @Test + public void getDeviceName() { + UpdateGpuCardCmd cmd = new UpdateGpuCardCmd(); + assertNull(cmd.getDeviceName()); + String deviceName = "GPU-1234"; + ReflectionTestUtils.setField(cmd, "deviceName", deviceName); + assertEquals(deviceName, cmd.getDeviceName()); + } + + @Test + public void getName() { + UpdateGpuCardCmd cmd = new UpdateGpuCardCmd(); + assertNull(cmd.getName()); + String name = "Test GPU Card"; + ReflectionTestUtils.setField(cmd, "name", name); + assertEquals(name, cmd.getName()); + } + + @Test + public void getVendorName() { + UpdateGpuCardCmd cmd = new UpdateGpuCardCmd(); + assertNull(cmd.getVendorName()); + String vendorName = "NVIDIA"; + ReflectionTestUtils.setField(cmd, "vendorName", vendorName); + assertEquals(vendorName, cmd.getVendorName()); + } + + @Test + public void getEntityOwnerId() { + UpdateGpuCardCmd cmd = new UpdateGpuCardCmd(); + assertEquals(Account.ACCOUNT_ID_SYSTEM, cmd.getEntityOwnerId()); + } +} diff --git a/api/src/test/java/org/apache/cloudstack/api/command/admin/gpu/UpdateGpuDeviceCmdTest.java b/api/src/test/java/org/apache/cloudstack/api/command/admin/gpu/UpdateGpuDeviceCmdTest.java new file mode 100644 index 00000000000..6ebec48aa00 --- /dev/null +++ b/api/src/test/java/org/apache/cloudstack/api/command/admin/gpu/UpdateGpuDeviceCmdTest.java @@ -0,0 +1,91 @@ +/* + * 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.gpu; + +import com.cloud.user.Account; +import org.apache.cloudstack.gpu.GpuDevice; +import org.junit.Test; +import org.springframework.test.util.ReflectionTestUtils; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNull; + +public class UpdateGpuDeviceCmdTest { + + @Test + public void getId() { + UpdateGpuDeviceCmd cmd = new UpdateGpuDeviceCmd(); + assertNull(cmd.getId()); + Long id = 1L; + ReflectionTestUtils.setField(cmd, "id", id); + assertEquals(id, cmd.getId()); + } + + @Test + public void getGpuCardId() { + UpdateGpuDeviceCmd cmd = new UpdateGpuDeviceCmd(); + assertNull(cmd.getGpuCardId()); + Long gpuCardId = 1L; + ReflectionTestUtils.setField(cmd, "gpuCardId", gpuCardId); + assertEquals(gpuCardId, cmd.getGpuCardId()); + } + + @Test + public void getVgpuProfileId() { + UpdateGpuDeviceCmd cmd = new UpdateGpuDeviceCmd(); + assertNull(cmd.getVgpuProfileId()); + Long vgpuProfileId = 1L; + ReflectionTestUtils.setField(cmd, "vgpuProfileId", vgpuProfileId); + assertEquals(vgpuProfileId, cmd.getVgpuProfileId()); + } + + @Test + public void getType() { + UpdateGpuDeviceCmd cmd = new UpdateGpuDeviceCmd(); + assertNull(cmd.getType()); + String type = "MDEV"; + ReflectionTestUtils.setField(cmd, "type", type); + assertEquals(GpuDevice.DeviceType.MDEV, cmd.getType()); + } + + @Test + public void getParentGpuDeviceId() { + UpdateGpuDeviceCmd cmd = new UpdateGpuDeviceCmd(); + assertNull(cmd.getParentGpuDeviceId()); + Long parentGpuDeviceId = 1L; + ReflectionTestUtils.setField(cmd, "parentGpuDeviceId", parentGpuDeviceId); + assertEquals(parentGpuDeviceId, cmd.getParentGpuDeviceId()); + } + + @Test + public void getNumaNode() { + UpdateGpuDeviceCmd cmd = new UpdateGpuDeviceCmd(); + assertNull(cmd.getNumaNode()); + String numaNode = "0"; + ReflectionTestUtils.setField(cmd, "numaNode", numaNode); + assertEquals(numaNode, cmd.getNumaNode()); + } + + @Test + public void getEntityOwnerId() { + UpdateGpuDeviceCmd cmd = new UpdateGpuDeviceCmd(); + assertEquals(Account.ACCOUNT_ID_SYSTEM, cmd.getEntityOwnerId()); + } +} diff --git a/api/src/test/java/org/apache/cloudstack/api/command/admin/gpu/UpdateVgpuProfileCmdTest.java b/api/src/test/java/org/apache/cloudstack/api/command/admin/gpu/UpdateVgpuProfileCmdTest.java new file mode 100644 index 00000000000..95acd71096e --- /dev/null +++ b/api/src/test/java/org/apache/cloudstack/api/command/admin/gpu/UpdateVgpuProfileCmdTest.java @@ -0,0 +1,108 @@ +/* + * 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.gpu; + +import com.cloud.user.Account; +import org.junit.Test; +import org.springframework.test.util.ReflectionTestUtils; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNull; + +public class UpdateVgpuProfileCmdTest { + + @Test + public void getId() { + UpdateVgpuProfileCmd cmd = new UpdateVgpuProfileCmd(); + assertNull(cmd.getId()); + Long id = 1L; + ReflectionTestUtils.setField(cmd, "id", id); + assertEquals(id, cmd.getId()); + } + + @Test + public void getProfileName() { + UpdateVgpuProfileCmd cmd = new UpdateVgpuProfileCmd(); + assertNull(cmd.getProfileName()); + String profileName = "Test VGPU Profile"; + ReflectionTestUtils.setField(cmd, "profileName", profileName); + assertEquals(profileName, cmd.getProfileName()); + } + + @Test + public void getDescription() { + UpdateVgpuProfileCmd cmd = new UpdateVgpuProfileCmd(); + assertNull(cmd.getDescription()); + String description = "Test VGPU Profile Description"; + ReflectionTestUtils.setField(cmd, "description", description); + assertEquals(description, cmd.getDescription()); + } + + @Test + public void getMaxVgpuPerPgpu() { + UpdateVgpuProfileCmd cmd = new UpdateVgpuProfileCmd(); + assertNull(cmd.getMaxVgpuPerPgpu()); + Long maxVgpuPerPgpu = 8L; + ReflectionTestUtils.setField(cmd, "maxVgpuPerPgpu", maxVgpuPerPgpu); + assertEquals(maxVgpuPerPgpu, cmd.getMaxVgpuPerPgpu()); + } + + @Test + public void getVideoRam() { + UpdateVgpuProfileCmd cmd = new UpdateVgpuProfileCmd(); + assertNull(cmd.getVideoRam()); + Long videoRam = 8192L; // 8 GB + ReflectionTestUtils.setField(cmd, "videoRam", videoRam); + assertEquals(videoRam, cmd.getVideoRam()); + } + + @Test + public void getMaxHeads() { + UpdateVgpuProfileCmd cmd = new UpdateVgpuProfileCmd(); + assertNull(cmd.getMaxHeads()); + Long maxHeads = 2L; + ReflectionTestUtils.setField(cmd, "maxHeads", maxHeads); + assertEquals(maxHeads, cmd.getMaxHeads()); + } + + @Test + public void getMaxResolutionX() { + UpdateVgpuProfileCmd cmd = new UpdateVgpuProfileCmd(); + assertNull(cmd.getMaxResolutionX()); + Long maxResolutionX = 1920L; // Example resolution + ReflectionTestUtils.setField(cmd, "maxResolutionX", maxResolutionX); + assertEquals(maxResolutionX, cmd.getMaxResolutionX()); + } + + @Test + public void getMaxResolutionY() { + UpdateVgpuProfileCmd cmd = new UpdateVgpuProfileCmd(); + assertNull(cmd.getMaxResolutionY()); + Long maxResolutionY = 1080L; // Example resolution + ReflectionTestUtils.setField(cmd, "maxResolutionY", maxResolutionY); + assertEquals(maxResolutionY, cmd.getMaxResolutionY()); + } + + @Test + public void getEntityOwnerId() { + UpdateVgpuProfileCmd cmd = new UpdateVgpuProfileCmd(); + assertEquals(Account.ACCOUNT_ID_SYSTEM, cmd.getEntityOwnerId()); + } +} diff --git a/api/src/test/java/org/apache/cloudstack/api/command/admin/offering/CreateServiceOfferingCmdTest.java b/api/src/test/java/org/apache/cloudstack/api/command/admin/offering/CreateServiceOfferingCmdTest.java index d6f9d9b6937..bc7f65b0756 100644 --- a/api/src/test/java/org/apache/cloudstack/api/command/admin/offering/CreateServiceOfferingCmdTest.java +++ b/api/src/test/java/org/apache/cloudstack/api/command/admin/offering/CreateServiceOfferingCmdTest.java @@ -78,4 +78,23 @@ public class CreateServiceOfferingCmdTest { ReflectionTestUtils.setField(createServiceOfferingCmd, "leaseExpiryAction", "Unknown"); Assert.assertEquals(null, createServiceOfferingCmd.getLeaseExpiryAction()); } + + @Test + public void testGetVgpuProfileId() { + ReflectionTestUtils.setField(createServiceOfferingCmd, "vgpuProfileId", 10L); + Assert.assertEquals(10L, createServiceOfferingCmd.getVgpuProfileId().longValue()); + } + + @Test + public void testGetGpuCount() { + ReflectionTestUtils.setField(createServiceOfferingCmd, "gpuCount", 2); + Assert.assertEquals(2, createServiceOfferingCmd.getGpuCount().intValue()); + } + + @Test + public void testGetGpuDisplay() { + ReflectionTestUtils.setField(createServiceOfferingCmd, "gpuDisplay", true); + Assert.assertTrue(createServiceOfferingCmd.getGpuDisplay()); + } + } diff --git a/api/src/test/java/org/apache/cloudstack/api/command/user/gpu/ListGpuCardsCmdTest.java b/api/src/test/java/org/apache/cloudstack/api/command/user/gpu/ListGpuCardsCmdTest.java new file mode 100644 index 00000000000..54e726eadbe --- /dev/null +++ b/api/src/test/java/org/apache/cloudstack/api/command/user/gpu/ListGpuCardsCmdTest.java @@ -0,0 +1,82 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +package org.apache.cloudstack.api.command.user.gpu; + +import org.junit.Assert; +import org.junit.Test; +import org.springframework.test.util.ReflectionTestUtils; + + +public class ListGpuCardsCmdTest { + + @Test + public void getId() { + ListGpuCardsCmd cmd = new ListGpuCardsCmd(); + Assert.assertNull(cmd.getId()); + Long id = 1L; + ReflectionTestUtils.setField(cmd, "id", id); + Assert.assertEquals(id, cmd.getId()); + } + + @Test + public void getVendorName() { + ListGpuCardsCmd cmd = new ListGpuCardsCmd(); + Assert.assertNull(cmd.getVendorName()); + String vendorName = "vendor name"; + ReflectionTestUtils.setField(cmd, "vendorName", vendorName); + Assert.assertEquals(vendorName, cmd.getVendorName()); + } + + @Test + public void getVendorId() { + ListGpuCardsCmd cmd = new ListGpuCardsCmd(); + Assert.assertNull(cmd.getVendorId()); + String vendorId = "vendor id"; + ReflectionTestUtils.setField(cmd, "vendorId", vendorId); + Assert.assertEquals(vendorId, cmd.getVendorId()); + } + + @Test + public void getDeviceId() { + ListGpuCardsCmd cmd = new ListGpuCardsCmd(); + Assert.assertNull(cmd.getDeviceId()); + String deviceId = "device id"; + ReflectionTestUtils.setField(cmd, "deviceId", deviceId); + Assert.assertEquals(deviceId, cmd.getDeviceId()); + } + + @Test + public void getDeviceName() { + ListGpuCardsCmd cmd = new ListGpuCardsCmd(); + Assert.assertNull(cmd.getDeviceName()); + String deviceName = "device name"; + ReflectionTestUtils.setField(cmd, "deviceName", deviceName); + Assert.assertEquals(deviceName, cmd.getDeviceName()); + } + + @Test + public void getActiveOnly() { + ListGpuCardsCmd cmd = new ListGpuCardsCmd(); + Assert.assertFalse(cmd.getActiveOnly()); + Boolean activeOnly = true; + ReflectionTestUtils.setField(cmd, "activeOnly", activeOnly); + Assert.assertEquals(activeOnly, cmd.getActiveOnly()); + } +} diff --git a/api/src/test/java/org/apache/cloudstack/api/command/user/gpu/ListGpuDevicesCmdTest.java b/api/src/test/java/org/apache/cloudstack/api/command/user/gpu/ListGpuDevicesCmdTest.java new file mode 100644 index 00000000000..e1a65ee0ece --- /dev/null +++ b/api/src/test/java/org/apache/cloudstack/api/command/user/gpu/ListGpuDevicesCmdTest.java @@ -0,0 +1,36 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +package org.apache.cloudstack.api.command.user.gpu; + +import org.junit.Assert; +import org.junit.Test; +import org.springframework.test.util.ReflectionTestUtils; + +public class ListGpuDevicesCmdTest { + + @Test + public void getVmId() { + ListGpuDevicesCmd cmd = new ListGpuDevicesCmd(); + Assert.assertNull(cmd.getVmId()); + Long vmId = 1L; + ReflectionTestUtils.setField(cmd, "vmId", vmId); + Assert.assertEquals(vmId, cmd.getVmId()); + } +} diff --git a/api/src/test/java/org/apache/cloudstack/api/command/user/gpu/ListVgpuProfilesCmdTest.java b/api/src/test/java/org/apache/cloudstack/api/command/user/gpu/ListVgpuProfilesCmdTest.java new file mode 100644 index 00000000000..7616abd1f8d --- /dev/null +++ b/api/src/test/java/org/apache/cloudstack/api/command/user/gpu/ListVgpuProfilesCmdTest.java @@ -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.api.command.user.gpu; + +import org.junit.Assert; +import org.junit.Test; +import org.springframework.test.util.ReflectionTestUtils; + +public class ListVgpuProfilesCmdTest { + + @Test + public void getId() { + ListVgpuProfilesCmd cmd = new ListVgpuProfilesCmd(); + Assert.assertNull(cmd.getId()); + Long id = 1L; + ReflectionTestUtils.setField(cmd, "id", id); + Assert.assertEquals(id, cmd.getId()); + } + + @Test + public void getName() { + ListVgpuProfilesCmd cmd = new ListVgpuProfilesCmd(); + Assert.assertNull(cmd.getName()); + String name = "Test VGPU Profile"; + ReflectionTestUtils.setField(cmd, "name", name); + Assert.assertEquals(name, cmd.getName()); + } + + @Test + public void getCardId() { + ListVgpuProfilesCmd cmd = new ListVgpuProfilesCmd(); + Assert.assertNull(cmd.getCardId()); + Long cardId = 1L; + ReflectionTestUtils.setField(cmd, "cardId", cardId); + Assert.assertEquals(cardId, cmd.getCardId()); + } + + @Test + public void getActiveOnly() { + ListVgpuProfilesCmd cmd = new ListVgpuProfilesCmd(); + Assert.assertFalse(cmd.getActiveOnly()); + Boolean activeOnly = true; + ReflectionTestUtils.setField(cmd, "activeOnly", activeOnly); + Assert.assertEquals(activeOnly, cmd.getActiveOnly()); + } +} diff --git a/api/src/test/java/org/apache/cloudstack/extension/ExtensionCustomActionTest.java b/api/src/test/java/org/apache/cloudstack/extension/ExtensionCustomActionTest.java new file mode 100644 index 00000000000..ae4314aa11a --- /dev/null +++ b/api/src/test/java/org/apache/cloudstack/extension/ExtensionCustomActionTest.java @@ -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 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 map = new HashMap<>(); + map.put(ApiConstants.NAME, ""); + map.put(ApiConstants.TYPE, "STRING"); + + ExtensionCustomAction.Parameter.fromMap(map); + } + + @Test(expected = InvalidParameterValueException.class) + public void testParameterFromMapNoType() { + Map map = new HashMap<>(); + map.put(ApiConstants.NAME, "testParam"); + + ExtensionCustomAction.Parameter.fromMap(map); + } + + @Test(expected = InvalidParameterValueException.class) + public void testParameterFromMapInvalidType() { + Map 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 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 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 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 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 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 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 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 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 suppliedValues = new HashMap<>(); + suppliedValues.put("required1", "value1"); + suppliedValues.put("required2", "42"); + suppliedValues.put("optional", "optionalValue"); + + Map 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 paramDefs = Arrays.asList( + new ExtensionCustomAction.Parameter("required1", ExtensionCustomAction.Parameter.Type.STRING, + ExtensionCustomAction.Parameter.ValidationFormat.NONE, null, true) + ); + + Map suppliedValues = new HashMap<>(); + + ExtensionCustomAction.Parameter.validateParameterValues(paramDefs, suppliedValues); + } + + @Test(expected = InvalidParameterValueException.class) + public void testValidateParameterValuesEmptyRequired() { + List paramDefs = Arrays.asList( + new ExtensionCustomAction.Parameter("required1", ExtensionCustomAction.Parameter.Type.STRING, + ExtensionCustomAction.Parameter.ValidationFormat.NONE, null, true) + ); + + Map suppliedValues = new HashMap<>(); + suppliedValues.put("required1", " "); + + ExtensionCustomAction.Parameter.validateParameterValues(paramDefs, suppliedValues); + } + + @Test + public void testValidateParameterValuesNullSupplied() { + List paramDefs = Arrays.asList( + new ExtensionCustomAction.Parameter("optional", ExtensionCustomAction.Parameter.Type.STRING, + ExtensionCustomAction.Parameter.ValidationFormat.NONE, null, false) + ); + + Map result = ExtensionCustomAction.Parameter.validateParameterValues(paramDefs, null); + assertTrue(result.isEmpty()); + } + + @Test + public void testJsonSerializationDeserialization() { + List 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 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")); + } +} diff --git a/build/replace.properties b/build/replace.properties index ce38727b80a..8c3812eb7d2 100644 --- a/build/replace.properties +++ b/build/replace.properties @@ -28,3 +28,4 @@ MSMNTDIR=/mnt COMPONENTS-SPEC=components.xml REMOTEHOST=localhost COMMONLIBDIR=client/target/common/ +EXTENSIONSDEPLOYMENTMODE=developer diff --git a/client/conf/server.properties.in b/client/conf/server.properties.in index 0a6078048d3..fd75c9d3ea0 100644 --- a/client/conf/server.properties.in +++ b/client/conf/server.properties.in @@ -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@ diff --git a/client/pom.xml b/client/pom.xml index 2b673d7750e..81e2b780934 100644 --- a/client/pom.xml +++ b/client/pom.xml @@ -321,16 +321,6 @@ cloud-plugin-hypervisor-ucs ${project.version} - - org.apache.cloudstack - cloud-plugin-hypervisor-ovm - ${project.version} - - - org.apache.cloudstack - cloud-plugin-hypervisor-ovm3 - ${project.version} - org.apache.cloudstack cloud-plugin-hypervisor-kvm @@ -347,6 +337,11 @@ cloud-plugin-hypervisor-hyperv ${project.version} + + org.apache.cloudstack + cloud-plugin-hypervisor-external + ${project.version} + org.apache.cloudstack cloud-plugin-storage-allocator-random @@ -1102,6 +1097,11 @@ cloud-plugin-network-nsx ${project.version} + + org.apache.cloudstack + cloud-plugin-network-netris + ${project.version} + org.apache.cloudstack cloud-plugin-network-tungsten diff --git a/core/src/main/java/com/cloud/agent/api/CheckS2SVpnConnectionsAnswer.java b/core/src/main/java/com/cloud/agent/api/CheckS2SVpnConnectionsAnswer.java index b299c602dde..351702a048c 100644 --- a/core/src/main/java/com/cloud/agent/api/CheckS2SVpnConnectionsAnswer.java +++ b/core/src/main/java/com/cloud/agent/api/CheckS2SVpnConnectionsAnswer.java @@ -25,7 +25,6 @@ import java.util.Map; public class CheckS2SVpnConnectionsAnswer extends Answer { Map ipToConnected; Map ipToDetail; - String details; protected CheckS2SVpnConnectionsAnswer() { ipToConnected = new HashMap(); diff --git a/core/src/main/java/com/cloud/agent/api/CheckVolumeAnswer.java b/core/src/main/java/com/cloud/agent/api/CheckVolumeAnswer.java index 5a32ab59a7a..07b7e102df9 100644 --- a/core/src/main/java/com/cloud/agent/api/CheckVolumeAnswer.java +++ b/core/src/main/java/com/cloud/agent/api/CheckVolumeAnswer.java @@ -17,22 +17,33 @@ package com.cloud.agent.api; +import org.apache.cloudstack.storage.volume.VolumeOnStorageTO; + +import java.util.Map; + public class CheckVolumeAnswer extends Answer { private long size; + private Map volumeDetails; CheckVolumeAnswer() { } - public CheckVolumeAnswer(CheckVolumeCommand cmd, String details, long size) { - super(cmd, true, details); + public CheckVolumeAnswer(CheckVolumeCommand cmd, final boolean success, String details, long size, + Map volumeDetails) { + super(cmd, success, details); this.size = size; + this.volumeDetails = volumeDetails; } public long getSize() { return size; } + public Map getVolumeDetails() { + return volumeDetails; + } + public String getString() { return "CheckVolumeAnswer [size=" + size + "]"; } diff --git a/core/src/main/java/com/cloud/agent/api/ConvertInstanceAnswer.java b/core/src/main/java/com/cloud/agent/api/ConvertInstanceAnswer.java index 174348f4f18..8092ab9b43f 100644 --- a/core/src/main/java/com/cloud/agent/api/ConvertInstanceAnswer.java +++ b/core/src/main/java/com/cloud/agent/api/ConvertInstanceAnswer.java @@ -16,8 +16,6 @@ // under the License. package com.cloud.agent.api; -import org.apache.cloudstack.vm.UnmanagedInstanceTO; - public class ConvertInstanceAnswer extends Answer { private String temporaryConvertUuid; @@ -25,16 +23,6 @@ public class ConvertInstanceAnswer extends Answer { public ConvertInstanceAnswer() { super(); } - private UnmanagedInstanceTO convertedInstance; - - public ConvertInstanceAnswer(Command command, boolean success, String details) { - super(command, success, details); - } - - public ConvertInstanceAnswer(Command command, UnmanagedInstanceTO convertedInstance) { - super(command, true, ""); - this.convertedInstance = convertedInstance; - } public ConvertInstanceAnswer(Command command, String temporaryConvertUuid) { super(command, true, ""); @@ -44,8 +32,4 @@ public class ConvertInstanceAnswer extends Answer { public String getTemporaryConvertUuid() { return temporaryConvertUuid; } - - public UnmanagedInstanceTO getConvertedInstance() { - return convertedInstance; - } } diff --git a/core/src/main/java/com/cloud/agent/api/ConvertInstanceCommand.java b/core/src/main/java/com/cloud/agent/api/ConvertInstanceCommand.java index b8250903f85..f938d0ac55d 100644 --- a/core/src/main/java/com/cloud/agent/api/ConvertInstanceCommand.java +++ b/core/src/main/java/com/cloud/agent/api/ConvertInstanceCommand.java @@ -20,13 +20,10 @@ import com.cloud.agent.api.to.DataStoreTO; import com.cloud.agent.api.to.RemoteInstanceTO; import com.cloud.hypervisor.Hypervisor; -import java.util.List; - public class ConvertInstanceCommand extends Command { private RemoteInstanceTO sourceInstance; private Hypervisor.HypervisorType destinationHypervisorType; - private List destinationStoragePools; private DataStoreTO conversionTemporaryLocation; private String templateDirOnConversionLocation; private boolean checkConversionSupport; @@ -36,12 +33,10 @@ public class ConvertInstanceCommand extends Command { public ConvertInstanceCommand() { } - public ConvertInstanceCommand(RemoteInstanceTO sourceInstance, Hypervisor.HypervisorType destinationHypervisorType, - List destinationStoragePools, DataStoreTO conversionTemporaryLocation, + public ConvertInstanceCommand(RemoteInstanceTO sourceInstance, Hypervisor.HypervisorType destinationHypervisorType, DataStoreTO conversionTemporaryLocation, String templateDirOnConversionLocation, boolean checkConversionSupport, boolean exportOvfToConversionLocation) { this.sourceInstance = sourceInstance; this.destinationHypervisorType = destinationHypervisorType; - this.destinationStoragePools = destinationStoragePools; this.conversionTemporaryLocation = conversionTemporaryLocation; this.templateDirOnConversionLocation = templateDirOnConversionLocation; this.checkConversionSupport = checkConversionSupport; @@ -56,10 +51,6 @@ public class ConvertInstanceCommand extends Command { return destinationHypervisorType; } - public List getDestinationStoragePools() { - return destinationStoragePools; - } - public DataStoreTO getConversionTemporaryLocation() { return conversionTemporaryLocation; } diff --git a/core/src/main/java/com/cloud/agent/api/CopyRemoteVolumeAnswer.java b/core/src/main/java/com/cloud/agent/api/CopyRemoteVolumeAnswer.java index e79005be71b..4aec0b26581 100644 --- a/core/src/main/java/com/cloud/agent/api/CopyRemoteVolumeAnswer.java +++ b/core/src/main/java/com/cloud/agent/api/CopyRemoteVolumeAnswer.java @@ -17,21 +17,28 @@ package com.cloud.agent.api; +import org.apache.cloudstack.storage.volume.VolumeOnStorageTO; + +import java.util.Map; + public class CopyRemoteVolumeAnswer extends Answer { private String remoteIp; private String filename; private long size; + private Map volumeDetails; CopyRemoteVolumeAnswer() { } - public CopyRemoteVolumeAnswer(CopyRemoteVolumeCommand cmd, String details, String filename, long size) { - super(cmd, true, details); + public CopyRemoteVolumeAnswer(CopyRemoteVolumeCommand cmd, final boolean success, String details, String filename, long size, + Map volumeDetails) { + super(cmd, success, details); this.remoteIp = cmd.getRemoteIp(); this.filename = filename; this.size = size; + this.volumeDetails = volumeDetails; } public String getRemoteIp() { @@ -54,6 +61,10 @@ public class CopyRemoteVolumeAnswer extends Answer { return size; } + public Map getVolumeDetails() { + return volumeDetails; + } + public String getString() { return "CopyRemoteVolumeAnswer [remoteIp=" + remoteIp + "]"; } diff --git a/core/src/main/java/com/cloud/agent/api/GetGPUStatsAnswer.java b/core/src/main/java/com/cloud/agent/api/GetGPUStatsAnswer.java index 8b3cd44e207..5bf70ed086f 100644 --- a/core/src/main/java/com/cloud/agent/api/GetGPUStatsAnswer.java +++ b/core/src/main/java/com/cloud/agent/api/GetGPUStatsAnswer.java @@ -19,7 +19,9 @@ package com.cloud.agent.api; +import java.util.ArrayList; import java.util.HashMap; +import java.util.List; import com.cloud.agent.api.LogLevel.Log4jLevel; @@ -27,6 +29,7 @@ import com.cloud.agent.api.LogLevel.Log4jLevel; public class GetGPUStatsAnswer extends Answer { private HashMap> groupDetails; + private List gpuDevices = new ArrayList<>(); public GetGPUStatsAnswer(final GetGPUStatsCommand cmd, final HashMap> groupDetails) { super(cmd); @@ -37,7 +40,21 @@ public class GetGPUStatsAnswer extends Answer { super(cmd, success, details); } + public GetGPUStatsAnswer(final GetGPUStatsCommand cmd, final List gpuDevices) { + super(cmd); + this.gpuDevices = gpuDevices; + } + + public HashMap> getGroupDetails() { return groupDetails; } + + public List getGpuDevices() { + return gpuDevices; + } + + public void setGpuDevices(List gpuDevices) { + this.gpuDevices = gpuDevices; + } } diff --git a/core/src/main/java/com/cloud/agent/api/PrepareExternalProvisioningAnswer.java b/core/src/main/java/com/cloud/agent/api/PrepareExternalProvisioningAnswer.java new file mode 100644 index 00000000000..b94d18c537e --- /dev/null +++ b/core/src/main/java/com/cloud/agent/api/PrepareExternalProvisioningAnswer.java @@ -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 serverDetails; + VirtualMachineTO virtualMachineTO; + + public PrepareExternalProvisioningAnswer() { + super(); + } + + public PrepareExternalProvisioningAnswer(PrepareExternalProvisioningCommand cmd, Map 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 getServerDetails() { + return serverDetails; + } + + public VirtualMachineTO getVirtualMachineTO() { + return virtualMachineTO; + } +} diff --git a/core/src/main/java/com/cloud/agent/api/PrepareExternalProvisioningCommand.java b/core/src/main/java/com/cloud/agent/api/PrepareExternalProvisioningCommand.java new file mode 100644 index 00000000000..44f57607eba --- /dev/null +++ b/core/src/main/java/com/cloud/agent/api/PrepareExternalProvisioningCommand.java @@ -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; + } +} diff --git a/core/src/main/java/com/cloud/agent/api/RunCustomActionAnswer.java b/core/src/main/java/com/cloud/agent/api/RunCustomActionAnswer.java new file mode 100644 index 00000000000..1deb789c995 --- /dev/null +++ b/core/src/main/java/com/cloud/agent/api/RunCustomActionAnswer.java @@ -0,0 +1,32 @@ +// +// Licensed to the Apache Software Foundation (ASF) under one +// or more contributor license agreements. See the NOTICE file +// distributed with this work for additional information +// regarding copyright ownership. The ASF licenses this file +// to you under the Apache License, Version 2.0 (the +// "License"); you may not use this file except in compliance +// with the License. You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, +// software distributed under the License is distributed on an +// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +// KIND, either express or implied. See the License for the +// specific language governing permissions and limitations +// under the License. +// + +package 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; + } +} diff --git a/core/src/main/java/com/cloud/agent/api/RunCustomActionCommand.java b/core/src/main/java/com/cloud/agent/api/RunCustomActionCommand.java new file mode 100644 index 00000000000..36489ad4fa5 --- /dev/null +++ b/core/src/main/java/com/cloud/agent/api/RunCustomActionCommand.java @@ -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 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 getParameters() { + return parameters; + } + + public void setParameters(Map parameters) { + this.parameters = parameters; + } + + @Override + public boolean executeInSequence() { + return false; + } +} diff --git a/core/src/main/java/com/cloud/agent/api/SetupGuestNetworkCommand.java b/core/src/main/java/com/cloud/agent/api/SetupGuestNetworkCommand.java index 06583f2d0d3..fef0ceed6b8 100644 --- a/core/src/main/java/com/cloud/agent/api/SetupGuestNetworkCommand.java +++ b/core/src/main/java/com/cloud/agent/api/SetupGuestNetworkCommand.java @@ -36,6 +36,7 @@ public class SetupGuestNetworkCommand extends NetworkElementCommand { String routerIpv6Gateway = null; String routerIpv6Cidr = null; boolean isVrGuestGateway = false; + long networkId; public NicTO getNic() { return nic; @@ -123,4 +124,12 @@ public class SetupGuestNetworkCommand extends NetworkElementCommand { public void setVrGuestGateway(boolean vrGuestGateway) { isVrGuestGateway = vrGuestGateway; } + + public long getNetworkId() { + return networkId; + } + + public void setNetworkId(long networkId) { + this.networkId = networkId; + } } diff --git a/core/src/main/java/com/cloud/agent/api/StartupRoutingCommand.java b/core/src/main/java/com/cloud/agent/api/StartupRoutingCommand.java index 286fced0c58..068196aabe5 100644 --- a/core/src/main/java/com/cloud/agent/api/StartupRoutingCommand.java +++ b/core/src/main/java/com/cloud/agent/api/StartupRoutingCommand.java @@ -45,6 +45,7 @@ public class StartupRoutingCommand extends StartupCommand { List hostTags = new ArrayList(); String hypervisorVersion; HashMap> groupDetails = new HashMap>(); + List gpuDevices = new ArrayList<>(); private Boolean hostHealthCheckResult; public StartupRoutingCommand() { @@ -179,7 +180,7 @@ public class StartupRoutingCommand extends StartupCommand { this.hostTags = hostTags; } - public HashMap> getGpuGroupDetails() { + public HashMap> getGpuGroupDetails() { return groupDetails; } @@ -187,6 +188,14 @@ public class StartupRoutingCommand extends StartupCommand { this.groupDetails = groupDetails; } + public List getGpuDevices() { + return gpuDevices; + } + + public void setGpuDevices(List gpuDevices) { + this.gpuDevices = gpuDevices; + } + public boolean getSupportsClonedVolumes() { return supportsClonedVolumes; } diff --git a/core/src/main/java/com/cloud/agent/api/StopCommand.java b/core/src/main/java/com/cloud/agent/api/StopCommand.java index 3923a35bd0a..d07ffa2e31f 100644 --- a/core/src/main/java/com/cloud/agent/api/StopCommand.java +++ b/core/src/main/java/com/cloud/agent/api/StopCommand.java @@ -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 dpdkInterfaceMapping; Map vlanToPersistenceMap; + boolean expungeVM = false; public Map getDpdkInterfaceMapping() { return dpdkInterfaceMapping; @@ -138,4 +139,12 @@ public class StopCommand extends RebootCommand { public void setVlanToPersistenceMap(Map vlanToPersistenceMap) { this.vlanToPersistenceMap = vlanToPersistenceMap; } + + public boolean isExpungeVM() { + return expungeVM; + } + + public void setExpungeVM(boolean expungeVM) { + this.expungeVM = expungeVM; + } } diff --git a/core/src/main/java/com/cloud/agent/api/storage/CreateDiskOnlyVmSnapshotAnswer.java b/core/src/main/java/com/cloud/agent/api/storage/CreateDiskOnlyVmSnapshotAnswer.java new file mode 100644 index 00000000000..4d61249c7cb --- /dev/null +++ b/core/src/main/java/com/cloud/agent/api/storage/CreateDiskOnlyVmSnapshotAnswer.java @@ -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.storage; + +import com.cloud.agent.api.Answer; +import com.cloud.agent.api.Command; +import com.cloud.utils.Pair; + +import java.util.Map; + +public class CreateDiskOnlyVmSnapshotAnswer extends Answer { + + protected Map> mapVolumeToSnapshotSizeAndNewVolumePath; + + public CreateDiskOnlyVmSnapshotAnswer(Command command, boolean success, String details, Map> mapVolumeToSnapshotSizeAndNewVolumePath) { + super(command, success, details); + this.mapVolumeToSnapshotSizeAndNewVolumePath = mapVolumeToSnapshotSizeAndNewVolumePath; + } + + public Map> getMapVolumeToSnapshotSizeAndNewVolumePath() { + return mapVolumeToSnapshotSizeAndNewVolumePath; + } +} diff --git a/core/src/main/java/com/cloud/agent/api/storage/CreateDiskOnlyVmSnapshotCommand.java b/core/src/main/java/com/cloud/agent/api/storage/CreateDiskOnlyVmSnapshotCommand.java new file mode 100644 index 00000000000..952bf0c971d --- /dev/null +++ b/core/src/main/java/com/cloud/agent/api/storage/CreateDiskOnlyVmSnapshotCommand.java @@ -0,0 +1,41 @@ +/* + * 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.storage; + + +import com.cloud.agent.api.VMSnapshotBaseCommand; +import com.cloud.agent.api.VMSnapshotTO; +import com.cloud.vm.VirtualMachine; +import org.apache.cloudstack.storage.to.VolumeObjectTO; + +import java.util.List; + +public class CreateDiskOnlyVmSnapshotCommand extends VMSnapshotBaseCommand { + + protected VirtualMachine.State vmState; + + public CreateDiskOnlyVmSnapshotCommand(String vmName, VMSnapshotTO snapshot, List volumeTOs, String guestOSType, VirtualMachine.State vmState) { + super(vmName, snapshot, volumeTOs, guestOSType); + this.vmState = vmState; + } + + public VirtualMachine.State getVmState() { + return vmState; + } +} diff --git a/core/src/main/java/com/cloud/agent/api/storage/DeleteDiskOnlyVmSnapshotCommand.java b/core/src/main/java/com/cloud/agent/api/storage/DeleteDiskOnlyVmSnapshotCommand.java new file mode 100644 index 00000000000..bf7bdd59736 --- /dev/null +++ b/core/src/main/java/com/cloud/agent/api/storage/DeleteDiskOnlyVmSnapshotCommand.java @@ -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 com.cloud.agent.api.storage; + +import com.cloud.agent.api.Command; + +import com.cloud.agent.api.to.DataTO; + + +import java.util.List; + +public class DeleteDiskOnlyVmSnapshotCommand extends Command { + + List snapshots; + + public DeleteDiskOnlyVmSnapshotCommand(List snapshots) { + this.snapshots = snapshots; + } + + public List getSnapshots() { + return snapshots; + } + + @Override + public boolean executeInSequence() { + return false; + } +} diff --git a/core/src/main/java/com/cloud/agent/api/storage/MergeDiskOnlyVmSnapshotCommand.java b/core/src/main/java/com/cloud/agent/api/storage/MergeDiskOnlyVmSnapshotCommand.java new file mode 100644 index 00000000000..b6396c24d10 --- /dev/null +++ b/core/src/main/java/com/cloud/agent/api/storage/MergeDiskOnlyVmSnapshotCommand.java @@ -0,0 +1,55 @@ +/* + * 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.storage; + +import com.cloud.agent.api.Command; +import com.cloud.vm.VirtualMachine; + +import java.util.List; + +public class MergeDiskOnlyVmSnapshotCommand extends Command { + + private List snapshotMergeTreeToList; + private VirtualMachine.State vmState; + private String vmName; + + public MergeDiskOnlyVmSnapshotCommand(List snapshotMergeTreeToList, VirtualMachine.State vmState, String vmName) { + this.snapshotMergeTreeToList = snapshotMergeTreeToList; + this.vmState = vmState; + this.vmName = vmName; + } + + public List getSnapshotMergeTreeToList() { + return snapshotMergeTreeToList; + } + + public VirtualMachine.State getVmState() { + return vmState; + } + + public String getVmName() { + return vmName; + } + + @Override + public boolean executeInSequence() { + return false; + } + +} diff --git a/core/src/main/java/com/cloud/agent/api/storage/RevertDiskOnlyVmSnapshotAnswer.java b/core/src/main/java/com/cloud/agent/api/storage/RevertDiskOnlyVmSnapshotAnswer.java new file mode 100644 index 00000000000..2ecf587d59d --- /dev/null +++ b/core/src/main/java/com/cloud/agent/api/storage/RevertDiskOnlyVmSnapshotAnswer.java @@ -0,0 +1,38 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +package com.cloud.agent.api.storage; + +import com.cloud.agent.api.Answer; +import com.cloud.agent.api.Command; +import org.apache.cloudstack.storage.to.VolumeObjectTO; + +import java.util.List; + +public class RevertDiskOnlyVmSnapshotAnswer extends Answer { + List volumeObjectTos; + + public RevertDiskOnlyVmSnapshotAnswer(Command cmd, List volumeObjectTos) { + super(cmd, true, null); + this.volumeObjectTos = volumeObjectTos; + } + + public List getVolumeObjectTos() { + return volumeObjectTos; + } +} diff --git a/core/src/main/java/com/cloud/agent/api/storage/RevertDiskOnlyVmSnapshotCommand.java b/core/src/main/java/com/cloud/agent/api/storage/RevertDiskOnlyVmSnapshotCommand.java new file mode 100644 index 00000000000..72bb92bcb10 --- /dev/null +++ b/core/src/main/java/com/cloud/agent/api/storage/RevertDiskOnlyVmSnapshotCommand.java @@ -0,0 +1,50 @@ +/* + * 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.storage; + +import com.cloud.agent.api.Command; +import org.apache.cloudstack.storage.to.SnapshotObjectTO; + +import java.util.List; + +public class RevertDiskOnlyVmSnapshotCommand extends Command { + + private List snapshotObjectTos; + private String vmName; + + public RevertDiskOnlyVmSnapshotCommand(List snapshotObjectTos, String vmName) { + super(); + this.snapshotObjectTos = snapshotObjectTos; + this.vmName = vmName; + } + + public List getSnapshotObjectTos() { + return snapshotObjectTos; + } + + public String getVmName() { + return vmName; + } + + @Override + public boolean executeInSequence() { + return false; + } + +} diff --git a/core/src/main/java/com/cloud/agent/api/storage/SnapshotMergeTreeTO.java b/core/src/main/java/com/cloud/agent/api/storage/SnapshotMergeTreeTO.java new file mode 100644 index 00000000000..78f23105e19 --- /dev/null +++ b/core/src/main/java/com/cloud/agent/api/storage/SnapshotMergeTreeTO.java @@ -0,0 +1,57 @@ +/* + * 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.storage; + +import com.cloud.agent.api.to.DataTO; +import org.apache.commons.lang3.builder.ReflectionToStringBuilder; + +import java.util.List; + +public class SnapshotMergeTreeTO { + DataTO parent; + DataTO child; + List grandChildren; + + public SnapshotMergeTreeTO(DataTO parent, DataTO child, List grandChildren) { + this.parent = parent; + this.child = child; + this.grandChildren = grandChildren; + } + + public DataTO getParent() { + return parent; + } + + public DataTO getChild() { + return child; + } + + public List getGrandChildren() { + return grandChildren; + } + + public void addGrandChild(DataTO grandChild) { + grandChildren.add(grandChild); + } + + @Override + public String toString() { + return ReflectionToStringBuilder.toString(this); + } +} diff --git a/core/src/main/java/com/cloud/agent/resource/virtualnetwork/facade/SetGuestNetworkConfigItem.java b/core/src/main/java/com/cloud/agent/resource/virtualnetwork/facade/SetGuestNetworkConfigItem.java index 1a6824ceb7f..57c96ec4bc9 100644 --- a/core/src/main/java/com/cloud/agent/resource/virtualnetwork/facade/SetGuestNetworkConfigItem.java +++ b/core/src/main/java/com/cloud/agent/resource/virtualnetwork/facade/SetGuestNetworkConfigItem.java @@ -76,6 +76,7 @@ public class SetGuestNetworkConfigItem extends AbstractConfigItemFacade { guestNetwork.setRouterIp6Gateway(command.getRouterIpv6Gateway()); guestNetwork.setRouterIp6Cidr(command.getRouterIpv6Cidr()); guestNetwork.setVrGuestGateway(command.isVrGuestGateway()); + guestNetwork.setNetworkId(command.getNetworkId()); return generateConfigItems(guestNetwork); } diff --git a/core/src/main/java/com/cloud/agent/resource/virtualnetwork/model/GuestNetwork.java b/core/src/main/java/com/cloud/agent/resource/virtualnetwork/model/GuestNetwork.java index a416b4bc5e4..6bf36d62bf9 100644 --- a/core/src/main/java/com/cloud/agent/resource/virtualnetwork/model/GuestNetwork.java +++ b/core/src/main/java/com/cloud/agent/resource/virtualnetwork/model/GuestNetwork.java @@ -38,6 +38,7 @@ public class GuestNetwork extends ConfigBase { private String routerIp6Gateway; private String routerIp6Cidr; private boolean isVrGuestGateway; + long networkId; private Integer mtu; @@ -211,4 +212,12 @@ public class GuestNetwork extends ConfigBase { public void setVrGuestGateway(boolean vrGuestGateway) { isVrGuestGateway = vrGuestGateway; } + + public long getNetworkId() { + return networkId; + } + + public void setNetworkId(long networkId) { + this.networkId = networkId; + } } diff --git a/core/src/main/java/com/cloud/agent/transport/ArrayTypeAdaptor.java b/core/src/main/java/com/cloud/agent/transport/ArrayTypeAdaptor.java index 7229f0903b7..eabcc82040d 100644 --- a/core/src/main/java/com/cloud/agent/transport/ArrayTypeAdaptor.java +++ b/core/src/main/java/com/cloud/agent/transport/ArrayTypeAdaptor.java @@ -75,13 +75,17 @@ public class ArrayTypeAdaptor implements JsonDeserializer, JsonSerialize try { clazz = Class.forName(name); } catch (ClassNotFoundException e) { - throw new CloudRuntimeException("can't find " + name); + throw new JsonParseException("can't find " + name); } T cmd = (T)_gson.fromJson(entry.getValue(), clazz); cmds.add(cmd); } - Class type = ((Class)typeOfT).getComponentType(); - T[] ts = (T[])Array.newInstance(type, cmds.size()); - return cmds.toArray(ts); + try { + Class type = Class.forName(typeOfT.getTypeName().replace("[]", "")); + T[] ts = (T[])Array.newInstance(type, cmds.size()); + return cmds.toArray(ts); + } catch (ClassNotFoundException e) { + throw new CloudRuntimeException("can't find " + typeOfT.getTypeName()); + } } } diff --git a/core/src/main/java/org/apache/cloudstack/direct/download/HttpsDirectTemplateDownloader.java b/core/src/main/java/org/apache/cloudstack/direct/download/HttpsDirectTemplateDownloader.java index b8a25a11b5c..b84718e4f93 100644 --- a/core/src/main/java/org/apache/cloudstack/direct/download/HttpsDirectTemplateDownloader.java +++ b/core/src/main/java/org/apache/cloudstack/direct/download/HttpsDirectTemplateDownloader.java @@ -39,9 +39,7 @@ import java.util.Map; import javax.net.ssl.HttpsURLConnection; import javax.net.ssl.SSLContext; -import javax.net.ssl.TrustManager; -import org.apache.cloudstack.utils.security.SSLUtils; import org.apache.commons.collections.MapUtils; import org.apache.commons.httpclient.HttpStatus; import org.apache.commons.io.IOUtils; @@ -55,6 +53,7 @@ import org.apache.http.client.methods.HttpUriRequest; import org.apache.http.conn.ssl.SSLConnectionSocketFactory; import org.apache.http.impl.client.CloseableHttpClient; import org.apache.http.impl.client.HttpClients; +import org.apache.http.ssl.SSLContexts; import org.apache.http.util.EntityUtils; import com.cloud.utils.Pair; @@ -98,8 +97,8 @@ public class HttpsDirectTemplateDownloader extends DirectTemplateDownloaderImpl req = new HttpGet(downloadUrl); setFollowRedirects(this.isFollowRedirects()); if (MapUtils.isNotEmpty(headers)) { - for (String headerKey: headers.keySet()) { - req.setHeader(headerKey, headers.get(headerKey)); + for (Map.Entry entry : headers.entrySet()) { + req.setHeader(entry.getKey(), entry.getValue()); } } } @@ -120,10 +119,10 @@ public class HttpsDirectTemplateDownloader extends DirectTemplateDownloaderImpl String password = "changeit"; defaultKeystore.load(is, password.toCharArray()); } - TrustManager[] tm = HttpsMultiTrustManager.getTrustManagersFromKeyStores(customKeystore, defaultKeystore); - SSLContext sslContext = SSLUtils.getSSLContext(); - sslContext.init(null, tm, null); - return sslContext; + return SSLContexts.custom() + .loadTrustMaterial(customKeystore, null) + .loadTrustMaterial(defaultKeystore, null) + .build(); } catch (KeyStoreException | NoSuchAlgorithmException | CertificateException | IOException | KeyManagementException e) { logger.error(String.format("Failure getting SSL context for HTTPS downloader, using default SSL context: %s", e.getMessage()), e); try { diff --git a/core/src/main/java/org/apache/cloudstack/direct/download/HttpsMultiTrustManager.java b/core/src/main/java/org/apache/cloudstack/direct/download/HttpsMultiTrustManager.java deleted file mode 100644 index fe47847c36c..00000000000 --- a/core/src/main/java/org/apache/cloudstack/direct/download/HttpsMultiTrustManager.java +++ /dev/null @@ -1,102 +0,0 @@ -// 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.direct.download; - -import java.security.KeyStore; -import java.security.KeyStoreException; -import java.security.NoSuchAlgorithmException; -import java.security.cert.CertificateException; -import java.security.cert.X509Certificate; -import java.util.ArrayList; -import java.util.Arrays; -import java.util.List; - -import javax.net.ssl.TrustManager; -import javax.net.ssl.TrustManagerFactory; -import javax.net.ssl.X509TrustManager; - -import com.google.common.collect.ImmutableList; -import com.google.common.collect.Iterables; - -public class HttpsMultiTrustManager implements X509TrustManager { - - private final List trustManagers; - - public HttpsMultiTrustManager(KeyStore... keystores) { - List trustManagers = new ArrayList<>(); - trustManagers.add(getTrustManager(null)); - for (KeyStore keystore : keystores) { - trustManagers.add(getTrustManager(keystore)); - } - this.trustManagers = ImmutableList.copyOf(trustManagers); - } - - public static TrustManager[] getTrustManagersFromKeyStores(KeyStore... keyStore) { - return new TrustManager[] { new HttpsMultiTrustManager(keyStore) }; - - } - - @Override - public void checkClientTrusted(X509Certificate[] chain, String authType) throws CertificateException { - for (X509TrustManager trustManager : trustManagers) { - try { - trustManager.checkClientTrusted(chain, authType); - return; - } catch (CertificateException ignored) {} - } - throw new CertificateException("None of the TrustManagers trust this certificate chain"); - } - - @Override - public void checkServerTrusted(X509Certificate[] chain, String authType) throws CertificateException { - for (X509TrustManager trustManager : trustManagers) { - try { - trustManager.checkServerTrusted(chain, authType); - return; - } catch (CertificateException ignored) {} - } - throw new CertificateException("None of the TrustManagers trust this certificate chain"); - } - - @Override - public X509Certificate[] getAcceptedIssuers() { - ImmutableList.Builder certificates = ImmutableList.builder(); - for (X509TrustManager trustManager : trustManagers) { - for (X509Certificate cert : trustManager.getAcceptedIssuers()) { - certificates.add(cert); - } - } - return Iterables.toArray(certificates.build(), X509Certificate.class); - } - - public X509TrustManager getTrustManager(KeyStore keystore) { - return getTrustManager(TrustManagerFactory.getDefaultAlgorithm(), keystore); - } - - public X509TrustManager getTrustManager(String algorithm, KeyStore keystore) { - TrustManagerFactory factory; - try { - factory = TrustManagerFactory.getInstance(algorithm); - factory.init(keystore); - return Iterables.getFirst(Iterables.filter( - Arrays.asList(factory.getTrustManagers()), X509TrustManager.class), null); - } catch (NoSuchAlgorithmException | KeyStoreException e) { - e.printStackTrace(); - } - return null; - } -} diff --git a/core/src/test/java/com/cloud/agent/transport/ResponseTest.java b/core/src/test/java/com/cloud/agent/transport/ResponseTest.java new file mode 100644 index 00000000000..06869b42eb9 --- /dev/null +++ b/core/src/test/java/com/cloud/agent/transport/ResponseTest.java @@ -0,0 +1,46 @@ +// +// Licensed to the Apache Software Foundation (ASF) under one +// or more contributor license agreements. See the NOTICE file +// distributed with this work for additional information +// regarding copyright ownership. The ASF licenses this file +// to you under the Apache License, Version 2.0 (the +// "License"); you may not use this file except in compliance +// with the License. You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, +// software distributed under the License is distributed on an +// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +// KIND, either express or implied. See the License for the +// specific language governing permissions and limitations +// under the License. +// + +package com.cloud.agent.transport; + +import junit.framework.TestCase; + +import org.apache.logging.log4j.Logger; +import org.apache.logging.log4j.LogManager; +import org.junit.Assert; +import com.cloud.agent.api.Answer; +import com.cloud.agent.api.CheckS2SVpnConnectionsAnswer; + +import com.cloud.agent.transport.Request.Version; + +public class ResponseTest extends TestCase { + protected Logger logger = LogManager.getLogger(getClass()); + + public void testCheckS2SVpnConnectionsAnswer() { + logger.info("Testing CheckS2SVpnConnectionsAnswer"); + String content = "[{\"com.cloud.agent.api.CheckS2SVpnConnectionsAnswer\":{\"ipToConnected\":{\"10.0.53.13\":true}," + + "\"ipToDetail\":{\"10.0.53.13\":\"IPsec SA found;Site-to-site VPN have connected\"}," + + "\"details\":\"10.0.53.13:0:IPsec SA found;Site-to-site VPN have connected\\u0026\\n\"," + + "\"result\":true,\"contextMap\":{},\"wait\":0,\"bypassHostMaintenance\":false}}]"; + Response response = new Response(Version.v2, 1L, 2L, 3L, 1L, (short)1, content); + Answer answer = response.getAnswer(); + Assert.assertTrue(answer instanceof CheckS2SVpnConnectionsAnswer); + } + +} diff --git a/debian/cloudstack-common.install b/debian/cloudstack-common.install index 08f56d4f117..51fc5bf2a66 100644 --- a/debian/cloudstack-common.install +++ b/debian/cloudstack-common.install @@ -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 diff --git a/debian/cloudstack-management.install b/debian/cloudstack-management.install index 3d0d7e23814..b2a32bd93c1 100644 --- a/debian/cloudstack-management.install +++ b/debian/cloudstack-management.install @@ -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 diff --git a/debian/cloudstack-management.postinst b/debian/cloudstack-management.postinst index d5d50a4718c..fde3ba96de0 100755 --- a/debian/cloudstack-management.postinst +++ b/debian/cloudstack-management.postinst @@ -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 diff --git a/debian/rules b/debian/rules index d178afa6730..89943408544 100755 --- a/debian/rules +++ b/debian/rules @@ -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 diff --git a/deps/install-non-oss.sh b/deps/install-non-oss.sh index 56cfc2cba9c..e40946ce312 100755 --- a/deps/install-non-oss.sh +++ b/deps/install-non-oss.sh @@ -85,4 +85,7 @@ mvn install:install-file -Dfile=juniper-contrail-api-1.0-SNAPSHOT.jar -DgroupId= # From https://github.com/radu-todirica/tungsten-api/raw/master/net/juniper/tungsten/juniper-tungsten-api/2.0/juniper-tungsten-api-2.0.jar mvn install:install-file -Dfile=juniper-tungsten-api-2.0.jar -DgroupId=net.juniper.tungsten -DartifactId=juniper-tungsten-api -Dversion=2.0 -Dpackaging=jar +# Netris Integration +mvn install:install-file -Dfile=netris-java-sdk-1.0.0.jar -DgroupId=io.netris -DartifactId=netris-java-sdk -Dversion=1.0.0 -Dpackaging=jar + exit 0 diff --git a/engine/api/src/main/java/com/cloud/vm/VirtualMachineGuru.java b/engine/api/src/main/java/com/cloud/vm/VirtualMachineGuru.java index f8032bf4b0e..76f0830f369 100644 --- a/engine/api/src/main/java/com/cloud/vm/VirtualMachineGuru.java +++ b/engine/api/src/main/java/com/cloud/vm/VirtualMachineGuru.java @@ -24,6 +24,7 @@ import com.cloud.utils.PasswordGenerator; import com.cloud.utils.exception.CloudRuntimeException; import org.apache.cloudstack.ca.CAManager; import org.apache.cloudstack.framework.ca.Certificate; +import org.apache.cloudstack.framework.config.ConfigKey; import org.apache.cloudstack.utils.security.CertUtils; import org.apache.cloudstack.utils.security.KeyStoreUtils; @@ -37,6 +38,9 @@ import java.util.Base64; */ public interface VirtualMachineGuru { + static final ConfigKey NTPServerConfig = new ConfigKey(String.class, "ntp.server.list", "Advanced", null, + "Comma separated list of NTP servers to configure in System VMs", true, ConfigKey.Scope.Global, null, null, null, null, null, ConfigKey.Kind.CSV, null); + boolean finalizeVirtualMachineProfile(VirtualMachineProfile profile, DeployDestination dest, ReservationContext context); /** diff --git a/engine/api/src/main/java/com/cloud/vm/VirtualMachineManager.java b/engine/api/src/main/java/com/cloud/vm/VirtualMachineManager.java index 238a78e89af..7841eba524a 100644 --- a/engine/api/src/main/java/com/cloud/vm/VirtualMachineManager.java +++ b/engine/api/src/main/java/com/cloud/vm/VirtualMachineManager.java @@ -16,6 +16,8 @@ // under the License. package com.cloud.vm; +import com.cloud.storage.Snapshot; +import com.cloud.storage.Volume; import java.net.URI; import java.util.HashMap; import java.util.LinkedHashMap; @@ -129,11 +131,11 @@ public interface VirtualMachineManager extends Manager { * @throws InsufficientCapacityException If there are insufficient capacity to deploy this vm. */ void allocate(String vmInstanceName, VirtualMachineTemplate template, ServiceOffering serviceOffering, DiskOfferingInfo rootDiskOfferingInfo, - List dataDiskOfferings, LinkedHashMap> auxiliaryNetworks, DeploymentPlan plan, - HypervisorType hyperType, Map> extraDhcpOptions, Map datadiskTemplateToDiskOfferingMap) throws InsufficientCapacityException; + List dataDiskOfferings, LinkedHashMap> auxiliaryNetworks, DeploymentPlan plan, + HypervisorType hyperType, Map> extraDhcpOptions, Map datadiskTemplateToDiskOfferingMap, Volume volume, Snapshot snapshot) throws InsufficientCapacityException; void allocate(String vmInstanceName, VirtualMachineTemplate template, ServiceOffering serviceOffering, - LinkedHashMap> networkProfiles, DeploymentPlan plan, HypervisorType hyperType) throws InsufficientCapacityException; + LinkedHashMap> networkProfiles, DeploymentPlan plan, HypervisorType hyperType, Volume volume, Snapshot snapshot) throws InsufficientCapacityException; void start(String vmUuid, Map params); diff --git a/engine/api/src/main/java/org/apache/cloudstack/engine/orchestration/service/NetworkOrchestrationService.java b/engine/api/src/main/java/org/apache/cloudstack/engine/orchestration/service/NetworkOrchestrationService.java index 8463d9cee98..adce5f2f8b4 100644 --- a/engine/api/src/main/java/org/apache/cloudstack/engine/orchestration/service/NetworkOrchestrationService.java +++ b/engine/api/src/main/java/org/apache/cloudstack/engine/orchestration/service/NetworkOrchestrationService.java @@ -112,6 +112,9 @@ public interface NetworkOrchestrationService { static final ConfigKey NSX_ENABLED = new ConfigKey<>(Boolean.class, "nsx.plugin.enable", "Advanced", "false", "Indicates whether to enable the NSX plugin", false, ConfigKey.Scope.Zone, null); + ConfigKey NETRIS_ENABLED = new ConfigKey<>(Boolean.class, "netris.plugin.enable", "Advanced", "false", + "Indicates whether to enable the Netris plugin", false, ConfigKey.Scope.Zone, null); + List setupNetwork(Account owner, NetworkOffering offering, DeploymentPlan plan, String name, String displayText, boolean isDefault) throws ConcurrentOperationException; diff --git a/engine/api/src/main/java/org/apache/cloudstack/engine/orchestration/service/VolumeOrchestrationService.java b/engine/api/src/main/java/org/apache/cloudstack/engine/orchestration/service/VolumeOrchestrationService.java index afc33eb5190..ccb5bba1c0a 100644 --- a/engine/api/src/main/java/org/apache/cloudstack/engine/orchestration/service/VolumeOrchestrationService.java +++ b/engine/api/src/main/java/org/apache/cloudstack/engine/orchestration/service/VolumeOrchestrationService.java @@ -149,7 +149,7 @@ public interface VolumeOrchestrationService { * Allocate a volume or multiple volumes in case of template is registered with the 'deploy-as-is' option, allowing multiple disks */ List allocateTemplatedVolumes(Type type, String name, DiskOffering offering, Long rootDisksize, Long minIops, Long maxIops, VirtualMachineTemplate template, VirtualMachine vm, - Account owner); + Account owner, Volume volume, Snapshot snapshot); String getVmNameFromVolumeId(long volumeId); diff --git a/engine/api/src/main/java/org/apache/cloudstack/engine/service/api/OrchestrationService.java b/engine/api/src/main/java/org/apache/cloudstack/engine/service/api/OrchestrationService.java index 3ffa496b544..ffe85818fc4 100644 --- a/engine/api/src/main/java/org/apache/cloudstack/engine/service/api/OrchestrationService.java +++ b/engine/api/src/main/java/org/apache/cloudstack/engine/service/api/OrchestrationService.java @@ -18,6 +18,8 @@ */ package org.apache.cloudstack.engine.service.api; +import com.cloud.storage.Snapshot; +import com.cloud.storage.Volume; import java.net.URL; import java.util.List; import java.util.Map; @@ -62,12 +64,12 @@ public interface OrchestrationService { @POST @Path("/createvm") VirtualMachineEntity createVirtualMachine(@QueryParam("id") String id, @QueryParam("owner") String owner, @QueryParam("template-id") String templateId, - @QueryParam("host-name") String hostName, @QueryParam("display-name") String displayName, @QueryParam("hypervisor") String hypervisor, - @QueryParam("cpu") int cpu, @QueryParam("speed") int speed, @QueryParam("ram") long memory, @QueryParam("disk-size") Long diskSize, - @QueryParam("compute-tags") List computeTags, @QueryParam("root-disk-tags") List rootDiskTags, - @QueryParam("network-nic-map") Map> networkNicMap, @QueryParam("deploymentplan") DeploymentPlan plan, - @QueryParam("root-disk-size") Long rootDiskSize, @QueryParam("extra-dhcp-option-map") Map> extraDhcpOptionMap, - @QueryParam("datadisktemplate-diskoffering-map") Map datadiskTemplateToDiskOfferingMap, @QueryParam("disk-offering-id") Long diskOfferingId, @QueryParam("root-disk-offering-id") Long rootDiskOfferingId) throws InsufficientCapacityException; + @QueryParam("host-name") String hostName, @QueryParam("display-name") String displayName, @QueryParam("hypervisor") String hypervisor, + @QueryParam("cpu") int cpu, @QueryParam("speed") int speed, @QueryParam("ram") long memory, @QueryParam("disk-size") Long diskSize, + @QueryParam("compute-tags") List computeTags, @QueryParam("root-disk-tags") List rootDiskTags, + @QueryParam("network-nic-map") Map> networkNicMap, @QueryParam("deploymentplan") DeploymentPlan plan, + @QueryParam("root-disk-size") Long rootDiskSize, @QueryParam("extra-dhcp-option-map") Map> extraDhcpOptionMap, + @QueryParam("datadisktemplate-diskoffering-map") Map datadiskTemplateToDiskOfferingMap, @QueryParam("disk-offering-id") Long diskOfferingId, @QueryParam("root-disk-offering-id") Long rootDiskOfferingId, Volume volume, Snapshot snapshot) throws InsufficientCapacityException; @POST VirtualMachineEntity createVirtualMachineFromScratch(@QueryParam("id") String id, @QueryParam("owner") String owner, @QueryParam("iso-id") String isoId, @@ -75,7 +77,7 @@ public interface OrchestrationService { @QueryParam("os") String os, @QueryParam("cpu") int cpu, @QueryParam("speed") int speed, @QueryParam("ram") long memory, @QueryParam("disk-size") Long diskSize, @QueryParam("compute-tags") List computeTags, @QueryParam("root-disk-tags") List rootDiskTags, @QueryParam("network-nic-map") Map> networkNicMap, @QueryParam("deploymentplan") DeploymentPlan plan, - @QueryParam("extra-dhcp-option-map") Map> extraDhcpOptionMap, @QueryParam("disk-offering-id") Long diskOfferingId) throws InsufficientCapacityException; + @QueryParam("extra-dhcp-option-map") Map> extraDhcpOptionMap, @QueryParam("disk-offering-id") Long diskOfferingId, Volume volume, Snapshot snapshot) throws InsufficientCapacityException; @POST NetworkEntity createNetwork(String id, String name, String domainName, String cidr, String gateway); diff --git a/engine/components-api/src/main/java/com/cloud/configuration/ConfigurationManager.java b/engine/components-api/src/main/java/com/cloud/configuration/ConfigurationManager.java index 859d1176213..eebf764289d 100644 --- a/engine/components-api/src/main/java/com/cloud/configuration/ConfigurationManager.java +++ b/engine/components-api/src/main/java/com/cloud/configuration/ConfigurationManager.java @@ -65,6 +65,12 @@ public interface ConfigurationManager { "allow.non.rfc1918.compliant.ips", "Advanced", "false", "Allows non-compliant RFC 1918 IPs for Shared, Isolated networks and VPCs", true, null); + ConfigKey HostCapacityTypeCpuMemoryWeight = new ConfigKey<>(ConfigKey.CATEGORY_ADVANCED, Float.class, + "host.capacityType.to.order.clusters.cputomemoryweight", + "0.5", + "Weight for CPU (as a value between 0 and 1) applied to compute capacity for Pods, Clusters and Hosts for COMBINED capacityType for ordering. Weight for RAM will be (1 - weight of CPU)", + true, ConfigKey.Scope.Global); + /** * @param offering * @return @@ -217,6 +223,7 @@ public interface ConfigurationManager { * @param forVpc * @param forTungsten * @param forNsx + * @param forNetris * @param domainIds * @param zoneIds * @return network offering object @@ -226,11 +233,11 @@ public interface ConfigurationManager { Integer networkRate, Map> serviceProviderMap, boolean isDefault, Network.GuestType type, boolean systemOnly, Long serviceOfferingId, boolean conserveMode, Map> serviceCapabilityMap, boolean specifyIpRanges, boolean isPersistent, Map details, boolean egressDefaultPolicy, Integer maxconn, boolean enableKeepAlive, Boolean forVpc, - Boolean forTungsten, boolean forNsx, NetworkOffering.NetworkMode networkMode, List domainIds, List zoneIds, boolean enableOffering, final NetUtils.InternetProtocol internetProtocol, + Boolean forTungsten, boolean forNsx, boolean forNetris, NetworkOffering.NetworkMode networkMode, List domainIds, List zoneIds, boolean enableOffering, final NetUtils.InternetProtocol internetProtocol, NetworkOffering.RoutingMode routingMode, boolean specifyAsNumber); Vlan createVlanAndPublicIpRange(long zoneId, long networkId, long physicalNetworkId, boolean forVirtualNetwork, boolean forSystemVms, Long podId, String startIP, String endIP, - String vlanGateway, String vlanNetmask, String vlanId, boolean bypassVlanOverlapCheck, Domain domain, Account vlanOwner, String startIPv6, String endIPv6, String vlanIp6Gateway, String vlanIp6Cidr, boolean forNsx) + String vlanGateway, String vlanNetmask, String vlanId, boolean bypassVlanOverlapCheck, Domain domain, Account vlanOwner, String startIPv6, String endIPv6, String vlanIp6Gateway, String vlanIp6Cidr, Provider provider) throws InsufficientCapacityException, ConcurrentOperationException, InvalidParameterValueException; void createDefaultSystemNetworks(long zoneId) throws ConcurrentOperationException; diff --git a/engine/components-api/src/main/java/com/cloud/hypervisor/ExternalProvisioner.java b/engine/components-api/src/main/java/com/cloud/hypervisor/ExternalProvisioner.java new file mode 100644 index 00000000000..a22ea421113 --- /dev/null +++ b/engine/components-api/src/main/java/com/cloud/hypervisor/ExternalProvisioner.java @@ -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 getHostVmStateReport(long hostId, String extensionName, String extensionRelativePath); + + RunCustomActionAnswer runCustomAction(String hostGuid, String extensionName, String extensionRelativePath, RunCustomActionCommand cmd); +} diff --git a/engine/components-api/src/main/java/com/cloud/network/addr/PublicIp.java b/engine/components-api/src/main/java/com/cloud/network/addr/PublicIp.java index d69a72a02c5..7132094ce11 100644 --- a/engine/components-api/src/main/java/com/cloud/network/addr/PublicIp.java +++ b/engine/components-api/src/main/java/com/cloud/network/addr/PublicIp.java @@ -275,5 +275,9 @@ public class PublicIp implements PublicIpAddress { return false; } + @Override + public boolean isForRouter() { + return _addr.isForRouter(); + } } diff --git a/engine/components-api/src/main/java/com/cloud/network/rules/RulesManager.java b/engine/components-api/src/main/java/com/cloud/network/rules/RulesManager.java index 79ffdfdb973..5ba74402a4c 100644 --- a/engine/components-api/src/main/java/com/cloud/network/rules/RulesManager.java +++ b/engine/components-api/src/main/java/com/cloud/network/rules/RulesManager.java @@ -54,6 +54,8 @@ public interface RulesManager extends RulesService { boolean disableStaticNat(long ipAddressId, Account caller, long callerUserId, boolean releaseIpIfElastic) throws ResourceUnavailableException; + boolean applyStaticNatForIp(long sourceIpId, boolean continueOnError, Account caller, boolean forRevoke); + /** * @param networkId * @param continueOnError diff --git a/engine/components-api/src/main/java/com/cloud/network/vpc/NetworkACLManager.java b/engine/components-api/src/main/java/com/cloud/network/vpc/NetworkACLManager.java index de69b894183..b192cd8d71c 100644 --- a/engine/components-api/src/main/java/com/cloud/network/vpc/NetworkACLManager.java +++ b/engine/components-api/src/main/java/com/cloud/network/vpc/NetworkACLManager.java @@ -93,5 +93,5 @@ public interface NetworkACLManager { boolean applyACLToPrivateGw(PrivateGateway gateway) throws ResourceUnavailableException; - boolean reorderAclRules(VpcVO vpc, List networks, List networkACLItems); + boolean reorderAclRules(VpcVO vpc, List networks, List networkACLItems, Network.Provider networkProvider); } diff --git a/engine/components-api/src/main/java/com/cloud/network/vpc/VpcManager.java b/engine/components-api/src/main/java/com/cloud/network/vpc/VpcManager.java index 626c4da1bb1..e7f41d079a7 100644 --- a/engine/components-api/src/main/java/com/cloud/network/vpc/VpcManager.java +++ b/engine/components-api/src/main/java/com/cloud/network/vpc/VpcManager.java @@ -195,4 +195,20 @@ public interface VpcManager { * @return */ boolean isSrcNatIpRequired(long vpcOfferingId); + + boolean isSrcNatIpRequiredForVpcVr(long vpcOfferingId); + + List getVpcStaticRoutes(Long vpcId); + + List getVpcStaticRoutes(List routes); + + boolean isProviderSupportServiceInVpc(long vpcId, Service service, Provider provider); + + IPAddressVO getIpAddressForVpcVr(Vpc vpc, IPAddressVO ipAddress, boolean allocateIpIfNeeded); + + boolean configStaticNatForVpcVr(Vpc vpc, IPAddressVO ipAddress); + + void reconfigStaticNatForVpcVr(Long vpcId); + + boolean applyStaticRouteForVpcVpnIfNeeded(Long vpcId, boolean updateAllVpn) throws ResourceUnavailableException; } diff --git a/engine/components-api/src/main/java/com/cloud/resource/Discoverer.java b/engine/components-api/src/main/java/com/cloud/resource/Discoverer.java index a2bb5945a9d..60b0167758c 100644 --- a/engine/components-api/src/main/java/com/cloud/resource/Discoverer.java +++ b/engine/components-api/src/main/java/com/cloud/resource/Discoverer.java @@ -50,5 +50,4 @@ public interface Discoverer extends Adapter { public void putParam(Map params); ServerResource reloadResource(HostVO host); - } diff --git a/engine/components-api/src/main/java/com/cloud/resource/ResourceManager.java b/engine/components-api/src/main/java/com/cloud/resource/ResourceManager.java index 83f9768a62a..8550dfdd906 100755 --- a/engine/components-api/src/main/java/com/cloud/resource/ResourceManager.java +++ b/engine/components-api/src/main/java/com/cloud/resource/ResourceManager.java @@ -21,8 +21,12 @@ import java.util.HashMap; import java.util.List; import java.util.Map; + +import com.cloud.offering.ServiceOffering; import org.apache.cloudstack.engine.subsystem.api.storage.DataStore; import org.apache.cloudstack.engine.subsystem.api.storage.PrimaryDataStoreInfo; +import com.cloud.gpu.VgpuProfileVO; +import com.cloud.vm.VirtualMachine; import org.apache.cloudstack.framework.config.ConfigKey; import org.apache.cloudstack.framework.config.Configurable; @@ -194,29 +198,34 @@ public interface ResourceManager extends ResourceService, Configurable { */ boolean isHostGpuEnabled(long hostId); - /** - * Check if host has GPU devices available - * @param hostId the host to be checked - * @param groupName: gpuCard name - * @param vgpuType the VGPU type - * @return true when the host has the capacity with given VGPU type - */ - boolean isGPUDeviceAvailable(Host host, String groupName, String vgpuType); + boolean isGPUDeviceAvailable(ServiceOffering offering, Host host, Long vmId); /** * Get available GPU device - * @param hostId the host to be checked - * @param groupName: gpuCard name - * @param vgpuType the VGPU type + * + * @param vm the vm for which GPU device is requested + * @param vgpuProfile the VGPU profile + * @param gpuCount + * @return GPUDeviceTO[] + */ + GPUDeviceTO getGPUDevice(VirtualMachine vm, long hostId, VgpuProfileVO vgpuProfile, int gpuCount); + + /** + * Get available GPU device + * + * @param hostId the host to be checked + * @param groupName gpuCard name + * @param vgpuType the VGPU type * @return GPUDeviceTO[] */ GPUDeviceTO getGPUDevice(long hostId, String groupName, String vgpuType); /** * Return listof available GPU devices - * @param hostId, the host to be checked - * @param groupName: gpuCard name - * @param vgpuType the VGPU type + * + * @param hostId the host to be checked + * @param groupName gpuCard name + * @param vgpuType the VGPU type * @return List of HostGpuGroupsVO. */ List listAvailableGPUDevice(long hostId, String groupName, String vgpuType); @@ -228,6 +237,16 @@ public interface ResourceManager extends ResourceService, Configurable { */ void updateGPUDetails(long hostId, HashMap> groupDetails); + /** + * Update GPU device details (post VM deployment) + * + * @param vm the VirtualMachine object + * @param gpuDeviceTO GPU device details + */ + void updateGPUDetailsForVmStop(VirtualMachine vm, GPUDeviceTO gpuDeviceTO); + + void updateGPUDetailsForVmStart(long hostId, long vmId, GPUDeviceTO gpuDevice); + /** * Get GPU details for a host * @param host, the Host object diff --git a/engine/components-api/src/main/java/com/cloud/template/TemplateManager.java b/engine/components-api/src/main/java/com/cloud/template/TemplateManager.java index b8912526fdf..28f41c677cb 100644 --- a/engine/components-api/src/main/java/com/cloud/template/TemplateManager.java +++ b/engine/components-api/src/main/java/com/cloud/template/TemplateManager.java @@ -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 getTemplateDisksOnImageStore(VirtualMachineTemplate template, DataStoreRole role, String configurationId); diff --git a/engine/components-api/src/main/java/com/cloud/vm/VirtualMachineProfileImpl.java b/engine/components-api/src/main/java/com/cloud/vm/VirtualMachineProfileImpl.java index a1c54b90328..29b96dcc917 100644 --- a/engine/components-api/src/main/java/com/cloud/vm/VirtualMachineProfileImpl.java +++ b/engine/components-api/src/main/java/com/cloud/vm/VirtualMachineProfileImpl.java @@ -40,7 +40,7 @@ public class VirtualMachineProfileImpl implements VirtualMachineProfile { VirtualMachine _vm; ServiceOffering _offering; VirtualMachineTemplate _template; - UserVmDetailVO _userVmDetails; + VMInstanceDetailVO _userVmDetails; Map _params; List _nics = new ArrayList(); List _disks = new ArrayList(); diff --git a/engine/orchestration/pom.xml b/engine/orchestration/pom.xml index 437c98dac87..151f95ff944 100755 --- a/engine/orchestration/pom.xml +++ b/engine/orchestration/pom.xml @@ -73,6 +73,11 @@ cloud-plugin-maintenance ${project.version} + + org.apache.cloudstack + cloud-plugin-hypervisor-external + ${project.version} + diff --git a/engine/orchestration/src/main/java/com/cloud/agent/manager/AgentManagerImpl.java b/engine/orchestration/src/main/java/com/cloud/agent/manager/AgentManagerImpl.java index dc7852ed82b..75e9fb20e5a 100644 --- a/engine/orchestration/src/main/java/com/cloud/agent/manager/AgentManagerImpl.java +++ b/engine/orchestration/src/main/java/com/cloud/agent/manager/AgentManagerImpl.java @@ -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 { diff --git a/engine/orchestration/src/main/java/com/cloud/agent/manager/ClusteredAgentManagerImpl.java b/engine/orchestration/src/main/java/com/cloud/agent/manager/ClusteredAgentManagerImpl.java index a7dca34f032..c6448982803 100644 --- a/engine/orchestration/src/main/java/com/cloud/agent/manager/ClusteredAgentManagerImpl.java +++ b/engine/orchestration/src/main/java/com/cloud/agent/manager/ClusteredAgentManagerImpl.java @@ -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 { diff --git a/engine/orchestration/src/main/java/com/cloud/agent/manager/DirectAgentAttache.java b/engine/orchestration/src/main/java/com/cloud/agent/manager/DirectAgentAttache.java index 81148c5db30..ed18d1e82b7 100644 --- a/engine/orchestration/src/main/java/com/cloud/agent/manager/DirectAgentAttache.java +++ b/engine/orchestration/src/main/java/com/cloud/agent/manager/DirectAgentAttache.java @@ -23,6 +23,7 @@ import java.util.concurrent.ScheduledFuture; import java.util.concurrent.TimeUnit; import java.util.concurrent.atomic.AtomicInteger; +import com.cloud.utils.ThreadUtil; import org.apache.cloudstack.framework.config.ConfigKey; import org.apache.cloudstack.managed.context.ManagedContextRunnable; @@ -166,7 +167,7 @@ public class DirectAgentAttache extends AgentAttache { PingCommand cmd = resource.getCurrentStatus(_id); int retried = 0; while (cmd == null && ++retried <= _HostPingRetryCount.value()) { - Thread.sleep(1000*_HostPingRetryTimer.value()); + ThreadUtil.wait(this, 1000L *_HostPingRetryTimer.value(), _id, _uuid, _name); cmd = resource.getCurrentStatus(_id); } diff --git a/engine/orchestration/src/main/java/com/cloud/vm/VirtualMachineManagerImpl.java b/engine/orchestration/src/main/java/com/cloud/vm/VirtualMachineManagerImpl.java index 13475579f77..3cd8ec0aae3 100755 --- a/engine/orchestration/src/main/java/com/cloud/vm/VirtualMachineManagerImpl.java +++ b/engine/orchestration/src/main/java/com/cloud/vm/VirtualMachineManagerImpl.java @@ -49,6 +49,7 @@ import javax.inject.Inject; import javax.naming.ConfigurationException; import javax.persistence.EntityExistsException; + import org.apache.cloudstack.affinity.dao.AffinityGroupVMMapDao; import org.apache.cloudstack.annotation.AnnotationService; import org.apache.cloudstack.annotation.dao.AnnotationDao; @@ -71,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; @@ -83,6 +87,7 @@ import org.apache.cloudstack.framework.jobs.impl.VmWorkJobVO; import org.apache.cloudstack.framework.messagebus.MessageBus; import org.apache.cloudstack.framework.messagebus.MessageDispatcher; import org.apache.cloudstack.framework.messagebus.MessageHandler; +import org.apache.cloudstack.gpu.GpuService; import org.apache.cloudstack.jobs.JobInfo; import org.apache.cloudstack.managed.context.ManagedContextRunnable; import org.apache.cloudstack.reservation.dao.ReservationDao; @@ -98,6 +103,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; @@ -122,6 +128,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; @@ -202,12 +210,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; @@ -230,6 +240,7 @@ import com.cloud.service.ServiceOfferingVO; import com.cloud.service.dao.ServiceOfferingDao; import com.cloud.storage.DiskOfferingVO; import com.cloud.storage.ScopeType; +import com.cloud.storage.Snapshot; import com.cloud.storage.Storage; import com.cloud.storage.Storage.ImageFormat; import com.cloud.storage.StorageManager; @@ -284,13 +295,14 @@ import com.cloud.vm.VirtualMachine.PowerState; import com.cloud.vm.VirtualMachine.State; import com.cloud.vm.dao.NicDao; import com.cloud.vm.dao.UserVmDao; -import com.cloud.vm.dao.UserVmDetailsDao; +import com.cloud.vm.dao.VMInstanceDetailsDao; import com.cloud.vm.dao.VMInstanceDao; import com.cloud.vm.snapshot.VMSnapshotManager; import com.cloud.vm.snapshot.VMSnapshotVO; import com.cloud.vm.snapshot.dao.VMSnapshotDao; import com.google.gson.Gson; + public class VirtualMachineManagerImpl extends ManagerBase implements VirtualMachineManager, VmWorkJobHandler, Listener, Configurable { public static final String VM_WORK_JOB_HANDLER = VirtualMachineManagerImpl.class.getSimpleName(); @@ -332,6 +344,8 @@ public class VirtualMachineManagerImpl extends ManagerBase implements VirtualMac @Inject private HostDao _hostDao; @Inject + private HostDetailsDao hostDetailsDao; + @Inject private AlertManager _alertMgr; @Inject private GuestOSCategoryDao _guestOsCategoryDao; @@ -378,10 +392,12 @@ public class VirtualMachineManagerImpl extends ManagerBase implements VirtualMac @Inject private ClusterDetailsDao _clusterDetailsDao; @Inject - private UserVmDetailsDao userVmDetailsDao; + private VMInstanceDetailsDao vmInstanceDetailsDao; @Inject private VolumeOrchestrationService volumeMgr; @Inject + private GpuService gpuService; + @Inject private DeploymentPlanningManager _dpMgr; @Inject private MessageBus _messageBus; @@ -414,6 +430,8 @@ public class VirtualMachineManagerImpl extends ManagerBase implements VirtualMac @Inject private DomainDao domainDao; @Inject + public NetworkService networkService; + @Inject ResourceCleanupService resourceCleanupService; @Inject VmWorkJobDao vmWorkJobDao; @@ -430,6 +448,10 @@ public class VirtualMachineManagerImpl extends ManagerBase implements VirtualMac @Inject private VolumeDataFactory volumeDataFactory; + @Inject + ExtensionsManager extensionsManager; + @Inject + ExtensionDetailsDao extensionDetailsDao; VmWorkJobHandlerProxy _jobHandlerProxy = new VmWorkJobHandlerProxy(this); @@ -503,8 +525,8 @@ public class VirtualMachineManagerImpl extends ManagerBase implements VirtualMac @Override @DB public void allocate(final String vmInstanceName, final VirtualMachineTemplate template, final ServiceOffering serviceOffering, - final DiskOfferingInfo rootDiskOfferingInfo, final List dataDiskOfferings, - final LinkedHashMap> auxiliaryNetworks, final DeploymentPlan plan, final HypervisorType hyperType, final Map> extraDhcpOptions, final Map datadiskTemplateToDiskOfferingMap) + final DiskOfferingInfo rootDiskOfferingInfo, final List dataDiskOfferings, + final LinkedHashMap> auxiliaryNetworks, final DeploymentPlan plan, final HypervisorType hyperType, final Map> extraDhcpOptions, final Map datadiskTemplateToDiskOfferingMap, Volume volume, Snapshot snapshot) throws InsufficientCapacityException { logger.info("allocating virtual machine from template: {} with hostname: {} and {} networks", template, vmInstanceName, auxiliaryNetworks.size()); @@ -542,7 +564,7 @@ public class VirtualMachineManagerImpl extends ManagerBase implements VirtualMac logger.debug("Allocating disks for {}", persistedVm); - allocateRootVolume(persistedVm, template, rootDiskOfferingInfo, owner, rootDiskSizeFinal); + allocateRootVolume(persistedVm, template, rootDiskOfferingInfo, owner, rootDiskSizeFinal, volume, snapshot); // Create new Volume context and inject event resource type, id and details to generate VOLUME.CREATE event for the ROOT disk. CallContext volumeContext = CallContext.register(CallContext.current(), ApiCommandResourceType.Volume); @@ -583,7 +605,7 @@ public class VirtualMachineManagerImpl extends ManagerBase implements VirtualMac } } - private void allocateRootVolume(VMInstanceVO vm, VirtualMachineTemplate template, DiskOfferingInfo rootDiskOfferingInfo, Account owner, Long rootDiskSizeFinal) { + private void allocateRootVolume(VMInstanceVO vm, VirtualMachineTemplate template, DiskOfferingInfo rootDiskOfferingInfo, Account owner, Long rootDiskSizeFinal, Volume volume, Snapshot snapshot) { // Create new Volume context and inject event resource type, id and details to generate VOLUME.CREATE event for the ROOT disk. CallContext volumeContext = CallContext.register(CallContext.current(), ApiCommandResourceType.Volume); try { @@ -591,11 +613,11 @@ 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); + rootDiskOfferingInfo.getMinIops(), rootDiskOfferingInfo.getMaxIops(), template, vm, owner, volume, snapshot); } } finally { // Remove volumeContext and pop vmContext back @@ -605,9 +627,9 @@ public class VirtualMachineManagerImpl extends ManagerBase implements VirtualMac @Override public void allocate(final String vmInstanceName, final VirtualMachineTemplate template, final ServiceOffering serviceOffering, - final LinkedHashMap> networks, final DeploymentPlan plan, final HypervisorType hyperType) throws InsufficientCapacityException { + final LinkedHashMap> networks, final DeploymentPlan plan, final HypervisorType hyperType, Volume volume, Snapshot snapshot) throws InsufficientCapacityException { DiskOffering diskOffering = _diskOfferingDao.findById(serviceOffering.getDiskOfferingId()); - allocate(vmInstanceName, template, serviceOffering, new DiskOfferingInfo(diskOffering), new ArrayList<>(), networks, plan, hyperType, null, null); + allocate(vmInstanceName, template, serviceOffering, new DiskOfferingInfo(diskOffering), new ArrayList<>(), networks, plan, hyperType, null, null, volume, snapshot); } VirtualMachineGuru getVmGuru(final VirtualMachine vm) { @@ -652,6 +674,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()); @@ -1144,6 +1173,141 @@ public class VirtualMachineManagerImpl extends ManagerBase implements VirtualMac vmTO.setMetadataProductName(metadataProduct); } + protected void updateExternalVmDetailsFromPrepareAnswer(VirtualMachineTO vmTO, UserVmVO userVmVO, + Map 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 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 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 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 vmDetails = virtualMachineTO.getExternalDetails(); + Map> 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 params, final DeploymentPlan planToDeploy, final DeploymentPlanner planner) throws InsufficientCapacityException, ConcurrentOperationException, ResourceUnavailableException { @@ -1155,6 +1319,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()); @@ -1254,7 +1420,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 { @@ -1297,8 +1463,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); } @@ -1317,13 +1486,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 sshAccessDetails = _networkMgr.getSystemVMAccessDetails(vm); final Map 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); @@ -1364,11 +1533,11 @@ public class VirtualMachineManagerImpl extends ManagerBase implements VirtualMac final GPUDeviceTO gpuDevice = startAnswer.getVirtualMachine().getGpuDevice(); if (gpuDevice != null) { - _resourceMgr.updateGPUDetails(destHostId, gpuDevice.getGroupDetails()); + _resourceMgr.updateGPUDetailsForVmStart(destHostId, vm.getId(), gpuDevice); } - if (userVmDetailsDao.findDetail(vm.getId(), VmDetailConstants.DEPLOY_VM) != null) { - userVmDetailsDao.removeDetail(vm.getId(), VmDetailConstants.DEPLOY_VM); + if (vmInstanceDetailsDao.findDetail(vm.getId(), VmDetailConstants.DEPLOY_VM) != null) { + vmInstanceDetailsDao.removeDetail(vm.getId(), VmDetailConstants.DEPLOY_VM); } startedVm = vm; @@ -1496,6 +1665,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 vmExternalDetails = vmTO.getExternalDetails(); + for (NicTO nic : vmTO.getNics()) { + if (!nic.isDefaultNic()) { + continue; + } + vmExternalDetails.put(VmDetailConstants.CLOUDSTACK_VLAN, networkService.getNicVlanValueForExternalVm(nic)); + } + Map> 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> 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> externalDetails = extensionsManager.getExternalAccessDetails(host, vmTO.getExternalDetails()); + rebootCmd.setExternalDetails(externalDetails); + } + public void setVmNetworkDetails(VMInstanceVO vm, VirtualMachineTO vmTO) { Map networkToNetworkNameMap = new HashMap<>(); if (VirtualMachine.Type.User.equals(vm.getType())) { @@ -1550,16 +1766,16 @@ public class VirtualMachineManagerImpl extends ManagerBase implements VirtualMac final ClusterDetailsVO clusterDetailRam = _clusterDetailsDao.findDetail(clusterId, VmDetailConstants.MEMORY_OVER_COMMIT_RATIO); final float parsedClusterCpuDetailCpu = Float.parseFloat(clusterDetailCpu.getValue()); final float parsedClusterDetailRam = Float.parseFloat(clusterDetailRam.getValue()); - UserVmDetailVO vmDetailCpu = userVmDetailsDao.findDetail(vmProfile.getId(), VmDetailConstants.CPU_OVER_COMMIT_RATIO); - UserVmDetailVO vmDetailRam = userVmDetailsDao.findDetail(vmProfile.getId(), VmDetailConstants.MEMORY_OVER_COMMIT_RATIO); + VMInstanceDetailVO vmDetailCpu = vmInstanceDetailsDao.findDetail(vmProfile.getId(), VmDetailConstants.CPU_OVER_COMMIT_RATIO); + VMInstanceDetailVO vmDetailRam = vmInstanceDetailsDao.findDetail(vmProfile.getId(), VmDetailConstants.MEMORY_OVER_COMMIT_RATIO); if ((vmDetailCpu == null && parsedClusterCpuDetailCpu > 1f) || (vmDetailCpu != null && Float.parseFloat(vmDetailCpu.getValue()) != parsedClusterCpuDetailCpu)) { - userVmDetailsDao.addDetail(vmProfile.getId(), VmDetailConstants.CPU_OVER_COMMIT_RATIO, clusterDetailCpu.getValue(), true); + vmInstanceDetailsDao.addDetail(vmProfile.getId(), VmDetailConstants.CPU_OVER_COMMIT_RATIO, clusterDetailCpu.getValue(), true); } if ((vmDetailRam == null && parsedClusterDetailRam > 1f) || (vmDetailRam != null && Float.parseFloat(vmDetailRam.getValue()) != parsedClusterDetailRam)) { - userVmDetailsDao.addDetail(vmProfile.getId(), VmDetailConstants.MEMORY_OVER_COMMIT_RATIO, clusterDetailRam.getValue(), true); + vmInstanceDetailsDao.addDetail(vmProfile.getId(), VmDetailConstants.MEMORY_OVER_COMMIT_RATIO, clusterDetailRam.getValue(), true); } vmProfile.setCpuOvercommitRatio(Float.parseFloat(clusterDetailCpu.getValue())); @@ -1883,7 +2099,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 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); } @@ -1908,9 +2126,7 @@ public class VirtualMachineManagerImpl extends ManagerBase implements VirtualMac } final GPUDeviceTO gpuDevice = stop.getGpuDevice(); - if (gpuDevice != null) { - _resourceMgr.updateGPUDetails(vm.getHostId(), gpuDevice.getGroupDetails()); - } + _resourceMgr.updateGPUDetailsForVmStop(vm, gpuDevice); if (!answer.getResult()) { final String details = answer.getDetails(); logger.debug("Unable to stop VM due to {}", details); @@ -2014,7 +2230,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); } @@ -2211,6 +2427,7 @@ public class VirtualMachineManagerImpl extends ManagerBase implements VirtualMac Map 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); } @@ -2238,9 +2455,7 @@ public class VirtualMachineManagerImpl extends ManagerBase implements VirtualMac } vmGuru.finalizeStop(profile, answer); final GPUDeviceTO gpuDevice = stop.getGpuDevice(); - if (gpuDevice != null) { - _resourceMgr.updateGPUDetails(vm.getHostId(), gpuDevice.getGroupDetails()); - } + _resourceMgr.updateGPUDetailsForVmStop(vm, gpuDevice); } else { throw new CloudRuntimeException("Invalid answer received in response to a StopCommand on " + vm.instanceName); } @@ -2260,6 +2475,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())) { @@ -2335,6 +2558,7 @@ public class VirtualMachineManagerImpl extends ManagerBase implements VirtualMac _reservationDao.setResourceId(Resource.ResourceType.user_vm, null); _reservationDao.setResourceId(Resource.ResourceType.cpu, null); _reservationDao.setResourceId(Resource.ResourceType.memory, null); + _reservationDao.setResourceId(Resource.ResourceType.gpu, null); } return _stateMachine.transitTo(vm, e, new Pair<>(vm.getHostId(), hostId), _vmDao); } @@ -2353,6 +2577,8 @@ public class VirtualMachineManagerImpl extends ManagerBase implements VirtualMac deleteVMSnapshots(vm, expunge); + gpuService.deallocateAllGpuDevicesForVm(vm.getId()); + Transaction.execute(new TransactionCallbackWithExceptionNoReturn() { @Override public void doInTransactionWithoutResult(final TransactionStatus status) throws CloudRuntimeException { @@ -2923,6 +3149,8 @@ public class VirtualMachineManagerImpl extends ManagerBase implements VirtualMac logger.info("Migration was unsuccessful. Cleaning up: {}", vm); _networkMgr.rollbackNicForMigration(vmSrc, profile); volumeMgr.release(vm.getId(), dstHostId); + // deallocate GPU devices for the VM on the destination host + gpuService.deallocateGpuDevicesForVmOnHost(vm.getId(), dstHostId); _alertMgr.sendAlert(alertType, fromHost.getDataCenterId(), fromHost.getPodId(), "Unable to migrate vm " + vm.getInstanceName() + " from host " + fromHost.getName() + " in zone " + dest.getDataCenter().getName() + " and pod " + @@ -2941,6 +3169,8 @@ public class VirtualMachineManagerImpl extends ManagerBase implements VirtualMac } else { _networkMgr.commitNicForMigration(vmSrc, profile); volumeMgr.release(vm.getId(), srcHostId); + // deallocate GPU devices for the VM on the src host after migration is complete + gpuService.deallocateGpuDevicesForVmOnHost(vm.getId(), srcHostId); _networkMgr.setHypervisorHostname(profile, dest, true); recreateCheckpointsKvmOnVmAfterMigration(vm, dstHostId); @@ -3321,7 +3551,7 @@ public class VirtualMachineManagerImpl extends ManagerBase implements VirtualMac if (defaultNic != null && VirtualMachine.Type.User.equals(vm.getType())) { UserVmVO userVm = _userVmDao.findById(vm.getId()); - Map details = userVmDetailsDao.listDetailsKeyPairs(vm.getId()); + Map details = vmInstanceDetailsDao.listDetailsKeyPairs(vm.getId()); userVm.setDetails(details); Network network = _networkModel.getNetwork(defaultNic.getNetworkId()); @@ -3727,6 +3957,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); @@ -3738,6 +3969,9 @@ public class VirtualMachineManagerImpl extends ManagerBase implements VirtualMac affectedVms.add(vm.getId()); _securityGroupManager.scheduleRulesetUpdateToHosts(affectedVms, true, null); } + if (vmTo.getGpuDevice() != null) { + _resourceMgr.updateGPUDetailsForVmStart(host.getId(), vm.getId(), vmTo.getGpuDevice()); + } return; } @@ -4839,29 +5073,29 @@ public class VirtualMachineManagerImpl extends ManagerBase implements VirtualMac } private void removeCustomOfferingDetails(long vmId) { - Map details = userVmDetailsDao.listDetailsKeyPairs(vmId); + Map details = vmInstanceDetailsDao.listDetailsKeyPairs(vmId); details.remove(UsageEventVO.DynamicParameters.cpuNumber.name()); details.remove(UsageEventVO.DynamicParameters.cpuSpeed.name()); details.remove(UsageEventVO.DynamicParameters.memory.name()); - List detailList = new ArrayList<>(); + List detailList = new ArrayList<>(); for(Map.Entry entry: details.entrySet()) { - UserVmDetailVO detailVO = new UserVmDetailVO(vmId, entry.getKey(), entry.getValue(), true); + VMInstanceDetailVO detailVO = new VMInstanceDetailVO(vmId, entry.getKey(), entry.getValue(), true); detailList.add(detailVO); } - userVmDetailsDao.saveDetails(detailList); + vmInstanceDetailsDao.saveDetails(detailList); } private void saveCustomOfferingDetails(long vmId, ServiceOffering serviceOffering) { - Map details = userVmDetailsDao.listDetailsKeyPairs(vmId); + Map details = vmInstanceDetailsDao.listDetailsKeyPairs(vmId); details.put(UsageEventVO.DynamicParameters.cpuNumber.name(), serviceOffering.getCpu().toString()); details.put(UsageEventVO.DynamicParameters.cpuSpeed.name(), serviceOffering.getSpeed().toString()); details.put(UsageEventVO.DynamicParameters.memory.name(), serviceOffering.getRamSize().toString()); - List detailList = new ArrayList<>(); + List detailList = new ArrayList<>(); for (Map.Entry entry: details.entrySet()) { - UserVmDetailVO detailVO = new UserVmDetailVO(vmId, entry.getKey(), entry.getValue(), true); + VMInstanceDetailVO detailVO = new VMInstanceDetailVO(vmId, entry.getKey(), entry.getValue(), true); detailList.add(detailVO); } - userVmDetailsDao.saveDetails(detailList); + vmInstanceDetailsDao.saveDetails(detailList); } @Override @@ -6179,7 +6413,7 @@ public class VirtualMachineManagerImpl extends ManagerBase implements VirtualMac @Override public Map getDiskOfferingSuitabilityForVm(long vmId, List diskOfferingIds) { VMInstanceVO vm = _vmDao.findById(vmId); - if (userVmDetailsDao.findDetail(vm.getId(), VmDetailConstants.DEPLOY_VM) != null) { + if (vmInstanceDetailsDao.findDetail(vm.getId(), VmDetailConstants.DEPLOY_VM) != null) { return new HashMap<>(); } VirtualMachineProfile profile = new VirtualMachineProfileImpl(vm); diff --git a/engine/orchestration/src/main/java/org/apache/cloudstack/engine/orchestration/CloudOrchestrator.java b/engine/orchestration/src/main/java/org/apache/cloudstack/engine/orchestration/CloudOrchestrator.java index 6763a13aed6..c9af76fbddd 100644 --- a/engine/orchestration/src/main/java/org/apache/cloudstack/engine/orchestration/CloudOrchestrator.java +++ b/engine/orchestration/src/main/java/org/apache/cloudstack/engine/orchestration/CloudOrchestrator.java @@ -18,6 +18,9 @@ */ package org.apache.cloudstack.engine.orchestration; +import com.cloud.storage.Snapshot; +import com.cloud.storage.Volume; +import com.cloud.template.VirtualMachineTemplate; import java.net.URL; import java.util.ArrayList; import java.util.LinkedHashMap; @@ -58,7 +61,7 @@ import com.cloud.vm.NicProfile; import com.cloud.vm.VMInstanceVO; import com.cloud.vm.VirtualMachineManager; import com.cloud.vm.dao.UserVmDao; -import com.cloud.vm.dao.UserVmDetailsDao; +import com.cloud.vm.dao.VMInstanceDetailsDao; import com.cloud.vm.dao.VMInstanceDao; import static org.apache.cloudstack.api.ApiConstants.MAX_IOPS; @@ -83,7 +86,7 @@ public class CloudOrchestrator implements OrchestrationService { protected UserVmDao _userVmDao = null; @Inject - protected UserVmDetailsDao _userVmDetailsDao = null; + protected VMInstanceDetailsDao _vmInstanceDetailsDao = null; @Inject protected ServiceOfferingDao _serviceOfferingDao; @@ -158,8 +161,8 @@ public class CloudOrchestrator implements OrchestrationService { @Override public VirtualMachineEntity createVirtualMachine(String id, String owner, String templateId, String hostName, String displayName, String hypervisor, int cpu, - int speed, long memory, Long diskSize, List computeTags, List rootDiskTags, Map> networkNicMap, DeploymentPlan plan, - Long rootDiskSize, Map> extraDhcpOptionMap, Map dataDiskTemplateToDiskOfferingMap, Long dataDiskOfferingId, Long rootDiskOfferingId) throws InsufficientCapacityException { + int speed, long memory, Long diskSize, List computeTags, List rootDiskTags, Map> networkNicMap, DeploymentPlan plan, + Long rootDiskSize, Map> extraDhcpOptionMap, Map dataDiskTemplateToDiskOfferingMap, Long dataDiskOfferingId, Long rootDiskOfferingId, Volume volume, Snapshot snapshot) throws InsufficientCapacityException { // VirtualMachineEntityImpl vmEntity = new VirtualMachineEntityImpl(id, owner, hostName, displayName, cpu, speed, memory, computeTags, rootDiskTags, networks, // vmEntityManager); @@ -196,7 +199,7 @@ public class CloudOrchestrator implements OrchestrationService { rootDiskOfferingInfo.setSize(rootDiskSize); if (rootDiskOffering.isCustomizedIops() != null && rootDiskOffering.isCustomizedIops()) { - Map userVmDetails = _userVmDetailsDao.listDetailsKeyPairs(vm.getId()); + Map userVmDetails = _vmInstanceDetailsDao.listDetailsKeyPairs(vm.getId()); if (userVmDetails != null) { String minIops = userVmDetails.get(MIN_IOPS); @@ -228,7 +231,7 @@ public class CloudOrchestrator implements OrchestrationService { dataDiskOfferingInfo.setSize(size); if (diskOffering.isCustomizedIops() != null && diskOffering.isCustomizedIops()) { - Map userVmDetails = _userVmDetailsDao.listDetailsKeyPairs(vm.getId()); + Map userVmDetails = _vmInstanceDetailsDao.listDetailsKeyPairs(vm.getId()); if (userVmDetails != null) { String minIops = userVmDetails.get("minIopsDo"); @@ -254,9 +257,13 @@ public class CloudOrchestrator implements OrchestrationService { } } } - - _itMgr.allocate(vm.getInstanceName(), _templateDao.findById(new Long(templateId)), computeOffering, rootDiskOfferingInfo, dataDiskOfferings, networkIpMap, plan, - hypervisorType, extraDhcpOptionMap, dataDiskTemplateToDiskOfferingMap); + VirtualMachineTemplate template = null; + if (volume != null || snapshot != null) { + template = _templateDao.findByIdIncludingRemoved(new Long(templateId)); + } else + template = _templateDao.findById(new Long(templateId)); + _itMgr.allocate(vm.getInstanceName(), template, computeOffering, rootDiskOfferingInfo, dataDiskOfferings, networkIpMap, plan, + hypervisorType, extraDhcpOptionMap, dataDiskTemplateToDiskOfferingMap, volume, snapshot); return vmEntity; } @@ -264,7 +271,7 @@ public class CloudOrchestrator implements OrchestrationService { @Override public VirtualMachineEntity createVirtualMachineFromScratch(String id, String owner, String isoId, String hostName, String displayName, String hypervisor, String os, int cpu, int speed, long memory, Long diskSize, List computeTags, List rootDiskTags, Map> networkNicMap, DeploymentPlan plan, - Map> extraDhcpOptionMap, Long diskOfferingId) + Map> extraDhcpOptionMap, Long diskOfferingId, Volume volume, Snapshot snapshot) throws InsufficientCapacityException { // VirtualMachineEntityImpl vmEntity = new VirtualMachineEntityImpl(id, owner, hostName, displayName, cpu, speed, memory, computeTags, rootDiskTags, networks, vmEntityManager); @@ -300,7 +307,7 @@ public class CloudOrchestrator implements OrchestrationService { rootDiskOfferingInfo.setSize(size); if (diskOffering.isCustomizedIops() != null && diskOffering.isCustomizedIops()) { - Map userVmDetails = _userVmDetailsDao.listDetailsKeyPairs(vm.getId()); + Map userVmDetails = _vmInstanceDetailsDao.listDetailsKeyPairs(vm.getId()); if (userVmDetails != null) { String minIops = userVmDetails.get("minIopsDo"); @@ -321,7 +328,7 @@ public class CloudOrchestrator implements OrchestrationService { HypervisorType hypervisorType = HypervisorType.valueOf(hypervisor); - _itMgr.allocate(vm.getInstanceName(), _templateDao.findById(new Long(isoId)), computeOffering, rootDiskOfferingInfo, new ArrayList(), networkIpMap, plan, hypervisorType, extraDhcpOptionMap, null); + _itMgr.allocate(vm.getInstanceName(), _templateDao.findByIdIncludingRemoved(new Long(isoId)), computeOffering, rootDiskOfferingInfo, new ArrayList(), networkIpMap, plan, hypervisorType, extraDhcpOptionMap, null, volume, snapshot); return vmEntity; } diff --git a/engine/orchestration/src/main/java/org/apache/cloudstack/engine/orchestration/NetworkOrchestrator.java b/engine/orchestration/src/main/java/org/apache/cloudstack/engine/orchestration/NetworkOrchestrator.java index 9f8688af8bf..07d1a60b7f8 100644 --- a/engine/orchestration/src/main/java/org/apache/cloudstack/engine/orchestration/NetworkOrchestrator.java +++ b/engine/orchestration/src/main/java/org/apache/cloudstack/engine/orchestration/NetworkOrchestrator.java @@ -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,12 +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.NsxProviderDao; import org.apache.cloudstack.acl.ControlledEntity.ACLType; import org.apache.cloudstack.annotation.AnnotationService; import org.apache.cloudstack.annotation.dao.AnnotationDao; @@ -65,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; @@ -86,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; @@ -95,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; @@ -151,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; @@ -161,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; @@ -248,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; @@ -261,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. @@ -355,9 +357,13 @@ public class NetworkOrchestrator extends ManagerBase implements NetworkOrchestra @Inject private NsxProviderDao nsxProviderDao; @Inject + private NetrisProviderDao netrisProviderDao; + @Inject private ASNumberDao asNumberDao; @Inject private BGPService bgpService; + @Inject + private Ipv6GuestPrefixSubnetNetworkMapDao ipv6GuestPrefixSubnetNetworkMapDao; @Override public List getNetworkGurus() { @@ -558,27 +564,27 @@ public class NetworkOrchestrator extends ManagerBase implements NetworkOrchestra if (_networkOfferingDao.findByUniqueName(NetworkOffering.QuickCloudNoServices) == null) { offering = _configMgr.createNetworkOffering(NetworkOffering.QuickCloudNoServices, "Offering for QuickCloud with no services", TrafficType.Guest, null, true, Availability.Optional, null, new HashMap>(), true, Network.GuestType.Shared, false, null, true, null, true, - false, null, false, null, true, false, false, false, null, null, null, true, null, null, false); + false, null, false, null, true, false, false, false, false, null, null, null, true, null, null, false); } //#2 - SG enabled network offering if (_networkOfferingDao.findByUniqueName(NetworkOffering.DefaultSharedNetworkOfferingWithSGService) == null) { offering = _configMgr.createNetworkOffering(NetworkOffering.DefaultSharedNetworkOfferingWithSGService, "Offering for Shared Security group enabled networks", TrafficType.Guest, null, true, Availability.Optional, null, defaultSharedNetworkOfferingProviders, true, Network.GuestType.Shared, false, null, true, - null, true, false, null, false, null, true, false, false, false, null, null, null, true, null, null, false); + null, true, false, null, false, null, true, false, false, false, false, null, null, null, true, null, null, false); } //#3 - shared network offering with no SG service if (_networkOfferingDao.findByUniqueName(NetworkOffering.DefaultSharedNetworkOffering) == null) { offering = _configMgr.createNetworkOffering(NetworkOffering.DefaultSharedNetworkOffering, "Offering for Shared networks", TrafficType.Guest, null, true, Availability.Optional, null, defaultSharedNetworkOfferingProviders, true, Network.GuestType.Shared, false, null, true, null, true, false, null, false, - null, true, false, false, false, null,null, null, true, null, null, false); + null, true, false, false, false, false, null, null, null, true, null, null, false); } if (_networkOfferingDao.findByUniqueName(NetworkOffering.DEFAULT_TUNGSTEN_SHARED_NETWORK_OFFERING_WITH_SGSERVICE) == null) { offering = _configMgr.createNetworkOffering(NetworkOffering.DEFAULT_TUNGSTEN_SHARED_NETWORK_OFFERING_WITH_SGSERVICE, "Offering for Tungsten Shared Security group enabled networks", TrafficType.Guest, null, true, Availability.Optional, null, defaultTungstenSharedSGEnabledNetworkOfferingProviders, true, Network.GuestType.Shared, false, null, true, - null, true, false, null, false, null, true, false, true, false, null, null,null, true, null, null, false); + null, true, false, null, false, null, true, false, true, false, false, null, null, null, true, null, null, false); offering.setState(NetworkOffering.State.Enabled); _networkOfferingDao.update(offering.getId(), offering); } @@ -588,15 +594,14 @@ public class NetworkOrchestrator extends ManagerBase implements NetworkOrchestra offering = _configMgr.createNetworkOffering(NetworkOffering.DefaultIsolatedNetworkOfferingWithSourceNatService, "Offering for Isolated networks with Source Nat service enabled", TrafficType.Guest, null, false, Availability.Required, null, defaultIsolatedSourceNatEnabledNetworkOfferingProviders, true, Network.GuestType.Isolated, false, null, true, null, false, false, null, false, null, - true, false, false, false, null, null,null, true, null, null, false); + true, false, false, false, false, null, null, null, true, null, null, false); } //#5 - default vpc offering with LB service if (_networkOfferingDao.findByUniqueName(NetworkOffering.DefaultIsolatedNetworkOfferingForVpcNetworks) == null) { offering = _configMgr.createNetworkOffering(NetworkOffering.DefaultIsolatedNetworkOfferingForVpcNetworks, "Offering for Isolated VPC networks with Source Nat service enabled", TrafficType.Guest, null, false, Availability.Optional, null, - defaultVPCOffProviders, true, Network.GuestType.Isolated, false, null, true, null, false, false, null, false, null, true, true, false, false, null, null, null,true, null, null, false); - + defaultVPCOffProviders, true, Network.GuestType.Isolated, false, null, true, null, false, false, null, false, null, true, true, false, false, false, null, null, null, true, null, null, false); } //#6 - default vpc offering with no LB service @@ -605,14 +610,14 @@ public class NetworkOrchestrator extends ManagerBase implements NetworkOrchestra defaultVPCOffProviders.remove(Service.Lb); offering = _configMgr.createNetworkOffering(NetworkOffering.DefaultIsolatedNetworkOfferingForVpcNetworksNoLB, "Offering for Isolated VPC networks with Source Nat service enabled and LB service disabled", TrafficType.Guest, null, false, Availability.Optional, - null, defaultVPCOffProviders, true, Network.GuestType.Isolated, false, null, false, null, false, false, null, false, null, true, true, false, false, null, null, null,true, null, null, false); + null, defaultVPCOffProviders, true, Network.GuestType.Isolated, false, null, false, null, false, false, null, false, null, true, true, false, false, false, null, null, null, true, null, null, false); } //#7 - isolated offering with source nat disabled if (_networkOfferingDao.findByUniqueName(NetworkOffering.DefaultIsolatedNetworkOffering) == null) { offering = _configMgr.createNetworkOffering(NetworkOffering.DefaultIsolatedNetworkOffering, "Offering for Isolated networks with no Source Nat service", TrafficType.Guest, null, true, Availability.Optional, null, defaultIsolatedNetworkOfferingProviders, true, Network.GuestType.Isolated, false, null, - true, null, true, false, null, false, null, true, false, false, false, null, null, null, true, null, null, false); + true, null, true, false, null, false, null, true, false, false, false, false, null, null, null, true, null, null, false); } //#8 - network offering with internal lb service @@ -634,7 +639,7 @@ public class NetworkOrchestrator extends ManagerBase implements NetworkOrchestra if (_networkOfferingDao.findByUniqueName(NetworkOffering.DefaultIsolatedNetworkOfferingForVpcNetworksWithInternalLB) == null) { offering = _configMgr.createNetworkOffering(NetworkOffering.DefaultIsolatedNetworkOfferingForVpcNetworksWithInternalLB, "Offering for Isolated VPC networks with Internal Lb support", TrafficType.Guest, null, false, Availability.Optional, null, internalLbOffProviders, - true, Network.GuestType.Isolated, false, null, false, null, false, false, null, false, null, true, true, false, false, null, null, null, true, null, null, false); + true, Network.GuestType.Isolated, false, null, false, null, false, false, null, false, null, true, true, false, false, false, null, null, null, true, null, null, false); offering.setInternalLb(true); offering.setPublicLb(false); _networkOfferingDao.update(offering.getId(), offering); @@ -665,7 +670,7 @@ public class NetworkOrchestrator extends ManagerBase implements NetworkOrchestra if (_networkOfferingDao.findByUniqueName(NetworkOffering.DefaultSharedEIPandELBNetworkOffering) == null) { offering = _configMgr.createNetworkOffering(NetworkOffering.DefaultSharedEIPandELBNetworkOffering, "Offering for Shared networks with Elastic IP and Elastic LB capabilities", TrafficType.Guest, null, true, Availability.Optional, null, - netscalerServiceProviders, true, Network.GuestType.Shared, false, null, true, serviceCapabilityMap, true, false, null, false, null, true, false, false, false, null, null, null, true, null, null, false); + netscalerServiceProviders, true, Network.GuestType.Shared, false, null, true, serviceCapabilityMap, true, false, null, false, null, true, false, false, false, false, null, null, null, true, null, null, false); offering.setDedicatedLB(false); _networkOfferingDao.update(offering.getId(), offering); } @@ -822,6 +827,11 @@ public class NetworkOrchestrator extends ManagerBase implements NetworkOrchestra if (domainId != null && aclType == ACLType.Domain) { _networksDao.addDomainToNetwork(id, domainId, subdomainAccess == null || subdomainAccess); } + String ipv6Cidr = network.getIp6Cidr(); + String ipv6Gateway = network.getIp6Gateway(); + if (StringUtils.isNoneBlank(ipv6Cidr, ipv6Gateway)) { + ipv6Service.assignIpv6SubnetToNetwork(ipv6Cidr, networkPersisted.getId()); + } } }); guru.setup(network, relatedFile); @@ -1056,7 +1066,7 @@ public class NetworkOrchestrator extends ManagerBase implements NetworkOrchestra return Transaction.execute(new TransactionCallback() { @Override public NicVO doInTransaction(TransactionStatus status) { - NicVO vo = _nicDao.findByIp4AddressAndNetworkId(profile.getIPv4Address(), networkId); + NicVO vo = _nicDao.findNonPlaceHolderByIp4AddressAndNetworkId(profile.getIPv4Address(), networkId); if (vo == null) { applyProfileToNic(nic, profile, deviceId); vo = _nicDao.persist(nic); @@ -1083,10 +1093,14 @@ public class NetworkOrchestrator extends ManagerBase implements NetworkOrchestra return null; } - if (isNicAllocatedForNsxPublicNetworkOnVR(network, profile, vm)) { + if (isNicAllocatedForProviderPublicNetworkOnVR(network, profile, vm, Provider.Nsx)) { String guruName = "NsxPublicNetworkGuru"; NetworkGuru nsxGuru = AdapterBase.getAdapterByName(networkGurus, guruName); nsxGuru.allocate(network, profile, vm); + } else if (isNicAllocatedForProviderPublicNetworkOnVR(network, profile, vm, Provider.Netris)) { + String guruName = "NetrisPublicNetworkGuru"; + NetworkGuru netrisGuru = AdapterBase.getAdapterByName(networkGurus, guruName); + netrisGuru.allocate(network, profile, vm); } if (isDefaultNic != null) { @@ -1149,7 +1163,7 @@ public class NetworkOrchestrator extends ManagerBase implements NetworkOrchestra return new Pair(vmNic, Integer.valueOf(deviceId)); } - private boolean isNicAllocatedForNsxPublicNetworkOnVR(Network network, NicProfile requested, VirtualMachineProfile vm) { + private boolean isNicAllocatedForProviderPublicNetworkOnVR(Network network, NicProfile requested, VirtualMachineProfile vm, Provider provider) { if (ObjectUtils.anyNull(network, requested, vm)) { return false; } @@ -1159,7 +1173,9 @@ public class NetworkOrchestrator extends ManagerBase implements NetworkOrchestra return false; } long dataCenterId = vm.getVirtualMachine().getDataCenterId(); - if (nsxProviderDao.findByZoneId(dataCenterId) == null) { + if (Provider.Nsx == provider && nsxProviderDao.findByZoneId(dataCenterId) == null) { + return false; + } else if (Provider.Netris == provider && netrisProviderDao.findByZoneId(dataCenterId) == null) { return false; } @@ -1169,14 +1185,16 @@ public class NetworkOrchestrator extends ManagerBase implements NetworkOrchestra if (CollectionUtils.isEmpty(ips)) { return false; } + ips = ips.stream().filter(x -> !x.getAddress().addr().equals(requested.getIPv4Address())).collect(Collectors.toList()); IPAddressVO ip = ips.get(0); - VlanDetailsVO vlanDetail = vlanDetailsDao.findDetail(ip.getVlanId(), ApiConstants.NSX_DETAIL_KEY); + String detailKey = Provider.Nsx == provider ? ApiConstants.NSX_DETAIL_KEY : ApiConstants.NETRIS_DETAIL_KEY; + VlanDetailsVO vlanDetail = vlanDetailsDao.findDetail(ip.getVlanId(), detailKey); if (vlanDetail == null) { return false; } - boolean isForNsx = vlanDetail.getValue().equalsIgnoreCase("true"); - return isForNsx && !ip.isForSystemVms(); + boolean isForProvider = vlanDetail.getValue().equalsIgnoreCase("true"); + return isForProvider && !ip.isForSystemVms(); } private void setMtuDetailsInVRNic(final Pair networks, Network network, NicVO vo) { @@ -1694,6 +1712,7 @@ public class NetworkOrchestrator extends ManagerBase implements NetworkOrchestra } } } + reconfigureAndApplyStaticRouteForVpcVpn(network); } finally { for (final NetworkElement element : networkElements) { if (element instanceof AggregatedCommandExecutor && providersToImplement.contains(element.getProvider())) { @@ -1703,6 +1722,17 @@ public class NetworkOrchestrator extends ManagerBase implements NetworkOrchestra } } + private void reconfigureAndApplyStaticRouteForVpcVpn(Network network) { + if (network.getVpcId() != null) { + _vpcMgr.reconfigStaticNatForVpcVr(network.getVpcId()); + try { + _vpcMgr.applyStaticRouteForVpcVpnIfNeeded(network.getVpcId(), true); + } catch (ResourceUnavailableException e) { + logger.error("Unable to apply static routes for vpc " + network.getVpcId() + " due to " + e.getMessage()); + } + } + } + private void implementNetworkElements(final DeployDestination dest, final ReservationContext context, final Network network, final NetworkOffering offering, final List providersToImplement) throws ConcurrentOperationException, ResourceUnavailableException, InsufficientCapacityException { for (NetworkElement element : networkElements) { @@ -1939,6 +1969,7 @@ public class NetworkOrchestrator extends ManagerBase implements NetworkOrchestra ip.setOneToOneNat(false); ip.setAssociatedWithVmId(null); ip.setVmIp(null); + ip.setForRouter(false); _ipAddressDao.update(ip.getId(), ip); } } @@ -3296,6 +3327,7 @@ public class NetworkOrchestrator extends ManagerBase implements NetworkOrchestra } } } + reconfigureAndApplyStaticRouteForVpcVpn(network); return success; } @@ -4877,6 +4909,6 @@ public class NetworkOrchestrator extends ManagerBase implements NetworkOrchestra return new ConfigKey[]{NetworkGcWait, NetworkGcInterval, NetworkLockTimeout, DeniedRoutes, GuestDomainSuffix, NetworkThrottlingRate, MinVRVersion, PromiscuousMode, MacAddressChanges, ForgedTransmits, MacLearning, RollingRestartEnabled, - TUNGSTEN_ENABLED, NSX_ENABLED }; + TUNGSTEN_ENABLED, NSX_ENABLED, NETRIS_ENABLED }; } } diff --git a/engine/orchestration/src/main/java/org/apache/cloudstack/engine/orchestration/VolumeOrchestrator.java b/engine/orchestration/src/main/java/org/apache/cloudstack/engine/orchestration/VolumeOrchestrator.java index e0075888caf..d9a79f9885b 100644 --- a/engine/orchestration/src/main/java/org/apache/cloudstack/engine/orchestration/VolumeOrchestrator.java +++ b/engine/orchestration/src/main/java/org/apache/cloudstack/engine/orchestration/VolumeOrchestrator.java @@ -57,6 +57,8 @@ import org.apache.cloudstack.engine.subsystem.api.storage.DataObject; import org.apache.cloudstack.engine.subsystem.api.storage.DataStore; import org.apache.cloudstack.engine.subsystem.api.storage.DataStoreDriver; import org.apache.cloudstack.engine.subsystem.api.storage.DataStoreManager; +import org.apache.cloudstack.engine.subsystem.api.storage.DataStoreProvider; +import org.apache.cloudstack.engine.subsystem.api.storage.DataStoreProviderManager; import org.apache.cloudstack.engine.subsystem.api.storage.PrimaryDataStore; import org.apache.cloudstack.engine.subsystem.api.storage.PrimaryDataStoreDriver; import org.apache.cloudstack.engine.subsystem.api.storage.SnapshotDataFactory; @@ -166,7 +168,7 @@ import com.cloud.utils.fsm.StateMachine2; import com.cloud.vm.DiskProfile; import com.cloud.vm.SecondaryStorageVmVO; import com.cloud.vm.UserVmCloneSettingVO; -import com.cloud.vm.UserVmDetailVO; +import com.cloud.vm.VMInstanceDetailVO; import com.cloud.vm.UserVmVO; import com.cloud.vm.VMInstanceVO; import com.cloud.vm.VirtualMachine; @@ -181,7 +183,7 @@ import com.cloud.vm.VmWorkTakeVolumeSnapshot; import com.cloud.vm.dao.SecondaryStorageVmDao; import com.cloud.vm.dao.UserVmCloneSettingDao; import com.cloud.vm.dao.UserVmDao; -import com.cloud.vm.dao.UserVmDetailsDao; +import com.cloud.vm.dao.VMInstanceDetailsDao; public class VolumeOrchestrator extends ManagerBase implements VolumeOrchestrationService, Configurable { @@ -253,7 +255,7 @@ public class VolumeOrchestrator extends ManagerBase implements VolumeOrchestrati @Inject TemplateService templateService; @Inject - UserVmDetailsDao userVmDetailsDao; + VMInstanceDetailsDao vmInstanceDetailsDao; @Inject private SecondaryStorageVmDao secondaryStorageVmDao; @Inject @@ -272,6 +274,9 @@ public class VolumeOrchestrator extends ManagerBase implements VolumeOrchestrati @Inject protected SnapshotHelper snapshotHelper; + @Inject + private DataStoreProviderManager dataStoreProviderMgr; + private final StateMachine2 _volStateMachine; protected List _storagePoolAllocators; @@ -898,10 +903,20 @@ public class VolumeOrchestrator extends ManagerBase implements VolumeOrchestrati } private DiskProfile allocateTemplatedVolume(Type type, String name, DiskOffering offering, Long rootDisksize, Long minIops, Long maxIops, VirtualMachineTemplate template, VirtualMachine vm, - Account owner, long deviceId, String configurationId) { + Account owner, long deviceId, String configurationId, Volume volume, Snapshot snapshot) { assert (template.getFormat() != ImageFormat.ISO) : "ISO is not a template."; - Long size = _tmpltMgr.getTemplateSize(template, vm.getDataCenterId()); + if (volume != null) { + volume = attachExistingVolumeToVm(vm, deviceId, volume, type); + provideVmInfoToTheStorageVolume(vm, volume); + return toDiskProfile(volume, offering); + } + Long size; + if (snapshot != null) { + size = _volsDao.findByIdIncludingRemoved(snapshot.getVolumeId()).getSize(); + } else { + size = _tmpltMgr.getTemplateSize(template, vm.getDataCenterId()); + } if (rootDisksize != null) { if (template.isDeployAsIs()) { // Volume size specified from template deploy-as-is @@ -961,9 +976,45 @@ public class VolumeOrchestrator extends ManagerBase implements VolumeOrchestrati _resourceLimitMgr.incrementVolumeResourceCount(vm.getAccountId(), vol.isDisplayVolume(), vol.getSize(), offering); } + if (snapshot != null) { + UserVmVO userVmVO = _userVmDao.findById(vm.getId()); + try { + VolumeInfo volumeInfo = createVolumeFromSnapshot(vol, snapshot, userVmVO); + return toDiskProfile(volumeInfo, offering); + } catch (StorageUnavailableException ex) { + throw new CloudRuntimeException("Could not create volume from a snapshot", ex); + } + } return toDiskProfile(vol, offering); } + private void provideVmInfoToTheStorageVolume(VirtualMachine vm, Volume volume) { + + StoragePoolVO pool = _storagePoolDao.findById(volume.getPoolId()); + if (pool != null) { + DataStoreProvider storeProvider = dataStoreProviderMgr + .getDataStoreProvider(pool.getStorageProviderName()); + DataStoreDriver storeDriver = storeProvider.getDataStoreDriver(); + if (storeDriver != null && storeDriver instanceof PrimaryDataStoreDriver && ((PrimaryDataStoreDriver) storeDriver).isVmInfoNeeded()) { + ((PrimaryDataStoreDriver) storeDriver).provideVmInfo(vm.getId(), volume.getId()); + } + } + } + + private Volume attachExistingVolumeToVm(VirtualMachine vm, long deviceId, Volume volume, Type type) { + VolumeVO volumeVO = _volumeDao.findById(volume.getId()); + if (volumeVO == null) { + throw new CloudRuntimeException(String.format("Could not find the volume %s in the DB", volume)); + } + volumeVO.setDeviceId(deviceId); + volumeVO.setVolumeType(type); + if (vm != null) { + volumeVO.setInstanceId(vm.getId()); + } + _volumeDao.update(volumeVO.getId(), volumeVO); + return volumeVO; + } + @Override public void saveVolumeDetails(Long diskOfferingId, Long volumeId) { List volumeDetailsVO = new ArrayList<>(); @@ -993,7 +1044,7 @@ public class VolumeOrchestrator extends ManagerBase implements VolumeOrchestrati @ActionEvent(eventType = EventTypes.EVENT_VOLUME_CREATE, eventDescription = "creating ROOT volume", create = true) @Override public List allocateTemplatedVolumes(Type type, String name, DiskOffering offering, Long rootDisksize, Long minIops, Long maxIops, VirtualMachineTemplate template, VirtualMachine vm, - Account owner) { + Account owner, Volume volume, Snapshot snapshot) { String templateToString = getReflectOnlySelectedFields(template); int volumesNumber = 1; @@ -1006,7 +1057,7 @@ public class VolumeOrchestrator extends ManagerBase implements VolumeOrchestrati logger.info("Could not find a running SSVM in datacenter [{}] for deploying VM as is. Not deploying VM [{}] as is.", vm.getDataCenterId(), vm); } else { - UserVmDetailVO configurationDetail = userVmDetailsDao.findDetail(vm.getId(), VmDetailConstants.DEPLOY_AS_IS_CONFIGURATION); + VMInstanceDetailVO configurationDetail = vmInstanceDetailsDao.findDetail(vm.getId(), VmDetailConstants.DEPLOY_AS_IS_CONFIGURATION); if (configurationDetail != null) { configurationId = configurationDetail.getValue(); } @@ -1040,7 +1091,7 @@ public class VolumeOrchestrator extends ManagerBase implements VolumeOrchestrati } logger.info("Adding disk object [{}] to VM [{}]", volumeName, vm); DiskProfile diskProfile = allocateTemplatedVolume(type, volumeName, offering, volumeSize, minIops, maxIops, - template, vm, owner, deviceId, configurationId); + template, vm, owner, deviceId, configurationId, volume, snapshot); profiles.add(diskProfile); } @@ -1075,13 +1126,13 @@ public class VolumeOrchestrator extends ManagerBase implements VolumeOrchestrati String diskControllerSubType = disksAsIs.get(0).getDiskControllerSubType(); if (StringUtils.isNotBlank(diskControllerSubType)) { long vmId = vm.getId(); - UserVmDetailVO detail = userVmDetailsDao.findDetail(vmId, VmDetailConstants.ROOT_DISK_CONTROLLER); + VMInstanceDetailVO detail = vmInstanceDetailsDao.findDetail(vmId, VmDetailConstants.ROOT_DISK_CONTROLLER); if (detail != null) { detail.setValue(diskControllerSubType); - userVmDetailsDao.update(detail.getId(), detail); + vmInstanceDetailsDao.update(detail.getId(), detail); } else { - detail = new UserVmDetailVO(vmId, VmDetailConstants.ROOT_DISK_CONTROLLER, diskControllerSubType, false); - userVmDetailsDao.persist(detail); + detail = new VMInstanceDetailVO(vmId, VmDetailConstants.ROOT_DISK_CONTROLLER, diskControllerSubType, false); + vmInstanceDetailsDao.persist(detail); } } } @@ -1570,7 +1621,7 @@ public class VolumeOrchestrator extends ManagerBase implements VolumeOrchestrati private void setIoDriverPolicy(Map details, StoragePoolVO storagePool, VolumeVO volume) { if (volume.getInstanceId() != null) { - UserVmDetailVO ioDriverPolicy = userVmDetailsDao.findDetail(volume.getInstanceId(), + VMInstanceDetailVO ioDriverPolicy = vmInstanceDetailsDao.findDetail(volume.getInstanceId(), VmDetailConstants.IO_POLICY); if (ioDriverPolicy != null) { if (IoDriverPolicy.STORAGE_SPECIFIC.toString().equals(ioDriverPolicy.getValue())) { diff --git a/engine/orchestration/src/test/java/com/cloud/vm/VirtualMachineManagerImplTest.java b/engine/orchestration/src/test/java/com/cloud/vm/VirtualMachineManagerImplTest.java index 4a451e4e2d4..4f6329f81cb 100644 --- a/engine/orchestration/src/test/java/com/cloud/vm/VirtualMachineManagerImplTest.java +++ b/engine/orchestration/src/test/java/com/cloud/vm/VirtualMachineManagerImplTest.java @@ -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,15 @@ import java.util.Random; import java.util.UUID; import java.util.stream.Collectors; +import com.cloud.resource.ResourceManager; +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 +73,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 +107,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,8 +144,9 @@ 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.UserVmDetailsDao; +import com.cloud.vm.dao.VMInstanceDetailsDao; import com.cloud.vm.dao.VMInstanceDao; @RunWith(MockitoJUnitRunner.class) @@ -216,6 +231,8 @@ public class VirtualMachineManagerImplTest { @Mock private EntityManager _entityMgr; @Mock + private ResourceManager _resourceMgr; + @Mock private DeploymentPlanningManager _dpMgr; @Mock private HypervisorGuruManager _hvGuruMgr; @@ -224,11 +241,19 @@ public class VirtualMachineManagerImplTest { @Mock private ClusterDetailsDao _clusterDetailsDao; @Mock - private UserVmDetailsDao userVmDetailsDao; + private VMInstanceDetailsDao vmInstanceDetailsDao; @Mock private ItWorkDao _workDao; @Mock protected StateMachine2 _stateMachine; + @Mock + ExtensionsManager extensionsManager; + @Mock + ExtensionDetailsDao extensionDetailsDao; + @Mock + NicDao _nicsDao; + @Mock + NetworkService networkService; private ConfigDepotImpl configDepotImpl; private boolean updatedConfigKeyDepot = false; @@ -458,8 +483,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 +494,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 +510,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 +535,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 +564,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 +591,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 +602,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 +702,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 +764,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 +783,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 +804,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); } @@ -1098,7 +1123,7 @@ public class VirtualMachineManagerImplTest { when(cluster.getId()).thenReturn(1L); when(_clusterDetailsDao.findDetail(1L, VmDetailConstants.CPU_OVER_COMMIT_RATIO)).thenReturn(cluster_detail_cpu); when(_clusterDetailsDao.findDetail(1L, VmDetailConstants.MEMORY_OVER_COMMIT_RATIO)).thenReturn(cluster_detail_ram); - when(userVmDetailsDao.findDetail(anyLong(), Mockito.anyString())).thenReturn(null); + when(vmInstanceDetailsDao.findDetail(anyLong(), Mockito.anyString())).thenReturn(null); when(cluster_detail_cpu.getValue()).thenReturn("1.0"); when(cluster_detail_ram.getValue()).thenReturn("1.0"); doReturn(false).when(virtualMachineManagerImpl).areAllVolumesAllocated(Mockito.anyLong()); @@ -1194,7 +1219,7 @@ public class VirtualMachineManagerImplTest { when(cluster.getId()).thenReturn(1L); when(_clusterDetailsDao.findDetail(1L, VmDetailConstants.CPU_OVER_COMMIT_RATIO)).thenReturn(cluster_detail_cpu); when(_clusterDetailsDao.findDetail(1L, VmDetailConstants.MEMORY_OVER_COMMIT_RATIO)).thenReturn(cluster_detail_ram); - when(userVmDetailsDao.findDetail(anyLong(), Mockito.anyString())).thenReturn(null); + when(vmInstanceDetailsDao.findDetail(anyLong(), Mockito.anyString())).thenReturn(null); when(cluster_detail_cpu.getValue()).thenReturn("1.0"); when(cluster_detail_ram.getValue()).thenReturn("1.0"); doReturn(true).when(virtualMachineManagerImpl).areAllVolumesAllocated(Mockito.anyLong()); @@ -1318,7 +1343,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 +1353,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 +1366,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 +1379,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 +1392,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 +1404,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 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)); } } diff --git a/engine/schema/src/main/java/com/cloud/capacity/dao/CapacityDao.java b/engine/schema/src/main/java/com/cloud/capacity/dao/CapacityDao.java index 7a4c96c6e1f..42313efa512 100644 --- a/engine/schema/src/main/java/com/cloud/capacity/dao/CapacityDao.java +++ b/engine/schema/src/main/java/com/cloud/capacity/dao/CapacityDao.java @@ -30,7 +30,7 @@ public interface CapacityDao extends GenericDao { List listByHostIdTypes(Long hostId, List capacityTypes); - List listClustersInZoneOrPodByHostCapacities(long id, long vmId, int requiredCpu, long requiredRam, short capacityTypeForOrdering, boolean isZone); + List listClustersInZoneOrPodByHostCapacities(long id, long vmId, int requiredCpu, long requiredRam, boolean isZone); List listHostsWithEnoughCapacity(int requiredCpu, long requiredRam, Long clusterId, String hostType); @@ -48,7 +48,7 @@ public interface CapacityDao extends GenericDao { List findFilteredCapacityBy(Integer capacityType, Long zoneId, Long podId, Long clusterId, List hostIds, List poolIds); - List listPodsByHostCapacities(long zoneId, int requiredCpu, long requiredRam, short capacityType); + List listPodsByHostCapacities(long zoneId, int requiredCpu, long requiredRam); Pair, Map> orderPodsByAggregateCapacity(long zoneId, short capacityType); @@ -65,4 +65,10 @@ public interface CapacityDao extends GenericDao { float findClusterConsumption(Long clusterId, short capacityType, long computeRequested); Pair, Map> orderHostsByFreeCapacity(Long zoneId, Long clusterId, short capacityType); + + List listHostCapacityByCapacityTypes(Long zoneId, Long clusterId, List capacityTypes); + + List listPodCapacityByCapacityTypes(Long zoneId, List capacityTypes); + + List listClusterCapacityByCapacityTypes(Long zoneId, Long podId, List capacityTypes); } diff --git a/engine/schema/src/main/java/com/cloud/capacity/dao/CapacityDaoImpl.java b/engine/schema/src/main/java/com/cloud/capacity/dao/CapacityDaoImpl.java index 0860f14518f..91949bded50 100644 --- a/engine/schema/src/main/java/com/cloud/capacity/dao/CapacityDaoImpl.java +++ b/engine/schema/src/main/java/com/cloud/capacity/dao/CapacityDaoImpl.java @@ -204,9 +204,9 @@ public class CapacityDaoImpl extends GenericDaoBase implements "(CASE WHEN ISNULL(service_offering.speed) THEN custom_speed.value ELSE service_offering.speed end) AS speed, " + "(CASE WHEN ISNULL(service_offering.ram_size) THEN custom_ram_size.value ELSE service_offering.ram_size end) AS ram_size " + "FROM vm_instance vi LEFT JOIN service_offering ON(((vi.service_offering_id = service_offering.id))) " + - "LEFT JOIN user_vm_details custom_cpu ON(((custom_cpu.vm_id = vi.id) AND (custom_cpu.name = 'CpuNumber'))) " + - "LEFT JOIN user_vm_details custom_speed ON(((custom_speed.vm_id = vi.id) AND (custom_speed.name = 'CpuSpeed'))) " + - "LEFT JOIN user_vm_details custom_ram_size ON(((custom_ram_size.vm_id = vi.id) AND (custom_ram_size.name = 'memory'))) "; + "LEFT JOIN vm_instance_details custom_cpu ON(((custom_cpu.vm_id = vi.id) AND (custom_cpu.name = 'CpuNumber'))) " + + "LEFT JOIN vm_instance_details custom_speed ON(((custom_speed.vm_id = vi.id) AND (custom_speed.name = 'CpuSpeed'))) " + + "LEFT JOIN vm_instance_details custom_ram_size ON(((custom_ram_size.vm_id = vi.id) AND (custom_ram_size.name = 'memory'))) "; private static final String WHERE_STATE_IS_NOT_DESTRUCTIVE = "WHERE ISNULL(vi.removed) AND vi.state NOT IN ('Destroyed', 'Error', 'Expunging')"; @@ -684,7 +684,7 @@ public class CapacityDaoImpl extends GenericDaoBase implements } @Override - public List listClustersInZoneOrPodByHostCapacities(long id, long vmId, int requiredCpu, long requiredRam, short capacityTypeForOrdering, boolean isZone) { + public List listClustersInZoneOrPodByHostCapacities(long id, long vmId, int requiredCpu, long requiredRam, boolean isZone) { TransactionLegacy txn = TransactionLegacy.currentTxn(); PreparedStatement pstmt = null; List result = new ArrayList(); @@ -1068,7 +1068,65 @@ public class CapacityDaoImpl extends GenericDaoBase implements } @Override - public List listPodsByHostCapacities(long zoneId, int requiredCpu, long requiredRam, short capacityType) { + public List listHostCapacityByCapacityTypes(Long zoneId, Long clusterId, List capacityTypes) { + SearchBuilder sb = createSearchBuilder(); + sb.and("zoneId", sb.entity().getDataCenterId(), SearchCriteria.Op.EQ); + sb.and("clusterId", sb.entity().getClusterId(), SearchCriteria.Op.EQ); + sb.and("capacityTypes", sb.entity().getCapacityType(), SearchCriteria.Op.IN); + sb.and("capacityState", sb.entity().getCapacityState(), Op.EQ); + sb.done(); + + SearchCriteria sc = sb.create(); + sc.setParameters("capacityState", "Enabled"); + if (zoneId != null) { + sc.setParameters("zoneId", zoneId); + } + if (clusterId != null) { + sc.setParameters("clusterId", clusterId); + } + sc.setParameters("capacityTypes", capacityTypes.toArray()); + return listBy(sc); + } + + @Override + public List listPodCapacityByCapacityTypes(Long zoneId, List capacityTypes) { + SearchBuilder sb = createSearchBuilder(); + sb.and("zoneId", sb.entity().getDataCenterId(), SearchCriteria.Op.EQ); + sb.and("capacityTypes", sb.entity().getCapacityType(), SearchCriteria.Op.IN); + sb.and("capacityState", sb.entity().getCapacityState(), Op.EQ); + sb.done(); + SearchCriteria sc = sb.create(); + sc.setParameters("capacityState", "Enabled"); + if (zoneId != null) { + sc.setParameters("zoneId", zoneId); + } + sc.setParameters("capacityTypes", capacityTypes.toArray()); + return listBy(sc); + } + + @Override + public List listClusterCapacityByCapacityTypes(Long zoneId, Long podId, List capacityTypes) { + SearchBuilder sb = createSearchBuilder(); + sb.and("zoneId", sb.entity().getDataCenterId(), SearchCriteria.Op.EQ); + sb.and("podId", sb.entity().getPodId(), SearchCriteria.Op.EQ); + sb.and("capacityTypes", sb.entity().getCapacityType(), SearchCriteria.Op.IN); + sb.and("capacityState", sb.entity().getCapacityState(), Op.EQ); + sb.done(); + + SearchCriteria sc = sb.create(); + sc.setParameters("capacityState", "Enabled"); + if (zoneId != null) { + sc.setParameters("zoneId", zoneId); + } + if (podId != null) { + sc.setParameters("podId", podId); + } + sc.setParameters("capacityTypes", capacityTypes.toArray()); + return listBy(sc); + } + + @Override + public List listPodsByHostCapacities(long zoneId, int requiredCpu, long requiredRam) { TransactionLegacy txn = TransactionLegacy.currentTxn(); PreparedStatement pstmt = null; List result = new ArrayList(); diff --git a/engine/schema/src/main/java/com/cloud/configuration/dao/ResourceCountDao.java b/engine/schema/src/main/java/com/cloud/configuration/dao/ResourceCountDao.java index b978cc04bfa..ebacbd8b3e5 100644 --- a/engine/schema/src/main/java/com/cloud/configuration/dao/ResourceCountDao.java +++ b/engine/schema/src/main/java/com/cloud/configuration/dao/ResourceCountDao.java @@ -62,20 +62,6 @@ public interface ResourceCountDao extends GenericDao { long removeEntriesByOwner(long ownerId, ResourceOwnerType ownerType); - /** - * Counts the number of CPU cores allocated for the given account. - * - * Side note: This method is not using the "resource_count" table. It is executing the actual count instead. - */ - long countCpuNumberAllocatedToAccount(long accountId); - - /** - * Counts the amount of memory allocated for the given account. - * - * Side note: This method is not using the "resource_count" table. It is executing the actual count instead. - */ - long countMemoryAllocatedToAccount(long accountId); - void removeResourceCountsForNonMatchingTags(Long ownerId, ResourceOwnerType ownerType, List types, List tags); List lockRows(Set ids); diff --git a/engine/schema/src/main/java/com/cloud/configuration/dao/ResourceCountDaoImpl.java b/engine/schema/src/main/java/com/cloud/configuration/dao/ResourceCountDaoImpl.java index 65d7fed2d1a..2083fb422d2 100644 --- a/engine/schema/src/main/java/com/cloud/configuration/dao/ResourceCountDaoImpl.java +++ b/engine/schema/src/main/java/com/cloud/configuration/dao/ResourceCountDaoImpl.java @@ -336,22 +336,9 @@ public class ResourceCountDaoImpl extends GenericDaoBase + " END)) as total " + " from vm_instance vm " + " join service_offering so on so.id = vm.service_offering_id " - + " left join user_vm_details vmd on vmd.vm_id = vm.id and vmd.name = '%s' " + + " left join vm_instance_details vmd on vmd.vm_id = vm.id and vmd.name = '%s' " + " where vm.type = 'User' and state not in ('Destroyed', 'Error', 'Expunging') and display_vm = true and account_id = ? "; - @Override - public long countCpuNumberAllocatedToAccount(long accountId) { - String sqlCountCpuNumberAllocatedToAccount = String.format(baseSqlCountComputingResourceAllocatedToAccount, ResourceType.cpu, ResourceType.cpu, "cpuNumber"); - return executeSqlCountComputingResourcesForAccount(accountId, sqlCountCpuNumberAllocatedToAccount); - } - - @Override - public long countMemoryAllocatedToAccount(long accountId) { - String serviceOfferingRamSizeField = "ram_size"; - String sqlCountCpuNumberAllocatedToAccount = String.format(baseSqlCountComputingResourceAllocatedToAccount, serviceOfferingRamSizeField, serviceOfferingRamSizeField, "memory"); - return executeSqlCountComputingResourcesForAccount(accountId, sqlCountCpuNumberAllocatedToAccount); - } - private long executeSqlCountComputingResourcesForAccount(long accountId, String sqlCountComputingResourcesAllocatedToAccount) { TransactionLegacy tx = TransactionLegacy.currentTxn(); try { diff --git a/engine/schema/src/main/java/com/cloud/dc/dao/ClusterDao.java b/engine/schema/src/main/java/com/cloud/dc/dao/ClusterDao.java index a6fe3123c4e..1745f5380e2 100644 --- a/engine/schema/src/main/java/com/cloud/dc/dao/ClusterDao.java +++ b/engine/schema/src/main/java/com/cloud/dc/dao/ClusterDao.java @@ -59,4 +59,6 @@ public interface ClusterDao extends GenericDao { List listClustersByArchAndZoneId(long zoneId, CPU.CPUArch arch); List listDistinctStorageAccessGroups(String name, String keyword); + + List listEnabledClusterIdsByZoneHypervisorArch(Long zoneId, HypervisorType hypervisorType, CPU.CPUArch arch); } diff --git a/engine/schema/src/main/java/com/cloud/dc/dao/ClusterDaoImpl.java b/engine/schema/src/main/java/com/cloud/dc/dao/ClusterDaoImpl.java index 7c0d0c53814..ea82a10f9c9 100644 --- a/engine/schema/src/main/java/com/cloud/dc/dao/ClusterDaoImpl.java +++ b/engine/schema/src/main/java/com/cloud/dc/dao/ClusterDaoImpl.java @@ -378,4 +378,29 @@ public class ClusterDaoImpl extends GenericDaoBase implements C return customSearch(sc, null); } + + @Override + public List listEnabledClusterIdsByZoneHypervisorArch(Long zoneId, HypervisorType hypervisorType, CPU.CPUArch arch) { + GenericSearchBuilder 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 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); + } } diff --git a/engine/schema/src/main/java/com/cloud/dc/dao/VlanDao.java b/engine/schema/src/main/java/com/cloud/dc/dao/VlanDao.java index 84f38f05441..a6c267bb189 100644 --- a/engine/schema/src/main/java/com/cloud/dc/dao/VlanDao.java +++ b/engine/schema/src/main/java/com/cloud/dc/dao/VlanDao.java @@ -64,4 +64,6 @@ public interface VlanDao extends GenericDao { List listIpv6RangeByZoneIdAndVlanId(long zoneId, String vlanId); List listIpv6SupportingVlansByZone(long zoneId); + + List listVlansForExternalNetworkProvider(long zoneId, String detailKey); } diff --git a/engine/schema/src/main/java/com/cloud/dc/dao/VlanDaoImpl.java b/engine/schema/src/main/java/com/cloud/dc/dao/VlanDaoImpl.java index 461a9a13b10..d9fad3cad12 100644 --- a/engine/schema/src/main/java/com/cloud/dc/dao/VlanDaoImpl.java +++ b/engine/schema/src/main/java/com/cloud/dc/dao/VlanDaoImpl.java @@ -26,6 +26,7 @@ import java.util.Map; import javax.inject.Inject; import javax.naming.ConfigurationException; +import com.cloud.dc.VlanDetailsVO; import org.apache.commons.lang3.ArrayUtils; import org.apache.commons.lang3.StringUtils; import org.springframework.stereotype.Component; @@ -67,6 +68,8 @@ public class VlanDaoImpl extends GenericDaoBase implements VlanDao protected SearchBuilder ZoneVlanIp6Search; protected SearchBuilder ZoneIp6Search; protected SearchBuilder ZoneVlansSearch; + protected SearchBuilder ProviderVlanSearch; + protected SearchBuilder VlanDetailsProviderSearch; protected SearchBuilder AccountVlanMapSearch; protected SearchBuilder DomainVlanMapSearch; @@ -79,6 +82,8 @@ public class VlanDaoImpl extends GenericDaoBase implements VlanDao protected DomainVlanMapDao _domainVlanMapDao; @Inject protected IPAddressDao _ipAddressDao; + @Inject + protected VlanDetailsDao vlanDetailsDao; @Override public VlanVO findByZoneAndVlanId(long zoneId, String vlanId) { @@ -277,6 +282,19 @@ public class VlanDaoImpl extends GenericDaoBase implements VlanDao ZoneVlansSearch.and("zoneId", ZoneVlansSearch.entity().getDataCenterId(), SearchCriteria.Op.EQ); ZoneVlansSearch.and("vlan", ZoneVlansSearch.entity().getVlanTag(), SearchCriteria.Op.IN); ZoneVlansSearch.done(); + + ProviderVlanSearch = createSearchBuilder(); + ProviderVlanSearch.and("removed", ProviderVlanSearch.entity().getRemoved(), SearchCriteria.Op.NULL); + ProviderVlanSearch.and("dataCenterId", ProviderVlanSearch.entity().getDataCenterId(), SearchCriteria.Op.EQ); + VlanDetailsProviderSearch = vlanDetailsDao.createSearchBuilder(); + VlanDetailsProviderSearch.and("name", VlanDetailsProviderSearch.entity().getName(), SearchCriteria.Op.EQ); + VlanDetailsProviderSearch.and("value", VlanDetailsProviderSearch.entity().getValue(), SearchCriteria.Op.EQ); + ProviderVlanSearch.join("VlanDetailsProviderSearch", VlanDetailsProviderSearch, ProviderVlanSearch.entity().getId(), + VlanDetailsProviderSearch.entity().getResourceId(), JoinBuilder.JoinType.INNER); + + VlanDetailsProviderSearch.done(); + ProviderVlanSearch.done(); + return result; } @@ -434,4 +452,13 @@ public class VlanDaoImpl extends GenericDaoBase implements VlanDao return listBy(sc); } + @Override + public List listVlansForExternalNetworkProvider(long zoneId, String detailKey) { + SearchCriteria sc = ProviderVlanSearch.create(); + sc.setParameters("dataCenterId", zoneId); + sc.setJoinParameters("VlanDetailsProviderSearch", "name", detailKey); + sc.setJoinParameters("VlanDetailsProviderSearch", "value", "true"); + return search(sc, null); + } + } diff --git a/engine/schema/src/main/java/com/cloud/gpu/GpuCardVO.java b/engine/schema/src/main/java/com/cloud/gpu/GpuCardVO.java new file mode 100644 index 00000000000..2410077c84a --- /dev/null +++ b/engine/schema/src/main/java/com/cloud/gpu/GpuCardVO.java @@ -0,0 +1,147 @@ +// 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.gpu; + +import com.cloud.utils.db.GenericDao; +import org.apache.cloudstack.gpu.GpuCard; +import org.apache.cloudstack.utils.reflectiontostringbuilderutils.ReflectionToStringBuilderUtils; + +import javax.persistence.Column; +import javax.persistence.Entity; +import javax.persistence.GeneratedValue; +import javax.persistence.GenerationType; +import javax.persistence.Id; +import javax.persistence.Table; +import java.util.Date; +import java.util.UUID; + +@Entity +@Table(name = "gpu_card") +public class GpuCardVO implements GpuCard { + + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + @Column(name = "id") + private long id; + + @Column(name = "uuid") + private String uuid; + + @Column(name = "device_id") + private String deviceId; + + @Column(name = "device_name") + private String deviceName; + + @Column(name = "name") + private String name; + + @Column(name = "vendor_name") + private String vendorName; + + @Column(name = "vendor_id") + private String vendorId; + + @Column(name = GenericDao.CREATED_COLUMN) + private Date created; + + public GpuCardVO() { + this.uuid = UUID.randomUUID().toString(); + } + + public GpuCardVO(String deviceId, String deviceName, String name, String vendorName, String vendorId) { + this.uuid = UUID.randomUUID().toString(); + this.deviceId = deviceId; + this.deviceName = deviceName; + this.name = name; + this.vendorName = vendorName; + this.vendorId = vendorId; + this.created = new Date(); + } + + @Override + public String toString() { + return String.format("GPUCard %s", ReflectionToStringBuilderUtils.reflectOnlySelectedFields( + this, "id", "uuid", "name", "deviceId", "deviceName", "vendorId", "vendorName")); + } + + @Override + public long getId() { + return id; + } + + @Override + public String getUuid() { + return uuid; + } + + @Override + public String getDeviceId() { + return deviceId; + } + + public void setDeviceId(String deviceId) { + this.deviceId = deviceId; + } + + @Override + public String getDeviceName() { + return deviceName; + } + + public void setDeviceName(String deviceName) { + this.deviceName = deviceName; + } + + @Override + public String getName() { + return name; + } + + public void setName(String name) { + this.name = name; + } + + @Override + public String getVendorName() { + return vendorName; + } + + public void setVendorName(String vendorName) { + this.vendorName = vendorName; + } + + @Override + public String getVendorId() { + return vendorId; + } + + public void setVendorId(String vendorId) { + this.vendorId = vendorId; + } + + @Override + public Date getCreated() { + return created; + } + + @Override + public String getGroupName() { + return "Group of " + getVendorName() + " " + getDeviceName() + " GPUs"; + } +} diff --git a/engine/schema/src/main/java/com/cloud/gpu/GpuDeviceVO.java b/engine/schema/src/main/java/com/cloud/gpu/GpuDeviceVO.java new file mode 100644 index 00000000000..ac20e74c360 --- /dev/null +++ b/engine/schema/src/main/java/com/cloud/gpu/GpuDeviceVO.java @@ -0,0 +1,200 @@ +// 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.gpu; + +import org.apache.cloudstack.gpu.GpuDevice; +import org.apache.cloudstack.utils.reflectiontostringbuilderutils.ReflectionToStringBuilderUtils; + +import javax.persistence.Column; +import javax.persistence.Entity; +import javax.persistence.EnumType; +import javax.persistence.Enumerated; +import javax.persistence.GeneratedValue; +import javax.persistence.GenerationType; +import javax.persistence.Id; +import javax.persistence.Table; +import java.util.UUID; + +@Entity +@Table(name = "gpu_device") +public class GpuDeviceVO implements GpuDevice { + + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + @Column(name = "id") + private long id; + + @Column(name = "uuid") + private String uuid; + + @Column(name = "card_id") + private long cardId; + + @Column(name = "vgpu_profile_id") + private long vgpuProfileId; + + @Column(name = "bus_address") + private String busAddress; + + @Column(name = "host_id") + private long hostId; + + @Column(name = "vm_id") + private Long vmId; + + @Column(name = "type") + @Enumerated(value = EnumType.STRING) + private DeviceType type = DeviceType.PCI; + + @Column(name = "state") + @Enumerated(value = EnumType.STRING) + private State state = State.Free; + + @Column(name = "managed_state") + @Enumerated(value = EnumType.STRING) + private ManagedState managedState = ManagedState.Managed; + + @Column(name = "parent_gpu_device_id") + private Long parentGpuDeviceId; + + @Column(name = "numa_node") + private String numaNode; + + @Column(name = "pci_root") + private String pciRoot; + + public GpuDeviceVO() { + this.uuid = UUID.randomUUID().toString(); + } + + public GpuDeviceVO(long cardId, long vgpuProfileId, String busAddress, long hostId, Long parentGpuDeviceId, + String numaNode, String pciRoot) { + this.uuid = UUID.randomUUID().toString(); + this.cardId = cardId; + this.vgpuProfileId = vgpuProfileId; + this.busAddress = busAddress; + this.hostId = hostId; + this.parentGpuDeviceId = parentGpuDeviceId; + this.numaNode = numaNode; + this.pciRoot = pciRoot; + } + + @Override + public String toString() { + return String.format("GpuDevice %s", ReflectionToStringBuilderUtils.reflectOnlySelectedFields( + this, "id", "uuid", "cardId", "vgpuProfileId", "busAddress", "hostId", "vmId", + "parentGpuDeviceId", "numaNode", "pciRoot", "state", "resourceState")); + } + + @Override + public long getId() { + return id; + } + + public String getUuid() { + return uuid; + } + + public long getCardId() { + return cardId; + } + + public void setCardId(long cardId) { + this.cardId = cardId; + } + + public long getVgpuProfileId() { + return vgpuProfileId; + } + + public void setVgpuProfileId(long vgpuProfileId) { + this.vgpuProfileId = vgpuProfileId; + } + + public String getBusAddress() { + return busAddress; + } + + public void setBusAddress(String busAddress) { + this.busAddress = busAddress; + } + + public long getHostId() { + return hostId; + } + + public void setHostId(long hostId) { + this.hostId = hostId; + } + + @Override + public State getState() { + return state; + } + + public void setState(State state) { + this.state = state; + } + + public DeviceType getType() { + return type; + } + + public void setType(DeviceType type) { + this.type = type; + } + + public ManagedState getManagedState() { + return managedState; + } + + public void setManagedState(ManagedState managedState) { + this.managedState = managedState; + } + + public Long getVmId() { + return vmId; + } + + public void setVmId(Long vmId) { + this.vmId = vmId; + } + + public Long getParentGpuDeviceId() { + return parentGpuDeviceId; + } + + public void setParentGpuDeviceId(Long parentGpuDeviceId) { + this.parentGpuDeviceId = parentGpuDeviceId; + } + + public String getNumaNode() { + return numaNode; + } + + public void setNumaNode(String numaNode) { + this.numaNode = numaNode; + } + + public String getPciRoot() { + return pciRoot; + } + + public void setPciRoot(String pciRoot) { + this.pciRoot = pciRoot; + } +} diff --git a/engine/schema/src/main/java/com/cloud/gpu/VGPUTypesVO.java b/engine/schema/src/main/java/com/cloud/gpu/VGPUTypesVO.java index 5bbf90854ee..4944d51f1b4 100644 --- a/engine/schema/src/main/java/com/cloud/gpu/VGPUTypesVO.java +++ b/engine/schema/src/main/java/com/cloud/gpu/VGPUTypesVO.java @@ -40,19 +40,19 @@ public class VGPUTypesVO implements InternalIdentity { private String vgpuType; @Column(name="video_ram") - private long videoRam; + private Long videoRam; @Column(name="max_heads") - private long maxHeads; + private Long maxHeads; @Column(name="max_resolution_x") - private long maxResolutionX; + private Long maxResolutionX; @Column(name="max_resolution_y") - private long maxResolutionY; + private Long maxResolutionY; @Column(name="max_vgpu_per_pgpu") - private long maxVgpuPerPgpu; + private Long maxVgpuPerPgpu; @Column(name="remaining_capacity") private long remainingCapacity; @@ -63,7 +63,7 @@ public class VGPUTypesVO implements InternalIdentity { protected VGPUTypesVO() { } - public VGPUTypesVO(long gpuGroupId, String vgpuType, long videoRam, long maxHeads, long maxResolutionX, long maxResolutionY, long maxVgpuPerPgpu, + public VGPUTypesVO(long gpuGroupId, String vgpuType, Long videoRam, Long maxHeads, Long maxResolutionX, Long maxResolutionY, Long maxVgpuPerPgpu, long remainingCapacity, long maxCapacity) { this.gpuGroupId = gpuGroupId; this.vgpuType = vgpuType; @@ -92,43 +92,43 @@ public class VGPUTypesVO implements InternalIdentity { this.vgpuType = vgpuType; } - public long getVideoRam() { + public Long getVideoRam() { return videoRam; } - public void setVideoRam(long videoRam) { + public void setVideoRam(Long videoRam) { this.videoRam = videoRam; } - public long getMaxHeads() { + public Long getMaxHeads() { return maxHeads; } - public void setMaxHeads(long maxHeads) { + public void setMaxHeads(Long maxHeads) { this.maxHeads = maxHeads; } - public long getMaxResolutionX() { + public Long getMaxResolutionX() { return maxResolutionX; } - public void setMaxResolutionX(long maxResolutionX) { + public void setMaxResolutionX(Long maxResolutionX) { this.maxResolutionX = maxResolutionX; } - public long getMaxResolutionY() { + public Long getMaxResolutionY() { return maxResolutionY; } - public void setMaxResolutionY(long maxResolutionY) { + public void setMaxResolutionY(Long maxResolutionY) { this.maxResolutionY = maxResolutionY; } - public long getMaxVgpuPerPgpu() { + public Long getMaxVgpuPerPgpu() { return maxVgpuPerPgpu; } - public void setMaxVgpuPerPgpu(long maxVgpuPerPgpu) { + public void setMaxVgpuPerPgpu(Long maxVgpuPerPgpu) { this.maxVgpuPerPgpu = maxVgpuPerPgpu; } diff --git a/engine/schema/src/main/java/com/cloud/gpu/VgpuProfileVO.java b/engine/schema/src/main/java/com/cloud/gpu/VgpuProfileVO.java new file mode 100644 index 00000000000..86f5eb94415 --- /dev/null +++ b/engine/schema/src/main/java/com/cloud/gpu/VgpuProfileVO.java @@ -0,0 +1,191 @@ +// 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.gpu; + +import com.cloud.utils.db.GenericDao; +import org.apache.cloudstack.gpu.VgpuProfile; +import org.apache.cloudstack.utils.reflectiontostringbuilderutils.ReflectionToStringBuilderUtils; + +import javax.persistence.Column; +import javax.persistence.Entity; +import javax.persistence.GeneratedValue; +import javax.persistence.GenerationType; +import javax.persistence.Id; +import javax.persistence.Table; +import java.util.Date; +import java.util.UUID; + +@Entity +@Table(name = "vgpu_profile") +public class VgpuProfileVO implements VgpuProfile { + + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + @Column(name = "id") + private long id; + + @Column(name = "uuid") + private String uuid; + + @Column(name = "name") + private String name; + + @Column(name = "description") + private String description; + + @Column(name = "card_id") + private Long cardId; + + @Column(name = "max_vgpu_per_pgpu") + private Long maxVgpuPerPgpu; + + @Column(name = "video_ram") + private Long videoRam; + + @Column(name = "max_heads") + private Long maxHeads; + + @Column(name = "max_resolution_x") + private Long maxResolutionX; + + @Column(name = "max_resolution_y") + private Long maxResolutionY; + + @Column(name = GenericDao.CREATED_COLUMN) + private Date created; + + public VgpuProfileVO() { + this.uuid = UUID.randomUUID().toString(); + } + + public VgpuProfileVO(String name, String description, Long gpuCardId, Long maxVgpuPerPgpu) { + this.uuid = UUID.randomUUID().toString(); + this.name = name; + this.description = description; + this.cardId = gpuCardId; + this.maxVgpuPerPgpu = maxVgpuPerPgpu; + this.created = new Date(); + } + + + public VgpuProfileVO(String name, String description, Long gpuCardId, Long maxVgpuPerPgpu, Long videoRam, Long maxHeads, Long maxResolutionX, Long maxResolutionY) { + this.uuid = UUID.randomUUID().toString(); + this.name = name; + this.description = description; + this.cardId = gpuCardId; + this.maxVgpuPerPgpu = maxVgpuPerPgpu; + this.videoRam = videoRam; + this.maxHeads = maxHeads; + this.maxResolutionX = maxResolutionX; + this.maxResolutionY = maxResolutionY; + this.created = new Date(); + } + + @Override + public String toString() { + return String.format("VgpuProfile %s", ReflectionToStringBuilderUtils.reflectOnlySelectedFields( + this, "id", "uuid", "name", "cardId")); + } + + @Override + public long getId() { + return id; + } + + @Override + public String getUuid() { + return uuid; + } + + @Override + public String getName() { + return name; + } + + public void setName(String name) { + this.name = name; + } + + @Override + public String getDescription() { + return description; + } + + public void setDescription(String description) { + this.description = description; + } + + @Override + public Date getCreated() { + return created; + } + + @Override + public Long getCardId() { + return cardId; + } + + public void setCardId(Long cardId) { + this.cardId = cardId; + } + + @Override + public Long getMaxVgpuPerPgpu() { + return maxVgpuPerPgpu; + } + + public void setMaxVgpuPerPgpu(Long maxVgpuPerPgpu) { + this.maxVgpuPerPgpu = maxVgpuPerPgpu; + } + + @Override + public Long getVideoRam() { + return videoRam; + } + + public void setVideoRam(Long videoRam) { + this.videoRam = videoRam; + } + + @Override + public Long getMaxHeads() { + return maxHeads; + } + + public void setMaxHeads(Long maxHeads) { + this.maxHeads = maxHeads; + } + + @Override + public Long getMaxResolutionX() { + return maxResolutionX; + } + + public void setMaxResolutionX(Long maxResolutionX) { + this.maxResolutionX = maxResolutionX; + } + + @Override + public Long getMaxResolutionY() { + return maxResolutionY; + } + + public void setMaxResolutionY(Long maxResolutionY) { + this.maxResolutionY = maxResolutionY; + } +} diff --git a/engine/schema/src/main/java/com/cloud/gpu/dao/GpuCardDao.java b/engine/schema/src/main/java/com/cloud/gpu/dao/GpuCardDao.java new file mode 100644 index 00000000000..4463a690ae9 --- /dev/null +++ b/engine/schema/src/main/java/com/cloud/gpu/dao/GpuCardDao.java @@ -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.gpu.dao; + +import com.cloud.gpu.GpuCardVO; +import com.cloud.utils.Pair; +import com.cloud.utils.db.GenericDao; + +import java.util.List; + +public interface GpuCardDao extends GenericDao { + + /** + * Find GPU card by vendor and device id + * + * @param vendorId the vendor id + * @param deviceId the device id + * @return GpuCardVO + */ + GpuCardVO findByVendorIdAndDeviceId(String vendorId, String deviceId); + + Pair, Integer> searchAndCountGpuCards( + Long id, String keyword, String vendorId, String vendorName, + String deviceId, String deviceName, boolean activeOnly, Long startIndex, Long pageSize); +} diff --git a/engine/schema/src/main/java/com/cloud/gpu/dao/GpuCardDaoImpl.java b/engine/schema/src/main/java/com/cloud/gpu/dao/GpuCardDaoImpl.java new file mode 100644 index 00000000000..8aad85d4508 --- /dev/null +++ b/engine/schema/src/main/java/com/cloud/gpu/dao/GpuCardDaoImpl.java @@ -0,0 +1,122 @@ +// 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.gpu.dao; + +import com.cloud.gpu.GpuCardVO; +import com.cloud.utils.Pair; +import com.cloud.utils.db.Filter; +import com.cloud.utils.db.GenericDaoBase; +import com.cloud.utils.db.SearchBuilder; +import com.cloud.utils.db.SearchCriteria; +import org.springframework.stereotype.Component; + +import javax.inject.Inject; +import java.util.List; + +@Component +public class GpuCardDaoImpl extends GenericDaoBase implements GpuCardDao { + + private final SearchBuilder allFieldSearch; + + @Inject + private GpuDeviceDao gpuDeviceDao; + + public GpuCardDaoImpl() { + allFieldSearch = createSearchBuilder(); + allFieldSearch.and("name", allFieldSearch.entity().getName(), SearchCriteria.Op.EQ); + allFieldSearch.and("vendorId", allFieldSearch.entity().getVendorId(), SearchCriteria.Op.EQ); + allFieldSearch.and("vendorName", allFieldSearch.entity().getVendorName(), SearchCriteria.Op.EQ); + allFieldSearch.and("deviceId", allFieldSearch.entity().getDeviceId(), SearchCriteria.Op.EQ); + allFieldSearch.and("deviceName", allFieldSearch.entity().getDeviceName(), SearchCriteria.Op.EQ); + allFieldSearch.done(); + } + + @Override + public GpuCardVO findByVendorIdAndDeviceId(String vendorId, String deviceId) { + SearchCriteria sc = allFieldSearch.create(); + sc.setParameters("vendorId", vendorId); + sc.setParameters("deviceId", deviceId); + return findOneBy(sc); + } + + @Override + public Pair, Integer> searchAndCountGpuCards(Long id, String keyword, String vendorId, + String vendorName, String deviceId, String deviceName, boolean activeOnly, Long startIndex, Long pageSize + ) { + + Filter searchFilter = new Filter(GpuCardVO.class, "id", true, startIndex, pageSize); + SearchBuilder sb = createSearchBuilder(); + + if (id != null) { + sb.and("id", sb.entity().getId(), SearchCriteria.Op.EQ); + } + if (keyword != null) { + sb.op("nameKeyword", sb.entity().getName(), SearchCriteria.Op.LIKE); + sb.and("deviceNameKeyword", sb.entity().getDeviceName(), SearchCriteria.Op.LIKE); + sb.and("vendorNameKeyword", sb.entity().getVendorName(), SearchCriteria.Op.LIKE); + sb.cp(); + } + if (vendorId != null) { + sb.and("vendorId", sb.entity().getVendorId(), SearchCriteria.Op.EQ); + } + if (vendorName != null) { + sb.and("vendorName", sb.entity().getVendorName(), SearchCriteria.Op.EQ); + } + if (deviceId != null) { + sb.and("deviceId", sb.entity().getDeviceId(), SearchCriteria.Op.EQ); + } + if (deviceName != null) { + sb.and("deviceName", sb.entity().getDeviceName(), SearchCriteria.Op.EQ); + } + if (activeOnly) { + sb.and("ids", sb.entity().getId(), SearchCriteria.Op.IN); + } + sb.done(); + + // Build search criteria + SearchCriteria sc = sb.create(); + if (id != null) { + sc.setParameters("id", id); + } + if (keyword != null) { + sc.setParameters("nameKeyword", "%" + keyword + "%"); + sc.setParameters("deviceNameKeyword", "%" + keyword + "%"); + sc.setParameters("vendorNameKeyword", "%" + keyword + "%"); + } + if (vendorId != null) { + sc.setParameters("vendorId", vendorId); + } + if (vendorName != null) { + sc.setParameters("vendorName", vendorName); + } + if (deviceId != null) { + sc.setParameters("deviceId", deviceId); + } + if (deviceName != null) { + sc.setParameters("deviceName", deviceName); + } + if (activeOnly) { + List cardIds = gpuDeviceDao.getDistinctGpuCardIds(); + if (cardIds.isEmpty()) { + return new Pair<>(List.of(), 0); + } + sc.setParameters("ids", cardIds.toArray()); + } + + return searchAndCount(sc, searchFilter); + } +} diff --git a/engine/schema/src/main/java/com/cloud/gpu/dao/GpuDeviceDao.java b/engine/schema/src/main/java/com/cloud/gpu/dao/GpuDeviceDao.java new file mode 100644 index 00000000000..e362f23888d --- /dev/null +++ b/engine/schema/src/main/java/com/cloud/gpu/dao/GpuDeviceDao.java @@ -0,0 +1,71 @@ +// 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.gpu.dao; + +import com.cloud.gpu.GpuDeviceVO; +import com.cloud.utils.Pair; +import com.cloud.utils.db.GenericDao; + +import java.util.List; + +public interface GpuDeviceDao extends GenericDao { + + List listByIds(List ids); + + /** + * Find GPU device by host ID and bus address + * + * @param hostId the host ID + * @param busAddress the PCI bus address + * @return GpuDeviceVO + */ + GpuDeviceVO findByHostIdAndBusAddress(long hostId, String busAddress); + + /** + * List GPU devices by host ID + * + * @param hostId the ID of the host + * @return a list of GPU devices for the host + */ + List listByHostId(long hostId); + + /** + * List GPU devices by VM ID + * + * @param vmId the VM ID + * @return list of GpuDeviceVO + */ + List listByVmId(long vmId); + + boolean isVgpuProfileInUse(long vgpuProfileId); + + boolean isGpuCardInUse(long cardId); + + List listByHostAndVm(Long hostId, long vmId); + + List listDevicesForAllocation(Long hostId, Long vgpuProfileId); + + Pair, Integer> searchAndCountGpuDevices( + Long id, String keyword, Long hostId, Long vmId, Long gpuCardId, Long vgpuProfileId, + Long startIndex, Long pageSize); + + List getDistinctGpuCardIds(); + + List getDistinctVgpuProfileIds(); + + List listByParentGpuDeviceId(Long parentGpuDeviceId); +} diff --git a/engine/schema/src/main/java/com/cloud/gpu/dao/GpuDeviceDaoImpl.java b/engine/schema/src/main/java/com/cloud/gpu/dao/GpuDeviceDaoImpl.java new file mode 100644 index 00000000000..bd7032aff27 --- /dev/null +++ b/engine/schema/src/main/java/com/cloud/gpu/dao/GpuDeviceDaoImpl.java @@ -0,0 +1,260 @@ +// 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.gpu.dao; + +import com.cloud.gpu.GpuCardVO; +import com.cloud.gpu.GpuDeviceVO; +import com.cloud.gpu.VgpuProfileVO; +import com.cloud.utils.Pair; +import com.cloud.utils.db.Filter; +import com.cloud.utils.db.GenericDaoBase; +import com.cloud.utils.db.JoinBuilder; +import com.cloud.utils.db.SearchBuilder; +import com.cloud.utils.db.SearchCriteria; +import org.apache.cloudstack.gpu.GpuDevice; +import org.apache.commons.collections.CollectionUtils; +import org.springframework.stereotype.Component; + +import javax.inject.Inject; +import javax.naming.ConfigurationException; +import java.util.Collections; +import java.util.List; +import java.util.Map; +import java.util.stream.Collectors; + +@Component +public class GpuDeviceDaoImpl extends GenericDaoBase implements GpuDeviceDao { + + private static final String IDS = "ids"; + private static final String HOST_ID = "hostId"; + private static final String VM_ID = "vmId"; + private static final String BUS_ADDRESS = "busAddress"; + private static final String CARD_ID = "cardId"; + private static final String VGPU_PROFILE_ID = "vgpuProfileId"; + private static final String PARENT_GPU_DEVICE_ID = "parentGpuDeviceId"; + private static final String STATE = "state"; + private static final String MANAGED_STATE = "managedState"; + private static final String TYPE = "type"; + private final SearchBuilder allFieldSearch; + private SearchBuilder devicesForAllocationSearch; + @Inject + private GpuCardDao gpuCardDao; + @Inject + private VgpuProfileDao vgpuProfileDao; + + public GpuDeviceDaoImpl() { + allFieldSearch = createSearchBuilder(); + allFieldSearch.and(IDS, allFieldSearch.entity().getId(), SearchCriteria.Op.IN); + allFieldSearch.and(HOST_ID, allFieldSearch.entity().getHostId(), SearchCriteria.Op.EQ); + allFieldSearch.and(CARD_ID, allFieldSearch.entity().getCardId(), SearchCriteria.Op.EQ); + allFieldSearch.and(BUS_ADDRESS, allFieldSearch.entity().getBusAddress(), SearchCriteria.Op.EQ); + allFieldSearch.and(STATE, allFieldSearch.entity().getState(), SearchCriteria.Op.EQ); + allFieldSearch.and(VGPU_PROFILE_ID, allFieldSearch.entity().getVgpuProfileId(), SearchCriteria.Op.EQ); + allFieldSearch.and(PARENT_GPU_DEVICE_ID, allFieldSearch.entity().getParentGpuDeviceId(), SearchCriteria.Op.EQ); + allFieldSearch.and(VM_ID, allFieldSearch.entity().getVmId(), SearchCriteria.Op.EQ); + allFieldSearch.done(); + + devicesForAllocationSearch = createSearchBuilder(); + devicesForAllocationSearch.and(HOST_ID, devicesForAllocationSearch.entity().getHostId(), SearchCriteria.Op.EQ); + devicesForAllocationSearch.and(VGPU_PROFILE_ID, devicesForAllocationSearch.entity().getVgpuProfileId(), SearchCriteria.Op.IN); + devicesForAllocationSearch.and(STATE, devicesForAllocationSearch.entity().getState(), SearchCriteria.Op.EQ); + devicesForAllocationSearch.and(MANAGED_STATE, devicesForAllocationSearch.entity().getManagedState(), SearchCriteria.Op.EQ); + devicesForAllocationSearch.and(TYPE, devicesForAllocationSearch.entity().getType(), SearchCriteria.Op.NEQ); + devicesForAllocationSearch.done(); + } + + @Override + public boolean configure(String name, Map params) throws ConfigurationException { + return super.configure(name, params); + } + + @Override + public List listByIds(List ids) { + if (CollectionUtils.isEmpty(ids)) { + return Collections.emptyList(); + } + SearchCriteria sc = allFieldSearch.create(); + sc.setParameters(IDS, ids.toArray()); + return listBy(sc); + } + + @Override + public GpuDeviceVO findByHostIdAndBusAddress(long hostId, String busAddress) { + SearchCriteria sc = allFieldSearch.create(); + sc.setParameters(HOST_ID, hostId); + sc.setParameters(BUS_ADDRESS, busAddress); + return findOneBy(sc); + } + + @Override + public List listByHostId(long hostId) { + SearchCriteria sc = allFieldSearch.create(); + sc.setParameters(HOST_ID, hostId); + return listBy(sc); + } + + @Override + public List listByVmId(long vmId) { + SearchCriteria sc = allFieldSearch.create(); + sc.setParameters(VM_ID, vmId); + return listBy(sc); + } + + @Override + public boolean isVgpuProfileInUse(long vgpuProfileId) { + SearchCriteria sc = allFieldSearch.create(); + sc.setParameters(VGPU_PROFILE_ID, vgpuProfileId); + return getCount(sc) > 0; + } + + @Override + public boolean isGpuCardInUse(long cardId) { + SearchCriteria sc = allFieldSearch.create(); + sc.setParameters(CARD_ID, cardId); + return getCount(sc) > 0; + } + + @Override + public List listByHostAndVm(Long hostId, long vmId) { + SearchCriteria sc = allFieldSearch.create(); + sc.setParameters(HOST_ID, hostId); + sc.setParameters(VM_ID, vmId); + return search(sc, null); + } + + @Override + public List listDevicesForAllocation(Long hostId, Long vgpuProfileId) { + SearchCriteria sc = devicesForAllocationSearch.create(); + sc.setParameters(HOST_ID, hostId); + sc.setParameters(VGPU_PROFILE_ID, vgpuProfileId); + sc.setParameters(STATE, GpuDevice.State.Free); + sc.setParameters(MANAGED_STATE, GpuDevice.ManagedState.Managed); + sc.setParameters(TYPE, GpuDevice.DeviceType.VGPUOnly); + return search(sc, null); + } + + @Override + public Pair, Integer> searchAndCountGpuDevices(Long id, String keyword, Long hostId, Long vmId, + Long gpuCardId, Long vgpuProfileId, Long startIndex, Long pageSize) { + Filter searchFilter = new Filter(GpuDeviceVO.class, "id", true, startIndex, pageSize); + SearchBuilder sb = createSearchBuilder(); + + if (id != null) { + sb.and("id", sb.entity().getId(), SearchCriteria.Op.EQ); + } + if (hostId != null) { + sb.and("hostId", sb.entity().getHostId(), SearchCriteria.Op.EQ); + } + if (vmId != null) { + sb.and("vmId", sb.entity().getVmId(), SearchCriteria.Op.EQ); + } + if (gpuCardId != null) { + sb.and("cardId", sb.entity().getCardId(), SearchCriteria.Op.EQ); + } + if (vgpuProfileId != null) { + sb.and("vgpuProfileId", sb.entity().getVgpuProfileId(), SearchCriteria.Op.EQ); + } + if (keyword != null) { + SearchBuilder cardSb = gpuCardDao.createSearchBuilder(); + SearchBuilder profileSb = vgpuProfileDao.createSearchBuilder(); + sb.join("cardJoin", cardSb, sb.entity().getCardId(), cardSb.entity().getId(), JoinBuilder.JoinType.INNER); + sb.join("profileJoin", profileSb, sb.entity().getCardId(), profileSb.entity().getId(), + JoinBuilder.JoinType.INNER); + + sb.op("cardNameKeyword", cardSb.entity().getName(), SearchCriteria.Op.LIKE); + sb.or("cardNameKeyword", cardSb.entity().getVendorName(), SearchCriteria.Op.LIKE); + sb.or("cardNameKeyword", cardSb.entity().getDeviceName(), SearchCriteria.Op.LIKE); + + sb.op("profileNameKeyword", profileSb.entity().getName(), SearchCriteria.Op.LIKE); + sb.op("profileDescriptionKeyword", profileSb.entity().getDescription(), SearchCriteria.Op.LIKE); + sb.cp(); + } + + sb.done(); + + // Build search criteria + SearchCriteria sc = sb.create(); + if (id != null) { + sc.setParameters("id", id); + } + if (hostId != null) { + sc.setParameters("hostId", hostId); + } + if (vmId != null) { + sc.setParameters("vmId", vmId); + } + if (gpuCardId != null) { + sc.setParameters("cardId", gpuCardId); + } + if (vgpuProfileId != null) { + sc.setParameters("vgpuProfileId", vgpuProfileId); + } + + if (keyword != null) { + sc.setJoinParameters("cardJoin", "cardNameKeyword", "%" + keyword + "%"); + sc.setJoinParameters("cardJoin", "cardNameKeyword", "%" + keyword + "%"); + sc.setJoinParameters("cardJoin", "cardNameKeyword", "%" + keyword + "%"); + sc.setJoinParameters("profileJoin", "profileNameKeyword", "%" + keyword + "%"); + sc.setJoinParameters("profileJoin", "profileDescriptionKeyword", "%" + keyword + "%"); + } + + return searchAndCount(sc, searchFilter); + } + + @Override + public List getDistinctGpuCardIds() { + SearchBuilder sb = createSearchBuilder(); + sb.select(null, SearchCriteria.Func.DISTINCT, sb.entity().getCardId()); + sb.done(); + SearchCriteria sc = sb.create(); + + List gpuDevices = listBy(sc); + if (CollectionUtils.isEmpty(gpuDevices)) { + return Collections.emptyList(); + } + + return gpuDevices.stream() + .map(GpuDeviceVO::getCardId) + .distinct() + .collect(Collectors.toList()); + } + + @Override + public List getDistinctVgpuProfileIds() { + SearchBuilder sb = createSearchBuilder(); + sb.select(null, SearchCriteria.Func.DISTINCT, sb.entity().getVgpuProfileId()); + sb.done(); + SearchCriteria sc = sb.create(); + + List gpuDevices = listBy(sc); + if (CollectionUtils.isEmpty(gpuDevices)) { + return Collections.emptyList(); + } + + return gpuDevices.stream() + .map(GpuDeviceVO::getVgpuProfileId) + .distinct() + .collect(Collectors.toList()); + } + + @Override + public List listByParentGpuDeviceId(Long parentGpuDeviceId) { + SearchCriteria sc = allFieldSearch.create(); + sc.setParameters(PARENT_GPU_DEVICE_ID, parentGpuDeviceId); + return listBy(sc); + } +} diff --git a/engine/schema/src/main/java/com/cloud/gpu/dao/HostGpuGroupsDao.java b/engine/schema/src/main/java/com/cloud/gpu/dao/HostGpuGroupsDao.java index 8e4f2f742ac..99e33617539 100644 --- a/engine/schema/src/main/java/com/cloud/gpu/dao/HostGpuGroupsDao.java +++ b/engine/schema/src/main/java/com/cloud/gpu/dao/HostGpuGroupsDao.java @@ -19,6 +19,7 @@ package com.cloud.gpu.dao; import java.util.List; import com.cloud.gpu.HostGpuGroupsVO; +import com.cloud.utils.Pair; import com.cloud.utils.db.GenericDao; public interface HostGpuGroupsDao extends GenericDao { @@ -57,4 +58,15 @@ public interface HostGpuGroupsDao extends GenericDao { */ void persist(long hostId, List gpuGroups); + + /** + * Returns max and remaining GPU capacity + * + * @param dcId + * @param podId + * @param clusterId + * @param hostId + * @return Pair containing max GPU capacity and remaining GPU capacity + */ + Pair getGpuStats(Long dcId, Long podId, Long clusterId, Long hostId); } diff --git a/engine/schema/src/main/java/com/cloud/gpu/dao/HostGpuGroupsDaoImpl.java b/engine/schema/src/main/java/com/cloud/gpu/dao/HostGpuGroupsDaoImpl.java index 30535c7e27d..343b144597c 100644 --- a/engine/schema/src/main/java/com/cloud/gpu/dao/HostGpuGroupsDaoImpl.java +++ b/engine/schema/src/main/java/com/cloud/gpu/dao/HostGpuGroupsDaoImpl.java @@ -16,9 +16,16 @@ // under the License. package com.cloud.gpu.dao; +import java.sql.PreparedStatement; +import java.sql.ResultSet; +import java.sql.SQLException; +import java.util.ArrayList; import java.util.List; +import com.cloud.utils.Pair; +import com.cloud.utils.db.TransactionLegacy; +import com.cloud.utils.exception.CloudRuntimeException; import org.springframework.stereotype.Component; import com.cloud.gpu.HostGpuGroupsVO; @@ -87,4 +94,75 @@ public class HostGpuGroupsDaoImpl extends GenericDaoBase sc.setParameters("hostId", hostId); remove(sc); } + + @Override + public Pair getGpuStats(Long dcId, Long podId, Long clusterId, Long hostId) { + TransactionLegacy txn = TransactionLegacy.currentTxn(); + Pair result = null; + List resourceIdList = new ArrayList<>(); + String query = getStatsQuery(resourceIdList, dcId, podId, clusterId, hostId); + + try { + PreparedStatement pstmt = txn.prepareAutoCloseStatement(query); + for (int i = 0; i < resourceIdList.size(); i++) { + pstmt.setLong(1 + i, resourceIdList.get(i)); + } + + ResultSet rs = pstmt.executeQuery(); + while (rs.next()) { + result = new Pair<>(rs.getLong(1), rs.getLong(2)); + } + return result; + } catch (SQLException e) { + throw new CloudRuntimeException("Error while fetching GPU stats: " + e.getMessage(), e); + } catch (Throwable e) { + throw new CloudRuntimeException("Caught: " + query, e); + } + } + + private String getStatsQuery(List resourceIdList, Long dcId, Long podId, Long clusterId, Long hostId) { + StringBuilder query = new StringBuilder("SELECT SUM(max_capacity), SUM(remaining_capacity)" + + "FROM vgpu_types " + + "WHERE" + + " gpu_group_id IN (" + + " SELECT" + + " host_gpu_groups.id" + + " FROM" + + " host_gpu_groups" + + " INNER JOIN host ON host.id = host_gpu_groups.host_id "); + if (dcId != null) { + query.append("WHERE host.data_center_id = ? "); + resourceIdList.add(dcId); + } + + if (podId != null) { + if (resourceIdList.isEmpty()) { + query.append("WHERE "); + } else { + query.append("AND "); + } + query.append(" host.pod_id = ? "); + resourceIdList.add(podId); + } + if (clusterId != null) { + if (resourceIdList.isEmpty()) { + query.append("WHERE "); + } else { + query.append("AND "); + } + query.append(" host.cluster_id = ? "); + resourceIdList.add(clusterId); + } + if (hostId != null) { + if (resourceIdList.isEmpty()) { + query.append("WHERE "); + } else { + query.append("AND "); + } + query.append(" host.id = ? "); + resourceIdList.add(hostId); + } + query.append(" )"); + return query.toString(); + } } diff --git a/engine/schema/src/main/java/com/cloud/gpu/dao/VGPUTypesDaoImpl.java b/engine/schema/src/main/java/com/cloud/gpu/dao/VGPUTypesDaoImpl.java index edc5e1f67c8..524feed2467 100644 --- a/engine/schema/src/main/java/com/cloud/gpu/dao/VGPUTypesDaoImpl.java +++ b/engine/schema/src/main/java/com/cloud/gpu/dao/VGPUTypesDaoImpl.java @@ -16,6 +16,17 @@ //under the License. package com.cloud.gpu.dao; +import com.cloud.agent.api.VgpuTypesInfo; +import com.cloud.gpu.HostGpuGroupsVO; +import com.cloud.gpu.VGPUTypesVO; +import com.cloud.utils.db.GenericDaoBase; +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 org.springframework.stereotype.Component; + +import javax.inject.Inject; import java.sql.PreparedStatement; import java.sql.ResultSet; import java.sql.SQLException; @@ -25,42 +36,30 @@ import java.util.Iterator; import java.util.List; import java.util.Map.Entry; -import javax.inject.Inject; - -import org.springframework.stereotype.Component; - -import com.cloud.agent.api.VgpuTypesInfo; -import com.cloud.gpu.HostGpuGroupsVO; -import com.cloud.gpu.VGPUTypesVO; -import com.cloud.utils.db.GenericDaoBase; -import com.cloud.utils.db.SearchBuilder; -import com.cloud.utils.db.SearchCriteria; -import com.cloud.utils.db.TransactionLegacy; -import com.cloud.utils.exception.CloudRuntimeException; - @Component public class VGPUTypesDaoImpl extends GenericDaoBase implements VGPUTypesDao { - private final SearchBuilder _searchByGroupId; - private final SearchBuilder _searchByGroupIdVGPUType; - - @Inject protected HostGpuGroupsDao _hostGpuGroupsDao; + @Inject + protected HostGpuGroupsDao hostGpuGroupsDao; private static final String LIST_ZONE_POD_CLUSTER_WIDE_GPU_CAPACITIES = "SELECT host_gpu_groups.group_name, vgpu_type, max_vgpu_per_pgpu, SUM(remaining_capacity) AS remaining_capacity, SUM(max_capacity) AS total_capacity FROM" + - " `cloud`.`vgpu_types` INNER JOIN `cloud`.`host_gpu_groups` ON vgpu_types.gpu_group_id = host_gpu_groups.id INNER JOIN `cloud`.`host`" + - " ON host_gpu_groups.host_id = host.id WHERE host.type = 'Routing' AND host.data_center_id = ?"; + " `cloud`.`vgpu_types` INNER JOIN `cloud`.`host_gpu_groups` ON vgpu_types.gpu_group_id = host_gpu_groups.id INNER JOIN `cloud`.`host`" + + " ON host_gpu_groups.host_id = host.id WHERE host.type = 'Routing' AND vgpu_types.max_capacity > 0 AND host.data_center_id = ?"; + + private final SearchBuilder searchByGroupId; + private final SearchBuilder searchByGroupIdVGPUType; public VGPUTypesDaoImpl() { - _searchByGroupId = createSearchBuilder(); - _searchByGroupId.and("groupId", _searchByGroupId.entity().getGpuGroupId(), SearchCriteria.Op.EQ); - _searchByGroupId.done(); + searchByGroupId = createSearchBuilder(); + searchByGroupId.and("groupId", searchByGroupId.entity().getGpuGroupId(), SearchCriteria.Op.EQ); + searchByGroupId.done(); - _searchByGroupIdVGPUType = createSearchBuilder(); - _searchByGroupIdVGPUType.and("groupId", _searchByGroupIdVGPUType.entity().getGpuGroupId(), SearchCriteria.Op.EQ); - _searchByGroupIdVGPUType.and("vgpuType", _searchByGroupIdVGPUType.entity().getVgpuType(), SearchCriteria.Op.EQ); - _searchByGroupIdVGPUType.done(); + searchByGroupIdVGPUType = createSearchBuilder(); + searchByGroupIdVGPUType.and("groupId", searchByGroupIdVGPUType.entity().getGpuGroupId(), SearchCriteria.Op.EQ); + searchByGroupIdVGPUType.and("vgpuType", searchByGroupIdVGPUType.entity().getVgpuType(), SearchCriteria.Op.EQ); + searchByGroupIdVGPUType.done(); } @Override @@ -83,7 +82,7 @@ public class VGPUTypesDaoImpl extends GenericDaoBase implemen finalQuery.append(" AND host.cluster_id = ?"); resourceIdList.add(clusterId); } - finalQuery.append(" GROUP BY host_gpu_groups.group_name, vgpu_type"); + finalQuery.append(" GROUP BY host_gpu_groups.group_name, vgpu_type, max_vgpu_per_pgpu"); try { pstmt = txn.prepareAutoCloseStatement(finalQuery.toString()); @@ -106,14 +105,14 @@ public class VGPUTypesDaoImpl extends GenericDaoBase implemen @Override public List listByGroupId(long groupId) { - SearchCriteria sc = _searchByGroupId.create(); + SearchCriteria sc = searchByGroupId.create(); sc.setParameters("groupId", groupId); return listBy(sc); } @Override public VGPUTypesVO findByGroupIdVGPUType(long groupId, String vgpuType) { - SearchCriteria sc = _searchByGroupIdVGPUType.create(); + SearchCriteria sc = searchByGroupIdVGPUType.create(); sc.setParameters("groupId", groupId); sc.setParameters("vgpuType", vgpuType); return findOneBy(sc); @@ -124,7 +123,7 @@ public class VGPUTypesDaoImpl extends GenericDaoBase implemen Iterator>> it1 = groupDetails.entrySet().iterator(); while (it1.hasNext()) { Entry> entry = it1.next(); - HostGpuGroupsVO gpuGroup = _hostGpuGroupsDao.findByHostIdGroupName(hostId, entry.getKey()); + HostGpuGroupsVO gpuGroup = hostGpuGroupsDao.findByHostIdGroupName(hostId, entry.getKey()); HashMap values = entry.getValue(); Iterator> it2 = values.entrySet().iterator(); while (it2.hasNext()) { diff --git a/engine/schema/src/main/java/com/cloud/gpu/dao/VgpuProfileDao.java b/engine/schema/src/main/java/com/cloud/gpu/dao/VgpuProfileDao.java new file mode 100644 index 00000000000..2628f1851f2 --- /dev/null +++ b/engine/schema/src/main/java/com/cloud/gpu/dao/VgpuProfileDao.java @@ -0,0 +1,33 @@ +// Licensed to the Apache Software Foundation (ASF) under one +// or more contributor license agreements. See the NOTICE file +// distributed with this work for additional information +// regarding copyright ownership. The ASF licenses this file +// to you under the Apache License, Version 2.0 (the +// "License"); you may not use this file except in compliance +// with the License. You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, +// software distributed under the License is distributed on an +// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +// KIND, either express or implied. See the License for the +// specific language governing permissions and limitations +// under the License. +package com.cloud.gpu.dao; + +import com.cloud.gpu.VgpuProfileVO; +import com.cloud.utils.Pair; +import com.cloud.utils.db.GenericDao; + +import java.util.List; + +public interface VgpuProfileDao extends GenericDao { + + VgpuProfileVO findByNameAndCardId(String name, long cardId); + + int removeByCardId(long cardId); + + Pair, Integer> searchAndCountVgpuProfiles(Long id, String name, String keyword, Long gpuCardId, + boolean activeOnly, Long startIndex, Long pageSize); +} diff --git a/engine/schema/src/main/java/com/cloud/gpu/dao/VgpuProfileDaoImpl.java b/engine/schema/src/main/java/com/cloud/gpu/dao/VgpuProfileDaoImpl.java new file mode 100644 index 00000000000..11dd7edb30d --- /dev/null +++ b/engine/schema/src/main/java/com/cloud/gpu/dao/VgpuProfileDaoImpl.java @@ -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 com.cloud.gpu.dao; + +import com.cloud.gpu.VgpuProfileVO; +import com.cloud.utils.Pair; +import com.cloud.utils.db.Filter; +import com.cloud.utils.db.GenericDaoBase; +import com.cloud.utils.db.SearchBuilder; +import com.cloud.utils.db.SearchCriteria; +import org.springframework.stereotype.Component; + +import javax.inject.Inject; +import java.util.List; + +@Component +public class VgpuProfileDaoImpl extends GenericDaoBase implements VgpuProfileDao { + + private final SearchBuilder allFieldSearch; + + @Inject + private GpuDeviceDao gpuDeviceDao; + + public VgpuProfileDaoImpl() { + allFieldSearch = createSearchBuilder(); + allFieldSearch.and("name", allFieldSearch.entity().getName(), SearchCriteria.Op.EQ); + allFieldSearch.and("cardId", allFieldSearch.entity().getCardId(), SearchCriteria.Op.IN); + allFieldSearch.done(); + } + + @Override + public VgpuProfileVO findByNameAndCardId(String name, long cardId) { + SearchCriteria sc = allFieldSearch.create(); + sc.setParameters("name", name); + sc.setParameters("cardId", cardId); + return findOneBy(sc); + } + + @Override + public int removeByCardId(long cardId) { + SearchCriteria sc = allFieldSearch.create(); + sc.setParameters("cardId", cardId); + return remove(sc); + } + + @Override + public Pair, Integer> searchAndCountVgpuProfiles(Long id, String name, String keyword, + Long gpuCardId, boolean activeOnly, Long startIndex, Long pageSize) { + Filter searchFilter = new Filter(VgpuProfileVO.class, "id", true, startIndex, pageSize); + SearchBuilder sb = createSearchBuilder(); + + if (id != null) { + sb.and("id", sb.entity().getId(), SearchCriteria.Op.EQ); + } + if (name != null) { + sb.and("name", sb.entity().getName(), SearchCriteria.Op.EQ); + } + if (keyword != null) { + sb.and("keywordName", sb.entity().getName(), SearchCriteria.Op.LIKE); + sb.and("keywordDescription", sb.entity().getDescription(), SearchCriteria.Op.LIKE); + } + if (gpuCardId != null) { + sb.and("cardId", sb.entity().getCardId(), SearchCriteria.Op.EQ); + } + if (activeOnly) { + sb.and("ids", sb.entity().getId(), SearchCriteria.Op.IN); + } + sb.done(); + + // Build search criteria + SearchCriteria sc = sb.create(); + if (id != null) { + sc.setParameters("id", id); + } + if (name != null) { + sc.setParameters("name", name); + } + if (keyword != null) { + sc.setParameters("keywordName", "%" + keyword + "%"); + sc.setParameters("keywordDescription", "%" + keyword + "%"); + } + if (gpuCardId != null) { + sc.setParameters("cardId", gpuCardId); + } + + if (activeOnly) { + List vgpuProfileIds = gpuDeviceDao.getDistinctVgpuProfileIds(); + if (vgpuProfileIds.isEmpty()) { + return new Pair<>(List.of(), 0); + } + sc.setParameters("ids", vgpuProfileIds.toArray()); + } + + return searchAndCount(sc, searchFilter); + } +} diff --git a/engine/schema/src/main/java/com/cloud/host/dao/HostDetailsDao.java b/engine/schema/src/main/java/com/cloud/host/dao/HostDetailsDao.java index 8dc4efa91f3..5d8bd0a0a3a 100644 --- a/engine/schema/src/main/java/com/cloud/host/dao/HostDetailsDao.java +++ b/engine/schema/src/main/java/com/cloud/host/dao/HostDetailsDao.java @@ -32,4 +32,7 @@ public interface HostDetailsDao extends GenericDao { void deleteDetails(long hostId); List findByName(String name); + + void replaceExternalDetails(long hostId, Map details); + } diff --git a/engine/schema/src/main/java/com/cloud/host/dao/HostDetailsDaoImpl.java b/engine/schema/src/main/java/com/cloud/host/dao/HostDetailsDaoImpl.java index 9c1340592f9..3eb9faeb1c1 100644 --- a/engine/schema/src/main/java/com/cloud/host/dao/HostDetailsDaoImpl.java +++ b/engine/schema/src/main/java/com/cloud/host/dao/HostDetailsDaoImpl.java @@ -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 implements HostDetailsDao { @@ -130,4 +132,34 @@ public class HostDetailsDaoImpl extends GenericDaoBase implement sc.setParameters("name", name); return listBy(sc); } + + @Override + public void replaceExternalDetails(long hostId, Map details) { + if (details.isEmpty()) { + return; + } + TransactionLegacy txn = TransactionLegacy.currentTxn(); + txn.start(); + List detailVOs = new ArrayList<>(); + for (Map.Entry 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 sb = createSearchBuilder(); + sb.and("hostId", sb.entity().getHostId(), SearchCriteria.Op.EQ); + sb.and("name", sb.entity().getName(), SearchCriteria.Op.LIKE); + sb.done(); + SearchCriteria sc = sb.create(); + sc.setParameters("hostId", hostId); + sc.setParameters("name", VmDetailConstants.EXTERNAL_DETAIL_PREFIX + "%"); + remove(sc); + for (DetailVO detail : detailVOs) { + persist(detail); + } + txn.commit(); + } } diff --git a/engine/schema/src/main/java/com/cloud/network/as/dao/AutoScaleVmGroupVmMapDao.java b/engine/schema/src/main/java/com/cloud/network/as/dao/AutoScaleVmGroupVmMapDao.java index 718511746c2..9775f8ad5b1 100644 --- a/engine/schema/src/main/java/com/cloud/network/as/dao/AutoScaleVmGroupVmMapDao.java +++ b/engine/schema/src/main/java/com/cloud/network/as/dao/AutoScaleVmGroupVmMapDao.java @@ -37,4 +37,6 @@ public interface AutoScaleVmGroupVmMapDao extends GenericDao vmIds, Long batchSize); + + int getErroredInstanceCount(long vmGroupId); } diff --git a/engine/schema/src/main/java/com/cloud/network/as/dao/AutoScaleVmGroupVmMapDaoImpl.java b/engine/schema/src/main/java/com/cloud/network/as/dao/AutoScaleVmGroupVmMapDaoImpl.java index 1ae55d97da2..b2f4e578a82 100644 --- a/engine/schema/src/main/java/com/cloud/network/as/dao/AutoScaleVmGroupVmMapDaoImpl.java +++ b/engine/schema/src/main/java/com/cloud/network/as/dao/AutoScaleVmGroupVmMapDaoImpl.java @@ -127,4 +127,13 @@ public class AutoScaleVmGroupVmMapDaoImpl extends GenericDaoBase sc = CountBy.create(); + sc.setParameters("vmGroupId", vmGroupId); + sc.setJoinParameters("vmSearch", "states", State.Error); + final List results = customSearch(sc, null); + return results.get(0); + } } diff --git a/engine/schema/src/main/java/com/cloud/network/dao/IPAddressDaoImpl.java b/engine/schema/src/main/java/com/cloud/network/dao/IPAddressDaoImpl.java index 5499d04e3a1..0a5ecd25667 100644 --- a/engine/schema/src/main/java/com/cloud/network/dao/IPAddressDaoImpl.java +++ b/engine/schema/src/main/java/com/cloud/network/dao/IPAddressDaoImpl.java @@ -197,6 +197,7 @@ public class IPAddressDaoImpl extends GenericDaoBase implemen address.setSourceNat(false); address.setOneToOneNat(false); address.setAssociatedWithVmId(null); + address.setForRouter(false); address.setState(State.Free); address.setAssociatedWithNetworkId(null); address.setVpcId(null); diff --git a/engine/schema/src/main/java/com/cloud/network/dao/IPAddressVO.java b/engine/schema/src/main/java/com/cloud/network/dao/IPAddressVO.java index 88e146d2a80..a3a65fdb01b 100644 --- a/engine/schema/src/main/java/com/cloud/network/dao/IPAddressVO.java +++ b/engine/schema/src/main/java/com/cloud/network/dao/IPAddressVO.java @@ -117,6 +117,9 @@ public class IPAddressVO implements IpAddress { @Column(name = "forsystemvms") private boolean forSystemVms = false; + @Column(name = "for_router") + private boolean forRouter = false; + @Column(name= GenericDao.REMOVED_COLUMN) private Date removed; @@ -388,4 +391,13 @@ public class IPAddressVO implements IpAddress { public boolean isForSystemVms() { return forSystemVms; } + + @Override + public boolean isForRouter() { + return forRouter; + } + + public void setForRouter(boolean forRouter) { + this.forRouter = forRouter; + } } diff --git a/engine/schema/src/main/java/com/cloud/network/dao/NetrisProviderDao.java b/engine/schema/src/main/java/com/cloud/network/dao/NetrisProviderDao.java new file mode 100644 index 00000000000..fe21f72e4db --- /dev/null +++ b/engine/schema/src/main/java/com/cloud/network/dao/NetrisProviderDao.java @@ -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 com.cloud.network.dao; + +import com.cloud.network.element.NetrisProviderVO; +import com.cloud.utils.db.GenericDao; + +public interface NetrisProviderDao extends GenericDao { + NetrisProviderVO findByZoneId(long zoneId); +} diff --git a/engine/schema/src/main/java/com/cloud/network/dao/NetrisProviderDaoImpl.java b/engine/schema/src/main/java/com/cloud/network/dao/NetrisProviderDaoImpl.java new file mode 100644 index 00000000000..86ea04f1db0 --- /dev/null +++ b/engine/schema/src/main/java/com/cloud/network/dao/NetrisProviderDaoImpl.java @@ -0,0 +1,52 @@ +// Licensed to the Apache Software Foundation (ASF) under one +// or more contributor license agreements. See the NOTICE file +// distributed with this work for additional information +// regarding copyright ownership. The ASF licenses this file +// to you under the Apache License, Version 2.0 (the +// "License"); you may not use this file except in compliance +// with the License. You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, +// software distributed under the License is distributed on an +// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +// KIND, either express or implied. See the License for the +// specific language governing permissions and limitations +// under the License. +package com.cloud.network.dao; + +import com.cloud.network.element.NetrisProviderVO; +import com.cloud.utils.db.DB; +import com.cloud.utils.db.GenericDaoBase; +import com.cloud.utils.db.SearchBuilder; +import com.cloud.utils.db.SearchCriteria; +import org.springframework.stereotype.Component; + +@Component +@DB() +public class NetrisProviderDaoImpl extends GenericDaoBase implements NetrisProviderDao { + + final SearchBuilder allFieldsSearch; + + public NetrisProviderDaoImpl() { + super(); + allFieldsSearch = createSearchBuilder(); + allFieldsSearch.and("id", allFieldsSearch.entity().getId(), + SearchCriteria.Op.EQ); + allFieldsSearch.and("uuid", allFieldsSearch.entity().getUuid(), + SearchCriteria.Op.EQ); + allFieldsSearch.and("hostname", allFieldsSearch.entity().getUrl(), + SearchCriteria.Op.EQ); + allFieldsSearch.and("zone_id", allFieldsSearch.entity().getZoneId(), + SearchCriteria.Op.EQ); + allFieldsSearch.done(); + } + + @Override + public NetrisProviderVO findByZoneId(long zoneId) { + SearchCriteria sc = allFieldsSearch.create(); + sc.setParameters("zone_id", zoneId); + return findOneBy(sc); + } +} diff --git a/engine/schema/src/main/java/com/cloud/network/dao/PhysicalNetworkTrafficTypeDaoImpl.java b/engine/schema/src/main/java/com/cloud/network/dao/PhysicalNetworkTrafficTypeDaoImpl.java index 4811b59d31e..09d9f1d7fbf 100644 --- a/engine/schema/src/main/java/com/cloud/network/dao/PhysicalNetworkTrafficTypeDaoImpl.java +++ b/engine/schema/src/main/java/com/cloud/network/dao/PhysicalNetworkTrafficTypeDaoImpl.java @@ -125,7 +125,7 @@ public class PhysicalNetworkTrafficTypeDaoImpl extends GenericDaoBase List findByAccount(Long accountId); List listByNetworkId(Long networkId); + + List listByVpcId(Long vpcId); } diff --git a/engine/schema/src/main/java/com/cloud/network/dao/RemoteAccessVpnDaoImpl.java b/engine/schema/src/main/java/com/cloud/network/dao/RemoteAccessVpnDaoImpl.java index 484aa6f6631..ccbc60a5562 100644 --- a/engine/schema/src/main/java/com/cloud/network/dao/RemoteAccessVpnDaoImpl.java +++ b/engine/schema/src/main/java/com/cloud/network/dao/RemoteAccessVpnDaoImpl.java @@ -85,4 +85,11 @@ public class RemoteAccessVpnDaoImpl extends GenericDaoBase listByVpcId(Long vpcId) { + SearchCriteria sc = AllFieldsSearch.create(); + sc.setParameters("vpcId", vpcId); + return listBy(sc); + } } diff --git a/engine/schema/src/main/java/com/cloud/network/dao/Site2SiteVpnGatewayDao.java b/engine/schema/src/main/java/com/cloud/network/dao/Site2SiteVpnGatewayDao.java index d3fef252f50..3475003c269 100644 --- a/engine/schema/src/main/java/com/cloud/network/dao/Site2SiteVpnGatewayDao.java +++ b/engine/schema/src/main/java/com/cloud/network/dao/Site2SiteVpnGatewayDao.java @@ -20,4 +20,6 @@ import com.cloud.utils.db.GenericDao; public interface Site2SiteVpnGatewayDao extends GenericDao { Site2SiteVpnGatewayVO findByVpcId(long vpcId); + + Site2SiteVpnGatewayVO findByPublicIpAddress(long ipAddressId); } diff --git a/engine/schema/src/main/java/com/cloud/network/dao/Site2SiteVpnGatewayDaoImpl.java b/engine/schema/src/main/java/com/cloud/network/dao/Site2SiteVpnGatewayDaoImpl.java index d1fde963217..0aeefe90c29 100644 --- a/engine/schema/src/main/java/com/cloud/network/dao/Site2SiteVpnGatewayDaoImpl.java +++ b/engine/schema/src/main/java/com/cloud/network/dao/Site2SiteVpnGatewayDaoImpl.java @@ -35,6 +35,7 @@ public class Site2SiteVpnGatewayDaoImpl extends GenericDaoBase sc = AllFieldsSearch.create(); + sc.setParameters("ipAddressId", ipAddressId); + return findOneBy(sc); + } } diff --git a/engine/schema/src/main/java/com/cloud/network/element/NetrisProviderVO.java b/engine/schema/src/main/java/com/cloud/network/element/NetrisProviderVO.java new file mode 100644 index 00000000000..113678f7b01 --- /dev/null +++ b/engine/schema/src/main/java/com/cloud/network/element/NetrisProviderVO.java @@ -0,0 +1,265 @@ +// Licensed to the Apache Software Foundation (ASF) under one +// or more contributor license agreements. See the NOTICE file +// distributed with this work for additional information +// regarding copyright ownership. The ASF licenses this file +// to you under the Apache License, Version 2.0 (the +// "License"); you may not use this file except in compliance +// with the License. You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, +// software distributed under the License is distributed on an +// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +// KIND, either express or implied. See the License for the +// specific language governing permissions and limitations +// under the License. +package com.cloud.network.element; + +import com.cloud.network.netris.NetrisProvider; + +import javax.persistence.Column; +import javax.persistence.Entity; +import javax.persistence.GeneratedValue; +import javax.persistence.GenerationType; +import javax.persistence.Id; +import javax.persistence.Table; +import java.util.Date; +import java.util.UUID; + +@Entity +@Table(name = "netris_providers") +public class NetrisProviderVO implements NetrisProvider { + + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + @Column(name = "id") + long id; + + @Column(name = "uuid") + private String uuid; + + @Column(name = "name") + private String name; + + @Column(name = "zone_id") + private long zoneId; + + @Column(name = "host_id") + private long hostId; + + @Column(name = "url") + private String url; + + @Column(name = "username") + private String username; + + @Column(name = "password") + private String password; + + @Column(name = "site_name") + private String siteName; + + @Column(name = "tenant_name") + private String tenantName; + + @Column(name = "netris_tag") + private String netrisTag; + + @Column(name = "created") + private Date created; + + @Column(name = "removed") + private Date removed; + + public NetrisProviderVO() { + this.uuid = UUID.randomUUID().toString(); + } + + @Override + public long getId() { + return id; + } + + public void setId(long id) { + this.id = id; + } + + @Override + public String getUuid() { + return uuid; + } + + public void setUuid(String uuid) { + this.uuid = uuid; + } + + @Override + public String getName() { + return name; + } + + public void setName(String name) { + this.name = name; + } + + @Override + public long getZoneId() { + return zoneId; + } + + public void setZoneId(long zoneId) { + this.zoneId = zoneId; + } + + public long getHostId() { + return hostId; + } + + public void setHostId(long hostId) { + this.hostId = hostId; + } + + @Override + public String getUrl() { + return url; + } + + public void setUrl(String url) { + this.url = url; + } + + @Override + public String getUsername() { + return username; + } + + public void setUsername(String username) { + this.username = username; + } + + public String getPassword() { + return password; + } + + public void setPassword(String password) { + this.password = password; + } + + public String getSiteName() { + return siteName; + } + + public void setSiteName(String siteName) { + this.siteName = siteName; + } + + public String getTenantName() { + return tenantName; + } + + public void setTenantName(String tenantName) { + this.tenantName = tenantName; + } + + public String getNetrisTag() { + return netrisTag; + } + + public void setNetrisTag(String netrisTag) { + this.netrisTag = netrisTag; + } + + 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; + } + + public static final class Builder { + private long zoneId; + private long hostId; + private String name; + private String url; + private String username; + private String password; + private String siteName; + private String tenantName; + private String netrisTag; + + public Builder() { + // Default constructor + } + + public Builder setZoneId(long zoneId) { + this.zoneId = zoneId; + return this; + } + + public Builder setHostId(long hostId) { + this.hostId = hostId; + return this; + } + + public Builder setName(String name) { + this.name = name; + return this; + } + + public Builder setUrl(String url) { + this.url = url; + return this; + } + + public Builder setUsername(String username) { + this.username = username; + return this; + } + + public Builder setPassword(String password) { + this.password = password; + return this; + } + + public Builder setSiteName(String siteName) { + this.siteName = siteName; + return this; + } + + public Builder setTenantName(String tenantName) { + this.tenantName = tenantName; + return this; + } + + public Builder setNetrisTag(String netrisTag) { + this.netrisTag = netrisTag; + return this; + } + + public NetrisProviderVO build() { + NetrisProviderVO provider = new NetrisProviderVO(); + provider.setZoneId(this.zoneId); + provider.setHostId(this.hostId); + provider.setUuid(UUID.randomUUID().toString()); + provider.setName(this.name); + provider.setUrl(this.url); + provider.setUsername(this.username); + provider.setPassword(this.password); + provider.setSiteName(this.siteName); + provider.setTenantName(this.tenantName); + provider.setNetrisTag(this.netrisTag); + provider.setCreated(new Date()); + return provider; + } + } +} diff --git a/engine/schema/src/main/java/com/cloud/network/vpc/StaticRouteVO.java b/engine/schema/src/main/java/com/cloud/network/vpc/StaticRouteVO.java index 2246bd6eed2..632d96819cd 100644 --- a/engine/schema/src/main/java/com/cloud/network/vpc/StaticRouteVO.java +++ b/engine/schema/src/main/java/com/cloud/network/vpc/StaticRouteVO.java @@ -27,6 +27,7 @@ import javax.persistence.GeneratedValue; import javax.persistence.GenerationType; import javax.persistence.Id; import javax.persistence.Table; +import javax.persistence.Transient; import com.cloud.utils.db.GenericDao; @@ -42,7 +43,10 @@ public class StaticRouteVO implements StaticRoute { String uuid; @Column(name = "vpc_gateway_id", updatable = false) - long vpcGatewayId; + Long vpcGatewayId; + + @Column(name = "next_hop") + private String nextHop; @Column(name = "cidr") private String cidr; @@ -67,6 +71,9 @@ public class StaticRouteVO implements StaticRoute { uuid = UUID.randomUUID().toString(); } + @Transient + boolean forVpn = false; + /** * @param vpcGatewayId * @param cidr @@ -74,7 +81,7 @@ public class StaticRouteVO implements StaticRoute { * @param accountId TODO * @param domainId TODO */ - public StaticRouteVO(long vpcGatewayId, String cidr, Long vpcId, long accountId, long domainId) { + public StaticRouteVO(Long vpcGatewayId, String cidr, Long vpcId, long accountId, long domainId, String nextHop) { super(); this.vpcGatewayId = vpcGatewayId; this.cidr = cidr; @@ -82,14 +89,32 @@ public class StaticRouteVO implements StaticRoute { this.vpcId = vpcId; this.accountId = accountId; this.domainId = domainId; + this.nextHop = nextHop; uuid = UUID.randomUUID().toString(); } + public StaticRouteVO(String cidr, Long vpcId, long accountId, long domainId, String nextHop, State state, boolean forVpn) { + super(); + this.cidr = cidr; + this.state = state; + this.vpcId = vpcId; + this.accountId = accountId; + this.domainId = domainId; + this.nextHop = nextHop; + uuid = UUID.randomUUID().toString(); + this.forVpn = forVpn; + } + @Override - public long getVpcGatewayId() { + public Long getVpcGatewayId() { return vpcGatewayId; } + @Override + public String getNextHop() { + return nextHop; + } + @Override public String getCidr() { return cidr; @@ -145,4 +170,8 @@ public class StaticRouteVO implements StaticRoute { public String getName() { return null; } + + public boolean isForVpn() { + return forVpn; + } } diff --git a/engine/schema/src/main/java/com/cloud/network/vpc/VpcOfferingVO.java b/engine/schema/src/main/java/com/cloud/network/vpc/VpcOfferingVO.java index 274b9fedecc..9320a37bc96 100644 --- a/engine/schema/src/main/java/com/cloud/network/vpc/VpcOfferingVO.java +++ b/engine/schema/src/main/java/com/cloud/network/vpc/VpcOfferingVO.java @@ -60,9 +60,6 @@ public class VpcOfferingVO implements VpcOffering { @Column(name = "default") boolean isDefault = false; - @Column(name = "for_nsx") - boolean forNsx = false; - @Column(name = "network_mode") NetworkOffering.NetworkMode networkMode; @@ -159,14 +156,6 @@ public class VpcOfferingVO implements VpcOffering { return isDefault; } - public boolean isForNsx() { - return forNsx; - } - - public void setForNsx(boolean forNsx) { - this.forNsx = forNsx; - } - public NetworkOffering.NetworkMode getNetworkMode() { return networkMode; } diff --git a/engine/schema/src/main/java/com/cloud/network/vpc/dao/VpcOfferingServiceMapDao.java b/engine/schema/src/main/java/com/cloud/network/vpc/dao/VpcOfferingServiceMapDao.java index 06cfd25e670..020536e97ec 100644 --- a/engine/schema/src/main/java/com/cloud/network/vpc/dao/VpcOfferingServiceMapDao.java +++ b/engine/schema/src/main/java/com/cloud/network/vpc/dao/VpcOfferingServiceMapDao.java @@ -18,6 +18,7 @@ package com.cloud.network.vpc.dao; import java.util.List; +import com.cloud.network.Network; import com.cloud.network.Network.Service; import com.cloud.network.vpc.VpcOfferingServiceMapVO; import com.cloud.utils.db.GenericDao; @@ -37,4 +38,8 @@ public interface VpcOfferingServiceMapDao extends GenericDao listProvidersForServiceForVpcOffering(long vpcOfferingId, Service service); + } diff --git a/engine/schema/src/main/java/com/cloud/network/vpc/dao/VpcOfferingServiceMapDaoImpl.java b/engine/schema/src/main/java/com/cloud/network/vpc/dao/VpcOfferingServiceMapDaoImpl.java index c7400f6edfd..dcb1becf9e8 100644 --- a/engine/schema/src/main/java/com/cloud/network/vpc/dao/VpcOfferingServiceMapDaoImpl.java +++ b/engine/schema/src/main/java/com/cloud/network/vpc/dao/VpcOfferingServiceMapDaoImpl.java @@ -19,6 +19,7 @@ package com.cloud.network.vpc.dao; import java.util.List; +import com.cloud.network.Network; import org.springframework.stereotype.Component; import com.cloud.network.Network.Service; @@ -110,4 +111,22 @@ public class VpcOfferingServiceMapDaoImpl extends GenericDaoBase sc = AllFieldsSearch.create(); + sc.setParameters("vpcOffId", vpcOfferingId); + sc.setParameters("provider", provider.getName()); + return findOneBy(sc) != null; + } + + @Override + public List listProvidersForServiceForVpcOffering(long vpcOfferingId, Service service) { + SearchCriteria sc = AllFieldsSearch.create(); + + sc.setParameters("vpcOffId", vpcOfferingId); + sc.setParameters("service", service.getName()); + + return customSearch(sc, null); + } } diff --git a/engine/schema/src/main/java/com/cloud/network/vpc/dao/VpcServiceMapDaoImpl.java b/engine/schema/src/main/java/com/cloud/network/vpc/dao/VpcServiceMapDaoImpl.java index 753c45fcc78..a5c4c83ff0f 100644 --- a/engine/schema/src/main/java/com/cloud/network/vpc/dao/VpcServiceMapDaoImpl.java +++ b/engine/schema/src/main/java/com/cloud/network/vpc/dao/VpcServiceMapDaoImpl.java @@ -68,8 +68,15 @@ public class VpcServiceMapDaoImpl extends GenericDaoBase @Override public boolean canProviderSupportServiceInVpc(long vpcId, Service service, Provider provider) { - // TODO Auto-generated method stub - return false; + SearchCriteria sc = AllFieldsSearch.create(); + sc.setParameters("vpcId", vpcId); + sc.setParameters("service", service.getName()); + sc.setParameters("provider", provider.getName()); + if (findOneBy(sc) != null) { + return true; + } else { + return false; + } } @Override diff --git a/engine/schema/src/main/java/com/cloud/offerings/NetworkOfferingVO.java b/engine/schema/src/main/java/com/cloud/offerings/NetworkOfferingVO.java index 5cad366945f..904c8e646eb 100644 --- a/engine/schema/src/main/java/com/cloud/offerings/NetworkOfferingVO.java +++ b/engine/schema/src/main/java/com/cloud/offerings/NetworkOfferingVO.java @@ -134,12 +134,6 @@ public class NetworkOfferingVO implements NetworkOffering { @Column(name = "for_vpc") boolean forVpc; - @Column(name = "for_tungsten") - boolean forTungsten = false; - - @Column(name = "for_nsx") - boolean forNsx = false; - @Column(name = "network_mode") NetworkMode networkMode; @@ -200,24 +194,6 @@ public class NetworkOfferingVO implements NetworkOffering { this.forVpc = isForVpc; } - @Override - public boolean isForTungsten() { - return forTungsten; - } - - public void setForTungsten(boolean forTungsten) { - this.forTungsten = forTungsten; - } - - @Override - public boolean isForNsx() { - return forNsx; - } - - public void setForNsx(boolean forNsx) { - this.forNsx = forNsx; - } - @Override public NetworkMode getNetworkMode() { return networkMode; diff --git a/engine/schema/src/main/java/com/cloud/service/ServiceOfferingVO.java b/engine/schema/src/main/java/com/cloud/service/ServiceOfferingVO.java index 7f5c1a7afa1..cfe8049f5b2 100644 --- a/engine/schema/src/main/java/com/cloud/service/ServiceOfferingVO.java +++ b/engine/schema/src/main/java/com/cloud/service/ServiceOfferingVO.java @@ -124,6 +124,15 @@ public class ServiceOfferingVO implements ServiceOffering { @Column(name = "dynamic_scaling_enabled") private boolean dynamicScalingEnabled = true; + @Column(name = "vgpu_profile_id") + private Long vgpuProfileId; + + @Column(name = "gpu_count") + private Integer gpuCount; + + @Column(name = "gpu_display") + private Boolean gpuDisplay; + // This is a delayed load value. If the value is null, // then this field has not been loaded yet. // Call service offering dao to load it. @@ -198,6 +207,8 @@ public class ServiceOfferingVO implements ServiceOffering { systemUse = offering.isSystemUse(); dynamicScalingEnabled = offering.isDynamicScalingEnabled(); diskOfferingStrictness = offering.diskOfferingStrictness; + vgpuProfileId = offering.vgpuProfileId; + gpuCount = offering.gpuCount; } @Override @@ -445,4 +456,30 @@ public class ServiceOfferingVO implements ServiceOffering { public void setDiskOfferingStrictness(boolean diskOfferingStrictness) { this.diskOfferingStrictness = diskOfferingStrictness; } + + @Override + public Long getVgpuProfileId() { + return vgpuProfileId; + } + + public void setVgpuProfileId(Long vgpuProfileId) { + this.vgpuProfileId = vgpuProfileId; + } + + @Override + public Integer getGpuCount() { + return gpuCount; + } + + public void setGpuCount(Integer gpuCount) { + this.gpuCount = gpuCount; + } + + public Boolean getGpuDisplay() { + return gpuDisplay; + } + + public void setGpuDisplay(Boolean gpuDisplay) { + this.gpuDisplay = gpuDisplay; + } } diff --git a/engine/schema/src/main/java/com/cloud/service/dao/ServiceOfferingDao.java b/engine/schema/src/main/java/com/cloud/service/dao/ServiceOfferingDao.java index ceb5b0a4fc1..d3bab4fcbe2 100644 --- a/engine/schema/src/main/java/com/cloud/service/dao/ServiceOfferingDao.java +++ b/engine/schema/src/main/java/com/cloud/service/dao/ServiceOfferingDao.java @@ -22,6 +22,7 @@ import java.util.Map; import com.cloud.service.ServiceOfferingVO; import com.cloud.storage.Storage.ProvisioningType; import com.cloud.utils.db.GenericDao; +import com.cloud.utils.db.SearchBuilder; import com.cloud.vm.VirtualMachine; /* @@ -57,4 +58,6 @@ public interface ServiceOfferingDao extends GenericDao ServiceOfferingVO findServiceOfferingByComputeOnlyDiskOffering(long diskOfferingId, boolean includingRemoved); List listIdsByHostTag(String tag); + + void addCheckForGpuEnabled(SearchBuilder serviceOfferingSearch, Boolean gpuEnabled); } diff --git a/engine/schema/src/main/java/com/cloud/service/dao/ServiceOfferingDaoImpl.java b/engine/schema/src/main/java/com/cloud/service/dao/ServiceOfferingDaoImpl.java index 803522fa6aa..f360770ad68 100644 --- a/engine/schema/src/main/java/com/cloud/service/dao/ServiceOfferingDaoImpl.java +++ b/engine/schema/src/main/java/com/cloud/service/dao/ServiceOfferingDaoImpl.java @@ -39,7 +39,7 @@ import com.cloud.utils.db.SearchBuilder; import com.cloud.utils.db.SearchCriteria; import com.cloud.utils.exception.CloudRuntimeException; import com.cloud.vm.VirtualMachine; -import com.cloud.vm.dao.UserVmDetailsDao; +import com.cloud.vm.dao.VMInstanceDetailsDao; @Component @DB() @@ -48,7 +48,7 @@ public class ServiceOfferingDaoImpl extends GenericDaoBase dynamicOffering = userVmDetailsDao.listDetailsKeyPairs(vmId); + Map dynamicOffering = vmInstanceDetailsDao.listDetailsKeyPairs(vmId); return getComputeOffering(offering, dynamicOffering); } return offering; @@ -190,7 +190,7 @@ public class ServiceOfferingDaoImpl extends GenericDaoBase dynamicOffering = userVmDetailsDao.listDetailsKeyPairs(vmId); + Map dynamicOffering = vmInstanceDetailsDao.listDetailsKeyPairs(vmId); return getComputeOffering(offering, dynamicOffering); } return offering; @@ -312,4 +312,13 @@ public class ServiceOfferingDaoImpl extends GenericDaoBase serviceOfferingSearch, Boolean gpuEnabled) { + if (gpuEnabled) { + serviceOfferingSearch.and("gpuEnabled", serviceOfferingSearch.entity().getVgpuProfileId(), SearchCriteria.Op.NNULL); + } else { + serviceOfferingSearch.and("gpuDisabled", serviceOfferingSearch.entity().getVgpuProfileId(), SearchCriteria.Op.NULL); + } + } } diff --git a/engine/schema/src/main/java/com/cloud/storage/VMTemplateVO.java b/engine/schema/src/main/java/com/cloud/storage/VMTemplateVO.java index 93f6a464019..3486bac010e 100644 --- a/engine/schema/src/main/java/com/cloud/storage/VMTemplateVO.java +++ b/engine/schema/src/main/java/com/cloud/storage/VMTemplateVO.java @@ -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 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; + } } diff --git a/engine/schema/src/main/java/com/cloud/storage/dao/SnapshotDao.java b/engine/schema/src/main/java/com/cloud/storage/dao/SnapshotDao.java index 171634fb104..3cda7d42760 100755 --- a/engine/schema/src/main/java/com/cloud/storage/dao/SnapshotDao.java +++ b/engine/schema/src/main/java/com/cloud/storage/dao/SnapshotDao.java @@ -58,4 +58,6 @@ public interface SnapshotDao extends GenericDao, StateDao listByIds(Object... ids); List searchByVolumes(List volumeIds); + + List listByVolumeIdAndTypeNotInAndStateNotRemoved(long volumeId, Type... type); } diff --git a/engine/schema/src/main/java/com/cloud/storage/dao/SnapshotDaoImpl.java b/engine/schema/src/main/java/com/cloud/storage/dao/SnapshotDaoImpl.java index f5fc9c47d03..c479a386d79 100755 --- a/engine/schema/src/main/java/com/cloud/storage/dao/SnapshotDaoImpl.java +++ b/engine/schema/src/main/java/com/cloud/storage/dao/SnapshotDaoImpl.java @@ -19,6 +19,7 @@ package com.cloud.storage.dao; import java.sql.PreparedStatement; import java.sql.ResultSet; import java.util.ArrayList; +import java.util.Arrays; import java.util.List; import javax.annotation.PostConstruct; @@ -56,6 +57,10 @@ public class SnapshotDaoImpl extends GenericDaoBase implements private static final String GET_LAST_SNAPSHOT = "SELECT snapshots.id FROM snapshot_store_ref, snapshots where snapshots.id = snapshot_store_ref.snapshot_id AND snapshosts.volume_id = ? AND snapshot_store_ref.role = ? ORDER BY created DESC"; + private static final String VOLUME_ID = "volumeId"; + private static final String NOT_TYPE = "notType"; + private static final String STATUS = "status"; + private SearchBuilder snapshotIdsSearch; private SearchBuilder VolumeIdSearch; private SearchBuilder VolumeIdTypeSearch; @@ -66,6 +71,8 @@ public class SnapshotDaoImpl extends GenericDaoBase implements private SearchBuilder StatusSearch; private SearchBuilder notInStatusSearch; private GenericSearchBuilder CountSnapshotsByAccount; + + private SearchBuilder volumeIdAndTypeNotInSearch; @Inject ResourceTagDao _tagsDao; @Inject @@ -181,6 +188,12 @@ public class SnapshotDaoImpl extends GenericDaoBase implements InstanceIdSearch.join("instanceSnapshots", volumeSearch, volumeSearch.entity().getId(), InstanceIdSearch.entity().getVolumeId(), JoinType.INNER); InstanceIdSearch.done(); + + volumeIdAndTypeNotInSearch = createSearchBuilder(); + volumeIdAndTypeNotInSearch.and(VOLUME_ID, volumeIdAndTypeNotInSearch.entity().getVolumeId(), SearchCriteria.Op.EQ); + volumeIdAndTypeNotInSearch.and(STATUS, volumeIdAndTypeNotInSearch.entity().getState(), SearchCriteria.Op.NEQ); + volumeIdAndTypeNotInSearch.and(NOT_TYPE, volumeIdAndTypeNotInSearch.entity().getTypeDescription(), SearchCriteria.Op.NOTIN); + volumeIdAndTypeNotInSearch.done(); } @Override @@ -299,4 +312,14 @@ public class SnapshotDaoImpl extends GenericDaoBase implements sc.setParameters("volumeIds", volumeIds.toArray()); return search(sc, null); } + + @Override + public List listByVolumeIdAndTypeNotInAndStateNotRemoved(long volumeId, Type... types) { + SearchCriteria sc = volumeIdAndTypeNotInSearch.create(); + sc.setParameters(VOLUME_ID, volumeId); + sc.setParameters(NOT_TYPE, Arrays.stream(types).map(Type::toString).toArray()); + sc.setParameters(STATUS, State.Destroyed); + + return listBy(sc); + } } diff --git a/engine/schema/src/main/java/com/cloud/storage/dao/VMTemplateDao.java b/engine/schema/src/main/java/com/cloud/storage/dao/VMTemplateDao.java index 2835cf3cb3c..d70eeb87653 100644 --- a/engine/schema/src/main/java/com/cloud/storage/dao/VMTemplateDao.java +++ b/engine/schema/src/main/java/com/cloud/storage/dao/VMTemplateDao.java @@ -101,4 +101,6 @@ public interface VMTemplateDao extends GenericDao, StateDao< List listByIds(List ids); List listIdsByTemplateTag(String tag); + + List listIdsByExtensionId(long extensionId); } diff --git a/engine/schema/src/main/java/com/cloud/storage/dao/VMTemplateDaoImpl.java b/engine/schema/src/main/java/com/cloud/storage/dao/VMTemplateDaoImpl.java index 138677927e6..267cef2169a 100644 --- a/engine/schema/src/main/java/com/cloud/storage/dao/VMTemplateDaoImpl.java +++ b/engine/schema/src/main/java/com/cloud/storage/dao/VMTemplateDaoImpl.java @@ -837,6 +837,17 @@ public class VMTemplateDaoImpl extends GenericDaoBase implem return customSearchIncludingRemoved(sc, null); } + @Override + public List listIdsByExtensionId(long extensionId) { + GenericSearchBuilder sb = createSearchBuilder(Long.class); + sb.selectFields(sb.entity().getId()); + sb.and("extensionId", sb.entity().getExtensionId(), SearchCriteria.Op.EQ); + sb.done(); + SearchCriteria sc = sb.create(); + sc.setParameters("extensionId", extensionId); + return customSearch(sc, null); + } + @Override public boolean updateState( com.cloud.template.VirtualMachineTemplate.State currentState, diff --git a/engine/schema/src/main/java/com/cloud/upgrade/dao/Upgrade42000to42010.java b/engine/schema/src/main/java/com/cloud/upgrade/dao/Upgrade42000to42010.java index d442dc89904..0c0a9f070ba 100644 --- a/engine/schema/src/main/java/com/cloud/upgrade/dao/Upgrade42000to42010.java +++ b/engine/schema/src/main/java/com/cloud/upgrade/dao/Upgrade42000to42010.java @@ -100,8 +100,6 @@ public class Upgrade42000to42010 extends DbUpgradeAbstractImpl implements DbUpgr DbUpgradeUtils.addIndexIfNeeded(conn, "network_offering_details", "name"); - DbUpgradeUtils.addIndexIfNeeded(conn, "network_offering_details", "resource_id", "resource_type"); - DbUpgradeUtils.addIndexIfNeeded(conn, "service_offering", "cpu"); DbUpgradeUtils.addIndexIfNeeded(conn, "service_offering", "speed"); DbUpgradeUtils.addIndexIfNeeded(conn, "service_offering", "ram_size"); diff --git a/engine/schema/src/main/java/com/cloud/usage/dao/UsageJobDao.java b/engine/schema/src/main/java/com/cloud/usage/dao/UsageJobDao.java index f22a906054d..d4038d4ceeb 100644 --- a/engine/schema/src/main/java/com/cloud/usage/dao/UsageJobDao.java +++ b/engine/schema/src/main/java/com/cloud/usage/dao/UsageJobDao.java @@ -37,4 +37,6 @@ public interface UsageJobDao extends GenericDao { UsageJobVO isOwner(String hostname, int pid); void updateJobSuccess(Long jobId, long startMillis, long endMillis, long execTime, boolean success); + + void removeLastOpenJobsOwned(String hostname, int pid); } diff --git a/engine/schema/src/main/java/com/cloud/usage/dao/UsageJobDaoImpl.java b/engine/schema/src/main/java/com/cloud/usage/dao/UsageJobDaoImpl.java index 6d460aadd09..44a7d1a8b72 100644 --- a/engine/schema/src/main/java/com/cloud/usage/dao/UsageJobDaoImpl.java +++ b/engine/schema/src/main/java/com/cloud/usage/dao/UsageJobDaoImpl.java @@ -22,6 +22,7 @@ import java.util.Date; import java.util.List; +import org.apache.commons.collections.CollectionUtils; import org.springframework.stereotype.Component; import com.cloud.usage.UsageJobVO; @@ -114,7 +115,7 @@ public class UsageJobDaoImpl extends GenericDaoBase implements public UsageJobVO isOwner(String hostname, int pid) { TransactionLegacy txn = TransactionLegacy.open(TransactionLegacy.USAGE_DB); try { - if ((hostname == null) || (pid <= 0)) { + if (hostname == null || pid <= 0) { return null; } @@ -174,7 +175,7 @@ public class UsageJobDaoImpl extends GenericDaoBase implements SearchCriteria sc = createSearchCriteria(); sc.addAnd("endMillis", SearchCriteria.Op.EQ, Long.valueOf(0)); sc.addAnd("jobType", SearchCriteria.Op.EQ, Integer.valueOf(UsageJobVO.JOB_TYPE_SINGLE)); - sc.addAnd("scheduled", SearchCriteria.Op.EQ, Integer.valueOf(0)); + sc.addAnd("scheduled", SearchCriteria.Op.EQ, Integer.valueOf(UsageJobVO.JOB_NOT_SCHEDULED)); List jobs = search(sc, filter); if ((jobs == null) || jobs.isEmpty()) { @@ -194,4 +195,36 @@ public class UsageJobDaoImpl extends GenericDaoBase implements } return jobs.get(0).getHeartbeat(); } + + private List getLastOpenJobsOwned(String hostname, int pid) { + SearchCriteria sc = createSearchCriteria(); + sc.addAnd("endMillis", SearchCriteria.Op.EQ, Long.valueOf(0)); + sc.addAnd("host", SearchCriteria.Op.EQ, hostname); + if (pid > 0) { + sc.addAnd("pid", SearchCriteria.Op.EQ, Integer.valueOf(pid)); + } + return listBy(sc); + } + + @Override + public void removeLastOpenJobsOwned(String hostname, int pid) { + if (hostname == null) { + return; + } + + TransactionLegacy txn = TransactionLegacy.open(TransactionLegacy.USAGE_DB); + try { + List jobs = getLastOpenJobsOwned(hostname, pid); + if (CollectionUtils.isNotEmpty(jobs)) { + logger.info("Found {} opens job, to remove", jobs.size()); + for (UsageJobVO job : jobs) { + logger.debug("Removing job - id: {}, pid: {}, job type: {}, scheduled: {}, heartbeat: {}", + job.getId(), job.getPid(), job.getJobType(), job.getScheduled(), job.getHeartbeat()); + remove(job.getId()); + } + } + } finally { + txn.close(); + } + } } diff --git a/engine/schema/src/main/java/com/cloud/vm/UserVmDetailVO.java b/engine/schema/src/main/java/com/cloud/vm/VMInstanceDetailVO.java similarity index 90% rename from engine/schema/src/main/java/com/cloud/vm/UserVmDetailVO.java rename to engine/schema/src/main/java/com/cloud/vm/VMInstanceDetailVO.java index 81bb6dd9d4f..7879aa24556 100755 --- a/engine/schema/src/main/java/com/cloud/vm/UserVmDetailVO.java +++ b/engine/schema/src/main/java/com/cloud/vm/VMInstanceDetailVO.java @@ -26,8 +26,8 @@ import javax.persistence.Table; import org.apache.cloudstack.api.ResourceDetail; @Entity -@Table(name = "user_vm_details") -public class UserVmDetailVO implements ResourceDetail { +@Table(name = "vm_instance_details") +public class VMInstanceDetailVO implements ResourceDetail { @Id @GeneratedValue(strategy = GenerationType.IDENTITY) @Column(name = "id") @@ -45,10 +45,10 @@ public class UserVmDetailVO implements ResourceDetail { @Column(name = "display") private boolean display = true; - public UserVmDetailVO() { + public VMInstanceDetailVO() { } - public UserVmDetailVO(long vmId, String name, String value, boolean display) { + public VMInstanceDetailVO(long vmId, String name, String value, boolean display) { this.resourceId = vmId; this.name = name; this.value = value; diff --git a/engine/schema/src/main/java/com/cloud/vm/dao/NicDao.java b/engine/schema/src/main/java/com/cloud/vm/dao/NicDao.java index d34b03c4cb0..70a2558e2d4 100644 --- a/engine/schema/src/main/java/com/cloud/vm/dao/NicDao.java +++ b/engine/schema/src/main/java/com/cloud/vm/dao/NicDao.java @@ -46,8 +46,12 @@ public interface NicDao extends GenericDao { NicVO findByNetworkIdAndTypeIncludingRemoved(long networkId, VirtualMachine.Type vmType); + NicVO findNonPlaceHolderByNetworkIdAndType(long networkId, VirtualMachine.Type vmType); + NicVO findByIp4AddressAndNetworkId(String ip4Address, long networkId); + NicVO findNonPlaceHolderByIp4AddressAndNetworkId(String ip4Address, long networkId); + NicVO findByNetworkIdAndMacAddress(long networkId, String mac); NicVO findDefaultNicForVM(long instanceId); diff --git a/engine/schema/src/main/java/com/cloud/vm/dao/NicDaoImpl.java b/engine/schema/src/main/java/com/cloud/vm/dao/NicDaoImpl.java index 7d1af1982ae..3618785c1b8 100644 --- a/engine/schema/src/main/java/com/cloud/vm/dao/NicDaoImpl.java +++ b/engine/schema/src/main/java/com/cloud/vm/dao/NicDaoImpl.java @@ -69,6 +69,7 @@ public class NicDaoImpl extends GenericDaoBase implements NicDao { AllFieldsSearch.and("secondaryip", AllFieldsSearch.entity().getSecondaryIp(), Op.EQ); AllFieldsSearch.and("nicid", AllFieldsSearch.entity().getId(), Op.EQ); AllFieldsSearch.and("strategy", AllFieldsSearch.entity().getReservationStrategy(), Op.EQ); + AllFieldsSearch.and("strategyNEQ", AllFieldsSearch.entity().getReservationStrategy(), Op.NEQ); AllFieldsSearch.and("reserverName",AllFieldsSearch.entity().getReserver(),Op.EQ); AllFieldsSearch.and("macAddress", AllFieldsSearch.entity().getMacAddress(), Op.EQ); AllFieldsSearch.and("deviceid", AllFieldsSearch.entity().getDeviceId(), Op.EQ); @@ -195,6 +196,15 @@ public class NicDaoImpl extends GenericDaoBase implements NicDao { return findByNetworkIdAndTypeInternal(networkId, vmType, true); } + @Override + public NicVO findNonPlaceHolderByNetworkIdAndType(long networkId, VirtualMachine.Type vmType) { + SearchCriteria sc = AllFieldsSearch.create(); + sc.setParameters("network", networkId); + sc.setParameters("vmType", vmType); + sc.setParameters("strategyNEQ", Nic.ReservationStrategy.PlaceHolder.toString()); + return findOneBy(sc); + } + @Override public NicVO findByNetworkIdTypeAndGateway(long networkId, VirtualMachine.Type vmType, String gateway) { SearchCriteria sc = AllFieldsSearch.create(); @@ -222,6 +232,16 @@ public class NicDaoImpl extends GenericDaoBase implements NicDao { return findOneBy(sc); } + @Override + public NicVO findNonPlaceHolderByIp4AddressAndNetworkId(String ip4Address, long networkId) { + SearchCriteria sc = AllFieldsSearch.create(); + sc.setParameters("address", ip4Address); + sc.setParameters("network", networkId); + sc.setParameters("strategyNEQ", Nic.ReservationStrategy.PlaceHolder.toString()); + return findOneBy(sc); + } + + @Override public NicVO findByNetworkIdAndMacAddress(long networkId, String mac) { SearchCriteria sc = AllFieldsSearch.create(); diff --git a/engine/schema/src/main/java/com/cloud/vm/dao/UserVmDaoImpl.java b/engine/schema/src/main/java/com/cloud/vm/dao/UserVmDaoImpl.java index cc8b9fc59a8..41bcb3155e5 100644 --- a/engine/schema/src/main/java/com/cloud/vm/dao/UserVmDaoImpl.java +++ b/engine/schema/src/main/java/com/cloud/vm/dao/UserVmDaoImpl.java @@ -57,7 +57,7 @@ import com.cloud.utils.db.SearchCriteria.Func; import com.cloud.utils.db.TransactionLegacy; import com.cloud.utils.exception.CloudRuntimeException; import com.cloud.vm.NicVO; -import com.cloud.vm.UserVmDetailVO; +import com.cloud.vm.VMInstanceDetailVO; import com.cloud.vm.UserVmVO; import com.cloud.vm.VirtualMachine; import com.cloud.vm.VirtualMachine.State; @@ -124,13 +124,13 @@ public class UserVmDaoImpl extends GenericDaoBase implements Use + "left join security_group on security_group_vm_map.security_group_id=security_group.id " + "left join nics on vm_instance.id=nics.instance_id " + "left join networks on nics.network_id=networks.id " + "left join user_ip_address on user_ip_address.vm_id=vm_instance.id " + "where vm_instance.id in ("; - private static final String VMS_DETAIL_BY_NAME = "select vm_instance.instance_name, vm_instance.vm_type, vm_instance.id , user_vm_details.value, user_vm_details.name from vm_instance " - + "left join user_vm_details on vm_instance.id = user_vm_details.vm_id where (user_vm_details.name is null or user_vm_details.name = ? ) and vm_instance.instance_name in ("; + private static final String VMS_DETAIL_BY_NAME = "select vm_instance.instance_name, vm_instance.vm_type, vm_instance.id , vm_instance_details.value, vm_instance_details.name from vm_instance " + + "left join vm_instance_details on vm_instance.id = vm_instance_details.vm_id where (vm_instance_details.name is null or vm_instance_details.name = ? ) and vm_instance.instance_name in ("; private static final int VM_DETAILS_BATCH_SIZE = 100; @Inject - protected UserVmDetailsDao _detailsDao; + protected VMInstanceDetailsDao _detailsDao; @Inject protected NicDao _nicDao; @@ -445,10 +445,10 @@ public class UserVmDaoImpl extends GenericDaoBase implements Use final Map visibilityMap = _detailsDao.listDetailsVisibility(vm.getId()); - List details = new ArrayList(); + List details = new ArrayList(); for (Map.Entry entry : detailsStr.entrySet()) { boolean display = !hiddenDetails.contains(entry.getKey()) && visibilityMap.getOrDefault(entry.getKey(), true); - details.add(new UserVmDetailVO(vm.getId(), entry.getKey(), entry.getValue(), display)); + details.add(new VMInstanceDetailVO(vm.getId(), entry.getKey(), entry.getValue(), display)); } _detailsDao.saveDetails(details); @@ -755,7 +755,7 @@ public class UserVmDaoImpl extends GenericDaoBase implements Use while (rs.next()) { vmsDetailByNames.add(new Pair, Pair>(new Pair( rs.getString("vm_instance.instance_name"), VirtualMachine.Type.valueOf(rs.getString("vm_type"))), - new Pair(rs.getLong("vm_instance.id"), rs.getString("user_vm_details.value")))); + new Pair(rs.getLong("vm_instance.id"), rs.getString("vm_instance_details.value")))); } } } catch (SQLException e) { @@ -821,6 +821,7 @@ public class UserVmDaoImpl extends GenericDaoBase implements Use reservationDao.setResourceId(Resource.ResourceType.user_vm, userVM.getId()); reservationDao.setResourceId(Resource.ResourceType.cpu, userVM.getId()); reservationDao.setResourceId(Resource.ResourceType.memory, userVM.getId()); + reservationDao.setResourceId(Resource.ResourceType.gpu, userVM.getId()); return userVM; }); } diff --git a/engine/schema/src/main/java/com/cloud/vm/dao/VMInstanceDaoImpl.java b/engine/schema/src/main/java/com/cloud/vm/dao/VMInstanceDaoImpl.java index ef10af63bae..dc0391f71fd 100755 --- a/engine/schema/src/main/java/com/cloud/vm/dao/VMInstanceDaoImpl.java +++ b/engine/schema/src/main/java/com/cloud/vm/dao/VMInstanceDaoImpl.java @@ -131,12 +131,22 @@ public class VMInstanceDaoImpl extends GenericDaoBase implem private static final String ORDER_HOSTS_NUMBER_OF_VMS_FOR_ACCOUNT_PART2 = " GROUP BY host.id ORDER BY 2 ASC "; - private static final String COUNT_VMS_BASED_ON_VGPU_TYPES1 = + private static final String COUNT_VMS_BASED_ON_VGPU_TYPES1_LEGACY = "SELECT pci, type, SUM(vmcount) FROM (SELECT MAX(IF(offering.name = 'pciDevice',value,'')) AS pci, MAX(IF(offering.name = 'vgpuType', value,'')) " + "AS type, COUNT(DISTINCT vm.id) AS vmcount FROM service_offering_details offering INNER JOIN vm_instance vm ON offering.service_offering_id = vm.service_offering_id " + "INNER JOIN `cloud`.`host` ON vm.host_id = host.id WHERE vm.state = 'Running' AND host.data_center_id = ? "; + private static final String COUNT_VMS_BASED_ON_VGPU_TYPES2_LEGACY = + "GROUP BY vm.service_offering_id) results GROUP BY pci, type"; + + private static final String COUNT_VMS_BASED_ON_VGPU_TYPES1 = + "SELECT CONCAT(gpu_card.vendor_name, ' ', gpu_card.device_name), vgpu_profile.name, COUNT(gpu_device.vm_id) " + + "FROM `cloud`.`gpu_device` " + + "INNER JOIN `cloud`.`host` ON gpu_device.host_id = host.id " + + "INNER JOIN `cloud`.`gpu_card` ON gpu_device.card_id = gpu_card.id " + + "INNER JOIN `cloud`.`vgpu_profile` ON vgpu_profile.id = gpu_device.vgpu_profile_id " + + "WHERE vm_id IS NOT NULL AND host.data_center_id = ? "; private static final String COUNT_VMS_BASED_ON_VGPU_TYPES2 = - "GROUP BY offering.service_offering_id) results GROUP BY pci, type"; + "GROUP BY gpu_card.name, vgpu_profile.name"; private static final String UPDATE_SYSTEM_VM_TEMPLATE_ID_FOR_HYPERVISOR = "UPDATE `cloud`.`vm_instance` SET vm_template_id = ? WHERE type <> 'User' AND hypervisor_type = ? AND removed is NULL"; @@ -794,40 +804,52 @@ public class VMInstanceDaoImpl extends GenericDaoBase implem @Override public HashMap countVgpuVMs(Long dcId, Long podId, Long clusterId) { + StringBuilder finalQueryLegacy = new StringBuilder(); StringBuilder finalQuery = new StringBuilder(); TransactionLegacy txn = TransactionLegacy.currentTxn(); + PreparedStatement pstmtLegacy = null; PreparedStatement pstmt = null; List resourceIdList = new ArrayList(); HashMap result = new HashMap(); resourceIdList.add(dcId); + finalQueryLegacy.append(COUNT_VMS_BASED_ON_VGPU_TYPES1_LEGACY); finalQuery.append(COUNT_VMS_BASED_ON_VGPU_TYPES1); if (podId != null) { + finalQueryLegacy.append("AND host.pod_id = ? "); finalQuery.append("AND host.pod_id = ? "); resourceIdList.add(podId); } if (clusterId != null) { + finalQueryLegacy.append("AND host.cluster_id = ? "); finalQuery.append("AND host.cluster_id = ? "); resourceIdList.add(clusterId); } + finalQueryLegacy.append(COUNT_VMS_BASED_ON_VGPU_TYPES2_LEGACY); finalQuery.append(COUNT_VMS_BASED_ON_VGPU_TYPES2); try { + pstmtLegacy = txn.prepareAutoCloseStatement(finalQueryLegacy.toString()); pstmt = txn.prepareAutoCloseStatement(finalQuery.toString()); for (int i = 0; i < resourceIdList.size(); i++) { + pstmtLegacy.setLong(1 + i, resourceIdList.get(i)); pstmt.setLong(1 + i, resourceIdList.get(i)); } - ResultSet rs = pstmt.executeQuery(); + ResultSet rs = pstmtLegacy.executeQuery(); + while (rs.next()) { + result.put(rs.getString(1).concat(rs.getString(2)), rs.getLong(3)); + } + rs = pstmt.executeQuery(); while (rs.next()) { result.put(rs.getString(1).concat(rs.getString(2)), rs.getLong(3)); } return result; } catch (SQLException e) { - throw new CloudRuntimeException("DB Exception on: " + finalQuery, e); + throw new CloudRuntimeException("DB Exception on: " + finalQueryLegacy, e); } catch (Throwable e) { - throw new CloudRuntimeException("Caught: " + finalQuery, e); + throw new CloudRuntimeException("Caught: " + finalQueryLegacy, e); } } diff --git a/engine/schema/src/main/java/com/cloud/vm/dao/UserVmDetailsDao.java b/engine/schema/src/main/java/com/cloud/vm/dao/VMInstanceDetailsDao.java similarity index 84% rename from engine/schema/src/main/java/com/cloud/vm/dao/UserVmDetailsDao.java rename to engine/schema/src/main/java/com/cloud/vm/dao/VMInstanceDetailsDao.java index c22da6b4ff5..ea9ac5afba6 100644 --- a/engine/schema/src/main/java/com/cloud/vm/dao/UserVmDetailsDao.java +++ b/engine/schema/src/main/java/com/cloud/vm/dao/VMInstanceDetailsDao.java @@ -19,7 +19,7 @@ package com.cloud.vm.dao; import org.apache.cloudstack.resourcedetail.ResourceDetailsDao; import com.cloud.utils.db.GenericDao; -import com.cloud.vm.UserVmDetailVO; +import com.cloud.vm.VMInstanceDetailVO; -public interface UserVmDetailsDao extends GenericDao, ResourceDetailsDao { +public interface VMInstanceDetailsDao extends GenericDao, ResourceDetailsDao { } diff --git a/engine/schema/src/main/java/com/cloud/vm/dao/UserVmDetailsDaoImpl.java b/engine/schema/src/main/java/com/cloud/vm/dao/VMInstanceDetailsDaoImpl.java similarity index 81% rename from engine/schema/src/main/java/com/cloud/vm/dao/UserVmDetailsDaoImpl.java rename to engine/schema/src/main/java/com/cloud/vm/dao/VMInstanceDetailsDaoImpl.java index d8f751842d5..ca11b005fb2 100644 --- a/engine/schema/src/main/java/com/cloud/vm/dao/UserVmDetailsDaoImpl.java +++ b/engine/schema/src/main/java/com/cloud/vm/dao/VMInstanceDetailsDaoImpl.java @@ -21,14 +21,14 @@ import org.springframework.stereotype.Component; import org.apache.cloudstack.resourcedetail.ResourceDetailsDaoBase; -import com.cloud.vm.UserVmDetailVO; +import com.cloud.vm.VMInstanceDetailVO; @Component -public class UserVmDetailsDaoImpl extends ResourceDetailsDaoBase implements UserVmDetailsDao { +public class VMInstanceDetailsDaoImpl extends ResourceDetailsDaoBase implements VMInstanceDetailsDao { @Override public void addDetail(long resourceId, String key, String value, boolean display) { - super.addDetail(new UserVmDetailVO(resourceId, key, value, display)); + super.addDetail(new VMInstanceDetailVO(resourceId, key, value, display)); } } diff --git a/engine/schema/src/main/java/com/cloud/vm/snapshot/dao/VMSnapshotDao.java b/engine/schema/src/main/java/com/cloud/vm/snapshot/dao/VMSnapshotDao.java index 0143aaa1e73..4045af58d4b 100644 --- a/engine/schema/src/main/java/com/cloud/vm/snapshot/dao/VMSnapshotDao.java +++ b/engine/schema/src/main/java/com/cloud/vm/snapshot/dao/VMSnapshotDao.java @@ -27,6 +27,8 @@ public interface VMSnapshotDao extends GenericDao, StateDao< List findByVm(Long vmId); + List findByVmAndByType(Long vmId, VMSnapshot.Type type); + List listExpungingSnapshot(); List listByInstanceId(Long vmId, VMSnapshot.State... status); @@ -35,6 +37,8 @@ public interface VMSnapshotDao extends GenericDao, StateDao< List listByParent(Long vmSnapshotId); + List listByParentAndStateIn(Long vmSnapshotId, VMSnapshot.State... states); + VMSnapshotVO findByName(Long vmId, String name); List listByAccountId(Long accountId); diff --git a/engine/schema/src/main/java/com/cloud/vm/snapshot/dao/VMSnapshotDaoImpl.java b/engine/schema/src/main/java/com/cloud/vm/snapshot/dao/VMSnapshotDaoImpl.java index 03a978f8546..83411b3cf8f 100644 --- a/engine/schema/src/main/java/com/cloud/vm/snapshot/dao/VMSnapshotDaoImpl.java +++ b/engine/schema/src/main/java/com/cloud/vm/snapshot/dao/VMSnapshotDaoImpl.java @@ -42,6 +42,12 @@ public class VMSnapshotDaoImpl extends GenericDaoBase implem private final SearchBuilder SnapshotStatusSearch; private final SearchBuilder AllFieldsSearch; + private SearchBuilder parentIdEqAndStateIn; + + private static final String PARENT = "parent"; + + private static final String STATE = "state"; + protected VMSnapshotDaoImpl() { AllFieldsSearch = createSearchBuilder(); AllFieldsSearch.and("state", AllFieldsSearch.entity().getState(), Op.EQ); @@ -71,6 +77,11 @@ public class VMSnapshotDaoImpl extends GenericDaoBase implem SnapshotStatusSearch.and("vm_id", SnapshotStatusSearch.entity().getVmId(), SearchCriteria.Op.EQ); SnapshotStatusSearch.and("state", SnapshotStatusSearch.entity().getState(), SearchCriteria.Op.IN); SnapshotStatusSearch.done(); + + parentIdEqAndStateIn = createSearchBuilder(); + parentIdEqAndStateIn.and(PARENT, parentIdEqAndStateIn.entity().getParent(), Op.EQ); + parentIdEqAndStateIn.and(STATE, parentIdEqAndStateIn.entity().getState(), Op.IN); + parentIdEqAndStateIn.done(); } @Override @@ -80,6 +91,14 @@ public class VMSnapshotDaoImpl extends GenericDaoBase implem return listBy(sc, null); } + @Override + public List findByVmAndByType(Long vmId, VMSnapshot.Type type) { + SearchCriteria sc = AllFieldsSearch.create(); + sc.setParameters("vm_id", vmId); + sc.setParameters("vm_snapshot_type", type); + return listBy(sc, null); + } + @Override public List listExpungingSnapshot() { SearchCriteria sc = ExpungingSnapshotSearch.create(); @@ -111,6 +130,14 @@ public class VMSnapshotDaoImpl extends GenericDaoBase implem return listBy(sc, null); } + @Override + public List listByParentAndStateIn(Long vmSnapshotId, State... states) { + SearchCriteria sc = parentIdEqAndStateIn.create(); + sc.setParameters(PARENT, vmSnapshotId); + sc.setParameters(STATE, (Object[])states); + return listBy(sc, null); + } + @Override public VMSnapshotVO findByName(Long vmId, String name) { SearchCriteria sc = AllFieldsSearch.create(); diff --git a/engine/schema/src/main/java/org/apache/cloudstack/backup/BackupScheduleVO.java b/engine/schema/src/main/java/org/apache/cloudstack/backup/BackupScheduleVO.java index 0258c42c52b..75c7a8be55c 100644 --- a/engine/schema/src/main/java/org/apache/cloudstack/backup/BackupScheduleVO.java +++ b/engine/schema/src/main/java/org/apache/cloudstack/backup/BackupScheduleVO.java @@ -18,6 +18,7 @@ package org.apache.cloudstack.backup; import java.util.Date; +import java.util.UUID; import javax.persistence.Column; import javax.persistence.Entity; @@ -39,6 +40,9 @@ public class BackupScheduleVO implements BackupSchedule { @Column(name = "id") private long id; + @Column(name = "uuid", nullable = false) + private String uuid = UUID.randomUUID().toString(); + @Column(name = "vm_id") private Long vmId; @@ -84,6 +88,11 @@ public class BackupScheduleVO implements BackupSchedule { return id; } + @Override + public String getUuid() { + return uuid; + } + public Long getVmId() { return vmId; } diff --git a/engine/schema/src/main/java/org/apache/cloudstack/backup/dao/BackupScheduleDaoImpl.java b/engine/schema/src/main/java/org/apache/cloudstack/backup/dao/BackupScheduleDaoImpl.java index aac2e3bf232..9374798dde3 100644 --- a/engine/schema/src/main/java/org/apache/cloudstack/backup/dao/BackupScheduleDaoImpl.java +++ b/engine/schema/src/main/java/org/apache/cloudstack/backup/dao/BackupScheduleDaoImpl.java @@ -92,12 +92,13 @@ public class BackupScheduleDaoImpl extends GenericDaoBase { + +} diff --git a/engine/schema/src/main/java/org/apache/cloudstack/gui/theme/dao/GuiThemeDaoImpl.java b/engine/schema/src/main/java/org/apache/cloudstack/gui/theme/dao/GuiThemeDaoImpl.java new file mode 100644 index 00000000000..bc58c5f80f3 --- /dev/null +++ b/engine/schema/src/main/java/org/apache/cloudstack/gui/theme/dao/GuiThemeDaoImpl.java @@ -0,0 +1,25 @@ +// 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.gui.theme.dao; + +import com.cloud.utils.db.GenericDaoBase; +import org.apache.cloudstack.gui.theme.GuiThemeVO; +import org.springframework.stereotype.Component; + +@Component +public class GuiThemeDaoImpl extends GenericDaoBase implements GuiThemeDao { +} diff --git a/engine/schema/src/main/java/org/apache/cloudstack/gui/theme/dao/GuiThemeDetailsDao.java b/engine/schema/src/main/java/org/apache/cloudstack/gui/theme/dao/GuiThemeDetailsDao.java new file mode 100644 index 00000000000..af243b1ffa4 --- /dev/null +++ b/engine/schema/src/main/java/org/apache/cloudstack/gui/theme/dao/GuiThemeDetailsDao.java @@ -0,0 +1,30 @@ +// 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.gui.theme.dao; + +import com.cloud.utils.db.GenericDao; +import org.apache.cloudstack.gui.theme.GuiThemeDetailsVO; + +import java.util.List; + +public interface GuiThemeDetailsDao extends GenericDao { + List listGuiThemeIdsByCommonName(String commonName); + + List listGuiThemeIdsByDomainUuids(String domainUuid); + + void expungeByGuiThemeId(long guiThemeId); +} diff --git a/engine/schema/src/main/java/org/apache/cloudstack/gui/theme/dao/GuiThemeDetailsDaoImpl.java b/engine/schema/src/main/java/org/apache/cloudstack/gui/theme/dao/GuiThemeDetailsDaoImpl.java new file mode 100644 index 00000000000..b0969833eb0 --- /dev/null +++ b/engine/schema/src/main/java/org/apache/cloudstack/gui/theme/dao/GuiThemeDetailsDaoImpl.java @@ -0,0 +1,126 @@ +// 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.gui.theme.dao; + +import com.cloud.domain.DomainVO; +import com.cloud.domain.dao.DomainDao; +import com.cloud.utils.db.GenericDaoBase; +import com.cloud.utils.db.GenericSearchBuilder; +import com.cloud.utils.db.JoinBuilder; +import com.cloud.utils.db.SearchBuilder; +import com.cloud.utils.db.SearchCriteria; +import org.apache.cloudstack.gui.theme.GuiThemeDetailsVO; +import org.apache.cloudstack.gui.theme.GuiThemeVO; +import org.springframework.stereotype.Component; + +import javax.inject.Inject; +import java.util.ArrayList; +import java.util.List; + +@Component +public class GuiThemeDetailsDaoImpl extends GenericDaoBase implements GuiThemeDetailsDao { + + @Inject + DomainDao domainDao; + + @Inject + GuiThemeDao guiThemeDao; + + public List listGuiThemeIdsByCommonName(String commonName) { + GenericSearchBuilder detailsDaoSearchBuilder = createSearchBuilder(Long.class); + detailsDaoSearchBuilder.selectFields(detailsDaoSearchBuilder.entity().getGuiThemeId()); + detailsDaoSearchBuilder.and("commonNameType", detailsDaoSearchBuilder.entity().getType(), SearchCriteria.Op.EQ); + detailsDaoSearchBuilder.and().op("firstReplace", detailsDaoSearchBuilder.entity().getValue(), SearchCriteria.Op.LIKE_REPLACE); + detailsDaoSearchBuilder.or("secondReplace", detailsDaoSearchBuilder.entity().getValue(), SearchCriteria.Op.LIKE_REPLACE).cp(); + detailsDaoSearchBuilder.done(); + + SearchCriteria searchCriteria = detailsDaoSearchBuilder.create(); + searchCriteria.setParameters("commonNameType", "commonName"); + searchCriteria.setParameters("firstReplace", commonName, "*", "%"); + searchCriteria.setParameters("secondReplace", commonName, "*.", "%"); + + return customSearch(searchCriteria, null); + } + + public List listGuiThemeIdsByDomainUuids(String domainUuid) { + List guiThemeIds = new ArrayList<>(); + String requestedDomainPath = domainDao.findByUuid(domainUuid).getPath(); + + SearchBuilder domainSearchBuilderPathLike = domainDao.createSearchBuilder(); + domainSearchBuilderPathLike.and("pathLike", domainSearchBuilderPathLike.entity().getPath(), SearchCriteria.Op.LIKE_CONCAT); + + SearchBuilder domainSearchBuilderPathEq = domainDao.createSearchBuilder(); + domainSearchBuilderPathEq.and("pathEq", domainSearchBuilderPathEq.entity().getPath(), SearchCriteria.Op.EQ); + + GenericSearchBuilder detailsSearchBuilderPathLike = createDetailsSearchBuilder(domainSearchBuilderPathLike); + SearchCriteria searchCriteriaDomainPathLike = setParametersDomainPathLike(detailsSearchBuilderPathLike, requestedDomainPath); + + GenericSearchBuilder detailsSearchBuilderPathEq = createDetailsSearchBuilder(domainSearchBuilderPathEq); + SearchCriteria searchCriteriaDomainPathEq = setParametersDomainPathEq(detailsSearchBuilderPathEq, requestedDomainPath); + + guiThemeIds.addAll(customSearch(searchCriteriaDomainPathLike, null)); + guiThemeIds.addAll(customSearch(searchCriteriaDomainPathEq, null)); + return guiThemeIds; + } + + private SearchCriteria setParametersDomainPathLike(GenericSearchBuilder detailsSearchBuilderPathLike, String requestedDomainPath) { + SearchCriteria searchCriteria = detailsSearchBuilderPathLike.create(); + searchCriteria.setParameters("domainUuidType", "domain"); + searchCriteria.setJoinParameters("domainJoin", "pathLike", requestedDomainPath, "%"); + searchCriteria.setJoinParameters("guiThemesJoin", "recursiveDomains", true); + + return searchCriteria; + } + + private SearchCriteria setParametersDomainPathEq(GenericSearchBuilder detailsSearchBuilderPathEq, String requestedDomainPath) { + SearchCriteria searchCriteria = detailsSearchBuilderPathEq.create(); + searchCriteria.setParameters("domainUuidType", "domain"); + searchCriteria.setJoinParameters("domainJoin", "pathEq", requestedDomainPath); + searchCriteria.setJoinParameters("guiThemesJoin", "recursiveDomains", false); + + return searchCriteria; + } + + private GenericSearchBuilder createDetailsSearchBuilder(SearchBuilder domainSearchBuilder) { + SearchBuilder guiThemeDaoSearchBuilder = guiThemeDao.createSearchBuilder(); + guiThemeDaoSearchBuilder.and("recursiveDomains", guiThemeDaoSearchBuilder.entity().isRecursiveDomains(), SearchCriteria.Op.EQ); + + GenericSearchBuilder guiThemesDetailsJoinDomainJoinGuiThemesSearchBuilder = createSearchBuilder(Long.class); + guiThemesDetailsJoinDomainJoinGuiThemesSearchBuilder.selectFields(guiThemesDetailsJoinDomainJoinGuiThemesSearchBuilder.entity().getGuiThemeId()); + guiThemesDetailsJoinDomainJoinGuiThemesSearchBuilder.and("domainUuidType", guiThemesDetailsJoinDomainJoinGuiThemesSearchBuilder.entity().getType(), SearchCriteria.Op.EQ); + guiThemesDetailsJoinDomainJoinGuiThemesSearchBuilder.join("domainJoin", domainSearchBuilder, domainSearchBuilder.entity().getUuid(), + guiThemesDetailsJoinDomainJoinGuiThemesSearchBuilder.entity().getValue(), JoinBuilder.JoinType.INNER); + guiThemesDetailsJoinDomainJoinGuiThemesSearchBuilder.join("guiThemesJoin", guiThemeDaoSearchBuilder, guiThemeDaoSearchBuilder.entity().getId(), + guiThemesDetailsJoinDomainJoinGuiThemesSearchBuilder.entity().getGuiThemeId(), JoinBuilder.JoinType.INNER); + + domainSearchBuilder.done(); + guiThemeDaoSearchBuilder.done(); + guiThemesDetailsJoinDomainJoinGuiThemesSearchBuilder.done(); + + return guiThemesDetailsJoinDomainJoinGuiThemesSearchBuilder; + } + + public void expungeByGuiThemeId(long guiThemeId) { + SearchBuilder searchBuilder = createSearchBuilder(); + searchBuilder.and("guiThemeId", searchBuilder.entity().getGuiThemeId(), SearchCriteria.Op.EQ); + searchBuilder.done(); + + SearchCriteria searchCriteria = searchBuilder.create(); + searchCriteria.setParameters("guiThemeId", guiThemeId); + expunge(searchCriteria); + } +} diff --git a/engine/schema/src/main/java/org/apache/cloudstack/gui/theme/dao/GuiThemeJoinDao.java b/engine/schema/src/main/java/org/apache/cloudstack/gui/theme/dao/GuiThemeJoinDao.java new file mode 100644 index 00000000000..740199cfca7 --- /dev/null +++ b/engine/schema/src/main/java/org/apache/cloudstack/gui/theme/dao/GuiThemeJoinDao.java @@ -0,0 +1,31 @@ +// Licensed to the Apache Software Foundation (ASF) under one +// or more contributor license agreements. See the NOTICE file +// distributed with this work for additional information +// regarding copyright ownership. The ASF licenses this file +// to you under the Apache License, Version 2.0 (the +// "License"); you may not use this file except in compliance +// with the License. You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, +// software distributed under the License is distributed on an +// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +// KIND, either express or implied. See the License for the +// specific language governing permissions and limitations +// under the License. +package org.apache.cloudstack.gui.theme.dao; + +import com.cloud.utils.Pair; +import com.cloud.utils.db.GenericDao; +import org.apache.cloudstack.gui.theme.GuiThemeJoinVO; + +import java.util.List; + +public interface GuiThemeJoinDao extends GenericDao { + GuiThemeJoinVO findDefaultTheme(); + + Pair, Integer> listGuiThemesWithNoAuthentication(String commonName); + + Pair, Integer> listGuiThemes(Long id, String name, String commonName, String domainUuid, String accountUuid, boolean listAll, boolean showRemoved, Boolean showPublic); +} diff --git a/engine/schema/src/main/java/org/apache/cloudstack/gui/theme/dao/GuiThemeJoinDaoImpl.java b/engine/schema/src/main/java/org/apache/cloudstack/gui/theme/dao/GuiThemeJoinDaoImpl.java new file mode 100644 index 00000000000..ce6f7055812 --- /dev/null +++ b/engine/schema/src/main/java/org/apache/cloudstack/gui/theme/dao/GuiThemeJoinDaoImpl.java @@ -0,0 +1,139 @@ +// 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.gui.theme.dao; + +import com.cloud.utils.Pair; +import com.cloud.utils.db.Filter; +import com.cloud.utils.db.GenericDaoBase; +import com.cloud.utils.db.SearchBuilder; +import com.cloud.utils.db.SearchCriteria; +import org.apache.cloudstack.api.ApiConstants; +import org.apache.cloudstack.gui.theme.GuiThemeJoinVO; +import org.apache.commons.collections.CollectionUtils; +import org.apache.commons.lang3.StringUtils; +import org.springframework.stereotype.Component; + +import javax.inject.Inject; +import java.util.ArrayList; +import java.util.List; + +@Component +public class GuiThemeJoinDaoImpl extends GenericDaoBase implements GuiThemeJoinDao { + @Inject + GuiThemeDetailsDao guiThemeDetailsDao; + + public static final Long INVALID_ID = -1L; + + public GuiThemeJoinVO findDefaultTheme() { + SearchBuilder searchBuilder = createSearchBuilder(); + searchBuilder.and("commonNames", searchBuilder.entity().getCommonNames(), SearchCriteria.Op.NULL); + searchBuilder.and("domainUuids", searchBuilder.entity().getDomains(), SearchCriteria.Op.NULL); + searchBuilder.and("accountUuids", searchBuilder.entity().getAccounts(), SearchCriteria.Op.NULL); + searchBuilder.done(); + + SearchCriteria searchCriteria = searchBuilder.create(); + + return findOneBy(searchCriteria); + } + + public Pair, Integer> listGuiThemesWithNoAuthentication(String commonName) { + SearchCriteria searchCriteria = createGuiThemeSearchCriteria(null, null, commonName, null, null, null, false); + return searchOrderByCreatedDate(searchCriteria, false); + } + + public Pair, Integer> listGuiThemes(Long id, String name, String commonName, String domainUuid, String accountUuid, boolean listAll, + boolean showRemoved, Boolean showPublic) { + SearchCriteria searchCriteria = createGuiThemeSearchCriteria(id, name, commonName, domainUuid, accountUuid, showPublic, listAll); + + if (listAll) { + showRemoved = false; + } + + return searchOrderByCreatedDate(searchCriteria, showRemoved); + } + + private Pair, Integer> searchOrderByCreatedDate(SearchCriteria searchCriteria, boolean showRemoved) { + Filter filter = new Filter(GuiThemeJoinVO.class, "created", false); + return searchAndCount(searchCriteria, filter, showRemoved); + } + + private SearchCriteria createGuiThemeSearchCriteria(Long id, String name, String commonName, String domainUuid, String accountUuid, Boolean showPublic, boolean listAll) { + SearchCriteria searchCriteria = createGuiThemeJoinSearchBuilder(listAll, showPublic).create(); + List idList = new ArrayList<>(); + + if (id != null) { + idList.add(id); + } + + searchCriteria.setParametersIfNotNull("name", name); + searchCriteria.setParametersIfNotNull("isPublic", showPublic); + + if (StringUtils.isNotBlank(accountUuid)) { + searchCriteria.setParameters("accountUuid", "%" + accountUuid + "%"); + } + + if (StringUtils.isNotBlank(commonName)) { + setGuiThemeIdsFilteredByType(idList, ApiConstants.COMMON_NAME, commonName); + } + + if (StringUtils.isNotBlank(domainUuid)) { + setGuiThemeIdsFilteredByType(idList, ApiConstants.DOMAIN, domainUuid); + } + + searchCriteria.setParametersIfNotNull("idIn", idList.toArray()); + + return searchCriteria; + } + + /** + * Sets the `id IN ( )` clause of the query. If the informed value of common name or domain ID does not retrieve any GUI theme ID; then, an invalid ID (-1) is passed to the + * list, as not a single entity has this ID. This is necessary as to set the parameter even if it did not find any GUI theme ID; otherwise, the query would not filter the + * common name or domain ID passed. + */ + public void setGuiThemeIdsFilteredByType(List idList, String type, String value) { + List guiThemeIdsFilteredByType = new ArrayList<>(); + + switch (type) { + case ApiConstants.COMMON_NAME: + guiThemeIdsFilteredByType = guiThemeDetailsDao.listGuiThemeIdsByCommonName(value); + break; + case ApiConstants.DOMAIN: + guiThemeIdsFilteredByType = guiThemeDetailsDao.listGuiThemeIdsByDomainUuids(value); + break; + } + + if (CollectionUtils.isNotEmpty(guiThemeIdsFilteredByType)) { + idList.addAll(guiThemeIdsFilteredByType); + return; + } + logger.trace(String.format("No GUI theme with the specified [%s] with UUID [%s] was found, adding an invalid ID for filtering.", type, value)); + idList.add(INVALID_ID); + } + + private SearchBuilder createGuiThemeJoinSearchBuilder(boolean listAll, Boolean showPublic) { + SearchBuilder guiThemeJoinSearchBuilder = createSearchBuilder(); + guiThemeJoinSearchBuilder.and("idIn", guiThemeJoinSearchBuilder.entity().getId(), SearchCriteria.Op.IN); + guiThemeJoinSearchBuilder.and("name", guiThemeJoinSearchBuilder.entity().getName(), SearchCriteria.Op.EQ); + guiThemeJoinSearchBuilder.and("accountUuid", guiThemeJoinSearchBuilder.entity().getAccounts(), SearchCriteria.Op.LIKE); + + if (!listAll && showPublic != null) { + guiThemeJoinSearchBuilder.and("isPublic", guiThemeJoinSearchBuilder.entity().getIsPublic(), SearchCriteria.Op.EQ); + } + + return guiThemeJoinSearchBuilder; + } +} diff --git a/engine/schema/src/main/java/org/apache/cloudstack/resourcedetail/ResourceDetailsDao.java b/engine/schema/src/main/java/org/apache/cloudstack/resourcedetail/ResourceDetailsDao.java index 6b6fe200c10..7c113a10af4 100644 --- a/engine/schema/src/main/java/org/apache/cloudstack/resourcedetail/ResourceDetailsDao.java +++ b/engine/schema/src/main/java/org/apache/cloudstack/resourcedetail/ResourceDetailsDao.java @@ -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 extends GenericDao { @@ -94,6 +95,8 @@ public interface ResourceDetailsDao extends GenericDao Map listDetailsVisibility(long resourceId); + Pair, Map> listDetailsKeyPairsWithVisibility(long resourceId); + void saveDetails(List details); void addDetail(long resourceId, String key, String value, boolean display); diff --git a/engine/schema/src/main/java/org/apache/cloudstack/resourcedetail/ResourceDetailsDaoBase.java b/engine/schema/src/main/java/org/apache/cloudstack/resourcedetail/ResourceDetailsDaoBase.java index 29d3f88fd90..58b60531e5a 100644 --- a/engine/schema/src/main/java/org/apache/cloudstack/resourcedetail/ResourceDetailsDaoBase.java +++ b/engine/schema/src/main/java/org/apache/cloudstack/resourcedetail/ResourceDetailsDaoBase.java @@ -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 extends G return details; } + @Override + public Pair, Map> listDetailsKeyPairsWithVisibility(long resourceId) { + SearchCriteria sc = AllFieldsSearch.create(); + sc.setParameters("resourceId", resourceId); + List results = search(sc, null); + Map> 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 listDetails(long resourceId) { SearchCriteria sc = AllFieldsSearch.create(); sc.setParameters("resourceId", resourceId); diff --git a/engine/schema/src/main/java/org/apache/cloudstack/storage/datastore/db/SnapshotDataStoreDao.java b/engine/schema/src/main/java/org/apache/cloudstack/storage/datastore/db/SnapshotDataStoreDao.java index f0072a414fb..db4c64bd0ab 100644 --- a/engine/schema/src/main/java/org/apache/cloudstack/storage/datastore/db/SnapshotDataStoreDao.java +++ b/engine/schema/src/main/java/org/apache/cloudstack/storage/datastore/db/SnapshotDataStoreDao.java @@ -67,6 +67,8 @@ StateDao findBySnapshotIdAndNotInDestroyedHiddenState(long snapshotId); + SnapshotDataStoreVO findBySnapshotIdInAnyState(long snapshotId, DataStoreRole role); + List listDestroyed(long storeId); List findBySnapshotId(long snapshotId); diff --git a/engine/schema/src/main/java/org/apache/cloudstack/storage/datastore/db/SnapshotDataStoreDaoImpl.java b/engine/schema/src/main/java/org/apache/cloudstack/storage/datastore/db/SnapshotDataStoreDaoImpl.java index 2b064be6b60..b5faa6caedf 100644 --- a/engine/schema/src/main/java/org/apache/cloudstack/storage/datastore/db/SnapshotDataStoreDaoImpl.java +++ b/engine/schema/src/main/java/org/apache/cloudstack/storage/datastore/db/SnapshotDataStoreDaoImpl.java @@ -417,6 +417,12 @@ public class SnapshotDataStoreDaoImpl extends GenericDaoBase sc = createSearchCriteriaBySnapshotIdAndStoreRole(snapshotId, role); + return findOneBy(sc); + } + @Override public List listAllByVolumeAndDataStore(long volumeId, DataStoreRole role) { SearchCriteria sc = searchFilteringStoreIdEqStateEqStoreRoleEqIdEqUpdateCountEqSnapshotIdEqVolumeIdEq.create(); diff --git a/engine/schema/src/main/resources/META-INF/cloudstack/core/spring-engine-schema-core-common-daos-between-management-and-usage-context.xml b/engine/schema/src/main/resources/META-INF/cloudstack/core/spring-engine-schema-core-common-daos-between-management-and-usage-context.xml index 96579b26516..d308a9e5aaf 100644 --- a/engine/schema/src/main/resources/META-INF/cloudstack/core/spring-engine-schema-core-common-daos-between-management-and-usage-context.xml +++ b/engine/schema/src/main/resources/META-INF/cloudstack/core/spring-engine-schema-core-common-daos-between-management-and-usage-context.xml @@ -63,7 +63,7 @@ - + diff --git a/engine/schema/src/main/resources/META-INF/cloudstack/core/spring-engine-schema-core-daos-context.xml b/engine/schema/src/main/resources/META-INF/cloudstack/core/spring-engine-schema-core-daos-context.xml index 570d2f7c699..4ae4506cab7 100644 --- a/engine/schema/src/main/resources/META-INF/cloudstack/core/spring-engine-schema-core-daos-context.xml +++ b/engine/schema/src/main/resources/META-INF/cloudstack/core/spring-engine-schema-core-daos-context.xml @@ -139,6 +139,7 @@ + @@ -302,4 +303,10 @@ + + + + + + diff --git a/engine/schema/src/main/resources/META-INF/db/schema-42010to42100.sql b/engine/schema/src/main/resources/META-INF/db/schema-42010to42100.sql index 12432e80873..54124fbc709 100644 --- a/engine/schema/src/main/resources/META-INF/db/schema-42010to42100.sql +++ b/engine/schema/src/main/resources/META-INF/db/schema-42010to42100.sql @@ -23,6 +23,10 @@ ALTER TABLE `cloud`.`backup_schedule` ADD COLUMN `max_backups` int(8) default NULL COMMENT 'maximum number of backups to maintain'; ALTER TABLE `cloud`.`backups` ADD COLUMN `backup_interval_type` int(5) COMMENT 'type of backup, e.g. manual, recurring - hourly, daily, weekly or monthly'; +-- Update default value for the config 'vm.network.nic.max.secondary.ipaddresses' (and value to default value if value is null) +UPDATE `cloud`.`configuration` SET default_value = '10' WHERE name = 'vm.network.nic.max.secondary.ipaddresses'; +UPDATE `cloud`.`configuration` SET value = '10' WHERE name = 'vm.network.nic.max.secondary.ipaddresses' AND value IS NULL; + -- Add console_endpoint_creator_address column to cloud.console_session table CALL `cloud`.`IDEMPOTENT_ADD_COLUMN`('cloud.console_session', 'console_endpoint_creator_address', 'VARCHAR(45)'); @@ -204,6 +208,463 @@ SET `sort_key` = CASE END; -- End: Changes for Guest OS category cleanup +-- Update description for configuration: host.capacityType.to.order.clusters +UPDATE `cloud`.`configuration` SET + `description` = 'The host capacity type (CPU, RAM or COMBINED) is used by deployment planner to order clusters during VM resource allocation' +WHERE `name` = 'host.capacityType.to.order.clusters' + AND `description` = 'The host capacity type (CPU or RAM) is used by deployment planner to order clusters during VM resource allocation'; + +-- Whitelabel GUI +CREATE TABLE IF NOT EXISTS `cloud`.`gui_themes` ( + `id` bigint(20) unsigned NOT NULL auto_increment, + `uuid` varchar(255) UNIQUE, + `name` varchar(2048) NOT NULL COMMENT 'A name to identify the theme.', + `description` varchar(4096) DEFAULT NULL COMMENT 'A description for the theme.', + `css` text DEFAULT NULL COMMENT 'The CSS to be retrieved and imported into the GUI when matching the theme access configurations.', + `json_configuration` text DEFAULT NULL COMMENT 'The JSON with the configurations to be retrieved and imported into the GUI when matching the theme access configurations.', + `recursive_domains` tinyint(1) DEFAULT 0 COMMENT 'Defines whether the subdomains of the informed domains are considered. Default value is false.', + `is_public` tinyint(1) default 1 COMMENT 'Defines whether a theme can be retrieved by anyone when only the `internet_domains_names` is informed. If the `domain_uuids` or `account_uuids` is informed, it is considered as `false`.', + `created` datetime NOT NULL, + `removed` datetime DEFAULT NULL, + PRIMARY KEY (`id`) +); + +CREATE TABLE IF NOT EXISTS `cloud`.`gui_themes_details` ( + `id` bigint(20) unsigned NOT NULL auto_increment, + `gui_theme_id` bigint(20) unsigned NOT NULL COMMENT 'Foreign key referencing the GUI theme on `gui_themes` table.', + `type` varchar(100) NOT NULL COMMENT 'The type of GUI theme details. Valid options are: `account`, `domain` and `commonName`', + `value` text NOT NULL COMMENT 'The value of the `type` details. Can be an UUID (account or domain) or internet common name.', + PRIMARY KEY (`id`), + CONSTRAINT `fk_gui_themes_details__gui_theme_id` FOREIGN KEY (`gui_theme_id`) REFERENCES `gui_themes`(`id`) +); + +-- Create the GPU card table to hold the GPU card information +CREATE TABLE IF NOT EXISTS `cloud`.`gpu_card` ( + `id` bigint unsigned NOT NULL AUTO_INCREMENT COMMENT 'id', + `uuid` varchar(40) NOT NULL UNIQUE, + `device_id` varchar(4) NOT NULL COMMENT 'device id of the GPU card', + `device_name` varchar(255) NOT NULL COMMENT 'device name of the GPU card', + `name` varchar(255) NOT NULL COMMENT 'name of the GPU card', + `vendor_name` varchar(255) NOT NULL COMMENT 'vendor name of the GPU card', + `vendor_id` varchar(4) NOT NULL COMMENT 'vendor id of the GPU card', + `created` datetime NOT NULL COMMENT 'date created', + PRIMARY KEY (`id`), + UNIQUE KEY (`vendor_id`, `device_id`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='GPU cards supported by CloudStack'; + +-- Create the vGPU profile table to hold the vGPU profile information. +CREATE TABLE IF NOT EXISTS `cloud`.`vgpu_profile` ( + `id` bigint unsigned NOT NULL AUTO_INCREMENT COMMENT 'id', + `uuid` varchar(40) NOT NULL UNIQUE, + `name` varchar(255) NOT NULL COMMENT 'name of the vGPU profile', + `description` varchar(255) DEFAULT NULL COMMENT 'description of the vGPU profile', + `card_id` bigint unsigned NOT NULL COMMENT 'id of the GPU card', + `video_ram` bigint unsigned DEFAULT NULL COMMENT 'video RAM of the vGPU profile', + `max_heads` bigint unsigned DEFAULT NULL COMMENT 'maximum number of heads of the vGPU profile', + `max_resolution_x` bigint unsigned DEFAULT NULL COMMENT 'maximum resolution x of the vGPU profile', + `max_resolution_y` bigint unsigned DEFAULT NULL COMMENT 'maximum resolution y of the vGPU profile', + `max_vgpu_per_pgpu` bigint unsigned DEFAULT NULL COMMENT 'Maximum number of vGPUs per physical GPU', + `created` datetime NOT NULL COMMENT 'date created', + PRIMARY KEY (`id`), + UNIQUE KEY (`name`, `card_id`), + CONSTRAINT `fk_vgpu_profile_card_id` FOREIGN KEY (`card_id`) REFERENCES `gpu_card`(`id`) ON DELETE CASCADE +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='vGPU profiles supported by CloudStack'; + +-- Create the GPU device table to hold the GPU device information on different hosts +CREATE TABLE IF NOT EXISTS `cloud`.`gpu_device` ( + `id` bigint unsigned NOT NULL AUTO_INCREMENT COMMENT 'id', + `uuid` varchar(40) NOT NULL UNIQUE, + `card_id` bigint unsigned NOT NULL COMMENT 'id of the GPU card', + `vgpu_profile_id` bigint unsigned DEFAULT NULL COMMENT 'id of the vGPU profile.', + `bus_address` varchar(255) NOT NULL COMMENT 'PCI bus address of the GPU device', + `type` varchar(32) NOT NULL COMMENT 'type of the GPU device. PCI or MDEV', + `host_id` bigint unsigned NOT NULL COMMENT 'id of the host where GPU is installed', + `vm_id` bigint unsigned DEFAULT NULL COMMENT 'id of the VM using this GPU device', + `numa_node` varchar(255) DEFAULT NULL COMMENT 'NUMA node of the GPU device', + `pci_root` varchar(255) DEFAULT NULL COMMENT 'PCI root of the GPU device', + `parent_gpu_device_id` bigint unsigned DEFAULT NULL COMMENT 'id of the parent GPU device. null if it is a physical GPU device and for vGPUs points to the actual GPU', + `state` varchar(32) NOT NULL COMMENT 'state of the GPU device', + `managed_state` varchar(32) NOT NULL COMMENT 'resource state of the GPU device', + PRIMARY KEY (`id`), + UNIQUE KEY (`bus_address`, `host_id`), + CONSTRAINT `fk_gpu_devices__card_id` FOREIGN KEY (`card_id`) REFERENCES `gpu_card` (`id`) ON DELETE CASCADE, + CONSTRAINT `fk_gpu_devices__host_id` FOREIGN KEY (`host_id`) REFERENCES `host` (`id`) ON DELETE CASCADE, + CONSTRAINT `fk_gpu_devices__vm_id` FOREIGN KEY (`vm_id`) REFERENCES `vm_instance` (`id`) ON DELETE SET NULL, + CONSTRAINT `fk_gpu_devices__parent_gpu_device_id` FOREIGN KEY (`parent_gpu_device_id`) REFERENCES `gpu_device` (`id`) ON DELETE SET NULL +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='GPU devices installed on hosts'; + +-- Add references to GPU tables +CALL `cloud`.`IDEMPOTENT_ADD_COLUMN`('cloud.service_offering', 'vgpu_profile_id', 'bigint unsigned DEFAULT NULL COMMENT "vgpu profile ID"'); +CALL `cloud`.`IDEMPOTENT_ADD_COLUMN`('cloud.service_offering', 'gpu_count', 'int unsigned DEFAULT NULL COMMENT "number of GPUs"'); +CALL `cloud`.`IDEMPOTENT_ADD_COLUMN`('cloud.service_offering', 'gpu_display', 'boolean DEFAULT false COMMENT "enable GPU display"'); +CALL `cloud`.`IDEMPOTENT_DROP_FOREIGN_KEY`('cloud.service_offering','fk_service_offering__vgpu_profile_id'); +CALL `cloud`.`IDEMPOTENT_ADD_FOREIGN_KEY`('cloud.service_offering', 'fk_service_offering__vgpu_profile_id', '(vgpu_profile_id)', '`vgpu_profile`(`id`)'); + +-- Netris Plugin +CREATE TABLE `cloud`.`netris_providers` ( + `id` bigint unsigned NOT NULL auto_increment COMMENT 'id', + `uuid` varchar(40), + `zone_id` bigint unsigned NOT NULL COMMENT 'Zone ID', + `host_id` bigint unsigned NOT NULL COMMENT 'Host ID', + `name` varchar(40), + `url` varchar(255) NOT NULL, + `username` varchar(255) NOT NULL, + `password` varchar(255) NOT NULL, + `site_name` varchar(255) NOT NULL, + `tenant_name` varchar(255) NOT NULL, + `netris_tag` varchar(255) NOT NULL, + `created` datetime NOT NULL COMMENT 'created date', + `removed` datetime COMMENT 'removed date if not null', + PRIMARY KEY (`id`), + CONSTRAINT `fk_netris_providers__zone_id` FOREIGN KEY `fk_netris_providers__zone_id` (`zone_id`) REFERENCES `data_center`(`id`) ON DELETE CASCADE, + INDEX `i_netris_providers__zone_id`(`zone_id`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8; + +-- Drop the Tungsten and NSX columns from the network offerings (replaced by checking the provider on the ntwk_offering_service_map table) +ALTER TABLE `cloud`.`network_offerings` DROP COLUMN `for_tungsten`; +ALTER TABLE `cloud`.`network_offerings` DROP COLUMN `for_nsx`; + +-- Drop the Tungsten and NSX columns from the VPC offerings (replaced by checking the provider on the vpc_offering_service_map table) +ALTER TABLE `cloud`.`vpc_offerings` DROP COLUMN `for_nsx`; + +-- Add next_hop to the static_routes table +CALL `cloud`.`IDEMPOTENT_ADD_COLUMN`('cloud.static_routes', 'next_hop', 'varchar(50) COMMENT "next hop of the static route" AFTER `vpc_gateway_id`'); + +-- Add `for_router` to `user_ip_address` table +CALL `cloud`.`IDEMPOTENT_ADD_COLUMN`('cloud.user_ip_address', 'for_router', 'tinyint(1) DEFAULT 0 COMMENT "True if the ip address is used by Domain Router to expose services"'); + +-- Add Netris Autoscaling rules +INSERT IGNORE INTO `cloud`.`counter` (uuid, provider, source, name, value, created) VALUES (UUID(), 'Netris', 'cpu', 'VM CPU - average percentage', 'vm.cpu.average.percentage', NOW()); +INSERT IGNORE INTO `cloud`.`counter` (uuid, provider, source, name, value, created) VALUES (UUID(), 'Netris', 'memory', 'VM Memory - average percentage', 'vm.memory.average.percentage', NOW()); + +-- Rename user_vm_details to vm_instance_details +ALTER TABLE `cloud`.`user_vm_details` RENAME TO `cloud`.`vm_instance_details`; +ALTER TABLE `cloud`.`vm_instance_details` DROP FOREIGN KEY `fk_user_vm_details__vm_id`; +ALTER TABLE `cloud`.`vm_instance_details` ADD CONSTRAINT `fk_vm_instance_details__vm_id` FOREIGN KEY (vm_id) REFERENCES vm_instance(id) ON DELETE CASCADE; + +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', + '[]' +); + +ALTER TABLE `cloud`.`networks` MODIFY COLUMN `cidr` varchar(255) DEFAULT NULL COMMENT 'CloudStack managed vms get IP address from cidr.In general this cidr also serves as the network CIDR. But in case IP reservation feature is being used by a Guest network, networkcidr is the Effective network CIDR for that network'; +ALTER TABLE `cloud`.`networks` MODIFY COLUMN `gateway` varchar(255) DEFAULT NULL COMMENT 'gateway(s) for this network configuration'; +ALTER TABLE `cloud`.`networks` MODIFY COLUMN `ip6_cidr` varchar(1024) DEFAULT NULL COMMENT 'IPv6 cidr(s) for this network'; +ALTER TABLE `cloud`.`networks` MODIFY COLUMN `ip6_gateway` varchar(1024) DEFAULT NULL COMMENT 'IPv6 gateway(s) for this network'; + -- Disk controller mappings CREATE TABLE IF NOT EXISTS `cloud`.`disk_controller_mapping` ( `id` bigint(20) unsigned NOT NULL auto_increment, diff --git a/engine/schema/src/main/resources/META-INF/db/views/cloud.account_view.sql b/engine/schema/src/main/resources/META-INF/db/views/cloud.account_view.sql index 6092fe8e845..edc164c40cb 100644 --- a/engine/schema/src/main/resources/META-INF/db/views/cloud.account_view.sql +++ b/engine/schema/src/main/resources/META-INF/db/views/cloud.account_view.sql @@ -64,6 +64,8 @@ select `cpucount`.`count` AS `cpuTotal`, `memorylimit`.`max` AS `memoryLimit`, `memorycount`.`count` AS `memoryTotal`, + `gpulimit`.`max` AS `gpuLimit`, + `gpucount`.`count` AS `gpuTotal`, `primary_storage_limit`.`max` AS `primaryStorageLimit`, `primary_storage_count`.`count` AS `primaryStorageTotal`, `secondary_storage_limit`.`max` AS `secondaryStorageLimit`, @@ -156,6 +158,12 @@ from `cloud`.`resource_count` memorycount ON account.id = memorycount.account_id and memorycount.type = 'memory' and memorycount.tag IS NULL left join + `cloud`.`resource_limit` gpulimit ON account.id = gpulimit.account_id + and gpulimit.type = 'gpu' and gpulimit.tag IS NULL + left join + `cloud`.`resource_count` gpucount ON account.id = gpucount.account_id + and gpucount.type = 'gpu' and gpucount.tag IS NULL + left join `cloud`.`resource_limit` primary_storage_limit ON account.id = primary_storage_limit.account_id and primary_storage_limit.type = 'primary_storage' and primary_storage_limit.tag IS NULL left join diff --git a/engine/schema/src/main/resources/META-INF/db/views/cloud.domain_view.sql b/engine/schema/src/main/resources/META-INF/db/views/cloud.domain_view.sql index c9f7bfc51e4..14fd87536aa 100644 --- a/engine/schema/src/main/resources/META-INF/db/views/cloud.domain_view.sql +++ b/engine/schema/src/main/resources/META-INF/db/views/cloud.domain_view.sql @@ -55,6 +55,8 @@ select `cpucount`.`count` AS `cpuTotal`, `memorylimit`.`max` AS `memoryLimit`, `memorycount`.`count` AS `memoryTotal`, + `gpulimit`.`max` AS `gpuLimit`, + `gpucount`.`count` AS `gpuTotal`, `primary_storage_limit`.`max` AS `primaryStorageLimit`, `primary_storage_count`.`count` AS `primaryStorageTotal`, `secondary_storage_limit`.`max` AS `secondaryStorageLimit`, @@ -130,6 +132,12 @@ from `cloud`.`resource_count` memorycount ON domain.id = memorycount.domain_id and memorycount.type = 'memory' and memorycount.tag IS NULL left join + `cloud`.`resource_limit` gpulimit ON domain.id = gpulimit.domain_id + and gpulimit.type = 'gpu' and gpulimit.tag IS NULL + left join + `cloud`.`resource_count` gpucount ON domain.id = gpucount.domain_id + and gpucount.type = 'gpu' and gpucount.tag IS NULL + left join `cloud`.`resource_limit` primary_storage_limit ON domain.id = primary_storage_limit.domain_id and primary_storage_limit.type = 'primary_storage' and primary_storage_limit.tag IS NULL left join diff --git a/engine/schema/src/main/resources/META-INF/db/views/cloud.gui_themes_view.sql b/engine/schema/src/main/resources/META-INF/db/views/cloud.gui_themes_view.sql new file mode 100644 index 00000000000..3173274623e --- /dev/null +++ b/engine/schema/src/main/resources/META-INF/db/views/cloud.gui_themes_view.sql @@ -0,0 +1,38 @@ +-- Licensed to the Apache Software Foundation (ASF) under one +-- or more contributor license agreements. See the NOTICE file +-- distributed with this work for additional information +-- regarding copyright ownership. The ASF licenses this file +-- to you under the Apache License, Version 2.0 (the +-- "License"); you may not use this file except in compliance +-- with the License. You may obtain a copy of the License at +-- +-- http://www.apache.org/licenses/LICENSE-2.0 +-- +-- Unless required by applicable law or agreed to in writing, +-- software distributed under the License is distributed on an +-- "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +-- KIND, either express or implied. See the License for the +-- specific language governing permissions and limitations +-- under the License. + +-- VIEW `cloud`.`gui_themes_view`; + +DROP VIEW IF EXISTS `cloud`.`gui_themes_view`; + +CREATE VIEW `cloud`.`gui_themes_view` AS +SELECT + `cloud`.`gui_themes`.`id` AS `id`, + `cloud`.`gui_themes`.`uuid` AS `uuid`, + `cloud`.`gui_themes`.`name` AS `name`, + `cloud`.`gui_themes`.`description` AS `description`, + `cloud`.`gui_themes`.`css` AS `css`, + `cloud`.`gui_themes`.`json_configuration` AS `json_configuration`, + (SELECT group_concat(gtd.`value` separator ',') FROM `cloud`.`gui_themes_details` gtd WHERE gtd.`type` = 'commonName' AND gtd.gui_theme_id = `cloud`.`gui_themes`.`id`) common_names, + (SELECT group_concat(gtd.`value` separator ',') FROM `cloud`.`gui_themes_details` gtd WHERE gtd.`type` = 'domain' AND gtd.gui_theme_id = `cloud`.`gui_themes`.`id`) domains, + (SELECT group_concat(gtd.`value` separator ',') FROM `cloud`.`gui_themes_details` gtd WHERE gtd.`type` = 'account' AND gtd.gui_theme_id = `cloud`.`gui_themes`.`id`) accounts, + `cloud`.`gui_themes`.`recursive_domains` AS `recursive_domains`, + `cloud`.`gui_themes`.`is_public` AS `is_public`, + `cloud`.`gui_themes`.`created` AS `created`, + `cloud`.`gui_themes`.`removed` AS `removed` +FROM `cloud`.`gui_themes` LEFT JOIN `cloud`.`gui_themes_details` ON `cloud`.`gui_themes_details`.`gui_theme_id` = `cloud`.`gui_themes`.`id` +GROUP BY `cloud`.`gui_themes`.`id`; diff --git a/engine/schema/src/main/resources/META-INF/db/views/cloud.network_offering_view.sql b/engine/schema/src/main/resources/META-INF/db/views/cloud.network_offering_view.sql index 640b2397a46..368566c32b3 100644 --- a/engine/schema/src/main/resources/META-INF/db/views/cloud.network_offering_view.sql +++ b/engine/schema/src/main/resources/META-INF/db/views/cloud.network_offering_view.sql @@ -59,8 +59,6 @@ SELECT `network_offerings`.`supports_public_access` AS `supports_public_access`, `network_offerings`.`supports_vm_autoscaling` AS `supports_vm_autoscaling`, `network_offerings`.`for_vpc` AS `for_vpc`, - `network_offerings`.`for_tungsten` AS `for_tungsten`, - `network_offerings`.`for_nsx` AS `for_nsx`, `network_offerings`.`network_mode` AS `network_mode`, `network_offerings`.`service_package_id` AS `service_package_id`, `network_offerings`.`routing_mode` AS `routing_mode`, diff --git a/engine/schema/src/main/resources/META-INF/db/views/cloud.service_offering_view.sql b/engine/schema/src/main/resources/META-INF/db/views/cloud.service_offering_view.sql index 18e6231ef89..eb987af3ffb 100644 --- a/engine/schema/src/main/resources/META-INF/db/views/cloud.service_offering_view.sql +++ b/engine/schema/src/main/resources/META-INF/db/views/cloud.service_offering_view.sql @@ -73,6 +73,18 @@ SELECT `vsphere_storage_policy`.`value` AS `vsphere_storage_policy`, `lease_duration_details`.`value` AS `lease_duration`, `lease_expiry_action_details`.`value` AS `lease_expiry_action`, + `gpu_card`.`id` AS `gpu_card_id`, + `gpu_card`.`uuid` AS `gpu_card_uuid`, + `gpu_card`.`name` AS `gpu_card_name`, + `vgpu_profile`.`id` AS `vgpu_profile_id`, + `vgpu_profile`.`uuid` AS `vgpu_profile_uuid`, + `vgpu_profile`.`name` AS `vgpu_profile_name`, + `vgpu_profile`.`video_ram` AS `vgpu_profile_video_ram`, + `vgpu_profile`.`max_heads` AS `vgpu_profile_max_heads`, + `vgpu_profile`.`max_resolution_x` AS `vgpu_profile_max_resolution_x`, + `vgpu_profile`.`max_resolution_y` AS `vgpu_profile_max_resolution_y`, + `service_offering`.`gpu_count` AS `gpu_count`, + `service_offering`.`gpu_display` AS `gpu_display`, GROUP_CONCAT(DISTINCT(domain.id)) AS domain_id, GROUP_CONCAT(DISTINCT(domain.uuid)) AS domain_uuid, GROUP_CONCAT(DISTINCT(domain.name)) AS domain_name, @@ -89,6 +101,10 @@ FROM INNER JOIN `cloud`.`disk_offering` ON service_offering.disk_offering_id = disk_offering.id LEFT JOIN + `cloud`.`vgpu_profile` ON service_offering.vgpu_profile_id = vgpu_profile.id + LEFT JOIN + `cloud`.`gpu_card` ON vgpu_profile.card_id = gpu_card.id + LEFT JOIN `cloud`.`service_offering_details` AS `domain_details` ON `domain_details`.`service_offering_id` = `service_offering`.`id` AND `domain_details`.`name`='domainid' LEFT JOIN `cloud`.`domain` AS `domain` ON FIND_IN_SET(`domain`.`id`, `domain_details`.`value`) diff --git a/engine/schema/src/main/resources/META-INF/db/views/cloud.template_view.sql b/engine/schema/src/main/resources/META-INF/db/views/cloud.template_view.sql index 6bfcdaddbcc..76a8be16bda 100644 --- a/engine/schema/src/main/resources/META-INF/db/views/cloud.template_view.sql +++ b/engine/schema/src/main/resources/META-INF/db/views/cloud.template_view.sql @@ -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'))))); diff --git a/engine/schema/src/main/resources/META-INF/db/views/cloud.user_vm_view.sql b/engine/schema/src/main/resources/META-INF/db/views/cloud.user_vm_view.sql index a0c2720fc63..94bc8640fd5 100644 --- a/engine/schema/src/main/resources/META-INF/db/views/cloud.user_vm_view.sql +++ b/engine/schema/src/main/resources/META-INF/db/views/cloud.user_vm_view.sql @@ -103,6 +103,17 @@ SELECT `backup_offering`.`uuid` AS `backup_offering_uuid`, `backup_offering`.`id` AS `backup_offering_id`, `service_offering`.`name` AS `service_offering_name`, + `service_offering`.`vgpu_profile_id` AS `vgpu_profile_id`, + `vgpu_profile`.`uuid` AS `vgpu_profile_uuid`, + `vgpu_profile`.`name` AS `vgpu_profile_name`, + `vgpu_profile`.`video_ram` AS `vgpu_profile_video_ram`, + `vgpu_profile`.`max_heads` AS `vgpu_profile_max_heads`, + `vgpu_profile`.`max_resolution_x` AS `vgpu_profile_max_resolution_x`, + `vgpu_profile`.`max_resolution_y` AS `vgpu_profile_max_resolution_y`, + `gpu_card`.`id` AS `gpu_card_id`, + `gpu_card`.`uuid` AS `gpu_card_uuid`, + `gpu_card`.`name` AS `gpu_card_name`, + `service_offering`.`gpu_count` AS `gpu_count`, `disk_offering`.`name` AS `disk_offering_name`, `backup_offering`.`name` AS `backup_offering_name`, `storage_pool`.`id` AS `pool_id`, @@ -174,7 +185,7 @@ SELECT `lease_expiry_action`.`value` AS `lease_expiry_action`, `lease_action_execution`.`value` AS `lease_action_execution` FROM - (((((((((((((((((((((((((((((((((((`user_vm` + (((((((((((((((((((((((((((((((((((((`user_vm` JOIN `vm_instance` ON (((`vm_instance`.`id` = `user_vm`.`id`) AND ISNULL(`vm_instance`.`removed`)))) JOIN `account` ON ((`vm_instance`.`account_id` = `account`.`id`))) @@ -192,6 +203,8 @@ FROM LEFT JOIN `service_offering` ON ((`vm_instance`.`service_offering_id` = `service_offering`.`id`))) LEFT JOIN `disk_offering` `svc_disk_offering` ON ((`volumes`.`disk_offering_id` = `svc_disk_offering`.`id`))) LEFT JOIN `disk_offering` ON ((`volumes`.`disk_offering_id` = `disk_offering`.`id`))) + LEFT JOIN `vgpu_profile` ON ((`service_offering`.`vgpu_profile_id` = `vgpu_profile`.`id`))) + LEFT JOIN `gpu_card` ON ((`vgpu_profile`.`card_id` = `gpu_card`.`id`))) LEFT JOIN `backup_offering` ON ((`vm_instance`.`backup_offering_id` = `backup_offering`.`id`))) LEFT JOIN `storage_pool` ON ((`volumes`.`pool_id` = `storage_pool`.`id`))) LEFT JOIN `security_group_vm_map` ON ((`vm_instance`.`id` = `security_group_vm_map`.`instance_id`))) @@ -203,7 +216,7 @@ FROM LEFT JOIN `vpc` ON (((`networks`.`vpc_id` = `vpc`.`id`) AND ISNULL(`vpc`.`removed`)))) LEFT JOIN `user_ip_address` FORCE INDEX(`fk_user_ip_address__vm_id`) ON ((`user_ip_address`.`vm_id` = `vm_instance`.`id`))) - LEFT JOIN `user_vm_details` `ssh_details` ON (((`ssh_details`.`vm_id` = `vm_instance`.`id`) + LEFT JOIN `vm_instance_details` `ssh_details` ON (((`ssh_details`.`vm_id` = `vm_instance`.`id`) AND (`ssh_details`.`name` = 'SSH.KeyPairNames')))) LEFT JOIN `resource_tags` ON (((`resource_tags`.`resource_id` = `vm_instance`.`id`) AND (`resource_tags`.`resource_type` = 'UserVm')))) @@ -214,15 +227,15 @@ FROM LEFT JOIN `affinity_group` ON ((`affinity_group_vm_map`.`affinity_group_id` = `affinity_group`.`id`))) LEFT JOIN `autoscale_vmgroup_vm_map` ON ((`autoscale_vmgroup_vm_map`.`instance_id` = `vm_instance`.`id`))) LEFT JOIN `autoscale_vmgroups` ON ((`autoscale_vmgroup_vm_map`.`vmgroup_id` = `autoscale_vmgroups`.`id`))) - LEFT JOIN `user_vm_details` `custom_cpu` ON (((`custom_cpu`.`vm_id` = `vm_instance`.`id`) + LEFT JOIN `vm_instance_details` `custom_cpu` ON (((`custom_cpu`.`vm_id` = `vm_instance`.`id`) AND (`custom_cpu`.`name` = 'CpuNumber')))) - LEFT JOIN `user_vm_details` `custom_speed` ON (((`custom_speed`.`vm_id` = `vm_instance`.`id`) + LEFT JOIN `vm_instance_details` `custom_speed` ON (((`custom_speed`.`vm_id` = `vm_instance`.`id`) AND (`custom_speed`.`name` = 'CpuSpeed')))) - LEFT JOIN `user_vm_details` `custom_ram_size` ON (((`custom_ram_size`.`vm_id` = `vm_instance`.`id`) + LEFT JOIN `vm_instance_details` `custom_ram_size` ON (((`custom_ram_size`.`vm_id` = `vm_instance`.`id`) AND (`custom_ram_size`.`name` = 'memory'))) - LEFT JOIN `user_vm_details` `lease_expiry_date` ON ((`lease_expiry_date`.`vm_id` = `vm_instance`.`id`) + LEFT JOIN `vm_instance_details` `lease_expiry_date` ON ((`lease_expiry_date`.`vm_id` = `vm_instance`.`id`) AND (`lease_expiry_date`.`name` = 'leaseexpirydate')) - LEFT JOIN `user_vm_details` `lease_action_execution` ON ((`lease_action_execution`.`vm_id` = `vm_instance`.`id`) + LEFT JOIN `vm_instance_details` `lease_action_execution` ON ((`lease_action_execution`.`vm_id` = `vm_instance`.`id`) AND (`lease_action_execution`.`name` = 'leaseactionexecution')) - LEFT JOIN `user_vm_details` `lease_expiry_action` ON (((`lease_expiry_action`.`vm_id` = `vm_instance`.`id`) + LEFT JOIN `vm_instance_details` `lease_expiry_action` ON (((`lease_expiry_action`.`vm_id` = `vm_instance`.`id`) AND (`lease_expiry_action`.`name` = 'leaseexpiryaction')))); diff --git a/engine/schema/src/main/resources/META-INF/db/views/cloud.vpc_offering_view.sql b/engine/schema/src/main/resources/META-INF/db/views/cloud.vpc_offering_view.sql index c74d50590de..751d8f91a25 100644 --- a/engine/schema/src/main/resources/META-INF/db/views/cloud.vpc_offering_view.sql +++ b/engine/schema/src/main/resources/META-INF/db/views/cloud.vpc_offering_view.sql @@ -28,7 +28,6 @@ select `vpc_offerings`.`display_text` AS `display_text`, `vpc_offerings`.`state` AS `state`, `vpc_offerings`.`default` AS `default`, - `vpc_offerings`.`for_nsx` AS `for_nsx`, `vpc_offerings`.`network_mode` AS `network_mode`, `vpc_offerings`.`created` AS `created`, `vpc_offerings`.`removed` AS `removed`, diff --git a/engine/schema/src/test/java/com/cloud/capacity/dao/CapacityDaoImplTest.java b/engine/schema/src/test/java/com/cloud/capacity/dao/CapacityDaoImplTest.java index 8e9a0bd34c7..f9528d5d57f 100644 --- a/engine/schema/src/test/java/com/cloud/capacity/dao/CapacityDaoImplTest.java +++ b/engine/schema/src/test/java/com/cloud/capacity/dao/CapacityDaoImplTest.java @@ -51,6 +51,7 @@ import static org.mockito.Mockito.doNothing; import static org.mockito.Mockito.doReturn; import static org.mockito.Mockito.eq; 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; @@ -61,6 +62,9 @@ public class CapacityDaoImplTest { @InjectMocks CapacityDaoImpl capacityDao = new CapacityDaoImpl(); + @Mock + private CapacityVO mockEntity; + @Mock private TransactionLegacy txn; @Mock @@ -71,6 +75,8 @@ public class CapacityDaoImplTest { private SearchBuilder searchBuilder; private SearchCriteria searchCriteria; + private List capacityTypes; + private List expectedCapacities; @Before public void setUp() { @@ -83,6 +89,17 @@ public class CapacityDaoImplTest { mockedTransactionLegacy = Mockito.mockStatic(TransactionLegacy.class); mockedTransactionLegacy.when(TransactionLegacy::currentTxn).thenReturn(txn); + + // Setup common test data + capacityTypes = Arrays.asList((short) 1, (short) 2, (short) 3); + expectedCapacities = Arrays.asList(mock(CapacityVO.class), mock(CapacityVO.class)); + doReturn(expectedCapacities).when(capacityDao).listBy(searchCriteria); + } + + private CapacityVO createMockCapacityVO(Long id) { + CapacityVO capacity = mock(CapacityVO.class); + when(capacity.getId()).thenReturn(id); + return capacity; } @After @@ -205,11 +222,11 @@ public class CapacityDaoImplTest { when(pstmt.executeQuery()).thenReturn(resultSet); when(resultSet.next()).thenReturn(false); - List resultZone = capacityDao.listClustersInZoneOrPodByHostCapacities(1L, 123L, 2, 2048L, (short)0, true); + List resultZone = capacityDao.listClustersInZoneOrPodByHostCapacities(1L, 123L, 2, 2048L, true); assertNotNull(resultZone); assertTrue(resultZone.isEmpty()); - List resultPod = capacityDao.listClustersInZoneOrPodByHostCapacities(1L, 123L, 2, 2048L, (short)0, false); + List resultPod = capacityDao.listClustersInZoneOrPodByHostCapacities(1L, 123L, 2, 2048L, false); assertNotNull(resultPod); assertTrue(resultPod.isEmpty()); } @@ -281,7 +298,7 @@ public class CapacityDaoImplTest { when(pstmt.executeQuery()).thenReturn(resultSet); when(resultSet.next()).thenReturn(false); - List result = capacityDao.listPodsByHostCapacities(1L, 2, 1024L, (short)0); + List result = capacityDao.listPodsByHostCapacities(1L, 2, 1024L); assertNotNull(result); assertTrue(result.isEmpty()); } @@ -330,4 +347,207 @@ public class CapacityDaoImplTest { assertNotNull(result); assertTrue(result.isEmpty()); } + + @Test + public void testListHostCapacityByCapacityTypes_WithAllParameters() { + // Given + Long zoneId = 100L; + Long clusterId = 200L; + + // When + List result = capacityDao.listHostCapacityByCapacityTypes(zoneId, clusterId, capacityTypes); + + // Then + verify(searchBuilder).and("zoneId", mockEntity.getDataCenterId(), SearchCriteria.Op.EQ); + verify(searchBuilder).and("clusterId", mockEntity.getClusterId(), SearchCriteria.Op.EQ); + verify(searchBuilder).and("capacityTypes", mockEntity.getCapacityType(), SearchCriteria.Op.IN); + verify(searchBuilder).and("capacityState", mockEntity.getCapacityState(), SearchCriteria.Op.EQ); + + verify(searchCriteria).setParameters("capacityState", "Enabled"); + verify(searchCriteria).setParameters("zoneId", zoneId); + verify(searchCriteria).setParameters("clusterId", clusterId); + verify(searchCriteria).setParameters("capacityTypes", capacityTypes.toArray()); + + verify(capacityDao).listBy(searchCriteria); + assertEquals("Should return expected capacities", expectedCapacities, result); + } + + @Test + public void testListHostCapacityByCapacityTypes_WithNullZoneId() { + // Given + Long clusterId = 200L; + + // When + List result = capacityDao.listHostCapacityByCapacityTypes(null, clusterId, capacityTypes); + + // Then + verify(searchCriteria).setParameters("capacityState", "Enabled"); + verify(searchCriteria, Mockito.times(0)).setParameters(eq("zoneId"), any()); + verify(searchCriteria).setParameters("clusterId", clusterId); + verify(searchCriteria).setParameters("capacityTypes", capacityTypes.toArray()); + + assertEquals("Should return expected capacities", expectedCapacities, result); + } + + @Test + public void testListHostCapacityByCapacityTypes_WithNullClusterId() { + // Given + Long zoneId = 100L; + + // When + List result = capacityDao.listHostCapacityByCapacityTypes(zoneId, null, capacityTypes); + + // Then + verify(searchCriteria).setParameters("capacityState", "Enabled"); + verify(searchCriteria).setParameters("zoneId", zoneId); + verify(searchCriteria, never()).setParameters(eq("clusterId"), any()); + verify(searchCriteria).setParameters("capacityTypes", capacityTypes.toArray()); + + assertEquals("Should return expected capacities", expectedCapacities, result); + } + + @Test + public void testListHostCapacityByCapacityTypes_WithEmptyCapacityTypes() { + // Given + Long zoneId = 100L; + Long clusterId = 200L; + List emptyCapacityTypes = Collections.emptyList(); + + // When + List result = capacityDao.listHostCapacityByCapacityTypes(zoneId, clusterId, emptyCapacityTypes); + + // Then + verify(searchCriteria).setParameters("capacityTypes", emptyCapacityTypes.toArray()); + assertEquals("Should return expected capacities", expectedCapacities, result); + } + + @Test + public void testListPodCapacityByCapacityTypes_WithAllParameters() { + // Given + Long zoneId = 100L; + + // When + List result = capacityDao.listPodCapacityByCapacityTypes(zoneId, capacityTypes); + + // Then + verify(searchBuilder).and("zoneId", mockEntity.getDataCenterId(), SearchCriteria.Op.EQ); + verify(searchBuilder).and("capacityTypes", mockEntity.getCapacityType(), SearchCriteria.Op.IN); + verify(searchBuilder).and("capacityState", mockEntity.getCapacityState(), SearchCriteria.Op.EQ); + + verify(searchCriteria).setParameters("capacityState", "Enabled"); + verify(searchCriteria).setParameters("zoneId", zoneId); + verify(searchCriteria).setParameters("capacityTypes", capacityTypes.toArray()); + + assertEquals("Should return expected capacities", expectedCapacities, result); + } + + @Test + public void testListPodCapacityByCapacityTypes_WithNullZoneId() { + // When + List result = capacityDao.listPodCapacityByCapacityTypes(null, capacityTypes); + + // Then + verify(searchCriteria).setParameters("capacityState", "Enabled"); + verify(searchCriteria, never()).setParameters(eq("zoneId"), any()); + verify(searchCriteria).setParameters("capacityTypes", capacityTypes.toArray()); + + assertEquals("Should return expected capacities", expectedCapacities, result); + } + + @Test + public void testListClusterCapacityByCapacityTypes_WithAllParameters() { + // Given + Long zoneId = 100L; + Long podId = 300L; + + // When + List result = capacityDao.listClusterCapacityByCapacityTypes(zoneId, podId, capacityTypes); + + // Then + verify(searchBuilder).and("zoneId", mockEntity.getDataCenterId(), SearchCriteria.Op.EQ); + verify(searchBuilder).and("podId", mockEntity.getPodId(), SearchCriteria.Op.EQ); + verify(searchBuilder).and("capacityTypes", mockEntity.getCapacityType(), SearchCriteria.Op.IN); + verify(searchBuilder).and("capacityState", mockEntity.getCapacityState(), SearchCriteria.Op.EQ); + + verify(searchCriteria).setParameters("capacityState", "Enabled"); + verify(searchCriteria).setParameters("zoneId", zoneId); + verify(searchCriteria).setParameters("podId", podId); + verify(searchCriteria).setParameters("capacityTypes", capacityTypes.toArray()); + + assertEquals("Should return expected capacities", expectedCapacities, result); + } + + @Test + public void testListClusterCapacityByCapacityTypes_WithNullZoneId() { + // Given + Long podId = 300L; + + // When + List result = capacityDao.listClusterCapacityByCapacityTypes(null, podId, capacityTypes); + + // Then + verify(searchCriteria).setParameters("capacityState", "Enabled"); + verify(searchCriteria, never()).setParameters(eq("zoneId"), any()); + verify(searchCriteria).setParameters("podId", podId); + verify(searchCriteria).setParameters("capacityTypes", capacityTypes.toArray()); + + assertEquals("Should return expected capacities", expectedCapacities, result); + } + + @Test + public void testListClusterCapacityByCapacityTypes_WithNullPodId() { + // Given + Long zoneId = 100L; + + // When + List result = capacityDao.listClusterCapacityByCapacityTypes(zoneId, null, capacityTypes); + + // Then + verify(searchCriteria).setParameters("capacityState", "Enabled"); + verify(searchCriteria).setParameters("zoneId", zoneId); + verify(searchCriteria, never()).setParameters(eq("podId"), any()); + verify(searchCriteria).setParameters("capacityTypes", capacityTypes.toArray()); + + assertEquals("Should return expected capacities", expectedCapacities, result); + } + + @Test + public void testListClusterCapacityByCapacityTypes_WithBothIdsNull() { + // When + List result = capacityDao.listClusterCapacityByCapacityTypes(null, null, capacityTypes); + + // Then + verify(searchCriteria).setParameters("capacityState", "Enabled"); + verify(searchCriteria, never()).setParameters(eq("zoneId"), any()); + verify(searchCriteria, never()).setParameters(eq("podId"), any()); + verify(searchCriteria).setParameters("capacityTypes", capacityTypes.toArray()); + + assertEquals("Should return expected capacities", expectedCapacities, result); + } + + @Test + public void testAllMethods_VerifySearchBuilderSetup() { + // Test that all methods properly set up the search builder + Long zoneId = 100L; + Long clusterId = 200L; + Long podId = 300L; + + // Test host capacity method + capacityDao.listHostCapacityByCapacityTypes(zoneId, clusterId, capacityTypes); + + // Test pod capacity method + capacityDao.listPodCapacityByCapacityTypes(zoneId, capacityTypes); + + // Test cluster capacity method + capacityDao.listClusterCapacityByCapacityTypes(zoneId, podId, capacityTypes); + + // Verify createSearchBuilder was called 3 times + verify(capacityDao, times(3)).createSearchBuilder(); + + // Verify done() was called 3 times + verify(searchBuilder, times(3)).done(); + + // Verify listBy was called 3 times + verify(capacityDao, times(3)).listBy(searchCriteria); + } } diff --git a/engine/schema/src/test/java/com/cloud/gpu/dao/GpuCardDaoImplTest.java b/engine/schema/src/test/java/com/cloud/gpu/dao/GpuCardDaoImplTest.java new file mode 100644 index 00000000000..e0a283add99 --- /dev/null +++ b/engine/schema/src/test/java/com/cloud/gpu/dao/GpuCardDaoImplTest.java @@ -0,0 +1,57 @@ +/* + * 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.gpu.dao; + +import com.cloud.gpu.GpuCardVO; +import com.cloud.utils.db.SearchCriteria; +import org.junit.Assert; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.ArgumentCaptor; +import org.mockito.InjectMocks; +import org.mockito.Spy; +import org.mockito.junit.MockitoJUnitRunner; + +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.doReturn; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.verify; + +@RunWith(MockitoJUnitRunner.class) +public class GpuCardDaoImplTest { + + @Spy + @InjectMocks + GpuCardDaoImpl gpuCardDaoImpl = new GpuCardDaoImpl(); + + @Test + public void findByVendorIdAndDeviceId() { + doReturn(mock(GpuCardVO.class)).when(gpuCardDaoImpl).findOneBy(any(SearchCriteria.class)); + + GpuCardVO gpuCard = gpuCardDaoImpl.findByVendorIdAndDeviceId("0d1a", "1a3b"); + Assert.assertNotNull("Expected non-null gpu card", gpuCard); + + ArgumentCaptor scCaptor = ArgumentCaptor.forClass(SearchCriteria.class); + verify(gpuCardDaoImpl).findOneBy(scCaptor.capture()); + Assert.assertEquals("Expected correct where clause", + "gpu_card.vendor_id = ? AND gpu_card.device_id = ?", + scCaptor.getValue().getWhereClause().trim()); + } +} diff --git a/engine/schema/src/test/java/com/cloud/gpu/dao/GpuDeviceDaoImplTest.java b/engine/schema/src/test/java/com/cloud/gpu/dao/GpuDeviceDaoImplTest.java new file mode 100644 index 00000000000..1780fbd3df3 --- /dev/null +++ b/engine/schema/src/test/java/com/cloud/gpu/dao/GpuDeviceDaoImplTest.java @@ -0,0 +1,277 @@ +/* + * 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.gpu.dao; + +import com.cloud.gpu.GpuDeviceVO; +import com.cloud.utils.db.Filter; +import com.cloud.utils.db.SearchCriteria; +import org.junit.After; +import org.junit.Assert; +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.ArgumentCaptor; +import org.mockito.InjectMocks; +import org.mockito.Spy; +import org.mockito.junit.MockitoJUnitRunner; + +import java.util.Collections; +import java.util.List; + +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.doReturn; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +@RunWith(MockitoJUnitRunner.class) +public class GpuDeviceDaoImplTest { + + @Spy + @InjectMocks + GpuDeviceDaoImpl gpuDeviceDao = new GpuDeviceDaoImpl(); + + @Before + public void setUp() throws Exception { + } + + @After + public void tearDown() throws Exception { + } + + @Test + public void listByIds_emptyList() { + List devices = gpuDeviceDao.listByIds(null); + Assert.assertTrue("Expected empty list", devices.isEmpty()); + devices = gpuDeviceDao.listByIds(Collections.emptyList()); + Assert.assertTrue("Expected empty list", devices.isEmpty()); + } + + @Test + public void listByIds() { + doReturn(List.of(mock(GpuDeviceVO.class))).when(gpuDeviceDao).listBy(any(SearchCriteria.class)); + + List devices = gpuDeviceDao.listByIds(List.of(1L, 2L, 3L)); + + Assert.assertFalse("Expected non empty list", devices.isEmpty()); + + ArgumentCaptor scCaptor = ArgumentCaptor.forClass(SearchCriteria.class); + verify(gpuDeviceDao).listBy(scCaptor.capture()); + SearchCriteria sc = scCaptor.getValue(); + Assert.assertEquals("Expected correct where clause", "gpu_device.id IN (?,?,?)", sc.getWhereClause().trim()); + } + + @Test + public void findByHostIdAndBusAddress() { + doReturn(mock(GpuDeviceVO.class)).when(gpuDeviceDao).findOneBy(any(SearchCriteria.class)); + + GpuDeviceVO device = gpuDeviceDao.findByHostIdAndBusAddress(1L, "0000:00:1f.6"); + + Assert.assertNotNull("Expected non-null device", device); + + ArgumentCaptor scCaptor = ArgumentCaptor.forClass(SearchCriteria.class); + verify(gpuDeviceDao).findOneBy(scCaptor.capture()); + Assert.assertEquals("Expected correct where clause", "gpu_device.host_id = ? AND gpu_device.bus_address = ?", + scCaptor.getValue().getWhereClause().trim()); + } + + @Test + public void listByHostId() { + doReturn(List.of(mock(GpuDeviceVO.class))).when(gpuDeviceDao).listBy(any(SearchCriteria.class)); + + List devices = gpuDeviceDao.listByHostId(1L); + + Assert.assertFalse("Expected non empty list", devices.isEmpty()); + + ArgumentCaptor scCaptor = ArgumentCaptor.forClass(SearchCriteria.class); + verify(gpuDeviceDao).listBy(scCaptor.capture()); + Assert.assertEquals("Expected correct where clause", "gpu_device.host_id = ?", + scCaptor.getValue().getWhereClause().trim()); + } + + @Test + public void listByVmId() { + doReturn(List.of(mock(GpuDeviceVO.class))).when(gpuDeviceDao).listBy(any(SearchCriteria.class)); + + List devices = gpuDeviceDao.listByVmId(1L); + + Assert.assertFalse("Expected non empty list", devices.isEmpty()); + ArgumentCaptor scCaptor = ArgumentCaptor.forClass(SearchCriteria.class); + verify(gpuDeviceDao).listBy(scCaptor.capture()); + + Assert.assertEquals("Expected correct where clause", "gpu_device.vm_id = ?", + scCaptor.getValue().getWhereClause().trim()); + } + + @Test + public void isVgpuProfileInUse() { + doReturn(1).when(gpuDeviceDao).getCount(any(SearchCriteria.class)); + + boolean vgpuProfileInUse = gpuDeviceDao.isVgpuProfileInUse(1L); + + Assert.assertTrue("Expected vGPU profile to be in use", vgpuProfileInUse); + + ArgumentCaptor scCaptor = ArgumentCaptor.forClass(SearchCriteria.class); + verify(gpuDeviceDao).getCount(scCaptor.capture()); + Assert.assertEquals("Expected correct where clause", "gpu_device.vgpu_profile_id = ?", + scCaptor.getValue().getWhereClause().trim()); + } + + @Test + public void isGpuCardInUse() { + doReturn(1).when(gpuDeviceDao).getCount(any(SearchCriteria.class)); + + boolean vgpuProfileInUse = gpuDeviceDao.isGpuCardInUse(1L); + + Assert.assertTrue("Expected GPU Card to be in use", vgpuProfileInUse); + + ArgumentCaptor scCaptor = ArgumentCaptor.forClass(SearchCriteria.class); + verify(gpuDeviceDao).getCount(scCaptor.capture()); + Assert.assertEquals("Expected correct where clause", "gpu_device.card_id = ?", + scCaptor.getValue().getWhereClause().trim()); + } + + @Test + public void listByHostAndVm() { + doReturn(List.of(mock(GpuDeviceVO.class))).when(gpuDeviceDao).search(any(SearchCriteria.class), any()); + + List devices = gpuDeviceDao.listByHostAndVm(1L, 2L); + + Assert.assertFalse("Expected non empty list", devices.isEmpty()); + + ArgumentCaptor scCaptor = ArgumentCaptor.forClass(SearchCriteria.class); + ArgumentCaptor filterCaptor = ArgumentCaptor.forClass(Filter.class); + verify(gpuDeviceDao).search(scCaptor.capture(), filterCaptor.capture()); + Assert.assertEquals("Expected correct where clause", "gpu_device.host_id = ? AND gpu_device.vm_id = ?", + scCaptor.getValue().getWhereClause().trim()); + Assert.assertNull("Expected no filter", filterCaptor.getValue()); + } + + @Test + public void listDevicesForAllocation() { + doReturn(List.of(mock(GpuDeviceVO.class))).when(gpuDeviceDao).search(any(SearchCriteria.class), any()); + + List devices = gpuDeviceDao.listDevicesForAllocation(1L, 2L); + + Assert.assertFalse("Expected non empty list", devices.isEmpty()); + + ArgumentCaptor scCaptor = ArgumentCaptor.forClass(SearchCriteria.class); + ArgumentCaptor filterCaptor = ArgumentCaptor.forClass(Filter.class); + verify(gpuDeviceDao).search(scCaptor.capture(), filterCaptor.capture()); + Assert.assertEquals("Expected correct where clause", + "gpu_device.host_id = ? AND gpu_device.vgpu_profile_id=? AND gpu_device.state = ? AND gpu_device" + + ".managed_state = ? AND gpu_device.type != ?", + scCaptor.getValue().getWhereClause().trim()); + Assert.assertNull("Expected no filter", filterCaptor.getValue()); + } + + @Test + public void searchAndCountGpuDevices() { + } + + @Test + public void getDistinctGpuCardIds_no_devices() { + doReturn(null).when(gpuDeviceDao).listBy(any(SearchCriteria.class)); + + List cardIds = gpuDeviceDao.getDistinctGpuCardIds(); + + Assert.assertTrue("Expected empty list", cardIds.isEmpty()); + + ArgumentCaptor scCaptor = ArgumentCaptor.forClass(SearchCriteria.class); + verify(gpuDeviceDao).listBy(scCaptor.capture()); + Assert.assertEquals("Expected correct where clause", "", scCaptor.getValue().getWhereClause().trim()); + } + + + @Test + public void getDistinctGpuCardIds() { + GpuDeviceVO device1 = mock(GpuDeviceVO.class); + GpuDeviceVO device2 = mock(GpuDeviceVO.class); + GpuDeviceVO device3 = mock(GpuDeviceVO.class); + when(device1.getCardId()).thenReturn(1L); + when(device2.getCardId()).thenReturn(2L); + when(device3.getCardId()).thenReturn(1L); + + doReturn(List.of(device1, device2, device3)).when(gpuDeviceDao).listBy(any(SearchCriteria.class)); + + List cardIds = gpuDeviceDao.getDistinctGpuCardIds(); + + Assert.assertEquals("Expected 2 card IDs", 2, cardIds.size()); + + Assert.assertTrue("Expected card ID 1 in list", cardIds.contains(1L)); + Assert.assertTrue("Expected card ID 2 in list", cardIds.contains(2L)); + + ArgumentCaptor scCaptor = ArgumentCaptor.forClass(SearchCriteria.class); + verify(gpuDeviceDao).listBy(scCaptor.capture()); + Assert.assertEquals("Expected correct where clause", "", scCaptor.getValue().getWhereClause().trim()); + } + + @Test + public void getDistinctVgpuProfileIds_no_devices() { + doReturn(null).when(gpuDeviceDao).listBy(any(SearchCriteria.class)); + + List cardIds = gpuDeviceDao.getDistinctVgpuProfileIds(); + + Assert.assertTrue("Expected empty list", cardIds.isEmpty()); + + ArgumentCaptor scCaptor = ArgumentCaptor.forClass(SearchCriteria.class); + verify(gpuDeviceDao).listBy(scCaptor.capture()); + Assert.assertEquals("Expected correct where clause", "", scCaptor.getValue().getWhereClause().trim()); + } + + + @Test + public void getDistinctVgpuProfileIds() { + GpuDeviceVO device1 = mock(GpuDeviceVO.class); + GpuDeviceVO device2 = mock(GpuDeviceVO.class); + GpuDeviceVO device3 = mock(GpuDeviceVO.class); + when(device1.getVgpuProfileId()).thenReturn(1L); + when(device2.getVgpuProfileId()).thenReturn(2L); + when(device3.getVgpuProfileId()).thenReturn(1L); + + doReturn(List.of(device1, device2, device3)).when(gpuDeviceDao).listBy(any(SearchCriteria.class)); + + List cardIds = gpuDeviceDao.getDistinctVgpuProfileIds(); + + Assert.assertEquals("Expected 2 VgpuProfile IDs", 2, cardIds.size()); + + Assert.assertTrue("Expected VgpuProfile ID 1 in list", cardIds.contains(1L)); + Assert.assertTrue("Expected VgpuProfile ID 2 in list", cardIds.contains(2L)); + + ArgumentCaptor scCaptor = ArgumentCaptor.forClass(SearchCriteria.class); + verify(gpuDeviceDao).listBy(scCaptor.capture()); + Assert.assertEquals("Expected correct where clause", "", scCaptor.getValue().getWhereClause().trim()); + } + + + @Test + public void listByParentGpuDeviceId() { + doReturn(List.of(mock(GpuDeviceVO.class))).when(gpuDeviceDao).listBy(any(SearchCriteria.class)); + + List devices = gpuDeviceDao.listByParentGpuDeviceId(1L); + + Assert.assertFalse("Expected non empty list", devices.isEmpty()); + + ArgumentCaptor scCaptor = ArgumentCaptor.forClass(SearchCriteria.class); + verify(gpuDeviceDao).listBy(scCaptor.capture()); + Assert.assertEquals("Expected correct where clause", "gpu_device.parent_gpu_device_id = ?", + scCaptor.getValue().getWhereClause().trim()); + } +} diff --git a/engine/schema/src/test/java/com/cloud/gpu/dao/VgpuProfileDaoImplTest.java b/engine/schema/src/test/java/com/cloud/gpu/dao/VgpuProfileDaoImplTest.java new file mode 100644 index 00000000000..cd7199d020f --- /dev/null +++ b/engine/schema/src/test/java/com/cloud/gpu/dao/VgpuProfileDaoImplTest.java @@ -0,0 +1,70 @@ +/* + * 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.gpu.dao; + +import com.cloud.gpu.VgpuProfileVO; +import com.cloud.utils.db.SearchCriteria; +import org.junit.Assert; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.ArgumentCaptor; +import org.mockito.InjectMocks; +import org.mockito.Spy; +import org.mockito.junit.MockitoJUnitRunner; + +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.doReturn; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.verify; + +@RunWith(MockitoJUnitRunner.class) +public class VgpuProfileDaoImplTest { + + @Spy + @InjectMocks + VgpuProfileDaoImpl vgpuProfileDaoImpl = new VgpuProfileDaoImpl(); + + @Test + public void findByNameAndCardId() { + doReturn(mock(VgpuProfileVO.class)).when(vgpuProfileDaoImpl).findOneBy(any(SearchCriteria.class)); + + VgpuProfileVO vgpuProfile = vgpuProfileDaoImpl.findByNameAndCardId("test-profile", 1L); + Assert.assertNotNull("Expected non-null vgpu profile", vgpuProfile); + + ArgumentCaptor scCaptor = ArgumentCaptor.forClass(SearchCriteria.class); + verify(vgpuProfileDaoImpl).findOneBy(scCaptor.capture()); + Assert.assertEquals("Expected correct where clause", + "vgpu_profile.name = ? AND vgpu_profile.card_id=?", + scCaptor.getValue().getWhereClause().trim()); + } + + @Test + public void removeByCardId() { + doReturn(1).when(vgpuProfileDaoImpl).remove(any(SearchCriteria.class)); + + int removed = vgpuProfileDaoImpl.removeByCardId(123L); + Assert.assertEquals("Expected one vgpu profile removed", 1, removed); + + ArgumentCaptor scCaptor = ArgumentCaptor.forClass(SearchCriteria.class); + verify(vgpuProfileDaoImpl).remove(scCaptor.capture()); + Assert.assertEquals("Expected correct where clause", "vgpu_profile.card_id=?", + scCaptor.getValue().getWhereClause().trim()); + } +} diff --git a/engine/schema/src/test/java/com/cloud/storage/dao/VMTemplateDaoImplTest.java b/engine/schema/src/test/java/com/cloud/storage/dao/VMTemplateDaoImplTest.java index 96f60547f50..7f151730c9c 100644 --- a/engine/schema/src/test/java/com/cloud/storage/dao/VMTemplateDaoImplTest.java +++ b/engine/schema/src/test/java/com/cloud/storage/dao/VMTemplateDaoImplTest.java @@ -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 expectedIds = Arrays.asList(1L, 2L, 3L); + GenericSearchBuilder searchBuilder = mock(GenericSearchBuilder.class); + SearchCriteria 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 result = templateDao.listIdsByExtensionId(extensionId); + assertEquals(expectedIds, result); + verify(searchCriteria).setParameters("extensionId", extensionId); + verify(templateDao).customSearchIncludingRemoved(eq(searchCriteria), isNull()); + } } diff --git a/engine/storage/image/src/main/java/org/apache/cloudstack/storage/image/TemplateServiceImpl.java b/engine/storage/image/src/main/java/org/apache/cloudstack/storage/image/TemplateServiceImpl.java index 38e0d0d081c..3092b00bac2 100644 --- a/engine/storage/image/src/main/java/org/apache/cloudstack/storage/image/TemplateServiceImpl.java +++ b/engine/storage/image/src/main/java/org/apache/cloudstack/storage/image/TemplateServiceImpl.java @@ -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); } diff --git a/engine/storage/image/src/main/java/org/apache/cloudstack/storage/image/store/TemplateObject.java b/engine/storage/image/src/main/java/org/apache/cloudstack/storage/image/store/TemplateObject.java index 0dbe4fd7246..5cb500f5e6c 100644 --- a/engine/storage/image/src/main/java/org/apache/cloudstack/storage/image/store/TemplateObject.java +++ b/engine/storage/image/src/main/java/org/apache/cloudstack/storage/image/store/TemplateObject.java @@ -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; diff --git a/engine/storage/integration-test/src/test/java/org/apache/cloudstack/storage/test/ChildTestConfiguration.java b/engine/storage/integration-test/src/test/java/org/apache/cloudstack/storage/test/ChildTestConfiguration.java index c5321ae6102..4f44051456e 100644 --- a/engine/storage/integration-test/src/test/java/org/apache/cloudstack/storage/test/ChildTestConfiguration.java +++ b/engine/storage/integration-test/src/test/java/org/apache/cloudstack/storage/test/ChildTestConfiguration.java @@ -91,7 +91,7 @@ import com.cloud.vm.dao.DomainRouterDao; import com.cloud.vm.dao.NicDaoImpl; import com.cloud.vm.dao.SecondaryStorageVmDaoImpl; import com.cloud.vm.dao.UserVmDaoImpl; -import com.cloud.vm.dao.UserVmDetailsDaoImpl; +import com.cloud.vm.dao.VMInstanceDetailsDaoImpl; import com.cloud.vm.dao.VMInstanceDaoImpl; import com.cloud.vm.snapshot.dao.VMSnapshotDaoImpl; @@ -101,7 +101,7 @@ import com.cloud.vm.snapshot.dao.VMSnapshotDaoImpl; ConfigurationGroupDaoImpl.class, ConfigurationSubGroupDaoImpl.class, ClusterDaoImpl.class, HostPodDaoImpl.class, VMTemplateZoneDaoImpl.class, VMTemplateDetailsDaoImpl.class, HostDetailsDaoImpl.class, HostTagsDaoImpl.class, HostTransferMapDaoImpl.class, DataCenterIpAddressDaoImpl.class, DataCenterLinkLocalIpAddressDaoImpl.class, DataCenterVnetDaoImpl.class, PodVlanDaoImpl.class, DataCenterDetailsDaoImpl.class, DiskOfferingDaoImpl.class, - StoragePoolHostDaoImpl.class, UserVmDaoImpl.class, UserVmDetailsDaoImpl.class, ServiceOfferingDaoImpl.class, CapacityDaoImpl.class, SnapshotDaoImpl.class, + StoragePoolHostDaoImpl.class, UserVmDaoImpl.class, VMInstanceDetailsDaoImpl.class, ServiceOfferingDaoImpl.class, CapacityDaoImpl.class, SnapshotDaoImpl.class, VMSnapshotDaoImpl.class, OCFS2ManagerImpl.class, ClusterDetailsDaoImpl.class, SecondaryStorageVmDaoImpl.class, ConsoleProxyDaoImpl.class, StoragePoolWorkDaoImpl.class, StorageCacheManagerImpl.class, UserDaoImpl.class, DataCenterDaoImpl.class, StoragePoolDetailsDaoImpl.class, DomainDaoImpl.class, DownloadMonitorImpl.class, AccountDaoImpl.class, ActionEventUtils.class, EventDaoImpl.class}, diff --git a/engine/storage/snapshot/src/main/java/org/apache/cloudstack/storage/snapshot/DefaultSnapshotStrategy.java b/engine/storage/snapshot/src/main/java/org/apache/cloudstack/storage/snapshot/DefaultSnapshotStrategy.java index 0801731630b..aedc2a12d0f 100644 --- a/engine/storage/snapshot/src/main/java/org/apache/cloudstack/storage/snapshot/DefaultSnapshotStrategy.java +++ b/engine/storage/snapshot/src/main/java/org/apache/cloudstack/storage/snapshot/DefaultSnapshotStrategy.java @@ -23,7 +23,15 @@ import java.util.Objects; import javax.inject.Inject; +import com.cloud.storage.VolumeApiServiceImpl; import com.cloud.utils.db.TransactionCallback; +import com.cloud.vm.VMInstanceVO; +import com.cloud.vm.dao.VMInstanceDao; +import com.cloud.vm.snapshot.VMSnapshot; +import com.cloud.vm.snapshot.VMSnapshotDetailsVO; +import com.cloud.vm.snapshot.VMSnapshotVO; +import com.cloud.vm.snapshot.dao.VMSnapshotDao; +import com.cloud.vm.snapshot.dao.VMSnapshotDetailsDao; import org.apache.cloudstack.engine.subsystem.api.storage.DataStore; import org.apache.cloudstack.engine.subsystem.api.storage.DataStoreManager; import org.apache.cloudstack.engine.subsystem.api.storage.ObjectInDataStoreStateMachine.Event; @@ -100,6 +108,15 @@ public class DefaultSnapshotStrategy extends SnapshotStrategyBase { @Inject SnapshotZoneDao snapshotZoneDao; + @Inject + private VMSnapshotDao vmSnapshotDao; + + @Inject + private VMSnapshotDetailsDao vmSnapshotDetailsDao; + + @Inject + private VMInstanceDao vmInstanceDao; + private final List snapshotStatesAbleToDeleteSnapshot = Arrays.asList(Snapshot.State.Destroying, Snapshot.State.Destroyed, Snapshot.State.Error, Snapshot.State.Hidden); public SnapshotDataStoreVO getSnapshotImageStoreRef(long snapshotId, long zoneId) { @@ -610,6 +627,9 @@ public class DefaultSnapshotStrategy extends SnapshotStrategyBase { @Override public StrategyPriority canHandle(Snapshot snapshot, Long zoneId, SnapshotOperation op) { + if (SnapshotOperation.TAKE.equals(op)) { + return validateVmSnapshot(snapshot); + } if (SnapshotOperation.REVERT.equals(op)) { long volumeId = snapshot.getVolumeId(); VolumeVO volumeVO = volumeDao.findById(volumeId); @@ -626,6 +646,30 @@ public class DefaultSnapshotStrategy extends SnapshotStrategyBase { return StrategyPriority.DEFAULT; } + private StrategyPriority validateVmSnapshot(Snapshot snapshot) { + VolumeVO volumeVO = volumeDao.findById(snapshot.getVolumeId()); + Long instanceId = volumeVO.getInstanceId(); + if (instanceId == null) { + return StrategyPriority.DEFAULT; + } + + VMInstanceVO vm = vmInstanceDao.findById(instanceId); + if (vm == null) { + return StrategyPriority.DEFAULT; + } + + for (VMSnapshotVO vmSnapshotVO : vmSnapshotDao.findByVmAndByType(vm.getId(), VMSnapshot.Type.Disk)) { + List vmSnapshotDetails = vmSnapshotDetailsDao.listDetails(vmSnapshotVO.getId()); + if (vmSnapshotDetails.stream().anyMatch(vmSnapshotDetailsVO -> VolumeApiServiceImpl.KVM_FILE_BASED_STORAGE_SNAPSHOT.equals(vmSnapshotDetailsVO.getName()))) { + logger.warn("VM [{}] already has KVM File-Based storage VM snapshots. These VM snapshots and volume snapshots are not supported " + + "together for KVM. As restoring volume snapshots will erase the VM snapshots and cause data loss.", vm.getUuid()); + return StrategyPriority.CANT_HANDLE; + } + } + + return StrategyPriority.DEFAULT; + } + protected boolean isSnapshotStoredOnSameZoneStoreForQCOW2Volume(Snapshot snapshot, VolumeVO volumeVO) { if (volumeVO == null || !ImageFormat.QCOW2.equals(volumeVO.getFormat())) { return false; diff --git a/engine/storage/snapshot/src/main/java/org/apache/cloudstack/storage/snapshot/SnapshotObject.java b/engine/storage/snapshot/src/main/java/org/apache/cloudstack/storage/snapshot/SnapshotObject.java index 178d42f5c74..6cec193cd0a 100644 --- a/engine/storage/snapshot/src/main/java/org/apache/cloudstack/storage/snapshot/SnapshotObject.java +++ b/engine/storage/snapshot/src/main/java/org/apache/cloudstack/storage/snapshot/SnapshotObject.java @@ -155,7 +155,9 @@ public class SnapshotObject implements SnapshotInfo { @Override public SnapshotInfo getChild() { QueryBuilder sc = QueryBuilder.create(SnapshotDataStoreVO.class); - sc.and(sc.entity().getDataStoreId(), Op.EQ, store.getId()); + if (!HypervisorType.KVM.equals(snapshot.getHypervisorType())) { + sc.and(sc.entity().getDataStoreId(), Op.EQ, store.getId()); + } sc.and(sc.entity().getRole(), Op.EQ, store.getRole()); sc.and(sc.entity().getState(), Op.NIN, State.Destroying, State.Destroyed, State.Error); sc.and(sc.entity().getParentSnapshotId(), Op.EQ, getId()); diff --git a/engine/storage/snapshot/src/main/java/org/apache/cloudstack/storage/vmsnapshot/DefaultVMSnapshotStrategy.java b/engine/storage/snapshot/src/main/java/org/apache/cloudstack/storage/vmsnapshot/DefaultVMSnapshotStrategy.java index 09f569e6f19..2d9b1e67e09 100644 --- a/engine/storage/snapshot/src/main/java/org/apache/cloudstack/storage/vmsnapshot/DefaultVMSnapshotStrategy.java +++ b/engine/storage/snapshot/src/main/java/org/apache/cloudstack/storage/vmsnapshot/DefaultVMSnapshotStrategy.java @@ -25,6 +25,9 @@ import java.util.Map; import javax.inject.Inject; import javax.naming.ConfigurationException; +import com.cloud.hypervisor.Hypervisor; +import com.cloud.vm.snapshot.VMSnapshotDetailsVO; +import com.cloud.vm.snapshot.dao.VMSnapshotDetailsDao; import org.apache.cloudstack.engine.subsystem.api.storage.StrategyPriority; import org.apache.cloudstack.engine.subsystem.api.storage.VMSnapshotOptions; import org.apache.cloudstack.engine.subsystem.api.storage.VMSnapshotStrategy; @@ -100,6 +103,13 @@ public class DefaultVMSnapshotStrategy extends ManagerBase implements VMSnapshot @Inject PrimaryDataStoreDao primaryDataStoreDao; + @Inject + VMSnapshotDetailsDao vmSnapshotDetailsDao; + + protected static final String KVM_FILE_BASED_STORAGE_SNAPSHOT = "kvmFileBasedStorageSnapshot"; + + protected static final String STORAGE_SNAPSHOT = "kvmStorageSnapshot"; + @Override public boolean configure(String name, Map params) throws ConfigurationException { String value = configurationDao.getValue("vmsnapshot.create.wait"); @@ -469,16 +479,39 @@ public class DefaultVMSnapshotStrategy extends ManagerBase implements VMSnapshot @Override public StrategyPriority canHandle(Long vmId, Long rootPoolId, boolean snapshotMemory) { UserVmVO vm = userVmDao.findById(vmId); - if (vm.getState() == State.Running && !snapshotMemory) { + if (State.Running.equals(vm.getState()) && !snapshotMemory) { + logger.debug("Default VM snapshot strategy cannot handle VM snapshot for [{}] as it is running and its memory will not be affected.", vm); + return StrategyPriority.CANT_HANDLE; + } + + if (vmHasKvmDiskOnlySnapshot(vm)) { + logger.debug("Default VM snapshot strategy cannot handle VM snapshot for [{}] as it has a disk-only VM snapshot using kvmFileBasedStorageSnapshot strategy." + + "These two strategies are not compatible, as reverting a disk-only VM snapshot will erase newer disk-and-memory VM snapshots.", vm); return StrategyPriority.CANT_HANDLE; } List volumes = volumeDao.findByInstance(vmId); for (VolumeVO volume : volumes) { if (volume.getFormat() != ImageFormat.QCOW2) { + logger.debug("Default VM snapshot strategy cannot handle VM snapshot for [{}] as it has a volume [{}] that is not in the QCOW2 format.", vm, volume); return StrategyPriority.CANT_HANDLE; } } return StrategyPriority.DEFAULT; } + + private boolean vmHasKvmDiskOnlySnapshot(UserVm vm) { + if (!Hypervisor.HypervisorType.KVM.equals(vm.getHypervisorType())) { + return false; + } + + for (VMSnapshotVO vmSnapshotVO : vmSnapshotDao.findByVmAndByType(vm.getId(), VMSnapshot.Type.Disk)) { + List vmSnapshotDetails = vmSnapshotDetailsDao.listDetails(vmSnapshotVO.getId()); + if (vmSnapshotDetails.stream().anyMatch(vmSnapshotDetailsVO -> vmSnapshotDetailsVO.getName().equals(KVM_FILE_BASED_STORAGE_SNAPSHOT))) { + return true; + } + } + + return false; + } } diff --git a/engine/storage/snapshot/src/main/java/org/apache/cloudstack/storage/vmsnapshot/KvmFileBasedStorageVmSnapshotStrategy.java b/engine/storage/snapshot/src/main/java/org/apache/cloudstack/storage/vmsnapshot/KvmFileBasedStorageVmSnapshotStrategy.java new file mode 100644 index 00000000000..003065e394f --- /dev/null +++ b/engine/storage/snapshot/src/main/java/org/apache/cloudstack/storage/vmsnapshot/KvmFileBasedStorageVmSnapshotStrategy.java @@ -0,0 +1,689 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +package org.apache.cloudstack.storage.vmsnapshot; + +import com.cloud.agent.api.Answer; +import com.cloud.agent.api.VMSnapshotTO; +import com.cloud.agent.api.storage.CreateDiskOnlyVmSnapshotAnswer; +import com.cloud.agent.api.storage.CreateDiskOnlyVmSnapshotCommand; +import com.cloud.agent.api.storage.DeleteDiskOnlyVmSnapshotCommand; +import com.cloud.agent.api.storage.MergeDiskOnlyVmSnapshotCommand; +import com.cloud.agent.api.storage.RevertDiskOnlyVmSnapshotAnswer; +import com.cloud.agent.api.storage.RevertDiskOnlyVmSnapshotCommand; +import com.cloud.agent.api.storage.SnapshotMergeTreeTO; +import com.cloud.agent.api.to.DataTO; +import com.cloud.configuration.Resource; +import com.cloud.event.EventTypes; +import com.cloud.event.UsageEventUtils; +import com.cloud.hypervisor.Hypervisor; +import com.cloud.storage.DataStoreRole; +import com.cloud.storage.Snapshot; +import com.cloud.storage.SnapshotVO; +import com.cloud.storage.Storage; +import com.cloud.storage.Volume; +import com.cloud.storage.VolumeVO; +import com.cloud.user.ResourceLimitService; +import com.cloud.uservm.UserVm; +import com.cloud.utils.DateUtil; +import com.cloud.utils.Pair; +import com.cloud.utils.exception.CloudRuntimeException; +import com.cloud.utils.fsm.NoTransitionException; +import com.cloud.vm.UserVmVO; +import com.cloud.vm.VirtualMachine; +import com.cloud.vm.snapshot.VMSnapshot; +import com.cloud.vm.snapshot.VMSnapshotDetailsVO; +import com.cloud.vm.snapshot.VMSnapshotVO; +import org.apache.cloudstack.backup.BackupOfferingVO; +import org.apache.cloudstack.backup.dao.BackupOfferingDao; +import org.apache.cloudstack.engine.subsystem.api.storage.ObjectInDataStoreStateMachine; +import org.apache.cloudstack.engine.subsystem.api.storage.SnapshotInfo; +import org.apache.cloudstack.engine.subsystem.api.storage.StrategyPriority; +import org.apache.cloudstack.engine.subsystem.api.storage.VMSnapshotOptions; +import org.apache.cloudstack.engine.subsystem.api.storage.VolumeInfo; +import org.apache.cloudstack.storage.datastore.db.SnapshotDataStoreDao; +import org.apache.cloudstack.storage.datastore.db.SnapshotDataStoreVO; +import org.apache.cloudstack.storage.datastore.db.StoragePoolVO; +import org.apache.cloudstack.storage.snapshot.SnapshotObject; +import org.apache.cloudstack.storage.to.SnapshotObjectTO; +import org.apache.cloudstack.storage.to.VolumeObjectTO; +import org.apache.commons.collections.CollectionUtils; + +import javax.inject.Inject; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.NoSuchElementException; +import java.util.Objects; +import java.util.stream.Collectors; + +public class KvmFileBasedStorageVmSnapshotStrategy extends StorageVMSnapshotStrategy { + + private static final List supportedStoragePoolTypes = List.of(Storage.StoragePoolType.Filesystem, Storage.StoragePoolType.NetworkFilesystem, Storage.StoragePoolType.SharedMountPoint); + + @Inject + protected SnapshotDataStoreDao snapshotDataStoreDao; + + @Inject + protected ResourceLimitService resourceLimitManager; + + @Inject + protected BackupOfferingDao backupOfferingDao; + + @Override + public VMSnapshot takeVMSnapshot(VMSnapshot vmSnapshot) { + Map volumeInfoToSnapshotObjectMap = new HashMap<>(); + try { + return takeVmSnapshotInternal(vmSnapshot, volumeInfoToSnapshotObjectMap); + } catch (CloudRuntimeException | NullPointerException | NoTransitionException ex) { + for (VolumeInfo volumeInfo : volumeInfoToSnapshotObjectMap.keySet()) { + volumeInfo.stateTransit(Volume.Event.OperationFailed); + SnapshotObject snapshot = volumeInfoToSnapshotObjectMap.get(volumeInfo); + try { + snapshot.processEvent(Snapshot.Event.OperationFailed); + } catch (NoTransitionException e) { + logger.error("Failed to change snapshot [{}] state due to [{}].", snapshot.getUuid(), e.getMessage(), e); + } + snapshot.processEvent(ObjectInDataStoreStateMachine.Event.OperationFailed); + } + try { + vmSnapshotHelper.vmSnapshotStateTransitTo(vmSnapshot, VMSnapshot.Event.OperationFailed); + } catch (NoTransitionException e) { + throw new CloudRuntimeException(e); + } + throw new CloudRuntimeException(ex); + } + } + + @Override + public boolean deleteVMSnapshot(VMSnapshot vmSnapshot) { + logger.info("Starting VM snapshot delete process for snapshot [{}].", vmSnapshot.getUuid()); + UserVmVO userVm = userVmDao.findById(vmSnapshot.getVmId()); + VMSnapshotVO vmSnapshotBeingDeleted = (VMSnapshotVO) vmSnapshot; + Long hostId = vmSnapshotHelper.pickRunningHost(vmSnapshotBeingDeleted.getVmId()); + long virtualSize = 0; + boolean isCurrent = vmSnapshotBeingDeleted.getCurrent(); + + transitStateWithoutThrow(vmSnapshotBeingDeleted, VMSnapshot.Event.ExpungeRequested); + + List volumeTOs = vmSnapshotHelper.getVolumeTOList(vmSnapshotBeingDeleted.getVmId()); + List snapshotChildren = vmSnapshotDao.listByParentAndStateIn(vmSnapshotBeingDeleted.getId(), VMSnapshot.State.Ready, VMSnapshot.State.Hidden); + + long realSize = getVMSnapshotRealSize(vmSnapshotBeingDeleted); + int numberOfChildren = snapshotChildren.size(); + + List volumeSnapshotVos = new ArrayList<>(); + if (isCurrent && numberOfChildren == 0) { + volumeSnapshotVos = mergeCurrentDeltaOnSnapshot(vmSnapshotBeingDeleted, userVm, hostId, volumeTOs); + } else if (numberOfChildren == 0) { + logger.debug("Deleting VM snapshot [{}] as no snapshots/volumes depend on it.", vmSnapshot.getUuid()); + volumeSnapshotVos = deleteSnapshot(vmSnapshotBeingDeleted, hostId); + mergeOldSiblingWithOldParentIfOldParentIsDead(vmSnapshotDao.findByIdIncludingRemoved(vmSnapshotBeingDeleted.getParent()), userVm, hostId, volumeTOs); + } else if (!isCurrent && numberOfChildren == 1) { + VMSnapshotVO childSnapshot = snapshotChildren.get(0); + volumeSnapshotVos = mergeSnapshots(vmSnapshotBeingDeleted, childSnapshot, userVm, volumeTOs, hostId); + } + + for (SnapshotVO snapshotVO : volumeSnapshotVos) { + snapshotVO.setState(Snapshot.State.Destroyed); + snapshotDao.update(snapshotVO.getId(), snapshotVO); + } + + for (VolumeObjectTO volumeTo : volumeTOs) { + publishUsageEvent(EventTypes.EVENT_VM_SNAPSHOT_DELETE, vmSnapshotBeingDeleted, userVm, volumeTo); + virtualSize += volumeTo.getSize(); + } + + publishUsageEvent(EventTypes.EVENT_VM_SNAPSHOT_OFF_PRIMARY, vmSnapshotBeingDeleted, userVm, realSize, virtualSize); + + if (numberOfChildren > 1 || (isCurrent && numberOfChildren == 1)) { + transitStateWithoutThrow(vmSnapshotBeingDeleted, VMSnapshot.Event.Hide); + return true; + } + + transitStateWithoutThrow(vmSnapshotBeingDeleted, VMSnapshot.Event.OperationSucceeded); + + vmSnapshotDetailsDao.removeDetails(vmSnapshotBeingDeleted.getId()); + + vmSnapshotBeingDeleted.setRemoved(DateUtil.now()); + vmSnapshotDao.update(vmSnapshotBeingDeleted.getId(), vmSnapshotBeingDeleted); + + return true; + } + + @Override + public boolean revertVMSnapshot(VMSnapshot vmSnapshot) { + UserVmVO userVm = userVmDao.findById(vmSnapshot.getVmId()); + if (!VirtualMachine.State.Stopped.equals(userVm.getState())) { + throw new CloudRuntimeException("VM must be stopped to revert disk-only VM snapshot."); + } + + VMSnapshotVO vmSnapshotBeingReverted = (VMSnapshotVO) vmSnapshot; + Long hostId = vmSnapshotHelper.pickRunningHost(vmSnapshotBeingReverted.getVmId()); + + transitStateWithoutThrow(vmSnapshotBeingReverted, VMSnapshot.Event.RevertRequested); + + List volumeSnapshots = getVolumeSnapshotsAssociatedWithVmSnapshot(vmSnapshotBeingReverted); + List volumeSnapshotTos = volumeSnapshots.stream() + .map(snapshot -> (SnapshotObjectTO) snapshotDataFactory.getSnapshot(snapshot.getSnapshotId(), snapshot.getDataStoreId(), DataStoreRole.Primary).getTO()) + .collect(Collectors.toList()); + + RevertDiskOnlyVmSnapshotCommand revertDiskOnlyVMSnapshotCommand = new RevertDiskOnlyVmSnapshotCommand(volumeSnapshotTos, userVm.getName()); + Answer answer = agentMgr.easySend(hostId, revertDiskOnlyVMSnapshotCommand); + + if (answer == null || !answer.getResult()) { + transitStateWithoutThrow(vmSnapshotBeingReverted, VMSnapshot.Event.OperationFailed); + logger.error(answer != null ? answer.getDetails() : String.format("Communication failure with host [%s].", hostId)); + throw new CloudRuntimeException(String.format("Error reverting VM snapshot [%s].", vmSnapshot.getUuid())); + } + + RevertDiskOnlyVmSnapshotAnswer revertDiskOnlyVMSnapshotAnswer = (RevertDiskOnlyVmSnapshotAnswer) answer; + + for (VolumeObjectTO volumeObjectTo : revertDiskOnlyVMSnapshotAnswer.getVolumeObjectTos()) { + VolumeVO volumeVO = volumeDao.findById(volumeObjectTo.getVolumeId()); + volumeVO.setPath(volumeObjectTo.getPath()); + updateSizeIfNeeded(volumeSnapshots, volumeVO, volumeObjectTo); + + volumeDao.update(volumeVO.getId(), volumeVO); + publishUsageEvent(EventTypes.EVENT_VM_SNAPSHOT_REVERT, vmSnapshotBeingReverted, userVm, volumeObjectTo); + } + + transitStateWithoutThrow(vmSnapshotBeingReverted, VMSnapshot.Event.OperationSucceeded); + + VMSnapshotVO currentVmSnapshot = vmSnapshotDao.findCurrentSnapshotByVmId(userVm.getId()); + currentVmSnapshot.setCurrent(false); + vmSnapshotBeingReverted.setCurrent(true); + + vmSnapshotDao.update(currentVmSnapshot.getId(), currentVmSnapshot); + vmSnapshotDao.update(vmSnapshotBeingReverted.getId(), vmSnapshotBeingReverted); + + mergeOldSiblingWithOldParentIfOldParentIsDead(currentVmSnapshot, userVm, hostId, vmSnapshotHelper.getVolumeTOList(userVm.getId())); + return true; + } + + /** + * Updates the volume size if it changed due to the snapshot reversion. + * */ + private void updateSizeIfNeeded(List volumeSnapshots, VolumeVO volumeVO, VolumeObjectTO volumeObjectTO) { + SnapshotDataStoreVO snapshotRef = volumeSnapshots.stream().filter(snapshotDataStoreVO -> snapshotDataStoreVO.getVolumeId() == volumeVO.getId()). + findFirst(). + orElseThrow(() -> new CloudRuntimeException(String.format("Unable to map any snapshot to volume [%s].", volumeVO))); + + if (volumeVO.getSize() == snapshotRef.getSize()) { + logger.debug("No need to update the volume size and launch a resize event as the snapshot [{}] and volume [{}] size are equal.", snapshotRef.getSnapshotId(), volumeVO.getUuid()); + return; + } + + long delta = volumeVO.getSize() - snapshotRef.getSize(); + if (delta < 0) { + resourceLimitManager.incrementResourceCount(volumeVO.getAccountId(), Resource.ResourceType.primary_storage, -delta); + } else { + resourceLimitManager.decrementResourceCount(volumeVO.getAccountId(), Resource.ResourceType.primary_storage, delta); + } + volumeVO.setSize(snapshotRef.getSize()); + volumeObjectTO.setSize(snapshotRef.getSize()); + volumeDao.update(volumeVO.getId(), volumeVO); + UsageEventUtils.publishUsageEvent(EventTypes.EVENT_VOLUME_RESIZE, volumeVO.getAccountId(), volumeVO.getDataCenterId(), volumeVO.getId(), volumeVO.getName(), + volumeVO.getDiskOfferingId(), volumeVO.getTemplateId(), volumeVO.getSize(), Volume.class.getName(), volumeVO.getUuid()); + } + + private void mergeOldSiblingWithOldParentIfOldParentIsDead(VMSnapshotVO oldParent, UserVmVO userVm, Long hostId, List volumeTOs) { + if (oldParent == null || oldParent.getRemoved() != null || !VMSnapshot.State.Hidden.equals(oldParent.getState())) { + return; + } + + List snapshotVos; + + if (oldParent.getCurrent()) { + snapshotVos = mergeCurrentDeltaOnSnapshot(oldParent, userVm, hostId, volumeTOs); + } else { + List oldSiblings = vmSnapshotDao.listByParentAndStateIn(oldParent.getId(), VMSnapshot.State.Ready, VMSnapshot.State.Hidden); + + if (oldSiblings.size() > 1) { + logger.debug("The old snapshot [{}] is dead and still has more than one live child snapshot. We will keep it on storage still.", oldParent.getUuid()); + return; + } + + if (oldSiblings.isEmpty()) { + logger.warn("The old snapshot [{}] is dead, but it only had one child. This is an inconsistency and should be analysed/reported.", oldParent.getUuid()); + return; + } + + VMSnapshotVO oldSibling = oldSiblings.get(0); + logger.debug("Merging VM snapshot [{}] with [{}] as the former was hidden and only the latter depends on it.", oldParent.getUuid(), oldSibling.getUuid()); + + snapshotVos = mergeSnapshots(oldParent, oldSibling, userVm, volumeTOs, hostId); + } + + for (SnapshotVO snapshotVO : snapshotVos) { + snapshotVO.setState(Snapshot.State.Destroyed); + snapshotDao.update(snapshotVO.getId(), snapshotVO); + } + + vmSnapshotDetailsDao.removeDetails(oldParent.getId()); + + oldParent.setRemoved(DateUtil.now()); + vmSnapshotDao.update(oldParent.getId(), oldParent); + + transitStateWithoutThrow(oldParent, VMSnapshot.Event.ExpungeRequested); + transitStateWithoutThrow(oldParent, VMSnapshot.Event.OperationSucceeded); + } + + @Override + public StrategyPriority canHandle(VMSnapshot vmSnapshot) { + if (!VMSnapshot.State.Allocated.equals(vmSnapshot.getState())) { + List vmSnapshotDetails = vmSnapshotDetailsDao.findDetails(vmSnapshot.getId(), KVM_FILE_BASED_STORAGE_SNAPSHOT); + if (CollectionUtils.isEmpty(vmSnapshotDetails)) { + logger.debug("KVM file based storage VM snapshot strategy cannot handle [{}] as it is not a KVM file based storage VM snapshot.", + vmSnapshot.getUuid()); + return StrategyPriority.CANT_HANDLE; + } + return StrategyPriority.HIGHEST; + } + + long vmId = vmSnapshot.getVmId(); + boolean memorySnapshot = VMSnapshot.Type.DiskAndMemory.equals(vmSnapshot.getType()); + return canHandle(vmId, null, memorySnapshot); + } + + @Override + public StrategyPriority canHandle(Long vmId, Long rootPoolId, boolean snapshotMemory) { + VirtualMachine vm = userVmDao.findById(vmId); + + String cantHandleLog = String.format("KVM file based storage VM snapshot strategy cannot handle VM snapshot for [%s]", vm); + if (snapshotMemory) { + logger.debug("{} as a snapshot with memory was requested.", cantHandleLog); + return StrategyPriority.CANT_HANDLE; + } + + if (!Hypervisor.HypervisorType.KVM.equals(vm.getHypervisorType())) { + logger.debug("{} as the hypervisor is not KVM.", cantHandleLog); + return StrategyPriority.CANT_HANDLE; + } + + if (CollectionUtils.isNotEmpty(vmSnapshotDao.findByVmAndByType(vmId, VMSnapshot.Type.DiskAndMemory))) { + logger.debug("{} as there is already a VM snapshot with disk and memory.", cantHandleLog); + return StrategyPriority.CANT_HANDLE; + } + + List volumes = volumeDao.findByInstance(vmId); + for (VolumeVO volume : volumes) { + StoragePoolVO storagePoolVO = storagePool.findById(volume.getPoolId()); + if (!supportedStoragePoolTypes.contains(storagePoolVO.getPoolType())) { + logger.debug(String.format("%s as the VM has a volume that is in a storage with unsupported type [%s].", cantHandleLog, storagePoolVO.getPoolType())); + return StrategyPriority.CANT_HANDLE; + } + List snapshots = snapshotDao.listByVolumeIdAndTypeNotInAndStateNotRemoved(volume.getId(), Snapshot.Type.GROUP); + if (CollectionUtils.isNotEmpty(snapshots)) { + logger.debug("{} as VM has a volume with snapshots {}. Volume snapshots and KvmFileBasedStorageVmSnapshotStrategy are not compatible, as restoring volume snapshots will erase VM " + + "snapshots and cause data loss.", cantHandleLog, snapshots); + return StrategyPriority.CANT_HANDLE; + } + } + + BackupOfferingVO backupOffering = backupOfferingDao.findById(vm.getBackupOfferingId()); + if (backupOffering != null) { + logger.debug("{} as the VM has a backup offering. This strategy does not support snapshots on VMs with current backup providers.", cantHandleLog); + return StrategyPriority.CANT_HANDLE; + } + + return StrategyPriority.HIGHEST; + } + + private List deleteSnapshot(VMSnapshotVO vmSnapshotVO, Long hostId) { + List volumeSnapshots = getVolumeSnapshotsAssociatedWithVmSnapshot(vmSnapshotVO); + List volumeSnapshotTOList = volumeSnapshots.stream() + .map(snapshotDataStoreVO -> snapshotDataFactory.getSnapshot(snapshotDataStoreVO.getSnapshotId(), snapshotDataStoreVO.getDataStoreId(), DataStoreRole.Primary).getTO()) + .collect(Collectors.toList()); + + DeleteDiskOnlyVmSnapshotCommand deleteSnapshotCommand = new DeleteDiskOnlyVmSnapshotCommand(volumeSnapshotTOList); + Answer answer = agentMgr.easySend(hostId, deleteSnapshotCommand); + if (answer == null || !answer.getResult()) { + logger.error("Failed to delete VM snapshot [{}] due to {}.", vmSnapshotVO.getUuid(), answer != null ? answer.getDetails() : "Communication failure"); + throw new CloudRuntimeException(String.format("Failed to delete VM snapshot [%s].", vmSnapshotVO.getUuid())); + } + + logger.debug("Updating metadata of VM snapshot [{}].", vmSnapshotVO.getUuid()); + List snapshotVOList = new ArrayList<>(); + for (SnapshotDataStoreVO snapshotDataStoreVO : volumeSnapshots) { + snapshotDataStoreDao.remove(snapshotDataStoreVO.getId()); + snapshotVOList.add(snapshotDao.findById(snapshotDataStoreVO.getSnapshotId())); + } + return snapshotVOList; + } + + private List mergeSnapshots(VMSnapshotVO vmSnapshotVO, VMSnapshotVO childSnapshot, UserVmVO userVm, List volumeObjectTOS, Long hostId) { + logger.debug("Merging VM snapshot [{}] with its child [{}].", vmSnapshotVO.getUuid(), childSnapshot.getUuid()); + + List snapshotGrandChildren = vmSnapshotDao.listByParentAndStateIn(childSnapshot.getId(), VMSnapshot.State.Ready, VMSnapshot.State.Hidden); + + if (VirtualMachine.State.Running.equals(userVm.getState()) && !snapshotGrandChildren.isEmpty()) { + logger.debug("Removing VM snapshots that are part of the VM's [{}] current backing chain from the list of snapshots to be rebased.", userVm.getUuid()); + removeCurrentBackingChainSnapshotFromVmSnapshotList(snapshotGrandChildren, userVm); + } + + List snapshotMergeTreeToList = generateSnapshotMergeTrees(vmSnapshotVO, childSnapshot, snapshotGrandChildren); + + if (childSnapshot.getCurrent() && !VirtualMachine.State.Running.equals(userVm.getState())) { + for (VolumeObjectTO volumeObjectTO : volumeObjectTOS) { + snapshotMergeTreeToList.stream().filter(snapshotTree -> Objects.equals(((SnapshotObjectTO) snapshotTree.getParent()).getVolume().getId(), volumeObjectTO.getId())) + .findFirst() + .orElseThrow(() -> new CloudRuntimeException(String.format("Failed to find volume snapshot for volume [%s].", volumeObjectTO.getUuid()))) + .addGrandChild(volumeObjectTO); + } + } + + MergeDiskOnlyVmSnapshotCommand mergeDiskOnlyVMSnapshotCommand = new MergeDiskOnlyVmSnapshotCommand(snapshotMergeTreeToList, userVm.getState(), userVm.getName()); + Answer answer = agentMgr.easySend(hostId, mergeDiskOnlyVMSnapshotCommand); + if (answer == null || !answer.getResult()) { + throw new CloudRuntimeException(String.format("Failed to merge VM snapshot [%s] due to %s.", vmSnapshotVO.getUuid(), answer != null ? answer.getDetails() : "Communication failure")); + } + + logger.debug("Updating metadata of VM snapshot [{}] and its child [{}].", vmSnapshotVO.getUuid(), childSnapshot.getUuid()); + List snapshotVOList = new ArrayList<>(); + for (SnapshotMergeTreeTO snapshotMergeTreeTO : snapshotMergeTreeToList) { + SnapshotObjectTO childTO = (SnapshotObjectTO) snapshotMergeTreeTO.getChild(); + SnapshotObjectTO parentTO = (SnapshotObjectTO) snapshotMergeTreeTO.getParent(); + + SnapshotDataStoreVO childSnapshotDataStoreVO = snapshotDataStoreDao.findBySnapshotIdInAnyState(childTO.getId(), DataStoreRole.Primary); + childSnapshotDataStoreVO.setInstallPath(parentTO.getPath()); + snapshotDataStoreDao.update(childSnapshotDataStoreVO.getId(), childSnapshotDataStoreVO); + + snapshotDataStoreDao.expungeReferenceBySnapshotIdAndDataStoreRole(parentTO.getId(), childSnapshotDataStoreVO.getDataStoreId(), DataStoreRole.Primary); + snapshotVOList.add(snapshotDao.findById(parentTO.getId())); + } + + childSnapshot.setParent(vmSnapshotVO.getParent()); + vmSnapshotDao.update(childSnapshot.getId(), childSnapshot); + + return snapshotVOList; + } + + private List mergeCurrentDeltaOnSnapshot(VMSnapshotVO vmSnapshotVo, UserVmVO userVmVO, Long hostId, List volumeObjectTOS) { + logger.debug("Merging VM snapshot [{}] with the current volume delta.", vmSnapshotVo.getUuid()); + List snapshotMergeTreeTOList = new ArrayList<>(); + List volumeSnapshots = getVolumeSnapshotsAssociatedWithVmSnapshot(vmSnapshotVo); + + for (VolumeObjectTO volumeObjectTO : volumeObjectTOS) { + SnapshotDataStoreVO volumeParentSnapshot = volumeSnapshots.stream().filter(snapshot -> Objects.equals(snapshot.getVolumeId(), volumeObjectTO.getId())) + .findFirst() + .orElseThrow(() -> new CloudRuntimeException(String.format("Failed to find volume snapshot for volume [%s].", volumeObjectTO.getUuid()))); + DataTO parentSnapshot = snapshotDataFactory.getSnapshot(volumeParentSnapshot.getSnapshotId(), volumeParentSnapshot.getDataStoreId(), DataStoreRole.Primary).getTO(); + snapshotMergeTreeTOList.add(new SnapshotMergeTreeTO(parentSnapshot, volumeObjectTO, new ArrayList<>())); + } + + MergeDiskOnlyVmSnapshotCommand mergeDiskOnlyVMSnapshotCommand = new MergeDiskOnlyVmSnapshotCommand(snapshotMergeTreeTOList, userVmVO.getState(), userVmVO.getName()); + + Answer answer = agentMgr.easySend(hostId, mergeDiskOnlyVMSnapshotCommand); + if (answer == null || !answer.getResult()) { + throw new CloudRuntimeException(String.format("Failed to delete VM snapshot [%s] due to %s.", vmSnapshotVo.getUuid(), answer != null ? answer.getDetails() : "Communication failure")); + } + + logger.debug("Updating metadata of VM snapshot [{}].", vmSnapshotVo.getUuid()); + List snapshotVOList = new ArrayList<>(); + for (SnapshotMergeTreeTO snapshotMergeTreeTO : snapshotMergeTreeTOList) { + VolumeObjectTO volumeObjectTO = (VolumeObjectTO) snapshotMergeTreeTO.getChild(); + SnapshotObjectTO parentTO = (SnapshotObjectTO) snapshotMergeTreeTO.getParent(); + + VolumeVO volumeVO = volumeDao.findById(volumeObjectTO.getId()); + volumeVO.setPath(parentTO.getPath()); + volumeDao.update(volumeVO.getId(), volumeVO); + + snapshotDataStoreDao.expungeReferenceBySnapshotIdAndDataStoreRole(parentTO.getId(), volumeVO.getPoolId(), DataStoreRole.Primary); + snapshotVOList.add(snapshotDao.findById(parentTO.getId())); + } + + vmSnapshotVo.setCurrent(false); + if (vmSnapshotVo.getParent() != null) { + VMSnapshotVO parentSnapshot = vmSnapshotDao.findById(vmSnapshotVo.getParent()); + parentSnapshot.setCurrent(true); + vmSnapshotDao.update(parentSnapshot.getId(), parentSnapshot); + } + + return snapshotVOList; + } + + /** + * Takes a disk-only VM snapshot, exceptions thrown will be caught deeper in the stack and treated there. + * @param vmSnapshot the definition of the VM Snapshot that will be created. + * @param volumeInfoToSnapshotObjectMap Empty map of VolumeInfo to SnapshotObject, will be populated within the method, used for treating the exceptions thrown. + * @return the VM Snapshot created. + * */ + protected VMSnapshot takeVmSnapshotInternal(VMSnapshot vmSnapshot, Map volumeInfoToSnapshotObjectMap) throws NoTransitionException { + UserVm userVm = userVmDao.findById(vmSnapshot.getVmId()); + + logger.info("Starting disk-only VM snapshot process for VM [{}].", userVm.getUuid()); + + Long hostId = vmSnapshotHelper.pickRunningHost(vmSnapshot.getVmId()); + VMSnapshotVO vmSnapshotVO = (VMSnapshotVO) vmSnapshot; + List volumeTOs = vmSnapshotHelper.getVolumeTOList(userVm.getId()); + + transitStateWithoutThrow(vmSnapshot, VMSnapshot.Event.CreateRequested); + + VMSnapshotTO parentSnapshotTo = null; + VMSnapshotVO parentSnapshotVo = vmSnapshotDao.findCurrentSnapshotByVmId(userVm.getId()); + if (parentSnapshotVo != null) { + parentSnapshotTo = vmSnapshotHelper.getSnapshotWithParents(parentSnapshotVo); + vmSnapshotVO.setParent(parentSnapshotTo.getId()); + } + + VMSnapshotOptions options = ((VMSnapshotVO) vmSnapshot).getOptions(); + boolean quiesceVm = false; + if (options != null) { + quiesceVm = options.needQuiesceVM(); + } + + long virtualSize = createVolumeSnapshotMetadataAndCalculateVirtualSize(vmSnapshot, volumeInfoToSnapshotObjectMap, volumeTOs); + + VMSnapshotTO target = new VMSnapshotTO(vmSnapshot.getId(), vmSnapshot.getName(), vmSnapshot.getType(), null, vmSnapshot.getDescription(), false, parentSnapshotTo, quiesceVm); + + CreateDiskOnlyVmSnapshotCommand ccmd = new CreateDiskOnlyVmSnapshotCommand(userVm.getInstanceName(), target, volumeTOs, null, userVm.getState()); + + logger.info("Sending disk-only VM snapshot creation of VM Snapshot [{}] command for host [{}].", vmSnapshot.getUuid(), hostId); + Answer answer = agentMgr.easySend(hostId, ccmd); + + if (answer != null && answer.getResult()) { + CreateDiskOnlyVmSnapshotAnswer createDiskOnlyVMSnapshotAnswer = (CreateDiskOnlyVmSnapshotAnswer) answer; + return processCreateVmSnapshotAnswer(vmSnapshot, volumeInfoToSnapshotObjectMap, createDiskOnlyVMSnapshotAnswer, userVm, vmSnapshotVO, virtualSize, parentSnapshotVo); + } + + logger.error("Disk-only VM snapshot for VM [{}] failed{}.", userVm.getUuid(), answer != null ? " due to" + answer.getDetails() : ""); + throw new CloudRuntimeException(String.format("Disk-only VM snapshot for VM [%s] failed.", userVm.getUuid())); + } + + /** + * Updates the needed metadata of the given VM Snapshot and its associated volume snapshots. + * */ + private VMSnapshot processCreateVmSnapshotAnswer(VMSnapshot vmSnapshot, Map volumeInfoToSnapshotObjectMap, CreateDiskOnlyVmSnapshotAnswer answer, UserVm userVm, VMSnapshotVO vmSnapshotVO, long virtualSize, VMSnapshotVO parentSnapshotVo) throws NoTransitionException { + logger.debug("Processing CreateDiskOnlyVMSnapshotCommand answer for disk-only VM snapshot [{}].", vmSnapshot.getUuid()); + Map> volumeUuidToSnapshotSizeAndNewVolumePathMap = answer.getMapVolumeToSnapshotSizeAndNewVolumePath(); + long vmSnapshotSize = 0; + + for (VolumeInfo volumeInfo : volumeInfoToSnapshotObjectMap.keySet()) { + VolumeVO volumeVO = (VolumeVO) volumeInfo.getVolume(); + Pair snapSizeAndNewVolumePath = volumeUuidToSnapshotSizeAndNewVolumePathMap.get(volumeVO.getUuid()); + + SnapshotObject snapshot = volumeInfoToSnapshotObjectMap.get(volumeInfo); + snapshot.markBackedUp(); + + logger.debug("Updating metadata for volume [{}] and its corresponding snapshot [{}].", volumeVO, snapshot.getSnapshotVO()); + + SnapshotDataStoreVO snapshotDataStoreVO = snapshotDataStoreDao.findBySnapshotId(snapshot.getId()).get(0); + snapshotDataStoreVO.setInstallPath(volumeVO.getPath()); + snapshotDataStoreVO.setPhysicalSize(snapSizeAndNewVolumePath.first()); + snapshotDataStoreVO.setState(ObjectInDataStoreStateMachine.State.Ready); + snapshotDataStoreDao.update(snapshotDataStoreVO.getId(), snapshotDataStoreVO); + + vmSnapshotSize += snapSizeAndNewVolumePath.first(); + + volumeVO.setPath(snapSizeAndNewVolumePath.second()); + volumeDao.update(volumeVO.getId(), volumeVO); + volumeInfo.stateTransit(Volume.Event.OperationSucceeded); + + vmSnapshotDetailsDao.persist(new VMSnapshotDetailsVO(vmSnapshot.getId(), KVM_FILE_BASED_STORAGE_SNAPSHOT, String.valueOf(snapshot.getId()), true)); + + publishUsageEvent(EventTypes.EVENT_VM_SNAPSHOT_CREATE, vmSnapshot, userVm, (VolumeObjectTO) volumeInfo.getTO()); + } + + vmSnapshotVO.setCurrent(true); + vmSnapshotDao.persist(vmSnapshotVO); + + if (parentSnapshotVo != null) { + parentSnapshotVo.setCurrent(false); + vmSnapshotDao.persist(parentSnapshotVo); + } + + vmSnapshotHelper.vmSnapshotStateTransitTo(vmSnapshot, VMSnapshot.Event.OperationSucceeded); + + publishUsageEvent(EventTypes.EVENT_VM_SNAPSHOT_ON_PRIMARY, vmSnapshot, userVm, vmSnapshotSize, virtualSize); + + return vmSnapshot; + } + + private long createVolumeSnapshotMetadataAndCalculateVirtualSize(VMSnapshot vmSnapshot, Map volumeInfoToSnapshotObjectMap, List volumeTOs) throws NoTransitionException { + long virtualSize = 0; + for (VolumeObjectTO volumeObjectTO : volumeTOs) { + VolumeInfo volumeInfo = volumeDataFactory.getVolume(volumeObjectTO.getId()); + volumeInfo.stateTransit(Volume.Event.SnapshotRequested); + virtualSize += volumeInfo.getSize(); + + String snapshotName = String.format("%s_%s", vmSnapshot.getId(), volumeObjectTO.getUuid()); + SnapshotVO snapshot = new SnapshotVO(volumeInfo.getDataCenterId(), volumeInfo.getAccountId(), volumeInfo.getDomainId(), volumeInfo.getId(), + volumeInfo.getDiskOfferingId(), snapshotName, (short) Snapshot.Type.GROUP.ordinal(), Snapshot.Type.GROUP.name(), volumeInfo.getSize(), volumeInfo.getMinIops(), + volumeInfo.getMaxIops(), Hypervisor.HypervisorType.KVM, null); + + logger.debug("Creating snapshot metadata [{}] as part of the disk-only snapshot process for VM [{}].", snapshot, volumeObjectTO.getVmName()); + + snapshot = snapshotDao.persist(snapshot); + + SnapshotInfo snapshotInfo = snapshotDataFactory.getSnapshot(snapshot.getId(), volumeInfo.getDataStore()); + SnapshotObject snapshotOnPrimary = (SnapshotObject) snapshotInfo.getDataStore().create(snapshotInfo); + + snapshotOnPrimary.processEvent(Snapshot.Event.CreateRequested); + snapshotOnPrimary.processEvent(ObjectInDataStoreStateMachine.Event.CreateOnlyRequested); + + volumeInfoToSnapshotObjectMap.put(volumeInfo, snapshotOnPrimary); + } + return virtualSize; + } + + private List generateSnapshotMergeTrees(VMSnapshotVO parent, VMSnapshotVO child, List grandChildren) throws NoSuchElementException { + logger.debug("Generating list of Snapshot Merge Trees for the merge process of VM Snapshot [{}].", parent.getUuid()); + + List snapshotMergeTrees = new ArrayList<>(); + List parentVolumeSnapshots = getVolumeSnapshotsAssociatedWithVmSnapshot(parent); + List childVolumeSnapshots = getVolumeSnapshotsAssociatedWithVmSnapshot(child); + List grandChildrenVolumeSnapshots = new ArrayList<>(); + + for (VMSnapshotVO grandChild : grandChildren) { + grandChildrenVolumeSnapshots.addAll(getVolumeSnapshotsAssociatedWithVmSnapshot(grandChild)); + } + + for (SnapshotDataStoreVO parentSnapshotDataStoreVO : parentVolumeSnapshots) { + DataTO parentTO = snapshotDataFactory.getSnapshot(parentSnapshotDataStoreVO.getSnapshotId(), parentSnapshotDataStoreVO.getDataStoreId(), DataStoreRole.Primary).getTO(); + + DataTO childTO = childVolumeSnapshots.stream() + .filter(childSnapshot -> Objects.equals(parentSnapshotDataStoreVO.getVolumeId(), childSnapshot.getVolumeId())) + .map(snapshotDataStoreVO -> snapshotDataFactory.getSnapshot(snapshotDataStoreVO.getSnapshotId(), snapshotDataStoreVO.getDataStoreId(), DataStoreRole.Primary).getTO()) + .findFirst().orElseThrow(() -> new CloudRuntimeException(String.format("Could not find child snapshot of parent [%s].", parentSnapshotDataStoreVO.getSnapshotId()))); + + List grandChildrenTOList = grandChildrenVolumeSnapshots.stream() + .filter(grandChildSnapshot -> Objects.equals(parentSnapshotDataStoreVO.getVolumeId(), grandChildSnapshot.getVolumeId())) + .map(snapshotDataStoreVO -> snapshotDataFactory.getSnapshot(snapshotDataStoreVO.getSnapshotId(), snapshotDataStoreVO.getDataStoreId(), DataStoreRole.Primary).getTO()) + .collect(Collectors.toList()); + + snapshotMergeTrees.add(new SnapshotMergeTreeTO(parentTO, childTO, grandChildrenTOList)); + } + + logger.debug("Generated the following list of Snapshot Merge Trees for the VM snapshot [{}]: [{}].", parent.getUuid(), snapshotMergeTrees); + return snapshotMergeTrees; + } + + /** + * For a given {@code VMSnapshotVO}, populates the {@code associatedVolumeSnapshots} list with all the volume snapshots that are + * part of the VMSnapshot. + * @param vmSnapshot the VMSnapshotVO that will have its size calculated + * @return the list that will be populated with the volume snapshots associated with the VM snapshot. + * */ + private List getVolumeSnapshotsAssociatedWithVmSnapshot(VMSnapshotVO vmSnapshot) { + List associatedVolumeSnapshots = new ArrayList<>(); + List snapshotDetailList = vmSnapshotDetailsDao.findDetails(vmSnapshot.getId(), KVM_FILE_BASED_STORAGE_SNAPSHOT); + for (VMSnapshotDetailsVO vmSnapshotDetailsVO : snapshotDetailList) { + SnapshotDataStoreVO snapshot = snapshotDataStoreDao.findOneBySnapshotAndDatastoreRole(Long.parseLong(vmSnapshotDetailsVO.getValue()), DataStoreRole.Primary); + if (snapshot == null) { + throw new CloudRuntimeException(String.format("Could not find snapshot for VM snapshot [%s].", vmSnapshot.getUuid())); + } + associatedVolumeSnapshots.add(snapshot); + } + return associatedVolumeSnapshots; + } + + /** + * For a given {@code VMSnapshotVO}, returns the real size of the snapshot. + * @param vmSnapshot the VMSnapshotVO that will have its size calculated + * */ + private long getVMSnapshotRealSize(VMSnapshotVO vmSnapshot) { + long realSize = 0; + List snapshotDetailList = vmSnapshotDetailsDao.findDetails(vmSnapshot.getId(), KVM_FILE_BASED_STORAGE_SNAPSHOT); + for (VMSnapshotDetailsVO vmSnapshotDetailsVO : snapshotDetailList) { + SnapshotDataStoreVO snapshot = snapshotDataStoreDao.findOneBySnapshotAndDatastoreRole(Long.parseLong(vmSnapshotDetailsVO.getValue()), DataStoreRole.Primary); + if (snapshot == null) { + throw new CloudRuntimeException(String.format("Could not find snapshot for VM snapshot [%s].", vmSnapshot.getUuid())); + } + realSize += snapshot.getPhysicalSize(); + } + return realSize; + } + + /** + * Given a list of VM snapshots, will remove any that are part of the current direct backing chain (all the direct ancestors of the current vm snapshot). + * This is done because, when using virDomainBlockCommit}, Libvirt will maintain + * the current backing chain consistent; thus we only need to rebase the snapshots that are not on the current backing chain. + * */ + private void removeCurrentBackingChainSnapshotFromVmSnapshotList(List vmSnapshotList, UserVm userVm) { + VMSnapshotVO currentSnapshotVO = vmSnapshotDao.findCurrentSnapshotByVmId(vmSnapshotList.get(0).getVmId()); + VMSnapshotTO currentSnapshotTO = vmSnapshotHelper.getSnapshotWithParents(currentSnapshotVO); + + List currentBranch = new ArrayList<>(); + currentBranch.add(currentSnapshotTO); + VMSnapshotTO parent = currentSnapshotTO.getParent(); + while (parent != null) { + currentBranch.add(parent); + parent = parent.getParent(); + } + + for (VMSnapshotVO vmSnapshotVO : vmSnapshotList) { + if (currentBranch.stream().anyMatch(currentBranchSnap -> Objects.equals(currentBranchSnap.getId(), vmSnapshotVO.getId()))) { + logger.trace("Removing snapshot [{}] from the list of VM snapshots of VM [{}] being rebased.", vmSnapshotVO.getUuid(), userVm.getUuid()); + vmSnapshotList.remove(vmSnapshotVO); + return; + } + } + } + + private void transitStateWithoutThrow(VMSnapshot vmSnapshot, VMSnapshot.Event event) { + try { + vmSnapshotHelper.vmSnapshotStateTransitTo(vmSnapshot, event); + } catch (NoTransitionException e) { + String msg = String.format("Failed to change VM snapshot [%s] state with event [%s].", vmSnapshot, event.toString()); + logger.error(msg, e); + throw new CloudRuntimeException(msg, e); + } + } +} diff --git a/engine/storage/snapshot/src/main/java/org/apache/cloudstack/storage/vmsnapshot/StorageVMSnapshotStrategy.java b/engine/storage/snapshot/src/main/java/org/apache/cloudstack/storage/vmsnapshot/StorageVMSnapshotStrategy.java index e33edc9ce80..70ac14ec79d 100644 --- a/engine/storage/snapshot/src/main/java/org/apache/cloudstack/storage/vmsnapshot/StorageVMSnapshotStrategy.java +++ b/engine/storage/snapshot/src/main/java/org/apache/cloudstack/storage/vmsnapshot/StorageVMSnapshotStrategy.java @@ -95,8 +95,6 @@ public class StorageVMSnapshotStrategy extends DefaultVMSnapshotStrategy { @Inject VMSnapshotDetailsDao vmSnapshotDetailsDao; - private static final String STORAGE_SNAPSHOT = "kvmStorageSnapshot"; - @Override public boolean configure(String name, Map params) throws ConfigurationException { return super.configure(name, params); @@ -356,12 +354,25 @@ public class StorageVMSnapshotStrategy extends DefaultVMSnapshotStrategy { @Override public StrategyPriority canHandle(Long vmId, Long rootPoolId, boolean snapshotMemory) { - if (SnapshotManager.VmStorageSnapshotKvm.value() && !snapshotMemory) { - UserVmVO vm = userVmDao.findById(vmId); - if (vm.getState() == VirtualMachine.State.Running) { - return StrategyPriority.HYPERVISOR; - } + UserVmVO vm = userVmDao.findById(vmId); + String cantHandleLog = String.format("Storage VM snapshot strategy cannot handle VM snapshot for [%s]", vm); + + if (CollectionUtils.isNotEmpty(vmSnapshotDao.findByVmAndByType(vmId, VMSnapshot.Type.DiskAndMemory))) { + logger.debug("{} as it has VM snapshots with disk and memory.", cantHandleLog); + return StrategyPriority.CANT_HANDLE; } + + if (!VirtualMachine.State.Running.equals(vm.getState())) { + logger.debug("{} as the VM is not running.", cantHandleLog); + return StrategyPriority.CANT_HANDLE; + } + + if (SnapshotManager.VmStorageSnapshotKvm.value() && !snapshotMemory) { + return StrategyPriority.HYPERVISOR; + } + + logger.debug("{} as {}.", () -> cantHandleLog, () -> snapshotMemory ? "A VM snapshot with memory was requested" : + String.format("%s is false", SnapshotManager.VmStorageSnapshotKvm.key())); return StrategyPriority.CANT_HANDLE; } diff --git a/engine/storage/snapshot/src/main/resources/META-INF/cloudstack/storage/spring-engine-storage-snapshot-storage-context.xml b/engine/storage/snapshot/src/main/resources/META-INF/cloudstack/storage/spring-engine-storage-snapshot-storage-context.xml index fc561159c8e..32ac6e9fc8b 100644 --- a/engine/storage/snapshot/src/main/resources/META-INF/cloudstack/storage/spring-engine-storage-snapshot-storage-context.xml +++ b/engine/storage/snapshot/src/main/resources/META-INF/cloudstack/storage/spring-engine-storage-snapshot-storage-context.xml @@ -48,4 +48,7 @@ + + diff --git a/engine/storage/snapshot/src/test/java/org/apache/cloudstack/storage/vmsnapshot/VMSnapshotStrategyTest.java b/engine/storage/snapshot/src/test/java/org/apache/cloudstack/storage/vmsnapshot/VMSnapshotStrategyTest.java index 7bcfd4dda58..829ca5ade39 100644 --- a/engine/storage/snapshot/src/test/java/org/apache/cloudstack/storage/vmsnapshot/VMSnapshotStrategyTest.java +++ b/engine/storage/snapshot/src/test/java/org/apache/cloudstack/storage/vmsnapshot/VMSnapshotStrategyTest.java @@ -25,6 +25,7 @@ import java.util.List; import javax.inject.Inject; +import com.cloud.vm.snapshot.dao.VMSnapshotDetailsDao; import org.apache.cloudstack.engine.subsystem.api.storage.VMSnapshotStrategy; import org.apache.cloudstack.framework.config.dao.ConfigurationDao; import org.apache.cloudstack.storage.datastore.db.PrimaryDataStoreDao; @@ -283,6 +284,11 @@ public class VMSnapshotStrategyTest extends TestCase { return Mockito.mock(VMSnapshotDao.class); } + @Bean + public VMSnapshotDetailsDao vmSnapshotDetailsDao() { + return Mockito.mock(VMSnapshotDetailsDao.class); + } + @Bean public ConfigurationDao configurationDao() { return Mockito.mock(ConfigurationDao.class); diff --git a/extensions/HyperV/hyperv.py b/extensions/HyperV/hyperv.py new file mode 100755 index 00000000000..83109ebb03a --- /dev/null +++ b/extensions/HyperV/hyperv.py @@ -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 = 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() diff --git a/extensions/Proxmox/proxmox.sh b/extensions/Proxmox/proxmox.sh new file mode 100755 index 00000000000..dbfdae1b972 --- /dev/null +++ b/extensions/Proxmox/proxmox.sh @@ -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 diff --git a/framework/db/src/main/java/com/cloud/utils/crypt/EncryptionSecretKeyChanger.java b/framework/db/src/main/java/com/cloud/utils/crypt/EncryptionSecretKeyChanger.java index e96b0a00894..a3e3cce237d 100644 --- a/framework/db/src/main/java/com/cloud/utils/crypt/EncryptionSecretKeyChanger.java +++ b/framework/db/src/main/java/com/cloud/utils/crypt/EncryptionSecretKeyChanger.java @@ -632,7 +632,7 @@ public class EncryptionSecretKeyChanger { private void migrateUserVmDetails(Connection conn) { System.out.println("Begin migrate user vm details"); - migrateDetails(conn, "user_vm_details", PASSWORD); + migrateDetails(conn, "vm_instance_details", PASSWORD); System.out.println("End migrate user vm details"); } diff --git a/framework/db/src/main/java/com/cloud/utils/db/SearchBase.java b/framework/db/src/main/java/com/cloud/utils/db/SearchBase.java index b4c27746799..512941f4ee3 100644 --- a/framework/db/src/main/java/com/cloud/utils/db/SearchBase.java +++ b/framework/db/src/main/java/com/cloud/utils/db/SearchBase.java @@ -479,6 +479,14 @@ public abstract class SearchBase, T, K> { sql.append(" FIND_IN_SET(?, "); } + if (op == Op.LIKE_REPLACE) { + sql.append(" ? LIKE REPLACE ("); + } + + if (op == Op.LIKE_CONCAT) { + sql.append(" ? LIKE CONCAT ("); + } + if (tableAlias == null) { if (joinName != null) { tableAlias = joinName; diff --git a/framework/db/src/main/java/com/cloud/utils/db/SearchCriteria.java b/framework/db/src/main/java/com/cloud/utils/db/SearchCriteria.java index 4a5349b31f4..15807eb26d4 100644 --- a/framework/db/src/main/java/com/cloud/utils/db/SearchCriteria.java +++ b/framework/db/src/main/java/com/cloud/utils/db/SearchCriteria.java @@ -39,7 +39,8 @@ public class SearchCriteria { " NOT BETWEEN ? AND ? ", 2), IN(" IN () ", -1), NOTIN(" NOT IN () ", -1), LIKE(" LIKE ? ", 1), NLIKE(" NOT LIKE ? ", 1), NIN(" NOT IN () ", -1), NULL(" IS NULL ", 0), NNULL( " IS NOT NULL ", - 0), SC(" () ", 1), TEXT(" () ", 1), RP("", 0), AND(" AND ", 0), OR(" OR ", 0), NOT(" NOT ", 0), FIND_IN_SET(" ) ", 1), BINARY_OR(" & ?) > 0", 1); + 0), SC(" () ", 1), TEXT(" () ", 1), RP("", 0), AND(" AND ", 0), OR(" OR ", 0), NOT(" NOT ", 0), FIND_IN_SET(" ) ", 1), BINARY_OR(" & ?) > 0", 1), + LIKE_REPLACE(", ?, ?)", 3), LIKE_CONCAT(", ?)", 2); private final String op; int params; @@ -60,7 +61,8 @@ public class SearchCriteria { } public enum Func { - NATIVE("@", 1), MAX("MAX(@)", 1), MIN("MIN(@)", 1), FIRST("FIRST(@)", 1), LAST("LAST(@)", 1), SUM("SUM(@)", 1), COUNT("COUNT(@)", 1), DISTINCT("DISTINCT(@)", 1), DISTINCT_PAIR("DISTINCT @, @", 2); + NATIVE("@", 1), MAX("MAX(@)", 1), MIN("MIN(@)", 1), FIRST("FIRST(@)", 1), LAST("LAST(@)", 1), SUM("SUM(@)", 1), COUNT("COUNT(@)", 1), DISTINCT("DISTINCT(@)", 1), DISTINCT_PAIR("DISTINCT @, @", 2), + CONCAT("CONCAT(@,@)", 2); private String func; private int count; diff --git a/framework/extensions/pom.xml b/framework/extensions/pom.xml new file mode 100644 index 00000000000..d3b8f81bc71 --- /dev/null +++ b/framework/extensions/pom.xml @@ -0,0 +1,54 @@ + + + 4.0.0 + cloud-framework-extensions + Apache CloudStack Framework - Extensions + + org.apache.cloudstack + cloudstack-framework + 4.21.0.0-SNAPSHOT + ../pom.xml + + + + org.apache.cloudstack + cloud-utils + ${project.version} + + + org.apache.cloudstack + cloud-api + ${project.version} + + + org.apache.cloudstack + cloud-engine-schema + 4.21.0.0-SNAPSHOT + compile + + + org.apache.cloudstack + cloud-engine-components-api + 4.21.0.0-SNAPSHOT + compile + + + diff --git a/framework/extensions/src/main/java/org/apache/cloudstack/framework/extensions/api/AddCustomActionCmd.java b/framework/extensions/src/main/java/org/apache/cloudstack/framework/extensions/api/AddCustomActionCmd.java new file mode 100644 index 00000000000..dcea754430c --- /dev/null +++ b/framework/extensions/src/main/java/org/apache/cloudstack/framework/extensions/api/AddCustomActionCmd.java @@ -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 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¶meters[0].type=BOOLEAN¶meters[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 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 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; + } +} diff --git a/framework/extensions/src/main/java/org/apache/cloudstack/framework/extensions/api/CreateExtensionCmd.java b/framework/extensions/src/main/java/org/apache/cloudstack/framework/extensions/api/CreateExtensionCmd.java new file mode 100644 index 00000000000..5ab54149645 --- /dev/null +++ b/framework/extensions/src/main/java/org/apache/cloudstack/framework/extensions/api/CreateExtensionCmd.java @@ -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 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; + } +} diff --git a/framework/extensions/src/main/java/org/apache/cloudstack/framework/extensions/api/DeleteCustomActionCmd.java b/framework/extensions/src/main/java/org/apache/cloudstack/framework/extensions/api/DeleteCustomActionCmd.java new file mode 100644 index 00000000000..6f2153ad6bc --- /dev/null +++ b/framework/extensions/src/main/java/org/apache/cloudstack/framework/extensions/api/DeleteCustomActionCmd.java @@ -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(); + } +} diff --git a/framework/extensions/src/main/java/org/apache/cloudstack/framework/extensions/api/DeleteExtensionCmd.java b/framework/extensions/src/main/java/org/apache/cloudstack/framework/extensions/api/DeleteExtensionCmd.java new file mode 100644 index 00000000000..cdae48fdb3a --- /dev/null +++ b/framework/extensions/src/main/java/org/apache/cloudstack/framework/extensions/api/DeleteExtensionCmd.java @@ -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(); + } +} diff --git a/framework/extensions/src/main/java/org/apache/cloudstack/framework/extensions/api/ListCustomActionCmd.java b/framework/extensions/src/main/java/org/apache/cloudstack/framework/extensions/api/ListCustomActionCmd.java new file mode 100644 index 00000000000..4f492bd20a6 --- /dev/null +++ b/framework/extensions/src/main/java/org/apache/cloudstack/framework/extensions/api/ListCustomActionCmd.java @@ -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 responses = extensionsManager.listCustomActions(this); + ListResponse response = new ListResponse<>(); + response.setResponses(responses, responses.size()); + response.setResponseName(getCommandName()); + setResponseObject(response); + } +} diff --git a/framework/extensions/src/main/java/org/apache/cloudstack/framework/extensions/api/ListExtensionsCmd.java b/framework/extensions/src/main/java/org/apache/cloudstack/framework/extensions/api/ListExtensionsCmd.java new file mode 100644 index 00000000000..4426f259380 --- /dev/null +++ b/framework/extensions/src/main/java/org/apache/cloudstack/framework/extensions/api/ListExtensionsCmd.java @@ -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 details; + + ///////////////////////////////////////////////////// + /////////////////// Accessors /////////////////////// + ///////////////////////////////////////////////////// + + public String getName() { + return name; + } + + public Long getExtensionId() { + return extensionId; + } + + public EnumSet getDetails() throws InvalidParameterValueException { + if (CollectionUtils.isEmpty(details)) { + return EnumSet.of(ApiConstants.ExtensionDetails.all); + } + try { + Set 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 responses = extensionsManager.listExtensions(this); + + ListResponse response = new ListResponse<>(); + response.setResponses(responses, responses.size()); + response.setResponseName(getCommandName()); + setResponseObject(response); + } +} diff --git a/framework/extensions/src/main/java/org/apache/cloudstack/framework/extensions/api/RegisterExtensionCmd.java b/framework/extensions/src/main/java/org/apache/cloudstack/framework/extensions/api/RegisterExtensionCmd.java new file mode 100644 index 00000000000..e8f71d7ac8c --- /dev/null +++ b/framework/extensions/src/main/java/org/apache/cloudstack/framework/extensions/api/RegisterExtensionCmd.java @@ -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 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(); + } +} diff --git a/framework/extensions/src/main/java/org/apache/cloudstack/framework/extensions/api/RunCustomActionCmd.java b/framework/extensions/src/main/java/org/apache/cloudstack/framework/extensions/api/RunCustomActionCmd.java new file mode 100644 index 00000000000..dea09cf1683 --- /dev/null +++ b/framework/extensions/src/main/java/org/apache/cloudstack/framework/extensions/api/RunCustomActionCmd.java @@ -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 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"; + } +} diff --git a/framework/extensions/src/main/java/org/apache/cloudstack/framework/extensions/api/UnregisterExtensionCmd.java b/framework/extensions/src/main/java/org/apache/cloudstack/framework/extensions/api/UnregisterExtensionCmd.java new file mode 100644 index 00000000000..0edc7a247fd --- /dev/null +++ b/framework/extensions/src/main/java/org/apache/cloudstack/framework/extensions/api/UnregisterExtensionCmd.java @@ -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(); + } +} diff --git a/framework/extensions/src/main/java/org/apache/cloudstack/framework/extensions/api/UpdateCustomActionCmd.java b/framework/extensions/src/main/java/org/apache/cloudstack/framework/extensions/api/UpdateCustomActionCmd.java new file mode 100644 index 00000000000..bb03be00c5d --- /dev/null +++ b/framework/extensions/src/main/java/org/apache/cloudstack/framework/extensions/api/UpdateCustomActionCmd.java @@ -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 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¶meters[0].type=BOOLEAN¶meters[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 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 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(); + } +} diff --git a/framework/extensions/src/main/java/org/apache/cloudstack/framework/extensions/api/UpdateExtensionCmd.java b/framework/extensions/src/main/java/org/apache/cloudstack/framework/extensions/api/UpdateExtensionCmd.java new file mode 100644 index 00000000000..713e7550a1e --- /dev/null +++ b/framework/extensions/src/main/java/org/apache/cloudstack/framework/extensions/api/UpdateExtensionCmd.java @@ -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 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(); + } +} diff --git a/framework/extensions/src/main/java/org/apache/cloudstack/framework/extensions/command/CleanupExtensionFilesCommand.java b/framework/extensions/src/main/java/org/apache/cloudstack/framework/extensions/command/CleanupExtensionFilesCommand.java new file mode 100644 index 00000000000..ba542d52e85 --- /dev/null +++ b/framework/extensions/src/main/java/org/apache/cloudstack/framework/extensions/command/CleanupExtensionFilesCommand.java @@ -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); + } +} diff --git a/framework/extensions/src/main/java/org/apache/cloudstack/framework/extensions/command/ExtensionBaseCommand.java b/framework/extensions/src/main/java/org/apache/cloudstack/framework/extensions/command/ExtensionBaseCommand.java new file mode 100644 index 00000000000..ead3c2e4012 --- /dev/null +++ b/framework/extensions/src/main/java/org/apache/cloudstack/framework/extensions/command/ExtensionBaseCommand.java @@ -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; + } +} diff --git a/framework/extensions/src/main/java/org/apache/cloudstack/framework/extensions/command/ExtensionRoutingUpdateCommand.java b/framework/extensions/src/main/java/org/apache/cloudstack/framework/extensions/command/ExtensionRoutingUpdateCommand.java new file mode 100644 index 00000000000..3e041112435 --- /dev/null +++ b/framework/extensions/src/main/java/org/apache/cloudstack/framework/extensions/command/ExtensionRoutingUpdateCommand.java @@ -0,0 +1,34 @@ +// Licensed to the Apache Software Foundation (ASF) under one +// or more contributor license agreements. See the NOTICE file +// distributed with this work for additional information +// regarding copyright ownership. The ASF licenses this file +// to you under the Apache License, Version 2.0 (the +// "License"); you may not use this file except in compliance +// with the License. You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, +// software distributed under the License is distributed on an +// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +// KIND, either express or implied. See the License for the +// specific language governing permissions and limitations +// under the License. + +package org.apache.cloudstack.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; + } +} diff --git a/framework/extensions/src/main/java/org/apache/cloudstack/framework/extensions/command/ExtensionServerActionBaseCommand.java b/framework/extensions/src/main/java/org/apache/cloudstack/framework/extensions/command/ExtensionServerActionBaseCommand.java new file mode 100644 index 00000000000..870dc8e2b7e --- /dev/null +++ b/framework/extensions/src/main/java/org/apache/cloudstack/framework/extensions/command/ExtensionServerActionBaseCommand.java @@ -0,0 +1,33 @@ +// Licensed to the Apache Software Foundation (ASF) under one +// or more contributor license agreements. See the NOTICE file +// distributed with this work for additional information +// regarding copyright ownership. The ASF licenses this file +// to you under the Apache License, Version 2.0 (the +// "License"); you may not use this file except in compliance +// with the License. You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, +// software distributed under the License is distributed on an +// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +// KIND, either express or implied. See the License for the +// specific language governing permissions and limitations +// under the License. + +package org.apache.cloudstack.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; + } +} diff --git a/framework/extensions/src/main/java/org/apache/cloudstack/framework/extensions/command/GetExtensionPathChecksumCommand.java b/framework/extensions/src/main/java/org/apache/cloudstack/framework/extensions/command/GetExtensionPathChecksumCommand.java new file mode 100644 index 00000000000..13f503d67ba --- /dev/null +++ b/framework/extensions/src/main/java/org/apache/cloudstack/framework/extensions/command/GetExtensionPathChecksumCommand.java @@ -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); + } +} diff --git a/framework/extensions/src/main/java/org/apache/cloudstack/framework/extensions/command/PrepareExtensionPathCommand.java b/framework/extensions/src/main/java/org/apache/cloudstack/framework/extensions/command/PrepareExtensionPathCommand.java new file mode 100644 index 00000000000..4c8b920b2f3 --- /dev/null +++ b/framework/extensions/src/main/java/org/apache/cloudstack/framework/extensions/command/PrepareExtensionPathCommand.java @@ -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 PrepareExtensionPathCommand extends ExtensionServerActionBaseCommand { + + public PrepareExtensionPathCommand(long msId, Extension extension) { + super(msId, extension); + } +} diff --git a/framework/extensions/src/main/java/org/apache/cloudstack/framework/extensions/dao/ExtensionCustomActionDao.java b/framework/extensions/src/main/java/org/apache/cloudstack/framework/extensions/dao/ExtensionCustomActionDao.java new file mode 100644 index 00000000000..6db0a02d976 --- /dev/null +++ b/framework/extensions/src/main/java/org/apache/cloudstack/framework/extensions/dao/ExtensionCustomActionDao.java @@ -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.dao; + +import java.util.List; + +import com.cloud.utils.db.GenericDao; +import org.apache.cloudstack.framework.extensions.vo.ExtensionCustomActionVO; + +public interface ExtensionCustomActionDao extends GenericDao { + ExtensionCustomActionVO findByNameAndExtensionId(long extensionId, String name); + List listIdsByExtensionId(long extensionId); +} diff --git a/framework/extensions/src/main/java/org/apache/cloudstack/framework/extensions/dao/ExtensionCustomActionDaoImpl.java b/framework/extensions/src/main/java/org/apache/cloudstack/framework/extensions/dao/ExtensionCustomActionDaoImpl.java new file mode 100644 index 00000000000..cd7731d2051 --- /dev/null +++ b/framework/extensions/src/main/java/org/apache/cloudstack/framework/extensions/dao/ExtensionCustomActionDaoImpl.java @@ -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 org.apache.cloudstack.framework.extensions.dao; + +import java.util.List; + +import org.apache.cloudstack.framework.extensions.vo.ExtensionCustomActionVO; + +import com.cloud.utils.db.GenericDaoBase; +import com.cloud.utils.db.GenericSearchBuilder; +import com.cloud.utils.db.SearchBuilder; +import com.cloud.utils.db.SearchCriteria; + +public class ExtensionCustomActionDaoImpl extends GenericDaoBase implements ExtensionCustomActionDao { + + private final SearchBuilder AllFieldSearch; + + public ExtensionCustomActionDaoImpl() { + AllFieldSearch = createSearchBuilder(); + AllFieldSearch.and("name", AllFieldSearch.entity().getName(), SearchCriteria.Op.EQ); + AllFieldSearch.and("extensionId", AllFieldSearch.entity().getExtensionId(), SearchCriteria.Op.EQ); + AllFieldSearch.done(); + } + + @Override + public ExtensionCustomActionVO findByNameAndExtensionId(long extensionId, String name) { + SearchCriteria sc = AllFieldSearch.create(); + sc.setParameters("extensionId", extensionId); + sc.setParameters("name", name); + + return findOneBy(sc); + } + + @Override + public List listIdsByExtensionId(long extensionId) { + GenericSearchBuilder sb = createSearchBuilder(Long.class); + sb.selectFields(sb.entity().getId()); + sb.and("extensionId", sb.entity().getExtensionId(), SearchCriteria.Op.EQ); + sb.done(); + SearchCriteria sc = sb.create(); + sc.setParameters("extensionId", extensionId); + return customSearch(sc, null); + } +} diff --git a/framework/extensions/src/main/java/org/apache/cloudstack/framework/extensions/dao/ExtensionCustomActionDetailsDao.java b/framework/extensions/src/main/java/org/apache/cloudstack/framework/extensions/dao/ExtensionCustomActionDetailsDao.java new file mode 100644 index 00000000000..a34eb0082d1 --- /dev/null +++ b/framework/extensions/src/main/java/org/apache/cloudstack/framework/extensions/dao/ExtensionCustomActionDetailsDao.java @@ -0,0 +1,26 @@ +// Licensed to the Apache Software Foundation (ASF) under one +// or more contributor license agreements. See the NOTICE file +// distributed with this work for additional information +// regarding copyright ownership. The ASF licenses this file +// to you under the Apache License, Version 2.0 (the +// "License"); you may not use this file except in compliance +// with the License. You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, +// software distributed under the License is distributed on an +// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +// KIND, either express or implied. See the License for the +// specific language governing permissions and limitations +// under the License. + +package org.apache.cloudstack.framework.extensions.dao; + + +import com.cloud.utils.db.GenericDao; +import org.apache.cloudstack.framework.extensions.vo.ExtensionCustomActionDetailsVO; +import org.apache.cloudstack.resourcedetail.ResourceDetailsDao; + +public interface ExtensionCustomActionDetailsDao extends GenericDao, ResourceDetailsDao { +} diff --git a/framework/extensions/src/main/java/org/apache/cloudstack/framework/extensions/dao/ExtensionCustomActionDetailsDaoImpl.java b/framework/extensions/src/main/java/org/apache/cloudstack/framework/extensions/dao/ExtensionCustomActionDetailsDaoImpl.java new file mode 100644 index 00000000000..80add008341 --- /dev/null +++ b/framework/extensions/src/main/java/org/apache/cloudstack/framework/extensions/dao/ExtensionCustomActionDetailsDaoImpl.java @@ -0,0 +1,28 @@ +// 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.dao; + +import org.apache.cloudstack.framework.extensions.vo.ExtensionCustomActionDetailsVO; +import org.apache.cloudstack.resourcedetail.ResourceDetailsDaoBase; + +public class ExtensionCustomActionDetailsDaoImpl extends ResourceDetailsDaoBase implements ExtensionCustomActionDetailsDao { + @Override + public void addDetail(long resourceId, String key, String value, boolean display) { + super.addDetail(new ExtensionCustomActionDetailsVO(resourceId, key, value, display)); + } +} diff --git a/framework/extensions/src/main/java/org/apache/cloudstack/framework/extensions/dao/ExtensionDao.java b/framework/extensions/src/main/java/org/apache/cloudstack/framework/extensions/dao/ExtensionDao.java new file mode 100644 index 00000000000..3355457ed25 --- /dev/null +++ b/framework/extensions/src/main/java/org/apache/cloudstack/framework/extensions/dao/ExtensionDao.java @@ -0,0 +1,26 @@ +// Licensed to the Apache Software Foundation (ASF) under one +// or more contributor license agreements. See the NOTICE file +// distributed with this work for additional information +// regarding copyright ownership. The ASF licenses this file +// to you under the Apache License, Version 2.0 (the +// "License"); you may not use this file except in compliance +// with the License. You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, +// software distributed under the License is distributed on an +// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +// KIND, either express or implied. See the License for the +// specific language governing permissions and limitations +// under the License. +package org.apache.cloudstack.framework.extensions.dao; + +import org.apache.cloudstack.framework.extensions.vo.ExtensionVO; + +import com.cloud.utils.db.GenericDao; + +public interface ExtensionDao extends GenericDao { + + ExtensionVO findByName(String name); +} diff --git a/framework/extensions/src/main/java/org/apache/cloudstack/framework/extensions/dao/ExtensionDaoImpl.java b/framework/extensions/src/main/java/org/apache/cloudstack/framework/extensions/dao/ExtensionDaoImpl.java new file mode 100644 index 00000000000..8e17199de6c --- /dev/null +++ b/framework/extensions/src/main/java/org/apache/cloudstack/framework/extensions/dao/ExtensionDaoImpl.java @@ -0,0 +1,45 @@ +// 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.dao; + +import org.apache.cloudstack.framework.extensions.vo.ExtensionVO; + +import com.cloud.utils.db.GenericDaoBase; +import com.cloud.utils.db.SearchBuilder; +import com.cloud.utils.db.SearchCriteria; + +public class ExtensionDaoImpl extends GenericDaoBase implements ExtensionDao { + + private final SearchBuilder AllFieldSearch; + + public ExtensionDaoImpl() { + AllFieldSearch = createSearchBuilder(); + AllFieldSearch.and("name", AllFieldSearch.entity().getName(), SearchCriteria.Op.EQ); + AllFieldSearch.and("type", AllFieldSearch.entity().getType(), SearchCriteria.Op.EQ); + AllFieldSearch.and("state", AllFieldSearch.entity().getState(), SearchCriteria.Op.EQ); + AllFieldSearch.done(); + } + + @Override + public ExtensionVO findByName(String name) { + SearchCriteria sc = AllFieldSearch.create(); + sc.setParameters("name", name); + + return findOneBy(sc); + } +} diff --git a/framework/extensions/src/main/java/org/apache/cloudstack/framework/extensions/dao/ExtensionDetailsDao.java b/framework/extensions/src/main/java/org/apache/cloudstack/framework/extensions/dao/ExtensionDetailsDao.java new file mode 100644 index 00000000000..a23a4eb7442 --- /dev/null +++ b/framework/extensions/src/main/java/org/apache/cloudstack/framework/extensions/dao/ExtensionDetailsDao.java @@ -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.framework.extensions.dao; + +import com.cloud.utils.db.GenericDao; +import org.apache.cloudstack.framework.extensions.vo.ExtensionDetailsVO; +import org.apache.cloudstack.resourcedetail.ResourceDetailsDao; + +public interface ExtensionDetailsDao extends GenericDao, ResourceDetailsDao { +} diff --git a/framework/extensions/src/main/java/org/apache/cloudstack/framework/extensions/dao/ExtensionDetailsDaoImpl.java b/framework/extensions/src/main/java/org/apache/cloudstack/framework/extensions/dao/ExtensionDetailsDaoImpl.java new file mode 100644 index 00000000000..1989db49c24 --- /dev/null +++ b/framework/extensions/src/main/java/org/apache/cloudstack/framework/extensions/dao/ExtensionDetailsDaoImpl.java @@ -0,0 +1,28 @@ +// 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.dao; + +import org.apache.cloudstack.framework.extensions.vo.ExtensionDetailsVO; +import org.apache.cloudstack.resourcedetail.ResourceDetailsDaoBase; + +public class ExtensionDetailsDaoImpl extends ResourceDetailsDaoBase implements ExtensionDetailsDao { + @Override + public void addDetail(long resourceId, String key, String value, boolean display) { + super.addDetail(new ExtensionDetailsVO(resourceId, key, value, display)); + } +} diff --git a/framework/extensions/src/main/java/org/apache/cloudstack/framework/extensions/dao/ExtensionResourceMapDao.java b/framework/extensions/src/main/java/org/apache/cloudstack/framework/extensions/dao/ExtensionResourceMapDao.java new file mode 100644 index 00000000000..930ef867553 --- /dev/null +++ b/framework/extensions/src/main/java/org/apache/cloudstack/framework/extensions/dao/ExtensionResourceMapDao.java @@ -0,0 +1,32 @@ +// Licensed to the Apache Software Foundation (ASF) under one +// or more contributor license agreements. See the NOTICE file +// distributed with this work for additional information +// regarding copyright ownership. The ASF licenses this file +// to you under the Apache License, Version 2.0 (the +// "License"); you may not use this file except in compliance +// with the License. You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, +// software distributed under the License is distributed on an +// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +// KIND, either express or implied. See the License for the +// specific language governing permissions and limitations +// under the License. + +package org.apache.cloudstack.framework.extensions.dao; + +import org.apache.cloudstack.extension.ExtensionResourceMap; +import com.cloud.utils.db.GenericDao; +import org.apache.cloudstack.framework.extensions.vo.ExtensionResourceMapVO; + +import java.util.List; + +public interface ExtensionResourceMapDao extends GenericDao { + List listByExtensionId(long extensionId); + + ExtensionResourceMapVO findByResourceIdAndType(long resourceId, ExtensionResourceMap.ResourceType resourceType); + + List listResourceIdsByExtensionIdAndType(long extensionId,ExtensionResourceMap.ResourceType resourceType); +} diff --git a/framework/extensions/src/main/java/org/apache/cloudstack/framework/extensions/dao/ExtensionResourceMapDaoImpl.java b/framework/extensions/src/main/java/org/apache/cloudstack/framework/extensions/dao/ExtensionResourceMapDaoImpl.java new file mode 100644 index 00000000000..6f19ef8b8b6 --- /dev/null +++ b/framework/extensions/src/main/java/org/apache/cloudstack/framework/extensions/dao/ExtensionResourceMapDaoImpl.java @@ -0,0 +1,70 @@ +// 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.dao; + +import org.apache.cloudstack.extension.ExtensionResourceMap; +import com.cloud.utils.db.GenericDaoBase; +import com.cloud.utils.db.GenericSearchBuilder; +import com.cloud.utils.db.SearchBuilder; +import com.cloud.utils.db.SearchCriteria; +import org.apache.cloudstack.framework.extensions.vo.ExtensionResourceMapVO; + +import java.util.List; + +public class ExtensionResourceMapDaoImpl extends GenericDaoBase implements ExtensionResourceMapDao { + private final SearchBuilder genericSearch; + + public ExtensionResourceMapDaoImpl() { + super(); + + genericSearch = createSearchBuilder(); + genericSearch.and("extensionId", genericSearch.entity().getExtensionId(), SearchCriteria.Op.EQ); + genericSearch.and("resourceId", genericSearch.entity().getResourceId(), SearchCriteria.Op.EQ); + genericSearch.and("resourceType", genericSearch.entity().getResourceType(), SearchCriteria.Op.EQ); + genericSearch.done(); + } + + @Override + public List listByExtensionId(long extensionId) { + SearchCriteria sc = genericSearch.create(); + sc.setParameters("extensionId", extensionId); + return listBy(sc); + } + + @Override + public ExtensionResourceMapVO findByResourceIdAndType(long resourceId, + ExtensionResourceMap.ResourceType resourceType) { + SearchCriteria sc = genericSearch.create(); + sc.setParameters("resourceId", resourceId); + sc.setParameters("resourceType", resourceType); + return findOneBy(sc); + } + + @Override + public List listResourceIdsByExtensionIdAndType(long extensionId, ExtensionResourceMap.ResourceType resourceType) { + GenericSearchBuilder sb = createSearchBuilder(Long.class); + sb.selectFields(sb.entity().getResourceId()); + sb.and("extensionId", sb.entity().getExtensionId(), SearchCriteria.Op.EQ); + sb.and("resourceType", sb.entity().getResourceType(), SearchCriteria.Op.EQ); + sb.done(); + SearchCriteria sc = sb.create(); + sc.setParameters("extensionId", extensionId); + sc.setParameters("resourceType", resourceType); + return customSearch(sc, null); + } +} diff --git a/framework/extensions/src/main/java/org/apache/cloudstack/framework/extensions/dao/ExtensionResourceMapDetailsDao.java b/framework/extensions/src/main/java/org/apache/cloudstack/framework/extensions/dao/ExtensionResourceMapDetailsDao.java new file mode 100644 index 00000000000..11d445b2242 --- /dev/null +++ b/framework/extensions/src/main/java/org/apache/cloudstack/framework/extensions/dao/ExtensionResourceMapDetailsDao.java @@ -0,0 +1,26 @@ +// Licensed to the Apache Software Foundation (ASF) under one +// or more contributor license agreements. See the NOTICE file +// distributed with this work for additional information +// regarding copyright ownership. The ASF licenses this file +// to you under the Apache License, Version 2.0 (the +// "License"); you may not use this file except in compliance +// with the License. You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, +// software distributed under the License is distributed on an +// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +// KIND, either express or implied. See the License for the +// specific language governing permissions and limitations +// under the License. + +package org.apache.cloudstack.framework.extensions.dao; + + +import com.cloud.utils.db.GenericDao; +import org.apache.cloudstack.framework.extensions.vo.ExtensionResourceMapDetailsVO; +import org.apache.cloudstack.resourcedetail.ResourceDetailsDao; + +public interface ExtensionResourceMapDetailsDao extends GenericDao, ResourceDetailsDao { +} diff --git a/framework/extensions/src/main/java/org/apache/cloudstack/framework/extensions/dao/ExtensionResourceMapDetailsDaoImpl.java b/framework/extensions/src/main/java/org/apache/cloudstack/framework/extensions/dao/ExtensionResourceMapDetailsDaoImpl.java new file mode 100644 index 00000000000..cff01495054 --- /dev/null +++ b/framework/extensions/src/main/java/org/apache/cloudstack/framework/extensions/dao/ExtensionResourceMapDetailsDaoImpl.java @@ -0,0 +1,28 @@ +// 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.dao; + +import org.apache.cloudstack.framework.extensions.vo.ExtensionResourceMapDetailsVO; +import org.apache.cloudstack.resourcedetail.ResourceDetailsDaoBase; + +public class ExtensionResourceMapDetailsDaoImpl extends ResourceDetailsDaoBase implements ExtensionResourceMapDetailsDao { + @Override + public void addDetail(long resourceId, String key, String value, boolean display) { + super.addDetail(new ExtensionResourceMapDetailsVO(resourceId, key, value, display)); + } +} diff --git a/framework/extensions/src/main/java/org/apache/cloudstack/framework/extensions/manager/ExtensionsManager.java b/framework/extensions/src/main/java/org/apache/cloudstack/framework/extensions/manager/ExtensionsManager.java new file mode 100644 index 00000000000..82174872e87 --- /dev/null +++ b/framework/extensions/src/main/java/org/apache/cloudstack/framework/extensions/manager/ExtensionsManager.java @@ -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.manager; + + +import java.util.EnumSet; +import java.util.List; +import java.util.Map; + +import org.apache.cloudstack.api.ApiConstants; +import org.apache.cloudstack.api.response.ExtensionCustomActionResponse; +import org.apache.cloudstack.api.response.ExtensionResponse; +import org.apache.cloudstack.extension.CustomActionResultResponse; +import org.apache.cloudstack.extension.Extension; +import org.apache.cloudstack.extension.ExtensionCustomAction; +import org.apache.cloudstack.extension.ExtensionResourceMap; +import org.apache.cloudstack.framework.extensions.api.AddCustomActionCmd; +import org.apache.cloudstack.framework.extensions.api.CreateExtensionCmd; +import org.apache.cloudstack.framework.extensions.api.DeleteCustomActionCmd; +import org.apache.cloudstack.framework.extensions.api.DeleteExtensionCmd; +import org.apache.cloudstack.framework.extensions.api.ListCustomActionCmd; +import org.apache.cloudstack.framework.extensions.api.ListExtensionsCmd; +import org.apache.cloudstack.framework.extensions.api.RegisterExtensionCmd; +import org.apache.cloudstack.framework.extensions.api.RunCustomActionCmd; +import org.apache.cloudstack.framework.extensions.api.UnregisterExtensionCmd; +import org.apache.cloudstack.framework.extensions.api.UpdateCustomActionCmd; +import org.apache.cloudstack.framework.extensions.api.UpdateExtensionCmd; +import org.apache.cloudstack.framework.extensions.command.ExtensionServerActionBaseCommand; + +import com.cloud.host.Host; +import com.cloud.org.Cluster; +import com.cloud.utils.Pair; +import com.cloud.utils.component.Manager; + +public interface ExtensionsManager extends Manager { + + String getExtensionsPath(); + + Extension createExtension(CreateExtensionCmd cmd); + + boolean prepareExtensionPathAcrossServers(Extension extension); + + List listExtensions(ListExtensionsCmd cmd); + + boolean deleteExtension(DeleteExtensionCmd cmd); + + Extension unregisterExtensionWithResource(UnregisterExtensionCmd cmd); + + Extension updateExtension(UpdateExtensionCmd cmd); + + Extension registerExtensionWithResource(RegisterExtensionCmd cmd); + + ExtensionResponse createExtensionResponse(Extension extension, EnumSet viewDetails); + + ExtensionResourceMap registerExtensionWithCluster(Cluster cluster, Extension extension, Map externalDetails); + + void unregisterExtensionWithCluster(Cluster cluster, Long extensionId); + + CustomActionResultResponse runCustomAction(RunCustomActionCmd cmd); + + ExtensionCustomAction addCustomAction(AddCustomActionCmd cmd); + + boolean deleteCustomAction(DeleteCustomActionCmd cmd); + + List listCustomActions(ListCustomActionCmd cmd); + + ExtensionCustomAction updateCustomAction(UpdateCustomActionCmd cmd); + + ExtensionCustomActionResponse createCustomActionResponse(ExtensionCustomAction customAction); + + Map> getExternalAccessDetails(Host host, Map vmDetails); + + String handleExtensionServerCommands(ExtensionServerActionBaseCommand cmd); + + Pair extensionResourceMapDetailsNeedUpdate(final long resourceId, + final ExtensionResourceMap.ResourceType resourceType, final Map details); + + void updateExtensionResourceMapDetails(final long extensionResourceMapId, final Map details); +} diff --git a/framework/extensions/src/main/java/org/apache/cloudstack/framework/extensions/manager/ExtensionsManagerImpl.java b/framework/extensions/src/main/java/org/apache/cloudstack/framework/extensions/manager/ExtensionsManagerImpl.java new file mode 100644 index 00000000000..5abf0f424a7 --- /dev/null +++ b/framework/extensions/src/main/java/org/apache/cloudstack/framework/extensions/manager/ExtensionsManagerImpl.java @@ -0,0 +1,1632 @@ +// +// 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.manager; + +import java.io.File; +import java.lang.reflect.InvocationTargetException; +import java.lang.reflect.Method; +import java.nio.file.Path; +import java.nio.file.Paths; +import java.security.InvalidParameterException; +import java.util.ArrayList; +import java.util.Collection; +import java.util.Collections; +import java.util.Comparator; +import java.util.EnumSet; +import java.util.HashMap; +import java.util.HashSet; +import java.util.List; +import java.util.Map; +import java.util.Optional; +import java.util.Set; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.ExecutionException; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; +import java.util.concurrent.Future; +import java.util.concurrent.ScheduledExecutorService; +import java.util.concurrent.TimeUnit; +import java.util.stream.Collectors; + +import javax.inject.Inject; +import javax.naming.ConfigurationException; + +import org.apache.cloudstack.acl.Role; +import org.apache.cloudstack.acl.RoleService; +import org.apache.cloudstack.acl.RoleType; +import org.apache.cloudstack.api.ApiConstants; +import org.apache.cloudstack.api.response.ExtensionCustomActionParameterResponse; +import org.apache.cloudstack.api.response.ExtensionCustomActionResponse; +import org.apache.cloudstack.api.response.ExtensionResourceResponse; +import org.apache.cloudstack.api.response.ExtensionResponse; +import org.apache.cloudstack.context.CallContext; +import org.apache.cloudstack.extension.CustomActionResultResponse; +import org.apache.cloudstack.extension.Extension; +import org.apache.cloudstack.extension.ExtensionCustomAction; +import org.apache.cloudstack.extension.ExtensionHelper; +import org.apache.cloudstack.extension.ExtensionResourceMap; +import org.apache.cloudstack.framework.config.ConfigKey; +import org.apache.cloudstack.framework.config.Configurable; +import org.apache.cloudstack.framework.extensions.api.AddCustomActionCmd; +import org.apache.cloudstack.framework.extensions.api.CreateExtensionCmd; +import org.apache.cloudstack.framework.extensions.api.DeleteCustomActionCmd; +import org.apache.cloudstack.framework.extensions.api.DeleteExtensionCmd; +import org.apache.cloudstack.framework.extensions.api.ListCustomActionCmd; +import org.apache.cloudstack.framework.extensions.api.ListExtensionsCmd; +import org.apache.cloudstack.framework.extensions.api.RegisterExtensionCmd; +import org.apache.cloudstack.framework.extensions.api.RunCustomActionCmd; +import org.apache.cloudstack.framework.extensions.api.UnregisterExtensionCmd; +import org.apache.cloudstack.framework.extensions.api.UpdateCustomActionCmd; +import org.apache.cloudstack.framework.extensions.api.UpdateExtensionCmd; +import org.apache.cloudstack.framework.extensions.command.CleanupExtensionFilesCommand; +import org.apache.cloudstack.framework.extensions.command.ExtensionRoutingUpdateCommand; +import org.apache.cloudstack.framework.extensions.command.ExtensionServerActionBaseCommand; +import org.apache.cloudstack.framework.extensions.command.GetExtensionPathChecksumCommand; +import org.apache.cloudstack.framework.extensions.command.PrepareExtensionPathCommand; +import org.apache.cloudstack.framework.extensions.dao.ExtensionCustomActionDao; +import org.apache.cloudstack.framework.extensions.dao.ExtensionCustomActionDetailsDao; +import org.apache.cloudstack.framework.extensions.dao.ExtensionDao; +import org.apache.cloudstack.framework.extensions.dao.ExtensionDetailsDao; +import org.apache.cloudstack.framework.extensions.dao.ExtensionResourceMapDao; +import org.apache.cloudstack.framework.extensions.dao.ExtensionResourceMapDetailsDao; +import org.apache.cloudstack.framework.extensions.vo.ExtensionCustomActionDetailsVO; +import org.apache.cloudstack.framework.extensions.vo.ExtensionCustomActionVO; +import org.apache.cloudstack.framework.extensions.vo.ExtensionDetailsVO; +import org.apache.cloudstack.framework.extensions.vo.ExtensionResourceMapDetailsVO; +import org.apache.cloudstack.framework.extensions.vo.ExtensionResourceMapVO; +import org.apache.cloudstack.framework.extensions.vo.ExtensionVO; +import org.apache.cloudstack.managed.context.ManagedContextRunnable; +import org.apache.cloudstack.management.ManagementServerHost; +import org.apache.cloudstack.utils.identity.ManagementServerNode; +import org.apache.commons.collections.CollectionUtils; +import org.apache.commons.collections.MapUtils; +import org.apache.commons.lang3.EnumUtils; +import org.apache.commons.lang3.ObjectUtils; +import org.apache.commons.lang3.StringUtils; + +import com.cloud.agent.AgentManager; +import com.cloud.agent.api.Answer; +import com.cloud.agent.api.Command; +import com.cloud.agent.api.RunCustomActionAnswer; +import com.cloud.agent.api.RunCustomActionCommand; +import com.cloud.alert.AlertManager; +import com.cloud.cluster.ClusterManager; +import com.cloud.cluster.ManagementServerHostVO; +import com.cloud.cluster.dao.ManagementServerHostDao; +import com.cloud.dc.ClusterVO; +import com.cloud.dc.dao.ClusterDao; +import com.cloud.event.ActionEvent; +import com.cloud.event.EventTypes; +import com.cloud.exception.AgentUnavailableException; +import com.cloud.exception.InvalidParameterValueException; +import com.cloud.exception.OperationTimedoutException; +import com.cloud.host.Host; +import com.cloud.host.HostVO; +import com.cloud.host.dao.HostDao; +import com.cloud.host.dao.HostDetailsDao; +import com.cloud.hypervisor.ExternalProvisioner; +import com.cloud.hypervisor.Hypervisor; +import com.cloud.org.Cluster; +import com.cloud.serializer.GsonHelper; +import com.cloud.storage.dao.VMTemplateDao; +import com.cloud.user.Account; +import com.cloud.utils.Pair; +import com.cloud.utils.component.ManagerBase; +import com.cloud.utils.component.PluggableService; +import com.cloud.utils.concurrency.NamedThreadFactory; +import com.cloud.utils.db.EntityManager; +import com.cloud.utils.db.Filter; +import com.cloud.utils.db.GlobalLock; +import com.cloud.utils.db.SearchBuilder; +import com.cloud.utils.db.SearchCriteria; +import com.cloud.utils.db.Transaction; +import com.cloud.utils.db.TransactionCallbackWithException; +import com.cloud.utils.exception.CloudRuntimeException; +import com.cloud.vm.VirtualMachine; +import com.cloud.vm.VirtualMachineManager; +import com.cloud.vm.VmDetailConstants; +import com.cloud.vm.dao.VMInstanceDao; + +public class ExtensionsManagerImpl extends ManagerBase implements ExtensionsManager, ExtensionHelper, PluggableService, Configurable { + + ConfigKey PathStateCheckInterval = new ConfigKey<>("Advanced", Integer.class, + "extension.path.state.check.interval", "300", + "Interval (in seconds) for checking entry-point state of extensions", + false, ConfigKey.Scope.Global); + + @Inject + ExtensionDao extensionDao; + + @Inject + ExtensionDetailsDao extensionDetailsDao; + + @Inject + ExtensionResourceMapDao extensionResourceMapDao; + + @Inject + ExtensionResourceMapDetailsDao extensionResourceMapDetailsDao; + + @Inject + ClusterDao clusterDao; + + @Inject + AgentManager agentMgr; + + @Inject + HostDao hostDao; + + @Inject + HostDetailsDao hostDetailsDao; + + @Inject + ExternalProvisioner externalProvisioner; + + @Inject + ExtensionCustomActionDao extensionCustomActionDao; + + @Inject + ExtensionCustomActionDetailsDao extensionCustomActionDetailsDao; + + @Inject + VMInstanceDao vmInstanceDao; + + @Inject + VirtualMachineManager virtualMachineManager; + + @Inject + EntityManager entityManager; + + @Inject + ManagementServerHostDao managementServerHostDao; + + @Inject + ClusterManager clusterManager; + + @Inject + AlertManager alertManager; + + @Inject + VMTemplateDao templateDao; + + @Inject + RoleService roleService; + + private ScheduledExecutorService extensionPathStateCheckExecutor; + + protected String getDefaultExtensionRelativePath(String name) { + String safeName = Extension.getDirectoryName(name); + return String.format("%s%s%s.sh", safeName, File.separator, safeName); + } + + protected String getValidatedExtensionRelativePath(String name, String relativePathPath) { + String safeName = Extension.getDirectoryName(name); + String normalizedPath = relativePathPath.replace("\\", "/"); + if (normalizedPath.startsWith("/")) { + normalizedPath = normalizedPath.substring(1); + } + if (normalizedPath.equals(safeName)) { + normalizedPath = safeName + "/" + safeName; + } else if (!normalizedPath.startsWith(safeName + "/")) { + normalizedPath = safeName + "/" + normalizedPath; + } + Path pathObj = Paths.get(normalizedPath); + int subDirCount = pathObj.getNameCount() - 1; + if (subDirCount > 2) { + throw new InvalidParameterException("Entry point path cannot be nested more than two sub-directories deep"); + } + return normalizedPath; + } + + protected Pair getResultFromAnswersString(String answersStr, Extension extension, + ManagementServerHostVO msHost, String op) { + Answer[] answers = null; + try { + answers = GsonHelper.getGson().fromJson(answersStr, Answer[].class); + } catch (Exception e) { + logger.error("Failed to parse answer JSON during {} for {} on {}: {}", + op, extension, msHost, e.getMessage(), e); + return new Pair<>(false, e.getMessage()); + } + Answer answer = answers != null && answers.length > 0 ? answers[0] : null; + boolean result = false; + String details = "Unknown error"; + if (answer != null) { + result = answer.getResult(); + details = answer.getDetails(); + } + if (!result) { + logger.error("Failed to {} for {} on {} due to {}", op, extension, msHost, details); + return new Pair<>(false, details); + } + return new Pair<>(true, details); + } + + protected boolean prepareExtensionPathOnMSPeer(Extension extension, ManagementServerHostVO msHost) { + final String msPeer = Long.toString(msHost.getMsid()); + logger.debug("Sending prepare extension entry-point for {} command to MS: {}", extension, msPeer); + final Command[] commands = new Command[1]; + commands[0] = new PrepareExtensionPathCommand(ManagementServerNode.getManagementServerId(), extension); + String answersStr = clusterManager.execute(msPeer, 0L, GsonHelper.getGson().toJson(commands), true); + return getResultFromAnswersString(answersStr, extension, msHost, "prepare entry-point").first(); + } + + protected Pair prepareExtensionPathOnCurrentServer(String name, boolean userDefined, + String relativePath) { + try { + externalProvisioner.prepareExtensionPath(name, userDefined, relativePath); + } catch (CloudRuntimeException e) { + logger.error("Failed to prepare entry-point for Extension [name: {}, userDefined: {}, relativePath: {}] on this server", + name, userDefined, relativePath, e); + return new Pair<>(false, e.getMessage()); + } + return new Pair<>(true, null); + } + + protected boolean cleanupExtensionFilesOnMSPeer(Extension extension, ManagementServerHostVO msHost) { + final String msPeer = Long.toString(msHost.getMsid()); + logger.debug("Sending cleanup extension entry-point for {} command to MS: {}", extension, msPeer); + final Command[] commands = new Command[1]; + commands[0] = new CleanupExtensionFilesCommand(ManagementServerNode.getManagementServerId(), extension); + String answersStr = clusterManager.execute(msPeer, 0L, GsonHelper.getGson().toJson(commands), true); + return getResultFromAnswersString(answersStr, extension, msHost, "cleanup entry-point").first(); + } + + protected Pair cleanupExtensionFilesOnCurrentServer(String name, String relativePath) { + try { + externalProvisioner.cleanupExtensionPath(name, relativePath); + externalProvisioner.cleanupExtensionData(name, 0, true); + } catch (CloudRuntimeException e) { + logger.error("Failed to cleanup entry-point files for Extension [name: {}, relativePath: {}] on this server", + name, relativePath, e); + return new Pair<>(false, e.getMessage()); + } + return new Pair<>(true, null); + } + + protected void cleanupExtensionFilesAcrossServers(Extension extension) { + boolean cleanup = true; + List msHosts = managementServerHostDao.listBy(ManagementServerHost.State.Up); + for (ManagementServerHostVO msHost : msHosts) { + if (msHost.getMsid() == ManagementServerNode.getManagementServerId()) { + cleanup = cleanup && cleanupExtensionFilesOnCurrentServer(extension.getName(), + extension.getRelativePath()).first(); + continue; + } + cleanup = cleanup && cleanupExtensionFilesOnMSPeer(extension, msHost); + } + if (!cleanup) { + throw new CloudRuntimeException("Extension is deleted but its entry-point files are not cleaned up across servers"); + } + } + + protected Pair getChecksumForExtensionPathOnMSPeer(Extension extension, ManagementServerHostVO msHost) { + final String msPeer = Long.toString(msHost.getMsid()); + logger.debug("Retrieving checksum for {} from MS: {}", extension, msPeer); + final Command[] cmds = new Command[1]; + cmds[0] = new GetExtensionPathChecksumCommand(ManagementServerNode.getManagementServerId(), + extension); + String answersStr = clusterManager.execute(msPeer, 0L, GsonHelper.getGson().toJson(cmds), true); + return getResultFromAnswersString(answersStr, extension, msHost, "prepare entry-point"); + } + + protected List getParametersListFromMap(String actionName, Map parametersMap) { + if (MapUtils.isEmpty(parametersMap)) { + return Collections.emptyList(); + } + List parameters = new ArrayList<>(); + for (Map entry : (Collection>)parametersMap.values()) { + ExtensionCustomAction.Parameter parameter = ExtensionCustomAction.Parameter.fromMap(entry); + logger.debug("Adding {} for custom action [{}]", parameter, actionName); + parameters.add(parameter); + } + return parameters; + } + + protected void unregisterExtensionWithCluster(String clusterUuid, Long extensionId) { + ClusterVO cluster = clusterDao.findByUuid(clusterUuid); + if (cluster == null) { + throw new InvalidParameterValueException("Unable to find cluster with given ID"); + } + unregisterExtensionWithCluster(cluster, extensionId); + } + + protected Extension getExtensionFromResource(ExtensionCustomAction.ResourceType resourceType, String resourceUuid) { + Object object = entityManager.findByUuid(resourceType.getAssociatedClass(), resourceUuid); + if (object == null) { + return null; + } + Long clusterId = null; + if (resourceType == ExtensionCustomAction.ResourceType.VirtualMachine) { + VirtualMachine vm = (VirtualMachine) object; + Pair clusterHostId = virtualMachineManager.findClusterAndHostIdForVm(vm, false); + clusterId = clusterHostId.first(); + } + if (clusterId == null) { + return null; + } + ExtensionResourceMapVO mapVO = + extensionResourceMapDao.findByResourceIdAndType(clusterId, ExtensionResourceMap.ResourceType.Cluster); + if (mapVO == null) { + return null; + } + return extensionDao.findById(mapVO.getExtensionId()); + } + + protected String getActionMessage(boolean success, ExtensionCustomAction action, Extension extension, + ExtensionCustomAction.ResourceType resourceType, Object resource) { + String msg = success ? action.getSuccessMessage() : action.getErrorMessage(); + if (StringUtils.isBlank(msg)) { + return success ? String.format("Successfully completed %s", action.getName()) : + String.format("Failed to complete %s", action.getName()); + } + Map values = new HashMap<>(); + values.put("actionName", action.getName()); + values.put("extensionName", extension.getName()); + if (msg.contains("{{resourceName}}")) { + String resourceName = resourceType.name(); + try { + Method getNameMethod = resource.getClass().getMethod("getName"); + Object result = getNameMethod.invoke(resource); + if (result instanceof String) { + resourceName = (String) result; + } + } catch (NoSuchMethodException | IllegalAccessException | InvocationTargetException e) { + logger.trace("Failed to get name for given resource of type: {}", resourceType, e); + } + values.put("resourceName", resourceName); + } + String result = msg; + for (Map.Entry entry : values.entrySet()) { + result = result.replace("{{" + entry.getKey() + "}}", entry.getValue()); + } + return result; + } + + protected Map getFilteredExternalDetails(Map details) { + if (MapUtils.isEmpty(details)) { + 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 + )); + } + + protected void sendExtensionPathNotReadyAlert(Extension extension) { + String msg = String.format("Path for %s not ready across management servers", + extension); + if (!Extension.State.Enabled.equals(extension.getState())) { + logger.warn(msg); + return; + } + alertManager.sendAlert(AlertManager.AlertType.ALERT_TYPE_EXTENSION_PATH_NOT_READY, 0L, 0L, msg, msg); + } + + protected void updateExtensionPathReady(Extension extension, boolean ready) { + if (!ready) { + sendExtensionPathNotReadyAlert(extension); + } + if (extension.isPathReady() == ready) { + return; + } + ExtensionVO extensionVO = extensionDao.createForUpdate(extension.getId()); + extensionVO.setPathReady(ready); + extensionDao.update(extension.getId(), extensionVO); + updateAllExtensionHosts(extension, null, false); + } + + protected void disableExtension(long extensionId) { + ExtensionVO extensionVO = extensionDao.createForUpdate(extensionId); + extensionVO.setState(Extension.State.Disabled); + extensionDao.update(extensionId, extensionVO); + } + + protected void updateAllExtensionHosts(Extension extension, Long clusterId, boolean remove) { + List hostIds = new ArrayList<>(); + List clusterIds = clusterId == null ? + extensionResourceMapDao.listResourceIdsByExtensionIdAndType(extension.getId(), + ExtensionResourceMap.ResourceType.Cluster) : + Collections.singletonList(clusterId); + for (Long cId : clusterIds) { + hostIds.addAll(hostDao.listIdsByClusterId(cId)); + } + if (CollectionUtils.isEmpty(hostIds)) { + return; + } + ConcurrentHashMap> futures = new ConcurrentHashMap<>(); + ExecutorService executorService = Executors.newFixedThreadPool(3, new NamedThreadFactory("ExtensionHostUpdateWorker")); + for (Long hostId : hostIds) { + futures.put(hostId, executorService.submit(() -> { + ExtensionRoutingUpdateCommand cmd = new ExtensionRoutingUpdateCommand(extension, remove); + agentMgr.send(hostId, cmd); + return null; + })); + } + for (Map.Entry> entry: futures.entrySet()) { + try { + entry.getValue().get(); + } catch (InterruptedException | ExecutionException e) { + logger.error(String.format("Error during updating %s for host: %d due to : %s", + extension, entry.getKey(), e.getMessage()), e); + } + } + executorService.shutdown(); + } + + protected Map> getExternalAccessDetails(Map actionDetails, long hostId, + ExtensionResourceMap resourceMap) { + Map> externalDetails = new HashMap<>(); + if (MapUtils.isNotEmpty(actionDetails)) { + externalDetails.put(ApiConstants.ACTION, actionDetails); + } + Map hostDetails = getFilteredExternalDetails(hostDetailsDao.findDetails(hostId)); + if (MapUtils.isNotEmpty(hostDetails)) { + externalDetails.put(ApiConstants.HOST, hostDetails); + } + if (resourceMap == null) { + return externalDetails; + } + Map resourceDetails = extensionResourceMapDetailsDao.listDetailsKeyPairs(resourceMap.getId(), true); + if (MapUtils.isNotEmpty(resourceDetails)) { + externalDetails.put(ApiConstants.RESOURCE_MAP, resourceDetails); + } + Map extensionDetails = extensionDetailsDao.listDetailsKeyPairs(resourceMap.getExtensionId(), true); + if (MapUtils.isNotEmpty(extensionDetails)) { + externalDetails.put(ApiConstants.EXTENSION, extensionDetails); + } + return externalDetails; + } + + protected void checkOrchestratorTemplates(Long extensionId) { + List extensionTemplateIds = templateDao.listIdsByExtensionId(extensionId); + if (CollectionUtils.isNotEmpty(extensionTemplateIds)) { + throw new CloudRuntimeException("Orchestrator extension has associated templates, remove them to delete the extension"); + } + } + + protected void checkExtensionPathState(Extension extension, List msHosts) { + String checksum = externalProvisioner.getChecksumForExtensionPath(extension.getName(), + extension.getRelativePath()); + if (StringUtils.isBlank(checksum)) { + updateExtensionPathReady(extension, false); + return; + } + if (CollectionUtils.isEmpty(msHosts)) { + updateExtensionPathReady(extension, true); + return; + } + for (ManagementServerHostVO msHost : msHosts) { + final Pair msPeerChecksumResult = getChecksumForExtensionPathOnMSPeer(extension, + msHost); + if (!msPeerChecksumResult.first() || !checksum.equals(msPeerChecksumResult.second())) { + logger.error("Entry-point checksum for {} is different [msid: {}, checksum: {}] and [msid: {}, checksum: {}]", + extension, ManagementServerNode.getManagementServerId(), checksum, msHost.getMsid(), + (msPeerChecksumResult.first() ? msPeerChecksumResult.second() : "unknown")); + updateExtensionPathReady(extension, false); + return; + } + } + updateExtensionPathReady(extension, true); + } + + @Override + public String getExtensionsPath() { + return externalProvisioner.getExtensionsPath(); + } + + @Override + @ActionEvent(eventType = EventTypes.EVENT_EXTENSION_CREATE, eventDescription = "creating extension") + public Extension createExtension(CreateExtensionCmd cmd) { + final String name = cmd.getName(); + final String description = cmd.getDescription(); + final String typeStr = cmd.getType(); + String relativePath = cmd.getPath(); + final Boolean orchestratorRequiresPrepareVm = cmd.isOrchestratorRequiresPrepareVm(); + final String stateStr = cmd.getState(); + ExtensionVO extensionByName = extensionDao.findByName(name); + if (extensionByName != null) { + throw new CloudRuntimeException("Extension by name already exists"); + } + final Extension.Type type = EnumUtils.getEnum(Extension.Type.class, typeStr); + if (type == null) { + throw new CloudRuntimeException(String.format("Invalid type specified - %s", typeStr)); + } + if (StringUtils.isBlank(relativePath)) { + relativePath = getDefaultExtensionRelativePath(name); + } else { + relativePath = getValidatedExtensionRelativePath(name, relativePath); + } + Extension.State state = Extension.State.Enabled; + if (StringUtils.isNotEmpty(stateStr)) { + try { + state = Extension.State.valueOf(stateStr); + } catch (IllegalArgumentException iae) { + throw new InvalidParameterValueException("Invalid state specified"); + } + } + if (orchestratorRequiresPrepareVm != null && !Extension.Type.Orchestrator.equals(type)) { + throw new InvalidParameterValueException(String.format("%s is applicable only with %s type", + ApiConstants.ORCHESTRATOR_REQUIRES_PREPARE_VM, type.name())); + } + final String relativePathFinal = relativePath; + final Extension.State stateFinal = state; + ExtensionVO extensionVO = Transaction.execute((TransactionCallbackWithException) status -> { + ExtensionVO extension = new ExtensionVO(name, description, type, + relativePathFinal, stateFinal); + if (!Extension.State.Enabled.equals(stateFinal)) { + extension.setPathReady(false); + } + extension = extensionDao.persist(extension); + + Map details = cmd.getDetails(); + List detailsVOList = new ArrayList<>(); + if (MapUtils.isNotEmpty(details)) { + for (Map.Entry entry : details.entrySet()) { + detailsVOList.add(new ExtensionDetailsVO(extension.getId(), entry.getKey(), entry.getValue())); + } + } + if (orchestratorRequiresPrepareVm != null) { + detailsVOList.add(new ExtensionDetailsVO(extension.getId(), + ApiConstants.ORCHESTRATOR_REQUIRES_PREPARE_VM, String.valueOf(orchestratorRequiresPrepareVm), + false)); + } + if (CollectionUtils.isNotEmpty(detailsVOList)) { + extensionDetailsDao.saveDetails(detailsVOList); + } + CallContext.current().setEventResourceId(extension.getId()); + return extension; + }); + if (Extension.State.Enabled.equals(extensionVO.getState()) && + !prepareExtensionPathAcrossServers(extensionVO)) { + disableExtension(extensionVO.getId()); + throw new CloudRuntimeException(String.format( + "Failed to enable extension: %s as it entry-point is not ready", + extensionVO.getName())); + } + return extensionVO; + } + + @Override + public boolean prepareExtensionPathAcrossServers(Extension extension) { + boolean prepared = true; + List msHosts = managementServerHostDao.listBy(ManagementServerHost.State.Up); + for (ManagementServerHostVO msHost : msHosts) { + if (msHost.getMsid() == ManagementServerNode.getManagementServerId()) { + prepared = prepared && prepareExtensionPathOnCurrentServer(extension.getName(), extension.isUserDefined(), + extension.getRelativePath()).first(); + continue; + } + prepared = prepared && prepareExtensionPathOnMSPeer(extension, msHost); + } + if (extension.isPathReady() != prepared) { + ExtensionVO updateExtension = extensionDao.createForUpdate(extension.getId()); + updateExtension.setPathReady(prepared); + extensionDao.update(extension.getId(), updateExtension); + } + return prepared; + } + + @Override + public List listExtensions(ListExtensionsCmd cmd) { + Long id = cmd.getExtensionId(); + String name = cmd.getName(); + String keyword = cmd.getKeyword(); + final SearchBuilder sb = extensionDao.createSearchBuilder(); + final Filter searchFilter = new Filter(ExtensionVO.class, "id", false, cmd.getStartIndex(), cmd.getPageSizeVal()); + + sb.and("id", sb.entity().getId(), SearchCriteria.Op.EQ); + sb.and("name", sb.entity().getName(), SearchCriteria.Op.EQ); + sb.and("keyword", sb.entity().getName(), SearchCriteria.Op.LIKE); + final SearchCriteria sc = sb.create(); + + if (id != null) { + sc.setParameters("id", id); + } + + if (name != null) { + sc.setParameters("name", name); + } + + if (keyword != null) { + sc.setParameters("keyword", "%" + keyword + "%"); + } + + final Pair, Integer> result = extensionDao.searchAndCount(sc, searchFilter); + List responses = new ArrayList<>(); + for (ExtensionVO extension : result.first()) { + ExtensionResponse response = createExtensionResponse(extension, cmd.getDetails()); + responses.add(response); + } + + return responses; + } + + @Override + @ActionEvent(eventType = EventTypes.EVENT_EXTENSION_UPDATE, eventDescription = "updating extension") + public Extension updateExtension(UpdateExtensionCmd cmd) { + final long id = cmd.getId(); + final String description = cmd.getDescription(); + final Boolean orchestratorRequiresPrepareVm = cmd.isOrchestratorRequiresPrepareVm(); + final String stateStr = cmd.getState(); + final Map details = cmd.getDetails(); + final Boolean cleanupDetails = cmd.isCleanupDetails(); + final ExtensionVO extensionVO = extensionDao.findById(id); + if (extensionVO == null) { + throw new InvalidParameterValueException("Failed to find the extension"); + } + boolean updateNeeded = false; + if (description != null && !description.equals(extensionVO.getDescription())) { + extensionVO.setDescription(description); + updateNeeded = true; + } + if (orchestratorRequiresPrepareVm != null && !Extension.Type.Orchestrator.equals(extensionVO.getType())) { + throw new InvalidParameterValueException(String.format("%s is applicable only with %s type", + ApiConstants.ORCHESTRATOR_REQUIRES_PREPARE_VM, extensionVO.getType())); + } + if (StringUtils.isNotBlank(stateStr) && !stateStr.equalsIgnoreCase(extensionVO.getState().name())) { + try { + Extension.State state = Extension.State.valueOf(stateStr); + extensionVO.setState(state); + updateNeeded = true; + } catch (IllegalArgumentException iae) { + throw new InvalidParameterValueException("Invalid state specified"); + } + } + final boolean updateNeededFinal = updateNeeded; + ExtensionVO result = Transaction.execute((TransactionCallbackWithException) status -> { + if (updateNeededFinal && !extensionDao.update(id, extensionVO)) { + throw new CloudRuntimeException(String.format("Failed to updated the extension: %s", + extensionVO.getName())); + } + updateExtensionsDetails(cleanupDetails, details, orchestratorRequiresPrepareVm, id); + return extensionVO; + }); + if (StringUtils.isNotBlank(stateStr)) { + if (Extension.State.Enabled.equals(result.getState()) && + !prepareExtensionPathAcrossServers(result)) { + disableExtension(result.getId()); + throw new CloudRuntimeException(String.format( + "Failed to enable extension: %s as it entry-point is not ready", + extensionVO.getName())); + } + updateAllExtensionHosts(extensionVO, null, false); + } + return result; + } + + protected void updateExtensionsDetails(Boolean cleanupDetails, Map details, Boolean orchestratorRequiresPrepareVm, long id) { + final boolean needToUpdateAllDetails = Boolean.TRUE.equals(cleanupDetails) || MapUtils.isNotEmpty(details); + if (!needToUpdateAllDetails && orchestratorRequiresPrepareVm == null) { + return; + } + if (needToUpdateAllDetails) { + Map hiddenDetails = + extensionDetailsDao.listDetailsKeyPairs(id, false); + List detailsVOList = new ArrayList<>(); + if (orchestratorRequiresPrepareVm != null) { + hiddenDetails.put(ApiConstants.ORCHESTRATOR_REQUIRES_PREPARE_VM, + String.valueOf(orchestratorRequiresPrepareVm)); + } + if (MapUtils.isNotEmpty(hiddenDetails)) { + hiddenDetails.forEach((key, value) -> detailsVOList.add( + new ExtensionDetailsVO(id, key, value, false))); + } + if (!Boolean.TRUE.equals(cleanupDetails) && MapUtils.isNotEmpty(details)) { + details.forEach((key, value) -> detailsVOList.add( + new ExtensionDetailsVO(id, key, value))); + } + if (CollectionUtils.isNotEmpty(detailsVOList)) { + extensionDetailsDao.saveDetails(detailsVOList); + } else if (Boolean.TRUE.equals(cleanupDetails)) { + extensionDetailsDao.removeDetails(id); + } + } else { + ExtensionDetailsVO detailsVO = extensionDetailsDao.findDetail(id, + ApiConstants.ORCHESTRATOR_REQUIRES_PREPARE_VM); + if (detailsVO == null) { + extensionDetailsDao.persist(new ExtensionDetailsVO(id, + ApiConstants.ORCHESTRATOR_REQUIRES_PREPARE_VM, + String.valueOf(orchestratorRequiresPrepareVm), false)); + } else if (Boolean.parseBoolean(detailsVO.getValue()) != orchestratorRequiresPrepareVm) { + detailsVO.setValue(String.valueOf(orchestratorRequiresPrepareVm)); + extensionDetailsDao.update(detailsVO.getId(), detailsVO); + } + } + } + + @Override + @ActionEvent(eventType = EventTypes.EVENT_EXTENSION_DELETE, eventDescription = "deleting extension") + public boolean deleteExtension(DeleteExtensionCmd cmd) { + Long extensionId = cmd.getId(); + final boolean cleanup = cmd.isCleanup(); + ExtensionVO extension = extensionDao.findById(extensionId); + if (extension == null) { + throw new InvalidParameterValueException("Unable to find the extension with the specified id"); + } + if (!extension.isUserDefined()) { + throw new InvalidParameterValueException("System extension can not be deleted"); + } + List registeredResources = extensionResourceMapDao.listByExtensionId(extensionId); + if (CollectionUtils.isNotEmpty(registeredResources)) { + throw new CloudRuntimeException("Extension has associated resources, unregister them to delete the extension"); + } + List customActionIds = extensionCustomActionDao.listIdsByExtensionId(extensionId); + if (CollectionUtils.isNotEmpty(customActionIds)) { + throw new CloudRuntimeException(String.format("Extension has %d custom actions, delete them to delete the extension", + customActionIds.size())); + } + checkOrchestratorTemplates(extensionId); + + boolean result = Transaction.execute((TransactionCallbackWithException) status -> { + extensionDetailsDao.removeDetails(extensionId); + extensionDao.remove(extensionId); + return true; + }); + if (result && cleanup) { + cleanupExtensionFilesAcrossServers(extension); + } + return true; + } + + @Override + @ActionEvent(eventType = EventTypes.EVENT_EXTENSION_RESOURCE_REGISTER, eventDescription = "registering extension resource") + public Extension registerExtensionWithResource(RegisterExtensionCmd cmd) { + String resourceId = cmd.getResourceId(); + Long extensionId = cmd.getExtensionId(); + String resourceType = cmd.getResourceType(); + if (!EnumUtils.isValidEnum(ExtensionResourceMap.ResourceType.class, resourceType)) { + throw new InvalidParameterValueException( + String.format("Currently only [%s] can be used to register an extension of type Orchestrator", + EnumSet.allOf(ExtensionResourceMap.ResourceType.class))); + } + ClusterVO clusterVO = clusterDao.findByUuid(resourceId); + if (clusterVO == null) { + throw new InvalidParameterValueException("Invalid cluster ID specified"); + } + ExtensionVO extension = extensionDao.findById(extensionId); + if (extension == null) { + throw new InvalidParameterValueException("Invalid extension specified"); + } + ExtensionResourceMap extensionResourceMap = registerExtensionWithCluster(clusterVO, extension, cmd.getDetails()); + return extensionDao.findById(extensionResourceMap.getExtensionId()); + } + + @Override + @ActionEvent(eventType = EventTypes.EVENT_EXTENSION_RESOURCE_REGISTER, eventDescription = "registering extension resource") + public ExtensionResourceMap registerExtensionWithCluster(Cluster cluster, Extension extension, + Map details) { + if (!Hypervisor.HypervisorType.External.equals(cluster.getHypervisorType())) { + throw new CloudRuntimeException( + String.format("Cluster ID: %s is not of %s hypervisor type", cluster.getId(), + cluster.getHypervisorType())); + } + final ExtensionResourceMap.ResourceType resourceType = ExtensionResourceMap.ResourceType.Cluster; + ExtensionResourceMapVO existing = + extensionResourceMapDao.findByResourceIdAndType(cluster.getId(), resourceType); + if (existing != null) { + if (existing.getExtensionId() == extension.getId()) { + throw new CloudRuntimeException(String.format( + "Extension: %s is already registered with this cluster: %s", + extension.getName(), cluster.getName())); + } else { + throw new CloudRuntimeException(String.format( + "An extension is already registered with this cluster: %s", cluster.getName())); + } + } + ExtensionResourceMap result = Transaction.execute((TransactionCallbackWithException) status -> { + ExtensionResourceMapVO extensionMap = new ExtensionResourceMapVO(extension.getId(), cluster.getId(), resourceType); + ExtensionResourceMapVO savedExtensionMap = extensionResourceMapDao.persist(extensionMap); + List detailsVOList = new ArrayList<>(); + if (MapUtils.isNotEmpty(details)) { + for (Map.Entry entry : details.entrySet()) { + detailsVOList.add(new ExtensionResourceMapDetailsVO(savedExtensionMap.getId(), + entry.getKey(), entry.getValue())); + } + extensionResourceMapDetailsDao.saveDetails(detailsVOList); + } + return extensionMap; + }); + updateAllExtensionHosts(extension, cluster.getId(), false); + return result; + } + + @Override + @ActionEvent(eventType = EventTypes.EVENT_EXTENSION_RESOURCE_UNREGISTER, eventDescription = "unregistering extension resource") + public Extension unregisterExtensionWithResource(UnregisterExtensionCmd cmd) { + final String resourceId = cmd.getResourceId(); + final Long extensionId = cmd.getExtensionId(); + final String resourceType = cmd.getResourceType(); + if (!EnumUtils.isValidEnum(ExtensionResourceMap.ResourceType.class, resourceType)) { + throw new InvalidParameterValueException( + String.format("Currently only [%s] can be used to unregister an extension of type Orchestrator", + EnumSet.allOf(ExtensionResourceMap.ResourceType.class))); + } + unregisterExtensionWithCluster(resourceId, extensionId); + return extensionDao.findById(extensionId); + } + + @Override + @ActionEvent(eventType = EventTypes.EVENT_EXTENSION_RESOURCE_UNREGISTER, eventDescription = "unregistering extension resource") + public void unregisterExtensionWithCluster(Cluster cluster, Long extensionId) { + ExtensionResourceMapVO existing = extensionResourceMapDao.findByResourceIdAndType(cluster.getId(), + ExtensionResourceMap.ResourceType.Cluster); + if (existing == null) { + return; + } + extensionResourceMapDao.remove(existing.getId()); + extensionResourceMapDetailsDao.removeDetails(existing.getId()); + ExtensionVO extensionVO = extensionDao.findById(extensionId); + if (extensionVO != null) { + updateAllExtensionHosts(extensionVO, cluster.getId(), true); + } + } + + @Override + public ExtensionResponse createExtensionResponse(Extension extension, + EnumSet viewDetails) { + ExtensionResponse response = new ExtensionResponse(extension.getUuid(), extension.getName(), + extension.getDescription(), extension.getType().name()); + response.setCreated(extension.getCreated()); + response.setPath(externalProvisioner.getExtensionPath(extension.getRelativePath())); + response.setPathReady(extension.isPathReady()); + response.setUserDefined(extension.isUserDefined()); + response.setState(extension.getState().name()); + if (viewDetails.contains(ApiConstants.ExtensionDetails.all) || + viewDetails.contains(ApiConstants.ExtensionDetails.resource)) { + List resourcesResponse = new ArrayList<>(); + List extensionResourceMapVOs = + extensionResourceMapDao.listByExtensionId(extension.getId()); + for (ExtensionResourceMapVO extensionResourceMapVO : extensionResourceMapVOs) { + ExtensionResourceResponse extensionResourceResponse = new ExtensionResourceResponse(); + extensionResourceResponse.setType(extensionResourceMapVO.getResourceType().name()); + extensionResourceResponse.setCreated(extensionResourceMapVO.getCreated()); + if (ExtensionResourceMap.ResourceType.Cluster.equals(extensionResourceMapVO.getResourceType())) { + Cluster cluster = clusterDao.findById(extensionResourceMapVO.getResourceId()); + extensionResourceResponse.setId(cluster.getUuid()); + extensionResourceResponse.setName(cluster.getName()); + } + Map details = extensionResourceMapDetailsDao.listDetailsKeyPairs( + extensionResourceMapVO.getId(), true); + if (MapUtils.isNotEmpty(details)) { + extensionResourceResponse.setDetails(details); + } + resourcesResponse.add(extensionResourceResponse); + } + if (CollectionUtils.isNotEmpty(resourcesResponse)) { + response.setResources(resourcesResponse); + } + } + Map hiddenDetails; + if (viewDetails.contains(ApiConstants.ExtensionDetails.all) || + viewDetails.contains(ApiConstants.ExtensionDetails.external)) { + Pair, Map> extensionDetails = + extensionDetailsDao.listDetailsKeyPairsWithVisibility(extension.getId()); + if (MapUtils.isNotEmpty(extensionDetails.first())) { + response.setDetails(extensionDetails.first()); + } + hiddenDetails = extensionDetails.second(); + } else { + hiddenDetails = extensionDetailsDao.listDetailsKeyPairs(extension.getId(), + List.of(ApiConstants.ORCHESTRATOR_REQUIRES_PREPARE_VM)); + } + if (hiddenDetails.containsKey(ApiConstants.ORCHESTRATOR_REQUIRES_PREPARE_VM)) { + response.setOrchestratorRequiresPrepareVm(Boolean.parseBoolean( + hiddenDetails.get(ApiConstants.ORCHESTRATOR_REQUIRES_PREPARE_VM))); + } + response.setObjectName(Extension.class.getSimpleName().toLowerCase()); + return response; + } + + @Override + @ActionEvent(eventType = EventTypes.EVENT_EXTENSION_CUSTOM_ACTION_ADD, eventDescription = "adding extension custom action") + public ExtensionCustomAction addCustomAction(AddCustomActionCmd cmd) { + String name = cmd.getName(); + String description = cmd.getDescription(); + Long extensionId = cmd.getExtensionId(); + String resourceTypeStr = cmd.getResourceType(); + List rolesStrList = cmd.getAllowedRoleTypes(); + final int timeout = ObjectUtils.defaultIfNull(cmd.getTimeout(), 3); + final boolean enabled = cmd.isEnabled(); + Map parametersMap = cmd.getParametersMap(); + final String successMessage = cmd.getSuccessMessage(); + final String errorMessage = cmd.getErrorMessage(); + Map details = cmd.getDetails(); + if (name == null || !name.matches("^[a-zA-Z0-9 _-]+$")) { + throw new InvalidParameterValueException(String.format("Invalid action name: %s. It can contain " + + "only alphabets, numbers, hyphen, underscore and space", name)); + } + ExtensionCustomActionVO existingCustomAction = extensionCustomActionDao.findByNameAndExtensionId(extensionId, name); + if (existingCustomAction != null) { + throw new CloudRuntimeException("Action by name already exists"); + } + ExtensionVO extensionVO = extensionDao.findById(extensionId); + if (extensionVO == null) { + throw new InvalidParameterValueException("Specified extension can not be found"); + } + List parameters = getParametersListFromMap(name, parametersMap); + ExtensionCustomAction.ResourceType resourceType = null; + if (StringUtils.isNotBlank(resourceTypeStr)) { + resourceType = EnumUtils.getEnumIgnoreCase(ExtensionCustomAction.ResourceType.class, resourceTypeStr); + if (resourceType == null) { + throw new InvalidParameterValueException( + String.format("Invalid resource type specified: %s. Valid values are: %s", resourceTypeStr, + EnumSet.allOf(ExtensionCustomAction.ResourceType.class))); + } + } + if (resourceType == null && Extension.Type.Orchestrator.equals(extensionVO.getType())) { + resourceType = ExtensionCustomAction.ResourceType.VirtualMachine; + } + final Set roleTypes = new HashSet<>(); + if (CollectionUtils.isNotEmpty(rolesStrList)) { + for (String roleTypeStr : rolesStrList) { + try { + RoleType roleType = RoleType.fromString(roleTypeStr); + roleTypes.add(roleType); + } catch (IllegalStateException ignored) { + throw new InvalidParameterValueException(String.format("Invalid role specified - %s", roleTypeStr)); + } + } + } + roleTypes.add(RoleType.Admin); + final ExtensionCustomAction.ResourceType resourceTypeFinal = resourceType; + return Transaction.execute((TransactionCallbackWithException) status -> { + ExtensionCustomActionVO customAction = + new ExtensionCustomActionVO(name, description, extensionId, successMessage, errorMessage, timeout, enabled); + if (resourceTypeFinal != null) { + customAction.setResourceType(resourceTypeFinal); + } + customAction.setAllowedRoleTypes(RoleType.toCombinedMask(roleTypes)); + ExtensionCustomActionVO savedAction = extensionCustomActionDao.persist(customAction); + List detailsVOList = new ArrayList<>(); + detailsVOList.add(new ExtensionCustomActionDetailsVO( + savedAction.getId(), + ApiConstants.PARAMETERS, + ExtensionCustomAction.Parameter.toJsonFromList(parameters), + false + )); + if (MapUtils.isNotEmpty(details)) { + details.forEach((key, value) -> detailsVOList.add( + new ExtensionCustomActionDetailsVO(savedAction.getId(), key, value))); + } + if (CollectionUtils.isNotEmpty(detailsVOList)) { + extensionCustomActionDetailsDao.saveDetails(detailsVOList); + } + CallContext.current().setEventResourceId(savedAction.getId()); + return savedAction; + }); + } + + @Override + @ActionEvent(eventType = EventTypes.EVENT_EXTENSION_CUSTOM_ACTION_DELETE, eventDescription = "deleting extension custom action") + public boolean deleteCustomAction(DeleteCustomActionCmd cmd) { + Long customActionId = cmd.getId(); + ExtensionCustomActionVO customActionVO = extensionCustomActionDao.findById(customActionId); + if (customActionVO == null) { + throw new InvalidParameterValueException("Unable to find the custom action with the specified id"); + } + return Transaction.execute((TransactionCallbackWithException) status -> { + extensionCustomActionDetailsDao.removeDetails(customActionId); + if (!extensionCustomActionDao.remove(customActionId)) { + throw new CloudRuntimeException("Failed to delete custom action"); + } + return true; + }); + } + + @Override + public List listCustomActions(ListCustomActionCmd cmd) { + Long id = cmd.getId(); + String name = cmd.getName(); + Long extensionId = cmd.getExtensionId(); + String keyword = cmd.getKeyword(); + final String resourceTypeStr = cmd.getResourceType(); + final String resourceId = cmd.getResourceId(); + final Boolean enabled = cmd.isEnabled(); + final SearchBuilder sb = extensionCustomActionDao.createSearchBuilder(); + final Filter searchFilter = new Filter(ExtensionCustomActionVO.class, "id", false, cmd.getStartIndex(), cmd.getPageSizeVal()); + final Account caller = CallContext.current().getCallingAccount(); + + ExtensionCustomAction.ResourceType resourceType = null; + if (StringUtils.isNotBlank(resourceTypeStr)) { + resourceType = EnumUtils.getEnum(ExtensionCustomAction.ResourceType.class, resourceTypeStr); + if (resourceType == null) { + throw new InvalidParameterValueException("Invalid resource type specified"); + } + } + + if (extensionId == null && resourceType != null && StringUtils.isNotBlank(resourceId)) { + Extension extension = getExtensionFromResource(resourceType, resourceId); + if (extension == null) { + logger.error("No extension found for the specified resource [type: {}, id: {}]", resourceTypeStr, resourceId); + throw new InvalidParameterValueException("Internal error listing custom actions with specified resource"); + } + extensionId = extension.getId(); + } + + final Role role = roleService.findRole(caller.getRoleId()); + + sb.and("id", sb.entity().getId(), SearchCriteria.Op.EQ); + sb.and("extensionId", sb.entity().getExtensionId(), SearchCriteria.Op.EQ); + sb.and("name", sb.entity().getName(), SearchCriteria.Op.EQ); + sb.and("keyword", sb.entity().getName(), SearchCriteria.Op.LIKE); + sb.and("enabled", sb.entity().isEnabled(), SearchCriteria.Op.EQ); + if (resourceType != null) { + sb.and().op("resourceTypeNull", sb.entity().getResourceType(), SearchCriteria.Op.NULL); + sb.or("resourceType", sb.entity().getResourceType(), SearchCriteria.Op.EQ); + sb.cp(); + } + if (!RoleType.Admin.equals(role.getRoleType())) { + sb.and("roleType", sb.entity().getAllowedRoleTypes(), SearchCriteria.Op.BINARY_OR); + } + sb.done(); + final SearchCriteria sc = sb.create(); + if (id != null) { + sc.setParameters("id", id); + } + if (extensionId != null) { + sc.setParameters("extensionId", extensionId); + } + if (StringUtils.isNotBlank(name)) { + sc.setParameters("name", name); + } + if (StringUtils.isNotBlank(keyword)) { + sc.setParameters("keyword", "%" + keyword + "%"); + } + if (enabled != null) { + sc.setParameters("enabled", true); + } + if (resourceType != null) { + sc.setParameters("resourceType", resourceType); + } + if (!RoleType.Admin.equals(role.getRoleType())) { + sc.setParameters("roleType", role.getRoleType().getMask()); + } + final Pair, Integer> result = extensionCustomActionDao.searchAndCount(sc, searchFilter); + List responses = new ArrayList<>(); + for (ExtensionCustomActionVO customAction : result.first()) { + responses.add(createCustomActionResponse(customAction)); + } + + return responses; + } + + @Override + @ActionEvent(eventType = EventTypes.EVENT_EXTENSION_CUSTOM_ACTION_UPDATE, eventDescription = "updating extension custom action") + public ExtensionCustomAction updateCustomAction(UpdateCustomActionCmd cmd) { + final long id = cmd.getId(); + String description = cmd.getDescription(); + String resourceTypeStr = cmd.getResourceType(); + List rolesStrList = cmd.getAllowedRoleTypes(); + Boolean enabled = cmd.isEnabled(); + Map parametersMap = cmd.getParametersMap(); + Boolean cleanupParameters = cmd.isCleanupParameters(); + final String successMessage = cmd.getSuccessMessage(); + final String errorMessage = cmd.getErrorMessage(); + final Integer timeout = cmd.getTimeout(); + Map details = cmd.getDetails(); + Boolean cleanupDetails = cmd.isCleanupDetails(); + + ExtensionCustomActionVO customAction = extensionCustomActionDao.findById(id); + if (customAction == null) { + throw new CloudRuntimeException("Action not found"); + } + + boolean needUpdate = false; + if (StringUtils.isNotBlank(description)) { + customAction.setDescription(description); + needUpdate = true; + } + if (resourceTypeStr != null) { + ExtensionCustomAction.ResourceType resourceType = + EnumUtils.getEnumIgnoreCase(ExtensionCustomAction.ResourceType.class, resourceTypeStr); + if (resourceType == null) { + throw new InvalidParameterValueException( + String.format("Invalid resource type specified: %s. Valid values are: %s", resourceTypeStr, + EnumSet.allOf(ExtensionCustomAction.ResourceType.class))); + } + customAction.setResourceType(resourceType); + needUpdate = true; + } + if (CollectionUtils.isNotEmpty(rolesStrList)) { + Set roles = new HashSet<>(); + for (String roleTypeStr : rolesStrList) { + try { + RoleType roleType = RoleType.fromString(roleTypeStr); + roles.add(roleType); + } catch (IllegalStateException ignored) { + throw new InvalidParameterValueException(String.format("Invalid role specified - %s", roleTypeStr)); + } + } + customAction.setAllowedRoleTypes(RoleType.toCombinedMask(roles)); + needUpdate = true; + } + if (successMessage != null) { + customAction.setSuccessMessage(successMessage); + needUpdate = true; + } + if (errorMessage != null) { + customAction.setErrorMessage(errorMessage); + needUpdate = true; + } + if (timeout != null) { + customAction.setTimeout(timeout); + needUpdate = true; + } + if (enabled != null) { + customAction.setEnabled(enabled); + needUpdate = true; + } + + List parameters = null; + if (!Boolean.TRUE.equals(cleanupParameters) && MapUtils.isNotEmpty(parametersMap)) { + parameters = getParametersListFromMap(customAction.getName(), parametersMap); + } + + final boolean needUpdateFinal = needUpdate; + final List parametersFinal = parameters; + return Transaction.execute((TransactionCallbackWithException) status -> { + if (needUpdateFinal) { + boolean result = extensionCustomActionDao.update(id, customAction); + if (!result) { + throw new CloudRuntimeException(String.format("Failed to update custom action: %s", + customAction.getName())); + } + } + updatedCustomActionDetails(id, cleanupDetails, details, cleanupParameters, parametersFinal); + return customAction; + }); + } + + protected void updatedCustomActionDetails(long id, Boolean cleanupDetails, Map details, + Boolean cleanupParameters, List parametersFinal) { + final boolean needToUpdateAllDetails = Boolean.TRUE.equals(cleanupDetails) || MapUtils.isNotEmpty(details); + final boolean needToUpdateParameters = Boolean.TRUE.equals(cleanupParameters) || CollectionUtils.isNotEmpty(parametersFinal); + if (!needToUpdateAllDetails && !needToUpdateParameters) { + return; + } + if (needToUpdateAllDetails) { + Map hiddenDetails = + extensionCustomActionDetailsDao.listDetailsKeyPairs(id, false); + List detailsVOList = new ArrayList<>(); + if (Boolean.TRUE.equals(cleanupParameters)) { + hiddenDetails.remove(ApiConstants.PARAMETERS); + } else if (CollectionUtils.isNotEmpty(parametersFinal)) { + hiddenDetails.put(ApiConstants.PARAMETERS, + ExtensionCustomAction.Parameter.toJsonFromList(parametersFinal)); + } + if (MapUtils.isNotEmpty(hiddenDetails)) { + hiddenDetails.forEach((key, value) -> detailsVOList.add( + new ExtensionCustomActionDetailsVO(id, key, value, false))); + } + if (!Boolean.TRUE.equals(cleanupDetails) && MapUtils.isNotEmpty(details)) { + details.forEach((key, value) -> detailsVOList.add( + new ExtensionCustomActionDetailsVO(id, key, value))); + } + if (CollectionUtils.isNotEmpty(detailsVOList)) { + extensionCustomActionDetailsDao.saveDetails(detailsVOList); + } else if (Boolean.TRUE.equals(cleanupDetails)) { + extensionCustomActionDetailsDao.removeDetails(id); + } + } else { + if (Boolean.TRUE.equals(cleanupParameters)) { + extensionCustomActionDetailsDao.removeDetail(id, ApiConstants.PARAMETERS); + } else if (CollectionUtils.isNotEmpty(parametersFinal)) { + ExtensionCustomActionDetailsVO detailsVO = extensionCustomActionDetailsDao.findDetail(id, + ApiConstants.PARAMETERS); + if (detailsVO == null) { + extensionCustomActionDetailsDao.persist(new ExtensionCustomActionDetailsVO(id, + ApiConstants.PARAMETERS, + ExtensionCustomAction.Parameter.toJsonFromList(parametersFinal), false)); + } else { + detailsVO.setValue(ExtensionCustomAction.Parameter.toJsonFromList(parametersFinal)); + extensionCustomActionDetailsDao.update(detailsVO.getId(), detailsVO); + } + } + } + } + + @Override + public CustomActionResultResponse runCustomAction(RunCustomActionCmd cmd) { + final Long id = cmd.getCustomActionId(); + final String resourceTypeStr = cmd.getResourceType(); + final String resourceUuid = cmd.getResourceId(); + Map cmdParameters = cmd.getParameters(); + final Account caller = CallContext.current().getCallingAccount(); + + String error = "Internal error running action"; + ExtensionCustomActionVO customActionVO = extensionCustomActionDao.findById(id); + if (customActionVO == null) { + logger.error("Invalid custom action specified with ID: {}", id); + throw new InvalidParameterValueException(error); + } + final Role role = roleService.findRole(caller.getRoleId()); + if (!RoleType.Admin.equals(role.getRoleType())) { + final Set allowedRoles = RoleType.fromCombinedMask(customActionVO.getAllowedRoleTypes()); + if (!allowedRoles.contains(role.getRoleType())) { + logger.error("Caller does not have permission to run {} with {} having role: {}", + customActionVO, caller, role.getRoleType().name()); + throw new InvalidParameterValueException(error); + } + } + if (!customActionVO.isEnabled()) { + logger.error("Failed to run {} as it is not enabled", customActionVO); + throw new InvalidParameterValueException(error); + } + final String actionName = customActionVO.getName(); + RunCustomActionCommand runCustomActionCommand = new RunCustomActionCommand(actionName); + final long extensionId = customActionVO.getExtensionId(); + final ExtensionVO extensionVO = extensionDao.findById(extensionId); + if (extensionVO == null) { + logger.error("Unable to find extension for {}", customActionVO); + throw new CloudRuntimeException(error); + } + if (!Extension.State.Enabled.equals(extensionVO.getState())) { + logger.error("{} is not in enabled state for running {}", extensionVO, customActionVO); + throw new CloudRuntimeException(error); + } + ExtensionCustomAction.ResourceType actionResourceType = customActionVO.getResourceType(); + if (actionResourceType == null && StringUtils.isBlank(resourceTypeStr)) { + throw new InvalidParameterValueException("Resource type not specified for the action"); + } + boolean validType = true; + if (StringUtils.isNotBlank(resourceTypeStr)) { + ExtensionCustomAction.ResourceType cmdResourceType = + EnumUtils.getEnumIgnoreCase(ExtensionCustomAction.ResourceType.class, resourceTypeStr); + validType = cmdResourceType != null && (actionResourceType == null || actionResourceType.equals(cmdResourceType)); + actionResourceType = cmdResourceType; + } + if (!validType || actionResourceType == null) { + logger.error("Invalid resource type - {} specified for {}", resourceTypeStr, customActionVO); + throw new CloudRuntimeException(error); + } + Object entity = entityManager.findByUuid(actionResourceType.getAssociatedClass(), resourceUuid); + if (entity == null) { + logger.error("Specified resource does not exist for running {}", customActionVO); + throw new CloudRuntimeException(error); + } + Long clusterId = null; + Long hostId = null; + if (entity instanceof Cluster) { + clusterId = ((Cluster)entity).getId(); + List hosts = hostDao.listByClusterAndHypervisorType(clusterId, Hypervisor.HypervisorType.External); + if (CollectionUtils.isEmpty(hosts)) { + logger.error("No hosts found for {} for running {}", entity, customActionVO); + throw new CloudRuntimeException(error); + } + hostId = hosts.get(0).getId(); + } else if (entity instanceof Host) { + Host host = (Host)entity; + if (!Hypervisor.HypervisorType.External.equals(host.getHypervisorType())) { + logger.error("Invalid {} specified as host resource for running {}", entity, customActionVO); + throw new InvalidParameterValueException(error); + } + hostId = host.getId(); + clusterId = host.getClusterId(); + } else if (entity instanceof VirtualMachine) { + VirtualMachine virtualMachine = (VirtualMachine)entity; + runCustomActionCommand.setVmId(virtualMachine.getId()); + if (!Hypervisor.HypervisorType.External.equals(virtualMachine.getHypervisorType())) { + logger.error("Invalid {} specified as VM resource for running {}", entity, customActionVO); + throw new InvalidParameterValueException(error); + } + Pair clusterAndHostId = virtualMachineManager.findClusterAndHostIdForVm(virtualMachine, false); + clusterId = clusterAndHostId.first(); + hostId = clusterAndHostId.second(); + } + + if (clusterId == null || hostId == null) { + logger.error( + "Unable to find cluster or host with the specified resource - cluster ID: {}, host ID: {}", + clusterId, hostId); + throw new CloudRuntimeException(error); + } + + ExtensionResourceMapVO extensionResource = extensionResourceMapDao.findByResourceIdAndType(clusterId, + ExtensionResourceMap.ResourceType.Cluster); + if (extensionResource == null) { + logger.error("No extension registered with cluster ID: {}", clusterId); + throw new CloudRuntimeException(error); + } + + List actionParameters = null; + Pair, Map> allDetails = + extensionCustomActionDetailsDao.listDetailsKeyPairsWithVisibility(customActionVO.getId()); + if (allDetails.second().containsKey(ApiConstants.PARAMETERS)) { + actionParameters = + ExtensionCustomAction.Parameter.toListFromJson(allDetails.second().get(ApiConstants.PARAMETERS)); + } + Map parameters = null; + if (CollectionUtils.isNotEmpty(actionParameters)) { + parameters = ExtensionCustomAction.Parameter.validateParameterValues(actionParameters, cmdParameters); + } + + CustomActionResultResponse response = new CustomActionResultResponse(); + response.setId(customActionVO.getUuid()); + response.setName(actionName); + response.setObjectName("customactionresult"); + Map result = new HashMap<>(); + response.setSuccess(false); + result.put(ApiConstants.MESSAGE, getActionMessage(false, customActionVO, extensionVO, + actionResourceType, entity)); + Map> externalDetails = + getExternalAccessDetails(allDetails.first(), hostId, extensionResource); + runCustomActionCommand.setParameters(parameters); + runCustomActionCommand.setExternalDetails(externalDetails); + runCustomActionCommand.setWait(customActionVO.getTimeout()); + try { + logger.info("Running custom action: {} with {} parameters", actionName, + (parameters != null ? parameters.keySet().size() : 0)); + Answer answer = agentMgr.send(hostId, runCustomActionCommand); + if (!(answer instanceof RunCustomActionAnswer)) { + logger.error("Unexpected answer [{}] received for {}", answer.getClass().getSimpleName(), + RunCustomActionCommand.class.getSimpleName()); + result.put(ApiConstants.DETAILS, error); + } else { + RunCustomActionAnswer customActionAnswer = (RunCustomActionAnswer) answer; + response.setSuccess(answer.getResult()); + result.put(ApiConstants.MESSAGE, getActionMessage(answer.getResult(), customActionVO, extensionVO, + actionResourceType, entity)); + result.put(ApiConstants.DETAILS, customActionAnswer.getDetails()); + } + } catch (AgentUnavailableException e) { + String msg = "Unable to run custom action"; + logger.error("{} due to {}", msg, e.getMessage(), e); + result.put(ApiConstants.DETAILS, msg); + } catch (OperationTimedoutException e) { + String msg = "Running custom action timed out, please try again"; + logger.error(msg, e); + result.put(ApiConstants.DETAILS, msg); + } + response.setResult(result); + return response; + } + + @Override + public ExtensionCustomActionResponse createCustomActionResponse(ExtensionCustomAction customAction) { + ExtensionCustomActionResponse response = new ExtensionCustomActionResponse(customAction.getUuid(), + customAction.getName(), customAction.getDescription()); + if (customAction.getResourceType() != null) { + response.setResourceType(customAction.getResourceType().name()); + } + Integer roles = ObjectUtils.defaultIfNull(customAction.getAllowedRoleTypes(), RoleType.Admin.getMask()); + response.setAllowedRoleTypes(RoleType.fromCombinedMask(roles) + .stream() + .map(Enum::name) + .collect(Collectors.toList())); + response.setSuccessMessage(customAction.getSuccessMessage()); + response.setErrorMessage(customAction.getErrorMessage()); + response.setTimeout(customAction.getTimeout()); + response.setEnabled(customAction.isEnabled()); + response.setCreated(customAction.getCreated()); + Optional.ofNullable(extensionDao.findById(customAction.getExtensionId())).ifPresent(extensionVO -> { + response.setExtensionId(extensionVO.getUuid()); + response.setExtensionName(extensionVO.getName()); + }); + Pair, Map> allDetails = + extensionCustomActionDetailsDao.listDetailsKeyPairsWithVisibility(customAction.getId()); + Optional.ofNullable(allDetails.second().get(ApiConstants.PARAMETERS)) + .map(ExtensionCustomAction.Parameter::toListFromJson) + .ifPresent(parameters -> { + List paramResponses = parameters.stream() + .map(p -> new ExtensionCustomActionParameterResponse(p.getName(), + p.getType().name(), p.getValidationFormat().name(), p.getValueOptions(), p.isRequired())) + .collect(Collectors.toList()); + response.setParameters(paramResponses); + }); + response.setDetails(allDetails.first()); + response.setObjectName(ExtensionCustomAction.class.getSimpleName().toLowerCase()); + return response; + } + + @Override + public Map> getExternalAccessDetails(Host host, Map vmDetails) { + long clusterId = host.getClusterId(); + ExtensionResourceMapVO resourceMap = extensionResourceMapDao.findByResourceIdAndType(clusterId, + ExtensionResourceMap.ResourceType.Cluster); + Map> details = getExternalAccessDetails(null, host.getId(), resourceMap); + if (MapUtils.isNotEmpty(vmDetails)) { + details.put(ApiConstants.VIRTUAL_MACHINE, vmDetails); + } + return details; + } + + @Override + public String handleExtensionServerCommands(ExtensionServerActionBaseCommand command) { + final String extensionName = command.getExtensionName(); + final String extensionRelativePath = command.getExtensionRelativePath(); + logger.debug("Received {} from MS: {} for extension [id: {}, name: {}, relativePath: {}]", + command.getClass().getSimpleName(), command.getMsId(), command.getExtensionId(), + extensionName, extensionRelativePath); + Answer answer = new Answer(command, false, "Unsupported command"); + if (command instanceof GetExtensionPathChecksumCommand) { + final GetExtensionPathChecksumCommand cmd = (GetExtensionPathChecksumCommand)command; + String checksum = externalProvisioner.getChecksumForExtensionPath(extensionName, + extensionRelativePath); + answer = new Answer(cmd, StringUtils.isNotBlank(checksum), checksum); + } else if (command instanceof PrepareExtensionPathCommand) { + final PrepareExtensionPathCommand cmd = (PrepareExtensionPathCommand)command; + Pair result = prepareExtensionPathOnCurrentServer( + extensionName, cmd.isExtensionUserDefined(), extensionRelativePath); + answer = new Answer(cmd, result.first(), result.second()); + } else if (command instanceof CleanupExtensionFilesCommand) { + final CleanupExtensionFilesCommand cmd = (CleanupExtensionFilesCommand)command; + Pair result = cleanupExtensionFilesOnCurrentServer(extensionName, + extensionRelativePath); + answer = new Answer(cmd, result.first(), result.second()); + } + final Answer[] answers = new Answer[1]; + answers[0] = answer; + return GsonHelper.getGson().toJson(answers); + } + + @Override + public Pair extensionResourceMapDetailsNeedUpdate(long resourceId, + ExtensionResourceMap.ResourceType resourceType, Map externalDetails) { + if (MapUtils.isEmpty(externalDetails)) { + return new Pair<>(false, null); + } + ExtensionResourceMapVO extensionResourceMapVO = + extensionResourceMapDao.findByResourceIdAndType(resourceId, resourceType); + if (extensionResourceMapVO == null) { + return new Pair<>(true, null); + } + Map mapDetails = + extensionResourceMapDetailsDao.listDetailsKeyPairs(extensionResourceMapVO.getId()); + if (MapUtils.isEmpty(mapDetails) || mapDetails.size() != externalDetails.size()) { + return new Pair<>(true, extensionResourceMapVO); + } + for (Map.Entry entry : externalDetails.entrySet()) { + String key = entry.getKey(); + String value = entry.getValue(); + if (!value.equals(mapDetails.get(key))) { + return new Pair<>(true, extensionResourceMapVO); + } + } + return new Pair<>(false, extensionResourceMapVO); + } + + @Override + public void updateExtensionResourceMapDetails(long extensionResourceMapId, Map details) { + if (MapUtils.isEmpty(details)) { + return; + } + List detailsList = new ArrayList<>(); + for (Map.Entry entry : details.entrySet()) { + detailsList.add(new ExtensionResourceMapDetailsVO(extensionResourceMapId, entry.getKey(), + entry.getValue())); + } + extensionResourceMapDetailsDao.saveDetails(detailsList); + } + + @Override + public Long getExtensionIdForCluster(long clusterId) { + ExtensionResourceMapVO map = extensionResourceMapDao.findByResourceIdAndType(clusterId, + ExtensionResourceMap.ResourceType.Cluster); + if (map == null) { + return null; + } + return map.getExtensionId(); + } + + @Override + public Extension getExtension(long id) { + return extensionDao.findById(id); + } + + @Override + public Extension getExtensionForCluster(long clusterId) { + Long extensionId = getExtensionIdForCluster(clusterId); + if (extensionId == null) { + return null; + } + return extensionDao.findById(extensionId); + } + + @Override + public boolean start() { + long pathStateCheckInterval = PathStateCheckInterval.value(); + long pathStateCheckInitialDelay = Math.min(60, pathStateCheckInterval); + logger.debug("Scheduling extensions path state check task with initial delay={}s and interval={}s", + pathStateCheckInitialDelay, pathStateCheckInterval); + extensionPathStateCheckExecutor.scheduleWithFixedDelay(new PathStateCheckWorker(), + pathStateCheckInitialDelay, pathStateCheckInterval, TimeUnit.SECONDS); + return true; + } + + @Override + public boolean configure(String name, Map params) throws ConfigurationException { + try { + extensionPathStateCheckExecutor = Executors.newScheduledThreadPool(1, + new NamedThreadFactory("Extension-Path-State-Check")); + } catch (final Exception e) { + throw new ConfigurationException("Unable to to configure ExtensionsManagerImpl"); + } + return true; + } + + @Override + public List> getCommands() { + List> cmds = new ArrayList<>(); + cmds.add(AddCustomActionCmd.class); + cmds.add(ListCustomActionCmd.class); + cmds.add(DeleteCustomActionCmd.class); + cmds.add(UpdateCustomActionCmd.class); + cmds.add(RunCustomActionCmd.class); + + cmds.add(CreateExtensionCmd.class); + cmds.add(ListExtensionsCmd.class); + cmds.add(DeleteExtensionCmd.class); + cmds.add(UpdateExtensionCmd.class); + cmds.add(RegisterExtensionCmd.class); + cmds.add(UnregisterExtensionCmd.class); + return cmds; + } + + @Override + public String getConfigComponentName() { + return ExtensionsManager.class.getSimpleName(); + } + + @Override + public ConfigKey[] getConfigKeys() { + return new ConfigKey[]{ + PathStateCheckInterval + }; + } + + public class PathStateCheckWorker extends ManagedContextRunnable { + + protected void runCheckUsingLongestRunningManagementServer() { + try { + List msHosts = managementServerHostDao.listBy(ManagementServerHost.State.Up); + msHosts.sort(Comparator.comparingLong(ManagementServerHostVO::getRunid)); + ManagementServerHostVO msHost = msHosts.remove(0); + if (msHost == null || (msHost.getMsid() != ManagementServerNode.getManagementServerId())) { + logger.debug("Skipping the extensions path state check on this management server"); + return; + } + List extensions = extensionDao.listAll(); + for (ExtensionVO extension : extensions) { + checkExtensionPathState(extension, msHosts); + } + } catch (Exception e) { + logger.warn("Extensions path state check failed", e); + } + } + + @Override + protected void runInContext() { + GlobalLock gcLock = GlobalLock.getInternLock("ExtensionPathStateCheck"); + try { + if (gcLock.lock(3)) { + try { + runCheckUsingLongestRunningManagementServer(); + } finally { + gcLock.unlock(); + } + } + } finally { + gcLock.releaseRef(); + } + } + } +} diff --git a/framework/extensions/src/main/java/org/apache/cloudstack/framework/extensions/vo/ExtensionCustomActionDetailsVO.java b/framework/extensions/src/main/java/org/apache/cloudstack/framework/extensions/vo/ExtensionCustomActionDetailsVO.java new file mode 100644 index 00000000000..15a5af4f60c --- /dev/null +++ b/framework/extensions/src/main/java/org/apache/cloudstack/framework/extensions/vo/ExtensionCustomActionDetailsVO.java @@ -0,0 +1,97 @@ +// 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.vo; + +import org.apache.cloudstack.api.ResourceDetail; + +import javax.persistence.Column; +import javax.persistence.Entity; +import javax.persistence.GeneratedValue; +import javax.persistence.GenerationType; +import javax.persistence.Id; +import javax.persistence.Table; + +@Entity +@Table(name = "extension_custom_action_details") +public class ExtensionCustomActionDetailsVO implements ResourceDetail { + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + @Column(name = "id") + private long id; + + @Column(name = "extension_custom_action_id", nullable = false) + private long resourceId; + + @Column(name = "name", nullable = false, length = 255) + private String name; + + @Column(name = "value", nullable = false, length = 65535) + private String value; + + @Column(name = "display") + private boolean display = true; + + public ExtensionCustomActionDetailsVO() { + } + + public ExtensionCustomActionDetailsVO(long resourceId, String name, String value) { + this.resourceId = resourceId; + this.name = name; + this.value = value; + } + + public ExtensionCustomActionDetailsVO(long id, String name, String value, boolean display) { + this.resourceId = id; + this.name = name; + this.value = value; + this.display = display; + } + + @Override + public long getId() { + return id; + } + + @Override + public String getName() { + return name; + } + + public void setName(String name) { + this.name = name; + } + + @Override + public String getValue() { + return value; + } + + @Override + public boolean isDisplay() { + return display; + } + + public void setValue(String value) { + this.value = value; + } + + @Override + public long getResourceId() { + return resourceId; + } +} diff --git a/framework/extensions/src/main/java/org/apache/cloudstack/framework/extensions/vo/ExtensionCustomActionVO.java b/framework/extensions/src/main/java/org/apache/cloudstack/framework/extensions/vo/ExtensionCustomActionVO.java new file mode 100644 index 00000000000..c5ab288d853 --- /dev/null +++ b/framework/extensions/src/main/java/org/apache/cloudstack/framework/extensions/vo/ExtensionCustomActionVO.java @@ -0,0 +1,215 @@ +// 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.vo; + +import org.apache.cloudstack.extension.ExtensionCustomAction; +import org.apache.cloudstack.utils.reflectiontostringbuilderutils.ReflectionToStringBuilderUtils; + +import com.cloud.utils.db.GenericDao; + +import javax.persistence.Column; +import javax.persistence.Entity; +import javax.persistence.EnumType; +import javax.persistence.Enumerated; +import javax.persistence.GeneratedValue; +import javax.persistence.GenerationType; +import javax.persistence.Id; +import javax.persistence.Table; +import javax.persistence.Temporal; +import javax.persistence.TemporalType; +import java.util.Date; +import java.util.UUID; + +@Entity +@Table(name = "extension_custom_action") +public class ExtensionCustomActionVO implements ExtensionCustomAction { + + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + @Column(name = "id") + private Long id; + + @Column(name = "uuid", nullable = false, unique = true) + private String uuid; + + @Column(name = "name", nullable = false) + private String name; + + @Column(name = "description", length = 4096) + private String description; + + @Column(name = "extension_id", nullable = false) + private Long extensionId; + + @Column(name = "resource_type") + @Enumerated(value = EnumType.STRING) + private ResourceType resourceType; + + @Column(name = "allowed_role_types") + private Integer allowedRoleTypes; + + @Column(name = "success_message", length = 4096) + private String successMessage; + + @Column(name = "error_message", length = 4096) + private String errorMessage; + + @Column(name = "timeout", nullable = false) + private int timeout; + + @Column(name = "enabled") + private boolean enabled; + + @Column(name = "created", nullable = false, updatable = false) + @Temporal(TemporalType.TIMESTAMP) + private Date created; + + @Column(name = GenericDao.REMOVED_COLUMN) + private Date removed; + + public ExtensionCustomActionVO() { + this.uuid = UUID.randomUUID().toString(); + this.created = new Date(); + } + + public ExtensionCustomActionVO(String name, String description, long extensionId, String successMessage, + String errorMessage, int timeout, boolean enabled) { + this.uuid = UUID.randomUUID().toString(); + this.created = new Date(); + this.name = name; + this.description = description; + this.extensionId = extensionId; + this.successMessage = successMessage; + this.errorMessage = errorMessage; + this.timeout = timeout; + this.enabled = enabled; + } + + @Override + public long getId() { + return id; + } + + @Override + public String getUuid() { + return uuid; + } + + public void setAllowedRoleTypes(int allowedRoleTypes) { + this.allowedRoleTypes = allowedRoleTypes; + } + + @Override + public Integer getAllowedRoleTypes() { + return allowedRoleTypes; + } + + public void setUuid(String uuid) { + this.uuid = uuid; + } + + 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; + } + + @Override + public long getExtensionId() { + return extensionId; + } + + public void setExtensionId(Long extensionId) { + this.extensionId = extensionId; + } + + @Override + public ResourceType getResourceType() { + return resourceType; + } + + public void setResourceType(ResourceType resourceType) { + this.resourceType = resourceType; + } + + public String getSuccessMessage() { + return successMessage; + } + + public void setSuccessMessage(String successMessage) { + this.successMessage = successMessage; + } + + public String getErrorMessage() { + return errorMessage; + } + + public void setErrorMessage(String errorMessage) { + this.errorMessage = errorMessage; + } + + @Override + public int getTimeout() { + return timeout; + } + + public void setTimeout(int timeout) { + this.timeout = timeout; + } + + @Override + public boolean isEnabled() { + return enabled; + } + + public void setEnabled(boolean enabled) { + this.enabled = enabled; + } + + public void setCreated(Date created) { + this.created = created; + } + + @Override + public Date getCreated() { + return created; + } + + public Date getRemoved() { + return removed; + } + + public void setRemoved(Date removed) { + this.removed = removed; + } + + @Override + public String toString() { + return String.format("Extension Custom Action %s", ReflectionToStringBuilderUtils.reflectOnlySelectedFields(this, "id", "uuid", "name", "extensionId", "resourceType")); + } + +} diff --git a/framework/extensions/src/main/java/org/apache/cloudstack/framework/extensions/vo/ExtensionDetailsVO.java b/framework/extensions/src/main/java/org/apache/cloudstack/framework/extensions/vo/ExtensionDetailsVO.java new file mode 100644 index 00000000000..535a0f70395 --- /dev/null +++ b/framework/extensions/src/main/java/org/apache/cloudstack/framework/extensions/vo/ExtensionDetailsVO.java @@ -0,0 +1,97 @@ +// 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.vo; + +import org.apache.cloudstack.api.ResourceDetail; + +import javax.persistence.Column; +import javax.persistence.Entity; +import javax.persistence.GeneratedValue; +import javax.persistence.GenerationType; +import javax.persistence.Id; +import javax.persistence.Table; + +@Entity +@Table(name = "extension_details") +public class ExtensionDetailsVO implements ResourceDetail { + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + @Column(name = "id") + private long id; + + @Column(name = "extension_id", nullable = false) + private long resourceId; + + @Column(name = "name", nullable = false, length = 255) + private String name; + + @Column(name = "value", nullable = false, length = 255) + private String value; + + @Column(name = "display") + private boolean display = true; + + public ExtensionDetailsVO() { + } + + public ExtensionDetailsVO(long resourceId, String name, String value) { + this.resourceId = resourceId; + this.name = name; + this.value = value; + } + + public ExtensionDetailsVO(long id, String name, String value, boolean display) { + this.resourceId = id; + this.name = name; + this.value = value; + this.display = display; + } + + @Override + public long getId() { + return id; + } + + @Override + public String getName() { + return name; + } + + public void setName(String name) { + this.name = name; + } + + @Override + public String getValue() { + return value; + } + + @Override + public boolean isDisplay() { + return display; + } + + public void setValue(String value) { + this.value = value; + } + + @Override + public long getResourceId() { + return resourceId; + } +} diff --git a/framework/extensions/src/main/java/org/apache/cloudstack/framework/extensions/vo/ExtensionResourceMapDetailsVO.java b/framework/extensions/src/main/java/org/apache/cloudstack/framework/extensions/vo/ExtensionResourceMapDetailsVO.java new file mode 100644 index 00000000000..5cb6f7b8511 --- /dev/null +++ b/framework/extensions/src/main/java/org/apache/cloudstack/framework/extensions/vo/ExtensionResourceMapDetailsVO.java @@ -0,0 +1,97 @@ +// 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.vo; + +import org.apache.cloudstack.api.ResourceDetail; + +import javax.persistence.Column; +import javax.persistence.Entity; +import javax.persistence.GeneratedValue; +import javax.persistence.GenerationType; +import javax.persistence.Id; +import javax.persistence.Table; + +@Entity +@Table(name = "extension_resource_map_details") +public class ExtensionResourceMapDetailsVO implements ResourceDetail { + + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + @Column(name = "id") + private long id; + + @Column(name = "extension_resource_map_id", nullable = false) + private long resourceId; + + @Column(name = "name", nullable = false, length = 255) + private String name; + + @Column(name = "value", nullable = false, length = 255) + private String value; + + @Column(name = "display") + private boolean display = true; + + public ExtensionResourceMapDetailsVO() { + } + + public ExtensionResourceMapDetailsVO(long resourceId, String name, String value) { + this.resourceId = resourceId; + this.name = name; + this.value = value; + } + + public ExtensionResourceMapDetailsVO(long id, String name, String value, boolean display) { + this.resourceId = id; + this.name = name; + this.value = value; + this.display = display; + } + + @Override + public long getId() { + return id; + } + + @Override + public String getName() { + return name; + } + + public void setName(String name) { + this.name = name; + } + + @Override + public String getValue() { + return value; + } + + @Override + public boolean isDisplay() { + return display; + } + + public void setValue(String value) { + this.value = value; + } + + @Override + public long getResourceId() { + return resourceId; + } +} diff --git a/framework/extensions/src/main/java/org/apache/cloudstack/framework/extensions/vo/ExtensionResourceMapVO.java b/framework/extensions/src/main/java/org/apache/cloudstack/framework/extensions/vo/ExtensionResourceMapVO.java new file mode 100644 index 00000000000..48d70b937e3 --- /dev/null +++ b/framework/extensions/src/main/java/org/apache/cloudstack/framework/extensions/vo/ExtensionResourceMapVO.java @@ -0,0 +1,122 @@ +// +// 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.vo; + +import org.apache.cloudstack.extension.ExtensionResourceMap; + +import javax.persistence.Column; +import javax.persistence.Entity; +import javax.persistence.EnumType; +import javax.persistence.Enumerated; +import javax.persistence.GeneratedValue; +import javax.persistence.GenerationType; +import javax.persistence.Id; +import javax.persistence.Table; +import javax.persistence.Temporal; +import javax.persistence.TemporalType; +import java.util.Date; + +@Entity +@Table(name = "extension_resource_map") +public class ExtensionResourceMapVO implements ExtensionResourceMap { + + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + @Column(name = "id") + private Long id; + + @Column(name = "extension_id", nullable = false) + private Long extensionId; + + @Column(name = "resource_id", nullable = false) + private Long resourceId; + + @Column(name = "resource_type", nullable = false) + @Enumerated(value = EnumType.STRING) + private ResourceType resourceType; + + @Column(name = "created", nullable = false, updatable = false) + @Temporal(TemporalType.TIMESTAMP) + private Date created; + + @Column(name = "removed") + @Temporal(TemporalType.TIMESTAMP) + private Date removed; + + public ExtensionResourceMapVO() { + } + + public ExtensionResourceMapVO(long extensionId, long resourceId, ResourceType resourceType) { + this.extensionId = extensionId; + this.resourceId = resourceId; + this.resourceType = resourceType; + } + + @Override + public long getExtensionId() { + return extensionId; + } + + + @Override + public long getResourceId() { + return resourceId; + } + + @Override + public ResourceType getResourceType() { + return resourceType; + } + + @Override + public Date getCreated() { + return created; + } + + public Date getRemoved() { + return removed; + } + + public void setExtensionId(Long extensionId) { + this.extensionId = extensionId; + } + + public void setResourceId(Long resourceId) { + this.resourceId = resourceId; + } + + public void setResourceType(ResourceType resourceType) { + this.resourceType = resourceType; + } + + public void setRemoved(Date removed) { + this.removed = removed; + } + + @Override + public long getId() { + return id; + } + + @Override + public String getUuid() { + return null; + } +} diff --git a/framework/extensions/src/main/java/org/apache/cloudstack/framework/extensions/vo/ExtensionVO.java b/framework/extensions/src/main/java/org/apache/cloudstack/framework/extensions/vo/ExtensionVO.java new file mode 100644 index 00000000000..20423764c1c --- /dev/null +++ b/framework/extensions/src/main/java/org/apache/cloudstack/framework/extensions/vo/ExtensionVO.java @@ -0,0 +1,179 @@ +//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.vo; + +import java.util.Date; +import java.util.UUID; + +import javax.persistence.Column; +import javax.persistence.Entity; +import javax.persistence.EnumType; +import javax.persistence.Enumerated; +import javax.persistence.GeneratedValue; +import javax.persistence.GenerationType; +import javax.persistence.Id; +import javax.persistence.Table; +import javax.persistence.Temporal; +import javax.persistence.TemporalType; + +import org.apache.cloudstack.extension.Extension; +import org.apache.cloudstack.utils.reflectiontostringbuilderutils.ReflectionToStringBuilderUtils; + +import com.cloud.utils.db.GenericDao; + +@Entity +@Table(name = "extension") +public class ExtensionVO implements Extension { + + public ExtensionVO() { + this.uuid = UUID.randomUUID().toString(); + this.created = new Date(); + } + + public ExtensionVO(String name, String description, Type type, String relativePath, State state) { + this.uuid = UUID.randomUUID().toString(); + this.name = name; + this.description = description; + this.type = type; + this.relativePath = relativePath; + this.userDefined = true; + this.pathReady = true; + this.state = state; + this.created = new Date(); + } + + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + @Column(name = "id") + private Long id; + + @Column(name = "uuid", nullable = false, unique = true) + private String uuid; + + @Column(name = "name", nullable = false) + private String name; + + @Column(name = "description", length = 4096) + private String description; + + @Column(name = "type", nullable = false) + @Enumerated(value = EnumType.STRING) + private Type type; + + @Column(name = "relative_path", nullable = false, length = 2048) + private String relativePath; + + @Column(name = "path_ready") + private boolean pathReady; + + @Column(name = "is_user_defined") + private boolean userDefined; + + @Column(name = "state") + @Enumerated(value = EnumType.STRING) + private State state; + + @Column(name = "created", nullable = false, updatable = false) + @Temporal(TemporalType.TIMESTAMP) + private Date created; + + @Column(name = GenericDao.REMOVED_COLUMN) + private Date removed; + + @Override + public long getId() { + return id; + } + + public String getUuid() { + return uuid; + } + + @Override + public String getName() { + return name; + } + + public void setName(String name) { + this.name = name; + } + + @Override + public String getDescription() { + return description; + } + + public void setDescription(String description) { + this.description = description; + } + + @Override + public Type getType() { + return type; + } + + @Override + public String getRelativePath() { + return relativePath; + } + + public void setRelativePath(String relativePath) { + this.relativePath = relativePath; + } + + @Override + public boolean isPathReady() { + return pathReady; + } + + public void setPathReady(boolean pathReady) { + this.pathReady = pathReady; + } + + @Override + public boolean isUserDefined() { + return userDefined; + } + + @Override + public State getState() { + return state; + } + + public void setState(State state) { + this.state = state; + } + + @Override + public Date getCreated() { + return created; + } + + public Date getRemoved() { + return removed; + } + + public void setRemoved(Date removed) { + this.removed = removed; + } + + @Override + public String toString() { + return String.format("Extension %s", ReflectionToStringBuilderUtils.reflectOnlySelectedFields(this, "id", "uuid", "name", "type")); + } +} diff --git a/framework/extensions/src/main/resources/META-INF/cloudstack/core/spring-framework-extensions-core-context.xml b/framework/extensions/src/main/resources/META-INF/cloudstack/core/spring-framework-extensions-core-context.xml new file mode 100644 index 00000000000..9d44d8ff7f3 --- /dev/null +++ b/framework/extensions/src/main/resources/META-INF/cloudstack/core/spring-framework-extensions-core-context.xml @@ -0,0 +1,36 @@ + + + + + + + + + + diff --git a/framework/extensions/src/test/java/org/apache/cloudstack/framework/extensions/api/AddCustomActionCmdTest.java b/framework/extensions/src/test/java/org/apache/cloudstack/framework/extensions/api/AddCustomActionCmdTest.java new file mode 100644 index 00000000000..b25de85a69d --- /dev/null +++ b/framework/extensions/src/test/java/org/apache/cloudstack/framework/extensions/api/AddCustomActionCmdTest.java @@ -0,0 +1,224 @@ +// 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 static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertNull; +import static org.junit.Assert.assertTrue; +import static org.mockito.Mockito.any; +import static org.mockito.Mockito.doNothing; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.spy; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +import java.util.Arrays; +import java.util.Collections; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +import org.apache.cloudstack.acl.RoleType; +import org.apache.cloudstack.api.ApiCommandResourceType; +import org.apache.cloudstack.api.response.ExtensionCustomActionResponse; +import org.apache.cloudstack.extension.ExtensionCustomAction; +import org.apache.cloudstack.framework.extensions.manager.ExtensionsManager; +import org.apache.commons.collections.MapUtils; +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.junit.MockitoJUnitRunner; +import org.springframework.test.util.ReflectionTestUtils; + +import com.cloud.user.Account; + +@RunWith(MockitoJUnitRunner.class) +public class AddCustomActionCmdTest { + + private AddCustomActionCmd cmd; + private ExtensionsManager extensionsManager; + + @Before + public void setUp() throws Exception { + cmd = new AddCustomActionCmd(); + extensionsManager = mock(ExtensionsManager.class); + cmd.extensionsManager = extensionsManager; + } + + private void setField(String fieldName, Object value) { + ReflectionTestUtils.setField(cmd, fieldName, value); + } + + @Test + public void testGetters() { + Long extensionId = 42L; + String name = "actionName"; + String description = "desc"; + String resourceType = "VM"; + List allowedRoleTypes = Arrays.asList(RoleType.Admin.name(), RoleType.User.name()); + Map parameters = new HashMap<>(); + parameters.put("name", "param1"); + String successMessage = "Success!"; + String errorMessage = "Error!"; + Integer timeout = 10; + Boolean enabled = true; + Map> details = new HashMap<>(); + Map inner = new HashMap<>(); + inner.put("vendor", "acme"); + details.put("details", inner); + setField("details", details); + + setField("extensionId", extensionId); + setField("name", name); + setField("description", description); + setField("resourceType", resourceType); + setField("allowedRoleTypes", allowedRoleTypes); + setField("parameters", parameters); + setField("successMessage", successMessage); + setField("errorMessage", errorMessage); + setField("timeout", timeout); + setField("enabled", enabled); + setField("details", details); + + assertEquals(extensionId, cmd.getExtensionId()); + assertEquals(name, cmd.getName()); + assertEquals(description, cmd.getDescription()); + assertEquals(resourceType, cmd.getResourceType()); + assertEquals(allowedRoleTypes, cmd.getAllowedRoleTypes()); + assertEquals(parameters, cmd.getParametersMap()); + assertEquals(successMessage, cmd.getSuccessMessage()); + assertEquals(errorMessage, cmd.getErrorMessage()); + assertEquals(timeout, cmd.getTimeout()); + assertTrue(cmd.isEnabled()); + assertTrue(MapUtils.isNotEmpty(cmd.getDetails())); + } + + @Test + public void testIsEnabledReturnsFalseWhenNull() { + setField("enabled", null); + assertFalse(cmd.isEnabled()); + } + + @Test + public void testIsEnabledReturnsFalseWhenFalse() { + setField("enabled", Boolean.FALSE); + assertFalse(cmd.isEnabled()); + } + + @Test + public void testIsEnabledReturnsTrueWhenTrue() { + setField("enabled", Boolean.TRUE); + assertTrue(cmd.isEnabled()); + } + + @Test + public void testGetAllowedRoleTypesReturnsNullWhenUnset() { + setField("allowedRoleTypes", null); + assertNull(cmd.getAllowedRoleTypes()); + } + + @Test + public void testGetAllowedRoleTypesReturnsEmptyList() { + setField("allowedRoleTypes", Collections.emptyList()); + assertEquals(0, cmd.getAllowedRoleTypes().size()); + } + + @Test + public void testGetParametersMapReturnsNullWhenUnset() { + setField("parameters", null); + assertNull(cmd.getParametersMap()); + } + + @Test + public void testGetParametersMapReturnsMap() { + Map parameters = new HashMap<>(); + parameters.put("foo", "bar"); + setField("parameters", parameters); + assertEquals(parameters, cmd.getParametersMap()); + } + + @Test + public void testGetDetailsReturnsNullWhenUnset() { + setField("details", null); + assertTrue(MapUtils.isEmpty(cmd.getDetails())); + } + + @Test + public void testGetDetailsReturnsMap() { + Map> details = new HashMap<>(); + Map inner = new HashMap<>(); + inner.put("key", "value"); + details.put("details", inner); + setField("details", details); + assertTrue(MapUtils.isNotEmpty(cmd.getDetails())); + } + + @Test + public void testGetDescriptionReturnsNullWhenUnset() { + setField("description", null); + assertNull(cmd.getDescription()); + } + + @Test + public void testGetSuccessMessageReturnsNullWhenUnset() { + setField("successMessage", null); + assertNull(cmd.getSuccessMessage()); + } + + @Test + public void testGetErrorMessageReturnsNullWhenUnset() { + setField("errorMessage", null); + assertNull(cmd.getErrorMessage()); + } + + @Test + public void testGetTimeoutReturnsNullWhenUnset() { + setField("timeout", null); + assertNull(cmd.getTimeout()); + } + + @Test + public void testExecuteCallsExtensionsManagerAndSetsResponse() { + ExtensionCustomAction extensionCustomAction = mock(ExtensionCustomAction.class); + ExtensionCustomActionResponse response = mock(ExtensionCustomActionResponse.class); + + when(extensionsManager.addCustomAction(any(AddCustomActionCmd.class))).thenReturn(extensionCustomAction); + when(extensionsManager.createCustomActionResponse(extensionCustomAction)).thenReturn(response); + + AddCustomActionCmd spyCmd = spy(cmd); + doNothing().when(spyCmd).setResponseObject(any()); + + spyCmd.execute(); + + verify(extensionsManager).addCustomAction(spyCmd); + verify(extensionsManager).createCustomActionResponse(extensionCustomAction); + verify(response).setResponseName(spyCmd.getCommandName()); + verify(spyCmd).setResponseObject(response); + } + + @Test + public void testGetEntityOwnerIdReturnsSystemAccount() { + assertEquals(Account.ACCOUNT_ID_SYSTEM, cmd.getEntityOwnerId()); + } + + @Test + public void testGetApiResourceTypeReturnsExtensionCustomAction() { + assertEquals(ApiCommandResourceType.ExtensionCustomAction, cmd.getApiResourceType()); + } +} diff --git a/framework/extensions/src/test/java/org/apache/cloudstack/framework/extensions/api/CreateExtensionCmdTest.java b/framework/extensions/src/test/java/org/apache/cloudstack/framework/extensions/api/CreateExtensionCmdTest.java new file mode 100644 index 00000000000..2edb6ea48e3 --- /dev/null +++ b/framework/extensions/src/test/java/org/apache/cloudstack/framework/extensions/api/CreateExtensionCmdTest.java @@ -0,0 +1,97 @@ +// 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 static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertNull; +import static org.junit.Assert.assertTrue; +import static org.springframework.test.util.ReflectionTestUtils.setField; + +import java.util.HashMap; +import java.util.Map; + +import org.apache.commons.collections.MapUtils; +import org.junit.Test; + +public class CreateExtensionCmdTest { + CreateExtensionCmd cmd = new CreateExtensionCmd(); + + @Test + public void testGetNameReturnsNullWhenUnset() { + assertNull(cmd.getName()); + } + + @Test + public void testGetNameReturnsValueWhenSet() { + String name = "name"; + setField(cmd, "name", name); + assertEquals(name, cmd.getName()); + } + + @Test + public void testGetTypeReturnsNullWhenUnset() { + setField(cmd, "type", null); + assertNull(cmd.getType()); + } + + @Test + public void testGetDescriptionReturnsValueWhenSet() { + String description = "description"; + setField(cmd, "description", description); + assertEquals(description, cmd.getDescription()); + } + + @Test + public void testGetPathReturnsValueWhenSet() { + String path = "/entry"; + setField(cmd, "path", path); + assertEquals(path, cmd.getPath()); + } + + @Test + public void testGetStateReturnsNullWhenUnset() { + setField(cmd, "state", null); + assertNull(cmd.getState()); + } + + @Test + public void testIsOrchestratorRequiresPrepareVm() { + assertNull(cmd.isOrchestratorRequiresPrepareVm()); + setField(cmd, "orchestratorRequiresPrepareVm", true); + assertTrue(cmd.isOrchestratorRequiresPrepareVm()); + setField(cmd, "orchestratorRequiresPrepareVm", false); + assertFalse(cmd.isOrchestratorRequiresPrepareVm()); + } + + @Test + public void testGetDetailsReturnsNullWhenUnset() { + setField(cmd, "details", null); + assertTrue(MapUtils.isEmpty(cmd.getDetails())); + } + + @Test + public void testGetDetailsReturnsMap() { + Map> details = new HashMap<>(); + Map inner = new HashMap<>(); + inner.put("key", "value"); + details.put("details", inner); + setField(cmd, "details", details); + assertTrue(MapUtils.isNotEmpty(cmd.getDetails())); + } +} diff --git a/framework/extensions/src/test/java/org/apache/cloudstack/framework/extensions/api/DeleteCustomActionCmdTest.java b/framework/extensions/src/test/java/org/apache/cloudstack/framework/extensions/api/DeleteCustomActionCmdTest.java new file mode 100644 index 00000000000..3a217d009fa --- /dev/null +++ b/framework/extensions/src/test/java/org/apache/cloudstack/framework/extensions/api/DeleteCustomActionCmdTest.java @@ -0,0 +1,81 @@ +// 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 static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNull; +import static org.junit.Assert.fail; + +import org.apache.cloudstack.api.ServerApiException; +import org.apache.cloudstack.api.response.SuccessResponse; +import org.apache.cloudstack.framework.extensions.manager.ExtensionsManager; +import org.junit.Before; +import org.junit.Test; +import org.mockito.Mockito; + +public class DeleteCustomActionCmdTest { + + private DeleteCustomActionCmd cmd; + private ExtensionsManager extensionsManager; + + @Before + public void setUp() throws Exception { + cmd = Mockito.spy(new DeleteCustomActionCmd()); + extensionsManager = Mockito.mock(ExtensionsManager.class); + java.lang.reflect.Field field = DeleteCustomActionCmd.class.getDeclaredField("extensionsManager"); + field.setAccessible(true); + field.set(cmd, extensionsManager); + } + + @Test + public void getIdReturnsNullWhenUnset() throws Exception { + java.lang.reflect.Field field = DeleteCustomActionCmd.class.getDeclaredField("id"); + field.setAccessible(true); + field.set(cmd, null); + assertNull(cmd.getId()); + } + + @Test + public void getIdReturnsValueWhenSet() throws Exception { + Long id = 12345L; + java.lang.reflect.Field field = DeleteCustomActionCmd.class.getDeclaredField("id"); + field.setAccessible(true); + field.set(cmd, id); + assertEquals(id, cmd.getId()); + } + + @Test + public void executeSetsSuccessResponseWhenManagerReturnsTrue() throws Exception { + Mockito.when(extensionsManager.deleteCustomAction(cmd)).thenReturn(true); + Mockito.doNothing().when(cmd).setResponseObject(Mockito.any()); + cmd.execute(); + Mockito.verify(extensionsManager).deleteCustomAction(cmd); + Mockito.verify(cmd).setResponseObject(Mockito.any(SuccessResponse.class)); + } + + @Test + public void executeThrowsServerApiExceptionWhenManagerReturnsFalse() throws Exception { + Mockito.when(extensionsManager.deleteCustomAction(cmd)).thenReturn(false); + try { + cmd.execute(); + fail("Expected ServerApiException"); + } catch (ServerApiException e) { + assertEquals("Failed to delete extension custom action", e.getDescription()); + } + } +} diff --git a/framework/extensions/src/test/java/org/apache/cloudstack/framework/extensions/api/DeleteExtensionCmdTest.java b/framework/extensions/src/test/java/org/apache/cloudstack/framework/extensions/api/DeleteExtensionCmdTest.java new file mode 100644 index 00000000000..9078a05dfda --- /dev/null +++ b/framework/extensions/src/test/java/org/apache/cloudstack/framework/extensions/api/DeleteExtensionCmdTest.java @@ -0,0 +1,90 @@ +// 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 static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertNull; +import static org.junit.Assert.assertTrue; +import static org.junit.Assert.fail; + +import org.apache.cloudstack.api.ServerApiException; +import org.apache.cloudstack.api.response.SuccessResponse; +import org.apache.cloudstack.framework.extensions.manager.ExtensionsManager; +import org.junit.Before; +import org.junit.Test; +import org.mockito.Mockito; +import org.springframework.test.util.ReflectionTestUtils; + +public class DeleteExtensionCmdTest { + + private DeleteExtensionCmd cmd; + private ExtensionsManager extensionsManager; + + @Before + public void setUp() { + cmd = Mockito.spy(new DeleteExtensionCmd()); + extensionsManager = Mockito.mock(ExtensionsManager.class); + cmd.extensionsManager = extensionsManager; + } + + @Test + public void getIdReturnsNullWhenUnset() { + ReflectionTestUtils.setField(cmd, "id", null); + assertNull(cmd.getId()); + } + + @Test + public void getIdReturnsValueWhenSet() { + Long id = 12345L; + ReflectionTestUtils.setField(cmd, "id", id); + assertEquals(id, cmd.getId()); + } + + @Test + public void isCleanupReturnsFalseWhenUnset() { + ReflectionTestUtils.setField(cmd, "cleanup", null); + assertFalse(cmd.isCleanup()); + } + + @Test + public void isCleanupReturnsTrueWhenSetTrue() { + ReflectionTestUtils.setField(cmd, "cleanup", true); + assertTrue(cmd.isCleanup()); + } + + @Test + public void executeSetsSuccessResponseWhenManagerReturnsTrue() { + Mockito.when(extensionsManager.deleteExtension(cmd)).thenReturn(true); + Mockito.doNothing().when(cmd).setResponseObject(Mockito.any()); + cmd.execute(); + Mockito.verify(extensionsManager).deleteExtension(cmd); + Mockito.verify(cmd).setResponseObject(Mockito.any(SuccessResponse.class)); + } + + @Test + public void executeThrowsServerApiExceptionWhenManagerReturnsFalse() { + Mockito.when(extensionsManager.deleteExtension(cmd)).thenReturn(false); + try { + cmd.execute(); + fail("Expected ServerApiException"); + } catch (ServerApiException e) { + assertEquals("Failed to delete extension", e.getDescription()); + } + } +} diff --git a/framework/extensions/src/test/java/org/apache/cloudstack/framework/extensions/api/ListCustomActionCmdTest.java b/framework/extensions/src/test/java/org/apache/cloudstack/framework/extensions/api/ListCustomActionCmdTest.java new file mode 100644 index 00000000000..6648b840c1c --- /dev/null +++ b/framework/extensions/src/test/java/org/apache/cloudstack/framework/extensions/api/ListCustomActionCmdTest.java @@ -0,0 +1,153 @@ +// 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 static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertNull; +import static org.junit.Assert.assertTrue; +import static org.mockito.Mockito.any; +import static org.mockito.Mockito.doNothing; +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.List; + +import org.apache.cloudstack.api.response.ExtensionCustomActionResponse; +import org.apache.cloudstack.api.response.ListResponse; +import org.apache.cloudstack.framework.extensions.manager.ExtensionsManager; +import org.junit.Before; +import org.junit.Test; +import org.mockito.Mockito; +import org.springframework.test.util.ReflectionTestUtils; + +public class ListCustomActionCmdTest { + + private ListCustomActionCmd cmd; + private ExtensionsManager extensionsManager; + + @Before + public void setUp() { + cmd = new ListCustomActionCmd(); + extensionsManager = mock(ExtensionsManager.class); + ReflectionTestUtils.setField(cmd, "extensionsManager", extensionsManager); + } + + private void setField(String fieldName, Object value) { + ReflectionTestUtils.setField(cmd, fieldName, value); + } + + @Test + public void getIdReturnsNullWhenUnset() { + setField("id", null); + assertNull(cmd.getId()); + } + + @Test + public void getIdReturnsValueWhenSet() { + Long id = 42L; + setField("id", id); + assertEquals(id, cmd.getId()); + } + + @Test + public void getNameReturnsNullWhenUnset() { + setField("name", null); + assertNull(cmd.getName()); + } + + @Test + public void getNameReturnsValueWhenSet() { + String name = "customAction"; + setField("name", name); + assertEquals(name, cmd.getName()); + } + + @Test + public void getExtensionIdReturnsNullWhenUnset() { + setField("extensionId", null); + assertNull(cmd.getExtensionId()); + } + + @Test + public void getExtensionIdReturnsValueWhenSet() { + Long extensionId = 99L; + setField("extensionId", extensionId); + assertEquals(extensionId, cmd.getExtensionId()); + } + + @Test + public void getResourceTypeReturnsNullWhenUnset() { + setField("resourceType", null); + assertNull(cmd.getResourceType()); + } + + @Test + public void getResourceTypeReturnsValueWhenSet() { + String resourceType = "VM"; + setField("resourceType", resourceType); + assertEquals(resourceType, cmd.getResourceType()); + } + + @Test + public void getResourceIdReturnsNullWhenUnset() { + setField("resourceId", null); + assertNull(cmd.getResourceId()); + } + + @Test + public void getResourceIdReturnsValueWhenSet() { + String resourceId = "abc-123"; + setField("resourceId", resourceId); + assertEquals(resourceId, cmd.getResourceId()); + } + + @Test + public void isEnabledReturnsNullWhenUnset() { + setField("enabled", null); + assertNull(cmd.isEnabled()); + } + + @Test + public void isEnabledReturnsTrueWhenSetTrue() { + setField("enabled", Boolean.TRUE); + assertTrue(cmd.isEnabled()); + } + + @Test + public void isEnabledReturnsFalseWhenSetFalse() { + setField("enabled", Boolean.FALSE); + assertFalse(cmd.isEnabled()); + } + + @Test + public void executeSetsListResponse() { + List responses = Arrays.asList(mock(ExtensionCustomActionResponse.class)); + when(extensionsManager.listCustomActions(cmd)).thenReturn(responses); + + ListCustomActionCmd spyCmd = Mockito.spy(cmd); + doNothing().when(spyCmd).setResponseObject(any()); + + spyCmd.execute(); + + verify(extensionsManager).listCustomActions(spyCmd); + verify(spyCmd).setResponseObject(any(ListResponse.class)); + } +} diff --git a/framework/extensions/src/test/java/org/apache/cloudstack/framework/extensions/api/ListExtensionsCmdTest.java b/framework/extensions/src/test/java/org/apache/cloudstack/framework/extensions/api/ListExtensionsCmdTest.java new file mode 100644 index 00000000000..1ca601293a3 --- /dev/null +++ b/framework/extensions/src/test/java/org/apache/cloudstack/framework/extensions/api/ListExtensionsCmdTest.java @@ -0,0 +1,92 @@ +// 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 static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertTrue; + +import java.util.Arrays; +import java.util.Collections; +import java.util.EnumSet; +import java.util.List; + +import org.apache.cloudstack.api.ApiConstants; +import org.junit.Before; +import org.junit.Test; +import org.springframework.test.util.ReflectionTestUtils; + +import com.cloud.exception.InvalidParameterValueException; + +public class ListExtensionsCmdTest { + + private ListExtensionsCmd cmd; + + @Before + public void setUp() { + cmd = new ListExtensionsCmd(); + } + + private void setPrivateField(String fieldName, Object value) { + ReflectionTestUtils.setField(cmd, fieldName, value); + } + + @Test + public void testGetName() { + String testName = "testExtension"; + setPrivateField("name", testName); + assertEquals(testName, cmd.getName()); + } + + @Test + public void testGetExtensionId() { + Long testId = 123L; + setPrivateField("extensionId", testId); + assertEquals(testId, cmd.getExtensionId()); + } + + @Test + public void testGetDetailsReturnsAllWhenNull() { + setPrivateField("details", null); + EnumSet result = cmd.getDetails(); + assertEquals(EnumSet.of(ApiConstants.ExtensionDetails.all), result); + } + + @Test + public void testGetDetailsReturnsAllWhenEmpty() { + setPrivateField("details", Collections.emptyList()); + EnumSet result = cmd.getDetails(); + assertEquals(EnumSet.of(ApiConstants.ExtensionDetails.all), result); + } + + @Test + public void testGetDetailsWithValidValues() { + List detailsList = Arrays.asList("all", "resource"); + setPrivateField("details", detailsList); + EnumSet result = cmd.getDetails(); + assertTrue(result.contains(ApiConstants.ExtensionDetails.all)); + assertTrue(result.contains(ApiConstants.ExtensionDetails.resource)); + assertEquals(2, result.size()); + } + + @Test(expected = InvalidParameterValueException.class) + public void testGetDetailsWithInvalidValueThrowsException() { + List detailsList = Arrays.asList("invalidValue"); + setPrivateField("details", detailsList); + cmd.getDetails(); + } +} diff --git a/framework/extensions/src/test/java/org/apache/cloudstack/framework/extensions/api/RegisterExtensionCmdTest.java b/framework/extensions/src/test/java/org/apache/cloudstack/framework/extensions/api/RegisterExtensionCmdTest.java new file mode 100644 index 00000000000..b5281342d4c --- /dev/null +++ b/framework/extensions/src/test/java/org/apache/cloudstack/framework/extensions/api/RegisterExtensionCmdTest.java @@ -0,0 +1,133 @@ +// 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 static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertNull; +import static org.junit.Assert.assertTrue; +import static org.junit.Assert.fail; +import static org.mockito.Mockito.any; +import static org.mockito.Mockito.doNothing; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +import java.util.EnumSet; +import java.util.Map; + +import org.apache.cloudstack.api.ApiConstants; +import org.apache.cloudstack.api.ApiErrorCode; +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 org.junit.Before; +import org.junit.Test; +import org.mockito.Mockito; +import org.springframework.test.util.ReflectionTestUtils; + +public class RegisterExtensionCmdTest { + + private RegisterExtensionCmd cmd; + private ExtensionsManager extensionsManager; + + @Before + public void setUp() { + cmd = Mockito.spy(new RegisterExtensionCmd()); + extensionsManager = mock(ExtensionsManager.class); + ReflectionTestUtils.setField(cmd, "extensionsManager", extensionsManager); + } + + @Test + public void extensionIdReturnsNullWhenUnset() { + ReflectionTestUtils.setField(cmd, "extensionId", null); + assertNull(cmd.getExtensionId()); + } + + @Test + public void extensionIdReturnsValueWhenSet() { + Long extensionId = 12345L; + ReflectionTestUtils.setField(cmd, "extensionId", extensionId); + assertEquals(extensionId, cmd.getExtensionId()); + } + + @Test + public void resourceIdReturnsNullWhenUnset() { + ReflectionTestUtils.setField(cmd, "resourceId", null); + assertNull(cmd.getResourceId()); + } + + @Test + public void resourceIdReturnsValueWhenSet() { + String resourceId = "resource-123"; + ReflectionTestUtils.setField(cmd, "resourceId", resourceId); + assertEquals(resourceId, cmd.getResourceId()); + } + + @Test + public void resourceTypeReturnsNullWhenUnset() { + ReflectionTestUtils.setField(cmd, "resourceType", null); + assertNull(cmd.getResourceType()); + } + + @Test + public void resourceTypeReturnsValueWhenSet() { + String resourceType = "VM"; + ReflectionTestUtils.setField(cmd, "resourceType", resourceType); + assertEquals(resourceType, cmd.getResourceType()); + } + + @Test + public void detailsReturnsEmptyMapWhenUnset() { + ReflectionTestUtils.setField(cmd, "details", null); + Map details = cmd.getDetails(); + assertNotNull(details); + assertTrue(details.isEmpty()); + } + + @Test + public void executeSetsExtensionResponseWhenManagerSucceeds() { + Extension extension = mock(Extension.class); + ExtensionResponse response = mock(ExtensionResponse.class); + when(extensionsManager.registerExtensionWithResource(cmd)).thenReturn(extension); + when(extensionsManager.createExtensionResponse(extension, EnumSet.of(ApiConstants.ExtensionDetails.all))) + .thenReturn(response); + + doNothing().when(cmd).setResponseObject(any()); + + cmd.execute(); + + verify(extensionsManager).registerExtensionWithResource(cmd); + verify(extensionsManager).createExtensionResponse(extension, EnumSet.of(ApiConstants.ExtensionDetails.all)); + verify(cmd).setResponseObject(response); + } + + @Test + public void executeThrowsServerApiExceptionWhenManagerFails() { + when(extensionsManager.registerExtensionWithResource(cmd)) + .thenThrow(new ServerApiException(ApiErrorCode.INTERNAL_ERROR, "Failed to register extension")); + + try { + cmd.execute(); + fail("Expected ServerApiException"); + } catch (ServerApiException e) { + assertEquals("Failed to register extension", e.getDescription()); + } + } +} diff --git a/framework/extensions/src/test/java/org/apache/cloudstack/framework/extensions/api/RunCustomActionCmdTest.java b/framework/extensions/src/test/java/org/apache/cloudstack/framework/extensions/api/RunCustomActionCmdTest.java new file mode 100644 index 00000000000..0fb4f628d27 --- /dev/null +++ b/framework/extensions/src/test/java/org/apache/cloudstack/framework/extensions/api/RunCustomActionCmdTest.java @@ -0,0 +1,127 @@ +// 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 static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertNull; +import static org.junit.Assert.assertTrue; +import static org.junit.Assert.fail; +import static org.mockito.Mockito.any; +import static org.mockito.Mockito.doNothing; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +import java.util.Map; + +import org.apache.cloudstack.api.ApiErrorCode; +import org.apache.cloudstack.api.ServerApiException; +import org.apache.cloudstack.extension.CustomActionResultResponse; +import org.apache.cloudstack.framework.extensions.manager.ExtensionsManager; +import org.junit.Before; +import org.junit.Test; +import org.mockito.Mockito; +import org.springframework.test.util.ReflectionTestUtils; + +public class RunCustomActionCmdTest { + + private RunCustomActionCmd cmd; + private ExtensionsManager extensionsManager; + + @Before + public void setUp() { + cmd = Mockito.spy(new RunCustomActionCmd()); + extensionsManager = mock(ExtensionsManager.class); + ReflectionTestUtils.setField(cmd, "extensionsManager", extensionsManager); + } + + @Test + public void customActionIdReturnsNullWhenUnset() { + ReflectionTestUtils.setField(cmd, "customActionId", null); + assertNull(cmd.getCustomActionId()); + } + + @Test + public void customActionIdReturnsValueWhenSet() { + Long customActionId = 12345L; + ReflectionTestUtils.setField(cmd, "customActionId", customActionId); + assertEquals(customActionId, cmd.getCustomActionId()); + } + + @Test + public void resourceTypeReturnsNullWhenUnset() { + ReflectionTestUtils.setField(cmd, "resourceType", null); + assertNull(cmd.getResourceType()); + } + + @Test + public void resourceTypeReturnsValueWhenSet() { + String resourceType = "VM"; + ReflectionTestUtils.setField(cmd, "resourceType", resourceType); + assertEquals(resourceType, cmd.getResourceType()); + } + + @Test + public void resourceIdReturnsNullWhenUnset() { + ReflectionTestUtils.setField(cmd, "resourceId", null); + assertNull(cmd.getResourceId()); + } + + @Test + public void resourceIdReturnsValueWhenSet() { + String resourceId = "resource-123"; + ReflectionTestUtils.setField(cmd, "resourceId", resourceId); + assertEquals(resourceId, cmd.getResourceId()); + } + + @Test + public void parametersReturnsEmptyMapWhenUnset() { + ReflectionTestUtils.setField(cmd, "parameters", null); + Map parameters = cmd.getParameters(); + assertNotNull(parameters); + assertTrue(parameters.isEmpty()); + } + + @Test + public void executeSetsCustomActionResultResponseWhenManagerSucceeds() { + CustomActionResultResponse response = mock(CustomActionResultResponse.class); + when(extensionsManager.runCustomAction(cmd)).thenReturn(response); + + doNothing().when(cmd).setResponseObject(any()); + + cmd.execute(); + + verify(extensionsManager).runCustomAction(cmd); + verify(cmd).setResponseObject(response); + } + + @Test + public void executeThrowsServerApiExceptionWhenManagerFails() { + when(extensionsManager.runCustomAction(cmd)) + .thenThrow(new ServerApiException(ApiErrorCode.INTERNAL_ERROR, "Failed to run custom action")); + + try { + cmd.execute(); + fail("Expected ServerApiException"); + } catch (ServerApiException e) { + assertEquals(ApiErrorCode.INTERNAL_ERROR, e.getErrorCode()); + assertEquals("Failed to run custom action", e.getDescription()); + } + } +} diff --git a/framework/extensions/src/test/java/org/apache/cloudstack/framework/extensions/api/UnregisterExtensionCmdTest.java b/framework/extensions/src/test/java/org/apache/cloudstack/framework/extensions/api/UnregisterExtensionCmdTest.java new file mode 100644 index 00000000000..f3c26f71f70 --- /dev/null +++ b/framework/extensions/src/test/java/org/apache/cloudstack/framework/extensions/api/UnregisterExtensionCmdTest.java @@ -0,0 +1,124 @@ +// 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 static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNull; +import static org.junit.Assert.fail; +import static org.mockito.Mockito.any; +import static org.mockito.Mockito.doNothing; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +import java.util.EnumSet; + +import org.apache.cloudstack.api.ApiConstants; +import org.apache.cloudstack.api.ApiErrorCode; +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 org.junit.Before; +import org.junit.Test; +import org.mockito.Mockito; +import org.springframework.test.util.ReflectionTestUtils; + +public class UnregisterExtensionCmdTest { + + private UnregisterExtensionCmd cmd; + private ExtensionsManager extensionsManager; + + @Before + public void setUp() { + cmd = Mockito.spy(new UnregisterExtensionCmd()); + extensionsManager = mock(ExtensionsManager.class); + ReflectionTestUtils.setField(cmd, "extensionsManager", extensionsManager); + } + + @Test + public void extensionIdReturnsNullWhenUnset() { + ReflectionTestUtils.setField(cmd, "extensionId", null); + assertNull(cmd.getExtensionId()); + } + + @Test + public void extensionIdReturnsValueWhenSet() { + Long extensionId = 12345L; + ReflectionTestUtils.setField(cmd, "extensionId", extensionId); + assertEquals(extensionId, cmd.getExtensionId()); + } + + @Test + public void resourceIdReturnsNullWhenUnset() { + ReflectionTestUtils.setField(cmd, "resourceId", null); + assertNull(cmd.getResourceId()); + } + + @Test + public void resourceIdReturnsValueWhenSet() { + String resourceId = "resource-123"; + ReflectionTestUtils.setField(cmd, "resourceId", resourceId); + assertEquals(resourceId, cmd.getResourceId()); + } + + @Test + public void resourceTypeReturnsNullWhenUnset() { + ReflectionTestUtils.setField(cmd, "resourceType", null); + assertNull(cmd.getResourceType()); + } + + @Test + public void resourceTypeReturnsValueWhenSet() { + String resourceType = "VM"; + ReflectionTestUtils.setField(cmd, "resourceType", resourceType); + assertEquals(resourceType, cmd.getResourceType()); + } + + @Test + public void executeSetsExtensionResponseWhenManagerSucceeds() { + Extension extension = mock(Extension.class); + ExtensionResponse response = mock(ExtensionResponse.class); + when(extensionsManager.unregisterExtensionWithResource(cmd)).thenReturn(extension); + when(extensionsManager.createExtensionResponse(extension, EnumSet.of(ApiConstants.ExtensionDetails.all))) + .thenReturn(response); + + doNothing().when(cmd).setResponseObject(any()); + + cmd.execute(); + + verify(extensionsManager).unregisterExtensionWithResource(cmd); + verify(extensionsManager).createExtensionResponse(extension, EnumSet.of(ApiConstants.ExtensionDetails.all)); + verify(response).setResponseName(cmd.getCommandName()); + verify(cmd).setResponseObject(response); + } + + @Test + public void executeThrowsServerApiExceptionWhenManagerFails() { + when(extensionsManager.unregisterExtensionWithResource(cmd)) + .thenThrow(new ServerApiException(ApiErrorCode.INTERNAL_ERROR, "Failed to unregister extension")); + + try { + cmd.execute(); + fail("Expected ServerApiException"); + } catch (ServerApiException e) { + assertEquals(ApiErrorCode.INTERNAL_ERROR, e.getErrorCode()); + assertEquals("Failed to unregister extension", e.getDescription()); + } + } +} diff --git a/framework/extensions/src/test/java/org/apache/cloudstack/framework/extensions/api/UpdateCustomActionCmdTest.java b/framework/extensions/src/test/java/org/apache/cloudstack/framework/extensions/api/UpdateCustomActionCmdTest.java new file mode 100644 index 00000000000..5ba17111c1c --- /dev/null +++ b/framework/extensions/src/test/java/org/apache/cloudstack/framework/extensions/api/UpdateCustomActionCmdTest.java @@ -0,0 +1,240 @@ +// 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 static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertNull; +import static org.junit.Assert.assertTrue; +import static org.junit.Assert.fail; +import static org.mockito.Mockito.any; +import static org.mockito.Mockito.doNothing; +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.HashMap; +import java.util.List; +import java.util.Map; + +import org.apache.cloudstack.api.ApiErrorCode; +import org.apache.cloudstack.api.ServerApiException; +import org.apache.cloudstack.api.response.ExtensionCustomActionResponse; +import org.apache.cloudstack.extension.ExtensionCustomAction; +import org.apache.cloudstack.framework.extensions.manager.ExtensionsManager; +import org.apache.commons.collections.MapUtils; +import org.junit.Before; +import org.junit.Test; +import org.mockito.Mockito; +import org.springframework.test.util.ReflectionTestUtils; + +public class UpdateCustomActionCmdTest { + + private UpdateCustomActionCmd cmd; + private ExtensionsManager extensionsManager; + + @Before + public void setUp() { + cmd = Mockito.spy(new UpdateCustomActionCmd()); + extensionsManager = mock(ExtensionsManager.class); + ReflectionTestUtils.setField(cmd, "extensionsManager", extensionsManager); + } + + @Test + public void idReturnsValueWhenSet() { + long id = 12345L; + ReflectionTestUtils.setField(cmd, "id", id); + assertEquals(id, cmd.getId()); + } + + @Test + public void descriptionReturnsNullWhenUnset() { + ReflectionTestUtils.setField(cmd, "description", null); + assertNull(cmd.getDescription()); + } + + @Test + public void descriptionReturnsValueWhenSet() { + String description = "Custom action description"; + ReflectionTestUtils.setField(cmd, "description", description); + assertEquals(description, cmd.getDescription()); + } + + @Test + public void resourceTypeReturnsNullWhenUnset() { + ReflectionTestUtils.setField(cmd, "resourceType", null); + assertNull(cmd.getResourceType()); + } + + @Test + public void resourceTypeReturnsValueWhenSet() { + String resourceType = "VM"; + ReflectionTestUtils.setField(cmd, "resourceType", resourceType); + assertEquals(resourceType, cmd.getResourceType()); + } + + @Test + public void allowedRoleTypesReturnsNullWhenUnset() { + ReflectionTestUtils.setField(cmd, "allowedRoleTypes", null); + assertNull(cmd.getAllowedRoleTypes()); + } + + @Test + public void allowedRoleTypesReturnsValueWhenSet() { + List roles = Arrays.asList("Admin", "User"); + ReflectionTestUtils.setField(cmd, "allowedRoleTypes", roles); + assertEquals(roles, cmd.getAllowedRoleTypes()); + } + + @Test + public void parametersMapReturnsNullWhenUnset() { + ReflectionTestUtils.setField(cmd, "parameters", null); + assertNull(cmd.getParametersMap()); + } + + @Test + public void parametersMapReturnsValueWhenSet() { + Map params = new HashMap<>(); + params.put("name", "param1"); + ReflectionTestUtils.setField(cmd, "parameters", params); + assertEquals(params, cmd.getParametersMap()); + } + + @Test + public void isCleanupParametersReturnsNullWhenUnset() { + ReflectionTestUtils.setField(cmd, "cleanupParameters", null); + assertNull(cmd.isCleanupParameters()); + } + + @Test + public void isCleanupParametersReturnsValueWhenSet() { + ReflectionTestUtils.setField(cmd, "cleanupParameters", Boolean.TRUE); + assertTrue(cmd.isCleanupParameters()); + } + + @Test + public void successMessageReturnsNullWhenUnset() { + ReflectionTestUtils.setField(cmd, "successMessage", null); + assertNull(cmd.getSuccessMessage()); + } + + @Test + public void successMessageReturnsValueWhenSet() { + String msg = "Success!"; + ReflectionTestUtils.setField(cmd, "successMessage", msg); + assertEquals(msg, cmd.getSuccessMessage()); + } + + @Test + public void errorMessageReturnsNullWhenUnset() { + ReflectionTestUtils.setField(cmd, "errorMessage", null); + assertNull(cmd.getErrorMessage()); + } + + @Test + public void errorMessageReturnsValueWhenSet() { + String msg = "Error!"; + ReflectionTestUtils.setField(cmd, "errorMessage", msg); + assertEquals(msg, cmd.getErrorMessage()); + } + + @Test + public void timeoutReturnsNullWhenUnset() { + ReflectionTestUtils.setField(cmd, "timeout", null); + assertNull(cmd.getTimeout()); + } + + @Test + public void timeoutReturnsValueWhenSet() { + Integer timeout = 10; + ReflectionTestUtils.setField(cmd, "timeout", timeout); + assertEquals(timeout, cmd.getTimeout()); + } + + @Test + public void isEnabledReturnsNullWhenUnset() { + ReflectionTestUtils.setField(cmd, "enabled", null); + assertNull(cmd.isEnabled()); + } + + @Test + public void isEnabledReturnsValueWhenSet() { + ReflectionTestUtils.setField(cmd, "enabled", Boolean.FALSE); + assertFalse(cmd.isEnabled()); + } + + @Test + public void detailsReturnsNullWhenUnset() { + ReflectionTestUtils.setField(cmd, "details", null); + assertTrue(MapUtils.isEmpty(cmd.getDetails())); + } + + @Test + public void detailsReturnsValueWhenSet() { + Map> details = new HashMap<>(); + Map inner = new HashMap<>(); + inner.put("vendor", "acme"); + details.put("details", inner); + ReflectionTestUtils.setField(cmd, "details", details); + assertTrue(MapUtils.isNotEmpty(cmd.getDetails())); + } + + @Test + public void isCleanupDetailsReturnsNullWhenUnset() { + ReflectionTestUtils.setField(cmd, "cleanupDetails", null); + assertNull(cmd.isCleanupDetails()); + } + + @Test + public void isCleanupDetailsReturnsValueWhenSet() { + ReflectionTestUtils.setField(cmd, "cleanupDetails", Boolean.TRUE); + assertTrue(cmd.isCleanupDetails()); + } + + @Test + public void executeSetsCustomActionResponseWhenManagerSucceeds() { + ExtensionCustomAction customAction = mock(ExtensionCustomAction.class); + ExtensionCustomActionResponse response = mock(ExtensionCustomActionResponse.class); + when(extensionsManager.updateCustomAction(cmd)).thenReturn(customAction); + when(extensionsManager.createCustomActionResponse(customAction)).thenReturn(response); + + doNothing().when(cmd).setResponseObject(any()); + + cmd.execute(); + + verify(extensionsManager).updateCustomAction(cmd); + verify(extensionsManager).createCustomActionResponse(customAction); + verify(response).setResponseName(cmd.getCommandName()); + verify(cmd).setResponseObject(response); + } + + @Test + public void executeThrowsServerApiExceptionWhenManagerFails() { + when(extensionsManager.updateCustomAction(cmd)) + .thenThrow(new ServerApiException(ApiErrorCode.INTERNAL_ERROR, "Failed to update custom action")); + + try { + cmd.execute(); + fail("Expected ServerApiException"); + } catch (ServerApiException e) { + assertEquals(ApiErrorCode.INTERNAL_ERROR, e.getErrorCode()); + assertEquals("Failed to update custom action", e.getDescription()); + } + } +} diff --git a/framework/extensions/src/test/java/org/apache/cloudstack/framework/extensions/api/UpdateExtensionCmdTest.java b/framework/extensions/src/test/java/org/apache/cloudstack/framework/extensions/api/UpdateExtensionCmdTest.java new file mode 100644 index 00000000000..f0a3a6fcf21 --- /dev/null +++ b/framework/extensions/src/test/java/org/apache/cloudstack/framework/extensions/api/UpdateExtensionCmdTest.java @@ -0,0 +1,168 @@ +// 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 static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNull; +import static org.junit.Assert.assertTrue; +import static org.junit.Assert.fail; +import static org.mockito.Mockito.any; +import static org.mockito.Mockito.doNothing; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +import java.util.EnumSet; +import java.util.HashMap; +import java.util.Map; + +import org.apache.cloudstack.api.ApiConstants; +import org.apache.cloudstack.api.ApiErrorCode; +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 org.apache.commons.collections.MapUtils; +import org.junit.Before; +import org.junit.Test; +import org.mockito.Mockito; +import org.springframework.test.util.ReflectionTestUtils; + +public class UpdateExtensionCmdTest { + + private UpdateExtensionCmd cmd; + private ExtensionsManager extensionsManager; + + @Before + public void setUp() { + cmd = Mockito.spy(new UpdateExtensionCmd()); + extensionsManager = mock(ExtensionsManager.class); + ReflectionTestUtils.setField(cmd, "extensionsManager", extensionsManager); + } + + @Test + public void idReturnsNullWhenUnset() { + ReflectionTestUtils.setField(cmd, "id", null); + assertNull(cmd.getId()); + } + + @Test + public void idReturnsValueWhenSet() { + Long id = 12345L; + ReflectionTestUtils.setField(cmd, "id", id); + assertEquals(id, cmd.getId()); + } + + @Test + public void descriptionReturnsNullWhenUnset() { + ReflectionTestUtils.setField(cmd, "description", null); + assertNull(cmd.getDescription()); + } + + @Test + public void descriptionReturnsValueWhenSet() { + String description = "Extension description"; + ReflectionTestUtils.setField(cmd, "description", description); + assertEquals(description, cmd.getDescription()); + } + + @Test + public void orchestratorRequiresPrepareVmReturnsNullWhenUnset() { + ReflectionTestUtils.setField(cmd, "orchestratorRequiresPrepareVm", null); + assertNull(cmd.isOrchestratorRequiresPrepareVm()); + } + + @Test + public void orchestratorRequiresPrepareVmReturnsValueWhenSet() { + ReflectionTestUtils.setField(cmd, "orchestratorRequiresPrepareVm", Boolean.TRUE); + assertTrue(cmd.isOrchestratorRequiresPrepareVm()); + } + + @Test + public void stateReturnsNullWhenUnset() { + ReflectionTestUtils.setField(cmd, "state", null); + assertNull(cmd.getState()); + } + + @Test + public void stateReturnsValueWhenSet() { + String state = "Active"; + ReflectionTestUtils.setField(cmd, "state", state); + assertEquals(state, cmd.getState()); + } + + @Test + public void detailsReturnsNullWhenUnset() { + ReflectionTestUtils.setField(cmd, "details", null); + assertTrue(MapUtils.isEmpty(cmd.getDetails())); + } + + @Test + public void detailsReturnsValueWhenSet() { + Map> details = new HashMap<>(); + Map inner = new HashMap<>(); + inner.put("vendor", "acme"); + details.put("details", inner); + ReflectionTestUtils.setField(cmd, "details", details); + assertTrue(MapUtils.isNotEmpty(cmd.getDetails())); + } + + @Test + public void isCleanupDetailsReturnsNullWhenUnset() { + ReflectionTestUtils.setField(cmd, "cleanupDetails", null); + assertNull(cmd.isCleanupDetails()); + } + + @Test + public void isCleanupDetailsReturnsValueWhenSet() { + ReflectionTestUtils.setField(cmd, "cleanupDetails", Boolean.TRUE); + assertTrue(cmd.isCleanupDetails()); + } + + @Test + public void executeSetsExtensionResponseWhenManagerSucceeds() { + Extension extension = mock(Extension.class); + ExtensionResponse response = mock(ExtensionResponse.class); + when(extensionsManager.updateExtension(cmd)).thenReturn(extension); + when(extensionsManager.createExtensionResponse(extension, EnumSet.of(ApiConstants.ExtensionDetails.all))) + .thenReturn(response); + + doNothing().when(cmd).setResponseObject(any()); + + cmd.execute(); + + verify(extensionsManager).updateExtension(cmd); + verify(extensionsManager).createExtensionResponse(extension, EnumSet.of(ApiConstants.ExtensionDetails.all)); + verify(response).setResponseName(cmd.getCommandName()); + verify(cmd).setResponseObject(response); + } + + @Test + public void executeThrowsServerApiExceptionWhenManagerFails() { + when(extensionsManager.updateExtension(cmd)) + .thenThrow(new ServerApiException(ApiErrorCode.INTERNAL_ERROR, "Failed to update extension")); + + try { + cmd.execute(); + fail("Expected ServerApiException"); + } catch (ServerApiException e) { + assertEquals(ApiErrorCode.INTERNAL_ERROR, e.getErrorCode()); + assertEquals("Failed to update extension", e.getDescription()); + } + } +} diff --git a/framework/extensions/src/test/java/org/apache/cloudstack/framework/extensions/command/ExtensionBaseCommandTest.java b/framework/extensions/src/test/java/org/apache/cloudstack/framework/extensions/command/ExtensionBaseCommandTest.java new file mode 100644 index 00000000000..ba95cc2da97 --- /dev/null +++ b/framework/extensions/src/test/java/org/apache/cloudstack/framework/extensions/command/ExtensionBaseCommandTest.java @@ -0,0 +1,78 @@ +// 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 static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertTrue; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + +import org.apache.cloudstack.extension.Extension; +import org.junit.Test; + +public class ExtensionBaseCommandTest { + + @Test + public void extensionIdReturnsCorrectValue() { + Extension extension = mock(Extension.class); + when(extension.getId()).thenReturn(12345L); + ExtensionBaseCommand command = new ExtensionBaseCommand(extension); + assertEquals(12345L, command.getExtensionId()); + } + + @Test + public void extensionNameReturnsCorrectValue() { + Extension extension = mock(Extension.class); + when(extension.getName()).thenReturn("TestExtension"); + ExtensionBaseCommand command = new ExtensionBaseCommand(extension); + assertEquals("TestExtension", command.getExtensionName()); + } + + @Test + public void extensionUserDefinedReturnsTrueWhenSet() { + Extension extension = mock(Extension.class); + when(extension.isUserDefined()).thenReturn(true); + ExtensionBaseCommand command = new ExtensionBaseCommand(extension); + assertTrue(command.isExtensionUserDefined()); + } + + @Test + public void extensionRelativePathReturnsCorrectValue() { + Extension extension = mock(Extension.class); + when(extension.getRelativePath()).thenReturn("/entry/point"); + ExtensionBaseCommand command = new ExtensionBaseCommand(extension); + assertEquals("/entry/point", command.getExtensionRelativePath()); + } + + @Test + public void extensionStateReturnsCorrectValue() { + Extension extension = mock(Extension.class); + Extension.State state = Extension.State.Enabled; + when(extension.getState()).thenReturn(state); + ExtensionBaseCommand command = new ExtensionBaseCommand(extension); + assertEquals(state, command.getExtensionState()); + } + + @Test + public void executeInSequenceReturnsFalse() { + Extension extension = mock(Extension.class); + ExtensionBaseCommand command = new ExtensionBaseCommand(extension); + assertFalse(command.executeInSequence()); + } +} diff --git a/framework/extensions/src/test/java/org/apache/cloudstack/framework/extensions/dao/ExtensionCustomActionDaoImplTest.java b/framework/extensions/src/test/java/org/apache/cloudstack/framework/extensions/dao/ExtensionCustomActionDaoImplTest.java new file mode 100644 index 00000000000..dccd1ebf1ed --- /dev/null +++ b/framework/extensions/src/test/java/org/apache/cloudstack/framework/extensions/dao/ExtensionCustomActionDaoImplTest.java @@ -0,0 +1,68 @@ +// 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.dao; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNull; +import static org.junit.Assert.assertTrue; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + +import java.util.List; + +import org.apache.cloudstack.framework.extensions.vo.ExtensionCustomActionVO; +import org.junit.Before; +import org.junit.Test; + +public class ExtensionCustomActionDaoImplTest { + + private ExtensionCustomActionDaoImpl dao; + + @Before + public void setUp() { + dao = mock(ExtensionCustomActionDaoImpl.class); + } + + @Test + public void findByNameAndExtensionIdReturnsNullWhenNoMatch() { + when(dao.findByNameAndExtensionId(1L, "nonexistent")).thenReturn(null); + assertNull(dao.findByNameAndExtensionId(1L, "nonexistent")); + } + + @Test + public void findByNameAndExtensionIdReturnsCorrectEntity() { + ExtensionCustomActionVO expected = new ExtensionCustomActionVO(); + expected.setName("actionName"); + expected.setExtensionId(1L); + when(dao.findByNameAndExtensionId(1L, "actionName")).thenReturn(expected); + assertEquals(expected, dao.findByNameAndExtensionId(1L, "actionName")); + } + + @Test + public void listIdsByExtensionIdReturnsEmptyListWhenNoMatch() { + when(dao.listIdsByExtensionId(999L)).thenReturn(List.of()); + assertTrue(dao.listIdsByExtensionId(999L).isEmpty()); + } + + @Test + public void listIdsByExtensionIdReturnsCorrectIds() { + List expectedIds = List.of(1L, 2L, 3L); + when(dao.listIdsByExtensionId(1L)).thenReturn(expectedIds); + assertEquals(expectedIds, dao.listIdsByExtensionId(1L)); + } +} diff --git a/framework/extensions/src/test/java/org/apache/cloudstack/framework/extensions/dao/ExtensionDaoImplTest.java b/framework/extensions/src/test/java/org/apache/cloudstack/framework/extensions/dao/ExtensionDaoImplTest.java new file mode 100644 index 00000000000..545feba0b3d --- /dev/null +++ b/framework/extensions/src/test/java/org/apache/cloudstack/framework/extensions/dao/ExtensionDaoImplTest.java @@ -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 org.apache.cloudstack.framework.extensions.dao; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNull; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + +import org.apache.cloudstack.framework.extensions.vo.ExtensionVO; +import org.junit.Before; +import org.junit.Test; + +public class ExtensionDaoImplTest { + + private ExtensionDaoImpl dao; + + @Before + public void setUp() { + dao = mock(ExtensionDaoImpl.class); + } + + @Test + public void findByNameReturnsNullWhenNoMatch() { + when(dao.findByName("nonexistent")).thenReturn(null); + assertNull(dao.findByName("nonexistent")); + } + + @Test + public void findByNameReturnsCorrectEntity() { + ExtensionVO expected = new ExtensionVO(); + expected.setName("extensionName"); + when(dao.findByName("extensionName")).thenReturn(expected); + assertEquals(expected, dao.findByName("extensionName")); + } +} diff --git a/framework/extensions/src/test/java/org/apache/cloudstack/framework/extensions/dao/ExtensionResourceMapDaoImplTest.java b/framework/extensions/src/test/java/org/apache/cloudstack/framework/extensions/dao/ExtensionResourceMapDaoImplTest.java new file mode 100644 index 00000000000..76a0175e757 --- /dev/null +++ b/framework/extensions/src/test/java/org/apache/cloudstack/framework/extensions/dao/ExtensionResourceMapDaoImplTest.java @@ -0,0 +1,86 @@ +// 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.dao; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNull; +import static org.junit.Assert.assertTrue; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + +import java.util.List; + +import org.apache.cloudstack.extension.ExtensionResourceMap; +import org.apache.cloudstack.framework.extensions.vo.ExtensionResourceMapVO; +import org.junit.Before; +import org.junit.Test; + +public class ExtensionResourceMapDaoImplTest { + + private ExtensionResourceMapDaoImpl dao; + + @Before + public void setUp() { + dao = mock(ExtensionResourceMapDaoImpl.class); + } + + @Test + public void listByExtensionIdReturnsEmptyListWhenNoMatch() { + when(dao.listByExtensionId(999L)).thenReturn(List.of()); + assertTrue(dao.listByExtensionId(999L).isEmpty()); + } + + @Test + public void listByExtensionIdReturnsCorrectEntities() { + ExtensionResourceMapVO entity1 = new ExtensionResourceMapVO(); + entity1.setExtensionId(1L); + ExtensionResourceMapVO entity2 = new ExtensionResourceMapVO(); + entity2.setExtensionId(1L); + List expected = List.of(entity1, entity2); + when(dao.listByExtensionId(1L)).thenReturn(expected); + assertEquals(expected, dao.listByExtensionId(1L)); + } + + @Test + public void findByResourceIdAndTypeReturnsNullWhenNoMatch() { + when(dao.findByResourceIdAndType(999L, ExtensionResourceMap.ResourceType.Cluster)).thenReturn(null); + assertNull(dao.findByResourceIdAndType(999L, ExtensionResourceMap.ResourceType.Cluster)); + } + + @Test + public void findByResourceIdAndTypeReturnsCorrectEntity() { + ExtensionResourceMapVO expected = new ExtensionResourceMapVO(); + expected.setResourceId(123L); + expected.setResourceType(ExtensionResourceMap.ResourceType.Cluster); + when(dao.findByResourceIdAndType(123L, ExtensionResourceMap.ResourceType.Cluster)).thenReturn(expected); + assertEquals(expected, dao.findByResourceIdAndType(123L, ExtensionResourceMap.ResourceType.Cluster)); + } + + @Test + public void listResourceIdsByExtensionIdAndTypeReturnsEmptyListWhenNoMatch() { + when(dao.listResourceIdsByExtensionIdAndType(999L, ExtensionResourceMap.ResourceType.Cluster)).thenReturn(List.of()); + assertTrue(dao.listResourceIdsByExtensionIdAndType(999L, ExtensionResourceMap.ResourceType.Cluster).isEmpty()); + } + + @Test + public void listResourceIdsByExtensionIdAndTypeReturnsCorrectIds() { + List expectedIds = List.of(1L, 2L, 3L); + when(dao.listResourceIdsByExtensionIdAndType(1L, ExtensionResourceMap.ResourceType.Cluster)).thenReturn(expectedIds); + assertEquals(expectedIds, dao.listResourceIdsByExtensionIdAndType(1L, ExtensionResourceMap.ResourceType.Cluster)); + } +} diff --git a/framework/extensions/src/test/java/org/apache/cloudstack/framework/extensions/manager/ExtensionsManagerImplTest.java b/framework/extensions/src/test/java/org/apache/cloudstack/framework/extensions/manager/ExtensionsManagerImplTest.java new file mode 100644 index 00000000000..fcceb16523e --- /dev/null +++ b/framework/extensions/src/test/java/org/apache/cloudstack/framework/extensions/manager/ExtensionsManagerImplTest.java @@ -0,0 +1,1885 @@ +// 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.manager; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertNull; +import static org.junit.Assert.assertThrows; +import static org.junit.Assert.assertTrue; +import static org.mockito.Mockito.any; +import static org.mockito.Mockito.anyBoolean; +import static org.mockito.Mockito.anyLong; +import static org.mockito.Mockito.anyString; +import static org.mockito.Mockito.atLeastOnce; +import static org.mockito.Mockito.doNothing; +import static org.mockito.Mockito.doReturn; +import static org.mockito.Mockito.doThrow; +import static org.mockito.Mockito.eq; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.mockStatic; +import static org.mockito.Mockito.never; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +import java.io.File; +import java.security.InvalidParameterException; +import java.util.Arrays; +import java.util.Collections; +import java.util.Date; +import java.util.EnumSet; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.UUID; + +import org.apache.cloudstack.acl.Role; +import org.apache.cloudstack.acl.RoleService; +import org.apache.cloudstack.acl.RoleType; +import org.apache.cloudstack.api.ApiConstants; +import org.apache.cloudstack.api.response.ExtensionCustomActionResponse; +import org.apache.cloudstack.api.response.ExtensionResponse; +import org.apache.cloudstack.context.CallContext; +import org.apache.cloudstack.extension.CustomActionResultResponse; +import org.apache.cloudstack.extension.Extension; +import org.apache.cloudstack.extension.ExtensionCustomAction; +import org.apache.cloudstack.extension.ExtensionResourceMap; +import org.apache.cloudstack.framework.extensions.api.AddCustomActionCmd; +import org.apache.cloudstack.framework.extensions.api.CreateExtensionCmd; +import org.apache.cloudstack.framework.extensions.api.DeleteCustomActionCmd; +import org.apache.cloudstack.framework.extensions.api.DeleteExtensionCmd; +import org.apache.cloudstack.framework.extensions.api.ListCustomActionCmd; +import org.apache.cloudstack.framework.extensions.api.ListExtensionsCmd; +import org.apache.cloudstack.framework.extensions.api.RegisterExtensionCmd; +import org.apache.cloudstack.framework.extensions.api.RunCustomActionCmd; +import org.apache.cloudstack.framework.extensions.api.UnregisterExtensionCmd; +import org.apache.cloudstack.framework.extensions.api.UpdateCustomActionCmd; +import org.apache.cloudstack.framework.extensions.api.UpdateExtensionCmd; +import org.apache.cloudstack.framework.extensions.command.CleanupExtensionFilesCommand; +import org.apache.cloudstack.framework.extensions.command.ExtensionServerActionBaseCommand; +import org.apache.cloudstack.framework.extensions.command.GetExtensionPathChecksumCommand; +import org.apache.cloudstack.framework.extensions.command.PrepareExtensionPathCommand; +import org.apache.cloudstack.framework.extensions.dao.ExtensionCustomActionDao; +import org.apache.cloudstack.framework.extensions.dao.ExtensionCustomActionDetailsDao; +import org.apache.cloudstack.framework.extensions.dao.ExtensionDao; +import org.apache.cloudstack.framework.extensions.dao.ExtensionDetailsDao; +import org.apache.cloudstack.framework.extensions.dao.ExtensionResourceMapDao; +import org.apache.cloudstack.framework.extensions.dao.ExtensionResourceMapDetailsDao; +import org.apache.cloudstack.framework.extensions.vo.ExtensionCustomActionDetailsVO; +import org.apache.cloudstack.framework.extensions.vo.ExtensionCustomActionVO; +import org.apache.cloudstack.framework.extensions.vo.ExtensionResourceMapVO; +import org.apache.cloudstack.framework.extensions.vo.ExtensionVO; +import org.apache.cloudstack.utils.identity.ManagementServerNode; +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.InjectMocks; +import org.mockito.Mock; +import org.mockito.MockedStatic; +import org.mockito.Mockito; +import org.mockito.MockitoAnnotations; +import org.mockito.Spy; +import org.mockito.junit.MockitoJUnitRunner; +import org.springframework.test.util.ReflectionTestUtils; + +import com.cloud.agent.AgentManager; +import com.cloud.agent.api.Answer; +import com.cloud.agent.api.Command; +import com.cloud.agent.api.RunCustomActionAnswer; +import com.cloud.alert.AlertManager; +import com.cloud.cluster.ClusterManager; +import com.cloud.cluster.ManagementServerHostVO; +import com.cloud.cluster.dao.ManagementServerHostDao; +import com.cloud.dc.ClusterVO; +import com.cloud.dc.dao.ClusterDao; +import com.cloud.exception.AgentUnavailableException; +import com.cloud.exception.InvalidParameterValueException; +import com.cloud.exception.OperationTimedoutException; +import com.cloud.host.dao.HostDao; +import com.cloud.host.dao.HostDetailsDao; +import com.cloud.hypervisor.ExternalProvisioner; +import com.cloud.hypervisor.Hypervisor; +import com.cloud.org.Cluster; +import com.cloud.serializer.GsonHelper; +import com.cloud.storage.dao.VMTemplateDao; +import com.cloud.user.Account; +import com.cloud.utils.Pair; +import com.cloud.utils.db.EntityManager; +import com.cloud.utils.db.SearchBuilder; +import com.cloud.utils.db.SearchCriteria; +import com.cloud.utils.exception.CloudRuntimeException; +import com.cloud.vm.VirtualMachine; +import com.cloud.vm.VirtualMachineManager; +import com.cloud.vm.VmDetailConstants; +import com.cloud.vm.dao.VMInstanceDao; + +@RunWith(MockitoJUnitRunner.class) +public class ExtensionsManagerImplTest { + + @Spy + @InjectMocks + private ExtensionsManagerImpl extensionsManager; + + @Mock + private ExtensionDao extensionDao; + @Mock + private ExtensionDetailsDao extensionDetailsDao; + @Mock + private ExtensionResourceMapDao extensionResourceMapDao; + @Mock + private ExtensionResourceMapDetailsDao extensionResourceMapDetailsDao; + @Mock + private ClusterDao clusterDao; + @Mock + private AgentManager agentMgr; + @Mock + private HostDao hostDao; + @Mock + private HostDetailsDao hostDetailsDao; + @Mock + private ExternalProvisioner externalProvisioner; + @Mock + private ExtensionCustomActionDao extensionCustomActionDao; + @Mock + private ExtensionCustomActionDetailsDao extensionCustomActionDetailsDao; + @Mock + private VMInstanceDao vmInstanceDao; + @Mock + private VirtualMachineManager virtualMachineManager; + @Mock + private EntityManager entityManager; + @Mock + private ManagementServerHostDao managementServerHostDao; + @Mock + private ClusterManager clusterManager; + @Mock + private AlertManager alertManager; + @Mock + private VMTemplateDao templateDao; + @Mock + private RoleService roleService; + + @Before + public void setUp() { + MockitoAnnotations.openMocks(this); + } + + @Test + public void getDefaultExtensionRelativePathReturnsExpectedPath() { + String name = "testExtension"; + String expected = Extension.getDirectoryName(name) + File.separator + Extension.getDirectoryName(name) + ".sh"; + String result = extensionsManager.getDefaultExtensionRelativePath(name); + assertEquals(expected, result); + } + + @Test + public void getValidatedExtensionRelativePathReturnsNormalizedPath() { + String name = "ext"; + String path = "ext/entry.sh"; + String result = extensionsManager.getValidatedExtensionRelativePath(name, path); + assertTrue(result.startsWith("ext/")); + } + + @Test(expected = InvalidParameterException.class) + public void getValidatedExtensionRelativePathThrowsForDeepPath() { + String name = "ext"; + String path = "ext/a/b/c/entry.sh"; + extensionsManager.getValidatedExtensionRelativePath(name, path); + } + + @Test + public void getResultFromAnswersStringReturnsSuccess() { + Extension ext = mock(Extension.class); + Answer[] answers = new Answer[]{new Answer(mock(PrepareExtensionPathCommand.class), true, "ok")}; + String json = GsonHelper.getGson().toJson(answers); + ManagementServerHostVO msHost = mock(ManagementServerHostVO.class); + Pair result = extensionsManager.getResultFromAnswersString(json, ext, msHost, "op"); + assertTrue(result.first()); + assertEquals("ok", result.second()); + } + + @Test + public void getResultFromAnswersStringReturnsFailure() { + Extension ext = mock(Extension.class); + Answer[] answers = new Answer[]{new Answer(mock(PrepareExtensionPathCommand.class), false, "fail")}; + String json = GsonHelper.getGson().toJson(answers); + ManagementServerHostVO msHost = mock(ManagementServerHostVO.class); + Pair result = extensionsManager.getResultFromAnswersString(json, ext, msHost, "op"); + assertFalse(result.first()); + assertEquals("fail", result.second()); + } + + @Test + public void prepareExtensionPathOnMSPeerReturnsTrueOnSuccess() { + Extension ext = mock(Extension.class); + ManagementServerHostVO msHost = mock(ManagementServerHostVO.class); + when(msHost.getMsid()).thenReturn(1L); + when(clusterManager.execute(anyString(), anyLong(), anyString(), eq(true))) + .thenReturn("answer"); + doReturn(new Pair<>(true, "ok")).when(extensionsManager).getResultFromAnswersString(anyString(), eq(ext), eq(msHost), anyString()); + assertTrue(extensionsManager.prepareExtensionPathOnMSPeer(ext, msHost)); + } + + @Test + public void prepareExtensionPathOnCurrentServerReturnsSuccess() { + doNothing().when(externalProvisioner).prepareExtensionPath(anyString(), anyBoolean(), anyString()); + Pair result = extensionsManager.prepareExtensionPathOnCurrentServer("name", true, "entry"); + assertTrue(result.first()); + assertNull(result.second()); + } + + @Test + public void cleanupExtensionFilesOnMSPeerReturnsTrueOnSuccess() { + Extension ext = mock(Extension.class); + ManagementServerHostVO msHost = mock(ManagementServerHostVO.class); + when(msHost.getMsid()).thenReturn(1L); + when(clusterManager.execute(anyString(), anyLong(), anyString(), eq(true))) + .thenReturn("answer"); + doReturn(new Pair<>(true, "ok")).when(extensionsManager).getResultFromAnswersString(anyString(), eq(ext), eq(msHost), anyString()); + assertTrue(extensionsManager.cleanupExtensionFilesOnMSPeer(ext, msHost)); + } + + @Test + public void cleanupExtensionFilesOnCurrentServerReturnsSuccess() { + Pair result = extensionsManager.cleanupExtensionFilesOnCurrentServer("name", "entry"); + assertTrue(result.first()); + } + + @Test + public void getParametersListFromMapReturnsEmptyListForNull() { + List result = extensionsManager.getParametersListFromMap("action", null); + assertTrue(result.isEmpty()); + } + + @Test(expected = InvalidParameterValueException.class) + public void unregisterExtensionWithClusterThrowsIfClusterNotFound() { + when(clusterDao.findByUuid(anyString())).thenReturn(null); + extensionsManager.unregisterExtensionWithCluster("uuid", 1L); + } + + @Test + public void getExtensionFromResourceReturnsNullIfEntityNotFound() { + when(entityManager.findByUuid(any(), anyString())).thenReturn(null); + assertNull(extensionsManager.getExtensionFromResource(ExtensionCustomAction.ResourceType.VirtualMachine, "uuid")); + } + + @Test + public void getActionMessageReturnsDefaultOnBlank() { + ExtensionCustomAction action = mock(ExtensionCustomAction.class); + Extension ext = mock(Extension.class); + when(action.getSuccessMessage()).thenReturn(null); + String msg = extensionsManager.getActionMessage(true, action, ext, ExtensionCustomAction.ResourceType.VirtualMachine, null); + assertTrue(msg.contains("Successfully completed")); + } + + @Test + public void getActionMessageReturnsDefaultMessageForSuccessWithoutCustomMessage() { + ExtensionCustomAction action = mock(ExtensionCustomAction.class); + Extension extension = mock(Extension.class); + when(action.getSuccessMessage()).thenReturn(null); + + String result = extensionsManager.getActionMessage(true, action, extension, ExtensionCustomAction.ResourceType.VirtualMachine, null); + + assertTrue(result.contains("Successfully completed")); + } + + @Test + public void getActionMessageReturnsCustomSuccessMessage() { + ExtensionCustomAction action = mock(ExtensionCustomAction.class); + when(action.getName()).thenReturn("actionName"); + Extension extension = mock(Extension.class); + when(extension.getName()).thenReturn("extension"); + when(action.getSuccessMessage()).thenReturn("Custom success message"); + String result = extensionsManager.getActionMessage(true, action, extension, ExtensionCustomAction.ResourceType.VirtualMachine, null); + assertEquals("Custom success message", result); + } + + @Test + public void getActionMessageReturnsDefaultMessageForFailureWithoutCustomMessage() { + ExtensionCustomAction action = mock(ExtensionCustomAction.class); + Extension extension = mock(Extension.class); + when(action.getErrorMessage()).thenReturn(null); + + String result = extensionsManager.getActionMessage(false, action, extension, ExtensionCustomAction.ResourceType.VirtualMachine, null); + + assertTrue(result.contains("Failed to complete")); + } + + @Test + public void getActionMessageReturnsCustomFailureMessage() { + ExtensionCustomAction action = mock(ExtensionCustomAction.class); + when(action.getName()).thenReturn("actionName"); + Extension extension = mock(Extension.class); + when(extension.getName()).thenReturn("extension"); + when(action.getErrorMessage()).thenReturn("Custom failure message"); + String result = extensionsManager.getActionMessage(false, action, extension, ExtensionCustomAction.ResourceType.VirtualMachine, null); + assertEquals("Custom failure message", result); + } + + @Test + public void getActionMessageHandlesNullActionMessage() { + ExtensionCustomAction action = mock(ExtensionCustomAction.class); + when(action.getSuccessMessage()).thenReturn(null); + Extension extension = mock(Extension.class); + String result = extensionsManager.getActionMessage(true, action, extension, ExtensionCustomAction.ResourceType.VirtualMachine, null); + assertTrue(result.contains("Successfully completed")); + } + + @Test + public void getFilteredExternalDetailsReturnsFilteredMap() { + Map details = new HashMap<>(); + String key = "detail.key"; + details.put(VmDetailConstants.EXTERNAL_DETAIL_PREFIX + key, "value"); + details.put("other.key", "value2"); + Map filtered = extensionsManager.getFilteredExternalDetails(details); + assertTrue(filtered.containsKey(key)); + assertFalse(filtered.containsKey("other.key")); + } + + @Test + public void sendExtensionPathNotReadyAlertCallsAlertManager() { + Extension ext = mock(Extension.class); + when(ext.getState()).thenReturn(Extension.State.Enabled); + extensionsManager.sendExtensionPathNotReadyAlert(ext); + verify(alertManager, atLeastOnce()).sendAlert(eq(AlertManager.AlertType.ALERT_TYPE_EXTENSION_PATH_NOT_READY), + anyLong(), anyLong(), anyString(), anyString()); + } + + @Test + public void sendExtensionPathNotReadyAlertDoesNotCallsAlertManager() { + Extension ext = mock(Extension.class); + when(ext.getState()).thenReturn(Extension.State.Disabled); + extensionsManager.sendExtensionPathNotReadyAlert(ext); + verify(alertManager, never()).sendAlert(eq(AlertManager.AlertType.ALERT_TYPE_EXTENSION_PATH_NOT_READY), + anyLong(), anyLong(), anyString(), anyString()); + } + + @Test + public void updateExtensionPathReadyUpdatesWhenStateDiffers() { + Extension ext = mock(Extension.class); + when(ext.getId()).thenReturn(1L); + when(ext.isPathReady()).thenReturn(false); + ExtensionVO vo = mock(ExtensionVO.class); + when(extensionDao.createForUpdate(1L)).thenReturn(vo); + when(extensionDao.update(1L, vo)).thenReturn(true); + extensionsManager.updateExtensionPathReady(ext, true); + verify(extensionDao).update(1L, vo); + } + + @Test + public void disableExtensionUpdatesState() { + ExtensionVO vo = mock(ExtensionVO.class); + when(extensionDao.createForUpdate(1L)).thenReturn(vo); + when(extensionDao.update(1L, vo)).thenReturn(true); + extensionsManager.disableExtension(1L); + verify(extensionDao).update(1L, vo); + } + + @Test + public void getExtensionFromResourceReturnsExtensionForValidResource() { + VirtualMachine vm = mock(VirtualMachine.class); + when(entityManager.findByUuid(eq(VirtualMachine.class), eq("vm-uuid"))).thenReturn(vm); + when(virtualMachineManager.findClusterAndHostIdForVm(vm, false)).thenReturn(new Pair<>(1L, 1L)); + ExtensionResourceMapVO mapVO = mock(ExtensionResourceMapVO.class); + when(mapVO.getExtensionId()).thenReturn(100L); + when(extensionResourceMapDao.findByResourceIdAndType(1L, ExtensionResourceMap.ResourceType.Cluster)).thenReturn(mapVO); + ExtensionVO extension = mock(ExtensionVO.class); + when(extensionDao.findById(100L)).thenReturn(extension); + + Extension result = extensionsManager.getExtensionFromResource(ExtensionCustomAction.ResourceType.VirtualMachine, "vm-uuid"); + + assertEquals(extension, result); + } + + @Test + public void getExtensionFromResourceReturnsNullForInvalidResourceUuid() { + when(entityManager.findByUuid(eq(VirtualMachine.class), eq("invalid-uuid"))).thenReturn(null); + + Extension result = extensionsManager.getExtensionFromResource(ExtensionCustomAction.ResourceType.VirtualMachine, "invalid-uuid"); + + assertNull(result); + } + + @Test + public void getExtensionFromResourceReturnsNullForMissingClusterMapping() { + VirtualMachine vm = mock(VirtualMachine.class); + when(entityManager.findByUuid(eq(VirtualMachine.class), eq("vm-uuid"))).thenReturn(vm); + when(virtualMachineManager.findClusterAndHostIdForVm(vm, false)).thenReturn(new Pair<>(null, null)); + + Extension result = extensionsManager.getExtensionFromResource(ExtensionCustomAction.ResourceType.VirtualMachine, "vm-uuid"); + + assertNull(result); + } + + @Test + public void getExtensionFromResourceReturnsNullForMissingExtensionMapping() { + VirtualMachine vm = mock(VirtualMachine.class); + when(entityManager.findByUuid(eq(VirtualMachine.class), eq("vm-uuid"))).thenReturn(vm); + when(virtualMachineManager.findClusterAndHostIdForVm(vm, false)).thenReturn(new Pair<>(1L, 1L)); + when(extensionResourceMapDao.findByResourceIdAndType(1L, ExtensionResourceMap.ResourceType.Cluster)).thenReturn(null); + + Extension result = extensionsManager.getExtensionFromResource(ExtensionCustomAction.ResourceType.VirtualMachine, "vm-uuid"); + + assertNull(result); + } + + @Test + public void updateExtensionPathReadyUpdatesStateWhenNotReady() { + Extension ext = mock(Extension.class); + when(ext.getId()).thenReturn(1L); + when(ext.isPathReady()).thenReturn(true); + ExtensionVO vo = mock(ExtensionVO.class); + when(extensionDao.createForUpdate(1L)).thenReturn(vo); + when(extensionDao.update(1L, vo)).thenReturn(true); + + extensionsManager.updateExtensionPathReady(ext, false); + + verify(extensionDao).update(1L, vo); + } + + @Test + public void updateExtensionPathReadyDoesNotUpdateWhenStateUnchanged() { + Extension ext = mock(Extension.class); + when(ext.isPathReady()).thenReturn(true); + extensionsManager.updateExtensionPathReady(ext, true); + verify(extensionDao, never()).update(anyLong(), any()); + } + + @Test + public void disableExtensionChangesStateToDisabled() { + ExtensionVO vo = mock(ExtensionVO.class); + when(extensionDao.createForUpdate(1L)).thenReturn(vo); + when(extensionDao.update(1L, vo)).thenReturn(true); + + extensionsManager.disableExtension(1L); + + verify(vo).setState(Extension.State.Disabled); + verify(extensionDao).update(1L, vo); + } + + @Test + public void updateAllExtensionHostsRemovesHostsSuccessfully() throws OperationTimedoutException, AgentUnavailableException { + Extension extension = mock(Extension.class); + when(extension.getId()).thenReturn(1L); + Long clusterId = 100L; + Long hostId = 200L; + when(hostDao.listIdsByClusterId(clusterId)).thenReturn(List.of(hostId)); + extensionsManager.updateAllExtensionHosts(extension, clusterId, true); + verify(agentMgr).send(eq(hostId), any(Command.class)); + } + + @Test + public void updateAllExtensionHostsAddsHostsSuccessfully() throws OperationTimedoutException, AgentUnavailableException { + Extension extension = mock(Extension.class); + when(extension.getId()).thenReturn(1L); + Long clusterId = 100L; + Long hostId = 200L; + when(hostDao.listIdsByClusterId(clusterId)).thenReturn(List.of(hostId)); + extensionsManager.updateAllExtensionHosts(extension, clusterId, false); + verify(agentMgr).send(eq(hostId), any(Command.class)); + } + + @Test + public void updateAllExtensionHostsHandlesEmptyHostListGracefully() throws OperationTimedoutException, AgentUnavailableException { + Extension extension = mock(Extension.class); + Long clusterId = 100L; + when(hostDao.listIdsByClusterId(clusterId)).thenReturn(Collections.emptyList()); + extensionsManager.updateAllExtensionHosts(extension, clusterId, false); + verify(agentMgr, never()).send(anyLong(), any(Command.class)); + } + + @Test + public void updateAllExtensionHostsHandlesNullClusterId() throws OperationTimedoutException, AgentUnavailableException { + Extension extension = mock(Extension.class); + when(extension.getId()).thenReturn(1L); + when(extensionResourceMapDao.listResourceIdsByExtensionIdAndType(eq(1L), any())).thenReturn(Collections.emptyList()); + extensionsManager.updateAllExtensionHosts(extension, null, false); + verify(agentMgr, never()).send(anyLong(), any(Command.class)); + } + + @Test + public void getExternalAccessDetailsReturnsMapWithHostAndExtension() { + Map map = new HashMap<>(); + map.put("external.detail.key", "value"); + long hostId = 1L; + ExtensionResourceMap resourceMap = mock(ExtensionResourceMap.class); + when(resourceMap.getId()).thenReturn(2L); + when(resourceMap.getExtensionId()).thenReturn(3L); + when(hostDetailsDao.findDetails(hostId)).thenReturn(null); + when(extensionResourceMapDetailsDao.listDetailsKeyPairs(2L, true)).thenReturn(Collections.emptyMap()); + when(extensionDetailsDao.listDetailsKeyPairs(3L, true)).thenReturn(map); + Map> result = extensionsManager.getExternalAccessDetails(map, hostId, resourceMap); + assertTrue(result.containsKey(ApiConstants.ACTION)); + assertFalse(result.containsKey(ApiConstants.HOST)); + assertFalse(result.containsKey(ApiConstants.RESOURCE_MAP)); + assertTrue(result.containsKey(ApiConstants.EXTENSION)); + } + + @Test(expected = CloudRuntimeException.class) + public void checkOrchestratorTemplatesThrowsIfTemplatesExist() { + when(templateDao.listIdsByExtensionId(1L)).thenReturn(Arrays.asList(1L, 2L)); + extensionsManager.checkOrchestratorTemplates(1L); + } + + @Test + public void getExtensionsPathReturnsProvisionerPath() { + when(externalProvisioner.getExtensionsPath()).thenReturn("/tmp/extensions"); + assertEquals("/tmp/extensions", extensionsManager.getExtensionsPath()); + } + + @Test + public void getExtensionIdForClusterReturnsNullIfNoMap() { + when(extensionResourceMapDao.findByResourceIdAndType(anyLong(), any())).thenReturn(null); + assertNull(extensionsManager.getExtensionIdForCluster(1L)); + } + + @Test + public void getExtensionIdForClusterReturnsIdIfMapExists() { + ExtensionResourceMapVO map = mock(ExtensionResourceMapVO.class); + when(map.getExtensionId()).thenReturn(5L); + when(extensionResourceMapDao.findByResourceIdAndType(anyLong(), any())).thenReturn(map); + assertEquals(Long.valueOf(5L), extensionsManager.getExtensionIdForCluster(1L)); + } + + @Test + public void getExtensionReturnsExtension() { + ExtensionVO ext = mock(ExtensionVO.class); + when(extensionDao.findById(1L)).thenReturn(ext); + assertEquals(ext, extensionsManager.getExtension(1L)); + } + + @Test + public void getExtensionForClusterReturnsNullIfNoId() { + when(extensionResourceMapDao.findByResourceIdAndType(anyLong(), any())).thenReturn(null); + assertNull(extensionsManager.getExtensionForCluster(1L)); + } + + @Test + public void getExtensionForClusterReturnsExtensionIfIdExists() { + ExtensionResourceMapVO map = mock(ExtensionResourceMapVO.class); + when(map.getExtensionId()).thenReturn(5L); + when(extensionResourceMapDao.findByResourceIdAndType(anyLong(), any())).thenReturn(map); + ExtensionVO ext = mock(ExtensionVO.class); + when(extensionDao.findById(5L)).thenReturn(ext); + assertEquals(ext, extensionsManager.getExtensionForCluster(1L)); + } + + @Test + public void checkExtensionPathSyncUpdatesReadyWhenChecksumIsBlank() { + Extension ext = mock(Extension.class); + when(ext.getName()).thenReturn("ext"); + when(ext.getRelativePath()).thenReturn("entry.sh"); + when(externalProvisioner.getChecksumForExtensionPath("ext", "entry.sh")).thenReturn(""); + + extensionsManager.checkExtensionPathState(ext, Collections.emptyList()); + + verify(extensionsManager).updateExtensionPathReady(ext, false); + } + + @Test + public void checkExtensionPathSyncUpdatesReadyWhenNoHostsProvided() { + ExtensionVO ext = mock(ExtensionVO.class); + when(ext.getName()).thenReturn("ext"); + when(ext.getRelativePath()).thenReturn("entry.sh"); + when(externalProvisioner.getChecksumForExtensionPath("ext", "entry.sh")).thenReturn("checksum123"); + when(extensionDao.createForUpdate(anyLong())).thenReturn(ext); + extensionsManager.checkExtensionPathState(ext, Collections.emptyList()); + verify(extensionsManager).updateExtensionPathReady(ext, true); + } + + @Test + public void checkExtensionPathSyncUpdatesReadyWhenChecksumsMatchAcrossHosts() { + ExtensionVO ext = mock(ExtensionVO.class); + when(ext.getName()).thenReturn("ext"); + when(ext.getRelativePath()).thenReturn("entry.sh"); + when(externalProvisioner.getChecksumForExtensionPath("ext", "entry.sh")).thenReturn("checksum123"); + when(extensionDao.createForUpdate(anyLong())).thenReturn(ext); + ManagementServerHostVO msHost = mock(ManagementServerHostVO.class); + doReturn(new Pair<>(true, "checksum123")).when(extensionsManager).getChecksumForExtensionPathOnMSPeer(ext, msHost); + extensionsManager.checkExtensionPathState(ext, Collections.singletonList(msHost)); + verify(extensionsManager).updateExtensionPathReady(ext, true); + } + + @Test + public void checkExtensionPathStateUpdatesNotReadyWhenChecksumsDifferAcrossHosts() { + Extension ext = mock(Extension.class); + when(ext.getName()).thenReturn("ext"); + when(ext.getRelativePath()).thenReturn("entry.sh"); + when(externalProvisioner.getChecksumForExtensionPath("ext", "entry.sh")).thenReturn("checksum123"); + ManagementServerHostVO msHost = mock(ManagementServerHostVO.class); + when(msHost.getMsid()).thenReturn(1L); + doReturn(new Pair<>(true, "checksum456")).when(extensionsManager).getChecksumForExtensionPathOnMSPeer(ext, msHost); + extensionsManager.checkExtensionPathState(ext, Collections.singletonList(msHost)); + verify(extensionsManager).updateExtensionPathReady(ext, false); + } + + @Test + public void checkExtensionPathStateUpdatesNotReadyWhenPeerChecksumFails() { + Extension ext = mock(Extension.class); + when(ext.getName()).thenReturn("ext"); + when(ext.getRelativePath()).thenReturn("entry.sh"); + when(externalProvisioner.getChecksumForExtensionPath("ext", "entry.sh")).thenReturn("checksum123"); + + ManagementServerHostVO msHost = mock(ManagementServerHostVO.class); + when(msHost.getMsid()).thenReturn(1L); + doReturn(new Pair<>(false, null)).when(extensionsManager).getChecksumForExtensionPathOnMSPeer(ext, msHost); + + extensionsManager.checkExtensionPathState(ext, Collections.singletonList(msHost)); + + verify(extensionsManager).updateExtensionPathReady(ext, false); + } + + @Test + public void testCreateExtension_Success() { + CreateExtensionCmd cmd = mock(CreateExtensionCmd.class); + when(cmd.getName()).thenReturn("ext1"); + when(cmd.getDescription()).thenReturn("desc"); + when(cmd.getType()).thenReturn("Orchestrator"); + when(cmd.getPath()).thenReturn(null); + when(cmd.isOrchestratorRequiresPrepareVm()).thenReturn(null); + when(cmd.getState()).thenReturn(null); + when(extensionDao.findByName("ext1")).thenReturn(null); + when(extensionDao.persist(any())).thenAnswer(inv -> { + ExtensionVO extensionVO = inv.getArgument(0); + ReflectionTestUtils.setField(extensionVO, "id", 1L); + return extensionVO; + }); + when(managementServerHostDao.listBy(any())).thenReturn(Collections.emptyList()); + + Extension ext = extensionsManager.createExtension(cmd); + + assertEquals("ext1", ext.getName()); + verify(extensionDao).persist(any()); + } + + @Test + public void testCreateExtension_DuplicateName() { + CreateExtensionCmd cmd = mock(CreateExtensionCmd.class); + when(cmd.getName()).thenReturn("ext1"); + when(extensionDao.findByName("ext1")).thenReturn(mock(ExtensionVO.class)); + + assertThrows(CloudRuntimeException.class, () -> extensionsManager.createExtension(cmd)); + } + + @Test + public void prepareExtensionPathAcrossServersReturnsTrueWhenAllServersSucceed() { + Extension ext = mock(Extension.class); + when(ext.getName()).thenReturn("ext"); + when(ext.isUserDefined()).thenReturn(true); + when(ext.getRelativePath()).thenReturn("entry.sh"); + when(ext.getId()).thenReturn(1L); + when(ext.isPathReady()).thenReturn(false); + + ManagementServerHostVO msHost1 = mock(ManagementServerHostVO.class); + ManagementServerHostVO msHost2 = mock(ManagementServerHostVO.class); + when(msHost1.getMsid()).thenReturn(100L); + when(msHost2.getMsid()).thenReturn(200L); + + when(managementServerHostDao.listBy(any())).thenReturn(Arrays.asList(msHost1, msHost2)); + + try (MockedStatic managementServerNodeMockedStatic = mockStatic(ManagementServerNode.class)) { + managementServerNodeMockedStatic.when(ManagementServerNode::getManagementServerId).thenReturn(101L); + doReturn(new Pair<>(true, "ok")).when(extensionsManager).prepareExtensionPathOnCurrentServer(anyString(), anyBoolean(), anyString()); + doReturn(true).when(extensionsManager).prepareExtensionPathOnMSPeer(eq(ext), eq(msHost2)); + + // Simulate current server is msHost1 + when(msHost1.getMsid()).thenReturn(101L); + + // Extension entry point ready state should be updated + ExtensionVO updateExt = mock(ExtensionVO.class); + when(extensionDao.createForUpdate(1L)).thenReturn(updateExt); + when(extensionDao.update(1L, updateExt)).thenReturn(true); + + boolean result = extensionsManager.prepareExtensionPathAcrossServers(ext); + assertTrue(result); + verify(extensionDao).update(1L, updateExt); + } + } + + @Test + public void prepareExtensionPathAcrossServersReturnsFalseWhenAnyServerFails() { + Extension ext = mock(Extension.class); + when(ext.getName()).thenReturn("ext"); + when(ext.isUserDefined()).thenReturn(true); + when(ext.getRelativePath()).thenReturn("entry.sh"); + when(ext.getId()).thenReturn(1L); + when(ext.isPathReady()).thenReturn(true); + + ManagementServerHostVO msHost1 = mock(ManagementServerHostVO.class); + ManagementServerHostVO msHost2 = mock(ManagementServerHostVO.class); + when(msHost1.getMsid()).thenReturn(101L); + when(msHost2.getMsid()).thenReturn(200L); + + when(managementServerHostDao.listBy(any())).thenReturn(Arrays.asList(msHost1, msHost2)); + + try (MockedStatic managementServerNodeMockedStatic = mockStatic(ManagementServerNode.class)) { + managementServerNodeMockedStatic.when(ManagementServerNode::getManagementServerId).thenReturn(101L); + doReturn(new Pair<>(true, "ok")).when(extensionsManager).prepareExtensionPathOnCurrentServer(anyString(), anyBoolean(), anyString()); + doReturn(false).when(extensionsManager).prepareExtensionPathOnMSPeer(eq(ext), eq(msHost2)); + + ExtensionVO updateExt = mock(ExtensionVO.class); + when(extensionDao.createForUpdate(1L)).thenReturn(updateExt); + when(extensionDao.update(1L, updateExt)).thenReturn(true); + + boolean result = extensionsManager.prepareExtensionPathAcrossServers(ext); + assertFalse(result); + verify(extensionDao).update(1L, updateExt); + } + } + + @Test + public void prepareExtensionPathAcrossServersDoesNotUpdateIfStateUnchanged() { + Extension ext = mock(Extension.class); + when(ext.getName()).thenReturn("ext"); + when(ext.isUserDefined()).thenReturn(true); + when(ext.getRelativePath()).thenReturn("entry.sh"); + when(ext.isPathReady()).thenReturn(true); + + ManagementServerHostVO msHost = mock(ManagementServerHostVO.class); + when(msHost.getMsid()).thenReturn(101L); + + when(managementServerHostDao.listBy(any())).thenReturn(Collections.singletonList(msHost)); + + try (MockedStatic managementServerNodeMockedStatic = mockStatic(ManagementServerNode.class)) { + managementServerNodeMockedStatic.when(ManagementServerNode::getManagementServerId).thenReturn(101L); + doReturn(new Pair<>(true, "ok")).when(extensionsManager).prepareExtensionPathOnCurrentServer(anyString(), anyBoolean(), anyString()); + + boolean result = extensionsManager.prepareExtensionPathAcrossServers(ext); + assertTrue(result); + verify(extensionDao, never()).update(anyLong(), any()); + } + } + + @Test + public void testListExtensionsReturnsResponses() { + ListExtensionsCmd cmd = mock(ListExtensionsCmd.class); + when(cmd.getExtensionId()).thenReturn(null); + when(cmd.getName()).thenReturn(null); + when(cmd.getKeyword()).thenReturn(null); + when(cmd.getStartIndex()).thenReturn(0L); + when(cmd.getPageSizeVal()).thenReturn(10L); + when(cmd.getDetails()).thenReturn(null); + + ExtensionVO ext1 = mock(ExtensionVO.class); + ExtensionVO ext2 = mock(ExtensionVO.class); + List extList = Arrays.asList(ext1, ext2); + SearchBuilder sb = mock(SearchBuilder.class); + when(sb.create()).thenReturn(mock(SearchCriteria.class)); + when(sb.entity()).thenReturn(mock(ExtensionVO.class)); + when(extensionDao.createSearchBuilder()).thenReturn(sb); + when(extensionDao.searchAndCount(any(), any())).thenReturn(new Pair<>(extList, 2)); + + // Spy createExtensionResponse to return a dummy response + ExtensionResponse resp1 = mock(ExtensionResponse.class); + ExtensionResponse resp2 = mock(ExtensionResponse.class); + doReturn(resp1).when(extensionsManager).createExtensionResponse(eq(ext1), any()); + doReturn(resp2).when(extensionsManager).createExtensionResponse(eq(ext2), any()); + + List result = extensionsManager.listExtensions(cmd); + + assertEquals(2, result.size()); + assertTrue(result.contains(resp1)); + assertTrue(result.contains(resp2)); + } + + @Test + public void testListExtensionsWithId() { + ListExtensionsCmd cmd = mock(ListExtensionsCmd.class); + when(cmd.getExtensionId()).thenReturn(42L); + when(cmd.getName()).thenReturn(null); + when(cmd.getKeyword()).thenReturn(null); + when(cmd.getStartIndex()).thenReturn(0L); + when(cmd.getPageSizeVal()).thenReturn(10L); + when(cmd.getDetails()).thenReturn(null); + + ExtensionVO ext = mock(ExtensionVO.class); + SearchBuilder sb = mock(SearchBuilder.class); + when(sb.create()).thenReturn(mock(SearchCriteria.class)); + when(sb.entity()).thenReturn(mock(ExtensionVO.class)); + when(extensionDao.createSearchBuilder()).thenReturn(sb); + when(extensionDao.searchAndCount(any(), any())).thenReturn(new Pair<>(Collections.singletonList(ext), 1)); + ExtensionResponse resp = mock(ExtensionResponse.class); + doReturn(resp).when(extensionsManager).createExtensionResponse(eq(ext), any()); + + List result = extensionsManager.listExtensions(cmd); + + assertEquals(1, result.size()); + assertEquals(resp, result.get(0)); + } + + @Test + public void testListExtensionsWithNameAndKeyword() { + ListExtensionsCmd cmd = mock(ListExtensionsCmd.class); + when(cmd.getExtensionId()).thenReturn(null); + when(cmd.getName()).thenReturn("testName"); + when(cmd.getKeyword()).thenReturn("key"); + when(cmd.getStartIndex()).thenReturn(0L); + when(cmd.getPageSizeVal()).thenReturn(10L); + when(cmd.getDetails()).thenReturn(null); + + ExtensionVO ext = mock(ExtensionVO.class); + SearchBuilder sb = mock(SearchBuilder.class); + when(sb.create()).thenReturn(mock(SearchCriteria.class)); + when(sb.entity()).thenReturn(mock(ExtensionVO.class)); + when(extensionDao.createSearchBuilder()).thenReturn(sb); + when(extensionDao.searchAndCount(any(), any())).thenReturn(new Pair<>(Collections.singletonList(ext), 1)); + ExtensionResponse resp = mock(ExtensionResponse.class); + doReturn(resp).when(extensionsManager).createExtensionResponse(eq(ext), any()); + + List result = extensionsManager.listExtensions(cmd); + + assertEquals(1, result.size()); + assertEquals(resp, result.get(0)); + } + + @Test + public void testUpdateExtension_SuccessfulDescriptionUpdate() { + UpdateExtensionCmd cmd = mock(UpdateExtensionCmd.class); + when(cmd.getId()).thenReturn(1L); + when(cmd.getDescription()).thenReturn("new desc"); + when(cmd.isOrchestratorRequiresPrepareVm()).thenReturn(null); + when(cmd.getState()).thenReturn(null); + when(cmd.getDetails()).thenReturn(null); + when(cmd.isCleanupDetails()).thenReturn(false); + + ExtensionVO ext = mock(ExtensionVO.class); + when(ext.getDescription()).thenReturn("old desc"); + when(extensionDao.findById(1L)).thenReturn(ext); + when(extensionDao.update(1L, ext)).thenReturn(true); + + Extension result = extensionsManager.updateExtension(cmd); + + assertEquals(ext, result); + verify(ext).setDescription("new desc"); + verify(extensionDao, atLeastOnce()).update(1L, ext); + } + + @Test(expected = InvalidParameterValueException.class) + public void testUpdateExtension_NotFound() { + UpdateExtensionCmd cmd = mock(UpdateExtensionCmd.class); + when(cmd.getId()).thenReturn(2L); + when(extensionDao.findById(2L)).thenReturn(null); + + extensionsManager.updateExtension(cmd); + } + + @Test(expected = InvalidParameterValueException.class) + public void testUpdateExtension_InvalidOrchestratorFlag() { + UpdateExtensionCmd cmd = mock(UpdateExtensionCmd.class); + when(cmd.getId()).thenReturn(3L); + when(cmd.isOrchestratorRequiresPrepareVm()).thenReturn(true); + + ExtensionVO ext = mock(ExtensionVO.class); + when(ext.getType()).thenReturn(null); + when(extensionDao.findById(3L)).thenReturn(ext); + + extensionsManager.updateExtension(cmd); + } + + @Test(expected = CloudRuntimeException.class) + public void testUpdateExtension_UpdateFails() { + UpdateExtensionCmd cmd = mock(UpdateExtensionCmd.class); + when(cmd.getId()).thenReturn(4L); + when(cmd.getDescription()).thenReturn("desc"); + when(cmd.isOrchestratorRequiresPrepareVm()).thenReturn(null); + when(cmd.getState()).thenReturn(null); + when(cmd.getDetails()).thenReturn(null); + when(cmd.isCleanupDetails()).thenReturn(false); + + ExtensionVO ext = mock(ExtensionVO.class); + when(ext.getDescription()).thenReturn("old"); + when(extensionDao.findById(4L)).thenReturn(ext); + when(extensionDao.update(4L, ext)).thenReturn(false); + + extensionsManager.updateExtension(cmd); + } + + @Test(expected = InvalidParameterValueException.class) + public void testUpdateExtension_InvalidState() { + UpdateExtensionCmd cmd = mock(UpdateExtensionCmd.class); + when(cmd.getId()).thenReturn(5L); + when(cmd.getState()).thenReturn("NonExistentState"); + + ExtensionVO ext = mock(ExtensionVO.class); + when(ext.getType()).thenReturn(Extension.Type.Orchestrator); + when(ext.getState()).thenReturn(Extension.State.Enabled); + when(extensionDao.findById(5L)).thenReturn(ext); + + extensionsManager.updateExtension(cmd); + } + + @Test + public void updateExtensionsDetails_SavesDetails_WhenDetailsProvided() { + long extensionId = 10L; + Map details = Map.of("foo", "bar", "baz", "qux"); + extensionsManager.updateExtensionsDetails(false, details, null, extensionId); + verify(extensionDetailsDao).saveDetails(any()); + } + + @Test + public void updateExtensionsDetails_DoesNothing_WhenDetailsAndCleanupAreNull() { + long extensionId = 11L; + extensionsManager.updateExtensionsDetails(null, null, null, extensionId); + verify(extensionDetailsDao, never()).removeDetails(anyLong()); + verify(extensionDetailsDao, never()).saveDetails(any()); + } + + @Test + public void updateExtensionsDetails_RemovesDetailsOnly_WhenCleanupIsTrue() { + long extensionId = 12L; + extensionsManager.updateExtensionsDetails(true, null, null, extensionId); + verify(extensionDetailsDao).removeDetails(extensionId); + verify(extensionDetailsDao, never()).saveDetails(any()); + } + + @Test + public void updateExtensionsDetails_PersistsOrchestratorFlag_WhenFlagIsNotNull() { + long extensionId = 13L; + extensionsManager.updateExtensionsDetails(false, null, true, extensionId); + verify(extensionDetailsDao).persist(any()); + } + + @Test(expected = CloudRuntimeException.class) + public void updateExtensionsDetails_ThrowsException_WhenPersistFails() { + long extensionId = 14L; + Map details = Map.of("foo", "bar"); + doThrow(CloudRuntimeException.class).when(extensionDetailsDao).saveDetails(any()); + extensionsManager.updateExtensionsDetails(false, details, null, extensionId); + } + + @Test + public void testDeleteExtension_Success() { + DeleteExtensionCmd cmd = mock(DeleteExtensionCmd.class); + when(cmd.getId()).thenReturn(1L); + when(cmd.isCleanup()).thenReturn(false); + ExtensionVO ext = mock(ExtensionVO.class); + when(ext.isUserDefined()).thenReturn(true); + when(extensionDao.findById(1L)).thenReturn(ext); + when(extensionResourceMapDao.listByExtensionId(1L)).thenReturn(Collections.emptyList()); + when(extensionCustomActionDao.listIdsByExtensionId(1L)).thenReturn(Collections.emptyList()); + doNothing().when(extensionDetailsDao).removeDetails(1L); + when(extensionDao.remove(1L)).thenReturn(true); + + assertTrue(extensionsManager.deleteExtension(cmd)); + verify(extensionDao).remove(1L); + } + + @Test + public void testRegisterExtensionWithResource_InvalidResourceType() { + RegisterExtensionCmd cmd = mock(RegisterExtensionCmd.class); + when(cmd.getResourceType()).thenReturn("InvalidType"); + + assertThrows(InvalidParameterValueException.class, () -> extensionsManager.registerExtensionWithResource(cmd)); + } + + @Test + public void registerExtensionWithResourceRegistersSuccessfullyForValidResourceType() { + RegisterExtensionCmd cmd = mock(RegisterExtensionCmd.class); + when(cmd.getResourceType()).thenReturn(ExtensionResourceMap.ResourceType.Cluster.name()); + when(cmd.getResourceId()).thenReturn(UUID.randomUUID().toString()); + when(cmd.getExtensionId()).thenReturn(1L); + ExtensionVO extension = mock(ExtensionVO.class); + ClusterVO clusterVO = mock(ClusterVO.class); + when(clusterVO.getHypervisorType()).thenReturn(Hypervisor.HypervisorType.External); + when(clusterDao.findByUuid(anyString())).thenReturn(clusterVO); + ExtensionResourceMapVO resourceMap = mock(ExtensionResourceMapVO.class); + when(extensionResourceMapDao.persist(any())).thenReturn(resourceMap); + when(extensionDao.findById(anyLong())).thenReturn(extension); + Extension result = extensionsManager.registerExtensionWithResource(cmd); + assertEquals(extension, result); + verify(extensionResourceMapDao).persist(any()); + } + + @Test(expected = InvalidParameterValueException.class) + public void registerExtensionWithResourceThrowsForInvalidResourceType() { + RegisterExtensionCmd cmd = mock(RegisterExtensionCmd.class); + when(cmd.getResourceType()).thenReturn("InvalidType"); + + extensionsManager.registerExtensionWithResource(cmd); + } + + @Test(expected = InvalidParameterValueException.class) + public void registerExtensionWithResourceThrowsForMissingExtension() { + RegisterExtensionCmd cmd = mock(RegisterExtensionCmd.class); + when(cmd.getResourceType()).thenReturn(ExtensionResourceMap.ResourceType.Cluster.name()); + when(cmd.getResourceId()).thenReturn(UUID.randomUUID().toString()); + ClusterVO clusterVO = mock(ClusterVO.class); + when(clusterDao.findByUuid(anyString())).thenReturn(clusterVO); + extensionsManager.registerExtensionWithResource(cmd); + } + + @Test(expected = CloudRuntimeException.class) + public void registerExtensionWithResourceThrowsForPersistFailure() { + RegisterExtensionCmd cmd = mock(RegisterExtensionCmd.class); + when(cmd.getResourceType()).thenReturn(ExtensionResourceMap.ResourceType.Cluster.name()); + when(cmd.getResourceId()).thenReturn(UUID.randomUUID().toString()); + when(cmd.getExtensionId()).thenReturn(1L); + ClusterVO clusterVO = mock(ClusterVO.class); + when(clusterVO.getHypervisorType()).thenReturn(Hypervisor.HypervisorType.External); + when(clusterDao.findByUuid(anyString())).thenReturn(clusterVO); + ExtensionVO extension = mock(ExtensionVO.class); + when(extensionDao.findById(1L)).thenReturn(extension); + when(extensionResourceMapDao.persist(any())).thenThrow(CloudRuntimeException.class); + extensionsManager.registerExtensionWithResource(cmd); + } + + @Test + public void registerExtensionWithClusterRegistersSuccessfullyForValidCluster() { + Cluster cluster = mock(Cluster.class); + when(cluster.getHypervisorType()).thenReturn(Hypervisor.HypervisorType.External); + Extension extension = mock(Extension.class); + Map details = Map.of("key1", "value1"); + ExtensionResourceMapVO resourceMap = mock(ExtensionResourceMapVO.class); + when(extensionResourceMapDao.persist(any())).thenReturn(resourceMap); + ExtensionResourceMap result = extensionsManager.registerExtensionWithCluster(cluster, extension, details); + assertNotNull(result); + verify(extensionResourceMapDao).persist(any()); + } + + @Test + public void registerExtensionWithClusterHandlesNullDetails() { + Cluster cluster = mock(Cluster.class); + when(cluster.getHypervisorType()).thenReturn(Hypervisor.HypervisorType.External); + Extension extension = mock(Extension.class); + ExtensionResourceMapVO resourceMap = mock(ExtensionResourceMapVO.class); + when(extensionResourceMapDao.persist(any())).thenReturn(resourceMap); + ExtensionResourceMap result = extensionsManager.registerExtensionWithCluster(cluster, extension, null); + assertNotNull(result); + verify(extensionResourceMapDao).persist(any()); + } + + @Test + public void testUnregisterExtensionWithResource_InvalidResourceType() { + UnregisterExtensionCmd cmd = mock(UnregisterExtensionCmd.class); + when(cmd.getResourceType()).thenReturn("InvalidType"); + + assertThrows(InvalidParameterValueException.class, () -> extensionsManager.unregisterExtensionWithResource(cmd)); + } + + @Test + public void unregisterExtensionWithClusterRemovesMappingSuccessfully() { + Cluster cluster = mock(Cluster.class); + when(cluster.getId()).thenReturn(100L); + Long extensionId = 1L; + ExtensionResourceMapVO resourceMap = mock(ExtensionResourceMapVO.class); + when(extensionResourceMapDao.findByResourceIdAndType(eq(100L), eq(ExtensionResourceMap.ResourceType.Cluster))) + .thenReturn(resourceMap); + extensionsManager.unregisterExtensionWithCluster(cluster, extensionId); + verify(extensionResourceMapDao).remove(resourceMap.getId()); + } + + @Test + public void unregisterExtensionWithClusterHandlesMissingMappingGracefully() { + Cluster cluster = mock(Cluster.class); + when(cluster.getId()).thenReturn(100L); + Long extensionId = 1L; + when(extensionResourceMapDao.findByResourceIdAndType(eq(100L), eq(ExtensionResourceMap.ResourceType.Cluster))) + .thenReturn(null); + extensionsManager.unregisterExtensionWithCluster(cluster, extensionId); + verify(extensionResourceMapDao, never()).remove(anyLong()); + } + + @Test + public void testCreateExtensionResponse_BasicFields() { + Extension extension = mock(Extension.class); + when(extension.getUuid()).thenReturn("uuid-1"); + when(extension.getName()).thenReturn("ext1"); + when(extension.getDescription()).thenReturn("desc"); + when(extension.getType()).thenReturn(Extension.Type.Orchestrator); + when(extension.getCreated()).thenReturn(new Date()); + when(extension.getRelativePath()).thenReturn("entry.sh"); + when(extension.isPathReady()).thenReturn(true); + when(extension.isUserDefined()).thenReturn(true); + when(extension.getState()).thenReturn(Extension.State.Enabled); + when(extension.getId()).thenReturn(1L); + + // Mock externalProvisioner + when(externalProvisioner.getExtensionPath("entry.sh")).thenReturn("/some/path/entry.sh"); + + // Mock detailsDao + Pair, Map> detailsPair = new Pair<>(Map.of("foo", "bar"), + Map.of(ApiConstants.ORCHESTRATOR_REQUIRES_PREPARE_VM, "true")); + when(extensionDetailsDao.listDetailsKeyPairsWithVisibility(1L)).thenReturn(detailsPair); + + EnumSet viewDetails = EnumSet.of(ApiConstants.ExtensionDetails.all); + + ExtensionResponse response = extensionsManager.createExtensionResponse(extension, viewDetails); + + assertEquals("uuid-1", response.getId()); + assertEquals("ext1", response.getName()); + assertEquals("desc", response.getDescription()); + assertEquals("Orchestrator", response.getType()); + assertEquals("/some/path/entry.sh", response.getPath()); + assertTrue(response.isPathReady()); + assertTrue(response.isUserDefined()); + assertEquals("Enabled", response.getState()); + assertEquals("bar", response.getDetails().get("foo")); + assertTrue(response.isOrchestratorRequiresPrepareVm()); + assertEquals("extension", response.getObjectName()); + } + + @Test + public void testCreateExtensionResponse_HiddenDetailsOnly() { + Extension extension = mock(Extension.class); + when(extension.getUuid()).thenReturn("uuid-2"); + when(extension.getName()).thenReturn("ext2"); + when(extension.getDescription()).thenReturn("desc2"); + when(extension.getType()).thenReturn(Extension.Type.Orchestrator); + when(extension.getCreated()).thenReturn(new Date()); + when(extension.getRelativePath()).thenReturn("entry2.sh"); + when(extension.isPathReady()).thenReturn(false); + when(extension.isUserDefined()).thenReturn(false); + when(extension.getState()).thenReturn(Extension.State.Disabled); + when(extension.getId()).thenReturn(2L); + + when(externalProvisioner.getExtensionPath("entry2.sh")).thenReturn("/some/path/entry2.sh"); + + Map hiddenDetails = Map.of(ApiConstants.ORCHESTRATOR_REQUIRES_PREPARE_VM, "false"); + when(extensionDetailsDao.listDetailsKeyPairs(2L, List.of(ApiConstants.ORCHESTRATOR_REQUIRES_PREPARE_VM))) + .thenReturn(hiddenDetails); + + EnumSet viewDetails = EnumSet.noneOf(ApiConstants.ExtensionDetails.class); + + ExtensionResponse response = extensionsManager.createExtensionResponse(extension, viewDetails); + + assertEquals("uuid-2", response.getId()); + assertEquals("ext2", response.getName()); + assertEquals("desc2", response.getDescription()); + assertEquals("Orchestrator", response.getType()); + assertEquals("/some/path/entry2.sh", response.getPath()); + assertFalse(response.isPathReady()); + assertFalse(response.isUserDefined()); + assertEquals("Disabled", response.getState()); + assertFalse(response.isOrchestratorRequiresPrepareVm()); + assertEquals("extension", response.getObjectName()); + } + + @Test + public void testAddCustomAction_Success() { + AddCustomActionCmd cmd = mock(AddCustomActionCmd.class); + when(cmd.getName()).thenReturn("action1"); + when(cmd.getDescription()).thenReturn("desc"); + when(cmd.getExtensionId()).thenReturn(1L); + when(cmd.getResourceType()).thenReturn("VirtualMachine"); + when(cmd.getAllowedRoleTypes()).thenReturn(List.of("Admin")); + when(cmd.getTimeout()).thenReturn(5); + when(cmd.isEnabled()).thenReturn(true); + when(cmd.getParametersMap()).thenReturn(null); + when(cmd.getSuccessMessage()).thenReturn("ok"); + when(cmd.getErrorMessage()).thenReturn("fail"); + when(cmd.getDetails()).thenReturn(null); + + when(extensionCustomActionDao.findByNameAndExtensionId(1L, "action1")).thenReturn(null); + ExtensionVO ext = mock(ExtensionVO.class); + when(extensionDao.findById(1L)).thenReturn(ext); + + ExtensionCustomActionVO actionVO = mock(ExtensionCustomActionVO.class); + when(extensionCustomActionDao.persist(any())).thenReturn(actionVO); + + ExtensionCustomAction result = extensionsManager.addCustomAction(cmd); + + assertEquals(actionVO, result); + verify(extensionCustomActionDao).persist(any()); + } + + @Test(expected = CloudRuntimeException.class) + public void testAddCustomAction_DuplicateName() { + AddCustomActionCmd cmd = mock(AddCustomActionCmd.class); + when(cmd.getName()).thenReturn("action1"); + when(cmd.getExtensionId()).thenReturn(1L); + when(extensionCustomActionDao.findByNameAndExtensionId(1L, "action1")).thenReturn(mock(ExtensionCustomActionVO.class)); + + extensionsManager.addCustomAction(cmd); + } + + @Test(expected = InvalidParameterValueException.class) + public void testAddCustomAction_ExtensionNotFound() { + AddCustomActionCmd cmd = mock(AddCustomActionCmd.class); + when(cmd.getName()).thenReturn("action1"); + when(cmd.getExtensionId()).thenReturn(2L); + when(extensionCustomActionDao.findByNameAndExtensionId(2L, "action1")).thenReturn(null); + when(extensionDao.findById(2L)).thenReturn(null); + + extensionsManager.addCustomAction(cmd); + } + + @Test(expected = CloudRuntimeException.class) + public void testAddCustomAction_InvalidResourceType() { + AddCustomActionCmd cmd = mock(AddCustomActionCmd.class); + when(cmd.getName()).thenReturn("action1"); + when(cmd.getExtensionId()).thenReturn(1L); + when(cmd.getResourceType()).thenReturn("InvalidType"); + when(extensionCustomActionDao.findByNameAndExtensionId(1L, "action1")).thenReturn(null); + ExtensionVO ext = mock(ExtensionVO.class); + when(extensionDao.findById(1L)).thenReturn(ext); + + extensionsManager.addCustomAction(cmd); + } + + @Test(expected = InvalidParameterValueException.class) + public void testAddCustomAction_InvalidName() { + AddCustomActionCmd cmd = mock(AddCustomActionCmd.class); + when(cmd.getName()).thenReturn("action;1"); + extensionsManager.addCustomAction(cmd); + } + + @Test + public void deleteCustomAction_RemovesActionAndDetails_ReturnsTrue() { + long actionId = 10L; + DeleteCustomActionCmd cmd = mock(DeleteCustomActionCmd.class); + when(cmd.getId()).thenReturn(actionId); + + ExtensionCustomActionVO actionVO = mock(ExtensionCustomActionVO.class); + when(extensionCustomActionDao.findById(actionId)).thenReturn(actionVO); + when(extensionCustomActionDao.remove(actionId)).thenReturn(true); + + boolean result = extensionsManager.deleteCustomAction(cmd); + + assertTrue(result); + verify(extensionCustomActionDetailsDao).removeDetails(actionId); + verify(extensionCustomActionDao).remove(actionId); + } + + @Test(expected = InvalidParameterValueException.class) + public void deleteCustomAction_ActionNotFound() { + long actionId = 20L; + DeleteCustomActionCmd cmd = mock(DeleteCustomActionCmd.class); + when(cmd.getId()).thenReturn(actionId); + when(extensionCustomActionDao.findById(actionId)).thenReturn(null); + extensionsManager.deleteCustomAction(cmd); + verify(extensionCustomActionDetailsDao, never()).removeDetails(anyLong()); + verify(extensionCustomActionDao, never()).remove(anyLong()); + } + + @Test(expected = CloudRuntimeException.class) + public void deleteCustomAction_RemoveFails() { + long actionId = 30L; + DeleteCustomActionCmd cmd = mock(DeleteCustomActionCmd.class); + when(cmd.getId()).thenReturn(actionId); + ExtensionCustomActionVO actionVO = mock(ExtensionCustomActionVO.class); + when(extensionCustomActionDao.findById(actionId)).thenReturn(actionVO); + when(extensionCustomActionDao.remove(actionId)).thenReturn(false); + extensionsManager.deleteCustomAction(cmd); + verify(extensionCustomActionDetailsDao).removeDetails(actionId); + verify(extensionCustomActionDao).remove(actionId); + } + + private void mockCallerRole(RoleType roleType) { + CallContext callContextMock = Mockito.mock(CallContext.class); + when(CallContext.current()).thenReturn(callContextMock); + Account accountMock = mock(Account.class); + when(accountMock.getRoleId()).thenReturn(1L); + Role role = mock(Role.class); + when(role.getRoleType()).thenReturn(roleType); + when(roleService.findRole(1L)).thenReturn(role); + when(callContextMock.getCallingAccount()).thenReturn(accountMock); + } + + @Test + public void testListCustomActions_ReturnsResponses() { + ListCustomActionCmd cmd = mock(ListCustomActionCmd.class); + when(cmd.getId()).thenReturn(null); + when(cmd.getName()).thenReturn(null); + when(cmd.getExtensionId()).thenReturn(1L); + when(cmd.getKeyword()).thenReturn(null); + when(cmd.getResourceType()).thenReturn(null); + when(cmd.getResourceId()).thenReturn(null); + when(cmd.isEnabled()).thenReturn(null); + when(cmd.getStartIndex()).thenReturn(0L); + when(cmd.getPageSizeVal()).thenReturn(10L); + + ExtensionCustomActionVO action1 = mock(ExtensionCustomActionVO.class); + ExtensionCustomActionVO action2 = mock(ExtensionCustomActionVO.class); + List actions = Arrays.asList(action1, action2); + SearchBuilder sb = mock(SearchBuilder.class); + when(sb.create()).thenReturn(mock(SearchCriteria.class)); + when(sb.entity()).thenReturn(mock(ExtensionCustomActionVO.class)); + when(extensionCustomActionDao.createSearchBuilder()).thenReturn(sb); + when(extensionCustomActionDao.searchAndCount(any(), any())).thenReturn(new Pair<>(actions, 2)); + + ExtensionCustomActionResponse resp1 = mock(ExtensionCustomActionResponse.class); + ExtensionCustomActionResponse resp2 = mock(ExtensionCustomActionResponse.class); + doReturn(resp1).when(extensionsManager).createCustomActionResponse(eq(action1)); + doReturn(resp2).when(extensionsManager).createCustomActionResponse(eq(action2)); + + + try (MockedStatic ignored = mockStatic(CallContext.class)) { + mockCallerRole(RoleType.Admin); + List result = extensionsManager.listCustomActions(cmd); + + assertEquals(2, result.size()); + assertTrue(result.contains(resp1)); + assertTrue(result.contains(resp2)); + } + } + + @Test + public void testUpdateCustomAction_UpdatesFields() { + long actionId = 1L; + String newDescription = "Updated description"; + String newResourceType = "VirtualMachine"; + List newRoles = List.of("Admin", "User"); + Boolean enabled = true; + int timeout = 10; + String successMsg = "Success!"; + String errorMsg = "Error!"; + Map details = Map.of("key", "value"); + + UpdateCustomActionCmd cmd = mock(UpdateCustomActionCmd.class); + when(cmd.getId()).thenReturn(actionId); + when(cmd.getDescription()).thenReturn(newDescription); + when(cmd.getResourceType()).thenReturn(newResourceType); + when(cmd.getAllowedRoleTypes()).thenReturn(newRoles); + when(cmd.isEnabled()).thenReturn(enabled); + when(cmd.getTimeout()).thenReturn(timeout); + when(cmd.getSuccessMessage()).thenReturn(successMsg); + when(cmd.getErrorMessage()).thenReturn(errorMsg); + when(cmd.getParametersMap()).thenReturn(null); + when(cmd.isCleanupParameters()).thenReturn(false); + when(cmd.getDetails()).thenReturn(details); + when(cmd.isCleanupDetails()).thenReturn(false); + + ExtensionCustomActionVO actionVO = new ExtensionCustomActionVO(); + ReflectionTestUtils.setField(actionVO, "id", 1L); + when(extensionCustomActionDao.findById(actionId)).thenReturn(actionVO); + when(extensionCustomActionDao.update(eq(actionId), any())).thenReturn(true); + + when(extensionCustomActionDetailsDao.listDetailsKeyPairs(eq(actionId), eq(false))) + .thenReturn(new HashMap<>()); + + ExtensionCustomAction result = extensionsManager.updateCustomAction(cmd); + + assertEquals(newDescription, result.getDescription()); + assertEquals(successMsg, result.getSuccessMessage()); + assertEquals(errorMsg, result.getErrorMessage()); + assertEquals(timeout, result.getTimeout()); + assertTrue(result.isEnabled()); + assertEquals(ExtensionCustomAction.ResourceType.VirtualMachine, result.getResourceType()); + } + + @Test(expected = CloudRuntimeException.class) + public void testUpdateCustomAction_ActionNotFound_ThrowsException() { + UpdateCustomActionCmd cmd = mock(UpdateCustomActionCmd.class); + when(cmd.getId()).thenReturn(99L); + when(extensionCustomActionDao.findById(99L)).thenReturn(null); + + extensionsManager.updateCustomAction(cmd); + } + + @Test(expected = InvalidParameterValueException.class) + public void testUpdateCustomAction_InvalidResourceType_ThrowsException() { + UpdateCustomActionCmd cmd = mock(UpdateCustomActionCmd.class); + when(cmd.getId()).thenReturn(1L); + ExtensionCustomActionVO action = mock(ExtensionCustomActionVO.class); + when(extensionCustomActionDao.findById(1L)).thenReturn(action); + when(cmd.getResourceType()).thenReturn("InvalidType"); + + extensionsManager.updateCustomAction(cmd); + } + + @Test(expected = InvalidParameterValueException.class) + public void testUpdateCustomAction_InvalidRoleType_ThrowsException() { + UpdateCustomActionCmd cmd = mock(UpdateCustomActionCmd.class); + when(cmd.getId()).thenReturn(1L); + ExtensionCustomActionVO action = mock(ExtensionCustomActionVO.class); + when(extensionCustomActionDao.findById(1L)).thenReturn(action); + when(cmd.getAllowedRoleTypes()).thenReturn(List.of("NotARole")); + + extensionsManager.updateCustomAction(cmd); + } + + @Test(expected = CloudRuntimeException.class) + public void testUpdateCustomAction_DaoUpdateFails_ThrowsException() { + UpdateCustomActionCmd cmd = mock(UpdateCustomActionCmd.class); + when(cmd.getId()).thenReturn(1L); + when(cmd.getDescription()).thenReturn("desc"); + ExtensionCustomActionVO action = mock(ExtensionCustomActionVO.class); + when(extensionCustomActionDao.findById(1L)).thenReturn(action); + when(extensionCustomActionDao.update(eq(1L), any())).thenReturn(false); + + extensionsManager.updateCustomAction(cmd); + } + + @Test + public void updatedCustomActionDetails_RemovesDetails_WhenCleanupDetailsIsTrue() { + long actionId = 1L; + Boolean cleanupDetails = true; + extensionsManager.updatedCustomActionDetails(actionId, cleanupDetails, null, false, null); + verify(extensionCustomActionDetailsDao).removeDetails(actionId); + verify(extensionCustomActionDetailsDao, never()).saveDetails(any()); + } + + @Test + public void updatedCustomActionDetails_SavesDetails_WhenDetailsProvided() { + long actionId = 2L; + Map details = Map.of("key1", "value1", "key2", "value2"); + extensionsManager.updatedCustomActionDetails(actionId, false, details, false, null); + verify(extensionCustomActionDetailsDao).saveDetails(any()); + verify(extensionCustomActionDetailsDao, never()).removeDetails(anyLong()); + } + + @Test + public void updatedCustomActionDetails_DoesNothing_WhenDetailsAndCleanupDetailsAreNull() { + long actionId = 3L; + extensionsManager.updatedCustomActionDetails(actionId, null, null, false, null); + verify(extensionCustomActionDetailsDao, never()).removeDetails(anyLong()); + verify(extensionCustomActionDetailsDao, never()).saveDetails(any()); + } + + @Test + public void updatedCustomActionDetails_HandlesEmptyDetailsGracefully() { + long actionId = 4L; + Map details = Collections.emptyMap(); + extensionsManager.updatedCustomActionDetails(actionId, false, details, false, null); + verify(extensionCustomActionDetailsDao, never()).saveDetails(any()); + verify(extensionCustomActionDetailsDao, never()).removeDetails(anyLong()); + } + + @Test(expected = CloudRuntimeException.class) + public void updatedCustomActionDetails_ThrowsException_WhenSaveDetailsFails() { + long actionId = 5L; + Map details = Map.of("key1", "value1"); + doThrow(CloudRuntimeException.class).when(extensionCustomActionDetailsDao).saveDetails(any()); + extensionsManager.updatedCustomActionDetails(actionId, false, details, false, null); + } + + @Test + public void updatedCustomActionDetails_RemovesDetails_WhenCleanupDetailsParametersAreTrue() { + long actionId = 1L; + Map hiddenDetails = new HashMap<>(); + hiddenDetails.put(ApiConstants.PARAMETERS, "Test"); + when(extensionCustomActionDetailsDao.listDetailsKeyPairs(actionId, false)).thenReturn(hiddenDetails); + extensionsManager.updatedCustomActionDetails(actionId, true, null, true, null); + verify(extensionCustomActionDetailsDao).removeDetails(actionId); + verify(extensionCustomActionDetailsDao, never()).saveDetails(any()); + } + + @Test + public void updatedCustomActionDetails_RemovesDetails_WhenCleanupDetailsTrueCleanupParametersFalse() { + long actionId = 1L; + Map hiddenDetails = new HashMap<>(); + hiddenDetails.put(ApiConstants.PARAMETERS, "Test"); + when(extensionCustomActionDetailsDao.listDetailsKeyPairs(actionId, false)).thenReturn(hiddenDetails); + extensionsManager.updatedCustomActionDetails(actionId, true, null, false, null); + verify(extensionCustomActionDetailsDao, never()).removeDetails(actionId); + verify(extensionCustomActionDetailsDao).saveDetails(any()); + } + + @Test + public void updatedCustomActionDetails_RemovesDetails_WhenParameterGiven() { + long actionId = 1L; + extensionsManager.updatedCustomActionDetails(actionId, false, null, false, + List.of(mock(ExtensionCustomAction.Parameter.class))); + verify(extensionCustomActionDetailsDao, never()).removeDetails(actionId); + verify(extensionCustomActionDetailsDao, never()).saveDetails(any()); + verify(extensionCustomActionDetailsDao).persist(any(ExtensionCustomActionDetailsVO.class)); + } + + @Test + public void runCustomAction_SuccessfulExecution_ReturnsExpectedResult() throws Exception { + RunCustomActionCmd cmd = mock(RunCustomActionCmd.class); + when(cmd.getCustomActionId()).thenReturn(1L); + when(cmd.getResourceId()).thenReturn("vm-123"); + when(cmd.getParameters()).thenReturn(Map.of("param1", "value1")); + + ExtensionCustomActionVO actionVO = mock(ExtensionCustomActionVO.class); + when(extensionCustomActionDao.findById(1L)).thenReturn(actionVO); + when(actionVO.isEnabled()).thenReturn(true); + when(actionVO.getResourceType()).thenReturn(ExtensionCustomAction.ResourceType.VirtualMachine); + when(actionVO.getAllowedRoleTypes()).thenReturn( + RoleType.toCombinedMask(List.of(RoleType.Admin, RoleType.DomainAdmin, RoleType.User))); + + ExtensionVO extensionVO = mock(ExtensionVO.class); + when(extensionDao.findById(anyLong())).thenReturn(extensionVO); + when(extensionVO.getState()).thenReturn(Extension.State.Enabled); + + RunCustomActionAnswer answer = mock(RunCustomActionAnswer.class); + when(answer.getResult()).thenReturn(true); + + VirtualMachine vm = mock(VirtualMachine.class); + when(vm.getHypervisorType()).thenReturn(Hypervisor.HypervisorType.External); + when(entityManager.findByUuid(eq(VirtualMachine.class), anyString())).thenReturn(vm); + when(virtualMachineManager.findClusterAndHostIdForVm(vm, false)).thenReturn(new Pair<>(1L, 1L)); + + when(extensionResourceMapDao.findByResourceIdAndType(1L, ExtensionResourceMap.ResourceType.Cluster)).thenReturn(mock(ExtensionResourceMapVO.class)); + when(extensionCustomActionDetailsDao.listDetailsKeyPairsWithVisibility(anyLong())).thenReturn(new Pair<>(new HashMap<>(), new HashMap<>())); + + when(agentMgr.send(anyLong(), any(Command.class))).thenReturn(answer); + + try (MockedStatic ignored = mockStatic(CallContext.class)) { + mockCallerRole(RoleType.User); + CustomActionResultResponse result = extensionsManager.runCustomAction(cmd); + + assertTrue(result.getSuccess()); + } + } + + @Test(expected = InvalidParameterValueException.class) + public void runCustomAction_ActionNotFound_ThrowsException() { + RunCustomActionCmd cmd = mock(RunCustomActionCmd.class); + when(cmd.getCustomActionId()).thenReturn(99L); + when(extensionCustomActionDao.findById(99L)).thenReturn(null); + + + try (MockedStatic ignored = mockStatic(CallContext.class)) { + mockCallerRole(RoleType.Admin); + extensionsManager.runCustomAction(cmd); + } + } + + @Test(expected = CloudRuntimeException.class) + public void runCustomAction_ActionNotAllowedForRole_ThrowsException() { + RunCustomActionCmd cmd = mock(RunCustomActionCmd.class); + when(cmd.getCustomActionId()).thenReturn(2L); + + ExtensionCustomActionVO actionVO = mock(ExtensionCustomActionVO.class); + when(extensionCustomActionDao.findById(2L)).thenReturn(actionVO); + when(actionVO.getAllowedRoleTypes()).thenReturn( + RoleType.toCombinedMask(List.of(RoleType.Admin, RoleType.DomainAdmin))); + + try (MockedStatic ignored = mockStatic(CallContext.class)) { + mockCallerRole(RoleType.User); + extensionsManager.runCustomAction(cmd); + } + } + + @Test(expected = CloudRuntimeException.class) + public void runCustomAction_ActionDisabled_ThrowsException() { + RunCustomActionCmd cmd = mock(RunCustomActionCmd.class); + when(cmd.getCustomActionId()).thenReturn(2L); + + ExtensionCustomActionVO actionVO = mock(ExtensionCustomActionVO.class); + when(extensionCustomActionDao.findById(2L)).thenReturn(actionVO); + when(actionVO.isEnabled()).thenReturn(false); + + try (MockedStatic ignored = mockStatic(CallContext.class)) { + mockCallerRole(RoleType.Admin); + extensionsManager.runCustomAction(cmd); + } + } + + @Test(expected = InvalidParameterValueException.class) + public void runCustomAction_InvalidResourceType_ThrowsException() { + RunCustomActionCmd cmd = mock(RunCustomActionCmd.class); + when(cmd.getCustomActionId()).thenReturn(3L); + + ExtensionCustomActionVO actionVO = mock(ExtensionCustomActionVO.class); + when(extensionCustomActionDao.findById(3L)).thenReturn(actionVO); + when(actionVO.isEnabled()).thenReturn(true); + when(actionVO.getResourceType()).thenReturn(null); + when(actionVO.getExtensionId()).thenReturn(1L); + ExtensionVO extensionVO = mock(ExtensionVO.class); + when(extensionVO.getState()).thenReturn(Extension.State.Enabled); + when(extensionDao.findById(1L)).thenReturn(extensionVO); + + + try (MockedStatic ignored = mockStatic(CallContext.class)) { + mockCallerRole(RoleType.Admin); + extensionsManager.runCustomAction(cmd); + } + } + + @Test + public void runCustomAction_ExecutionThrowsException() throws Exception { + RunCustomActionCmd cmd = mock(RunCustomActionCmd.class); + when(cmd.getCustomActionId()).thenReturn(1L); + when(cmd.getResourceId()).thenReturn("vm-123"); + when(cmd.getParameters()).thenReturn(Map.of("param1", "value1")); + + ExtensionCustomActionVO actionVO = mock(ExtensionCustomActionVO.class); + when(extensionCustomActionDao.findById(1L)).thenReturn(actionVO); + when(actionVO.isEnabled()).thenReturn(true); + when(actionVO.getResourceType()).thenReturn(ExtensionCustomAction.ResourceType.VirtualMachine); + + ExtensionVO extensionVO = mock(ExtensionVO.class); + when(extensionDao.findById(anyLong())).thenReturn(extensionVO); + when(extensionVO.getState()).thenReturn(Extension.State.Enabled); + + VirtualMachine vm = mock(VirtualMachine.class); + when(vm.getHypervisorType()).thenReturn(Hypervisor.HypervisorType.External); + when(entityManager.findByUuid(eq(VirtualMachine.class), anyString())).thenReturn(vm); + when(virtualMachineManager.findClusterAndHostIdForVm(vm, false)).thenReturn(new Pair<>(1L, 1L)); + + when(extensionResourceMapDao.findByResourceIdAndType(1L, ExtensionResourceMap.ResourceType.Cluster)).thenReturn(mock(ExtensionResourceMapVO.class)); + when(extensionCustomActionDetailsDao.listDetailsKeyPairsWithVisibility(anyLong())).thenReturn(new Pair<>(new HashMap<>(), new HashMap<>())); + + when(agentMgr.send(anyLong(), any(Command.class))).thenThrow(OperationTimedoutException.class); + + try (MockedStatic ignored = mockStatic(CallContext.class)) { + mockCallerRole(RoleType.Admin); + CustomActionResultResponse result = extensionsManager.runCustomAction(cmd); + + assertFalse(result.getSuccess()); + } + } + + @Test + public void createCustomActionResponse_SetsBasicFields() { + ExtensionCustomAction action = mock(ExtensionCustomAction.class); + when(action.getUuid()).thenReturn("uuid-1"); + when(action.getName()).thenReturn("action1"); + when(action.getDescription()).thenReturn("desc"); + when(action.getResourceType()).thenReturn(ExtensionCustomAction.ResourceType.VirtualMachine); + + when(extensionCustomActionDetailsDao.listDetailsKeyPairsWithVisibility(anyLong())) + .thenReturn(new Pair<>(Map.of("foo", "bar"), Map.of())); + + ExtensionCustomActionResponse response = extensionsManager.createCustomActionResponse(action); + + assertEquals("uuid-1", response.getId()); + assertEquals("action1", response.getName()); + assertEquals("desc", response.getDescription()); + assertEquals("VirtualMachine", response.getResourceType()); + assertEquals("bar", response.getDetails().get("foo")); + } + + @Test + public void createCustomActionResponse_HandlesNullResourceType() { + ExtensionCustomAction action = mock(ExtensionCustomAction.class); + when(action.getUuid()).thenReturn("uuid-2"); + when(action.getName()).thenReturn("action2"); + when(action.getDescription()).thenReturn("desc2"); + when(action.getResourceType()).thenReturn(null); + + when(extensionCustomActionDetailsDao.listDetailsKeyPairsWithVisibility(anyLong())) + .thenReturn(new Pair<>(Collections.emptyMap(), Collections.emptyMap())); + + ExtensionCustomActionResponse response = extensionsManager.createCustomActionResponse(action); + + assertEquals("uuid-2", response.getId()); + assertNull(response.getResourceType()); + assertTrue(response.getDetails().isEmpty()); + } + + @Test + public void createCustomActionResponse_ParametersAreSetIfPresent() { + ExtensionCustomAction action = mock(ExtensionCustomAction.class); + when(action.getUuid()).thenReturn("uuid-3"); + when(action.getName()).thenReturn("action3"); + when(action.getDescription()).thenReturn("desc3"); + when(action.getResourceType()).thenReturn(ExtensionCustomAction.ResourceType.VirtualMachine); + + Map details = Map.of("foo", "bar"); + ExtensionCustomAction.Parameter param = new ExtensionCustomAction.Parameter("param1", + ExtensionCustomAction.Parameter.Type.STRING, ExtensionCustomAction.Parameter.ValidationFormat.NONE, + null, false); + Map hidden = Map.of(ApiConstants.PARAMETERS, + ExtensionCustomAction.Parameter.toJsonFromList(List.of(param))); + when(extensionCustomActionDetailsDao.listDetailsKeyPairsWithVisibility(anyLong())) + .thenReturn(new Pair<>(details, hidden)); + + ExtensionCustomActionResponse response = extensionsManager.createCustomActionResponse(action); + + assertEquals(ExtensionCustomAction.ResourceType.VirtualMachine.name(), response.getResourceType()); + assertEquals("bar", response.getDetails().get("foo")); + assertNotNull(response.getParameters()); + assertFalse(response.getParameters().isEmpty()); + } + + @Test + public void handleExtensionServerCommands_GetChecksumCommand_ReturnsChecksumAnswer() { + GetExtensionPathChecksumCommand cmd = mock(GetExtensionPathChecksumCommand.class); + when(cmd.getExtensionName()).thenReturn("ext"); + when(cmd.getExtensionRelativePath()).thenReturn("ext/entry.sh"); + when(extensionsManager.externalProvisioner.getChecksumForExtensionPath(anyString(), anyString())) + .thenReturn("checksum123"); + String json = extensionsManager.handleExtensionServerCommands(cmd); + assertTrue(json.contains("checksum123")); + assertTrue(json.contains("\"result\":true")); + } + + @Test + public void handleExtensionServerCommands_PreparePathCommand_ReturnsSuccessAnswer() { + PrepareExtensionPathCommand cmd = mock(PrepareExtensionPathCommand.class); + when(cmd.getExtensionName()).thenReturn("ext"); + when(cmd.getExtensionRelativePath()).thenReturn("ext/entry.sh"); + when(cmd.isExtensionUserDefined()).thenReturn(true); + doReturn(new Pair<>(true, "ok")).when(extensionsManager) + .prepareExtensionPathOnCurrentServer(anyString(), anyBoolean(), anyString()); + + String json = extensionsManager.handleExtensionServerCommands(cmd); + assertTrue(json.contains("\"result\":true")); + assertTrue(json.contains("ok")); + } + + @Test + public void handleExtensionServerCommands_CleanupFilesCommand_ReturnsSuccessAnswer() { + CleanupExtensionFilesCommand cmd = mock(CleanupExtensionFilesCommand.class); + when(cmd.getExtensionName()).thenReturn("ext"); + when(cmd.getExtensionRelativePath()).thenReturn("ext/entry.sh"); + doReturn(new Pair<>(true, "cleaned")).when(extensionsManager) + .cleanupExtensionFilesOnCurrentServer(anyString(), anyString()); + + String json = extensionsManager.handleExtensionServerCommands(cmd); + assertTrue(json.contains("\"result\":true")); + assertTrue(json.contains("cleaned")); + } + + @Test + public void handleExtensionServerCommands_UnsupportedCommand_ReturnsUnsupportedAnswer() { + ExtensionServerActionBaseCommand cmd = mock(ExtensionServerActionBaseCommand.class); + when(cmd.getExtensionName()).thenReturn("ext"); + when(cmd.getExtensionRelativePath()).thenReturn("ext/entry.sh"); + + String json = extensionsManager.handleExtensionServerCommands(cmd); + assertTrue(json.contains("Unsupported command")); + assertTrue(json.contains("\"result\":false")); + } + + @Test + public void extensionResourceMapDetailsNeedUpdateReturnsTrueWhenNoResourceMapExists() { + when(extensionResourceMapDao.findByResourceIdAndType(1L, ExtensionResourceMap.ResourceType.Cluster)).thenReturn(null); + Map externalDetails = Map.of("key", "value"); + Pair result = extensionsManager.extensionResourceMapDetailsNeedUpdate(1L, + ExtensionResourceMap.ResourceType.Cluster, externalDetails); + assertTrue(result.first()); + assertNull(result.second()); + } + + @Test + public void extensionResourceMapDetailsNeedUpdateReturnsFalseWhenDetailsMatch() { + ExtensionResourceMapVO resourceMap = mock(ExtensionResourceMapVO.class); + when(extensionResourceMapDao.findByResourceIdAndType(1L, ExtensionResourceMap.ResourceType.Cluster)).thenReturn(resourceMap); + when(extensionResourceMapDetailsDao.listDetailsKeyPairs(resourceMap.getId())).thenReturn(Map.of("key", "value")); + Map externalDetails = Map.of("key", "value"); + Pair result = extensionsManager.extensionResourceMapDetailsNeedUpdate(1L, + ExtensionResourceMap.ResourceType.Cluster, externalDetails); + assertFalse(result.first()); + assertEquals(resourceMap, result.second()); + } + + @Test + public void extensionResourceMapDetailsNeedUpdateReturnsTrueWhenDetailsDiffer() { + ExtensionResourceMapVO resourceMap = mock(ExtensionResourceMapVO.class); + when(extensionResourceMapDao.findByResourceIdAndType(1L, ExtensionResourceMap.ResourceType.Cluster)).thenReturn(resourceMap); + when(extensionResourceMapDetailsDao.listDetailsKeyPairs(resourceMap.getId())).thenReturn(Map.of("key", "oldValue")); + Map externalDetails = Map.of("key", "newValue"); + Pair result = extensionsManager.extensionResourceMapDetailsNeedUpdate(1L, + ExtensionResourceMap.ResourceType.Cluster, externalDetails); + assertTrue(result.first()); + assertEquals(resourceMap, result.second()); + } + + @Test + public void extensionResourceMapDetailsNeedUpdateReturnsTrueWhenExternalDetailsHaveExtraKeys() { + ExtensionResourceMapVO resourceMap = mock(ExtensionResourceMapVO.class); + when(extensionResourceMapDao.findByResourceIdAndType(1L, ExtensionResourceMap.ResourceType.Cluster)).thenReturn(resourceMap); + when(extensionResourceMapDetailsDao.listDetailsKeyPairs(resourceMap.getId())).thenReturn(Map.of("key", "value")); + Map externalDetails = Map.of("key", "value", "extra", "something"); + Pair result = extensionsManager.extensionResourceMapDetailsNeedUpdate(1L, + ExtensionResourceMap.ResourceType.Cluster, externalDetails); + assertTrue(result.first()); + assertEquals(resourceMap, result.second()); + } + + @Test + public void updateExtensionResourceMapDetails_SavesDetails_WhenDetailsProvided() { + long resourceMapId = 100L; + Map details = Map.of("foo", "bar", "baz", "qux"); + extensionsManager.updateExtensionResourceMapDetails(resourceMapId, details); + verify(extensionResourceMapDetailsDao).saveDetails(any()); + } + + @Test + public void updateExtensionResourceMapDetails_RemovesDetails_WhenDetailsIsNull() { + long resourceMapId = 101L; + extensionsManager.updateExtensionResourceMapDetails(resourceMapId, null); + verify(extensionResourceMapDetailsDao, never()).saveDetails(any()); + } + + @Test + public void updateExtensionResourceMapDetails_RemovesDetails_WhenDetailsIsEmpty() { + long resourceMapId = 102L; + extensionsManager.updateExtensionResourceMapDetails(resourceMapId, Collections.emptyMap()); + verify(extensionResourceMapDetailsDao, never()).saveDetails(any()); + } + + @Test(expected = CloudRuntimeException.class) + public void updateExtensionResourceMapDetails_ThrowsException_WhenSaveFails() { + long resourceMapId = 103L; + Map details = Map.of("foo", "bar"); + doThrow(CloudRuntimeException.class).when(extensionResourceMapDetailsDao).saveDetails(any()); + extensionsManager.updateExtensionResourceMapDetails(resourceMapId, details); + } + + @Test + public void getExtensionIdForCluster_WhenMappingExists_ReturnsExtensionId() { + long clusterId = 1L; + long extensionId = 100L; + ExtensionResourceMapVO mapVO = mock(ExtensionResourceMapVO.class); + when(extensionResourceMapDao.findByResourceIdAndType(eq(clusterId), any())) + .thenReturn(mapVO); + when(mapVO.getExtensionId()).thenReturn(extensionId); + + Long result = extensionsManager.getExtensionIdForCluster(clusterId); + + assertEquals(Long.valueOf(extensionId), result); + } + + @Test + public void getExtensionIdForCluster_WhenNoMappingExists_ReturnsNull() { + long clusterId = 42L; + when(extensionResourceMapDao.findByResourceIdAndType(eq(clusterId), any())) + .thenReturn(null); + + Long result = extensionsManager.getExtensionIdForCluster(clusterId); + + assertNull(result); + } + + @Test + public void getExtension_WhenExtensionExists_ReturnsExtension() { + long id = 1L; + ExtensionVO ext = mock(ExtensionVO.class); + when(extensionDao.findById(id)).thenReturn(ext); + + Extension result = extensionsManager.getExtension(id); + + assertEquals(ext, result); + } + + @Test + public void getExtension_WhenExtensionDoesNotExist_ReturnsNull() { + long id = 2L; + when(extensionDao.findById(id)).thenReturn(null); + + Extension result = extensionsManager.getExtension(id); + + assertNull(result); + } + + @Test + public void getExtensionForCluster_WhenMappingExists_ReturnsExtension() { + long clusterId = 10L; + long extensionId = 20L; + ExtensionVO ext = mock(ExtensionVO.class); + when(extensionsManager.getExtensionIdForCluster(clusterId)).thenReturn(extensionId); + when(extensionDao.findById(extensionId)).thenReturn(ext); + Extension result = extensionsManager.getExtensionForCluster(clusterId); + assertEquals(ext, result); + } + + @Test + public void getExtensionForCluster_WhenNoMappingExists_ReturnsNull() { + long clusterId = 10L; + when(extensionsManager.getExtensionIdForCluster(clusterId)).thenReturn(null); + Extension result = extensionsManager.getExtensionForCluster(clusterId); + assertNull(result); + } +} diff --git a/framework/pom.xml b/framework/pom.xml index 77a2710c335..3b534a4bb5a 100644 --- a/framework/pom.xml +++ b/framework/pom.xml @@ -51,6 +51,7 @@ db direct-download events + extensions ipc jobs managed-context diff --git a/framework/quota/src/main/java/org/apache/cloudstack/quota/activationrule/presetvariables/PresetVariableHelper.java b/framework/quota/src/main/java/org/apache/cloudstack/quota/activationrule/presetvariables/PresetVariableHelper.java index 05b75f4f64d..2a6ad132f63 100644 --- a/framework/quota/src/main/java/org/apache/cloudstack/quota/activationrule/presetvariables/PresetVariableHelper.java +++ b/framework/quota/src/main/java/org/apache/cloudstack/quota/activationrule/presetvariables/PresetVariableHelper.java @@ -92,10 +92,10 @@ import com.cloud.user.AccountVO; import com.cloud.user.dao.AccountDao; import com.cloud.utils.Pair; import com.cloud.utils.exception.CloudRuntimeException; -import com.cloud.vm.UserVmDetailVO; +import com.cloud.vm.VMInstanceDetailVO; import com.cloud.vm.VMInstanceVO; import com.cloud.vm.constants.VmDetails; -import com.cloud.vm.dao.UserVmDetailsDao; +import com.cloud.vm.dao.VMInstanceDetailsDao; import com.cloud.vm.dao.VMInstanceDao; import com.cloud.vm.snapshot.VMSnapshotVO; import com.cloud.vm.snapshot.dao.VMSnapshotDao; @@ -174,7 +174,7 @@ public class PresetVariableHelper { VMSnapshotDao vmSnapshotDao; @Inject - UserVmDetailsDao userVmDetailsDao; + VMInstanceDetailsDao vmInstanceDetailsDao; @Inject BackupOfferingDao backupOfferingDao; @@ -465,7 +465,7 @@ public class PresetVariableHelper { computingResources.setCpuSpeed(serviceOfferingVo.getSpeed()); if (serviceOfferingVo.isDynamic()) { - List details = userVmDetailsDao.listDetails(vmVo.getId()); + List details = vmInstanceDetailsDao.listDetails(vmVo.getId()); computingResources.setMemory(getDetailByName(details, VmDetails.MEMORY.getName(), computingResources.getMemory())); computingResources.setCpuNumber(getDetailByName(details, VmDetails.CPU_NUMBER.getName(), computingResources.getCpuNumber())); @@ -485,14 +485,14 @@ public class PresetVariableHelper { } } - protected Integer getDetailByName(List details, String name, Integer defaultValue) { - List detailFiltered = details.stream().filter(det -> name.equals(det.getName())).collect(Collectors.toList()); + protected Integer getDetailByName(List details, String name, Integer defaultValue) { + List detailFiltered = details.stream().filter(det -> name.equals(det.getName())).collect(Collectors.toList()); if (CollectionUtils.isEmpty(detailFiltered)) { return defaultValue; } - UserVmDetailVO detail = detailFiltered.get(0); + VMInstanceDetailVO detail = detailFiltered.get(0); if (detail.getValue() != null) { return Integer.valueOf(detail.getValue()); diff --git a/framework/quota/src/main/java/org/apache/cloudstack/quota/dao/UserVmDetailsDao.java b/framework/quota/src/main/java/org/apache/cloudstack/quota/dao/VMInstanceDetailsDao.java similarity index 87% rename from framework/quota/src/main/java/org/apache/cloudstack/quota/dao/UserVmDetailsDao.java rename to framework/quota/src/main/java/org/apache/cloudstack/quota/dao/VMInstanceDetailsDao.java index f8ab3b9dbc4..af94136cecd 100644 --- a/framework/quota/src/main/java/org/apache/cloudstack/quota/dao/UserVmDetailsDao.java +++ b/framework/quota/src/main/java/org/apache/cloudstack/quota/dao/VMInstanceDetailsDao.java @@ -20,8 +20,8 @@ import java.util.Map; import com.cloud.utils.db.GenericDao; -import org.apache.cloudstack.quota.vo.UserVmDetailVO; +import org.apache.cloudstack.quota.vo.VMInstanceDetailVO; -public interface UserVmDetailsDao extends GenericDao { +public interface VMInstanceDetailsDao extends GenericDao { Map listDetailsKeyPairs(long resourceId); } diff --git a/framework/quota/src/main/java/org/apache/cloudstack/quota/dao/UserVmDetailsDaoImpl.java b/framework/quota/src/main/java/org/apache/cloudstack/quota/dao/VMInstanceDetailsDaoImpl.java similarity index 80% rename from framework/quota/src/main/java/org/apache/cloudstack/quota/dao/UserVmDetailsDaoImpl.java rename to framework/quota/src/main/java/org/apache/cloudstack/quota/dao/VMInstanceDetailsDaoImpl.java index eb852e12aba..b0f87c1c550 100644 --- a/framework/quota/src/main/java/org/apache/cloudstack/quota/dao/UserVmDetailsDaoImpl.java +++ b/framework/quota/src/main/java/org/apache/cloudstack/quota/dao/VMInstanceDetailsDaoImpl.java @@ -20,7 +20,7 @@ import java.util.HashMap; import java.util.List; import java.util.Map; -import org.apache.cloudstack.quota.vo.UserVmDetailVO; +import org.apache.cloudstack.quota.vo.VMInstanceDetailVO; import org.springframework.stereotype.Component; import com.cloud.utils.db.GenericDaoBase; @@ -28,10 +28,10 @@ import com.cloud.utils.db.SearchBuilder; import com.cloud.utils.db.SearchCriteria; @Component -public class UserVmDetailsDaoImpl extends GenericDaoBase implements UserVmDetailsDao { - private SearchBuilder AllFieldsSearch; +public class VMInstanceDetailsDaoImpl extends GenericDaoBase implements VMInstanceDetailsDao { + private SearchBuilder AllFieldsSearch; - public UserVmDetailsDaoImpl() { + public VMInstanceDetailsDaoImpl() { AllFieldsSearch = createSearchBuilder(); AllFieldsSearch.and("resourceId", AllFieldsSearch.entity().getResourceId(), SearchCriteria.Op.EQ); AllFieldsSearch.and("name", AllFieldsSearch.entity().getName(), SearchCriteria.Op.EQ); @@ -43,11 +43,11 @@ public class UserVmDetailsDaoImpl extends GenericDaoBase i @Override public Map listDetailsKeyPairs(long resourceId) { Map details = new HashMap(); - SearchCriteria sc = AllFieldsSearch.create(); + SearchCriteria sc = AllFieldsSearch.create(); sc.setParameters("resourceId", resourceId); - List results = search(sc, null); - for (UserVmDetailVO result : results) { + List results = search(sc, null); + for (VMInstanceDetailVO result : results) { details.put(result.getName(), result.getValue()); } return details; diff --git a/framework/quota/src/main/java/org/apache/cloudstack/quota/vo/UserVmDetailVO.java b/framework/quota/src/main/java/org/apache/cloudstack/quota/vo/VMInstanceDetailVO.java similarity index 89% rename from framework/quota/src/main/java/org/apache/cloudstack/quota/vo/UserVmDetailVO.java rename to framework/quota/src/main/java/org/apache/cloudstack/quota/vo/VMInstanceDetailVO.java index 21fcdbdb52a..c5ca590e95c 100644 --- a/framework/quota/src/main/java/org/apache/cloudstack/quota/vo/UserVmDetailVO.java +++ b/framework/quota/src/main/java/org/apache/cloudstack/quota/vo/VMInstanceDetailVO.java @@ -26,8 +26,8 @@ import javax.persistence.Table; import org.apache.cloudstack.api.ResourceDetail; @Entity -@Table(name = "user_vm_details") -public class UserVmDetailVO implements ResourceDetail { +@Table(name = "vm_instance_details") +public class VMInstanceDetailVO implements ResourceDetail { @Id @GeneratedValue(strategy = GenerationType.IDENTITY) @Column(name = "id") @@ -45,10 +45,10 @@ public class UserVmDetailVO implements ResourceDetail { @Column(name = "display") private boolean display = true; - public UserVmDetailVO() { + public VMInstanceDetailVO() { } - public UserVmDetailVO(long vmId, String name, String value, boolean display) { + public VMInstanceDetailVO(long vmId, String name, String value, boolean display) { this.resourceId = vmId; this.name = name; this.value = value; diff --git a/framework/quota/src/main/resources/META-INF/cloudstack/quota/spring-framework-quota-context.xml b/framework/quota/src/main/resources/META-INF/cloudstack/quota/spring-framework-quota-context.xml index e634321208f..453355c8522 100644 --- a/framework/quota/src/main/resources/META-INF/cloudstack/quota/spring-framework-quota-context.xml +++ b/framework/quota/src/main/resources/META-INF/cloudstack/quota/spring-framework-quota-context.xml @@ -25,7 +25,7 @@ - + diff --git a/framework/quota/src/test/java/org/apache/cloudstack/quota/activationrule/presetvariables/PresetVariableHelperTest.java b/framework/quota/src/test/java/org/apache/cloudstack/quota/activationrule/presetvariables/PresetVariableHelperTest.java index e2be3acbbb5..c692cb7c1e7 100644 --- a/framework/quota/src/test/java/org/apache/cloudstack/quota/activationrule/presetvariables/PresetVariableHelperTest.java +++ b/framework/quota/src/test/java/org/apache/cloudstack/quota/activationrule/presetvariables/PresetVariableHelperTest.java @@ -93,9 +93,9 @@ import com.cloud.user.AccountVO; import com.cloud.user.dao.AccountDao; import com.cloud.utils.Pair; import com.cloud.utils.exception.CloudRuntimeException; -import com.cloud.vm.UserVmDetailVO; +import com.cloud.vm.VMInstanceDetailVO; import com.cloud.vm.VMInstanceVO; -import com.cloud.vm.dao.UserVmDetailsDao; +import com.cloud.vm.dao.VMInstanceDetailsDao; import com.cloud.vm.dao.VMInstanceDao; import com.cloud.vm.snapshot.VMSnapshot; import com.cloud.vm.snapshot.VMSnapshotVO; @@ -171,7 +171,7 @@ public class PresetVariableHelperTest { VolumeDao volumeDaoMock; @Mock - UserVmDetailsDao userVmDetailsDaoMock; + VMInstanceDetailsDao vmInstanceDetailsDaoMock; @InjectMocks PresetVariableHelper presetVariableHelperSpy = Mockito.spy(PresetVariableHelper.class); @@ -296,11 +296,11 @@ public class PresetVariableHelperTest { return quotaTypesMap.entrySet(); } - private List getVmDetailsForTests() { - List details = new LinkedList<>(); - details.add(new UserVmDetailVO(1l, "test_with_value", "277", false)); - details.add(new UserVmDetailVO(1l, "test_with_invalid_value", "invalid", false)); - details.add(new UserVmDetailVO(1l, "test_with_null", null, false)); + private List getVmDetailsForTests() { + List details = new LinkedList<>(); + details.add(new VMInstanceDetailVO(1l, "test_with_value", "277", false)); + details.add(new VMInstanceDetailVO(1l, "test_with_invalid_value", "invalid", false)); + details.add(new VMInstanceDetailVO(1l, "test_with_null", null, false)); return details; } @@ -1245,7 +1245,7 @@ public class PresetVariableHelperTest { ComputingResources result = presetVariableHelperSpy.getPresetVariableValueComputingResource(vmInstanceVoMock, serviceOfferingVoMock); Assert.assertEquals(expected.toString(), result.toString()); - Mockito.verify(userVmDetailsDaoMock, Mockito.never()).listDetails(Mockito.anyLong()); + Mockito.verify(vmInstanceDetailsDaoMock, Mockito.never()).listDetails(Mockito.anyLong()); } @Test @@ -1263,7 +1263,7 @@ public class PresetVariableHelperTest { ComputingResources result = presetVariableHelperSpy.getPresetVariableValueComputingResource(vmInstanceVoMock, serviceOfferingVoMock); Assert.assertEquals(expected.toString(), result.toString()); - Mockito.verify(userVmDetailsDaoMock).listDetails(Mockito.anyLong()); + Mockito.verify(vmInstanceDetailsDaoMock).listDetails(Mockito.anyLong()); } @Test diff --git a/framework/spring/module/src/test/java/org/apache/cloudstack/spring/module/factory/ModuleBasedContextFactoryTest.java b/framework/spring/module/src/test/java/org/apache/cloudstack/spring/module/factory/ModuleBasedContextFactoryTest.java index d8d109f6575..884665efed2 100644 --- a/framework/spring/module/src/test/java/org/apache/cloudstack/spring/module/factory/ModuleBasedContextFactoryTest.java +++ b/framework/spring/module/src/test/java/org/apache/cloudstack/spring/module/factory/ModuleBasedContextFactoryTest.java @@ -18,27 +18,31 @@ */ package org.apache.cloudstack.spring.module.factory; +import org.apache.cloudstack.spring.module.locator.impl.ClasspathModuleDefinitionLocator; +import org.apache.cloudstack.spring.module.model.ModuleDefinition; +import org.apache.cloudstack.spring.module.model.ModuleDefinitionSet; +import org.junit.Before; +import org.junit.Test; +import org.springframework.beans.factory.NoSuchBeanDefinitionException; +import org.springframework.context.ApplicationContext; +import org.springframework.core.io.Resource; + +import java.io.IOException; +import java.util.Arrays; +import java.util.Collection; +import java.util.List; +import java.util.stream.Collectors; + import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertNotNull; import static org.junit.Assert.assertNull; import static org.junit.Assert.assertTrue; import static org.junit.Assert.fail; -import java.io.IOException; -import java.util.Collection; - -import org.junit.Before; -import org.junit.Test; -import org.springframework.beans.factory.NoSuchBeanDefinitionException; -import org.springframework.context.ApplicationContext; - -import org.apache.cloudstack.spring.module.locator.impl.ClasspathModuleDefinitionLocator; -import org.apache.cloudstack.spring.module.model.ModuleDefinition; -import org.apache.cloudstack.spring.module.model.ModuleDefinitionSet; - public class ModuleBasedContextFactoryTest { Collection defs; + ModuleBasedContextFactory factory = new ModuleBasedContextFactory(); @Before public void setUp() throws IOException { @@ -51,8 +55,6 @@ public class ModuleBasedContextFactoryTest { @Test public void testLoad() throws IOException { - ModuleBasedContextFactory factory = new ModuleBasedContextFactory(); - ModuleDefinitionSet set = factory.loadModules(defs, "base"); assertNotNull(set.getApplicationContext("base")); @@ -63,8 +65,6 @@ public class ModuleBasedContextFactoryTest { InitTest.initted = false; - ModuleBasedContextFactory factory = new ModuleBasedContextFactory(); - ModuleDefinitionSet set = factory.loadModules(defs, "base"); assertTrue(!InitTest.initted); @@ -73,7 +73,6 @@ public class ModuleBasedContextFactoryTest { @Test public void testExcluded() throws IOException { - ModuleBasedContextFactory factory = new ModuleBasedContextFactory(); ModuleDefinitionSet set = factory.loadModules(defs, "base"); assertNull(set.getApplicationContext("excluded")); @@ -83,7 +82,6 @@ public class ModuleBasedContextFactoryTest { @Test public void testBeans() throws IOException { - ModuleBasedContextFactory factory = new ModuleBasedContextFactory(); ModuleDefinitionSet set = factory.loadModules(defs, "base"); testBeansInContext(set, "base", 1, new String[] {"base"}, new String[] {"child1", "child2", "child1-1"}); @@ -92,6 +90,51 @@ public class ModuleBasedContextFactoryTest { testBeansInContext(set, "child1-1", 3, new String[] {"base", "child1", "child1-1"}, new String[] {"child2"}); } + @Test + public void testEmptyNameConfigResources() throws IOException { + ModuleDefinitionSet set = factory.loadModules(defs, "base"); + testConfigResourcesArray(new String[] {}, set.getConfigResources("")); + } + + @Test + public void testBaseConfigResources() throws IOException { + ModuleDefinitionSet set = factory.loadModules(defs, "base"); + testConfigResourcesArray(new String[] {"base-context.xml", "base-context-inheritable.xml"}, set.getConfigResources("base")); + } + + @Test + public void testChild1ConfigResources() throws IOException { + ModuleDefinitionSet set = factory.loadModules(defs, "base"); + testConfigResourcesArray(new String[] { + "child1-context.xml", "child1-context-inheritable.xml", + "base-context-inheritable.xml", "child1-context-override.xml" + }, set.getConfigResources("child1")); + } + + @Test + public void testChild2ConfigResources() throws IOException { + ModuleDefinitionSet set = factory.loadModules(defs, "base"); + testConfigResourcesArray(new String[] { + "child2-context.xml", "base-context-inheritable.xml" + }, set.getConfigResources("child2")); + } + + @Test + public void testChild1_1ConfigResources() throws IOException { + ModuleDefinitionSet set = factory.loadModules(defs, "base"); + testConfigResourcesArray(new String[] { + "child1-1-context.xml", "child1-context-inheritable.xml", "base-context-inheritable.xml" + }, set.getConfigResources("child1-1")); + } + + private void testConfigResourcesArray(String[] expected, Resource[] actual) { + assertEquals(expected.length, actual.length); + List actualFileNameList = Arrays.stream(actual).map(Resource::getFilename).collect(Collectors.toList()); + for (int i = 0; i < expected.length; i++) { + assertEquals(expected[i], actualFileNameList.get(i)); + } + } + protected void testBeansInContext(ModuleDefinitionSet set, String name, int order, String[] parents, String[] notTheres) { ApplicationContext context = set.getApplicationContext(name); diff --git a/framework/spring/module/src/test/resources/testhierarchy/base/test-context-inheritable.xml b/framework/spring/module/src/test/resources/testhierarchy/base/base-context-inheritable.xml similarity index 100% rename from framework/spring/module/src/test/resources/testhierarchy/base/test-context-inheritable.xml rename to framework/spring/module/src/test/resources/testhierarchy/base/base-context-inheritable.xml diff --git a/framework/spring/module/src/test/resources/testhierarchy/base/test-context.xml b/framework/spring/module/src/test/resources/testhierarchy/base/base-context.xml similarity index 100% rename from framework/spring/module/src/test/resources/testhierarchy/base/test-context.xml rename to framework/spring/module/src/test/resources/testhierarchy/base/base-context.xml diff --git a/framework/spring/module/src/test/resources/testhierarchy/child1-1/test-context.xml b/framework/spring/module/src/test/resources/testhierarchy/child1-1/child1-1-context.xml similarity index 100% rename from framework/spring/module/src/test/resources/testhierarchy/child1-1/test-context.xml rename to framework/spring/module/src/test/resources/testhierarchy/child1-1/child1-1-context.xml diff --git a/framework/spring/module/src/test/resources/testhierarchy/child1/child1-context-inheritable.xml b/framework/spring/module/src/test/resources/testhierarchy/child1/child1-context-inheritable.xml new file mode 100644 index 00000000000..2cea17e97e3 --- /dev/null +++ b/framework/spring/module/src/test/resources/testhierarchy/child1/child1-context-inheritable.xml @@ -0,0 +1,24 @@ + + + + diff --git a/framework/spring/module/src/test/resources/testhierarchy/child1/test-context-override.xml b/framework/spring/module/src/test/resources/testhierarchy/child1/child1-context-override.xml similarity index 100% rename from framework/spring/module/src/test/resources/testhierarchy/child1/test-context-override.xml rename to framework/spring/module/src/test/resources/testhierarchy/child1/child1-context-override.xml diff --git a/framework/spring/module/src/test/resources/testhierarchy/child1/test-context.xml b/framework/spring/module/src/test/resources/testhierarchy/child1/child1-context.xml similarity index 100% rename from framework/spring/module/src/test/resources/testhierarchy/child1/test-context.xml rename to framework/spring/module/src/test/resources/testhierarchy/child1/child1-context.xml diff --git a/framework/spring/module/src/test/resources/testhierarchy/child2/test-context.xml b/framework/spring/module/src/test/resources/testhierarchy/child2/child2-context.xml similarity index 100% rename from framework/spring/module/src/test/resources/testhierarchy/child2/test-context.xml rename to framework/spring/module/src/test/resources/testhierarchy/child2/child2-context.xml diff --git a/packaging/debian/replace.properties b/packaging/debian/replace.properties index db88310d81c..5ea4a03b275 100644 --- a/packaging/debian/replace.properties +++ b/packaging/debian/replace.properties @@ -58,3 +58,4 @@ USAGECLASSPATH= USAGELOG=/var/log/cloudstack/usage/usage.log USAGESYSCONFDIR=/etc/cloudstack/usage PACKAGE=cloudstack +EXTENSIONSDEPLOYMENTMODE=production diff --git a/packaging/el8/cloud.spec b/packaging/el8/cloud.spec index 2c6898cac7c..995f758033a 100644 --- a/packaging/el8/cloud.spec +++ b/packaging/el8/cloud.spec @@ -319,6 +319,11 @@ mkdir -p ${RPM_BUILD_ROOT}%{_datadir}/%{name}-management/templates/systemvm cp -r engine/schema/dist/systemvm-templates/* ${RPM_BUILD_ROOT}%{_datadir}/%{name}-management/templates/systemvm rm -rf ${RPM_BUILD_ROOT}%{_datadir}/%{name}-management/templates/systemvm/md5sum.txt +# Sample Extensions +mkdir -p ${RPM_BUILD_ROOT}%{_sysconfdir}/%{name}/extensions +cp -r extensions/* ${RPM_BUILD_ROOT}%{_sysconfdir}/%{name}/extensions +ln -sf %{_sysconfdir}/%{name}/extensions ${RPM_BUILD_ROOT}%{_datadir}/%{name}-management/extensions + # UI mkdir -p ${RPM_BUILD_ROOT}%{_sysconfdir}/%{name}/ui mkdir -p ${RPM_BUILD_ROOT}%{_datadir}/%{name}-ui/ @@ -607,6 +612,7 @@ pip3 install --upgrade /usr/share/cloudstack-marvin/Marvin-*.tar.gz %{_datadir}/%{name}-management/lib/*.jar %{_datadir}/%{name}-management/logs %{_datadir}/%{name}-management/templates +%{_datadir}/%{name}-management/extensions %attr(0755,root,root) %{_bindir}/%{name}-setup-databases %attr(0755,root,root) %{_bindir}/%{name}-migrate-databases %attr(0755,root,root) %{_bindir}/%{name}-set-guest-password @@ -628,6 +634,8 @@ pip3 install --upgrade /usr/share/cloudstack-marvin/Marvin-*.tar.gz %{_defaultdocdir}/%{name}-management-%{version}/LICENSE %{_defaultdocdir}/%{name}-management-%{version}/NOTICE %{_datadir}/%{name}-management/setup/wheel/*.whl +%dir %attr(0755,cloud,cloud) %{_sysconfdir}/%{name}/extensions +%attr(0755,cloud,cloud) %{_sysconfdir}/%{name}/extensions/* %files agent %attr(0755,root,root) %{_bindir}/%{name}-setup-agent diff --git a/packaging/el8/replace.properties b/packaging/el8/replace.properties index efeab01166e..a6094b59c73 100644 --- a/packaging/el8/replace.properties +++ b/packaging/el8/replace.properties @@ -57,3 +57,4 @@ SYSTEMJARS= USAGECLASSPATH= USAGELOG=/var/log/cloudstack/usage/usage.log USAGESYSCONFDIR=/etc/sysconfig +EXTENSIONSDEPLOYMENTMODE=production diff --git a/plugins/backup/nas/src/main/java/org/apache/cloudstack/backup/NASBackupProvider.java b/plugins/backup/nas/src/main/java/org/apache/cloudstack/backup/NASBackupProvider.java index fa9d2b58629..f73d82d87c4 100644 --- a/plugins/backup/nas/src/main/java/org/apache/cloudstack/backup/NASBackupProvider.java +++ b/plugins/backup/nas/src/main/java/org/apache/cloudstack/backup/NASBackupProvider.java @@ -28,6 +28,7 @@ import com.cloud.storage.ScopeType; import com.cloud.storage.Storage; import com.cloud.storage.StoragePoolHostVO; import com.cloud.storage.Volume; +import com.cloud.storage.VolumeApiServiceImpl; import com.cloud.storage.VolumeVO; import com.cloud.storage.dao.StoragePoolHostDao; import com.cloud.storage.dao.VolumeDao; @@ -36,6 +37,12 @@ import com.cloud.utils.component.AdapterBase; import com.cloud.utils.exception.CloudRuntimeException; import com.cloud.vm.VirtualMachine; import com.cloud.vm.dao.VMInstanceDao; +import com.cloud.vm.snapshot.VMSnapshot; +import com.cloud.vm.snapshot.VMSnapshotDetailsVO; +import com.cloud.vm.snapshot.VMSnapshotVO; +import com.cloud.vm.snapshot.dao.VMSnapshotDao; +import com.cloud.vm.snapshot.dao.VMSnapshotDetailsDao; + import org.apache.cloudstack.backup.dao.BackupDao; import org.apache.cloudstack.backup.dao.BackupRepositoryDao; @@ -51,6 +58,7 @@ import javax.inject.Inject; import java.text.SimpleDateFormat; import java.util.ArrayList; import java.util.Collections; +import java.util.Comparator; import java.util.Date; import java.util.List; import java.util.Locale; @@ -88,6 +96,12 @@ public class NASBackupProvider extends AdapterBase implements BackupProvider, Co @Inject private AgentManager agentManager; + @Inject + private VMSnapshotDao vmSnapshotDao; + + @Inject + private VMSnapshotDetailsDao vmSnapshotDetailsDao; + protected Host getLastVMHypervisorHost(VirtualMachine vm) { Long hostId = vm.getLastHostId(); if (hostId == null) { @@ -156,6 +170,7 @@ public class NASBackupProvider extends AdapterBase implements BackupProvider, Co if (VirtualMachine.State.Stopped.equals(vm.getState())) { List vmVolumes = volumeDao.findByInstance(vm.getId()); + vmVolumes.sort(Comparator.comparing(Volume::getDeviceId)); List volumePaths = getVolumePaths(vmVolumes); command.setVolumePaths(volumePaths); } @@ -210,7 +225,10 @@ public class NASBackupProvider extends AdapterBase implements BackupProvider, Co @Override public boolean restoreVMFromBackup(VirtualMachine vm, Backup backup) { List backedVolumes = backup.getBackedUpVolumes(); - List volumes = backedVolumes.stream().map(volume -> volumeDao.findByUuid(volume.getUuid())).collect(Collectors.toList()); + List volumes = backedVolumes.stream() + .map(volume -> volumeDao.findByUuid(volume.getUuid())) + .sorted((v1, v2) -> Long.compare(v1.getDeviceId(), v2.getDeviceId())) + .collect(Collectors.toList()); LOG.debug("Restoring vm {} from backup {} on the NAS Backup Provider", vm, backup); BackupRepository backupRepository = getBackupRepository(vm, backup); @@ -244,9 +262,13 @@ public class NASBackupProvider extends AdapterBase implements BackupProvider, Co if (Objects.isNull(storagePool)) { throw new CloudRuntimeException("Unable to find storage pool associated to the volume"); } - String volumePathPrefix = String.format("/mnt/%s", storagePool.getUuid()); + String volumePathPrefix; if (ScopeType.HOST.equals(storagePool.getScope())) { volumePathPrefix = storagePool.getPath(); + } else if (Storage.StoragePoolType.SharedMountPoint.equals(storagePool.getPoolType())) { + volumePathPrefix = storagePool.getPath(); + } else { + volumePathPrefix = String.format("/mnt/%s", storagePool.getUuid()); } volumePaths.add(String.format("%s/%s", volumePathPrefix, volume.getPath())); } @@ -402,6 +424,14 @@ public class NASBackupProvider extends AdapterBase implements BackupProvider, Co @Override public boolean assignVMToBackupOffering(VirtualMachine vm, BackupOffering backupOffering) { + for (VMSnapshotVO vmSnapshotVO : vmSnapshotDao.findByVmAndByType(vm.getId(), VMSnapshot.Type.Disk)) { + List vmSnapshotDetails = vmSnapshotDetailsDao.listDetails(vmSnapshotVO.getId()); + if (vmSnapshotDetails.stream().anyMatch(vmSnapshotDetailsVO -> VolumeApiServiceImpl.KVM_FILE_BASED_STORAGE_SNAPSHOT.equals(vmSnapshotDetailsVO.getName()))) { + logger.warn("VM [{}] has VM snapshots using the KvmFileBasedStorageVmSnapshot Strategy; this provider does not support backups on VMs with these snapshots!"); + return false; + } + } + return Hypervisor.HypervisorType.KVM.equals(vm.getHypervisorType()); } diff --git a/plugins/deployment-planners/implicit-dedication/src/test/java/org/apache/cloudstack/implicitplanner/ImplicitPlannerTest.java b/plugins/deployment-planners/implicit-dedication/src/test/java/org/apache/cloudstack/implicitplanner/ImplicitPlannerTest.java index 2d2b4c78261..d859ebd0ffb 100644 --- a/plugins/deployment-planners/implicit-dedication/src/test/java/org/apache/cloudstack/implicitplanner/ImplicitPlannerTest.java +++ b/plugins/deployment-planners/implicit-dedication/src/test/java/org/apache/cloudstack/implicitplanner/ImplicitPlannerTest.java @@ -95,7 +95,7 @@ import com.cloud.vm.UserVmVO; import com.cloud.vm.VMInstanceVO; import com.cloud.vm.VirtualMachineProfileImpl; import com.cloud.vm.dao.UserVmDao; -import com.cloud.vm.dao.UserVmDetailsDao; +import com.cloud.vm.dao.VMInstanceDetailsDao; import com.cloud.vm.dao.VMInstanceDao; @RunWith(SpringJUnit4ClassRunner.class) @@ -123,7 +123,7 @@ public class ImplicitPlannerTest { @Inject UserVmDao vmDao; @Inject - UserVmDetailsDao vmDetailsDao; + VMInstanceDetailsDao vmDetailsDao; @Inject VMInstanceDao vmInstanceDao; @Inject @@ -359,7 +359,7 @@ public class ImplicitPlannerTest { clustersWithEnoughCapacity.add(3L); when( capacityDao.listClustersInZoneOrPodByHostCapacities(dataCenterId, 12L, noOfCpusInOffering * cpuSpeedInOffering, ramInOffering * 1024L * 1024L, - Capacity.CAPACITY_TYPE_CPU, true)).thenReturn(clustersWithEnoughCapacity); + true)).thenReturn(clustersWithEnoughCapacity); Map clusterCapacityMap = new HashMap(); clusterCapacityMap.put(1L, 2048D); @@ -489,8 +489,8 @@ public class ImplicitPlannerTest { } @Bean - public UserVmDetailsDao userVmDetailsDao() { - return Mockito.mock(UserVmDetailsDao.class); + public VMInstanceDetailsDao vmInstanceDetailsDao() { + return Mockito.mock(VMInstanceDetailsDao.class); } @Bean diff --git a/plugins/hypervisors/baremetal/src/main/resources/security_group_agent/cs-sgagent b/plugins/hypervisors/baremetal/src/main/resources/security_group_agent/cs-sgagent index f8a5d7d2c9d..3b3a52561b6 100755 --- a/plugins/hypervisors/baremetal/src/main/resources/security_group_agent/cs-sgagent +++ b/plugins/hypervisors/baremetal/src/main/resources/security_group_agent/cs-sgagent @@ -1,7 +1,7 @@ #!/bin/sh # the following is chkconfig init header # -# cs-sgagent: cloudStack baremetal sercurity group agent +# cs-sgagent: cloudStack baremetal security group agent # # chkconfig: 345 97 03 # description: This is a daemon instructed by CloudStack management server \ diff --git a/plugins/hypervisors/external/pom.xml b/plugins/hypervisors/external/pom.xml new file mode 100644 index 00000000000..05a22cd2f9d --- /dev/null +++ b/plugins/hypervisors/external/pom.xml @@ -0,0 +1,44 @@ + + + 4.0.0 + + org.apache.cloudstack + cloudstack-plugins + 4.21.0.0-SNAPSHOT + ../../pom.xml + + cloud-plugin-hypervisor-external + Apache CloudStack Plugin - Hypervisor External + External Hypervisor for Cloudstack + + + org.apache.cloudstack + cloud-agent + ${project.version} + + + com.fasterxml.jackson.core + jackson-databind + ${cs.jackson.version} + compile + + + diff --git a/plugins/hypervisors/external/src/main/java/org/apache/cloudstack/agent/manager/ExternalAgentManager.java b/plugins/hypervisors/external/src/main/java/org/apache/cloudstack/agent/manager/ExternalAgentManager.java new file mode 100644 index 00000000000..24f0816868c --- /dev/null +++ b/plugins/hypervisors/external/src/main/java/org/apache/cloudstack/agent/manager/ExternalAgentManager.java @@ -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.agent.manager; + +import com.cloud.utils.component.Manager; + +public interface ExternalAgentManager extends Manager { + +} diff --git a/plugins/hypervisors/external/src/main/java/org/apache/cloudstack/agent/manager/ExternalAgentManagerImpl.java b/plugins/hypervisors/external/src/main/java/org/apache/cloudstack/agent/manager/ExternalAgentManagerImpl.java new file mode 100644 index 00000000000..c33ce479885 --- /dev/null +++ b/plugins/hypervisors/external/src/main/java/org/apache/cloudstack/agent/manager/ExternalAgentManagerImpl.java @@ -0,0 +1,55 @@ +// +// 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.agent.manager; + +import java.util.ArrayList; +import java.util.List; + +import org.apache.cloudstack.framework.config.ConfigKey; +import org.apache.cloudstack.framework.config.Configurable; + +import com.cloud.utils.component.ManagerBase; +import com.cloud.utils.component.PluggableService; + +public class ExternalAgentManagerImpl extends ManagerBase implements ExternalAgentManager, Configurable, PluggableService { + + public static final ConfigKey expectMacAddressFromExternalProvisioner = new ConfigKey<>(Boolean.class, "expect.macaddress.from.external.provisioner", "Advanced", "false", + "Sample external provisioning config, any value that has to be sent", true, ConfigKey.Scope.Cluster, null); + + @Override + public boolean start() { + return true; + } + + @Override + public List> getCommands() { + return new ArrayList<>(); + } + + @Override + public String getConfigComponentName() { + return ExternalAgentManagerImpl.class.getSimpleName(); + } + + @Override + public ConfigKey[] getConfigKeys() { + return new ConfigKey[] {expectMacAddressFromExternalProvisioner}; + } +} diff --git a/plugins/hypervisors/external/src/main/java/org/apache/cloudstack/agent/manager/ExternalServerPlanner.java b/plugins/hypervisors/external/src/main/java/org/apache/cloudstack/agent/manager/ExternalServerPlanner.java new file mode 100644 index 00000000000..33da0373b6a --- /dev/null +++ b/plugins/hypervisors/external/src/main/java/org/apache/cloudstack/agent/manager/ExternalServerPlanner.java @@ -0,0 +1,183 @@ +// 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.agent.manager; + +import java.util.Collections; +import java.util.List; +import java.util.Map; +import java.util.stream.Collectors; + +import javax.inject.Inject; +import javax.naming.ConfigurationException; + +import org.apache.cloudstack.extension.Extension; +import org.apache.cloudstack.extension.ExtensionResourceMap; +import org.apache.cloudstack.framework.extensions.dao.ExtensionDao; +import org.apache.cloudstack.framework.extensions.dao.ExtensionResourceMapDao; +import org.apache.cloudstack.framework.extensions.vo.ExtensionVO; +import org.apache.commons.collections.CollectionUtils; + +import com.cloud.dc.DataCenter; +import com.cloud.dc.Pod; +import com.cloud.dc.dao.ClusterDao; +import com.cloud.dc.dao.DataCenterDao; +import com.cloud.dc.dao.HostPodDao; +import com.cloud.deploy.DeployDestination; +import com.cloud.deploy.DeploymentPlan; +import com.cloud.deploy.DeploymentPlanner; +import com.cloud.exception.InsufficientServerCapacityException; +import com.cloud.host.Host; +import com.cloud.host.HostVO; +import com.cloud.host.dao.HostDao; +import com.cloud.hypervisor.Hypervisor.HypervisorType; +import com.cloud.offering.ServiceOffering; +import com.cloud.org.Cluster; +import com.cloud.resource.ResourceManager; +import com.cloud.template.VirtualMachineTemplate; +import com.cloud.utils.component.AdapterBase; +import com.cloud.vm.VirtualMachine; +import com.cloud.vm.VirtualMachineProfile; + +public class ExternalServerPlanner extends AdapterBase implements DeploymentPlanner { + + @Inject + protected DataCenterDao dcDao; + @Inject + protected HostPodDao podDao; + @Inject + protected ClusterDao clusterDao; + @Inject + protected HostDao hostDao; + @Inject + protected ResourceManager resourceMgr; + @Inject + ExtensionDao extensionDao; + @Inject + ExtensionResourceMapDao extensionResourceMapDao; + + @Override + public DeployDestination plan(VirtualMachineProfile vmProfile, DeploymentPlan plan, ExcludeList avoid) throws InsufficientServerCapacityException { + VirtualMachine vm = vmProfile.getVirtualMachine(); + ServiceOffering offering = vmProfile.getServiceOffering(); + VirtualMachineTemplate template = vmProfile.getTemplate(); + Long extensionId = template.getExtensionId(); + final ExtensionVO extensionVO = extensionDao.findById(extensionId); + if (extensionVO == null) { + logger.error("Extension associated with {} cannot be found during deployment of external instance {}", + template, vmProfile.getInstanceName()); + return null; + } + if (!Extension.State.Enabled.equals(extensionVO.getState())) { + logger.error("{} is not in enabled state therefore planning can not be done for deployment of external instance {}", + extensionVO, vmProfile.getInstanceName()); + return null; + } + if (!extensionVO.isPathReady()) { + logger.error("{} path is not in ready state therefore planning can not be done for deployment of external instance {}", + extensionVO, vmProfile.getInstanceName()); + return null; + } + + String haVmTag = (String)vmProfile.getParameter(VirtualMachineProfile.Param.HaTag); + + if (vm.getLastHostId() != null) { + HostVO h = hostDao.findById(vm.getLastHostId()); + DataCenter dc = dcDao.findById(h.getDataCenterId()); + Pod pod = podDao.findById(h.getPodId()); + Cluster c = clusterDao.findById(h.getClusterId()); + logger.debug("Start external {} on last used {}", vm, h); + return new DeployDestination(dc, pod, c, h); + } + + String hostTag = null; + if (haVmTag != null) { + hostTag = haVmTag; + } else if (offering.getHostTag() != null) { + String[] tags = offering.getHostTag().split(","); + if (tags.length > 0) { + hostTag = tags[0]; + } + } + + List clusterIds = clusterDao.listEnabledClusterIdsByZoneHypervisorArch(vm.getDataCenterId(), + HypervisorType.External, vmProfile.getTemplate().getArch()); + List extensionClusterIds = extensionResourceMapDao.listResourceIdsByExtensionIdAndType(extensionId, + ExtensionResourceMap.ResourceType.Cluster); + if (CollectionUtils.isEmpty(extensionClusterIds)) { + logger.error("No clusters associated with {} to plan deployment of external instance {}", + vmProfile.getInstanceName()); + return null; + } + clusterIds = clusterIds.stream() + .filter(extensionClusterIds::contains) + .collect(Collectors.toList()); + logger.debug("Found {} clusters associated with {}", clusterIds.size(), extensionVO); + HostVO target = null; + List hosts; + for (Long clusterId : clusterIds) { + hosts = resourceMgr.listAllUpAndEnabledHosts(Host.Type.Routing, clusterId, null, + vm.getDataCenterId()); + if (hostTag != null) { + for (HostVO host : hosts) { + hostDao.loadHostTags(host); + List hostTags = host.getHostTags(); + if (hostTags.contains(hostTag)) { + target = host; + break; + } + } + } else { + if (CollectionUtils.isNotEmpty(hosts)) { + Collections.shuffle(hosts); + target = hosts.get(0); + break; + } + } + } + + if (target != null) { + DataCenter dc = dcDao.findById(target.getDataCenterId()); + Pod pod = podDao.findById(target.getPodId()); + Cluster cluster = clusterDao.findById(target.getClusterId()); + return new DeployDestination(dc, pod, cluster, target); + } + + logger.warn("Cannot find suitable host for deploying external instance {}", vmProfile.getInstanceName()); + return null; + } + + @Override + public boolean canHandle(VirtualMachineProfile vm, DeploymentPlan plan, ExcludeList avoid) { + return vm.getHypervisorType() == HypervisorType.External; + } + + @Override + public boolean configure(String name, Map params) throws ConfigurationException { + return true; + } + + @Override + public boolean start() { + return true; + } + + @Override + public boolean stop() { + return true; + } + +} diff --git a/plugins/hypervisors/external/src/main/java/org/apache/cloudstack/agent/manager/ExternalTemplateAdapter.java b/plugins/hypervisors/external/src/main/java/org/apache/cloudstack/agent/manager/ExternalTemplateAdapter.java new file mode 100644 index 00000000000..aefe9d0d180 --- /dev/null +++ b/plugins/hypervisors/external/src/main/java/org/apache/cloudstack/agent/manager/ExternalTemplateAdapter.java @@ -0,0 +1,279 @@ +// 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.agent.manager; + +import java.util.Arrays; +import java.util.Date; +import java.util.LinkedList; +import java.util.List; +import java.util.Map; + +import org.apache.cloudstack.api.command.user.iso.DeleteIsoCmd; +import org.apache.cloudstack.api.command.user.iso.GetUploadParamsForIsoCmd; +import org.apache.cloudstack.api.command.user.iso.RegisterIsoCmd; +import org.apache.cloudstack.api.command.user.template.RegisterTemplateCmd; +import org.apache.cloudstack.context.CallContext; +import org.apache.cloudstack.engine.subsystem.api.storage.DataStore; +import org.apache.cloudstack.storage.command.TemplateOrVolumePostUploadCommand; +import org.apache.cloudstack.storage.datastore.db.ImageStoreVO; +import org.apache.cloudstack.storage.datastore.db.TemplateDataStoreVO; +import org.apache.commons.collections.CollectionUtils; +import org.apache.commons.lang3.StringUtils; + +import com.cloud.configuration.Resource; +import com.cloud.dc.DataCenterVO; +import com.cloud.event.EventTypes; +import com.cloud.event.UsageEventVO; +import com.cloud.exception.InvalidParameterValueException; +import com.cloud.exception.ResourceAllocationException; +import com.cloud.hypervisor.Hypervisor; +import com.cloud.storage.Storage; +import com.cloud.storage.TemplateProfile; +import com.cloud.storage.VMTemplateStorageResourceAssoc; +import com.cloud.storage.VMTemplateVO; +import com.cloud.storage.VMTemplateZoneVO; +import com.cloud.template.TemplateAdapter; +import com.cloud.template.TemplateAdapterBase; +import com.cloud.template.VirtualMachineTemplate; +import com.cloud.user.Account; +import com.cloud.utils.db.DB; +import com.cloud.utils.db.Transaction; +import com.cloud.utils.db.TransactionCallback; +import com.cloud.utils.exception.CloudRuntimeException; + +public class ExternalTemplateAdapter extends TemplateAdapterBase implements TemplateAdapter { + + @Override + public String getName() { + return TemplateAdapterType.External.getName(); + } + + @Override + public TemplateProfile prepare(RegisterTemplateCmd cmd) throws ResourceAllocationException { + Account caller = CallContext.current().getCallingAccount(); + Account owner = _accountMgr.getAccount(cmd.getEntityOwnerId()); + _accountMgr.checkAccess(caller, null, true, owner); + Storage.TemplateType templateType = templateMgr.validateTemplateType(cmd, _accountMgr.isAdmin(caller.getAccountId()), + CollectionUtils.isEmpty(cmd.getZoneIds()), Hypervisor.HypervisorType.External); + + List zoneId = cmd.getZoneIds(); + // ignore passed zoneId if we are using region wide image store + List stores = _imgStoreDao.findRegionImageStores(); + if (stores != null && stores.size() > 0) { + zoneId = null; + } + + Hypervisor.HypervisorType hypervisorType = Hypervisor.HypervisorType.getType(cmd.getHypervisor()); + if(hypervisorType == Hypervisor.HypervisorType.None) { + throw new InvalidParameterValueException(String.format( + "Hypervisor Type: %s is invalid. Supported Hypervisor types are: %s", + cmd.getHypervisor(), + StringUtils.join(Arrays.stream(Hypervisor.HypervisorType.values()).filter(h -> h != Hypervisor.HypervisorType.None).map(Hypervisor.HypervisorType::name).toArray(), ", "))); + } + + Map details = cmd.getDetails(); + Map externalDetails = cmd.getExternalDetails(); + if (details != null) { + details.putAll(externalDetails); + } else { + details = externalDetails; + } + + return prepare(false, CallContext.current().getCallingUserId(), cmd.getTemplateName(), cmd.getDisplayText(), cmd.getArch(), cmd.getBits(), cmd.isPasswordEnabled(), cmd.getRequiresHvm(), + cmd.getUrl(), cmd.isPublic(), cmd.isFeatured(), cmd.isExtractable(), cmd.getFormat(), cmd.getOsTypeId(), zoneId, hypervisorType, cmd.getChecksum(), true, + cmd.getTemplateTag(), owner, details, cmd.isSshKeyEnabled(), null, cmd.isDynamicallyScalable(), templateType, + cmd.isDirectDownload(), cmd.isDeployAsIs(), cmd.isForCks(), cmd.getExtensionId()); + } + + @Override + public TemplateProfile prepare(RegisterIsoCmd cmd) throws ResourceAllocationException { + throw new CloudRuntimeException("External hypervisor doesn't support ISO template"); + } + + @Override + public TemplateProfile prepare(GetUploadParamsForIsoCmd cmd) throws ResourceAllocationException { + throw new CloudRuntimeException("External hypervisor doesn't support ISO template"); + } + + private void templateCreateUsage(VMTemplateVO template, long dcId) { + if (template.getAccountId() != Account.ACCOUNT_ID_SYSTEM) { + UsageEventVO usageEvent = + new UsageEventVO(EventTypes.EVENT_TEMPLATE_CREATE, template.getAccountId(), dcId, template.getId(), template.getName(), null, + template.getSourceTemplateId(), 0L); + _usageEventDao.persist(usageEvent); + } + } + + @Override + public VMTemplateVO create(TemplateProfile profile) { + VMTemplateVO template = persistTemplate(profile, VirtualMachineTemplate.State.Active); + List zones = profile.getZoneIdList(); + + // create an entry at template_store_ref with store_id = null to represent that this template is ready for use. + TemplateDataStoreVO vmTemplateHost = + new TemplateDataStoreVO(null, template.getId(), new Date(), 100, VMTemplateStorageResourceAssoc.Status.DOWNLOADED, null, null, null, null, template.getUrl()); + this._tmpltStoreDao.persist(vmTemplateHost); + + if (zones == null) { + List dcs = _dcDao.listAllIncludingRemoved(); + if (dcs != null && dcs.size() > 0) { + templateCreateUsage(template, dcs.get(0).getId()); + } + } else { + for (Long zoneId: zones) { + templateCreateUsage(template, zoneId); + } + } + + _resourceLimitMgr.incrementResourceCount(profile.getAccountId(), Resource.ResourceType.template); + return template; + } + + @Override + public List createTemplateForPostUpload(TemplateProfile profile) { + return Transaction.execute((TransactionCallback>) status -> { + if (Storage.ImageFormat.ISO.equals(profile.getFormat())) { + throw new CloudRuntimeException("ISO upload is not supported for External hypervisor"); + } + List zoneIdList = profile.getZoneIdList(); + if (zoneIdList == null) { + throw new CloudRuntimeException("Zone ID is null, cannot upload template."); + } + if (zoneIdList.size() > 1) { + throw new CloudRuntimeException("Operation is not supported for more than one zone id at a time."); + } + VMTemplateVO template = persistTemplate(profile, VirtualMachineTemplate.State.NotUploaded); + if (template == null) { + throw new CloudRuntimeException("Unable to persist the template " + profile.getTemplate()); + } + // Set Event Details for Template/ISO Upload + String eventResourceId = template.getUuid(); + CallContext.current().setEventDetails(String.format("Template Id: %s", eventResourceId)); + CallContext.current().putContextParameter(VirtualMachineTemplate.class, eventResourceId); + Long zoneId = zoneIdList.get(0); + DataStore imageStore = verifyHeuristicRulesForZone(template, zoneId); + List payloads = new LinkedList<>(); + if (imageStore == null) { + List imageStores = getImageStoresThrowsExceptionIfNotFound(zoneId, profile); + postUploadAllocation(imageStores, template, payloads); + } else { + postUploadAllocation(List.of(imageStore), template, payloads); + } + if (payloads.isEmpty()) { + throw new CloudRuntimeException("Unable to find zone or an image store with enough capacity"); + } + _resourceLimitMgr.incrementResourceCount(profile.getAccountId(), Resource.ResourceType.template); + return payloads; + }); + } + + @Override + public TemplateProfile prepareDelete(DeleteIsoCmd cmd) { + throw new CloudRuntimeException("External hypervisor doesn't support ISO, how the delete get here???"); + } + + @Override + @DB + public boolean delete(TemplateProfile profile) { + VMTemplateVO template = profile.getTemplate(); + Long templateId = template.getId(); + boolean success = true; + String zoneName; + + if (profile.getZoneIdList() != null && profile.getZoneIdList().size() > 1) + throw new CloudRuntimeException("Operation is not supported for more than one zone id at a time"); + + if (!template.isCrossZones() && profile.getZoneIdList() != null) { + //get the first element in the list + zoneName = profile.getZoneIdList().get(0).toString(); + } else { + zoneName = "all zones"; + } + + logger.debug("Attempting to mark template host refs for {} as destroyed in zone: {}", template, zoneName); + Account account = _accountDao.findByIdIncludingRemoved(template.getAccountId()); + String eventType = EventTypes.EVENT_TEMPLATE_DELETE; + List templateHostVOs = this._tmpltStoreDao.listByTemplate(templateId); + + for (TemplateDataStoreVO vo : templateHostVOs) { + TemplateDataStoreVO lock = null; + try { + lock = _tmpltStoreDao.acquireInLockTable(vo.getId()); + if (lock == null) { + logger.debug("Failed to acquire lock when deleting templateDataStoreVO with ID: {}", vo.getId()); + success = false; + break; + } + + vo.setDestroyed(true); + _tmpltStoreDao.update(vo.getId(), vo); + + } finally { + if (lock != null) { + _tmpltStoreDao.releaseFromLockTable(lock.getId()); + } + } + } + + if (profile.getZoneIdList() != null) { + UsageEventVO usageEvent = new UsageEventVO(eventType, account.getId(), profile.getZoneIdList().get(0), + templateId, null); + _usageEventDao.persist(usageEvent); + + VMTemplateZoneVO templateZone = _tmpltZoneDao.findByZoneTemplate(profile.getZoneIdList().get(0), templateId); + + if (templateZone != null) { + _tmpltZoneDao.remove(templateZone.getId()); + } + } else { + List dcs = _dcDao.listAllIncludingRemoved(); + for (DataCenterVO dc : dcs) { + UsageEventVO usageEvent = new UsageEventVO(eventType, account.getId(), dc.getId(), templateId, null); + _usageEventDao.persist(usageEvent); + } + } + + logger.debug("Successfully marked template host refs for {}} as destroyed in zone: {}", template, zoneName); + + // If there are no more non-destroyed template host entries for this template, delete it + if (success && _tmpltStoreDao.listByTemplate(templateId).isEmpty()) { + long accountId = template.getAccountId(); + + VMTemplateVO lock = _tmpltDao.acquireInLockTable(templateId); + + try { + if (lock == null) { + logger.debug("Failed to acquire lock when deleting template with ID: {}", templateId); + success = false; + } else if (_tmpltDao.remove(templateId)) { + // Decrement the number of templates and total secondary storage space used by the account. + _resourceLimitMgr.decrementResourceCount(accountId, Resource.ResourceType.template); + _resourceLimitMgr.recalculateResourceCount(accountId, _accountMgr.getAccount(accountId).getDomainId(), Resource.ResourceType.secondary_storage.getOrdinal()); + } + + } finally { + if (lock != null) { + _tmpltDao.releaseFromLockTable(lock.getId()); + } + } + logger.debug("Removed template: {} because all of its template host refs were marked as destroyed.", template.getName()); + } + + return success; + } +} diff --git a/plugins/hypervisors/external/src/main/java/org/apache/cloudstack/guru/ExternalHypervisorGuru.java b/plugins/hypervisors/external/src/main/java/org/apache/cloudstack/guru/ExternalHypervisorGuru.java new file mode 100644 index 00000000000..cd6a2cf996a --- /dev/null +++ b/plugins/hypervisors/external/src/main/java/org/apache/cloudstack/guru/ExternalHypervisorGuru.java @@ -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.guru; + +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +import javax.inject.Inject; + +import org.apache.cloudstack.framework.extensions.manager.ExtensionsManager; +import org.apache.commons.collections.MapUtils; + +import com.cloud.agent.api.Command; +import com.cloud.agent.api.StopCommand; +import com.cloud.agent.api.to.VirtualMachineTO; +import com.cloud.host.Host; +import com.cloud.hypervisor.Hypervisor; +import com.cloud.hypervisor.HypervisorGuru; +import com.cloud.hypervisor.HypervisorGuruBase; +import com.cloud.vm.VirtualMachine; +import com.cloud.vm.VirtualMachineManager; +import com.cloud.vm.VirtualMachineProfile; +import com.cloud.vm.VirtualMachineProfileImpl; +import com.cloud.vm.dao.UserVmDao; + +public class ExternalHypervisorGuru extends HypervisorGuruBase implements HypervisorGuru { + + @Inject + private VirtualMachineManager virtualMachineManager; + @Inject + private UserVmDao userVmDao; + @Inject + ExtensionsManager extensionsManager; + + protected ExternalHypervisorGuru() { + super(); + } + + @Override + public Hypervisor.HypervisorType getHypervisorType() { + return Hypervisor.HypervisorType.External; + } + + @Override + public VirtualMachineTO implement(VirtualMachineProfile vm) { + VirtualMachineTO to = toVirtualMachineTO(vm); + return to; + } + + @Override + public boolean trackVmHostChange() { + return false; + } + + @Override + protected VirtualMachineTO toVirtualMachineTO(VirtualMachineProfile vmProfile) { + VirtualMachineTO to = super.toVirtualMachineTO(vmProfile); + + Map newDetails = new HashMap<>(); + Map toDetails = to.getDetails(); + Map serviceOfferingDetails = _serviceOfferingDetailsDao.listDetailsKeyPairs(vmProfile.getServiceOfferingId()); + if (MapUtils.isNotEmpty(serviceOfferingDetails)) { + newDetails.putAll(serviceOfferingDetails); + } + newDetails.putAll(toDetails); + if (MapUtils.isNotEmpty(newDetails)) { + to.setDetails(newDetails); + } + + return to; + } + + protected void updateStopCommandForExternalHypervisorType(final Hypervisor.HypervisorType hypervisorType, + final Long hostId, final Map vmExternalDetails, final StopCommand stopCommand) { + if (!Hypervisor.HypervisorType.External.equals(hypervisorType) || hostId == null) { + return; + } + Host host = hostDao.findById(hostId); + if (host == null) { + return; + } + stopCommand.setExternalDetails(extensionsManager.getExternalAccessDetails(host, vmExternalDetails)); + stopCommand.setExpungeVM(true); + } + + public List finalizeExpunge(VirtualMachine vm) { + List commands = new ArrayList<>(); + final StopCommand stop = new StopCommand(vm, virtualMachineManager.getExecuteInSequence(vm.getHypervisorType()), false, false); + VirtualMachineProfile profile = new VirtualMachineProfileImpl(vm); + VirtualMachineTO virtualMachineTO = toVirtualMachineTO(profile); + stop.setVirtualMachine(virtualMachineTO); + final Long hostId = vm.getHostId() != null ? vm.getHostId() : vm.getLastHostId(); + updateStopCommandForExternalHypervisorType(vm.getHypervisorType(), hostId, + virtualMachineTO.getExternalDetails(), stop); + commands.add(stop); + return commands; + } +} diff --git a/plugins/hypervisors/external/src/main/java/org/apache/cloudstack/hypervisor/external/discoverer/ExternalServerDiscoverer.java b/plugins/hypervisors/external/src/main/java/org/apache/cloudstack/hypervisor/external/discoverer/ExternalServerDiscoverer.java new file mode 100644 index 00000000000..643a29fe3ee --- /dev/null +++ b/plugins/hypervisors/external/src/main/java/org/apache/cloudstack/hypervisor/external/discoverer/ExternalServerDiscoverer.java @@ -0,0 +1,294 @@ +// 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.hypervisor.external.discoverer; + +import java.net.URI; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.UUID; + +import javax.inject.Inject; +import javax.naming.ConfigurationException; + +import org.apache.cloudstack.extension.ExtensionResourceMap; +import org.apache.cloudstack.framework.extensions.dao.ExtensionDao; +import org.apache.cloudstack.framework.extensions.dao.ExtensionResourceMapDao; +import org.apache.cloudstack.framework.extensions.manager.ExtensionsManager; +import org.apache.cloudstack.framework.extensions.vo.ExtensionResourceMapVO; +import org.apache.cloudstack.framework.extensions.vo.ExtensionVO; +import org.apache.cloudstack.hypervisor.external.resource.ExternalResource; + +import com.cloud.agent.AgentManager; +import com.cloud.agent.Listener; +import com.cloud.agent.api.AgentControlAnswer; +import com.cloud.agent.api.AgentControlCommand; +import com.cloud.agent.api.Answer; +import com.cloud.agent.api.Command; +import com.cloud.agent.api.StartupCommand; +import com.cloud.agent.api.StartupRoutingCommand; +import com.cloud.dc.ClusterVO; +import com.cloud.exception.ConnectionException; +import com.cloud.exception.DiscoveryException; +import com.cloud.host.Host; +import com.cloud.host.HostVO; +import com.cloud.host.Status; +import com.cloud.hypervisor.Hypervisor; +import com.cloud.resource.Discoverer; +import com.cloud.resource.DiscovererBase; +import com.cloud.resource.ResourceStateAdapter; +import com.cloud.resource.ServerResource; +import com.cloud.resource.UnableDeleteHostException; + +public class ExternalServerDiscoverer extends DiscovererBase implements Discoverer, Listener, ResourceStateAdapter { + + @Inject + AgentManager agentManager; + + @Inject + ExtensionDao extensionDao; + + @Inject + ExtensionResourceMapDao extensionResourceMapDao; + + @Inject + ExtensionsManager extensionsManager; + + @Override + public boolean processAnswers(long agentId, long seq, Answer[] answers) { + return false; + } + + @Override + public boolean processCommands(long agentId, long seq, Command[] commands) { + return false; + } + + @Override + public AgentControlAnswer processControlCommand(long agentId, AgentControlCommand cmd) { + return null; + } + + @Override + public void processHostAdded(long hostId) { + + } + + @Override + public void processConnect(Host host, StartupCommand cmd, boolean forRebalance) throws ConnectionException { + + } + + @Override + public boolean processDisconnect(long agentId, Status state) { + return false; + } + + @Override + public void processHostAboutToBeRemoved(long hostId) { + + } + + @Override + public void processHostRemoved(long hostId, long clusterId) { + + } + + @Override + public boolean isRecurring() { + return false; + } + + @Override + public int getTimeout() { + return 0; + } + + @Override + public boolean processTimeout(long agentId, long seq) { + return false; + } + + @Override + public boolean configure(String name, Map params) throws ConfigurationException { + super.configure(name, params); + agentManager.registerForHostEvents(this, true, false, true); + _resourceMgr.registerResourceStateAdapter(this.getClass().getSimpleName(), this); + return true; + } + + protected String getResourceGuidFromName(String name) { + return "External:" + UUID.nameUUIDFromBytes(name.getBytes()); + } + + protected void addExtensionDataToResourceParams(ExtensionVO extension, Map params) { + params.put("extensionName", extension.getName()); + params.put("extensionRelativePath", extension.getRelativePath()); + params.put("extensionState", extension.getState()); + params.put("extensionPathReady", extension.isPathReady()); + } + + @Override + public Map> find(long dcId, Long podId, Long clusterId, URI uri, String username, String password, List hostTags) throws DiscoveryException { + Map> resources; + String errorMessage; + if (clusterId == null) { + errorMessage = "Must specify cluster Id when adding host"; + logger.error(errorMessage); + throw new DiscoveryException(errorMessage); + } + ClusterVO cluster = _clusterDao.findById(clusterId); + if (cluster == null || (cluster.getHypervisorType() != Hypervisor.HypervisorType.External)) { + errorMessage = "Invalid cluster id or cluster is not for External hypervisors"; + logger.error(errorMessage); + throw new DiscoveryException(errorMessage); + } + if (podId == null) { + errorMessage = "Must specify pod when adding host"; + logger.error(errorMessage); + throw new DiscoveryException(errorMessage); + } + ExtensionResourceMapVO extensionResourceMapVO = extensionResourceMapDao.findByResourceIdAndType(clusterId, + ExtensionResourceMap.ResourceType.Cluster); + if (extensionResourceMapVO == null) { + logger.error("External hypervisor {} must be registered with an extension when adding host", + cluster); + throw new DiscoveryException(String.format("Cluster: %s is not registered with an extension", + cluster.getName())); + } + ExtensionVO extensionVO = extensionDao.findById(extensionResourceMapVO.getExtensionId()); + if (extensionVO == null) { + logger.error("Extension ID: {} to which {} cluster is registered is not found", + extensionResourceMapVO.getExtensionId(), cluster); + throw new DiscoveryException(String.format("Cluster: %s is registered with an inexistent extension", + cluster.getName())); + } + if (cluster.getGuid() == null) { + cluster.setGuid(UUID.randomUUID().toString()); + _clusterDao.update(clusterId, cluster); + } + Map params = new HashMap<>(); + params.put("username", username); + params.put("password", password); + params.put("zone", Long.toString(dcId)); + params.put("pod", Long.toString(podId)); + params.put("cluster", Long.toString(clusterId)); + String name = uri.toString(); + params.put("guid", getResourceGuidFromName(name)); + addExtensionDataToResourceParams(extensionVO, params); + resources = createAgentResource(name, params); + if (resources == null) { + throw new DiscoveryException("Failed to create external agent"); + } + return resources; + } + + @Override + protected HashMap buildConfigParams(HostVO host) { + HashMap params = super.buildConfigParams(host); + long clusterId = Long.parseLong((String) params.get("cluster")); + ExtensionResourceMapVO extensionResourceMapVO = + extensionResourceMapDao.findByResourceIdAndType(clusterId, + ExtensionResourceMap.ResourceType.Cluster); + if (extensionResourceMapVO == null) { + logger.debug("Cluster ID: {} not registered with any extension", clusterId); + return params; + } + ExtensionVO extensionVO = extensionDao.findById(extensionResourceMapVO.getExtensionId()); + if (extensionVO == null) { + logger.error("Extension with ID: {} not found", extensionResourceMapVO.getExtensionId()); + return params; + } + addExtensionDataToResourceParams(extensionVO, params); + return params; + } + + private Map> createAgentResource(String name, Map params) { + try { + logger.info("Creating external server resource: {}", name); + Map args = new HashMap<>(); + Map> newResources = new HashMap<>(); + ExternalResource agentResource; + synchronized (this) { + agentResource = new ExternalResource(); + try { + agentResource.start(); + agentResource.configure(name, params); + args.put("guid", (String)params.get("guid")); + newResources.put(agentResource, args); + } catch (ConfigurationException e) { + logger.error("Error while configuring server resource {}", e.getMessage()); + } + } + return newResources; + } catch (Exception ex) { + logger.warn("Caught creating external server resources {}", name, ex); + } + return null; + } + + @Override + public void postDiscovery(List hosts, long msId) { + } + + @Override + public boolean matchHypervisor(String hypervisor) { + if (hypervisor == null) + return true; + + return getHypervisorType().toString().equalsIgnoreCase(hypervisor); + } + + @Override + public Hypervisor.HypervisorType getHypervisorType() { + return Hypervisor.HypervisorType.External; + } + + @Override + public HostVO createHostVOForConnectedAgent(HostVO host, StartupCommand[] cmd) { + return null; + } + + @Override + public HostVO createHostVOForDirectConnectAgent(HostVO host, StartupCommand[] startup, ServerResource resource, + Map details, List hostTags) { + StartupCommand firstCmd = startup[0]; + if (!(firstCmd instanceof StartupRoutingCommand)) { + return null; + } + StartupRoutingCommand ssCmd = (StartupRoutingCommand)firstCmd; + if (ssCmd.getHypervisorType() != Hypervisor.HypervisorType.External) { + return null; + } + ExtensionResourceMapVO extensionResourceMapVO = extensionResourceMapDao.findByResourceIdAndType( + host.getClusterId(), ExtensionResourceMap.ResourceType.Cluster); + if (extensionResourceMapVO != null) { + ExtensionVO extension = extensionDao.findById(extensionResourceMapVO.getExtensionId()); + logger.debug("Creating {} for {}", host, extension); + extensionsManager.prepareExtensionPathAcrossServers(extension); + } else { + logger.debug("Creating {}. No extension registered for cluster ID: {}", host, host.getClusterId()); + } + return _resourceMgr.fillRoutingHostVO(host, ssCmd, Hypervisor.HypervisorType.External, details, hostTags); + } + + @Override + public DeleteHostAnswer deleteHost(HostVO host, boolean isForced, boolean isForceDeleteStorage) throws UnableDeleteHostException { + return new DeleteHostAnswer(true); + } +} diff --git a/plugins/hypervisors/external/src/main/java/org/apache/cloudstack/hypervisor/external/provisioner/ExternalPathPayloadProvisioner.java b/plugins/hypervisors/external/src/main/java/org/apache/cloudstack/hypervisor/external/provisioner/ExternalPathPayloadProvisioner.java new file mode 100644 index 00000000000..5a1632ce977 --- /dev/null +++ b/plugins/hypervisors/external/src/main/java/org/apache/cloudstack/hypervisor/external/provisioner/ExternalPathPayloadProvisioner.java @@ -0,0 +1,832 @@ +// +// 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.hypervisor.external.provisioner; + +import java.io.BufferedReader; +import java.io.File; +import java.io.FileInputStream; +import java.io.IOException; +import java.io.InputStreamReader; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.Paths; +import java.nio.file.StandardCopyOption; +import java.nio.file.StandardOpenOption; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Comparator; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.Properties; +import java.util.UUID; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; +import java.util.concurrent.Future; +import java.util.concurrent.RejectedExecutionException; +import java.util.concurrent.ScheduledExecutorService; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.atomic.AtomicReference; +import java.util.stream.Stream; + +import javax.inject.Inject; +import javax.naming.ConfigurationException; + +import org.apache.cloudstack.api.ApiConstants; +import org.apache.cloudstack.extension.Extension; +import org.apache.cloudstack.framework.extensions.manager.ExtensionsManager; +import org.apache.cloudstack.utils.security.DigestHelper; +import org.apache.commons.collections.CollectionUtils; +import org.apache.commons.collections.MapUtils; + +import com.cloud.agent.AgentManager; +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.agent.api.to.VirtualMachineTO; +import com.cloud.host.HostVO; +import com.cloud.host.dao.HostDao; +import com.cloud.hypervisor.ExternalProvisioner; +import com.cloud.hypervisor.Hypervisor; +import com.cloud.hypervisor.HypervisorGuru; +import com.cloud.hypervisor.HypervisorGuruManager; +import com.cloud.serializer.GsonHelper; +import com.cloud.utils.FileUtil; +import com.cloud.utils.Pair; +import com.cloud.utils.PropertiesUtil; +import com.cloud.utils.StringUtils; +import com.cloud.utils.component.ManagerBase; +import com.cloud.utils.component.PluggableService; +import com.cloud.utils.exception.CloudRuntimeException; +import com.cloud.utils.json.JsonMergeUtil; +import com.cloud.utils.script.Script; +import com.cloud.vm.UserVmVO; +import com.cloud.vm.VMInstanceVO; +import com.cloud.vm.VirtualMachine; +import com.cloud.vm.VirtualMachineProfile; +import com.cloud.vm.VirtualMachineProfileImpl; +import com.cloud.vm.VmDetailConstants; +import com.cloud.vm.dao.UserVmDao; +import com.cloud.vm.dao.VMInstanceDao; +import com.google.gson.JsonObject; +import com.google.gson.JsonParser; + +public class ExternalPathPayloadProvisioner extends ManagerBase implements ExternalProvisioner, PluggableService { + + public static final String BASE_EXTERNAL_PROVISIONER_SCRIPTS_DIR = "scripts/vm/hypervisor/external/provisioner"; + public static final String BASE_EXTERNAL_PROVISIONER_SHELL_SCRIPT = + BASE_EXTERNAL_PROVISIONER_SCRIPTS_DIR + "/provisioner.sh"; + + private static final String PROPERTIES_FILE = "server.properties"; + private static final String EXTENSIONS_DEPLOYMENT_MODE_NAME = "extensions.deployment.mode"; + private static final String EXTENSIONS_DIRECTORY_PROD = "/usr/share/cloudstack-management/extensions"; + private static final String EXTENSIONS_DATA_DIRECTORY_PROD = "/var/lib/cloudstack/management/extensions"; + private static final String EXTENSIONS_DIRECTORY_DEV = "extensions"; + private static final String EXTENSIONS_DATA_DIRECTORY_DEV = "client/target/extensions-data"; + + @Inject + UserVmDao _uservmDao; + + @Inject + HostDao hostDao; + + @Inject + VMInstanceDao vmInstanceDao; + + @Inject + HypervisorGuruManager hypervisorGuruManager; + + @Inject + ExtensionsManager extensionsManager; + + private static final AtomicReference propertiesRef = new AtomicReference<>(); + private String extensionsDirectory; + private String extensionsDataDirectory; + private ExecutorService payloadCleanupExecutor; + private ScheduledExecutorService payloadCleanupScheduler; + private static final List TRIVIAL_ACTIONS = Arrays.asList( + "status" + ); + + @Override + public String getName() { + return getClass().getSimpleName(); + } + + protected Map loadAccessDetails(Map> externalDetails, + VirtualMachineTO virtualMachineTO) { + Map modifiedDetails = new HashMap<>(); + if (MapUtils.isNotEmpty(externalDetails)) { + modifiedDetails.put(ApiConstants.EXTERNAL_DETAILS, externalDetails); + } + if (virtualMachineTO != null) { + modifiedDetails.put(ApiConstants.VIRTUAL_MACHINE_ID, virtualMachineTO.getUuid()); + modifiedDetails.put(ApiConstants.VIRTUAL_MACHINE_NAME, virtualMachineTO.getName()); + modifiedDetails.put(VmDetailConstants.CLOUDSTACK_VM_DETAILS, virtualMachineTO); + } + return modifiedDetails; + } + + protected String getExtensionCheckedPath(String extensionName, String extensionRelativePath) { + String path = getExtensionPath(extensionRelativePath); + File file = new File(path); + String errorSuffix = String.format("Entry point [%s] for extension: %s", path, extensionName); + if (!file.exists()) { + logger.error("{} does not exist", errorSuffix); + return null; + } + if (!file.isFile()) { + logger.error("{} is not a file", errorSuffix); + return null; + } + if (!file.canRead()) { + logger.error("{} is not readable", errorSuffix); + return null; + } + if (!file.canExecute()) { + logger.error("{} is not executable", errorSuffix); + return null; + } + return path; + + } + + protected boolean checkExtensionsDirectory() { + File dir = new File(extensionsDirectory); + if (!dir.exists() || !dir.isDirectory() || !dir.canWrite()) { + logger.error("Extension directory [{}] is not properly set up. It must exist, be a directory, and be writeable", + dir.getAbsolutePath()); + return false; + } + if (!extensionsDirectory.equals(dir.getAbsolutePath())) { + extensionsDirectory = dir.getAbsolutePath(); + } + logger.info("Extensions directory path: {}", extensionsDirectory); + return true; + } + + protected void createOrCheckExtensionsDataDirectory() throws ConfigurationException { + File dir = new File(extensionsDataDirectory); + if (!dir.exists()) { + try { + Files.createDirectories(dir.toPath()); + } catch (IOException e) { + logger.error("Unable to create extensions data directory [{}]", dir.getAbsolutePath(), e); + throw new ConfigurationException("Unable to create extensions data directory path"); + } + } + if (!dir.isDirectory() || !dir.canWrite()) { + logger.error("Extensions data directory [{}] is not properly set up. It must exist, be a directory, and be writeable", + dir.getAbsolutePath()); + throw new ConfigurationException("Extensions data directory path is not accessible"); + } + extensionsDataDirectory = dir.getAbsolutePath(); + logger.info("Extensions data directory path: {}", extensionsDataDirectory); + } + + private String getServerProperty(String name) { + Properties props = propertiesRef.get(); + if (props == null) { + File propsFile = PropertiesUtil.findConfigFile(PROPERTIES_FILE); + if (propsFile == null) { + logger.error("{} file not found", PROPERTIES_FILE); + return null; + } + Properties tempProps = new Properties(); + try (FileInputStream is = new FileInputStream(propsFile)) { + tempProps.load(is); + } catch (IOException e) { + logger.error("Error loading {}: {}", PROPERTIES_FILE, e.getMessage(), e); + return null; + } + if (!propertiesRef.compareAndSet(null, tempProps)) { + tempProps = propertiesRef.get(); + } + props = tempProps; + } + return props.getProperty(name); + } + + @Override + public boolean configure(String name, Map params) throws ConfigurationException { + super.configure(name, params); + + initializeExtensionDirectories(); + checkExtensionsDirectory(); + createOrCheckExtensionsDataDirectory(); + return true; + } + + private void initializeExtensionDirectories() { + String deploymentMode = getServerProperty(EXTENSIONS_DEPLOYMENT_MODE_NAME); + if ("developer".equals(deploymentMode)) { + extensionsDirectory = EXTENSIONS_DIRECTORY_DEV; + extensionsDataDirectory = EXTENSIONS_DATA_DIRECTORY_DEV; + } else { + extensionsDirectory = EXTENSIONS_DIRECTORY_PROD; + extensionsDataDirectory = EXTENSIONS_DATA_DIRECTORY_PROD; + } + } + + @Override + public boolean start() { + payloadCleanupExecutor = Executors.newSingleThreadExecutor(); + payloadCleanupScheduler = Executors.newSingleThreadScheduledExecutor(); + return true; + } + + @Override + public boolean stop() { + payloadCleanupExecutor.shutdown(); + payloadCleanupScheduler.shutdown(); + return true; + } + + @Override + public String getExtensionsPath() { + return extensionsDirectory; + } + + @Override + public String getExtensionPath(String relativePath) { + return String.format("%s%s%s", extensionsDirectory, File.separator, relativePath); + } + + @Override + public String getChecksumForExtensionPath(String extensionName, String relativePath) { + String path = getExtensionCheckedPath(extensionName, relativePath); + if (StringUtils.isBlank(path)) { + return null; + } + try { + return DigestHelper.calculateChecksum(new File(path)); + } catch (CloudRuntimeException ignored) { + return null; + } + } + + @Override + public PrepareExternalProvisioningAnswer prepareExternalProvisioning(String hostGuid, + String extensionName, String extensionRelativePath, PrepareExternalProvisioningCommand cmd) { + String extensionPath = getExtensionCheckedPath(extensionName, extensionRelativePath); + if (StringUtils.isEmpty(extensionPath)) { + return new PrepareExternalProvisioningAnswer(cmd, false, "Extension not configured"); + } + VirtualMachineTO vmTO = cmd.getVirtualMachineTO(); + String vmUUID = vmTO.getUuid(); + logger.debug("Executing PrepareExternalProvisioningCommand in the external provisioner " + + "for the VM {} as part of VM deployment", vmUUID); + Map accessDetails = loadAccessDetails(cmd.getExternalDetails(), vmTO); + Pair result = prepareExternalProvisioningInternal(extensionName, extensionPath, + vmUUID, accessDetails, cmd.getWait()); + String output = result.second(); + if (!result.first()) { + return new PrepareExternalProvisioningAnswer(cmd, false, output); + } + if (StringUtils.isEmpty(output)) { + return new PrepareExternalProvisioningAnswer(cmd, result.first(), ""); + } + try { + String merged = JsonMergeUtil.mergeJsonPatch(GsonHelper.getGson().toJson(vmTO), result.second()); + VirtualMachineTO virtualMachineTO = GsonHelper.getGson().fromJson(merged, VirtualMachineTO.class); + return new PrepareExternalProvisioningAnswer(cmd, null, virtualMachineTO, null); + } catch (Exception e) { + logger.warn("Failed to parse the output from preparing external provisioning operation as " + + "part of VM deployment: {}", e.getMessage(), e); + return new PrepareExternalProvisioningAnswer(cmd, false, "Failed to parse VM"); + } + } + + @Override + public StartAnswer startInstance(String hostGuid, String extensionName, String extensionRelativePath, + StartCommand cmd) { + String extensionPath = getExtensionCheckedPath(extensionName, extensionRelativePath); + if (StringUtils.isEmpty(extensionPath)) { + return new StartAnswer(cmd, "Extension not configured"); + } + VirtualMachineTO virtualMachineTO = cmd.getVirtualMachine(); + Map accessDetails = loadAccessDetails(cmd.getExternalDetails(), virtualMachineTO); + String vmUUID = virtualMachineTO.getUuid(); + + logger.debug(String.format("Executing StartCommand in the external provisioner for VM %s", vmUUID)); + + Object deployvm = virtualMachineTO.getDetails().get("deployvm"); + boolean isDeploy = (deployvm != null && Boolean.parseBoolean((String)deployvm)); + String operation = isDeploy ? "Deploying" : "Starting"; + try { + Pair result = executeStartCommandOnExternalSystem(extensionName, isDeploy, + extensionPath, vmUUID, accessDetails, cmd.getWait()); + + if (!result.first()) { + String errMsg = String.format("%s VM %s on the external system failed: %s", operation, vmUUID, result.second()); + logger.debug(errMsg); + return new StartAnswer(cmd, result.second()); + } + logger.debug(String.format("%s VM %s on the external system", operation, vmUUID)); + return new StartAnswer(cmd); + + } catch (CloudRuntimeException e) { + String errMsg = String.format("%s VM %s on the external system failed: %s", operation, vmUUID, e.getMessage()); + logger.debug(errMsg); + return new StartAnswer(cmd, errMsg); + } + } + + private Pair executeStartCommandOnExternalSystem(String extensionName, boolean isDeploy, + String filename, String vmUUID, Map accessDetails, int wait) { + if (isDeploy) { + return deployInstanceOnExternalSystem(extensionName, filename, vmUUID, accessDetails, wait); + } else { + return startInstanceOnExternalSystem(extensionName, filename, vmUUID, accessDetails, wait); + } + } + + @Override + public StopAnswer stopInstance(String hostGuid, String extensionName, String extensionRelativePath, + StopCommand cmd) { + String extensionPath = getExtensionCheckedPath(extensionName, extensionRelativePath); + if (StringUtils.isEmpty(extensionPath)) { + return new StopAnswer(cmd, "Extension not configured", false); + } + logger.debug("Executing stop command on the external provisioner"); + VirtualMachineTO virtualMachineTO = cmd.getVirtualMachine(); + String vmUUID = cmd.getVirtualMachine().getUuid(); + logger.debug("Executing stop command in the external system for the VM {}", vmUUID); + Map accessDetails = loadAccessDetails(cmd.getExternalDetails(), virtualMachineTO); + Pair result = stopInstanceOnExternalSystem(extensionName, extensionPath, vmUUID, + accessDetails, cmd.getWait()); + if (result.first()) { + return new StopAnswer(cmd, null, true); + } else { + return new StopAnswer(cmd, result.second(), false); + } + } + + @Override + public RebootAnswer rebootInstance(String hostGuid, String extensionName, String extensionRelativePath, + RebootCommand cmd) { + String extensionPath = getExtensionCheckedPath(extensionName, extensionRelativePath); + if (StringUtils.isEmpty(extensionPath)) { + return new RebootAnswer(cmd, "Extension not configured", false); + } + logger.debug("Executing reboot command using IPMI in the external provisioner"); + VirtualMachineTO virtualMachineTO = cmd.getVirtualMachine(); + String vmUUID = virtualMachineTO.getUuid(); + logger.debug("Executing reboot command in the external system for the VM {}", vmUUID); + Map accessDetails = loadAccessDetails(cmd.getExternalDetails(), virtualMachineTO); + Pair result = rebootInstanceOnExternalSystem(extensionName, extensionPath, vmUUID, + accessDetails, cmd.getWait()); + if (result.first()) { + return new RebootAnswer(cmd, null, true); + } else { + return new RebootAnswer(cmd, result.second(), false); + } + } + + @Override + public StopAnswer expungeInstance(String hostGuid, String extensionName, String extensionRelativePath, + StopCommand cmd) { + String extensionPath = getExtensionCheckedPath(extensionName, extensionRelativePath); + if (StringUtils.isEmpty(extensionPath)) { + return new StopAnswer(cmd, "Extension not configured", false); + } + VirtualMachineTO virtualMachineTO = cmd.getVirtualMachine(); + String vmUUID = virtualMachineTO.getUuid(); + logger.debug("Executing stop command as part of expunge in the external system for the VM {}", vmUUID); + Map accessDetails = loadAccessDetails(cmd.getExternalDetails(), virtualMachineTO); + Pair result = deleteInstanceOnExternalSystem(extensionName, extensionPath, vmUUID, + accessDetails, cmd.getWait()); + if (result.first()) { + return new StopAnswer(cmd, null, true); + } else { + return new StopAnswer(cmd, result.second(), false); + } + } + + @Override + public Map getHostVmStateReport(long hostId, String extensionName, + String extensionRelativePath) { + final Map vmStates = new HashMap<>(); + String extensionPath = getExtensionCheckedPath(extensionName, extensionRelativePath); + if (StringUtils.isEmpty(extensionPath)) { + return vmStates; + } + HostVO host = hostDao.findById(hostId); + if (host == null) { + logger.error("Host with ID: {} not found", hostId); + return vmStates; + } + List allVms = _uservmDao.listByHostId(hostId); + allVms.addAll(_uservmDao.listByLastHostId(hostId)); + if (CollectionUtils.isEmpty(allVms)) { + logger.debug("No VMs found for the {}", host); + return vmStates; + } + Map> accessDetails = + extensionsManager.getExternalAccessDetails(host, null); + for (UserVmVO vm: allVms) { + VirtualMachine.PowerState powerState = getVmPowerState(vm, accessDetails, extensionName, extensionPath); + vmStates.put(vm.getInstanceName(), new HostVmStateReportEntry(powerState, "host-" + hostId)); + } + return vmStates; + } + + @Override + public RunCustomActionAnswer runCustomAction(String hostGuid, String extensionName, + String extensionRelativePath, RunCustomActionCommand cmd) { + String extensionPath = getExtensionCheckedPath(extensionName, extensionRelativePath); + if (StringUtils.isEmpty(extensionPath)) { + return new RunCustomActionAnswer(cmd, false, "Extension not configured"); + } + final String actionName = cmd.getActionName(); + final Map parameters = cmd.getParameters(); + logger.debug("Executing custom action '{}' in the external provisioner", actionName); + VirtualMachineTO virtualMachineTO = null; + if (cmd.getVmId() != null) { + VMInstanceVO vm = vmInstanceDao.findById(cmd.getVmId()); + final HypervisorGuru hvGuru = hypervisorGuruManager.getGuru(Hypervisor.HypervisorType.External); + VirtualMachineProfile profile = new VirtualMachineProfileImpl(vm); + virtualMachineTO = hvGuru.implement(profile); + } + logger.debug("Executing custom action '{}' in the external system", actionName); + Map accessDetails = loadAccessDetails(cmd.getExternalDetails(), virtualMachineTO); + accessDetails.put(ApiConstants.ACTION, actionName); + if (MapUtils.isNotEmpty(parameters)) { + accessDetails.put(ApiConstants.PARAMETERS, parameters); + } + Pair result = runCustomActionOnExternalSystem(extensionName, extensionPath, + actionName, accessDetails, cmd.getWait()); + return new RunCustomActionAnswer(cmd, result.first(), result.second()); + } + + protected boolean createExtensionPath(String extensionName, Path destinationPathObj) throws IOException { + String sourceScriptPath = Script.findScript("", BASE_EXTERNAL_PROVISIONER_SHELL_SCRIPT); + if(sourceScriptPath == null) { + logger.error("Failed to find base script for preparing extension: {}", + extensionName); + return false; + } + Path sourcePath = Paths.get(sourceScriptPath); + Files.copy(sourcePath, destinationPathObj, StandardCopyOption.REPLACE_EXISTING); + return true; + } + + @Override + public void prepareExtensionPath(String extensionName, boolean userDefined, + String extensionRelativePath) { + logger.debug("Preparing entry point for Extension [name: {}, user-defined: {}]", extensionName, userDefined); + if (!userDefined) { + logger.debug("Skipping preparing entry point for inbuilt extension: {}", extensionName); + return; + } + String destinationPath = getExtensionPath(extensionRelativePath); + if (!destinationPath.endsWith(".sh")) { + logger.info("File {} for extension: {} is not a bash script, skipping copy.", destinationPath, + extensionName); + return; + } + File destinationFile = new File(destinationPath); + if (destinationFile.exists()) { + logger.info("File already exists at {} for extension: {}, skipping copy.", destinationPath, + extensionName); + return; + } + CloudRuntimeException exception = + new CloudRuntimeException(String.format("Failed to prepare scripts for extension: %s", extensionName)); + if (!checkExtensionsDirectory()) { + throw exception; + } + Path destinationPathObj = Paths.get(destinationPath); + Path destinationDirPath = destinationPathObj.getParent(); + if (destinationDirPath == null) { + logger.error("Failed to find parent directory for extension: {} script path {}", + extensionName, destinationPath); + throw exception; + } + try { + Files.createDirectories(destinationDirPath); + } catch (IOException e) { + logger.error("Failed to create directory: {} for extension: {}", destinationDirPath, + extensionName, e); + throw exception; + } + try { + if (!createExtensionPath(extensionName, destinationPathObj)) { + throw exception; + } + } catch (IOException e) { + logger.error("Failed to copy entry point file to [{}] for extension: {}", + destinationPath, extensionName, e); + throw exception; + } + logger.debug("Successfully prepared entry point [{}] for extension: {}", destinationPath, + extensionName); + } + + @Override + public void cleanupExtensionPath(String extensionName, String extensionRelativePath) { + String normalizedPath = extensionRelativePath; + if (normalizedPath.startsWith("/")) { + normalizedPath = normalizedPath.substring(1); + } + try { + Path rootPath = Paths.get(extensionsDirectory).toAbsolutePath().normalize(); + String extensionDirName = Extension.getDirectoryName(extensionName); + Path filePath = rootPath + .resolve(normalizedPath.startsWith(extensionDirName) ? extensionDirName : normalizedPath) + .normalize(); + if (!Files.exists(filePath)) { + return; + } + if (!Files.isDirectory(filePath) && !Files.isRegularFile(filePath)) { + throw new CloudRuntimeException( + String.format("Failed to cleanup extension entry-point: %s for extension: %s as it either " + + "does not exist or is not a regular file/directory", + extensionName, extensionRelativePath)); + } + if (!FileUtil.deleteRecursively(filePath)) { + throw new CloudRuntimeException( + String.format("Failed to delete extension entry-point: %s for extension: %s", + extensionName, filePath)); + } + } catch (IOException e) { + throw new CloudRuntimeException( + String.format("Failed to cleanup extension entry-point: %s for extension: %s due to: %s", + extensionName, normalizedPath, e.getMessage()), e); + } + } + + @Override + public void cleanupExtensionData(String extensionName, int olderThanDays, boolean cleanupDirectory) { + String extensionPayloadDirPath = extensionsDataDirectory + File.separator + extensionName; + Path dirPath = Paths.get(extensionPayloadDirPath); + if (!Files.exists(dirPath)) { + return; + } + try { + if (cleanupDirectory) { + try (Stream paths = Files.walk(dirPath)) { + paths.sorted(Comparator.reverseOrder()) + .map(Path::toFile) + .forEach(File::delete); + } + return; + } + long cutoffMillis = System.currentTimeMillis() - (olderThanDays * 24L * 60 * 60 * 1000); + long lastModified = Files.getLastModifiedTime(dirPath).toMillis(); + if (lastModified < cutoffMillis) { + return; + } + try (Stream paths = Files.walk(dirPath)) { + paths.filter(path -> !path.equals(dirPath)) + .filter(path -> { + try { + return Files.getLastModifiedTime(path).toMillis() < cutoffMillis; + } catch (IOException e) { + return false; + } + }) + .sorted(Comparator.reverseOrder()) + .map(Path::toFile) + .forEach(File::delete); + } + } catch (IOException e) { + logger.warn("Failed to clean up extension payloads for {}: {}", extensionName, e.getMessage()); + } + } + + public Pair runCustomActionOnExternalSystem(String extensionName, String filename, + String actionName, Map accessDetails, int wait) { + return executeExternalCommand(extensionName, actionName, accessDetails, wait, + String.format("Failed to execute custom action '%s' on external system", actionName), filename); + } + + protected VirtualMachine.PowerState getPowerStateFromString(String powerStateStr) { + if (StringUtils.isBlank(powerStateStr)) { + return VirtualMachine.PowerState.PowerUnknown; + } + if (powerStateStr.equalsIgnoreCase(VirtualMachine.PowerState.PowerOn.toString())) { + return VirtualMachine.PowerState.PowerOn; + } else if (powerStateStr.equalsIgnoreCase(VirtualMachine.PowerState.PowerOff.toString())) { + return VirtualMachine.PowerState.PowerOff; + } + return VirtualMachine.PowerState.PowerUnknown; + } + + protected VirtualMachine.PowerState parsePowerStateFromResponse(UserVmVO userVmVO, String response) { + logger.debug("Power status response from the external system for {} : {}", userVmVO, response); + if (StringUtils.isBlank(response)) { + logger.warn("Empty response while trying to fetch the power status of the {}", userVmVO); + return VirtualMachine.PowerState.PowerUnknown; + } + if (!response.trim().startsWith("{")) { + return getPowerStateFromString(response); + } + try { + JsonObject jsonObj = new JsonParser().parse(response).getAsJsonObject(); + String powerState = jsonObj.has("power_state") ? jsonObj.get("power_state").getAsString() : null; + return getPowerStateFromString(powerState); + } catch (Exception e) { + logger.warn("Failed to parse power status response: {} for {} as JSON: {}", + response, userVmVO, e.getMessage()); + return VirtualMachine.PowerState.PowerUnknown; + } + } + + private VirtualMachine.PowerState getVmPowerState(UserVmVO userVmVO, Map> accessDetails, + String extensionName, String extensionPath) { + final HypervisorGuru hvGuru = hypervisorGuruManager.getGuru(Hypervisor.HypervisorType.External); + VirtualMachineProfile profile = new VirtualMachineProfileImpl(userVmVO); + VirtualMachineTO virtualMachineTO = hvGuru.implement(profile); + accessDetails.put(ApiConstants.VIRTUAL_MACHINE, virtualMachineTO.getExternalDetails()); + Map modifiedDetails = loadAccessDetails(accessDetails, virtualMachineTO); + String vmUUID = userVmVO.getUuid(); + logger.debug("Trying to get VM power status from the external system for {}", userVmVO); + Pair result = getInstanceStatusOnExternalSystem(extensionName, extensionPath, vmUUID, + modifiedDetails, AgentManager.Wait.value()); + if (!result.first()) { + logger.warn("Failure response received while trying to fetch the power status of the {} : {}", + userVmVO, result.second()); + return VirtualMachine.PowerState.PowerUnknown; + } + return parsePowerStateFromResponse(userVmVO, result.second()); + } + public Pair prepareExternalProvisioningInternal(String extensionName, String filename, + String vmUUID, Map accessDetails, int wait) { + return executeExternalCommand(extensionName, "prepare", accessDetails, wait, + String.format("Failed to prepare external provisioner for deploying VM %s on external system", vmUUID), + filename); + } + + public Pair deployInstanceOnExternalSystem(String extensionName, String filename, String vmUUID, + Map accessDetails, int wait) { + return executeExternalCommand(extensionName, "create", accessDetails, wait, + String.format("Failed to create the instance %s on external system", vmUUID), filename); + } + + public Pair startInstanceOnExternalSystem(String extensionName, String filename, String vmUUID, + Map accessDetails, int wait) { + return executeExternalCommand(extensionName, "start", accessDetails, wait, + String.format("Failed to start the instance %s on external system", vmUUID), filename); + } + + public Pair stopInstanceOnExternalSystem(String extensionName, String filename, String vmUUID, + Map accessDetails, int wait) { + return executeExternalCommand(extensionName, "stop", accessDetails, wait, + String.format("Failed to stop the instance %s on external system", vmUUID), filename); + } + + public Pair rebootInstanceOnExternalSystem(String extensionName, String filename, String vmUUID, + Map accessDetails, int wait) { + return executeExternalCommand(extensionName, "reboot", accessDetails, wait, + String.format("Failed to reboot the instance %s on external system", vmUUID), filename); + } + + public Pair deleteInstanceOnExternalSystem(String extensionName, String filename, String vmUUID, + Map accessDetails, int wait) { + return executeExternalCommand(extensionName, "delete", accessDetails, wait, + String.format("Failed to delete the instance %s on external system", vmUUID), filename); + } + + public Pair getInstanceStatusOnExternalSystem(String extensionName, String filename, String vmUUID, + Map accessDetails, int wait) { + return executeExternalCommand(extensionName, "status", accessDetails, wait, + String.format("Failed to get the instance power status %s on external system", vmUUID), filename); + } + + public Pair executeExternalCommand(String extensionName, String action, + Map accessDetails, int wait, String errorLogPrefix, String file) { + try { + Path executablePath = Paths.get(file).toAbsolutePath().normalize(); + if (!Files.isExecutable(executablePath)) { + logger.error("{}: File is not executable: {}", errorLogPrefix, executablePath); + return new Pair<>(false, "File is not executable"); + } + if (wait == 0) { + wait = AgentManager.Wait.value(); + } + List command = new ArrayList<>(); + command.add(executablePath.toString()); + command.add(action); + String dataFile = prepareExternalPayload(extensionName, accessDetails); + command.add(dataFile); + command.add(Integer.toString(wait)); + ProcessBuilder builder = new ProcessBuilder(command); + builder.redirectErrorStream(true); + + logger.debug("Executing {} for command: {} with wait: {} and data file: {}", executablePath, + action, wait, dataFile); + + Process process = builder.start(); + boolean finished = process.waitFor(wait, TimeUnit.SECONDS); + if (!finished) { + process.destroyForcibly(); + logger.error("{}: External API execution timed out after {} seconds", errorLogPrefix, wait); + return new Pair<>(false, "Timeout"); + } + StringBuilder output = new StringBuilder(); + try (BufferedReader reader = new BufferedReader(new InputStreamReader(process.getInputStream()))) { + String line; + while ((line = reader.readLine()) != null) { + output.append(line).append(System.lineSeparator()); + } + } + int exitCode = process.exitValue(); + if (exitCode != 0) { + logger.warn("{}: External API execution failed with exit code {}", errorLogPrefix, exitCode); + return new Pair<>(false, "Exit code: " + exitCode + ", Output: " + output.toString().trim()); + } + deleteExtensionPayloadFile(extensionName, action, dataFile); + return new Pair<>(true, output.toString().trim()); + + } catch (IOException | InterruptedException e) { + logger.error("{}: External operation failed", errorLogPrefix, e); + throw new CloudRuntimeException(String.format("%s: External operation failed", errorLogPrefix), e); + } + } + + protected void deleteExtensionPayloadFile(String extensionName, String action, String payloadFileName) { + if (!TRIVIAL_ACTIONS.contains(action)) { + return; + } + logger.trace("Deleting payload file: {} for extension: {}, action: {}, file: {}", + payloadFileName, extensionName, action); + FileUtil.deletePath(payloadFileName); + } + + protected void scheduleExtensionPayloadDirectoryCleanup(String extensionName) { + try { + Future future = payloadCleanupExecutor.submit(() -> { + try { + cleanupExtensionData(extensionName, 1, false); + logger.trace("Cleaned up payload directory for extension: {}", extensionName); + } catch (Exception e) { + logger.warn("Exception during payload cleanup for extension: {} due to {}", extensionName, + e.getMessage()); + logger.trace(e); + } + }); + payloadCleanupScheduler.schedule(() -> { + try { + if (!future.isDone()) { + future.cancel(true); + logger.trace("Cancelled cleaning up payload directory for extension: {} as it " + + "running for more than 3 seconds", extensionName); + } + } catch (Exception e) { + logger.warn("Failed to cancel payload cleanup task for extension: {} due to {}", + extensionName, e.getMessage()); + logger.trace(e); + } + }, 3, TimeUnit.SECONDS); + } catch (RejectedExecutionException e) { + logger.warn("Payload cleanup task for extension: {} was rejected due to: {}", extensionName, + e.getMessage()); + logger.trace(e); + } + } + + protected String prepareExternalPayload(String extensionName, Map details) throws IOException { + String json = GsonHelper.getGson().toJson(details); + String fileName = UUID.randomUUID() + ".json"; + String extensionPayloadDir = extensionsDataDirectory + File.separator + extensionName; + Path payloadDirPath = Paths.get(extensionPayloadDir); + if (!Files.exists(payloadDirPath)) { + Files.createDirectories(payloadDirPath); + } else { + scheduleExtensionPayloadDirectoryCleanup(extensionName); + } + Path payloadFile = payloadDirPath.resolve(fileName); + Files.writeString(payloadFile, json, StandardOpenOption.CREATE_NEW); + return payloadFile.toAbsolutePath().toString(); + } + + @Override + public List> getCommands() { + return new ArrayList<>(); + } +} diff --git a/plugins/hypervisors/external/src/main/java/org/apache/cloudstack/hypervisor/external/resource/ExternalResource.java b/plugins/hypervisors/external/src/main/java/org/apache/cloudstack/hypervisor/external/resource/ExternalResource.java new file mode 100644 index 00000000000..ab70c880a81 --- /dev/null +++ b/plugins/hypervisors/external/src/main/java/org/apache/cloudstack/hypervisor/external/resource/ExternalResource.java @@ -0,0 +1,376 @@ +// 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.hypervisor.external.resource; + +import java.util.HashMap; +import java.util.Map; + +import javax.naming.ConfigurationException; + +import org.apache.cloudstack.extension.Extension; +import org.apache.cloudstack.framework.extensions.command.ExtensionRoutingUpdateCommand; +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; +import org.springframework.beans.factory.NoSuchBeanDefinitionException; + +import com.cloud.agent.IAgentControl; +import com.cloud.agent.api.Answer; +import com.cloud.agent.api.CheckHealthAnswer; +import com.cloud.agent.api.CheckHealthCommand; +import com.cloud.agent.api.CheckNetworkAnswer; +import com.cloud.agent.api.CheckNetworkCommand; +import com.cloud.agent.api.CleanupNetworkRulesCmd; +import com.cloud.agent.api.Command; +import com.cloud.agent.api.GetHostStatsAnswer; +import com.cloud.agent.api.GetHostStatsCommand; +import com.cloud.agent.api.GetVmStatsCommand; +import com.cloud.agent.api.HostVmStateReportEntry; +import com.cloud.agent.api.MaintainAnswer; +import com.cloud.agent.api.MaintainCommand; +import com.cloud.agent.api.PingCommand; +import com.cloud.agent.api.PingRoutingCommand; +import com.cloud.agent.api.PingTestCommand; +import com.cloud.agent.api.PrepareExternalProvisioningAnswer; +import com.cloud.agent.api.PrepareExternalProvisioningCommand; +import com.cloud.agent.api.ReadyAnswer; +import com.cloud.agent.api.ReadyCommand; +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.StartupCommand; +import com.cloud.agent.api.StartupRoutingCommand; +import com.cloud.agent.api.StopAnswer; +import com.cloud.agent.api.StopCommand; +import com.cloud.host.Host; +import com.cloud.hypervisor.ExternalProvisioner; +import com.cloud.hypervisor.Hypervisor; +import com.cloud.network.Networks; +import com.cloud.resource.ServerResource; +import com.cloud.utils.StringUtils; +import com.cloud.utils.component.ComponentContext; + +public class ExternalResource implements ServerResource { + protected Logger logger = LogManager.getLogger(getClass()); + protected static final int CPU = 0; + protected static final long CPU_SPEED = 0; + protected static final long RAM = 0; + protected static final long DOM0_RAM = 0; + protected static final String CAPABILITIES = "hvm"; + + protected ExternalProvisioner externalProvisioner; + + protected String url; + protected String dcId; + protected String pod; + protected String cluster; + protected String name; + protected String guid; + private final Host.Type type; + + private String extensionName; + private String extensionRelativePath; + private Extension.State extensionState; + private boolean extensionPathReady; + + protected boolean isExtensionDisconnected() { + return StringUtils.isAnyBlank(extensionName, extensionRelativePath); + } + + protected boolean isExtensionNotEnabled() { + return !Extension.State.Enabled.equals(extensionState); + } + + protected boolean isExtensionPathNotReady() { + return !extensionPathReady; + } + + public ExternalResource() { + type = Host.Type.Routing; + } + + @Override + public Host.Type getType() { + return type; + } + + @Override + public StartupCommand[] initialize() { + final StartupRoutingCommand cmd = + new StartupRoutingCommand(CPU, CPU_SPEED, RAM, DOM0_RAM, CAPABILITIES, + Hypervisor.HypervisorType.External,Networks.RouterPrivateIpStrategy.HostLocal); + cmd.setDataCenter(dcId); + cmd.setPod(pod); + cmd.setCluster(cluster); + cmd.setHostType(type); + cmd.setName(name); + cmd.setPrivateIpAddress(Hypervisor.HypervisorType.External.toString()); + cmd.setGuid(guid); + cmd.setIqn(guid); + cmd.setVersion(ExternalResource.class.getPackage().getImplementationVersion()); + return new StartupCommand[] {cmd}; + } + + @Override + public PingCommand getCurrentStatus(long id) { + if (isExtensionDisconnected()) { + return new PingRoutingCommand(Host.Type.Routing, id, new HashMap<>()); + } + final Map vmStates = externalProvisioner.getHostVmStateReport(id, extensionName, + extensionRelativePath); + return new PingRoutingCommand(Host.Type.Routing, id, vmStates); + } + + @Override + public Answer executeRequest(Command cmd) { + try { + if (cmd instanceof ExtensionRoutingUpdateCommand) { + return execute((ExtensionRoutingUpdateCommand)cmd); + } else if (cmd instanceof PingTestCommand) { + return execute((PingTestCommand) cmd); + } else if (cmd instanceof ReadyCommand) { + return execute((ReadyCommand)cmd); + } else if (cmd instanceof CheckHealthCommand) { + return execute((CheckHealthCommand) cmd); + } else if (cmd instanceof CheckNetworkCommand) { + return execute((CheckNetworkCommand)cmd); + }else if (cmd instanceof CleanupNetworkRulesCmd) { + return execute((CleanupNetworkRulesCmd) cmd); + } else if (cmd instanceof GetVmStatsCommand) { + return execute((GetVmStatsCommand) cmd); + } else if (cmd instanceof MaintainCommand) { + return execute((MaintainCommand) cmd); + } else if (cmd instanceof StartCommand) { + return execute((StartCommand) cmd); + } else if (cmd instanceof StopCommand) { + return execute((StopCommand) cmd); + } else if (cmd instanceof RebootCommand) { + return execute((RebootCommand) cmd); + } else if (cmd instanceof PrepareExternalProvisioningCommand) { + return execute((PrepareExternalProvisioningCommand) cmd); + } else if (cmd instanceof GetHostStatsCommand) { + return execute((GetHostStatsCommand) cmd); + } else if (cmd instanceof RunCustomActionCommand) { + return execute((RunCustomActionCommand) cmd); + } else { + return execute(cmd); + } + } catch (IllegalArgumentException e) { + return new Answer(cmd, false, e.getMessage()); + } + } + + protected String logAndGetExtensionNotConnectedOrDisabledError() { + if (isExtensionDisconnected()) { + logger.error("Extension not connected to host: {}", name); + return "Extension not connected"; + } + if (isExtensionNotEnabled()) { + logger.error("Extension: {} connected to host: {} is not in Enabled state", extensionName, name); + return "Extension is disabled"; + } + logger.error("Extension: {} connected to host: {} is not having path in Ready state", extensionName, name); + return "Extension is not ready"; + } + + private Answer execute(ExtensionRoutingUpdateCommand cmd) { + if (StringUtils.isNotBlank(extensionName) && !extensionName.equals(cmd.getExtensionName())) { + return new Answer(cmd, false, "Not same extension"); + } + if (cmd.isRemoved()) { + extensionName = null; + extensionRelativePath = null; + extensionState = Extension.State.Disabled; + return new Answer(cmd); + } + extensionName = cmd.getExtensionName(); + extensionRelativePath = cmd.getExtensionRelativePath(); + extensionState = cmd.getExtensionState(); + return new Answer(cmd); + } + + private Answer execute(PingTestCommand cmd) { + return new Answer(cmd); + } + + private Answer execute(ReadyCommand cmd) { + return new ReadyAnswer(cmd); + } + + private Answer execute(CheckHealthCommand cmd) { + if (isExtensionDisconnected()) { + logAndGetExtensionNotConnectedOrDisabledError(); + } + return new CheckHealthAnswer(cmd, !isExtensionDisconnected()); + } + + private Answer execute(CheckNetworkCommand cmd) { + return new CheckNetworkAnswer(cmd, true, "Network Setup check by names is done"); + } + + private Answer execute(CleanupNetworkRulesCmd cmd) { + if (isExtensionDisconnected()) { + return new Answer(cmd, false, logAndGetExtensionNotConnectedOrDisabledError()); + } + return new Answer(cmd, false, "Not supported"); + } + + private Answer execute(GetVmStatsCommand cmd) { + if (isExtensionDisconnected()) { + return new Answer(cmd, false, logAndGetExtensionNotConnectedOrDisabledError()); + } + return new Answer(cmd, false, "Not supported"); + } + + private MaintainAnswer execute(MaintainCommand cmd) { + return new MaintainAnswer(cmd, false); + } + + public GetHostStatsAnswer execute(GetHostStatsCommand cmd) { + if (isExtensionDisconnected()) { + logAndGetExtensionNotConnectedOrDisabledError(); + } + return new GetHostStatsAnswer(cmd, null); + } + + public StartAnswer execute(StartCommand cmd) { + if (isExtensionDisconnected() || isExtensionNotEnabled() || isExtensionPathNotReady()) { + return new StartAnswer(cmd, logAndGetExtensionNotConnectedOrDisabledError()); + } + return externalProvisioner.startInstance(guid, extensionName, extensionRelativePath, cmd); + } + + public StopAnswer execute(StopCommand cmd) { + if (isExtensionDisconnected() || isExtensionNotEnabled() || isExtensionPathNotReady()) { + return new StopAnswer(cmd, logAndGetExtensionNotConnectedOrDisabledError(), false); + } + if (cmd.isExpungeVM()) { + return externalProvisioner.expungeInstance(guid, extensionName, extensionRelativePath, cmd); + } + return externalProvisioner.stopInstance(guid, extensionName, extensionRelativePath, cmd); + } + + public RebootAnswer execute(RebootCommand cmd) { + if (isExtensionDisconnected() || isExtensionNotEnabled() || isExtensionPathNotReady()) { + return new RebootAnswer(cmd, logAndGetExtensionNotConnectedOrDisabledError(), false); + } + return externalProvisioner.rebootInstance(guid, extensionName, extensionRelativePath, cmd); + } + + public PrepareExternalProvisioningAnswer execute(PrepareExternalProvisioningCommand cmd) { + if (isExtensionDisconnected() || isExtensionNotEnabled() || isExtensionPathNotReady()) { + return new PrepareExternalProvisioningAnswer(cmd, false, logAndGetExtensionNotConnectedOrDisabledError()); + } + return externalProvisioner.prepareExternalProvisioning(guid, extensionName, extensionRelativePath, cmd); + } + + public RunCustomActionAnswer execute(RunCustomActionCommand cmd) { + if (isExtensionDisconnected() || isExtensionNotEnabled() || isExtensionPathNotReady()) { + return new RunCustomActionAnswer(cmd, false, logAndGetExtensionNotConnectedOrDisabledError()); + } + return externalProvisioner.runCustomAction(guid, extensionName, extensionRelativePath, cmd); + } + + public Answer execute(Command cmd) { + if (isExtensionDisconnected() || isExtensionNotEnabled() || isExtensionPathNotReady()) { + return new Answer(cmd, false, logAndGetExtensionNotConnectedOrDisabledError()); + } + RunCustomActionCommand runCustomActionCommand = new RunCustomActionCommand(cmd.toString()); + RunCustomActionAnswer customActionAnswer = externalProvisioner.runCustomAction(guid, extensionName, + extensionRelativePath, runCustomActionCommand); + return new Answer(cmd, customActionAnswer.getResult(), customActionAnswer.getDetails()); + } + + @Override + public void disconnected() { + + } + + @Override + public IAgentControl getAgentControl() { + return null; + } + + @Override + public void setAgentControl(IAgentControl agentControl) { + + } + + @Override + public String getName() { + return null; + } + + @Override + public void setName(String name) { + + } + + @Override + public void setConfigParams(Map params) { + + } + + @Override + public Map getConfigParams() { + return null; + } + + @Override + public int getRunLevel() { + return 0; + } + + @Override + public void setRunLevel(int level) { + + } + + @Override + public boolean configure(String name, Map params) throws ConfigurationException { + try { + externalProvisioner = ComponentContext.getDelegateComponentOfType(ExternalProvisioner.class); + } catch (NoSuchBeanDefinitionException e) { + throw new ConfigurationException( + String.format("Unable to find an ExternalProvisioner for the external resource: %s", name) + ); + } + externalProvisioner.configure(name, params); + dcId = (String)params.get("zone"); + pod = (String)params.get("pod"); + cluster = (String)params.get("cluster"); + this.name = name; + guid = (String)params.get("guid"); + extensionName = (String)params.get("extensionName"); + extensionRelativePath = (String)params.get("extensionRelativePath"); + extensionState = (Extension.State)params.get("extensionState"); + extensionPathReady = (boolean)params.get("extensionPathReady"); + return true; + } + + @Override + public boolean start() { + return true; + } + + @Override + public boolean stop() { + return true; + } +} diff --git a/plugins/hypervisors/external/src/main/resources/META-INF/cloudstack/core/spring-external-core-context.xml b/plugins/hypervisors/external/src/main/resources/META-INF/cloudstack/core/spring-external-core-context.xml new file mode 100644 index 00000000000..abc704c9947 --- /dev/null +++ b/plugins/hypervisors/external/src/main/resources/META-INF/cloudstack/core/spring-external-core-context.xml @@ -0,0 +1,31 @@ + + + + + diff --git a/plugins/hypervisors/external/src/main/resources/META-INF/cloudstack/external-compute/module.properties b/plugins/hypervisors/external/src/main/resources/META-INF/cloudstack/external-compute/module.properties new file mode 100644 index 00000000000..726cb6d197d --- /dev/null +++ b/plugins/hypervisors/external/src/main/resources/META-INF/cloudstack/external-compute/module.properties @@ -0,0 +1,18 @@ +# 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. +name=external-compute +parent=compute diff --git a/plugins/hypervisors/external/src/main/resources/META-INF/cloudstack/external-compute/spring-external-compute-context.xml b/plugins/hypervisors/external/src/main/resources/META-INF/cloudstack/external-compute/spring-external-compute-context.xml new file mode 100644 index 00000000000..7dd21d6f7a9 --- /dev/null +++ b/plugins/hypervisors/external/src/main/resources/META-INF/cloudstack/external-compute/spring-external-compute-context.xml @@ -0,0 +1,31 @@ + + + + + + + diff --git a/plugins/hypervisors/external/src/main/resources/META-INF/cloudstack/external-discoverer/module.properties b/plugins/hypervisors/external/src/main/resources/META-INF/cloudstack/external-discoverer/module.properties new file mode 100644 index 00000000000..2220b459b44 --- /dev/null +++ b/plugins/hypervisors/external/src/main/resources/META-INF/cloudstack/external-discoverer/module.properties @@ -0,0 +1,18 @@ +# 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. +name=external-discoverer +parent=discoverer diff --git a/plugins/hypervisors/external/src/main/resources/META-INF/cloudstack/external-discoverer/spring-external-discoverer-context.xml b/plugins/hypervisors/external/src/main/resources/META-INF/cloudstack/external-discoverer/spring-external-discoverer-context.xml new file mode 100644 index 00000000000..6a7181bdcd2 --- /dev/null +++ b/plugins/hypervisors/external/src/main/resources/META-INF/cloudstack/external-discoverer/spring-external-discoverer-context.xml @@ -0,0 +1,34 @@ + + + + + + + + diff --git a/plugins/hypervisors/external/src/main/resources/META-INF/cloudstack/external-planner/module.properties b/plugins/hypervisors/external/src/main/resources/META-INF/cloudstack/external-planner/module.properties new file mode 100644 index 00000000000..3d9d10b8537 --- /dev/null +++ b/plugins/hypervisors/external/src/main/resources/META-INF/cloudstack/external-planner/module.properties @@ -0,0 +1,18 @@ +# 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. +name=external-planner +parent=planner diff --git a/plugins/hypervisors/external/src/main/resources/META-INF/cloudstack/external-planner/spring-external-planner-context.xml b/plugins/hypervisors/external/src/main/resources/META-INF/cloudstack/external-planner/spring-external-planner-context.xml new file mode 100644 index 00000000000..da915b1557d --- /dev/null +++ b/plugins/hypervisors/external/src/main/resources/META-INF/cloudstack/external-planner/spring-external-planner-context.xml @@ -0,0 +1,34 @@ + + + + + + + + diff --git a/plugins/hypervisors/external/src/main/resources/META-INF/cloudstack/external-storage/module.properties b/plugins/hypervisors/external/src/main/resources/META-INF/cloudstack/external-storage/module.properties new file mode 100644 index 00000000000..c040872c19c --- /dev/null +++ b/plugins/hypervisors/external/src/main/resources/META-INF/cloudstack/external-storage/module.properties @@ -0,0 +1,18 @@ +# 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. +name=external-storage +parent=storage diff --git a/plugins/hypervisors/external/src/main/resources/META-INF/cloudstack/external-storage/spring-external-storage-context.xml b/plugins/hypervisors/external/src/main/resources/META-INF/cloudstack/external-storage/spring-external-storage-context.xml new file mode 100644 index 00000000000..b3f5b6c306b --- /dev/null +++ b/plugins/hypervisors/external/src/main/resources/META-INF/cloudstack/external-storage/spring-external-storage-context.xml @@ -0,0 +1,32 @@ + + + + + + diff --git a/plugins/hypervisors/external/src/test/java/org/apache/cloudstack/agent/manager/ExternalTemplateAdapterTest.java b/plugins/hypervisors/external/src/test/java/org/apache/cloudstack/agent/manager/ExternalTemplateAdapterTest.java new file mode 100644 index 00000000000..3349b61c1d9 --- /dev/null +++ b/plugins/hypervisors/external/src/test/java/org/apache/cloudstack/agent/manager/ExternalTemplateAdapterTest.java @@ -0,0 +1,173 @@ +// 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.agent.manager; + +import static org.junit.Assert.assertNull; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.Mockito.any; +import static org.mockito.Mockito.anyBoolean; +import static org.mockito.Mockito.anyLong; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + +import java.util.Collections; +import java.util.List; + +import org.apache.cloudstack.api.command.user.template.RegisterTemplateCmd; +import org.apache.cloudstack.context.CallContext; +import org.apache.cloudstack.engine.subsystem.api.storage.DataStore; +import org.apache.cloudstack.storage.datastore.db.ImageStoreDao; +import org.apache.cloudstack.storage.datastore.db.ImageStoreVO; +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.InjectMocks; +import org.mockito.Mock; +import org.mockito.MockedStatic; +import org.mockito.Mockito; +import org.mockito.Spy; +import org.mockito.junit.MockitoJUnitRunner; +import org.springframework.test.util.ReflectionTestUtils; + +import com.cloud.cpu.CPU; +import com.cloud.exception.InvalidParameterValueException; +import com.cloud.exception.ResourceAllocationException; +import com.cloud.hypervisor.Hypervisor; +import com.cloud.storage.TemplateProfile; +import com.cloud.storage.dao.VMTemplateDao; +import com.cloud.template.TemplateManager; +import com.cloud.user.Account; +import com.cloud.user.AccountManager; +import com.cloud.user.AccountVO; +import com.cloud.user.ResourceLimitService; +import com.cloud.user.UserVO; +import com.cloud.user.dao.UserDao; +import com.cloud.utils.exception.CloudRuntimeException; + +@RunWith(MockitoJUnitRunner.class) +public class ExternalTemplateAdapterTest { + @Mock + ImageStoreDao _imgStoreDao; + @Mock + AccountManager _accountMgr; + @Mock + TemplateManager templateMgr; + @Mock + ResourceLimitService _resourceLimitMgr; + @Mock + UserDao _userDao; + @Mock + VMTemplateDao _tmpltDao; + + @Spy + @InjectMocks + ExternalTemplateAdapter adapter; + + private RegisterTemplateCmd cmd; + private TemplateProfile profile; + private DataStore dataStore; + + @Before + public void setUp() { + cmd = mock(RegisterTemplateCmd.class); + profile = mock(TemplateProfile.class); + dataStore = mock(DataStore.class); + } + + @Test(expected = InvalidParameterValueException.class) + public void prepare_WhenHypervisorTypeIsNone_ThrowsInvalidParameterValueException() throws ResourceAllocationException { + Account adminAccount = new AccountVO("system", 1L, "", Account.Type.ADMIN, "uuid"); + ReflectionTestUtils.setField(adminAccount, "id", 1L); + CallContext callContext = Mockito.mock(CallContext.class); + when(callContext.getCallingAccount()).thenReturn(adminAccount); + try (MockedStatic mockedCallContext = Mockito.mockStatic(CallContext.class)) { + mockedCallContext.when(CallContext::current).thenReturn(callContext); + when(cmd.getHypervisor()).thenReturn("None"); + when(cmd.getZoneIds()).thenReturn(List.of(1L)); + when(_imgStoreDao.findRegionImageStores()).thenReturn(Collections.emptyList()); + + adapter.prepare(cmd); + } + } + + @Test + public void prepare_WhenRegionImageStoresExist_ZoneIdsAreIgnored() throws ResourceAllocationException { + Account adminAccount = new AccountVO("system", 1L, "", Account.Type.ADMIN, "uuid"); + ReflectionTestUtils.setField(adminAccount, "id", 1L); + CallContext callContext = Mockito.mock(CallContext.class); + when(callContext.getCallingAccount()).thenReturn(adminAccount); + try (MockedStatic mockedCallContext = Mockito.mockStatic(CallContext.class)) { + mockedCallContext.when(CallContext::current).thenReturn(callContext); + when(cmd.getZoneIds()).thenReturn(List.of(1L, 2L)); + when(_imgStoreDao.findRegionImageStores()).thenReturn(List.of(mock(ImageStoreVO.class))); + when(cmd.getHypervisor()).thenReturn("KVM"); + when(cmd.getDetails()).thenReturn(null); + when(cmd.getExternalDetails()).thenReturn(Collections.emptyMap()); + when(_accountMgr.getAccount(anyLong())).thenReturn(mock(com.cloud.user.Account.class)); + when(_accountMgr.isAdmin(anyLong())).thenReturn(true); + when(templateMgr.validateTemplateType(any(), anyBoolean(), anyBoolean(), eq(Hypervisor.HypervisorType.External))).thenReturn(com.cloud.storage.Storage.TemplateType.USER); + when(_userDao.findById(any())).thenReturn(mock(UserVO.class)); + when(cmd.getEntityOwnerId()).thenReturn(1L); + when(cmd.getTemplateName()).thenReturn("t"); + when(cmd.getDisplayText()).thenReturn("d"); + when(cmd.getArch()).thenReturn(CPU.CPUArch.amd64); + when(cmd.getBits()).thenReturn(64); + when(cmd.isPasswordEnabled()).thenReturn(false); + when(cmd.getRequiresHvm()).thenReturn(false); + when(cmd.getUrl()).thenReturn("http://example.com"); + when(cmd.isPublic()).thenReturn(false); + when(cmd.isFeatured()).thenReturn(false); + when(cmd.isExtractable()).thenReturn(false); + when(cmd.getFormat()).thenReturn("QCOW2"); + when(cmd.getOsTypeId()).thenReturn(1L); + when(cmd.getChecksum()).thenReturn("abc"); + when(cmd.getTemplateTag()).thenReturn(null); + when(cmd.isSshKeyEnabled()).thenReturn(false); + when(cmd.isDynamicallyScalable()).thenReturn(false); + when(cmd.isDirectDownload()).thenReturn(false); + when(cmd.isDeployAsIs()).thenReturn(false); + when(cmd.isForCks()).thenReturn(false); + when(cmd.getExtensionId()).thenReturn(null); + + TemplateProfile result = adapter.prepare(cmd); + + assertNull(result.getZoneIdList()); + } + } + + @Test(expected = CloudRuntimeException.class) + public void createTemplateForPostUpload_WhenZoneIdListIsNull_ThrowsCloudRuntimeException() { + when(profile.getFormat()).thenReturn(com.cloud.storage.Storage.ImageFormat.QCOW2); + when(profile.getZoneIdList()).thenReturn(null); + + adapter.createTemplateForPostUpload(profile); + } + + @Test(expected = CloudRuntimeException.class) + public void createTemplateForPostUpload_WhenMultipleZoneIds_ThrowsCloudRuntimeException() { + when(profile.getFormat()).thenReturn(com.cloud.storage.Storage.ImageFormat.QCOW2); + when(profile.getZoneIdList()).thenReturn(List.of(1L, 2L)); + + adapter.createTemplateForPostUpload(profile); + } + + @Test(expected = CloudRuntimeException.class) + public void prepareDelete_AlwaysThrowsCloudRuntimeException() { + adapter.prepareDelete(mock(org.apache.cloudstack.api.command.user.iso.DeleteIsoCmd.class)); + } +} diff --git a/plugins/hypervisors/external/src/test/java/org/apache/cloudstack/hypervisor/external/discoverer/ExternalServerDiscovererTest.java b/plugins/hypervisors/external/src/test/java/org/apache/cloudstack/hypervisor/external/discoverer/ExternalServerDiscovererTest.java new file mode 100644 index 00000000000..367ec58fcec --- /dev/null +++ b/plugins/hypervisors/external/src/test/java/org/apache/cloudstack/hypervisor/external/discoverer/ExternalServerDiscovererTest.java @@ -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.hypervisor.external.discoverer; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertTrue; +import static org.mockito.Mockito.anyLong; +import static org.mockito.Mockito.when; + +import java.net.URI; +import java.util.Collections; +import java.util.HashMap; +import java.util.Map; +import java.util.UUID; + +import org.apache.cloudstack.extension.ExtensionResourceMap; +import org.apache.cloudstack.framework.config.dao.ConfigurationDao; +import org.apache.cloudstack.framework.extensions.dao.ExtensionDao; +import org.apache.cloudstack.framework.extensions.dao.ExtensionResourceMapDao; +import org.apache.cloudstack.framework.extensions.manager.ExtensionsManager; +import org.apache.cloudstack.framework.extensions.vo.ExtensionResourceMapVO; +import org.apache.cloudstack.framework.extensions.vo.ExtensionVO; +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.InjectMocks; +import org.mockito.Mock; +import org.mockito.junit.MockitoJUnitRunner; + +import com.cloud.agent.AgentManager; +import com.cloud.dc.ClusterVO; +import com.cloud.dc.dao.ClusterDao; +import com.cloud.exception.DiscoveryException; +import com.cloud.host.HostVO; +import com.cloud.hypervisor.Hypervisor; + +@RunWith(MockitoJUnitRunner.class) +public class ExternalServerDiscovererTest { + + @Mock + private AgentManager agentManager; + @Mock + private ExtensionDao extensionDao; + @Mock + private ExtensionResourceMapDao extensionResourceMapDao; + @Mock + private ExtensionsManager extensionsManager; + @Mock + ClusterDao _clusterDao; + @Mock + ConfigurationDao _configDao; + @Mock + private ClusterVO clusterVO; + @Mock + private ExtensionResourceMapVO extensionResourceMapVO; + @Mock + private ExtensionVO extensionVO; + @Mock + private HostVO hostVO; + + @InjectMocks + private ExternalServerDiscoverer discoverer; + + @Before + public void setUp() { + } + + @Test + public void testGetResourceGuidFromName() { + String name = "test-resource"; + String guid = discoverer.getResourceGuidFromName(name); + assertTrue(guid.startsWith("External:")); + assertTrue(guid.length() > "External:".length()); + } + + @Test + public void testAddExtensionDataToResourceParams() { + Map params = new HashMap<>(); + when(extensionVO.getName()).thenReturn("ext"); + when(extensionVO.getRelativePath()).thenReturn("entry.sh"); + when(extensionVO.getState()).thenReturn(ExtensionVO.State.Enabled); + + discoverer.addExtensionDataToResourceParams(extensionVO, params); + + assertEquals("ext", params.get("extensionName")); + assertEquals("entry.sh", params.get("extensionRelativePath")); + assertEquals(ExtensionVO.State.Enabled, params.get("extensionState")); + } + + @Test(expected = DiscoveryException.class) + public void testFindThrowsWhenClusterIdNull() throws Exception { + discoverer.find(1L, 2L, null, new URI("http://host"), "user", "pass", Collections.emptyList()); + } + + @Test(expected = DiscoveryException.class) + public void testFindThrowsWhenClusterNotExternal() throws Exception { + when(clusterVO.getHypervisorType()).thenReturn(Hypervisor.HypervisorType.KVM); + when(_clusterDao.findById(1L)).thenReturn(clusterVO); + discoverer.find(1L, 2L, 1L, new URI("http://host"), "user", "pass", Collections.emptyList()); + } + + @Test(expected = DiscoveryException.class) + public void testFindThrowsWhenPodIdNull() throws Exception { + when(clusterVO.getHypervisorType()).thenReturn(Hypervisor.HypervisorType.External); + when(_clusterDao.findById(1L)).thenReturn(clusterVO); + discoverer.find(1L, null, 1L, new URI("http://host"), "user", "pass", Collections.emptyList()); + } + + @Test(expected = DiscoveryException.class) + public void testFindThrowsWhenNoExtensionResourceMap() throws Exception { + when(clusterVO.getHypervisorType()).thenReturn(Hypervisor.HypervisorType.External); + when(_clusterDao.findById(1L)).thenReturn(clusterVO); + when(extensionResourceMapDao.findByResourceIdAndType(1L, ExtensionResourceMap.ResourceType.Cluster)).thenReturn(null); + discoverer.find(1L, 2L, 1L, new URI("http://host"), "user", "pass", Collections.emptyList()); + } + + @Test(expected = DiscoveryException.class) + public void testFindThrowsWhenNoExtensionVO() throws Exception { + when(clusterVO.getHypervisorType()).thenReturn(Hypervisor.HypervisorType.External); + when(_clusterDao.findById(1L)).thenReturn(clusterVO); + when(extensionResourceMapDao.findByResourceIdAndType(1L, ExtensionResourceMap.ResourceType.Cluster)).thenReturn(extensionResourceMapVO); + when(extensionResourceMapVO.getExtensionId()).thenReturn(10L); + when(extensionDao.findById(10L)).thenReturn(null); + discoverer.find(1L, 2L, 1L, new URI("http://host"), "user", "pass", Collections.emptyList()); + } + + @Test + public void testBuildConfigParamsAddsExtensionData() { + when(hostVO.getClusterId()).thenReturn(1L); + HashMap params = new HashMap<>(); + params.put("cluster", "1"); + discoverer.extensionResourceMapDao = extensionResourceMapDao; + discoverer.extensionDao = extensionDao; + when(extensionResourceMapDao.findByResourceIdAndType(1L, ExtensionResourceMap.ResourceType.Cluster)).thenReturn(extensionResourceMapVO); + when(extensionResourceMapVO.getExtensionId()).thenReturn(10L); + when(extensionDao.findById(10L)).thenReturn(extensionVO); + when(extensionVO.getName()).thenReturn("ext"); + when(extensionVO.getRelativePath()).thenReturn("entry.sh"); + when(extensionVO.getState()).thenReturn(ExtensionVO.State.Enabled); + when(_clusterDao.findById(anyLong())).thenReturn(clusterVO); + when(clusterVO.getGuid()).thenReturn(UUID.randomUUID().toString()); + + HashMap result = discoverer.buildConfigParams(hostVO); + assertEquals("ext", result.get("extensionName")); + assertEquals("entry.sh", result.get("extensionRelativePath")); + assertEquals(ExtensionVO.State.Enabled, result.get("extensionState")); + } + + @Test + public void testMatchHypervisor() { + assertTrue(discoverer.matchHypervisor(null)); + assertTrue(discoverer.matchHypervisor("External")); + assertFalse(discoverer.matchHypervisor("KVM")); + } + + @Test + public void testGetHypervisorType() { + assertEquals(Hypervisor.HypervisorType.External, discoverer.getHypervisorType()); + } + + @Test + public void testIsRecurringAndTimeout() { + assertFalse(discoverer.isRecurring()); + assertEquals(0, discoverer.getTimeout()); + } +} diff --git a/plugins/hypervisors/external/src/test/java/org/apache/cloudstack/hypervisor/external/provisioner/ExternalPathPayloadProvisionerTest.java b/plugins/hypervisors/external/src/test/java/org/apache/cloudstack/hypervisor/external/provisioner/ExternalPathPayloadProvisionerTest.java new file mode 100644 index 00000000000..8c63a20fa31 --- /dev/null +++ b/plugins/hypervisors/external/src/test/java/org/apache/cloudstack/hypervisor/external/provisioner/ExternalPathPayloadProvisionerTest.java @@ -0,0 +1,750 @@ +// +// 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.hypervisor.external.provisioner; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertNull; +import static org.junit.Assert.assertTrue; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.anyInt; +import static org.mockito.ArgumentMatchers.anyLong; +import static org.mockito.ArgumentMatchers.anyMap; +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.when; + +import java.io.File; +import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.Paths; +import java.nio.file.StandardOpenOption; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.Properties; + +import javax.naming.ConfigurationException; + +import org.apache.cloudstack.api.ApiConstants; +import org.apache.cloudstack.extension.Extension; +import org.apache.cloudstack.framework.extensions.manager.ExtensionsManager; +import org.apache.logging.log4j.Logger; +import org.junit.After; +import org.junit.Assume; +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.InjectMocks; +import org.mockito.Mock; +import org.mockito.MockedStatic; +import org.mockito.Mockito; +import org.mockito.Spy; +import org.mockito.junit.MockitoJUnitRunner; +import org.springframework.test.util.ReflectionTestUtils; + +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.agent.api.to.VirtualMachineTO; +import com.cloud.host.HostVO; +import com.cloud.host.dao.HostDao; +import com.cloud.hypervisor.Hypervisor; +import com.cloud.hypervisor.HypervisorGuru; +import com.cloud.hypervisor.HypervisorGuruManager; +import com.cloud.template.VirtualMachineTemplate; +import com.cloud.utils.FileUtil; +import com.cloud.utils.Pair; +import com.cloud.utils.PropertiesUtil; +import com.cloud.utils.script.Script; +import com.cloud.vm.UserVmVO; +import com.cloud.vm.VMInstanceVO; +import com.cloud.vm.VirtualMachine; +import com.cloud.vm.VirtualMachineProfile; +import com.cloud.vm.dao.UserVmDao; +import com.cloud.vm.dao.VMInstanceDao; + +@RunWith(MockitoJUnitRunner.class) +public class ExternalPathPayloadProvisionerTest { + + @Spy + @InjectMocks + private ExternalPathPayloadProvisioner provisioner; + + @Mock + private UserVmDao userVmDao; + + @Mock + private HostDao hostDao; + + @Mock + private VMInstanceDao vmInstanceDao; + + @Mock + private HypervisorGuruManager hypervisorGuruManager; + + @Mock + private HypervisorGuru hypervisorGuru; + + @Mock + private Logger logger; + + @Mock + private ExtensionsManager extensionsManager; + + private File tempDir; + private File tempDataDir; + private Properties testProperties; + private File testScript; + + @Before + public void setUp() throws IOException { + tempDir = Files.createTempDirectory("extensions-test").toFile(); + tempDataDir = Files.createTempDirectory("extensions-data-test").toFile(); + + testScript = new File(tempDir, "test-extension.sh"); + testScript.createNewFile(); + resetTestScript(); + + testProperties = new Properties(); + testProperties.setProperty("extensions.deployment.mode", "developer"); + + ReflectionTestUtils.setField(provisioner, "extensionsDirectory", tempDir.getAbsolutePath()); + ReflectionTestUtils.setField(provisioner, "extensionsDataDirectory", tempDataDir.getAbsolutePath()); + + try (MockedStatic propertiesUtilMock = Mockito.mockStatic(PropertiesUtil.class)) { + File mockPropsFile = mock(File.class); + propertiesUtilMock.when(() -> PropertiesUtil.findConfigFile(anyString())).thenReturn(mockPropsFile); + } + } + + @After + public void tearDown() { + if (tempDir != null && tempDir.exists()) { + deleteDirectory(tempDir); + } + if (tempDataDir != null && tempDataDir.exists()) { + deleteDirectory(tempDataDir); + } + } + + private void deleteDirectory(File dir) { + if (dir.isDirectory()) { + File[] files = dir.listFiles(); + if (files != null) { + for (File file : files) { + deleteDirectory(file); + } + } + } + dir.delete(); + } + + private void resetTestScript() { + testScript.setExecutable(true); + testScript.setReadable(true); + testScript.setWritable(true); + } + + @Test + public void testLoadAccessDetails() { + Map> externalDetails = new HashMap<>(); + externalDetails.put(ApiConstants.EXTENSION, Map.of("key1", "value1")); + + VirtualMachineTO vmTO = mock(VirtualMachineTO.class); + when(vmTO.getUuid()).thenReturn("test-uuid"); + when(vmTO.getName()).thenReturn("test-vm"); + + Map result = provisioner.loadAccessDetails(externalDetails, vmTO); + + assertNotNull(result); + assertEquals(externalDetails, result.get(ApiConstants.EXTERNAL_DETAILS)); + assertEquals("test-uuid", result.get(ApiConstants.VIRTUAL_MACHINE_ID)); + assertEquals("test-vm", result.get(ApiConstants.VIRTUAL_MACHINE_NAME)); + assertEquals(vmTO, result.get("cloudstack.vm.details")); + } + + @Test + public void testLoadAccessDetailsWithNullExternalDetails() { + VirtualMachineTO vmTO = mock(VirtualMachineTO.class); + when(vmTO.getUuid()).thenReturn("test-uuid"); + when(vmTO.getName()).thenReturn("test-vm"); + + Map result = provisioner.loadAccessDetails(null, vmTO); + + assertNotNull(result); + assertNull(result.get(ApiConstants.EXTERNAL_DETAILS)); + assertEquals("test-uuid", result.get(ApiConstants.VIRTUAL_MACHINE_ID)); + assertEquals("test-vm", result.get(ApiConstants.VIRTUAL_MACHINE_NAME)); + } + + @Test + public void testGetExtensionCheckedPathValidFile() { + String result = provisioner.getExtensionCheckedPath("test-extension", "test-extension.sh"); + + assertEquals(testScript.getAbsolutePath(), result); + } + + @Test + public void testGetExtensionCheckedPathFileNotExists() { + String result = provisioner.getExtensionCheckedPath("test-extension", "nonexistent.sh"); + + assertNull(result); + } + + @Test + public void testGetExtensionCheckedPathNoExecutePermissions() { + testScript.setExecutable(false); + String result = provisioner.getExtensionCheckedPath("test-extension", "test-extension.sh"); + assertNull(result); + Mockito.verify(logger).error("{} is not executable", "Entry point [" + testScript.getAbsolutePath() + "] for extension: test-extension"); + } + + @Test + public void testGetExtensionCheckedPathNoReadPermissions() { + testScript.setWritable(false); + testScript.setReadable(false); + Assume.assumeFalse("Skipping test as file can not be marked unreadable", testScript.canRead()); + String result = provisioner.getExtensionCheckedPath("test-extension", "test-extension.sh"); + assertNull(result); + Mockito.verify(logger).error("{} is not readable", "Entry point [" + testScript.getAbsolutePath() + "] for extension: test-extension"); + } + + @Test + public void testCheckExtensionsDirectoryValid() { + boolean result = provisioner.checkExtensionsDirectory(); + assertTrue(result); + } + + @Test + public void testCheckExtensionsDirectoryInvalid() { + ReflectionTestUtils.setField(provisioner, "extensionsDirectory", "/nonexistent/path"); + + boolean result = provisioner.checkExtensionsDirectory(); + assertFalse(result); + } + + @Test + public void testCreateOrCheckExtensionsDataDirectory() throws ConfigurationException { + provisioner.createOrCheckExtensionsDataDirectory(); + Mockito.verify(logger).info("Extensions data directory path: {}", tempDataDir.getAbsolutePath()); + } + + @Test(expected = ConfigurationException.class) + public void testCreateOrCheckExtensionsDataDirectoryCreateThrowsExceptionFail() throws ConfigurationException { + ReflectionTestUtils.setField(provisioner, "extensionsDataDirectory", "/nonexistent/path"); + try(MockedStatic filesMock = Mockito.mockStatic(Files.class)) { + filesMock.when(() -> Files.createDirectories(any())).thenThrow(new IOException("fail")); + provisioner.createOrCheckExtensionsDataDirectory(); + } + } + + @Test(expected = ConfigurationException.class) + public void testCreateOrCheckExtensionsDataDirectoryNoCreateFail() throws ConfigurationException { + ReflectionTestUtils.setField(provisioner, "extensionsDataDirectory", "/nonexistent/path"); + try(MockedStatic filesMock = Mockito.mockStatic(Files.class)) { + filesMock.when(() -> Files.createDirectories(any())).thenReturn(mock(Path.class)); + provisioner.createOrCheckExtensionsDataDirectory(); + } + } + + @Test + public void testGetExtensionPath() { + String result = provisioner.getExtensionPath("test-extension.sh"); + String expected = tempDir.getAbsolutePath() + File.separator + "test-extension.sh"; + assertEquals(expected, result); + } + + @Test + public void testGetChecksumForExtensionPath() { + String result = provisioner.getChecksumForExtensionPath("test-extension", "test-extension.sh"); + + assertNotNull(result); + } + + @Test + public void testGetChecksumForExtensionPath_InvalidFile() { + String result = provisioner.getChecksumForExtensionPath("test-extension", "nonexistent.sh"); + + assertNull(result); + } + + @Test + public void testPrepareExternalProvisioning() { + try (MockedStatic - + + @@ -58,7 +109,7 @@ - +
@@ -73,18 +124,18 @@ + title="Move/Drag viewport"> - + + title="Show extra keys">
- + title="Full screen"> @@ -255,10 +306,10 @@
-
- +
+
- + @@ -273,7 +324,7 @@ - +
@@ -288,7 +339,7 @@ - +
@@ -298,21 +349,21 @@ The server has provided the following identifying information:
- Fingerprint: + Fingerprint:
Please verify that the information is correct and press "Approve". Otherwise press "Reject".
-
- - +
+ +
- +
@@ -326,17 +377,17 @@
-
- +
+
- +
- +
diff --git a/systemvm/agent/noVNC/vnc_lite.html b/systemvm/agent/noVNC/vnc_lite.html index 1eea4602222..8e6aaeabdec 100644 --- a/systemvm/agent/noVNC/vnc_lite.html +++ b/systemvm/agent/noVNC/vnc_lite.html @@ -7,7 +7,7 @@ This is a self-contained file which doesn't import WebUtil or external CSS. - Copyright (C) 2019 The noVNC Authors + Copyright (C) 2019 The noVNC authors noVNC is licensed under the MPL 2.0 (see LICENSE.txt) This file is licensed under the 2-Clause BSD license (see LICENSE.txt). @@ -87,7 +87,7 @@ // When this function is called, the server requires // credentials to authenticate function credentialsAreRequired(e) { - const password = prompt("Password Required:"); + const password = prompt("Password required:"); rfb.sendCredentials({ password: password }); } @@ -119,20 +119,14 @@ // query string. If the variable isn't defined in the URL // it returns the default value instead. function readQueryVariable(name, defaultValue) { - // A URL with a query parameter can look like this (But will most probably get logged on the http server): + // A URL with a query parameter can look like this: // https://www.example.com?myqueryparam=myvalue // - // For privacy (Using a hastag #, the parameters will not be sent to the server) - // the url can be requested in the following way: - // https://www.example.com#myqueryparam=myvalue&password=secreatvalue - // - // Even Mixing public and non public parameters will work: - // https://www.example.com?nonsecretparam=example.com#password=secreatvalue - // // Note that we use location.href instead of location.search // because Firefox < 53 has a bug w.r.t location.search const re = new RegExp('.*[?&]' + name + '=([^&#]*)'), - match = ''.concat(document.location.href, window.location.hash).match(re); + match = document.location.href.match(re); + if (typeof defaultValue === 'undefined') { defaultValue = null; } if (match) { diff --git a/systemvm/agent/ui/viewer.ftl b/systemvm/agent/ui/viewer.ftl index e6b12071111..fef0d7e0b68 100644 --- a/systemvm/agent/ui/viewer.ftl +++ b/systemvm/agent/ui/viewer.ftl @@ -18,10 +18,10 @@ under the License. --> - - - - + + + + ${title} @@ -44,7 +44,7 @@ under the License.
-
- diff --git a/ui/public/locales/ar.json b/ui/public/locales/ar.json index 20499c63e04..e8e4db66cb5 100644 --- a/ui/public/locales/ar.json +++ b/ui/public/locales/ar.json @@ -14,12 +14,12 @@ "label.account.specific": "Account-Specific", "label.accounts": "Accounts", "label.accounttype": "Account Type", -"label.acl.export": "Export ACLs", +"label.acl.export": "Export ACL rules", "label.acl.id": "ACL ID", -"label.acl.list.rules": "ACL List Rules", +"label.acl.rules": "ACL Rules", "label.acl.reason.description": "Enter the reason behind an ACL rule.", "label.aclid": "ACL", -"label.aclname": "ACL Name", +"label.acl.rule.name": "ACL Name", "label.acquire.new.ip": "Acquire New IP", "label.acquire.new.secondary.ip": "Acquire new secondary IP", "label.action": "Action", @@ -119,8 +119,8 @@ "label.activeviewersessions": "Active Sessions", "label.add": "Add", "label.add.account": "Add Account", -"label.add.acl": "\u0625\u0636\u0627\u0641\u0629 ACL", -"label.add.acl.list": "Add ACL List", +"label.add.acl.rule": "\u0625\u0636\u0627\u0641\u0629 ACL", +"label.add.acl": "Add ACL", "label.add.affinity.group": "Add new affinity group", "label.add.baremetal.dhcp.device": "Add Baremetal DHCP Device", "label.add.bigswitchbcf.device": "Add BigSwitch BCF Controller", @@ -142,12 +142,12 @@ "label.add.ip.range": "Add IP Range", "label.add.isolated.network": "Add Isolated Network", "label.add.ldap.account": "Add LDAP account", -"label.add.list.name": "ACL List Name", +"label.add.acl.name": "ACL Name", "label.add.more": "Add More", "label.add.netscaler.device": "Add Netscaler device", "label.add.network": "Add Network", "label.add.network.acl": "\u0625\u0636\u0627\u0641\u0629 \u0634\u0628\u0643\u0629 ACL", -"label.add.network.acl.list": "Add Network ACL List", +"label.add.network.acl": "Add Network ACL", "label.add.network.offering": "Add network offering", "label.add.new.gateway": "\u0623\u0636\u0641 \u0628\u0648\u0627\u0628\u0629 \u062c\u062f\u064a\u062f\u0629", "label.add.new.tier": "\u0625\u0636\u0627\u0641\u0629 \u0637\u0628\u0642\u0629 \u062c\u062f\u064a\u062f\u0629", @@ -334,7 +334,7 @@ "label.default.use": "Default Use", "label.default.view": "\u0637\u0631\u064a\u0642\u0629 \u0627\u0644\u0639\u0631\u0636 \u0627\u0644\u0627\u0641\u062a\u0631\u0627\u0636\u064a\u0629", "label.delete": "Delete", -"label.delete.acl.list": "Delete ACL List", +"label.delete.acl": "Delete ACL", "label.delete.affinity.group": "Delete Affinity Group", "label.delete.alerts": "Delete alerts", "label.delete.bigswitchbcf": "Remove BigSwitch BCF Controller", @@ -419,7 +419,7 @@ "label.dpd": "\u0643\u0634\u0641 \u0627\u0644\u0642\u0631\u064a\u0646 \u0627\u0644\u0645\u0641\u0642\u0648\u062f", "label.driver": "Driver", "label.edit": "Edit", -"label.edit.acl.list": "Edit ACL List", +"label.edit.acl": "Edit ACL", "label.edit.acl.rule": "Edit ACL rule", "label.edit.project.details": "\u0627\u0636\u0627\u0641\u0629 \u062a\u0641\u0627\u0635\u064a\u0644 \u0627\u0644\u0645\u0634\u0631\u0648\u0639", "label.edit.role": "Edit Role", @@ -924,7 +924,6 @@ "label.remove.vpc.offering": "Remove VPC offering", "label.removing": "Removing", "label.replace.acl": "Replace ACL", -"label.replace.acl.list": "Replace ACL List", "label.required": "Required", "label.requireshvm": "HVM", "label.requiresupgrade": "Requires Upgrade", @@ -1150,8 +1149,7 @@ "label.usehttps": "\u0627\u0633\u062a\u062e\u062f\u0645 HTTPS", "label.usenewdiskoffering": "Replace disk offering?", "label.user": "User", -"label.userdata": "Userdata", -"label.userdatal2": "User Data", +"label.user.data": "User Data", "label.username": "Username", "label.users": "Users", "label.utilization": "Utilisation", @@ -1329,7 +1327,7 @@ "message.confirm.archive.selected.alerts": "Please confirm you would like to archive the selected alerts", "message.confirm.archive.selected.events": "Please confirm you would like to archive the selected events", "message.confirm.attach.disk": "Are you sure you want to attach disk?", -"message.confirm.delete.acl.list": "Are you sure you want to delete this ACL list?", +"message.confirm.delete.acl": "Are you sure you want to delete this ACL?", "message.confirm.delete.bigswitchbcf": "Please confirm that you would like to delete this BigSwitch BCF Controller", "message.confirm.delete.brocadevcs": "Please confirm that you would like to delete Brocade Vcs Switch", "message.confirm.delete.ciscoasa1000v": "Please confirm you want to delete CiscoASA1000v", diff --git a/ui/public/locales/ca.json b/ui/public/locales/ca.json index 30ed3161448..d3e417eb821 100644 --- a/ui/public/locales/ca.json +++ b/ui/public/locales/ca.json @@ -14,12 +14,12 @@ "label.account.specific": "Account-Specific", "label.accounts": "Accounts", "label.accounttype": "Account Type", -"label.acl.export": "Export ACLs", +"label.acl.export": "Export ACL rules", "label.acl.id": "ACL ID", -"label.acl.list.rules": "ACL List Rules", +"label.acl.rules": "ACL Rules", "label.acl.reason.description": "Enter the reason behind an ACL rule.", "label.aclid": "ACL", -"label.aclname": "ACL Name", +"label.acl.rule.name": "ACL Name", "label.acquire.new.ip": "Acquire New IP", "label.acquire.new.secondary.ip": "Acquire new secondary IP", "label.action": "Action", @@ -119,8 +119,8 @@ "label.activeviewersessions": "Active Sessions", "label.add": "Add", "label.add.account": "Add Account", +"label.add.acl.rule": "Add ACL rule", "label.add.acl": "Add ACL", -"label.add.acl.list": "Add ACL List", "label.add.affinity.group": "Add new affinity group", "label.add.baremetal.dhcp.device": "Add Baremetal DHCP Device", "label.add.bigswitchbcf.device": "Add BigSwitch BCF Controller", @@ -142,12 +142,12 @@ "label.add.ip.range": "Add IP Range", "label.add.isolated.network": "Add Isolated Network", "label.add.ldap.account": "Add LDAP account", -"label.add.list.name": "ACL List Name", +"label.add.acl.name": "ACL Name", "label.add.more": "Add More", "label.add.netscaler.device": "Add Netscaler device", "label.add.network": "Add Network", "label.add.network.acl": "Add network ACL", -"label.add.network.acl.list": "Add Network ACL List", +"label.add.network.acl": "Add Network ACL", "label.add.network.offering": "Add network offering", "label.add.new.gateway": "Add new gateway", "label.add.new.tier": "Add new tier", @@ -334,7 +334,7 @@ "label.default.use": "Default Use", "label.default.view": "Default View", "label.delete": "Delete", -"label.delete.acl.list": "Delete ACL List", +"label.delete.acl": "Delete ACL", "label.delete.affinity.group": "Delete Affinity Group", "label.delete.alerts": "Delete alerts", "label.delete.bigswitchbcf": "Remove BigSwitch BCF Controller", @@ -419,7 +419,7 @@ "label.dpd": "Dead Peer Detection", "label.driver": "Driver", "label.edit": "Edit", -"label.edit.acl.list": "Edit ACL List", +"label.edit.acl": "Edit ACL", "label.edit.acl.rule": "Edit ACL rule", "label.edit.project.details": "Editar detalls del projecte", "label.edit.role": "Edit Role", @@ -924,7 +924,6 @@ "label.remove.vpc.offering": "Remove VPC offering", "label.removing": "Esborrant", "label.replace.acl": "Replace ACL", -"label.replace.acl.list": "Replace ACL List", "label.required": "Required", "label.requireshvm": "HVM", "label.requiresupgrade": "Requires Upgrade", @@ -1150,8 +1149,7 @@ "label.usehttps": "Use HTTPS", "label.usenewdiskoffering": "Replace disk offering?", "label.user": "User", -"label.userdata": "Userdata", -"label.userdatal2": "User Data", +"label.user.data": "User Data", "label.username": "Username", "label.users": "Users", "label.utilization": "Utilisation", @@ -1329,7 +1327,7 @@ "message.confirm.archive.selected.alerts": "Please confirm you would like to archive the selected alerts", "message.confirm.archive.selected.events": "Please confirm you would like to archive the selected events", "message.confirm.attach.disk": "Are you sure you want to attach disk?", -"message.confirm.delete.acl.list": "Are you sure you want to delete this ACL list?", +"message.confirm.delete.acl": "Are you sure you want to delete this ACL?", "message.confirm.delete.bigswitchbcf": "Please confirm that you would like to delete this BigSwitch BCF Controller", "message.confirm.delete.brocadevcs": "Please confirm that you would like to delete Brocade Vcs Switch", "message.confirm.delete.ciscoasa1000v": "Please confirm you want to delete CiscoASA1000v", diff --git a/ui/public/locales/de_DE.json b/ui/public/locales/de_DE.json index c20bf175826..fd29ac3eb54 100644 --- a/ui/public/locales/de_DE.json +++ b/ui/public/locales/de_DE.json @@ -26,12 +26,12 @@ "label.account.specific": "Besonderheiten des Benutzerkontos", "label.accounts": "Benutzerkonten", "label.accounttype": "Benutzerkontotyp", -"label.acl.export": "Export ACLs", +"label.acl.export": "Export ACL rules", "label.acl.id": "ACL-Kennung", -"label.acl.list.rules": "ACL-Listenregeln", +"label.acl.rules": "ACL-Listenregeln", "label.acl.reason.description": "Enter the reason behind an ACL rule.", "label.aclid": "ACL", -"label.aclname": "ACL-Name", +"label.acl.rule.name": "ACL-Name", "label.acquire.new.ip": "Neue IP erwerben", "label.acquire.new.secondary.ip": "Neue sekundäre IP anfordern", "label.acquiring.ip": "IP anfordern", @@ -144,8 +144,8 @@ "label.activeviewersessions": "Aktive Sitzungen", "label.add": "Hinzufügen", "label.add.account": "Konto hinzufügen", -"label.add.acl": "ACL hinzufügen", -"label.add.acl.list": "ACL-Liste hinzufügen", +"label.add.acl.rule": "ACL hinzufügen", +"label.add.acl": "ACL-Liste hinzufügen", "label.add.affinity.group": "Neue Affinitätsgruppe hinzufügen", "label.add.baremetal.dhcp.device": "Baremetal DHCP-Gerät hinzufügen", "label.add.bigswitchbcf.device": "Füge BigSwitch BCF Controller hinzu", @@ -169,12 +169,12 @@ "label.add.isolated.network": "Isoliertes Netzwerk hinzufügen", "label.add.kubernetes.cluster": "Kubernetes Cluster hinzufügen", "label.add.ldap.account": "LDAP-Konto hinzufügen", -"label.add.list.name": "ACL-Listename", +"label.add.acl.name": "ACL-Listename", "label.add.more": "Mehr hinzufügen", "label.add.netscaler.device": "Netscaler-Gerät hinzufügen", "label.add.network": "Netzwerk hinzufügen", "label.add.network.acl": "Netzwerk-ACL hinzufügen", -"label.add.network.acl.list": "Netzwerk-ACL-Liste hinzufügen", +"label.add.network.acl": "Netzwerk-ACL-Liste hinzufügen", "label.add.network.offering": "Netzwerkangebot hinzufügen", "label.add.new.gateway": "Neues Gateway hinzufügen", "label.add.new.tier": "Neue Ebene hinzufügen", @@ -417,7 +417,7 @@ "label.default.view": "Standardansicht", "label.defaultnetwork": "Standard-Netzwerk", "label.delete": "Löschen", -"label.delete.acl.list": "ACL-Liste ersetzen", +"label.delete.acl": "ACL-Liste ersetzen", "label.delete.affinity.group": "Affinitätsgruppe entfernen", "label.delete.alerts": "Alarme löschen", "label.delete.backup": "Backup löschen", @@ -525,7 +525,7 @@ "label.driver": "Treiber", "label.dynamicscalingenabled": "Dynamische Skalierung aktiviert", "label.edit": "Bearbeiten", -"label.edit.acl.list": "Edit ACL List", +"label.edit.acl": "Edit ACL", "label.edit.acl.rule": "ACL-Regel bearbeiten", "label.edit.project.details": "Projektdetails bearbeiten", "label.edit.role": "Rolle bearbeiten", @@ -948,7 +948,7 @@ "label.netscaler.vpx": "NetScaler VPX LoadBalancer", "label.network": "Netzwerk", "label.network.acl": "Netzwerk-ACL", -"label.network.acl.lists": "Netzwerk ACL Listen", +"label.network.acls": "Netzwerk ACL Listen", "label.network.addvm": "Netzwerk zur VM hinzufügen", "label.network.desc": "Netzwerkbeschreibung", "label.network.domain": "Netzwerk-Domain", @@ -1198,7 +1198,6 @@ "label.remove.vpc.offering": "VPC-Angebot entfernen", "label.removing": "am Entfernen", "label.replace.acl": "ACL ersetzen", -"label.replace.acl.list": "ACL-Liste ersetzen", "label.report.bug": "Fehler melden", "label.required": "Erforderlich", "label.requireshvm": "HVM", @@ -1492,8 +1491,7 @@ "label.usenewdiskoffering": "Replace disk offering?", "label.user": "Benutzer", "label.user.conflict": "Konflikt", -"label.userdata": "Benutzerdaten", -"label.userdatal2": "Benutzerdaten", +"label.user.data": "Benutzerdaten", "label.username": "Benutzername", "label.users": "Benutzer", "label.usersource": "Benutzertyp", @@ -1733,7 +1731,7 @@ "message.confirm.archive.selected.alerts": "Bitte bestätigen Sie, dass Sie die ausgewählten Alarme archivieren möchten", "message.confirm.archive.selected.events": "Bitte bestätigen Sie, dass Sie die ausgewählten Vorgänge archivieren möchten", "message.confirm.attach.disk": "Sind Sie sicher, dass Sie eine Platte hinzufügen möchten?", -"message.confirm.delete.acl.list": "Sind Sie sicher, dass Sie diese ACL-Liste löschen möchten?", +"message.confirm.delete.acl": "Sind Sie sicher, dass Sie diese ACL-Liste löschen möchten?", "message.confirm.delete.bigswitchbcf": "Bitte bestätigen Sie, dass Sie diesen BigSwitch BCF Controller löschen möchten", "message.confirm.delete.brocadevcs": "Bitte bestätigen Sie, dass Sie Brocade Vcs Switch löschen möchten", "message.confirm.delete.ciscoasa1000v": "Bitte bestätigen Sie, dass Sie CiscoASA1000v löschen möchten", diff --git a/ui/public/locales/el_GR.json b/ui/public/locales/el_GR.json index f072dd43c09..6b939f52029 100644 --- a/ui/public/locales/el_GR.json +++ b/ui/public/locales/el_GR.json @@ -33,10 +33,10 @@ "label.access.kubernetes.nodes": "Πρόσβαση στους κόμβους Κυβερνητών", "label.acl.export": "Εξαγωγή Λίστας Πρόσβασης", "label.acl.id": "Αναγνωριστικό Λίστας Πρόσβασης", -"label.acl.list.rules": "Κανόνες λίστας Πρόσωασης", +"label.acl.rules": "Κανόνες λίστας Πρόσωασης", "label.acl.reason.description": "Εισαγάγετε αιτιολογία για τον κανόνα", "label.aclid": "Λίστα πρόσβασης", -"label.aclname": "Όνομα Λίστας Πρόσβασης", +"label.acl.rule.name": "Όνομα Λίστας Πρόσβασης", "label.acquire.new.ip": "Απόκτηση νέας διεύθυνση IP", "label.acquire.new.secondary.ip": "Απόκτηση νέας δευτερεύουσας διεύθυνσης IP", "label.acquiring.ip": "Απόδοση IP", @@ -173,7 +173,7 @@ "label.action.unmanage.virtualmachine": "Μη διαχείριση εικονικής μηχανής", "label.action.update.offering.access": "Ενημέρωση πρόσβασης για προσφορές", "label.action.update.resource.count": "Ενημέρωση πλήθους πόρων", -"label.action.userdata.reset": "Επαναφορά δεδομένων χρήστη", +"label.action.user.data.reset": "Επαναφορά δεδομένων χρήστη", "label.action.vmsnapshot.create": "Λήψη στιγμιότυπου εικονικής μηχανής", "label.action.vmsnapshot.delete": "Διαγραφή στιγμιότυπου εικονικής μηχανής", "label.action.vmsnapshot.revert": "Επαναφορά στο στιγμιότυπο εικονικής μηχανής", @@ -183,8 +183,8 @@ "label.activeviewersessions": "Ενεργές περίοδοι λειτουργίας", "label.add": "Προσθήκη", "label.add.account": "Προσθήκη λογαριασμού", -"label.add.acl": "Προσθήκη εγγραφής στην λίστα πρόσβασης", -"label.add.acl.list": "Προσθήκη λίστας πρόσβασης", +"label.add.acl.rule": "Προσθήκη εγγραφής στην λίστα πρόσβασης", +"label.add.acl": "Προσθήκη λίστας πρόσβασης", "label.add.affinity.group": "Προσθήκη νέας ομάδας συνάφειας", "label.add.baremetal.dhcp.device": "Προσθήκη συσκευής DHCP baremetal", "label.add.bigswitchbcf.device": "Προσθήκη ελεγκτή BCF BigSwitch", @@ -209,12 +209,12 @@ "label.add.isolated.network": "Προσθήκη απομονωμένου δικτύου", "label.add.kubernetes.cluster": "Προσθήκη ομάδας Κυβερνητών", "label.add.ldap.account": "Προσθήκη λογαριασμού LDAP", -"label.add.list.name": "Όνομα λίστας Πρόσβασης", +"label.add.acl.name": "Όνομα λίστας Πρόσβασης", "label.add.more": "Προσθήκη περισσότερων", "label.add.netscaler.device": "Προσθήκη συσκευής Netsccaler", "label.add.network": "Προσθήκη δικτύου", "label.add.network.acl": "Προσθήκη εγγραφής πρόσβασης δικτύου", -"label.add.network.acl.list": "Προσθήκη λίστας πρόσβασης δικτύου", +"label.add.network.acl": "Προσθήκη λίστας πρόσβασης δικτύου", "label.add.network.offering": "Προσθήκη προσφοράς υπηρεσίας δικτύου", "label.add.network.permission": "Προσθήκη δικαιωμάτων δικτύου", "label.add.new.gateway": "Προσθήκη νέας πύλης", @@ -516,7 +516,7 @@ "label.default.view": "Προεπιλεγμένη προβολή", "label.defaultnetwork": "Προεπιλεγμένο δίκτυο", "label.delete": "Διαγραφή", -"label.delete.acl.list": "Διαγραφή λίστας πρόσβασης", +"label.delete.acl": "Διαγραφή λίστας πρόσβασης", "label.delete.affinity.group": "Διαγραφή ομάδας συνάφειας", "label.delete.alerts": "Διαγραφή ειδοποιήσεων", "label.delete.backup": "Διαγραφή αντιγράφου ασφαλείας", @@ -649,7 +649,7 @@ "label.dynamicscalingenabled": "Δυναμική αναπροσαρμογή Ενεργοποίημένη", "label.dynamicscalingenabled.tooltip": "Η εικονική μηχανή μπορεί να αναπροσαρμόζεται δυναμικά μόνο αν το πρότυπο, η προσφερόμενη υπηρεσία και οι γενικές ρυθμίσεις έχουν την επιλογή δυναμικής αναπροσαρμογής ενεργοποιήμενη.", "label.edit": "Επεξεργασία", -"label.edit.acl.list": "Επεξεργασία λίστας Πρόσβασης", +"label.edit.acl": "Επεξεργασία λίστας Πρόσβασης", "label.edit.acl.rule": "Επεξεργασία κανόνα Λίστας Πρόσβασης", "label.edit.project.details": "Επεξεργασία λεπτομερειών έργου", "label.edit.project.role": "Επεξεργασία ρόλου έργου", @@ -1142,7 +1142,7 @@ "label.netscaler.vpx": "Εξισορρόπηση φόρτου VPX του NetScaler", "label.network": "Δίκτυο", "label.network.acl": "ACL δικτύου", -"label.network.acl.lists": "Λίστες Λίστα Πρόσβασης δικτύου", +"label.network.acls": "Λίστες Λίστα Πρόσβασης δικτύου", "label.network.addvm": "Προσθήκη δικτύου για την εικονική μηχανή", "label.network.addvm": "Προσθήκη δικτύου σε εικονική μηχανή", "label.network.desc": "Δίκτυο Desc", @@ -1436,7 +1436,6 @@ "label.remove.vpc.offering": "Κατάργηση προσφοράς υπηρεσίας Εικον. Ιδιωτ. Νέφους", "label.removing": "Αφαίρεση", "label.replace.acl": "Αντικατάσταση λίστας Πρόσβασης", -"label.replace.acl.list": "Αντικατάσταση λίστας Πρόσβασης", "label.report.bug": "Θέμα αναφοράς", "label.required": "Απαιτείται", "label.requireshvm": "HVM", @@ -1455,7 +1454,7 @@ "label.reset.config.value": "Επαναφορά στις τιμές προεπιλογής", "label.reset.ssh.key.pair": "Επαναφορά ζεύγους κλειδιών SSH", "label.reset.to.default": "Επαναφορά στην τιμή προεπιλογής", -"label.reset.userdata.on.vm": "Επαναφορά δεδομένων χρήστη της εικονικής μηχανής", +"label.reset.user.data.on.vm": "Επαναφορά δεδομένων χρήστη της εικονικής μηχανής", "label.reset.vpn.connection": "Επαναφορά σύνδεσης Εικον. Ιδιωτ. Δικτύου", "label.resource": "Πόρος", "label.resource.limit.exceeded": "Υπέρβαση ορίου πόρων", @@ -1811,19 +1810,17 @@ "label.usenewdiskoffering": "Αντικατάσταση προσφοράς υπηρεσίας δίσκου;", "label.user": "Χρήστη", "label.user.conflict": "Σύγκρουση", -"label.user.data": "Δεδομένα χρηστών", -"label.userdata": "Δεδομένα χρήστη", -"label.userdata.do.append": "Πρόσθεση δεδομένων χρήστη", -"label.userdata.do.override": "Παράκαμψη δεδομένων χρήστη", -"label.userdata.registered": "Εγγεγραμένα δεδομένα χρήστη", -"label.userdata.text": "Κείμενο δεδομένων χρήστη", -"label.userdatadetails": "Λεπτομέρειες δεδομένων χρήστη", -"label.userdataid": "Αναγνωριστικό δεδομένων χρήστη", -"label.userdatal2": "Δεδομένα χρήστη", -"label.userdataname": "Όνομα δεδομένων χρήστη", -"label.userdataparams": "Παράμετροι δεδομένων χρήστη", -"label.userdatapolicy": "Συνδεμένες πολιτικές δεδομένων χρήστη", -"label.userdatapolicy.tooltip": "Τα δεδομένα χρήστη που έχουν συνδεθεί στο πρότυπο μπορούν να παρακαμφθούν απο τα δεδομένα χρηστών που ορίστηκαν κατα την δημιουργία της vm. Διαλέξτε την πολιτική παράκαμψης ανάλογα με τις απαιτήσεις.", +"label.user.data": "Δεδομένα χρήστη", +"label.user.data.do.append": "Πρόσθεση δεδομένων χρήστη", +"label.user.data.do.override": "Παράκαμψη δεδομένων χρήστη", +"label.user.data.registered": "Εγγεγραμένα δεδομένα χρήστη", +"label.user.data.text": "Κείμενο δεδομένων χρήστη", +"label.user.data.details": "Λεπτομέρειες δεδομένων χρήστη", +"label.user.data.id": "Αναγνωριστικό δεδομένων χρήστη", +"label.user.data.name": "Όνομα δεδομένων χρήστη", +"label.user.data.params": "Παράμετροι δεδομένων χρήστη", +"label.user.data.policy": "Συνδεμένες πολιτικές δεδομένων χρήστη", +"label.user.data.policy.tooltip": "Τα δεδομένα χρήστη που έχουν συνδεθεί στο πρότυπο μπορούν να παρακαμφθούν απο τα δεδομένα χρηστών που ορίστηκαν κατα την δημιουργία της vm. Διαλέξτε την πολιτική παράκαμψης ανάλογα με τις απαιτήσεις.", "label.username": "Όνομα χρήστη", "label.users": "Χρήστες", "label.usersource": "Τύπος χρήστη", @@ -2116,7 +2113,7 @@ "message.confirm.attach.disk": "Είστε βέβαιοι ότι θέλετε να επισυνάψετε το δίσκο;", "message.confirm.change.offering.for.volume": "Παρακαλώ επιβεβαιώστε ότι επιθυμείτε την αλλαγή της προσφοράς δίσκου για αυτόν τον τόμο", "message.confirm.configure.ovs": "Είστε βέβαιοι ότι θέλετε να ρυθμίσετε τις παραμέτρους του Ovs;", -"message.confirm.delete.acl.list": "Είστε βέβαιοι ότι θέλετε να διαγράψετε αυτήν τη λίστα Λίστα Πρόσβασης;", +"message.confirm.delete.acl": "Είστε βέβαιοι ότι θέλετε να διαγράψετε αυτήν τη λίστα Λίστα Πρόσβασης;", "message.confirm.delete.bigswitchbcf": "Επιβεβαιώστε ότι θέλετε να διαγράψετε αυτόν τον ελεγκτή BigSwitch BCF", "message.confirm.delete.brocadevcs": "Επιβεβαιώστε ότι θέλετε να διαγράψετε το εναλλάκτη Brocade Vcs", "message.confirm.delete.ciscoasa1000v": "Παρακαλώ επιβεβαιώστε ότι θέλετε να διαγράψετε CiscoASA1000v", @@ -2350,7 +2347,7 @@ "message.error.upload.template": "Η αποστολή του προτύπου απέτυχε", "message.error.upload.template.description": "Μόνο ένα πρότυπο μπορεί να αποσταλεί κάθε φορά", "message.error.url": "Πληκτρολογήστε διεύθυνση URL", -"message.error.userdata": "Εισάγωγή δεδομένων χρηστών", +"message.error.user.data": "Εισάγωγή δεδομένων χρηστών", "message.error.username": "Εισάγετε το όνομα χρήστη σας", "message.error.valid.iops.range": "Εισαγεται ένα σωστό εύρος εργασίων εισαγ./εξαγ ανα δευτ.", "message.error.vcenter.datacenter": "Πληκτρολογήστε vCenter Datacenter", diff --git a/ui/public/locales/en.json b/ui/public/locales/en.json index b649f6914bb..d3a44789489 100644 --- a/ui/public/locales/en.json +++ b/ui/public/locales/en.json @@ -1,23 +1,23 @@ { "message.delete.account.not.disabled": "Please disable the account before attempting to delete it.", -"alert.service.domainrouter": "Domain router", +"alert.service.domainrouter": "Domain Router", "error.dedicate.bgp.peer.failed":"Failed to dedicate BGP peer", -"error.dedicate.cluster.failed": "Failed to dedicate cluster.", -"error.dedicate.host.failed": "Failed to dedicate host.", +"error.dedicate.cluster.failed": "Failed to dedicate Cluster.", +"error.dedicate.host.failed": "Failed to dedicate Host.", "error.dedicate.ipv4.subnet.failed": "Failed to dedicate IPv4 subnet.", -"error.dedicate.pod.failed": "Failed to dedicate pod.", -"error.dedicate.zone.failed": "Failed to dedicate zone.", +"error.dedicate.pod.failed": "Failed to dedicate Pod.", +"error.dedicate.zone.failed": "Failed to dedicate Zone.", "error.empty.counter.operator.threshold": "Either Counter, Operator or Threshold is empty", "error.execute.api.failed": "Failed to execute API.", "error.fetching.async.job.result": "Error encountered while fetching async job result.", "error.form.message": "There are errors in the form. Please fix them.", "error.password.not.match": "The password fields do not match", "error.release.dedicate.bgp.peer": "Failed to release dedicated BGP peer.", -"error.release.dedicate.cluster": "Failed to release dedicated cluster.", +"error.release.dedicate.cluster": "Failed to release dedicated Cluster.", "error.release.dedicate.host": "Failed to release dedicated host.", "error.release.dedicate.ipv4.subnet": "Failed to release dedicated IPv4 subnet.", -"error.release.dedicate.pod": "Failed to release dedicated pod.", -"error.release.dedicate.zone": "Failed to release dedicated zone.", +"error.release.dedicate.pod": "Failed to release dedicated Pod.", +"error.release.dedicate.zone": "Failed to release dedicated Zone.", "error.unable.to.add.setting.extraconfig": "It is not allowed to add setting for extraconfig. Please update VirtualMachine with extraconfig parameter.", "error.unable.to.proceed": "Unable to proceed. Please contact your administrator.", "firewall.close": "Firewall", @@ -30,9 +30,9 @@ "label.accept.project.invitation": "Accept project invitation", "label.access": "Access", "label.access.kubernetes.nodes": "Access Kubernetes nodes", -"label.accesskey": "Access key", -"label.access.key": "Access key", -"label.secret.key": "Secret key", +"label.accesskey": "Access Key", +"label.access.key": "Access Key", +"label.secret.key": "Secret Key", "label.apikeyaccess": "Api Key Access", "label.account": "Account", "label.account.and.security.group": "Account - security group", @@ -42,12 +42,12 @@ "label.accounts": "Accounts", "label.accountstate": "Account state", "label.accounttype": "Account type", -"label.acl.export": "Export ACLs", +"label.acl.export": "Export ACL rules", "label.acl.id": "ACL ID", -"label.acl.list.rules": "ACL list rules", +"label.acl.rules": "ACL rules", "label.acl.reason.description": "Enter the reason behind an ACL rule.", "label.aclid": "ACL", -"label.aclname": "ACL name", +"label.acl.rule.name": "ACL rule name", "label.acquire.new.ip": "Acquire new IP", "label.acquire.new.secondary.ip": "Acquire new secondary IP", "label.acquiring.ip": "Acquiring IP", @@ -55,7 +55,7 @@ "label.action": "Action", "label.action.add.nodes.to.kubernetes.cluster": "Add nodes to Kubernetes cluster", "label.action.remove.nodes.from.kubernetes.cluster": "Remove nodes from Kubernetes cluster", -"label.action.attach.disk": "Attach disk", +"label.action.attach.disk": "Attach Disk", "label.action.attach.iso": "Attach ISO", "label.action.attach.to.instance": "Attach to Instance", "label.action.bulk.delete.egress.firewall.rules": "Bulk delete egress firewall rules", @@ -65,14 +65,14 @@ "label.action.bulk.delete.load.balancer.rules": "Bulk delete load balancer rules", "label.action.bulk.delete.portforward.rules": "Bulk delete port forward rules", "label.action.bulk.delete.routing.firewall.rules": "Bulk remove IPv4 Routing firewall rules", -"label.action.bulk.delete.snapshots": "Bulk delete snapshots", +"label.action.bulk.delete.snapshots": "Bulk delete Snapshots", "label.action.bulk.delete.templates": "Bulk delete Templates", "label.action.bulk.release.public.ip.address": "Bulk release public IP addresses", "label.action.cancel.maintenance.mode": "Cancel maintenance mode", "label.action.change.password": "Change password", "label.action.clear.webhook.deliveries": "Clear deliveries", "label.action.delete.webhook.deliveries": "Delete deliveries", -"label.action.change.primary.storage.scope": "Change primary storage scope", +"label.action.change.primary.storage.scope": "Change Primary Storage scope", "label.action.configure.stickiness": "Stickiness", "label.action.configure.storage.access.group": "Update storage access group", "label.action.copy.iso": "Copy ISO", @@ -80,79 +80,81 @@ "label.action.copy.template": "Copy Template", "label.action.create.snapshot.from.vmsnapshot": "Create Snapshot from Instance Snapshot", "label.action.create.template.from.volume": "Create Template from volume", -"label.action.create.volume": "Create volume", +"label.action.create.volume": "Create Volume", "label.action.create.volume.add": "Create and Add Volume", "label.action.delete.account": "Delete Account", "label.action.delete.backup.offering": "Delete backup offering", -"label.action.delete.cluster": "Delete cluster", -"label.action.delete.domain": "Delete domain", -"label.action.delete.egress.firewall": "Delete egress firewall rule", -"label.action.delete.firewall": "Delete firewall rule", +"label.action.delete.cluster": "Delete Cluster", +"label.action.delete.domain": "Delete Domain", +"label.action.delete.egress.firewall": "Delete Egress Firewall Rule", +"label.action.delete.firewall": "Delete Firewall Rule", "label.action.delete.interface.static.route": "Remove Tungsten Fabric interface static route", +"label.action.delete.gpu.card": "Delete GPU card", "label.action.delete.guest.os": "Delete guest OS", "label.action.delete.guest.os.category": "Delete guest OS category", "label.action.delete.guest.os.hypervisor.mapping": "Delete guest OS hypervisor mapping", "label.action.delete.ip.range": "Delete IP range", "label.action.delete.iso": "Delete ISO", -"label.action.delete.load.balancer": "Delete load balancer rule", +"label.action.delete.load.balancer": "Delete Load Balancer Rule", "label.action.delete.network": "Delete Network", "label.action.delete.network.static.route": "Remove Tungsten Fabric Network static route", "label.action.delete.network.permission": "Delete Network permission", -"label.action.delete.node": "Delete node", +"label.action.delete.node": "Delete Node", "label.action.delete.oauth.provider": "Delete OAuth provider", "label.action.delete.physical.network": "Delete physical Network", "label.action.delete.pod": "Delete Pod", -"label.action.delete.primary.storage": "Delete primary storage", +"label.action.delete.primary.storage": "Delete Primary Storage", "label.action.delete.routing.firewall.rule": "Delete IPv4 Routing firewall rule", -"label.action.delete.secondary.storage": "Delete secondary storage", -"label.action.delete.security.group": "Delete security group", +"label.action.delete.secondary.storage": "Delete Secondary Storage", +"label.action.delete.security.group": "Delete Security Group", "label.action.delete.snapshot": "Delete Snapshot", "label.action.delete.template": "Delete Template", "label.action.delete.tungsten.router.table": "Remove Tungsten Fabric route table from Network", "label.action.delete.user": "Delete User", -"label.action.delete.volume": "Delete volume", -"label.action.delete.zone": "Delete zone", +"label.action.delete.vgpu.profile": "Delete vGPU profile", +"label.action.delete.volume": "Delete Volume", +"label.action.delete.zone": "Delete Zone", "label.action.destroy.instance": "Destroy Instance", -"label.action.destroy.systemvm": "Destroy system VM", -"label.action.destroy.volume": "Destroy volume", -"label.action.detach.disk": "Detach disk", +"label.action.destroy.systemvm": "Destroy System VM", +"label.action.destroy.volume": "Destroy Volume", +"label.action.detach.disk": "Detach Disk", "label.action.detach.iso": "Detach ISO", "label.action.disable.account": "Disable Account", -"label.action.disable.cluster": "Disable cluster", -"label.action.disable.disk.offering": "Disable disk offering", +"label.action.disable.cluster": "Disable Cluster", +"label.action.disable.disk.offering": "Disable Disk Offering", "label.action.disable.physical.network": "Disable physical Network", -"label.action.disable.pod": "Disable pod", +"label.action.disable.pod": "Disable Pod", "label.action.disable.role": "Disable Role", "label.action.disable.static.nat": "Disable static NAT", -"label.action.disable.service.offering": "Disable service offering", -"label.action.disable.system.service.offering": "Disable system service offering", +"label.action.disable.service.offering": "Disable Service Offering", +"label.action.disable.system.service.offering": "Disable System Service Offering", "label.action.disable.user": "Disable User", -"label.action.disable.zone": "Disable zone", +"label.action.disable.zone": "Disable Zone", "label.action.download.iso": "Download ISO", "label.action.download.snapshot": "Download Snapshot", "label.action.download.template": "Download Template", -"label.action.download.volume": "Download volume", +"label.action.download.volume": "Download Volume", "label.action.edit.account": "Edit Account", -"label.action.edit.domain": "Edit domain", +"label.action.edit.domain": "Edit Domain", "label.action.edit.instance": "Edit Instance", "label.action.edit.iso": "Edit ISO", "label.action.edit.nfs.options": "Edit NFS mount options", "label.action.edit.template": "Edit Template", -"label.action.edit.zone": "Edit zone", +"label.action.edit.zone": "Edit Zone", "label.action.enable.two.factor.authentication": "Enabled Two factor authentication", "label.action.verify.two.factor.authentication": "Verified Two factor authentication", "label.action.enable.account": "Enable Account", -"label.action.enable.cluster": "Enable cluster", +"label.action.enable.cluster": "Enable Cluster", "label.action.enable.disk.offering": "Enable disk offering", "label.action.enable.maintenance.mode": "Enable maintenance mode", "label.action.enable.physical.network": "Enable physical Network", -"label.action.enable.pod": "Enable pod", +"label.action.enable.pod": "Enable Pod", "label.action.enable.role": "Enable Role", -"label.action.enable.service.offering": "Enable service offering", -"label.action.enable.system.service.offering": "Enable system service offering", +"label.action.enable.service.offering": "Enable Service Offering", +"label.action.enable.system.service.offering": "Enable System Service Offering", "label.action.enable.static.nat": "Enable static NAT", "label.action.enable.user": "Enable User", -"label.action.enable.zone": "Enable zone", +"label.action.enable.zone": "Enable Zone", "label.action.expunge.instance": "Expunge Instance", "label.action.force.reconnect": "Force reconnect", "label.action.generate.keys": "Generate keys", @@ -168,22 +170,22 @@ "label.action.iso.share": "Update ISO sharing", "label.action.lock.account": "Lock Account", "label.action.lock.user": "Lock User", -"label.action.manage.cluster": "Manage cluster", -"label.action.migrate.router": "Migrate router", +"label.action.manage.cluster": "Manage Cluster", +"label.action.migrate.router": "Migrate Router", "label.action.migrate.systemvm": "Migrate System VM", -"label.action.migrate.systemvm.to.ps": "Migrate System VM to another primary storage", -"label.action.patch.systemvm": "Patch system VM", +"label.action.migrate.systemvm.to.ps": "Migrate System VM to another Primary Storage", +"label.action.patch.systemvm": "Patch System VM", "label.action.patch.systemvm.vpc": "Patch System VM - VPC Router", "label.action.patch.systemvm.processing": "Patching System VM....", -"label.action.project.add.account": "Add Account to project", -"label.action.project.add.user": "Add User to project", +"label.action.project.add.account": "Add Account to Project", +"label.action.project.add.user": "Add User to Project", "label.action.quota.tariff.create": "Create Quota Tariff", "label.action.quota.tariff.edit": "Edit Quota Tariff", "label.action.quota.tariff.remove": "Remove Quota Tariff", "label.action.reboot.instance": "Reboot Instance", -"label.action.reboot.router": "Reboot router", +"label.action.reboot.router": "Reboot Router", "label.action.reboot.systemvm": "Reboot System VM", -"label.action.recover.volume": "Recover volume", +"label.action.recover.volume": "Recover Volume", "label.action.resize.sharedfs": "Resize Shared FileSystem", "label.action.restart.sharedfs": "Restart Shared FileSystem", "label.action.recurring.snapshot": "Recurring Snapshots", @@ -193,7 +195,7 @@ "label.action.release.asnumber": "Release AS Number", "label.action.release.ip": "Release IP", "label.action.release.reserved.ip": "Release reserved IP", -"label.action.remove.host": "Remove host", +"label.action.remove.host": "Remove Host", "label.action.remove.logical.router": "Remove Logical Router", "label.action.remove.network.policy": "Remove Network Policy", "label.action.remove.router.table.from.interface": "Remove Tungsten Fabric route table from interface", @@ -202,25 +204,25 @@ "label.action.reserve.ip": "Reserve Public IP", "label.action.reset.network.permissions": "Reset Network permissions", "label.action.reset.password": "Reset password", -"label.action.resize.volume": "Resize volume", +"label.action.resize.volume": "Resize Volume", "label.action.revert.snapshot": "Revert to Snapshot", "label.action.router.health.checks": "Get health checks result", -"label.action.run.diagnostics": "Run diagnostics", +"label.action.run.diagnostics": "Run Diagnostics", "label.action.secure.host": "Provision host security keys", "label.action.set.as.source.nat.ip": "make source NAT", "label.action.setup.2FA.user.auth": "Setup User Two Factor Authentication", "label.action.start.sharedfs": "Start Shared FileSystem", "label.action.start.instance": "Start Instance", -"label.action.start.router": "Start router", -"label.action.start.systemvm": "Start system VM", +"label.action.start.router": "Start Router", +"label.action.start.systemvm": "Start System VM", "label.action.stop.sharedfs": "Stop Shared FileSystem", "label.action.stop.instance": "Stop Instance", -"label.action.stop.router": "Stop router", -"label.action.stop.systemvm": "Stop system VM", +"label.action.stop.router": "Stop Router", +"label.action.stop.systemvm": "Stop System VM", "label.action.take.snapshot": "Take Snapshot", "label.action.template.permission": "Update Template permissions", "label.action.template.share": "Update Template sharing", -"label.action.unmanage.cluster": "Unmanage cluster", +"label.action.unmanage.cluster": "Unmanage Cluster", "label.action.unmanage.instance": "Unmanage Instance", "label.action.unmanage.instances": "Unmanage Instances", "label.action.unmanage.virtualmachine": "Unmanage Instance", @@ -230,12 +232,13 @@ "label.action.update.storage.pool": "Update storage pool", "label.action.unmanage.volume": "Unmanage Volume", "label.action.unmanage.volumes": "Unmanage Volumes", -"label.action.update.host": "Update host", +"label.action.unregister.extension.resource": "Unregister extension resource", +"label.action.update.host": "Update Host", "label.action.update.security.groups": "Update security groups", "label.action.update.offering.access": "Update offering access", "label.action.update.resource.count": "Update resource count", +"label.action.user.data.reset": "Reset User Data", "label.action.value": "Action/Value", -"label.action.userdata.reset": "Reset Userdata", "label.action.vmsnapshot.create": "Take Instance Snapshot", "label.action.vmsnapshot.delete": "Delete Instance Snapshot", "label.action.vmsnapshot.revert": "Revert to Instance Snapshot", @@ -246,75 +249,78 @@ "label.activeviewersessions": "Active sessions", "label.add": "Add", "label.add.account": "Add Account", +"label.add.acl.rule": "Add ACL rule", "label.add.acl": "Add ACL", -"label.add.acl.list": "Add ACL list", -"label.add.affinity.group": "Add new affinity group", +"label.add.affinity.group": "Add new Affinity Group", "label.add.baremetal.dhcp.device": "Add bare metal DHCP device", -"label.add.bgp.peer": "Add BGP peer", -"label.add.bigswitchbcf.device": "Add BigSwitch BCF controller", +"label.add.bgp.peer": "Add BGP Peer", +"label.add.bigswitchbcf.device": "Add BigSwitch BCF Controller", "label.add.brocadevcs.device": "Add Brocade Vcs Switch", "label.add.by": "Add by", "label.add.certificate": "Add certificate", "label.add.ciscoasa1000v": "Add CiscoASA1000v resource", -"label.add.cluster": "Add cluster", -"label.add.compute.offering": "Add compute offering", +"label.add.cluster": "Add Cluster", +"label.add.compute.offering": "Add Compute Offering", "label.add.condition": "Add condition", -"label.add.disk.offering": "Add disk offering", -"label.add.domain": "Add domain", -"label.add.egress.rule": "Add egress rule", +"label.add.custom.action": "Add Custom Action", +"label.add.disk.offering": "Add Disk Offering", +"label.add.domain": "Add Domain", +"label.add.egress.rule": "Add Egress Rule", +"label.add.external.details": "Add external details", "label.add.f5.device": "Add F5 device", -"label.add.firewall": "Add firewall rule", +"label.add.firewall": "Add Firewall Rule", "label.add.firewallrule": "Add Firewall Rule", -"label.add.guest.network": "Add guest Network", -"label.add.guest.os": "Add guest OS", +"label.add.gpu.card": "Add GPU card", +"label.add.gpu.device": "Add GPU device", "label.add.guest.os.category": "Add guest OS category", -"label.add.guest.os.hypervisor.mapping": "Add guest OS hypervisor mapping", -"label.add.host": "Add host", -"label.add.ingress.rule": "Add ingress rule", +"label.add.guest.network": "Add Guest Network", +"label.add.guest.os": "Add Guest OS", +"label.add.guest.os.hypervisor.mapping": "Add guest os hypervisor mapping", +"label.add.host": "Add Host", +"label.add.ingress.rule": "Add Ingress Rule", "label.add.intermediate.certificate": "Add intermediate certificate", "label.add.internal.lb": "Add internal LB", -"label.add.ip.range": "Add IP range", -"label.add.ipv4.subnet": "Add IPv4 subnet for Routed networks", +"label.add.ip.range": "Add IP Range", +"label.add.ipv4.subnet": "Add IPv4 Subnet for Routed Networks", "label.add.ip.v6.prefix": "Add IPv6 prefix", -"label.add.isolated.network": "Add isolated Network", -"label.add.kubernetes.cluster": "Add Kubernetes cluster", +"label.add.isolated.network": "Add Isolated Network", +"label.add.kubernetes.cluster": "Add Kubernetes Cluster", +"label.add.acl.name": "ACL name", "label.add.ldap.account": "Add LDAP Account", -"label.add.list.name": "ACL List name", "label.add.logical.router": "Add Logical Router to this Network", "label.add.more": "Add more", "label.add.nodes": "Add Nodes to Kubernetes Cluster", -"label.add.netscaler.device": "Add Netscaler device", +"label.add.netscaler.device": "Add Netscaler Device", "label.add.network": "Add Network", "label.add.network.acl": "Add Network ACL", -"label.add.network.acl.list": "Add Network ACL list", -"label.add.network.offering": "Add Network offering", +"label.add.network.offering": "Add Network Offering", "label.add.network.permission": "Add Network permission", -"label.add.new.gateway": "Add new gateway", +"label.add.new.gateway": "Add new Gateway", "label.add.new.tier": "Add new Network Tier", "label.add.niciranvp.device": "Add Nvp controller", "label.add.note": "Add comment", "label.add.opendaylight.device": "Add OpenDaylight controller", "label.add.pa.device": "Add Palo Alto device", "label.add.param": "Add param", -"label.add.physical.network": "Add physical Network", -"label.add.pod": "Add pod", +"label.add.physical.network": "Add Physical Network", +"label.add.pod": "Add Pod", "label.add.prefix": "Add prefix", "label.add.policy": "Add policy", -"label.add.primary.storage": "Add primary storage", -"label.add.private.gateway": "Add private gateway", -"label.add.resources": "Add resources", -"label.add.role": "Add role", -"label.add.route": "Add route", -"label.add.router.table.to.instance": "Add router table to this Instance", -"label.add.routing.policy": "Add routing policy", -"label.add.rule": "Add rule", -"label.add.secondary.ip": "Add secondary IP", -"label.add.secondary.storage": "Add secondary storage", -"label.add.security.group": "Add security group", +"label.add.primary.storage": "Add Primary Storage", +"label.add.private.gateway": "Add Private Gateway", +"label.add.resources": "Add Resources", +"label.add.role": "Add Role", +"label.add.route": "Add Route", +"label.add.router.table.to.instance": "Add Router Table to this Instance", +"label.add.routing.policy": "Add Routing Policy", +"label.add.rule": "Add Rule", +"label.add.secondary.ip": "Add Secondary IP", +"label.add.secondary.storage": "Add Secondary Storage", +"label.add.security.group": "Add Security Group", "label.add.setting": "Add setting", -"label.add.srx.device": "Add SRX device", -"label.add.static.route": "Add static route", -"label.add.system.service.offering": "Add system service offering", +"label.add.srx.device": "Add SRX Device", +"label.add.static.route": "Add Static Route", +"label.add.system.service.offering": "Add System Service Offering", "label.add.term.then": "Add term", "label.add.traffic": "Add traffic", "label.add.traffic.type": "Add traffic type", @@ -336,26 +342,27 @@ "label.add.user": "Add User", "label.add.upstream.ipv4.routes": "Add upstream IPv4 routes", "label.add.upstream.ipv6.routes": "Add upstream IPv6 routes", +"label.add.vgpu.profile": "Add profile", "label.add.vm": "Add Instance", "label.add.vms": "Add Instances", -"label.add.vmware.datacenter": "Add VMware datacenter", -"label.add.vnmc.device": "Add VNMC device", +"label.add.vmware.datacenter": "Add VMware Datacenter", +"label.add.vnmc.device": "Add VNMC Device", "label.add.vpc": "Add VPC", -"label.add.vpc.offering": "Add VPC offering", -"label.add.vpn.customer.gateway": "Add VPN customer gateway", -"label.add.vpn.gateway": "Add VPN gateway", +"label.add.vpc.offering": "Add VPC Offering", +"label.add.vpn.customer.gateway": "Add VPN Customer Gateway", +"label.add.vpn.gateway": "Add VPN Gateway", "label.add.vpn.user": "Add VPN User", -"label.add.zone": "Add zone", +"label.add.zone": "Add Zone", "label.adding": "Adding", "label.adding.user": "Adding User...", "label.address": "Address", "label.address.group": "Address group", -"label.admin": "Domain admin", +"label.admin": "Domain Admin", "label.advanced": "Advanced", "label.advanced.mode": "Advanced mode", "label.affinity": "Affinity", -"label.affinity.groups": "Affinity groups", -"label.affinitygroup": "Affinity group", +"label.affinity.groups": "Affinity Groups", +"label.affinitygroup": "Affinity Group", "label.agentcount": "Number Of connected agents", "label.agent.password": "Agent password", "label.agent.username": "Agent username", @@ -370,11 +377,12 @@ "label.all": "All", "label.all.available.data": "All available data", "label.all.ipv6": "All IPv6", -"label.all.zone": "All zones", +"label.all.zone": "All Zones", "label.allocated": "Allocated", "label.allocatedonly": "Allocated", "label.allocationstate": "Allocation state", "label.allow": "Allow", +"label.allowedroletypes": "Allowed Role Types", "label.allow.duplicate.macaddresses": "Allow duplicate MAC addresses", "label.allowuserdrivenbackups": "Allow User driven backups", "label.annotation": "Comment", @@ -439,16 +447,16 @@ "label.backup.configure.schedule": "Configure Backup Schedule", "label.backup.offering.assign": "Assign Instance to backup offering", "label.backup.offering.remove": "Remove Instance from backup offering", -"label.backup.offerings": "Backup offerings", +"label.backup.offerings": "Backup Offerings", "label.backup.repository": "Backup Repository", "label.backup.restore": "Restore Instance backup", -"label.backupofferingid": "Backup offering", -"label.backupofferingname": "Backup offering", -"label.backup.repository.add": "Add backup repository", -"label.backup.repository.remove": "Remove backup repository", "label.backuplimit": "Backup Limits", "label.backup.storage": "Backup Storage", "label.backupstoragelimit": "Backup Storage Limits (GiB)", +"label.backupofferingid": "Backup Offering ID", +"label.backupofferingname": "Backup Offering Name", +"label.backup.repository.add": "Add Backup Repository", +"label.backup.repository.remove": "Remove Backup Repository", "label.balance": "Balance", "label.bandwidth": "Bandwidth", "label.baremetal.dhcp.devices": "Bare metal DHCP devices", @@ -479,12 +487,12 @@ "label.bucket": "Bucket", "label.bucketlimit": "Bucket Limits", "label.by.account": "By Account", -"label.by.domain": "By domain", +"label.by.domain": "By Domain", "label.by.level": "By level", -"label.by.pod": "By pod", +"label.by.pod": "By Pod", "label.by.state": "By state", "label.by.type": "By type", -"label.by.zone": "By zone", +"label.by.zone": "By Zone", "label.bypassvlanoverlapcheck": "Bypass VLAN id/range overlap", "label.cachemode": "Write-cache type", "label.cancel": "Cancel", @@ -534,6 +542,7 @@ "label.cks.cluster.worker.nodes.templateid": "Template for Worker Nodes", "label.cleanup": "Clean up", "label.clear": "Clear", +"label.clear.all": "Clear all", "label.clear.list": "Clear list", "label.clear.notification": "Clear notification", "label.clientid": "Provider Client ID", @@ -564,12 +573,13 @@ "label.community": "Community", "label.complete": "Complete", "label.compute": "Compute", -"label.compute.offerings": "Compute offerings", -"label.compute.offering.for.sharedfs.instance": "Compute offering for Instance", +"label.compute.offerings": "Compute Offerings", +"label.compute.offering.for.sharedfs.instance": "Compute Offering for Instance", "label.computeonly.offering": "Compute only disk offering", "label.computeonly.offering.tooltip": "Option to specify root disk related information in the compute offering or to directly link a disk offering to the compute offering", "label.conditions": "Conditions", "label.configuration": "Configuration", +"label.configuration.details": "Configuration Details", "label.configure": "Configure", "label.configure.health.monitor": "Configure Health Monitor", "label.configure.app": "Configure the App", @@ -583,7 +593,7 @@ "label.confirm.delete.loadbalancer.rules": "Please confirm you wish to delete the selected load balancing rules.", "label.confirm.delete.portforward.rules": "Please confirm you wish to delete the selected port-forward rules.", "label.confirm.delete.routing.firewall.rules": "Please confirm you wish to delete the selected IPv4 Routing firewall rules", -"label.confirm.delete.snapshot.zones": "Please confirm you wish to delete the Snapshot in the selected zones.", +"label.confirm.delete.snapshot.zones": "Please confirm you wish to delete the Snapshot in the selected Zones.", "label.confirm.delete.templates": "Please confirm you wish to delete the selected Templates.", "label.confirm.delete.tungsten.address.group": "Please confirm that you would like to delete this Address Group", "label.confirm.delete.tungsten.firewall.policy": "Please confirm that you would like to delete this Firewall Policy", @@ -619,11 +629,12 @@ "label.copyid": "Copy ID", "label.copy.password": "Copy password", "label.core": "Core", -"label.core.zone.type": "Core zone type", +"label.core.zone.type": "Core Zone type", +"label.count": "Count", "label.counter": "Counter", "label.counter.name": "Name of the counter for which the policy will be evaluated", "label.cpu": "CPU", -"label.cpu.sockets": "CPU sockets", +"label.cpu.sockets": "CPU Sockets", "label.cpu.usage.info": "CPU usage information", "label.cpuallocated": "CPU allocated for Instances", "label.cpuallocatedghz": "CPU allocated", @@ -638,19 +649,20 @@ "label.cpuused": "CPU utilized", "label.cpuusedghz": "CPU used", "label.create": "Create", -"label.create.instance": "Create cloud server", +"label.create.instance": "Create Cloud Server", "label.create.account": "Create Account", "label.create.asnrange": "Create AS Range", -"label.create.backup": "Start backup", +"label.create.backup": "Start Backup", +"label.create.extension": "Create Extension", "label.create.sharedfs": "Create Shared FileSystem", "label.create.network": "Create new Network", "label.create.nfs.secondary.staging.storage": "Create NFS secondary staging storage", -"label.create.project": "Create project", -"label.create.project.role": "Create project role", +"label.create.project": "Create Project", +"label.create.project.role": "Create Project Role", "label.create.routing.policy": "Create Routing Policy", "label.create.site.vpn.connection": "Create site-to-site VPN connection", "label.create.site.vpn.gateway": "Create site-to-site VPN gateway", -"label.create.snapshot.for.volume": "Created Snapshot for volume", +"label.create.snapshot.for.volume": "Created Snapshot for Volume", "label.create.ssh.key.pair": "Create a SSH Key Pair", "label.create.template": "Create Template", "label.create.tier.aclid.description": "The ACL associated with the Network Tier.", @@ -671,22 +683,25 @@ "label.credit": "Credit", "label.cron": "Cron expression", "label.cron.mode": "Cron mode", -"label.crosszones": "Cross zones", +"label.crosszones": "Cross Zones", "label.currency": "Currency", "label.current": "Current", "label.current.storage": "Current storage", "label.currentpassword": "Current password", "label.custom": "Custom", +"label.customactionid": "Custom Action", +"label.customactions": "Custom Actions", +"label.custom.actions": "Custom Actions", "label.customconstrained": "Custom constrained", "label.customdisksize": "Custom disk size", "label.customunconstrained": "Custom unconstrained", "label.daily": "Daily", "label.dark.mode": "Dark mode", "label.dashboard": "Dashboard", -"label.data.disk": "Data disk", +"label.data.disk": "Data Disk", +"label.data.disk.offering": "Data Disk Offering", "label.data.pool": "Data pool", "label.data.pool.description": "Data pool is required when using a Ceph pool with erasure code", -"label.data.disk.offering": "Data disk offering", "label.date": "Date", "label.datetime.filter.period": "From {startDate} to {endDate}", "label.datetime.filter.starting": "Starting {startDate}.", @@ -695,19 +710,19 @@ "label.days": "Days", "label.day.of.month": "Day of month", "label.day.of.week": "Day of week", -"label.db.usage.metrics": "DB/Usage server", +"label.db.usage.metrics": "DB/Usage Server", "label.dbislocal": "The db runs locally", "label.dc.name": "DC name", "label.declare.host.as.degraded": "Declare host as degraded", "label.decline.invitation": "Decline invitation", "label.dedicate": "Dedicate", "label.dedicate.bgp.peer": "Dedicate BGP peer", -"label.dedicate.cluster": "Dedicate cluster", -"label.dedicate.host": "Dedicate host", +"label.dedicate.cluster": "Dedicate Cluster", +"label.dedicate.host": "Dedicate Host", "label.dedicate.ipv4.subnet": "Dedicate IPv4 subnet", -"label.dedicate.pod": "Dedicate pod", +"label.dedicate.pod": "Dedicate Pod", "label.dedicate.vlan.vni.range": "Dedicate VLAN/VNI range", -"label.dedicate.zone": "Dedicate zone", +"label.dedicate.zone": "Dedicate Zone", "label.dedicated": "Dedicated", "label.dedicated.vlan.vni.ranges": "Dedicated VLAN/VNI ranges", "label.dedicatedresources": "Dedicated resources", @@ -718,7 +733,7 @@ "label.default.network.guestcidraddress.isolated.network": "Default guest CIDR for Isolated Networks", "label.defaultnetwork": "Default Network", "label.delete": "Delete", -"label.delete.acl.list": "Delete ACL list", +"label.delete.acl": "Delete ACL", "label.delete.affinity.group": "Delete affinity group", "label.delete.alerts": "Delete alerts", "label.delete.asnrange": "Delete AS Range", @@ -731,11 +746,12 @@ "label.delete.ciscoasa1000v": "Delete CiscoASA1000v", "label.delete.ciscovnmc.resource": "Delete CiscoVNMC resource", "label.delete.condition": "Delete condition", +"label.delete.custom.action": "Delete Custom Action", "label.delete.dedicated.vlan.range": "Deleted dedicated VLAN/VNI range.", "label.delete.domain": "Delete domain", "label.delete.events": "Delete events", +"label.delete.extension": "Delete Extension", "label.delete.f5": "Delete F5", -"label.destroy.sharedfs": "Destroy Shared FileSystem", "label.delete.gateway": "Delete gateway", "label.delete.icon": "Delete icon", "label.delete.instance.group": "Delete Instance group", @@ -749,13 +765,13 @@ "label.delete.portforward.rules": "Delete port forward rules", "label.delete.project": "Delete project", "label.delete.project.role": "Delete project role", -"label.delete.role": "Delete role", -"label.delete.rule": "Delete rule", -"label.delete.setting": "Delete setting", +"label.delete.role": "Delete Role", +"label.delete.rule": "Delete Rule", +"label.delete.setting": "Delete Setting", "label.delete.snapshot.policy": "Delete Snapshot policy", "label.delete.srx": "Delete SRX", "label.delete.sslcertificate": "Delete SSL certificate", -"label.delete.tag": "Remove tag", +"label.delete.tag": "Remove Tag", "label.delete.term": "Delete term", "label.delete.traffic.type": "Delete traffic type", "label.delete.tungsten.address.group": "Delete Address Group", @@ -785,13 +801,13 @@ "label.deployasis": "Read Instance settings from OVA", "label.deploymentplanner": "Deployment planner", "label.desc.db.stats": "Database Statistics", -"label.desc.importexportinstancewizard": "Import and export Instances to/from an existing VMware or KVM cluster.", +"label.desc.importexportinstancewizard": "Import and export Instances to/from an existing VMware or KVM Cluster.", "label.desc.import.ext.kvm.wizard": "Import Instance from remote KVM host", "label.desc.import.local.kvm.wizard": "Import QCOW2 image from Local Storage", "label.desc.import.shared.kvm.wizard": "Import QCOW2 image from Shared Storage", "label.desc.import.unmanage.volume": "Import and unmanage volume on Storage Pools", "label.desc.ingesttinstancewizard": "Ingest instances from an external KVM host", -"label.desc.importmigratefromvmwarewizard": "Import instances from VMware into a KVM cluster", +"label.desc.importmigratefromvmwarewizard": "Import instances from VMware into a KVM Cluster", "label.desc.usage.stats": "Usage Server Statistics", "label.description": "Description", "label.destaddressgroupuuid": "Destination Address Group", @@ -814,10 +830,12 @@ "label.destroying": "Destroying", "label.destroyed": "Destroyed", "label.destroy.router": "Destroy router", +"label.destroy.sharedfs": "Destroy Shared FileSystem", "label.deststartport": "Destination Start Port", "label.desttaguuid": "Destination Tag", "label.details": "Details", "label.deviceid": "Device ID", +"label.devicename": "Device Name", "label.devices": "Devices", "label.dhcp": "DHCP", "label.direct.attached.public.ip": "Direct attached public IP", @@ -825,17 +843,20 @@ "label.directdownload": "Direct download", "label.direction": "Direction", "label.disable.autoscale.vmgroup": "Disable AutoScaling Group", +"label.disable.custom.action": "Disable Custom Action", +"label.disable.extension": "Disable Extension", "label.disable.host": "Disable host", -"label.disable.network.offering": "Disable Network offering", +"label.disable.network.offering": "Disable Network Offering", "label.disable.provider": "Disable provider", "label.disable.storage": "Disable storage pool", -"label.disable.vpc.offering": "Disable VPC offering", +"label.disable.vpc.offering": "Disable VPC Offering", "label.disable.vpn": "Disable remote access VPN", "label.disable.webhook": "Disable Webhook", "label.disabled": "Disabled", "label.disconnected": "Last disconnected", +"label.discover.gpu.devices": "Discover GPU devices", "label.disk": "Disk", -"label.disk.offerings": "Disk offerings", +"label.disk.offerings": "Disk Offerings", "label.disk.path": "Disk Path", "label.disk.tooltip": "Disk Image filename in the selected Storage Pool", "label.disk.selection": "Disk selection", @@ -856,9 +877,9 @@ "label.diskkbswrite": "Disk write (KiB)", "label.diskread": "Disk read", "label.diskwrite": "Disk write", -"label.diskoffering": "Disk offering", -"label.diskofferingdisplaytext": "Disk offering", -"label.diskofferingid": "Disk offering", +"label.diskoffering": "Disk Offering", +"label.diskofferingdisplaytext": "Disk Offering", +"label.diskofferingid": "Disk Offering", "label.disksize": "Disk size (in GB)", "label.disksizeallocated": "Disk allocated", "label.disksizeallocatedgb": "Allocated", @@ -879,7 +900,7 @@ "label.domain": "Domain", "label.domain.id": "Domain ID", "label.domain.name": "Domain name", -"label.domain.router": "Domain router", +"label.domain.router": "Domain Router", "label.domain.suffix": "DNS domain suffix (i.e., xyz.com)", "label.domainid": "Domain", "label.domainname": "Domain", @@ -919,15 +940,15 @@ "label.edge.zone": "Edge Zone", "label.edit": "Edit", "label.edit.account": "Edit Account", -"label.edit.acl.list": "Edit ACL list", +"label.edit.acl": "Edit ACL", "label.edit.acl.rule": "Edit ACL rule", "label.edit.autoscale.vmprofile": "Edit AutoScale Instance Profile", "label.edit.project.details": "Edit project details", "label.edit.project.role": "Edit project role", -"label.edit.role": "Edit role", -"label.edit.rule": "Edit rule", +"label.edit.role": "Edit Role", +"label.edit.rule": "Edit Rule", "label.edit.secondary.ips": "Edit secondary IPs", -"label.edit.tags": "Edit tags", +"label.edit.tags": "Edit Tags", "label.edit.traffic.type": "Edit traffic type", "label.edit.user": "Edit User", "label.egress": "Egress", @@ -937,12 +958,14 @@ "label.elastic": "Elastic", "label.email": "Email", "label.enable.autoscale.vmgroup": "Enable AutoScaling Group", +"label.enable.custom.action": "Enable Custom Action", +"label.enable.extension": "Enable Extension", "label.enable.host": "Enable Host", -"label.enable.network.offering": "Enable Network offering", +"label.enable.network.offering": "Enable Network Offering", "label.enable.oauth": "Enable OAuth Login", "label.enable.provider": "Enable provider", -"label.enable.storage": "Enable storage pool", -"label.enable.vpc.offering": "Enable VPC offering", +"label.enable.storage": "Enable Storage Pool", +"label.enable.vpc.offering": "Enable VPC Offering", "label.enable.vpn": "Enable remote access VPN", "label.enable.webhook": "Enable Webhook", "label.enabled": "Enabled", @@ -968,6 +991,7 @@ "label.entityid": "Entity", "label.entitytype": "Entity Type", "label.error": "Error", +"label.errormessage": "Error message", "label.error.caught": "Error caught", "label.error.code": "Error code", "label.error.file.read": "Cannot read file.", @@ -995,14 +1019,22 @@ "label.existing": "Existing", "label.execute": "Execute", "label.expunge": "Expunge", - "label.expunge.sharedfs": "Expunge Shared FileSystem", +"label.expunge.sharedfs": "Expunge Shared FileSystem", "label.expungevmgraceperiod": "Expunge Instance grace period (in sec)", "label.expunged": "Expunged", "label.expunging": "Expunging", "label.export.rules": "Export Rules", "label.ext.hostname.tooltip": "External Host Name or IP Address", -"label.external.managed": "ExternalManaged", +"label.extension": "Extension", +"label.extensions": "Extensions", +"label.extensionid": "Extension", +"label.extensionname": "Extension", "label.external": "External", +"label.external.managed": "ExternalManaged", +"label.external.details": "External provisioning details", +"label.externaldetails": "External details", +"label.external.details.tooltip": "Details that will be passed to the external provisioner while deploying an instance", +"label.externalprovisioner": "External provisioner", "label.external.link": "External link", "label.externalid": "External Id", "label.externalloadbalanceripaddress": "External load balancer IP address.", @@ -1023,21 +1055,23 @@ "label.filter.annotations.self": "Created by me", "label.filterby": "Filter by", "label.fingerprint": "FingerPrint", +"label.skip": "Skip", "label.finish": "Finish", "label.firewall": "Firewall", "label.firewall.policy": "Firewall Policy", "label.firewallpolicy": "Firewall Policy", -"label.firewallrule": "Firewall rule", +"label.firewallrule": "Firewall Rule", "label.firewallruleuuid": "Firewall Rule", "label.firstname": "First name", "label.firstname.lower": "firstname", "label.fix.errors": "Fix errors", -"label.fixed": "Fixed offering", +"label.fixed": "Fixed Offering", "label.for": "for", "label.forcks": "For CKS", "label.forbidden": "Forbidden", "label.forced": "Force", "label.force.ms.to.import.vm.files": "Enable to force OVF Download via Management Server. Disable to use KVM Host ovftool (if installed)", +"label.force.update.os.type": "Force update OS type", "label.force.stop": "Force stop", "label.force.reboot": "Force reboot", "label.forceencap": "Force UDP encapsulation of ESP packets", @@ -1064,6 +1098,26 @@ "label.glustervolume": "Volume", "label.go.back": "Go back", "label.gpu": "GPU", +"label.gpucardid": "GPU Card", +"label.gpucardname": "GPU Card", +"label.gpu.card": "GPU Card", +"label.gpu.card.types": "GPU Card Types", +"label.gpu.count": "GPU Count", +"label.gpucount": "GPU Count", +"label.gpu.device": "GPU Device", +"label.gpu.devices": "GPU Devices", +"label.gpu.enabled": "GPU Enabled", +"label.gpuenabled": "GPU Enabled", +"label.gpudevicetype": "Device Type", +"label.gpu.devices.add": "Add GPU Device", +"label.gpu.devices.delete": "Delete GPU Device", +"label.gpu.devices.manage": "Manage GPU Device", +"label.gpu.devices.unmanage": "Unmanage GPU Device", +"label.gpu.display": "GPU Display", +"label.gpulimit": "GPU limits", +"label.gpu.summary": "Summary", +"label.gputotal": "GPU Total", +"label.gpuused": "GPU Used", "label.chart.info": "Information about the charts", "label.group": "Group", "label.group.optional": "Group (Optional)", @@ -1141,7 +1195,7 @@ "label.hosttags.implicit.description": "The host tags defined by CloudStack Agent", "label.hourly": "Hourly", "label.hypervisor": "Hypervisor", -"label.hypervisor.capabilities": "Hypervisor capabilities", +"label.hypervisor.capabilities": "Hypervisor Capabilities", "label.hypervisor.type": "Hypervisor type", "label.hypervisors": "Hypervisors", "label.hypervisorsnapshotreserve": "Hypervisor Snapshot reserve", @@ -1165,12 +1219,13 @@ "label.image.type": "Image type", "label.images": "Images", "label.imagestoreid": "Secondary Storage", -"label.import.backup.offering": "Import backup offering", +"label.import.backup.offering": "Import Backup Offering", "label.import.instance": "Import Instance", -"label.import.offering": "Import offering", -"label.import.role": "Import role", +"label.import.offering": "Import Offering", +"label.import.role": "Import Role", "label.import.volume": "Import Volume", "label.inactive": "Inactive", +"label.inbuilt": "Inbuilt", "label.in.progress": "in progress", "label.in.progress.for": "in progress for", "label.info": "Info", @@ -1182,12 +1237,12 @@ "label.initial": "Inital", "label.initialized": "Initalized", "label.insideportprofile": "Inside port profile", -"label.installwizard.addzoneintro.title": "Let's add a zone", +"label.installwizard.addzoneintro.title": "Let's add a Zone", "label.installwizard.subtitle": "This guide will aid you in setting up your CloudStack™ installation", "label.installwizard.title": "Hello and welcome to CloudStack™", "label.instance": "Instance", "label.instance.conversion.support": "Instance Conversion Supported", -"label.instance.groups": "Instance groups", +"label.instance.groups": "Instance Groups", "label.instance.name": "Instance name", "label.instancename": "Internal name", "label.instanceport": "Instance port", @@ -1310,18 +1365,18 @@ "label.keyboardtype": "Keyboard type", "label.keypair": "SSH key pair", "label.keypairs": "SSH key pair(s)", -"label.kubeconfig.cluster": "Kubernetes cluster config", +"label.kubeconfig.cluster": "Kubernetes Cluster config", "label.kubernetes": "Kubernetes", -"label.kubernetes.access.details": "The kubernetes nodes can be accessed via ssh using:
ssh -i [ssh_key] -p [port_number] cloud@[public_ip_address]

where,
ssh_key: points to the ssh private key file corresponding to the key that was associated while creating the Kubernetes cluster. If no ssh key was provided during Kubernetes cluster creation, use the ssh private key of the management server.
port_number: can be obtained from the Port Forwarding Tab (Public Port column)", -"label.kubernetes.cluster": "Kubernetes cluster", +"label.kubernetes.access.details": "The kubernetes nodes can be accessed via ssh using:
ssh -i [ssh_key] -p [port_number] cloud@[public_ip_address]

where,
ssh_key: points to the ssh private key file corresponding to the key that was associated while creating the Kubernetes Cluster. If no ssh key was provided during Kubernetes cluster creation, use the ssh private key of the management server.
port_number: can be obtained from the Port Forwarding Tab (Public Port column)", "label.kubernetes.cluster.add.nodes.to.cluster": "Add nodes to Kubernetes cluster", -"label.kubernetes.cluster.create": "Create Kubernetes cluster", -"label.kubernetes.cluster.delete": "Delete Kubernetes cluster", -"label.kubernetes.cluster.scale": "Scale Kubernetes cluster", -"label.kubernetes.cluster.start": "Start Kubernetes cluster", -"label.kubernetes.cluster.stop": "Stop Kubernetes cluster", "label.kubernetes.cluster.remove.nodes.from.cluster": "Remove nodes from Kubernetes cluster", -"label.kubernetes.cluster.upgrade": "Upgrade Kubernetes cluster", +"label.kubernetes.cluster": "Kubernetes Cluster", +"label.kubernetes.cluster.create": "Create Kubernetes Cluster", +"label.kubernetes.cluster.delete": "Delete Kubernetes Cluster", +"label.kubernetes.cluster.scale": "Scale Kubernetes Cluster", +"label.kubernetes.cluster.start": "Start Kubernetes Cluster", +"label.kubernetes.cluster.stop": "Stop Kubernetes Cluster", +"label.kubernetes.cluster.upgrade": "Upgrade Kubernetes Cluster", "label.kubernetes.dashboard": "Kubernetes dashboard UI", "label.kubernetes.dashboard.create.token": "Create token for Kubernetes dashboard", "label.kubernetes.dashboard.create.token.desc": "Since Kubernetes v1.24.0, there is no auto-generation of secret-based service Account token due to security reason. You need to create a service Account and an optional long-lived Bearer Token for the service Account.", @@ -1351,9 +1406,9 @@ "label.launch": "Launch", "label.launch.vm": "Launch Instance", "label.launch.vm.and.stay": "Launch Instance & stay on this page", -"label.launch.vnf.appliance": "Launch VNF appliance", -"label.launch.vnf.appliance.and.stay": "Launch VNF appliance & stay on this page", -"label.launch.zone": "Launch zone", +"label.launch.vnf.appliance": "Launch VNF Appliance", +"label.launch.vnf.appliance.and.stay": "Launch VNF Appliance & stay on this page", +"label.launch.zone": "Launch Zone", "label.lb.algorithm.leastconn": "Least connections", "label.lb.algorithm.roundrobin": "Round-robin", "label.lb.algorithm.source": "Source", @@ -1367,8 +1422,8 @@ "label.lbruleid": "Load balancer ID", "label.lbtype": "Load balancer type", "label.ldap": "LDAP", -"label.ldap.configuration": "LDAP configuration", -"label.ldap.group.name": "LDAP group", +"label.ldap.configuration": "LDAP Configuration", +"label.ldap.group.name": "LDAP Group", "label.level": "Level", "label.license.agreements": "License agreements", "label.limit": "Limit", @@ -1382,11 +1437,11 @@ "label.list.ciscoasa1000v": "ASA 1000v", "label.list.ciscovnmc": "Cisco VNMC", "label.list.nodes": "List nodes", -"label.list.pods": "List pods", +"label.list.pods": "List Pods", "label.list.services": "List services", "label.list.vmware.vcenter.vms": "List VMware Instances", "label.livepatch": "Live patch Network's router(s)", -"label.load.balancer": "Load balancer", +"label.load.balancer": "Load Balancer", "label.loadbalancerinstance": "Assigned Instances", "label.loadbalancerrule": "Load balancing rule", "label.loadbalancing": "Load balancing", @@ -1428,10 +1483,9 @@ "label.managementserverid": "Management server", "label.managementservername": "Management server", "label.management.ips": "Management IP addresses", -"label.management.server": "Management server", -"label.management.servers": "Management servers", +"label.management.server": "Management Server", +"label.management.servers": "Management Servers", "label.management.server.peers": "Peers", -"label.managementservername": "Management Server", "label.managementservers": "Number of management servers", "label.matchall": "Match all", "label.max": "Max.", @@ -1446,8 +1500,10 @@ "label.maxcpunumber": "Max CPU cores", "label.maxdatavolumeslimit": "Max data volumes limit", "label.maxerrorretry": "Max error retry", +"label.maxgpu": "Max. GPUs", "label.maxguestslimit": "Max guest limit", -"label.maxhostspercluster": "Max hosts per cluster", +"label.maxheads": "Max. heads", +"label.maxhostspercluster": "Max hosts per Cluster", "label.maximum": "Maximum", "label.maxinstance": "Max Instances", "label.maxiops": "Max IOPS", @@ -1456,14 +1512,17 @@ "label.maxnetwork": "Max. Networks", "label.maxobjectstorage": "Max. Object Storage (GiB)", "label.maxprimarystorage": "Max. primary storage (GiB)", -"label.maxproject": "Max. projects", +"label.maxproject": "Max. Projects", "label.maxpublicip": "Max. public IPs", +"label.maxresolutionx": "Max. resolution X", +"label.maxresolutiony": "Max. resolution Y", "label.maxsecondarystorage": "Max. secondary storage (GiB)", "label.maxsize": "Maximum size", "label.maxsnapshot": "Max. Snapshots", "label.maxtemplate": "Max. Templates", "label.maxuservm": "Max. User Instances", -"label.maxvolume": "Max. volumes", +"label.maxvgpuperphysicalgpu": "Max. vGPUs per physical GPU", +"label.maxvolume": "Max. Volumes", "label.maxvpc": "Max. VPCs", "label.may.continue": "You may now continue.", "label.mb.memory": "MB memory", @@ -1483,8 +1542,8 @@ "label.memoryused": "Used memory", "label.memoryusedgb": "Memory used", "label.memused": "Memory usage", -"label.menu.security.groups": "Security groups", -"label.menu.service.offerings": "Service offerings", +"label.menu.security.groups": "Security Groups", +"label.menu.service.offerings": "Service Offerings", "label.metadata": "Metadata", "label.metadata.description": "Metadata of the Object", "label.metadata.upload.description": "Set metadata for the object", @@ -1496,8 +1555,8 @@ "label.migrate.instance.to.ps": "Migrate Instance to another primary storage", "label.migrate.instance.single.storage": "Migrate all volume(s) of the Instance to a single primary storage", "label.migrate.instance.specific.storages": "Migrate volume(s) of the Instance to specific primary storages", -"label.migrate.systemvm.to": "Migrate system VM to", -"label.migrate.volume": "Migrate volume", +"label.migrate.systemvm.to": "Migrate System VM to", +"label.migrate.volume": "Migrate Volume", "message.memory.usage.info.hypervisor.additionals": "The data shown may not reflect the actual memory usage if the Instance does not have the additional hypervisor tools installed", "message.memory.usage.info.negative.value": "If the Instance's memory usage cannot be obtained from the hypervisor, the lines for free memory in the raw data graph and memory usage in the percentage graph will be disabled", "message.migrate.volume.tooltip": "Volume can be migrated to any suitable storage pool. Admin has to choose the appropriate disk offering to replace, that supports the new storage pool", @@ -1544,20 +1603,30 @@ "label.native": "Native", "label.ncc": "NCC", "label.netmask": "Netmask", +"label.netris": "Netris", +"label.netristag": "Netris tag", +"label.netris.provider": "Netris Provider", +"label.netris.provider.name": "Netris provider name", +"label.netris.provider.username": "Netris provider username", +"label.netris.provider.password": "Netris provider password", +"label.netris.provider.site": "Netris provider Site name", +"label.netris.provider.tenant.name": "Netris provider Admin Tenant name", +"label.netris.provider.tag": "Netris Tag", +"label.netris.provider.url": "Netris provider URL", "label.netscaler": "NetScaler", "label.netscaler.mpx": "NetScaler MPX LoadBalancer", "label.netscaler.sdx": "NetScaler SDX LoadBalancer", "label.netscaler.vpx": "NetScaler VPX LoadBalancer", "label.network": "Network", "label.network.acl": "Network ACL", -"label.network.acl.lists": "Network ACL lists", +"label.network.acls": "Network ACLs", "label.network.addvm": "Add Network to Instance", "label.network.desc": "Network desc", -"label.network.domain": "Network domain", +"label.network.domain": "Network Domain", "label.network.label.display.for.blank.value": "Use default gateway", "label.network.name": "Network name", -"label.network.offering": "Network offering", -"label.network.offerings": "Network offerings", +"label.network.offering": "Network Offerings", +"label.network.offerings": "Network Offerings", "label.network.policy": "Network Policy", "label.network.restart.required": "Network restart required", "label.network.route.table": "Network route table", @@ -1574,9 +1643,9 @@ "label.networklimit": "Network limits", "label.networkmode": "Network Mode", "label.networkname": "Network name", -"label.networkofferingdisplaytext": "Network offering", -"label.networkofferingid": "Network offering", -"label.networkofferingname": "Network offering", +"label.networkofferingdisplaytext": "Network Offering", +"label.networkofferingid": "Network Offering", +"label.networkofferingname": "Network Offering", "label.networkrate": "Network rate (Mb/s)", "label.networkread": "Network read", "label.networks": "Networks", @@ -1586,9 +1655,9 @@ "label.never": "Never", "label.new": "New", "label.new.autoscale.vmgroup": "New AutoScaling Group", -"label.new.instance.group": "New Instance group", +"label.new.instance.group": "New Instance Group", "label.new.password": "New password", -"label.new.project": "New project", +"label.new.project": "New Project", "label.new.secondaryip.description": "Enter new secondary IP address", "label.new.tag": "New tag", "label.new.vm": "New Instance", @@ -1597,6 +1666,7 @@ "label.newinstance": "New Instance", "label.newname": "New name", "label.next": "Next", +"label.nexthop": "Next hop", "label.nfs": "NFS", "label.nfsmountopts": "NFS mount options", "label.nfsserver": "NFS server", @@ -1630,16 +1700,17 @@ "label.nsx.provider.port": "NSX provider port", "label.nsx.provider.username": "NSX provider username", "label.nsx.provider.password": "NSX provider password", -"label.nsx.provider.edgecluster": "NSX provider edge cluster", +"label.nsx.provider.edgecluster": "NSX provider edge Cluster", "label.nsx.provider.tier0gateway": "NSX provider tier-0 gateway", -"label.nsx.provider.transportzone": "NSX provider transport zone", +"label.nsx.provider.transportzone": "NSX provider transport Zone", "label.nsx.supports.internal.lb": "Enable NSX internal LB service", "label.nsx.supports.lb": "Enable NSX LB service", "label.num.cpu.cores": "# of CPU cores", +"label.numanode": "NUMA node", "label.number": "#Rule", "label.numretries": "Number of retries", "label.nvpdeviceid": "ID", -"label.oauth.configuration": "OAuth configuration", +"label.oauth.configuration": "OAuth Configuration", "label.oauth.verification": "OAuth verification", "label.ocfs2": "OCFS2", "label.object.storage" : "Object Storage", @@ -1657,7 +1728,7 @@ "label.of.month": "of month", "label.offerha": "Offer HA", "label.offeringid": "Offering ID", -"label.offeringtype": "Compute offering type", +"label.offeringtype": "Compute Offering type", "label.ok": "OK", "label.only.end.date.and.time": "Only end date and time", "label.only.start.date.and.time": "Only start date and time", @@ -1674,6 +1745,9 @@ "label.operator.less.or.equal": "Less than or equals to", "label.operator.equal": "Equals to", "label.optional": "Optional", +"label.options": "Options", +"label.orchestrator": "Orchestrator", +"label.orchestratorrequirespreparevm": "Requires Prepare Instance", "label.order": "Order", "label.os": "Operating System", "label.oscategoryid": "OS category", @@ -1715,9 +1789,11 @@ "label.palo.alto.firewall": "Palo Alto firewall", "label.palp": "Palo Alto log profile", "label.params": "Parameters", +"label.parameters": "Parameters", "label.param.name": "Parameter name", "label.param.value": "Parameter value", "label.parentdomainname": "Parent domain", +"label.parentgpudeviceid": "Parent GPU device", "label.parentname": "Parent", "label.parentsubnet": "Parent Subnet", "label.passive": "Passive", @@ -1727,8 +1803,10 @@ "label.password.tooltip": "The password for the Host", "label.passwordenabled": "Password enabled", "label.path": "Path", +"label.pathready": "Path Ready", +"label.pathstate": "Path State", "label.patp": "Palo Alto threat profile", -"label.pavr": "Virtual router", +"label.pavr": "Virtual Router", "label.payload": "Payload", "label.payloadurl": "Payload URL", "label.pcidevice": "GPU", @@ -1742,7 +1820,7 @@ "label.pending.jobs": "Pending Jobs", "label.pendingjobscount": "Number Of pending jobs", "label.per.account": "Per Account", -"label.per.zone": "Per zone", +"label.per.zone": "Per Zone", "label.percentage": "Percentage", "label.perfectforwardsecrecy": "Perfect forward secrecy", "label.perform.fresh.checks": "Perform fresh checks", @@ -1774,7 +1852,7 @@ "label.powerflex.gateway": "Gateway", "label.powerflex.gateway.password": "Gateway password", "label.powerflex.gateway.username": "Gateway username", -"label.powerflex.storage.pool": "Storage pool", +"label.powerflex.storage.pool": "Storage Pool", "label.powerstate": "Power state", "label.preferred": "Preferred", "label.prefix": "Prefix", @@ -1791,13 +1869,13 @@ "label.flashArray.username.tooltip": "The username with edit privileges", "label.flashArray.url.tooltip": "URL designating the Flash Array endpoint, formatted as: http[s]://HOSTNAME:PORT?pod=NAME&hostgroup=NAME[&skipTlsValidation=true][&postCopyWaitMs=#][&keyttl=#][&connectTimeoutMs=#][&apiLoginVersion=#][&apiVersion=#] where values in [] are optional.", "label.primary": "Primary", -"label.primary.storage": "Primary storage", -"label.primary.storage.allocated": "Primary storage allocated", -"label.primary.storage.used": "Primary storage used", -"label.primarystoragelimit": "Primary storage limits (GiB)", -"label.primarystoragetotal": "Primary storage", +"label.primary.storage": "Primary Storage", +"label.primary.storage.allocated": "Primary Storage allocated", +"label.primary.storage.used": "Primary Storage used", +"label.primarystoragelimit": "Primary Storage limits (GiB)", +"label.primarystoragetotal": "Primary Storage", "label.privatemtu": "Private Interface MTU", -"label.private.gateway": "Private gateway", +"label.private.gateway": "Private Gateway", "label.private.interface": "Private interface", "label.private.registry": "Private registry", "label.privateinterface": "Private interface", @@ -1807,7 +1885,7 @@ "label.privateport": "Private port", "label.profilename": "Profile", "label.project": "Project", -"label.project.invitation": "Project invitations", +"label.project.invitation": "Project Invitations", "label.project.name": "Project name", "label.project.owner": "Project owner(s)", "label.project.role": "Project role", @@ -1835,10 +1913,11 @@ "label.publicmtu": "Public Interface MTU", "label.public.interface": "Public interface", "label.public.ip": "Public IP address", -"label.public.ip.addresses": "Public IP addresses", +"label.public.ip.addresses": "Public IP Addresses", "label.public.ips": "Public IP addresses", "label.public.lb": "Public LB", "label.public.traffic": "Public traffic", +"label.public.traffic.netris": "Netris Public IP Pool", "label.public.traffic.nsx": "NSX Public traffic", "label.publicinterface": "Public interface", "label.publicip": "IP address", @@ -1917,10 +1996,12 @@ "label.redundantvpcrouter": "Redundant VPC", "label.refresh": "Refresh", "label.region": "Region", +"label.register.extension": "Register Extension", "label.register.oauth": "Register OAuth", "label.register.template": "Register Template", "label.register.user.data": "Register User Data", "label.register.cni.config": "Register CNI Configuration", +"label.register.user.data.details": "Enter the User Data in plain text or in Base64 encoding. Up to 32KB of Base64 encoded User Data can be sent by default. The setting vm.userdata.max.length can be used to increase the limit to upto 1MB.", "label.reinstall.vm": "Reinstall Instance", "label.reject": "Reject", "label.related": "Related", @@ -1928,11 +2009,11 @@ "label.release": "Release", "label.release.account": "Release from Account", "label.release.dedicated.bgp.peer": "Release dedicated BGP peer", -"label.release.dedicated.cluster": "Release dedicated cluster", +"label.release.dedicated.cluster": "Release dedicated Cluster", "label.release.dedicated.host": "Release dedicated host", "label.release.dedicated.ipv4.subnet": "Release dedicated IPv4 subnet", -"label.release.dedicated.pod": "Release dedicated pod", -"label.release.dedicated.zone": "Release dedicated zone", +"label.release.dedicated.pod": "Release dedicated Pod", +"label.release.dedicated.zone": "Release dedicated Zone", "label.releasing.ip": "Releasing IP", "label.remote.instances": "Remote Instances", "label.remove": "Remove", @@ -1946,7 +2027,7 @@ "label.remove.ldap": "Remove LDAP", "label.remove.logical.network": "Remove Network from logical router", "label.remove.logical.router": "Remove logical router", -"label.remove.network.offering": "Remove Network offering", +"label.remove.network.offering": "Remove Network Offering", "label.remove.network.route.table": "Remove Tungsten Fabric Network routing table", "label.remove.nodes": "Remove nodes from Kubernetes cluster", "label.remove.pf": "Remove port forwarding rule", @@ -1958,15 +2039,14 @@ "label.remove.rule": "Remove rule", "label.remove.ssh.key.pair": "Remove SSH Key pair", "label.remove.tungsten.tag": "Remove Tag", -"label.remove.user.data": "Remove Userdata", +"label.remove.user.data": "Remove User Data", "label.remove.vm.from.lb": "Remove Instance from load balancer rule", "label.remove.vmware.datacenter": "Remove VMware Datacenter", "label.remove.vpc": "Remove VPC", -"label.remove.vpc.offering": "Remove VPC offering", +"label.remove.vpc.offering": "Remove VPC Offering", "label.removed": "Removed", "label.removing": "Removing", "label.replace.acl": "Replace ACL", -"label.replace.acl.list": "Replace ACL list", "label.report.bug": "Ask a question or Report an issue", "label.request": "Request", "label.required": "Required", @@ -1985,9 +2065,10 @@ "label.reset.config.value": "Reset to default value", "label.reset.ssh.key.pair": "Reset SSH key pair", "label.reset.to.default": "Reset to default", -"label.reset.userdata.on.autoscale.vm.group": "Reset Userdata on AutoScale VM Group", -"label.reset.userdata.on.vm": "Reset Userdata on Instance", +"label.reset.user.data.on.autoscale.vm.group": "Reset User Data on AutoScale VM Group", +"label.reset.user.data.on.vm": "Reset User Data on Instance", "label.reset.vpn.connection": "Reset VPN connection", +"label.resolution": "Resolution", "label.resource": "Resource", "label.resource.limit.exceeded": "Resource limit exceeded", "label.resource.name": "Resource name", @@ -2038,6 +2119,7 @@ "label.rules.file.import.description": "Click or drag rule definitions CSV file to import.", "label.rules.file.to.import": "Rule definitions CSV file to import", "label.run.proxy.locally": "Run proxy locally", +"label.run.custom.action": "Run Action", "label.running": "Running", "label.running.vms": "Running Instances", "label.s2scustomergatewayid": "Site to site customer gateway ID", @@ -2076,17 +2158,17 @@ "label.search": "Search", "label.secondary.isolated.vlan.type.isolated": "Isolated", "label.secondary.isolated.vlan.type.promiscuous": "Promiscuous", -"label.secondary.storage": "Secondary storage", -"label.secondary.storage.vm": "Secondary storage VM", +"label.secondary.storage": "Secondary Storage", +"label.secondary.storage.vm": "Secondary Storage VM", "label.secondaryips": "Secondary IPs", -"label.secondarystoragelimit": "Secondary storage limits (GiB)", +"label.secondarystoragelimit": "Secondary Storage limits (GiB)", "label.secretkey": "Secret key", "label.secured": "Secured", -"label.security.groups": "Security groups", -"label.securitygroup": "Security group", -"label.securitygroupenabled": "Security groups enabled", +"label.security.groups": "Security Groups", +"label.securitygroup": "Security Group", +"label.securitygroupenabled": "Security Groups enabled", "label.securitygroups": "Security groups", -"label.securitygroupsenabled": "Security groups enabled", +"label.securitygroupsenabled": "Security Groups enabled", "label.select": "Select", "label.see.more.info.cpu.usage": "See more info about CPU usage", "label.see.more.info.memory.usage": "See more info about memory usage", @@ -2094,18 +2176,20 @@ "label.see.more.info.disk.usage": "See more info about disk usage", "label.see.more.info.shown.charts": "See more info about the shown charts", "label.select-view": "Select view", -"label.select.a.zone": "Select a zone", +"label.select.all": "Select all", +"label.select.columns": "Select columns", +"label.select.a.zone": "Select a Zone", "label.select.deployment.infrastructure": "Select deployment infrastructure", "label.select.guest.os.type": "Please select the guest OS type", "label.select.network": "Select Network", "label.select.period": "Select period", -"label.select.project": "Select project", -"label.select.projects": "Select projects", -"label.select.ps": "Select primary storage", +"label.select.project": "Select Project", +"label.select.projects": "Select Projects", +"label.select.ps": "Select Primary Storage", "label.select.root.disk": "Select the ROOT disk", "label.select.source.vcenter.datacenter": "Select the source VMware vCenter Datacenter", "label.select.tier": "Select Network Tier", -"label.select.zones": "Select zones", +"label.select.zones": "Select Zones", "label.select.2fa.provider": "Select the provider", "label.selected.storage": "Selected storage", "label.self": "Mine", @@ -2116,26 +2200,26 @@ "label.sequence": "Sequence", "label.server": "Server", "label.server.certificate": "Server certificate", -"label.serviceip": "Service IP", +"label.serviceip": "Management Service IP", "label.service.connectivity.distributedroutercapabilitycheckbox": "Distributed router", "label.service.connectivity.regionlevelvpccapabilitycheckbox": "Region level VPC", -"label.service.group": "Service group", -"label.serviceip": "Management IP", +"label.service.group": "Service Group", +"label.management.ip": "Management IP", "label.service.lb.elasticlbcheckbox": "Elastic LB", "label.service.lb.inlinemodedropdown": "Mode", "label.service.lb.lbisolationdropdown": "LB isolation", "label.service.lb.netscaler.servicepackages": "Netscaler service packages", "label.service.lb.netscaler.servicepackages.description": "Service package description", -"label.service.offering": "Service offering", "label.service.offering.controlnodes": "Compute offering for Control Nodes", "label.service.offering.etcdnodes": "Compute offering for etcd Nodes", "label.service.offering.workernodes": "Compute offering for Worker Nodes", +"label.service.offering": "Service Offering", "label.service.staticnat.associatepublicip": "Associate public IP", "label.service.staticnat.elasticipcheckbox": "Elastic IP", "label.servicegroupuuid": "Service Group", "label.servicelist": "Services", -"label.serviceofferingid": "Compute offering", -"label.serviceofferingname": "Compute offering", +"label.serviceofferingid": "Compute Offering", +"label.serviceofferingname": "Compute Offering", "label.sessions": "Active client sessions", "label.set.default.nic": "Set default NIC", "label.set.reservation": "Set reservation", @@ -2152,14 +2236,16 @@ "label.sharedrouteripv6": "IPv6 address for the VR in this shared Network.", "label.sharewith": "Share with", "label.showing": "Showing", +"label.show.only.gpu.enabled.offerings": "Show only GPU enabled offerings", "label.show.usage.records": "Show usage records", "label.showing.results.for": "Showing results for \"%x\"", "label.shrinkok": "Shrink OK", "label.shutdown": "Shutdown", "label.shutdown.provider": "Shutdown provider", "label.simplified.chinese.keyboard": "Simplified Chinese keyboard", +"label.site": "Netris Site", "label.site.to.site.vpn": "Site-to-site VPN", -"label.site.to.site.vpn.connections": "Site-to-site VPN Connections", +"label.site.to.site.vpn.connections": "VPN Connections", "label.size": "Size", "label.sizegb": "Size", "label.smb.domain": "SMB domain", @@ -2204,24 +2290,26 @@ "label.srctaguuid": "Source Tag", "label.srx": "SRX", "label.srx.firewall": "Juniper SRX firewall", -"label.ssh.key.pairs": "SSH key pairs", "label.storageaccessgroups": "Storage Access Groups", "label.clusterstorageaccessgroups": "Cluster Storage Access Groups", "label.podstorageaccessgroups": "Pod Storage Access Groups", "label.zonestorageaccessgroups": "Zone Storage Access Groups", +"label.ssh.key.pairs": "SSH Key Pairs", "label.uefi.supported": "UEFI supported", +"label.unregister.extension": "Unregister Extension", "label.usediops": "IOPS used", -"label.userdataid": "Userdata ID", -"label.userdataname": "Userdata name", -"label.userdatadetails": "Userdata details", -"label.userdataparams": "Userdata parameters", -"label.userdatapolicy": "Userdata link policy", -"label.userdata.text": "Manual Userdata entry", -"label.userdata.registered": "Stored Userdata", -"label.userdata.do.override": "Userdata override", -"label.userdata.do.append": "Userdata append", -"label.userdatapolicy.tooltip": "Userdata linked to the Template can be overridden by Userdata provided during Instance deploy. Select the override policy as required.", +"label.user.data.id": "User Data ID", +"label.user.data.name": "User Data name", +"label.user.data.details": "User Data details", +"label.user.data.params": "User Data parameters", +"label.user.data.policy": "User Data link policy", +"label.user.data.text": "Manual User Data entry", +"label.user.data.registered": "Stored User Data", +"label.user.data.do.override": "User Data override", +"label.user.data.do.append": "User Data append", +"label.user.data.policy.tooltip": "User Data linked to the Template can be overridden by User Data provided during Instance deploy. Select the override policy as required.", "label.user.data": "User Data", +"label.user.data.library": "User Data Library", "label.ssh.port": "SSH port", "label.sshkeypair": "New SSH key pair", "label.sshkeypairs": "SSH key pairs", @@ -2248,7 +2336,7 @@ "label.state.reported": "Reported State", "label.staticnat": "Static NAT", "label.static": "Static", -"label.static.routes": "Static routes", +"label.static.routes": "Static Routes", "label.status": "Status", "label.step.1": "Step 1", "label.step.2": "Step 2", @@ -2274,9 +2362,9 @@ "label.stopping": "Stopping", "label.storage": "Storage", "label.storage.migration.required": "Storage migration required", -"label.storage.tags": "Storage tags", +"label.storage.tags": "Storage Tags", "label.storage.traffic": "Storage traffic", -"label.storageid": "Primary storage", +"label.storageid": "Primary Storage", "label.storagemotionenabled": "Storage motion enabled", "label.storagepolicy": "Storage policy", "label.storagepool": "Storage pool", @@ -2289,6 +2377,7 @@ "label.subnet": "Subnet", "label.succeeded": "Succeeded", "label.success": "Success", +"label.successmessage": "Success message", "label.success.migrations": "Successful migrations", "label.success.set": "Successfully set", "label.success.updated": "Successfully updated", @@ -2303,12 +2392,12 @@ "label.supportspublicaccess": "Supports public access", "label.supportsstrechedl2subnet": "Supports stretched L2 subnet", "label.supportsvmautoscaling": "Supports auto scaling", -"label.suspend.project": "Suspend project", +"label.suspend.project": "Suspend Project", "label.switch.type": "Switch type", -"label.sync.storage": "Sync storage pool", +"label.sync.storage": "Sync Storage Pool", "label.system.ip.pool": "System Pool", -"label.system.offering": "System offering", -"label.system.offerings": "System offerings", +"label.system.offering": "System Offering", +"label.system.offerings": "System Offerings", "label.system.service.offering": "System service offering", "label.system.vm": "System VM", "label.system.vms": "System VMs", @@ -2322,6 +2411,7 @@ "label.systemvm": "System VM", "label.systemvmtype": "System VM type", "label.tag": "Tag", +"label.tag.netris": "netris", "label.tag.nsx": "nsx", "label.tag.key": "Tag key", "label.tag.systemvm": "systemvm", @@ -2353,6 +2443,7 @@ "label.templatesubject": "Subject", "label.templatetype": "Template type", "label.templateversion": "Template version", +"label.tenantname": "Netris Tenant", "label.term.type": "Term type", "label.test": "Test", "label.test.webhook.delivery": "Test Webhook Delivery", @@ -2375,6 +2466,7 @@ "label.threshold": "Threshold", "label.threshold.description": "Value for which the Counter will be evaluated with the Operator selected", "label.thursday": "Thursday", +"label.tier0gateway": "Tier-0 Gateway", "label.time": "Time", "label.timeout": "Timeout", "label.timeout.in.second ": " Timeout (seconds)", @@ -2388,11 +2480,12 @@ "label.total": "Total", "label.total.network": "Total Networks", "label.total.vms": "Total Instances", -"label.total.volume": "Total volumes", +"label.total.volume": "Total Volumes", "label.totalcpu": "Total CPU", "label.traffic.label": "Traffic label", "label.traffic.types": "Traffic types", "label.traffictype": "Traffic type", +"label.transportzone": "Transport Zone", "label.transportzoneuuid": "Transport zone UUID", "label.trigger.shutdown": "Trigger Safe Shutdown", "label.true": "True", @@ -2453,6 +2546,10 @@ "label.update.autoscale.vmgroup": "Update AutoScaling Group", "label.update.bgp.peer": "Update BGP peer", "label.update.condition": "Update condition", +"label.update.gpu.device": "Update GPU device", +"label.update.vgpu.profile": "Update vGPU profile", +"label.update.custom.action": "Update Custom Action", +"label.update.extension": "Update Extension", "label.update.sharedfs": "Update Shared FileSystem", "label.update.instance.group": "Update Instance group", "label.update.ip.range": "Update IP range", @@ -2476,8 +2573,8 @@ "label.upload.resource.icon": "Upload icon", "label.upload.template.from.local": "Upload Template from local", "label.upload.volume": "Upload volume", -"label.upload.volume.from.local": "Upload volume from local", -"label.upload.volume.from.url": "Upload volume from URL", +"label.upload.volume.from.local": "Upload Volume from local", +"label.upload.volume.from.url": "Upload Volume from URL", "label.url": "URL", "label.usage.explanation": "Note: Only the usage server that owns the active usage job is shown here.", "label.usage": "Usage", @@ -2504,11 +2601,9 @@ "label.use.router.ip.resolver": "Use Virtual Router IP as resolver", "label.used": "Used", "label.usehttps": "Use HTTPS", -"label.usenewdiskoffering": "Replace disk offering?", +"label.usenewdiskoffering": "Replace Disk Offering?", "label.user": "User", "label.user.conflict": "Conflict", -"label.userdata": "Userdata", -"label.userdatal2": "User data", "label.username": "Username", "label.username.tooltip": "The Username for the Host", "label.users": "Users", @@ -2517,6 +2612,8 @@ "label.utilization": "Utilization", "label.uuid": "ID", "label.value": "Value", +"label.validationformat": "Validation Format", +"label.valueoptions": "Values Options", "label.vcenter": "VMware datacenter vCenter", "label.vcenter.datacenter": "vCenter datacenter", "label.vcenter.datastore": "vCenter datastore", @@ -2528,22 +2625,29 @@ "label.vcenterpassword": "vCenter password", "label.vcenterusername": "vCenter username", "label.vcsdeviceid": "ID", +"label.vendorid": "Vendor ID", +"label.vendorname": "Vendor Name", "label.verify": "Verify", "label.version": "Version", "label.versions": "Versions", -"label.vgpu": "VGPU", +"label.vgpu": "Profile", +"label.vgpuprofileid": "Profile", +"label.vgpuprofileids": "Profile", +"label.vgpuprofilename": "Profile", +"label.vgpu.profile": "GPU Profile", "label.vgputype": "vGPU type", +"label.videoram": "Video RAM", "label.view": "View", "label.view.all": "View all", "label.view.console": "View console", "label.viewing": "Viewing", "label.virtualmachine": "Instance", "label.virtualmachinecount": "Instances Count", -"label.virtual.machine": "Virtual machine", -"label.virtual.machines": "Virtual machines", +"label.virtual.machine": "Virtual Machine", +"label.virtual.machines": "Virtual Machines", "label.virtual.network": "Virtual Network", "label.virtual.networking": "Virtual Networking", -"label.virtual.routers": "Virtual routers", +"label.virtual.routers": "Virtual Routers", "label.virtualmachineid": "Instance ID", "label.virtualmachinename": "Instance name", "label.virtualsize": "Virtual Size", @@ -2575,21 +2679,21 @@ "label.vmwaredcvcenter": "VMware datacenter vCenter", "label.vmwarenetworklabel": "VMware traffic label", "label.vnf.appliance": "VNF Appliance", -"label.vnf.appliances": "VNF appliances", +"label.vnf.appliances": "VNF Appliances", "label.vnf.appliance.add": "Add VNF Appliance", -"label.vnf.appliance.access.methods": "Management access information for this VNF appliance", -"label.vnf.app.action.destroy": "Destroy VNF appliance", -"label.vnf.app.action.edit": "Edit VNF appliance", -"label.vnf.app.action.expunge": "Expunge VNF appliance", -"label.vnf.app.action.migrate.to.host": "Migrate VNF appliance to another host", -"label.vnf.app.action.migrate.to.ps": "Migrate VNF appliance to another primary storage", -"label.vnf.app.action.recover": "Recover VNF appliance", -"label.vnf.app.action.scale": "Scale VNF appliance", -"label.vnf.app.action.start": "Start VNF appliance", -"label.vnf.app.action.stop": "Stop VNF appliance", -"label.vnf.app.action.reboot": "Reboot VNF appliance", -"label.vnf.app.action.reinstall": "Reinstall VNF appliance", -"label.vnf.cidr.list": "CIDR from which access to the VNF appliance’s Management interface should be allowed from", +"label.vnf.appliance.access.methods": "Management access information for this VNF Appliance", +"label.vnf.app.action.destroy": "Destroy VNF Appliance", +"label.vnf.app.action.edit": "Edit VNF Appliance", +"label.vnf.app.action.expunge": "Expunge VNF Appliance", +"label.vnf.app.action.migrate.to.host": "Migrate VNF Appliance to another host", +"label.vnf.app.action.migrate.to.ps": "Migrate VNF Appliance to another primary storage", +"label.vnf.app.action.recover": "Recover VNF Appliance", +"label.vnf.app.action.scale": "Scale VNF Appliance", +"label.vnf.app.action.start": "Start VNF Appliance", +"label.vnf.app.action.stop": "Stop VNF Appliance", +"label.vnf.app.action.reboot": "Reboot VNF Appliance", +"label.vnf.app.action.reinstall": "Reinstall VNF Appliance", +"label.vnf.cidr.list": "CIDR from which access to the VNF appliance's Management interface should be allowed from", "label.vnf.cidr.list.tooltip": "the CIDR list to forward traffic from to the VNF management interface. Multiple entries must be separated by a single comma character (,). The default value is 0.0.0.0/0.", "label.vnf.configure.management": "Configure Firewall and Port Forwarding rules for VNF's management interfaces", "label.vnf.configure.management.tooltip": "True by default, security group or network rules (source nat and firewall rules) will be configured for VNF management interfaces. False otherwise. Learn what rules are configured at http://docs.cloudstack.apache.org/en/latest/adminguide/networking/vnf_templates_appliances.html#deploying-vnf-appliances", @@ -2612,7 +2716,7 @@ "label.vnf.templates": "VNF templates", "label.vnf.template.register": "Register VNF template", "label.vnmc": "VNMC", -"label.volgroup": "Volume group", +"label.volgroup": "Volume Group", "label.volume": "Volume", "label.vms.empty": "No VMs available to be added to the Kubernetes cluster", "label.vms.remove.empty": "No external VMs present in the Kubernetes cluster to be removed", @@ -2623,7 +2727,7 @@ "label.volumechecksum": "MD5 checksum", "label.volumechecksum.description": "Use the hash that you created at the start of the volume upload procedure.", "label.volumefileupload": "Local file", -"label.volumegroup": "Volume group", +"label.volumegroup": "Volume Group", "label.volumeid": "Volume", "label.volumeids": "Volumes to be deleted", "label.volumelimit": "Volume limits", @@ -2633,21 +2737,23 @@ "label.volumetype": "Volume Type", "label.vpc": "VPC", "label.vpcs": "VPCs", +"label.vpc.gateway.ip": "VPC Gateway IP", "label.vpc.id": "VPC ID", -"label.vpc.offerings": "VPC offerings", -"label.vpc.virtual.router": "VPC virtual router", +"label.vpc.offerings": "VPC Offerings", +"label.vpc.virtual.router": "VPC Virtual Router", "label.vpc.restart.required": "VPC restart required", "label.vpcid": "VPC", "label.vpclimit": "VPC limits", "label.vpcname": "VPC", -"label.vpcoffering": "VPC offering", -"label.vpcofferingid": "VPC offering", +"label.vpcoffering": "VPC Offering", +"label.vpcofferingid": "VPC Offering", "label.vpn": "VPN", "label.vpn.connection": "VPN connection", "label.vpn.gateway": "VPN gateway", "label.vpn.users": "VPN Users", "label.vpncustomergateway": "IP address of the remote gateway", -"label.vpncustomergatewayid": "VPN customer gateway", +"label.vramsize": "VRAM Size", +"label.vpncustomergatewayid": "VPN Customer Gateway", "label.vsmipaddress": "Nexus 1000v IP address", "label.vsmpassword": "Nexus 1000v password", "label.vsmusername": "Nexus 1000v username", @@ -2682,7 +2788,7 @@ "label.xenservertoolsversion61plus": "Original XS Version is 6.1+", "label.yes": "Yes", "label.yourinstance": "Your Instance", -"label.your.autoscale.vmgroup": "Your autoscaling group", +"label.your.autoscale.vmgroup": "Your Autoscaling Group", "label.zone": "Zone", "label.zone.dedicated": "Zone dedicated", "label.zone.details": "Zone details", @@ -2695,6 +2801,7 @@ "label.zones": "Zones", "label.zonewizard.traffictype.storage": "Storage: Traffic between primary and secondary storage servers, such as Instance Templates and Snapshots.", "label.buckets": "Buckets", +"label.busaddress": "Address", "label.objectstorageid": "Object Storage Pool", "label.oobm.address": "Out-of-band management address", "label.oobm.driver": "Out-of-band management driver", @@ -2704,6 +2811,8 @@ "label.bucket.update": "Update Bucket", "label.bucket.delete": "Delete Bucket", "label.quotagib": "Quota in GiB", +"label.quotagb": "Quota in GB", +"label.edgecluster": "Edge Cluster", "label.encryption": "Encryption", "label.etcdnodes": "Number of etcd nodes", "label.versioning": "Versioning", @@ -2732,12 +2841,15 @@ "message.action.delete.autoscale.vmgroup": "Please confirm that you want to delete this autoscaling group.", "message.action.delete.backup.offering": "Please confirm that you want to delete this backup offering?", "message.action.delete.backup.repository": "Please confirm that you want to delete this backup repository?", -"message.action.delete.cluster": "Please confirm that you want to delete this cluster.", +"message.action.delete.cluster": "Please confirm that you want to delete this Cluster.", +"message.action.delete.custom.action": "Please confirm that you want to delete this custom action.", "message.action.delete.domain": "Please confirm that you want to delete this domain.", +"message.action.delete.extension": "Please confirm that you want to delete the extension", "message.action.delete.external.firewall": "Please confirm that you would like to remove this external firewall. Warning: If you are planning to add back the same external firewall, you must reset usage data on the device.", "message.action.delete.external.load.balancer": "Please confirm that you would like to remove this external load balancer. Warning: If you are planning to add back the same external load balancer, you must reset usage data on the device.", "message.action.delete.ingress.rule": "Please confirm that you want to delete this ingress rule.", "message.action.delete.ipv4.subnet": "Please confirm that you want to delete this IPv4 subnet.", +"message.action.delete.gpu.card": "Please confirm that you want to delete this GPU card.", "message.action.delete.guest.os": "Please confirm that you want to delete this guest os. System defined entry cannot be deleted.", "message.action.delete.guest.os.category": "Please confirm that you want to delete this guest os category.", "message.action.delete.guest.os.hypervisor.mapping": "Please confirm that you want to delete this guest os hypervisor mapping. System defined entry cannot be deleted.", @@ -2750,15 +2862,16 @@ "message.action.delete.node": "Please confirm that you want to delete this node.", "message.action.delete.oauth.provider": "Please confirm that you want to delete the OAuth provider.", "message.action.delete.physical.network": "Please confirm that you want to delete this physical Network.", -"message.action.delete.pod": "Please confirm that you want to delete this pod.", +"message.action.delete.pod": "Please confirm that you want to delete this Pod.", "message.action.delete.secondary.storage": "Please confirm that you want to delete this secondary storage.", "message.action.delete.security.group": "Please confirm that you want to delete this security group.", "message.action.delete.snapshot": "Please confirm that you want to delete this Snapshot.", "message.action.delete.template": "Please confirm that you want to delete this Template.", "message.action.delete.tungsten.router.table": "Please confirm that you want to remove Route Table from this Network?", +"message.action.delete.vgpu.profile": "Please confirm that you want to delete this vGPU profile.", "message.action.delete.volume": "Please confirm that you want to delete this volume. Note: this will not delete any Snapshots of this volume.", "message.action.delete.vpn.user": "Please confirm that you want to delete the VPN user.", -"message.action.delete.zone": "Please confirm that you want to delete this zone.", +"message.action.delete.zone": "Please confirm that you want to delete this Zone.", "message.action.destroy.sharedfs": "Please confirm that you want to destroy this Shared FileSystem.
Caution: This will delete all the data of the Shared FileSystem as well.", "message.action.destroy.instance": "Please confirm that you want to destroy the Instance.", "message.action.destroy.instance.with.backups": "Please confirm that you want to destroy the Instance. There may be backups associated with the Instance which will not be deleted.", @@ -2766,35 +2879,36 @@ "message.action.destroy.volume": "Please confirm that you want to destroy the volume.", "message.action.disable.2FA.user.auth": "Please confirm that you want to disable User two factor authentication.", "message.action.about.mandate.and.disable.2FA.user.auth": "Two factor authentication is mandated for the User, if this is disabled now User will need to setup two factor authentication again during next login.

Please confirm that you want to disable.", -"message.action.disable.cluster": "Please confirm that you want to disable this cluster.", +"message.action.disable.cluster": "Please confirm that you want to disable this Cluster.", "message.action.disable.disk.offering": "Please confirm that you want to disable this disk offering.", "message.action.disable.service.offering": "Please confirm that you want to disable this service offering.", "message.action.disable.system.service.offering": "Please confirm that you want to disable this system service offering.", "message.action.disable.physical.network": "Please confirm that you want to disable this physical Network.", -"message.action.disable.pod": "Please confirm that you want to disable this pod.", +"message.action.disable.pod": "Please confirm that you want to disable this Pod.", "message.action.disable.static.nat": "Please confirm that you want to disable static NAT.", -"message.action.disable.zone": "Please confirm that you want to disable this zone.", +"message.action.disable.zone": "Please confirm that you want to disable this Zone.", +"message.action.discover.gpu.devices": "Please confirm that you want to discover GPU devices.", "message.action.download.iso": "Please confirm that you want to download this ISO.", "message.action.download.snapshot": "Please confirm that you want to download this Snapshot.", "message.action.download.template": "Please confirm that you want to download this Template.", "message.action.edit.nfs.mount.options": "Changes to NFS mount options will only take affect on cancelling maintenance mode which will cause the storage pool to be remounted on all KVM hosts with the new mount options.", -"message.action.enable.cluster": "Please confirm that you want to enable this cluster.", +"message.action.enable.cluster": "Please confirm that you want to enable this Cluster.", "message.action.enable.disk.offering": "Please confirm that you want to enable this disk offering.", "message.action.enable.service.offering": "Please confirm that you want to enable this service offering.", "message.action.enable.system.service.offering": "Please confirm that you want to enable this system service offering.", "message.action.enable.physical.network": "Please confirm that you want to enable this physical Network.", -"message.action.enable.pod": "Please confirm that you want to enable this pod.", -"message.action.enable.zone": "Please confirm that you want to enable this zone.", +"message.action.enable.pod": "Please confirm that you want to enable this Pod.", +"message.action.enable.zone": "Please confirm that you want to enable this Zone.", "message.action.expunge.sharedfs": "Please confirm that you want to expunge this Shared FileSystem.", "message.action.expunge.instance": "Please confirm that you want to expunge this Instance.", "message.action.expunge.instance.with.backups": "Please confirm that you want to expunge this Instance. There may be backups associated with the Instance which will not be deleted.", "message.action.host.enable.maintenance.mode": "Enabling maintenance mode will cause a live migration of all running Instances on this host to any available host.", "message.action.instance.reset.password": "Please confirm that you want to change the ROOT password for this Instance.", -"message.action.manage.cluster": "Please confirm that you want to manage the cluster.", +"message.action.manage.cluster": "Please confirm that you want to manage the Cluster.", "message.action.patch.router": "Please confirm that you want to live patch the router.
This operation is equivalent updating the router packages and restarting the Network without cleanup.", "message.action.patch.systemvm": "Please confirm that you want to patch the System VM.", -"message.action.primary.storage.scope.cluster": "Please confirm that you want to change the scope from zone to the specified cluster.
This operation will update the database and disconnect the storage pool from all hosts that were previously connected to the primary storage and are not part of the specified cluster.", -"message.action.primary.storage.scope.zone": "Please confirm that you want to change the scope from cluster to zone.
This operation will update the database and connect the storage pool to all hosts of the zone running the same hypervisor as set on the storage pool.", +"message.action.primary.storage.scope.cluster": "Please confirm that you want to change the scope from Zone to the specified Cluster.
This operation will update the database and disconnect the storage pool from all hosts that were previously connected to the primary storage and are not part of the specified cluster.", +"message.action.primary.storage.scope.zone": "Please confirm that you want to change the scope from Cluster to Zone.
This operation will update the database and connect the storage pool to all hosts of the zone running the same hypervisor as set on the storage pool.", "message.action.primarystorage.enable.maintenance.mode": "Warning: placing the primary storage into maintenance mode will cause all Instances using volumes from it to be stopped. Do you want to continue?", "message.action.quota.tariff.create.error.namerequired": "Please, inform a name for the quota tariff.", "message.action.quota.tariff.create.error.usagetyperequired": "Please, select the usage type of the quota tariff.", @@ -2805,6 +2919,7 @@ "message.action.reboot.systemvm": "Please confirm that you want to reboot this system VM.", "message.action.recover.sharedfs": "Please confirm that you would like to recover this Shared FileSystem.", "message.action.recover.volume": "Please confirm that you would like to recover this volume.", +"message.action.register.extension": "Register the extension to a resource", "message.action.release.asnumber": "Please confirm that you want to release this AS Number.", "message.action.release.ip": "Please confirm that you want to release this IP.", "message.action.remove.host": "Please confirm that you want to remove this host.", @@ -2816,7 +2931,7 @@ "message.action.revert.snapshot": "Please confirm that you want to revert the owning volume to this Snapshot.", "message.action.router.health.checks": "Health checks result will be fetched from router.", "message.action.router.health.checks.disabled.warning": "Please enable router health checks.", -"message.action.scale.kubernetes.cluster.warning": "Please do not manually scale the cluster if cluster auto scaling is enabled.", +"message.action.scale.kubernetes.cluster.warning": "Please do not manually scale the Cluster if cluster auto scaling is enabled.", "message.action.secondary.storage.read.only": "Please confirm that you want to make this secondary storage read only.", "message.action.secondary.storage.read.write": "Please confirm that you want to make this secondary storage read write.", "message.action.secure.host": "This will restart the host agent and libvirtd process after applying new X509 certificates, please confirm?", @@ -2829,7 +2944,7 @@ "message.action.stop.instance": "Please confirm that you want to stop this Instance.", "message.action.stop.router": "All services provided by this virtual router will be interrupted. Please confirm that you want to stop this router.", "message.action.stop.systemvm": "Please confirm that you want to stop this system VM.", -"message.action.unmanage.cluster": "Please confirm that you want to unmanage the cluster.", +"message.action.unmanage.cluster": "Please confirm that you want to unmanage the Cluster.", "message.action.unmanage.instance": "Please confirm that you want to unmanage the Instance.", "message.action.unmanage.instances": "Please confirm that you want to unmanage the Instances.", "message.action.unmanage.virtualmachine": "Please confirm that you want to unmanage the Instance.", @@ -2837,14 +2952,19 @@ "message.action.unmanage.volumes": "Please confirm that you want to unmanage the Volumes.", "message.action.vmsnapshot.delete": "Please confirm that you want to delete this Instance Snapshot.
Please notice that the Instance will be paused before the Snapshot deletion, and resumed after deletion, if it runs on KVM.", "message.activate.project": "Are you sure you want to activate this project?", +"message.add.custom.action.parameters": "Parameters to be made available while running the custom action.", "message.add.egress.rule.failed": "Adding new egress rule failed.", "message.add.egress.rule.processing": "Adding new egress rule...", "message.add.failed": "Adding failed.", -"message.add.firewall": "Add a firewall to zone", +"message.add.firewall": "Add a firewall to Zone", "message.add.firewall.rule.failed": "Adding new Firewall rule failed", "message.add.firewall.rule.processing": "Adding new Firewall rule...", "message.add.firewallrule.failed": "Adding Firewall Rule failed", "message.add.host": "Please specify the following parameters to add a new host.", +"message.add.extension.custom.action.details": "Details to be sent to the extension during execution of this action.", +"message.add.extension.details": "Details to be sent to the extension on any operation.", +"message.add.extension.resource.details": "Details to be sent to the extension during any operation for this resource.", +"message.add.orchestrator.resource.details": "Details to be sent to the hypervisor.", "message.add.host.sshkey": "WARNING: In order to add a host with SSH key, you must ensure your hypervisor host has been configured correctly.", "message.add.iprange.processing": "Adding IP Range...", "message.add.ipv4.subnet.for.guest.network.failed": "Failed to add IPv4 subnet for guest network", @@ -2857,23 +2977,24 @@ "message.remove.ip.v6.firewall.rule.failed": "Failed to remove IPv6 firewall rule", "message.remove.ip.v6.firewall.rule.processing": "Removing IPv6 firewall rule...", "message.remove.ip.v6.firewall.rule.success": "Removed IPv6 firewall rule", +"message.add.netris.controller": "Add Netris Provider", "message.add.nsx.controller": "Add NSX Provider", -"message.add.network": "Add a new network for zone: ", -"message.add.network.acl.failed": "Adding network ACL list failed.", -"message.add.network.acl.processing": "Adding network ACL list...", +"message.add.network": "Add a new network for Zone: ", +"message.add.network.acl.failed": "Adding network ACL failed.", +"message.add.network.acl.processing": "Adding network ACL...", "message.add.network.failed": "Adding network failed.", "message.add.network.processing": "Adding network...", "message.add.new.gateway.to.vpc": "Please specify the information to add a new gateway to this VPC.", "message.add.physical.network.failed": "Adding physical network failed", "message.add.physical.network.processing": "Adding a new physical network...", -"message.add.pod": "Add a new pod for zone ", -"message.add.pod.during.zone.creation": "Each zone must contain one or more pods. We will add the first pod now. A pod contains hosts and primary storage servers, which you will add in a later step. First, configure a range of reserved IP addresses for CloudStack's internal management traffic. The reserved IP range must be unique for each zone in the cloud.", +"message.add.pod": "Add a new Pod for Zone ", +"message.add.pod.during.zone.creation": "Each Zone must contain one or more Pods. We will add the first pod now. A pod contains hosts and primary storage servers, which you will add in a later step. First, configure a range of reserved IP addresses for CloudStack's internal management traffic. The reserved IP range must be unique for each zone in the cloud.", "message.add.port.forward.failed": "Adding new port forwarding rule failed.", "message.add.port.forward.processing": "Adding new port forwarding rule...", "message.add.private.gateway.failed": "Adding private gateway failed.", "message.add.private.gateway.processing": "Adding private gateway...", "message.add.resource.description": "Add infrastructure resources", -"message.add.resource.hint": "Add infrastructure resources - pods, clusters, primary/secondary storages.", +"message.add.resource.hint": "Add infrastructure resources - Pods, Clusters, primary/secondary storages.", "message.add.routing.firewall.rule.failed": "Failed to add IPv4 Routing firewall rule", "message.add.routing.firewall.rule.processing": "Adding IPv4 Routing firewall rule...", "message.add.routing.firewall.rule.success": "Added IPv4 Routing firewall rule", @@ -2947,6 +3068,7 @@ "message.configuring.guest.traffic": "Configuring guest traffic", "message.configuring.physical.networks": "Configuring physical Networks", "message.configuring.public.traffic": "Configuring public traffic", +"message.configuring.netris.public.traffic": "Configuring Netris public traffic", "message.configuring.storage.access.failed": "Configuring storage access failed", "message.configuring.nsx.public.traffic": "Configuring NSX public traffic", "message.configuring.storage.traffic": "Configuring storage traffic", @@ -2960,11 +3082,13 @@ "message.confirm.change.offering.for.volume": "Please confirm that you want to change disk offering for the volume", "message.confirm.change.service.offering.for.sharedfs": "Please confirm that you want to change the service offering for the Shared FileSystem.", "message.confirm.configure.ovs": "Are you sure you want to configure Ovs?", -"message.confirm.delete.acl.list": "Are you sure you want to delete this ACL list?", +"message.confirm.delete.acl": "Are you sure you want to delete this ACL?", "message.confirm.delete.bigswitchbcf": "Please confirm that you would like to delete this BigSwitch BCF Controller.", "message.confirm.delete.brocadevcs": "Please confirm that you would like to delete Brocade Vcs Switch.", "message.confirm.delete.ciscoasa1000v": "Please confirm you want to delete CiscoASA1000v.", "message.confirm.delete.ciscovnmc.resource": "Please confirm you want to delete CiscoVNMC resource.", +"message.confirm.delete.gpu.devices": "Please confirm that you would like to delete this GPU device?", +"message.confirm.delete.vgpu.profile": "Please confirm that you want to delete this vGPU profile?", "message.confirm.delete.f5": "Please confirm that you would like to delete F5.", "message.confirm.delete.internal.lb": "Please confirm you want to delete internal LB.", "message.confirm.delete.netscaler": "Please confirm that you would like to delete NetScaler.", @@ -2975,19 +3099,25 @@ "message.confirm.delete.traffic.type": "Please confirm that you would like to delete traffic type.", "message.confirm.destroy.router": "All services provided by this virtual router will be interrupted. Please confirm that you want to stop this router. Please confirm that you would like to destroy this router.", "message.confirm.disable.autoscale.vmgroup": "Please confirm that you want to disable this autoscaling group.", +"message.confirm.disable.custom.action": "Please confirm that you want to disable this custom action.", +"message.confirm.disable.extension": "Please confirm that you want to disable this extension.", "message.confirm.disable.host": "Please confirm that you want to disable the host.", "message.confirm.disable.network.offering": "Are you sure you want to disable this Network offering?", "message.confirm.disable.provider": "Please confirm that you would like to disable this provider.", "message.confirm.disable.storage": "Please confirm that you want to disable the storage pool.", "message.confirm.disable.vpc.offering": "Are you sure you want to disable this VPC offering?", "message.confirm.disable.webhook": "Please confirm that you want to disable this webhook.", +"message.confirm.discover.gpu.devices": "Please confirm that you want to discover GPU devices?", "message.confirm.enable.autoscale.vmgroup": "Please confirm that you want to enable this autoscaling group.", +"message.confirm.enable.custom.action": "Please confirm that you want to enable this custom action.", +"message.confirm.enable.extension": "Please confirm that you want to enable this extension.", "message.confirm.enable.host": "Please confirm that you want to enable the host.", "message.confirm.enable.network.offering": "Are you sure you want to enable this Network offering?", "message.confirm.enable.provider": "Please confirm that you would like to enable this provider.", "message.confirm.enable.storage": "Please confirm that you want to enable the storage pool.", "message.confirm.enable.vpc.offering": "Are you sure you want to enable this VPC offering?", "message.confirm.enable.webhook": "Please confirm that you want to enable this webhook.", +"message.confirm.manage.gpu.devices": "Please confirm that you want to manage the selected GPU devices?", "message.confirm.remove.firewall.rule": "Please confirm that you want to delete this Firewall Rule?", "message.confirm.remove.ip.range": "Please confirm that you would like to remove this IP range.", "message.confirm.remove.network.offering": "Are you sure you want to remove this Network offering?", @@ -3005,11 +3135,12 @@ "message.confirm.start.lb.vm": "Please confirm you want to start LB Instance.", "message.confirm.sync.storage": "Please confirm you want to sync the storage pool", "message.confirm.type": "To confirm, please type", +"message.confirm.unmanage.gpu.devices": "Please confirm that you want to unmanage the selected GPU devices?", "message.confirm.upgrade.router.newer.template": "Please confirm that you want to upgrade router to use newer Template.", "message.cpu.usage.info": "The CPU usage percentage can exceed 100% if the Instance has more than 1 vCPU or when CPU Cap is not enabled. This behavior happens according to the hypervisor being used (e.g: in KVM), due to how they account the stats", "message.create.bucket.failed": "Failed to create bucket.", "message.create.bucket.processing": "Bucket creation in progress", -"message.create.compute.offering": "Compute offering created", +"message.create.compute.offering": "Compute Offering created", "message.create.sharedfs.failed": "Failed to create Shared FileSystem.", "message.create.sharedfs.processing": "Shared FileSystem creation in progress.", "message.create.tungsten.public.network": "Create Tungsten-Fabric public Network", @@ -3021,7 +3152,7 @@ "message.create.snapshot.from.vmsnapshot.progress": "Snapshot creation in progress", "message.create.template.failed": "Failed to create template.", "message.create.template.processing": "Template creation in progress", -"message.create.volume.failed": "Failed to create volume.", +"message.create.volume.failed": "Failed to create Volume.", "message.create.volume.processing": "Volume creation in progress", "message.create.vpc.offering": "VPC offering created.", "message.create.vpn.customer.gateway.failed": "VPN customer gateway creation failed.", @@ -3031,23 +3162,23 @@ "message.creating.autoscale.scaledown.policy": "Creating ScaleDown policy", "message.creating.autoscale.scaleup.conditions": "Creating ScaleUp conditions", "message.creating.autoscale.scaleup.policy": "Creating ScaleUp policy", -"message.creating.cluster": "Creating cluster", +"message.creating.cluster": "Creating Cluster", "message.creating.guest.network": "Creating guest Network", "message.creating.physical.networks": "Creating physical Networks", -"message.creating.pod": "Creating pod", +"message.creating.pod": "Creating Pod", "message.creating.primary.storage": "Creating primary storage", "message.creating.secondary.storage": "Creating secondary storage", -"message.creating.zone": "Creating zone", +"message.creating.zone": "Creating Zone", "message.data.migration": "Data migration", "message.data.migration.progress": "Data migration between image stores", "message.datacenter.description": "Name of the datacenter on vCenter.", "message.datastore.description": "Name of the datastore on vCenter.", -"message.dedicate.zone": "Dedicating zone", +"message.dedicate.zone": "Dedicating Zone", "message.dedicated.zone.released": "Zone dedication released.", -"message.dedicating.cluster": "Dedicating cluster...", +"message.dedicating.cluster": "Dedicating Cluster...", "message.dedicating.host": "Dedicating host...", -"message.dedicating.pod": "Dedicating pod...", -"message.dedicating.zone": "Dedicating zone...", +"message.dedicating.pod": "Dedicating Pod...", +"message.dedicating.zone": "Dedicating Zone...", "message.delete.account.confirm": "Please confirm that you want to delete this account by entering the name of the account below.", "message.delete.account.failed": "Delete account failed", "message.delete.account.processing": "Deleting account", @@ -3077,6 +3208,7 @@ "message.delete.vpn.connection": "Please confirm that you want to delete VPN connection.", "message.delete.vpn.customer.gateway": "Please confirm that you want to delete this VPN customer gateway.", "message.delete.vpn.gateway": "Please confirm that you want to delete this VPN Gateway.", +"message.delete.vpn.gateway.failed": "Failed to delete VPN Gateway.", "message.delete.webhook": "Please confirm that you want to delete this Webhook.", "message.delete.webhook.delivery": "Please confirm that you want to delete this Webhook delivery.", "message.deleting.firewall.policy": "Deleting Firewall Policy", @@ -3086,28 +3218,31 @@ "message.deployasis": "Selected Template is Deploy As-Is i.e., the Instance is deployed by importing an OVA with vApps directly into vCenter. Root disk(s) resize is allowed only on stopped Instances for such Templates.", "message.desc.advanced.zone": "This is recommended and allows more sophisticated Network topologies. This Network model provides the most flexibility in defining guest Networks and providing custom Network offerings such as firewall, VPN, or load balancer support.", "message.desc.basic.zone": "Provide a single Network where each Instance is assigned an IP directly from the Network. Guest isolation can be provided through layer-3 means such as security groups (IP address source filtering).", -"message.desc.core.zone": "Core Zones are intended for Datacenter based deployments and allow the full range of Networking and other functionality in Apache CloudStack. Core zones have a number of prerequisites and rely on the presence of shared storage and helper Instances.", -"message.desc.edge.zone": "Edge Zones are lightweight zones, designed for deploying in edge computing scenarios. They are limited in functionality but have far fewer prerequisites than core zones.

Please refer to the Apache CloudStack documentation for more information on Zone Types
http://docs.cloudstack.apache.org/en/latest/installguide/configuration.html#adding-a-zone", -"message.desc.cluster": "Each pod must contain one or more clusters. We will add the first cluster now. A cluster provides a way to group hosts. The hosts in a cluster all have identical hardware, run the same hypervisor, are on the same subnet, and access the same shared storage. Each cluster consists of one or more hosts and one or more primary storage servers.", +"message.desc.core.zone": "Core Zones are intended for Datacenter based deployments and allow the full range of Networking and other functionality in Apache CloudStack. Core Zones have a number of prerequisites and rely on the presence of shared storage and helper Instances.", +"message.desc.edge.zone": "Edge Zones are lightweight Zones, designed for deploying in edge computing scenarios. They are limited in functionality but have far fewer prerequisites than core zones.

Please refer to the Apache CloudStack documentation for more information on Zone Types
http://docs.cloudstack.apache.org/en/latest/installguide/configuration.html#adding-a-zone", +"message.desc.cluster": "Each Pod must contain one or more Clusters. We will add the first cluster now. A cluster provides a way to group hosts. The hosts in a cluster all have identical hardware, run the same hypervisor, are on the same subnet, and access the same shared storage. Each cluster consists of one or more hosts and one or more primary storage servers.", "message.desc.create.ssh.key.pair": "Please fill in the following data to create or register a ssh key pair.

(1) If public key is set, CloudStack will register the public key. You can use it through your private key.

(2) If public key is not set, CloudStack will create a new SSH key pair. In this case, please copy and save the private key. CloudStack will not keep it.
", "message.desc.created.ssh.key.pair": "Created a SSH key pair.", -"message.desc.host": "Each cluster must contain at least one host (computer) for guest Instances to run on. We will add the first host now. For a host to function in CloudStack, you must install hypervisor software on the host, assign an IP address to the host, and ensure the host is connected to the CloudStack management server.

Give the host's DNS or IP address, the user name (usually root) and password, and any labels you use to categorize hosts.", -"message.desc.importingestinstancewizard": "This feature only applies to libvirt based KVM instances. Only Stopped instances can be ingested", +"message.desc.host": "Each Cluster must contain at least one host (computer) for guest Instances to run on. We will add the first host now. For a host to function in CloudStack, you must install hypervisor software on the host, assign an IP address to the host, and ensure the host is connected to the CloudStack management server.

Give the host's DNS or IP address, the user name (usually root) and password, and any labels you use to categorize hosts.", "message.desc.import.ext.kvm.wizard": "Import libvirt domain from External KVM Host not managed by CloudStack", "message.desc.import.local.kvm.wizard": "Import QCOW2 image from Local Storage of selected KVM Host", "message.desc.import.shared.kvm.wizard": "Import QCOW2 image from selected Primary Storage Pool", "message.desc.import.unmanage.volume": "Please choose a storage pool that you want to import or unmanage volumes. The storage pool should be in Up status.
This feature only supports KVM.", "message.desc.importexportinstancewizard": "By choosing to manage an Instance, CloudStack takes over the orchestration of that Instance. Unmanaging an Instance removes CloudStack ability to manage it. In both cases, the Instance is left running and no changes are done to the VM on the hypervisor.

For KVM, managing a VM is an experimental feature.", -"message.desc.importmigratefromvmwarewizard": "By selecting an existing or external VMware Datacenter and an instance to import, CloudStack migrates the selected instance from VMware to KVM on a conversion host using virt-v2v and imports it into a KVM cluster", -"message.desc.primary.storage": "Each cluster must contain one or more primary storage servers. We will add the first one now. Primary storage contains the disk volumes for all the Instances running on hosts in the cluster. Use any standards-compliant protocol that is supported by the underlying hypervisor.", -"message.desc.reset.ssh.key.pair": "Please specify a ssh key pair that you would like to add to this Instance.", -"message.desc.secondary.storage": "Each zone must have at least one NFS or secondary storage server. We will add the first one now. Secondary storage stores Instance Templates, ISO images, and Instance disk volume Snapshots. This server must be available to all hosts in the zone.

Provide the IP address and exported path.", -"message.desc.register.user.data": "Please fill in the following data to register a User data.", +"message.desc.importingestinstancewizard": "This feature only applies to libvirt based KVM instances. Only Stopped instances can be ingested", +"message.desc.importmigratefromvmwarewizard": "By selecting an existing or external VMware Datacenter and an instance to import, CloudStack migrates the selected instance from VMware to KVM on a conversion host using virt-v2v and imports it into a KVM Cluster", +"message.desc.primary.storage": "Each Cluster must contain one or more primary storage servers. We will add the first one now. Primary storage contains the disk volumes for all the Instances running on hosts in the cluster. Use any standards-compliant protocol that is supported by the underlying hypervisor.", +"message.desc.register.template": "Hosted on download.cloudstack.org, these templates can be easily registered directly within CloudStack. Simply click Register Template for the templates you wish to use.", "message.desc.register.cni.config": "Please fill in the following data to register CNI Configuration as user data.", +"message.desc.register.user.data": "Please fill in the following to register new User Data.", "message.desc.registered.user.data": "Registered a User Data.", -"message.desc.zone": "A zone is the largest organizational unit in CloudStack, and it typically corresponds to a single datacenter. Zones provide physical isolation and redundancy. A zone consists of one or more pods (each of which contains hosts and primary storage servers) and a secondary storage server which is shared by all pods in the zone.", -"message.desc.zone.edge": "A zone is the largest organizational unit in CloudStack, and it typically corresponds to a single datacenter. Zones provide physical isolation and redundancy. An edge zone consists of one or more hosts (each of which provides local storage as primary storage servers). Only shared and L2 Networks can be deployed in such zones and functionalities that require secondary storages are not supported.", -"message.drs.plan.description": "The maximum number of live migrations allowed for DRS. Configure DRS under the settings tab before generating a plan or to enable automatic DRS for the cluster.", +"message.desc.reset.ssh.key.pair": "Please specify a ssh key pair that you would like to add to this Instance.", +"message.desc.secondary.storage": "Each Zone must have at least one NFS or secondary storage server. We will add the first one now. Secondary storage stores Instance Templates, ISO images, and Instance disk volume Snapshots. This server must be available to all hosts in the zone.

Provide the IP address and exported path.", +"message.desc.validationformat": "Specifies the format used to validate the parameter value, such as EMAIL, URL, UUID, DECIMAL, etc.", +"message.desc.valueoptions": "Provide a comma-separated list of values that will appear as selectable options for this parameter", +"message.desc.zone": "A Zone is the largest organizational unit in CloudStack, and it typically corresponds to a single datacenter. Zones provide physical isolation and redundancy. A zone consists of one or more Pods (each of which contains hosts and primary storage servers) and a secondary storage server which is shared by all pods in the zone.", +"message.desc.zone.edge": "A Zone is the largest organizational unit in CloudStack, and it typically corresponds to a single datacenter. Zones provide physical isolation and redundancy. An edge zone consists of one or more hosts (each of which provides local storage as primary storage servers). Only shared and L2 Networks can be deployed in such zones and functionalities that require secondary storages are not supported.", +"message.drs.plan.description": "The maximum number of live migrations allowed for DRS. Configure DRS under the settings tab before generating a plan or to enable automatic DRS for the Cluster.", "message.drs.plan.executed": "DRS plan executed successfully.", "message.zone.edge.local.storage": "Local storage will be used by default for User Instances and virtual routers", "message.detach.disk": "Are you sure you want to detach this disk?", @@ -3141,7 +3276,7 @@ "message.enable.vpn": "Please confirm that you want remote access VPN enabled for this IP address.", "message.enable.vpn.failed": "Failed to enable VPN.", "message.enable.vpn.processing": "Enabling VPN...", -"message.enabled.vpn": "Your remote access VPN is currently enabled and can be accessed via the IP.", +"message.enabled.vpn": "Your remote access VPN is currently enabled and can be accessed via the IP", "message.enabled.vpn.ip.sec": "Your IPSec pre-shared key is", "message.enabling.security.group.provider": "Enabling security group provider", "message.enter.valid.nic.ip": "Please enter a valid IP address for NIC", @@ -3165,8 +3300,8 @@ "message.error.cidr": "CIDR is required", "message.error.cidr.or.cidrsize": "CIDR or cidr size is required", "message.error.cloudian.console": "Single-Sign-On failed for Cloudian management console. Please ask your administrator to fix integration issues.", -"message.error.cluster.description": "Please enter Kubernetes cluster description.", -"message.error.cluster.name": "Please enter cluster name.", +"message.error.cluster.description": "Please enter Kubernetes Cluster description.", +"message.error.cluster.name": "Please enter Cluster name.", "message.error.confirm.password": "Please confirm new password.", "message.error.confirm.text": "Please enter the confirmation text", "message.error.current.password": "Please enter current password.", @@ -3195,6 +3330,9 @@ "message.error.host.username": "Please enter host username.", "message.error.hypervisor.type": "Please select hypervisor type.", "message.error.input.value": "Please enter value.", +"message.error.input.invalidemail": "Please enter a valid email.", +"message.error.input.invalidurl": "Please enter a valid URL.", +"message.error.input.invaliduuid": "Please enter a valid UUID.", "message.error.internal.dns1": "Please enter internal DNS 1", "message.error.internallb.instance.port": "Please specify a Instance port.", "message.error.internallb.name": "Please specify a name for the internal LB.", @@ -3207,7 +3345,7 @@ "message.error.ipv6.address": "Please enter a valid IP v6 address.", "message.error.ipv6.gateway": "Please enter IpV6 Gateway", "message.error.ipv6.gateway.format": "Please enter a valid IPv6 Gateway.", -"message.error.kubecluster.name": "Please enter Kubernetes cluster name.", +"message.error.kubecluster.name": "Please enter Kubernetes Cluster name.", "message.error.kuberversion": "Please enter Kubernetes semantic version.", "message.error.limit.value": "The value must not be less than", "message.error.loading.setting": "There was an error loading these settings.", @@ -3241,7 +3379,7 @@ "message.error.remove.vm.schedule": "Removing Instance Schedule failed", "message.error.required.input": "Please enter input", "message.error.reset.config": "Unable to reset config to default value", -"message.error.retrieve.kubeconfig": "Unable to retrieve Kubernetes cluster config", +"message.error.retrieve.kubeconfig": "Unable to retrieve Kubernetes Cluster config", "message.error.routing.policy.term": "Community need to have the following format number:number", "message.error.s3nfs.path": "Please enter S3 NFS Path", "message.error.s3nfs.server": "Please enter S3 NFS Server", @@ -3258,12 +3396,13 @@ "message.error.sbdomain.username": "Please enter SMB domain username.", "message.error.secret.key": "Please enter secret key.", "message.error.select": "Please select option.", +"message.error.select.account.to.dedicate": "Please select an account to dedicate to.", "message.error.select.domain.to.dedicate": "Please select domain to dedicate to.", -"message.error.select.zone.type": "Please select zone type below.", +"message.error.select.zone.type": "Please select Zone type below.", "message.error.server": "Please enter server.", -"message.error.serviceoffering.for.cluster": "Please select service offering for Kubernetes cluster.", +"message.error.serviceoffering.for.cluster": "Please select service offering for Kubernetes Cluster.", "message.error.size": "Please enter size in GB.", -"message.error.size.for.cluster": "Please enter size for Kubernetes cluster.", +"message.error.size.for.cluster": "Please enter size for Kubernetes Cluster.", "message.error.smb.password": "Please enter SMB password.", "message.error.smb.username": "Please enter SMB username.", "message.error.specify.stickiness.method": "Please specify a stickiness method", @@ -3281,7 +3420,7 @@ "message.error.upload.template": "Template upload failed.", "message.error.upload.template.description": "Only one Template can be uploaded at a time.", "message.error.url": "Please enter URL.", -"message.error.userdata": "Please enter Userdata", +"message.error.user.data": "Please enter the User Data", "message.error.username": "Enter your username.", "message.error.valid.iops.range": "Please enter a valid IOPS range.", "message.error.vcenter.datacenter": "Please enter vCenter datacenter.", @@ -3289,16 +3428,16 @@ "message.error.vcenter.host": "Please enter vCenter host.", "message.error.vcenter.password": "Please enter vCenter password.", "message.error.vcenter.username": "Please enter vCenter username.", -"message.error.version.for.cluster": "Please select Kubernetes version for Kubernetes cluster.", +"message.error.version.for.cluster": "Please select Kubernetes version for Kubernetes Cluster.", "message.error.vlan.range": "Please enter a valid VLAN/VNI range.", "message.error.volume.name": "Please enter volume name.", "message.error.volume": "Please enter volume.", "message.error.volume.group": "Please enter volume group.", -"message.error.zone": "Please select a zone.", -"message.error.zone.combined": "All zones cannot be combined with any other zone.", -"message.error.zone.for.cluster": "Please select zone for Kubernetes cluster.", -"message.error.zone.name": "Please enter zone name.", -"message.error.zone.type": "Please select zone type.", +"message.error.zone": "Please select a Zone.", +"message.error.zone.combined": "All Zones cannot be combined with any other zone.", +"message.error.zone.for.cluster": "Please select Zone for Kubernetes Cluster.", +"message.error.zone.name": "Please enter Zone name.", +"message.error.zone.type": "Please select Zone type.", "message.error.linstor.resourcegroup": "Please enter the Linstor Resource-Group.", "message.error.fixed.offering.kvm": "It's not possible to scale up Instances that utilize KVM hypervisor with a fixed compute offering.", "message.error.create.webhook.local.account": "Account must be provided for creating a Webhook with Local scope.", @@ -3318,12 +3457,14 @@ "message.host.controlstate.retry": "Some actions on this Instance will fail, if so please wait a while and retry.", "message.host.dedicated": "Host Dedicated", "message.host.dedication.released": "Host dedication released.", +"message.host.external.datadisk": "Usage of data disks for the selected template is not applicable", "message.import.running.instance.warning": "The selected VM is powered-on on the VMware Datacenter. The recommended state to convert a VMware VM into KVM is powered-off after a graceful shutdown of the guest OS.", "message.import.volume": "Please specify the domain, account or project name.
If not set, the volume will be imported for the caller.", "message.info.cloudian.console": "Cloudian Management Console should open in another window.", "message.installwizard.cloudstack.helptext.website": " * Project website:\t ", -"message.infra.setup.nsx.description": "This zone must contain an NSX provider because the isolation method is NSX", -"message.infra.setup.tungsten.description": "This zone must contain a Tungsten-Fabric provider because the isolation method is TF", +"message.infra.setup.netris.description": "This zone must contain a Netris provider because the isolation method is Netris", +"message.infra.setup.nsx.description": "This Zone must contain an NSX provider because the isolation method is NSX", +"message.infra.setup.tungsten.description": "This Zone must contain a Tungsten-Fabric provider because the isolation method is TF", "message.installwizard.cloudstack.helptext.document": " * Documentation:\t ", "message.installwizard.cloudstack.helptext.header": "\nYou can find more information about Apache CloudStack™ on the pages listed below.\n", "message.installwizard.cloudstack.helptext.issues": " * Report issues:\t ", @@ -3331,20 +3472,27 @@ "message.installwizard.cloudstack.helptext.releasenotes": " * Release notes:\t ", "message.installwizard.cloudstack.helptext.survey": " * Take the survey:\t ", "message.installwizard.copy.whatiscloudstack": "CloudStack™ is a software platform that pools computing resources to build public, private, and hybrid Infrastructure as a Service (IaaS) clouds. CloudStack™ manages the Network, storage, and compute nodes that make up a cloud infrastructure. Use CloudStack™ to deploy, manage, and configure cloud computing environments.\n\nExtending beyond individual Instance images running on commodity hardware, CloudStack™ provides a turnkey cloud infrastructure software stack for delivering virtual datacenters as a service - delivering all of the essential components to build, deploy, and manage multi-tier and multi-tenant cloud applications.", -"message.installwizard.tooltip.addpod.name": "A name for the pod.", +"message.installwizard.tooltip.addpod.name": "A name for the Pod.", "message.installwizard.tooltip.addpod.reservedsystemendip": "This is the IP range in the private Network that the CloudStack uses to manage Secondary Storage VMs and Console Proxy VMs. These IP addresses are taken from the same subnet as computing servers.", -"message.installwizard.tooltip.addpod.reservedsystemgateway": "The gateway for the hosts in that pod.", +"message.installwizard.tooltip.addpod.reservedsystemgateway": "The gateway for the hosts in that Pod.", "message.installwizard.tooltip.addpod.reservedsystemstartip": "This is the IP range in the private Network that the CloudStack uses to manage Secondary Storage VMs and Console Proxy VMs. These IP addresses are taken from the same subnet as computing servers.", -"message.installwizard.tooltip.configureguesttraffic.guestendip": "The range of IP addresses that will be available for allocation to guests in this zone. If one NIC is used, these IPs should be in the same CIDR as the pod CIDR.", +"message.installwizard.tooltip.configureguesttraffic.guestendip": "The range of IP addresses that will be available for allocation to guests in this Zone. If one NIC is used, these IPs should be in the same CIDR as the Pod CIDR.", "message.installwizard.tooltip.configureguesttraffic.guestgateway": "The gateway that the guests should use.", "message.installwizard.tooltip.configureguesttraffic.guestnetmask": "The netmask in use on the subnet that the guests should use.", -"message.installwizard.tooltip.configureguesttraffic.gueststartip": "The range of IP addresses that will be available for allocation to guests in this zone. If one NIC is used, these IPs should be in the same CIDR as the pod CIDR.", +"message.installwizard.tooltip.configureguesttraffic.gueststartip": "The range of IP addresses that will be available for allocation to guests in this Zone. If one NIC is used, these IPs should be in the same CIDR as the Pod CIDR.", +"message.installwizard.tooltip.netris.provider.name": "Netris Provider name is required", +"message.installwizard.tooltip.netris.provider.url": "Netris Provider URL not provided", +"message.installwizard.tooltip.netris.provider.username": "Netris Provider username not provided", +"message.installwizard.tooltip.netris.provider.password": "Netris Provider password not provided", +"message.installwizard.tooltip.netris.provider.site": "Netris Provider Site name not provided", +"message.installwizard.tooltip.netris.provider.tag": "Netris Tag to be assigned to vNets", +"message.installwizard.tooltip.netris.provider.tenant.name": "Netris Provider Admin Tenant name not provided", "message.installwizard.tooltip.nsx.provider.hostname": "NSX Provider hostname / IP address not provided", "message.installwizard.tooltip.nsx.provider.username": "NSX Provider username not provided", "message.installwizard.tooltip.nsx.provider.password": "NSX Provider password not provided", -"message.installwizard.tooltip.nsx.provider.edgecluster": "NSX Provider edge cluster information not provided", +"message.installwizard.tooltip.nsx.provider.edgecluster": "NSX Provider edge Cluster information not provided", "message.installwizard.tooltip.nsx.provider.tier0gateway": "NSX Provider tier-0 gateway information not provided", -"message.installwizard.tooltip.nsx.provider.transportZone": "NSX Provider transport zone information not provided", +"message.installwizard.tooltip.nsx.provider.transportZone": "NSX Provider transport Zone information not provided", "message.installwizard.tooltip.tungsten.provider.gateway": "Tungsten provider gateway is required", "message.installwizard.tooltip.tungsten.provider.hostname": "Tungsten provider hostname is required", "message.installwizard.tooltip.tungsten.provider.introspectport": "Tungsten provider introspect port is required", @@ -3361,16 +3509,16 @@ "message.ip.v6.prefix.delete": "IPv6 prefix deleted", "message.iso.arch": "Please select an ISO architecture", "message.iso.desc": "Disc image containing data or bootable media for OS.", -"message.kubeconfig.cluster.not.available": "Kubernetes cluster kubeconfig not available currently.", "message.kubernetes.cluster.add.nodes": "Please confirm that you want to add the following nodes to the cluster", -"message.kubernetes.cluster.delete": "Please confirm that you want to destroy the cluster.", -"message.kubernetes.cluster.scale": "Please select desired cluster configuration.", -"message.kubernetes.cluster.start": "Please confirm that you want to start the cluster.", -"message.kubernetes.cluster.stop": "Please confirm that you want to stop the cluster.", +"message.kubernetes.cluster.delete": "Please confirm that you want to destroy the Cluster.", +"message.kubeconfig.cluster.not.available": "Kubernetes Cluster kubeconfig not available currently.", "message.kubernetes.cluster.remove.nodes": "Please confirm that you want to remove the following nodes from the cluster", +"message.kubernetes.cluster.scale": "Please select desired Cluster configuration.", +"message.kubernetes.cluster.start": "Please confirm that you want to start the Cluster.", +"message.kubernetes.cluster.stop": "Please confirm that you want to stop the Cluster.", "message.kubernetes.cluster.upgrade": "Please select new Kubernetes version.", "message.kubernetes.version.delete": "Please confirm that you want to delete this Kubernetes version.", -"message.l2.network.unsupported.for.nsx": "L2 networks aren't supported for NSX enabled zones", +"message.l2.network.unsupported.for.nsx": "L2 networks aren't supported for NSX enabled Zones", "message.launch.zone": "Zone is ready to launch; please proceed to the next step.", "message.launch.zone.description": "Zone is ready to launch; please proceed to the next step.", "message.launch.zone.hint": "Configure Network components and traffic including IP addresses.", @@ -3395,8 +3543,9 @@ "message.lock.user": "Please confirm that you want to lock the User \"{user}\". By locking this User, they will no longer be able to manage their cloud resources. Existing resources can still be accessed.", "message.lock.user.success": "Successfully locked User \"{user}\"", "message.login.failed": "Login Failed", -"message.migrate.instance.host.auto.assign": "Host for the Instance will be automatically chosen based on the suitability within the same cluster", -"message.migrate.instance.to.host": "Please confirm that you want to migrate this Instance to another host. When migration is between hosts of different clusters volume(s) of the Instance may get migrated to suitable storage pools.", +"message.maintenance.initiated": "Maintenance has been initiated. This Management Server will not accept new jobs", +"message.migrate.instance.host.auto.assign": "Host for the Instance will be automatically chosen based on the suitability within the same Cluster", +"message.migrate.instance.to.host": "Please confirm that you want to migrate this Instance to another host. When migration is between hosts of different Clusters volume(s) of the Instance may get migrated to suitable storage pools.", "message.migrate.instance.to.ps": "Please confirm that you want to migrate this Instance to another primary storage.", "message.migrate.resource.to.ss": "Please confirm that you want to migrate this resource to another secondary storage.", "message.migrate.router.confirm": "Please confirm the host you wish to migrate the router to:", @@ -3447,11 +3596,11 @@ "message.path.description": "NFS: exported path from the server. VMFS: /datacenter name/datastore name. SharedMountPoint: path where primary storage is mounted, such as /mnt/primary.", "message.please.confirm.remove.cni.configuration": "Please confirm that you want to remove this CNI Configuration", "message.please.confirm.remove.ssh.key.pair": "Please confirm that you want to remove this SSH key pair.", -"message.please.confirm.remove.user.data": "Please confirm that you want to remove this Userdata", +"message.please.confirm.remove.user.data": "Please confirm that you want to remove this User Data", "message.please.enter.valid.value": "Please enter a valid value.", "message.please.enter.value": "Please enter values.", "message.please.wait.while.autoscale.vmgroup.is.being.created": "Please wait while your AutoScaling Group is being created; this may take a while...", -"message.please.wait.while.zone.is.being.created": "Please wait while your zone is being created; this may take a while...", +"message.please.wait.while.zone.is.being.created": "Please wait while your Zone is being created; this may take a while...", "message.pod.dedicated": "Pod dedicated.", "message.pod.dedication.released": "Pod dedication released.", "message.prepare.for.shutdown": "Please confirm that you would like to prepare this Management Server for shutdown. It will not accept any new Async Jobs but will NOT terminate after there are no pending jobs.", @@ -3468,10 +3617,10 @@ "message.recover.vm": "Please confirm that you would like to recover this Instance.", "message.reinstall.vm": "NOTE: Proceed with caution. This will cause the Instance to be reinstalled from the Template; data on the root disk will be lost. Extra data volumes, if any, will not be touched.", "message.release.ip.failed": "Failed to release IP", -"message.releasing.dedicated.cluster": "Releasing dedicated cluster...", +"message.releasing.dedicated.cluster": "Releasing dedicated Cluster...", "message.releasing.dedicated.host": "Releasing dedicated host...", -"message.releasing.dedicated.pod": "Releasing dedicated pod...", -"message.releasing.dedicated.zone": "Releasing dedicated zone...", +"message.releasing.dedicated.pod": "Releasing dedicated Pod...", +"message.releasing.dedicated.zone": "Releasing dedicated Zone...", "message.remove.annotation": "Are you sure you want to delete the comment?", "message.remove.egress.rule.failed": "Removing egress rule failed", "message.remove.egress.rule.processing": "Deleting egress rule...", @@ -3508,6 +3657,7 @@ "message.restart.vm.to.update.settings": "Update in fields other than name and display name will require the Instance to be restarted.", "message.restart.vpc": "Please confirm that you want to restart the VPC.", "message.restart.vpc.remark": "Please confirm that you want to restart the VPC

Remark: making a non-redundant VPC redundant will force a clean up. The Networks will not be available for a couple of minutes.

", +"message.running.custom.action": "Running action", "message.scale.processing": "Scale in progress", "message.scaledown.policies": "Please add at least a ScaleDown policy. The AutoScale Group will be scaled down when all conditions in a ScaleDown policy are matched. ScaleDown policies will be checked after ScaleUp policies.", "message.scaledown.policy.continue": "Please add at least condition to ScaleDown policy to continue", @@ -3517,7 +3667,7 @@ "message.scaleup.policy.continue": "Please add at least a condition to ScaleUp policy to continue", "message.scaleup.policy.duration.continue": "Please input a valid duration to ScaleUp policy to continue", "message.scaleup.policy.name.continue": "Please input a name to ScaleUp policy to continue", -"message.select.a.zone": "A zone typically corresponds to a single datacenter. Multiple zones help make the cloud more reliable by providing physical isolation and redundancy.", +"message.select.a.zone": "A Zone typically corresponds to a single datacenter. Multiple zones help make the cloud more reliable by providing physical isolation and redundancy.", "message.select.affinity.groups": "Please select any affinity groups you want this Instance to belong to:", "message.select.bgp.peers": "Please select / deselect the BGP peers associated to the network or VPC:", "message.select.deselect.desired.options": "Please select / deselect the desired options", @@ -3525,29 +3675,28 @@ "message.select.destination.image.stores": "Please select Image Store(s) to which data is to be migrated to", "message.select.disk.offering": "Please select a disk offering for disk", "message.select.end.date.and.time": "Select an end date & time.", -"message.select.kvm.host.instance.conversion": "(Optional) Select a KVM host in the zone to perform the instance conversion through virt-v2v", -"message.select.kvm.host.instance.import": "(Optional) Select a KVM host in the cluster to perform the importing of the converted instance", +"message.select.kvm.host.instance.conversion": "(Optional) Select a KVM host in the Zone to perform the instance conversion through virt-v2v", +"message.select.kvm.host.instance.import": "(Optional) Select a KVM host in the Cluster to perform the importing of the converted instance", "message.select.load.balancer.rule": "Please select a load balancer rule for your AutoScale Instance group.", "message.select.migration.policy": "Please select a migration policy.", "message.select.nic.network": "Please select a Network for NIC", "message.select.security.groups": "Please select security group(s) for your new Instance.", "message.select.start.date.and.time": "Select a start date & time.", "message.select.temporary.storage.instance.conversion": "(Optional) Select a Storage temporary destination for the converted disks through virt-v2v", -"message.select.zone.description": "Select type of zone basic/advanced.", -"message.select.zone.hint": "This is the type of zone deployment that you want to use. Basic zone: provides a single Network where each Instance is assigned an IP directly from the Network. Guest isolation can be provided through layer-3 means such as security groups (IP address source filtering). Advanced zone: For more sophisticated Network topologies. This Network model provides the most flexibility in defining guest Networks and providing custom Network offerings such as firewall, VPN, or load balancer support.", +"message.select.zone.description": "Select type of Zone basic/advanced.", +"message.select.zone.hint": "This is the type of Zone deployment that you want to use. Basic zone: provides a single Network where each Instance is assigned an IP directly from the Network. Guest isolation can be provided through layer-3 means such as security groups (IP address source filtering). Advanced zone: For more sophisticated Network topologies. This Network model provides the most flexibility in defining guest Networks and providing custom Network offerings such as firewall, VPN, or load balancer support.", "message.server": "Server : ", "message.server.description": "NFS, iSCSI, or PreSetup: IP address or DNS name of the storage device. VMWare PreSetup: IP address or DNS name of the vCenter server. Linstor: http(s) url of the linstor-controller.", "message.set.default.nic": "Please confirm that you would like to make this NIC the default for this Instance.", "message.set.default.nic.manual": "Please manually update the default NIC on the Instance now.", "message.setting.updated": "Setting Updated:", "message.setting.update.delay": "The new value will take effect within 30 seconds.", -"message.setup.physical.network.during.zone.creation": "When adding a zone, you need to set up one or more physical networks. Each physical network can carry one or more types of traffic, with certain restrictions on how they may be combined. Add or remove one or more traffic types onto each physical network.", -"message.setup.physical.network.during.zone.creation.basic": "When adding a basic zone, you can set up one physical Network, which corresponds to a NIC on the hypervisor. The Network carries several types of traffic.

You may also add other traffic types onto the physical Network.", +"message.setup.physical.network.during.zone.creation": "When adding a Zone, you need to set up one or more physical networks. Each physical network can carry one or more types of traffic, with certain restrictions on how they may be combined. Add or remove one or more traffic types onto each physical network.", +"message.setup.physical.network.during.zone.creation.basic": "When adding a basic Zone, you can set up one physical Network, which corresponds to a NIC on the hypervisor. The Network carries several types of traffic.

You may also add other traffic types onto the physical Network.", "message.shared.network.offering.warning": "Domain admins and regular Users can only create shared Networks from Network offering with the setting specifyvlan=false. Please contact an administrator to create a Network offering if this list is empty.", -"message.shared.network.unsupported.for.nsx": "Shared networks aren't supported for NSX enabled zones", -"message.shutdown.triggered": "Shutdown has been triggered. This Management Server will not accept new jobs", -"message.maintenance.initiated": "Maintenance has been initiated. This Management Server will not accept new jobs", -"message.snapshot.additional.zones": "Snapshots will always be created in its native zone - %x, here you can select additional zone(s) where it will be copied to at creation time", +"message.shared.network.unsupported.for.nsx": "Shared networks aren't supported for NSX enabled Zones", +"message.shutdown.triggered": "A shutdown has been triggered. CloudStack will not accept new jobs", +"message.snapshot.additional.zones": "Snapshots will always be created in its native Zone - %x, here you can select additional zone(s) where it will be copied to at creation time", "message.sourcenatip.change.warning": "WARNING: Changing the sourcenat IP address of the network will cause connectivity downtime for the Instances with NICs in the Network.", "message.sourcenatip.change.inhibited": "Changing the sourcenat to this IP of the Network to this address is inhibited as firewall rules are defined for it. This can include port forwarding or load balancing rules.\n - If this is an Isolated Network, please use updateNetwork/click the edit button.\n - If this is a VPC, first clear all other rules for this address.", "message.specify.tag.key": "Please specify a tag key.", @@ -3561,6 +3710,7 @@ "message.success.add.egress.rule": "Successfully added new egress rule", "message.success.add.firewall.rule": "Successfully added new firewall rule", "message.success.add.guest.network": "Successfully created guest Network", +"message.success.add.gpu.device": "Successfully added GPU device", "message.success.add.interface.static.route": "Successfully added interface Static Route", "message.success.add.iprange": "Successfully added IP range", "message.success.add.ipv4.subnet": "Successfully added IPv4 subnet", @@ -3569,7 +3719,7 @@ "message.success.add.kuberversion": "Successfully added Kubernetes version", "message.success.add.logical.router": "Successfully added Logical Router", "message.success.add.network": "Successfully added Network", -"message.success.add.network.acl": "Successfully added Network ACL list", +"message.success.add.network.acl": "Successfully added Network ACL", "message.success.add.network.static.route": "Successfully added Network Static Route", "message.success.add.network.permissions": "Successfully added Network permissions", "message.success.add.nodes.to.cluster": "Successfully added nodes to Kubernetes cluster", @@ -3611,11 +3761,12 @@ "message.success.create.account": "Successfully created Account", "message.success.create.asnrange": "Successfully created AS Range", "message.success.create.bucket": "Successfully created bucket", +"message.success.create.extension": "Successfully created Extension", "message.success.create.sharedfs": "Successfully created Shared FileSystem", "message.success.create.internallb": "Successfully created Internal Load Balancer", "message.success.create.isolated.network": "Successfully created isolated Network", "message.success.create.keypair": "Successfully created SSH key pair", -"message.success.create.kubernetes.cluter": "Successfully created Kubernetes cluster", +"message.success.create.kubernetes.cluter": "Successfully created Kubernetes Cluster", "message.success.create.l2.network": "Successfully created L2 Network", "message.success.create.snapshot.from.vmsnapshot": "Successfully created Snapshot from Instance Snapshot", "message.success.create.template": "Successfully created Template", @@ -3629,6 +3780,8 @@ "message.success.delete.acl.rule": "Successfully removed ACL rule", "message.success.delete.backup.schedule": "Successfully deleted configure Instance backup schedule", "message.success.delete.bgp.peer": "Successfully deleted BGP peer", +"message.success.delete.custom.action": "Successfully deleted Custom Action", +"message.success.delete.gpu.devices": "Successfully deleted GPU device(s)", "message.success.delete.icon": "Successfully deleted icon of", "message.success.delete.interface.static.route": "Successfully removed interface Static Route", "message.success.delete.ipv4.subnet": "Successfully removed IPv4 subnet", @@ -3641,23 +3794,27 @@ "message.success.delete.tungsten.router.table": "Successfully removed Router Table", "message.success.delete.tungsten.tag": "Successfully removed Tag", "message.success.delete.vm": "Successfully deleted Instance", +"message.success.delete.vpn.gateway": "Successfully deleted VPN gateway", "message.success.disable.saml.auth": "Successfully disabled SAML authorization", "message.success.disable.vpn": "Successfully disabled VPN", +"message.success.discover.gpu.devices": "Successfully discovered GPU devices", "message.success.edit.acl": "Successfully edited ACL rule", "message.success.edit.primary.storage": "Successfully edited Primary Storage", "message.success.edit.rule": "Successfully edited rule", "message.success.enable.saml.auth": "Successfully enabled SAML Authorization", "message.success.import.instance": "Successfully imported Instance", "message.success.import.volume": "Successfully imported Volume", +"message.success.manage.gpu.devices": "Successfully managed GPU device(s)", "message.success.migrate.volume": "Successfully migrated volume", "message.success.migrating": "Migration completed successfully for", "message.success.migration": "Migration completed successfully", "message.success.move.acl.order": "Successfully moved ACL rule", "message.success.recurring.snapshot": "Successfully recurring Snapshots", +"message.success.register.extension": "Successfully registered Extension", "message.success.register.iso": "Successfully registered ISO", "message.success.register.keypair": "Successfully registered SSH key pair", "message.success.register.template": "Successfully registered Template", -"message.success.register.user.data": "Successfully registered Userdata", +"message.success.register.user.data": "Successfully registered User Data", "message.success.release.ip": "Successfully released IP", "message.success.release.dedicated.bgp.peer": "Successfully released dedicated BGP peer", "message.success.release.dedicated.ipv4.subnet": "Successfully released dedicated IPv4 subnet", @@ -3680,22 +3837,31 @@ "message.success.remove.tungsten.routing.policy": "Successfully removed Tungsten-Fabric Routing Policy from Network", "message.success.reset.network.permissions": "Successfully reset Network Permissions", "message.success.resize.volume": "Successfully resized volume", -"message.success.scale.kubernetes": "Successfully scaled Kubernetes cluster", +"message.success.scale.kubernetes": "Successfully scaled Kubernetes Cluster", +"message.success.unmanage.gpu.devices": "Successfully unmanaged GPU device(s)", "message.success.unmanage.instance": "Successfully unmanaged Instance", "message.success.unmanage.volume": "Successfully unmanaged Volume", +"message.success.unregister.extension": "Successfull unregistered Extension", "message.success.update.account": "Successfully updated Account", "message.success.update.bgp.peer": "Successfully updated BGP peer", "message.success.update.bucket": "Successfully updated bucket", "message.success.update.condition": "Successfully updated condition", +"message.success.update.gpu.device": "Successfully updated GPU device", +"message.success.create.vgpu.profile": "Successfully created vGPU profile", +"message.success.update.vgpu.profile": "Successfully updated vGPU profile", +"message.success.delete.vgpu.profile": "Successfully deleted vGPU profile", +"message.success.update.custom.action": "Successfully updated Custom Action", +"message.success.update.extension": "Successfully updated Extension", "message.success.update.sharedfs": "Successfully updated Shared FileSystem", "message.success.update.ipaddress": "Successfully updated IP address", "message.success.update.iprange": "Successfully updated IP range", "message.success.update.ipv4.subnet": "Successfully updated IPv4 subnet", +"message.success.update.iso": "Successfully updated ISO", "message.success.update.kubeversion": "Successfully updated Kubernetes supported version", "message.success.update.network": "Successfully updated Network", "message.success.update.template": "Successfully updated Template", "message.success.update.user": "Successfully updated User", -"message.success.upgrade.kubernetes": "Successfully upgraded Kubernetes cluster", +"message.success.upgrade.kubernetes": "Successfully upgraded Kubernetes Cluster", "message.success.upload": "Successfully uploaded", "message.success.upload.description": "This ISO file has been uploaded. Please check its status in the Templates menu.", "message.success.upload.icon": "Successfully uploaded icon for ", @@ -3710,10 +3876,10 @@ "message.template.import.vm.temporary": "If a temporary Template is used, the reset Instance operation will not work after importing it.", "message.template.iso": "Please select a Template or ISO to continue.", "message.template.type.change.warning": "WARNING: Changing the Template type to SYSTEM will disable further changes to the Template.", -"message.tooltip.reserved.system.netmask": "The Network prefix that defines the pod subnet. Uses CIDR notation.", +"message.tooltip.reserved.system.netmask": "The Network prefix that defines the Pod subnet. Uses CIDR notation.", "message.traffic.type.deleted": "Successfully deleted traffic type", -"message.traffic.type.to.basic.zone": "traffic type to basic zone", -"message.trigger.shutdown": "Please confirm that you would like to trigger a shutdown on this Management Server. It will not accept any new Async Jobs and will terminate after there are no pending jobs.", +"message.traffic.type.to.basic.zone": "traffic type to basic Zone", +"message.trigger.shutdown": "Please confirm that you would like to trigger a shutdown on this Management server. It will not accept any new Async Jobs and will terminate after there are no pending jobs.", "message.type.values.to.add": "Please add additional values by typing them in", "message.update.autoscale.policy.failed": "Failed to update autoscale policy", "message.update.autoscale.vmgroup.failed": "Failed to update autoscale group", @@ -3810,13 +3976,15 @@ "message.warn.change.primary.storage.scope": "This feature is tested and supported for the following configurations:
KVM - NFS/Ceph - DefaultPrimary
VMware - NFS - DefaultPrimary
*There might be extra steps involved to make it work for other configurations.", "message.warn.filetype": "jpg, jpeg, png, bmp and svg are the only supported image formats.", "message.warn.importing.instance.without.nic": "WARNING: This Instance is being imported without NICs and many Network resources will not be available. Consider creating a NIC via vCenter before importing or as soon as the Instance is imported.", -"message.warn.zone.mtu.update": "Please note that this limit won't affect pre-existing Network’s MTU settings", +"message.warn.select.template": "Please select a Template for Registration.", +"message.warn.zone.mtu.update": "Please note that this limit won't affect pre-existing Network's MTU settings", "message.webhook.deliveries.time.filter": "Webhook deliveries list can be filtered based on date-time. Select 'Custom' for specifying start and end date range.", "message.zone.creation.complete": "Zone creation complete.", -"message.zone.detail.description": "Populate zone details.", -"message.zone.detail.hint": "A zone is the largest organizational unit in CloudStack, and it typically corresponds to a single datacenter. Zones provide physical isolation and redundancy. A zone consists of one or more pods (each of which contains hosts and primary storage servers) and a secondary storage server which is shared by all pods in the zone.", +"message.zone.detail.description": "Populate Zone details.", +"message.zone.detail.hint": "A Zone is the largest organizational unit in CloudStack, and it typically corresponds to a single datacenter. Zones provide physical isolation and redundancy. A zone consists of one or more Pods (each of which contains hosts and primary storage servers) and a secondary storage server which is shared by all pods in the zone.", "message.validate.min": "Please enter a value greater than or equal to {0}.", "message.action.delete.object.storage": "Please confirm that you want to delete this Object Store", +"message.action.unregister.extension.resource": "Please confirm that you want to unregister extension with this resource", "message.bgp.peers.null": "Please note, if no BGP peers are selected, the VR will connect to
(1) dedicated BGP peers the owner can access, if the owner has dedicated BGP peers and account setting use.system.bgp.peers is set to false;
(2) all BGP peers the owner can access, otherwise.
", "message.bucket.delete": "Please confirm that you want to delete this Bucket", "migrate.from": "Migrate from", @@ -3850,6 +4018,7 @@ "state.stopped": "Stopped", "state.stopping": "Stopping", "state.suspended": "Suspended", +"state.partiallyallocated": "Partially Allocated", "user.login": "Login", "user.logout": "Logout", "ALLOCATED_VM": "Allocated VM", diff --git a/ui/public/locales/es.json b/ui/public/locales/es.json index ae4fa19f033..ab57c951531 100644 --- a/ui/public/locales/es.json +++ b/ui/public/locales/es.json @@ -14,12 +14,12 @@ "label.account.specific": "espec\u00edficas de la cuenta", "label.accounts": "Cuentas", "label.accounttype": "Tipo de Cuenta", -"label.acl.export": "Export ACLs", +"label.acl.export": "Export ACL rules", "label.acl.id": "ID de ACL", -"label.acl.list.rules": "Lista de Reglas ACL", +"label.acl.rules": "Lista de Reglas ACL", "label.acl.reason.description": "Enter the reason behind an ACL rule.", "label.aclid": "ACL", -"label.aclname": "Nombre de ACL", +"label.acl.rule.name": "Nombre de ACL", "label.acquire.new.ip": "Adquirir nueva IP", "label.acquire.new.secondary.ip": "Adquirir nueva IP secundaria", "label.action": "Acci\u00f3n", @@ -119,8 +119,8 @@ "label.activeviewersessions": "Sesiones activas", "label.add": "Agregar", "label.add.account": "A\u00f1adir Cuenta", -"label.add.acl": "Agregar ACL", -"label.add.acl.list": "Agregar Lista ACL", +"label.add.acl.rule": "Agregar ACL", +"label.add.acl": "Agregar Lista ACL", "label.add.affinity.group": "Agregar un nuevo grupo de afinidad", "label.add.baremetal.dhcp.device": "Agregar dispositivo DHCP Baremetal", "label.add.bigswitchbcf.device": "Agregar Controlador BigSwitch BCF", @@ -142,12 +142,12 @@ "label.add.ip.range": "A\u00f1adir Rango IP", "label.add.isolated.network": "Agregar Red Aislada", "label.add.ldap.account": "Agregar cuenta LDAP", -"label.add.list.name": "Nombre de la Lista ACL", +"label.add.acl.name": "Nombre de la Lista ACL", "label.add.more": "A\u00f1adir m\u00e1s", "label.add.netscaler.device": "Agregar dispositivo Netscaler", "label.add.network": "Agregar Red", "label.add.network.acl": "Agregar ACL de Red", -"label.add.network.acl.list": "Agregar Lista ACL de Red", +"label.add.network.acl": "Agregar Lista ACL de Red", "label.add.network.offering": "Agregar Oferta de Red", "label.add.new.gateway": "Agregar nuevo gateway", "label.add.new.tier": "Agregar un nuevo tier", @@ -344,7 +344,7 @@ "label.default.use": "Uso por defecto", "label.default.view": "Vista Por Defecto", "label.delete": "Eliminar", -"label.delete.acl.list": "Borrar Lista ACL", +"label.delete.acl": "Borrar Lista ACL", "label.delete.affinity.group": "Borrar Grupo de Afinidad", "label.delete.alerts": "Eliminar alertas", "label.delete.bigswitchbcf": "Remover Controlador BigSwitch BCF", @@ -431,7 +431,7 @@ "label.driver": "Controlador", "label.dynamicscalingenabled": "Escalado din\u00e1mico habilitado", "label.edit": "Editar", -"label.edit.acl.list": "Edit ACL List", +"label.edit.acl": "Edit ACL", "label.edit.acl.rule": "Editar regla ACL", "label.edit.project.details": "Editar detalles de proyecto", "label.edit.role": "Editar Rol", @@ -947,7 +947,6 @@ "label.remove.vpc.offering": "Quitar Oferta VPC", "label.removing": "Quitando..", "label.replace.acl": "Reemplazar ACL", -"label.replace.acl.list": "Reemplazar Lista ACL", "label.report.bug": "Reportar un Error", "label.required": "Requerido", "label.requireshvm": "HVM", @@ -1180,8 +1179,7 @@ "label.usehttps": "Use HTTPS", "label.usenewdiskoffering": "Replace disk offering?", "label.user": "Usuario", -"label.userdata": "DatosUsuario", -"label.userdatal2": "Datos de Usuario", +"label.user.data": "DatosUsuario", "label.username": "Nombre de usuario", "label.users": "Usuarios", "label.utilization": "Utilisation", @@ -1361,7 +1359,7 @@ "message.confirm.archive.selected.alerts": "Por favor confirme que desea archivar las alertas seleccionadas", "message.confirm.archive.selected.events": "Por favor confirme que desea archivar los eventos seleccionados", "message.confirm.attach.disk": "\u00bf Est\u00e1 seguro que desea conectar el disco?", -"message.confirm.delete.acl.list": "\u00bfEsta seguro que desea borrar esta lista de ACL?", +"message.confirm.delete.acl": "\u00bfEsta seguro que desea borrar esta lista de ACL?", "message.confirm.delete.bigswitchbcf": "Por favor confirme que desa borrar este Controlador BigSwitch BCF", "message.confirm.delete.brocadevcs": "Por favor confirme que desa borrar este Switch Brocade Vcs", "message.confirm.delete.ciscoasa1000v": "Por favor confirme que desea borrar CiscoASA1000v", diff --git a/ui/public/locales/fr_FR.json b/ui/public/locales/fr_FR.json index 48a0560f176..424f4749aa3 100644 --- a/ui/public/locales/fr_FR.json +++ b/ui/public/locales/fr_FR.json @@ -14,12 +14,12 @@ "label.account.specific": "Sp\u00e9cifique au compte", "label.accounts": "Comptes", "label.accounttype": "Type Compte", -"label.acl.export": "Export ACLs", +"label.acl.export": "Export ACL rules", "label.acl.id": "ID ACL", -"label.acl.list.rules": "Liste r\u00e8gles ACL", +"label.acl.rules": "Liste r\u00e8gles ACL", "label.acl.reason.description": "Enter the reason behind an ACL rule.", "label.aclid": "ACL", -"label.aclname": "Nom ACL", +"label.acl.rule.name": "Nom ACL", "label.acquire.new.ip": "Acqu\u00e9rir nouvelle adr. IP", "label.acquire.new.secondary.ip": "Acqu\u00e9rir nouvelle IP secondaire", "label.action": "Action", @@ -119,8 +119,8 @@ "label.activeviewersessions": "Sessions actives", "label.add": "Ajouter", "label.add.account": "Ajouter un compte", -"label.add.acl": "Ajouter r\u00e8gle ACL", -"label.add.acl.list": "Ajouter Liste ACL", +"label.add.acl.rule": "Ajouter r\u00e8gle ACL", +"label.add.acl": "Ajouter Liste ACL", "label.add.affinity.group": "Ajouter nouveau groupe d'affinit\u00e9", "label.add.baremetal.dhcp.device": "Ajouter un DHCP Baremetal", "label.add.bigswitchbcf.device": "Ajouter un contr\u00f4leur BigSwitch BCF", @@ -142,12 +142,12 @@ "label.add.ip.range": "Ajouter une plage IP", "label.add.isolated.network": "Ajouter un r\u00e9seau isol\u00e9", "label.add.ldap.account": "Ajouter un compte LDAP", -"label.add.list.name": "Nom Liste ACL", +"label.add.acl.name": "Nom Liste ACL", "label.add.more": "Ajouter plus", "label.add.netscaler.device": "Ajouter un Netscaler", "label.add.network": "Ajouter un r\u00e9seau", "label.add.network.acl": "Ajouter une r\u00e8gle d'acc\u00e8s r\u00e9seau ACL", -"label.add.network.acl.list": "Ajouter Liste ACL r\u00e9seau", +"label.add.network.acl": "Ajouter Liste ACL r\u00e9seau", "label.add.network.offering": "Ajouter Offre R\u00e9seau", "label.add.new.gateway": "Ajouter une nouvelle passerelle", "label.add.new.tier": "Ajouter un nouveau tiers", @@ -334,7 +334,7 @@ "label.default.use": "Utilisation par d\u00e9faut", "label.default.view": "Vue par d\u00e9faut", "label.delete": "Supprimer", -"label.delete.acl.list": "Supprimer Liste ACL", +"label.delete.acl": "Supprimer Liste ACL", "label.delete.affinity.group": "Supprimer le groupe d'affinit\u00e9", "label.delete.alerts": "Supprimer alertes", "label.delete.bigswitchbcf": "Supprimer contr\u00f4leur BigSwitch BCF", @@ -419,7 +419,7 @@ "label.dpd": "D\u00e9tection de pair mort", "label.driver": "Pilote", "label.edit": "Modifier", -"label.edit.acl.list": "Edit ACL List", +"label.edit.acl": "Edit ACL", "label.edit.acl.rule": "Modifier r\u00e8gle ACL", "label.edit.project.details": "Modifier les d\u00e9tails du projet", "label.edit.role": "\u00c9diter R\u00f4le", @@ -926,7 +926,6 @@ "label.remove.vpc.offering": "Supprimer offre VPC", "label.removing": "Suppression", "label.replace.acl": "Remplacer ACL", -"label.replace.acl.list": "Remplacer Liste ACL", "label.required": "Requis", "label.requireshvm": "HVM", "label.requiresupgrade": "Mise \u00e0 jour n\u00e9cessaire", @@ -1152,8 +1151,7 @@ "label.usehttps": "Utiliser HTTPS", "label.usenewdiskoffering": "Replace disk offering?", "label.user": "Utilisateur", -"label.userdata": "Donn\u00e9es Utilisateur", -"label.userdatal2": "Donn\u00e9es utilisateur", +"label.user.data": "Donn\u00e9es Utilisateur", "label.username": "Identifiant", "label.users": "Utilisateurs", "label.utilization": "Utilisation", @@ -1331,7 +1329,7 @@ "message.confirm.archive.selected.alerts": "Confirmer l'archivage des alertes s\u00e9lectionn\u00e9es", "message.confirm.archive.selected.events": "Confirmez l'archivage des \u00e9v\u00e9nements s\u00e9lectionn\u00e9s", "message.confirm.attach.disk": "Confirmer le rattachement de ce disque ?", -"message.confirm.delete.acl.list": "Confirmer la suppression de cette liste ACL ?", +"message.confirm.delete.acl": "Confirmer la suppression de cette liste ACL ?", "message.confirm.delete.bigswitchbcf": "Confirmer que vous voulez supprimer ce contr\u00f4leur BigSwitch BCF", "message.confirm.delete.brocadevcs": "Confirmer la suppression du switch Brocade Vcs", "message.confirm.delete.ciscoasa1000v": "Confirmez la suppression du CiscoASA1000v", diff --git a/ui/public/locales/hi.json b/ui/public/locales/hi.json index 055909360b5..9d7440b9262 100644 --- a/ui/public/locales/hi.json +++ b/ui/public/locales/hi.json @@ -6,7 +6,7 @@ "label.accounts": "लेखा", "label.accounttype": "खाता प्रकार", "label.aclid": "ACL", -"label.aclname": "ACL नाम", +"label.acl.rule.name": "ACL नाम", "label.actions": "क्रियाएँ", "label.add": "जोड़ें", "label.adding": "जोड़ना", @@ -410,7 +410,7 @@ "label.usageunit": "Unit", "label.usehttps": "HTTPS का उपयोग करें", "label.user": "उपयोगकर्ता", -"label.userdata": "Userdata", +"label.user.data": "User Data", "label.username": "उपयोगकर्ता नाम", "label.users": "उपयोगकर्ता", "label.uuid": "ID", diff --git a/ui/public/locales/hu.json b/ui/public/locales/hu.json index 48275ce81fc..ae58379d343 100644 --- a/ui/public/locales/hu.json +++ b/ui/public/locales/hu.json @@ -14,12 +14,12 @@ "label.account.specific": "Sz\u00e1mla-specifikus", "label.accounts": "Sz\u00e1ml\u00e1k", "label.accounttype": "Sz\u00e1mla t\u00edpus", -"label.acl.export": "Export ACLs", +"label.acl.export": "Export ACL rules", "label.acl.id": "ACL ID", -"label.acl.list.rules": "ACL List Rules", +"label.acl.rules": "ACL Rules", "label.acl.reason.description": "Enter the reason behind an ACL rule.", "label.aclid": "ACL", -"label.aclname": "ACL n\u00e9v", +"label.acl.rule.name": "ACL n\u00e9v", "label.acquire.new.ip": "\u00daj IP c\u00edm beszerz\u00e9se", "label.acquire.new.secondary.ip": "\u00daj m\u00e1sodlagos IP c\u00edm beszerz\u00e9se", "label.action": "M\u0171velet", @@ -119,8 +119,8 @@ "label.activeviewersessions": "Akt\u00edv munkamenetek", "label.add": "Felv\u00e9tel", "label.add.account": "Sz\u00e1mla felv\u00e9tele", -"label.add.acl": "ACL felv\u00e9tele", -"label.add.acl.list": "ACL lista felv\u00e9tele", +"label.add.acl.rule": "ACL felv\u00e9tele", +"label.add.acl": "ACL Lista felv\u00e9tele", "label.add.affinity.group": "\u00daj affin\u00edt\u00e1si csoport felv\u00e9tele", "label.add.baremetal.dhcp.device": "Baremetal DHCP eszk\u00f6z felv\u00e9tele", "label.add.bigswitchbcf.device": "BigSwitch BCF vez\u00e9rl\u0151 felv\u00e9tele", @@ -142,12 +142,12 @@ "label.add.ip.range": "IP c\u00edmtartom\u00e1ny felv\u00e9tele", "label.add.isolated.network": "Izol\u00e1lt h\u00e1l\u00f3zat felv\u00e9tele", "label.add.ldap.account": "LDAP hozz\u00e1f\u00e9r\u00e9s felv\u00e9tele", -"label.add.list.name": "ACL lista n\u00e9v", +"label.add.acl.name": "ACL Lista n\u00e9v", "label.add.more": "Tov\u00e1bbi felv\u00e9tele", "label.add.netscaler.device": "Netscaler eszk\u00f6z felv\u00e9tele", "label.add.network": "H\u00e1l\u00f3zat felv\u00e9tele", "label.add.network.acl": "H\u00e1l\u00f3zati ACL felv\u00e9tele", -"label.add.network.acl.list": "H\u00e1l\u00f3zati ACL lista felv\u00e9tele", +"label.add.network.acl": "H\u00e1l\u00f3zati ACL Lista felv\u00e9tele", "label.add.network.offering": "H\u00e1l\u00f3zati aj\u00e1nlat felv\u00e9tele", "label.add.new.gateway": "\u00daj \u00e1tj\u00e1r\u00f3 felv\u00e9tele", "label.add.new.tier": "\u00daj r\u00e9teg felv\u00e9tele", @@ -334,7 +334,7 @@ "label.default.use": "Alap\u00e9rtelmezett haszn\u00e1lat", "label.default.view": "Alap\u00e9rtelmezett n\u00e9zet", "label.delete": "T\u00f6rl\u00e9s", -"label.delete.acl.list": "ACL lista t\u00f6rl\u00e9se", +"label.delete.acl": "ACL Lista t\u00f6rl\u00e9se", "label.delete.affinity.group": "Affin\u00edt\u00e1si csoport t\u00f6rl\u00e9se", "label.delete.alerts": "T\u00f6rl\u00e9s riaszt\u00e1sok", "label.delete.bigswitchbcf": "BigSwitch BCF vez\u00e9rl\u0151 elt\u00e1vol\u00edt\u00e1sa", @@ -419,7 +419,7 @@ "label.driver": "Driver", "label.dynamicscalingenabled": "dinamikus m\u00e9retez\u00e9s enged\u00e9lyezve", "label.edit": "Szerkeszt\u00e9s", -"label.edit.acl.list": "Edit ACL List", +"label.edit.acl": "Edit ACL", "label.edit.acl.rule": "ACL szab\u00e1ly szerkeszt\u00e9se", "label.edit.project.details": "Projekt r\u00e9szletek szerkeszt\u00e9se", "label.edit.role": "Edit Role", @@ -924,7 +924,6 @@ "label.remove.vpc.offering": "VPC aj\u00e1nlat t\u00f6rl\u00e9se", "label.removing": "T\u00f6rl\u00e9s", "label.replace.acl": "ACL csere", -"label.replace.acl.list": "ACL lista cser\u00e9je", "label.required": "Sz\u00fcks\u00e9ges", "label.requireshvm": "HVM", "label.requiresupgrade": "Friss\u00edt\u00e9st ig\u00e9nyel", @@ -1150,8 +1149,7 @@ "label.usehttps": "HTTPS haszn\u00e1lata", "label.usenewdiskoffering": "Replace disk offering?", "label.user": "Felhaszn\u00e1l\u00f3", -"label.userdata": "Felhaszn\u00e1l\u00f3 adat", -"label.userdatal2": "Felhaszn\u00e1l\u00f3i adat", +"label.user.data": "Felhaszn\u00e1l\u00f3 adat", "label.username": "Felhaszn\u00e1l\u00f3n\u00e9v", "label.users": "Felhaszn\u00e1l\u00f3k", "label.utilization": "Utilisation", @@ -1329,7 +1327,7 @@ "message.confirm.archive.selected.alerts": "Er\u0151s\u00edtsd meg, hogy le akarod archiv\u00e1lni a kiv\u00e1lasztott riaszt\u00e1sokat!", "message.confirm.archive.selected.events": "Er\u0151s\u00edtsd meg, hogy archiv\u00e1lni szeretn\u00e9d a kiv\u00e1lasztott esem\u00e9nyeket!", "message.confirm.attach.disk": "Biztosan csatolni szeretn\u00e9d a merevlemezt?", -"message.confirm.delete.acl.list": "Biztosan t\u00f6r\u00f6lni akarod ezt a ACL list\u00e1t?", +"message.confirm.delete.acl": "Biztosan t\u00f6r\u00f6lni akarod ezt a ACL List\u00e1t?", "message.confirm.delete.bigswitchbcf": "Er\u0151s\u00edtsd meg, hogy t\u00f6r\u00f6lni szeretn\u00e9d ezt a BigSwitch BCF vez\u00e9rl\u0151t!", "message.confirm.delete.brocadevcs": "Er\u0151s\u00edtsd meg, hogy t\u00f6r\u00f6lni szeretn\u00e9d a Brocade Vcs Switch-et", "message.confirm.delete.ciscoasa1000v": "Er\u0151s\u00edtsd meg, hogy t\u00f6r\u00f6lni akarod a CiscoASA1000v-t", @@ -1354,7 +1352,7 @@ "message.confirm.remove.selected.events": "Er\u0151s\u00edtsd meg, hogy t\u00f6r\u00f6lni szeretn\u00e9d a kiv\u00e1lasztott esem\u00e9nyeket", "message.confirm.remove.vmware.datacenter": "Er\u0151s\u00edtsd meg, hogy el akarod t\u00e1vol\u00edtani a VMware adatk\u00f6zpontot!", "message.confirm.remove.vpc.offering": "Biztos vagy abban, hogy t\u00f6r\u00f6lni akarod ezt a VPC aj\u00e1nlatot?", -"message.confirm.replace.acl.new.one": "Le akarod cser\u00e9lni ez ACL list\u00e1t egy \u00fajjal?", +"message.confirm.replace.acl.new.one": "Le akarod cser\u00e9lni ez ACL List\u00e1t egy \u00fajjal?", "message.confirm.scale.up.router.vm": "Biztosan fel akarod m\u00e9retezni a router VM-et?", "message.confirm.scale.up.system.vm": "Biztosan fel akarod m\u00e9retezni a rendszer VM-et?", "message.confirm.start.lb.vm": "Er\u0151s\u00edtsd meg, hogy el akarod ind\u00edtani az LB VM-et!", diff --git a/ui/public/locales/it_IT.json b/ui/public/locales/it_IT.json index 844763eba3a..a2907b71f23 100644 --- a/ui/public/locales/it_IT.json +++ b/ui/public/locales/it_IT.json @@ -14,12 +14,12 @@ "label.account.specific": "Specifico dell'Account", "label.accounts": "Utenti", "label.accounttype": "Account Type", -"label.acl.export": "Export ACLs", +"label.acl.export": "Export ACL rules", "label.acl.id": "ACL ID", -"label.acl.list.rules": "ACL List Rules", +"label.acl.rules": "ACL Rules", "label.acl.reason.description": "Enter the reason behind an ACL rule.", "label.aclid": "ACL", -"label.aclname": "ACL Name", +"label.acl.rule.name": "ACL Name", "label.acquire.new.ip": "Acquisizione nuovo indirizzo IP", "label.acquire.new.secondary.ip": "Acquisizione nuovo IP secondario", "label.action": "Action", @@ -119,8 +119,8 @@ "label.activeviewersessions": "Sessioni Attive", "label.add": "Add", "label.add.account": "Aggiungi un Account", -"label.add.acl": "Aggiungere ACL", -"label.add.acl.list": "Add ACL List", +"label.add.acl.rule": "Aggiungere ACL", +"label.add.acl": "Add ACL", "label.add.affinity.group": "Aggiungere un nuovo gruppo di affinit\u00e0", "label.add.baremetal.dhcp.device": "Add Baremetal DHCP Device", "label.add.bigswitchbcf.device": "Aggiungere Controller BigSwitch BCF", @@ -142,12 +142,12 @@ "label.add.ip.range": "Aggiungere un IP Range", "label.add.isolated.network": "Add Isolated Network", "label.add.ldap.account": "Aggiungi un account LDAP", -"label.add.list.name": "ACL List Name", +"label.add.acl.name": "ACL Name", "label.add.more": "Add More", "label.add.netscaler.device": "Aggiungere device Netscaler", "label.add.network": "Aggiungere una Rete", "label.add.network.acl": "Aggiungere le ACL di rete", -"label.add.network.acl.list": "Add Network ACL List", +"label.add.network.acl": "Add Network ACL", "label.add.network.offering": "Aggiungere offerta di rete", "label.add.new.gateway": "Aggiungere un nuovo gateway", "label.add.new.tier": "Aggiungere un nuovo livello", @@ -334,7 +334,7 @@ "label.default.use": "Default Use", "label.default.view": "Vista di default", "label.delete": "Cancellare", -"label.delete.acl.list": "Delete ACL List", +"label.delete.acl": "Delete ACL", "label.delete.affinity.group": "Cancellare Gruppo di Affinit\u00e0", "label.delete.alerts": "Cancella allarmi", "label.delete.bigswitchbcf": "Rimuovere Controller BigSwitch BCF", @@ -419,7 +419,7 @@ "label.dpd": "Dead Peer Detection", "label.driver": "Driver", "label.edit": "Modifica", -"label.edit.acl.list": "Edit ACL List", +"label.edit.acl": "Edit ACL", "label.edit.acl.rule": "Edit ACL rule", "label.edit.project.details": "Modificare i dettagli del progetto", "label.edit.role": "Edit Role", @@ -924,7 +924,6 @@ "label.remove.vpc.offering": "Remove VPC offering", "label.removing": "Rimozione", "label.replace.acl": "Replace ACL", -"label.replace.acl.list": "Replace ACL List", "label.required": "Required", "label.requireshvm": "HVM", "label.requiresupgrade": "Requires Upgrade", @@ -1150,8 +1149,7 @@ "label.usehttps": "Utilizzare HTTPS", "label.usenewdiskoffering": "Replace disk offering?", "label.user": "User", -"label.userdata": "Userdata", -"label.userdatal2": "User Data", +"label.user.data": "User Data", "label.username": "Username", "label.users": "Users", "label.utilization": "Utilisation", @@ -1329,7 +1327,7 @@ "message.confirm.archive.selected.alerts": "Please confirm you would like to archive the selected alerts", "message.confirm.archive.selected.events": "Please confirm you would like to archive the selected events", "message.confirm.attach.disk": "Are you sure you want to attach disk?", -"message.confirm.delete.acl.list": "Are you sure you want to delete this ACL list?", +"message.confirm.delete.acl": "Are you sure you want to delete this ACL?", "message.confirm.delete.bigswitchbcf": "Please confirm that you would like to delete this BigSwitch BCF Controller", "message.confirm.delete.brocadevcs": "Please confirm that you would like to delete Brocade Vcs Switch", "message.confirm.delete.ciscoasa1000v": "Please confirm you want to delete CiscoASA1000v", diff --git a/ui/public/locales/ja_JP.json b/ui/public/locales/ja_JP.json index 8357b9d49a2..3ab374a2a06 100644 --- a/ui/public/locales/ja_JP.json +++ b/ui/public/locales/ja_JP.json @@ -58,11 +58,11 @@ "label.access.kubernetes.nodes": "Kubernetesノードに接続", "label.acl.export": "エクスポートACLs", "label.acl.id": "ACL ID", - "label.acl.list.rules": "ACLルールのリスト", + "label.acl.rules": "ACLルールのリスト", "label.acl.reason.description": "ACLルールの理由を入力してください。", "label.acl.replaced": "ACLが置き換えられました", "label.aclid": "ACL", - "label.aclname": "ACL名", + "label.acl.rule.name": "ACL名", "label.acltotal": "ネットワークACL合計", "label.acquire.new.ip": "新しいIPアドレスの取得", "label.acquire.new.secondary.ip": "セカンダリIPアドレスの取得", @@ -298,8 +298,8 @@ "label.add.account": "アカウント追加", "label.add.accounts": "アカウント追加", "label.add.accounts.to": "アカウントの追加先:", - "label.add.acl": "ACL追加", - "label.add.acl.list": "ACL一覧追加", + "label.add.acl.rule": "ACL追加", + "label.add.acl": "ACL一覧追加", "label.add.affinity.group": "新しいアフィニティグループ追加", "label.add.baremetal.dhcp.device": "ベアメタルDHCPデバイス追加", "label.add.baremetal.rack.configuration": "ベアメタルラック設定追加", @@ -333,14 +333,14 @@ "label.add.l2.guest.network": "L2ゲストネットワーク追加", "label.add.ldap.account": "LDAPアカウント追加", "label.add.ldap.list.users": "LDAPユーザー一覧", - "label.add.list.name": "ACL一覧名", + "label.add.acl.name": "ACL一覧名", "label.add.load.balancer": "ロードバランサー追加", "label.add.management.ip.range": "マネージメントIP範囲追加", "label.add.more": "その他の項目追加", "label.add.netscaler.device": "NetScalerデバイス追加", "label.add.network": "ネットワーク追加", "label.add.network.acl": "ネットワークACL追加", - "label.add.network.acl.list": "ネットワークACL一覧追加", + "label.add.network.acl": "ネットワークACL一覧追加", "label.add.network.device": "ネットワークデバイス追加", "label.add.network.offering": "ネットワークオファリング追加", "label.add.new.f5": "新しいF5追加", @@ -718,7 +718,7 @@ "label.default.view": "デフォルトビュー", "label.defaultnetwork": "デフォルトネットワーク", "label.delete": "削除", - "label.delete.acl.list": "ACL一覧削除", + "label.delete.acl": "ACL一覧削除", "label.delete.affinity.group": "アフィニティグループ削除", "label.delete.alerts": "アラート削除", "label.delete.backup": "バックアップ削除", @@ -872,7 +872,7 @@ "label.dynamicscalingenabled": "ダイナミックスケーリング有効", "label.dynamicscalingenabled.tooltip": "テンプレート、サービスオファリング、およびグローバル設定で動的スケーリングが有効になっている場合にのみ、VMは動的にスケーリングできます。", "label.edit": "編集", - "label.edit.acl.list": "ACL一覧編集", + "label.edit.acl": "ACL一覧編集", "label.edit.acl.rule": "ACLルール編集", "label.edit.affinity.group": "アフィニティグループ編集", "label.edit.lb.rule": "LBルール編集", @@ -1498,7 +1498,7 @@ "label.netscaler.vpx": "NetScaler VPXロードバランサー", "label.network": "ネットワーク", "label.network.acl": "ネットワークACL", - "label.network.acl.lists": "ネットワークACL一覧", + "label.network.acls": "ネットワークACL一覧", "label.network.acls": "ネットワークACL", "label.network.addvm": "VMへのネットワーク追加", "label.network.desc": "ネットワークの説明", @@ -1871,7 +1871,6 @@ "label.removing": "削除しています", "label.removing.user": "ユーザーを削除しています", "label.replace.acl": "ACLの置き換え", - "label.replace.acl.list": "ACL一覧の置き換え", "label.report.bug": "問題レポート", "label.required": "必須です", "label.requireshvm": "HVM", @@ -2321,8 +2320,7 @@ "label.user.details": "ユーザーの詳細", "label.user.source": "ソース", "label.user.vm": "ユーザーVM", - "label.userdata": "ユーザーデータ", - "label.userdatal2": "ユーザーデータ", + "label.user.data": "ユーザーデータ", "label.username": "ユーザー名", "label.users": "ユーザー", "label.usersource": "ユーザータイプ", @@ -2727,7 +2725,7 @@ "message.confirm.dedicate.host.domain.account": "このホストをドメイン/アカウント専用に設定してもよろしいですか?", "message.confirm.dedicate.pod.domain.account": "このポッドをドメイン/アカウント専用に設定してもよろしいですか?", "message.confirm.dedicate.zone": "このゾーンをドメイン/アカウント専用に設定してもよろしいですか?", - "message.confirm.delete.acl.list": "このACL一覧を削除してもよろしいですか?", + "message.confirm.delete.acl": "このACL一覧を削除してもよろしいですか?", "message.confirm.delete.alert": "このアラートを削除してもよろしいですか?", "message.confirm.delete.baremetal.rack.configuration": "ベアメタルラック設定を削除してもよろしいですか?", "message.confirm.delete.bigswitchbcf": "このBigSwitchBCFコントローラーを削除してもよろしいですか?", diff --git a/ui/public/locales/ko_KR.json b/ui/public/locales/ko_KR.json index f1fab4b584c..cc9683633be 100644 --- a/ui/public/locales/ko_KR.json +++ b/ui/public/locales/ko_KR.json @@ -32,10 +32,10 @@ "label.accounttype": "\uacc4\uc815 \uc720\ud615", "label.acl.export": "ACL \ub0b4\ubcf4\ub0b4\uae30", "label.acl.id": "ACL ID", -"label.acl.list.rules": "ACL \ubaa9\ub85d \uaddc\uce59", +"label.acl.rules": "ACL \ubaa9\ub85d \uaddc\uce59", "label.acl.reason.description": "ACL \uaddc\uce59 \ub4a4\uc5d0 \uc124\uba85\uc744 \uc785\ub825\ud558\uc2ed\uc2dc\uc624.", "label.aclid": "ACL", -"label.aclname": "ACL \uc774\ub984", +"label.acl.rule.name": "ACL \uc774\ub984", "label.acquire.new.ip": "\uc0c8 IP \uc8fc\uc18c \uac00\uc838\uc624\uae30", "label.acquire.new.secondary.ip": "\uc0c8 \ubcf4\uc870 IP \uc8fc\uc18c \uac00\uc838\uc624\uae30", "label.acquiring.ip": "IP \uac00\uc838\uc624\uae30", @@ -153,8 +153,8 @@ "label.activeviewersessions": "\ud65c\uc131 \uc138\uc158", "label.add": "\ucd94\uac00", "label.add.account": "\uacc4\uc815 \ucd94\uac00", -"label.add.acl": "\uad8c\ud55c \uad00\ub9ac(ACL) \ucd94\uac00", -"label.add.acl.list": "ACL \ubaa9\ub85d \ucd94\uac00", +"label.add.acl.rule": "\uad8c\ud55c \uad00\ub9ac(ACL) \ucd94\uac00", +"label.add.acl": "ACL \ubaa9\ub85d \ucd94\uac00", "label.add.affinity.group": "\uc0c8 Affinity \uadf8\ub8f9 \ucd94\uac00", "label.add.baremetal.dhcp.device": "Baremetal DHCP \uc7a5\uce58 \ucd94\uac00", "label.add.bigswitchbcf.device": "BigSwitch BCF \ucee8\ud2b8\ub864\ub7ec \ucd94\uac00", @@ -178,12 +178,12 @@ "label.add.isolated.network": "isolated \ub124\ud2b8\uc6cc\ud06c \ucd94\uac00", "label.add.kubernetes.cluster": "\ucfe0\ubc84\ub124\ud14c\uc2a4 \ud074\ub7ec\uc2a4\ud130 \ucd94\uac00", "label.add.ldap.account": "LDAP \uacc4\uc815 \ucd94\uac00", -"label.add.list.name": "ACL \ubaa9\ub85d \uc774\ub984", +"label.add.acl.name": "ACL \ubaa9\ub85d \uc774\ub984", "label.add.more": "\ub2e4\ub978 \ud56d\ubaa9 \ucd94\uac00", "label.add.netscaler.device": "Netscaler \uc7a5\uce58 \ucd94\uac00", "label.add.network": "\ub124\ud2b8\uc6cc\ud06c \ucd94\uac00", "label.add.network.acl": "\ub124\ud2b8\uc6cc\ud06c \uad8c\ud55c \uad00\ub9ac(ACL) \ucd94\uac00", -"label.add.network.acl.list": "\ub124\ud2b8\uc6cc\ud06c ACL \ubaa9\ub85d \ucd94\uac00", +"label.add.network.acl": "\ub124\ud2b8\uc6cc\ud06c ACL \ubaa9\ub85d \ucd94\uac00", "label.add.network.offering": "\ub124\ud2b8\uc6cc\ud06c \uc624\ud37c\ub9c1 \ucd94\uac00", "label.add.new.gateway": "\uc0c8 \uac8c\uc774\ud2b8\uc6e8\uc774 \ucd94\uac00\ud558\uae30", "label.add.new.tier": "\uc0c8 \uc11c\ube0c\ub137 \ucd94\uac00", @@ -436,7 +436,7 @@ "label.default.view": "\uae30\ubcf8 \ubcf4\uae30", "label.defaultnetwork": "\uae30\ubcf8 \ub124\ud2b8\uc6cc\ud06c", "label.delete": "\uc0ad\uc81c", -"label.delete.acl.list": "ACL \ubaa9\ub85d \uc0ad\uc81c", +"label.delete.acl": "ACL \ubaa9\ub85d \uc0ad\uc81c", "label.delete.affinity.group": "Affinity \uadf8\ub8f9 \uc0ad\uc81c", "label.delete.alerts": "\uc54c\ub9bc \uc0ad\uc81c", "label.delete.backup": "\ubc31\uc5c5 \uc0ad\uc81c", @@ -549,7 +549,7 @@ "label.dpd": "Dead \ud53c\uc5b4 \uac10\uc9c0", "label.driver": "\ub4dc\ub77c\uc774\ubc84", "label.edit": "\ud3b8\uc9d1", -"label.edit.acl.list": "ACL \ubaa9\ub85d \ud3b8\uc9d1", +"label.edit.acl": "ACL \ubaa9\ub85d \ud3b8\uc9d1", "label.edit.acl.rule": "ACL \uaddc\uce59 \ud3b8\uc9d1", "label.edit.project.details": "\ud504\ub85c\uc81d\ud2b8 \uc0c1\uc138 \ud3b8\uc9d1", "label.edit.project.role": "\ud504\ub85c\uc81d\ud2b8 \uc5ed\ud560 \ud3b8\uc9d1", @@ -985,7 +985,7 @@ "label.netscaler.vpx": "NetScaler VPX \ub85c\ub4dc\ubc38\ub7f0\uc11c", "label.network": "\ub124\ud2b8\uc6cc\ud06c", "label.network.acl": "\ub124\ud2b8\uc6cc\ud06c \uad8c\ud55c \uad00\ub9ac(ACL)", -"label.network.acl.lists": "Network ACL \ubaa9\ub85d", +"label.network.acls": "Network ACL \ubaa9\ub85d", "label.network.addvm": "VM\uc5d0 \ub124\ud2b8\uc6cc\ud06c \ucd94\uac00", "label.network.desc": "\ub124\ud2b8\uc6cc\ud06c \uc124\uba85", "label.network.domain": "\ub124\ud2b8\uc6cc\ud06c \ub3c4\uba54\uc778", @@ -1251,7 +1251,6 @@ "label.remove.vpc.offering": "VPC \uc624\ud37c\ub9c1 \uc0ad\uc81c", "label.removing": "\uc0ad\uc81c\ud558\ub294 \uc911...", "label.replace.acl": "ACL \uad50\uccb4", -"label.replace.acl.list": "ACL \ubaa9\ub85d \uad50\uccb4", "label.report.bug": "\uc774\uc288 \ub9ac\ud3ec\ud2b8", "label.required": "\ud544\uc218 \uc0ac\ud56d", "label.requireshvm": "HVM", @@ -1560,8 +1559,7 @@ "label.usenewdiskoffering": "\ub514\uc2a4\ud06c \uc624\ud37c\ub9c1\uc744 \ubcc0\uacbd\ud558\uc2dc\uaca0\uc2b5\ub2c8\uae4c?", "label.user": "\uc0ac\uc6a9\uc790", "label.user.conflict": "\ucda9\ub3cc", -"label.userdata": "\uc0ac\uc6a9\uc790 \ub370\uc774\ud130", -"label.userdatal2": "\uc0ac\uc6a9\uc790 \ub370\uc774\ud130", +"label.user.data": "\uc0ac\uc6a9\uc790 \ub370\uc774\ud130", "label.username": "\uc0ac\uc6a9\uc790 \uc774\ub984", "label.users": "\uc0ac\uc6a9\uc790", "label.usersource": "\uc0ac\uc6a9\uc790 \uc720\ud615", @@ -1818,7 +1816,7 @@ "message.confirm.archive.selected.events": "\uc120\ud0dd\ud55c \uc774\ubca4\ud2b8\ub97c \ubcf4\uad00\ud560 \uac83\uc778\uc9c0 \ud655\uc778\ud558\uc2ed\uc2dc\uc624.", "message.confirm.attach.disk": "\ub514\uc2a4\ud06c\ub97c \uc5f0\uacb0 \ud558\uc2dc\uaca0\uc2b5\ub2c8\uae4c?", "message.confirm.configure.ovs": "Ovs\ub97c \uad6c\uc131\ud558\uc2dc\uaca0\uc2b5\ub2c8\uae4c?", -"message.confirm.delete.acl.list": "\uc774 ACL \ubaa9\ub85d\uc744 \uc0ad\uc81c \ud558\uc2dc\uaca0\uc2b5\ub2c8\uae4c?", +"message.confirm.delete.acl": "\uc774 ACL \ubaa9\ub85d\uc744 \uc0ad\uc81c \ud558\uc2dc\uaca0\uc2b5\ub2c8\uae4c?", "message.confirm.delete.bigswitchbcf": "\uc774 BigSwitch BCF \ucee8\ud2b8\ub864\ub7ec\ub97c \uc0ad\uc81c\ud560 \uac83\uc778\uc9c0 \ud655\uc778\ud558\uc2ed\uc2dc\uc624.", "message.confirm.delete.brocadevcs": "Brocade Vcs \uc2a4\uc704\uce58\ub97c \uc0ad\uc81c\ud560 \uac83\uc778\uc9c0 \ud655\uc778\ud558\uc2ed\uc2dc\uc624.", "message.confirm.delete.ciscoasa1000v": "CiscoASA1000\uc744 \uc0ad\uc81c\ud560 \uac83\uc778\uc9c0 \ud655\uc778\ud558\uc2ed\uc2dc\uc624.", diff --git a/ui/public/locales/nb_NO.json b/ui/public/locales/nb_NO.json index ec51f7415d3..586d1fc5b25 100644 --- a/ui/public/locales/nb_NO.json +++ b/ui/public/locales/nb_NO.json @@ -14,12 +14,12 @@ "label.account.specific": "Kontospesifikk", "label.accounts": "Kontoer", "label.accounttype": "Kontotype", -"label.acl.export": "Export ACLs", +"label.acl.export": "Export ACL rules", "label.acl.id": "ACL ID", -"label.acl.list.rules": "ACL Liste Regler", +"label.acl.rules": "ACL Liste Regler", "label.acl.reason.description": "Enter the reason behind an ACL rule.", "label.aclid": "ACL", -"label.aclname": "ACL Navn", +"label.acl.rule.name": "ACL Navn", "label.acquire.new.ip": "Tilegne ny IP", "label.acquire.new.secondary.ip": "Tilegne ny sekund\u00e6r IP", "label.action": "Handling", @@ -119,8 +119,8 @@ "label.activeviewersessions": "Aktive sesjoner", "label.add": "Legg til", "label.add.account": "Legg til konto", -"label.add.acl": "Legg til ACL", -"label.add.acl.list": "Legg til ACL liste", +"label.add.acl.rule": "Legg til ACL", +"label.add.acl": "Legg til ACL liste", "label.add.affinity.group": "Legg til affinitetsgruppe", "label.add.baremetal.dhcp.device": "Legg Til Barmetall DHCP Enhet", "label.add.bigswitchbcf.device": "Legg til BigSwitch BCF kontroller", @@ -142,12 +142,12 @@ "label.add.ip.range": "Legg til IP-rekke", "label.add.isolated.network": "Legg Til Isolert Nettverk", "label.add.ldap.account": "Legg til LDAP-konto", -"label.add.list.name": "ACL listenavn", +"label.add.acl.name": "ACL listenavn", "label.add.more": "Legg til mer", "label.add.netscaler.device": "Legg til Netscaler enhet", "label.add.network": "Legg til nettverk", "label.add.network.acl": "Legg til nettverk ACL", -"label.add.network.acl.list": "Legg til nettverk ACL liste", +"label.add.network.acl": "Legg til nettverk ACL liste", "label.add.network.offering": "Legg til nettverkstilbud", "label.add.new.gateway": "Legg til ny gateway", "label.add.new.tier": "Legg til ny gren", @@ -334,7 +334,7 @@ "label.default.use": "Standard bruk", "label.default.view": "Standardvisning", "label.delete": "Slett", -"label.delete.acl.list": "Slett ACL liste", +"label.delete.acl": "Slett ACL liste", "label.delete.affinity.group": "Slett affinitetsgruppe", "label.delete.alerts": "Slette varsler", "label.delete.bigswitchbcf": "Fjern BigSwitch BCF-kontroller", @@ -419,7 +419,7 @@ "label.dpd": "D\u00f8d endepunkt-deteksjon", "label.driver": "Driver", "label.edit": "Editer", -"label.edit.acl.list": "Edit ACL List", +"label.edit.acl": "Edit ACL", "label.edit.acl.rule": "Endre ACL regel", "label.edit.project.details": "Editer prosjektdetaljer", "label.edit.role": "Edit Role", @@ -924,7 +924,6 @@ "label.remove.vpc.offering": "Fjern VPC tilbud", "label.removing": "Fjerner", "label.replace.acl": "Erstatt ACL", -"label.replace.acl.list": "Erstatt ACL Liste", "label.required": "P\u00e5krevd", "label.requireshvm": "HVM", "label.requiresupgrade": "Krever oppgradering", @@ -1150,8 +1149,7 @@ "label.usehttps": "Bruk HTTPS", "label.usenewdiskoffering": "Replace disk offering?", "label.user": "Bruker", -"label.userdata": "Brukerdata", -"label.userdatal2": "Brukerdata", +"label.user.data": "Brukerdata", "label.username": "Brukernavn", "label.users": "Brukere", "label.utilization": "Utilisation", @@ -1329,7 +1327,7 @@ "message.confirm.archive.selected.alerts": "Vennligst bekreft at du \u00f8nsker \u00e5 arkivere valgte varsler", "message.confirm.archive.selected.events": "Vennligst bekreft at du vil arkivere valgte hendelser", "message.confirm.attach.disk": "Er du sikker p\u00e5 at du vil tildele disk?", -"message.confirm.delete.acl.list": "Er du sikker p\u00e5 at du \u00f8nsker \u00e5 slette denne ACL listen?", +"message.confirm.delete.acl": "Er du sikker p\u00e5 at du \u00f8nsker \u00e5 slette denne ACL listen?", "message.confirm.delete.bigswitchbcf": "Vennligst bekreft at du \u00f8nsker \u00e5 slette denne BigSwitch BCF Controlleren?", "message.confirm.delete.brocadevcs": "Vennligst bekreft at du vil slette denne Brocade Vcs svitsjen", "message.confirm.delete.ciscoasa1000v": "Vennligst bekreft at du vil slette CiscoASA1000v", diff --git a/ui/public/locales/nl_NL.json b/ui/public/locales/nl_NL.json index 6bd1bf67159..15b1bbb1394 100644 --- a/ui/public/locales/nl_NL.json +++ b/ui/public/locales/nl_NL.json @@ -14,12 +14,12 @@ "label.account.specific": "Account-specifiek", "label.accounts": "Accounts", "label.accounttype": "Account type", -"label.acl.export": "Export ACLs", +"label.acl.export": "Export ACL rules", "label.acl.id": "ACL ID", -"label.acl.list.rules": "ACL lijst regels", +"label.acl.rules": "ACL lijst regels", "label.acl.reason.description": "Enter the reason behind an ACL rule.", "label.aclid": "ACL", -"label.aclname": "ACL naam", +"label.acl.rule.name": "ACL naam", "label.acquire.new.ip": "Bemachtig nieuw IP", "label.acquire.new.secondary.ip": "Verkrijg nieuw secundair IP", "label.action": "Actie", @@ -119,8 +119,8 @@ "label.activeviewersessions": "Actieve Sessies", "label.add": "Voeg toe", "label.add.account": "Voeg Account toe", -"label.add.acl": "Voeg ACL toe", -"label.add.acl.list": "voeg een ACL lijst toe", +"label.add.acl.rule": "Voeg ACL toe", +"label.add.acl": "voeg een ACL lijst toe", "label.add.affinity.group": "Nieuwe affinity groep toevoegen", "label.add.baremetal.dhcp.device": "Voeg Baremetal DHCP Apparaat toe", "label.add.bigswitchbcf.device": "Voeg eenBigSwitch BCF controller toe", @@ -142,12 +142,12 @@ "label.add.ip.range": "Voeg IP range toe", "label.add.isolated.network": "Geisoleerd Netwerk Toevoegen", "label.add.ldap.account": "Voeg LDAP account toe", -"label.add.list.name": "ACL lijst naam", +"label.add.acl.name": "ACL lijst naam", "label.add.more": "Voeg meer toe", "label.add.netscaler.device": "Voeg Netscaler apparaat toe", "label.add.network": "Voeg Netwerk toe", "label.add.network.acl": "Voeg netwerk ACL toe", -"label.add.network.acl.list": "voeg netwerk ACL lijst toe", +"label.add.network.acl": "voeg netwerk ACL lijst toe", "label.add.network.offering": "Voeg netwerk aanbieding toe", "label.add.new.gateway": "Voeg nieuwe gateway toe", "label.add.new.tier": "Voeg nieuwe Tier toe", @@ -334,7 +334,7 @@ "label.default.use": "Standaard Gebruik", "label.default.view": "Standaard Weergave", "label.delete": "Verwijder", -"label.delete.acl.list": "verwijder ACL lijst", +"label.delete.acl": "verwijder ACL lijst", "label.delete.affinity.group": "Verwijder Affinity Groep", "label.delete.alerts": "Verwijder waarschuwingen", "label.delete.bigswitchbcf": "Verwijder BigSwitch BCF Controller", @@ -420,7 +420,7 @@ "label.driver": "Driver", "label.dynamicscalingenabled": "Dynamisch schalen ingeschakeld\n", "label.edit": "Wijzig", -"label.edit.acl.list": "Verander een ACL lijst", +"label.edit.acl": "Verander een ACL lijst", "label.edit.acl.rule": "wijzig ACL regel", "label.edit.project.details": "Wijzig project details", "label.edit.role": "Edit Role", @@ -925,7 +925,6 @@ "label.remove.vpc.offering": "VPC aanbieding verwijderen", "label.removing": "Verwijderen", "label.replace.acl": "vervang ACL", -"label.replace.acl.list": "vervang ACL lijst", "label.required": "Vereist", "label.requireshvm": "HVM", "label.requiresupgrade": "Upgrade Benodigd", @@ -1151,8 +1150,7 @@ "label.usehttps": "Gebruik HTTPS", "label.usenewdiskoffering": "Replace disk offering?", "label.user": "Gebruiker", -"label.userdata": "Gebruikers gegevens", -"label.userdatal2": "Gebruiker Data", +"label.user.data": "Gebruiker Data", "label.username": "Gebruikersnaam", "label.users": "Gebruikers", "label.utilization": "Utilisation", @@ -1330,7 +1328,7 @@ "message.confirm.archive.selected.alerts": "bevestig dat u de geselecteerde meldingen wilt archiveren, alstublieft", "message.confirm.archive.selected.events": "bevestig dat u de geselecteerde gebeurtenissen wilt archiveren, alstublieft", "message.confirm.attach.disk": "Weet U zeker dat U een disk wilt koppelen?", -"message.confirm.delete.acl.list": "Weet U zeker dat U dit ACL wilt verwijderen?", +"message.confirm.delete.acl": "Weet U zeker dat U dit ACL wilt verwijderen?", "message.confirm.delete.bigswitchbcf": "bevestig dat u deze BigSwitch BCF Controller wilt verwijderen, alstublieft", "message.confirm.delete.brocadevcs": "bevestigd dat Brocade Vcs Switch wilt verwijderen, altublieft", "message.confirm.delete.ciscoasa1000v": "bevestig dat u CiscoASA100v wilt verwijderen, alstublieft", diff --git a/ui/public/locales/pl.json b/ui/public/locales/pl.json index 6077da5928e..fbbbf612bc1 100644 --- a/ui/public/locales/pl.json +++ b/ui/public/locales/pl.json @@ -14,12 +14,12 @@ "label.account.specific": "Account-Specific", "label.accounts": "Konta", "label.accounttype": "Account Type", -"label.acl.export": "Export ACLs", +"label.acl.export": "Export ACL rules", "label.acl.id": "ACL ID", -"label.acl.list.rules": "ACL List Rules", +"label.acl.rules": "ACL Rules", "label.acl.reason.description": "Enter the reason behind an ACL rule.", "label.aclid": "ACL", -"label.aclname": "ACL Name", +"label.acl.rule.name": "ACL Name", "label.acquire.new.ip": "Acquire New IP", "label.acquire.new.secondary.ip": "Acquire new secondary IP", "label.action": "Action", @@ -119,8 +119,8 @@ "label.activeviewersessions": "Active Sessions", "label.add": "Dodaj", "label.add.account": "Dodaj konto", -"label.add.acl": "Dodaj ACL", -"label.add.acl.list": "Add ACL List", +"label.add.acl.rule": "Dodaj ACL", +"label.add.acl": "Add ACL rule List", "label.add.affinity.group": "Add new affinity group", "label.add.baremetal.dhcp.device": "Add Baremetal DHCP Device", "label.add.bigswitchbcf.device": "Add BigSwitch BCF Controller", @@ -142,12 +142,12 @@ "label.add.ip.range": "Add IP Range", "label.add.isolated.network": "Add Isolated Network", "label.add.ldap.account": "Add LDAP account", -"label.add.list.name": "ACL List Name", +"label.add.acl.name": "ACL Name", "label.add.more": "Dodaj wi\u0119cej", "label.add.netscaler.device": "Add Netscaler device", "label.add.network": "Dodaj sie\u0107", "label.add.network.acl": "Add network ACL", -"label.add.network.acl.list": "Add Network ACL List", +"label.add.network.acl": "Add Network ACL", "label.add.network.offering": "Add network offering", "label.add.new.gateway": "Add new gateway", "label.add.new.tier": "Add new tier", @@ -334,7 +334,7 @@ "label.default.use": "Default Use", "label.default.view": "Widok domy\u015blny", "label.delete": "Usu\u0144", -"label.delete.acl.list": "Delete ACL List", +"label.delete.acl": "Delete ACL", "label.delete.affinity.group": "Delete Affinity Group", "label.delete.alerts": "Delete alerts", "label.delete.bigswitchbcf": "Remove BigSwitch BCF Controller", @@ -419,7 +419,7 @@ "label.dpd": "Dead Peer Detection", "label.driver": "Driver", "label.edit": "Edytuj", -"label.edit.acl.list": "Edit ACL List", +"label.edit.acl": "Edit ACL", "label.edit.acl.rule": "Edit ACL rule", "label.edit.project.details": "Zmie\u0144 szczeg\u00f3\u0142y projektu", "label.edit.role": "Edit Role", @@ -924,7 +924,6 @@ "label.remove.vpc.offering": "Remove VPC offering", "label.removing": "Usuwanie", "label.replace.acl": "Replace ACL", -"label.replace.acl.list": "Replace ACL List", "label.required": "Wymagane", "label.requireshvm": "HVM", "label.requiresupgrade": "Requires Upgrade", @@ -1150,8 +1149,7 @@ "label.usehttps": "Use HTTPS", "label.usenewdiskoffering": "Replace disk offering?", "label.user": "U\u017cytkowni", -"label.userdata": "Userdata", -"label.userdatal2": "User Data", +"label.user.data": "User Data", "label.username": "Nazwa u\u017cytkownika", "label.users": "U\u017cytkownicy", "label.utilization": "Utilisation", @@ -1329,7 +1327,7 @@ "message.confirm.archive.selected.alerts": "Please confirm you would like to archive the selected alerts", "message.confirm.archive.selected.events": "Please confirm you would like to archive the selected events", "message.confirm.attach.disk": "Are you sure you want to attach disk?", -"message.confirm.delete.acl.list": "Are you sure you want to delete this ACL list?", +"message.confirm.delete.acl": "Are you sure you want to delete this ACL?", "message.confirm.delete.bigswitchbcf": "Please confirm that you would like to delete this BigSwitch BCF Controller", "message.confirm.delete.brocadevcs": "Please confirm that you would like to delete Brocade Vcs Switch", "message.confirm.delete.ciscoasa1000v": "Please confirm you want to delete CiscoASA1000v", diff --git a/ui/public/locales/pt_BR.json b/ui/public/locales/pt_BR.json index 8aba6fe2c78..09b0242ef1a 100644 --- a/ui/public/locales/pt_BR.json +++ b/ui/public/locales/pt_BR.json @@ -33,10 +33,10 @@ "label.accounttype": "Tipo de conta", "label.acl.export": "Exportar ACLs", "label.acl.id": "ACL ID", -"label.acl.list.rules": "Lista de regras de ACL", +"label.acl.rules": "Lista de regras de ACL", "label.acl.reason.description": "Motivo para se utilizar a regra.", "label.aclid": "ACL", -"label.aclname": "Nome da ACL", +"label.acl.rule.name": "Nome da ACL", "label.acquire.new.ip": "Adquirir novo IP", "label.acquire.new.secondary.ip": "Adquira um novo IP secund\u00e1rio", "label.acquiring.ip": "Obtendo IP", @@ -171,8 +171,8 @@ "label.activeviewersessions": "Sess\u00f5es ativas", "label.add": "Adicionar", "label.add.account": "Adicionar conta", -"label.add.acl": "Adicionar ACL", -"label.add.acl.list": "Adiciona lista ACL", +"label.add.acl.rule": "Adicionar ACL", +"label.add.acl": "Adiciona lista ACL", "label.add.affinity.group": "Adicionar um grupo de afinidade", "label.add.baremetal.dhcp.device": "Adicionar dispositivo DHCP baremetal", "label.add.bigswitchbcf.device": "Adicionar controlador BigSwitch BCF", @@ -196,12 +196,12 @@ "label.add.isolated.network": "Adiciona rede isolada", "label.add.kubernetes.cluster": "Adicionar cluster Kubernetes", "label.add.ldap.account": "Adicionar conta LDAP", -"label.add.list.name": "Nome da lista ACL", +"label.add.acl.name": "Nome da lista ACL", "label.add.more": "Adicionar mais", "label.add.netscaler.device": "Adicionar dispositivo Netscaler", "label.add.network": "Adicionar rede", "label.add.network.acl": "Adicione ACL de rede", -"label.add.network.acl.list": "Adicionar lista de ACL de rede", +"label.add.network.acl": "Adicionar lista de ACL de rede", "label.add.network.offering": "Adicionar oferta de rede", "label.add.new.gateway": "Adicionar novo gateway", "label.add.new.tier": "Adicionar nova camada", @@ -485,7 +485,7 @@ "label.default.view": "Visualiza\u00e7\u00e3o padr\u00e3o", "label.defaultnetwork": "Rede padr\u00e3o", "label.delete": "Remover", -"label.delete.acl.list": "Apagar lista ACL", +"label.delete.acl": "Apagar lista ACL", "label.delete.affinity.group": "Apagar grupo de afinidade", "label.delete.alerts": "Remover alertas", "label.delete.backup": "Apagar backup", @@ -612,7 +612,7 @@ "label.dynamicscalingenabled": "Escalonamento din\u00e2mico habilitado", "label.dynamicscalingenabled.tooltip": "VM s\u00f3 pode ser dinamicamente escalonada quando o escalonamento din\u00e2mico estiver habilitado no template, oferta de computa\u00e7\u00e3o e nas configura\u00e7\u00e3oes globais", "label.edit": "Editar", -"label.edit.acl.list": "Editar lista ACL", +"label.edit.acl": "Editar lista ACL", "label.edit.acl.rule": "Editar regra ACL", "label.edit.project.details": "Editar detalhes do projeto", "label.edit.project.role": "Editar fun\u00e7\u00e3o do projeto", @@ -1069,7 +1069,7 @@ "label.netscaler.vpx": "NetScaler VPX LoadBalancer", "label.network": "Rede", "label.network.acl": "ACL de rede", -"label.network.acl.lists": "Lista de redes ACL", +"label.network.acls": "Lista de redes ACL", "label.network.addvm": "Adicionar rede para VM", "label.network.desc": "Descri\u00e7\u00e3o de rede", "label.network.domain": "Dom\u00ednio de rede", @@ -1356,7 +1356,6 @@ "label.removed": "Removido", "label.removing": "Removendo", "label.replace.acl": "Substituir ACL", -"label.replace.acl.list": "Substituir lista ACL", "label.report.bug": "Reportar um problema", "label.required": "Obrigat\u00f3rio", "label.requireshvm": "HVM", @@ -1701,8 +1700,7 @@ "label.usenewdiskoffering": "Substituir a oferta de disco?", "label.user": "Usu\u00e1rio", "label.user.conflict": "Conflito", -"label.userdata": "Dados de usu\u00e1rio", -"label.userdatal2": "Dados de usu\u00e1rio", +"label.user.data": "Dados de usu\u00e1rio", "label.username": "Nome de usu\u00e1rio", "label.users": "Usu\u00e1rios", "label.usersource": "Tipo de usu\u00e1rio", @@ -1978,7 +1976,7 @@ "message.confirm.archive.selected.events": "Por favor confirme que voc\u00ea deseja arquivar os eventos selecionados", "message.confirm.attach.disk": "Voc\u00ea tem certeza que deseja conectar este disco?", "message.confirm.configure.ovs": "Voc\u00ea tem certeza de que quer configurar os Ovs?", -"message.confirm.delete.acl.list": "Voc\u00ea tem certeza que deseja apagar esta lista ACL?", +"message.confirm.delete.acl": "Voc\u00ea tem certeza que deseja apagar esta lista ACL?", "message.confirm.delete.bigswitchbcf": "Por favor, confirme que voc\u00ea deseja deletar este controlador BigSwitch BCF", "message.confirm.delete.brocadevcs": "Por favor confirme que voc\u00ea deseja remover o switch Brocade Vcs", "message.confirm.delete.ciscoasa1000v": "Favor confirmar que voc\u00ea deseja apagar este CiscoASA1000v", diff --git a/ui/public/locales/ru_RU.json b/ui/public/locales/ru_RU.json index 46d70081f1b..4fb3788bc51 100644 --- a/ui/public/locales/ru_RU.json +++ b/ui/public/locales/ru_RU.json @@ -14,12 +14,12 @@ "label.account.specific": "\u0421\u043f\u0435\u0446\u0438\u0444\u0438\u043a\u0430 \u0430\u043a\u043a\u0430\u0443\u043d\u043d\u0442\u0430", "label.accounts": "\u0423\u0447\u0451\u0442\u043d\u044b\u0435 \u0437\u0430\u043f\u0438\u0441\u0438", "label.accounttype": "Account Type", -"label.acl.export": "Export ACLs", +"label.acl.export": "Export ACL rules", "label.acl.id": "ACL ID", -"label.acl.list.rules": "ACL List Rules", +"label.acl.rules": "ACL Rules", "label.acl.reason.description": "Enter the reason behind an ACL rule.", "label.aclid": "ACL", -"label.aclname": "ACL Name", +"label.acl.rule.name": "ACL Name", "label.acquire.new.ip": "\u041f\u043e\u043b\u0443\u0447\u0438\u0442\u044c \u043d\u043e\u0432\u044b\u0439 IP", "label.acquire.new.secondary.ip": "\u0417\u0430\u043f\u0440\u043e\u0441\u0438\u0442\u044c \u0434\u043e\u043f\u043e\u043b\u043d\u0438\u0442\u0435\u043b\u044c\u043d\u044b\u0439 IP-\u0430\u0434\u0440\u0435\u0441", "label.action": "\u0414\u0435\u0439\u0441\u0442\u0432\u0438\u044f", @@ -119,8 +119,8 @@ "label.activeviewersessions": "\u0410\u043a\u0442\u0438\u0432\u043d\u044b\u0435 \u0441\u0435\u0441\u0441\u0438\u0438", "label.add": "\u0414\u043e\u0431\u0430\u0432\u0438\u0442\u044c", "label.add.account": "\u0414\u043e\u0431\u0430\u0432\u0438\u0442\u044c \u0443\u0447\u0435\u0442\u043d\u0443\u044e \u0437\u0430\u043f\u0438\u0441\u044c", -"label.add.acl": "\u0414\u043e\u0431\u0430\u0432\u0438\u0442\u044c ACL", -"label.add.acl.list": "Add ACL List", +"label.add.acl.rule": "\u0414\u043e\u0431\u0430\u0432\u0438\u0442\u044c ACL", +"label.add.acl": "Add ACL", "label.add.affinity.group": "\u0414\u043e\u0431\u0430\u0432\u0438\u0442\u044c \u043d\u043e\u0432\u0443\u044e affinity group", "label.add.baremetal.dhcp.device": "Add Baremetal DHCP Device", "label.add.bigswitchbcf.device": "Add BigSwitch BCF Controller", @@ -142,12 +142,12 @@ "label.add.ip.range": "\u0414\u043e\u0431\u0430\u0432\u0438\u0442\u044c \u0434\u0438\u0430\u043f\u0430\u0437\u043e\u043d \u0430\u0434\u0440\u0435\u0441\u043e\u0432", "label.add.isolated.network": "\u0414\u043e\u0431\u0430\u0432\u0438\u0442\u044c \u0438\u0437\u043e\u043b\u0438\u0440\u043e\u0432\u0430\u043d\u043d\u0443\u044e \u0441\u0435\u0442\u044c", "label.add.ldap.account": "\u0414\u043e\u0431\u0430\u0432\u0438\u0442\u044c LDAP \u0430\u043a\u043a\u0430\u0443\u043d\u0442", -"label.add.list.name": "ACL List Name", +"label.add.acl.name": "ACL Name", "label.add.more": "\u0414\u043e\u0431\u0430\u0432\u0438\u0442\u044c \u0447\u0442\u043e-\u0442\u043e \u0435\u0449\u0435", "label.add.netscaler.device": "\u0414\u043e\u0431\u0430\u0432\u0438\u0442\u044c Netscaler \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e", "label.add.network": "\u0414\u043e\u0431\u0430\u0432\u0438\u0442\u044c \u0441\u0435\u0442\u044c", "label.add.network.acl": "\u0414\u043e\u0431\u0430\u0432\u0438\u0442\u044c \u0441\u0435\u0442\u0435\u0432\u0443\u044e ACL", -"label.add.network.acl.list": "Add Network ACL List", +"label.add.network.acl": "Add Network ACL", "label.add.network.offering": "\u0414\u043e\u0431\u0430\u0432\u0438\u0442\u044c \u0441\u0435\u0442\u0435\u0432\u044b\u0435 \u0440\u0435\u0441\u0443\u0440\u0441\u044b", "label.add.new.gateway": "\u0414\u043e\u0431\u0430\u0432\u0438\u0442\u044c \u043d\u043e\u0432\u044b\u0439 \u0448\u043b\u044e\u0437", "label.add.new.tier": "\u0414\u043e\u0431\u0430\u0432\u0438\u0442\u044c \u043d\u043e\u0432\u044b\u0439 Tier", @@ -334,7 +334,7 @@ "label.default.use": "\u041f\u043e \u0443\u043c\u043e\u043b\u0447\u0430\u043d\u0438\u044e", "label.default.view": "\u0421\u0442\u0430\u043d\u0434\u0430\u0440\u0442\u043d\u044b\u0439 \u0432\u0438\u0434", "label.delete": "\u0423\u0434\u0430\u043b\u0438\u0442\u044c", -"label.delete.acl.list": "Delete ACL List", +"label.delete.acl": "Delete ACL", "label.delete.affinity.group": "\u0423\u0434\u0430\u043b\u0438\u0442\u044c affinity group", "label.delete.alerts": "\u0423\u0434\u0430\u043b\u0438\u0442\u044c \u0442\u0440\u0435\u0432\u043e\u0433\u0438", "label.delete.bigswitchbcf": "Remove BigSwitch BCF Controller", @@ -418,7 +418,7 @@ "label.dpd": "Dead Peer Detection", "label.driver": "Driver", "label.edit": "\u0420\u0435\u0434\u0430\u043a\u0442\u0438\u0440\u043e\u0432\u0430\u0442\u044c", -"label.edit.acl.list": "Edit ACL List", +"label.edit.acl": "Edit ACL", "label.edit.acl.rule": "Edit ACL rule", "label.edit.project.details": "\u0420\u0435\u0434\u0430\u043a\u0442\u0438\u0440\u043e\u0432\u0430\u0442\u044c \u0434\u0435\u0442\u0430\u043b\u0438 \u043f\u0440\u043e\u0435\u043a\u0442\u0430", "label.edit.role": "Edit Role", @@ -923,7 +923,6 @@ "label.remove.vpc.offering": "\u0423\u0434\u0430\u043b\u0438\u0442\u044c \u0443\u0441\u043b\u0443\u0433\u0443 VPC", "label.removing": "\u0423\u0434\u0430\u043b\u0435\u043d\u0438\u0435", "label.replace.acl": "Replace ACL", -"label.replace.acl.list": "Replace ACL List", "label.required": "\u0422\u0440\u0435\u0431\u0443\u0435\u0442\u0441\u044f", "label.requireshvm": "HVM", "label.requiresupgrade": "Requires Upgrade", @@ -1149,8 +1148,7 @@ "label.usehttps": "\u0418\u0441\u043f\u043e\u043b\u044c\u0437\u0443\u0439\u0442\u0435 HTTPS", "label.usenewdiskoffering": "Replace disk offering?", "label.user": "\u041f\u043e\u043b\u044c\u0437\u043e\u0432\u0430\u0442\u0435\u043b\u044c", -"label.userdata": "Userdata", -"label.userdatal2": "User Data", +"label.user.data": "User Data", "label.username": "\u0418\u043c\u044f \u043f\u043e\u043b\u044c\u0437\u043e\u0432\u0430\u0442\u0435\u043b\u044f", "label.users": "\u041f\u043e\u043b\u044c\u0437\u043e\u0432\u0430\u0442\u0435\u043b\u0438", "label.utilization": "Utilisation", @@ -1328,7 +1326,7 @@ "message.confirm.archive.selected.alerts": "Please confirm you would like to archive the selected alerts", "message.confirm.archive.selected.events": "Please confirm you would like to archive the selected events", "message.confirm.attach.disk": "Are you sure you want to attach disk?", -"message.confirm.delete.acl.list": "Are you sure you want to delete this ACL list?", +"message.confirm.delete.acl": "Are you sure you want to delete this ACL?", "message.confirm.delete.bigswitchbcf": "Please confirm that you would like to delete this BigSwitch BCF Controller", "message.confirm.delete.brocadevcs": "Please confirm that you would like to delete Brocade Vcs Switch", "message.confirm.delete.ciscoasa1000v": "Please confirm you want to delete CiscoASA1000v", diff --git a/ui/public/locales/zh_CN.json b/ui/public/locales/zh_CN.json index fc3a456a8fb..d09f960edf5 100644 --- a/ui/public/locales/zh_CN.json +++ b/ui/public/locales/zh_CN.json @@ -66,12 +66,12 @@ "label.acl.export": "\u5BFC\u51FA ACL \u89C4\u5219", "label.acl.id": "ACL \u6807\u8BC6\u7801", - "label.acl.list.rules": "ACL\u5217\u8868\u7B56\u7565", + "label.acl.rules": "ACL\u5217\u8868\u7B56\u7565", "label.acl.reason.description": "\u8F93\u5165\u5B9A\u4E49 ACL \u7B56\u7565\u7684\u539F\u56E0\u3002", "label.acl.replaced": "ACL \u5DF2\u66FF\u6362", "label.aclid": "\u8BBF\u95EE\u63A7\u5236\uFF08ACL\uFF09", - "label.aclname": "ACL \u540D\u79F0", + "label.acl.rule.name": "ACL \u540D\u79F0", "label.acltotal": "\u7F51\u7EDC ACL \u603B\u6570", "label.acquire.new.ip": "\u83B7\u53D6\u65B0 IP \u5730\u5740", "label.acquire.new.secondary.ip": "\u83B7\u53D6\u65B0\u4E8C\u7EA7 IP \u5730\u5740", @@ -336,8 +336,8 @@ "label.add.account": "\u6DFB\u52A0\u5E10\u6237", "label.add.accounts": "\u6DFB\u52A0\u5E10\u6237", "label.add.accounts.to": "\u6DFB\u52A0\u5E10\u6237\u81F3", - "label.add.acl": "\u6DFB\u52A0 ACL", - "label.add.acl.list": "\u6DFB\u52A0 ACL \u5217\u8868", + "label.add.acl.rule": "\u6DFB\u52A0 ACL", + "label.add.acl": "\u6DFB\u52A0 ACL \u5217\u8868", "label.add.affinity.group": "\u6DFB\u52A0\u65B0\u5173\u8054\u6027\u7EC4", "label.add.baremetal.dhcp.device": "\u6DFB\u52A0\u88F8\u673A DHCP \u8BBE\u5907", @@ -381,7 +381,7 @@ "label.add.l2.guest.network": "\u6DFB\u52A0 L2 \u6765\u5BBE\u7F51\u7EDC", "label.add.ldap.account": "\u6DFB\u52A0 LDAP \u8D26\u6237", "label.add.ldap.list.users": "\u5217\u51FA LDAP \u7528\u6237", - "label.add.list.name": "ACL \u5217\u8868\u540D\u79F0", + "label.add.acl.name": "ACL \u5217\u8868\u540D\u79F0", "label.add.load.balancer": "\u6DFB\u52A0\u8D1F\u8F7D\u5747\u8861\u5668", "label.add.management.ip.range": "\u6DFB\u52A0\u7BA1\u7406 IP \u5730\u5740\u8303\u56F4", @@ -389,7 +389,7 @@ "label.add.netscaler.device": "\u6DFB\u52A0 Netscaler \u8BBE\u5907", "label.add.network": "\u6DFB\u52A0\u7F51\u7EDC", "label.add.network.acl": "\u6DFB\u52A0\u7F51\u7EDC ACL", - "label.add.network.acl.list": "\u6DFB\u52A0\u7F51\u7EDC ACL \u5217\u8868", + "label.add.network.acl": "\u6DFB\u52A0\u7F51\u7EDC ACL \u5217\u8868", "label.add.network.device": "\u6DFB\u52A0\u7F51\u7EDC\u8BBE\u5907", "label.add.network.offering": "\u6DFB\u52A0\u7F51\u7EDC\u65B9\u6848", @@ -829,7 +829,7 @@ "label.defaultnetwork": "\u9ED8\u8BA4\u7F51\u7EDC", "label.delete": "\u5220\u9664", - "label.delete.acl.list": "\u5220\u9664 ACL \u5217\u8868", + "label.delete.acl": "\u5220\u9664 ACL \u5217\u8868", "label.delete.affinity.group": "\u5220\u9664\u5173\u8054\u6027\u7EC4", "label.delete.alerts": "\u5220\u9664\u8B66\u62A5", "label.delete.backup": "\u5220\u9664\u5907\u4EFD", @@ -1007,7 +1007,7 @@ "label.computeonly.offering.tooltip": "\u5728\u8BA1\u7B97\u65B9\u6848\u4E2D\u6307\u5B9A\u4E0E\u6839\u78C1\u76D8\u76F8\u5173\u7684\u4FE1\u606F\u6216\u5C06\u78C1\u76D8\u65B9\u6848\u76F4\u63A5\u94FE\u63A5\u5230\u8BA1\u7B97\u65B9\u6848\u7684\u9009\u9879", "label.edit": "\u7F16\u8F91", - "label.edit.acl.list": "\u7F16\u8F91 ACL \u5217\u8868", + "label.edit.acl": "\u7F16\u8F91 ACL \u5217\u8868", "label.edit.acl.rule": "\u7F16\u8F91 ACL \u89C4\u5219", "label.edit.affinity.group": "\u7F16\u8F91\u5173\u8054\u6027\u7EC4", "label.edit.lb.rule": "\u7F16\u8F91\u8D1F\u8F7D\u5747\u8861\u5668\u89C4\u5219", @@ -1717,7 +1717,7 @@ "label.netscaler.vpx": "NetScaler VPX \u8D1F\u8F7D\u5747\u8861\u5668", "label.network": "\u7F51\u7EDC", "label.network.acl": "\u7F51\u7EDC ACL", - "label.network.acl.lists": "\u7F51\u7EDC ACL \u5217\u8868", + "label.network.acls": "\u7F51\u7EDC ACL \u5217\u8868", "label.network.acls": "\u7F51\u7EDC ACL", "label.network.addvm": "\u5C06\u7F51\u7EDC\u6DFB\u52A0\u5230\u865A\u62DF\u673A", "label.network.desc": "\u7F51\u7EDC\u63CF\u8FF0", @@ -2136,7 +2136,6 @@ "label.removing.user": "\u6B63\u5728\u5220\u9664\u7528\u6237", "label.replace.acl": "\u66FF\u6362 ACL", - "label.replace.acl.list": "\u66FF\u6362 ACL \u5217\u8868", "label.report.bug": "\u62A5\u544A\u95EE\u9898", "label.required": "\u5FC5\u586B\u9879", @@ -2666,8 +2665,7 @@ "label.user.details": "\u7528\u6237\u8BE6\u60C5", "label.user.source": "\u6765\u6E90", "label.user.vm": "\u7528\u6237\u865A\u62DF\u673A", - "label.userdata": "\u7528\u6237\u6570\u636E", - "label.userdatal2": "\u7528\u6237\u6570\u636E", + "label.user.data": "\u7528\u6237\u6570\u636E", "label.username": "\u7528\u6237\u540D", "label.users": "\u7528\u6237", "label.usersource": "\u7528\u6237\u7C7B\u578B", @@ -3129,7 +3127,7 @@ "message.confirm.dedicate.pod.domain.account": "\u662F\u5426\u786E\u5B9E\u8981\u5C06\u6B64\u63D0\u4F9B\u70B9\u4E13\u7528\u4E8E\u57DF/\u5E10\u6237\uFF1F", "message.confirm.dedicate.zone": "\u662F\u5426\u8981\u5C06\u6B64\u8D44\u6E90\u57DF\u4E13\u7528\u4E8E\u57DF/\u5E10\u6237\uFF1F", - "message.confirm.delete.acl.list": "\u662F\u5426\u786E\u5B9E\u8981\u5220\u9664\u6B64 ACL \u5217\u8868\uFF1F", + "message.confirm.delete.acl": "\u662F\u5426\u786E\u5B9E\u8981\u5220\u9664\u6B64 ACL \u5217\u8868\uFF1F", "message.confirm.delete.alert": "\u662F\u5426\u786E\u5B9E\u8981\u5220\u9664\u6B64\u8B66\u62A5\uFF1F", "message.confirm.delete.baremetal.rack.configuration": "\u8BF7\u786E\u8BA4\u60A8\u786E\u5B9E\u8981\u5220\u9664 Baremetal Rack \u914D\u7F6E", "message.confirm.delete.bigswitchbcf": "\u8BF7\u786E\u8BA4\u60A8\u786E\u5B9E\u8981\u5220\u9664\u6B64 BigSwitch BCF \u63A7\u5236\u5668", diff --git a/ui/src/api/index.js b/ui/src/api/index.js index 85c46c483b2..1f532c36336 100644 --- a/ui/src/api/index.js +++ b/ui/src/api/index.js @@ -47,7 +47,7 @@ export function postAPI (command, data = {}) { params.append('response', 'json') if (data) { Object.entries(data).forEach(([key, value]) => { - if (value !== undefined && value !== null && value !== '') { + if (value !== undefined && value !== null) { params.append(key, value) } }) @@ -88,10 +88,13 @@ export function login (arg) { }) } -export function logout () { - message.destroy() - notification.destroy() - return postAPI('logout') +export async function logout () { + const result = await postAPI('logout').finally(() => { + sourceToken.cancel() + message.destroy() + notification.destroy() + }) + return result } export function oauthlogin (arg) { diff --git a/ui/src/components/CheckBoxSelectPair.vue b/ui/src/components/CheckBoxSelectPair.vue index 4fba1da2556..480e515be29 100644 --- a/ui/src/components/CheckBoxSelectPair.vue +++ b/ui/src/components/CheckBoxSelectPair.vue @@ -21,7 +21,7 @@ {{ checkBoxLabel }} @@ -32,7 +32,7 @@ :label="selectLabel"> - + @@ -29,7 +29,7 @@ icon="code" /> - + diff --git a/ui/src/components/view/DetailsTab.vue b/ui/src/components/view/DetailsTab.vue index c04a1937816..3a4d0fa043b 100644 --- a/ui/src/components/view/DetailsTab.vue +++ b/ui/src/components/view/DetailsTab.vue @@ -135,6 +135,9 @@
{{ dataResource.cniconfigname }}
+
+ {{ dataResource[item].join(', ') }} +
{{ dataResource[item] }}
@@ -173,6 +176,27 @@
{{ dataResource[item].rbd_default_data_pool }}
+ +
+ {{ $t('label.configuration.details') }} +
+
+ +
+
+
+ +
+ {{ $t('label.' + String(item).toLowerCase()) }} +
+
+ +
+
+
+ @@ -184,6 +208,8 @@ import DedicateData from './DedicateData' import HostInfo from '@/views/infra/HostInfo' import VmwareData from './VmwareData' +import ObjectListTable from '@/components/view/ObjectListTable' +import ExternalConfigurationDetails from '@/views/extension/ExternalConfigurationDetails' import { genericCompare } from '@/utils/sort' export default { @@ -191,7 +217,9 @@ export default { components: { DedicateData, HostInfo, - VmwareData + VmwareData, + ObjectListTable, + ExternalConfigurationDetails }, props: { resource: { @@ -229,13 +257,14 @@ export default { }, computed: { customDisplayItems () { - var items = ['ip4routes', 'ip6routes', 'privatemtu', 'publicmtu', 'provider', 'details'] + var items = ['ip4routes', 'ip6routes', 'privatemtu', 'publicmtu', 'provider', 'details', 'parameters'] if (this.$route.meta.name === 'webhookdeliveries') { items.push('startdate') items.push('enddate') - } - if (this.$route.meta.name === 'vm') { + } else if (this.$route.meta.name === 'vm') { items.push('leaseexpirydate') + } else if (['cluster', 'host', 'computeoffering'].includes(this.$route.meta.name)) { + items.push('externaldetails') } return items }, diff --git a/ui/src/components/view/GPUDevicesTab.vue b/ui/src/components/view/GPUDevicesTab.vue new file mode 100644 index 00000000000..ee67a81b07d --- /dev/null +++ b/ui/src/components/view/GPUDevicesTab.vue @@ -0,0 +1,902 @@ +// 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. + + + + + + diff --git a/ui/src/components/view/GPUSummaryTab.vue b/ui/src/components/view/GPUSummaryTab.vue new file mode 100644 index 00000000000..8b649e05662 --- /dev/null +++ b/ui/src/components/view/GPUSummaryTab.vue @@ -0,0 +1,301 @@ + +// 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. + + + + + + diff --git a/ui/src/components/view/GPUTab.vue b/ui/src/components/view/GPUTab.vue new file mode 100644 index 00000000000..d2cd2f3d2cb --- /dev/null +++ b/ui/src/components/view/GPUTab.vue @@ -0,0 +1,440 @@ +// 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. + + + + + + diff --git a/ui/src/components/view/ImageDeployInstanceButton.vue b/ui/src/components/view/ImageDeployInstanceButton.vue index 4f632cc0383..b2d4b55bc6a 100644 --- a/ui/src/components/view/ImageDeployInstanceButton.vue +++ b/ui/src/components/view/ImageDeployInstanceButton.vue @@ -41,7 +41,7 @@ diff --git a/ui/src/components/view/ObjectStoreBrowser.vue b/ui/src/components/view/ObjectStoreBrowser.vue index 9f94cc619c7..5507540d1f4 100644 --- a/ui/src/components/view/ObjectStoreBrowser.vue +++ b/ui/src/components/view/ObjectStoreBrowser.vue @@ -489,7 +489,12 @@ export default { asyncUploadFile (file, objectName) { return new Promise((resolve, reject) => { file.arrayBuffer().then((buffer) => { - this.client.putObject(this.resource.name, objectName, Buffer.from(buffer), file.size, this.uploadMetaData, err => { + const metadata = { + ...this.uploadMetaData, + 'Content-Type': file.type || 'binary/octet-stream' + } + + this.client.putObject(this.resource.name, objectName, Buffer.from(buffer), file.size, metadata, err => { if (err) { return reject(this.$notification.error({ message: this.$t('message.upload.failed'), diff --git a/ui/src/components/view/ResourceCountUsage.vue b/ui/src/components/view/ResourceCountUsage.vue index 265c6e78955..688a706948b 100644 --- a/ui/src/components/view/ResourceCountUsage.vue +++ b/ui/src/components/view/ResourceCountUsage.vue @@ -91,7 +91,7 @@ export default { data () { return { usageList: [ - 'vm', 'cpu', 'memory', 'primarystorage', 'volume', 'ip', 'network', + 'vm', 'cpu', 'memory', 'gpu', 'primarystorage', 'volume', 'ip', 'network', 'vpc', 'secondarystorage', 'snapshot', 'template', 'project', 'backup', 'backupstorage', 'bucket', 'objectstorage' ], taggedUsage: {}, @@ -115,6 +115,7 @@ export default { 0: 'vm', 8: 'cpu', 9: 'memory', + 16: 'gpu', 2: 'volume', 10: 'primarystorage' } diff --git a/ui/src/components/view/SearchView.vue b/ui/src/components/view/SearchView.vue index 1581e1748af..a254669b756 100644 --- a/ui/src/components/view/SearchView.vue +++ b/ui/src/components/view/SearchView.vue @@ -72,7 +72,7 @@ v-for="(opt, idx) in field.opts" :key="idx" :value="['account'].includes(field.name) ? opt.name : opt.id" - :label="$t((['storageid'].includes(field.name) || !opt.path) ? opt.name : opt.path)"> + :label="field.name === 'vgpuprofileid' ? `${opt.gpucardname} - ${opt.name}` : $t((field.name.startsWith('domain') && opt.path) ? opt.path : opt.name)">
@@ -89,7 +89,12 @@ - {{ $t((['storageid'].includes(field.name) || !opt.path) ? opt.name : opt.path) }} + + {{ opt.gpucardname }} - {{ opt.name }} + + + {{ $t((field.name.startsWith('domain') && opt.path) ? opt.path : opt.name) }} +
@@ -253,6 +258,13 @@ export default { if (fetchAccountOptions) { this.fetchDynamicFieldData('account') } + + const fetchVgpuProfileOptions = fieldname === 'gpucardid' && this.fields.some((field) => field.name === 'vgpuprofileid') + if (fetchVgpuProfileOptions) { + // Clear the currently selected vGPU profile when GPU card changes + this.form.vgpuprofileid = null + this.fetchDynamicFieldData('vgpuprofileid') + } }, onVisibleForm () { this.visibleFilter = !this.visibleFilter @@ -308,8 +320,9 @@ export default { if (['zoneid', 'domainid', 'imagestoreid', 'storageid', 'state', 'account', 'hypervisor', 'level', 'clusterid', 'podid', 'groupid', 'entitytype', 'accounttype', 'systemvmtype', 'scope', 'provider', 'type', 'scope', 'managementserverid', 'serviceofferingid', - 'diskofferingid', 'networkid', 'usagetype', 'restartrequired', - 'displaynetwork', 'guestiptype', 'usersource', 'arch', 'oscategoryid', 'templatetype'].includes(item) + 'diskofferingid', 'networkid', 'usagetype', 'restartrequired', 'gpuenabled', + 'displaynetwork', 'guestiptype', 'usersource', 'arch', 'oscategoryid', 'templatetype', 'gpucardid', 'vgpuprofileid', + 'extensionid'].includes(item) ) { type = 'list' } else if (item === 'tags') { @@ -417,6 +430,16 @@ export default { this.fields[restartRequiredIndex].loading = false } + if (arrayField.includes('gpuenabled')) { + const gpuEnabledIndex = this.fields.findIndex(item => item.name === 'gpuenabled') + this.fields[gpuEnabledIndex].loading = true + this.fields[gpuEnabledIndex].opts = [ + { id: 'true', name: 'label.yes' }, + { id: 'false', name: 'label.no' } + ] + this.fields[gpuEnabledIndex].loading = false + } + if (arrayField.includes('resourcetype')) { const resourceTypeIndex = this.fields.findIndex(item => item.name === 'resourcetype') this.fields[resourceTypeIndex].loading = true @@ -485,6 +508,9 @@ export default { let usageTypeIndex = -1 let volumeIndex = -1 let osCategoryIndex = -1 + let gpuCardIndex = -1 + let vgpuProfileIndex = -1 + let extensionIndex = -1 if (arrayField.includes('type')) { if (this.$route.path === '/alert') { @@ -504,6 +530,12 @@ export default { promises.push(await this.fetchZones(searchKeyword)) } + if (arrayField.includes('extensionid')) { + extensionIndex = this.fields.findIndex(item => item.name === 'extensionid') + this.fields[extensionIndex].loading = true + promises.push(await this.fetchExtensions(searchKeyword)) + } + if (arrayField.includes('domainid')) { domainIndex = this.fields.findIndex(item => item.name === 'domainid') this.fields[domainIndex].loading = true @@ -594,6 +626,18 @@ export default { promises.push(await this.fetchOsCategories(searchKeyword)) } + if (arrayField.includes('gpucardid')) { + gpuCardIndex = this.fields.findIndex(item => item.name === 'gpucardid') + this.fields[gpuCardIndex].loading = true + promises.push(await this.fetchGpuCards(searchKeyword)) + } + + if (arrayField.includes('vgpuprofileid')) { + vgpuProfileIndex = this.fields.findIndex(item => item.name === 'vgpuprofileid') + this.fields[vgpuProfileIndex].loading = true + promises.push(await this.fetchVgpuProfiles(searchKeyword)) + } + Promise.all(promises).then(response => { if (typeIndex > -1) { const types = response.filter(item => item.type === 'type') @@ -607,6 +651,12 @@ export default { this.fields[zoneIndex].opts = this.sortArray(zones[0].data) } } + if (extensionIndex > -1) { + const extensions = response.filter(item => item.type === 'extensionid') + if (extensions && extensions.length > 0) { + this.fields[extensionIndex].opts = this.sortArray(extensions[0].data) + } + } if (domainIndex > -1) { const domain = response.filter(item => item.type === 'domainid') if (domain && domain.length > 0) { @@ -697,6 +747,20 @@ export default { this.fields[osCategoryIndex].opts = this.sortArray(osCategories[0].data) } } + + if (gpuCardIndex > -1) { + const gpuCards = response.filter(item => item.type === 'gpucardid') + if (gpuCards && gpuCards.length > 0) { + this.fields[gpuCardIndex].opts = this.sortArray(gpuCards[0].data) + } + } + + if (vgpuProfileIndex > -1) { + const vgpuProfiles = response.filter(item => item.type === 'vgpuprofileid') + if (vgpuProfiles && vgpuProfiles.length > 0) { + this.fields[vgpuProfileIndex].opts = this.sortArray(vgpuProfiles[0].data) + } + } }).finally(() => { if (typeIndex > -1) { this.fields[typeIndex].loading = false @@ -704,6 +768,9 @@ export default { if (zoneIndex > -1) { this.fields[zoneIndex].loading = false } + if (extensionIndex > -1) { + this.fields[extensionIndex].loading = false + } if (domainIndex > -1) { this.fields[domainIndex].loading = false } @@ -746,6 +813,12 @@ export default { if (osCategoryIndex > -1) { this.fields[osCategoryIndex].loading = false } + if (gpuCardIndex > -1) { + this.fields[gpuCardIndex].loading = false + } + if (vgpuProfileIndex > -1) { + this.fields[vgpuProfileIndex].loading = false + } if (Array.isArray(arrayField)) { this.fillFormFieldValues() } @@ -796,6 +869,19 @@ export default { }) }) }, + fetchExtensions (searchKeyword) { + return new Promise((resolve, reject) => { + getAPI('listExtensions', { details: 'min', showicon: true, keyword: searchKeyword }).then(json => { + const extensions = json.listextensionsresponse.extension + resolve({ + type: 'extensionid', + data: extensions + }) + }).catch(error => { + reject(error.response.headers['x-description']) + }) + }) + }, fetchDomains (searchKeyword) { return new Promise((resolve, reject) => { getAPI('listDomains', { listAll: true, details: 'min', showicon: true, keyword: searchKeyword }).then(json => { @@ -1006,7 +1092,7 @@ export default { }, fetchVolumes (searchKeyword) { return new Promise((resolve, reject) => { - getAPI('listvolumes', { listAll: true, isencrypted: searchKeyword }).then(json => { + getAPI('listVolumes', { listAll: true, isencrypted: searchKeyword }).then(json => { const volumes = json.listvolumesresponse.volume resolve({ type: 'isencrypted', @@ -1373,6 +1459,39 @@ export default { } ] }, + fetchGpuCards (searchKeyword) { + return new Promise((resolve, reject) => { + getAPI('listGpuCards', { keyword: searchKeyword }).then(json => { + const gpuCards = json.listgpucardsresponse.gpucard + resolve({ + type: 'gpucardid', + data: gpuCards + }) + }).catch(error => { + reject(error.response.headers['x-description']) + }) + }) + }, + fetchVgpuProfiles (searchKeyword) { + return new Promise((resolve, reject) => { + const params = { keyword: searchKeyword } + + // If a GPU card is selected, filter vGPU profiles by that GPU card + if (this.form.gpucardid) { + params.gpucardid = this.form.gpucardid + } + + getAPI('listVgpuProfiles', params).then(json => { + const vgpuProfiles = json.listvgpuprofilesresponse.vgpuprofile + resolve({ + type: 'vgpuprofileid', + data: vgpuProfiles + }) + }).catch(error => { + reject(error.response.headers['x-description']) + }) + }) + }, onSearch (value) { this.paramsFilter = {} this.searchQuery = value @@ -1394,6 +1513,13 @@ export default { await this.fetchDynamicFieldData('account') } + const refreshVgpuProfileOptions = ['gpucardid', 'vgpuprofileid'].every((field) => { + return this.fields.some((searchViewField) => searchViewField.name === field) + }) + if (refreshVgpuProfileOptions) { + await this.fetchDynamicFieldData('vgpuprofileid') + } + this.$emit('search', this.paramsFilter) }, handleSubmit () { diff --git a/ui/src/components/view/VgpuProfilesTab.vue b/ui/src/components/view/VgpuProfilesTab.vue new file mode 100644 index 00000000000..b5c2af591c2 --- /dev/null +++ b/ui/src/components/view/VgpuProfilesTab.vue @@ -0,0 +1,405 @@ +// 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. + + + + diff --git a/ui/src/components/widgets/DetailsInput.vue b/ui/src/components/widgets/DetailsInput.vue new file mode 100644 index 00000000000..eeab0e3d528 --- /dev/null +++ b/ui/src/components/widgets/DetailsInput.vue @@ -0,0 +1,186 @@ +// 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. + + + + + diff --git a/ui/src/components/widgets/Status.vue b/ui/src/components/widgets/Status.vue index 22a8236d6cd..6b9f71b5de8 100644 --- a/ui/src/components/widgets/Status.vue +++ b/ui/src/components/widgets/Status.vue @@ -88,6 +88,9 @@ export default { case 'ReadWrite': state = this.$t('state.readwrite') break + case 'partiallyallocated': + state = this.$t('state.partiallyallocated') + break case 'InProgress': state = this.$t('state.inprogress') break @@ -97,6 +100,12 @@ export default { case 'Up': state = this.$t('state.up') break + case 'Yes': + state = this.$t('label.yes') + break + case 'no': + state = this.$t('label.no') + break } return state.charAt(0).toUpperCase() + state.slice(1) } @@ -124,6 +133,8 @@ export default { case 'success': case 'poweron': case 'primary': + case 'managed': + case 'yes': status = 'success' break case 'alert': @@ -138,6 +149,8 @@ export default { case 'poweroff': case 'stopped': case 'failed': + case 'unmanaged': + case 'no': status = 'error' break case 'migrating': @@ -161,6 +174,7 @@ export default { case 'unsecure': case 'warning': case 'backup': + case 'partiallyallocated': status = 'warning' break } diff --git a/ui/src/config/router.js b/ui/src/config/router.js index aa85f452b73..582fbaaf2f3 100644 --- a/ui/src/config/router.js +++ b/ui/src/config/router.js @@ -38,6 +38,8 @@ import infra from '@/config/section/infra' import zone from '@/config/section/zone' import offering from '@/config/section/offering' import config from '@/config/section/config' +import extension from '@/config/section/extension' +import customaction from '@/config/section/extension/customaction' import tools from '@/config/section/tools' import quota from '@/config/section/plugin/quota' import cloudian from '@/config/section/plugin/cloudian' @@ -221,6 +223,8 @@ export function asyncRouterMap () { generateRouterMap(zone), generateRouterMap(offering), generateRouterMap(config), + generateRouterMap(extension), + generateRouterMap(customaction), generateRouterMap(tools), generateRouterMap(quota), generateRouterMap(cloudian), diff --git a/ui/src/config/section/account.js b/ui/src/config/section/account.js index 57416585e18..55b950d3901 100644 --- a/ui/src/config/section/account.js +++ b/ui/src/config/section/account.js @@ -55,7 +55,7 @@ export default { param: 'account' }, { name: 'userdata', - title: 'label.userdata', + title: 'label.user.data', param: 'account' }, { name: 'template', diff --git a/ui/src/config/section/compute.js b/ui/src/config/section/compute.js index c5647fd2804..3c8b7ed227f 100644 --- a/ui/src/config/section/compute.js +++ b/ui/src/config/section/compute.js @@ -54,8 +54,8 @@ export default { const metricsFields = ['cpunumber', 'cputotal', 'cpuused', 'memorytotal', { memoryused: (record) => { - if (record.memoryintfreekbs <= 0 || record.memorykbs <= 0) { - return '-' + if (!record.memoryintfreekbs || record.memoryintfreekbs <= 0 || record.memorykbs <= 0) { + return '' } return parseFloat(100.0 * (record.memorykbs - record.memoryintfreekbs) / record.memorykbs).toFixed(2) + '%' } @@ -81,10 +81,10 @@ export default { fields.push('zonename') return fields }, - searchFilters: ['name', 'zoneid', 'domainid', 'account', 'groupid', 'arch', 'tags'], + searchFilters: ['name', 'gpuenabled', 'zoneid', 'domainid', 'account', 'groupid', 'arch', 'extensionid', 'tags'], details: () => { var fields = ['name', 'displayname', 'id', 'state', 'ipaddress', 'ip6address', 'templatename', 'ostypename', - 'serviceofferingname', 'isdynamicallyscalable', 'haenable', 'hypervisor', 'arch', 'boottype', 'bootmode', 'account', + 'serviceofferingname', 'gpucount', 'isdynamicallyscalable', 'haenable', 'hypervisor', 'arch', 'boottype', 'bootmode', 'account', 'domain', 'zonename', 'userdataid', 'userdataname', 'userdataparams', 'userdatadetails', 'userdatapolicy', 'hostcontrolstate', 'deleteprotection', 'leaseexpirydate', 'leaseexpiryaction'] const listZoneHaveSGEnabled = store.getters.zones.filter(zone => zone.securitygroupsenabled === true) @@ -182,7 +182,7 @@ export default { message: 'message.reinstall.vm', dataView: true, popup: true, - show: (record) => { return ['Running', 'Stopped'].includes(record.state) && record.vmtype !== 'sharedfsvm' }, + show: (record) => { return record.hypervisor !== 'External' && ['Running', 'Stopped'].includes(record.state) && record.vmtype !== 'sharedfsvm' }, disabled: (record) => { return record.hostcontrolstate === 'Offline' }, component: shallowRef(defineAsyncComponent(() => import('@/views/compute/ReinstallVm.vue'))) }, @@ -200,9 +200,9 @@ export default { return args }, show: (record) => { - return (((['Running'].includes(record.state) && record.hypervisor !== 'LXC') || - (['Stopped'].includes(record.state) && ((record.hypervisor !== 'KVM' && record.hypervisor !== 'LXC') || - (record.hypervisor === 'KVM' && record.pooltype === 'PowerFlex')))) && record.vmtype !== 'sharedfsvm') + return record.hypervisor !== 'External' && (((['Running'].includes(record.state) && record.hypervisor !== 'LXC') || + (['Stopped'].includes(record.state) && (!['KVM', 'LXC'].includes(record.hypervisor) || + (record.hypervisor === 'KVM' && ['PowerFlex', 'Filesystem', 'NetworkFilesystem', 'SharedMountPoint'].includes(record.pooltype))))) && record.vmtype !== 'sharedfsvm') }, disabled: (record) => { return record.hostcontrolstate === 'Offline' && record.hypervisor === 'KVM' }, mapping: { @@ -219,9 +219,9 @@ export default { dataView: true, popup: true, show: (record, store) => { - return (record.hypervisor !== 'KVM') || + return record.hypervisor !== 'External' && ((record.hypervisor !== 'KVM') || ['Stopped', 'Destroyed'].includes(record.state) || - store.features.kvmsnapshotenabled + store.features.kvmsnapshotenabled) }, disabled: (record) => { return record.hostcontrolstate === 'Offline' && record.hypervisor === 'KVM' }, component: shallowRef(defineAsyncComponent(() => import('@/views/compute/CreateSnapshotWizard.vue'))) @@ -234,7 +234,7 @@ export default { docHelp: 'adminguide/virtual_machines.html#backup-offerings', dataView: true, args: ['virtualmachineid', 'backupofferingid'], - show: (record) => { return !record.backupofferingid }, + show: (record) => { return record.hypervisor !== 'External' && !record.backupofferingid }, mapping: { backupofferingid: { api: 'listBackupOfferings', @@ -300,7 +300,7 @@ export default { docHelp: 'adminguide/templates.html#attaching-an-iso-to-a-vm', dataView: true, popup: true, - show: (record) => { return ['Running', 'Stopped'].includes(record.state) && !record.isoid && record.vmtype !== 'sharedfsvm' }, + show: (record) => { return record.hypervisor !== 'External' && ['Running', 'Stopped'].includes(record.state) && !record.isoid && record.vmtype !== 'sharedfsvm' }, disabled: (record) => { return record.hostcontrolstate === 'Offline' || record.hostcontrolstate === 'Maintenance' }, component: shallowRef(defineAsyncComponent(() => import('@/views/compute/AttachIso.vue'))) }, @@ -317,7 +317,7 @@ export default { } return args }, - show: (record) => { return ['Running', 'Stopped'].includes(record.state) && 'isoid' in record && record.isoid && record.vmtype !== 'sharedfsvm' }, + show: (record) => { return record.hypervisor !== 'External' && ['Running', 'Stopped'].includes(record.state) && 'isoid' in record && record.isoid && record.vmtype !== 'sharedfsvm' }, disabled: (record) => { return record.hostcontrolstate === 'Offline' || record.hostcontrolstate === 'Maintenance' }, mapping: { virtualmachineid: { @@ -332,7 +332,7 @@ export default { docHelp: 'adminguide/virtual_machines.html#change-affinity-group-for-an-existing-vm', dataView: true, args: ['affinitygroupids'], - show: (record) => { return ['Stopped'].includes(record.state) && record.vmtype !== 'sharedfsvm' }, + show: (record) => { return record.hypervisor !== 'External' && ['Stopped'].includes(record.state) && record.vmtype !== 'sharedfsvm' }, component: shallowRef(defineAsyncComponent(() => import('@/views/compute/ChangeAffinity'))), popup: true }, @@ -342,7 +342,7 @@ export default { label: 'label.scale.vm', docHelp: 'adminguide/virtual_machines.html#how-to-dynamically-scale-cpu-and-ram', dataView: true, - show: (record) => { return (['Stopped'].includes(record.state) || (['Running'].includes(record.state) && record.hypervisor !== 'LXC')) && record.vmtype !== 'sharedfsvm' }, + show: (record) => { return record.hypervisor !== 'External' && (['Stopped'].includes(record.state) || (['Running'].includes(record.state) && record.hypervisor !== 'LXC')) && record.vmtype !== 'sharedfsvm' }, disabled: (record) => { return record.state === 'Running' && !record.isdynamicallyscalable }, popup: true, component: shallowRef(defineAsyncComponent(() => import('@/views/compute/ScaleVM.vue'))) @@ -353,7 +353,7 @@ export default { label: 'label.migrate.instance.to.host', docHelp: 'adminguide/virtual_machines.html#moving-vms-between-hosts-manual-live-migration', dataView: true, - show: (record, store) => { return ['Running'].includes(record.state) && ['Admin'].includes(store.userInfo.roletype) }, + show: (record, store) => { return record.hypervisor !== 'External' && ['Running'].includes(record.state) && ['Admin'].includes(store.userInfo.roletype) }, disabled: (record) => { return record.hostcontrolstate === 'Offline' }, popup: true, component: shallowRef(defineAsyncComponent(() => import('@/views/compute/MigrateWizard.vue'))) @@ -365,7 +365,7 @@ export default { message: 'message.migrate.instance.to.ps', docHelp: 'adminguide/virtual_machines.html#moving-vms-between-hosts-manual-live-migration', dataView: true, - show: (record, store) => { return ['Stopped'].includes(record.state) && ['Admin'].includes(store.userInfo.roletype) }, + show: (record, store) => { return record.hypervisor !== 'External' && ['Stopped'].includes(record.state) && ['Admin'].includes(store.userInfo.roletype) }, disabled: (record) => { return record.hostcontrolstate === 'Offline' }, component: shallowRef(defineAsyncComponent(() => import('@/views/compute/MigrateVMStorage'))), popup: true @@ -377,7 +377,7 @@ export default { message: 'message.action.instance.reset.password', dataView: true, args: ['password'], - show: (record) => { return ['Stopped'].includes(record.state) && record.passwordenabled }, + show: (record) => { return record.hypervisor !== 'External' && ['Stopped'].includes(record.state) && record.passwordenabled }, response: (result) => { return { message: result.virtualmachine && result.virtualmachine.password ? `The password of VM ${result.virtualmachine.displayname} is ${result.virtualmachine.password}` : null, @@ -393,18 +393,18 @@ export default { message: 'message.desc.reset.ssh.key.pair', docHelp: 'adminguide/virtual_machines.html#resetting-ssh-keys', dataView: true, - show: (record) => { return ['Stopped'].includes(record.state) && record.vmtype !== 'sharedfsvm' }, + show: (record) => { return record.hypervisor !== 'External' && ['Stopped'].includes(record.state) && record.vmtype !== 'sharedfsvm' }, popup: true, component: shallowRef(defineAsyncComponent(() => import('@/views/compute/ResetSshKeyPair'))) }, { api: 'resetUserDataForVirtualMachine', icon: 'solution-outlined', - label: 'label.reset.userdata.on.vm', + label: 'label.reset.user.data.on.vm', message: 'message.desc.reset.userdata', docHelp: 'adminguide/virtual_machines.html#resetting-userdata', dataView: true, - show: (record) => { return ['Stopped'].includes(record.state) && record.vmtype !== 'sharedfsvm' }, + show: (record) => { return record.hypervisor !== 'External' && ['Stopped'].includes(record.state) && record.vmtype !== 'sharedfsvm' }, popup: true, component: shallowRef(defineAsyncComponent(() => import('@/views/compute/ResetUserData'))) }, @@ -415,7 +415,7 @@ export default { dataView: true, component: shallowRef(defineAsyncComponent(() => import('@/views/compute/AssignInstance'))), popup: true, - show: (record) => { return ['Stopped'].includes(record.state) && record.vmtype !== 'sharedfsvm' } + show: (record) => { return record.hypervisor !== 'External' && ['Stopped'].includes(record.state) && record.vmtype !== 'sharedfsvm' } }, { api: 'recoverVirtualMachine', @@ -423,7 +423,16 @@ export default { label: 'label.recover.vm', message: 'message.recover.vm', dataView: true, - show: (record, store) => { return ['Destroyed'].includes(record.state) && store.features.allowuserexpungerecovervm && record.vmtype !== 'sharedfsvm' } + show: (record, store) => { return record.hypervisor !== 'External' && ['Destroyed'].includes(record.state) && store.features.allowuserexpungerecovervm && record.vmtype !== 'sharedfsvm' } + }, + { + api: 'runCustomAction', + icon: 'play-square-outlined', + label: 'label.run.custom.action', + dataView: true, + component: shallowRef(defineAsyncComponent(() => import('@/views/extension/RunCustomAction'))), + popup: true, + show: (record) => { return ['External'].includes(record.hypervisor) } }, { api: 'unmanageVirtualMachine', @@ -431,7 +440,7 @@ export default { label: 'label.action.unmanage.virtualmachine', message: 'message.action.unmanage.virtualmachine', dataView: true, - show: (record) => { return ['Running', 'Stopped'].includes(record.state) && ['VMware', 'KVM'].includes(record.hypervisor) && record.vmtype !== 'sharedfsvm' } + show: (record) => { return record.hypervisor !== 'External' && ['Running', 'Stopped'].includes(record.state) && ['VMware', 'KVM'].includes(record.hypervisor) && record.vmtype !== 'sharedfsvm' } }, { api: 'expungeVirtualMachine', @@ -932,7 +941,7 @@ export default { }, { name: 'userdata', - title: 'label.user.data', + title: 'label.user.data.library', icon: 'solution-outlined', docHelp: 'adminguide/virtual_machines.html#user-data-and-meta-data', permission: ['listUserData'], @@ -971,7 +980,7 @@ export default { api: 'registerUserData', icon: 'plus-outlined', label: 'label.register.user.data', - docHelp: 'adminguide/virtual_machines.html#creating-the-ssh-keypair', + docHelp: 'adminguide/virtual_machines.html#user-data-and-meta-data', listView: true, popup: true, component: shallowRef(defineAsyncComponent(() => import('@/views/compute/RegisterUserData.vue'))) diff --git a/ui/src/config/section/config.js b/ui/src/config/section/config.js index e075506866d..a432eafc082 100644 --- a/ui/src/config/section/config.js +++ b/ui/src/config/section/config.js @@ -15,6 +15,8 @@ // specific language governing permissions and limitations // under the License. +import { shallowRef, defineAsyncComponent } from 'vue' + export default { name: 'config', title: 'label.configuration', @@ -340,6 +342,95 @@ export default { popup: true } ] + }, + { + name: 'gpucard', + title: 'label.gpu.card.types', + icon: 'laptop-outlined', + permission: ['listGpuCards'], + columns: ['name', 'deviceid', 'devicename', 'vendorid', 'vendorname'], + details: ['name', 'deviceid', 'devicename', 'vendorid', 'vendorname'], + related: [{ + name: 'gpudevices', + title: 'label.gpu.device', + param: 'gpucardid' + }, { + name: 'vgpuprofile', + title: 'label.vgpu.profile', + param: 'gpucardid' + }], + tabs: [{ + name: 'details', + component: shallowRef(defineAsyncComponent(() => import('@/components/view/DetailsTab.vue'))) + }, { + name: 'vgpu', + component: shallowRef(defineAsyncComponent(() => import('@/components/view/VgpuProfilesTab.vue'))) + }], + actions: [ + { + api: 'createGpuCard', + icon: 'plus-outlined', + label: 'label.add.gpu.card', + listView: true, + dataView: false, + args: ['name', 'deviceid', 'devicename', 'vendorid', 'vendorname', 'videoram'] + }, + { + api: 'updateGpuCard', + icon: 'edit-outlined', + label: 'label.edit', + dataView: true, + popup: true, + args: ['name', 'devicename', 'vendorname'] + }, + { + api: 'deleteGpuCard', + icon: 'delete-outlined', + label: 'label.action.delete.gpu.card', + message: 'message.action.delete.gpu.card', + dataView: true, + popup: true, + groupAction: true, + groupMap: (selection) => { return selection.map(x => { return { id: x } }) } + } + ] + }, + { + name: 'vgpuprofile', + title: 'label.vgpu.profile', + icon: 'laptop-outlined', + permission: ['listVgpuProfiles'], + hidden: true, + columns: ['name', 'gpucardname', 'description', 'videoram', 'maxheads', 'resolution', 'maxvgpuperphysicalgpu'], + details: ['gpucardname', 'name', 'description', 'videoram', 'maxheads', 'maxresolutionx', 'maxresolutiony', 'maxvgpuperphysicalgpu'], + actions: [ + { + api: 'createVgpuProfile', + icon: 'plus-outlined', + label: 'label.add.vgpu.profile', + listView: true, + dataView: false, + args: ['name', 'description', 'gpucardid', 'videoram', 'maxheads', 'maxresolutionx', 'maxresolutiony', 'maxvgpuperphysicalgpu'] + }, + { + api: 'updateVgpuProfile', + icon: 'edit-outlined', + label: 'label.edit', + dataView: true, + popup: true, + args: ['name', 'description', 'videoram', 'maxheads', 'maxresolutionx', 'maxresolutiony', 'maxvgpuperphysicalgpu'] + }, + { + api: 'deleteVgpuProfile', + icon: 'delete-outlined', + label: 'label.action.delete.vgpu.profile', + message: 'message.action.delete.vgpu.profile', + dataView: true, + popup: true, + groupAction: true, + groupMap: (selection) => { return selection.map(x => { return { id: x } }) } + } + ] } ] } diff --git a/ui/src/config/section/domain.js b/ui/src/config/section/domain.js index e6807f06278..fbe20ef8891 100644 --- a/ui/src/config/section/domain.js +++ b/ui/src/config/section/domain.js @@ -48,6 +48,11 @@ export default { name: 'template', title: 'label.templates', param: 'domainid' + }, + { + name: 'iso', + title: 'label.isos', + param: 'domainid' }], tabs: [ { diff --git a/ui/src/config/section/extension.js b/ui/src/config/section/extension.js new file mode 100644 index 00000000000..4c6d9ebf076 --- /dev/null +++ b/ui/src/config/section/extension.js @@ -0,0 +1,147 @@ +// 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 { shallowRef, defineAsyncComponent } from 'vue' +import store from '@/store' + +export default { + name: 'extension', + title: 'label.extensions', + icon: 'appstore-add-outlined', + docHelp: 'adminguide/extensions.html', + permission: ['listExtensions'], + params: (dataView) => { + const params = {} + if (!dataView) { + params.details = 'min' + } + return params + }, + resourceType: 'Extension', + columns: () => { + var fields = ['name', 'state', 'type', 'path', + { + availability: (record) => { + if (record.pathready) { + return 'Ready' + } + return 'Not Ready' + } + }, 'created'] + return fields + }, + details: ['name', 'description', 'id', 'type', 'details', 'path', 'pathready', 'isuserdefined', 'orchestratorrequirespreparevm', 'created'], + filters: ['orchestrator'], + tabs: [{ + name: 'details', + component: shallowRef(defineAsyncComponent(() => import('@/components/view/DetailsTab.vue'))) + }, + { + name: 'resources', + component: shallowRef(defineAsyncComponent(() => import('@/views/extension/ExtensionResourcesTab.vue'))) + }, + { + name: 'customactions', + component: shallowRef(defineAsyncComponent(() => import('@/views/extension/ExtensionCustomActionsTab.vue'))) + }, + { + name: 'events', + resourceType: 'Extension', + component: shallowRef(defineAsyncComponent(() => import('@/components/view/EventsTab.vue'))), + show: () => { return 'listEvents' in store.getters.apis } + }], + related: [ + { + name: 'vm', + title: 'label.instances', + param: 'extensionid' + }, + { + name: 'template', + title: 'label.templates', + param: 'extensionid' + } + ], + actions: [ + { + api: 'createExtension', + icon: 'plus-outlined', + label: 'label.create.extension', + docHelp: 'adminguide/extensions.html', + listView: true, + popup: true, + component: shallowRef(defineAsyncComponent(() => import('@/views/extension/CreateExtension.vue'))) + }, + { + api: 'updateExtension', + icon: 'edit-outlined', + label: 'label.update.extension', + dataView: true, + popup: true, + component: shallowRef(defineAsyncComponent(() => import('@/views/extension/UpdateExtension.vue'))) + }, + { + api: 'registerExtension', + icon: 'api-outlined', + label: 'label.register.extension', + message: 'message.action.register.extension', + dataView: true, + popup: true, + component: shallowRef(defineAsyncComponent(() => import('@/views/extension/RegisterExtension.vue'))) + }, + { + api: 'updateExtension', + icon: 'play-circle-outlined', + label: 'label.enable.extension', + message: 'message.confirm.enable.extension', + dataView: true, + groupAction: true, + popup: true, + defaultArgs: { state: 'Enabled' }, + groupMap: (selection) => { return selection.map(x => { return { id: x } }) }, + show: (record) => { return ['Disabled'].includes(record.state) } + }, + { + api: 'updateExtension', + icon: 'pause-circle-outlined', + label: 'label.disable.extension', + message: 'message.confirm.disable.extension', + dataView: true, + groupAction: true, + popup: true, + defaultArgs: { state: 'Disabled' }, + groupMap: (selection) => { return selection.map(x => { return { id: x } }) }, + show: (record) => { return ['Enabled'].includes(record.state) } + }, + { + api: 'deleteExtension', + icon: 'delete-outlined', + label: 'label.delete.extension', + message: 'message.action.delete.extension', + dataView: true, + popup: true, + args: ['id', 'cleanup'], + mapping: { + id: { + value: (record, params) => { return record.id } + }, + cleanup: false + }, + show: (record) => { return record.isuserdefined } + } + ] +} diff --git a/ui/src/config/section/extension/customaction.js b/ui/src/config/section/extension/customaction.js new file mode 100644 index 00000000000..e01f9bc944b --- /dev/null +++ b/ui/src/config/section/extension/customaction.js @@ -0,0 +1,94 @@ +// 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 { shallowRef, defineAsyncComponent } from 'vue' +import store from '@/store' + +export default { + name: 'customaction', + title: 'label.custom.actions', + icon: 'play-square-outlined', + docHelp: 'adminguide/extensions.html#custom-actions', + permission: ['listCustomActions'], + resourceType: 'ExtensionCustomAction', + hidden: true, + columns: ['name', 'extensionname', 'enabled', 'created'], + details: ['name', 'id', 'description', 'extensionname', 'allowedroletypes', 'resourcetype', 'parameters', 'timeout', 'successmessage', 'errormessage', 'details', 'created'], + tabs: [{ + name: 'details', + component: shallowRef(defineAsyncComponent(() => import('@/components/view/DetailsTab.vue'))) + }, + { + name: 'events', + resourceType: 'ExtensionCustomAction', + component: shallowRef(defineAsyncComponent(() => import('@/components/view/EventsTab.vue'))), + show: () => { return 'listEvents' in store.getters.apis } + }], + actions: [ + { + api: 'addCustomAction', + icon: 'plus-outlined', + label: 'label.add.custom.action', + docHelp: 'adminguide/extensions.html#custom-actions', + listView: true, + popup: true, + show: (record) => { return false }, // Hidden for now + component: shallowRef(defineAsyncComponent(() => import('@/views/extension/AddCustomAction.vue'))) + }, + { + api: 'updateCustomAction', + icon: 'edit-outlined', + label: 'label.update.custom.action', + message: 'message.action.update.extension', + dataView: true, + popup: true, + component: shallowRef(defineAsyncComponent(() => import('@/views/extension/UpdateCustomAction.vue'))) + }, + { + api: 'updateCustomAction', + icon: 'play-circle-outlined', + label: 'label.enable.custom.action', + message: 'message.confirm.enable.custom.action', + dataView: true, + groupAction: true, + popup: true, + defaultArgs: { enabled: true }, + groupMap: (selection) => { return selection.map(x => { return { id: x } }) }, + show: (record) => { return !record.enabled } + }, + { + api: 'updateCustomAction', + icon: 'pause-circle-outlined', + label: 'label.disable.custom.action', + message: 'message.confirm.disable.custom.action', + dataView: true, + groupAction: true, + popup: true, + defaultArgs: { enabled: false }, + groupMap: (selection) => { return selection.map(x => { return { id: x } }) }, + show: (record) => { return record.enabled } + }, + { + api: 'deleteCustomAction', + icon: 'delete-outlined', + label: 'label.delete.custom.action', + message: 'message.action.delete.custom.action', + dataView: true, + popup: true + } + ] +} diff --git a/ui/src/config/section/image.js b/ui/src/config/section/image.js index a00224c6377..f0458088b24 100644 --- a/ui/src/config/section/image.js +++ b/ui/src/config/section/image.js @@ -58,7 +58,7 @@ export default { return fields }, details: () => { - var fields = ['name', 'id', 'displaytext', 'checksum', 'hypervisor', 'arch', 'format', 'ostypename', 'size', 'physicalsize', 'isready', 'passwordenabled', + var fields = ['name', 'id', 'displaytext', 'checksum', 'hypervisor', 'arch', 'format', 'externalprovisioner', 'ostypename', 'size', 'physicalsize', 'isready', 'passwordenabled', 'crossZones', 'templatetype', 'directdownload', 'deployasis', 'ispublic', 'isfeatured', 'isextractable', 'isdynamicallyscalable', 'crosszones', 'type', 'account', 'domain', 'created', 'userdatadetails', 'userdatapolicy', 'forcks'] if (['Admin'].includes(store.getters.userInfo.roletype)) { @@ -67,7 +67,7 @@ export default { return fields }, searchFilters: () => { - var filters = ['name', 'zoneid', 'tags', 'arch', 'oscategoryid', 'templatetype'] + var filters = ['name', 'zoneid', 'tags', 'arch', 'oscategoryid', 'templatetype', 'extensionid'] if (['Admin', 'DomainAdmin'].includes(store.getters.userInfo.roletype)) { filters.push('storageid') filters.push('imagestoreid') diff --git a/ui/src/config/section/infra.js b/ui/src/config/section/infra.js index cb91d4d7b04..ae074002834 100644 --- a/ui/src/config/section/infra.js +++ b/ui/src/config/section/infra.js @@ -63,6 +63,27 @@ export default { permission: ['listHosts'], component: () => import('@/views/infra/CpuSockets.vue') }, + { + name: 'gpudevices', + title: 'label.gpu.devices', + icon: 'BoxPlotOutlined', + hidden: true, + permission: ['listGpuDevices'], + columns: ['busaddress', 'gpucardname', 'vgpuprofilename', 'hostname', 'virtualmachinename'], + details: ['id', 'busaddress', 'gpucardname', 'vgpuprofilename', 'hostname', 'virtualmachinename'], + searchFilters: ['gpucardid', 'vgpuprofileid'], + actions: [ + { + api: 'deleteGpuDevice', + icon: 'delete-outlined', + label: 'label.delete.gpu.device', + dataView: true, + popup: true, + groupAction: true, + groupMap: (selection) => { return selection.map(x => { return { id: x.id } }) } + } + ] + }, { name: 'metric', title: 'label.db.usage.metrics', diff --git a/ui/src/config/section/infra/clusters.js b/ui/src/config/section/infra/clusters.js index c03a1716a8d..ad6c59dda42 100644 --- a/ui/src/config/section/infra/clusters.js +++ b/ui/src/config/section/infra/clusters.js @@ -35,7 +35,7 @@ export default { fields.push('zonename') return fields }, - details: ['name', 'id', 'allocationstate', 'clustertype', 'managedstate', 'arch', 'hypervisortype', 'podname', 'zonename', 'drsimbalance', 'storageaccessgroups', 'podstorageaccessgroups', 'zonestorageaccessgroups'], + details: ['name', 'id', 'allocationstate', 'clustertype', 'managedstate', 'arch', 'hypervisortype', 'externalprovisioner', 'podname', 'zonename', 'drsimbalance', 'storageaccessgroups', 'podstorageaccessgroups', 'zonestorageaccessgroups', 'externaldetails'], related: [{ name: 'host', title: 'label.hosts', @@ -57,7 +57,8 @@ export default { component: shallowRef(defineAsyncComponent(() => import('@/components/view/SettingsTab.vue'))) }, { name: 'drs', - component: shallowRef(defineAsyncComponent(() => import('@/views/infra/ClusterDRSTab.vue'))) + component: shallowRef(defineAsyncComponent(() => import('@/views/infra/ClusterDRSTab.vue'))), + show: (resource) => { return resource.hypervisortype !== 'External' } }, { name: 'comments', component: shallowRef(defineAsyncComponent(() => import('@/components/view/AnnotationsTab.vue'))) @@ -132,7 +133,7 @@ export default { dataView: true, defaultArgs: { iterations: null }, args: ['iterations'], - show: (record) => { return record.managedstate === 'Managed' } + show: (record) => { return record.hypervisortype !== 'External' && record.managedstate === 'Managed' } }, { api: 'enableOutOfBandManagementForCluster', @@ -141,7 +142,7 @@ export default { message: 'label.outofbandmanagement.enable', dataView: true, show: (record) => { - return record?.resourcedetails?.outOfBandManagementEnabled === 'false' + return record.hypervisortype !== 'External' && record?.resourcedetails?.outOfBandManagementEnabled === 'false' }, args: ['clusterid'], mapping: { @@ -157,7 +158,7 @@ export default { message: 'label.outofbandmanagement.disable', dataView: true, show: (record) => { - return !(record?.resourcedetails?.outOfBandManagementEnabled === 'false') + return record.hypervisortype !== 'External' && !(record?.resourcedetails?.outOfBandManagementEnabled === 'false') }, args: ['clusterid'], mapping: { @@ -173,7 +174,7 @@ export default { message: 'label.ha.enable', dataView: true, show: (record) => { - return record?.resourcedetails?.resourceHAEnabled === 'false' + return record.hypervisortype !== 'External' && record?.resourcedetails?.resourceHAEnabled === 'false' }, args: ['clusterid'], mapping: { @@ -189,7 +190,7 @@ export default { message: 'label.ha.disable', dataView: true, show: (record) => { - return !(record?.resourcedetails?.resourceHAEnabled === 'false') + return record.hypervisortype !== 'External' && !(record?.resourcedetails?.resourceHAEnabled === 'false') }, args: ['clusterid'], mapping: { @@ -209,6 +210,9 @@ export default { clusterids: { value: (record) => { return record.id } } + }, + show: (record) => { + return record.hypervisortype !== 'External' } }, { diff --git a/ui/src/config/section/infra/hosts.js b/ui/src/config/section/infra/hosts.js index 474177918e4..2f27db5780b 100644 --- a/ui/src/config/section/infra/hosts.js +++ b/ui/src/config/section/infra/hosts.js @@ -36,7 +36,7 @@ export default { 'name', 'state', 'resourcestate', 'ipaddress', 'arch', 'hypervisor', { field: 'systeminstances', customTitle: 'system.vms' }, 'version' ] - const metricsFields = ['instances', 'powerstate', 'cpunumber', 'cputotalghz', 'cpuusedghz', 'cpuallocatedghz', 'memorytotalgb', 'memoryusedgb', 'memoryallocatedgb', 'networkread', 'networkwrite'] + const metricsFields = ['instances', 'powerstate', 'cpunumber', 'cputotalghz', 'cpuusedghz', 'cpuallocatedghz', 'memorytotalgb', 'memoryusedgb', 'memoryallocatedgb', 'gputotal', 'gpuused', 'networkread', 'networkwrite'] if (store.getters.metrics) { fields.push(...metricsFields) } @@ -45,10 +45,14 @@ export default { fields.push('managementservername') return fields }, - details: ['name', 'id', 'resourcestate', 'ipaddress', 'hypervisor', 'arch', 'type', 'clustername', 'podname', 'zonename', 'storageaccessgroups', 'clusterstorageaccessgroups', 'podstorageaccessgroups', 'zonestorageaccessgroups', 'managementservername', 'disconnected', 'created'], + details: ['name', 'id', 'resourcestate', 'ipaddress', 'hypervisor', 'externalprovisioner', 'arch', 'type', 'clustername', 'podname', 'zonename', 'storageaccessgroups', 'clusterstorageaccessgroups', 'podstorageaccessgroups', 'zonestorageaccessgroups', 'managementservername', 'disconnected', 'created', 'externaldetails'], tabs: [{ name: 'details', component: shallowRef(defineAsyncComponent(() => import('@/components/view/DetailsTab.vue'))) + }, { + name: 'gpu', + resourceType: 'Host', + component: shallowRef(defineAsyncComponent(() => import('@/components/view/GPUTab.vue'))) }, { name: 'events', resourceType: 'Host', @@ -87,6 +91,7 @@ export default { label: 'label.action.change.password', dataView: true, popup: true, + show: (record) => { return record.hypervisor !== 'External' }, component: shallowRef(defineAsyncComponent(() => import('@/views/infra/ChangeHostPassword.vue'))) }, { @@ -169,6 +174,7 @@ export default { docHelp: 'adminguide/hosts.html#out-of-band-management', dataView: true, popup: true, + show: (record) => { return record.hypervisor !== 'External' }, component: shallowRef(defineAsyncComponent(() => import('@/views/infra/ConfigureHostOOBM'))) }, { @@ -179,7 +185,7 @@ export default { docHelp: 'adminguide/hosts.html#out-of-band-management', dataView: true, show: (record) => { - return !(record?.outofbandmanagement?.enabled === true) + return record.hypervisor !== 'External' && !(record?.outofbandmanagement?.enabled === true) }, args: ['hostid'], mapping: { @@ -196,7 +202,7 @@ export default { docHelp: 'adminguide/hosts.html#out-of-band-management', dataView: true, show: (record) => { - return record?.outofbandmanagement?.enabled === true + return record.hypervisor !== 'External' && record?.outofbandmanagement?.enabled === true }, args: ['hostid'], mapping: { @@ -213,7 +219,7 @@ export default { docHelp: 'adminguide/hosts.html#out-of-band-management', dataView: true, show: (record) => { - return record?.outofbandmanagement?.enabled === true + return record.hypervisor !== 'External' && record?.outofbandmanagement?.enabled === true }, args: ['hostid', 'action'], mapping: { @@ -233,7 +239,7 @@ export default { docHelp: 'adminguide/hosts.html#out-of-band-management', dataView: true, show: (record) => { - return record?.outofbandmanagement?.enabled === true + return record.hypervisor !== 'External' && record?.outofbandmanagement?.enabled === true }, args: ['hostid', 'password'], mapping: { @@ -268,7 +274,7 @@ export default { docHelp: 'adminguide/reliability.html#ha-for-hosts', dataView: true, show: (record) => { - return !(record?.hostha?.haenable === true) + return record.hypervisor !== 'External' && !(record?.hostha?.haenable === true) }, args: ['hostid'], mapping: { diff --git a/ui/src/config/section/infra/phynetworks.js b/ui/src/config/section/infra/phynetworks.js index 578a12516fa..977fc984d2c 100644 --- a/ui/src/config/section/infra/phynetworks.js +++ b/ui/src/config/section/infra/phynetworks.js @@ -57,7 +57,7 @@ export default { args: ['name', 'zoneid', 'isolationmethods', 'vlan', 'tags', 'networkspeed', 'broadcastdomainrange'], mapping: { isolationmethods: { - options: ['VLAN', 'VXLAN', 'GRE', 'STT', 'BCF_SEGMENT', 'SSP', 'ODL', 'L3VPN', 'VCS'] + options: ['VLAN', 'VXLAN', 'GRE', 'STT', 'BCF_SEGMENT', 'SSP', 'ODL', 'L3VPN', 'VCS', 'NSX', 'NETRIS'] } } }, diff --git a/ui/src/config/section/infra/zones.js b/ui/src/config/section/infra/zones.js index de971858ab4..c957f885469 100644 --- a/ui/src/config/section/infra/zones.js +++ b/ui/src/config/section/infra/zones.js @@ -27,7 +27,7 @@ export default { searchFilters: ['name', 'domainid', 'tags'], columns: () => { const fields = ['name', 'allocationstate', 'type', 'networktype'] - const metricsFields = ['clusters', 'cpuused', 'cpumaxdeviation', 'cpuallocated', 'cputotal', 'memoryused', 'memorymaxdeviation', 'memoryallocated', 'memorytotal'] + const metricsFields = ['clusters', 'cpuused', 'cpumaxdeviation', 'cpuallocated', 'cputotal', 'memoryused', 'memorymaxdeviation', 'memoryallocated', 'memorytotal', 'gputotal', 'gpuused'] if (store.getters.metrics) { fields.push(...metricsFields) } diff --git a/ui/src/config/section/network.js b/ui/src/config/section/network.js index 4e58c7a4cd8..8c1ffcdf9cd 100644 --- a/ui/src/config/section/network.js +++ b/ui/src/config/section/network.js @@ -182,7 +182,7 @@ export default { { api: 'replaceNetworkACLList', icon: 'swap-outlined', - label: 'label.replace.acl.list', + label: 'label.replace.acl', message: 'message.confirm.replace.acl.new.one', docHelp: 'adminguide/networking_and_traffic.html#configuring-network-access-control-list', dataView: true, @@ -698,7 +698,7 @@ export default { { api: 'resetUserDataForVirtualMachine', icon: 'solution-outlined', - label: 'label.reset.userdata.on.vm', + label: 'label.reset.user.data.on.vm', message: 'message.desc.reset.userdata', docHelp: 'adminguide/virtual_machines.html#resetting-userdata', dataView: true, @@ -800,7 +800,7 @@ export default { }, { name: 'vpn', component: shallowRef(defineAsyncComponent(() => import('@/views/network/VpnDetails.vue'))), - show: (record) => { return record.issourcenat } + show: (record) => { return record.issourcenat || record.virtualmachinetype === 'DomainRouter' || !record.hasrules } }, { name: 'events', @@ -965,7 +965,7 @@ export default { { api: 'replaceNetworkACLList', icon: 'swap-outlined', - label: 'label.replace.acl.list', + label: 'label.replace.acl', message: 'message.confirm.replace.acl.new.one', docHelp: 'adminguide/networking_and_traffic.html#acl-on-private-gateway', dataView: true, @@ -1021,7 +1021,6 @@ export default { title: 'label.site.to.site.vpn.connections', docHelp: 'adminguide/networking_and_traffic.html#setting-up-a-site-to-site-vpn-connection', icon: 'sync-outlined', - hidden: true, permission: ['listVpnConnections'], columns: ['publicip', 'state', 'gateway', 'ipsecpsk', 'ikepolicy', 'esppolicy'], details: ['publicip', 'gateway', 'passive', 'cidrlist', 'ipsecpsk', 'ikepolicy', 'esppolicy', 'ikelifetime', 'ikeversion', 'esplifetime', 'dpd', 'splitconnections', 'forceencap', 'created'], @@ -1062,10 +1061,9 @@ export default { }, { name: 'acllist', - title: 'label.network.acl.lists', + title: 'label.network.acls', icon: 'bars-outlined', docHelp: 'adminguide/networking_and_traffic.html#configuring-network-access-control-list', - hidden: true, permission: ['listNetworkACLLists'], columns: ['name', 'description', 'id'], details: ['name', 'description', 'id'], @@ -1073,15 +1071,15 @@ export default { name: 'details', component: shallowRef(defineAsyncComponent(() => import('@/components/view/DetailsTab.vue'))) }, { - name: 'acl.list.rules', - component: shallowRef(defineAsyncComponent(() => import('@/views/network/AclListRulesTab.vue'))), + name: 'acl.rules', + component: shallowRef(defineAsyncComponent(() => import('@/views/network/AclRulesTab.vue'))), show: () => true }], actions: [ { api: 'createNetworkACLList', icon: 'plus-outlined', - label: 'label.add.acl.list', + label: 'label.add.acl', docHelp: 'adminguide/networking_and_traffic.html#creating-acl-lists', listView: true, args: ['name', 'description', 'vpcid'] @@ -1089,15 +1087,15 @@ export default { { api: 'updateNetworkACLList', icon: 'edit-outlined', - label: 'label.edit.acl.list', + label: 'label.edit.acl', dataView: true, args: ['name', 'description'] }, { api: 'deleteNetworkACLList', icon: 'delete-outlined', - label: 'label.delete.acl.list', - message: 'message.confirm.delete.acl.list', + label: 'label.delete.acl', + message: 'message.confirm.delete.acl', dataView: true } ] diff --git a/ui/src/config/section/offering.js b/ui/src/config/section/offering.js index 024573b84ca..7bdd992039c 100644 --- a/ui/src/config/section/offering.js +++ b/ui/src/config/section/offering.js @@ -16,6 +16,7 @@ // under the License. import { shallowRef, defineAsyncComponent } from 'vue' import store from '@/store' +import { getFilteredExternalDetails } from '@/utils/extension' export default { name: 'offering', @@ -29,7 +30,7 @@ export default { docHelp: 'adminguide/service_offerings.html#compute-and-disk-service-offerings', icon: 'cloud-outlined', permission: ['listServiceOfferings'], - searchFilters: ['name', 'zoneid', 'domainid', 'cpunumber', 'cpuspeed', 'memory'], + searchFilters: ['name', 'gpuenabled', 'zoneid', 'domainid', 'cpunumber', 'cpuspeed', 'memory'], params: () => { var params = {} if (['Admin', 'DomainAdmin'].includes(store.getters.userInfo.roletype)) { @@ -38,9 +39,9 @@ export default { return params }, filters: ['active', 'inactive'], - columns: ['name', 'displaytext', 'state', 'cpunumber', 'cpuspeed', 'memory', 'domain', 'zone', 'order'], + columns: ['name', 'displaytext', 'state', 'cpunumber', 'cpuspeed', 'memory', 'gpu', 'domain', 'zone', 'order'], details: () => { - var fields = ['name', 'id', 'displaytext', 'offerha', 'provisioningtype', 'storagetype', 'iscustomized', 'iscustomizediops', 'limitcpuuse', 'cpunumber', 'cpuspeed', 'memory', 'hosttags', 'tags', 'storageaccessgroups', 'storagetags', 'domain', 'zone', 'created', 'dynamicscalingenabled', 'diskofferingstrictness', 'encryptroot', 'purgeresources', 'leaseduration', 'leaseexpiryaction'] + var fields = ['name', 'id', 'displaytext', 'offerha', 'provisioningtype', 'storagetype', 'iscustomized', 'iscustomizediops', 'limitcpuuse', 'cpunumber', 'cpuspeed', 'memory', 'hosttags', 'tags', 'storageaccessgroups', 'storagetags', 'domain', 'zone', 'created', 'dynamicscalingenabled', 'diskofferingstrictness', 'encryptroot', 'purgeresources', 'leaseduration', 'gpucardid', 'gpucardname', 'vgpuprofileid', 'vgpuprofilename', 'gpucount', 'gpudisplay', 'leaseexpiryaction', 'externaldetails'] if (store.getters.apis.createServiceOffering && store.getters.apis.createServiceOffering.params.filter(x => x.name === 'storagepolicy').length > 0) { fields.splice(6, 0, 'vspherestoragepolicy') @@ -95,7 +96,12 @@ export default { label: 'label.edit', docHelp: 'adminguide/service_offerings.html#modifying-or-deleting-a-service-offering', dataView: true, - args: ['name', 'displaytext', 'storageaccessgroups', 'hosttags'] + args: ['name', 'displaytext', 'storageatags', 'hosttags', 'externaldetails'], + mapping: { + externaldetails: { + transformedvalue: (record) => { return getFilteredExternalDetails(record.serviceofferingdetails) } + } + } }, { api: 'updateServiceOffering', icon: 'lock-outlined', diff --git a/ui/src/config/section/storage.js b/ui/src/config/section/storage.js index bcd48944497..5c98a81579e 100644 --- a/ui/src/config/section/storage.js +++ b/ui/src/config/section/storage.js @@ -39,7 +39,7 @@ export default { } }, columns: () => { - const fields = ['name', 'state', 'sizegb', 'type', 'vmname', 'vmstate'] + const fields = ['name', 'state', 'size', 'type', 'vmname', 'vmstate'] const metricsFields = ['diskkbsread', 'diskkbswrite', 'diskiopstotal'] if (store.getters.userInfo.roletype === 'Admin') { diff --git a/ui/src/core/ext.js b/ui/src/core/ext.js index 06bea8cd7d2..ac8e0e45fcb 100644 --- a/ui/src/core/ext.js +++ b/ui/src/core/ext.js @@ -19,10 +19,10 @@ import { library } from '@fortawesome/fontawesome-svg-core' import { FontAwesomeIcon } from '@fortawesome/vue-fontawesome' import { faCentos, faUbuntu, faDebian, faSuse, faRedhat, faFedora, faLinux, faFreebsd, faApple, faWindows, faJava } from '@fortawesome/free-brands-svg-icons' -import { fas, faCompactDisc, faCameraRetro, faDharmachakra } from '@fortawesome/free-solid-svg-icons' +import { fas, faCompactDisc, faCameraRetro, faDharmachakra, faMicrochip } from '@fortawesome/free-solid-svg-icons' library.add(faCentos, faUbuntu, faDebian, faSuse, faRedhat, faFedora, faLinux, faFreebsd, faApple, faWindows, faJava) -library.add(fas, faCompactDisc, faCameraRetro, faDharmachakra) +library.add(fas, faCompactDisc, faCameraRetro, faDharmachakra, faMicrochip) export default { install: (app) => { diff --git a/ui/src/core/lazy_lib/icons_use.js b/ui/src/core/lazy_lib/icons_use.js index 01ffa2e6859..502eb5de0b6 100644 --- a/ui/src/core/lazy_lib/icons_use.js +++ b/ui/src/core/lazy_lib/icons_use.js @@ -19,6 +19,7 @@ import { AimOutlined, ApartmentOutlined, ApiOutlined, + AppstoreAddOutlined, AppstoreOutlined, ArrowDownOutlined, ArrowRightOutlined, @@ -82,6 +83,7 @@ import { FieldTimeOutlined, FileDoneOutlined, FileProtectOutlined, + FileSyncOutlined, FileTextOutlined, FileZipOutlined, FilterOutlined, @@ -137,6 +139,7 @@ import { PictureOutlined, PieChartOutlined, PlayCircleOutlined, + PlaySquareOutlined, PlusCircleOutlined, PlusOutlined, PlusSquareOutlined, @@ -191,6 +194,7 @@ export default { app.component('AimOutlined', AimOutlined) app.component('ApartmentOutlined', ApartmentOutlined) app.component('ApiOutlined', ApiOutlined) + app.component('AppstoreAddOutlined', AppstoreAddOutlined) app.component('AppstoreOutlined', AppstoreOutlined) app.component('ArrowDownOutlined', ArrowDownOutlined) app.component('ArrowRightOutlined', ArrowRightOutlined) @@ -254,6 +258,7 @@ export default { app.component('FieldTimeOutlined', FieldTimeOutlined) app.component('FileDoneOutlined', FileDoneOutlined) app.component('FileProtectOutlined', FileProtectOutlined) + app.component('FileSyncOutlined', FileSyncOutlined) app.component('FileTextOutlined', FileTextOutlined) app.component('FileZipOutlined', FileZipOutlined) app.component('FilterOutlined', FilterOutlined) @@ -309,6 +314,7 @@ export default { app.component('PictureOutlined', PictureOutlined) app.component('PieChartOutlined', PieChartOutlined) app.component('PlayCircleOutlined', PlayCircleOutlined) + app.component('PlaySquareOutlined', PlaySquareOutlined) app.component('PlusCircleOutlined', PlusCircleOutlined) app.component('PlusOutlined', PlusOutlined) app.component('PlusSquareOutlined', PlusSquareOutlined) diff --git a/ui/src/main.js b/ui/src/main.js index d7f32ff503d..f117fb57810 100644 --- a/ui/src/main.js +++ b/ui/src/main.js @@ -39,10 +39,14 @@ import { localesPlugin, dialogUtilPlugin, cpuArchitectureUtilPlugin, - imagesUtilPlugin + imagesUtilPlugin, + extensionsUtilPlugin } from './utils/plugins' import { VueAxios } from './utils/request' import directives from './utils/directives' +import Cookies from 'js-cookie' +import { getAPI } from '@/api' +import { applyCustomGuiTheme } from './utils/guiTheme' vueApp.use(VueAxios, router) vueApp.use(pollJobPlugin) @@ -58,6 +62,7 @@ vueApp.use(genericUtilPlugin) vueApp.use(dialogUtilPlugin) vueApp.use(cpuArchitectureUtilPlugin) vueApp.use(imagesUtilPlugin) +vueApp.use(extensionsUtilPlugin) vueApp.use(extensions) vueApp.use(directives) @@ -89,7 +94,7 @@ fetch('config.json?ts=' + Date.now()) } return response.json() }) - .then(config => { + .then(async config => { vueProps.$config = config let baseUrl = config.apiBase if (config.multipleServer) { @@ -98,6 +103,19 @@ fetch('config.json?ts=' + Date.now()) vueProps.axios.defaults.baseURL = baseUrl + const userid = Cookies.get('userid') + let accountid = null + let domainid = null + + if (userid !== undefined && Cookies.get('sessionkey')) { + await getAPI('listUsers', { userid: userid }).then(response => { + accountid = response.listusersresponse.user[0].accountid + domainid = response.listusersresponse.user[0].domainid + }) + } + + await applyCustomGuiTheme(accountid, domainid) + loadLanguageAsync().then(() => { vueApp.use(store) .use(router) diff --git a/ui/src/store/modules/user.js b/ui/src/store/modules/user.js index ffa0c2b136e..0fbdc5788c0 100644 --- a/ui/src/store/modules/user.js +++ b/ui/src/store/modules/user.js @@ -47,6 +47,10 @@ import { LATEST_CS_VERSION } from '@/store/mutation-types' +import { + applyCustomGuiTheme +} from '@/utils/guiTheme' + const user = { state: { token: '', @@ -243,7 +247,6 @@ const user = { const latestVersion = vueProps.$localStorage.get(LATEST_CS_VERSION, { version: '', fetchedTs: 0 }) commit('SET_LATEST_VERSION', latestVersion) notification.destroy() - resolve() }).catch(error => { reject(error) @@ -406,6 +409,7 @@ const user = { getAPI('listUsers', { id: Cookies.get('userid'), showicon: true }).then(response => { const result = response.listusersresponse.user[0] + applyCustomGuiTheme(result.accountid, result.domainid) commit('SET_INFO', result) commit('SET_NAME', result.firstname + ' ' + result.lastname) commit('SET_AVATAR', result.icon?.base64image || '') diff --git a/ui/src/utils/extension.js b/ui/src/utils/extension.js new file mode 100644 index 00000000000..cd8d0c4daca --- /dev/null +++ b/ui/src/utils/extension.js @@ -0,0 +1,30 @@ +// 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. + +export function getFilteredExternalDetails (details) { + if (!details || typeof details !== 'object') { + return null + } + const prefix = 'External:' + const result = {} + for (const key in details) { + if (key.startsWith(prefix)) { + result[key.substring(prefix.length)] = details[key] + } + } + return Object.keys(result).length > 0 ? result : null +} diff --git a/ui/src/utils/guiTheme.js b/ui/src/utils/guiTheme.js new file mode 100644 index 00000000000..b1a7209fd27 --- /dev/null +++ b/ui/src/utils/guiTheme.js @@ -0,0 +1,103 @@ +// 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 { vueProps } from '@/vue-app' +import { getAPI } from '@/api' + +export async function applyCustomGuiTheme (accountid, domainid) { + await fetch('config.json').then(response => response.json()).then(config => { + vueProps.$config = config + }) + + let guiTheme + + if (accountid != null) { + guiTheme = await fetchGuiTheme({ accountid: accountid }) + } + + if (guiTheme === undefined && domainid != null) { + guiTheme = await fetchGuiTheme({ domainid: domainid }) + } + + if (guiTheme === undefined) { + guiTheme = await fetchGuiTheme({ commonname: window.location.hostname }) + } + + if (guiTheme === undefined) { + guiTheme = await fetchGuiTheme({ listonlydefaulttheme: true }) + } + + await applyDynamicCustomization(guiTheme) +} + +async function fetchGuiTheme (params) { + return await getAPI('listGuiThemes', params).then(response => { + if (response.listguithemesresponse.guiThemes) { + return response.listguithemesresponse.guiThemes[0] + } + }).catch(error => { + console.error('Error fetching GUI theme:', error) + return null + }) +} + +async function applyDynamicCustomization (response) { + let jsonConfig + + if (response?.jsonconfiguration) { + jsonConfig = JSON.parse(response?.jsonconfiguration) + } + + // Sets custom GUI fields only if is not nullish. + vueProps.$config.appTitle = jsonConfig?.appTitle ?? vueProps.$config.appTitle + vueProps.$config.footer = jsonConfig?.footer ?? vueProps.$config.footer + vueProps.$config.loginFooter = jsonConfig?.loginFooter ?? vueProps.$config.loginFooter + vueProps.$config.logo = jsonConfig?.logo ?? vueProps.$config.logo + vueProps.$config.minilogo = jsonConfig?.minilogo ?? vueProps.$config.minilogo + vueProps.$config.banner = jsonConfig?.banner ?? vueProps.$config.banner + + if (jsonConfig?.error) { + vueProps.$config.error[403] = jsonConfig?.error[403] ?? vueProps.$config.error[403] + vueProps.$config.error[404] = jsonConfig?.error[404] ?? vueProps.$config.error[404] + vueProps.$config.error[500] = jsonConfig?.error[500] ?? vueProps.$config.error[500] + } + + if (jsonConfig?.plugins) { + jsonConfig.plugins.forEach(plugin => { + vueProps.$config.plugins.push(plugin) + }) + } + + vueProps.$config.favicon = jsonConfig?.favicon ?? vueProps.$config.favicon + vueProps.$config.css = response?.css ?? null + + await applyStaticCustomization(vueProps.$config.favicon, vueProps.$config.css) +} + +async function applyStaticCustomization (favicon, css) { + document.getElementById('favicon').href = favicon + + let style = document.getElementById('guiThemeCSS') + if (style != null) { + style.innerHTML = css + } else { + style = document.createElement('style') + style.setAttribute('id', 'guiThemeCSS') + style.innerHTML = css + document.body.appendChild(style) + } +} diff --git a/ui/src/utils/plugins.js b/ui/src/utils/plugins.js index a8b9b54fc9e..61456d98b12 100644 --- a/ui/src/utils/plugins.js +++ b/ui/src/utils/plugins.js @@ -32,6 +32,7 @@ export const pollJobPlugin = { * @param {String} [name=''] * @param {String} [title=''] * @param {String} [description=''] + * @param {Boolean} [showSuccessMessage=true] * @param {String} [successMessage=Success] * @param {Function} [successMethod=() => {}] * @param {String} [errorMessage=Error] @@ -49,6 +50,7 @@ export const pollJobPlugin = { name = '', title = '', description = '', + showSuccessMessage = true, successMessage = i18n.global.t('label.success'), successMethod = () => {}, errorMessage = i18n.global.t('label.error'), @@ -92,18 +94,22 @@ export const pollJobPlugin = { const result = json.queryasyncjobresultresponse eventBus.emit('update-job-details', { jobId, resourceId }) if (result.jobstatus === 1) { - var content = successMessage - if (successMessage === 'Success' && action && action.label) { - content = i18n.global.t(action.label) + if (showSuccessMessage) { + var content = successMessage + if (successMessage === 'Success' && action && action.label) { + content = i18n.global.t(action.label) + } + if (name) { + content = content + ' - ' + name + } + message.success({ + content, + key: jobId, + duration: 2 + }) + } else { + message.destroy(jobId) } - if (name) { - content = content + ' - ' + name - } - message.success({ - content, - key: jobId, - duration: 2 - }) store.dispatch('AddHeaderNotice', { key: jobId, title, @@ -384,6 +390,8 @@ export const resourceTypePlugin = { return 'kubernetes' case 'KubernetesSupportedVersion': return 'kubernetesiso' + case 'ExtensionCustomAction': + return 'customaction' case 'SystemVm': case 'PhysicalNetwork': case 'Backup': @@ -413,6 +421,7 @@ export const resourceTypePlugin = { case 'AutoScaleVmGroup': case 'QuotaTariff': case 'GuestOsCategory': + case 'Extension': return resourceType.toLowerCase() } return '' @@ -557,8 +566,12 @@ export const cpuArchitectureUtilPlugin = { export const imagesUtilPlugin = { install (app) { - app.config.globalProperties.$fetchTemplateTypes = function () { - const baseTypes = ['USER', 'VNF'] + app.config.globalProperties.$fetchTemplateTypes = function (hypervisor) { + const baseTypes = ['USER'] + if (hypervisor === 'External') { + return baseTypes.map(type => ({ id: type, name: type, description: type })) + } + baseTypes.push('VNF') const adminTypes = ['SYSTEM', 'BUILTIN', 'ROUTING'] const types = [...baseTypes] if (store.getters.userInfo?.roletype === 'Admin') { @@ -568,3 +581,19 @@ export const imagesUtilPlugin = { } } } + +export const extensionsUtilPlugin = { + install (app) { + app.config.globalProperties.$fetchCustomActionRoleTypes = function () { + const roleTypes = [] + const roleTypesList = ['Admin', 'Resource Admin', 'Domain Admin', 'User'] + roleTypesList.forEach((item) => { + roleTypes.push({ + id: item.replace(' ', ''), + description: item + }) + }) + return roleTypes + } + } +} diff --git a/ui/src/utils/request.js b/ui/src/utils/request.js index 7c757691f2b..42b26c9785b 100644 --- a/ui/src/utils/request.js +++ b/ui/src/utils/request.js @@ -149,6 +149,15 @@ const err = (error) => { service.interceptors.request.use(config => { source = sourceToken.getSource() config.cancelToken = source.token + + handleGetRequestParams(config) + + handlePostRequestParams(config) + + return config +}, err) + +function handleGetRequestParams (config) { if (config && config.params) { config.params.response = 'json' const project = vueProps.$localStorage.get(CURRENT_PROJECT) @@ -160,11 +169,30 @@ service.interceptors.request.use(config => { } } if (config.params.ignoreproject !== undefined) { - config.params.ignoreproject = null + delete config.params.ignoreproject } } - return config -}, err) +} + +function handlePostRequestParams (config) { + if (config && config.data && config.data instanceof URLSearchParams) { + const project = vueProps.$localStorage.get(CURRENT_PROJECT) + const command = config.data.get('command') + const hasProjectId = config.data.has('projectid') + const ignoreProject = config.data.has('ignoreproject') + + if (!hasProjectId && !ignoreProject && project && project.id) { + if (command === 'listTags') { + config.data.append('projectid', '-1') + } else if (command !== 'assignVirtualMachine') { + config.data.append('projectid', project.id) + } + } + if (config.data.has('ignoreproject')) { + config.data.delete('ignoreproject') + } + } +} // response interceptor service.interceptors.response.use((response) => { diff --git a/ui/src/views/AutogenView.vue b/ui/src/views/AutogenView.vue index 3199b0cdeeb..1483669a3be 100644 --- a/ui/src/views/AutogenView.vue +++ b/ui/src/views/AutogenView.vue @@ -18,9 +18,15 @@ @@ -146,7 +169,8 @@ @refresh-data="fetchData" @poll-action="pollActionCompletion" @close-action="closeAction" - @cancel-bulk-action="handleCancel"/> + @cancel-bulk-action="handleCancel" + /> @@ -170,25 +194,39 @@ v-if="currentAction.docHelp || $route.meta.docHelp" style="margin-left: 5px" :href="$config.docBase + '/' + (currentAction.docHelp || $route.meta.docHelp)" - target="_blank"> + target="_blank" + > - +
+ type="error" + > - + @@ -218,15 +256,19 @@
-
+
-
+ layout="vertical" + > +
- + :tooltip="field.description" + /> + = 0 }" > - {{ }} + {{ }} + :label="opt" + > {{ opt }} @@ -280,11 +331,15 @@ }" v-focus="fieldIndex === firstIndex" > - {{ }} + {{ }} + :label="opt.name || opt.description || opt.traffictype || opt.publicip" + > {{ opt.name || opt.description || opt.traffictype || opt.publicip }} @@ -300,44 +355,96 @@ }" v-focus="fieldIndex === firstIndex" > - {{ }} - + {{ }} +
- + - + - + - + - + - + - + - + - + - + - + - + {{ opt.name || opt.description || opt.traffictype || opt.publicip }}
@@ -359,10 +466,14 @@ - {{ opt.name && opt.type ? opt.name + ' (' + opt.type + ')' : opt.name || opt.description }} + :label="(opt.name && opt.type ? opt.name + ' (' + opt.type + ')' : opt.name || opt.description)" + > + {{ (opt.name && opt.type ? opt.name + ' (' + opt.type + ')' : opt.name || opt.description) }} + + :placeholder="field.description" + />
-
+
{{ $t('label.cancel') }} - {{ $t('label.ok') }} + {{ $t('label.ok') }}
@@ -402,16 +521,25 @@
-
+
- + + :tabs="$route.meta.tabs" + />
-
+
+ showQuickJumper + > @@ -450,7 +579,8 @@ :selectedItems="selectedItems" :selectedColumns="bulkColumns" :message="modalInfo" - @handle-cancel="handleCancel" /> + @handle-cancel="handleCancel" + />
@@ -473,6 +603,7 @@ import OsLogo from '@/components/widgets/OsLogo' import ResourceIcon from '@/components/view/ResourceIcon' import BulkActionProgress from '@/components/view/BulkActionProgress' import TooltipLabel from '@/components/widgets/TooltipLabel' +import DetailsInput from '@/components/widgets/DetailsInput' export default { name: 'Resource', @@ -485,7 +616,8 @@ export default { BulkActionProgress, TooltipLabel, OsLogo, - ResourceIcon + ResourceIcon, + DetailsInput }, mixins: [mixinDevice], provide: function () { @@ -648,7 +780,8 @@ export default { }, watch: { '$route' (to, from) { - if (to.fullPath !== from.fullPath && !to.fullPath.includes('action/') && to?.query?.tab !== 'browser') { + console.log('DEBUG - Route changed from', from.fullPath, 'to', to.fullPath) + if (to.fullPath !== from.fullPath && !to.fullPath.includes('/action/') && to?.query?.tab !== 'browser') { if ('page' in to.query) { this.page = Number(to.query.page) this.pageSize = Number(to.query.pagesize) @@ -692,7 +825,7 @@ export default { return this.$route.query.filter } const routeName = this.$route.name - if ((this.projectView && routeName === 'vm') || (['Admin', 'DomainAdmin'].includes(this.$store.getters.userInfo.roletype) && ['vm', 'iso', 'template', 'pod', 'cluster', 'host', 'systemvm', 'router', 'storagepool'].includes(routeName)) || ['account', 'guestnetwork', 'guestvlans', 'oauthsetting', 'guestos', 'guestoshypervisormapping', 'kubernetes', 'asnumbers', 'networkoffering'].includes(routeName)) { + if ((this.projectView && routeName === 'vm') || (['Admin', 'DomainAdmin'].includes(this.$store.getters.userInfo.roletype) && ['vm', 'iso', 'template', 'pod', 'cluster', 'host', 'systemvm', 'router', 'storagepool'].includes(routeName)) || ['account', 'guestnetwork', 'guestvlans', 'oauthsetting', 'guestos', 'guestoshypervisormapping', 'kubernetes', 'asnumbers', 'networkoffering', 'extension'].includes(routeName)) { return 'all' } if (['publicip'].includes(routeName)) { @@ -763,10 +896,13 @@ export default { const refreshed = ('irefresh' in params) params.listall = true + + this.dataView = !!(this.$route?.params?.id || !!this.$route?.query?.dataView) + if (this.$route.meta.params) { const metaParams = this.$route.meta.params if (typeof metaParams === 'function') { - Object.assign(params, metaParams()) + Object.assign(params, metaParams(this.dataView)) } else { Object.assign(params, metaParams) } @@ -810,14 +946,9 @@ export default { 'vpc', 'securitygroups', 'publicip', 'vpncustomergateway', 'template', 'iso', 'event', 'kubernetes', 'sharedfs', 'autoscalevmgroup', 'vnfapp', 'webhook'].includes(this.$route.name) - if ((this.$route && this.$route.params && this.$route.params.id) || this.$route.query.dataView) { - this.dataView = true - if (!refreshed) { - this.resource = {} - this.$emit('change-resource', this.resource) - } - } else { - this.dataView = false + if (this.dataView && !refreshed) { + this.resource = {} + this.$emit('change-resource', this.resource) } if (this.dataView && ['Admin'].includes(this.$store.getters.userInfo.roletype) && this.routeName === 'volume') { @@ -1424,6 +1555,15 @@ export default { this.form[field.name] = fieldValue } else if (field.type === 'boolean' && field.name === 'rebalance' && this.currentAction.api === 'cancelMaintenance') { this.form[field.name] = true + } else if (field.type === 'map') { + const transformedValue = this.currentAction.mapping?.[field.name]?.transformedvalue + if (typeof transformedValue === 'function') { + this.form[field.name] = transformedValue(this.resource) + } else if (typeof transformedValue === 'object' && transformedValue !== null) { + this.form[field.name] = { ...transformedValue } + } else { + this.form[field.name] = {} + } } }) }, @@ -1624,6 +1764,10 @@ export default { } else { params[key] = param.opts[input].name } + } else if (param.type === 'map' && typeof input === 'object') { + Object.entries(values.externaldetails).forEach(([key, value]) => { + params[param.name + '[0].' + key] = value + }) } else { params[key] = input } @@ -1819,6 +1963,12 @@ export default { } } else if (['computeoffering', 'systemoffering', 'diskoffering'].includes(this.$route.name)) { query.state = filter + } else if (['extension'].includes(this.$route.name)) { + if (filter === 'all') { + delete query.type + } else { + query.type = filter + } } query.filter = filter query.page = '1' @@ -1950,7 +2100,6 @@ export default { this.rules[field.name].push(rule) break case (this.currentAction.mapping && field.name in this.currentAction.mapping && 'options' in this.currentAction.mapping[field.name]): - console.log('op: ' + field) rule.required = field.required rule.message = this.$t('message.error.select') this.rules[field.name].push(rule) @@ -1961,20 +2110,17 @@ export default { this.rules[field.name].push(rule) break case (field.type === 'uuid'): - console.log('uuid: ' + field) rule.required = field.required rule.message = this.$t('message.error.select') this.rules[field.name].push(rule) break case (field.type === 'list'): - console.log('list: ' + field) rule.type = 'array' rule.required = field.required rule.message = this.$t('message.error.select') this.rules[field.name].push(rule) break case (field.type === 'long'): - console.log(field) rule.type = 'number' rule.required = field.required rule.message = this.$t('message.validate.number') diff --git a/ui/src/views/auth/Login.vue b/ui/src/views/auth/Login.vue index a3a20428f0b..24065f47b1a 100644 --- a/ui/src/views/auth/Login.vue +++ b/ui/src/views/auth/Login.vue @@ -91,6 +91,18 @@ type="text" :placeholder="$t('label.domain')" v-model:value="form.domain" + > + + + + + @@ -907,6 +923,7 @@ import UserDataSelection from '@views/compute/wizard/UserDataSelection' import SecurityGroupSelection from '@views/compute/wizard/SecurityGroupSelection' import TooltipLabel from '@/components/widgets/TooltipLabel' import InstanceNicsNetworkSelectListView from '@/components/view/InstanceNicsNetworkSelectListView' +import DetailsInput from '@/components/widgets/DetailsInput' export default { name: 'Wizard', @@ -931,7 +948,8 @@ export default { ComputeSelection, SecurityGroupSelection, TooltipLabel, - InstanceNicsNetworkSelectListView + InstanceNicsNetworkSelectListView, + DetailsInput }, props: { visible: { @@ -996,8 +1014,7 @@ export default { keyboards: [], bootTypes: [], bootModes: [], - ioPolicyTypes: [], - dynamicScalingVmConfig: false + ioPolicyTypes: [] }, rowCount: {}, loading: { @@ -1058,11 +1075,11 @@ export default { userDataValues: {}, templateUserDataCols: [ { - title: this.$t('label.userdata'), + title: this.$t('label.user.data'), dataIndex: 'userdata' }, { - title: this.$t('label.userdatapolicy'), + title: this.$t('label.user.data.policy'), dataIndex: 'userdataoverridepolicy' } ], @@ -1106,7 +1123,9 @@ export default { }, architectureTypes: { opts: [] - } + }, + externalDetailsEnabled: false, + selectedExtensionId: null } }, computed: { @@ -1418,11 +1437,11 @@ export default { let tabList = [] tabList = [{ key: 'userdataregistered', - tab: this.$t('label.userdata.registered') + tab: this.$t('label.user.data.registered') }, { key: 'userdatatext', - tab: this.$t('label.userdata.text') + tab: this.$t('label.user.data.text') }] return tabList @@ -1449,7 +1468,7 @@ export default { return Boolean('listUserData' in this.$store.getters.apis) }, dynamicScalingVmConfigValue () { - return this.options.dynamicScalingVmConfig?.[0]?.value === 'true' + return this.$store.getters.features.dynamicscalingenabled }, isCustomizedDiskIOPS () { return this.diskSelected?.iscustomizediops || false @@ -1474,6 +1493,9 @@ export default { }, guestOsCategoriesSelectionDisallowed () { return (!this.queryGuestOsCategoryId || this.options.guestOsCategories.length === 0) && (!!this.queryTemplateId || !!this.queryIsoId) + }, + isTemplateHypervisorExternal () { + return !!this.template && this.template.hypervisor === 'External' } }, watch: { @@ -1598,6 +1620,11 @@ export default { if (this.serviceOffering.memory) { this.vm.memory = this.serviceOffering.memory } + this.vm.gpucardid = this.serviceOffering.gpucardid ? this.serviceOffering.gpucardid : '' + this.vm.gpucardname = this.serviceOffering.gpucardname ? this.serviceOffering.gpucardname : '' + this.vm.gpucount = this.serviceOffering.gpucount ? this.serviceOffering.gpucount : 0 + this.vm.vgpuprofileid = this.serviceOffering.vgpuprofileid ? this.serviceOffering.vgpuprofileid : '' + this.vm.vgpuprofilename = this.serviceOffering.vgpuprofilename ? this.serviceOffering.vgpuprofilename : '' } if (this.template && !this.template.deployasis && this.template.childtemplates && this.template.childtemplates.length > 0) { @@ -1641,6 +1668,9 @@ export default { this.doUserdataAppend = false } }, + beforeCreate () { + this.apiParams = this.$getApiParams('deployVirtualMachine') + }, created () { this.initForm() this.dataPreFill = this.preFillContent && Object.keys(this.preFillContent).length > 0 ? this.preFillContent : {} @@ -1951,6 +1981,7 @@ export default { if (template.details['vmware-to-kvm-mac-addresses']) { this.dataPreFill.macAddressArray = JSON.parse(template.details['vmware-to-kvm-mac-addresses']) } + this.dataPreFill.hypervisorType = template.hypervisor } } else if (name === 'isoid') { this.imageType = 'isoid' @@ -2123,19 +2154,11 @@ export default { }, changeArchitecture (arch) { this.selectedArchitecture = arch - if (this.isModernImageSelection) { - this.fetchGuestOsCategories() - return - } - this.fetchImages() + this.updateImages() }, changeImageType (imageType) { this.imageType = imageType - if (this.isModernImageSelection) { - this.fetchGuestOsCategories() - } else { - this.fetchImages() - } + this.updateImages() }, handleSubmitAndStay (e) { this.form.stayonpage = true @@ -2143,7 +2166,6 @@ export default { }, handleSubmit (e) { console.log('wizard submit') - e.preventDefault() if (this.loading.deploy) return this.formRef.value.validate().then(async () => { const values = toRaw(this.form) @@ -2357,6 +2379,12 @@ export default { deployVmData.projectid = this.owner.projectid } + if (this.imageType === 'templateid' && this.template && this.template.hypervisor === 'External' && values.externaldetails) { + Object.entries(values.externaldetails).forEach(([key, value]) => { + deployVmData['externaldetails[0].' + key] = value + }) + } + const title = this.$t('label.launch.vm') const description = values.name || '' const password = this.$t('label.password') @@ -2504,7 +2532,7 @@ export default { param.loading = true param.opts = [] const options = param.options || {} - if (!('listall' in options) && !['zones', 'pods', 'clusters', 'hosts', 'dynamicScalingVmConfig', 'hypervisors'].includes(name)) { + if (!('listall' in options) && !['zones', 'pods', 'clusters', 'hosts', 'hypervisors'].includes(name)) { options.listall = true } postAPI(param.list, options).then((response) => { @@ -2513,42 +2541,51 @@ export default { if (Object.keys(responseItem).length === 0) { this.rowCount[name] = 0 this.options[name] = [] - return resolve(null) + return } if (!responseKey.includes('response')) { - return resolve(null) + return } _.map(responseItem, (response, key) => { if (key === 'count') { this.rowCount[name] = response return } - param.opts = response - this.options[name] = response - - if (name === 'hypervisors') { - const hypervisorFromResponse = response[0] && response[0].name ? response[0].name : null - this.dataPreFill.hypervisor = hypervisorFromResponse - this.form.hypervisor = hypervisorFromResponse + if (!responseKey.includes('response')) { + return resolve(null) } + _.map(responseItem, (response, key) => { + if (key === 'count') { + this.rowCount[name] = response + return + } + param.opts = response + this.options[name] = response - if (param.field) { - this.fillValue(param.field) + if (name === 'hypervisors') { + const hypervisorFromResponse = response[0] && response[0].name ? response[0].name : null + this.dataPreFill.hypervisor = hypervisorFromResponse + this.form.hypervisor = hypervisorFromResponse + } + + if (param.field) { + this.fillValue(param.field) + } + }) + + if (name === 'zones') { + let zoneid = '' + if (this.$route.query.zoneid) { + zoneid = this.$route.query.zoneid + } else if (this.options.zones.length === 1) { + zoneid = this.options.zones[0].id + } + if (zoneid) { + this.form.zoneid = zoneid + this.onSelectZoneId(zoneid) + } } }) - - if (name === 'zones') { - let zoneid = '' - if (this.$route.query.zoneid) { - zoneid = this.$route.query.zoneid - } else if (this.options.zones.length === 1) { - zoneid = this.options.zones[0].id - } - if (zoneid) { - this.form.zoneid = zoneid - this.onSelectZoneId(zoneid) - } - } }) resolve(response) }).catch(function (error) { @@ -2573,6 +2610,9 @@ export default { if (this.isZoneSelectedMultiArch) { args.arch = this.selectedArchitecture } + if (this.selectedExtensionId) { + args.extensionid = this.selectedExtensionId + } args.account = store.getters.project?.id ? null : this.owner.account args.domainid = store.getters.project?.id ? null : this.owner.domainid args.projectid = store.getters.project?.id || this.owner.projectid @@ -2772,7 +2812,7 @@ export default { this.fetchOptions(this.params.hosts, 'hosts') if (this.clusterId && Array.isArray(this.options.clusters)) { const cluster = this.options.clusters.find(c => c.id === this.clusterId) - this.handleArchResourceSelected(cluster.arch) + this.handleComputeResourceSelected(cluster) } }, onSelectHostId (value) { @@ -2782,15 +2822,40 @@ export default { } if (this.hostId && Array.isArray(this.options.hosts)) { const host = this.options.hosts.find(h => h.id === this.hostId) - this.handleArchResourceSelected(host.arch) + this.handleComputeResourceSelected(host) } }, - handleArchResourceSelected (resourceArch) { - if (!resourceArch || !this.isZoneSelectedMultiArch || this.selectedArchitecture === resourceArch) { + updateImages () { + if (this.isModernImageSelection) { + this.fetchGuestOsCategories() return } - this.selectedArchitecture = resourceArch - this.changeArchitecture(resourceArch, this.tabKey === 'templateid') + this.fetchImages() + }, + handleComputeResourceSelected (computeResource) { + if (!computeResource) { + this.selectedExtensionId = null + return + } + const resourceArch = computeResource.arch + const needArchChange = resourceArch && + this.isZoneSelectedMultiArch && + this.selectedArchitecture !== resourceArch + const resourceHypervisor = computeResource.hypervisor || computeResource.hypervisortype + const resourceExtensionId = resourceHypervisor === 'External' ? computeResource.extensionid : null + const needExtensionIdChange = this.selectedExtensionId !== resourceExtensionId + if (!needArchChange && !needExtensionIdChange) { + return + } + if (needArchChange && !needExtensionIdChange) { + this.changeArchitecture(resourceArch, this.imageType === 'templateid') + return + } + this.selectedExtensionId = resourceExtensionId + if (needArchChange) { + this.selectedArchitecture = resourceArch + } + this.updateImages() }, onSelectGuestOsCategory (value) { this.form.guestoscategoryid = value @@ -3113,6 +3178,12 @@ export default { return Promise.reject(this.$t('message.error.number')) } return Promise.resolve() + }, + onExternalDetailsEnabledChange (val) { + if (val || !this.form.externaldetails) { + return + } + this.form.externaldetails = undefined } } } @@ -3153,10 +3224,20 @@ export default { .ant-card-body { min-height: 250px; max-height: calc(100vh - 140px); + overflow: hidden; // Prevent the entire card from scrolling + } + + .card-content { + max-height: calc(100vh - 240px); // Reserve space for footer and card header/padding overflow-y: auto; scroll-behavior: smooth; } + .card-footer { + border-top: 1px solid #f0f0f0; + flex-shrink: 0; // Ensure footer doesn't shrink + } + .resource-detail-item__label { font-weight: normal; } diff --git a/ui/src/views/compute/DeployVnfAppliance.vue b/ui/src/views/compute/DeployVnfAppliance.vue index 19939634259..cf8677a2c61 100644 --- a/ui/src/views/compute/DeployVnfAppliance.vue +++ b/ui/src/views/compute/DeployVnfAppliance.vue @@ -163,6 +163,7 @@ :value="serviceOffering ? serviceOffering.id : ''" :loading="loading.serviceOfferings" :preFillContent="dataPreFill" + :show-gpu-filter="zone.gputotal && zone.gputotal > 0" :minimum-cpunumber="templateConfigurationExists && selectedTemplateConfiguration && selectedTemplateConfiguration.cpunumber ? selectedTemplateConfiguration.cpunumber : 0" :minimum-cpuspeed="templateConfigurationExists && selectedTemplateConfiguration && selectedTemplateConfiguration.cpuspeed ? selectedTemplateConfiguration.cpuspeed : 0" :minimum-memory="templateConfigurationExists && selectedTemplateConfiguration && selectedTemplateConfiguration.memory ? selectedTemplateConfiguration.memory : 0" @@ -549,7 +550,7 @@ @change="val => { dynamicscalingenabled = val }"/> - +
@@ -603,11 +604,11 @@


- {{ $t('label.userdata.do.override') }} + {{ $t('label.user.data.do.override') }} - {{ $t('label.userdata.do.append') }} + {{ $t('label.user.data.do.append') }} + @handle-deploy-menu="(index, e) => handleSubmitAndStay(e)" />
@@ -825,7 +826,7 @@ :deployButtonMenuOptions="deployMenuOptions" @handle-cancel="() => $router.back()" @handle-deploy="handleSubmit" - @handle-deploy-menu="handleSubmitAndStay" /> + @handle-deploy-menu="(index, e) => handleSubmitAndStay(e)" /> @@ -952,8 +953,7 @@ export default { keyboards: [], bootTypes: [], bootModes: [], - ioPolicyTypes: [], - dynamicScalingVmConfig: false + ioPolicyTypes: [] }, rowCount: {}, loading: { @@ -1011,11 +1011,11 @@ export default { userDataValues: {}, templateUserDataCols: [ { - title: this.$t('label.userdata'), + title: this.$t('label.user.data'), dataIndex: 'userdata' }, { - title: this.$t('label.userdatapolicy'), + title: this.$t('label.user.data.policy'), dataIndex: 'userdataoverridepolicy' } ], @@ -1290,16 +1290,17 @@ export default { }] }, userdataTabList () { - return [ - { - key: 'userdataregistered', - tab: this.$t('label.userdata.registered') - }, - { - key: 'userdatatext', - tab: this.$t('label.userdata.text') - } - ] + let tabList = [] + tabList = [{ + key: 'userdataregistered', + tab: this.$t('label.user.data.registered') + }, + { + key: 'userdatatext', + tab: this.$t('label.user.data.text') + }] + + return tabList }, showVnfNicsSection () { return this.networks && this.networks.length > 0 && this.vm.templateid && this.templateVnfNics && this.templateVnfNics.length > 0 @@ -1330,7 +1331,7 @@ export default { return Boolean('listUserData' in this.$store.getters.apis) }, dynamicScalingVmConfigValue () { - return this.options.dynamicScalingVmConfig?.[0]?.value === 'true' + return this.$store.getters.features.dynamicscalingenabled }, isCustomizedDiskIOPS () { return this.diskSelected?.iscustomizediops || false @@ -2475,7 +2476,7 @@ export default { param.loading = true param.opts = [] const options = param.options || {} - if (!('listall' in options) && !['zones', 'pods', 'clusters', 'hosts', 'dynamicScalingVmConfig', 'hypervisors'].includes(name)) { + if (!('listall' in options) && !['zones', 'pods', 'clusters', 'hosts', 'hypervisors'].includes(name)) { options.listall = true } postAPI(param.list, options).then((response) => { @@ -2484,42 +2485,51 @@ export default { if (Object.keys(responseItem).length === 0) { this.rowCount[name] = 0 this.options[name] = [] - return resolve(null) + return } if (!responseKey.includes('response')) { - return resolve(null) + return } _.map(responseItem, (response, key) => { if (key === 'count') { this.rowCount[name] = response return } - param.opts = response - this.options[name] = response - - if (name === 'hypervisors') { - const hypervisorFromResponse = response[0] && response[0].name ? response[0].name : null - this.dataPreFill.hypervisor = hypervisorFromResponse - this.form.hypervisor = hypervisorFromResponse + if (!responseKey.includes('response')) { + return resolve(null) } + _.map(responseItem, (response, key) => { + if (key === 'count') { + this.rowCount[name] = response + return + } + param.opts = response + this.options[name] = response - if (param.field) { - this.fillValue(param.field) + if (name === 'hypervisors') { + const hypervisorFromResponse = response[0] && response[0].name ? response[0].name : null + this.dataPreFill.hypervisor = hypervisorFromResponse + this.form.hypervisor = hypervisorFromResponse + } + + if (param.field) { + this.fillValue(param.field) + } + }) + + if (name === 'zones') { + let zoneid = '' + if (this.$route.query.zoneid) { + zoneid = this.$route.query.zoneid + } else if (this.options.zones.length === 1) { + zoneid = this.options.zones[0].id + } + if (zoneid) { + this.form.zoneid = zoneid + this.onSelectZoneId(zoneid) + } } }) - - if (name === 'zones') { - let zoneid = '' - if (this.$route.query.zoneid) { - zoneid = this.$route.query.zoneid - } else if (this.options.zones.length === 1) { - zoneid = this.options.zones[0].id - } - if (zoneid) { - this.form.zoneid = zoneid - this.onSelectZoneId(zoneid) - } - } }) resolve(response) }).catch(function (error) { diff --git a/ui/src/views/compute/EditVM.vue b/ui/src/views/compute/EditVM.vue index f7b7649b96b..e35ca6dd49d 100644 --- a/ui/src/views/compute/EditVM.vue +++ b/ui/src/views/compute/EditVM.vue @@ -86,7 +86,7 @@
@@ -180,7 +180,6 @@ export default { template: {}, userDataEnabled: false, securityGroupsEnabled: false, - dynamicScalingVmConfig: false, loading: false, securitygroups: { loading: false, @@ -239,7 +238,6 @@ export default { this.fetchInstaceGroups() this.fetchServiceOfferingData() this.fetchTemplateData() - this.fetchDynamicScalingVmConfig() this.fetchUserData() }, fetchZoneDetails () { @@ -304,6 +302,9 @@ export default { canDynamicScalingEnabled () { return this.template.isdynamicallyscalable && this.serviceOffering.dynamicscalingenabled && this.dynamicScalingVmConfig }, + isDynamicScalingEnabled () { + return this.template.isdynamicallyscalable && this.serviceOffering.dynamicscalingenabled && this.$store.getters.features.dynamicscalingenabled + }, fetchOsTypes () { this.osTypes.loading = true this.osTypes.opts = [] diff --git a/ui/src/views/compute/InstanceTab.vue b/ui/src/views/compute/InstanceTab.vue index fa50e6e79b7..291f5510e03 100644 --- a/ui/src/views/compute/InstanceTab.vue +++ b/ui/src/views/compute/InstanceTab.vue @@ -39,11 +39,20 @@ style="width: 100%; margin-bottom: 10px" @click="showAddVolModal" :loading="loading" - :disabled="!('createVolume' in $store.getters.apis) || this.vm.state === 'Error'"> + :disabled="!('createVolume' in $store.getters.apis) || this.vm.state === 'Error' || resource.hypervisor === 'External'"> {{ $t('label.action.create.volume.add') }} + + + @@ -143,6 +152,7 @@ import ResourceIcon from '@/components/view/ResourceIcon' import AnnotationsTab from '@/components/view/AnnotationsTab' import VolumesTab from '@/components/view/VolumesTab.vue' import SecurityGroupSelection from '@views/compute/wizard/SecurityGroupSelection' +import GPUTab from '@/components/view/GPUTab.vue' export default { name: 'InstanceTab', @@ -154,6 +164,7 @@ export default { DetailSettings, CreateVolume, NicsTab, + GPUTab, InstanceSchedules, ListResourceTable, SecurityGroupSelection, diff --git a/ui/src/views/compute/KubernetesAddNodes.vue b/ui/src/views/compute/KubernetesAddNodes.vue index dddcc90be25..2ee0b5ed528 100644 --- a/ui/src/views/compute/KubernetesAddNodes.vue +++ b/ui/src/views/compute/KubernetesAddNodes.vue @@ -66,7 +66,7 @@ + + diff --git a/ui/src/views/extension/CreateExtension.vue b/ui/src/views/extension/CreateExtension.vue new file mode 100644 index 00000000000..11d0c776e4d --- /dev/null +++ b/ui/src/views/extension/CreateExtension.vue @@ -0,0 +1,256 @@ +// 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. + + + + + + diff --git a/ui/src/views/extension/ExtensionCustomActionsTab.vue b/ui/src/views/extension/ExtensionCustomActionsTab.vue new file mode 100644 index 00000000000..a7b6d59c1cf --- /dev/null +++ b/ui/src/views/extension/ExtensionCustomActionsTab.vue @@ -0,0 +1,274 @@ +// 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. + + + + diff --git a/ui/src/views/extension/ExtensionResourcesTab.vue b/ui/src/views/extension/ExtensionResourcesTab.vue new file mode 100644 index 00000000000..c69c79306c8 --- /dev/null +++ b/ui/src/views/extension/ExtensionResourcesTab.vue @@ -0,0 +1,135 @@ +// 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. + + + + diff --git a/ui/src/views/extension/ExternalConfigurationDetails.vue b/ui/src/views/extension/ExternalConfigurationDetails.vue new file mode 100644 index 00000000000..e7b3f298fe2 --- /dev/null +++ b/ui/src/views/extension/ExternalConfigurationDetails.vue @@ -0,0 +1,116 @@ +// 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. + + + + diff --git a/ui/src/views/extension/ParametersInput.vue b/ui/src/views/extension/ParametersInput.vue new file mode 100644 index 00000000000..f77b0471eaa --- /dev/null +++ b/ui/src/views/extension/ParametersInput.vue @@ -0,0 +1,328 @@ +// 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. + + + + + + diff --git a/ui/src/views/extension/RegisterExtension.vue b/ui/src/views/extension/RegisterExtension.vue new file mode 100644 index 00000000000..f856c181f8c --- /dev/null +++ b/ui/src/views/extension/RegisterExtension.vue @@ -0,0 +1,200 @@ +// 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. + + + + + + diff --git a/ui/src/views/extension/RunCustomAction.vue b/ui/src/views/extension/RunCustomAction.vue new file mode 100644 index 00000000000..bd26cf7f375 --- /dev/null +++ b/ui/src/views/extension/RunCustomAction.vue @@ -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. + + + + + + diff --git a/ui/src/views/extension/UpdateCustomAction.vue b/ui/src/views/extension/UpdateCustomAction.vue new file mode 100644 index 00000000000..008882a5238 --- /dev/null +++ b/ui/src/views/extension/UpdateCustomAction.vue @@ -0,0 +1,232 @@ +// 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. + + + + + + diff --git a/ui/src/views/extension/UpdateExtension.vue b/ui/src/views/extension/UpdateExtension.vue new file mode 100644 index 00000000000..192a339b43a --- /dev/null +++ b/ui/src/views/extension/UpdateExtension.vue @@ -0,0 +1,160 @@ +// 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. + + + + + + diff --git a/ui/src/views/iam/CreateRole.vue b/ui/src/views/iam/CreateRole.vue index 3f6013acd80..11cecf69efe 100644 --- a/ui/src/views/iam/CreateRole.vue +++ b/ui/src/views/iam/CreateRole.vue @@ -143,7 +143,7 @@ export default { }, watch: { '$route' (to, from) { - if (to.fullPath !== from.fullPath && !to.fullPath.includes('action/')) { + if (to.fullPath !== from.fullPath && !to.fullPath.includes('/action/')) { this.fetchRoles() } }, diff --git a/ui/src/views/image/RegisterOrUploadIso.vue b/ui/src/views/image/RegisterOrUploadIso.vue index cb86d745e3c..e7536027e7c 100644 --- a/ui/src/views/image/RegisterOrUploadIso.vue +++ b/ui/src/views/image/RegisterOrUploadIso.vue @@ -218,7 +218,7 @@ name="userdataid" ref="userdataid"> - + @@ -212,10 +212,38 @@ + + + + + {{ extension.name || extension.description }} + + + - - + + + + +
{{ $t('message.add.orchestrator.resource.details') }}
+ +
+
+ +