Add support for Neutron's L3 conntrack helper resource

Neutron has got CRUD API for L3 conntrack helper since some time.
This patch adds support for it in the OSC.
OpenStack SDK supports that since [1]

This patch also bumps minimum OpenStack SDK version to
the 0.56.0 as that version introduced support for the
Neutron's L3 conntrack helper.

[1] https://review.opendev.org/c/openstack/openstacksdk/+/782870

Change-Id: I55604182ae50b6ad70c8bc1f7efad8859f191269
This commit is contained in:
Slawek Kaplonski 2021-04-07 23:40:16 +02:00
parent 82fcf1dbe5
commit fa8c8d26a7
8 changed files with 814 additions and 0 deletions

View File

@ -106,6 +106,7 @@
q-metering: true
q-qos: true
neutron-tag-ports-during-bulk-creation: true
neutron-conntrack-helper: true
devstack_localrc:
Q_AGENT: openvswitch
Q_ML2_TENANT_NETWORK_TYPE: vxlan

View File

@ -0,0 +1,8 @@
===========================
network l3 conntrack helper
===========================
Network v2
.. autoprogram-cliff:: openstack.network.v2
:command: network l3 conntrack helper *

View File

@ -0,0 +1,255 @@
# 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.
#
"""L3 Conntrack Helper action implementations"""
import logging
from osc_lib.command import command
from osc_lib import exceptions
from osc_lib import utils
from openstackclient.i18n import _
from openstackclient.network import sdk_utils
LOG = logging.getLogger(__name__)
def _get_columns(item):
column_map = {}
return sdk_utils.get_osc_show_columns_for_sdk_resource(item, column_map)
def _get_attrs(client, parsed_args):
router = client.find_router(parsed_args.router, ignore_missing=False)
attrs = {'router_id': router.id}
if parsed_args.helper:
attrs['helper'] = parsed_args.helper
if parsed_args.protocol:
attrs['protocol'] = parsed_args.protocol
if parsed_args.port:
attrs['port'] = parsed_args.port
return attrs
class CreateConntrackHelper(command.ShowOne):
_description = _("Create a new L3 conntrack helper")
def get_parser(self, prog_name):
parser = super(CreateConntrackHelper, self).get_parser(prog_name)
parser.add_argument(
'router',
metavar='<router>',
help=_('Router for which conntrack helper will be created')
)
parser.add_argument(
'--helper',
required=True,
metavar='<helper>',
help=_('The netfilter conntrack helper module')
)
parser.add_argument(
'--protocol',
required=True,
metavar='<protocol>',
help=_('The network protocol for the netfilter conntrack target '
'rule')
)
parser.add_argument(
'--port',
required=True,
metavar='<port>',
type=int,
help=_('The network port for the netfilter conntrack target rule')
)
return parser
def take_action(self, parsed_args):
client = self.app.client_manager.network
attrs = _get_attrs(client, parsed_args)
obj = client.create_conntrack_helper(attrs.pop('router_id'), **attrs)
display_columns, columns = _get_columns(obj)
data = utils.get_item_properties(obj, columns, formatters={})
return (display_columns, data)
class DeleteConntrackHelper(command.Command):
_description = _("Delete L3 conntrack helper")
def get_parser(self, prog_name):
parser = super(DeleteConntrackHelper, self).get_parser(prog_name)
parser.add_argument(
'router',
metavar='<router>',
help=_('Router that the conntrack helper belong to')
)
parser.add_argument(
'conntrack_helper_ids',
metavar='<conntrack-helper-ids>',
nargs='+',
help=_('The ID of the conntrack helper(s) to delete')
)
return parser
def take_action(self, parsed_args):
client = self.app.client_manager.network
result = 0
router = client.find_router(parsed_args.router, ignore_missing=False)
for ct_helper in parsed_args.conntrack_helper_ids:
try:
client.delete_conntrack_helper(
ct_helper, router.id, ignore_missing=False)
except Exception as e:
result += 1
LOG.error(_("Failed to delete L3 conntrack helper with "
"ID '%(ct_helper)s': %(e)s"),
{'ct_helper': ct_helper, 'e': e})
if result > 0:
total = len(parsed_args.conntrack_helper_ids)
msg = (_("%(result)s of %(total)s L3 conntrack helpers failed "
"to delete.") % {'result': result, 'total': total})
raise exceptions.CommandError(msg)
class ListConntrackHelper(command.Lister):
_description = _("List L3 conntrack helpers")
def get_parser(self, prog_name):
parser = super(ListConntrackHelper, self).get_parser(prog_name)
parser.add_argument(
'router',
metavar='<router>',
help=_('Router that the conntrack helper belong to')
)
parser.add_argument(
'--helper',
metavar='<helper>',
help=_('The netfilter conntrack helper module')
)
parser.add_argument(
'--protocol',
metavar='<protocol>',
help=_('The network protocol for the netfilter conntrack target '
'rule')
)
parser.add_argument(
'--port',
metavar='<port>',
help=_('The network port for the netfilter conntrack target rule')
)
return parser
def take_action(self, parsed_args):
client = self.app.client_manager.network
columns = (
'id',
'router_id',
'helper',
'protocol',
'port',
)
column_headers = (
'ID',
'Router ID',
'Helper',
'Protocol',
'Port',
)
attrs = _get_attrs(client, parsed_args)
data = client.conntrack_helpers(attrs.pop('router_id'), **attrs)
return (column_headers,
(utils.get_item_properties(
s, columns, formatters={},
) for s in data))
class SetConntrackHelper(command.Command):
_description = _("Set L3 conntrack helper properties")
def get_parser(self, prog_name):
parser = super(SetConntrackHelper, self).get_parser(prog_name)
parser.add_argument(
'router',
metavar='<router>',
help=_('Router that the conntrack helper belong to')
)
parser.add_argument(
'conntrack_helper_id',
metavar='<conntrack-helper-id>',
help=_('The ID of the conntrack helper(s)')
)
parser.add_argument(
'--helper',
metavar='<helper>',
help=_('The netfilter conntrack helper module')
)
parser.add_argument(
'--protocol',
metavar='<protocol>',
help=_('The network protocol for the netfilter conntrack target '
'rule')
)
parser.add_argument(
'--port',
metavar='<port>',
type=int,
help=_('The network port for the netfilter conntrack target rule')
)
return parser
def take_action(self, parsed_args):
client = self.app.client_manager.network
attrs = _get_attrs(client, parsed_args)
if attrs:
client.update_conntrack_helper(
parsed_args.conntrack_helper_id, attrs.pop('router_id'),
**attrs)
class ShowConntrackHelper(command.ShowOne):
_description = _("Display L3 conntrack helper details")
def get_parser(self, prog_name):
parser = super(ShowConntrackHelper, self).get_parser(prog_name)
parser.add_argument(
'router',
metavar='<router>',
help=_('Router that the conntrack helper belong to')
)
parser.add_argument(
'conntrack_helper_id',
metavar='<conntrack-helper-id>',
help=_('The ID of the conntrack helper')
)
return parser
def take_action(self, parsed_args):
client = self.app.client_manager.network
router = client.find_router(parsed_args.router, ignore_missing=False)
obj = client.get_conntrack_helper(
parsed_args.conntrack_helper_id, router.id)
display_columns, columns = _get_columns(obj)
data = utils.get_item_properties(obj, columns, formatters={})
return (display_columns, data)

