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 6e93d45abdb2..c1438251b858 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 d2574ff442e0..783d78fdce32 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.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 Date getStartDate() {
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 eecd6be6c521..3eeaaef2afac 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 @@
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 void setAccountId(String accountId) {
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 void setCreated(Date created) {
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 0159fb2e7919..834291ef21c8 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 @@
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 0ff5c3203b93..a548d676a671 100644
--- a/client/pom.xml
+++ b/client/pom.xml
@@ -552,6 +552,11 @@
cloud-plugin-integrations-kubernetes-service${project.version}
+
+ org.apache.cloudstack
+ cloud-plugin-shutdown
+ ${project.version}
+
diff --git a/engine/orchestration/pom.xml b/engine/orchestration/pom.xml
index 2cd17519e0ef..e5eeb88f6c5b 100755
--- a/engine/orchestration/pom.xml
+++ b/engine/orchestration/pom.xml
@@ -68,6 +68,11 @@
cloud-server${project.version}
+
+ org.apache.cloudstack
+ cloud-plugin-shutdown
+ ${project.version}
+
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 749a738e63ed..bd4e259a7885 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.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 String dispatch(final ClusterServicePdu pdu) {
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 String dispatch(final ClusterServicePdu pdu) {
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 b8eee33cad74..f5efb07dd779 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 5e13b1c0157e..541d3dbacac3 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 b8200bf82219..bde9b4af1671 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 static interface Constants {
@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 8542407524b1..52ef10a4adc4 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 @@ void joinJob(long jobId, long joinJobId, String wakeupHandler, String wakupDispa
List 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 2696e105cce4..9f7a4ad6e058 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 {
List 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 7dd73435093d..1914ff714602 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 implements
private final SearchBuilder expiringCompletedAsyncJobSearch;
private final SearchBuilder failureMsidAsyncJobSearch;
private final GenericSearchBuilder asyncJobTypeSearch;
+ private final GenericSearchBuilder pendingNonPseudoAsyncJobsSearch;
public AsyncJobDaoImpl() {
pendingAsyncJobSearch = createSearchBuilder();
@@ -103,6 +104,11 @@ public AsyncJobDaoImpl() {
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 List getFailureJobsSinceLastMsStart(long msId, String... cmds
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 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 results = customSearch(sc, null);
+ return results.get(0);
+ }
+
@Override
public long countPendingJobs(String havingInfo, String... cmds) {
SearchCriteria 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 a963357122e5..0546d998913a 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 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 long submitAsyncJob(AsyncJob job) {
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 long submitAsyncJob(AsyncJob job, boolean scheduleJobExecutionInContext)
@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 @@ protected void runInContext() {
protected void reallyRun() {
try {
+ if (!isAsyncJobsEnabled()) {
+ s_logger.info("A shutdown has been triggered. Not executing any async job");
+ return;
+ }
+
List l = _queueMgr.dequeueFromAny(getMsid(), MAX_ONETIME_SCHEDULE_SIZE);
if (l != null && l.size() > 0) {
for (SyncQueueItemVO item : l) {
@@ -1171,4 +1190,26 @@ public List findFailureAsyncJobs(String... cmds) {
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 8f3c0337837b..6b85ae27f58a 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 Long getExecutingMsid() {
return executingMsid;
}
+ @Override
public void setExecutingMsid(Long executingMsid) {
this.executingMsid = executingMsid;
}
diff --git a/plugins/pom.xml b/plugins/pom.xml
index 529cf3832887..d0661c01a2c4 100755
--- a/plugins/pom.xml
+++ b/plugins/pom.xml
@@ -115,6 +115,8 @@
outofbandmanagement-drivers/nested-cloudstackoutofbandmanagement-drivers/redfish
+ shutdown
+
storage/image/defaultstorage/image/s3storage/image/sample
diff --git a/plugins/shutdown/pom.xml b/plugins/shutdown/pom.xml
new file mode 100644
index 000000000000..0f34cc084828
--- /dev/null
+++ b/plugins/shutdown/pom.xml
@@ -0,0 +1,44 @@
+
+
+
+ 4.0.0
+ cloud-plugin-shutdown
+ Apache CloudStack Plugin - Safe Shutdown
+
+ org.apache.cloudstack
+ cloudstack-plugins
+ 4.19.0.0-SNAPSHOT
+ ../pom.xml
+
+
+
+ org.apache.cloudstack
+ cloud-api
+ ${project.version}
+
+
+ org.apache.cloudstack
+ cloud-utils
+ ${project.version}
+
+
+
diff --git a/plugins/shutdown/src/main/java/org/apache/cloudstack/api/command/BaseShutdownActionCmd.java b/plugins/shutdown/src/main/java/org/apache/cloudstack/api/command/BaseShutdownActionCmd.java
new file mode 100644
index 000000000000..d7f4953291b6
--- /dev/null
+++ b/plugins/shutdown/src/main/java/org/apache/cloudstack/api/command/BaseShutdownActionCmd.java
@@ -0,0 +1,49 @@
+// 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.ApiConstants;
+import org.apache.cloudstack.api.BaseCmd;
+import org.apache.cloudstack.api.Parameter;
+
+
+import org.apache.cloudstack.api.response.ManagementServerResponse;
+import org.apache.cloudstack.shutdown.ShutdownManager;
+
+public abstract class BaseShutdownActionCmd extends BaseCmd {
+
+ @Inject
+ protected ShutdownManager shutdownManager;
+
+ /////////////////////////////////////////////////////
+ //////////////// API parameters /////////////////////
+ /////////////////////////////////////////////////////
+
+ @Parameter(name = ApiConstants.MANAGEMENT_SERVER_ID, type = CommandType.UUID, entityType = ManagementServerResponse.class, description = "the uuid of the management server", required = true)
+ private Long managementServerId;
+
+ /////////////////////////////////////////////////////
+ /////////////////// Accessors ///////////////////////
+ /////////////////////////////////////////////////////
+
+ 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 000000000000..fe6204fd0cc3
--- /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 000000000000..01ea1797a105
--- /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 000000000000..d7ab6a24ee68
--- /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 000000000000..3abde0b1f3b1
--- /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 000000000000..d1b2353d2a33
--- /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 000000000000..22f43cb4f626
--- /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 000000000000..b8f5fb57155d
--- /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 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> getCommands() {
+ final List> 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/plugins/shutdown/src/main/java/org/apache/cloudstack/shutdown/command/BaseShutdownManagementServerHostCommand.java b/plugins/shutdown/src/main/java/org/apache/cloudstack/shutdown/command/BaseShutdownManagementServerHostCommand.java
new file mode 100644
index 000000000000..8fe33317bc0c
--- /dev/null
+++ b/plugins/shutdown/src/main/java/org/apache/cloudstack/shutdown/command/BaseShutdownManagementServerHostCommand.java
@@ -0,0 +1,38 @@
+// Licensed to the Apache Software Foundation (ASF) under one
+// or more contributor license agreements. See the NOTICE file
+// distributed with this work for additional information
+// regarding copyright ownership. The ASF licenses this file
+// to you under the Apache License, Version 2.0 (the
+// "License"); you may not use this file except in compliance
+// with the License. You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing,
+// software distributed under the License is distributed on an
+// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+// KIND, either express or implied. See the License for the
+// specific language governing permissions and limitations
+// under the License.
+
+
+package org.apache.cloudstack.shutdown.command;
+
+import com.cloud.agent.api.Command;
+
+public class BaseShutdownManagementServerHostCommand extends Command {
+ long msId;
+
+ public BaseShutdownManagementServerHostCommand(long msId) {
+ this.msId = msId;
+ }
+
+ public long getMsId() {
+ return msId;
+ }
+
+ @Override
+ public boolean executeInSequence() {
+ return false;
+ }
+}
diff --git a/plugins/shutdown/src/main/java/org/apache/cloudstack/shutdown/command/CancelShutdownManagementServerHostCommand.java b/plugins/shutdown/src/main/java/org/apache/cloudstack/shutdown/command/CancelShutdownManagementServerHostCommand.java
new file mode 100644
index 000000000000..eef44446aa14
--- /dev/null
+++ b/plugins/shutdown/src/main/java/org/apache/cloudstack/shutdown/command/CancelShutdownManagementServerHostCommand.java
@@ -0,0 +1,27 @@
+// 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.command;
+
+public class CancelShutdownManagementServerHostCommand extends BaseShutdownManagementServerHostCommand {
+
+ public CancelShutdownManagementServerHostCommand(long msId) {
+ super(msId);
+ }
+
+}
diff --git a/plugins/shutdown/src/main/java/org/apache/cloudstack/shutdown/command/PrepareForShutdownManagementServerHostCommand.java b/plugins/shutdown/src/main/java/org/apache/cloudstack/shutdown/command/PrepareForShutdownManagementServerHostCommand.java
new file mode 100644
index 000000000000..32a9201d5516
--- /dev/null
+++ b/plugins/shutdown/src/main/java/org/apache/cloudstack/shutdown/command/PrepareForShutdownManagementServerHostCommand.java
@@ -0,0 +1,26 @@
+// 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.command;
+
+public class PrepareForShutdownManagementServerHostCommand extends BaseShutdownManagementServerHostCommand {
+
+ public PrepareForShutdownManagementServerHostCommand(long msId) {
+ super(msId);
+ }
+}
diff --git a/plugins/shutdown/src/main/java/org/apache/cloudstack/shutdown/command/TriggerShutdownManagementServerHostCommand.java b/plugins/shutdown/src/main/java/org/apache/cloudstack/shutdown/command/TriggerShutdownManagementServerHostCommand.java
new file mode 100644
index 000000000000..e0d1879fa358
--- /dev/null
+++ b/plugins/shutdown/src/main/java/org/apache/cloudstack/shutdown/command/TriggerShutdownManagementServerHostCommand.java
@@ -0,0 +1,26 @@
+// 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.command;
+
+public class TriggerShutdownManagementServerHostCommand extends BaseShutdownManagementServerHostCommand {
+
+ 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 000000000000..fd85c3085ca1
--- /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 000000000000..5318b3bf4462
--- /dev/null
+++ b/plugins/shutdown/src/main/resources/META-INF/cloudstack/shutdown/spring-shutdown-context.xml
@@ -0,0 +1,29 @@
+
+
+
+
+
+
+
+
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 000000000000..19ded7844db3
--- /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 3880f2aa9d1b..09a7a92a4a16 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.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.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 void setMigrateQueueSizeLimit(final Long migrateLimit) {
}
public void dispatchCreateCmd(final BaseAsyncCreateCmd cmd, final Map 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