Apple FR66 - Host Control Plane Status (#213)

* Apple FR66 - Host Control Plane Status

* Apple FR66 - use Offline instead of Disconnected

* Apple FR66: fix smoke test test_router_host_control_state

* Apple FR66: reorder import

* Apple FR66: revert DetailsTab.vue and apply new changes

* Update PR: update en.json

* Update PR: add hostcontrolstate to routers/systemvms

* Update PR: test stop/start cloudstack-agent in smoke test

* Update PR: fix UI build error (The template root requires exactly one element  vue/valid-template-root)

* Update PR: update message on ui

* Update PR: Disable rebootVM and create vm/volume snapshot for KVM VMs

* Update PR: add more unit tests
This commit is contained in:
Wei Zhou 2023-01-04 13:01:10 +01:00 committed by GitHub
parent f77ab62377
commit 11f38ad102
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
20 changed files with 648 additions and 47 deletions

View File

@ -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;
}
}
}

View File

@ -192,6 +192,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";

View File

@ -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;
@ -298,6 +302,10 @@ public class DomainRouterResponse extends BaseResponseWithAnnotations implements
this.hostName = hostName;
}
public void setHostControlState(String hostControlState) {
this.hostControlState = hostControlState;
}
public String getHypervisor() {
return hypervisor;
}

View File

@ -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;
}

View File

@ -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;
@ -449,6 +453,10 @@ public class UserVmResponse extends BaseResponseWithTagInformation implements Co
return hostName;
}
public String getHostControlState() {
return hostControlState;
}
public String getTemplateId() {
return templateId;
}
@ -687,6 +695,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;
}

View File

@ -0,0 +1,98 @@
// Licensed to the Apache Software Foundation (ASF) under one
// or more contributor license agreements. See the NOTICE file
// distributed with this work for additional information
// regarding copyright ownership. The ASF licenses this file
// to you under the Apache License, Version 2.0 (the
// "License"); you may not use this file except in compliance
// with the License. You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing,
// software distributed under the License is distributed on an
// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
// KIND, either express or implied. See the License for the
// specific language governing permissions and limitations
// under the License.
package 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 testHostControlState() {
// 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);
// 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);
// 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 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);
// 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);
// 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);
}
}

View File

