http_handlers.py 22.7 KB
Newer Older
Pavel Spirek's avatar
Pavel Spirek committed
1 2 3
import json
import os
import mimetypes
4

Pavel Spirek's avatar
Pavel Spirek committed
5
from collections import OrderedDict
6
from enum import Enum
7
from colorlog import error, warning as warn, info
Pavel Spirek's avatar
Pavel Spirek committed
8
from urllib.parse import parse_qs
9
from datetime import datetime
10
from typing import Dict, List, Optional
Pavel Spirek's avatar
Pavel Spirek committed
11

12
from yangson.exceptions import YangsonException, NonexistentSchemaNode, SchemaError, SemanticError
13
from yangson.schemanode import ContainerNode, ListNode, GroupNode, LeafListNode, LeafNode
14
from yangson.instance import NonexistentInstance, InstanceValueError, RootNode
Pavel Spirek's avatar
Pavel Spirek committed
15
from yangson.datatype import YangTypeError
Pavel Spirek's avatar
Pavel Spirek committed
16

Pavel Spirek's avatar
Pavel Spirek committed
17 18
from .knot_api import KnotError
from .config import CONFIG_GLOBAL, CONFIG_HTTP, CONFIG_NACM, API_ROOT_data, API_ROOT_STAGING_data, API_ROOT_ops
19
from .helpers import CertHelpers, DateTimeHelpers, ErrorHelpers, LogHelpers, SSLCertT
Pavel Spirek's avatar
Pavel Spirek committed
20
from .nacm import NacmForbiddenError
21 22 23 24 25 26 27
from .data import (
    BaseDatastore,
    RpcInfo,
    DataLockError,
    NoHandlerError,
    NoHandlerForOpError,
    InstanceAlreadyPresent,
28
    ChangeType,
29 30
    ConfHandlerFailedError,
    OpHandlerFailedError
Pavel Spirek's avatar
Pavel Spirek committed
31
)
Pavel Spirek's avatar
Pavel Spirek committed
32

33
QueryStrT = Dict[str, List[str]]
Pavel Spirek's avatar
Pavel Spirek committed
34
epretty = ErrorHelpers.epretty
35
errtag = ErrorHelpers.errtag
36
debug_httph = LogHelpers.create_module_dbg_logger(__name__)
37

Pavel Spirek's avatar
Pavel Spirek committed
38

39 40 41 42
CT_PLAIN = "text/plain"
CT_YANG_JSON = "application/yang.api+json"


43 44 45 46
class HttpRequestError(Exception):
    pass


47 48 49 50
class HttpStatus(Enum):
    Ok          = ("200", "OK")
    Created     = ("201", "Created")
    NoContent   = ("204", "No Content")
51
    NotModified = ("304", "Not Modified")
52 53 54 55 56 57
    BadRequest  = ("400", "Bad Request")
    Forbidden   = ("403", "Forbidden")
    NotFound    = ("404", "Not Found")
    MethodNotAllowed    = ("405", "Method Not Allowed")
    NotAcceptable       = ("406", "Not Acceptable")
    Conflict    = ("409", "Conflict")
Pavel Spirek's avatar
Pavel Spirek committed
58
    ReqTooLarge = ("413", "Request Entity Too Large")
59 60 61 62 63 64 65 66 67 68 69
    InternalServerError = ("500", "Internal Server Error")

    @property
    def code(self) -> str:
        return self.value[0]

    @property
    def msg(self) -> str:
        return self.value[1]


70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86
ERRTAG_MALFORMED = "malformed-message"
ERRTAG_REQLARGE = "request-too-large"
ERRTAG_OPNOTSUPPORTED = "operation-not-supported"
ERRTAG_OPFAILED = "operation-failed"
ERRTAG_ACCDENIED = "access-denied"
ERRTAG_LOCKDENIED = "lock-denied"
ERRTAG_INVVALUE = "invalid-value"
ERRTAG_EXISTS = "data-exists"


class RestconfErrType(Enum):
    Transport   = "transport"
    Rpc         = "rpc"
    Protocol    = "protocol"
    Application = "application"


87 88 89 90
class HttpResponse:
    def __init__(self, status: HttpStatus, data: bytes, content_type: str, extra_headers: OrderedDict=None):
        self.status_code = status.code
        self.data = data
