http_handlers.py 24.9 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

16
from .config import CONFIG_GLOBAL, CONFIG_HTTP, CONFIG_NACM, API_ROOT_data, API_ROOT_RUNNING_data, API_ROOT_ops
17
from .helpers import CertHelpers, DateTimeHelpers, ErrorHelpers, LogHelpers, SSLCertT
18
from .errors import BackendError
Pavel Spirek's avatar
Pavel Spirek committed
19
from .nacm import NacmForbiddenError
20 21 22 23 24 25 26
from .data import (
    BaseDatastore,
    RpcInfo,
    DataLockError,
    NoHandlerError,
    NoHandlerForOpError,
    InstanceAlreadyPresent,
27
    ChangeType,
28
    ConfHandlerFailedError,
Pavel Spirek's avatar
Pavel Spirek committed
29 30
    OpHandlerFailedError,
    StagingDataException
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
    @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

124 125 126 127
            try:
                err_body["error-message"] = exception.message
            except AttributeError:
                err_body["error-message"] = str(exception)
128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148

        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)

149

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

158

159
def _get_yl_date() -> str:
160 161 162 163 164 165
    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

166 167 168 169 170 171
    return yang_lib_date


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

172 173 174 175
    top_res = {
        "ietf-restconf:restconf": {
            "data": {},
            "operations": {},
176
            "yang-library-version": _get_yl_date()
177 178
        }
    }
179

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


184 185 186 187 188 189 190 191 192
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)


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

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

206 207 208 209
    # Skip NACM check for privileged users
    if username in CONFIG_NACM["ALLOWED_USERS"]:
        rpc1.skip_nacm_check = True

Pavel Spirek's avatar
Pavel Spirek committed
210 211
    try:
        ds.lock_data(username)
212
        http_resp = None
213

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

247 248 249 250 251 252 253 254 255 256
        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:
257 258
            n_value = n.raw_value()

259 260 261 262 263 264 265
            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)):
266 267
                    restconf_env = "{}:{}".format(sn.qual_name[1], sn.qual_name[0])
                    restconf_n_value = {restconf_env: n_value}
268
                elif isinstance(sn, ListNode):
269 270 271 272 273 274 275 276 277 278
                    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)
279 280

            add_headers = OrderedDict()
281
            add_headers["ETag"] = n_etag
282 283 284 285 286 287 288 289
            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)
290

Pavel Spirek's avatar
Pavel Spirek committed
291
    except DataLockError as e:
292 293 294 295 296 297
        http_resp = HttpResponse.error(
            HttpStatus.Conflict,
            RestconfErrType.Protocol,
            ERRTAG_LOCKDENIED,
            exception=e
        )
298
    except HttpRequestError as e:
299 300 301 302 303 304
        http_resp = HttpResponse.error(
            HttpStatus.BadRequest,
            RestconfErrType.Protocol,
            ERRTAG_INVVALUE,
            exception=e
        )
305 306

    return http_resp
Pavel Spirek's avatar
Pavel Spirek committed
307 308


309
def create_get_api(ds: BaseDatastore):
310 311
    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
312
        info("[{}] api_get: {}".format(username, headers[":path"]))
Pavel Spirek's avatar
Pavel Spirek committed
313

314
        api_pth = headers[":path"][len(API_ROOT_data):]
315
        http_resp = _get(ds, headers, api_pth, username, staging=True)
316
        return http_resp
Pavel Spirek's avatar
Pavel Spirek committed
317

318
    return get_api_closure
Pavel Spirek's avatar
Pavel Spirek committed
319 320


321
def create_get_running_api(ds: BaseDatastore):
322 323
    def get_staging_api_closure(headers: OrderedDict, data: Optional[str], client_cert: SSLCertT) -> HttpResponse:
        username = CertHelpers.get_field(client_cert, "emailAddress")
324 325
        info("[{}] api_get_staging: {}".format(username, headers[":path"]))

326 327
        api_pth = headers[":path"][len(API_ROOT_RUNNING_data):]
        http_resp = _get(ds, headers, api_pth, username, staging=False)
328
        return http_resp
329 330 331 332

    return get_staging_api_closure


