| # 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. |
| |
| import marvin |
| from marvin.cloudstackTestCase import * |
| from marvin.cloudstackAPI import * |
| from marvin.lib.utils import * |
| from marvin.lib.base import * |
| from marvin.lib.common import * |
| from nose.plugins.attrib import attr |
| |
| import random |
| |
| from ipmisim.ipmisim import IpmiServerContext, IpmiServer, ThreadedIpmiServer |
| |
| import random |
| import socket |
| import sys |
| import _thread |
| import time |
| |
| |
| class TestHAKVM(cloudstackTestCase): |
| """ Test cases for host HA using KVM host(s) |
| """ |
| |
| def setUp(self): |
| self.testClient = super(TestHAKVM, self).getClsTestClient() |
| self.apiclient = self.testClient.getApiClient() |
| self.dbclient = self.testClient.getDbConnection() |
| self.services = self.testClient.getParsedTestDataConfig() |
| self.logger = logging.getLogger('TestHAKVM') |
| |
| #Get Zone specifics |
| self.zone = get_zone(self.apiclient, self.testClient.getZoneForTests()) |
| self.hypervisor = self.testClient.getHypervisorInfo() |
| self.host = self.getHost() |
| self.hostConfig = self.config.__dict__["zones"][0].__dict__["pods"][0].__dict__["clusters"][0].__dict__["hosts"][0].__dict__ |
| self.mgtSvrDetails = self.config.__dict__["mgtSvr"][0].__dict__ |
| self.cluster_id = self.host.clusterid |
| |
| # Cleanup any existing configs |
| self.dbclient.execute("delete from ha_config where resource_type='Host'") |
| |
| # use random port for ipmisim |
| self.fakeMsId = random.randint(10000, 99999) * random.randint(10, 20) |
| s = socket.socket() |
| s.bind(('', 0)) |
| self.serverPort = s.getsockname()[1] |
| s.close() |
| |
| # Set Cluster-level setting in order to run tests faster |
| self.updateConfiguration("kvm.ha.activity.check.failure.ratio", "0.6") |
| self.updateConfiguration("kvm.ha.activity.check.interval", "8") |
| self.updateConfiguration("kvm.ha.activity.check.max.attempts", "5") |
| self.updateConfiguration("kvm.ha.activity.check.timeout", "30") |
| self.updateConfiguration("kvm.ha.degraded.max.period", "30") |
| self.updateConfiguration("kvm.ha.fence.timeout", "30") |
| self.updateConfiguration("kvm.ha.health.check.timeout", "30") |
| self.updateConfiguration("kvm.ha.recover.failure.threshold", "2") |
| self.updateConfiguration("kvm.ha.recover.timeout", "30") |
| self.updateConfiguration("kvm.ha.recover.wait.period", "30") |
| |
| self.service_offering = ServiceOffering.create( |
| self.apiclient, |
| self.services["service_offerings"]["hasmall"] |
| ) |
| |
| self.template = get_test_template( |
| self.apiclient, |
| self.zone.id, |
| self.hypervisor |
| ) |
| |
| self.configureAndDisableHostHa() |
| self.cleanup = [self.service_offering] |
| |
| def updateConfiguration(self, name, value): |
| cmd = updateConfiguration.updateConfigurationCmd() |
| cmd.name = name |
| cmd.value = value |
| cmd.clusterid = self.cluster_id |
| self.apiclient.updateConfiguration(cmd) |
| |
| def getFakeMsId(self): |
| return self.fakeMsId |
| |
| def getFakeMsRunId(self): |
| return self.fakeMsId * 1000 |
| |
| def tearDown(self): |
| self.configureAndDisableHostHa() |
| self.host = None |
| try: |
| self.dbclient.execute("delete from mshost_peer where peer_runid=%s" % self.getFakeMsRunId()) |
| self.dbclient.execute("delete from mshost where runid=%s" % self.getFakeMsRunId()) |
| self.dbclient.execute("delete from cluster_details where name='resourceHAEnabled'") |
| self.dbclient.execute("delete from data_center_details where name='resourceHAEnabled'") |
| self.dbclient.execute("delete from ha_config where resource_type='Host'") |
| self.dbclient.execute("delete from oobm where port=%d" % self.getIpmiServerPort()) |
| self.dbclient.execute("delete from mshost_peer where peer_runid=%s" % self.getFakeMsRunId()) |
| self.dbclient.execute("delete from mshost where runid=%s" % self.getFakeMsRunId()) |
| self.dbclient.execute("delete from cluster_details where name='outOfBandManagementEnabled'") |
| self.dbclient.execute("delete from data_center_details where name='outOfBandManagementEnabled'") |
| cleanup_resources(self.apiclient, self.cleanup) |
| except Exception as e: |
| raise Exception("Warning: Exception during cleanup : %s" % e) |
| |
| def getHostHaEnableCmd(self): |
| cmd = enableHAForHost.enableHAForHostCmd() |
| cmd.hostid = self.host.id |
| return cmd |
| |
| def getHost(self, hostId=None): |
| response = list_hosts( |
| self.apiclient, |
| type='Routing', |
| hypervisor='kvm', |
| id=hostId |
| ) |
| # Check if more than one kvm hosts are available in order to successfully configure host-ha |
| if response and len(response) > 0: |
| self.host = response[0] |
| return self.host |
| raise self.skipTest("Not enough KVM hosts found, skipping host-ha test") |
| |
| def getHostHaConfigCmd(self, provider='kvmhaprovider'): |
| cmd = configureHAForHost.configureHAForHostCmd() |
| cmd.provider = provider |
| cmd.hostid = self.host.id |
| return cmd |
| |
| def getHostHaDisableCmd(self): |
| cmd = disableHAForHost.disableHAForHostCmd() |
| cmd.hostid = self.host.id |
| return cmd |
| |
| def configureAndEnableHostHa(self): |
| #Adding sleep between configuring and enabling |
| self.apiclient.configureHAForHost(self.getHostHaConfigCmd()) |
| response = self.apiclient.enableHAForHost(self.getHostHaEnableCmd()) |
| self.assertEqual(response.haenable, True) |
| |
| def configureAndDisableHostHa(self): |
| self.apiclient.configureHAForHost(self.getHostHaConfigCmd()) |
| cmd = self.getHostHaDisableCmd() |
| cmd.hostid = self.host.id |
| response = self.apiclient.disableHAForHost(cmd) |
| self.assertEqual(response.hostid, cmd.hostid) |
| self.assertEqual(response.haenable, False) |
| |
| def enableHostHa(self, hostId): |
| cmd = self.getHostHaEnableCmd() |
| cmd.hostid = hostId |
| response = self.apiclient.enableHAForHost(cmd) |
| self.assertEqual(response.hostid, cmd.hostid) |
| self.assertEqual(response.haenable, True) |
| return response |
| |
| def disableAgent(self): |
| SshClient(self.host.ipaddress, port=22, user=self.hostConfig["username"], passwd=self.hostConfig["password"]).execute\ |
| ("systemctl disable cloudstack-agent || chkconfig cloudstack-agent off") |
| |
| def resetHost(self): |
| SshClient(self.host.ipaddress, port=22, user=self.hostConfig["username"], |
| passwd=self.hostConfig["password"]).execute \ |
| ("reboot") |
| |
| def enableAgent(self): |
| SshClient(self.host.ipaddress, port=22, user=self.hostConfig["username"], passwd=self.hostConfig["password"]).execute\ |
| ("systemctl enable cloudstack-agent || chkconfig cloudstack-agent on") |
| |
| def waitUntilHostInState(self, state="Available", interval=3): |
| def checkForState(expectedState): |
| response = self.getHost(self.host.id) |
| print("checkForState:: expected=%s, actual=%s" % (state, response.hostha)) |
| return response.hostha.hastate == expectedState, None |
| |
| res, _ = wait_until(interval, 200, checkForState, state) |
| if not res: |
| self.fail("Failed to see host ha state in :" + state) |
| |
| def deployVM(self): |
| try: |
| vm = VirtualMachine.create( |
| self.apiclient, |
| services=self.services["virtual_machine"], |
| serviceofferingid=self.service_offering.id, |
| templateid=self.template.id, |
| zoneid=self.zone.id, |
| hostid = self.host.id, |
| method="POST" |
| ) |
| self.cleanup.append(vm) |
| except Exception as e: |
| raise self.skipTest("Failed to deploy VM, skipping kvm host-ha test case") |
| |
| def skipIfMSIsUnsupported(self) : |
| os_details = SshClient(self.mgtSvrDetails["mgtSvrIp"], 22, self.mgtSvrDetails["user"], self.mgtSvrDetails["passwd"]).execute \ |
| ("/usr/share/cloudstack-common/scripts/vm/hypervisor/versions.sh | cut -d '=' -f2") |
| os = os_details[0].lower() |
| if 'ubuntu' in os or 'debian' in os : |
| return |
| # RHEL < 8 works fine |
| os_ver = os_details[1].split('.')[0] |
| if float(os_ver) < 8: |
| return |
| self.skipTest("Skipping since RHEL8 / SUSE have known IPMI issues") |
| |
| @attr(tags=["devcloud", "advanced", "advancedns", "smoke", "basic", "sg"], required_hardware="true") |
| def test_disable_oobm_ha_state_ineligible(self): |
| """ |
| Tests that when HA is enabled for a host, if oobm is disabled HA State should turn into Ineligible |
| """ |
| self.logger.debug("Starting test_disable_oobm_ha_state_ineligible") |
| |
| # Enable ha for host |
| self.configureAndEnableHostHa() |
| |
| # Disable OOBM |
| self.apiclient.configureOutOfBandManagement(self.getOobmConfigCmd()) |
| oobm_cmd = self.getOobmDisableCmd() |
| oobm_cmd.hostid = self.host.id |
| response = self.apiclient.disableOutOfBandManagementForHost(oobm_cmd) |
| self.assertEqual(response.hostid, oobm_cmd.hostid) |
| self.assertEqual(response.enabled, False) |
| |
| response = self.getHost(hostId=self.host.id).outofbandmanagement |
| self.assertEqual(response.powerstate, 'Disabled') |
| |
| # Verify HA State is Ineligeble |
| self.waitUntilHostInState("Ineligible") |
| |
| @attr(tags=["devcloud", "advanced", "advancedns", "smoke", "basic", "sg"], required_hardware="true") |
| def test_hostha_configure_default_driver(self): |
| """ |
| Tests host-ha configuration with valid data |
| """ |
| self.logger.debug("Starting test_hostha_configure_default_driver") |
| |
| cmd = self.getHostHaConfigCmd() |
| response = self.apiclient.configureHAForHost(cmd) |
| self.assertEqual(response.hostid, cmd.hostid) |
| self.assertEqual(response.haprovider, cmd.provider.lower()) |
| |
| @attr(tags=["devcloud", "advanced", "advancedns", "smoke", "basic", "sg"], required_hardware="true") |
| def test_hostha_enable_ha_when_host_disabled(self): |
| """ |
| Tests Enable HA when host is disconnected, should be Ineligible |
| """ |
| self.logger.debug("Starting test_hostha_enable_ha_when_host_disabled") |
| |
| # Enable HA |
| self.configureAndEnableHostHa() |
| |
| # Disable Host |
| self.disableHost(self.host.id) |
| |
| # Check HA State |
| try: |
| self.waitUntilHostInState("Ineligible") |
| except Exception as e: |
| self.enableHost(self.host.id) |
| self.fail(e) |
| |
| # Enable Host |
| self.enableHost(self.host.id) |
| |
| @attr(tags=["devcloud", "advanced", "advancedns", "smoke", "basic", "sg"], required_hardware="true") |
| def test_hostha_enable_ha_when_host_in_maintenance(self): |
| """ |
| Tests Enable HA when host is in Maintenance mode, should be Ineligible |
| """ |
| self.logger.debug("Starting test_hostha_enable_ha_when_host_in_maintenance") |
| self.logger.debug("Pausing to wait for VMs to have finished starting") |
| time.sleep(300) |
| |
| # Enable HA |
| self.configureAndEnableHostHa() |
| |
| |
| # Prepare for maintenance Host |
| self.setHostToMaintanance(self.host.id) |
| |
| # Check HA State |
| try: |
| self.waitUntilHostInState("Ineligible") |
| except Exception as e: |
| self.cancelMaintenance() |
| self.fail(e) |
| |
| # Enable Host |
| self.cancelMaintenance() |
| |
| @attr(tags=["devcloud", "advanced", "advancedns", "smoke", "basic", "sg"], required_hardware="true") |
| def test_hostha_enable_ha_when_host_disconected(self): |
| """ |
| Tests Enable HA when host is disconnected, should be Ineligible |
| """ |
| self.logger.debug("Starting test_hostha_enable_ha_when_host_disconected") |
| |
| # Enable HA |
| self.apiclient.configureHAForHost(self.getHostHaConfigCmd()) |
| cmd = self.getHostHaEnableCmd() |
| cmd.hostid = self.host.id |
| enable = self.apiclient.enableHAForHost(cmd) |
| self.assertEqual(enable.hostid, cmd.hostid) |
| self.assertEqual(enable.haenable, True) |
| |
| # Make Host Disconnected |
| self.killAgent() |
| |
| # Check HA State |
| try: |
| self.waitUntilHostInState("Ineligible") |
| except Exception as e: |
| self.startAgent() |
| self.fail(e) |
| |
| # Enable Host |
| self.startAgent() |
| |
| @attr(tags=["devcloud", "advanced", "advancedns", "smoke", "basic", "sg"], required_hardware="true") |
| def test_remove_ha_provider_not_possible(self): |
| """ |
| Tests HA Provider should be possible to be removed when HA is enabled |
| """ |
| self.logger.debug("Starting test_remove_ha_provider_not_possible") |
| |
| |
| # Enable HA |
| self.apiclient.configureHAForHost(self.getHostHaConfigCmd()) |
| cmd = self.getHostHaEnableCmd() |
| cmd.hostid = self.host.id |
| enable = self.apiclient.enableHAForHost(cmd) |
| self.assertEqual(enable.hostid, cmd.hostid) |
| self.assertEqual(enable.haenable, True) |
| |
| try: |
| self.apiclient.configureHAForHost(self.getHostHaConfigCmd('')) |
| except Exception: |
| pass |
| else: |
| self.fail("Expected an exception to be thrown, failing") |
| |
| @attr(tags = ["devcloud", "advanced", "advancedns", "smoke", "basic", "sg"], required_hardware="true") |
| def test_hostha_kvm_host_degraded(self): |
| """ |
| Tests degraded HA state when agent is stopped/killed |
| """ |
| |
| self.skipIfMSIsUnsupported() |
| self.configureAndStartIpmiServer() |
| self.assertIssueCommandState('ON', 'On') |
| self.configureAndEnableHostHa() |
| |
| self.deployVM() |
| |
| # Start with the available state |
| self.waitUntilHostInState("Available") |
| |
| # SSH into the KVM Host and executes kill -9 of the agent |
| self.stopAgent() |
| |
| # Check if host would go into Suspect state |
| try: |
| self.waitUntilHostInState("Suspect") |
| except Exception as e: |
| self.startAgent() |
| raise Exception("Warning: Exception during test execution : %s" % e) |
| |
| # Checks if the host would turn into Degraded |
| try: |
| self.waitUntilHostInState("Degraded") |
| except Exception as e: |
| self.startAgent() |
| raise Exception("Warning: Exception during test execution : %s" % e) |
| |
| self.startAgent() |
| self.waitUntilHostInState("Available") |
| |
| |
| @attr(tags=["devcloud", "advanced", "advancedns", "smoke", "basic", "sg"], required_hardware="true") |
| def test_hostha_kvm_host_recovering(self): |
| """ |
| Tests recovery and fencing HA state transitions |
| """ |
| |
| self.skipIfMSIsUnsupported() |
| self.configureAndStartIpmiServer() |
| self.assertIssueCommandState('ON', 'On') |
| self.configureAndEnableHostHa() |
| |
| self.deployVM() |
| |
| # Start with the available state |
| self.waitUntilHostInState("Available") |
| |
| # Kill host by triggering a fault |
| self.killAgent() |
| self.disableAgent() |
| self.resetHost() |
| |
| # Check if host would go into Suspect state |
| try: |
| self.waitUntilHostInState("Suspect") |
| except Exception as e: |
| self.startAgent() |
| raise Exception("Warning: Exception during test execution : %s" % e) |
| |
| # Checks if the host would turn into Recovered |
| try: |
| self.waitUntilHostInState("Recovered") |
| except Exception as e: |
| self.startAgent() |
| raise Exception("Warning: Exception during test execution : %s" % e) |
| |
| self.enableAgent() |
| self.startAgent() |
| self.waitUntilHostInState("Available") |
| |
| @attr(tags=["devcloud", "advanced", "advancedns", "smoke", "basic", "sg"], required_hardware="true") |
| def test_hostha_kvm_host_fencing(self): |
| """ |
| Tests fencing/fenced HA state when host crashes |
| """ |
| self.logger.debug("Starting test_ha_kvm_host_fencing") |
| |
| self.skipIfMSIsUnsupported() |
| self.configureAndStartIpmiServer() |
| self.assertIssueCommandState('ON', 'On') |
| self.configureAndEnableHostHa() |
| |
| self.deployVM() |
| |
| # Start with the available state |
| self.waitUntilHostInState("Available") |
| |
| # Fail oobm commands |
| cmd = self.getOobmConfigCmd() |
| cmd.address = "1.1.1.1" |
| self.apiclient.configureOutOfBandManagement(cmd) |
| |
| # Kill host by triggering a fault |
| self.killAgent() |
| self.disableAgent() |
| self.resetHost() |
| |
| # Check if host would go into Suspect state |
| try: |
| self.waitUntilHostInState("Suspect") |
| except Exception as e: |
| self.startAgent() |
| raise Exception("Warning: Exception during test execution : %s" % e) |
| |
| # Checks if the host would turn into Fencing |
| try: |
| self.waitUntilHostInState("Fencing") |
| except Exception as e: |
| self.startAgent() |
| raise Exception("Warning: Exception during test execution : %s" % e) |
| |
| # Allow oobm commands to work now |
| self.configureAndEnableOobm() |
| |
| # Checks if the host would turn into Fenced |
| try: |
| self.waitUntilHostInState("Fenced") |
| except Exception as e: |
| self.startAgent() |
| raise Exception("Warning: Exception during test execution : %s" % e) |
| |
| self.enableAgent() |
| self.startAgent() |
| self.cancelMaintenance() |
| self.waitUntilHostInState("Available") |
| |
| def configureAndStartIpmiServer(self, power_state=None): |
| """ |
| Setup ipmisim and enable out-of-band management for host |
| """ |
| self.configureAndEnableOobm() |
| self.startIpmiServer() |
| if power_state: |
| bmc = IpmiServerContext().bmc |
| bmc.powerstate = power_state |
| |
| def assertIssueCommandState(self, command, expected): |
| """ |
| Asserts power action result for a given power command |
| """ |
| if command != 'STATUS': |
| self.issuePowerActionCmd(command) |
| try: |
| response = self.issuePowerActionCmd('STATUS') |
| self.assertEqual(response.powerstate, expected) |
| except: |
| pass # in case of ipmisim errors ignore |
| |
| def configureAndEnableOobm(self): |
| self.apiclient.configureOutOfBandManagement(self.getOobmConfigCmd()) |
| response = self.apiclient.enableOutOfBandManagementForHost(self.getOobmEnableCmd()) |
| self.assertEqual(response.enabled, True) |
| |
| def startIpmiServer(self): |
| def startIpmiServer(tname, server): |
| self.debug("Starting ipmisim server") |
| try: |
| server.serve_forever() |
| except Exception: pass |
| IpmiServerContext('reset') |
| ThreadedIpmiServer.allow_reuse_address = False |
| server = ThreadedIpmiServer(('0.0.0.0', self.getIpmiServerPort()), IpmiServer) |
| _thread.start_new_thread(startIpmiServer, ("ipmi-server", server,)) |
| self.server = server |
| |
| def stopIpmiServer(self): |
| if self.server: |
| self.server.shutdown() |
| self.server.server_close() |
| |
| def getOobmIssueActionCmd(self): |
| cmd = issueOutOfBandManagementPowerAction.issueOutOfBandManagementPowerActionCmd() |
| cmd.hostid = self.host.id |
| cmd.action = 'STATUS' |
| return cmd |
| |
| def issuePowerActionCmd(self, action, timeout=None): |
| cmd = self.getOobmIssueActionCmd() |
| cmd.action = action |
| if timeout: |
| cmd.timeout = timeout |
| |
| try: |
| return self.apiclient.issueOutOfBandManagementPowerAction(cmd) |
| except Exception as e: |
| if "packet session id 0x0 does not match active session" in str(e): |
| raise self.skipTest("Known ipmitool issue hit, skipping test") |
| raise e |
| |
| def getOobmEnableCmd(self): |
| cmd = enableOutOfBandManagementForHost.enableOutOfBandManagementForHostCmd() |
| cmd.hostid = self.host.id |
| return cmd |
| |
| def getOobmDisableCmd(self): |
| cmd = disableOutOfBandManagementForHost.disableOutOfBandManagementForHostCmd() |
| cmd.hostid = self.host.id |
| return cmd |
| |
| def getIpmiServerPort(self): |
| return self.serverPort |
| |
| def getOobmConfigCmd(self): |
| cmd = configureOutOfBandManagement.configureOutOfBandManagementCmd() |
| cmd.driver = 'ipmitool' # The default available driver |
| cmd.address = self.getIpmiServerIp() |
| cmd.port = self.getIpmiServerPort() |
| cmd.username = 'admin' |
| cmd.password = 'password' |
| cmd.hostid = self.host.id |
| return cmd |
| |
| def getIpmiServerIp(self): |
| s = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) |
| s.connect((self.mgtSvrDetails["mgtSvrIp"], self.mgtSvrDetails["port"])) |
| return s.getsockname()[0] |
| |
| def get_non_configured_ha_host(self): |
| |
| response = list_hosts( |
| self.apiclient, |
| type='Routing' |
| ) |
| |
| for host in response: |
| if host.haprovider is None: |
| return host |
| else: |
| cloudstackTestCase.skipTest(self, "There is no non configured hosts. Skipping test.") |
| |
| raise self.skipTest("No KVM hosts found, skipping host-ha test") |
| |
| def getHAState(self, id): |
| cmd = listHostHAResources.listHostHAResourcesCmd() |
| cmd.hostid = id |
| response = self.apiclient.listHostHAResources(cmd) |
| |
| return response[0] |
| |
| def startAgent(self): |
| SshClient(host=self.host.ipaddress, port=22, user=self.hostConfig["username"], |
| passwd=self.hostConfig["password"]).execute \ |
| ("systemctl start cloudstack-agent || service cloudstack-agent start") |
| |
| def stopAgent(self): |
| SshClient(host=self.host.ipaddress, port=22, user=self.hostConfig["username"], |
| passwd=self.hostConfig["password"]).execute \ |
| ("systemctl stop cloudstack-agent || service cloudstack-agent stop") |
| |
| def killAgent(self): |
| SshClient(host=self.host.ipaddress, port=22, user=self.hostConfig["username"], passwd=self.hostConfig["password"]).execute\ |
| ("kill -9 $(ps aux | grep 'cloudstack-agent' | awk '{print $2}')") |
| |
| def disableHost(self, id): |
| cmd = updateHost.updateHostCmd() |
| cmd.id = id |
| cmd.allocationstate = "Disable" |
| response = self.apiclient.updateHost(cmd) |
| self.assertEqual(response.resourcestate, "Disabled") |
| |
| def enableHost(self, id): |
| cmd = updateHost.updateHostCmd() |
| cmd.id = id |
| cmd.allocationstate = "Enable" |
| response = self.apiclient.updateHost(cmd) |
| self.assertEqual(response.resourcestate, "Enabled") |
| |
| def setHostToMaintanance(self, id): |
| cmd = prepareHostForMaintenance.prepareHostForMaintenanceCmd() |
| cmd.id = id |
| |
| response = self.apiclient.prepareHostForMaintenance(cmd) |
| |
| self.assertEqual(response.resourcestate, "PrepareForMaintenance") |
| |
| def cancelMaintenance(self): |
| cmd = cancelHostMaintenance.cancelHostMaintenanceCmd() |
| cmd.id = self.host.id |
| response = self.apiclient.cancelHostMaintenance(cmd) |
| |
| self.assertEqual(response.resourcestate, "Enabled") |