91
        self.content_length = len(data)
92 93 94 95
        self.content_type = content_type
        self.extra_headers = extra_headers

    @classmethod
96
    def empty(cls, status: HttpStatus, status_in_body: bool=False) -> "HttpResponse":
97 98 99 100 101 102 103
        if status_in_body:
            response = status.code + " " + status.msg + "\n"
        else:
            response = ""

        return cls(status, response.encode(), CT_PLAIN)

104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145
    @classmethod
    def error(cls, status: HttpStatus, err_type: RestconfErrType, err_tag: str, err_apptag: str=None,
              err_path: str=None, err_msg: str=None, exception: Exception=None) -> "HttpResponse":
        err_body = {
            "error-type": err_type.value,
            "error-tag": err_tag
        }

        # Auto-fill app-tag, path and mesage fields from Python's Exception attributes
        if exception is not None:
            try:
                err_body["error-app-tag"] = exception.tag
            except AttributeError:
                pass

            try:
                err_body["error-path"] = exception.path
            except AttributeError:
                pass

            err_body["error-message"] = epretty(exception)

        if err_apptag is not None:
            err_body["error-app-tag"] = err_apptag

        if err_path is not None:
            err_body["error-path"] = err_path

        if err_msg is not None:
            err_body["error-message"] = err_msg

        err_template = {
            "ietf-restconf:errors": {
                "error": [
                    err_body
                ]
            }
        }

        response = json.dumps(err_template, indent=4)
        return cls(status, response.encode(), CT_YANG_JSON)

146

147
def unknown_req_handler(headers: OrderedDict, data: Optional[str], client_cert: SSLCertT) -> HttpResponse:
148 149 150 151 152 153
    return HttpResponse.error(
        HttpStatus.BadRequest,
        RestconfErrType.Transport,
        ERRTAG_MALFORMED,
        "unknown_req_handler"
    )
154

155

156
def _get_yl_date() -> str:
157 158 159 160 161 162
    try:
        yang_lib_date_ts = os.path.getmtime(os.path.join(CONFIG_GLOBAL["YANG_LIB_DIR"], "yang-library-data.json"))
        yang_lib_date = datetime.fromtimestamp(yang_lib_date_ts).strftime("%Y-%m-%d")
    except OSError:
        yang_lib_date = None

163 164 165 166 167 168
    return yang_lib_date


def api_root_handler(headers: OrderedDict, data: Optional[str], client_cert: SSLCertT):
    # Top level api resource (appendix B.1.1)

169 170 171 172
    top_res = {
        "ietf-restconf:restconf": {
            "data": {},
            "operations": {},
173
            "yang-library-version": _get_yl_date()
174 175
        }
    }
176

177
    response = json.dumps(top_res, indent=4)
178
    return HttpResponse(HttpStatus.Ok, response.encode(), CT_YANG_JSON)
Pavel Spirek's avatar
Pavel Spirek committed
179 180


181 182 183 184 185 186 187 188 189
def api_ylv_handler(headers: OrderedDict, data: Optional[str], client_cert: SSLCertT):
    ylv = {
        "ietf-restconf:yang-library-version": _get_yl_date()
    }

    response = json.dumps(ylv, indent=4)
    return HttpResponse(HttpStatus.Ok, response.encode(), CT_YANG_JSON)


190
def _get(ds: BaseDatastore, req_headers: OrderedDict, pth: str, username: str, staging: bool=False) -> HttpResponse:
191 192 193 194 195 196 197
    url_split = pth.split("?")
    url_path = url_split[0]
    if len(url_split) > 1:
        query_string = parse_qs(url_split[1])
    else:
        query_string = {}

198
    rpc1 = RpcInfo()
Pavel Spirek's avatar
Pavel Spirek committed
199
    rpc1.username = username
200
    rpc1.path = url_path.rstrip("/")
201
    rpc1.qs = query_string
Pavel Spirek's avatar
Pavel Spirek committed
202 203 204

    try:
        ds.lock_data(username)
205
        http_resp = None
206

207
        try:
Pavel Spirek's avatar
Pavel Spirek committed
208
            n = ds.get_node_rpc(rpc1, staging)
