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 1380c604b1e server: add Host Control Plane State to uservm and 
systemvm response (#6946)
1380c604b1e is described below

commit 1380c604b1eebc3ae1238c38773c9f86067fc12b
Author: Wei Zhou <[email protected]>
AuthorDate: Thu Jan 5 09:59:28 2023 +0100

    server: add Host Control Plane State to uservm and systemvm response (#6946)
    
    Co-authored-by: dahn <[email protected]>
---
 api/src/main/java/com/cloud/host/ControlState.java |  41 ++++
 .../org/apache/cloudstack/api/ApiConstants.java    |   1 +
 .../api/response/DomainRouterResponse.java         |   8 +
 .../cloudstack/api/response/SystemVmResponse.java  |  12 +
 .../cloudstack/api/response/UserVmResponse.java    |  12 +
 .../test/java/com/cloud/host/ControlStateTest.java | 109 +++++++++
 .../resources/META-INF/db/schema-41720to41800.sql  |   4 +
 .../main/java/com/cloud/api/ApiResponseHelper.java |   2 +
 .../api/query/dao/DomainRouterJoinDaoImpl.java     |   2 +
 .../com/cloud/api/query/dao/UserVmJoinDaoImpl.java |   4 +
 .../com/cloud/api/query/vo/DomainRouterJoinVO.java |  16 ++
 .../java/com/cloud/api/query/vo/UserVmJoinVO.java  |  18 +-
 test/integration/smoke/test_host_control_state.py  | 252 +++++++++++++++++++++
 ui/public/locales/en.json                          |   3 +
 ui/src/components/view/DetailsTab.vue              |   7 +
 ui/src/components/widgets/Console.vue              |   2 +-
 ui/src/config/section/compute.js                   |  10 +-
 ui/src/config/section/infra/ilbvms.js              |   4 +-
 ui/src/config/section/infra/routers.js             |   4 +-
 ui/src/config/section/infra/systemVms.js           |   4 +-
 20 files changed, 509 insertions(+), 6 deletions(-)

diff --git a/api/src/main/java/com/cloud/host/ControlState.java 
b/api/src/main/java/com/cloud/host/ControlState.java
new file mode 100644
index 00000000000..335125dde20
--- /dev/null
+++ b/api/src/main/java/com/cloud/host/ControlState.java
@@ -0,0 +1,41 @@
+// Licensed to the Apache Software Foundation (ASF) under one
+// or more contributor license agreements.  See the NOTICE file
+// distributed with this work for additional information
+// regarding copyright ownership.  The ASF licenses this file
+// to you under the Apache License, Version 2.0 (the
+// "License"); you may not use this file except in compliance
+// with the License.  You may obtain a copy of the License at
+//
+//   http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing,
+// software distributed under the License is distributed on an
+// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+// KIND, either express or implied.  See the License for the
+// specific language governing permissions and limitations
+// under the License.
+package com.cloud.host;
+
+import com.cloud.resource.ResourceState;
+
+public enum ControlState {
+    Enabled,
+    Disabled,
+    Offline,
+    Maintenance,
+    Unknown;
+
+    public static ControlState getControlState(Status hostStatus, 
ResourceState hostResourceState) {
+        if (hostStatus == null || Status.Unknown.equals(hostStatus) || 
hostResourceState == null) {
+            return ControlState.Unknown;
+        } else if (hostStatus.lostConnection()) {
+            return Offline;
+        } else if (ResourceState.isMaintenanceState(hostResourceState)) {
+            return Maintenance;
+        } else if (ResourceState.Enabled.equals(hostResourceState)) {
+            return Enabled;
+        } else {
+            return Disabled;
+        }
+    }
+}
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 96ed750852e..3cb937e8427 100644
--- a/api/src/main/java/org/apache/cloudstack/api/ApiConstants.java
+++ b/api/src/main/java/org/apache/cloudstack/api/ApiConstants.java
@@ -203,6 +203,7 @@ public class ApiConstants {
     public static final String HOST_ID = "hostid";
     public static final String HOST_IDS = "hostids";
     public static final String HOST_NAME = "hostname";
+    public static final String HOST_CONTROL_STATE = "hostcontrolstate";
     public static final String HOSTS_MAP = "hostsmap";
     public static final String HYPERVISOR = "hypervisor";
     public static final String INLINE = "inline";
diff --git 
a/api/src/main/java/org/apache/cloudstack/api/response/DomainRouterResponse.java
 
b/api/src/main/java/org/apache/cloudstack/api/response/DomainRouterResponse.java
index a5fa2bd08c2..99e5f6ccdfa 100644
--- 
a/api/src/main/java/org/apache/cloudstack/api/response/DomainRouterResponse.java
+++ 
b/api/src/main/java/org/apache/cloudstack/api/response/DomainRouterResponse.java
@@ -89,6 +89,10 @@ public class DomainRouterResponse extends 
BaseResponseWithAnnotations implements
     @Param(description = "the hostname for the router")
     private String hostName;
 
+    @SerializedName(ApiConstants.HOST_CONTROL_STATE)
+    @Param(description = "the control state of the host for the router")
+    private String hostControlState;
+
     @SerializedName("hypervisor")
     @Param(description = "the hypervisor on which the template runs")
     private String hypervisor;
@@ -302,6 +306,10 @@ public class DomainRouterResponse extends 
BaseResponseWithAnnotations implements
         this.hostName = hostName;
     }
 
+    public void setHostControlState(String hostControlState) {
+        this.hostControlState = hostControlState;
+    }
+
     public String getHypervisor() {
         return hypervisor;
     }
diff --git 
a/api/src/main/java/org/apache/cloudstack/api/response/SystemVmResponse.java 
b/api/src/main/java/org/apache/cloudstack/api/response/SystemVmResponse.java
index ce2b406d15f..69b9b4cad9c 100644
--- a/api/src/main/java/org/apache/cloudstack/api/response/SystemVmResponse.java
+++ b/api/src/main/java/org/apache/cloudstack/api/response/SystemVmResponse.java
@@ -90,6 +90,10 @@ public class SystemVmResponse extends 
BaseResponseWithAnnotations {
     @Param(description = "the hostname for the system VM")
     private String hostName;
 
+    @SerializedName(ApiConstants.HOST_CONTROL_STATE)
+    @Param(description = "the control state of the host for the system VM")
+    private String hostControlState;
+
     @SerializedName("hypervisor")
     @Param(description = "the hypervisor on which the template runs")
     private String hypervisor;
@@ -283,6 +287,14 @@ public class SystemVmResponse extends 
BaseResponseWithAnnotations {
         this.hostName = hostName;
     }
 
+    public String getHostControlState() {
+        return hostControlState;
+    }
+
+    public void setHostControlState(String hostControlState) {
+        this.hostControlState = hostControlState;
+    }
+
     public String getHypervisor() {
         return hypervisor;
     }
diff --git 
a/api/src/main/java/org/apache/cloudstack/api/response/UserVmResponse.java 
b/api/src/main/java/org/apache/cloudstack/api/response/UserVmResponse.java
index 108e480deb0..f2903b2626c 100644
--- a/api/src/main/java/org/apache/cloudstack/api/response/UserVmResponse.java
+++ b/api/src/main/java/org/apache/cloudstack/api/response/UserVmResponse.java
@@ -118,6 +118,10 @@ public class UserVmResponse extends 
BaseResponseWithTagInformation implements Co
     @Param(description = "the name of the host for the virtual machine")
     private String hostName;
 
+    @SerializedName(ApiConstants.HOST_CONTROL_STATE)
+    @Param(description = "the control state of the host for the virtual 
machine")
+    private String hostControlState;
+
     @SerializedName(ApiConstants.TEMPLATE_ID)
     @Param(description = "the ID of the template for the virtual machine. A -1 
is returned if the virtual machine was created from an ISO file.")
     private String templateId;
@@ -461,6 +465,10 @@ public class UserVmResponse extends 
BaseResponseWithTagInformation implements Co
         return hostName;
     }
 
+    public String getHostControlState() {
+        return hostControlState;
+    }
+
     public String getTemplateId() {
         return templateId;
     }
@@ -703,6 +711,10 @@ public class UserVmResponse extends 
BaseResponseWithTagInformation implements Co
         this.hostName = hostName;
     }
 
+    public void setHostControlState(String hostControlState) {
+        this.hostControlState = hostControlState;
+    }
+
     public void setTemplateId(String templateId) {
         this.templateId = templateId;
     }
diff --git a/api/src/test/java/com/cloud/host/ControlStateTest.java 
b/api/src/test/java/com/cloud/host/ControlStateTest.java
new file mode 100644
index 00000000000..2e8b38cd54a
--- /dev/null
+++ b/api/src/test/java/com/cloud/host/ControlStateTest.java
@@ -0,0 +1,109 @@
+// Licensed to the Apache Software Foundation (ASF) under one
+// or more contributor license agreements.  See the NOTICE file
+// distributed with this work for additional information
+// regarding copyright ownership.  The ASF licenses this file
+// to you under the Apache License, Version 2.0 (the
+// "License"); you may not use this file except in compliance
+// with the License.  You may obtain a copy of the License at
+//
+//   http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing,
+// software distributed under the License is distributed on an
+// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+// KIND, either express or implied.  See the License for the
+// specific language governing permissions and limitations
+// under the License.
+package com.cloud.host;
+
+import com.cloud.resource.ResourceState;
+
+import junit.framework.TestCase;
+
+import org.junit.Assert;
+import org.junit.Test;
+
+public class ControlStateTest extends TestCase {
+
+    void verifyHostControlState(Status hostStatus, ResourceState 
hostResourceState, ControlState expectedControlState) {
+        Assert.assertEquals(expectedControlState, 
ControlState.getControlState(hostStatus, hostResourceState));
+    }
+
+    @Test
+    public void testHostControlState1() {
+        // Unknown state
+        verifyHostControlState(null, null, ControlState.Unknown);
+        verifyHostControlState(null, ResourceState.Enabled, 
ControlState.Unknown);
+        verifyHostControlState(Status.Up, null, ControlState.Unknown);
+        verifyHostControlState(Status.Disconnected, null, 
ControlState.Unknown);
+        verifyHostControlState(Status.Down, null, ControlState.Unknown);
+
+        verifyHostControlState(Status.Unknown, null, ControlState.Unknown);
+        verifyHostControlState(Status.Unknown, ResourceState.Enabled, 
ControlState.Unknown);
+        verifyHostControlState(Status.Unknown, 
ResourceState.ErrorInPrepareForMaintenance, ControlState.Unknown);
+        verifyHostControlState(Status.Unknown, 
ResourceState.PrepareForMaintenance, ControlState.Unknown);
+        verifyHostControlState(Status.Unknown, 
ResourceState.ErrorInMaintenance, ControlState.Unknown);
+        verifyHostControlState(Status.Unknown, ResourceState.Maintenance, 
ControlState.Unknown);
+        verifyHostControlState(Status.Unknown, ResourceState.Creating, 
ControlState.Unknown);
+        verifyHostControlState(Status.Unknown, ResourceState.Disabled, 
ControlState.Unknown);
+        verifyHostControlState(Status.Unknown, ResourceState.Error, 
ControlState.Unknown);
+        verifyHostControlState(Status.Unknown, ResourceState.Degraded, 
ControlState.Unknown);
+    }
+    @Test
+    public void testHostControlState2() {
+        // Host is Up and Enabled
+        verifyHostControlState(Status.Creating, ResourceState.Enabled, 
ControlState.Enabled);
+        verifyHostControlState(Status.Connecting, ResourceState.Enabled, 
ControlState.Enabled);
+        verifyHostControlState(Status.Up, ResourceState.Enabled, 
ControlState.Enabled);
+    }
+
+    @Test
+    public void testHostControlState3() {
+        // Host is Up and not Enabled
+        verifyHostControlState(Status.Up, ResourceState.Creating, 
ControlState.Disabled);
+        verifyHostControlState(Status.Up, ResourceState.Disabled, 
ControlState.Disabled);
+        verifyHostControlState(Status.Up, ResourceState.Error, 
ControlState.Disabled);
+        verifyHostControlState(Status.Up, ResourceState.Degraded, 
ControlState.Disabled);
+
+        // Host is Creating and not Enabled
+        verifyHostControlState(Status.Creating, ResourceState.Creating, 
ControlState.Disabled);
+        verifyHostControlState(Status.Creating, ResourceState.Disabled, 
ControlState.Disabled);
+        verifyHostControlState(Status.Creating, ResourceState.Error, 
ControlState.Disabled);
+        verifyHostControlState(Status.Creating, ResourceState.Degraded, 
ControlState.Disabled);
+
+        // Host is Connecting and not Enabled
+        verifyHostControlState(Status.Connecting, ResourceState.Creating, 
ControlState.Disabled);
+        verifyHostControlState(Status.Connecting, ResourceState.Disabled, 
ControlState.Disabled);
+        verifyHostControlState(Status.Connecting, ResourceState.Error, 
ControlState.Disabled);
+        verifyHostControlState(Status.Connecting, ResourceState.Degraded, 
ControlState.Disabled);
+    }
+
+    @Test
+    public void testHostControlState4() {
+        // Host is Up and Maintenance mode
+        verifyHostControlState(Status.Up, 
ResourceState.ErrorInPrepareForMaintenance, ControlState.Maintenance);
+        verifyHostControlState(Status.Up, ResourceState.PrepareForMaintenance, 
ControlState.Maintenance);
+        verifyHostControlState(Status.Up, ResourceState.ErrorInMaintenance, 
ControlState.Maintenance);
+        verifyHostControlState(Status.Up, ResourceState.Maintenance, 
ControlState.Maintenance);
+    }
+
+    @Test
+    public void testHostControlState5() {
+        // Host in other states and Enabled
+        verifyHostControlState(Status.Down, ResourceState.Enabled, 
ControlState.Offline);
+        verifyHostControlState(Status.Disconnected, ResourceState.Enabled, 
ControlState.Offline);
+        verifyHostControlState(Status.Alert, ResourceState.Enabled, 
ControlState.Offline);
+        verifyHostControlState(Status.Removed, ResourceState.Enabled, 
ControlState.Offline);
+        verifyHostControlState(Status.Error, ResourceState.Enabled, 
ControlState.Offline);
+        verifyHostControlState(Status.Rebalancing, ResourceState.Enabled, 
ControlState.Offline);
+
+        // Host in other states and Disabled
+        verifyHostControlState(Status.Down, ResourceState.Disabled, 
ControlState.Offline);
+        verifyHostControlState(Status.Disconnected, ResourceState.Disabled, 
ControlState.Offline);
+        verifyHostControlState(Status.Alert, ResourceState.Disabled, 
ControlState.Offline);
+        verifyHostControlState(Status.Removed, ResourceState.Disabled, 
ControlState.Offline);
+        verifyHostControlState(Status.Error, ResourceState.Disabled, 
ControlState.Offline);
+        verifyHostControlState(Status.Rebalancing, ResourceState.Disabled, 
ControlState.Offline);
+    }
+
+}
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 d1180c8f2c2..c3a18ec7e2d 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
@@ -65,6 +65,8 @@ CREATE VIEW `cloud`.`domain_router_view` AS
         host.name host_name,
         host.hypervisor_type,
         host.cluster_id cluster_id,
+        host.status host_status,
+        host.resource_state host_resource_state,
         vm_template.id template_id,
         vm_template.uuid template_uuid,
         service_offering.id service_offering_id,
@@ -744,6 +746,8 @@ SELECT
     `host`.`uuid` AS `host_uuid`,
     `host`.`name` AS `host_name`,
     `host`.`cluster_id` AS `cluster_id`,
+    `host`.`status` AS `host_status`,
+    `host`.`resource_state` AS `host_resource_state`,
     `vm_template`.`id` AS `template_id`,
     `vm_template`.`uuid` AS `template_uuid`,
     `vm_template`.`name` AS `template_name`,
diff --git a/server/src/main/java/com/cloud/api/ApiResponseHelper.java 
b/server/src/main/java/com/cloud/api/ApiResponseHelper.java
index c7cfda4fc67..f6d30d52dfa 100644
--- a/server/src/main/java/com/cloud/api/ApiResponseHelper.java
+++ b/server/src/main/java/com/cloud/api/ApiResponseHelper.java
@@ -37,6 +37,7 @@ import java.util.stream.Collectors;
 
 import javax.inject.Inject;
 
+import com.cloud.host.ControlState;
 import com.cloud.utils.security.CertificateHelper;
 import com.cloud.user.UserData;
 import com.cloud.api.query.dao.UserVmJoinDao;
@@ -1549,6 +1550,7 @@ public class ApiResponseHelper implements 
ResponseGenerator {
                 if (host != null) {
                     vmResponse.setHostId(host.getUuid());
                     vmResponse.setHostName(host.getName());
+                    
vmResponse.setHostControlState(ControlState.getControlState(host.getStatus(), 
host.getResourceState()).toString());
                 }
             }
 
diff --git 
a/server/src/main/java/com/cloud/api/query/dao/DomainRouterJoinDaoImpl.java 
b/server/src/main/java/com/cloud/api/query/dao/DomainRouterJoinDaoImpl.java
index feeaa8b952a..83a89622bd2 100644
--- a/server/src/main/java/com/cloud/api/query/dao/DomainRouterJoinDaoImpl.java
+++ b/server/src/main/java/com/cloud/api/query/dao/DomainRouterJoinDaoImpl.java
@@ -38,6 +38,7 @@ import com.cloud.api.ApiDBUtils;
 import com.cloud.api.ApiResponseHelper;
 import com.cloud.api.query.vo.DomainRouterJoinVO;
 import com.cloud.dc.HostPodVO;
+import com.cloud.host.ControlState;
 import com.cloud.network.Networks.TrafficType;
 import com.cloud.network.router.VirtualRouter;
 import com.cloud.network.router.VirtualRouter.Role;
@@ -135,6 +136,7 @@ public class DomainRouterJoinDaoImpl extends 
GenericDaoBase<DomainRouterJoinVO,
             if (router.getHostId() != null) {
                 routerResponse.setHostId(router.getHostUuid());
                 routerResponse.setHostName(router.getHostName());
+                
routerResponse.setHostControlState(ControlState.getControlState(router.getHostStatus(),
 router.getHostResourceState()).toString());
             }
             routerResponse.setPodId(router.getPodUuid());
             HostPodVO pod = ApiDBUtils.findPodById(router.getPodId());
diff --git 
a/server/src/main/java/com/cloud/api/query/dao/UserVmJoinDaoImpl.java 
b/server/src/main/java/com/cloud/api/query/dao/UserVmJoinDaoImpl.java
index 0396a9f663f..6d59b691836 100644
--- a/server/src/main/java/com/cloud/api/query/dao/UserVmJoinDaoImpl.java
+++ b/server/src/main/java/com/cloud/api/query/dao/UserVmJoinDaoImpl.java
@@ -52,6 +52,7 @@ import com.cloud.api.ApiDBUtils;
 import com.cloud.api.ApiResponseHelper;
 import com.cloud.api.query.vo.UserVmJoinVO;
 import com.cloud.gpu.GPU;
+import com.cloud.host.ControlState;
 import com.cloud.service.ServiceOfferingDetailsVO;
 import com.cloud.storage.GuestOS;
 import com.cloud.user.Account;
@@ -172,6 +173,9 @@ public class UserVmJoinDaoImpl extends 
GenericDaoBaseWithTagInformation<UserVmJo
             userVmResponse.setHostId(userVm.getHostUuid());
             userVmResponse.setHostName(userVm.getHostName());
         }
+        if (userVm.getHostStatus() != null) {
+            
userVmResponse.setHostControlState(ControlState.getControlState(userVm.getHostStatus(),
 userVm.getHostResourceState()).toString());
+        }
 
         if (details.contains(VMDetails.all) || 
details.contains(VMDetails.tmpl)) {
             userVmResponse.setTemplateId(userVm.getTemplateUuid());
diff --git 
a/server/src/main/java/com/cloud/api/query/vo/DomainRouterJoinVO.java 
b/server/src/main/java/com/cloud/api/query/vo/DomainRouterJoinVO.java
index 78a794009e6..a907506af54 100644
--- a/server/src/main/java/com/cloud/api/query/vo/DomainRouterJoinVO.java
+++ b/server/src/main/java/com/cloud/api/query/vo/DomainRouterJoinVO.java
@@ -26,11 +26,13 @@ import javax.persistence.Enumerated;
 import javax.persistence.Id;
 import javax.persistence.Table;
 
+import com.cloud.host.Status;
 import com.cloud.hypervisor.Hypervisor;
 import com.cloud.network.Network.GuestType;
 import com.cloud.network.Networks.TrafficType;
 import com.cloud.network.router.VirtualRouter;
 import com.cloud.network.router.VirtualRouter.RedundantState;
+import com.cloud.resource.ResourceState;
 import com.cloud.user.Account;
 import com.cloud.utils.db.GenericDao;
 import com.cloud.vm.VirtualMachine;
@@ -129,6 +131,12 @@ public class DomainRouterJoinVO extends BaseViewVO 
implements ControlledViewEnti
     @Column(name = "host_name", nullable = false)
     private String hostName;
 
+    @Column(name = "host_status")
+    private Status hostStatus;
+
+    @Column(name = "host_resource_state")
+    private ResourceState hostResourceState;
+
     @Column(name="hypervisor_type")
     @Enumerated(value=EnumType.STRING)
     private Hypervisor.HypervisorType hypervisorType;
@@ -354,6 +362,14 @@ public class DomainRouterJoinVO extends BaseViewVO 
implements ControlledViewEnti
         return hostName;
     }
 
+    public Status getHostStatus() {
+        return hostStatus;
+    }
+
+    public ResourceState getHostResourceState() {
+        return hostResourceState;
+    }
+
     public Hypervisor.HypervisorType getHypervisorType() {
         return hypervisorType;
     }
diff --git a/server/src/main/java/com/cloud/api/query/vo/UserVmJoinVO.java 
b/server/src/main/java/com/cloud/api/query/vo/UserVmJoinVO.java
index 9a17a89c4b5..4fa4581f00d 100644
--- a/server/src/main/java/com/cloud/api/query/vo/UserVmJoinVO.java
+++ b/server/src/main/java/com/cloud/api/query/vo/UserVmJoinVO.java
@@ -29,9 +29,11 @@ import javax.persistence.Id;
 import javax.persistence.Table;
 import javax.persistence.Transient;
 
+import com.cloud.host.Status;
 import com.cloud.hypervisor.Hypervisor.HypervisorType;
 import com.cloud.network.Network.GuestType;
 import com.cloud.network.Networks.TrafficType;
+import com.cloud.resource.ResourceState;
 import com.cloud.storage.Storage.StoragePoolType;
 import com.cloud.storage.Volume;
 import com.cloud.user.Account;
@@ -171,9 +173,15 @@ public class UserVmJoinVO extends 
BaseViewWithTagInformationVO implements Contro
     @Column(name = "host_uuid")
     private String hostUuid;
 
-    @Column(name = "host_name", nullable = false)
+    @Column(name = "host_name")
     private String hostName;
 
+    @Column(name = "host_status")
+    private Status hostStatus;
+
+    @Column(name = "host_resource_state")
+    private ResourceState hostResourceState;
+
     @Column(name = "template_id", updatable = true, nullable = true, length = 
17)
     private long templateId;
 
@@ -604,6 +612,14 @@ public class UserVmJoinVO extends 
BaseViewWithTagInformationVO implements Contro
         return hostName;
     }
 
+    public Status getHostStatus() {
+        return hostStatus;
+    }
+
+    public ResourceState getHostResourceState() {
+        return hostResourceState;
+    }
+
     public long getTemplateId() {
         return templateId;
     }
diff --git a/test/integration/smoke/test_host_control_state.py 
b/test/integration/smoke/test_host_control_state.py
new file mode 100644
index 00000000000..809af7d2a0e
--- /dev/null
+++ b/test/integration/smoke/test_host_control_state.py
@@ -0,0 +1,252 @@
+#!/usr/bin/env python
+# Licensed to the Apache Software Foundation (ASF) under one
+# or more contributor license agreements.  See the NOTICE file
+# distributed with this work for additional information
+# regarding copyright ownership.  The ASF licenses this file
+# to you under the Apache License, Version 2.0 (the
+# "License"); you may not use this file except in compliance
+# with the License.  You may obtain a copy of the License at
+#
+#   http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing,
+# software distributed under the License is distributed on an
+# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+# KIND, either express or implied.  See the License for the
+# specific language governing permissions and limitations
+# under the License.
+
+"""
+Tests for host control state
+"""
+
+from marvin.cloudstackAPI import updateHost
+from nose.plugins.attrib import attr
+from marvin.cloudstackTestCase import cloudstackTestCase
+from marvin.lib.common import (get_domain,
+                               get_zone,
+                               get_template,
+                               list_hosts,
+                               list_routers,
+                               list_ssvms)
+from marvin.lib.base import (Account,
+                             Domain,
+                             Host,
+                             ServiceOffering,
+                             VirtualMachine)
+from marvin.sshClient import SshClient
+import time
+
+
+class TestHostControlState(cloudstackTestCase):
+
+    @classmethod
+    def setUpClass(cls):
+        cls.testClient = super(TestHostControlState, cls).getClsTestClient()
+        cls.apiclient = cls.testClient.getApiClient()
+        cls.services = cls.testClient.getParsedTestDataConfig()
+        # Get Zone, Domain and templates
+        cls.zone = get_zone(cls.apiclient, cls.testClient.getZoneForTests())
+        cls.hypervisor = cls.testClient.getHypervisorInfo()
+        cls.hostConfig = 
cls.config.__dict__["zones"][0].__dict__["pods"][0].__dict__["clusters"][0].__dict__["hosts"][0].__dict__
+
+        cls.template = get_template(
+            cls.apiclient,
+            cls.zone.id,
+            cls.hypervisor
+        )
+
+        cls.services["virtual_machine"]["zoneid"] = cls.zone.id
+        cls.services["template"] = cls.template.id
+        cls.services["zoneid"] = cls.zone.id
+
+        cls._cleanup = []
+
+        cls.domain = Domain.create(
+            cls.apiclient,
+            cls.services["acl"]["domain1"]
+        )
+        cls._cleanup.append(cls.domain)
+        cls.account = Account.create(
+            cls.apiclient,
+            cls.services["account"],
+            domainid=cls.domain.id
+        )
+        cls._cleanup.append(cls.account)
+        cls.service_offering = ServiceOffering.create(
+            cls.apiclient,
+            cls.services["service_offerings"]["tiny"]
+        )
+        cls._cleanup.append(cls.service_offering)
+        cls.vm = VirtualMachine.create(
+            cls.apiclient,
+            cls.services["virtual_machine"],
+            templateid=cls.template.id,
+            accountid=cls.account.name,
+            domainid=cls.account.domainid,
+            serviceofferingid=cls.service_offering.id
+        )
+        cls._cleanup.append(cls.vm)
+
+    @classmethod
+    def tearDownClass(cls):
+        super(TestHostControlState, cls).tearDownClass()
+
+    def setUp(self):
+        self.apiclient = self.testClient.getApiClient()
+        self.cleanup = []
+        return
+
+    def tearDown(self):
+        super(TestHostControlState, self).tearDown()
+
+    def disable_host(self, id):
+        cmd = updateHost.updateHostCmd()
+        cmd.id = id
+        cmd.allocationstate = "Disable"
+        response = self.apiclient.updateHost(cmd)
+        self.assertEqual(response.resourcestate, "Disabled")
+
+    def enable_host(self, id):
+        cmd = updateHost.updateHostCmd()
+        cmd.id = id
+        cmd.allocationstate = "Enable"
+        response = self.apiclient.updateHost(cmd)
+        self.assertEqual(response.resourcestate, "Enabled")
+
+    def get_host_ipaddress(self, hostId):
+        hosts = list_hosts(
+            self.apiclient,
+            type='Routing',
+            id=hostId
+        )
+        return hosts[0].ipaddress
+
+    def stop_agent(self, host_ipaddress):
+        SshClient(host_ipaddress, port=22, user=self.hostConfig["username"], 
passwd=self.hostConfig["password"]).execute\
+            ("systemctl stop cloudstack-agent || service cloudstack-agent 
stop")
+
+    def start_agent(self, host_ipaddress):
+        SshClient(host_ipaddress, port=22, user=self.hostConfig["username"], 
passwd=self.hostConfig["password"]).execute\
+            ("systemctl start cloudstack-agent || service cloudstack-agent 
start")
+
+    def verify_uservm_host_control_state(self, vm_id, state):
+        list_vms = VirtualMachine.list(
+            self.apiclient,
+            id=vm_id
+        )
+        vm = list_vms[0]
+        self.assertEqual(vm.hostcontrolstate,
+                         state,
+                         msg="host control state should be %s, but it is %s" % 
(state, vm.hostcontrolstate))
+
+    def verify_ssvm_host_control_state(self, vm_id, state):
+        list_ssvm_response = list_ssvms(
+            self.apiclient,
+            id=vm_id
+        )
+        vm = list_ssvm_response[0]
+        self.assertEqual(vm.hostcontrolstate,
+                         state,
+                         msg="host control state should be %s, but it is %s" % 
(state, vm.hostcontrolstate))
+
+    def verify_router_host_control_state(self, vm_id, state):
+        list_router_response = list_routers(
+            self.apiclient,
+            id=vm_id
+        )
+        vm = list_router_response[0]
+        self.assertEqual(vm.hostcontrolstate,
+                         state,
+                         msg="host control state should be %s, but it is %s" % 
(state, vm.hostcontrolstate))
+
+    @attr(tags=["basic", "advanced"], required_hardware="false")
+    def test_uservm_host_control_state(self):
+        """ Verify host control state for user vm """
+        # 1. verify hostcontrolstate = Enabled
+        # 2. Disable the host, verify hostcontrolstate = Disabled
+
+        list_vms = VirtualMachine.list(
+            self.apiclient,
+            id=self.vm.id
+        )
+        host_id = list_vms[0].hostid
+
+        self.verify_uservm_host_control_state(self.vm.id, "Enabled")
+
+        self.disable_host(host_id)
+        self.verify_uservm_host_control_state(self.vm.id, "Disabled")
+
+        if self.hypervisor == "kvm":
+            host_ipaddress = self.get_host_ipaddress(host_id)
+
+            self.stop_agent(host_ipaddress)
+            time.sleep(5)  # wait for the host to be Disconnected
+            self.verify_uservm_host_control_state(self.vm.id, "Offline")
+
+            self.enable_host(host_id)
+            self.verify_uservm_host_control_state(self.vm.id, "Offline")
+
+            self.start_agent(host_ipaddress)
+            time.sleep(10)  # wait for the host to be Up
+            self.verify_uservm_host_control_state(self.vm.id, "Enabled")
+
+        else:
+            self.enable_host(host_id)
+            self.verify_uservm_host_control_state(self.vm.id, "Enabled")
+
+    @attr(tags=["basic", "advanced"], required_hardware="false")
+    def test_ssvm_host_control_state(self):
+        """ Verify host control state for systemvm """
+        # 1. verify hostcontrolstate = Enabled
+        # 2. Disable the host, verify hostcontrolstate = Disabled
+
+        list_ssvm_response = list_ssvms(
+            self.apiclient,
+            systemvmtype='secondarystoragevm',
+            state='Running',
+            zoneid=self.zone.id
+        )
+        self.assertEqual(
+            isinstance(list_ssvm_response, list),
+            True,
+            "Check list response returns a valid list"
+        )
+        ssvm = list_ssvm_response[0]
+        host_id = ssvm.hostid
+
+        self.verify_ssvm_host_control_state(ssvm.id, "Enabled")
+
+        self.disable_host(host_id)
+        self.verify_ssvm_host_control_state(ssvm.id, "Disabled")
+
+        self.enable_host(host_id)
+        self.verify_ssvm_host_control_state(ssvm.id, "Enabled")
+
+    @attr(tags=["basic", "advanced"], required_hardware="false")
+    def test_router_host_control_state(self):
+        """ Verify host control state for router """
+        # 1. verify hostcontrolstate = Enabled
+        # 2. Disable the host, verify hostcontrolstate = Disabled
+
+        list_router_response = list_routers(
+            self.apiclient,
+            state='Running',
+            listall=True,
+            zoneid=self.zone.id
+        )
+        self.assertEqual(
+            isinstance(list_router_response, list),
+            True,
+            "Check list response returns a valid list"
+        )
+        router = list_router_response[0]
+        host_id = router.hostid
+
+        self.verify_router_host_control_state(router.id, "Enabled")
+
+        self.disable_host(host_id)
+        self.verify_router_host_control_state(router.id, "Disabled")
+
+        self.enable_host(host_id)
+        self.verify_router_host_control_state(router.id, "Enabled")
diff --git a/ui/public/locales/en.json b/ui/public/locales/en.json
index 222ac634a25..25f86c33b9f 100644
--- a/ui/public/locales/en.json
+++ b/ui/public/locales/en.json
@@ -821,6 +821,7 @@
 "label.host.alerts": "Hosts in alert state",
 "label.host.name": "Host name",
 "label.host.tag": "Host tag",
+"label.hostcontrolstate": "Control Plane Status",
 "label.hostid": "Host",
 "label.hostname": "Host",
 "label.hostnamelabel": "Host name",
@@ -2424,6 +2425,8 @@
 "message.chart.statistic.info": "The shown charts are self-adjustable, that 
means, if the value gets close to the limit or overpass it, it will grow to 
adjust the shown value",
 "message.guest.traffic.in.advanced.zone": "Guest network traffic is 
communication between end-user virtual machines. Specify a range of VLAN IDs or 
VXLAN network identifiers (VNIs) to carry guest traffic for each physical 
network.",
 "message.guest.traffic.in.basic.zone": "Guest network traffic is communication 
between end-user virtual machines. Specify a range of IP addresses that 
CloudStack can assign to guest VMs. Make sure this range does not overlap the 
reserved system IP range.",
+"message.host.controlstate": "The Control Plane Status of this instance is ",
+"message.host.controlstate.retry": "Some actions on this instance will fail, 
if so please wait a while and retry.",
 "message.host.dedicated": "Host Dedicated",
 "message.host.dedication.released": "Host dedication released.",
 "message.info.cloudian.console": "Cloudian Management Console should open in 
another window.",
diff --git a/ui/src/components/view/DetailsTab.vue 
b/ui/src/components/view/DetailsTab.vue
index ee5e0db3868..17a4e958dd0 100644
--- a/ui/src/components/view/DetailsTab.vue
+++ b/ui/src/components/view/DetailsTab.vue
@@ -16,6 +16,13 @@
 // under the License.
 
 <template>
+  <a-alert type="error" v-if="['vm', 'systemvm', 'router', 
'ilbvm'].includes($route.meta.name) && 'hostcontrolstate' in resource && 
resource.hostcontrolstate !== 'Enabled'">
+    <template #message>
+      <div class="title">
+        {{ $t('message.host.controlstate') }} {{ resource.hostcontrolstate }}. 
{{ $t('message.host.controlstate.retry') }}
+      </div>
+    </template>
+  </a-alert>
   <a-alert v-if="ip6routes" type="info" :showIcon="true" 
:message="$t('label.add.upstream.ipv6.routes')">
     <template #description>
       <p v-html="ip6routes" />
diff --git a/ui/src/components/widgets/Console.vue 
b/ui/src/components/widgets/Console.vue
index 7125dfabe87..03a37a2e03a 100644
--- a/ui/src/components/widgets/Console.vue
+++ b/ui/src/components/widgets/Console.vue
@@ -19,7 +19,7 @@
   <a
     v-if="['vm', 'systemvm', 'router', 'ilbvm'].includes($route.meta.name) && 
'listVirtualMachines' in $store.getters.apis && 'createConsoleEndpoint' in 
$store.getters.apis"
     @click="consoleUrl">
-    <a-button style="margin-left: 5px" shape="circle" type="dashed" 
:size="size" :disabled="['Stopped', 'Error', 
'Destroyed'].includes(resource.state)" >
+    <a-button style="margin-left: 5px" shape="circle" type="dashed" 
:size="size" :disabled="['Stopped', 'Error', 
'Destroyed'].includes(resource.state) || resource.hostcontrolstate === 
'Offline'" >
       <code-outlined />
     </a-button>
   </a>