View File

@ -0,0 +1,145 @@
# 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 json
import uuid
from openstackclient.tests.functional.network.v2 import common
class L3ConntrackHelperTests(common.NetworkTests):
def setUp(self):
super(L3ConntrackHelperTests, self).setUp()
# Nothing in this class works with Nova Network
if not self.haz_network:
self.skipTest("No Network service present")
if not self.is_extension_enabled('l3-conntrack-helper'):
self.skipTest("No l3-conntrack-helper extension present")
if not self.is_extension_enabled('expose-l3-conntrack-helper'):
self.skipTest("No expose-l3-conntrack-helper extension present")
def _create_router(self):
router_name = uuid.uuid4().hex
json_output = json.loads(self.openstack(
'router create -f json ' + router_name
))
self.assertIsNotNone(json_output['id'])
router_id = json_output['id']
self.addCleanup(self.openstack, 'router delete ' + router_id)
return router_id
def _create_helpers(self, router_id, helpers):
created_helpers = []
for helper in helpers:
output = json.loads(self.openstack(
'network l3 conntrack helper create %(router)s '
'--helper %(helper)s --protocol %(protocol)s --port %(port)s '
'-f json' % {'router': router_id,
'helper': helper['helper'],
'protocol': helper['protocol'],
'port': helper['port']}))
self.assertEqual(helper['helper'], output['helper'])
self.assertEqual(helper['protocol'], output['protocol'])
self.assertEqual(helper['port'], output['port'])
created_helpers.append(output)
return created_helpers
def test_l3_conntrack_helper_create_and_delete(self):
"""Test create, delete multiple"""
helpers = [
{
'helper': 'tftp',
'protocol': 'udp',
'port': 69
}, {
'helper': 'ftp',
'protocol': 'tcp',
'port': 21
}
]
router_id = self._create_router()
created_helpers = self._create_helpers(router_id, helpers)
ct_ids = " ".join([ct['id'] for ct in created_helpers])
raw_output = self.openstack(
'--debug network l3 conntrack helper delete %(router)s '
'%(ct_ids)s' % {
'router': router_id, 'ct_ids': ct_ids})
self.assertOutput('', raw_output)
def test_l3_conntrack_helper_list(self):
helpers = [
{
'helper': 'tftp',
'protocol': 'udp',
'port': 69
}, {
'helper': 'ftp',
'protocol': 'tcp',
'port': 21
}
]
expected_helpers = [
{
'Helper': 'tftp',
'Protocol': 'udp',
'Port': 69
}, {
'Helper': 'ftp',
'Protocol': 'tcp',
'Port': 21
}
]
router_id = self._create_router()
self._create_helpers(router_id, helpers)
output = json.loads(self.openstack(
'network l3 conntrack helper list %s -f json ' % router_id
))
for ct in output:
self.assertEqual(router_id, ct.pop('Router ID'))
ct.pop("ID")
self.assertIn(ct, expected_helpers)
def test_l3_conntrack_helper_set_and_show(self):
helper = {
'helper': 'tftp',
'protocol': 'udp',
'port': 69}
router_id = self._create_router()
created_helper = self._create_helpers(router_id, [helper])[0]
output = json.loads(self.openstack(
'network l3 conntrack helper show %(router_id)s %(ct_id)s '
'-f json' % {
'router_id': router_id, 'ct_id': created_helper['id']}))
self.assertEqual(helper['helper'], output['helper'])
self.assertEqual(helper['protocol'], output['protocol'])
self.assertEqual(helper['port'], output['port'])
raw_output = self.openstack(
'network l3 conntrack helper set %(router_id)s %(ct_id)s '
'--port %(port)s ' % {
'router_id': router_id,
'ct_id': created_helper['id'],
'port': helper['port'] + 1})
self.assertOutput('', raw_output)
output = json.loads(self.openstack(
'network l3 conntrack helper show %(router_id)s %(ct_id)s '
'-f json' % {
'router_id': router_id, 'ct_id': created_helper['id']}))
self.assertEqual(helper['port'] + 1, output['port'])
self.assertEqual(helper['helper'], output['helper'])
self.assertEqual(helper['protocol'], output['protocol'])

View File

@ -1973,3 +1973,78 @@ class FakeFloatingIPPortForwarding(object):
)
return mock.Mock(side_effect=port_forwardings)
class FakeL3ConntrackHelper(object):
""""Fake one or more L3 conntrack helper"""
@staticmethod
def create_one_l3_conntrack_helper(attrs=None):
"""Create a fake L3 conntrack helper.
:param Dictionary attrs:
A dictionary with all attributes
:return:
A FakeResource object with protocol, port, etc.
"""
attrs = attrs or {}
router_id = (
attrs.get('router_id') or 'router-id-' + uuid.uuid4().hex
)
# Set default attributes.
ct_attrs = {
'id': uuid.uuid4().hex,
'router_id': router_id,
'helper': 'tftp',
'protocol': 'tcp',
'port': randint(1, 65535),
}
# Overwrite default attributes.
ct_attrs.update(attrs)
ct = fakes.FakeResource(
info=copy.deepcopy(ct_attrs),
loaded=True
)
return ct
@staticmethod
def create_l3_conntrack_helpers(attrs=None, count=2):
"""Create multiple fake L3 Conntrack helpers.
:param Dictionary attrs:
A dictionary with all attributes
:param int count:
The number of L3 Conntrack helper rule to fake
:return:
A list of FakeResource objects faking the Conntrack helpers
"""
ct_helpers = []
for i in range(0, count):
ct_helpers.append(
FakeL3ConntrackHelper.create_one_l3_conntrack_helper(attrs)
)
return ct_helpers
@staticmethod
def get_l3_conntrack_helpers(ct_helpers=None, count=2):
"""Get a list of faked L3 Conntrack helpers.
If ct_helpers list is provided, then initialize the Mock object
with the list. Otherwise create one.
:param List ct_helpers:
A list of FakeResource objects faking conntrack helpers
:param int count:
The number of L3 conntrack helpers to fake
:return:
An iterable Mock object with side_effect set to a list of faked
L3 conntrack helpers
"""
if ct_helpers is None:
ct_helpers = (
FakeL3ConntrackHelper.create_l3_conntrack_helpers(count)
)
return mock.Mock(side_effect=ct_helpers)

View File

@ -0,0 +1,316 @@
# 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 unittest import mock
from osc_lib import exceptions
from openstackclient.network.v2 import l3_conntrack_helper
from openstackclient.tests.unit.network.v2 import fakes as network_fakes
from openstackclient.tests.unit import utils as tests_utils
class TestConntrackHelper(network_fakes.TestNetworkV2):
def setUp(self):
super(TestConntrackHelper, self).setUp()
# Get a shortcut to the network client
self.network = self.app.client_manager.network
self.router = network_fakes.FakeRouter.create_one_router()
self.network.find_router = mock.Mock(return_value=self.router)
class TestCreateL3ConntrackHelper(TestConntrackHelper):
def setUp(self):
super(TestCreateL3ConntrackHelper, self).setUp()
attrs = {'router_id': self.router.id}
self.ct_helper = (
network_fakes.FakeL3ConntrackHelper.create_one_l3_conntrack_helper(
attrs))
self.columns = (
'helper',
'id',
'port',
'protocol',
'router_id'
)
self.data = (
self.ct_helper.helper,
self.ct_helper.id,
self.ct_helper.port,
self.ct_helper.protocol,
self.ct_helper.router_id
)
self.network.create_conntrack_helper = mock.Mock(
return_value=self.ct_helper)
# Get the command object to test
self.cmd = l3_conntrack_helper.CreateConntrackHelper(self.app,
self.namespace)
def test_create_no_options(self):
arglist = []
verifylist = []
# Missing required args should bail here
self.assertRaises(tests_utils.ParserException, self.check_parser,
self.cmd, arglist, verifylist)
def test_create_default_options(self):
arglist = [
'--helper', 'tftp',
'--protocol', 'udp',
'--port', '69',
self.router.id,
]
verifylist = [
('helper', 'tftp'),
('protocol', 'udp'),
('port', 69),
]
parsed_args = self.check_parser(self.cmd, arglist, verifylist)
columns, data = (self.cmd.take_action(parsed_args))
self.network.create_conntrack_helper.assert_called_once_with(
self.router.id,
**{'helper': 'tftp', 'protocol': 'udp',
'port': 69}
)
self.assertEqual(self.columns, columns)
self.assertEqual(self.data, data)
def test_create_wrong_options(self):
arglist = [
'--protocol', 'udp',
'--port', '69',
self.router.id,
]
self.assertRaises(
tests_utils.ParserException,
self.check_parser,
self.cmd, arglist, None)
class TestDeleteL3ConntrackHelper(TestConntrackHelper):
def setUp(self):
super(TestDeleteL3ConntrackHelper, self).setUp()
attrs = {'router_id': self.router.id}
self.ct_helper = (
network_fakes.FakeL3ConntrackHelper.create_one_l3_conntrack_helper(
attrs))
self.network.delete_conntrack_helper = mock.Mock(
return_value=None)
# Get the command object to test
self.cmd = l3_conntrack_helper.DeleteConntrackHelper(self.app,
self.namespace)
def test_delete(self):
arglist = [
self.ct_helper.router_id,
self.ct_helper.id
]
verifylist = [
('conntrack_helper_ids', [self.ct_helper.id]),
('router', self.ct_helper.router_id),
]
parsed_args = self.check_parser(self.cmd, arglist, verifylist)
result = self.cmd.take_action(parsed_args)
self.network.delete_conntrack_helper.assert_called_once_with(
self.ct_helper.id, self.router.id,
ignore_missing=False)
self.assertIsNone(result)
def test_delete_error(self):
arglist = [
self.router.id,
self.ct_helper.id
]
verifylist = [
('conntrack_helper_ids', [self.ct_helper.id]),
('router', self.router.id),
]
self.network.delete_conntrack_helper.side_effect = Exception(
'Error message')
parsed_args = self.check_parser(self.cmd, arglist, verifylist)
self.assertRaises(
exceptions.CommandError,
self.cmd.take_action, parsed_args)
class TestListL3ConntrackHelper(TestConntrackHelper):
def setUp(self):
super(TestListL3ConntrackHelper, self).setUp()
attrs = {'router_id': self.router.id}
ct_helpers = (
network_fakes.FakeL3ConntrackHelper.create_l3_conntrack_helpers(
attrs, count=3))
self.columns = (
'ID',
'Router ID',
'Helper',
'Protocol',
'Port',
)
self.data = []
for ct_helper in ct_helpers:
self.data.append((
ct_helper.id,
ct_helper.router_id,
ct_helper.helper,
ct_helper.protocol,
ct_helper.port,
))
self.network.conntrack_helpers = mock.Mock(
return_value=ct_helpers)
# Get the command object to test
self.cmd = l3_conntrack_helper.ListConntrackHelper(self.app,
self.namespace)
def test_conntrack_helpers_list(self):
arglist = [
self.router.id
]
verifylist = [
('router', self.router.id),
]
parsed_args = self.check_parser(self.cmd, arglist, verifylist)
columns, data = self.cmd.take_action(parsed_args)
self.network.conntrack_helpers.assert_called_once_with(
self.router.id)
self.assertEqual(self.columns, columns)
list_data = list(data)
self.assertEqual(len(self.data), len(list_data))
for index in range(len(list_data)):
self.assertEqual(self.data[index], list_data[index])
class TestSetL3ConntrackHelper(TestConntrackHelper):
def setUp(self):
super(TestSetL3ConntrackHelper, self).setUp()
attrs = {'router_id': self.router.id}
self.ct_helper = (
network_fakes.FakeL3ConntrackHelper.create_one_l3_conntrack_helper(
attrs))
self.network.update_conntrack_helper = mock.Mock(return_value=None)
# Get the command object to test
self.cmd = l3_conntrack_helper.SetConntrackHelper(self.app,
self.namespace)
def test_set_nothing(self):
arglist = [
self.router.id,
self.ct_helper.id,
]
verifylist = [
('router', self.router.id),
('conntrack_helper_id', self.ct_helper.id),
]
parsed_args = self.check_parser(self.cmd, arglist, verifylist)
result = (self.cmd.take_action(parsed_args))
self.network.update_conntrack_helper.assert_called_once_with(
self.ct_helper.id, self.router.id
)
self.assertIsNone(result)
def test_set_port(self):
arglist = [
self.router.id,
self.ct_helper.id,
'--port', '124',
]
verifylist = [
('router', self.router.id),
('conntrack_helper_id', self.ct_helper.id),
('port', 124),
]
parsed_args = self.check_parser(self.cmd, arglist, verifylist)
result = (self.cmd.take_action(parsed_args))
self.network.update_conntrack_helper.assert_called_once_with(
self.ct_helper.id, self.router.id, port=124
)
self.assertIsNone(result)
class TestShowL3ConntrackHelper(TestConntrackHelper):
def setUp(self):
super(TestShowL3ConntrackHelper, self).setUp()
attrs = {'router_id': self.router.id}
self.ct_helper = (
network_fakes.FakeL3ConntrackHelper.create_one_l3_conntrack_helper(
attrs))
self.columns = (
'helper',
'id',
'port',
'protocol',
'router_id'
)
self.data = (
self.ct_helper.helper,
self.ct_helper.id,
self.ct_helper.port,
self.ct_helper.protocol,
self.ct_helper.router_id
)
self.network.get_conntrack_helper = mock.Mock(
return_value=self.ct_helper)
# Get the command object to test
self.cmd = l3_conntrack_helper.ShowConntrackHelper(self.app,
self.namespace)
def test_show_no_options(self):
arglist = []
verifylist = []
# Missing required args should bail here
self.assertRaises(tests_utils.ParserException, self.check_parser,
self.cmd, arglist, verifylist)
def test_show_default_options(self):
arglist = [
self.router.id,
self.ct_helper.id,
]
verifylist = [
('router', self.router.id),
('conntrack_helper_id', self.ct_helper.id),
]
parsed_args = self.check_parser(self.cmd, arglist, verifylist)
columns, data = (self.cmd.take_action(parsed_args))
self.network.get_conntrack_helper.assert_called_once_with(
self.ct_helper.id, self.router.id
)
self.assertEqual(self.columns, columns)
self.assertEqual(self.data, data)

