metalsmith/metalsmith/test/test_provisioner.py

457 lines
21 KiB
Python

# Copyright 2018 Red Hat, Inc.
#
# Licensed 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 mock
import testtools
from metalsmith import _exceptions
from metalsmith import _os_api
from metalsmith import _provisioner
class Base(testtools.TestCase):
def setUp(self):
super(Base, self).setUp()
self.pr = _provisioner.Provisioner(mock.Mock())
self._reset_api_mock()
self.node = mock.Mock(spec=['name', 'uuid', 'properties', 'extra'],
uuid='000', properties={'local_gb': 100},
extra={})
self.node.name = 'control-0'
def _reset_api_mock(self):
self.api = mock.Mock(spec=_os_api.API)
self.api.get_node.side_effect = lambda n: n
self.api.update_node.side_effect = lambda n, _u: n
self.pr._api = self.api
class TestReserveNode(Base):
def test_no_nodes(self):
self.api.list_nodes.return_value = []
self.assertRaises(_exceptions.ResourceClassNotFound,
self.pr.reserve_node, 'control')
self.assertFalse(self.api.reserve_node.called)
def test_simple_ok(self):
nodes = [
mock.Mock(spec=['uuid', 'name', 'properties'],
properties={'local_gb': 100})
]
self.api.list_nodes.return_value = nodes
self.api.reserve_node.side_effect = lambda n, instance_uuid: n
node = self.pr.reserve_node('control')
self.assertIn(node, nodes)
def test_with_capabilities(self):
nodes = [
mock.Mock(spec=['uuid', 'name', 'properties'],
properties={'local_gb': 100, 'capabilities': caps})
for caps in ['answer:1', 'answer:42', None]
]
expected = nodes[1]
self.api.list_nodes.return_value = nodes
self.api.reserve_node.side_effect = lambda n, instance_uuid: n
node = self.pr.reserve_node('control', {'answer': '42'})
self.assertIs(node, expected)
CLEAN_UP = {
'/extra/metalsmith_created_ports': _os_api.REMOVE,
'/extra/metalsmith_attached_ports': _os_api.REMOVE
}
class TestProvisionNode(Base):
def setUp(self):
super(TestProvisionNode, self).setUp()
image = self.api.get_image_info.return_value
self.updates = {
'/instance_info/ramdisk': image.ramdisk_id,
'/instance_info/kernel': image.kernel_id,
'/instance_info/image_source': image.id,
'/instance_info/root_gb': 99, # 100 - 1
'/instance_info/capabilities': {'boot_option': 'local'},
'/extra/metalsmith_created_ports': [
self.api.create_port.return_value.id
],
'/extra/metalsmith_attached_ports': [
self.api.create_port.return_value.id
]
}
def test_ok(self):
self.pr.provision_node(self.node, 'image', [{'network': 'network'}])
self.api.create_port.assert_called_once_with(
network_id=self.api.get_network.return_value.id)
self.api.attach_port_to_node.assert_called_once_with(
self.node.uuid, self.api.create_port.return_value.id)
self.api.update_node.assert_called_once_with(self.node, self.updates)
self.api.validate_node.assert_called_once_with(self.node,
validate_deploy=True)
self.api.node_action.assert_called_once_with(self.node, 'active',
configdrive=mock.ANY)
self.assertFalse(self.api.wait_for_node_state.called)
self.assertFalse(self.api.release_node.called)
self.assertFalse(self.api.delete_port.called)
def test_with_ports(self):
self.updates['/extra/metalsmith_created_ports'] = []
self.updates['/extra/metalsmith_attached_ports'] = [
self.api.get_port.return_value.id
] * 2
self.pr.provision_node(self.node, 'image',
[{'port': 'port1'}, {'port': 'port2'}])
self.assertFalse(self.api.create_port.called)
self.api.attach_port_to_node.assert_called_with(
self.node.uuid, self.api.get_port.return_value.id)
self.assertEqual(2, self.api.attach_port_to_node.call_count)
self.assertEqual([mock.call('port1'), mock.call('port2')],
self.api.get_port.call_args_list)
self.api.update_node.assert_called_once_with(self.node, self.updates)
self.api.validate_node.assert_called_once_with(self.node,
validate_deploy=True)
self.api.node_action.assert_called_once_with(self.node, 'active',
configdrive=mock.ANY)
self.assertFalse(self.api.wait_for_node_state.called)
self.assertFalse(self.api.release_node.called)
self.assertFalse(self.api.delete_port.called)
def test_whole_disk(self):
image = self.api.get_image_info.return_value
image.kernel_id = None
image.ramdisk_id = None
del self.updates['/instance_info/kernel']
del self.updates['/instance_info/ramdisk']
self.pr.provision_node(self.node, 'image', [{'network': 'network'}])
self.api.create_port.assert_called_once_with(
network_id=self.api.get_network.return_value.id)
self.api.attach_port_to_node.assert_called_once_with(
self.node.uuid, self.api.create_port.return_value.id)
self.api.update_node.assert_called_once_with(self.node, self.updates)
self.api.validate_node.assert_called_once_with(self.node,
validate_deploy=True)
self.api.node_action.assert_called_once_with(self.node, 'active',
configdrive=mock.ANY)
self.assertFalse(self.api.wait_for_node_state.called)
self.assertFalse(self.api.release_node.called)
self.assertFalse(self.api.delete_port.called)
def test_with_root_disk_size(self):
self.updates['/instance_info/root_gb'] = 50
self.pr.provision_node(self.node, 'image', [{'network': 'network'}],
root_disk_size=50)
self.api.create_port.assert_called_once_with(
network_id=self.api.get_network.return_value.id)
self.api.attach_port_to_node.assert_called_once_with(
self.node.uuid, self.api.create_port.return_value.id)
self.api.update_node.assert_called_once_with(self.node, self.updates)
self.api.validate_node.assert_called_once_with(self.node,
validate_deploy=True)
self.api.node_action.assert_called_once_with(self.node, 'active',
configdrive=mock.ANY)
self.assertFalse(self.api.wait_for_node_state.called)
self.assertFalse(self.api.release_node.called)
self.assertFalse(self.api.delete_port.called)
def test_with_wait(self):
self.api.get_port.return_value = mock.Mock(
spec=['fixed_ips'],
fixed_ips=[{'ip_address': '192.168.1.5'}, {}]
)
self.pr.provision_node(self.node, 'image', [{'network': 'network'}],
wait=3600)
self.api.create_port.assert_called_once_with(
network_id=self.api.get_network.return_value.id)
self.api.attach_port_to_node.assert_called_once_with(
self.node.uuid, self.api.create_port.return_value.id)
self.api.update_node.assert_called_once_with(self.node, self.updates)
self.api.validate_node.assert_called_once_with(self.node,
validate_deploy=True)
self.api.node_action.assert_called_once_with(self.node, 'active',
configdrive=mock.ANY)
self.api.wait_for_node_state.assert_called_once_with(self.node,
'active',
timeout=3600)
self.api.get_port.assert_called_once_with(
self.api.create_port.return_value.id)
self.assertFalse(self.api.release_node.called)
self.assertFalse(self.api.delete_port.called)
@mock.patch.object(_provisioner.LOG, 'warning', autospec=True)
def test_with_wait_no_ips(self, mock_warn):
self.api.get_port.return_value = mock.Mock(
spec=['fixed_ips'], fixed_ips=[]
)
self.pr.provision_node(self.node, 'image', [{'network': 'network'}],
wait=3600)
self.api.node_action.assert_called_once_with(self.node, 'active',
configdrive=mock.ANY)
self.api.wait_for_node_state.assert_called_once_with(self.node,
'active',
timeout=3600)
mock_warn.assert_called_once_with('No IPs for node %s', mock.ANY)
def test_dry_run(self):
self.pr._dry_run = True
self.pr.provision_node(self.node, 'image', [{'network': 'network'}])
self.assertFalse(self.api.create_port.called)
self.assertFalse(self.api.attach_port_to_node.called)
self.assertFalse(self.api.update_node.called)
self.assertFalse(self.api.node_action.called)
self.assertFalse(self.api.wait_for_node_state.called)
self.assertFalse(self.api.release_node.called)
self.assertFalse(self.api.delete_port.called)
def test_deploy_failure(self):
self.api.node_action.side_effect = RuntimeError('boom')
self.assertRaisesRegex(RuntimeError, 'boom',
self.pr.provision_node, self.node,
'image', [{'network': 'n1'}, {'port': 'p1'}],
wait=3600)
self.api.update_node.assert_any_call(self.node, CLEAN_UP)
self.assertFalse(self.api.wait_for_node_state.called)
self.api.release_node.assert_called_once_with(self.node)
self.api.delete_port.assert_called_once_with(
self.api.create_port.return_value.id)
calls = [
mock.call(self.node, self.api.create_port.return_value.id),
mock.call(self.node, self.api.get_port.return_value.id)
]
self.api.detach_port_from_node.assert_has_calls(calls, any_order=True)
def test_port_creation_failure(self):
self.api.create_port.side_effect = RuntimeError('boom')
self.assertRaisesRegex(RuntimeError, 'boom',
self.pr.provision_node, self.node,
'image', [{'network': 'network'}], wait=3600)
self.api.update_node.assert_called_once_with(self.node, CLEAN_UP)
self.assertFalse(self.api.node_action.called)
self.api.release_node.assert_called_once_with(self.node)
self.assertFalse(self.api.delete_port.called)
self.assertFalse(self.api.detach_port_from_node.called)
def test_port_attach_failure(self):
self.api.attach_port_to_node.side_effect = RuntimeError('boom')
self.assertRaisesRegex(RuntimeError, 'boom',
self.pr.provision_node, self.node,
'image', [{'network': 'network'}], wait=3600)
self.api.update_node.assert_called_once_with(self.node, CLEAN_UP)
self.assertFalse(self.api.node_action.called)
self.api.release_node.assert_called_once_with(self.node)
self.api.delete_port.assert_called_once_with(
self.api.create_port.return_value.id)
self.api.detach_port_from_node.assert_called_once_with(
self.node, self.api.create_port.return_value.id)
@mock.patch.object(_provisioner.LOG, 'exception', autospec=True)
def test_failure_during_deploy_failure(self, mock_log_exc):
for failed_call in ['detach_port_from_node',
'delete_port', 'release_node']:
self._reset_api_mock()
getattr(self.api, failed_call).side_effect = AssertionError()
self.api.node_action.side_effect = RuntimeError('boom')
self.assertRaisesRegex(RuntimeError, 'boom',
self.pr.provision_node, self.node,
'image', [{'network': 'network'}],
wait=3600)
self.assertFalse(self.api.wait_for_node_state.called)
self.api.release_node.assert_called_once_with(self.node)
self.api.delete_port.assert_called_once_with(
self.api.create_port.return_value.id)
self.api.detach_port_from_node.assert_called_once_with(
self.node, self.api.create_port.return_value.id)
self.assertEqual(mock_log_exc.called,
failed_call == 'release_node')
def test_failure_during_extra_update_on_deploy_failure(self):
self.api.update_node.side_effect = [self.node, AssertionError()]
self.api.node_action.side_effect = RuntimeError('boom')
self.assertRaisesRegex(RuntimeError, 'boom',
self.pr.provision_node, self.node,
'image', [{'network': 'network'}],
wait=3600)
self.assertFalse(self.api.wait_for_node_state.called)
self.api.release_node.assert_called_once_with(self.node)
self.api.delete_port.assert_called_once_with(
self.api.create_port.return_value.id)
self.api.detach_port_from_node.assert_called_once_with(
self.node, self.api.create_port.return_value.id)
def test_missing_image(self):
self.api.get_image_info.side_effect = RuntimeError('Not found')
self.assertRaisesRegex(_exceptions.InvalidImage, 'Not found',
self.pr.provision_node,
self.node, 'image', [{'network': 'network'}])
self.api.update_node.assert_called_once_with(self.node, CLEAN_UP)
self.assertFalse(self.api.node_action.called)
self.api.release_node.assert_called_once_with(self.node)
def test_invalid_network(self):
self.api.get_network.side_effect = RuntimeError('Not found')
self.assertRaisesRegex(_exceptions.InvalidNIC, 'Not found',
self.pr.provision_node,
self.node, 'image', [{'network': 'network'}])
self.api.update_node.assert_called_once_with(self.node, CLEAN_UP)
self.assertFalse(self.api.create_port.called)
self.assertFalse(self.api.node_action.called)
self.api.release_node.assert_called_once_with(self.node)
def test_invalid_port(self):
self.api.get_port.side_effect = RuntimeError('Not found')
self.assertRaisesRegex(_exceptions.InvalidNIC, 'Not found',
self.pr.provision_node,
self.node, 'image', [{'port': 'port1'}])
self.api.update_node.assert_called_once_with(self.node, CLEAN_UP)
self.assertFalse(self.api.create_port.called)
self.assertFalse(self.api.node_action.called)
self.api.release_node.assert_called_once_with(self.node)
def test_no_local_gb(self):
self.node.properties = {}
self.assertRaises(_exceptions.UnknownRootDiskSize,
self.pr.provision_node,
self.node, 'image', [{'network': 'network'}])
self.assertFalse(self.api.create_port.called)
self.assertFalse(self.api.node_action.called)
self.api.release_node.assert_called_once_with(self.node)
def test_invalid_local_gb(self):
for value in (None, 'meow', -42, []):
self.node.properties = {'local_gb': value}
self.assertRaises(_exceptions.UnknownRootDiskSize,
self.pr.provision_node,
self.node, 'image', [{'network': 'network'}])
self.assertFalse(self.api.create_port.called)
self.assertFalse(self.api.node_action.called)
self.api.release_node.assert_called_with(self.node)
def test_invalid_root_disk_size(self):
self.assertRaises(TypeError,
self.pr.provision_node,
self.node, 'image', [{'network': 'network'}],
root_disk_size={})
self.assertRaises(ValueError,
self.pr.provision_node,
self.node, 'image', [{'network': 'network'}],
root_disk_size=0)
self.assertFalse(self.api.create_port.called)
self.assertFalse(self.api.node_action.called)
self.api.release_node.assert_called_with(self.node)
def test_invalid_nics(self):
self.assertRaisesRegex(TypeError, 'must be a list',
self.pr.provision_node,
self.node, 'image', 42)
self.assertFalse(self.api.create_port.called)
self.assertFalse(self.api.attach_port_to_node.called)
self.assertFalse(self.api.node_action.called)
self.api.release_node.assert_called_once_with(self.node)
def test_invalid_nic(self):
for item in ('string', ['string'], [{1: 2, 3: 4}]):
self.assertRaisesRegex(TypeError, 'must be a dict',
self.pr.provision_node,
self.node, 'image', item)
self.assertFalse(self.api.create_port.called)
self.assertFalse(self.api.attach_port_to_node.called)
self.assertFalse(self.api.node_action.called)
self.api.release_node.assert_called_with(self.node)
def test_invalid_nic_type(self):
self.assertRaisesRegex(ValueError, 'Unexpected NIC type foo',
self.pr.provision_node,
self.node, 'image', [{'foo': 'bar'}])
self.assertFalse(self.api.create_port.called)
self.assertFalse(self.api.attach_port_to_node.called)
self.assertFalse(self.api.node_action.called)
self.api.release_node.assert_called_once_with(self.node)
class TestUnprovisionNode(Base):
def test_ok(self):
self.node.extra['metalsmith_created_ports'] = ['port1']
self.pr.unprovision_node(self.node)
self.api.delete_port.assert_called_once_with('port1')
self.api.detach_port_from_node.assert_called_once_with(self.node,
'port1')
self.api.node_action.assert_called_once_with(self.node, 'deleted')
self.api.release_node.assert_called_once_with(self.node)
self.assertFalse(self.api.wait_for_node_state.called)
self.api.update_node.assert_called_once_with(self.node, CLEAN_UP)
def test_with_attached(self):
self.node.extra['metalsmith_created_ports'] = ['port1']
self.node.extra['metalsmith_attached_ports'] = ['port1', 'port2']
self.pr.unprovision_node(self.node)
self.api.delete_port.assert_called_once_with('port1')
calls = [mock.call(self.node, 'port1'), mock.call(self.node, 'port2')]
self.api.detach_port_from_node.assert_has_calls(calls, any_order=True)
self.api.node_action.assert_called_once_with(self.node, 'deleted')
self.api.release_node.assert_called_once_with(self.node)
self.assertFalse(self.api.wait_for_node_state.called)
self.api.update_node.assert_called_once_with(self.node, CLEAN_UP)
def test_with_wait(self):
self.node.extra['metalsmith_created_ports'] = ['port1']
self.pr.unprovision_node(self.node, wait=3600)
self.api.delete_port.assert_called_once_with('port1')
self.api.detach_port_from_node.assert_called_once_with(self.node,
'port1')
self.api.node_action.assert_called_once_with(self.node, 'deleted')
self.api.release_node.assert_called_once_with(self.node)
self.api.wait_for_node_state.assert_called_once_with(self.node,
'available',
timeout=3600)
def test_dry_run(self):
self.pr._dry_run = True
self.node.extra['metalsmith_created_ports'] = ['port1']
self.pr.unprovision_node(self.node)
self.assertFalse(self.api.node_action.called)
self.assertFalse(self.api.release_node.called)
self.assertFalse(self.api.delete_port.called)
self.assertFalse(self.api.detach_port_from_node.called)
self.assertFalse(self.api.wait_for_node_state.called)
self.assertFalse(self.api.update_node.called)