mirror of https://github.com/apache/cloudstack.git
Multiple CD-ROM / ISO Support Per VM (#13101)
* pre-allocate a second empty cdrom slot at boot (hardcoded) * drive cdrom slot count via vm.cdrom.max.count ConfigKey * add vm_iso_map table + VO/DAO * persist multi-ISO state via vm_iso_map * carry target cdrom slot through AttachCommand to KVM agent * enforce per-VM cdrom cap, clamp to hypervisor max * make detachIso accepts an ISO id * expose attached ISOs as isos[] in listVirtualMachines response * extract CDROM_PRIMARY_DEVICE_SEQ constant * unit tests for cdrom slot allocation logic * implement multi-ISO attachment and detachment for VMs with enhanced validation * implement multi-ISO display in InstanceTab with computed property for attached ISOs * add warning alert for max CDROM selections and enhance global capacity fetching * enhance ISO attachment validation to handle multiple ISOs and prevent duplicates * refactor ISO attachment logic for detachment and validation * add unit tests for ISO detachment resolution and validation logic * add mock for VmIsoMapDao in UserVmJoinDaoImplTest and set lenient behavior for listByVmId * refactor ISO attachment logic and enhance UI for multi-CDROM management * refactor ISO attachment methods to use VM ID and improve parameter handling * remove unnecessary mock for VM ISO mapping in TemplateManagerImplTest * add 'since' attribute to ISO detach command parameter description * scope vm.cdrom.max.count to cluster * add support for configurable CD-ROM count per VM and improve handling in TemplateManager * add HostDetailsDao mock to UserVmJoinDaoImplTest * fix: handle null poolId when loading attached ISO slots in prepareIsoForVmProfile * implement listByIsoId method in VmIsoMapDao and update TemplateManagerImpl for ISO deletion checks * improve logging messages for ISO deletion checks * add unit tests for CD-ROM handling and enforce limits in TemplateManager * refactor: update configuration value handling and improve notification logic * refactor: rename CD-ROM references to ISO and update related logic * refactor: enhance effective CD-ROM max count logic to handle missing host IDs and improve cluster ID retrieval * refactor: enhance effective CD-ROM max count logic to handle misconfigurations during VM boot * refactor: enhance effective CD-ROM max count logic to retrieve host ID from candidates based on hypervisor type * refactor: enhance host ID retrieval logic for VMs based on hypervisor type * feat: add bootable ISO flag to AttachedIsoResponse and update UI to display it * refactor: simplify effectiveMaxCdroms method and improve logging for CD-ROM capacity * test: update AttachedIsoResponseTest to include bootable flag in constructor tests * feat: include bootable flag in AttachedIsoResponse for user VMs * feat: enhance CD-ROM management by defining empty slots for user VMs
This commit is contained in:
parent
21e4475d96
commit
ea6cbada9b
|
|
@ -63,6 +63,7 @@ public interface Host extends StateObject<Status>, Identity, Partition, HAResour
|
|||
String HOST_OVFTOOL_VERSION = "host.ovftool.version";
|
||||
String HOST_VIRTV2V_VERSION = "host.virtv2v.version";
|
||||
String HOST_SSH_PORT = "host.ssh.port";
|
||||
String HOST_CDROM_MAX_COUNT = "host.cdrom.max.count";
|
||||
String GUEST_OS_CATEGORY_ID = "guest.os.category.id";
|
||||
String GUEST_OS_RULE = "guest.os.rule";
|
||||
|
||||
|
|
|
|||
|
|
@ -27,6 +27,7 @@ import org.apache.cloudstack.api.ResponseObject.ResponseView;
|
|||
import org.apache.cloudstack.api.ServerApiException;
|
||||
import org.apache.cloudstack.api.command.user.UserCmd;
|
||||
import org.apache.cloudstack.api.command.user.vm.DeployVMCmd;
|
||||
import org.apache.cloudstack.api.response.TemplateResponse;
|
||||
import org.apache.cloudstack.api.response.UserVmResponse;
|
||||
|
||||
import com.cloud.event.EventTypes;
|
||||
|
|
@ -51,6 +52,10 @@ public class DetachIsoCmd extends BaseAsyncCmd implements UserCmd {
|
|||
description = "If true, ejects the ISO before detaching on VMware. Default: false", since = "4.15.1")
|
||||
protected Boolean forced;
|
||||
|
||||
@Parameter(name = ApiConstants.ID, type = CommandType.UUID, entityType = TemplateResponse.class,
|
||||
description = "The ID of the ISO to detach. Required when the Instance has more than one ISO attached.", since = "4.23.0")
|
||||
protected Long id;
|
||||
|
||||
/////////////////////////////////////////////////////
|
||||
/////////////////// Accessors ///////////////////////
|
||||
/////////////////////////////////////////////////////
|
||||
|
|
@ -104,7 +109,7 @@ public class DetachIsoCmd extends BaseAsyncCmd implements UserCmd {
|
|||
|
||||
@Override
|
||||
public void execute() {
|
||||
boolean result = _templateService.detachIso(virtualMachineId, null, isForced());
|
||||
boolean result = _templateService.detachIso(virtualMachineId, id, isForced());
|
||||
if (result) {
|
||||
UserVm userVm = _entityMgr.findById(UserVm.class, virtualMachineId);
|
||||
UserVmResponse response = _responseGenerator.createUserVmResponse(getResponseView(), "virtualmachine", userVm).get(0);
|
||||
|
|
|
|||
|
|
@ -0,0 +1,76 @@
|
|||
// Licensed to the Apache Software Foundation (ASF) under one
|
||||
// or more contributor license agreements. See the NOTICE file
|
||||
// distributed with this work for additional information
|
||||
// regarding copyright ownership. The ASF licenses this file
|
||||
// to you under the Apache License, Version 2.0 (the
|
||||
// "License"); you may not use this file except in compliance
|
||||
// with the License. You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing,
|
||||
// software distributed under the License is distributed on an
|
||||
// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
||||
// KIND, either express or implied. See the License for the
|
||||
// specific language governing permissions and limitations
|
||||
// under the License.
|
||||
package org.apache.cloudstack.api.response;
|
||||
|
||||
import org.apache.cloudstack.api.BaseResponse;
|
||||
|
||||
import com.cloud.serializer.Param;
|
||||
import com.google.gson.annotations.SerializedName;
|
||||
|
||||
public class AttachedIsoResponse extends BaseResponse {
|
||||
|
||||
@SerializedName("id")
|
||||
@Param(description = "The ID of the attached ISO")
|
||||
private String id;
|
||||
|
||||
@SerializedName("name")
|
||||
@Param(description = "The name of the attached ISO")
|
||||
private String name;
|
||||
|
||||
@SerializedName("displaytext")
|
||||
@Param(description = "The display text of the attached ISO")
|
||||
private String displayText;
|
||||
|
||||
@SerializedName("deviceseq")
|
||||
@Param(description = "The cdrom slot that holds this ISO (3=hdc, 4=hdd, ...)")
|
||||
private Integer deviceSeq;
|
||||
|
||||
@SerializedName("bootable")
|
||||
@Param(description = "Whether this is the bootable ISO for the VM")
|
||||
private Boolean bootable;
|
||||
|
||||
public AttachedIsoResponse() {
|
||||
}
|
||||
|
||||
public AttachedIsoResponse(String id, String name, String displayText, Integer deviceSeq, boolean bootable) {
|
||||
this.id = id;
|
||||
this.name = name;
|
||||
this.displayText = displayText;
|
||||
this.deviceSeq = deviceSeq;
|
||||
this.bootable = bootable;
|
||||
}
|
||||
|
||||
public String getId() {
|
||||
return id;
|
||||
}
|
||||
|
||||
public String getName() {
|
||||
return name;
|
||||
}
|
||||
|
||||
public String getDisplayText() {
|
||||
return displayText;
|
||||
}
|
||||
|
||||
public Integer getDeviceSeq() {
|
||||
return deviceSeq;
|
||||
}
|
||||
|
||||
public Boolean getBootable() {
|
||||
return bootable;
|
||||
}
|
||||
}
|
||||
|
|
@ -166,6 +166,14 @@ public class UserVmResponse extends BaseResponseWithTagInformation implements Co
|
|||
@Param(description = "An alternate display text of the ISO attached to the Instance")
|
||||
private String isoDisplayText;
|
||||
|
||||
@SerializedName("isos")
|
||||
@Param(description = "All ISOs attached to the Instance, keyed by cdrom slot. The first entry mirrors isoid/isoname for back-compat.", responseObject = AttachedIsoResponse.class, since = "4.23.0")
|
||||
private List<AttachedIsoResponse> isos;
|
||||
|
||||
@SerializedName("isomaxcount")
|
||||
@Param(description = "Maximum number of ISOs that may be attached to this Instance, after applying the cluster-scoped vm.iso.max.count and the hypervisor's own cap.", since = "4.23.0")
|
||||
private Integer isoMaxCount;
|
||||
|
||||
@SerializedName(ApiConstants.SERVICE_OFFERING_ID)
|
||||
@Param(description = "The ID of the service offering of the Instance")
|
||||
private String serviceOfferingId;
|
||||
|
|
@ -871,6 +879,22 @@ public class UserVmResponse extends BaseResponseWithTagInformation implements Co
|
|||
this.isoId = isoId;
|
||||
}
|
||||
|
||||
public void setIsos(List<AttachedIsoResponse> isos) {
|
||||
this.isos = isos;
|
||||
}
|
||||
|
||||
public List<AttachedIsoResponse> getIsos() {
|
||||
return isos;
|
||||
}
|
||||
|
||||
public void setIsoMaxCount(Integer isoMaxCount) {
|
||||
this.isoMaxCount = isoMaxCount;
|
||||
}
|
||||
|
||||
public Integer getIsoMaxCount() {
|
||||
return isoMaxCount;
|
||||
}
|
||||
|
||||
public void setIsoName(String isoName) {
|
||||
this.isoName = isoName;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,46 @@
|
|||
// Licensed to the Apache Software Foundation (ASF) under one
|
||||
// or more contributor license agreements. See the NOTICE file
|
||||
// distributed with this work for additional information
|
||||
// regarding copyright ownership. The ASF licenses this file
|
||||
// to you under the Apache License, Version 2.0 (the
|
||||
// "License"); you may not use this file except in compliance
|
||||
// with the License. You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing,
|
||||
// software distributed under the License is distributed on an
|
||||
// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
||||
// KIND, either express or implied. See the License for the
|
||||
// specific language governing permissions and limitations
|
||||
// under the License.
|
||||
package org.apache.cloudstack.api.response;
|
||||
|
||||
import org.junit.Assert;
|
||||
import org.junit.Test;
|
||||
import org.junit.runner.RunWith;
|
||||
import org.mockito.junit.MockitoJUnitRunner;
|
||||
|
||||
@RunWith(MockitoJUnitRunner.class)
|
||||
public final class AttachedIsoResponseTest {
|
||||
|
||||
@Test
|
||||
public void testFullConstructorPopulatesAllFields() {
|
||||
AttachedIsoResponse response = new AttachedIsoResponse("uuid-1", "alpine-iso", "Alpine boot", 3, true);
|
||||
Assert.assertEquals("uuid-1", response.getId());
|
||||
Assert.assertEquals("alpine-iso", response.getName());
|
||||
Assert.assertEquals("Alpine boot", response.getDisplayText());
|
||||
Assert.assertEquals(Integer.valueOf(3), response.getDeviceSeq());
|
||||
Assert.assertTrue(response.getBootable());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testNoArgConstructorLeavesFieldsNull() {
|
||||
AttachedIsoResponse response = new AttachedIsoResponse();
|
||||
Assert.assertNull(response.getId());
|
||||
Assert.assertNull(response.getName());
|
||||
Assert.assertNull(response.getDisplayText());
|
||||
Assert.assertNull(response.getDeviceSeq());
|
||||
Assert.assertNull(response.getBootable());
|
||||
}
|
||||
}
|
||||
|
|
@ -64,6 +64,21 @@ public interface TemplateManager {
|
|||
true,
|
||||
ConfigKey.Scope.Global);
|
||||
|
||||
ConfigKey<Integer> VmIsoMaxCount = new ConfigKey<Integer>("Advanced",
|
||||
Integer.class,
|
||||
"vm.iso.max.count", "1",
|
||||
"Maximum number of ISOs that may be attached to a VM.",
|
||||
true,
|
||||
ConfigKey.Scope.Cluster);
|
||||
|
||||
// KVM/libvirt maps deviceSeq=3 to hdc (hda/hdb are taken by the root volume on i440fx/IDE).
|
||||
// user_vm.iso_id has always pointed at this slot; additional cdroms live in vm_iso_map.
|
||||
int CDROM_PRIMARY_DEVICE_SEQ = 3;
|
||||
|
||||
// Fallback per-VM cdrom cap when the placement host hasn't advertised host.cdrom.max.count
|
||||
// (older agent, never-deployed VM, etc.).
|
||||
int DEFAULT_CDROM_MAX_PER_VM = 1;
|
||||
|
||||
static final String VMWARE_TOOLS_ISO = "vmware-tools.iso";
|
||||
static final String XS_TOOLS_ISO = "xs-tools.iso";
|
||||
|
||||
|
|
|
|||
|
|
@ -0,0 +1,83 @@
|
|||
// Licensed to the Apache Software Foundation (ASF) under one
|
||||
// or more contributor license agreements. See the NOTICE file
|
||||
// distributed with this work for additional information
|
||||
// regarding copyright ownership. The ASF licenses this file
|
||||
// to you under the Apache License, Version 2.0 (the
|
||||
// "License"); you may not use this file except in compliance
|
||||
// with the License. You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing,
|
||||
// software distributed under the License is distributed on an
|
||||
// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
||||
// KIND, either express or implied. See the License for the
|
||||
// specific language governing permissions and limitations
|
||||
// under the License.
|
||||
package com.cloud.vm;
|
||||
|
||||
import java.util.Date;
|
||||
|
||||
import javax.persistence.Column;
|
||||
import javax.persistence.Entity;
|
||||
import javax.persistence.GeneratedValue;
|
||||
import javax.persistence.GenerationType;
|
||||
import javax.persistence.Id;
|
||||
import javax.persistence.Table;
|
||||
import javax.persistence.Temporal;
|
||||
import javax.persistence.TemporalType;
|
||||
|
||||
import org.apache.cloudstack.api.InternalIdentity;
|
||||
|
||||
@Entity
|
||||
@Table(name = "vm_iso_map")
|
||||
public class VmIsoMapVO implements InternalIdentity {
|
||||
@Id
|
||||
@GeneratedValue(strategy = GenerationType.IDENTITY)
|
||||
@Column(name = "id")
|
||||
private Long id;
|
||||
|
||||
@Column(name = "vm_id")
|
||||
private long vmId;
|
||||
|
||||
@Column(name = "iso_id")
|
||||
private long isoId;
|
||||
|
||||
@Column(name = "device_seq")
|
||||
private int deviceSeq;
|
||||
|
||||
@Column(name = "created")
|
||||
@Temporal(TemporalType.TIMESTAMP)
|
||||
private Date created;
|
||||
|
||||
public VmIsoMapVO() {
|
||||
}
|
||||
|
||||
public VmIsoMapVO(long vmId, long isoId, int deviceSeq) {
|
||||
this.vmId = vmId;
|
||||
this.isoId = isoId;
|
||||
this.deviceSeq = deviceSeq;
|
||||
this.created = new Date();
|
||||
}
|
||||
|
||||
@Override
|
||||
public long getId() {
|
||||
return id;
|
||||
}
|
||||
|
||||
public long getVmId() {
|
||||
return vmId;
|
||||
}
|
||||
|
||||
public long getIsoId() {
|
||||
return isoId;
|
||||
}
|
||||
|
||||
public int getDeviceSeq() {
|
||||
return deviceSeq;
|
||||
}
|
||||
|
||||
public Date getCreated() {
|
||||
return created;
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,34 @@
|
|||
// Licensed to the Apache Software Foundation (ASF) under one
|
||||
// or more contributor license agreements. See the NOTICE file
|
||||
// distributed with this work for additional information
|
||||
// regarding copyright ownership. The ASF licenses this file
|
||||
// to you under the Apache License, Version 2.0 (the
|
||||
// "License"); you may not use this file except in compliance
|
||||
// with the License. You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing,
|
||||
// software distributed under the License is distributed on an
|
||||
// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
||||
// KIND, either express or implied. See the License for the
|
||||
// specific language governing permissions and limitations
|
||||
// under the License.
|
||||
package com.cloud.vm.dao;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
import com.cloud.utils.db.GenericDao;
|
||||
import com.cloud.vm.VmIsoMapVO;
|
||||
|
||||
public interface VmIsoMapDao extends GenericDao<VmIsoMapVO, Long> {
|
||||
List<VmIsoMapVO> listByVmId(long vmId);
|
||||
|
||||
List<VmIsoMapVO> listByIsoId(long isoId);
|
||||
|
||||
VmIsoMapVO findByVmIdDeviceSeq(long vmId, int deviceSeq);
|
||||
|
||||
VmIsoMapVO findByVmIdIsoId(long vmId, long isoId);
|
||||
|
||||
int removeByVmId(long vmId);
|
||||
}
|
||||
|
|
@ -0,0 +1,92 @@
|
|||
// Licensed to the Apache Software Foundation (ASF) under one
|
||||
// or more contributor license agreements. See the NOTICE file
|
||||
// distributed with this work for additional information
|
||||
// regarding copyright ownership. The ASF licenses this file
|
||||
// to you under the Apache License, Version 2.0 (the
|
||||
// "License"); you may not use this file except in compliance
|
||||
// with the License. You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing,
|
||||
// software distributed under the License is distributed on an
|
||||
// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
||||
// KIND, either express or implied. See the License for the
|
||||
// specific language governing permissions and limitations
|
||||
// under the License.
|
||||
package com.cloud.vm.dao;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
import com.cloud.utils.db.GenericDaoBase;
|
||||
import com.cloud.utils.db.SearchBuilder;
|
||||
import com.cloud.utils.db.SearchCriteria;
|
||||
import com.cloud.vm.VmIsoMapVO;
|
||||
|
||||
@Component
|
||||
public class VmIsoMapDaoImpl extends GenericDaoBase<VmIsoMapVO, Long> implements VmIsoMapDao {
|
||||
|
||||
private SearchBuilder<VmIsoMapVO> ListByVmId;
|
||||
private SearchBuilder<VmIsoMapVO> ListByIsoId;
|
||||
private SearchBuilder<VmIsoMapVO> ByVmIdDeviceSeq;
|
||||
private SearchBuilder<VmIsoMapVO> ByVmIdIsoId;
|
||||
|
||||
protected VmIsoMapDaoImpl() {
|
||||
ListByVmId = createSearchBuilder();
|
||||
ListByVmId.and("vmId", ListByVmId.entity().getVmId(), SearchCriteria.Op.EQ);
|
||||
ListByVmId.done();
|
||||
|
||||
ListByIsoId = createSearchBuilder();
|
||||
ListByIsoId.and("isoId", ListByIsoId.entity().getIsoId(), SearchCriteria.Op.EQ);
|
||||
ListByIsoId.done();
|
||||
|
||||
ByVmIdDeviceSeq = createSearchBuilder();
|
||||
ByVmIdDeviceSeq.and("vmId", ByVmIdDeviceSeq.entity().getVmId(), SearchCriteria.Op.EQ);
|
||||
ByVmIdDeviceSeq.and("deviceSeq", ByVmIdDeviceSeq.entity().getDeviceSeq(), SearchCriteria.Op.EQ);
|
||||
ByVmIdDeviceSeq.done();
|
||||
|
||||
ByVmIdIsoId = createSearchBuilder();
|
||||
ByVmIdIsoId.and("vmId", ByVmIdIsoId.entity().getVmId(), SearchCriteria.Op.EQ);
|
||||
ByVmIdIsoId.and("isoId", ByVmIdIsoId.entity().getIsoId(), SearchCriteria.Op.EQ);
|
||||
ByVmIdIsoId.done();
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<VmIsoMapVO> listByVmId(long vmId) {
|
||||
SearchCriteria<VmIsoMapVO> sc = ListByVmId.create();
|
||||
sc.setParameters("vmId", vmId);
|
||||
return listBy(sc);
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<VmIsoMapVO> listByIsoId(long isoId) {
|
||||
SearchCriteria<VmIsoMapVO> sc = ListByIsoId.create();
|
||||
sc.setParameters("isoId", isoId);
|
||||
return listBy(sc);
|
||||
}
|
||||
|
||||
@Override
|
||||
public VmIsoMapVO findByVmIdDeviceSeq(long vmId, int deviceSeq) {
|
||||
SearchCriteria<VmIsoMapVO> sc = ByVmIdDeviceSeq.create();
|
||||
sc.setParameters("vmId", vmId);
|
||||
sc.setParameters("deviceSeq", deviceSeq);
|
||||
return findOneBy(sc);
|
||||
}
|
||||
|
||||
@Override
|
||||
public VmIsoMapVO findByVmIdIsoId(long vmId, long isoId) {
|
||||
SearchCriteria<VmIsoMapVO> sc = ByVmIdIsoId.create();
|
||||
sc.setParameters("vmId", vmId);
|
||||
sc.setParameters("isoId", isoId);
|
||||
return findOneBy(sc);
|
||||
}
|
||||
|
||||
@Override
|
||||
public int removeByVmId(long vmId) {
|
||||
SearchCriteria<VmIsoMapVO> sc = ListByVmId.create();
|
||||
sc.setParameters("vmId", vmId);
|
||||
return remove(sc);
|
||||
}
|
||||
}
|
||||
|
|
@ -108,6 +108,7 @@
|
|||
<bean id="instanceGroupJoinDaoImpl" class="com.cloud.api.query.dao.InstanceGroupJoinDaoImpl" />
|
||||
<bean id="managementServerJoinDaoImpl" class="com.cloud.api.query.dao.ManagementServerJoinDaoImpl" />
|
||||
<bean id="instanceGroupVMMapDaoImpl" class="com.cloud.vm.dao.InstanceGroupVMMapDaoImpl" />
|
||||
<bean id="vmIsoMapDaoImpl" class="com.cloud.vm.dao.VmIsoMapDaoImpl" />
|
||||
<bean id="itWorkDaoImpl" class="com.cloud.vm.ItWorkDaoImpl" />
|
||||
<bean id="lBHealthCheckPolicyDaoImpl" class="com.cloud.network.dao.LBHealthCheckPolicyDaoImpl" />
|
||||
<bean id="lBStickinessPolicyDaoImpl" class="com.cloud.network.dao.LBStickinessPolicyDaoImpl" />
|
||||
|
|
|
|||
|
|
@ -136,6 +136,20 @@ CREATE TABLE IF NOT EXISTS `cloud_usage`.`quota_tariff_usage` (
|
|||
CONSTRAINT `fk_quota_tariff_usage__tariff_id` FOREIGN KEY (`tariff_id`) REFERENCES `cloud_usage`.`quota_tariff` (`id`),
|
||||
CONSTRAINT `fk_quota_tariff_usage__quota_usage_id` FOREIGN KEY (`quota_usage_id`) REFERENCES `cloud_usage`.`quota_usage` (`id`));
|
||||
|
||||
--- Per-VM ISO attachments. user_vm.iso_id remains as the primary/bootable ISO pointer.
|
||||
CREATE TABLE IF NOT EXISTS `cloud`.`vm_iso_map` (
|
||||
`id` bigint(20) unsigned NOT NULL AUTO_INCREMENT,
|
||||
`vm_id` bigint(20) unsigned NOT NULL COMMENT 'foreign key to user_vm',
|
||||
`iso_id` bigint(20) unsigned NOT NULL COMMENT 'foreign key to vm_template (ISOs are templates of format ISO)',
|
||||
`device_seq` int(10) unsigned NOT NULL COMMENT 'cdrom slot index used to derive the libvirt device label (3=hdc, 4=hdd)',
|
||||
`created` datetime NOT NULL,
|
||||
PRIMARY KEY (`id`),
|
||||
UNIQUE KEY `uc_vm_iso_map__vm_iso` (`vm_id`, `iso_id`),
|
||||
UNIQUE KEY `uc_vm_iso_map__vm_seq` (`vm_id`, `device_seq`),
|
||||
CONSTRAINT `fk_vm_iso_map__vm_id` FOREIGN KEY (`vm_id`) REFERENCES `cloud`.`user_vm` (`id`) ON DELETE CASCADE,
|
||||
CONSTRAINT `fk_vm_iso_map__iso_id` FOREIGN KEY (`iso_id`) REFERENCES `cloud`.`vm_template` (`id`)
|
||||
);
|
||||
|
||||
-- Add the 'keep_mac_address_on_public_nic' column to the 'cloud.networks' and 'cloud.vpc' tables
|
||||
CALL `cloud`.`IDEMPOTENT_ADD_COLUMN`('cloud.networks', 'keep_mac_address_on_public_nic', 'TINYINT(1) NOT NULL DEFAULT 1');
|
||||
CALL `cloud`.`IDEMPOTENT_ADD_COLUMN`('cloud.vpc', 'keep_mac_address_on_public_nic', 'TINYINT(1) NOT NULL DEFAULT 1');
|
||||
|
|
|
|||
|
|
@ -0,0 +1,41 @@
|
|||
// Licensed to the Apache Software Foundation (ASF) under one
|
||||
// or more contributor license agreements. See the NOTICE file
|
||||
// distributed with this work for additional information
|
||||
// regarding copyright ownership. The ASF licenses this file
|
||||
// to you under the Apache License, Version 2.0 (the
|
||||
// "License"); you may not use this file except in compliance
|
||||
// with the License. You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing,
|
||||
// software distributed under the License is distributed on an
|
||||
// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
||||
// KIND, either express or implied. See the License for the
|
||||
// specific language governing permissions and limitations
|
||||
// under the License.
|
||||
package com.cloud.vm;
|
||||
|
||||
import org.junit.Assert;
|
||||
import org.junit.Test;
|
||||
|
||||
public class VmIsoMapVOTest {
|
||||
|
||||
@Test
|
||||
public void testFullConstructorPopulatesAllFields() {
|
||||
VmIsoMapVO row = new VmIsoMapVO(7L, 42L, 4);
|
||||
Assert.assertEquals(7L, row.getVmId());
|
||||
Assert.assertEquals(42L, row.getIsoId());
|
||||
Assert.assertEquals(4, row.getDeviceSeq());
|
||||
Assert.assertNotNull(row.getCreated());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testNoArgConstructorLeavesNonIdFieldsAtDefaults() {
|
||||
VmIsoMapVO row = new VmIsoMapVO();
|
||||
Assert.assertEquals(0L, row.getVmId());
|
||||
Assert.assertEquals(0L, row.getIsoId());
|
||||
Assert.assertEquals(0, row.getDeviceSeq());
|
||||
Assert.assertNull(row.getCreated());
|
||||
}
|
||||
}
|
||||
|
|
@ -16,6 +16,7 @@
|
|||
// under the License.
|
||||
package com.cloud.hypervisor.kvm.resource;
|
||||
|
||||
import static com.cloud.host.Host.HOST_CDROM_MAX_COUNT;
|
||||
import static com.cloud.host.Host.HOST_INSTANCE_CONVERSION;
|
||||
import static com.cloud.host.Host.HOST_OVFTOOL_VERSION;
|
||||
import static com.cloud.host.Host.HOST_VDDK_LIB_DIR;
|
||||
|
|
@ -226,6 +227,7 @@ import com.cloud.resource.ResourceStatusUpdater;
|
|||
import com.cloud.resource.ServerResource;
|
||||
import com.cloud.resource.ServerResourceBase;
|
||||
import com.cloud.storage.JavaStorageLayer;
|
||||
import com.cloud.template.TemplateManager;
|
||||
import com.cloud.storage.Storage;
|
||||
import com.cloud.storage.Storage.StoragePoolType;
|
||||
import com.cloud.storage.StorageLayer;
|
||||
|
|
@ -3696,6 +3698,7 @@ public class LibvirtComputingResource extends ServerResourceBase implements Serv
|
|||
if (vmSpec.getOs().toLowerCase().contains("window")) {
|
||||
isWindowsTemplate = true;
|
||||
}
|
||||
final Set<Integer> definedCdromSlots = new HashSet<>();
|
||||
for (final DiskTO volume : disks) {
|
||||
KVMPhysicalDisk physicalDisk = null;
|
||||
KVMStoragePool pool = null;
|
||||
|
|
@ -3774,6 +3777,7 @@ public class LibvirtComputingResource extends ServerResourceBase implements Serv
|
|||
if (volume.getType() == Volume.Type.ISO) {
|
||||
final DiskDef.DiskType diskType = getDiskType(physicalDisk);
|
||||
disk.defISODisk(volPath, devId, isUefiEnabled, diskType);
|
||||
definedCdromSlots.add(devId);
|
||||
|
||||
if (guestCpuArch != null && (guestCpuArch.equals("aarch64") || guestCpuArch.equals("s390x"))) {
|
||||
disk.setBusType(DiskDef.DiskBus.SCSI);
|
||||
|
|
@ -3871,6 +3875,17 @@ public class LibvirtComputingResource extends ServerResourceBase implements Serv
|
|||
vm.getDevices().addDevice(disk);
|
||||
}
|
||||
|
||||
if (vmSpec.getType() == VirtualMachine.Type.User) {
|
||||
for (int slot = TemplateManager.CDROM_PRIMARY_DEVICE_SEQ;
|
||||
slot < TemplateManager.CDROM_PRIMARY_DEVICE_SEQ + LibvirtVMDef.MAX_CDROMS_PER_VM; slot++) {
|
||||
if (!definedCdromSlots.contains(slot)) {
|
||||
final DiskDef emptyCdrom = new DiskDef();
|
||||
emptyCdrom.defISODisk(null, slot, isUefiEnabled, DiskDef.DiskType.FILE);
|
||||
vm.getDevices().addDevice(emptyCdrom);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (vmSpec.getType() != VirtualMachine.Type.User) {
|
||||
final DiskDef iso = new DiskDef();
|
||||
iso.defISODisk(sysvmISOPath, DiskDef.DiskType.FILE);
|
||||
|
|
@ -4381,6 +4396,7 @@ public class LibvirtComputingResource extends ServerResourceBase implements Serv
|
|||
boolean instanceConversionSupported = hostSupportsInstanceConversion();
|
||||
cmd.getHostDetails().put(HOST_INSTANCE_CONVERSION, String.valueOf(instanceConversionSupported));
|
||||
cmd.getHostDetails().put(HOST_VDDK_SUPPORT, String.valueOf(hostSupportsVddk()));
|
||||
cmd.getHostDetails().put(HOST_CDROM_MAX_COUNT, String.valueOf(LibvirtVMDef.MAX_CDROMS_PER_VM));
|
||||
if (StringUtils.isNotBlank(vddkLibDir)) {
|
||||
cmd.getHostDetails().put(HOST_VDDK_LIB_DIR, vddkLibDir);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -57,6 +57,10 @@ import static java.time.format.DateTimeFormatter.ISO_LOCAL_DATE_TIME;
|
|||
public class LibvirtVMDef {
|
||||
protected static Logger LOGGER = LogManager.getLogger(LibvirtVMDef.class);
|
||||
|
||||
// CD-ROM slot allocation: getDevLabel() maps deviceSeq=3,4 to hdc and hdd on the IDE bus.
|
||||
// Bumping this requires extending getDevLabel() (e.g. to spill onto SATA or a second IDE controller).
|
||||
public static final int MAX_CDROMS_PER_VM = 2;
|
||||
|
||||
private String _hvsType;
|
||||
private static long s_libvirtVersion;
|
||||
private static long s_qemuVersion;
|
||||
|
|
|
|||
|
|
@ -21,6 +21,8 @@ package com.cloud.hypervisor.kvm.storage;
|
|||
import static com.cloud.utils.NumbersUtil.toHumanReadableSize;
|
||||
import static com.cloud.utils.storage.S3.S3Utils.putFile;
|
||||
|
||||
import com.cloud.template.TemplateManager;
|
||||
|
||||
import java.io.File;
|
||||
import java.io.FileNotFoundException;
|
||||
import java.io.FileOutputStream;
|
||||
|
|
@ -1346,10 +1348,11 @@ public class KVMStorageProcessor implements StorageProcessor {
|
|||
}
|
||||
}
|
||||
|
||||
protected synchronized void attachOrDetachISO(final Connect conn, final String vmName, String isoPath, final boolean isAttach, Map<String, String> params, DataStoreTO store) throws
|
||||
protected synchronized void attachOrDetachISO(final Connect conn, final String vmName, String isoPath, final boolean isAttach, Map<String, String> params, DataStoreTO store, Integer deviceSeq) throws
|
||||
LibvirtException, InternalErrorException {
|
||||
DiskDef iso = new DiskDef();
|
||||
boolean isUefiEnabled = MapUtils.isNotEmpty(params) && params.containsKey("UEFI");
|
||||
Integer devId = (deviceSeq != null) ? deviceSeq : TemplateManager.CDROM_PRIMARY_DEVICE_SEQ;
|
||||
if (isoPath != null && isAttach) {
|
||||
final int index = isoPath.lastIndexOf("/");
|
||||
final String path = isoPath.substring(0, index);
|
||||
|
|
@ -1365,9 +1368,9 @@ public class KVMStorageProcessor implements StorageProcessor {
|
|||
final DiskDef.DiskType isoDiskType = LibvirtComputingResource.getDiskType(isoVol);
|
||||
isoPath = isoVol.getPath();
|
||||
|
||||
iso.defISODisk(isoPath, isUefiEnabled, isoDiskType);
|
||||
iso.defISODisk(isoPath, devId, isUefiEnabled, isoDiskType);
|
||||
} else {
|
||||
iso.defISODisk(null, isUefiEnabled, DiskDef.DiskType.FILE);
|
||||
iso.defISODisk(null, devId, isUefiEnabled, DiskDef.DiskType.FILE);
|
||||
}
|
||||
|
||||
final List<DiskDef> disks = resource.getDisks(conn, vmName);
|
||||
|
|
@ -1387,11 +1390,12 @@ public class KVMStorageProcessor implements StorageProcessor {
|
|||
final DiskTO disk = cmd.getDisk();
|
||||
final TemplateObjectTO isoTO = (TemplateObjectTO)disk.getData();
|
||||
final DataStoreTO store = isoTO.getDataStore();
|
||||
final Integer deviceSeq = (disk.getDiskSeq() != null) ? disk.getDiskSeq().intValue() : null;
|
||||
|
||||
try {
|
||||
String dataStoreUrl = getDataStoreUrlFromStore(store);
|
||||
final Connect conn = LibvirtConnection.getConnectionByVmName(cmd.getVmName());
|
||||
attachOrDetachISO(conn, cmd.getVmName(), dataStoreUrl + File.separator + isoTO.getPath(), true, cmd.getControllerInfo(), store);
|
||||
attachOrDetachISO(conn, cmd.getVmName(), dataStoreUrl + File.separator + isoTO.getPath(), true, cmd.getControllerInfo(), store, deviceSeq);
|
||||
} catch (final LibvirtException e) {
|
||||
return new Answer(cmd, false, e.toString());
|
||||
} catch (final InternalErrorException e) {
|
||||
|
|
@ -1408,11 +1412,12 @@ public class KVMStorageProcessor implements StorageProcessor {
|
|||
final DiskTO disk = cmd.getDisk();
|
||||
final TemplateObjectTO isoTO = (TemplateObjectTO)disk.getData();
|
||||
final DataStoreTO store = isoTO.getDataStore();
|
||||
final Integer deviceSeq = (disk.getDiskSeq() != null) ? disk.getDiskSeq().intValue() : null;
|
||||
|
||||
try {
|
||||
String dataStoreUrl = getDataStoreUrlFromStore(store);
|
||||
final Connect conn = LibvirtConnection.getConnectionByVmName(cmd.getVmName());
|
||||
attachOrDetachISO(conn, cmd.getVmName(), dataStoreUrl + File.separator + isoTO.getPath(), false, cmd.getParams(), store);
|
||||
attachOrDetachISO(conn, cmd.getVmName(), dataStoreUrl + File.separator + isoTO.getPath(), false, cmd.getParams(), store, deviceSeq);
|
||||
} catch (final LibvirtException e) {
|
||||
return new Answer(cmd, false, e.toString());
|
||||
} catch (final InternalErrorException e) {
|
||||
|
|
|
|||
|
|
@ -39,6 +39,7 @@ import org.apache.cloudstack.annotation.dao.AnnotationDao;
|
|||
import org.apache.cloudstack.api.ApiConstants;
|
||||
import org.apache.cloudstack.api.ApiConstants.VMDetails;
|
||||
import org.apache.cloudstack.api.ResponseObject.ResponseView;
|
||||
import org.apache.cloudstack.api.response.AttachedIsoResponse;
|
||||
import org.apache.cloudstack.api.response.NicExtraDhcpOptionResponse;
|
||||
import org.apache.cloudstack.api.response.NicResponse;
|
||||
import org.apache.cloudstack.api.response.NicSecondaryIpResponse;
|
||||
|
|
@ -62,6 +63,11 @@ import com.cloud.gpu.GPU;
|
|||
import com.cloud.gpu.dao.VgpuProfileDao;
|
||||
import com.cloud.host.ControlState;
|
||||
import com.cloud.hypervisor.Hypervisor;
|
||||
import com.cloud.host.DetailVO;
|
||||
import com.cloud.host.Host;
|
||||
import com.cloud.host.HostVO;
|
||||
import com.cloud.host.dao.HostDao;
|
||||
import com.cloud.host.dao.HostDetailsDao;
|
||||
import com.cloud.network.IpAddress;
|
||||
import com.cloud.network.vpc.VpcVO;
|
||||
import com.cloud.network.vpc.dao.VpcDao;
|
||||
|
|
@ -72,6 +78,7 @@ import com.cloud.storage.GuestOS;
|
|||
import com.cloud.storage.Storage.TemplateType;
|
||||
import com.cloud.storage.VMTemplateVO;
|
||||
import com.cloud.storage.VnfTemplateDetailVO;
|
||||
import com.cloud.template.TemplateManager;
|
||||
import com.cloud.storage.VnfTemplateNicVO;
|
||||
import com.cloud.storage.Volume;
|
||||
import com.cloud.storage.dao.VMTemplateDao;
|
||||
|
|
@ -93,10 +100,12 @@ import com.cloud.vm.UserVmManager;
|
|||
import com.cloud.vm.VMInstanceDetailVO;
|
||||
import com.cloud.vm.VirtualMachine;
|
||||
import com.cloud.vm.VirtualMachine.State;
|
||||
import com.cloud.vm.VmIsoMapVO;
|
||||
import com.cloud.vm.VmStats;
|
||||
import com.cloud.vm.dao.NicExtraDhcpOptionDao;
|
||||
import com.cloud.vm.dao.NicSecondaryIpVO;
|
||||
import com.cloud.vm.dao.VMInstanceDetailsDao;
|
||||
import com.cloud.vm.dao.VmIsoMapDao;
|
||||
|
||||
@Component
|
||||
public class UserVmJoinDaoImpl extends GenericDaoBaseWithTagInformation<UserVmJoinVO, UserVmResponse> implements UserVmJoinDao {
|
||||
|
|
@ -130,6 +139,12 @@ public class UserVmJoinDaoImpl extends GenericDaoBaseWithTagInformation<UserVmJo
|
|||
@Inject
|
||||
VMTemplateDao vmTemplateDao;
|
||||
@Inject
|
||||
VmIsoMapDao vmIsoMapDao;
|
||||
@Inject
|
||||
HostDetailsDao hostDetailsDao;
|
||||
@Inject
|
||||
HostDao hostDao;
|
||||
@Inject
|
||||
ExtensionHelper extensionHelper;
|
||||
|
||||
private final SearchBuilder<UserVmJoinVO> VmDetailSearch;
|
||||
|
|
@ -246,6 +261,23 @@ public class UserVmJoinDaoImpl extends GenericDaoBaseWithTagInformation<UserVmJo
|
|||
userVmResponse.setIsoId(userVm.getIsoUuid());
|
||||
userVmResponse.setIsoName(userVm.getIsoName());
|
||||
userVmResponse.setIsoDisplayText(userVm.getIsoDisplayText());
|
||||
|
||||
List<AttachedIsoResponse> attachedIsos = new ArrayList<>();
|
||||
if (userVm.getIsoUuid() != null) {
|
||||
VMTemplateVO bootIso = vmTemplateDao.findById(userVm.getIsoId());
|
||||
boolean bootIsoBootable = bootIso != null && bootIso.isBootable();
|
||||
attachedIsos.add(new AttachedIsoResponse(userVm.getIsoUuid(), userVm.getIsoName(),
|
||||
userVm.getIsoDisplayText(), TemplateManager.CDROM_PRIMARY_DEVICE_SEQ, bootIsoBootable));
|
||||
}
|
||||
for (VmIsoMapVO row : vmIsoMapDao.listByVmId(userVm.getId())) {
|
||||
VMTemplateVO tmpl = vmTemplateDao.findById(row.getIsoId());
|
||||
if (tmpl != null) {
|
||||
attachedIsos.add(new AttachedIsoResponse(tmpl.getUuid(), tmpl.getName(),
|
||||
tmpl.getDisplayText(), row.getDeviceSeq(), false));
|
||||
}
|
||||
}
|
||||
userVmResponse.setIsos(attachedIsos);
|
||||
userVmResponse.setIsoMaxCount(effectiveCdromMaxCount(userVm));
|
||||
}
|
||||
if (details.contains(VMDetails.all) || details.contains(VMDetails.servoff)) {
|
||||
userVmResponse.setServiceOfferingId(userVm.getServiceOfferingUuid());
|
||||
|
|
@ -540,6 +572,44 @@ public class UserVmJoinDaoImpl extends GenericDaoBaseWithTagInformation<UserVmJo
|
|||
return ChronoUnit.DAYS.between(createdDate, expiryDate);
|
||||
}
|
||||
|
||||
int effectiveCdromMaxCount(UserVmJoinVO userVm) {
|
||||
Long hostId = userVm.getHostId() != null && userVm.getHostId() > 0
|
||||
? userVm.getHostId() : userVm.getLastHostId();
|
||||
if (hostId == null && userVm.getHypervisorType() != null) {
|
||||
List<HostVO> candidates = hostDao.listByDataCenterIdAndHypervisorType(userVm.getDataCenterId(), userVm.getHypervisorType());
|
||||
if (!candidates.isEmpty()) {
|
||||
hostId = candidates.get(0).getId();
|
||||
}
|
||||
}
|
||||
Long clusterId = userVm.getClusterId();
|
||||
if (clusterId == null && hostId != null) {
|
||||
HostVO host = hostDao.findById(hostId);
|
||||
if (host != null) {
|
||||
clusterId = host.getClusterId();
|
||||
}
|
||||
}
|
||||
int configuredCap = TemplateManager.VmIsoMaxCount.valueIn(clusterId);
|
||||
int hypervisorCap = advertisedCdromCap(hostId);
|
||||
// List endpoint clamps for display robustness; the action paths in TemplateManagerImpl
|
||||
// throw on misconfiguration so operators still see the loud error when they try to attach.
|
||||
return Math.min(configuredCap, hypervisorCap);
|
||||
}
|
||||
|
||||
int advertisedCdromCap(Long hostId) {
|
||||
if (hostId == null) {
|
||||
return TemplateManager.DEFAULT_CDROM_MAX_PER_VM;
|
||||
}
|
||||
DetailVO detail = hostDetailsDao.findDetail(hostId, Host.HOST_CDROM_MAX_COUNT);
|
||||
if (detail == null || detail.getValue() == null) {
|
||||
return TemplateManager.DEFAULT_CDROM_MAX_PER_VM;
|
||||
}
|
||||
try {
|
||||
return Integer.parseInt(detail.getValue());
|
||||
} catch (NumberFormatException e) {
|
||||
return TemplateManager.DEFAULT_CDROM_MAX_PER_VM;
|
||||
}
|
||||
}
|
||||
|
||||
private void addVnfInfoToserVmResponse(UserVmJoinVO userVm, UserVmResponse userVmResponse) {
|
||||
List<VnfTemplateNicVO> vnfNics = vnfTemplateNicDao.listByTemplateId(userVm.getTemplateId());
|
||||
for (VnfTemplateNicVO nic : vnfNics) {
|
||||
|
|
|
|||
|
|
@ -21,6 +21,7 @@ import java.net.URISyntaxException;
|
|||
import java.net.URL;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Arrays;
|
||||
import java.util.Collections;
|
||||
import java.util.Date;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
|
|
@ -145,8 +146,11 @@ import com.cloud.exception.InvalidParameterValueException;
|
|||
import com.cloud.exception.PermissionDeniedException;
|
||||
import com.cloud.exception.ResourceAllocationException;
|
||||
import com.cloud.exception.StorageUnavailableException;
|
||||
import com.cloud.host.DetailVO;
|
||||
import com.cloud.host.Host;
|
||||
import com.cloud.host.HostVO;
|
||||
import com.cloud.host.dao.HostDao;
|
||||
import com.cloud.host.dao.HostDetailsDao;
|
||||
import com.cloud.hypervisor.Hypervisor;
|
||||
import com.cloud.hypervisor.Hypervisor.HypervisorType;
|
||||
import com.cloud.hypervisor.HypervisorGuru;
|
||||
|
|
@ -222,7 +226,9 @@ import com.cloud.vm.VirtualMachine.State;
|
|||
import com.cloud.vm.VirtualMachineProfile;
|
||||
import com.cloud.vm.VirtualMachineProfileImpl;
|
||||
import com.cloud.vm.VmDetailConstants;
|
||||
import com.cloud.vm.VmIsoMapVO;
|
||||
import com.cloud.vm.dao.UserVmDao;
|
||||
import com.cloud.vm.dao.VmIsoMapDao;
|
||||
import com.cloud.vm.dao.VMInstanceDao;
|
||||
import com.google.gson.Gson;
|
||||
import com.google.gson.GsonBuilder;
|
||||
|
|
@ -252,10 +258,14 @@ public class TemplateManagerImpl extends ManagerBase implements TemplateManager,
|
|||
@Inject
|
||||
private HostDao _hostDao;
|
||||
@Inject
|
||||
private HostDetailsDao _hostDetailsDao;
|
||||
@Inject
|
||||
private DataCenterDao _dcDao;
|
||||
@Inject
|
||||
private UserVmDao _userVmDao;
|
||||
@Inject
|
||||
private VmIsoMapDao _vmIsoMapDao;
|
||||
@Inject
|
||||
private VolumeDao _volumeDao;
|
||||
@Inject
|
||||
private SnapshotDao _snapshotDao;
|
||||
|
|
@ -679,47 +689,75 @@ public class TemplateManagerImpl extends ManagerBase implements TemplateManager,
|
|||
@Override
|
||||
public void prepareIsoForVmProfile(VirtualMachineProfile profile, DeployDestination dest) {
|
||||
UserVmVO vm = _userVmDao.findById(profile.getId());
|
||||
if (vm.getIsoId() != null) {
|
||||
Map<Volume, StoragePool> storageForDisks = dest.getStorageForDisks();
|
||||
Long poolId = null;
|
||||
TemplateInfo template;
|
||||
if (MapUtils.isNotEmpty(storageForDisks)) {
|
||||
for (StoragePool storagePool : storageForDisks.values()) {
|
||||
if (poolId != null && storagePool.getId() != poolId) {
|
||||
throw new CloudRuntimeException("Cannot determine where to download ISO");
|
||||
}
|
||||
poolId = storagePool.getId();
|
||||
}
|
||||
}
|
||||
template = prepareIso(vm.getIsoId(), vm.getDataCenterId(), dest.getHost().getId(), poolId);
|
||||
Map<Integer, Long> slotToIsoId = loadAttachedIsoSlots(vm);
|
||||
Long poolId = slotToIsoId.isEmpty() ? null : singleStoragePoolId(dest);
|
||||
|
||||
if (template == null){
|
||||
logger.error("Failed to prepare ISO on secondary or cache storage");
|
||||
throw new CloudRuntimeException("Failed to prepare ISO on secondary or cache storage");
|
||||
}
|
||||
if (template.isBootable()) {
|
||||
profile.setBootLoaderType(BootloaderType.CD);
|
||||
}
|
||||
|
||||
GuestOSVO guestOS = _guestOSDao.findById(template.getGuestOSId());
|
||||
String displayName = null;
|
||||
if (guestOS != null) {
|
||||
displayName = guestOS.getDisplayName();
|
||||
}
|
||||
|
||||
TemplateObjectTO iso = (TemplateObjectTO)template.getTO();
|
||||
iso.setDirectDownload(template.isDirectDownload());
|
||||
iso.setGuestOsType(displayName);
|
||||
DiskTO disk = new DiskTO(iso, 3L, null, Volume.Type.ISO);
|
||||
profile.addDisk(disk);
|
||||
} else {
|
||||
TemplateObjectTO iso = new TemplateObjectTO();
|
||||
iso.setFormat(ImageFormat.ISO);
|
||||
DiskTO disk = new DiskTO(iso, 3L, null, Volume.Type.ISO);
|
||||
profile.addDisk(disk);
|
||||
// Pre-allocate every cdrom slot at boot. QEMU/IDE refuses to hot-add new cdrom drives, so
|
||||
// runtime attachIso can only media-swap into a slot the domain already owns.
|
||||
int totalSlots = Math.max(effectiveMaxCdroms(vm, dest.getHost().getId()), slotsNeededFor(slotToIsoId));
|
||||
for (int i = 0; i < totalSlots; i++) {
|
||||
int diskSeq = CDROM_PRIMARY_DEVICE_SEQ + i;
|
||||
Long isoId = slotToIsoId.get(diskSeq);
|
||||
profile.addDisk(isoId != null
|
||||
? buildIsoDisk(profile, vm, dest, poolId, diskSeq, isoId)
|
||||
: buildEmptyCdromDisk(diskSeq));
|
||||
}
|
||||
}
|
||||
|
||||
private Long singleStoragePoolId(DeployDestination dest) {
|
||||
Long poolId = null;
|
||||
Map<Volume, StoragePool> storageForDisks = dest.getStorageForDisks();
|
||||
if (MapUtils.isNotEmpty(storageForDisks)) {
|
||||
for (StoragePool pool : storageForDisks.values()) {
|
||||
if (poolId != null && pool.getId() != poolId) {
|
||||
throw new CloudRuntimeException("Cannot determine where to download ISO");
|
||||
}
|
||||
poolId = pool.getId();
|
||||
}
|
||||
}
|
||||
return poolId;
|
||||
}
|
||||
|
||||
private Map<Integer, Long> loadAttachedIsoSlots(UserVmVO vm) {
|
||||
Map<Integer, Long> slots = new HashMap<>();
|
||||
if (vm.getIsoId() != null) {
|
||||
slots.put(CDROM_PRIMARY_DEVICE_SEQ, vm.getIsoId());
|
||||
}
|
||||
for (VmIsoMapVO row : _vmIsoMapDao.listByVmId(vm.getId())) {
|
||||
slots.put(row.getDeviceSeq(), row.getIsoId());
|
||||
}
|
||||
return slots;
|
||||
}
|
||||
|
||||
private int slotsNeededFor(Map<Integer, Long> slotToIsoId) {
|
||||
if (slotToIsoId.isEmpty()) {
|
||||
return 0;
|
||||
}
|
||||
return Collections.max(slotToIsoId.keySet()) - CDROM_PRIMARY_DEVICE_SEQ + 1;
|
||||
}
|
||||
|
||||
private DiskTO buildIsoDisk(VirtualMachineProfile profile, UserVmVO vm, DeployDestination dest, Long poolId, int diskSeq, long isoId) {
|
||||
TemplateInfo template = prepareIso(isoId, vm.getDataCenterId(), dest.getHost().getId(), poolId);
|
||||
if (template == null) {
|
||||
logger.error("Failed to prepare ISO on secondary or cache storage");
|
||||
throw new CloudRuntimeException("Failed to prepare ISO on secondary or cache storage");
|
||||
}
|
||||
if (diskSeq == CDROM_PRIMARY_DEVICE_SEQ && template.isBootable()) {
|
||||
profile.setBootLoaderType(BootloaderType.CD);
|
||||
}
|
||||
GuestOSVO guestOS = _guestOSDao.findById(template.getGuestOSId());
|
||||
TemplateObjectTO iso = (TemplateObjectTO) template.getTO();
|
||||
iso.setDirectDownload(template.isDirectDownload());
|
||||
iso.setGuestOsType(guestOS != null ? guestOS.getDisplayName() : null);
|
||||
return new DiskTO(iso, (long) diskSeq, null, Volume.Type.ISO);
|
||||
}
|
||||
|
||||
private DiskTO buildEmptyCdromDisk(int diskSeq) {
|
||||
TemplateObjectTO empty = new TemplateObjectTO();
|
||||
empty.setFormat(ImageFormat.ISO);
|
||||
return new DiskTO(empty, (long) diskSeq, null, Volume.Type.ISO);
|
||||
}
|
||||
|
||||
private void prepareTemplateInOneStoragePool(final VMTemplateVO template, final StoragePoolVO pool) {
|
||||
logger.info("Schedule to preload Template {} into primary storage {}", template, pool);
|
||||
if (pool.getPoolType() == Storage.StoragePoolType.DatastoreCluster) {
|
||||
|
|
@ -1206,17 +1244,20 @@ public class TemplateManagerImpl extends ManagerBase implements TemplateManager,
|
|||
|
||||
@Override
|
||||
public boolean templateIsDeleteable(long templateId) {
|
||||
// ISO can only be referenced by user_vm.iso_id (primary cdrom slot) or vm_iso_map (extra slots).
|
||||
// Templates always live on primary storage and aren't tracked here.
|
||||
List<UserVmJoinVO> userVmUsingIso = _userVmJoinDao.listActiveByIsoId(templateId);
|
||||
// check if there is any Vm using this ISO. We only need to check the
|
||||
// case where templateId is an ISO since
|
||||
// VM can be launched from ISO in secondary storage, while template will
|
||||
// always be copied to
|
||||
// primary storage before deploying VM.
|
||||
if (!userVmUsingIso.isEmpty()) {
|
||||
logger.debug("ISO " + templateId + " is not deleteable because it is attached to " + userVmUsingIso.size() + " Instances");
|
||||
logger.debug("Unable to delete ISO {} because it is attached to {} Instances", templateId, userVmUsingIso.size());
|
||||
return false;
|
||||
}
|
||||
|
||||
for (VmIsoMapVO row : _vmIsoMapDao.listByIsoId(templateId)) {
|
||||
UserVmVO vm = _userVmDao.findById(row.getVmId());
|
||||
if (vm != null && vm.getState() != State.Error && vm.getState() != State.Expunging) {
|
||||
logger.debug("Unable to delete ISO {} because it is attached to Instance {} at slot {}", templateId, vm.getUuid(), row.getDeviceSeq());
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
|
|
@ -1237,7 +1278,14 @@ public class TemplateManagerImpl extends ManagerBase implements TemplateManager,
|
|||
|
||||
_accountMgr.checkAccess(caller, null, true, virtualMachine);
|
||||
|
||||
Long isoId = !isVirtualRouter ? ((UserVm) virtualMachine).getIsoId() : isoParamId;
|
||||
Long isoId;
|
||||
if (isVirtualRouter) {
|
||||
isoId = isoParamId;
|
||||
} else {
|
||||
Long primaryIsoId = ((UserVm) virtualMachine).getIsoId();
|
||||
List<VmIsoMapVO> extras = _vmIsoMapDao.listByVmId(vmId);
|
||||
isoId = resolveIsoIdForDetach(primaryIsoId, extras, isoParamId);
|
||||
}
|
||||
if (isoId == null) {
|
||||
throw new InvalidParameterValueException("The specified instance has no ISO attached to it.");
|
||||
}
|
||||
|
|
@ -1321,6 +1369,9 @@ public class TemplateManagerImpl extends ManagerBase implements TemplateManager,
|
|||
if (VMWARE_TOOLS_ISO.equals(iso.getUniqueName()) && vm.getHypervisorType() != Hypervisor.HypervisorType.VMware) {
|
||||
throw new InvalidParameterValueException("Cannot attach VMware tools drivers to incompatible hypervisor " + vm.getHypervisorType());
|
||||
}
|
||||
if (!isVirtualRouter) {
|
||||
enforceCdromAttachLimits(vmId, (UserVm) vm, isoId);
|
||||
}
|
||||
boolean result = attachISOToVM(vmId, userId, isoId, true, forced, isVirtualRouter);
|
||||
if (result) {
|
||||
return result;
|
||||
|
|
@ -1360,7 +1411,7 @@ public class TemplateManagerImpl extends ManagerBase implements TemplateManager,
|
|||
}
|
||||
}
|
||||
|
||||
private boolean attachISOToVM(long vmId, long isoId, boolean attach, boolean forced, boolean isVirtualRouter) {
|
||||
private boolean attachISOToVM(long vmId, long isoId, int deviceSeq, boolean attach, boolean forced, boolean isVirtualRouter) {
|
||||
VirtualMachine vm = !isVirtualRouter ? _userVmDao.findById(vmId) : _vmInstanceDao.findById(vmId);
|
||||
|
||||
if (vm == null || (isVirtualRouter && vm.getType() != VirtualMachine.Type.DomainRouter)) {
|
||||
|
|
@ -1384,7 +1435,7 @@ public class TemplateManagerImpl extends ManagerBase implements TemplateManager,
|
|||
}
|
||||
|
||||
DataTO isoTO = tmplt.getTO();
|
||||
DiskTO disk = new DiskTO(isoTO, null, null, Volume.Type.ISO);
|
||||
DiskTO disk = new DiskTO(isoTO, (long) deviceSeq, null, Volume.Type.ISO);
|
||||
|
||||
HypervisorGuru hvGuru = _hvGuruMgr.getGuru(vm.getHypervisorType());
|
||||
VirtualMachineProfile profile = new VirtualMachineProfileImpl(vm);
|
||||
|
|
@ -1402,22 +1453,150 @@ public class TemplateManagerImpl extends ManagerBase implements TemplateManager,
|
|||
return (a != null && a.getResult());
|
||||
}
|
||||
|
||||
private boolean attachISOToVM(long vmId, long userId, long isoId, boolean attach, boolean forced, boolean isVirtualRouter) {
|
||||
boolean attachISOToVM(long vmId, long userId, long isoId, boolean attach, boolean forced, boolean isVirtualRouter) {
|
||||
UserVmVO vm = _userVmDao.findById(vmId);
|
||||
VMTemplateVO iso = _tmpltDao.findById(isoId);
|
||||
|
||||
boolean success = attachISOToVM(vmId, isoId, attach, forced, isVirtualRouter);
|
||||
if (success && attach && !isVirtualRouter) {
|
||||
vm.setIsoId(iso.getId());
|
||||
_userVmDao.update(vmId, vm);
|
||||
int targetSlot = attach ? chooseAttachSlot(vmId, vm) : findAttachedSlot(vmId, vm, isoId);
|
||||
boolean success = attachISOToVM(vmId, isoId, targetSlot, attach, forced, isVirtualRouter);
|
||||
if (!success || isVirtualRouter) {
|
||||
return success;
|
||||
}
|
||||
if (success && !attach && !isVirtualRouter) {
|
||||
vm.setIsoId(null);
|
||||
_userVmDao.update(vmId, vm);
|
||||
if (attach) {
|
||||
persistIsoAttachment(vmId, vm, iso, targetSlot);
|
||||
} else {
|
||||
persistIsoDetachment(vmId, vm, isoId, targetSlot);
|
||||
}
|
||||
return success;
|
||||
}
|
||||
|
||||
private int chooseAttachSlot(long vmId, UserVmVO vm) {
|
||||
if (vm.getIsoId() == null) {
|
||||
return CDROM_PRIMARY_DEVICE_SEQ;
|
||||
}
|
||||
VmIsoMapVO highest = highestCdromMapEntry(vmId);
|
||||
return highest == null ? CDROM_PRIMARY_DEVICE_SEQ + 1 : highest.getDeviceSeq() + 1;
|
||||
}
|
||||
|
||||
private int findAttachedSlot(long vmId, UserVmVO vm, long isoId) {
|
||||
if (vm.getIsoId() != null && vm.getIsoId() == isoId) {
|
||||
return CDROM_PRIMARY_DEVICE_SEQ;
|
||||
}
|
||||
VmIsoMapVO entry = _vmIsoMapDao.findByVmIdIsoId(vmId, isoId);
|
||||
return entry != null ? entry.getDeviceSeq() : CDROM_PRIMARY_DEVICE_SEQ;
|
||||
}
|
||||
|
||||
private void persistIsoAttachment(long vmId, UserVmVO vm, VMTemplateVO iso, int slot) {
|
||||
if (slot == CDROM_PRIMARY_DEVICE_SEQ) {
|
||||
vm.setIsoId(iso.getId());
|
||||
_userVmDao.update(vmId, vm);
|
||||
} else {
|
||||
_vmIsoMapDao.persist(new VmIsoMapVO(vmId, iso.getId(), slot));
|
||||
}
|
||||
}
|
||||
|
||||
private void persistIsoDetachment(long vmId, UserVmVO vm, long isoId, int slot) {
|
||||
if (slot == CDROM_PRIMARY_DEVICE_SEQ) {
|
||||
vm.setIsoId(null);
|
||||
_userVmDao.update(vmId, vm);
|
||||
return;
|
||||
}
|
||||
VmIsoMapVO entry = _vmIsoMapDao.findByVmIdIsoId(vmId, isoId);
|
||||
if (entry != null) {
|
||||
_vmIsoMapDao.remove(entry.getId());
|
||||
}
|
||||
}
|
||||
|
||||
VmIsoMapVO highestCdromMapEntry(long vmId) {
|
||||
VmIsoMapVO highest = null;
|
||||
for (VmIsoMapVO row : _vmIsoMapDao.listByVmId(vmId)) {
|
||||
if (highest == null || row.getDeviceSeq() > highest.getDeviceSeq()) {
|
||||
highest = row;
|
||||
}
|
||||
}
|
||||
return highest;
|
||||
}
|
||||
|
||||
Long resolveIsoIdForDetach(Long primaryIsoId, List<VmIsoMapVO> extras, Long isoParamId) {
|
||||
if (isoParamId != null) {
|
||||
boolean attached = (primaryIsoId != null && primaryIsoId.equals(isoParamId))
|
||||
|| extras.stream().anyMatch(r -> r.getIsoId() == isoParamId);
|
||||
if (!attached) {
|
||||
throw new InvalidParameterValueException("The specified ISO is not attached to this Instance.");
|
||||
}
|
||||
return isoParamId;
|
||||
}
|
||||
int totalAttached = (primaryIsoId != null ? 1 : 0) + extras.size();
|
||||
if (totalAttached == 0) {
|
||||
throw new InvalidParameterValueException("The specified instance has no ISO attached to it.");
|
||||
}
|
||||
if (totalAttached > 1) {
|
||||
throw new InvalidParameterValueException("Instance has more than one ISO attached; specify the 'id' parameter to choose which to detach.");
|
||||
}
|
||||
return primaryIsoId != null ? primaryIsoId : extras.get(0).getIsoId();
|
||||
}
|
||||
|
||||
boolean isIsoAlreadyAttached(long vmId, Long primaryIsoId, long isoId) {
|
||||
if (primaryIsoId != null && primaryIsoId.equals(isoId)) {
|
||||
return true;
|
||||
}
|
||||
return _vmIsoMapDao.findByVmIdIsoId(vmId, isoId) != null;
|
||||
}
|
||||
|
||||
void enforceCdromAttachLimits(long vmId, UserVm vm, long isoId) {
|
||||
Long primaryIsoId = vm.getIsoId();
|
||||
if (isIsoAlreadyAttached(vmId, primaryIsoId, isoId)) {
|
||||
throw new InvalidParameterValueException("The specified ISO is already attached to this Instance.");
|
||||
}
|
||||
int effectiveMax = effectiveMaxCdroms(vm, hostIdForVm(vm));
|
||||
int attached = (primaryIsoId != null ? 1 : 0) + _vmIsoMapDao.listByVmId(vmId).size();
|
||||
if (attached >= effectiveMax) {
|
||||
throw new InvalidParameterValueException(String.format(
|
||||
"Instance has reached the maximum of %d attached CD-ROM(s); detach one before attaching another.", effectiveMax));
|
||||
}
|
||||
}
|
||||
|
||||
int effectiveMaxCdroms(VirtualMachine vm, Long hostId) {
|
||||
HostVO host = hostId != null ? _hostDao.findById(hostId) : null;
|
||||
Long clusterId = host != null ? host.getClusterId() : null;
|
||||
int configuredCap = VmIsoMaxCount.valueIn(clusterId);
|
||||
int hypervisorCap = advertisedCdromCap(hostId);
|
||||
if (configuredCap > hypervisorCap) {
|
||||
logger.warn("{} is set to {} but the placement host supports a maximum of {} CD-ROM(s) per Instance. Clamping to {}.",
|
||||
VmIsoMaxCount.key(), configuredCap, hypervisorCap, hypervisorCap);
|
||||
return hypervisorCap;
|
||||
}
|
||||
return configuredCap;
|
||||
}
|
||||
|
||||
int advertisedCdromCap(Long hostId) {
|
||||
if (hostId == null) {
|
||||
return DEFAULT_CDROM_MAX_PER_VM;
|
||||
}
|
||||
DetailVO detail = _hostDetailsDao.findDetail(hostId, Host.HOST_CDROM_MAX_COUNT);
|
||||
if (detail == null || detail.getValue() == null) {
|
||||
return DEFAULT_CDROM_MAX_PER_VM;
|
||||
}
|
||||
try {
|
||||
return Integer.parseInt(detail.getValue());
|
||||
} catch (NumberFormatException e) {
|
||||
logger.warn("Invalid {} value '{}' for host {}; using default {}.",
|
||||
Host.HOST_CDROM_MAX_COUNT, detail.getValue(), hostId, DEFAULT_CDROM_MAX_PER_VM);
|
||||
return DEFAULT_CDROM_MAX_PER_VM;
|
||||
}
|
||||
}
|
||||
|
||||
Long hostIdForVm(VirtualMachine vm) {
|
||||
Long hostId = vm.getHostId() != null ? vm.getHostId() : vm.getLastHostId();
|
||||
if (hostId == null && vm.getHypervisorType() != null) {
|
||||
List<HostVO> candidates = _hostDao.listByDataCenterIdAndHypervisorType(vm.getDataCenterId(), vm.getHypervisorType());
|
||||
if (!candidates.isEmpty()) {
|
||||
hostId = candidates.get(0).getId();
|
||||
}
|
||||
}
|
||||
return hostId;
|
||||
}
|
||||
|
||||
@Override
|
||||
@ActionEvent(eventType = EventTypes.EVENT_TEMPLATE_DELETE, eventDescription = "Deleting Template", async = true)
|
||||
public boolean deleteTemplate(DeleteTemplateCmd cmd) {
|
||||
|
|
@ -2538,7 +2717,8 @@ public class TemplateManagerImpl extends ManagerBase implements TemplateManager,
|
|||
return new ConfigKey<?>[] {AllowPublicUserTemplates,
|
||||
TemplatePreloaderPoolSize,
|
||||
ValidateUrlIsResolvableBeforeRegisteringTemplate,
|
||||
TemplateDeleteFromPrimaryStorage};
|
||||
TemplateDeleteFromPrimaryStorage,
|
||||
VmIsoMaxCount};
|
||||
}
|
||||
|
||||
public List<TemplateAdapter> getTemplateAdapters() {
|
||||
|
|
|
|||
|
|
@ -16,10 +16,12 @@
|
|||
// under the License.
|
||||
package com.cloud.api.query.dao;
|
||||
|
||||
import static org.mockito.ArgumentMatchers.anyLong;
|
||||
import static org.mockito.ArgumentMatchers.nullable;
|
||||
import static org.mockito.MockitoAnnotations.openMocks;
|
||||
|
||||
import java.util.Arrays;
|
||||
import java.util.Collections;
|
||||
import java.util.EnumSet;
|
||||
|
||||
import com.cloud.storage.dao.VMTemplateDao;
|
||||
|
|
@ -49,9 +51,11 @@ import com.cloud.user.AccountManager;
|
|||
import com.cloud.user.UserStatisticsVO;
|
||||
import com.cloud.user.dao.UserDao;
|
||||
import com.cloud.user.dao.UserStatisticsDao;
|
||||
import com.cloud.host.dao.HostDetailsDao;
|
||||
import com.cloud.utils.db.SearchBuilder;
|
||||
import com.cloud.utils.db.SearchCriteria;
|
||||
import com.cloud.vm.dao.VMInstanceDetailsDao;
|
||||
import com.cloud.vm.dao.VmIsoMapDao;
|
||||
|
||||
@RunWith(MockitoJUnitRunner.class)
|
||||
public class UserVmJoinDaoImplTest extends GenericDaoBaseWithTagInformationBaseTest<UserVmJoinVO, UserVmResponse> {
|
||||
|
|
@ -83,6 +87,12 @@ public class UserVmJoinDaoImplTest extends GenericDaoBaseWithTagInformationBaseT
|
|||
@Mock
|
||||
private VMTemplateDao vmTemplateDao;
|
||||
|
||||
@Mock
|
||||
private VmIsoMapDao vmIsoMapDao;
|
||||
|
||||
@Mock
|
||||
private HostDetailsDao hostDetailsDao;
|
||||
|
||||
@Mock
|
||||
ExtensionHelper extensionHelper;
|
||||
|
||||
|
|
@ -103,6 +113,7 @@ public class UserVmJoinDaoImplTest extends GenericDaoBaseWithTagInformationBaseT
|
|||
@Before
|
||||
public void setup() {
|
||||
closeable = openMocks(this);
|
||||
Mockito.lenient().when(vmIsoMapDao.listByVmId(anyLong())).thenReturn(Collections.emptyList());
|
||||
prepareSetup();
|
||||
}
|
||||
|
||||
|
|
@ -166,4 +177,39 @@ public class UserVmJoinDaoImplTest extends GenericDaoBaseWithTagInformationBaseT
|
|||
Assert.assertEquals(2, response.getVnfNics().size());
|
||||
Assert.assertEquals(3, response.getVnfDetails().size());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void advertisedCdromCapReturnsDefaultWhenHostIdNull() {
|
||||
Assert.assertEquals(com.cloud.template.TemplateManager.DEFAULT_CDROM_MAX_PER_VM,
|
||||
_userVmJoinDaoImpl.advertisedCdromCap(null));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void advertisedCdromCapReturnsParsedValue() {
|
||||
com.cloud.host.DetailVO detail = Mockito.mock(com.cloud.host.DetailVO.class);
|
||||
Mockito.when(detail.getValue()).thenReturn("2");
|
||||
Mockito.when(hostDetailsDao.findDetail(7L, com.cloud.host.Host.HOST_CDROM_MAX_COUNT)).thenReturn(detail);
|
||||
Assert.assertEquals(2, _userVmJoinDaoImpl.advertisedCdromCap(7L));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void advertisedCdromCapFallsBackOnInvalidValue() {
|
||||
com.cloud.host.DetailVO detail = Mockito.mock(com.cloud.host.DetailVO.class);
|
||||
Mockito.when(detail.getValue()).thenReturn("xyz");
|
||||
Mockito.when(hostDetailsDao.findDetail(7L, com.cloud.host.Host.HOST_CDROM_MAX_COUNT)).thenReturn(detail);
|
||||
Assert.assertEquals(com.cloud.template.TemplateManager.DEFAULT_CDROM_MAX_PER_VM,
|
||||
_userVmJoinDaoImpl.advertisedCdromCap(7L));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void effectiveCdromMaxCountClampsToHypervisorCap() {
|
||||
UserVmJoinVO userVm = Mockito.mock(UserVmJoinVO.class);
|
||||
Mockito.when(userVm.getHostId()).thenReturn(7L);
|
||||
Mockito.when(userVm.getClusterId()).thenReturn(5L);
|
||||
com.cloud.host.DetailVO detail = Mockito.mock(com.cloud.host.DetailVO.class);
|
||||
Mockito.when(detail.getValue()).thenReturn("2");
|
||||
Mockito.when(hostDetailsDao.findDetail(7L, com.cloud.host.Host.HOST_CDROM_MAX_COUNT)).thenReturn(detail);
|
||||
// Configured cap defaults to 1 (no cluster override mocked); host advertises 2; clamps to 1.
|
||||
Assert.assertEquals(1, _userVmJoinDaoImpl.effectiveCdromMaxCount(userVm));
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -22,6 +22,7 @@ package com.cloud.template;
|
|||
import com.cloud.agent.AgentManager;
|
||||
import com.cloud.api.query.dao.SnapshotJoinDao;
|
||||
import com.cloud.api.query.dao.UserVmJoinDao;
|
||||
import com.cloud.api.query.vo.UserVmJoinVO;
|
||||
import com.cloud.dc.dao.DataCenterDao;
|
||||
import com.cloud.deployasis.dao.TemplateDeployAsIsDetailsDao;
|
||||
import com.cloud.domain.dao.DomainDao;
|
||||
|
|
@ -29,7 +30,11 @@ import com.cloud.event.dao.UsageEventDao;
|
|||
import com.cloud.exception.InvalidParameterValueException;
|
||||
import com.cloud.exception.ResourceAllocationException;
|
||||
import com.cloud.host.Status;
|
||||
import com.cloud.host.DetailVO;
|
||||
import com.cloud.host.Host;
|
||||
import com.cloud.host.HostVO;
|
||||
import com.cloud.host.dao.HostDao;
|
||||
import com.cloud.host.dao.HostDetailsDao;
|
||||
import com.cloud.hypervisor.Hypervisor;
|
||||
import com.cloud.hypervisor.HypervisorGuruManager;
|
||||
import com.cloud.projects.ProjectManager;
|
||||
|
|
@ -66,9 +71,15 @@ import com.cloud.user.UserVO;
|
|||
import com.cloud.user.dao.AccountDao;
|
||||
import com.cloud.utils.concurrency.NamedThreadFactory;
|
||||
import com.cloud.utils.exception.CloudRuntimeException;
|
||||
import com.cloud.uservm.UserVm;
|
||||
import com.cloud.vm.UserVmVO;
|
||||
import com.cloud.vm.VMInstanceVO;
|
||||
import com.cloud.vm.VirtualMachine;
|
||||
import com.cloud.vm.VirtualMachine.State;
|
||||
import com.cloud.vm.VmIsoMapVO;
|
||||
import com.cloud.vm.dao.UserVmDao;
|
||||
import com.cloud.vm.dao.VMInstanceDao;
|
||||
import com.cloud.vm.dao.VmIsoMapDao;
|
||||
|
||||
import junit.framework.TestCase;
|
||||
|
||||
|
|
@ -133,6 +144,7 @@ import org.springframework.core.type.filter.TypeFilter;
|
|||
|
||||
import java.io.IOException;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Arrays;
|
||||
import java.util.List;
|
||||
import java.util.UUID;
|
||||
import java.util.concurrent.BlockingQueue;
|
||||
|
|
@ -220,6 +232,21 @@ public class TemplateManagerImplTest extends TestCase {
|
|||
@Mock
|
||||
HeuristicRuleHelper heuristicRuleHelperMock;
|
||||
|
||||
@Mock
|
||||
UserVmDao _userVmDao;
|
||||
|
||||
@Mock
|
||||
VmIsoMapDao _vmIsoMapDao;
|
||||
|
||||
@Mock
|
||||
HostDao _hostDao;
|
||||
|
||||
@Mock
|
||||
HostDetailsDao _hostDetailsDao;
|
||||
|
||||
@Mock
|
||||
UserVmJoinDao _userVmJoinDao;
|
||||
|
||||
public class CustomThreadPoolExecutor extends ThreadPoolExecutor {
|
||||
AtomicInteger ai = new AtomicInteger(0);
|
||||
public CustomThreadPoolExecutor(int corePoolSize, int maximumPoolSize, long keepAliveTime, TimeUnit unit,
|
||||
|
|
@ -750,6 +777,222 @@ public class TemplateManagerImplTest extends TestCase {
|
|||
Mockito.verify(heuristicRuleHelperMock, Mockito.times(1)).getImageStoreIfThereIsHeuristicRule(1L, HeuristicType.TEMPLATE, vmTemplateVOMock);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void highestCdromMapEntryReturnsNullWhenMapIsEmpty() {
|
||||
Mockito.when(_vmIsoMapDao.listByVmId(1L)).thenReturn(new ArrayList<>());
|
||||
Assert.assertNull(templateManager.highestCdromMapEntry(1L));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void highestCdromMapEntryReturnsEntryWithMaxDeviceSeq() {
|
||||
VmIsoMapVO low = new VmIsoMapVO(1L, 100L, 4);
|
||||
VmIsoMapVO high = new VmIsoMapVO(1L, 200L, 5);
|
||||
Mockito.when(_vmIsoMapDao.listByVmId(1L)).thenReturn(Arrays.asList(low, high));
|
||||
VmIsoMapVO result = templateManager.highestCdromMapEntry(1L);
|
||||
Assert.assertNotNull(result);
|
||||
Assert.assertEquals(5, result.getDeviceSeq());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void attachISOToVMAttachWritesToIsoIdWhenPrimarySlotEmpty() {
|
||||
UserVmVO vm = Mockito.mock(UserVmVO.class);
|
||||
VMTemplateVO iso = Mockito.mock(VMTemplateVO.class);
|
||||
Mockito.when(_userVmDao.findById(1L)).thenReturn(vm);
|
||||
Mockito.when(vmTemplateDao.findById(42L)).thenReturn(iso);
|
||||
Mockito.when(iso.getId()).thenReturn(42L);
|
||||
Mockito.when(vm.getIsoId()).thenReturn(null);
|
||||
|
||||
boolean result = templateManager.attachISOToVM(1L, 1L, 42L, true, false, false);
|
||||
|
||||
Assert.assertTrue(result);
|
||||
Mockito.verify(vm).setIsoId(42L);
|
||||
Mockito.verify(_userVmDao).update(eq(1L), eq(vm));
|
||||
Mockito.verify(_vmIsoMapDao, Mockito.never()).persist(any(VmIsoMapVO.class));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void resolveIsoIdForDetachReturnsPrimaryWhenOnlyPrimaryIsAttached() {
|
||||
Long resolved = templateManager.resolveIsoIdForDetach(99L, new ArrayList<>(), null);
|
||||
Assert.assertEquals(Long.valueOf(99L), resolved);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void resolveIsoIdForDetachReturnsMapEntryWhenOnlyMapHasOne() {
|
||||
VmIsoMapVO row = new VmIsoMapVO(1L, 100L, 4);
|
||||
Long resolved = templateManager.resolveIsoIdForDetach(null, Arrays.asList(row), null);
|
||||
Assert.assertEquals(Long.valueOf(100L), resolved);
|
||||
}
|
||||
|
||||
@Test(expected = InvalidParameterValueException.class)
|
||||
public void resolveIsoIdForDetachThrowsWhenMultipleAttachedAndNoIdGiven() {
|
||||
VmIsoMapVO row = new VmIsoMapVO(1L, 100L, 4);
|
||||
templateManager.resolveIsoIdForDetach(99L, Arrays.asList(row), null);
|
||||
}
|
||||
|
||||
@Test(expected = InvalidParameterValueException.class)
|
||||
public void resolveIsoIdForDetachThrowsWhenNothingAttached() {
|
||||
templateManager.resolveIsoIdForDetach(null, new ArrayList<>(), null);
|
||||
}
|
||||
|
||||
@Test(expected = InvalidParameterValueException.class)
|
||||
public void resolveIsoIdForDetachThrowsWhenIdNotAttached() {
|
||||
templateManager.resolveIsoIdForDetach(99L, new ArrayList<>(), 42L);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void isIsoAlreadyAttachedReturnsTrueWhenPrimaryMatches() {
|
||||
Assert.assertTrue(templateManager.isIsoAlreadyAttached(1L, 42L, 42L));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void isIsoAlreadyAttachedReturnsTrueWhenInMap() {
|
||||
Mockito.when(_vmIsoMapDao.findByVmIdIsoId(1L, 42L)).thenReturn(new VmIsoMapVO(1L, 42L, 4));
|
||||
Assert.assertTrue(templateManager.isIsoAlreadyAttached(1L, 99L, 42L));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void isIsoAlreadyAttachedReturnsFalseWhenNotAttached() {
|
||||
Mockito.when(_vmIsoMapDao.findByVmIdIsoId(1L, 42L)).thenReturn(null);
|
||||
Assert.assertFalse(templateManager.isIsoAlreadyAttached(1L, null, 42L));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void attachISOToVMAttachWritesToVmIsoMapWhenPrimarySlotOccupied() {
|
||||
UserVmVO vm = Mockito.mock(UserVmVO.class);
|
||||
VMTemplateVO iso = Mockito.mock(VMTemplateVO.class);
|
||||
Mockito.when(_userVmDao.findById(1L)).thenReturn(vm);
|
||||
Mockito.when(vmTemplateDao.findById(42L)).thenReturn(iso);
|
||||
Mockito.when(iso.getId()).thenReturn(42L);
|
||||
Mockito.when(vm.getIsoId()).thenReturn(99L);
|
||||
Mockito.when(_vmIsoMapDao.listByVmId(1L)).thenReturn(new ArrayList<>());
|
||||
|
||||
boolean result = templateManager.attachISOToVM(1L, 1L, 42L, true, false, false);
|
||||
|
||||
Assert.assertTrue(result);
|
||||
Mockito.verify(_vmIsoMapDao).persist(Mockito.argThat(row ->
|
||||
row.getVmId() == 1L && row.getIsoId() == 42L
|
||||
&& row.getDeviceSeq() == TemplateManager.CDROM_PRIMARY_DEVICE_SEQ + 1));
|
||||
Mockito.verify(vm, Mockito.never()).setIsoId(anyLong());
|
||||
}
|
||||
|
||||
@Test(expected = InvalidParameterValueException.class)
|
||||
public void enforceCdromAttachLimitsThrowsWhenIsoAlreadyAttachedAtPrimary() {
|
||||
UserVm vm = Mockito.mock(UserVm.class);
|
||||
Mockito.when(vm.getIsoId()).thenReturn(42L);
|
||||
templateManager.enforceCdromAttachLimits(1L, vm, 42L);
|
||||
}
|
||||
|
||||
@Test(expected = InvalidParameterValueException.class)
|
||||
public void enforceCdromAttachLimitsThrowsWhenIsoAlreadyAttachedInMap() {
|
||||
UserVm vm = Mockito.mock(UserVm.class);
|
||||
Mockito.when(vm.getIsoId()).thenReturn(99L);
|
||||
Mockito.when(_vmIsoMapDao.findByVmIdIsoId(1L, 42L)).thenReturn(new VmIsoMapVO(1L, 42L, 4));
|
||||
templateManager.enforceCdromAttachLimits(1L, vm, 42L);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void advertisedCdromCapReturnsDefaultWhenHostIdNull() {
|
||||
Assert.assertEquals(TemplateManager.DEFAULT_CDROM_MAX_PER_VM, templateManager.advertisedCdromCap(null));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void advertisedCdromCapReturnsDefaultWhenDetailMissing() {
|
||||
Mockito.when(_hostDetailsDao.findDetail(7L, Host.HOST_CDROM_MAX_COUNT)).thenReturn(null);
|
||||
Assert.assertEquals(TemplateManager.DEFAULT_CDROM_MAX_PER_VM, templateManager.advertisedCdromCap(7L));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void advertisedCdromCapReturnsParsedValue() {
|
||||
DetailVO detail = Mockito.mock(DetailVO.class);
|
||||
Mockito.when(detail.getValue()).thenReturn("3");
|
||||
Mockito.when(_hostDetailsDao.findDetail(7L, Host.HOST_CDROM_MAX_COUNT)).thenReturn(detail);
|
||||
Assert.assertEquals(3, templateManager.advertisedCdromCap(7L));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void advertisedCdromCapFallsBackOnInvalidValue() {
|
||||
DetailVO detail = Mockito.mock(DetailVO.class);
|
||||
Mockito.when(detail.getValue()).thenReturn("not-a-number");
|
||||
Mockito.when(_hostDetailsDao.findDetail(7L, Host.HOST_CDROM_MAX_COUNT)).thenReturn(detail);
|
||||
Assert.assertEquals(TemplateManager.DEFAULT_CDROM_MAX_PER_VM, templateManager.advertisedCdromCap(7L));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void hostIdForVmReturnsCurrentHost() {
|
||||
VirtualMachine vm = Mockito.mock(VirtualMachine.class);
|
||||
Mockito.when(vm.getHostId()).thenReturn(42L);
|
||||
Assert.assertEquals(Long.valueOf(42L), templateManager.hostIdForVm(vm));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void hostIdForVmFallsBackToLastHost() {
|
||||
VirtualMachine vm = Mockito.mock(VirtualMachine.class);
|
||||
Mockito.when(vm.getHostId()).thenReturn(null);
|
||||
Mockito.when(vm.getLastHostId()).thenReturn(99L);
|
||||
Assert.assertEquals(Long.valueOf(99L), templateManager.hostIdForVm(vm));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void hostIdForVmReturnsNullWhenNoHost() {
|
||||
VirtualMachine vm = Mockito.mock(VirtualMachine.class);
|
||||
Mockito.when(vm.getHostId()).thenReturn(null);
|
||||
Mockito.when(vm.getLastHostId()).thenReturn(null);
|
||||
Assert.assertNull(templateManager.hostIdForVm(vm));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void effectiveMaxCdromsReturnsConfiguredCapWhenWithinHypervisorCap() {
|
||||
VirtualMachine vm = Mockito.mock(VirtualMachine.class);
|
||||
DetailVO detail = Mockito.mock(DetailVO.class);
|
||||
Mockito.when(detail.getValue()).thenReturn("2");
|
||||
HostVO host = Mockito.mock(HostVO.class);
|
||||
Mockito.when(host.getClusterId()).thenReturn(5L);
|
||||
Mockito.when(_hostDao.findById(7L)).thenReturn(host);
|
||||
Mockito.when(_hostDetailsDao.findDetail(7L, Host.HOST_CDROM_MAX_COUNT)).thenReturn(detail);
|
||||
// Configured cap defaults to 1 (no cluster override mocked); hypervisor cap is 2; 1 <= 2 → no throw, returns 1.
|
||||
Assert.assertEquals(1, templateManager.effectiveMaxCdroms(vm, 7L));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void templateIsDeleteableReturnsTrueWhenNoVmsUseIso() {
|
||||
Mockito.when(_userVmJoinDao.listActiveByIsoId(42L)).thenReturn(new ArrayList<>());
|
||||
Mockito.when(_vmIsoMapDao.listByIsoId(42L)).thenReturn(new ArrayList<>());
|
||||
Assert.assertTrue(templateManager.templateIsDeleteable(42L));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void templateIsDeleteableReturnsFalseWhenPrimarySlotInUse() {
|
||||
Mockito.when(_userVmJoinDao.listActiveByIsoId(42L))
|
||||
.thenReturn(java.util.Collections.singletonList(Mockito.mock(UserVmJoinVO.class)));
|
||||
Assert.assertFalse(templateManager.templateIsDeleteable(42L));
|
||||
// Should not even need to consult vm_iso_map once primary slot in use.
|
||||
Mockito.verify(_vmIsoMapDao, Mockito.never()).listByIsoId(anyLong());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void templateIsDeleteableReturnsFalseWhenAttachedViaVmIsoMapToActiveVm() {
|
||||
Mockito.when(_userVmJoinDao.listActiveByIsoId(42L)).thenReturn(new ArrayList<>());
|
||||
Mockito.when(_vmIsoMapDao.listByIsoId(42L))
|
||||
.thenReturn(java.util.Collections.singletonList(new VmIsoMapVO(1L, 42L, 4)));
|
||||
UserVmVO vm = Mockito.mock(UserVmVO.class);
|
||||
Mockito.when(vm.getState()).thenReturn(State.Running);
|
||||
Mockito.when(vm.getUuid()).thenReturn("uuid-1");
|
||||
Mockito.when(_userVmDao.findById(1L)).thenReturn(vm);
|
||||
Assert.assertFalse(templateManager.templateIsDeleteable(42L));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void templateIsDeleteableIgnoresVmIsoMapForDestroyedVm() {
|
||||
Mockito.when(_userVmJoinDao.listActiveByIsoId(42L)).thenReturn(new ArrayList<>());
|
||||
Mockito.when(_vmIsoMapDao.listByIsoId(42L))
|
||||
.thenReturn(java.util.Collections.singletonList(new VmIsoMapVO(1L, 42L, 4)));
|
||||
UserVmVO vm = Mockito.mock(UserVmVO.class);
|
||||
Mockito.when(vm.getState()).thenReturn(State.Expunging);
|
||||
Mockito.when(_userVmDao.findById(1L)).thenReturn(vm);
|
||||
Assert.assertTrue(templateManager.templateIsDeleteable(42L));
|
||||
}
|
||||
|
||||
|
||||
@Configuration
|
||||
@ComponentScan(basePackageClasses = {TemplateManagerImpl.class},
|
||||
includeFilters = {@ComponentScan.Filter(value = TestConfiguration.Library.class, type = FilterType.CUSTOM)},
|
||||
|
|
|
|||
|
|
@ -22,6 +22,15 @@ import { getAPI, postAPI, getBaseUrl } from '@/api'
|
|||
import { getLatestKubernetesIsoParams } from '@/utils/acsrepo'
|
||||
import kubernetesIcon from '@/assets/icons/kubernetes.svg?inline'
|
||||
|
||||
const attachedIsoCount = (record) => (record.isos && record.isos.length) || (record.isoid ? 1 : 0)
|
||||
// Server pre-computes the effective cap (cluster-scoped vm.iso.max.count clamped to the
|
||||
// hypervisor's own limit). Fall back to the hypervisor floor for older servers.
|
||||
const isoMaxCount = (record) => record.isomaxcount != null
|
||||
? record.isomaxcount
|
||||
: (record.hypervisor === 'KVM' ? 2 : 1)
|
||||
const isoActionAvailable = (record) =>
|
||||
record.hypervisor !== 'External' && ['Running', 'Stopped'].includes(record.state) && record.vmtype !== 'sharedfsvm'
|
||||
|
||||
export default {
|
||||
name: 'compute',
|
||||
title: 'label.compute',
|
||||
|
|
@ -299,7 +308,7 @@ export default {
|
|||
docHelp: 'adminguide/templates.html#attaching-an-iso-to-a-vm',
|
||||
dataView: true,
|
||||
popup: true,
|
||||
show: (record) => { return record.hypervisor !== 'External' && ['Running', 'Stopped'].includes(record.state) && !record.isoid && record.vmtype !== 'sharedfsvm' },
|
||||
show: (record) => isoActionAvailable(record) && attachedIsoCount(record) < isoMaxCount(record),
|
||||
disabled: (record) => { return record.hostcontrolstate === 'Offline' || record.hostcontrolstate === 'Maintenance' },
|
||||
component: shallowRef(defineAsyncComponent(() => import('@/views/compute/AttachIso.vue')))
|
||||
},
|
||||
|
|
@ -307,22 +316,11 @@ export default {
|
|||
api: 'detachIso',
|
||||
icon: 'link-outlined',
|
||||
label: 'label.action.detach.iso',
|
||||
message: 'message.detach.iso.confirm',
|
||||
dataView: true,
|
||||
args: (record, store) => {
|
||||
var args = ['virtualmachineid']
|
||||
if (record && record.hypervisor && record.hypervisor === 'VMware') {
|
||||
args.push('forced')
|
||||
}
|
||||
return args
|
||||
},
|
||||
show: (record) => { return record.hypervisor !== 'External' && ['Running', 'Stopped'].includes(record.state) && 'isoid' in record && record.isoid && record.vmtype !== 'sharedfsvm' },
|
||||
popup: true,
|
||||
show: (record) => isoActionAvailable(record) && attachedIsoCount(record) > 0,
|
||||
disabled: (record) => { return record.hostcontrolstate === 'Offline' || record.hostcontrolstate === 'Maintenance' },
|
||||
mapping: {
|
||||
virtualmachineid: {
|
||||
value: (record, params) => { return record.id }
|
||||
}
|
||||
}
|
||||
component: shallowRef(defineAsyncComponent(() => import('@/views/compute/DetachIso.vue')))
|
||||
},
|
||||
{
|
||||
api: 'updateVMAffinityGroup',
|
||||
|
|
|
|||
|
|
@ -17,23 +17,38 @@
|
|||
<template>
|
||||
<div class="form-layout" v-ctrl-enter="handleSubmit">
|
||||
<a-spin :spinning="loading">
|
||||
<a-alert
|
||||
v-if="!loading && maxSelections === 0"
|
||||
type="warning"
|
||||
showIcon
|
||||
:message="$t('label.iso.name') + ': max reached'"
|
||||
style="margin-bottom: 12px;" />
|
||||
<a-form
|
||||
:ref="formRef"
|
||||
:model="form"
|
||||
:rules="rules"
|
||||
layout="vertical"
|
||||
@finish="handleSubmit">
|
||||
<a-form-item :label="$t('label.iso.name')" ref="id" name="id">
|
||||
<a-form-item
|
||||
:label="$t('label.iso.name') + ' (' + form.ids.length + ' / ' + maxSelections + ')'"
|
||||
ref="ids"
|
||||
name="ids">
|
||||
<a-select
|
||||
mode="multiple"
|
||||
:loading="loading"
|
||||
v-model:value="form.id"
|
||||
v-model:value="form.ids"
|
||||
v-focus="true"
|
||||
:disabled="maxSelections === 0"
|
||||
showSearch
|
||||
optionFilterProp="label"
|
||||
:filterOption="(input, option) => {
|
||||
return option.label.toLowerCase().indexOf(input.toLowerCase()) >= 0
|
||||
}">
|
||||
<a-select-option v-for="iso in isos" :key="iso.id" :label="iso.displaytext || iso.name">
|
||||
<a-select-option
|
||||
v-for="iso in isos"
|
||||
:key="iso.id"
|
||||
:label="iso.displaytext || iso.name"
|
||||
:disabled="form.ids.length >= maxSelections && !form.ids.includes(iso.id)">
|
||||
{{ iso.displaytext || iso.name }}
|
||||
</a-select-option>
|
||||
</a-select>
|
||||
|
|
@ -69,19 +84,44 @@ export default {
|
|||
data () {
|
||||
return {
|
||||
loading: false,
|
||||
isos: []
|
||||
isos: [],
|
||||
maxSelections: 1
|
||||
}
|
||||
},
|
||||
created () {
|
||||
this.initForm()
|
||||
this.computeMaxSelections()
|
||||
this.fetchData()
|
||||
},
|
||||
watch: {
|
||||
'form.ids' (newVal) {
|
||||
if (newVal && newVal.length > this.maxSelections) {
|
||||
this.form.ids = newVal.slice(0, this.maxSelections)
|
||||
this.$message.warning(this.$t('label.iso.name') + ': max ' + this.maxSelections)
|
||||
}
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
computeMaxSelections () {
|
||||
// Server pre-computes the effective cap (cluster-scoped vm.iso.max.count clamped to
|
||||
// the hypervisor's own limit) and exposes it on the VM as isomaxcount.
|
||||
const effectiveCap = this.resource.isomaxcount != null
|
||||
? this.resource.isomaxcount
|
||||
: (this.resource.hypervisor === 'KVM' ? 2 : 1)
|
||||
const alreadyAttached = (this.resource.isos && this.resource.isos.length) ||
|
||||
(this.resource.isoid ? 1 : 0)
|
||||
this.maxSelections = Math.max(0, effectiveCap - alreadyAttached)
|
||||
},
|
||||
initForm () {
|
||||
this.formRef = ref()
|
||||
this.form = reactive({})
|
||||
this.form = reactive({ ids: [] })
|
||||
this.rules = reactive({
|
||||
id: [{ required: true, message: `${this.$t('label.required')}` }]
|
||||
ids: [{
|
||||
required: true,
|
||||
type: 'array',
|
||||
min: 1,
|
||||
message: `${this.$t('label.required')}`
|
||||
}]
|
||||
})
|
||||
},
|
||||
fetchData () {
|
||||
|
|
@ -93,9 +133,6 @@ export default {
|
|||
})
|
||||
Promise.all(promises).then(() => {
|
||||
this.isos = _.uniqBy(this.isos, 'id')
|
||||
if (this.isos.length > 0) {
|
||||
this.form.id = this.isos[0].id
|
||||
}
|
||||
}).catch((error) => {
|
||||
console.log(error)
|
||||
}).finally(() => {
|
||||
|
|
@ -127,35 +164,42 @@ export default {
|
|||
if (this.loading) return
|
||||
this.formRef.value.validate().then(() => {
|
||||
const values = toRaw(this.form)
|
||||
const params = {
|
||||
id: values.id,
|
||||
virtualmachineid: this.resource.id
|
||||
}
|
||||
|
||||
if (values.forced) {
|
||||
params.forced = values.forced
|
||||
}
|
||||
const ids = values.ids || []
|
||||
if (ids.length === 0) return
|
||||
|
||||
this.loading = true
|
||||
const title = this.$t('label.action.attach.iso')
|
||||
postAPI('attachIso', params).then(json => {
|
||||
const jobId = json.attachisoresponse.jobid
|
||||
if (jobId) {
|
||||
this.$pollJob({
|
||||
jobId,
|
||||
title,
|
||||
description: values.id,
|
||||
successMessage: `${this.$t('label.action.attach.iso')} ${this.$t('label.success')}`,
|
||||
loadingMessage: `${title} ${this.$t('label.in.progress')}`,
|
||||
catchMessage: this.$t('error.fetching.async.job.result')
|
||||
})
|
||||
// attachIso is single-ISO server-side; fan out one call per selection.
|
||||
const sendOne = (isoId) => {
|
||||
const params = {
|
||||
id: isoId,
|
||||
virtualmachineid: this.resource.id
|
||||
}
|
||||
this.closeAction()
|
||||
}).catch(error => {
|
||||
this.$notifyError(error)
|
||||
}).finally(() => {
|
||||
this.loading = false
|
||||
})
|
||||
if (values.forced) {
|
||||
params.forced = values.forced
|
||||
}
|
||||
return new Promise((resolve, reject) => {
|
||||
postAPI('attachIso', params).then(json => {
|
||||
const jobId = json.attachisoresponse && json.attachisoresponse.jobid
|
||||
if (jobId) {
|
||||
this.$pollJob({
|
||||
jobId,
|
||||
title,
|
||||
description: isoId,
|
||||
successMessage: `${this.$t('label.action.attach.iso')} ${this.$t('label.success')}`,
|
||||
loadingMessage: `${title} ${this.$t('label.in.progress')}`,
|
||||
catchMessage: this.$t('error.fetching.async.job.result')
|
||||
})
|
||||
}
|
||||
resolve()
|
||||
}).catch(reject)
|
||||
})
|
||||
}
|
||||
|
||||
ids.reduce((p, id) => p.then(() => sendOne(id)), Promise.resolve())
|
||||
.then(() => { this.closeAction() })
|
||||
.catch(error => { this.$notifyError(error) })
|
||||
.finally(() => { this.loading = false })
|
||||
}).catch(error => {
|
||||
this.formRef.value.scrollToField(error.errorFields[0].name)
|
||||
})
|
||||
|
|
|
|||
|
|
@ -0,0 +1,178 @@
|
|||
// Licensed to the Apache Software Foundation (ASF) under one
|
||||
// or more contributor license agreements. See the NOTICE file
|
||||
// distributed with this work for additional information
|
||||
// regarding copyright ownership. The ASF licenses this file
|
||||
// to you under the Apache License, Version 2.0 (the
|
||||
// "License"); you may not use this file except in compliance
|
||||
// with the License. You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing,
|
||||
// software distributed under the License is distributed on an
|
||||
// "AS IS" BASIS, WITHOUT 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 class="form-layout" v-ctrl-enter="handleSubmit">
|
||||
<a-spin :spinning="loading">
|
||||
<a-form
|
||||
:ref="formRef"
|
||||
:model="form"
|
||||
:rules="rules"
|
||||
layout="vertical"
|
||||
@finish="handleSubmit">
|
||||
<a-form-item
|
||||
:label="$t('label.iso.name') + ' (' + form.ids.length + ' / ' + attached.length + ')'"
|
||||
ref="ids"
|
||||
name="ids">
|
||||
<a-select
|
||||
mode="multiple"
|
||||
:loading="loading"
|
||||
v-model:value="form.ids"
|
||||
v-focus="true">
|
||||
<a-select-option
|
||||
v-for="iso in attached"
|
||||
:key="iso.id"
|
||||
:label="iso.displaytext || iso.name">
|
||||
{{ (iso.displaytext || iso.name) + ' (' + slotLabel(iso.deviceseq) + ')' }}
|
||||
</a-select-option>
|
||||
</a-select>
|
||||
</a-form-item>
|
||||
<a-form-item
|
||||
:label="$t('label.forced')"
|
||||
v-if="resource && resource.hypervisor === 'VMware'"
|
||||
ref="forced"
|
||||
name="forced">
|
||||
<a-switch v-model:checked="form.forced" v-focus="true" />
|
||||
</a-form-item>
|
||||
</a-form>
|
||||
<div :span="24" class="action-button">
|
||||
<a-button @click="closeAction">{{ $t('label.cancel') }}</a-button>
|
||||
<a-button :loading="loading" type="primary" @click="handleSubmit" ref="submit">{{ $t('label.ok') }}</a-button>
|
||||
</div>
|
||||
</a-spin>
|
||||
</div>
|
||||
</template>
|
||||
<script>
|
||||
import { ref, reactive, toRaw } from 'vue'
|
||||
import { postAPI } from '@/api'
|
||||
|
||||
export default {
|
||||
name: 'DetachIso',
|
||||
props: {
|
||||
resource: {
|
||||
type: Object,
|
||||
required: true
|
||||
}
|
||||
},
|
||||
data () {
|
||||
return {
|
||||
loading: false,
|
||||
attached: []
|
||||
}
|
||||
},
|
||||
created () {
|
||||
this.initForm()
|
||||
this.populateAttached()
|
||||
},
|
||||
methods: {
|
||||
initForm () {
|
||||
this.formRef = ref()
|
||||
this.form = reactive({ ids: [] })
|
||||
this.rules = reactive({
|
||||
ids: [{
|
||||
required: true,
|
||||
type: 'array',
|
||||
min: 1,
|
||||
message: `${this.$t('label.required')}`
|
||||
}]
|
||||
})
|
||||
},
|
||||
populateAttached () {
|
||||
if (this.resource.isos && this.resource.isos.length > 0) {
|
||||
this.attached = [...this.resource.isos].sort((a, b) => (a.deviceseq || 0) - (b.deviceseq || 0))
|
||||
} else if (this.resource.isoid) {
|
||||
this.attached = [{
|
||||
id: this.resource.isoid,
|
||||
name: this.resource.isoname,
|
||||
displaytext: this.resource.isodisplaytext,
|
||||
deviceseq: 3
|
||||
}]
|
||||
}
|
||||
if (this.attached.length === 1) {
|
||||
this.form.ids = [this.attached[0].id]
|
||||
}
|
||||
},
|
||||
slotLabel (deviceseq) {
|
||||
// 3 -> hdc, 4 -> hdd, ... matches LibvirtVMDef.getDevLabel for the IDE bus on KVM.
|
||||
if (typeof deviceseq !== 'number') return ''
|
||||
return 'hd' + String.fromCharCode('a'.charCodeAt(0) + deviceseq - 1)
|
||||
},
|
||||
closeAction () {
|
||||
this.$emit('close-action')
|
||||
},
|
||||
handleSubmit (e) {
|
||||
e.preventDefault()
|
||||
if (this.loading) return
|
||||
this.formRef.value.validate().then(() => {
|
||||
const values = toRaw(this.form)
|
||||
const ids = values.ids || []
|
||||
if (ids.length === 0) return
|
||||
|
||||
this.loading = true
|
||||
const title = this.$t('label.action.detach.iso')
|
||||
// detachIso is single-ISO server-side; fan out one call per selection.
|
||||
const sendOne = (isoId) => {
|
||||
const params = {
|
||||
virtualmachineid: this.resource.id
|
||||
}
|
||||
// Single-attached: omit id so older servers (without the id parameter) still accept the call.
|
||||
if (this.attached.length > 1 || ids.length > 1) {
|
||||
params.id = isoId
|
||||
}
|
||||
if (values.forced) {
|
||||
params.forced = values.forced
|
||||
}
|
||||
return new Promise((resolve, reject) => {
|
||||
postAPI('detachIso', params).then(json => {
|
||||
const jobId = json.detachisoresponse && json.detachisoresponse.jobid
|
||||
if (jobId) {
|
||||
this.$pollJob({
|
||||
jobId,
|
||||
title,
|
||||
description: isoId,
|
||||
successMessage: `${this.$t('label.action.detach.iso')} ${this.$t('label.success')}`,
|
||||
loadingMessage: `${title} ${this.$t('label.in.progress')}`,
|
||||
catchMessage: this.$t('error.fetching.async.job.result')
|
||||
})
|
||||
}
|
||||
resolve()
|
||||
}).catch(reject)
|
||||
})
|
||||
}
|
||||
|
||||
ids.reduce((p, id) => p.then(() => sendOne(id)), Promise.resolve())
|
||||
.then(() => { this.closeAction() })
|
||||
.catch(error => { this.$notifyError(error) })
|
||||
.finally(() => { this.loading = false })
|
||||
}).catch(error => {
|
||||
this.formRef.value.scrollToField(error.errorFields[0].name)
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
<style lang="scss" scoped>
|
||||
.form-layout {
|
||||
width: 80vw;
|
||||
@media (min-width: 700px) {
|
||||
width: 600px;
|
||||
}
|
||||
}
|
||||
|
||||
.form {
|
||||
margin: 10px 0;
|
||||
}
|
||||
</style>
|
||||
|
|
@ -28,10 +28,15 @@
|
|||
<a-tab-pane :tab="$t('label.metrics')" key="stats">
|
||||
<StatsTab :resource="resource"/>
|
||||
</a-tab-pane>
|
||||
<a-tab-pane :tab="$t('label.iso')" key="cdrom" v-if="vm.isoid">
|
||||
<usb-outlined />
|
||||
<router-link :to="{ path: '/iso/' + vm.isoid }">{{ vm.isoname }}</router-link> <br/>
|
||||
<barcode-outlined /> {{ vm.isoid }}
|
||||
<a-tab-pane :tab="$t('label.iso')" key="cdrom" v-if="attachedIsos.length > 0">
|
||||
<div v-for="iso in attachedIsos" :key="iso.id" style="margin-bottom: 12px;">
|
||||
<usb-outlined />
|
||||
<router-link :to="{ path: '/iso/' + iso.id }">{{ iso.displaytext || iso.name }}</router-link>
|
||||
<a-tag style="margin-left: 8px;">{{ slotLabel(iso.deviceseq) }}</a-tag>
|
||||
<a-tag v-if="iso.bootable" color="blue" style="margin-left: 4px;">{{ $t('label.bootable') }}</a-tag>
|
||||
<br/>
|
||||
<barcode-outlined /> {{ iso.id }}
|
||||
</div>
|
||||
</a-tab-pane>
|
||||
<a-tab-pane :tab="$t('label.volumes')" key="volumes" v-if="'listVolumes' in $store.getters.apis">
|
||||
<a-button
|
||||
|
|
@ -226,7 +231,28 @@ export default {
|
|||
mounted () {
|
||||
this.setCurrentTab()
|
||||
},
|
||||
computed: {
|
||||
attachedIsos () {
|
||||
if (this.vm.isos && this.vm.isos.length > 0) {
|
||||
return [...this.vm.isos].sort((a, b) => (a.deviceseq || 0) - (b.deviceseq || 0))
|
||||
}
|
||||
if (this.vm.isoid) {
|
||||
return [{
|
||||
id: this.vm.isoid,
|
||||
name: this.vm.isoname,
|
||||
displaytext: this.vm.isodisplaytext,
|
||||
deviceseq: 3
|
||||
}]
|
||||
}
|
||||
return []
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
slotLabel (deviceseq) {
|
||||
// 3 -> hdc, 4 -> hdd, ... matches LibvirtVMDef.getDevLabel for the IDE bus on KVM.
|
||||
if (typeof deviceseq !== 'number') return ''
|
||||
return 'hd' + String.fromCharCode('a'.charCodeAt(0) + deviceseq - 1)
|
||||
},
|
||||
setCurrentTab () {
|
||||
this.currentTab = this.$route.query.tab ? this.$route.query.tab : 'details'
|
||||
},
|
||||
|
|
|
|||
|
|
@ -295,13 +295,14 @@ export default {
|
|||
params[this.scopeKey] = this.resource?.id
|
||||
}
|
||||
postAPI('updateConfiguration', params).then(json => {
|
||||
configRecordEntry = json.updateconfigurationresponse.configuration
|
||||
const apiRecord = json.updateconfigurationresponse.configuration
|
||||
configRecordEntry = { ...apiRecord, value: String(newValue) }
|
||||
this.editableValue = this.getEditableValue(configRecordEntry)
|
||||
this.actualValue = this.editableValue
|
||||
this.$emit('change-config', { value: newValue })
|
||||
this.$store.dispatch('RefreshFeatures')
|
||||
this.$messageConfigSuccess(`${this.$t('message.setting.updated')} ${configrecord.name}`, configrecord)
|
||||
this.$notifyConfigurationValueChange(json?.updateconfigurationresponse?.configuration || null)
|
||||
this.$notifyConfigurationValueChange(configRecordEntry)
|
||||
}).catch(error => {
|
||||
this.editableValue = this.actualValue
|
||||
console.error(error)
|
||||
|
|
|
|||
Loading…
Reference in New Issue