• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

atlanticwave-sdx / sdx-controller / 24296999924

12 Apr 2026 02:39AM UTC coverage: 53.205% (-0.7%) from 53.92%
24296999924

Pull #523

github

web-flow
Merge ab48a841f into 765380db0
Pull Request #523: return 400 for invalid patch request

15 of 79 new or added lines in 3 files covered. (18.99%)

7 existing lines in 3 files now uncovered.

1295 of 2434 relevant lines covered (53.2%)

1.06 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

51.35
/sdx_controller/controllers/l2vpn_controller.py
1
import copy
2✔
2
import logging
2✔
3
import os
2✔
4
import time
2✔
5
import uuid
2✔
6

7
import connexion
2✔
8
from flask import current_app
2✔
9
from sdx_datamodel.connection_sm import ConnectionStateMachine
2✔
10
from sdx_datamodel.constants import MongoCollections
2✔
11

12
from sdx_controller.handlers.connection_handler import (
2✔
13
    ConnectionHandler,
14
    connection_state_machine,
15
    get_connection_status,
16
    parse_conn_status,
17
)
18

19
# from sdx_controller.models.l2vpn_service_id_body import L2vpnServiceIdBody  # noqa: E501
20
from sdx_controller.utils.db_utils import DbUtils
2✔
21

22
LOG_FORMAT = (
2✔
23
    "%(levelname) -10s %(asctime)s %(name) -30s %(funcName) "
24
    "-35s %(lineno) -5d: %(message)s"
25
)
26
logger = logging.getLogger(__name__)
2✔
27
logging.getLogger("pika").setLevel(logging.WARNING)
2✔
28
logger.setLevel(logging.getLevelName(os.getenv("LOG_LEVEL", "DEBUG")))
2✔
29

30
# Get DB connection and tables set up.
31
db_instance = DbUtils()
2✔
32
db_instance.initialize_db()
2✔
33
connection_handler = ConnectionHandler(db_instance)
2✔
34

35

36
def delete_connection(service_id):
2✔
37
    """
38
    Delete connection order by ID.
39

40
    :param service_id: ID of the connection that needs to be
41
        deleted
42
    :type service_id: str
43

44
    :rtype: None
45
    """
46
    logger.info(
2✔
47
        f"Handling delete (service id: {service_id}) "
48
        f"with te_manager: {current_app.te_manager}"
49
    )
50

51
    # # Looking up by UUID do not seem work yet.  Will address in
52
    # # https://github.com/atlanticwave-sdx/sdx-controller/issues/252.
53
    #
54
    # value = db_instance.read_from_db(f"{service_id}")
55
    # print(f"value: {value}")
56
    # if not value:
57
    #     return "Not found", 404
58

59
    try:
2✔
60
        # TODO: pce's unreserve_vlan() method silently returns even if the
61
        # service_id is not found.  This should in fact be an error.
62
        #
63
        # https://github.com/atlanticwave-sdx/pce/issues/180
64
        connection = db_instance.get_value_from_db(
2✔
65
            MongoCollections.CONNECTIONS, f"{service_id}"
66
        )
67

68
        if not connection:
2✔
69
            return "Did not find connection", 404
2✔
70

71
        logger.info(f"connection: {connection} {type(connection)}")
2✔
72
        if connection.get("status") is None:
2✔
73
            logger.error("Missing field: status is not in connection.")
×
74
            connection["status"] = str(ConnectionStateMachine.State.DELETED)
×
75
        elif connection["status"] == str(ConnectionStateMachine.State.UP):
2✔
76
            connection, _ = connection_state_machine(
2✔
77
                connection, ConnectionStateMachine.State.DELETED
78
            )
79
        elif connection["status"] == str(
×
80
            ConnectionStateMachine.State.UNDER_PROVISIONING
81
        ):
82
            connection, _ = connection_state_machine(
×
83
                connection, ConnectionStateMachine.State.DOWN
84
            )
85
            connection, _ = connection_state_machine(
×
86
                connection, ConnectionStateMachine.State.DELETED
87
            )
88
        else:
89
            connection, _ = connection_state_machine(
×
90
                connection, ConnectionStateMachine.State.DELETED
91
            )
92

93
        logger.info(f"Removing connection: {service_id} {connection.get('status')}")
2✔
94

95
        connection_handler.remove_connection(current_app.te_manager, service_id, "API")
2✔
96
        db_instance.mark_deleted(MongoCollections.CONNECTIONS, f"{service_id}")
2✔
97
        db_instance.mark_deleted(MongoCollections.BREAKDOWNS, f"{service_id}")
