Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Handle connection requests with VLAN set to "any" #327

Merged
merged 14 commits into from
Sep 20, 2024
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