journal.py 5.64 KB
Newer Older
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113
from enum import Enum
from colorlog import error, info
from typing import List, Dict

from yangson.enumerations import ContentType, ValidationScope
from yangson.schemanode import SchemaError, SemanticError
from yangson.instvalue import ObjectValue
from yangson.instance import InstanceNode

from . import config
from .helpers import ErrorHelpers, LogHelpers, PathFormat, JsonNodeT
from .errors import ConfHandlerFailedError

epretty = ErrorHelpers.epretty
debug_journal = LogHelpers.create_module_dbg_logger(__name__)


class ChangeType(Enum):
    CREATE = 0,
    REPLACE = 1,
    DELETE = 2


class RpcInfo:
    def __init__(self):
        self.username = None    # type: str
        self.path = None        # type: str
        self.qs = None          # type: Dict[str, List[str]]
        self.path_format = PathFormat.URL   # type: PathFormat
        self.skip_nacm_check = False        # type: bool
        self.op_name = None                 # type: str
        self.op_input_args = None           # type: ObjectValue


class DataChange:
    def __init__(self, change_type: ChangeType, rpc_info: RpcInfo, input_data: JsonNodeT, root_after_change: InstanceNode, nacm_modified: bool):
        self.change_type = change_type
        self.rpc_info = rpc_info
        self.input_data = input_data
        self.root_after_change = root_after_change
        self.nacm_modified = nacm_modified


class UsrChangeJournal:
    def __init__(self, root_origin: InstanceNode):
        self._root_origin = root_origin
        self._journal = []  # type: List[DataChange]

    def get_root_head(self) -> InstanceNode:
        if len(self._journal) > 0:
            return self._journal[-1].root_after_change
        else:
            return self._root_origin

    def get_root_origin(self) -> InstanceNode:
        return self._root_origin

    def add(self, change: DataChange):
        self._journal.append(change)

    def list(self) -> JsonNodeT:
        changes_info = []
        for ch in self._journal:
            changes_info.append([ch.change_type.name, ch.rpc_info.path])

        return changes_info

    def commit(self, ds: "BaseDatastore") -> bool:
        nacm_modified = False

        if len(self._journal) == 0:
            return False

        if hash(ds.get_data_root()) == hash(self._root_origin):
            info("Commiting new configuration (swapping roots)")
            # Set new root
            nr = self.get_root_head()

            for change in self._journal:
                nacm_modified = nacm_modified or change.nacm_modified
        else:
            info("Commiting new configuration (re-applying changes)")
            nr = ds.get_data_root()

            for change in self._journal:
                nacm_modified = nacm_modified or change.nacm_modified

                if change.change_type == ChangeType.CREATE:
                    nr = ds.create_node_rpc(nr, change.rpc_info, change.input_data)[0]
                elif change.change_type == ChangeType.REPLACE:
                    nr = ds.update_node_rpc(nr, change.rpc_info, change.input_data)[0]
                elif change.change_type == ChangeType.DELETE:
                    nr = ds.delete_node_rpc(nr, change.rpc_info)[0]

        try:
            # Validate syntax and semantics of new data
            if config.CFG.glob["VALIDATE_TRANSACTIONS"] is True:
                nr.validate(ValidationScope.all, ContentType.config)
        except (SchemaError, SemanticError) as e:
            error("Data validation error:")
            error(epretty(e))
            raise e

        # Set new data root
        ds.set_data_root(nr)

        # Update NACM if NACM data has been affected by any edit
        if nacm_modified and ds.nacm is not None:
            ds.nacm.update()

        # Call commit begin hook
        begin_hook_failed = False
        try:
Pavel Spirek's avatar
Pavel Spirek committed
114
            ds.handlers.commit_begin()
115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134
        except Exception as e:
            error("Exception occured in commit_begin handler: {}".format(epretty(e)))
            begin_hook_failed = True

        # Run schema node handlers
        conf_handler_failed = False
        if not begin_hook_failed:
            try:
                for change in self._journal:
                    ii = ds.parse_ii(change.rpc_info.path, change.rpc_info.path_format)
                    ds.run_conf_edit_handler(ii, change)
            except Exception as e:
                error("Exception occured in edit handler: {}".format(epretty(e)))
                conf_handler_failed = True

        # Call commit end hook
        end_hook_failed = False
        end_hook_abort_failed = False
        if not (begin_hook_failed or conf_handler_failed):
            try:
Pavel Spirek's avatar
Pavel Spirek committed
135
                ds.handlers.commit_end(failed=False)
136 137 138 139 140 141 142
            except Exception as e:
                error("Exception occured in commit_end handler: {}".format(epretty(e)))
                end_hook_failed = True

        if begin_hook_failed or conf_handler_failed or end_hook_failed:
            try:
                # Call commit_end callback again with "failed" argument set to True
Pavel Spirek's avatar
Pavel Spirek committed
143
                ds.handlers.commit_end(failed=True)
144 145 146 147 148 149 150 151 152 153 154 155 156 157 158
            except Exception as e:
                error("Exception occured in commit_end handler (abort): {}".format(epretty(e)))
                end_hook_abort_failed = True

        # Return to previous version of data and raise an exception if something went wrong
        if begin_hook_failed or conf_handler_failed or end_hook_failed or end_hook_abort_failed:
            ds.data_root_rollback(history_steps=1, store_current=False)

            # Update NACM again after rollback
            if nacm_modified and ds.nacm is not None:
                ds.nacm.update()

            raise ConfHandlerFailedError("(see logged)")

        return True