Return Instance object from provisioning API

Change-Id: I2676ee281bfebb12abbb5702bcc6639526fe3189
This commit is contained in:
Dmitry Tantsur 2018-05-22 13:32:47 +02:00
parent c7dddc1f28
commit 804f0a7a3a
6 changed files with 164 additions and 6 deletions

View File

@ -13,6 +13,7 @@
# See the License for the specific language governing permissions and
# limitations under the License.
from metalsmith._provisioner import Instance
from metalsmith._provisioner import Provisioner
__all__ = ['Provisioner']
__all__ = ['Instance', 'Provisioner']

View File

@ -23,7 +23,7 @@ import six
LOG = logging.getLogger(__name__)
REMOVE = object()
NODE_FIELDS = ['name', 'uuid', 'instance_uuid', 'maintenance',
'maintenance_reason', 'properties', 'extra']
'maintenance_reason', 'properties', 'provision_state', 'extra']
class DictWithAttrs(dict):
@ -87,6 +87,9 @@ class API(object):
def get_node(self, node):
if isinstance(node, six.string_types):
return self.ironic.node.get(node, fields=NODE_FIELDS)
elif hasattr(node, 'node'):
# Instance object
return node.node
else:
return node

View File

@ -30,6 +30,72 @@ 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 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
@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'
@property
def uuid(self):
"""Instance UUID (the same as `Node` UUID for metalsmith)."""
return self._uuid
class Provisioner(object):
@ -135,7 +201,9 @@ class Provisioner(object):
:param netboot: Whether to use networking boot for final instances.
:param wait: How many seconds to wait for the deployment to finish,
None to return immediately.
:return: provisioned `Node` object.
:return: :py:class:`metalsmith.Instance` object with the current
status of provisioning. If ``wait`` is not ``None``, provisioning
is already finished.
:raises: :py:class:`metalsmith.exceptions.Error`
"""
node = self._check_node_for_deploy(node)
@ -208,7 +276,7 @@ class Provisioner(object):
LOG.info('Deploy succeeded on node %s', _utils.log_node(node))
self._log_ips(node, created_ports)
return node
return Instance(self._api, node)
def _log_ips(self, node, created_ports):
ips = []
@ -313,7 +381,8 @@ class Provisioner(object):
def unprovision_node(self, node, wait=None):
"""Unprovision a previously provisioned node.
:param node: node object, UUID or name.
:param node: `Node` object, :py:class:`metalsmith.Instance`,
UUID or name.
:param wait: How many seconds to wait for the process to finish,
None to return immediately.
:return: nothing.

View File

@ -13,10 +13,12 @@
# See the License for the specific language governing permissions and
# limitations under the License.
import fixtures
import mock
import testtools
from metalsmith import _os_api
from metalsmith import _provisioner
class TestInit(testtools.TestCase):
@ -38,3 +40,31 @@ class TestInit(testtools.TestCase):
api = _os_api.API(cloud_region=region)
self.assertIs(api.session, region.get_session.return_value)
mock_conn.assert_called_once_with(config=region)
class TestNodes(testtools.TestCase):
def setUp(self):
super(TestNodes, self).setUp()
self.session = mock.Mock()
self.ironic_fixture = self.useFixture(
fixtures.MockPatchObject(_os_api.ir_client, 'get_client',
autospec=True))
self.cli = self.ironic_fixture.mock.return_value
self.api = _os_api.API(session=self.session)
def test_get_node_by_uuid(self):
res = self.api.get_node('uuid1')
self.cli.node.get.assert_called_once_with('uuid1',
fields=_os_api.NODE_FIELDS)
self.assertIs(res, self.cli.node.get.return_value)
def test_get_node_by_node(self):
res = self.api.get_node(mock.sentinel.node)
self.assertIs(res, mock.sentinel.node)
self.assertFalse(self.cli.node.get.called)
def test_get_node_by_instance(self):
inst = _provisioner.Instance(mock.Mock(), mock.Mock())
res = self.api.get_node(inst)
self.assertIs(res, inst.node)
self.assertFalse(self.cli.node.get.called)

View File

@ -108,7 +108,11 @@ class TestProvisionNode(Base):
}
def test_ok(self):
self.pr.provision_node(self.node, 'image', [{'network': 'network'}])
inst = self.pr.provision_node(self.node, 'image',
[{'network': 'network'}])
self.assertEqual(inst.uuid, self.node.uuid)
self.assertEqual(inst.node, self.node)
self.api.create_port.assert_called_once_with(
network_id=self.api.get_network.return_value.id)
@ -529,3 +533,53 @@ class TestUnprovisionNode(Base):
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)
class TestInstance(Base):
def setUp(self):
super(TestInstance, 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)

View File

@ -4,6 +4,7 @@
coverage!=4.4,>=4.0 # Apache-2.0
doc8>=0.6.0 # Apache-2.0
flake8-import-order>=0.13 # LGPLv3
fixtures>=3.0.0 # Apache-2.0/BSD
hacking>=1.0.0 # Apache-2.0
mock>=2.0 # BSD
testtools>=2.2.0 # MIT