2✔
98
    except Exception as e:
×
99
        logger.info(f"Delete failed (connection id: {service_id}): {e}")
×
100
        return f"Failed, reason: {e}", 500
×
101

102
    return "OK", 200
2✔
103

104

105
def get_connection_by_id(service_id):
2✔
106
    """
107
    Find connection by ID.
108

109
    :param service_id: ID of connection that needs to be fetched
110
    :type service_id: str
111

112
    :rtype: Connection
113
    """
114

115
    value = get_connection_status(db_instance, service_id)
2✔
116

117
    if not value:
2✔
118
        return "Connection not found", 404
2✔
119

120
    return value
2✔
121

122

123
def get_connections():  # noqa: E501
2✔
124
    """
125
    List all connections
126

127
    connection details # noqa: E501
128

129
    :rtype: Connection
130
    """
131
    values = db_instance.get_all_entries_in_collection(MongoCollections.CONNECTIONS)
2✔
132
    if not values:
2✔
133
        return "No connection was found", 404
2✔
134
    return_values = {}
2✔
135
    for connection in values:
2✔
136
        service_id = next(iter(connection))
2✔
137
        logger.info(f"service_id: {service_id}")
2✔
138
        connection_status = get_connection_status(db_instance, service_id)
2✔
139
        if connection_status:
2✔
140
            return_values[service_id] = connection_status.get(service_id)
2✔
141
    return return_values
2✔
142

143

144
def get_archived_connections():
2✔
145
    """
146
    List all archived connections.
147

148
    :rtype: dict
149
    """
150
    values = db_instance.get_all_entries_in_collection(
2✔
151
        MongoCollections.HISTORICAL_CONNECTIONS
152
    )
153
    if not values:
2✔
154
        return "No archived connection was found", 404
2✔
155

156
    return_values = {}
2✔
157
    for archived_connection in values:
2✔
158
        service_id = next(iter(archived_connection))
2✔
159
        archived_events = connection_handler.get_archived_connections(service_id)
2✔
160
        if archived_events:
2✔
161
            return_values[service_id] = archived_events
2✔
162

163
    if not return_values:
2✔
164
        return "No archived connection was found", 404
×
165
    return return_values
2✔
166

167

168
def place_connection(body):
2✔
169
    """
170
    Place an connection request from the SDX-Controller.
171

172
    :param body: order placed for creating a connection
173
    :type body: dict | bytes
174

175
    :rtype: Connection
176
    """
177
    logger.info(f"Placing connection: {body}")
2✔
178
    if not connexion.request.is_json:
2✔
179
        return "Request body must be JSON", 400
×
180

181
    body = connexion.request.get_json()
2✔
182
    logger.info(f"Gathered connexion JSON: {body}")
2✔
183

184
    logger.info("Placing connection. Saving to database.")
2✔
185

186
    service_id = body.get("id")
2✔
187

188
    if service_id is None:
2✔
189
        service_id = str(uuid.uuid4())
2✔
190
        body["id"] = service_id
2✔
191
        logger.info(f"Request has no ID. Generated ID: {service_id}")
2✔
192

193
    conn_status = ConnectionStateMachine.State.REQUESTED
2✔
194
    body["status"] = str(conn_status)
2✔
195

196
    # used in lc_message_handler to count the oxp success response
197
    body["oxp_success_count"] = 0
2✔
198

199
    db_instance.add_key_value_pair_to_db(MongoCollections.CONNECTIONS, service_id, body)
2✔
200

201
    logger.info(
2✔
202
        f"Handling request {service_id} with te_manager: {current_app.te_manager}"
203
    )
204
    reason, code = connection_handler.place_connection(current_app.te_manager, body)
2✔
205

206
    if code // 100 == 2:
2✔
207
        conn_status = ConnectionStateMachine.State.UNDER_PROVISIONING
2✔
208
        body, _ = connection_state_machine(body, conn_status)
2✔
209
        db_instance.update_field_in_json(
2✔
210
            MongoCollections.CONNECTIONS,
211
            service_id,
212
            "status",
213
            str(conn_status),
214
        )
215
    else:
216
        conn_status = ConnectionStateMachine.State.REJECTED
2✔
217
        body, _ = connection_state_machine(body, conn_status)
2✔
218
        db_instance.update_field_in_json(
2✔
219
            MongoCollections.CONNECTIONS,
220
            service_id,
221
            "status",
222
            str(conn_status),
223
        )
224
    logger.info(
2✔
225
        f"place_connection result: ID: {service_id} reason='{reason}', code={code}"
226
    )
227