333
def get_file(headers: OrderedDict, data: Optional[str], client_cert: SSLCertT) -> HttpResponse:
Pavel Spirek's avatar
Pavel Spirek committed
334
    # Ordinary file on filesystem
335
    username = CertHelpers.get_field(client_cert, "emailAddress")
336 337 338
    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
339 340 341 342

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

343
    ctype = mimetypes.guess_type(file_path)[0] or "application/octet-stream"
Pavel Spirek's avatar
Pavel Spirek committed
344 345 346 347 348 349

    try:
        fd = open(file_path, 'rb')
        response = fd.read()
        fd.close()
    except FileNotFoundError:
Pavel Spirek's avatar
Pavel Spirek committed
350
        warn("[{}] Cannot open requested file \"{}\"".format(username, file_path))
351 352 353 354
        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
355

356
    return http_resp
Pavel Spirek's avatar
Pavel Spirek committed
357 358


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

362 363 364 365 366 367 368
    url_split = pth.split("?")
    url_path = url_split[0]
    if len(url_split) > 1:
        query_string = parse_qs(url_split[1])
    else:
        query_string = {}

369
    rpc1 = RpcInfo()
370
    rpc1.username = username
371
    rpc1.path = url_path.rstrip("/")
372
    rpc1.qs = query_string
373

374 375 376 377
    # Skip NACM check for privileged users
    if username in CONFIG_NACM["ALLOWED_USERS"]:
        rpc1.skip_nacm_check = True

Pavel Spirek's avatar
Pavel Spirek committed
378
    try:
379
        json_data = json.loads(data) if len(data) > 0 else {}
Pavel Spirek's avatar
Pavel Spirek committed
380
    except ValueError as e:
Pavel Spirek's avatar
Pavel Spirek committed
381
        error("Failed to parse POST data: " + epretty(e))
382 383 384 385 386 387
        return HttpResponse.error(
            HttpStatus.BadRequest,
            RestconfErrType.Protocol,
            ERRTAG_INVVALUE,
            exception=e
        )
388 389 390

    try:
        ds.lock_data(username)
391 392

        try:
393 394 395 396 397 398 399
            try:
                staging_root = ds.get_data_root_staging(rpc1.username)
            except StagingDataException:
                info("Starting transaction for user \"{}\"".format(rpc1.username))
                ds.make_user_journal(rpc1.username, None)
                staging_root = ds.get_data_root_staging(rpc1.username)
            new_root = ds.create_node_rpc(staging_root, rpc1, json_data)
400
            ds.add_to_journal_rpc(ChangeType.CREATE, rpc1, json_data, *new_root)
401 402 403 404 405 406 407 408 409 410 411 412 413 414 415 416 417 418 419 420 421 422
            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
            )
423
        except (InstanceValueError, YangsonException, ValueError) as e:
424 425 426 427 428 429 430 431 432 433 434 435 436
            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
            )
437
    except DataLockError as e:
438 439 440 441 442 443
        http_resp = HttpResponse.error(
            HttpStatus.Conflict,
            RestconfErrType.Protocol,
            ERRTAG_LOCKDENIED,
            exception=e
        )
444 445 446
    finally:
        ds.unlock_data()

447 448
    return http_resp

449 450

def create_post_api(ds: BaseDatastore):
451 452
    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
453
        info("[{}] api_post: {}".format(username, headers[":path"]))
454

455
        api_pth = headers[":path"][len(API_ROOT_data):]
456
        http_resp = _post(ds, api_pth, username, data)
457
        return http_resp
458 459 460 461

    return post_api_closure


462
def _put(ds: BaseDatastore, pth: str, username: str, data: str) -> HttpResponse:
463
    debug_httph("HTTP data received: " + data)
464 465 466 467

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

468
    rpc1 = RpcInfo()
469
    rpc1.username = username
470
    rpc1.path = url_path.rstrip("/")
471

472 473 474 475
    # Skip NACM check for privileged users
    if username in CONFIG_NACM["ALLOWED_USERS"]:
        rpc1.skip_nacm_check = True

Pavel Spirek's avatar
Pavel Spirek committed
476
    try:
