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:
@@ -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
|
||||||
|
|||||||
@@ -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)
|
||||||
|
|||||||
@@ -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
121
quark/port_vlan_id.py
Normal 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
|
||||||
@@ -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
|
||||||
|
|||||||
170
quark/tests/test_port_vlan_id.py
Normal file
170
quark/tests/test_port_vlan_id.py
Normal 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.")
|
||||||
Reference in New Issue
Block a user