228
    response = {
2✔
229
        "service_id": service_id,
230
        "status": parse_conn_status(str(conn_status)),
231
        "reason": reason,
232
    }
233

234
    # # TODO: our response is supposed to be shaped just like request
235
    # # ('#/components/schemas/connection'), and in that case the below
236
    # # code would be a quick implementation.
237
    # #
238
    # # https://github.com/atlanticwave-sdx/sdx-controller/issues/251
239
    # response = body
240

241
    # response["id"] = service_id
242
    # response["status"] = "success" if code == 2xx else "failure"
243
    # response["reason"] = reason # `reason` is not present in schema though.
244

245
    return response, code
2✔
246

247

248
def patch_connection(service_id, body=None):  # noqa: E501
2✔
249
    """Edit and change an existing L2vpn connection by ID from the SDX-Controller
250

251
     # noqa: E501
252

253
    :param service_id: ID of l2vpn connection that needs to be changed
254
    :type service_id: dict | bytes'
255
    :param body:
256
    :type body: dict | bytes
257

258
    :rtype: Connection
259
    """
260
    body = db_instance.get_value_from_db(MongoCollections.CONNECTIONS, f"{service_id}")
×
261
    if not body:
×
262
        return "Connection not found", 404
×
263

264
    if not connexion.request.is_json:
×
265
        return "Request body must be JSON", 400
×
266

267
    new_body = connexion.request.get_json()
×
268

269
    logger.info(f"Gathered connexion JSON: {new_body}")
×
270

NEW
271
    if "id" not in new_body:
×
NEW
272
        new_body["id"] = service_id
×
273

274
    # Validate the new request body before making any change to the existing connection.
275
    # This is to avoid the case where we have already removed the original connection but the new request body is invalid, which will cause the connection to be deleted but not re-created.
276
    # We can reuse the same validation function used in place_connection since the request body for patch_connection has the same schema as place_connection.
277
    #
NEW
278
    te_manager = current_app.te_manager  # Assuming te_manager is accessible like this
×
NEW
279
    try:
×
280
        # Validate the new request body
NEW
281
        te_manager.generate_traffic_matrix(connection_request=new_body)
×
NEW
282
    except Exception as request_err:
×
NEW
283
        logger.error("ERROR: invalid patch request: " + str(request_err))
×
NEW
284
        error_code = str(request_err).split("Code: ")[-1].replace(")", "").strip()
×
NEW
285
        return f"Error: patch request is no valid: {request_err}", int(error_code)
×
286

NEW
287
    logger.info("Modifying connection")
×
288
    # Get roll back connection before removing connection
289
    rollback_conn_body = copy.deepcopy(body)
×
290
    body.update(new_body)
×
291

NEW
292
    conn_status = ConnectionStateMachine.State.MODIFYING
×
NEW
293
    body, _ = connection_state_machine(body, conn_status)
×
NEW
294
    db_instance.update_field_in_json(
×
295
        MongoCollections.CONNECTIONS,
296
        service_id,
297
        "status",
298
        str(conn_status),
299
    )
300

301
    try:
×
302
        logger.info("Removing connection")
×
303
        remove_conn_reason, remove_conn_code = connection_handler.remove_connection(
×
304
            current_app.te_manager, service_id, "API"
305
        )
306

307
        if remove_conn_code // 100 != 2:
×
NEW
308
            conn_status = ConnectionStateMachine.State.DOWN
×
NEW
309
            body, _ = connection_state_machine(body, conn_status)
×
NEW
310
            db_instance.update_field_in_json(
×
311
                MongoCollections.CONNECTIONS,
312
                service_id,
313
                "status",
314
                str(conn_status),
315
            )
316
            response = {
×
317
                "service_id": service_id,
318
                "status": parse_conn_status(body["status"]),
319
                "reason": f"Failure to modify L2VPN during removal: {remove_conn_reason}",
320
            }
321
            return response, remove_conn_code
×
322

323
        logger.info(f"Removed connection: {service_id}")
×
324
    except Exception as e:
×
325
        logger.info(f"Delete failed (connection id: {service_id}): {e}")
×
NEW
326
        conn_status = ConnectionStateMachine.State.DOWN
×
NEW
327
        body, _ = connection_state_machine(body, conn_status)
×
NEW
328
        db_instance.update_field_in_json(
×
329
            MongoCollections.CONNECTIONS,
330
            service_id,
331
            "status",
332
            str(conn_status),
333
        )
UNCOV
334
        return f"Failed, reason: {e}", 500
×
NEW
335
    time.sleep(10)
