Skip to content

Commit 3a28a87

Browse files
committed
Merge branch '4.20' of https://github.com/apache/cloudstack
2 parents 48f890a + 88916dc commit 3a28a87

File tree

10 files changed

+191
-26
lines changed

10 files changed

+191
-26
lines changed

api/src/main/java/com/cloud/storage/MigrationOptions.java

+9-2
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@ public class MigrationOptions implements Serializable {
2424

2525
private String srcPoolUuid;
2626
private Storage.StoragePoolType srcPoolType;
27+
private Long srcPoolClusterId;
2728
private Type type;
2829
private ScopeType scopeType;
2930
private String srcBackingFilePath;
@@ -38,21 +39,23 @@ public enum Type {
3839
public MigrationOptions() {
3940
}
4041

41-
public MigrationOptions(String srcPoolUuid, Storage.StoragePoolType srcPoolType, String srcBackingFilePath, boolean copySrcTemplate, ScopeType scopeType) {
42+
public MigrationOptions(String srcPoolUuid, Storage.StoragePoolType srcPoolType, String srcBackingFilePath, boolean copySrcTemplate, ScopeType scopeType, Long srcPoolClusterId) {
4243
this.srcPoolUuid = srcPoolUuid;
4344
this.srcPoolType = srcPoolType;
4445
this.type = Type.LinkedClone;
4546
this.scopeType = scopeType;
4647
this.srcBackingFilePath = srcBackingFilePath;
4748
this.copySrcTemplate = copySrcTemplate;
49+
this.srcPoolClusterId = srcPoolClusterId;
4850
}
4951

50-
public MigrationOptions(String srcPoolUuid, Storage.StoragePoolType srcPoolType, String srcVolumeUuid, ScopeType scopeType) {
52+
public MigrationOptions(String srcPoolUuid, Storage.StoragePoolType srcPoolType, String srcVolumeUuid, ScopeType scopeType, Long srcPoolClusterId) {
5153
this.srcPoolUuid = srcPoolUuid;
5254
this.srcPoolType = srcPoolType;
5355
this.type = Type.FullClone;
5456
this.scopeType = scopeType;
5557
this.srcVolumeUuid = srcVolumeUuid;
58+
this.srcPoolClusterId = srcPoolClusterId;
5659
}
5760

5861
public String getSrcPoolUuid() {
@@ -63,6 +66,10 @@ public Storage.StoragePoolType getSrcPoolType() {
6366
return srcPoolType;
6467
}
6568

69+
public Long getSrcPoolClusterId() {
70+
return srcPoolClusterId;
71+
}
72+
6673
public ScopeType getScopeType() { return scopeType; }
6774

6875
public String getSrcBackingFilePath() {

engine/orchestration/src/main/java/com/cloud/vm/VirtualMachinePowerStateSyncImpl.java

+5-5
Original file line numberDiff line numberDiff line change
@@ -77,19 +77,19 @@ public void processHostVmStatePingReport(long hostId, Map<String, HostVmStateRep
7777
processReport(hostId, translatedInfo, force);
7878
}
7979

80-
private void updateAndPublishVmPowerStates(long hostId, Map<Long, VirtualMachine.PowerState> instancePowerStates,
81-
Date updateTime) {
80+
protected void updateAndPublishVmPowerStates(long hostId, Map<Long, VirtualMachine.PowerState> instancePowerStates,
81+
Date updateTime) {
8282
if (instancePowerStates.isEmpty()) {
8383
return;
8484
}
8585
Set<Long> vmIds = instancePowerStates.keySet();
86-
Map<Long, VirtualMachine.PowerState> notUpdated = _instanceDao.updatePowerState(instancePowerStates, hostId,
87-
updateTime);
86+
Map<Long, VirtualMachine.PowerState> notUpdated =
87+
_instanceDao.updatePowerState(instancePowerStates, hostId, updateTime);
8888
if (notUpdated.size() > vmIds.size()) {
8989
return;
9090
}
9191
for (Long vmId : vmIds) {
92-
if (!notUpdated.isEmpty() && !notUpdated.containsKey(vmId)) {
92+
if (!notUpdated.containsKey(vmId)) {
9393
logger.debug("VM state report is updated. {}, {}, power state: {}",
9494
() -> hostCache.get(hostId), () -> vmCache.get(vmId), () -> instancePowerStates.get(vmId));
9595
_messageBus.publish(null, VirtualMachineManager.Topics.VM_POWER_STATE,
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,107 @@
1+
// Licensed to the Apache Software Foundation (ASF) under one
2+
// or more contributor license agreements. See the NOTICE file
3+
// distributed with this work for additional information
4+
// regarding copyright ownership. The ASF licenses this file
5+
// to you under the Apache License, Version 2.0 (the
6+
// "License"); you may not use this file except in compliance
7+
// with the License. You may obtain a copy of the License at
8+
//
9+
// http://www.apache.org/licenses/LICENSE-2.0
10+
//
11+
// Unless required by applicable law or agreed to in writing,
12+
// software distributed under the License is distributed on an
13+
// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
14+
// KIND, either express or implied. See the License for the
15+
// specific language governing permissions and limitations
16+
// under the License.
17+
package com.cloud.vm;
18+
19+
import java.util.Date;
20+
import java.util.HashMap;
21+
import java.util.Map;
22+
23+
import org.apache.cloudstack.framework.messagebus.MessageBus;
24+
import org.apache.cloudstack.framework.messagebus.PublishScope;
25+
import org.junit.Before;
26+
import org.junit.Test;
27+
import org.junit.runner.RunWith;
28+
import org.mockito.InjectMocks;
29+
import org.mockito.Mock;
30+
import org.mockito.Mockito;
31+
import org.mockito.junit.MockitoJUnitRunner;
32+
33+
import com.cloud.host.HostVO;
34+
import com.cloud.host.dao.HostDao;
35+
import com.cloud.vm.dao.VMInstanceDao;
36+
37+
@RunWith(MockitoJUnitRunner.class)
38+
public class VirtualMachinePowerStateSyncImplTest {
39+
@Mock
40+
MessageBus messageBus;
41+
@Mock
42+
VMInstanceDao instanceDao;
43+
@Mock
44+
HostDao hostDao;
45+
46+
@InjectMocks
47+
VirtualMachinePowerStateSyncImpl virtualMachinePowerStateSync = new VirtualMachinePowerStateSyncImpl();
48+
49+
@Before
50+
public void setup() {
51+
Mockito.lenient().when(instanceDao.findById(Mockito.anyLong())).thenReturn(Mockito.mock(VMInstanceVO.class));
52+
Mockito.lenient().when(hostDao.findById(Mockito.anyLong())).thenReturn(Mockito.mock(HostVO.class));
53+
}
54+
55+
@Test
56+
public void test_updateAndPublishVmPowerStates_emptyStates() {
57+
virtualMachinePowerStateSync.updateAndPublishVmPowerStates(1L, new HashMap<>(), new Date());
58+
Mockito.verify(instanceDao, Mockito.never()).updatePowerState(Mockito.anyMap(), Mockito.anyLong(),
59+
Mockito.any(Date.class));
60+
}
61+
62+
@Test
63+
public void test_updateAndPublishVmPowerStates_moreNotUpdated() {
64+
Map<Long, VirtualMachine.PowerState> powerStates = new HashMap<>();
65+
powerStates.put(1L, VirtualMachine.PowerState.PowerOff);
66+
Map<Long, VirtualMachine.PowerState> notUpdated = new HashMap<>(powerStates);
67+
notUpdated.put(2L, VirtualMachine.PowerState.PowerOn);
68+
Mockito.when(instanceDao.updatePowerState(Mockito.anyMap(), Mockito.anyLong(),
69+
Mockito.any(Date.class))).thenReturn(notUpdated);
70+
virtualMachinePowerStateSync.updateAndPublishVmPowerStates(1L, powerStates, new Date());
71+
Mockito.verify(messageBus, Mockito.never()).publish(Mockito.nullable(String.class), Mockito.anyString(),
72+
Mockito.any(PublishScope.class), Mockito.anyLong());
73+
}
74+
75+
@Test
76+
public void test_updateAndPublishVmPowerStates_allUpdated() {
77+
Map<Long, VirtualMachine.PowerState> powerStates = new HashMap<>();
78+
powerStates.put(1L, VirtualMachine.PowerState.PowerOff);
79+
Mockito.when(instanceDao.updatePowerState(Mockito.anyMap(), Mockito.anyLong(),
80+
Mockito.any(Date.class))).thenReturn(new HashMap<>());
81+
virtualMachinePowerStateSync.updateAndPublishVmPowerStates(1L, powerStates, new Date());
82+
Mockito.verify(messageBus, Mockito.times(1)).publish(null,
83+
VirtualMachineManager.Topics.VM_POWER_STATE,
84+
PublishScope.GLOBAL,
85+
1L);
86+
}
87+
88+
@Test
89+
public void test_updateAndPublishVmPowerStates_partialUpdated() {
90+
Map<Long, VirtualMachine.PowerState> powerStates = new HashMap<>();
91+
powerStates.put(1L, VirtualMachine.PowerState.PowerOn);
92+
powerStates.put(2L, VirtualMachine.PowerState.PowerOff);
93+
Map<Long, VirtualMachine.PowerState> notUpdated = new HashMap<>();
94+
notUpdated.put(2L, VirtualMachine.PowerState.PowerOff);
95+
Mockito.when(instanceDao.updatePowerState(Mockito.anyMap(), Mockito.anyLong(),
96+
Mockito.any(Date.class))).thenReturn(notUpdated);
97+
virtualMachinePowerStateSync.updateAndPublishVmPowerStates(1L, powerStates, new Date());
98+
Mockito.verify(messageBus, Mockito.times(1)).publish(null,
99+
VirtualMachineManager.Topics.VM_POWER_STATE,
100+
PublishScope.GLOBAL,
101+
1L);
102+
Mockito.verify(messageBus, Mockito.never()).publish(null,
103+
VirtualMachineManager.Topics.VM_POWER_STATE,
104+
PublishScope.GLOBAL,
105+
2L);
106+
}
107+
}

engine/storage/datamotion/src/main/java/org/apache/cloudstack/storage/motion/KvmNonManagedStorageDataMotionStrategy.java

+1-1
Original file line numberDiff line numberDiff line change
@@ -215,7 +215,7 @@ protected void copyTemplateToTargetFilesystemStorageIfNeeded(VolumeInfo srcVolum
215215
}
216216

217217
VMTemplateStoragePoolVO sourceVolumeTemplateStoragePoolVO = vmTemplatePoolDao.findByPoolTemplate(destStoragePool.getId(), srcVolumeInfo.getTemplateId(), null);
218-
if (sourceVolumeTemplateStoragePoolVO == null && (isStoragePoolTypeInList(destStoragePool.getPoolType(), StoragePoolType.Filesystem, StoragePoolType.SharedMountPoint))) {
218+
if (sourceVolumeTemplateStoragePoolVO == null && (isStoragePoolTypeInList(destStoragePool.getPoolType(), StoragePoolType.NetworkFilesystem, StoragePoolType.Filesystem, StoragePoolType.SharedMountPoint))) {
219219
DataStore sourceTemplateDataStore = dataStoreManagerImpl.getRandomImageStore(srcVolumeInfo.getDataCenterId());
220220
if (sourceTemplateDataStore != null) {
221221
TemplateInfo sourceTemplateInfo = templateDataFactory.getTemplate(srcVolumeInfo.getTemplateId(), sourceTemplateDataStore);

engine/storage/datamotion/src/main/java/org/apache/cloudstack/storage/motion/StorageSystemDataMotionStrategy.java

+14-6
Original file line numberDiff line numberDiff line change
@@ -1949,18 +1949,26 @@ private SnapshotDetailsVO handleSnapshotDetails(long csSnapshotId, String value)
19491949
/**
19501950
* Return expected MigrationOptions for a linked clone volume live storage migration
19511951
*/
1952-
protected MigrationOptions createLinkedCloneMigrationOptions(VolumeInfo srcVolumeInfo, VolumeInfo destVolumeInfo, String srcVolumeBackingFile, String srcPoolUuid, Storage.StoragePoolType srcPoolType) {
1952+
protected MigrationOptions createLinkedCloneMigrationOptions(VolumeInfo srcVolumeInfo, VolumeInfo destVolumeInfo, String srcVolumeBackingFile, StoragePoolVO srcPool) {
1953+
String srcPoolUuid = srcPool.getUuid();
1954+
Storage.StoragePoolType srcPoolType = srcPool.getPoolType();
1955+
Long srcPoolClusterId = srcPool.getClusterId();
19531956
VMTemplateStoragePoolVO ref = templatePoolDao.findByPoolTemplate(destVolumeInfo.getPoolId(), srcVolumeInfo.getTemplateId(), null);
19541957
boolean updateBackingFileReference = ref == null;
19551958
String backingFile = !updateBackingFileReference ? ref.getInstallPath() : srcVolumeBackingFile;
1956-
return new MigrationOptions(srcPoolUuid, srcPoolType, backingFile, updateBackingFileReference, srcVolumeInfo.getDataStore().getScope().getScopeType());
1959+
ScopeType scopeType = srcVolumeInfo.getDataStore().getScope().getScopeType();
1960+
return new MigrationOptions(srcPoolUuid, srcPoolType, backingFile, updateBackingFileReference, scopeType, srcPoolClusterId);
19571961
}
19581962

19591963
/**
19601964
* Return expected MigrationOptions for a full clone volume live storage migration
19611965
*/
1962-
protected MigrationOptions createFullCloneMigrationOptions(VolumeInfo srcVolumeInfo, VirtualMachineTO vmTO, Host srcHost, String srcPoolUuid, Storage.StoragePoolType srcPoolType) {
1963-
return new MigrationOptions(srcPoolUuid, srcPoolType, srcVolumeInfo.getPath(), srcVolumeInfo.getDataStore().getScope().getScopeType());
1966+
protected MigrationOptions createFullCloneMigrationOptions(VolumeInfo srcVolumeInfo, VirtualMachineTO vmTO, Host srcHost, StoragePoolVO srcPool) {
1967+
String srcPoolUuid = srcPool.getUuid();
1968+
Storage.StoragePoolType srcPoolType = srcPool.getPoolType();
1969+
Long srcPoolClusterId = srcPool.getClusterId();
1970+
ScopeType scopeType = srcVolumeInfo.getDataStore().getScope().getScopeType();
1971+
return new MigrationOptions(srcPoolUuid, srcPoolType, srcVolumeInfo.getPath(), scopeType, srcPoolClusterId);
19641972
}
19651973

19661974
/**
@@ -1983,9 +1991,9 @@ protected void setVolumeMigrationOptions(VolumeInfo srcVolumeInfo, VolumeInfo de
19831991

19841992
MigrationOptions migrationOptions;
19851993
if (MigrationOptions.Type.LinkedClone.equals(migrationType)) {
1986-
migrationOptions = createLinkedCloneMigrationOptions(srcVolumeInfo, destVolumeInfo, srcVolumeBackingFile, srcPoolUuid, srcPoolType);
1994+
migrationOptions = createLinkedCloneMigrationOptions(srcVolumeInfo, destVolumeInfo, srcVolumeBackingFile, srcPool);
19871995
} else {
1988-
migrationOptions = createFullCloneMigrationOptions(srcVolumeInfo, vmTO, srcHost, srcPoolUuid, srcPoolType);
1996+
migrationOptions = createFullCloneMigrationOptions(srcVolumeInfo, vmTO, srcHost, srcPool);
19891997
}
19901998
migrationOptions.setTimeout(StorageManager.KvmStorageOnlineMigrationWait.value());
19911999
destVolumeInfo.setMigrationOptions(migrationOptions);

plugins/hypervisors/kvm/src/main/java/com/cloud/hypervisor/kvm/resource/LibvirtComputingResource.java

+29-7
Original file line numberDiff line numberDiff line change
@@ -331,6 +331,16 @@ public class LibvirtComputingResource extends ServerResourceBase implements Serv
331331
public static final String UBUNTU_WINDOWS_GUEST_CONVERSION_SUPPORTED_CHECK_CMD = "dpkg -l virtio-win";
332332
public static final String UBUNTU_NBDKIT_PKG_CHECK_CMD = "dpkg -l nbdkit";
333333

334+
public static final int LIBVIRT_CGROUP_CPU_SHARES_MIN = 2;
335+
public static final int LIBVIRT_CGROUP_CPU_SHARES_MAX = 262144;
336+
/**
337+
* The minimal value for the LIBVIRT_CGROUPV2_WEIGHT_MIN is actually 1.
338+
* However, due to an old libvirt bug, it is raised to 2.
339+
* See: https://github.com/libvirt/libvirt/commit/38af6497610075e5fe386734b87186731d4c17ac
340+
*/
341+
public static final int LIBVIRT_CGROUPV2_WEIGHT_MIN = 2;
342+
public static final int LIBVIRT_CGROUPV2_WEIGHT_MAX = 10000;
343+
334344
private String modifyVlanPath;
335345
private String versionStringPath;
336346
private String patchScriptPath;
@@ -512,8 +522,6 @@ public class LibvirtComputingResource extends ServerResourceBase implements Serv
512522

513523
private static int hostCpuMaxCapacity = 0;
514524

515-
private static final int CGROUP_V2_UPPER_LIMIT = 10000;
516-
517525
private static final String COMMAND_GET_CGROUP_HOST_VERSION = "stat -fc %T /sys/fs/cgroup/";
518526

519527
public static final String CGROUP_V2 = "cgroup2fs";
@@ -641,6 +649,10 @@ public LibvirtUtilitiesHelper getLibvirtUtilitiesHelper() {
641649
return libvirtUtilitiesHelper;
642650
}
643651

652+
public String getClusterId() {
653+
return clusterId;
654+
}
655+
644656
public CPUStat getCPUStat() {
645657
return cpuStat;
646658
}
@@ -2821,14 +2833,24 @@ public int calculateCpuShares(VirtualMachineTO vmTO) {
28212833
int requestedCpuShares = vCpus * cpuSpeed;
28222834
int hostCpuMaxCapacity = getHostCpuMaxCapacity();
28232835

2836+
// cgroup v2 is in use
28242837
if (hostCpuMaxCapacity > 0) {
2825-
int updatedCpuShares = (int) Math.ceil((requestedCpuShares * CGROUP_V2_UPPER_LIMIT) / (double) hostCpuMaxCapacity);
2826-
LOGGER.debug(String.format("This host utilizes cgroupv2 (as the max shares value is [%s]), thus, the VM requested shares of [%s] will be converted to " +
2827-
"consider the host limits; the new CPU shares value is [%s].", hostCpuMaxCapacity, requestedCpuShares, updatedCpuShares));
2838+
2839+
int updatedCpuShares = (int) Math.ceil((requestedCpuShares * LIBVIRT_CGROUPV2_WEIGHT_MAX) / (double) hostCpuMaxCapacity);
2840+
LOGGER.debug("This host utilizes cgroupv2 (as the max shares value is [{}]), thus, the VM requested shares of [{}] will be converted to " +
2841+
"consider the host limits; the new CPU shares value is [{}].", hostCpuMaxCapacity, requestedCpuShares, updatedCpuShares);
2842+
2843+
if (updatedCpuShares < LIBVIRT_CGROUPV2_WEIGHT_MIN) updatedCpuShares = LIBVIRT_CGROUPV2_WEIGHT_MIN;
2844+
if (updatedCpuShares > LIBVIRT_CGROUPV2_WEIGHT_MAX) updatedCpuShares = LIBVIRT_CGROUPV2_WEIGHT_MAX;
28282845
return updatedCpuShares;
28292846
}
2830-
LOGGER.debug(String.format("This host does not have a maximum CPU shares set; therefore, this host utilizes cgroupv1 and the VM requested CPU shares [%s] will not be " +
2831-
"converted.", requestedCpuShares));
2847+
2848+
// cgroup v1 is in use
2849+
LOGGER.debug("This host does not have a maximum CPU shares set; therefore, this host utilizes cgroupv1 and the VM requested CPU shares [{}] will not be " +
2850+
"converted.", requestedCpuShares);
2851+
2852+
if (requestedCpuShares < LIBVIRT_CGROUP_CPU_SHARES_MIN) requestedCpuShares = LIBVIRT_CGROUP_CPU_SHARES_MIN;
2853+
if (requestedCpuShares > LIBVIRT_CGROUP_CPU_SHARES_MAX) requestedCpuShares = LIBVIRT_CGROUP_CPU_SHARES_MAX;
28322854
return requestedCpuShares;
28332855
}
28342856

plugins/hypervisors/kvm/src/main/java/com/cloud/hypervisor/kvm/storage/KVMStorageProcessor.java

+6
Original file line numberDiff line numberDiff line change
@@ -2660,6 +2660,12 @@ public KVMStoragePool getTemplateSourcePoolUsingMigrationOptions(KVMStoragePool
26602660
return localPool;
26612661
}
26622662

2663+
if (migrationOptions.getScopeType().equals(ScopeType.CLUSTER)
2664+
&& migrationOptions.getSrcPoolClusterId() != null
2665+
&& !migrationOptions.getSrcPoolClusterId().toString().equals(resource.getClusterId())) {
2666+
return localPool;
2667+
}
2668+
26632669
return storagePoolMgr.getStoragePool(migrationOptions.getSrcPoolType(), migrationOptions.getSrcPoolUuid());
26642670
}
26652671
}

ui/src/components/view/InfoCard.vue

+5
Original file line numberDiff line numberDiff line change
@@ -1208,6 +1208,11 @@ export default {
12081208
if (item.value) {
12091209
query[item.param] = this.resource[item.value]
12101210
} else {
1211+
if (item.name === 'template') {
1212+
query.templatefilter = 'self'
1213+
query.filter = 'self'
1214+
}
1215+
12111216
if (item.param === 'account') {
12121217
query[item.param] = this.resource.name
12131218
query.domainid = this.resource.domainid

ui/src/components/view/ListView.vue

+6-2
Original file line numberDiff line numberDiff line change
@@ -160,6 +160,10 @@
160160
&nbsp;
161161
<a-tag>static-nat</a-tag>
162162
</span>
163+
<span v-if="record.issystem">
164+
&nbsp;
165+
<a-tag>system</a-tag>
166+
</span>
163167
</template>
164168
<template v-if="column.key === 'ip6address'" href="javascript:;">
165169
<span>{{ ipV6Address(text, record) }}</span>
@@ -421,8 +425,8 @@
421425
<status :text="record.enabled ? record.enabled.toString() : 'false'" />
422426
{{ record.enabled ? 'Enabled' : 'Disabled' }}
423427
</template>
424-
<template v-if="['created', 'sent', 'removed', 'effectiveDate', 'endDate'].includes(column.key) || (['startdate'].includes(column.key) && ['webhook'].includes($route.path.split('/')[1])) || (column.key === 'allocated' && ['asnumbers', 'publicip', 'ipv4subnets'].includes($route.meta.name) && text)">
425-
{{ $toLocaleDate(text) }}
428+
<template v-if="['created', 'sent', 'removed', 'effectiveDate', 'endDate', 'allocated'].includes(column.key) || (['startdate'].includes(column.key) && ['webhook'].includes($route.path.split('/')[1])) || (column.key === 'allocated' && ['asnumbers', 'publicip', 'ipv4subnets'].includes($route.meta.name) && text)">
429+
{{ text && $toLocaleDate(text) }}
426430
</template>
427431
<template v-if="['startdate', 'enddate'].includes(column.key) && ['vm', 'vnfapp'].includes($route.path.split('/')[1])">
428432
{{ getDateAtTimeZone(text, record.timezone) }}

ui/src/config/section/network.js

+9-3
Original file line numberDiff line numberDiff line change
@@ -840,10 +840,13 @@ export default {
840840
message: 'message.action.release.ip',
841841
docHelp: 'adminguide/networking_and_traffic.html#releasing-an-ip-address-alloted-to-a-vpc',
842842
dataView: true,
843-
show: (record) => { return record.state === 'Allocated' && !record.issourcenat },
843+
show: (record) => { return record.state === 'Allocated' && !record.issourcenat && !record.issystem },
844844
groupAction: true,
845845
popup: true,
846-
groupMap: (selection) => { return selection.map(x => { return { id: x } }) }
846+
groupMap: (selection) => { return selection.map(x => { return { id: x } }) },
847+
groupShow: (selectedIps) => {
848+
return selectedIps.every((ip) => ip.state === 'Allocated' && !ip.issourcenat && !ip.issystem)
849+
}
847850
},
848851
{
849852
api: 'reserveIpAddress',
@@ -863,7 +866,10 @@ export default {
863866
show: (record) => { return record.state === 'Reserved' },
864867
groupAction: true,
865868
popup: true,
866-
groupMap: (selection) => { return selection.map(x => { return { id: x } }) }
869+
groupMap: (selection) => { return selection.map(x => { return { id: x } }) },
870+
groupShow: (selectedIps) => {
871+
return selectedIps.every((ip) => ip.state === 'Reserved')
872+
}
867873
}
868874
]
869875
},

0 commit comments

Comments
 (0)