From 7b3561bb9a9488201e53ac8cd73274602af21ba9 Mon Sep 17 00:00:00 2001 From: Dmitry Tantsur Date: Mon, 18 Jun 2018 16:33:25 +0200 Subject: [PATCH] Move Instance to its own file Change-Id: I607b804f719e2a660c2e52a672b68b9281af4569 --- metalsmith/__init__.py | 2 +- metalsmith/_instance.py | 127 ++++++++++++++++++++++++++++ metalsmith/_provisioner.py | 121 ++------------------------ metalsmith/test/test_cmd.py | 13 +-- metalsmith/test/test_instance.py | 116 +++++++++++++++++++++++++ metalsmith/test/test_os_api.py | 8 +- metalsmith/test/test_provisioner.py | 102 +--------------------- 7 files changed, 266 insertions(+), 223 deletions(-) create mode 100644 metalsmith/_instance.py create mode 100644 metalsmith/test/test_instance.py diff --git a/metalsmith/__init__.py b/metalsmith/__init__.py index 2fdc15e..cc9ff6d 100644 --- a/metalsmith/__init__.py +++ b/metalsmith/__init__.py @@ -13,7 +13,7 @@ # See the License for the specific language governing permissions and # limitations under the License. -from metalsmith._provisioner import Instance +from metalsmith._instance import Instance from metalsmith._provisioner import Provisioner __all__ = ['Instance', 'Provisioner'] diff --git a/metalsmith/_instance.py b/metalsmith/_instance.py new file mode 100644 index 0000000..4c367b3 --- /dev/null +++ b/metalsmith/_instance.py @@ -0,0 +1,127 @@ +# 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. + +from metalsmith import _os_api + + +# NOTE(dtantsur): include available since there is a period of time between +# claiming the instance and starting the actual provisioning via ironic. +_DEPLOYING_STATES = frozenset(['available', 'deploying', 'wait call-back', + 'deploy complete']) +_ACTIVE_STATES = frozenset(['active']) +_ERROR_STATE = frozenset(['error', 'deploy failed']) + +_HEALTHY_STATES = frozenset(['deploying', 'active']) + + +class Instance(object): + """Instance status in metalsmith.""" + + def __init__(self, api, node): + self._api = api + self._uuid = node.uuid + self._node = node + + @property + def hostname(self): + """Node's hostname.""" + return self._node.instance_info.get(_os_api.HOSTNAME_FIELD) + + def ip_addresses(self): + """Returns IP addresses for this instance. + + :return: dict mapping network name or ID to a list of IP addresses. + """ + result = {} + for nic in self.nics(): + net = getattr(nic.network, 'name', None) or nic.network.id + result.setdefault(net, []).extend( + ip['ip_address'] for ip in nic.fixed_ips + if ip.get('ip_address') + ) + return result + + @property + def is_deployed(self): + """Whether the node is deployed.""" + return self._node.provision_state in _ACTIVE_STATES + + @property + def is_healthy(self): + """Whether the node is not at fault or maintenance.""" + return self.state in _HEALTHY_STATES and not self._node.maintenance + + def nics(self): + """List NICs for this instance. + + :return: List of `Port` objects with additional ``network`` fields + with full representations of their networks. + """ + result = [] + vifs = self._api.list_node_attached_ports(self.node) + for vif in vifs: + port = self._api.get_port(vif.id) + port.network = self._api.get_network(port.network_id) + result.append(port) + return result + + @property + def node(self): + """Underlying `Node` object.""" + return self._node + + @property + def state(self): + """Instance state. + + ``deploying`` + deployment is in progress + ``active`` + node is provisioned + ``maintenance`` + node is provisioned but is in maintenance mode + ``error`` + node has a failure + ``unknown`` + node in unexpected state (maybe unprovisioned or modified by + a third party) + """ + prov_state = self._node.provision_state + if prov_state in _DEPLOYING_STATES: + return 'deploying' + elif prov_state in _ERROR_STATE: + return 'error' + elif prov_state in _ACTIVE_STATES: + if self._node.maintenance: + return 'maintenance' + else: + return 'active' + else: + return 'unknown' + + def to_dict(self): + """Convert instance to a dict.""" + return { + 'hostname': self.hostname, + 'ip_addresses': self.ip_addresses(), + 'node': self._node.to_dict(), + 'state': self.state, + 'uuid': self._uuid, + } + + @property + def uuid(self): + """Instance UUID (the same as `Node` UUID for metalsmith).""" + return self._uuid diff --git a/metalsmith/_provisioner.py b/metalsmith/_provisioner.py index 3124220..db93bc9 100644 --- a/metalsmith/_provisioner.py +++ b/metalsmith/_provisioner.py @@ -20,6 +20,7 @@ import sys import six +from metalsmith import _instance from metalsmith import _os_api from metalsmith import _scheduler from metalsmith import _utils @@ -30,115 +31,6 @@ LOG = logging.getLogger(__name__) _CREATED_PORTS = 'metalsmith_created_ports' _ATTACHED_PORTS = 'metalsmith_attached_ports' -# NOTE(dtantsur): include available since there is a period of time between -# claiming the instance and starting the actual provisioning via ironic. -_DEPLOYING_STATES = frozenset(['available', 'deploying', 'wait call-back', - 'deploy complete']) -_ACTIVE_STATES = frozenset(['active']) -_ERROR_STATE = frozenset(['error', 'deploy failed']) - -_HEALTHY_STATES = frozenset(['deploying', 'active']) - - -class Instance(object): - """Instance status in metalsmith.""" - - def __init__(self, api, node): - self._api = api - self._uuid = node.uuid - self._node = node - - @property - def hostname(self): - """Node's hostname.""" - return self._node.instance_info.get(_os_api.HOSTNAME_FIELD) - - def ip_addresses(self): - """Returns IP addresses for this instance. - - :return: dict mapping network name or ID to a list of IP addresses. - """ - result = {} - for nic in self.nics(): - net = getattr(nic.network, 'name', None) or nic.network.id - result.setdefault(net, []).extend( - ip['ip_address'] for ip in nic.fixed_ips - if ip.get('ip_address') - ) - return result - - @property - def is_deployed(self): - """Whether the node is deployed.""" - return self._node.provision_state in _ACTIVE_STATES - - @property - def is_healthy(self): - """Whether the node is not at fault or maintenance.""" - return self.state in _HEALTHY_STATES and not self._node.maintenance - - def nics(self): - """List NICs for this instance. - - :return: List of `Port` objects with additional ``network`` fields - with full representations of their networks. - """ - result = [] - vifs = self._api.list_node_attached_ports(self.node) - for vif in vifs: - port = self._api.get_port(vif.id) - port.network = self._api.get_network(port.network_id) - result.append(port) - return result - - @property - def node(self): - """Underlying `Node` object.""" - return self._node - - @property - def state(self): - """Instance state. - - ``deploying`` - deployment is in progress - ``active`` - node is provisioned - ``maintenance`` - node is provisioned but is in maintenance mode - ``error`` - node has a failure - ``unknown`` - node in unexpected state (maybe unprovisioned or modified by - a third party) - """ - prov_state = self._node.provision_state - if prov_state in _DEPLOYING_STATES: - return 'deploying' - elif prov_state in _ERROR_STATE: - return 'error' - elif prov_state in _ACTIVE_STATES: - if self._node.maintenance: - return 'maintenance' - else: - return 'active' - else: - return 'unknown' - - def to_dict(self): - """Convert instance to a dict.""" - return { - 'hostname': self.hostname, - 'ip_addresses': self.ip_addresses(), - 'node': self._node.to_dict(), - 'state': self.state, - 'uuid': self._uuid, - } - - @property - def uuid(self): - """Instance UUID (the same as `Node` UUID for metalsmith).""" - return self._uuid class Provisioner(object): @@ -362,7 +254,7 @@ class Provisioner(object): # Update the node to return it's latest state node = self._api.get_node(node, refresh=True) - return Instance(self._api, node) + return _instance.Instance(self._api, node) def _get_nics(self, nics): """Validate and get the NICs.""" @@ -506,6 +398,9 @@ class Provisioner(object): order as ``instances``. """ with self._api.cache_node_list_for_lookup(): - return [Instance(self._api, - self._api.get_node(inst, accept_hostname=True)) - for inst in instances] + return [ + _instance.Instance( + self._api, + self._api.get_node(inst, accept_hostname=True)) + for inst in instances + ] diff --git a/metalsmith/test/test_cmd.py b/metalsmith/test/test_cmd.py index 64566df..b20e9b9 100644 --- a/metalsmith/test/test_cmd.py +++ b/metalsmith/test/test_cmd.py @@ -22,6 +22,7 @@ import six import testtools from metalsmith import _cmd +from metalsmith import _instance from metalsmith import _provisioner @@ -37,7 +38,7 @@ class TestDeploy(testtools.TestCase): @mock.patch.object(_cmd, 'logging', autospec=True) def test_args_ok(self, mock_log, mock_os_conf, mock_pr): instance = mock_pr.return_value.provision_node.return_value - instance.create_autospec(_provisioner.Instance) + instance.create_autospec(_instance.Instance) instance.node.name = None instance.node.uuid = '123' instance.state = 'active' @@ -79,7 +80,7 @@ class TestDeploy(testtools.TestCase): @mock.patch.object(_cmd, 'logging', autospec=True) def test_args_json_format(self, mock_log, mock_os_conf, mock_pr): instance = mock_pr.return_value.provision_node.return_value - instance.create_autospec(_provisioner.Instance) + instance.create_autospec(_instance.Instance) instance.to_dict.return_value = {'node': 'dict'} args = ['--format', 'json', 'deploy', '--network', 'mynet', @@ -116,7 +117,7 @@ class TestDeploy(testtools.TestCase): def test_no_ips(self, mock_os_conf, mock_pr): instance = mock_pr.return_value.provision_node.return_value - instance.create_autospec(_provisioner.Instance) + instance.create_autospec(_instance.Instance) instance.is_deployed = True instance.ip_addresses.return_value = {} instance.node.name = None @@ -131,7 +132,7 @@ class TestDeploy(testtools.TestCase): def test_not_deployed_no_ips(self, mock_os_conf, mock_pr): instance = mock_pr.return_value.provision_node.return_value - instance.create_autospec(_provisioner.Instance) + instance.create_autospec(_instance.Instance) instance.is_deployed = False instance.node.name = None instance.node.uuid = '123' @@ -146,7 +147,7 @@ class TestDeploy(testtools.TestCase): @mock.patch.object(_cmd.LOG, 'info', autospec=True) def test_no_logs_not_deployed(self, mock_log, mock_os_conf, mock_pr): instance = mock_pr.return_value.provision_node.return_value - instance.create_autospec(_provisioner.Instance) + instance.create_autospec(_instance.Instance) instance.is_deployed = False args = ['deploy', '--network', 'mynet', '--image', 'myimg', 'compute'] @@ -619,7 +620,7 @@ class TestShow(testtools.TestCase): 'metalsmith._format._print', autospec=True)) self.mock_print = self.print_fixture.mock self.instances = [ - mock.Mock(spec=_provisioner.Instance, hostname=hostname, + mock.Mock(spec=_instance.Instance, hostname=hostname, uuid=hostname[-1], is_deployed=(hostname[-1] == '1'), state=('active' if hostname[-1] == '1' else 'deploying'), **{'ip_addresses.return_value': {'private': diff --git a/metalsmith/test/test_instance.py b/metalsmith/test/test_instance.py new file mode 100644 index 0000000..c1f0a89 --- /dev/null +++ b/metalsmith/test/test_instance.py @@ -0,0 +1,116 @@ +# 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 + +from metalsmith import _instance +from metalsmith.test import test_provisioner + + +class TestInstanceIPAddresses(test_provisioner.Base): + def setUp(self): + super(TestInstanceIPAddresses, self).setUp() + self.instance = _instance.Instance(self.api, self.node) + self.api.list_node_attached_ports.return_value = [ + mock.Mock(spec=['id'], id=i) for i in ('111', '222') + ] + self.ports = [ + mock.Mock(spec=['network_id', 'fixed_ips', 'network'], + network_id=n, fixed_ips=[{'ip_address': ip}]) + for n, ip in [('0', '192.168.0.1'), ('1', '10.0.0.2')] + ] + self.api.get_port.side_effect = self.ports + self.nets = [ + mock.Mock(spec=['id', 'name'], id=str(i)) for i in range(2) + ] + for n in self.nets: + n.name = 'name-%s' % n.id + self.api.get_network.side_effect = self.nets + + def test_ip_addresses(self): + ips = self.instance.ip_addresses() + self.assertEqual({'name-0': ['192.168.0.1'], + 'name-1': ['10.0.0.2']}, + ips) + + def test_missing_ip(self): + self.ports[0].fixed_ips = {} + ips = self.instance.ip_addresses() + self.assertEqual({'name-0': [], + 'name-1': ['10.0.0.2']}, ips) + + +class TestInstanceStates(test_provisioner.Base): + def setUp(self): + super(TestInstanceStates, self).setUp() + self.instance = _instance.Instance(self.api, self.node) + + def test_state_deploying(self): + self.node.provision_state = 'wait call-back' + self.assertEqual('deploying', self.instance.state) + self.assertFalse(self.instance.is_deployed) + self.assertTrue(self.instance.is_healthy) + + def test_state_deploying_when_available(self): + self.node.provision_state = 'available' + self.assertEqual('deploying', self.instance.state) + self.assertFalse(self.instance.is_deployed) + self.assertTrue(self.instance.is_healthy) + + def test_state_deploying_maintenance(self): + self.node.maintenance = True + self.node.provision_state = 'wait call-back' + self.assertEqual('deploying', self.instance.state) + self.assertFalse(self.instance.is_deployed) + self.assertFalse(self.instance.is_healthy) + + def test_state_active(self): + self.node.provision_state = 'active' + self.assertEqual('active', self.instance.state) + self.assertTrue(self.instance.is_deployed) + self.assertTrue(self.instance.is_healthy) + + def test_state_maintenance(self): + self.node.maintenance = True + self.node.provision_state = 'active' + self.assertEqual('maintenance', self.instance.state) + self.assertTrue(self.instance.is_deployed) + self.assertFalse(self.instance.is_healthy) + + def test_state_error(self): + self.node.provision_state = 'deploy failed' + self.assertEqual('error', self.instance.state) + self.assertFalse(self.instance.is_deployed) + self.assertFalse(self.instance.is_healthy) + + def test_state_unknown(self): + self.node.provision_state = 'enroll' + self.assertEqual('unknown', self.instance.state) + self.assertFalse(self.instance.is_deployed) + self.assertFalse(self.instance.is_healthy) + + @mock.patch.object(_instance.Instance, 'ip_addresses', autospec=True) + def test_to_dict(self, mock_ips): + self.node.provision_state = 'wait call-back' + self.node.to_dict.return_value = {'node': 'dict'} + self.node.instance_info = {'metalsmith_hostname': 'host'} + mock_ips.return_value = {'private': ['1.2.3.4']} + + self.assertEqual({'hostname': 'host', + 'ip_addresses': {'private': ['1.2.3.4']}, + 'node': {'node': 'dict'}, + 'state': 'deploying', + 'uuid': self.node.uuid}, + self.instance.to_dict()) diff --git a/metalsmith/test/test_os_api.py b/metalsmith/test/test_os_api.py index b3a0fd1..51f410d 100644 --- a/metalsmith/test/test_os_api.py +++ b/metalsmith/test/test_os_api.py @@ -17,8 +17,8 @@ import fixtures import mock import testtools +from metalsmith import _instance from metalsmith import _os_api -from metalsmith import _provisioner class TestInit(testtools.TestCase): @@ -95,14 +95,14 @@ class TestNodes(testtools.TestCase): self.assertIs(res, self.cli.node.get.return_value) def test_get_node_by_instance(self): - inst = _provisioner.Instance(mock.Mock(), mock.Mock()) + inst = _instance.Instance(mock.Mock(), mock.Mock()) res = self.api.get_node(inst) self.assertIs(res, inst.node) self.assertFalse(self.cli.node.get.called) def test_get_node_by_instance_with_refresh(self): - inst = _provisioner.Instance(mock.Mock(), - mock.Mock(spec=['uuid'], uuid='uuid1')) + inst = _instance.Instance(mock.Mock(), + mock.Mock(spec=['uuid'], uuid='uuid1')) res = self.api.get_node(inst, refresh=True) self.cli.node.get.assert_called_once_with('uuid1', fields=_os_api.NODE_FIELDS) diff --git a/metalsmith/test/test_provisioner.py b/metalsmith/test/test_provisioner.py index acd55d8..96c9311 100644 --- a/metalsmith/test/test_provisioner.py +++ b/metalsmith/test/test_provisioner.py @@ -16,6 +16,7 @@ import mock import testtools +from metalsmith import _instance from metalsmith import _os_api from metalsmith import _provisioner from metalsmith import exceptions @@ -612,7 +613,7 @@ class TestShowInstance(Base): inst = self.pr.show_instance('uuid1') self.api.get_node.assert_called_once_with('uuid1', accept_hostname=True) - self.assertIsInstance(inst, _provisioner.Instance) + self.assertIsInstance(inst, _instance.Instance) self.assertIs(inst.node, self.node) self.assertIs(inst.uuid, self.node.uuid) self.api.cache_node_list_for_lookup.assert_called_once_with() @@ -626,104 +627,7 @@ class TestShowInstance(Base): ]) self.assertIsInstance(result, list) for inst in result: - self.assertIsInstance(inst, _provisioner.Instance) + self.assertIsInstance(inst, _instance.Instance) self.assertIs(result[0].node, self.node) self.assertIs(result[0].uuid, self.node.uuid) self.api.cache_node_list_for_lookup.assert_called_once_with() - - -class TestInstanceStates(Base): - def setUp(self): - super(TestInstanceStates, self).setUp() - self.instance = _provisioner.Instance(self.api, self.node) - - def test_state_deploying(self): - self.node.provision_state = 'wait call-back' - self.assertEqual('deploying', self.instance.state) - self.assertFalse(self.instance.is_deployed) - self.assertTrue(self.instance.is_healthy) - - def test_state_deploying_when_available(self): - self.node.provision_state = 'available' - self.assertEqual('deploying', self.instance.state) - self.assertFalse(self.instance.is_deployed) - self.assertTrue(self.instance.is_healthy) - - def test_state_deploying_maintenance(self): - self.node.maintenance = True - self.node.provision_state = 'wait call-back' - self.assertEqual('deploying', self.instance.state) - self.assertFalse(self.instance.is_deployed) - self.assertFalse(self.instance.is_healthy) - - def test_state_active(self): - self.node.provision_state = 'active' - self.assertEqual('active', self.instance.state) - self.assertTrue(self.instance.is_deployed) - self.assertTrue(self.instance.is_healthy) - - def test_state_maintenance(self): - self.node.maintenance = True - self.node.provision_state = 'active' - self.assertEqual('maintenance', self.instance.state) - self.assertTrue(self.instance.is_deployed) - self.assertFalse(self.instance.is_healthy) - - def test_state_error(self): - self.node.provision_state = 'deploy failed' - self.assertEqual('error', self.instance.state) - self.assertFalse(self.instance.is_deployed) - self.assertFalse(self.instance.is_healthy) - - def test_state_unknown(self): - self.node.provision_state = 'enroll' - self.assertEqual('unknown', self.instance.state) - self.assertFalse(self.instance.is_deployed) - self.assertFalse(self.instance.is_healthy) - - @mock.patch.object(_provisioner.Instance, 'ip_addresses', autospec=True) - def test_to_dict(self, mock_ips): - self.node.provision_state = 'wait call-back' - self.node.to_dict.return_value = {'node': 'dict'} - self.node.instance_info = {'metalsmith_hostname': 'host'} - mock_ips.return_value = {'private': ['1.2.3.4']} - - self.assertEqual({'hostname': 'host', - 'ip_addresses': {'private': ['1.2.3.4']}, - 'node': {'node': 'dict'}, - 'state': 'deploying', - 'uuid': self.node.uuid}, - self.instance.to_dict()) - - -class TestInstanceIPAddresses(Base): - def setUp(self): - super(TestInstanceIPAddresses, self).setUp() - self.instance = _provisioner.Instance(self.api, self.node) - self.api.list_node_attached_ports.return_value = [ - mock.Mock(spec=['id'], id=i) for i in ('111', '222') - ] - self.ports = [ - mock.Mock(spec=['network_id', 'fixed_ips', 'network'], - network_id=n, fixed_ips=[{'ip_address': ip}]) - for n, ip in [('0', '192.168.0.1'), ('1', '10.0.0.2')] - ] - self.api.get_port.side_effect = self.ports - self.nets = [ - mock.Mock(spec=['id', 'name'], id=str(i)) for i in range(2) - ] - for n in self.nets: - n.name = 'name-%s' % n.id - self.api.get_network.side_effect = self.nets - - def test_ip_addresses(self): - ips = self.instance.ip_addresses() - self.assertEqual({'name-0': ['192.168.0.1'], - 'name-1': ['10.0.0.2']}, - ips) - - def test_missing_ip(self): - self.ports[0].fixed_ips = {} - ips = self.instance.ip_addresses() - self.assertEqual({'name-0': [], - 'name-1': ['10.0.0.2']}, ips)