Move Instance to its own file

Change-Id: I607b804f719e2a660c2e52a672b68b9281af4569
This commit is contained in:
Dmitry Tantsur 2018-06-18 16:33:25 +02:00
parent 8464fced77
commit 7b3561bb9a
7 changed files with 266 additions and 223 deletions

View File

@ -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']

127
metalsmith/_instance.py Normal file
View File

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

View File

@ -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,
return [
_instance.Instance(
self._api,
self._api.get_node(inst, accept_hostname=True))
for inst in instances]
for inst in instances
]

View File

@ -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':

View File

@ -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())

View File

@ -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,13 +95,13 @@ 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(),
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',

View File

@ -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)