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
},