×
336
    logger.info(
×
337
        f"Modifying: Placing new connection {service_id} with te_manager: {current_app.te_manager}"
338
    )
339
    # reset: the original one has been removed from db
340
    reason, code = connection_handler.place_connection(current_app.te_manager, body)
×
341

NEW
342
    body["oxp_success_count"] = 0
×
NEW
343
    body["oxp_response"] = {}
×
UNCOV
344
    if code // 100 == 2:
×
345
        # Service created successfully
NEW
346
        conn_status = ConnectionStateMachine.State.UNDER_PROVISIONING
×
NEW
347
        body, _ = connection_state_machine(body, conn_status)
×
NEW
348
        db_instance.add_key_value_pair_to_db(
×
349
            MongoCollections.CONNECTIONS, service_id, body
350
        )
351
        code = 201
×
352
        logger.info(f"Placed: ID: {service_id} reason='{reason}', code={code}")
×
353
        response = {
×
354
            "service_id": service_id,
355
            "status": parse_conn_status(body["status"]),
356
            "reason": reason,
357
        }
358
        return response, code
×
359

NEW
360
    conn_status = ConnectionStateMachine.State.DOWN
×
NEW
361
    body, _ = connection_state_machine(body, conn_status)
×
362

363
    logger.info(
×
364
        f"Modifying: Failed to place new connection. ID: {service_id} reason='{reason}', code={code}"
365
    )
366
    logger.info("Rolling back to old connection.")
×
367

368
    # because above placement failed, so re-place the original connection request.
369

370
    rollback_conn_body["status"] = str(ConnectionStateMachine.State.REQUESTED)
×
371
    # used in lc_message_handler to count the oxp success response
372
    rollback_conn_body["oxp_success_count"] = 0
×
NEW
373
    rollback_conn_body["oxp_response"] = {}
×
374

375
    conn_request = rollback_conn_body
×
376
    conn_request["id"] = service_id
×
NEW
377
    db_instance.add_key_value_pair_to_db(
×
378
        MongoCollections.CONNECTIONS, service_id, conn_request
379
    )
380

381
    try:
×
382
        rollback_conn_reason, rollback_conn_code = connection_handler.place_connection(
×
383
            current_app.te_manager, conn_request
384
        )
NEW
385
        if rollback_conn_code // 100 == 2:
×
NEW
386
            conn_status = ConnectionStateMachine.State.UNDER_PROVISIONING
×
NEW
387
            rollback_conn_body, _ = connection_state_machine(
×
388
                rollback_conn_body, conn_status
389
            )
NEW
390
            db_instance.update_field_in_json(
×
391
                MongoCollections.CONNECTIONS,
392
                service_id,
393
                "status",
394
                str(conn_status),
395
            )
396
            # still return 400 to indicate the patch request is not successful, since we have already rolled back to original connection, which is under provisioning state, so the connection is not down and not failed.
NEW
397
            rollback_conn_code = code
×
398
        else:
399
            conn_status = ConnectionStateMachine.State.REJECTED
×
NEW
400
            body, _ = connection_state_machine(body, conn_status)
×
UNCOV
401
            db_instance.update_field_in_json(
×
402
                MongoCollections.CONNECTIONS,
403
                service_id,
404
                "status",
405
                str(conn_status),
406
            )
407
        logger.info(
×
408
            f"Roll back connection result: ID: {service_id} reason='{rollback_conn_reason}', code={rollback_conn_code}"
409
        )
410
    except Exception as e:
×
411
        conn_status = ConnectionStateMachine.State.REJECTED
×
412
        db_instance.update_field_in_json(
×
413
            MongoCollections.CONNECTIONS,
414
            service_id,
415
            "status",
416
            str(conn_status),
417
        )
418
        logger.info(f"Rollback failed (connection id: {service_id}): {e}")
×
419
        rollback_conn_code = 500
×
420

421
    response = {
×
422
        "service_id": service_id,
423
        "reason": f"Patch Failure,rolled back to last successful L2VPN: {rollback_conn_reason}",
424
        "status": parse_conn_status(str(conn_status)),
425
    }
426
    return response, rollback_conn_code
×
427

428

429
def get_archived_connections_by_id(service_id):
2✔
430
    """
431
    List archived connection by ID.
432

433
    :param service_id: ID of connection that needs to be fetched
434
    :type service_id: str
435

436
    :rtype: Connection
437
    """
438

439
    value = connection_handler.get_archived_connections(service_id)
2✔
440

441
    if not value:
2✔
442
        return "Archived connection not found", 404
2✔
443

444
    return {service_id: value}
2✔
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc