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

atlanticwave-sdx / sdx-controller / 24095270525

07 Apr 2026 05:32PM UTC coverage: 53.264% (-0.7%) from 53.92%
24095270525

Pull #523

github

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

6 of 63 new or added lines in 3 files covered. (9.52%)

6 existing lines in 3 files now uncovered.

1289 of 2420 relevant lines covered (53.26%)

1.07 hits per line

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

51.96
/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
    try:
×
299
        logger.info("Removing connection")
×
300
        remove_conn_reason, remove_conn_code = connection_handler.remove_connection(
×
301
            current_app.te_manager, service_id, "API"
302
        )
303

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

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

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

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

NEW
356
    conn_status = ConnectionStateMachine.State.DOWN
×
NEW
357
    body, _ = connection_state_machine(body, conn_status)
×
NEW
358
    db_instance.add_key_value_pair_to_db(MongoCollections.CONNECTIONS, service_id, body)
×
359

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

365
    # because above placement failed, so re-place the original connection request.
366

367
    rollback_conn_body["status"] = str(ConnectionStateMachine.State.REQUESTED)
×
368
    # used in lc_message_handler to count the oxp success response
369
    rollback_conn_body["oxp_success_count"] = 0
×
370

371
    conn_request = rollback_conn_body
×
372
    conn_request["id"] = service_id
×
373

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

414
    response = {
×
415
        "service_id": service_id,
416
        "reason": f"Patch Failure,rolled back to last successful L2VPN: {rollback_conn_reason}",
417
        "status": parse_conn_status(str(conn_status)),
418
    }
419
    return response, rollback_conn_code
×
420

421

422
def get_archived_connections_by_id(service_id):
2✔
423
    """
424
    List archived connection by ID.
425

426
    :param service_id: ID of connection that needs to be fetched
427
    :type service_id: str
428

429
    :rtype: Connection
430
    """
431

432
    value = connection_handler.get_archived_connections(service_id)
2✔
433

434
    if not value:
2✔
435
        return "Archived connection not found", 404
2✔
436

437
    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