477
        json_data = json.loads(data) if len(data) > 0 else {}
Pavel Spirek's avatar
Pavel Spirek committed
478 479
    except ValueError as e:
        error("Failed to parse PUT data: " + epretty(e))
480 481 482 483 484 485
        return HttpResponse.error(
            HttpStatus.BadRequest,
            RestconfErrType.Protocol,
            ERRTAG_INVVALUE,
            exception=e
        )
486 487 488

    try:
        ds.lock_data(username)
489 490

        try:
491 492 493 494 495 496 497
            try:
                staging_root = ds.get_data_root_staging(rpc1.username)
            except StagingDataException:
                info("Starting transaction for user \"{}\"".format(rpc1.username))
                ds.make_user_journal(rpc1.username, None)
                staging_root = ds.get_data_root_staging(rpc1.username)
            new_root = ds.update_node_rpc(staging_root, rpc1, json_data)
498
            ds.add_to_journal_rpc(ChangeType.REPLACE, rpc1, json_data, *new_root)
499 500 501 502 503 504 505 506 507 508 509 510 511 512 513 514 515 516 517 518 519 520
            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
            )
521 522 523 524 525 526 527
        except (InstanceValueError, StagingDataException, YangsonException, ValueError) as e:
            http_resp = HttpResponse.error(
                HttpStatus.BadRequest,
                RestconfErrType.Protocol,
                ERRTAG_INVVALUE,
                exception=e
            )
528
    except DataLockError as e:
529 530 531 532 533 534
        http_resp = HttpResponse.error(
            HttpStatus.Conflict,
            RestconfErrType.Protocol,
            ERRTAG_LOCKDENIED,
            exception=e
        )
535 536 537
    finally:
        ds.unlock_data()

538 539
    return http_resp

540 541

def create_put_api(ds: BaseDatastore):
542 543
    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
544
        info("[{}] api_put: {}".format(username, headers[":path"]))
545

546
        api_pth = headers[":path"][len(API_ROOT_data):]
547
        http_resp = _put(ds, api_pth, username, data)
548
        return http_resp
549 550 551

    return put_api_closure

Pavel Spirek's avatar
Pavel Spirek committed
552

553
def _delete(ds: BaseDatastore, pth: str, username: str) -> HttpResponse:
554 555
        url_split = pth.split("?")
        url_path = url_split[0]
Pavel Spirek's avatar
Pavel Spirek committed
556

557
        rpc1 = RpcInfo()
Pavel Spirek's avatar
Pavel Spirek committed
558
        rpc1.username = username
559
        rpc1.path = url_path.rstrip("/")
Pavel Spirek's avatar
Pavel Spirek committed
560

561 562 563 564
        # Skip NACM check for privileged users
        if username in CONFIG_NACM["ALLOWED_USERS"]:
            rpc1.skip_nacm_check = True

Pavel Spirek's avatar
Pavel Spirek committed
565
        try:
566
            ds.lock_data(username)
567 568

            try:
569 570 571 572 573 574 575
                try:
                    staging_root = ds.get_data_root_staging(rpc1.username)
                except StagingDataException:
                    info("Starting transaction for user \"{}\"".format(rpc1.username))
                    ds.make_user_journal(rpc1.username, None)
                    staging_root = ds.get_data_root_staging(rpc1.username)
                new_root = ds.delete_node_rpc(staging_root, rpc1)
576
                ds.add_to_journal_rpc(ChangeType.DELETE, rpc1, None, *new_root)
577 578 579 580 581 582 583 584 585 586 587 588 589 590 591 592 593 594 595 596 597 598
                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
                )
599 600 601 602 603 604 605
            except (InstanceValueError, StagingDataException, YangsonException) as e:
                http_resp = HttpResponse.error(
                    HttpStatus.BadRequest,
                    RestconfErrType.Protocol,
                    ERRTAG_INVVALUE,
                    exception=e
                )
Pavel Spirek's avatar
Pavel Spirek committed
606
        except DataLockError as e:
607 608 609 610 611 612
            http_resp = HttpResponse.error(
                HttpStatus.Conflict,
                RestconfErrType.Protocol,
                ERRTAG_LOCKDENIED,
                exception=e
            )
