diff --git a/quark/db/api.py b/quark/db/api.py index 9d54a71..7f6e622 100644 --- a/quark/db/api.py +++ b/quark/db/api.py @@ -31,6 +31,7 @@ from sqlalchemy.orm import class_mapper from quark.db import models from quark.db import sqlalchemy_adapter as quark_sa from quark import network_strategy +from quark import port_vlan_id from quark import protocols @@ -226,11 +227,12 @@ def port_count_all(context, **filters): def port_create(context, **port_dict): - port = models.Port() + port = models.Port(tags=[]) port.update(port_dict) port["tenant_id"] = context.tenant_id if "addresses" in port_dict: port["ip_addresses"].extend(port_dict["addresses"]) + _port_store_vlan_id(port, **port_dict) context.session.add(port) return port @@ -279,9 +281,20 @@ def update_port_associations_for_ip(context, ports, address): assoc_ports - new_ports, new_address) +def _port_store_vlan_id(port, **kwargs): + if "vlan_id" in kwargs: + try: + port_vlan_id.store_vlan_id(port, kwargs.pop("vlan_id")) + except Exception as e: + LOG.error("Exception occurred while trying to store VLAN ID on " + "port '%(port_id)d': %(message)s", + {'port_id': port.id, 'message': e.message}) + + def port_update(context, port, **kwargs): if "addresses" in kwargs: port["ip_addresses"] = kwargs.pop("addresses") + _port_store_vlan_id(port, **kwargs) port.update(kwargs) context.session.add(port) return port diff --git a/quark/db/models.py b/quark/db/models.py index ad250e2..8e9acfb 100644 --- a/quark/db/models.py +++ b/quark/db/models.py @@ -412,7 +412,7 @@ class SecurityGroup(BASEV2, models.HasId): tenant_id = sa.Column(sa.String(255), index=True) -class Port(BASEV2, models.HasTenant, models.HasId): +class Port(BASEV2, models.HasTenant, models.HasId, IsHazTags): __tablename__ = "quark_ports" id = sa.Column(sa.String(36), primary_key=True) name = sa.Column(sa.String(255), index=True) diff --git a/quark/plugin_views.py b/quark/plugin_views.py index aa4b736..ab11d9c 100644 --- a/quark/plugin_views.py +++ b/quark/plugin_views.py @@ -23,6 +23,7 @@ from oslo_log import log as logging from quark.db import ip_types from quark import network_strategy +from quark import port_vlan_id from quark import protocols @@ -179,6 +180,11 @@ def _port_dict(port, fields=None): # NOTE(mdietz): more pythonic key in dict check fails here. Leave as get if port.get("bridge"): res["bridge"] = port["bridge"] + + vlan_id = port_vlan_id.retrieve_vlan_id(port) + if vlan_id: + res["vlan_id"] = vlan_id + return res diff --git a/quark/port_vlan_id.py b/quark/port_vlan_id.py new file mode 100644 index 0000000..78ad549 --- /dev/null +++ b/quark/port_vlan_id.py @@ -0,0 +1,121 @@ +# Copyright 2015 Rackspace +# 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. + +VLAN_TAG_PREFIX = "VLAN_ID:" +MIN_VLAN_ID = 1 +MAX_VLAN_ID = 4096 + + +class InvalidVlanIdError(Exception): + """Raised if an invalid VLAN ID is detected.""" + def __init__(self, vlan_id): + self.vlan_id = vlan_id + self.message = ("Invalid VLAN ID detected. Got '%(vlan_id)s'. " + "Integer conversion yields: '%(vlan_id_int)d'. " + "VLAN ID should be between %(min)d and %(max)d " + "inclusive." % {'vlan_id': vlan_id, + 'vlan_id_int': int(vlan_id), + 'min': MIN_VLAN_ID, + 'max': MAX_VLAN_ID}) + + +def _validate_vlan_id(vlan_id): + """Validates a VLAN ID. + + :param vlan_id: The VLAN ID to validate against. + :raises InvalidVlanIdError: Raised if the VLAN ID is invalid. + """ + vlan_id_int = int(vlan_id) + if vlan_id_int < MIN_VLAN_ID or vlan_id_int > MAX_VLAN_ID: + raise InvalidVlanIdError(vlan_id) + + +def _build_vlan_tag_string(vlan_id): + """Builds a VLAN ID tag string. + + :param vlan_id: The VLAN ID as a string. + :returns: The VLAN ID string as appropriate for a port tag. + """ + return "%s%d" % (VLAN_TAG_PREFIX, int(vlan_id)) + + +def store_vlan_id(port, vlan_id): + """Stores a VLAN ID on a specified port. + + :param port: The port object on which to store the VLAN ID. + :param vlan_id: The VLAN ID as a string. + + :raises InvalidVlanIdError: If the vlan_id is invalid, this exception + is raised. + """ + _validate_vlan_id(vlan_id) + port.tags.append(_build_vlan_tag_string(vlan_id)) + + +def retrieve_vlan_id(port): + """Retrieves the VLAN ID associated with the given port, if it exists. + + :param port: The port object. + :returns: The VLAN ID as an integer, if the port has one attached. + Otherwise returns None. + + :raises InvalidVlanIdError: This exception is raised if the retrieved + VLAN ID is invalid. + """ + for tag in port.tags: + if is_vlan_id_tag(tag): + vlan_id = _extract_vlan_id_from_tag(tag) + _validate_vlan_id(vlan_id) + return vlan_id + + return None + + +def _extract_vlan_id_from_tag(tag): + """Extracts the VLAN ID from a given tag, if possible. + + Assumes the tag argument is definitely a VLAN ID tag as identified by + is_vlan_id_tag(tag). + + :param tag: The tag object. + :returns: The VLAN ID as an integer if extraction is successful + Otherwise returns None. + """ + try: + vlan_id = int(tag[len(VLAN_TAG_PREFIX):]) + except Exception: + return None + return vlan_id + + +def is_vlan_id_tag(tag): + """Determines if the given tag is a VLAN tag. + + :param tag: A tag model object. + :returns: True if the tag is a VLAN ID tag. False otherwise. + """ + return tag[0:len(VLAN_TAG_PREFIX)] == VLAN_TAG_PREFIX + + +def has_vlan_id(port): + """Determines if the specified port has a VLAN ID attached. + + :param port: The port object. + :returns: True if the port has an associated VLAN ID, False otherwise. + """ + for tag in port.tags: + if is_vlan_id_tag(tag): + return True + return False diff --git a/quark/tests/plugin_modules/test_ports.py b/quark/tests/plugin_modules/test_ports.py index 4299550..891d8cc 100644 --- a/quark/tests/plugin_modules/test_ports.py +++ b/quark/tests/plugin_modules/test_ports.py @@ -26,6 +26,7 @@ from quark.db import models from quark import exceptions as q_exc from quark import network_strategy from quark.plugin_modules import ports as quark_ports +from quark import port_vlan_id from quark.tests import test_quark_plugin @@ -128,6 +129,26 @@ class TestQuarkGetPorts(test_quark_plugin.TestQuarkPlugin): with self.assertRaises(exceptions.PortNotFound): self.plugin.get_port(self.context, 1) + def test_port_show_vlan_id(self): + """Prove VLAN IDs are included in port information when available.""" + port_tags = [port_vlan_id._build_vlan_tag_string("5")] + port = dict(mac_address=int('AABBCCDDEEFF', 16), network_id=1, + tenant_id=self.context.tenant_id, device_id=2, + tags=port_tags) + expected = {'status': "ACTIVE", + 'device_owner': None, + 'mac_address': 'AA:BB:CC:DD:EE:FF', + 'network_id': 1, + 'tenant_id': self.context.tenant_id, + 'admin_state_up': None, + 'fixed_ips': [], + 'device_id': 2, + 'vlan_id': 5} + with self._stubs(ports=port): + result = self.plugin.get_port(self.context, 1) + for key in expected.keys(): + self.assertEqual(result[key], expected[key]) + class TestQuarkGetPortsByIPAddress(test_quark_plugin.TestQuarkPlugin): @contextlib.contextmanager diff --git a/quark/tests/test_port_vlan_id.py b/quark/tests/test_port_vlan_id.py new file mode 100644 index 0000000..70024a7 --- /dev/null +++ b/quark/tests/test_port_vlan_id.py @@ -0,0 +1,170 @@ +# Copyright 2015 Rackspace +# 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 random +import string + +from quark.db import models +from quark import port_vlan_id +from quark.tests import test_base + +MAX_VLAN_ID = port_vlan_id.MAX_VLAN_ID +MIN_VLAN_ID = port_vlan_id.MIN_VLAN_ID + + +class TestPortVlanId(test_base.TestBase): + def setUp(self): + super(TestPortVlanId, self).setUp() + self.port_with_vlan = ( + self._create_port_with_vlan_id( + 0, random.randrange(MIN_VLAN_ID, MAX_VLAN_ID))) + self.port_without_vlan = self._create_test_port(1) + + def _create_test_port_with_lots_of_tags(self, port_id, vlan_id): + port = self._create_test_port(port_id) + port.tags.append("One weird olde tag.") + port.tags.append("Yet another tag.") + if vlan_id is not None: + port_vlan_id.store_vlan_id(port, vlan_id) + port.tags.append("One final tag") + return port + + def _create_test_port(self, port_id): + port = models.Port(id=port_id, network_id="1", ip_addresses=[], + tags=[]) + return port + + def _create_port_with_vlan_id(self, port_id, vlan_id): + port = self._create_test_port(port_id) + tag_contents = "%s%d" % (port_vlan_id.VLAN_TAG_PREFIX, vlan_id) + port.tags.append(tag_contents) + return port + + def test__validate_vlan_id(self): + valid_ids = [MIN_VLAN_ID, MAX_VLAN_ID] + for n in range(0, 10): + valid_ids.append(random.randrange(MIN_VLAN_ID, MAX_VLAN_ID)) + for vlan_id in valid_ids: + try: + port_vlan_id._validate_vlan_id(vlan_id) + except port_vlan_id.InvalidVlanIdError as e: + self.assertFalse(True, + "_validate_vlan_id raised an exception on " + "what should be a valid VLAN ID. Exception " + "message: %s" % (e.message)) + + invalid_ids = [MIN_VLAN_ID - 1, MAX_VLAN_ID + 1] + for n in range(0, 5): + valid_ids.append(random.randrange(MAX_VLAN_ID + 1, + MAX_VLAN_ID + 100)) + valid_ids.append(random.randrange(MIN_VLAN_ID - 100, + MIN_VLAN_ID - 1)) + for vlan_id in invalid_ids: + # _validate_vlan_id should raise on invalid IDs. + self.assertRaises(port_vlan_id.InvalidVlanIdError, + port_vlan_id._validate_vlan_id, + vlan_id) + + def test_store_vlan_id_vlan(self): + # Test against a valid VLAN ID + port = self.port_without_vlan + port_vlan_id.store_vlan_id(port, MIN_VLAN_ID) + self.assertTrue(len(port.tags) == 1) + vlan_tag = port.tags[0] + self.assertTrue(string.find(vlan_tag, + port_vlan_id.VLAN_TAG_PREFIX) == 0, + "Couldn't find the VLAN tag prefix in the vlan tag!") + self.assertTrue(string.find(vlan_tag, str(MIN_VLAN_ID)) != -1, + "The VLAN ID was not stored in the VLAN tag!") + + # Also test against an invalid ID + self.port_without_vlan = self._create_test_port(2) + port = self.port_without_vlan + self.assertRaises(port_vlan_id.InvalidVlanIdError, + port_vlan_id.store_vlan_id, + port, port_vlan_id.MIN_VLAN_ID - 1) + self.assertTrue(len(port.tags) == 0, + "The port has a new tag, even though the VLAN ID was " + "invalid!") + + def test_retrieve_vlan_id(self): + # VLAN ID exists + port = self.port_with_vlan + vlan_id = port_vlan_id.retrieve_vlan_id(port) + self.assertIsNotNone(vlan_id, + "VLAN ID returned by retrieve_vlan_id " + "is None despite having stored the VLAN ID on " + "this port earlier.") + + # VLAN ID is absent + port = self.port_without_vlan + vlan_id = port_vlan_id.retrieve_vlan_id(port) + self.assertIsNone(vlan_id, + "VLAN ID is not None, even though the port does " + "not have a VLAN ID stored.") + + # Other tags are present on the port. + port = self._create_test_port_with_lots_of_tags(3, 5) + vlan_id = port_vlan_id.retrieve_vlan_id(port) + self.assertEqual(5, vlan_id, + "Retrieved VLAN ID did not match expectations with " + "another tag present.") + + def test_is_vlan_id_tag(self): + # Test some good cases, note that is_vlan_id_tag doesn't validate + # the VLAN_ID itself, as it should've been validated before it was + # stored to the port model. + test_ids = [-1, 2, 3, 4, 100, 1000, 5234, "puppy", "dog"] + good_tags = [("%s%s" % (port_vlan_id.VLAN_TAG_PREFIX, vlan_id)) + for vlan_id in str(test_ids)] + for tag in good_tags: + self.assertTrue(port_vlan_id.is_vlan_id_tag(tag), + "A known good tag was not recognized as one by " + "is_vlan_id_tag. Tag: '%(tag)s'" % {'tag': tag}) + + # Test some bad ones + bad_tags = ["", "snake:50", "cipher", "zero", "[]asdrf897y", + port_vlan_id.VLAN_TAG_PREFIX[:-2], + "some_other_key_value_pair:234"] + for tag in bad_tags: + self.assertFalse(port_vlan_id.is_vlan_id_tag(tag), + "A known bad tag was recognized as a VLAN ID tag " + "by is_vlan_id_tag. Tag: '%(tag)s'" % + {'tag': tag}) + + def test_has_vlan_id(self): + # Test port with VLAN ID, but no other tags + port = self.port_with_vlan + self.assertTrue(port_vlan_id.has_vlan_id(port), + "has_vlan_id returned False even though the port is " + "known to have a valid VLAN ID tag.") + + # Test port without, no tags + port = self.port_without_vlan + self.assertFalse(port_vlan_id.has_vlan_id(port), + "has_vlan_id returned True even though the port " + "doesn't have a VLAN ID tag.") + + # Test port with VLAN ID, and several tags + port = self._create_test_port_with_lots_of_tags(5, 1337) + self.assertTrue(port_vlan_id.has_vlan_id(port), + "has_vlan_id returned False even though the port " + "has a VLAN ID tag.") + + # Test port without VLAN ID, but with several tags + port = self._create_test_port_with_lots_of_tags(5, None) + self.assertFalse(port_vlan_id.has_vlan_id(port), + "has_vlan_id returned True even though the port " + "does not have a VLAN ID tag.")