Neutron cli support for gateway device

Python-neutronclient extension supports the following resources.
* gateway-device
* remote-mac-entry

Change-Id: Ic310d9b632dc127e474da8bc56787752b69f0ee0
Closes-Bug: #1555205
This commit is contained in:
Yoichiro Iura
2016-03-28 02:32:44 +00:00
parent dc4febc3e3
commit f334112b05
10 changed files with 650 additions and 1 deletions

View File

@@ -0,0 +1,110 @@
# Copyright (C) 2016 Midokura SARL
# All Rights Reserved.
#
# 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.
import mock
from mox3 import mox
from neutronclient import shell
from neutronclient.tests.unit import test_cli20
TOKEN = test_cli20.TOKEN
class MyResp(test_cli20.MyResp):
pass
class MyApp(test_cli20.MyApp):
pass
class MyComparator(test_cli20.MyComparator):
pass
class CLIExtTestV20Base(test_cli20.CLITestV20Base):
def setUp(self, plurals=None):
super(CLIExtTestV20Base, self).setUp(plurals=plurals)
def _setup_mock_patch(self, name):
patcher = mock.patch(name)
thing = patcher.start()
self.addCleanup(patcher.stop)
return thing
def _mock_load_extensions(self, resource):
load_method = ('neutronclient.common.extension.' +
'_discover_via_entry_points')
load_ext_mock = self._setup_mock_patch(load_method)
load_ext_mock.return_value = [resource]
return load_ext_mock
def _test_show_ext_resource(self, resource, cmd, myid, args, fields=(),
cmd_resource=None, parent_id=None):
self.mox.StubOutWithMock(cmd, "get_client")
self.mox.StubOutWithMock(self.client.httpclient, "request")
cmd.get_client().MultipleTimes().AndReturn(self.client)
if not cmd_resource:
cmd_resource = resource
query = "&".join(["fields=%s" % field for field in fields])
expected_res = {resource:
{self.id_field: myid,
'name': 'myname', }, }
resstr = self.client.serialize(expected_res)
path = getattr(self.client, cmd_resource + "_path")
if parent_id:
path = path % parent_id
path = path % myid
self.client.httpclient.request(
test_cli20.end_url(path, query, format=self.format), 'GET',
body=None,
headers=mox.ContainsKeyValue(
'X-Auth-Token', TOKEN)).AndReturn((MyResp(200), resstr))
self.mox.ReplayAll()
cmd_parser = cmd.get_parser("show_" + cmd_resource)
shell.run_command(cmd, cmd_parser, args)
self.mox.VerifyAll()
self.mox.UnsetStubs()
_str = self.fake_stdout.make_string()
self.assertIn(myid, _str)
self.assertIn('myname', _str)
def _test_delete_ext_resource(self, resource, cmd, myid, args,
cmd_resource=None, parent_id=None):
self.mox.StubOutWithMock(cmd, "get_client")
self.mox.StubOutWithMock(self.client.httpclient, "request")
cmd.get_client().MultipleTimes().AndReturn(self.client)
if not cmd_resource:
cmd_resource = resource
path = getattr(self.client, cmd_resource + "_path")
if parent_id:
path = path % parent_id
path = path % myid
self.client.httpclient.request(
test_cli20.end_url(path, format=self.format), 'DELETE',
body=None,
headers=mox.ContainsKeyValue(
'X-Auth-Token', TOKEN)).AndReturn((MyResp(204), None))
self.mox.ReplayAll()
cmd_parser = cmd.get_parser("delete_" + cmd_resource)
shell.run_command(cmd, cmd_parser, args)
self.mox.VerifyAll()
self.mox.UnsetStubs()
_str = self.fake_stdout.make_string()
self.assertIn(myid, _str)

View File

@@ -0,0 +1,193 @@
# Copyright (C) 2016 Midokura SARL
# All Rights Reserved.
#
# 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.
import sys
from midonet.neutron.tests.unit.neutronclient_ext import test_cli20
from midonet.neutronclient.gateway_device_extension import _gateway_device
from neutronclient import shell
class CLITestV20GatewayDeviceJSON(test_cli20.CLIExtTestV20Base):
def setUp(self):
gw_device = ("gateway_device", _gateway_device)
self._mock_load_extensions(gw_device)
super(CLITestV20GatewayDeviceJSON,
self).setUp(plurals={'gateway_devices': 'gateway_device'})
self.register_non_admin_status_resource('gateway_device')
def test_gateway_device_cmd_loaded(self):
shell.NeutronShell('2.0')
gw_device_cmd = {'gateway-device-list':
_gateway_device.GatewayDeviceList,
'gateway-device-create':
_gateway_device.GatewayDeviceCreate,
'gateway-device-update':
_gateway_device.GatewayDeviceUpdate,
'gateway-device-delete':
_gateway_device.GatewayDeviceDelete,
'gateway-device-show':
_gateway_device.GatewayDeviceShow
}
self.assertDictContainsSubset(gw_device_cmd, shell.COMMANDS['2.0'])
def _create_gateway_device(self, name, args,
position_names, position_values):
resource = 'gateway_device'
cmd = _gateway_device.GatewayDeviceCreate(
test_cli20.MyApp(sys.stdout), None)
self._test_create_resource(resource, cmd, name, 'myid',
args, position_names, position_values)
def _update_gateway_device(self, args, values):
resource = 'gateway_device'
cmd = _gateway_device.GatewayDeviceUpdate(
test_cli20.MyApp(sys.stdout), None)
self._test_update_resource(resource, cmd, 'myid',
args, values)
def test_create_gateway_device_for_hw_vtep_mandatory_params(self):
name = 'hw-vtep-mandatory'
gw_type = 'hw_vtep'
mng_ip = '10.0.0.100'
mng_port = '22'
mng_protocol = 'ovsdb'
args = ['--type', gw_type,
'--management-ip', mng_ip,
'--management-port', mng_port,
'--management-protocol', mng_protocol]
position_names = ['type', 'management_ip',
'management_port', 'management_protocol']
position_values = [gw_type, mng_ip, mng_port, mng_protocol]
self._create_gateway_device(name, args,
position_names, position_values)
def test_create_gateway_device_for_hw_vtep_with_optional_params(self):
name = 'hw-vtep-optional'
tenant_id = 'my_tenant'
gw_type = 'hw_vtep'
mng_ip = '10.0.0.100'
mng_port = '22'
mng_protocol = 'ovsdb'
tunnel_ip = '200.200.200.4'
args = ['--tenant-id', tenant_id,
'--type', gw_type,
'--management-ip', mng_ip,
'--management-port', mng_port,
'--management-protocol', mng_protocol,
'--name', name,
'--tunnel-ip', tunnel_ip]
position_names = ['type', 'management_ip', 'management_port',
'management_protocol', 'tenant_id',
'name', 'tunnel_ips']
position_values = [gw_type, mng_ip, mng_port, mng_protocol,
tenant_id, name, [tunnel_ip]]
self._create_gateway_device(name, args,
position_names, position_values)
def test_create_gateway_device_for_router_vtep_with_mandatory_params(self):
name = 'router-vtep-mandatory'
gw_type = 'router_vtep'
resource_id = 'my_router_id'
args = ['--type', gw_type, '--resource-id', resource_id]
position_names = ['type', 'resource_id']
position_values = [gw_type, resource_id]
self._create_gateway_device(name, args,
position_names, position_values)
def test_create_gateway_device_for_router_vtep_with_optional_params(self):
name = 'router-vtep-optional'
tenant_id = 'my_tenant'
gw_type = 'router_vtep'
resource_id = 'my_router_id'
tunnel_ip = '200.200.200.4'
args = ['--tenant-id', tenant_id,
'--type', gw_type,
'--resource-id', resource_id,
'--name', name,
'--tunnel-ip', tunnel_ip]
position_names = ['type', 'resource_id',
'tenant_id', 'name', 'tunnel_ips']
position_values = [gw_type, resource_id,
tenant_id, name, [tunnel_ip]]
self._create_gateway_device(name, args,
position_names, position_values)
def test_update_gateway_device_with_name(self):
args = ['myid', '--name', 'name_updated']
values = {'name': 'name_updated'}
self._update_gateway_device(args, values)
def test_update_gateway_device_with_tunnel_ip(self):
args = ['myid', '--tunnel-ip', '200.200.200.4']
values = {'tunnel_ips': ['200.200.200.4']}
self._update_gateway_device(args, values)
def test_update_gateway_device_with_tunnel_ips(self):
args = ['myid', '--tunnel-ip', '200.200.200.4',
'--tunnel-ip', '200.200.200.10']
values = {'tunnel_ips': ['200.200.200.4',
'200.200.200.10']}
self._update_gateway_device(args, values)
def test_delete_gateway_device(self):
resource = 'gateway_device'
cmd = _gateway_device.GatewayDeviceDelete(
test_cli20.MyApp(sys.stdout), None)
my_id = 'my-id'
args = [my_id]
self._test_delete_resource(resource, cmd, my_id, args)
def test_list_gateway_devices(self):
resources = 'gateway_devices'
cmd = _gateway_device.GatewayDeviceList(
test_cli20.MyApp(sys.stdout), None)
self._test_list_resources(resources, cmd)
def test_list_gateway_devices_with_pagination(self):
resources = 'gateway_devices'
cmd = _gateway_device.GatewayDeviceList(
test_cli20.MyApp(sys.stdout), None)
self._test_list_resources_with_pagination(resources, cmd)
def test_list_gateway_device_with_remote_mac_entries(self):
resources = 'gateway_devices'
cmd = _gateway_device.GatewayDeviceList(
test_cli20.MyApp(sys.stdout), None)
rme = [{"segmentation_id": 100,
"vtep_address": "192.168.100.1",
"id": "remote_mac_entry_id1",
"mac_address": "fa:16:3e:db:79:80"},
{"segmentation_id": 100,
"vtep_address": "192.168.100.50",
"id": "remote_mac_entry_id1",
"mac_address": "fa:16:3e:df:79:80"},
]
response = {'gateway_devices': [{"id": 'myid',
"name": 'gw_device',
"type": "router_vtep",
"resource_id": "router_id",
"tunnel_ips": [],
"remote_mac_entries": rme}]}
args = ['-c', 'id', '-c', 'remote_mac_entries']
self._test_list_columns(cmd, resources, response, args)
def test_show_gateway_device(self):
resource = 'gateway_device'
cmd = _gateway_device.GatewayDeviceShow(
test_cli20.MyApp(sys.stdout), None)
args = ['--fields', 'id', '--fields', 'name', self.test_id]
self._test_show_resource(resource, cmd, self.test_id, args,
['id', 'name'])

View File

@@ -0,0 +1,115 @@
# Copyright (C) 2016 Midokura SARL
# All Rights Reserved.
#
# 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.
import sys
from midonet.neutron.tests.unit.neutronclient_ext import test_cli20
from midonet.neutronclient.gateway_device_extension import _remote_mac_entry
from neutronclient import shell
class CLITestV20RemoteMacEntryJSON(test_cli20.CLIExtTestV20Base):
def setUp(self):
remote_mac_entry = ("remote_mac_entry", _remote_mac_entry)
self._mock_load_extensions(remote_mac_entry)
super(CLITestV20RemoteMacEntryJSON,
self).setUp(plurals={'remote_mac_entries': 'remote_mac_entry'})
self.register_non_admin_status_resource('remote_mac_entry')
def test_remote_mac_entry_cmd_loaded(self):
shell.NeutronShell('2.0')
remote_mac_entry_cmd = {'gateway-device-remote-mac-entry-list':
_remote_mac_entry.RemoteMacEntryList,
'gateway-device-remote-mac-entry-create':
_remote_mac_entry.RemoteMacEntryCreate,
'gateway-device-remote-mac-entry-delete':
_remote_mac_entry.RemoteMacEntryDelete,
'gateway-device-remote-mac-entry-show':
_remote_mac_entry.RemoteMacEntryShow}
self.assertDictContainsSubset(remote_mac_entry_cmd,
shell.COMMANDS['2.0'])
def _create_remote_mac_entry(self, args, position_names,
position_values, parent_id=None):
resource = 'remote_mac_entry'
cmd = _remote_mac_entry.RemoteMacEntryCreate(
test_cli20.MyApp(sys.stdout), None)
self._test_create_resource(resource, cmd, None, 'myid',
args, position_names, position_values,
parent_id=parent_id)
def test_create_remote_mac_entry(self):
gw_device_id = 'my_gw_device'
mac_addr = 'fa:16:3e:db:79:80'
vtep_addr = '192.168.100.1'
seg_id = '200'
args = ['--mac-address', mac_addr, '--vtep-address', vtep_addr,
'--segmentation-id', seg_id, gw_device_id]
position_names = ['mac_address', 'vtep_address', 'segmentation_id']
position_values = [mac_addr, vtep_addr, seg_id]
self._create_remote_mac_entry(args, position_names,
position_values, parent_id=gw_device_id)
def test_create_remote_mac_entry_with_missing_gw_device_id(self):
mac_addr = ''
vtep_addr = ''
seg_id = '200'
args = ['--mac-address', mac_addr,
'--vtep-address', vtep_addr, '--segmentation-id', seg_id]
position_names = []
position_values = []
self.assertRaises(SystemExit, self._create_remote_mac_entry,
args, position_names, position_values)
def test_create_remote_mac_entry_with_missing_seg_id(self):
gw_device_id = 'my_gw_device'
mac_addr = ''
vtep_addr = ''
args = ['--mac-address', mac_addr, '--vtep-address', vtep_addr,
gw_device_id]
position_names = []
position_values = []
self.assertRaises(SystemExit, self._create_remote_mac_entry,
args, position_names, position_values,
parent_id=gw_device_id)
def test_delete_remote_mac_entry(self):
resource = 'remote_mac_entry'
cmd = _remote_mac_entry.RemoteMacEntryDelete(
test_cli20.MyApp(sys.stdout), None)
gw_device_id = 'my_gw_device'
my_id = 'myid'
args = [my_id, gw_device_id]
self._test_delete_ext_resource(resource, cmd, my_id, args,
parent_id=gw_device_id)
def test_list_remote_mac_entries(self):
resources = 'remote_mac_entries'
cmd = _remote_mac_entry.RemoteMacEntryList(
test_cli20.MyApp(sys.stdout), None)
gw_device_id = 'my_gw_device'
args = [gw_device_id]
self._test_list_resources(resources, cmd, base_args=args,
parent_id=gw_device_id)
def test_show_remote_mac_entry(self):
resource = 'remote_mac_entry'
cmd = _remote_mac_entry.RemoteMacEntryShow(
test_cli20.MyApp(sys.stdout), None)
gw_device_id = 'my_gw_device'
my_id = 'myid'
args = [my_id, gw_device_id]
self._test_show_ext_resource(resource, cmd, my_id, args,
parent_id=gw_device_id)

View File

View File

@@ -0,0 +1,133 @@
# Copyright (C) 2016 Midokura SARL
# All Rights Reserved.
#
# 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.
#
from midonet.neutron._i18n import _
from neutronclient.common import extension
from neutronclient.neutron import v2_0 as gw_deviceV20
from oslo_serialization import jsonutils
def add_name_and_tunnel_ips_to_arguments(parser):
parser.add_argument(
'--name', dest='name',
help=_('User defined device name.'))
parser.add_argument(
'--tunnel-ip', metavar='TUNNEL_IP',
action='append', dest='tunnel_ips',
help=_('IP address on which gateway device originates or '
'terminates tunnel.'))
def _format_remote_mac_entries(gw_device):
try:
return '\n'.join([jsonutils.dumps(rm_entry) for rm_entry
in gw_device['remote_mac_entries']])
except (TypeError, KeyError):
return ''
class GatewayDevice(extension.NeutronClientExtension):
resource = 'gateway_device'
resource_plural = 'gateway_devices'
path = 'gw/gateway_devices'
object_path = '/%s' % path
resource_path = '/%s/%%s' % path
versions = ['2.0']
class GatewayDeviceCreate(extension.ClientExtensionCreate, GatewayDevice):
"""Create Gateway Device information."""
shell_command = 'gateway-device-create'
def add_known_arguments(self, parser):
parser.add_argument(
'--management-ip',
dest='management_ip',
help=_('Management IP to the device. Defaults to None.'))
parser.add_argument(
'--management-port',
dest='management_port',
help=_('Management port to the device. Defaults to None.'))
parser.add_argument(
'--management-protocol',
dest='management_protocol',
help=_('Management protocol to manage the device: ovsdb or none. '
'If management ip and port are specified, '
'defaults to ovsdb. Otherwise to none.'))
parser.add_argument(
'--type',
metavar='<hw_vtep | router_vtep>',
choices=['hw_vtep', 'router_vtep'],
help=_('Type of the device: hw_vtep or router_vtep. '
'Defaults to hw_vtep'))
parser.add_argument(
'--resource-id',
dest='resource_id',
help=_('Resource UUID or None (for type router_vtep will '
'be router UUID)'))
add_name_and_tunnel_ips_to_arguments(parser)
return parser
def args2body(self, args):
body = {}
attributes = ['name', 'type', 'management_ip',
'management_port', 'management_protocol',
'resource_id', 'tenant_id', 'tunnel_ips']
gw_deviceV20.update_dict(args, body, attributes)
return {'gateway_device': body}
class GatewayDeviceList(extension.ClientExtensionList, GatewayDevice):
"""List Gateway Devices."""
shell_command = 'gateway-device-list'
list_columns = ['id', 'name', 'type', 'resource_id', 'tunnel_ips']
pagination_support = True
sorting_support = True
_formatters = {'remote_mac_entries': _format_remote_mac_entries, }
class GatewayDeviceShow(extension.ClientExtensionShow, GatewayDevice):
"""Show information of a given gateway-device."""
shell_command = 'gateway-device-show'
class GatewayDeviceDelete(extension.ClientExtensionDelete, GatewayDevice):
"""Delete a given gateway-device."""
shell_command = 'gateway-device-delete'
class GatewayDeviceUpdate(extension.ClientExtensionUpdate, GatewayDevice):
"""Update a given gateway-device."""
shell_command = 'gateway-device-update'
def add_known_arguments(self, parser):
add_name_and_tunnel_ips_to_arguments(parser)
def args2body(self, args):
body = {}
attributes = ['name', 'tunnel_ips']
gw_deviceV20.update_dict(args, body, attributes)
return {'gateway_device': body}

