Skip to content

Commit 3ee23da

Browse files
committed
address copilot review
Signed-off-by: Abhishek Kumar <abhishek.mrt22@gmail.com>
1 parent b3f2947 commit 3ee23da

10 files changed

Lines changed: 278 additions & 18 deletions

‎server/src/main/java/com/cloud/vm/UserVmManagerImpl.java‎

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3885,6 +3885,8 @@ public boolean deleteVmGroup(long groupId) {
38853885
sc.addAnd("instanceId", SearchCriteria.Op.EQ, groupMap.getInstanceId());
38863886
_groupVMMapDao.expunge(sc);
38873887
}
3888+
// don't leave a stale instance boot group member pointing at a group that no longer exists
3889+
instanceBootGroupMembershipGuard.removeInstanceGroupBootGroupMembershipIfPresent(groupId);
38883890

38893891
if (_vmGroupDao.remove(groupId)) {
38903892
return true;

‎server/src/main/java/org/apache/cloudstack/vm/bootgroup/InstanceBootGroupApiServiceImpl.java‎

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -261,6 +261,11 @@ public ListResponse<InstanceBootGroupResponse> listInstanceBootGroups(ListInstan
261261
final Account caller = ctx.getCallingAccount();
262262
final Long id = cmd.getId();
263263
final String keyword = cmd.getKeyword();
264+
final Long virtualMachineId = cmd.getVirtualMachineId();
265+
final Long instanceGroupId = cmd.getInstanceGroupId();
266+
if (virtualMachineId != null && instanceGroupId != null) {
267+
throw new InvalidParameterValueException("Only one of virtualmachineid or instancegroupid may be specified");
268+
}
264269

265270
List<InstanceBootGroupResponse> responsesList = new ArrayList<>();
266271
List<Long> permittedAccounts = new ArrayList<>();
@@ -272,12 +277,30 @@ public ListResponse<InstanceBootGroupResponse> listInstanceBootGroups(ListInstan
272277
Boolean isRecursive = domainIdRecursiveListProject.second();
273278
Project.ListProjectResourcesCriteria listProjectResourcesCriteria = domainIdRecursiveListProject.third();
274279

280+
// A VM or Instance Group belongs to at most one boot group (unique per member), so this filter
281+
// resolves to either exactly one boot group id, or no results at all.
282+
Long memberBootGroupId = null;
283+
if (virtualMachineId != null || instanceGroupId != null) {
284+
InstanceBootGroupMember.MemberType memberType = virtualMachineId != null
285+
? InstanceBootGroupMember.MemberType.VirtualMachine
286+
: InstanceBootGroupMember.MemberType.InstanceGroup;
287+
long memberId = virtualMachineId != null ? virtualMachineId : instanceGroupId;
288+
InstanceBootGroupMemberVO member = instanceBootGroupMemberDao.findByMember(memberType, memberId);
289+
if (member == null) {
290+
ListResponse<InstanceBootGroupResponse> emptyResponse = new ListResponse<>();
291+
emptyResponse.setResponses(new ArrayList<>(), 0);
292+
return emptyResponse;
293+
}
294+
memberBootGroupId = member.getBootGroupId();
295+
}
296+
275297
Filter searchFilter = new Filter(InstanceBootGroupJoinVO.class, "id", true, cmd.getStartIndex(),
276298
cmd.getPageSizeVal());
277299
SearchBuilder<InstanceBootGroupJoinVO> sb = instanceBootGroupJoinDao.createSearchBuilder();
278300
accountManager.buildACLSearchBuilder(sb, domainId, isRecursive, permittedAccounts,
279301
listProjectResourcesCriteria);
280302
sb.and("id", sb.entity().getId(), SearchCriteria.Op.EQ);
303+
sb.and("memberBootGroupId", sb.entity().getId(), SearchCriteria.Op.EQ);
281304
sb.and("name", sb.entity().getName(), SearchCriteria.Op.EQ);
282305
sb.and("keyword", sb.entity().getName(), SearchCriteria.Op.LIKE);
283306
SearchCriteria<InstanceBootGroupJoinVO> sc = sb.create();
@@ -289,6 +312,9 @@ public ListResponse<InstanceBootGroupResponse> listInstanceBootGroups(ListInstan
289312
if (id != null) {
290313
sc.setParameters("id", id);
291314
}
315+
if (memberBootGroupId != null) {
316+
sc.setParameters("memberBootGroupId", memberBootGroupId);
317+
}
292318
Pair<List<InstanceBootGroupJoinVO>, Integer> bootGroupsAndCount = instanceBootGroupJoinDao.searchAndCount(sc, searchFilter);
293319
for (InstanceBootGroupJoinVO bootGroup : bootGroupsAndCount.first()) {
294320
InstanceBootGroupResponse response = createInstanceBootGroupResponse(bootGroup);

‎server/src/main/java/org/apache/cloudstack/vm/bootgroup/InstanceBootGroupManagerImpl.java‎

Lines changed: 31 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -201,7 +201,7 @@ public void startInstanceBootGroup(InstanceBootGroupVO group) {
201201
userVmService.startVirtualMachine(vm, null);
202202
}
203203
anchorInitialDelay(group, progressByVmId.get(vmId), vm, alreadyRunning);
204-
});
204+
}, true);
205205
} catch (CloudRuntimeException e) {
206206
halt(group, "Failed to start a VM in tier " + tierOrder + ": " + e.getMessage());
207207
throw e;
@@ -489,7 +489,7 @@ public void stopInstanceBootGroup(InstanceBootGroupVO group, boolean forced) {
489489
if (vm != null && vm.getState() != com.cloud.vm.VirtualMachine.State.Stopped) {
490490
userVmService.stopVirtualMachine(vmId, forced);
491491
}
492-
});
492+
}, false);
493493
}
494494