209
        except NacmForbiddenError as e:
210 211 212 213 214 215
            http_resp = HttpResponse.error(
                HttpStatus.Forbidden,
                RestconfErrType.Protocol,
                ERRTAG_ACCDENIED,
                exception=e
            )
216
        except (NonexistentSchemaNode, NonexistentInstance) as e:
217 218 219 220 221 222
            http_resp = HttpResponse.error(
                HttpStatus.NotFound,
                RestconfErrType.Protocol,
                ERRTAG_INVVALUE,
                exception=e
            )
223
        except (InstanceValueError, ValueError) as e:
224 225 226 227 228 229
            http_resp = HttpResponse.error(
                HttpStatus.BadRequest,
                RestconfErrType.Protocol,
                ERRTAG_INVVALUE,
                exception=e
            )
230
        except (ConfHandlerFailedError, NoHandlerError, KnotError, YangsonException) as e:
231 232 233 234 235 236
            http_resp = HttpResponse.error(
                HttpStatus.InternalServerError,
                RestconfErrType.Protocol,
                ERRTAG_OPFAILED,
                exception=e
            )
237 238 239
        finally:
            ds.unlock_data()

240 241 242 243 244 245 246 247 248 249
        if http_resp is not None:
            # Return error response
            return http_resp

        hdr_inm = req_headers.get("if-none-match")
        n_etag = str(hash(n.value))

        if (hdr_inm is not None) and (hdr_inm == n_etag):
            http_resp = HttpResponse.empty(HttpStatus.NotModified)
        else:
250 251
            n_value = n.raw_value()

252 253 254 255 256 257 258
            if isinstance(n, RootNode):
                # Getting top-level node
                restconf_env = "ietf-restconf:data"
                restconf_n_value = {restconf_env: n_value}
            else:
                sn = n.schema_node
                if isinstance(sn, (ContainerNode, GroupNode, LeafNode)):
259 260
                    restconf_env = "{}:{}".format(sn.qual_name[1], sn.qual_name[0])
                    restconf_n_value = {restconf_env: n_value}
261
                elif isinstance(sn, ListNode):
262 263 264 265 266 267 268 269 270 271
                    restconf_env = "{}:{}".format(sn.qual_name[1], sn.qual_name[0])
                    if isinstance(n_value, list):
                        # List and list item points to the same schema node
                        restconf_n_value = {restconf_env: n_value}
                    else:
                        restconf_n_value = {restconf_env: [n_value]}
                else:
                    raise HttpRequestError()

            response = json.dumps(restconf_n_value, indent=4)
272 273

            add_headers = OrderedDict()
274
            add_headers["ETag"] = n_etag
275 276 277 278 279 280 281 282
            try:
                lm_time = DateTimeHelpers.to_httpdate_str(n.value.timestamp, CONFIG_GLOBAL["TIMEZONE"])
                add_headers["Last-Modified"] = lm_time
            except AttributeError:
                # Only arrays and objects have last_modified attribute
                pass

            http_resp = HttpResponse(HttpStatus.Ok, response.encode(), CT_YANG_JSON, extra_headers=add_headers)
283

Pavel Spirek's avatar
Pavel Spirek committed
284
    except DataLockError as e:
285 286 287 288 289 290
        http_resp = HttpResponse.error(
            HttpStatus.Conflict,
            RestconfErrType.Protocol,
            ERRTAG_LOCKDENIED,
            exception=e
        )
291
    except HttpRequestError as e:
292 293 294 295 296 297
        http_resp = HttpResponse.error(
            HttpStatus.BadRequest,
            RestconfErrType.Protocol,
            ERRTAG_INVVALUE,
            exception=e
        )
298 299

    return http_resp
Pavel Spirek's avatar
Pavel Spirek committed
300 301


302
def create_get_api(ds: BaseDatastore):
303 304
    def get_api_closure(headers: OrderedDict, data: Optional[str], client_cert: SSLCertT) -> HttpResponse:
        username = CertHelpers.get_field(client_cert, "emailAddress")
Pavel Spirek's avatar
Pavel Spirek committed
305
        info("[{}] api_get: {}".format(username, headers[":path"]))
Pavel Spirek's avatar
Pavel Spirek committed
306

