Add interface attach/detach support

This patch add the ironicclient part of the interface attach/detach API
support.
New commands are introduced:

* ironic node-vif-list <node_id>
* ironic node-vif-attach <node_id> <vif_id>
* ironic node-vif-detach <node_id> <vif_id>

* openstack baremetal node vif list <node_id>
* openstack baremetal node vif attach <node_id> <vif_id>
* openstack baremetal node vif detach <node_id> <vif_id>

Bump OSC plugin last known API version to 1.28

Co-Authored-By: Vasyl Saienko <vsaienko@mirantis.com>
Co-Authored-By: Vladyslav Drok <vdrok@mirantis.com>

Depends-On: I70f1166a15a26f392734e21d6bc30a03da4e5486
Change-Id: I801c5633d72e3eb392e5a04362306c44f6100764
Partial-Bug: #1582188
This commit is contained in:
Sam Betts 2016-09-01 17:03:50 +01:00 committed by Vladyslav Drok
parent 8595b59e1a
commit 288371a43a
11 changed files with 299 additions and 2 deletions

View File

@ -25,7 +25,7 @@ LOG = logging.getLogger(__name__)
API_VERSION_OPTION = 'os_baremetal_api_version'
API_NAME = 'baremetal'
LAST_KNOWN_API_VERSION = 25
LAST_KNOWN_API_VERSION = 28
API_VERSIONS = {
'1.%d' % i: 'ironicclient.v1.client.Client'
for i in range(1, LAST_KNOWN_API_VERSION + 1)

View File

@ -1148,3 +1148,85 @@ class ValidateBaremetalNode(command.Lister):
data = oscutils.sort_items(data, 'interface')
return (field_labels,
(oscutils.get_dict_properties(s, fields) for s in data))
class VifListBaremetalNode(command.Lister):
"""Show attached VIFs for a node"""
log = logging.getLogger(__name__ + ".VifListBaremetalNode")
def get_parser(self, prog_name):
parser = super(VifListBaremetalNode, self).get_parser(prog_name)
parser.add_argument(
'node',
metavar='<node>',
help="Name or UUID of the node"
)
return parser
def take_action(self, parsed_args):
self.log.debug("take_action(%s)", parsed_args)
columns = res_fields.VIF_RESOURCE.fields
labels = res_fields.VIF_RESOURCE.labels
baremetal_client = self.app.client_manager.baremetal
data = baremetal_client.node.vif_list(parsed_args.node)
return (labels,
(oscutils.get_item_properties(s, columns) for s in data))
class VifAttachBaremetalNode(command.Command):
"""Attach VIF to a given node"""
log = logging.getLogger(__name__ + ".VifAttachBaremetalNode")
def get_parser(self, prog_name):
parser = super(VifAttachBaremetalNode, self).get_parser(prog_name)
parser.add_argument(
'node',
metavar='<node>',
help="Name or UUID of the node"
)
parser.add_argument(
'vif_id',
metavar='<vif-id>',
help="Name or UUID of the VIF to attach to a node."
)
return parser
def take_action(self, parsed_args):
self.log.debug("take_action(%s)", parsed_args)
baremetal_client = self.app.client_manager.baremetal
baremetal_client.node.vif_attach(parsed_args.node, parsed_args.vif_id)
class VifDetachBaremetalNode(command.Command):
"""Detach VIF from a given node"""
log = logging.getLogger(__name__ + ".VifDetachBaremetalNode")
def get_parser(self, prog_name):
parser = super(VifDetachBaremetalNode, self).get_parser(prog_name)
parser.add_argument(
'node',
metavar='<node>',
help="Name or UUID of the node"
)
parser.add_argument(
'vif_id',
metavar='<vif-id>',
help="Name or UUID of the VIF to detach from a node."
)
return parser
def take_action(self, parsed_args):
self.log.debug("take_action(%s)", parsed_args)
baremetal_client = self.app.client_manager.baremetal
baremetal_client.node.vif_detach(parsed_args.node, parsed_args.vif_id)

View File

@ -86,6 +86,8 @@ PORTGROUP = {'uuid': baremetal_portgroup_uuid,
'address': baremetal_portgroup_address,
'extra': baremetal_portgroup_extra}
VIFS = {'vifs': [{'id': 'aaa-aa'}]}
class TestBaremetal(utils.TestCommand):

View File

@ -1730,3 +1730,69 @@ class TestValidate(TestBaremetal):
self.cmd.take_action(parsed_args)
self.baremetal_mock.node.validate.assert_called_once_with('node_uuid')
class TestVifList(TestBaremetal):
def setUp(self):
super(TestVifList, self).setUp()
self.baremetal_mock.node.vif_list.return_value = [
baremetal_fakes.FakeBaremetalResource(
None,
copy.deepcopy(baremetal_fakes.VIFS),
loaded=True,
),
]
# Get the command object to test
self.cmd = baremetal_node.VifListBaremetalNode(self.app, None)
def test_baremetal_vif_list(self):
arglist = ['node_uuid']
verifylist = [('node', 'node_uuid')]
parsed_args = self.check_parser(self.cmd, arglist, verifylist)
self.cmd.take_action(parsed_args)
self.baremetal_mock.node.vif_list.assert_called_once_with('node_uuid')
class TestVifAttach(TestBaremetal):
def setUp(self):
super(TestVifAttach, self).setUp()
# Get the command object to test
self.cmd = baremetal_node.VifAttachBaremetalNode(self.app, None)
def test_baremetal_vif_attach(self):
arglist = ['node_uuid', 'aaa-aaa']
verifylist = [('node', 'node_uuid'),
('vif_id', 'aaa-aaa')]
parsed_args = self.check_parser(self.cmd, arglist, verifylist)
self.cmd.take_action(parsed_args)
self.baremetal_mock.node.vif_attach.assert_called_once_with(
'node_uuid', 'aaa-aaa')
class TestVifDetach(TestBaremetal):
def setUp(self):
super(TestVifDetach, self).setUp()
# Get the command object to test
self.cmd = baremetal_node.VifDetachBaremetalNode(self.app, None)
def test_baremetal_vif_detach(self):
arglist = ['node_uuid', 'aaa-aaa']
verifylist = [('node', 'node_uuid'),
('vif_id', 'aaa-aaa')]
parsed_args = self.check_parser(self.cmd, arglist, verifylist)
self.cmd.take_action(parsed_args)
self.baremetal_mock.node.vif_detach.assert_called_once_with(
'node_uuid', 'aaa-aaa')

View File

@ -91,6 +91,8 @@ NODE_VENDOR_PASSTHRU_METHOD = {"heartbeat": {"attach": "false",
"description": "",
"async": "true"}}
VIFS = {'vifs': [{'id': 'aaa-aaa'}]}
CREATE_NODE = copy.deepcopy(NODE1)
del CREATE_NODE['id']
del CREATE_NODE['uuid']
@ -383,6 +385,13 @@ fake_responses = {
NODE_VENDOR_PASSTHRU_METHOD,
),
},
'/v1/nodes/%s/vifs' % NODE1['uuid']:
{
'GET': (
{},
VIFS,
),
}
}
fake_responses_pagination = {
@ -1082,6 +1091,39 @@ class NodeManagerTest(testtools.TestCase):
self.assertRaises(exc.InvalidAttribute, self.mgr.vendor_passthru,
**kwargs)
@mock.patch.object(node.NodeManager, '_list')
def test_vif_list(self, _list_mock):
kwargs = {
'node_ident': NODE1['uuid'],
}
final_path = '/v1/nodes/%s/vifs' % NODE1['uuid']
self.mgr.vif_list(**kwargs)
_list_mock.assert_called_once_with(final_path, "vifs")
@mock.patch.object(node.NodeManager, 'update')
def test_vif_attach(self, update_mock):
kwargs = {
'node_ident': NODE1['uuid'],
'vif_id': 'vif_id',
}
final_path = '%s/vifs' % NODE1['uuid']
self.mgr.vif_attach(**kwargs)
update_mock.assert_called_once_with(final_path, {'id': 'vif_id'},
http_method="POST")
@mock.patch.object(node.NodeManager, 'delete')
def test_vif_detach(self, delete_mock):
kwargs = {
'node_ident': NODE1['uuid'],
'vif_id': 'vif_id',
}
final_path = '%s/vifs/vif_id' % NODE1['uuid']
self.mgr.vif_detach(**kwargs)
delete_mock.assert_called_once_with(final_path)
def _test_node_set_boot_device(self, boot_device, persistent=False):
self.mgr.set_boot_device(NODE1['uuid'], boot_device, persistent)
body = {'boot_device': boot_device, 'persistent': persistent}

View File

@ -1127,3 +1127,29 @@ class NodeShellTest(utils.BaseTestCase):
n_shell.do_node_get_vendor_passthru_methods(client_mock, args)
client_mock.node.get_vendor_passthru_methods.assert_called_once_with(
'node_uuid')
def test_do_node_vif_list(self):
client_mock = mock.MagicMock()
args = mock.MagicMock()
args.node = 'node_uuid'
n_shell.do_node_vif_list(client_mock, args)
client_mock.node.vif_list.assert_called_once_with(
'node_uuid')
def test_do_node_vif_attach(self):
client_mock = mock.MagicMock()
args = mock.MagicMock()
args.node = 'node_uuid'
args.vif_id = 'aaa-aaa'
n_shell.do_node_vif_attach(client_mock, args)
client_mock.node.vif_attach.assert_called_once_with(
'node_uuid', 'aaa-aaa')
def test_do_node_vif_detach(self):
client_mock = mock.MagicMock()
args = mock.MagicMock()
args.node = 'node_uuid'
args.vif_id = 'aaa-aaa'
n_shell.do_node_vif_detach(client_mock, args)
client_mock.node.vif_detach.assert_called_once_with(
'node_uuid', 'aaa-aaa')

View File

@ -244,6 +244,35 @@ class NodeManager(base.CreateManager):
raise exc.InvalidAttribute(
_('Unknown HTTP method: %s') % http_method)
def vif_list(self, node_ident):
"""List VIFs attached to a given node.
:param node_ident: The UUID or Name of the node.
"""
path = "%s/vifs" % node_ident
return self._list(self._path(path), "vifs")
def vif_attach(self, node_ident, vif_id):
"""Attach VIF to a given node.
param node_ident: The UUID or Name of the node.
param vif_id: The UUID or Name of the VIF to attach.
"""
path = "%s/vifs" % node_ident
data = {"id": vif_id}
# TODO(vdrok): cleanup places doing custom path and http_method
self.update(path, data, http_method="POST")
def vif_detach(self, node_ident, vif_id):
"""Detach VIF from a given node.
param node_ident: The UUID or Name of the node.
param vif_id: The UUID or Name of the VIF to detach.
"""
path = "%s/vifs/%s" % (node_ident, vif_id)
self.delete(path)
def set_maintenance(self, node_id, state, maint_reason=None):
"""Set the maintenance mode for the node.

View File

@ -599,3 +599,30 @@ def do_node_get_vendor_passthru_methods(cc, args):
field_labels=field_labels,
sortby_index=None,
json_flag=args.json)
@cliutils.arg('node', metavar='<node>', help="Name or UUID of the node.")
def do_node_vif_list(cc, args):
"""List VIFs for a given node."""
vifs = cc.node.vif_list(args.node)
fields = res_fields.VIF_RESOURCE.fields
field_labels = res_fields.VIF_RESOURCE.labels
cliutils.print_list(vifs, fields, field_labels=field_labels,
sortby_index=None,
json_flag=args.json)
@cliutils.arg('node', metavar='<node>', help="Name or UUID of the node.")
@cliutils.arg('vif_id', metavar='<vif-id>',
help="Name or UUID of the VIF to attach to node.")
def do_node_vif_attach(cc, args):
"""Attach VIF to a given node."""
cc.node.vif_attach(args.node, args.vif_id)
@cliutils.arg('node', metavar='<node>', help="Name or UUID of the node.")
@cliutils.arg('vif_id', metavar='<vif-id>',
help="Name or UUID of the VIF to detach from node.")
def do_node_vif_detach(cc, args):
"""Detach VIF from a given node."""
cc.node.vif_detach(args.node, args.vif_id)

View File

@ -70,7 +70,8 @@ class Resource(object):
'pxe_enabled': 'PXE boot enabled',
'portgroup_uuid': 'Portgroup UUID',
'network_interface': 'Network Interface',
'standalone_ports_supported': 'Standalone Ports Supported'
'standalone_ports_supported': 'Standalone Ports Supported',
'id': 'ID',
}
def __init__(self, field_ids, sort_excluded=None):
@ -244,3 +245,8 @@ PORTGROUP_RESOURCE = Resource(
'address',
'name',
])
# VIFs
VIF_RESOURCE = Resource(
['id'],
)

View File

@ -0,0 +1,14 @@
---
features:
- |
Adds support for attaching and detaching VIFs. This is available starting
with ironic API microversion 1.28.
The new commands are:
* ``ironic node-vif-list <node>``
* ``ironic node-vif-attach <node> <vif-id>``
* ``ironic node-vif-detach <node> <vif-id>``
* ``openstack baremetal node vif list <node>``
* ``openstack baremetal node vif attach <node> <vif-id>``
* ``openstack baremetal node vif detach <node> <vif-id>``

View File

@ -69,6 +69,9 @@ openstack.baremetal.v1 =
baremetal_node_undeploy = ironicclient.osc.v1.baremetal_node:UndeployBaremetalNode
baremetal_node_unset = ironicclient.osc.v1.baremetal_node:UnsetBaremetalNode
baremetal_node_validate = ironicclient.osc.v1.baremetal_node:ValidateBaremetalNode
baremetal_node_vif_attach = ironicclient.osc.v1.baremetal_node:VifAttachBaremetalNode
baremetal_node_vif_detach = ironicclient.osc.v1.baremetal_node:VifDetachBaremetalNode
baremetal_node_vif_list = ironicclient.osc.v1.baremetal_node:VifListBaremetalNode
baremetal_port_create = ironicclient.osc.v1.baremetal_port:CreateBaremetalPort
baremetal_port_delete = ironicclient.osc.v1.baremetal_port:DeleteBaremetalPort
baremetal_port_list = ironicclient.osc.v1.baremetal_port:ListBaremetalPort