diff --git a/api/src/main/java/com/cloud/event/EventTypes.java b/api/src/main/java/com/cloud/event/EventTypes.java index f7d13343d469..6dc9be25dbd1 100644 --- a/api/src/main/java/com/cloud/event/EventTypes.java +++ b/api/src/main/java/com/cloud/event/EventTypes.java @@ -49,6 +49,8 @@ import org.apache.cloudstack.storage.sharedfs.SharedFS; import org.apache.cloudstack.usage.Usage; import org.apache.cloudstack.schedule.ResourceSchedule; +import org.apache.cloudstack.vm.bootgroup.InstanceBootGroup; +import org.apache.cloudstack.vm.bootgroup.readiness.InstanceBootGroupReadinessRule; import com.cloud.dc.DataCenter; import com.cloud.dc.DataCenterGuestIpv6Prefix; @@ -899,6 +901,21 @@ public class EventTypes { public static final String EVENT_DNS_RECORD_DELETE = "DNS.RECORD.DELETE"; public static final String EVENT_DNS_NAME_COLLISION = "DNS.NAME.COLLISION"; + // Instance Boot Group + public static final String EVENT_INSTANCE_BOOT_GROUP_CREATE = "INSTANCE.BOOT.GROUP.CREATE"; + public static final String EVENT_INSTANCE_BOOT_GROUP_DELETE = "INSTANCE.BOOT.GROUP.DELETE"; + public static final String EVENT_INSTANCE_BOOT_GROUP_UPDATE = "INSTANCE.BOOT.GROUP.UPDATE"; + public static final String EVENT_INSTANCE_BOOT_GROUP_START = "INSTANCE.BOOT.GROUP.START"; + public static final String EVENT_INSTANCE_BOOT_GROUP_STOP = "INSTANCE.BOOT.GROUP.STOP"; + public static final String EVENT_INSTANCE_BOOT_GROUP_REBOOT = "INSTANCE.BOOT.GROUP.REBOOT"; + public static final String EVENT_INSTANCE_BOOT_GROUP_MEMBER_ADD = "INSTANCE.BOOT.GROUP.MEMBER.ADD"; + public static final String EVENT_INSTANCE_BOOT_GROUP_MEMBER_REMOVE = "INSTANCE.BOOT.GROUP.MEMBER.REMOVE"; + public static final String EVENT_INSTANCE_BOOT_GROUP_MEMBER_REORDER = "INSTANCE.BOOT.GROUP.MEMBER.REORDER"; + public static final String EVENT_INSTANCE_BOOT_GROUP_READINESS_RULE_CREATE = "INSTANCE.BOOT.GROUP.READINESS.RULE.CREATE"; + public static final String EVENT_INSTANCE_BOOT_GROUP_READINESS_RULE_UPDATE = "INSTANCE.BOOT.GROUP.READINESS.RULE.UPDATE"; + public static final String EVENT_INSTANCE_BOOT_GROUP_READINESS_RULE_DELETE = "INSTANCE.BOOT.GROUP.READINESS.RULE.DELETE"; + + static { // TODO: need a way to force author adding event types to declare the entity details as well, with out braking @@ -1468,6 +1485,19 @@ public class EventTypes { entityEventDetails.put(EVENT_DNS_RECORD_CREATE, DnsRecord.class); entityEventDetails.put(EVENT_DNS_RECORD_DELETE, DnsRecord.class); + // Instance Boot Group + entityEventDetails.put(EVENT_INSTANCE_BOOT_GROUP_CREATE, InstanceBootGroup.class); + entityEventDetails.put(EVENT_INSTANCE_BOOT_GROUP_DELETE, InstanceBootGroup.class); + entityEventDetails.put(EVENT_INSTANCE_BOOT_GROUP_UPDATE, InstanceBootGroup.class); + entityEventDetails.put(EVENT_INSTANCE_BOOT_GROUP_START, InstanceBootGroup.class); + entityEventDetails.put(EVENT_INSTANCE_BOOT_GROUP_STOP, InstanceBootGroup.class); + entityEventDetails.put(EVENT_INSTANCE_BOOT_GROUP_REBOOT, InstanceBootGroup.class); + entityEventDetails.put(EVENT_INSTANCE_BOOT_GROUP_MEMBER_ADD, InstanceBootGroup.class); + entityEventDetails.put(EVENT_INSTANCE_BOOT_GROUP_MEMBER_REMOVE, InstanceBootGroup.class); + entityEventDetails.put(EVENT_INSTANCE_BOOT_GROUP_MEMBER_REORDER, InstanceBootGroup.class); + entityEventDetails.put(EVENT_INSTANCE_BOOT_GROUP_READINESS_RULE_CREATE, InstanceBootGroupReadinessRule.class); + entityEventDetails.put(EVENT_INSTANCE_BOOT_GROUP_READINESS_RULE_UPDATE, InstanceBootGroupReadinessRule.class); + entityEventDetails.put(EVENT_INSTANCE_BOOT_GROUP_READINESS_RULE_DELETE, InstanceBootGroupReadinessRule.class); } public static boolean isNetworkEvent(String eventType) { diff --git a/api/src/main/java/org/apache/cloudstack/api/ApiCommandResourceType.java b/api/src/main/java/org/apache/cloudstack/api/ApiCommandResourceType.java index 2aa97b65a3d5..e12a709e6f5c 100644 --- a/api/src/main/java/org/apache/cloudstack/api/ApiCommandResourceType.java +++ b/api/src/main/java/org/apache/cloudstack/api/ApiCommandResourceType.java @@ -22,6 +22,7 @@ import java.util.Map; import org.apache.cloudstack.region.PortableIp; +import org.apache.cloudstack.vm.bootgroup.readiness.InstanceBootGroupReadinessRule; import org.apache.commons.collections.CollectionUtils; import org.apache.commons.lang3.EnumUtils; import org.apache.commons.lang3.StringUtils; @@ -91,7 +92,9 @@ public enum ApiCommandResourceType { Extension(org.apache.cloudstack.extension.Extension.class), ExtensionCustomAction(org.apache.cloudstack.extension.ExtensionCustomAction.class), KmsKey(org.apache.cloudstack.kms.KMSKey.class), - HsmProfile(org.apache.cloudstack.kms.HSMProfile.class); + HsmProfile(org.apache.cloudstack.kms.HSMProfile.class), + InstanceBootGroup(org.apache.cloudstack.vm.bootgroup.InstanceBootGroup.class), + InstanceBootGroupReadinessRule(InstanceBootGroupReadinessRule.class); private final Class clazz; diff --git a/api/src/main/java/org/apache/cloudstack/api/ApiConstants.java b/api/src/main/java/org/apache/cloudstack/api/ApiConstants.java index ac6acdf42516..98ed4e81ea37 100644 --- a/api/src/main/java/org/apache/cloudstack/api/ApiConstants.java +++ b/api/src/main/java/org/apache/cloudstack/api/ApiConstants.java @@ -93,6 +93,23 @@ public class ApiConstants { public static final String CAPACITY = "capacity"; public static final String CATEGORY = "category"; public static final String CAN_REVERT = "canrevert"; + public static final String BOOT_GROUP_ID = "bootgroupid"; + public static final String BOOT_ORDER = "order"; + public static final String MEMBER_TYPE = "membertype"; + public static final String MEMBER_ID = "memberid"; + public static final String MEMBER_NAME = "membername"; + public static final String MEMBER_STATE = "memberstate"; + public static final String CHILDREN = "children"; + public static final String RULE_TYPE = "ruletype"; + public static final String INHERITED = "inherited"; + public static final String READINESS_MODE = "readinessmode"; + public static final String READINESS_STATUS = "readinessstatus"; + public static final String READINESS_MESSAGE = "readinessmessage"; + public static final String READINESS_ATTEMPT_TIMEOUT_SECONDS = "readinessattempttimeoutseconds"; + public static final String READINESS_MAX_RETRY_ATTEMPTS = "readinessmaxretryattempts"; + public static final String READINESS_REBOOT_ON_RETRY = "readinessrebootonretry"; + public static final String READINESS_INITIAL_DELAY_SECONDS = "readinessinitialdelayseconds"; + public static final String IGNORE_INSTANCE_STATE = "ignoreinstancestate"; public static final String CA_CERTIFICATES = "cacertificates"; public static final String CERTIFICATE = "certificate"; public static final String CERTIFICATE_CHAIN = "certchain"; @@ -322,6 +339,7 @@ public class ApiConstants { public static final String INTERNAL_DNS2 = "internaldns2"; public static final String INTERNET_PROTOCOL = "internetprotocol"; public static final String INTERVAL_TYPE = "intervaltype"; + public static final String INSTANCE_GROUP_ID = "instancegroupid"; public static final String INSTANCE_LEASE_DURATION = "leaseduration"; public static final String INSTANCE_LEASE_ENABLED = "instanceleaseenabled"; public static final String INSTANCE_LEASE_EXPIRY_ACTION = "leaseexpiryaction"; diff --git a/api/src/main/java/org/apache/cloudstack/api/command/user/bootgroup/AddMemberToInstanceBootGroupCmd.java b/api/src/main/java/org/apache/cloudstack/api/command/user/bootgroup/AddMemberToInstanceBootGroupCmd.java new file mode 100644 index 000000000000..d687ec7b42ad --- /dev/null +++ b/api/src/main/java/org/apache/cloudstack/api/command/user/bootgroup/AddMemberToInstanceBootGroupCmd.java @@ -0,0 +1,106 @@ +// 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.command.user.bootgroup; + +import javax.inject.Inject; + +import org.apache.cloudstack.acl.RoleType; +import org.apache.cloudstack.api.APICommand; +import org.apache.cloudstack.api.ApiCommandResourceType; +import org.apache.cloudstack.api.ApiConstants; +import org.apache.cloudstack.api.ApiErrorCode; +import org.apache.cloudstack.api.BaseCmd; +import org.apache.cloudstack.api.Parameter; +import org.apache.cloudstack.api.ServerApiException; +import org.apache.cloudstack.api.command.user.UserCmd; +import org.apache.cloudstack.api.response.InstanceBootGroupMemberResponse; +import org.apache.cloudstack.api.response.InstanceBootGroupResponse; +import org.apache.cloudstack.api.response.InstanceGroupResponse; +import org.apache.cloudstack.api.response.UserVmResponse; +import org.apache.cloudstack.context.CallContext; +import org.apache.cloudstack.vm.bootgroup.InstanceBootGroupMember; +import org.apache.cloudstack.vm.bootgroup.InstanceBootGroupService; + +@APICommand(name = "addMemberToInstanceBootGroup", + description = "Adds a VM or instance group to an instance boot group. Exactly one of virtualmachineid or instancegroupid must be specified.", + responseObject = InstanceBootGroupMemberResponse.class, + entityType = {InstanceBootGroupMember.class}, + requestHasSensitiveInfo = false, + responseHasSensitiveInfo = false, + authorized = {RoleType.Admin, RoleType.ResourceAdmin, RoleType.DomainAdmin, RoleType.User}) +public class AddMemberToInstanceBootGroupCmd extends BaseCmd implements UserCmd { + + @Inject + InstanceBootGroupService instanceBootGroupService; + + @Parameter(name = ApiConstants.ID, type = CommandType.UUID, entityType = InstanceBootGroupResponse.class, required = true, description = "The ID of the instance boot group") + private Long id; + + @Parameter(name = ApiConstants.VIRTUAL_MACHINE_ID, type = CommandType.UUID, entityType = UserVmResponse.class, description = "The ID of the VM to add (exclusive with instancegroupid)") + private Long virtualMachineId; + + @Parameter(name = ApiConstants.INSTANCE_GROUP_ID, type = CommandType.UUID, entityType = InstanceGroupResponse.class, description = "The ID of the instance group to add (exclusive with virtualmachineid)") + private Long instanceGroupId; + + @Parameter(name = ApiConstants.BOOT_ORDER, type = CommandType.INTEGER, required = true, + description = "The boot order value for this member (0 or greater; non-contiguous values are allowed). " + + "Any existing member already at or past this value is shifted one slot later to make room.") + private int order; + + public Long getId() { + return id; + } + + public Long getVirtualMachineId() { + return virtualMachineId; + } + + public Long getInstanceGroupId() { + return instanceGroupId; + } + + public int getOrder() { + return order; + } + + @Override + public long getEntityOwnerId() { + return CallContext.current().getCallingAccount().getId(); + } + + @Override + public Long getApiResourceId() { + return id; + } + + @Override + public ApiCommandResourceType getApiResourceType() { + return ApiCommandResourceType.InstanceBootGroup; + } + + @Override + public void execute() { + InstanceBootGroupMember result = instanceBootGroupService.addMemberToInstanceBootGroup(this); + if (result == null) { + throw new ServerApiException(ApiErrorCode.INTERNAL_ERROR, "Failed to add member to instance boot group"); + } + InstanceBootGroupMemberResponse response = instanceBootGroupService.createInstanceBootGroupMemberResponse(result); + response.setResponseName(getCommandName()); + setResponseObject(response); + } +} diff --git a/api/src/main/java/org/apache/cloudstack/api/command/user/bootgroup/CreateInstanceBootGroupCmd.java b/api/src/main/java/org/apache/cloudstack/api/command/user/bootgroup/CreateInstanceBootGroupCmd.java new file mode 100644 index 000000000000..69eb19a727fe --- /dev/null +++ b/api/src/main/java/org/apache/cloudstack/api/command/user/bootgroup/CreateInstanceBootGroupCmd.java @@ -0,0 +1,141 @@ +// 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.command.user.bootgroup; + +import javax.inject.Inject; + +import org.apache.cloudstack.acl.RoleType; +import org.apache.cloudstack.api.APICommand; +import org.apache.cloudstack.api.ApiCommandResourceType; +import org.apache.cloudstack.api.ApiConstants; +import org.apache.cloudstack.api.ApiErrorCode; +import org.apache.cloudstack.api.BaseCmd; +import org.apache.cloudstack.api.Parameter; +import org.apache.cloudstack.api.ServerApiException; +import org.apache.cloudstack.api.command.user.UserCmd; +import org.apache.cloudstack.api.response.DomainResponse; +import org.apache.cloudstack.api.response.InstanceBootGroupResponse; +import org.apache.cloudstack.api.response.ProjectResponse; +import org.apache.cloudstack.context.CallContext; +import org.apache.cloudstack.vm.bootgroup.InstanceBootGroup; +import org.apache.cloudstack.vm.bootgroup.InstanceBootGroupService; + +@APICommand(name = "createInstanceBootGroup", + description = "Creates an instance boot group", + responseObject = InstanceBootGroupResponse.class, + entityType = {InstanceBootGroup.class}, + requestHasSensitiveInfo = false, + responseHasSensitiveInfo = false, + authorized = {RoleType.Admin, RoleType.ResourceAdmin, RoleType.DomainAdmin, RoleType.User}) +public class CreateInstanceBootGroupCmd extends BaseCmd implements UserCmd { + + @Inject + InstanceBootGroupService instanceBootGroupService; + + @Parameter(name = ApiConstants.NAME, type = CommandType.STRING, required = true, description = "The name of the instance boot group") + private String name; + + @Parameter(name = ApiConstants.DESCRIPTION, type = CommandType.STRING, description = "The description of the instance boot group") + private String description; + + @Parameter(name = ApiConstants.ACCOUNT, type = CommandType.STRING, description = "The account of the instance boot group. Must be used with domainId.") + private String accountName; + + @Parameter(name = ApiConstants.DOMAIN_ID, type = CommandType.UUID, entityType = DomainResponse.class, description = "The domain ID of the account owning the instance boot group") + private Long domainId; + + @Parameter(name = ApiConstants.PROJECT_ID, type = CommandType.UUID, entityType = ProjectResponse.class, description = "The project of the instance boot group") + private Long projectId; + + @Parameter(name = ApiConstants.READINESS_ATTEMPT_TIMEOUT_SECONDS, type = CommandType.LONG, + description = "Per-boot-group override of the global timeout (seconds) for each readiness retry attempt") + private Long readinessAttemptTimeoutSeconds; + + @Parameter(name = ApiConstants.READINESS_MAX_RETRY_ATTEMPTS, type = CommandType.LONG, + description = "Per-boot-group override of the global maximum number of readiness retry attempts") + private Long readinessMaxRetryAttempts; + + @Parameter(name = ApiConstants.READINESS_REBOOT_ON_RETRY, type = CommandType.BOOLEAN, + description = "Per-boot-group override of whether an instance is rebooted between readiness retry attempts") + private Boolean readinessRebootOnRetry; + + @Parameter(name = ApiConstants.READINESS_INITIAL_DELAY_SECONDS, type = CommandType.LONG, + description = "Per-boot-group override of the global delay (seconds) after starting or rebooting an instance before its first readiness check of that attempt") + private Long readinessInitialDelaySeconds; + + public String getName() { + return name; + } + + public String getDescription() { + return description; + } + + public String getAccountName() { + return accountName; + } + + public Long getDomainId() { + return domainId; + } + + public Long getProjectId() { + return projectId; + } + + public Long getReadinessAttemptTimeoutSeconds() { + return readinessAttemptTimeoutSeconds; + } + + public Long getReadinessMaxRetryAttempts() { + return readinessMaxRetryAttempts; + } + + public Boolean getReadinessRebootOnRetry() { + return readinessRebootOnRetry; + } + + public Long getReadinessInitialDelaySeconds() { + return readinessInitialDelaySeconds; + } + + @Override + public long getEntityOwnerId() { + Long accountId = _accountService.finalizeAccountId(accountName, domainId, projectId, true); + if (accountId == null) { + return CallContext.current().getCallingAccount().getId(); + } + return accountId; + } + + @Override + public ApiCommandResourceType getApiResourceType() { + return ApiCommandResourceType.InstanceBootGroup; + } + + @Override + public void execute() { + InstanceBootGroup result = instanceBootGroupService.createInstanceBootGroup(this); + if (result == null) { + throw new ServerApiException(ApiErrorCode.INTERNAL_ERROR, "Failed to create instance boot group"); + } + InstanceBootGroupResponse response = instanceBootGroupService.createInstanceBootGroupResponse(result.getId()); + response.setResponseName(getCommandName()); + setResponseObject(response); + } +} diff --git a/api/src/main/java/org/apache/cloudstack/api/command/user/bootgroup/CreateInstanceBootGroupReadinessRuleCmd.java b/api/src/main/java/org/apache/cloudstack/api/command/user/bootgroup/CreateInstanceBootGroupReadinessRuleCmd.java new file mode 100644 index 000000000000..9948e59659d2 --- /dev/null +++ b/api/src/main/java/org/apache/cloudstack/api/command/user/bootgroup/CreateInstanceBootGroupReadinessRuleCmd.java @@ -0,0 +1,132 @@ +// 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.command.user.bootgroup; + +import java.util.Collection; +import java.util.Map; + +import javax.inject.Inject; + +import org.apache.cloudstack.acl.RoleType; +import org.apache.cloudstack.api.APICommand; +import org.apache.cloudstack.api.ApiCommandResourceType; +import org.apache.cloudstack.api.ApiConstants; +import org.apache.cloudstack.api.ApiErrorCode; +import org.apache.cloudstack.api.BaseCmd; +import org.apache.cloudstack.api.Parameter; +import org.apache.cloudstack.api.ServerApiException; +import org.apache.cloudstack.api.command.user.UserCmd; +import org.apache.cloudstack.api.response.InstanceBootGroupReadinessRuleResponse; +import org.apache.cloudstack.api.response.InstanceBootGroupResponse; +import org.apache.cloudstack.api.response.InstanceGroupResponse; +import org.apache.cloudstack.api.response.UserVmResponse; +import org.apache.cloudstack.context.CallContext; +import org.apache.cloudstack.vm.bootgroup.readiness.InstanceBootGroupReadinessRule; +import org.apache.cloudstack.vm.bootgroup.InstanceBootGroupService; + +@APICommand(name = "createInstanceBootGroupReadinessRule", + description = "Creates a readiness rule for a VM or instance group that is a member (directly, or via its instance group) of an instance boot group. " + + "Exactly one of virtualmachineid or instancegroupid must be specified.", + responseObject = InstanceBootGroupReadinessRuleResponse.class, + entityType = {InstanceBootGroupReadinessRule.class}, + requestHasSensitiveInfo = false, + responseHasSensitiveInfo = false, + authorized = {RoleType.Admin, RoleType.ResourceAdmin, RoleType.DomainAdmin, RoleType.User}) +public class CreateInstanceBootGroupReadinessRuleCmd extends BaseCmd implements UserCmd { + + @Inject + InstanceBootGroupService instanceBootGroupService; + + @Parameter(name = ApiConstants.BOOT_GROUP_ID, type = CommandType.UUID, entityType = InstanceBootGroupResponse.class, required = true, + description = "The ID of the boot group this rule belongs to") + private Long bootGroupId; + + @Parameter(name = ApiConstants.VIRTUAL_MACHINE_ID, type = CommandType.UUID, entityType = UserVmResponse.class, + description = "The ID of the VM this rule applies to (exclusive with instancegroupid)") + private Long virtualMachineId; + + @Parameter(name = ApiConstants.INSTANCE_GROUP_ID, type = CommandType.UUID, entityType = InstanceGroupResponse.class, + description = "The ID of the instance group this rule applies to (exclusive with virtualmachineid)") + private Long instanceGroupId; + + @Parameter(name = ApiConstants.RULE_TYPE, type = CommandType.STRING, required = true, + description = "The readiness rule type: GuestAgentLiveness, Ping, PortCheck, MemberQuorum or CustomScript") + private String ruleType; + + @Parameter(name = ApiConstants.NAME, type = CommandType.STRING, description = "The name of the readiness rule; auto-generated if not provided") + private String name; + + @Parameter(name = ApiConstants.ENABLED, type = CommandType.BOOLEAN, description = "Whether the rule is enabled; defaults to true") + private Boolean enabled; + + @Parameter(name = ApiConstants.DETAILS, type = CommandType.MAP, description = "Rule-type-specific configuration, e.g. port/protocol, script, threshold_type/threshold_value") + private Map details; + + public Long getBootGroupId() { + return bootGroupId; + } + + public Long getVirtualMachineId() { + return virtualMachineId; + } + + public Long getInstanceGroupId() { + return instanceGroupId; + } + + public String getRuleType() { + return ruleType; + } + + public String getName() { + return name; + } + + public boolean isEnabled() { + return enabled == null || enabled; + } + + public Map getDetails() { + if (this.details == null || this.details.isEmpty()) { + return null; + } + Collection paramsCollection = this.details.values(); + return (Map) (paramsCollection.toArray())[0]; + } + + @Override + public long getEntityOwnerId() { + return CallContext.current().getCallingAccount().getId(); + } + + @Override + public ApiCommandResourceType getApiResourceType() { + return ApiCommandResourceType.InstanceBootGroupReadinessRule; + } + + @Override + public void execute() { + InstanceBootGroupReadinessRule result = instanceBootGroupService.createInstanceBootGroupReadinessRule(this); + if (result == null) { + throw new ServerApiException(ApiErrorCode.INTERNAL_ERROR, "Failed to create instance boot group readiness rule"); + } + InstanceBootGroupReadinessRuleResponse response = instanceBootGroupService.createInstanceBootGroupReadinessRuleResponse(result); + response.setResponseName(getCommandName()); + setResponseObject(response); + } +} diff --git a/api/src/main/java/org/apache/cloudstack/api/command/user/bootgroup/DeleteInstanceBootGroupCmd.java b/api/src/main/java/org/apache/cloudstack/api/command/user/bootgroup/DeleteInstanceBootGroupCmd.java new file mode 100644 index 000000000000..ffb5349b3190 --- /dev/null +++ b/api/src/main/java/org/apache/cloudstack/api/command/user/bootgroup/DeleteInstanceBootGroupCmd.java @@ -0,0 +1,80 @@ +// 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.command.user.bootgroup; + +import javax.inject.Inject; + +import org.apache.cloudstack.acl.RoleType; +import org.apache.cloudstack.api.APICommand; +import org.apache.cloudstack.api.ApiCommandResourceType; +import org.apache.cloudstack.api.ApiConstants; +import org.apache.cloudstack.api.ApiErrorCode; +import org.apache.cloudstack.api.BaseCmd; +import org.apache.cloudstack.api.Parameter; +import org.apache.cloudstack.api.ServerApiException; +import org.apache.cloudstack.api.command.user.UserCmd; +import org.apache.cloudstack.api.response.InstanceBootGroupResponse; +import org.apache.cloudstack.api.response.SuccessResponse; +import org.apache.cloudstack.context.CallContext; +import org.apache.cloudstack.vm.bootgroup.InstanceBootGroup; +import org.apache.cloudstack.vm.bootgroup.InstanceBootGroupService; + +@APICommand(name = "deleteInstanceBootGroup", + description = "Deletes an instance boot group", + responseObject = SuccessResponse.class, + entityType = {InstanceBootGroup.class}, + requestHasSensitiveInfo = false, + responseHasSensitiveInfo = false, + authorized = {RoleType.Admin, RoleType.ResourceAdmin, RoleType.DomainAdmin, RoleType.User}) +public class DeleteInstanceBootGroupCmd extends BaseCmd implements UserCmd { + + @Inject + InstanceBootGroupService instanceBootGroupService; + + @Parameter(name = ApiConstants.ID, type = CommandType.UUID, entityType = InstanceBootGroupResponse.class, required = true, description = "The ID of the instance boot group") + private Long id; + + public Long getId() { + return id; + } + + @Override + public long getEntityOwnerId() { + return CallContext.current().getCallingAccount().getId(); + } + + @Override + public Long getApiResourceId() { + return id; + } + + @Override + public ApiCommandResourceType getApiResourceType() { + return ApiCommandResourceType.InstanceBootGroup; + } + + @Override + public void execute() { + boolean result = instanceBootGroupService.deleteInstanceBootGroup(this); + if (!result) { + throw new ServerApiException(ApiErrorCode.INTERNAL_ERROR, "Failed to delete instance boot group"); + } + SuccessResponse response = new SuccessResponse(getCommandName()); + setResponseObject(response); + } +} diff --git a/api/src/main/java/org/apache/cloudstack/api/command/user/bootgroup/DeleteInstanceBootGroupReadinessRuleCmd.java b/api/src/main/java/org/apache/cloudstack/api/command/user/bootgroup/DeleteInstanceBootGroupReadinessRuleCmd.java new file mode 100644 index 000000000000..c8fe2b6383de --- /dev/null +++ b/api/src/main/java/org/apache/cloudstack/api/command/user/bootgroup/DeleteInstanceBootGroupReadinessRuleCmd.java @@ -0,0 +1,81 @@ +// 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.command.user.bootgroup; + +import javax.inject.Inject; + +import org.apache.cloudstack.acl.RoleType; +import org.apache.cloudstack.api.APICommand; +import org.apache.cloudstack.api.ApiCommandResourceType; +import org.apache.cloudstack.api.ApiConstants; +import org.apache.cloudstack.api.ApiErrorCode; +import org.apache.cloudstack.api.BaseCmd; +import org.apache.cloudstack.api.Parameter; +import org.apache.cloudstack.api.ServerApiException; +import org.apache.cloudstack.api.command.user.UserCmd; +import org.apache.cloudstack.api.response.InstanceBootGroupReadinessRuleResponse; +import org.apache.cloudstack.api.response.SuccessResponse; +import org.apache.cloudstack.context.CallContext; +import org.apache.cloudstack.vm.bootgroup.readiness.InstanceBootGroupReadinessRule; +import org.apache.cloudstack.vm.bootgroup.InstanceBootGroupService; + +@APICommand(name = "deleteInstanceBootGroupReadinessRule", + description = "Deletes an instance boot group readiness rule", + responseObject = SuccessResponse.class, + entityType = {InstanceBootGroupReadinessRule.class}, + requestHasSensitiveInfo = false, + responseHasSensitiveInfo = false, + authorized = {RoleType.Admin, RoleType.ResourceAdmin, RoleType.DomainAdmin, RoleType.User}) +public class DeleteInstanceBootGroupReadinessRuleCmd extends BaseCmd implements UserCmd { + + @Inject + InstanceBootGroupService instanceBootGroupService; + + @Parameter(name = ApiConstants.ID, type = CommandType.UUID, entityType = InstanceBootGroupReadinessRuleResponse.class, required = true, + description = "The ID of the readiness rule") + private Long id; + + public Long getId() { + return id; + } + + @Override + public long getEntityOwnerId() { + return CallContext.current().getCallingAccount().getId(); + } + + @Override + public Long getApiResourceId() { + return id; + } + + @Override + public ApiCommandResourceType getApiResourceType() { + return ApiCommandResourceType.InstanceBootGroupReadinessRule; + } + + @Override + public void execute() { + boolean result = instanceBootGroupService.deleteInstanceBootGroupReadinessRule(this); + if (!result) { + throw new ServerApiException(ApiErrorCode.INTERNAL_ERROR, "Failed to delete instance boot group readiness rule"); + } + SuccessResponse response = new SuccessResponse(getCommandName()); + setResponseObject(response); + } +} diff --git a/api/src/main/java/org/apache/cloudstack/api/command/user/bootgroup/ListInstanceBootGroupMembersCmd.java b/api/src/main/java/org/apache/cloudstack/api/command/user/bootgroup/ListInstanceBootGroupMembersCmd.java new file mode 100644 index 000000000000..9f6c36c505b0 --- /dev/null +++ b/api/src/main/java/org/apache/cloudstack/api/command/user/bootgroup/ListInstanceBootGroupMembersCmd.java @@ -0,0 +1,95 @@ +// 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.command.user.bootgroup; + +import java.util.List; + +import javax.inject.Inject; + +import org.apache.commons.lang3.BooleanUtils; + +import org.apache.cloudstack.acl.RoleType; +import org.apache.cloudstack.api.APICommand; +import org.apache.cloudstack.api.ApiConstants; +import org.apache.cloudstack.api.BaseListCmd; +import org.apache.cloudstack.api.Parameter; +import org.apache.cloudstack.api.command.user.UserCmd; +import org.apache.cloudstack.api.response.InstanceBootGroupMemberResponse; +import org.apache.cloudstack.api.response.InstanceBootGroupResponse; +import org.apache.cloudstack.api.response.ListResponse; +import org.apache.cloudstack.vm.bootgroup.InstanceBootGroupMember; +import org.apache.cloudstack.vm.bootgroup.InstanceBootGroupService; + +@APICommand(name = "listInstanceBootGroupMembers", + description = "Lists members of an instance boot group, sorted by boot order", + responseObject = InstanceBootGroupMemberResponse.class, + entityType = {InstanceBootGroupMember.class}, + requestHasSensitiveInfo = false, + responseHasSensitiveInfo = false, + authorized = {RoleType.Admin, RoleType.ResourceAdmin, RoleType.DomainAdmin, RoleType.User}) +public class ListInstanceBootGroupMembersCmd extends BaseListCmd implements UserCmd { + + @Inject + InstanceBootGroupService instanceBootGroupService; + + @Parameter(name = ApiConstants.BOOT_GROUP_ID, type = CommandType.UUID, required = true, entityType = InstanceBootGroupResponse.class, description = "The ID of the instance boot group") + private Long bootGroupId; + + @Parameter(name = ApiConstants.MEMBER_TYPE, type = CommandType.STRING, description = "Filter by member type: VirtualMachine or InstanceGroup") + private String memberType; + + @Parameter(name = ApiConstants.DETAILS, + type = CommandType.LIST, + collectionType = CommandType.STRING, + description = "Comma separated list of additional details requested, value can be a list of [all, readiness, children]. " + + "Readiness fields are computed from cached check results (not a live re-check) and are omitted unless requested, since computing them is not free. " + + "Children returns the member VMs of InstanceGroup-type members (omitted for VirtualMachine-type members); combine with readiness to also include per-child readiness.") + private List viewDetails; + + @Parameter(name = ApiConstants.IGNORE_INSTANCE_STATE, type = CommandType.BOOLEAN, + description = "If true, readiness status/message reflect the last cached rule check regardless of the member's current instance state. " + + "If false (default), a VM that isn't Running is always reported NotReady, even if its rules were last cached Ready.") + private Boolean ignoreInstanceState; + + public Long getBootGroupId() { + return bootGroupId; + } + + public String getMemberType() { + return memberType; + } + + public boolean isIgnoreInstanceState() { + return BooleanUtils.toBoolean(ignoreInstanceState); + } + + public boolean isReadinessDetailRequested() { + return viewDetails != null && (viewDetails.contains("readiness") || viewDetails.contains("all")); + } + + public boolean isChildrenDetailRequested() { + return viewDetails != null && (viewDetails.contains("children") || viewDetails.contains("all")); + } + + @Override + public void execute() { + ListResponse response = instanceBootGroupService.listInstanceBootGroupMembers(this); + response.setResponseName(getCommandName()); + setResponseObject(response); + } +} diff --git a/api/src/main/java/org/apache/cloudstack/api/command/user/bootgroup/ListInstanceBootGroupReadinessRulesCmd.java b/api/src/main/java/org/apache/cloudstack/api/command/user/bootgroup/ListInstanceBootGroupReadinessRulesCmd.java new file mode 100644 index 000000000000..8589b97e875a --- /dev/null +++ b/api/src/main/java/org/apache/cloudstack/api/command/user/bootgroup/ListInstanceBootGroupReadinessRulesCmd.java @@ -0,0 +1,90 @@ +// 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.command.user.bootgroup; + +import javax.inject.Inject; + +import org.apache.cloudstack.acl.RoleType; +import org.apache.cloudstack.api.APICommand; +import org.apache.cloudstack.api.ApiConstants; +import org.apache.cloudstack.api.BaseListCmd; +import org.apache.cloudstack.api.Parameter; +import org.apache.cloudstack.api.command.user.UserCmd; +import org.apache.cloudstack.api.response.InstanceBootGroupReadinessRuleResponse; +import org.apache.cloudstack.api.response.InstanceBootGroupResponse; +import org.apache.cloudstack.api.response.InstanceGroupResponse; +import org.apache.cloudstack.api.response.ListResponse; +import org.apache.cloudstack.api.response.UserVmResponse; +import org.apache.cloudstack.vm.bootgroup.readiness.InstanceBootGroupReadinessRule; +import org.apache.cloudstack.vm.bootgroup.InstanceBootGroupService; + +@APICommand(name = "listInstanceBootGroupReadinessRules", + description = "Lists readiness rules for an instance boot group", + responseObject = InstanceBootGroupReadinessRuleResponse.class, + entityType = {InstanceBootGroupReadinessRule.class}, + requestHasSensitiveInfo = false, + responseHasSensitiveInfo = false, + authorized = {RoleType.Admin, RoleType.ResourceAdmin, RoleType.DomainAdmin, RoleType.User}) +public class ListInstanceBootGroupReadinessRulesCmd extends BaseListCmd implements UserCmd { + + @Inject + InstanceBootGroupService instanceBootGroupService; + + @Parameter(name = ApiConstants.BOOT_GROUP_ID, type = CommandType.UUID, entityType = InstanceBootGroupResponse.class, required = true, + description = "The ID of the instance boot group; listing is always scoped to one boot group") + private Long bootGroupId; + + @Parameter(name = ApiConstants.ID, type = CommandType.UUID, entityType = InstanceBootGroupReadinessRuleResponse.class, description = "List by readiness rule ID") + private Long id; + + @Parameter(name = ApiConstants.VIRTUAL_MACHINE_ID, type = CommandType.UUID, entityType = UserVmResponse.class, description = "Narrow to this VM's rules") + private Long virtualMachineId; + + @Parameter(name = ApiConstants.INSTANCE_GROUP_ID, type = CommandType.UUID, entityType = InstanceGroupResponse.class, description = "Narrow to this instance group's rules") + private Long instanceGroupId; + + @Parameter(name = ApiConstants.RULE_TYPE, type = CommandType.STRING, description = "Filter by readiness rule type") + private String ruleType; + + public Long getBootGroupId() { + return bootGroupId; + } + + public Long getId() { + return id; + } + + public Long getVirtualMachineId() { + return virtualMachineId; + } + + public Long getInstanceGroupId() { + return instanceGroupId; + } + + public String getRuleType() { + return ruleType; + } + + @Override + public void execute() { + ListResponse response = instanceBootGroupService.listInstanceBootGroupReadinessRules(this); + response.setResponseName(getCommandName()); + setResponseObject(response); + } +} diff --git a/api/src/main/java/org/apache/cloudstack/api/command/user/bootgroup/ListInstanceBootGroupsCmd.java b/api/src/main/java/org/apache/cloudstack/api/command/user/bootgroup/ListInstanceBootGroupsCmd.java new file mode 100644 index 000000000000..43afdacf8132 --- /dev/null +++ b/api/src/main/java/org/apache/cloudstack/api/command/user/bootgroup/ListInstanceBootGroupsCmd.java @@ -0,0 +1,82 @@ +// 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.command.user.bootgroup; + +import javax.inject.Inject; + +import org.apache.cloudstack.acl.RoleType; +import org.apache.cloudstack.api.APICommand; +import org.apache.cloudstack.api.ApiConstants; +import org.apache.cloudstack.api.BaseListProjectAndAccountResourcesCmd; +import org.apache.cloudstack.api.Parameter; +import org.apache.cloudstack.api.command.user.UserCmd; +import org.apache.cloudstack.api.response.InstanceBootGroupResponse; +import org.apache.cloudstack.api.response.InstanceGroupResponse; +import org.apache.cloudstack.api.response.ListResponse; +import org.apache.cloudstack.api.response.UserVmResponse; +import org.apache.cloudstack.vm.bootgroup.InstanceBootGroup; +import org.apache.cloudstack.vm.bootgroup.InstanceBootGroupService; + +@APICommand(name = "listInstanceBootGroups", + description = "Lists instance boot groups", + responseObject = InstanceBootGroupResponse.class, + entityType = {InstanceBootGroup.class}, + requestHasSensitiveInfo = false, + responseHasSensitiveInfo = false, + authorized = {RoleType.Admin, RoleType.ResourceAdmin, RoleType.DomainAdmin, RoleType.User}) +public class ListInstanceBootGroupsCmd extends BaseListProjectAndAccountResourcesCmd implements UserCmd { + + @Inject + InstanceBootGroupService instanceBootGroupService; + + @Parameter(name = ApiConstants.ID, type = CommandType.UUID, entityType = InstanceBootGroupResponse.class, description = "List instance boot groups by ID") + private Long id; + + @Parameter(name = ApiConstants.KEYWORD, type = CommandType.STRING, description = "List instance boot groups by name keyword") + private String keyword; + + @Parameter(name = ApiConstants.VIRTUAL_MACHINE_ID, type = CommandType.UUID, entityType = UserVmResponse.class, description = "List boot groups that contain this VM") + private Long virtualMachineId; + + @Parameter(name = ApiConstants.INSTANCE_GROUP_ID, type = CommandType.UUID, entityType = InstanceGroupResponse.class, description = "List boot groups that contain this instance group") + private Long instanceGroupId; + + public Long getId() { + return id; + } + + @Override + public String getKeyword() { + return keyword; + } + + public Long getVirtualMachineId() { + return virtualMachineId; + } + + public Long getInstanceGroupId() { + return instanceGroupId; + } + + @Override + public void execute() { + ListResponse response = instanceBootGroupService.listInstanceBootGroups(this); + response.setResponseName(getCommandName()); + setResponseObject(response); + } +} diff --git a/api/src/main/java/org/apache/cloudstack/api/command/user/bootgroup/RebootInstanceBootGroupCmd.java b/api/src/main/java/org/apache/cloudstack/api/command/user/bootgroup/RebootInstanceBootGroupCmd.java new file mode 100644 index 000000000000..917f91037aba --- /dev/null +++ b/api/src/main/java/org/apache/cloudstack/api/command/user/bootgroup/RebootInstanceBootGroupCmd.java @@ -0,0 +1,106 @@ +// 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.command.user.bootgroup; + +import javax.inject.Inject; + +import org.apache.cloudstack.acl.RoleType; +import org.apache.cloudstack.api.APICommand; +import org.apache.cloudstack.api.ApiCommandResourceType; +import org.apache.cloudstack.api.ApiConstants; +import org.apache.cloudstack.api.ApiErrorCode; +import org.apache.cloudstack.api.BaseAsyncCmd; +import org.apache.cloudstack.api.Parameter; +import org.apache.cloudstack.api.ServerApiException; +import org.apache.cloudstack.api.command.user.UserCmd; +import org.apache.cloudstack.api.response.InstanceBootGroupResponse; +import org.apache.cloudstack.context.CallContext; +import org.apache.cloudstack.vm.bootgroup.InstanceBootGroup; +import org.apache.cloudstack.vm.bootgroup.InstanceBootGroupService; + +import com.cloud.event.EventTypes; +import com.cloud.utils.exception.CloudRuntimeException; + +@APICommand(name = "rebootInstanceBootGroup", + description = "Reboots all VMs in an instance boot group: stops in reverse order then starts in forward order.", + responseObject = InstanceBootGroupResponse.class, + entityType = {InstanceBootGroup.class}, + requestHasSensitiveInfo = false, + responseHasSensitiveInfo = false, + authorized = {RoleType.Admin, RoleType.ResourceAdmin, RoleType.DomainAdmin, RoleType.User}) +public class RebootInstanceBootGroupCmd extends BaseAsyncCmd implements UserCmd { + + @Inject + InstanceBootGroupService instanceBootGroupService; + + @Parameter(name = ApiConstants.ID, type = CommandType.UUID, entityType = InstanceBootGroupResponse.class, required = true, description = "The ID of the instance boot group") + private Long id; + + @Parameter(name = ApiConstants.FORCED, type = CommandType.BOOLEAN, required = false, + description = "Force stop every Instance in the instance boot group during the stop phase of the reboot (It is force-stopped and then started)") + private Boolean forced; + + public Long getId() { + return id; + } + + public boolean isForced() { + return forced != null && forced; + } + + @Override + public long getEntityOwnerId() { + return CallContext.current().getCallingAccount().getId(); + } + + @Override + public String getEventType() { + return EventTypes.EVENT_INSTANCE_BOOT_GROUP_REBOOT; + } + + @Override + public String getEventDescription() { + return "Rebooting instance boot group with ID: " + getResourceUuid(ApiConstants.ID); + } + + @Override + public Long getApiResourceId() { + return id; + } + + @Override + public ApiCommandResourceType getApiResourceType() { + return ApiCommandResourceType.InstanceBootGroup; + } + + @Override + public void execute() { + InstanceBootGroup result; + try { + result = instanceBootGroupService.rebootInstanceBootGroup(this); + } catch (CloudRuntimeException e) { + throw new ServerApiException(ApiErrorCode.INTERNAL_ERROR, e.getMessage()); + } + if (result == null) { + throw new ServerApiException(ApiErrorCode.INTERNAL_ERROR, "Failed to reboot instance boot group"); + } + InstanceBootGroupResponse response = instanceBootGroupService.createInstanceBootGroupResponse(result.getId()); + response.setResponseName(getCommandName()); + setResponseObject(response); + } +} diff --git a/api/src/main/java/org/apache/cloudstack/api/command/user/bootgroup/RemoveInstanceBootGroupMemberCmd.java b/api/src/main/java/org/apache/cloudstack/api/command/user/bootgroup/RemoveInstanceBootGroupMemberCmd.java new file mode 100644 index 000000000000..8200940b38d1 --- /dev/null +++ b/api/src/main/java/org/apache/cloudstack/api/command/user/bootgroup/RemoveInstanceBootGroupMemberCmd.java @@ -0,0 +1,80 @@ +// 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.command.user.bootgroup; + +import javax.inject.Inject; + +import org.apache.cloudstack.acl.RoleType; +import org.apache.cloudstack.api.APICommand; +import org.apache.cloudstack.api.ApiCommandResourceType; +import org.apache.cloudstack.api.ApiConstants; +import org.apache.cloudstack.api.ApiErrorCode; +import org.apache.cloudstack.api.BaseCmd; +import org.apache.cloudstack.api.Parameter; +import org.apache.cloudstack.api.ServerApiException; +import org.apache.cloudstack.api.command.user.UserCmd; +import org.apache.cloudstack.api.response.InstanceBootGroupMemberResponse; +import org.apache.cloudstack.api.response.SuccessResponse; +import org.apache.cloudstack.context.CallContext; +import org.apache.cloudstack.vm.bootgroup.InstanceBootGroupMember; +import org.apache.cloudstack.vm.bootgroup.InstanceBootGroupService; + +@APICommand(name = "removeInstanceBootGroupMember", + description = "Removes a member (VM or instance group) from an instance boot group", + responseObject = SuccessResponse.class, + entityType = {InstanceBootGroupMember.class}, + requestHasSensitiveInfo = false, + responseHasSensitiveInfo = false, + authorized = {RoleType.Admin, RoleType.ResourceAdmin, RoleType.DomainAdmin, RoleType.User}) +public class RemoveInstanceBootGroupMemberCmd extends BaseCmd implements UserCmd { + + @Inject + InstanceBootGroupService instanceBootGroupService; + + @Parameter(name = ApiConstants.ID, type = CommandType.UUID, entityType = InstanceBootGroupMemberResponse.class, required = true, description = "The ID of the boot group member entry to remove") + private Long id; + + public Long getId() { + return id; + } + + @Override + public long getEntityOwnerId() { + return CallContext.current().getCallingAccount().getId(); + } + + @Override + public Long getApiResourceId() { + return instanceBootGroupService.getInstanceBootGroupIdForMember(getId()); + } + + @Override + public ApiCommandResourceType getApiResourceType() { + return ApiCommandResourceType.InstanceBootGroup; + } + + @Override + public void execute() { + boolean result = instanceBootGroupService.removeInstanceBootGroupMember(this); + if (!result) { + throw new ServerApiException(ApiErrorCode.INTERNAL_ERROR, "Failed to remove member from instance boot group"); + } + SuccessResponse response = new SuccessResponse(getCommandName()); + setResponseObject(response); + } +} diff --git a/api/src/main/java/org/apache/cloudstack/api/command/user/bootgroup/StartInstanceBootGroupCmd.java b/api/src/main/java/org/apache/cloudstack/api/command/user/bootgroup/StartInstanceBootGroupCmd.java new file mode 100644 index 000000000000..d6f89a48f63c --- /dev/null +++ b/api/src/main/java/org/apache/cloudstack/api/command/user/bootgroup/StartInstanceBootGroupCmd.java @@ -0,0 +1,98 @@ +// 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.command.user.bootgroup; + +import javax.inject.Inject; + +import org.apache.cloudstack.acl.RoleType; +import org.apache.cloudstack.api.APICommand; +import org.apache.cloudstack.api.ApiCommandResourceType; +import org.apache.cloudstack.api.ApiConstants; +import org.apache.cloudstack.api.ApiErrorCode; +import org.apache.cloudstack.api.BaseAsyncCmd; +import org.apache.cloudstack.api.Parameter; +import org.apache.cloudstack.api.ServerApiException; +import org.apache.cloudstack.api.command.user.UserCmd; +import org.apache.cloudstack.api.response.InstanceBootGroupResponse; +import org.apache.cloudstack.context.CallContext; +import org.apache.cloudstack.vm.bootgroup.InstanceBootGroup; +import org.apache.cloudstack.vm.bootgroup.InstanceBootGroupService; + +import com.cloud.event.EventTypes; +import com.cloud.utils.exception.CloudRuntimeException; + +@APICommand(name = "startInstanceBootGroup", + description = "Starts all VMs in an instance boot group in order (lowest boot order first). VMs within the same order tier start concurrently.", + responseObject = InstanceBootGroupResponse.class, + entityType = {InstanceBootGroup.class}, + requestHasSensitiveInfo = false, + responseHasSensitiveInfo = false, + authorized = {RoleType.Admin, RoleType.ResourceAdmin, RoleType.DomainAdmin, RoleType.User}) +public class StartInstanceBootGroupCmd extends BaseAsyncCmd implements UserCmd { + + @Inject + InstanceBootGroupService instanceBootGroupService; + + @Parameter(name = ApiConstants.ID, type = CommandType.UUID, entityType = InstanceBootGroupResponse.class, required = true, description = "The ID of the instance boot group") + private Long id; + + public Long getId() { + return id; + } + + @Override + public long getEntityOwnerId() { + return CallContext.current().getCallingAccount().getId(); + } + + @Override + public String getEventType() { + return EventTypes.EVENT_INSTANCE_BOOT_GROUP_START; + } + + @Override + public String getEventDescription() { + return "Starting instance boot group with ID: " + getResourceUuid(ApiConstants.ID); + } + + @Override + public Long getApiResourceId() { + return id; + } + + @Override + public ApiCommandResourceType getApiResourceType() { + return ApiCommandResourceType.InstanceBootGroup; + } + + @Override + public void execute() { + InstanceBootGroup result; + try { + result = instanceBootGroupService.startInstanceBootGroup(this); + } catch (CloudRuntimeException e) { + throw new ServerApiException(ApiErrorCode.INTERNAL_ERROR, e.getMessage()); + } + if (result == null) { + throw new ServerApiException(ApiErrorCode.INTERNAL_ERROR, "Failed to start instance boot group"); + } + InstanceBootGroupResponse response = instanceBootGroupService.createInstanceBootGroupResponse(result.getId()); + response.setResponseName(getCommandName()); + setResponseObject(response); + } +} diff --git a/api/src/main/java/org/apache/cloudstack/api/command/user/bootgroup/StopInstanceBootGroupCmd.java b/api/src/main/java/org/apache/cloudstack/api/command/user/bootgroup/StopInstanceBootGroupCmd.java new file mode 100644 index 000000000000..414204d2b708 --- /dev/null +++ b/api/src/main/java/org/apache/cloudstack/api/command/user/bootgroup/StopInstanceBootGroupCmd.java @@ -0,0 +1,100 @@ +// 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.command.user.bootgroup; + +import javax.inject.Inject; + +import org.apache.cloudstack.acl.RoleType; +import org.apache.cloudstack.api.APICommand; +import org.apache.cloudstack.api.ApiCommandResourceType; +import org.apache.cloudstack.api.ApiConstants; +import org.apache.cloudstack.api.ApiErrorCode; +import org.apache.cloudstack.api.BaseAsyncCmd; +import org.apache.cloudstack.api.Parameter; +import org.apache.cloudstack.api.ServerApiException; +import org.apache.cloudstack.api.command.user.UserCmd; +import org.apache.cloudstack.api.response.InstanceBootGroupResponse; +import org.apache.cloudstack.context.CallContext; +import org.apache.cloudstack.vm.bootgroup.InstanceBootGroup; +import org.apache.cloudstack.vm.bootgroup.InstanceBootGroupService; + +import com.cloud.event.EventTypes; + +@APICommand(name = "stopInstanceBootGroup", + description = "Stops all VMs in an instance boot group in reverse order (highest boot order first). Continues through all tiers even if some VMs fail to stop.", + responseObject = InstanceBootGroupResponse.class, + entityType = {InstanceBootGroup.class}, + requestHasSensitiveInfo = false, + responseHasSensitiveInfo = false, + authorized = {RoleType.Admin, RoleType.ResourceAdmin, RoleType.DomainAdmin, RoleType.User}) +public class StopInstanceBootGroupCmd extends BaseAsyncCmd implements UserCmd { + + @Inject + InstanceBootGroupService instanceBootGroupService; + + @Parameter(name = ApiConstants.ID, type = CommandType.UUID, entityType = InstanceBootGroupResponse.class, required = true, description = "The ID of the instance boot group") + private Long id; + + @Parameter(name = ApiConstants.FORCED, type = CommandType.BOOLEAN, required = false, + description = "Force stop every Instance in the instance boot group (marked as Stopped even when the stop command fails to be sent to the backend, otherwise a force poweroff is attempted)") + private Boolean forced; + + public Long getId() { + return id; + } + + public boolean isForced() { + return forced != null && forced; + } + + @Override + public long getEntityOwnerId() { + return CallContext.current().getCallingAccount().getId(); + } + + @Override + public String getEventType() { + return EventTypes.EVENT_INSTANCE_BOOT_GROUP_STOP; + } + + @Override + public String getEventDescription() { + return "Stopping instance boot group with ID: " + getResourceUuid(ApiConstants.ID); + } + + @Override + public Long getApiResourceId() { + return id; + } + + @Override + public ApiCommandResourceType getApiResourceType() { + return ApiCommandResourceType.InstanceBootGroup; + } + + @Override + public void execute() { + InstanceBootGroup result = instanceBootGroupService.stopInstanceBootGroup(this); + if (result == null) { + throw new ServerApiException(ApiErrorCode.INTERNAL_ERROR, "Failed to stop instance boot group"); + } + InstanceBootGroupResponse response = instanceBootGroupService.createInstanceBootGroupResponse(result.getId()); + response.setResponseName(getCommandName()); + setResponseObject(response); + } +} diff --git a/api/src/main/java/org/apache/cloudstack/api/command/user/bootgroup/UpdateInstanceBootGroupCmd.java b/api/src/main/java/org/apache/cloudstack/api/command/user/bootgroup/UpdateInstanceBootGroupCmd.java new file mode 100644 index 000000000000..33c985c94a02 --- /dev/null +++ b/api/src/main/java/org/apache/cloudstack/api/command/user/bootgroup/UpdateInstanceBootGroupCmd.java @@ -0,0 +1,126 @@ +// 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.command.user.bootgroup; + +import javax.inject.Inject; + +import org.apache.cloudstack.acl.RoleType; +import org.apache.cloudstack.api.APICommand; +import org.apache.cloudstack.api.ApiCommandResourceType; +import org.apache.cloudstack.api.ApiConstants; +import org.apache.cloudstack.api.ApiErrorCode; +import org.apache.cloudstack.api.BaseCmd; +import org.apache.cloudstack.api.Parameter; +import org.apache.cloudstack.api.ServerApiException; +import org.apache.cloudstack.api.command.user.UserCmd; +import org.apache.cloudstack.api.response.InstanceBootGroupResponse; +import org.apache.cloudstack.context.CallContext; +import org.apache.cloudstack.vm.bootgroup.InstanceBootGroup; +import org.apache.cloudstack.vm.bootgroup.InstanceBootGroupService; + +@APICommand(name = "updateInstanceBootGroup", + description = "Updates an instance boot group", + responseObject = InstanceBootGroupResponse.class, + entityType = {InstanceBootGroup.class}, + requestHasSensitiveInfo = false, + responseHasSensitiveInfo = false, + authorized = {RoleType.Admin, RoleType.ResourceAdmin, RoleType.DomainAdmin, RoleType.User}) +public class UpdateInstanceBootGroupCmd extends BaseCmd implements UserCmd { + + @Inject + InstanceBootGroupService instanceBootGroupService; + + @Parameter(name = ApiConstants.ID, type = CommandType.UUID, entityType = InstanceBootGroupResponse.class, required = true, description = "The ID of the instance boot group") + private Long id; + + @Parameter(name = ApiConstants.NAME, type = CommandType.STRING, description = "New name for the instance boot group") + private String name; + + @Parameter(name = ApiConstants.DESCRIPTION, type = CommandType.STRING, description = "New description for the instance boot group") + private String description; + + @Parameter(name = ApiConstants.READINESS_ATTEMPT_TIMEOUT_SECONDS, type = CommandType.LONG, + description = "Per-boot-group override of the global timeout (seconds) for each readiness retry attempt. Pass -1 to clear the override and fall back to the global setting.") + private Long readinessAttemptTimeoutSeconds; + + @Parameter(name = ApiConstants.READINESS_MAX_RETRY_ATTEMPTS, type = CommandType.LONG, + description = "Per-boot-group override of the global maximum number of readiness retry attempts. Pass -1 to clear the override and fall back to the global setting.") + private Long readinessMaxRetryAttempts; + + @Parameter(name = ApiConstants.READINESS_REBOOT_ON_RETRY, type = CommandType.BOOLEAN, + description = "Per-boot-group override of whether an instance is rebooted between readiness retry attempts") + private Boolean readinessRebootOnRetry; + + @Parameter(name = ApiConstants.READINESS_INITIAL_DELAY_SECONDS, type = CommandType.LONG, + description = "Per-boot-group override of the global delay (seconds) after starting or rebooting an instance before its first readiness check of that attempt. Pass -1 to clear the override and fall back to the global setting.") + private Long readinessInitialDelaySeconds; + + public Long getId() { + return id; + } + + public String getName() { + return name; + } + + public String getDescription() { + return description; + } + + public Long getReadinessAttemptTimeoutSeconds() { + return readinessAttemptTimeoutSeconds; + } + + public Long getReadinessMaxRetryAttempts() { + return readinessMaxRetryAttempts; + } + + public Boolean getReadinessRebootOnRetry() { + return readinessRebootOnRetry; + } + + public Long getReadinessInitialDelaySeconds() { + return readinessInitialDelaySeconds; + } + + @Override + public long getEntityOwnerId() { + return CallContext.current().getCallingAccount().getId(); + } + + @Override + public Long getApiResourceId() { + return id; + } + + @Override + public ApiCommandResourceType getApiResourceType() { + return ApiCommandResourceType.InstanceBootGroup; + } + + @Override + public void execute() { + InstanceBootGroup result = instanceBootGroupService.updateInstanceBootGroup(this); + if (result == null) { + throw new ServerApiException(ApiErrorCode.INTERNAL_ERROR, "Failed to update instance boot group"); + } + InstanceBootGroupResponse response = instanceBootGroupService.createInstanceBootGroupResponse(result.getId()); + response.setResponseName(getCommandName()); + setResponseObject(response); + } +} diff --git a/api/src/main/java/org/apache/cloudstack/api/command/user/bootgroup/UpdateInstanceBootGroupMemberCmd.java b/api/src/main/java/org/apache/cloudstack/api/command/user/bootgroup/UpdateInstanceBootGroupMemberCmd.java new file mode 100644 index 000000000000..4e612b4cdce7 --- /dev/null +++ b/api/src/main/java/org/apache/cloudstack/api/command/user/bootgroup/UpdateInstanceBootGroupMemberCmd.java @@ -0,0 +1,87 @@ +// 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.command.user.bootgroup; + +import javax.inject.Inject; + +import org.apache.cloudstack.acl.RoleType; +import org.apache.cloudstack.api.APICommand; +import org.apache.cloudstack.api.ApiCommandResourceType; +import org.apache.cloudstack.api.ApiConstants; +import org.apache.cloudstack.api.ApiErrorCode; +import org.apache.cloudstack.api.BaseCmd; +import org.apache.cloudstack.api.Parameter; +import org.apache.cloudstack.api.ServerApiException; +import org.apache.cloudstack.api.command.user.UserCmd; +import org.apache.cloudstack.api.response.InstanceBootGroupMemberResponse; +import org.apache.cloudstack.context.CallContext; +import org.apache.cloudstack.vm.bootgroup.InstanceBootGroupMember; +import org.apache.cloudstack.vm.bootgroup.InstanceBootGroupService; + +@APICommand(name = "updateInstanceBootGroupMember", + description = "Updates the boot order of a member in an instance boot group", + responseObject = InstanceBootGroupMemberResponse.class, + entityType = {InstanceBootGroupMember.class}, + requestHasSensitiveInfo = false, + responseHasSensitiveInfo = false, + authorized = {RoleType.Admin, RoleType.ResourceAdmin, RoleType.DomainAdmin, RoleType.User}) +public class UpdateInstanceBootGroupMemberCmd extends BaseCmd implements UserCmd { + + @Inject + InstanceBootGroupService instanceBootGroupService; + + @Parameter(name = ApiConstants.ID, type = CommandType.UUID, entityType = InstanceBootGroupMemberResponse.class, required = true, description = "The UUID of the boot group member entry") + private Long id; + + @Parameter(name = ApiConstants.BOOT_ORDER, type = CommandType.INTEGER, required = true, description = "The new boot order value (0 or greater)") + private int order; + + public Long getId() { + return id; + } + + public int getOrder() { + return order; + } + + @Override + public long getEntityOwnerId() { + return CallContext.current().getCallingAccount().getId(); + } + + @Override + public Long getApiResourceId() { + return instanceBootGroupService.getInstanceBootGroupIdForMember(getId()); + } + + @Override + public ApiCommandResourceType getApiResourceType() { + return ApiCommandResourceType.InstanceBootGroup; + } + + @Override + public void execute() { + InstanceBootGroupMember result = instanceBootGroupService.updateInstanceBootGroupMember(this); + if (result == null) { + throw new ServerApiException(ApiErrorCode.INTERNAL_ERROR, "Failed to update instance boot group member"); + } + InstanceBootGroupMemberResponse response = instanceBootGroupService.createInstanceBootGroupMemberResponse(result); + response.setResponseName(getCommandName()); + setResponseObject(response); + } +} diff --git a/api/src/main/java/org/apache/cloudstack/api/command/user/bootgroup/UpdateInstanceBootGroupReadinessRuleCmd.java b/api/src/main/java/org/apache/cloudstack/api/command/user/bootgroup/UpdateInstanceBootGroupReadinessRuleCmd.java new file mode 100644 index 000000000000..93875c24375b --- /dev/null +++ b/api/src/main/java/org/apache/cloudstack/api/command/user/bootgroup/UpdateInstanceBootGroupReadinessRuleCmd.java @@ -0,0 +1,109 @@ +// 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.command.user.bootgroup; + +import java.util.Collection; +import java.util.Map; + +import javax.inject.Inject; + +import org.apache.cloudstack.acl.RoleType; +import org.apache.cloudstack.api.APICommand; +import org.apache.cloudstack.api.ApiCommandResourceType; +import org.apache.cloudstack.api.ApiConstants; +import org.apache.cloudstack.api.ApiErrorCode; +import org.apache.cloudstack.api.BaseCmd; +import org.apache.cloudstack.api.Parameter; +import org.apache.cloudstack.api.ServerApiException; +import org.apache.cloudstack.api.command.user.UserCmd; +import org.apache.cloudstack.api.response.InstanceBootGroupReadinessRuleResponse; +import org.apache.cloudstack.context.CallContext; +import org.apache.cloudstack.vm.bootgroup.readiness.InstanceBootGroupReadinessRule; +import org.apache.cloudstack.vm.bootgroup.InstanceBootGroupService; + +@APICommand(name = "updateInstanceBootGroupReadinessRule", + description = "Updates an instance boot group readiness rule. The rule type, boot group and item are immutable after creation.", + responseObject = InstanceBootGroupReadinessRuleResponse.class, + entityType = {InstanceBootGroupReadinessRule.class}, + requestHasSensitiveInfo = false, + responseHasSensitiveInfo = false, + authorized = {RoleType.Admin, RoleType.ResourceAdmin, RoleType.DomainAdmin, RoleType.User}) +public class UpdateInstanceBootGroupReadinessRuleCmd extends BaseCmd implements UserCmd { + + @Inject + InstanceBootGroupService instanceBootGroupService; + + @Parameter(name = ApiConstants.ID, type = CommandType.UUID, entityType = InstanceBootGroupReadinessRuleResponse.class, required = true, + description = "The ID of the readiness rule") + private Long id; + + @Parameter(name = ApiConstants.NAME, type = CommandType.STRING, description = "New name for the readiness rule") + private String name; + + @Parameter(name = ApiConstants.ENABLED, type = CommandType.BOOLEAN, description = "Whether the rule is enabled") + private Boolean enabled; + + @Parameter(name = ApiConstants.DETAILS, type = CommandType.MAP, description = "Rule-type-specific configuration") + private Map details; + + public Long getId() { + return id; + } + + public String getName() { + return name; + } + + public Boolean getEnabled() { + return enabled; + } + + public Map getDetails() { + if (this.details == null || this.details.isEmpty()) { + return null; + } + Collection paramsCollection = this.details.values(); + return (Map) (paramsCollection.toArray())[0]; + } + + @Override + public long getEntityOwnerId() { + return CallContext.current().getCallingAccount().getId(); + } + + @Override + public Long getApiResourceId() { + return id; + } + + @Override + public ApiCommandResourceType getApiResourceType() { + return ApiCommandResourceType.InstanceBootGroupReadinessRule; + } + + @Override + public void execute() { + InstanceBootGroupReadinessRule result = instanceBootGroupService.updateInstanceBootGroupReadinessRule(this); + if (result == null) { + throw new ServerApiException(ApiErrorCode.INTERNAL_ERROR, "Failed to update instance boot group readiness rule"); + } + InstanceBootGroupReadinessRuleResponse response = instanceBootGroupService.createInstanceBootGroupReadinessRuleResponse(result); + response.setResponseName(getCommandName()); + setResponseObject(response); + } +} diff --git a/api/src/main/java/org/apache/cloudstack/api/response/InstanceBootGroupMemberChildResponse.java b/api/src/main/java/org/apache/cloudstack/api/response/InstanceBootGroupMemberChildResponse.java new file mode 100644 index 000000000000..d1164209d003 --- /dev/null +++ b/api/src/main/java/org/apache/cloudstack/api/response/InstanceBootGroupMemberChildResponse.java @@ -0,0 +1,57 @@ +// 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 com.google.gson.annotations.SerializedName; + +import org.apache.cloudstack.api.ApiConstants; + +import com.cloud.serializer.Param; + +/** + * Extends {@link UserVmResponse} rather than duplicating id/name/state fields, since this is set + * up from a plain {@code UserVmVO} (basic details only, via {@code UserVmDao}) with just a few + * boot-group-specific fields of its own — never populated from the join-based VM response builder. + */ +@SuppressWarnings("unused") +public class InstanceBootGroupMemberChildResponse extends UserVmResponse { + + @SerializedName(ApiConstants.READINESS_MODE) + @Param(description = "None, ChildDependent or RuleBased, computed from whether readiness rules are attached") + private String readinessMode; + + @SerializedName(ApiConstants.READINESS_STATUS) + @Param(description = "The last cached readiness evaluation") + private String readinessStatus; + + @SerializedName(ApiConstants.READINESS_MESSAGE) + @Param(description = "Why readinessstatus is what it is: the failing rule(s)' cached message") + private String readinessMessage; + + public void setReadinessMode(String readinessMode) { + this.readinessMode = readinessMode; + } + + public void setReadinessStatus(String readinessStatus) { + this.readinessStatus = readinessStatus; + } + + public void setReadinessMessage(String readinessMessage) { + this.readinessMessage = readinessMessage; + } +} diff --git a/api/src/main/java/org/apache/cloudstack/api/response/InstanceBootGroupMemberResponse.java b/api/src/main/java/org/apache/cloudstack/api/response/InstanceBootGroupMemberResponse.java new file mode 100644 index 000000000000..4d91714216ce --- /dev/null +++ b/api/src/main/java/org/apache/cloudstack/api/response/InstanceBootGroupMemberResponse.java @@ -0,0 +1,131 @@ +// 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 java.util.Date; +import java.util.List; + +import com.google.gson.annotations.SerializedName; + +import org.apache.cloudstack.api.ApiConstants; +import org.apache.cloudstack.api.BaseResponse; +import org.apache.cloudstack.api.EntityReference; +import org.apache.cloudstack.vm.bootgroup.InstanceBootGroupMember; + +import com.cloud.serializer.Param; + +@SuppressWarnings("unused") +@EntityReference(value = InstanceBootGroupMember.class) +public class InstanceBootGroupMemberResponse extends BaseResponse { + + @SerializedName(ApiConstants.ID) + @Param(description = "The UUID of this member entry") + private String id; + + @SerializedName(ApiConstants.BOOT_GROUP_ID) + @Param(description = "The ID of the boot group this member belongs to") + private String bootGroupId; + + @SerializedName(ApiConstants.MEMBER_TYPE) + @Param(description = "The type of the member: VirtualMachine or InstanceGroup") + private String memberType; + + @SerializedName(ApiConstants.MEMBER_ID) + @Param(description = "The ID of the VM or InstanceGroup") + private String memberId; + + @SerializedName(ApiConstants.MEMBER_NAME) + @Param(description = "The name of the VM or InstanceGroup") + private String memberName; + + @SerializedName(ApiConstants.MEMBER_STATE) + @Param(description = "The state of the Instance") + private String memberState; + + @SerializedName(ApiConstants.BOOT_ORDER) + @Param(description = "The boot order value for this member") + private int order; + + @SerializedName(ApiConstants.CREATED) + @Param(description = "The date the member was added to the boot group") + private Date created; + + @SerializedName(ApiConstants.READINESS_MODE) + @Param(description = "None, CHILD_DEPENDENT or RULE_BASED, computed from whether readiness rules are attached") + private String readinessMode; + + @SerializedName(ApiConstants.READINESS_STATUS) + @Param(description = "The last cached readiness evaluation") + private String readinessStatus; + + @SerializedName(ApiConstants.READINESS_MESSAGE) + @Param(description = "Why readinessstatus is what it is: the failing rule(s)' cached message, and/or a count of not-ready member VMs for InstanceGroup members") + private String readinessMessage; + + @SerializedName(ApiConstants.CHILDREN) + @Param(description = "For InstanceGroup members, the VMs within the group (only present when requested via details=children)", responseObject = InstanceBootGroupMemberChildResponse.class) + private List children; + + public void setId(String id) { + this.id = id; + } + + public void setBootGroupId(String bootGroupId) { + this.bootGroupId = bootGroupId; + } + + public void setMemberType(String memberType) { + this.memberType = memberType; + } + + public void setMemberId(String memberId) { + this.memberId = memberId; + } + + public void setMemberName(String memberName) { + this.memberName = memberName; + } + + public void setMemberState(String memberState) { + this.memberState = memberState; + } + + public void setOrder(int order) { + this.order = order; + } + + public void setCreated(Date created) { + this.created = created; + } + + public void setReadinessMode(String readinessMode) { + this.readinessMode = readinessMode; + } + + public void setReadinessStatus(String readinessStatus) { + this.readinessStatus = readinessStatus; + } + + public void setReadinessMessage(String readinessMessage) { + this.readinessMessage = readinessMessage; + } + + public void setChildren(List children) { + this.children = children; + } +} diff --git a/api/src/main/java/org/apache/cloudstack/api/response/InstanceBootGroupReadinessRuleResponse.java b/api/src/main/java/org/apache/cloudstack/api/response/InstanceBootGroupReadinessRuleResponse.java new file mode 100644 index 000000000000..26228c8e8784 --- /dev/null +++ b/api/src/main/java/org/apache/cloudstack/api/response/InstanceBootGroupReadinessRuleResponse.java @@ -0,0 +1,148 @@ +// 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 java.util.Date; +import java.util.Map; + +import com.google.gson.annotations.SerializedName; + +import org.apache.cloudstack.api.ApiConstants; +import org.apache.cloudstack.api.BaseResponse; +import org.apache.cloudstack.api.EntityReference; +import org.apache.cloudstack.vm.bootgroup.readiness.InstanceBootGroupReadinessRule; + +import com.cloud.serializer.Param; + +@SuppressWarnings("unused") +@EntityReference(value = InstanceBootGroupReadinessRule.class) +public class InstanceBootGroupReadinessRuleResponse extends BaseResponse { + + @SerializedName(ApiConstants.ID) + @Param(description = "The ID of the readiness rule") + private String id; + + @SerializedName(ApiConstants.NAME) + @Param(description = "The name of the readiness rule") + private String name; + + @SerializedName(ApiConstants.BOOT_GROUP_ID) + @Param(description = "The ID of the boot group this rule belongs to") + private String bootGroupId; + + @SerializedName(ApiConstants.MEMBER_TYPE) + @Param(description = "The item type this rule applies to: VirtualMachine or InstanceGroup") + private String itemType; + + @SerializedName(ApiConstants.MEMBER_ID) + @Param(description = "The ID of the item (VM or instance group) this rule applies to") + private String itemId; + + @SerializedName(ApiConstants.MEMBER_NAME) + @Param(description = "The name of the item (VM or instance group) this rule applies to") + private String itemName; + + @SerializedName(ApiConstants.RULE_TYPE) + @Param(description = "The readiness rule type") + private String ruleType; + + @SerializedName(ApiConstants.ENABLED) + @Param(description = "Whether the rule is enabled") + private boolean enabled; + + @SerializedName(ApiConstants.INHERITED) + @Param(description = "True if this rule is not attached to the queried item directly, but inherited from its owning InstanceGroup " + + "(only possible when listing by virtualmachineid; the rule's itemtype/itemid still refer to the InstanceGroup it's actually attached to)") + private boolean inherited; + + @SerializedName(ApiConstants.DETAILS) + @Param(description = "Rule-type-specific configuration") + private Map details; + + @SerializedName(ApiConstants.CREATED) + @Param(description = "The date the rule was created") + private Date created; + + @SerializedName(ApiConstants.READINESS_STATUS) + @Param(description = "The last cached evaluation status of this rule: READY, NOT_READY, ERROR or UNKNOWN") + private String status; + + @SerializedName("statusmessage") + @Param(description = "The message from the last evaluation of this rule") + private String statusMessage; + + @SerializedName("checkedon") + @Param(description = "When this rule was last evaluated") + private Date checkedOn; + + public void setId(String id) { + this.id = id; + } + + public void setName(String name) { + this.name = name; + } + + public void setBootGroupId(String bootGroupId) { + this.bootGroupId = bootGroupId; + } + + public void setItemType(String itemType) { + this.itemType = itemType; + } + + public void setItemId(String itemId) { + this.itemId = itemId; + } + + public void setItemName(String itemName) { + this.itemName = itemName; + } + + public void setRuleType(String ruleType) { + this.ruleType = ruleType; + } + + public void setEnabled(boolean enabled) { + this.enabled = enabled; + } + + public void setInherited(boolean inherited) { + this.inherited = inherited; + } + + public void setDetails(Map details) { + this.details = details; + } + + public void setCreated(Date created) { + this.created = created; + } + + public void setStatus(String status) { + this.status = status; + } + + public void setStatusMessage(String statusMessage) { + this.statusMessage = statusMessage; + } + + public void setCheckedOn(Date checkedOn) { + this.checkedOn = checkedOn; + } +} diff --git a/api/src/main/java/org/apache/cloudstack/api/response/InstanceBootGroupResponse.java b/api/src/main/java/org/apache/cloudstack/api/response/InstanceBootGroupResponse.java new file mode 100644 index 000000000000..127eeda17744 --- /dev/null +++ b/api/src/main/java/org/apache/cloudstack/api/response/InstanceBootGroupResponse.java @@ -0,0 +1,160 @@ +// 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 java.util.Date; + +import com.google.gson.annotations.SerializedName; + +import org.apache.cloudstack.api.ApiConstants; +import org.apache.cloudstack.api.BaseResponse; +import org.apache.cloudstack.api.EntityReference; +import org.apache.cloudstack.vm.bootgroup.InstanceBootGroup; + +import com.cloud.serializer.Param; + +@SuppressWarnings("unused") +@EntityReference(value = InstanceBootGroup.class) +public class InstanceBootGroupResponse extends BaseResponse implements ControlledViewEntityResponse { + + @SerializedName(ApiConstants.ID) + @Param(description = "The ID of the instance boot group") + private String id; + + @SerializedName(ApiConstants.NAME) + @Param(description = "The name of the instance boot group") + private String name; + + @SerializedName(ApiConstants.DESCRIPTION) + @Param(description = "The description of the instance boot group") + private String description; + + @SerializedName(ApiConstants.CREATED) + @Param(description = "The date the instance boot group was created") + private Date created; + + @SerializedName(ApiConstants.ACCOUNT) + @Param(description = "The account owning the instance boot group") + private String accountName; + + @SerializedName(ApiConstants.ACCOUNT_ID) + @Param(description = "The account ID owning the instance boot group") + private String accountId; + + @SerializedName(ApiConstants.DOMAIN_ID) + @Param(description = "The domain ID of the instance boot group") + private String domainId; + + @SerializedName(ApiConstants.DOMAIN) + @Param(description = "The domain name of the instance boot group") + private String domainName; + + @SerializedName(ApiConstants.DOMAIN_PATH) + @Param(description = "The path of the domain the instance boot group belongs to") + private String domainPath; + + @SerializedName(ApiConstants.PROJECT_ID) + @Param(description = "The project ID of the instance boot group") + private String projectId; + + @SerializedName(ApiConstants.PROJECT) + @Param(description = "The project name of the instance boot group") + private String projectName; + + @SerializedName(ApiConstants.READINESS_ATTEMPT_TIMEOUT_SECONDS) + @Param(description = "Effective timeout in seconds for each readiness retry attempt (per-boot-group override if set, else the global default)") + private long readinessAttemptTimeoutSeconds; + + @SerializedName(ApiConstants.READINESS_MAX_RETRY_ATTEMPTS) + @Param(description = "Effective maximum number of readiness retry attempts (per-boot-group override if set, else the global default)") + private long readinessMaxRetryAttempts; + + @SerializedName(ApiConstants.READINESS_REBOOT_ON_RETRY) + @Param(description = "Effective setting for whether an instance is rebooted between readiness retry attempts (per-boot-group override if set, else the global default)") + private boolean readinessRebootOnRetry; + + @SerializedName(ApiConstants.READINESS_INITIAL_DELAY_SECONDS) + @Param(description = "Effective delay in seconds after starting or rebooting an instance before its first readiness check of that attempt (per-boot-group override if set, else the global default)") + private long readinessInitialDelaySeconds; + + public void setId(String id) { + this.id = id; + } + + public void setName(String name) { + this.name = name; + } + + public void setDescription(String description) { + this.description = description; + } + + public void setCreated(Date created) { + this.created = created; + } + + public void setAccountId(String accountId) { + this.accountId = accountId; + } + + @Override + public void setAccountName(String accountName) { + this.accountName = accountName; + } + + @Override + public void setDomainId(String domainId) { + this.domainId = domainId; + } + + @Override + public void setDomainName(String domainName) { + this.domainName = domainName; + } + + @Override + public void setDomainPath(String domainPath) { + this.domainPath = domainPath; + } + + @Override + public void setProjectId(String projectId) { + this.projectId = projectId; + } + + @Override + public void setProjectName(String projectName) { + this.projectName = projectName; + } + + public void setReadinessAttemptTimeoutSeconds(long readinessAttemptTimeoutSeconds) { + this.readinessAttemptTimeoutSeconds = readinessAttemptTimeoutSeconds; + } + + public void setReadinessMaxRetryAttempts(long readinessMaxRetryAttempts) { + this.readinessMaxRetryAttempts = readinessMaxRetryAttempts; + } + + public void setReadinessRebootOnRetry(boolean readinessRebootOnRetry) { + this.readinessRebootOnRetry = readinessRebootOnRetry; + } + + public void setReadinessInitialDelaySeconds(long readinessInitialDelaySeconds) { + this.readinessInitialDelaySeconds = readinessInitialDelaySeconds; + } +} diff --git a/api/src/main/java/org/apache/cloudstack/vm/bootgroup/InstanceBootGroup.java b/api/src/main/java/org/apache/cloudstack/vm/bootgroup/InstanceBootGroup.java new file mode 100644 index 000000000000..f22a4f4b8e98 --- /dev/null +++ b/api/src/main/java/org/apache/cloudstack/vm/bootgroup/InstanceBootGroup.java @@ -0,0 +1,33 @@ +// 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.vm.bootgroup; + +import java.util.Date; + +import org.apache.cloudstack.acl.ControlledEntity; +import org.apache.cloudstack.api.Identity; +import org.apache.cloudstack.api.InternalIdentity; + +public interface InstanceBootGroup extends ControlledEntity, Identity, InternalIdentity { + + String getName(); + + String getDescription(); + + Date getCreated(); +} diff --git a/api/src/main/java/org/apache/cloudstack/vm/bootgroup/InstanceBootGroupMember.java b/api/src/main/java/org/apache/cloudstack/vm/bootgroup/InstanceBootGroupMember.java new file mode 100644 index 000000000000..ae811ed2f3b2 --- /dev/null +++ b/api/src/main/java/org/apache/cloudstack/vm/bootgroup/InstanceBootGroupMember.java @@ -0,0 +1,44 @@ +// 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.vm.bootgroup; + +import java.util.Date; + +import org.apache.cloudstack.api.Identity; +import org.apache.cloudstack.api.InternalIdentity; + +public interface InstanceBootGroupMember extends Identity, InternalIdentity { + + long getBootGroupId(); + + MemberType getMemberType(); + + long getMemberId(); + + int getOrder(); + + Date getCreated(); + + enum MemberType { + VirtualMachine, InstanceGroup + } + + enum ReadinessMode { + None, RuleBased, ChildDependent + } +} diff --git a/api/src/main/java/org/apache/cloudstack/vm/bootgroup/InstanceBootGroupService.java b/api/src/main/java/org/apache/cloudstack/vm/bootgroup/InstanceBootGroupService.java new file mode 100644 index 000000000000..111bf4c50840 --- /dev/null +++ b/api/src/main/java/org/apache/cloudstack/vm/bootgroup/InstanceBootGroupService.java @@ -0,0 +1,81 @@ +// 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.vm.bootgroup; + +import org.apache.cloudstack.api.command.user.bootgroup.AddMemberToInstanceBootGroupCmd; +import org.apache.cloudstack.api.command.user.bootgroup.CreateInstanceBootGroupCmd; +import org.apache.cloudstack.api.command.user.bootgroup.CreateInstanceBootGroupReadinessRuleCmd; +import org.apache.cloudstack.api.command.user.bootgroup.DeleteInstanceBootGroupCmd; +import org.apache.cloudstack.api.command.user.bootgroup.DeleteInstanceBootGroupReadinessRuleCmd; +import org.apache.cloudstack.api.command.user.bootgroup.ListInstanceBootGroupMembersCmd; +import org.apache.cloudstack.api.command.user.bootgroup.ListInstanceBootGroupReadinessRulesCmd; +import org.apache.cloudstack.api.command.user.bootgroup.ListInstanceBootGroupsCmd; +import org.apache.cloudstack.api.command.user.bootgroup.RebootInstanceBootGroupCmd; +import org.apache.cloudstack.api.command.user.bootgroup.RemoveInstanceBootGroupMemberCmd; +import org.apache.cloudstack.api.command.user.bootgroup.StartInstanceBootGroupCmd; +import org.apache.cloudstack.api.command.user.bootgroup.StopInstanceBootGroupCmd; +import org.apache.cloudstack.api.command.user.bootgroup.UpdateInstanceBootGroupCmd; +import org.apache.cloudstack.api.command.user.bootgroup.UpdateInstanceBootGroupMemberCmd; +import org.apache.cloudstack.api.command.user.bootgroup.UpdateInstanceBootGroupReadinessRuleCmd; +import org.apache.cloudstack.api.response.InstanceBootGroupMemberResponse; +import org.apache.cloudstack.api.response.InstanceBootGroupReadinessRuleResponse; +import org.apache.cloudstack.api.response.InstanceBootGroupResponse; +import org.apache.cloudstack.api.response.ListResponse; +import org.apache.cloudstack.vm.bootgroup.readiness.InstanceBootGroupReadinessRule; + +public interface InstanceBootGroupService { + + InstanceBootGroup createInstanceBootGroup(CreateInstanceBootGroupCmd cmd); + + boolean deleteInstanceBootGroup(DeleteInstanceBootGroupCmd cmd); + + InstanceBootGroup updateInstanceBootGroup(UpdateInstanceBootGroupCmd cmd); + + ListResponse listInstanceBootGroups(ListInstanceBootGroupsCmd cmd); + + InstanceBootGroupMember addMemberToInstanceBootGroup(AddMemberToInstanceBootGroupCmd cmd); + + boolean removeInstanceBootGroupMember(RemoveInstanceBootGroupMemberCmd cmd); + + InstanceBootGroupMember updateInstanceBootGroupMember(UpdateInstanceBootGroupMemberCmd cmd); + + ListResponse listInstanceBootGroupMembers(ListInstanceBootGroupMembersCmd cmd); + + InstanceBootGroup startInstanceBootGroup(StartInstanceBootGroupCmd cmd); + + InstanceBootGroup stopInstanceBootGroup(StopInstanceBootGroupCmd cmd); + + InstanceBootGroup rebootInstanceBootGroup(RebootInstanceBootGroupCmd cmd); + + InstanceBootGroupResponse createInstanceBootGroupResponse(long id); + + InstanceBootGroupMemberResponse createInstanceBootGroupMemberResponse(InstanceBootGroupMember member); + + InstanceBootGroupReadinessRule createInstanceBootGroupReadinessRule(CreateInstanceBootGroupReadinessRuleCmd cmd); + + InstanceBootGroupReadinessRule updateInstanceBootGroupReadinessRule(UpdateInstanceBootGroupReadinessRuleCmd cmd); + + boolean deleteInstanceBootGroupReadinessRule(DeleteInstanceBootGroupReadinessRuleCmd cmd); + + ListResponse listInstanceBootGroupReadinessRules(ListInstanceBootGroupReadinessRulesCmd cmd); + + InstanceBootGroupReadinessRuleResponse createInstanceBootGroupReadinessRuleResponse(InstanceBootGroupReadinessRule rule); + + Long getInstanceBootGroupIdForMember(long memberId); + +} diff --git a/api/src/main/java/org/apache/cloudstack/vm/bootgroup/readiness/InstanceBootGroupReadinessRule.java b/api/src/main/java/org/apache/cloudstack/vm/bootgroup/readiness/InstanceBootGroupReadinessRule.java new file mode 100644 index 000000000000..7a2eb4f4f710 --- /dev/null +++ b/api/src/main/java/org/apache/cloudstack/vm/bootgroup/readiness/InstanceBootGroupReadinessRule.java @@ -0,0 +1,80 @@ +// 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.vm.bootgroup.readiness; + +import java.util.Date; + +import org.apache.cloudstack.api.Identity; +import org.apache.cloudstack.api.InternalIdentity; +import org.apache.cloudstack.vm.bootgroup.InstanceBootGroupMember; + +/** + * A readiness rule always belongs to exactly one boot group and references one "item" within it: + * a direct {@code VirtualMachine}-type member, a direct {@code InstanceGroup}-type member, or a VM + * sitting inside one of the boot group's {@code InstanceGroup}-type members (no member row of its + * own, referenced directly by VM id). + */ +public interface InstanceBootGroupReadinessRule extends Identity, InternalIdentity { + + long getBootGroupId(); + + InstanceBootGroupMember.MemberType getItemType(); + + long getItemId(); + + RuleType getRuleType(); + + String getName(); + + boolean isEnabled(); + + Date getCreated(); + + /** + * Which rule types are valid depends on {@link InstanceBootGroupMember.MemberType}, + * hypervisor-agnostic at the DB/API layer (no qemu/KVM naming stored). Ping/PortCheck/ + * CustomScript/GuestAgentLiveness apply to VirtualMachine items; MemberQuorum and + * (group-scope) CustomScript apply to InstanceGroup items. + */ + enum RuleType { + GuestAgentLiveness(true), + Ping(true), + PortCheck(true), + CustomScript(false), + MemberQuorum(false); + + private final boolean memberTargeted; + + RuleType(boolean memberTargeted) { + this.memberTargeted = memberTargeted; + } + + /** + * True for a rule type that, when attached to an InstanceGroup, is evaluated against every + * current member individually and inherited by each member's own readiness — unlike + * MemberQuorum/(group-scope) CustomScript, which only ever operate at group scope. + */ + public boolean isMemberTargeted() { + return memberTargeted; + } + } + + enum Status { + Ready, NotReady, Error, Unknown + } +} diff --git a/api/src/main/java/org/apache/cloudstack/vm/bootgroup/readiness/ReadinessChecker.java b/api/src/main/java/org/apache/cloudstack/vm/bootgroup/readiness/ReadinessChecker.java new file mode 100644 index 000000000000..cb66ad2b1d82 --- /dev/null +++ b/api/src/main/java/org/apache/cloudstack/vm/bootgroup/readiness/ReadinessChecker.java @@ -0,0 +1,93 @@ +// 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.vm.bootgroup.readiness; + +import java.util.Map; + +import org.apache.logging.log4j.Level; +import org.apache.logging.log4j.Logger; + +/** + * Strategy interface for evaluating one readiness rule type. Implementations are collected by + * {@code InstanceBootGroupReadinessRuleManagerImpl} via Spring's {@code List} + * autowiring and dispatched by {@link #getRuleType()} — an internal implementation detail, not + * API-facing, so left unprefixed. + */ +public interface ReadinessChecker { + + /** + * Below this much remaining budget, a checker should not even attempt to dispatch a remote + * command — there isn't enough time left for a meaningful wait, and dispatching anyway would + * either use an unhelpfully tiny (or, worse, a zero/negative, which some transports treat as "no + * override, use the default") wait value. + */ + long MIN_REMAINING_MS_TO_DISPATCH = 2000L; + + InstanceBootGroupReadinessRule.RuleType getRuleType(); + + /** + * @param remainingMs time budget left for this VM's current attempt; bound any remote dispatch + * to it (e.g. via {@code Command.setWait}) and return {@code Status.Error} directly if + * it's already too small to be worth dispatching. + */ + Result check(InstanceBootGroupReadinessRule rule, Map details, long vmId, long remainingMs); + + class Result { + private final InstanceBootGroupReadinessRule.Status status; + private final String message; + + public Result(InstanceBootGroupReadinessRule.Status status, String message) { + this.status = status; + this.message = message; + } + + public InstanceBootGroupReadinessRule.Status getStatus() { + return status; + } + + public String getMessage() { + return message; + } + } + + default Logger getLogger() { + return null; + } + + default Result logAndReturn(InstanceBootGroupReadinessRule rule, Object vmOrId, Result result) { + Logger log = getLogger(); + if (log == null) { + return result; + } + Level level = InstanceBootGroupReadinessRule.Status.Ready.equals(result.getStatus()) + ? Level.DEBUG + : Level.WARN; + + log.log(level, "{} evaluated for {}: status={}, message={}", + rule, vmOrId, result.getStatus(), result.getMessage()); + return result; + } + + /** + * Halves the remaining budget and floors at 1 second for {@code Command.setWait}, which doubles + * whatever value it's given and treats 0 as "use the global default" rather than "no wait". + */ + default int computeWaitSeconds(long remainingMs) { + return (int) Math.max(1, remainingMs / 2000); + } +} diff --git a/api/src/test/java/org/apache/cloudstack/api/command/user/bootgroup/AddMemberToInstanceBootGroupCmdTest.java b/api/src/test/java/org/apache/cloudstack/api/command/user/bootgroup/AddMemberToInstanceBootGroupCmdTest.java new file mode 100644 index 000000000000..45880112bcad --- /dev/null +++ b/api/src/test/java/org/apache/cloudstack/api/command/user/bootgroup/AddMemberToInstanceBootGroupCmdTest.java @@ -0,0 +1,95 @@ +// 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.command.user.bootgroup; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNotNull; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +import org.apache.cloudstack.api.ApiCommandResourceType; +import org.apache.cloudstack.api.ServerApiException; +import org.apache.cloudstack.api.response.InstanceBootGroupMemberResponse; +import org.apache.cloudstack.vm.bootgroup.InstanceBootGroupMember; +import org.junit.Test; + +public class AddMemberToInstanceBootGroupCmdTest extends BaseBootGroupCmdTest { + + private AddMemberToInstanceBootGroupCmd createCmd() throws Exception { + AddMemberToInstanceBootGroupCmd cmd = new AddMemberToInstanceBootGroupCmd(); + setField(cmd, "instanceBootGroupService", instanceBootGroupService); + setField(cmd, "id", ENTITY_ID); + setField(cmd, "virtualMachineId", 500L); + setField(cmd, "order", 2); + return cmd; + } + + @Test + public void testAccessors() throws Exception { + AddMemberToInstanceBootGroupCmd cmd = createCmd(); + assertEquals(Long.valueOf(ENTITY_ID), cmd.getId()); + assertEquals(Long.valueOf(500L), cmd.getVirtualMachineId()); + assertEquals(2, cmd.getOrder()); + } + + @Test + public void testGetEntityOwnerId() throws Exception { + AddMemberToInstanceBootGroupCmd cmd = createCmd(); + assertEquals(ACCOUNT_ID, cmd.getEntityOwnerId()); + } + + @Test + public void testGetApiResourceType() throws Exception { + AddMemberToInstanceBootGroupCmd cmd = createCmd(); + assertEquals(ApiCommandResourceType.InstanceBootGroup, cmd.getApiResourceType()); + assertEquals(Long.valueOf(ENTITY_ID), cmd.getApiResourceId()); + } + + @Test + public void testExecuteSuccess() throws Exception { + AddMemberToInstanceBootGroupCmd cmd = createCmd(); + + InstanceBootGroupMember member = mock(InstanceBootGroupMember.class); + InstanceBootGroupMemberResponse mockResponse = new InstanceBootGroupMemberResponse(); + + when(instanceBootGroupService.addMemberToInstanceBootGroup(cmd)).thenReturn(member); + when(instanceBootGroupService.createInstanceBootGroupMemberResponse(member)).thenReturn(mockResponse); + + cmd.execute(); + + InstanceBootGroupMemberResponse response = (InstanceBootGroupMemberResponse) cmd.getResponseObject(); + assertNotNull(response); + assertEquals("addmembertoinstancebootgroupresponse", response.getResponseName()); + verify(instanceBootGroupService).addMemberToInstanceBootGroup(cmd); + verify(instanceBootGroupService).createInstanceBootGroupMemberResponse(member); + } + + @Test(expected = ServerApiException.class) + public void testExecuteReturnsNull() throws Exception { + AddMemberToInstanceBootGroupCmd cmd = createCmd(); + when(instanceBootGroupService.addMemberToInstanceBootGroup(cmd)).thenReturn(null); + cmd.execute(); + } + + @Test(expected = RuntimeException.class) + public void testExecutePropagatesServiceException() throws Exception { + AddMemberToInstanceBootGroupCmd cmd = createCmd(); + when(instanceBootGroupService.addMemberToInstanceBootGroup(cmd)).thenThrow(new RuntimeException("conflict")); + cmd.execute(); + } +} diff --git a/api/src/test/java/org/apache/cloudstack/api/command/user/bootgroup/BaseBootGroupCmdTest.java b/api/src/test/java/org/apache/cloudstack/api/command/user/bootgroup/BaseBootGroupCmdTest.java new file mode 100644 index 000000000000..f3d62d207733 --- /dev/null +++ b/api/src/test/java/org/apache/cloudstack/api/command/user/bootgroup/BaseBootGroupCmdTest.java @@ -0,0 +1,88 @@ +// 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.command.user.bootgroup; + +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + +import java.lang.reflect.Field; + +import org.apache.cloudstack.context.CallContext; +import org.apache.cloudstack.vm.bootgroup.InstanceBootGroupService; +import org.junit.After; +import org.junit.Before; +import org.mockito.MockedStatic; +import org.mockito.Mockito; + +import com.cloud.user.Account; +import com.cloud.user.AccountService; + +/** + * Shared setup for all Instance Boot Group command unit tests. + */ +public abstract class BaseBootGroupCmdTest { + + protected static final long ACCOUNT_ID = 42L; + protected static final long ENTITY_ID = 100L; + + protected InstanceBootGroupService instanceBootGroupService; + protected AccountService accountService; + protected Account callingAccount; + + private MockedStatic callContextMock; + + @Before + public void setUp() { + instanceBootGroupService = mock(InstanceBootGroupService.class); + accountService = mock(AccountService.class); + + callingAccount = mock(Account.class); + when(callingAccount.getId()).thenReturn(ACCOUNT_ID); + + CallContext callContext = mock(CallContext.class); + when(callContext.getCallingAccount()).thenReturn(callingAccount); + + callContextMock = Mockito.mockStatic(CallContext.class); + callContextMock.when(CallContext::current).thenReturn(callContext); + } + + @After + public void tearDown() { + callContextMock.close(); + } + + /** + * Sets a private/inherited field value via reflection. + */ + protected void setField(Object target, String fieldName, Object value) throws Exception { + Field field = null; + Class clazz = target.getClass(); + while (clazz != null) { + try { + field = clazz.getDeclaredField(fieldName); + break; + } catch (NoSuchFieldException e) { + clazz = clazz.getSuperclass(); + } + } + if (field == null) { + throw new NoSuchFieldException(fieldName + " not found in hierarchy of " + target.getClass().getName()); + } + field.setAccessible(true); + field.set(target, value); + } +} diff --git a/api/src/test/java/org/apache/cloudstack/api/command/user/bootgroup/CreateInstanceBootGroupCmdTest.java b/api/src/test/java/org/apache/cloudstack/api/command/user/bootgroup/CreateInstanceBootGroupCmdTest.java new file mode 100644 index 000000000000..c6352dcc94ef --- /dev/null +++ b/api/src/test/java/org/apache/cloudstack/api/command/user/bootgroup/CreateInstanceBootGroupCmdTest.java @@ -0,0 +1,107 @@ +// 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.command.user.bootgroup; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNotNull; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +import org.apache.cloudstack.api.ServerApiException; +import org.apache.cloudstack.api.response.InstanceBootGroupResponse; +import org.apache.cloudstack.vm.bootgroup.InstanceBootGroup; +import org.junit.Test; + +public class CreateInstanceBootGroupCmdTest extends BaseBootGroupCmdTest { + + private CreateInstanceBootGroupCmd createCmd() throws Exception { + CreateInstanceBootGroupCmd cmd = new CreateInstanceBootGroupCmd(); + setField(cmd, "instanceBootGroupService", instanceBootGroupService); + setField(cmd, "_accountService", accountService); + setField(cmd, "name", "web-tier"); + setField(cmd, "description", "web tier boot group"); + setField(cmd, "readinessAttemptTimeoutSeconds", 120L); + setField(cmd, "readinessMaxRetryAttempts", 3L); + setField(cmd, "readinessRebootOnRetry", true); + setField(cmd, "readinessInitialDelaySeconds", 15L); + return cmd; + } + + @Test + public void testAccessors() throws Exception { + CreateInstanceBootGroupCmd cmd = createCmd(); + assertEquals("web-tier", cmd.getName()); + assertEquals("web tier boot group", cmd.getDescription()); + assertEquals(Long.valueOf(120L), cmd.getReadinessAttemptTimeoutSeconds()); + assertEquals(Long.valueOf(3L), cmd.getReadinessMaxRetryAttempts()); + assertEquals(Boolean.TRUE, cmd.getReadinessRebootOnRetry()); + assertEquals(Long.valueOf(15L), cmd.getReadinessInitialDelaySeconds()); + } + + @Test + public void testGetEntityOwnerIdResolvedFromAccount() throws Exception { + CreateInstanceBootGroupCmd cmd = createCmd(); + setField(cmd, "accountName", "someaccount"); + setField(cmd, "domainId", 5L); + when(accountService.finalizeAccountId("someaccount", 5L, null, true)).thenReturn(200L); + + assertEquals(200L, cmd.getEntityOwnerId()); + } + + @Test + public void testGetEntityOwnerIdFallsBackToCaller() throws Exception { + CreateInstanceBootGroupCmd cmd = createCmd(); + when(accountService.finalizeAccountId(null, null, null, true)).thenReturn(null); + + assertEquals(ACCOUNT_ID, cmd.getEntityOwnerId()); + } + + @Test + public void testExecuteSuccess() throws Exception { + CreateInstanceBootGroupCmd cmd = createCmd(); + + InstanceBootGroup group = mock(InstanceBootGroup.class); + when(group.getId()).thenReturn(ENTITY_ID); + InstanceBootGroupResponse mockResponse = new InstanceBootGroupResponse(); + + when(instanceBootGroupService.createInstanceBootGroup(cmd)).thenReturn(group); + when(instanceBootGroupService.createInstanceBootGroupResponse(ENTITY_ID)).thenReturn(mockResponse); + + cmd.execute(); + + InstanceBootGroupResponse response = (InstanceBootGroupResponse) cmd.getResponseObject(); + assertNotNull(response); + assertEquals("createinstancebootgroupresponse", response.getResponseName()); + verify(instanceBootGroupService).createInstanceBootGroup(cmd); + verify(instanceBootGroupService).createInstanceBootGroupResponse(ENTITY_ID); + } + + @Test(expected = ServerApiException.class) + public void testExecuteReturnsNull() throws Exception { + CreateInstanceBootGroupCmd cmd = createCmd(); + when(instanceBootGroupService.createInstanceBootGroup(cmd)).thenReturn(null); + cmd.execute(); + } + + @Test(expected = RuntimeException.class) + public void testExecutePropagatesServiceException() throws Exception { + CreateInstanceBootGroupCmd cmd = createCmd(); + when(instanceBootGroupService.createInstanceBootGroup(cmd)).thenThrow(new RuntimeException("db error")); + cmd.execute(); + } +} diff --git a/api/src/test/java/org/apache/cloudstack/api/command/user/bootgroup/CreateInstanceBootGroupReadinessRuleCmdTest.java b/api/src/test/java/org/apache/cloudstack/api/command/user/bootgroup/CreateInstanceBootGroupReadinessRuleCmdTest.java new file mode 100644 index 000000000000..7efb8ec4c797 --- /dev/null +++ b/api/src/test/java/org/apache/cloudstack/api/command/user/bootgroup/CreateInstanceBootGroupReadinessRuleCmdTest.java @@ -0,0 +1,158 @@ +// 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.command.user.bootgroup; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertNull; +import static org.junit.Assert.assertTrue; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +import java.util.HashMap; +import java.util.Map; + +import org.apache.cloudstack.api.ApiCommandResourceType; +import org.apache.cloudstack.api.ServerApiException; +import org.apache.cloudstack.api.response.InstanceBootGroupReadinessRuleResponse; +import org.apache.cloudstack.vm.bootgroup.readiness.InstanceBootGroupReadinessRule; +import org.junit.Test; + +public class CreateInstanceBootGroupReadinessRuleCmdTest extends BaseBootGroupCmdTest { + + private CreateInstanceBootGroupReadinessRuleCmd createCmd() throws Exception { + CreateInstanceBootGroupReadinessRuleCmd cmd = new CreateInstanceBootGroupReadinessRuleCmd(); + setField(cmd, "instanceBootGroupService", instanceBootGroupService); + setField(cmd, "bootGroupId", ENTITY_ID); + setField(cmd, "virtualMachineId", 500L); + setField(cmd, "instanceGroupId", null); + setField(cmd, "ruleType", "PortCheck"); + setField(cmd, "name", "port-check-rule"); + return cmd; + } + + @Test + public void testAccessors() throws Exception { + CreateInstanceBootGroupReadinessRuleCmd cmd = createCmd(); + assertEquals(Long.valueOf(ENTITY_ID), cmd.getBootGroupId()); + assertEquals(Long.valueOf(500L), cmd.getVirtualMachineId()); + assertNull(cmd.getInstanceGroupId()); + assertEquals("PortCheck", cmd.getRuleType()); + assertEquals("port-check-rule", cmd.getName()); + } + + @Test + public void testGetEntityOwnerId() throws Exception { + CreateInstanceBootGroupReadinessRuleCmd cmd = createCmd(); + assertEquals(ACCOUNT_ID, cmd.getEntityOwnerId()); + } + + @Test + public void testGetApiResourceType() throws Exception { + CreateInstanceBootGroupReadinessRuleCmd cmd = createCmd(); + assertEquals(ApiCommandResourceType.InstanceBootGroupReadinessRule, cmd.getApiResourceType()); + } + + @Test + public void testIsEnabledDefaultsToTrueWhenNull() throws Exception { + CreateInstanceBootGroupReadinessRuleCmd cmd = createCmd(); + setField(cmd, "enabled", null); + assertTrue(cmd.isEnabled()); + } + + @Test + public void testIsEnabledFalse() throws Exception { + CreateInstanceBootGroupReadinessRuleCmd cmd = createCmd(); + setField(cmd, "enabled", false); + assertFalse(cmd.isEnabled()); + } + + @Test + public void testIsEnabledTrue() throws Exception { + CreateInstanceBootGroupReadinessRuleCmd cmd = createCmd(); + setField(cmd, "enabled", true); + assertTrue(cmd.isEnabled()); + } + + @Test + public void testGetDetailsNull() throws Exception { + CreateInstanceBootGroupReadinessRuleCmd cmd = createCmd(); + setField(cmd, "details", null); + assertNull(cmd.getDetails()); + } + + @Test + public void testGetDetailsEmptyMap() throws Exception { + CreateInstanceBootGroupReadinessRuleCmd cmd = createCmd(); + setField(cmd, "details", new HashMap<>()); + assertNull(cmd.getDetails()); + } + + @Test + public void testGetDetailsPopulatedMap() throws Exception { + CreateInstanceBootGroupReadinessRuleCmd cmd = createCmd(); + + Map innerMap = new HashMap<>(); + innerMap.put("port", "8080"); + innerMap.put("protocol", "tcp"); + + Map> outerMap = new HashMap<>(); + outerMap.put("0", innerMap); + + setField(cmd, "details", outerMap); + + Map details = cmd.getDetails(); + assertNotNull(details); + assertEquals("8080", details.get("port")); + assertEquals("tcp", details.get("protocol")); + } + + @Test + public void testExecuteSuccess() throws Exception { + CreateInstanceBootGroupReadinessRuleCmd cmd = createCmd(); + + InstanceBootGroupReadinessRule rule = mock(InstanceBootGroupReadinessRule.class); + InstanceBootGroupReadinessRuleResponse mockResponse = new InstanceBootGroupReadinessRuleResponse(); + + when(instanceBootGroupService.createInstanceBootGroupReadinessRule(cmd)).thenReturn(rule); + when(instanceBootGroupService.createInstanceBootGroupReadinessRuleResponse(rule)).thenReturn(mockResponse); + + cmd.execute(); + + InstanceBootGroupReadinessRuleResponse response = (InstanceBootGroupReadinessRuleResponse) cmd.getResponseObject(); + assertNotNull(response); + assertEquals("createinstancebootgroupreadinessruleresponse", response.getResponseName()); + verify(instanceBootGroupService).createInstanceBootGroupReadinessRule(cmd); + verify(instanceBootGroupService).createInstanceBootGroupReadinessRuleResponse(rule); + } + + @Test(expected = ServerApiException.class) + public void testExecuteReturnsNull() throws Exception { + CreateInstanceBootGroupReadinessRuleCmd cmd = createCmd(); + when(instanceBootGroupService.createInstanceBootGroupReadinessRule(cmd)).thenReturn(null); + cmd.execute(); + } + + @Test(expected = RuntimeException.class) + public void testExecutePropagatesServiceException() throws Exception { + CreateInstanceBootGroupReadinessRuleCmd cmd = createCmd(); + when(instanceBootGroupService.createInstanceBootGroupReadinessRule(cmd)).thenThrow(new RuntimeException("db error")); + cmd.execute(); + } +} diff --git a/api/src/test/java/org/apache/cloudstack/api/command/user/bootgroup/DeleteInstanceBootGroupCmdTest.java b/api/src/test/java/org/apache/cloudstack/api/command/user/bootgroup/DeleteInstanceBootGroupCmdTest.java new file mode 100644 index 000000000000..503efc5e94b1 --- /dev/null +++ b/api/src/test/java/org/apache/cloudstack/api/command/user/bootgroup/DeleteInstanceBootGroupCmdTest.java @@ -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 org.apache.cloudstack.api.command.user.bootgroup; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNotNull; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +import org.apache.cloudstack.api.ApiCommandResourceType; +import org.apache.cloudstack.api.ServerApiException; +import org.apache.cloudstack.api.response.SuccessResponse; +import org.junit.Test; + +public class DeleteInstanceBootGroupCmdTest extends BaseBootGroupCmdTest { + + private DeleteInstanceBootGroupCmd createCmd() throws Exception { + DeleteInstanceBootGroupCmd cmd = new DeleteInstanceBootGroupCmd(); + setField(cmd, "instanceBootGroupService", instanceBootGroupService); + setField(cmd, "id", ENTITY_ID); + return cmd; + } + + @Test + public void testAccessors() throws Exception { + DeleteInstanceBootGroupCmd cmd = createCmd(); + assertEquals(Long.valueOf(ENTITY_ID), cmd.getId()); + } + + @Test + public void testGetEntityOwnerId() throws Exception { + DeleteInstanceBootGroupCmd cmd = createCmd(); + assertEquals(ACCOUNT_ID, cmd.getEntityOwnerId()); + } + + @Test + public void testGetApiResourceType() throws Exception { + DeleteInstanceBootGroupCmd cmd = createCmd(); + assertEquals(ApiCommandResourceType.InstanceBootGroup, cmd.getApiResourceType()); + assertEquals(Long.valueOf(ENTITY_ID), cmd.getApiResourceId()); + } + + @Test + public void testExecuteSuccess() throws Exception { + DeleteInstanceBootGroupCmd cmd = createCmd(); + when(instanceBootGroupService.deleteInstanceBootGroup(cmd)).thenReturn(true); + + cmd.execute(); + + SuccessResponse response = (SuccessResponse) cmd.getResponseObject(); + assertNotNull(response); + assertEquals("deleteinstancebootgroupresponse", response.getResponseName()); + verify(instanceBootGroupService).deleteInstanceBootGroup(cmd); + } + + @Test(expected = ServerApiException.class) + public void testExecuteReturnsFalse() throws Exception { + DeleteInstanceBootGroupCmd cmd = createCmd(); + when(instanceBootGroupService.deleteInstanceBootGroup(cmd)).thenReturn(false); + cmd.execute(); + } + + @Test(expected = RuntimeException.class) + public void testExecutePropagatesServiceException() throws Exception { + DeleteInstanceBootGroupCmd cmd = createCmd(); + when(instanceBootGroupService.deleteInstanceBootGroup(cmd)).thenThrow(new RuntimeException("db error")); + cmd.execute(); + } +} diff --git a/api/src/test/java/org/apache/cloudstack/api/command/user/bootgroup/DeleteInstanceBootGroupReadinessRuleCmdTest.java b/api/src/test/java/org/apache/cloudstack/api/command/user/bootgroup/DeleteInstanceBootGroupReadinessRuleCmdTest.java new file mode 100644 index 000000000000..d31803703a3e --- /dev/null +++ b/api/src/test/java/org/apache/cloudstack/api/command/user/bootgroup/DeleteInstanceBootGroupReadinessRuleCmdTest.java @@ -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 org.apache.cloudstack.api.command.user.bootgroup; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNotNull; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +import org.apache.cloudstack.api.ApiCommandResourceType; +import org.apache.cloudstack.api.ServerApiException; +import org.apache.cloudstack.api.response.SuccessResponse; +import org.junit.Test; + +public class DeleteInstanceBootGroupReadinessRuleCmdTest extends BaseBootGroupCmdTest { + + private DeleteInstanceBootGroupReadinessRuleCmd createCmd() throws Exception { + DeleteInstanceBootGroupReadinessRuleCmd cmd = new DeleteInstanceBootGroupReadinessRuleCmd(); + setField(cmd, "instanceBootGroupService", instanceBootGroupService); + setField(cmd, "id", ENTITY_ID); + return cmd; + } + + @Test + public void testAccessors() throws Exception { + DeleteInstanceBootGroupReadinessRuleCmd cmd = createCmd(); + assertEquals(Long.valueOf(ENTITY_ID), cmd.getId()); + } + + @Test + public void testGetEntityOwnerId() throws Exception { + DeleteInstanceBootGroupReadinessRuleCmd cmd = createCmd(); + assertEquals(ACCOUNT_ID, cmd.getEntityOwnerId()); + } + + @Test + public void testGetApiResourceType() throws Exception { + DeleteInstanceBootGroupReadinessRuleCmd cmd = createCmd(); + assertEquals(ApiCommandResourceType.InstanceBootGroupReadinessRule, cmd.getApiResourceType()); + assertEquals(Long.valueOf(ENTITY_ID), cmd.getApiResourceId()); + } + + @Test + public void testExecuteSuccess() throws Exception { + DeleteInstanceBootGroupReadinessRuleCmd cmd = createCmd(); + when(instanceBootGroupService.deleteInstanceBootGroupReadinessRule(cmd)).thenReturn(true); + + cmd.execute(); + + SuccessResponse response = (SuccessResponse) cmd.getResponseObject(); + assertNotNull(response); + assertEquals("deleteinstancebootgroupreadinessruleresponse", response.getResponseName()); + verify(instanceBootGroupService).deleteInstanceBootGroupReadinessRule(cmd); + } + + @Test(expected = ServerApiException.class) + public void testExecuteReturnsFalse() throws Exception { + DeleteInstanceBootGroupReadinessRuleCmd cmd = createCmd(); + when(instanceBootGroupService.deleteInstanceBootGroupReadinessRule(cmd)).thenReturn(false); + cmd.execute(); + } + + @Test(expected = RuntimeException.class) + public void testExecutePropagatesServiceException() throws Exception { + DeleteInstanceBootGroupReadinessRuleCmd cmd = createCmd(); + when(instanceBootGroupService.deleteInstanceBootGroupReadinessRule(cmd)).thenThrow(new RuntimeException("db error")); + cmd.execute(); + } +} diff --git a/api/src/test/java/org/apache/cloudstack/api/command/user/bootgroup/ListInstanceBootGroupMembersCmdTest.java b/api/src/test/java/org/apache/cloudstack/api/command/user/bootgroup/ListInstanceBootGroupMembersCmdTest.java new file mode 100644 index 000000000000..3a9f34fef4d6 --- /dev/null +++ b/api/src/test/java/org/apache/cloudstack/api/command/user/bootgroup/ListInstanceBootGroupMembersCmdTest.java @@ -0,0 +1,143 @@ +// 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.command.user.bootgroup; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertTrue; +import static org.mockito.Mockito.when; + +import java.util.Arrays; +import java.util.Collections; +import java.util.List; + +import org.apache.cloudstack.api.response.InstanceBootGroupMemberResponse; +import org.apache.cloudstack.api.response.ListResponse; +import org.junit.Test; + +public class ListInstanceBootGroupMembersCmdTest extends BaseBootGroupCmdTest { + + private ListInstanceBootGroupMembersCmd createCmd() throws Exception { + ListInstanceBootGroupMembersCmd cmd = new ListInstanceBootGroupMembersCmd(); + setField(cmd, "instanceBootGroupService", instanceBootGroupService); + setField(cmd, "bootGroupId", ENTITY_ID); + setField(cmd, "memberType", "VirtualMachine"); + return cmd; + } + + @Test + public void testAccessors() throws Exception { + ListInstanceBootGroupMembersCmd cmd = createCmd(); + assertEquals(Long.valueOf(ENTITY_ID), cmd.getBootGroupId()); + assertEquals("VirtualMachine", cmd.getMemberType()); + } + + @Test + public void testIsIgnoreInstanceStateNull() throws Exception { + ListInstanceBootGroupMembersCmd cmd = createCmd(); + assertFalse(cmd.isIgnoreInstanceState()); + } + + @Test + public void testIsIgnoreInstanceStateFalse() throws Exception { + ListInstanceBootGroupMembersCmd cmd = createCmd(); + setField(cmd, "ignoreInstanceState", false); + assertFalse(cmd.isIgnoreInstanceState()); + } + + @Test + public void testIsIgnoreInstanceStateTrue() throws Exception { + ListInstanceBootGroupMembersCmd cmd = createCmd(); + setField(cmd, "ignoreInstanceState", true); + assertTrue(cmd.isIgnoreInstanceState()); + } + + @Test + public void testDetailFlagsNullList() throws Exception { + ListInstanceBootGroupMembersCmd cmd = createCmd(); + assertFalse(cmd.isReadinessDetailRequested()); + assertFalse(cmd.isChildrenDetailRequested()); + } + + @Test + public void testDetailFlagsEmptyList() throws Exception { + ListInstanceBootGroupMembersCmd cmd = createCmd(); + setField(cmd, "viewDetails", Collections.emptyList()); + assertFalse(cmd.isReadinessDetailRequested()); + assertFalse(cmd.isChildrenDetailRequested()); + } + + @Test + public void testDetailFlagsListWithoutKeyword() throws Exception { + ListInstanceBootGroupMembersCmd cmd = createCmd(); + setField(cmd, "viewDetails", Collections.singletonList("other")); + assertFalse(cmd.isReadinessDetailRequested()); + assertFalse(cmd.isChildrenDetailRequested()); + } + + @Test + public void testDetailFlagsReadinessOnly() throws Exception { + ListInstanceBootGroupMembersCmd cmd = createCmd(); + List details = Collections.singletonList("readiness"); + setField(cmd, "viewDetails", details); + assertTrue(cmd.isReadinessDetailRequested()); + assertFalse(cmd.isChildrenDetailRequested()); + } + + @Test + public void testDetailFlagsChildrenOnly() throws Exception { + ListInstanceBootGroupMembersCmd cmd = createCmd(); + List details = Collections.singletonList("children"); + setField(cmd, "viewDetails", details); + assertFalse(cmd.isReadinessDetailRequested()); + assertTrue(cmd.isChildrenDetailRequested()); + } + + @Test + public void testDetailFlagsAll() throws Exception { + ListInstanceBootGroupMembersCmd cmd = createCmd(); + List details = Collections.singletonList("all"); + setField(cmd, "viewDetails", details); + assertTrue(cmd.isReadinessDetailRequested()); + assertTrue(cmd.isChildrenDetailRequested()); + } + + @Test + public void testDetailFlagsBothExplicit() throws Exception { + ListInstanceBootGroupMembersCmd cmd = createCmd(); + List details = Arrays.asList("readiness", "children"); + setField(cmd, "viewDetails", details); + assertTrue(cmd.isReadinessDetailRequested()); + assertTrue(cmd.isChildrenDetailRequested()); + } + + @Test + public void testExecute() throws Exception { + ListInstanceBootGroupMembersCmd cmd = createCmd(); + + ListResponse mockListResponse = new ListResponse<>(); + when(instanceBootGroupService.listInstanceBootGroupMembers(cmd)).thenReturn(mockListResponse); + + cmd.execute(); + + @SuppressWarnings("unchecked") + ListResponse response = (ListResponse) cmd.getResponseObject(); + assertNotNull(response); + assertEquals("listinstancebootgroupmembersresponse", response.getResponseName()); + } +} diff --git a/api/src/test/java/org/apache/cloudstack/api/command/user/bootgroup/ListInstanceBootGroupReadinessRulesCmdTest.java b/api/src/test/java/org/apache/cloudstack/api/command/user/bootgroup/ListInstanceBootGroupReadinessRulesCmdTest.java new file mode 100644 index 000000000000..92b6f22110ba --- /dev/null +++ b/api/src/test/java/org/apache/cloudstack/api/command/user/bootgroup/ListInstanceBootGroupReadinessRulesCmdTest.java @@ -0,0 +1,65 @@ +// 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.command.user.bootgroup; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNotNull; +import static org.mockito.Mockito.when; + +import org.apache.cloudstack.api.response.InstanceBootGroupReadinessRuleResponse; +import org.apache.cloudstack.api.response.ListResponse; +import org.junit.Test; + +public class ListInstanceBootGroupReadinessRulesCmdTest extends BaseBootGroupCmdTest { + + private ListInstanceBootGroupReadinessRulesCmd createCmd() throws Exception { + ListInstanceBootGroupReadinessRulesCmd cmd = new ListInstanceBootGroupReadinessRulesCmd(); + setField(cmd, "instanceBootGroupService", instanceBootGroupService); + setField(cmd, "bootGroupId", ENTITY_ID); + setField(cmd, "id", 700L); + setField(cmd, "virtualMachineId", 500L); + setField(cmd, "instanceGroupId", 600L); + setField(cmd, "ruleType", "Http"); + return cmd; + } + + @Test + public void testAccessors() throws Exception { + ListInstanceBootGroupReadinessRulesCmd cmd = createCmd(); + assertEquals(Long.valueOf(ENTITY_ID), cmd.getBootGroupId()); + assertEquals(Long.valueOf(700L), cmd.getId()); + assertEquals(Long.valueOf(500L), cmd.getVirtualMachineId()); + assertEquals(Long.valueOf(600L), cmd.getInstanceGroupId()); + assertEquals("Http", cmd.getRuleType()); + } + + @Test + public void testExecute() throws Exception { + ListInstanceBootGroupReadinessRulesCmd cmd = createCmd(); + + ListResponse mockListResponse = new ListResponse<>(); + when(instanceBootGroupService.listInstanceBootGroupReadinessRules(cmd)).thenReturn(mockListResponse); + + cmd.execute(); + + @SuppressWarnings("unchecked") + ListResponse response = + (ListResponse) cmd.getResponseObject(); + assertNotNull(response); + assertEquals("listinstancebootgroupreadinessrulesresponse", response.getResponseName()); + } +} diff --git a/api/src/test/java/org/apache/cloudstack/api/command/user/bootgroup/ListInstanceBootGroupsCmdTest.java b/api/src/test/java/org/apache/cloudstack/api/command/user/bootgroup/ListInstanceBootGroupsCmdTest.java new file mode 100644 index 000000000000..599cb05c3ba7 --- /dev/null +++ b/api/src/test/java/org/apache/cloudstack/api/command/user/bootgroup/ListInstanceBootGroupsCmdTest.java @@ -0,0 +1,62 @@ +// 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.command.user.bootgroup; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNotNull; +import static org.mockito.Mockito.when; + +import org.apache.cloudstack.api.response.InstanceBootGroupResponse; +import org.apache.cloudstack.api.response.ListResponse; +import org.junit.Test; + +public class ListInstanceBootGroupsCmdTest extends BaseBootGroupCmdTest { + + private ListInstanceBootGroupsCmd createCmd() throws Exception { + ListInstanceBootGroupsCmd cmd = new ListInstanceBootGroupsCmd(); + setField(cmd, "instanceBootGroupService", instanceBootGroupService); + setField(cmd, "id", ENTITY_ID); + setField(cmd, "keyword", "web"); + setField(cmd, "virtualMachineId", 500L); + setField(cmd, "instanceGroupId", 600L); + return cmd; + } + + @Test + public void testAccessors() throws Exception { + ListInstanceBootGroupsCmd cmd = createCmd(); + assertEquals(Long.valueOf(ENTITY_ID), cmd.getId()); + assertEquals("web", cmd.getKeyword()); + assertEquals(Long.valueOf(500L), cmd.getVirtualMachineId()); + assertEquals(Long.valueOf(600L), cmd.getInstanceGroupId()); + } + + @Test + public void testExecute() throws Exception { + ListInstanceBootGroupsCmd cmd = createCmd(); + + ListResponse mockListResponse = new ListResponse<>(); + when(instanceBootGroupService.listInstanceBootGroups(cmd)).thenReturn(mockListResponse); + + cmd.execute(); + + @SuppressWarnings("unchecked") + ListResponse response = (ListResponse) cmd.getResponseObject(); + assertNotNull(response); + assertEquals("listinstancebootgroupsresponse", response.getResponseName()); + } +} diff --git a/api/src/test/java/org/apache/cloudstack/api/command/user/bootgroup/RebootInstanceBootGroupCmdTest.java b/api/src/test/java/org/apache/cloudstack/api/command/user/bootgroup/RebootInstanceBootGroupCmdTest.java new file mode 100644 index 000000000000..f3b2ca10aff7 --- /dev/null +++ b/api/src/test/java/org/apache/cloudstack/api/command/user/bootgroup/RebootInstanceBootGroupCmdTest.java @@ -0,0 +1,118 @@ +// 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.command.user.bootgroup; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNotNull; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +import org.apache.cloudstack.api.ApiCommandResourceType; +import org.apache.cloudstack.api.ApiConstants; +import org.apache.cloudstack.api.ServerApiException; +import org.apache.cloudstack.api.response.InstanceBootGroupResponse; +import org.apache.cloudstack.context.CallContext; +import org.apache.cloudstack.vm.bootgroup.InstanceBootGroup; +import org.junit.Test; + +import com.cloud.event.EventTypes; +import com.cloud.utils.exception.CloudRuntimeException; + +public class RebootInstanceBootGroupCmdTest extends BaseBootGroupCmdTest { + + private RebootInstanceBootGroupCmd createCmd() throws Exception { + RebootInstanceBootGroupCmd cmd = new RebootInstanceBootGroupCmd(); + setField(cmd, "instanceBootGroupService", instanceBootGroupService); + setField(cmd, "id", ENTITY_ID); + return cmd; + } + + @Test + public void testAccessors() throws Exception { + RebootInstanceBootGroupCmd cmd = createCmd(); + assertEquals(Long.valueOf(ENTITY_ID), cmd.getId()); + } + + @Test + public void testGetEntityOwnerId() throws Exception { + RebootInstanceBootGroupCmd cmd = createCmd(); + assertEquals(ACCOUNT_ID, cmd.getEntityOwnerId()); + } + + @Test + public void testGetApiResourceType() throws Exception { + RebootInstanceBootGroupCmd cmd = createCmd(); + assertEquals(ApiCommandResourceType.InstanceBootGroup, cmd.getApiResourceType()); + assertEquals(Long.valueOf(ENTITY_ID), cmd.getApiResourceId()); + } + + @Test + public void testEventType() throws Exception { + RebootInstanceBootGroupCmd cmd = createCmd(); + assertEquals(EventTypes.EVENT_INSTANCE_BOOT_GROUP_REBOOT, cmd.getEventType()); + } + + @Test + public void testEventDescription() throws Exception { + RebootInstanceBootGroupCmd cmd = createCmd(); + when(CallContext.current().getApiResourceUuid(ApiConstants.ID)).thenReturn("group-uuid"); + + assertEquals("Rebooting instance boot group with ID: group-uuid", cmd.getEventDescription()); + } + + @Test + public void testExecuteSuccess() throws Exception { + RebootInstanceBootGroupCmd cmd = createCmd(); + + InstanceBootGroup group = mock(InstanceBootGroup.class); + when(group.getId()).thenReturn(ENTITY_ID); + InstanceBootGroupResponse mockResponse = new InstanceBootGroupResponse(); + + when(instanceBootGroupService.rebootInstanceBootGroup(cmd)).thenReturn(group); + when(instanceBootGroupService.createInstanceBootGroupResponse(ENTITY_ID)).thenReturn(mockResponse); + + cmd.execute(); + + InstanceBootGroupResponse response = (InstanceBootGroupResponse) cmd.getResponseObject(); + assertNotNull(response); + assertEquals("rebootinstancebootgroupresponse", response.getResponseName()); + verify(instanceBootGroupService).rebootInstanceBootGroup(cmd); + verify(instanceBootGroupService).createInstanceBootGroupResponse(ENTITY_ID); + } + + @Test(expected = ServerApiException.class) + public void testExecuteReturnsNull() throws Exception { + RebootInstanceBootGroupCmd cmd = createCmd(); + when(instanceBootGroupService.rebootInstanceBootGroup(cmd)).thenReturn(null); + cmd.execute(); + } + + @Test(expected = RuntimeException.class) + public void testExecutePropagatesServiceException() throws Exception { + RebootInstanceBootGroupCmd cmd = createCmd(); + when(instanceBootGroupService.rebootInstanceBootGroup(cmd)).thenThrow(new RuntimeException("db error")); + cmd.execute(); + } + + @Test(expected = ServerApiException.class) + public void testExecuteWrapsCloudRuntimeException() throws Exception { + RebootInstanceBootGroupCmd cmd = createCmd(); + when(instanceBootGroupService.rebootInstanceBootGroup(cmd)).thenThrow(new CloudRuntimeException("cannot reboot")); + cmd.execute(); + } +} diff --git a/api/src/test/java/org/apache/cloudstack/api/command/user/bootgroup/RemoveInstanceBootGroupMemberCmdTest.java b/api/src/test/java/org/apache/cloudstack/api/command/user/bootgroup/RemoveInstanceBootGroupMemberCmdTest.java new file mode 100644 index 000000000000..08ef6c5173cf --- /dev/null +++ b/api/src/test/java/org/apache/cloudstack/api/command/user/bootgroup/RemoveInstanceBootGroupMemberCmdTest.java @@ -0,0 +1,85 @@ +// 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.command.user.bootgroup; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNotNull; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +import org.apache.cloudstack.api.ApiCommandResourceType; +import org.apache.cloudstack.api.ServerApiException; +import org.apache.cloudstack.api.response.SuccessResponse; +import org.junit.Test; + +public class RemoveInstanceBootGroupMemberCmdTest extends BaseBootGroupCmdTest { + + private RemoveInstanceBootGroupMemberCmd createCmd() throws Exception { + RemoveInstanceBootGroupMemberCmd cmd = new RemoveInstanceBootGroupMemberCmd(); + setField(cmd, "instanceBootGroupService", instanceBootGroupService); + setField(cmd, "id", ENTITY_ID); + return cmd; + } + + @Test + public void testAccessors() throws Exception { + RemoveInstanceBootGroupMemberCmd cmd = createCmd(); + assertEquals(Long.valueOf(ENTITY_ID), cmd.getId()); + } + + @Test + public void testGetEntityOwnerId() throws Exception { + RemoveInstanceBootGroupMemberCmd cmd = createCmd(); + assertEquals(ACCOUNT_ID, cmd.getEntityOwnerId()); + } + + @Test + public void testGetApiResourceType() throws Exception { + RemoveInstanceBootGroupMemberCmd cmd = createCmd(); + Long groupId = 101L; + when(instanceBootGroupService.getInstanceBootGroupIdForMember(ENTITY_ID)).thenReturn(groupId); + assertEquals(ApiCommandResourceType.InstanceBootGroup, cmd.getApiResourceType()); + assertEquals(groupId, cmd.getApiResourceId()); + } + + @Test + public void testExecuteSuccess() throws Exception { + RemoveInstanceBootGroupMemberCmd cmd = createCmd(); + when(instanceBootGroupService.removeInstanceBootGroupMember(cmd)).thenReturn(true); + + cmd.execute(); + + SuccessResponse response = (SuccessResponse) cmd.getResponseObject(); + assertNotNull(response); + assertEquals("removeinstancebootgroupmemberresponse", response.getResponseName()); + verify(instanceBootGroupService).removeInstanceBootGroupMember(cmd); + } + + @Test(expected = ServerApiException.class) + public void testExecuteReturnsFalse() throws Exception { + RemoveInstanceBootGroupMemberCmd cmd = createCmd(); + when(instanceBootGroupService.removeInstanceBootGroupMember(cmd)).thenReturn(false); + cmd.execute(); + } + + @Test(expected = RuntimeException.class) + public void testExecutePropagatesServiceException() throws Exception { + RemoveInstanceBootGroupMemberCmd cmd = createCmd(); + when(instanceBootGroupService.removeInstanceBootGroupMember(cmd)).thenThrow(new RuntimeException("conflict")); + cmd.execute(); + } +} diff --git a/api/src/test/java/org/apache/cloudstack/api/command/user/bootgroup/StartInstanceBootGroupCmdTest.java b/api/src/test/java/org/apache/cloudstack/api/command/user/bootgroup/StartInstanceBootGroupCmdTest.java new file mode 100644 index 000000000000..1faed56ef609 --- /dev/null +++ b/api/src/test/java/org/apache/cloudstack/api/command/user/bootgroup/StartInstanceBootGroupCmdTest.java @@ -0,0 +1,118 @@ +// 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.command.user.bootgroup; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNotNull; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +import org.apache.cloudstack.api.ApiCommandResourceType; +import org.apache.cloudstack.api.ApiConstants; +import org.apache.cloudstack.api.ServerApiException; +import org.apache.cloudstack.api.response.InstanceBootGroupResponse; +import org.apache.cloudstack.context.CallContext; +import org.apache.cloudstack.vm.bootgroup.InstanceBootGroup; +import org.junit.Test; + +import com.cloud.event.EventTypes; +import com.cloud.utils.exception.CloudRuntimeException; + +public class StartInstanceBootGroupCmdTest extends BaseBootGroupCmdTest { + + private StartInstanceBootGroupCmd createCmd() throws Exception { + StartInstanceBootGroupCmd cmd = new StartInstanceBootGroupCmd(); + setField(cmd, "instanceBootGroupService", instanceBootGroupService); + setField(cmd, "id", ENTITY_ID); + return cmd; + } + + @Test + public void testAccessors() throws Exception { + StartInstanceBootGroupCmd cmd = createCmd(); + assertEquals(Long.valueOf(ENTITY_ID), cmd.getId()); + } + + @Test + public void testGetEntityOwnerId() throws Exception { + StartInstanceBootGroupCmd cmd = createCmd(); + assertEquals(ACCOUNT_ID, cmd.getEntityOwnerId()); + } + + @Test + public void testGetApiResourceType() throws Exception { + StartInstanceBootGroupCmd cmd = createCmd(); + assertEquals(ApiCommandResourceType.InstanceBootGroup, cmd.getApiResourceType()); + assertEquals(Long.valueOf(ENTITY_ID), cmd.getApiResourceId()); + } + + @Test + public void testEventType() throws Exception { + StartInstanceBootGroupCmd cmd = createCmd(); + assertEquals(EventTypes.EVENT_INSTANCE_BOOT_GROUP_START, cmd.getEventType()); + } + + @Test + public void testEventDescription() throws Exception { + StartInstanceBootGroupCmd cmd = createCmd(); + when(CallContext.current().getApiResourceUuid(ApiConstants.ID)).thenReturn("group-uuid"); + + assertEquals("Starting instance boot group with ID: group-uuid", cmd.getEventDescription()); + } + + @Test + public void testExecuteSuccess() throws Exception { + StartInstanceBootGroupCmd cmd = createCmd(); + + InstanceBootGroup group = mock(InstanceBootGroup.class); + when(group.getId()).thenReturn(ENTITY_ID); + InstanceBootGroupResponse mockResponse = new InstanceBootGroupResponse(); + + when(instanceBootGroupService.startInstanceBootGroup(cmd)).thenReturn(group); + when(instanceBootGroupService.createInstanceBootGroupResponse(ENTITY_ID)).thenReturn(mockResponse); + + cmd.execute(); + + InstanceBootGroupResponse response = (InstanceBootGroupResponse) cmd.getResponseObject(); + assertNotNull(response); + assertEquals("startinstancebootgroupresponse", response.getResponseName()); + verify(instanceBootGroupService).startInstanceBootGroup(cmd); + verify(instanceBootGroupService).createInstanceBootGroupResponse(ENTITY_ID); + } + + @Test(expected = ServerApiException.class) + public void testExecuteReturnsNull() throws Exception { + StartInstanceBootGroupCmd cmd = createCmd(); + when(instanceBootGroupService.startInstanceBootGroup(cmd)).thenReturn(null); + cmd.execute(); + } + + @Test(expected = RuntimeException.class) + public void testExecutePropagatesServiceException() throws Exception { + StartInstanceBootGroupCmd cmd = createCmd(); + when(instanceBootGroupService.startInstanceBootGroup(cmd)).thenThrow(new RuntimeException("db error")); + cmd.execute(); + } + + @Test(expected = ServerApiException.class) + public void testExecuteWrapsCloudRuntimeException() throws Exception { + StartInstanceBootGroupCmd cmd = createCmd(); + when(instanceBootGroupService.startInstanceBootGroup(cmd)).thenThrow(new CloudRuntimeException("cannot start")); + cmd.execute(); + } +} diff --git a/api/src/test/java/org/apache/cloudstack/api/command/user/bootgroup/StopInstanceBootGroupCmdTest.java b/api/src/test/java/org/apache/cloudstack/api/command/user/bootgroup/StopInstanceBootGroupCmdTest.java new file mode 100644 index 000000000000..e46573398c67 --- /dev/null +++ b/api/src/test/java/org/apache/cloudstack/api/command/user/bootgroup/StopInstanceBootGroupCmdTest.java @@ -0,0 +1,110 @@ +// 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.command.user.bootgroup; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNotNull; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +import org.apache.cloudstack.api.ApiCommandResourceType; +import org.apache.cloudstack.api.ApiConstants; +import org.apache.cloudstack.api.ServerApiException; +import org.apache.cloudstack.api.response.InstanceBootGroupResponse; +import org.apache.cloudstack.context.CallContext; +import org.apache.cloudstack.vm.bootgroup.InstanceBootGroup; +import org.junit.Test; + +import com.cloud.event.EventTypes; + +public class StopInstanceBootGroupCmdTest extends BaseBootGroupCmdTest { + + private StopInstanceBootGroupCmd createCmd() throws Exception { + StopInstanceBootGroupCmd cmd = new StopInstanceBootGroupCmd(); + setField(cmd, "instanceBootGroupService", instanceBootGroupService); + setField(cmd, "id", ENTITY_ID); + return cmd; + } + + @Test + public void testAccessors() throws Exception { + StopInstanceBootGroupCmd cmd = createCmd(); + assertEquals(Long.valueOf(ENTITY_ID), cmd.getId()); + } + + @Test + public void testGetEntityOwnerId() throws Exception { + StopInstanceBootGroupCmd cmd = createCmd(); + assertEquals(ACCOUNT_ID, cmd.getEntityOwnerId()); + } + + @Test + public void testGetApiResourceType() throws Exception { + StopInstanceBootGroupCmd cmd = createCmd(); + assertEquals(ApiCommandResourceType.InstanceBootGroup, cmd.getApiResourceType()); + assertEquals(Long.valueOf(ENTITY_ID), cmd.getApiResourceId()); + } + + @Test + public void testEventType() throws Exception { + StopInstanceBootGroupCmd cmd = createCmd(); + assertEquals(EventTypes.EVENT_INSTANCE_BOOT_GROUP_STOP, cmd.getEventType()); + } + + @Test + public void testEventDescription() throws Exception { + StopInstanceBootGroupCmd cmd = createCmd(); + when(CallContext.current().getApiResourceUuid(ApiConstants.ID)).thenReturn("group-uuid"); + + assertEquals("Stopping instance boot group with ID: group-uuid", cmd.getEventDescription()); + } + + @Test + public void testExecuteSuccess() throws Exception { + StopInstanceBootGroupCmd cmd = createCmd(); + + InstanceBootGroup group = mock(InstanceBootGroup.class); + when(group.getId()).thenReturn(ENTITY_ID); + InstanceBootGroupResponse mockResponse = new InstanceBootGroupResponse(); + + when(instanceBootGroupService.stopInstanceBootGroup(cmd)).thenReturn(group); + when(instanceBootGroupService.createInstanceBootGroupResponse(ENTITY_ID)).thenReturn(mockResponse); + + cmd.execute(); + + InstanceBootGroupResponse response = (InstanceBootGroupResponse) cmd.getResponseObject(); + assertNotNull(response); + assertEquals("stopinstancebootgroupresponse", response.getResponseName()); + verify(instanceBootGroupService).stopInstanceBootGroup(cmd); + verify(instanceBootGroupService).createInstanceBootGroupResponse(ENTITY_ID); + } + + @Test(expected = ServerApiException.class) + public void testExecuteReturnsNull() throws Exception { + StopInstanceBootGroupCmd cmd = createCmd(); + when(instanceBootGroupService.stopInstanceBootGroup(cmd)).thenReturn(null); + cmd.execute(); + } + + @Test(expected = RuntimeException.class) + public void testExecutePropagatesServiceException() throws Exception { + StopInstanceBootGroupCmd cmd = createCmd(); + when(instanceBootGroupService.stopInstanceBootGroup(cmd)).thenThrow(new RuntimeException("db error")); + cmd.execute(); + } +} diff --git a/api/src/test/java/org/apache/cloudstack/api/command/user/bootgroup/UpdateInstanceBootGroupCmdTest.java b/api/src/test/java/org/apache/cloudstack/api/command/user/bootgroup/UpdateInstanceBootGroupCmdTest.java new file mode 100644 index 000000000000..f64b1298b2e2 --- /dev/null +++ b/api/src/test/java/org/apache/cloudstack/api/command/user/bootgroup/UpdateInstanceBootGroupCmdTest.java @@ -0,0 +1,104 @@ +// 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.command.user.bootgroup; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNotNull; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +import org.apache.cloudstack.api.ApiCommandResourceType; +import org.apache.cloudstack.api.ServerApiException; +import org.apache.cloudstack.api.response.InstanceBootGroupResponse; +import org.apache.cloudstack.vm.bootgroup.InstanceBootGroup; +import org.junit.Test; + +public class UpdateInstanceBootGroupCmdTest extends BaseBootGroupCmdTest { + + private UpdateInstanceBootGroupCmd createCmd() throws Exception { + UpdateInstanceBootGroupCmd cmd = new UpdateInstanceBootGroupCmd(); + setField(cmd, "instanceBootGroupService", instanceBootGroupService); + setField(cmd, "id", ENTITY_ID); + setField(cmd, "name", "web-tier-renamed"); + setField(cmd, "description", "updated description"); + setField(cmd, "readinessAttemptTimeoutSeconds", 180L); + setField(cmd, "readinessMaxRetryAttempts", 5L); + setField(cmd, "readinessRebootOnRetry", false); + setField(cmd, "readinessInitialDelaySeconds", 30L); + return cmd; + } + + @Test + public void testAccessors() throws Exception { + UpdateInstanceBootGroupCmd cmd = createCmd(); + assertEquals(Long.valueOf(ENTITY_ID), cmd.getId()); + assertEquals("web-tier-renamed", cmd.getName()); + assertEquals("updated description", cmd.getDescription()); + assertEquals(Long.valueOf(180L), cmd.getReadinessAttemptTimeoutSeconds()); + assertEquals(Long.valueOf(5L), cmd.getReadinessMaxRetryAttempts()); + assertEquals(Boolean.FALSE, cmd.getReadinessRebootOnRetry()); + assertEquals(Long.valueOf(30L), cmd.getReadinessInitialDelaySeconds()); + } + + @Test + public void testGetEntityOwnerId() throws Exception { + UpdateInstanceBootGroupCmd cmd = createCmd(); + assertEquals(ACCOUNT_ID, cmd.getEntityOwnerId()); + } + + @Test + public void testGetApiResourceType() throws Exception { + UpdateInstanceBootGroupCmd cmd = createCmd(); + assertEquals(ApiCommandResourceType.InstanceBootGroup, cmd.getApiResourceType()); + assertEquals(Long.valueOf(ENTITY_ID), cmd.getApiResourceId()); + } + + @Test + public void testExecuteSuccess() throws Exception { + UpdateInstanceBootGroupCmd cmd = createCmd(); + + InstanceBootGroup group = mock(InstanceBootGroup.class); + when(group.getId()).thenReturn(ENTITY_ID); + InstanceBootGroupResponse mockResponse = new InstanceBootGroupResponse(); + + when(instanceBootGroupService.updateInstanceBootGroup(cmd)).thenReturn(group); + when(instanceBootGroupService.createInstanceBootGroupResponse(ENTITY_ID)).thenReturn(mockResponse); + + cmd.execute(); + + InstanceBootGroupResponse response = (InstanceBootGroupResponse) cmd.getResponseObject(); + assertNotNull(response); + assertEquals("updateinstancebootgroupresponse", response.getResponseName()); + verify(instanceBootGroupService).updateInstanceBootGroup(cmd); + verify(instanceBootGroupService).createInstanceBootGroupResponse(ENTITY_ID); + } + + @Test(expected = ServerApiException.class) + public void testExecuteReturnsNull() throws Exception { + UpdateInstanceBootGroupCmd cmd = createCmd(); + when(instanceBootGroupService.updateInstanceBootGroup(cmd)).thenReturn(null); + cmd.execute(); + } + + @Test(expected = RuntimeException.class) + public void testExecutePropagatesServiceException() throws Exception { + UpdateInstanceBootGroupCmd cmd = createCmd(); + when(instanceBootGroupService.updateInstanceBootGroup(cmd)).thenThrow(new RuntimeException("db error")); + cmd.execute(); + } +} diff --git a/api/src/test/java/org/apache/cloudstack/api/command/user/bootgroup/UpdateInstanceBootGroupMemberCmdTest.java b/api/src/test/java/org/apache/cloudstack/api/command/user/bootgroup/UpdateInstanceBootGroupMemberCmdTest.java new file mode 100644 index 000000000000..219ae3ce67fb --- /dev/null +++ b/api/src/test/java/org/apache/cloudstack/api/command/user/bootgroup/UpdateInstanceBootGroupMemberCmdTest.java @@ -0,0 +1,95 @@ +// 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.command.user.bootgroup; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNotNull; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +import org.apache.cloudstack.api.ApiCommandResourceType; +import org.apache.cloudstack.api.ServerApiException; +import org.apache.cloudstack.api.response.InstanceBootGroupMemberResponse; +import org.apache.cloudstack.vm.bootgroup.InstanceBootGroupMember; +import org.junit.Test; + +public class UpdateInstanceBootGroupMemberCmdTest extends BaseBootGroupCmdTest { + + private UpdateInstanceBootGroupMemberCmd createCmd() throws Exception { + UpdateInstanceBootGroupMemberCmd cmd = new UpdateInstanceBootGroupMemberCmd(); + setField(cmd, "instanceBootGroupService", instanceBootGroupService); + setField(cmd, "id", ENTITY_ID); + setField(cmd, "order", 3); + return cmd; + } + + @Test + public void testAccessors() throws Exception { + UpdateInstanceBootGroupMemberCmd cmd = createCmd(); + assertEquals(Long.valueOf(ENTITY_ID), cmd.getId()); + assertEquals(3, cmd.getOrder()); + } + + @Test + public void testGetEntityOwnerId() throws Exception { + UpdateInstanceBootGroupMemberCmd cmd = createCmd(); + assertEquals(ACCOUNT_ID, cmd.getEntityOwnerId()); + } + + @Test + public void testGetApiResourceType() throws Exception { + UpdateInstanceBootGroupMemberCmd cmd = createCmd(); + Long groupId = 101L; + when(instanceBootGroupService.getInstanceBootGroupIdForMember(ENTITY_ID)).thenReturn(groupId); + assertEquals(ApiCommandResourceType.InstanceBootGroup, cmd.getApiResourceType()); + assertEquals(groupId, cmd.getApiResourceId()); + } + + @Test + public void testExecuteSuccess() throws Exception { + UpdateInstanceBootGroupMemberCmd cmd = createCmd(); + + InstanceBootGroupMember member = mock(InstanceBootGroupMember.class); + InstanceBootGroupMemberResponse mockResponse = new InstanceBootGroupMemberResponse(); + + when(instanceBootGroupService.updateInstanceBootGroupMember(cmd)).thenReturn(member); + when(instanceBootGroupService.createInstanceBootGroupMemberResponse(member)).thenReturn(mockResponse); + + cmd.execute(); + + InstanceBootGroupMemberResponse response = (InstanceBootGroupMemberResponse) cmd.getResponseObject(); + assertNotNull(response); + assertEquals("updateinstancebootgroupmemberresponse", response.getResponseName()); + verify(instanceBootGroupService).updateInstanceBootGroupMember(cmd); + verify(instanceBootGroupService).createInstanceBootGroupMemberResponse(member); + } + + @Test(expected = ServerApiException.class) + public void testExecuteReturnsNull() throws Exception { + UpdateInstanceBootGroupMemberCmd cmd = createCmd(); + when(instanceBootGroupService.updateInstanceBootGroupMember(cmd)).thenReturn(null); + cmd.execute(); + } + + @Test(expected = RuntimeException.class) + public void testExecutePropagatesServiceException() throws Exception { + UpdateInstanceBootGroupMemberCmd cmd = createCmd(); + when(instanceBootGroupService.updateInstanceBootGroupMember(cmd)).thenThrow(new RuntimeException("conflict")); + cmd.execute(); + } +} diff --git a/api/src/test/java/org/apache/cloudstack/api/command/user/bootgroup/UpdateInstanceBootGroupReadinessRuleCmdTest.java b/api/src/test/java/org/apache/cloudstack/api/command/user/bootgroup/UpdateInstanceBootGroupReadinessRuleCmdTest.java new file mode 100644 index 000000000000..b4f0e3608287 --- /dev/null +++ b/api/src/test/java/org/apache/cloudstack/api/command/user/bootgroup/UpdateInstanceBootGroupReadinessRuleCmdTest.java @@ -0,0 +1,139 @@ +// 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.command.user.bootgroup; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertNull; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +import java.util.HashMap; +import java.util.Map; + +import org.apache.cloudstack.api.ApiCommandResourceType; +import org.apache.cloudstack.api.ServerApiException; +import org.apache.cloudstack.api.response.InstanceBootGroupReadinessRuleResponse; +import org.apache.cloudstack.vm.bootgroup.readiness.InstanceBootGroupReadinessRule; +import org.junit.Test; + +public class UpdateInstanceBootGroupReadinessRuleCmdTest extends BaseBootGroupCmdTest { + + private UpdateInstanceBootGroupReadinessRuleCmd createCmd() throws Exception { + UpdateInstanceBootGroupReadinessRuleCmd cmd = new UpdateInstanceBootGroupReadinessRuleCmd(); + setField(cmd, "instanceBootGroupService", instanceBootGroupService); + setField(cmd, "id", ENTITY_ID); + setField(cmd, "name", "renamed-rule"); + setField(cmd, "enabled", false); + return cmd; + } + + @Test + public void testAccessors() throws Exception { + UpdateInstanceBootGroupReadinessRuleCmd cmd = createCmd(); + assertEquals(Long.valueOf(ENTITY_ID), cmd.getId()); + assertEquals("renamed-rule", cmd.getName()); + assertEquals(Boolean.FALSE, cmd.getEnabled()); + } + + @Test + public void testGetEnabledNullWhenNotSet() throws Exception { + UpdateInstanceBootGroupReadinessRuleCmd cmd = createCmd(); + setField(cmd, "enabled", null); + assertNull(cmd.getEnabled()); + } + + @Test + public void testGetEntityOwnerId() throws Exception { + UpdateInstanceBootGroupReadinessRuleCmd cmd = createCmd(); + assertEquals(ACCOUNT_ID, cmd.getEntityOwnerId()); + } + + @Test + public void testGetApiResourceType() throws Exception { + UpdateInstanceBootGroupReadinessRuleCmd cmd = createCmd(); + assertEquals(ApiCommandResourceType.InstanceBootGroupReadinessRule, cmd.getApiResourceType()); + assertEquals(Long.valueOf(ENTITY_ID), cmd.getApiResourceId()); + } + + @Test + public void testGetDetailsNull() throws Exception { + UpdateInstanceBootGroupReadinessRuleCmd cmd = createCmd(); + setField(cmd, "details", null); + assertNull(cmd.getDetails()); + } + + @Test + public void testGetDetailsEmptyMap() throws Exception { + UpdateInstanceBootGroupReadinessRuleCmd cmd = createCmd(); + setField(cmd, "details", new HashMap<>()); + assertNull(cmd.getDetails()); + } + + @Test + public void testGetDetailsPopulatedMap() throws Exception { + UpdateInstanceBootGroupReadinessRuleCmd cmd = createCmd(); + + Map innerMap = new HashMap<>(); + innerMap.put("port", "9090"); + innerMap.put("protocol", "udp"); + + Map> outerMap = new HashMap<>(); + outerMap.put("0", innerMap); + + setField(cmd, "details", outerMap); + + Map details = cmd.getDetails(); + assertNotNull(details); + assertEquals("9090", details.get("port")); + assertEquals("udp", details.get("protocol")); + } + + @Test + public void testExecuteSuccess() throws Exception { + UpdateInstanceBootGroupReadinessRuleCmd cmd = createCmd(); + + InstanceBootGroupReadinessRule rule = mock(InstanceBootGroupReadinessRule.class); + InstanceBootGroupReadinessRuleResponse mockResponse = new InstanceBootGroupReadinessRuleResponse(); + + when(instanceBootGroupService.updateInstanceBootGroupReadinessRule(cmd)).thenReturn(rule); + when(instanceBootGroupService.createInstanceBootGroupReadinessRuleResponse(rule)).thenReturn(mockResponse); + + cmd.execute(); + + InstanceBootGroupReadinessRuleResponse response = (InstanceBootGroupReadinessRuleResponse) cmd.getResponseObject(); + assertNotNull(response); + assertEquals("updateinstancebootgroupreadinessruleresponse", response.getResponseName()); + verify(instanceBootGroupService).updateInstanceBootGroupReadinessRule(cmd); + verify(instanceBootGroupService).createInstanceBootGroupReadinessRuleResponse(rule); + } + + @Test(expected = ServerApiException.class) + public void testExecuteReturnsNull() throws Exception { + UpdateInstanceBootGroupReadinessRuleCmd cmd = createCmd(); + when(instanceBootGroupService.updateInstanceBootGroupReadinessRule(cmd)).thenReturn(null); + cmd.execute(); + } + + @Test(expected = RuntimeException.class) + public void testExecutePropagatesServiceException() throws Exception { + UpdateInstanceBootGroupReadinessRuleCmd cmd = createCmd(); + when(instanceBootGroupService.updateInstanceBootGroupReadinessRule(cmd)).thenThrow(new RuntimeException("db error")); + cmd.execute(); + } +} diff --git a/core/src/main/java/com/cloud/agent/api/CheckGuestAgentLivenessAnswer.java b/core/src/main/java/com/cloud/agent/api/CheckGuestAgentLivenessAnswer.java new file mode 100644 index 000000000000..4019977de810 --- /dev/null +++ b/core/src/main/java/com/cloud/agent/api/CheckGuestAgentLivenessAnswer.java @@ -0,0 +1,35 @@ +// +// 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.agent.api; + +public class CheckGuestAgentLivenessAnswer extends Answer { + + public CheckGuestAgentLivenessAnswer() { + super(); + } + + public CheckGuestAgentLivenessAnswer(CheckGuestAgentLivenessCommand command, boolean alive, String details) { + super(command, alive, details); + } + + public CheckGuestAgentLivenessAnswer(CheckGuestAgentLivenessCommand command, Exception e) { + super(command, e); + } +} diff --git a/core/src/main/java/com/cloud/agent/api/CheckGuestAgentLivenessCommand.java b/core/src/main/java/com/cloud/agent/api/CheckGuestAgentLivenessCommand.java new file mode 100644 index 000000000000..b608beb156cd --- /dev/null +++ b/core/src/main/java/com/cloud/agent/api/CheckGuestAgentLivenessCommand.java @@ -0,0 +1,42 @@ +// +// 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.agent.api; + +public class CheckGuestAgentLivenessCommand extends Command { + + private String vmName; + + public CheckGuestAgentLivenessCommand(String vmName) { + this.vmName = vmName; + } + + public String getVmName() { + return vmName; + } + + public void setVmName(String vmName) { + this.vmName = vmName; + } + + @Override + public boolean executeInSequence() { + return false; + } +} diff --git a/core/src/main/java/com/cloud/agent/resource/virtualnetwork/VRScripts.java b/core/src/main/java/com/cloud/agent/resource/virtualnetwork/VRScripts.java index 7bfbf786e9b4..95ac5d10c7bf 100644 --- a/core/src/main/java/com/cloud/agent/resource/virtualnetwork/VRScripts.java +++ b/core/src/main/java/com/cloud/agent/resource/virtualnetwork/VRScripts.java @@ -77,6 +77,7 @@ public class VRScripts { public static final String DIAGNOSTICS = "diagnostics.py"; public static final String RETRIEVE_DIAGNOSTICS = "get_diagnostics_files.py"; + public static final String INSTANCE_READINESS_CHECK = "instance_readiness_check.py"; public static final String VR_FILE_CLEANUP = "cleanup.sh"; public static final String VR_UPDATE_INTERFACE_CONFIG = "update_interface_config.sh"; diff --git a/core/src/main/java/com/cloud/agent/resource/virtualnetwork/VirtualRoutingResource.java b/core/src/main/java/com/cloud/agent/resource/virtualnetwork/VirtualRoutingResource.java index b9ac455130f3..6d7988c5f6cd 100644 --- a/core/src/main/java/com/cloud/agent/resource/virtualnetwork/VirtualRoutingResource.java +++ b/core/src/main/java/com/cloud/agent/resource/virtualnetwork/VirtualRoutingResource.java @@ -50,6 +50,8 @@ import org.apache.cloudstack.diagnostics.DiagnosticsCommand; import org.apache.cloudstack.diagnostics.PrepareFilesAnswer; import org.apache.cloudstack.diagnostics.PrepareFilesCommand; +import org.apache.cloudstack.vm.bootgroup.readiness.InstanceReadinessCheckAnswer; +import org.apache.cloudstack.vm.bootgroup.readiness.InstanceReadinessCheckCommand; import org.apache.cloudstack.utils.security.KeyStoreUtils; import org.apache.commons.lang3.StringUtils; import org.apache.commons.net.util.SubnetUtils; @@ -231,6 +233,8 @@ private Answer executeQueryCommand(NetworkElementCommand cmd) { return execute((GetRouterAlertsCommand)cmd); } else if (cmd instanceof DiagnosticsCommand) { return execute((DiagnosticsCommand) cmd); + } else if (cmd instanceof InstanceReadinessCheckCommand) { + return execute((InstanceReadinessCheckCommand) cmd); } else if (cmd instanceof PrepareFilesCommand) { return execute((PrepareFilesCommand) cmd); } else if (cmd instanceof DeleteFileInVrCommand) { @@ -486,6 +490,15 @@ private Answer execute(DiagnosticsCommand cmd) { return new DiagnosticsAnswer(cmd, result.isSuccess(), result.getDetails()); } + private Answer execute(InstanceReadinessCheckCommand cmd) { + _eachTimeout = Duration.standardSeconds(Math.max(cmd.getWait(), 1)); + String args = cmd.getPort() == null ? + String.format("%s %s", cmd.getCheckType(), cmd.getIpAddress()) : + String.format("%s %s %s", cmd.getCheckType(), cmd.getIpAddress(), cmd.getPort()); + final ExecutionResult result = _vrDeployer.executeInVR(cmd.getRouterAccessIp(), VRScripts.INSTANCE_READINESS_CHECK, args, _eachTimeout); + return new InstanceReadinessCheckAnswer(cmd, result.isSuccess(), result.getDetails()); + } + private Answer execute(PrepareFilesCommand cmd) { String fileList = String.join(" ", cmd.getFilesToRetrieveList()); _eachTimeout = Duration.standardSeconds(cmd.getTimeout()); diff --git a/core/src/main/java/org/apache/cloudstack/vm/bootgroup/readiness/InstanceReadinessCheckAnswer.java b/core/src/main/java/org/apache/cloudstack/vm/bootgroup/readiness/InstanceReadinessCheckAnswer.java new file mode 100644 index 000000000000..87b19bd90b3a --- /dev/null +++ b/core/src/main/java/org/apache/cloudstack/vm/bootgroup/readiness/InstanceReadinessCheckAnswer.java @@ -0,0 +1,56 @@ +// 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.vm.bootgroup.readiness; + +import java.util.HashMap; +import java.util.Map; + +import org.apache.commons.lang3.StringUtils; + +import com.cloud.agent.api.Answer; +import com.cloud.utils.exception.CloudRuntimeException; + +public class InstanceReadinessCheckAnswer extends Answer { + + public static final String STDOUT = "stdout"; + public static final String STDERR = "stderr"; + public static final String EXITCODE = "exitcode"; + + public InstanceReadinessCheckAnswer(InstanceReadinessCheckCommand cmd, boolean result, String details) { + super(cmd, result, details); + } + + public Map getExecutionDetails() { + final Map executionDetails = new HashMap<>(); + if (getResult() && StringUtils.isNotEmpty(getDetails())) { + final String[] parts = getDetails().split("&&"); + if (parts.length >= 3) { + executionDetails.put(STDOUT, parts[0].trim()); + executionDetails.put(STDERR, parts[1].trim()); + executionDetails.put(EXITCODE, parts[2].trim()); + } else { + throw new CloudRuntimeException("Unsupported instance boot group readiness check output format"); + } + } else { + executionDetails.put(STDOUT, ""); + executionDetails.put(STDERR, getDetails()); + executionDetails.put(EXITCODE, "-1"); + } + return executionDetails; + } +} diff --git a/core/src/main/java/org/apache/cloudstack/vm/bootgroup/readiness/InstanceReadinessCheckCommand.java b/core/src/main/java/org/apache/cloudstack/vm/bootgroup/readiness/InstanceReadinessCheckCommand.java new file mode 100644 index 000000000000..8ae58304ae82 --- /dev/null +++ b/core/src/main/java/org/apache/cloudstack/vm/bootgroup/readiness/InstanceReadinessCheckCommand.java @@ -0,0 +1,73 @@ +// 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.vm.bootgroup.readiness; + +import com.cloud.agent.api.routing.NetworkElementCommand; + +/** + * Dispatched to a VR to run a readiness check (ping or TCP port connect) against a user VM's IP, + * on behalf of the instance boot group readiness feature. Deliberately separate from + * {@code org.apache.cloudstack.diagnostics.DiagnosticsCommand}, which is a general-purpose admin + * tool scoped to system VMs (SSVM/CPVM/VR) — this command runs its own dedicated VR script instead + * of sharing that one. + */ +public class InstanceReadinessCheckCommand extends NetworkElementCommand { + + public static final String CHECK_TYPE_PING = "ping"; + public static final String CHECK_TYPE_PORT_CHECK = "portcheck"; + + private final String checkType; + private final String ipAddress; + private Integer port; + private final boolean executeInSequence; + + public InstanceReadinessCheckCommand(String ipAddress, boolean executeInSequence) { + this.checkType = CHECK_TYPE_PING; + this.ipAddress = ipAddress; + this.executeInSequence = executeInSequence; + } + + public InstanceReadinessCheckCommand(String ipAddress, Integer port, boolean executeInSequence) { + this.checkType = CHECK_TYPE_PORT_CHECK; + this.ipAddress = ipAddress; + this.port = port; + this.executeInSequence = executeInSequence; + } + + public String getCheckType() { + return checkType; + } + + public String getIpAddress() { + return ipAddress; + } + + public Integer getPort() { + return port; + } + + @Override + public boolean isQuery() { + return true; + } + + @Override + public boolean executeInSequence() { + return executeInSequence; + } +} diff --git a/core/src/main/resources/META-INF/cloudstack/core/spring-core-registry-core-context.xml b/core/src/main/resources/META-INF/cloudstack/core/spring-core-registry-core-context.xml index cf43b8527a97..6e53ccaf425b 100644 --- a/core/src/main/resources/META-INF/cloudstack/core/spring-core-registry-core-context.xml +++ b/core/src/main/resources/META-INF/cloudstack/core/spring-core-registry-core-context.xml @@ -375,4 +375,19 @@ + + + + + + + + diff --git a/core/src/test/java/com/cloud/agent/resource/virtualnetwork/InstanceReadinessCheckExecutionTest.java b/core/src/test/java/com/cloud/agent/resource/virtualnetwork/InstanceReadinessCheckExecutionTest.java new file mode 100644 index 000000000000..226bf132d04b --- /dev/null +++ b/core/src/test/java/com/cloud/agent/resource/virtualnetwork/InstanceReadinessCheckExecutionTest.java @@ -0,0 +1,122 @@ +// 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.agent.resource.virtualnetwork; + +import static org.junit.Assert.assertEquals; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.anyString; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + +import java.util.HashMap; + +import javax.naming.ConfigurationException; + +import org.joda.time.Duration; +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.ArgumentCaptor; +import org.mockito.junit.MockitoJUnitRunner; + +import com.cloud.agent.api.Answer; +import com.cloud.agent.api.routing.NetworkElementCommand; +import com.cloud.utils.ExecutionResult; + +import org.apache.cloudstack.vm.bootgroup.readiness.InstanceReadinessCheckAnswer; +import org.apache.cloudstack.vm.bootgroup.readiness.InstanceReadinessCheckCommand; + +@RunWith(MockitoJUnitRunner.class) +public class InstanceReadinessCheckExecutionTest { + + private static final String ROUTER_IP = "169.254.3.4"; + + VirtualRoutingResource resource; + VirtualRouterDeployer deployer; + + @Before + public void setUp() throws ConfigurationException { + deployer = mock(VirtualRouterDeployer.class); + when(deployer.prepareCommand(any(NetworkElementCommand.class))).thenAnswer(invocation -> { + NetworkElementCommand cmd = invocation.getArgument(0); + cmd.setRouterAccessIp(ROUTER_IP); + return new ExecutionResult(true, null); + }); + when(deployer.cleanupCommand(any(NetworkElementCommand.class))).thenReturn(new ExecutionResult(true, null)); + when(deployer.executeInVR(anyString(), anyString(), anyString(), any(Duration.class))).thenReturn(new ExecutionResult(true, "out&&err&&0")); + + resource = new VirtualRoutingResource(deployer); + resource.configure("VRResource", new HashMap<>()); + } + + @Test + public void zeroWaitIsFlooredToOneSecondTimeout() { + InstanceReadinessCheckCommand cmd = new InstanceReadinessCheckCommand("10.1.1.5", false); + cmd.setWait(0); + + resource.executeRequest(cmd); + + ArgumentCaptor captor = ArgumentCaptor.forClass(Duration.class); + org.mockito.Mockito.verify(deployer).executeInVR(eq(ROUTER_IP), anyString(), anyString(), captor.capture()); + assertEquals(1L, captor.getValue().getStandardSeconds()); + } + + @Test + public void positiveWaitIsUsedAsTimeoutSeconds() { + InstanceReadinessCheckCommand cmd = new InstanceReadinessCheckCommand("10.1.1.5", false); + cmd.setWait(5); + + resource.executeRequest(cmd); + + ArgumentCaptor captor = ArgumentCaptor.forClass(Duration.class); + org.mockito.Mockito.verify(deployer).executeInVR(eq(ROUTER_IP), anyString(), anyString(), captor.capture()); + assertEquals(5L, captor.getValue().getStandardSeconds()); + } + + @Test + public void pingCommandOmitsPortFromArgs() { + InstanceReadinessCheckCommand cmd = new InstanceReadinessCheckCommand("10.1.1.5", false); + + resource.executeRequest(cmd); + + ArgumentCaptor argsCaptor = ArgumentCaptor.forClass(String.class); + org.mockito.Mockito.verify(deployer).executeInVR(eq(ROUTER_IP), anyString(), argsCaptor.capture(), any(Duration.class)); + assertEquals("ping 10.1.1.5", argsCaptor.getValue()); + } + + @Test + public void portCheckCommandIncludesPortInArgs() { + InstanceReadinessCheckCommand cmd = new InstanceReadinessCheckCommand("10.1.1.5", 8080, false); + + resource.executeRequest(cmd); + + ArgumentCaptor argsCaptor = ArgumentCaptor.forClass(String.class); + org.mockito.Mockito.verify(deployer).executeInVR(eq(ROUTER_IP), anyString(), argsCaptor.capture(), any(Duration.class)); + assertEquals("portcheck 10.1.1.5 8080", argsCaptor.getValue()); + } + + @Test + public void resultIsWrappedInInstanceReadinessCheckAnswer() { + InstanceReadinessCheckCommand cmd = new InstanceReadinessCheckCommand("10.1.1.5", false); + + Answer answer = resource.executeRequest(cmd); + + org.junit.Assert.assertTrue(answer instanceof InstanceReadinessCheckAnswer); + assertEquals("0", ((InstanceReadinessCheckAnswer) answer).getExecutionDetails().get(InstanceReadinessCheckAnswer.EXITCODE)); + } +} diff --git a/core/src/test/java/org/apache/cloudstack/vm/bootgroup/readiness/InstanceReadinessCheckAnswerTest.java b/core/src/test/java/org/apache/cloudstack/vm/bootgroup/readiness/InstanceReadinessCheckAnswerTest.java new file mode 100644 index 000000000000..63da0225345b --- /dev/null +++ b/core/src/test/java/org/apache/cloudstack/vm/bootgroup/readiness/InstanceReadinessCheckAnswerTest.java @@ -0,0 +1,91 @@ +// 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.vm.bootgroup.readiness; + +import java.util.Map; + +import org.junit.Assert; +import org.junit.Test; + +import com.cloud.utils.exception.CloudRuntimeException; + +public class InstanceReadinessCheckAnswerTest { + + private InstanceReadinessCheckCommand cmd() { + return new InstanceReadinessCheckCommand("10.1.1.5", false); + } + + @Test + public void wellFormedSuccessfulDetailsAreParsed() { + InstanceReadinessCheckAnswer answer = new InstanceReadinessCheckAnswer(cmd(), true, " out \n && err && 0 "); + + Map details = answer.getExecutionDetails(); + + Assert.assertEquals("out", details.get(InstanceReadinessCheckAnswer.STDOUT)); + Assert.assertEquals("err", details.get(InstanceReadinessCheckAnswer.STDERR)); + Assert.assertEquals("0", details.get(InstanceReadinessCheckAnswer.EXITCODE)); + } + + @Test + public void extraDelimitedSegmentsAreIgnoredPastTheThird() { + InstanceReadinessCheckAnswer answer = new InstanceReadinessCheckAnswer(cmd(), true, "out&&err&&1&&extra"); + + Map details = answer.getExecutionDetails(); + + Assert.assertEquals("out", details.get(InstanceReadinessCheckAnswer.STDOUT)); + Assert.assertEquals("err", details.get(InstanceReadinessCheckAnswer.STDERR)); + Assert.assertEquals("1", details.get(InstanceReadinessCheckAnswer.EXITCODE)); + } + + @Test(expected = CloudRuntimeException.class) + public void malformedSuccessfulDetailsThrow() { + new InstanceReadinessCheckAnswer(cmd(), true, "out&&err").getExecutionDetails(); + } + + @Test + public void failedResultIsNotParsedEvenIfWellFormed() { + InstanceReadinessCheckAnswer answer = new InstanceReadinessCheckAnswer(cmd(), false, "out&&err&&0"); + + Map details = answer.getExecutionDetails(); + + Assert.assertEquals("", details.get(InstanceReadinessCheckAnswer.STDOUT)); + Assert.assertEquals("out&&err&&0", details.get(InstanceReadinessCheckAnswer.STDERR)); + Assert.assertEquals("-1", details.get(InstanceReadinessCheckAnswer.EXITCODE)); + } + + @Test + public void blankDetailsWithSuccessfulResultFallsBackToDefaults() { + InstanceReadinessCheckAnswer answer = new InstanceReadinessCheckAnswer(cmd(), true, ""); + + Map details = answer.getExecutionDetails(); + + Assert.assertEquals("", details.get(InstanceReadinessCheckAnswer.STDOUT)); + Assert.assertEquals("", details.get(InstanceReadinessCheckAnswer.STDERR)); + Assert.assertEquals("-1", details.get(InstanceReadinessCheckAnswer.EXITCODE)); + } + + @Test + public void nullDetailsWithSuccessfulResultFallsBackToDefaults() { + InstanceReadinessCheckAnswer answer = new InstanceReadinessCheckAnswer(cmd(), true, null); + + Map details = answer.getExecutionDetails(); + + Assert.assertEquals("", details.get(InstanceReadinessCheckAnswer.STDOUT)); + Assert.assertNull(details.get(InstanceReadinessCheckAnswer.STDERR)); + Assert.assertEquals("-1", details.get(InstanceReadinessCheckAnswer.EXITCODE)); + } +} diff --git a/engine/schema/src/main/java/com/cloud/vm/dao/InstanceBootGroupDao.java b/engine/schema/src/main/java/com/cloud/vm/dao/InstanceBootGroupDao.java new file mode 100644 index 000000000000..e7ce4deedfcf --- /dev/null +++ b/engine/schema/src/main/java/com/cloud/vm/dao/InstanceBootGroupDao.java @@ -0,0 +1,30 @@ +// 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 org.apache.cloudstack.vm.bootgroup.InstanceBootGroupVO; + +public interface InstanceBootGroupDao extends GenericDao { + + List listByAccountId(long accountId); + + boolean isNameInUse(long accountId, String name); +} diff --git a/engine/schema/src/main/java/com/cloud/vm/dao/InstanceBootGroupDaoImpl.java b/engine/schema/src/main/java/com/cloud/vm/dao/InstanceBootGroupDaoImpl.java new file mode 100644 index 000000000000..3a80e7eabeed --- /dev/null +++ b/engine/schema/src/main/java/com/cloud/vm/dao/InstanceBootGroupDaoImpl.java @@ -0,0 +1,60 @@ +// 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 org.apache.cloudstack.vm.bootgroup.InstanceBootGroupVO; + +@Component +public class InstanceBootGroupDaoImpl extends GenericDaoBase implements InstanceBootGroupDao { + + private final SearchBuilder accountSearch; + private final SearchBuilder accountNameSearch; + + public InstanceBootGroupDaoImpl() { + accountSearch = createSearchBuilder(); + accountSearch.and("accountId", accountSearch.entity().getAccountId(), SearchCriteria.Op.EQ); + accountSearch.done(); + + accountNameSearch = createSearchBuilder(); + accountNameSearch.and("accountId", accountNameSearch.entity().getAccountId(), SearchCriteria.Op.EQ); + accountNameSearch.and("name", accountNameSearch.entity().getName(), SearchCriteria.Op.EQ); + accountNameSearch.done(); + } + + @Override + public List listByAccountId(long accountId) { + SearchCriteria sc = accountSearch.create(); + sc.setParameters("accountId", accountId); + return listBy(sc); + } + + @Override + public boolean isNameInUse(long accountId, String name) { + SearchCriteria sc = accountNameSearch.create(); + sc.setParameters("accountId", accountId); + sc.setParameters("name", name); + return !listBy(sc).isEmpty(); + } +} diff --git a/engine/schema/src/main/java/com/cloud/vm/dao/InstanceBootGroupDetailsDao.java b/engine/schema/src/main/java/com/cloud/vm/dao/InstanceBootGroupDetailsDao.java new file mode 100644 index 000000000000..6b1c241cddb0 --- /dev/null +++ b/engine/schema/src/main/java/com/cloud/vm/dao/InstanceBootGroupDetailsDao.java @@ -0,0 +1,28 @@ +// 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 org.apache.cloudstack.resourcedetail.ResourceDetailsDao; +import org.apache.cloudstack.vm.bootgroup.InstanceBootGroupDetailsVO; + +public interface InstanceBootGroupDetailsDao extends ResourceDetailsDao { + + String getDetail(long bootGroupId, String name); + + void setDetail(long bootGroupId, String name, String value); +} diff --git a/engine/schema/src/main/java/com/cloud/vm/dao/InstanceBootGroupDetailsDaoImpl.java b/engine/schema/src/main/java/com/cloud/vm/dao/InstanceBootGroupDetailsDaoImpl.java new file mode 100644 index 000000000000..065b09571a96 --- /dev/null +++ b/engine/schema/src/main/java/com/cloud/vm/dao/InstanceBootGroupDetailsDaoImpl.java @@ -0,0 +1,71 @@ +// 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 org.apache.cloudstack.resourcedetail.ResourceDetailsDaoBase; +import org.apache.cloudstack.vm.bootgroup.InstanceBootGroupDetailsVO; +import org.springframework.stereotype.Component; + +import com.cloud.utils.db.SearchBuilder; +import com.cloud.utils.db.SearchCriteria; + +@Component +public class InstanceBootGroupDetailsDaoImpl extends ResourceDetailsDaoBase implements InstanceBootGroupDetailsDao { + + private final SearchBuilder bootGroupSearch; + private final SearchBuilder bootGroupNameSearch; + + public InstanceBootGroupDetailsDaoImpl() { + bootGroupSearch = createSearchBuilder(); + bootGroupSearch.and("bootGroupId", bootGroupSearch.entity().getResourceId(), SearchCriteria.Op.EQ); + bootGroupSearch.done(); + + bootGroupNameSearch = createSearchBuilder(); + bootGroupNameSearch.and("bootGroupId", bootGroupNameSearch.entity().getResourceId(), SearchCriteria.Op.EQ); + bootGroupNameSearch.and("name", bootGroupNameSearch.entity().getName(), SearchCriteria.Op.EQ); + bootGroupNameSearch.done(); + } + + @Override + public void addDetail(long resourceId, String key, String value, boolean display) { + super.addDetail(new InstanceBootGroupDetailsVO(resourceId, key, value, display)); + } + + @Override + public String getDetail(long bootGroupId, String name) { + SearchCriteria sc = bootGroupNameSearch.create(); + sc.setParameters("bootGroupId", bootGroupId); + sc.setParameters("name", name); + InstanceBootGroupDetailsVO detail = findOneBy(sc); + return detail == null ? null : detail.getValue(); + } + + @Override + public void setDetail(long bootGroupId, String name, String value) { + SearchCriteria sc = bootGroupNameSearch.create(); + sc.setParameters("bootGroupId", bootGroupId); + sc.setParameters("name", name); + InstanceBootGroupDetailsVO existing = findOneBy(sc); + if (existing == null) { + persist(new InstanceBootGroupDetailsVO(bootGroupId, name, value)); + } else { + existing.setValue(value); + update(existing.getId(), existing); + } + } +} diff --git a/engine/schema/src/main/java/com/cloud/vm/dao/InstanceBootGroupMemberDao.java b/engine/schema/src/main/java/com/cloud/vm/dao/InstanceBootGroupMemberDao.java new file mode 100644 index 000000000000..1fce0ad3d90c --- /dev/null +++ b/engine/schema/src/main/java/com/cloud/vm/dao/InstanceBootGroupMemberDao.java @@ -0,0 +1,38 @@ +// 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.Pair; +import com.cloud.utils.db.GenericDao; +import org.apache.cloudstack.vm.bootgroup.InstanceBootGroupMemberVO; +import org.apache.cloudstack.vm.bootgroup.InstanceBootGroupMember; + +public interface InstanceBootGroupMemberDao extends GenericDao { + + List listByBootGroupId(long bootGroupId); + + Pair, Integer> searchAndCountByBootGroupId(long bootGroupId); + + Pair, Integer> searchAndCountByBootGroupIdAndType(long bootGroupId, InstanceBootGroupMember.MemberType memberType); + + InstanceBootGroupMemberVO findByMember(InstanceBootGroupMember.MemberType memberType, long memberId); + + void deleteByBootGroupId(long bootGroupId); +} diff --git a/engine/schema/src/main/java/com/cloud/vm/dao/InstanceBootGroupMemberDaoImpl.java b/engine/schema/src/main/java/com/cloud/vm/dao/InstanceBootGroupMemberDaoImpl.java new file mode 100644 index 000000000000..9cb4b742c4fe --- /dev/null +++ b/engine/schema/src/main/java/com/cloud/vm/dao/InstanceBootGroupMemberDaoImpl.java @@ -0,0 +1,90 @@ +// 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.Pair; +import com.cloud.utils.db.GenericDaoBase; +import com.cloud.utils.db.SearchBuilder; +import com.cloud.utils.db.SearchCriteria; +import org.apache.cloudstack.vm.bootgroup.InstanceBootGroupMemberVO; +import org.apache.cloudstack.vm.bootgroup.InstanceBootGroupMember; + +@Component +public class InstanceBootGroupMemberDaoImpl extends GenericDaoBase implements InstanceBootGroupMemberDao { + + private final SearchBuilder bootGroupSearch; + private final SearchBuilder bootGroupTypeSearch; + private final SearchBuilder memberSearch; + + public InstanceBootGroupMemberDaoImpl() { + bootGroupSearch = createSearchBuilder(); + bootGroupSearch.and("bootGroupId", bootGroupSearch.entity().getBootGroupId(), SearchCriteria.Op.EQ); + bootGroupSearch.done(); + + bootGroupTypeSearch = createSearchBuilder(); + bootGroupTypeSearch.and("bootGroupId", bootGroupTypeSearch.entity().getBootGroupId(), SearchCriteria.Op.EQ); + bootGroupTypeSearch.and("memberType", bootGroupTypeSearch.entity().getMemberType(), SearchCriteria.Op.EQ); + bootGroupTypeSearch.done(); + + memberSearch = createSearchBuilder(); + memberSearch.and("memberType", memberSearch.entity().getMemberType(), SearchCriteria.Op.EQ); + memberSearch.and("memberId", memberSearch.entity().getMemberId(), SearchCriteria.Op.EQ); + memberSearch.done(); + } + + @Override + public List listByBootGroupId(long bootGroupId) { + SearchCriteria sc = bootGroupSearch.create(); + sc.setParameters("bootGroupId", bootGroupId); + return listBy(sc, null); + } + + @Override + public Pair, Integer> searchAndCountByBootGroupId(long bootGroupId) { + SearchCriteria sc = bootGroupSearch.create(); + sc.setParameters("bootGroupId", bootGroupId); + return searchAndCount(sc, null); + } + + @Override + public Pair, Integer> searchAndCountByBootGroupIdAndType(long bootGroupId, InstanceBootGroupMember.MemberType memberType) { + SearchCriteria sc = bootGroupTypeSearch.create(); + sc.setParameters("bootGroupId", bootGroupId); + sc.setParameters("memberType", memberType); + return searchAndCount(sc, null); + } + + @Override + public InstanceBootGroupMemberVO findByMember(InstanceBootGroupMember.MemberType memberType, long memberId) { + SearchCriteria sc = memberSearch.create(); + sc.setParameters("memberType", memberType); + sc.setParameters("memberId", memberId); + return findOneBy(sc); + } + + @Override + public void deleteByBootGroupId(long bootGroupId) { + SearchCriteria sc = bootGroupSearch.create(); + sc.setParameters("bootGroupId", bootGroupId); + expunge(sc); + } +} diff --git a/engine/schema/src/main/java/com/cloud/vm/dao/InstanceBootGroupReadinessCheckResultDao.java b/engine/schema/src/main/java/com/cloud/vm/dao/InstanceBootGroupReadinessCheckResultDao.java new file mode 100644 index 000000000000..2f4fb1185df0 --- /dev/null +++ b/engine/schema/src/main/java/com/cloud/vm/dao/InstanceBootGroupReadinessCheckResultDao.java @@ -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.dao; + +import java.util.Date; + +import org.apache.cloudstack.vm.bootgroup.InstanceBootGroupReadinessCheckResultVO; +import org.apache.cloudstack.vm.bootgroup.readiness.InstanceBootGroupReadinessRule; + +import com.cloud.utils.db.GenericDao; + +public interface InstanceBootGroupReadinessCheckResultDao extends GenericDao { + + /** + * vmId 0 is the rule's own row (a VM-scoped rule's single target, or a group-scoped rule's + * all-members aggregate); any other vmId is one inherited member's individual result. + */ + InstanceBootGroupReadinessCheckResultVO findByRuleAndVm(long ruleId, long vmId); + + /** + * Inserts or updates the single cached result row for (ruleId, vmId) (no history, by design). + */ + void upsert(long ruleId, long vmId, InstanceBootGroupReadinessRule.Status status, String message, Date checkedOn); + + void deleteByRuleId(long ruleId); +} diff --git a/engine/schema/src/main/java/com/cloud/vm/dao/InstanceBootGroupReadinessCheckResultDaoImpl.java b/engine/schema/src/main/java/com/cloud/vm/dao/InstanceBootGroupReadinessCheckResultDaoImpl.java new file mode 100644 index 000000000000..56251a31214e --- /dev/null +++ b/engine/schema/src/main/java/com/cloud/vm/dao/InstanceBootGroupReadinessCheckResultDaoImpl.java @@ -0,0 +1,74 @@ +// 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.Date; + +import org.apache.cloudstack.vm.bootgroup.InstanceBootGroupReadinessCheckResultVO; +import org.apache.cloudstack.vm.bootgroup.readiness.InstanceBootGroupReadinessRule; +import org.springframework.stereotype.Component; + +import com.cloud.utils.db.GenericDaoBase; +import com.cloud.utils.db.SearchBuilder; +import com.cloud.utils.db.SearchCriteria; + +@Component +public class InstanceBootGroupReadinessCheckResultDaoImpl extends GenericDaoBase implements InstanceBootGroupReadinessCheckResultDao { + + private final SearchBuilder ruleIdSearch; + private final SearchBuilder ruleAndVmSearch; + + public InstanceBootGroupReadinessCheckResultDaoImpl() { + ruleIdSearch = createSearchBuilder(); + ruleIdSearch.and("ruleId", ruleIdSearch.entity().getRuleId(), SearchCriteria.Op.EQ); + ruleIdSearch.done(); + + ruleAndVmSearch = createSearchBuilder(); + ruleAndVmSearch.and("ruleId", ruleAndVmSearch.entity().getRuleId(), SearchCriteria.Op.EQ); + ruleAndVmSearch.and("vmId", ruleAndVmSearch.entity().getVmId(), SearchCriteria.Op.EQ); + ruleAndVmSearch.done(); + } + + @Override + public InstanceBootGroupReadinessCheckResultVO findByRuleAndVm(long ruleId, long vmId) { + SearchCriteria sc = ruleAndVmSearch.create(); + sc.setParameters("ruleId", ruleId); + sc.setParameters("vmId", vmId); + return findOneBy(sc); + } + + @Override + public void upsert(long ruleId, long vmId, InstanceBootGroupReadinessRule.Status status, String message, Date checkedOn) { + InstanceBootGroupReadinessCheckResultVO existing = findByRuleAndVm(ruleId, vmId); + if (existing == null) { + persist(new InstanceBootGroupReadinessCheckResultVO(ruleId, vmId, status, message, checkedOn)); + } else { + existing.setStatus(status); + existing.setMessage(message); + existing.setCheckedOn(checkedOn); + update(existing.getId(), existing); + } + } + + @Override + public void deleteByRuleId(long ruleId) { + SearchCriteria sc = ruleIdSearch.create(); + sc.setParameters("ruleId", ruleId); + expunge(sc); + } +} diff --git a/engine/schema/src/main/java/com/cloud/vm/dao/InstanceBootGroupReadinessRuleDao.java b/engine/schema/src/main/java/com/cloud/vm/dao/InstanceBootGroupReadinessRuleDao.java new file mode 100644 index 000000000000..c5599346be96 --- /dev/null +++ b/engine/schema/src/main/java/com/cloud/vm/dao/InstanceBootGroupReadinessRuleDao.java @@ -0,0 +1,44 @@ +// 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.apache.cloudstack.vm.bootgroup.InstanceBootGroupMember; +import org.apache.cloudstack.vm.bootgroup.InstanceBootGroupReadinessRuleVO; +import org.apache.cloudstack.vm.bootgroup.readiness.InstanceBootGroupReadinessRule; + +import com.cloud.utils.Pair; +import com.cloud.utils.db.GenericDao; + +public interface InstanceBootGroupReadinessRuleDao extends GenericDao { + + Pair, Integer> searchAndCountByBootGroupId(long bootGroupId, + Long id, + InstanceBootGroupMember.MemberType itemType, + Long itemId, + InstanceBootGroupReadinessRule.RuleType ruleType, + String keyword, + Long startIndex, + Long pageSize); + + List listEnabledByItem(long bootGroupId, InstanceBootGroupMember.MemberType itemType, long itemId); + + List listByItem(long bootGroupId, InstanceBootGroupMember.MemberType itemType, long itemId); + +} diff --git a/engine/schema/src/main/java/com/cloud/vm/dao/InstanceBootGroupReadinessRuleDaoImpl.java b/engine/schema/src/main/java/com/cloud/vm/dao/InstanceBootGroupReadinessRuleDaoImpl.java new file mode 100644 index 000000000000..125766a853b1 --- /dev/null +++ b/engine/schema/src/main/java/com/cloud/vm/dao/InstanceBootGroupReadinessRuleDaoImpl.java @@ -0,0 +1,119 @@ +// 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.apache.cloudstack.vm.bootgroup.InstanceBootGroupMember; +import org.apache.cloudstack.vm.bootgroup.InstanceBootGroupReadinessRuleVO; +import org.apache.cloudstack.vm.bootgroup.readiness.InstanceBootGroupReadinessRule; +import org.springframework.stereotype.Component; + +import com.cloud.utils.Pair; +import com.cloud.utils.db.Filter; +import com.cloud.utils.db.GenericDaoBase; +import com.cloud.utils.db.SearchBuilder; +import com.cloud.utils.db.SearchCriteria; + +@Component +public class InstanceBootGroupReadinessRuleDaoImpl extends GenericDaoBase implements InstanceBootGroupReadinessRuleDao { + + private final SearchBuilder itemSearch; + private final SearchBuilder enabledByItemSearch; + private final SearchBuilder byItemSearch; + + public InstanceBootGroupReadinessRuleDaoImpl() { + + itemSearch = createSearchBuilder(); + itemSearch.and("itemType", itemSearch.entity().getItemType(), SearchCriteria.Op.EQ); + itemSearch.and("itemId", itemSearch.entity().getItemId(), SearchCriteria.Op.EQ); + itemSearch.done(); + + enabledByItemSearch = createSearchBuilder(); + enabledByItemSearch.and("bootGroupId", enabledByItemSearch.entity().getBootGroupId(), SearchCriteria.Op.EQ); + enabledByItemSearch.and("itemType", enabledByItemSearch.entity().getItemType(), SearchCriteria.Op.EQ); + enabledByItemSearch.and("itemId", enabledByItemSearch.entity().getItemId(), SearchCriteria.Op.EQ); + enabledByItemSearch.and("enabled", enabledByItemSearch.entity().isEnabled(), SearchCriteria.Op.EQ); + enabledByItemSearch.done(); + + byItemSearch = createSearchBuilder(); + byItemSearch.and("bootGroupId", byItemSearch.entity().getBootGroupId(), SearchCriteria.Op.EQ); + byItemSearch.and("itemType", byItemSearch.entity().getItemType(), SearchCriteria.Op.EQ); + byItemSearch.and("itemId", byItemSearch.entity().getItemId(), SearchCriteria.Op.EQ); + byItemSearch.done(); + } + + @Override + public Pair, Integer> searchAndCountByBootGroupId(long bootGroupId, + Long id, + InstanceBootGroupMember.MemberType itemType, + Long itemId, + InstanceBootGroupReadinessRule.RuleType ruleType, + String keyword, + Long startIndex, + Long pageSize) { + SearchBuilder sb = createSearchBuilder(); + sb.and("bootGroupId", sb.entity().getBootGroupId(), SearchCriteria.Op.EQ); + sb.and("id", sb.entity().getId(), SearchCriteria.Op.EQ); + sb.and("itemType", sb.entity().getItemType(), SearchCriteria.Op.EQ); + sb.and("itemId", sb.entity().getItemId(), SearchCriteria.Op.EQ); + sb.and("ruleType", sb.entity().getRuleType(), SearchCriteria.Op.EQ); + sb.and("keyword", sb.entity().getName(), SearchCriteria.Op.LIKE); + sb.done(); + + SearchCriteria sc = sb.create(); + sc.setParameters("bootGroupId", bootGroupId); + if (id != null) { + sc.setParameters("id", id); + } + if (itemType != null) { + sc.setParameters("itemType", itemType); + } + if (itemId != null) { + sc.setParameters("itemId", itemId); + } + if (ruleType != null) { + sc.setParameters("ruleType", ruleType); + } + if (keyword != null) { + sc.setParameters("keyword", "%" + keyword + "%"); + } + + Filter searchFilter = new Filter(InstanceBootGroupReadinessRuleVO.class, "id", true, startIndex, pageSize); + return searchAndCount(sc, searchFilter); + } + + @Override + public List listEnabledByItem(long bootGroupId, InstanceBootGroupMember.MemberType itemType, long itemId) { + SearchCriteria sc = enabledByItemSearch.create(); + sc.setParameters("bootGroupId", bootGroupId); + sc.setParameters("itemType", itemType); + sc.setParameters("itemId", itemId); + sc.setParameters("enabled", true); + return listBy(sc); + } + + @Override + public List listByItem(long bootGroupId, InstanceBootGroupMember.MemberType itemType, long itemId) { + SearchCriteria sc = byItemSearch.create(); + sc.setParameters("bootGroupId", bootGroupId); + sc.setParameters("itemType", itemType); + sc.setParameters("itemId", itemId); + return listBy(sc); + } +} diff --git a/engine/schema/src/main/java/com/cloud/vm/dao/InstanceBootGroupReadinessRuleDetailsDao.java b/engine/schema/src/main/java/com/cloud/vm/dao/InstanceBootGroupReadinessRuleDetailsDao.java new file mode 100644 index 000000000000..eb835f1578bd --- /dev/null +++ b/engine/schema/src/main/java/com/cloud/vm/dao/InstanceBootGroupReadinessRuleDetailsDao.java @@ -0,0 +1,32 @@ +// 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.Map; + +import org.apache.cloudstack.resourcedetail.ResourceDetailsDao; +import org.apache.cloudstack.vm.bootgroup.InstanceBootGroupReadinessRuleDetailsVO; + +public interface InstanceBootGroupReadinessRuleDetailsDao extends ResourceDetailsDao { + + /** + * Like {@link #listDetailsKeyPairs(long)} but transparently decrypts the {@code script} key + * (CustomScript rule content is stored encrypted at rest). + */ + Map getDetails(long ruleId); +} diff --git a/engine/schema/src/main/java/com/cloud/vm/dao/InstanceBootGroupReadinessRuleDetailsDaoImpl.java b/engine/schema/src/main/java/com/cloud/vm/dao/InstanceBootGroupReadinessRuleDetailsDaoImpl.java new file mode 100644 index 000000000000..68e01ad3feac --- /dev/null +++ b/engine/schema/src/main/java/com/cloud/vm/dao/InstanceBootGroupReadinessRuleDetailsDaoImpl.java @@ -0,0 +1,69 @@ +// 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.HashMap; +import java.util.List; +import java.util.Map; + +import org.apache.cloudstack.resourcedetail.ResourceDetailsDaoBase; +import org.apache.cloudstack.vm.bootgroup.InstanceBootGroupReadinessRuleDetailsVO; +import org.springframework.stereotype.Component; + +import com.cloud.utils.crypt.DBEncryptionUtil; +import com.cloud.utils.db.SearchBuilder; +import com.cloud.utils.db.SearchCriteria; + +@Component +public class InstanceBootGroupReadinessRuleDetailsDaoImpl extends ResourceDetailsDaoBase implements InstanceBootGroupReadinessRuleDetailsDao { + + private static final String ENCRYPTED_KEY = "script"; + + private final SearchBuilder ruleSearch; + + public InstanceBootGroupReadinessRuleDetailsDaoImpl() { + super(); + ruleSearch = createSearchBuilder(); + ruleSearch.and("ruleId", ruleSearch.entity().getResourceId(), SearchCriteria.Op.EQ); + ruleSearch.done(); + } + + @Override + public void addDetail(long resourceId, String key, String value, boolean display) { + String storedValue = ENCRYPTED_KEY.equals(key) ? DBEncryptionUtil.encrypt(value) : value; + super.addDetail(new InstanceBootGroupReadinessRuleDetailsVO(resourceId, key, storedValue, display)); + } + + @Override + public Map getDetails(long ruleId) { + SearchCriteria sc = ruleSearch.create(); + sc.setParameters("ruleId", ruleId); + + List details = listBy(sc); + Map detailsMap = new HashMap<>(); + for (InstanceBootGroupReadinessRuleDetailsVO detail : details) { + String name = detail.getName(); + String value = detail.getValue(); + if (ENCRYPTED_KEY.equals(name)) { + value = DBEncryptionUtil.decrypt(value); + } + detailsMap.put(name, value); + } + return detailsMap; + } +} diff --git a/engine/schema/src/main/java/org/apache/cloudstack/vm/bootgroup/InstanceBootGroupDetailsVO.java b/engine/schema/src/main/java/org/apache/cloudstack/vm/bootgroup/InstanceBootGroupDetailsVO.java new file mode 100644 index 000000000000..14cf82a777eb --- /dev/null +++ b/engine/schema/src/main/java/org/apache/cloudstack/vm/bootgroup/InstanceBootGroupDetailsVO.java @@ -0,0 +1,93 @@ +// 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.vm.bootgroup; + +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 org.apache.cloudstack.api.ResourceDetail; + +/** + * Per-boot-group override of the global readiness ConfigKeys. {@code name} is the exact ConfigKey + * key string, so override resolution needs no separate key-mapping. + */ +@Entity +@Table(name = "instance_boot_group_details") +public class InstanceBootGroupDetailsVO implements ResourceDetail { + + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + @Column(name = "id") + private long id; + + @Column(name = "boot_group_id") + private long resourceId; + + @Column(name = "name") + private String name; + + @Column(name = "value") + private String value; + + @Column(name = "display") + private boolean display = true; + + protected InstanceBootGroupDetailsVO() { + } + + public InstanceBootGroupDetailsVO(long bootGroupId, String name, String value) { + this.resourceId = bootGroupId; + this.name = name; + this.value = value; + } + + public InstanceBootGroupDetailsVO(long bootGroupId, String name, String value, boolean display) { + this(bootGroupId, name, value); + this.display = display; + } + + @Override + public long getId() { + return id; + } + + public long getResourceId() { + return resourceId; + } + + public String getName() { + return name; + } + + public String getValue() { + return value; + } + + public void setValue(String value) { + this.value = value; + } + + @Override + public boolean isDisplay() { + return display; + } +} diff --git a/engine/schema/src/main/java/org/apache/cloudstack/vm/bootgroup/InstanceBootGroupMemberVO.java b/engine/schema/src/main/java/org/apache/cloudstack/vm/bootgroup/InstanceBootGroupMemberVO.java new file mode 100644 index 000000000000..66914ad9fdf4 --- /dev/null +++ b/engine/schema/src/main/java/org/apache/cloudstack/vm/bootgroup/InstanceBootGroupMemberVO.java @@ -0,0 +1,116 @@ +// 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.vm.bootgroup; + +import java.util.Date; +import java.util.UUID; + +import javax.persistence.Column; +import javax.persistence.Entity; +import javax.persistence.EnumType; +import javax.persistence.Enumerated; +import javax.persistence.GeneratedValue; +import javax.persistence.GenerationType; +import javax.persistence.Id; +import javax.persistence.Table; + +import org.apache.cloudstack.utils.reflectiontostringbuilderutils.ReflectionToStringBuilderUtils; + +@Entity +@Table(name = "instance_boot_group_member") +public class InstanceBootGroupMemberVO implements InstanceBootGroupMember { + + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + @Column(name = "id") + private long id; + + @Column(name = "uuid") + private String uuid; + + @Column(name = "boot_group_id") + private long bootGroupId; + + @Column(name = "member_type") + @Enumerated(EnumType.STRING) + private MemberType memberType; + + @Column(name = "member_id") + private long memberId; + + @Column(name = "order") + private int order; + + @Column(name = "created") + private Date created; + + protected InstanceBootGroupMemberVO() { + } + + public InstanceBootGroupMemberVO(long bootGroupId, MemberType memberType, long memberId, int order) { + this.bootGroupId = bootGroupId; + this.memberType = memberType; + this.memberId = memberId; + this.order = order; + this.uuid = UUID.randomUUID().toString(); + } + + @Override + public long getId() { + return id; + } + + @Override + public String getUuid() { + return uuid; + } + + @Override + public long getBootGroupId() { + return bootGroupId; + } + + @Override + public MemberType getMemberType() { + return memberType; + } + + @Override + public long getMemberId() { + return memberId; + } + + @Override + public int getOrder() { + return order; + } + + public void setOrder(int order) { + this.order = order; + } + + @Override + public Date getCreated() { + return created; + } + + @Override + public String toString() { + return String.format("BootGroupMember %s", ReflectionToStringBuilderUtils.reflectOnlySelectedFields(this, "id", "uuid", "bootGroupId", "memberType", "memberId", "order")); + } +} diff --git a/engine/schema/src/main/java/org/apache/cloudstack/vm/bootgroup/InstanceBootGroupReadinessCheckResultVO.java b/engine/schema/src/main/java/org/apache/cloudstack/vm/bootgroup/InstanceBootGroupReadinessCheckResultVO.java new file mode 100644 index 000000000000..4863dd7d9199 --- /dev/null +++ b/engine/schema/src/main/java/org/apache/cloudstack/vm/bootgroup/InstanceBootGroupReadinessCheckResultVO.java @@ -0,0 +1,113 @@ +// 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.vm.bootgroup; + +import java.util.Date; + +import javax.persistence.Column; +import javax.persistence.Entity; +import javax.persistence.EnumType; +import javax.persistence.Enumerated; +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.vm.bootgroup.readiness.InstanceBootGroupReadinessRule; + +/** + * Last cached evaluation result for a readiness rule, upserted in place — no history, by design. + * {@code vmId} is 0 for the rule's "own" row (a VM-scoped rule's single target, or a group-scoped + * rule's all-members aggregate); a group-scoped rule inherited by its members additionally gets one + * row per (ruleId, vmId) for that member's own individual result. + */ +@Entity +@Table(name = "instance_boot_group_readiness_check_result") +public class InstanceBootGroupReadinessCheckResultVO { + + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + @Column(name = "id") + private long id; + + @Column(name = "rule_id") + private long ruleId; + + @Column(name = "vm_id") + private long vmId; + + @Column(name = "status") + @Enumerated(EnumType.STRING) + private InstanceBootGroupReadinessRule.Status status; + + @Column(name = "message") + private String message; + + @Column(name = "checked_on") + @Temporal(TemporalType.TIMESTAMP) + private Date checkedOn; + + protected InstanceBootGroupReadinessCheckResultVO() { + } + + public InstanceBootGroupReadinessCheckResultVO(long ruleId, long vmId, InstanceBootGroupReadinessRule.Status status, String message, Date checkedOn) { + this.ruleId = ruleId; + this.vmId = vmId; + this.status = status; + this.message = message; + this.checkedOn = checkedOn; + } + + public long getId() { + return id; + } + + public long getRuleId() { + return ruleId; + } + + public long getVmId() { + return vmId; + } + + public InstanceBootGroupReadinessRule.Status getStatus() { + return status; + } + + public void setStatus(InstanceBootGroupReadinessRule.Status status) { + this.status = status; + } + + public String getMessage() { + return message; + } + + public void setMessage(String message) { + this.message = message; + } + + public Date getCheckedOn() { + return checkedOn; + } + + public void setCheckedOn(Date checkedOn) { + this.checkedOn = checkedOn; + } +} diff --git a/engine/schema/src/main/java/org/apache/cloudstack/vm/bootgroup/InstanceBootGroupReadinessRuleDetailsVO.java b/engine/schema/src/main/java/org/apache/cloudstack/vm/bootgroup/InstanceBootGroupReadinessRuleDetailsVO.java new file mode 100644 index 000000000000..f91bbb99b8c6 --- /dev/null +++ b/engine/schema/src/main/java/org/apache/cloudstack/vm/bootgroup/InstanceBootGroupReadinessRuleDetailsVO.java @@ -0,0 +1,99 @@ +// 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.vm.bootgroup; + +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 org.apache.cloudstack.api.ResourceDetail; + +/** + * Generic key/value config for a readiness rule (port/protocol, script content, threshold, ...). + * The {@code script} key is encrypted at rest by {@code InstanceBootGroupReadinessRuleDetailsDaoImpl} + * for CustomScript rules, the same manual encrypt-by-known-key-name pattern used for S3/Swift + * secrets in {@code ImageStoreDetailVO} — there is no boolean "encrypted" flag column convention in + * this codebase. + */ +@Entity +@Table(name = "instance_boot_group_readiness_rule_details") +public class InstanceBootGroupReadinessRuleDetailsVO implements ResourceDetail { + + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + @Column(name = "id") + private long id; + + @Column(name = "rule_id") + private long resourceId; + + @Column(name = "name") + private String name; + + @Column(name = "value") + private String value; + + @Column(name = "display") + private boolean display = true; + + protected InstanceBootGroupReadinessRuleDetailsVO() { + } + + public InstanceBootGroupReadinessRuleDetailsVO(long ruleId, String name, String value) { + this.resourceId = ruleId; + this.name = name; + this.value = value; + } + + public InstanceBootGroupReadinessRuleDetailsVO(long ruleId, String name, String value, boolean display) { + this(ruleId, name, value); + this.display = display; + } + + @Override + public long getId() { + return id; + } + + @Override + public long getResourceId() { + return resourceId; + } + + @Override + public String getName() { + return name; + } + + @Override + public String getValue() { + return value; + } + + public void setValue(String value) { + this.value = value; + } + + @Override + public boolean isDisplay() { + return display; + } +} diff --git a/engine/schema/src/main/java/org/apache/cloudstack/vm/bootgroup/InstanceBootGroupReadinessRuleVO.java b/engine/schema/src/main/java/org/apache/cloudstack/vm/bootgroup/InstanceBootGroupReadinessRuleVO.java new file mode 100644 index 000000000000..55596b2d580a --- /dev/null +++ b/engine/schema/src/main/java/org/apache/cloudstack/vm/bootgroup/InstanceBootGroupReadinessRuleVO.java @@ -0,0 +1,149 @@ +// 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.vm.bootgroup; + +import java.util.Date; +import java.util.UUID; + +import javax.persistence.Column; +import javax.persistence.Entity; +import javax.persistence.EnumType; +import javax.persistence.Enumerated; +import javax.persistence.GeneratedValue; +import javax.persistence.GenerationType; +import javax.persistence.Id; +import javax.persistence.Table; + +import org.apache.cloudstack.utils.reflectiontostringbuilderutils.ReflectionToStringBuilderUtils; +import org.apache.cloudstack.vm.bootgroup.readiness.InstanceBootGroupReadinessRule; + +import com.cloud.utils.db.GenericDao; + +@Entity +@Table(name = "instance_boot_group_readiness_rule") +public class InstanceBootGroupReadinessRuleVO implements InstanceBootGroupReadinessRule { + + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + @Column(name = "id") + private long id; + + @Column(name = "uuid") + private String uuid; + + @Column(name = "name") + private String name; + + @Column(name = "boot_group_id") + private long bootGroupId; + + @Column(name = "item_type") + @Enumerated(EnumType.STRING) + private InstanceBootGroupMember.MemberType itemType; + + @Column(name = "item_id") + private long itemId; + + @Column(name = "rule_type") + @Enumerated(EnumType.STRING) + private RuleType ruleType; + + @Column(name = "enabled") + private boolean enabled = true; + + @Column(name = GenericDao.CREATED_COLUMN) + private Date created; + + @Column(name = GenericDao.REMOVED_COLUMN) + private Date removed; + + protected InstanceBootGroupReadinessRuleVO() { + } + + public InstanceBootGroupReadinessRuleVO(String name, long bootGroupId, InstanceBootGroupMember.MemberType itemType, long itemId, RuleType ruleType, boolean enabled) { + this.name = name; + this.bootGroupId = bootGroupId; + this.itemType = itemType; + this.itemId = itemId; + this.ruleType = ruleType; + this.enabled = enabled; + this.uuid = UUID.randomUUID().toString(); + } + + @Override + public long getId() { + return id; + } + + @Override + public String getUuid() { + return uuid; + } + + @Override + public String getName() { + return name; + } + + public void setName(String name) { + this.name = name; + } + + @Override + public long getBootGroupId() { + return bootGroupId; + } + + @Override + public InstanceBootGroupMember.MemberType getItemType() { + return itemType; + } + + @Override + public long getItemId() { + return itemId; + } + + @Override + public RuleType getRuleType() { + return ruleType; + } + + @Override + public boolean isEnabled() { + return enabled; + } + + public void setEnabled(boolean enabled) { + this.enabled = enabled; + } + + @Override + public Date getCreated() { + return created; + } + + public Date getRemoved() { + return removed; + } + + @Override + public String toString() { + return String.format("ReadinessRule %s", ReflectionToStringBuilderUtils.reflectOnlySelectedFields(this, "id", "uuid", "name", "itemType", "itemId", "ruleType", "enabled")); + } +} diff --git a/engine/schema/src/main/java/org/apache/cloudstack/vm/bootgroup/InstanceBootGroupVO.java b/engine/schema/src/main/java/org/apache/cloudstack/vm/bootgroup/InstanceBootGroupVO.java new file mode 100644 index 000000000000..7ac79fda789d --- /dev/null +++ b/engine/schema/src/main/java/org/apache/cloudstack/vm/bootgroup/InstanceBootGroupVO.java @@ -0,0 +1,131 @@ +// 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.vm.bootgroup; + +import java.util.Date; +import java.util.UUID; + +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 org.apache.cloudstack.utils.reflectiontostringbuilderutils.ReflectionToStringBuilderUtils; + +import com.cloud.utils.db.GenericDao; + +@Entity +@Table(name = "instance_boot_group") +public class InstanceBootGroupVO implements InstanceBootGroup { + + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + @Column(name = "id") + private long id; + + @Column(name = "uuid") + private String uuid; + + @Column(name = "name") + private String name; + + @Column(name = "description") + private String description; + + @Column(name = "account_id") + private long accountId; + + @Column(name = "domain_id") + private long domainId; + + @Column(name = GenericDao.CREATED_COLUMN) + private Date created; + + @Column(name = GenericDao.REMOVED_COLUMN) + private Date removed; + + protected InstanceBootGroupVO() { + } + + public InstanceBootGroupVO(String name, String description, long accountId, long domainId) { + this.name = name; + this.description = description; + this.accountId = accountId; + this.domainId = domainId; + this.uuid = UUID.randomUUID().toString(); + } + + @Override + public long getId() { + return id; + } + + @Override + public String getUuid() { + return uuid; + } + + @Override + public String getName() { + return name; + } + + public void setName(String name) { + this.name = name; + } + + @Override + public String getDescription() { + return description; + } + + public void setDescription(String description) { + this.description = description; + } + + @Override + public long getAccountId() { + return accountId; + } + + @Override + public long getDomainId() { + return domainId; + } + + @Override + public Date getCreated() { + return created; + } + + public Date getRemoved() { + return removed; + } + + @Override + public Class getEntityType() { + return InstanceBootGroup.class; + } + + @Override + public String toString() { + return String.format("BootGroup %s", ReflectionToStringBuilderUtils.reflectOnlySelectedFields(this, "id", "uuid", "name")); + } +} diff --git a/engine/schema/src/main/resources/META-INF/cloudstack/core/spring-engine-schema-core-daos-context.xml b/engine/schema/src/main/resources/META-INF/cloudstack/core/spring-engine-schema-core-daos-context.xml index 932db538f30b..484bebe22a09 100644 --- a/engine/schema/src/main/resources/META-INF/cloudstack/core/spring-engine-schema-core-daos-context.xml +++ b/engine/schema/src/main/resources/META-INF/cloudstack/core/spring-engine-schema-core-daos-context.xml @@ -108,6 +108,12 @@ + + + + + + @@ -329,5 +335,6 @@ + diff --git a/engine/schema/src/main/resources/META-INF/db/schema-42210to42300.sql b/engine/schema/src/main/resources/META-INF/db/schema-42210to42300.sql index 417d69a162eb..01b6ca05f76d 100644 --- a/engine/schema/src/main/resources/META-INF/db/schema-42210to42300.sql +++ b/engine/schema/src/main/resources/META-INF/db/schema-42210to42300.sql @@ -651,3 +651,91 @@ WHERE `name`='user.vm.readonly.details' AND `value` IS NOT NULL; -- usage records introduced in 4.22.1 (cumulative and per-VM) can coexist. See #13399. CALL `cloud_usage`.`IDEMPOTENT_DROP_INDEX`('id', 'cloud_usage.usage_volume'); CALL `cloud_usage`.`IDEMPOTENT_ADD_UNIQUE_INDEX`('cloud_usage.usage_volume', 'id', '(volume_id ASC, created ASC, vm_id ASC)'); + +-- InstanceBootGroup: ordered boot sequencing for VMs and InstanceGroups +CREATE TABLE IF NOT EXISTS `cloud`.`instance_boot_group` ( + `id` bigint unsigned NOT NULL UNIQUE AUTO_INCREMENT, + `uuid` varchar(40) NOT NULL, + `name` varchar(255) NOT NULL, + `description` varchar(4096) DEFAULT NULL, + `account_id` bigint unsigned NOT NULL COMMENT 'owner; foreign key to account table', + `domain_id` bigint unsigned NOT NULL, + `created` datetime NOT NULL, + `removed` datetime DEFAULT NULL COMMENT 'date the group was soft-deleted', + PRIMARY KEY (`id`), + CONSTRAINT `uc_instance_boot_group__uuid` UNIQUE (`uuid`), + CONSTRAINT `fk_instance_boot_group__account_id` FOREIGN KEY (`account_id`) REFERENCES `account` (`id`), + CONSTRAINT `fk_instance_boot_group__domain_id` FOREIGN KEY (`domain_id`) REFERENCES `domain` (`id`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; + +CREATE TABLE IF NOT EXISTS `cloud`.`instance_boot_group_member` ( + `id` bigint unsigned NOT NULL AUTO_INCREMENT, + `uuid` varchar(40) NOT NULL, + `boot_group_id` bigint unsigned NOT NULL, + `member_type` varchar(32) NOT NULL COMMENT 'VirtualMachine or InstanceGroup', + `member_id` bigint unsigned NOT NULL, + `order` int NOT NULL DEFAULT 0, + `created` datetime NOT NULL, + PRIMARY KEY (`id`), + CONSTRAINT `uc_instance_boot_group_member__uuid` UNIQUE (`uuid`), + CONSTRAINT `uq_instance_boot_group_member__member` UNIQUE (`member_type`, `member_id`), + CONSTRAINT `fk_instance_boot_group_member__group_id` FOREIGN KEY (`boot_group_id`) REFERENCES `instance_boot_group` (`id`) ON DELETE CASCADE +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; + +-- InstanceBootGroupReadinessRule: a readiness rule always belongs to exactly one boot group and +-- references either a VirtualMachine or InstanceGroup item within it +CREATE TABLE IF NOT EXISTS `cloud`.`instance_boot_group_readiness_rule` ( + `id` bigint unsigned NOT NULL UNIQUE AUTO_INCREMENT, + `uuid` varchar(40) NOT NULL, + `name` varchar(255) NOT NULL, + `boot_group_id` bigint unsigned NOT NULL, + `item_type` varchar(32) NOT NULL COMMENT 'VirtualMachine or InstanceGroup', + `item_id` bigint unsigned NOT NULL, + `rule_type` varchar(64) NOT NULL, + `enabled` tinyint(1) NOT NULL DEFAULT 1, + `created` datetime NOT NULL, + `removed` datetime DEFAULT NULL COMMENT 'date the rule was soft-deleted', + PRIMARY KEY (`id`), + CONSTRAINT `uc_instance_boot_group_readiness_rule__uuid` UNIQUE (`uuid`), + CONSTRAINT `fk_instance_boot_group_readiness_rule__group_id` FOREIGN KEY (`boot_group_id`) REFERENCES `instance_boot_group` (`id`) ON DELETE CASCADE +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; + +-- Generic key/value config for a readiness rule (port/protocol, script content, threshold, ...); +-- the 'script' key is encrypted at rest by the DAO for CUSTOM_SCRIPT rules +CREATE TABLE IF NOT EXISTS `cloud`.`instance_boot_group_readiness_rule_details` ( + `id` bigint unsigned NOT NULL AUTO_INCREMENT, + `rule_id` bigint unsigned NOT NULL, + `name` varchar(255) NOT NULL, + `value` text DEFAULT NULL, + `display` tinyint(1) NOT NULL DEFAULT 1 COMMENT 'Whether detail be displayed to the end user', + PRIMARY KEY (`id`), + CONSTRAINT `fk_instance_boot_group_readiness_rule_details__rule_id` FOREIGN KEY (`rule_id`) REFERENCES `instance_boot_group_readiness_rule` (`id`) ON DELETE CASCADE +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; + +-- Last cached evaluation result for a readiness rule, upserted in place (no history, by design). +-- vm_id=0 is the rule's "own" row (a VM-scoped rule's single target, or a group-scoped rule's +-- all-members aggregate); a group-scoped rule inherited by its members additionally gets one row +-- per (rule_id, vm_id) for that member's own individual result. +CREATE TABLE IF NOT EXISTS `cloud`.`instance_boot_group_readiness_check_result` ( + `id` bigint unsigned NOT NULL AUTO_INCREMENT, + `rule_id` bigint unsigned NOT NULL, + `vm_id` bigint unsigned NOT NULL DEFAULT 0, + `status` varchar(32) NOT NULL DEFAULT 'Unknown', + `message` varchar(4096) DEFAULT NULL, + `checked_on` datetime DEFAULT NULL, + PRIMARY KEY (`id`), + UNIQUE KEY `uk_instance_boot_group_readiness_check_result__rule_vm` (`rule_id`, `vm_id`), + CONSTRAINT `fk_instance_boot_group_readiness_check_result__rule_id` FOREIGN KEY (`rule_id`) REFERENCES `instance_boot_group_readiness_rule` (`id`) ON DELETE CASCADE +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; + +-- Per-boot-group override of the global readiness ConfigKeys (timeout/max-reboot-attempts); the +-- 'name' values are the exact ConfigKey key strings, so override resolution needs no key-mapping +CREATE TABLE IF NOT EXISTS `cloud`.`instance_boot_group_details` ( + `id` bigint unsigned NOT NULL AUTO_INCREMENT, + `boot_group_id` bigint unsigned NOT NULL, + `name` varchar(255) NOT NULL, + `value` varchar(255) DEFAULT NULL, + `display` tinyint(1) NOT NULL DEFAULT 1 COMMENT 'Whether detail be displayed to the end user', + PRIMARY KEY (`id`), + CONSTRAINT `fk_instance_boot_group_details__group_id` FOREIGN KEY (`boot_group_id`) REFERENCES `instance_boot_group` (`id`) ON DELETE CASCADE +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; diff --git a/engine/schema/src/main/resources/META-INF/db/views/cloud.instance_boot_group_view.sql b/engine/schema/src/main/resources/META-INF/db/views/cloud.instance_boot_group_view.sql new file mode 100644 index 000000000000..7547354f9034 --- /dev/null +++ b/engine/schema/src/main/resources/META-INF/db/views/cloud.instance_boot_group_view.sql @@ -0,0 +1,47 @@ +-- 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. + +-- VIEW `cloud`.`instance_boot_group_view`; + +DROP VIEW IF EXISTS `cloud`.`instance_boot_group_view`; +CREATE VIEW `cloud`.`instance_boot_group_view` AS +SELECT + instance_boot_group.id, + instance_boot_group.uuid, + instance_boot_group.name, + instance_boot_group.description, + instance_boot_group.created, + instance_boot_group.removed, + account.id account_id, + account.uuid account_uuid, + account.account_name account_name, + account.type account_type, + domain.id domain_id, + domain.uuid domain_uuid, + domain.name domain_name, + domain.path domain_path, + projects.id project_id, + projects.uuid project_uuid, + projects.name project_name +FROM + `cloud`.`instance_boot_group` + INNER JOIN + `cloud`.`account` ON instance_boot_group.account_id = account.id + INNER JOIN + `cloud`.`domain` ON instance_boot_group.domain_id = domain.id + LEFT JOIN + `cloud`.`projects` ON projects.project_account_id = instance_boot_group.account_id; diff --git a/engine/schema/src/test/java/com/cloud/vm/dao/InstanceBootGroupDaoImplTest.java b/engine/schema/src/test/java/com/cloud/vm/dao/InstanceBootGroupDaoImplTest.java new file mode 100644 index 000000000000..6aa5f2813c53 --- /dev/null +++ b/engine/schema/src/test/java/com/cloud/vm/dao/InstanceBootGroupDaoImplTest.java @@ -0,0 +1,99 @@ +// 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.ArrayList; +import java.util.Collections; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +import org.apache.cloudstack.vm.bootgroup.InstanceBootGroupVO; +import org.junit.Assert; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.ArgumentCaptor; +import org.mockito.Mockito; +import org.mockito.Spy; +import org.mockito.junit.MockitoJUnitRunner; + +import com.cloud.utils.Pair; +import com.cloud.utils.db.Attribute; +import com.cloud.utils.db.SearchCriteria; + +@RunWith(MockitoJUnitRunner.class) +public class InstanceBootGroupDaoImplTest { + + @Spy + InstanceBootGroupDaoImpl instanceBootGroupDaoImplSpy; + + private static final long ACCOUNT_ID = 10L; + private static final String NAME = "boot-group-1"; + + private Map paramMap(SearchCriteria sc) { + Map map = new HashMap<>(); + for (Pair pair : sc.getValues()) { + map.put(pair.first().getColumnName(), pair.second()); + } + return map; + } + + @SuppressWarnings("unchecked") + @Test + public void testListByAccountId() { + List expected = new ArrayList<>(); + expected.add(Mockito.mock(InstanceBootGroupVO.class)); + + Mockito.doReturn(expected).when(instanceBootGroupDaoImplSpy).listBy(Mockito.any(SearchCriteria.class)); + + List result = instanceBootGroupDaoImplSpy.listByAccountId(ACCOUNT_ID); + + Assert.assertEquals(expected, result); + + ArgumentCaptor scCaptor = ArgumentCaptor.forClass(SearchCriteria.class); + Mockito.verify(instanceBootGroupDaoImplSpy).listBy(scCaptor.capture()); + Map params = paramMap(scCaptor.getValue()); + Assert.assertEquals(ACCOUNT_ID, params.get("account_id")); + } + + @SuppressWarnings("unchecked") + @Test + public void testIsNameInUseTrueWhenResultsFound() { + List found = Collections.singletonList(Mockito.mock(InstanceBootGroupVO.class)); + Mockito.doReturn(found).when(instanceBootGroupDaoImplSpy).listBy(Mockito.any(SearchCriteria.class)); + + boolean result = instanceBootGroupDaoImplSpy.isNameInUse(ACCOUNT_ID, NAME); + + Assert.assertTrue(result); + + ArgumentCaptor scCaptor = ArgumentCaptor.forClass(SearchCriteria.class); + Mockito.verify(instanceBootGroupDaoImplSpy).listBy(scCaptor.capture()); + Map params = paramMap(scCaptor.getValue()); + Assert.assertEquals(ACCOUNT_ID, params.get("account_id")); + Assert.assertEquals(NAME, params.get("name")); + } + + @SuppressWarnings("unchecked") + @Test + public void testIsNameInUseFalseWhenNoResults() { + Mockito.doReturn(new ArrayList<>()).when(instanceBootGroupDaoImplSpy).listBy(Mockito.any(SearchCriteria.class)); + + boolean result = instanceBootGroupDaoImplSpy.isNameInUse(ACCOUNT_ID, NAME); + + Assert.assertFalse(result); + } +} diff --git a/engine/schema/src/test/java/com/cloud/vm/dao/InstanceBootGroupDetailsDaoImplTest.java b/engine/schema/src/test/java/com/cloud/vm/dao/InstanceBootGroupDetailsDaoImplTest.java new file mode 100644 index 000000000000..3fe22b5889fe --- /dev/null +++ b/engine/schema/src/test/java/com/cloud/vm/dao/InstanceBootGroupDetailsDaoImplTest.java @@ -0,0 +1,143 @@ +// 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.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +import org.apache.cloudstack.vm.bootgroup.InstanceBootGroupDetailsVO; +import org.junit.Assert; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.ArgumentCaptor; +import org.mockito.Mockito; +import org.mockito.Spy; +import org.mockito.junit.MockitoJUnitRunner; + +import com.cloud.utils.Pair; +import com.cloud.utils.db.Attribute; +import com.cloud.utils.db.SearchCriteria; + +@RunWith(MockitoJUnitRunner.class) +public class InstanceBootGroupDetailsDaoImplTest { + + @Spy + InstanceBootGroupDetailsDaoImpl instanceBootGroupDetailsDaoImplSpy; + + private static final long BOOT_GROUP_ID = 7L; + private static final String NAME = "readiness.timeout"; + private static final String VALUE = "300"; + + private Map paramMap(SearchCriteria sc) { + Map map = new HashMap<>(); + for (Pair pair : sc.getValues()) { + map.put(pair.first().getColumnName(), pair.second()); + } + return map; + } + + @SuppressWarnings("unchecked") + @Test + public void testGetDetailWhenExists() { + InstanceBootGroupDetailsVO detail = Mockito.mock(InstanceBootGroupDetailsVO.class); + Mockito.when(detail.getValue()).thenReturn(VALUE); + Mockito.doReturn(detail).when(instanceBootGroupDetailsDaoImplSpy).findOneBy(Mockito.any(SearchCriteria.class)); + + String result = instanceBootGroupDetailsDaoImplSpy.getDetail(BOOT_GROUP_ID, NAME); + + Assert.assertEquals(VALUE, result); + + ArgumentCaptor scCaptor = ArgumentCaptor.forClass(SearchCriteria.class); + Mockito.verify(instanceBootGroupDetailsDaoImplSpy).findOneBy(scCaptor.capture()); + Map params = paramMap(scCaptor.getValue()); + Assert.assertEquals(BOOT_GROUP_ID, params.get("boot_group_id")); + Assert.assertEquals(NAME, params.get("name")); + } + + @SuppressWarnings("unchecked") + @Test + public void testGetDetailWhenNotExists() { + Mockito.doReturn(null).when(instanceBootGroupDetailsDaoImplSpy).findOneBy(Mockito.any(SearchCriteria.class)); + + String result = instanceBootGroupDetailsDaoImplSpy.getDetail(BOOT_GROUP_ID, NAME); + + Assert.assertNull(result); + } + + @SuppressWarnings("unchecked") + @Test + public void testSetDetailInsertsWhenNotExisting() { + Mockito.doReturn(null).when(instanceBootGroupDetailsDaoImplSpy).findOneBy(Mockito.any(SearchCriteria.class)); + Mockito.doReturn(null).when(instanceBootGroupDetailsDaoImplSpy).persist(Mockito.any(InstanceBootGroupDetailsVO.class)); + + instanceBootGroupDetailsDaoImplSpy.setDetail(BOOT_GROUP_ID, NAME, VALUE); + + ArgumentCaptor voCaptor = ArgumentCaptor.forClass(InstanceBootGroupDetailsVO.class); + Mockito.verify(instanceBootGroupDetailsDaoImplSpy).persist(voCaptor.capture()); + Assert.assertEquals(BOOT_GROUP_ID, voCaptor.getValue().getResourceId()); + Assert.assertEquals(NAME, voCaptor.getValue().getName()); + Assert.assertEquals(VALUE, voCaptor.getValue().getValue()); + Mockito.verify(instanceBootGroupDetailsDaoImplSpy, Mockito.never()).update(Mockito.anyLong(), Mockito.any()); + } + + @SuppressWarnings("unchecked") + @Test + public void testSetDetailUpdatesWhenExisting() { + InstanceBootGroupDetailsVO existing = new InstanceBootGroupDetailsVO(BOOT_GROUP_ID, NAME, "old-value"); + Mockito.doReturn(existing).when(instanceBootGroupDetailsDaoImplSpy).findOneBy(Mockito.any(SearchCriteria.class)); + Mockito.doReturn(true).when(instanceBootGroupDetailsDaoImplSpy).update(Mockito.anyLong(), Mockito.any()); + + instanceBootGroupDetailsDaoImplSpy.setDetail(BOOT_GROUP_ID, NAME, VALUE); + + Assert.assertEquals(VALUE, existing.getValue()); + Mockito.verify(instanceBootGroupDetailsDaoImplSpy).update(existing.getId(), existing); + Mockito.verify(instanceBootGroupDetailsDaoImplSpy, Mockito.never()).persist(Mockito.any(InstanceBootGroupDetailsVO.class)); + } + + @SuppressWarnings("unchecked") + @Test + public void testListDetails() { + List details = new ArrayList<>(); + details.add(new InstanceBootGroupDetailsVO(BOOT_GROUP_ID, "key1", "value1")); + details.add(new InstanceBootGroupDetailsVO(BOOT_GROUP_ID, "key2", "value2")); + Mockito.doReturn(details).when(instanceBootGroupDetailsDaoImplSpy).search(Mockito.any(SearchCriteria.class), Mockito.isNull()); + + Map result = instanceBootGroupDetailsDaoImplSpy.listDetailsKeyPairs(BOOT_GROUP_ID); + + Assert.assertEquals(2, result.size()); + Assert.assertEquals("value1", result.get("key1")); + Assert.assertEquals("value2", result.get("key2")); + + ArgumentCaptor scCaptor = ArgumentCaptor.forClass(SearchCriteria.class); + Mockito.verify(instanceBootGroupDetailsDaoImplSpy).search(scCaptor.capture(), Mockito.isNull()); + Map params = paramMap(scCaptor.getValue()); + Assert.assertEquals(BOOT_GROUP_ID, params.get("boot_group_id")); + } + + @SuppressWarnings("unchecked") + @Test + public void testListDetailsEmpty() { + Mockito.doReturn(new ArrayList<>()).when(instanceBootGroupDetailsDaoImplSpy).search(Mockito.any(SearchCriteria.class), Mockito.isNull()); + + Map result = instanceBootGroupDetailsDaoImplSpy.listDetailsKeyPairs(BOOT_GROUP_ID); + + Assert.assertNotNull(result); + Assert.assertTrue(result.isEmpty()); + } +} diff --git a/engine/schema/src/test/java/com/cloud/vm/dao/InstanceBootGroupMemberDaoImplTest.java b/engine/schema/src/test/java/com/cloud/vm/dao/InstanceBootGroupMemberDaoImplTest.java new file mode 100644 index 000000000000..e2719a17a727 --- /dev/null +++ b/engine/schema/src/test/java/com/cloud/vm/dao/InstanceBootGroupMemberDaoImplTest.java @@ -0,0 +1,144 @@ +// 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.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +import org.apache.cloudstack.vm.bootgroup.InstanceBootGroupMember; +import org.apache.cloudstack.vm.bootgroup.InstanceBootGroupMemberVO; +import org.junit.Assert; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.ArgumentCaptor; +import org.mockito.Mockito; +import org.mockito.Spy; +import org.mockito.junit.MockitoJUnitRunner; + +import com.cloud.utils.Pair; +import com.cloud.utils.db.Attribute; +import com.cloud.utils.db.SearchCriteria; + +@RunWith(MockitoJUnitRunner.class) +public class InstanceBootGroupMemberDaoImplTest { + + @Spy + InstanceBootGroupMemberDaoImpl instanceBootGroupMemberDaoImplSpy; + + private static final long BOOT_GROUP_ID = 5L; + private static final long MEMBER_ID = 42L; + + private Map paramMap(SearchCriteria sc) { + Map map = new HashMap<>(); + for (Pair pair : sc.getValues()) { + map.put(pair.first().getColumnName(), pair.second()); + } + return map; + } + + @SuppressWarnings("unchecked") + @Test + public void testListByBootGroupId() { + List expected = List.of(Mockito.mock(InstanceBootGroupMemberVO.class)); + Mockito.doReturn(expected).when(instanceBootGroupMemberDaoImplSpy).listBy(Mockito.any(SearchCriteria.class), Mockito.isNull()); + + List result = instanceBootGroupMemberDaoImplSpy.listByBootGroupId(BOOT_GROUP_ID); + + Assert.assertEquals(expected, result); + + ArgumentCaptor scCaptor = ArgumentCaptor.forClass(SearchCriteria.class); + Mockito.verify(instanceBootGroupMemberDaoImplSpy).listBy(scCaptor.capture(), Mockito.isNull()); + Map params = paramMap(scCaptor.getValue()); + Assert.assertEquals(BOOT_GROUP_ID, params.get("boot_group_id")); + } + + @SuppressWarnings("unchecked") + @Test + public void testSearchAndCountByBootGroupId() { + Pair, Integer> expected = new Pair<>(new ArrayList<>(), 0); + Mockito.doReturn(expected).when(instanceBootGroupMemberDaoImplSpy).searchAndCount(Mockito.any(SearchCriteria.class), Mockito.isNull()); + + Pair, Integer> result = instanceBootGroupMemberDaoImplSpy.searchAndCountByBootGroupId(BOOT_GROUP_ID); + + Assert.assertEquals(expected, result); + + ArgumentCaptor scCaptor = ArgumentCaptor.forClass(SearchCriteria.class); + Mockito.verify(instanceBootGroupMemberDaoImplSpy).searchAndCount(scCaptor.capture(), Mockito.isNull()); + Map params = paramMap(scCaptor.getValue()); + Assert.assertEquals(BOOT_GROUP_ID, params.get("boot_group_id")); + } + + @SuppressWarnings("unchecked") + @Test + public void testSearchAndCountByBootGroupIdAndType() { + Pair, Integer> expected = new Pair<>(new ArrayList<>(), 0); + Mockito.doReturn(expected).when(instanceBootGroupMemberDaoImplSpy).searchAndCount(Mockito.any(SearchCriteria.class), Mockito.isNull()); + + Pair, Integer> result = instanceBootGroupMemberDaoImplSpy + .searchAndCountByBootGroupIdAndType(BOOT_GROUP_ID, InstanceBootGroupMember.MemberType.VirtualMachine); + + Assert.assertEquals(expected, result); + + ArgumentCaptor scCaptor = ArgumentCaptor.forClass(SearchCriteria.class); + Mockito.verify(instanceBootGroupMemberDaoImplSpy).searchAndCount(scCaptor.capture(), Mockito.isNull()); + Map params = paramMap(scCaptor.getValue()); + Assert.assertEquals(BOOT_GROUP_ID, params.get("boot_group_id")); + Assert.assertEquals(InstanceBootGroupMember.MemberType.VirtualMachine, params.get("member_type")); + } + + @SuppressWarnings("unchecked") + @Test + public void testFindByMember() { + InstanceBootGroupMemberVO expected = Mockito.mock(InstanceBootGroupMemberVO.class); + Mockito.doReturn(expected).when(instanceBootGroupMemberDaoImplSpy).findOneBy(Mockito.any(SearchCriteria.class)); + + InstanceBootGroupMemberVO result = instanceBootGroupMemberDaoImplSpy.findByMember(InstanceBootGroupMember.MemberType.InstanceGroup, MEMBER_ID); + + Assert.assertEquals(expected, result); + + ArgumentCaptor scCaptor = ArgumentCaptor.forClass(SearchCriteria.class); + Mockito.verify(instanceBootGroupMemberDaoImplSpy).findOneBy(scCaptor.capture()); + Map params = paramMap(scCaptor.getValue()); + Assert.assertEquals(InstanceBootGroupMember.MemberType.InstanceGroup, params.get("member_type")); + Assert.assertEquals(MEMBER_ID, params.get("member_id")); + } + + @SuppressWarnings("unchecked") + @Test + public void testFindByMemberNotFound() { + Mockito.doReturn(null).when(instanceBootGroupMemberDaoImplSpy).findOneBy(Mockito.any(SearchCriteria.class)); + + InstanceBootGroupMemberVO result = instanceBootGroupMemberDaoImplSpy.findByMember(InstanceBootGroupMember.MemberType.VirtualMachine, MEMBER_ID); + + Assert.assertNull(result); + } + + @SuppressWarnings("unchecked") + @Test + public void testDeleteByBootGroupId() { + Mockito.doReturn(1).when(instanceBootGroupMemberDaoImplSpy).expunge(Mockito.any(SearchCriteria.class)); + + instanceBootGroupMemberDaoImplSpy.deleteByBootGroupId(BOOT_GROUP_ID); + + ArgumentCaptor scCaptor = ArgumentCaptor.forClass(SearchCriteria.class); + Mockito.verify(instanceBootGroupMemberDaoImplSpy).expunge(scCaptor.capture()); + Map params = paramMap(scCaptor.getValue()); + Assert.assertEquals(BOOT_GROUP_ID, params.get("boot_group_id")); + } +} diff --git a/engine/schema/src/test/java/com/cloud/vm/dao/InstanceBootGroupReadinessCheckResultDaoImplTest.java b/engine/schema/src/test/java/com/cloud/vm/dao/InstanceBootGroupReadinessCheckResultDaoImplTest.java new file mode 100644 index 000000000000..29b76943552e --- /dev/null +++ b/engine/schema/src/test/java/com/cloud/vm/dao/InstanceBootGroupReadinessCheckResultDaoImplTest.java @@ -0,0 +1,120 @@ +// 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.Date; +import java.util.HashMap; +import java.util.Map; + +import org.apache.cloudstack.vm.bootgroup.InstanceBootGroupReadinessCheckResultVO; +import org.apache.cloudstack.vm.bootgroup.readiness.InstanceBootGroupReadinessRule; +import org.junit.Assert; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.ArgumentCaptor; +import org.mockito.Mockito; +import org.mockito.Spy; +import org.mockito.junit.MockitoJUnitRunner; + +import com.cloud.utils.Pair; +import com.cloud.utils.db.Attribute; +import com.cloud.utils.db.SearchCriteria; + +@RunWith(MockitoJUnitRunner.class) +public class InstanceBootGroupReadinessCheckResultDaoImplTest { + + @Spy + InstanceBootGroupReadinessCheckResultDaoImpl instanceBootGroupReadinessCheckResultDaoImplSpy; + + private static final long RULE_ID = 21L; + private static final long VM_ID = 99L; + + private Map paramMap(SearchCriteria sc) { + Map map = new HashMap<>(); + for (Pair pair : sc.getValues()) { + map.put(pair.first().getColumnName(), pair.second()); + } + return map; + } + + @SuppressWarnings("unchecked") + @Test + public void testFindByRuleAndVm() { + InstanceBootGroupReadinessCheckResultVO expected = Mockito.mock(InstanceBootGroupReadinessCheckResultVO.class); + Mockito.doReturn(expected).when(instanceBootGroupReadinessCheckResultDaoImplSpy).findOneBy(Mockito.any(SearchCriteria.class)); + + InstanceBootGroupReadinessCheckResultVO result = instanceBootGroupReadinessCheckResultDaoImplSpy.findByRuleAndVm(RULE_ID, VM_ID); + + Assert.assertEquals(expected, result); + + ArgumentCaptor scCaptor = ArgumentCaptor.forClass(SearchCriteria.class); + Mockito.verify(instanceBootGroupReadinessCheckResultDaoImplSpy).findOneBy(scCaptor.capture()); + Map params = paramMap(scCaptor.getValue()); + Assert.assertEquals(RULE_ID, params.get("rule_id")); + Assert.assertEquals(VM_ID, params.get("vm_id")); + } + + @SuppressWarnings("unchecked") + @Test + public void testUpsertInsertsWhenNoExistingResult() { + Mockito.doReturn(null).when(instanceBootGroupReadinessCheckResultDaoImplSpy).findOneBy(Mockito.any(SearchCriteria.class)); + Mockito.doReturn(null).when(instanceBootGroupReadinessCheckResultDaoImplSpy).persist(Mockito.any(InstanceBootGroupReadinessCheckResultVO.class)); + + Date checkedOn = new Date(); + instanceBootGroupReadinessCheckResultDaoImplSpy.upsert(RULE_ID, VM_ID, InstanceBootGroupReadinessRule.Status.Ready, "all good", checkedOn); + + ArgumentCaptor voCaptor = ArgumentCaptor.forClass(InstanceBootGroupReadinessCheckResultVO.class); + Mockito.verify(instanceBootGroupReadinessCheckResultDaoImplSpy).persist(voCaptor.capture()); + Assert.assertEquals(RULE_ID, voCaptor.getValue().getRuleId()); + Assert.assertEquals(VM_ID, voCaptor.getValue().getVmId()); + Assert.assertEquals(InstanceBootGroupReadinessRule.Status.Ready, voCaptor.getValue().getStatus()); + Assert.assertEquals("all good", voCaptor.getValue().getMessage()); + Assert.assertEquals(checkedOn, voCaptor.getValue().getCheckedOn()); + Mockito.verify(instanceBootGroupReadinessCheckResultDaoImplSpy, Mockito.never()).update(Mockito.anyLong(), Mockito.any()); + } + + @SuppressWarnings("unchecked") + @Test + public void testUpsertUpdatesWhenExistingResult() { + InstanceBootGroupReadinessCheckResultVO existing = new InstanceBootGroupReadinessCheckResultVO( + RULE_ID, VM_ID, InstanceBootGroupReadinessRule.Status.Unknown, "old", new Date(0)); + Mockito.doReturn(existing).when(instanceBootGroupReadinessCheckResultDaoImplSpy).findOneBy(Mockito.any(SearchCriteria.class)); + Mockito.doReturn(true).when(instanceBootGroupReadinessCheckResultDaoImplSpy).update(Mockito.anyLong(), Mockito.any()); + + Date checkedOn = new Date(); + instanceBootGroupReadinessCheckResultDaoImplSpy.upsert(RULE_ID, VM_ID, InstanceBootGroupReadinessRule.Status.NotReady, "still booting", checkedOn); + + Assert.assertEquals(InstanceBootGroupReadinessRule.Status.NotReady, existing.getStatus()); + Assert.assertEquals("still booting", existing.getMessage()); + Assert.assertEquals(checkedOn, existing.getCheckedOn()); + Mockito.verify(instanceBootGroupReadinessCheckResultDaoImplSpy).update(existing.getId(), existing); + Mockito.verify(instanceBootGroupReadinessCheckResultDaoImplSpy, Mockito.never()).persist(Mockito.any(InstanceBootGroupReadinessCheckResultVO.class)); + } + + @SuppressWarnings("unchecked") + @Test + public void testDeleteByRuleId() { + Mockito.doReturn(1).when(instanceBootGroupReadinessCheckResultDaoImplSpy).expunge(Mockito.any(SearchCriteria.class)); + + instanceBootGroupReadinessCheckResultDaoImplSpy.deleteByRuleId(RULE_ID); + + ArgumentCaptor scCaptor = ArgumentCaptor.forClass(SearchCriteria.class); + Mockito.verify(instanceBootGroupReadinessCheckResultDaoImplSpy).expunge(scCaptor.capture()); + Map params = paramMap(scCaptor.getValue()); + Assert.assertEquals(RULE_ID, params.get("rule_id")); + } +} diff --git a/engine/schema/src/test/java/com/cloud/vm/dao/InstanceBootGroupReadinessRuleDaoImplTest.java b/engine/schema/src/test/java/com/cloud/vm/dao/InstanceBootGroupReadinessRuleDaoImplTest.java new file mode 100644 index 000000000000..372ddc536ac2 --- /dev/null +++ b/engine/schema/src/test/java/com/cloud/vm/dao/InstanceBootGroupReadinessRuleDaoImplTest.java @@ -0,0 +1,147 @@ +// 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.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +import org.apache.cloudstack.vm.bootgroup.InstanceBootGroupMember; +import org.apache.cloudstack.vm.bootgroup.InstanceBootGroupReadinessRuleVO; +import org.apache.cloudstack.vm.bootgroup.readiness.InstanceBootGroupReadinessRule; +import org.junit.Assert; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.ArgumentCaptor; +import org.mockito.Mockito; +import org.mockito.Spy; +import org.mockito.junit.MockitoJUnitRunner; + +import com.cloud.utils.Pair; +import com.cloud.utils.db.Attribute; +import com.cloud.utils.db.Filter; +import com.cloud.utils.db.SearchCriteria; + +@RunWith(MockitoJUnitRunner.class) +public class InstanceBootGroupReadinessRuleDaoImplTest { + + @Spy + InstanceBootGroupReadinessRuleDaoImpl instanceBootGroupReadinessRuleDaoImplSpy; + + private static final long BOOT_GROUP_ID = 3L; + private static final long ITEM_ID = 8L; + + private Map paramMap(SearchCriteria sc) { + Map map = new HashMap<>(); + for (Pair pair : sc.getValues()) { + map.put(pair.first().getColumnName(), pair.second()); + } + return map; + } + + @SuppressWarnings("unchecked") + @Test + public void testSearchAndCountByBootGroupIdWithAllFiltersSet() { + Pair, Integer> expected = new Pair<>(new ArrayList<>(), 0); + Mockito.doReturn(expected).when(instanceBootGroupReadinessRuleDaoImplSpy) + .searchAndCount(Mockito.any(SearchCriteria.class), Mockito.any(Filter.class)); + + Pair, Integer> result = instanceBootGroupReadinessRuleDaoImplSpy.searchAndCountByBootGroupId( + BOOT_GROUP_ID, 1L, InstanceBootGroupMember.MemberType.VirtualMachine, ITEM_ID, + InstanceBootGroupReadinessRule.RuleType.Ping, "healthcheck", 0L, 10L); + + Assert.assertEquals(expected, result); + + ArgumentCaptor scCaptor = ArgumentCaptor.forClass(SearchCriteria.class); + ArgumentCaptor filterCaptor = ArgumentCaptor.forClass(Filter.class); + Mockito.verify(instanceBootGroupReadinessRuleDaoImplSpy).searchAndCount(scCaptor.capture(), filterCaptor.capture()); + + Map params = paramMap(scCaptor.getValue()); + Assert.assertEquals(BOOT_GROUP_ID, params.get("boot_group_id")); + Assert.assertEquals(1L, params.get("id")); + Assert.assertEquals(InstanceBootGroupMember.MemberType.VirtualMachine, params.get("item_type")); + Assert.assertEquals(ITEM_ID, params.get("item_id")); + Assert.assertEquals(InstanceBootGroupReadinessRule.RuleType.Ping, params.get("rule_type")); + Assert.assertEquals("%healthcheck%", params.get("name")); + Assert.assertNotNull(filterCaptor.getValue()); + } + + @SuppressWarnings("unchecked") + @Test + public void testSearchAndCountByBootGroupIdWithOnlyMandatoryParams() { + Pair, Integer> expected = new Pair<>(new ArrayList<>(), 0); + Mockito.doReturn(expected).when(instanceBootGroupReadinessRuleDaoImplSpy) + .searchAndCount(Mockito.any(SearchCriteria.class), Mockito.any(Filter.class)); + + Pair, Integer> result = instanceBootGroupReadinessRuleDaoImplSpy.searchAndCountByBootGroupId( + BOOT_GROUP_ID, null, null, null, null, null, null, null); + + Assert.assertEquals(expected, result); + + ArgumentCaptor scCaptor = ArgumentCaptor.forClass(SearchCriteria.class); + Mockito.verify(instanceBootGroupReadinessRuleDaoImplSpy).searchAndCount(scCaptor.capture(), Mockito.any(Filter.class)); + + Map params = paramMap(scCaptor.getValue()); + Assert.assertEquals(BOOT_GROUP_ID, params.get("boot_group_id")); + Assert.assertFalse(params.containsKey("id")); + Assert.assertFalse(params.containsKey("item_type")); + Assert.assertFalse(params.containsKey("item_id")); + Assert.assertFalse(params.containsKey("rule_type")); + Assert.assertFalse(params.containsKey("name")); + } + + @SuppressWarnings("unchecked") + @Test + public void testListEnabledByItem() { + List expected = List.of(Mockito.mock(InstanceBootGroupReadinessRuleVO.class)); + Mockito.doReturn(expected).when(instanceBootGroupReadinessRuleDaoImplSpy).listBy(Mockito.any(SearchCriteria.class)); + + List result = instanceBootGroupReadinessRuleDaoImplSpy.listEnabledByItem( + BOOT_GROUP_ID, InstanceBootGroupMember.MemberType.InstanceGroup, ITEM_ID); + + Assert.assertEquals(expected, result); + + ArgumentCaptor scCaptor = ArgumentCaptor.forClass(SearchCriteria.class); + Mockito.verify(instanceBootGroupReadinessRuleDaoImplSpy).listBy(scCaptor.capture()); + Map params = paramMap(scCaptor.getValue()); + Assert.assertEquals(BOOT_GROUP_ID, params.get("boot_group_id")); + Assert.assertEquals(InstanceBootGroupMember.MemberType.InstanceGroup, params.get("item_type")); + Assert.assertEquals(ITEM_ID, params.get("item_id")); + Assert.assertEquals(Boolean.TRUE, params.get("enabled")); + } + + @SuppressWarnings("unchecked") + @Test + public void testListByItem() { + List expected = List.of(Mockito.mock(InstanceBootGroupReadinessRuleVO.class)); + Mockito.doReturn(expected).when(instanceBootGroupReadinessRuleDaoImplSpy).listBy(Mockito.any(SearchCriteria.class)); + + List result = instanceBootGroupReadinessRuleDaoImplSpy.listByItem( + BOOT_GROUP_ID, InstanceBootGroupMember.MemberType.VirtualMachine, ITEM_ID); + + Assert.assertEquals(expected, result); + + ArgumentCaptor scCaptor = ArgumentCaptor.forClass(SearchCriteria.class); + Mockito.verify(instanceBootGroupReadinessRuleDaoImplSpy).listBy(scCaptor.capture()); + Map params = paramMap(scCaptor.getValue()); + Assert.assertEquals(BOOT_GROUP_ID, params.get("boot_group_id")); + Assert.assertEquals(InstanceBootGroupMember.MemberType.VirtualMachine, params.get("item_type")); + Assert.assertEquals(ITEM_ID, params.get("item_id")); + Assert.assertFalse(params.containsKey("enabled")); + } +} diff --git a/engine/schema/src/test/java/com/cloud/vm/dao/InstanceBootGroupReadinessRuleDetailsDaoImplTest.java b/engine/schema/src/test/java/com/cloud/vm/dao/InstanceBootGroupReadinessRuleDetailsDaoImplTest.java new file mode 100644 index 000000000000..dcfa00f9f362 --- /dev/null +++ b/engine/schema/src/test/java/com/cloud/vm/dao/InstanceBootGroupReadinessRuleDetailsDaoImplTest.java @@ -0,0 +1,116 @@ +// 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.ArrayList; +import java.util.List; +import java.util.Map; + +import org.apache.cloudstack.vm.bootgroup.InstanceBootGroupReadinessRuleDetailsVO; +import org.junit.Assert; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.ArgumentCaptor; +import org.mockito.Mockito; +import org.mockito.Spy; +import org.mockito.junit.MockitoJUnitRunner; + +import com.cloud.utils.crypt.DBEncryptionUtil; +import com.cloud.utils.db.SearchCriteria; + +@RunWith(MockitoJUnitRunner.class) +public class InstanceBootGroupReadinessRuleDetailsDaoImplTest { + + @Spy + InstanceBootGroupReadinessRuleDetailsDaoImpl instanceBootGroupReadinessRuleDetailsDaoImplSpy; + + private static final long RULE_ID = 15L; + + @SuppressWarnings("unchecked") + @Test + public void testAddDetailNonScriptKeyStoredAsIs() { + Mockito.doReturn(null).when(instanceBootGroupReadinessRuleDetailsDaoImplSpy).findDetail(RULE_ID, "port"); + Mockito.doReturn(null).when(instanceBootGroupReadinessRuleDetailsDaoImplSpy).persist(Mockito.any(InstanceBootGroupReadinessRuleDetailsVO.class)); + + instanceBootGroupReadinessRuleDetailsDaoImplSpy.addDetail(RULE_ID, "port", "8080", true); + + ArgumentCaptor voCaptor = ArgumentCaptor.forClass(InstanceBootGroupReadinessRuleDetailsVO.class); + Mockito.verify(instanceBootGroupReadinessRuleDetailsDaoImplSpy).persist(voCaptor.capture()); + Assert.assertEquals(RULE_ID, voCaptor.getValue().getResourceId()); + Assert.assertEquals("port", voCaptor.getValue().getName()); + Assert.assertEquals("8080", voCaptor.getValue().getValue()); + } + + @SuppressWarnings("unchecked") + @Test + public void testAddDetailScriptKeyStoredEncrypted() { + Mockito.doReturn(null).when(instanceBootGroupReadinessRuleDetailsDaoImplSpy).findDetail(RULE_ID, "script"); + Mockito.doReturn(null).when(instanceBootGroupReadinessRuleDetailsDaoImplSpy).persist(Mockito.any(InstanceBootGroupReadinessRuleDetailsVO.class)); + + String plainScript = "#!/bin/bash\necho ok"; + instanceBootGroupReadinessRuleDetailsDaoImplSpy.addDetail(RULE_ID, "script", plainScript, true); + + ArgumentCaptor voCaptor = ArgumentCaptor.forClass(InstanceBootGroupReadinessRuleDetailsVO.class); + Mockito.verify(instanceBootGroupReadinessRuleDetailsDaoImplSpy).persist(voCaptor.capture()); + Assert.assertEquals(RULE_ID, voCaptor.getValue().getResourceId()); + Assert.assertEquals("script", voCaptor.getValue().getName()); + Assert.assertEquals(DBEncryptionUtil.encrypt(plainScript), voCaptor.getValue().getValue()); + } + + @SuppressWarnings("unchecked") + @Test + public void testAddDetailReplacesExistingDetail() { + InstanceBootGroupReadinessRuleDetailsVO existing = new InstanceBootGroupReadinessRuleDetailsVO(RULE_ID, "port", "9090"); + Mockito.doReturn(existing).when(instanceBootGroupReadinessRuleDetailsDaoImplSpy).findDetail(RULE_ID, "port"); + Mockito.doReturn(true).when(instanceBootGroupReadinessRuleDetailsDaoImplSpy).remove(existing.getId()); + Mockito.doReturn(null).when(instanceBootGroupReadinessRuleDetailsDaoImplSpy).persist(Mockito.any(InstanceBootGroupReadinessRuleDetailsVO.class)); + + instanceBootGroupReadinessRuleDetailsDaoImplSpy.addDetail(RULE_ID, "port", "8080", true); + + Mockito.verify(instanceBootGroupReadinessRuleDetailsDaoImplSpy).remove(existing.getId()); + ArgumentCaptor voCaptor = ArgumentCaptor.forClass(InstanceBootGroupReadinessRuleDetailsVO.class); + Mockito.verify(instanceBootGroupReadinessRuleDetailsDaoImplSpy).persist(voCaptor.capture()); + Assert.assertEquals("8080", voCaptor.getValue().getValue()); + } + + @SuppressWarnings("unchecked") + @Test + public void testGetDetailsDecryptsScriptKeyAndLeavesOthersAsIs() { + String encryptedScript = DBEncryptionUtil.encrypt("plain-script-body"); + List details = new ArrayList<>(); + details.add(new InstanceBootGroupReadinessRuleDetailsVO(RULE_ID, "script", encryptedScript)); + details.add(new InstanceBootGroupReadinessRuleDetailsVO(RULE_ID, "port", "8080")); + Mockito.doReturn(details).when(instanceBootGroupReadinessRuleDetailsDaoImplSpy).listBy(Mockito.any(SearchCriteria.class)); + + Map result = instanceBootGroupReadinessRuleDetailsDaoImplSpy.getDetails(RULE_ID); + + Assert.assertEquals(2, result.size()); + Assert.assertEquals(DBEncryptionUtil.decrypt(encryptedScript), result.get("script")); + Assert.assertEquals("8080", result.get("port")); + } + + @SuppressWarnings("unchecked") + @Test + public void testGetDetailsEmpty() { + Mockito.doReturn(new ArrayList<>()).when(instanceBootGroupReadinessRuleDetailsDaoImplSpy).listBy(Mockito.any(SearchCriteria.class)); + + Map result = instanceBootGroupReadinessRuleDetailsDaoImplSpy.getDetails(RULE_ID); + + Assert.assertNotNull(result); + Assert.assertTrue(result.isEmpty()); + } +} diff --git a/plugins/hypervisors/kvm/src/main/java/com/cloud/hypervisor/kvm/resource/wrapper/LibvirtCheckGuestAgentLivenessCommandWrapper.java b/plugins/hypervisors/kvm/src/main/java/com/cloud/hypervisor/kvm/resource/wrapper/LibvirtCheckGuestAgentLivenessCommandWrapper.java new file mode 100644 index 000000000000..2be2426ec28a --- /dev/null +++ b/plugins/hypervisors/kvm/src/main/java/com/cloud/hypervisor/kvm/resource/wrapper/LibvirtCheckGuestAgentLivenessCommandWrapper.java @@ -0,0 +1,99 @@ +// +// 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.hypervisor.kvm.resource.wrapper; + +import org.apache.cloudstack.utils.qemu.QemuCommand; +import org.libvirt.Connect; +import org.libvirt.Domain; +import org.libvirt.DomainInfo.DomainState; +import org.libvirt.LibvirtException; + +import com.cloud.agent.api.Answer; +import com.cloud.agent.api.CheckGuestAgentLivenessAnswer; +import com.cloud.agent.api.CheckGuestAgentLivenessCommand; +import com.cloud.hypervisor.kvm.resource.LibvirtComputingResource; +import com.cloud.resource.CommandWrapper; +import com.cloud.resource.ResourceWrapper; +import com.google.gson.JsonElement; +import com.google.gson.JsonParseException; +import com.google.gson.JsonParser; + +@ResourceWrapper(handles = CheckGuestAgentLivenessCommand.class) +public class LibvirtCheckGuestAgentLivenessCommandWrapper extends CommandWrapper { + + private static final int MIN_AGENT_PING_TIMEOUT_SECONDS = 1; + private static final int MAX_RESULT_MESSAGE_LENGTH = 256; + + @Override + public Answer execute(CheckGuestAgentLivenessCommand command, LibvirtComputingResource serverResource) { + String vmName = command.getVmName(); + Domain domain = null; + try { + final LibvirtUtilitiesHelper libvirtUtilitiesHelper = serverResource.getLibvirtUtilitiesHelper(); + Connect connect = libvirtUtilitiesHelper.getConnection(); + domain = serverResource.getDomain(connect, vmName); + if (domain == null) { + return new CheckGuestAgentLivenessAnswer(command, false, String.format("VM %s was not found", vmName)); + } + + DomainState domainState = domain.getInfo().state; + if (domainState != DomainState.VIR_DOMAIN_RUNNING) { + return new CheckGuestAgentLivenessAnswer(command, false, String.format("VM %s is in %s state", vmName, domainState)); + } + + int timeoutSeconds = Math.max(command.getWait(), MIN_AGENT_PING_TIMEOUT_SECONDS); + String result = domain.qemuAgentCommand(QemuCommand.buildQemuCommand(QemuCommand.AGENT_PING, null), timeoutSeconds, 0); + return parseJsonResult(command, result); + } catch (LibvirtException e) { + return new CheckGuestAgentLivenessAnswer(command, false, "guest agent did not respond: " + e.getMessage()); + } finally { + if (domain != null) { + try { + domain.free(); + } catch (LibvirtException e) { + logger.trace("Ignore error ", e); + } + } + } + } + + private CheckGuestAgentLivenessAnswer parseJsonResult(CheckGuestAgentLivenessCommand command, String result) { + if (result == null || result.isBlank()) { + logger.error("Guest agent returned empty response"); + return new CheckGuestAgentLivenessAnswer(command, false, "guest agent returned empty response"); + } + try { + JsonElement parsedResult = JsonParser.parseString(result); + if (parsedResult.isJsonObject() && parsedResult.getAsJsonObject().has("return") && !parsedResult.getAsJsonObject().has("error")) { + return new CheckGuestAgentLivenessAnswer(command, true, "guest agent responded"); + } + } catch (JsonParseException e) { + return new CheckGuestAgentLivenessAnswer(command, false, "guest agent returned invalid JSON: " + abbreviateResultForMessage(result)); + } + return new CheckGuestAgentLivenessAnswer(command, false, "guest agent did not respond as expected: " + abbreviateResultForMessage(result)); + } + + private String abbreviateResultForMessage(String result) { + if (result.length() <= MAX_RESULT_MESSAGE_LENGTH) { + return result; + } + return result.substring(0, MAX_RESULT_MESSAGE_LENGTH) + "..."; + } +} diff --git a/plugins/hypervisors/kvm/src/main/java/org/apache/cloudstack/utils/qemu/QemuCommand.java b/plugins/hypervisors/kvm/src/main/java/org/apache/cloudstack/utils/qemu/QemuCommand.java index 56d44ff51ba8..9e0a6b364fb6 100644 --- a/plugins/hypervisors/kvm/src/main/java/org/apache/cloudstack/utils/qemu/QemuCommand.java +++ b/plugins/hypervisors/kvm/src/main/java/org/apache/cloudstack/utils/qemu/QemuCommand.java @@ -28,6 +28,7 @@ public class QemuCommand { public static final String AGENT_FREEZE = "guest-fsfreeze-freeze"; public static final String AGENT_THAW = "guest-fsfreeze-thaw"; public static final String AGENT_FREEZE_STATUS = "guest-fsfreeze-status"; + public static final String AGENT_PING = "guest-ping"; public static final String QEMU_CMD = "execute"; diff --git a/plugins/hypervisors/kvm/src/test/java/com/cloud/hypervisor/kvm/resource/wrapper/LibvirtCheckGuestAgentLivenessCommandWrapperTest.java b/plugins/hypervisors/kvm/src/test/java/com/cloud/hypervisor/kvm/resource/wrapper/LibvirtCheckGuestAgentLivenessCommandWrapperTest.java new file mode 100644 index 000000000000..9d603e0753c2 --- /dev/null +++ b/plugins/hypervisors/kvm/src/test/java/com/cloud/hypervisor/kvm/resource/wrapper/LibvirtCheckGuestAgentLivenessCommandWrapperTest.java @@ -0,0 +1,236 @@ +// 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.hypervisor.kvm.resource.wrapper; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertTrue; +import static org.mockito.ArgumentMatchers.anyInt; +import static org.mockito.ArgumentMatchers.anyString; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.libvirt.Connect; +import org.libvirt.Domain; +import org.libvirt.DomainInfo; +import org.libvirt.DomainInfo.DomainState; +import org.libvirt.LibvirtException; +import org.mockito.ArgumentCaptor; +import org.mockito.Mock; +import org.mockito.junit.MockitoJUnitRunner; + +import com.cloud.agent.api.CheckGuestAgentLivenessAnswer; +import com.cloud.agent.api.CheckGuestAgentLivenessCommand; +import com.cloud.hypervisor.kvm.resource.LibvirtComputingResource; + +@RunWith(MockitoJUnitRunner.class) +public class LibvirtCheckGuestAgentLivenessCommandWrapperTest { + + private static final String VM_NAME = "i-2-3-VM"; + + @Mock + private LibvirtComputingResource libvirtComputingResource; + @Mock + private LibvirtUtilitiesHelper libvirtUtilitiesHelper; + @Mock + private Connect conn; + @Mock + private Domain domain; + + private LibvirtCheckGuestAgentLivenessCommandWrapper wrapper; + private CheckGuestAgentLivenessCommand command; + + @Before + public void setUp() throws LibvirtException { + wrapper = new LibvirtCheckGuestAgentLivenessCommandWrapper(); + command = new CheckGuestAgentLivenessCommand(VM_NAME); + + when(libvirtComputingResource.getLibvirtUtilitiesHelper()).thenReturn(libvirtUtilitiesHelper); + when(libvirtUtilitiesHelper.getConnection()).thenReturn(conn); + } + + private void mockRunningDomain() throws LibvirtException { + when(libvirtComputingResource.getDomain(conn, VM_NAME)).thenReturn(domain); + DomainInfo domainInfo = new DomainInfo(); + domainInfo.state = DomainState.VIR_DOMAIN_RUNNING; + when(domain.getInfo()).thenReturn(domainInfo); + } + + @Test + public void domainNotFoundIsNotAlive() throws LibvirtException { + when(libvirtComputingResource.getDomain(conn, VM_NAME)).thenReturn(null); + + CheckGuestAgentLivenessAnswer answer = (CheckGuestAgentLivenessAnswer) wrapper.execute(command, libvirtComputingResource); + + assertFalse(answer.getResult()); + assertTrue(answer.getDetails().contains("was not found")); + } + + @Test + public void domainNotRunningIsNotAlive() throws LibvirtException { + when(libvirtComputingResource.getDomain(conn, VM_NAME)).thenReturn(domain); + DomainInfo domainInfo = new DomainInfo(); + domainInfo.state = DomainState.VIR_DOMAIN_PAUSED; + when(domain.getInfo()).thenReturn(domainInfo); + + CheckGuestAgentLivenessAnswer answer = (CheckGuestAgentLivenessAnswer) wrapper.execute(command, libvirtComputingResource); + + assertFalse(answer.getResult()); + assertTrue(answer.getDetails().contains("VIR_DOMAIN_PAUSED")); + verify(domain, org.mockito.Mockito.never()).qemuAgentCommand(anyString(), anyInt(), anyInt()); + } + + @Test + public void libvirtExceptionOnConnectionIsNotAlive() throws LibvirtException { + LibvirtException libvirtException = mock(LibvirtException.class); + when(libvirtException.getMessage()).thenReturn("connection refused"); + when(libvirtUtilitiesHelper.getConnection()).thenThrow(libvirtException); + + CheckGuestAgentLivenessAnswer answer = (CheckGuestAgentLivenessAnswer) wrapper.execute(command, libvirtComputingResource); + + assertFalse(answer.getResult()); + assertTrue(answer.getDetails().contains("connection refused")); + } + + @Test + public void positiveJsonReturnIsAlive() throws LibvirtException { + mockRunningDomain(); + when(domain.qemuAgentCommand(anyString(), anyInt(), anyInt())).thenReturn("{\"return\": {}}"); + + CheckGuestAgentLivenessAnswer answer = (CheckGuestAgentLivenessAnswer) wrapper.execute(command, libvirtComputingResource); + + assertTrue(answer.getResult()); + } + + @Test + public void errorFieldInJsonReturnIsNotAlive() throws LibvirtException { + mockRunningDomain(); + when(domain.qemuAgentCommand(anyString(), anyInt(), anyInt())).thenReturn("{\"return\": {}, \"error\": {\"desc\": \"timeout\"}}"); + + CheckGuestAgentLivenessAnswer answer = (CheckGuestAgentLivenessAnswer) wrapper.execute(command, libvirtComputingResource); + + assertFalse(answer.getResult()); + } + + @Test + public void nonJsonObjectReturnIsNotAlive() throws LibvirtException { + mockRunningDomain(); + when(domain.qemuAgentCommand(anyString(), anyInt(), anyInt())).thenReturn("[]"); + + CheckGuestAgentLivenessAnswer answer = (CheckGuestAgentLivenessAnswer) wrapper.execute(command, libvirtComputingResource); + + assertFalse(answer.getResult()); + } + + @Test + public void missingReturnFieldIsNotAlive() throws LibvirtException { + mockRunningDomain(); + when(domain.qemuAgentCommand(anyString(), anyInt(), anyInt())).thenReturn("{\"foo\": \"bar\"}"); + + CheckGuestAgentLivenessAnswer answer = (CheckGuestAgentLivenessAnswer) wrapper.execute(command, libvirtComputingResource); + + assertFalse(answer.getResult()); + } + + @Test + public void invalidJsonIsNotAliveAndMessageIsAbbreviated() throws LibvirtException { + mockRunningDomain(); + String garbage = "{\"return\": \"" + "x".repeat(300); + when(domain.qemuAgentCommand(anyString(), anyInt(), anyInt())).thenReturn(garbage); + + CheckGuestAgentLivenessAnswer answer = (CheckGuestAgentLivenessAnswer) wrapper.execute(command, libvirtComputingResource); + + assertFalse(answer.getResult()); + assertTrue(answer.getDetails().contains("invalid JSON")); + assertTrue(answer.getDetails().endsWith("...")); + assertTrue(answer.getDetails().length() < garbage.length()); + } + + @Test + public void emptyResponseIsNotAlive() throws LibvirtException { + mockRunningDomain(); + when(domain.qemuAgentCommand(anyString(), anyInt(), anyInt())).thenReturn(" "); + + CheckGuestAgentLivenessAnswer answer = (CheckGuestAgentLivenessAnswer) wrapper.execute(command, libvirtComputingResource); + + assertFalse(answer.getResult()); + assertTrue(answer.getDetails().contains("empty response")); + } + + @Test + public void nullResponseIsNotAlive() throws LibvirtException { + mockRunningDomain(); + when(domain.qemuAgentCommand(anyString(), anyInt(), anyInt())).thenReturn(null); + + CheckGuestAgentLivenessAnswer answer = (CheckGuestAgentLivenessAnswer) wrapper.execute(command, libvirtComputingResource); + + assertFalse(answer.getResult()); + } + + @Test + public void zeroWaitIsFlooredToMinimumTimeoutSeconds() throws LibvirtException { + mockRunningDomain(); + command.setWait(0); + when(domain.qemuAgentCommand(anyString(), anyInt(), anyInt())).thenReturn("{\"return\": {}}"); + + wrapper.execute(command, libvirtComputingResource); + + ArgumentCaptor timeoutCaptor = ArgumentCaptor.forClass(Integer.class); + verify(domain).qemuAgentCommand(anyString(), timeoutCaptor.capture(), anyInt()); + assertEquals(Integer.valueOf(1), timeoutCaptor.getValue()); + } + + @Test + public void positiveWaitIsUsedAsTimeoutSeconds() throws LibvirtException { + mockRunningDomain(); + command.setWait(10); + when(domain.qemuAgentCommand(anyString(), anyInt(), anyInt())).thenReturn("{\"return\": {}}"); + + wrapper.execute(command, libvirtComputingResource); + + ArgumentCaptor timeoutCaptor = ArgumentCaptor.forClass(Integer.class); + verify(domain).qemuAgentCommand(anyString(), timeoutCaptor.capture(), anyInt()); + assertEquals(Integer.valueOf(10), timeoutCaptor.getValue()); + } + + @Test + public void domainIsFreedAfterExecution() throws LibvirtException { + mockRunningDomain(); + when(domain.qemuAgentCommand(anyString(), anyInt(), anyInt())).thenReturn("{\"return\": {}}"); + + wrapper.execute(command, libvirtComputingResource); + + verify(domain).free(); + } + + @Test + public void domainFreeExceptionIsSwallowed() throws LibvirtException { + mockRunningDomain(); + when(domain.qemuAgentCommand(anyString(), anyInt(), anyInt())).thenReturn("{\"return\": {}}"); + org.mockito.Mockito.doThrow(mock(LibvirtException.class)).when(domain).free(); + + CheckGuestAgentLivenessAnswer answer = (CheckGuestAgentLivenessAnswer) wrapper.execute(command, libvirtComputingResource); + + assertTrue(answer.getResult()); + } +} diff --git a/server/src/main/java/com/cloud/api/ApiResponseHelper.java b/server/src/main/java/com/cloud/api/ApiResponseHelper.java index 2510dc0a88b4..e49ab29edcf2 100644 --- a/server/src/main/java/com/cloud/api/ApiResponseHelper.java +++ b/server/src/main/java/com/cloud/api/ApiResponseHelper.java @@ -62,6 +62,7 @@ import org.apache.cloudstack.api.response.ASNRangeResponse; import org.apache.cloudstack.api.response.ASNumberResponse; import org.apache.cloudstack.api.response.AccountResponse; +import org.apache.cloudstack.api.response.ApiKeyPairResponse; import org.apache.cloudstack.api.response.ApplicationLoadBalancerInstanceResponse; import org.apache.cloudstack.api.response.ApplicationLoadBalancerResponse; import org.apache.cloudstack.api.response.ApplicationLoadBalancerRuleResponse; @@ -120,7 +121,6 @@ import org.apache.cloudstack.api.response.Ipv4RouteResponse; import org.apache.cloudstack.api.response.Ipv6RouteResponse; import org.apache.cloudstack.api.response.IsolationMethodResponse; -import org.apache.cloudstack.api.response.ApiKeyPairResponse; import org.apache.cloudstack.api.response.LBHealthCheckPolicyResponse; import org.apache.cloudstack.api.response.LBHealthCheckResponse; import org.apache.cloudstack.api.response.LBStickinessPolicyResponse; @@ -247,6 +247,7 @@ import org.apache.logging.log4j.Logger; import com.cloud.agent.api.VgpuTypesInfo; +import com.cloud.api.query.ResourceIdSupport; import com.cloud.api.query.ViewResponseHelper; import com.cloud.api.query.dao.UserVmJoinDao; import com.cloud.api.query.vo.AccountJoinVO; @@ -263,7 +264,6 @@ import com.cloud.api.query.vo.ProjectAccountJoinVO; import com.cloud.api.query.vo.ProjectInvitationJoinVO; import com.cloud.api.query.vo.ProjectJoinVO; -import com.cloud.api.query.ResourceIdSupport; import com.cloud.api.query.vo.ResourceTagJoinVO; import com.cloud.api.query.vo.SecurityGroupJoinVO; import com.cloud.api.query.vo.ServiceOfferingJoinVO; @@ -570,6 +570,18 @@ public static String getPrettyDomainPath(String path) { @Inject private DomainDao domainDao; + @Inject + private com.cloud.vm.dao.InstanceBootGroupDao instanceBootGroupDao; + + @Inject + private com.cloud.vm.dao.InstanceBootGroupMemberDao instanceBootGroupMemberDao; + + @Inject + private com.cloud.vm.dao.UserVmDao userVmDao; + + @Inject + private com.cloud.vm.dao.InstanceGroupDao instanceGroupDao; + @Override public UserResponse createUserResponse(User user) { UserAccountJoinVO vUser = ApiDBUtils.newUserView(user); @@ -1567,7 +1579,6 @@ public VolumeResponse createVolumeResponse(ResponseView view, Volume volume) { public InstanceGroupResponse createInstanceGroupResponse(InstanceGroup group) { InstanceGroupJoinVO vgroup = ApiDBUtils.newInstanceGroupView(group); return ApiDBUtils.newInstanceGroupResponse(vgroup); - } @Override diff --git a/server/src/main/java/com/cloud/vm/UserVmManagerImpl.java b/server/src/main/java/com/cloud/vm/UserVmManagerImpl.java index 8fe862316350..abdb4da444e2 100644 --- a/server/src/main/java/com/cloud/vm/UserVmManagerImpl.java +++ b/server/src/main/java/com/cloud/vm/UserVmManagerImpl.java @@ -164,6 +164,7 @@ import org.apache.cloudstack.utils.bytescale.ByteScaleUtils; import org.apache.cloudstack.utils.security.ParserUtils; import org.apache.cloudstack.vm.UnmanagedVMsManager; +import org.apache.cloudstack.vm.bootgroup.InstanceBootGroupMembershipGuard; import org.apache.cloudstack.vm.lease.VMLeaseManager; import org.apache.commons.collections.CollectionUtils; import org.apache.commons.collections.MapUtils; @@ -657,6 +658,8 @@ public class UserVmManagerImpl extends ManagerBase implements UserVmManager, Vir @Inject private AutoScaleManager autoScaleManager; @Inject + private InstanceBootGroupMembershipGuard instanceBootGroupMembershipGuard; + @Inject NsxProviderDao nsxProviderDao; @Inject NetworkService networkService; @@ -3892,6 +3895,8 @@ public boolean deleteVmGroup(long groupId) { public boolean addInstanceToGroup(final long userVmId, String groupName) { UserVmVO vm = _vmDao.findById(userVmId); + instanceBootGroupMembershipGuard.validateVmEligibleForGroupMembership(userVmId); + InstanceGroupVO group = _vmGroupDao.findByAccountAndName(vm.getAccountId(), groupName); // Create vm group if the group doesn't exist for this account if (group == null) { diff --git a/server/src/main/java/org/apache/cloudstack/api/query/dao/InstanceBootGroupJoinDao.java b/server/src/main/java/org/apache/cloudstack/api/query/dao/InstanceBootGroupJoinDao.java new file mode 100644 index 000000000000..38cc792b5123 --- /dev/null +++ b/server/src/main/java/org/apache/cloudstack/api/query/dao/InstanceBootGroupJoinDao.java @@ -0,0 +1,25 @@ +// 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.query.dao; + +import org.apache.cloudstack.api.query.vo.InstanceBootGroupJoinVO; + +import com.cloud.utils.db.GenericDao; + +public interface InstanceBootGroupJoinDao extends GenericDao { +} diff --git a/server/src/main/java/org/apache/cloudstack/api/query/dao/InstanceBootGroupJoinDaoImpl.java b/server/src/main/java/org/apache/cloudstack/api/query/dao/InstanceBootGroupJoinDaoImpl.java new file mode 100644 index 000000000000..86eb9eb7f554 --- /dev/null +++ b/server/src/main/java/org/apache/cloudstack/api/query/dao/InstanceBootGroupJoinDaoImpl.java @@ -0,0 +1,25 @@ +// 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.query.dao; + +import org.apache.cloudstack.api.query.vo.InstanceBootGroupJoinVO; + +import com.cloud.utils.db.GenericDaoBase; + +public class InstanceBootGroupJoinDaoImpl extends GenericDaoBase implements InstanceBootGroupJoinDao { +} diff --git a/server/src/main/java/org/apache/cloudstack/api/query/vo/InstanceBootGroupJoinVO.java b/server/src/main/java/org/apache/cloudstack/api/query/vo/InstanceBootGroupJoinVO.java new file mode 100644 index 000000000000..91aef4532dfe --- /dev/null +++ b/server/src/main/java/org/apache/cloudstack/api/query/vo/InstanceBootGroupJoinVO.java @@ -0,0 +1,190 @@ +// 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.query.vo; + +import java.util.Date; + +import javax.persistence.Column; +import javax.persistence.Entity; +import javax.persistence.EnumType; +import javax.persistence.Enumerated; +import javax.persistence.Id; +import javax.persistence.Table; + +import com.cloud.api.query.vo.ControlledViewEntity; + +import org.apache.cloudstack.utils.reflectiontostringbuilderutils.ReflectionToStringBuilderUtils; + +import com.cloud.user.Account; +import com.cloud.utils.db.GenericDao; + +@Entity +@Table(name = "instance_boot_group_view") +public class InstanceBootGroupJoinVO implements ControlledViewEntity { + + @Id + @Column(name = "id", updatable = false, nullable = false) + private long id; + + @Column(name = "uuid") + private String uuid; + + @Column(name = "name") + private String name; + + @Column(name = "description", length = 4096) + private String description; + + @Column(name = GenericDao.CREATED_COLUMN) + private Date created; + + @Column(name = GenericDao.REMOVED_COLUMN) + private Date removed; + + @Column(name = "account_id") + private long accountId; + + @Column(name = "account_uuid") + private String accountUuid; + + @Column(name = "account_name") + private String accountName; + + @Column(name = "account_type") + @Enumerated(value = EnumType.STRING) + private Account.Type accountType; + + @Column(name = "domain_id") + private long domainId; + + @Column(name = "domain_uuid") + private String domainUuid; + + @Column(name = "domain_name") + private String domainName; + + @Column(name = "domain_path") + private String domainPath; + + @Column(name = "project_id") + private long projectId; + + @Column(name = "project_uuid") + private String projectUuid; + + @Column(name = "project_name") + private String projectName; + + @Override + public long getId() { + return id; + } + + @Override + public String getUuid() { + return uuid; + } + + public String getName() { + return name; + } + + public void setName(String name) { + this.name = name; + } + + public String getDescription() { + return description; + } + + public void setDescription(String description) { + this.description = description; + } + + public Date getCreated() { + return created; + } + + public Date getRemoved() { + return removed; + } + + @Override + public long getDomainId() { + return domainId; + } + + @Override + public String getDomainPath() { + return domainPath; + } + + @Override + public String getDomainUuid() { + return domainUuid; + } + + @Override + public String getDomainName() { + return domainName; + } + + @Override + public Account.Type getAccountType() { + return accountType; + } + + @Override + public long getAccountId() { + return accountId; + } + + @Override + public String getAccountUuid() { + return accountUuid; + } + + @Override + public String getAccountName() { + return accountName; + } + + @Override + public String getProjectUuid() { + return projectUuid; + } + + @Override + public String getProjectName() { + return projectName; + } + + @Override + public Class getEntityType() { + return InstanceBootGroupJoinVO.class; + } + + @Override + public String toString() { + return String.format("InstanceBootGroupJoinVO %s", ReflectionToStringBuilderUtils.reflectOnlySelectedFields( + this, "id", "uuid", "name")); + } + + public InstanceBootGroupJoinVO() { + } +} diff --git a/server/src/main/java/org/apache/cloudstack/vm/bootgroup/InstanceBootGroupApiServiceImpl.java b/server/src/main/java/org/apache/cloudstack/vm/bootgroup/InstanceBootGroupApiServiceImpl.java new file mode 100644 index 000000000000..0b76556262c4 --- /dev/null +++ b/server/src/main/java/org/apache/cloudstack/vm/bootgroup/InstanceBootGroupApiServiceImpl.java @@ -0,0 +1,909 @@ +// 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.vm.bootgroup; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.Comparator; +import java.util.List; +import java.util.Objects; +import java.util.stream.Collectors; + +import javax.inject.Inject; + +import org.apache.cloudstack.api.command.user.bootgroup.AddMemberToInstanceBootGroupCmd; +import org.apache.cloudstack.api.command.user.bootgroup.CreateInstanceBootGroupCmd; +import org.apache.cloudstack.api.command.user.bootgroup.CreateInstanceBootGroupReadinessRuleCmd; +import org.apache.cloudstack.api.command.user.bootgroup.DeleteInstanceBootGroupCmd; +import org.apache.cloudstack.api.command.user.bootgroup.DeleteInstanceBootGroupReadinessRuleCmd; +import org.apache.cloudstack.api.command.user.bootgroup.ListInstanceBootGroupMembersCmd; +import org.apache.cloudstack.api.command.user.bootgroup.ListInstanceBootGroupReadinessRulesCmd; +import org.apache.cloudstack.api.command.user.bootgroup.ListInstanceBootGroupsCmd; +import org.apache.cloudstack.api.command.user.bootgroup.RebootInstanceBootGroupCmd; +import org.apache.cloudstack.api.command.user.bootgroup.RemoveInstanceBootGroupMemberCmd; +import org.apache.cloudstack.api.command.user.bootgroup.StartInstanceBootGroupCmd; +import org.apache.cloudstack.api.command.user.bootgroup.StopInstanceBootGroupCmd; +import org.apache.cloudstack.api.command.user.bootgroup.UpdateInstanceBootGroupCmd; +import org.apache.cloudstack.api.command.user.bootgroup.UpdateInstanceBootGroupMemberCmd; +import org.apache.cloudstack.api.command.user.bootgroup.UpdateInstanceBootGroupReadinessRuleCmd; +import org.apache.cloudstack.api.query.dao.InstanceBootGroupJoinDao; +import org.apache.cloudstack.api.query.vo.InstanceBootGroupJoinVO; +import org.apache.cloudstack.api.response.InstanceBootGroupMemberChildResponse; +import org.apache.cloudstack.api.response.InstanceBootGroupMemberResponse; +import org.apache.cloudstack.api.response.InstanceBootGroupReadinessRuleResponse; +import org.apache.cloudstack.api.response.InstanceBootGroupResponse; +import org.apache.cloudstack.api.response.ListResponse; +import org.apache.cloudstack.context.CallContext; +import org.apache.cloudstack.vm.bootgroup.readiness.InstanceBootGroupReadinessRule; +import org.apache.cloudstack.vm.bootgroup.readiness.InstanceBootGroupReadinessRuleService; +import org.apache.cloudstack.vm.bootgroup.readiness.ReadinessChecker; +import org.apache.commons.lang3.EnumUtils; +import org.apache.commons.lang3.StringUtils; +import org.jetbrains.annotations.NotNull; +import org.springframework.stereotype.Component; + +import com.cloud.api.ApiResponseHelper; +import com.cloud.event.ActionEvent; +import com.cloud.event.EventTypes; +import com.cloud.exception.InvalidParameterValueException; +import com.cloud.exception.PermissionDeniedException; +import com.cloud.projects.Project; +import com.cloud.user.Account; +import com.cloud.user.AccountManager; +import com.cloud.uservm.UserVm; +import com.cloud.utils.Pair; +import com.cloud.utils.Ternary; +import com.cloud.utils.component.PluggableService; +import com.cloud.utils.db.Filter; +import com.cloud.utils.db.SearchBuilder; +import com.cloud.utils.db.SearchCriteria; +import com.cloud.utils.db.Transaction; +import com.cloud.utils.db.TransactionCallback; +import com.cloud.vm.InstanceGroupVMMapVO; +import com.cloud.vm.InstanceGroupVO; +import com.cloud.vm.UserVmVO; +import com.cloud.vm.dao.InstanceBootGroupDao; +import com.cloud.vm.dao.InstanceBootGroupDetailsDao; +import com.cloud.vm.dao.InstanceBootGroupMemberDao; +import com.cloud.vm.dao.InstanceBootGroupReadinessCheckResultDao; +import com.cloud.vm.dao.InstanceBootGroupReadinessRuleDao; +import com.cloud.vm.dao.InstanceBootGroupReadinessRuleDetailsDao; +import com.cloud.vm.dao.InstanceGroupDao; +import com.cloud.vm.dao.InstanceGroupVMMapDao; +import com.cloud.vm.dao.UserVmDao; + +/** + * API-facing half of the Instance Boot Group feature: ACL, param validation, response building, + * command registration. Delegates orchestration/hypervisor work to {@link InstanceBootGroupManager} + * and membership eligibility checks to {@link InstanceBootGroupMembershipGuard}. + */ +@Component +public class InstanceBootGroupApiServiceImpl implements InstanceBootGroupService, PluggableService { + + @Inject + private InstanceBootGroupDao instanceBootGroupDao; + + @Inject + private InstanceBootGroupJoinDao instanceBootGroupJoinDao; + + @Inject + private InstanceBootGroupMemberDao instanceBootGroupMemberDao; + + @Inject + private AccountManager accountManager; + + @Inject + private UserVmDao userVmDao; + + @Inject + private InstanceGroupDao instanceGroupDao; + + @Inject + private InstanceBootGroupManager instanceBootGroupManager; + + @Inject + private InstanceBootGroupMembershipGuard instanceBootGroupMembershipGuard; + + @Inject + private InstanceBootGroupReadinessRuleService instanceBootGroupReadinessRuleService; + + @Inject + private InstanceBootGroupReadinessRuleDao instanceBootGroupReadinessRuleDao; + + @Inject + private InstanceBootGroupReadinessRuleDetailsDao instanceBootGroupReadinessRuleDetailsDao; + + @Inject + private InstanceBootGroupReadinessCheckResultDao instanceBootGroupReadinessCheckResultDao; + + @Inject + private InstanceBootGroupDetailsDao instanceBootGroupDetailsDao; + + @Inject + private InstanceGroupVMMapDao instanceGroupVMMapDao; + + @NotNull + protected InstanceBootGroupVO getGroupAndCheckAccess(long id) { + InstanceBootGroupVO group = instanceBootGroupDao.findById(id); + if (group == null) { + throw new InvalidParameterValueException("Unable to find instance boot group with ID: " + id); + } + Account caller = CallContext.current().getCallingAccount(); + accountManager.checkAccess(caller, null, true, group); + return group; + } + + protected InstanceBootGroupResponse createInstanceBootGroupResponse(InstanceBootGroupJoinVO bootGroup) { + InstanceBootGroupResponse response = new InstanceBootGroupResponse(); + response.setId(bootGroup.getUuid()); + response.setName(bootGroup.getName()); + response.setDescription(bootGroup.getDescription()); + response.setCreated(bootGroup.getCreated()); + ApiResponseHelper.populateOwner(response, bootGroup); + + String timeoutOverride = instanceBootGroupDetailsDao.getDetail(bootGroup.getId(), InstanceBootGroupManagerImpl.ReadinessAttemptTimeoutSeconds.key()); + response.setReadinessAttemptTimeoutSeconds(timeoutOverride != null ? Long.parseLong(timeoutOverride) : InstanceBootGroupManagerImpl.ReadinessAttemptTimeoutSeconds.value()); + String maxRetryOverride = instanceBootGroupDetailsDao.getDetail(bootGroup.getId(), InstanceBootGroupManagerImpl.ReadinessMaxRetryAttempts.key()); + response.setReadinessMaxRetryAttempts(maxRetryOverride != null ? Long.parseLong(maxRetryOverride) : InstanceBootGroupManagerImpl.ReadinessMaxRetryAttempts.value()); + String rebootOnRetryOverride = instanceBootGroupDetailsDao.getDetail(bootGroup.getId(), InstanceBootGroupManagerImpl.ReadinessRebootOnRetry.key()); + response.setReadinessRebootOnRetry(rebootOnRetryOverride != null ? Boolean.parseBoolean(rebootOnRetryOverride) : InstanceBootGroupManagerImpl.ReadinessRebootOnRetry.value()); + String initialDelayOverride = instanceBootGroupDetailsDao.getDetail(bootGroup.getId(), InstanceBootGroupManagerImpl.ReadinessInitialDelaySeconds.key()); + response.setReadinessInitialDelaySeconds(initialDelayOverride != null ? Long.parseLong(initialDelayOverride) : InstanceBootGroupManagerImpl.ReadinessInitialDelaySeconds.value()); + + response.setObjectName("instancebootgroup"); + return response; + } + + @Override + @ActionEvent(eventType = EventTypes.EVENT_INSTANCE_BOOT_GROUP_CREATE, eventDescription = "creating Instance Boot Group") + public InstanceBootGroup createInstanceBootGroup(CreateInstanceBootGroupCmd cmd) { + Account caller = CallContext.current().getCallingAccount(); + Account owner = accountManager.finalizeOwner(caller, cmd.getAccountName(), cmd.getDomainId(), cmd.getProjectId()); + + if (instanceBootGroupDao.isNameInUse(owner.getId(), cmd.getName())) { + throw new InvalidParameterValueException("An instance boot group with name '" + cmd.getName() + "' already exists in this account"); + } + + return Transaction.execute((TransactionCallback) status -> { + InstanceBootGroupVO group = new InstanceBootGroupVO(cmd.getName(), cmd.getDescription(), owner.getId(), owner.getDomainId()); + group = instanceBootGroupDao.persist(group); + CallContext.current().setEventResourceId(group.getId()); + + if (cmd.getReadinessAttemptTimeoutSeconds() != null) { + setOrClearOverride(group.getId(), InstanceBootGroupManagerImpl.ReadinessAttemptTimeoutSeconds.key(), cmd.getReadinessAttemptTimeoutSeconds()); + } + if (cmd.getReadinessMaxRetryAttempts() != null) { + setOrClearOverride(group.getId(), InstanceBootGroupManagerImpl.ReadinessMaxRetryAttempts.key(), cmd.getReadinessMaxRetryAttempts()); + } + if (cmd.getReadinessRebootOnRetry() != null) { + instanceBootGroupDetailsDao.setDetail(group.getId(), InstanceBootGroupManagerImpl.ReadinessRebootOnRetry.key(), String.valueOf(cmd.getReadinessRebootOnRetry())); + } + if (cmd.getReadinessInitialDelaySeconds() != null) { + setOrClearOverride(group.getId(), InstanceBootGroupManagerImpl.ReadinessInitialDelaySeconds.key(), cmd.getReadinessInitialDelaySeconds()); + } + + return group; + }); + } + + @Override + @ActionEvent(eventType = EventTypes.EVENT_INSTANCE_BOOT_GROUP_DELETE, eventDescription = "deleting Instance Boot Group") + public boolean deleteInstanceBootGroup(DeleteInstanceBootGroupCmd cmd) { + InstanceBootGroupVO group = getGroupAndCheckAccess(cmd.getId()); + return Transaction.execute((TransactionCallback) status -> { + instanceBootGroupMemberDao.deleteByBootGroupId(group.getId()); + instanceBootGroupDao.remove(group.getId()); + return true; + }); + } + + @Override + @ActionEvent(eventType = EventTypes.EVENT_INSTANCE_BOOT_GROUP_UPDATE, eventDescription = "updating Instance Boot Group") + public InstanceBootGroup updateInstanceBootGroup(UpdateInstanceBootGroupCmd cmd) { + InstanceBootGroupVO group = getGroupAndCheckAccess(cmd.getId()); + + if (cmd.getName() != null && !Objects.equals(cmd.getName(), group.getName())) { + Account owner = accountManager.getAccount(group.getAccountId()); + if (instanceBootGroupDao.isNameInUse(owner.getId(), cmd.getName())) { + throw new InvalidParameterValueException("An instance boot group with name '" + cmd.getName() + "' already exists in this account"); + } + group.setName(cmd.getName()); + } + if (cmd.getDescription() != null) { + group.setDescription(cmd.getDescription()); + } + + return Transaction.execute((TransactionCallback) status -> { + if (cmd.getReadinessAttemptTimeoutSeconds() != null) { + setOrClearOverride(group.getId(), InstanceBootGroupManagerImpl.ReadinessAttemptTimeoutSeconds.key(), cmd.getReadinessAttemptTimeoutSeconds()); + } + if (cmd.getReadinessMaxRetryAttempts() != null) { + setOrClearOverride(group.getId(), InstanceBootGroupManagerImpl.ReadinessMaxRetryAttempts.key(), cmd.getReadinessMaxRetryAttempts()); + } + if (cmd.getReadinessRebootOnRetry() != null) { + instanceBootGroupDetailsDao.setDetail(group.getId(), InstanceBootGroupManagerImpl.ReadinessRebootOnRetry.key(), String.valueOf(cmd.getReadinessRebootOnRetry())); + } + if (cmd.getReadinessInitialDelaySeconds() != null) { + setOrClearOverride(group.getId(), InstanceBootGroupManagerImpl.ReadinessInitialDelaySeconds.key(), cmd.getReadinessInitialDelaySeconds()); + } + + instanceBootGroupDao.update(group.getId(), group); + return instanceBootGroupDao.findById(group.getId()); + }); + } + + private void setOrClearOverride(long bootGroupId, String key, long value) { + if (value < 0) { + instanceBootGroupDetailsDao.setDetail(bootGroupId, key, null); + } else { + instanceBootGroupDetailsDao.setDetail(bootGroupId, key, String.valueOf(value)); + } + } + + @Override + public ListResponse listInstanceBootGroups(ListInstanceBootGroupsCmd cmd) { + final CallContext ctx = CallContext.current(); + final Account caller = ctx.getCallingAccount(); + final Long id = cmd.getId(); + final String keyword = cmd.getKeyword(); + + List responsesList = new ArrayList<>(); + List permittedAccounts = new ArrayList<>(); + Ternary domainIdRecursiveListProject = + new Ternary<>(cmd.getDomainId(), cmd.isRecursive(), null); + accountManager.buildACLSearchParameters(caller, id, cmd.getAccountName(), cmd.getProjectId(), + permittedAccounts, domainIdRecursiveListProject, cmd.listAll(), false); + Long domainId = domainIdRecursiveListProject.first(); + Boolean isRecursive = domainIdRecursiveListProject.second(); + Project.ListProjectResourcesCriteria listProjectResourcesCriteria = domainIdRecursiveListProject.third(); + + Filter searchFilter = new Filter(InstanceBootGroupJoinVO.class, "id", true, cmd.getStartIndex(), + cmd.getPageSizeVal()); + SearchBuilder sb = instanceBootGroupJoinDao.createSearchBuilder(); + accountManager.buildACLSearchBuilder(sb, domainId, isRecursive, permittedAccounts, + listProjectResourcesCriteria); + sb.and("id", sb.entity().getId(), SearchCriteria.Op.EQ); + sb.and("name", sb.entity().getName(), SearchCriteria.Op.EQ); + sb.and("keyword", sb.entity().getName(), SearchCriteria.Op.LIKE); + SearchCriteria sc = sb.create(); + accountManager.buildACLSearchCriteria(sc, domainId, isRecursive, permittedAccounts, + listProjectResourcesCriteria); + if (keyword != null) { + sc.setParameters("keyword", "%" + keyword + "%"); + } + if (id != null) { + sc.setParameters("id", id); + } + Pair, Integer> bootGroupsAndCount = instanceBootGroupJoinDao.searchAndCount(sc, searchFilter); + for (InstanceBootGroupJoinVO bootGroup : bootGroupsAndCount.first()) { + InstanceBootGroupResponse response = createInstanceBootGroupResponse(bootGroup); + responsesList.add(response); + } + ListResponse response = new ListResponse<>(); + response.setResponses(responsesList, bootGroupsAndCount.second()); + return response; + } + + @Override + @ActionEvent(eventType = EventTypes.EVENT_INSTANCE_BOOT_GROUP_MEMBER_ADD, eventDescription = "adding Instance Boot Group member") + public InstanceBootGroupMember addMemberToInstanceBootGroup(AddMemberToInstanceBootGroupCmd cmd) { + InstanceBootGroupVO group = getGroupAndCheckAccess(cmd.getId()); + + if (cmd.getOrder() < 0) { + throw new InvalidParameterValueException("Order value must be 0 or greater"); + } + + InstanceBootGroupMember.MemberType memberType; + long memberId; + + validateEitherVirtualMachineIdOrInstanceGroupIdParam(cmd.getVirtualMachineId(), cmd.getInstanceGroupId()); + + if (cmd.getVirtualMachineId() != null) { + UserVm vm = getValidatedVmForAddMember(group, cmd.getVirtualMachineId()); + memberType = InstanceBootGroupMember.MemberType.VirtualMachine; + memberId = vm.getId(); + } else { + InstanceGroupVO instanceGroup = getValidatedInstanceGroupAddMember(group, cmd.getInstanceGroupId()); + memberType = InstanceBootGroupMember.MemberType.InstanceGroup; + memberId = instanceGroup.getId(); + } + + if (instanceBootGroupMemberDao.findByMember(memberType, memberId) != null) { + throw new InvalidParameterValueException(String.format("This %s already belongs to an instance boot group", memberType.name())); + } + + List siblings = instanceBootGroupMemberDao.listByBootGroupId(group.getId()); + long maxMembers = InstanceBootGroupManagerImpl.MaxMembersPerBootGroup.valueIn(group.getDomainId()); + if (siblings.size() >= maxMembers) { + throw new InvalidParameterValueException(String.format( + "Instance boot group %s already has the maximum of %d member(s) allowed", group, maxMembers)); + } + + shiftSiblingOrdersForInsert(siblings, cmd.getOrder()); + InstanceBootGroupMemberVO member = new InstanceBootGroupMemberVO(group.getId(), memberType, memberId, cmd.getOrder()); + return instanceBootGroupMemberDao.persist(member); + } + + /** + * Makes room for a new member at {@code order} by bumping every existing member already at or + * past it up by one slot, rather than letting the new member silently share that order. + */ + private void shiftSiblingOrdersForInsert(List siblings, int order) { + for (InstanceBootGroupMemberVO sibling : siblings) { + if (sibling.getOrder() >= order) { + sibling.setOrder(sibling.getOrder() + 1); + instanceBootGroupMemberDao.update(sibling.getId(), sibling); + } + } + } + + @NotNull + private UserVm getValidatedVmForAddMember(InstanceBootGroupVO group, long virtualMachineId) { + UserVm vm = userVmDao.findById(virtualMachineId); + if (vm == null) { + throw new InvalidParameterValueException("Unable to find virtual machine with ID: " + virtualMachineId); + } + validateMemberAccount(vm.getAccountId(), group.getAccountId()); + instanceBootGroupMembershipGuard.validateVmEligibleForGroupMembership(vm.getId()); + return vm; + } + + @NotNull + private InstanceGroupVO getValidatedInstanceGroupAddMember(InstanceBootGroupVO group, long instanceGroupId) { + InstanceGroupVO instanceGroup = instanceGroupDao.findById(instanceGroupId); + if (instanceGroup == null || instanceGroup.getRemoved() != null) { + throw new InvalidParameterValueException("Unable to find instance group with ID: " + instanceGroupId); + } + validateMemberAccount(instanceGroup.getAccountId(), group.getAccountId()); + instanceBootGroupMembershipGuard.validateInstanceGroupEligibleForBootGroupMembership(instanceGroup.getId()); + return instanceGroup; + } + + protected static void validateEitherVirtualMachineIdOrInstanceGroupIdParam(Long virtualMachineId, Long instanceGroupId) { + if (virtualMachineId != null && instanceGroupId != null) { + throw new InvalidParameterValueException("Only one of virtualmachineid or instancegroupid may be specified"); + } + if (virtualMachineId == null && instanceGroupId == null) { + throw new InvalidParameterValueException("Either virtualmachineid or instancegroupid must be specified"); + } + } + + @Override + @ActionEvent(eventType = EventTypes.EVENT_INSTANCE_BOOT_GROUP_MEMBER_REMOVE, eventDescription = "removing Instance Boot Group member") + public boolean removeInstanceBootGroupMember(RemoveInstanceBootGroupMemberCmd cmd) { + InstanceBootGroupMemberVO member = instanceBootGroupMemberDao.findById(cmd.getId()); + if (member == null) { + throw new InvalidParameterValueException("Unable to find boot group member with ID: " + cmd.getId()); + } + getGroupAndCheckAccess(member.getBootGroupId()); + instanceBootGroupMemberDao.expunge(member.getId()); + return true; + } + + @Override + @ActionEvent(eventType = EventTypes.EVENT_INSTANCE_BOOT_GROUP_MEMBER_REORDER, eventDescription = "reordering Instance Boot Group member") + public InstanceBootGroupMember updateInstanceBootGroupMember(UpdateInstanceBootGroupMemberCmd cmd) { + InstanceBootGroupMemberVO member = instanceBootGroupMemberDao.findById(cmd.getId()); + if (member == null) { + throw new InvalidParameterValueException("Unable to find boot group member with ID: " + cmd.getId()); + } + int newOrder = cmd.getOrder(); + if (newOrder < 0) { + throw new InvalidParameterValueException("Order value must be 0 or greater"); + } + getGroupAndCheckAccess(member.getBootGroupId()); + + int oldOrder = member.getOrder(); + if (newOrder != oldOrder) { + shiftSiblingOrders(member, oldOrder, newOrder); + member.setOrder(newOrder); + instanceBootGroupMemberDao.update(member.getId(), member); + } + return instanceBootGroupMemberDao.findById(member.getId()); + } + + /** + * Shifts every other member between the old and new position by one slot — list-reorder + * semantics, not just moving the single member whose order was explicitly given. + */ + private void shiftSiblingOrders(InstanceBootGroupMemberVO member, int oldOrder, int newOrder) { + List siblings = instanceBootGroupMemberDao.listByBootGroupId(member.getBootGroupId()); + for (InstanceBootGroupMemberVO sibling : siblings) { + if (sibling.getId() == member.getId()) { + continue; + } + int siblingOrder = sibling.getOrder(); + if (newOrder > oldOrder && siblingOrder > oldOrder && siblingOrder <= newOrder) { + sibling.setOrder(siblingOrder - 1); + instanceBootGroupMemberDao.update(sibling.getId(), sibling); + } else if (newOrder < oldOrder && siblingOrder >= newOrder && siblingOrder < oldOrder) { + sibling.setOrder(siblingOrder + 1); + instanceBootGroupMemberDao.update(sibling.getId(), sibling); + } + } + } + + @Override + public ListResponse listInstanceBootGroupMembers(ListInstanceBootGroupMembersCmd cmd) { + InstanceBootGroupVO group = getGroupAndCheckAccess(cmd.getBootGroupId()); + + Pair, Integer> result; + if (cmd.getMemberType() != null) { + InstanceBootGroupMember.MemberType type = InstanceBootGroupMember.MemberType.valueOf(cmd.getMemberType()); + result = instanceBootGroupMemberDao.searchAndCountByBootGroupIdAndType(group.getId(), type); + } else { + result = instanceBootGroupMemberDao.searchAndCountByBootGroupId(group.getId()); + } + + List members = result.first(); + members.sort(Comparator.comparingInt(InstanceBootGroupMemberVO::getOrder)); + boolean includeReadiness = cmd.isReadinessDetailRequested(); + boolean includeChildren = cmd.isChildrenDetailRequested(); + boolean ignoreVmState = cmd.isIgnoreInstanceState(); + ListResponse response = new ListResponse<>(); + response.setResponses(members.stream().map(member -> createInstanceBootGroupMemberResponse(member, includeReadiness, includeChildren, ignoreVmState)).collect(Collectors.toList()), result.second()); + return response; + } + + @Override + @ActionEvent(eventType = EventTypes.EVENT_INSTANCE_BOOT_GROUP_START, eventDescription = "starting Instance Boot Group", async = true) + public InstanceBootGroup startInstanceBootGroup(final StartInstanceBootGroupCmd cmd) { + InstanceBootGroupVO group = getGroupAndCheckAccess(cmd.getId()); + instanceBootGroupManager.startInstanceBootGroup(group); + return group; + } + + @Override + @ActionEvent(eventType = EventTypes.EVENT_INSTANCE_BOOT_GROUP_STOP, eventDescription = "stopping Instance Boot Group", async = true) + public InstanceBootGroup stopInstanceBootGroup(final StopInstanceBootGroupCmd cmd) { + InstanceBootGroupVO group = getGroupAndCheckAccess(cmd.getId()); + instanceBootGroupManager.stopInstanceBootGroup(group, cmd.isForced()); + return group; + } + + @Override + @ActionEvent(eventType = EventTypes.EVENT_INSTANCE_BOOT_GROUP_REBOOT, eventDescription = "rebooting Instance Boot Group", async = true) + public InstanceBootGroup rebootInstanceBootGroup(final RebootInstanceBootGroupCmd cmd) { + InstanceBootGroupVO group = getGroupAndCheckAccess(cmd.getId()); + instanceBootGroupManager.rebootInstanceBootGroup(group, cmd.isForced()); + return group; + } + + @Override + public InstanceBootGroupResponse createInstanceBootGroupResponse(long id) { + return createInstanceBootGroupResponse(instanceBootGroupJoinDao.findById(id)); + } + + @Override + public InstanceBootGroupMemberResponse createInstanceBootGroupMemberResponse(InstanceBootGroupMember member) { + return createInstanceBootGroupMemberResponse(member, false, false, false); + } + + private InstanceBootGroupMemberResponse createInstanceBootGroupMemberResponse(InstanceBootGroupMember member, boolean includeReadiness, boolean includeChildren, boolean ignoreVmState) { + InstanceBootGroupMemberResponse response = new InstanceBootGroupMemberResponse(); + response.setId(member.getUuid()); + InstanceBootGroupVO group = instanceBootGroupDao.findById(member.getBootGroupId()); + if (group != null) { + response.setBootGroupId(group.getUuid()); + } + response.setMemberType(member.getMemberType().name()); + response.setOrder(member.getOrder()); + response.setCreated(member.getCreated()); + + List childVmIds = new ArrayList<>(); + if (member.getMemberType() == InstanceBootGroupMember.MemberType.VirtualMachine) { + UserVmVO vm = userVmDao.findById(member.getMemberId()); + if (vm != null) { + response.setMemberId(vm.getUuid()); + response.setMemberName(StringUtils.defaultIfEmpty(vm.getDisplayName(), vm.getHostName())); + response.setMemberState(vm.getState().toString()); + } + } else { + InstanceGroupVO instanceGroup = instanceGroupDao.findById(member.getMemberId()); + if (instanceGroup != null) { + response.setMemberId(instanceGroup.getUuid()); + response.setMemberName(instanceGroup.getName()); + } + if (includeReadiness || includeChildren) { + childVmIds = instanceGroupVMMapDao.listByGroupId(member.getMemberId()).stream() + .map(InstanceGroupVMMapVO::getInstanceId) + .collect(Collectors.toList()); + } + } + + if (includeReadiness) { + response.setReadinessMode(computeReadinessMode(member.getMemberType(), member.getBootGroupId(), member.getMemberId())); + ReadinessChecker.Result readinessResult = member.getMemberType() == InstanceBootGroupMember.MemberType.VirtualMachine + ? computeCachedVmReadinessResult(member.getBootGroupId(), member.getMemberId(), ignoreVmState) + : computeCachedInstanceGroupReadinessResult(member.getBootGroupId(), member.getMemberId(), childVmIds, ignoreVmState); + response.setReadinessStatus(readinessResult.getStatus().name()); + response.setReadinessMessage(readinessResult.getMessage()); + } + + if (includeChildren && member.getMemberType() == InstanceBootGroupMember.MemberType.InstanceGroup) { + response.setChildren(buildChildrenResponses(member.getBootGroupId(), childVmIds, includeReadiness, ignoreVmState)); + } + + response.setObjectName("instancebootgroupmember"); + return response; + } + + /** + * Basic VM fields only (id/name/state, all off UserVmDao) — fetched in a single batch via + * listByIds rather than one findById per VM, since an InstanceGroup can hold many VMs. + */ + private List buildChildrenResponses(long bootGroupId, List vmIds, boolean includeReadiness, boolean ignoreVmState) { + List children = new ArrayList<>(); + if (vmIds.isEmpty()) { + return children; + } + for (UserVmVO vm : userVmDao.listByIds(vmIds)) { + InstanceBootGroupMemberChildResponse child = new InstanceBootGroupMemberChildResponse(); + child.setId(vm.getUuid()); + child.setName(StringUtils.defaultIfEmpty(vm.getDisplayName(), vm.getHostName())); + child.setState(vm.getState().toString()); + if (includeReadiness) { + child.setReadinessMode(computeReadinessMode(InstanceBootGroupMember.MemberType.VirtualMachine, bootGroupId, vm.getId())); + ReadinessChecker.Result readinessResult = computeCachedVmReadinessResult(bootGroupId, vm.getId(), ignoreVmState); + child.setReadinessStatus(readinessResult.getStatus().name()); + child.setReadinessMessage(readinessResult.getMessage()); + } + children.add(child); + } + return children; + } + + private String computeReadinessMode(InstanceBootGroupMember.MemberType itemType, long bootGroupId, long itemId) { + boolean hasRules = !instanceBootGroupReadinessRuleDao.listEnabledByItem(bootGroupId, itemType, itemId).isEmpty(); + boolean hasInheritedRules = itemType == InstanceBootGroupMember.MemberType.VirtualMachine + && !instanceBootGroupReadinessRuleService.findInheritedGroupRules(bootGroupId, itemId).isEmpty(); + InstanceBootGroupMember.ReadinessMode readinessMode = InstanceBootGroupMember.ReadinessMode.None; + if (hasRules || hasInheritedRules) { + readinessMode = InstanceBootGroupMember.ReadinessMode.RuleBased; + } else if (InstanceBootGroupMember.MemberType.InstanceGroup.equals(itemType)) { + readinessMode = InstanceBootGroupMember.ReadinessMode.ChildDependent; + } + return readinessMode.name(); + } + + /** + * Reads cached results only — viewing/polling the member list must never itself dispatch a remote + * check as a side effect. Combines the VM's own direct rules (cached at vmId 0) with any rules it + * inherits from its owning InstanceGroup (cached per-member at this VM's own id). + * @param ignoreVmState if false (the normal case), a non-Running VM is always NotReady regardless of + * any cached rule result, so a VM that stopped can't keep reporting a stale Ready; pass true + * to see the raw last-cached rule result for diagnosing what happened before it stopped. + */ + private ReadinessChecker.Result computeCachedVmReadinessResult(long bootGroupId, long vmId, boolean ignoreVmState) { + List directRules = instanceBootGroupReadinessRuleDao.listEnabledByItem(bootGroupId, InstanceBootGroupMember.MemberType.VirtualMachine, vmId); + List inheritedRules = instanceBootGroupReadinessRuleService.findInheritedGroupRules(bootGroupId, vmId); + + UserVmVO vm = userVmDao.findById(vmId); + boolean running = vm != null && vm.getState() == com.cloud.vm.VirtualMachine.State.Running; + String vmState = vm != null ? vm.getState().toString() : "unknown"; + + if (directRules.isEmpty() && inheritedRules.isEmpty()) { + InstanceBootGroupReadinessRule.Status status = running ? InstanceBootGroupReadinessRule.Status.Ready : InstanceBootGroupReadinessRule.Status.NotReady; + return new ReadinessChecker.Result(status, "Instance state is " + vmState + ". No readiness rules attached"); + } + if (!running && !ignoreVmState) { + return new ReadinessChecker.Result(InstanceBootGroupReadinessRule.Status.NotReady, "Instance state is " + vmState); + } + + List> ruleAndCacheVmIds = new ArrayList<>(); + for (InstanceBootGroupReadinessRuleVO rule : directRules) { + ruleAndCacheVmIds.add(new Pair<>(rule, 0L)); + } + for (InstanceBootGroupReadinessRule rule : inheritedRules) { + ruleAndCacheVmIds.add(new Pair<>(rule, vmId)); + } + return combineCachedRuleResults(ruleAndCacheVmIds); + } + + /** + * With a MemberQuorum rule, neither a member's own status nor any other member-targeted group + * rule's all-members aggregate gates {@code ownResult} — the quorum rule's own tolerance-aware + * verdict is the one that counts. Without one, every member (and every group rule) must be ready. + * Unless {@code ignoreVmState}, the group's own rule rows are refreshed first — a group-scope + * rule (MemberQuorum in particular) isn't tied to any one VM's state, so nothing else re-derives + * it once a member stops outside of active boot-group orchestration. + */ + private ReadinessChecker.Result computeCachedInstanceGroupReadinessResult(long bootGroupId, long instanceGroupId, List memberVmIds, boolean ignoreVmState) { + List groupRules = instanceBootGroupReadinessRuleDao.listEnabledByItem(bootGroupId, InstanceBootGroupMember.MemberType.InstanceGroup, instanceGroupId); + if (!ignoreVmState && !groupRules.isEmpty()) { + instanceBootGroupReadinessRuleService.evaluateInstanceGroupReadiness(bootGroupId, instanceGroupId, Collections.emptySet()); + } + boolean hasMemberQuorumRule = groupRules.stream().anyMatch(rule -> rule.getRuleType() == InstanceBootGroupReadinessRule.RuleType.MemberQuorum); + List> ownRuleAndCacheVmIds = new ArrayList<>(); + for (InstanceBootGroupReadinessRuleVO rule : groupRules) { + if (hasMemberQuorumRule && rule.getRuleType().isMemberTargeted()) { + continue; + } + ownRuleAndCacheVmIds.add(new Pair<>(rule, 0L)); + } + ReadinessChecker.Result ownResult = combineCachedRuleResults(ownRuleAndCacheVmIds); + + boolean anyError = ownResult.getStatus() == InstanceBootGroupReadinessRule.Status.Error; + boolean anyNotReady = !anyError && ownResult.getStatus() != InstanceBootGroupReadinessRule.Status.Ready; + + int notReadyChildren = 0; + for (Long vmId : memberVmIds) { + InstanceBootGroupReadinessRule.Status vmStatus = computeCachedVmReadinessResult(bootGroupId, vmId, ignoreVmState).getStatus(); + if (vmStatus != InstanceBootGroupReadinessRule.Status.Ready) { + notReadyChildren++; + } + if (hasMemberQuorumRule) { + continue; + } + if (vmStatus == InstanceBootGroupReadinessRule.Status.Error) { + anyError = true; + } else if (vmStatus != InstanceBootGroupReadinessRule.Status.Ready) { + anyNotReady = true; + } + } + + InstanceBootGroupReadinessRule.Status aggregateStatus = anyError ? InstanceBootGroupReadinessRule.Status.Error + : (anyNotReady ? InstanceBootGroupReadinessRule.Status.NotReady : InstanceBootGroupReadinessRule.Status.Ready); + + List messageParts = new ArrayList<>(); + if (!groupRules.isEmpty() && ownResult.getStatus() != InstanceBootGroupReadinessRule.Status.Ready) { + messageParts.add(ownResult.getMessage()); + } + if (notReadyChildren > 0) { + messageParts.add(String.format("%d of %d member VM(s) not ready", notReadyChildren, memberVmIds.size())); + } + String message = messageParts.isEmpty() ? "all readiness rules ready" : String.join("; ", messageParts); + + return new ReadinessChecker.Result(aggregateStatus, message); + } + + /** + * Reduces multiple rules to one status (any Error wins, else any non-Ready means NotReady) plus + * the cached message(s) of whichever rule(s) are at that worst severity, rule-type-prefixed. + * @param ruleAndCacheVmIds cache-vmId is 0 for the rule's own row, or a member's vmId if inherited. + */ + private ReadinessChecker.Result combineCachedRuleResults(List> ruleAndCacheVmIds) { + boolean anyError = false; + boolean anyNotReady = false; + List errorMessages = new ArrayList<>(); + List notReadyMessages = new ArrayList<>(); + for (Pair entry : ruleAndCacheVmIds) { + InstanceBootGroupReadinessRule rule = entry.first(); + InstanceBootGroupReadinessCheckResultVO result = instanceBootGroupReadinessCheckResultDao.findByRuleAndVm(rule.getId(), entry.second()); + InstanceBootGroupReadinessRule.Status status = (result != null && result.getStatus() != null) ? result.getStatus() : InstanceBootGroupReadinessRule.Status.Unknown; + String detail = result != null ? result.getMessage() : null; + String labeledMessage = rule.getRuleType().name() + (StringUtils.isNotBlank(detail) ? (": " + detail) : ""); + if (status == InstanceBootGroupReadinessRule.Status.Error) { + anyError = true; + errorMessages.add(labeledMessage); + } else if (status != InstanceBootGroupReadinessRule.Status.Ready) { + anyNotReady = true; + notReadyMessages.add(labeledMessage); + } + } + if (anyError) { + return new ReadinessChecker.Result(InstanceBootGroupReadinessRule.Status.Error, String.join("; ", errorMessages)); + } + if (anyNotReady) { + return new ReadinessChecker.Result(InstanceBootGroupReadinessRule.Status.NotReady, String.join("; ", notReadyMessages)); + } + return new ReadinessChecker.Result(InstanceBootGroupReadinessRule.Status.Ready, "All readiness rules ready"); + } + + private void validateMemberAccount(long memberAccountId, long groupAccountId) { + if (memberAccountId != groupAccountId) { + throw new PermissionDeniedException("Member must belong to the same account as the boot group"); + } + } + + /** + * Resolves the mutually-exclusive virtualmachineid/instancegroupid item params and checks access + * on the resolved item (in addition to the boot group, already checked via getGroupAndCheckAccess). + */ + private Pair resolveAndCheckAccessToItem(Long virtualMachineId, Long instanceGroupId) { + validateEitherVirtualMachineIdOrInstanceGroupIdParam(virtualMachineId, instanceGroupId); + + Account caller = CallContext.current().getCallingAccount(); + if (virtualMachineId != null) { + UserVmVO vm = userVmDao.findById(virtualMachineId); + if (vm == null) { + throw new InvalidParameterValueException("Unable to find virtual machine with ID: " + virtualMachineId); + } + accountManager.checkAccess(caller, null, true, vm); + return new Pair<>(InstanceBootGroupMember.MemberType.VirtualMachine, vm.getId()); + } + InstanceGroupVO instanceGroup = instanceGroupDao.findById(instanceGroupId); + if (instanceGroup == null || instanceGroup.getRemoved() != null) { + throw new InvalidParameterValueException("Unable to find instance group with ID: " + instanceGroupId); + } + accountManager.checkAccess(caller, null, true, instanceGroup); + return new Pair<>(InstanceBootGroupMember.MemberType.InstanceGroup, instanceGroup.getId()); + } + + @Override + @ActionEvent(eventType = EventTypes.EVENT_INSTANCE_BOOT_GROUP_READINESS_RULE_CREATE, eventDescription = "creating Instance Boot Group readiness rule") + public InstanceBootGroupReadinessRule createInstanceBootGroupReadinessRule(CreateInstanceBootGroupReadinessRuleCmd cmd) { + getGroupAndCheckAccess(cmd.getBootGroupId()); + Pair item = resolveAndCheckAccessToItem(cmd.getVirtualMachineId(), cmd.getInstanceGroupId()); + + InstanceBootGroupReadinessRule.RuleType ruleType = EnumUtils.getEnumIgnoreCase(InstanceBootGroupReadinessRule.RuleType.class, cmd.getRuleType()); + if (ruleType == null) { + throw new InvalidParameterValueException("Invalid rule type: " + cmd.getRuleType()); + } + + return instanceBootGroupReadinessRuleService.createReadinessRule(cmd.getBootGroupId(), item.first(), item.second(), + ruleType, cmd.getName(), cmd.isEnabled(), cmd.getDetails()); + } + + @Override + @ActionEvent(eventType = EventTypes.EVENT_INSTANCE_BOOT_GROUP_READINESS_RULE_UPDATE, eventDescription = "updating Instance Boot Group readiness rule") + public InstanceBootGroupReadinessRule updateInstanceBootGroupReadinessRule(UpdateInstanceBootGroupReadinessRuleCmd cmd) { + InstanceBootGroupReadinessRule rule = instanceBootGroupReadinessRuleDao.findById(cmd.getId()); + if (rule == null) { + throw new InvalidParameterValueException("Unable to find a readiness rule with ID: " + cmd.getId()); + } + getGroupAndCheckAccess(rule.getBootGroupId()); + + return instanceBootGroupReadinessRuleService.updateReadinessRule(cmd.getId(), cmd.getName(), cmd.getEnabled(), cmd.getDetails()); + } + + @Override + @ActionEvent(eventType = EventTypes.EVENT_INSTANCE_BOOT_GROUP_READINESS_RULE_DELETE, eventDescription = "deleting Instance Boot Group readiness rule") + public boolean deleteInstanceBootGroupReadinessRule(DeleteInstanceBootGroupReadinessRuleCmd cmd) { + InstanceBootGroupReadinessRule rule = instanceBootGroupReadinessRuleDao.findById(cmd.getId()); + if (rule == null) { + throw new InvalidParameterValueException("Unable to find a readiness rule with ID: " + cmd.getId()); + } + getGroupAndCheckAccess(rule.getBootGroupId()); + + return instanceBootGroupReadinessRuleService.deleteReadinessRule(cmd.getId()); + } + + /** + * When filtering by VM, also surfaces Ping/PortCheck/GuestAgentLiveness rules the VM inherits from + * its owning InstanceGroup (marked {@code inherited=true}), not just its own direct rules. + */ + @Override + public ListResponse listInstanceBootGroupReadinessRules(ListInstanceBootGroupReadinessRulesCmd cmd) { + getGroupAndCheckAccess(cmd.getBootGroupId()); + + if (cmd.getVirtualMachineId() != null && cmd.getInstanceGroupId() != null) { + throw new InvalidParameterValueException("Only one of virtualmachineid or instancegroupid may be specified"); + } + InstanceBootGroupReadinessRule.RuleType ruleType = null; + if (cmd.getRuleType() != null) { + ruleType = EnumUtils.getEnumIgnoreCase(InstanceBootGroupReadinessRule.RuleType.class, cmd.getRuleType()); + if (ruleType == null) { + throw new InvalidParameterValueException("Invalid rule type: " + cmd.getRuleType()); + } + } + + InstanceBootGroupMember.MemberType memberType = null; + Long memberId = null; + if (cmd.getVirtualMachineId() != null) { + memberType = InstanceBootGroupMember.MemberType.VirtualMachine; + memberId = cmd.getVirtualMachineId(); + } else if (cmd.getInstanceGroupId() != null) { + memberType = InstanceBootGroupMember.MemberType.InstanceGroup; + memberId = cmd.getInstanceGroupId(); + } + Pair, Integer> rulesAndCount = instanceBootGroupReadinessRuleDao.searchAndCountByBootGroupId( + cmd.getBootGroupId(), + cmd.getId(), + memberType, + memberId, + ruleType, + cmd.getKeyword(), + cmd.getStartIndex(), + cmd.getPageSizeVal()); + + List responsesList = rulesAndCount.first().stream() + .map(rule -> createInstanceBootGroupReadinessRuleResponse(rule, false, 0)) + .collect(Collectors.toList()); + int totalCount = rulesAndCount.second(); + + if (cmd.getId() == null && cmd.getVirtualMachineId() != null) { + for (InstanceBootGroupReadinessRule rule : instanceBootGroupReadinessRuleService.findInheritedGroupRules(cmd.getBootGroupId(), cmd.getVirtualMachineId())) { + if (ruleType != null && rule.getRuleType() != ruleType) { + continue; + } + if (cmd.getKeyword() != null && (rule.getName() == null || !rule.getName().toLowerCase().contains(cmd.getKeyword().toLowerCase()))) { + continue; + } + responsesList.add(createInstanceBootGroupReadinessRuleResponse(rule, true, cmd.getVirtualMachineId())); + totalCount++; + } + } + + ListResponse response = new ListResponse<>(); + response.setResponses(responsesList, totalCount); + return response; + } + + @Override + public InstanceBootGroupReadinessRuleResponse createInstanceBootGroupReadinessRuleResponse(InstanceBootGroupReadinessRule rule) { + return createInstanceBootGroupReadinessRuleResponse(rule, false, 0); + } + + @Override + public Long getInstanceBootGroupIdForMember(long memberId) { + InstanceBootGroupMember member = instanceBootGroupMemberDao.findById(memberId); + return member == null ? null : member.getBootGroupId(); + } + + private InstanceBootGroupReadinessRuleResponse createInstanceBootGroupReadinessRuleResponse(InstanceBootGroupReadinessRule rule, boolean inherited, long statusVmId) { + InstanceBootGroupReadinessRuleResponse response = new InstanceBootGroupReadinessRuleResponse(); + response.setId(rule.getUuid()); + response.setName(rule.getName()); + InstanceBootGroupVO group = instanceBootGroupDao.findById(rule.getBootGroupId()); + if (group != null) { + response.setBootGroupId(group.getUuid()); + } + response.setItemType(rule.getItemType().name()); + response.setEnabled(rule.isEnabled()); + response.setRuleType(rule.getRuleType().name()); + response.setCreated(rule.getCreated()); + response.setDetails(instanceBootGroupReadinessRuleDetailsDao.getDetails(rule.getId())); + response.setInherited(inherited); + + if (rule.getItemType() == InstanceBootGroupMember.MemberType.VirtualMachine) { + UserVmVO vm = userVmDao.findById(rule.getItemId()); + if (vm != null) { + response.setItemId(vm.getUuid()); + response.setItemName(StringUtils.defaultIfEmpty(vm.getDisplayName(), vm.getHostName())); + } + } else { + InstanceGroupVO instanceGroup = instanceGroupDao.findById(rule.getItemId()); + if (instanceGroup != null) { + response.setItemId(instanceGroup.getUuid()); + response.setItemName(instanceGroup.getName()); + } + } + + InstanceBootGroupReadinessCheckResultVO result = instanceBootGroupReadinessCheckResultDao.findByRuleAndVm(rule.getId(), statusVmId); + if (result != null) { + response.setStatus(result.getStatus() == null ? null : result.getStatus().name()); + response.setStatusMessage(result.getMessage()); + response.setCheckedOn(result.getCheckedOn()); + } + + response.setObjectName("instancebootgroupreadinessrule"); + return response; + } + + @Override + public List> getCommands() { + return List.of( + CreateInstanceBootGroupCmd.class, + DeleteInstanceBootGroupCmd.class, + UpdateInstanceBootGroupCmd.class, + ListInstanceBootGroupsCmd.class, + AddMemberToInstanceBootGroupCmd.class, + RemoveInstanceBootGroupMemberCmd.class, + UpdateInstanceBootGroupMemberCmd.class, + ListInstanceBootGroupMembersCmd.class, + StartInstanceBootGroupCmd.class, + StopInstanceBootGroupCmd.class, + RebootInstanceBootGroupCmd.class, + CreateInstanceBootGroupReadinessRuleCmd.class, + UpdateInstanceBootGroupReadinessRuleCmd.class, + DeleteInstanceBootGroupReadinessRuleCmd.class, + ListInstanceBootGroupReadinessRulesCmd.class + ); + } +} diff --git a/server/src/main/java/org/apache/cloudstack/vm/bootgroup/InstanceBootGroupManager.java b/server/src/main/java/org/apache/cloudstack/vm/bootgroup/InstanceBootGroupManager.java new file mode 100644 index 000000000000..8e391c776434 --- /dev/null +++ b/server/src/main/java/org/apache/cloudstack/vm/bootgroup/InstanceBootGroupManager.java @@ -0,0 +1,35 @@ +// 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.vm.bootgroup; + +import com.cloud.utils.component.Manager; + +/** + * Backend/orchestration counterpart to {@link InstanceBootGroupService} (the API-facing contract, + * implemented by {@code InstanceBootGroupApiServiceImpl}). Deliberately takes domain objects, never + * API {@code Cmd} types, so it has no API-layer dependency and can be invoked outside a request + * context (e.g. a background job) without fabricating a CallContext. + */ +public interface InstanceBootGroupManager extends Manager { + + void startInstanceBootGroup(InstanceBootGroupVO group); + + void stopInstanceBootGroup(InstanceBootGroupVO group, boolean forced); + + void rebootInstanceBootGroup(InstanceBootGroupVO group, boolean forced); +} diff --git a/server/src/main/java/org/apache/cloudstack/vm/bootgroup/InstanceBootGroupManagerImpl.java b/server/src/main/java/org/apache/cloudstack/vm/bootgroup/InstanceBootGroupManagerImpl.java new file mode 100644 index 000000000000..e0d2466169e6 --- /dev/null +++ b/server/src/main/java/org/apache/cloudstack/vm/bootgroup/InstanceBootGroupManagerImpl.java @@ -0,0 +1,597 @@ +// 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.vm.bootgroup; + +import java.util.ArrayList; +import java.util.Collection; +import java.util.Comparator; +import java.util.HashMap; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.TreeMap; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.ExecutionException; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; +import java.util.concurrent.Future; +import java.util.stream.Collectors; + +import javax.inject.Inject; +import javax.naming.ConfigurationException; + +import org.apache.cloudstack.api.ApiCommandResourceType; +import org.apache.cloudstack.context.CallContext; +import org.apache.cloudstack.framework.config.ConfigKey; +import org.apache.cloudstack.framework.config.Configurable; +import org.apache.cloudstack.managed.context.ManagedContextRunnable; +import org.apache.cloudstack.vm.bootgroup.readiness.InstanceBootGroupReadinessRule; +import org.apache.cloudstack.vm.bootgroup.readiness.InstanceBootGroupReadinessRuleService; +import org.springframework.stereotype.Component; + +import com.cloud.utils.component.ManagerBase; +import com.cloud.utils.concurrency.NamedThreadFactory; +import com.cloud.utils.exception.CloudRuntimeException; +import com.cloud.vm.InstanceGroup; +import com.cloud.vm.UserVmService; +import com.cloud.vm.UserVmVO; +import com.cloud.vm.VirtualMachine; +import com.cloud.vm.VirtualMachineManager; +import com.cloud.vm.dao.InstanceBootGroupDetailsDao; +import com.cloud.vm.dao.InstanceBootGroupMemberDao; +import com.cloud.vm.dao.InstanceGroupDao; +import com.cloud.vm.dao.InstanceGroupVMMapDao; +import com.cloud.vm.dao.UserVmDao; + +/** + * Backend/orchestration half of the Instance Boot Group feature — tier concurrency, hypervisor + * start/stop/reboot calls, and readiness-gated tier progression. API-cmd handling (ACL, validation, + * response building, command registration) lives in {@code InstanceBootGroupApiServiceImpl}, which + * delegates here with resolved domain objects. + * + *

Per-VM timeout/reboot-attempt bookkeeping during a start is kept purely in-memory, scoped to the + * async job thread executing the start — it is not persisted. Surviving a management-server restart + * mid-run is explicitly not a goal here; if the process restarts, the job (and this bookkeeping) is + * simply lost, same as any other in-flight async job. Current readiness is queryable at any time via + * {@code listInstanceBootGroupMembers?details=readiness}, not via a separate run-history API.

+ */ +@Component +public class InstanceBootGroupManagerImpl extends ManagerBase implements InstanceBootGroupManager, Configurable { + + public static final ConfigKey ReadinessAttemptTimeoutSeconds = new ConfigKey<>("Advanced", Long.class, + "instance.boot.group.readiness.timeout.seconds", "300", + "How long to wait (in seconds) for an instance to become ready during boot group orchestration before starting a new readiness retry attempt. Overridable per boot group.", true); + + public static final ConfigKey ReadinessMaxRetryAttempts = new ConfigKey<>("Advanced", Long.class, + "instance.boot.group.readiness.max.retry.attempts", "5", + "Maximum number of readiness retry attempts for an instance that fails to become ready during boot group orchestration before the boot group start is halted. Overridable per boot group.", true); + + public static final ConfigKey ReadinessPollIntervalSeconds = new ConfigKey<>("Advanced", Long.class, + "instance.boot.group.readiness.poll.interval.seconds", "10", + "How often (in seconds) to re-check instance/instance-group readiness during boot group orchestration, including the minimum pause after a readiness retry attempt that did not reboot the instance before repeating the check that just failed. A very low value can cause rapid repeated (\"hammering\") readiness retries against an instance/VR/host. Global only, not overridable per boot group.", true); + + public static final ConfigKey ReadinessInitialDelaySeconds = new ConfigKey<>("Advanced", Long.class, + "instance.boot.group.readiness.initial.delay.seconds", "30", + "How long to wait (in seconds) after starting or rebooting an instance before its first readiness check of that attempt, giving the guest OS/agent/network time to come up. Overridable per boot group.", true); + + public static final ConfigKey ReadinessRebootOnRetry = new ConfigKey<>("Advanced", Boolean.class, + "instance.boot.group.readiness.reboot.on.retry", "false", + "Whether to reboot an instance between readiness retry attempts during boot group orchestration, instead of just waiting longer. Overridable per boot group.", true); + + public static final ConfigKey ReadinessCheckConcurrency = new ConfigKey<>("Advanced", Long.class, + "instance.boot.group.readiness.check.concurrency", "10", + "Maximum number of instances within a single boot-order tier whose readiness is checked concurrently during boot group orchestration, so one slow check cannot delay every other instance's check in the same poll. Global only, not overridable per boot group.", true); + + public static final ConfigKey MaxMembersPerBootGroup = new ConfigKey<>("Advanced", Long.class, + "instance.boot.group.max.members", "10", + "Maximum number of members that can be added to a single instance boot group.", true, ConfigKey.Scope.Domain); + + @Inject + private InstanceBootGroupMemberDao instanceBootGroupMemberDao; + + @Inject + private UserVmService userVmService; + + @Inject + private UserVmDao userVmDao; + + @Inject + private InstanceGroupDao instanceGroupDao; + + @Inject + private InstanceGroupVMMapDao instanceGroupVMMapDao; + + @Inject + private VirtualMachineManager virtualMachineManager; + + @Inject + private InstanceBootGroupReadinessRuleService instanceBootGroupReadinessRuleService; + + @Inject + private InstanceBootGroupDetailsDao instanceBootGroupDetailsDao; + + @Override + public boolean configure(String name, Map params) throws ConfigurationException { + VirtualMachine.State.getStateMachine().registerListener(new InstanceBootGroupVmStateListener(instanceBootGroupReadinessRuleService)); + return true; + } + + @Override + public String getConfigComponentName() { + return InstanceBootGroupManagerImpl.class.getSimpleName(); + } + + @Override + public ConfigKey[] getConfigKeys() { + return new ConfigKey[]{ReadinessAttemptTimeoutSeconds, ReadinessMaxRetryAttempts, ReadinessPollIntervalSeconds, ReadinessInitialDelaySeconds, ReadinessRebootOnRetry, ReadinessCheckConcurrency, + MaxMembersPerBootGroup}; + } + + /** In-memory-only per-VM progress for a single start attempt — never persisted. */ + private static final class VmProgress { + private final long vmId; + private final Long bootGroupMemberId; + private boolean ready; + /** Set when this VM has exhausted its retry attempts but belongs to an InstanceGroup + * member, so the group's own readiness rule (e.g. a quorum rule) gets the final say + * instead of this one VM halting the whole boot group. */ + private boolean gaveUp; + private int retryAttempts; + /** Anchor for the per-attempt timeout window; reset on every retry, rebooted or not. */ + private long enteredWaitAtMs; + /** Anchor for the initial-delay grace period; only reset on the initial start and on an + * actual reboot — a no-op retry (reboot-on-retry disabled) leaves this alone, since there's + * no fresh boot to wait out. */ + private long lastBootedAtMs; + + private VmProgress(long vmId, Long bootGroupMemberId) { + this.vmId = vmId; + this.bootGroupMemberId = bootGroupMemberId; + } + + private String getAttemptsLog(long maxAttempts) { + return String.format("%d/%d", retryAttempts + 1, maxAttempts); + } + } + + @Override + public void startInstanceBootGroup(InstanceBootGroupVO group) { + List members = instanceBootGroupMemberDao.listByBootGroupId(group.getId()); + Map> tiers = groupByOrder(members); + logger.info("Starting {}: {} tier(s), {} member(s) total", group, tiers.size(), members.size()); + long groupStartedAtMs = System.currentTimeMillis(); + + for (Map.Entry> tierEntry : tiers.entrySet()) { + int tierOrder = tierEntry.getKey(); + List tierMembers = tierEntry.getValue(); + + Map progressByVmId = new LinkedHashMap<>(); + for (InstanceBootGroupMemberVO member : tierMembers) { + for (Long vmId : resolveVmIds(List.of(member))) { + progressByVmId.put(vmId, new VmProgress(vmId, member.getId())); + } + } + List tierVmIds = new ArrayList<>(progressByVmId.keySet()); + logger.info("Starting tier {} of {}: {} member(s), {} VM(s)", tierOrder, group, tierMembers.size(), tierVmIds.size()); + long tierStartedAtMs = System.currentTimeMillis(); + + try { + runTierConcurrently(tierVmIds, group, "start", vmId -> { + UserVmVO vm = userVmDao.findById(vmId); + boolean alreadyRunning = vm != null && VirtualMachine.State.Running.equals(vm.getState()); + if (vm != null && !alreadyRunning) { + userVmService.startVirtualMachine(vm, null); + } + anchorInitialDelay(group, progressByVmId.get(vmId), vm, alreadyRunning); + }); + } catch (CloudRuntimeException e) { + halt(group, "Failed to start a VM in tier " + tierOrder + ": " + e.getMessage()); + throw e; + } + + waitForTierReady(group, tierOrder, tierMembers, progressByVmId); + logger.info("Tier {} of {} is ready ({}ms)", tierOrder, group, System.currentTimeMillis() - tierStartedAtMs); + } + + logger.info("{} start completed ({}ms)", group, System.currentTimeMillis() - groupStartedAtMs); + } + + /** + * If the VM was already running, anchors the initial-delay grace period to when CloudStack last + * confirmed its power state rather than to "now" — so it waits out only what's left of the + * delay (or none) instead of a full fresh wait it doesn't need. + */ + private void anchorInitialDelay(InstanceBootGroupVO group, VmProgress progress, UserVmVO vm, boolean alreadyRunning) { + long now = System.currentTimeMillis(); + progress.enteredWaitAtMs = now; + if (alreadyRunning && vm.getPowerStateUpdateTime() != null) { + progress.lastBootedAtMs = vm.getPowerStateUpdateTime().getTime(); + logger.debug("{} was already running (power state last confirmed {}); readiness checks begin after any remaining portion of the {}s initial delay", + vm, vm.getPowerStateUpdateTime(), effectiveInitialDelaySeconds(group)); + } else { + progress.lastBootedAtMs = now; + logger.debug("{} start action completed; readiness checks begin after the {}s initial delay", vm, effectiveInitialDelaySeconds(group)); + } + } + + /** + * Polls every not-yet-settled VM in the tier concurrently (bounded by + * {@code ReadinessCheckConcurrency}) until the whole tier — VMs and any InstanceGroup + * members — reports ready, or a halt is triggered. + */ + private void waitForTierReady(InstanceBootGroupVO group, int tierOrder, List tierMembers, Map progressByVmId) { + Map memberById = new HashMap<>(); + Map membersReadyStatus = new ConcurrentHashMap<>(); + for (InstanceBootGroupMemberVO member : tierMembers) { + memberById.put(member.getId(), member); + membersReadyStatus.put(member.getId(), false); + } + final long effectiveMaxRetryAttempts = effectiveMaxRetryAttempts(group); + final long effectiveTimeoutSeconds = effectiveTimeoutSeconds(group); + final long effectivePollIntervalSeconds = effectivePollIntervalSeconds(); + final long pollIntervalMs = effectivePollIntervalSeconds * 1000L; + final boolean effectiveRebootOnRetry = effectiveRebootOnRetry(group); + int concurrency = (int) Math.max(1, Math.min(progressByVmId.size(), effectiveReadinessCheckConcurrency())); + // Bound the polling loop: initial delay + (maxRetries + 1) full timeout windows + inter-poll sleeps. + long maxWaitMs = (effectiveInitialDelaySeconds(group) + (effectiveMaxRetryAttempts + 1) * effectiveTimeoutSeconds + + effectiveMaxRetryAttempts * effectivePollIntervalSeconds) * 1000L; + long deadline = System.currentTimeMillis() + maxWaitMs; + logger.debug("Waiting for tier {} of {} to become ready: {} VM(s) tracked, timeout={}s, pollInterval={}s, checkConcurrency={}, maxWait={}ms", + tierOrder, group, progressByVmId.size(), effectiveTimeoutSeconds, effectivePollIntervalSeconds, concurrency, maxWaitMs); + + CallContext callerContext = CallContext.current(); + ExecutorService readinessExecutor = Executors.newFixedThreadPool(concurrency, new NamedThreadFactory("InstanceBootGroup-readiness-" + tierOrder)); + try { + while (System.currentTimeMillis() < deadline) { + List> futures = new ArrayList<>(); + for (VmProgress progress : progressByVmId.values()) { + if (progress.ready || progress.gaveUp) { + continue; + } + futures.add(readinessExecutor.submit(() -> { + CallContext.register(callerContext, ApiCommandResourceType.VirtualMachine); + try { + checkVmReadiness(group, progress, memberById, membersReadyStatus, + effectiveMaxRetryAttempts, effectiveTimeoutSeconds, effectiveRebootOnRetry); + } finally { + CallContext.unregister(); + } + return null; + })); + } + for (Future future : futures) { + try { + future.get(); + } catch (ExecutionException e) { + Throwable cause = e.getCause() != null ? e.getCause() : e; + if (cause instanceof CloudRuntimeException) { + throw (CloudRuntimeException) cause; + } + throw new CloudRuntimeException("Failed to evaluate readiness for a VM in tier " + tierOrder + " of " + group.getName() + ": " + cause.getMessage(), cause); + } catch (InterruptedException e) { + Thread.currentThread().interrupt(); + throw new CloudRuntimeException("Interrupted while evaluating readiness for tier " + tierOrder + " of " + group.getName(), e); + } + } + + checkInstanceGroupMembersReady(group, tierMembers, progressByVmId, membersReadyStatus); + + if (membersReadyStatus.values().stream().allMatch(Boolean::booleanValue)) { + return; + } + + sleep(pollIntervalMs); + } + String reason = String.format("Tier %d of boot group '%s' did not become ready within the maximum wait of %dms", tierOrder, group.getName(), maxWaitMs); + logger.error(reason); + halt(group, reason); + throw new CloudRuntimeException(reason); + } finally { + readinessExecutor.shutdown(); + } + } + + /** + * Runs on one of {@code waitForTierReady}'s pooled threads for a single VM: gates on the + * initial-delay window, dispatches this poll's check with the remaining time budget, and treats + * Error the same as NotReady — both get a retry before anything halts. + */ + private void checkVmReadiness(InstanceBootGroupVO group, VmProgress progress, Map memberById, + Map membersReadyStatus, long effectiveMaxRetryAttempts, long effectiveTimeoutSeconds, + boolean effectiveRebootOnRetry) { + UserVmVO vm = userVmDao.findById(progress.vmId); + long elapsedMs = System.currentTimeMillis() - progress.enteredWaitAtMs; + long elapsedSinceBootMs = System.currentTimeMillis() - progress.lastBootedAtMs; + long initialDelayMs = effectiveInitialDelaySeconds(group) * 1000L; + if (elapsedSinceBootMs < initialDelayMs) { + logger.debug("{} still within the initial delay window ({}ms elapsed of {}ms since last boot) — skipping readiness check this poll. Attempt: {}", + vm, elapsedSinceBootMs, initialDelayMs, progress.getAttemptsLog(effectiveMaxRetryAttempts)); + return; + } + + long remainingMs = Math.max(0, effectiveTimeoutSeconds * 1000L - elapsedMs); + String attemptLabel = progress.getAttemptsLog(effectiveMaxRetryAttempts); + logger.debug("Evaluating readiness of {} for {} ({}ms since this attempt started, {}ms remaining budget). Attempt: {}", + vm, group, elapsedMs, remainingMs, attemptLabel); + InstanceBootGroupReadinessRule.Status readiness = instanceBootGroupReadinessRuleService.evaluateVmReadiness(group.getId(), progress.vmId, remainingMs, attemptLabel); + if (readiness == InstanceBootGroupReadinessRule.Status.Ready) { + progress.ready = true; + logger.debug("{} is ready for {}", vm, group); + InstanceBootGroupMemberVO member = memberById.get(progress.bootGroupMemberId); + if (member != null && InstanceBootGroupMember.MemberType.VirtualMachine.equals(member.getMemberType())) { + membersReadyStatus.put(progress.bootGroupMemberId, true); + } + return; + } + + if (progress.retryAttempts < effectiveMaxRetryAttempts) { + long now = System.currentTimeMillis(); + if (effectiveRebootOnRetry) { + rebootVm(progress.vmId); + progress.lastBootedAtMs = now; + } + logger.debug("{} readiness retry attempt {} of {} with a reboot={}", + vm, progress.getAttemptsLog(effectiveMaxRetryAttempts), group, effectiveRebootOnRetry); + progress.retryAttempts++; + progress.enteredWaitAtMs = now; + } else { + progress.gaveUp = true; + InstanceBootGroupMemberVO member = memberById.get(progress.bootGroupMemberId); + if (member != null && InstanceBootGroupMember.MemberType.InstanceGroup.equals(member.getMemberType())) { + logger.warn("{} failed readiness after {} retry attempts; giving up on it and deferring to {}'s own readiness rule", + vm, progress.getAttemptsLog(effectiveMaxRetryAttempts), member); + } else { + String reason = String.format("Instance '%s' failed readiness after %s retry attempts", + vm.getName(), progress.getAttemptsLog(effectiveMaxRetryAttempts)); + logger.warn("{} failed readiness after {} retry attempts; halting {}", + vm, progress.getAttemptsLog(effectiveMaxRetryAttempts), group); + halt(group, reason); + throw new CloudRuntimeException(reason); + } + } + } + + /** + * Sequential pass over the tier's InstanceGroup members, run once all of this poll's per-VM + * tasks finish. An empty member list is treated as settled — {@code allMatch()} on an empty + * stream is vacuously true either way, so there's nothing left to wait for. + */ + private void checkInstanceGroupMembersReady(InstanceBootGroupVO group, List tierMembers, + Map progressByVmId, Map membersReadyStatus) { + for (InstanceBootGroupMemberVO member : tierMembers) { + if (!InstanceBootGroupMember.MemberType.InstanceGroup.equals(member.getMemberType()) || membersReadyStatus.getOrDefault(member.getId(), false)) { + continue; + } + InstanceGroup instanceGroup = instanceGroupDao.findById(member.getMemberId()); + Collection memberProgresses = progressByVmId.values().stream() + .filter(p -> member.getId() == (p.bootGroupMemberId == null ? -1 : p.bootGroupMemberId)) + .collect(Collectors.toList()); + if (!memberProgresses.isEmpty() && memberProgresses.stream().allMatch(p -> !p.ready && !p.gaveUp)) { + logger.debug("{} part of {} has no VMs that are ready or have exhausted their retries yet", instanceGroup, group); + continue; + } + + Set permanentlyFailedVmIds = memberProgresses.stream() + .filter(p -> p.gaveUp) + .map(p -> p.vmId) + .collect(Collectors.toSet()); + InstanceBootGroupReadinessRule.Status groupStatus = + instanceBootGroupReadinessRuleService.evaluateInstanceGroupReadiness(group.getId(), member.getMemberId(), permanentlyFailedVmIds); + if (groupStatus == InstanceBootGroupReadinessRule.Status.Ready) { + membersReadyStatus.put(member.getId(), true); + logger.info("{} part of {} reached readiness state Ready", instanceGroup, group); + continue; + } + if (InstanceBootGroupReadinessRule.Status.Error.equals(groupStatus) || + (InstanceBootGroupReadinessRule.Status.NotReady.equals(groupStatus) && + memberProgresses.stream().allMatch(p -> p.ready || p.gaveUp))) { + String reason = String.format("Instance group '%s' failed its own readiness rules", instanceGroup.getName()); + logger.error("{} failed its own readiness rules; halting {}", instanceGroup, group); + halt(group, reason); + throw new CloudRuntimeException(reason); + } + } + } + + /** + * Only stops the orchestration loop — never stops a VM, since every VM touched by this point may + * already be running and tearing it down would be destructive, not recoverable. + */ + private void halt(InstanceBootGroupVO group, String reason) { + logger.warn("Halting {} start: {}", group, reason); + } + + private void rebootVm(long vmId) { + UserVmVO vm = userVmDao.findById(vmId); + if (vm == null) { + logger.warn("Cannot reboot instance id {} for a boot group readiness retry: VM not found", vmId); + return; + } + logger.debug("Rebooting {} for a boot group readiness retry attempt", vm); + try { + virtualMachineManager.reboot(vm.getUuid(), null); + } catch (Exception e) { + throw new CloudRuntimeException("Failed to reboot VM " + vm + " during boot group readiness retry: " + e.getMessage(), e); + } + } + + private void sleep(long millis) { + try { + Thread.sleep(millis); + } catch (InterruptedException e) { + Thread.currentThread().interrupt(); + throw new CloudRuntimeException("Interrupted while waiting for boot group tier readiness", e); + } + } + + private long effectiveTimeoutSeconds(InstanceBootGroupVO group) { + String override = instanceBootGroupDetailsDao.getDetail(group.getId(), ReadinessAttemptTimeoutSeconds.key()); + return override != null ? Long.parseLong(override) : ReadinessAttemptTimeoutSeconds.value(); + } + + private long effectiveMaxRetryAttempts(InstanceBootGroupVO group) { + String override = instanceBootGroupDetailsDao.getDetail(group.getId(), ReadinessMaxRetryAttempts.key()); + return override != null ? Long.parseLong(override) : ReadinessMaxRetryAttempts.value(); + } + + private long effectivePollIntervalSeconds() { + return ReadinessPollIntervalSeconds.value(); + } + + private long effectiveReadinessCheckConcurrency() { + return ReadinessCheckConcurrency.value(); + } + + private long effectiveInitialDelaySeconds(InstanceBootGroupVO group) { + String override = instanceBootGroupDetailsDao.getDetail(group.getId(), ReadinessInitialDelaySeconds.key()); + return override != null ? Long.parseLong(override) : ReadinessInitialDelaySeconds.value(); + } + + private boolean effectiveRebootOnRetry(InstanceBootGroupVO group) { + String override = instanceBootGroupDetailsDao.getDetail(group.getId(), ReadinessRebootOnRetry.key()); + return override != null ? Boolean.parseBoolean(override) : ReadinessRebootOnRetry.value(); + } + + @Override + public void stopInstanceBootGroup(InstanceBootGroupVO group, boolean forced) { + List members = instanceBootGroupMemberDao.listByBootGroupId(group.getId()); + Map> tiers = groupByOrderDescending(members); + logger.info("Stopping {}: {} tier(s), {} member(s) total, forced={}", group, tiers.size(), members.size(), forced); + long groupStoppedAtMs = System.currentTimeMillis(); + + for (Map.Entry> tier : tiers.entrySet()) { + List vmIds = resolveVmIds(tier.getValue()); + runTierConcurrently(vmIds, group, "stop", vmId -> { + UserVmVO vm = userVmDao.findById(vmId); + if (vm != null && vm.getState() != com.cloud.vm.VirtualMachine.State.Stopped) { + userVmService.stopVirtualMachine(vmId, forced); + } + }); + } + + logger.info("{} stop completed ({}ms)", group, System.currentTimeMillis() - groupStoppedAtMs); + } + + @Override + public void rebootInstanceBootGroup(InstanceBootGroupVO group, boolean forced) { + logger.info("Rebooting {}: stopping, then starting", group); + stopInstanceBootGroup(group, forced); + startInstanceBootGroup(group); + } + + /** + * Runs {@code action} for every VM in a tier concurrently and aborts on the first failure. Each + * thread gets a copied {@link CallContext} — without one, a VM lifecycle action routed through + * the job-queue path fails to submit its sub-job ("no lock found"). + */ + private void runTierConcurrently(List vmIds, InstanceBootGroupVO group, + String actionName, VmAction action) { + if (vmIds.isEmpty()) { + return; + } + + logger.debug("Running '{}' action for a tier of {}: {} VM id(s) {}", + actionName, group, vmIds.size(), vmIds); + long actionStartedAtMs = System.currentTimeMillis(); + CallContext callerContext = CallContext.current(); + int threadCount = Math.min(vmIds.size(), ReadinessCheckConcurrency.value().intValue()); + ExecutorService executor = Executors.newFixedThreadPool( + threadCount, new NamedThreadFactory("InstanceBootGroup-" + actionName)); + + try { + List> futures = new ArrayList<>(vmIds.size()); + + for (Long vmId : vmIds) { + futures.add(executor.submit(new ManagedContextRunnable() { + @Override + protected void runInContext() { + CallContext.register(callerContext, ApiCommandResourceType.VirtualMachine); + CallContext.current().setEventResourceId(vmId); + try { + action.run(vmId); + } catch (Exception e) { + throw new CloudRuntimeException(String.format("Failed to %s VM %d", actionName, vmId), e); + } finally { + CallContext.unregister(); + } + } + })); + } + + for (Future future : futures) { + try { + future.get(); + } catch (ExecutionException e) { + Throwable cause = e.getCause() != null ? e.getCause() : e; + throw new CloudRuntimeException( + String.format("Failed to %s a VM in boot group %s: %s", + actionName, group.getName(), cause.getMessage()), + cause); + + } catch (InterruptedException e) { + Thread.currentThread().interrupt(); + throw new CloudRuntimeException( + String.format("Interrupted while waiting to %s VMs in boot group %s", + actionName, group.getName()), + e); + } + } + } finally { + executor.shutdown(); + } + + logger.debug( + "'{}' action for a tier of {} completed for {} VM(s) in {}ms", + actionName, group, vmIds.size(), System.currentTimeMillis() - actionStartedAtMs); + } + + private Map> groupByOrder(List members) { + Map> tiers = new TreeMap<>(); + for (InstanceBootGroupMemberVO m : members) { + tiers.computeIfAbsent(m.getOrder(), k -> new ArrayList<>()).add(m); + } + return tiers; + } + + private Map> groupByOrderDescending(List members) { + Map> tiers = new TreeMap<>(Comparator.reverseOrder()); + for (InstanceBootGroupMemberVO m : members) { + tiers.computeIfAbsent(m.getOrder(), k -> new ArrayList<>()).add(m); + } + return tiers; + } + + private List resolveVmIds(List tierMembers) { + List vmIds = new ArrayList<>(); + for (InstanceBootGroupMemberVO member : tierMembers) { + if (member.getMemberType() == InstanceBootGroupMember.MemberType.VirtualMachine) { + vmIds.add(member.getMemberId()); + } else { + instanceGroupVMMapDao.listByGroupId(member.getMemberId()) + .forEach(map -> vmIds.add(map.getInstanceId())); + } + } + return vmIds; + } + + @FunctionalInterface + private interface VmAction { + void run(long vmId) throws Exception; + } +} diff --git a/server/src/main/java/org/apache/cloudstack/vm/bootgroup/InstanceBootGroupMembershipGuard.java b/server/src/main/java/org/apache/cloudstack/vm/bootgroup/InstanceBootGroupMembershipGuard.java new file mode 100644 index 000000000000..38aaee44ece9 --- /dev/null +++ b/server/src/main/java/org/apache/cloudstack/vm/bootgroup/InstanceBootGroupMembershipGuard.java @@ -0,0 +1,110 @@ +// 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.vm.bootgroup; + +import java.util.List; + +import javax.inject.Inject; + +import org.apache.commons.collections.CollectionUtils; +import org.springframework.stereotype.Component; + +import com.cloud.exception.InvalidParameterValueException; +import com.cloud.network.as.dao.AutoScaleVmGroupVmMapDao; +import com.cloud.storage.Storage; +import com.cloud.storage.VMTemplateVO; +import com.cloud.storage.dao.VMTemplateDao; +import com.cloud.vm.InstanceGroupVMMapVO; +import com.cloud.vm.UserVmVO; +import com.cloud.vm.dao.InstanceBootGroupMemberDao; +import com.cloud.vm.dao.InstanceGroupVMMapDao; +import com.cloud.vm.dao.UserVmDao; + +/** + * Eligibility guard shared by the two places a VM can end up governed by a boot group: joining a + * plain Instance Group ({@code UserVmManagerImpl.addInstanceToGroup}) and being added directly to a + * boot group ({@code InstanceBootGroupApiServiceImpl.addMemberToInstanceBootGroup}). Kept as its own + * leaf component (no dependency on UserVmService/UserVmManager) so UserVmManagerImpl can depend on it + * without a circular Spring bean dependency back through InstanceBootGroupApiServiceImpl/Manager, + * which themselves depend on UserVmService. + */ +@Component +public class InstanceBootGroupMembershipGuard { + + @Inject + private UserVmDao userVmDao; + + @Inject + private VMTemplateDao templateDao; + + @Inject + private AutoScaleVmGroupVmMapDao autoScaleVmGroupVmMapDao; + + @Inject + private InstanceBootGroupMemberDao instanceBootGroupMemberDao; + + @Inject + private InstanceGroupVMMapDao instanceGroupVMMapDao; + + /** + * Rejects a VM that is a VNF appliance, currently in any AutoScale VM group, already an + * independent boot-group member, or currently in an Instance Group that is itself already a + * boot-group member. Used both when adding a VM to a plain Instance Group and when adding it + * directly to a boot group. + */ + public void validateVmEligibleForGroupMembership(long vmId) { + UserVmVO vm = userVmDao.findById(vmId); + if (vm == null) { + throw new InvalidParameterValueException("Unable to find a VM with ID: " + vmId); + } + + VMTemplateVO template = templateDao.findByIdIncludingRemoved(vm.getTemplateId()); + if (template != null && Storage.TemplateType.VNF.equals(template.getTemplateType())) { + throw new InvalidParameterValueException(String.format( + "VM %s is a VNF appliance and cannot be added to an instance group or boot group", vm)); + } + + if (CollectionUtils.isNotEmpty(autoScaleVmGroupVmMapDao.listByVm(vmId))) { + throw new InvalidParameterValueException(String.format( + "VM %s is part of an AutoScale VM group and cannot be added to an instance group or boot group", vm)); + } + + if (instanceBootGroupMemberDao.findByMember(InstanceBootGroupMember.MemberType.VirtualMachine, vmId) != null) { + throw new InvalidParameterValueException(String.format( + "VM %s is already an independent member of an instance boot group", vm)); + } + + for (InstanceGroupVMMapVO mapping : instanceGroupVMMapDao.listByInstanceId(vmId)) { + if (instanceBootGroupMemberDao.findByMember(InstanceBootGroupMember.MemberType.InstanceGroup, mapping.getGroupId()) != null) { + throw new InvalidParameterValueException(String.format( + "VM %s is currently in an instance group that is already a member of an instance boot group", vm)); + } + } + } + + /** + * Rejects an Instance Group for boot-group membership if any VM currently in it fails + * {@link #validateVmEligibleForGroupMembership(long)}. + */ + public void validateInstanceGroupEligibleForBootGroupMembership(long instanceGroupId) { + List members = instanceGroupVMMapDao.listByGroupId(instanceGroupId); + for (InstanceGroupVMMapVO member : members) { + validateVmEligibleForGroupMembership(member.getInstanceId()); + } + } +} diff --git a/server/src/main/java/org/apache/cloudstack/vm/bootgroup/InstanceBootGroupVmStateListener.java b/server/src/main/java/org/apache/cloudstack/vm/bootgroup/InstanceBootGroupVmStateListener.java new file mode 100644 index 000000000000..c023a7dddad5 --- /dev/null +++ b/server/src/main/java/org/apache/cloudstack/vm/bootgroup/InstanceBootGroupVmStateListener.java @@ -0,0 +1,66 @@ +// 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.vm.bootgroup; + +import org.apache.cloudstack.vm.bootgroup.readiness.InstanceBootGroupReadinessRuleService; +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; + +import com.cloud.utils.fsm.StateListener; +import com.cloud.utils.fsm.StateMachine2; +import com.cloud.vm.VirtualMachine; +import com.cloud.vm.VirtualMachine.Event; +import com.cloud.vm.VirtualMachine.State; + +/** + * Invalidates a VM's cached boot-group readiness the moment it actually comes up from a cold start + * (Starting -> Running only — not a live-migration landing in Running, nor a same-state + * confirmation self-transition), so a VM started outside boot group orchestration can't keep + * reporting a stale Ready left over from before it stopped. Deliberately does nothing on the way + * down to Stopped — {@code InstanceBootGroupReadinessRuleManagerImpl}'s own state-aware reads already + * make a stopped VM report NotReady regardless of cache, and leaving that cache row untouched is what + * lets {@code ignoreinstancestate} keep showing the last real check result for diagnosis. + */ +public class InstanceBootGroupVmStateListener implements StateListener { + + protected Logger logger = LogManager.getLogger(getClass()); + + private final InstanceBootGroupReadinessRuleService instanceBootGroupReadinessRuleService; + + public InstanceBootGroupVmStateListener(InstanceBootGroupReadinessRuleService instanceBootGroupReadinessRuleService) { + this.instanceBootGroupReadinessRuleService = instanceBootGroupReadinessRuleService; + } + + @Override + public boolean preStateTransitionEvent(State oldState, Event event, State newState, VirtualMachine vo, boolean status, Object opaque) { + return true; + } + + @Override + public boolean postStateTransitionEvent(StateMachine2.Transition transition, VirtualMachine vo, boolean status, Object opaque) { + if (!status || transition.getCurrentState() != State.Starting || transition.getToState() != State.Running) { + return true; + } + try { + instanceBootGroupReadinessRuleService.invalidateCachedReadinessOnRestart(vo.getId()); + } catch (Exception e) { + logger.warn("Failed to invalidate instance boot group readiness cache for {} after it started", vo, e); + } + return true; + } +} diff --git a/server/src/main/java/org/apache/cloudstack/vm/bootgroup/readiness/GuestAgentLivenessChecker.java b/server/src/main/java/org/apache/cloudstack/vm/bootgroup/readiness/GuestAgentLivenessChecker.java new file mode 100644 index 000000000000..c17f4b7a0255 --- /dev/null +++ b/server/src/main/java/org/apache/cloudstack/vm/bootgroup/readiness/GuestAgentLivenessChecker.java @@ -0,0 +1,100 @@ +// 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.vm.bootgroup.readiness; + +import java.util.Map; + +import javax.inject.Inject; + +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; +import org.springframework.stereotype.Component; + +import com.cloud.agent.AgentManager; +import com.cloud.agent.api.Answer; +import com.cloud.agent.api.CheckGuestAgentLivenessCommand; +import com.cloud.hypervisor.Hypervisor.HypervisorType; +import com.cloud.vm.UserVmVO; +import com.cloud.vm.VirtualMachine; +import com.cloud.vm.dao.UserVmDao; + +/** + * GuestAgentLiveness: dispatched directly to the VM's hypervisor host rather than via the VR, + * asking libvirt to relay a qemu-guest-agent "guest-ping" over the VM's virtio-serial channel. + * Only supported on KVM, since that channel is a KVM/libvirt-specific mechanism. + */ +@Component +public class GuestAgentLivenessChecker implements ReadinessChecker { + protected static Logger LOGGER = LogManager.getLogger(GuestAgentLivenessChecker.class); + + @Inject + private UserVmDao userVmDao; + + @Inject + private AgentManager agentManager; + + @Override + public InstanceBootGroupReadinessRule.RuleType getRuleType() { + return InstanceBootGroupReadinessRule.RuleType.GuestAgentLiveness; + } + + @Override + public Logger getLogger() { + return LOGGER; + } + + @Override + public Result check(InstanceBootGroupReadinessRule rule, Map details, long vmId, long remainingMs) { + UserVmVO vm = userVmDao.findById(vmId); + if (vm == null) { + return logAndReturn(rule, vmId, new Result(InstanceBootGroupReadinessRule.Status.Error, "Instance not found")); + } + + LOGGER.debug("Checking guest agent liveness for {} due to rule {}", vm, rule); + + if (vm.getHypervisorType() != HypervisorType.KVM) { + return logAndReturn(rule, vm, new Result(InstanceBootGroupReadinessRule.Status.Error, + "Guest agent liveness checks are only supported on KVM; Instance's hypervisor is " + vm.getHypervisorType())); + } + if (!VirtualMachine.State.Running.equals(vm.getState()) || vm.getHostId() == null) { + return logAndReturn(rule, vm, new Result(InstanceBootGroupReadinessRule.Status.NotReady, "Instance is not running")); + } + + if (remainingMs < MIN_REMAINING_MS_TO_DISPATCH) { + return logAndReturn(rule, vm, new Result(InstanceBootGroupReadinessRule.Status.Error, + "Insufficient time remaining in this attempt's budget (" + remainingMs + "ms) to dispatch a guest agent liveness check")); + } + + CheckGuestAgentLivenessCommand command = new CheckGuestAgentLivenessCommand(vm.getInstanceName()); + command.setWait(computeWaitSeconds(remainingMs)); + LOGGER.debug("Dispatching guest agent liveness check for {} to host id {} with {}ms remaining budget", vm, vm.getHostId(), remainingMs); + Answer answer; + try { + answer = agentManager.easySend(vm.getHostId(), command); + } catch (Exception e) { + return logAndReturn(rule, vm, new Result(InstanceBootGroupReadinessRule.Status.Error, "Failed to dispatch guest agent liveness check: " + e.getMessage())); + } + if (answer == null) { + return logAndReturn(rule, vm, new Result(InstanceBootGroupReadinessRule.Status.Error, "No answer from the Instance's host")); + } + if (answer.getResult()) { + return logAndReturn(rule, vm, new Result(InstanceBootGroupReadinessRule.Status.Ready, "guest agent responded")); + } + return logAndReturn(rule, vm, new Result(InstanceBootGroupReadinessRule.Status.NotReady, "guest agent did not respond: " + answer.getDetails())); + } +} diff --git a/server/src/main/java/org/apache/cloudstack/vm/bootgroup/readiness/InstanceBootGroupReadinessRuleManagerImpl.java b/server/src/main/java/org/apache/cloudstack/vm/bootgroup/readiness/InstanceBootGroupReadinessRuleManagerImpl.java new file mode 100644 index 000000000000..6189eec1f50d --- /dev/null +++ b/server/src/main/java/org/apache/cloudstack/vm/bootgroup/readiness/InstanceBootGroupReadinessRuleManagerImpl.java @@ -0,0 +1,620 @@ +// 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.vm.bootgroup.readiness; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.Date; +import java.util.EnumSet; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.stream.Collectors; + +import javax.inject.Inject; + +import org.apache.cloudstack.vm.bootgroup.InstanceBootGroupMember; +import org.apache.cloudstack.vm.bootgroup.InstanceBootGroupMemberVO; +import org.apache.cloudstack.vm.bootgroup.InstanceBootGroupReadinessCheckResultVO; +import org.apache.cloudstack.vm.bootgroup.InstanceBootGroupReadinessRuleVO; +import org.apache.commons.collections.MapUtils; +import org.apache.commons.lang3.StringUtils; +import org.springframework.stereotype.Component; + +import com.cloud.exception.InvalidParameterValueException; +import com.cloud.hypervisor.Hypervisor.HypervisorType; +import com.cloud.utils.component.ManagerBase; +import com.cloud.utils.db.Transaction; +import com.cloud.utils.db.TransactionCallback; +import com.cloud.vm.InstanceGroupVMMapVO; +import com.cloud.vm.UserVmVO; +import com.cloud.vm.VirtualMachine; +import com.cloud.vm.dao.InstanceBootGroupMemberDao; +import com.cloud.vm.dao.InstanceBootGroupReadinessCheckResultDao; +import com.cloud.vm.dao.InstanceBootGroupReadinessRuleDao; +import com.cloud.vm.dao.InstanceBootGroupReadinessRuleDetailsDao; +import com.cloud.vm.dao.InstanceGroupDao; +import com.cloud.vm.dao.InstanceGroupVMMapDao; +import com.cloud.vm.dao.UserVmDao; + +@Component +public class InstanceBootGroupReadinessRuleManagerImpl extends ManagerBase implements InstanceBootGroupReadinessRuleService { + + private static final String THRESHOLD_TYPE_KEY = "threshold_type"; + private static final String THRESHOLD_VALUE_KEY = "threshold_value"; + private static final String PORT_KEY = "port"; + private static final String PROTOCOL_KEY = "protocol"; + + private static final Map> VALID_RULE_TYPES_BY_ITEM_TYPE = Map.of( + InstanceBootGroupMember.MemberType.VirtualMachine, EnumSet.of( + InstanceBootGroupReadinessRule.RuleType.GuestAgentLiveness, + InstanceBootGroupReadinessRule.RuleType.Ping, + InstanceBootGroupReadinessRule.RuleType.PortCheck, + InstanceBootGroupReadinessRule.RuleType.CustomScript), + InstanceBootGroupMember.MemberType.InstanceGroup, EnumSet.of( + InstanceBootGroupReadinessRule.RuleType.GuestAgentLiveness, + InstanceBootGroupReadinessRule.RuleType.Ping, + InstanceBootGroupReadinessRule.RuleType.PortCheck, + InstanceBootGroupReadinessRule.RuleType.CustomScript, + InstanceBootGroupReadinessRule.RuleType.MemberQuorum)); + + /** + * Rule types an item may have at most one of — Ping/GuestAgentLiveness each check a single fixed + * target on the VM, and MemberQuorum aggregates the whole InstanceGroup, so a second one would + * just be redundant. PortCheck (different ports) and CustomScript are not singletons. + */ + private static final Set SINGLETON_RULE_TYPES = EnumSet.of( + InstanceBootGroupReadinessRule.RuleType.Ping, + InstanceBootGroupReadinessRule.RuleType.GuestAgentLiveness, + InstanceBootGroupReadinessRule.RuleType.MemberQuorum); + + @Inject + private InstanceBootGroupReadinessRuleDao instanceBootGroupReadinessRuleDao; + + @Inject + private InstanceBootGroupReadinessRuleDetailsDao instanceBootGroupReadinessRuleDetailsDao; + + @Inject + private InstanceBootGroupReadinessCheckResultDao instanceBootGroupReadinessCheckResultDao; + + @Inject + private InstanceBootGroupMemberDao instanceBootGroupMemberDao; + + @Inject + private InstanceGroupVMMapDao instanceGroupVMMapDao; + + @Inject + private InstanceGroupDao instanceGroupDao; + + @Inject + private UserVmDao userVmDao; + + private List readinessCheckers; + + private Map checkersByRuleType; + + protected void updateCheckersByRuleType(boolean forced) { + if (MapUtils.isNotEmpty(checkersByRuleType) && !forced) { + return; + } + checkersByRuleType = new HashMap<>(); + for (ReadinessChecker checker : readinessCheckers) { + checkersByRuleType.put(checker.getRuleType(), checker); + } + } + + protected ReadinessChecker getCheckerByRuleType(InstanceBootGroupReadinessRule.RuleType ruleType) { + updateCheckersByRuleType(false); + return checkersByRuleType.get(ruleType); + } + + public List getReadinessCheckers() { + return readinessCheckers; + } + + public void setReadinessCheckers(List readinessCheckers) { + this.readinessCheckers = readinessCheckers; + updateCheckersByRuleType(true); + } + + @Override + public InstanceBootGroupReadinessRule createReadinessRule(long bootGroupId, InstanceBootGroupMember.MemberType itemType, long itemId, + InstanceBootGroupReadinessRule.RuleType ruleType, String name, boolean enabled, Map details) { + validateRuleTypeForItemType(itemType, ruleType); + validateItemBelongsToBootGroup(bootGroupId, itemType, itemId); + validateSingletonRuleType(bootGroupId, itemType, itemId, ruleType); + validateGuestAgentLivenessSupported(itemType, itemId, ruleType); + validateRuleTypeSpecificDetails(ruleType, details); + + String effectiveName = StringUtils.isNotBlank(name) ? name : String.format("%s-%s-%d", ruleType.name(), itemType.name(), itemId); + InstanceBootGroupReadinessRuleVO rule = new InstanceBootGroupReadinessRuleVO(effectiveName, bootGroupId, itemType, itemId, ruleType, enabled); + rule = instanceBootGroupReadinessRuleDao.persist(rule); + + if (details != null) { + for (Map.Entry entry : details.entrySet()) { + instanceBootGroupReadinessRuleDetailsDao.addDetail(rule.getId(), entry.getKey(), entry.getValue(), true); + } + } + return rule; + } + + @Override + public InstanceBootGroupReadinessRule updateReadinessRule(long ruleId, String name, Boolean enabled, Map details) { + InstanceBootGroupReadinessRuleVO rule = instanceBootGroupReadinessRuleDao.findById(ruleId); + if (rule == null) { + throw new InvalidParameterValueException("Unable to find a readiness rule with ID: " + ruleId); + } + + if (StringUtils.isNotBlank(name)) { + rule.setName(name); + } + if (enabled != null) { + rule.setEnabled(enabled); + } + if (details != null) { + Map mergedDetails = new HashMap<>(instanceBootGroupReadinessRuleDetailsDao.getDetails(rule.getId())); + mergedDetails.putAll(details); + validateRuleTypeSpecificDetails(rule.getRuleType(), mergedDetails); + } + instanceBootGroupReadinessRuleDao.update(rule.getId(), rule); + + if (details != null) { + for (Map.Entry entry : details.entrySet()) { + instanceBootGroupReadinessRuleDetailsDao.addDetail(rule.getId(), entry.getKey(), entry.getValue(), true); + } + } + return instanceBootGroupReadinessRuleDao.findById(rule.getId()); + } + + @Override + public boolean deleteReadinessRule(long ruleId) { + InstanceBootGroupReadinessRuleVO rule = instanceBootGroupReadinessRuleDao.findById(ruleId); + if (rule == null) { + throw new InvalidParameterValueException("Unable to find a readiness rule with ID: " + ruleId); + } + return Transaction.execute((TransactionCallback) status -> { + instanceBootGroupReadinessRuleDetailsDao.removeDetails(rule.getId()); + instanceBootGroupReadinessCheckResultDao.deleteByRuleId(rule.getId()); + instanceBootGroupReadinessRuleDao.remove(rule.getId()); + return true; + }); + } + + @Override + public InstanceBootGroupReadinessRule findById(long ruleId) { + return instanceBootGroupReadinessRuleDao.findById(ruleId); + } + + @Override + public Map getRuleDetails(long ruleId) { + return instanceBootGroupReadinessRuleDetailsDao.getDetails(ruleId); + } + + @Override + public InstanceBootGroupReadinessRule.Status evaluateVmReadiness(long bootGroupId, long vmId, long remainingMs, String attemptLabel) { + return resolveVmReadiness(bootGroupId, vmId, true, remainingMs, attemptLabel); + } + + /** + * Same aggregation as {@link #evaluateVmReadiness}, but reads each rule's last-cached result + * instead of dispatching a fresh check — for callers that run after the per-VM loop has already + * dispatched this poll, so a live re-check would just repeat the same remote command. + */ + private InstanceBootGroupReadinessRule.Status getCachedVmReadiness(long bootGroupId, long vmId) { + return resolveVmReadiness(bootGroupId, vmId, false, Long.MAX_VALUE, null); + } + + /** + * Shared by the dispatching and cache-reading paths. A non-Running VM is always NotReady regardless + * of any cached rule result. Direct rules cache at vmId 0, inherited group rules per member vmId; + * when dispatching, the remaining budget shrinks across a VM's rules so N slow rules can't each burn + * the full per-attempt timeout. + */ + private InstanceBootGroupReadinessRule.Status resolveVmReadiness(long bootGroupId, long vmId, boolean dispatch, long remainingMs, String attemptLabel) { + UserVmVO vm = userVmDao.findById(vmId); + if (vm == null) { + logger.debug("VM id {} not found while evaluating readiness for boot group id {}; treating as NotReady", vmId, bootGroupId); + return InstanceBootGroupReadinessRule.Status.NotReady; + } + if (!VirtualMachine.State.Running.equals(vm.getState())) { + logger.debug("{} is not Running (state={}) for boot group id {}; treating as NotReady without evaluating or dispatching its readiness rules", vm, vm.getState(), bootGroupId); + return InstanceBootGroupReadinessRule.Status.NotReady; + } + List directRules = instanceBootGroupReadinessRuleDao.listEnabledByItem(bootGroupId, InstanceBootGroupMember.MemberType.VirtualMachine, vmId); + List inheritedRules = findInheritedGroupRuleVOs(bootGroupId, vmId); + + if (directRules.isEmpty() && inheritedRules.isEmpty()) { + logger.debug("{} has no readiness rules for boot group id {}; derived readiness Ready from its current state ({})", vm, bootGroupId, vm.getState()); + return InstanceBootGroupReadinessRule.Status.Ready; + } + + if (dispatch) { + logger.debug("Evaluating readiness of {} for boot group id {}: {} direct rule(s), {} inherited rule(s), {}ms remaining budget", + vm, bootGroupId, directRules.size(), inheritedRules.size(), remainingMs); + } + + boolean anyError = false; + boolean anyNotReady = false; + long remainingBudgetMs = remainingMs; + for (InstanceBootGroupReadinessRuleVO rule : directRules) { + long startedAtMs = System.currentTimeMillis(); + InstanceBootGroupReadinessRule.Status status = dispatch ? evaluateAndCacheRule(rule, vmId, 0, remainingBudgetMs, attemptLabel) : readCachedRuleStatus(rule.getId(), 0); + if (dispatch) { + remainingBudgetMs = Math.max(0, remainingBudgetMs - (System.currentTimeMillis() - startedAtMs)); + } + if (status == InstanceBootGroupReadinessRule.Status.Error) { + anyError = true; + } else if (status != InstanceBootGroupReadinessRule.Status.Ready) { + anyNotReady = true; + } + } + for (InstanceBootGroupReadinessRuleVO rule : inheritedRules) { + long startedAtMs = System.currentTimeMillis(); + InstanceBootGroupReadinessRule.Status status = dispatch ? evaluateAndCacheRule(rule, vmId, vmId, remainingBudgetMs, attemptLabel) : readCachedRuleStatus(rule.getId(), vmId); + if (dispatch) { + remainingBudgetMs = Math.max(0, remainingBudgetMs - (System.currentTimeMillis() - startedAtMs)); + } + if (status == InstanceBootGroupReadinessRule.Status.Error) { + anyError = true; + } else if (status != InstanceBootGroupReadinessRule.Status.Ready) { + anyNotReady = true; + } + } + + InstanceBootGroupReadinessRule.Status overallStatus = anyError ? InstanceBootGroupReadinessRule.Status.Error + : (anyNotReady ? InstanceBootGroupReadinessRule.Status.NotReady : InstanceBootGroupReadinessRule.Status.Ready); + if (dispatch) { + logger.debug("{} readiness for boot group id {} evaluated as {}", vm, bootGroupId, overallStatus); + } + return overallStatus; + } + + private InstanceBootGroupReadinessRule.Status readCachedRuleStatus(long ruleId, long cacheVmId) { + InstanceBootGroupReadinessCheckResultVO cached = instanceBootGroupReadinessCheckResultDao.findByRuleAndVm(ruleId, cacheVmId); + return (cached != null && cached.getStatus() != null) ? cached.getStatus() : InstanceBootGroupReadinessRule.Status.Unknown; + } + + /** + * Dispatches (or, for a missing checker, synthesizes) one rule's check and persists the result — + * the single place a check actually happens, so every caller shares this one log/cache path. + * @param attemptLabel e.g. {@code "2/5"}, appended to the persisted message; pass {@code null} to skip. + */ + private InstanceBootGroupReadinessRule.Status evaluateAndCacheRule(InstanceBootGroupReadinessRuleVO rule, long vmId, long cacheVmId, long remainingMs, String attemptLabel) { + logger.debug("Evaluating rule {} against VM id {} with {}ms remaining budget", () -> rule, () -> userVmDao.findById(vmId), () -> remainingMs); + ReadinessChecker checker = getCheckerByRuleType(rule.getRuleType()); + InstanceBootGroupReadinessRule.Status status; + String message; + if (checker == null) { + status = InstanceBootGroupReadinessRule.Status.Error; + message = "No checker implemented yet for rule type " + rule.getRuleType(); + } else { + Map details = instanceBootGroupReadinessRuleDetailsDao.getDetails(rule.getId()); + ReadinessChecker.Result result = checker.check(rule, details, vmId, remainingMs); + status = result.getStatus(); + message = result.getMessage(); + } + String finalMessage = StringUtils.isNotBlank(attemptLabel) ? message + " (attempt " + attemptLabel + ")" : message; + logger.debug("Rule {} evaluated against VM id {}: status={}, message={}", () -> rule, () -> userVmDao.findById(vmId), () -> status, () -> finalMessage); + instanceBootGroupReadinessCheckResultDao.upsert(rule.getId(), cacheVmId, status, finalMessage, new Date()); + return status; + } + + @Override + public List findInheritedGroupRules(long bootGroupId, long vmId) { + return new ArrayList<>(findInheritedGroupRuleVOs(bootGroupId, vmId)); + } + + /** + * Resets a VM's own cached rule results (direct and inherited) to Unknown when it starts, so a + * VM restarted outside boot group orchestration can't keep reporting a stale Ready from before + * it stopped. A no-op for a VM with no boot-group involvement at all. + */ + @Override + public void invalidateCachedReadinessOnRestart(long vmId) { + InstanceBootGroupMemberVO directMember = instanceBootGroupMemberDao.findByMember(InstanceBootGroupMember.MemberType.VirtualMachine, vmId); + if (directMember != null) { + invalidateRuleResults(instanceBootGroupReadinessRuleDao.listEnabledByItem(directMember.getBootGroupId(), InstanceBootGroupMember.MemberType.VirtualMachine, vmId), 0L); + } + for (InstanceGroupVMMapVO mapping : instanceGroupVMMapDao.listByInstanceId(vmId)) { + InstanceBootGroupMemberVO groupMember = instanceBootGroupMemberDao.findByMember(InstanceBootGroupMember.MemberType.InstanceGroup, mapping.getGroupId()); + if (groupMember == null) { + continue; + } + List memberTargetedRules = instanceBootGroupReadinessRuleDao.listEnabledByItem(groupMember.getBootGroupId(), + InstanceBootGroupMember.MemberType.InstanceGroup, mapping.getGroupId()).stream() + .filter(rule -> rule.getRuleType().isMemberTargeted()) + .collect(Collectors.toList()); + invalidateRuleResults(memberTargetedRules, vmId); + } + } + + private void invalidateRuleResults(List rules, long cacheVmId) { + for (InstanceBootGroupReadinessRuleVO rule : rules) { + instanceBootGroupReadinessCheckResultDao.upsert(rule.getId(), cacheVmId, InstanceBootGroupReadinessRule.Status.Unknown, + "Instance (re)started; not yet re-verified this session", new Date()); + } + } + + /** + * A VM inherits its owning InstanceGroup's Ping/PortCheck/GuestAgentLiveness rules (not + * MemberQuorum/CustomScript, which only ever make sense at group scope) — resolved by finding + * the InstanceGroup, among any this VM belongs to, that is itself a member of this boot group. + */ + private List findInheritedGroupRuleVOs(long bootGroupId, long vmId) { + for (InstanceGroupVMMapVO mapping : instanceGroupVMMapDao.listByInstanceId(vmId)) { + InstanceBootGroupMemberVO groupMember = instanceBootGroupMemberDao.findByMember(InstanceBootGroupMember.MemberType.InstanceGroup, mapping.getGroupId()); + if (groupMember != null && groupMember.getBootGroupId() == bootGroupId) { + return instanceBootGroupReadinessRuleDao.listEnabledByItem(bootGroupId, InstanceBootGroupMember.MemberType.InstanceGroup, mapping.getGroupId()).stream() + .filter(rule -> rule.getRuleType().isMemberTargeted()) + .collect(Collectors.toList()); + } + } + return Collections.emptyList(); + } + + /** + * AND of the group's own rules and every member's cached readiness (read-only — the per-VM loop + * already dispatched this poll). A MemberQuorum rule's own tolerance-aware verdict decides + * Ready/Error on its own; without one, a member still mid-retry only counts as NotReady, never Error. + * Once a MemberQuorum rule governs the group, every other member-targeted rule's own all-members + * aggregate becomes informational only — it's still evaluated and shown, but no longer gates the + * overall verdict, since that's exactly what attaching a quorum rule is meant to relax. + */ + @Override + public InstanceBootGroupReadinessRule.Status evaluateInstanceGroupReadiness(long bootGroupId, long instanceGroupId, Set permanentlyFailedVmIds) { + List groupRules = instanceBootGroupReadinessRuleDao.listEnabledByItem(bootGroupId, InstanceBootGroupMember.MemberType.InstanceGroup, instanceGroupId); + List members = instanceGroupVMMapDao.listByGroupId(instanceGroupId); + boolean hasMemberQuorumRule = groupRules.stream().anyMatch(rule -> rule.getRuleType() == InstanceBootGroupReadinessRule.RuleType.MemberQuorum); + + logger.debug("Evaluating readiness of instance group id {} for boot group id {}: {} member VM(s), {} own rule(s), quorum-governed={}", + () -> instanceGroupDao.findById(instanceGroupId), () -> bootGroupId, () -> members.size(), () -> groupRules.size(), () -> hasMemberQuorumRule); + + boolean anyError = false; + boolean anyNotReady = false; + + for (InstanceGroupVMMapVO member : members) { + InstanceBootGroupReadinessRule.Status vmStatus = getCachedVmReadiness(bootGroupId, member.getInstanceId()); + if (hasMemberQuorumRule || vmStatus == InstanceBootGroupReadinessRule.Status.Ready) { + continue; + } + if (permanentlyFailedVmIds.contains(member.getInstanceId())) { + anyError = true; + } else { + anyNotReady = true; + } + } + + for (InstanceBootGroupReadinessRuleVO rule : groupRules) { + ReadinessChecker.Result result; + boolean memberTargeted = rule.getRuleType().isMemberTargeted(); + if (rule.getRuleType() == InstanceBootGroupReadinessRule.RuleType.MemberQuorum) { + logger.debug("Evaluating group-scoped rule {} for instance group id {} via member quorum", rule, instanceGroupId); + Map details = instanceBootGroupReadinessRuleDetailsDao.getDetails(rule.getId()); + result = evaluateInstanceQuorum(bootGroupId, instanceGroupId, details, permanentlyFailedVmIds); + } else if (memberTargeted) { + logger.debug("Evaluating group-scoped rule {} for instance group id {} by aggregating its {} member(s)' own cached results", rule, instanceGroupId, members.size()); + result = aggregateMemberTargetedGroupRule(rule, members); + } else { + result = new ReadinessChecker.Result(InstanceBootGroupReadinessRule.Status.Error, + "No evaluator implemented yet for rule type " + rule.getRuleType()); + } + logger.debug("Group-scoped rule {} evaluated for instance group id {}: status={}, message={}", rule, instanceGroupId, result.getStatus(), result.getMessage()); + instanceBootGroupReadinessCheckResultDao.upsert(rule.getId(), 0, result.getStatus(), result.getMessage(), new Date()); + + if (memberTargeted && hasMemberQuorumRule) { + continue; + } + if (result.getStatus() == InstanceBootGroupReadinessRule.Status.Error) { + anyError = true; + } else if (result.getStatus() != InstanceBootGroupReadinessRule.Status.Ready) { + anyNotReady = true; + } + } + + InstanceBootGroupReadinessRule.Status overallStatus = anyError ? InstanceBootGroupReadinessRule.Status.Error + : (anyNotReady ? InstanceBootGroupReadinessRule.Status.NotReady : InstanceBootGroupReadinessRule.Status.Ready); + logger.debug("Instance group id {} readiness for boot group id {} evaluated as {}", instanceGroupId, bootGroupId, overallStatus); + return overallStatus; + } + + /** + * Reads the per-member cached results {@link #evaluateVmReadiness} already wrote for this rule, + * rather than dispatching it again. + */ + private ReadinessChecker.Result aggregateMemberTargetedGroupRule(InstanceBootGroupReadinessRuleVO rule, List members) { + if (members.isEmpty()) { + return new ReadinessChecker.Result(InstanceBootGroupReadinessRule.Status.NotReady, "Instance group has no members"); + } + boolean anyError = false; + int readyCount = 0; + for (InstanceGroupVMMapVO member : members) { + InstanceBootGroupReadinessCheckResultVO cached = instanceBootGroupReadinessCheckResultDao.findByRuleAndVm(rule.getId(), member.getInstanceId()); + InstanceBootGroupReadinessRule.Status status = (cached != null && cached.getStatus() != null) ? cached.getStatus() : InstanceBootGroupReadinessRule.Status.Unknown; + if (status == InstanceBootGroupReadinessRule.Status.Ready) { + readyCount++; + } else if (status == InstanceBootGroupReadinessRule.Status.Error) { + anyError = true; + } + } + int total = members.size(); + String message = String.format("%d of %d member(s) ready via %s", readyCount, total, rule.getRuleType().name()); + if (anyError) { + return new ReadinessChecker.Result(InstanceBootGroupReadinessRule.Status.Error, message); + } + return new ReadinessChecker.Result(readyCount == total ? InstanceBootGroupReadinessRule.Status.Ready : InstanceBootGroupReadinessRule.Status.NotReady, message); + } + + /** + * Pure computation, no dispatch: counts members currently READY against the configured threshold. + * @param permanentlyFailedVmIds excluded from "achievable" so a hopeless quorum reports Error + * instead of NotReady once it can never be met, even with every remaining member succeeding. + */ + private ReadinessChecker.Result evaluateInstanceQuorum(long bootGroupId, long instanceGroupId, Map details, Set permanentlyFailedVmIds) { + List members = instanceGroupVMMapDao.listByGroupId(instanceGroupId); + int total = members.size(); + if (total == 0) { + return new ReadinessChecker.Result(InstanceBootGroupReadinessRule.Status.NotReady, "Instance group has no members"); + } + + long readyCount = members.stream() + .filter(member -> getCachedVmReadiness(bootGroupId, member.getInstanceId()) == InstanceBootGroupReadinessRule.Status.Ready) + .count(); + long permanentlyFailedCount = members.stream() + .filter(member -> permanentlyFailedVmIds.contains(member.getInstanceId())) + .count(); + long achievableCount = total - permanentlyFailedCount; + + String thresholdType = details == null ? null : details.get(THRESHOLD_TYPE_KEY); + String thresholdValue = details == null ? null : details.get(THRESHOLD_VALUE_KEY); + + boolean met; + boolean achievable; + try { + if ("PERCENTAGE".equalsIgnoreCase(thresholdType)) { + double thresholdPercentage = Double.parseDouble(thresholdValue); + met = (readyCount * 100.0 / total) >= thresholdPercentage; + achievable = (achievableCount * 100.0 / total) >= thresholdPercentage; + } else { + long thresholdCount = Long.parseLong(thresholdValue); + met = readyCount >= thresholdCount; + achievable = achievableCount >= thresholdCount; + } + } catch (NumberFormatException e) { + return new ReadinessChecker.Result(InstanceBootGroupReadinessRule.Status.Error, "Invalid threshold configuration: " + thresholdType + "=" + thresholdValue); + } + + String message = String.format("%d/%d members ready (%s threshold %s)", readyCount, total, thresholdType, thresholdValue); + if (!met && !achievable) { + String reason = String.format("%s; unreachable — %d/%d member(s) have permanently failed readiness", message, permanentlyFailedCount, total); + logger.debug("Instance group id {} quorum check: {}", instanceGroupId, reason); + return new ReadinessChecker.Result(InstanceBootGroupReadinessRule.Status.Error, reason); + } + logger.debug("Instance group id {} quorum check: {}, met={}", instanceGroupId, message, met); + return new ReadinessChecker.Result(met ? InstanceBootGroupReadinessRule.Status.Ready : InstanceBootGroupReadinessRule.Status.NotReady, message); + } + + private void validateRuleTypeSpecificDetails(InstanceBootGroupReadinessRule.RuleType ruleType, Map details) { + if (ruleType == InstanceBootGroupReadinessRule.RuleType.MemberQuorum) { + validateInstanceQuorumDetails(details); + } else if (ruleType == InstanceBootGroupReadinessRule.RuleType.PortCheck) { + validatePortCheckDetails(details); + } + } + + private void validateInstanceQuorumDetails(Map details) { + String thresholdType = details == null ? null : details.get(THRESHOLD_TYPE_KEY); + String thresholdValue = details == null ? null : details.get(THRESHOLD_VALUE_KEY); + if (StringUtils.isBlank(thresholdType) || StringUtils.isBlank(thresholdValue)) { + throw new InvalidParameterValueException(String.format("%s rules require '%s' (COUNT or PERCENTAGE) and '%s' details", InstanceBootGroupReadinessRule.RuleType.MemberQuorum.name(), THRESHOLD_TYPE_KEY, THRESHOLD_VALUE_KEY)); + } + if (!"COUNT".equalsIgnoreCase(thresholdType) && !"PERCENTAGE".equalsIgnoreCase(thresholdType)) { + throw new InvalidParameterValueException(THRESHOLD_TYPE_KEY + " must be COUNT or PERCENTAGE"); + } + try { + if ("PERCENTAGE".equalsIgnoreCase(thresholdType)) { + Double.parseDouble(thresholdValue); + } else { + Long.parseLong(thresholdValue); + } + } catch (NumberFormatException e) { + throw new InvalidParameterValueException("Invalid " + THRESHOLD_VALUE_KEY + ": " + thresholdValue); + } + } + + private void validatePortCheckDetails(Map details) { + String protocol = details == null ? null : details.get(PROTOCOL_KEY); + if (StringUtils.isNotBlank(protocol) && !"tcp".equalsIgnoreCase(protocol)) { + throw new InvalidParameterValueException(InstanceBootGroupReadinessRule.RuleType.PortCheck.name() + " rules only support the tcp protocol currently"); + } + String port = details == null ? null : details.get(PORT_KEY); + if (StringUtils.isBlank(port)) { + throw new InvalidParameterValueException(InstanceBootGroupReadinessRule.RuleType.PortCheck.name() + " rules require a '" + PORT_KEY + "' detail"); + } + try { + int portValue = Integer.parseInt(port); + if (portValue < 1 || portValue > 65535) { + throw new NumberFormatException(); + } + } catch (NumberFormatException e) { + throw new InvalidParameterValueException("Invalid " + PORT_KEY + ": " + port); + } + } + + private void validateSingletonRuleType(long bootGroupId, InstanceBootGroupMember.MemberType itemType, long itemId, InstanceBootGroupReadinessRule.RuleType ruleType) { + if (!SINGLETON_RULE_TYPES.contains(ruleType)) { + return; + } + boolean alreadyExists = instanceBootGroupReadinessRuleDao.listByItem(bootGroupId, itemType, itemId).stream() + .anyMatch(rule -> rule.getRuleType() == ruleType); + if (alreadyExists) { + throw new InvalidParameterValueException(String.format("A %s rule already exists for this %s", ruleType.name(), itemType.name())); + } + } + + /** + * GuestAgentLiveness is KVM/libvirt-specific — reject it up front on other hypervisors. Only + * checked for a direct VM-scoped rule; a group-scoped rule can't be validated once at creation + * since membership is dynamic, so a non-KVM member just evaluates to Error per-member instead. + */ + private void validateGuestAgentLivenessSupported(InstanceBootGroupMember.MemberType itemType, long vmId, InstanceBootGroupReadinessRule.RuleType ruleType) { + if (ruleType != InstanceBootGroupReadinessRule.RuleType.GuestAgentLiveness || itemType != InstanceBootGroupMember.MemberType.VirtualMachine) { + return; + } + UserVmVO vm = userVmDao.findById(vmId); + if (vm == null) { + throw new InvalidParameterValueException("Unable to find an Instance with ID: " + vmId); + } + if (vm.getHypervisorType() != HypervisorType.KVM) { + throw new InvalidParameterValueException(String.format( + "%s rules are only supported on KVM Instances; this Instance's hypervisor is %s", ruleType.name(), vm.getHypervisorType())); + } + } + + private void validateRuleTypeForItemType(InstanceBootGroupMember.MemberType itemType, InstanceBootGroupReadinessRule.RuleType ruleType) { + if (!VALID_RULE_TYPES_BY_ITEM_TYPE.getOrDefault(itemType, Collections.emptySet()).contains(ruleType)) { + throw new InvalidParameterValueException(String.format("Rule type %s is not valid for item type %s", ruleType, itemType)); + } + } + + private void validateItemBelongsToBootGroup(long bootGroupId, InstanceBootGroupMember.MemberType itemType, long itemId) { + if (itemType == InstanceBootGroupMember.MemberType.InstanceGroup) { + InstanceBootGroupMemberVO member = instanceBootGroupMemberDao.findByMember(InstanceBootGroupMember.MemberType.InstanceGroup, itemId); + if (member == null || member.getBootGroupId() != bootGroupId) { + throw new InvalidParameterValueException(String.format("Instance group %d is not a member of boot group %d", itemId, bootGroupId)); + } + return; + } + + InstanceBootGroupMemberVO directMember = instanceBootGroupMemberDao.findByMember(InstanceBootGroupMember.MemberType.VirtualMachine, itemId); + if (directMember != null && directMember.getBootGroupId() == bootGroupId) { + return; + } + + List mappings = instanceGroupVMMapDao.listByInstanceId(itemId); + for (InstanceGroupVMMapVO mapping : mappings) { + InstanceBootGroupMemberVO groupMember = instanceBootGroupMemberDao.findByMember(InstanceBootGroupMember.MemberType.InstanceGroup, mapping.getGroupId()); + if (groupMember != null && groupMember.getBootGroupId() == bootGroupId) { + return; + } + } + + throw new InvalidParameterValueException(String.format( + "VM %d is not part of boot group %d, neither directly nor via its instance group", itemId, bootGroupId)); + } +} diff --git a/server/src/main/java/org/apache/cloudstack/vm/bootgroup/readiness/InstanceBootGroupReadinessRuleService.java b/server/src/main/java/org/apache/cloudstack/vm/bootgroup/readiness/InstanceBootGroupReadinessRuleService.java new file mode 100644 index 000000000000..8314b9a7eb1e --- /dev/null +++ b/server/src/main/java/org/apache/cloudstack/vm/bootgroup/readiness/InstanceBootGroupReadinessRuleService.java @@ -0,0 +1,75 @@ +// 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.vm.bootgroup.readiness; + +import java.util.List; +import java.util.Map; +import java.util.Set; + +import org.apache.cloudstack.vm.bootgroup.InstanceBootGroupMember; + +/** + * Backend counterpart consumed by {@code InstanceBootGroupApiServiceImpl} for mutating readiness-rule + * operations and evaluation. Listing goes straight through the DAO from the API layer, same as + * {@code InstanceBootGroupMember} listing does — this interface only covers create/update/delete and + * evaluation, which need the domain validation in {@code InstanceBootGroupReadinessRuleManagerImpl}. + */ +public interface InstanceBootGroupReadinessRuleService { + + InstanceBootGroupReadinessRule createReadinessRule(long bootGroupId, InstanceBootGroupMember.MemberType itemType, long itemId, + InstanceBootGroupReadinessRule.RuleType ruleType, String name, boolean enabled, Map details); + + InstanceBootGroupReadinessRule updateReadinessRule(long ruleId, String name, Boolean enabled, Map details); + + boolean deleteReadinessRule(long ruleId); + + InstanceBootGroupReadinessRule findById(long ruleId); + + Map getRuleDetails(long ruleId); + + /** + * AND across all enabled rules for this VM within the boot group; Ready if none are attached + * and the VM is Running. + * @param remainingMs dispatch time budget left for this VM's current attempt; pass a generous + * value (e.g. {@code Long.MAX_VALUE}) outside budget-tracking contexts. + * @param attemptLabel appended to each persisted rule message (e.g. {@code "2/5"}); pass + * {@code null} to skip. + */ + InstanceBootGroupReadinessRule.Status evaluateVmReadiness(long bootGroupId, long vmId, long remainingMs, String attemptLabel); + + /** + * AND of the group's own enabled rules (e.g. MemberQuorum) and every member VM's own readiness. + * @param permanentlyFailedVmIds members the caller has given up retrying — lets a MemberQuorum + * rule distinguish "not met yet" from "mathematically impossible"; pass an empty set + * outside that orchestration context. + */ + InstanceBootGroupReadinessRule.Status evaluateInstanceGroupReadiness(long bootGroupId, long instanceGroupId, Set permanentlyFailedVmIds); + + /** + * The Ping/PortCheck/GuestAgentLiveness rules {@code vmId} inherits from its owning + * InstanceGroup, if any — empty if none apply. + */ + List findInheritedGroupRules(long bootGroupId, long vmId); + + /** + * Resets {@code vmId}'s own cached rule results (direct and inherited) to Unknown — called when + * the VM starts, so a restart outside boot group orchestration can't keep reporting a stale Ready + * from before it stopped. A no-op if the VM has no boot-group involvement. + */ + void invalidateCachedReadinessOnRestart(long vmId); +} diff --git a/server/src/main/java/org/apache/cloudstack/vm/bootgroup/readiness/PortCheckChecker.java b/server/src/main/java/org/apache/cloudstack/vm/bootgroup/readiness/PortCheckChecker.java new file mode 100644 index 000000000000..ddedcec9ccd5 --- /dev/null +++ b/server/src/main/java/org/apache/cloudstack/vm/bootgroup/readiness/PortCheckChecker.java @@ -0,0 +1,168 @@ +// 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.vm.bootgroup.readiness; + +import java.util.List; +import java.util.Map; + +import javax.inject.Inject; + +import org.apache.cloudstack.engine.orchestration.service.NetworkOrchestrationService; +import org.apache.commons.lang3.StringUtils; +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; +import org.springframework.stereotype.Component; + +import com.cloud.agent.AgentManager; +import com.cloud.agent.api.Answer; +import com.cloud.agent.api.routing.NetworkElementCommand; +import com.cloud.network.Network; +import com.cloud.network.dao.NetworkDao; +import com.cloud.network.dao.NetworkVO; +import com.cloud.network.router.VirtualRouter; +import com.cloud.network.router.VpcVirtualNetworkApplianceManager; +import com.cloud.vm.NicVO; +import com.cloud.vm.UserVmVO; +import com.cloud.vm.VirtualMachine; +import com.cloud.vm.VirtualMachineManager; +import com.cloud.vm.dao.NicDao; +import com.cloud.vm.dao.UserVmDao; + +/** + * PortCheck: dispatches a dedicated {@link InstanceReadinessCheckCommand} to the VR of + * the VM's default network (same transport {@link VrPingChecker} uses), asking it to attempt a TCP + * connect to the VM's default IP on the configured port. Only TCP is currently supported. + */ +@Component +public class PortCheckChecker implements ReadinessChecker { + protected static Logger LOGGER = LogManager.getLogger(PortCheckChecker.class); + + private static final String PORT_KEY = "port"; + private static final String PROTOCOL_KEY = "protocol"; + + @Inject + private UserVmDao userVmDao; + + @Inject + private NicDao nicDao; + + @Inject + private NetworkDao networkDao; + + // VirtualNetworkApplianceManager itself is ambiguous (VirtualNetworkApplianceManagerImpl and + // VpcVirtualNetworkApplianceManagerImpl both implement it) — inject the more specific + // sub-interface, matching the convention used by VirtualRouterElement/VpcVirtualRouterElement. + @Inject + private VpcVirtualNetworkApplianceManager virtualNetworkApplianceManager; + + @Inject + private VirtualMachineManager virtualMachineManager; + + @Inject + private NetworkOrchestrationService networkOrchestrationService; + + @Inject + private AgentManager agentManager; + + @Override + public InstanceBootGroupReadinessRule.RuleType getRuleType() { + return InstanceBootGroupReadinessRule.RuleType.PortCheck; + } + + @Override + public Logger getLogger() { + return LOGGER; + } + + @Override + public Result check(InstanceBootGroupReadinessRule rule, Map details, long vmId, long remainingMs) { + UserVmVO vm = userVmDao.findById(vmId); + if (vm == null) { + return logAndReturn(rule, vmId, new Result(InstanceBootGroupReadinessRule.Status.Error, "VM not found")); + } + + LOGGER.debug("Checking port readiness for {} due to rule {}", vm, rule); + + String protocol = details == null ? null : details.get(PROTOCOL_KEY); + if (StringUtils.isNotBlank(protocol) && !"tcp".equalsIgnoreCase(protocol)) { + return logAndReturn(rule, vm, new Result(InstanceBootGroupReadinessRule.Status.Error, "Only tcp port checks are supported, got: " + protocol)); + } + + String portValue = details == null ? null : details.get(PORT_KEY); + int port; + try { + port = Integer.parseInt(portValue); + if (port < 1 || port > 65535) { + throw new NumberFormatException(); + } + } catch (NumberFormatException e) { + return logAndReturn(rule, vm, new Result(InstanceBootGroupReadinessRule.Status.Error, "Invalid or missing port detail: " + portValue)); + } + + NicVO nic = nicDao.findDefaultNicForVM(vmId); + if (nic == null || StringUtils.isEmpty(nic.getIPv4Address())) { + return logAndReturn(rule, vm, new Result(InstanceBootGroupReadinessRule.Status.Error, "VM has no default NIC/IPv4 address yet")); + } + + NetworkVO network = networkDao.findById(nic.getNetworkId()); + if (network != null && Network.GuestType.L2.equals(network.getGuestType())) { + return logAndReturn(rule, vm, new Result(InstanceBootGroupReadinessRule.Status.Error, "The VM's default network is an L2 network; there is no VR to check the port from")); + } + + List routers = virtualNetworkApplianceManager.getRoutersForNetwork(nic.getNetworkId()); + VirtualRouter router = routers.stream() + .filter(r -> r.getState() == VirtualMachine.State.Running) + .findFirst() + .orElse(null); + if (router == null || router.getHostId() == null) { + return logAndReturn(rule, vm, new Result(InstanceBootGroupReadinessRule.Status.Error, "No running VR found for the VM's default network")); + } + + InstanceReadinessCheckCommand command = new InstanceReadinessCheckCommand(nic.getIPv4Address(), port, + virtualMachineManager.getExecuteInSequence(router.getHypervisorType())); + Map accessDetails = networkOrchestrationService.getSystemVMAccessDetails(router); + if (StringUtils.isEmpty(accessDetails.get(NetworkElementCommand.ROUTER_IP))) { + return logAndReturn(rule, vm, new Result(InstanceBootGroupReadinessRule.Status.Error, "Unable to determine the VR's control IP")); + } + command.setAccessDetail(accessDetails); + + if (remainingMs < MIN_REMAINING_MS_TO_DISPATCH) { + return logAndReturn(rule, vm, new Result(InstanceBootGroupReadinessRule.Status.Error, + "Insufficient time remaining in this attempt's budget (" + remainingMs + "ms) to dispatch a port check")); + } + command.setWait(computeWaitSeconds(remainingMs)); + + LOGGER.debug("Dispatching port check of {}:{} via {} with {}ms remaining budget", nic.getIPv4Address(), port, router, remainingMs); + Answer answer; + try { + answer = agentManager.easySend(router.getHostId(), command); + } catch (Exception e) { + return logAndReturn(rule, vm, new Result(InstanceBootGroupReadinessRule.Status.Error, "Failed to dispatch port check via VR: " + e.getMessage())); + } + if (answer == null) { + return logAndReturn(rule, vm, new Result(InstanceBootGroupReadinessRule.Status.Error, "No answer from the VR's host")); + } + + Map executionDetails = ((InstanceReadinessCheckAnswer) answer).getExecutionDetails(); + String exitCode = executionDetails.get(InstanceReadinessCheckAnswer.EXITCODE); + if ("0".equals(exitCode)) { + return logAndReturn(rule, vm, new Result(InstanceBootGroupReadinessRule.Status.Ready, "port " + port + "/tcp is open")); + } + return logAndReturn(rule, vm, new Result(InstanceBootGroupReadinessRule.Status.Error, "port " + port + "/tcp check failed: " + executionDetails.get(InstanceReadinessCheckAnswer.STDERR))); + } +} diff --git a/server/src/main/java/org/apache/cloudstack/vm/bootgroup/readiness/VrPingChecker.java b/server/src/main/java/org/apache/cloudstack/vm/bootgroup/readiness/VrPingChecker.java new file mode 100644 index 000000000000..9d4c4da48a55 --- /dev/null +++ b/server/src/main/java/org/apache/cloudstack/vm/bootgroup/readiness/VrPingChecker.java @@ -0,0 +1,149 @@ +// 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.vm.bootgroup.readiness; + +import java.util.List; +import java.util.Map; + +import javax.inject.Inject; + +import org.apache.cloudstack.engine.orchestration.service.NetworkOrchestrationService; +import org.apache.commons.lang3.StringUtils; +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; +import org.springframework.stereotype.Component; + +import com.cloud.agent.AgentManager; +import com.cloud.agent.api.Answer; +import com.cloud.agent.api.routing.NetworkElementCommand; +import com.cloud.network.Network; +import com.cloud.network.dao.NetworkDao; +import com.cloud.network.dao.NetworkVO; +import com.cloud.network.router.VirtualRouter; +import com.cloud.network.router.VpcVirtualNetworkApplianceManager; +import com.cloud.vm.NicVO; +import com.cloud.vm.UserVmVO; +import com.cloud.vm.VirtualMachine; +import com.cloud.vm.VirtualMachineManager; +import com.cloud.vm.dao.NicDao; +import com.cloud.vm.dao.UserVmDao; + +/** + * PING: dispatches a dedicated {@link InstanceReadinessCheckCommand} to the VR of the VM's + * default network. Deliberately does not reuse {@code DiagnosticsCommand}/{@code DiagnosticsType} — + * that plumbing is a general-purpose admin tool scoped to system VMs, not user instances. + */ +@Component +public class VrPingChecker implements ReadinessChecker { + protected static Logger LOGGER = LogManager.getLogger(VrPingChecker.class); + + @Inject + private UserVmDao userVmDao; + + @Inject + private NicDao nicDao; + + @Inject + private NetworkDao networkDao; + + // VirtualNetworkApplianceManager itself is ambiguous (VirtualNetworkApplianceManagerImpl and + // VpcVirtualNetworkApplianceManagerImpl both implement it) — inject the more specific + // sub-interface, matching the convention used by VirtualRouterElement/VpcVirtualRouterElement. + @Inject + private VpcVirtualNetworkApplianceManager virtualNetworkApplianceManager; + + @Inject + private VirtualMachineManager virtualMachineManager; + + @Inject + private NetworkOrchestrationService networkOrchestrationService; + + @Inject + private AgentManager agentManager; + + @Override + public InstanceBootGroupReadinessRule.RuleType getRuleType() { + return InstanceBootGroupReadinessRule.RuleType.Ping; + } + + @Override + public Logger getLogger() { + return LOGGER; + } + + @Override + public Result check(InstanceBootGroupReadinessRule rule, Map details, long vmId, long remainingMs) { + UserVmVO vm = userVmDao.findById(vmId); + if (vm == null) { + return logAndReturn(rule, vmId, new Result(InstanceBootGroupReadinessRule.Status.Error, "VM not found")); + } + + LOGGER.debug("Checking ping readiness for {} due to rule {}", vm, rule); + + NicVO nic = nicDao.findDefaultNicForVM(vmId); + if (nic == null || StringUtils.isEmpty(nic.getIPv4Address())) { + return logAndReturn(rule, vm, new Result(InstanceBootGroupReadinessRule.Status.Error, "VM has no default NIC/IPv4 address yet")); + } + + NetworkVO network = networkDao.findById(nic.getNetworkId()); + if (network != null && Network.GuestType.L2.equals(network.getGuestType())) { + return logAndReturn(rule, vm, new Result(InstanceBootGroupReadinessRule.Status.Error, "The VM's default network is an L2 network; there is no VR to ping from")); + } + + List routers = virtualNetworkApplianceManager.getRoutersForNetwork(nic.getNetworkId()); + VirtualRouter router = routers.stream() + .filter(r -> r.getState() == VirtualMachine.State.Running) + .findFirst() + .orElse(null); + if (router == null || router.getHostId() == null) { + return logAndReturn(rule, vm, new Result(InstanceBootGroupReadinessRule.Status.Error, "No running VR found for the VM's default network")); + } + + InstanceReadinessCheckCommand command = new InstanceReadinessCheckCommand(nic.getIPv4Address(), + virtualMachineManager.getExecuteInSequence(router.getHypervisorType())); + Map accessDetails = networkOrchestrationService.getSystemVMAccessDetails(router); + if (StringUtils.isEmpty(accessDetails.get(NetworkElementCommand.ROUTER_IP))) { + return logAndReturn(rule, vm, new Result(InstanceBootGroupReadinessRule.Status.Error, "Unable to determine the VR's control IP")); + } + command.setAccessDetail(accessDetails); + + if (remainingMs < MIN_REMAINING_MS_TO_DISPATCH) { + return logAndReturn(rule, vm, new Result(InstanceBootGroupReadinessRule.Status.Error, + "Insufficient time remaining in this attempt's budget (" + remainingMs + "ms) to dispatch a ping check")); + } + command.setWait(computeWaitSeconds(remainingMs)); + + LOGGER.debug("Dispatching ping of {} to {} via {} with {}ms remaining budget", nic.getIPv4Address(), router, router.getHostId(), remainingMs); + Answer answer; + try { + answer = agentManager.easySend(router.getHostId(), command); + } catch (Exception e) { + return logAndReturn(rule, vm, new Result(InstanceBootGroupReadinessRule.Status.Error, "Failed to dispatch ping via VR: " + e.getMessage())); + } + if (answer == null) { + return logAndReturn(rule, vm, new Result(InstanceBootGroupReadinessRule.Status.Error, "No answer from the VR's host")); + } + + Map executionDetails = ((InstanceReadinessCheckAnswer) answer).getExecutionDetails(); + String exitCode = executionDetails.get(InstanceReadinessCheckAnswer.EXITCODE); + if ("0".equals(exitCode)) { + return logAndReturn(rule, vm, new Result(InstanceBootGroupReadinessRule.Status.Ready, "ping succeeded")); + } + return logAndReturn(rule, vm, new Result(InstanceBootGroupReadinessRule.Status.Error, "ping failed: " + executionDetails.get(InstanceReadinessCheckAnswer.STDERR))); + } +} diff --git a/server/src/main/resources/META-INF/cloudstack/core/spring-server-core-managers-context.xml b/server/src/main/resources/META-INF/cloudstack/core/spring-server-core-managers-context.xml index c0bcba44c642..61b5b541bc75 100644 --- a/server/src/main/resources/META-INF/cloudstack/core/spring-server-core-managers-context.xml +++ b/server/src/main/resources/META-INF/cloudstack/core/spring-server-core-managers-context.xml @@ -423,4 +423,16 @@ + + + + + + + + + + + + diff --git a/server/src/test/java/org/apache/cloudstack/vm/bootgroup/InstanceBootGroupApiServiceImplTest.java b/server/src/test/java/org/apache/cloudstack/vm/bootgroup/InstanceBootGroupApiServiceImplTest.java new file mode 100644 index 000000000000..ec6b100f1ead --- /dev/null +++ b/server/src/test/java/org/apache/cloudstack/vm/bootgroup/InstanceBootGroupApiServiceImplTest.java @@ -0,0 +1,1212 @@ +// 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.vm.bootgroup; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertTrue; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.anyLong; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.Mockito.doNothing; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.never; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collections; +import java.util.Date; +import java.util.List; + +import org.apache.cloudstack.api.command.user.bootgroup.AddMemberToInstanceBootGroupCmd; +import org.apache.cloudstack.api.command.user.bootgroup.CreateInstanceBootGroupCmd; +import org.apache.cloudstack.api.command.user.bootgroup.CreateInstanceBootGroupReadinessRuleCmd; +import org.apache.cloudstack.api.command.user.bootgroup.DeleteInstanceBootGroupCmd; +import org.apache.cloudstack.api.command.user.bootgroup.DeleteInstanceBootGroupReadinessRuleCmd; +import org.apache.cloudstack.api.command.user.bootgroup.ListInstanceBootGroupMembersCmd; +import org.apache.cloudstack.api.command.user.bootgroup.ListInstanceBootGroupReadinessRulesCmd; +import org.apache.cloudstack.api.command.user.bootgroup.RebootInstanceBootGroupCmd; +import org.apache.cloudstack.api.command.user.bootgroup.RemoveInstanceBootGroupMemberCmd; +import org.apache.cloudstack.api.command.user.bootgroup.StartInstanceBootGroupCmd; +import org.apache.cloudstack.api.command.user.bootgroup.StopInstanceBootGroupCmd; +import org.apache.cloudstack.api.command.user.bootgroup.UpdateInstanceBootGroupCmd; +import org.apache.cloudstack.api.command.user.bootgroup.UpdateInstanceBootGroupMemberCmd; +import org.apache.cloudstack.api.command.user.bootgroup.UpdateInstanceBootGroupReadinessRuleCmd; +import org.apache.cloudstack.api.query.dao.InstanceBootGroupJoinDao; +import org.apache.cloudstack.api.response.InstanceBootGroupMemberChildResponse; +import org.apache.cloudstack.api.response.InstanceBootGroupMemberResponse; +import org.apache.cloudstack.api.response.InstanceBootGroupReadinessRuleResponse; +import org.apache.cloudstack.api.response.ListResponse; +import org.apache.cloudstack.context.CallContext; +import org.apache.cloudstack.vm.bootgroup.readiness.InstanceBootGroupReadinessRule; +import org.apache.cloudstack.vm.bootgroup.readiness.InstanceBootGroupReadinessRuleService; +import org.junit.After; +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.ArgumentCaptor; +import org.mockito.InjectMocks; +import org.mockito.Mock; +import org.mockito.MockedStatic; +import org.mockito.Mockito; +import org.mockito.junit.MockitoJUnitRunner; +import org.springframework.test.util.ReflectionTestUtils; + +import com.cloud.exception.InvalidParameterValueException; +import com.cloud.exception.PermissionDeniedException; +import com.cloud.hypervisor.Hypervisor.HypervisorType; +import com.cloud.user.Account; +import com.cloud.user.AccountManager; +import com.cloud.utils.db.Transaction; +import com.cloud.utils.db.TransactionCallback; +import com.cloud.vm.InstanceGroupVMMapVO; +import com.cloud.vm.InstanceGroupVO; +import com.cloud.vm.UserVmVO; +import com.cloud.vm.VirtualMachine; +import com.cloud.vm.dao.InstanceBootGroupDao; +import com.cloud.vm.dao.InstanceBootGroupDetailsDao; +import com.cloud.vm.dao.InstanceBootGroupMemberDao; +import com.cloud.vm.dao.InstanceBootGroupReadinessCheckResultDao; +import com.cloud.vm.dao.InstanceBootGroupReadinessRuleDao; +import com.cloud.vm.dao.InstanceBootGroupReadinessRuleDetailsDao; +import com.cloud.vm.dao.InstanceGroupDao; +import com.cloud.vm.dao.InstanceGroupVMMapDao; +import com.cloud.vm.dao.UserVmDao; + +@RunWith(MockitoJUnitRunner.class) +public class InstanceBootGroupApiServiceImplTest { + + private static final long ACCOUNT_ID = 1L; + private static final long DOMAIN_ID = 10L; + private static final long GROUP_ID = 100L; + private static final long VM_ID = 200L; + private static final long VM2_ID = 201L; + private static final long INSTANCE_GROUP_ID = 300L; + private static final long MEMBER_ID = 400L; + private static final long RULE_ID = 500L; + + @InjectMocks + InstanceBootGroupApiServiceImpl service; + + @Mock + InstanceBootGroupDao instanceBootGroupDao; + @Mock + InstanceBootGroupJoinDao instanceBootGroupJoinDao; + @Mock + InstanceBootGroupMemberDao instanceBootGroupMemberDao; + @Mock + AccountManager accountManager; + @Mock + UserVmDao userVmDao; + @Mock + InstanceGroupDao instanceGroupDao; + @Mock + InstanceBootGroupManager instanceBootGroupManager; + @Mock + InstanceBootGroupMembershipGuard instanceBootGroupMembershipGuard; + @Mock + InstanceBootGroupReadinessRuleService instanceBootGroupReadinessRuleService; + @Mock + InstanceBootGroupReadinessRuleDao instanceBootGroupReadinessRuleDao; + @Mock + InstanceBootGroupReadinessRuleDetailsDao instanceBootGroupReadinessRuleDetailsDao; + @Mock + InstanceBootGroupReadinessCheckResultDao instanceBootGroupReadinessCheckResultDao; + @Mock + InstanceBootGroupDetailsDao instanceBootGroupDetailsDao; + @Mock + InstanceGroupVMMapDao instanceGroupVMMapDao; + + @Mock + Account callerMock; + + private MockedStatic callContextMocked; + private CallContext callContextMock; + + @Before + public void setUp() { + callContextMocked = Mockito.mockStatic(CallContext.class); + callContextMock = mock(CallContext.class); + callContextMocked.when(CallContext::current).thenReturn(callContextMock); + when(callContextMock.getCallingAccount()).thenReturn(callerMock); + Mockito.lenient().when(callerMock.getId()).thenReturn(ACCOUNT_ID); + Mockito.lenient().when(callerMock.getDomainId()).thenReturn(DOMAIN_ID); + + doNothing().when(accountManager).checkAccess(any(Account.class), any(), eq(true), any()); + } + + @After + public void tearDown() { + callContextMocked.close(); + } + + // ---------------------------------------------------------------- helpers + + private InstanceBootGroupVO newGroup(long id, String name, long accountId) { + InstanceBootGroupVO group = new InstanceBootGroupVO(name, "desc", accountId, DOMAIN_ID); + ReflectionTestUtils.setField(group, "id", id); + return group; + } + + private InstanceGroupVO newInstanceGroup(long id, String name, long accountId) { + InstanceGroupVO group = new InstanceGroupVO(name, accountId); + ReflectionTestUtils.setField(group, "id", id); + return group; + } + + private UserVmVO newVm(long id, String displayName, String hostName, long accountId, VirtualMachine.State state) { + UserVmVO vm = new UserVmVO(id, "i-1-" + id + "-VM", displayName, 1L, HypervisorType.KVM, 1L, false, false, + DOMAIN_ID, accountId, 1L, 1L, null, null, null, "i-1-" + id + "-VM"); + vm.setHostName(hostName); + vm.setState(state); + return vm; + } + + private InstanceBootGroupMemberVO newMember(long id, long bootGroupId, InstanceBootGroupMember.MemberType type, long memberId, int order) { + InstanceBootGroupMemberVO member = new InstanceBootGroupMemberVO(bootGroupId, type, memberId, order); + ReflectionTestUtils.setField(member, "id", id); + return member; + } + + private InstanceBootGroupReadinessRuleVO newRule(long id, long bootGroupId, InstanceBootGroupMember.MemberType itemType, long itemId, + InstanceBootGroupReadinessRule.RuleType ruleType, boolean enabled, String name) { + InstanceBootGroupReadinessRuleVO rule = new InstanceBootGroupReadinessRuleVO(name, bootGroupId, itemType, itemId, ruleType, enabled); + ReflectionTestUtils.setField(rule, "id", id); + return rule; + } + + private String field(Object response, String name) { + Object value = ReflectionTestUtils.getField(response, name); + return value == null ? null : value.toString(); + } + + // ---------------------------------------------------------------- createInstanceBootGroup + + @Test + public void testCreateInstanceBootGroupSuccessWithOverrides() { + CreateInstanceBootGroupCmd cmd = mock(CreateInstanceBootGroupCmd.class); + when(cmd.getName()).thenReturn("group1"); + when(cmd.getDescription()).thenReturn("desc"); + when(cmd.getAccountName()).thenReturn("acct"); + when(cmd.getDomainId()).thenReturn(DOMAIN_ID); + when(cmd.getProjectId()).thenReturn(null); + when(cmd.getReadinessAttemptTimeoutSeconds()).thenReturn(100L); + when(cmd.getReadinessMaxRetryAttempts()).thenReturn(-1L); + when(cmd.getReadinessRebootOnRetry()).thenReturn(true); + when(cmd.getReadinessInitialDelaySeconds()).thenReturn(null); + + Account owner = mock(Account.class); + when(owner.getId()).thenReturn(ACCOUNT_ID); + when(owner.getDomainId()).thenReturn(DOMAIN_ID); + when(accountManager.finalizeOwner(callerMock, "acct", DOMAIN_ID, null)).thenReturn(owner); + when(instanceBootGroupDao.isNameInUse(ACCOUNT_ID, "group1")).thenReturn(false); + when(instanceBootGroupDao.persist(any(InstanceBootGroupVO.class))).thenAnswer(inv -> { + InstanceBootGroupVO vo = inv.getArgument(0); + ReflectionTestUtils.setField(vo, "id", GROUP_ID); + return vo; + }); + + InstanceBootGroup result = service.createInstanceBootGroup(cmd); + + assertNotNull(result); + assertEquals(GROUP_ID, result.getId()); + verify(callContextMock).setEventResourceId(GROUP_ID); + verify(instanceBootGroupDetailsDao).setDetail(GROUP_ID, InstanceBootGroupManagerImpl.ReadinessAttemptTimeoutSeconds.key(), "100"); + verify(instanceBootGroupDetailsDao).setDetail(GROUP_ID, InstanceBootGroupManagerImpl.ReadinessMaxRetryAttempts.key(), null); + verify(instanceBootGroupDetailsDao).setDetail(GROUP_ID, InstanceBootGroupManagerImpl.ReadinessRebootOnRetry.key(), "true"); + verify(instanceBootGroupDetailsDao, never()).setDetail(eq(GROUP_ID), eq(InstanceBootGroupManagerImpl.ReadinessInitialDelaySeconds.key()), any()); + } + + @Test(expected = InvalidParameterValueException.class) + public void testCreateInstanceBootGroupNameInUseThrows() { + CreateInstanceBootGroupCmd cmd = mock(CreateInstanceBootGroupCmd.class); + when(cmd.getName()).thenReturn("group1"); + Account owner = mock(Account.class); + when(owner.getId()).thenReturn(ACCOUNT_ID); + when(accountManager.finalizeOwner(any(), any(), any(), any())).thenReturn(owner); + when(instanceBootGroupDao.isNameInUse(ACCOUNT_ID, "group1")).thenReturn(true); + + service.createInstanceBootGroup(cmd); + } + + // ---------------------------------------------------------------- deleteInstanceBootGroup + + @Test + public void testDeleteInstanceBootGroupSuccess() { + DeleteInstanceBootGroupCmd cmd = mock(DeleteInstanceBootGroupCmd.class); + when(cmd.getId()).thenReturn(GROUP_ID); + InstanceBootGroupVO group = newGroup(GROUP_ID, "group1", ACCOUNT_ID); + when(instanceBootGroupDao.findById(GROUP_ID)).thenReturn(group); + + try (MockedStatic transactionMock = Mockito.mockStatic(Transaction.class)) { + transactionMock.when(() -> Transaction.execute(any(TransactionCallback.class))).thenAnswer(invocation -> { + TransactionCallback callback = invocation.getArgument(0); + return callback.doInTransaction(null); + }); + + boolean result = service.deleteInstanceBootGroup(cmd); + + assertTrue(result); + verify(instanceBootGroupMemberDao).deleteByBootGroupId(GROUP_ID); + verify(instanceBootGroupDao).remove(GROUP_ID); + } + } + + @Test(expected = InvalidParameterValueException.class) + public void testDeleteInstanceBootGroupNotFound() { + DeleteInstanceBootGroupCmd cmd = mock(DeleteInstanceBootGroupCmd.class); + when(cmd.getId()).thenReturn(GROUP_ID); + when(instanceBootGroupDao.findById(GROUP_ID)).thenReturn(null); + + service.deleteInstanceBootGroup(cmd); + } + + // ---------------------------------------------------------------- updateInstanceBootGroup + + @Test + public void testUpdateInstanceBootGroupChangeNameSuccess() { + UpdateInstanceBootGroupCmd cmd = mock(UpdateInstanceBootGroupCmd.class); + when(cmd.getId()).thenReturn(GROUP_ID); + when(cmd.getName()).thenReturn("newname"); + when(cmd.getDescription()).thenReturn("newdesc"); + when(cmd.getReadinessAttemptTimeoutSeconds()).thenReturn(null); + when(cmd.getReadinessMaxRetryAttempts()).thenReturn(null); + when(cmd.getReadinessRebootOnRetry()).thenReturn(null); + when(cmd.getReadinessInitialDelaySeconds()).thenReturn(null); + + InstanceBootGroupVO group = newGroup(GROUP_ID, "oldname", ACCOUNT_ID); + when(instanceBootGroupDao.findById(GROUP_ID)).thenReturn(group); + Account owner = mock(Account.class); + when(owner.getId()).thenReturn(ACCOUNT_ID); + when(accountManager.getAccount(ACCOUNT_ID)).thenReturn(owner); + when(instanceBootGroupDao.isNameInUse(ACCOUNT_ID, "newname")).thenReturn(false); + + InstanceBootGroup result = service.updateInstanceBootGroup(cmd); + + assertNotNull(result); + assertEquals("newname", group.getName()); + assertEquals("newdesc", group.getDescription()); + verify(instanceBootGroupDao).update(GROUP_ID, group); + } + + @Test + public void testUpdateInstanceBootGroupSameNameSkipsUniquenessCheck() { + UpdateInstanceBootGroupCmd cmd = mock(UpdateInstanceBootGroupCmd.class); + when(cmd.getId()).thenReturn(GROUP_ID); + when(cmd.getName()).thenReturn("samename"); + InstanceBootGroupVO group = newGroup(GROUP_ID, "samename", ACCOUNT_ID); + when(instanceBootGroupDao.findById(GROUP_ID)).thenReturn(group); + + service.updateInstanceBootGroup(cmd); + + verify(instanceBootGroupDao, never()).isNameInUse(anyLong(), any()); + verify(accountManager, never()).getAccount(anyLong()); + } + + @Test(expected = InvalidParameterValueException.class) + public void testUpdateInstanceBootGroupNameInUseThrows() { + UpdateInstanceBootGroupCmd cmd = mock(UpdateInstanceBootGroupCmd.class); + when(cmd.getId()).thenReturn(GROUP_ID); + when(cmd.getName()).thenReturn("newname"); + InstanceBootGroupVO group = newGroup(GROUP_ID, "oldname", ACCOUNT_ID); + when(instanceBootGroupDao.findById(GROUP_ID)).thenReturn(group); + Account owner = mock(Account.class); + when(owner.getId()).thenReturn(ACCOUNT_ID); + when(accountManager.getAccount(ACCOUNT_ID)).thenReturn(owner); + when(instanceBootGroupDao.isNameInUse(ACCOUNT_ID, "newname")).thenReturn(true); + + service.updateInstanceBootGroup(cmd); + } + + @Test + public void testUpdateInstanceBootGroupOverridesSetAndCleared() { + UpdateInstanceBootGroupCmd cmd = mock(UpdateInstanceBootGroupCmd.class); + when(cmd.getId()).thenReturn(GROUP_ID); + when(cmd.getReadinessAttemptTimeoutSeconds()).thenReturn(-1L); + when(cmd.getReadinessMaxRetryAttempts()).thenReturn(7L); + when(cmd.getReadinessRebootOnRetry()).thenReturn(false); + when(cmd.getReadinessInitialDelaySeconds()).thenReturn(15L); + InstanceBootGroupVO group = newGroup(GROUP_ID, "group1", ACCOUNT_ID); + when(instanceBootGroupDao.findById(GROUP_ID)).thenReturn(group); + + service.updateInstanceBootGroup(cmd); + + verify(instanceBootGroupDetailsDao).setDetail(GROUP_ID, InstanceBootGroupManagerImpl.ReadinessAttemptTimeoutSeconds.key(), null); + verify(instanceBootGroupDetailsDao).setDetail(GROUP_ID, InstanceBootGroupManagerImpl.ReadinessMaxRetryAttempts.key(), "7"); + verify(instanceBootGroupDetailsDao).setDetail(GROUP_ID, InstanceBootGroupManagerImpl.ReadinessRebootOnRetry.key(), "false"); + verify(instanceBootGroupDetailsDao).setDetail(GROUP_ID, InstanceBootGroupManagerImpl.ReadinessInitialDelaySeconds.key(), "15"); + } + + // ---------------------------------------------------------------- getGroupAndCheckAccess + + @Test(expected = InvalidParameterValueException.class) + public void testGetGroupAndCheckAccessNotFoundThrows() { + when(instanceBootGroupDao.findById(GROUP_ID)).thenReturn(null); + service.getGroupAndCheckAccess(GROUP_ID); + } + + @Test + public void testGetGroupAndCheckAccessSuccessDelegatesToAccountManager() { + InstanceBootGroupVO group = newGroup(GROUP_ID, "group1", ACCOUNT_ID); + when(instanceBootGroupDao.findById(GROUP_ID)).thenReturn(group); + + InstanceBootGroupVO result = service.getGroupAndCheckAccess(GROUP_ID); + + assertEquals(group, result); + verify(accountManager).checkAccess(callerMock, null, true, group); + } + + // ---------------------------------------------------------------- addMemberToInstanceBootGroup + + @Test + public void testAddMemberToInstanceBootGroupVirtualMachineSuccess() { + AddMemberToInstanceBootGroupCmd cmd = mock(AddMemberToInstanceBootGroupCmd.class); + when(cmd.getId()).thenReturn(GROUP_ID); + when(cmd.getOrder()).thenReturn(0); + when(cmd.getVirtualMachineId()).thenReturn(VM_ID); + when(cmd.getInstanceGroupId()).thenReturn(null); + + InstanceBootGroupVO group = newGroup(GROUP_ID, "group1", ACCOUNT_ID); + when(instanceBootGroupDao.findById(GROUP_ID)).thenReturn(group); + UserVmVO vm = newVm(VM_ID, "vm1", "vm1host", ACCOUNT_ID, VirtualMachine.State.Running); + when(userVmDao.findById(VM_ID)).thenReturn(vm); + when(instanceBootGroupMemberDao.findByMember(InstanceBootGroupMember.MemberType.VirtualMachine, VM_ID)).thenReturn(null); + when(instanceBootGroupMemberDao.listByBootGroupId(GROUP_ID)).thenReturn(new ArrayList<>()); + when(instanceBootGroupMemberDao.persist(any(InstanceBootGroupMemberVO.class))).thenAnswer(inv -> inv.getArgument(0)); + + InstanceBootGroupMember result = service.addMemberToInstanceBootGroup(cmd); + + assertNotNull(result); + assertEquals(InstanceBootGroupMember.MemberType.VirtualMachine, result.getMemberType()); + assertEquals(VM_ID, result.getMemberId()); + verify(instanceBootGroupMembershipGuard).validateVmEligibleForGroupMembership(VM_ID); + } + + @Test + public void testAddMemberToInstanceBootGroupInstanceGroupSuccess() { + AddMemberToInstanceBootGroupCmd cmd = mock(AddMemberToInstanceBootGroupCmd.class); + when(cmd.getId()).thenReturn(GROUP_ID); + when(cmd.getOrder()).thenReturn(0); + when(cmd.getVirtualMachineId()).thenReturn(null); + when(cmd.getInstanceGroupId()).thenReturn(INSTANCE_GROUP_ID); + + InstanceBootGroupVO group = newGroup(GROUP_ID, "group1", ACCOUNT_ID); + when(instanceBootGroupDao.findById(GROUP_ID)).thenReturn(group); + InstanceGroupVO instanceGroup = newInstanceGroup(INSTANCE_GROUP_ID, "ig1", ACCOUNT_ID); + when(instanceGroupDao.findById(INSTANCE_GROUP_ID)).thenReturn(instanceGroup); + when(instanceBootGroupMemberDao.findByMember(InstanceBootGroupMember.MemberType.InstanceGroup, INSTANCE_GROUP_ID)).thenReturn(null); + when(instanceBootGroupMemberDao.listByBootGroupId(GROUP_ID)).thenReturn(new ArrayList<>()); + when(instanceBootGroupMemberDao.persist(any(InstanceBootGroupMemberVO.class))).thenAnswer(inv -> inv.getArgument(0)); + + InstanceBootGroupMember result = service.addMemberToInstanceBootGroup(cmd); + + assertNotNull(result); + assertEquals(InstanceBootGroupMember.MemberType.InstanceGroup, result.getMemberType()); + assertEquals(INSTANCE_GROUP_ID, result.getMemberId()); + verify(instanceBootGroupMembershipGuard).validateInstanceGroupEligibleForBootGroupMembership(INSTANCE_GROUP_ID); + } + + @Test(expected = InvalidParameterValueException.class) + public void testAddMemberToInstanceBootGroupNegativeOrderThrows() { + AddMemberToInstanceBootGroupCmd cmd = mock(AddMemberToInstanceBootGroupCmd.class); + when(cmd.getId()).thenReturn(GROUP_ID); + when(cmd.getOrder()).thenReturn(-1); + InstanceBootGroupVO group = newGroup(GROUP_ID, "group1", ACCOUNT_ID); + when(instanceBootGroupDao.findById(GROUP_ID)).thenReturn(group); + + service.addMemberToInstanceBootGroup(cmd); + } + + @Test(expected = InvalidParameterValueException.class) + public void testAddMemberToInstanceBootGroupBothIdsSpecifiedThrows() { + AddMemberToInstanceBootGroupCmd cmd = mock(AddMemberToInstanceBootGroupCmd.class); + when(cmd.getId()).thenReturn(GROUP_ID); + when(cmd.getOrder()).thenReturn(0); + when(cmd.getVirtualMachineId()).thenReturn(VM_ID); + when(cmd.getInstanceGroupId()).thenReturn(INSTANCE_GROUP_ID); + InstanceBootGroupVO group = newGroup(GROUP_ID, "group1", ACCOUNT_ID); + when(instanceBootGroupDao.findById(GROUP_ID)).thenReturn(group); + + service.addMemberToInstanceBootGroup(cmd); + } + + @Test(expected = InvalidParameterValueException.class) + public void testAddMemberToInstanceBootGroupNeitherIdSpecifiedThrows() { + AddMemberToInstanceBootGroupCmd cmd = mock(AddMemberToInstanceBootGroupCmd.class); + when(cmd.getId()).thenReturn(GROUP_ID); + when(cmd.getOrder()).thenReturn(0); + when(cmd.getVirtualMachineId()).thenReturn(null); + when(cmd.getInstanceGroupId()).thenReturn(null); + InstanceBootGroupVO group = newGroup(GROUP_ID, "group1", ACCOUNT_ID); + when(instanceBootGroupDao.findById(GROUP_ID)).thenReturn(group); + + service.addMemberToInstanceBootGroup(cmd); + } + + @Test(expected = InvalidParameterValueException.class) + public void testAddMemberToInstanceBootGroupAlreadyMemberThrows() { + AddMemberToInstanceBootGroupCmd cmd = mock(AddMemberToInstanceBootGroupCmd.class); + when(cmd.getId()).thenReturn(GROUP_ID); + when(cmd.getOrder()).thenReturn(0); + when(cmd.getVirtualMachineId()).thenReturn(VM_ID); + when(cmd.getInstanceGroupId()).thenReturn(null); + InstanceBootGroupVO group = newGroup(GROUP_ID, "group1", ACCOUNT_ID); + when(instanceBootGroupDao.findById(GROUP_ID)).thenReturn(group); + UserVmVO vm = newVm(VM_ID, "vm1", "vm1host", ACCOUNT_ID, VirtualMachine.State.Running); + when(userVmDao.findById(VM_ID)).thenReturn(vm); + InstanceBootGroupMemberVO existing = newMember(999L, GROUP_ID, InstanceBootGroupMember.MemberType.VirtualMachine, VM_ID, 0); + when(instanceBootGroupMemberDao.findByMember(InstanceBootGroupMember.MemberType.VirtualMachine, VM_ID)).thenReturn(existing); + + service.addMemberToInstanceBootGroup(cmd); + } + + @Test(expected = InvalidParameterValueException.class) + public void testAddMemberToInstanceBootGroupAtMaxMembersThrows() { + AddMemberToInstanceBootGroupCmd cmd = mock(AddMemberToInstanceBootGroupCmd.class); + when(cmd.getId()).thenReturn(GROUP_ID); + when(cmd.getOrder()).thenReturn(0); + when(cmd.getVirtualMachineId()).thenReturn(VM_ID); + when(cmd.getInstanceGroupId()).thenReturn(null); + InstanceBootGroupVO group = newGroup(GROUP_ID, "group1", ACCOUNT_ID); + when(instanceBootGroupDao.findById(GROUP_ID)).thenReturn(group); + UserVmVO vm = newVm(VM_ID, "vm1", "vm1host", ACCOUNT_ID, VirtualMachine.State.Running); + when(userVmDao.findById(VM_ID)).thenReturn(vm); + when(instanceBootGroupMemberDao.findByMember(InstanceBootGroupMember.MemberType.VirtualMachine, VM_ID)).thenReturn(null); + + long maxMembers = InstanceBootGroupManagerImpl.MaxMembersPerBootGroup.value(); + List existingMembers = new ArrayList<>(); + for (int i = 0; i < maxMembers; i++) { + existingMembers.add(newMember(100L + i, GROUP_ID, InstanceBootGroupMember.MemberType.VirtualMachine, 200L + i, i)); + } + when(instanceBootGroupMemberDao.listByBootGroupId(GROUP_ID)).thenReturn(existingMembers); + + service.addMemberToInstanceBootGroup(cmd); + } + + @Test(expected = PermissionDeniedException.class) + public void testAddMemberToInstanceBootGroupDifferentAccountThrows() { + AddMemberToInstanceBootGroupCmd cmd = mock(AddMemberToInstanceBootGroupCmd.class); + when(cmd.getId()).thenReturn(GROUP_ID); + when(cmd.getOrder()).thenReturn(0); + when(cmd.getVirtualMachineId()).thenReturn(VM_ID); + when(cmd.getInstanceGroupId()).thenReturn(null); + InstanceBootGroupVO group = newGroup(GROUP_ID, "group1", ACCOUNT_ID); + when(instanceBootGroupDao.findById(GROUP_ID)).thenReturn(group); + UserVmVO vm = newVm(VM_ID, "vm1", "vm1host", ACCOUNT_ID + 1, VirtualMachine.State.Running); + when(userVmDao.findById(VM_ID)).thenReturn(vm); + + service.addMemberToInstanceBootGroup(cmd); + } + + @Test(expected = InvalidParameterValueException.class) + public void testAddMemberToInstanceBootGroupInstanceGroupNotFoundThrows() { + AddMemberToInstanceBootGroupCmd cmd = mock(AddMemberToInstanceBootGroupCmd.class); + when(cmd.getId()).thenReturn(GROUP_ID); + when(cmd.getOrder()).thenReturn(0); + when(cmd.getVirtualMachineId()).thenReturn(null); + when(cmd.getInstanceGroupId()).thenReturn(INSTANCE_GROUP_ID); + InstanceBootGroupVO group = newGroup(GROUP_ID, "group1", ACCOUNT_ID); + when(instanceBootGroupDao.findById(GROUP_ID)).thenReturn(group); + when(instanceGroupDao.findById(INSTANCE_GROUP_ID)).thenReturn(null); + + service.addMemberToInstanceBootGroup(cmd); + } + + @Test + public void testAddMemberToInstanceBootGroupShiftsCollidingSiblingsUp() { + AddMemberToInstanceBootGroupCmd cmd = mock(AddMemberToInstanceBootGroupCmd.class); + when(cmd.getId()).thenReturn(GROUP_ID); + when(cmd.getOrder()).thenReturn(2); + when(cmd.getVirtualMachineId()).thenReturn(VM_ID); + when(cmd.getInstanceGroupId()).thenReturn(null); + InstanceBootGroupVO group = newGroup(GROUP_ID, "group1", ACCOUNT_ID); + when(instanceBootGroupDao.findById(GROUP_ID)).thenReturn(group); + UserVmVO vm = newVm(VM_ID, "vm1", "vm1host", ACCOUNT_ID, VirtualMachine.State.Running); + when(userVmDao.findById(VM_ID)).thenReturn(vm); + when(instanceBootGroupMemberDao.findByMember(InstanceBootGroupMember.MemberType.VirtualMachine, VM_ID)).thenReturn(null); + + InstanceBootGroupMemberVO siblingBelow = newMember(1L, GROUP_ID, InstanceBootGroupMember.MemberType.VirtualMachine, 11L, 1); + InstanceBootGroupMemberVO siblingAtOrder = newMember(2L, GROUP_ID, InstanceBootGroupMember.MemberType.VirtualMachine, 12L, 2); + InstanceBootGroupMemberVO siblingAbove = newMember(3L, GROUP_ID, InstanceBootGroupMember.MemberType.VirtualMachine, 13L, 5); + when(instanceBootGroupMemberDao.listByBootGroupId(GROUP_ID)).thenReturn(Arrays.asList(siblingBelow, siblingAtOrder, siblingAbove)); + when(instanceBootGroupMemberDao.persist(any(InstanceBootGroupMemberVO.class))).thenAnswer(inv -> inv.getArgument(0)); + + service.addMemberToInstanceBootGroup(cmd); + + verify(instanceBootGroupMemberDao, never()).update(eq(1L), any()); + ArgumentCaptor captor = ArgumentCaptor.forClass(InstanceBootGroupMemberVO.class); + verify(instanceBootGroupMemberDao).update(eq(2L), captor.capture()); + assertEquals(3, captor.getValue().getOrder()); + verify(instanceBootGroupMemberDao).update(eq(3L), captor.capture()); + assertEquals(6, captor.getValue().getOrder()); + } + + // ---------------------------------------------------------------- removeInstanceBootGroupMember + + @Test + public void testRemoveInstanceBootGroupMemberSuccess() { + RemoveInstanceBootGroupMemberCmd cmd = mock(RemoveInstanceBootGroupMemberCmd.class); + when(cmd.getId()).thenReturn(MEMBER_ID); + InstanceBootGroupMemberVO member = newMember(MEMBER_ID, GROUP_ID, InstanceBootGroupMember.MemberType.VirtualMachine, VM_ID, 0); + when(instanceBootGroupMemberDao.findById(MEMBER_ID)).thenReturn(member); + InstanceBootGroupVO group = newGroup(GROUP_ID, "group1", ACCOUNT_ID); + when(instanceBootGroupDao.findById(GROUP_ID)).thenReturn(group); + + boolean result = service.removeInstanceBootGroupMember(cmd); + + assertTrue(result); + verify(instanceBootGroupMemberDao).expunge(MEMBER_ID); + } + + @Test(expected = InvalidParameterValueException.class) + public void testRemoveInstanceBootGroupMemberNotFoundThrows() { + RemoveInstanceBootGroupMemberCmd cmd = mock(RemoveInstanceBootGroupMemberCmd.class); + when(cmd.getId()).thenReturn(MEMBER_ID); + when(instanceBootGroupMemberDao.findById(MEMBER_ID)).thenReturn(null); + + service.removeInstanceBootGroupMember(cmd); + } + + // ---------------------------------------------------------------- updateInstanceBootGroupMember (reorder) + + @Test + public void testUpdateInstanceBootGroupMemberNotFoundThrows() { + UpdateInstanceBootGroupMemberCmd cmd = mock(UpdateInstanceBootGroupMemberCmd.class); + when(cmd.getId()).thenReturn(MEMBER_ID); + when(instanceBootGroupMemberDao.findById(MEMBER_ID)).thenReturn(null); + try { + service.updateInstanceBootGroupMember(cmd); + org.junit.Assert.fail("expected InvalidParameterValueException"); + } catch (InvalidParameterValueException expected) { + // expected + } + } + + @Test(expected = InvalidParameterValueException.class) + public void testUpdateInstanceBootGroupMemberNegativeOrderThrows() { + UpdateInstanceBootGroupMemberCmd cmd = mock(UpdateInstanceBootGroupMemberCmd.class); + when(cmd.getId()).thenReturn(MEMBER_ID); + when(cmd.getOrder()).thenReturn(-1); + InstanceBootGroupMemberVO member = newMember(MEMBER_ID, GROUP_ID, InstanceBootGroupMember.MemberType.VirtualMachine, VM_ID, 2); + when(instanceBootGroupMemberDao.findById(MEMBER_ID)).thenReturn(member); + + service.updateInstanceBootGroupMember(cmd); + } + + @Test + public void testUpdateInstanceBootGroupMemberNoChangeDoesNotShiftOrPersist() { + UpdateInstanceBootGroupMemberCmd cmd = mock(UpdateInstanceBootGroupMemberCmd.class); + when(cmd.getId()).thenReturn(MEMBER_ID); + when(cmd.getOrder()).thenReturn(2); + InstanceBootGroupMemberVO member = newMember(MEMBER_ID, GROUP_ID, InstanceBootGroupMember.MemberType.VirtualMachine, VM_ID, 2); + when(instanceBootGroupMemberDao.findById(MEMBER_ID)).thenReturn(member); + InstanceBootGroupVO group = newGroup(GROUP_ID, "group1", ACCOUNT_ID); + when(instanceBootGroupDao.findById(GROUP_ID)).thenReturn(group); + + service.updateInstanceBootGroupMember(cmd); + + verify(instanceBootGroupMemberDao, never()).update(anyLong(), any()); + verify(instanceBootGroupMemberDao, never()).listByBootGroupId(anyLong()); + } + + @Test + public void testUpdateInstanceBootGroupMemberMoveDownShiftsBetweenSiblingsDown() { + UpdateInstanceBootGroupMemberCmd cmd = mock(UpdateInstanceBootGroupMemberCmd.class); + when(cmd.getId()).thenReturn(MEMBER_ID); + when(cmd.getOrder()).thenReturn(3); + InstanceBootGroupMemberVO member = newMember(MEMBER_ID, GROUP_ID, InstanceBootGroupMember.MemberType.VirtualMachine, VM_ID, 1); + when(instanceBootGroupMemberDao.findById(MEMBER_ID)).thenReturn(member); + InstanceBootGroupVO group = newGroup(GROUP_ID, "group1", ACCOUNT_ID); + when(instanceBootGroupDao.findById(GROUP_ID)).thenReturn(group); + + InstanceBootGroupMemberVO siblingUnaffectedLow = newMember(11L, GROUP_ID, InstanceBootGroupMember.MemberType.VirtualMachine, 1L, 0); + InstanceBootGroupMemberVO siblingB = newMember(12L, GROUP_ID, InstanceBootGroupMember.MemberType.VirtualMachine, 2L, 2); + InstanceBootGroupMemberVO siblingC = newMember(13L, GROUP_ID, InstanceBootGroupMember.MemberType.VirtualMachine, 3L, 3); + InstanceBootGroupMemberVO siblingUnaffectedHigh = newMember(14L, GROUP_ID, InstanceBootGroupMember.MemberType.VirtualMachine, 4L, 5); + when(instanceBootGroupMemberDao.listByBootGroupId(GROUP_ID)) + .thenReturn(Arrays.asList(member, siblingUnaffectedLow, siblingB, siblingC, siblingUnaffectedHigh)); + + service.updateInstanceBootGroupMember(cmd); + + verify(instanceBootGroupMemberDao, never()).update(eq(11L), any()); + verify(instanceBootGroupMemberDao, never()).update(eq(14L), any()); + ArgumentCaptor captor = ArgumentCaptor.forClass(InstanceBootGroupMemberVO.class); + verify(instanceBootGroupMemberDao).update(eq(12L), captor.capture()); + assertEquals(1, captor.getValue().getOrder()); + verify(instanceBootGroupMemberDao).update(eq(13L), captor.capture()); + assertEquals(2, captor.getValue().getOrder()); + verify(instanceBootGroupMemberDao).update(eq(MEMBER_ID), captor.capture()); + assertEquals(3, captor.getValue().getOrder()); + } + + @Test + public void testUpdateInstanceBootGroupMemberMoveUpShiftsBetweenSiblingsUp() { + UpdateInstanceBootGroupMemberCmd cmd = mock(UpdateInstanceBootGroupMemberCmd.class); + when(cmd.getId()).thenReturn(MEMBER_ID); + when(cmd.getOrder()).thenReturn(1); + InstanceBootGroupMemberVO member = newMember(MEMBER_ID, GROUP_ID, InstanceBootGroupMember.MemberType.VirtualMachine, VM_ID, 3); + when(instanceBootGroupMemberDao.findById(MEMBER_ID)).thenReturn(member); + InstanceBootGroupVO group = newGroup(GROUP_ID, "group1", ACCOUNT_ID); + when(instanceBootGroupDao.findById(GROUP_ID)).thenReturn(group); + + InstanceBootGroupMemberVO siblingUnaffectedLow = newMember(11L, GROUP_ID, InstanceBootGroupMember.MemberType.VirtualMachine, 1L, 0); + InstanceBootGroupMemberVO siblingB = newMember(12L, GROUP_ID, InstanceBootGroupMember.MemberType.VirtualMachine, 2L, 1); + InstanceBootGroupMemberVO siblingC = newMember(13L, GROUP_ID, InstanceBootGroupMember.MemberType.VirtualMachine, 3L, 2); + InstanceBootGroupMemberVO siblingUnaffectedHigh = newMember(14L, GROUP_ID, InstanceBootGroupMember.MemberType.VirtualMachine, 4L, 4); + when(instanceBootGroupMemberDao.listByBootGroupId(GROUP_ID)) + .thenReturn(Arrays.asList(member, siblingUnaffectedLow, siblingB, siblingC, siblingUnaffectedHigh)); + + service.updateInstanceBootGroupMember(cmd); + + verify(instanceBootGroupMemberDao, never()).update(eq(11L), any()); + verify(instanceBootGroupMemberDao, never()).update(eq(14L), any()); + ArgumentCaptor captor = ArgumentCaptor.forClass(InstanceBootGroupMemberVO.class); + verify(instanceBootGroupMemberDao).update(eq(12L), captor.capture()); + assertEquals(2, captor.getValue().getOrder()); + verify(instanceBootGroupMemberDao).update(eq(13L), captor.capture()); + assertEquals(3, captor.getValue().getOrder()); + verify(instanceBootGroupMemberDao).update(eq(MEMBER_ID), captor.capture()); + assertEquals(1, captor.getValue().getOrder()); + } + + // ---------------------------------------------------------------- listInstanceBootGroupMembers / readiness + + private ListInstanceBootGroupMembersCmd baseListMembersCmd(boolean readiness, boolean children, boolean ignoreState) { + ListInstanceBootGroupMembersCmd cmd = mock(ListInstanceBootGroupMembersCmd.class); + when(cmd.getBootGroupId()).thenReturn(GROUP_ID); + when(cmd.getMemberType()).thenReturn(null); + when(cmd.isReadinessDetailRequested()).thenReturn(readiness); + when(cmd.isChildrenDetailRequested()).thenReturn(children); + when(cmd.isIgnoreInstanceState()).thenReturn(ignoreState); + return cmd; + } + + @Test + public void testListInstanceBootGroupMembersSortedByOrder() { + ListInstanceBootGroupMembersCmd cmd = baseListMembersCmd(false, false, false); + InstanceBootGroupVO group = newGroup(GROUP_ID, "group1", ACCOUNT_ID); + when(instanceBootGroupDao.findById(GROUP_ID)).thenReturn(group); + + InstanceBootGroupMemberVO memberHighOrder = newMember(1L, GROUP_ID, InstanceBootGroupMember.MemberType.VirtualMachine, VM_ID, 5); + InstanceBootGroupMemberVO memberLowOrder = newMember(2L, GROUP_ID, InstanceBootGroupMember.MemberType.VirtualMachine, VM2_ID, 1); + ReflectionTestUtils.setField(memberHighOrder, "uuid", "uuid-high-order"); + ReflectionTestUtils.setField(memberLowOrder, "uuid", "uuid-low-order"); + when(instanceBootGroupMemberDao.searchAndCountByBootGroupId(GROUP_ID)) + .thenReturn(new com.cloud.utils.Pair<>(new ArrayList<>(Arrays.asList(memberHighOrder, memberLowOrder)), 2)); + when(userVmDao.findById(VM_ID)).thenReturn(newVm(VM_ID, "vm1", "vm1host", ACCOUNT_ID, VirtualMachine.State.Running)); + when(userVmDao.findById(VM2_ID)).thenReturn(newVm(VM2_ID, "vm2", "vm2host", ACCOUNT_ID, VirtualMachine.State.Running)); + + ListResponse response = service.listInstanceBootGroupMembers(cmd); + + assertEquals(2, response.getResponses().size()); + assertEquals("uuid-low-order", field(response.getResponses().get(0), "id")); + assertEquals("uuid-high-order", field(response.getResponses().get(1), "id")); + } + + @Test + public void testListInstanceBootGroupMembersNoRulesRunningVmIsReady() { + ListInstanceBootGroupMembersCmd cmd = baseListMembersCmd(true, false, false); + InstanceBootGroupVO group = newGroup(GROUP_ID, "group1", ACCOUNT_ID); + when(instanceBootGroupDao.findById(GROUP_ID)).thenReturn(group); + InstanceBootGroupMemberVO member = newMember(1L, GROUP_ID, InstanceBootGroupMember.MemberType.VirtualMachine, VM_ID, 0); + when(instanceBootGroupMemberDao.searchAndCountByBootGroupId(GROUP_ID)) + .thenReturn(new com.cloud.utils.Pair<>(new ArrayList<>(Collections.singletonList(member)), 1)); + when(userVmDao.findById(VM_ID)).thenReturn(newVm(VM_ID, "vm1", "vm1host", ACCOUNT_ID, VirtualMachine.State.Running)); + when(instanceBootGroupReadinessRuleDao.listEnabledByItem(GROUP_ID, InstanceBootGroupMember.MemberType.VirtualMachine, VM_ID)) + .thenReturn(Collections.emptyList()); + when(instanceBootGroupReadinessRuleService.findInheritedGroupRules(GROUP_ID, VM_ID)).thenReturn(Collections.emptyList()); + + ListResponse response = service.listInstanceBootGroupMembers(cmd); + + InstanceBootGroupMemberResponse memberResponse = response.getResponses().get(0); + assertEquals("Ready", field(memberResponse, "readinessStatus")); + assertEquals("None", field(memberResponse, "readinessMode")); + assertTrue(field(memberResponse, "readinessMessage").contains("No readiness rules attached")); + } + + @Test + public void testListInstanceBootGroupMembersNoRulesStoppedVmIsNotReady() { + ListInstanceBootGroupMembersCmd cmd = baseListMembersCmd(true, false, false); + InstanceBootGroupVO group = newGroup(GROUP_ID, "group1", ACCOUNT_ID); + when(instanceBootGroupDao.findById(GROUP_ID)).thenReturn(group); + InstanceBootGroupMemberVO member = newMember(1L, GROUP_ID, InstanceBootGroupMember.MemberType.VirtualMachine, VM_ID, 0); + when(instanceBootGroupMemberDao.searchAndCountByBootGroupId(GROUP_ID)) + .thenReturn(new com.cloud.utils.Pair<>(new ArrayList<>(Collections.singletonList(member)), 1)); + when(userVmDao.findById(VM_ID)).thenReturn(newVm(VM_ID, "vm1", "vm1host", ACCOUNT_ID, VirtualMachine.State.Stopped)); + when(instanceBootGroupReadinessRuleDao.listEnabledByItem(GROUP_ID, InstanceBootGroupMember.MemberType.VirtualMachine, VM_ID)) + .thenReturn(Collections.emptyList()); + when(instanceBootGroupReadinessRuleService.findInheritedGroupRules(GROUP_ID, VM_ID)).thenReturn(Collections.emptyList()); + + ListResponse response = service.listInstanceBootGroupMembers(cmd); + + InstanceBootGroupMemberResponse memberResponse = response.getResponses().get(0); + assertEquals("NotReady", field(memberResponse, "readinessStatus")); + } + + @Test + public void testListInstanceBootGroupMembersWithRulesStoppedVmForcedNotReadyRegardlessOfCache() { + ListInstanceBootGroupMembersCmd cmd = baseListMembersCmd(true, false, false); + InstanceBootGroupVO group = newGroup(GROUP_ID, "group1", ACCOUNT_ID); + when(instanceBootGroupDao.findById(GROUP_ID)).thenReturn(group); + InstanceBootGroupMemberVO member = newMember(1L, GROUP_ID, InstanceBootGroupMember.MemberType.VirtualMachine, VM_ID, 0); + when(instanceBootGroupMemberDao.searchAndCountByBootGroupId(GROUP_ID)) + .thenReturn(new com.cloud.utils.Pair<>(new ArrayList<>(Collections.singletonList(member)), 1)); + when(userVmDao.findById(VM_ID)).thenReturn(newVm(VM_ID, "vm1", "vm1host", ACCOUNT_ID, VirtualMachine.State.Stopped)); + InstanceBootGroupReadinessRuleVO rule = newRule(RULE_ID, GROUP_ID, InstanceBootGroupMember.MemberType.VirtualMachine, VM_ID, + InstanceBootGroupReadinessRule.RuleType.Ping, true, "ping-rule"); + when(instanceBootGroupReadinessRuleDao.listEnabledByItem(GROUP_ID, InstanceBootGroupMember.MemberType.VirtualMachine, VM_ID)) + .thenReturn(Collections.singletonList(rule)); + when(instanceBootGroupReadinessRuleService.findInheritedGroupRules(GROUP_ID, VM_ID)).thenReturn(Collections.emptyList()); + + ListResponse response = service.listInstanceBootGroupMembers(cmd); + + InstanceBootGroupMemberResponse memberResponse = response.getResponses().get(0); + assertEquals("NotReady", field(memberResponse, "readinessStatus")); + assertTrue(field(memberResponse, "readinessMessage").contains("Instance state is Stopped")); + verify(instanceBootGroupReadinessCheckResultDao, never()).findByRuleAndVm(RULE_ID, 0L); + } + + @Test + public void testListInstanceBootGroupMembersIgnoreVmStateUsesCachedRuleResult() { + ListInstanceBootGroupMembersCmd cmd = baseListMembersCmd(true, false, true); + InstanceBootGroupVO group = newGroup(GROUP_ID, "group1", ACCOUNT_ID); + when(instanceBootGroupDao.findById(GROUP_ID)).thenReturn(group); + InstanceBootGroupMemberVO member = newMember(1L, GROUP_ID, InstanceBootGroupMember.MemberType.VirtualMachine, VM_ID, 0); + when(instanceBootGroupMemberDao.searchAndCountByBootGroupId(GROUP_ID)) + .thenReturn(new com.cloud.utils.Pair<>(new ArrayList<>(Collections.singletonList(member)), 1)); + when(userVmDao.findById(VM_ID)).thenReturn(newVm(VM_ID, "vm1", "vm1host", ACCOUNT_ID, VirtualMachine.State.Stopped)); + InstanceBootGroupReadinessRuleVO rule = newRule(RULE_ID, GROUP_ID, InstanceBootGroupMember.MemberType.VirtualMachine, VM_ID, + InstanceBootGroupReadinessRule.RuleType.Ping, true, "ping-rule"); + when(instanceBootGroupReadinessRuleDao.listEnabledByItem(GROUP_ID, InstanceBootGroupMember.MemberType.VirtualMachine, VM_ID)) + .thenReturn(Collections.singletonList(rule)); + when(instanceBootGroupReadinessRuleService.findInheritedGroupRules(GROUP_ID, VM_ID)).thenReturn(Collections.emptyList()); + when(instanceBootGroupReadinessCheckResultDao.findByRuleAndVm(RULE_ID, 0L)) + .thenReturn(new InstanceBootGroupReadinessCheckResultVO(RULE_ID, 0L, InstanceBootGroupReadinessRule.Status.NotReady, "timeout", new Date())); + + ListResponse response = service.listInstanceBootGroupMembers(cmd); + + InstanceBootGroupMemberResponse memberResponse = response.getResponses().get(0); + assertEquals("NotReady", field(memberResponse, "readinessStatus")); + assertEquals("Ping: timeout", field(memberResponse, "readinessMessage")); + assertEquals("RuleBased", field(memberResponse, "readinessMode")); + } + + @Test + public void testListInstanceBootGroupMembersInstanceGroupMemberQuorumExcludesOwnStatusFromAggregate() { + ListInstanceBootGroupMembersCmd cmd = baseListMembersCmd(true, true, false); + InstanceBootGroupVO group = newGroup(GROUP_ID, "group1", ACCOUNT_ID); + when(instanceBootGroupDao.findById(GROUP_ID)).thenReturn(group); + InstanceBootGroupMemberVO member = newMember(1L, GROUP_ID, InstanceBootGroupMember.MemberType.InstanceGroup, INSTANCE_GROUP_ID, 0); + when(instanceBootGroupMemberDao.searchAndCountByBootGroupId(GROUP_ID)) + .thenReturn(new com.cloud.utils.Pair<>(new ArrayList<>(Collections.singletonList(member)), 1)); + InstanceGroupVO instanceGroup = newInstanceGroup(INSTANCE_GROUP_ID, "ig1", ACCOUNT_ID); + when(instanceGroupDao.findById(INSTANCE_GROUP_ID)).thenReturn(instanceGroup); + + InstanceGroupVMMapVO map1 = new InstanceGroupVMMapVO(INSTANCE_GROUP_ID, VM_ID); + InstanceGroupVMMapVO map2 = new InstanceGroupVMMapVO(INSTANCE_GROUP_ID, VM2_ID); + when(instanceGroupVMMapDao.listByGroupId(INSTANCE_GROUP_ID)).thenReturn(Arrays.asList(map1, map2)); + when(userVmDao.listByIds(any())).thenReturn(Arrays.asList( + newVm(VM_ID, "vm1", "vm1host", ACCOUNT_ID, VirtualMachine.State.Running), + newVm(VM2_ID, "vm2", "vm2host", ACCOUNT_ID, VirtualMachine.State.Stopped))); + when(userVmDao.findById(VM_ID)).thenReturn(newVm(VM_ID, "vm1", "vm1host", ACCOUNT_ID, VirtualMachine.State.Running)); + when(userVmDao.findById(VM2_ID)).thenReturn(newVm(VM2_ID, "vm2", "vm2host", ACCOUNT_ID, VirtualMachine.State.Stopped)); + + InstanceBootGroupReadinessRuleVO quorumRule = newRule(RULE_ID, GROUP_ID, InstanceBootGroupMember.MemberType.InstanceGroup, INSTANCE_GROUP_ID, + InstanceBootGroupReadinessRule.RuleType.MemberQuorum, true, "quorum-rule"); + when(instanceBootGroupReadinessRuleDao.listEnabledByItem(GROUP_ID, InstanceBootGroupMember.MemberType.InstanceGroup, INSTANCE_GROUP_ID)) + .thenReturn(Collections.singletonList(quorumRule)); + when(instanceBootGroupReadinessCheckResultDao.findByRuleAndVm(RULE_ID, 0L)) + .thenReturn(new InstanceBootGroupReadinessCheckResultVO(RULE_ID, 0L, InstanceBootGroupReadinessRule.Status.Ready, "quorum met", new Date())); + // VM children have no direct rules of their own + when(instanceBootGroupReadinessRuleDao.listEnabledByItem(GROUP_ID, InstanceBootGroupMember.MemberType.VirtualMachine, VM_ID)) + .thenReturn(Collections.emptyList()); + when(instanceBootGroupReadinessRuleDao.listEnabledByItem(GROUP_ID, InstanceBootGroupMember.MemberType.VirtualMachine, VM2_ID)) + .thenReturn(Collections.emptyList()); + when(instanceBootGroupReadinessRuleService.findInheritedGroupRules(eq(GROUP_ID), anyLong())).thenReturn(Collections.emptyList()); + + ListResponse response = service.listInstanceBootGroupMembers(cmd); + + InstanceBootGroupMemberResponse memberResponse = response.getResponses().get(0); + // Own MemberQuorum result is Ready, so the group is Ready even though one child VM is NotReady. + assertEquals("Ready", field(memberResponse, "readinessStatus")); + assertEquals("1 of 2 member VM(s) not ready", field(memberResponse, "readinessMessage")); + assertEquals("RuleBased", field(memberResponse, "readinessMode")); + } + + @Test + public void testListInstanceBootGroupMembersInstanceGroupMemberQuorumOverridesFailingMemberTargetedRule() { + ListInstanceBootGroupMembersCmd cmd = baseListMembersCmd(true, true, false); + InstanceBootGroupVO group = newGroup(GROUP_ID, "group1", ACCOUNT_ID); + when(instanceBootGroupDao.findById(GROUP_ID)).thenReturn(group); + InstanceBootGroupMemberVO member = newMember(1L, GROUP_ID, InstanceBootGroupMember.MemberType.InstanceGroup, INSTANCE_GROUP_ID, 0); + when(instanceBootGroupMemberDao.searchAndCountByBootGroupId(GROUP_ID)) + .thenReturn(new com.cloud.utils.Pair<>(new ArrayList<>(Collections.singletonList(member)), 1)); + InstanceGroupVO instanceGroup = newInstanceGroup(INSTANCE_GROUP_ID, "ig1", ACCOUNT_ID); + when(instanceGroupDao.findById(INSTANCE_GROUP_ID)).thenReturn(instanceGroup); + + InstanceGroupVMMapVO map1 = new InstanceGroupVMMapVO(INSTANCE_GROUP_ID, VM_ID); + InstanceGroupVMMapVO map2 = new InstanceGroupVMMapVO(INSTANCE_GROUP_ID, VM2_ID); + when(instanceGroupVMMapDao.listByGroupId(INSTANCE_GROUP_ID)).thenReturn(Arrays.asList(map1, map2)); + when(userVmDao.listByIds(any())).thenReturn(Arrays.asList( + newVm(VM_ID, "vm1", "vm1host", ACCOUNT_ID, VirtualMachine.State.Running), + newVm(VM2_ID, "vm2", "vm2host", ACCOUNT_ID, VirtualMachine.State.Running))); + when(userVmDao.findById(VM_ID)).thenReturn(newVm(VM_ID, "vm1", "vm1host", ACCOUNT_ID, VirtualMachine.State.Running)); + when(userVmDao.findById(VM2_ID)).thenReturn(newVm(VM2_ID, "vm2", "vm2host", ACCOUNT_ID, VirtualMachine.State.Running)); + + InstanceBootGroupReadinessRuleVO guestAgentRule = newRule(RULE_ID, GROUP_ID, InstanceBootGroupMember.MemberType.InstanceGroup, INSTANCE_GROUP_ID, + InstanceBootGroupReadinessRule.RuleType.GuestAgentLiveness, true, "guest-agent-rule"); + InstanceBootGroupReadinessRuleVO quorumRule = newRule(501L, GROUP_ID, InstanceBootGroupMember.MemberType.InstanceGroup, INSTANCE_GROUP_ID, + InstanceBootGroupReadinessRule.RuleType.MemberQuorum, true, "quorum-rule"); + when(instanceBootGroupReadinessRuleDao.listEnabledByItem(GROUP_ID, InstanceBootGroupMember.MemberType.InstanceGroup, INSTANCE_GROUP_ID)) + .thenReturn(Arrays.asList(guestAgentRule, quorumRule)); + when(instanceBootGroupReadinessCheckResultDao.findByRuleAndVm(501L, 0L)) + .thenReturn(new InstanceBootGroupReadinessCheckResultVO(501L, 0L, InstanceBootGroupReadinessRule.Status.Ready, "quorum met", new Date())); + when(instanceBootGroupReadinessRuleDao.listEnabledByItem(GROUP_ID, InstanceBootGroupMember.MemberType.VirtualMachine, VM_ID)) + .thenReturn(Collections.emptyList()); + when(instanceBootGroupReadinessRuleDao.listEnabledByItem(GROUP_ID, InstanceBootGroupMember.MemberType.VirtualMachine, VM2_ID)) + .thenReturn(Collections.emptyList()); + when(instanceBootGroupReadinessRuleService.findInheritedGroupRules(eq(GROUP_ID), anyLong())).thenReturn(Collections.emptyList()); + + ListResponse response = service.listInstanceBootGroupMembers(cmd); + + InstanceBootGroupMemberResponse memberResponse = response.getResponses().get(0); + // The failing GuestAgentLiveness aggregate must not veto the group once MemberQuorum is met — + // its cached row isn't even consulted for the overall verdict. + assertEquals("Ready", field(memberResponse, "readinessStatus")); + verify(instanceBootGroupReadinessCheckResultDao, never()).findByRuleAndVm(RULE_ID, 0L); + } + + /** + * A group-scope rule (MemberQuorum in particular) isn't tied to any one VM, so nothing else + * re-derives it once a member stops outside of active orchestration — reading the member list + * must refresh it first, or it can keep reporting a stale Ready from the last successful start. + */ + @Test + public void testListInstanceBootGroupMembersRefreshesGroupOwnRulesBeforeReadingCache() { + ListInstanceBootGroupMembersCmd cmd = baseListMembersCmd(true, false, false); + InstanceBootGroupVO group = newGroup(GROUP_ID, "group1", ACCOUNT_ID); + when(instanceBootGroupDao.findById(GROUP_ID)).thenReturn(group); + InstanceBootGroupMemberVO member = newMember(1L, GROUP_ID, InstanceBootGroupMember.MemberType.InstanceGroup, INSTANCE_GROUP_ID, 0); + when(instanceBootGroupMemberDao.searchAndCountByBootGroupId(GROUP_ID)) + .thenReturn(new com.cloud.utils.Pair<>(new ArrayList<>(Collections.singletonList(member)), 1)); + InstanceGroupVO instanceGroup = newInstanceGroup(INSTANCE_GROUP_ID, "ig1", ACCOUNT_ID); + when(instanceGroupDao.findById(INSTANCE_GROUP_ID)).thenReturn(instanceGroup); + when(instanceGroupVMMapDao.listByGroupId(INSTANCE_GROUP_ID)).thenReturn(Collections.emptyList()); + + InstanceBootGroupReadinessRuleVO quorumRule = newRule(RULE_ID, GROUP_ID, InstanceBootGroupMember.MemberType.InstanceGroup, INSTANCE_GROUP_ID, + InstanceBootGroupReadinessRule.RuleType.MemberQuorum, true, "quorum-rule"); + when(instanceBootGroupReadinessRuleDao.listEnabledByItem(GROUP_ID, InstanceBootGroupMember.MemberType.InstanceGroup, INSTANCE_GROUP_ID)) + .thenReturn(Collections.singletonList(quorumRule)); + when(instanceBootGroupReadinessCheckResultDao.findByRuleAndVm(RULE_ID, 0L)) + .thenReturn(new InstanceBootGroupReadinessCheckResultVO(RULE_ID, 0L, InstanceBootGroupReadinessRule.Status.Ready, "stale", new Date())); + + service.listInstanceBootGroupMembers(cmd); + + verify(instanceBootGroupReadinessRuleService).evaluateInstanceGroupReadiness(GROUP_ID, INSTANCE_GROUP_ID, Collections.emptySet()); + } + + @Test + public void testListInstanceBootGroupMembersIgnoreVmStateSkipsGroupOwnRuleRefresh() { + ListInstanceBootGroupMembersCmd cmd = baseListMembersCmd(true, false, true); + InstanceBootGroupVO group = newGroup(GROUP_ID, "group1", ACCOUNT_ID); + when(instanceBootGroupDao.findById(GROUP_ID)).thenReturn(group); + InstanceBootGroupMemberVO member = newMember(1L, GROUP_ID, InstanceBootGroupMember.MemberType.InstanceGroup, INSTANCE_GROUP_ID, 0); + when(instanceBootGroupMemberDao.searchAndCountByBootGroupId(GROUP_ID)) + .thenReturn(new com.cloud.utils.Pair<>(new ArrayList<>(Collections.singletonList(member)), 1)); + InstanceGroupVO instanceGroup = newInstanceGroup(INSTANCE_GROUP_ID, "ig1", ACCOUNT_ID); + when(instanceGroupDao.findById(INSTANCE_GROUP_ID)).thenReturn(instanceGroup); + when(instanceGroupVMMapDao.listByGroupId(INSTANCE_GROUP_ID)).thenReturn(Collections.emptyList()); + + InstanceBootGroupReadinessRuleVO quorumRule = newRule(RULE_ID, GROUP_ID, InstanceBootGroupMember.MemberType.InstanceGroup, INSTANCE_GROUP_ID, + InstanceBootGroupReadinessRule.RuleType.MemberQuorum, true, "quorum-rule"); + when(instanceBootGroupReadinessRuleDao.listEnabledByItem(GROUP_ID, InstanceBootGroupMember.MemberType.InstanceGroup, INSTANCE_GROUP_ID)) + .thenReturn(Collections.singletonList(quorumRule)); + when(instanceBootGroupReadinessCheckResultDao.findByRuleAndVm(RULE_ID, 0L)) + .thenReturn(new InstanceBootGroupReadinessCheckResultVO(RULE_ID, 0L, InstanceBootGroupReadinessRule.Status.Ready, "last known", new Date())); + + service.listInstanceBootGroupMembers(cmd); + + verify(instanceBootGroupReadinessRuleService, never()).evaluateInstanceGroupReadiness(anyLong(), anyLong(), any()); + } + + @Test + public void testListInstanceBootGroupMembersInstanceGroupWithoutQuorumRequiresAllChildrenReady() { + ListInstanceBootGroupMembersCmd cmd = baseListMembersCmd(true, true, false); + InstanceBootGroupVO group = newGroup(GROUP_ID, "group1", ACCOUNT_ID); + when(instanceBootGroupDao.findById(GROUP_ID)).thenReturn(group); + InstanceBootGroupMemberVO member = newMember(1L, GROUP_ID, InstanceBootGroupMember.MemberType.InstanceGroup, INSTANCE_GROUP_ID, 0); + when(instanceBootGroupMemberDao.searchAndCountByBootGroupId(GROUP_ID)) + .thenReturn(new com.cloud.utils.Pair<>(new ArrayList<>(Collections.singletonList(member)), 1)); + InstanceGroupVO instanceGroup = newInstanceGroup(INSTANCE_GROUP_ID, "ig1", ACCOUNT_ID); + when(instanceGroupDao.findById(INSTANCE_GROUP_ID)).thenReturn(instanceGroup); + + InstanceGroupVMMapVO map1 = new InstanceGroupVMMapVO(INSTANCE_GROUP_ID, VM_ID); + InstanceGroupVMMapVO map2 = new InstanceGroupVMMapVO(INSTANCE_GROUP_ID, VM2_ID); + when(instanceGroupVMMapDao.listByGroupId(INSTANCE_GROUP_ID)).thenReturn(Arrays.asList(map1, map2)); + UserVmVO runningVm = newVm(VM_ID, "vm1", "vm1host", ACCOUNT_ID, VirtualMachine.State.Running); + UserVmVO stoppedVm = newVm(VM2_ID, "vm2", "vm2host", ACCOUNT_ID, VirtualMachine.State.Stopped); + when(userVmDao.listByIds(any())).thenReturn(Arrays.asList(runningVm, stoppedVm)); + when(userVmDao.findById(VM_ID)).thenReturn(runningVm); + when(userVmDao.findById(VM2_ID)).thenReturn(stoppedVm); + + // No group-scope rules attached -> ChildDependent mode, all children must be Ready. + when(instanceBootGroupReadinessRuleDao.listEnabledByItem(GROUP_ID, InstanceBootGroupMember.MemberType.InstanceGroup, INSTANCE_GROUP_ID)) + .thenReturn(Collections.emptyList()); + when(instanceBootGroupReadinessRuleDao.listEnabledByItem(GROUP_ID, InstanceBootGroupMember.MemberType.VirtualMachine, VM_ID)) + .thenReturn(Collections.emptyList()); + when(instanceBootGroupReadinessRuleDao.listEnabledByItem(GROUP_ID, InstanceBootGroupMember.MemberType.VirtualMachine, VM2_ID)) + .thenReturn(Collections.emptyList()); + when(instanceBootGroupReadinessRuleService.findInheritedGroupRules(eq(GROUP_ID), anyLong())).thenReturn(Collections.emptyList()); + + ListResponse response = service.listInstanceBootGroupMembers(cmd); + + InstanceBootGroupMemberResponse memberResponse = response.getResponses().get(0); + assertEquals("NotReady", field(memberResponse, "readinessStatus")); + assertEquals("1 of 2 member VM(s) not ready", field(memberResponse, "readinessMessage")); + assertEquals("ChildDependent", field(memberResponse, "readinessMode")); + + @SuppressWarnings("unchecked") + List children = + (List) ReflectionTestUtils.getField(memberResponse, "children"); + assertNotNull(children); + assertEquals(2, children.size()); + } + + // ---------------------------------------------------------------- start/stop/reboot + + @Test + public void testStartInstanceBootGroupDelegatesToManager() { + StartInstanceBootGroupCmd cmd = mock(StartInstanceBootGroupCmd.class); + when(cmd.getId()).thenReturn(GROUP_ID); + InstanceBootGroupVO group = newGroup(GROUP_ID, "group1", ACCOUNT_ID); + when(instanceBootGroupDao.findById(GROUP_ID)).thenReturn(group); + + InstanceBootGroup result = service.startInstanceBootGroup(cmd); + + assertEquals(group, result); + verify(instanceBootGroupManager).startInstanceBootGroup(group); + } + + @Test + public void testStopInstanceBootGroupDelegatesToManager() { + StopInstanceBootGroupCmd cmd = mock(StopInstanceBootGroupCmd.class); + when(cmd.getId()).thenReturn(GROUP_ID); + InstanceBootGroupVO group = newGroup(GROUP_ID, "group1", ACCOUNT_ID); + when(instanceBootGroupDao.findById(GROUP_ID)).thenReturn(group); + + InstanceBootGroup result = service.stopInstanceBootGroup(cmd); + + assertEquals(group, result); + verify(instanceBootGroupManager).stopInstanceBootGroup(group, false); + } + + @Test + public void testStopInstanceBootGroupDelegatesForcedFlagToManager() { + StopInstanceBootGroupCmd cmd = mock(StopInstanceBootGroupCmd.class); + when(cmd.getId()).thenReturn(GROUP_ID); + when(cmd.isForced()).thenReturn(true); + InstanceBootGroupVO group = newGroup(GROUP_ID, "group1", ACCOUNT_ID); + when(instanceBootGroupDao.findById(GROUP_ID)).thenReturn(group); + + service.stopInstanceBootGroup(cmd); + + verify(instanceBootGroupManager).stopInstanceBootGroup(group, true); + } + + @Test + public void testRebootInstanceBootGroupDelegatesToManager() { + RebootInstanceBootGroupCmd cmd = mock(RebootInstanceBootGroupCmd.class); + when(cmd.getId()).thenReturn(GROUP_ID); + InstanceBootGroupVO group = newGroup(GROUP_ID, "group1", ACCOUNT_ID); + when(instanceBootGroupDao.findById(GROUP_ID)).thenReturn(group); + + InstanceBootGroup result = service.rebootInstanceBootGroup(cmd); + + assertEquals(group, result); + verify(instanceBootGroupManager).rebootInstanceBootGroup(group, false); + } + + @Test + public void testRebootInstanceBootGroupDelegatesForcedFlagToManager() { + RebootInstanceBootGroupCmd cmd = mock(RebootInstanceBootGroupCmd.class); + when(cmd.getId()).thenReturn(GROUP_ID); + when(cmd.isForced()).thenReturn(true); + InstanceBootGroupVO group = newGroup(GROUP_ID, "group1", ACCOUNT_ID); + when(instanceBootGroupDao.findById(GROUP_ID)).thenReturn(group); + + service.rebootInstanceBootGroup(cmd); + + verify(instanceBootGroupManager).rebootInstanceBootGroup(group, true); + } + + // ---------------------------------------------------------------- readiness rule create/update/delete + + @Test + public void testCreateInstanceBootGroupReadinessRuleSuccess() { + CreateInstanceBootGroupReadinessRuleCmd cmd = mock(CreateInstanceBootGroupReadinessRuleCmd.class); + when(cmd.getBootGroupId()).thenReturn(GROUP_ID); + when(cmd.getVirtualMachineId()).thenReturn(VM_ID); + when(cmd.getInstanceGroupId()).thenReturn(null); + when(cmd.getRuleType()).thenReturn("Ping"); + when(cmd.getName()).thenReturn("rule1"); + when(cmd.isEnabled()).thenReturn(true); + when(cmd.getDetails()).thenReturn(null); + InstanceBootGroupVO group = newGroup(GROUP_ID, "group1", ACCOUNT_ID); + when(instanceBootGroupDao.findById(GROUP_ID)).thenReturn(group); + UserVmVO vm = newVm(VM_ID, "vm1", "vm1host", ACCOUNT_ID, VirtualMachine.State.Running); + when(userVmDao.findById(VM_ID)).thenReturn(vm); + InstanceBootGroupReadinessRule created = mock(InstanceBootGroupReadinessRule.class); + when(instanceBootGroupReadinessRuleService.createReadinessRule(GROUP_ID, InstanceBootGroupMember.MemberType.VirtualMachine, VM_ID, + InstanceBootGroupReadinessRule.RuleType.Ping, "rule1", true, null)).thenReturn(created); + + InstanceBootGroupReadinessRule result = service.createInstanceBootGroupReadinessRule(cmd); + + assertEquals(created, result); + verify(accountManager).checkAccess(callerMock, null, true, vm); + } + + @Test(expected = InvalidParameterValueException.class) + public void testCreateInstanceBootGroupReadinessRuleInvalidRuleTypeThrows() { + CreateInstanceBootGroupReadinessRuleCmd cmd = mock(CreateInstanceBootGroupReadinessRuleCmd.class); + when(cmd.getBootGroupId()).thenReturn(GROUP_ID); + when(cmd.getVirtualMachineId()).thenReturn(VM_ID); + when(cmd.getInstanceGroupId()).thenReturn(null); + when(cmd.getRuleType()).thenReturn("NotARealType"); + InstanceBootGroupVO group = newGroup(GROUP_ID, "group1", ACCOUNT_ID); + when(instanceBootGroupDao.findById(GROUP_ID)).thenReturn(group); + UserVmVO vm = newVm(VM_ID, "vm1", "vm1host", ACCOUNT_ID, VirtualMachine.State.Running); + when(userVmDao.findById(VM_ID)).thenReturn(vm); + + service.createInstanceBootGroupReadinessRule(cmd); + } + + @Test + public void testUpdateInstanceBootGroupReadinessRuleSuccess() { + UpdateInstanceBootGroupReadinessRuleCmd cmd = mock(UpdateInstanceBootGroupReadinessRuleCmd.class); + when(cmd.getId()).thenReturn(RULE_ID); + when(cmd.getName()).thenReturn("newname"); + when(cmd.getEnabled()).thenReturn(false); + when(cmd.getDetails()).thenReturn(null); + InstanceBootGroupReadinessRuleVO rule = newRule(RULE_ID, GROUP_ID, InstanceBootGroupMember.MemberType.VirtualMachine, VM_ID, + InstanceBootGroupReadinessRule.RuleType.Ping, true, "oldname"); + when(instanceBootGroupReadinessRuleDao.findById(RULE_ID)).thenReturn(rule); + InstanceBootGroupVO group = newGroup(GROUP_ID, "group1", ACCOUNT_ID); + when(instanceBootGroupDao.findById(GROUP_ID)).thenReturn(group); + InstanceBootGroupReadinessRule updated = mock(InstanceBootGroupReadinessRule.class); + when(instanceBootGroupReadinessRuleService.updateReadinessRule(RULE_ID, "newname", false, null)).thenReturn(updated); + + InstanceBootGroupReadinessRule result = service.updateInstanceBootGroupReadinessRule(cmd); + + assertEquals(updated, result); + } + + @Test(expected = InvalidParameterValueException.class) + public void testUpdateInstanceBootGroupReadinessRuleNotFoundThrows() { + UpdateInstanceBootGroupReadinessRuleCmd cmd = mock(UpdateInstanceBootGroupReadinessRuleCmd.class); + when(cmd.getId()).thenReturn(RULE_ID); + when(instanceBootGroupReadinessRuleDao.findById(RULE_ID)).thenReturn(null); + + service.updateInstanceBootGroupReadinessRule(cmd); + } + + @Test + public void testDeleteInstanceBootGroupReadinessRuleSuccess() { + DeleteInstanceBootGroupReadinessRuleCmd cmd = mock(DeleteInstanceBootGroupReadinessRuleCmd.class); + when(cmd.getId()).thenReturn(RULE_ID); + InstanceBootGroupReadinessRuleVO rule = newRule(RULE_ID, GROUP_ID, InstanceBootGroupMember.MemberType.VirtualMachine, VM_ID, + InstanceBootGroupReadinessRule.RuleType.Ping, true, "rule1"); + when(instanceBootGroupReadinessRuleDao.findById(RULE_ID)).thenReturn(rule); + InstanceBootGroupVO group = newGroup(GROUP_ID, "group1", ACCOUNT_ID); + when(instanceBootGroupDao.findById(GROUP_ID)).thenReturn(group); + when(instanceBootGroupReadinessRuleService.deleteReadinessRule(RULE_ID)).thenReturn(true); + + boolean result = service.deleteInstanceBootGroupReadinessRule(cmd); + + assertTrue(result); + } + + @Test(expected = InvalidParameterValueException.class) + public void testDeleteInstanceBootGroupReadinessRuleNotFoundThrows() { + DeleteInstanceBootGroupReadinessRuleCmd cmd = mock(DeleteInstanceBootGroupReadinessRuleCmd.class); + when(cmd.getId()).thenReturn(RULE_ID); + when(instanceBootGroupReadinessRuleDao.findById(RULE_ID)).thenReturn(null); + + service.deleteInstanceBootGroupReadinessRule(cmd); + } + + // ---------------------------------------------------------------- listInstanceBootGroupReadinessRules + + @Test + public void testListInstanceBootGroupReadinessRulesSurfacesInheritedRulesForVm() { + ListInstanceBootGroupReadinessRulesCmd cmd = mock(ListInstanceBootGroupReadinessRulesCmd.class); + when(cmd.getBootGroupId()).thenReturn(GROUP_ID); + when(cmd.getId()).thenReturn(null); + when(cmd.getVirtualMachineId()).thenReturn(VM_ID); + when(cmd.getInstanceGroupId()).thenReturn(null); + when(cmd.getRuleType()).thenReturn(null); + when(cmd.getKeyword()).thenReturn(null); + InstanceBootGroupVO group = newGroup(GROUP_ID, "group1", ACCOUNT_ID); + when(instanceBootGroupDao.findById(GROUP_ID)).thenReturn(group); + + InstanceBootGroupReadinessRuleVO directRule = newRule(RULE_ID, GROUP_ID, InstanceBootGroupMember.MemberType.VirtualMachine, VM_ID, + InstanceBootGroupReadinessRule.RuleType.GuestAgentLiveness, true, "direct-rule"); + when(instanceBootGroupReadinessRuleDao.searchAndCountByBootGroupId(eq(GROUP_ID), eq((Long) null), + eq(InstanceBootGroupMember.MemberType.VirtualMachine), eq(VM_ID), eq((InstanceBootGroupReadinessRule.RuleType) null), + any(), any(), any())) + .thenReturn(new com.cloud.utils.Pair<>(Collections.singletonList(directRule), 1)); + + InstanceBootGroupReadinessRuleVO inheritedRule = newRule(RULE_ID + 1, GROUP_ID, InstanceBootGroupMember.MemberType.InstanceGroup, INSTANCE_GROUP_ID, + InstanceBootGroupReadinessRule.RuleType.Ping, true, "inherited-rule"); + when(instanceBootGroupReadinessRuleService.findInheritedGroupRules(GROUP_ID, VM_ID)).thenReturn(Collections.singletonList(inheritedRule)); + + ListResponse response = service.listInstanceBootGroupReadinessRules(cmd); + + assertEquals(2, response.getResponses().size()); + assertEquals(Integer.valueOf(2), response.getCount()); + InstanceBootGroupReadinessRuleResponse directResponse = response.getResponses().get(0); + InstanceBootGroupReadinessRuleResponse inheritedResponse = response.getResponses().get(1); + assertEquals("false", field(directResponse, "inherited")); + assertEquals("true", field(inheritedResponse, "inherited")); + } + + @Test(expected = InvalidParameterValueException.class) + public void testListInstanceBootGroupReadinessRulesBothIdsSpecifiedThrows() { + ListInstanceBootGroupReadinessRulesCmd cmd = mock(ListInstanceBootGroupReadinessRulesCmd.class); + when(cmd.getBootGroupId()).thenReturn(GROUP_ID); + when(cmd.getVirtualMachineId()).thenReturn(VM_ID); + when(cmd.getInstanceGroupId()).thenReturn(INSTANCE_GROUP_ID); + InstanceBootGroupVO group = newGroup(GROUP_ID, "group1", ACCOUNT_ID); + when(instanceBootGroupDao.findById(GROUP_ID)).thenReturn(group); + + service.listInstanceBootGroupReadinessRules(cmd); + } + + @Test(expected = InvalidParameterValueException.class) + public void testListInstanceBootGroupReadinessRulesInvalidRuleTypeThrows() { + ListInstanceBootGroupReadinessRulesCmd cmd = mock(ListInstanceBootGroupReadinessRulesCmd.class); + when(cmd.getBootGroupId()).thenReturn(GROUP_ID); + when(cmd.getVirtualMachineId()).thenReturn(null); + when(cmd.getRuleType()).thenReturn("Bogus"); + InstanceBootGroupVO group = newGroup(GROUP_ID, "group1", ACCOUNT_ID); + when(instanceBootGroupDao.findById(GROUP_ID)).thenReturn(group); + + service.listInstanceBootGroupReadinessRules(cmd); + } +} diff --git a/server/src/test/java/org/apache/cloudstack/vm/bootgroup/InstanceBootGroupManagerImplTest.java b/server/src/test/java/org/apache/cloudstack/vm/bootgroup/InstanceBootGroupManagerImplTest.java new file mode 100644 index 000000000000..38ba0b5e8a7a --- /dev/null +++ b/server/src/test/java/org/apache/cloudstack/vm/bootgroup/InstanceBootGroupManagerImplTest.java @@ -0,0 +1,723 @@ +// 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.vm.bootgroup; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertTrue; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.anyLong; +import static org.mockito.ArgumentMatchers.anyString; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.never; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.verifyNoInteractions; +import static org.mockito.Mockito.when; + +import java.lang.reflect.Constructor; +import java.lang.reflect.Field; +import java.lang.reflect.InvocationTargetException; +import java.lang.reflect.Method; +import java.util.ArrayList; +import java.util.Collections; +import java.util.Date; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.Set; + +import org.apache.cloudstack.context.CallContext; +import org.apache.cloudstack.framework.config.ConfigKey; +import org.apache.cloudstack.vm.bootgroup.readiness.InstanceBootGroupReadinessRule; +import org.apache.cloudstack.vm.bootgroup.readiness.InstanceBootGroupReadinessRuleService; +import org.junit.After; +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.InOrder; +import org.mockito.InjectMocks; +import org.mockito.Mock; +import org.mockito.Mockito; +import org.mockito.junit.MockitoJUnitRunner; + +import com.cloud.user.Account; +import com.cloud.user.User; +import com.cloud.utils.db.EntityManager; +import com.cloud.utils.exception.CloudRuntimeException; +import com.cloud.vm.InstanceGroupVMMapVO; +import com.cloud.vm.InstanceGroupVO; +import com.cloud.vm.UserVmService; +import com.cloud.vm.UserVmVO; +import com.cloud.vm.VirtualMachine; +import com.cloud.vm.VirtualMachineManager; +import com.cloud.vm.dao.InstanceBootGroupDetailsDao; +import com.cloud.vm.dao.InstanceBootGroupMemberDao; +import com.cloud.vm.dao.InstanceGroupDao; +import com.cloud.vm.dao.InstanceGroupVMMapDao; +import com.cloud.vm.dao.UserVmDao; + +@RunWith(MockitoJUnitRunner.class) +public class InstanceBootGroupManagerImplTest { + + private static final long ACCOUNT_ID = 1L; + private static final long DOMAIN_ID = 1L; + private static final long GROUP_ID = 500L; + private static final long MEMBER_ID_1 = 601L; + private static final long MEMBER_ID_2 = 602L; + private static final long VM_ID_1 = 701L; + private static final long VM_ID_2 = 702L; + private static final long INSTANCE_GROUP_ID = 801L; + + @InjectMocks + InstanceBootGroupManagerImpl manager; + + @Mock + InstanceBootGroupMemberDao instanceBootGroupMemberDao; + @Mock + UserVmService userVmService; + @Mock + UserVmDao userVmDao; + @Mock + InstanceGroupDao instanceGroupDao; + @Mock + InstanceGroupVMMapDao instanceGroupVMMapDao; + @Mock + VirtualMachineManager virtualMachineManager; + @Mock + InstanceBootGroupReadinessRuleService instanceBootGroupReadinessRuleService; + @Mock + InstanceBootGroupDetailsDao instanceBootGroupDetailsDao; + + private static final long CALLER_USER_ID = 2L; + private static final long CALLER_ACCOUNT_ID = 3L; + + private static final Class VM_PROGRESS_CLASS; + + static { + try { + VM_PROGRESS_CLASS = Class.forName(InstanceBootGroupManagerImpl.class.getName() + "$VmProgress"); + } catch (ClassNotFoundException e) { + throw new ExceptionInInitializerError(e); + } + } + + // CallContext orchestration (runTierConcurrently / waitForTierReady) runs its per-VM actions on + // real pooled threads, so a MockedStatic (thread-confined) can't be used here — the + // worker threads would fall through to the real static methods and NPE on the uninitialized + // entity manager. Instead we back CallContext with a minimal real registration, resolvable from + // any thread via its normal ThreadLocal-based mechanism. + @Before + public void setUp() { + EntityManager entityManager = mock(EntityManager.class); + Account callerAccount = mock(Account.class); + when(callerAccount.getId()).thenReturn(CALLER_ACCOUNT_ID); + User callerUser = mock(User.class); + when(callerUser.getId()).thenReturn(CALLER_USER_ID); + when(entityManager.findById(eq(Account.class), any(Long.class))).thenReturn(callerAccount); + when(entityManager.findById(eq(User.class), any(Long.class))).thenReturn(callerUser); + CallContext.init(entityManager); + CallContext.register(callerUser, callerAccount); + } + + @After + public void tearDown() { + CallContext.unregisterAll(); + } + + // + // Helpers + // + + private InstanceBootGroupVO newGroup(long id, String name) { + InstanceBootGroupVO group = Mockito.spy(new InstanceBootGroupVO(name, "desc", ACCOUNT_ID, DOMAIN_ID)); + Mockito.lenient().doReturn(id).when(group).getId(); + return group; + } + + private InstanceBootGroupMemberVO newMember(long id, long bootGroupId, InstanceBootGroupMember.MemberType type, long memberId, int order) { + InstanceBootGroupMemberVO member = Mockito.spy(new InstanceBootGroupMemberVO(bootGroupId, type, memberId, order)); + Mockito.lenient().doReturn(id).when(member).getId(); + return member; + } + + private Object invokePrivate(String name, Class[] paramTypes, Object... args) throws Exception { + Method m = InstanceBootGroupManagerImpl.class.getDeclaredMethod(name, paramTypes); + m.setAccessible(true); + try { + return m.invoke(manager, args); + } catch (InvocationTargetException e) { + if (e.getCause() instanceof Exception) { + throw (Exception) e.getCause(); + } + throw e; + } + } + + private Object newVmProgress(long vmId, Long memberId) throws Exception { + Constructor ctor = VM_PROGRESS_CLASS.getDeclaredConstructor(long.class, Long.class); + ctor.setAccessible(true); + return ctor.newInstance(vmId, memberId); + } + + private void setProgressField(Object progress, String field, Object value) throws Exception { + Field f = VM_PROGRESS_CLASS.getDeclaredField(field); + f.setAccessible(true); + f.set(progress, value); + } + + private Object getProgressField(Object progress, String field) throws Exception { + Field f = VM_PROGRESS_CLASS.getDeclaredField(field); + f.setAccessible(true); + return f.get(progress); + } + + // + // effective* config-override resolution + // + + @Test + public void testEffectiveTimeoutSecondsUsesOverrideWhenPresent() throws Exception { + InstanceBootGroupVO group = newGroup(GROUP_ID, "group1"); + when(instanceBootGroupDetailsDao.getDetail(GROUP_ID, InstanceBootGroupManagerImpl.ReadinessAttemptTimeoutSeconds.key())) + .thenReturn("120"); + long result = (Long) invokePrivate("effectiveTimeoutSeconds", new Class[]{InstanceBootGroupVO.class}, group); + assertEquals(120L, result); + } + + @Test + public void testEffectiveTimeoutSecondsFallsBackToConfigDefault() throws Exception { + InstanceBootGroupVO group = newGroup(GROUP_ID, "group1"); + when(instanceBootGroupDetailsDao.getDetail(GROUP_ID, InstanceBootGroupManagerImpl.ReadinessAttemptTimeoutSeconds.key())) + .thenReturn(null); + long result = (Long) invokePrivate("effectiveTimeoutSeconds", new Class[]{InstanceBootGroupVO.class}, group); + assertEquals(300L, result); + } + + @Test + public void testEffectiveMaxRetryAttemptsUsesOverrideWhenPresent() throws Exception { + InstanceBootGroupVO group = newGroup(GROUP_ID, "group1"); + when(instanceBootGroupDetailsDao.getDetail(GROUP_ID, InstanceBootGroupManagerImpl.ReadinessMaxRetryAttempts.key())) + .thenReturn("2"); + long result = (Long) invokePrivate("effectiveMaxRetryAttempts", new Class[]{InstanceBootGroupVO.class}, group); + assertEquals(2L, result); + } + + @Test + public void testEffectiveMaxRetryAttemptsFallsBackToConfigDefault() throws Exception { + InstanceBootGroupVO group = newGroup(GROUP_ID, "group1"); + long result = (Long) invokePrivate("effectiveMaxRetryAttempts", new Class[]{InstanceBootGroupVO.class}, group); + assertEquals(5L, result); + } + + @Test + public void testEffectiveInitialDelaySecondsUsesOverrideWhenPresent() throws Exception { + InstanceBootGroupVO group = newGroup(GROUP_ID, "group1"); + when(instanceBootGroupDetailsDao.getDetail(GROUP_ID, InstanceBootGroupManagerImpl.ReadinessInitialDelaySeconds.key())) + .thenReturn("5"); + long result = (Long) invokePrivate("effectiveInitialDelaySeconds", new Class[]{InstanceBootGroupVO.class}, group); + assertEquals(5L, result); + } + + @Test + public void testEffectiveInitialDelaySecondsFallsBackToConfigDefault() throws Exception { + InstanceBootGroupVO group = newGroup(GROUP_ID, "group1"); + long result = (Long) invokePrivate("effectiveInitialDelaySeconds", new Class[]{InstanceBootGroupVO.class}, group); + assertEquals(30L, result); + } + + @Test + public void testEffectiveRebootOnRetryUsesOverrideWhenPresent() throws Exception { + InstanceBootGroupVO group = newGroup(GROUP_ID, "group1"); + when(instanceBootGroupDetailsDao.getDetail(GROUP_ID, InstanceBootGroupManagerImpl.ReadinessRebootOnRetry.key())) + .thenReturn("true"); + boolean result = (Boolean) invokePrivate("effectiveRebootOnRetry", new Class[]{InstanceBootGroupVO.class}, group); + assertTrue(result); + } + + @Test + public void testEffectiveRebootOnRetryFallsBackToConfigDefault() throws Exception { + InstanceBootGroupVO group = newGroup(GROUP_ID, "group1"); + boolean result = (Boolean) invokePrivate("effectiveRebootOnRetry", new Class[]{InstanceBootGroupVO.class}, group); + assertFalse(result); + } + + @Test + public void testEffectivePollIntervalSecondsIsGlobalOnly() throws Exception { + long result = (Long) invokePrivate("effectivePollIntervalSeconds", new Class[]{}); + assertEquals(10L, result); + verifyNoInteractions(instanceBootGroupDetailsDao); + } + + @Test + public void testEffectiveReadinessCheckConcurrencyIsGlobalOnly() throws Exception { + long result = (Long) invokePrivate("effectiveReadinessCheckConcurrency", new Class[]{}); + assertEquals(10L, result); + verifyNoInteractions(instanceBootGroupDetailsDao); + } + + // + // Tier grouping + // + + @Test + @SuppressWarnings("unchecked") + public void testGroupByOrderGroupsAndSortsAscending() throws Exception { + InstanceBootGroupMemberVO m3 = newMember(1L, GROUP_ID, InstanceBootGroupMember.MemberType.VirtualMachine, 901L, 3); + InstanceBootGroupMemberVO m1a = newMember(2L, GROUP_ID, InstanceBootGroupMember.MemberType.VirtualMachine, 902L, 1); + InstanceBootGroupMemberVO m2 = newMember(3L, GROUP_ID, InstanceBootGroupMember.MemberType.VirtualMachine, 903L, 2); + InstanceBootGroupMemberVO m1b = newMember(4L, GROUP_ID, InstanceBootGroupMember.MemberType.VirtualMachine, 904L, 1); + List members = List.of(m3, m1a, m2, m1b); + + Map> tiers = (Map>) + invokePrivate("groupByOrder", new Class[]{List.class}, members); + + assertEquals(List.of(1, 2, 3), new ArrayList<>(tiers.keySet())); + assertEquals(2, tiers.get(1).size()); + assertEquals(1, tiers.get(2).size()); + assertEquals(1, tiers.get(3).size()); + } + + @Test + @SuppressWarnings("unchecked") + public void testGroupByOrderDescendingSortsDescending() throws Exception { + InstanceBootGroupMemberVO m3 = newMember(1L, GROUP_ID, InstanceBootGroupMember.MemberType.VirtualMachine, 901L, 3); + InstanceBootGroupMemberVO m1 = newMember(2L, GROUP_ID, InstanceBootGroupMember.MemberType.VirtualMachine, 902L, 1); + InstanceBootGroupMemberVO m2 = newMember(3L, GROUP_ID, InstanceBootGroupMember.MemberType.VirtualMachine, 903L, 2); + List members = List.of(m3, m1, m2); + + Map> tiers = (Map>) + invokePrivate("groupByOrderDescending", new Class[]{List.class}, members); + + assertEquals(List.of(3, 2, 1), new ArrayList<>(tiers.keySet())); + } + + // + // resolveVmIds + // + + @Test + @SuppressWarnings("unchecked") + public void testResolveVmIdsDirectVmMember() throws Exception { + InstanceBootGroupMemberVO member = newMember(MEMBER_ID_1, GROUP_ID, InstanceBootGroupMember.MemberType.VirtualMachine, VM_ID_1, 1); + List ids = (List) invokePrivate("resolveVmIds", new Class[]{List.class}, List.of(member)); + assertEquals(List.of(VM_ID_1), ids); + verifyNoInteractions(instanceGroupVMMapDao); + } + + @Test + @SuppressWarnings("unchecked") + public void testResolveVmIdsInstanceGroupMemberExpandsToGroupVms() throws Exception { + InstanceBootGroupMemberVO member = newMember(MEMBER_ID_1, GROUP_ID, InstanceBootGroupMember.MemberType.InstanceGroup, INSTANCE_GROUP_ID, 1); + InstanceGroupVMMapVO map1 = new InstanceGroupVMMapVO(INSTANCE_GROUP_ID, VM_ID_1); + InstanceGroupVMMapVO map2 = new InstanceGroupVMMapVO(INSTANCE_GROUP_ID, VM_ID_2); + when(instanceGroupVMMapDao.listByGroupId(INSTANCE_GROUP_ID)).thenReturn(List.of(map1, map2)); + + List ids = (List) invokePrivate("resolveVmIds", new Class[]{List.class}, List.of(member)); + assertEquals(List.of(VM_ID_1, VM_ID_2), ids); + } + + @Test + @SuppressWarnings("unchecked") + public void testResolveVmIdsMixedMembers() throws Exception { + InstanceBootGroupMemberVO vmMember = newMember(MEMBER_ID_1, GROUP_ID, InstanceBootGroupMember.MemberType.VirtualMachine, VM_ID_1, 1); + InstanceBootGroupMemberVO groupMember = newMember(MEMBER_ID_2, GROUP_ID, InstanceBootGroupMember.MemberType.InstanceGroup, INSTANCE_GROUP_ID, 2); + InstanceGroupVMMapVO map1 = new InstanceGroupVMMapVO(INSTANCE_GROUP_ID, VM_ID_2); + when(instanceGroupVMMapDao.listByGroupId(INSTANCE_GROUP_ID)).thenReturn(List.of(map1)); + + List ids = (List) invokePrivate("resolveVmIds", new Class[]{List.class}, List.of(vmMember, groupMember)); + assertEquals(List.of(VM_ID_1, VM_ID_2), ids); + } + + // + // anchorInitialDelay + // + + @Test + public void testAnchorInitialDelayNotAlreadyRunningAnchorsToNow() throws Exception { + InstanceBootGroupVO group = newGroup(GROUP_ID, "group1"); + Object progress = newVmProgress(VM_ID_1, MEMBER_ID_1); + UserVmVO vm = mock(UserVmVO.class); + + long before = System.currentTimeMillis(); + invokePrivate("anchorInitialDelay", new Class[]{InstanceBootGroupVO.class, VM_PROGRESS_CLASS, UserVmVO.class, boolean.class}, + group, progress, vm, false); + long after = System.currentTimeMillis(); + + long entered = (Long) getProgressField(progress, "enteredWaitAtMs"); + long lastBooted = (Long) getProgressField(progress, "lastBootedAtMs"); + assertTrue(entered >= before && entered <= after); + assertEquals(entered, lastBooted); + } + + @Test + public void testAnchorInitialDelayAlreadyRunningUsesPowerStateUpdateTime() throws Exception { + InstanceBootGroupVO group = newGroup(GROUP_ID, "group1"); + Object progress = newVmProgress(VM_ID_1, MEMBER_ID_1); + UserVmVO vm = mock(UserVmVO.class); + Date powerStateTime = new Date(System.currentTimeMillis() - 5000); + when(vm.getPowerStateUpdateTime()).thenReturn(powerStateTime); + + invokePrivate("anchorInitialDelay", new Class[]{InstanceBootGroupVO.class, VM_PROGRESS_CLASS, UserVmVO.class, boolean.class}, + group, progress, vm, true); + + long lastBooted = (Long) getProgressField(progress, "lastBootedAtMs"); + assertEquals(powerStateTime.getTime(), lastBooted); + } + + @Test + public void testAnchorInitialDelayAlreadyRunningWithNullPowerStateFallsBackToNow() throws Exception { + InstanceBootGroupVO group = newGroup(GROUP_ID, "group1"); + Object progress = newVmProgress(VM_ID_1, MEMBER_ID_1); + UserVmVO vm = mock(UserVmVO.class); + when(vm.getPowerStateUpdateTime()).thenReturn(null); + + long before = System.currentTimeMillis(); + invokePrivate("anchorInitialDelay", new Class[]{InstanceBootGroupVO.class, VM_PROGRESS_CLASS, UserVmVO.class, boolean.class}, + group, progress, vm, true); + long after = System.currentTimeMillis(); + + long lastBooted = (Long) getProgressField(progress, "lastBootedAtMs"); + assertTrue(lastBooted >= before && lastBooted <= after); + } + + // + // halt + // + + @Test + public void testHaltDoesNotThrow() throws Exception { + InstanceBootGroupVO group = newGroup(GROUP_ID, "group1"); + invokePrivate("halt", new Class[]{InstanceBootGroupVO.class, String.class}, group, "some reason"); + } + + // + // checkInstanceGroupMembersReady + // + + private static final Class[] CHECK_GROUP_MEMBERS_READY_PARAMS = + new Class[]{InstanceBootGroupVO.class, List.class, Map.class, Map.class}; + + @Test + public void testCheckInstanceGroupMembersReadyEmptyTierMembersIsVacuouslySettled() throws Exception { + InstanceBootGroupVO group = newGroup(GROUP_ID, "group1"); + Map progressByVmId = new HashMap<>(); + Map membersReadyStatus = new HashMap<>(); + + invokePrivate("checkInstanceGroupMembersReady", CHECK_GROUP_MEMBERS_READY_PARAMS, + group, Collections.emptyList(), progressByVmId, membersReadyStatus); + + verifyNoInteractions(instanceGroupDao); + verifyNoInteractions(instanceBootGroupReadinessRuleService); + assertTrue(membersReadyStatus.isEmpty()); + } + + @Test + public void testCheckInstanceGroupMembersReadySkipsAlreadyReadyMember() throws Exception { + InstanceBootGroupVO group = newGroup(GROUP_ID, "group1"); + InstanceBootGroupMemberVO groupMember = newMember(MEMBER_ID_1, GROUP_ID, InstanceBootGroupMember.MemberType.InstanceGroup, INSTANCE_GROUP_ID, 1); + Map progressByVmId = new HashMap<>(); + Map membersReadyStatus = new HashMap<>(); + membersReadyStatus.put(MEMBER_ID_1, true); + + invokePrivate("checkInstanceGroupMembersReady", CHECK_GROUP_MEMBERS_READY_PARAMS, + group, List.of(groupMember), progressByVmId, membersReadyStatus); + + verifyNoInteractions(instanceGroupDao); + verifyNoInteractions(instanceBootGroupReadinessRuleService); + } + + @Test + public void testCheckInstanceGroupMembersReadySkipsWhenMembersNotSettledYet() throws Exception { + InstanceBootGroupVO group = newGroup(GROUP_ID, "group1"); + InstanceBootGroupMemberVO groupMember = newMember(MEMBER_ID_1, GROUP_ID, InstanceBootGroupMember.MemberType.InstanceGroup, INSTANCE_GROUP_ID, 1); + Object progress = newVmProgress(VM_ID_1, MEMBER_ID_1); // ready=false, gaveUp=false + + Map progressByVmId = new HashMap<>(); + progressByVmId.put(VM_ID_1, progress); + Map membersReadyStatus = new HashMap<>(); + membersReadyStatus.put(MEMBER_ID_1, false); + when(instanceGroupDao.findById(INSTANCE_GROUP_ID)).thenReturn(mock(InstanceGroupVO.class)); + + invokePrivate("checkInstanceGroupMembersReady", CHECK_GROUP_MEMBERS_READY_PARAMS, + group, List.of(groupMember), progressByVmId, membersReadyStatus); + + verifyNoInteractions(instanceBootGroupReadinessRuleService); + assertFalse(membersReadyStatus.get(MEMBER_ID_1)); + } + + @Test + public void testCheckInstanceGroupMembersReadyEvaluatesImmediatelyWhenNoMatchingVmProgress() throws Exception { + InstanceBootGroupVO group = newGroup(GROUP_ID, "group1"); + InstanceBootGroupMemberVO groupMember = newMember(MEMBER_ID_1, GROUP_ID, InstanceBootGroupMember.MemberType.InstanceGroup, INSTANCE_GROUP_ID, 1); + when(instanceGroupDao.findById(INSTANCE_GROUP_ID)).thenReturn(mock(InstanceGroupVO.class)); + when(instanceBootGroupReadinessRuleService.evaluateInstanceGroupReadiness(GROUP_ID, INSTANCE_GROUP_ID, Collections.emptySet())) + .thenReturn(InstanceBootGroupReadinessRule.Status.Ready); + + Map progressByVmId = new HashMap<>(); // empty: no progress references this member + Map membersReadyStatus = new HashMap<>(); + membersReadyStatus.put(MEMBER_ID_1, false); + + invokePrivate("checkInstanceGroupMembersReady", CHECK_GROUP_MEMBERS_READY_PARAMS, + group, List.of(groupMember), progressByVmId, membersReadyStatus); + + assertTrue(membersReadyStatus.get(MEMBER_ID_1)); + verify(instanceBootGroupReadinessRuleService).evaluateInstanceGroupReadiness(GROUP_ID, INSTANCE_GROUP_ID, Collections.emptySet()); + } + + @Test + public void testCheckInstanceGroupMembersReadyPassesGaveUpVmIdsAsPermanentlyFailedAndMarksReady() throws Exception { + InstanceBootGroupVO group = newGroup(GROUP_ID, "group1"); + InstanceBootGroupMemberVO groupMember = newMember(MEMBER_ID_1, GROUP_ID, InstanceBootGroupMember.MemberType.InstanceGroup, INSTANCE_GROUP_ID, 1); + when(instanceGroupDao.findById(INSTANCE_GROUP_ID)).thenReturn(mock(InstanceGroupVO.class)); + + Object progress1 = newVmProgress(VM_ID_1, MEMBER_ID_1); + setProgressField(progress1, "gaveUp", true); + Object progress2 = newVmProgress(VM_ID_2, MEMBER_ID_1); + setProgressField(progress2, "ready", true); + + Map progressByVmId = new HashMap<>(); + progressByVmId.put(VM_ID_1, progress1); + progressByVmId.put(VM_ID_2, progress2); + Map membersReadyStatus = new HashMap<>(); + membersReadyStatus.put(MEMBER_ID_1, false); + + when(instanceBootGroupReadinessRuleService.evaluateInstanceGroupReadiness(GROUP_ID, INSTANCE_GROUP_ID, Set.of(VM_ID_1))) + .thenReturn(InstanceBootGroupReadinessRule.Status.Ready); + + invokePrivate("checkInstanceGroupMembersReady", CHECK_GROUP_MEMBERS_READY_PARAMS, + group, List.of(groupMember), progressByVmId, membersReadyStatus); + + assertTrue(membersReadyStatus.get(MEMBER_ID_1)); + } + + @Test(expected = CloudRuntimeException.class) + public void testCheckInstanceGroupMembersReadyHaltsAndThrowsOnErrorStatus() throws Exception { + InstanceBootGroupVO group = newGroup(GROUP_ID, "group1"); + InstanceBootGroupMemberVO groupMember = newMember(MEMBER_ID_1, GROUP_ID, InstanceBootGroupMember.MemberType.InstanceGroup, INSTANCE_GROUP_ID, 1); + InstanceGroupVO ig = mock(InstanceGroupVO.class); + when(ig.getName()).thenReturn("grp"); + when(instanceGroupDao.findById(INSTANCE_GROUP_ID)).thenReturn(ig); + when(instanceBootGroupReadinessRuleService.evaluateInstanceGroupReadiness(eq(GROUP_ID), eq(INSTANCE_GROUP_ID), any())) + .thenReturn(InstanceBootGroupReadinessRule.Status.Error); + + Map progressByVmId = new HashMap<>(); + Map membersReadyStatus = new HashMap<>(); + membersReadyStatus.put(MEMBER_ID_1, false); + + invokePrivate("checkInstanceGroupMembersReady", CHECK_GROUP_MEMBERS_READY_PARAMS, + group, List.of(groupMember), progressByVmId, membersReadyStatus); + } + + @Test(expected = CloudRuntimeException.class) + public void testCheckInstanceGroupMembersReadyHaltsAndThrowsWhenNotReadyAndAllMembersSettled() throws Exception { + InstanceBootGroupVO group = newGroup(GROUP_ID, "group1"); + InstanceBootGroupMemberVO groupMember = newMember(MEMBER_ID_1, GROUP_ID, InstanceBootGroupMember.MemberType.InstanceGroup, INSTANCE_GROUP_ID, 1); + InstanceGroupVO ig = mock(InstanceGroupVO.class); + when(ig.getName()).thenReturn("grp"); + when(instanceGroupDao.findById(INSTANCE_GROUP_ID)).thenReturn(ig); + + Object progress1 = newVmProgress(VM_ID_1, MEMBER_ID_1); + setProgressField(progress1, "ready", true); + Map progressByVmId = new HashMap<>(); + progressByVmId.put(VM_ID_1, progress1); + Map membersReadyStatus = new HashMap<>(); + membersReadyStatus.put(MEMBER_ID_1, false); + + when(instanceBootGroupReadinessRuleService.evaluateInstanceGroupReadiness(eq(GROUP_ID), eq(INSTANCE_GROUP_ID), any())) + .thenReturn(InstanceBootGroupReadinessRule.Status.NotReady); + + invokePrivate("checkInstanceGroupMembersReady", CHECK_GROUP_MEMBERS_READY_PARAMS, + group, List.of(groupMember), progressByVmId, membersReadyStatus); + } + + // + // startInstanceBootGroup / stopInstanceBootGroup / rebootInstanceBootGroup — small fast end-to-end flows + // + + @Test + public void testStartInstanceBootGroupSingleTierSingleVmReadyImmediately() throws Exception { + InstanceBootGroupVO group = newGroup(GROUP_ID, "group1"); + InstanceBootGroupMemberVO member = newMember(MEMBER_ID_1, GROUP_ID, InstanceBootGroupMember.MemberType.VirtualMachine, VM_ID_1, 1); + when(instanceBootGroupMemberDao.listByBootGroupId(GROUP_ID)).thenReturn(List.of(member)); + + UserVmVO vm = mock(UserVmVO.class); + when(vm.getState()).thenReturn(VirtualMachine.State.Stopped); + when(userVmDao.findById(VM_ID_1)).thenReturn(vm); + + when(instanceBootGroupDetailsDao.getDetail(eq(GROUP_ID), eq(InstanceBootGroupManagerImpl.ReadinessInitialDelaySeconds.key()))) + .thenReturn("0"); + when(instanceBootGroupReadinessRuleService.evaluateVmReadiness(eq(GROUP_ID), eq(VM_ID_1), anyLong(), anyString())) + .thenReturn(InstanceBootGroupReadinessRule.Status.Ready); + + manager.startInstanceBootGroup(group); + + verify(userVmService).startVirtualMachine(vm, null); + verify(instanceBootGroupReadinessRuleService, Mockito.atLeastOnce()) + .evaluateVmReadiness(eq(GROUP_ID), eq(VM_ID_1), anyLong(), anyString()); + } + + @Test + public void testStartInstanceBootGroupAlreadyRunningVmSkipsStartCall() throws Exception { + InstanceBootGroupVO group = newGroup(GROUP_ID, "group1"); + InstanceBootGroupMemberVO member = newMember(MEMBER_ID_1, GROUP_ID, InstanceBootGroupMember.MemberType.VirtualMachine, VM_ID_1, 1); + when(instanceBootGroupMemberDao.listByBootGroupId(GROUP_ID)).thenReturn(List.of(member)); + + UserVmVO vm = mock(UserVmVO.class); + when(vm.getState()).thenReturn(VirtualMachine.State.Running); + when(vm.getPowerStateUpdateTime()).thenReturn(new Date(System.currentTimeMillis() - 1000)); + when(userVmDao.findById(VM_ID_1)).thenReturn(vm); + + when(instanceBootGroupDetailsDao.getDetail(eq(GROUP_ID), eq(InstanceBootGroupManagerImpl.ReadinessInitialDelaySeconds.key()))) + .thenReturn("0"); + when(instanceBootGroupReadinessRuleService.evaluateVmReadiness(eq(GROUP_ID), eq(VM_ID_1), anyLong(), anyString())) + .thenReturn(InstanceBootGroupReadinessRule.Status.Ready); + + manager.startInstanceBootGroup(group); + + verify(userVmService, never()).startVirtualMachine(any(), any()); + } + + @Test(expected = CloudRuntimeException.class) + public void testStartInstanceBootGroupHaltsOnExhaustedRetriesAndSkipsLaterTiers() throws Throwable { + InstanceBootGroupVO group = newGroup(GROUP_ID, "group1"); + InstanceBootGroupMemberVO member1 = newMember(MEMBER_ID_1, GROUP_ID, InstanceBootGroupMember.MemberType.VirtualMachine, VM_ID_1, 1); + InstanceBootGroupMemberVO member2 = newMember(MEMBER_ID_2, GROUP_ID, InstanceBootGroupMember.MemberType.VirtualMachine, VM_ID_2, 2); + when(instanceBootGroupMemberDao.listByBootGroupId(GROUP_ID)).thenReturn(List.of(member1, member2)); + + UserVmVO vm1 = mock(UserVmVO.class); + when(vm1.getState()).thenReturn(VirtualMachine.State.Stopped); + when(vm1.getName()).thenReturn("vm1"); + when(userVmDao.findById(VM_ID_1)).thenReturn(vm1); + + when(instanceBootGroupDetailsDao.getDetail(eq(GROUP_ID), eq(InstanceBootGroupManagerImpl.ReadinessInitialDelaySeconds.key()))) + .thenReturn("0"); + when(instanceBootGroupDetailsDao.getDetail(eq(GROUP_ID), eq(InstanceBootGroupManagerImpl.ReadinessMaxRetryAttempts.key()))) + .thenReturn("0"); + when(instanceBootGroupReadinessRuleService.evaluateVmReadiness(eq(GROUP_ID), eq(VM_ID_1), anyLong(), anyString())) + .thenReturn(InstanceBootGroupReadinessRule.Status.NotReady); + + try { + manager.startInstanceBootGroup(group); + } finally { + // Only vm1 (tier 1) is ever started; tier 2 (vm2) is never reached because tier 1 halts. + verify(userVmService, Mockito.times(1)).startVirtualMachine(any(), any()); + verify(userVmService).startVirtualMachine(vm1, null); + verify(userVmDao, never()).findById(VM_ID_2); + } + } + + @Test + public void testStopInstanceBootGroupStopsTiersInReverseOrder() { + InstanceBootGroupVO group = newGroup(GROUP_ID, "group1"); + InstanceBootGroupMemberVO member1 = newMember(MEMBER_ID_1, GROUP_ID, InstanceBootGroupMember.MemberType.VirtualMachine, VM_ID_1, 1); + InstanceBootGroupMemberVO member2 = newMember(MEMBER_ID_2, GROUP_ID, InstanceBootGroupMember.MemberType.VirtualMachine, VM_ID_2, 2); + when(instanceBootGroupMemberDao.listByBootGroupId(GROUP_ID)).thenReturn(List.of(member1, member2)); + + UserVmVO vm1 = mock(UserVmVO.class); + when(vm1.getState()).thenReturn(VirtualMachine.State.Running); + when(userVmDao.findById(VM_ID_1)).thenReturn(vm1); + + UserVmVO vm2 = mock(UserVmVO.class); + when(vm2.getState()).thenReturn(VirtualMachine.State.Running); + when(userVmDao.findById(VM_ID_2)).thenReturn(vm2); + + manager.stopInstanceBootGroup(group, false); + + InOrder inOrder = Mockito.inOrder(userVmService); + inOrder.verify(userVmService).stopVirtualMachine(VM_ID_2, false); + inOrder.verify(userVmService).stopVirtualMachine(VM_ID_1, false); + } + + @Test + public void testStopInstanceBootGroupSkipsAlreadyStoppedVm() { + InstanceBootGroupVO group = newGroup(GROUP_ID, "group1"); + InstanceBootGroupMemberVO member = newMember(MEMBER_ID_1, GROUP_ID, InstanceBootGroupMember.MemberType.VirtualMachine, VM_ID_1, 1); + when(instanceBootGroupMemberDao.listByBootGroupId(GROUP_ID)).thenReturn(List.of(member)); + + UserVmVO vm = mock(UserVmVO.class); + when(vm.getState()).thenReturn(VirtualMachine.State.Stopped); + when(userVmDao.findById(VM_ID_1)).thenReturn(vm); + + manager.stopInstanceBootGroup(group, false); + + verify(userVmService, never()).stopVirtualMachine(anyLong(), any(Boolean.class)); + } + + @Test + public void testStopInstanceBootGroupPropagatesForcedFlag() { + InstanceBootGroupVO group = newGroup(GROUP_ID, "group1"); + InstanceBootGroupMemberVO member = newMember(MEMBER_ID_1, GROUP_ID, InstanceBootGroupMember.MemberType.VirtualMachine, VM_ID_1, 1); + when(instanceBootGroupMemberDao.listByBootGroupId(GROUP_ID)).thenReturn(List.of(member)); + + UserVmVO vm = mock(UserVmVO.class); + when(vm.getState()).thenReturn(VirtualMachine.State.Running); + when(userVmDao.findById(VM_ID_1)).thenReturn(vm); + + manager.stopInstanceBootGroup(group, true); + + verify(userVmService).stopVirtualMachine(VM_ID_1, true); + } + + @Test + public void testRebootInstanceBootGroupCallsStopThenStart() { + InstanceBootGroupVO group = newGroup(GROUP_ID, "group1"); + InstanceBootGroupManagerImpl spyManager = Mockito.spy(manager); + Mockito.doNothing().when(spyManager).stopInstanceBootGroup(group, false); + Mockito.doNothing().when(spyManager).startInstanceBootGroup(group); + + spyManager.rebootInstanceBootGroup(group, false); + + InOrder inOrder = Mockito.inOrder(spyManager); + inOrder.verify(spyManager).stopInstanceBootGroup(group, false); + inOrder.verify(spyManager).startInstanceBootGroup(group); + } + + @Test + public void testRebootInstanceBootGroupPropagatesForcedFlagToStop() { + InstanceBootGroupVO group = newGroup(GROUP_ID, "group1"); + InstanceBootGroupManagerImpl spyManager = Mockito.spy(manager); + Mockito.doNothing().when(spyManager).stopInstanceBootGroup(group, true); + Mockito.doNothing().when(spyManager).startInstanceBootGroup(group); + + spyManager.rebootInstanceBootGroup(group, true); + + verify(spyManager).stopInstanceBootGroup(group, true); + verify(spyManager, never()).stopInstanceBootGroup(group, false); + } + + // + // Configurable / ManagerBase plumbing + // + + @Test + public void testGetConfigKeysReturnsAllKeys() { + ConfigKey[] keys = manager.getConfigKeys(); + assertEquals(7, keys.length); + } + + @Test + public void testGetConfigComponentName() { + assertEquals("InstanceBootGroupManagerImpl", manager.getConfigComponentName()); + } + + @Test + public void testConfigureReturnsTrue() throws Exception { + assertTrue(manager.configure("InstanceBootGroupManagerImpl", Collections.emptyMap())); + } +} diff --git a/server/src/test/java/org/apache/cloudstack/vm/bootgroup/InstanceBootGroupMembershipGuardTest.java b/server/src/test/java/org/apache/cloudstack/vm/bootgroup/InstanceBootGroupMembershipGuardTest.java new file mode 100644 index 000000000000..d24bd2e4e53f --- /dev/null +++ b/server/src/test/java/org/apache/cloudstack/vm/bootgroup/InstanceBootGroupMembershipGuardTest.java @@ -0,0 +1,160 @@ +// 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.vm.bootgroup; + +import static org.junit.Assert.assertThrows; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + +import java.util.Arrays; +import java.util.Collections; + +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.InjectMocks; +import org.mockito.Mock; +import org.mockito.junit.MockitoJUnitRunner; + +import com.cloud.exception.InvalidParameterValueException; +import com.cloud.network.as.dao.AutoScaleVmGroupVmMapDao; +import com.cloud.storage.Storage; +import com.cloud.storage.VMTemplateVO; +import com.cloud.storage.dao.VMTemplateDao; +import com.cloud.vm.InstanceGroupVMMapVO; +import com.cloud.vm.UserVmVO; +import com.cloud.vm.dao.InstanceBootGroupMemberDao; +import com.cloud.vm.dao.InstanceGroupVMMapDao; +import com.cloud.vm.dao.UserVmDao; + +@RunWith(MockitoJUnitRunner.class) +public class InstanceBootGroupMembershipGuardTest { + + private static final long VM_ID = 100L; + private static final long TEMPLATE_ID = 200L; + private static final long FIRST_GROUP_ID = 10L; + private static final long SECOND_GROUP_ID = 20L; + + @InjectMocks + InstanceBootGroupMembershipGuard guard; + + @Mock + UserVmDao userVmDao; + + @Mock + VMTemplateDao templateDao; + + @Mock + AutoScaleVmGroupVmMapDao autoScaleVmGroupVmMapDao; + + @Mock + InstanceBootGroupMemberDao instanceBootGroupMemberDao; + + @Mock + InstanceGroupVMMapDao instanceGroupVMMapDao; + + @Mock + UserVmVO vm; + + @Before + public void setUp() { + when(userVmDao.findById(VM_ID)).thenReturn(vm); + when(vm.getTemplateId()).thenReturn(TEMPLATE_ID); + when(templateDao.findByIdIncludingRemoved(TEMPLATE_ID)).thenReturn(null); + when(autoScaleVmGroupVmMapDao.listByVm(VM_ID)).thenReturn(Collections.emptyList()); + when(instanceBootGroupMemberDao.findByMember(InstanceBootGroupMember.MemberType.VirtualMachine, VM_ID)).thenReturn(null); + } + + @Test + public void testVmNotFoundThrows() { + when(userVmDao.findById(VM_ID)).thenReturn(null); + assertThrows(InvalidParameterValueException.class, () -> guard.validateVmEligibleForGroupMembership(VM_ID)); + } + + @Test + public void testVnfTemplateThrows() { + VMTemplateVO template = mock(VMTemplateVO.class); + when(template.getTemplateType()).thenReturn(Storage.TemplateType.VNF); + when(templateDao.findByIdIncludingRemoved(TEMPLATE_ID)).thenReturn(template); + + assertThrows(InvalidParameterValueException.class, () -> guard.validateVmEligibleForGroupMembership(VM_ID)); + } + + @Test + public void testInAutoScaleGroupThrows() { + when(autoScaleVmGroupVmMapDao.listByVm(VM_ID)).thenReturn(Collections.singletonList(mock(com.cloud.network.as.AutoScaleVmGroupVmMapVO.class))); + assertThrows(InvalidParameterValueException.class, () -> guard.validateVmEligibleForGroupMembership(VM_ID)); + } + + @Test + public void testAlreadyIndependentBootGroupMemberThrows() { + when(instanceBootGroupMemberDao.findByMember(InstanceBootGroupMember.MemberType.VirtualMachine, VM_ID)) + .thenReturn(mock(InstanceBootGroupMemberVO.class)); + assertThrows(InvalidParameterValueException.class, () -> guard.validateVmEligibleForGroupMembership(VM_ID)); + } + + @Test + public void testNoInstanceGroupMappingsPasses() { + when(instanceGroupVMMapDao.listByInstanceId(VM_ID)).thenReturn(Collections.emptyList()); + guard.validateVmEligibleForGroupMembership(VM_ID); + } + + @Test + public void testFirstInstanceGroupIsBootGroupMemberThrows() { + InstanceGroupVMMapVO firstMapping = mock(InstanceGroupVMMapVO.class); + when(firstMapping.getGroupId()).thenReturn(FIRST_GROUP_ID); + when(instanceGroupVMMapDao.listByInstanceId(VM_ID)).thenReturn(Collections.singletonList(firstMapping)); + when(instanceBootGroupMemberDao.findByMember(InstanceBootGroupMember.MemberType.InstanceGroup, FIRST_GROUP_ID)) + .thenReturn(mock(InstanceBootGroupMemberVO.class)); + + assertThrows(InvalidParameterValueException.class, () -> guard.validateVmEligibleForGroupMembership(VM_ID)); + } + + /** + * Regression test: a VM can belong to more than one Instance Group (instance_group_vm_map has no + * one-group-per-VM constraint), so a disqualifying group must not be missed just because it isn't + * the first mapping returned. + */ + @Test + public void testSecondInstanceGroupIsBootGroupMemberThrows() { + InstanceGroupVMMapVO firstMapping = mock(InstanceGroupVMMapVO.class); + when(firstMapping.getGroupId()).thenReturn(FIRST_GROUP_ID); + InstanceGroupVMMapVO secondMapping = mock(InstanceGroupVMMapVO.class); + when(secondMapping.getGroupId()).thenReturn(SECOND_GROUP_ID); + when(instanceGroupVMMapDao.listByInstanceId(VM_ID)).thenReturn(Arrays.asList(firstMapping, secondMapping)); + + when(instanceBootGroupMemberDao.findByMember(InstanceBootGroupMember.MemberType.InstanceGroup, FIRST_GROUP_ID)).thenReturn(null); + when(instanceBootGroupMemberDao.findByMember(InstanceBootGroupMember.MemberType.InstanceGroup, SECOND_GROUP_ID)) + .thenReturn(mock(InstanceBootGroupMemberVO.class)); + + assertThrows(InvalidParameterValueException.class, () -> guard.validateVmEligibleForGroupMembership(VM_ID)); + } + + @Test + public void testMultipleInstanceGroupsNoneDisqualifyingPasses() { + InstanceGroupVMMapVO firstMapping = mock(InstanceGroupVMMapVO.class); + when(firstMapping.getGroupId()).thenReturn(FIRST_GROUP_ID); + InstanceGroupVMMapVO secondMapping = mock(InstanceGroupVMMapVO.class); + when(secondMapping.getGroupId()).thenReturn(SECOND_GROUP_ID); + when(instanceGroupVMMapDao.listByInstanceId(VM_ID)).thenReturn(Arrays.asList(firstMapping, secondMapping)); + + when(instanceBootGroupMemberDao.findByMember(InstanceBootGroupMember.MemberType.InstanceGroup, FIRST_GROUP_ID)).thenReturn(null); + when(instanceBootGroupMemberDao.findByMember(InstanceBootGroupMember.MemberType.InstanceGroup, SECOND_GROUP_ID)).thenReturn(null); + + guard.validateVmEligibleForGroupMembership(VM_ID); + } +} diff --git a/server/src/test/java/org/apache/cloudstack/vm/bootgroup/InstanceBootGroupVmStateListenerTest.java b/server/src/test/java/org/apache/cloudstack/vm/bootgroup/InstanceBootGroupVmStateListenerTest.java new file mode 100644 index 000000000000..02abd35f3177 --- /dev/null +++ b/server/src/test/java/org/apache/cloudstack/vm/bootgroup/InstanceBootGroupVmStateListenerTest.java @@ -0,0 +1,106 @@ +// 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.vm.bootgroup; + +import static org.junit.Assert.assertTrue; +import static org.mockito.ArgumentMatchers.anyLong; +import static org.mockito.Mockito.doThrow; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.never; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +import org.apache.cloudstack.vm.bootgroup.readiness.InstanceBootGroupReadinessRuleService; +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.junit.MockitoJUnitRunner; + +import com.cloud.utils.fsm.StateMachine2; +import com.cloud.vm.VirtualMachine; +import com.cloud.vm.VirtualMachine.Event; +import com.cloud.vm.VirtualMachine.State; + +@RunWith(MockitoJUnitRunner.class) +public class InstanceBootGroupVmStateListenerTest { + + private static final long VM_ID = 100L; + + private InstanceBootGroupReadinessRuleService instanceBootGroupReadinessRuleService; + private InstanceBootGroupVmStateListener listener; + private VirtualMachine vm; + + @Before + public void setUp() { + instanceBootGroupReadinessRuleService = mock(InstanceBootGroupReadinessRuleService.class); + listener = new InstanceBootGroupVmStateListener(instanceBootGroupReadinessRuleService); + vm = mock(VirtualMachine.class); + when(vm.getId()).thenReturn(VM_ID); + } + + private StateMachine2.Transition transition(State from, Event event, State to) { + return new StateMachine2.Transition<>(from, event, to, null); + } + + @Test + public void preStateTransitionEventAlwaysAllowsTheTransition() { + assertTrue(listener.preStateTransitionEvent(State.Starting, Event.OperationSucceeded, State.Running, vm, true, null)); + } + + @Test + public void invalidatesOnColdStart() { + listener.postStateTransitionEvent(transition(State.Starting, Event.OperationSucceeded, State.Running), vm, true, null); + verify(instanceBootGroupReadinessRuleService).invalidateCachedReadinessOnRestart(VM_ID); + } + + @Test + public void invalidatesOnColdStartConfirmedByAgentReport() { + listener.postStateTransitionEvent(transition(State.Starting, Event.AgentReportRunning, State.Running), vm, true, null); + verify(instanceBootGroupReadinessRuleService).invalidateCachedReadinessOnRestart(VM_ID); + } + + @Test + public void ignoresFailedTransitions() { + listener.postStateTransitionEvent(transition(State.Starting, Event.OperationSucceeded, State.Running), vm, false, null); + verify(instanceBootGroupReadinessRuleService, never()).invalidateCachedReadinessOnRestart(anyLong()); + } + + @Test + public void ignoresSameStateConfirmation() { + listener.postStateTransitionEvent(transition(State.Running, Event.AgentReportRunning, State.Running), vm, true, null); + verify(instanceBootGroupReadinessRuleService, never()).invalidateCachedReadinessOnRestart(anyLong()); + } + + @Test + public void ignoresMigrationLandingInRunning() { + listener.postStateTransitionEvent(transition(State.Migrating, Event.OperationSucceeded, State.Running), vm, true, null); + verify(instanceBootGroupReadinessRuleService, never()).invalidateCachedReadinessOnRestart(anyLong()); + } + + @Test + public void ignoresTransitionsNotLandingInRunning() { + listener.postStateTransitionEvent(transition(State.Running, Event.StopRequested, State.Stopping), vm, true, null); + verify(instanceBootGroupReadinessRuleService, never()).invalidateCachedReadinessOnRestart(anyLong()); + } + + @Test + public void exceptionFromServiceDoesNotPropagate() { + doThrow(new RuntimeException("db down")).when(instanceBootGroupReadinessRuleService).invalidateCachedReadinessOnRestart(VM_ID); + boolean result = listener.postStateTransitionEvent(transition(State.Starting, Event.OperationSucceeded, State.Running), vm, true, null); + assertTrue(result); + } +} diff --git a/server/src/test/java/org/apache/cloudstack/vm/bootgroup/readiness/GuestAgentLivenessCheckerTest.java b/server/src/test/java/org/apache/cloudstack/vm/bootgroup/readiness/GuestAgentLivenessCheckerTest.java new file mode 100644 index 000000000000..ba7d7185889d --- /dev/null +++ b/server/src/test/java/org/apache/cloudstack/vm/bootgroup/readiness/GuestAgentLivenessCheckerTest.java @@ -0,0 +1,163 @@ +// 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.vm.bootgroup.readiness; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertTrue; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.ArgumentCaptor; +import org.mockito.InjectMocks; +import org.mockito.Mock; +import org.mockito.junit.MockitoJUnitRunner; + +import com.cloud.agent.AgentManager; +import com.cloud.agent.api.Answer; +import com.cloud.agent.api.CheckGuestAgentLivenessAnswer; +import com.cloud.agent.api.CheckGuestAgentLivenessCommand; +import com.cloud.hypervisor.Hypervisor.HypervisorType; +import com.cloud.vm.UserVmVO; +import com.cloud.vm.VirtualMachine; +import com.cloud.vm.dao.UserVmDao; + +@RunWith(MockitoJUnitRunner.class) +public class GuestAgentLivenessCheckerTest { + + private static final long VM_ID = 100L; + private static final long HOST_ID = 300L; + + @InjectMocks + GuestAgentLivenessChecker checker; + + @Mock + UserVmDao userVmDao; + @Mock + AgentManager agentManager; + + private UserVmVO vm; + + @Before + public void setUp() { + vm = mock(UserVmVO.class); + when(userVmDao.findById(VM_ID)).thenReturn(vm); + when(vm.getHypervisorType()).thenReturn(HypervisorType.KVM); + when(vm.getState()).thenReturn(VirtualMachine.State.Running); + when(vm.getHostId()).thenReturn(HOST_ID); + } + + private InstanceBootGroupReadinessRule rule() { + return mock(InstanceBootGroupReadinessRule.class); + } + + @Test + public void getRuleTypeIsGuestAgentLiveness() { + assertEquals(InstanceBootGroupReadinessRule.RuleType.GuestAgentLiveness, checker.getRuleType()); + } + + @Test + public void vmNotFoundIsError() { + when(userVmDao.findById(VM_ID)).thenReturn(null); + ReadinessChecker.Result result = checker.check(rule(), null, VM_ID, 60000); + assertEquals(InstanceBootGroupReadinessRule.Status.Error, result.getStatus()); + } + + @Test + public void nonKvmHypervisorIsError() { + when(vm.getHypervisorType()).thenReturn(HypervisorType.VMware); + ReadinessChecker.Result result = checker.check(rule(), null, VM_ID, 60000); + assertEquals(InstanceBootGroupReadinessRule.Status.Error, result.getStatus()); + assertTrue(result.getMessage().contains("only supported on KVM")); + } + + @Test + public void notRunningIsNotReady() { + when(vm.getState()).thenReturn(VirtualMachine.State.Stopped); + ReadinessChecker.Result result = checker.check(rule(), null, VM_ID, 60000); + assertEquals(InstanceBootGroupReadinessRule.Status.NotReady, result.getStatus()); + } + + @Test + public void nullHostIdIsNotReady() { + when(vm.getHostId()).thenReturn(null); + ReadinessChecker.Result result = checker.check(rule(), null, VM_ID, 60000); + assertEquals(InstanceBootGroupReadinessRule.Status.NotReady, result.getStatus()); + } + + @Test + public void insufficientBudgetIsError() { + ReadinessChecker.Result result = checker.check(rule(), null, VM_ID, 500); + assertEquals(InstanceBootGroupReadinessRule.Status.Error, result.getStatus()); + } + + @Test + public void dispatchExceptionIsError() { + when(agentManager.easySend(eq(HOST_ID), any())).thenThrow(new RuntimeException("agent down")); + ReadinessChecker.Result result = checker.check(rule(), null, VM_ID, 60000); + assertEquals(InstanceBootGroupReadinessRule.Status.Error, result.getStatus()); + assertTrue(result.getMessage().contains("agent down")); + } + + @Test + public void nullAnswerIsError() { + when(agentManager.easySend(eq(HOST_ID), any())).thenReturn(null); + ReadinessChecker.Result result = checker.check(rule(), null, VM_ID, 60000); + assertEquals(InstanceBootGroupReadinessRule.Status.Error, result.getStatus()); + } + + @Test + public void positiveAnswerIsReady() { + Answer answer = mock(CheckGuestAgentLivenessAnswer.class); + when(answer.getResult()).thenReturn(true); + when(agentManager.easySend(eq(HOST_ID), any())).thenReturn(answer); + + ReadinessChecker.Result result = checker.check(rule(), null, VM_ID, 60000); + assertEquals(InstanceBootGroupReadinessRule.Status.Ready, result.getStatus()); + assertEquals("guest agent responded", result.getMessage()); + } + + @Test + public void negativeAnswerIsNotReady() { + Answer answer = mock(CheckGuestAgentLivenessAnswer.class); + when(answer.getResult()).thenReturn(false); + when(answer.getDetails()).thenReturn("agent not connected"); + when(agentManager.easySend(eq(HOST_ID), any())).thenReturn(answer); + + ReadinessChecker.Result result = checker.check(rule(), null, VM_ID, 60000); + assertEquals(InstanceBootGroupReadinessRule.Status.NotReady, result.getStatus()); + assertTrue(result.getMessage().contains("agent not connected")); + } + + @Test + public void waitIsHalvedRemainingBudgetFlooredAtOne() { + Answer answer = mock(CheckGuestAgentLivenessAnswer.class); + when(answer.getResult()).thenReturn(true); + when(agentManager.easySend(eq(HOST_ID), any())).thenReturn(answer); + ArgumentCaptor captor = ArgumentCaptor.forClass(CheckGuestAgentLivenessCommand.class); + + checker.check(rule(), null, VM_ID, 2500); + + verify(agentManager).easySend(eq(HOST_ID), captor.capture()); + assertEquals(1, captor.getValue().getWait()); + } +} diff --git a/server/src/test/java/org/apache/cloudstack/vm/bootgroup/readiness/InstanceBootGroupReadinessRuleManagerImplTest.java b/server/src/test/java/org/apache/cloudstack/vm/bootgroup/readiness/InstanceBootGroupReadinessRuleManagerImplTest.java new file mode 100644 index 000000000000..0ea0ed5fc476 --- /dev/null +++ b/server/src/test/java/org/apache/cloudstack/vm/bootgroup/readiness/InstanceBootGroupReadinessRuleManagerImplTest.java @@ -0,0 +1,963 @@ +// 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.vm.bootgroup.readiness; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertTrue; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.anyLong; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.never; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collections; +import java.util.Date; +import java.util.HashMap; +import java.util.HashSet; +import java.util.List; +import java.util.Map; +import java.util.Set; + +import org.apache.cloudstack.vm.bootgroup.InstanceBootGroupMember.MemberType; +import org.apache.cloudstack.vm.bootgroup.InstanceBootGroupMemberVO; +import org.apache.cloudstack.vm.bootgroup.InstanceBootGroupReadinessCheckResultVO; +import org.apache.cloudstack.vm.bootgroup.InstanceBootGroupReadinessRuleVO; +import org.apache.cloudstack.vm.bootgroup.readiness.InstanceBootGroupReadinessRule.RuleType; +import org.apache.cloudstack.vm.bootgroup.readiness.InstanceBootGroupReadinessRule.Status; +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.ArgumentCaptor; +import org.mockito.InjectMocks; +import org.mockito.Mock; +import org.mockito.Mockito; +import org.mockito.junit.MockitoJUnitRunner; +import org.springframework.test.util.ReflectionTestUtils; + +import com.cloud.exception.InvalidParameterValueException; +import com.cloud.hypervisor.Hypervisor.HypervisorType; +import com.cloud.vm.InstanceGroupVMMapVO; +import com.cloud.vm.UserVmVO; +import com.cloud.vm.VirtualMachine; +import com.cloud.vm.dao.InstanceBootGroupMemberDao; +import com.cloud.vm.dao.InstanceBootGroupReadinessCheckResultDao; +import com.cloud.vm.dao.InstanceBootGroupReadinessRuleDao; +import com.cloud.vm.dao.InstanceBootGroupReadinessRuleDetailsDao; +import com.cloud.vm.dao.InstanceGroupDao; +import com.cloud.vm.dao.InstanceGroupVMMapDao; +import com.cloud.vm.dao.UserVmDao; + +@RunWith(MockitoJUnitRunner.class) +public class InstanceBootGroupReadinessRuleManagerImplTest { + + private static final long BOOT_GROUP_ID = 1L; + private static final long VM_ID = 100L; + private static final long VM_ID_2 = 101L; + private static final long GROUP_ID = 200L; + private static final long RULE_ID = 10L; + + @InjectMocks + InstanceBootGroupReadinessRuleManagerImpl manager; + + @Mock + InstanceBootGroupReadinessRuleDao instanceBootGroupReadinessRuleDao; + @Mock + InstanceBootGroupReadinessRuleDetailsDao instanceBootGroupReadinessRuleDetailsDao; + @Mock + InstanceBootGroupReadinessCheckResultDao instanceBootGroupReadinessCheckResultDao; + @Mock + InstanceBootGroupMemberDao instanceBootGroupMemberDao; + @Mock + InstanceGroupVMMapDao instanceGroupVMMapDao; + @Mock + InstanceGroupDao instanceGroupDao; + @Mock + UserVmDao userVmDao; + + @Before + public void setUp() { + manager.setReadinessCheckers(Collections.emptyList()); + } + + // ------------------------------------------------------------------ + // helpers + // ------------------------------------------------------------------ + + private InstanceBootGroupReadinessRuleVO ruleVO(long id, long bootGroupId, MemberType itemType, long itemId, RuleType ruleType, boolean enabled) { + InstanceBootGroupReadinessRuleVO vo = new InstanceBootGroupReadinessRuleVO("rule-" + id, bootGroupId, itemType, itemId, ruleType, enabled); + ReflectionTestUtils.setField(vo, "id", id); + return vo; + } + + private InstanceBootGroupMemberVO memberVO(long bootGroupId, MemberType memberType, long memberId) { + return new InstanceBootGroupMemberVO(bootGroupId, memberType, memberId, 0); + } + + private UserVmVO mockVm(long id, VirtualMachine.State state, HypervisorType hypervisorType) { + UserVmVO vm = mock(UserVmVO.class); + Mockito.lenient().when(vm.getId()).thenReturn(id); + Mockito.lenient().when(vm.getState()).thenReturn(state); + Mockito.lenient().when(vm.getHypervisorType()).thenReturn(hypervisorType); + return vm; + } + + private ReadinessChecker mockChecker(RuleType type, Status status, String message) { + ReadinessChecker checker = mock(ReadinessChecker.class); + when(checker.getRuleType()).thenReturn(type); + when(checker.check(any(), any(), anyLong(), anyLong())).thenReturn(new ReadinessChecker.Result(status, message)); + return checker; + } + + /** Satisfies validateItemBelongsToBootGroup for a direct VirtualMachine-type item. */ + private void stubVmDirectMember(long itemId) { + when(instanceBootGroupMemberDao.findByMember(MemberType.VirtualMachine, itemId)).thenReturn(memberVO(BOOT_GROUP_ID, MemberType.VirtualMachine, itemId)); + } + + /** Satisfies validateItemBelongsToBootGroup for a direct InstanceGroup-type item. */ + private void stubGroupDirectMember(long itemId) { + when(instanceBootGroupMemberDao.findByMember(MemberType.InstanceGroup, itemId)).thenReturn(memberVO(BOOT_GROUP_ID, MemberType.InstanceGroup, itemId)); + } + + // ================================================================== + // createReadinessRule + // ================================================================== + + @Test(expected = InvalidParameterValueException.class) + public void createReadinessRuleRejectsInvalidRuleTypeForVm() { + manager.createReadinessRule(BOOT_GROUP_ID, MemberType.VirtualMachine, VM_ID, RuleType.MemberQuorum, null, true, null); + } + + @Test + public void createReadinessRuleAcceptsMemberQuorumForInstanceGroup() { + stubGroupDirectMember(GROUP_ID); + Map details = new HashMap<>(); + details.put("threshold_type", "COUNT"); + details.put("threshold_value", "2"); + when(instanceBootGroupReadinessRuleDao.persist(any())).thenReturn(ruleVO(RULE_ID, BOOT_GROUP_ID, MemberType.InstanceGroup, GROUP_ID, RuleType.MemberQuorum, true)); + + InstanceBootGroupReadinessRule result = manager.createReadinessRule(BOOT_GROUP_ID, MemberType.InstanceGroup, GROUP_ID, RuleType.MemberQuorum, null, true, details); + + assertEquals(RULE_ID, result.getId()); + verify(instanceBootGroupReadinessRuleDetailsDao).addDetail(RULE_ID, "threshold_type", "COUNT", true); + verify(instanceBootGroupReadinessRuleDetailsDao).addDetail(RULE_ID, "threshold_value", "2", true); + } + + @Test(expected = InvalidParameterValueException.class) + public void createReadinessRuleRejectsItemNotInBootGroup() { + when(instanceBootGroupMemberDao.findByMember(MemberType.VirtualMachine, VM_ID)).thenReturn(null); + when(instanceGroupVMMapDao.listByInstanceId(VM_ID)).thenReturn(Collections.emptyList()); + manager.createReadinessRule(BOOT_GROUP_ID, MemberType.VirtualMachine, VM_ID, RuleType.Ping, null, true, null); + } + + @Test + public void createReadinessRuleAllowsItemViaInstanceGroupMembership() { + when(instanceBootGroupMemberDao.findByMember(MemberType.VirtualMachine, VM_ID)).thenReturn(null); + InstanceGroupVMMapVO mapping = new InstanceGroupVMMapVO(GROUP_ID, VM_ID); + when(instanceGroupVMMapDao.listByInstanceId(VM_ID)).thenReturn(Collections.singletonList(mapping)); + when(instanceBootGroupMemberDao.findByMember(MemberType.InstanceGroup, GROUP_ID)).thenReturn(memberVO(BOOT_GROUP_ID, MemberType.InstanceGroup, GROUP_ID)); + when(instanceBootGroupReadinessRuleDao.persist(any())).thenReturn(ruleVO(RULE_ID, BOOT_GROUP_ID, MemberType.VirtualMachine, VM_ID, RuleType.Ping, true)); + + InstanceBootGroupReadinessRule result = manager.createReadinessRule(BOOT_GROUP_ID, MemberType.VirtualMachine, VM_ID, RuleType.Ping, null, true, null); + assertEquals(RULE_ID, result.getId()); + } + + @Test(expected = InvalidParameterValueException.class) + public void createReadinessRuleRejectsSecondSingletonRuleType() { + stubVmDirectMember(VM_ID); + when(instanceBootGroupReadinessRuleDao.listByItem(BOOT_GROUP_ID, MemberType.VirtualMachine, VM_ID)) + .thenReturn(Collections.singletonList(ruleVO(5L, BOOT_GROUP_ID, MemberType.VirtualMachine, VM_ID, RuleType.Ping, true))); + manager.createReadinessRule(BOOT_GROUP_ID, MemberType.VirtualMachine, VM_ID, RuleType.Ping, null, true, null); + } + + @Test + public void createReadinessRuleAllowsSecondPortCheckRule() { + // PortCheck is not a singleton rule type, so validateSingletonRuleType never even + // consults the existing-rules DAO for it — no need to stub listByItem here. + stubVmDirectMember(VM_ID); + Map details = new HashMap<>(); + details.put("port", "22"); + when(instanceBootGroupReadinessRuleDao.persist(any())).thenReturn(ruleVO(RULE_ID, BOOT_GROUP_ID, MemberType.VirtualMachine, VM_ID, RuleType.PortCheck, true)); + + InstanceBootGroupReadinessRule result = manager.createReadinessRule(BOOT_GROUP_ID, MemberType.VirtualMachine, VM_ID, RuleType.PortCheck, null, true, details); + assertEquals(RULE_ID, result.getId()); + } + + @Test(expected = InvalidParameterValueException.class) + public void createReadinessRuleRejectsGuestAgentLivenessOnNonKvm() { + stubVmDirectMember(VM_ID); + when(instanceBootGroupReadinessRuleDao.listByItem(BOOT_GROUP_ID, MemberType.VirtualMachine, VM_ID)).thenReturn(Collections.emptyList()); + UserVmVO vm22141 = mockVm(VM_ID, VirtualMachine.State.Running, HypervisorType.VMware); + when(userVmDao.findById(VM_ID)).thenReturn(vm22141); + manager.createReadinessRule(BOOT_GROUP_ID, MemberType.VirtualMachine, VM_ID, RuleType.GuestAgentLiveness, null, true, null); + } + + @Test(expected = InvalidParameterValueException.class) + public void createReadinessRuleRejectsGuestAgentLivenessVmNotFound() { + stubVmDirectMember(VM_ID); + when(instanceBootGroupReadinessRuleDao.listByItem(BOOT_GROUP_ID, MemberType.VirtualMachine, VM_ID)).thenReturn(Collections.emptyList()); + when(userVmDao.findById(VM_ID)).thenReturn(null); + manager.createReadinessRule(BOOT_GROUP_ID, MemberType.VirtualMachine, VM_ID, RuleType.GuestAgentLiveness, null, true, null); + } + + @Test + public void createReadinessRuleAllowsGuestAgentLivenessOnKvm() { + stubVmDirectMember(VM_ID); + when(instanceBootGroupReadinessRuleDao.listByItem(BOOT_GROUP_ID, MemberType.VirtualMachine, VM_ID)).thenReturn(Collections.emptyList()); + UserVmVO vm85153 = mockVm(VM_ID, VirtualMachine.State.Running, HypervisorType.KVM); + when(userVmDao.findById(VM_ID)).thenReturn(vm85153); + when(instanceBootGroupReadinessRuleDao.persist(any())).thenReturn(ruleVO(RULE_ID, BOOT_GROUP_ID, MemberType.VirtualMachine, VM_ID, RuleType.GuestAgentLiveness, true)); + + InstanceBootGroupReadinessRule result = manager.createReadinessRule(BOOT_GROUP_ID, MemberType.VirtualMachine, VM_ID, RuleType.GuestAgentLiveness, null, true, null); + assertEquals(RULE_ID, result.getId()); + } + + @Test + public void createReadinessRuleDoesNotValidateGuestAgentLivenessForGroupScope() { + stubGroupDirectMember(GROUP_ID); + when(instanceBootGroupReadinessRuleDao.listByItem(BOOT_GROUP_ID, MemberType.InstanceGroup, GROUP_ID)).thenReturn(Collections.emptyList()); + when(instanceBootGroupReadinessRuleDao.persist(any())).thenReturn(ruleVO(RULE_ID, BOOT_GROUP_ID, MemberType.InstanceGroup, GROUP_ID, RuleType.GuestAgentLiveness, true)); + + InstanceBootGroupReadinessRule result = manager.createReadinessRule(BOOT_GROUP_ID, MemberType.InstanceGroup, GROUP_ID, RuleType.GuestAgentLiveness, null, true, null); + + assertEquals(RULE_ID, result.getId()); + verify(userVmDao, never()).findById(anyLong()); + } + + @Test(expected = InvalidParameterValueException.class) + public void createReadinessRuleRejectsInvalidPortCheckDetails() { + stubVmDirectMember(VM_ID); + Map details = new HashMap<>(); + details.put("port", "70000"); + manager.createReadinessRule(BOOT_GROUP_ID, MemberType.VirtualMachine, VM_ID, RuleType.PortCheck, null, true, details); + } + + @Test(expected = InvalidParameterValueException.class) + public void createReadinessRuleRejectsMissingPortCheckPort() { + stubVmDirectMember(VM_ID); + manager.createReadinessRule(BOOT_GROUP_ID, MemberType.VirtualMachine, VM_ID, RuleType.PortCheck, null, true, new HashMap<>()); + } + + @Test(expected = InvalidParameterValueException.class) + public void createReadinessRuleRejectsNonTcpProtocol() { + stubVmDirectMember(VM_ID); + Map details = new HashMap<>(); + details.put("port", "22"); + details.put("protocol", "udp"); + manager.createReadinessRule(BOOT_GROUP_ID, MemberType.VirtualMachine, VM_ID, RuleType.PortCheck, null, true, details); + } + + @Test(expected = InvalidParameterValueException.class) + public void createReadinessRuleRejectsMissingMemberQuorumDetails() { + stubGroupDirectMember(GROUP_ID); + manager.createReadinessRule(BOOT_GROUP_ID, MemberType.InstanceGroup, GROUP_ID, RuleType.MemberQuorum, null, true, new HashMap<>()); + } + + @Test(expected = InvalidParameterValueException.class) + public void createReadinessRuleRejectsInvalidMemberQuorumThresholdType() { + stubGroupDirectMember(GROUP_ID); + Map details = new HashMap<>(); + details.put("threshold_type", "BOGUS"); + details.put("threshold_value", "2"); + manager.createReadinessRule(BOOT_GROUP_ID, MemberType.InstanceGroup, GROUP_ID, RuleType.MemberQuorum, null, true, details); + } + + @Test(expected = InvalidParameterValueException.class) + public void createReadinessRuleRejectsNonNumericMemberQuorumThreshold() { + stubGroupDirectMember(GROUP_ID); + Map details = new HashMap<>(); + details.put("threshold_type", "PERCENTAGE"); + details.put("threshold_value", "abc"); + manager.createReadinessRule(BOOT_GROUP_ID, MemberType.InstanceGroup, GROUP_ID, RuleType.MemberQuorum, null, true, details); + } + + @Test + public void createReadinessRuleGeneratesDefaultNameWhenBlank() { + stubVmDirectMember(VM_ID); + when(instanceBootGroupReadinessRuleDao.listByItem(BOOT_GROUP_ID, MemberType.VirtualMachine, VM_ID)).thenReturn(Collections.emptyList()); + ArgumentCaptor captor = ArgumentCaptor.forClass(InstanceBootGroupReadinessRuleVO.class); + when(instanceBootGroupReadinessRuleDao.persist(captor.capture())).thenReturn(ruleVO(RULE_ID, BOOT_GROUP_ID, MemberType.VirtualMachine, VM_ID, RuleType.Ping, true)); + + manager.createReadinessRule(BOOT_GROUP_ID, MemberType.VirtualMachine, VM_ID, RuleType.Ping, " ", true, null); + + assertEquals(String.format("%s-%s-%d", RuleType.Ping.name(), MemberType.VirtualMachine.name(), VM_ID), captor.getValue().getName()); + } + + @Test + public void createReadinessRuleUsesGivenName() { + stubVmDirectMember(VM_ID); + when(instanceBootGroupReadinessRuleDao.listByItem(BOOT_GROUP_ID, MemberType.VirtualMachine, VM_ID)).thenReturn(Collections.emptyList()); + ArgumentCaptor captor = ArgumentCaptor.forClass(InstanceBootGroupReadinessRuleVO.class); + when(instanceBootGroupReadinessRuleDao.persist(captor.capture())).thenReturn(ruleVO(RULE_ID, BOOT_GROUP_ID, MemberType.VirtualMachine, VM_ID, RuleType.Ping, true)); + + manager.createReadinessRule(BOOT_GROUP_ID, MemberType.VirtualMachine, VM_ID, RuleType.Ping, "my-rule", true, null); + + assertEquals("my-rule", captor.getValue().getName()); + } + + // ================================================================== + // updateReadinessRule + // ================================================================== + + @Test(expected = InvalidParameterValueException.class) + public void updateReadinessRuleNotFoundThrows() { + when(instanceBootGroupReadinessRuleDao.findById(RULE_ID)).thenReturn(null); + manager.updateReadinessRule(RULE_ID, "name", true, null); + } + + @Test + public void updateReadinessRuleNoChanges() { + InstanceBootGroupReadinessRuleVO rule = ruleVO(RULE_ID, BOOT_GROUP_ID, MemberType.VirtualMachine, VM_ID, RuleType.Ping, true); + when(instanceBootGroupReadinessRuleDao.findById(RULE_ID)).thenReturn(rule); + + manager.updateReadinessRule(RULE_ID, null, null, null); + + verify(instanceBootGroupReadinessRuleDao).update(eq(RULE_ID), any()); + verify(instanceBootGroupReadinessRuleDetailsDao, never()).addDetail(anyLong(), any(), any(), Mockito.anyBoolean()); + } + + @Test + public void updateReadinessRuleNameAndEnabled() { + InstanceBootGroupReadinessRuleVO rule = ruleVO(RULE_ID, BOOT_GROUP_ID, MemberType.VirtualMachine, VM_ID, RuleType.Ping, true); + when(instanceBootGroupReadinessRuleDao.findById(RULE_ID)).thenReturn(rule); + + manager.updateReadinessRule(RULE_ID, "new-name", false, null); + + assertEquals("new-name", rule.getName()); + assertEquals(false, rule.isEnabled()); + verify(instanceBootGroupReadinessRuleDao).update(RULE_ID, rule); + } + + @Test + public void updateReadinessRuleMergesAndValidatesDetails() { + InstanceBootGroupReadinessRuleVO rule = ruleVO(RULE_ID, BOOT_GROUP_ID, MemberType.VirtualMachine, VM_ID, RuleType.PortCheck, true); + when(instanceBootGroupReadinessRuleDao.findById(RULE_ID)).thenReturn(rule); + Map existing = new HashMap<>(); + existing.put("port", "8080"); + existing.put("protocol", "tcp"); + when(instanceBootGroupReadinessRuleDetailsDao.getDetails(RULE_ID)).thenReturn(existing); + + Map update = new HashMap<>(); + update.put("port", "9090"); + manager.updateReadinessRule(RULE_ID, null, null, update); + + verify(instanceBootGroupReadinessRuleDetailsDao).addDetail(RULE_ID, "port", "9090", true); + verify(instanceBootGroupReadinessRuleDao).update(RULE_ID, rule); + } + + @Test(expected = InvalidParameterValueException.class) + public void updateReadinessRuleRejectsInvalidMergedDetails() { + InstanceBootGroupReadinessRuleVO rule = ruleVO(RULE_ID, BOOT_GROUP_ID, MemberType.VirtualMachine, VM_ID, RuleType.PortCheck, true); + when(instanceBootGroupReadinessRuleDao.findById(RULE_ID)).thenReturn(rule); + when(instanceBootGroupReadinessRuleDetailsDao.getDetails(RULE_ID)).thenReturn(new HashMap<>()); + + Map update = new HashMap<>(); + update.put("port", "invalid"); + try { + manager.updateReadinessRule(RULE_ID, null, null, update); + } finally { + verify(instanceBootGroupReadinessRuleDao, never()).update(anyLong(), any()); + } + } + + // ================================================================== + // deleteReadinessRule + // ================================================================== + + @Test(expected = InvalidParameterValueException.class) + public void deleteReadinessRuleNotFoundThrows() { + when(instanceBootGroupReadinessRuleDao.findById(RULE_ID)).thenReturn(null); + manager.deleteReadinessRule(RULE_ID); + } + + @Test + public void deleteReadinessRuleSuccess() { + InstanceBootGroupReadinessRuleVO rule = ruleVO(RULE_ID, BOOT_GROUP_ID, MemberType.VirtualMachine, VM_ID, RuleType.Ping, true); + when(instanceBootGroupReadinessRuleDao.findById(RULE_ID)).thenReturn(rule); + + boolean result = manager.deleteReadinessRule(RULE_ID); + + assertTrue(result); + verify(instanceBootGroupReadinessRuleDetailsDao).removeDetails(RULE_ID); + verify(instanceBootGroupReadinessCheckResultDao).deleteByRuleId(RULE_ID); + verify(instanceBootGroupReadinessRuleDao).remove(RULE_ID); + } + + // ================================================================== + // findById / getRuleDetails + // ================================================================== + + @Test + public void findByIdDelegatesToDao() { + InstanceBootGroupReadinessRuleVO rule = ruleVO(RULE_ID, BOOT_GROUP_ID, MemberType.VirtualMachine, VM_ID, RuleType.Ping, true); + when(instanceBootGroupReadinessRuleDao.findById(RULE_ID)).thenReturn(rule); + assertEquals(rule, manager.findById(RULE_ID)); + } + + @Test + public void getRuleDetailsDelegatesToDetailsDao() { + Map details = Collections.singletonMap("k", "v"); + when(instanceBootGroupReadinessRuleDetailsDao.getDetails(RULE_ID)).thenReturn(details); + assertEquals(details, manager.getRuleDetails(RULE_ID)); + } + + // ================================================================== + // evaluateVmReadiness / resolveVmReadiness (dispatch path) + // ================================================================== + + @Test + public void evaluateVmReadinessVmNotFoundIsNotReady() { + when(userVmDao.findById(VM_ID)).thenReturn(null); + Status status = manager.evaluateVmReadiness(BOOT_GROUP_ID, VM_ID, 10000L, null); + assertEquals(Status.NotReady, status); + } + + @Test + public void evaluateVmReadinessNonRunningVmIsNotReadyWithoutDispatch() { + UserVmVO vm70096 = mockVm(VM_ID, VirtualMachine.State.Stopped, HypervisorType.KVM); + when(userVmDao.findById(VM_ID)).thenReturn(vm70096); + Status status = manager.evaluateVmReadiness(BOOT_GROUP_ID, VM_ID, 10000L, null); + assertEquals(Status.NotReady, status); + verify(instanceBootGroupReadinessRuleDao, never()).listEnabledByItem(anyLong(), any(), anyLong()); + verify(instanceBootGroupReadinessCheckResultDao, never()).upsert(anyLong(), anyLong(), any(), any(), any()); + } + + @Test + public void evaluateVmReadinessNoRulesIsReady() { + UserVmVO vm85153 = mockVm(VM_ID, VirtualMachine.State.Running, HypervisorType.KVM); + when(userVmDao.findById(VM_ID)).thenReturn(vm85153); + when(instanceBootGroupReadinessRuleDao.listEnabledByItem(BOOT_GROUP_ID, MemberType.VirtualMachine, VM_ID)).thenReturn(Collections.emptyList()); + when(instanceGroupVMMapDao.listByInstanceId(VM_ID)).thenReturn(Collections.emptyList()); + + Status status = manager.evaluateVmReadiness(BOOT_GROUP_ID, VM_ID, 10000L, null); + assertEquals(Status.Ready, status); + } + + @Test + public void evaluateVmReadinessDispatchesDirectRuleAndCaches() { + UserVmVO vm85153 = mockVm(VM_ID, VirtualMachine.State.Running, HypervisorType.KVM); + when(userVmDao.findById(VM_ID)).thenReturn(vm85153); + InstanceBootGroupReadinessRuleVO rule = ruleVO(RULE_ID, BOOT_GROUP_ID, MemberType.VirtualMachine, VM_ID, RuleType.Ping, true); + when(instanceBootGroupReadinessRuleDao.listEnabledByItem(BOOT_GROUP_ID, MemberType.VirtualMachine, VM_ID)).thenReturn(Collections.singletonList(rule)); + when(instanceGroupVMMapDao.listByInstanceId(VM_ID)).thenReturn(Collections.emptyList()); + manager.setReadinessCheckers(Collections.singletonList(mockChecker(RuleType.Ping, Status.Ready, "pong"))); + + Status status = manager.evaluateVmReadiness(BOOT_GROUP_ID, VM_ID, 10000L, null); + + assertEquals(Status.Ready, status); + verify(instanceBootGroupReadinessCheckResultDao).upsert(eq(RULE_ID), eq(0L), eq(Status.Ready), eq("pong"), any(Date.class)); + } + + @Test + public void evaluateVmReadinessAppendsAttemptLabelToMessage() { + UserVmVO vm85153 = mockVm(VM_ID, VirtualMachine.State.Running, HypervisorType.KVM); + when(userVmDao.findById(VM_ID)).thenReturn(vm85153); + InstanceBootGroupReadinessRuleVO rule = ruleVO(RULE_ID, BOOT_GROUP_ID, MemberType.VirtualMachine, VM_ID, RuleType.Ping, true); + when(instanceBootGroupReadinessRuleDao.listEnabledByItem(BOOT_GROUP_ID, MemberType.VirtualMachine, VM_ID)).thenReturn(Collections.singletonList(rule)); + when(instanceGroupVMMapDao.listByInstanceId(VM_ID)).thenReturn(Collections.emptyList()); + manager.setReadinessCheckers(Collections.singletonList(mockChecker(RuleType.Ping, Status.NotReady, "no reply"))); + + manager.evaluateVmReadiness(BOOT_GROUP_ID, VM_ID, 10000L, "2/5"); + + verify(instanceBootGroupReadinessCheckResultDao).upsert(eq(RULE_ID), eq(0L), eq(Status.NotReady), eq("no reply (attempt 2/5)"), any(Date.class)); + } + + @Test + public void evaluateVmReadinessSkipsAttemptLabelWhenBlank() { + UserVmVO vm85153 = mockVm(VM_ID, VirtualMachine.State.Running, HypervisorType.KVM); + when(userVmDao.findById(VM_ID)).thenReturn(vm85153); + InstanceBootGroupReadinessRuleVO rule = ruleVO(RULE_ID, BOOT_GROUP_ID, MemberType.VirtualMachine, VM_ID, RuleType.Ping, true); + when(instanceBootGroupReadinessRuleDao.listEnabledByItem(BOOT_GROUP_ID, MemberType.VirtualMachine, VM_ID)).thenReturn(Collections.singletonList(rule)); + when(instanceGroupVMMapDao.listByInstanceId(VM_ID)).thenReturn(Collections.emptyList()); + manager.setReadinessCheckers(Collections.singletonList(mockChecker(RuleType.Ping, Status.Ready, "pong"))); + + manager.evaluateVmReadiness(BOOT_GROUP_ID, VM_ID, 10000L, null); + + verify(instanceBootGroupReadinessCheckResultDao).upsert(eq(RULE_ID), eq(0L), eq(Status.Ready), eq("pong"), any(Date.class)); + } + + @Test + public void evaluateVmReadinessNoCheckerRegisteredSynthesizesError() { + UserVmVO vm85153 = mockVm(VM_ID, VirtualMachine.State.Running, HypervisorType.KVM); + when(userVmDao.findById(VM_ID)).thenReturn(vm85153); + InstanceBootGroupReadinessRuleVO rule = ruleVO(RULE_ID, BOOT_GROUP_ID, MemberType.VirtualMachine, VM_ID, RuleType.CustomScript, true); + when(instanceBootGroupReadinessRuleDao.listEnabledByItem(BOOT_GROUP_ID, MemberType.VirtualMachine, VM_ID)).thenReturn(Collections.singletonList(rule)); + when(instanceGroupVMMapDao.listByInstanceId(VM_ID)).thenReturn(Collections.emptyList()); + // no checkers registered at all (set in @Before) + + Status status = manager.evaluateVmReadiness(BOOT_GROUP_ID, VM_ID, 10000L, "1/3"); + + assertEquals(Status.Error, status); + verify(instanceBootGroupReadinessCheckResultDao).upsert(eq(RULE_ID), eq(0L), eq(Status.Error), + eq("No checker implemented yet for rule type CustomScript (attempt 1/3)"), any(Date.class)); + } + + @Test + public void evaluateVmReadinessErrorTakesPrecedenceOverNotReady() { + UserVmVO vm85153 = mockVm(VM_ID, VirtualMachine.State.Running, HypervisorType.KVM); + when(userVmDao.findById(VM_ID)).thenReturn(vm85153); + InstanceBootGroupReadinessRuleVO pingRule = ruleVO(11L, BOOT_GROUP_ID, MemberType.VirtualMachine, VM_ID, RuleType.Ping, true); + InstanceBootGroupReadinessRuleVO portRule = ruleVO(12L, BOOT_GROUP_ID, MemberType.VirtualMachine, VM_ID, RuleType.PortCheck, true); + when(instanceBootGroupReadinessRuleDao.listEnabledByItem(BOOT_GROUP_ID, MemberType.VirtualMachine, VM_ID)).thenReturn(Arrays.asList(pingRule, portRule)); + when(instanceGroupVMMapDao.listByInstanceId(VM_ID)).thenReturn(Collections.emptyList()); + List checkers = Arrays.asList( + mockChecker(RuleType.Ping, Status.NotReady, "not yet"), + mockChecker(RuleType.PortCheck, Status.Error, "boom")); + manager.setReadinessCheckers(checkers); + + Status status = manager.evaluateVmReadiness(BOOT_GROUP_ID, VM_ID, 10000L, null); + assertEquals(Status.Error, status); + } + + @Test + public void evaluateVmReadinessAllReadyIsReady() { + UserVmVO vm85153 = mockVm(VM_ID, VirtualMachine.State.Running, HypervisorType.KVM); + when(userVmDao.findById(VM_ID)).thenReturn(vm85153); + InstanceBootGroupReadinessRuleVO pingRule = ruleVO(11L, BOOT_GROUP_ID, MemberType.VirtualMachine, VM_ID, RuleType.Ping, true); + when(instanceBootGroupReadinessRuleDao.listEnabledByItem(BOOT_GROUP_ID, MemberType.VirtualMachine, VM_ID)).thenReturn(Collections.singletonList(pingRule)); + when(instanceGroupVMMapDao.listByInstanceId(VM_ID)).thenReturn(Collections.emptyList()); + manager.setReadinessCheckers(Collections.singletonList(mockChecker(RuleType.Ping, Status.Ready, "pong"))); + + assertEquals(Status.Ready, manager.evaluateVmReadiness(BOOT_GROUP_ID, VM_ID, 10000L, null)); + } + + @Test + public void evaluateVmReadinessDispatchesInheritedGroupRuleAtMemberVmId() { + UserVmVO vm85153 = mockVm(VM_ID, VirtualMachine.State.Running, HypervisorType.KVM); + when(userVmDao.findById(VM_ID)).thenReturn(vm85153); + when(instanceBootGroupReadinessRuleDao.listEnabledByItem(BOOT_GROUP_ID, MemberType.VirtualMachine, VM_ID)).thenReturn(Collections.emptyList()); + + InstanceGroupVMMapVO mapping = new InstanceGroupVMMapVO(GROUP_ID, VM_ID); + when(instanceGroupVMMapDao.listByInstanceId(VM_ID)).thenReturn(Collections.singletonList(mapping)); + when(instanceBootGroupMemberDao.findByMember(MemberType.InstanceGroup, GROUP_ID)).thenReturn(memberVO(BOOT_GROUP_ID, MemberType.InstanceGroup, GROUP_ID)); + InstanceBootGroupReadinessRuleVO groupPingRule = ruleVO(RULE_ID, BOOT_GROUP_ID, MemberType.InstanceGroup, GROUP_ID, RuleType.Ping, true); + when(instanceBootGroupReadinessRuleDao.listEnabledByItem(BOOT_GROUP_ID, MemberType.InstanceGroup, GROUP_ID)).thenReturn(Collections.singletonList(groupPingRule)); + manager.setReadinessCheckers(Collections.singletonList(mockChecker(RuleType.Ping, Status.Ready, "pong"))); + + Status status = manager.evaluateVmReadiness(BOOT_GROUP_ID, VM_ID, 10000L, null); + + assertEquals(Status.Ready, status); + verify(instanceBootGroupReadinessCheckResultDao).upsert(eq(RULE_ID), eq(VM_ID), eq(Status.Ready), eq("pong"), any(Date.class)); + } + + @Test + public void evaluateVmReadinessBudgetDoesNotIncreaseAcrossRules() { + UserVmVO vm85153 = mockVm(VM_ID, VirtualMachine.State.Running, HypervisorType.KVM); + when(userVmDao.findById(VM_ID)).thenReturn(vm85153); + InstanceBootGroupReadinessRuleVO rule1 = ruleVO(11L, BOOT_GROUP_ID, MemberType.VirtualMachine, VM_ID, RuleType.PortCheck, true); + InstanceBootGroupReadinessRuleVO rule2 = ruleVO(12L, BOOT_GROUP_ID, MemberType.VirtualMachine, VM_ID, RuleType.PortCheck, true); + when(instanceBootGroupReadinessRuleDao.listEnabledByItem(BOOT_GROUP_ID, MemberType.VirtualMachine, VM_ID)).thenReturn(Arrays.asList(rule1, rule2)); + when(instanceGroupVMMapDao.listByInstanceId(VM_ID)).thenReturn(Collections.emptyList()); + + ReadinessChecker checker = mock(ReadinessChecker.class); + when(checker.getRuleType()).thenReturn(RuleType.PortCheck); + ArgumentCaptor remainingCaptor = ArgumentCaptor.forClass(Long.class); + when(checker.check(any(), any(), anyLong(), remainingCaptor.capture())).thenReturn(new ReadinessChecker.Result(Status.Ready, "ok")); + manager.setReadinessCheckers(Collections.singletonList(checker)); + + manager.evaluateVmReadiness(BOOT_GROUP_ID, VM_ID, 10000L, null); + + List remaining = remainingCaptor.getAllValues(); + assertEquals(2, remaining.size()); + assertTrue("budget for first rule should be at most the initial budget", remaining.get(0) <= 10000L); + assertTrue("budget must not increase across rules", remaining.get(1) <= remaining.get(0)); + } + + // ================================================================== + // findInheritedGroupRules + // ================================================================== + + @Test + public void findInheritedGroupRulesEmptyWhenVmNotInAnyGroup() { + when(instanceGroupVMMapDao.listByInstanceId(VM_ID)).thenReturn(Collections.emptyList()); + assertTrue(manager.findInheritedGroupRules(BOOT_GROUP_ID, VM_ID).isEmpty()); + } + + @Test + public void findInheritedGroupRulesEmptyWhenGroupNotABootGroupMember() { + InstanceGroupVMMapVO mapping = new InstanceGroupVMMapVO(GROUP_ID, VM_ID); + when(instanceGroupVMMapDao.listByInstanceId(VM_ID)).thenReturn(Collections.singletonList(mapping)); + when(instanceBootGroupMemberDao.findByMember(MemberType.InstanceGroup, GROUP_ID)).thenReturn(null); + assertTrue(manager.findInheritedGroupRules(BOOT_GROUP_ID, VM_ID).isEmpty()); + } + + @Test + public void findInheritedGroupRulesEmptyWhenGroupBelongsToDifferentBootGroup() { + InstanceGroupVMMapVO mapping = new InstanceGroupVMMapVO(GROUP_ID, VM_ID); + when(instanceGroupVMMapDao.listByInstanceId(VM_ID)).thenReturn(Collections.singletonList(mapping)); + when(instanceBootGroupMemberDao.findByMember(MemberType.InstanceGroup, GROUP_ID)).thenReturn(memberVO(999L, MemberType.InstanceGroup, GROUP_ID)); + assertTrue(manager.findInheritedGroupRules(BOOT_GROUP_ID, VM_ID).isEmpty()); + } + + @Test + public void findInheritedGroupRulesFiltersToMemberTargetedTypes() { + InstanceGroupVMMapVO mapping = new InstanceGroupVMMapVO(GROUP_ID, VM_ID); + when(instanceGroupVMMapDao.listByInstanceId(VM_ID)).thenReturn(Collections.singletonList(mapping)); + when(instanceBootGroupMemberDao.findByMember(MemberType.InstanceGroup, GROUP_ID)).thenReturn(memberVO(BOOT_GROUP_ID, MemberType.InstanceGroup, GROUP_ID)); + + InstanceBootGroupReadinessRuleVO pingRule = ruleVO(11L, BOOT_GROUP_ID, MemberType.InstanceGroup, GROUP_ID, RuleType.Ping, true); + InstanceBootGroupReadinessRuleVO quorumRule = ruleVO(12L, BOOT_GROUP_ID, MemberType.InstanceGroup, GROUP_ID, RuleType.MemberQuorum, true); + InstanceBootGroupReadinessRuleVO scriptRule = ruleVO(13L, BOOT_GROUP_ID, MemberType.InstanceGroup, GROUP_ID, RuleType.CustomScript, true); + when(instanceBootGroupReadinessRuleDao.listEnabledByItem(BOOT_GROUP_ID, MemberType.InstanceGroup, GROUP_ID)) + .thenReturn(Arrays.asList(pingRule, quorumRule, scriptRule)); + + List result = manager.findInheritedGroupRules(BOOT_GROUP_ID, VM_ID); + + assertEquals(1, result.size()); + assertEquals(RuleType.Ping, result.get(0).getRuleType()); + } + + @Test + public void findInheritedGroupRulesSkipsNonMatchingMappingThenMatches() { + InstanceGroupVMMapVO mapping1 = new InstanceGroupVMMapVO(GROUP_ID, VM_ID); + InstanceGroupVMMapVO mapping2 = new InstanceGroupVMMapVO(GROUP_ID + 1, VM_ID); + when(instanceGroupVMMapDao.listByInstanceId(VM_ID)).thenReturn(Arrays.asList(mapping1, mapping2)); + when(instanceBootGroupMemberDao.findByMember(MemberType.InstanceGroup, GROUP_ID)).thenReturn(null); + when(instanceBootGroupMemberDao.findByMember(MemberType.InstanceGroup, GROUP_ID + 1)).thenReturn(memberVO(BOOT_GROUP_ID, MemberType.InstanceGroup, GROUP_ID + 1)); + InstanceBootGroupReadinessRuleVO pingRule = ruleVO(11L, BOOT_GROUP_ID, MemberType.InstanceGroup, GROUP_ID + 1, RuleType.Ping, true); + when(instanceBootGroupReadinessRuleDao.listEnabledByItem(BOOT_GROUP_ID, MemberType.InstanceGroup, GROUP_ID + 1)).thenReturn(Collections.singletonList(pingRule)); + + List result = manager.findInheritedGroupRules(BOOT_GROUP_ID, VM_ID); + assertEquals(1, result.size()); + } + + // ================================================================== + // invalidateCachedReadinessOnRestart + // ================================================================== + + @Test + public void invalidateCachedReadinessOnRestartNoBootGroupInvolvementIsNoOp() { + when(instanceBootGroupMemberDao.findByMember(MemberType.VirtualMachine, VM_ID)).thenReturn(null); + when(instanceGroupVMMapDao.listByInstanceId(VM_ID)).thenReturn(Collections.emptyList()); + + manager.invalidateCachedReadinessOnRestart(VM_ID); + + verify(instanceBootGroupReadinessCheckResultDao, never()).upsert(anyLong(), anyLong(), any(), any(), any()); + } + + @Test + public void invalidateCachedReadinessOnRestartDirectMemberInvalidatesOwnRules() { + when(instanceBootGroupMemberDao.findByMember(MemberType.VirtualMachine, VM_ID)).thenReturn(memberVO(BOOT_GROUP_ID, MemberType.VirtualMachine, VM_ID)); + InstanceBootGroupReadinessRuleVO pingRule = ruleVO(RULE_ID, BOOT_GROUP_ID, MemberType.VirtualMachine, VM_ID, RuleType.Ping, true); + when(instanceBootGroupReadinessRuleDao.listEnabledByItem(BOOT_GROUP_ID, MemberType.VirtualMachine, VM_ID)).thenReturn(Collections.singletonList(pingRule)); + when(instanceGroupVMMapDao.listByInstanceId(VM_ID)).thenReturn(Collections.emptyList()); + + manager.invalidateCachedReadinessOnRestart(VM_ID); + + verify(instanceBootGroupReadinessCheckResultDao).upsert(eq(RULE_ID), eq(0L), eq(Status.Unknown), any(), any(Date.class)); + } + + @Test + public void invalidateCachedReadinessOnRestartInheritedGroupRulesInvalidateOnlyMemberTargetedTypes() { + when(instanceBootGroupMemberDao.findByMember(MemberType.VirtualMachine, VM_ID)).thenReturn(null); + InstanceGroupVMMapVO mapping = new InstanceGroupVMMapVO(GROUP_ID, VM_ID); + when(instanceGroupVMMapDao.listByInstanceId(VM_ID)).thenReturn(Collections.singletonList(mapping)); + when(instanceBootGroupMemberDao.findByMember(MemberType.InstanceGroup, GROUP_ID)).thenReturn(memberVO(BOOT_GROUP_ID, MemberType.InstanceGroup, GROUP_ID)); + + InstanceBootGroupReadinessRuleVO pingRule = ruleVO(11L, BOOT_GROUP_ID, MemberType.InstanceGroup, GROUP_ID, RuleType.Ping, true); + InstanceBootGroupReadinessRuleVO quorumRule = ruleVO(12L, BOOT_GROUP_ID, MemberType.InstanceGroup, GROUP_ID, RuleType.MemberQuorum, true); + when(instanceBootGroupReadinessRuleDao.listEnabledByItem(BOOT_GROUP_ID, MemberType.InstanceGroup, GROUP_ID)) + .thenReturn(Arrays.asList(pingRule, quorumRule)); + + manager.invalidateCachedReadinessOnRestart(VM_ID); + + verify(instanceBootGroupReadinessCheckResultDao).upsert(eq(11L), eq(VM_ID), eq(Status.Unknown), any(), any(Date.class)); + verify(instanceBootGroupReadinessCheckResultDao, never()).upsert(eq(12L), anyLong(), any(), any(), any()); + } + + @Test + public void invalidateCachedReadinessOnRestartGroupNotABootGroupMemberIsNoOp() { + when(instanceBootGroupMemberDao.findByMember(MemberType.VirtualMachine, VM_ID)).thenReturn(null); + InstanceGroupVMMapVO mapping = new InstanceGroupVMMapVO(GROUP_ID, VM_ID); + when(instanceGroupVMMapDao.listByInstanceId(VM_ID)).thenReturn(Collections.singletonList(mapping)); + when(instanceBootGroupMemberDao.findByMember(MemberType.InstanceGroup, GROUP_ID)).thenReturn(null); + + manager.invalidateCachedReadinessOnRestart(VM_ID); + + verify(instanceBootGroupReadinessCheckResultDao, never()).upsert(anyLong(), anyLong(), any(), any(), any()); + } + + @Test + public void invalidateCachedReadinessOnRestartInvalidatesBothDirectAndInheritedRules() { + when(instanceBootGroupMemberDao.findByMember(MemberType.VirtualMachine, VM_ID)).thenReturn(memberVO(BOOT_GROUP_ID, MemberType.VirtualMachine, VM_ID)); + InstanceBootGroupReadinessRuleVO directRule = ruleVO(RULE_ID, BOOT_GROUP_ID, MemberType.VirtualMachine, VM_ID, RuleType.PortCheck, true); + when(instanceBootGroupReadinessRuleDao.listEnabledByItem(BOOT_GROUP_ID, MemberType.VirtualMachine, VM_ID)).thenReturn(Collections.singletonList(directRule)); + + InstanceGroupVMMapVO mapping = new InstanceGroupVMMapVO(GROUP_ID, VM_ID); + when(instanceGroupVMMapDao.listByInstanceId(VM_ID)).thenReturn(Collections.singletonList(mapping)); + when(instanceBootGroupMemberDao.findByMember(MemberType.InstanceGroup, GROUP_ID)).thenReturn(memberVO(BOOT_GROUP_ID, MemberType.InstanceGroup, GROUP_ID)); + InstanceBootGroupReadinessRuleVO groupRule = ruleVO(11L, BOOT_GROUP_ID, MemberType.InstanceGroup, GROUP_ID, RuleType.GuestAgentLiveness, true); + when(instanceBootGroupReadinessRuleDao.listEnabledByItem(BOOT_GROUP_ID, MemberType.InstanceGroup, GROUP_ID)).thenReturn(Collections.singletonList(groupRule)); + + manager.invalidateCachedReadinessOnRestart(VM_ID); + + verify(instanceBootGroupReadinessCheckResultDao).upsert(eq(RULE_ID), eq(0L), eq(Status.Unknown), any(), any(Date.class)); + verify(instanceBootGroupReadinessCheckResultDao).upsert(eq(11L), eq(VM_ID), eq(Status.Unknown), any(), any(Date.class)); + } + + // ================================================================== + // evaluateInstanceGroupReadiness + // ================================================================== + + private void stubNoRuleVm(long vmId, VirtualMachine.State state) { + UserVmVO vm56538 = mockVm(vmId, state, HypervisorType.KVM); + when(userVmDao.findById(vmId)).thenReturn(vm56538); + when(instanceBootGroupReadinessRuleDao.listEnabledByItem(BOOT_GROUP_ID, MemberType.VirtualMachine, vmId)).thenReturn(Collections.emptyList()); + when(instanceGroupVMMapDao.listByInstanceId(vmId)).thenReturn(Collections.emptyList()); + } + + @Test + public void evaluateInstanceGroupReadinessAllMembersReadyNoRules() { + when(instanceBootGroupReadinessRuleDao.listEnabledByItem(BOOT_GROUP_ID, MemberType.InstanceGroup, GROUP_ID)).thenReturn(Collections.emptyList()); + InstanceGroupVMMapVO m1 = new InstanceGroupVMMapVO(GROUP_ID, VM_ID); + InstanceGroupVMMapVO m2 = new InstanceGroupVMMapVO(GROUP_ID, VM_ID_2); + when(instanceGroupVMMapDao.listByGroupId(GROUP_ID)).thenReturn(Arrays.asList(m1, m2)); + stubNoRuleVm(VM_ID, VirtualMachine.State.Running); + stubNoRuleVm(VM_ID_2, VirtualMachine.State.Running); + + Status status = manager.evaluateInstanceGroupReadiness(BOOT_GROUP_ID, GROUP_ID, Collections.emptySet()); + assertEquals(Status.Ready, status); + } + + @Test + public void evaluateInstanceGroupReadinessMidRetryMemberIsNotReadyNotError() { + when(instanceBootGroupReadinessRuleDao.listEnabledByItem(BOOT_GROUP_ID, MemberType.InstanceGroup, GROUP_ID)).thenReturn(Collections.emptyList()); + InstanceGroupVMMapVO m1 = new InstanceGroupVMMapVO(GROUP_ID, VM_ID); + InstanceGroupVMMapVO m2 = new InstanceGroupVMMapVO(GROUP_ID, VM_ID_2); + when(instanceGroupVMMapDao.listByGroupId(GROUP_ID)).thenReturn(Arrays.asList(m1, m2)); + stubNoRuleVm(VM_ID, VirtualMachine.State.Running); + stubNoRuleVm(VM_ID_2, VirtualMachine.State.Starting); + + Status status = manager.evaluateInstanceGroupReadiness(BOOT_GROUP_ID, GROUP_ID, Collections.emptySet()); + assertEquals(Status.NotReady, status); + } + + @Test + public void evaluateInstanceGroupReadinessPermanentlyFailedMemberIsError() { + when(instanceBootGroupReadinessRuleDao.listEnabledByItem(BOOT_GROUP_ID, MemberType.InstanceGroup, GROUP_ID)).thenReturn(Collections.emptyList()); + InstanceGroupVMMapVO m1 = new InstanceGroupVMMapVO(GROUP_ID, VM_ID); + InstanceGroupVMMapVO m2 = new InstanceGroupVMMapVO(GROUP_ID, VM_ID_2); + when(instanceGroupVMMapDao.listByGroupId(GROUP_ID)).thenReturn(Arrays.asList(m1, m2)); + stubNoRuleVm(VM_ID, VirtualMachine.State.Running); + stubNoRuleVm(VM_ID_2, VirtualMachine.State.Starting); + + Set permanentlyFailed = new HashSet<>(Collections.singletonList(VM_ID_2)); + Status status = manager.evaluateInstanceGroupReadiness(BOOT_GROUP_ID, GROUP_ID, permanentlyFailed); + assertEquals(Status.Error, status); + } + + @Test + public void evaluateInstanceGroupReadinessQuorumExcludesOwnMemberStatuses() { + InstanceBootGroupReadinessRuleVO quorumRule = ruleVO(RULE_ID, BOOT_GROUP_ID, MemberType.InstanceGroup, GROUP_ID, RuleType.MemberQuorum, true); + when(instanceBootGroupReadinessRuleDao.listEnabledByItem(BOOT_GROUP_ID, MemberType.InstanceGroup, GROUP_ID)).thenReturn(Collections.singletonList(quorumRule)); + Map details = new HashMap<>(); + details.put("threshold_type", "COUNT"); + details.put("threshold_value", "1"); + when(instanceBootGroupReadinessRuleDetailsDao.getDetails(RULE_ID)).thenReturn(details); + + InstanceGroupVMMapVO m1 = new InstanceGroupVMMapVO(GROUP_ID, VM_ID); + InstanceGroupVMMapVO m2 = new InstanceGroupVMMapVO(GROUP_ID, VM_ID_2); + when(instanceGroupVMMapDao.listByGroupId(GROUP_ID)).thenReturn(Arrays.asList(m1, m2)); + stubNoRuleVm(VM_ID, VirtualMachine.State.Running); + stubNoRuleVm(VM_ID_2, VirtualMachine.State.Stopped); // NotReady, but must not affect result since quorum-governed + + Status status = manager.evaluateInstanceGroupReadiness(BOOT_GROUP_ID, GROUP_ID, Collections.emptySet()); + + assertEquals(Status.Ready, status); + verify(instanceBootGroupReadinessCheckResultDao).upsert(eq(RULE_ID), eq(0L), eq(Status.Ready), any(), any(Date.class)); + } + + @Test + public void evaluateInstanceGroupReadinessQuorumMetOverridesFailingMemberTargetedRule() { + InstanceBootGroupReadinessRuleVO guestAgentRule = ruleVO(RULE_ID, BOOT_GROUP_ID, MemberType.InstanceGroup, GROUP_ID, RuleType.GuestAgentLiveness, true); + InstanceBootGroupReadinessRuleVO quorumRule = ruleVO(20L, BOOT_GROUP_ID, MemberType.InstanceGroup, GROUP_ID, RuleType.MemberQuorum, true); + when(instanceBootGroupReadinessRuleDao.listEnabledByItem(BOOT_GROUP_ID, MemberType.InstanceGroup, GROUP_ID)) + .thenReturn(Arrays.asList(guestAgentRule, quorumRule)); + when(instanceBootGroupReadinessRuleDetailsDao.getDetails(20L)).thenReturn(thresholdDetails("COUNT", "1")); + + InstanceGroupVMMapVO m1 = new InstanceGroupVMMapVO(GROUP_ID, VM_ID); + InstanceGroupVMMapVO m2 = new InstanceGroupVMMapVO(GROUP_ID, VM_ID_2); + when(instanceGroupVMMapDao.listByGroupId(GROUP_ID)).thenReturn(Arrays.asList(m1, m2)); + stubNoRuleVm(VM_ID, VirtualMachine.State.Running); + stubNoRuleVm(VM_ID_2, VirtualMachine.State.Running); + + InstanceBootGroupReadinessCheckResultVO ready = new InstanceBootGroupReadinessCheckResultVO(RULE_ID, VM_ID, Status.Ready, "ok", new Date()); + InstanceBootGroupReadinessCheckResultVO notReady = new InstanceBootGroupReadinessCheckResultVO(RULE_ID, VM_ID_2, Status.NotReady, "no agent", new Date()); + when(instanceBootGroupReadinessCheckResultDao.findByRuleAndVm(RULE_ID, VM_ID)).thenReturn(ready); + when(instanceBootGroupReadinessCheckResultDao.findByRuleAndVm(RULE_ID, VM_ID_2)).thenReturn(notReady); + + Status status = manager.evaluateInstanceGroupReadiness(BOOT_GROUP_ID, GROUP_ID, Collections.emptySet()); + + assertEquals(Status.Ready, status); + verify(instanceBootGroupReadinessCheckResultDao).upsert(eq(RULE_ID), eq(0L), eq(Status.NotReady), any(), any(Date.class)); + verify(instanceBootGroupReadinessCheckResultDao).upsert(eq(20L), eq(0L), eq(Status.Ready), any(), any(Date.class)); + } + + @Test + public void evaluateInstanceGroupReadinessMemberTargetedRuleAggregatesCachedResults() { + InstanceBootGroupReadinessRuleVO pingRule = ruleVO(RULE_ID, BOOT_GROUP_ID, MemberType.InstanceGroup, GROUP_ID, RuleType.Ping, true); + when(instanceBootGroupReadinessRuleDao.listEnabledByItem(BOOT_GROUP_ID, MemberType.InstanceGroup, GROUP_ID)).thenReturn(Collections.singletonList(pingRule)); + + InstanceGroupVMMapVO m1 = new InstanceGroupVMMapVO(GROUP_ID, VM_ID); + InstanceGroupVMMapVO m2 = new InstanceGroupVMMapVO(GROUP_ID, VM_ID_2); + when(instanceGroupVMMapDao.listByGroupId(GROUP_ID)).thenReturn(Arrays.asList(m1, m2)); + stubNoRuleVm(VM_ID, VirtualMachine.State.Running); + stubNoRuleVm(VM_ID_2, VirtualMachine.State.Running); + + InstanceBootGroupReadinessCheckResultVO cached1 = new InstanceBootGroupReadinessCheckResultVO(RULE_ID, VM_ID, Status.Ready, "ok", new Date()); + InstanceBootGroupReadinessCheckResultVO cached2 = new InstanceBootGroupReadinessCheckResultVO(RULE_ID, VM_ID_2, Status.Ready, "ok", new Date()); + when(instanceBootGroupReadinessCheckResultDao.findByRuleAndVm(RULE_ID, VM_ID)).thenReturn(cached1); + when(instanceBootGroupReadinessCheckResultDao.findByRuleAndVm(RULE_ID, VM_ID_2)).thenReturn(cached2); + + Status status = manager.evaluateInstanceGroupReadiness(BOOT_GROUP_ID, GROUP_ID, Collections.emptySet()); + + assertEquals(Status.Ready, status); + verify(instanceBootGroupReadinessCheckResultDao).upsert(eq(RULE_ID), eq(0L), eq(Status.Ready), eq("2 of 2 member(s) ready via Ping"), any(Date.class)); + } + + @Test + public void evaluateInstanceGroupReadinessMemberTargetedRuleNoMembersIsNotReady() { + InstanceBootGroupReadinessRuleVO pingRule = ruleVO(RULE_ID, BOOT_GROUP_ID, MemberType.InstanceGroup, GROUP_ID, RuleType.Ping, true); + when(instanceBootGroupReadinessRuleDao.listEnabledByItem(BOOT_GROUP_ID, MemberType.InstanceGroup, GROUP_ID)).thenReturn(Collections.singletonList(pingRule)); + when(instanceGroupVMMapDao.listByGroupId(GROUP_ID)).thenReturn(Collections.emptyList()); + + Status status = manager.evaluateInstanceGroupReadiness(BOOT_GROUP_ID, GROUP_ID, Collections.emptySet()); + + assertEquals(Status.NotReady, status); + verify(instanceBootGroupReadinessCheckResultDao).upsert(eq(RULE_ID), eq(0L), eq(Status.NotReady), eq("Instance group has no members"), any(Date.class)); + } + + @Test + public void evaluateInstanceGroupReadinessMemberTargetedRuleAnyErrorIsError() { + InstanceBootGroupReadinessRuleVO pingRule = ruleVO(RULE_ID, BOOT_GROUP_ID, MemberType.InstanceGroup, GROUP_ID, RuleType.Ping, true); + when(instanceBootGroupReadinessRuleDao.listEnabledByItem(BOOT_GROUP_ID, MemberType.InstanceGroup, GROUP_ID)).thenReturn(Collections.singletonList(pingRule)); + + InstanceGroupVMMapVO m1 = new InstanceGroupVMMapVO(GROUP_ID, VM_ID); + when(instanceGroupVMMapDao.listByGroupId(GROUP_ID)).thenReturn(Collections.singletonList(m1)); + stubNoRuleVm(VM_ID, VirtualMachine.State.Running); + InstanceBootGroupReadinessCheckResultVO cached = new InstanceBootGroupReadinessCheckResultVO(RULE_ID, VM_ID, Status.Error, "unreachable", new Date()); + when(instanceBootGroupReadinessCheckResultDao.findByRuleAndVm(RULE_ID, VM_ID)).thenReturn(cached); + + Status status = manager.evaluateInstanceGroupReadiness(BOOT_GROUP_ID, GROUP_ID, Collections.emptySet()); + assertEquals(Status.Error, status); + } + + @Test + public void evaluateInstanceGroupReadinessUnimplementedGroupRuleTypeIsError() { + InstanceBootGroupReadinessRuleVO scriptRule = ruleVO(RULE_ID, BOOT_GROUP_ID, MemberType.InstanceGroup, GROUP_ID, RuleType.CustomScript, true); + when(instanceBootGroupReadinessRuleDao.listEnabledByItem(BOOT_GROUP_ID, MemberType.InstanceGroup, GROUP_ID)).thenReturn(Collections.singletonList(scriptRule)); + when(instanceGroupVMMapDao.listByGroupId(GROUP_ID)).thenReturn(Collections.emptyList()); + + Status status = manager.evaluateInstanceGroupReadiness(BOOT_GROUP_ID, GROUP_ID, Collections.emptySet()); + + assertEquals(Status.Error, status); + verify(instanceBootGroupReadinessCheckResultDao).upsert(eq(RULE_ID), eq(0L), eq(Status.Error), + eq("No evaluator implemented yet for rule type CustomScript"), any(Date.class)); + } + + // ================================================================== + // evaluateInstanceQuorum (via evaluateInstanceGroupReadiness + MemberQuorum rule) + // ================================================================== + + private Status evaluateQuorum(Map details, List members, Set permanentlyFailed) { + InstanceBootGroupReadinessRuleVO quorumRule = ruleVO(RULE_ID, BOOT_GROUP_ID, MemberType.InstanceGroup, GROUP_ID, RuleType.MemberQuorum, true); + when(instanceBootGroupReadinessRuleDao.listEnabledByItem(BOOT_GROUP_ID, MemberType.InstanceGroup, GROUP_ID)).thenReturn(Collections.singletonList(quorumRule)); + when(instanceBootGroupReadinessRuleDetailsDao.getDetails(RULE_ID)).thenReturn(details); + when(instanceGroupVMMapDao.listByGroupId(GROUP_ID)).thenReturn(members); + return manager.evaluateInstanceGroupReadiness(BOOT_GROUP_ID, GROUP_ID, permanentlyFailed); + } + + private Map thresholdDetails(String type, String value) { + Map details = new HashMap<>(); + details.put("threshold_type", type); + details.put("threshold_value", value); + return details; + } + + @Test + public void evaluateInstanceQuorumNoMembersIsNotReady() { + Status status = evaluateQuorum(thresholdDetails("COUNT", "1"), Collections.emptyList(), Collections.emptySet()); + assertEquals(Status.NotReady, status); + verify(instanceBootGroupReadinessCheckResultDao).upsert(eq(RULE_ID), eq(0L), eq(Status.NotReady), eq("Instance group has no members"), any(Date.class)); + } + + @Test + public void evaluateInstanceQuorumPercentageThresholdMet() { + List members = new ArrayList<>(); + for (long i = 0; i < 4; i++) { + members.add(new InstanceGroupVMMapVO(GROUP_ID, VM_ID + i)); + stubNoRuleVm(VM_ID + i, i < 3 ? VirtualMachine.State.Running : VirtualMachine.State.Stopped); + } + Status status = evaluateQuorum(thresholdDetails("PERCENTAGE", "50"), members, Collections.emptySet()); + assertEquals(Status.Ready, status); + } + + @Test + public void evaluateInstanceQuorumPercentageThresholdNotMetButAchievable() { + List members = new ArrayList<>(); + for (long i = 0; i < 4; i++) { + members.add(new InstanceGroupVMMapVO(GROUP_ID, VM_ID + i)); + stubNoRuleVm(VM_ID + i, i < 1 ? VirtualMachine.State.Running : VirtualMachine.State.Stopped); + } + Status status = evaluateQuorum(thresholdDetails("PERCENTAGE", "50"), members, Collections.emptySet()); + assertEquals(Status.NotReady, status); + } + + @Test + public void evaluateInstanceQuorumPercentageThresholdUnreachableIsError() { + List members = new ArrayList<>(); + Set permanentlyFailed = new HashSet<>(); + for (long i = 0; i < 4; i++) { + long vmId = VM_ID + i; + members.add(new InstanceGroupVMMapVO(GROUP_ID, vmId)); + if (i < 1) { + stubNoRuleVm(vmId, VirtualMachine.State.Running); + } else { + stubNoRuleVm(vmId, VirtualMachine.State.Stopped); + permanentlyFailed.add(vmId); + } + } + // 1 ready, achievableCount = 4 - 3 = 1, 25% < 90% -> unreachable + Status status = evaluateQuorum(thresholdDetails("PERCENTAGE", "90"), members, permanentlyFailed); + assertEquals(Status.Error, status); + } + + @Test + public void evaluateInstanceQuorumCountThresholdMet() { + List members = new ArrayList<>(); + for (long i = 0; i < 3; i++) { + members.add(new InstanceGroupVMMapVO(GROUP_ID, VM_ID + i)); + stubNoRuleVm(VM_ID + i, i < 2 ? VirtualMachine.State.Running : VirtualMachine.State.Stopped); + } + Status status = evaluateQuorum(thresholdDetails("COUNT", "2"), members, Collections.emptySet()); + assertEquals(Status.Ready, status); + } + + @Test + public void evaluateInstanceQuorumInvalidThresholdValueIsError() { + List members = Collections.singletonList(new InstanceGroupVMMapVO(GROUP_ID, VM_ID)); + stubNoRuleVm(VM_ID, VirtualMachine.State.Running); + Status status = evaluateQuorum(thresholdDetails("COUNT", "abc"), members, Collections.emptySet()); + assertEquals(Status.Error, status); + verify(instanceBootGroupReadinessCheckResultDao).upsert(eq(RULE_ID), eq(0L), eq(Status.Error), + eq("Invalid threshold configuration: COUNT=abc"), any(Date.class)); + } +} diff --git a/server/src/test/java/org/apache/cloudstack/vm/bootgroup/readiness/PortCheckCheckerTest.java b/server/src/test/java/org/apache/cloudstack/vm/bootgroup/readiness/PortCheckCheckerTest.java new file mode 100644 index 000000000000..f64ffb4dfd9b --- /dev/null +++ b/server/src/test/java/org/apache/cloudstack/vm/bootgroup/readiness/PortCheckCheckerTest.java @@ -0,0 +1,245 @@ +// 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.vm.bootgroup.readiness; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertTrue; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + +import java.util.Collections; +import java.util.HashMap; +import java.util.Map; + +import org.apache.cloudstack.engine.orchestration.service.NetworkOrchestrationService; +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.InjectMocks; +import org.mockito.Mock; +import org.mockito.junit.MockitoJUnitRunner; + +import com.cloud.agent.AgentManager; +import com.cloud.agent.api.routing.NetworkElementCommand; +import com.cloud.hypervisor.Hypervisor.HypervisorType; +import com.cloud.network.Network; +import com.cloud.network.dao.NetworkDao; +import com.cloud.network.dao.NetworkVO; +import com.cloud.network.router.VirtualRouter; +import com.cloud.network.router.VpcVirtualNetworkApplianceManager; +import com.cloud.vm.NicVO; +import com.cloud.vm.UserVmVO; +import com.cloud.vm.VirtualMachine; +import com.cloud.vm.VirtualMachineManager; +import com.cloud.vm.dao.NicDao; +import com.cloud.vm.dao.UserVmDao; + +@RunWith(MockitoJUnitRunner.class) +public class PortCheckCheckerTest { + + private static final long VM_ID = 100L; + private static final long NETWORK_ID = 200L; + private static final long HOST_ID = 300L; + private static final String IP = "10.1.1.5"; + + @InjectMocks + PortCheckChecker checker; + + @Mock + UserVmDao userVmDao; + @Mock + NicDao nicDao; + @Mock + NetworkDao networkDao; + @Mock + VpcVirtualNetworkApplianceManager virtualNetworkApplianceManager; + @Mock + VirtualMachineManager virtualMachineManager; + @Mock + NetworkOrchestrationService networkOrchestrationService; + @Mock + AgentManager agentManager; + + private UserVmVO vm; + private VirtualRouter router; + private Map details; + + @Before + public void setUp() { + vm = mock(UserVmVO.class); + when(userVmDao.findById(VM_ID)).thenReturn(vm); + + details = new HashMap<>(); + details.put("port", "80"); + details.put("protocol", "tcp"); + + NicVO nic = mock(NicVO.class); + when(nic.getIPv4Address()).thenReturn(IP); + when(nic.getNetworkId()).thenReturn(NETWORK_ID); + when(nicDao.findDefaultNicForVM(VM_ID)).thenReturn(nic); + + NetworkVO network = mock(NetworkVO.class); + when(network.getGuestType()).thenReturn(Network.GuestType.Isolated); + when(networkDao.findById(NETWORK_ID)).thenReturn(network); + + router = mock(VirtualRouter.class); + when(router.getState()).thenReturn(VirtualMachine.State.Running); + when(router.getHostId()).thenReturn(HOST_ID); + when(router.getHypervisorType()).thenReturn(HypervisorType.KVM); + when(virtualNetworkApplianceManager.getRoutersForNetwork(NETWORK_ID)).thenReturn(Collections.singletonList(router)); + + when(virtualMachineManager.getExecuteInSequence(HypervisorType.KVM)).thenReturn(false); + + Map accessDetails = new HashMap<>(); + accessDetails.put(NetworkElementCommand.ROUTER_IP, "10.1.1.1"); + when(networkOrchestrationService.getSystemVMAccessDetails(router)).thenReturn(accessDetails); + } + + private InstanceBootGroupReadinessRule rule() { + return mock(InstanceBootGroupReadinessRule.class); + } + + @Test + public void getRuleTypeIsPortCheck() { + assertEquals(InstanceBootGroupReadinessRule.RuleType.PortCheck, checker.getRuleType()); + } + + @Test + public void vmNotFoundIsError() { + when(userVmDao.findById(VM_ID)).thenReturn(null); + ReadinessChecker.Result result = checker.check(rule(), details, VM_ID, 60000); + assertEquals(InstanceBootGroupReadinessRule.Status.Error, result.getStatus()); + } + + @Test + public void nonTcpProtocolIsError() { + details.put("protocol", "udp"); + ReadinessChecker.Result result = checker.check(rule(), details, VM_ID, 60000); + assertEquals(InstanceBootGroupReadinessRule.Status.Error, result.getStatus()); + assertTrue(result.getMessage().contains("Only tcp")); + } + + @Test + public void blankProtocolDefaultsToTcp() { + details.remove("protocol"); + when(agentManager.easySend(eq(HOST_ID), any())).thenReturn(readinessAnswer(true, "out&&err&&0")); + ReadinessChecker.Result result = checker.check(rule(), details, VM_ID, 60000); + assertEquals(InstanceBootGroupReadinessRule.Status.Ready, result.getStatus()); + } + + @Test + public void missingPortIsError() { + details.remove("port"); + ReadinessChecker.Result result = checker.check(rule(), details, VM_ID, 60000); + assertEquals(InstanceBootGroupReadinessRule.Status.Error, result.getStatus()); + assertTrue(result.getMessage().contains("Invalid or missing port")); + } + + @Test + public void nonNumericPortIsError() { + details.put("port", "not-a-number"); + ReadinessChecker.Result result = checker.check(rule(), details, VM_ID, 60000); + assertEquals(InstanceBootGroupReadinessRule.Status.Error, result.getStatus()); + } + + @Test + public void portOutOfRangeIsError() { + details.put("port", "70000"); + ReadinessChecker.Result result = checker.check(rule(), details, VM_ID, 60000); + assertEquals(InstanceBootGroupReadinessRule.Status.Error, result.getStatus()); + } + + @Test + public void zeroPortIsError() { + details.put("port", "0"); + ReadinessChecker.Result result = checker.check(rule(), details, VM_ID, 60000); + assertEquals(InstanceBootGroupReadinessRule.Status.Error, result.getStatus()); + } + + @Test + public void noDefaultNicIsError() { + when(nicDao.findDefaultNicForVM(VM_ID)).thenReturn(null); + ReadinessChecker.Result result = checker.check(rule(), details, VM_ID, 60000); + assertEquals(InstanceBootGroupReadinessRule.Status.Error, result.getStatus()); + } + + @Test + public void l2NetworkIsError() { + NetworkVO l2Network = mock(NetworkVO.class); + when(l2Network.getGuestType()).thenReturn(Network.GuestType.L2); + when(networkDao.findById(NETWORK_ID)).thenReturn(l2Network); + ReadinessChecker.Result result = checker.check(rule(), details, VM_ID, 60000); + assertEquals(InstanceBootGroupReadinessRule.Status.Error, result.getStatus()); + } + + @Test + public void noRunningRouterIsError() { + when(virtualNetworkApplianceManager.getRoutersForNetwork(NETWORK_ID)).thenReturn(Collections.emptyList()); + ReadinessChecker.Result result = checker.check(rule(), details, VM_ID, 60000); + assertEquals(InstanceBootGroupReadinessRule.Status.Error, result.getStatus()); + } + + @Test + public void missingRouterControlIpIsError() { + when(networkOrchestrationService.getSystemVMAccessDetails(router)).thenReturn(new HashMap<>()); + ReadinessChecker.Result result = checker.check(rule(), details, VM_ID, 60000); + assertEquals(InstanceBootGroupReadinessRule.Status.Error, result.getStatus()); + } + + @Test + public void insufficientBudgetIsError() { + ReadinessChecker.Result result = checker.check(rule(), details, VM_ID, 500); + assertEquals(InstanceBootGroupReadinessRule.Status.Error, result.getStatus()); + } + + @Test + public void dispatchExceptionIsError() { + when(agentManager.easySend(eq(HOST_ID), any())).thenThrow(new RuntimeException("agent down")); + ReadinessChecker.Result result = checker.check(rule(), details, VM_ID, 60000); + assertEquals(InstanceBootGroupReadinessRule.Status.Error, result.getStatus()); + } + + @Test + public void nullAnswerIsError() { + when(agentManager.easySend(eq(HOST_ID), any())).thenReturn(null); + ReadinessChecker.Result result = checker.check(rule(), details, VM_ID, 60000); + assertEquals(InstanceBootGroupReadinessRule.Status.Error, result.getStatus()); + } + + @Test + public void exitCodeZeroIsReady() { + when(agentManager.easySend(eq(HOST_ID), any())).thenReturn(readinessAnswer(true, "out&&err&&0")); + ReadinessChecker.Result result = checker.check(rule(), details, VM_ID, 60000); + assertEquals(InstanceBootGroupReadinessRule.Status.Ready, result.getStatus()); + assertEquals("port 80/tcp is open", result.getMessage()); + } + + @Test + public void nonZeroExitCodeIsError() { + when(agentManager.easySend(eq(HOST_ID), any())).thenReturn(readinessAnswer(true, "out&&connection refused&&1")); + ReadinessChecker.Result result = checker.check(rule(), details, VM_ID, 60000); + assertEquals(InstanceBootGroupReadinessRule.Status.Error, result.getStatus()); + assertTrue(result.getMessage().contains("connection refused")); + } + + private InstanceReadinessCheckAnswer readinessAnswer(boolean result, String rawDetails) { + InstanceReadinessCheckCommand cmd = new InstanceReadinessCheckCommand(IP, 80, false); + return new InstanceReadinessCheckAnswer(cmd, result, rawDetails); + } +} diff --git a/server/src/test/java/org/apache/cloudstack/vm/bootgroup/readiness/VrPingCheckerTest.java b/server/src/test/java/org/apache/cloudstack/vm/bootgroup/readiness/VrPingCheckerTest.java new file mode 100644 index 000000000000..4536c1662391 --- /dev/null +++ b/server/src/test/java/org/apache/cloudstack/vm/bootgroup/readiness/VrPingCheckerTest.java @@ -0,0 +1,244 @@ +// 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.vm.bootgroup.readiness; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertTrue; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + +import java.util.Collections; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +import org.apache.cloudstack.engine.orchestration.service.NetworkOrchestrationService; +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.ArgumentCaptor; +import org.mockito.InjectMocks; +import org.mockito.Mock; +import org.mockito.junit.MockitoJUnitRunner; + +import com.cloud.agent.AgentManager; +import com.cloud.agent.api.routing.NetworkElementCommand; +import com.cloud.hypervisor.Hypervisor.HypervisorType; +import com.cloud.network.Network; +import com.cloud.network.dao.NetworkDao; +import com.cloud.network.dao.NetworkVO; +import com.cloud.network.router.VirtualRouter; +import com.cloud.network.router.VpcVirtualNetworkApplianceManager; +import com.cloud.vm.NicVO; +import com.cloud.vm.UserVmVO; +import com.cloud.vm.VirtualMachine; +import com.cloud.vm.VirtualMachineManager; +import com.cloud.vm.dao.NicDao; +import com.cloud.vm.dao.UserVmDao; + +@RunWith(MockitoJUnitRunner.class) +public class VrPingCheckerTest { + + private static final long VM_ID = 100L; + private static final long NETWORK_ID = 200L; + private static final long HOST_ID = 300L; + private static final String IP = "10.1.1.5"; + + @InjectMocks + VrPingChecker checker; + + @Mock + UserVmDao userVmDao; + @Mock + NicDao nicDao; + @Mock + NetworkDao networkDao; + @Mock + VpcVirtualNetworkApplianceManager virtualNetworkApplianceManager; + @Mock + VirtualMachineManager virtualMachineManager; + @Mock + NetworkOrchestrationService networkOrchestrationService; + @Mock + AgentManager agentManager; + + private UserVmVO vm; + private NicVO nic; + private VirtualRouter router; + + @Before + public void setUp() { + vm = mock(UserVmVO.class); + when(userVmDao.findById(VM_ID)).thenReturn(vm); + + nic = mock(NicVO.class); + when(nic.getIPv4Address()).thenReturn(IP); + when(nic.getNetworkId()).thenReturn(NETWORK_ID); + when(nicDao.findDefaultNicForVM(VM_ID)).thenReturn(nic); + + NetworkVO network = mock(NetworkVO.class); + when(network.getGuestType()).thenReturn(Network.GuestType.Isolated); + when(networkDao.findById(NETWORK_ID)).thenReturn(network); + + router = mock(VirtualRouter.class); + when(router.getState()).thenReturn(VirtualMachine.State.Running); + when(router.getHostId()).thenReturn(HOST_ID); + when(router.getHypervisorType()).thenReturn(HypervisorType.KVM); + when(virtualNetworkApplianceManager.getRoutersForNetwork(NETWORK_ID)).thenReturn(Collections.singletonList(router)); + + when(virtualMachineManager.getExecuteInSequence(HypervisorType.KVM)).thenReturn(false); + + Map accessDetails = new HashMap<>(); + accessDetails.put(NetworkElementCommand.ROUTER_IP, "10.1.1.1"); + when(networkOrchestrationService.getSystemVMAccessDetails(router)).thenReturn(accessDetails); + } + + private InstanceBootGroupReadinessRule rule() { + return mock(InstanceBootGroupReadinessRule.class); + } + + @Test + public void getRuleTypeIsPing() { + assertEquals(InstanceBootGroupReadinessRule.RuleType.Ping, checker.getRuleType()); + } + + @Test + public void vmNotFoundIsError() { + when(userVmDao.findById(VM_ID)).thenReturn(null); + ReadinessChecker.Result result = checker.check(rule(), null, VM_ID, 60000); + assertEquals(InstanceBootGroupReadinessRule.Status.Error, result.getStatus()); + assertEquals("VM not found", result.getMessage()); + } + + @Test + public void noDefaultNicIsError() { + when(nicDao.findDefaultNicForVM(VM_ID)).thenReturn(null); + ReadinessChecker.Result result = checker.check(rule(), null, VM_ID, 60000); + assertEquals(InstanceBootGroupReadinessRule.Status.Error, result.getStatus()); + assertTrue(result.getMessage().contains("no default NIC")); + } + + @Test + public void blankIpv4AddressIsError() { + when(nic.getIPv4Address()).thenReturn(""); + ReadinessChecker.Result result = checker.check(rule(), null, VM_ID, 60000); + assertEquals(InstanceBootGroupReadinessRule.Status.Error, result.getStatus()); + } + + @Test + public void l2NetworkIsError() { + NetworkVO l2Network = mock(NetworkVO.class); + when(l2Network.getGuestType()).thenReturn(Network.GuestType.L2); + when(networkDao.findById(NETWORK_ID)).thenReturn(l2Network); + + ReadinessChecker.Result result = checker.check(rule(), null, VM_ID, 60000); + assertEquals(InstanceBootGroupReadinessRule.Status.Error, result.getStatus()); + assertTrue(result.getMessage().contains("L2 network")); + } + + @Test + public void noRunningRouterIsError() { + when(virtualNetworkApplianceManager.getRoutersForNetwork(NETWORK_ID)).thenReturn(Collections.emptyList()); + ReadinessChecker.Result result = checker.check(rule(), null, VM_ID, 60000); + assertEquals(InstanceBootGroupReadinessRule.Status.Error, result.getStatus()); + assertTrue(result.getMessage().contains("No running VR found")); + } + + @Test + public void routerWithoutHostIdIsError() { + when(router.getHostId()).thenReturn(null); + ReadinessChecker.Result result = checker.check(rule(), null, VM_ID, 60000); + assertEquals(InstanceBootGroupReadinessRule.Status.Error, result.getStatus()); + assertTrue(result.getMessage().contains("No running VR found")); + } + + @Test + public void nonRunningRouterIsSkippedForRunningOne() { + VirtualRouter stoppedRouter = mock(VirtualRouter.class); + when(stoppedRouter.getState()).thenReturn(VirtualMachine.State.Stopped); + when(virtualNetworkApplianceManager.getRoutersForNetwork(NETWORK_ID)).thenReturn(List.of(stoppedRouter, router)); + when(agentManager.easySend(eq(HOST_ID), any())).thenReturn(readinessAnswer(true, "out&&err&&0")); + + ReadinessChecker.Result result = checker.check(rule(), null, VM_ID, 60000); + assertEquals(InstanceBootGroupReadinessRule.Status.Ready, result.getStatus()); + } + + @Test + public void missingRouterControlIpIsError() { + when(networkOrchestrationService.getSystemVMAccessDetails(router)).thenReturn(new HashMap<>()); + ReadinessChecker.Result result = checker.check(rule(), null, VM_ID, 60000); + assertEquals(InstanceBootGroupReadinessRule.Status.Error, result.getStatus()); + assertTrue(result.getMessage().contains("control IP")); + } + + @Test + public void insufficientBudgetIsError() { + ReadinessChecker.Result result = checker.check(rule(), null, VM_ID, 500); + assertEquals(InstanceBootGroupReadinessRule.Status.Error, result.getStatus()); + assertTrue(result.getMessage().contains("Insufficient time remaining")); + } + + @Test + public void dispatchExceptionIsError() { + when(agentManager.easySend(eq(HOST_ID), any())).thenThrow(new RuntimeException("agent down")); + ReadinessChecker.Result result = checker.check(rule(), null, VM_ID, 60000); + assertEquals(InstanceBootGroupReadinessRule.Status.Error, result.getStatus()); + assertTrue(result.getMessage().contains("agent down")); + } + + @Test + public void nullAnswerIsError() { + when(agentManager.easySend(eq(HOST_ID), any())).thenReturn(null); + ReadinessChecker.Result result = checker.check(rule(), null, VM_ID, 60000); + assertEquals(InstanceBootGroupReadinessRule.Status.Error, result.getStatus()); + assertTrue(result.getMessage().contains("No answer")); + } + + @Test + public void exitCodeZeroIsReady() { + when(agentManager.easySend(eq(HOST_ID), any())).thenReturn(readinessAnswer(true, "out&&err&&0")); + ReadinessChecker.Result result = checker.check(rule(), null, VM_ID, 60000); + assertEquals(InstanceBootGroupReadinessRule.Status.Ready, result.getStatus()); + assertEquals("ping succeeded", result.getMessage()); + } + + @Test + public void nonZeroExitCodeIsError() { + when(agentManager.easySend(eq(HOST_ID), any())).thenReturn(readinessAnswer(true, "out&&unreachable&&1")); + ReadinessChecker.Result result = checker.check(rule(), null, VM_ID, 60000); + assertEquals(InstanceBootGroupReadinessRule.Status.Error, result.getStatus()); + assertTrue(result.getMessage().contains("unreachable")); + } + + @Test + public void waitIsHalvedRemainingBudgetFlooredAtOne() { + when(agentManager.easySend(eq(HOST_ID), any())).thenReturn(readinessAnswer(true, "out&&err&&0")); + ArgumentCaptor captor = ArgumentCaptor.forClass(InstanceReadinessCheckCommand.class); + + checker.check(rule(), null, VM_ID, 2500); + + org.mockito.Mockito.verify(agentManager).easySend(eq(HOST_ID), captor.capture()); + assertEquals(1, captor.getValue().getWait()); + } + + private InstanceReadinessCheckAnswer readinessAnswer(boolean result, String details) { + InstanceReadinessCheckCommand cmd = new InstanceReadinessCheckCommand(IP, false); + return new InstanceReadinessCheckAnswer(cmd, result, details); + } +} diff --git a/systemvm/debian/opt/cloud/bin/instance_readiness_check.py b/systemvm/debian/opt/cloud/bin/instance_readiness_check.py new file mode 100644 index 000000000000..2d68ccc513f7 --- /dev/null +++ b/systemvm/debian/opt/cloud/bin/instance_readiness_check.py @@ -0,0 +1,71 @@ +#!/usr/bin/env python +# 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. + +# Standalone readiness-check helper for the instance boot group feature. Kept separate from +# diagnostics.py, which is a general-purpose admin tool for system VMs: this script is invoked +# on behalf of user instance readiness rules and should not share code/behaviour with that tool. + +import socket +import subprocess +import sys + + +def emit(stdout, stderr, exit_code): + print('%s&&' % stdout) + print('%s&&' % stderr) + print('%s' % exit_code) + + +def check_ping(host, count=4): + try: + p = subprocess.Popen(['ping', '-c', str(count), host], stdout=subprocess.PIPE, stderr=subprocess.PIPE) + stdout, stderr = p.communicate() + emit(stdout.decode().strip(), stderr.decode().strip(), p.returncode) + except OSError as e: + emit('', 'Exception occurred: %s' % e, 1) + + +def check_port(host, port, timeout=3): + try: + with socket.create_connection((host, int(port)), timeout=timeout): + emit('connected', '', 0) + except Exception as e: + emit('', str(e), 1) + + +def main(): + if len(sys.argv) < 3: + emit('', 'Usage: instance_readiness_check.py [port]', 1) + return + + check_type = sys.argv[1] + host = sys.argv[2] + + if check_type == 'ping': + check_ping(host) + elif check_type == 'portcheck': + if len(sys.argv) < 4: + emit('', 'portcheck requires a port argument', 1) + return + check_port(host, sys.argv[3]) + else: + emit('', 'Unknown check type: %s' % check_type, 1) + + +if __name__ == "__main__": + main() diff --git a/test/integration/smoke/test_instance_boot_group.py b/test/integration/smoke/test_instance_boot_group.py new file mode 100644 index 000000000000..69c0e333637e --- /dev/null +++ b/test/integration/smoke/test_instance_boot_group.py @@ -0,0 +1,310 @@ +# 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. +""" BVT tests for Instance Boot Group and readiness rules lifecycle +""" +# Import Local Modules +from marvin.cloudstackTestCase import cloudstackTestCase +from marvin.lib.base import (Account, + ServiceOffering, + VirtualMachine, + InstanceGroup, + InstanceBootGroup, + InstanceBootGroupReadinessRule) +from marvin.lib.common import (get_domain, + get_zone, + get_template) +from marvin.lib.utils import (random_gen) +from marvin.cloudstackException import CloudstackAPIException +from marvin.codes import FAILED +from nose.plugins.attrib import attr +import logging +import time + +_multiprocess_shared_ = True + + +class TestInstanceBootGroup(cloudstackTestCase): + + @classmethod + def setUpClass(cls): + testClient = super(TestInstanceBootGroup, cls).getClsTestClient() + cls.apiclient = testClient.getApiClient() + cls.services = testClient.getParsedTestDataConfig() + cls.logger = logging.getLogger('TestInstanceBootGroup') + cls.logger.setLevel(logging.DEBUG) + + cls.domain = get_domain(cls.apiclient) + cls.zone = get_zone(cls.apiclient, testClient.getZoneForTests()) + cls.hypervisor = testClient.getHypervisorInfo() + + cls.template = get_template( + cls.apiclient, + zone_id=cls.zone.id, + hypervisor=cls.hypervisor + ) + if cls.template == FAILED: + assert False, "get_template() failed to return template" + + cls._cleanup = [] + cls.account = Account.create( + cls.apiclient, + cls.services["account"], + domainid=cls.domain.id + ) + cls._cleanup.append(cls.account) + + cls.service_offering = ServiceOffering.create( + cls.apiclient, + cls.services["service_offerings"]["tiny"] + ) + cls._cleanup.append(cls.service_offering) + + @classmethod + def tearDownClass(cls): + super(TestInstanceBootGroup, cls).tearDownClass() + + def setUp(self): + self.apiclient = self.testClient.getApiClient() + self.cleanup = [] + + def tearDown(self): + super(TestInstanceBootGroup, self).tearDown() + + def deployTestVm(self, name_hint, group=None): + """Deploys a small VM under the shared test account, tracked for cleanup""" + vm = VirtualMachine.create( + self.apiclient, + self.services["virtual_machine"], + templateid=self.template.id, + zoneid=self.zone.id, + accountid=self.account.name, + domainid=self.account.domainid, + serviceofferingid=self.service_offering.id, + group=group + ) + self.cleanup.append(vm) + return vm + + def createTestBootGroup(self, name_hint): + boot_group = InstanceBootGroup.create( + self.apiclient, + name="-".join([name_hint, random_gen()]), + description="smoke test boot group", + account=self.account.name, + domainid=self.account.domainid + ) + self.cleanup.append(boot_group) + return boot_group + + def waitForMemberReadinessStatus(self, boot_group, expected_status, timeout=180, interval=5, ignoreinstancestate=None): + """Polls listInstanceBootGroupMembers (with readiness details requested) until every + member reaches expected_status, or timeout""" + members = [] + waited = 0 + while waited <= timeout: + kwargs = {"details": ["readiness"]} + if ignoreinstancestate is not None: + kwargs["ignoreinstancestate"] = ignoreinstancestate + members = InstanceBootGroup.listMembers(self.apiclient, bootgroupid=boot_group.id, **kwargs) + if members and all(m.readinessstatus == expected_status for m in members): + return members + time.sleep(interval) + waited += interval + return members + + @attr(tags=["advanced", "advancedns", "smoke", "basic"], required_hardware="false") + def test_01_boot_group_and_member_lifecycle(self): + """Create a boot group, add a VM member and an InstanceGroup member with + distinct boot order tiers, verify listing, update a member's order, remove a + member, then delete the boot group and confirm its members are cleaned up""" + + vm1 = self.deployTestVm("smoke-boot-db") + + group = InstanceGroup.create( + self.apiclient, + name="smoke-boot-web-grp", + account=self.account.name, + domainid=self.account.domainid + ) + self.cleanup.append(group) + vm2 = self.deployTestVm("smoke-boot-web", group=group.name) + + boot_group = self.createTestBootGroup("smoke-boot-group") + self.assertEqual(boot_group.name.startswith("smoke-boot-group"), True, "Check boot group name") + + member_vm = boot_group.addMember(self.apiclient, order=0, virtualmachineid=vm1.id) + self.assertEqual(member_vm.membertype, "VirtualMachine", "Check VM member type") + self.assertEqual(member_vm.memberid, vm1.id, "Check VM member id") + self.assertEqual(member_vm.order, 0, "Check VM member boot order") + + member_group = boot_group.addMember(self.apiclient, order=1, instancegroupid=group.id) + self.assertEqual(member_group.membertype, "InstanceGroup", "Check instance group member type") + self.assertEqual(member_group.memberid, group.id, "Check instance group member id") + self.assertEqual(member_group.order, 1, "Check instance group member boot order") + + members = InstanceBootGroup.listMembers(self.apiclient, bootgroupid=boot_group.id) + self.assertEqual(len(members), 2, "Check both members are listed") + + member_vm.update(self.apiclient, order=5) + members = InstanceBootGroup.listMembers(self.apiclient, bootgroupid=boot_group.id) + updated_member_vm = next(m for m in members if m.id == member_vm.id) + self.assertEqual(updated_member_vm.order, 5, "Check member boot order was updated") + + member_group.delete(self.apiclient) + members = InstanceBootGroup.listMembers(self.apiclient, bootgroupid=boot_group.id) + self.assertEqual(len(members), 1, "Check member was removed") + self.assertEqual(members[0].id, member_vm.id, "Check remaining member is the VM member") + + boot_group_id = boot_group.id + boot_group.delete(self.apiclient) + self.cleanup.remove(boot_group) + boot_groups = InstanceBootGroup.list(self.apiclient, id=boot_group_id) + self.assertTrue( + boot_groups is None or len(boot_groups) == 0, + "Check boot group no longer listed after delete" + ) + # The parent boot group is gone, so listing its members is itself an invalid request + # (bootgroupid must reference an existing group), not an empty-result query. + with self.assertRaises(CloudstackAPIException): + InstanceBootGroup.listMembers(self.apiclient, bootgroupid=boot_group_id) + + @attr(tags=["advanced", "advancedns", "smoke", "basic"], required_hardware="false") + def test_02_readiness_rule_lifecycle(self): + """Create, list, update and delete a readiness rule on a boot group member""" + + vm = self.deployTestVm("smoke-boot-rule-vm") + boot_group = self.createTestBootGroup("smoke-boot-rule-group") + boot_group.addMember(self.apiclient, order=0, virtualmachineid=vm.id) + + rule = InstanceBootGroupReadinessRule.create( + self.apiclient, + bootgroupid=boot_group.id, + ruletype="Ping", + virtualmachineid=vm.id + ) + self.assertEqual(rule.ruletype, "Ping", "Check readiness rule type") + self.assertEqual(rule.enabled, True, "Check readiness rule is enabled by default") + + rules = InstanceBootGroupReadinessRule.list(self.apiclient, bootgroupid=boot_group.id) + self.assertEqual(len(rules), 1, "Check readiness rule is listed") + + rule.update(self.apiclient, enabled=False) + rules = InstanceBootGroupReadinessRule.list(self.apiclient, bootgroupid=boot_group.id, id=rule.id) + self.assertEqual(rules[0].enabled, False, "Check readiness rule was disabled") + + rule.delete(self.apiclient) + rules = InstanceBootGroupReadinessRule.list(self.apiclient, bootgroupid=boot_group.id) + self.assertTrue( + rules is None or len(rules) == 0, + "Check readiness rule no longer listed after delete" + ) + + @attr(tags=["advanced", "advancedns", "smoke", "basic"], required_hardware="false") + def test_03_start_stop_reboot_orchestration(self): + """Deploy a two-tier boot group with no readiness rules attached (pure boot-order + semantics), then exercise startInstanceBootGroup / rebootInstanceBootGroup / + stopInstanceBootGroup and confirm all members end each phase in the right state""" + + vm1 = self.deployTestVm("smoke-boot-tier0") + vm2 = self.deployTestVm("smoke-boot-tier1") + boot_group = self.createTestBootGroup("smoke-boot-orch-group") + boot_group.addMember(self.apiclient, order=0, virtualmachineid=vm1.id) + boot_group.addMember(self.apiclient, order=1, virtualmachineid=vm2.id) + + vm1.stop(self.apiclient, forced=True) + vm2.stop(self.apiclient, forced=True) + + boot_group.start(self.apiclient) + vm1 = VirtualMachine.list(self.apiclient, id=vm1.id)[0] + vm2 = VirtualMachine.list(self.apiclient, id=vm2.id)[0] + self.assertEqual(vm1.state, 'Running', "Check tier0 VM is Running after boot group start") + self.assertEqual(vm2.state, 'Running', "Check tier1 VM is Running after boot group start") + + boot_group.reboot(self.apiclient, forced=True) + vm1 = VirtualMachine.list(self.apiclient, id=vm1.id)[0] + vm2 = VirtualMachine.list(self.apiclient, id=vm2.id)[0] + self.assertEqual(vm1.state, 'Running', "Check tier0 VM is Running after boot group reboot") + self.assertEqual(vm2.state, 'Running', "Check tier1 VM is Running after boot group reboot") + + boot_group.stop(self.apiclient, forced=True) + vm1 = VirtualMachine.list(self.apiclient, id=vm1.id)[0] + vm2 = VirtualMachine.list(self.apiclient, id=vm2.id)[0] + self.assertEqual(vm1.state, 'Stopped', "Check tier0 VM is Stopped after boot group stop") + self.assertEqual(vm2.state, 'Stopped', "Check tier1 VM is Stopped after boot group stop") + + @attr(tags=["advanced", "advancedns"], required_hardware="false") + def test_04_readiness_cache_invalidated_on_out_of_band_restart(self): + """Regression test for InstanceBootGroupVmStateListener: once a member is Ready, + restarting its VM directly (outside boot group orchestration) must invalidate the + cached rule result (internally Unknown, surfaced as a NotReady aggregate) rather + than leaving a stale Ready behind""" + + if self.zone.networktype.lower() != 'advanced': + self.skipTest("Ping readiness rule requires an advanced zone with a virtual router") + + vm = self.deployTestVm("smoke-boot-cache-vm") + boot_group = self.createTestBootGroup("smoke-boot-cache-group") + boot_group.addMember(self.apiclient, order=0, virtualmachineid=vm.id) + InstanceBootGroupReadinessRule.create( + self.apiclient, + bootgroupid=boot_group.id, + ruletype="Ping", + virtualmachineid=vm.id + ) + + vm.stop(self.apiclient, forced=True) + boot_group.start(self.apiclient) + + members = self.waitForMemberReadinessStatus(boot_group, "Ready") + self.assertTrue(len(members) > 0, "Check member list is not empty") + self.assertEqual(members[0].readinessstatus, "Ready", "Check member is Ready after boot group start") + + # Restart the VM directly, bypassing boot group orchestration entirely. + vm.reboot(self.apiclient, forced=True) + + members = InstanceBootGroup.listMembers( + self.apiclient, + bootgroupid=boot_group.id, + details=["readiness"], + ignoreinstancestate=True + ) + # The per-rule cache is invalidated to Unknown, but the member-level aggregate treats + # any non-Ready rule status (Unknown included) as NotReady - Unknown never surfaces as + # the aggregate readinessstatus itself. The listener's message is what distinguishes + # this from a genuine failed check, so assert on that instead. + self.assertEqual( + members[0].readinessstatus, + "NotReady", + "Cached readiness must be invalidated (no longer a stale Ready) after an out-of-band VM restart" + ) + self.assertTrue( + "not yet re-verified" in members[0].readinessmessage, + "Check the NotReady status is specifically due to cache invalidation on restart, not some other failure" + ) + + @attr(tags=["advanced", "advancedns", "smoke", "basic"], required_hardware="false") + def test_05_membership_guard_rejects_duplicate_membership(self): + """A VM already a member of one boot group cannot be added as a member of another""" + + vm = self.deployTestVm("smoke-boot-guard-vm") + boot_group1 = self.createTestBootGroup("smoke-boot-guard-group1") + boot_group2 = self.createTestBootGroup("smoke-boot-guard-group2") + + boot_group1.addMember(self.apiclient, order=0, virtualmachineid=vm.id) + + with self.assertRaises(CloudstackAPIException): + boot_group2.addMember(self.apiclient, order=0, virtualmachineid=vm.id) diff --git a/tools/apidoc/gen_toc.py b/tools/apidoc/gen_toc.py index c99328fff9ff..30b97bedabd1 100644 --- a/tools/apidoc/gen_toc.py +++ b/tools/apidoc/gen_toc.py @@ -283,7 +283,8 @@ 'CustomActions' : 'Extension', 'ImportVmTask': 'Import VM Task', 'Dns': 'DNS', - 'downloadValidationScreenshot': 'Backup and Recovery' + 'downloadValidationScreenshot': 'Backup and Recovery', + 'InstanceBootGroup': 'Instance Boot Group' } diff --git a/tools/marvin/marvin/lib/base.py b/tools/marvin/marvin/lib/base.py index e7fa2f763db5..b95a710db989 100755 --- a/tools/marvin/marvin/lib/base.py +++ b/tools/marvin/marvin/lib/base.py @@ -5701,6 +5701,170 @@ def recoverInstances(self, apiclient): apiclient.recoverVirtualMachine(cmd) +class InstanceBootGroup: + """Manage Instance Boot Group lifecycle""" + + def __init__(self, items): + self.__dict__.update(items) + + @classmethod + def create(cls, apiclient, name, description=None, account=None, + domainid=None, projectid=None, readinessattempttimeoutseconds=None, + readinessmaxretryattempts=None, readinessrebootonretry=None, + readinessinitialdelayseconds=None): + """Creates an instance boot group""" + + cmd = createInstanceBootGroup.createInstanceBootGroupCmd() + cmd.name = name + if description is not None: + cmd.description = description + if account is not None: + cmd.account = account + if domainid is not None: + cmd.domainid = domainid + if projectid is not None: + cmd.projectid = projectid + if readinessattempttimeoutseconds is not None: + cmd.readinessattempttimeoutseconds = readinessattempttimeoutseconds + if readinessmaxretryattempts is not None: + cmd.readinessmaxretryattempts = readinessmaxretryattempts + if readinessrebootonretry is not None: + cmd.readinessrebootonretry = readinessrebootonretry + if readinessinitialdelayseconds is not None: + cmd.readinessinitialdelayseconds = readinessinitialdelayseconds + return InstanceBootGroup(apiclient.createInstanceBootGroup(cmd).__dict__) + + def update(self, apiclient, **kwargs): + """Updates the instance boot group""" + cmd = updateInstanceBootGroup.updateInstanceBootGroupCmd() + cmd.id = self.id + [setattr(cmd, k, v) for k, v in list(kwargs.items())] + return apiclient.updateInstanceBootGroup(cmd) + + def delete(self, apiclient): + """Delete the instance boot group""" + cmd = deleteInstanceBootGroup.deleteInstanceBootGroupCmd() + cmd.id = self.id + return apiclient.deleteInstanceBootGroup(cmd) + + @classmethod + def list(cls, apiclient, **kwargs): + """List all instance boot groups""" + cmd = listInstanceBootGroups.listInstanceBootGroupsCmd() + [setattr(cmd, k, v) for k, v in list(kwargs.items())] + if 'account' in list(kwargs.keys()) and 'domainid' in list(kwargs.keys()): + cmd.listall = True + return apiclient.listInstanceBootGroups(cmd) + + def start(self, apiclient): + """Starts all members of the instance boot group in boot order""" + cmd = startInstanceBootGroup.startInstanceBootGroupCmd() + cmd.id = self.id + return apiclient.startInstanceBootGroup(cmd) + + def stop(self, apiclient, forced=None): + """Stops all members of the instance boot group in reverse boot order""" + cmd = stopInstanceBootGroup.stopInstanceBootGroupCmd() + cmd.id = self.id + if forced is not None: + cmd.forced = forced + return apiclient.stopInstanceBootGroup(cmd) + + def reboot(self, apiclient, forced=None): + """Reboots the instance boot group (stop then start, with readiness gating)""" + cmd = rebootInstanceBootGroup.rebootInstanceBootGroupCmd() + cmd.id = self.id + if forced is not None: + cmd.forced = forced + return apiclient.rebootInstanceBootGroup(cmd) + + def addMember(self, apiclient, order, virtualmachineid=None, instancegroupid=None): + """Adds a VM or instance group member to the instance boot group""" + cmd = addMemberToInstanceBootGroup.addMemberToInstanceBootGroupCmd() + cmd.id = self.id + cmd.order = order + if virtualmachineid is not None: + cmd.virtualmachineid = virtualmachineid + if instancegroupid is not None: + cmd.instancegroupid = instancegroupid + return InstanceBootGroupMember(apiclient.addMemberToInstanceBootGroup(cmd).__dict__) + + @classmethod + def listMembers(cls, apiclient, bootgroupid, **kwargs): + """List members of an instance boot group""" + cmd = listInstanceBootGroupMembers.listInstanceBootGroupMembersCmd() + cmd.bootgroupid = bootgroupid + [setattr(cmd, k, v) for k, v in list(kwargs.items())] + return apiclient.listInstanceBootGroupMembers(cmd) + + +class InstanceBootGroupMember: + """Manage an individual instance boot group member entry""" + + def __init__(self, items): + self.__dict__.update(items) + + def update(self, apiclient, order): + """Updates the boot order of this member""" + cmd = updateInstanceBootGroupMember.updateInstanceBootGroupMemberCmd() + cmd.id = self.id + cmd.order = order + return apiclient.updateInstanceBootGroupMember(cmd) + + def delete(self, apiclient): + """Removes this member from its instance boot group""" + cmd = removeInstanceBootGroupMember.removeInstanceBootGroupMemberCmd() + cmd.id = self.id + return apiclient.removeInstanceBootGroupMember(cmd) + + +class InstanceBootGroupReadinessRule: + """Manage Instance Boot Group readiness rules""" + + def __init__(self, items): + self.__dict__.update(items) + + @classmethod + def create(cls, apiclient, bootgroupid, ruletype, virtualmachineid=None, + instancegroupid=None, name=None, enabled=None, details=None): + """Creates a readiness rule for a boot group member""" + cmd = createInstanceBootGroupReadinessRule.createInstanceBootGroupReadinessRuleCmd() + cmd.bootgroupid = bootgroupid + cmd.ruletype = ruletype + if virtualmachineid is not None: + cmd.virtualmachineid = virtualmachineid + if instancegroupid is not None: + cmd.instancegroupid = instancegroupid + if name is not None: + cmd.name = name + if enabled is not None: + cmd.enabled = enabled + if details is not None: + cmd.details = details + return InstanceBootGroupReadinessRule(apiclient.createInstanceBootGroupReadinessRule(cmd).__dict__) + + def update(self, apiclient, **kwargs): + """Updates the readiness rule""" + cmd = updateInstanceBootGroupReadinessRule.updateInstanceBootGroupReadinessRuleCmd() + cmd.id = self.id + [setattr(cmd, k, v) for k, v in list(kwargs.items())] + return apiclient.updateInstanceBootGroupReadinessRule(cmd) + + def delete(self, apiclient): + """Deletes the readiness rule""" + cmd = deleteInstanceBootGroupReadinessRule.deleteInstanceBootGroupReadinessRuleCmd() + cmd.id = self.id + return apiclient.deleteInstanceBootGroupReadinessRule(cmd) + + @classmethod + def list(cls, apiclient, bootgroupid, **kwargs): + """List readiness rules for an instance boot group""" + cmd = listInstanceBootGroupReadinessRules.listInstanceBootGroupReadinessRulesCmd() + cmd.bootgroupid = bootgroupid + [setattr(cmd, k, v) for k, v in list(kwargs.items())] + return apiclient.listInstanceBootGroupReadinessRules(cmd) + + class ASA1000V: """Manage ASA 1000v lifecycle""" diff --git a/ui/public/config.json b/ui/public/config.json index 81d3938a5dee..1ca42f04b67c 100644 --- a/ui/public/config.json +++ b/ui/public/config.json @@ -105,7 +105,155 @@ "imageSelectionInterface": "modern", "showUserCategoryForModernImageSelection": true, "showAllCategoryForModernImageSelection": false, - "docHelpMappings": {}, + "docHelpMappings": { + "adminguide/accounts.html": "adminguide/accounts.html", + "adminguide/accounts.html#domains": "adminguide/accounts.html#domains", + "adminguide/accounts.html#keypairs": "adminguide/accounts.html#keypairs", + "adminguide/accounts.html#roles": "adminguide/accounts.html#roles", + "adminguide/accounts.html#users": "adminguide/accounts.html#users", + "adminguide/accounts.html#using-an-ldap-server-for-user-authentication": "adminguide/accounts.html#using-an-ldap-server-for-user-authentication", + "adminguide/autoscale_with_virtual_router.html": "adminguide/autoscale_with_virtual_router.html", + "adminguide/events.html": "adminguide/events.html", + "adminguide/events.html#creating-webhooks": "adminguide/events.html#creating-webhooks", + "adminguide/events.html#deleting-and-archiving-events-and-alerts": "adminguide/events.html#deleting-and-archiving-events-and-alerts", + "adminguide/extensions.html": "adminguide/extensions.html", + "adminguide/extensions.html#custom-actions": "adminguide/extensions.html#custom-actions", + "adminguide/guest_os.html#guest-os": "adminguide/guest_os.html#guest-os", + "adminguide/guest_os.html#guest-os-categories": "adminguide/guest_os.html#guest-os-categories", + "adminguide/guest_os.html#guest-os-hypervisor-mapping": "adminguide/guest_os.html#guest-os-hypervisor-mapping", + "adminguide/hosts.html#disabling-and-enabling-zones-pods-and-clusters": "adminguide/hosts.html#disabling-and-enabling-zones-pods-and-clusters", + "adminguide/hosts.html?highlight=Hypervisor%20capabilities#hypervisor-capabilities": "adminguide/hosts.html?highlight=Hypervisor%20capabilities#hypervisor-capabilities", + "adminguide/hosts.html#kvm-rolling-maintenance": "adminguide/hosts.html#kvm-rolling-maintenance", + "adminguide/hosts.html#maintaining-hypervisors-on-hosts": "adminguide/hosts.html#maintaining-hypervisors-on-hosts", + "adminguide/hosts.html#out-of-band-management": "adminguide/hosts.html#out-of-band-management", + "adminguide/hosts.html#removing-hosts": "adminguide/hosts.html#removing-hosts", + "adminguide/index.html#tuning": "adminguide/index.html#tuning", + "adminguide/kms.html#adding-an-hsm-profile": "adminguide/kms.html#adding-an-hsm-profile", + "adminguide/kms.html#creating-a-kms-key": "adminguide/kms.html#creating-a-kms-key", + "adminguide/kms.html#migrating-existing-volumes-to-kms": "adminguide/kms.html#migrating-existing-volumes-to-kms", + "adminguide/kms.html#rotating-a-kms-key": "adminguide/kms.html#rotating-a-kms-key", + "adminguide/management.html#administrator-alerts": "adminguide/management.html#administrator-alerts", + "adminguide/management.html#metrics": "adminguide/management.html#metrics", + "adminguide/management.html#reporting-cpu-sockets": "adminguide/management.html#reporting-cpu-sockets", + "adminguide/nas_plugin.html": "adminguide/nas_plugin.html", + "adminguide/networking_and_traffic.html#acl-on-private-gateway": "adminguide/networking_and_traffic.html#acl-on-private-gateway", + "adminguide/networking_and_traffic.html#adding-an-additional-guest-network": "adminguide/networking_and_traffic.html#adding-an-additional-guest-network", + "adminguide/networking_and_traffic.html#adding-a-private-gateway-to-a-vpc": "adminguide/networking_and_traffic.html#adding-a-private-gateway-to-a-vpc", + "adminguide/networking_and_traffic.html#adding-a-security-group": "adminguide/networking_and_traffic.html#adding-a-security-group", + "adminguide/networking_and_traffic.html#adding-a-virtual-private-cloud": "adminguide/networking_and_traffic.html#adding-a-virtual-private-cloud", + "adminguide/networking_and_traffic.html#advanced-zone-physical-network-configuration": "adminguide/networking_and_traffic.html#advanced-zone-physical-network-configuration", + "adminguide/networking_and_traffic.html#basic-zone-physical-network-configuration": "adminguide/networking_and_traffic.html#basic-zone-physical-network-configuration", + "adminguide/networking_and_traffic.html#configure-guest-traffic-in-an-advanced-zone": "adminguide/networking_and_traffic.html#configure-guest-traffic-in-an-advanced-zone", + "adminguide/networking_and_traffic.html#configuring-a-virtual-private-cloud": "adminguide/networking_and_traffic.html#configuring-a-virtual-private-cloud", + "adminguide/networking_and_traffic.html#configuring-network-access-control-list": "adminguide/networking_and_traffic.html#configuring-network-access-control-list", + "adminguide/networking_and_traffic.html#creating-acl-lists": "adminguide/networking_and_traffic.html#creating-acl-lists", + "adminguide/networking_and_traffic.html#creating-and-updating-a-vpn-customer-gateway": "adminguide/networking_and_traffic.html#creating-and-updating-a-vpn-customer-gateway", + "adminguide/networking_and_traffic.html#creating-an-internal-lb-rule": "adminguide/networking_and_traffic.html#creating-an-internal-lb-rule", + "adminguide/networking_and_traffic.html#creating-a-vpn-connection": "adminguide/networking_and_traffic.html#creating-a-vpn-connection", + "adminguide/networking_and_traffic.html#creating-a-vpn-gateway-for-the-vpc": "adminguide/networking_and_traffic.html#creating-a-vpn-gateway-for-the-vpc", + "adminguide/networking_and_traffic.html#enabling-or-disabling-static-nat": "adminguide/networking_and_traffic.html#enabling-or-disabling-static-nat", + "adminguide/networking_and_traffic.html#load-balancing-across-tiers": "adminguide/networking_and_traffic.html#load-balancing-across-tiers", + "adminguide/networking_and_traffic.html#releasing-an-ip-address-alloted-to-a-vpc": "adminguide/networking_and_traffic.html#releasing-an-ip-address-alloted-to-a-vpc", + "adminguide/networking_and_traffic.html#reserving-public-ip-addresses-and-vlans-for-accounts": "adminguide/networking_and_traffic.html#reserving-public-ip-addresses-and-vlans-for-accounts", + "adminguide/networking_and_traffic.html#restarting-and-removing-a-vpn-connection": "adminguide/networking_and_traffic.html#restarting-and-removing-a-vpn-connection", + "adminguide/networking_and_traffic.html#security-groups": "adminguide/networking_and_traffic.html#security-groups", + "adminguide/networking_and_traffic.html#setting-up-a-site-to-site-vpn-connection": "adminguide/networking_and_traffic.html#setting-up-a-site-to-site-vpn-connection", + "adminguide/networking_and_traffic.html#updating-and-removing-a-vpn-customer-gateway": "adminguide/networking_and_traffic.html#updating-and-removing-a-vpn-customer-gateway", + "adminguide/networking.html#creating-a-new-network-offering": "adminguide/networking.html#creating-a-new-network-offering", + "adminguide/networking.html#network-offerings": "adminguide/networking.html#network-offerings", + "adminguide/networking.html#network-service-providers": "adminguide/networking.html#network-service-providers", + "adminguide/networking/vnf_templates_appliances.html#deploying-vnf-appliances": "adminguide/networking/vnf_templates_appliances.html#deploying-vnf-appliances", + "adminguide/object_storage.html#update-bucket": "adminguide/object_storage.html#update-bucket", + "adminguide/projects.html": "adminguide/projects.html", + "adminguide/projects.html#accepting-a-membership-invitation": "adminguide/projects.html#accepting-a-membership-invitation", + "adminguide/projects.html#adding-project-members-from-the-ui": "adminguide/projects.html#adding-project-members-from-the-ui", + "adminguide/projects.html#creating-a-new-project": "adminguide/projects.html#creating-a-new-project", + "adminguide/projects.html#sending-project-membership-invitations": "adminguide/projects.html#sending-project-membership-invitations", + "adminguide/projects.html#suspending-or-deleting-a-project": "adminguide/projects.html#suspending-or-deleting-a-project", + "adminguide/reliability.html#ha-for-hosts": "adminguide/reliability.html#ha-for-hosts", + "adminguide/service_offerings.html#compute-and-disk-service-offerings": "adminguide/service_offerings.html#compute-and-disk-service-offerings", + "adminguide/service_offerings.html#creating-a-new-compute-offering": "adminguide/service_offerings.html#creating-a-new-compute-offering", + "adminguide/service_offerings.html#creating-a-new-disk-offering": "adminguide/service_offerings.html#creating-a-new-disk-offering", + "adminguide/service_offerings.html#creating-a-new-system-service-offering": "adminguide/service_offerings.html#creating-a-new-system-service-offering", + "adminguide/service_offerings.html#modifying-or-deleting-a-service-offering": "adminguide/service_offerings.html#modifying-or-deleting-a-service-offering", + "adminguide/service_offerings.html#system-service-offerings": "adminguide/service_offerings.html#system-service-offerings", + "adminguide/storage.html#creating-a-new-file-share": "adminguide/storage.html#creating-a-new-file-share", + "adminguide/storage.html#creating-a-new-volume": "adminguide/storage.html#creating-a-new-volume", + "adminguide/storage.html#id2": "adminguide/storage.html#id2", + "adminguide/storage.html#lifecycle-operations": "adminguide/storage.html#lifecycle-operations", + "adminguide/storage.html#object-storage": "adminguide/storage.html#object-storage", + "adminguide/storage.html#primary-storage": "adminguide/storage.html#primary-storage", + "adminguide/storage.html#resizing-volumes": "adminguide/storage.html#resizing-volumes", + "adminguide/storage.html#secondary-storage": "adminguide/storage.html#secondary-storage", + "adminguide/storage.html#uploading-an-existing-volume-to-a-virtual-machine": "adminguide/storage.html#uploading-an-existing-volume-to-a-virtual-machine", + "adminguide/storage.html#working-with-volumes": "adminguide/storage.html#working-with-volumes", + "adminguide/storage.html#working-with-volume-snapshots": "adminguide/storage.html#working-with-volume-snapshots", + "adminguide/systemvm.html": "adminguide/systemvm.html", + "adminguide/systemvm.html#upgrading-virtual-routers": "adminguide/systemvm.html#upgrading-virtual-routers", + "adminguide/systemvm.html#virtual-router": "adminguide/systemvm.html#virtual-router", + "adminguide/templates.html": "adminguide/templates.html", + "adminguide/templates.html#attaching-an-iso-to-a-vm": "adminguide/templates.html#attaching-an-iso-to-a-vm", + "adminguide/templates.html#exporting-templates": "adminguide/templates.html#exporting-templates", + "adminguide/templates.html#id10": "adminguide/templates.html#id10", + "adminguide/templates.html#sharing-templates-with-other-accounts-projects": "adminguide/templates.html#sharing-templates-with-other-accounts-projects", + "adminguide/templates.html#uploading-templates-and-isos-from-a-local-computer": "adminguide/templates.html#uploading-templates-and-isos-from-a-local-computer", + "adminguide/templates.html#uploading-templates-from-a-remote-http-server": "adminguide/templates.html#uploading-templates-from-a-remote-http-server", + "adminguide/templates.html#working-with-isos": "adminguide/templates.html#working-with-isos", + "adminguide/virtual_machines.html": "adminguide/virtual_machines.html", + "adminguide/virtual_machines.html#affinity-groups": "adminguide/virtual_machines.html#affinity-groups", + "adminguide/virtual_machines.html#backup-offerings": "adminguide/virtual_machines.html#backup-offerings", + "adminguide/virtual_machines.html#change-affinity-group-for-an-existing-vm": "adminguide/virtual_machines.html#change-affinity-group-for-an-existing-vm", + "adminguide/virtual_machines.html#changing-the-vm-name-os-or-group": "adminguide/virtual_machines.html#changing-the-vm-name-os-or-group", + "adminguide/virtual_machines.html#creating-a-new-affinity-group": "adminguide/virtual_machines.html#creating-a-new-affinity-group", + "adminguide/virtual_machines.html#creating-a-new-instance-from-backup": "adminguide/virtual_machines.html#creating-a-new-instance-from-backup", + "adminguide/virtual_machines.html#creating-the-ssh-keypair": "adminguide/virtual_machines.html#creating-the-ssh-keypair", + "adminguide/virtual_machines.html#creating-vm-backups": "adminguide/virtual_machines.html#creating-vm-backups", + "adminguide/virtual_machines.html#creating-vms": "adminguide/virtual_machines.html#creating-vms", + "adminguide/virtual_machines.html#delete-an-affinity-group": "adminguide/virtual_machines.html#delete-an-affinity-group", + "adminguide/virtual_machines.html#deleting-vms": "adminguide/virtual_machines.html#deleting-vms", + "adminguide/virtual_machines.html#how-to-dynamically-scale-cpu-and-ram": "adminguide/virtual_machines.html#how-to-dynamically-scale-cpu-and-ram", + "adminguide/virtual_machines.html#importing-and-unmanaging-virtual-machine": "adminguide/virtual_machines.html#importing-and-unmanaging-virtual-machine", + "adminguide/virtual_machines.html#importing-and-unmanaging-volume": "adminguide/virtual_machines.html#importing-and-unmanaging-volume", + "adminguide/virtual_machines.html#importing-backup-offerings": "adminguide/virtual_machines.html#importing-backup-offerings", + "adminguide/virtual_machines.html#moving-vms-between-hosts-manual-live-migration": "adminguide/virtual_machines.html#moving-vms-between-hosts-manual-live-migration", + "adminguide/virtual_machines.html#resetting-ssh-keys": "adminguide/virtual_machines.html#resetting-ssh-keys", + "adminguide/virtual_machines.html#resetting-userdata": "adminguide/virtual_machines.html#resetting-userdata", + "adminguide/virtual_machines.html#restoring-instance-backups": "adminguide/virtual_machines.html#restoring-instance-backups", + "adminguide/virtual_machines.html#restoring-vm-backups": "adminguide/virtual_machines.html#restoring-vm-backups", + "adminguide/virtual_machines.html#stopping-and-starting-vms": "adminguide/virtual_machines.html#stopping-and-starting-vms", + "adminguide/virtual_machines.html#user-data-and-meta-data": "adminguide/virtual_machines.html#user-data-and-meta-data", + "adminguide/virtual_machines.html#using-ssh-keys-for-authentication": "adminguide/virtual_machines.html#using-ssh-keys-for-authentication", + "adminguide/virtual_machines.html#virtual-machine-snapshots": "adminguide/virtual_machines.html#virtual-machine-snapshots", + "adminguide/webhooks.html": "adminguide/webhooks.html", + "conceptsandterminology/concepts.html#about-clusters": "conceptsandterminology/concepts.html#about-clusters", + "conceptsandterminology/concepts.html#about-hosts": "conceptsandterminology/concepts.html#about-hosts", + "conceptsandterminology/concepts.html#about-pods": "conceptsandterminology/concepts.html#about-pods", + "conceptsandterminology/concepts.html#about-zones": "conceptsandterminology/concepts.html#about-zones", + "conceptsandterminology/concepts.html#management-server-overview": "conceptsandterminology/concepts.html#management-server-overview", + "conceptsandterminology/network_setup.html#vlan-allocation-example": "conceptsandterminology/network_setup.html#vlan-allocation-example", + "installguide/configuration.html#adding-a-cluster": "installguide/configuration.html#adding-a-cluster", + "installguide/configuration.html#adding-a-host": "installguide/configuration.html#adding-a-host", + "installguide/configuration.html#adding-a-pod": "installguide/configuration.html#adding-a-pod", + "installguide/configuration.html#adding-a-zone": "installguide/configuration.html#adding-a-zone", + "installguide/configuration.html#add-object-storage": "installguide/configuration.html#add-object-storage", + "installguide/configuration.html#add-primary-storage": "installguide/configuration.html#add-primary-storage", + "installguide/configuration.html#add-secondary-storage": "installguide/configuration.html#add-secondary-storage", + "installguide/configuration.html#create-bucket": "installguide/configuration.html#create-bucket", + "plugins/cloudian-connector.html": "plugins/cloudian-connector.html", + "plugins/cloudstack-kubernetes-service.html": "plugins/cloudstack-kubernetes-service.html", + "plugins/cloudstack-kubernetes-service.html#creating-a-new-kubernetes-cluster": "plugins/cloudstack-kubernetes-service.html#creating-a-new-kubernetes-cluster", + "plugins/cloudstack-kubernetes-service.html#deleting-kubernetes-cluster": "plugins/cloudstack-kubernetes-service.html#deleting-kubernetes-cluster", + "plugins/cloudstack-kubernetes-service.html#kubernetes-supported-versions": "plugins/cloudstack-kubernetes-service.html#kubernetes-supported-versions", + "plugins/cloudstack-kubernetes-service.html#scaling-kubernetes-cluster": "plugins/cloudstack-kubernetes-service.html#scaling-kubernetes-cluster", + "plugins/cloudstack-kubernetes-service.html#starting-a-stopped-kubernetes-cluster": "plugins/cloudstack-kubernetes-service.html#starting-a-stopped-kubernetes-cluster", + "plugins/cloudstack-kubernetes-service.html#stopping-kubernetes-cluster": "plugins/cloudstack-kubernetes-service.html#stopping-kubernetes-cluster", + "plugins/cloudstack-kubernetes-service.html#upgrading-kubernetes-cluster": "plugins/cloudstack-kubernetes-service.html#upgrading-kubernetes-cluster", + "plugins/nuage-plugin.html?#optional-create-and-enable-vpc-offering": "plugins/nuage-plugin.html?#optional-create-and-enable-vpc-offering", + "plugins/nuage-plugin.html?#vpc-offerings": "plugins/nuage-plugin.html?#vpc-offerings", + "plugins/quota.html": "plugins/quota.html", + "plugins/quota.html#quota-credits": "plugins/quota.html#quota-credits", + "plugins/quota.html#quota-tariff": "plugins/quota.html#quota-tariff" + }, "notifyLatestCSVersion": true, "showSearchFilters": true, "announcementBanner": { diff --git a/ui/public/locales/en.json b/ui/public/locales/en.json index f57460efa482..8c61136f5335 100644 --- a/ui/public/locales/en.json +++ b/ui/public/locales/en.json @@ -195,6 +195,7 @@ "label.action.quota.tariff.edit": "Edit Quota Tariff", "label.action.quota.tariff.remove": "Remove Quota Tariff", "label.action.reboot.instance": "Reboot Instance", +"label.action.reboot.instance.boot.group": "Reboot Instance Boot Group", "label.action.reboot.router": "Reboot Router", "label.action.reboot.systemvm": "Reboot System VM", "label.action.recover.volume": "Recover Volume", @@ -225,10 +226,12 @@ "label.action.setup.2FA.user.auth": "Setup User Two Factor Authentication", "label.action.start.sharedfs": "Start Shared FileSystem", "label.action.start.instance": "Start Instance", +"label.action.start.instance.boot.group": "Start Instance Boot Group", "label.action.start.router": "Start Router", "label.action.start.systemvm": "Start System VM", "label.action.stop.sharedfs": "Stop Shared FileSystem", "label.action.stop.instance": "Stop Instance", +"label.action.stop.instance.boot.group": "Stop Instance Boot Group", "label.action.stop.router": "Stop Router", "label.action.stop.systemvm": "Stop System VM", "label.action.take.snapshot": "Take Snapshot", @@ -294,6 +297,7 @@ "label.add.guest.os.hypervisor.mapping": "Add guest os hypervisor mapping", "label.add.host": "Add Host", "label.add.ingress.rule": "Add Ingress Rule", +"label.add.instance.boot.group": "Add Instance Boot Group", "label.add.intermediate.certificate": "Add intermediate certificate", "label.add.internal.lb": "Add internal LB", "label.add.ip.range": "Add IP Range", @@ -305,6 +309,7 @@ "label.add.latest.kubernetes.iso": "Add latest Kubernetes ISO", "label.add.ldap.account": "Add LDAP Account", "label.add.logical.router": "Add Logical Router to this Network", +"label.add.member": "Add Member", "label.add.minimum.required.compute.offering": "Add minimum required Compute Offering", "label.add.more": "Add more", "label.add.nodes": "Add Nodes to Kubernetes Cluster", @@ -326,6 +331,7 @@ "label.add.policy": "Add policy", "label.add.primary.storage": "Add Primary Storage", "label.add.private.gateway": "Add Private Gateway", +"label.add.readiness.rule": "Add Readiness Rule", "label.add.resources": "Add Resources", "label.add.role": "Add Role", "label.add.route": "Add Route", @@ -515,6 +521,7 @@ "label.bigswitch.controller.address": "BigSwitch BCF controller address", "label.bladeid": "Blade ID", "label.blades": "Blades", +"label.boot.order": "Boot Order", "label.bootable": "Bootable", "label.bootintosetup": "Boot into hardware setup", "label.bootmode": "Boot mode", @@ -836,6 +843,7 @@ "label.delete.f5": "Delete F5", "label.delete.gateway": "Delete gateway", "label.delete.icon": "Delete icon", +"label.delete.instance.boot.group": "Delete Instance Boot Group", "label.delete.instance.group": "Delete Instance group", "label.delete.internal.lb": "Delete internal LB", "label.delete.ipv4.subnet": "Delete IPv4 subnet", @@ -1060,6 +1068,7 @@ "label.edit.nic": "Edit NIC", "label.edit.project.details": "Edit project details", "label.edit.project.role": "Edit project role", +"label.edit.readiness.rule": "Edit Readiness Rule", "label.edit.role": "Edit Role", "label.edit.rule": "Edit Rule", "label.edit.secondary.ips": "Edit secondary IPs", @@ -1263,6 +1272,7 @@ "label.gslbproviderprivateip": "GSLB service private IP", "label.gslbproviderpublicip": "GSLB service public IP", "label.guest": "Guest", +"label.guestagentliveness": "Guest Agent Liveness", "label.guest.cidr": "Guest CIDR", "label.guest.end.ip": "Guest end IP", "label.guest.gateway": "Guest gateway", @@ -1376,13 +1386,17 @@ "label.ingress": "Ingress", "label.ingress.rule": "Ingress Rule", "label.initial": "Initial", +"label.inherited": "Inherited", "label.initialized": "Initialized", "label.insideportprofile": "Inside port profile", "label.installwizard.addzoneintro.title": "Let's add a Zone", "label.installwizard.subtitle": "This guide will aid you in setting up your CloudStack™ installation", "label.installwizard.title": "Hello and welcome to CloudStack™", "label.instance": "Instance", +"label.instance.boot.group": "Instance Boot Group", +"label.instance.boot.groups": "Instance Boot Groups", "label.instance.conversion.support": "Instance Conversion Supported", +"label.instance.group": "Instance Group", "label.instance.groups": "Instance Groups", "label.instance.metadata": "Instance metadata", "label.instance.name": "Instance name", @@ -1557,6 +1571,7 @@ "label.l2gatewayserviceuuid": "L2 Gateway Service UUID", "label.l3gatewayserviceuuid": "L3 Gateway Service UUID", "label.label": "Label", +"label.last.checked": "Last checked", "label.last.updated": "Last update", "label.lastupdated": "Last update", "label.lastannotated": "Last annotation date", @@ -1642,6 +1657,7 @@ "label.make.user.project.owner": "Make User project owner", "label.makeredundant": "Make redundant", "label.manage": "Manage", +"label.manage.readiness.rules": "Manage Readiness Rules", "label.manage.ssl.cert": "Manage SSL certificate", "label.manage.vpn.user": "Manage VPN Users", "label.managed.instances": "Managed Instances", @@ -1696,6 +1712,10 @@ "label.maxvpc": "Max. VPCs", "label.may.continue": "You may now continue.", "label.mb.memory": "MB memory", +"label.memberquorum": "Member Quorum", +"label.member.type": "Member Type", +"label.message": "Message", +"label.members": "Members", "label.memory": "Memory", "label.memory.free": "Memory free", "label.memory.maximum.mb": "Max memory (in MB)", @@ -2010,6 +2030,7 @@ "label.physicalnetworkname": "Physical Network name", "label.physicalsize": "Physical size", "label.pin": "PIN", +"label.ping": "Ping", "label.ping.path": "Ping path", "label.pkcs.private.certificate": "PKCS#8 private certificate", "label.plannermode": "Planner mode", @@ -2025,6 +2046,7 @@ "label.policy": "Policy", "label.policyuuid": "Network Policy", "label.port": "Port", +"label.portcheck": "Port Check", "label.port.range": "Port range", "label.portforwarding": "Port forwarding", "label.portforwarding.rule": "Port forwarding rule", @@ -2178,6 +2200,15 @@ "label.readonly": "Read-Only", "label.reason": "Reason", "label.rebalance": "Rebalance", +"label.readiness": "Readiness", +"label.readiness.mode.child.dependent": "Depends on children", +"label.readiness.mode.no.readiness": "No Readiness", +"label.readiness.mode.rule.based": "Rule-based", +"label.readiness.rules": "Readiness Rules", +"label.readinessattempttimeoutseconds": "Readiness retry timeout (seconds)", +"label.readinessinitialdelayseconds": "Readiness initial delay (seconds)", +"label.readinessmaxretryattempts": "Max readiness retry attempts", +"label.readinessrebootonretry": "Reboot instance on readiness retry", "label.reboot": "Reboot", "label.recent.deliveries": "Recent deliveries", "label.receivedbytes": "Bytes received", @@ -2319,6 +2350,7 @@ "label.routing.policy.terms": "Routing policy terms", "label.routing.policy.terms.then": "Routing policy terms then", "label.rule": "Rule", +"label.rule.type": "Rule Type", "label.rule.number": "Rule number", "label.rules": "Rules", "label.rules.file": "Rules file", @@ -2694,6 +2726,8 @@ "label.threadstotalcount": "Total Thread count", "label.threadswaitingcount": "Waiting Threads", "label.threshold": "Threshold", +"label.threshold.type": "Threshold Type", +"label.threshold.value": "Threshold Value", "label.threshold.description": "Value for which the Counter will be evaluated with the Operator selected", "label.thursday": "Thursday", "label.tier0gateway": "Tier-0 Gateway", @@ -2784,6 +2818,7 @@ "label.update.custom.action": "Update Custom Action", "label.update.extension": "Update Extension", "label.update.sharedfs": "Update Shared FileSystem", +"label.update.instance.boot.group": "Update Instance Boot Group", "label.update.instance.group": "Update Instance group", "label.update.ip.range": "Update IP range", "label.update.ipv4.subnet": "Update IPv4 subnet", @@ -2878,6 +2913,7 @@ "label.view": "View", "label.view.all": "View all", "label.view.console": "View console", +"label.view.failing.readiness.rules": "View failing readiness rules", "label.viewing": "Viewing", "label.virtualmachine": "Instance", "label.virtualmachinecount": "Instances Count", @@ -3070,6 +3106,8 @@ "label.lease.enable.tooltip": "The Instance Lease feature allows to set a lease duration (in days) for instances, after which they automatically expire. Upon expiry, the instance can either be stopped (powered off) or destroyed, based on the configured policy", "label.instance.lease": "Instance lease", "label.instance.lease.placeholder": "Lease duration in days ( > 0)", +"label.instance.not.running.view.last.readiness.results": "Instance is not in Running state. View last readiness results", +"label.instancegroup.instances.not.running.view.last.readiness.results": "Instance(s) of the group are not in Running state. View last readiness results", "label.leaseduration": "Lease duration (in days)", "label.leaseexpiry.date.and.time": "Lease expiry date", "label.leaseexpiryaction": "Lease expiry action", @@ -3118,6 +3156,7 @@ "message.action.delete.guest.os.category": "Please confirm that you want to delete this guest os category.", "message.action.delete.guest.os.hypervisor.mapping": "Please confirm that you want to delete this guest os hypervisor mapping. System defined entry cannot be deleted.", "message.action.delete.hsm.profile": "Please confirm that you want to delete this HSM profile.", +"message.action.delete.instance.boot.group": "Please confirm that you want to delete this Instance Boot Group.", "message.action.delete.instance.group": "Please confirm that you want to delete the Instance group.", "message.action.delete.interface.static.route": "Please confirm that you want to remove this interface Static Route?", "message.action.delete.iso": "Please confirm that you want to delete this ISO.", @@ -3187,6 +3226,7 @@ "message.action.quota.tariff.create.error.valuerequired": "Please, inform a value for the quota tariff.", "message.action.quota.tariff.remove": "Please confirm that you want to remove this Quota Tariff.", "message.action.reboot.instance": "Please confirm that you want to reboot this Instance.", +"message.action.reboot.instance.boot.group": "Please confirm that you want to reboot this Instance Boot Group. Members will be stopped highest-order first, then started lowest-order first.", "message.action.reboot.router": "All services provided by this virtual router will be interrupted. Please confirm that you want to reboot this router.", "message.action.reboot.systemvm": "Please confirm that you want to reboot this system VM.", "message.action.recover.sharedfs": "Please confirm that you would like to recover this Shared FileSystem.", @@ -3210,10 +3250,12 @@ "message.action.settings.warning.vm.running": "Please stop the Instance to access settings.", "message.action.start.sharedfs": "Please confirm that you want to start this Shared FileSystem.", "message.action.start.instance": "Please confirm that you want to start this Instance.", +"message.action.start.instance.boot.group": "Please confirm that you want to start this Instance Boot Group. Members will be started in ascending boot order.", "message.action.start.router": "Please confirm that you want to start this router.", "message.action.start.systemvm": "Please confirm that you want to start this system VM.", "message.action.stop.sharedfs": "Please confirm that you want to stop this Shared FileSystem.", "message.action.stop.instance": "Please confirm that you want to stop this Instance.", +"message.action.stop.instance.boot.group": "Please confirm that you want to stop this Instance Boot Group. Members will be stopped in descending boot order.", "message.action.stop.router": "All services provided by this virtual router will be interrupted. Please confirm that you want to stop this router.", "message.action.stop.systemvm": "Please confirm that you want to stop this system VM.", "message.action.unmanage.cluster": "Please confirm that you want to unmanage the Cluster.", @@ -3410,6 +3452,8 @@ "message.confirm.manage.gpu.devices": "Please confirm that you want to manage the selected GPU devices?", "message.confirm.remove.firewall.rule": "Please confirm that you want to delete this Firewall Rule?", "message.confirm.remove.ip.range": "Please confirm that you would like to remove this IP range.", +"message.confirm.delete.readiness.rule": "Are you sure you want to delete this readiness rule?", +"message.confirm.remove.member": "Please confirm that you want to remove this member from the Instance Boot Group.", "message.confirm.remove.network.offering": "Are you sure you want to remove this Network offering?", "message.confirm.remove.network.policy": "Please confirm that you want to remove this Network Policy?", "message.confirm.remove.routing.policy": "Please confirm that you want to delete this Routing Policy?", @@ -3771,6 +3815,7 @@ "message.import.running.instance.warning": "The selected VM is powered-on on the VMware Datacenter. The recommended state to convert a VMware VM into KVM is powered-off after a graceful shutdown of the guest OS.", "message.import.vm.tasks": "Import from VMware to KVM tasks", "message.import.volume": "Please specify the domain, account or project name.
If not set, the volume will be imported for the caller.", +"message.inherited.readiness.rule": "This rule is inherited from the instance group; delete it from the group's readiness rules instead.", "message.info.cloudian.console": "Cloudian Management Console should open in another window.", "message.installwizard.cloudstack.helptext.website": " * Project website:\t ", "message.infra.setup.netris.description": "This zone must contain a Netris provider because the isolation method is Netris", @@ -3903,6 +3948,7 @@ "message.new.version.available": "A new version of CloudStack is available. Click here to check the details", "message.no.data.to.show.for.period": "No data to show for the selected period.", "message.no.description": "No description entered.", +"message.no.more.readiness.rule.types": "All applicable readiness rule types have already been added.", "message.note.about.keypair.permissions.title": "Note about API key pair rule permissions", "message.note.about.keypair.permissions.body": "During the creation of API key pairs, it is possible to define a corresponding set of rule permissions. If a rule set is defined, the API key pair will only have access to APIs for which access has been explicitly granted (i.e., APIs whose corresponding rules are marked as allowed). On the other hand, if no rule set is specified, the API key pair permissions will follow and adapt to the permission set of the user's account role.", "message.offering.internet.protocol.warning": "WARNING: IPv6 supported Networks use static routing and will require upstream routes to be configured manually.", @@ -3935,6 +3981,7 @@ "message.quota.usage.resource.warn": "Resources that are tagged as do not have constant metadata (if removed, the data is deleted) and cannot be retrieved.", "message.read.accept.license.agreements": "Please read and accept the terms for the license agreements.", "message.read.admin.guide.scaling.up": "Please read the dynamic scaling section in the admin guide before scaling up.", +"message.readiness.guest.liveness.warning": "Please ensure that the guest agent is running and responsive for the instance(s).", "message.recover.vm": "Please confirm that you would like to recover this Instance.", "message.reinstall.vm": "NOTE: Proceed with caution. This will cause the Instance to be reinstalled from the Template; data on the root disk will be lost. Extra data volumes, if any, will not be touched.", "message.release.ip.failed": "Failed to release IP", @@ -4059,6 +4106,9 @@ "message.success.add.ip.v6.prefix": "Successfully added IPv6 Prefix", "message.success.add.kuberversion": "Successfully added Kubernetes version", "message.success.add.logical.router": "Successfully added Logical Router", +"message.success.add.member": "Successfully added member to Instance Boot Group", +"message.success.add.readiness.rule": "Successfully added readiness rule", +"message.success.delete.readiness.rule": "Successfully deleted readiness rule", "message.success.add.network": "Successfully added Network", "message.success.add.network.acl": "Successfully added Network ACL", "message.success.add.network.static.route": "Successfully added Network Static Route", @@ -4144,6 +4194,7 @@ "message.success.discover.gpu.devices": "Successfully discovered GPU devices", "message.success.edit.acl": "Successfully edited ACL rule", "message.success.edit.primary.storage": "Successfully edited Primary Storage", +"message.success.edit.readiness.rule": "Successfully edited readiness rule", "message.success.edit.rule": "Successfully edited rule", "message.success.enable.saml.auth": "Successfully enabled SAML Authorization", "message.success.import.instance": "Successfully imported Instance", @@ -4172,6 +4223,7 @@ "message.success.remove.ip": "Successfully removed IP", "message.success.remove.iprange": "Successfully removed IP Range", "message.success.remove.logical.router": "Successfully removed Logical Router", +"message.success.remove.member": "Successfully removed member from Instance Boot Group", "message.success.remove.network.policy": "Successfully removed Network Policy", "message.success.remove.network.permissions": "Successfully removed Network Permissions", "message.success.remove.nic": "Successfully removed", @@ -4192,6 +4244,7 @@ "message.success.update.bgp.peer": "Successfully updated BGP peer", "message.success.update.bucket": "Successfully updated bucket", "message.success.update.condition": "Successfully updated condition", +"message.success.update.member.order": "Successfully updated boot order", "message.success.update.gpu.device": "Successfully updated GPU device", "message.success.create.vgpu.profile": "Successfully created vGPU profile", "message.success.update.vgpu.profile": "Successfully updated vGPU profile", diff --git a/ui/src/components/view/ListView.vue b/ui/src/components/view/ListView.vue index 9272617900f1..3d557a4efe01 100644 --- a/ui/src/components/view/ListView.vue +++ b/ui/src/components/view/ListView.vue @@ -1211,7 +1211,7 @@ export default { quickViewEnabled (actions, columns, key) { return actions.length > 0 && (columns && key === columns[0].dataIndex) && - new RegExp(['/vm', '/kubernetes', '/ssh', '/userdata', '/vmgroup', '/affinitygroup', '/autoscalevmgroup', + new RegExp(['/vm', '/kubernetes', '/ssh', '/userdata', '/vmgroup', '/instancebootgroup', '/affinitygroup', '/autoscalevmgroup', '/volume', '/snapshot', '/vmsnapshot', '/backup', '/guestnetwork', '/vpc', '/vpncustomergateway', '/vnfapp', '/template', '/iso', diff --git a/ui/src/config/section/compute.js b/ui/src/config/section/compute.js index 598c80bc4800..c771bce5cc48 100644 --- a/ui/src/config/section/compute.js +++ b/ui/src/config/section/compute.js @@ -1064,6 +1064,91 @@ export default { } ] }, + { + name: 'instancebootgroup', + title: 'label.instance.boot.groups', + icon: 'ordered-list-outlined', + resourceType: 'InstanceBootGroup', + permission: ['listInstanceBootGroups'], + searchFilters: ['name', 'domainid', 'account'], + columns: (store) => { + var fields = ['name', 'description', 'account'] + if (store.listAllProjects) { + fields.push('project') + } + fields.push('domain') + fields.push('created') + return fields + }, + details: ['name', 'id', 'description', 'account', 'domain', 'created'], + tabs: [ + { + name: 'details', + component: shallowRef(defineAsyncComponent(() => import('@/components/view/DetailsTab.vue'))) + }, + { + name: 'members', + component: shallowRef(defineAsyncComponent(() => import('@/views/compute/InstanceBootGroupMembersTab.vue'))) + }, + { + name: 'events', + resourceType: 'InstanceBootGroup', + component: shallowRef(defineAsyncComponent(() => import('@/components/view/EventsTab.vue'))) + } + ], + actions: [ + { + api: 'createInstanceBootGroup', + icon: 'plus-outlined', + label: 'label.add.instance.boot.group', + listView: true, + args: ['name', 'description', 'domainid', 'account', 'readinessattempttimeoutseconds', 'readinessmaxretryattempts', 'readinessrebootonretry', 'readinessinitialdelayseconds'] + }, + { + api: 'updateInstanceBootGroup', + icon: 'edit-outlined', + label: 'label.update.instance.boot.group', + dataView: true, + args: ['name', 'description', 'readinessattempttimeoutseconds', 'readinessmaxretryattempts', 'readinessrebootonretry', 'readinessinitialdelayseconds'] + }, + { + api: 'startInstanceBootGroup', + icon: 'caret-right-outlined', + label: 'label.action.start.instance.boot.group', + message: 'message.action.start.instance.boot.group', + dataView: true, + popup: true + }, + { + api: 'stopInstanceBootGroup', + icon: 'poweroff-outlined', + label: 'label.action.stop.instance.boot.group', + message: 'message.action.stop.instance.boot.group', + dataView: true, + popup: true, + args: ['forced'] + }, + { + api: 'rebootInstanceBootGroup', + icon: 'reload-outlined', + label: 'label.action.reboot.instance.boot.group', + message: 'message.action.reboot.instance.boot.group', + dataView: true, + popup: true, + args: ['forced'] + }, + { + api: 'deleteInstanceBootGroup', + icon: 'delete-outlined', + label: 'label.delete.instance.boot.group', + message: 'message.action.delete.instance.boot.group', + dataView: true, + groupAction: true, + popup: true, + groupMap: (selection) => { return selection.map(x => { return { id: x } }) } + } + ] + }, { name: 'ssh', title: 'label.ssh.key.pairs', diff --git a/ui/src/core/lazy_lib/icons_use.js b/ui/src/core/lazy_lib/icons_use.js index 43c6f822de31..ed7b883ddbb7 100644 --- a/ui/src/core/lazy_lib/icons_use.js +++ b/ui/src/core/lazy_lib/icons_use.js @@ -182,8 +182,8 @@ import { UserSwitchOutlined, UploadOutlined, VerticalAlignBottomOutlined, - VerticalAlignTopOutlined, VerticalAlignMiddleOutlined, + VerticalAlignTopOutlined, WarningOutlined, WifiOutlined, SolutionOutlined @@ -358,8 +358,8 @@ export default { app.component('UserSwitchOutlined', UserSwitchOutlined) app.component('UploadOutlined', UploadOutlined) app.component('VerticalAlignBottomOutlined', VerticalAlignBottomOutlined) - app.component('VerticalAlignTopOutlined', VerticalAlignTopOutlined) app.component('VerticalAlignMiddleOutlined', VerticalAlignMiddleOutlined) + app.component('VerticalAlignTopOutlined', VerticalAlignTopOutlined) app.component('WarningOutlined', WarningOutlined) app.component('WifiOutlined', WifiOutlined) app.component('renderIcon', renderIcon) diff --git a/ui/src/views/compute/AddInstanceBootGroupMember.vue b/ui/src/views/compute/AddInstanceBootGroupMember.vue new file mode 100644 index 000000000000..64a9e32249f2 --- /dev/null +++ b/ui/src/views/compute/AddInstanceBootGroupMember.vue @@ -0,0 +1,174 @@ +// 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. + + + + + + diff --git a/ui/src/views/compute/InstanceBootGroupMembersTab.vue b/ui/src/views/compute/InstanceBootGroupMembersTab.vue new file mode 100644 index 000000000000..d4253c6033a6 --- /dev/null +++ b/ui/src/views/compute/InstanceBootGroupMembersTab.vue @@ -0,0 +1,352 @@ +// 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. + + + + diff --git a/ui/src/views/compute/InstanceBootGroupReadinessRulesModal.vue b/ui/src/views/compute/InstanceBootGroupReadinessRulesModal.vue new file mode 100644 index 000000000000..88323ecfbe76 --- /dev/null +++ b/ui/src/views/compute/InstanceBootGroupReadinessRulesModal.vue @@ -0,0 +1,408 @@ +// 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. + + + + + + diff --git a/ui/src/views/compute/UpdateInstanceBootGroupMemberOrder.vue b/ui/src/views/compute/UpdateInstanceBootGroupMemberOrder.vue new file mode 100644 index 000000000000..ff9d7d09d6b7 --- /dev/null +++ b/ui/src/views/compute/UpdateInstanceBootGroupMemberOrder.vue @@ -0,0 +1,116 @@ +// 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. + + + + + +