Skip to content

Instantly share code, notes, and snippets.

@rodrigogansobarbieri
Created February 3, 2021 17:50
Show Gist options
  • Save rodrigogansobarbieri/9e40d5746cd18e4c06a3c1cc7aece287 to your computer and use it in GitHub Desktop.
Save rodrigogansobarbieri/9e40d5746cd18e4c06a3c1cc7aece287 to your computer and use it in GitHub Desktop.
#!/usr/bin/env python3
"""Unit tests for EvacuationAssistant."""
import ddt
import copy
import json
import mock
from evacuation_assistant import evacuation_assistant
import random
import unittest
GROUP_LIST = """
[
{
"ID": "group1",
"Policies": "affinity",
"Members": "Aff1(1), Aff2(1), Aff3(1)"
},
{
"ID": "group2",
"Policies": "affinity",
"Members": "Aff2(1), Aff4(1), Aff5(1)"
},
{
"ID": "group3",
"Policies": "affinity",
"Members": "Aff6(1), Aff7(1)"
},
{
"ID": "group4",
"Policies": "affinity",
"Members": "Aff7(1), Aff8(3)"
},
{
"ID": "group5",
"Policies": "anti-affinity",
"Members": "antiaff1(1), antiaff2(2), antiaff3(3)"
},
{
"ID": "group6",
"Policies": "anti-affinity",
"Members": "antiaff2(2), antiaff4(1)"
},
{
"ID": "group7",
"Policies": "anti-affinity",
"Members": "antiaff2(2), antiaff6(1)"
},
{
"ID": "group8",
"Policies": "anti-affinity",
"Members": "antiaff6(1), antiaff7(3), antiaff8(4)"
},
{
"ID": "group9",
"Policies": "anti-affinity",
"Members": "antiaff1(1), antiaff3(3), antiaff9(1)"
}
]
"""
GROUP_LIST_ERROR = """
[
{
"ID": "group1",
"Policies": "affinity",
"Members": "Aff1(1), Aff2(2), Aff3(3)"
}
]
"""
SERVER_LIST_ERROR = """
[
{
"ID": "Aff1(1)",
"Status": "ACTIVE",
"Power State": "Running",
"Image ID": "N/A (booted from volume)",
"Flavor ID": "1",
"Host": "hyp1"
},
{
"ID": "Aff2(2)",
"Status": "ACTIVE",
"Power State": "Running",
"Image ID": "image1",
"Flavor ID": "1",
"Host": "hyp2"
},
{
"ID": "Aff3(3)",
"Status": "ACTIVE",
"Power State": "Running",
"Image ID": "image1",
"Flavor ID": "1",
"Host": "hyp3"
}
]
"""
SERVER_LIST = """
[
{
"ID": "Aff1(1)",
"Status": "ACTIVE",
"Power State": "Running",
"Image ID": "N/A (booted from volume)",
"Flavor ID": "1",
"Host": "hyp1"
},
{
"ID": "Aff2(1)",
"Status": "ACTIVE",
"Power State": "Running",
"Image ID": "image1",
"Flavor ID": "1",
"Host": "hyp1"
},
{
"ID": "Aff3(1)",
"Status": "ACTIVE",
"Power State": "Running",
"Image ID": "image1",
"Flavor ID": "1",
"Host": "hyp1"
},
{
"ID": "Aff4(1)",
"Status": "ACTIVE",
"Power State": "Running",
"Image ID": "N/A (booted from volume)",
"Flavor ID": "2",
"Host": "hyp1"
},
{
"ID": "Aff5(1)",
"Status": "ACTIVE",
"Power State": "Running",
"Image ID": "image1",
"Flavor ID": "2",
"Host": "hyp1"
},
{
"ID": "Aff6(1)",
"Status": "ACTIVE",
"Power State": "Running",
"Image ID": "image1",
"Flavor ID": "2",
"Host": "hyp1"
},
{
"ID": "Aff7(1)",
"Status": "ACTIVE",
"Power State": "Running",
"Image ID": "image1",
"Flavor ID": "2",
"Host": "hyp1"
},
{
"ID": "Aff8(3)",
"Status": "ACTIVE",
"Power State": "Running",
"Image ID": "image1",
"Flavor ID": "2",
"Host": "hyp3"
},
{
"ID": "antiaff1(1)",
"Status": "ACTIVE",
"Power State": "Running",
"Image ID": "N/A (booted from volume)",
"Flavor ID": "1",
"Host": "hyp1"
},
{
"ID": "antiaff2(2)",
"Status": "ACTIVE",
"Power State": "Running",
"Image ID": "image1",
"Flavor ID": "1",
"Host": "hyp2"
},
{
"ID": "antiaff3(3)",
"Status": "ACTIVE",
"Power State": "Running",
"Image ID": "image1",
"Flavor ID": "1",
"Host": "hyp3"
},
{
"ID": "antiaff4(1)",
"Status": "ACTIVE",
"Power State": "Running",
"Image ID": "N/A (booted from volume)",
"Flavor ID": "2",
"Host": "hyp1"
},
{
"ID": "antiaff5(3)",
"Status": "ACTIVE",
"Power State": "Running",
"Image ID": "image1",
"Flavor ID": "2",
"Host": "hyp3"
},
{
"ID": "antiaff6(1)",
"Status": "ACTIVE",
"Power State": "Running",
"Image ID": "image1",
"Flavor ID": "2",
"Host": "hyp1"
},
{
"ID": "antiaff7(3)",
"Status": "ACTIVE",
"Power State": "Running",
"Image ID": "image1",
"Flavor ID": "2",
"Host": "hyp3"
},
{
"ID": "antiaff8(4)",
"Status": "ACTIVE",
"Power State": "Running",
"Image ID": "image1",
"Flavor ID": "2",
"Host": "hyp4"
},
{
"ID": "antiaff9(1)",
"Status": "ACTIVE",
"Power State": "Running",
"Image ID": "image1",
"Flavor ID": "2",
"Host": "hyp1"
},
{
"ID": "vm1(4)",
"Status": "ACTIVE",
"Power State": "Running",
"Image ID": "image1",
"Flavor ID": "2",
"Host": "hyp4"
},
{
"ID": "vm2(1)",
"Status": "ACTIVE",
"Power State": "Running",
"Image ID": "N/A (booted from volume)",
"Flavor ID": "2",
"Host": "hyp1"
}
]
"""
INVENTORY_LIST = """
[
{
"resource_class": "VCPU",
"allocation_ratio": 16.0
},
{
"resource_class": "MEMORY_MB",
"allocation_ratio": 1.5
},
{
"resource_class": "DISK_GB",
"allocation_ratio": 1.0
}
]
"""
HYPERVISOR_SHOW = """
{
"disk_available_least": 31
}
"""
HYPERVISOR_LIST = """
[
{
"Hypervisor Hostname": "hyp1",
"State": "up",
"vCPUs Used": 1,
"vCPUs": 2,
"Memory MB Used": 576,
"Memory MB": 3944
},
{
"Hypervisor Hostname": "hyp2",
"State": "up",
"vCPUs Used": 2,
"vCPUs": 4,
"Memory MB Used": 768,
"Memory MB": 3944
},
{
"Hypervisor Hostname": "hyp3",
"State": "up",
"vCPUs Used": 1,
"vCPUs": 2,
"Memory MB Used": 576,
"Memory MB": 3944
},
{
"Hypervisor Hostname": "hyp4",
"State": "up",
"vCPUs Used": 1,
"vCPUs": 2,
"Memory MB Used": 576,
"Memory MB": 3944
}
]
"""
HYPERVISOR_REMAINING = json.loads("""
{
"Hypervisor Hostname": "hyp2",
"State": "up",
"vCPUs Used": 1,
"vCPUs": 2,
"vCPUs Remaining": 31,
"Memory MB Used": 576,
"Memory MB": 3944,
"Memory MB Remaining": 5340,
"Disk Remaining": 31,
"Inventory": [
{
"resource_class": "VCPU",
"allocation_ratio": 16.0
},
{
"resource_class": "MEMORY_MB",
"allocation_ratio": 1.5
},
{
"resource_class": "DISK_GB",
"allocation_ratio": 1.0
}
],
"Details": {
"disk_available_least": 31
}
}
""")
FLAVOR_LIST = """
[
{
"ID": "1",
"RAM": 512,
"Disk": 2,
"Ephemeral": 1,
"VCPUs": 1
},
{
"ID": "2",
"RAM": 2048,
"Disk": 20,
"Ephemeral": 0,
"VCPUs": 2
}
]
"""
PROVIDER_LIST = """
[
{
"uuid": "rp1",
"name": "hyp1"
},
{
"uuid": "rp2",
"name": "hyp2"
},
{
"uuid": "rp3",
"name": "hyp3"
},
{
"uuid": "rp4",
"name": "hyp4"
}
]
"""
PROVIDER_LIST_ERROR = """
[
{
"uuid": "rp2",
"name": "hyp2"
},
{
"uuid": "rp3",
"name": "hyp3"
},
{
"uuid": "rp4",
"name": "hyp4"
}
]
"""
CALCULATED_DESTINATIONS1 = json.loads("""
{
"Aff1(1)": {
"aff_vms": [
"Aff2(1)",
"Aff3(1)"
],
"anti_vms": [],
"candidate_hosts": [
"hyp2",
"hyp3",
"hyp4"
]
},
"Aff2(1)": {
"aff_vms": [
"Aff1(1)",
"Aff3(1)",
"Aff4(1)",
"Aff5(1)"
],
"anti_vms": [],
"candidate_hosts": [
"hyp2",
"hyp3",
"hyp4"
]
},
"Aff3(1)": {
"aff_vms": [
"Aff1(1)",
"Aff2(1)"
],
"anti_vms": [],
"candidate_hosts": [
"hyp2",
"hyp3",
"hyp4"
]
},
"Aff4(1)": {
"aff_vms": [
"Aff2(1)",
"Aff5(1)"
],
"anti_vms": [],
"candidate_hosts": [
"hyp2",
"hyp3",
"hyp4"
]
},
"Aff5(1)": {
"aff_vms": [
"Aff2(1)",
"Aff4(1)"
],
"anti_vms": [],
"candidate_hosts": [
"hyp2",
"hyp3",
"hyp4"
]
},
"Aff6(1)": {
"aff_vms": [
"Aff7(1)"
],
"anti_vms": [],
"candidate_hosts": [
"hyp3"
]
},
"Aff7(1)": {
"aff_vms": [
"Aff6(1)",
"Aff8(3)"
],
"anti_vms": [],
"candidate_hosts": [
"hyp3"
]
},
"antiaff1(1)": {
"aff_vms": [],
"anti_vms": [
"antiaff2(2)",
"antiaff3(3)",
"antiaff9(1)"
],
"candidate_hosts": [
"hyp4"
]
},
"antiaff4(1)": {
"aff_vms": [],
"anti_vms": [
"antiaff2(2)"
],
"candidate_hosts": [
"hyp3",
"hyp4"
]
},
"antiaff6(1)": {
"aff_vms": [],
"anti_vms": [
"antiaff2(2)",
"antiaff7(3)",
"antiaff8(4)"
],
"candidate_hosts": []
},
"antiaff9(1)": {
"aff_vms": [],
"anti_vms": [
"antiaff1(1)",
"antiaff3(3)"
],
"candidate_hosts": [
"hyp2",
"hyp4"
]
},
"vm2(1)": {
"aff_vms": [],
"anti_vms": [],
"candidate_hosts": [
"hyp2",
"hyp3",
"hyp4"
]
}
}
""")
CALCULATED_DESTINATIONS2 = json.loads("""
{
"Aff1(1)": {},
"Aff2(1)": {},
"antiaff1(1)": {}
}
""")
@ddt.ddt
class TestEvacuationAssistant(unittest.TestCase):
def mock_object(self, obj, attr_name, new_attr=None):
"""Helper to avoid using mock decorators."""
if not new_attr:
new_attr = mock.Mock()
patcher = mock.patch.object(obj, attr_name, new_attr)
patcher.start()
self.addCleanup(patcher.stop)
return new_attr
def setUp(self):
""""Set up resources common to each test."""
super(TestEvacuationAssistant, self).setUp()
self.maxDiff = None
self.mock_object(evacuation_assistant, 'LOG')
self.run_cmd = self.mock_object(evacuation_assistant, "run_cmd")
self.args = evacuation_assistant.parse_args(["--host", "hyp1"])
self.mig = evacuation_assistant.EvacuationAssistant(self.args)
self.run_cmd.side_effect = [
PROVIDER_LIST, GROUP_LIST, SERVER_LIST, HYPERVISOR_LIST,
FLAVOR_LIST,
]
def test_gather_info_error_placement1(self):
mig = evacuation_assistant.EvacuationAssistant(self.args)
self.run_cmd.side_effect = evacuation_assistant.CommandFailedException
self.assertRaises(SystemExit, mig.gather_info)
def test_gather_info_error_placement2(self):
mig = evacuation_assistant.EvacuationAssistant(self.args)
self.run_cmd.side_effect = [("openstack: 'resource provider foo "
"--format json' is not an openstack "
"command. See 'openstack --help'.")]
self.assertRaises(SystemExit, mig.gather_info)
def test_gather_info_error_evac_host(self):
mig = evacuation_assistant.EvacuationAssistant(self.args)
self.run_cmd.side_effect = [PROVIDER_LIST_ERROR]
self.assertRaises(SystemExit, mig.gather_info)
def test_calculate_destinations(self):
self.mig.gather_info()
result = self.mig.calculate_destinations()
self.assertEqual(CALCULATED_DESTINATIONS1, result)
def test_calculate_destinations_affinity_destination_conflict(self):
self.run_cmd.side_effect = [
PROVIDER_LIST, GROUP_LIST_ERROR, SERVER_LIST_ERROR,
HYPERVISOR_LIST, FLAVOR_LIST,
]
self.mig.gather_info()
self.assertRaises(evacuation_assistant.AffinityDestinationConflict,
self.mig.calculate_destinations)
def test_select_hosts(self):
self.mig.gather_info()
self.mig.dest_map = copy.deepcopy(CALCULATED_DESTINATIONS1)
self.mig.in_progress = {
"Aff1(1)": "hyp2",
"antiaff1(1)": "hyp4",
}
expected1 = 'hyp2'
expected2 = 'hyp3'
self.mock_object(self.mig, 'filter_hosts', mock.Mock(
return_value=['hyp3', 'hyp4']))
self.assertEqual(expected1, self.mig.select_host("Aff2(1)"))
self.assertEqual(expected2, self.mig.select_host("antiaff4(1)"))
self.mig.filter_hosts.assert_called_once_with(
['hyp3', 'hyp4'], 'antiaff4(1)')
def test_select_host_antiaffinity_violated(self):
self.mig.gather_info()
self.mig.dest_map = copy.deepcopy(CALCULATED_DESTINATIONS1)
self.mig.in_progress = {
"antiaff1(1)": "hyp4",
}
self.mock_object(self.mig, 'filter_hosts', mock.Mock(
return_value=['hyp2']))
self.assertEqual('hyp2', self.mig.select_host("antiaff9(1)"))
self.mig.filter_hosts.assert_called_once_with(
['hyp2'], 'antiaff9(1)')
def test_calculate_remaining_resources(self):
self.run_cmd.side_effect = [
PROVIDER_LIST, GROUP_LIST, SERVER_LIST, HYPERVISOR_LIST,
FLAVOR_LIST, INVENTORY_LIST, HYPERVISOR_SHOW,
]
self.mig.gather_info()
expected = {
"Hypervisor Hostname": "hyp2",
"State": "up",
"vCPUs Used": 2,
"vCPUs": 4,
"vCPUs Remaining": 62,
"Memory MB Used": 768,
"Memory MB": 3944,
"Memory MB Remaining": 5148,
"Disk Remaining": 31,
"Inventory": [
{
"resource_class": "VCPU",
"allocation_ratio": 16.0
},
{
"resource_class": "MEMORY_MB",
"allocation_ratio": 1.5
},
{
"resource_class": "DISK_GB",
"allocation_ratio": 1.0
}
],
"Details": {
"disk_available_least": 31
},
}
self.mig.calculate_initial_remaining_resources("hyp2")
self.assertEqual(expected, self.mig.hosts_indexed['hyp2'])
def test_adjust_remaining_resources(self):
self.mig.gather_info()
self.mig.hosts_indexed['hyp2'] = copy.deepcopy(HYPERVISOR_REMAINING)
expected = copy.deepcopy(HYPERVISOR_REMAINING)
self.mig.adjust_remaining_resources("antiaff5(3)", 'hyp2')
expected.update({
"Memory MB Remaining": 3292,
"vCPUs Remaining": 29,
"Disk Remaining": 11,
})
self.assertEqual(expected, self.mig.hosts_indexed['hyp2'])
self.mig.adjust_remaining_resources("antiaff1(1)", 'hyp2')
expected.update({
"Memory MB Remaining": 2780,
"vCPUs Remaining": 28,
})
self.assertEqual(expected, self.mig.hosts_indexed['hyp2'])
def test_filter_hosts(self):
self.mig.gather_info()
self.mig.hosts_indexed['hyp3'] = copy.deepcopy(HYPERVISOR_REMAINING)
hyp = copy.deepcopy(HYPERVISOR_REMAINING)
hyp.update({
'Memory MB remaining': 1536,
'Disk Remaining': 10,
})
self.mig.hosts_indexed['hyp2'] = hyp
self.assertEqual(
['hyp3'], self.mig.filter_hosts(['hyp2', 'hyp3'], "antiaff5(3)"))
self.assertEqual(
['hyp2', 'hyp3'],
self.mig.filter_hosts(['hyp2', 'hyp3'], "antiaff1(1)"))
def test_is_volume_backed(self):
self.mig.gather_info()
self.assertEqual(False, self.mig.is_volume_backed("antiaff5(3)"))
self.assertEqual(True, self.mig.is_volume_backed("antiaff1(1)"))
def test_build_cmd(self):
self.mig.gather_info()
self.mig.dest_map = copy.deepcopy(CALCULATED_DESTINATIONS1)
expected1 = "openstack server migrate Aff4(1) --live hyp3"
expected2 = ("openstack server migrate antiaff6(1) --block-migration "
"--live-migration --host hyp4 "
"--os-compute-api-version 2.30")
self.assertEqual(expected1, self.mig.build_cmd('hyp3', 'Aff4(1)'))
self.assertEqual(expected2, self.mig.build_cmd('hyp4', 'antiaff6(1)'))
def test_run(self):
self.mock_object(self.mig, 'check_dependencies')
self.mock_object(self.mig, 'evacuate')
self.mock_object(self.mig, 'gather_info')
self.mock_object(self.mig, 'calculate_destinations')
self.mig.run()
self.mig.check_dependencies.assert_called_once_with()
self.mig.evacuate.assert_called_once_with()
self.mig.gather_info.assert_called_once_with()
self.mig.calculate_destinations.assert_called_once_with()
def test_evacuate(self):
self.mig.gather_info()
self.mig.dest_map = copy.deepcopy(CALCULATED_DESTINATIONS2)
def _build_cmd_side_effect(host, vm_id):
return "cmd_{}_{}".format(vm_id, host)
self.mock_object(self.mig, 'calculate_initial_remaining_resources')
self.mock_object(self.mig, 'select_host', mock.Mock(
side_effect=[None, 'hyp1', 'hyp3']))
self.mock_object(self.mig, 'build_cmd', mock.Mock(
side_effect=_build_cmd_side_effect))
self.mock_object(self.mig, 'adjust_remaining_resources')
self.mig.evacuate()
self.mig.calculate_initial_remaining_resources.assert_has_calls([
mock.call(host) for host in self.mig.hosts_indexed.keys()
])
self.mig.select_host.assert_has_calls([
mock.call(vm_id) for vm_id in self.mig.dest_map.keys()
])
self.mig.build_cmd.assert_has_calls([
mock.call('hyp1', 'Aff2(1)'),
mock.call('hyp3', 'antiaff1(1)'),
])
self.mig.adjust_remaining_resources.assert_has_calls([
mock.call('Aff2(1)', 'hyp1'),
mock.call('antiaff1(1)', 'hyp3'),
])
expected_inprogress = {
'Aff2(1)': 'hyp1',
'antiaff1(1)': 'hyp3',
}
self.assertEqual(expected_inprogress, self.mig.in_progress)
evacuation_assistant.LOG.error.assert_called_once_with(
"Failed to find a destination host for the following VMs: "
"\nAff1(1)"
)
@mock.patch.dict(evacuation_assistant.os.environ,
{'OS_AUTH_URL': 'http://keystone', 'foo': 'bar'})
def test_check_dependencies(self):
self.mig.check_dependencies()
@mock.patch.dict(evacuation_assistant.os.environ, {'foo': 'bar'})
def test_check_dependencies_exit(self):
self.assertRaises(SystemExit, self.mig.check_dependencies)
class Pipes(object):
"""Fake class for testing subprocess.Popen"""
def __init__(self, return_code):
self.stdout = b'stdout test'
self.stderr = b'stderr test'
self.returncode = return_code
def communicate(self):
"""Returns test data."""
return self.stdout, self.stderr
@ddt.ddt
class TestModule(unittest.TestCase):
@ddt.data(0, 1)
@mock.patch.object(evacuation_assistant.subprocess, 'Popen')
def test_run_cmd(self, return_code, popen):
popen.return_value = Pipes(return_code)
if return_code == 1:
self.assertRaises(
evacuation_assistant.CommandFailedException,
evacuation_assistant.run_cmd, "foo bar test")
else:
evacuation_assistant.run_cmd("foo bar test")
popen.assert_called_once_with(['foo', 'bar', 'test'],
stdout=-1, stderr=-1)
if __name__ == '__main__':
unittest.main()
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment