Add command and function to show instances

Story: #2002170
Task: #20027
Change-Id: I78991f75ea45ea61553d75301117c77a16ea8885
This commit is contained in:
Dmitry Tantsur 2018-06-06 18:21:04 +02:00
parent 1cda22151d
commit 320144a73e
8 changed files with 165 additions and 26 deletions

View File

@ -68,6 +68,11 @@ def _do_undeploy(api, args, formatter):
formatter.undeploy(node)
def _do_show(api, args, formatter):
instances = api.show_instances(args.instance)
formatter.show(instances)
def _parse_args(args, config):
parser = argparse.ArgumentParser(
description='Deployment and Scheduling tool for Bare Metal')
@ -122,6 +127,11 @@ def _parse_args(args, config):
undeploy.add_argument('--wait', type=int,
help='time (in seconds) to wait for node to become '
'available for deployment again')
show = subparsers.add_parser('show')
show.set_defaults(func=_do_show)
show.add_argument('instance', nargs='+', help='instance UUID(s)')
return parser.parse_args(args)

View File

@ -43,15 +43,7 @@ class DefaultFormat(object):
def deploy(self, instance):
"""Output result of the deploy."""
_print("Node %(node)s, current state is %(state)s",
node=_utils.log_node(instance.node), state=instance.state)
if instance.is_deployed:
ips = instance.ip_addresses()
if ips:
ips = '; '.join('%s=%s' % (net, ','.join(ips))
for net, ips in ips.items())
_print('IP addresses: %(ips)s', ips=ips)
self.show([instance])
def undeploy(self, node):
"""Output result of undeploy."""
@ -62,6 +54,18 @@ class DefaultFormat(object):
_print(message, node=_utils.log_node(node))
def show(self, instances):
for instance in instances:
_print("Node %(node)s, current state is %(state)s",
node=_utils.log_node(instance.node), state=instance.state)
if instance.is_deployed:
ips = instance.ip_addresses()
if ips:
ips = '; '.join('%s=%s' % (net, ','.join(ips))
for net, ips in ips.items())
_print('* IP addresses: %(ips)s', ips=ips)
class JsonFormat(object):
"""JSON formatter."""
@ -77,6 +81,11 @@ class JsonFormat(object):
}
json.dump(result, sys.stdout)
def show(self, instances):
"""Output instance statuses."""
json.dump({instance.hostname: instance.to_dict()
for instance in instances}, sys.stdout)
FORMATS = {
'default': DefaultFormat(),

View File

@ -13,6 +13,7 @@
# See the License for the specific language governing permissions and
# limitations under the License.
import contextlib
import logging
from ironicclient import client as ir_client
@ -57,6 +58,8 @@ class API(object):
IRONIC_VERSION = '1'
IRONIC_MICRO_VERSION = '1.28'
_node_list = None
def __init__(self, session=None, cloud_region=None):
if cloud_region is None:
if session is None:
@ -76,9 +79,22 @@ class API(object):
self.IRONIC_VERSION, session=self.session,
os_ironic_api_version=self.IRONIC_MICRO_VERSION)
def _nodes_for_lookup(self):
return self.list_nodes(maintenance=None,
associated=None,
provision_state=None,
fields=['uuid', 'name', 'instance_info'])
def attach_port_to_node(self, node, port_id):
self.ironic.node.vif_attach(_node_id(node), port_id)
@contextlib.contextmanager
def cache_node_list_for_lookup(self):
if self._node_list is None:
self._node_list = self._nodes_for_lookup()
yield self._node_list
self._node_list = None
def create_port(self, network_id, **kwargs):
return self.connection.network.create_port(network_id=network_id,
admin_state_up=True,
@ -92,9 +108,7 @@ class API(object):
self.ironic.node.vif_detach(_node_id(node), port_id)
def find_node_by_hostname(self, hostname):
nodes = self.list_nodes(maintenance=None, associated=None,
provision_state=None,
fields=['uuid', 'name', 'instance_info'])
nodes = self._node_list or self._nodes_for_lookup()
existing = [n for n in nodes
if n.instance_info.get(HOSTNAME_FIELD) == hostname]
if len(existing) > 1:

View File

@ -486,3 +486,26 @@ class Provisioner(object):
LOG.info('Node %s undeployed successfully', _utils.log_node(node))
return self._api.get_node(node, refresh=True)
def show_instance(self, instance_id):
"""Show information about instance.
:param instance_id: hostname, UUID or node name.
:return: :py:class:`metalsmith.Instance` object.
"""
return self.show_instances([instance_id])[0]
def show_instances(self, instances):
"""Show information about instance.
More efficient than calling :meth:`show_instance` in a loop, because
it caches the node list.
:param instances: list of hostnames, UUIDs or node names.
:return: list of :py:class:`metalsmith.Instance` objects in the same
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]

View File

@ -608,3 +608,47 @@ class TestUndeploy(testtools.TestCase):
mock_pr.return_value.unprovision_node.assert_called_once_with(
'123456', wait=None
)
@mock.patch.object(_provisioner, 'Provisioner', autospec=True)
@mock.patch.object(_cmd.os_config, 'OpenStackConfig', autospec=True)
class TestShow(testtools.TestCase):
def setUp(self):
super(TestShow, self).setUp()
self.print_fixture = self.useFixture(fixtures.MockPatch(
'metalsmith._format._print', autospec=True))
self.mock_print = self.print_fixture.mock
self.instances = [
mock.Mock(spec=_provisioner.Instance, hostname=hostname,
uuid=hostname[-1], is_deployed=(hostname[-1] == '1'),
state=('active' if hostname[-1] == '1' else 'deploying'),
**{'ip_addresses.return_value': {'private':
['1.2.3.4']}})
for hostname in ['hostname1', 'hostname2']
]
for inst in self.instances:
inst.node.uuid = inst.uuid
inst.node.name = 'name-%s' % inst.uuid
inst.to_dict.return_value = {inst.node.uuid: inst.node.name}
def test_show(self, mock_os_conf, mock_pr):
mock_pr.return_value.show_instances.return_value = self.instances
args = ['show', 'uuid1', 'hostname2']
_cmd.main(args)
self.mock_print.assert_has_calls([
mock.call(mock.ANY, node='name-1 (UUID 1)', state='active'),
mock.call(mock.ANY, ips='private=1.2.3.4'),
mock.call(mock.ANY, node='name-2 (UUID 2)', state='deploying'),
])
def test_show_json(self, mock_os_conf, mock_pr):
mock_pr.return_value.show_instances.return_value = self.instances
args = ['--format', 'json', 'show', 'uuid1', 'hostname2']
fake_io = six.StringIO()
with mock.patch('sys.stdout', fake_io):
_cmd.main(args)
self.assertEqual(json.loads(fake_io.getvalue()),
{'hostname1': {'1': 'name-1'},
'hostname2': {'2': 'name-2'}})

View File

@ -120,6 +120,21 @@ class TestNodes(testtools.TestCase):
fields=_os_api.NODE_FIELDS)
self.assertIs(res, self.cli.node.get.return_value)
def test_find_node_by_hostname_cached(self):
self.cli.node.list.return_value = [
mock.Mock(uuid='uuid0', instance_info={}),
mock.Mock(uuid='uuid1',
instance_info={'metalsmith_hostname': 'host1'}),
]
with self.api.cache_node_list_for_lookup():
res = self.api.find_node_by_hostname('host1')
self.assertIs(res, self.cli.node.get.return_value)
self.assertIsNone(self.api.find_node_by_hostname('host2'))
self.assertEqual(1, self.cli.node.list.call_count)
# This call is no longer cached
self.assertIsNone(self.api.find_node_by_hostname('host2'))
self.assertEqual(2, self.cli.node.list.call_count)
def test_find_node_by_hostname_not_found(self):
self.cli.node.list.return_value = [
mock.Mock(uuid='uuid0', instance_info={}),

View File

@ -45,6 +45,7 @@ class Base(testtools.TestCase):
for (uuid, pxe) in [('000', True), ('111', False)]
]
self.api.find_node_by_hostname.return_value = None
self.api.cache_node_list_for_lookup = mock.MagicMock()
self.pr._api = self.api
@ -605,6 +606,32 @@ class TestUnprovisionNode(Base):
self.assertFalse(self.api.update_node.called)
class TestShowInstance(Base):
def test_show_instance(self):
self.api.get_node.side_effect = lambda n, *a, **kw: self.node
inst = self.pr.show_instance('uuid1')
self.api.get_node.assert_called_once_with('uuid1',
accept_hostname=True)
self.assertIsInstance(inst, _provisioner.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()
def test_show_instances(self):
self.api.get_node.side_effect = [self.node, mock.Mock()]
result = self.pr.show_instances(['1', '2'])
self.api.get_node.assert_has_calls([
mock.call('1', accept_hostname=True),
mock.call('2', accept_hostname=True)
])
self.assertIsInstance(result, list)
for inst in result:
self.assertIsInstance(inst, _provisioner.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()

View File

@ -19,30 +19,27 @@
--image {{ image }}
--ssh-public-key {{ ssh_key_file }}
--root-disk-size 9
--hostname test
{{ extra_args }}
baremetal
- name: Find the deployed node
command: openstack baremetal node list --provision-state active -f value -c UUID
register: active_node_result
- name: Get instance info via CLI
command: metalsmith --format=json show test
register: instance_info
- name: Check that the deployed node was found
fail:
msg: The deployed node cannot be found
when: active_node_result.stdout == ""
- name: Set active node fact
- name: Register instance information
set_fact:
active_node: "{{ active_node_result.stdout }}"
instance: "{{ (instance_info.stdout | from_json).test }}"
failed_when: instance.state != 'active' or instance.node.provision_state != 'active'
- name: Show active node information
command: openstack baremetal node show {{ active_node }}
command: openstack baremetal node show {{ instance.node.uuid }}
- name: Undeploy a node
command: metalsmith --debug undeploy --wait 900 {{ active_node }}
command: metalsmith --debug undeploy --wait 900 test
- name: Get the current status of the deployed node
command: openstack baremetal node show {{ active_node }} -f json
command: openstack baremetal node show {{ instance.node.uuid }} -f json
register: undeployed_node_result
- name: Parse node state
@ -60,7 +57,7 @@
when: undeployed_node.extra != {}
- name: Get attached VIFs for the node
command: openstack baremetal node vif list {{ active_node }} -f value -c ID
command: openstack baremetal node vif list {{ instance.node.uuid }} -f value -c ID
register: vif_list_output
- name: Check that no VIFs are still attached