307
        api_pth = headers[":path"][len(API_ROOT_data):]
308
        http_resp = _get(ds, headers, api_pth, username)
309
        return http_resp
Pavel Spirek's avatar
Pavel Spirek committed
310

311
    return get_api_closure
Pavel Spirek's avatar
Pavel Spirek committed
312 313


314
def create_get_staging_api(ds: BaseDatastore):
315 316
    def get_staging_api_closure(headers: OrderedDict, data: Optional[str], client_cert: SSLCertT) -> HttpResponse:
        username = CertHelpers.get_field(client_cert, "emailAddress")
317 318 319
        info("[{}] api_get_staging: {}".format(username, headers[":path"]))

        api_pth = headers[":path"][len(API_ROOT_STAGING_data):]
320
        http_resp = _get(ds, headers, api_pth, username, staging=True)
321
        return http_resp
322 323 324 325

    return get_staging_api_closure


326
def get_file(headers: OrderedDict, data: Optional[str], client_cert: SSLCertT) -> HttpResponse:
Pavel Spirek's avatar
Pavel Spirek committed
327
    # Ordinary file on filesystem
328
    username = CertHelpers.get_field(client_cert, "emailAddress")
329 330 331
    url_path = headers[":path"].split("?")[0]
    url_path_safe = "".join(filter(lambda c: c.isalpha() or c in "/-_.", url_path)).replace("..", "").strip("/")
    file_path = os.path.join(CONFIG_HTTP["DOC_ROOT"], url_path_safe)
Pavel Spirek's avatar
Pavel Spirek committed
332 333 334 335

    if os.path.isdir(file_path):
        file_path = os.path.join(file_path, CONFIG_HTTP["DOC_DEFAULT_NAME"])

336
    ctype = mimetypes.guess_type(file_path)[0] or "application/octet-stream"
Pavel Spirek's avatar
Pavel Spirek committed
337 338 339 340 341 342

    try:
        fd = open(file_path, 'rb')
        response = fd.read()
        fd.close()
    except FileNotFoundError:
Pavel Spirek's avatar
Pavel Spirek committed
343
        warn("[{}] Cannot open requested file \"{}\"".format(username, file_path))
344 345 346 347
        http_resp = HttpResponse.empty(HttpStatus.NotFound)
    else:
        info("[{}] Serving ordinary file {} of type \"{}\"".format(username, file_path, ctype))
        http_resp = HttpResponse(HttpStatus.Ok, response, ctype)
Pavel Spirek's avatar
Pavel Spirek committed
348

349
    return http_resp
Pavel Spirek's avatar
Pavel Spirek committed
350 351


352
def _post(ds: BaseDatastore, pth: str, username: str, data: str) -> HttpResponse:
353
    debug_httph("HTTP data received: " + data)
Pavel Spirek's avatar
Pavel Spirek committed
354

355 356 357 358 359 360 361
    url_split = pth.split("?")
    url_path = url_split[0]
    if len(url_split) > 1:
        query_string = parse_qs(url_split[1])
    else:
        query_string = {}

362
    rpc1 = RpcInfo()
363
    rpc1.username = username
364
    rpc1.path = url_path.rstrip("/")
365
    rpc1.qs = query_string
366

Pavel Spirek's avatar
Pavel Spirek committed
367
    try:
368
        json_data = json.loads(data) if len(data) > 0 else {}
Pavel Spirek's avatar
Pavel Spirek committed
369
    except ValueError as e:
Pavel Spirek's avatar
Pavel Spirek committed
370
        error("Failed to parse POST data: " + epretty(e))
371 372 373 374 375 376
        return HttpResponse.error(
            HttpStatus.BadRequest,
            RestconfErrType.Protocol,
            ERRTAG_INVVALUE,
            exception=e
        )
377 378 379

    try:
        ds.lock_data(username)
380 381 382

        try:
            new_root = ds.create_node_rpc(ds.get_data_root_staging(rpc1.username), rpc1, json_data)
383
            ds.add_to_journal_rpc(ChangeType.CREATE, rpc1, json_data, *new_root)
