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

atlanticwave-sdx / sdx-controller / 24293957145

11 Apr 2026 11:26PM UTC coverage: 53.229% (-0.7%) from 53.92%
24293957145

Pull #523

github

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

14 of 76 new or added lines in 3 files covered. (18.42%)

6 existing lines in 3 files now uncovered.

1294 of 2431 relevant lines covered (53.23%)

1.06 hits per line

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

51.37
/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
    conn_status = ConnectionStateMachine.State.REQUESTED
2✔
193
    body["status"] = str(conn_status)
2✔
194

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

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

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

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

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

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

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

244
    return response, code
2✔
245

246

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

250
     # noqa: E501
251

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

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

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

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

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

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

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

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

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

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

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

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

335
    logger.info(
×
336
        f"Modifying: Placing new connection {service_id} with te_manager: {current_app.te_manager}"
337
    )
338
    # reset: the original one has been removed from db
339
    reason, code = connection_handler.place_connection(current_app.te_manager, body)
×
340

NEW
341
    body["oxp_success_count"] = 0
×
NEW
342
    body["oxp_response"] = {}
×
UNCOV
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.add_key_value_pair_to_db(
×
348
            MongoCollections.CONNECTIONS, service_id, body
349
        )
350
        code = 201
×
351
        logger.info(f"Placed: ID: {service_id} reason='{reason}', code={code}")
×
352
        response = {
×
353
            "service_id": service_id,
354
            "status": parse_conn_status(body["status"]),
355
            "reason": reason,
356
        }
357
        return response, code
×
358

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

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

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

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

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

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

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

427

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

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

435
    :rtype: Connection
436
    """
437

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

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

443
    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