495495
logger.info("{} stop completed ({}ms)", group, System.currentTimeMillis() - groupStoppedAtMs);
@@ -503,12 +503,17 @@ public void rebootInstanceBootGroup(InstanceBootGroupVO group, boolean forced) {
503503
}
504504

505505
/**
506-
* Runs {@code action} for every VM in a tier concurrently and aborts on the first failure. Each
507-
* thread gets a copied {@link CallContext} — without one, a VM lifecycle action routed through
508-
* the job-queue path fails to submit its sub-job ("no lock found").
506+
* Runs {@code action} for every VM in a tier concurrently. Each thread gets a copied
507+
* {@link CallContext} — without one, a VM lifecycle action routed through the job-queue path
508+
* fails to submit its sub-job ("no lock found").
509+
*
510+
* @param haltOnFailure when true (start), the first per-VM failure aborts immediately so the
511+
* caller can halt the whole boot group; when false (stop, which continues
512+
* through every tier regardless per its own documented contract), every
513+
* VM's action is still attempted and failures are logged, not thrown.
509514
*/
510515
private void runTierConcurrently(List<Long> vmIds, InstanceBootGroupVO group,
511-
String actionName, VmAction action) {
516+
String actionName, VmAction action, boolean haltOnFailure) {
512517
if (vmIds.isEmpty()) {
513518
return;
514519
}
@@ -517,7 +522,7 @@ private void runTierConcurrently(List<Long> vmIds, InstanceBootGroupVO group,
517522
actionName, group, vmIds.size(), vmIds);
518523
long actionStartedAtMs = System.currentTimeMillis();
519524
CallContext callerContext = CallContext.current();
520-
int threadCount = Math.min(vmIds.size(), ReadinessCheckConcurrency.value().intValue());
525+
int threadCount = Math.max(1, Math.min(vmIds.size(), ReadinessCheckConcurrency.value().intValue()));
521526
ExecutorService executor = Executors.newFixedThreadPool(
522527
threadCount, new NamedThreadFactory("InstanceBootGroup-" + actionName));
523528

@@ -541,24 +546,34 @@ protected void runInContext() {
541546
}));
542547
}
543548

549+
List<String> failures = new ArrayList<>();
544550
for (Future<?> future : futures) {
545551
try {
546552
future.get();
547553
} catch (ExecutionException e) {
548554
Throwable cause = e.getCause() != null ? e.getCause() : e;
549-
throw new CloudRuntimeException(
550-
String.format("Failed to %s a VM in boot group %s: %s",
551-
actionName, group.getName(), cause.getMessage()),
552-
cause);
553-
555+
String message = String.format("Failed to %s a VM in boot group %s: %s",
556+
actionName, group.getName(), cause.getMessage());
557+
if (haltOnFailure) {
558+
throw new CloudRuntimeException(message, cause);
559+
}
560+
logger.warn(message, cause);
561+
failures.add(message);
554562
} catch (InterruptedException e) {
555563
Thread.currentThread().interrupt();
556-
throw new CloudRuntimeException(
557-
String.format("Interrupted while waiting to %s VMs in boot group %s",
558-
actionName, group.getName()),
559-
e);
564+
String message = String.format("Interrupted while waiting to %s VMs in boot group %s",
565+
actionName, group.getName());
566+
if (haltOnFailure) {
567+
throw new CloudRuntimeException(message, e);
568+
}
569+
logger.warn(message, e);
570+
failures.add(message);
560571
}
561572
}
573+
if (!failures.isEmpty()) {
574+
logger.warn("'{}' action for a tier of {} completed with {} failure(s) out of {} VM(s)",
575+
actionName, group, failures.size(), vmIds.size());
576+
}
562577
} finally {
563578
executor.shutdown();
564579
}