613
        finally:
614
            ds.unlock_data()
Pavel Spirek's avatar
Pavel Spirek committed
615

616 617
        return http_resp

618 619

def create_api_delete(ds: BaseDatastore):
620 621
    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
622
        info("[{}] api_delete: {}".format(username, headers[":path"]))
623

624
        api_pth = headers[":path"][len(API_ROOT_data):]
625
        http_resp = _delete(ds, api_pth, username)
626
        return http_resp
627 628

    return api_delete_closure
Pavel Spirek's avatar
Pavel Spirek committed
629 630 631


def create_api_op(ds: BaseDatastore):
632 633
    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
634
        info("[{}] invoke_op: {}".format(username, headers[":path"]))
Pavel Spirek's avatar
Pavel Spirek committed
635

636
        api_pth = headers[":path"][len(API_ROOT_ops):]
637
        op_name_fq = api_pth[1:].split("/", maxsplit=1)[0]
Pavel Spirek's avatar
Pavel Spirek committed
638

639
        try:
640 641 642 643 644 645 646 647
            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"
            )
Pavel Spirek's avatar
Pavel Spirek committed
648 649

        try:
650
            json_data = json.loads(data) if len(data) > 0 else {}
Pavel Spirek's avatar
Pavel Spirek committed
651
        except ValueError as e:
652 653 654 655 656 657
            return HttpResponse.error(
                HttpStatus.BadRequest,
                RestconfErrType.Protocol,
                ERRTAG_MALFORMED,
                "Failed to parse POST data: " + epretty(e)
            )
Pavel Spirek's avatar
Pavel Spirek committed
658 659 660

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

661
        rpc1 = RpcInfo()
662 663
        rpc1.username = username
        rpc1.path = api_pth
664
        rpc1.op_name = op_name_fq
665 666 667
        rpc1.op_input_args = input_args

        # Skip NACM check for privileged users
Pavel Spirek's avatar
Pavel Spirek committed
668
        if username in CONFIG_NACM["ALLOWED_USERS"]:
669 670 671 672 673
            rpc1.skip_nacm_check = True

        try:
            ret_data = ds.invoke_op_rpc(rpc1)
            if ret_data is None:
674
                http_resp = HttpResponse.empty(HttpStatus.NoContent, status_in_body=False)
675
            else:
676 677 678 679
                if not isinstance(ret_data, str):
                    response = json.dumps(ret_data, indent=4)
                else:
                    response = ret_data
680
                http_resp = HttpResponse(HttpStatus.Ok, response.encode(), CT_YANG_JSON)
681
        except NacmForbiddenError as e:
682 683 684 685 686 687
            http_resp = HttpResponse.error(
                HttpStatus.Forbidden,
                RestconfErrType.Protocol,
                ERRTAG_ACCDENIED,
                exception=e
            )
688
        except (NonexistentSchemaNode, NonexistentInstance) as e:
689 690 691 692 693 694 695 696 697 698 699 700 701 702 703 704 705 706 707 708
            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
            )
709
        except (SchemaError, SemanticError) as e:
710
            http_resp = HttpResponse.error(
711
                HttpStatus.BadRequest,
712
                RestconfErrType.Protocol,
713
                ERRTAG_INVVALUE,
714 715
                exception=e
            )
Pavel Spirek's avatar
Pavel Spirek committed
716
        except (ConfHandlerFailedError, OpHandlerFailedError, StagingDataException, YangsonException) as e:
717
            http_resp = HttpResponse.error(
718
                HttpStatus.InternalServerError,
719
                RestconfErrType.Protocol,
720
                ERRTAG_OPFAILED,
721 722
                exception=e
            )
723 724 725 726 727 728 729
        except ValueError as e:
            http_resp = HttpResponse.error(
                HttpStatus.BadRequest,
                RestconfErrType.Protocol,
                ERRTAG_INVVALUE,
                exception=e
            )
730 731

        return http_resp
Pavel Spirek's avatar
Pavel Spirek committed
732 733

    return api_op_closure
734 735 736 737 738 739 740 741 742 743


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