UI improvements (#9773)

* Show Usage Server configuration in a separate pane

* UI: Option to attach volume to an instance during create volume

* Show service ip in management server details tab

* change Schedule Snapshots to Recurring Snapshots

* Change the hypervisor order so that kvm, vmware, xenserver show up first

* Remove extra space in hypervisor names in config.java

* Fix `updateTemplatePermission` when the UI is set to a language other than English (#9766)

* Fix updateTemplatePermission UI in non-english language

* Improve fix

---------

Co-authored-by: Lucas Martins <lucas.martins@scclouds.com.br>

* Autofill vcenter details in add cluster form

* UI: condition to display create vm-vol-snapshots to same as create vol-snapshots

* Fix alignment on wrapping in global settings tabs

* rename Autofill vCenter credentials to Autofill vCenter credentials from Zone

* Rename Service Ip to Ip Address in management server response

* Change description of kvm.snapshot.enabled to say that it applies to volume snapshots

* Return error when kvm vm snapshot is taken withoutsnapshot memory

* Minor naming changes and grammar

* Fix tooltip for attach volume to instance button

* Show Usage Server configuration in a separate pane

* UI: Option to attach volume to an instance during create volume

* Show service ip in management server details tab

* change Schedule Snapshots to Recurring Snapshots

* Change the hypervisor order so that kvm, vmware, xenserver show up first

* Remove extra space in hypervisor names in config.java

* Autofill vcenter details in add cluster form

* UI: condition to display create vm-vol-snapshots to same as create vol-snapshots

* Fix alignment on wrapping in global settings tabs

* rename Autofill vCenter credentials to Autofill vCenter credentials from Zone

* Rename Service Ip to Ip Address in management server response

* Change description of kvm.snapshot.enabled to say that it applies to volume snapshots

* Return error when kvm vm snapshot is taken withoutsnapshot memory

* Minor naming changes and grammar

* Fix tooltip for attach volume to instance button

* Show Usage Server configuration in a separate pane

* UI: Option to attach volume to an instance during create volume

* Show service ip in management server details tab

* change Schedule Snapshots to Recurring Snapshots

* Change the hypervisor order so that kvm, vmware, xenserver show up first

* Remove extra space in hypervisor names in config.java

* Autofill vcenter details in add cluster form

* UI: condition to display create vm-vol-snapshots to same as create vol-snapshots

* Fix alignment on wrapping in global settings tabs

* rename Autofill vCenter credentials to Autofill vCenter credentials from Zone

* Rename Service Ip to Ip Address in management server response

* Change description of kvm.snapshot.enabled to say that it applies to volume snapshots

* Return error when kvm vm snapshot is taken withoutsnapshot memory

* Minor naming changes and grammar

* Fix tooltip for attach volume to instance button

* UI: Option to attach volume to an instance during create volume

* UI: condition to display create vm-vol-snapshots to same as create vol-snapshots

* moved db changes from 41900to42000 to 42000to42010

* Update group_id in already present usage configuration settings

* remove "schedule" from message in create Recurring Snapshots form

* Update server/src/main/java/com/cloud/vm/snapshot/VMSnapshotManagerImpl.java

---------

Co-authored-by: Daan Hoogland <daan@onecht.net>
Co-authored-by: Lucas Martins <56271185+lucas-a-martins@users.noreply.github.com>
Co-authored-by: Lucas Martins <lucas.martins@scclouds.com.br>
Co-authored-by: Boris Stoyanov - a.k.a Bobby <bss.stoyanov@gmail.com>
Co-authored-by: Andrija Panic <45762285+andrijapanicsb@users.noreply.github.com>
This commit is contained in:
Abhisar Sinha 2025-02-06 11:18:40 +05:30 committed by GitHub
parent 986ec81b66
commit c5afee2101
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
21 changed files with 214 additions and 79 deletions

View File

@ -447,7 +447,6 @@ public class ApiConstants {
public static final String SENT = "sent"; public static final String SENT = "sent";
public static final String SENT_BYTES = "sentbytes"; public static final String SENT_BYTES = "sentbytes";
public static final String SERIAL = "serial"; public static final String SERIAL = "serial";
public static final String SERVICE_IP = "serviceip";
public static final String SERVICE_OFFERING_ID = "serviceofferingid"; public static final String SERVICE_OFFERING_ID = "serviceofferingid";
public static final String SESSIONKEY = "sessionkey"; public static final String SESSIONKEY = "sessionkey";
public static final String SHOW_CAPACITIES = "showcapacities"; public static final String SHOW_CAPACITIES = "showcapacities";

View File

@ -74,9 +74,9 @@ public class ManagementServerResponse extends BaseResponse {
@Param(description = "the running OS kernel version for this Management Server") @Param(description = "the running OS kernel version for this Management Server")
private String kernelVersion; private String kernelVersion;
@SerializedName(ApiConstants.SERVICE_IP) @SerializedName(ApiConstants.IP_ADDRESS)
@Param(description = "the IP Address for this Management Server") @Param(description = "the IP Address for this Management Server")
private String serviceIp; private String ipAddress;
@SerializedName(ApiConstants.PEERS) @SerializedName(ApiConstants.PEERS)
@Param(description = "the Management Server Peers") @Param(description = "the Management Server Peers")
@ -122,8 +122,8 @@ public class ManagementServerResponse extends BaseResponse {
return lastBoot; return lastBoot;
} }
public String getServiceIp() { public String getIpAddress() {
return serviceIp; return ipAddress;
} }
public void setId(String id) { public void setId(String id) {
@ -170,8 +170,8 @@ public class ManagementServerResponse extends BaseResponse {
this.kernelVersion = kernelVersion; this.kernelVersion = kernelVersion;
} }
public void setServiceIp(String serviceIp) { public void setIpAddress(String ipAddress) {
this.serviceIp = serviceIp; this.ipAddress = ipAddress;
} }
public String getKernelVersion() { public String getKernelVersion() {

View File

@ -24,6 +24,14 @@ CALL `cloud`.`IDEMPOTENT_ADD_COLUMN`('cloud.user', 'api_key_access', 'boolean DE
CALL `cloud`.`IDEMPOTENT_ADD_COLUMN`('cloud.account', 'api_key_access', 'boolean DEFAULT NULL COMMENT "is api key access allowed for the account" '); CALL `cloud`.`IDEMPOTENT_ADD_COLUMN`('cloud.account', 'api_key_access', 'boolean DEFAULT NULL COMMENT "is api key access allowed for the account" ');
CALL `cloud_usage`.`IDEMPOTENT_ADD_COLUMN`('cloud_usage.account', 'api_key_access', 'boolean DEFAULT NULL COMMENT "is api key access allowed for the account" '); CALL `cloud_usage`.`IDEMPOTENT_ADD_COLUMN`('cloud_usage.account', 'api_key_access', 'boolean DEFAULT NULL COMMENT "is api key access allowed for the account" ');
-- Create a new group for Usage Server related configurations
INSERT INTO `cloud`.`configuration_group` (`name`, `description`, `precedence`) VALUES ('Usage Server', 'Usage Server related configuration', 9);
UPDATE `cloud`.`configuration_subgroup` set `group_id` = (SELECT `id` FROM `cloud`.`configuration_group` WHERE `name` = 'Usage Server'), `precedence` = 1 WHERE `name`='Usage';
UPDATE `cloud`.`configuration` SET `group_id` = (SELECT `id` FROM `cloud`.`configuration_group` WHERE `name` = 'Usage Server') where `subgroup_id` = (SELECT `id` FROM `cloud`.`configuration_subgroup` WHERE `name` = 'Usage');
-- Update the description to indicate this setting applies only to volume snapshots on running instances
UPDATE `cloud`.`configuration` SET `description`='whether volume snapshot is enabled on running instances on KVM hosts' WHERE `name`='kvm.snapshot.enabled';
-- Modify index for mshost_peer -- Modify index for mshost_peer
DELETE FROM `cloud`.`mshost_peer`; DELETE FROM `cloud`.`mshost_peer`;
CALL `cloud`.`IDEMPOTENT_DROP_FOREIGN_KEY`('cloud.mshost_peer','fk_mshost_peer__owner_mshost'); CALL `cloud`.`IDEMPOTENT_DROP_FOREIGN_KEY`('cloud.mshost_peer','fk_mshost_peer__owner_mshost');

View File

@ -5413,13 +5413,13 @@ public class QueryManagerImpl extends MutualExclusiveIdsManagerBase implements Q
mgmtResponse.setLastServerStart(mgmt.getLastJvmStart()); mgmtResponse.setLastServerStart(mgmt.getLastJvmStart());
mgmtResponse.setLastServerStop(mgmt.getLastJvmStop()); mgmtResponse.setLastServerStop(mgmt.getLastJvmStop());
mgmtResponse.setLastBoot(mgmt.getLastSystemBoot()); mgmtResponse.setLastBoot(mgmt.getLastSystemBoot());
mgmtResponse.setServiceIp(mgmt.getServiceIP());
if (listPeers) { if (listPeers) {
List<ManagementServerHostPeerJoinVO> peers = mshostPeerJoinDao.listByOwnerMshostId(mgmt.getId()); List<ManagementServerHostPeerJoinVO> peers = mshostPeerJoinDao.listByOwnerMshostId(mgmt.getId());
for (ManagementServerHostPeerJoinVO peer: peers) { for (ManagementServerHostPeerJoinVO peer: peers) {
mgmtResponse.addPeer(createPeerManagementServerNodeResponse(peer)); mgmtResponse.addPeer(createPeerManagementServerNodeResponse(peer));
} }
} }
mgmtResponse.setIpAddress(mgmt.getServiceIP());
mgmtResponse.setObjectName("managementserver"); mgmtResponse.setObjectName("managementserver");
return mgmtResponse; return mgmtResponse;
} }

View File

@ -506,7 +506,7 @@ public enum Config {
"The time interval in seconds when the management server polls for snapshots to be scheduled.", "The time interval in seconds when the management server polls for snapshots to be scheduled.",
null), null),
SnapshotDeltaMax("Snapshots", SnapshotManager.class, Integer.class, "snapshot.delta.max", "16", "max delta snapshots between two full snapshots.", null), SnapshotDeltaMax("Snapshots", SnapshotManager.class, Integer.class, "snapshot.delta.max", "16", "max delta snapshots between two full snapshots.", null),
KVMSnapshotEnabled("Hidden", SnapshotManager.class, Boolean.class, "kvm.snapshot.enabled", "false", "whether snapshot is enabled for KVM hosts", null), KVMSnapshotEnabled("Hidden", SnapshotManager.class, Boolean.class, "kvm.snapshot.enabled", "false", "Whether volume snapshot is enabled on running instances on a KVM host", null),
// Advanced // Advanced
EventPurgeInterval( EventPurgeInterval(
@ -665,8 +665,8 @@ public enum Config {
ManagementServer.class, ManagementServer.class,
String.class, String.class,
"hypervisor.list", "hypervisor.list",
HypervisorType.Hyperv + "," + HypervisorType.KVM + "," + HypervisorType.XenServer + "," + HypervisorType.VMware + "," + HypervisorType.BareMetal + "," + HypervisorType.KVM + "," + HypervisorType.VMware + "," + HypervisorType.XenServer + "," + HypervisorType.Hyperv + "," +
HypervisorType.Ovm + "," + HypervisorType.LXC + "," + HypervisorType.Ovm3, HypervisorType.BareMetal + "," + HypervisorType.Ovm + "," + HypervisorType.LXC + "," + HypervisorType.Ovm3,
"The list of hypervisors that this deployment will use.", "The list of hypervisors that this deployment will use.",
"hypervisorList", "hypervisorList",
ConfigKey.Kind.CSV, ConfigKey.Kind.CSV,

View File

@ -343,7 +343,7 @@ public class Site2SiteVpnManagerImpl extends ManagerBase implements Site2SiteVpn
private void validatePrerequisiteVpnGateway(Site2SiteVpnGateway vpnGateway) { private void validatePrerequisiteVpnGateway(Site2SiteVpnGateway vpnGateway) {
// check if gateway has been defined on the VPC // check if gateway has been defined on the VPC
if (_vpnGatewayDao.findByVpcId(vpnGateway.getVpcId()) == null) { if (_vpnGatewayDao.findByVpcId(vpnGateway.getVpcId()) == null) {
throw new InvalidParameterValueException("we can not create a VPN connection for a VPC that does not have a VPN gateway defined"); throw new InvalidParameterValueException("We can not create a VPN connection for a VPC that does not have a VPN gateway defined");
} }
} }
@ -590,7 +590,7 @@ public class Site2SiteVpnManagerImpl extends ManagerBase implements Site2SiteVpn
private void stopVpnConnection(Long id) throws ResourceUnavailableException { private void stopVpnConnection(Long id) throws ResourceUnavailableException {
Site2SiteVpnConnectionVO conn = _vpnConnectionDao.acquireInLockTable(id); Site2SiteVpnConnectionVO conn = _vpnConnectionDao.acquireInLockTable(id);
if (conn == null) { if (conn == null) {
throw new CloudRuntimeException("Unable to acquire lock for stopping of VPN connection with ID " + id); throw new CloudRuntimeException("Unable to acquire lock for stopping VPN connection with ID " + id);
} }
try { try {
if (conn.getState() == State.Pending) { if (conn.getState() == State.Pending) {

View File

@ -27,6 +27,7 @@ import java.util.Map;
import javax.inject.Inject; import javax.inject.Inject;
import javax.naming.ConfigurationException; import javax.naming.ConfigurationException;
import com.cloud.storage.snapshot.SnapshotManager;
import org.apache.cloudstack.annotation.AnnotationService; import org.apache.cloudstack.annotation.AnnotationService;
import org.apache.cloudstack.annotation.dao.AnnotationDao; import org.apache.cloudstack.annotation.dao.AnnotationDao;
import org.apache.cloudstack.api.ApiConstants; import org.apache.cloudstack.api.ApiConstants;
@ -377,9 +378,14 @@ public class VMSnapshotManagerImpl extends MutualExclusiveIdsManagerBase impleme
//StorageVMSnapshotStrategy - allows volume snapshots without memory; VM has to be in Running state; No limitation of the image format if the storage plugin supports volume snapshots; "kvm.vmstoragesnapshot.enabled" has to be enabled //StorageVMSnapshotStrategy - allows volume snapshots without memory; VM has to be in Running state; No limitation of the image format if the storage plugin supports volume snapshots; "kvm.vmstoragesnapshot.enabled" has to be enabled
//Other Storage volume plugins could integrate this with their own functionality for group snapshots //Other Storage volume plugins could integrate this with their own functionality for group snapshots
VMSnapshotStrategy snapshotStrategy = storageStrategyFactory.getVmSnapshotStrategy(userVmVo.getId(), rootVolumePool.getId(), snapshotMemory); VMSnapshotStrategy snapshotStrategy = storageStrategyFactory.getVmSnapshotStrategy(userVmVo.getId(), rootVolumePool.getId(), snapshotMemory);
if (snapshotStrategy == null) { if (snapshotStrategy == null) {
String message = "KVM does not support the type of snapshot requested"; String message;
if (!SnapshotManager.VmStorageSnapshotKvm.value() && !snapshotMemory) {
message = "Creating a snapshot of a running KVM instance without memory is not supported";
} else {
message = "KVM does not support the type of snapshot requested";
}
logger.debug(message); logger.debug(message);
throw new CloudRuntimeException(message); throw new CloudRuntimeException(message);
} }

View File

@ -1265,7 +1265,6 @@
"label.save.new.rule": "Neue Regel Speichern", "label.save.new.rule": "Neue Regel Speichern",
"label.schedule": "Zeitplan", "label.schedule": "Zeitplan",
"label.scheduled.backups": "geplante Backups", "label.scheduled.backups": "geplante Backups",
"label.scheduled.snapshots": "geplante Schnappschüsse",
"label.scope": "Geltungsbereich", "label.scope": "Geltungsbereich",
"label.search": "Suche", "label.search": "Suche",
"label.secondary.isolated.vlan.type.isolated": "Isoliert", "label.secondary.isolated.vlan.type.isolated": "Isoliert",

View File

@ -1517,7 +1517,6 @@
"label.scale.vm": "Κλίμακα εικονικής μηχανής", "label.scale.vm": "Κλίμακα εικονικής μηχανής",
"label.schedule": "Πρόγραμμα", "label.schedule": "Πρόγραμμα",
"label.scheduled.backups": "Προγραμματισμένα αντίγραφα ασφαλείας", "label.scheduled.backups": "Προγραμματισμένα αντίγραφα ασφαλείας",
"label.scheduled.snapshots": "Προγραμματισμένα στιγμιότυπα",
"label.scope": "Πεδίο εφαρμογής", "label.scope": "Πεδίο εφαρμογής",
"label.search": "Αναζήτηση", "label.search": "Αναζήτηση",
"label.secondary.isolated.vlan.type.isolated": "Απομονωμένες", "label.secondary.isolated.vlan.type.isolated": "Απομονωμένες",

View File

@ -54,6 +54,7 @@
"label.action": "Action", "label.action": "Action",
"label.action.attach.disk": "Attach disk", "label.action.attach.disk": "Attach disk",
"label.action.attach.iso": "Attach ISO", "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", "label.action.bulk.delete.egress.firewall.rules": "Bulk delete egress firewall rules",
"label.action.bulk.delete.firewall.rules": "Bulk delete firewall rules", "label.action.bulk.delete.firewall.rules": "Bulk delete firewall rules",
"label.action.bulk.delete.ip.v6.firewall.rules": "Bulk remove IPv6 firewall rules", "label.action.bulk.delete.ip.v6.firewall.rules": "Bulk remove IPv6 firewall rules",
@ -399,9 +400,11 @@
"label.associatednetworkid": "Associated Network ID", "label.associatednetworkid": "Associated Network ID",
"label.associatednetworkname": "Network name", "label.associatednetworkname": "Network name",
"label.asyncbackup": "Async backup", "label.asyncbackup": "Async backup",
"label.attach.vol.to.instance": "Attach the created Volume to an existing Instance",
"label.attaching": "Attaching", "label.attaching": "Attaching",
"label.authentication.method": "Authentication Method", "label.authentication.method": "Authentication Method",
"label.authentication.sshkey": "System SSH Key", "label.authentication.sshkey": "System SSH Key",
"label.use.existing.vcenter.credentials.from.zone": "Use existing vCenter credentials from the Zone",
"label.autoscale": "AutoScale", "label.autoscale": "AutoScale",
"label.autoscalevmgroupname": "AutoScaling Group", "label.autoscalevmgroupname": "AutoScaling Group",
"label.author.email": "Author e-mail", "label.author.email": "Author e-mail",
@ -1073,7 +1076,7 @@
"label.hastate": "HA state", "label.hastate": "HA state",
"label.headers": "Headers", "label.headers": "Headers",
"label.header.backup.schedule": "You can set up recurring backup schedules by selecting from the available options below and applying your policy preference.", "label.header.backup.schedule": "You can set up recurring backup schedules by selecting from the available options below and applying your policy preference.",
"label.header.volume.snapshot": "You can set up recurring Snapshot schedules by selecting from the available options below and applying your policy preference.", "label.header.volume.snapshot": "You can set up recurring Snapshots by selecting from the available options below and applying your policy preference.",
"label.header.volume.take.snapshot": "Please confirm that you want to take a Snapshot of this volume.", "label.header.volume.take.snapshot": "Please confirm that you want to take a Snapshot of this volume.",
"label.health.check": "Health check", "label.health.check": "Health check",
"label.heapmemoryused": "Heap-memory used", "label.heapmemoryused": "Heap-memory used",
@ -1999,7 +2002,6 @@
"label.schedule": "Schedule", "label.schedule": "Schedule",
"label.schedule.add": "Add schedule", "label.schedule.add": "Add schedule",
"label.scheduled.backups": "Scheduled backups", "label.scheduled.backups": "Scheduled backups",
"label.scheduled.snapshots": "Scheduled Snapshots",
"label.schedules": "Schedules", "label.schedules": "Schedules",
"label.scope": "Scope", "label.scope": "Scope",
"label.scope.tooltip": "Primary Storage Pool Scope", "label.scope.tooltip": "Primary Storage Pool Scope",

View File

@ -1969,7 +1969,6 @@
"label.scaleup.policy": "スケールアップポリシー", "label.scaleup.policy": "スケールアップポリシー",
"label.schedule": "スケジュール", "label.schedule": "スケジュール",
"label.scheduled.backups": "スケジュールされたバックアップ", "label.scheduled.backups": "スケジュールされたバックアップ",
"label.scheduled.snapshots": "スケジュールされたスナップショット",
"label.scope": "スコープ", "label.scope": "スコープ",
"label.search": "検索", "label.search": "検索",
"label.secondary.isolated.vlan.type.isolated": "隔離", "label.secondary.isolated.vlan.type.isolated": "隔離",

View File

@ -1322,7 +1322,6 @@
"label.scale.vm": "VM \ud655\uc7a5", "label.scale.vm": "VM \ud655\uc7a5",
"label.schedule": "\uc2a4\ucf00\uc904", "label.schedule": "\uc2a4\ucf00\uc904",
"label.scheduled.backups": "\uc608\uc57d\ub41c \ubc31\uc5c5", "label.scheduled.backups": "\uc608\uc57d\ub41c \ubc31\uc5c5",
"label.scheduled.snapshots": "\uc608\uc57d\ub41c \uc2a4\ub0c5\uc0f7",
"label.scope": "\ubc94\uc704", "label.scope": "\ubc94\uc704",
"label.search": "\uac80\uc0c9", "label.search": "\uac80\uc0c9",
"label.secondary.isolated.vlan.type.isolated": "isolated", "label.secondary.isolated.vlan.type.isolated": "isolated",

View File

@ -1428,7 +1428,6 @@
"label.scale.vm": "Escalar VM", "label.scale.vm": "Escalar VM",
"label.schedule": "Programar", "label.schedule": "Programar",
"label.scheduled.backups": "Backups programados", "label.scheduled.backups": "Backups programados",
"label.scheduled.snapshots": "Snapshots programados",
"label.scope": "Escopo", "label.scope": "Escopo",
"label.search": "Pesquisar", "label.search": "Pesquisar",
"label.secondary.isolated.vlan.type.isolated": "Isolada", "label.secondary.isolated.vlan.type.isolated": "Isolada",

View File

@ -2248,7 +2248,6 @@
"label.schedule": "\u65E5\u7A0B", "label.schedule": "\u65E5\u7A0B",
"label.scheduled.backups": "\u5B9A\u65F6\u5907\u4EFD", "label.scheduled.backups": "\u5B9A\u65F6\u5907\u4EFD",
"label.scheduled.snapshots": "\u8BA1\u5212\u5FEB\u7167",
"label.scope": "\u8303\u56F4", "label.scope": "\u8303\u56F4",
"label.search": "\u641C\u7D22", "label.search": "\u641C\u7D22",

View File

@ -207,9 +207,10 @@ export default {
docHelp: 'adminguide/virtual_machines.html#virtual-machine-snapshots', docHelp: 'adminguide/virtual_machines.html#virtual-machine-snapshots',
dataView: true, dataView: true,
popup: true, popup: true,
show: (record) => { show: (record, store) => {
return ((['Running'].includes(record.state) && record.hypervisor !== 'LXC') || return (record.hypervisor !== 'KVM') ||
(['Stopped'].includes(record.state) && !['KVM', 'LXC'].includes(record.hypervisor))) ['Stopped', 'Destroyed'].includes(record.state) ||
store.features.kvmsnapshotenabled
}, },
disabled: (record) => { return record.hostcontrolstate === 'Offline' && record.hypervisor === 'KVM' }, disabled: (record) => { return record.hostcontrolstate === 'Offline' && record.hypervisor === 'KVM' },
component: shallowRef(defineAsyncComponent(() => import('@/views/compute/CreateSnapshotWizard.vue'))) component: shallowRef(defineAsyncComponent(() => import('@/views/compute/CreateSnapshotWizard.vue')))

View File

@ -26,14 +26,14 @@ export default {
permission: ['listManagementServersMetrics'], permission: ['listManagementServersMetrics'],
resourceType: 'ManagementServer', resourceType: 'ManagementServer',
columns: () => { columns: () => {
const fields = ['name', 'state', 'serviceip', 'version', 'osdistribution', 'agentcount'] const fields = ['name', 'state', 'ipaddress', 'version', 'osdistribution', 'agentcount']
const metricsFields = ['collectiontime', 'availableprocessors', 'cpuload', 'heapmemoryused'] const metricsFields = ['collectiontime', 'availableprocessors', 'cpuload', 'heapmemoryused']
if (store.getters.metrics) { if (store.getters.metrics) {
fields.push(...metricsFields) fields.push(...metricsFields)
} }
return fields return fields
}, },
details: ['collectiontime', 'usageislocal', 'dbislocal', 'lastserverstart', 'lastserverstop', 'lastboottime', 'version', 'loginfo', 'systemtotalcpucycles', 'systemloadaverages', 'systemcycleusage', 'systemmemorytotal', 'systemmemoryfree', 'systemmemoryvirtualsize', 'availableprocessors', 'javadistribution', 'javaversion', 'osdistribution', 'kernelversion', 'agentcount', 'sessions', 'heapmemoryused', 'heapmemorytotal', 'threadsblockedcount', 'threadsdeamoncount', 'threadsnewcount', 'threadsrunnablecount', 'threadsterminatedcount', 'threadstotalcount', 'threadswaitingcount'], details: ['ipaddress', 'collectiontime', 'usageislocal', 'dbislocal', 'lastserverstart', 'lastserverstop', 'lastboottime', 'version', 'loginfo', 'systemtotalcpucycles', 'systemloadaverages', 'systemcycleusage', 'systemmemorytotal', 'systemmemoryfree', 'systemmemoryvirtualsize', 'availableprocessors', 'javadistribution', 'javaversion', 'osdistribution', 'kernelversion', 'agentcount', 'sessions', 'heapmemoryused', 'heapmemorytotal', 'threadsblockedcount', 'threadsdeamoncount', 'threadsnewcount', 'threadsrunnablecount', 'threadsterminatedcount', 'threadstotalcount', 'threadswaitingcount'],
tabs: [ tabs: [
{ {
name: 'details', name: 'details',

View File

@ -165,9 +165,10 @@ export default {
label: 'label.action.take.snapshot', label: 'label.action.take.snapshot',
dataView: true, dataView: true,
show: (record, store) => { show: (record, store) => {
return record.state === 'Ready' && (record.hypervisor !== 'KVM' || return record.state === 'Ready' &&
record.hypervisor === 'KVM' && record.vmstate === 'Running' && store.features.kvmsnapshotenabled || (record.hypervisor !== 'KVM' ||
record.hypervisor === 'KVM' && record.vmstate !== 'Running') ['Stopped', 'Destroyed'].includes(record.vmstate) ||
store.features.kvmsnapshotenabled)
}, },
popup: true, popup: true,
component: shallowRef(defineAsyncComponent(() => import('@/views/storage/TakeSnapshot.vue'))) component: shallowRef(defineAsyncComponent(() => import('@/views/storage/TakeSnapshot.vue')))
@ -179,9 +180,10 @@ export default {
label: 'label.action.recurring.snapshot', label: 'label.action.recurring.snapshot',
dataView: true, dataView: true,
show: (record, store) => { show: (record, store) => {
return record.state === 'Ready' && (record.hypervisor !== 'KVM' || return record.state === 'Ready' &&
record.hypervisor === 'KVM' && record.vmstate === 'Running' && store.features.kvmsnapshotenabled || (record.hypervisor !== 'KVM' ||
record.hypervisor === 'KVM' && record.vmstate !== 'Running') (['Stopped', 'Destroyed'].includes(record.vmstate)) ||
(store.features.kvmsnapshotenabled))
}, },
popup: true, popup: true,
component: shallowRef(defineAsyncComponent(() => import('@/views/storage/RecurringSnapshotVolume.vue'))), component: shallowRef(defineAsyncComponent(() => import('@/views/storage/RecurringSnapshotVolume.vue'))),

View File

@ -47,7 +47,7 @@
<div class="form__label">{{ $t('label.hypervisor') }}</div> <div class="form__label">{{ $t('label.hypervisor') }}</div>
<a-select <a-select
v-model:value="hypervisor" v-model:value="hypervisor"
@change="resetAllFields" @change="hypervisor => onChangeHypervisor(hypervisor)"
showSearch showSearch
optionFilterProp="value" optionFilterProp="value"
:filterOption="(input, option) => { :filterOption="(input, option) => {
@ -109,19 +109,28 @@
</div> </div>
<div class="form__item"> <div class="form__item">
<div class="form__label">{{ $t('label.vcenterusername') }}</div> <div class="form__label">{{ $t('label.vcenterdatacenter') }}</div>
<a-input v-model:value="dataCenter"></a-input>
</div>
<div class="form__item" name="useDefaultVMwareCred">
<div class="form__label">{{ $t('label.use.existing.vcenter.credentials.from.zone') }}</div>
<a-switch v-model="useDefaultVMwareCred" :checked="useDefaultVMwareCred" @change="onChangeUseDefaultVMwareCred()" />
</div>
<template v-if="useDefaultVMwareCred === false">
<div class="form__item">
<div class="form__label"><span class="required">* </span>{{ $t('label.vcenterusername') }}</div>
<span class="required required-label" ref="requiredUsername">{{ $t('label.required') }}</span>
<a-input v-model:value="username"></a-input> <a-input v-model:value="username"></a-input>
</div> </div>
<div class="form__item"> <div class="form__item">
<div class="form__label">{{ $t('label.vcenterpassword') }}</div> <div class="form__label"><span class="required">* </span>{{ $t('label.vcenterpassword') }}</div>
<span class="required required-label" ref="requiredPassword">{{ $t('label.required') }}</span>
<a-input type="password" v-model:value="password"></a-input> <a-input type="password" v-model:value="password"></a-input>
</div> </div>
</template>
<div class="form__item">
<div class="form__label">{{ $t('label.vcenterdatacenter') }}</div>
<a-input v-model:value="dataCenter"></a-input>
</div>
</template> </template>
<div class="form__item"> <div class="form__item">
@ -176,6 +185,7 @@ export default {
username: null, username: null,
password: null, password: null,
url: null, url: null,
useDefaultVMwareCred: true,
host: null, host: null,
dataCenter: null, dataCenter: null,
ovm3pool: null, ovm3pool: null,
@ -257,6 +267,27 @@ export default {
this.loading = false this.loading = false
}) })
}, },
fetchVMwareCred () {
this.loading = true
this.clustertype = 'ExternalManaged'
api('listVmwareDcs', {
zoneid: this.zoneId
}).then(response => {
var vmwaredcs = response.listvmwaredcsresponse.VMwareDC
if (vmwaredcs !== null) {
this.host = vmwaredcs[0].vcenter
this.dataCenter = vmwaredcs[0].name
}
}).catch(error => {
this.$notification.error({
message: `${this.$t('label.error')} ${error.response.status}`,
description: error.response.data.listvmwaredcsresponse.errortext,
duration: 0
})
}).finally(() => {
this.loading = false
})
},
toggleDedicated () { toggleDedicated () {
this.dedicatedDomainId = null this.dedicatedDomainId = null
this.dedicatedAccount = null this.dedicatedAccount = null
@ -270,35 +301,24 @@ export default {
} }
this.$refs.requiredCluster.classList.remove('required-label--visible') this.$refs.requiredCluster.classList.remove('required-label--visible')
if (this.hypervisor === 'VMware' && this.useDefaultVMwareCred === false) {
if (!this.username) {
this.$refs.requiredUsername.classList.add('required-label--visible')
return
}
if (!this.password) {
this.$refs.requiredPassword.classList.add('required-label--visible')
return
}
this.$refs.requiredUsername.classList.remove('required-label--visible')
this.$refs.requiredPassword.classList.remove('required-label--visible')
}
if (this.hypervisor === 'Ovm3') { if (this.hypervisor === 'Ovm3') {
this.ovm3pool = 'on' this.ovm3pool = 'on'
this.ovm3cluster = 'undefined' this.ovm3cluster = 'undefined'
this.ovm3vip = '' this.ovm3vip = ''
} }
if (this.hypervisor === 'VMware') {
this.clustertype = 'ExternalManaged'
if ((this.host === null || this.host.length === 0) &&
(this.dataCenter === null || this.dataCenter.length === 0)) {
api('listVmwareDcs', {
zoneid: this.zoneId
}).then(response => {
var vmwaredcs = response.listvmwaredcsresponse.VMwareDC
if (vmwaredcs !== null) {
this.host = vmwaredcs[0].vcenter
this.dataCenter = vmwaredcs[0].name
}
this.addCluster()
}).catch(error => {
this.$notification.error({
message: `${this.$t('label.error')} ${error.response.status}`,
description: error.response.data.listvmwaredcsresponse.errortext,
duration: 0
})
})
return
}
}
this.addCluster() this.addCluster()
}, },
addCluster () { addCluster () {
@ -387,7 +407,7 @@ export default {
this.loading = false this.loading = false
}) })
}, },
resetAllFields () { onChangeHypervisor (hypervisor) {
this.clustertype = 'CloudManaged' this.clustertype = 'CloudManaged'
this.username = null this.username = null
this.password = null this.password = null
@ -397,6 +417,16 @@ export default {
this.ovm3pool = null this.ovm3pool = null
this.ovm3cluster = null this.ovm3cluster = null
this.ovm3vip = null this.ovm3vip = null
if (hypervisor === 'VMware') {
this.fetchVMwareCred()
}
},
onChangeUseDefaultVMwareCred () {
this.useDefaultVMwareCred = !this.useDefaultVMwareCred
if (this.useDefaultVMwareCred) {
this.username = null
this.password = null
}
}, },
returnPlaceholder (field) { returnPlaceholder (field) {
this.params.find(i => { this.params.find(i => {

View File

@ -28,10 +28,9 @@
<template #bodyCell="{ column, record }"> <template #bodyCell="{ column, record }">
<template v-if="column.key === 'name'"> <template v-if="column.key === 'name'">
<span :style="hierarchyExists ? 'padding-left: 0px;' : 'padding-left: 25px;'"> <a-row :style="hierarchyExists ? 'padding-left: 0px;' : 'padding-left: 25px;'">
<b><span v-if="record.parent"> &nbsp;</span>{{record.displaytext }} </b> {{ ' (' + record.name + ')' }} <b><span v-if="record.parent"> &nbsp;</span>{{record.displaytext }}&nbsp</b> {{ '(' + record.name + ')' }}
</span> </a-row>
<br/>
<span :style="record.parent ? 'padding-left: 50px; display:block' : 'padding-left: 25px; display:block'">{{ record.description }}</span> <span :style="record.parent ? 'padding-left: 50px; display:block' : 'padding-left: 25px; display:block'">{{ record.description }}</span>
</template> </template>
<template v-if="column.key === 'value'"> <template v-if="column.key === 'value'">

View File

@ -116,6 +116,48 @@
:placeholder="apiParams.maxiops.description"/> :placeholder="apiParams.maxiops.description"/>
</a-form-item> </a-form-item>
</span> </span>
<a-form-item name="attachVolume" ref="attachVolume" v-if="!createVolumeFromVM">
<template #label>
<tooltip-label :title="$t('label.action.attach.to.instance')" :tooltip="$t('label.attach.vol.to.instance')" />
</template>
<a-switch v-model:checked="form.attachVolume" :checked="attachVolume" @change="zone => onChangeAttachToVM(zone.id)" />
</a-form-item>
<span v-if="attachVolume">
<a-form-item :label="$t('label.virtualmachineid')" name="virtualmachineid" ref="virtualmachineid">
<a-select
v-focus="true"
v-model:value="form.virtualmachineid"
:placeholder="attachVolumeApiParams.virtualmachineid.description"
showSearch
optionFilterProp="label"
:filterOption="(input, option) => {
return option.label.toLowerCase().indexOf(input.toLowerCase()) >= 0
}" >
<a-select-option v-for="vm in virtualmachines" :key="vm.id" :label="vm.name || vm.displayname">
{{ vm.name || vm.displayname }}
</a-select-option>
</a-select>
</a-form-item >
<a-form-item :label="$t('label.deviceid')">
<div style="margin-bottom: 10px">
<a-collapse>
<a-collapse-panel header="More information about deviceID">
<a-alert type="warning">
<template #message>
<span v-html="attachVolumeApiParams.deviceid.description" />
</template>
</a-alert>
</a-collapse-panel>
</a-collapse>
</div>
<a-input-number
v-model:value="form.deviceid"
style="width: 100%;"
:min="0"
:placeholder="$t('label.deviceid')"
/>
</a-form-item>
</span>
<div :span="24" class="action-button"> <div :span="24" class="action-button">
<a-button @click="closeModal">{{ $t('label.cancel') }}</a-button> <a-button @click="closeModal">{{ $t('label.cancel') }}</a-button>
<a-button type="primary" ref="submit" @click="handleSubmit">{{ $t('label.ok') }}</a-button> <a-button type="primary" ref="submit" @click="handleSubmit">{{ $t('label.ok') }}</a-button>
@ -159,7 +201,10 @@ export default {
offerings: [], offerings: [],
customDiskOffering: false, customDiskOffering: false,
loading: false, loading: false,
isCustomizedDiskIOps: false isCustomizedDiskIOps: false,
virtualmachines: [],
attachVolume: false,
vmidtoattach: null
} }
}, },
computed: { computed: {
@ -204,6 +249,10 @@ export default {
} }
}] }]
}) })
if (this.attachVolume) {
this.rules.virtualmachineid = [{ required: true, message: this.$t('message.error.select') }]
this.rules.deviceid = [{ required: true, message: this.$t('message.error.select') }]
}
if (!this.createVolumeFromSnapshot) { if (!this.createVolumeFromSnapshot) {
this.rules.name = [{ required: true, message: this.$t('message.error.volume.name') }] this.rules.name = [{ required: true, message: this.$t('message.error.volume.name') }]
this.rules.diskofferingid = [{ required: true, message: this.$t('message.error.select') }] this.rules.diskofferingid = [{ required: true, message: this.$t('message.error.select') }]
@ -248,6 +297,9 @@ export default {
this.zones = json.listzonesresponse.zone || [] this.zones = json.listzonesresponse.zone || []
this.form.zoneid = this.zones[0].id || '' this.form.zoneid = this.zones[0].id || ''
this.fetchDiskOfferings(this.form.zoneid) this.fetchDiskOfferings(this.form.zoneid)
if (this.attachVolume) {
this.fetchVirtualMachines(this.form.zoneid)
}
}).finally(() => { }).finally(() => {
this.loading = false this.loading = false
}) })
@ -301,6 +353,31 @@ export default {
this.loading = false this.loading = false
}) })
}, },
fetchVirtualMachines (zoneId) {
var params = {
zoneid: zoneId,
details: 'min'
}
if (this.owner.projectid) {
params.projectid = this.owner.projectid
} else {
params.account = this.owner.account
params.domainid = this.owner.domainid
}
this.loading = true
var vmStates = ['Running', 'Stopped']
vmStates.forEach((state) => {
params.state = state
api('listVirtualMachines', params).then(response => {
this.virtualmachines = this.virtualmachines.concat(response.listvirtualmachinesresponse.virtualmachine || [])
}).catch(error => {
this.$notifyError(error)
}).finally(() => {
this.loading = false
})
})
},
handleSubmit (e) { handleSubmit (e) {
if (this.loading) return if (this.loading) return
this.formRef.value.validate().then(() => { this.formRef.value.validate().then(() => {
@ -315,6 +392,10 @@ export default {
if (this.createVolumeFromSnapshot) { if (this.createVolumeFromSnapshot) {
values.snapshotid = this.resource.id values.snapshotid = this.resource.id
} }
if (this.attachVolume) {
this.vmidtoattach = values.virtualmachineid
values.virtualmachineid = null
}
values.domainid = this.owner.domainid values.domainid = this.owner.domainid
if (this.owner.projectid) { if (this.owner.projectid) {
values.projectid = this.owner.projectid values.projectid = this.owner.projectid
@ -330,10 +411,15 @@ export default {
successMessage: this.$t('message.success.create.volume'), successMessage: this.$t('message.success.create.volume'),
successMethod: (result) => { successMethod: (result) => {
this.closeModal() this.closeModal()
if (this.createVolumeFromVM) { if (this.createVolumeFromVM || this.attachVolume) {
const params = {} const params = {}
params.id = result.jobresult.volume.id params.id = result.jobresult.volume.id
if (this.createVolumeFromVM) {
params.virtualmachineid = this.resource.id params.virtualmachineid = this.resource.id
} else {
params.virtualmachineid = this.vmidtoattach
params.deviceid = values.deviceid
}
api('attachVolume', params).then(response => { api('attachVolume', params).then(response => {
this.$pollJob({ this.$pollJob({
jobId: response.attachvolumeresponse.jobid, jobId: response.attachvolumeresponse.jobid,
@ -368,6 +454,14 @@ export default {
const offering = this.offerings.filter(x => x.id === id) const offering = this.offerings.filter(x => x.id === id)
this.customDiskOffering = offering[0]?.iscustomized || false this.customDiskOffering = offering[0]?.iscustomized || false
this.isCustomizedDiskIOps = offering[0]?.iscustomizediops || false this.isCustomizedDiskIOps = offering[0]?.iscustomizediops || false
},
onChangeAttachToVM (zone) {
this.attachVolume = !this.attachVolume
this.virtualmachines = []
if (this.attachVolume) {
this.attachVolumeApiParams = this.$getApiParams('attachVolume')
this.fetchVirtualMachines(this.form.zoneid)
}
} }
} }
} }

View File

@ -27,7 +27,7 @@
@close-action="closeAction" @close-action="closeAction"
@refresh="handleRefresh"/> @refresh="handleRefresh"/>
</a-tab-pane> </a-tab-pane>
<a-tab-pane :tab="$t('label.scheduled.snapshots')" key="2"> <a-tab-pane :tab="$t('label.action.recurring.snapshot')" key="2">
<ScheduledSnapshots <ScheduledSnapshots
:loading="loading" :loading="loading"
:resource="resource" :resource="resource"