diff --git a/ui/src/config/section/compute.js b/ui/src/config/section/compute.js
index 591c312d73d..476b055e7fd 100644
--- a/ui/src/config/section/compute.js
+++ b/ui/src/config/section/compute.js
@@ -79,7 +79,7 @@ export default {
       details: () => {
         var fields = ['displayname', 'name', 'id', 'state', 'ipaddress', 
'ip6address', 'templatename', 'ostypename',
           'serviceofferingname', 'isdynamicallyscalable', 'haenable', 
'hypervisor', 'boottype', 'bootmode', 'account',
-          'domain', 'zonename', 'userdataid', 'userdataname', 
'userdataparams', 'userdatadetails', 'userdatapolicy']
+          'domain', 'zonename', 'userdataid', 'userdataname', 
'userdataparams', 'userdatadetails', 'userdatapolicy', 'hostcontrolstate']
         const listZoneHaveSGEnabled = store.getters.zones.filter(zone => 
zone.securitygroupsenabled === true)
         if (!listZoneHaveSGEnabled || listZoneHaveSGEnabled.length === 0) {
           return fields
@@ -142,6 +142,7 @@ export default {
           docHelp: 
'adminguide/virtual_machines.html#stopping-and-starting-vms',
           dataView: true,
           show: (record) => { return ['Running'].includes(record.state) },
+          disabled: (record) => { return record.hostcontrolstate === 'Offline' 
},
           args: (record, store) => {
             var fields = []
             fields.push('forced')
@@ -169,6 +170,7 @@ export default {
               value: (record) => { return record.id }
             }
           },
+          disabled: (record) => { return record.hostcontrolstate === 'Offline' 
},
           successMethod: (obj, result) => {
             const vm = result.jobresult.virtualmachine || {}
             if (result.jobstatus === 1 && vm.password) {
@@ -193,6 +195,7 @@ export default {
               (['Stopped'].includes(record.state) && ((record.hypervisor !== 
'KVM' && record.hypervisor !== 'LXC') ||
               (record.hypervisor === 'KVM' && record.pooltype === 
'PowerFlex'))))
           },
+          disabled: (record) => { return record.hostcontrolstate === 'Offline' 
&& record.hypervisor === 'KVM' },
           mapping: {
             virtualmachineid: {
               value: (record, params) => { return record.id }
@@ -210,6 +213,7 @@ export default {
             return ((['Running'].includes(record.state) && record.hypervisor 
!== 'LXC') ||
               (['Stopped'].includes(record.state) && !['KVM', 
'LXC'].includes(record.hypervisor)))
           },
+          disabled: (record) => { return record.hostcontrolstate === 'Offline' 
&& record.hypervisor === 'KVM' },
           component: shallowRef(defineAsyncComponent(() => 
import('@/views/compute/CreateSnapshotWizard.vue')))
         },
         {
@@ -283,6 +287,7 @@ export default {
           dataView: true,
           popup: true,
           show: (record) => { return ['Running', 
'Stopped'].includes(record.state) && !record.isoid },
+          disabled: (record) => { return record.hostcontrolstate === 'Offline' 
|| record.hostcontrolstate === 'Maintenance' },
           component: shallowRef(defineAsyncComponent(() => 
import('@/views/compute/AttachIso.vue')))
         },
         {
@@ -299,6 +304,7 @@ export default {
             return args
           },
           show: (record) => { return ['Running', 
'Stopped'].includes(record.state) && 'isoid' in record && record.isoid },
+          disabled: (record) => { return record.hostcontrolstate === 'Offline' 
|| record.hostcontrolstate === 'Maintenance' },
           mapping: {
             virtualmachineid: {
               value: (record, params) => { return record.id }
@@ -334,6 +340,7 @@ export default {
           docHelp: 
'adminguide/virtual_machines.html#moving-vms-between-hosts-manual-live-migration',
           dataView: true,
           show: (record, store) => { return ['Running'].includes(record.state) 
&& ['Admin'].includes(store.userInfo.roletype) },
+          disabled: (record) => { return record.hostcontrolstate === 'Offline' 
},
           popup: true,
           component: shallowRef(defineAsyncComponent(() => 
import('@/views/compute/MigrateWizard.vue')))
         },
@@ -345,6 +352,7 @@ export default {
           docHelp: 
'adminguide/virtual_machines.html#moving-vms-between-hosts-manual-live-migration',
           dataView: true,
           show: (record, store) => { return ['Stopped'].includes(record.state) 
&& ['Admin'].includes(store.userInfo.roletype) },
+          disabled: (record) => { return record.hostcontrolstate === 'Offline' 
},
           component: shallowRef(defineAsyncComponent(() => 
import('@/views/compute/MigrateVMStorage'))),
           popup: true
         },
diff --git a/ui/src/config/section/infra/ilbvms.js 
b/ui/src/config/section/infra/ilbvms.js
index 1c27a960af6..beba33e24a7 100644
--- a/ui/src/config/section/infra/ilbvms.js
+++ b/ui/src/config/section/infra/ilbvms.js
@@ -23,7 +23,7 @@ export default {
   permission: ['listInternalLoadBalancerVMs'],
   params: { projectid: '-1' },
   columns: ['name', 'state', 'publicip', 'guestnetworkname', 'vpcname', 
'version', 'softwareversion', 'hostname', 'account', 'zonename', 
'requiresupgrade'],
-  details: ['name', 'id', 'version', 'softwareversion', 'requiresupgrade', 
'guestnetworkname', 'vpcname', 'publicip', 'guestipaddress', 'linklocalip', 
'serviceofferingname', 'networkdomain', 'isredundantrouter', 'redundantstate', 
'hostname', 'account', 'zonename', 'created'],
+  details: ['name', 'id', 'version', 'softwareversion', 'requiresupgrade', 
'guestnetworkname', 'vpcname', 'publicip', 'guestipaddress', 'linklocalip', 
'serviceofferingname', 'networkdomain', 'isredundantrouter', 'redundantstate', 
'hostname', 'account', 'zonename', 'created', 'hostcontrolstate'],
   actions: [
     {
       api: 'startInternalLoadBalancerVM',
@@ -53,6 +53,7 @@ export default {
       label: 'label.action.migrate.router',
       dataView: true,
       show: (record, store) => { return record.state === 'Running' && 
['Admin'].includes(store.userInfo.roletype) },
+      disabled: (record) => { return record.hostcontrolstate === 'Offline' },
       component: shallowRef(defineAsyncComponent(() => 
import('@/views/compute/MigrateWizard'))),
       popup: true
     },
@@ -62,6 +63,7 @@ export default {
       label: 'label.action.migrate.systemvm.to.ps',
       dataView: true,
       show: (record, store) => { return ['Stopped'].includes(record.state) && 
['VMware'].includes(record.hypervisor) },
+      disabled: (record) => { return record.hostcontrolstate === 'Offline' },
       component: shallowRef(defineAsyncComponent(() => 
import('@/views/compute/MigrateVMStorage'))),
       popup: true
     }
diff --git a/ui/src/config/section/infra/routers.js 
b/ui/src/config/section/infra/routers.js
index 9e69d6bccb2..498f8ac1176 100644
--- a/ui/src/config/section/infra/routers.js
+++ b/ui/src/config/section/infra/routers.js
@@ -31,7 +31,7 @@ export default {
     return columns
   },
   searchFilters: ['name', 'zoneid', 'podid', 'clusterid'],
-  details: ['name', 'id', 'version', 'softwareversion', 'requiresupgrade', 
'guestnetworkname', 'vpcname', 'publicip', 'guestipaddress', 'linklocalip', 
'serviceofferingname', 'networkdomain', 'isredundantrouter', 'redundantstate', 
'hostname', 'account', 'zonename', 'created'],
+  details: ['name', 'id', 'version', 'softwareversion', 'requiresupgrade', 
'guestnetworkname', 'vpcname', 'publicip', 'guestipaddress', 'linklocalip', 
'serviceofferingname', 'networkdomain', 'isredundantrouter', 'redundantstate', 
'hostname', 'account', 'zonename', 'created', 'hostcontrolstate'],
   resourceType: 'VirtualRouter',
   tabs: [{
     name: 'details',
@@ -188,6 +188,7 @@ export default {
       message: 'message.migrate.router.confirm',
       dataView: true,
       show: (record, store) => { return record.state === 'Running' && 
['Admin'].includes(store.userInfo.roletype) },
+      disabled: (record) => { return record.hostcontrolstate === 'Offline' },
       component: shallowRef(defineAsyncComponent(() => 
import('@/views/compute/MigrateWizard'))),
       popup: true
     },
@@ -197,6 +198,7 @@ export default {
       label: 'label.action.migrate.systemvm.to.ps',
       dataView: true,
       show: (record, store) => { return ['Stopped'].includes(record.state) && 
['VMware', 'KVM'].includes(record.hypervisor) },
+      disabled: (record) => { return record.hostcontrolstate === 'Offline' },
       component: shallowRef(defineAsyncComponent(() => 
import('@/views/compute/MigrateVMStorage'))),
       popup: true
     },
diff --git a/ui/src/config/section/infra/systemVms.js 
b/ui/src/config/section/infra/systemVms.js
index 70f52e3d899..9774e2290c7 100644
--- a/ui/src/config/section/infra/systemVms.js
+++ b/ui/src/config/section/infra/systemVms.js
@@ -25,7 +25,7 @@ export default {
   docHelp: 'adminguide/systemvm.html',
   permission: ['listSystemVms'],
   columns: ['name', 'state', 'agentstate', 'systemvmtype', 'publicip', 
'privateip', 'linklocalip', 'hostname', 'zonename'],
-  details: ['name', 'id', 'agentstate', 'systemvmtype', 'publicip', 
'privateip', 'linklocalip', 'gateway', 'hostname', 'zonename', 'created', 
'activeviewersessions', 'isdynamicallyscalable'],
+  details: ['name', 'id', 'agentstate', 'systemvmtype', 'publicip', 
'privateip', 'linklocalip', 'gateway', 'hostname', 'zonename', 'created', 
'activeviewersessions', 'isdynamicallyscalable', 'hostcontrolstate'],
   resourceType: 'SystemVm',
   tabs: [
     {
@@ -105,6 +105,7 @@ export default {
       message: 'message.migrate.systemvm.confirm',
       dataView: true,
       show: (record, store) => { return record.state === 'Running' && 
['Admin'].includes(store.userInfo.roletype) },
+      disabled: (record) => { return record.hostcontrolstate === 'Offline' },
       component: shallowRef(defineAsyncComponent(() => 
import('@/views/compute/MigrateWizard'))),
       popup: true
     },
@@ -114,6 +115,7 @@ export default {
       label: 'label.action.migrate.systemvm.to.ps',
       dataView: true,
       show: (record, store) => { return ['Stopped'].includes(record.state) && 
['VMware', 'KVM'].includes(record.hypervisor) },
+      disabled: (record) => { return record.hostcontrolstate === 'Offline' },
       component: shallowRef(defineAsyncComponent(() => 
import('@/views/compute/MigrateVMStorage'))),
       popup: true
     },


Reply via email to