Add new introspection commands for interface data including lldp
This adds two new baremetal introspection commands to display interface data, including switch information from lldp packets, as described in the lldp-reporting.rst specification https://specs.openstack.org/openstack/ironic-inspector-specs/ specs/lldp-reporting.html. The new commands are "interface list <uuid>" and "interface show <uuid> <interface>". The list command will show specific interface fields for all interfaces in a node; additional fields can be displayed using the "--fields" argument. The show command will show all interface fields for a particular node and interface. Change-Id: I40d348c4fefffb85affb14f3d224a8d4bf4fb297 Closes-Bug: 1626253 Related-Bug: 1647515 Depends-On: I854826787ff045ffb2807970deaba8b77cbe277d
This commit is contained in:
parent
f3163834fd
commit
e4513c9b83
|
@ -160,3 +160,34 @@ calls.
|
|||
|
||||
|
||||
.. _introspection rules documentation: http://docs.openstack.org/developer/ironic-inspector/usage.html#introspection-rules
|
||||
|
||||
|
||||
List interface data
|
||||
~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
::
|
||||
|
||||
$ openstack baremetal introspection interface list NODE_IDENT
|
||||
[--fields=<field>] [--vlan=<vlan>]
|
||||
|
||||
* ``NODE_IDENT`` - Ironic node UUID or name
|
||||
* ``fields`` - name of one or more interface columns to display.
|
||||
* ``vlan`` - list only interfaces configured for this vlan id
|
||||
|
||||
Returns a list of interface data, including attached switch information,
|
||||
for each interface on the node.
|
||||
|
||||
Show interface data
|
||||
~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
::
|
||||
|
||||
$ openstack baremetal introspection interface show NODE_IDENT INTERFACE
|
||||
[--fields=<field>]
|
||||
|
||||
* ``NODE_IDENT`` - Ironic node UUID or name
|
||||
* ``INTERFACE`` - interface name on this node
|
||||
* ``fields`` - name of one or more interface rows to display.
|
||||
|
||||
Show interface data, including attached switch information,
|
||||
for a particular node and interface.
|
||||
|
|
|
@ -0,0 +1,93 @@
|
|||
|
||||
# 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.
|
||||
|
||||
|
||||
class InterfaceResource(object):
|
||||
"""InterfaceResource class
|
||||
|
||||
This class is used to manage the fields including Link Layer Discovery
|
||||
Protocols (LLDP) fields, that an interface contains. An individual
|
||||
field consists of a 'field_id' (key) and a 'label' (value).
|
||||
"""
|
||||
|
||||
FIELDS = {
|
||||
'interface': 'Interface',
|
||||
'mac': 'MAC Address',
|
||||
'node_ident': 'Node',
|
||||
'switch_capabilities_enabled': 'Switch Capabilities Enabled',
|
||||
'switch_capabilities_support': 'Switch Capabilities Supported',
|
||||
'switch_chassis_id': 'Switch Chassis ID',
|
||||
'switch_port_autonegotiation_enabled':
|
||||
'Switch Port Autonegotiation Enabled',
|
||||
'switch_port_autonegotiation_support':
|
||||
'Switch Port Autonegotiation Supported',
|
||||
'switch_port_description': 'Switch Port Description',
|
||||
'switch_port_id': 'Switch Port ID',
|
||||
'switch_port_link_aggregation_enabled':
|
||||
'Switch Port Link Aggregation Enabled',
|
||||
'switch_port_link_aggregation_support':
|
||||
'Switch Port Link Aggregation Supported',
|
||||
'switch_port_link_aggregation_id': 'Switch Port Link Aggregation ID',
|
||||
'switch_port_management_vlan_id': 'Switch Port Mgmt VLAN ID',
|
||||
'switch_port_mau_type': 'Switch Port Mau Type',
|
||||
'switch_port_mtu': 'Switch Port MTU',
|
||||
'switch_port_physical_capabilities':
|
||||
'Switch Port Physical Capabilities',
|
||||
'switch_port_protocol_vlan_enabled':
|
||||
'Switch Port Protocol VLAN Enabled',
|
||||
'switch_port_protocol_vlan_support':
|
||||
'Switch Port Protocol VLAN Supported',
|
||||
'switch_port_protocol_vlan_ids': 'Switch Port Protocol VLAN IDs',
|
||||
'switch_port_untagged_vlan_id': 'Switch Port Untagged VLAN',
|
||||
'switch_port_vlans': 'Switch Port VLANs',
|
||||
'switch_port_vlan_ids': 'Switch Port VLAN IDs',
|
||||
'switch_protocol_identities': 'Switch Protocol Identities',
|
||||
'switch_system_name': 'Switch System Name'
|
||||
}
|
||||
|
||||
DEFAULT_FIELD_IDS = ['interface',
|
||||
'mac',
|
||||
'switch_port_vlan_ids',
|
||||
'switch_chassis_id',
|
||||
'switch_port_id']
|
||||
|
||||
def __init__(self, field_ids=None, detailed=False):
|
||||
"""Create an InterfaceResource object
|
||||
|
||||
:param field_ids: A list of strings that the Resource object will
|
||||
contain. Each string must match an existing key in
|
||||
FIELDS.
|
||||
:param detailed: If True, use the all of the keys in FIELDS instead
|
||||
of input field_ids
|
||||
"""
|
||||
if field_ids is None:
|
||||
# Default field set in logical format, so don't sort
|
||||
field_ids = self.DEFAULT_FIELD_IDS
|
||||
|
||||
if detailed:
|
||||
field_ids = sorted(self.FIELDS.keys())
|
||||
|
||||
self._fields = tuple(field_ids)
|
||||
self._labels = tuple(self.FIELDS[x] for x in field_ids)
|
||||
|
||||
@property
|
||||
def fields(self):
|
||||
return self._fields
|
||||
|
||||
@property
|
||||
def labels(self):
|
||||
return self._labels
|
||||
|
||||
|
||||
INTERFACE_DEFAULT = InterfaceResource()
|
|
@ -22,7 +22,7 @@ from osc_lib.command import command
|
|||
from osc_lib import utils
|
||||
|
||||
import ironic_inspector_client
|
||||
|
||||
from ironic_inspector_client import resource as res
|
||||
|
||||
API_NAME = 'baremetal_introspection'
|
||||
API_VERSION_OPTION = 'inspector_api_version'
|
||||
|
@ -281,3 +281,77 @@ class DataSaveCommand(command.Command):
|
|||
fp.write(data)
|
||||
else:
|
||||
json.dump(data, sys.stdout)
|
||||
|
||||
|
||||
class InterfaceListCommand(command.Lister):
|
||||
"""List interface data including attached switch port information."""
|
||||
|
||||
def get_parser(self, prog_name):
|
||||
parser = super(InterfaceListCommand, self).get_parser(prog_name)
|
||||
parser.add_argument('node_ident', help='baremetal node UUID or name')
|
||||
parser.add_argument("--vlan",
|
||||
action='append',
|
||||
default=[], type=int,
|
||||
help="List only interfaces configured "
|
||||
"for this vlan id, can be repeated")
|
||||
display_group = parser.add_mutually_exclusive_group()
|
||||
display_group.add_argument(
|
||||
'--long', dest='detail',
|
||||
action='store_true', default=False,
|
||||
help="Show detailed information about interfaces.")
|
||||
display_group.add_argument(
|
||||
'--fields', nargs='+', dest='fields',
|
||||
metavar='<field>',
|
||||
choices=sorted(res.InterfaceResource(detailed=True).fields),
|
||||
help="Display one or more fields. "
|
||||
"Can not be used when '--long' is specified")
|
||||
|
||||
return parser
|
||||
|
||||
def take_action(self, parsed_args):
|
||||
|
||||
client = self.app.client_manager.baremetal_introspection
|
||||
|
||||
# If --long defined, use all fields
|
||||
interface_res = res.InterfaceResource(parsed_args.fields,
|
||||
parsed_args.detail)
|
||||
|
||||
rows = client.get_all_interface_data(parsed_args.node_ident,
|
||||
interface_res.fields,
|
||||
vlan=parsed_args.vlan)
|
||||
|
||||
return interface_res.labels, rows
|
||||
|
||||
|
||||
class InterfaceShowCommand(command.ShowOne):
|
||||
"""Show interface data including attached switch port information."""
|
||||
|
||||
COLUMNS = ("Field", "Value")
|
||||
|
||||
def get_parser(self, prog_name):
|
||||
parser = super(InterfaceShowCommand, self).get_parser(prog_name)
|
||||
parser.add_argument('node_ident', help='baremetal node UUID or name')
|
||||
parser.add_argument('interface', help='interface name')
|
||||
parser.add_argument(
|
||||
'--fields', nargs='+', dest='fields',
|
||||
metavar='<field>',
|
||||
choices=sorted(res.InterfaceResource(detailed=True).fields),
|
||||
help="Display one or more fields.")
|
||||
|
||||
return parser
|
||||
|
||||
def take_action(self, parsed_args):
|
||||
|
||||
client = self.app.client_manager.baremetal_introspection
|
||||
|
||||
if parsed_args.fields:
|
||||
interface_res = res.InterfaceResource(parsed_args.fields)
|
||||
else:
|
||||
# Show all fields in detailed resource
|
||||
interface_res = res.InterfaceResource(detailed=True)
|
||||
|
||||
iface_dict = client.get_interface_data(parsed_args.node_ident,
|
||||
parsed_args.interface,
|
||||
interface_res.fields)
|
||||
|
||||
return tuple(zip(*(iface_dict.items())))
|
||||
|
|
|
@ -14,6 +14,7 @@
|
|||
import eventlet
|
||||
eventlet.monkey_patch()
|
||||
|
||||
import copy
|
||||
import json
|
||||
import mock
|
||||
import os
|
||||
|
@ -331,6 +332,34 @@ class BaseCLITest(functional.Base):
|
|||
|
||||
|
||||
class TestCLI(BaseCLITest):
|
||||
def setup_lldp(self):
|
||||
functional.cfg.CONF.set_override('store_data', 'swift', 'processing')
|
||||
|
||||
self.all_interfaces = {
|
||||
'eth1': {'mac': self.macs[0], 'ip': self.ips[0],
|
||||
'client_id': None, 'lldp_processed':
|
||||
{'switch_chassis_id': "11:22:33:aa:bb:cc",
|
||||
'switch_port_vlans':
|
||||
[{"name": "vlan101", "id": 101},
|
||||
{"name": "vlan102", "id": 102},
|
||||
{"name": "vlan104", "id": 104},
|
||||
{"name": "vlan201", "id": 201},
|
||||
{"name": "vlan203", "id": 203}],
|
||||
'switch_port_id': "554",
|
||||
'switch_port_mtu': 1514}},
|
||||
'eth3': {'mac': self.macs[1], 'ip': None,
|
||||
'client_id': None, 'lldp_processed':
|
||||
{'switch_chassis_id': "11:22:33:aa:bb:cc",
|
||||
'switch_port_vlans':
|
||||
[{"name": "vlan101", "id": 101},
|
||||
{"name": "vlan102", "id": 102},
|
||||
{"name": "vlan104", "id": 106}],
|
||||
'switch_port_id': "557",
|
||||
'switch_port_mtu': 9216}}
|
||||
}
|
||||
|
||||
self.data['all_interfaces'] = self.all_interfaces
|
||||
|
||||
def _fake_status(self, **kwargs):
|
||||
# to remove the hidden fields
|
||||
hidden_status_items = shell.StatusCommand.hidden_status_items
|
||||
|
@ -352,6 +381,9 @@ class TestCLI(BaseCLITest):
|
|||
err = self.run_cli('rule', 'delete', 'uuid', expect_error=True)
|
||||
self.assertIn('not found', err)
|
||||
|
||||
err = self.run_cli('interface', 'list', expect_error=True)
|
||||
self.assertIn('too few arguments', err)
|
||||
|
||||
def test_introspect_get_status(self):
|
||||
self.run_cli('start', self.uuid)
|
||||
eventlet.greenthread.sleep(functional.DEFAULT_SLEEP)
|
||||
|
@ -410,6 +442,62 @@ class TestCLI(BaseCLITest):
|
|||
res = self.run_cli('rule', 'list', parse_json=True)
|
||||
self.assertEqual([], res)
|
||||
|
||||
@mock.patch.object(swift, 'get_introspection_data', autospec=True)
|
||||
def test_interface_list(self, get_mock):
|
||||
self.setup_lldp()
|
||||
get_mock.return_value = json.dumps(copy.deepcopy(self.data))
|
||||
|
||||
expected_eth1 = {u'Interface': u'eth1',
|
||||
u'MAC Address': u'11:22:33:44:55:66',
|
||||
u'Switch Chassis ID': u'11:22:33:aa:bb:cc',
|
||||
u'Switch Port ID': u'554',
|
||||
u'Switch Port VLAN IDs': [101, 102, 104, 201, 203]}
|
||||
expected_eth3 = {u'Interface': u'eth3',
|
||||
u'MAC Address': u'66:55:44:33:22:11',
|
||||
u'Switch Chassis ID': u'11:22:33:aa:bb:cc',
|
||||
u'Switch Port ID': u'557',
|
||||
u'Switch Port VLAN IDs': [101, 102, 106]}
|
||||
|
||||
res = self.run_cli('interface', 'list', self.uuid, parse_json=True)
|
||||
res.sort()
|
||||
self.assertEqual(expected_eth1, res[0])
|
||||
self.assertEqual(expected_eth3, res[1])
|
||||
|
||||
# Filter on vlan
|
||||
res = self.run_cli('interface', 'list', self.uuid, '--vlan', '106',
|
||||
parse_json=True)
|
||||
res.sort()
|
||||
self.assertEqual(expected_eth3, res[0])
|
||||
|
||||
# Select fields
|
||||
res = self.run_cli('interface', 'list', self.uuid, '--fields',
|
||||
'switch_port_mtu',
|
||||
parse_json=True)
|
||||
res.sort()
|
||||
self.assertEqual({u'Switch Port MTU': 1514}, res[0])
|
||||
self.assertEqual({u'Switch Port MTU': 9216}, res[1])
|
||||
|
||||
@mock.patch.object(swift, 'get_introspection_data', autospec=True)
|
||||
def test_interface_show(self, get_mock):
|
||||
self.setup_lldp()
|
||||
get_mock.return_value = json.dumps(copy.deepcopy(self.data))
|
||||
|
||||
res = self.run_cli('interface', 'show', self.uuid, "eth1",
|
||||
parse_json=True)
|
||||
|
||||
expected = {u'interface': u'eth1',
|
||||
u'mac': u'11:22:33:44:55:66',
|
||||
u'switch_chassis_id': u'11:22:33:aa:bb:cc',
|
||||
u'switch_port_id': u'554',
|
||||
u'switch_port_mtu': 1514,
|
||||
u'switch_port_vlan_ids': [101, 102, 104, 201, 203],
|
||||
u'switch_port_vlans': [{u'id': 101, u'name': u'vlan101'},
|
||||
{u'id': 102, u'name': u'vlan102'},
|
||||
{u'id': 104, u'name': u'vlan104'},
|
||||
{u'id': 201, u'name': u'vlan201'},
|
||||
{u'id': 203, u'name': u'vlan203'}]}
|
||||
|
||||
self.assertDictContainsSubset(expected, res)
|
||||
|
||||
if __name__ == '__main__':
|
||||
with functional.mocked_server():
|
||||
|
|
|
@ -13,6 +13,7 @@
|
|||
|
||||
import sys
|
||||
|
||||
from collections import OrderedDict
|
||||
import mock
|
||||
from osc_lib.tests import utils
|
||||
import six
|
||||
|
@ -351,3 +352,204 @@ class TestDataSave(BaseTest):
|
|||
|
||||
self.assertEqual(b'{"answer": 42}', content)
|
||||
self.client.get_data.assert_called_once_with('uuid1', raw=True)
|
||||
|
||||
|
||||
class TestInterfaceCmds(BaseTest):
|
||||
def setUp(self):
|
||||
super(TestInterfaceCmds, self).setUp()
|
||||
|
||||
self.inspector_db = {
|
||||
"all_interfaces":
|
||||
{
|
||||
'em1': {'mac': "00:11:22:33:44:55", 'ip': "10.10.1.1",
|
||||
"lldp_processed": {
|
||||
"switch_chassis_id": "99:aa:bb:cc:dd:ff",
|
||||
"switch_port_id": "555",
|
||||
"switch_port_vlans":
|
||||
[{"id": 101, "name": "vlan101"},
|
||||
{"id": 102, "name": "vlan102"},
|
||||
{"id": 104, "name": "vlan104"},
|
||||
{"id": 201, "name": "vlan201"},
|
||||
{"id": 203, "name": "vlan203"}],
|
||||
"switch_port_mtu": 1514
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
def test_list(self):
|
||||
self.client.get_all_interface_data.return_value = [
|
||||
["em1", "00:11:22:33:44:55", [101, 102, 104, 201, 203],
|
||||
"99:aa:bb:cc:dd:ff", "555"],
|
||||
["em2", "00:11:22:66:77:88", [201, 203],
|
||||
"99:aa:bb:cc:dd:ff", "777"],
|
||||
["em3", "00:11:22:aa:bb:cc", '', '', '']]
|
||||
|
||||
arglist = ['uuid1']
|
||||
verifylist = [('node_ident', 'uuid1')]
|
||||
|
||||
cmd = shell.InterfaceListCommand(self.app, None)
|
||||
parsed_args = self.check_parser(cmd, arglist, verifylist)
|
||||
cols, values = cmd.take_action(parsed_args)
|
||||
|
||||
expected_cols = ("Interface", "MAC Address", "Switch Port VLAN IDs",
|
||||
"Switch Chassis ID", "Switch Port ID")
|
||||
|
||||
# Note that em3 has no lldp data
|
||||
expected_rows = [["em1", "00:11:22:33:44:55",
|
||||
[101, 102, 104, 201, 203],
|
||||
"99:aa:bb:cc:dd:ff",
|
||||
"555"],
|
||||
["em2", "00:11:22:66:77:88",
|
||||
[201, 203],
|
||||
"99:aa:bb:cc:dd:ff",
|
||||
"777"],
|
||||
["em3", "00:11:22:aa:bb:cc", '', '', '']]
|
||||
|
||||
self.assertEqual(expected_cols, cols)
|
||||
self.assertEqual(expected_rows, values)
|
||||
|
||||
def test_list_field(self):
|
||||
self.client.get_all_interface_data.return_value = [
|
||||
["em1", 1514],
|
||||
["em2", 9216],
|
||||
["em3", '']]
|
||||
|
||||
arglist = ['uuid1', '--fields', 'interface',
|
||||
"switch_port_mtu"]
|
||||
verifylist = [('node_ident', 'uuid1'),
|
||||
('fields', ["interface", "switch_port_mtu"])]
|
||||
|
||||
cmd = shell.InterfaceListCommand(self.app, None)
|
||||
parsed_args = self.check_parser(cmd, arglist, verifylist)
|
||||
cols, values = cmd.take_action(parsed_args)
|
||||
|
||||
expected_cols = ("Interface", "Switch Port MTU")
|
||||
expected_rows = [["em1", 1514],
|
||||
["em2", 9216],
|
||||
["em3", '']]
|
||||
|
||||
self.assertEqual(expected_cols, cols)
|
||||
self.assertEqual(expected_rows, values)
|
||||
|
||||
def test_list_filtered(self):
|
||||
self.client.get_all_interface_data.return_value = [
|
||||
["em1",
|
||||
"00:11:22:33:44:55",
|
||||
[101, 102, 104, 201, 203],
|
||||
"99:aa:bb:cc:dd:ff",
|
||||
"555"]]
|
||||
|
||||
arglist = ['uuid1', '--vlan', '104']
|
||||
verifylist = [('node_ident', 'uuid1'),
|
||||
('vlan', [104])]
|
||||
|
||||
cmd = shell.InterfaceListCommand(self.app, None)
|
||||
parsed_args = self.check_parser(cmd, arglist, verifylist)
|
||||
cols, values = cmd.take_action(parsed_args)
|
||||
|
||||
expected_cols = ("Interface", "MAC Address", "Switch Port VLAN IDs",
|
||||
"Switch Chassis ID", "Switch Port ID")
|
||||
expected_rows = [["em1", "00:11:22:33:44:55",
|
||||
[101, 102, 104, 201, 203],
|
||||
"99:aa:bb:cc:dd:ff",
|
||||
"555"]]
|
||||
|
||||
self.assertEqual(expected_cols, cols)
|
||||
self.assertEqual(expected_rows, values)
|
||||
|
||||
def test_list_no_data(self):
|
||||
self.client.get_all_interface_data.return_value = [[]]
|
||||
|
||||
arglist = ['uuid1']
|
||||
verifylist = [('node_ident', 'uuid1')]
|
||||
|
||||
cmd = shell.InterfaceListCommand(self.app, None)
|
||||
parsed_args = self.check_parser(cmd, arglist, verifylist)
|
||||
cols, values = cmd.take_action(parsed_args)
|
||||
|
||||
expected_cols = ("Interface", "MAC Address", "Switch Port VLAN IDs",
|
||||
"Switch Chassis ID", "Switch Port ID")
|
||||
expected_rows = [[]]
|
||||
|
||||
self.assertEqual(expected_cols, cols)
|
||||
self.assertEqual(expected_rows, values)
|
||||
|
||||
def test_show(self):
|
||||
self.client.get_data.return_value = self.inspector_db
|
||||
|
||||
data = OrderedDict(
|
||||
[('node_ident', "uuid1"),
|
||||
('interface', "em1"),
|
||||
('mac', "00:11:22:33:44:55"),
|
||||
('switch_chassis_id', "99:aa:bb:cc:dd:ff"),
|
||||
('switch_port_id', "555"),
|
||||
('switch_port_mtu', 1514),
|
||||
('switch_port_vlans',
|
||||
[{"id": 101, "name": "vlan101"},
|
||||
{"id": 102, "name": "vlan102"},
|
||||
{"id": 104, "name": "vlan104"},
|
||||
{"id": 201, "name": "vlan201"},
|
||||
{"id": 203, "name": "vlan203"}])]
|
||||
)
|
||||
|
||||
self.client.get_interface_data.return_value = data
|
||||
|
||||
arglist = ['uuid1', 'em1']
|
||||
verifylist = [('node_ident', 'uuid1'), ('interface', 'em1')]
|
||||
|
||||
cmd = shell.InterfaceShowCommand(self.app, None)
|
||||
parsed_args = self.check_parser(cmd, arglist, verifylist)
|
||||
cols, values = cmd.take_action(parsed_args)
|
||||
|
||||
expected_cols = ("node_ident", "interface", "mac",
|
||||
"switch_chassis_id", "switch_port_id",
|
||||
"switch_port_mtu", "switch_port_vlans")
|
||||
|
||||
expected_rows = ("uuid1", "em1", "00:11:22:33:44:55",
|
||||
"99:aa:bb:cc:dd:ff", "555", 1514,
|
||||
[{"id": 101, "name": "vlan101"},
|
||||
{"id": 102, "name": "vlan102"},
|
||||
{"id": 104, "name": "vlan104"},
|
||||
{"id": 201, "name": "vlan201"},
|
||||
{"id": 203, "name": "vlan203"}])
|
||||
|
||||
self.assertEqual(expected_cols, cols)
|
||||
self.assertEqual(expected_rows, values)
|
||||
|
||||
def test_show_field(self):
|
||||
self.client.get_data.return_value = self.inspector_db
|
||||
|
||||
data = OrderedDict([('node_ident', "uuid1"),
|
||||
('interface', "em1"),
|
||||
('switch_port_vlans',
|
||||
[{"id": 101, "name": "vlan101"},
|
||||
{"id": 102, "name": "vlan102"},
|
||||
{"id": 104, "name": "vlan104"},
|
||||
{"id": 201, "name": "vlan201"},
|
||||
{"id": 203, "name": "vlan203"}])
|
||||
])
|
||||
|
||||
self.client.get_interface_data.return_value = data
|
||||
|
||||
arglist = ['uuid1', 'em1', '--fields', 'node_ident', 'interface',
|
||||
"switch_port_vlans"]
|
||||
verifylist = [('node_ident', 'uuid1'), ('interface', 'em1'),
|
||||
('fields', ["node_ident", "interface",
|
||||
"switch_port_vlans"])]
|
||||
|
||||
cmd = shell.InterfaceShowCommand(self.app, None)
|
||||
parsed_args = self.check_parser(cmd, arglist, verifylist)
|
||||
cols, values = cmd.take_action(parsed_args)
|
||||
|
||||
expected_cols = ("node_ident", "interface", "switch_port_vlans")
|
||||
|
||||
expected_rows = ("uuid1", "em1",
|
||||
[{"id": 101, "name": "vlan101"},
|
||||
{"id": 102, "name": "vlan102"},
|
||||
{"id": 104, "name": "vlan104"},
|
||||
{"id": 201, "name": "vlan201"},
|
||||
{"id": 203, "name": "vlan203"}])
|
||||
|
||||
self.assertEqual(expected_cols, cols)
|
||||
self.assertEqual(expected_rows, values)
|
||||
|
|
|
@ -11,6 +11,7 @@
|
|||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
|
||||
from collections import OrderedDict
|
||||
import six
|
||||
import unittest
|
||||
|
||||
|
@ -325,3 +326,122 @@ class TestAbort(BaseTest):
|
|||
|
||||
def test_invalid_input(self, _):
|
||||
self.assertRaises(TypeError, self.get_client().abort, 42)
|
||||
|
||||
|
||||
@mock.patch.object(http.BaseClient, 'request')
|
||||
class TestInterfaceApi(BaseTest):
|
||||
def setUp(self):
|
||||
super(TestInterfaceApi, self).setUp()
|
||||
|
||||
self.inspector_db = {
|
||||
"all_interfaces": {
|
||||
'em1': {'mac': "00:11:22:33:44:55", 'ip': "10.10.1.1",
|
||||
"lldp_processed": {
|
||||
"switch_chassis_id": "99:aa:bb:cc:dd:ff",
|
||||
"switch_port_id": "555",
|
||||
"switch_port_vlans":
|
||||
[{"id": 101, "name": "vlan101"},
|
||||
{"id": 102, "name": "vlan102"},
|
||||
{"id": 104, "name": "vlan104"},
|
||||
{"id": 201, "name": "vlan201"},
|
||||
{"id": 203, "name": "vlan203"}],
|
||||
"switch_port_mtu": 1514}
|
||||
},
|
||||
'em2': {'mac': "00:11:22:66:77:88", 'ip': "10.10.1.2",
|
||||
"lldp_processed": {
|
||||
"switch_chassis_id": "99:aa:bb:cc:dd:ff",
|
||||
"switch_port_id": "777",
|
||||
"switch_port_vlans":
|
||||
[{"id": 201, "name": "vlan201"},
|
||||
{"id": 203, "name": "vlan203"}],
|
||||
"switch_port_mtu": 9216}
|
||||
},
|
||||
'em3': {'mac': "00:11:22:aa:bb:cc", 'ip': "10.10.1.2"}
|
||||
}
|
||||
}
|
||||
|
||||
def test_all_interfaces(self, mock_req):
|
||||
mock_req.return_value.json.return_value = self.inspector_db
|
||||
|
||||
fields = ['interface', 'mac', 'switch_chassis_id', 'switch_port_id',
|
||||
'switch_port_vlans']
|
||||
expected = [['em1', '00:11:22:33:44:55', '99:aa:bb:cc:dd:ff', '555',
|
||||
[{"id": 101, "name": "vlan101"},
|
||||
{"id": 102, "name": "vlan102"},
|
||||
{"id": 104, "name": "vlan104"},
|
||||
{"id": 201, "name": "vlan201"},
|
||||
{"id": 203, "name": "vlan203"}]],
|
||||
['em2', '00:11:22:66:77:88', '99:aa:bb:cc:dd:ff', '777',
|
||||
[{"id": 201, "name": "vlan201"},
|
||||
{"id": 203, "name": "vlan203"}]],
|
||||
['em3', '00:11:22:aa:bb:cc', None, None, None]]
|
||||
|
||||
actual = self.get_client().get_all_interface_data(self.uuid,
|
||||
fields)
|
||||
|
||||
self.assertEqual(sorted(expected), sorted(actual))
|
||||
|
||||
# Change fields
|
||||
fields = ['interface', 'switch_port_mtu']
|
||||
expected = [
|
||||
['em1', 1514],
|
||||
['em2', 9216],
|
||||
['em3', None]]
|
||||
|
||||
actual = self.get_client().get_all_interface_data(self.uuid, fields)
|
||||
self.assertEqual(expected, sorted(actual))
|
||||
|
||||
def test_all_interfaces_filtered(self, mock_req):
|
||||
mock_req.return_value.json.return_value = self.inspector_db
|
||||
|
||||
fields = ['interface', 'mac', 'switch_chassis_id', 'switch_port_id',
|
||||
'switch_port_vlan_ids']
|
||||
expected = [['em1', '00:11:22:33:44:55', '99:aa:bb:cc:dd:ff', '555',
|
||||
[101, 102, 104, 201, 203]]]
|
||||
|
||||
# Filter on expected VLAN
|
||||
vlan = [104]
|
||||
actual = self.get_client().get_all_interface_data(self.uuid,
|
||||
fields, vlan=vlan)
|
||||
self.assertEqual(expected, actual)
|
||||
|
||||
# VLANs don't match existing vlans
|
||||
vlan = [111, 555]
|
||||
actual = self.get_client().get_all_interface_data(self.uuid,
|
||||
fields, vlan=vlan)
|
||||
self.assertEqual([], actual)
|
||||
|
||||
def test_one_interface(self, mock_req):
|
||||
mock_req.return_value.json.return_value = self.inspector_db
|
||||
|
||||
# Note that a value for 'switch_foo' will not be found
|
||||
fields = ["node_ident", "interface", "mac", "switch_port_vlan_ids",
|
||||
"switch_chassis_id", "switch_port_id",
|
||||
"switch_port_mtu", "switch_port_vlans", "switch_foo"]
|
||||
|
||||
expected_values = OrderedDict(
|
||||
[('node_ident', self.uuid),
|
||||
('interface', "em1"),
|
||||
('mac', "00:11:22:33:44:55"),
|
||||
('switch_port_vlan_ids',
|
||||
[101, 102, 104, 201, 203]),
|
||||
('switch_chassis_id', "99:aa:bb:cc:dd:ff"),
|
||||
('switch_port_id', "555"),
|
||||
('switch_port_mtu', 1514),
|
||||
('switch_port_vlans',
|
||||
[{"id": 101, "name": "vlan101"},
|
||||
{"id": 102, "name": "vlan102"},
|
||||
{"id": 104, "name": "vlan104"},
|
||||
{"id": 201, "name": "vlan201"},
|
||||
{"id": 203, "name": "vlan203"}]),
|
||||
("switch_foo", None)])
|
||||
|
||||
iface_dict = self.get_client().get_interface_data(
|
||||
self.uuid, "em1", fields)
|
||||
self.assertEqual(expected_values, iface_dict)
|
||||
|
||||
# Test interface name not in 'all_interfaces'
|
||||
expected_values = OrderedDict()
|
||||
iface_dict = self.get_client().get_interface_data(
|
||||
self.uuid, "em55", fields)
|
||||
self.assertEqual(expected_values, iface_dict)
|
||||
|
|
|
@ -13,6 +13,7 @@
|
|||
|
||||
"""Client for V1 API."""
|
||||
|
||||
from collections import OrderedDict
|
||||
import logging
|
||||
import time
|
||||
|
||||
|
@ -274,6 +275,84 @@ class ClientV1(http.BaseClient):
|
|||
|
||||
return self.request('post', '/introspection/%s/abort' % uuid)
|
||||
|
||||
def get_interface_data(self, node_ident, interface, field_sel):
|
||||
"""Get interface data for the input node and interface
|
||||
|
||||
To get LLDP data, collection must be enabled by the kernel parameter
|
||||
ipa-collect-lldp=1, and the inspector plugin ``basic_lldp`` must
|
||||
be enabled.
|
||||
|
||||
:param node_ident: node UUID or name
|
||||
:param interface: interface name
|
||||
:param field_sel: list of all fields for which to get data
|
||||
:returns interface data in OrderedDict
|
||||
"""
|
||||
# Use OrderedDict to maintain order of user-entered fields
|
||||
iface_data = OrderedDict()
|
||||
|
||||
data = self.get_data(node_ident)
|
||||
all_interfaces = data.get('all_interfaces', [])
|
||||
|
||||
# Make sure interface name is valid
|
||||
if interface not in all_interfaces:
|
||||
return iface_data
|
||||
|
||||
# If lldp data not available this will still return interface,
|
||||
# mac, node_ident etc.
|
||||
lldp_proc = all_interfaces[interface].get('lldp_processed', {})
|
||||
|
||||
for f in field_sel:
|
||||
if f == 'node_ident':
|
||||
iface_data[f] = node_ident
|
||||
elif f == 'interface':
|
||||
iface_data[f] = interface
|
||||
elif f == 'mac':
|
||||
iface_data[f] = all_interfaces[interface].get(f)
|
||||
elif f == 'switch_port_vlan_ids':
|
||||
iface_data[f] = [item['id'] for item in
|
||||
lldp_proc.get('switch_port_vlans', [])]
|
||||
else:
|
||||
iface_data[f] = lldp_proc.get(f)
|
||||
|
||||
return iface_data
|
||||
|
||||
def get_all_interface_data(self, node_ident,
|
||||
field_sel, vlan=None):
|
||||
"""Get interface data for all of the interfaces on this node
|
||||
|
||||
:param node_ident: node UUID or name
|
||||
:param field_sel: list of all fields for which to get data
|
||||
:param vlan: list of vlans used to filter the lists returned
|
||||
:returns list of interface data, each interface in a list
|
||||
"""
|
||||
|
||||
# Get inventory data for this node
|
||||
data = self.get_data(node_ident)
|
||||
all_interfaces = data.get('all_interfaces', [])
|
||||
|
||||
rows = []
|
||||
if vlan:
|
||||
vlan = set(vlan)
|
||||
# walk all interfaces, appending data to row if not filtered
|
||||
for interface in all_interfaces:
|
||||
iface_dict = self.get_interface_data(node_ident,
|
||||
interface,
|
||||
field_sel)
|
||||
|
||||
values = list(iface_dict.values())
|
||||
|
||||
# Use (optional) vlans to filter row
|
||||
if not vlan:
|
||||
rows.append(values)
|
||||
continue
|
||||
|
||||
# curr_vlans may be None
|
||||
curr_vlans = iface_dict.get('switch_port_vlan_ids', [])
|
||||
if curr_vlans and (vlan & set(curr_vlans)):
|
||||
rows.append(values) # vlan matches, display this row
|
||||
|
||||
return rows
|
||||
|
||||
|
||||
class RulesAPI(object):
|
||||
"""Introspection rules API.
|
||||
|
|
|
@ -0,0 +1,8 @@
|
|||
---
|
||||
features:
|
||||
- Add ``introspection interface list`` and ``introspection interface show``
|
||||
commands to display stored introspection data for the node including
|
||||
attached switch port information. In order to get switch data,
|
||||
LLDP data collection must be enabled by the kernel parameter
|
||||
``ipa-collect-lldp=1`` and the inspector plugin ``basic_lldp`` must
|
||||
be enabled.
|
|
@ -35,6 +35,8 @@ openstack.baremetal_introspection.v1 =
|
|||
baremetal_introspection_rule_show = ironic_inspector_client.shell:RuleShowCommand
|
||||
baremetal_introspection_rule_delete = ironic_inspector_client.shell:RuleDeleteCommand
|
||||
baremetal_introspection_rule_purge = ironic_inspector_client.shell:RulePurgeCommand
|
||||
baremetal_introspection_interface_list = ironic_inspector_client.shell:InterfaceListCommand
|
||||
baremetal_introspection_interface_show = ironic_inspector_client.shell:InterfaceShowCommand
|
||||
|
||||
[pbr]
|
||||
autodoc_index_modules = True
|
||||
|
|
Loading…
Reference in New Issue