‎server/src/main/java/org/apache/cloudstack/vm/bootgroup/InstanceBootGroupMembershipGuard.java‎

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -135,4 +135,18 @@ public void validateInstanceGroupEligibleForBootGroupMembership(long instanceGro
135135
validateVmEligibleForGroupMembership(member.getInstanceId());
136136
}
137137
}
138+
139+
/**
140+
* Removes the boot-group membership row for an Instance Group being deleted, if any, so the
141+
* delete doesn't leave a stale member pointing at a group that no longer exists. Cascades
142+
* silently rather than blocking the delete, since {@code UserVmManagerImpl.deleteVmGroup} is
143+
* also invoked during account cleanup, where a hard failure here would be worse than the group
144+
* simply dropping out of its boot group.
145+
*/
146+
public void removeInstanceGroupBootGroupMembershipIfPresent(long instanceGroupId) {
147+
InstanceBootGroupMemberVO member = instanceBootGroupMemberDao.findByMember(InstanceBootGroupMember.MemberType.InstanceGroup, instanceGroupId);
148+
if (member != null) {
149+
instanceBootGroupMemberDao.expunge(member.getId());
150+
}
151+
}
138152
}

‎server/src/main/java/org/apache/cloudstack/vm/bootgroup/readiness/InstanceBootGroupReadinessRuleManagerImpl.java‎

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -526,9 +526,15 @@ private void validateInstanceQuorumDetails(Map<String, String> details) {
526526
}
527527
try {
528528
if ("PERCENTAGE".equalsIgnoreCase(thresholdType)) {
529-
Double.parseDouble(thresholdValue);
529+
double percentage = Double.parseDouble(thresholdValue);
530+
if (percentage < 0) {
531+
throw new InvalidParameterValueException(THRESHOLD_VALUE_KEY + " must not be negative: " + thresholdValue);
532+
}
530533
} else {
531-
Long.parseLong(thresholdValue);
534+
long count = Long.parseLong(thresholdValue);
535+
if (count < 0) {
536+
throw new InvalidParameterValueException(THRESHOLD_VALUE_KEY + " must not be negative: " + thresholdValue);
537+
}
532538
}
533539
} catch (NumberFormatException e) {
534540
throw new InvalidParameterValueException("Invalid " + THRESHOLD_VALUE_KEY + ": " + thresholdValue);

‎server/src/test/java/com/cloud/vm/UserVmManagerImplTest.java‎

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -100,6 +100,8 @@
100100
import org.apache.cloudstack.storage.template.VnfTemplateManager;
101101
import org.apache.cloudstack.userdata.UserDataManager;
102102
import org.apache.cloudstack.vm.UnmanagedVMsManager;
103+
import org.apache.cloudstack.annotation.AnnotationService;
104+
import org.apache.cloudstack.annotation.dao.AnnotationDao;
103105
import org.apache.cloudstack.vm.bootgroup.InstanceBootGroupMembershipGuard;
104106
import org.apache.cloudstack.vm.lease.VMLeaseManager;
105107
import org.junit.After;
@@ -209,6 +211,8 @@
209211
import com.cloud.utils.exception.CloudRuntimeException;
210212
import com.cloud.utils.exception.ExceptionProxyObject;
211213
import com.cloud.utils.fsm.NoTransitionException;
214+
import com.cloud.vm.dao.InstanceGroupDao;
215+
import com.cloud.vm.dao.InstanceGroupVMMapDao;
212216
import com.cloud.vm.dao.NicDao;
213217
import com.cloud.vm.dao.UserVmDao;
214218
import com.cloud.vm.dao.VMInstanceDetailsDao;
@@ -474,6 +478,15 @@ public class UserVmManagerImplTest {
474478
@Mock
475479
private InstanceBootGroupMembershipGuard instanceBootGroupMembershipGuard;
476480

481+
@Mock
482+
private InstanceGroupDao _vmGroupDao;
483+
484+
@Mock
485+
private InstanceGroupVMMapDao _groupVMMapDao;
486+
487+
@Mock
488+
private AnnotationDao annotationDao;
489+
477490
@Mock
478491
private UUIDManager uuidMgr;
479492

@@ -3907,6 +3920,23 @@ public void testDestroyVmBlockedWhenPartOfBootGroup() {
39073920
}
39083921
}
39093922

3923+
@Test
3924+
public void testDeleteVmGroupCascadesBootGroupMembershipCleanup() {
3925+
long groupId = 55L;
3926+
InstanceGroupVO group = mock(InstanceGroupVO.class);
3927+
when(group.getUuid()).thenReturn("group-uuid");
3928+
when(_vmGroupDao.findById(groupId)).thenReturn(group);
3929+
when(_groupVMMapDao.listByGroupId(groupId)).thenReturn(new ArrayList<>());
3930+
when(_vmGroupDao.remove(groupId)).thenReturn(true);
3931+
3932+
boolean result = userVmManagerImpl.deleteVmGroup(groupId);
3933+
3934+
assertTrue(result);
3935+
Mockito.verify(instanceBootGroupMembershipGuard).removeInstanceGroupBootGroupMembershipIfPresent(groupId);
3936+
Mockito.verify(annotationDao).removeByEntityType(AnnotationService.EntityType.INSTANCE_GROUP.name(), "group-uuid");
3937+
Mockito.verify(_vmGroupDao).remove(groupId);
3938+
}
3939+
39103940
@Test(expected = InvalidParameterValueException.class)
39113941
public void testValidateLeasePropertiesInvalidDuration() {
39123942
userVmManagerImpl.validateLeaseProperties(-2, VMLeaseManager.ExpiryAction.STOP);

‎server/src/test/java/org/apache/cloudstack/vm/bootgroup/InstanceBootGroupApiServiceImplTest.java‎

Lines changed: 73 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,7 @@
4343
import org.apache.cloudstack.api.command.user.bootgroup.DeleteInstanceBootGroupReadinessRuleCmd;
4444
import org.apache.cloudstack.api.command.user.bootgroup.ListInstanceBootGroupMembersCmd;
4545
import org.apache.cloudstack.api.command.user.bootgroup.ListInstanceBootGroupReadinessRulesCmd;
46+
import org.apache.cloudstack.api.command.user.bootgroup.ListInstanceBootGroupsCmd;
4647
import org.apache.cloudstack.api.command.user.bootgroup.RebootInstanceBootGroupCmd;
4748
import org.apache.cloudstack.api.command.user.bootgroup.RemoveInstanceBootGroupMemberCmd;
4849
import org.apache.cloudstack.api.command.user.bootgroup.StartInstanceBootGroupCmd;
@@ -51,9 +52,11 @@
5152
import org.apache.cloudstack.api.command.user.bootgroup.UpdateInstanceBootGroupMemberCmd;
5253
import org.apache.cloudstack.api.command.user.bootgroup.UpdateInstanceBootGroupReadinessRuleCmd;
5354
import org.apache.cloudstack.api.query.dao.InstanceBootGroupJoinDao;
55+
import org.apache.cloudstack.api.query.vo.InstanceBootGroupJoinVO;
5456
import org.apache.cloudstack.api.response.InstanceBootGroupMemberChildResponse;
5557
import org.apache.cloudstack.api.response.InstanceBootGroupMemberResponse;
5658
import org.apache.cloudstack.api.response.InstanceBootGroupReadinessRuleResponse;
59+
import org.apache.cloudstack.api.response.InstanceBootGroupResponse;
5760
import org.apache.cloudstack.api.response.ListResponse;
5861
import org.apache.cloudstack.context.CallContext;
5962
import org.apache.cloudstack.vm.bootgroup.readiness.InstanceBootGroupReadinessRule;
@@ -75,6 +78,8 @@
7578
import com.cloud.hypervisor.Hypervisor.HypervisorType;
7679
import com.cloud.user.Account;
7780
import com.cloud.user.AccountManager;
81+
import com.cloud.utils.db.SearchBuilder;
82+
import com.cloud.utils.db.SearchCriteria;
7883
import com.cloud.utils.db.Transaction;
7984
import com.cloud.utils.db.TransactionCallback;
8085
import com.cloud.vm.InstanceGroupVMMapVO;
@@ -374,6 +379,74 @@ public void testGetGroupAndCheckAccessSuccessDelegatesToAccountManager() {
374379
verify(accountManager).checkAccess(callerMock, null, true, group);
375380
}
376381

382+
// ---------------------------------------------------------------- listInstanceBootGroups
383+
384+
private ListInstanceBootGroupsCmd baseListGroupsCmd(Long virtualMachineId, Long instanceGroupId) {
385+
ListInstanceBootGroupsCmd cmd = mock(ListInstanceBootGroupsCmd.class);
386+
when(cmd.getVirtualMachineId()).thenReturn(virtualMachineId);
387+
when(cmd.getInstanceGroupId()).thenReturn(instanceGroupId);
388+
return cmd;
389+
}
390+
391+
private SearchBuilder<InstanceBootGroupJoinVO> mockSearchBuilder() {
392+
@SuppressWarnings("unchecked")
393+
SearchBuilder<InstanceBootGroupJoinVO> sb = mock(SearchBuilder.class);
394+
@SuppressWarnings("unchecked")
395+
SearchCriteria<InstanceBootGroupJoinVO> sc = mock(SearchCriteria.class);
396+
when(sb.entity()).thenReturn(new InstanceBootGroupJoinVO());
397+
when(sb.create()).thenReturn(sc);
398+
when(instanceBootGroupJoinDao.createSearchBuilder()).thenReturn(sb);
399+
return sb;
400+
}
401+
402+
@Test(expected = InvalidParameterValueException.class)
403+
public void testListInstanceBootGroupsBothVmAndInstanceGroupIdThrows() {
404+
ListInstanceBootGroupsCmd cmd = baseListGroupsCmd(VM_ID, INSTANCE_GROUP_ID);
405+
service.listInstanceBootGroups(cmd);
406+
}
407+
408+
@Test
409+
public void testListInstanceBootGroupsByVirtualMachineIdNotAMemberReturnsEmpty() {
410+
ListInstanceBootGroupsCmd cmd = baseListGroupsCmd(VM_ID, null);
411+
when(instanceBootGroupMemberDao.findByMember(InstanceBootGroupMember.MemberType.VirtualMachine, VM_ID)).thenReturn(null);
412+
413+
ListResponse<InstanceBootGroupResponse> response = service.listInstanceBootGroups(cmd);
414+
415+
assertEquals(0, response.getCount().intValue());
416+
assertTrue(response.getResponses().isEmpty());
417+
verify(instanceBootGroupJoinDao, never()).createSearchBuilder();
418+
}
419+
420+
@Test
421+
public void testListInstanceBootGroupsByVirtualMachineIdFiltersToMemberBootGroup() {
422+
ListInstanceBootGroupsCmd cmd = baseListGroupsCmd(VM_ID, null);
423+
InstanceBootGroupMemberVO member = newMember(MEMBER_ID, GROUP_ID, InstanceBootGroupMember.MemberType.VirtualMachine, VM_ID, 0);
424+
when(instanceBootGroupMemberDao.findByMember(InstanceBootGroupMember.MemberType.VirtualMachine, VM_ID)).thenReturn(member);
425+
426+
SearchBuilder<InstanceBootGroupJoinVO> sb = mockSearchBuilder();
427+
InstanceBootGroupJoinVO joinVO = new InstanceBootGroupJoinVO();
428+
ReflectionTestUtils.setField(joinVO, "id", GROUP_ID);
429+
ReflectionTestUtils.setField(joinVO, "name", "group1");
430+
when(instanceBootGroupJoinDao.searchAndCount(any(), any()))
431+
.thenReturn(new com.cloud.utils.Pair<>(Collections.singletonList(joinVO), 1));
432+
433+
ListResponse<InstanceBootGroupResponse> response = service.listInstanceBootGroups(cmd);
434+
435+
assertEquals(1, response.getResponses().size());
436+
verify(sb.create()).setParameters("memberBootGroupId", GROUP_ID);
437+
}
438+
439+
@Test
440+
public void testListInstanceBootGroupsByInstanceGroupIdNotAMemberReturnsEmpty() {
441+
ListInstanceBootGroupsCmd cmd = baseListGroupsCmd(null, INSTANCE_GROUP_ID);
442+
when(instanceBootGroupMemberDao.findByMember(InstanceBootGroupMember.MemberType.InstanceGroup, INSTANCE_GROUP_ID)).thenReturn(null);
443+
444+
ListResponse<InstanceBootGroupResponse> response = service.listInstanceBootGroups(cmd);
445+
446+
assertEquals(0, response.getCount().intValue());
447+
verify(instanceBootGroupJoinDao, never()).createSearchBuilder();
448+
}
449+
377450
// ---------------------------------------------------------------- addMemberToInstanceBootGroup
378451

379452
@Test

0 commit comments

Comments
 (0)