@ -586,6 +586,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`,
@ -725,3 +727,110 @@ FROM
AND (`custom_speed`.`name` = 'CpuSpeed'))))
LEFT JOIN `user_vm_details` `custom_ram_size` ON (((`custom_ram_size`.`vm_id` = `vm_instance`.`id`)
AND (`custom_ram_size`.`name` = 'memory'))));
DROP VIEW IF EXISTS `cloud`.`domain_router_view`;
CREATE VIEW `cloud`.`domain_router_view` AS
select
vm_instance.id id,
vm_instance.name name,
account.id account_id,
account.uuid account_uuid,
account.account_name account_name,
account.type account_type,
domain.id domain_id,
domain.uuid domain_uuid,
domain.name domain_name,
domain.path domain_path,
projects.id project_id,
projects.uuid project_uuid,
projects.name project_name,
vm_instance.uuid uuid,
vm_instance.created created,
vm_instance.state state,
vm_instance.removed removed,
vm_instance.pod_id pod_id,
vm_instance.instance_name instance_name,
host_pod_ref.uuid pod_uuid,
data_center.id data_center_id,
data_center.uuid data_center_uuid,
data_center.name data_center_name,
data_center.networktype data_center_type,
data_center.dns1 dns1,
data_center.dns2 dns2,
data_center.ip6_dns1 ip6_dns1,
data_center.ip6_dns2 ip6_dns2,
host.id host_id,
host.uuid host_uuid,
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,
disk_offering.uuid service_offering_uuid,
disk_offering.name service_offering_name,
nics.id nic_id,
nics.uuid nic_uuid,
nics.network_id network_id,
nics.ip4_address ip_address,
nics.ip6_address ip6_address,
nics.ip6_gateway ip6_gateway,
nics.ip6_cidr ip6_cidr,
nics.default_nic is_default_nic,
nics.gateway gateway,
nics.netmask netmask,
nics.mac_address mac_address,
nics.broadcast_uri broadcast_uri,
nics.isolation_uri isolation_uri,
vpc.id vpc_id,
vpc.uuid vpc_uuid,
vpc.name vpc_name,
networks.uuid network_uuid,
networks.name network_name,
networks.network_domain network_domain,
networks.traffic_type traffic_type,
networks.guest_type guest_type,
async_job.id job_id,
async_job.uuid job_uuid,
async_job.job_status job_status,
async_job.account_id job_account_id,
domain_router.template_version template_version,
domain_router.scripts_version scripts_version,
domain_router.is_redundant_router is_redundant_router,
domain_router.redundant_state redundant_state,
domain_router.stop_pending stop_pending,
domain_router.role role
from
`cloud`.`domain_router`
inner join
`cloud`.`vm_instance` ON vm_instance.id = domain_router.id
inner join
`cloud`.`account` ON vm_instance.account_id = account.id
inner join
`cloud`.`domain` ON vm_instance.domain_id = domain.id
left join
`cloud`.`host_pod_ref` ON vm_instance.pod_id = host_pod_ref.id
left join
`cloud`.`projects` ON projects.project_account_id = account.id
left join
`cloud`.`data_center` ON vm_instance.data_center_id = data_center.id
left join
`cloud`.`host` ON vm_instance.host_id = host.id
left join
`cloud`.`vm_template` ON vm_instance.vm_template_id = vm_template.id
left join
`cloud`.`service_offering` ON vm_instance.service_offering_id = service_offering.id
left join
`cloud`.`disk_offering` ON vm_instance.service_offering_id = disk_offering.id
left join
`cloud`.`nics` ON vm_instance.id = nics.instance_id and nics.removed is null
left join
`cloud`.`networks` ON nics.network_id = networks.id
left join
`cloud`.`vpc` ON domain_router.vpc_id = vpc.id and vpc.removed is null
left join
`cloud`.`async_job` ON async_job.instance_id = vm_instance.id
and async_job.instance_type = 'DomainRouter'
and async_job.job_status = 0;

View File

@ -36,6 +36,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 org.apache.cloudstack.acl.ControlledEntity;
@ -1499,6 +1500,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());
vmResponse.setHypervisor(host.getHypervisorType().toString());
}
} else if (vm.getLastHostId() != null) {

View File

@ -37,6 +37,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;
@ -110,6 +111,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.setHypervisor(router.getHypervisorType().toString());
}
routerResponse.setPodId(router.getPodUuid());

View File

@ -49,6 +49,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;
@ -166,6 +167,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());

View File

@ -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.utils.db.GenericDao;
import com.cloud.vm.VirtualMachine;
import com.cloud.vm.VirtualMachine.State;
@ -127,6 +129,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;
@ -346,6 +354,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;
}

View File

@ -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.utils.db.GenericDao;
@ -169,9 +171,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;
@ -596,6 +604,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;
}

View File

@ -0,0 +1,254 @@
#!/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.domain = Domain.create(
cls.apiclient,
cls.services["acl"]["domain1"]
)
cls.account = Account.create(
cls.apiclient,
cls.services["account"],
domainid=cls.domain.id
)
cls.service_offering = ServiceOffering.create(
cls.apiclient,
cls.services["service_offerings"]["tiny"]
)
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 = [
cls.domain,
cls.account,
cls.vm,
cls.service_offering
]
return
@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")

View File

@ -1068,6 +1068,7 @@
"label.host.name": "Host Name",
"label.host.tag": "Host Tag",
"label.host.ueficapability": "UEFI Supported",
"label.hostcontrolstate": "Control Plane Status",
"label.hostid": "Host",
"label.hostname": "Host",
"label.hostnamelabel": "Host Name",
@ -3041,6 +3042,8 @@
"message.guestnetwork.state.implementing": "Indicates the network configuration is being implemented",
"message.guestnetwork.state.setup": "Indicates the network configuration is setup",
"message.guestnetwork.state.shutdown": "Indicates the network configuration is being destroyed",
"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",

View File

@ -16,52 +16,61 @@
// under the License.
<template>
<a-list
size="small"
:dataSource="fetchDetails()">
<a-list-item slot="renderItem" slot-scope="item" v-if="item in resource">
<div>
<strong>{{ item === 'service' ? $t('label.supportedservices') : $t('label.' + String(item).toLowerCase()) }}</strong>
<br/>
<div v-if="Array.isArray(resource[item]) && item === 'service'">
<div v-for="(service, idx) in resource[item]" :key="idx">
{{ service.name }} : {{ service.provider[0].name }}
<div>
<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-list
size="small"
:dataSource="fetchDetails()">
<a-list-item slot="renderItem" slot-scope="item" v-if="item in resource">
<div>
<strong>{{ item === 'service' ? $t('label.supportedservices') : $t('label.' + String(item).toLowerCase()) }}</strong>
<br/>
<div v-if="Array.isArray(resource[item]) && item === 'service'">
<div v-for="(service, idx) in resource[item]" :key="idx">
{{ service.name }} : {{ service.provider[0].name }}
</div>
</div>
</div>
<div v-else-if="$route.meta.name === 'backup' && item === 'volumes'">
<div v-for="(volume, idx) in JSON.parse(resource[item])" :key="idx">
<router-link :to="{ path: '/volume/' + volume.uuid }">{{ volume.type }} - {{ volume.path }}</router-link> ({{ parseFloat(volume.size / (1024.0 * 1024.0 * 1024.0)).toFixed(1) }} GB)
<div v-else-if="$route.meta.name === 'backup' && item === 'volumes'">
<div v-for="(volume, idx) in JSON.parse(resource[item])" :key="idx">
<router-link :to="{ path: '/volume/' + volume.uuid }">{{ volume.type }} - {{ volume.path }}</router-link> ({{ parseFloat(volume.size / (1024.0 * 1024.0 * 1024.0)).toFixed(1) }} GB)
</div>
</div>
</div>
<div v-else-if="$route.meta.name === 'computeoffering' && item === 'rootdisksize'">
<div>
{{ resource.rootdisksize }} GB
<div v-else-if="$route.meta.name === 'computeoffering' && item === 'rootdisksize'">
<div>
{{ resource.rootdisksize }} GB
</div>
</div>
<div v-else-if="['name', 'type'].includes(item)">
<span v-if="['USER.LOGIN', 'USER.LOGOUT', 'ROUTER.HEALTH.CHECKS', 'FIREWALL.CLOSE', 'ALERT.SERVICE.DOMAINROUTER'].includes(resource[item])">{{ $t(resource[item].toLowerCase()) }}</span>
<span v-else>{{ resource[item] }}</span>
</div>
<div v-else-if="['created', 'sent', 'lastannotated'].includes(item)">
{{ $toLocaleDate(resource[item]) }}
</div>
<div v-else-if="$route.meta.name === 'userdata' && item === 'userdata'">
<div style="white-space: pre-wrap;"> {{ decodeUserData(resource.userdata) }} </div>
</div>
<div v-else>{{ resource[item] }}</div>
</div>
<div v-else-if="['name', 'type'].includes(item)">
<span v-if="['USER.LOGIN', 'USER.LOGOUT', 'ROUTER.HEALTH.CHECKS', 'FIREWALL.CLOSE', 'ALERT.SERVICE.DOMAINROUTER'].includes(resource[item])">{{ $t(resource[item].toLowerCase()) }}</span>
<span v-else>{{ resource[item] }}</span>
</a-list-item>
<a-list-item slot="renderItem" slot-scope="item" v-else-if="item === 'ip6address' && ipV6Address && ipV6Address.length > 0">
<div>
<strong>{{ $t('label.' + String(item).toLowerCase()) }}</strong>
<br/>
<div>{{ ipV6Address }}</div>
</div>
<div v-else-if="['created', 'sent', 'lastannotated'].includes(item)">
{{ $toLocaleDate(resource[item]) }}
</div>
<div v-else-if="$route.meta.name === 'userdata' && item === 'userdata'">
<div style="white-space: pre-wrap;"> {{ decodeUserData(resource.userdata) }} </div>
</div>
<div v-else>{{ resource[item] }}</div>
</div>
</a-list-item>
<a-list-item slot="renderItem" slot-scope="item" v-else-if="item === 'ip6address' && ipV6Address && ipV6Address.length > 0">
<div>
<strong>{{ $t('label.' + String(item).toLowerCase()) }}</strong>
<br/>
<div>{{ ipV6Address }}</div>
</div>
</a-list-item>
<HostInfo :resource="resource" v-if="$route.meta.name === 'host' && 'listHosts' in $store.getters.apis" />
<DedicateData :resource="resource" v-if="dedicatedSectionActive" />
<VmwareData :resource="resource" v-if="$route.meta.name === 'zone' && 'listVmwareDcs' in $store.getters.apis" />
</a-list>
</a-list-item>
<HostInfo :resource="resource" v-if="$route.meta.name === 'host' && 'listHosts' in $store.getters.apis" />
<DedicateData :resource="resource" v-if="dedicatedSectionActive" />
<VmwareData :resource="resource" v-if="$route.meta.name === 'zone' && 'listVmwareDcs' in $store.getters.apis" />
</a-list>
</div>
</template>
<script>

View File

@ -19,7 +19,7 @@
<a
v-if="['vm', 'systemvm', 'router', 'ilbvm'].includes($route.meta.name) && 'updateVirtualMachine' 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'" >
<a-icon type="code" />
</a-button>
</a>

View File

@ -66,7 +66,7 @@ export default {
return fields
},
searchFilters: ['name', 'zoneid', 'domainid', 'account', 'tags'],
details: ['displayname', 'name', 'id', 'state', 'ipaddress', 'ip6address', 'templatename', 'ostypename', 'serviceofferingname', 'isdynamicallyscalable', 'haenable', 'hypervisor', 'boottype', 'bootmode', 'account', 'domain', 'zonename', 'userdataid', 'userdataname', 'userdataparams', 'userdatadetails', 'userdatapolicy'],
details: ['displayname', 'name', 'id', 'state', 'ipaddress', 'ip6address', 'templatename', 'ostypename', 'serviceofferingname', 'isdynamicallyscalable', 'haenable', 'hypervisor', 'boottype', 'bootmode', 'account', 'domain', 'zonename', 'userdataid', 'userdataname', 'userdataparams', 'userdatadetails', 'userdatapolicy', 'hostcontrolstate'],
tabs: [{
component: () => import('@/views/compute/InstanceTab.vue')
}],
@ -121,6 +121,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')
@ -148,6 +149,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) {
@ -172,6 +174,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 }
@ -189,6 +192,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: () => import('@/views/compute/CreateSnapshotWizard.vue')
},
{
@ -262,6 +266,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: () => import('@/views/compute/AttachIso.vue')
},
{
@ -278,6 +283,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 }
@ -313,6 +319,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: () => import('@/views/compute/MigrateWizard')
},
@ -324,6 +331,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: () => import('@/views/compute/MigrateVMStorage'),
popup: true
},

View File

@ -22,7 +22,7 @@ export default {
permission: ['listInternalLoadBalancerVMs'],
params: { projectid: '-1' },
columns: ['name', 'state', 'publicip', 'guestnetworkname', 'vpcname', 'version', 'hostname', 'account', 'zonename', 'requiresupgrade'],
details: ['name', 'id', 'version', 'requiresupgrade', 'guestnetworkname', 'vpcname', 'publicip', 'guestipaddress', 'linklocalip', 'serviceofferingname', 'networkdomain', 'isredundantrouter', 'redundantstate', 'hostname', 'account', 'zonename', 'created'],
details: ['name', 'id', 'version', 'requiresupgrade', 'guestnetworkname', 'vpcname', 'publicip', 'guestipaddress', 'linklocalip', 'serviceofferingname', 'networkdomain', 'isredundantrouter', 'redundantstate', 'hostname', 'account', 'zonename', 'created', 'hostcontrolstate'],
actions: [
{
api: 'startInternalLoadBalancerVM',
@ -52,6 +52,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: () => import('@/views/compute/MigrateWizard'),
popup: true
},
@ -61,6 +62,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: () => import('@/views/compute/MigrateVMStorage'),
popup: true
}

View File

@ -26,7 +26,7 @@ export default {
params: { projectid: '-1' },
columns: ['name', 'state', 'publicip', 'guestnetworkname', 'vpcname', 'redundantstate', 'version', 'hostname', 'account', 'zonename', 'requiresupgrade'],
searchFilters: ['name', 'zoneid', 'podid', 'clusterid'],
details: ['name', 'id', 'version', 'requiresupgrade', 'guestnetworkname', 'vpcname', 'publicip', 'guestipaddress', 'linklocalip', 'serviceofferingname', 'networkdomain', 'isredundantrouter', 'redundantstate', 'hostname', 'account', 'zonename', 'created'],
details: ['name', 'id', 'version', 'requiresupgrade', 'guestnetworkname', 'vpcname', 'publicip', 'guestipaddress', 'linklocalip', 'serviceofferingname', 'networkdomain', 'isredundantrouter', 'redundantstate', 'hostname', 'account', 'zonename', 'created', 'hostcontrolstate'],
resourceType: 'VirtualRouter',
tabs: [{
name: 'details',
@ -127,6 +127,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: () => import('@/views/compute/MigrateWizard'),
popup: true
},
@ -136,6 +137,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: () => import('@/views/compute/MigrateVMStorage'),
popup: true
},

View File

@ -24,7 +24,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: [
{
@ -100,6 +100,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: () => import('@/views/compute/MigrateWizard'),
popup: true
},
@ -109,6 +110,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: () => import('@/views/compute/MigrateVMStorage'),
popup: true
},