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

atlanticwave-sdx / sdx-controller / 24064169728

07 Apr 2026 04:24AM UTC coverage: 53.533% (-0.4%) from 53.92%
24064169728

Pull #523

github

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

5 of 34 new or added lines in 1 file covered. (14.71%)

2 existing lines in 1 file now uncovered.

1288 of 2406 relevant lines covered (53.53%)

1.07 hits per line

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

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

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

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

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

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

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

34

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

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

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

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

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

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

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

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

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

101
    return "OK", 200
2✔
102

103

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

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

111
    :rtype: Connection
112
    """
113

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

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

119
    return value
2✔
120

121

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

126
    connection details # noqa: E501
127

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

142

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

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

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

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

166

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

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

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

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

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

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

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

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

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

197
    db_instance.add_key_value_pair_to_db(MongoCollections.CONNECTIONS, service_id, body)
2✔
198

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

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

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

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

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

243
    return response, code
2✔
244

245

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

249
     # noqa: E501
250

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

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

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

265
    new_body = connexion.request.get_json()
×
266

267
    logger.info(f"Gathered connexion JSON: {new_body}")
×
268

NEW
269
    if "id" not in new_body:
×
NEW
270
        new_body["id"] = service_id
×
271

272
    # Validate the new request body before making any change to the existing connection.
273
    # 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.
274
    # 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.
275
    #
NEW
276
    te_manager = current_app.te_manager  # Assuming te_manager is accessible like this
×
NEW
277
    try:
×
278
        # Validate the new request body
NEW
279
        te_manager.generate_traffic_matrix(connection_request=new_body)
×
NEW
280
    except Exception as request_err:
×
NEW
281
        logger.error("ERROR: invalid patch request: " + str(request_err))
×
NEW
282
        return f"Error: patch request is no valid: {request_err}", 400
×
283

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

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

298
    body["oxp_success_count"] = 0
×
299

300
    # db_instance.add_key_value_pair_to_db(MongoCollections.CONNECTIONS, service_id, body)
301

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

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

324
        logger.info(f"Removed connection: {service_id}")
×
325
    except Exception as e:
×
326
        logger.info(f"Delete failed (connection id: {service_id}): {e}")
×
NEW
327
        conn_status = ConnectionStateMachine.State.DOWN
×
NEW
328
        body, _ = connection_state_machine(body, conn_status)
×
NEW
329
        db_instance.update_field_in_json(
×
330
            MongoCollections.CONNECTIONS,
331
            service_id,
332
            "status",
333
            str(conn_status),
334
        )
UNCOV
335
        return f"Failed, reason: {e}", 500
×
336

337
    logger.info(
×
338
        f"Modifying: Placing new connection {service_id} with te_manager: {current_app.te_manager}"
339
    )
340

341
    reason, code = connection_handler.place_connection(current_app.te_manager, body)
×
342

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

NEW
362
    conn_status = ConnectionStateMachine.State.DOWN
×
NEW
363
    body, _ = connection_state_machine(body, conn_status)
×
NEW
364
    db_instance.update_field_in_json(
×
365
        MongoCollections.CONNECTIONS,
366
        service_id,
367
        "status",
368
        str(conn_status),
369
    )
370

371
    logger.info(
×
372
        f"Failed to place new connection. ID: {service_id} reason='{reason}', code={code}"
373
    )
374
    logger.info("Rolling back to old connection.")
×
375

376
    # because above placement failed, so re-place the original connection request.
377

378
    rollback_conn_body["status"] = str(ConnectionStateMachine.State.REQUESTED)
×
379
    # used in lc_message_handler to count the oxp success response
380
    rollback_conn_body["oxp_success_count"] = 0
×
381

382
    conn_request = rollback_conn_body
×
383
    conn_request["id"] = service_id
×
384

385
    try:
×
386
        rollback_conn_reason, rollback_conn_code = connection_handler.place_connection(
×
387
            current_app.te_manager, conn_request
388
        )
NEW
389
        if rollback_conn_code // 100 == 2:
×
NEW
390
            conn_status = ConnectionStateMachine.State.UNDER_PROVISIONING
×
NEW
391
            rollback_conn_body, _ = connection_state_machine(
×
392
                rollback_conn_body, conn_status
393
            )
NEW
394
            db_instance.update_field_in_json(
×
395
                MongoCollections.CONNECTIONS,
396
                service_id,
397
                "status",
398
                str(conn_status),
399
            )
400
        else:
401
            conn_status = ConnectionStateMachine.State.REJECTED
×
NEW
402
            body, _ = connection_state_machine(body, conn_status)
×
UNCOV
403
            db_instance.update_field_in_json(
×
404
                MongoCollections.CONNECTIONS,
405
                service_id,
406
                "status",
407
                str(conn_status),
408
            )
409
        logger.info(
×
410
            f"Roll back connection result: ID: {service_id} reason='{rollback_conn_reason}', code={rollback_conn_code}"
411
        )
412
    except Exception as e:
×
413
        conn_status = ConnectionStateMachine.State.REJECTED
×
414
        db_instance.update_field_in_json(
×
415
            MongoCollections.CONNECTIONS,
416
            service_id,
417
            "status",
418
            str(conn_status),
419
        )
420
        logger.info(f"Rollback failed (connection id: {service_id}): {e}")
×
421
        rollback_conn_code = 500
×
422

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

430

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

435
    :param service_id: ID of connection that needs to be fetched
436
    :type service_id: str
437

438
    :rtype: Connection
439
    """
440

441
    value = connection_handler.get_archived_connections(service_id)
2✔
442

443
    if not value:
2✔
444
        return "Archived connection not found", 404
2✔
445

446
    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