View File

@@ -0,0 +1,93 @@
# Copyright (C) 2016 Midokura SARL
# All Rights Reserved.
#
# 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.
from neutronclient.common import extension
from neutronclient.i18n import _
from neutronclient.neutron import v2_0 as gw_deviceV20
def _get_gateway_device_id(client, gw_device_id_or_name):
return gw_deviceV20.find_resourceid_by_name_or_id(client, 'gateway_device',
gw_device_id_or_name)
class RemoteMacEntry(extension.NeutronClientExtension):
parent_resource = 'gateway_devices'
resource = 'remote_mac_entry'
resource_plural = 'remote_mac_entries'
object_path = '/gw/%s/%%s/%s' % (parent_resource, resource_plural)
resource_path = '/gw/%s/%%s/%s/%%%%s' % (parent_resource, resource_plural)
versions = ['2.0']
def add_known_arguments(self, parser):
parser.add_argument(
'gateway_device', metavar='GATEWAY_DEVICE',
help=_('ID of the gateway device.'))
def set_extra_attrs(self, parsed_args):
self.parent_id = _get_gateway_device_id(self.get_client(),
parsed_args.gateway_device)
class RemoteMacEntryCreate(extension.ClientExtensionCreate, RemoteMacEntry):
"""Create Gateway Device Remote Mac Entry information."""
shell_command = 'gateway-device-remote-mac-entry-create'
def get_parser(self, parser):
parser = super(gw_deviceV20.CreateCommand, self).get_parser(parser)
parser.add_argument(
'--mac-address', dest='mac_address',
required=True,
help=_('Remote MAC address'))
parser.add_argument(
'--vtep-address', dest='vtep_address',
required=True,
help=_('Remote VTEP Tunnel IP'))
parser.add_argument(
'--segmentation-id', dest='segmentation_id',
required=True,
help=_('VNI to be used'))
self.add_known_arguments(parser)
return parser
def args2body(self, args):
body = {}
attributes = ['mac_address', 'vtep_address', 'segmentation_id']
gw_deviceV20.update_dict(args, body, attributes)
return {'remote_mac_entry': body}
class RemoteMacEntryList(extension.ClientExtensionList, RemoteMacEntry):
"""List Gateway Device Remote Mac Entries."""
shell_command = 'gateway-device-remote-mac-entry-list'
list_columns = ['id', 'mac_address', 'vtep_address', 'segmentation_id']
pagination_support = True
sorting_support = True
class RemoteMacEntryShow(extension.ClientExtensionShow, RemoteMacEntry):
"""Show information of a given gateway-device-remote-mac-entry."""
shell_command = 'gateway-device-remote-mac-entry-show'
allow_names = False
class RemoteMacEntryDelete(extension.ClientExtensionDelete, RemoteMacEntry):
"""Delete a given gateway-device-remote-mac-entry."""
shell_command = 'gateway-device-remote-mac-entry-delete'
allow_names = False

View File

@@ -70,6 +70,9 @@ neutron.interface_drivers =
oslo.config.opts =
midonet_v1 = midonet.neutron.plugin_v1:list_opts
midonet_v2 = midonet.neutron.common.config:list_opts
neutronclient.extension =
gateway_device = midonet.neutronclient.gateway_device_extension._gateway_device
remote_mac_entry = midonet.neutronclient.gateway_device_extension._remote_mac_entry
[wheel]
universal = 1

View File

@@ -34,7 +34,9 @@ commands = python -m testtools.run \
midonet.neutron.tests.unit.test_midonet_plugin_ml2 \
midonet.neutron.tests.unit.test_midonet_plugin_v2 \
midonet.neutron.tests.unit.test_midonet_type_driver \
midonet.neutron.tests.unit.test_uplink_type_driver}
midonet.neutron.tests.unit.test_uplink_type_driver \
midonet.neutron.tests.unit.neutronclient_ext.test_cli20_gateway_device \
midonet.neutron.tests.unit.neutronclient_ext.test_cli20_remote_mac_entry}
[testenv:cover]
basepython = python2.7