From 0fcdb6d29c930771d887f4f0a040f92935835995 Mon Sep 17 00:00:00 2001 From: Abhishek Kumar Date: Mon, 6 Jul 2026 12:03:28 +0530 Subject: [PATCH 1/9] wip: initial skeleton Signed-off-by: Abhishek Kumar --- .../main/java/com/cloud/event/EventTypes.java | 23 + .../api/ApiCommandResourceType.java | 3 +- .../apache/cloudstack/api/ApiConstants.java | 7 + .../AddMemberToInstanceBootGroupCmd.java | 104 ++++ .../bootgroup/CreateInstanceBootGroupCmd.java | 109 ++++ .../bootgroup/DeleteInstanceBootGroupCmd.java | 80 +++ .../ListInstanceBootGroupMembersCmd.java | 66 +++ .../bootgroup/ListInstanceBootGroupsCmd.java | 82 +++ .../bootgroup/RebootInstanceBootGroupCmd.java | 98 ++++ .../RemoveInstanceBootGroupMemberCmd.java | 80 +++ .../bootgroup/StartInstanceBootGroupCmd.java | 98 ++++ .../bootgroup/StopInstanceBootGroupCmd.java | 92 ++++ .../bootgroup/UpdateInstanceBootGroupCmd.java | 94 ++++ .../UpdateInstanceBootGroupMemberCmd.java | 87 +++ .../InstanceBootGroupMemberResponse.java | 98 ++++ .../response/InstanceBootGroupResponse.java | 128 +++++ .../vm/bootgroup/InstanceBootGroup.java | 33 ++ .../vm/bootgroup/InstanceBootGroupMember.java | 40 ++ .../bootgroup/InstanceBootGroupService.java | 63 +++ .../cloud/vm/dao/InstanceBootGroupDao.java | 30 ++ .../vm/dao/InstanceBootGroupDaoImpl.java | 60 +++ .../vm/dao/InstanceBootGroupMemberDao.java | 38 ++ .../dao/InstanceBootGroupMemberDaoImpl.java | 90 ++++ .../bootgroup/InstanceBootGroupMemberVO.java | 109 ++++ .../vm/bootgroup/InstanceBootGroupVO.java | 124 +++++ ...spring-engine-schema-core-daos-context.xml | 3 + .../META-INF/db/schema-42210to42300.sql | 30 ++ .../views/cloud.instance_boot_group_view.sql | 47 ++ .../java/com/cloud/api/ApiResponseHelper.java | 17 +- .../query/dao/InstanceBootGroupJoinDao.java | 25 + .../dao/InstanceBootGroupJoinDaoImpl.java | 25 + .../api/query/vo/InstanceBootGroupJoinVO.java | 190 +++++++ .../bootgroup/InstanceBootGroupManager.java | 23 + .../InstanceBootGroupManagerImpl.java | 506 ++++++++++++++++++ .../spring-server-core-managers-context.xml | 2 + tools/apidoc/gen_toc.py | 3 +- ui/public/config.json | 150 +++++- ui/public/locales/en.json | 21 + ui/src/components/view/ListView.vue | 2 +- ui/src/config/section/compute.js | 83 +++ .../compute/AddInstanceBootGroupMember.vue | 168 ++++++ .../compute/InstanceBootGroupMembersTab.vue | 267 +++++++++ .../UpdateInstanceBootGroupMemberOrder.vue | 116 ++++ 43 files changed, 3507 insertions(+), 7 deletions(-) create mode 100644 api/src/main/java/org/apache/cloudstack/api/command/user/bootgroup/AddMemberToInstanceBootGroupCmd.java create mode 100644 api/src/main/java/org/apache/cloudstack/api/command/user/bootgroup/CreateInstanceBootGroupCmd.java create mode 100644 api/src/main/java/org/apache/cloudstack/api/command/user/bootgroup/DeleteInstanceBootGroupCmd.java create mode 100644 api/src/main/java/org/apache/cloudstack/api/command/user/bootgroup/ListInstanceBootGroupMembersCmd.java create mode 100644 api/src/main/java/org/apache/cloudstack/api/command/user/bootgroup/ListInstanceBootGroupsCmd.java create mode 100644 api/src/main/java/org/apache/cloudstack/api/command/user/bootgroup/RebootInstanceBootGroupCmd.java create mode 100644 api/src/main/java/org/apache/cloudstack/api/command/user/bootgroup/RemoveInstanceBootGroupMemberCmd.java create mode 100644 api/src/main/java/org/apache/cloudstack/api/command/user/bootgroup/StartInstanceBootGroupCmd.java create mode 100644 api/src/main/java/org/apache/cloudstack/api/command/user/bootgroup/StopInstanceBootGroupCmd.java create mode 100644 api/src/main/java/org/apache/cloudstack/api/command/user/bootgroup/UpdateInstanceBootGroupCmd.java create mode 100644 api/src/main/java/org/apache/cloudstack/api/command/user/bootgroup/UpdateInstanceBootGroupMemberCmd.java create mode 100644 api/src/main/java/org/apache/cloudstack/api/response/InstanceBootGroupMemberResponse.java create mode 100644 api/src/main/java/org/apache/cloudstack/api/response/InstanceBootGroupResponse.java create mode 100644 api/src/main/java/org/apache/cloudstack/vm/bootgroup/InstanceBootGroup.java create mode 100644 api/src/main/java/org/apache/cloudstack/vm/bootgroup/InstanceBootGroupMember.java create mode 100644 api/src/main/java/org/apache/cloudstack/vm/bootgroup/InstanceBootGroupService.java create mode 100644 engine/schema/src/main/java/com/cloud/vm/dao/InstanceBootGroupDao.java create mode 100644 engine/schema/src/main/java/com/cloud/vm/dao/InstanceBootGroupDaoImpl.java create mode 100644 engine/schema/src/main/java/com/cloud/vm/dao/InstanceBootGroupMemberDao.java create mode 100644 engine/schema/src/main/java/com/cloud/vm/dao/InstanceBootGroupMemberDaoImpl.java create mode 100644 engine/schema/src/main/java/org/apache/cloudstack/vm/bootgroup/InstanceBootGroupMemberVO.java create mode 100644 engine/schema/src/main/java/org/apache/cloudstack/vm/bootgroup/InstanceBootGroupVO.java create mode 100644 engine/schema/src/main/resources/META-INF/db/views/cloud.instance_boot_group_view.sql create mode 100644 server/src/main/java/org/apache/cloudstack/api/query/dao/InstanceBootGroupJoinDao.java create mode 100644 server/src/main/java/org/apache/cloudstack/api/query/dao/InstanceBootGroupJoinDaoImpl.java create mode 100644 server/src/main/java/org/apache/cloudstack/api/query/vo/InstanceBootGroupJoinVO.java create mode 100644 server/src/main/java/org/apache/cloudstack/vm/bootgroup/InstanceBootGroupManager.java create mode 100644 server/src/main/java/org/apache/cloudstack/vm/bootgroup/InstanceBootGroupManagerImpl.java create mode 100644 ui/src/views/compute/AddInstanceBootGroupMember.vue create mode 100644 ui/src/views/compute/InstanceBootGroupMembersTab.vue create mode 100644 ui/src/views/compute/UpdateInstanceBootGroupMemberOrder.vue diff --git a/api/src/main/java/com/cloud/event/EventTypes.java b/api/src/main/java/com/cloud/event/EventTypes.java index c97e916fe430..4283fc24c5d0 100644 --- a/api/src/main/java/com/cloud/event/EventTypes.java +++ b/api/src/main/java/com/cloud/event/EventTypes.java @@ -49,6 +49,7 @@ 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 com.cloud.dc.DataCenter; import com.cloud.dc.DataCenterGuestIpv6Prefix; @@ -897,6 +898,18 @@ 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.REODER"; + + static { // TODO: need a way to force author adding event types to declare the entity details as well, with out braking @@ -1466,6 +1479,16 @@ 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); } 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..6dfa4987b899 100644 --- a/api/src/main/java/org/apache/cloudstack/api/ApiCommandResourceType.java +++ b/api/src/main/java/org/apache/cloudstack/api/ApiCommandResourceType.java @@ -91,7 +91,8 @@ 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); 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 5c53430388da..570599b170c7 100644 --- a/api/src/main/java/org/apache/cloudstack/api/ApiConstants.java +++ b/api/src/main/java/org/apache/cloudstack/api/ApiConstants.java @@ -92,6 +92,12 @@ 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 CA_CERTIFICATES = "cacertificates"; public static final String CERTIFICATE = "certificate"; public static final String CERTIFICATE_CHAIN = "certchain"; @@ -320,6 +326,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..8d3e611b07b8 --- /dev/null +++ b/api/src/main/java/org/apache/cloudstack/api/command/user/bootgroup/AddMemberToInstanceBootGroupCmd.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 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)") + 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..4d7c5c6cdcbd --- /dev/null +++ b/api/src/main/java/org/apache/cloudstack/api/command/user/bootgroup/CreateInstanceBootGroupCmd.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 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; + + 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; + } + + @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/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/ListInstanceBootGroupMembersCmd.java b/api/src/main/java/org/apache/cloudstack/api/command/user/bootgroup/ListInstanceBootGroupMembersCmd.java new file mode 100644 index 000000000000..7de1a8f50a77 --- /dev/null +++ b/api/src/main/java/org/apache/cloudstack/api/command/user/bootgroup/ListInstanceBootGroupMembersCmd.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.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.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; + + public Long getBootGroupId() { + return bootGroupId; + } + + public String getMemberType() { + return memberType; + } + + @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/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..e22736088d1a --- /dev/null +++ b/api/src/main/java/org/apache/cloudstack/api/command/user/bootgroup/RebootInstanceBootGroupCmd.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 = "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; + + public Long getId() { + return id; + } + + @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..ea88d008dd9a --- /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 UUID 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 id; + } + + @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..010e0fbb1ecd --- /dev/null +++ b/api/src/main/java/org/apache/cloudstack/api/command/user/bootgroup/StopInstanceBootGroupCmd.java @@ -0,0 +1,92 @@ +// Licensed to the Apache Software Foundation (ASF) under one +// or more contributor license agreements. See the NOTICE file +// distributed with this work for additional information +// regarding copyright ownership. The ASF licenses this file +// to you under the Apache License, Version 2.0 (the +// "License"); you may not use this file except in compliance +// with the License. You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, +// software distributed under the License is distributed on an +// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +// KIND, either express or implied. See the License for the +// specific language governing permissions and limitations +// under the License. + +package 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; + + public Long getId() { + return id; + } + + @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..8d1f3499cd96 --- /dev/null +++ b/api/src/main/java/org/apache/cloudstack/api/command/user/bootgroup/UpdateInstanceBootGroupCmd.java @@ -0,0 +1,94 @@ +// 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; + + public Long getId() { + return id; + } + + public String getName() { + return name; + } + + public String getDescription() { + return description; + } + + @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..c4ccc8d60482 --- /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 id; + } + + @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/response/InstanceBootGroupMemberResponse.java b/api/src/main/java/org/apache/cloudstack/api/response/InstanceBootGroupMemberResponse.java new file mode 100644 index 000000000000..3bb323b10e0a --- /dev/null +++ b/api/src/main/java/org/apache/cloudstack/api/response/InstanceBootGroupMemberResponse.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.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.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; + + 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; + } +} 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..e7af5c941f21 --- /dev/null +++ b/api/src/main/java/org/apache/cloudstack/api/response/InstanceBootGroupResponse.java @@ -0,0 +1,128 @@ +// 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; + + 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; + } +} 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..fc6ce54cee95 --- /dev/null +++ b/api/src/main/java/org/apache/cloudstack/vm/bootgroup/InstanceBootGroupMember.java @@ -0,0 +1,40 @@ +// 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 + } +} 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..0d694cb43e3d --- /dev/null +++ b/api/src/main/java/org/apache/cloudstack/vm/bootgroup/InstanceBootGroupService.java @@ -0,0 +1,63 @@ +// 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.DeleteInstanceBootGroupCmd; +import org.apache.cloudstack.api.command.user.bootgroup.ListInstanceBootGroupMembersCmd; +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.response.InstanceBootGroupMemberResponse; +import org.apache.cloudstack.api.response.InstanceBootGroupResponse; +import org.apache.cloudstack.api.response.ListResponse; + +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); + +} 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/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/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..5a4c2d5deb14 --- /dev/null +++ b/engine/schema/src/main/java/org/apache/cloudstack/vm/bootgroup/InstanceBootGroupMemberVO.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.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; + +@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; + } +} 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..a8211c197969 --- /dev/null +++ b/engine/schema/src/main/java/org/apache/cloudstack/vm/bootgroup/InstanceBootGroupVO.java @@ -0,0 +1,124 @@ +// 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 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; + } +} 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 d7cacd09b9a8..8f941c4d3384 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,8 @@ + + @@ -326,5 +328,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 9f4353490956..0aaba4e251bf 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 @@ -575,3 +575,33 @@ CREATE TABLE IF NOT EXISTS `cloud`.`dns_zone_network_map` ( CONSTRAINT `fk_dns_map__zone_id` FOREIGN KEY (`dns_zone_id`) REFERENCES `dns_zone` (`id`) ON DELETE CASCADE, CONSTRAINT `fk_dns_map__network_id` FOREIGN KEY (`network_id`) REFERENCES `networks` (`id`) ON DELETE CASCADE ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; + +-- 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; 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/server/src/main/java/com/cloud/api/ApiResponseHelper.java b/server/src/main/java/com/cloud/api/ApiResponseHelper.java index 7cebeed71402..e0bd2785209e 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; @@ -568,6 +568,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); @@ -1565,7 +1577,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/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/InstanceBootGroupManager.java b/server/src/main/java/org/apache/cloudstack/vm/bootgroup/InstanceBootGroupManager.java new file mode 100644 index 000000000000..4ab708304034 --- /dev/null +++ b/server/src/main/java/org/apache/cloudstack/vm/bootgroup/InstanceBootGroupManager.java @@ -0,0 +1,23 @@ +// 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; + +public interface InstanceBootGroupManager extends InstanceBootGroupService, Manager { +} 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..a882b7e17b2f --- /dev/null +++ b/server/src/main/java/org/apache/cloudstack/vm/bootgroup/InstanceBootGroupManagerImpl.java @@ -0,0 +1,506 @@ +// 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.Comparator; +import java.util.List; +import java.util.Map; +import java.util.TreeMap; +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.command.user.bootgroup.AddMemberToInstanceBootGroupCmd; +import org.apache.cloudstack.api.command.user.bootgroup.CreateInstanceBootGroupCmd; +import org.apache.cloudstack.api.command.user.bootgroup.DeleteInstanceBootGroupCmd; +import org.apache.cloudstack.api.command.user.bootgroup.ListInstanceBootGroupMembersCmd; +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.query.dao.InstanceBootGroupJoinDao; +import org.apache.cloudstack.api.query.vo.InstanceBootGroupJoinVO; +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.context.CallContext; +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.ManagerBase; +import com.cloud.utils.component.PluggableService; +import com.cloud.utils.concurrency.NamedThreadFactory; +import com.cloud.utils.db.Filter; +import com.cloud.utils.db.SearchBuilder; +import com.cloud.utils.db.SearchCriteria; +import com.cloud.utils.exception.CloudRuntimeException; +import com.cloud.vm.InstanceGroupVO; +import com.cloud.vm.UserVmService; +import com.cloud.vm.UserVmVO; +import com.cloud.vm.dao.InstanceBootGroupDao; +import com.cloud.vm.dao.InstanceBootGroupMemberDao; +import com.cloud.vm.dao.InstanceGroupDao; +import com.cloud.vm.dao.InstanceGroupVMMapDao; +import com.cloud.vm.dao.UserVmDao; + +@Component +public class InstanceBootGroupManagerImpl extends ManagerBase implements InstanceBootGroupManager, PluggableService { + + @Inject + private InstanceBootGroupDao instanceBootGroupDao; + + @Inject + private InstanceBootGroupJoinDao instanceBootGroupJoinDao; + + @Inject + private InstanceBootGroupMemberDao instanceBootGroupMemberDao; + + @Inject + private AccountManager accountManager; + + @Inject + private UserVmService userVmService; + + @Inject + private UserVmDao userVmDao; + + @Inject + private InstanceGroupDao instanceGroupDao; + + @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); + response.setObjectName("instancebootgroup"); + return response; + } + + @Override + public boolean configure(String name, Map params) throws ConfigurationException { + return true; + } + + @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"); + } + + InstanceBootGroupVO group = new InstanceBootGroupVO(cmd.getName(), cmd.getDescription(), owner.getId(), owner.getDomainId()); + group = instanceBootGroupDao.persist(group); + CallContext.current().setEventResourceId(group.getId()); + 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()); + 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) { + 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()); + } + + instanceBootGroupDao.update(group.getId(), group); + return instanceBootGroupDao.findById(group.getId()); + } + + @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 webhook : bootGroupsAndCount.first()) { + InstanceBootGroupResponse response = createInstanceBootGroupResponse(webhook); + 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; + + if (cmd.getVirtualMachineId() != null && cmd.getInstanceGroupId() != null) { + throw new InvalidParameterValueException("Only one of virtualmachineid or instancegroupid may be specified"); + } + if (cmd.getVirtualMachineId() == null && cmd.getInstanceGroupId() == null) { + throw new InvalidParameterValueException("Either virtualmachineid or instancegroupid must be specified"); + } + + if (cmd.getVirtualMachineId() != null) { + UserVm vm = userVmDao.findById(cmd.getVirtualMachineId()); + if (vm == null) { + throw new InvalidParameterValueException("Unable to find virtual machine with ID: " + cmd.getVirtualMachineId()); + } + validateMemberAccount(vm.getAccountId(), group.getAccountId()); + memberType = InstanceBootGroupMember.MemberType.VirtualMachine; + memberId = vm.getId(); + } else { + InstanceGroupVO instanceGroup = instanceGroupDao.findById(cmd.getInstanceGroupId()); + if (instanceGroup == null || instanceGroup.getRemoved() != null) { + throw new InvalidParameterValueException("Unable to find instance group with ID: " + cmd.getInstanceGroupId()); + } + validateMemberAccount(instanceGroup.getAccountId(), group.getAccountId()); + 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())); + } + + InstanceBootGroupMemberVO member = new InstanceBootGroupMemberVO(group.getId(), memberType, memberId, cmd.getOrder()); + return instanceBootGroupMemberDao.persist(member); + } + + @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()); + } + if (cmd.getOrder() < 0) { + throw new InvalidParameterValueException("Order value must be 0 or greater"); + } + getGroupAndCheckAccess(member.getBootGroupId()); + member.setOrder(cmd.getOrder()); + instanceBootGroupMemberDao.update(member.getId(), member); + return instanceBootGroupMemberDao.findById(member.getId()); + } + + @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)); + ListResponse response = new ListResponse<>(); + response.setResponses(members.stream().map(this::createInstanceBootGroupMemberResponse).collect(Collectors.toList()), result.second()); + return response; + } + + protected void startInstanceBootGroupInternal(InstanceBootGroupVO group) { + List members = instanceBootGroupMemberDao.listByBootGroupId(group.getId()); + Map> tiers = groupByOrder(members); + + for (Map.Entry> tier : tiers.entrySet()) { + List vmIds = resolveVmIds(tier.getValue()); + runTierConcurrently(vmIds, group, "start", vmId -> { + UserVm vm = userVmDao.findById(vmId); + if (vm != null && vm.getState() != com.cloud.vm.VirtualMachine.State.Running) { + userVmService.startVirtualMachine(vm, null); + } + }); + } + } + + @Override + @ActionEvent(eventType = EventTypes.EVENT_INSTANCE_BOOT_GROUP_START, eventDescription = "starting Instance Boot Group", async = true) + public InstanceBootGroup startInstanceBootGroup(final StartInstanceBootGroupCmd cmd) { + final long id = cmd.getId(); + InstanceBootGroupVO group = getGroupAndCheckAccess(id); + startInstanceBootGroupInternal(group); + return group; + } + + protected void stopInstanceBootGroupInternal(InstanceBootGroupVO group) { + List members = instanceBootGroupMemberDao.listByBootGroupId(group.getId()); + Map> tiers = groupByOrderDescending(members); + + for (Map.Entry> tier : tiers.entrySet()) { + List vmIds = resolveVmIds(tier.getValue()); + runTierConcurrently(vmIds, group, "stop", vmId -> { + UserVm vm = userVmDao.findById(vmId); + if (vm != null && vm.getState() != com.cloud.vm.VirtualMachine.State.Stopped) { + userVmService.stopVirtualMachine(vmId, false); + } + }); + } + } + + @Override + @ActionEvent(eventType = EventTypes.EVENT_INSTANCE_BOOT_GROUP_STOP, eventDescription = "stopping Instance Boot Group", async = true) + public InstanceBootGroup stopInstanceBootGroup(final StopInstanceBootGroupCmd cmd) { + final long id = cmd.getId(); + InstanceBootGroupVO group = getGroupAndCheckAccess(id); + stopInstanceBootGroupInternal(group); + return group; + } + + /** + * Runs {@code action} for every VM in a single boot-order tier concurrently (one thread per VM), + * waits for all of them to finish, and aborts on the first failure so the caller does not proceed + * to the next tier with a partially started/stopped tier. + */ + private void runTierConcurrently(List vmIds, InstanceBootGroupVO group, String actionName, VmAction action) { + if (vmIds.isEmpty()) { + return; + } + + ExecutorService executor = Executors.newFixedThreadPool(vmIds.size(), new NamedThreadFactory("InstanceBootGroup-" + actionName)); + try { + List> futures = new ArrayList<>(); + for (Long vmId : vmIds) { + futures.add(executor.submit(() -> { + action.run(vmId); + return null; + })); + } + for (Future future : futures) { + try { + future.get(); + } catch (ExecutionException e) { + Throwable cause = e.getCause() != null ? e.getCause() : e; + throw new CloudRuntimeException("Failed to " + actionName + " a VM in boot group " + group.getName() + ": " + cause.getMessage(), cause); + } catch (InterruptedException e) { + Thread.currentThread().interrupt(); + throw new CloudRuntimeException("Interrupted while waiting to " + actionName + " VMs in boot group " + group.getName(), e); + } + } + } finally { + executor.shutdown(); + } + } + + @Override + @ActionEvent(eventType = EventTypes.EVENT_INSTANCE_BOOT_GROUP_REBOOT, eventDescription = "rebooting Instance Boot Group", async = true) + public InstanceBootGroup rebootInstanceBootGroup(final RebootInstanceBootGroupCmd cmd) { + final long id = cmd.getId(); + InstanceBootGroupVO group = getGroupAndCheckAccess(id); + stopInstanceBootGroupInternal(group); + startInstanceBootGroupInternal(group); + return group; + } + + @Override + public InstanceBootGroupResponse createInstanceBootGroupResponse(long id) { + return createInstanceBootGroupResponse(instanceBootGroupJoinDao.findById(id)); + } + + @Override + public InstanceBootGroupMemberResponse createInstanceBootGroupMemberResponse(InstanceBootGroupMember member) { + 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()); + if (member.getMemberType() == org.apache.cloudstack.vm.bootgroup.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 { + com.cloud.vm.InstanceGroupVO instanceGroup = instanceGroupDao.findById(member.getMemberId()); + if (instanceGroup != null) { + response.setMemberId(instanceGroup.getUuid()); + response.setMemberName(instanceGroup.getName()); + } + } + response.setObjectName("instancebootgroupmember"); + return response; + } + + private void validateMemberAccount(long memberAccountId, long groupAccountId) { + if (memberAccountId != groupAccountId) { + throw new PermissionDeniedException("Member must belong to the same account as the boot group"); + } + } + + 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; + } + + @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 + ); + } + + @Override + public String getName() { + return InstanceBootGroupMember.class.getSimpleName(); + } +} 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 eef304226af2..04412e6e7cd6 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 @@ -395,6 +395,8 @@ + + diff --git a/tools/apidoc/gen_toc.py b/tools/apidoc/gen_toc.py index d703769eb1c1..06b02b160e74 100644 --- a/tools/apidoc/gen_toc.py +++ b/tools/apidoc/gen_toc.py @@ -282,7 +282,8 @@ 'CustomAction' : 'Extension', 'CustomActions' : 'Extension', 'ImportVmTask': 'Import VM Task', - 'Dns': 'DNS' + 'Dns': 'DNS', + 'InstanceBootGroup': 'InstanceBootGroup', } 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 6ba6f458a0a2..7a87a70db8e7 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", @@ -292,6 +295,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", @@ -303,6 +307,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", @@ -508,6 +513,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", @@ -822,6 +828,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", @@ -1363,7 +1370,10 @@ "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", @@ -1676,6 +1686,8 @@ "label.maxvpc": "Max. VPCs", "label.may.continue": "You may now continue.", "label.mb.memory": "MB memory", +"label.member.type": "Member Type", +"label.members": "Members", "label.memory": "Memory", "label.memory.free": "Memory free", "label.memory.maximum.mb": "Max memory (in MB)", @@ -2746,6 +2758,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", @@ -3077,6 +3090,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.", @@ -3142,6 +3156,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.", @@ -3165,10 +3180,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.", @@ -3365,6 +3382,7 @@ "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.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?", @@ -4012,6 +4030,7 @@ "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.network": "Successfully added Network", "message.success.add.network.acl": "Successfully added Network ACL", "message.success.add.network.static.route": "Successfully added Network Static Route", @@ -4125,6 +4144,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", @@ -4145,6 +4165,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 37abd43e46c7..334a758e5d37 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 9262664d448e..d8b65838a37e 100644 --- a/ui/src/config/section/compute.js +++ b/ui/src/config/section/compute.js @@ -1048,6 +1048,89 @@ 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'] + }, + { + api: 'updateInstanceBootGroup', + icon: 'edit-outlined', + label: 'label.update.instance.boot.group', + dataView: true, + args: ['name', 'description'] + }, + { + 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 + }, + { + api: 'rebootInstanceBootGroup', + icon: 'reload-outlined', + label: 'label.action.reboot.instance.boot.group', + message: 'message.action.reboot.instance.boot.group', + dataView: true, + popup: true + }, + { + 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/views/compute/AddInstanceBootGroupMember.vue b/ui/src/views/compute/AddInstanceBootGroupMember.vue new file mode 100644 index 000000000000..2d39e85f5d8b --- /dev/null +++ b/ui/src/views/compute/AddInstanceBootGroupMember.vue @@ -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. + + + + + + diff --git a/ui/src/views/compute/InstanceBootGroupMembersTab.vue b/ui/src/views/compute/InstanceBootGroupMembersTab.vue new file mode 100644 index 000000000000..40e33d106dee --- /dev/null +++ b/ui/src/views/compute/InstanceBootGroupMembersTab.vue @@ -0,0 +1,267 @@ +// 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. + + + + + + From db30602c2db96cd1c0febc0a0b73a39bcb7f5264 Mon Sep 17 00:00:00 2001 From: Abhishek Kumar Date: Tue, 14 Jul 2026 10:44:58 +0530 Subject: [PATCH 2/9] refactor and readiness changes Signed-off-by: Abhishek Kumar --- .../main/java/com/cloud/event/EventTypes.java | 7 + .../api/ApiCommandResourceType.java | 3 +- .../apache/cloudstack/api/ApiConstants.java | 3 + ...eateInstanceBootGroupReadinessRuleCmd.java | 132 ++++ ...leteInstanceBootGroupReadinessRuleCmd.java | 81 +++ ...istInstanceBootGroupReadinessRulesCmd.java | 90 +++ ...dateInstanceBootGroupReadinessRuleCmd.java | 109 ++++ ...nstanceBootGroupReadinessRuleResponse.java | 139 +++++ .../InstanceBootGroupReadinessRule.java | 64 ++ .../bootgroup/InstanceBootGroupService.java | 15 + ...tanceBootGroupReadinessCheckResultDao.java | 37 ++ ...eBootGroupReadinessCheckResultDaoImpl.java | 67 ++ .../InstanceBootGroupReadinessRuleDao.java | 39 ++ ...InstanceBootGroupReadinessRuleDaoImpl.java | 79 +++ ...tanceBootGroupReadinessRuleDetailsDao.java | 32 + ...eBootGroupReadinessRuleDetailsDaoImpl.java | 69 +++ ...stanceBootGroupReadinessCheckResultVO.java | 87 +++ ...stanceBootGroupReadinessRuleDetailsVO.java | 91 +++ .../InstanceBootGroupReadinessRuleVO.java | 141 +++++ ...spring-engine-schema-core-daos-context.xml | 3 + .../META-INF/db/schema-42210to42300.sql | 39 ++ .../java/com/cloud/vm/UserVmManagerImpl.java | 5 + .../InstanceBootGroupApiServiceImpl.java | 570 ++++++++++++++++++ .../bootgroup/InstanceBootGroupManager.java | 14 +- .../InstanceBootGroupManagerImpl.java | 357 +---------- .../InstanceBootGroupMembershipGuard.java | 112 ++++ ...anceBootGroupReadinessRuleManagerImpl.java | 225 +++++++ ...InstanceBootGroupReadinessRuleService.java | 46 ++ .../readiness/checker/ReadinessChecker.java | 59 ++ .../readiness/checker/VrPingChecker.java | 136 +++++ .../spring-server-core-managers-context.xml | 3 + 31 files changed, 2508 insertions(+), 346 deletions(-) create mode 100644 api/src/main/java/org/apache/cloudstack/api/command/user/bootgroup/CreateInstanceBootGroupReadinessRuleCmd.java create mode 100644 api/src/main/java/org/apache/cloudstack/api/command/user/bootgroup/DeleteInstanceBootGroupReadinessRuleCmd.java create mode 100644 api/src/main/java/org/apache/cloudstack/api/command/user/bootgroup/ListInstanceBootGroupReadinessRulesCmd.java create mode 100644 api/src/main/java/org/apache/cloudstack/api/command/user/bootgroup/UpdateInstanceBootGroupReadinessRuleCmd.java create mode 100644 api/src/main/java/org/apache/cloudstack/api/response/InstanceBootGroupReadinessRuleResponse.java create mode 100644 api/src/main/java/org/apache/cloudstack/vm/bootgroup/InstanceBootGroupReadinessRule.java create mode 100644 engine/schema/src/main/java/com/cloud/vm/dao/InstanceBootGroupReadinessCheckResultDao.java create mode 100644 engine/schema/src/main/java/com/cloud/vm/dao/InstanceBootGroupReadinessCheckResultDaoImpl.java create mode 100644 engine/schema/src/main/java/com/cloud/vm/dao/InstanceBootGroupReadinessRuleDao.java create mode 100644 engine/schema/src/main/java/com/cloud/vm/dao/InstanceBootGroupReadinessRuleDaoImpl.java create mode 100644 engine/schema/src/main/java/com/cloud/vm/dao/InstanceBootGroupReadinessRuleDetailsDao.java create mode 100644 engine/schema/src/main/java/com/cloud/vm/dao/InstanceBootGroupReadinessRuleDetailsDaoImpl.java create mode 100644 engine/schema/src/main/java/org/apache/cloudstack/vm/bootgroup/InstanceBootGroupReadinessCheckResultVO.java create mode 100644 engine/schema/src/main/java/org/apache/cloudstack/vm/bootgroup/InstanceBootGroupReadinessRuleDetailsVO.java create mode 100644 engine/schema/src/main/java/org/apache/cloudstack/vm/bootgroup/InstanceBootGroupReadinessRuleVO.java create mode 100644 server/src/main/java/org/apache/cloudstack/vm/bootgroup/InstanceBootGroupApiServiceImpl.java create mode 100644 server/src/main/java/org/apache/cloudstack/vm/bootgroup/InstanceBootGroupMembershipGuard.java create mode 100644 server/src/main/java/org/apache/cloudstack/vm/bootgroup/InstanceBootGroupReadinessRuleManagerImpl.java create mode 100644 server/src/main/java/org/apache/cloudstack/vm/bootgroup/InstanceBootGroupReadinessRuleService.java create mode 100644 server/src/main/java/org/apache/cloudstack/vm/bootgroup/readiness/checker/ReadinessChecker.java create mode 100644 server/src/main/java/org/apache/cloudstack/vm/bootgroup/readiness/checker/VrPingChecker.java diff --git a/api/src/main/java/com/cloud/event/EventTypes.java b/api/src/main/java/com/cloud/event/EventTypes.java index 4283fc24c5d0..e5d8788c2b26 100644 --- a/api/src/main/java/com/cloud/event/EventTypes.java +++ b/api/src/main/java/com/cloud/event/EventTypes.java @@ -50,6 +50,7 @@ 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.InstanceBootGroupReadinessRule; import com.cloud.dc.DataCenter; import com.cloud.dc.DataCenterGuestIpv6Prefix; @@ -908,6 +909,9 @@ public class EventTypes { 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.REODER"; + 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 { @@ -1489,6 +1493,9 @@ public class EventTypes { 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 6dfa4987b899..128dc9f0b61d 100644 --- a/api/src/main/java/org/apache/cloudstack/api/ApiCommandResourceType.java +++ b/api/src/main/java/org/apache/cloudstack/api/ApiCommandResourceType.java @@ -92,7 +92,8 @@ public enum ApiCommandResourceType { ExtensionCustomAction(org.apache.cloudstack.extension.ExtensionCustomAction.class), KmsKey(org.apache.cloudstack.kms.KMSKey.class), HsmProfile(org.apache.cloudstack.kms.HSMProfile.class), - InstanceBootGroup(org.apache.cloudstack.vm.bootgroup.InstanceBootGroup.class); + InstanceBootGroup(org.apache.cloudstack.vm.bootgroup.InstanceBootGroup.class), + InstanceBootGroupReadinessRule(org.apache.cloudstack.vm.bootgroup.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 570599b170c7..13b2e7d25b00 100644 --- a/api/src/main/java/org/apache/cloudstack/api/ApiConstants.java +++ b/api/src/main/java/org/apache/cloudstack/api/ApiConstants.java @@ -98,6 +98,9 @@ public class ApiConstants { 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 RULE_TYPE = "ruletype"; + public static final String READINESS_MODE = "readinessmode"; + public static final String READINESS_STATUS = "readinessstatus"; public static final String CA_CERTIFICATES = "cacertificates"; public static final String CERTIFICATE = "certificate"; public static final String CERTIFICATE_CHAIN = "certchain"; 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..92eecd292995 --- /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.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: GUEST_AGENT_LIVENESS, VR_PING, PORT_CHECK, CUSTOM_SCRIPT or INSTANCE_QUORUM") + private String ruleType; + + @Parameter(name = ApiConstants.NAME, type = CommandType.STRING, description = "The name of the readiness rule; auto-generated if omitted") + 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/DeleteInstanceBootGroupReadinessRuleCmd.java b/api/src/main/java/org/apache/cloudstack/api/command/user/bootgroup/DeleteInstanceBootGroupReadinessRuleCmd.java new file mode 100644 index 000000000000..ccffd9957402 --- /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.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/ListInstanceBootGroupReadinessRulesCmd.java b/api/src/main/java/org/apache/cloudstack/api/command/user/bootgroup/ListInstanceBootGroupReadinessRulesCmd.java new file mode 100644 index 000000000000..0a61f1750fcc --- /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.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/UpdateInstanceBootGroupReadinessRuleCmd.java b/api/src/main/java/org/apache/cloudstack/api/command/user/bootgroup/UpdateInstanceBootGroupReadinessRuleCmd.java new file mode 100644 index 000000000000..4a89f9e8f2b7 --- /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.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/InstanceBootGroupReadinessRuleResponse.java b/api/src/main/java/org/apache/cloudstack/api/response/InstanceBootGroupReadinessRuleResponse.java new file mode 100644 index 000000000000..04794f333aab --- /dev/null +++ b/api/src/main/java/org/apache/cloudstack/api/response/InstanceBootGroupReadinessRuleResponse.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.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.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.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 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/vm/bootgroup/InstanceBootGroupReadinessRule.java b/api/src/main/java/org/apache/cloudstack/vm/bootgroup/InstanceBootGroupReadinessRule.java new file mode 100644 index 000000000000..679450bb227d --- /dev/null +++ b/api/src/main/java/org/apache/cloudstack/vm/bootgroup/InstanceBootGroupReadinessRule.java @@ -0,0 +1,64 @@ +// 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; + +/** + * 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}: KVM-only, + * hypervisor-agnostic at the DB/API layer (no qemu/KVM naming stored). VR_PING/PORT_CHECK/ + * CUSTOM_SCRIPT/GUEST_AGENT_LIVENESS apply to VirtualMachine items; INSTANCE_QUORUM and + * (group-scope) CUSTOM_SCRIPT apply to InstanceGroup items. + */ + enum RuleType { + GUEST_AGENT_LIVENESS, + VR_PING, + PORT_CHECK, + CUSTOM_SCRIPT, + INSTANCE_QUORUM + } + + enum Status { + READY, NOT_READY, ERROR, UNKNOWN + } +} 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 index 0d694cb43e3d..1dc865083160 100644 --- a/api/src/main/java/org/apache/cloudstack/vm/bootgroup/InstanceBootGroupService.java +++ b/api/src/main/java/org/apache/cloudstack/vm/bootgroup/InstanceBootGroupService.java @@ -19,8 +19,11 @@ 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; @@ -28,7 +31,9 @@ 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; @@ -60,4 +65,14 @@ public interface InstanceBootGroupService { InstanceBootGroupMemberResponse createInstanceBootGroupMemberResponse(InstanceBootGroupMember member); + InstanceBootGroupReadinessRule createInstanceBootGroupReadinessRule(CreateInstanceBootGroupReadinessRuleCmd cmd); + + InstanceBootGroupReadinessRule updateInstanceBootGroupReadinessRule(UpdateInstanceBootGroupReadinessRuleCmd cmd); + + boolean deleteInstanceBootGroupReadinessRule(DeleteInstanceBootGroupReadinessRuleCmd cmd); + + ListResponse listInstanceBootGroupReadinessRules(ListInstanceBootGroupReadinessRulesCmd cmd); + + InstanceBootGroupReadinessRuleResponse createInstanceBootGroupReadinessRuleResponse(InstanceBootGroupReadinessRule rule); + } 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..ff375b0d6207 --- /dev/null +++ b/engine/schema/src/main/java/com/cloud/vm/dao/InstanceBootGroupReadinessCheckResultDao.java @@ -0,0 +1,37 @@ +// 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.InstanceBootGroupReadinessRule; + +import com.cloud.utils.db.GenericDao; + +public interface InstanceBootGroupReadinessCheckResultDao extends GenericDao { + + InstanceBootGroupReadinessCheckResultVO findByRuleId(long ruleId); + + /** + * Inserts or updates the single cached result row for a rule (no history, by design). + */ + void upsert(long ruleId, 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..ba1b3c8ab667 --- /dev/null +++ b/engine/schema/src/main/java/com/cloud/vm/dao/InstanceBootGroupReadinessCheckResultDaoImpl.java @@ -0,0 +1,67 @@ +// 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.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; + + public InstanceBootGroupReadinessCheckResultDaoImpl() { + ruleIdSearch = createSearchBuilder(); + ruleIdSearch.and("ruleId", ruleIdSearch.entity().getRuleId(), SearchCriteria.Op.EQ); + ruleIdSearch.done(); + } + + @Override + public InstanceBootGroupReadinessCheckResultVO findByRuleId(long ruleId) { + SearchCriteria sc = ruleIdSearch.create(); + sc.setParameters("ruleId", ruleId); + return findOneBy(sc); + } + + @Override + public void upsert(long ruleId, InstanceBootGroupReadinessRule.Status status, String message, Date checkedOn) { + InstanceBootGroupReadinessCheckResultVO existing = findByRuleId(ruleId); + if (existing == null) { + persist(new InstanceBootGroupReadinessCheckResultVO(ruleId, status, message, checkedOn)); + } else { + existing.setStatus(status); + existing.setMessage(message); + existing.setCheckedOn(checkedOn); + update(ruleId, 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..a24262200d7a --- /dev/null +++ b/engine/schema/src/main/java/com/cloud/vm/dao/InstanceBootGroupReadinessRuleDao.java @@ -0,0 +1,39 @@ +// 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 com.cloud.utils.db.GenericDao; + +public interface InstanceBootGroupReadinessRuleDao extends GenericDao { + + List listByBootGroupId(long bootGroupId); + + List listEnabledByItem(long bootGroupId, InstanceBootGroupMember.MemberType itemType, long itemId); + + /** + * Cleanup for a member removed from its boot group, or a VM leaving its Instance Group — there's + * no FK path for this (item_id doesn't reference instance_boot_group_member/instance_group_vm_map), + * so it's enforced here in code instead of a DB cascade. + */ + void deleteByItem(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..23a2ffe6280b --- /dev/null +++ b/engine/schema/src/main/java/com/cloud/vm/dao/InstanceBootGroupReadinessRuleDaoImpl.java @@ -0,0 +1,79 @@ +// 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.springframework.stereotype.Component; + +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 bootGroupSearch; + private final SearchBuilder itemSearch; + private final SearchBuilder enabledByItemSearch; + + public InstanceBootGroupReadinessRuleDaoImpl() { + bootGroupSearch = createSearchBuilder(); + bootGroupSearch.and("bootGroupId", bootGroupSearch.entity().getBootGroupId(), SearchCriteria.Op.EQ); + bootGroupSearch.done(); + + 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(); + } + + @Override + public List listByBootGroupId(long bootGroupId) { + SearchCriteria sc = bootGroupSearch.create(); + sc.setParameters("bootGroupId", bootGroupId); + return listBy(sc); + } + + @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 void deleteByItem(InstanceBootGroupMember.MemberType itemType, long itemId) { + SearchCriteria sc = itemSearch.create(); + sc.setParameters("itemType", itemType); + sc.setParameters("itemId", itemId); + expunge(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..ec28bd2ef36c --- /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 + * (CUSTOM_SCRIPT 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..0b35cf2331d0 --- /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)); + } + + @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/InstanceBootGroupReadinessCheckResultVO.java b/engine/schema/src/main/java/org/apache/cloudstack/vm/bootgroup/InstanceBootGroupReadinessCheckResultVO.java new file mode 100644 index 000000000000..8e08279a2394 --- /dev/null +++ b/engine/schema/src/main/java/org/apache/cloudstack/vm/bootgroup/InstanceBootGroupReadinessCheckResultVO.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.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.Id; +import javax.persistence.Table; + +/** + * Last cached evaluation result for a readiness rule, upserted in place — no history, by design. + */ +@Entity +@Table(name = "instance_boot_group_readiness_check_result") +public class InstanceBootGroupReadinessCheckResultVO { + + @Id + @Column(name = "rule_id") + private long ruleId; + + @Column(name = "status") + @Enumerated(EnumType.STRING) + private InstanceBootGroupReadinessRule.Status status; + + @Column(name = "message") + private String message; + + @Column(name = "checked_on") + private Date checkedOn; + + protected InstanceBootGroupReadinessCheckResultVO() { + } + + public InstanceBootGroupReadinessCheckResultVO(long ruleId, InstanceBootGroupReadinessRule.Status status, String message, Date checkedOn) { + this.ruleId = ruleId; + this.status = status; + this.message = message; + this.checkedOn = checkedOn; + } + + public long getRuleId() { + return ruleId; + } + + 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..9e905b4647e1 --- /dev/null +++ b/engine/schema/src/main/java/org/apache/cloudstack/vm/bootgroup/InstanceBootGroupReadinessRuleDetailsVO.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; + +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 CUSTOM_SCRIPT 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 ruleId; + + @Column(name = "name") + private String name; + + @Column(name = "value") + private String value; + + protected InstanceBootGroupReadinessRuleDetailsVO() { + } + + public InstanceBootGroupReadinessRuleDetailsVO(long ruleId, String name, String value) { + this.ruleId = ruleId; + this.name = name; + this.value = value; + } + + @Override + public long getId() { + return id; + } + + @Override + public long getResourceId() { + return ruleId; + } + + @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 true; + } +} 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..a8974c2cdb51 --- /dev/null +++ b/engine/schema/src/main/java/org/apache/cloudstack/vm/bootgroup/InstanceBootGroupReadinessRuleVO.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.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 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; + } +} 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 8f941c4d3384..27077b120e8a 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 @@ -110,6 +110,9 @@ + + + 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 0aaba4e251bf..f4c53ff95129 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 @@ -605,3 +605,42 @@ CREATE TABLE IF NOT EXISTS `cloud`.`instance_boot_group_member` ( 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, + 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) +CREATE TABLE IF NOT EXISTS `cloud`.`instance_boot_group_readiness_check_result` ( + `rule_id` bigint unsigned NOT NULL, + `status` varchar(32) NOT NULL DEFAULT 'UNKNOWN', + `message` varchar(4096) DEFAULT NULL, + `checked_on` datetime DEFAULT NULL, + PRIMARY KEY (`rule_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; diff --git a/server/src/main/java/com/cloud/vm/UserVmManagerImpl.java b/server/src/main/java/com/cloud/vm/UserVmManagerImpl.java index d4e7e0ced2b4..ae717bd813f7 100644 --- a/server/src/main/java/com/cloud/vm/UserVmManagerImpl.java +++ b/server/src/main/java/com/cloud/vm/UserVmManagerImpl.java @@ -159,6 +159,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; @@ -647,6 +648,8 @@ public class UserVmManagerImpl extends ManagerBase implements UserVmManager, Vir @Inject private AutoScaleManager autoScaleManager; @Inject + private InstanceBootGroupMembershipGuard instanceBootGroupMembershipGuard; + @Inject NsxProviderDao nsxProviderDao; @Inject NetworkService networkService; @@ -3870,6 +3873,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/vm/bootgroup/InstanceBootGroupApiServiceImpl.java b/server/src/main/java/org/apache/cloudstack/vm/bootgroup/InstanceBootGroupApiServiceImpl.java new file mode 100644 index 000000000000..d720d8977ee8 --- /dev/null +++ b/server/src/main/java/org/apache/cloudstack/vm/bootgroup/InstanceBootGroupApiServiceImpl.java @@ -0,0 +1,570 @@ +// 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.Comparator; +import java.util.List; +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.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.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.uservm.UserVm; +import com.cloud.user.Account; +import com.cloud.user.AccountManager; +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.vm.InstanceGroupVO; +import com.cloud.vm.UserVmVO; +import com.cloud.vm.dao.InstanceBootGroupDao; +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.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; + + @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); + 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"); + } + + InstanceBootGroupVO group = new InstanceBootGroupVO(cmd.getName(), cmd.getDescription(), owner.getId(), owner.getDomainId()); + group = instanceBootGroupDao.persist(group); + CallContext.current().setEventResourceId(group.getId()); + 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()); + 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) { + 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()); + } + + instanceBootGroupDao.update(group.getId(), group); + return instanceBootGroupDao.findById(group.getId()); + } + + @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; + + if (cmd.getVirtualMachineId() != null && cmd.getInstanceGroupId() != null) { + throw new InvalidParameterValueException("Only one of virtualmachineid or instancegroupid may be specified"); + } + if (cmd.getVirtualMachineId() == null && cmd.getInstanceGroupId() == null) { + throw new InvalidParameterValueException("Either virtualmachineid or instancegroupid must be specified"); + } + + if (cmd.getVirtualMachineId() != null) { + UserVm vm = userVmDao.findById(cmd.getVirtualMachineId()); + if (vm == null) { + throw new InvalidParameterValueException("Unable to find virtual machine with ID: " + cmd.getVirtualMachineId()); + } + validateMemberAccount(vm.getAccountId(), group.getAccountId()); + instanceBootGroupMembershipGuard.validateVmEligibleForGroupMembership(vm.getId()); + memberType = InstanceBootGroupMember.MemberType.VirtualMachine; + memberId = vm.getId(); + } else { + InstanceGroupVO instanceGroup = instanceGroupDao.findById(cmd.getInstanceGroupId()); + if (instanceGroup == null || instanceGroup.getRemoved() != null) { + throw new InvalidParameterValueException("Unable to find instance group with ID: " + cmd.getInstanceGroupId()); + } + validateMemberAccount(instanceGroup.getAccountId(), group.getAccountId()); + instanceBootGroupMembershipGuard.validateInstanceGroupEligibleForBootGroupMembership(instanceGroup.getId()); + 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())); + } + + InstanceBootGroupMemberVO member = new InstanceBootGroupMemberVO(group.getId(), memberType, memberId, cmd.getOrder()); + return instanceBootGroupMemberDao.persist(member); + } + + @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()); + } + if (cmd.getOrder() < 0) { + throw new InvalidParameterValueException("Order value must be 0 or greater"); + } + getGroupAndCheckAccess(member.getBootGroupId()); + member.setOrder(cmd.getOrder()); + instanceBootGroupMemberDao.update(member.getId(), member); + return instanceBootGroupMemberDao.findById(member.getId()); + } + + @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)); + ListResponse response = new ListResponse<>(); + response.setResponses(members.stream().map(this::createInstanceBootGroupMemberResponse).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); + 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); + return group; + } + + @Override + public InstanceBootGroupResponse createInstanceBootGroupResponse(long id) { + return createInstanceBootGroupResponse(instanceBootGroupJoinDao.findById(id)); + } + + @Override + public InstanceBootGroupMemberResponse createInstanceBootGroupMemberResponse(InstanceBootGroupMember member) { + 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()); + 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()); + } + } + response.setObjectName("instancebootgroupmember"); + return response; + } + + 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) { + 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"); + } + + 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()); + } + + @Override + public ListResponse listInstanceBootGroupReadinessRules(ListInstanceBootGroupReadinessRulesCmd cmd) { + getGroupAndCheckAccess(cmd.getBootGroupId()); + + SearchBuilder sb = instanceBootGroupReadinessRuleDao.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", cmd.getBootGroupId()); + if (cmd.getId() != null) { + sc.setParameters("id", cmd.getId()); + } + if (cmd.getVirtualMachineId() != null && cmd.getInstanceGroupId() != null) { + throw new InvalidParameterValueException("Only one of virtualmachineid or instancegroupid may be specified"); + } + if (cmd.getVirtualMachineId() != null) { + sc.setParameters("itemType", InstanceBootGroupMember.MemberType.VirtualMachine); + sc.setParameters("itemId", cmd.getVirtualMachineId()); + } else if (cmd.getInstanceGroupId() != null) { + sc.setParameters("itemType", InstanceBootGroupMember.MemberType.InstanceGroup); + sc.setParameters("itemId", cmd.getInstanceGroupId()); + } + if (cmd.getRuleType() != null) { + InstanceBootGroupReadinessRule.RuleType ruleType = EnumUtils.getEnumIgnoreCase(InstanceBootGroupReadinessRule.RuleType.class, cmd.getRuleType()); + if (ruleType == null) { + throw new InvalidParameterValueException("Invalid rule type: " + cmd.getRuleType()); + } + sc.setParameters("ruleType", ruleType); + } + if (cmd.getKeyword() != null) { + sc.setParameters("keyword", "%" + cmd.getKeyword() + "%"); + } + + Filter searchFilter = new Filter(InstanceBootGroupReadinessRuleVO.class, "id", true, cmd.getStartIndex(), cmd.getPageSizeVal()); + Pair, Integer> rulesAndCount = instanceBootGroupReadinessRuleDao.searchAndCount(sc, searchFilter); + + List responsesList = rulesAndCount.first().stream() + .map(this::createInstanceBootGroupReadinessRuleResponse) + .collect(Collectors.toList()); + ListResponse response = new ListResponse<>(); + response.setResponses(responsesList, rulesAndCount.second()); + return response; + } + + @Override + public InstanceBootGroupReadinessRuleResponse createInstanceBootGroupReadinessRuleResponse(InstanceBootGroupReadinessRule rule) { + 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())); + + 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.findByRuleId(rule.getId()); + 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 index 4ab708304034..fac5f3409b16 100644 --- a/server/src/main/java/org/apache/cloudstack/vm/bootgroup/InstanceBootGroupManager.java +++ b/server/src/main/java/org/apache/cloudstack/vm/bootgroup/InstanceBootGroupManager.java @@ -19,5 +19,17 @@ import com.cloud.utils.component.Manager; -public interface InstanceBootGroupManager extends InstanceBootGroupService, 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); + + void rebootInstanceBootGroup(InstanceBootGroupVO group); } 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 index a882b7e17b2f..9ede597daf6c 100644 --- a/server/src/main/java/org/apache/cloudstack/vm/bootgroup/InstanceBootGroupManagerImpl.java +++ b/server/src/main/java/org/apache/cloudstack/vm/bootgroup/InstanceBootGroupManagerImpl.java @@ -26,303 +26,55 @@ 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.command.user.bootgroup.AddMemberToInstanceBootGroupCmd; -import org.apache.cloudstack.api.command.user.bootgroup.CreateInstanceBootGroupCmd; -import org.apache.cloudstack.api.command.user.bootgroup.DeleteInstanceBootGroupCmd; -import org.apache.cloudstack.api.command.user.bootgroup.ListInstanceBootGroupMembersCmd; -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.query.dao.InstanceBootGroupJoinDao; -import org.apache.cloudstack.api.query.vo.InstanceBootGroupJoinVO; -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.context.CallContext; -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.ManagerBase; -import com.cloud.utils.component.PluggableService; import com.cloud.utils.concurrency.NamedThreadFactory; -import com.cloud.utils.db.Filter; -import com.cloud.utils.db.SearchBuilder; -import com.cloud.utils.db.SearchCriteria; import com.cloud.utils.exception.CloudRuntimeException; -import com.cloud.vm.InstanceGroupVO; import com.cloud.vm.UserVmService; import com.cloud.vm.UserVmVO; -import com.cloud.vm.dao.InstanceBootGroupDao; 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 and hypervisor + * start/stop calls only. API-cmd handling (ACL, validation, response building, command registration) + * lives in {@code InstanceBootGroupApiServiceImpl}, which delegates here with resolved domain objects. + */ @Component -public class InstanceBootGroupManagerImpl extends ManagerBase implements InstanceBootGroupManager, PluggableService { - - @Inject - private InstanceBootGroupDao instanceBootGroupDao; - - @Inject - private InstanceBootGroupJoinDao instanceBootGroupJoinDao; +public class InstanceBootGroupManagerImpl extends ManagerBase implements InstanceBootGroupManager { @Inject private InstanceBootGroupMemberDao instanceBootGroupMemberDao; - @Inject - private AccountManager accountManager; - @Inject private UserVmService userVmService; @Inject private UserVmDao userVmDao; - @Inject - private InstanceGroupDao instanceGroupDao; - @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); - response.setObjectName("instancebootgroup"); - return response; - } - @Override public boolean configure(String name, Map params) throws ConfigurationException { return true; } @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"); - } - - InstanceBootGroupVO group = new InstanceBootGroupVO(cmd.getName(), cmd.getDescription(), owner.getId(), owner.getDomainId()); - group = instanceBootGroupDao.persist(group); - CallContext.current().setEventResourceId(group.getId()); - 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()); - 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) { - 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()); - } - - instanceBootGroupDao.update(group.getId(), group); - return instanceBootGroupDao.findById(group.getId()); - } - - @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 webhook : bootGroupsAndCount.first()) { - InstanceBootGroupResponse response = createInstanceBootGroupResponse(webhook); - 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; - - if (cmd.getVirtualMachineId() != null && cmd.getInstanceGroupId() != null) { - throw new InvalidParameterValueException("Only one of virtualmachineid or instancegroupid may be specified"); - } - if (cmd.getVirtualMachineId() == null && cmd.getInstanceGroupId() == null) { - throw new InvalidParameterValueException("Either virtualmachineid or instancegroupid must be specified"); - } - - if (cmd.getVirtualMachineId() != null) { - UserVm vm = userVmDao.findById(cmd.getVirtualMachineId()); - if (vm == null) { - throw new InvalidParameterValueException("Unable to find virtual machine with ID: " + cmd.getVirtualMachineId()); - } - validateMemberAccount(vm.getAccountId(), group.getAccountId()); - memberType = InstanceBootGroupMember.MemberType.VirtualMachine; - memberId = vm.getId(); - } else { - InstanceGroupVO instanceGroup = instanceGroupDao.findById(cmd.getInstanceGroupId()); - if (instanceGroup == null || instanceGroup.getRemoved() != null) { - throw new InvalidParameterValueException("Unable to find instance group with ID: " + cmd.getInstanceGroupId()); - } - validateMemberAccount(instanceGroup.getAccountId(), group.getAccountId()); - 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())); - } - - InstanceBootGroupMemberVO member = new InstanceBootGroupMemberVO(group.getId(), memberType, memberId, cmd.getOrder()); - return instanceBootGroupMemberDao.persist(member); - } - - @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()); - } - if (cmd.getOrder() < 0) { - throw new InvalidParameterValueException("Order value must be 0 or greater"); - } - getGroupAndCheckAccess(member.getBootGroupId()); - member.setOrder(cmd.getOrder()); - instanceBootGroupMemberDao.update(member.getId(), member); - return instanceBootGroupMemberDao.findById(member.getId()); - } - - @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)); - ListResponse response = new ListResponse<>(); - response.setResponses(members.stream().map(this::createInstanceBootGroupMemberResponse).collect(Collectors.toList()), result.second()); - return response; - } - - protected void startInstanceBootGroupInternal(InstanceBootGroupVO group) { + public void startInstanceBootGroup(InstanceBootGroupVO group) { List members = instanceBootGroupMemberDao.listByBootGroupId(group.getId()); Map> tiers = groupByOrder(members); for (Map.Entry> tier : tiers.entrySet()) { List vmIds = resolveVmIds(tier.getValue()); runTierConcurrently(vmIds, group, "start", vmId -> { - UserVm vm = userVmDao.findById(vmId); + UserVmVO vm = userVmDao.findById(vmId); if (vm != null && vm.getState() != com.cloud.vm.VirtualMachine.State.Running) { userVmService.startVirtualMachine(vm, null); } @@ -331,22 +83,14 @@ protected void startInstanceBootGroupInternal(InstanceBootGroupVO group) { } @Override - @ActionEvent(eventType = EventTypes.EVENT_INSTANCE_BOOT_GROUP_START, eventDescription = "starting Instance Boot Group", async = true) - public InstanceBootGroup startInstanceBootGroup(final StartInstanceBootGroupCmd cmd) { - final long id = cmd.getId(); - InstanceBootGroupVO group = getGroupAndCheckAccess(id); - startInstanceBootGroupInternal(group); - return group; - } - - protected void stopInstanceBootGroupInternal(InstanceBootGroupVO group) { + public void stopInstanceBootGroup(InstanceBootGroupVO group) { List members = instanceBootGroupMemberDao.listByBootGroupId(group.getId()); Map> tiers = groupByOrderDescending(members); for (Map.Entry> tier : tiers.entrySet()) { List vmIds = resolveVmIds(tier.getValue()); runTierConcurrently(vmIds, group, "stop", vmId -> { - UserVm vm = userVmDao.findById(vmId); + UserVmVO vm = userVmDao.findById(vmId); if (vm != null && vm.getState() != com.cloud.vm.VirtualMachine.State.Stopped) { userVmService.stopVirtualMachine(vmId, false); } @@ -355,12 +99,9 @@ protected void stopInstanceBootGroupInternal(InstanceBootGroupVO group) { } @Override - @ActionEvent(eventType = EventTypes.EVENT_INSTANCE_BOOT_GROUP_STOP, eventDescription = "stopping Instance Boot Group", async = true) - public InstanceBootGroup stopInstanceBootGroup(final StopInstanceBootGroupCmd cmd) { - final long id = cmd.getId(); - InstanceBootGroupVO group = getGroupAndCheckAccess(id); - stopInstanceBootGroupInternal(group); - return group; + public void rebootInstanceBootGroup(InstanceBootGroupVO group) { + stopInstanceBootGroup(group); + startInstanceBootGroup(group); } /** @@ -398,56 +139,6 @@ private void runTierConcurrently(List vmIds, InstanceBootGroupVO group, St } } - @Override - @ActionEvent(eventType = EventTypes.EVENT_INSTANCE_BOOT_GROUP_REBOOT, eventDescription = "rebooting Instance Boot Group", async = true) - public InstanceBootGroup rebootInstanceBootGroup(final RebootInstanceBootGroupCmd cmd) { - final long id = cmd.getId(); - InstanceBootGroupVO group = getGroupAndCheckAccess(id); - stopInstanceBootGroupInternal(group); - startInstanceBootGroupInternal(group); - return group; - } - - @Override - public InstanceBootGroupResponse createInstanceBootGroupResponse(long id) { - return createInstanceBootGroupResponse(instanceBootGroupJoinDao.findById(id)); - } - - @Override - public InstanceBootGroupMemberResponse createInstanceBootGroupMemberResponse(InstanceBootGroupMember member) { - 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()); - if (member.getMemberType() == org.apache.cloudstack.vm.bootgroup.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 { - com.cloud.vm.InstanceGroupVO instanceGroup = instanceGroupDao.findById(member.getMemberId()); - if (instanceGroup != null) { - response.setMemberId(instanceGroup.getUuid()); - response.setMemberName(instanceGroup.getName()); - } - } - response.setObjectName("instancebootgroupmember"); - return response; - } - - private void validateMemberAccount(long memberAccountId, long groupAccountId) { - if (memberAccountId != groupAccountId) { - throw new PermissionDeniedException("Member must belong to the same account as the boot group"); - } - } - private Map> groupByOrder(List members) { Map> tiers = new TreeMap<>(); for (InstanceBootGroupMemberVO m : members) { @@ -481,26 +172,4 @@ private List resolveVmIds(List tierMembers) { private interface VmAction { void run(long vmId) throws Exception; } - - @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 - ); - } - - @Override - public String getName() { - return InstanceBootGroupMember.class.getSimpleName(); - } } 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..079c15fbc47b --- /dev/null +++ b/server/src/main/java/org/apache/cloudstack/vm/bootgroup/InstanceBootGroupMembershipGuard.java @@ -0,0 +1,112 @@ +// 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)); + } + + List currentMappings = instanceGroupVMMapDao.listByInstanceId(vmId); + if (CollectionUtils.isNotEmpty(currentMappings)) { + long currentGroupId = currentMappings.get(0).getGroupId(); + if (instanceBootGroupMemberDao.findByMember(InstanceBootGroupMember.MemberType.InstanceGroup, currentGroupId) != 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/InstanceBootGroupReadinessRuleManagerImpl.java b/server/src/main/java/org/apache/cloudstack/vm/bootgroup/InstanceBootGroupReadinessRuleManagerImpl.java new file mode 100644 index 000000000000..38453d79453c --- /dev/null +++ b/server/src/main/java/org/apache/cloudstack/vm/bootgroup/InstanceBootGroupReadinessRuleManagerImpl.java @@ -0,0 +1,225 @@ +// 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.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 javax.annotation.PostConstruct; +import javax.inject.Inject; + +import org.apache.commons.lang3.StringUtils; +import org.springframework.stereotype.Component; + +import com.cloud.exception.InvalidParameterValueException; +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.InstanceGroupVMMapDao; +import com.cloud.vm.dao.UserVmDao; + +import org.apache.cloudstack.vm.bootgroup.readiness.checker.ReadinessChecker; + +@Component +public class InstanceBootGroupReadinessRuleManagerImpl implements InstanceBootGroupReadinessRuleService { + + private static final Map> VALID_RULE_TYPES_BY_ITEM_TYPE = Map.of( + InstanceBootGroupMember.MemberType.VirtualMachine, EnumSet.of( + InstanceBootGroupReadinessRule.RuleType.GUEST_AGENT_LIVENESS, + InstanceBootGroupReadinessRule.RuleType.VR_PING, + InstanceBootGroupReadinessRule.RuleType.PORT_CHECK, + InstanceBootGroupReadinessRule.RuleType.CUSTOM_SCRIPT), + InstanceBootGroupMember.MemberType.InstanceGroup, EnumSet.of( + InstanceBootGroupReadinessRule.RuleType.CUSTOM_SCRIPT, + InstanceBootGroupReadinessRule.RuleType.INSTANCE_QUORUM)); + + @Inject + private InstanceBootGroupReadinessRuleDao instanceBootGroupReadinessRuleDao; + + @Inject + private InstanceBootGroupReadinessRuleDetailsDao instanceBootGroupReadinessRuleDetailsDao; + + @Inject + private InstanceBootGroupReadinessCheckResultDao instanceBootGroupReadinessCheckResultDao; + + @Inject + private InstanceBootGroupMemberDao instanceBootGroupMemberDao; + + @Inject + private InstanceGroupVMMapDao instanceGroupVMMapDao; + + @Inject + private UserVmDao userVmDao; + + @Inject + private List readinessCheckers; + + private Map checkersByRuleType; + + @PostConstruct + public void init() { + checkersByRuleType = new HashMap<>(); + for (ReadinessChecker checker : readinessCheckers) { + checkersByRuleType.put(checker.getRuleType(), checker); + } + } + + @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); + + 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); + } + 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); + } + 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) { + List rules = instanceBootGroupReadinessRuleDao.listEnabledByItem(bootGroupId, InstanceBootGroupMember.MemberType.VirtualMachine, vmId); + + if (rules.isEmpty()) { + UserVmVO vm = userVmDao.findById(vmId); + boolean running = vm != null && vm.getState() == VirtualMachine.State.Running; + return running ? InstanceBootGroupReadinessRule.Status.READY : InstanceBootGroupReadinessRule.Status.NOT_READY; + } + + boolean anyError = false; + boolean anyNotReady = false; + for (InstanceBootGroupReadinessRuleVO rule : rules) { + ReadinessChecker checker = checkersByRuleType.get(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); + status = result.getStatus(); + message = result.getMessage(); + } + instanceBootGroupReadinessCheckResultDao.upsert(rule.getId(), status, message, new Date()); + + if (status == InstanceBootGroupReadinessRule.Status.ERROR) { + anyError = true; + } else if (status != InstanceBootGroupReadinessRule.Status.READY) { + anyNotReady = true; + } + } + + if (anyError) { + return InstanceBootGroupReadinessRule.Status.ERROR; + } + return anyNotReady ? InstanceBootGroupReadinessRule.Status.NOT_READY : InstanceBootGroupReadinessRule.Status.READY; + } + + 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/InstanceBootGroupReadinessRuleService.java b/server/src/main/java/org/apache/cloudstack/vm/bootgroup/InstanceBootGroupReadinessRuleService.java new file mode 100644 index 000000000000..31fbe915444e --- /dev/null +++ b/server/src/main/java/org/apache/cloudstack/vm/bootgroup/InstanceBootGroupReadinessRuleService.java @@ -0,0 +1,46 @@ +// Licensed to the Apache Software Foundation (ASF) under one +// or more contributor license agreements. See the NOTICE file +// distributed with this work for additional information +// regarding copyright ownership. The ASF licenses this file +// to you under the Apache License, Version 2.0 (the +// "License"); you may not use this file except in compliance +// with the License. You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, +// software distributed under the License is distributed on an +// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +// KIND, either express or implied. See the License for the +// specific language governing permissions and limitations +// under the License. + +package org.apache.cloudstack.vm.bootgroup; + +import java.util.Map; + +/** + * 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 where {@code item_type = VirtualMachine, item_id = vmId} within + * this boot group. Zero rules attached -> READY iff the VM's power state is Running. + */ + InstanceBootGroupReadinessRule.Status evaluateVmReadiness(long bootGroupId, long vmId); +} diff --git a/server/src/main/java/org/apache/cloudstack/vm/bootgroup/readiness/checker/ReadinessChecker.java b/server/src/main/java/org/apache/cloudstack/vm/bootgroup/readiness/checker/ReadinessChecker.java new file mode 100644 index 000000000000..cf8574467d26 --- /dev/null +++ b/server/src/main/java/org/apache/cloudstack/vm/bootgroup/readiness/checker/ReadinessChecker.java @@ -0,0 +1,59 @@ +// 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.checker; + +import java.util.Map; + +import org.apache.cloudstack.vm.bootgroup.InstanceBootGroupReadinessRule; +import org.apache.cloudstack.vm.bootgroup.InstanceBootGroupReadinessRuleVO; + +/** + * 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 { + + InstanceBootGroupReadinessRule.RuleType getRuleType(); + + /** + * @param rule the rule being evaluated + * @param details the rule's decrypted detail key/value pairs + * @param vmId the VM to check readiness for + */ + Result check(InstanceBootGroupReadinessRuleVO rule, Map details, long vmId); + + 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; + } + } +} diff --git a/server/src/main/java/org/apache/cloudstack/vm/bootgroup/readiness/checker/VrPingChecker.java b/server/src/main/java/org/apache/cloudstack/vm/bootgroup/readiness/checker/VrPingChecker.java new file mode 100644 index 000000000000..b0d072af99dd --- /dev/null +++ b/server/src/main/java/org/apache/cloudstack/vm/bootgroup/readiness/checker/VrPingChecker.java @@ -0,0 +1,136 @@ +// 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.checker; + +import java.util.List; +import java.util.Map; + +import javax.inject.Inject; + +import org.apache.commons.lang3.StringUtils; +import org.springframework.stereotype.Component; + +import org.apache.cloudstack.api.ApiConstants; +import org.apache.cloudstack.diagnostics.DiagnosticsAnswer; +import org.apache.cloudstack.diagnostics.DiagnosticsCommand; +import org.apache.cloudstack.diagnostics.DiagnosticsType; +import org.apache.cloudstack.engine.orchestration.service.NetworkOrchestrationService; +import org.apache.cloudstack.vm.bootgroup.InstanceBootGroupReadinessRule; +import org.apache.cloudstack.vm.bootgroup.InstanceBootGroupReadinessRuleVO; + +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.VirtualNetworkApplianceManager; +import com.cloud.network.router.VirtualRouter; +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; + +/** + * VR_PING: reuses the existing diagnostics plumbing ({@code DiagnosticsCommand}/{@code DiagnosticsType.PING}), + * the same path {@code RunDiagnosticsCmd}/{@code DiagnosticsServiceImpl} use, dispatched to the VR of + * the VM's default network. + */ +@Component +public class VrPingChecker implements ReadinessChecker { + + @Inject + private UserVmDao userVmDao; + + @Inject + private NicDao nicDao; + + @Inject + private NetworkDao networkDao; + + @Inject + private VirtualNetworkApplianceManager virtualNetworkApplianceManager; + + @Inject + private VirtualMachineManager virtualMachineManager; + + @Inject + private NetworkOrchestrationService networkOrchestrationService; + + @Inject + private AgentManager agentManager; + + @Override + public InstanceBootGroupReadinessRule.RuleType getRuleType() { + return InstanceBootGroupReadinessRule.RuleType.VR_PING; + } + + @Override + public Result check(InstanceBootGroupReadinessRuleVO rule, Map details, long vmId) { + UserVmVO vm = userVmDao.findById(vmId); + if (vm == null) { + return new Result(InstanceBootGroupReadinessRule.Status.ERROR, "VM not found"); + } + + NicVO nic = nicDao.findDefaultNicForVM(vmId); + if (nic == null || StringUtils.isEmpty(nic.getIPv4Address())) { + return new Result(InstanceBootGroupReadinessRule.Status.NOT_READY, "VM has no default NIC/IPv4 address yet"); + } + + NetworkVO network = networkDao.findById(nic.getNetworkId()); + if (network != null && Network.GuestType.L2.equals(network.getGuestType())) { + return 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 new Result(InstanceBootGroupReadinessRule.Status.NOT_READY, "No running VR found for the VM's default network"); + } + + String shellCmd = DiagnosticsType.PING.getValue() + " " + nic.getIPv4Address(); + DiagnosticsCommand command = new DiagnosticsCommand(shellCmd, virtualMachineManager.getExecuteInSequence(router.getHypervisorType())); + Map accessDetails = networkOrchestrationService.getSystemVMAccessDetails(router); + if (StringUtils.isEmpty(accessDetails.get(NetworkElementCommand.ROUTER_IP))) { + return new Result(InstanceBootGroupReadinessRule.Status.ERROR, "Unable to determine the VR's control IP"); + } + command.setAccessDetail(accessDetails); + + Answer answer; + try { + answer = agentManager.easySend(router.getHostId(), command); + } catch (Exception e) { + return new Result(InstanceBootGroupReadinessRule.Status.ERROR, "Failed to dispatch ping via VR: " + e.getMessage()); + } + if (answer == null) { + return new Result(InstanceBootGroupReadinessRule.Status.ERROR, "No answer from the VR's host"); + } + + Map executionDetails = ((DiagnosticsAnswer) answer).getExecutionDetails(); + String exitCode = executionDetails.get(ApiConstants.EXITCODE); + if ("0".equals(exitCode)) { + return new Result(InstanceBootGroupReadinessRule.Status.READY, "ping succeeded"); + } + return new Result(InstanceBootGroupReadinessRule.Status.NOT_READY, "ping failed: " + executionDetails.get(ApiConstants.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 04412e6e7cd6..784223727475 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 @@ -396,6 +396,9 @@ + + + From ac3f7e8390035d8960c5cea0d052a5733ddf36ab Mon Sep 17 00:00:00 2001 From: Abhishek Kumar Date: Tue, 14 Jul 2026 13:47:03 +0530 Subject: [PATCH 3/9] instancegroup readiness/evaluation Signed-off-by: Abhishek Kumar --- ...anceBootGroupReadinessRuleManagerImpl.java | 110 ++++++++++++++++++ ...InstanceBootGroupReadinessRuleService.java | 8 ++ 2 files changed, 118 insertions(+) diff --git a/server/src/main/java/org/apache/cloudstack/vm/bootgroup/InstanceBootGroupReadinessRuleManagerImpl.java b/server/src/main/java/org/apache/cloudstack/vm/bootgroup/InstanceBootGroupReadinessRuleManagerImpl.java index 38453d79453c..961b1b23381f 100644 --- a/server/src/main/java/org/apache/cloudstack/vm/bootgroup/InstanceBootGroupReadinessRuleManagerImpl.java +++ b/server/src/main/java/org/apache/cloudstack/vm/bootgroup/InstanceBootGroupReadinessRuleManagerImpl.java @@ -47,6 +47,9 @@ @Component public class InstanceBootGroupReadinessRuleManagerImpl implements InstanceBootGroupReadinessRuleService { + private static final String THRESHOLD_TYPE_KEY = "threshold_type"; + private static final String THRESHOLD_VALUE_KEY = "threshold_value"; + private static final Map> VALID_RULE_TYPES_BY_ITEM_TYPE = Map.of( InstanceBootGroupMember.MemberType.VirtualMachine, EnumSet.of( InstanceBootGroupReadinessRule.RuleType.GUEST_AGENT_LIVENESS, @@ -93,6 +96,7 @@ public InstanceBootGroupReadinessRule createReadinessRule(long bootGroupId, Inst InstanceBootGroupReadinessRule.RuleType ruleType, String name, boolean enabled, Map details) { validateRuleTypeForItemType(itemType, ruleType); validateItemBelongsToBootGroup(bootGroupId, itemType, itemId); + 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); @@ -119,6 +123,11 @@ public InstanceBootGroupReadinessRule updateReadinessRule(long ruleId, String na 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) { @@ -191,6 +200,107 @@ public InstanceBootGroupReadinessRule.Status evaluateVmReadiness(long bootGroupI return anyNotReady ? InstanceBootGroupReadinessRule.Status.NOT_READY : InstanceBootGroupReadinessRule.Status.READY; } + @Override + public InstanceBootGroupReadinessRule.Status evaluateInstanceGroupReadiness(long bootGroupId, long instanceGroupId) { + List groupRules = instanceBootGroupReadinessRuleDao.listEnabledByItem(bootGroupId, InstanceBootGroupMember.MemberType.InstanceGroup, instanceGroupId); + + boolean anyError = false; + boolean anyNotReady = false; + + for (InstanceBootGroupReadinessRuleVO rule : groupRules) { + ReadinessChecker.Result result; + if (rule.getRuleType() == InstanceBootGroupReadinessRule.RuleType.INSTANCE_QUORUM) { + Map details = instanceBootGroupReadinessRuleDetailsDao.getDetails(rule.getId()); + result = evaluateInstanceQuorum(bootGroupId, instanceGroupId, details); + } else { + // e.g. group-scope CUSTOM_SCRIPT — no evaluator implemented yet, ships later + result = new ReadinessChecker.Result(InstanceBootGroupReadinessRule.Status.ERROR, + "No evaluator implemented yet for rule type " + rule.getRuleType()); + } + instanceBootGroupReadinessCheckResultDao.upsert(rule.getId(), result.getStatus(), result.getMessage(), new Date()); + + if (result.getStatus() == InstanceBootGroupReadinessRule.Status.ERROR) { + anyError = true; + } else if (result.getStatus() != InstanceBootGroupReadinessRule.Status.READY) { + anyNotReady = true; + } + } + + List members = instanceGroupVMMapDao.listByGroupId(instanceGroupId); + for (InstanceGroupVMMapVO member : members) { + InstanceBootGroupReadinessRule.Status vmStatus = evaluateVmReadiness(bootGroupId, member.getInstanceId()); + if (vmStatus == InstanceBootGroupReadinessRule.Status.ERROR) { + anyError = true; + } else if (vmStatus != InstanceBootGroupReadinessRule.Status.READY) { + anyNotReady = true; + } + } + + if (anyError) { + return InstanceBootGroupReadinessRule.Status.ERROR; + } + return anyNotReady ? InstanceBootGroupReadinessRule.Status.NOT_READY : InstanceBootGroupReadinessRule.Status.READY; + } + + /** + * Pure computation, no remote execution: counts how many of the group's member VMs currently + * evaluate READY (via their own rules) and compares against the configured threshold. + */ + private ReadinessChecker.Result evaluateInstanceQuorum(long bootGroupId, long instanceGroupId, Map details) { + List members = instanceGroupVMMapDao.listByGroupId(instanceGroupId); + int total = members.size(); + if (total == 0) { + return new ReadinessChecker.Result(InstanceBootGroupReadinessRule.Status.NOT_READY, "Instance group has no members"); + } + + long readyCount = members.stream() + .filter(member -> evaluateVmReadiness(bootGroupId, member.getInstanceId()) == InstanceBootGroupReadinessRule.Status.READY) + .count(); + + String thresholdType = details == null ? null : details.get(THRESHOLD_TYPE_KEY); + String thresholdValue = details == null ? null : details.get(THRESHOLD_VALUE_KEY); + + boolean met; + try { + if ("PERCENTAGE".equalsIgnoreCase(thresholdType)) { + double thresholdPercentage = Double.parseDouble(thresholdValue); + met = (readyCount * 100.0 / total) >= thresholdPercentage; + } else { + long thresholdCount = Long.parseLong(thresholdValue); + met = readyCount >= 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); + return new ReadinessChecker.Result(met ? InstanceBootGroupReadinessRule.Status.READY : InstanceBootGroupReadinessRule.Status.NOT_READY, message); + } + + private void validateRuleTypeSpecificDetails(InstanceBootGroupReadinessRule.RuleType ruleType, Map details) { + if (ruleType != InstanceBootGroupReadinessRule.RuleType.INSTANCE_QUORUM) { + return; + } + 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( + "INSTANCE_QUORUM rules require '%s' (COUNT or PERCENTAGE) and '%s' details", 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 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)); diff --git a/server/src/main/java/org/apache/cloudstack/vm/bootgroup/InstanceBootGroupReadinessRuleService.java b/server/src/main/java/org/apache/cloudstack/vm/bootgroup/InstanceBootGroupReadinessRuleService.java index 31fbe915444e..7efce1f56cf9 100644 --- a/server/src/main/java/org/apache/cloudstack/vm/bootgroup/InstanceBootGroupReadinessRuleService.java +++ b/server/src/main/java/org/apache/cloudstack/vm/bootgroup/InstanceBootGroupReadinessRuleService.java @@ -43,4 +43,12 @@ InstanceBootGroupReadinessRule createReadinessRule(long bootGroupId, InstanceBoo * this boot group. Zero rules attached -> READY iff the VM's power state is Running. */ InstanceBootGroupReadinessRule.Status evaluateVmReadiness(long bootGroupId, long vmId); + + /** + * AND of: all enabled rules where {@code item_type = InstanceGroup, item_id = instanceGroupId} + * (currently only INSTANCE_QUORUM has an evaluator; group-scope CUSTOM_SCRIPT ships later) AND + * every VM currently in the group, each evaluated via {@link #evaluateVmReadiness(long, long)} — + * a VM's own rules, if it has any, are fully honored, not reduced to a proxy. + */ + InstanceBootGroupReadinessRule.Status evaluateInstanceGroupReadiness(long bootGroupId, long instanceGroupId); } From 172964134bddeae2d9da3e7f7736b2b27b0ea48c Mon Sep 17 00:00:00 2001 From: Abhishek Kumar Date: Tue, 14 Jul 2026 17:53:16 +0530 Subject: [PATCH 4/9] wip Signed-off-by: Abhishek Kumar --- .../apache/cloudstack/api/ApiConstants.java | 2 + .../ListInstanceBootGroupMembersCmd.java | 13 + .../bootgroup/UpdateInstanceBootGroupCmd.java | 16 ++ .../InstanceBootGroupMemberResponse.java | 16 ++ .../response/InstanceBootGroupResponse.java | 16 ++ .../vm/dao/InstanceBootGroupDetailsDao.java | 33 +++ .../dao/InstanceBootGroupDetailsDaoImpl.java | 82 ++++++ .../bootgroup/InstanceBootGroupDetailsVO.java | 77 ++++++ ...spring-engine-schema-core-daos-context.xml | 1 + .../META-INF/db/schema-42210to42300.sql | 11 + .../InstanceBootGroupApiServiceImpl.java | 114 +++++++- .../InstanceBootGroupManagerImpl.java | 216 ++++++++++++++- ui/public/locales/en.json | 13 + .../compute/InstanceBootGroupMembersTab.vue | 70 ++++- .../InstanceBootGroupReadinessRulesModal.vue | 252 ++++++++++++++++++ 15 files changed, 917 insertions(+), 15 deletions(-) create mode 100644 engine/schema/src/main/java/com/cloud/vm/dao/InstanceBootGroupDetailsDao.java create mode 100644 engine/schema/src/main/java/com/cloud/vm/dao/InstanceBootGroupDetailsDaoImpl.java create mode 100644 engine/schema/src/main/java/org/apache/cloudstack/vm/bootgroup/InstanceBootGroupDetailsVO.java create mode 100644 ui/src/views/compute/InstanceBootGroupReadinessRulesModal.vue 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 13b2e7d25b00..bd6239a27f2f 100644 --- a/api/src/main/java/org/apache/cloudstack/api/ApiConstants.java +++ b/api/src/main/java/org/apache/cloudstack/api/ApiConstants.java @@ -101,6 +101,8 @@ public class ApiConstants { public static final String RULE_TYPE = "ruletype"; public static final String READINESS_MODE = "readinessmode"; public static final String READINESS_STATUS = "readinessstatus"; + public static final String READINESS_TIMEOUT_SECONDS = "readinesstimeoutseconds"; + public static final String READINESS_MAX_REBOOT_ATTEMPTS = "readinessmaxrebootattempts"; public static final String CA_CERTIFICATES = "cacertificates"; public static final String CERTIFICATE = "certificate"; public static final String CERTIFICATE_CHAIN = "certchain"; 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 index 7de1a8f50a77..3de04139f3e0 100644 --- 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 @@ -17,6 +17,8 @@ package org.apache.cloudstack.api.command.user.bootgroup; +import java.util.List; + import javax.inject.Inject; import org.apache.cloudstack.acl.RoleType; @@ -49,6 +51,13 @@ public class ListInstanceBootGroupMembersCmd extends BaseListCmd implements User @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]. " + + "Readiness fields are computed from cached check results (not a live re-check) and are omitted unless requested, since computing them is not free.") + private List viewDetails; + public Long getBootGroupId() { return bootGroupId; } @@ -57,6 +66,10 @@ public String getMemberType() { return memberType; } + public boolean isReadinessDetailRequested() { + return viewDetails != null && (viewDetails.contains("readiness") || viewDetails.contains("all")); + } + @Override public void execute() { ListResponse response = instanceBootGroupService.listInstanceBootGroupMembers(this); 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 index 8d1f3499cd96..c4edceec72b7 100644 --- 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 @@ -54,6 +54,14 @@ public class UpdateInstanceBootGroupCmd extends BaseCmd implements UserCmd { @Parameter(name = ApiConstants.DESCRIPTION, type = CommandType.STRING, description = "New description for the instance boot group") private String description; + @Parameter(name = ApiConstants.READINESS_TIMEOUT_SECONDS, type = CommandType.LONG, + description = "Per-boot-group override of the global readiness timeout (seconds) before an instance is rebooted. Pass -1 to clear the override and fall back to the global setting.") + private Long readinessTimeoutSeconds; + + @Parameter(name = ApiConstants.READINESS_MAX_REBOOT_ATTEMPTS, type = CommandType.LONG, + description = "Per-boot-group override of the global maximum readiness reboot attempts. Pass -1 to clear the override and fall back to the global setting.") + private Long readinessMaxRebootAttempts; + public Long getId() { return id; } @@ -66,6 +74,14 @@ public String getDescription() { return description; } + public Long getReadinessTimeoutSeconds() { + return readinessTimeoutSeconds; + } + + public Long getReadinessMaxRebootAttempts() { + return readinessMaxRebootAttempts; + } + @Override public long getEntityOwnerId() { return CallContext.current().getCallingAccount().getId(); 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 index 3bb323b10e0a..79099d23a11d 100644 --- a/api/src/main/java/org/apache/cloudstack/api/response/InstanceBootGroupMemberResponse.java +++ b/api/src/main/java/org/apache/cloudstack/api/response/InstanceBootGroupMemberResponse.java @@ -64,6 +64,14 @@ public class InstanceBootGroupMemberResponse extends BaseResponse { @Param(description = "The date the member was added to the boot group") private Date created; + @SerializedName(ApiConstants.READINESS_MODE) + @Param(description = "NO_READINESS, 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: READY, NOT_READY, ERROR or UNKNOWN") + private String readinessStatus; + public void setId(String id) { this.id = id; } @@ -95,4 +103,12 @@ public void setOrder(int 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; + } } 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 index e7af5c941f21..83bae00545c3 100644 --- a/api/src/main/java/org/apache/cloudstack/api/response/InstanceBootGroupResponse.java +++ b/api/src/main/java/org/apache/cloudstack/api/response/InstanceBootGroupResponse.java @@ -76,6 +76,14 @@ public class InstanceBootGroupResponse extends BaseResponse implements Controlle @Param(description = "The project name of the instance boot group") private String projectName; + @SerializedName(ApiConstants.READINESS_TIMEOUT_SECONDS) + @Param(description = "Effective readiness timeout in seconds (per-boot-group override if set, else the global default)") + private long readinessTimeoutSeconds; + + @SerializedName(ApiConstants.READINESS_MAX_REBOOT_ATTEMPTS) + @Param(description = "Effective maximum readiness reboot attempts (per-boot-group override if set, else the global default)") + private long readinessMaxRebootAttempts; + public void setId(String id) { this.id = id; } @@ -125,4 +133,12 @@ public void setProjectId(String projectId) { public void setProjectName(String projectName) { this.projectName = projectName; } + + public void setReadinessTimeoutSeconds(long readinessTimeoutSeconds) { + this.readinessTimeoutSeconds = readinessTimeoutSeconds; + } + + public void setReadinessMaxRebootAttempts(long readinessMaxRebootAttempts) { + this.readinessMaxRebootAttempts = readinessMaxRebootAttempts; + } } 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..e69e6dcd741e --- /dev/null +++ b/engine/schema/src/main/java/com/cloud/vm/dao/InstanceBootGroupDetailsDao.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 com.cloud.vm.dao; + +import java.util.Map; + +import org.apache.cloudstack.vm.bootgroup.InstanceBootGroupDetailsVO; + +import com.cloud.utils.db.GenericDao; + +public interface InstanceBootGroupDetailsDao extends GenericDao { + + String getDetail(long bootGroupId, String name); + + void setDetail(long bootGroupId, String name, String value); + + Map listDetails(long bootGroupId); +} 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..d25a63412114 --- /dev/null +++ b/engine/schema/src/main/java/com/cloud/vm/dao/InstanceBootGroupDetailsDaoImpl.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 com.cloud.vm.dao; + +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +import org.apache.cloudstack.vm.bootgroup.InstanceBootGroupDetailsVO; +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 InstanceBootGroupDetailsDaoImpl extends GenericDaoBase implements InstanceBootGroupDetailsDao { + + private final SearchBuilder bootGroupSearch; + private final SearchBuilder bootGroupNameSearch; + + public InstanceBootGroupDetailsDaoImpl() { + bootGroupSearch = createSearchBuilder(); + bootGroupSearch.and("bootGroupId", bootGroupSearch.entity().getBootGroupId(), SearchCriteria.Op.EQ); + bootGroupSearch.done(); + + bootGroupNameSearch = createSearchBuilder(); + bootGroupNameSearch.and("bootGroupId", bootGroupNameSearch.entity().getBootGroupId(), SearchCriteria.Op.EQ); + bootGroupNameSearch.and("name", bootGroupNameSearch.entity().getName(), SearchCriteria.Op.EQ); + bootGroupNameSearch.done(); + } + + @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); + } + } + + @Override + public Map listDetails(long bootGroupId) { + SearchCriteria sc = bootGroupSearch.create(); + sc.setParameters("bootGroupId", bootGroupId); + List details = listBy(sc); + Map result = new HashMap<>(); + for (InstanceBootGroupDetailsVO detail : details) { + result.put(detail.getName(), detail.getValue()); + } + return result; + } +} 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..15a75dd51dde --- /dev/null +++ b/engine/schema/src/main/java/org/apache/cloudstack/vm/bootgroup/InstanceBootGroupDetailsVO.java @@ -0,0 +1,77 @@ +// 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; + +/** + * 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 { + + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + @Column(name = "id") + private long id; + + @Column(name = "boot_group_id") + private long bootGroupId; + + @Column(name = "name") + private String name; + + @Column(name = "value") + private String value; + + protected InstanceBootGroupDetailsVO() { + } + + public InstanceBootGroupDetailsVO(long bootGroupId, String name, String value) { + this.bootGroupId = bootGroupId; + this.name = name; + this.value = value; + } + + public long getId() { + return id; + } + + public long getBootGroupId() { + return bootGroupId; + } + + public String getName() { + return name; + } + + public String getValue() { + return value; + } + + public void setValue(String value) { + this.value = value; + } +} 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 27077b120e8a..48ad53ee12d7 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 @@ -113,6 +113,7 @@ + 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 f4c53ff95129..d31fc6881d2b 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 @@ -644,3 +644,14 @@ CREATE TABLE IF NOT EXISTS `cloud`.`instance_boot_group_readiness_check_result` PRIMARY KEY (`rule_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, + 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/server/src/main/java/org/apache/cloudstack/vm/bootgroup/InstanceBootGroupApiServiceImpl.java b/server/src/main/java/org/apache/cloudstack/vm/bootgroup/InstanceBootGroupApiServiceImpl.java index d720d8977ee8..e8451985bf40 100644 --- a/server/src/main/java/org/apache/cloudstack/vm/bootgroup/InstanceBootGroupApiServiceImpl.java +++ b/server/src/main/java/org/apache/cloudstack/vm/bootgroup/InstanceBootGroupApiServiceImpl.java @@ -66,14 +66,17 @@ import com.cloud.utils.db.Filter; import com.cloud.utils.db.SearchBuilder; import com.cloud.utils.db.SearchCriteria; +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; /** @@ -120,6 +123,12 @@ public class InstanceBootGroupApiServiceImpl implements InstanceBootGroupService @Inject private InstanceBootGroupReadinessCheckResultDao instanceBootGroupReadinessCheckResultDao; + @Inject + private InstanceBootGroupDetailsDao instanceBootGroupDetailsDao; + + @Inject + private InstanceGroupVMMapDao instanceGroupVMMapDao; + @NotNull protected InstanceBootGroupVO getGroupAndCheckAccess(long id) { InstanceBootGroupVO group = instanceBootGroupDao.findById(id); @@ -138,6 +147,12 @@ protected InstanceBootGroupResponse createInstanceBootGroupResponse(InstanceBoot response.setDescription(bootGroup.getDescription()); response.setCreated(bootGroup.getCreated()); ApiResponseHelper.populateOwner(response, bootGroup); + + String timeoutOverride = instanceBootGroupDetailsDao.getDetail(bootGroup.getId(), InstanceBootGroupManagerImpl.ReadinessTimeoutSeconds.key()); + response.setReadinessTimeoutSeconds(timeoutOverride != null ? Long.parseLong(timeoutOverride) : InstanceBootGroupManagerImpl.ReadinessTimeoutSeconds.value()); + String maxRebootOverride = instanceBootGroupDetailsDao.getDetail(bootGroup.getId(), InstanceBootGroupManagerImpl.ReadinessMaxRebootAttempts.key()); + response.setReadinessMaxRebootAttempts(maxRebootOverride != null ? Long.parseLong(maxRebootOverride) : InstanceBootGroupManagerImpl.ReadinessMaxRebootAttempts.value()); + response.setObjectName("instancebootgroup"); return response; } @@ -182,11 +197,25 @@ public InstanceBootGroup updateInstanceBootGroup(UpdateInstanceBootGroupCmd cmd) if (cmd.getDescription() != null) { group.setDescription(cmd.getDescription()); } + if (cmd.getReadinessTimeoutSeconds() != null) { + setOrClearOverride(group.getId(), InstanceBootGroupManagerImpl.ReadinessTimeoutSeconds.key(), cmd.getReadinessTimeoutSeconds()); + } + if (cmd.getReadinessMaxRebootAttempts() != null) { + setOrClearOverride(group.getId(), InstanceBootGroupManagerImpl.ReadinessMaxRebootAttempts.key(), cmd.getReadinessMaxRebootAttempts()); + } 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(); @@ -320,8 +349,9 @@ public ListResponse listInstanceBootGroupMember List members = result.first(); members.sort(Comparator.comparingInt(InstanceBootGroupMemberVO::getOrder)); + boolean includeReadiness = cmd.isReadinessDetailRequested(); ListResponse response = new ListResponse<>(); - response.setResponses(members.stream().map(this::createInstanceBootGroupMemberResponse).collect(Collectors.toList()), result.second()); + response.setResponses(members.stream().map(member -> createInstanceBootGroupMemberResponse(member, includeReadiness)).collect(Collectors.toList()), result.second()); return response; } @@ -356,6 +386,10 @@ public InstanceBootGroupResponse createInstanceBootGroupResponse(long id) { @Override public InstanceBootGroupMemberResponse createInstanceBootGroupMemberResponse(InstanceBootGroupMember member) { + return createInstanceBootGroupMemberResponse(member, false); + } + + private InstanceBootGroupMemberResponse createInstanceBootGroupMemberResponse(InstanceBootGroupMember member, boolean includeReadiness) { InstanceBootGroupMemberResponse response = new InstanceBootGroupMemberResponse(); response.setId(member.getUuid()); InstanceBootGroupVO group = instanceBootGroupDao.findById(member.getBootGroupId()); @@ -379,10 +413,88 @@ public InstanceBootGroupMemberResponse createInstanceBootGroupMemberResponse(Ins response.setMemberName(instanceGroup.getName()); } } + + if (includeReadiness) { + response.setReadinessMode(computeReadinessMode(member.getMemberType(), member.getBootGroupId(), member.getMemberId())); + InstanceBootGroupReadinessRule.Status status = member.getMemberType() == InstanceBootGroupMember.MemberType.VirtualMachine + ? computeCachedVmReadinessStatus(member.getBootGroupId(), member.getMemberId()) + : computeCachedInstanceGroupReadinessStatus(member.getBootGroupId(), member.getMemberId()); + response.setReadinessStatus(status.name()); + } + response.setObjectName("instancebootgroupmember"); return response; } + private String computeReadinessMode(InstanceBootGroupMember.MemberType itemType, long bootGroupId, long itemId) { + boolean hasRules = !instanceBootGroupReadinessRuleDao.listEnabledByItem(bootGroupId, itemType, itemId).isEmpty(); + if (hasRules) { + return "RULE_BASED"; + } + return itemType == InstanceBootGroupMember.MemberType.VirtualMachine ? "NO_READINESS" : "CHILD_DEPENDENT"; + } + + /** + * Uses the last cached {@code instance_boot_group_readiness_check_result} per rule rather than + * triggering a live re-evaluation — viewing/polling the member list should never itself dispatch + * remote checks (VR_PING etc.) as a side effect. + */ + private InstanceBootGroupReadinessRule.Status computeCachedVmReadinessStatus(long bootGroupId, long vmId) { + List rules = instanceBootGroupReadinessRuleDao.listEnabledByItem(bootGroupId, InstanceBootGroupMember.MemberType.VirtualMachine, vmId); + if (rules.isEmpty()) { + UserVmVO vm = userVmDao.findById(vmId); + boolean running = vm != null && vm.getState() == com.cloud.vm.VirtualMachine.State.Running; + return running ? InstanceBootGroupReadinessRule.Status.READY : InstanceBootGroupReadinessRule.Status.NOT_READY; + } + return combineCachedRuleStatuses(rules); + } + + private InstanceBootGroupReadinessRule.Status computeCachedInstanceGroupReadinessStatus(long bootGroupId, long instanceGroupId) { + List groupRules = instanceBootGroupReadinessRuleDao.listEnabledByItem(bootGroupId, InstanceBootGroupMember.MemberType.InstanceGroup, instanceGroupId); + boolean anyError = false; + boolean anyNotReady = false; + + InstanceBootGroupReadinessRule.Status ownStatus = combineCachedRuleStatuses(groupRules); + if (ownStatus == InstanceBootGroupReadinessRule.Status.ERROR) { + anyError = true; + } else if (ownStatus != InstanceBootGroupReadinessRule.Status.READY) { + anyNotReady = true; + } + + List memberVms = instanceGroupVMMapDao.listByGroupId(instanceGroupId); + for (InstanceGroupVMMapVO memberVm : memberVms) { + InstanceBootGroupReadinessRule.Status vmStatus = computeCachedVmReadinessStatus(bootGroupId, memberVm.getInstanceId()); + if (vmStatus == InstanceBootGroupReadinessRule.Status.ERROR) { + anyError = true; + } else if (vmStatus != InstanceBootGroupReadinessRule.Status.READY) { + anyNotReady = true; + } + } + + if (anyError) { + return InstanceBootGroupReadinessRule.Status.ERROR; + } + return anyNotReady ? InstanceBootGroupReadinessRule.Status.NOT_READY : InstanceBootGroupReadinessRule.Status.READY; + } + + private InstanceBootGroupReadinessRule.Status combineCachedRuleStatuses(List rules) { + boolean anyError = false; + boolean anyNotReady = false; + for (InstanceBootGroupReadinessRuleVO rule : rules) { + InstanceBootGroupReadinessCheckResultVO result = instanceBootGroupReadinessCheckResultDao.findByRuleId(rule.getId()); + InstanceBootGroupReadinessRule.Status status = (result != null && result.getStatus() != null) ? result.getStatus() : InstanceBootGroupReadinessRule.Status.UNKNOWN; + if (status == InstanceBootGroupReadinessRule.Status.ERROR) { + anyError = true; + } else if (status != InstanceBootGroupReadinessRule.Status.READY) { + anyNotReady = true; + } + } + if (anyError) { + return InstanceBootGroupReadinessRule.Status.ERROR; + } + return anyNotReady ? InstanceBootGroupReadinessRule.Status.NOT_READY : InstanceBootGroupReadinessRule.Status.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"); 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 index 9ede597daf6c..f56026ad2e14 100644 --- a/server/src/main/java/org/apache/cloudstack/vm/bootgroup/InstanceBootGroupManagerImpl.java +++ b/server/src/main/java/org/apache/cloudstack/vm/bootgroup/InstanceBootGroupManagerImpl.java @@ -19,6 +19,8 @@ import java.util.ArrayList; import java.util.Comparator; +import java.util.HashMap; +import java.util.LinkedHashMap; import java.util.List; import java.util.Map; import java.util.TreeMap; @@ -32,22 +34,46 @@ import org.springframework.stereotype.Component; +import org.apache.cloudstack.framework.config.ConfigKey; +import org.apache.cloudstack.framework.config.Configurable; + import com.cloud.utils.component.ManagerBase; import com.cloud.utils.concurrency.NamedThreadFactory; import com.cloud.utils.exception.CloudRuntimeException; import com.cloud.vm.UserVmService; import com.cloud.vm.UserVmVO; +import com.cloud.vm.VirtualMachineManager; +import com.cloud.vm.dao.InstanceBootGroupDetailsDao; import com.cloud.vm.dao.InstanceBootGroupMemberDao; import com.cloud.vm.dao.InstanceGroupVMMapDao; import com.cloud.vm.dao.UserVmDao; /** - * Backend/orchestration half of the Instance Boot Group feature — tier concurrency and hypervisor - * start/stop calls only. API-cmd handling (ACL, validation, response building, command registration) - * lives in {@code InstanceBootGroupApiServiceImpl}, which delegates here with resolved domain objects. + * 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 { +public class InstanceBootGroupManagerImpl extends ManagerBase implements InstanceBootGroupManager, Configurable { + + public static final ConfigKey ReadinessTimeoutSeconds = 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 rebooting it. Overridable per boot group.", true); + + public static final ConfigKey ReadinessMaxRebootAttempts = new ConfigKey<>("Advanced", Long.class, + "instance.boot.group.readiness.max.reboot.attempts", "5", + "Maximum number of reboot 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. Global only, not overridable per boot group.", true); @Inject private InstanceBootGroupMemberDao instanceBootGroupMemberDao; @@ -61,27 +87,195 @@ public class InstanceBootGroupManagerImpl extends ManagerBase implements Instanc @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 { return true; } + @Override + public String getConfigComponentName() { + return InstanceBootGroupManagerImpl.class.getSimpleName(); + } + + @Override + public ConfigKey[] getConfigKeys() { + return new ConfigKey[]{ReadinessTimeoutSeconds, ReadinessMaxRebootAttempts, ReadinessPollIntervalSeconds}; + } + + /** 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; + private int rebootAttempts; + private long enteredWaitAtMs; + + private VmProgress(long vmId, Long bootGroupMemberId) { + this.vmId = vmId; + this.bootGroupMemberId = bootGroupMemberId; + } + } + @Override public void startInstanceBootGroup(InstanceBootGroupVO group) { List members = instanceBootGroupMemberDao.listByBootGroupId(group.getId()); Map> tiers = groupByOrder(members); - for (Map.Entry> tier : tiers.entrySet()) { - List vmIds = resolveVmIds(tier.getValue()); - runTierConcurrently(vmIds, group, "start", vmId -> { - UserVmVO vm = userVmDao.findById(vmId); - if (vm != null && vm.getState() != com.cloud.vm.VirtualMachine.State.Running) { - userVmService.startVirtualMachine(vm, null); + 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()); + + try { + runTierConcurrently(tierVmIds, group, "start", vmId -> { + UserVmVO vm = userVmDao.findById(vmId); + if (vm != null && vm.getState() != com.cloud.vm.VirtualMachine.State.Running) { + userVmService.startVirtualMachine(vm, null); + } + progressByVmId.get(vmId).enteredWaitAtMs = System.currentTimeMillis(); + }); + } catch (CloudRuntimeException e) { + haltAndStop(group, "Failed to start a VM in tier " + tierOrder + ": " + e.getMessage()); + throw e; + } + + waitForTierReady(group, tierOrder, tierMembers, progressByVmId); + } + } + + private void waitForTierReady(InstanceBootGroupVO group, int tierOrder, List tierMembers, Map progressByVmId) { + long pollIntervalMs = effectivePollIntervalSeconds() * 1000L; + Map groupMemberFirstAllReadyAtMs = new HashMap<>(); + + while (true) { + boolean allVmsReady = true; + + for (VmProgress progress : progressByVmId.values()) { + if (progress.ready) { + continue; + } + + InstanceBootGroupReadinessRule.Status readiness = instanceBootGroupReadinessRuleService.evaluateVmReadiness(group.getId(), progress.vmId); + if (readiness == InstanceBootGroupReadinessRule.Status.READY) { + progress.ready = true; + continue; + } + + allVmsReady = false; + long elapsedMs = System.currentTimeMillis() - progress.enteredWaitAtMs; + long timeoutMs = effectiveTimeoutSeconds(group) * 1000L; + if (elapsedMs <= timeoutMs) { + continue; + } + + if (progress.rebootAttempts < effectiveMaxRebootAttempts(group)) { + rebootVm(progress.vmId); + progress.rebootAttempts++; + progress.enteredWaitAtMs = System.currentTimeMillis(); + } else { + String reason = String.format("Instance %d failed readiness after %d reboot attempts", progress.vmId, progress.rebootAttempts); + haltAndStop(group, reason); + throw new CloudRuntimeException(reason); + } + } + + boolean allGroupMembersReady = true; + for (InstanceBootGroupMemberVO member : tierMembers) { + if (member.getMemberType() != InstanceBootGroupMember.MemberType.InstanceGroup) { + continue; + } + + boolean memberVmsReady = progressByVmId.values().stream() + .filter(p -> member.getId() == (p.bootGroupMemberId == null ? -1 : p.bootGroupMemberId)) + .allMatch(p -> p.ready); + if (!memberVmsReady) { + allGroupMembersReady = false; + continue; + } + + InstanceBootGroupReadinessRule.Status groupStatus = instanceBootGroupReadinessRuleService.evaluateInstanceGroupReadiness(group.getId(), member.getMemberId()); + if (groupStatus == InstanceBootGroupReadinessRule.Status.READY) { + continue; + } + + allGroupMembersReady = false; + long firstReadyAtMs = groupMemberFirstAllReadyAtMs.computeIfAbsent(member.getId(), k -> System.currentTimeMillis()); + long elapsedMs = System.currentTimeMillis() - firstReadyAtMs; + if (elapsedMs > effectiveTimeoutSeconds(group) * 1000L) { + String reason = String.format("Instance group %d failed its own readiness rules after timeout", member.getMemberId()); + haltAndStop(group, reason); + throw new CloudRuntimeException(reason); + } + } + + if (allVmsReady && allGroupMembersReady) { + return; + } + + sleep(pollIntervalMs); } } + private void haltAndStop(InstanceBootGroupVO group, String reason) { + logger.warn("Halting boot group {} start: {}", group, reason); + try { + stopInstanceBootGroup(group); + } catch (RuntimeException e) { + logger.warn("Failed to stop boot group {} after halting: {}", group, e.getMessage()); + } + } + + private void rebootVm(long vmId) { + UserVmVO vm = userVmDao.findById(vmId); + if (vm == null) { + return; + } + 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(), ReadinessTimeoutSeconds.key()); + return override != null ? Long.parseLong(override) : ReadinessTimeoutSeconds.value(); + } + + private long effectiveMaxRebootAttempts(InstanceBootGroupVO group) { + String override = instanceBootGroupDetailsDao.getDetail(group.getId(), ReadinessMaxRebootAttempts.key()); + return override != null ? Long.parseLong(override) : ReadinessMaxRebootAttempts.value(); + } + + private long effectivePollIntervalSeconds() { + return ReadinessPollIntervalSeconds.value(); + } + @Override public void stopInstanceBootGroup(InstanceBootGroupVO group) { List members = instanceBootGroupMemberDao.listByBootGroupId(group.getId()); diff --git a/ui/public/locales/en.json b/ui/public/locales/en.json index 7a87a70db8e7..2b3d6ab5ccca 100644 --- a/ui/public/locales/en.json +++ b/ui/public/locales/en.json @@ -329,6 +329,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", @@ -1687,6 +1688,7 @@ "label.may.continue": "You may now continue.", "label.mb.memory": "MB memory", "label.member.type": "Member Type", +"label.message": "Message", "label.members": "Members", "label.memory": "Memory", "label.memory.free": "Memory free", @@ -2152,6 +2154,11 @@ "label.readonly": "Read-Only", "label.reason": "Reason", "label.rebalance": "Rebalance", +"label.readiness": "Readiness", +"label.readiness.mode.child.dependent": "Depends on members", +"label.readiness.mode.no.readiness": "No Readiness", +"label.readiness.mode.rule.based": "Rule-based", +"label.readiness.rules": "Readiness Rules", "label.reboot": "Reboot", "label.recent.deliveries": "Recent deliveries", "label.receivedbytes": "Bytes received", @@ -2293,6 +2300,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", @@ -2668,6 +2676,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", @@ -3382,6 +3392,7 @@ "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?", @@ -4031,6 +4042,8 @@ "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", diff --git a/ui/src/views/compute/InstanceBootGroupMembersTab.vue b/ui/src/views/compute/InstanceBootGroupMembersTab.vue index 40e33d106dee..a63866918b29 100644 --- a/ui/src/views/compute/InstanceBootGroupMembersTab.vue +++ b/ui/src/views/compute/InstanceBootGroupMembersTab.vue @@ -49,10 +49,20 @@ + + @@ -125,6 +141,21 @@ :resource="memberForUpdate" @close-action="closeModals" /> + + + + @@ -134,6 +165,7 @@ import TooltipButton from '@/components/widgets/TooltipButton' import Status from '@/components/widgets/Status' import AddInstanceBootGroupMember from '@/views/compute/AddInstanceBootGroupMember.vue' import UpdateInstanceBootGroupMemberOrder from '@/views/compute/UpdateInstanceBootGroupMemberOrder.vue' +import InstanceBootGroupReadinessRulesModal from '@/views/compute/InstanceBootGroupReadinessRulesModal.vue' export default { name: 'InstanceBootGroupMembersTab', @@ -141,7 +173,8 @@ export default { TooltipButton, Status, AddInstanceBootGroupMember, - UpdateInstanceBootGroupMemberOrder + UpdateInstanceBootGroupMemberOrder, + InstanceBootGroupReadinessRulesModal }, props: { resource: { @@ -160,11 +193,16 @@ export default { showAddMember: false, showUpdateOrder: false, memberForUpdate: {}, + showReadinessRules: false, + readinessRulesItemType: 'VirtualMachine', + readinessRulesItemId: null, + readinessRulesItemName: '', columns: [ { key: 'order', title: this.$t('label.boot.order'), dataIndex: 'order' }, { key: 'membertype', title: this.$t('label.member.type'), dataIndex: 'membertype' }, { key: 'membername', title: this.$t('label.name'), dataIndex: 'membername' }, { key: 'state', title: this.$t('label.state'), dataIndex: 'memberstate' }, + { key: 'readiness', title: this.$t('label.readiness'), dataIndex: 'readinessstatus' }, { key: 'created', title: this.$t('label.created'), dataIndex: 'created' }, { key: 'actions', title: this.$t('label.actions') } ], @@ -172,7 +210,8 @@ export default { { key: 'name', title: this.$t('label.name'), dataIndex: 'name' }, { title: this.$t('label.displayname'), dataIndex: 'displayname' }, { key: 'state', title: this.$t('label.state'), dataIndex: 'state' }, - { title: this.$t('label.zonename'), dataIndex: 'zonename' } + { title: this.$t('label.zonename'), dataIndex: 'zonename' }, + { key: 'vmactions', title: this.$t('label.actions') } ] } }, @@ -199,7 +238,9 @@ export default { return } this.tabLoading = true - getAPI('listInstanceBootGroupMembers', { bootgroupid: this.resource.id, listall: true }).then(json => { + // details=readiness is opt-in: computing readiness is not free, so listInstanceBootGroupMembers + // only includes it when explicitly requested + getAPI('listInstanceBootGroupMembers', { bootgroupid: this.resource.id, listall: true, details: 'readiness' }).then(json => { const members = json?.listinstancebootgroupmembersresponse?.instancebootgroupmember || [] this.members = members.slice().sort((a, b) => a.order - b.order) }).catch(error => { @@ -208,6 +249,22 @@ export default { this.tabLoading = false }) }, + readinessStatusColor (status) { + switch (status) { + case 'READY': return 'success' + case 'NOT_READY': return 'warning' + case 'ERROR': return 'error' + default: return 'default' + } + }, + readinessModeLabel (mode) { + switch (mode) { + case 'NO_READINESS': return this.$t('label.readiness.mode.no.readiness') + case 'CHILD_DEPENDENT': return this.$t('label.readiness.mode.child.dependent') + case 'RULE_BASED': return this.$t('label.readiness.mode.rule.based') + default: return '' + } + }, onExpand (expanded, record) { if (expanded) { if (!this.expandedRowKeys.includes(record.id)) { @@ -242,9 +299,16 @@ export default { this.memberForUpdate = record this.showUpdateOrder = true }, + openReadinessRulesModal (itemType, itemId, itemName) { + this.readinessRulesItemType = itemType + this.readinessRulesItemId = itemId + this.readinessRulesItemName = itemName + this.showReadinessRules = true + }, closeModals () { this.showAddMember = false this.showUpdateOrder = false + this.showReadinessRules = false this.memberForUpdate = {} this.fetchMembers() }, diff --git a/ui/src/views/compute/InstanceBootGroupReadinessRulesModal.vue b/ui/src/views/compute/InstanceBootGroupReadinessRulesModal.vue new file mode 100644 index 000000000000..33d3f1ca5383 --- /dev/null +++ b/ui/src/views/compute/InstanceBootGroupReadinessRulesModal.vue @@ -0,0 +1,252 @@ +// 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. + + + + + + From 757153f33bfa3318e764c280c0a86c74c1bd1e42 Mon Sep 17 00:00:00 2001 From: Abhishek Kumar Date: Fri, 17 Jul 2026 15:57:10 +0530 Subject: [PATCH 5/9] wip --- .../main/java/com/cloud/event/EventTypes.java | 2 +- .../cloudstack/api/ApiCommandResourceType.java | 3 ++- ...CreateInstanceBootGroupReadinessRuleCmd.java | 2 +- ...DeleteInstanceBootGroupReadinessRuleCmd.java | 2 +- .../ListInstanceBootGroupReadinessRulesCmd.java | 2 +- ...UpdateInstanceBootGroupReadinessRuleCmd.java | 2 +- .../InstanceBootGroupReadinessRuleResponse.java | 2 +- .../vm/bootgroup/InstanceBootGroupService.java | 1 + .../InstanceBootGroupReadinessRule.java | 3 ++- .../bootgroup/readiness}/ReadinessChecker.java | 5 ++--- ...g-core-lifecycle-api-context-inheritable.xml | 5 +++++ ...nstanceBootGroupReadinessCheckResultDao.java | 2 +- ...nceBootGroupReadinessCheckResultDaoImpl.java | 2 +- ...InstanceBootGroupReadinessCheckResultVO.java | 2 ++ .../InstanceBootGroupReadinessRuleVO.java | 2 ++ .../InstanceBootGroupApiServiceImpl.java | 2 ++ .../bootgroup/InstanceBootGroupManagerImpl.java | 2 ++ ...stanceBootGroupReadinessRuleManagerImpl.java | 17 ++++++++++------- .../InstanceBootGroupReadinessRuleService.java | 6 ++++-- .../readiness/{checker => }/VrPingChecker.java | 11 ++++------- .../spring-server-core-managers-context.xml | 4 +++- 21 files changed, 49 insertions(+), 30 deletions(-) rename api/src/main/java/org/apache/cloudstack/vm/bootgroup/{ => readiness}/InstanceBootGroupReadinessRule.java (94%) rename {server/src/main/java/org/apache/cloudstack/vm/bootgroup/readiness/checker => api/src/main/java/org/apache/cloudstack/vm/bootgroup/readiness}/ReadinessChecker.java (89%) rename server/src/main/java/org/apache/cloudstack/vm/bootgroup/{ => readiness}/InstanceBootGroupReadinessRuleManagerImpl.java (97%) rename server/src/main/java/org/apache/cloudstack/vm/bootgroup/{ => readiness}/InstanceBootGroupReadinessRuleService.java (89%) rename server/src/main/java/org/apache/cloudstack/vm/bootgroup/readiness/{checker => }/VrPingChecker.java (94%) diff --git a/api/src/main/java/com/cloud/event/EventTypes.java b/api/src/main/java/com/cloud/event/EventTypes.java index e5d8788c2b26..f38d59d915b8 100644 --- a/api/src/main/java/com/cloud/event/EventTypes.java +++ b/api/src/main/java/com/cloud/event/EventTypes.java @@ -50,7 +50,7 @@ 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.InstanceBootGroupReadinessRule; +import org.apache.cloudstack.vm.bootgroup.readiness.InstanceBootGroupReadinessRule; import com.cloud.dc.DataCenter; import com.cloud.dc.DataCenterGuestIpv6Prefix; 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 128dc9f0b61d..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; @@ -93,7 +94,7 @@ public enum ApiCommandResourceType { KmsKey(org.apache.cloudstack.kms.KMSKey.class), HsmProfile(org.apache.cloudstack.kms.HSMProfile.class), InstanceBootGroup(org.apache.cloudstack.vm.bootgroup.InstanceBootGroup.class), - InstanceBootGroupReadinessRule(org.apache.cloudstack.vm.bootgroup.InstanceBootGroupReadinessRule.class); + InstanceBootGroupReadinessRule(InstanceBootGroupReadinessRule.class); private final Class clazz; 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 index 92eecd292995..8a827b5ce93f 100644 --- 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 @@ -36,7 +36,7 @@ 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.InstanceBootGroupReadinessRule; +import org.apache.cloudstack.vm.bootgroup.readiness.InstanceBootGroupReadinessRule; import org.apache.cloudstack.vm.bootgroup.InstanceBootGroupService; @APICommand(name = "createInstanceBootGroupReadinessRule", 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 index ccffd9957402..c8fe2b6383de 100644 --- 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 @@ -31,7 +31,7 @@ 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.InstanceBootGroupReadinessRule; +import org.apache.cloudstack.vm.bootgroup.readiness.InstanceBootGroupReadinessRule; import org.apache.cloudstack.vm.bootgroup.InstanceBootGroupService; @APICommand(name = "deleteInstanceBootGroupReadinessRule", 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 index 0a61f1750fcc..8589b97e875a 100644 --- 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 @@ -30,7 +30,7 @@ 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.InstanceBootGroupReadinessRule; +import org.apache.cloudstack.vm.bootgroup.readiness.InstanceBootGroupReadinessRule; import org.apache.cloudstack.vm.bootgroup.InstanceBootGroupService; @APICommand(name = "listInstanceBootGroupReadinessRules", 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 index 4a89f9e8f2b7..93875c24375b 100644 --- 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 @@ -33,7 +33,7 @@ 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.InstanceBootGroupReadinessRule; +import org.apache.cloudstack.vm.bootgroup.readiness.InstanceBootGroupReadinessRule; import org.apache.cloudstack.vm.bootgroup.InstanceBootGroupService; @APICommand(name = "updateInstanceBootGroupReadinessRule", 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 index 04794f333aab..11e7d79985ee 100644 --- a/api/src/main/java/org/apache/cloudstack/api/response/InstanceBootGroupReadinessRuleResponse.java +++ b/api/src/main/java/org/apache/cloudstack/api/response/InstanceBootGroupReadinessRuleResponse.java @@ -25,7 +25,7 @@ import org.apache.cloudstack.api.ApiConstants; import org.apache.cloudstack.api.BaseResponse; import org.apache.cloudstack.api.EntityReference; -import org.apache.cloudstack.vm.bootgroup.InstanceBootGroupReadinessRule; +import org.apache.cloudstack.vm.bootgroup.readiness.InstanceBootGroupReadinessRule; import com.cloud.serializer.Param; 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 index 1dc865083160..29d4d6f6cc08 100644 --- a/api/src/main/java/org/apache/cloudstack/vm/bootgroup/InstanceBootGroupService.java +++ b/api/src/main/java/org/apache/cloudstack/vm/bootgroup/InstanceBootGroupService.java @@ -36,6 +36,7 @@ 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 { diff --git a/api/src/main/java/org/apache/cloudstack/vm/bootgroup/InstanceBootGroupReadinessRule.java b/api/src/main/java/org/apache/cloudstack/vm/bootgroup/readiness/InstanceBootGroupReadinessRule.java similarity index 94% rename from api/src/main/java/org/apache/cloudstack/vm/bootgroup/InstanceBootGroupReadinessRule.java rename to api/src/main/java/org/apache/cloudstack/vm/bootgroup/readiness/InstanceBootGroupReadinessRule.java index 679450bb227d..4a5a3f908359 100644 --- a/api/src/main/java/org/apache/cloudstack/vm/bootgroup/InstanceBootGroupReadinessRule.java +++ b/api/src/main/java/org/apache/cloudstack/vm/bootgroup/readiness/InstanceBootGroupReadinessRule.java @@ -15,12 +15,13 @@ // specific language governing permissions and limitations // under the License. -package org.apache.cloudstack.vm.bootgroup; +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: diff --git a/server/src/main/java/org/apache/cloudstack/vm/bootgroup/readiness/checker/ReadinessChecker.java b/api/src/main/java/org/apache/cloudstack/vm/bootgroup/readiness/ReadinessChecker.java similarity index 89% rename from server/src/main/java/org/apache/cloudstack/vm/bootgroup/readiness/checker/ReadinessChecker.java rename to api/src/main/java/org/apache/cloudstack/vm/bootgroup/readiness/ReadinessChecker.java index cf8574467d26..8ccf68c39b5c 100644 --- a/server/src/main/java/org/apache/cloudstack/vm/bootgroup/readiness/checker/ReadinessChecker.java +++ b/api/src/main/java/org/apache/cloudstack/vm/bootgroup/readiness/ReadinessChecker.java @@ -15,11 +15,10 @@ // specific language governing permissions and limitations // under the License. -package org.apache.cloudstack.vm.bootgroup.readiness.checker; +package org.apache.cloudstack.vm.bootgroup.readiness; import java.util.Map; -import org.apache.cloudstack.vm.bootgroup.InstanceBootGroupReadinessRule; import org.apache.cloudstack.vm.bootgroup.InstanceBootGroupReadinessRuleVO; /** @@ -37,7 +36,7 @@ public interface ReadinessChecker { * @param details the rule's decrypted detail key/value pairs * @param vmId the VM to check readiness for */ - Result check(InstanceBootGroupReadinessRuleVO rule, Map details, long vmId); + Result check(InstanceBootGroupReadinessRule rule, Map details, long vmId); class Result { private final InstanceBootGroupReadinessRule.Status status; diff --git a/core/src/main/resources/META-INF/cloudstack/api/spring-core-lifecycle-api-context-inheritable.xml b/core/src/main/resources/META-INF/cloudstack/api/spring-core-lifecycle-api-context-inheritable.xml index 995ed30eb5e6..edb1f0e3d228 100644 --- a/core/src/main/resources/META-INF/cloudstack/api/spring-core-lifecycle-api-context-inheritable.xml +++ b/core/src/main/resources/META-INF/cloudstack/api/spring-core-lifecycle-api-context-inheritable.xml @@ -73,4 +73,9 @@
+ + + + + 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 index ff375b0d6207..317a541bd29c 100644 --- a/engine/schema/src/main/java/com/cloud/vm/dao/InstanceBootGroupReadinessCheckResultDao.java +++ b/engine/schema/src/main/java/com/cloud/vm/dao/InstanceBootGroupReadinessCheckResultDao.java @@ -20,7 +20,7 @@ import java.util.Date; import org.apache.cloudstack.vm.bootgroup.InstanceBootGroupReadinessCheckResultVO; -import org.apache.cloudstack.vm.bootgroup.InstanceBootGroupReadinessRule; +import org.apache.cloudstack.vm.bootgroup.readiness.InstanceBootGroupReadinessRule; import com.cloud.utils.db.GenericDao; 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 index ba1b3c8ab667..138610bfcb9e 100644 --- a/engine/schema/src/main/java/com/cloud/vm/dao/InstanceBootGroupReadinessCheckResultDaoImpl.java +++ b/engine/schema/src/main/java/com/cloud/vm/dao/InstanceBootGroupReadinessCheckResultDaoImpl.java @@ -20,7 +20,7 @@ import java.util.Date; import org.apache.cloudstack.vm.bootgroup.InstanceBootGroupReadinessCheckResultVO; -import org.apache.cloudstack.vm.bootgroup.InstanceBootGroupReadinessRule; +import org.apache.cloudstack.vm.bootgroup.readiness.InstanceBootGroupReadinessRule; import org.springframework.stereotype.Component; import com.cloud.utils.db.GenericDaoBase; 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 index 8e08279a2394..705a36bd74c7 100644 --- 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 @@ -26,6 +26,8 @@ import javax.persistence.Id; import javax.persistence.Table; +import org.apache.cloudstack.vm.bootgroup.readiness.InstanceBootGroupReadinessRule; + /** * Last cached evaluation result for a readiness rule, upserted in place — no history, by design. */ 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 index a8974c2cdb51..ab2065148c2f 100644 --- 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 @@ -29,6 +29,8 @@ import javax.persistence.Id; import javax.persistence.Table; +import org.apache.cloudstack.vm.bootgroup.readiness.InstanceBootGroupReadinessRule; + import com.cloud.utils.db.GenericDao; @Entity 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 index e8451985bf40..3f4b09c6a9ff 100644 --- a/server/src/main/java/org/apache/cloudstack/vm/bootgroup/InstanceBootGroupApiServiceImpl.java +++ b/server/src/main/java/org/apache/cloudstack/vm/bootgroup/InstanceBootGroupApiServiceImpl.java @@ -46,6 +46,8 @@ 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.commons.lang3.EnumUtils; import org.apache.commons.lang3.StringUtils; import org.jetbrains.annotations.NotNull; 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 index f56026ad2e14..c39de9678a3e 100644 --- a/server/src/main/java/org/apache/cloudstack/vm/bootgroup/InstanceBootGroupManagerImpl.java +++ b/server/src/main/java/org/apache/cloudstack/vm/bootgroup/InstanceBootGroupManagerImpl.java @@ -32,6 +32,8 @@ import javax.inject.Inject; import javax.naming.ConfigurationException; +import org.apache.cloudstack.vm.bootgroup.readiness.InstanceBootGroupReadinessRule; +import org.apache.cloudstack.vm.bootgroup.readiness.InstanceBootGroupReadinessRuleService; import org.springframework.stereotype.Component; import org.apache.cloudstack.framework.config.ConfigKey; diff --git a/server/src/main/java/org/apache/cloudstack/vm/bootgroup/InstanceBootGroupReadinessRuleManagerImpl.java b/server/src/main/java/org/apache/cloudstack/vm/bootgroup/readiness/InstanceBootGroupReadinessRuleManagerImpl.java similarity index 97% rename from server/src/main/java/org/apache/cloudstack/vm/bootgroup/InstanceBootGroupReadinessRuleManagerImpl.java rename to server/src/main/java/org/apache/cloudstack/vm/bootgroup/readiness/InstanceBootGroupReadinessRuleManagerImpl.java index 961b1b23381f..c88dd883bd75 100644 --- a/server/src/main/java/org/apache/cloudstack/vm/bootgroup/InstanceBootGroupReadinessRuleManagerImpl.java +++ b/server/src/main/java/org/apache/cloudstack/vm/bootgroup/readiness/InstanceBootGroupReadinessRuleManagerImpl.java @@ -15,7 +15,7 @@ // specific language governing permissions and limitations // under the License. -package org.apache.cloudstack.vm.bootgroup; +package org.apache.cloudstack.vm.bootgroup.readiness; import java.util.Collections; import java.util.Date; @@ -25,9 +25,11 @@ import java.util.Map; import java.util.Set; -import javax.annotation.PostConstruct; import javax.inject.Inject; +import org.apache.cloudstack.vm.bootgroup.InstanceBootGroupMember; +import org.apache.cloudstack.vm.bootgroup.InstanceBootGroupMemberVO; +import org.apache.cloudstack.vm.bootgroup.InstanceBootGroupReadinessRuleVO; import org.apache.commons.lang3.StringUtils; import org.springframework.stereotype.Component; @@ -42,8 +44,6 @@ import com.cloud.vm.dao.InstanceGroupVMMapDao; import com.cloud.vm.dao.UserVmDao; -import org.apache.cloudstack.vm.bootgroup.readiness.checker.ReadinessChecker; - @Component public class InstanceBootGroupReadinessRuleManagerImpl implements InstanceBootGroupReadinessRuleService { @@ -78,13 +78,16 @@ public class InstanceBootGroupReadinessRuleManagerImpl implements InstanceBootGr @Inject private UserVmDao userVmDao; - @Inject private List readinessCheckers; private Map checkersByRuleType; - @PostConstruct - public void init() { + public List getReadinessCheckers() { + return readinessCheckers; + } + + public void setReadinessCheckers(List readinessCheckers) { + this.readinessCheckers = readinessCheckers; checkersByRuleType = new HashMap<>(); for (ReadinessChecker checker : readinessCheckers) { checkersByRuleType.put(checker.getRuleType(), checker); diff --git a/server/src/main/java/org/apache/cloudstack/vm/bootgroup/InstanceBootGroupReadinessRuleService.java b/server/src/main/java/org/apache/cloudstack/vm/bootgroup/readiness/InstanceBootGroupReadinessRuleService.java similarity index 89% rename from server/src/main/java/org/apache/cloudstack/vm/bootgroup/InstanceBootGroupReadinessRuleService.java rename to server/src/main/java/org/apache/cloudstack/vm/bootgroup/readiness/InstanceBootGroupReadinessRuleService.java index 7efce1f56cf9..30de50fa3260 100644 --- a/server/src/main/java/org/apache/cloudstack/vm/bootgroup/InstanceBootGroupReadinessRuleService.java +++ b/server/src/main/java/org/apache/cloudstack/vm/bootgroup/readiness/InstanceBootGroupReadinessRuleService.java @@ -15,10 +15,12 @@ // specific language governing permissions and limitations // under the License. -package org.apache.cloudstack.vm.bootgroup; +package org.apache.cloudstack.vm.bootgroup.readiness; import java.util.Map; +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 @@ -28,7 +30,7 @@ public interface InstanceBootGroupReadinessRuleService { InstanceBootGroupReadinessRule createReadinessRule(long bootGroupId, InstanceBootGroupMember.MemberType itemType, long itemId, - InstanceBootGroupReadinessRule.RuleType ruleType, String name, boolean enabled, Map details); + InstanceBootGroupReadinessRule.RuleType ruleType, String name, boolean enabled, Map details); InstanceBootGroupReadinessRule updateReadinessRule(long ruleId, String name, Boolean enabled, Map details); diff --git a/server/src/main/java/org/apache/cloudstack/vm/bootgroup/readiness/checker/VrPingChecker.java b/server/src/main/java/org/apache/cloudstack/vm/bootgroup/readiness/VrPingChecker.java similarity index 94% rename from server/src/main/java/org/apache/cloudstack/vm/bootgroup/readiness/checker/VrPingChecker.java rename to server/src/main/java/org/apache/cloudstack/vm/bootgroup/readiness/VrPingChecker.java index b0d072af99dd..1c2bc70407ea 100644 --- a/server/src/main/java/org/apache/cloudstack/vm/bootgroup/readiness/checker/VrPingChecker.java +++ b/server/src/main/java/org/apache/cloudstack/vm/bootgroup/readiness/VrPingChecker.java @@ -15,23 +15,20 @@ // specific language governing permissions and limitations // under the License. -package org.apache.cloudstack.vm.bootgroup.readiness.checker; +package org.apache.cloudstack.vm.bootgroup.readiness; import java.util.List; import java.util.Map; import javax.inject.Inject; -import org.apache.commons.lang3.StringUtils; -import org.springframework.stereotype.Component; - import org.apache.cloudstack.api.ApiConstants; import org.apache.cloudstack.diagnostics.DiagnosticsAnswer; import org.apache.cloudstack.diagnostics.DiagnosticsCommand; import org.apache.cloudstack.diagnostics.DiagnosticsType; import org.apache.cloudstack.engine.orchestration.service.NetworkOrchestrationService; -import org.apache.cloudstack.vm.bootgroup.InstanceBootGroupReadinessRule; -import org.apache.cloudstack.vm.bootgroup.InstanceBootGroupReadinessRuleVO; +import org.apache.commons.lang3.StringUtils; +import org.springframework.stereotype.Component; import com.cloud.agent.AgentManager; import com.cloud.agent.api.Answer; @@ -83,7 +80,7 @@ public InstanceBootGroupReadinessRule.RuleType getRuleType() { } @Override - public Result check(InstanceBootGroupReadinessRuleVO rule, Map details, long vmId) { + public Result check(InstanceBootGroupReadinessRule rule, Map details, long vmId) { UserVmVO vm = userVmDao.findById(vmId); if (vm == null) { return new Result(InstanceBootGroupReadinessRule.Status.ERROR, "VM not found"); 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 784223727475..5d12ccd596fb 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 @@ -398,7 +398,9 @@ - + + + From a4da4ddde5b4bf7571f1cc684092ef4b65a59662 Mon Sep 17 00:00:00 2001 From: Abhishek Kumar Date: Fri, 17 Jul 2026 16:23:21 +0530 Subject: [PATCH 6/9] fixes --- .../vm/bootgroup/readiness/ReadinessChecker.java | 2 -- .../core/spring-core-registry-core-context.xml | 2 ++ .../core/spring-server-core-managers-context.xml | 14 +++++++------- 3 files changed, 9 insertions(+), 9 deletions(-) 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 index 8ccf68c39b5c..08f39b1893b3 100644 --- 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 @@ -19,8 +19,6 @@ import java.util.Map; -import org.apache.cloudstack.vm.bootgroup.InstanceBootGroupReadinessRuleVO; - /** * Strategy interface for evaluating one readiness rule type. Implementations are collected by * {@code InstanceBootGroupReadinessRuleManagerImpl} via Spring's {@code List} 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 0a92e8a637bc..1535ff7080f2 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 @@ -371,4 +371,6 @@ + + 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 5d12ccd596fb..c83478a4d2ba 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 @@ -395,13 +395,6 @@ - - - - - - - @@ -425,4 +418,11 @@ + + + + + + + From 24b8068b734a6f32c6ef450e6fdb23eb6f1e4dd4 Mon Sep 17 00:00:00 2001 From: Abhishek Kumar Date: Mon, 20 Jul 2026 17:06:38 +0530 Subject: [PATCH 7/9] wip --- ...eateInstanceBootGroupReadinessRuleCmd.java | 2 +- .../InstanceBootGroupReadinessRule.java | 4 +- .../api/CheckGuestAgentLivenessAnswer.java | 35 ++++ .../api/CheckGuestAgentLivenessCommand.java | 42 +++++ .../resource/virtualnetwork/VRScripts.java | 1 + .../VirtualRoutingResource.java | 13 ++ .../InstanceReadinessCheckAnswer.java | 56 +++++++ .../InstanceReadinessCheckCommand.java | 67 ++++++++ ...CheckGuestAgentLivenessCommandWrapper.java | 75 +++++++++ .../cloudstack/utils/qemu/QemuCommand.java | 1 + .../InstanceBootGroupApiServiceImpl.java | 2 +- .../readiness/GuestAgentLivenessChecker.java | 82 ++++++++++ ...anceBootGroupReadinessRuleManagerImpl.java | 32 +++- .../bootgroup/readiness/PortCheckChecker.java | 150 ++++++++++++++++++ .../vm/bootgroup/readiness/VrPingChecker.java | 23 ++- .../opt/cloud/bin/instance_readiness_check.py | 71 +++++++++ .../InstanceBootGroupReadinessRulesModal.vue | 3 +- 17 files changed, 637 insertions(+), 22 deletions(-) create mode 100644 core/src/main/java/com/cloud/agent/api/CheckGuestAgentLivenessAnswer.java create mode 100644 core/src/main/java/com/cloud/agent/api/CheckGuestAgentLivenessCommand.java create mode 100644 core/src/main/java/org/apache/cloudstack/vm/bootgroup/readiness/InstanceReadinessCheckAnswer.java create mode 100644 core/src/main/java/org/apache/cloudstack/vm/bootgroup/readiness/InstanceReadinessCheckCommand.java create mode 100644 plugins/hypervisors/kvm/src/main/java/com/cloud/hypervisor/kvm/resource/wrapper/LibvirtCheckGuestAgentLivenessCommandWrapper.java create mode 100644 server/src/main/java/org/apache/cloudstack/vm/bootgroup/readiness/GuestAgentLivenessChecker.java create mode 100644 server/src/main/java/org/apache/cloudstack/vm/bootgroup/readiness/PortCheckChecker.java create mode 100644 systemvm/debian/opt/cloud/bin/instance_readiness_check.py 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 index 8a827b5ce93f..70cf303f108f 100644 --- 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 @@ -65,7 +65,7 @@ public class CreateInstanceBootGroupReadinessRuleCmd extends BaseCmd implements private Long instanceGroupId; @Parameter(name = ApiConstants.RULE_TYPE, type = CommandType.STRING, required = true, - description = "The readiness rule type: GUEST_AGENT_LIVENESS, VR_PING, PORT_CHECK, CUSTOM_SCRIPT or INSTANCE_QUORUM") + description = "The readiness rule type: GUEST_AGENT_LIVENESS, PING, PORT_CHECK, CUSTOM_SCRIPT or INSTANCE_QUORUM") private String ruleType; @Parameter(name = ApiConstants.NAME, type = CommandType.STRING, description = "The name of the readiness rule; auto-generated if omitted") 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 index 4a5a3f908359..45e323fd4229 100644 --- 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 @@ -47,13 +47,13 @@ public interface InstanceBootGroupReadinessRule extends Identity, InternalIdenti /** * Which rule types are valid depends on {@link InstanceBootGroupMember.MemberType}: KVM-only, - * hypervisor-agnostic at the DB/API layer (no qemu/KVM naming stored). VR_PING/PORT_CHECK/ + * hypervisor-agnostic at the DB/API layer (no qemu/KVM naming stored). PING/PORT_CHECK/ * CUSTOM_SCRIPT/GUEST_AGENT_LIVENESS apply to VirtualMachine items; INSTANCE_QUORUM and * (group-scope) CUSTOM_SCRIPT apply to InstanceGroup items. */ enum RuleType { GUEST_AGENT_LIVENESS, - VR_PING, + PING, PORT_CHECK, CUSTOM_SCRIPT, INSTANCE_QUORUM 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..2596ae5b7c7a 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(NumbersUtil.parseInt("60", 60)); + 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..8458b4e19213 --- /dev/null +++ b/core/src/main/java/org/apache/cloudstack/vm/bootgroup/readiness/InstanceReadinessCheckCommand.java @@ -0,0 +1,67 @@ +// 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 final Integer port; + private final boolean executeInSequence; + + public InstanceReadinessCheckCommand(String checkType, String ipAddress, Integer port, boolean executeInSequence) { + this.checkType = checkType; + 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/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..a698860305e9 --- /dev/null +++ b/plugins/hypervisors/kvm/src/main/java/com/cloud/hypervisor/kvm/resource/wrapper/LibvirtCheckGuestAgentLivenessCommandWrapper.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 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.JsonParser; + +@ResourceWrapper(handles = CheckGuestAgentLivenessCommand.class) +public class LibvirtCheckGuestAgentLivenessCommandWrapper extends CommandWrapper { + + private static final int AGENT_PING_TIMEOUT_SECONDS = 5; + + @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)); + } + + String result = domain.qemuAgentCommand(QemuCommand.buildQemuCommand(QemuCommand.AGENT_PING, null), AGENT_PING_TIMEOUT_SECONDS, 0); + if (result != null && new JsonParser().parse(result).isJsonObject() && !new JsonParser().parse(result).getAsJsonObject().has("error")) { + return new CheckGuestAgentLivenessAnswer(command, true, "guest agent responded"); + } + return new CheckGuestAgentLivenessAnswer(command, false, "guest agent did not respond as expected: " + 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); + } + } + } + } +} 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/server/src/main/java/org/apache/cloudstack/vm/bootgroup/InstanceBootGroupApiServiceImpl.java b/server/src/main/java/org/apache/cloudstack/vm/bootgroup/InstanceBootGroupApiServiceImpl.java index 3f4b09c6a9ff..091d6117f5ff 100644 --- a/server/src/main/java/org/apache/cloudstack/vm/bootgroup/InstanceBootGroupApiServiceImpl.java +++ b/server/src/main/java/org/apache/cloudstack/vm/bootgroup/InstanceBootGroupApiServiceImpl.java @@ -439,7 +439,7 @@ private String computeReadinessMode(InstanceBootGroupMember.MemberType itemType, /** * Uses the last cached {@code instance_boot_group_readiness_check_result} per rule rather than * triggering a live re-evaluation — viewing/polling the member list should never itself dispatch - * remote checks (VR_PING etc.) as a side effect. + * remote checks (PING etc.) as a side effect. */ private InstanceBootGroupReadinessRule.Status computeCachedVmReadinessStatus(long bootGroupId, long vmId) { List rules = instanceBootGroupReadinessRuleDao.listEnabledByItem(bootGroupId, InstanceBootGroupMember.MemberType.VirtualMachine, vmId); 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..66dbd7942e68 --- /dev/null +++ b/server/src/main/java/org/apache/cloudstack/vm/bootgroup/readiness/GuestAgentLivenessChecker.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.vm.bootgroup.readiness; + +import java.util.Map; + +import javax.inject.Inject; + +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; + +/** + * GUEST_AGENT_LIVENESS: 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 { + + @Inject + private UserVmDao userVmDao; + + @Inject + private AgentManager agentManager; + + @Override + public InstanceBootGroupReadinessRule.RuleType getRuleType() { + return InstanceBootGroupReadinessRule.RuleType.GUEST_AGENT_LIVENESS; + } + + @Override + public Result check(InstanceBootGroupReadinessRule rule, Map details, long vmId) { + UserVmVO vm = userVmDao.findById(vmId); + if (vm == null) { + return new Result(InstanceBootGroupReadinessRule.Status.ERROR, "VM not found"); + } + if (vm.getHypervisorType() != HypervisorType.KVM) { + return new Result(InstanceBootGroupReadinessRule.Status.ERROR, + "Guest agent liveness checks are only supported on KVM; VM's hypervisor is " + vm.getHypervisorType()); + } + if (vm.getState() != VirtualMachine.State.Running || vm.getHostId() == null) { + return new Result(InstanceBootGroupReadinessRule.Status.NOT_READY, "VM is not running"); + } + + CheckGuestAgentLivenessCommand command = new CheckGuestAgentLivenessCommand(vm.getInstanceName()); + Answer answer; + try { + answer = agentManager.easySend(vm.getHostId(), command); + } catch (Exception e) { + return new Result(InstanceBootGroupReadinessRule.Status.ERROR, "Failed to dispatch guest agent liveness check: " + e.getMessage()); + } + if (answer == null) { + return new Result(InstanceBootGroupReadinessRule.Status.ERROR, "No answer from the VM's host"); + } + if (answer.getResult()) { + return new Result(InstanceBootGroupReadinessRule.Status.READY, "guest agent responded"); + } + return new Result(InstanceBootGroupReadinessRule.Status.NOT_READY, "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 index c88dd883bd75..386c6cd5fe91 100644 --- 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 @@ -49,11 +49,13 @@ public class InstanceBootGroupReadinessRuleManagerImpl implements InstanceBootGr 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.GUEST_AGENT_LIVENESS, - InstanceBootGroupReadinessRule.RuleType.VR_PING, + InstanceBootGroupReadinessRule.RuleType.PING, InstanceBootGroupReadinessRule.RuleType.PORT_CHECK, InstanceBootGroupReadinessRule.RuleType.CUSTOM_SCRIPT), InstanceBootGroupMember.MemberType.InstanceGroup, EnumSet.of( @@ -281,9 +283,14 @@ private ReadinessChecker.Result evaluateInstanceQuorum(long bootGroupId, long in } private void validateRuleTypeSpecificDetails(InstanceBootGroupReadinessRule.RuleType ruleType, Map details) { - if (ruleType != InstanceBootGroupReadinessRule.RuleType.INSTANCE_QUORUM) { - return; + if (ruleType == InstanceBootGroupReadinessRule.RuleType.INSTANCE_QUORUM) { + validateInstanceQuorumDetails(details); + } else if (ruleType == InstanceBootGroupReadinessRule.RuleType.PORT_CHECK) { + 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)) { @@ -304,6 +311,25 @@ private void validateRuleTypeSpecificDetails(InstanceBootGroupReadinessRule.Rule } } + private void validatePortCheckDetails(Map details) { + String protocol = details == null ? null : details.get(PROTOCOL_KEY); + if (StringUtils.isNotBlank(protocol) && !"tcp".equalsIgnoreCase(protocol)) { + throw new InvalidParameterValueException("PORT_CHECK rules only support the tcp protocol currently"); + } + String port = details == null ? null : details.get(PORT_KEY); + if (StringUtils.isBlank(port)) { + throw new InvalidParameterValueException("PORT_CHECK 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 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)); 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..94013580a3b5 --- /dev/null +++ b/server/src/main/java/org/apache/cloudstack/vm/bootgroup/readiness/PortCheckChecker.java @@ -0,0 +1,150 @@ +// 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.commons.lang3.StringUtils; +import org.springframework.stereotype.Component; + +import org.apache.cloudstack.engine.orchestration.service.NetworkOrchestrationService; + +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.VirtualNetworkApplianceManager; +import com.cloud.network.router.VirtualRouter; +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; + +/** + * PORT_CHECK: 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 { + + 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; + + @Inject + private VirtualNetworkApplianceManager virtualNetworkApplianceManager; + + @Inject + private VirtualMachineManager virtualMachineManager; + + @Inject + private NetworkOrchestrationService networkOrchestrationService; + + @Inject + private AgentManager agentManager; + + @Override + public InstanceBootGroupReadinessRule.RuleType getRuleType() { + return InstanceBootGroupReadinessRule.RuleType.PORT_CHECK; + } + + @Override + public Result check(InstanceBootGroupReadinessRule rule, Map details, long vmId) { + String protocol = details == null ? null : details.get(PROTOCOL_KEY); + if (StringUtils.isNotBlank(protocol) && !"tcp".equalsIgnoreCase(protocol)) { + return 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 new Result(InstanceBootGroupReadinessRule.Status.ERROR, "Invalid or missing port detail: " + portValue); + } + + UserVmVO vm = userVmDao.findById(vmId); + if (vm == null) { + return new Result(InstanceBootGroupReadinessRule.Status.ERROR, "VM not found"); + } + + NicVO nic = nicDao.findDefaultNicForVM(vmId); + if (nic == null || StringUtils.isEmpty(nic.getIPv4Address())) { + return new Result(InstanceBootGroupReadinessRule.Status.NOT_READY, "VM has no default NIC/IPv4 address yet"); + } + + NetworkVO network = networkDao.findById(nic.getNetworkId()); + if (network != null && Network.GuestType.L2.equals(network.getGuestType())) { + return 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 new Result(InstanceBootGroupReadinessRule.Status.NOT_READY, "No running VR found for the VM's default network"); + } + + InstanceReadinessCheckCommand command = new InstanceReadinessCheckCommand( + InstanceReadinessCheckCommand.CHECK_TYPE_PORT_CHECK, nic.getIPv4Address(), port, + virtualMachineManager.getExecuteInSequence(router.getHypervisorType())); + Map accessDetails = networkOrchestrationService.getSystemVMAccessDetails(router); + if (StringUtils.isEmpty(accessDetails.get(NetworkElementCommand.ROUTER_IP))) { + return new Result(InstanceBootGroupReadinessRule.Status.ERROR, "Unable to determine the VR's control IP"); + } + command.setAccessDetail(accessDetails); + + Answer answer; + try { + answer = agentManager.easySend(router.getHostId(), command); + } catch (Exception e) { + return new Result(InstanceBootGroupReadinessRule.Status.ERROR, "Failed to dispatch port check via VR: " + e.getMessage()); + } + if (answer == null) { + return 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 new Result(InstanceBootGroupReadinessRule.Status.READY, "port " + port + "/tcp is open"); + } + return new Result(InstanceBootGroupReadinessRule.Status.NOT_READY, "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 index 1c2bc70407ea..ffabca3c8507 100644 --- 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 @@ -22,10 +22,6 @@ import javax.inject.Inject; -import org.apache.cloudstack.api.ApiConstants; -import org.apache.cloudstack.diagnostics.DiagnosticsAnswer; -import org.apache.cloudstack.diagnostics.DiagnosticsCommand; -import org.apache.cloudstack.diagnostics.DiagnosticsType; import org.apache.cloudstack.engine.orchestration.service.NetworkOrchestrationService; import org.apache.commons.lang3.StringUtils; import org.springframework.stereotype.Component; @@ -46,9 +42,9 @@ import com.cloud.vm.dao.UserVmDao; /** - * VR_PING: reuses the existing diagnostics plumbing ({@code DiagnosticsCommand}/{@code DiagnosticsType.PING}), - * the same path {@code RunDiagnosticsCmd}/{@code DiagnosticsServiceImpl} use, dispatched to the VR of - * the VM's default network. + * 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 { @@ -76,7 +72,7 @@ public class VrPingChecker implements ReadinessChecker { @Override public InstanceBootGroupReadinessRule.RuleType getRuleType() { - return InstanceBootGroupReadinessRule.RuleType.VR_PING; + return InstanceBootGroupReadinessRule.RuleType.PING; } @Override @@ -105,8 +101,9 @@ public Result check(InstanceBootGroupReadinessRule rule, Map det return new Result(InstanceBootGroupReadinessRule.Status.NOT_READY, "No running VR found for the VM's default network"); } - String shellCmd = DiagnosticsType.PING.getValue() + " " + nic.getIPv4Address(); - DiagnosticsCommand command = new DiagnosticsCommand(shellCmd, virtualMachineManager.getExecuteInSequence(router.getHypervisorType())); + InstanceReadinessCheckCommand command = new InstanceReadinessCheckCommand( + InstanceReadinessCheckCommand.CHECK_TYPE_PING, nic.getIPv4Address(), null, + virtualMachineManager.getExecuteInSequence(router.getHypervisorType())); Map accessDetails = networkOrchestrationService.getSystemVMAccessDetails(router); if (StringUtils.isEmpty(accessDetails.get(NetworkElementCommand.ROUTER_IP))) { return new Result(InstanceBootGroupReadinessRule.Status.ERROR, "Unable to determine the VR's control IP"); @@ -123,11 +120,11 @@ public Result check(InstanceBootGroupReadinessRule rule, Map det return new Result(InstanceBootGroupReadinessRule.Status.ERROR, "No answer from the VR's host"); } - Map executionDetails = ((DiagnosticsAnswer) answer).getExecutionDetails(); - String exitCode = executionDetails.get(ApiConstants.EXITCODE); + Map executionDetails = ((InstanceReadinessCheckAnswer) answer).getExecutionDetails(); + String exitCode = executionDetails.get(InstanceReadinessCheckAnswer.EXITCODE); if ("0".equals(exitCode)) { return new Result(InstanceBootGroupReadinessRule.Status.READY, "ping succeeded"); } - return new Result(InstanceBootGroupReadinessRule.Status.NOT_READY, "ping failed: " + executionDetails.get(ApiConstants.STDERR)); + return new Result(InstanceBootGroupReadinessRule.Status.NOT_READY, "ping failed: " + executionDetails.get(InstanceReadinessCheckAnswer.STDERR)); } } 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/ui/src/views/compute/InstanceBootGroupReadinessRulesModal.vue b/ui/src/views/compute/InstanceBootGroupReadinessRulesModal.vue index 33d3f1ca5383..eb796bbf37e6 100644 --- a/ui/src/views/compute/InstanceBootGroupReadinessRulesModal.vue +++ b/ui/src/views/compute/InstanceBootGroupReadinessRulesModal.vue @@ -74,7 +74,6 @@ tcp - udp @@ -140,7 +139,7 @@ export default { computed: { availableRuleTypes () { return this.itemType === 'VirtualMachine' - ? ['VR_PING', 'GUEST_AGENT_LIVENESS', 'PORT_CHECK'] + ? ['PING', 'GUEST_AGENT_LIVENESS', 'PORT_CHECK'] : ['INSTANCE_QUORUM'] } }, From a6d6f27e988589767248886018da66a2005878cd Mon Sep 17 00:00:00 2001 From: Abhishek Kumar Date: Wed, 22 Jul 2026 00:57:18 +0530 Subject: [PATCH 8/9] fix --- ...eateInstanceBootGroupReadinessRuleCmd.java | 2 +- .../InstanceBootGroupMemberResponse.java | 4 +- .../vm/bootgroup/InstanceBootGroupMember.java | 4 ++ .../InstanceBootGroupReadinessRule.java | 20 +++---- .../InstanceReadinessCheckCommand.java | 12 +++- ...tanceBootGroupReadinessRuleDetailsDao.java | 2 +- ...stanceBootGroupReadinessRuleDetailsVO.java | 8 +-- .../InstanceBootGroupApiServiceImpl.java | 31 +++++----- .../InstanceBootGroupManagerImpl.java | 4 +- .../readiness/GuestAgentLivenessChecker.java | 18 +++--- ...anceBootGroupReadinessRuleManagerImpl.java | 60 +++++++++---------- ...InstanceBootGroupReadinessRuleService.java | 2 +- .../bootgroup/readiness/PortCheckChecker.java | 29 +++++---- .../vm/bootgroup/readiness/VrPingChecker.java | 23 ++++--- ui/public/locales/en.json | 6 +- ui/src/core/lazy_lib/icons_use.js | 2 + .../compute/InstanceBootGroupMembersTab.vue | 18 +++--- .../InstanceBootGroupReadinessRulesModal.vue | 27 +++++---- 18 files changed, 145 insertions(+), 127 deletions(-) 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 index 70cf303f108f..0a8f6e1cf7e9 100644 --- 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 @@ -65,7 +65,7 @@ public class CreateInstanceBootGroupReadinessRuleCmd extends BaseCmd implements private Long instanceGroupId; @Parameter(name = ApiConstants.RULE_TYPE, type = CommandType.STRING, required = true, - description = "The readiness rule type: GUEST_AGENT_LIVENESS, PING, PORT_CHECK, CUSTOM_SCRIPT or INSTANCE_QUORUM") + description = "The readiness rule type: GuestAgentLiveness, Ping, PortCheck, InstanceQuorum or CustomScript") private String ruleType; @Parameter(name = ApiConstants.NAME, type = CommandType.STRING, description = "The name of the readiness rule; auto-generated if omitted") 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 index 79099d23a11d..2c77242d4a12 100644 --- a/api/src/main/java/org/apache/cloudstack/api/response/InstanceBootGroupMemberResponse.java +++ b/api/src/main/java/org/apache/cloudstack/api/response/InstanceBootGroupMemberResponse.java @@ -65,11 +65,11 @@ public class InstanceBootGroupMemberResponse extends BaseResponse { private Date created; @SerializedName(ApiConstants.READINESS_MODE) - @Param(description = "NO_READINESS, CHILD_DEPENDENT or RULE_BASED, computed from whether readiness rules are attached") + @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: READY, NOT_READY, ERROR or UNKNOWN") + @Param(description = "The last cached readiness evaluation") private String readinessStatus; public void setId(String id) { 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 index fc6ce54cee95..ae811ed2f3b2 100644 --- a/api/src/main/java/org/apache/cloudstack/vm/bootgroup/InstanceBootGroupMember.java +++ b/api/src/main/java/org/apache/cloudstack/vm/bootgroup/InstanceBootGroupMember.java @@ -37,4 +37,8 @@ public interface InstanceBootGroupMember extends Identity, InternalIdentity { enum MemberType { VirtualMachine, InstanceGroup } + + enum ReadinessMode { + None, RuleBased, ChildDependent + } } 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 index 45e323fd4229..8647410f9e6a 100644 --- 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 @@ -46,20 +46,20 @@ public interface InstanceBootGroupReadinessRule extends Identity, InternalIdenti Date getCreated(); /** - * Which rule types are valid depends on {@link InstanceBootGroupMember.MemberType}: KVM-only, - * hypervisor-agnostic at the DB/API layer (no qemu/KVM naming stored). PING/PORT_CHECK/ - * CUSTOM_SCRIPT/GUEST_AGENT_LIVENESS apply to VirtualMachine items; INSTANCE_QUORUM and - * (group-scope) CUSTOM_SCRIPT apply to InstanceGroup items. + * 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 { - GUEST_AGENT_LIVENESS, - PING, - PORT_CHECK, - CUSTOM_SCRIPT, - INSTANCE_QUORUM + GuestAgentLiveness, + Ping, + PortCheck, + CustomScript, + MemberQuorum } enum Status { - READY, NOT_READY, ERROR, UNKNOWN + Ready, NotReady, Error, Unknown } } 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 index 8458b4e19213..8ae58304ae82 100644 --- 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 @@ -33,11 +33,17 @@ public class InstanceReadinessCheckCommand extends NetworkElementCommand { private final String checkType; private final String ipAddress; - private final Integer port; + private Integer port; private final boolean executeInSequence; - public InstanceReadinessCheckCommand(String checkType, String ipAddress, Integer port, boolean executeInSequence) { - this.checkType = checkType; + 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; 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 index ec28bd2ef36c..eb835f1578bd 100644 --- a/engine/schema/src/main/java/com/cloud/vm/dao/InstanceBootGroupReadinessRuleDetailsDao.java +++ b/engine/schema/src/main/java/com/cloud/vm/dao/InstanceBootGroupReadinessRuleDetailsDao.java @@ -26,7 +26,7 @@ public interface InstanceBootGroupReadinessRuleDetailsDao extends ResourceDetail /** * Like {@link #listDetailsKeyPairs(long)} but transparently decrypts the {@code script} key - * (CUSTOM_SCRIPT rule content is stored encrypted at rest). + * (CustomScript rule content is stored encrypted at rest). */ Map getDetails(long ruleId); } 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 index 9e905b4647e1..193f7fa64a50 100644 --- 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 @@ -29,7 +29,7 @@ /** * 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 CUSTOM_SCRIPT rules, the same manual encrypt-by-known-key-name pattern used for S3/Swift + * 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. */ @@ -43,7 +43,7 @@ public class InstanceBootGroupReadinessRuleDetailsVO implements ResourceDetail { private long id; @Column(name = "rule_id") - private long ruleId; + private long resourceId; @Column(name = "name") private String name; @@ -55,7 +55,7 @@ protected InstanceBootGroupReadinessRuleDetailsVO() { } public InstanceBootGroupReadinessRuleDetailsVO(long ruleId, String name, String value) { - this.ruleId = ruleId; + this.resourceId = ruleId; this.name = name; this.value = value; } @@ -67,7 +67,7 @@ public long getId() { @Override public long getResourceId() { - return ruleId; + return resourceId; } @Override 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 index 091d6117f5ff..59e874dcdff8 100644 --- a/server/src/main/java/org/apache/cloudstack/vm/bootgroup/InstanceBootGroupApiServiceImpl.java +++ b/server/src/main/java/org/apache/cloudstack/vm/bootgroup/InstanceBootGroupApiServiceImpl.java @@ -430,10 +430,13 @@ private InstanceBootGroupMemberResponse createInstanceBootGroupMemberResponse(In private String computeReadinessMode(InstanceBootGroupMember.MemberType itemType, long bootGroupId, long itemId) { boolean hasRules = !instanceBootGroupReadinessRuleDao.listEnabledByItem(bootGroupId, itemType, itemId).isEmpty(); + InstanceBootGroupMember.ReadinessMode readinessMode = InstanceBootGroupMember.ReadinessMode.None; if (hasRules) { - return "RULE_BASED"; + readinessMode = InstanceBootGroupMember.ReadinessMode.RuleBased; + } else if (InstanceBootGroupMember.MemberType.InstanceGroup.equals(itemType)) { + readinessMode = InstanceBootGroupMember.ReadinessMode.ChildDependent; } - return itemType == InstanceBootGroupMember.MemberType.VirtualMachine ? "NO_READINESS" : "CHILD_DEPENDENT"; + return readinessMode.name(); } /** @@ -446,7 +449,7 @@ private InstanceBootGroupReadinessRule.Status computeCachedVmReadinessStatus(lon if (rules.isEmpty()) { UserVmVO vm = userVmDao.findById(vmId); boolean running = vm != null && vm.getState() == com.cloud.vm.VirtualMachine.State.Running; - return running ? InstanceBootGroupReadinessRule.Status.READY : InstanceBootGroupReadinessRule.Status.NOT_READY; + return running ? InstanceBootGroupReadinessRule.Status.Ready : InstanceBootGroupReadinessRule.Status.NotReady; } return combineCachedRuleStatuses(rules); } @@ -457,26 +460,26 @@ private InstanceBootGroupReadinessRule.Status computeCachedInstanceGroupReadines boolean anyNotReady = false; InstanceBootGroupReadinessRule.Status ownStatus = combineCachedRuleStatuses(groupRules); - if (ownStatus == InstanceBootGroupReadinessRule.Status.ERROR) { + if (ownStatus == InstanceBootGroupReadinessRule.Status.Error) { anyError = true; - } else if (ownStatus != InstanceBootGroupReadinessRule.Status.READY) { + } else if (ownStatus != InstanceBootGroupReadinessRule.Status.Ready) { anyNotReady = true; } List memberVms = instanceGroupVMMapDao.listByGroupId(instanceGroupId); for (InstanceGroupVMMapVO memberVm : memberVms) { InstanceBootGroupReadinessRule.Status vmStatus = computeCachedVmReadinessStatus(bootGroupId, memberVm.getInstanceId()); - if (vmStatus == InstanceBootGroupReadinessRule.Status.ERROR) { + if (vmStatus == InstanceBootGroupReadinessRule.Status.Error) { anyError = true; - } else if (vmStatus != InstanceBootGroupReadinessRule.Status.READY) { + } else if (vmStatus != InstanceBootGroupReadinessRule.Status.Ready) { anyNotReady = true; } } if (anyError) { - return InstanceBootGroupReadinessRule.Status.ERROR; + return InstanceBootGroupReadinessRule.Status.Error; } - return anyNotReady ? InstanceBootGroupReadinessRule.Status.NOT_READY : InstanceBootGroupReadinessRule.Status.READY; + return anyNotReady ? InstanceBootGroupReadinessRule.Status.NotReady : InstanceBootGroupReadinessRule.Status.Ready; } private InstanceBootGroupReadinessRule.Status combineCachedRuleStatuses(List rules) { @@ -484,17 +487,17 @@ private InstanceBootGroupReadinessRule.Status combineCachedRuleStatuses(List details, long vmId) { UserVmVO vm = userVmDao.findById(vmId); if (vm == null) { - return new Result(InstanceBootGroupReadinessRule.Status.ERROR, "VM not found"); + return new Result(InstanceBootGroupReadinessRule.Status.Error, "VM not found"); } if (vm.getHypervisorType() != HypervisorType.KVM) { - return new Result(InstanceBootGroupReadinessRule.Status.ERROR, + return new Result(InstanceBootGroupReadinessRule.Status.Error, "Guest agent liveness checks are only supported on KVM; VM's hypervisor is " + vm.getHypervisorType()); } if (vm.getState() != VirtualMachine.State.Running || vm.getHostId() == null) { - return new Result(InstanceBootGroupReadinessRule.Status.NOT_READY, "VM is not running"); + return new Result(InstanceBootGroupReadinessRule.Status.NotReady, "VM is not running"); } CheckGuestAgentLivenessCommand command = new CheckGuestAgentLivenessCommand(vm.getInstanceName()); @@ -69,14 +69,14 @@ public Result check(InstanceBootGroupReadinessRule rule, Map det try { answer = agentManager.easySend(vm.getHostId(), command); } catch (Exception e) { - return new Result(InstanceBootGroupReadinessRule.Status.ERROR, "Failed to dispatch guest agent liveness check: " + e.getMessage()); + return new Result(InstanceBootGroupReadinessRule.Status.Error, "Failed to dispatch guest agent liveness check: " + e.getMessage()); } if (answer == null) { - return new Result(InstanceBootGroupReadinessRule.Status.ERROR, "No answer from the VM's host"); + return new Result(InstanceBootGroupReadinessRule.Status.Error, "No answer from the VM's host"); } if (answer.getResult()) { - return new Result(InstanceBootGroupReadinessRule.Status.READY, "guest agent responded"); + return new Result(InstanceBootGroupReadinessRule.Status.Ready, "guest agent responded"); } - return new Result(InstanceBootGroupReadinessRule.Status.NOT_READY, "guest agent did not respond: " + answer.getDetails()); + return 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 index 386c6cd5fe91..1220bb361a86 100644 --- 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 @@ -54,13 +54,13 @@ public class InstanceBootGroupReadinessRuleManagerImpl implements InstanceBootGr private static final Map> VALID_RULE_TYPES_BY_ITEM_TYPE = Map.of( InstanceBootGroupMember.MemberType.VirtualMachine, EnumSet.of( - InstanceBootGroupReadinessRule.RuleType.GUEST_AGENT_LIVENESS, - InstanceBootGroupReadinessRule.RuleType.PING, - InstanceBootGroupReadinessRule.RuleType.PORT_CHECK, - InstanceBootGroupReadinessRule.RuleType.CUSTOM_SCRIPT), + InstanceBootGroupReadinessRule.RuleType.GuestAgentLiveness, + InstanceBootGroupReadinessRule.RuleType.Ping, + InstanceBootGroupReadinessRule.RuleType.PortCheck, + InstanceBootGroupReadinessRule.RuleType.CustomScript), InstanceBootGroupMember.MemberType.InstanceGroup, EnumSet.of( - InstanceBootGroupReadinessRule.RuleType.CUSTOM_SCRIPT, - InstanceBootGroupReadinessRule.RuleType.INSTANCE_QUORUM)); + InstanceBootGroupReadinessRule.RuleType.CustomScript, + InstanceBootGroupReadinessRule.RuleType.MemberQuorum)); @Inject private InstanceBootGroupReadinessRuleDao instanceBootGroupReadinessRuleDao; @@ -172,7 +172,7 @@ public InstanceBootGroupReadinessRule.Status evaluateVmReadiness(long bootGroupI if (rules.isEmpty()) { UserVmVO vm = userVmDao.findById(vmId); boolean running = vm != null && vm.getState() == VirtualMachine.State.Running; - return running ? InstanceBootGroupReadinessRule.Status.READY : InstanceBootGroupReadinessRule.Status.NOT_READY; + return running ? InstanceBootGroupReadinessRule.Status.Ready : InstanceBootGroupReadinessRule.Status.NotReady; } boolean anyError = false; @@ -182,7 +182,7 @@ public InstanceBootGroupReadinessRule.Status evaluateVmReadiness(long bootGroupI InstanceBootGroupReadinessRule.Status status; String message; if (checker == null) { - status = InstanceBootGroupReadinessRule.Status.ERROR; + status = InstanceBootGroupReadinessRule.Status.Error; message = "No checker implemented yet for rule type " + rule.getRuleType(); } else { Map details = instanceBootGroupReadinessRuleDetailsDao.getDetails(rule.getId()); @@ -192,17 +192,17 @@ public InstanceBootGroupReadinessRule.Status evaluateVmReadiness(long bootGroupI } instanceBootGroupReadinessCheckResultDao.upsert(rule.getId(), status, message, new Date()); - if (status == InstanceBootGroupReadinessRule.Status.ERROR) { + if (status == InstanceBootGroupReadinessRule.Status.Error) { anyError = true; - } else if (status != InstanceBootGroupReadinessRule.Status.READY) { + } else if (status != InstanceBootGroupReadinessRule.Status.Ready) { anyNotReady = true; } } if (anyError) { - return InstanceBootGroupReadinessRule.Status.ERROR; + return InstanceBootGroupReadinessRule.Status.Error; } - return anyNotReady ? InstanceBootGroupReadinessRule.Status.NOT_READY : InstanceBootGroupReadinessRule.Status.READY; + return anyNotReady ? InstanceBootGroupReadinessRule.Status.NotReady : InstanceBootGroupReadinessRule.Status.Ready; } @Override @@ -214,19 +214,18 @@ public InstanceBootGroupReadinessRule.Status evaluateInstanceGroupReadiness(long for (InstanceBootGroupReadinessRuleVO rule : groupRules) { ReadinessChecker.Result result; - if (rule.getRuleType() == InstanceBootGroupReadinessRule.RuleType.INSTANCE_QUORUM) { + if (rule.getRuleType() == InstanceBootGroupReadinessRule.RuleType.MemberQuorum) { Map details = instanceBootGroupReadinessRuleDetailsDao.getDetails(rule.getId()); result = evaluateInstanceQuorum(bootGroupId, instanceGroupId, details); } else { - // e.g. group-scope CUSTOM_SCRIPT — no evaluator implemented yet, ships later - result = new ReadinessChecker.Result(InstanceBootGroupReadinessRule.Status.ERROR, + result = new ReadinessChecker.Result(InstanceBootGroupReadinessRule.Status.Error, "No evaluator implemented yet for rule type " + rule.getRuleType()); } instanceBootGroupReadinessCheckResultDao.upsert(rule.getId(), result.getStatus(), result.getMessage(), new Date()); - if (result.getStatus() == InstanceBootGroupReadinessRule.Status.ERROR) { + if (result.getStatus() == InstanceBootGroupReadinessRule.Status.Error) { anyError = true; - } else if (result.getStatus() != InstanceBootGroupReadinessRule.Status.READY) { + } else if (result.getStatus() != InstanceBootGroupReadinessRule.Status.Ready) { anyNotReady = true; } } @@ -234,17 +233,17 @@ public InstanceBootGroupReadinessRule.Status evaluateInstanceGroupReadiness(long List members = instanceGroupVMMapDao.listByGroupId(instanceGroupId); for (InstanceGroupVMMapVO member : members) { InstanceBootGroupReadinessRule.Status vmStatus = evaluateVmReadiness(bootGroupId, member.getInstanceId()); - if (vmStatus == InstanceBootGroupReadinessRule.Status.ERROR) { + if (vmStatus == InstanceBootGroupReadinessRule.Status.Error) { anyError = true; - } else if (vmStatus != InstanceBootGroupReadinessRule.Status.READY) { + } else if (vmStatus != InstanceBootGroupReadinessRule.Status.Ready) { anyNotReady = true; } } if (anyError) { - return InstanceBootGroupReadinessRule.Status.ERROR; + return InstanceBootGroupReadinessRule.Status.Error; } - return anyNotReady ? InstanceBootGroupReadinessRule.Status.NOT_READY : InstanceBootGroupReadinessRule.Status.READY; + return anyNotReady ? InstanceBootGroupReadinessRule.Status.NotReady : InstanceBootGroupReadinessRule.Status.Ready; } /** @@ -255,11 +254,11 @@ private ReadinessChecker.Result evaluateInstanceQuorum(long bootGroupId, long in List members = instanceGroupVMMapDao.listByGroupId(instanceGroupId); int total = members.size(); if (total == 0) { - return new ReadinessChecker.Result(InstanceBootGroupReadinessRule.Status.NOT_READY, "Instance group has no members"); + return new ReadinessChecker.Result(InstanceBootGroupReadinessRule.Status.NotReady, "Instance group has no members"); } long readyCount = members.stream() - .filter(member -> evaluateVmReadiness(bootGroupId, member.getInstanceId()) == InstanceBootGroupReadinessRule.Status.READY) + .filter(member -> evaluateVmReadiness(bootGroupId, member.getInstanceId()) == InstanceBootGroupReadinessRule.Status.Ready) .count(); String thresholdType = details == null ? null : details.get(THRESHOLD_TYPE_KEY); @@ -275,17 +274,17 @@ private ReadinessChecker.Result evaluateInstanceQuorum(long bootGroupId, long in met = readyCount >= thresholdCount; } } catch (NumberFormatException e) { - return new ReadinessChecker.Result(InstanceBootGroupReadinessRule.Status.ERROR, "Invalid threshold configuration: " + thresholdType + "=" + thresholdValue); + 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); - return new ReadinessChecker.Result(met ? InstanceBootGroupReadinessRule.Status.READY : InstanceBootGroupReadinessRule.Status.NOT_READY, message); + return new ReadinessChecker.Result(met ? InstanceBootGroupReadinessRule.Status.Ready : InstanceBootGroupReadinessRule.Status.NotReady, message); } private void validateRuleTypeSpecificDetails(InstanceBootGroupReadinessRule.RuleType ruleType, Map details) { - if (ruleType == InstanceBootGroupReadinessRule.RuleType.INSTANCE_QUORUM) { + if (ruleType == InstanceBootGroupReadinessRule.RuleType.MemberQuorum) { validateInstanceQuorumDetails(details); - } else if (ruleType == InstanceBootGroupReadinessRule.RuleType.PORT_CHECK) { + } else if (ruleType == InstanceBootGroupReadinessRule.RuleType.PortCheck) { validatePortCheckDetails(details); } } @@ -294,8 +293,7 @@ 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( - "INSTANCE_QUORUM rules require '%s' (COUNT or PERCENTAGE) and '%s' details", THRESHOLD_TYPE_KEY, THRESHOLD_VALUE_KEY)); + 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"); @@ -314,11 +312,11 @@ private void validateInstanceQuorumDetails(Map details) { private void validatePortCheckDetails(Map details) { String protocol = details == null ? null : details.get(PROTOCOL_KEY); if (StringUtils.isNotBlank(protocol) && !"tcp".equalsIgnoreCase(protocol)) { - throw new InvalidParameterValueException("PORT_CHECK rules only support the tcp protocol currently"); + 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("PORT_CHECK rules require a '" + PORT_KEY + "' detail"); + throw new InvalidParameterValueException(InstanceBootGroupReadinessRule.RuleType.PortCheck.name() + " rules require a '" + PORT_KEY + "' detail"); } try { int portValue = Integer.parseInt(port); 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 index 30de50fa3260..e7a18e79381a 100644 --- 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 @@ -48,7 +48,7 @@ InstanceBootGroupReadinessRule createReadinessRule(long bootGroupId, InstanceBoo /** * AND of: all enabled rules where {@code item_type = InstanceGroup, item_id = instanceGroupId} - * (currently only INSTANCE_QUORUM has an evaluator; group-scope CUSTOM_SCRIPT ships later) AND + * (currently only MemberQuorum has an evaluator) AND * every VM currently in the group, each evaluated via {@link #evaluateVmReadiness(long, long)} — * a VM's own rules, if it has any, are fully honored, not reduced to a proxy. */ 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 index 94013580a3b5..74eaeeeb0204 100644 --- 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 @@ -43,7 +43,7 @@ import com.cloud.vm.dao.UserVmDao; /** - * PORT_CHECK: dispatches a dedicated {@link InstanceReadinessCheckCommand} to the VR of + * 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. */ @@ -76,14 +76,14 @@ public class PortCheckChecker implements ReadinessChecker { @Override public InstanceBootGroupReadinessRule.RuleType getRuleType() { - return InstanceBootGroupReadinessRule.RuleType.PORT_CHECK; + return InstanceBootGroupReadinessRule.RuleType.PortCheck; } @Override public Result check(InstanceBootGroupReadinessRule rule, Map details, long vmId) { String protocol = details == null ? null : details.get(PROTOCOL_KEY); if (StringUtils.isNotBlank(protocol) && !"tcp".equalsIgnoreCase(protocol)) { - return new Result(InstanceBootGroupReadinessRule.Status.ERROR, "Only tcp port checks are supported, got: " + protocol); + return new Result(InstanceBootGroupReadinessRule.Status.Error, "Only tcp port checks are supported, got: " + protocol); } String portValue = details == null ? null : details.get(PORT_KEY); @@ -94,22 +94,22 @@ public Result check(InstanceBootGroupReadinessRule rule, Map det throw new NumberFormatException(); } } catch (NumberFormatException e) { - return new Result(InstanceBootGroupReadinessRule.Status.ERROR, "Invalid or missing port detail: " + portValue); + return new Result(InstanceBootGroupReadinessRule.Status.Error, "Invalid or missing port detail: " + portValue); } UserVmVO vm = userVmDao.findById(vmId); if (vm == null) { - return new Result(InstanceBootGroupReadinessRule.Status.ERROR, "VM not found"); + return new Result(InstanceBootGroupReadinessRule.Status.Error, "VM not found"); } NicVO nic = nicDao.findDefaultNicForVM(vmId); if (nic == null || StringUtils.isEmpty(nic.getIPv4Address())) { - return new Result(InstanceBootGroupReadinessRule.Status.NOT_READY, "VM has no default NIC/IPv4 address yet"); + return new Result(InstanceBootGroupReadinessRule.Status.NotReady, "VM has no default NIC/IPv4 address yet"); } NetworkVO network = networkDao.findById(nic.getNetworkId()); if (network != null && Network.GuestType.L2.equals(network.getGuestType())) { - return new Result(InstanceBootGroupReadinessRule.Status.ERROR, "The VM's default network is an L2 network; there is no VR to check the port from"); + return 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()); @@ -118,15 +118,14 @@ public Result check(InstanceBootGroupReadinessRule rule, Map det .findFirst() .orElse(null); if (router == null || router.getHostId() == null) { - return new Result(InstanceBootGroupReadinessRule.Status.NOT_READY, "No running VR found for the VM's default network"); + return new Result(InstanceBootGroupReadinessRule.Status.NotReady, "No running VR found for the VM's default network"); } - InstanceReadinessCheckCommand command = new InstanceReadinessCheckCommand( - InstanceReadinessCheckCommand.CHECK_TYPE_PORT_CHECK, nic.getIPv4Address(), port, + 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 new Result(InstanceBootGroupReadinessRule.Status.ERROR, "Unable to determine the VR's control IP"); + return new Result(InstanceBootGroupReadinessRule.Status.Error, "Unable to determine the VR's control IP"); } command.setAccessDetail(accessDetails); @@ -134,17 +133,17 @@ public Result check(InstanceBootGroupReadinessRule rule, Map det try { answer = agentManager.easySend(router.getHostId(), command); } catch (Exception e) { - return new Result(InstanceBootGroupReadinessRule.Status.ERROR, "Failed to dispatch port check via VR: " + e.getMessage()); + return new Result(InstanceBootGroupReadinessRule.Status.Error, "Failed to dispatch port check via VR: " + e.getMessage()); } if (answer == null) { - return new Result(InstanceBootGroupReadinessRule.Status.ERROR, "No answer from the VR's host"); + return 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 new Result(InstanceBootGroupReadinessRule.Status.READY, "port " + port + "/tcp is open"); + return new Result(InstanceBootGroupReadinessRule.Status.Ready, "port " + port + "/tcp is open"); } - return new Result(InstanceBootGroupReadinessRule.Status.NOT_READY, "port " + port + "/tcp check failed: " + executionDetails.get(InstanceReadinessCheckAnswer.STDERR)); + return new Result(InstanceBootGroupReadinessRule.Status.NotReady, "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 index ffabca3c8507..076aa0194346 100644 --- 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 @@ -72,24 +72,24 @@ public class VrPingChecker implements ReadinessChecker { @Override public InstanceBootGroupReadinessRule.RuleType getRuleType() { - return InstanceBootGroupReadinessRule.RuleType.PING; + return InstanceBootGroupReadinessRule.RuleType.Ping; } @Override public Result check(InstanceBootGroupReadinessRule rule, Map details, long vmId) { UserVmVO vm = userVmDao.findById(vmId); if (vm == null) { - return new Result(InstanceBootGroupReadinessRule.Status.ERROR, "VM not found"); + return new Result(InstanceBootGroupReadinessRule.Status.Error, "VM not found"); } NicVO nic = nicDao.findDefaultNicForVM(vmId); if (nic == null || StringUtils.isEmpty(nic.getIPv4Address())) { - return new Result(InstanceBootGroupReadinessRule.Status.NOT_READY, "VM has no default NIC/IPv4 address yet"); + return new Result(InstanceBootGroupReadinessRule.Status.NotReady, "VM has no default NIC/IPv4 address yet"); } NetworkVO network = networkDao.findById(nic.getNetworkId()); if (network != null && Network.GuestType.L2.equals(network.getGuestType())) { - return new Result(InstanceBootGroupReadinessRule.Status.ERROR, "The VM's default network is an L2 network; there is no VR to ping from"); + return 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()); @@ -98,15 +98,14 @@ public Result check(InstanceBootGroupReadinessRule rule, Map det .findFirst() .orElse(null); if (router == null || router.getHostId() == null) { - return new Result(InstanceBootGroupReadinessRule.Status.NOT_READY, "No running VR found for the VM's default network"); + return new Result(InstanceBootGroupReadinessRule.Status.NotReady, "No running VR found for the VM's default network"); } - InstanceReadinessCheckCommand command = new InstanceReadinessCheckCommand( - InstanceReadinessCheckCommand.CHECK_TYPE_PING, nic.getIPv4Address(), null, + InstanceReadinessCheckCommand command = new InstanceReadinessCheckCommand(nic.getIPv4Address(), virtualMachineManager.getExecuteInSequence(router.getHypervisorType())); Map accessDetails = networkOrchestrationService.getSystemVMAccessDetails(router); if (StringUtils.isEmpty(accessDetails.get(NetworkElementCommand.ROUTER_IP))) { - return new Result(InstanceBootGroupReadinessRule.Status.ERROR, "Unable to determine the VR's control IP"); + return new Result(InstanceBootGroupReadinessRule.Status.Error, "Unable to determine the VR's control IP"); } command.setAccessDetail(accessDetails); @@ -114,17 +113,17 @@ public Result check(InstanceBootGroupReadinessRule rule, Map det try { answer = agentManager.easySend(router.getHostId(), command); } catch (Exception e) { - return new Result(InstanceBootGroupReadinessRule.Status.ERROR, "Failed to dispatch ping via VR: " + e.getMessage()); + return new Result(InstanceBootGroupReadinessRule.Status.Error, "Failed to dispatch ping via VR: " + e.getMessage()); } if (answer == null) { - return new Result(InstanceBootGroupReadinessRule.Status.ERROR, "No answer from the VR's host"); + return 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 new Result(InstanceBootGroupReadinessRule.Status.READY, "ping succeeded"); + return new Result(InstanceBootGroupReadinessRule.Status.Ready, "ping succeeded"); } - return new Result(InstanceBootGroupReadinessRule.Status.NOT_READY, "ping failed: " + executionDetails.get(InstanceReadinessCheckAnswer.STDERR)); + return new Result(InstanceBootGroupReadinessRule.Status.NotReady, "ping failed: " + executionDetails.get(InstanceReadinessCheckAnswer.STDERR)); } } diff --git a/ui/public/locales/en.json b/ui/public/locales/en.json index 2b3d6ab5ccca..9a91c828f46b 100644 --- a/ui/public/locales/en.json +++ b/ui/public/locales/en.json @@ -1252,6 +1252,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", @@ -1687,6 +1688,7 @@ "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", @@ -2002,6 +2004,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", @@ -2017,6 +2020,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", @@ -2155,7 +2159,7 @@ "label.reason": "Reason", "label.rebalance": "Rebalance", "label.readiness": "Readiness", -"label.readiness.mode.child.dependent": "Depends on members", +"label.readiness.mode.member.dependent": "Depends on members", "label.readiness.mode.no.readiness": "No Readiness", "label.readiness.mode.rule.based": "Rule-based", "label.readiness.rules": "Readiness Rules", diff --git a/ui/src/core/lazy_lib/icons_use.js b/ui/src/core/lazy_lib/icons_use.js index 502eb5de0b63..ed7b883ddbb7 100644 --- a/ui/src/core/lazy_lib/icons_use.js +++ b/ui/src/core/lazy_lib/icons_use.js @@ -182,6 +182,7 @@ import { UserSwitchOutlined, UploadOutlined, VerticalAlignBottomOutlined, + VerticalAlignMiddleOutlined, VerticalAlignTopOutlined, WarningOutlined, WifiOutlined, @@ -357,6 +358,7 @@ export default { app.component('UserSwitchOutlined', UserSwitchOutlined) app.component('UploadOutlined', UploadOutlined) app.component('VerticalAlignBottomOutlined', VerticalAlignBottomOutlined) + app.component('VerticalAlignMiddleOutlined', VerticalAlignMiddleOutlined) app.component('VerticalAlignTopOutlined', VerticalAlignTopOutlined) app.component('WarningOutlined', WarningOutlined) app.component('WifiOutlined', WifiOutlined) diff --git a/ui/src/views/compute/InstanceBootGroupMembersTab.vue b/ui/src/views/compute/InstanceBootGroupMembersTab.vue index a63866918b29..f7736ebe6f91 100644 --- a/ui/src/views/compute/InstanceBootGroupMembersTab.vue +++ b/ui/src/views/compute/InstanceBootGroupMembersTab.vue @@ -60,14 +60,14 @@ @@ -107,7 +107,7 @@ @@ -251,17 +251,17 @@ export default { }, readinessStatusColor (status) { switch (status) { - case 'READY': return 'success' - case 'NOT_READY': return 'warning' - case 'ERROR': return 'error' + case 'Ready': return 'success' + case 'NotReady': return 'warning' + case 'Error': return 'error' default: return 'default' } }, readinessModeLabel (mode) { switch (mode) { - case 'NO_READINESS': return this.$t('label.readiness.mode.no.readiness') - case 'CHILD_DEPENDENT': return this.$t('label.readiness.mode.child.dependent') - case 'RULE_BASED': return this.$t('label.readiness.mode.rule.based') + case 'None': return this.$t('label.readiness.mode.no.readiness') + case 'MemberDependent': return this.$t('label.readiness.mode.member.dependent') + case 'RuleBased': return this.$t('label.readiness.mode.rule.based') default: return '' } }, diff --git a/ui/src/views/compute/InstanceBootGroupReadinessRulesModal.vue b/ui/src/views/compute/InstanceBootGroupReadinessRulesModal.vue index eb796bbf37e6..58d65bb18d94 100644 --- a/ui/src/views/compute/InstanceBootGroupReadinessRulesModal.vue +++ b/ui/src/views/compute/InstanceBootGroupReadinessRulesModal.vue @@ -25,6 +25,9 @@ :pagination="false" :loading="tabLoading">