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

atlanticwave-sdx / sdx-controller / 24052997469

06 Apr 2026 09:50PM UTC coverage: 53.779% (-0.1%) from 53.92%
24052997469

Pull #523

github

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

5 of 14 new or added lines in 1 file covered. (35.71%)

1 existing line in 1 file now uncovered.

1288 of 2395 relevant lines covered (53.78%)

1.08 hits per line

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

55.09
/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
    logger.info(
2✔
217
        f"place_connection result: ID: {service_id} reason='{reason}', code={code}"
218
    )
219

220
    response = {
2✔
221
        "service_id": service_id,
222
        "status": parse_conn_status(str(conn_status)),
223
        "reason": reason,
224
    }
225

226
    # # TODO: our response is supposed to be shaped just like request
227
    # # ('#/components/schemas/connection'), and in that case the below
228
    # # code would be a quick implementation.
229
    # #
230
    # # https://github.com/atlanticwave-sdx/sdx-controller/issues/251
231
    # response = body
232

233
    # response["id"] = service_id
234
    # response["status"] = "success" if code == 2xx else "failure"
235
    # response["reason"] = reason # `reason` is not present in schema though.
236

237
    return response, code
2✔
238

239

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

243
     # noqa: E501
244

245
    :param service_id: ID of l2vpn connection that needs to be changed
246
    :type service_id: dict | bytes'
247
    :param body:
248
    :type body: dict | bytes
249

250
    :rtype: Connection
251
    """
252
    body = db_instance.get_value_from_db(MongoCollections.CONNECTIONS, f"{service_id}")
×
253
    if not body:
×
254
        return "Connection not found", 404
×
255

256
    if not connexion.request.is_json:
×
257
        return "Request body must be JSON", 400
×
258

259
    new_body = connexion.request.get_json()
×
260

261
    logger.info(f"Gathered connexion JSON: {new_body}")
×
262

NEW
263
    if "id" not in new_body:
×
NEW
264
        new_body["id"] = service_id
×
265

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

NEW
278
    logger.info("Modifying connection")
×
279
    # Get roll back connection before removing connection
280
    rollback_conn_body = copy.deepcopy(body)
×
281
    body.update(new_body)
×
282

283
    body, _ = connection_state_machine(body, ConnectionStateMachine.State.MODIFYING)
×
284

285
    body["oxp_success_count"] = 0
×
286

287
    db_instance.add_key_value_pair_to_db(MongoCollections.CONNECTIONS, service_id, body)
×
288

289
    try:
×
290
        logger.info("Removing connection")
×
291
        remove_conn_reason, remove_conn_code = connection_handler.remove_connection(
×
292
            current_app.te_manager, service_id, "API"
293
        )
294

295
        if remove_conn_code // 100 != 2:
×
296
            body, _ = connection_state_machine(body, ConnectionStateMachine.State.DOWN)
×
297
            db_instance.add_key_value_pair_to_db(
×
298
                MongoCollections.CONNECTIONS, service_id, body
299
            )
300
            response = {
×
301
                "service_id": service_id,
302
                "status": parse_conn_status(body["status"]),
303
                "reason": f"Failure to modify L2VPN during removal: {remove_conn_reason}",
304
            }
305
            return response, remove_conn_code
×
306

307
        logger.info(f"Removed connection: {service_id}")
×
308
    except Exception as e:
×
309
        logger.info(f"Delete failed (connection id: {service_id}): {e}")
×
310
        return f"Failed, reason: {e}", 500
×
311

312
    logger.info(
×
313
        f"Placing new connection {service_id} with te_manager: {current_app.te_manager}"
314
    )
315

316
    body, _ = connection_state_machine(
×
317
        body, ConnectionStateMachine.State.UNDER_PROVISIONING
318
    )
319
    db_instance.add_key_value_pair_to_db(MongoCollections.CONNECTIONS, service_id, body)
×
320
    reason, code = connection_handler.place_connection(current_app.te_manager, body)
×
321

322
    if code // 100 == 2:
×
323
        # Service created successfully
324
        code = 201
×
325
        logger.info(f"Placed: ID: {service_id} reason='{reason}', code={code}")
×
326
        response = {
×
327
            "service_id": service_id,
328
            "status": parse_conn_status(body["status"]),
329
            "reason": reason,
330
        }
331
        return response, code
×
332
    else:
333
        body, _ = connection_state_machine(body, ConnectionStateMachine.State.DOWN)
×
334

335
    logger.info(
×
336
        f"Failed to place new connection. ID: {service_id} reason='{reason}', code={code}"
337
    )
338
    logger.info("Rolling back to old connection.")
×
339

340
    # because above placement failed, so re-place the original connection request.
341

342
    rollback_conn_body["status"] = str(ConnectionStateMachine.State.REQUESTED)
×
343
    # used in lc_message_handler to count the oxp success response
344
    rollback_conn_body["oxp_success_count"] = 0
×
345
    conn_status = ConnectionStateMachine.State.UNDER_PROVISIONING
×
346
    rollback_conn_body, _ = connection_state_machine(rollback_conn_body, conn_status)
×
347

348
    conn_request = rollback_conn_body
×
349
    conn_request["id"] = service_id
×
350

351
    try:
×
352
        rollback_conn_reason, rollback_conn_code = connection_handler.place_connection(
×
353
            current_app.te_manager, conn_request
354
        )
355
        if rollback_conn_code // 100 != 2:
×
356
            conn_status = ConnectionStateMachine.State.REJECTED
×
357
            db_instance.update_field_in_json(
×
358
                MongoCollections.CONNECTIONS,
359
                service_id,
360
                "status",
361
                str(conn_status),
362
            )
363
        logger.info(
×
364
            f"Roll back connection result: ID: {service_id} reason='{rollback_conn_reason}', code={rollback_conn_code}"
365
        )
366
    except Exception as e:
×
367
        conn_status = ConnectionStateMachine.State.REJECTED
×
368
        db_instance.update_field_in_json(
×
369
            MongoCollections.CONNECTIONS,
370
            service_id,
371
            "status",
372
            str(conn_status),
373
        )
374
        logger.info(f"Rollback failed (connection id: {service_id}): {e}")
×
375
        rollback_conn_code = 500
×
376

377
    response = {
×
378
        "service_id": service_id,
379
        "reason": f"Patch Failure,rolled back to last successful L2VPN: {rollback_conn_reason}",
380
        "status": parse_conn_status(str(conn_status)),
381
    }
382
    return response, rollback_conn_code
×
383

384

385
def get_archived_connections_by_id(service_id):
2✔
386
    """
387
    List archived connection by ID.
388

389
    :param service_id: ID of connection that needs to be fetched
390
    :type service_id: str
391

392
    :rtype: Connection
393
    """
394

395
    value = connection_handler.get_archived_connections(service_id)
2✔
396

397
    if not value:
2✔
398
        return "Archived connection not found", 404
2✔
399

400
    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