ui: vmware vm import-unmanage (#5075)

Adds UI for importing and unmanaging VMs.
A new navigation section - Tools has been added in the UI.

Doc PR: apache/cloudstack-documentation#221

Signed-off-by: Abhishek Kumar <abhishek.mrt22@gmail.com>
This commit is contained in:
Abhishek Kumar 2021-07-27 11:12:37 +05:30 committed by GitHub
parent 37f3fc30c9
commit 87ee86679e
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
20 changed files with 2246 additions and 127 deletions

View File

@ -43,6 +43,10 @@ public class UnmanagedInstanceResponse extends BaseResponse {
@Param(description = "the ID of the host to which virtual machine belongs")
private String hostId;
@SerializedName(ApiConstants.HOST_NAME)
@Param(description = "the name of the host to which virtual machine belongs")
private String hostName;
@SerializedName(ApiConstants.POWER_STATE)
@Param(description = "the power state of the virtual machine")
private String powerState;
@ -108,6 +112,14 @@ public class UnmanagedInstanceResponse extends BaseResponse {
this.hostId = hostId;
}
public String getHostName() {
return hostName;
}
public void setHostName(String hostName) {
this.hostName = hostName;
}
public String getPowerState() {
return powerState;
}

View File

@ -25,17 +25,6 @@ import java.util.Set;
import javax.inject.Inject;
import com.cloud.agent.api.PrepareUnmanageVMInstanceAnswer;
import com.cloud.agent.api.PrepareUnmanageVMInstanceCommand;
import com.cloud.event.ActionEvent;
import com.cloud.exception.UnsupportedServiceException;
import com.cloud.storage.Snapshot;
import com.cloud.storage.SnapshotVO;
import com.cloud.storage.dao.SnapshotDao;
import com.cloud.vm.NicVO;
import com.cloud.vm.UserVmVO;
import com.cloud.vm.dao.UserVmDao;
import com.cloud.vm.snapshot.dao.VMSnapshotDao;
import org.apache.cloudstack.api.ApiConstants;
import org.apache.cloudstack.api.ApiErrorCode;
import org.apache.cloudstack.api.ResponseGenerator;
@ -59,12 +48,15 @@ import org.apache.cloudstack.storage.datastore.db.StoragePoolVO;
import org.apache.cloudstack.utils.volume.VirtualMachineDiskInfo;
import org.apache.commons.collections.CollectionUtils;
import org.apache.commons.collections.MapUtils;
import org.apache.commons.lang3.StringUtils;
import org.apache.log4j.Logger;
import com.cloud.agent.AgentManager;
import com.cloud.agent.api.Answer;
import com.cloud.agent.api.GetUnmanagedInstancesAnswer;
import com.cloud.agent.api.GetUnmanagedInstancesCommand;
import com.cloud.agent.api.PrepareUnmanageVMInstanceAnswer;
import com.cloud.agent.api.PrepareUnmanageVMInstanceCommand;
import com.cloud.capacity.CapacityManager;
import com.cloud.configuration.Config;
import com.cloud.configuration.Resource;
@ -75,6 +67,7 @@ import com.cloud.deploy.DataCenterDeployment;
import com.cloud.deploy.DeployDestination;
import com.cloud.deploy.DeploymentPlanner;
import com.cloud.deploy.DeploymentPlanningManager;
import com.cloud.event.ActionEvent;
import com.cloud.event.EventTypes;
import com.cloud.event.UsageEventUtils;
import com.cloud.exception.InsufficientAddressCapacityException;
@ -83,6 +76,7 @@ import com.cloud.exception.InsufficientVirtualNetworkCapacityException;
import com.cloud.exception.InvalidParameterValueException;
import com.cloud.exception.PermissionDeniedException;
import com.cloud.exception.ResourceAllocationException;
import com.cloud.exception.UnsupportedServiceException;
import com.cloud.host.Host;
import com.cloud.host.HostVO;
import com.cloud.host.Status;
@ -103,6 +97,8 @@ import com.cloud.service.ServiceOfferingVO;
import com.cloud.service.dao.ServiceOfferingDao;
import com.cloud.storage.GuestOS;
import com.cloud.storage.GuestOSHypervisor;
import com.cloud.storage.Snapshot;
import com.cloud.storage.SnapshotVO;
import com.cloud.storage.StoragePool;
import com.cloud.storage.VMTemplateStoragePoolVO;
import com.cloud.storage.VMTemplateVO;
@ -112,6 +108,7 @@ import com.cloud.storage.VolumeVO;
import com.cloud.storage.dao.DiskOfferingDao;
import com.cloud.storage.dao.GuestOSDao;
import com.cloud.storage.dao.GuestOSHypervisorDao;
import com.cloud.storage.dao.SnapshotDao;
import com.cloud.storage.dao.VMTemplateDao;
import com.cloud.storage.dao.VMTemplatePoolDao;
import com.cloud.storage.dao.VolumeDao;
@ -127,7 +124,9 @@ import com.cloud.utils.exception.CloudRuntimeException;
import com.cloud.utils.net.NetUtils;
import com.cloud.vm.DiskProfile;
import com.cloud.vm.NicProfile;
import com.cloud.vm.NicVO;
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;
@ -135,7 +134,9 @@ import com.cloud.vm.VirtualMachineProfile;
import com.cloud.vm.VirtualMachineProfileImpl;
import com.cloud.vm.VmDetailConstants;
import com.cloud.vm.dao.NicDao;
import com.cloud.vm.dao.UserVmDao;
import com.cloud.vm.dao.VMInstanceDao;
import com.cloud.vm.snapshot.dao.VMSnapshotDao;
import com.google.common.base.Strings;
import com.google.gson.Gson;
@ -243,6 +244,7 @@ public class UnmanagedVMsManagerImpl implements UnmanagedVMsManager {
}
if (host != null) {
response.setHostId(host.getUuid());
response.setHostName(host.getName());
}
response.setPowerState(instance.getPowerState().toString());
response.setCpuCores(instance.getCpuCores());
@ -1078,6 +1080,10 @@ public class UnmanagedVMsManagerImpl implements UnmanagedVMsManager {
if (cluster.getHypervisorType() != Hypervisor.HypervisorType.VMware) {
throw new InvalidParameterValueException(String.format("VM ingestion is currently not supported for hypervisor: %s", cluster.getHypervisorType().toString()));
}
String keyword = cmd.getKeyword();
if (StringUtils.isNotEmpty(keyword)) {
keyword = keyword.toLowerCase();
}
List<HostVO> hosts = resourceManager.listHostsInClusterByStatus(clusterId, Status.Up);
List<String> additionalNameFilters = getAdditionalNameFilters(cluster);
List<UnmanagedInstanceResponse> responses = new ArrayList<>();
@ -1097,11 +1103,15 @@ public class UnmanagedVMsManagerImpl implements UnmanagedVMsManager {
continue;
}
GetUnmanagedInstancesAnswer unmanagedInstancesAnswer = (GetUnmanagedInstancesAnswer) answer;
HashMap<String, UnmanagedInstanceTO> unmanagedInstances = new HashMap<>();
unmanagedInstances.putAll(unmanagedInstancesAnswer.getUnmanagedInstances());
HashMap<String, UnmanagedInstanceTO> unmanagedInstances = new HashMap<>(unmanagedInstancesAnswer.getUnmanagedInstances());
Set<String> keys = unmanagedInstances.keySet();
for (String key : keys) {
responses.add(createUnmanagedInstanceResponse(unmanagedInstances.get(key), cluster, host));
UnmanagedInstanceTO instance = unmanagedInstances.get(key);
if (StringUtils.isNotEmpty(keyword) &&
!instance.getName().toLowerCase().contains(keyword)) {
continue;
}
responses.add(createUnmanagedInstanceResponse(instance, cluster, host));
}
}
ListResponse<UnmanagedInstanceResponse> listResponses = new ListResponse<>();

View File

@ -219,6 +219,7 @@
"label.action.lock.account.processing": "Locking account....",
"label.action.manage.cluster": "Manage Cluster",
"label.action.manage.cluster.processing": "Managing Cluster....",
"label.action.import.export.instances":"Import-Export Instances",
"label.action.migrate.instance": "Migrate Instance",
"label.action.migrate.instance.processing": "Migrating Instance....",
"label.action.migrate.router": "Migrate Router",
@ -276,6 +277,8 @@
"label.action.unmanage.cluster": "Unmanage Cluster",
"label.action.unmanage.cluster.processing": "Unmanaging Cluster....",
"label.action.unmanage.virtualmachine": "Unmanage VM",
"label.action.unmanage.instance": "Unmanage Instance",
"label.action.unmanage.instances": "Unmanage Instances",
"label.action.update.offering.access": "Update Offering Access",
"label.action.update.os.preference": "Update OS Preference",
"label.action.update.os.preference.processing": "Updating OS Preference....",
@ -461,6 +464,8 @@
"label.asyncbackup": "Async Backup",
"label.author.email": "Author e-mail",
"label.author.name": "Author name",
"label.auto.assign.diskoffering.disk.size": "Automatically assign offering matching the disk size",
"label.auto.assign.random.ip": "Automatically assign a random IP address",
"label.autoscale": "AutoScale",
"label.autoscale.configuration.wizard": "AutoScale Configuration Wizard",
"label.availability": "Availability",
@ -773,6 +778,7 @@
"label.disk.offering.access": "Disk offering access",
"label.disk.offering.details": "Disk offering details",
"label.disk.offerings": "Disk Offerings",
"label.disk.selection": "Disk Selection",
"label.disk.size": "Disk Size",
"label.disk.volume": "Disk Volume",
"label.diskbytesreadrate": "Disk Read Rate (BPS)",
@ -1057,6 +1063,7 @@
"label.ikeversion": "IKE Version",
"label.images": "Images",
"label.import.backup.offering": "Import Backup Offering",
"label.import.instance": "Import Instance",
"label.import.offering": "Import Offering",
"label.import.role": "Import Role",
"label.in.progress": "in progress",
@ -1301,6 +1308,7 @@
"label.manage.resources": "Manage Resources",
"label.manage.vpn.user": "Manage VPN Users",
"label.managedstate": "Managed State",
"label.managed.instances": "Managed Instances",
"label.management": "Management",
"label.management.ips": "Management IP Addresses",
"label.management.server": "Management Server",
@ -1383,6 +1391,7 @@
"label.metrics.network.usage": "Network Usage",
"label.metrics.network.write": "Write",
"label.metrics.num.cpu.cores": "Cores",
"label.migrate.allowed": "Migrate Allowed",
"label.migrate.data.from.image.store": "Migrate Data from Image store",
"label.migrate.instance.to": "Migrate instance to",
"label.migrate.instance.to.host": "Migrate instance to another host",
@ -1453,6 +1462,7 @@
"label.network.offering.display.text": "Network Offering Display Text",
"label.network.offering.name": "Network Offering Name",
"label.network.offerings": "Network Offerings",
"label.network.selection": "Network Selection",
"label.network.service.providers": "Network Service Providers",
"label.networkcidr": "Network CIDR",
"label.networkdevicetype": "Type",
@ -1493,6 +1503,7 @@
"label.nfscachepath": "Path",
"label.nfscachezoneid": "Zone",
"label.nfsserver": "NFS Server",
"label.nic": "NIC",
"label.nicadaptertype": "NIC adapter type",
"label.nicira.controller.address": "Controller Address",
"label.nicira.nvp.details": "Nicira NVP details",
@ -1844,6 +1855,7 @@
"label.rolename": "Role",
"label.roles": "Roles",
"label.roletype": "Role Type",
"label.rootdisk": "ROOT disk",
"label.rootdisksize": "Root disk size (GB)",
"label.root.certificate": "Root certificate",
"label.root.disk.offering": "Root Disk Offering",
@ -2133,6 +2145,8 @@
"label.templatesubject": "Subject",
"label.templatetotal": "Template",
"label.templatetype": "Template Type",
"label.template.temporary.import": "Use a temporary template for import",
"label.template.select.existing": "Select an existing template",
"label.tftp.dir": "TFTP Directory",
"label.tftpdir": "Tftp root directory",
"label.theme.default": "Default Theme",
@ -2151,6 +2165,7 @@
"label.to": "to",
"label.token": "Token",
"label.token.for.dashboard.login": "Token for dashboard login can be retrieved using following command",
"label.tools": "Tools",
"label.total": "Total",
"label.total.hosts": "Total Hosts",
"label.total.memory": "Total Memory",
@ -2177,6 +2192,9 @@
"label.unit": "Usage Unit",
"label.unknown": "Unknown",
"label.unlimited": "Unlimited",
"label.unmanage.instance": "Unmanage Instance",
"label.unmanaged.instance": "Unmanaged Instance",
"label.unmanaged.instances": "Unmanaged Instances",
"label.untagged": "Untagged",
"label.update.instance.group": "Update Instance Group",
"label.update.physical.network": "Update Physical Network",
@ -2487,7 +2505,10 @@
"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.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 virtual machine.",
"message.action.unmanage.virtualmachines": "Please confirm that you want to unmanage the virtual machines.",
"message.action.vmsnapshot.create": "Please confirm that you want to take a snapshot of this instance. <br>Please notice that the instance will be paused during the snapshoting, and resumed after snapshotting, if it runs on KVM.",
"message.action.vmsnapshot.delete": "Please confirm that you want to delete this VM snapshot. <br>Please notice that the instance will be paused before the snapshot deletion, and resumed after deletion, if it runs on KVM.",
"message.action.vmsnapshot.revert": "Revert VM snapshot",
@ -2786,6 +2807,7 @@
"message.enabling.zone.dots": "Enabling zone...",
"message.enter.seperated.list.multiple.cidrs": "Please enter a comma separated list of CIDRs if more than one",
"message.enter.token": "Please enter the token that you were given in your invite e-mail.",
"message.enter.valid.nic.ip": "Please enter a valid IP address for NIC",
"message.error.access.key": "Please enter Access Key",
"message.error.add.guest.network": "Either IPv4 fields or IPv6 fields need to be filled when adding a guest network",
"message.error.add.secondary.ipaddress": "There was an error adding the secondary IP Address",
@ -2917,6 +2939,7 @@
"message.guestnetwork.state.shutdown": "Indicates the network configuration is being destroyed",
"message.host.dedicated": "Host Dedicated",
"message.host.dedication.released": "Host dedication released",
"message.desc.importexportinstancewizard": "Import and export instances to/from an existing VMware zone.<br/><br/>This feature only applies Cloudstack VMware zones. By choosing to Manage an instance, CloudStack takes over the orchestration of that instance. The instance is left running and not physically moved. Unmanaging instances, removes CloudStack ability to mange them (but they are left running and not destroyed)",
"message.info.cloudian.console": "Cloudian Management Console should open in another window",
"message.installwizard.click.retry": "Click the button to retry launch.",
"message.installwizard.copy.whatisacluster": "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. Virtual machine instances (VMs) can be live-migrated from one host to another within the same cluster, without interrupting service to the user. A cluster is the third-largest organizational unit within a CloudStack™; deployment. Clusters are contained within pods, and pods are contained within zones.<br/><br/>CloudStack™; allows multiple clusters in a cloud deployment, but for a Basic Installation, we only need one cluster.",
@ -2952,10 +2975,13 @@
"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.name": "A name for your network",
"message.instance.scaled.up.confirm": "Do you really want to scale Up your instance ?",
"message.instances.managed": "Instances or VMs controlled by CloudStack",
"message.instances.scaled.up.confirm": "Do you really want to scale Up your instance ?",
"message.instances.unmanaged": "Instances or VMs not controlled by CloudStack",
"message.instancewizard.notemplates": "You do not have any templates available; please add a compatible template, and re-launch the instance wizard.",
"message.interloadbalance.not.return.elementid": "error: listInternalLoadBalancerElements API doesn't return Internal LB Element Id",
"message.ip.address.changed": "Your IP addresses may have changed; would you like to refresh the listing? Note that in this case the details pane will close.",
"message.ip.address.changes.effect.after.vm.restart": "IP address changes takes effect only after VM restart.",
"message.iso.desc": "Disc image containing data or bootable media for OS",
"message.join.project": "You have now joined a project. Please switch to Project view to see the project.",
"message.kubeconfig.cluster.not.available": "Kubernetes cluster kubeconfig not available currently",
@ -3037,6 +3063,7 @@
"message.pending.projects.2": "To view, please go to the projects section, then select invitations from the drop-down.",
"message.please.add.at.lease.one.traffic.range": "Please add at least one traffic range.",
"message.please.confirm.remove.ssh.key.pair": "Please confirm that you want to remove this SSH Key Pair",
"message.please.enter.valid.value": "Please enter a valid value",
"message.please.enter.value": "Please enter values",
"message.please.proceed": "Please proceed to the next step.",
"message.please.select.a.configuration.for.your.zone": "Please select a configuration for your zone.",
@ -3110,10 +3137,12 @@
"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 VM to belong to:",
"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.instance": "Please select an instance.",
"message.select.iso": "Please select an ISO for your new virtual instance.",
"message.select.item": "Please select an item.",
"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 VM",
"message.select.template": "Please select a template for your new virtual instance.",
"message.select.tier": "Please select a tier",
@ -3184,6 +3213,7 @@
"message.success.edit.acl": "Successfully edited ACL rule",
"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.migrate.volume": "Successfully migrated volume",
"message.success.migrating": "Migration completed successfully for",
"message.success.move.acl.order": "Successfully moved ACL rule",
@ -3213,6 +3243,7 @@
"message.success.upload.iso.description": "This ISO file has been uploaded. Please check its status in the Images > ISOs menu",
"message.success.upload.template.description": "This template file has been uploaded. Please check its status at Templates menu",
"message.success.upload.volume.description": "This Volume has been uploaded. Please check its status in the Volumes menu",
"message.success.unmanage.instance": "Successfully unmanaged instance",
"message.suspend.project": "Are you sure you want to suspend this project?",
"message.sussess.discovering.feature": "Discovered all available features!",
"message.switch.to": "Switched to",
@ -3221,6 +3252,7 @@
"message.template.copying": "Template is being copied.",
"message.template.desc": "OS image that can be used to boot VMs",
"message.template.iso": "Please select a template or ISO to continue",
"message.template.import.vm.temporary": "If a temporary template is used, reset VM operation will not work after import.",
"message.tier.required": "Tier is required",
"message.tooltip.dns.1": "Name of a DNS server for use by VMs in the zone. The public IP addresses for the zone must have a route to this server.",
"message.tooltip.dns.2": "A second DNS server name for use by VMs in the zone. The public IP addresses for the zone must have a route to this server.",

View File

@ -0,0 +1,117 @@
// 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.
<template>
<div v-if="visible" style="width: 100%">
<a-col>
<a-row :md="24" :lg="layout === 'horizontal' ? 12 : 24">
<a-checkbox
v-decorator="[checkBoxDecorator, {}]"
:checked="checked"
@change="handleCheckChange">
{{ checkBoxLabel }}
</a-checkbox>
</a-row>
<a-row :md="24" :lg="layout === 'horizontal' ? 12 : 24">
<a-form-item
:label="inputLabel"
v-if="reversed !== checked">
<a-input
v-decorator="[inputDecorator, {
initialValue: defaultInputValue
}]"
@change="val => handleInputChangeTimed(val)" />
</a-form-item>
</a-row>
</a-col>
</div>
</template>
<script>
export default {
name: 'CheckBoxInputPair',
props: {
layout: {
type: String,
default: 'horizontal'
},
resourceKey: {
type: String,
required: true
},
checkBoxLabel: {
type: String,
required: true
},
checkBoxDecorator: {
type: String,
default: ''
},
defaultCheckBoxValue: {
type: Boolean,
default: false
},
defaultInputValue: {
type: String,
default: ''
},
inputLabel: {
type: String,
default: ''
},
inputDecorator: {
type: String,
default: ''
},
visible: {
type: Boolean,
default: true
},
reversed: {
type: Boolean,
default: false
}
},
data () {
return {
checked: false,
inputValue: '',
inputUpdateTimer: null
}
},
created () {
this.checked = this.defaultCheckBoxValue
},
methods: {
handleCheckChange (e) {
this.checked = e.target.checked
this.$emit('handle-checkinputpair-change', this.resourceKey, this.checked, this.inputValue)
},
handleInputChange (e) {
this.inputValue = e.target.value
this.$emit('handle-checkinputpair-change', this.resourceKey, this.checked, this.inputValue)
},
handleInputChangeTimed (e) {
clearTimeout(this.inputUpdateTimer)
this.inputUpdateTimer = setTimeout(() => {
this.handleInputChange(e)
}, 500)
}
}
}
</script>

View File

@ -16,26 +16,39 @@
// under the License.
<template>
<div>
<a-checkbox v-decorator="[checkBoxDecorator, {}]" class="pair-checkbox" @change="handleCheckChange">
{{ checkBoxLabel }}
</a-checkbox>
<a-form-item class="pair-select-container" :label="selectLabel" v-if="this.checked">
<a-select
v-decorator="[selectDecorator, {
initialValue: selectedOption ? selectedOption : this.getSelectInitialValue()
}]"
showSearch
optionFilterProp="children"
:filterOption="(input, option) => {
return option.componentOptions.children[0].text.toLowerCase().indexOf(input.toLowerCase()) >= 0
}"
@change="val => { this.handleSelectChange(val) }">
<a-select-option v-for="(opt) in selectOptions" :key="opt.name" :disabled="opt.enabled === false">
{{ opt.name || opt.description }}
</a-select-option>
</a-select>
</a-form-item>
<div style="width: 100%">
<a-row :gutter="6">
<a-col :md="24" :lg="layout === 'horizontal' ? 12 : 24">
<a-checkbox
v-decorator="[checkBoxDecorator, {}]"
:checked="checked"
@change="handleCheckChange">
{{ checkBoxLabel }}
</a-checkbox>
</a-col>
<a-col :md="24" :lg="layout === 'horizontal' ? 12 : 24">
<a-form-item
v-if="reversed != checked"
:label="selectLabel">
<a-select
v-decorator="[selectDecorator, { initialValue: selectedOption ? selectedOption : getSelectInitialValue()}]"
:defaultValue="selectDecorator ? undefined : selectedOption ? selectedOption : getSelectInitialValue()"
showSearch
optionFilterProp="children"
:filterOption="(input, option) => {
return option.componentOptions.children[0].text.toLowerCase().indexOf(input.toLowerCase()) >= 0
}"
@change="val => { this.handleSelectChange(val) }">
<a-select-option
v-for="(opt) in selectSource"
:key="opt.id"
:disabled="opt.enabled === false">
{{ opt.displaytext || opt.name || opt.description }}
</a-select-option>
</a-select>
</a-form-item>
</a-col>
</a-row>
</div>
</template>
@ -44,6 +57,10 @@
export default {
name: 'CheckBoxSelectPair',
props: {
layout: {
type: String,
default: 'horizontal'
},
resourceKey: {
type: String,
required: true
@ -56,6 +73,10 @@ export default {
type: String,
default: ''
},
defaultCheckBoxValue: {
type: Boolean,
default: false
},
selectOptions: {
type: Array,
required: true
@ -67,12 +88,30 @@ export default {
selectDecorator: {
type: String,
default: ''
},
reversed: {
type: Boolean,
default: false
}
},
data () {
return {
checked: false,
selectedOption: ''
selectedOption: null
}
},
created () {
this.checked = this.defaultCheckBoxValue
},
computed: {
selectSource () {
return this.selectOptions.map(item => {
var option = { ...item }
if (!('id' in option)) {
option.id = option.name
}
return option
})
}
},
methods: {
@ -80,30 +119,18 @@ export default {
return array !== null && array !== undefined && Array.isArray(array) && array.length > 0
},
getSelectInitialValue () {
const provider = this.selectOptions?.filter(x => x.enabled)?.[0]?.name || ''
this.handleSelectChange(provider)
return provider
const initialValue = this.selectSource?.filter(x => x.enabled !== false)?.[0]?.id || ''
this.handleSelectChange(initialValue)
return initialValue
},
handleCheckChange (e) {
this.checked = e.target.checked
this.$emit('handle-checkpair-change', this.resourceKey, this.checked, '')
this.$emit('handle-checkselectpair-change', this.resourceKey, this.checked, this.selectedOption)
},
handleSelectChange (val) {
this.selectedOption = val
this.$emit('handle-checkpair-change', this.resourceKey, this.checked, val)
this.$emit('handle-checkselectpair-change', this.resourceKey, this.checked, this.selectedOption)
}
}
}
</script>
<style scoped lang="scss">
.pair-checkbox {
width: 180px;
}
.pair-select-container {
position: relative;
float: right;
margin-bottom: -5px;
width: 20vw;
}
</style>

View File

@ -280,7 +280,7 @@
:key="eth.id"
style="margin-left: -24px; margin-top: 5px;">
<a-icon type="api" />eth{{ index }} {{ eth.ipaddress }}
<router-link v-if="eth.networkname && eth.networkid" :to="{ path: '/guestnetwork/' + eth.networkid }">({{ eth.networkname }})</router-link>
<router-link v-if="!isStatic && eth.networkname && eth.networkid" :to="{ path: '/guestnetwork/' + eth.networkid }">({{ eth.networkname }})</router-link>
<a-tag v-if="eth.isdefault">
{{ $t('label.default') }}
</a-tag >
@ -311,7 +311,7 @@
type="environment"
@click="$message.success(`${$t('label.copied.clipboard')} : ${ ipaddress }`)"
v-clipboard:copy="ipaddress" />
<router-link v-if="resource.ipaddressid" :to="{ path: '/publicip/' + resource.ipaddressid }">{{ ipaddress }}</router-link>
<router-link v-if="!isStatic && resource.ipaddressid" :to="{ path: '/publicip/' + resource.ipaddressid }">{{ ipaddress }}</router-link>
<span v-else>{{ ipaddress }}</span>
</div>
</div>
@ -329,7 +329,7 @@
<div class="resource-detail-item__label">{{ $t('label.project') }}</div>
<div class="resource-detail-item__details">
<a-icon type="project" />
<router-link v-if="resource.projectid" :to="{ path: '/project/' + resource.projectid }">{{ resource.project || resource.projectname || resource.projectid }}</router-link>
<router-link v-if="!isStatic && resource.projectid" :to="{ path: '/project/' + resource.projectid }">{{ resource.project || resource.projectname || resource.projectid }}</router-link>
<router-link v-else :to="{ path: '/project', query: { name: resource.projectname }}">{{ resource.projectname }}</router-link>
</div>
</div>
@ -412,10 +412,10 @@
<div class="resource-detail-item__details">
<a-icon type="picture" />
<div v-if="resource.isoid">
<router-link :to="{ path: '/iso/' + resource.isoid }">{{ resource.isoname || resource.isoid }} </router-link>
<router-link :to="{ path: '/iso/' + resource.isoid }">{{ resource.isodisplaytext || resource.isoname || resource.isoid }} </router-link>
</div>
<div v-else>
<router-link :to="{ path: '/template/' + resource.templateid }">{{ resource.templatename || resource.templateid }} </router-link>
<router-link :to="{ path: '/template/' + resource.templateid }">{{ resource.templatedisplaytext || resource.templatename || resource.templateid }} </router-link>
</div>
</div>
</div>
@ -423,7 +423,7 @@
<div class="resource-detail-item__label">{{ $t('label.serviceofferingname') }}</div>
<div class="resource-detail-item__details">
<a-icon type="cloud" />
<router-link v-if="$route.meta.name === 'router'" :to="{ path: '/computeoffering/' + resource.serviceofferingid, query: { issystem: true } }">{{ resource.serviceofferingname || resource.serviceofferingid }} </router-link>
<router-link v-if="!isStatic && $route.meta.name === 'router'" :to="{ path: '/computeoffering/' + resource.serviceofferingid, query: { issystem: true } }">{{ resource.serviceofferingname || resource.serviceofferingid }} </router-link>
<router-link v-else-if="$router.resolve('/computeoffering/' + resource.serviceofferingid).route.name !== '404'" :to="{ path: '/computeoffering/' + resource.serviceofferingid }">{{ resource.serviceofferingname || resource.serviceofferingid }} </router-link>
<span v-else>{{ resource.serviceofferingname || resource.serviceofferingid }}</span>
</div>
@ -432,21 +432,21 @@
<div class="resource-detail-item__label">{{ $t('label.diskoffering') }}</div>
<div class="resource-detail-item__details">
<a-icon type="hdd" />
<router-link v-if="$router.resolve('/diskoffering/' + resource.diskofferingid).route.name !== '404'" :to="{ path: '/diskoffering/' + resource.diskofferingid }">{{ resource.diskofferingname || resource.diskofferingid }} </router-link>
<router-link v-if="!isStatic && $router.resolve('/diskoffering/' + resource.diskofferingid).route.name !== '404'" :to="{ path: '/diskoffering/' + resource.diskofferingid }">{{ resource.diskofferingname || resource.diskofferingid }} </router-link>
<span v-else>{{ resource.diskofferingname || resource.diskofferingid }}</span>
</div>
</div>
<div class="resource-detail-item" v-if="resource.backupofferingid">
<div class="resource-detail-item__label">{{ $t('label.backupofferingid') }}</div>
<a-icon type="cloud-upload" />
<router-link v-if="$router.resolve('/backupoffering/' + resource.backupofferingid).route.name !== '404'" :to="{ path: '/backupoffering/' + resource.backupofferingid }">{{ resource.backupofferingname || resource.backupofferingid }} </router-link>
<router-link v-if="!isStatic && $router.resolve('/backupoffering/' + resource.backupofferingid).route.name !== '404'" :to="{ path: '/backupoffering/' + resource.backupofferingid }">{{ resource.backupofferingname || resource.backupofferingid }} </router-link>
<span v-else>{{ resource.backupofferingname || resource.backupofferingid }}</span>
</div>
<div class="resource-detail-item" v-if="resource.networkofferingid">
<div class="resource-detail-item__label">{{ $t('label.networkofferingid') }}</div>
<div class="resource-detail-item__details">
<a-icon type="wifi" />
<router-link v-if="$router.resolve('/networkoffering/' + resource.networkofferingid).route.name !== '404'" :to="{ path: '/networkoffering/' + resource.networkofferingid }">{{ resource.networkofferingname || resource.networkofferingid }} </router-link>
<router-link v-if="!isStatic && $router.resolve('/networkoffering/' + resource.networkofferingid).route.name !== '404'" :to="{ path: '/networkoffering/' + resource.networkofferingid }">{{ resource.networkofferingname || resource.networkofferingid }} </router-link>
<span v-else>{{ resource.networkofferingname || resource.networkofferingid }}</span>
</div>
</div>
@ -454,7 +454,7 @@
<div class="resource-detail-item__label">{{ $t('label.vpcoffering') }}</div>
<div class="resource-detail-item__details">
<a-icon type="deployment-unit" />
<router-link v-if="$router.resolve('/vpcoffering/' + resource.vpcofferingid).route.name !== '404'" :to="{ path: '/vpcoffering/' + resource.vpcofferingid }">{{ resource.vpcofferingname || resource.vpcofferingid }} </router-link>
<router-link v-if="!isStatic && $router.resolve('/vpcoffering/' + resource.vpcofferingid).route.name !== '404'" :to="{ path: '/vpcoffering/' + resource.vpcofferingid }">{{ resource.vpcofferingname || resource.vpcofferingid }} </router-link>
<span v-else>{{ resource.vpcofferingname || resource.vpcofferingid }}</span>
</div>
</div>
@ -462,7 +462,7 @@
<div class="resource-detail-item__label">{{ $t('label.storagepool') }}</div>
<div class="resource-detail-item__details">
<a-icon type="database" />
<router-link v-if="$router.resolve('/storagepool/' + resource.storageid).route.name !== '404'" :to="{ path: '/storagepool/' + resource.storageid }">{{ resource.storage || resource.storageid }} </router-link>
<router-link v-if="!isStatic && $router.resolve('/storagepool/' + resource.storageid).route.name !== '404'" :to="{ path: '/storagepool/' + resource.storageid }">{{ resource.storage || resource.storageid }} </router-link>
<span v-else>{{ resource.storage || resource.storageid }}</span>
<a-tag style="margin-left: 5px;" v-if="resource.storagetype">
{{ resource.storagetype }}
@ -473,7 +473,7 @@
<div class="resource-detail-item__label">{{ $t('label.hostname') }}</div>
<div class="resource-detail-item__details">
<a-icon type="desktop" />
<router-link v-if="$router.resolve('/host/' + resource.hostid).route.name !== '404'" :to="{ path: '/host/' + resource.hostid }">{{ resource.hostname || resource.hostid }} </router-link>
<router-link v-if="!isStatic && $router.resolve('/host/' + resource.hostid).route.name !== '404'" :to="{ path: '/host/' + resource.hostid }">{{ resource.hostname || resource.hostid }} </router-link>
<span v-else>{{ resource.hostname || resource.hostid }}</span>
</div>
</div>
@ -481,7 +481,7 @@
<div class="resource-detail-item__label">{{ $t('label.clusterid') }}</div>
<div class="resource-detail-item__details">
<a-icon type="cluster" />
<router-link v-if="$router.resolve('/cluster/' + resource.clusterid).route.name !== '404'" :to="{ path: '/cluster/' + resource.clusterid }">{{ resource.clustername || resource.cluster || resource.clusterid }}</router-link>
<router-link v-if="!isStatic && $router.resolve('/cluster/' + resource.clusterid).route.name !== '404'" :to="{ path: '/cluster/' + resource.clusterid }">{{ resource.clustername || resource.cluster || resource.clusterid }}</router-link>
<span v-else>{{ resource.clustername || resource.cluster || resource.clusterid }}</span>
</div>
</div>
@ -489,7 +489,7 @@
<div class="resource-detail-item__label">{{ $t('label.podid') }}</div>
<div class="resource-detail-item__details">
<a-icon type="appstore" />
<router-link v-if="$router.resolve('/pod/' + resource.podid).route.name !== '404'" :to="{ path: '/pod/' + resource.podid }">{{ resource.podname || resource.pod || resource.podid }}</router-link>
<router-link v-if="!isStatic && $router.resolve('/pod/' + resource.podid).route.name !== '404'" :to="{ path: '/pod/' + resource.podid }">{{ resource.podname || resource.pod || resource.podid }}</router-link>
<span v-else>{{ resource.podname || resource.pod || resource.podid }}</span>
</div>
</div>
@ -497,7 +497,7 @@
<div class="resource-detail-item__label">{{ $t('label.zone') }}</div>
<div class="resource-detail-item__details">
<a-icon type="global" />
<router-link v-if="$router.resolve('/zone/' + resource.zoneid).route.name !== '404'" :to="{ path: '/zone/' + resource.zoneid }">{{ resource.zone || resource.zonename || resource.zoneid }}</router-link>
<router-link v-if="!isStatic && $router.resolve('/zone/' + resource.zoneid).route.name !== '404'" :to="{ path: '/zone/' + resource.zoneid }">{{ resource.zone || resource.zonename || resource.zoneid }}</router-link>
<span v-else>{{ resource.zone || resource.zonename || resource.zoneid }}</span>
</div>
</div>
@ -508,7 +508,7 @@
<template v-for="(item,idx) in resource.owner">
<span style="margin-right:5px" :key="idx">
<span v-if="$store.getters.userInfo.roletype !== 'User'">
<router-link v-if="'user' in item" :to="{ path: '/accountuser', query: { username: item.user, domainid: resource.domainid }}">{{ item.account + '(' + item.user + ')' }}</router-link>
<router-link v-if="!isStatic && 'user' in item" :to="{ path: '/accountuser', query: { username: item.user, domainid: resource.domainid }}">{{ item.account + '(' + item.user + ')' }}</router-link>
<router-link v-else :to="{ path: '/account', query: { name: item.account, domainid: resource.domainid } }">{{ item.account }}</router-link>
</span>
<span v-else>{{ item.user ? item.account + '(' + item.user + ')' : item.account }}</span>
@ -520,7 +520,7 @@
<div class="resource-detail-item__label">{{ $t('label.account') }}</div>
<div class="resource-detail-item__details">
<a-icon type="user" />
<router-link v-if="$store.getters.userInfo.roletype !== 'User'" :to="{ path: '/account', query: { name: resource.account, domainid: resource.domainid } }">{{ resource.account }}</router-link>
<router-link v-if="!isStatic && $store.getters.userInfo.roletype !== 'User'" :to="{ path: '/account', query: { name: resource.account, domainid: resource.domainid } }">{{ resource.account }}</router-link>
<span v-else>{{ resource.account }}</span>
</div>
</div>
@ -528,7 +528,7 @@
<div class="resource-detail-item__label">{{ $t('label.role') }}</div>
<div class="resource-detail-item__details">
<a-icon type="idcard" />
<router-link v-if="$router.resolve('/role/' + resource.roleid).route.name !== '404'" :to="{ path: '/role/' + resource.roleid }">{{ resource.rolename || resource.role || resource.roleid }}</router-link>
<router-link v-if="!isStatic && $router.resolve('/role/' + resource.roleid).route.name !== '404'" :to="{ path: '/role/' + resource.roleid }">{{ resource.rolename || resource.role || resource.roleid }}</router-link>
<span v-else>{{ resource.rolename || resource.role || resource.roleid }}</span>
</div>
</div>
@ -536,7 +536,7 @@
<div class="resource-detail-item__label">{{ $t('label.domain') }}</div>
<div class="resource-detail-item__details">
<a-icon type="block" />
<router-link v-if="$store.getters.userInfo.roletype !== 'User'" :to="{ path: '/domain/' + resource.domainid + '?tab=details' }">{{ resource.domain || resource.domainid }}</router-link>
<router-link v-if="!isStatic && $store.getters.userInfo.roletype !== 'User'" :to="{ path: '/domain/' + resource.domainid + '?tab=details' }">{{ resource.domain || resource.domainid }}</router-link>
<span v-else>{{ resource.domain || resource.domainid }}</span>
</div>
</div>
@ -544,7 +544,7 @@
<div class="resource-detail-item__label">{{ $t('label.management.servers') }}</div>
<div class="resource-detail-item__details">
<a-icon type="rocket" />
<router-link v-if="$router.resolve('/managementserver/' + resource.managementserverid).route.name !== '404'" :to="{ path: '/managementserver/' + resource.managementserverid }">{{ resource.managementserver || resource.managementserverid }}</router-link>
<router-link v-if="!isStatic && $router.resolve('/managementserver/' + resource.managementserverid).route.name !== '404'" :to="{ path: '/managementserver/' + resource.managementserverid }">{{ resource.managementserver || resource.managementserverid }}</router-link>
<span v-else>{{ resource.managementserver || resource.managementserverid }}</span>
</div>
</div>
@ -607,7 +607,7 @@
</div>
</div>
<div class="account-center-tags" v-if="resourceType && 'listTags' in $store.getters.apis">
<div class="account-center-tags" v-if="!isStatic && resourceType && 'listTags' in $store.getters.apis">
<a-divider/>
<a-spin :spinning="loadingTags">
<div class="title">{{ $t('label.tags') }}</div>
@ -639,7 +639,7 @@
</a-spin>
</div>
<div class="account-center-team" v-if="annotationType && 'listAnnotations' in $store.getters.apis">
<div class="account-center-team" v-if="!isStatic && annotationType && 'listAnnotations' in $store.getters.apis">
<a-divider :dashed="true"/>
<a-spin :spinning="loadingAnnotations">
<div class="title">
@ -725,6 +725,10 @@ export default {
bordered: {
type: Boolean,
default: true
},
isStatic: {
type: Boolean,
default: false
}
},
data () {

View File

@ -133,7 +133,6 @@ export default {
default: () => {}
}
},
inject: ['parentSearch', 'parentChangeFilter'],
data () {
return {
searchQuery: null,
@ -419,7 +418,7 @@ export default {
onSearch (value) {
this.paramsFilter = {}
this.searchQuery = value
this.parentSearch({ searchQuery: this.searchQuery })
this.$emit('search', { searchQuery: this.searchQuery })
},
onClear () {
this.searchFilters.map(item => {
@ -432,7 +431,7 @@ export default {
this.inputValue = null
this.searchQuery = null
this.paramsFilter = {}
this.parentSearch(this.paramsFilter)
this.$emit('search', this.paramsFilter)
},
handleSubmit (e) {
e.preventDefault()
@ -455,7 +454,7 @@ export default {
this.paramsFilter['tags[0].value'] = this.inputValue
}
}
this.parentSearch(this.paramsFilter)
this.$emit('search', this.paramsFilter)
})
},
handleKeyChange (e) {
@ -465,7 +464,7 @@ export default {
this.inputValue = e.target.value
},
changeFilter (filter) {
this.parentChangeFilter(filter)
this.$emit('change-filter', filter)
}
}
}

View File

@ -105,6 +105,7 @@ export default {
case 'True':
case 'Up':
case 'enabled':
case 'PowerOn':
case 'success':
status = 'success'
break
@ -116,6 +117,7 @@ export default {
case 'Error':
case 'False':
case 'Stopped':
case 'PowerOff':
case 'failed':
status = 'error'
break

View File

@ -34,6 +34,7 @@ import role from '@/config/section/role'
import infra from '@/config/section/infra'
import offering from '@/config/section/offering'
import config from '@/config/section/config'
import tools from '@/config/section/tools'
import quota from '@/config/section/plugin/quota'
import cloudian from '@/config/section/plugin/cloudian'
@ -224,6 +225,7 @@ export function asyncRouterMap () {
generateRouterMap(infra),
generateRouterMap(offering),
generateRouterMap(config),
generateRouterMap(tools),
generateRouterMap(quota),
generateRouterMap(cloudian),

View File

@ -0,0 +1,34 @@
// Licensed to the Apache Software Foundation (ASF) under one
// or more contributor license agreements. See the NOTICE file
// distributed with this work for additional information
// regarding copyright ownership. The ASF licenses this file
// to you under the Apache License, Version 2.0 (the
// "License"); you may not use this file except in compliance
// with the License. You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing,
// software distributed under the License is distributed on an
// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
// KIND, either express or implied. See the License for the
// specific language governing permissions and limitations
// under the License.
export default {
name: 'tools',
title: 'label.tools',
icon: 'tool',
permission: ['listInfrastructure'],
children: [
{
name: 'manageinstances',
title: 'label.action.import.export.instances',
icon: 'interaction',
docHelp: 'adminguide/virtual_machines.html#importing-and-unmanaging-virtual-machine',
resourceType: 'UserVm',
permission: ['listUnmanagedInstances'],
component: () => import('@/views/tools/ManageInstances.vue')
}
]
}

View File

@ -79,7 +79,9 @@
v-if="!dataView"
:searchFilters="searchFilters"
:searchParams="searchParams"
:apiName="apiName"/>
:apiName="apiName"
@search="onSearch"
@change-filter="changeFilter"/>
</a-col>
</a-row>
</a-card>

View File

@ -200,9 +200,9 @@
></compute-offering-selection>
<compute-selection
v-if="serviceOffering && (serviceOffering.iscustomized || serviceOffering.iscustomizediops)"
cpunumber-input-decorator="cpunumber"
cpuspeed-input-decorator="cpuspeed"
memory-input-decorator="memory"
cpuNumberInputDecorator="cpunumber"
cpuSpeedInputDecorator="cpuspeed"
memoryInputDecorator="memory"
:preFillContent="dataPreFill"
:computeOfferingId="instanceConfig.computeofferingid"
:isConstrained="'serviceofferingdetails' in serviceOffering"

View File

@ -33,9 +33,9 @@
<compute-selection
v-if="selectedOffering && selectedOffering.iscustomized"
:cpunumber-input-decorator="cpuNumberKey"
:cpuspeed-input-decorator="cpuSpeedKey"
:memory-input-decorator="memoryKey"
:cpuNumberInputDecorator="cpuNumberKey"
:cpuSpeedInputDecorator="cpuSpeedKey"
:memoryInputDecorator="memoryKey"
:computeOfferingId="selectedOffering.id"
:isConstrained="'serviceofferingdetails' in selectedOffering"
:minCpu="getMinCpu()"

View File

@ -123,11 +123,11 @@ export default {
type: Number,
default: 256
},
cpunumberInputDecorator: {
cpuNumberInputDecorator: {
type: String,
default: ''
},
cpuspeedInputDecorator: {
cpuSpeedInputDecorator: {
type: String,
default: ''
},
@ -219,10 +219,10 @@ export default {
if (!this.validateInput('cpu', value)) {
return
}
this.$emit('update-compute-cpunumber', this.cpunumberInputDecorator, value)
this.$emit('update-compute-cpunumber', this.cpuNumberInputDecorator, value)
},
updateComputeCpuSpeed (value) {
this.$emit('update-compute-cpuspeed', this.cpuspeedInputDecorator, value)
this.$emit('update-compute-cpuspeed', this.cpuSpeedInputDecorator, value)
},
updateComputeMemory (value) {
if (!value) this.memoryInputValue = 0

View File

@ -26,16 +26,35 @@
:rowSelection="rowSelection"
:scroll="{ y: 225 }" >
<span slot="offering" slot-scope="text, record">
<a-select
autoFocus
v-if="validOfferings[record.id] && validOfferings[record.id].length > 0"
@change="updateOffering($event, record.id)"
:defaultValue="validOfferings[record.id][0].id">
<a-select-option v-for="offering in validOfferings[record.id]" :key="offering.id">
{{ offering.displaytext }}
</a-select-option>
</a-select>
<span slot="name" slot-scope="text, record">
<span>{{ record.displaytext || record.name }}</span>
<div v-if="record.meta">
<template v-for="meta in record.meta">
<a-tag style="margin-top: 5px" :key="meta.key">{{ meta.key + ': ' + meta.value }}</a-tag>
</template>
</div>
</span>
<span slot="offering" slot-scope="text, record" style="width: 50%">
<span
v-if="validOfferings[record.id] && validOfferings[record.id].length > 0">
<check-box-select-pair
v-if="selectedCustomDiskOffering!=null"
layout="vertical"
:resourceKey="record.id"
:selectOptions="validOfferings[record.id]"
:checkBoxLabel="autoSelectLabel"
:defaultCheckBoxValue="true"
:reversed="true"
@handle-checkselectpair-change="updateOfferingCheckPairSelect" />
<a-select
v-else
@change="updateOfferingSelect($event, record.id)"
:defaultValue="validOfferings[record.id][0].id">
<a-select-option v-for="offering in validOfferings[record.id]" :key="offering.id">
{{ offering.displaytext }}
</a-select-option>
</a-select>
</span>
<span v-else>
{{ $t('label.no.matching.offering') }}
</span>
@ -46,9 +65,13 @@
<script>
import { api } from '@/api'
import CheckBoxSelectPair from '@/components/CheckBoxSelectPair'
export default {
name: 'MultiDiskSelection',
components: {
CheckBoxSelectPair
},
props: {
items: {
type: Array,
@ -57,6 +80,22 @@ export default {
zoneId: {
type: String,
default: () => ''
},
selectionEnabled: {
type: Boolean,
default: true
},
customOfferingsAllowed: {
type: Boolean,
default: false
},
autoSelectCustomOffering: {
type: Boolean,
default: false
},
autoSelectLabel: {
type: String,
default: ''
}
},
data () {
@ -64,7 +103,8 @@ export default {
columns: [
{
dataIndex: 'name',
title: this.$t('label.data.disk')
title: this.$t('label.data.disk'),
scopedSlots: { customRender: 'name' }
},
{
dataIndex: 'offering',
@ -76,33 +116,35 @@ export default {
selectedRowKeys: [],
diskOfferings: [],
validOfferings: {},
selectedCustomDiskOffering: null,
values: {}
}
},
computed: {
tableSource () {
return this.items.map(item => {
return {
id: item.id,
name: `${item.name} (${item.size} GB)`,
disabled: this.validOfferings[item.id] && this.validOfferings[item.id].length === 0
}
var disk = { ...item, disabled: this.validOfferings[item.id] && this.validOfferings[item.id].length === 0 }
disk.name = `${item.name} (${item.size} GB)`
return disk
})
},
rowSelection () {
return {
type: 'checkbox',
selectedRowKeys: this.selectedRowKeys,
getCheckboxProps: record => ({
props: {
disabled: record.disabled
if (this.selectionEnabled === true) {
return {
type: 'checkbox',
selectedRowKeys: this.selectedRowKeys,
getCheckboxProps: record => ({
props: {
disabled: record.disabled
}
}),
onChange: (rows) => {
this.selectedRowKeys = rows
this.sendValues()
}
}),
onChange: (rows) => {
this.selectedRowKeys = rows
this.sendValues()
}
}
return null
}
},
watch: {
@ -128,7 +170,9 @@ export default {
listall: true
}).then(response => {
this.diskOfferings = response.listdiskofferingsresponse.diskoffering || []
this.diskOfferings = this.diskOfferings.filter(x => !x.iscustomized)
if (!this.customOfferingsAllowed) {
this.diskOfferings = this.diskOfferings.filter(x => !x.iscustomized)
}
this.orderDiskOfferings()
}).finally(() => {
this.loading = false
@ -137,8 +181,11 @@ export default {
orderDiskOfferings () {
this.loading = true
this.validOfferings = {}
if (this.customOfferingsAllowed && this.autoSelectCustomOffering) {
this.selectedCustomDiskOffering = this.diskOfferings.filter(x => x.iscustomized)?.[0]
}
for (const item of this.items) {
this.validOfferings[item.id] = this.diskOfferings.filter(x => x.disksize >= item.size)
this.validOfferings[item.id] = this.diskOfferings.filter(x => x.disksize >= item.size || (this.customOfferingsAllowed && x.iscustomized))
}
this.setDefaultValues()
this.loading = false
@ -146,18 +193,31 @@ export default {
setDefaultValues () {
this.values = {}
for (const item of this.items) {
this.values[item.id] = this.validOfferings[item.id].length > 0 ? this.validOfferings[item.id][0].id : ''
this.values[item.id] = this.selectedCustomDiskOffering?.id || this.validOfferings[item.id]?.[0]?.id || ''
}
this.sendValues()
},
updateOfferingCheckPairSelect (diskId, checked, value) {
if (this.selectedCustomDiskOffering) {
this.values[diskId] = checked ? this.selectedCustomDiskOffering.id : value
this.sendValues()
}
},
updateOffering (value, templateid) {
this.values[templateid] = value
updateOfferingSelect (value, diskId) {
this.values[diskId] = value
this.sendValues()
},
sendValues () {
const data = {}
this.selectedRowKeys.map(x => {
data[x] = this.values[x]
})
if (this.selectionEnabled) {
this.selectedRowKeys.map(x => {
data[x] = this.values[x]
})
} else {
for (var x in this.values) {
data[x] = this.values[x]
}
}
this.$emit('select-multi-disk-offering', data)
}
}

View File

@ -0,0 +1,257 @@
// 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.
<template>
<div>
<a-table
:loading="loading"
:columns="columns"
:dataSource="tableSource"
:rowKey="record => record.id"
:pagination="false"
:rowSelection="rowSelection"
:scroll="{ y: 225 }" >
<span slot="name" slot-scope="text, record">
<span>{{ record.displaytext || record.name }}</span>
<div v-if="record.meta">
<template v-for="meta in record.meta">
<a-tag style="margin-top: 5px" :key="meta.key">{{ meta.key + ': ' + meta.value }}</a-tag>
</template>
</div>
</span>
<span slot="network" slot-scope="text, record">
<a-select
v-if="validNetworks[record.id] && validNetworks[record.id].length > 0"
:defaultValue="validNetworks[record.id][0].id"
@change="val => handleNetworkChange(record, val)">
<a-select-option v-for="network in validNetworks[record.id]" :key="network.id">
{{ network.displaytext + (network.broadcasturi ? ' (' + network.broadcasturi + ')' : '') }}
</a-select-option>
</a-select>
<span v-else>
{{ $t('label.no.matching.network') }}
</span>
</span>
<span slot="ipaddress" slot-scope="text, record">
<check-box-input-pair
layout="vertical"
:resourceKey="record.id"
:checkBoxLabel="$t('label.auto.assign.random.ip')"
:defaultCheckBoxValue="true"
:reversed="true"
:visible="(indexNum > 0 && ipAddressesEnabled[record.id])"
@handle-checkinputpair-change="setIpAddress" />
</span>
</a-table>
</div>
</template>
<script>
import { api } from '@/api'
import _ from 'lodash'
import CheckBoxInputPair from '@/components/CheckBoxInputPair'
export default {
name: 'MultiDiskSelection',
components: {
CheckBoxInputPair
},
props: {
items: {
type: Array,
default: () => []
},
zoneId: {
type: String,
default: () => ''
},
selectionEnabled: {
type: Boolean,
default: true
},
filterUnimplementedNetworks: {
type: Boolean,
default: false
},
filterMatchKey: {
type: String,
default: null
}
},
data () {
return {
columns: [
{
dataIndex: 'name',
title: this.$t('label.nic'),
scopedSlots: { customRender: 'name' }
},
{
dataIndex: 'network',
title: this.$t('label.network'),
scopedSlots: { customRender: 'network' }
},
{
dataIndex: 'ipaddress',
title: this.$t('label.ipaddress'),
scopedSlots: { customRender: 'ipaddress' }
}
],
loading: false,
selectedRowKeys: [],
networks: [],
validNetworks: {},
values: {},
ipAddressesEnabled: {},
ipAddresses: {},
indexNum: 1,
sendValuesTimer: null
}
},
computed: {
tableSource () {
return this.items.map(item => {
var nic = { ...item, disabled: this.validNetworks[item.id] && this.validNetworks[item.id].length === 0 }
nic.name = item.displaytext || item.name
return nic
})
},
rowSelection () {
if (this.selectionEnabled === true) {
return {
type: 'checkbox',
selectedRowKeys: this.selectedRowKeys,
getCheckboxProps: record => ({
props: {
disabled: record.disabled
}
}),
onChange: (rows) => {
this.selectedRowKeys = rows
this.sendValues()
}
}
}
return null
}
},
watch: {
items (newData, oldData) {
this.items = newData
this.selectedRowKeys = []
this.fetchNetworks()
},
zoneId (newData) {
this.zoneId = newData
this.fetchNetworks()
}
},
created () {
this.fetchNetworks()
},
methods: {
fetchNetworks () {
this.networks = []
if (!this.zoneId || this.zoneId.length === 0) {
return
}
this.loading = true
api('listNetworks', {
zoneid: this.zoneId,
listall: true
}).then(response => {
this.networks = response.listnetworksresponse.network || []
this.orderNetworks()
}).finally(() => {
this.loading = false
})
},
orderNetworks () {
this.loading = true
this.validNetworks = {}
for (const item of this.items) {
this.validNetworks[item.id] = this.networks
if (this.filterUnimplementedNetworks) {
this.validNetworks[item.id] = this.validNetworks[item.id].filter(x => x.state === 'Implemented')
}
if (this.filterMatchKey) {
this.validNetworks[item.id] = this.validNetworks[item.id].filter(x => x[this.filterMatchKey] === item[this.filterMatchKey])
}
}
this.setDefaultValues()
this.loading = false
},
setIpAddressEnabled (nic, network) {
this.ipAddressesEnabled[nic.id] = network && network.type !== 'L2'
this.indexNum = (this.indexNum % 2) + 1
},
setIpAddress (nicId, autoAssign, ipAddress) {
this.ipAddresses[nicId] = autoAssign ? 'auto' : ipAddress
this.sendValuesTimed()
},
setDefaultValues () {
this.values = {}
this.ipAddresses = {}
for (const item of this.items) {
var network = this.validNetworks[item.id]?.[0] || null
this.values[item.id] = network ? network.id : ''
this.ipAddresses[item.id] = (!network || network.type === 'L2') ? null : 'auto'
this.setIpAddressEnabled(item, network)
}
this.sendValuesTimed()
},
handleNetworkChange (nic, networkId) {
this.setIpAddressEnabled(nic, _.find(this.validNetworks[nic.id], (option) => option.id === networkId))
this.sendValuesTimed()
},
sendValuesTimed () {
clearTimeout(this.sendValuesTimer)
this.sendValuesTimer = setTimeout(() => {
this.sendValues(this.selectedScope)
}, 500)
},
sendValues () {
const data = {}
if (this.selectionEnabled) {
this.selectedRowKeys.map(x => {
var d = { network: this.values[x] }
if (this.ipAddresses[x]) {
d.ipAddress = this.ipAddresses[x]
}
data[x] = d
})
} else {
for (var x in this.values) {
var d = { network: this.values[x] }
if (this.ipAddresses[x] != null && this.ipAddresses[x] !== undefined) {
d.ipAddress = this.ipAddresses[x]
}
data[x] = d
}
}
this.$emit('select-multi-network', data)
}
}
}
</script>
<style lang="less" scoped>
.ant-table-wrapper {
margin: 2rem 0;
}
</style>

View File

@ -174,7 +174,7 @@
:checkBoxDecorator="'service.' + item.name"
:selectOptions="item.provider"
:selectDecorator="item.name + '.provider'"
@handle-checkpair-change="handleSupportedServiceChange"/>
@handle-checkselectpair-change="handleSupportedServiceChange"/>
</a-list-item>
</a-list>
</div>

View File

@ -51,7 +51,7 @@
:checkBoxDecorator="'service.' + item.name"
:selectOptions="item.provider"
:selectDecorator="item.name + '.provider'"
@handle-checkpair-change="handleSupportedServiceChange"/>
@handle-checkselectpair-change="handleSupportedServiceChange"/>
</a-list-item>
</a-list>
</div>

View File

@ -0,0 +1,739 @@
// 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.
<template>
<div>
<a-spin :spinning="loading">
<a-row :gutter="12">
<a-col :md="24" :lg="7">
<info-card
class="vm-info-card"
:isStatic="true"
:resource="resource"
:title="this.$t('label.unmanaged.instance')" />
</a-col>
<a-col :md="24" :lg="17">
<a-card :bordered="true">
<a-form
:form="form"
@submit="handleSubmit"
layout="vertical">
<a-form-item>
<tooltip-label slot="label" :title="$t('label.displayname')" :tooltip="apiParams.displayname.description"/>
<a-input
v-decorator="['displayname', {
rules: [{ required: true, message: $t('message.error.input.value') }]
}]"
:placeholder="apiParams.displayname.description"
ref="displayname"
autoFocus />
</a-form-item>
<a-form-item>
<tooltip-label slot="label" :title="$t('label.hostnamelabel')" :tooltip="apiParams.hostname.description"/>
<a-input
v-decorator="['hostname', {}]"
:placeholder="apiParams.hostname.description" />
</a-form-item>
<a-form-item>
<tooltip-label slot="label" :title="$t('label.domainid')" :tooltip="apiParams.domainid.description"/>
<a-select
v-decorator="['domainid', {}]"
showSearch
optionFilterProp="children"
:filterOption="(input, option) => {
return option.componentOptions.children[0].text.toLowerCase().indexOf(input.toLowerCase()) >= 0
}"
:options="domainSelectOptions"
:loading="optionsLoading.domains"
:placeholder="apiParams.domainid.description"
@change="val => { this.selectedDomainId = val }" />
</a-form-item>
<a-form-item v-if="selectedDomainId">
<tooltip-label slot="label" :title="$t('label.account')" :tooltip="apiParams.account.description"/>
<a-input
v-decorator="['account', {}]"
:placeholder="apiParams.account.description"/>
</a-form-item>
<a-form-item>
<tooltip-label slot="label" :title="$t('label.project')" :tooltip="apiParams.projectid.description"/>
<a-select
v-decorator="['projectid', {}]"
showSearch
optionFilterProp="children"
:filterOption="(input, option) => {
return option.componentOptions.children[0].text.toLowerCase().indexOf(input.toLowerCase()) >= 0
}"
:options="projectSelectOptions"
:loading="optionsLoading.projects"
:placeholder="apiParams.projectid.description" />
</a-form-item>
<a-form-item>
<tooltip-label slot="label" :title="$t('label.templatename')" :tooltip="apiParams.templateid.description + '. ' + $t('message.template.import.vm.temporary')"/>
<a-radio-group
style="width:100%"
:value="templateType"
@change="changeTemplateType">
<a-row :gutter="12">
<a-col :md="24" :lg="12">
<a-radio value="auto">
{{ $t('label.template.temporary.import') }}
</a-radio>
</a-col>
<a-col :md="24" :lg="12">
<a-radio value="custom">
{{ $t('label.template.select.existing') }}
</a-radio>
<a-select
:disabled="templateType === 'auto'"
style="margin-top:10px"
v-decorator="['templateid', {
rules: [{ required: templateType !== 'auto', message: $t('message.error.input.value') }]
}]"
showSearch
optionFilterProp="children"
:filterOption="(input, option) => {
return option.componentOptions.children[0].text.toLowerCase().indexOf(input.toLowerCase()) >= 0
}"
:options="templateSelectOptions"
:loading="optionsLoading.templates"
:placeholder="apiParams.templateid.description" />
</a-col>
</a-row>
</a-radio-group>
</a-form-item>
<a-form-item>
<tooltip-label slot="label" :title="$t('label.serviceofferingid')" :tooltip="apiParams.serviceofferingid.description"/>
</a-form-item>
<compute-offering-selection
:compute-items="computeOfferings"
:loading="computeOfferingLoading"
:rowCount="totalComputeOfferings"
:value="computeOffering ? computeOffering.id : ''"
:minimumCpunumber="isVmRunning ? resource.cpunumber : null"
:minimumCpuspeed="isVmRunning ? resource.cpuspeed : null"
:minimumMemory="isVmRunning ? resource.memory : null"
size="small"
@select-compute-item="($event) => updateComputeOffering($event)"
@handle-search-filter="($event) => fetchComputeOfferings($event)" />
<compute-selection
class="row-element"
v-if="computeOffering && computeOffering.iscustomized"
:cpuNumberInputDecorator="cpuNumberKey"
:cpuSpeedInputDecorator="cpuSpeedKey"
:memoryInputDecorator="memoryKey"
:computeOfferingId="computeOffering.id"
:preFillContent="this.resource"
:isConstrained="'serviceofferingdetails' in computeOffering"
:minCpu="getMinCpu()"
:maxCpu="getMaxCpu()"
:minMemory="getMinMemory()"
:maxMemory="getMaxMemory()"
@update-compute-cpunumber="updateFieldValue"
@update-compute-cpuspeed="updateFieldValue"
@update-compute-memory="updateFieldValue" />
<div v-if="resource.disk && resource.disk.length > 1">
<a-form-item>
<tooltip-label slot="label" :title="$t('label.disk.selection')" :tooltip="apiParams.datadiskofferinglist.description"/>
</a-form-item>
<a-form-item :title="$t('label.rootdisk')">
<a-select
v-decorator="['rootdiskid', {
rules: [{ required: true, message: $t('message.error.input.value'), }],
initialValue: 0
}]"
defaultActiveFirstOption
showSearch
optionFilterProp="children"
:filterOption="(input, option) => {
return option.componentOptions.children[0].text.toLowerCase().indexOf(input.toLowerCase()) >= 0
}"
@change="val => { selectedRootDiskIndex = val }">
<a-select-option v-for="(opt, optIndex) in resource.disk" :key="optIndex">
{{ opt.label || opt.id }}
</a-select-option>
</a-select>
</a-form-item>
<multi-disk-selection
:items="dataDisks"
:zoneId="cluster.zoneid"
:selectionEnabled="false"
:customOfferingsAllowed="true"
:autoSelectCustomOffering="true"
:autoSelectLabel="$t('label.auto.assign.diskoffering.disk.size')"
@select-multi-disk-offering="updateMultiDiskOffering" />
</div>
<div v-if="resource.nic && resource.nic.length > 0">
<a-form-item>
<tooltip-label slot="label" :title="$t('label.network.selection')" :tooltip="apiParams.nicnetworklist.description"/>
<span>{{ $t('message.ip.address.changes.effect.after.vm.restart') }}</span>
</a-form-item>
<multi-network-selection
:items="nics"
:zoneId="cluster.zoneid"
:selectionEnabled="false"
:filterUnimplementedNetworks="true"
filterMatchKey="broadcasturi"
@select-multi-network="updateMultiNetworkOffering" />
</div>
<a-row :gutter="12">
<a-col :md="24" :lg="12">
<a-form-item>
<tooltip-label slot="label" :title="$t('label.migrate.allowed')" :tooltip="apiParams.migrateallowed.description"/>
<a-switch v-decorator="['migrateallowed', {initialValue: this.switches.migrateAllowed}]" :checked="this.switches.migrateAllowed" @change="val => { this.switches.migrateAllowed = val }" />
</a-form-item>
</a-col>
<a-col :md="24" :lg="12">
<a-form-item>
<tooltip-label slot="label" :title="$t('label.forced')" :tooltip="apiParams.forced.description"/>
<a-switch v-decorator="['forced', {initialValue: this.switches.forced}]" :checked="this.switches.forced" @change="val => { this.switches.forced = val }" />
</a-form-item>
</a-col>
</a-row>
<div :span="24" class="action-button">
<a-button @click="closeAction">{{ this.$t('label.cancel') }}</a-button>
<a-button :loading="loading" type="primary" @click="handleSubmit">{{ this.$t('label.ok') }}</a-button>
</div>
</a-form>
</a-card>
</a-col>
</a-row>
</a-spin>
</div>
</template>
<script>
import { api } from '@/api'
import _ from 'lodash'
import InfoCard from '@/components/view/InfoCard'
import TooltipLabel from '@/components/widgets/TooltipLabel'
import ComputeOfferingSelection from '@views/compute/wizard/ComputeOfferingSelection'
import ComputeSelection from '@views/compute/wizard/ComputeSelection'
import MultiDiskSelection from '@views/compute/wizard/MultiDiskSelection'
import MultiNetworkSelection from '@views/compute/wizard/MultiNetworkSelection'
export default {
name: 'ImportUnmanagedInstances',
components: {
InfoCard,
TooltipLabel,
ComputeOfferingSelection,
ComputeSelection,
MultiDiskSelection,
MultiNetworkSelection
},
props: {
cluster: {
type: Object,
required: true
},
resource: {
type: Object,
required: true
},
isOpen: {
type: Boolean,
required: false
}
},
data () {
return {
options: {
domains: [],
projects: [],
templates: []
},
rowCount: {},
optionsLoading: {
domains: false,
projects: false,
templates: false
},
domains: [],
domainLoading: false,
selectedDomainId: null,
templates: [],
templateLoading: false,
templateType: 'auto',
totalComputeOfferings: 0,
computeOfferings: [],
computeOfferingLoading: false,
computeOffering: {},
selectedRootDiskIndex: 0,
dataDisksOfferingsMapping: {},
nicsNetworksMapping: {},
cpuNumberKey: 'cpuNumber',
cpuSpeedKey: 'cpuSpeed',
memoryKey: 'memory',
switches: {},
loading: false
}
},
beforeCreate () {
this.form = this.$form.createForm(this)
this.apiConfig = this.$store.getters.apis.importUnmanagedInstance || {}
this.apiParams = {}
this.apiConfig.params.forEach(param => {
this.apiParams[param.name] = param
})
},
created () {
this.fetchData()
this.form.getFieldDecorator('computeofferingid', { initialValue: undefined, preserve: true })
this.form.getFieldDecorator(this.cpuNumberKey, { initialValue: undefined, preserve: true })
this.form.getFieldDecorator(this.cpuSpeedKey, { initialValue: undefined, preserve: true })
this.form.getFieldDecorator(this.memoryKey, { initialValue: undefined, preserve: true })
},
computed: {
params () {
return {
domains: {
list: 'listDomains',
isLoad: true,
field: 'domainid',
options: {
details: 'min'
}
},
pods: {
list: 'listProjects',
isLoad: true,
field: 'projectid',
options: {
details: 'min'
}
},
templates: {
list: 'listTemplates',
isLoad: true,
options: {
templatefilter: 'all',
hypervisor: this.cluster.hypervisortype
},
field: 'templateid'
}
}
},
isVmRunning () {
if (this.resource && this.resource.powerstate === 'PowerOn') {
return true
}
return false
},
domainSelectOptions () {
var domains = this.options.domains.map((domain) => {
return {
label: domain.path || domain.name,
value: domain.id
}
})
domains.unshift({
label: '',
value: null
})
return domains
},
projectSelectOptions () {
var projects = this.options.projects.map((project) => {
return {
label: project.name,
value: project.id
}
})
projects.unshift({
label: '',
value: null
})
return projects
},
templateSelectOptions () {
return this.options.templates.map((template) => {
return {
label: template.name,
value: template.id
}
})
},
dataDisks () {
var disks = []
if (this.resource.disk && this.resource.disk.length > 1) {
for (var index = 0; index < this.resource.disk.length; ++index) {
if (index !== this.selectedRootDiskIndex) {
var disk = { ...this.resource.disk[index] }
disk.size = disk.capacity / (1024 * 1024 * 1024)
disk.name = disk.label
disk.meta = this.getMeta(disk, { controller: 'controller', datastorename: 'datastore', position: 'position' })
disks.push(disk)
}
}
}
return disks
},
nics () {
var nics = []
if (this.resource.nic && this.resource.nic.length > 0) {
for (var nicEntry of this.resource.nic) {
var nic = { ...nicEntry }
nic.name = nic.name || nic.id
nic.displaytext = nic.name
if (nic.vlanid) {
nic.broadcasturi = 'vlan://' + nic.vlanid
if (nic.isolatedpvlan) {
nic.broadcasturi = 'pvlan://' + nic.vlanid + '-i' + nic.isolatedpvlan
}
}
nic.meta = this.getMeta(nic, { macaddress: 'mac', vlanid: 'vlan', networkname: 'network' })
nics.push(nic)
}
}
return nics
}
},
watch: {
isOpen (newValue) {
if (newValue) {
this.resetForm()
this.$refs.displayname.focus()
this.selectMatchingComputeOffering()
}
}
},
methods: {
fetchData () {
_.each(this.params, (param, name) => {
if (param.isLoad) {
this.fetchOptions(param, name)
}
})
this.fetchComputeOfferings({
keyword: '',
pageSize: 10,
page: 1
})
},
getMeta (obj, metaKeys) {
var meta = []
for (var key in metaKeys) {
if (key in obj) {
meta.push({ key: metaKeys[key], value: obj[key] })
}
}
return meta
},
getMinCpu () {
if (this.isVmRunning) {
return this.resource.cpunumber
}
return 'serviceofferingdetails' in this.computeOffering ? this.computeOffering.serviceofferingdetails.mincpunumber * 1 : 1
},
getMinMemory () {
if (this.isVmRunning) {
return this.resource.memory
}
return 'serviceofferingdetails' in this.computeOffering ? this.computeOffering.serviceofferingdetails.minmemory * 1 : 32
},
getMaxCpu () {
if (this.isVmRunning) {
return this.resource.cpunumber
}
return 'serviceofferingdetails' in this.computeOffering ? this.computeOffering.serviceofferingdetails.maxcpunumber * 1 : Number.MAX_SAFE_INTEGER
},
getMaxMemory () {
if (this.isVmRunning) {
return this.resource.memory
}
return 'serviceofferingdetails' in this.computeOffering ? this.computeOffering.serviceofferingdetails.maxmemory * 1 : Number.MAX_SAFE_INTEGER
},
fetchOptions (param, name, exclude) {
if (exclude && exclude.length > 0) {
if (exclude.includes(name)) {
return
}
}
this.optionsLoading[name] = true
param.loading = true
param.opts = []
const options = param.options || {}
if (!('listall' in options)) {
options.listall = true
}
api(param.list, options).then((response) => {
param.loading = false
_.map(response, (responseItem, responseKey) => {
if (Object.keys(responseItem).length === 0) {
this.rowCount[name] = 0
this.options[name] = []
this.$forceUpdate()
return
}
if (!responseKey.includes('response')) {
return
}
_.map(responseItem, (response, key) => {
if (key === 'count') {
this.rowCount[name] = response
return
}
param.opts = response
this.options[name] = response
this.$forceUpdate()
})
})
}).catch(function (error) {
console.log(error.stack)
param.loading = false
}).finally(() => {
this.optionsLoading[name] = false
})
},
fetchComputeOfferings (options) {
this.computeOfferingLoading = true
this.totalComputeOfferings = 0
this.computeOfferings = []
this.offeringsMap = []
api('listServiceOfferings', {
keyword: options.keyword,
page: options.page,
pageSize: options.pageSize,
details: 'min',
response: 'json'
}).then(response => {
this.totalComputeOfferings = response.listserviceofferingsresponse.count
if (this.totalComputeOfferings === 0) {
return
}
this.computeOfferings = response.listserviceofferingsresponse.serviceoffering
this.computeOfferings.map(i => { this.offeringsMap[i.id] = i })
}).finally(() => {
this.computeOfferingLoading = false
this.selectMatchingComputeOffering()
})
},
updateFieldValue (name, value) {
if (name === this.cpuNumberKey) {
}
this.form.setFieldsValue({
[name]: value
})
},
updateComputeOffering (id) {
this.updateFieldValue('computeofferingid', id)
this.computeOffering = this.computeOfferings.filter(x => x.id === id)[0]
},
updateMultiDiskOffering (data) {
this.dataDisksOfferingsMapping = data
},
updateMultiNetworkOffering (data) {
this.nicsNetworksMapping = data
},
changeTemplateType (e) {
this.templateType = e.target.value
if (this.templateType === 'auto') {
this.updateFieldValue('templateid', undefined)
}
},
selectMatchingComputeOffering () {
var offerings = [...this.computeOfferings]
offerings.sort(function (a, b) {
return a.cpunumber - b.cpunumber
})
for (var offering of offerings) {
var cpuNumberMatches = false
var cpuSpeedMatches = false
var memoryMatches = false
if (!offering.iscustomized) {
cpuNumberMatches = offering.cpunumber === this.resource.cpunumber
cpuSpeedMatches = !this.resource.cpuspeed || offering.cpuspeed === this.resource.cpuspeed
memoryMatches = offering.memory === this.resource.memory
} else {
cpuNumberMatches = cpuSpeedMatches = memoryMatches = true
if (offering.serviceofferingdetails) {
cpuNumberMatches = (this.resource.cpunumber >= offering.serviceofferingdetails.mincpunumber &&
this.resource.cpunumber <= offering.serviceofferingdetails.maxcpunumber)
memoryMatches = (this.resource.memory >= offering.serviceofferingdetails.minmemory &&
this.resource.memory <= offering.serviceofferingdetails.maxmemory)
cpuSpeedMatches = !this.resource.cpuspeed || offering.cpuspeed === this.resource.cpuspeed
}
}
if (cpuNumberMatches && cpuSpeedMatches && memoryMatches) {
setTimeout(() => {
this.updateComputeOffering(offering.id)
}, 250)
break
}
}
},
handleSubmit (e) {
e.preventDefault()
this.form.validateFields((err, values) => {
if (err) {
return
}
const params = {
name: this.resource.name,
clusterid: this.cluster.id,
displayname: values.displayname
}
if (!this.computeOffering || !this.computeOffering.id) {
this.$notification.error({
message: this.$t('message.request.failed'),
description: this.$t('message.step.2.continue')
})
return
}
params.serviceofferingid = values.computeofferingid
if (this.computeOffering.iscustomized) {
var details = [this.cpuNumberKey, this.cpuSpeedKey, this.memoryKey]
for (var detail of details) {
if (!(values[detail] || this.computeOffering[detail])) {
this.$notification.error({
message: this.$t('message.request.failed'),
description: this.$t('message.please.enter.valid.value') + ': ' + this.$t('label.' + detail.toLowerCase())
})
return
}
if (values[detail]) {
params['details[0].' + detail] = values[detail]
}
}
}
var keys = ['hostname', 'domainid', 'projectid', 'account', 'migrateallowed', 'forced']
if (this.templateType !== 'auto') {
keys.push('templateid')
}
for (var key of keys) {
if (values[key]) {
params[key] = values[key]
}
}
var diskOfferingIndex = 0
for (var diskId in this.dataDisksOfferingsMapping) {
if (!this.dataDisksOfferingsMapping[diskId]) {
this.$notification.error({
message: this.$t('message.request.failed'),
description: this.$t('message.select.disk.offering') + ': ' + diskId
})
return
}
params['datadiskofferinglist[' + diskOfferingIndex + '].disk'] = diskId
params['datadiskofferinglist[' + diskOfferingIndex + '].diskOffering'] = this.dataDisksOfferingsMapping[diskId]
diskOfferingIndex++
}
var nicNetworkIndex = 0
var nicIpIndex = 0
for (var nicId in this.nicsNetworksMapping) {
if (!this.nicsNetworksMapping[nicId].network) {
this.$notification.error({
message: this.$t('message.request.failed'),
description: this.$t('message.select.nic.network') + ': ' + nicId
})
return
}
params['nicnetworklist[' + nicNetworkIndex + '].nic'] = nicId
params['nicnetworklist[' + nicNetworkIndex + '].network'] = this.nicsNetworksMapping[nicId].network
nicNetworkIndex++
if ('ipAddress' in this.nicsNetworksMapping[nicId]) {
if (!this.nicsNetworksMapping[nicId].ipAddress) {
this.$notification.error({
message: this.$t('message.request.failed'),
description: this.$t('message.enter.valid.nic.ip') + ': ' + nicId
})
return
}
params['nicipaddresslist[' + nicIpIndex + '].nic'] = nicId
params['nicipaddresslist[' + nicIpIndex + '].ip4Address'] = this.nicsNetworksMapping[nicId].ipAddress
nicIpIndex++
}
}
this.updateLoading(true)
const name = this.resource.name
api('importUnmanagedInstance', params).then(json => {
const jobId = json.importunmanagedinstanceresponse.jobid
this.$store.dispatch('AddAsyncJob', {
title: this.$t('label.import.instance'),
jobid: jobId,
description: name,
status: 'progress'
})
this.$pollJob({
jobId,
loadingMessage: `${this.$t('label.import.instance')} ${name} ${this.$t('label.in.progress')}`,
catchMessage: this.$t('error.fetching.async.job.result'),
successMessage: this.$t('message.success.import.instance') + ' ' + name,
successMethod: result => {
this.$emit('refresh-data')
}
})
this.closeAction()
}).catch(error => {
this.$notifyError(error)
}).finally(() => {
this.updateLoading(false)
})
})
},
updateLoading (value) {
this.loading = value
this.$emit('loading-changed', value)
},
resetForm () {
var fields = ['displayname', 'hostname', 'domainid', 'account', 'projectid', 'computeofferingid']
for (var field of fields) {
this.updateFieldValue(field, undefined)
}
this.templateType = 'auto'
this.updateComputeOffering(undefined)
this.switches = {}
},
closeAction () {
this.$emit('close-action')
}
}
}
</script>
<style lang="less">
@import url('../../style/index');
.ant-table-selection-column {
// Fix for the table header if the row selection use radio buttons instead of checkboxes
> div:empty {
width: 16px;
}
}
.ant-collapse-borderless > .ant-collapse-item {
border: 1px solid @border-color-split;
border-radius: @border-radius-base !important;
margin: 0 0 1.2rem;
}
.form-layout {
width: 120vw;
@media (min-width: 1000px) {
width: 550px;
}
}
.action-button {
text-align: right;
button {
margin-right: 5px;
}
}
</style>

View File

@ -0,0 +1,822 @@
// 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.
<template>
<a-row :gutter="12" v-if="isPageAllowed">
<a-col :md="24">
<a-card class="breadcrumb-card">
<a-col :md="24" style="display: flex">
<breadcrumb style="padding-top: 6px; padding-left: 8px" />
<a-button
style="margin-left: 12px; margin-top: 4px"
:loading="viewLoading"
icon="reload"
size="small"
shape="round"
@click="fetchData()" >
{{ $t('label.refresh') }}
</a-button>
</a-col>
</a-card>
</a-col>
<a-col
:md="24">
<div>
<a-card>
<a-card class="row-element"><span v-html="$t('message.desc.importexportinstancewizard')" /></a-card>
<a-row :gutter="18">
<a-form
style="min-width: 170px"
:form="form"
layout="vertical">
<a-col :md="24" :lg="8">
<a-form-item :label="this.$t('label.zoneid')">
<a-select
v-decorator="['zoneid', {}]"
showSearch
optionFilterProp="children"
:filterOption="filterOption"
:options="zoneSelectOptions"
@change="onSelectZoneId"
:loading="optionLoading.zones"
autoFocus
></a-select>
</a-form-item>
</a-col>
<a-col :md="24" :lg="8">
<a-form-item
:label="this.$t('label.podid')">
<a-select
v-decorator="['podid']"
showSearch
optionFilterProp="children"
:filterOption="filterOption"
:options="podSelectOptions"
:loading="optionLoading.pods"
@change="onSelectPodId"
></a-select>
</a-form-item>
</a-col>
<a-col :md="24" :lg="8">
<a-form-item
:label="this.$t('label.clusterid')">
<a-select
v-decorator="['clusterid']"
showSearch
optionFilterProp="children"
:filterOption="filterOption"
:options="clusterSelectOptions"
:loading="optionLoading.clusters"
@change="onSelectClusterId"
></a-select>
</a-form-item>
</a-col>
</a-form>
</a-row>
<a-divider />
<a-row :gutter="12" style="display: flex">
<a-col :md="24" :lg="12">
<a-card class="instances-card">
<span slot="title">
{{ $t('label.unmanaged.instances') }}
<a-tooltip :title="$t('message.instances.unmanaged')">
<a-icon type="info-circle" style="color: rgba(0,0,0,.45)" />
</a-tooltip>
<a-button
style="margin-left: 12px; margin-top: 4px"
:loading="unmanagedInstancesLoading"
icon="reload"
size="small"
shape="round"
@click="fetchUnmanagedInstances()" >
</a-button>
<span style="margin-left: 12px">
<search-view
:searchFilters="searchFilters.unmanaged"
:searchParams="searchParams.unmanaged"
:apiName="listInstancesApi.unmanaged"
@search="searchUnmanagedInstances"
/>
</span>
</span>
<a-table
class="instances-card-table"
:loading="unmanagedInstancesLoading"
:rowSelection="unmanagedInstanceSelection"
:rowKey="(record, index) => index"
:columns="unmanagedInstancesColumns"
:data-source="unmanagedInstances"
:pagination="false"
size="middle"
:rowClassName="getRowClassName"
>
<template slot="state" slot-scope="text">
<status :text="text ? text : ''" displayText />
</template>
</a-table>
<div class="instances-card-footer">
<a-pagination
class="row-element"
size="small"
:current="page.unmanaged"
:pageSize="pageSize.unmanaged"
:total="itemCount.unmanaged"
:showTotal="total => `${$t('label.showing')} ${Math.min(total, 1+((page.unmanaged-1)*pageSize.unmanaged))}-${Math.min(page.unmanaged*pageSize.unmanaged, total)} ${$t('label.of')} ${total} ${$t('label.items')}`"
@change="fetchUnmanagedInstances"
showQuickJumper>
<template slot="buildOptionText" slot-scope="props">
<span>{{ props.value }} / {{ $t('label.page') }}</span>
</template>
</a-pagination>
<div :span="24" class="action-button-left">
<a-button
:loading="importUnmanagedInstanceLoading"
:disabled="!(('importUnmanagedInstance' in $store.getters.apis) && unmanagedInstancesSelectedRowKeys.length > 0)"
type="primary"
icon="import"
@click="onManageInstanceAction">
{{ $t('label.import.instance') }}
</a-button>
</div>
</div>
</a-card>
</a-col>
<a-col :md="24" :lg="12">
<a-card class="instances-card">
<span slot="title">
{{ $t('label.managed.instances') }}
<a-tooltip :title="$t('message.instances.managed')">
<a-icon type="info-circle" style="color: rgba(0,0,0,.45)" />
</a-tooltip>
<a-button
style="margin-left: 12px; margin-top: 4px"
:loading="managedInstancesLoading"
icon="reload"
size="small"
shape="round"
@click="fetchManagedInstances()" >
</a-button>
<span style="margin-left: 12px">
<search-view
:searchFilters="searchFilters.managed"
:searchParams="searchParams.managed"
:apiName="listInstancesApi.managed"
@search="searchManagedInstances"
/>
</span>
</span>
<a-table
class="instances-card-table"
:loading="managedInstancesLoading"
:rowSelection="managedInstanceSelection"
:rowKey="(record, index) => index"
:columns="managedInstancesColumns"
:data-source="managedInstances"
:pagination="false"
size="middle"
:rowClassName="getRowClassName"
>
<a slot="name" slot-scope="text, record" href="javascript:;">
<router-link :to="{ path: '/vm/' + record.id }">{{ text }}</router-link>
</a>
<template slot="state" slot-scope="text">
<status :text="text ? text : ''" displayText />
</template>
</a-table>
<div class="instances-card-footer">
<a-pagination
class="row-element"
size="small"
:current="page.managed"
:pageSize="pageSize.managed"
:total="itemCount.managed"
:showTotal="total => `${$t('label.showing')} ${Math.min(total, 1+((page.managed-1)*pageSize.managed))}-${Math.min(page.managed*pageSize.managed, total)} ${$t('label.of')} ${total} ${$t('label.items')}`"
@change="fetchManagedInstances"
showQuickJumper>
<template slot="buildOptionText" slot-scope="props">
<span>{{ props.value }} / {{ $t('label.page') }}</span>
</template>
</a-pagination>
<div :span="24" class="action-button-right">
<a-button
:disabled="!(('unmanageVirtualMachine' in $store.getters.apis) && managedInstancesSelectedRowKeys.length > 0)"
type="primary"
icon="disconnect"
@click="onUnmanageInstanceAction">
{{ managedInstancesSelectedRowKeys.length > 1 ? $t('label.action.unmanage.instances') : $t('label.action.unmanage.instance') }}
</a-button>
</div>
</div>
</a-card>
</a-col>
</a-row>
</a-card>
<a-modal
:visible="showUnmanageForm"
:title="$t('label.import.instance')"
:closable="true"
:maskClosable="false"
:footer="null"
:cancelText="$t('label.cancel')"
@cancel="showUnmanageForm = false"
centered
ref="importModal"
width="auto">
<import-unmanaged-instances
class="importform"
:resource="selectedUnmanagedInstance"
:cluster="selectedCluster"
:isOpen="showUnmanageForm"
@refresh-data="fetchInstances"
@close-action="closeImportUnmanagedInstanceForm"
@loading-changed="updateManageInstanceActionLoading"
/>
</a-modal>
</div>
</a-col>
</a-row>
</template>
<script>
import { api } from '@/api'
import _ from 'lodash'
import Breadcrumb from '@/components/widgets/Breadcrumb'
import Status from '@/components/widgets/Status'
import SearchView from '@/components/view/SearchView'
import ImportUnmanagedInstances from '@/views/tools/ImportUnmanagedInstance'
export default {
components: {
Breadcrumb,
Status,
SearchView,
ImportUnmanagedInstances
},
name: 'ManageVms',
data () {
const unmanagedInstancesColumns = [
{
title: this.$t('label.name'),
dataIndex: 'name',
width: 100
},
{
title: this.$t('label.state'),
dataIndex: 'powerstate',
scopedSlots: { customRender: 'state' }
},
{
title: this.$t('label.hostname'),
dataIndex: 'hostname'
},
{
title: this.$t('label.ostypename'),
dataIndex: 'osdisplayname'
}
]
const managedInstancesColumns = [
{
title: this.$t('label.name'),
dataIndex: 'name',
width: 100,
scopedSlots: { customRender: 'name' }
},
{
title: this.$t('label.instancename'),
dataIndex: 'instancename'
},
{
title: this.$t('label.state'),
dataIndex: 'state',
scopedSlots: { customRender: 'state' }
},
{
title: this.$t('label.hostname'),
dataIndex: 'hostname'
},
{
title: this.$t('label.templatename'),
dataIndex: 'templatedisplaytext'
}
]
return {
options: {
zones: [],
pods: [],
clusters: []
},
rowCount: {},
optionLoading: {
zones: false,
pods: false,
clusters: false
},
page: {
unmanaged: 1,
managed: 1
},
pageSize: {
unmanaged: 10,
managed: 10
},
searchFilters: {
unmanaged: [],
managed: []
},
searchParams: {
unmanaged: {},
managed: {}
},
itemCount: {},
zone: {},
zoneId: undefined,
podId: undefined,
clusterId: undefined,
listInstancesApi: {
unmanaged: 'listUnmanagedInstances',
managed: 'listVirtualMachines'
},
unmanagedInstancesColumns,
unmanagedInstancesLoading: false,
unmanagedInstances: [],
unmanagedInstancesSelectedRowKeys: [],
importUnmanagedInstanceLoading: false,
managedInstancesColumns,
managedInstancesLoading: false,
managedInstances: [],
managedInstancesSelectedRowKeys: [],
showUnmanageForm: false,
selectedUnmanagedInstance: {},
query: {}
}
},
beforeCreate () {
this.form = this.$form.createForm(this)
},
created () {
this.page.unmanaged = parseInt(this.$route.query.unmanagedpage || 1)
this.page.managed = parseInt(this.$route.query.managedpage || 1)
this.fetchData()
},
computed: {
isPageAllowed () {
if (this.$route.meta.permission) {
for (var apiName of this.$route.meta.permission) {
if (!(apiName in this.$store.getters.apis)) {
return false
}
}
}
return true
},
params () {
return {
zones: {
list: 'listZones',
isLoad: true,
field: 'zoneid'
},
pods: {
list: 'listPods',
isLoad: false,
options: {
zoneid: _.get(this.zone, 'id')
},
field: 'podid'
},
clusters: {
list: 'listClusters',
isLoad: false,
options: {
zoneid: _.get(this.zone, 'id'),
podid: this.podId
},
field: 'clusterid'
}
}
},
viewLoading () {
for (var key in this.optionLoading) {
if (this.optionLoading[key]) {
return true
}
}
return this.unmanagedInstancesLoading || this.managedInstancesLoading
},
zoneSelectOptions () {
return this.options.zones.map((zone) => {
return {
label: zone.name,
value: zone.id
}
})
},
podSelectOptions () {
const options = this.options.pods.map((pod) => {
return {
label: pod.name,
value: pod.id
}
})
return options
},
clusterSelectOptions () {
const options = this.options.clusters.map((cluster) => {
return {
label: cluster.name,
value: cluster.id
}
})
return options
},
unmanagedInstanceSelection () {
return {
type: 'radio',
selectedRowKeys: this.unmanagedInstancesSelectedRowKeys || [],
onChange: this.onUnmanagedInstanceSelectRow
}
},
managedInstanceSelection () {
return {
type: 'checkbox',
selectedRowKeys: this.managedInstancesSelectedRowKeys || [],
onChange: this.onManagedInstanceSelectRow
}
},
selectedCluster () {
if (this.options.clusters &&
this.options.clusters.length > 0 &&
this.clusterId) {
return _.find(this.options.clusters, (option) => option.id === this.clusterId)
}
return {}
}
},
methods: {
fetchData () {
this.unmanagedInstances = []
this.managedInstances = []
_.each(this.params, (param, name) => {
if (param.isLoad) {
this.fetchOptions(param, name)
}
})
},
filterOption (input, option) {
return (
option.componentOptions.children[0].text.toUpperCase().indexOf(input.toUpperCase()) >= 0
)
},
fetchOptions (param, name, exclude) {
if (exclude && exclude.length > 0) {
if (exclude.includes(name)) {
return
}
}
this.optionLoading[name] = true
param.loading = true
param.opts = []
const options = param.options || {}
if (!('listall' in options)) {
options.listall = true
}
api(param.list, options).then((response) => {
param.loading = false
_.map(response, (responseItem, responseKey) => {
if (Object.keys(responseItem).length === 0) {
this.rowCount[name] = 0
this.options[name] = []
this.$forceUpdate()
return
}
if (!responseKey.includes('response')) {
return
}
_.map(responseItem, (response, key) => {
if (key === 'count') {
this.rowCount[name] = response
return
}
param.opts = response
this.options[name] = response
this.$forceUpdate()
})
this.handleFetchOptionsSuccess(name, param)
})
}).catch(function (error) {
console.log(error.stack)
param.loading = false
}).finally(() => {
this.optionLoading[name] = false
})
},
getRowClassName (record, index) {
if (index % 2 === 0) {
return 'light-row'
}
return 'dark-row'
},
handleFetchOptionsSuccess (name, param) {
if (['zones', 'pods', 'clusters'].includes(name)) {
let paramid = ''
const query = Object.assign({}, this.$route.query)
if (query[param.field] && _.find(this.options[name], (option) => option.id === query[param.field])) {
paramid = query[param.field]
}
if (!paramid && this.options[name].length === 1) {
paramid = (this.options[name])[0].id
}
if (paramid) {
this.form.getFieldDecorator([param.field], { initialValue: paramid })
if (name === 'zones') {
this.onSelectZoneId(paramid)
} else if (name === 'pods') {
this.form.setFieldsValue({
podid: paramid
})
this.onSelectPodId(paramid)
} else if (name === 'clusters') {
this.form.setFieldsValue({
clusterid: paramid
})
this.onSelectClusterId(paramid)
}
}
}
},
updateQuery (field, value) {
const query = Object.assign({}, this.$route.query)
if (query[field] === value + '') {
return
}
query[field] = value
if (['zoneid', 'podid', 'clusterid'].includes(field)) {
query.managedpage = 1
query.unmanagedpage = 1
}
this.$router.push({ query })
},
resetLists () {
this.page.unmanaged = 1
this.unmanagedInstances = []
this.unmanagedInstancesSelectedRowKeys = []
this.page.managed = 1
this.managedInstances = []
this.managedInstancesSelectedRowKeys = []
},
onSelectZoneId (value) {
this.zoneId = value
this.podId = null
this.clusterId = null
this.zone = _.find(this.options.zones, (option) => option.id === value)
this.resetLists()
this.form.setFieldsValue({
clusterid: undefined,
podid: undefined
})
this.updateQuery('zoneid', value)
this.fetchOptions(this.params.pods, 'pods')
},
onSelectPodId (value) {
this.podId = value
this.resetLists()
this.form.setFieldsValue({
clusterid: undefined
})
this.updateQuery('podid', value)
this.fetchOptions(this.params.clusters, 'clusters', value)
},
onSelectClusterId (value) {
this.clusterId = value
this.resetLists()
this.updateQuery('clusterid', value)
this.fetchInstances()
},
fetchInstances () {
if (this.selectedCluster.hypervisortype === 'VMware') {
this.fetchUnmanagedInstances()
this.fetchManagedInstances()
}
},
fetchUnmanagedInstances (page, pageSize) {
const params = {
clusterid: this.clusterId
}
const query = Object.assign({}, this.$route.query)
this.page.unmanaged = page || parseInt(query.unmanagedpage) || this.page.unmanaged
this.updateQuery('unmanagedpage', this.page.unmanaged)
params.page = this.page.unmanaged
this.pageSize.unmanaged = pageSize || this.pageSize.unmanaged
params.pagesize = this.pageSize.unmanaged
this.unmanagedInstances = []
this.unmanagedInstancesSelectedRowKeys = []
if (this.searchParams.unmanaged.keyword) {
params.keyword = this.searchParams.unmanaged.keyword
}
if (!this.clusterId) {
return
}
this.unmanagedInstancesLoading = true
this.searchParams.unmanaged = params
api(this.listInstancesApi.unmanaged, params).then(json => {
const listUnmanagedInstances = json.listunmanagedinstancesresponse.unmanagedinstance
if (this.arrayHasItems(listUnmanagedInstances)) {
this.unmanagedInstances = this.unmanagedInstances.concat(listUnmanagedInstances)
}
this.itemCount.unmanaged = json.listunmanagedinstancesresponse.count
}).finally(() => {
this.unmanagedInstancesLoading = false
})
},
searchUnmanagedInstances (params) {
this.searchParams.unmanaged.keyword = params.searchQuery
this.fetchUnmanagedInstances()
},
fetchManagedInstances (page, pageSize) {
const params = {
listall: true,
clusterid: this.clusterId
}
const query = Object.assign({}, this.$route.query)
this.page.managed = page || parseInt(query.managedpage) || this.page.managed
this.updateQuery('managedpage', this.page.managed)
params.page = this.page.managed
this.pageSize.managed = pageSize || this.pageSize.managed
params.pagesize = this.pageSize.managed
this.managedInstances = []
this.managedInstancesSelectedRowKeys = []
if (this.searchParams.managed.keyword) {
params.keyword = this.searchParams.managed.keyword
}
if (!this.clusterId) {
return
}
this.managedInstancesLoading = true
this.searchParams.managed = params
api(this.listInstancesApi.managed, params).then(json => {
const listManagedInstances = json.listvirtualmachinesresponse.virtualmachine
if (this.arrayHasItems(listManagedInstances)) {
this.managedInstances = this.managedInstances.concat(listManagedInstances)
}
this.itemCount.managed = json.listvirtualmachinesresponse.count
}).finally(() => {
this.managedInstancesLoading = false
})
},
searchManagedInstances (params) {
this.searchParams.managed.keyword = params.searchQuery
this.fetchManagedInstances()
},
onUnmanagedInstanceSelectRow (value) {
this.unmanagedInstancesSelectedRowKeys = value
},
onManagedInstanceSelectRow (value) {
this.managedInstancesSelectedRowKeys = value
},
isValidValueForKey (obj, key) {
return key in obj && obj[key] != null
},
arrayHasItems (array) {
return array !== null && array !== undefined && Array.isArray(array) && array.length > 0
},
isObjectEmpty (obj) {
return !(obj !== null && obj !== undefined && Object.keys(obj).length > 0 && obj.constructor === Object)
},
updateManageInstanceActionLoading (value) {
this.importUnmanagedInstanceLoading = value
},
onManageInstanceAction () {
this.selectedUnmanagedInstance = {}
if (this.unmanagedInstances.length > 0 &&
this.unmanagedInstancesSelectedRowKeys.length > 0) {
this.selectedUnmanagedInstance = this.unmanagedInstances[this.unmanagedInstancesSelectedRowKeys[0]]
this.selectedUnmanagedInstance.ostypename = this.selectedUnmanagedInstance.osdisplayname
this.selectedUnmanagedInstance.state = this.selectedUnmanagedInstance.powerstate
}
this.showUnmanageForm = true
},
closeImportUnmanagedInstanceForm () {
this.selectedUnmanagedInstance = {}
this.showUnmanageForm = false
this.$refs.importModal.$forceUpdate()
},
onUnmanageInstanceAction () {
const self = this
const title = this.managedInstancesSelectedRowKeys.length > 1
? this.$t('message.action.unmanage.instances')
: this.$t('message.action.unmanage.instance')
var vmNames = []
for (var index of this.managedInstancesSelectedRowKeys) {
vmNames.push(this.managedInstances[index].name)
}
const content = vmNames.join(', ')
this.$confirm({
title: title,
okText: this.$t('label.ok'),
okType: 'danger',
content: content,
cancelText: this.$t('label.cancel'),
onOk () {
self.unmanageInstances()
}
})
},
unmanageInstances () {
for (var index of this.managedInstancesSelectedRowKeys) {
const vm = this.managedInstances[index]
var params = { id: vm.id }
api('unmanageVirtualMachine', params).then(json => {
const jobId = json.unmanagevirtualmachineresponse.jobid
this.$store.dispatch('AddAsyncJob', {
title: this.$t('label.unmanage.instance'),
jobid: jobId,
description: vm.name,
status: 'progress'
})
this.$pollJob({
jobId,
loadingMessage: `${this.$t('label.unmanage.instance')} ${vm.name} ${this.$t('label.in.progress')}`,
catchMessage: this.$t('error.fetching.async.job.result'),
successMessage: this.$t('message.success.unmanage.instance') + ' ' + vm.name,
successMethod: result => {
if (index === this.managedInstancesSelectedRowKeys[this.managedInstancesSelectedRowKeys.length - 1]) {
this.fetchInstances()
}
}
})
}).catch(error => {
this.$notifyError(error)
}).finally(() => {
this.loading = false
})
}
}
}
}
</script>
<style scoped>
/deep/ .ant-table-thead {
background-color: #f9f9f9;
}
/deep/ .ant-table-small > .ant-table-content > .ant-table-body {
margin: 0;
}
/deep/ .light-row {
background-color: #fff;
}
/deep/ .dark-row {
background-color: #f9f9f9;
}
</style>
<style scoped lang="less">
.importform {
width: 80vw;
}
.instances-card {
height: 100%;
}
.instances-card-table {
overflow-y: auto;
margin-bottom: 100px;
}
.instances-card-footer {
height: 100px;
position: absolute;
bottom: 0;
left: 0;
margin-left: 10px;
right: 0;
margin-right: 10px;
}
.row-element {
margin-top: 10px;
margin-bottom: 10px;
}
.action-button-left {
text-align: left;
}
.action-button-right {
text-align: right;
}
</style>