View File

@ -0,0 +1,8 @@
---
features:
- |
Add new commands ``network l3 conntrack helper create``,
``network l3 conntrack helper set``, ``network l3 conntrack helper show``,
``network l3 conntrack helper set`` and
``network l3 conntrack helper delete`` to support Neutron L3 conntrack
helper CRUD operations.

View File

@ -448,6 +448,12 @@ openstack.network.v2 =
network_show = openstackclient.network.v2.network:ShowNetwork
network_unset = openstackclient.network.v2.network:UnsetNetwork
network_l3_conntrack_helper_create = openstackclient.network.v2.l3_conntrack_helper:CreateConntrackHelper
network_l3_conntrack_helper_delete = openstackclient.network.v2.l3_conntrack_helper:DeleteConntrackHelper
network_l3_conntrack_helper_list = openstackclient.network.v2.l3_conntrack_helper:ListConntrackHelper
network_l3_conntrack_helper_set = openstackclient.network.v2.l3_conntrack_helper:SetConntrackHelper
network_l3_conntrack_helper_show = openstackclient.network.v2.l3_conntrack_helper:ShowConntrackHelper
network_meter_create = openstackclient.network.v2.network_meter:CreateMeter
network_meter_delete = openstackclient.network.v2.network_meter:DeleteMeter
network_meter_list = openstackclient.network.v2.network_meter:ListMeter