Add command and function to show instances
Story: #2002170 Task: #20027 Change-Id: I78991f75ea45ea61553d75301117c77a16ea8885
This commit is contained in:
parent
1cda22151d
commit
320144a73e
|
@ -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)
|
||||
|
||||
|
||||
|
|
|
@ -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(),
|
||||
|
|
|
@ -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:
|
||||
|
|
|
@ -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]
|
||||
|
|
|
@ -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'}})
|
||||
|
|
|
@ -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={}),
|
||||
|
|
|
@ -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()
|
||||
|
|
|
@ -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
|
||||
|
|
Loading…
Reference in New Issue