384 385 386 387 388 389 390 391 392 393 394 395 396 397 398 399 400 401 402 403 404 405 406 407 408 409 410 411 412 413 414 415 416 417 418 419
            http_resp = HttpResponse.empty(HttpStatus.Created)
        except NacmForbiddenError as e:
            http_resp = HttpResponse.error(
                HttpStatus.Forbidden,
                RestconfErrType.Protocol,
                ERRTAG_ACCDENIED,
                exception=e
            )
        except (NonexistentSchemaNode, NonexistentInstance) as e:
            http_resp = HttpResponse.error(
                HttpStatus.NotFound,
                RestconfErrType.Protocol,
                ERRTAG_INVVALUE,
                exception=e
            )
        except NoHandlerError as e:
            http_resp = HttpResponse.error(
                HttpStatus.BadRequest,
                RestconfErrType.Protocol,
                ERRTAG_OPNOTSUPPORTED,
                exception=e
            )
        except (InstanceValueError, YangTypeError, ValueError) as e:
            http_resp = HttpResponse.error(
                HttpStatus.BadRequest,
                RestconfErrType.Protocol,
                ERRTAG_INVVALUE,
                exception=e
            )
        except InstanceAlreadyPresent as e:
            http_resp = HttpResponse.error(
                HttpStatus.Conflict,
                RestconfErrType.Protocol,
                ERRTAG_EXISTS,
                exception=e
            )
420
    except DataLockError as e:
421 422 423 424 425 426
        http_resp = HttpResponse.error(
            HttpStatus.Conflict,
            RestconfErrType.Protocol,
            ERRTAG_LOCKDENIED,
            exception=e
        )
427 428 429
    finally:
        ds.unlock_data()

430 431
    return http_resp

432 433

def create_post_api(ds: BaseDatastore):
434 435
    def post_api_closure(headers: OrderedDict, data: Optional[str], client_cert: SSLCertT) -> HttpResponse:
        username = CertHelpers.get_field(client_cert, "emailAddress")
Pavel Spirek's avatar
Pavel Spirek committed
436
        info("[{}] api_post: {}".format(username, headers[":path"]))
437

438
        api_pth = headers[":path"][len(API_ROOT_data):]
439
        http_resp = _post(ds, api_pth, username, data)
440
        return http_resp
441 442 443 444

    return post_api_closure


445
def _put(ds: BaseDatastore, pth: str, username: str, data: str) -> HttpResponse:
446
    debug_httph("HTTP data received: " + data)
447 448 449 450

    url_split = pth.split("?")
    url_path = url_split[0]

451
    rpc1 = RpcInfo()
452
    rpc1.username = username
453
    rpc1.path = url_path.rstrip("/")
454

Pavel Spirek's avatar
Pavel Spirek committed
455
    try:
456
        json_data = json.loads(data) if len(data) > 0 else {}
Pavel Spirek's avatar
Pavel Spirek committed
457 458
    except ValueError as e:
        error("Failed to parse PUT data: " + epretty(e))
459 460 461 462 463 464
        return HttpResponse.error(
            HttpStatus.BadRequest,
            RestconfErrType.Protocol,
            ERRTAG_INVVALUE,
            exception=e
        )
465 466 467

    try:
        ds.lock_data(username)
468 469 470

        try:
            new_root = ds.update_node_rpc(ds.get_data_root_staging(rpc1.username), rpc1, json_data)
471
            ds.add_to_journal_rpc(ChangeType.REPLACE, rpc1, json_data, *new_root)
472 473 474 475 476 477 478 479 480 481 482 483 484 485 486 487 488 489 490 491 492 493
            http_resp = HttpResponse.empty(HttpStatus.NoContent, status_in_body=False)
        except NacmForbiddenError as e:
            http_resp = HttpResponse.error(
                HttpStatus.Forbidden,
                RestconfErrType.Protocol,
                ERRTAG_ACCDENIED,
                exception=e
            )
        except (NonexistentSchemaNode, NonexistentInstance) as e:
            http_resp = HttpResponse.error(
                HttpStatus.NotFound,
                RestconfErrType.Protocol,
                ERRTAG_INVVALUE,
                exception=e
            )
        except NoHandlerError as e:
            http_resp = HttpResponse.error(
                HttpStatus.BadRequest,
                RestconfErrType.Protocol,
                ERRTAG_OPNOTSUPPORTED,
                exception=e
            )
