This is an automated email from the ASF dual-hosted git repository.
dahn pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/cloudstack.git
The following commit(s) were added to refs/heads/main by this push:
new 941cc83372f Feature: Safely shutdown cloudstack (#6755)
941cc83372f is described below
commit 941cc83372f356181af5ffc2fcd0aeb33ccc3b3b
Author: David Jumani <[email protected]>
AuthorDate: Wed Apr 12 16:14:14 2023 +0530
Feature: Safely shutdown cloudstack (#6755)
Co-authored-by: dahn <[email protected]>
---
.../org/apache/cloudstack/api/ApiConstants.java | 5 +
.../api/command/user/job/ListAsyncJobsCmd.java | 8 +
.../cloudstack/api/response/AsyncJobResponse.java | 34 ++-
.../management/ManagementServerHost.java | 2 +-
client/pom.xml | 5 +
engine/orchestration/pom.xml | 5 +
.../agent/manager/ClusteredAgentManagerImpl.java | 43 +++-
.../resources/META-INF/db/schema-41720to41800.sql | 2 +-
.../resources/META-INF/db/schema-41810to41900.sql | 112 +++++++++
.../apache/cloudstack/framework/jobs/AsyncJob.java | 2 +
.../cloudstack/framework/jobs/AsyncJobManager.java | 10 +
.../cloudstack/framework/jobs/dao/AsyncJobDao.java | 4 +
.../framework/jobs/dao/AsyncJobDaoImpl.java | 20 ++
.../framework/jobs/impl/AsyncJobManagerImpl.java | 41 ++++
.../cloudstack/framework/jobs/impl/AsyncJobVO.java | 1 +
plugins/pom.xml | 2 +
plugins/shutdown/pom.xml | 44 ++++
.../api/command/BaseShutdownActionCmd.java | 41 ++--
.../cloudstack/api/command/CancelShutdownCmd.java | 62 +++++
.../api/command/PrepareForShutdownCmd.java | 61 +++++
.../api/command/ReadyForShutdownCmd.java | 80 +++++++
.../cloudstack/api/command/TriggerShutdownCmd.java | 64 +++++
.../api/response/ReadyForShutdownResponse.java | 81 +++++++
.../cloudstack/shutdown/ShutdownManager.java | 60 +++++
.../cloudstack/shutdown/ShutdownManagerImpl.java | 265 +++++++++++++++++++++
.../BaseShutdownManagementServerHostCommand.java | 26 +-
.../CancelShutdownManagementServerHostCommand.java | 19 +-
...pareForShutdownManagementServerHostCommand.java | 20 +-
...TriggerShutdownManagementServerHostCommand.java | 20 +-
.../META-INF/cloudstack/shutdown/module.properties | 18 ++
.../shutdown/spring-shutdown-context.xml | 29 +++
.../shutdown/ShutdownManagerImplTest.java | 78 ++++++
.../src/main/java/com/cloud/api/ApiDispatcher.java | 11 +-
.../java/com/cloud/api/query/QueryManagerImpl.java | 15 ++
.../cloud/api/query/dao/AsyncJobJoinDaoImpl.java | 6 +
.../com/cloud/api/query/vo/AsyncJobJoinVO.java | 7 +
.../core/spring-server-core-managers-context.xml | 41 ++--
test/integration/smoke/test_safe_shutdown.py | 120 ++++++++++
tools/apidoc/gen_toc.py | 3 +-
ui/public/locales/en.json | 12 +-
ui/src/components/page/GlobalLayout.vue | 185 ++++++++------
ui/src/config/section/infra/managementServers.js | 43 ++++
ui/src/store/getters.js | 1 +
ui/src/store/modules/app.js | 6 +
ui/src/store/modules/user.js | 4 +
ui/src/views/AutogenView.vue | 80 ++++---
ui/src/views/infra/AsyncJobsTab.vue | 104 ++++++++
ui/src/views/infra/Confirmation.vue | 129 ++++++++++
48 files changed, 1805 insertions(+), 226 deletions(-)
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 6e93d45abdb..c1438251b85 100644
--- a/api/src/main/java/org/apache/cloudstack/api/ApiConstants.java
+++ b/api/src/main/java/org/apache/cloudstack/api/ApiConstants.java
@@ -523,6 +523,7 @@ public class ApiConstants {
public static final String PRIVATE_NETWORK_ID = "privatenetworkid";
public static final String ALLOCATION_STATE = "allocationstate";
public static final String MANAGED_STATE = "managedstate";
+ public static final String MANAGEMENT_SERVER_ID = "managementserverid";
public static final String STORAGE_ID = "storageid";
public static final String PING_STORAGE_SERVER_IP = "pingstorageserverip";
public static final String PING_DIR = "pingdir";
@@ -1018,6 +1019,10 @@ public class ApiConstants {
public static final String LOGOUT = "logout";
public static final String LIST_IDPS = "listIdps";
+ public static final String READY_FOR_SHUTDOWN = "readyforshutdown";
+ public static final String SHUTDOWN_TRIGGERED = "shutdowntriggered";
+ public static final String PENDING_JOBS_COUNT = "pendingjobscount";
+
public static final String PUBLIC_MTU = "publicmtu";
public static final String PRIVATE_MTU = "privatemtu";
public static final String MTU = "mtu";
diff --git
a/api/src/main/java/org/apache/cloudstack/api/command/user/job/ListAsyncJobsCmd.java
b/api/src/main/java/org/apache/cloudstack/api/command/user/job/ListAsyncJobsCmd.java
index d2574ff442e..783d78fdce3 100644
---
a/api/src/main/java/org/apache/cloudstack/api/command/user/job/ListAsyncJobsCmd.java
+++
b/api/src/main/java/org/apache/cloudstack/api/command/user/job/ListAsyncJobsCmd.java
@@ -24,6 +24,7 @@ import org.apache.cloudstack.api.BaseListAccountResourcesCmd;
import org.apache.cloudstack.api.Parameter;
import org.apache.cloudstack.api.response.AsyncJobResponse;
import org.apache.cloudstack.api.response.ListResponse;
+import org.apache.cloudstack.api.response.ManagementServerResponse;
@APICommand(name = "listAsyncJobs", description = "Lists all pending
asynchronous jobs for the account.", responseObject = AsyncJobResponse.class,
requestHasSensitiveInfo = false, responseHasSensitiveInfo = false)
@@ -36,6 +37,9 @@ public class ListAsyncJobsCmd extends
BaseListAccountResourcesCmd {
@Parameter(name = ApiConstants.START_DATE, type = CommandType.DATE,
description = "The start date of the async job (use format
\"yyyy-MM-dd'T'HH:mm:ss'+'SSSS\")")
private Date startDate;
+ @Parameter(name = ApiConstants.MANAGEMENT_SERVER_ID, type =
CommandType.UUID, entityType = ManagementServerResponse.class, description =
"The id of the management server", since="4.19")
+ private Long managementServerId;
+
/////////////////////////////////////////////////////
/////////////////// Accessors ///////////////////////
/////////////////////////////////////////////////////
@@ -44,6 +48,10 @@ public class ListAsyncJobsCmd extends
BaseListAccountResourcesCmd {
return startDate;
}
+ public Long getManagementServerId() {
+ return managementServerId;
+ }
+
/////////////////////////////////////////////////////
/////////////// API Implementation///////////////////
/////////////////////////////////////////////////////
diff --git
a/api/src/main/java/org/apache/cloudstack/api/response/AsyncJobResponse.java
b/api/src/main/java/org/apache/cloudstack/api/response/AsyncJobResponse.java
index eecd6be6c52..3eeaaef2afa 100644
--- a/api/src/main/java/org/apache/cloudstack/api/response/AsyncJobResponse.java
+++ b/api/src/main/java/org/apache/cloudstack/api/response/AsyncJobResponse.java
@@ -32,9 +32,21 @@ import com.cloud.serializer.Param;
public class AsyncJobResponse extends BaseResponse {
@SerializedName("accountid")
- @Param(description = "the account that executed the async command")
+ @Param(description = "the account id that executed the async command")
private String accountId;
+ @SerializedName("account")
+ @Param(description = "the account that executed the async command")
+ private String account;
+
+ @SerializedName("domainid")
+ @Param(description = "the domain id that executed the async command")
+ private String domainid;
+
+ @SerializedName("domainpath")
+ @Param(description = "the domain that executed the async command")
+ private String domainPath;
+
@SerializedName(ApiConstants.USER_ID)
@Param(description = "the user that executed the async command")
private String userId;
@@ -71,6 +83,10 @@ public class AsyncJobResponse extends BaseResponse {
@Param(description = "the unique ID of the instance/entity object related
to the job")
private String jobInstanceId;
+ @SerializedName("managementserverid")
+ @Param(description = "the msid of the management server on which the job
is running", since = "4.19")
+ private Long msid;
+
@SerializedName(ApiConstants.CREATED)
@Param(description = " the created date of the job")
private Date created;
@@ -83,6 +99,18 @@ public class AsyncJobResponse extends BaseResponse {
this.accountId = accountId;
}
+ public void setAccount(String account) {
+ this.account = account;
+ }
+
+ public void setDomainId(String domainid) {
+ this.domainid = domainid;
+ }
+
+ public void setDomainPath(String domainPath) {
+ this.domainPath = domainPath;
+ }
+
public void setUserId(String userId) {
this.userId = userId;
}
@@ -127,4 +155,8 @@ public class AsyncJobResponse extends BaseResponse {
public void setRemoved(final Date removed) {
this.removed = removed;
}
+
+ public void setMsid(Long msid) {
+ this.msid = msid;
+ }
}
diff --git
a/api/src/main/java/org/apache/cloudstack/management/ManagementServerHost.java
b/api/src/main/java/org/apache/cloudstack/management/ManagementServerHost.java
index 0159fb2e791..834291ef21c 100644
---
a/api/src/main/java/org/apache/cloudstack/management/ManagementServerHost.java
+++
b/api/src/main/java/org/apache/cloudstack/management/ManagementServerHost.java
@@ -21,7 +21,7 @@ import org.apache.cloudstack.api.InternalIdentity;
public interface ManagementServerHost extends InternalIdentity, Identity {
enum State {
- Up, Down
+ Up, Down, PreparingToShutDown, ReadyToShutDown, ShuttingDown
}
long getMsid();
diff --git a/client/pom.xml b/client/pom.xml
index 0ff5c3203b9..a548d676a67 100644
--- a/client/pom.xml
+++ b/client/pom.xml
@@ -552,6 +552,11 @@
<artifactId>cloud-plugin-integrations-kubernetes-service</artifactId>
<version>${project.version}</version>
</dependency>
+ <dependency>
+ <groupId>org.apache.cloudstack</groupId>
+ <artifactId>cloud-plugin-shutdown</artifactId>
+ <version>${project.version}</version>
+ </dependency>
</dependencies>
<build>
<plugins>
diff --git a/engine/orchestration/pom.xml b/engine/orchestration/pom.xml
index 2cd17519e0e..e5eeb88f6c5 100755
--- a/engine/orchestration/pom.xml
+++ b/engine/orchestration/pom.xml
@@ -68,6 +68,11 @@
<artifactId>cloud-server</artifactId>
<version>${project.version}</version>
</dependency>
+ <dependency>
+ <groupId>org.apache.cloudstack</groupId>
+ <artifactId>cloud-plugin-shutdown</artifactId>
+ <version>${project.version}</version>
+ </dependency>
</dependencies>
<build>
<plugins>
diff --git
a/engine/orchestration/src/main/java/com/cloud/agent/manager/ClusteredAgentManagerImpl.java
b/engine/orchestration/src/main/java/com/cloud/agent/manager/ClusteredAgentManagerImpl.java
index 749a738e63e..bd4e259a788 100644
---
a/engine/orchestration/src/main/java/com/cloud/agent/manager/ClusteredAgentManagerImpl.java
+++
b/engine/orchestration/src/main/java/com/cloud/agent/manager/ClusteredAgentManagerImpl.java
@@ -50,6 +50,11 @@ import org.apache.cloudstack.ha.dao.HAConfigDao;
import org.apache.cloudstack.managed.context.ManagedContextRunnable;
import org.apache.cloudstack.managed.context.ManagedContextTimerTask;
import org.apache.cloudstack.outofbandmanagement.dao.OutOfBandManagementDao;
+import org.apache.cloudstack.shutdown.ShutdownManager;
+import
org.apache.cloudstack.shutdown.command.CancelShutdownManagementServerHostCommand;
+import
org.apache.cloudstack.shutdown.command.PrepareForShutdownManagementServerHostCommand;
+import
org.apache.cloudstack.shutdown.command.BaseShutdownManagementServerHostCommand;
+import
org.apache.cloudstack.shutdown.command.TriggerShutdownManagementServerHostCommand;
import org.apache.cloudstack.utils.identity.ManagementServerNode;
import org.apache.cloudstack.utils.security.SSLUtils;
import org.apache.log4j.Logger;
@@ -129,6 +134,8 @@ public class ClusteredAgentManagerImpl extends
AgentManagerImpl implements Clust
private HAConfigDao haConfigDao;
@Inject
private CAManager caService;
+ @Inject
+ private ShutdownManager shutdownManager;
protected ClusteredAgentManagerImpl() {
super();
@@ -1341,8 +1348,10 @@ public class ClusteredAgentManagerImpl extends
AgentManagerImpl implements Clust
return _gson.toJson(answers);
} else if (cmds.length == 1 && cmds[0] instanceof
ScheduleHostScanTaskCommand) {
final ScheduleHostScanTaskCommand cmd =
(ScheduleHostScanTaskCommand)cmds[0];
- final String response = handleScheduleHostScanTaskCommand(cmd);
- return response;
+ return handleScheduleHostScanTaskCommand(cmd);
+ } else if (cmds.length == 1 && cmds[0] instanceof
BaseShutdownManagementServerHostCommand) {
+ final BaseShutdownManagementServerHostCommand cmd =
(BaseShutdownManagementServerHostCommand)cmds[0];
+ return handleShutdownManagementServerHostCommand(cmd);
}
try {
@@ -1376,6 +1385,36 @@ public class ClusteredAgentManagerImpl extends
AgentManagerImpl implements Clust
return null;
}
+ private String
handleShutdownManagementServerHostCommand(BaseShutdownManagementServerHostCommand
cmd) {
+ if (cmd instanceof PrepareForShutdownManagementServerHostCommand) {
+ s_logger.debug("Received
BaseShutdownManagementServerHostCommand - preparing to shut down");
+ try {
+ shutdownManager.prepareForShutdown();
+ return "Successfully prepared for shutdown";
+ } catch(CloudRuntimeException e) {
+ return e.getMessage();
+ }
+ }
+ if (cmd instanceof TriggerShutdownManagementServerHostCommand) {
+ s_logger.debug("Received
TriggerShutdownManagementServerHostCommand - triggering a shut down");
+ try {
+ shutdownManager.triggerShutdown();
+ return "Successfully triggered shutdown";
+ } catch(CloudRuntimeException e) {
+ return e.getMessage();
+ }
+ }
+ if (cmd instanceof CancelShutdownManagementServerHostCommand) {
+ s_logger.debug("Received
CancelShutdownManagementServerHostCommand - cancelling shut down");
+ try {
+ shutdownManager.cancelShutdown();
+ return "Successfully prepared for shutdown";
+ } catch(CloudRuntimeException e) {
+ return e.getMessage();
+ }
+ }
+ throw new CloudRuntimeException("Unknown
BaseShutdownManagementServerHostCommand command received : " + cmd);
+ }
}
public boolean executeAgentUserRequest(final long agentId, final Event
event) throws AgentUnavailableException {
diff --git
a/engine/schema/src/main/resources/META-INF/db/schema-41720to41800.sql
b/engine/schema/src/main/resources/META-INF/db/schema-41720to41800.sql
index b8eee33cad7..f5efb07dd77 100644
--- a/engine/schema/src/main/resources/META-INF/db/schema-41720to41800.sql
+++ b/engine/schema/src/main/resources/META-INF/db/schema-41720to41800.sql
@@ -1582,4 +1582,4 @@ UPDATE
SET
usage_type = 22
WHERE
- usage_type = 24 AND usage_display like '% io write';
\ No newline at end of file
+ usage_type = 24 AND usage_display like '% io write';
diff --git
a/engine/schema/src/main/resources/META-INF/db/schema-41810to41900.sql
b/engine/schema/src/main/resources/META-INF/db/schema-41810to41900.sql
index 5e13b1c0157..541d3dbacac 100644
--- a/engine/schema/src/main/resources/META-INF/db/schema-41810to41900.sql
+++ b/engine/schema/src/main/resources/META-INF/db/schema-41810to41900.sql
@@ -19,3 +19,115 @@
-- Schema upgrade from 4.18.1.0 to 4.19.0.0
--;
+ALTER TABLE `cloud`.`mshost` MODIFY COLUMN `state` varchar(25);
+
+DROP VIEW IF EXISTS `cloud`.`async_job_view`;
+CREATE VIEW `cloud`.`async_job_view` AS
+ select
+ 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,
+ user.id user_id,
+ user.uuid user_uuid,
+ async_job.id,
+ async_job.uuid,
+ async_job.job_cmd,
+ async_job.job_status,
+ async_job.job_process_status,
+ async_job.job_result_code,
+ async_job.job_result,
+ async_job.created,
+ async_job.removed,
+ async_job.instance_type,
+ async_job.instance_id,
+ async_job.job_executing_msid,
+ CASE
+ WHEN async_job.instance_type = 'Volume' THEN volumes.uuid
+ WHEN
+ async_job.instance_type = 'Template'
+ or async_job.instance_type = 'Iso'
+ THEN
+ vm_template.uuid
+ WHEN
+ async_job.instance_type = 'VirtualMachine'
+ or async_job.instance_type = 'ConsoleProxy'
+ or async_job.instance_type = 'SystemVm'
+ or async_job.instance_type = 'DomainRouter'
+ THEN
+ vm_instance.uuid
+ WHEN async_job.instance_type = 'Snapshot' THEN snapshots.uuid
+ WHEN async_job.instance_type = 'Host' THEN host.uuid
+ WHEN async_job.instance_type = 'StoragePool' THEN storage_pool.uuid
+ WHEN async_job.instance_type = 'IpAddress' THEN
user_ip_address.uuid
+ WHEN async_job.instance_type = 'SecurityGroup' THEN
security_group.uuid
+ WHEN async_job.instance_type = 'PhysicalNetwork' THEN
physical_network.uuid
+ WHEN async_job.instance_type = 'TrafficType' THEN
physical_network_traffic_types.uuid
+ WHEN async_job.instance_type = 'PhysicalNetworkServiceProvider'
THEN physical_network_service_providers.uuid
+ WHEN async_job.instance_type = 'FirewallRule' THEN
firewall_rules.uuid
+ WHEN async_job.instance_type = 'Account' THEN acct.uuid
+ WHEN async_job.instance_type = 'User' THEN us.uuid
+ WHEN async_job.instance_type = 'StaticRoute' THEN
static_routes.uuid
+ WHEN async_job.instance_type = 'PrivateGateway' THEN
vpc_gateways.uuid
+ WHEN async_job.instance_type = 'Counter' THEN counter.uuid
+ WHEN async_job.instance_type = 'Condition' THEN conditions.uuid
+ WHEN async_job.instance_type = 'AutoScalePolicy' THEN
autoscale_policies.uuid
+ WHEN async_job.instance_type = 'AutoScaleVmProfile' THEN
autoscale_vmprofiles.uuid
+ WHEN async_job.instance_type = 'AutoScaleVmGroup' THEN
autoscale_vmgroups.uuid
+ ELSE null
+ END instance_uuid
+ from
+ `cloud`.`async_job`
+ left join
+ `cloud`.`account` ON async_job.account_id = account.id
+ left join
+ `cloud`.`domain` ON domain.id = account.domain_id
+ left join
+ `cloud`.`user` ON async_job.user_id = user.id
+ left join
+ `cloud`.`volumes` ON async_job.instance_id = volumes.id
+ left join
+ `cloud`.`vm_template` ON async_job.instance_id = vm_template.id
+ left join
+ `cloud`.`vm_instance` ON async_job.instance_id = vm_instance.id
+ left join
+ `cloud`.`snapshots` ON async_job.instance_id = snapshots.id
+ left join
+ `cloud`.`host` ON async_job.instance_id = host.id
+ left join
+ `cloud`.`storage_pool` ON async_job.instance_id = storage_pool.id
+ left join
+ `cloud`.`user_ip_address` ON async_job.instance_id = user_ip_address.id
+ left join
+ `cloud`.`security_group` ON async_job.instance_id = security_group.id
+ left join
+ `cloud`.`physical_network` ON async_job.instance_id =
physical_network.id
+ left join
+ `cloud`.`physical_network_traffic_types` ON async_job.instance_id =
physical_network_traffic_types.id
+ left join
+ `cloud`.`physical_network_service_providers` ON async_job.instance_id
= physical_network_service_providers.id
+ left join
+ `cloud`.`firewall_rules` ON async_job.instance_id = firewall_rules.id
+ left join
+ `cloud`.`account` acct ON async_job.instance_id = acct.id
+ left join
+ `cloud`.`user` us ON async_job.instance_id = us.id
+ left join
+ `cloud`.`static_routes` ON async_job.instance_id = static_routes.id
+ left join
+ `cloud`.`vpc_gateways` ON async_job.instance_id = vpc_gateways.id
+ left join
+ `cloud`.`counter` ON async_job.instance_id = counter.id
+ left join
+ `cloud`.`conditions` ON async_job.instance_id = conditions.id
+ left join
+ `cloud`.`autoscale_policies` ON async_job.instance_id =
autoscale_policies.id
+ left join
+ `cloud`.`autoscale_vmprofiles` ON async_job.instance_id =
autoscale_vmprofiles.id
+ left join
+ `cloud`.`autoscale_vmgroups` ON async_job.instance_id =
autoscale_vmgroups.id;
+
diff --git
a/framework/jobs/src/main/java/org/apache/cloudstack/framework/jobs/AsyncJob.java
b/framework/jobs/src/main/java/org/apache/cloudstack/framework/jobs/AsyncJob.java
index b8200bf8221..bde9b4af167 100644
---
a/framework/jobs/src/main/java/org/apache/cloudstack/framework/jobs/AsyncJob.java
+++
b/framework/jobs/src/main/java/org/apache/cloudstack/framework/jobs/AsyncJob.java
@@ -90,6 +90,8 @@ public interface AsyncJob extends JobInfo {
@Override
Long getExecutingMsid();
+ void setExecutingMsid(Long msid);
+
@Override
Long getCompleteMsid();
diff --git
a/framework/jobs/src/main/java/org/apache/cloudstack/framework/jobs/AsyncJobManager.java
b/framework/jobs/src/main/java/org/apache/cloudstack/framework/jobs/AsyncJobManager.java
index 8542407524b..52ef10a4adc 100644
---
a/framework/jobs/src/main/java/org/apache/cloudstack/framework/jobs/AsyncJobManager.java
+++
b/framework/jobs/src/main/java/org/apache/cloudstack/framework/jobs/AsyncJobManager.java
@@ -133,4 +133,14 @@ public interface AsyncJobManager extends Manager {
List<AsyncJobVO> findFailureAsyncJobs(String... cmds);
long countPendingJobs(String havingInfo, String... cmds);
+
+ // Returns the number of pending jobs for the given Management server
msids.
+ // NOTE: This is the msid and NOT the id
+ long countPendingNonPseudoJobs(Long... msIds);
+
+ void enableAsyncJobs();
+
+ void disableAsyncJobs();
+
+ boolean isAsyncJobsEnabled();
}
diff --git
a/framework/jobs/src/main/java/org/apache/cloudstack/framework/jobs/dao/AsyncJobDao.java
b/framework/jobs/src/main/java/org/apache/cloudstack/framework/jobs/dao/AsyncJobDao.java
index 2696e105cce..9f7a4ad6e05 100644
---
a/framework/jobs/src/main/java/org/apache/cloudstack/framework/jobs/dao/AsyncJobDao.java
+++
b/framework/jobs/src/main/java/org/apache/cloudstack/framework/jobs/dao/AsyncJobDao.java
@@ -46,4 +46,8 @@ public interface AsyncJobDao extends GenericDao<AsyncJobVO,
Long> {
List<AsyncJobVO> getFailureJobsSinceLastMsStart(long msId, String... cmds);
long countPendingJobs(String havingInfo, String... cmds);
+
+ // Returns the number of pending jobs for the given Management server
msids.
+ // NOTE: This is the msid and NOT the id
+ long countPendingNonPseudoJobs(Long... msIds);
}
diff --git
a/framework/jobs/src/main/java/org/apache/cloudstack/framework/jobs/dao/AsyncJobDaoImpl.java
b/framework/jobs/src/main/java/org/apache/cloudstack/framework/jobs/dao/AsyncJobDaoImpl.java
index 7dd73435093..1914ff71460 100644
---
a/framework/jobs/src/main/java/org/apache/cloudstack/framework/jobs/dao/AsyncJobDaoImpl.java
+++
b/framework/jobs/src/main/java/org/apache/cloudstack/framework/jobs/dao/AsyncJobDaoImpl.java
@@ -48,6 +48,7 @@ public class AsyncJobDaoImpl extends
GenericDaoBase<AsyncJobVO, Long> implements
private final SearchBuilder<AsyncJobVO> expiringCompletedAsyncJobSearch;
private final SearchBuilder<AsyncJobVO> failureMsidAsyncJobSearch;
private final GenericSearchBuilder<AsyncJobVO, Long> asyncJobTypeSearch;
+ private final GenericSearchBuilder<AsyncJobVO, Long>
pendingNonPseudoAsyncJobsSearch;
public AsyncJobDaoImpl() {
pendingAsyncJobSearch = createSearchBuilder();
@@ -103,6 +104,11 @@ public class AsyncJobDaoImpl extends
GenericDaoBase<AsyncJobVO, Long> implements
asyncJobTypeSearch.and("status",
asyncJobTypeSearch.entity().getStatus(), SearchCriteria.Op.EQ);
asyncJobTypeSearch.done();
+ pendingNonPseudoAsyncJobsSearch = createSearchBuilder(Long.class);
+ pendingNonPseudoAsyncJobsSearch.select(null,
SearchCriteria.Func.COUNT, pendingNonPseudoAsyncJobsSearch.entity().getId());
+ pendingNonPseudoAsyncJobsSearch.and("instanceTypeNEQ",
pendingNonPseudoAsyncJobsSearch.entity().getInstanceType(),
SearchCriteria.Op.NEQ);
+ pendingNonPseudoAsyncJobsSearch.and("jobStatusEQ",
pendingNonPseudoAsyncJobsSearch.entity().getStatus(), SearchCriteria.Op.EQ);
+ pendingNonPseudoAsyncJobsSearch.and("executingMsidIN",
pendingNonPseudoAsyncJobsSearch.entity().getExecutingMsid(),
SearchCriteria.Op.IN);
}
@Override
@@ -237,6 +243,20 @@ public class AsyncJobDaoImpl extends
GenericDaoBase<AsyncJobVO, Long> implements
return listBy(sc);
}
+ // Returns the number of pending jobs for the given Management server
msids.
+ // NOTE: This is the msid and NOT the id
+ @Override
+ public long countPendingNonPseudoJobs(Long... msIds) {
+ SearchCriteria<Long> sc = pendingNonPseudoAsyncJobsSearch.create();
+ sc.setParameters("instanceTypeNEQ",
AsyncJobVO.PSEUDO_JOB_INSTANCE_TYPE);
+ sc.setParameters("jobStatusEQ", JobInfo.Status.IN_PROGRESS);
+ if (msIds != null) {
+ sc.setParameters("executingMsidIN", (Object[])msIds);
+ }
+ List<Long> results = customSearch(sc, null);
+ return results.get(0);
+ }
+
@Override
public long countPendingJobs(String havingInfo, String... cmds) {
SearchCriteria<Long> sc = asyncJobTypeSearch.create();
diff --git
a/framework/jobs/src/main/java/org/apache/cloudstack/framework/jobs/impl/AsyncJobManagerImpl.java
b/framework/jobs/src/main/java/org/apache/cloudstack/framework/jobs/impl/AsyncJobManagerImpl.java
index a963357122e..0546d998913 100644
---
a/framework/jobs/src/main/java/org/apache/cloudstack/framework/jobs/impl/AsyncJobManagerImpl.java
+++
b/framework/jobs/src/main/java/org/apache/cloudstack/framework/jobs/impl/AsyncJobManagerImpl.java
@@ -64,6 +64,7 @@ import org.apache.log4j.MDC;
import com.cloud.cluster.ClusterManagerListener;
import org.apache.cloudstack.management.ManagementServerHost;
+
import com.cloud.storage.DataStoreRole;
import com.cloud.storage.Snapshot;
import com.cloud.storage.dao.SnapshotDao;
@@ -155,6 +156,8 @@ public class AsyncJobManagerImpl extends ManagerBase
implements AsyncJobManager,
private ExecutorService _apiJobExecutor;
private ExecutorService _workerJobExecutor;
+ private boolean asyncJobsEnabled = true;
+
@Override
public String getConfigComponentName() {
return AsyncJobManager.class.getSimpleName();
@@ -197,12 +200,21 @@ public class AsyncJobManagerImpl extends ManagerBase
implements AsyncJobManager,
return submitAsyncJob(job, false);
}
+ private void checkShutdown() {
+ if (!isAsyncJobsEnabled()) {
+ throw new CloudRuntimeException("A shutdown has been triggered.
Can not accept new jobs");
+ }
+ }
+
@SuppressWarnings("unchecked")
@DB
public long submitAsyncJob(AsyncJob job, boolean
scheduleJobExecutionInContext) {
+ checkShutdown();
+
@SuppressWarnings("rawtypes")
GenericDao dao = GenericDaoBase.getDao(job.getClass());
job.setInitMsid(getMsid());
+ job.setExecutingMsid(getMsid());
job.setSyncSource(null); // no sync source originally
dao.persist(job);
@@ -218,6 +230,8 @@ public class AsyncJobManagerImpl extends ManagerBase
implements AsyncJobManager,
@Override
@DB
public long submitAsyncJob(final AsyncJob job, final String syncObjType,
final long syncObjId) {
+ checkShutdown();
+
try {
@SuppressWarnings("rawtypes")
final GenericDao dao = GenericDaoBase.getDao(job.getClass());
@@ -827,6 +841,11 @@ public class AsyncJobManagerImpl extends ManagerBase
implements AsyncJobManager,
protected void reallyRun() {
try {
+ if (!isAsyncJobsEnabled()) {
+ s_logger.info("A shutdown has been triggered. Not
executing any async job");
+ return;
+ }
+
List<SyncQueueItemVO> l =
_queueMgr.dequeueFromAny(getMsid(), MAX_ONETIME_SCHEDULE_SIZE);
if (l != null && l.size() > 0) {
for (SyncQueueItemVO item : l) {
@@ -1171,4 +1190,26 @@ public class AsyncJobManagerImpl extends ManagerBase
implements AsyncJobManager,
public long countPendingJobs(String havingInfo, String... cmds) {
return _jobDao.countPendingJobs(havingInfo, cmds);
}
+
+ // Returns the number of pending jobs for the given Management server
msids.
+ // NOTE: This is the msid and NOT the id
+ @Override
+ public long countPendingNonPseudoJobs(Long... msIds) {
+ return _jobDao.countPendingNonPseudoJobs(msIds);
+ }
+
+ @Override
+ public void enableAsyncJobs() {
+ this.asyncJobsEnabled = true;
+ }
+
+ @Override
+ public void disableAsyncJobs() {
+ this.asyncJobsEnabled = false;
+ }
+
+ @Override
+ public boolean isAsyncJobsEnabled() {
+ return asyncJobsEnabled;
+ }
}
diff --git
a/framework/jobs/src/main/java/org/apache/cloudstack/framework/jobs/impl/AsyncJobVO.java
b/framework/jobs/src/main/java/org/apache/cloudstack/framework/jobs/impl/AsyncJobVO.java
index 8f3c0337837..6b85ae27f58 100644
---
a/framework/jobs/src/main/java/org/apache/cloudstack/framework/jobs/impl/AsyncJobVO.java
+++
b/framework/jobs/src/main/java/org/apache/cloudstack/framework/jobs/impl/AsyncJobVO.java
@@ -294,6 +294,7 @@ public class AsyncJobVO implements AsyncJob, JobInfo {
return executingMsid;
}
+ @Override
public void setExecutingMsid(Long executingMsid) {
this.executingMsid = executingMsid;
}
diff --git a/plugins/pom.xml b/plugins/pom.xml
index 529cf383288..d0661c01a2c 100755
--- a/plugins/pom.xml
+++ b/plugins/pom.xml
@@ -115,6 +115,8 @@
<module>outofbandmanagement-drivers/nested-cloudstack</module>
<module>outofbandmanagement-drivers/redfish</module>
+ <module>shutdown</module>
+
<module>storage/image/default</module>
<module>storage/image/s3</module>
<module>storage/image/sample</module>
diff --git a/plugins/shutdown/pom.xml b/plugins/shutdown/pom.xml
new file mode 100644
index 00000000000..0f34cc08482
--- /dev/null
+++ b/plugins/shutdown/pom.xml
@@ -0,0 +1,44 @@
+<!--
+ Licensed to the Apache Software Foundation (ASF) under one
+ or more contributor license agreements. See the NOTICE file
+ distributed with this work for additional information
+ regarding copyright ownership. The ASF licenses this file
+ to you under the Apache License, Version 2.0 (the
+ "License"); you may not use this file except in compliance
+ with the License. You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing,
+ software distributed under the License is distributed on an
+ "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ KIND, either express or implied. See the License for the
+ specific language governing permissions and limitations
+ under the License.
+-->
+
+<project xmlns="http://maven.apache.org/POM/4.0.0"
+ xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
+ xsi:schemaLocation="http://maven.apache.org/POM/4.0.0
http://maven.apache.org/xsd/maven-4.0.0.xsd">
+ <modelVersion>4.0.0</modelVersion>
+ <artifactId>cloud-plugin-shutdown</artifactId>
+ <name>Apache CloudStack Plugin - Safe Shutdown</name>
+ <parent>
+ <groupId>org.apache.cloudstack</groupId>
+ <artifactId>cloudstack-plugins</artifactId>
+ <version>4.19.0.0-SNAPSHOT</version>
+ <relativePath>../pom.xml</relativePath>
+ </parent>
+ <dependencies>
+ <dependency>
+ <groupId>org.apache.cloudstack</groupId>
+ <artifactId>cloud-api</artifactId>
+ <version>${project.version}</version>
+ </dependency>
+ <dependency>
+ <groupId>org.apache.cloudstack</groupId>
+ <artifactId>cloud-utils</artifactId>
+ <version>${project.version}</version>
+ </dependency>
+ </dependencies>
+</project>
diff --git
a/api/src/main/java/org/apache/cloudstack/api/command/user/job/ListAsyncJobsCmd.java
b/plugins/shutdown/src/main/java/org/apache/cloudstack/api/command/BaseShutdownActionCmd.java
similarity index 50%
copy from
api/src/main/java/org/apache/cloudstack/api/command/user/job/ListAsyncJobsCmd.java
copy to
plugins/shutdown/src/main/java/org/apache/cloudstack/api/command/BaseShutdownActionCmd.java
index d2574ff442e..d7f4953291b 100644
---
a/api/src/main/java/org/apache/cloudstack/api/command/user/job/ListAsyncJobsCmd.java
+++
b/plugins/shutdown/src/main/java/org/apache/cloudstack/api/command/BaseShutdownActionCmd.java
@@ -14,45 +14,36 @@
// 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.job;
-import java.util.Date;
+package org.apache.cloudstack.api.command;
+
+import javax.inject.Inject;
-import org.apache.cloudstack.api.APICommand;
import org.apache.cloudstack.api.ApiConstants;
-import org.apache.cloudstack.api.BaseListAccountResourcesCmd;
+import org.apache.cloudstack.api.BaseCmd;
import org.apache.cloudstack.api.Parameter;
-import org.apache.cloudstack.api.response.AsyncJobResponse;
-import org.apache.cloudstack.api.response.ListResponse;
-@APICommand(name = "listAsyncJobs", description = "Lists all pending
asynchronous jobs for the account.", responseObject = AsyncJobResponse.class,
- requestHasSensitiveInfo = false, responseHasSensitiveInfo = false)
-public class ListAsyncJobsCmd extends BaseListAccountResourcesCmd {
- /////////////////////////////////////////////////////
- //////////////// API parameters /////////////////////
- /////////////////////////////////////////////////////
+import org.apache.cloudstack.api.response.ManagementServerResponse;
+import org.apache.cloudstack.shutdown.ShutdownManager;
+
+public abstract class BaseShutdownActionCmd extends BaseCmd {
- @Parameter(name = ApiConstants.START_DATE, type = CommandType.DATE,
description = "The start date of the async job (use format
\"yyyy-MM-dd'T'HH:mm:ss'+'SSSS\")")
- private Date startDate;
+ @Inject
+ protected ShutdownManager shutdownManager;
/////////////////////////////////////////////////////
- /////////////////// Accessors ///////////////////////
+ //////////////// API parameters /////////////////////
/////////////////////////////////////////////////////
- public Date getStartDate() {
- return startDate;
- }
+ @Parameter(name = ApiConstants.MANAGEMENT_SERVER_ID, type =
CommandType.UUID, entityType = ManagementServerResponse.class, description =
"the uuid of the management server", required = true)
+ private Long managementServerId;
/////////////////////////////////////////////////////
- /////////////// API Implementation///////////////////
+ /////////////////// Accessors ///////////////////////
/////////////////////////////////////////////////////
- @Override
- public void execute() {
-
- ListResponse<AsyncJobResponse> response =
_queryService.searchForAsyncJobs(this);
- response.setResponseName(getCommandName());
- this.setResponseObject(response);
+ public Long getManagementServerId() {
+ return managementServerId;
}
}
diff --git
a/plugins/shutdown/src/main/java/org/apache/cloudstack/api/command/CancelShutdownCmd.java
b/plugins/shutdown/src/main/java/org/apache/cloudstack/api/command/CancelShutdownCmd.java
new file mode 100644
index 00000000000..fe6204fd0cc
--- /dev/null
+++
b/plugins/shutdown/src/main/java/org/apache/cloudstack/api/command/CancelShutdownCmd.java
@@ -0,0 +1,62 @@
+// Licensed to the Apache Software Foundation (ASF) under one
+// or more contributor license agreements. See the NOTICE file
+// distributed with this work for additional information
+// regarding copyright ownership. The ASF licenses this file
+// to you under the Apache License, Version 2.0 (the
+// "License"); you may not use this file except in compliance
+// with the License. You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing,
+// software distributed under the License is distributed on an
+// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+// KIND, either express or implied. See the License for the
+// specific language governing permissions and limitations
+// under the License.
+
+ package org.apache.cloudstack.api.command;
+
+import org.apache.cloudstack.api.APICommand;
+import org.apache.cloudstack.api.BaseCmd;
+import org.apache.log4j.Logger;
+
+import com.cloud.user.Account;
+
+import org.apache.cloudstack.api.response.ReadyForShutdownResponse;
+import org.apache.cloudstack.acl.RoleType;
+
+@APICommand(name = CancelShutdownCmd.APINAME,
+ description = "Cancels a triggered shutdown",
+ since = "4.19.0",
+ responseObject = ReadyForShutdownResponse.class,
+ requestHasSensitiveInfo = false, responseHasSensitiveInfo = false,
+ authorized = {RoleType.Admin})
+
+public class CancelShutdownCmd extends BaseShutdownActionCmd {
+
+ public static final Logger LOG = Logger.getLogger(CancelShutdownCmd.class);
+ public static final String APINAME = "cancelShutdown";
+
+ @Override
+ public String getCommandName() {
+ return APINAME.toLowerCase() + BaseCmd.RESPONSE_SUFFIX;
+ }
+
+ @Override
+ public long getEntityOwnerId() {
+ return Account.ACCOUNT_ID_SYSTEM;
+ }
+
+ /////////////////////////////////////////////////////
+ /////////////// API Implementation///////////////////
+ /////////////////////////////////////////////////////
+
+ @Override
+ public void execute() {
+ final ReadyForShutdownResponse response =
shutdownManager.cancelShutdown(this);
+ response.setResponseName(getCommandName());
+ response.setObjectName("cancelshutdown");
+ setResponseObject(response);
+ }
+}
diff --git
a/plugins/shutdown/src/main/java/org/apache/cloudstack/api/command/PrepareForShutdownCmd.java
b/plugins/shutdown/src/main/java/org/apache/cloudstack/api/command/PrepareForShutdownCmd.java
new file mode 100644
index 00000000000..01ea1797a10
--- /dev/null
+++
b/plugins/shutdown/src/main/java/org/apache/cloudstack/api/command/PrepareForShutdownCmd.java
@@ -0,0 +1,61 @@
+// 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;
+
+
+import org.apache.cloudstack.api.APICommand;
+import org.apache.cloudstack.api.BaseCmd;
+import org.apache.log4j.Logger;
+
+import com.cloud.user.Account;
+
+import org.apache.cloudstack.api.response.ReadyForShutdownResponse;
+import org.apache.cloudstack.acl.RoleType;
+
+@APICommand(name = PrepareForShutdownCmd.APINAME,
+ description = "Prepares CloudStack for a safe manual shutdown by
preventing new jobs from being accepted",
+ since = "4.19.0",
+ responseObject = ReadyForShutdownResponse.class,
+ requestHasSensitiveInfo = false, responseHasSensitiveInfo = false,
+ authorized = {RoleType.Admin})
+public class PrepareForShutdownCmd extends BaseShutdownActionCmd {
+ public static final Logger LOG =
Logger.getLogger(PrepareForShutdownCmd.class);
+ public static final String APINAME = "prepareForShutdown";
+
+ @Override
+ public String getCommandName() {
+ return APINAME.toLowerCase() + BaseCmd.RESPONSE_SUFFIX;
+ }
+
+ @Override
+ public long getEntityOwnerId() {
+ return Account.ACCOUNT_ID_SYSTEM;
+ }
+
+ /////////////////////////////////////////////////////
+ /////////////// API Implementation///////////////////
+ /////////////////////////////////////////////////////
+
+ @Override
+ public void execute() {
+ final ReadyForShutdownResponse response =
shutdownManager.prepareForShutdown(this);
+ response.setResponseName(getCommandName());
+ response.setObjectName("prepareforshutdown");
+ setResponseObject(response);
+ }
+}
diff --git
a/plugins/shutdown/src/main/java/org/apache/cloudstack/api/command/ReadyForShutdownCmd.java
b/plugins/shutdown/src/main/java/org/apache/cloudstack/api/command/ReadyForShutdownCmd.java
new file mode 100644
index 00000000000..d7ab6a24ee6
--- /dev/null
+++
b/plugins/shutdown/src/main/java/org/apache/cloudstack/api/command/ReadyForShutdownCmd.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;
+
+import javax.inject.Inject;
+
+import org.apache.cloudstack.api.APICommand;
+import org.apache.cloudstack.api.ApiConstants;
+import org.apache.cloudstack.api.BaseCmd;
+import org.apache.cloudstack.api.Parameter;
+import org.apache.cloudstack.api.response.ManagementServerResponse;
+import org.apache.cloudstack.api.response.ReadyForShutdownResponse;
+import org.apache.cloudstack.shutdown.ShutdownManager;
+import org.apache.log4j.Logger;
+import com.cloud.user.Account;
+
+@APICommand(name = ReadyForShutdownCmd.APINAME,
+ description = "Returs the status of CloudStack, whether a shutdown
has been triggered and if ready to shutdown",
+ since = "4.19.0",
+ responseObject = ReadyForShutdownResponse.class,
+ requestHasSensitiveInfo = false, responseHasSensitiveInfo = false)
+public class ReadyForShutdownCmd extends BaseCmd {
+ public static final Logger LOG =
Logger.getLogger(ReadyForShutdownCmd.class);
+ public static final String APINAME = "readyForShutdown";
+
+ @Inject
+ private ShutdownManager shutdownManager;
+
+ /////////////////////////////////////////////////////
+ //////////////// API parameters /////////////////////
+ /////////////////////////////////////////////////////
+
+ @Parameter(name = ApiConstants.MANAGEMENT_SERVER_ID, type =
CommandType.UUID, entityType = ManagementServerResponse.class, description =
"the uuid of the management server")
+ private Long managementServerId;
+
+ /////////////////////////////////////////////////////
+ /////////////////// Accessors ///////////////////////
+ /////////////////////////////////////////////////////
+
+ public Long getManagementServerId() {
+ return managementServerId;
+ }
+
+ /////////////////////////////////////////////////////
+ /////////////// API Implementation///////////////////
+ /////////////////////////////////////////////////////
+
+ @Override
+ public void execute() {
+ final ReadyForShutdownResponse response =
shutdownManager.readyForShutdown(this);
+ response.setResponseName(getCommandName());
+ response.setObjectName("readyforshutdown");
+ setResponseObject(response);
+ }
+
+ @Override
+ public String getCommandName() {
+ return APINAME.toLowerCase() + BaseCmd.RESPONSE_SUFFIX;
+ }
+
+ @Override
+ public long getEntityOwnerId() {
+ return Account.ACCOUNT_ID_SYSTEM;
+ }
+}
diff --git
a/plugins/shutdown/src/main/java/org/apache/cloudstack/api/command/TriggerShutdownCmd.java
b/plugins/shutdown/src/main/java/org/apache/cloudstack/api/command/TriggerShutdownCmd.java
new file mode 100644
index 00000000000..3abde0b1f3b
--- /dev/null
+++
b/plugins/shutdown/src/main/java/org/apache/cloudstack/api/command/TriggerShutdownCmd.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.api.command;
+
+import org.apache.cloudstack.api.APICommand;
+import org.apache.cloudstack.api.BaseCmd;
+import org.apache.log4j.Logger;
+
+import com.cloud.user.Account;
+
+import org.apache.cloudstack.api.response.ReadyForShutdownResponse;
+import org.apache.cloudstack.acl.RoleType;
+
+@APICommand(name = TriggerShutdownCmd.APINAME,
+ description = "Triggers an automatic safe shutdown of CloudStack
by not accepting new jobs and shutting down when all pending jobbs have been
completed. Triggers an immediate shutdown if forced",
+ since = "4.19.0",
+ responseObject = ReadyForShutdownResponse.class,
+ requestHasSensitiveInfo = false, responseHasSensitiveInfo = false,
+ authorized = {RoleType.Admin})
+public class TriggerShutdownCmd extends BaseShutdownActionCmd {
+ public static final Logger LOG =
Logger.getLogger(TriggerShutdownCmd.class);
+ public static final String APINAME = "triggerShutdown";
+
+ /////////////////////////////////////////////////////
+ /////////////////// Accessors ///////////////////////
+ /////////////////////////////////////////////////////
+
+ @Override
+ public String getCommandName() {
+ return APINAME.toLowerCase() + BaseCmd.RESPONSE_SUFFIX;
+ }
+
+ @Override
+ public long getEntityOwnerId() {
+ return Account.ACCOUNT_ID_SYSTEM;
+ }
+
+ /////////////////////////////////////////////////////
+ /////////////// API Implementation///////////////////
+ /////////////////////////////////////////////////////
+
+ @Override
+ public void execute() {
+ final ReadyForShutdownResponse response =
shutdownManager.triggerShutdown(this);
+ response.setResponseName(getCommandName());
+ response.setObjectName("triggershutdown");
+ setResponseObject(response);
+ }
+}
diff --git
a/plugins/shutdown/src/main/java/org/apache/cloudstack/api/response/ReadyForShutdownResponse.java
b/plugins/shutdown/src/main/java/org/apache/cloudstack/api/response/ReadyForShutdownResponse.java
new file mode 100644
index 00000000000..d1b2353d2a3
--- /dev/null
+++
b/plugins/shutdown/src/main/java/org/apache/cloudstack/api/response/ReadyForShutdownResponse.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.response;
+
+
+import org.apache.cloudstack.api.ApiConstants;
+import org.apache.cloudstack.api.BaseResponse;
+
+import com.cloud.serializer.Param;
+import com.google.gson.annotations.SerializedName;
+
+public class ReadyForShutdownResponse extends BaseResponse {
+ @SerializedName(ApiConstants.READY_FOR_SHUTDOWN)
+ @Param(description = "Indicates whether CloudStack is ready to shutdown")
+ private Boolean readyForShutdown;
+
+ @SerializedName(ApiConstants.SHUTDOWN_TRIGGERED)
+ @Param(description = "Indicates whether a shutdown has been triggered")
+ private Boolean shutdownTriggered;
+
+ @SerializedName(ApiConstants.PENDING_JOBS_COUNT)
+ @Param(description = "The number of jobs in progress")
+ private Long pendingJobsCount;
+
+ @SerializedName(ApiConstants.MANAGEMENT_SERVER_ID)
+ @Param(description = "The id of the management server")
+ private Long msId;
+
+ public ReadyForShutdownResponse(Long msId, Boolean shutdownTriggered,
Boolean readyForShutdown, long pendingJobsCount) {
+ this.msId = msId;
+ this.shutdownTriggered = shutdownTriggered;
+ this.readyForShutdown = readyForShutdown;
+ this.pendingJobsCount = pendingJobsCount;
+ }
+
+ public Boolean getShutdownTriggered() {
+ return this.shutdownTriggered;
+ }
+
+ public void setShutdownTriggered(Boolean shutdownTriggered) {
+ this.shutdownTriggered = shutdownTriggered;
+ }
+
+ public Boolean getReadyForShutdown() {
+ return this.readyForShutdown;
+ }
+
+ public void setReadyForShutdown(Boolean readyForShutdown) {
+ this.readyForShutdown = readyForShutdown;
+ }
+
+ public Long getPendingJobsCount() {
+ return this.pendingJobsCount;
+ }
+
+ public void setPendingJobsCount(Long pendingJobsCount) {
+ this.pendingJobsCount = pendingJobsCount;
+ }
+
+ public Long getMsId() {
+ return msId;
+ }
+
+ public void setMsId(Long msId) {
+ this.msId = msId;
+ }
+}
diff --git
a/plugins/shutdown/src/main/java/org/apache/cloudstack/shutdown/ShutdownManager.java
b/plugins/shutdown/src/main/java/org/apache/cloudstack/shutdown/ShutdownManager.java
new file mode 100644
index 00000000000..22f43cb4f62
--- /dev/null
+++
b/plugins/shutdown/src/main/java/org/apache/cloudstack/shutdown/ShutdownManager.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 org.apache.cloudstack.shutdown;
+
+import org.apache.cloudstack.api.command.CancelShutdownCmd;
+import org.apache.cloudstack.api.command.PrepareForShutdownCmd;
+import org.apache.cloudstack.api.command.ReadyForShutdownCmd;
+import org.apache.cloudstack.api.command.TriggerShutdownCmd;
+import org.apache.cloudstack.api.response.ReadyForShutdownResponse;
+
+public interface ShutdownManager {
+ // Returns the number of pending jobs for the given Management server
msids.
+ // NOTE: This is the msid and NOT the id
+ long countPendingJobs(Long... msIds);
+
+ // Indicates whether a shutdown has been triggered on the current
management server
+ boolean isShutdownTriggered();
+
+ // Indicates whether the current management server is preparing to shutdown
+ boolean isPreparingForShutdown();
+
+ // Triggers a shutdown on the current management server by not accepting
any more async jobs and shutting down when there are no pending jobs
+ void triggerShutdown();
+
+ // Prepares the current management server to shutdown by not accepting any
more async jobs
+ void prepareForShutdown();
+
+ // Cancels the shutdown on the current management server
+ void cancelShutdown();
+
+ // Returns whether the given ms can be shut down
+ ReadyForShutdownResponse readyForShutdown(Long managementserverid);
+
+ // Returns whether the any of the ms can be shut down and if a shutdown
has been triggered on any running ms
+ ReadyForShutdownResponse readyForShutdown(ReadyForShutdownCmd cmd);
+
+ // Prepares the specified management server to shutdown by not accepting
any more async jobs
+ ReadyForShutdownResponse prepareForShutdown(PrepareForShutdownCmd cmd);
+
+ // Cancels the shutdown on the specified management server
+ ReadyForShutdownResponse cancelShutdown(CancelShutdownCmd cmd);
+
+ // Triggers a shutdown on the specified management server by not accepting
any more async jobs and shutting down when there are no pending jobs
+ ReadyForShutdownResponse triggerShutdown(TriggerShutdownCmd cmd);
+}
diff --git
a/plugins/shutdown/src/main/java/org/apache/cloudstack/shutdown/ShutdownManagerImpl.java
b/plugins/shutdown/src/main/java/org/apache/cloudstack/shutdown/ShutdownManagerImpl.java
new file mode 100644
index 00000000000..b8f5fb57155
--- /dev/null
+++
b/plugins/shutdown/src/main/java/org/apache/cloudstack/shutdown/ShutdownManagerImpl.java
@@ -0,0 +1,265 @@
+// 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.shutdown;
+
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.List;
+import java.util.Timer;
+import java.util.TimerTask;
+
+import javax.inject.Inject;
+
+import org.apache.cloudstack.api.command.CancelShutdownCmd;
+import org.apache.cloudstack.api.command.PrepareForShutdownCmd;
+import org.apache.cloudstack.api.command.ReadyForShutdownCmd;
+import org.apache.cloudstack.api.command.TriggerShutdownCmd;
+import org.apache.cloudstack.api.response.ReadyForShutdownResponse;
+import org.apache.cloudstack.framework.jobs.AsyncJobManager;
+import org.apache.cloudstack.management.ManagementServerHost.State;
+import
org.apache.cloudstack.shutdown.command.CancelShutdownManagementServerHostCommand;
+import
org.apache.cloudstack.shutdown.command.PrepareForShutdownManagementServerHostCommand;
+import
org.apache.cloudstack.shutdown.command.TriggerShutdownManagementServerHostCommand;
+import org.apache.cloudstack.utils.identity.ManagementServerNode;
+import org.apache.log4j.Logger;
+
+import com.cloud.agent.api.Command;
+import com.cloud.cluster.ClusterManager;
+import com.cloud.cluster.ManagementServerHostVO;
+import com.cloud.cluster.dao.ManagementServerHostDao;
+import com.cloud.serializer.GsonHelper;
+import com.cloud.utils.component.ManagerBase;
+import com.cloud.utils.component.PluggableService;
+import com.cloud.utils.exception.CloudRuntimeException;
+import com.google.gson.Gson;
+
+public class ShutdownManagerImpl extends ManagerBase implements
ShutdownManager, PluggableService{
+
+ private static Logger logger = Logger.getLogger(ShutdownManagerImpl.class);
+ Gson gson;
+
+ @Inject
+ private AsyncJobManager jobManager;
+ @Inject
+ private ManagementServerHostDao msHostDao;
+ @Inject
+ private ClusterManager clusterManager;
+
+ private boolean shutdownTriggered = false;
+ private boolean preparingForShutdown = false;
+
+ private Timer timer = new Timer();
+ private TimerTask shutdownTask;
+
+ protected ShutdownManagerImpl() {
+ super();
+ gson = GsonHelper.getGson();
+ }
+
+ @Override
+ public boolean isShutdownTriggered() {
+ return shutdownTriggered;
+ }
+
+ @Override
+ public boolean isPreparingForShutdown() {
+ return preparingForShutdown;
+ }
+
+ @Override
+ public long countPendingJobs(Long... msIds) {
+ return jobManager.countPendingNonPseudoJobs(msIds);
+ }
+
+ @Override
+ public void triggerShutdown() {
+ if (this.shutdownTriggered) {
+ throw new CloudRuntimeException("A shutdown has already been
triggered");
+ }
+ this.shutdownTriggered = true;
+ prepareForShutdown(true);
+ }
+
+ private void prepareForShutdown(boolean postTrigger) {
+ // Ensure we don't throw an error if triggering a shutdown after just
preparing for it
+ if (!postTrigger && this.preparingForShutdown) {
+ throw new CloudRuntimeException("A shutdown has already been
triggered");
+ }
+ this.preparingForShutdown = true;
+ jobManager.disableAsyncJobs();
+ if (this.shutdownTask != null) {
+ this.shutdownTask.cancel();
+ this.shutdownTask = null;
+ }
+ this.shutdownTask = new ShutdownTask(this);
+ timer.scheduleAtFixedRate(shutdownTask, 0, 30L * 1000);
+ }
+
+ @Override
+ public void prepareForShutdown() {
+ prepareForShutdown(false);
+ }
+
+ @Override
+ public void cancelShutdown() {
+ if (!this.preparingForShutdown) {
+ throw new CloudRuntimeException("A shutdown has not been
triggered");
+ }
+
+ this.preparingForShutdown = false;
+ this.shutdownTriggered = false;
+ jobManager.enableAsyncJobs();
+ if (shutdownTask != null) {
+ shutdownTask.cancel();
+ }
+ shutdownTask = null;
+ }
+
+ @Override
+ public ReadyForShutdownResponse readyForShutdown(Long managementserverid) {
+ Long[] msIds = null;
+ boolean shutdownTriggeredAnywhere = false;
+ State[] shutdownTriggeredStates = {State.ShuttingDown,
State.PreparingToShutDown, State.ReadyToShutDown};
+ if (managementserverid == null) {
+ List<ManagementServerHostVO> msHosts =
msHostDao.listBy(shutdownTriggeredStates);
+ if (msHosts != null && !msHosts.isEmpty()) {
+ msIds = new Long[msHosts.size()];
+ for (int i = 0; i < msHosts.size(); i++) {
+ msIds[i] = msHosts.get(i).getMsid();
+ }
+ shutdownTriggeredAnywhere = !msHosts.isEmpty();
+ }
+ } else {
+ ManagementServerHostVO msHost =
msHostDao.findById(managementserverid);
+ msIds = new Long[]{msHost.getMsid()};
+ shutdownTriggeredAnywhere =
Arrays.asList(shutdownTriggeredStates).contains(msHost.getState());
+ }
+ long pendingJobCount = countPendingJobs(msIds);
+ return new ReadyForShutdownResponse(managementserverid,
shutdownTriggeredAnywhere, pendingJobCount == 0, pendingJobCount);
+ }
+
+ @Override
+ public ReadyForShutdownResponse readyForShutdown(ReadyForShutdownCmd cmd) {
+ return readyForShutdown(cmd.getManagementServerId());
+ }
+
+ @Override
+ public ReadyForShutdownResponse prepareForShutdown(PrepareForShutdownCmd
cmd) {
+ ManagementServerHostVO msHost =
msHostDao.findById(cmd.getManagementServerId());
+ final Command[] cmds = new Command[1];
+ cmds[0] = new
PrepareForShutdownManagementServerHostCommand(msHost.getMsid());
+ String result =
clusterManager.execute(String.valueOf(msHost.getMsid()), 0, gson.toJson(cmds),
true);
+ logger.info("PrepareForShutdownCmd result : " + result);
+ if (!result.contains("Success")) {
+ throw new CloudRuntimeException(result);
+ }
+
+ msHost.setState(State.PreparingToShutDown);
+ msHostDao.persist(msHost);
+
+ return readyForShutdown(cmd.getManagementServerId());
+ }
+
+ @Override
+ public ReadyForShutdownResponse triggerShutdown(TriggerShutdownCmd cmd) {
+ ManagementServerHostVO msHost =
msHostDao.findById(cmd.getManagementServerId());
+ final Command[] cmds = new Command[1];
+ cmds[0] = new
TriggerShutdownManagementServerHostCommand(msHost.getMsid());
+ String result =
clusterManager.execute(String.valueOf(msHost.getMsid()), 0, gson.toJson(cmds),
true);
+ logger.info("TriggerShutdownCmd result : " + result);
+ if (!result.contains("Success")) {
+ throw new CloudRuntimeException(result);
+ }
+
+ msHost.setState(State.ShuttingDown);
+ msHostDao.persist(msHost);
+
+ return readyForShutdown(cmd.getManagementServerId());
+ }
+
+ @Override
+ public ReadyForShutdownResponse cancelShutdown(CancelShutdownCmd cmd) {
+ ManagementServerHostVO msHost =
msHostDao.findById(cmd.getManagementServerId());
+ final Command[] cmds = new Command[1];
+ cmds[0] = new
CancelShutdownManagementServerHostCommand(msHost.getMsid());
+ String result =
clusterManager.execute(String.valueOf(msHost.getMsid()), 0, gson.toJson(cmds),
true);
+ logger.info("CancelShutdownCmd result : " + result);
+ if (!result.contains("Success")) {
+ throw new CloudRuntimeException(result);
+ }
+
+ msHost.setState(State.Up);
+ msHostDao.persist(msHost);
+
+ return readyForShutdown(cmd.getManagementServerId());
+ }
+
+ @Override
+ public List<Class<?>> getCommands() {
+ final List<Class<?>> cmdList = new ArrayList<>();
+ cmdList.add(CancelShutdownCmd.class);
+ cmdList.add(PrepareForShutdownCmd.class);
+ cmdList.add(ReadyForShutdownCmd.class);
+ cmdList.add(TriggerShutdownCmd.class);
+ return cmdList;
+ }
+
+ private final class ShutdownTask extends TimerTask {
+
+ private ShutdownManager shutdownManager;
+
+ public ShutdownTask(ShutdownManager shutdownManager) {
+ this.shutdownManager = shutdownManager;
+ }
+
+ @Override
+ public void run() {
+ try {
+ Long totalPendingJobs =
shutdownManager.countPendingJobs(ManagementServerNode.getManagementServerId());
+ String msg = String.format("Checking for triggered shutdown...
shutdownTriggered [%b] AllowAsyncJobs [%b] PendingJobCount [%d]",
+ shutdownManager.isShutdownTriggered(),
shutdownManager.isPreparingForShutdown(), totalPendingJobs);
+ logger.info(msg);
+
+ // If the shutdown has been cancelled
+ if (!shutdownManager.isPreparingForShutdown()) {
+ logger.info("Shutdown cancelled. Terminating the shutdown
timer task");
+ this.cancel();
+ return;
+ }
+
+ // No more pending jobs. Good to terminate
+ if (totalPendingJobs == 0) {
+ if (shutdownManager.isShutdownTriggered()) {
+ logger.info("Shutting down now");
+ System.exit(0);
+ }
+ if (shutdownManager.isPreparingForShutdown()) {
+ logger.info("Ready to shutdown");
+ ManagementServerHostVO msHost =
msHostDao.findByMsid(ManagementServerNode.getManagementServerId());
+ msHost.setState(State.ReadyToShutDown);
+ msHostDao.persist(msHost);
+ }
+ }
+
+ logger.info("Pending jobs. Trying again later");
+ } catch (final Exception e) {
+ logger.error("Error trying to run shutdown task", e);
+ }
+ }
+ }
+}
diff --git
a/api/src/main/java/org/apache/cloudstack/management/ManagementServerHost.java
b/plugins/shutdown/src/main/java/org/apache/cloudstack/shutdown/command/BaseShutdownManagementServerHostCommand.java
similarity index 67%
copy from
api/src/main/java/org/apache/cloudstack/management/ManagementServerHost.java
copy to
plugins/shutdown/src/main/java/org/apache/cloudstack/shutdown/command/BaseShutdownManagementServerHostCommand.java
index 0159fb2e791..8fe33317bc0 100644
---
a/api/src/main/java/org/apache/cloudstack/management/ManagementServerHost.java
+++
b/plugins/shutdown/src/main/java/org/apache/cloudstack/shutdown/command/BaseShutdownManagementServerHostCommand.java
@@ -14,23 +14,25 @@
// KIND, either express or implied. See the License for the
// specific language governing permissions and limitations
// under the License.
-package org.apache.cloudstack.management;
-import org.apache.cloudstack.api.Identity;
-import org.apache.cloudstack.api.InternalIdentity;
-public interface ManagementServerHost extends InternalIdentity, Identity {
- enum State {
- Up, Down
- }
+package org.apache.cloudstack.shutdown.command;
- long getMsid();
+import com.cloud.agent.api.Command;
- State getState();
+public class BaseShutdownManagementServerHostCommand extends Command {
+ long msId;
- String getName();
+ public BaseShutdownManagementServerHostCommand(long msId) {
+ this.msId = msId;
+ }
- String getVersion();
+ public long getMsId() {
+ return msId;
+ }
- String getServiceIP();
+ @Override
+ public boolean executeInSequence() {
+ return false;
+ }
}
diff --git
a/api/src/main/java/org/apache/cloudstack/management/ManagementServerHost.java
b/plugins/shutdown/src/main/java/org/apache/cloudstack/shutdown/command/CancelShutdownManagementServerHostCommand.java
similarity index 69%
copy from
api/src/main/java/org/apache/cloudstack/management/ManagementServerHost.java
copy to
plugins/shutdown/src/main/java/org/apache/cloudstack/shutdown/command/CancelShutdownManagementServerHostCommand.java
index 0159fb2e791..eef44446aa1 100644
---
a/api/src/main/java/org/apache/cloudstack/management/ManagementServerHost.java
+++
b/plugins/shutdown/src/main/java/org/apache/cloudstack/shutdown/command/CancelShutdownManagementServerHostCommand.java
@@ -14,23 +14,14 @@
// KIND, either express or implied. See the License for the
// specific language governing permissions and limitations
// under the License.
-package org.apache.cloudstack.management;
-import org.apache.cloudstack.api.Identity;
-import org.apache.cloudstack.api.InternalIdentity;
-public interface ManagementServerHost extends InternalIdentity, Identity {
- enum State {
- Up, Down
- }
-
- long getMsid();
+package org.apache.cloudstack.shutdown.command;
- State getState();
+public class CancelShutdownManagementServerHostCommand extends
BaseShutdownManagementServerHostCommand {
- String getName();
-
- String getVersion();
+ public CancelShutdownManagementServerHostCommand(long msId) {
+ super(msId);
+ }
- String getServiceIP();
}
diff --git
a/api/src/main/java/org/apache/cloudstack/management/ManagementServerHost.java
b/plugins/shutdown/src/main/java/org/apache/cloudstack/shutdown/command/PrepareForShutdownManagementServerHostCommand.java
similarity index 69%
copy from
api/src/main/java/org/apache/cloudstack/management/ManagementServerHost.java
copy to
plugins/shutdown/src/main/java/org/apache/cloudstack/shutdown/command/PrepareForShutdownManagementServerHostCommand.java
index 0159fb2e791..32a9201d551 100644
---
a/api/src/main/java/org/apache/cloudstack/management/ManagementServerHost.java
+++
b/plugins/shutdown/src/main/java/org/apache/cloudstack/shutdown/command/PrepareForShutdownManagementServerHostCommand.java
@@ -14,23 +14,13 @@
// KIND, either express or implied. See the License for the
// specific language governing permissions and limitations
// under the License.
-package org.apache.cloudstack.management;
-import org.apache.cloudstack.api.Identity;
-import org.apache.cloudstack.api.InternalIdentity;
-public interface ManagementServerHost extends InternalIdentity, Identity {
- enum State {
- Up, Down
- }
-
- long getMsid();
-
- State getState();
+package org.apache.cloudstack.shutdown.command;
- String getName();
+public class PrepareForShutdownManagementServerHostCommand extends
BaseShutdownManagementServerHostCommand {
- String getVersion();
-
- String getServiceIP();
+ public PrepareForShutdownManagementServerHostCommand(long msId) {
+ super(msId);
+ }
}
diff --git
a/api/src/main/java/org/apache/cloudstack/management/ManagementServerHost.java
b/plugins/shutdown/src/main/java/org/apache/cloudstack/shutdown/command/TriggerShutdownManagementServerHostCommand.java
similarity index 69%
copy from
api/src/main/java/org/apache/cloudstack/management/ManagementServerHost.java
copy to
plugins/shutdown/src/main/java/org/apache/cloudstack/shutdown/command/TriggerShutdownManagementServerHostCommand.java
index 0159fb2e791..e0d1879fa35 100644
---
a/api/src/main/java/org/apache/cloudstack/management/ManagementServerHost.java
+++
b/plugins/shutdown/src/main/java/org/apache/cloudstack/shutdown/command/TriggerShutdownManagementServerHostCommand.java
@@ -14,23 +14,13 @@
// KIND, either express or implied. See the License for the
// specific language governing permissions and limitations
// under the License.
-package org.apache.cloudstack.management;
-import org.apache.cloudstack.api.Identity;
-import org.apache.cloudstack.api.InternalIdentity;
-public interface ManagementServerHost extends InternalIdentity, Identity {
- enum State {
- Up, Down
- }
-
- long getMsid();
-
- State getState();
+package org.apache.cloudstack.shutdown.command;
- String getName();
+public class TriggerShutdownManagementServerHostCommand extends
BaseShutdownManagementServerHostCommand {
- String getVersion();
-
- String getServiceIP();
+ public TriggerShutdownManagementServerHostCommand(long msId) {
+ super(msId);
+ }
}
diff --git
a/plugins/shutdown/src/main/resources/META-INF/cloudstack/shutdown/module.properties
b/plugins/shutdown/src/main/resources/META-INF/cloudstack/shutdown/module.properties
new file mode 100644
index 00000000000..fd85c3085ca
--- /dev/null
+++
b/plugins/shutdown/src/main/resources/META-INF/cloudstack/shutdown/module.properties
@@ -0,0 +1,18 @@
+# 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.
+name=shutdown
+parent=api
diff --git
a/plugins/shutdown/src/main/resources/META-INF/cloudstack/shutdown/spring-shutdown-context.xml
b/plugins/shutdown/src/main/resources/META-INF/cloudstack/shutdown/spring-shutdown-context.xml
new file mode 100644
index 00000000000..5318b3bf446
--- /dev/null
+++
b/plugins/shutdown/src/main/resources/META-INF/cloudstack/shutdown/spring-shutdown-context.xml
@@ -0,0 +1,29 @@
+<!--
+ 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.
+-->
+<beans xmlns="http://www.springframework.org/schema/beans"
+ xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
+ xsi:schemaLocation="http://www.springframework.org/schema/beans
+
http://www.springframework.org/schema/beans/spring-beans.xsd"
+>
+
+ <bean id="shutdownManager"
class="org.apache.cloudstack.shutdown.ShutdownManagerImpl" >
+ <property name="name" value="shutdownManager" />
+ </bean>
+
+</beans>
diff --git
a/plugins/shutdown/src/test/java/org/apache/cloudstack/shutdown/ShutdownManagerImplTest.java
b/plugins/shutdown/src/test/java/org/apache/cloudstack/shutdown/ShutdownManagerImplTest.java
new file mode 100644
index 00000000000..19ded7844db
--- /dev/null
+++
b/plugins/shutdown/src/test/java/org/apache/cloudstack/shutdown/ShutdownManagerImplTest.java
@@ -0,0 +1,78 @@
+// 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.shutdown;
+
+import org.apache.cloudstack.framework.jobs.AsyncJobManager;
+import org.junit.Assert;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.mockito.InjectMocks;
+import org.mockito.Mock;
+import org.mockito.Mockito;
+import org.mockito.Spy;
+import org.mockito.junit.MockitoJUnitRunner;
+
+import com.cloud.utils.exception.CloudRuntimeException;
+
+
+@RunWith(MockitoJUnitRunner.class)
+public class ShutdownManagerImplTest {
+
+ @Spy
+ @InjectMocks
+ ShutdownManagerImpl spy;
+
+ @Mock
+ AsyncJobManager jobManagerMock;
+
+ private long prepareCountPendingJobs() {
+ long expectedCount = 1L;
+
Mockito.doReturn(expectedCount).when(jobManagerMock).countPendingNonPseudoJobs(1L);
+ return expectedCount;
+ }
+
+ @Test
+ public void countPendingJobs() {
+ long expectedCount = prepareCountPendingJobs();
+ long count = spy.countPendingJobs(1L);
+ Assert.assertEquals(expectedCount, count);
+ }
+
+ @Test
+ public void cancelShutdown() {
+ Assert.assertThrows(CloudRuntimeException.class, () -> {
+ spy.cancelShutdown();
+ });
+ }
+
+ @Test
+ public void prepareForShutdown() {
+ Mockito.doNothing().when(jobManagerMock).disableAsyncJobs();
+ spy.prepareForShutdown();
+ Mockito.verify(jobManagerMock).disableAsyncJobs();
+
+ Assert.assertThrows(CloudRuntimeException.class, () -> {
+ spy.prepareForShutdown();
+ });
+
+
+ Mockito.doNothing().when(jobManagerMock).enableAsyncJobs();
+ spy.cancelShutdown();
+ Mockito.verify(jobManagerMock).enableAsyncJobs();
+ }
+}
diff --git a/server/src/main/java/com/cloud/api/ApiDispatcher.java
b/server/src/main/java/com/cloud/api/ApiDispatcher.java
index 3880f2aa9d1..09a7a92a4a1 100644
--- a/server/src/main/java/com/cloud/api/ApiDispatcher.java
+++ b/server/src/main/java/com/cloud/api/ApiDispatcher.java
@@ -35,6 +35,7 @@ import org.apache.cloudstack.api.BaseCustomIdCmd;
import org.apache.cloudstack.context.CallContext;
import org.apache.cloudstack.framework.jobs.AsyncJob;
import org.apache.cloudstack.framework.jobs.AsyncJobManager;
+import org.apache.cloudstack.framework.jobs.impl.AsyncJobManagerImpl;
import org.apache.log4j.Logger;
import com.cloud.api.dispatch.DispatchChain;
@@ -44,6 +45,7 @@ import com.cloud.projects.Project;
import com.cloud.user.Account;
import com.cloud.user.AccountManager;
import com.cloud.utils.db.EntityManager;
+import com.cloud.utils.exception.CloudRuntimeException;
public class ApiDispatcher {
private static final Logger s_logger =
Logger.getLogger(ApiDispatcher.class.getName());
@@ -63,6 +65,9 @@ public class ApiDispatcher {
@Inject()
protected DispatchChainFactory dispatchChainFactory;
+ @Inject
+ AsyncJobManagerImpl asyncJobManager;
+
protected DispatchChain standardDispatchChain;
protected DispatchChain asyncCreationDispatchChain;
@@ -85,7 +90,11 @@ public class ApiDispatcher {
}
public void dispatchCreateCmd(final BaseAsyncCreateCmd cmd, final
Map<String, String> params) throws Exception {
- asyncCreationDispatchChain.dispatch(new DispatchTask(cmd, params));
+ if (asyncJobManager.isAsyncJobsEnabled()) {
+ asyncCreationDispatchChain.dispatch(new DispatchTask(cmd, params));
+ } else {
+ throw new CloudRuntimeException("A shutdown has been triggered.
Can not accept new jobs");
+ }
}
private void doAccessChecks(BaseCmd cmd, Map<Object, AccessType>
entitiesToAccess) {
diff --git a/server/src/main/java/com/cloud/api/query/QueryManagerImpl.java
b/server/src/main/java/com/cloud/api/query/QueryManagerImpl.java
index c33aac83b40..d6c166e7ed7 100644
--- a/server/src/main/java/com/cloud/api/query/QueryManagerImpl.java
+++ b/server/src/main/java/com/cloud/api/query/QueryManagerImpl.java
@@ -184,6 +184,8 @@ import com.cloud.api.query.vo.TemplateJoinVO;
import com.cloud.api.query.vo.UserAccountJoinVO;
import com.cloud.api.query.vo.UserVmJoinVO;
import com.cloud.api.query.vo.VolumeJoinVO;
+import com.cloud.cluster.ManagementServerHostVO;
+import com.cloud.cluster.dao.ManagementServerHostDao;
import com.cloud.dc.DataCenter;
import com.cloud.dc.DedicatedResourceVO;
import com.cloud.dc.dao.DedicatedResourceDao;
@@ -455,6 +457,10 @@ public class QueryManagerImpl extends
MutualExclusiveIdsManagerBase implements Q
@Inject
private ResourceIconDao resourceIconDao;
+ @Inject
+ private ManagementServerHostDao msHostDao;
+
+
@Inject
EntityManager entityManager;
@@ -2544,6 +2550,10 @@ public class QueryManagerImpl extends
MutualExclusiveIdsManagerBase implements Q
}
}
+ if (cmd.getManagementServerId() != null) {
+ sb.and("executingMsid", sb.entity().getExecutingMsid(),
SearchCriteria.Op.EQ);
+ }
+
Object keyword = cmd.getKeyword();
Object startDate = cmd.getStartDate();
@@ -2572,6 +2582,11 @@ public class QueryManagerImpl extends
MutualExclusiveIdsManagerBase implements Q
sc.addAnd("created", SearchCriteria.Op.GTEQ, startDate);
}
+ if (cmd.getManagementServerId() != null) {
+ ManagementServerHostVO msHost =
msHostDao.findById(cmd.getManagementServerId());
+ sc.setParameters("executingMsid", msHost.getMsid());
+ }
+
return _jobJoinDao.searchAndCount(sc, searchFilter);
}
diff --git
a/server/src/main/java/com/cloud/api/query/dao/AsyncJobJoinDaoImpl.java
b/server/src/main/java/com/cloud/api/query/dao/AsyncJobJoinDaoImpl.java
index bd110154e37..32cd1c21dd6 100644
--- a/server/src/main/java/com/cloud/api/query/dao/AsyncJobJoinDaoImpl.java
+++ b/server/src/main/java/com/cloud/api/query/dao/AsyncJobJoinDaoImpl.java
@@ -53,6 +53,11 @@ public class AsyncJobJoinDaoImpl extends
GenericDaoBase<AsyncJobJoinVO, Long> im
public AsyncJobResponse newAsyncJobResponse(final AsyncJobJoinVO job) {
final AsyncJobResponse jobResponse = new AsyncJobResponse();
jobResponse.setAccountId(job.getAccountUuid());
+ jobResponse.setAccount(job.getAccountName());
+ jobResponse.setDomainId(job.getDomainUuid());
+ StringBuilder domainPath = new StringBuilder("ROOT");
+
(domainPath.append(job.getDomainPath())).deleteCharAt(domainPath.length() - 1);
+ jobResponse.setDomainPath(domainPath.toString());
jobResponse.setUserId(job.getUserUuid());
jobResponse.setCmd(job.getCmd());
jobResponse.setCreated(job.getCreated());
@@ -60,6 +65,7 @@ public class AsyncJobJoinDaoImpl extends
GenericDaoBase<AsyncJobJoinVO, Long> im
jobResponse.setJobId(job.getUuid());
jobResponse.setJobStatus(job.getStatus());
jobResponse.setJobProcStatus(job.getProcessStatus());
+ jobResponse.setMsid(job.getExecutingMsid());
if (job.getInstanceType() != null && job.getInstanceId() != null) {
jobResponse.setJobInstanceType(job.getInstanceType().toString());
diff --git a/server/src/main/java/com/cloud/api/query/vo/AsyncJobJoinVO.java
b/server/src/main/java/com/cloud/api/query/vo/AsyncJobJoinVO.java
index a4db864367f..cee5526796b 100644
--- a/server/src/main/java/com/cloud/api/query/vo/AsyncJobJoinVO.java
+++ b/server/src/main/java/com/cloud/api/query/vo/AsyncJobJoinVO.java
@@ -75,6 +75,9 @@ public class AsyncJobJoinVO extends BaseViewVO implements
ControlledViewEntity {
@Column(name = "job_cmd")
private String cmd;
+ @Column(name = "job_executing_msid")
+ private Long executingMsid;
+
@Column(name = "job_status")
private int status;
@@ -214,6 +217,10 @@ public class AsyncJobJoinVO extends BaseViewVO implements
ControlledViewEntity {
return null;
}
+ public Long getExecutingMsid() {
+ return executingMsid;
+ }
+
@Override
public String getProjectUuid() {
// TODO Auto-generated method stub
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 a9db15979c9..8bebc59a518 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
@@ -21,10 +21,10 @@
xmlns:context="http://www.springframework.org/schema/context"
xmlns:aop="http://www.springframework.org/schema/aop"
xmlns:util="http://www.springframework.org/schema/util"
-
+
xsi:schemaLocation="http://www.springframework.org/schema/beans
http://www.springframework.org/schema/beans/spring-beans.xsd
- http://www.springframework.org/schema/aop
+ http://www.springframework.org/schema/aop
http://www.springframework.org/schema/aop/spring-aop.xsd
http://www.springframework.org/schema/context
http://www.springframework.org/schema/context/spring-context.xsd
@@ -129,7 +129,7 @@
<bean id="capacityManagerImpl"
class="com.cloud.capacity.CapacityManagerImpl" />
- <bean id="configurationManagerImpl"
class="com.cloud.configuration.ConfigurationManagerImpl" >
+ <bean id="configurationManagerImpl"
class="com.cloud.configuration.ConfigurationManagerImpl" >
<property name="secChecker"
value="#{securityCheckersRegistry.registered}" />
</bean>
@@ -211,45 +211,45 @@
<bean id="uploadMonitorImpl"
class="com.cloud.storage.upload.UploadMonitorImpl" />
<bean id="usageServiceImpl" class="com.cloud.usage.UsageServiceImpl" />
-
+
<bean id="virtualNetworkApplianceManagerImpl"
class="com.cloud.network.router.VirtualNetworkApplianceManagerImpl" />
-
+
<bean id="vpcManagerImpl" class="com.cloud.network.vpc.VpcManagerImpl" >
<property name="vpcElements"
value="#{vpcProvidersRegistry.registered}"></property>
</bean>
-
+
<bean id="vpcTxCallable"
class="com.cloud.network.vpc.VpcPrivateGatewayTransactionCallable" />
-
+
<bean id="vpcVirtualNetworkApplianceManagerImpl"
class="com.cloud.network.router.VpcVirtualNetworkApplianceManagerImpl"
/>
-
+
<bean id="virtualNetworkApplianceFactory"
class="com.cloud.network.rules.VirtualNetworkApplianceFactory" />
-
+
<bean id="topologyContext"
class="org.apache.cloudstack.network.topology.NetworkTopologyContext"
init-method="init" />
-
+
<bean id="basicNetworkTopology"
class="org.apache.cloudstack.network.topology.BasicNetworkTopology" />
<bean id="advancedNetworkTopology"
class="org.apache.cloudstack.network.topology.AdvancedNetworkTopology" />
-
+
<bean id="basicNetworkVisitor"
class="org.apache.cloudstack.network.topology.BasicNetworkVisitor" />
<bean id="advancedNetworkVisitor"
class="org.apache.cloudstack.network.topology.AdvancedNetworkVisitor" />
-
+
<bean id="commandSetupHelper"
class="com.cloud.network.router.CommandSetupHelper" />
-
+
<bean id="routerControlHelper"
class="com.cloud.network.router.RouterControlHelper" />
-
+
<bean id="networkHelper"
class="com.cloud.network.router.NetworkHelperImpl" />
-
+
<bean id="vpcNetworkHelper"
class="com.cloud.network.router.VpcNetworkHelperImpl" />
-
+
<bean id="nicProfileHelper"
class="com.cloud.network.router.NicProfileHelperImpl" />
-
+
<bean id="routerDeploymentDefinitionBuilder"
class="org.apache.cloudstack.network.router.deployment.RouterDeploymentDefinitionBuilder"
/>
@@ -257,6 +257,9 @@
<property name="name" value="ApiAsyncJobDispatcher" />
</bean>
+ <bean id="shutdownManager"
class="org.apache.cloudstack.shutdown.ShutdownManagerImpl" >
+ <property name="name" value="shutdownManager" />
+ </bean>
<bean id="statsCollector" class="com.cloud.server.StatsCollector" />
@@ -265,14 +268,14 @@
<bean id="domainManagerImpl" class="com.cloud.user.DomainManagerImpl" />
<bean id="downloadMonitorImpl"
class="com.cloud.storage.download.DownloadMonitorImpl" />
-
+
<bean id="lBHealthCheckManagerImpl"
class="com.cloud.network.lb.LBHealthCheckManagerImpl" />
<bean id="volumeApiServiceImpl"
class="com.cloud.storage.VolumeApiServiceImpl">
<property name="storagePoolAllocators"
value="#{storagePoolAllocatorsRegistry.registered}" />
</bean>
-
+
<bean id="ApplicationLoadBalancerService"
class="org.apache.cloudstack.network.lb.ApplicationLoadBalancerManagerImpl" />
<bean id="vMSnapshotManagerImpl"
class="com.cloud.vm.snapshot.VMSnapshotManagerImpl" />
diff --git a/test/integration/smoke/test_safe_shutdown.py
b/test/integration/smoke/test_safe_shutdown.py
new file mode 100644
index 00000000000..8277ae923f9
--- /dev/null
+++ b/test/integration/smoke/test_safe_shutdown.py
@@ -0,0 +1,120 @@
+# Licensed to the Apache Software Foundation (ASF) under one
+# or more contributor license agreements. See the NOTICE file
+# distributed with this work for additional information
+# regarding copyright ownership. The ASF licenses this file
+# to you under the Apache License, Version 2.0 (the
+# "License"); you may not use this file except in compliance
+# with the License. You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing,
+# software distributed under the License is distributed on an
+# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+# KIND, either express or implied. See the License for the
+# specific language governing permissions and limitations
+# under the License.
+
+from nose.plugins.attrib import attr
+from marvin.cloudstackTestCase import *
+from marvin.cloudstackAPI import *
+from marvin.lib.utils import *
+from marvin.lib.base import *
+from marvin.lib.common import *
+
+class TestSafeShutdown(cloudstackTestCase):
+ """
+ Tests safely shutting down the Management Server
+ """
+
+ def setUp(self):
+ self.apiclient = self.testClient.getApiClient()
+ self.mgtSvrDetails = self.config.__dict__["mgtSvr"][0].__dict__
+ self.cleanup = []
+
+ def tearDown(self):
+ super(TestSafeShutdown, self).tearDown()
+
+ def isServerShutdown(self):
+ sshClient = SshClient(
+ self.mgtSvrDetails["mgtSvrIp"],
+ 22,
+ self.mgtSvrDetails["user"],
+ self.mgtSvrDetails["passwd"]
+ )
+
+ timeout = time.time() + 300
+ while time.time() < timeout:
+ command = "service cloudstack-management status | grep dead"
+ results = sshClient.execute(command)
+
+ if len(results) > 0 and "(dead)" in results[0] :
+ return
+ time.sleep(30)
+ return self.fail("Management server did shut down, failing")
+
+ def isManagementUp(self):
+ try:
+
self.apiclient.listInfrastructure(listInfrastructure.listInfrastructureCmd())
+ return True
+ except Exception:
+ return False
+
+ def startServer(self):
+ """Start management server"""
+
+ sshClient = SshClient(
+ self.mgtSvrDetails["mgtSvrIp"],
+ 22,
+ self.mgtSvrDetails["user"],
+ self.mgtSvrDetails["passwd"]
+ )
+
+ command = "service cloudstack-management start"
+ sshClient.execute(command)
+
+ #Waits for management to come up in 5 mins, when it's up it will
continue
+ timeout = time.time() + 300
+ while time.time() < timeout:
+ if self.isManagementUp() is True: return
+ time.sleep(5)
+ return self.fail("Management server did not come up, failing")
+
+ def run_async_cmd(self) :
+ return Project.create(
+ self.apiclient,
+ {"name": "test", "displaytext": "test"}
+ )
+
+ @attr(tags=["advanced", "smoke"])
+ def test_01_prepare_and_cancel_shutdown(self):
+ try :
+ prepare_for_shutdown_cmd =
prepareForShutdown.prepareForShutdownCmd()
+ prepare_for_shutdown_cmd.managementserverid = 1
+ self.apiclient.prepareForShutdown(prepare_for_shutdown_cmd)
+ try :
+ self.run_async_cmd()
+ except Exception as e:
+ self.debug("Prepare for shutdown check successful, API
failure: %s" % e)
+ finally :
+ cancel_shutdown_cmd = cancelShutdown.cancelShutdownCmd()
+ cancel_shutdown_cmd.managementserverid = 1
+ response = self.apiclient.cancelShutdown(cancel_shutdown_cmd)
+ self.assertEqual(
+ response.shutdowntriggered,
+ False,
+ "Failed to cancel shutdown"
+ )
+ ## Just to be sure, run another async command
+ project = self.run_async_cmd()
+ self.cleanup.append(project)
+
+ @attr(tags=["advanced", "smoke"])
+ def test_02_trigger_shutdown(self):
+ try :
+ cmd = triggerShutdown.triggerShutdownCmd()
+ cmd.managementserverid = 1
+ self.apiclient.triggerShutdown(cmd)
+ self.isServerShutdown()
+ finally :
+ self.startServer()
diff --git a/tools/apidoc/gen_toc.py b/tools/apidoc/gen_toc.py
index 1352800c2bf..bd102e9c7cc 100644
--- a/tools/apidoc/gen_toc.py
+++ b/tools/apidoc/gen_toc.py
@@ -248,7 +248,8 @@ known_categories = {
'Rolling': 'Rolling Maintenance',
'importVsphereStoragePolicies' : 'vSphere storage policies',
'listVsphereStoragePolicies' : 'vSphere storage policies',
- 'ConsoleEndpoint': 'Console Endpoint'
+ 'ConsoleEndpoint': 'Console Endpoint',
+ 'Shutdown': 'Shutdown'
}
diff --git a/ui/public/locales/en.json b/ui/public/locales/en.json
index cdc013f82dc..d66f936f2c0 100644
--- a/ui/public/locales/en.json
+++ b/ui/public/locales/en.json
@@ -344,7 +344,6 @@
"label.asyncbackup": "Async backup",
"label.authentication.method": "Authentication Method",
"label.authentication.sshkey": "System SSH Key",
-"label.automigrate.volume": "Auto migrate volume to another storage pool if
required",
"label.autoscale": "AutoScale",
"label.autoscalevmgroupname": "AutoScale VM Group",
"label.author.email": "Author e-mail",
@@ -404,6 +403,7 @@
"label.bypassvlanoverlapcheck": "Bypass VLAN id/range overlap",
"label.cachemode": "Write-cache type",
"label.cancel": "Cancel",
+"label.cancel.shutdown": "Cancel Shutdown",
"label.capacity": "Capacity",
"label.capacitybytes": "Capacity bytes",
"label.capacityiops": "IOPS total",
@@ -453,6 +453,7 @@
"label.collectiontime": "Collection time",
"label.columns": "Columns",
"label.comma.separated.list.description": "Enter comma-separated list of
commands",
+"label.command": "Command",
"label.comments": "Comments",
"label.communities": "Communities",
"label.community": "Community",
@@ -1408,6 +1409,7 @@
"label.pavr": "Virtual router",
"label.payload": "Payload",
"label.pcidevice": "GPU",
+"label.pending.jobs": "Pending Jobs",
"label.per.account": "Per account",
"label.per.zone": "Per zone",
"label.percentage": "Percentage",
@@ -1446,6 +1448,7 @@
"label.preferred": "Preferred",
"label.prefix": "Prefix",
"label.prefix.type": "Prefix type",
+"label.prepare.for.shutdown": "Prepare for Shutdown",
"label.presetup": "PreSetup",
"label.prev": "Prev",
"label.previous": "Previous",
@@ -1958,6 +1961,7 @@
"label.traffic.types": "Traffic types",
"label.traffictype": "Traffic type",
"label.transportzoneuuid": "Transport zone UUID",
+"label.trigger.shutdown": "Trigger Safe Shutdown",
"label.try.again": "Try again",
"label.tuesday": "Tuesday",
"label.two.factor.authentication.secret.key": "Your Two factor authentication
secret key",
@@ -2337,6 +2341,7 @@
"message.backup.create": "Are you sure you want create a VM backup?",
"message.backup.offering.remove": "Are you sure you want to remove VM from
backup offering and delete the backup chain?",
"message.backup.restore": "Please confirm that you want to restore the vm
backup?",
+"message.cancel.shutdown": "Please confirm that you would like to cancel the
shutdown on this Management server. It will resume accepting any new Async
Jobs.",
"message.certificate.upload.processing": "Certificate upload in progress",
"message.change.offering.confirm": "Please confirm that you wish to change the
service offering of this virtual instance.",
"message.change.offering.for.volume": "Successfully changed offering for the
volume",
@@ -2400,6 +2405,7 @@
"message.confirm.scale.up.system.vm": "Do you really want to scale up the
system VM?",
"message.confirm.start.lb.vm": "Please confirm you want to start LB VM.",
"message.confirm.sync.storage": "Please confirm you want to sync the storage
pool",
+"message.confirm.type": "To confirm, please type",
"message.confirm.upgrade.router.newer.template": "Please confirm that you want
to upgrade router to use newer template.",
"message.cpu.usage.info": "The CPU usage percentage can exceed 100% if the VM
has more than 1 vCPU or when CPU Cap is not enabled. This behavior happens
according to the hypervisor being used (e.g: in KVM), due to how they account
the stats",
"message.create.compute.offering": "Compute offering created",
@@ -2534,6 +2540,7 @@
"message.error.cluster.description": "Please enter Kubernetes cluster
description.",
"message.error.cluster.name": "Please enter cluster name.",
"message.error.confirm.password": "Please confirm new password.",
+"message.error.confirm.text": "Please enter the confirmation text",
"message.error.current.password": "Please enter current password.",
"message.error.custom.disk.size": "Please enter custom disk size.",
"message.error.date": "Please select a date.",
@@ -2776,6 +2783,7 @@
"message.please.wait.while.zone.is.being.created": "Please wait while your
zone is being created; this may take a while...",
"message.pod.dedicated": "Pod dedicated.",
"message.pod.dedication.released": "Pod dedication released.",
+"message.prepare.for.shutdown": "Please confirm that you would like to prep
this Management server for shutdown. It will not accept any new Async Jobs but
will NOT terminate after there are no pending jobs.",
"message.primary.storage.invalid.state": "Primary storage is not in Up state",
"message.processing.complete": "Processing complete!",
"message.protocol.description": "For XenServer, choose NFS, iSCSI, or
PreSetup. For KVM, choose NFS, SharedMountPoint, RDB, CLVM or Gluster. For
vSphere, choose NFS, PreSetup (VMFS or iSCSI or FiberChannel or vSAN or vVols)
or DatastoreCluster. For Hyper-V, choose SMB/CIFS. For LXC, choose NFS or
SharedMountPoint. For OVM, choose NFS or OCFS2.",
@@ -2851,6 +2859,7 @@
"message.setup.physical.network.during.zone.creation": "When adding a zone,
you need to set up one or more physical networks. Each network corresponds to a
NIC on the hypervisor. Each physical network can carry one or more types of
traffic, with certain restrictions on how they may be combined. Add or remove
one or more traffic types onto each physical network.",
"message.setup.physical.network.during.zone.creation.basic": "When adding a
basic zone, you can set up one physical network, which corresponds to a NIC on
the hypervisor. The network carries several types of traffic.<br/><br/>You may
also <strong>add</strong> other traffic types onto the physical network.",
"message.shared.network.offering.warning": "Domain admins and regular users
can only create shared networks from network offering with the setting
specifyvlan=false. Please contact an administrator to create a network offering
if this list is empty.",
+"message.shutdown.triggered": "A shutdown has been triggered. CloudStack will
not accept new jobs",
"message.specify.tag.key": "Please specify a tag key.",
"message.specify.tag.value": "Please specify a tag value.",
"message.step.2.continue": "Please select a service offering to continue.",
@@ -2976,6 +2985,7 @@
"message.template.type.change.warning": "WARNING: Changing the template type
to SYSTEM will disable further changes to the template.",
"message.tooltip.reserved.system.netmask": "The network prefix that defines
the pod subnet. Uses CIDR notation.",
"message.traffic.type.to.basic.zone": "traffic type to basic zone",
+"message.trigger.shutdown": "Please confirm that you would like to trigger a
shutdown on this Management server. It will not accept any new Async Jobs and
will terminate after there are no pending jobs.",
"message.type.values.to.add": "Please add additonal values by typing them in",
"message.update.autoscale.policy.failed": "Failed to update autoscale policy",
"message.update.autoscale.vmgroup.failed": "Failed to update autoscale vm
group",
diff --git a/ui/src/components/page/GlobalLayout.vue
b/ui/src/components/page/GlobalLayout.vue
index 3f8779d6062..97ebcfaeeb9 100644
--- a/ui/src/components/page/GlobalLayout.vue
+++ b/ui/src/components/page/GlobalLayout.vue
@@ -16,98 +16,105 @@
// under the License.
<template>
- <a-layout class="layout" :class="[device]">
- <a-affix style="z-index: 200">
- <template v-if="isSideMenu()">
- <a-drawer
- v-if="isMobile()"
- :wrapClassName="'drawer-sider ' + navTheme"
- :closable="false"
- :visible="collapsed"
- placement="left"
- @close="() => this.collapsed = false"
- >
+ <div>
+ <a-affix v-if="this.$store.getters.shutdownTriggered" >
+ <div class="shutdownHeader" v-html="$t('message.shutdown.triggered')"/>
+ </a-affix>
+ <a-layout class="layout" :class="[device]">
+ <a-affix style="z-index: 200"
:offsetTop="this.$store.getters.shutdownTriggered ? 25 : 0">
+ <template v-if="isSideMenu()">
+ <a-drawer
+ v-if="isMobile()"
+ :wrapClassName="'drawer-sider ' + navTheme"
+ :closable="false"
+ :visible="collapsed"
+ placement="left"
+ @close="() => this.collapsed = false"
+ >
+ <side-menu
+ :menus="menus"
+ :theme="navTheme"
+ :collapsed="false"
+ :collapsible="true"
+ mode="inline"
+ @menuSelect="menuSelect"></side-menu>
+ </a-drawer>
<side-menu
- :menus="menus"
- :theme="navTheme"
- :collapsed="false"
- :collapsible="true"
+ v-else
mode="inline"
- @menuSelect="menuSelect"></side-menu>
- </a-drawer>
-
- <side-menu
- v-else
- mode="inline"
- :menus="menus"
- :theme="navTheme"
- :collapsed="collapsed"
- :collapsible="true"></side-menu>
- </template>
- <template v-else>
- <a-drawer
- v-if="isMobile()"
- :wrapClassName="'drawer-sider ' + navTheme"
- placement="left"
- @close="() => this.collapsed = false"
- :closable="false"
- :visible="collapsed"
- >
- <side-menu
:menus="menus"
:theme="navTheme"
- :collapsed="false"
- :collapsible="true"
- mode="inline"
- @menuSelect="menuSelect"></side-menu>
- </a-drawer>
- </template>
-
- <drawer :visible="showSetting" placement="right" v-if="isAdmin &&
(isDevelopmentMode || allowSettingTheme)">
- <template #handler>
- <a-button type="primary" size="large">
- <close-outlined v-if="showSetting" />
- <setting-outlined v-else />
- </a-button>
+ :collapsed="collapsed"
+ :collapsible="true"></side-menu>
</template>
- <template #drawer>
- <setting :visible="showSetting" />
+ <template v-else>
+ <a-drawer
+ v-if="isMobile()"
+ :wrapClassName="'drawer-sider ' + navTheme"
+ placement="left"
+ @close="() => this.collapsed = false"
+ :closable="false"
+ :visible="collapsed"
+ >
+ <side-menu
+ :menus="menus"
+ :theme="navTheme"
+ :collapsed="false"
+ :collapsible="true"
+ mode="inline"
+ @menuSelect="menuSelect"></side-menu>
+ </a-drawer>
</template>
- </drawer>
- </a-affix>
+ <drawer :visible="showSetting" placement="right" v-if="isAdmin &&
(isDevelopmentMode || allowSettingTheme)">
+ <template #handler>
+ <a-button type="primary" size="large">
+ <close-outlined v-if="showSetting" />
+ <setting-outlined v-else />
+ </a-button>
+ </template>
+ <template #drawer>
+ <setting :visible="showSetting" />
+ </template>
+ </drawer>
- <a-layout :class="[layoutMode, `content-width-${contentWidth}`]" :style="{
paddingLeft: contentPaddingLeft, minHeight: '100vh' }">
- <!-- layout header -->
- <a-affix style="z-index: 100">
- <global-header
- :mode="layoutMode"
- :menus="menus"
- :theme="navTheme"
- :collapsed="collapsed"
- :device="device"
- @toggle="toggle"
- />
</a-affix>
- <a-button
- v-if="showClear"
- type="default"
- size="small"
- class="button-clear-notification"
- @click="onClearNotification">{{ $t('label.clear.notification')
}}</a-button>
+ <a-layout :class="[layoutMode, `content-width-${contentWidth}`]"
:style="{ paddingLeft: contentPaddingLeft, minHeight: '100vh' }">
+ <!-- layout header -->
+ <a-affix style="z-index: 100">
+ <global-header
+ :style="this.$store.getters.shutdownTriggered ? 'margin-top:
25px;' : null"
+ :mode="layoutMode"
+ :menus="menus"
+ :theme="navTheme"
+ :collapsed="collapsed"
+ :device="device"
+ @toggle="toggle"
+ />
+ </a-affix>
- <!-- layout content -->
- <a-layout-content class="layout-content" :class="{'is-header-fixed':
fixedHeader}">
- <slot></slot>
- </a-layout-content>
+ <a-button
+ v-if="showClear"
+ type="default"
+ size="small"
+ class="button-clear-notification"
+ @click="onClearNotification">{{ $t('label.clear.notification')
}}</a-button>
- <!-- layout footer -->
- <a-layout-footer style="padding: 0">
- <global-footer />
- </a-layout-footer>
+ <!-- layout content -->
+ <a-layout-content
+ class="layout-content"
+ :class="{'is-header-fixed': fixedHeader}">
+ <slot></slot>
+ </a-layout-content>
+
+ <!-- layout footer -->
+ <a-layout-footer style="padding: 0">
+ <global-footer />
+ </a-layout-footer>
+ </a-layout>
</a-layout>
- </a-layout>
+ </div>
</template>
<script>
@@ -118,6 +125,7 @@ import { triggerWindowResizeEvent } from '@/utils/util'
import { mapState, mapActions } from 'vuex'
import { mixin, mixinDevice } from '@/utils/mixin.js'
import { isAdmin } from '@/role'
+import { api } from '@/api'
import Drawer from '@/components/widgets/Drawer'
import Setting from '@/components/view/Setting.vue'
@@ -191,6 +199,7 @@ export default {
created () {
this.menus = this.mainMenu.find((item) => item.path === '/').children
this.collapsed = !this.sidebarOpened
+ setInterval(this.checkShutdown, 5000)
},
mounted () {
const layoutMode = this.$config.theme['@layout-mode'] || 'light'
@@ -243,6 +252,11 @@ export default {
onClearNotification () {
this.$notification.destroy()
this.$store.commit('SET_COUNT_NOTIFY', 0)
+ },
+ checkShutdown () {
+ api('readyForShutdown', {}).then(json => {
+ this.$store.dispatch('SetShutdownTriggered',
json.readyforshutdownresponse.readyforshutdown.shutdowntriggered || false)
+ })
}
}
}
@@ -279,4 +293,17 @@ export default {
padding: 0
}
}
+
+.shutdownHeader {
+ color: red;
+ background: black;
+ font-weight: 600;
+ height: 25px;
+ text-align: center;
+ padding: 0px;
+ margin: 0px;
+ width: 100vw;
+ position: absolute;
+}
+
</style>
diff --git a/ui/src/config/section/infra/managementServers.js
b/ui/src/config/section/infra/managementServers.js
index eb680c20093..e4f9217a00c 100644
--- a/ui/src/config/section/infra/managementServers.js
+++ b/ui/src/config/section/infra/managementServers.js
@@ -37,6 +37,49 @@ export default {
{
name: 'details',
component: shallowRef(defineAsyncComponent(() =>
import('@/components/view/DetailsTab.vue')))
+ },
+ {
+ name: 'pending.jobs',
+ component: shallowRef(defineAsyncComponent(() =>
import('@/views/infra/AsyncJobsTab.vue')))
+ }
+ ],
+ actions: [
+ {
+ api: 'prepareForShutdown',
+ icon: 'exclamation-circle-outlined',
+ label: 'label.prepare.for.shutdown',
+ message: 'message.prepare.for.shutdown',
+ dataView: true,
+ popup: true,
+ confirmationText: 'SHUTDOWN',
+ show: (record, store) => { return record.state === 'Up' },
+ component: shallowRef(defineAsyncComponent(() =>
import('@/views/infra/Confirmation.vue')))
+ },
+ {
+ api: 'triggerShutdown',
+ icon: 'poweroff-outlined',
+ label: 'label.trigger.shutdown',
+ message: 'message.trigger.shutdown',
+ dataView: true,
+ popup: true,
+ confirmationText: 'SHUTDOWN',
+ show: (record, store) => { return ['Up', 'PreparingToShutDown',
'ReadyToShutDown'].includes(record.state) },
+ component: shallowRef(defineAsyncComponent(() =>
import('@/views/infra/Confirmation.vue')))
+ },
+ {
+ api: 'cancelShutdown',
+ icon: 'close-circle-outlined',
+ label: 'label.cancel.shutdown',
+ message: 'message.cancel.shutdown',
+ docHelp: 'installguide/configuration.html#adding-a-zone',
+ dataView: true,
+ popup: true,
+ show: (record, store) => { return ['PreparingToShutDown',
'ReadyToShutDown', 'ShuttingDown'].includes(record.state) },
+ mapping: {
+ managementserverid: {
+ value: (record, params) => { return record.id }
+ }
+ }
}
]
}
diff --git a/ui/src/store/getters.js b/ui/src/store/getters.js
index f6a6dab8ad2..d795e3318c1 100644
--- a/ui/src/store/getters.js
+++ b/ui/src/store/getters.js
@@ -44,6 +44,7 @@ const getters = {
countNotify: state => state.user.countNotify,
customColumns: state => state.user.customColumns,
logoutFlag: state => state.user.logoutFlag,
+ shutdownTriggered: state => state.user.shutdownTriggered,
twoFaEnabled: state => state.user.twoFaEnabled,
twoFaProvider: state => state.user.twoFaProvider,
twoFaIssuer: state => state.user.twoFaIssuer,
diff --git a/ui/src/store/modules/app.js b/ui/src/store/modules/app.js
index 8ed27f88b4c..f8753a1c10b 100644
--- a/ui/src/store/modules/app.js
+++ b/ui/src/store/modules/app.js
@@ -121,6 +121,9 @@ const app = {
SET_CUSTOM_COLUMNS: (state, customColumns) => {
vueProps.$localStorage.set(CUSTOM_COLUMNS, customColumns)
state.customColumns = customColumns
+ },
+ SET_SHUTDOWN_TRIGGERED: (state, shutdownTriggered) => {
+ state.shutdownTriggered = shutdownTriggered
}
},
actions: {
@@ -177,6 +180,9 @@ const app = {
},
SetCustomColumns ({ commit }, bool) {
commit('SET_CUSTOM_COLUMNS', bool)
+ },
+ SetShutdownTriggered ({ commit }, bool) {
+ commit('SET_SHUTDOWN_TRIGGERED', bool)
}
}
}
diff --git a/ui/src/store/modules/user.js b/ui/src/store/modules/user.js
index 3e9aef7af08..051380231a2 100644
--- a/ui/src/store/modules/user.js
+++ b/ui/src/store/modules/user.js
@@ -61,6 +61,7 @@ const user = {
loginFlag: false,
logoutFlag: false,
customColumns: {},
+ shutdownTriggered: false,
twoFaEnabled: false,
twoFaProvider: '',
twoFaIssuer: ''
@@ -133,6 +134,9 @@ const user = {
vueProps.$localStorage.set(CUSTOM_COLUMNS, customColumns)
state.customColumns = customColumns
},
+ SET_SHUTDOWN_TRIGGERED: (state, shutdownTriggered) => {
+ state.shutdownTriggered = shutdownTriggered
+ },
SET_LOGOUT_FLAG: (state, flag) => {
state.logoutFlag = flag
},
diff --git a/ui/src/views/AutogenView.vue b/ui/src/views/AutogenView.vue
index c19712c5ea3..e71e6dd8fa8 100644
--- a/ui/src/views/AutogenView.vue
+++ b/ui/src/views/AutogenView.vue
@@ -17,7 +17,7 @@
<template>
<div>
- <a-affix :offsetTop="78">
+ <a-affix :offsetTop="this.$store.getters.shutdownTriggered ? 103 : 78">
<a-card class="breadcrumb-card" style="z-index: 10">
<a-row>
<a-col :span="device === 'mobile' ? 24 : 12" style="padding-left:
12px">
@@ -390,44 +390,46 @@
</a-modal>
</div>
- <div v-if="dataView" style="margin-top: -10px">
- <slot name="resource" v-if="$route.path.startsWith('/quotasummary') ||
$route.path.startsWith('/publicip')"></slot>
- <resource-view
- v-else
- :resource="resource"
- :loading="loading"
- :tabs="$route.meta.tabs" />
- </div>
- <div class="row-element" v-else>
- <list-view
- :loading="loading"
- :columns="columns"
- :items="items"
- :actions="actions"
- :columnKeys="columnKeys"
- :selectedColumns="selectedColumns"
- ref="listview"
- @update-selected-columns="updateSelectedColumns"
- @selection-change="onRowSelectionChange"
- @refresh="fetchData"
- @edit-tariff-action="(showAction, record) =>
$emit('edit-tariff-action', showAction, record)"/>
- <a-pagination
- class="row-element"
- style="margin-top: 10px"
- size="small"
- :current="page"
- :pageSize="pageSize"
- :total="itemCount"
- :showTotal="total => `${$t('label.showing')} ${Math.min(total,
1+((page-1)*pageSize))}-${Math.min(page*pageSize, total)} ${$t('label.of')}
${total} ${$t('label.items')}`"
- :pageSizeOptions="pageSizeOptions"
- @change="changePage"
- @showSizeChange="changePageSize"
- showSizeChanger
- showQuickJumper>
- <template #buildOptionText="props">
- <span>{{ props.value }} / {{ $t('label.page') }}</span>
- </template>
- </a-pagination>
+ <div :style="this.$store.getters.shutdownTriggered ? 'margin-top: 25px;' :
null">
+ <div v-if="dataView" style="margin-top: -10px">
+ <slot name="resource" v-if="$route.path.startsWith('/quotasummary') ||
$route.path.startsWith('/publicip')"></slot>
+ <resource-view
+ v-else
+ :resource="resource"
+ :loading="loading"
+ :tabs="$route.meta.tabs" />
+ </div>
+ <div class="row-element" v-else>
+ <list-view
+ :loading="loading"
+ :columns="columns"
+ :items="items"
+ :actions="actions"
+ :columnKeys="columnKeys"
+ :selectedColumns="selectedColumns"
+ ref="listview"
+ @update-selected-columns="updateSelectedColumns"
+ @selection-change="onRowSelectionChange"
+ @refresh="fetchData"
+ @edit-tariff-action="(showAction, record) =>
$emit('edit-tariff-action', showAction, record)"/>
+ <a-pagination
+ class="row-element"
+ style="margin-top: 10px"
+ size="small"
+ :current="page"
+ :pageSize="pageSize"
+ :total="itemCount"
+ :showTotal="total => `${$t('label.showing')} ${Math.min(total,
1+((page-1)*pageSize))}-${Math.min(page*pageSize, total)} ${$t('label.of')}
${total} ${$t('label.items')}`"
+ :pageSizeOptions="pageSizeOptions"
+ @change="changePage"
+ @showSizeChange="changePageSize"
+ showSizeChanger
+ showQuickJumper>
+ <template #buildOptionText="props">
+ <span>{{ props.value }} / {{ $t('label.page') }}</span>
+ </template>
+ </a-pagination>
+ </div>
</div>
<bulk-action-progress
:showGroupActionModal="showGroupActionModal"
diff --git a/ui/src/views/infra/AsyncJobsTab.vue
b/ui/src/views/infra/AsyncJobsTab.vue
new file mode 100644
index 00000000000..7b514a4eca9
--- /dev/null
+++ b/ui/src/views/infra/AsyncJobsTab.vue
@@ -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.
+
+<template>
+ <a-table
+ class="table"
+ size="small"
+ :columns="columns"
+ :dataSource="jobs"
+ :rowKey="item => item.id"
+ :pagination="false" >
+ <template #cmd="{ text }">
+ {{ text.split('.').pop() }}
+ </template>
+ <template #account="{ text, record }">
+ <router-link :to="{ path: '/account/' + record.accountid }">{{ text
}}</router-link>
+ </template>
+ <template #domainpath="{ text, record }">
+ <router-link :to="{ path: '/domain/' + record.domainid, query: { tab:
'details' } }">{{ text }}</router-link>
+ </template>
+ </a-table>
+</template>
+
+<script>
+import { api } from '@/api'
+import Status from '@/components/widgets/Status'
+
+export default {
+ name: 'AsyncJobsTab',
+ components: {
+ Status
+ },
+ props: {
+ resource: {
+ type: Object,
+ required: true
+ }
+ },
+ data () {
+ return {
+ jobs: [],
+ columns: [
+ {
+ title: this.$t('label.command'),
+ dataIndex: 'cmd',
+ slots: { customRender: 'cmd' }
+ },
+ {
+ title: this.$t('label.resourcetype'),
+ dataIndex: 'jobinstancetype'
+ },
+ {
+ title: this.$t('label.account'),
+ dataIndex: 'account',
+ slots: { customRender: 'account' }
+ },
+ {
+ title: this.$t('label.domain'),
+ dataIndex: 'domainpath',
+ slots: { customRender: 'domainpath' }
+ },
+ {
+ title: this.$t('label.created'),
+ dataIndex: 'created'
+ }
+ ]
+ }
+ },
+ created () {
+ this.fetchData()
+ },
+ watch: {
+ resource: function (newItem) {
+ this.fetchData()
+ }
+ },
+ methods: {
+ fetchData () {
+ this.jobs = []
+ api('listAsyncJobs', {
+ listall: true,
+ isrecursive: true,
+ managementserverid: this.resource.id
+ }).then(json => {
+ this.jobs = json.listasyncjobsresponse.asyncjobs || []
+ })
+ }
+ }
+}
+</script>
diff --git a/ui/src/views/infra/Confirmation.vue
b/ui/src/views/infra/Confirmation.vue
new file mode 100644
index 00000000000..96b7f6f69ce
--- /dev/null
+++ b/ui/src/views/infra/Confirmation.vue
@@ -0,0 +1,129 @@
+// Licensed to the Apache Software Foundation (ASF) under one
+// or more contributor license agreements. See the NOTICE file
+// distributed with this work for additional information
+// regarding copyright ownership. The ASF licenses this file
+// to you under the Apache License, Version 2.0 (the
+// "License"); you may not use this file except in compliance
+// with the License. You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing,
+// software distributed under the License is distributed on an
+// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+// KIND, either express or implied. See the License for the
+// specific language governing permissions and limitations
+// under the License.
+
+<template>
+ <a-spin :spinning="loading">
+ <div class="form-layout" v-ctrl-enter="handleSubmit">
+ <a-form
+ :ref="formRef"
+ :model="form"
+ :rules="rules"
+ @finish="handleSubmit"
+ >
+ <a-alert type="error">
+ <template #message>
+ <span v-html="$t(action.currentAction.message)" />
+ </template>
+ </a-alert>
+ <a-alert type="warning" style="margin-top: 10px">
+ <template #message>
+ <span>{{ $t('message.confirm.type') }} "{{
action.currentAction.confirmationText }}"</span>
+ </template>
+ </a-alert>
+ <a-form-item ref="confirmation" name="confirmation" style="margin-top:
10px">
+ <a-input v-model:value="form.confirmation" />
+ </a-form-item>
+ </a-form>
+
+ <div :span="24" class="action-button">
+ <a-button @click="closeAction">{{ $t('label.cancel') }}</a-button>
+ <a-button type="primary" ref="submit" @click="handleSubmit">{{
$t('label.ok') }}</a-button>
+ </div>
+
+ </div>
+ </a-spin>
+</template>
+
+<script>
+
+import { api } from '@/api'
+import { ref, reactive } from 'vue'
+
+export default {
+ name: 'Confirmation',
+ props: {
+ resource: {
+ type: Object,
+ required: true
+ },
+ action: {
+ type: Object,
+ default: () => {}
+ }
+ },
+ inject: ['parentFetchData'],
+ data () {
+ return {
+ loading: false
+ }
+ },
+ created () {
+ this.initForm()
+ },
+ methods: {
+ initForm () {
+ this.formRef = ref()
+ this.form = reactive({})
+ this.rules = reactive({
+ confirmation: [{
+ validator: this.checkConfirmation,
+ message: this.$t('message.error.confirm.text')
+ }]
+ })
+ },
+ async checkConfirmation (rule, value) {
+ if (value && value === 'SHUTDOWN') {
+ return Promise.resolve()
+ }
+ return Promise.reject(rule.message)
+ },
+ handleSubmit (e) {
+ e.preventDefault()
+ if (this.loading) return
+ this.formRef.value.validate().then(() => {
+ this.loading = true
+ const params = { managementserverid: this.resource.id }
+ api(this.action.currentAction.api, params).then(() => {
+ this.$message.success(this.$t(this.action.currentAction.label) + ' :
' + this.resource.name)
+ this.closeAction()
+ this.parentFetchData()
+ }).catch(error => {
+ this.$notifyError(error)
+ }).finally(() => {
+ this.loading = false
+ })
+ })
+ },
+ closeAction () {
+ this.$emit('close-action')
+ }
+ }
+}
+
+</script>
+<style lang="scss" scoped>
+.form-layout {
+ width: 80vw;
+ @media (min-width: 700px) {
+ width: 600px;
+ }
+}
+
+.form {
+ margin: 10px 0;
+}
+</style>