WIP: Adding VLAN ID to Port objects via Tags

Adds a module to store and retrieve VLAN IDs using Tags on the Port
model. Also, VLAN ID data is attached to Port descriptions, where
appropriate,  obtained via port show API requests.

Along with necessary unit tests.
This commit is contained in:
Clif Houck
2015-08-31 17:08:52 -05:00
parent 8dc27e7495
commit 8fb86aa58f
6 changed files with 333 additions and 2 deletions

View File

@@ -31,6 +31,7 @@ from sqlalchemy.orm import class_mapper
from quark.db import models from quark.db import models
from quark.db import sqlalchemy_adapter as quark_sa from quark.db import sqlalchemy_adapter as quark_sa
from quark import network_strategy from quark import network_strategy
from quark import port_vlan_id
from quark import protocols from quark import protocols
@@ -226,11 +227,12 @@ def port_count_all(context, **filters):
def port_create(context, **port_dict): def port_create(context, **port_dict):
port = models.Port() port = models.Port(tags=[])
port.update(port_dict) port.update(port_dict)
port["tenant_id"] = context.tenant_id port["tenant_id"] = context.tenant_id
if "addresses" in port_dict: if "addresses" in port_dict:
port["ip_addresses"].extend(port_dict["addresses"]) port["ip_addresses"].extend(port_dict["addresses"])
_port_store_vlan_id(port, **port_dict)
context.session.add(port) context.session.add(port)
return port return port
@@ -279,9 +281,20 @@ def update_port_associations_for_ip(context, ports, address):
assoc_ports - new_ports, new_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): def port_update(context, port, **kwargs):
if "addresses" in kwargs: if "addresses" in kwargs:
port["ip_addresses"] = kwargs.pop("addresses") port["ip_addresses"] = kwargs.pop("addresses")
_port_store_vlan_id(port, **kwargs)
port.update(kwargs) port.update(kwargs)
context.session.add(port) context.session.add(port)
return port return port

View File

@@ -412,7 +412,7 @@ class SecurityGroup(BASEV2, models.HasId):
tenant_id = sa.Column(sa.String(255), index=True) 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" __tablename__ = "quark_ports"
id = sa.Column(sa.String(36), primary_key=True) id = sa.Column(sa.String(36), primary_key=True)
name = sa.Column(sa.String(255), index=True) name = sa.Column(sa.String(255), index=True)

View File

@@ -23,6 +23,7 @@ from oslo_log import log as logging
from quark.db import ip_types from quark.db import ip_types
from quark import network_strategy from quark import network_strategy
from quark import port_vlan_id
from quark import protocols 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 # NOTE(mdietz): more pythonic key in dict check fails here. Leave as get
if port.get("bridge"): if port.get("bridge"):
res["bridge"] = port["bridge"] res["bridge"] = port["bridge"]
vlan_id = port_vlan_id.retrieve_vlan_id(port)
if vlan_id:
res["vlan_id"] = vlan_id
return res return res

121
quark/port_vlan_id.py Normal file
View File

@@ -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

View File

@@ -26,6 +26,7 @@ from quark.db import models
from quark import exceptions as q_exc from quark import exceptions as q_exc
from quark import network_strategy from quark import network_strategy
from quark.plugin_modules import ports as quark_ports from quark.plugin_modules import ports as quark_ports
from quark import port_vlan_id
from quark.tests import test_quark_plugin from quark.tests import test_quark_plugin
@@ -128,6 +129,26 @@ class TestQuarkGetPorts(test_quark_plugin.TestQuarkPlugin):
with self.assertRaises(exceptions.PortNotFound): with self.assertRaises(exceptions.PortNotFound):
self.plugin.get_port(self.context, 1) 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): class TestQuarkGetPortsByIPAddress(test_quark_plugin.TestQuarkPlugin):
@contextlib.contextmanager @contextlib.contextmanager

View File

@@ -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.")