494
    except DataLockError as e:
495 496 497 498 499 500
        http_resp = HttpResponse.error(
            HttpStatus.Conflict,
            RestconfErrType.Protocol,
            ERRTAG_LOCKDENIED,
            exception=e
        )
501 502 503
    finally:
        ds.unlock_data()

504 505
    return http_resp

506 507

def create_put_api(ds: BaseDatastore):
508 509
    def put_api_closure(headers: OrderedDict, data: Optional[str], client_cert: SSLCertT) -> HttpResponse:
        username = CertHelpers.get_field(client_cert, "emailAddress")
Pavel Spirek's avatar
Pavel Spirek committed
510
        info("[{}] api_put: {}".format(username, headers[":path"]))
511

512
        api_pth = headers[":path"][len(API_ROOT_data):]
513
        http_resp = _put(ds, api_pth, username, data)
514
        return http_resp
515 516 517

    return put_api_closure

Pavel Spirek's avatar
Pavel Spirek committed
518

519
def _delete(ds: BaseDatastore, pth: str, username: str) -> HttpResponse:
520 521
        url_split = pth.split("?")
        url_path = url_split[0]
Pavel Spirek's avatar
Pavel Spirek committed
522

523
        rpc1 = RpcInfo()
Pavel Spirek's avatar
Pavel Spirek committed
524
        rpc1.username = username
525
        rpc1.path = url_path.rstrip("/")
Pavel Spirek's avatar
Pavel Spirek committed
526 527

        try:
528
            ds.lock_data(username)
529 530 531

            try:
                new_root = ds.delete_node_rpc(ds.get_data_root_staging(rpc1.username), rpc1)
532
                ds.add_to_journal_rpc(ChangeType.DELETE, rpc1, None, *new_root)
533 534 535 536 537 538 539 540 541 542 543 544 545 546 547 548 549 550 551 552 553 554
                http_resp = HttpResponse.empty(HttpStatus.NoContent, status_in_body=False)
            except NacmForbiddenError as e:
                http_resp = HttpResponse.error(
                    HttpStatus.Forbidden,
                    RestconfErrType.Protocol,
                    ERRTAG_ACCDENIED,
                    exception=e
                )
            except (NonexistentSchemaNode, NonexistentInstance) as e:
                http_resp = HttpResponse.error(
                    HttpStatus.NotFound,
                    RestconfErrType.Protocol,
                    ERRTAG_INVVALUE,
                    exception=e
                )
            except NoHandlerError as e:
                http_resp = HttpResponse.error(
                    HttpStatus.BadRequest,
                    RestconfErrType.Protocol,
                    ERRTAG_OPNOTSUPPORTED,
                    exception=e
                )
Pavel Spirek's avatar
Pavel Spirek committed
555
        except DataLockError as e:
556 557 558 559 560 561
            http_resp = HttpResponse.error(
                HttpStatus.Conflict,
                RestconfErrType.Protocol,
                ERRTAG_LOCKDENIED,
                exception=e
            )
562
        finally:
563
            ds.unlock_data()
Pavel Spirek's avatar
Pavel Spirek committed
564

565 566
        return http_resp

567 568

def create_api_delete(ds: BaseDatastore):
569 570
    def api_delete_closure(headers: OrderedDict, data: Optional[str], client_cert: SSLCertT) -> HttpResponse:
        username = CertHelpers.get_field(client_cert, "emailAddress")
Pavel Spirek's avatar
Pavel Spirek committed
571
        info("[{}] api_delete: {}".format(username, headers[":path"]))
572

573
        api_pth = headers[":path"][len(API_ROOT_data):]
574
        http_resp = _delete(ds, api_pth, username)
575
        return http_resp
576 577

    return api_delete_closure
578 579 580


def create_api_op(ds: BaseDatastore):
581 582
    def api_op_closure(headers: OrderedDict, data: Optional[str], client_cert: SSLCertT) -> HttpResponse:
        username = CertHelpers.get_field(client_cert, "emailAddress")
Pavel Spirek's avatar
Pavel Spirek committed
583
        info("[{}] invoke_op: {}".format(username, headers[":path"]))
584

