Skip to content

Commit

Permalink
Merge pull request #327 from atlanticwave-sdx/320.any-vlan-requests
Browse files Browse the repository at this point in the history
Handle connection requests with VLAN set to "any"
  • Loading branch information
sajith authored Sep 20, 2024
2 parents c906388 + 9595cbb commit a6b251d
Show file tree
Hide file tree
Showing 4 changed files with 261 additions and 8 deletions.
2 changes: 1 addition & 1 deletion pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@ dependencies = [
"pika >= 1.2.0",
"dataset",
"pymongo > 3.0",
"sdx-pce @ git+https://github.com/atlanticwave-sdx/[email protected].dev0",
"sdx-pce @ git+https://github.com/atlanticwave-sdx/[email protected].dev1",
]

[project.optional-dependencies]
Expand Down
11 changes: 8 additions & 3 deletions sdx_controller/controllers/l2vpn_controller.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,10 @@
from flask import current_app

from sdx_controller import util
from sdx_controller.handlers.connection_handler import ConnectionHandler
from sdx_controller.handlers.connection_handler import (
ConnectionHandler,
get_connection_status,
)
from sdx_controller.models.connection import Connection # noqa: E501
from sdx_controller.models.l2vpn_body import L2vpnBody # noqa: E501
from sdx_controller.models.l2vpn_service_id_body import L2vpnServiceIdBody # noqa: E501
Expand Down Expand Up @@ -77,10 +80,12 @@ def getconnection_by_id(service_id):
:rtype: Connection
"""

value = db_instance.read_from_db("connections", f"{service_id}")
value = get_connection_status(db_instance, service_id)

if not value:
return "Connection not found", 404
return json.loads(value[service_id])

return value


def getconnections(): # noqa: E501
Expand Down
119 changes: 119 additions & 0 deletions sdx_controller/handlers/connection_handler.py
Original file line number Diff line number Diff line change
Expand Up @@ -270,3 +270,122 @@ def handle_link_failure(self, te_manager, failed_links):
logger.debug("Removed connection:")
logger.debug(connection)
self.place_connection(te_manager, connection)


def get_connection_status(db, service_id: str):
"""
Form a response to `GET /l2vpn/1.0/{service_id}`.
"""
assert db is not None
assert service_id is not None

breakdown = db.read_from_db("breakdowns", service_id)
if not breakdown:
logger.info(f"Could not find breakdown for {service_id}")
return None

logger.info(f"breakdown for {service_id}: {breakdown}")

# The breakdown we read from DB is in this shape:
#
# {
# "_id": ObjectId("66ec71770c7022eb0922f41a"),
# "5b7df397-2269-489b-8e03-f256461265a0": {
# "urn:sdx:topology:amlight.net": {
# "name": "AMLIGHT_vlan_1000_10001",
# "dynamic_backup_path": True,
# "uni_a": {
# "tag": {"value": 1000, "tag_type": 1},
# "port_id": "urn:sdx:port:amlight.net:A1:1",
# },
# "uni_z": {
# "tag": {"value": 10001, "tag_type": 1},
# "port_id": "urn:sdx:port:amlight.net:B1:3",
# },
# }
# },
# }
#
# We need to shape that into this form, at a minimum:
#
# {
# "c73da8e1-5d03-4620-a1db-7cdf23e8978c": {
# "service_id": "c73da8e1-5d03-4620-a1db-7cdf23e8978c",
# "name": "new-connection",
# "endpoints": [
# {
# "port_id": "urn:sdx:port:amlight.net:A1:1",
# "vlan": "150"
# },
# {
# "port_id": "urn:sdx:port:amlight:B1:1",
# "vlan": "300"}
# ],
# }
# }
#
# See https://sdx-docs.readthedocs.io/en/latest/specs/provisioning-api-1.0.html#request-format-2
#

response = {}

domains = breakdown.get(service_id)
logger.info(f"domains for {service_id}: {domains.keys()}")

endpoints = list()

for domain, breakdown in domains.items():
uni_a_port = breakdown.get("uni_a").get("port_id")
uni_a_vlan = breakdown.get("uni_a").get("tag").get("value")

endpoint_a = {
"port_id": uni_a_port,
"vlan": str(uni_a_vlan),
}

endpoints.append(endpoint_a)

uni_z_port = breakdown.get("uni_z").get("port_id")
uni_z_vlan = breakdown.get("uni_z").get("tag").get("value")

endpoint_z = {
"port_id": uni_z_port,
"vlan": str(uni_z_vlan),
}

endpoints.append(endpoint_z)

# Find the name and description from the original connection
# request for this service_id.
name = "unknown"
description = "unknown"

request = db.read_from_db("connections", service_id)
if not request:
logger.error(f"Can't find a connection request for {service_id}")
# TODO: we're in a strange state here. Should we panic?
else:
logger.info(f"Found request for {service_id}: {request}")
# We seem to have saved the original request in the form of a
# string into the DB, not a record.
request_dict = json.loads(request.get(service_id))
name = request_dict.get("name")
description = request_dict.get("description")

# TODO: we're missing many of the attributes in the response here
# which have been specified in the provisioning spec, such as:
# name, description, qos_metrics, notifications, ownership,
# creation_date, archived_date, status, state, counters_location,
# last_modified, current_path, oxp_service_ids. Implementing each
# of them would be worth a separate ticket each, so we'll just
# make do with this minimal response for now.
response[service_id] = {
"service_id": service_id,
"name": name,
"description": description,
"endpoints": endpoints,
}

logger.info(f"Formed a response: {response}")

return response
137 changes: 133 additions & 4 deletions sdx_controller/test/test_l2vpn_controller.py
Original file line number Diff line number Diff line change
Expand Up @@ -335,14 +335,24 @@ def test_place_connection_v2_with_three_topologies_200_response(self):
self.__add_the_three_topologies()

# There should be solution for this request.
request = TestData.CONNECTION_REQ_V2_AMLIGHT_ZAOXI.read_text()
request = json.loads(TestData.CONNECTION_REQ_V2_AMLIGHT_ZAOXI.read_text())

# Remove any existing request ID.
request_json = json.loads(request)
original_request_id = request_json.pop("id")
original_request_id = request.pop("id")
print(f"original_request_id: {original_request_id}")

new_request = json.dumps(request_json)
# TODO: As a temporary workaround until the original
# connection request is corrected in datamodel, we modify the
# connection request for this test so that we have a solvable
# one. The original one asks for (1) a VLAN that is not
# present on the ingress port (777), and (2) a range ("55:90")
# on the egress port. This is an unsolvable request because
# of (1), and an invalid one because of (2) since both ports
# have to ask for either a range or a single VLAN.
request["endpoints"][0]["vlan"] = "100"
request["endpoints"][1]["vlan"] = "100"

new_request = json.dumps(request)
print(f"new_request: {new_request}")

response = self.client.open(
Expand Down Expand Up @@ -371,6 +381,125 @@ def test_place_connection_v2_with_three_topologies_200_response(self):
service_id = response.get_json().get("service_id")
self.assertNotEqual(service_id, original_request_id)

def test_place_connection_v2_with_any_vlan_in_request(self):
"""
Test that we get a valid response when the VLAN requested for
is "any".
"""
self.__add_the_three_topologies()

connection_request = """
{
"name": "new-connection",
"endpoints": [
{
"port_id": "urn:sdx:port:amlight.net:A1:1",
"vlan": "any"
},
{
"port_id": "urn:sdx:port:amlight:B1:1",
"vlan": "any"
}
]
}
"""

response = self.client.open(
f"{BASE_PATH}/l2vpn/1.0",
method="POST",
data=connection_request,
content_type="application/json",
)

print(f"POST response body is : {response.data.decode('utf-8')}")
print(f"POST Response JSON is : {response.get_json()}")

self.assertStatus(response, 200)

service_id = response.get_json().get("service_id")

response = self.client.open(
f"{BASE_PATH}/l2vpn/1.0/{service_id}",
method="GET",
)

print(f"GET response body is : {response.data.decode('utf-8')}")
print(f"GET response JSON is : {response.get_json()}")

self.assertStatus(response, 200)

# Expect a response like this:
#
# {
# "c73da8e1-5d03-4620-a1db-7cdf23e8978c": {
# "service_id": "c73da8e1-5d03-4620-a1db-7cdf23e8978c",
# "name": "new-connection",
# "endpoints": [
# {
# "port_id": "urn:sdx:port:amlight.net:A1:1",
# "vlan": "150"
# },
# {
# "port_id": "urn:sdx:port:amlight:B1:1",
# "vlan": "300"}
# ],
# }
# }
#
# See https://sdx-docs.readthedocs.io/en/latest/specs/provisioning-api-1.0.html#request-format-2

service = response.get_json().get(service_id)

self.assertIsNotNone(service)
self.assertEqual(service_id, service.get("service_id"))

endpoints = service.get("endpoints")
print(f"response endpoints: {endpoints}")

self.assertEqual(len(endpoints), 2)

# What were the original port_ids now?
request_dict = json.loads(connection_request)
requested_port0 = request_dict.get("endpoints")[0].get("port_id")
requested_port1 = request_dict.get("endpoints")[1].get("port_id")

# print(f"requested_port0: {requested_port0}")
# print(f"requested_port1: {requested_port1}")

# # TODO: There seems to be a little bit of inconsistency in
# # port names in amlight "user" topology file, present in
# # datamodel repository. Some ports have IDs like
# # `"urn:sdx:port:amlight:B1:1"` - note the missing ".net".
# # Just skip the assertion for now.
# self.assertEqual(endpoints[0].get("port_id"), requested_port0)
# self.assertEqual(endpoints[1].get("port_id"), requested_port1)

def is_integer(s: str):
"""
Retrun True if `s` wraps a number as a string.
"""
try:
int(s)
return True
except:
return False

vlan0 = endpoints[0].get("vlan")
vlan1 = endpoints[1].get("vlan")

# Check that one VLAN has been assigned on ingress port...
self.assertIsInstance(vlan0, str)
self.assertTrue(is_integer(vlan0))

# ... and one VLAN has been assigned on egress port.
self.assertIsInstance(vlan1, str)
self.assertTrue(is_integer(vlan1))

# Check that name and description match in request and
# response.
self.assertEqual(service.get("name"), request_dict.get("name"))
self.assertEqual(service.get("description"), request_dict.get("description"))

def test_z100_getconnection_by_id_expect_404(self):
"""
Test getconnection_by_id with a non-existent connection ID.
Expand Down

0 comments on commit a6b251d

Please sign in to comment.