585
        api_pth = headers[":path"][len(API_ROOT_ops):]
586
        op_name_fq = api_pth[1:].split("/", maxsplit=1)[0]
587

588
        try:
589 590 591 592 593 594 595 596
            ns, sel1 = op_name_fq.split(":", maxsplit=1)
        except ValueError:
            return HttpResponse.error(
                HttpStatus.BadRequest,
                RestconfErrType.Protocol,
                ERRTAG_MALFORMED,
                "Operation name must be in fully-qualified format"
            )
597 598

        try:
599
            json_data = json.loads(data) if len(data) > 0 else {}
600
        except ValueError as e:
601 602 603 604 605 606
            return HttpResponse.error(
                HttpStatus.BadRequest,
                RestconfErrType.Protocol,
                ERRTAG_MALFORMED,
                "Failed to parse POST data: " + epretty(e)
            )
607 608 609

        input_args = json_data.get(ns + ":input")

610
        rpc1 = RpcInfo()
611 612
        rpc1.username = username
        rpc1.path = api_pth
613
        rpc1.op_name = op_name_fq
614 615 616
        rpc1.op_input_args = input_args

        # Skip NACM check for privileged users
Pavel Spirek's avatar
Pavel Spirek committed
617
        if username in CONFIG_NACM["ALLOWED_USERS"]:
618 619 620 621 622
            rpc1.skip_nacm_check = True

        try:
            ret_data = ds.invoke_op_rpc(rpc1)
            if ret_data is None:
623
                http_resp = HttpResponse.empty(HttpStatus.NoContent, status_in_body=False)
624
            else:
625 626 627 628
                if not isinstance(ret_data, str):
                    response = json.dumps(ret_data, indent=4)
                else:
                    response = ret_data
629
                http_resp = HttpResponse(HttpStatus.Ok, response.encode(), CT_YANG_JSON)
630
        except NacmForbiddenError as e:
631 632 633 634 635 636
            http_resp = HttpResponse.error(
                HttpStatus.Forbidden,
                RestconfErrType.Protocol,
                ERRTAG_ACCDENIED,
                exception=e
            )
637
        except NonexistentSchemaNode as e:
638 639 640 641 642 643 644 645 646 647 648 649 650 651 652 653 654 655 656 657
            http_resp = HttpResponse.error(
                HttpStatus.NotFound,
                RestconfErrType.Protocol,
                ERRTAG_INVVALUE,
                exception=e
            )
        except InstanceAlreadyPresent as e:
            http_resp = HttpResponse.error(
                HttpStatus.Conflict,
                RestconfErrType.Protocol,
                ERRTAG_EXISTS,
                exception=e
            )
        except NoHandlerForOpError as e:
            http_resp = HttpResponse.error(
                HttpStatus.BadRequest,
                RestconfErrType.Protocol,
                ERRTAG_OPNOTSUPPORTED,
                exception=e
            )
658
        except (ConfHandlerFailedError, OpHandlerFailedError) as e:
659 660 661 662 663 664
            http_resp = HttpResponse.error(
                HttpStatus.InternalServerError,
                RestconfErrType.Protocol,
                ERRTAG_OPFAILED,
                exception=e
            )
665 666 667 668 669 670 671
        except (SchemaError, SemanticError) as e:
            http_resp = HttpResponse.error(
                HttpStatus.BadRequest,
                RestconfErrType.Protocol,
                ERRTAG_INVVALUE,
                exception=e
            )
672 673 674 675 676 677 678
        except ValueError as e:
            http_resp = HttpResponse.error(
                HttpStatus.BadRequest,
                RestconfErrType.Protocol,
                ERRTAG_INVVALUE,
                exception=e
            )
679 680

        return http_resp
681 682

    return api_op_closure
683 684 685 686 687 688 689 690 691 692


def options_api(headers: OrderedDict, data: Optional[str], client_cert: SSLCertT) -> HttpResponse:
    info("api_options: {}".format(headers[":path"]))
    headers_extra = OrderedDict()
    headers_extra["Allow"] = "GET, PUT, POST, OPTIONS, HEAD, DELETE"
    http_resp = HttpResponse(HttpStatus.Ok, bytes(), CT_PLAIN, extra_headers=headers_extra)

    return http_resp