Blame nmstatectl/nmstatectl.py

Packit Service 0535c1
#
Packit Service 0535c1
# Copyright (c) 2018-2020 Red Hat, Inc.
Packit Service 0535c1
#
Packit Service 0535c1
# This file is part of nmstate
Packit Service 0535c1
#
Packit Service 0535c1
# This program is free software: you can redistribute it and/or modify
Packit Service 0535c1
# it under the terms of the GNU Lesser General Public License as published by
Packit Service 0535c1
# the Free Software Foundation, either version 2.1 of the License, or
Packit Service 0535c1
# (at your option) any later version.
Packit Service 0535c1
#
Packit Service 0535c1
# This program is distributed in the hope that it will be useful,
Packit Service 0535c1
# but WITHOUT ANY WARRANTY; without even the implied warranty of
Packit Service 0535c1
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
Packit Service 0535c1
# GNU Lesser General Public License for more details.
Packit Service 0535c1
#
Packit Service 0535c1
# You should have received a copy of the GNU Lesser General Public License
Packit Service 0535c1
# along with this program. If not, see <https://www.gnu.org/licenses/>.
Packit Service 0535c1
#
Packit Service 0535c1
import argparse
Packit Service 0535c1
import errno
Packit Service 0535c1
import fnmatch
Packit Service 0535c1
import json
Packit Service 0535c1
import logging
Packit Service 0535c1
import os
Packit Service 0535c1
import subprocess
Packit Service 0535c1
import sys
Packit Service 0535c1
import tempfile
Packit Service 0535c1
Packit Service 0535c1
import yaml
Packit Service 0535c1
Packit Service 0535c1
import libnmstate
Packit Service 0535c1
from libnmstate import PrettyState
Packit Service 0535c1
from libnmstate.error import NmstateConflictError
Packit Service 0535c1
from libnmstate.error import NmstatePermissionError
Packit Service 0535c1
from libnmstate.error import NmstateValueError
Packit Service 0535c1
from libnmstate.schema import Interface
Packit Service 0535c1
from libnmstate.schema import Route
Packit Service 0535c1
from libnmstate.schema import RouteRule
Packit Service 0535c1
Packit Service 0535c1
Packit Service 0535c1
def main():
Packit Service 0535c1
    logging.basicConfig(
Packit Service 0535c1
        format="%(asctime)s %(name)-12s %(levelname)-8s %(message)s",
Packit Service 0535c1
        level=logging.DEBUG,
Packit Service 0535c1
    )
Packit Service 0535c1
Packit Service 0535c1
    parser = argparse.ArgumentParser()
Packit Service 0535c1
Packit Service 0535c1
    subparsers = parser.add_subparsers()
Packit Service 0535c1
    setup_subcommand_commit(subparsers)
Packit Service 0535c1
    setup_subcommand_edit(subparsers)
Packit Service 0535c1
    setup_subcommand_rollback(subparsers)
Packit Service 0535c1
    setup_subcommand_set(subparsers)
Packit Service 0535c1
    setup_subcommand_show(subparsers)
Packit Service 0535c1
    setup_subcommand_version(subparsers)
Packit Service 0535c1
    parser.add_argument(
Packit Service 0535c1
        "--version", action="store_true", help="Display nmstate version"
Packit Service 0535c1
    )
Packit Service 0535c1
Packit Service 0535c1
    if len(sys.argv) == 1:
Packit Service 0535c1
        parser.print_usage()
Packit Service 0535c1
        return errno.EINVAL
Packit Service 0535c1
    args = parser.parse_args()
Packit Service 0535c1
    if args.version:
Packit Service 0535c1
        print(libnmstate.__version__)
Packit Service 0535c1
    else:
Packit Service 0535c1
        return args.func(args)
Packit Service 0535c1
Packit Service 0535c1
Packit Service 0535c1
def setup_subcommand_commit(subparsers):
Packit Service 0535c1
    parser_commit = subparsers.add_parser("commit", help="Commit a change")
Packit Service 0535c1
    parser_commit.add_argument(
Packit Service 0535c1
        "checkpoint", nargs="?", default=None, help="checkpoint to commit"
Packit Service 0535c1
    )
Packit Service 0535c1
    parser_commit.set_defaults(func=commit)
Packit Service 0535c1
Packit Service 0535c1
Packit Service 0535c1
def setup_subcommand_edit(subparsers):
Packit Service 0535c1
    parser_edit = subparsers.add_parser(
Packit Service 0535c1
        "edit", help="Edit network state in EDITOR"
Packit Service 0535c1
    )
Packit Service 0535c1
    parser_edit.set_defaults(func=edit)
Packit Service 0535c1
    parser_edit.add_argument(
Packit Service 0535c1
        "--json",
Packit Service 0535c1
        help="Edit as JSON",
Packit Service 0535c1
        default=True,
Packit Service 0535c1
        action="store_false",
Packit Service 0535c1
        dest="yaml",
Packit Service 0535c1
    )
Packit Service 0535c1
    parser_edit.add_argument(
Packit Service 0535c1
        "only",
Packit Service 0535c1
        default="*",
Packit Service 0535c1
        nargs="?",
Packit Service 0535c1
        metavar=Interface.KEY,
Packit Service 0535c1
        help="Edit only specified interfaces (comma-separated)",
Packit Service 0535c1
    )
Packit Service 0535c1
    parser_edit.add_argument(
Packit Service 0535c1
        "--no-verify",
Packit Service 0535c1
        action="store_false",
Packit Service 0535c1
        dest="verify",
Packit Service 0535c1
        default=True,
Packit Service 0535c1
        help="Do not verify that the state was completely set and disable "
Packit Service 0535c1
        "rollback to previous state.",
Packit Service 0535c1
    )
Packit Service 0535c1
    parser_edit.add_argument(
Packit Service 0535c1
        "--memory-only",
Packit Service 0535c1
        action="store_false",
Packit Service 0535c1
        dest="save_to_disk",
Packit Service 0535c1
        default=True,
Packit Service 0535c1
        help="Do not make the state persistent.",
Packit Service 0535c1
    )
Packit Service 0535c1
Packit Service 0535c1
Packit Service 0535c1
def setup_subcommand_rollback(subparsers):
Packit Service 0535c1
    parser_rollback = subparsers.add_parser(
Packit Service 0535c1
        "rollback", help="Rollback a change"
Packit Service 0535c1
    )
Packit Service 0535c1
    parser_rollback.add_argument(
Packit Service 0535c1
        "checkpoint", nargs="?", default=None, help="checkpoint to roll back"
Packit Service 0535c1
    )
Packit Service 0535c1
    parser_rollback.set_defaults(func=rollback)
Packit Service 0535c1
Packit Service 0535c1
Packit Service 0535c1
def setup_subcommand_set(subparsers):
Packit Service 0535c1
    parser_set = subparsers.add_parser("set", help="Set network state")
Packit Service 0535c1
    parser_set.add_argument(
Packit Service 0535c1
        "file",
Packit Service 0535c1
        help="File containing desired state. "
Packit Service 0535c1
        "stdin is used when no file is specified.",
Packit Service 0535c1
        nargs="*",
Packit Service 0535c1
    )
Packit Service 0535c1
    parser_set.add_argument(
Packit Service 0535c1
        "--no-verify",
Packit Service 0535c1
        action="store_false",
Packit Service 0535c1
        dest="verify",
Packit Service 0535c1
        default=True,
Packit Service 0535c1
        help="Do not verify that the state was completely set and disable "
Packit Service 0535c1
        "rollback to previous state",
Packit Service 0535c1
    )
Packit Service 0535c1
    parser_set.add_argument(
Packit Service 0535c1
        "--no-commit",
Packit Service 0535c1
        action="store_false",
Packit Service 0535c1
        dest="commit",
Packit Service 0535c1
        default=True,
Packit Service 0535c1
        help="Do not commit new state after verification",
Packit Service 0535c1
    )
Packit Service 0535c1
    parser_set.add_argument(
Packit Service 0535c1
        "--timeout",
Packit Service 0535c1
        type=int,
Packit Service 0535c1
        default=60,
Packit Service 0535c1
        help="Timeout in seconds before reverting uncommited changes.",
Packit Service 0535c1
    )
Packit Service 0535c1
    parser_set.add_argument(
Packit Service 0535c1
        "--memory-only",
Packit Service 0535c1
        action="store_false",
Packit Service 0535c1
        dest="save_to_disk",
Packit Service 0535c1
        default=True,
Packit Service 0535c1
        help="Do not make the state persistent.",
Packit Service 0535c1
    )
Packit Service 0535c1
    parser_set.set_defaults(func=apply)
Packit Service 0535c1
Packit Service 0535c1
Packit Service 0535c1
def setup_subcommand_show(subparsers):
Packit Service 0535c1
    parser_show = subparsers.add_parser("show", help="Show network state")
Packit Service 0535c1
    parser_show.set_defaults(func=show)
Packit Service 0535c1
    parser_show.add_argument(
Packit Service 0535c1
        "--json",
Packit Service 0535c1
        help="Edit as JSON",
Packit Service 0535c1
        default=True,
Packit Service 0535c1
        action="store_false",
Packit Service 0535c1
        dest="yaml",
Packit Service 0535c1
    )
Packit Service 0535c1
    parser_show.add_argument(
Packit Service 0535c1
        "only",
Packit Service 0535c1
        default="*",
Packit Service 0535c1
        nargs="?",
Packit Service 0535c1
        metavar=Interface.KEY,
Packit Service 0535c1
        help="Show only specified interfaces (comma-separated)",
Packit Service 0535c1
    )
Packit Service 0535c1
Packit Service 0535c1
Packit Service 0535c1
def setup_subcommand_version(subparsers):
Packit Service 0535c1
    parser_version = subparsers.add_parser(
Packit Service 0535c1
        "version", help="Display nmstate version"
Packit Service 0535c1
    )
Packit Service 0535c1
    parser_version.set_defaults(func=version)
Packit Service 0535c1
Packit Service 0535c1
Packit Service 0535c1
def version(args):
Packit Service 0535c1
    print(libnmstate.__version__)
Packit Service 0535c1
Packit Service 0535c1
Packit Service 0535c1
def commit(args):
Packit Service 0535c1
    try:
Packit Service 0535c1
        libnmstate.commit(checkpoint=args.checkpoint)
Packit Service 0535c1
    except NmstateValueError as e:
Packit Service 0535c1
        print("ERROR committing change: {}\n".format(str(e)))
Packit Service 0535c1
        return os.EX_DATAERR
Packit Service 0535c1
Packit Service 0535c1
Packit Service 0535c1
def edit(args):
Packit Service 0535c1
    state = _filter_state(libnmstate.show(), args.only)
Packit Service 0535c1
Packit Service 0535c1
    if not state[Interface.KEY]:
Packit Service 0535c1
        sys.stderr.write("ERROR: No such interface\n")
Packit Service 0535c1
        return os.EX_USAGE
Packit Service 0535c1
Packit Service 0535c1
    pretty_state = PrettyState(state)
Packit Service 0535c1
Packit Service 0535c1
    if args.yaml:
Packit Service 0535c1
        suffix = ".yaml"
Packit Service 0535c1
        txtstate = pretty_state.yaml
Packit Service 0535c1
    else:
Packit Service 0535c1
        suffix = ".json"
Packit Service 0535c1
        txtstate = pretty_state.json
Packit Service 0535c1
Packit Service 0535c1
    new_state = _get_edited_state(txtstate, suffix, args.yaml)
Packit Service 0535c1
    if not new_state:
Packit Service 0535c1
        return os.EX_DATAERR
Packit Service 0535c1
Packit Service 0535c1
    print("Applying the following state: ")
Packit Service 0535c1
    print_state(new_state, use_yaml=args.yaml)
Packit Service 0535c1
Packit Service 0535c1
    libnmstate.apply(
Packit Service 0535c1
        new_state, verify_change=args.verify, save_to_disk=args.save_to_disk
Packit Service 0535c1
    )
Packit Service 0535c1
Packit Service 0535c1
Packit Service 0535c1
def rollback(args):
Packit Service 0535c1
    try:
Packit Service 0535c1
        libnmstate.rollback(checkpoint=args.checkpoint)
Packit Service 0535c1
    except NmstateValueError as e:
Packit Service 0535c1
        print("ERROR rolling back change: {}\n".format(str(e)))
Packit Service 0535c1
        return os.EX_DATAERR
Packit Service 0535c1
Packit Service 0535c1
Packit Service 0535c1
def show(args):
Packit Service 0535c1
    state = _filter_state(libnmstate.show(), args.only)
Packit Service 0535c1
    print_state(state, use_yaml=args.yaml)
Packit Service 0535c1
Packit Service 0535c1
Packit Service 0535c1
def apply(args):
Packit Service 0535c1
    if args.file:
Packit Service 0535c1
        for statefile in args.file:
Packit Service 0535c1
            if statefile == "-" and not os.path.isfile(statefile):
Packit Service 0535c1
                statedata = sys.stdin.read()
Packit Service 0535c1
            else:
Packit Service 0535c1
                with open(statefile) as statefile:
Packit Service 0535c1
                    statedata = statefile.read()
Packit Service 0535c1
Packit Service 0535c1
            ret = apply_state(
Packit Service 0535c1
                statedata,
Packit Service 0535c1
                args.verify,
Packit Service 0535c1
                args.commit,
Packit Service 0535c1
                args.timeout,
Packit Service 0535c1
                args.save_to_disk,
Packit Service 0535c1
            )
Packit Service 0535c1
            if ret:
Packit Service 0535c1
                return ret
Packit Service 0535c1
    elif not sys.stdin.isatty():
Packit Service 0535c1
        statedata = sys.stdin.read()
Packit Service 0535c1
        return apply_state(
Packit Service 0535c1
            statedata,
Packit Service 0535c1
            args.verify,
Packit Service 0535c1
            args.commit,
Packit Service 0535c1
            args.timeout,
Packit Service 0535c1
            args.save_to_disk,
Packit Service 0535c1
        )
Packit Service 0535c1
    else:
Packit Service 0535c1
        sys.stderr.write("ERROR: No state specified\n")
Packit Service 0535c1
        return 1
Packit Service 0535c1
Packit Service 0535c1
Packit Service 0535c1
def apply_state(statedata, verify_change, commit, timeout, save_to_disk):
Packit Service 0535c1
    use_yaml = False
Packit Service 0535c1
    # JSON dictionaries start with a curly brace
Packit Service 0535c1
    if statedata[0] == "{":
Packit Service 0535c1
        state = json.loads(statedata)
Packit Service 0535c1
    else:
Packit Service 0535c1
        state = yaml.load(statedata, Loader=yaml.SafeLoader)
Packit Service 0535c1
        use_yaml = True
Packit Service 0535c1
Packit Service 0535c1
    try:
Packit Service 0535c1
        checkpoint = libnmstate.apply(
Packit Service 0535c1
            state,
Packit Service 0535c1
            verify_change=verify_change,
Packit Service 0535c1
            commit=commit,
Packit Service 0535c1
            rollback_timeout=timeout,
Packit Service 0535c1
            save_to_disk=save_to_disk,
Packit Service 0535c1
        )
Packit Service 0535c1
    except NmstatePermissionError as e:
Packit Service 0535c1
        sys.stderr.write("ERROR: Missing permissions:{}\n".format(str(e)))
Packit Service 0535c1
        return os.EX_NOPERM
Packit Service 0535c1
    except NmstateConflictError:
Packit Service 0535c1
        sys.stderr.write(
Packit Service 0535c1
            "ERROR: State editing already in progress.\n"
Packit Service 0535c1
            "Commit, roll back or wait before retrying.\n"
Packit Service 0535c1
        )
Packit Service 0535c1
        return os.EX_UNAVAILABLE
Packit Service 0535c1
Packit Service 0535c1
    print("Desired state applied: ")
Packit Service 0535c1
    print_state(state, use_yaml=use_yaml)
Packit Service 0535c1
    if checkpoint:
Packit Service 0535c1
        print("Checkpoint: {}".format(checkpoint))
Packit Service 0535c1
Packit Service 0535c1
Packit Service 0535c1
def _filter_state(state, whitelist):
Packit Service 0535c1
    if whitelist != "*":
Packit Service 0535c1
        patterns = [p for p in whitelist.split(",")]
Packit Service 0535c1
        state[Interface.KEY] = _filter_interfaces(state, patterns)
Packit Service 0535c1
        state[Route.KEY] = _filter_routes(state, patterns)
Packit Service 0535c1
        state[RouteRule.KEY] = _filter_route_rule(state)
Packit Service 0535c1
    return state
Packit Service 0535c1
Packit Service 0535c1
Packit Service 0535c1
def _filter_interfaces(state, patterns):
Packit Service 0535c1
    """
Packit Service 0535c1
    return the states for all interfaces from `state` that match at least one
Packit Service 0535c1
    of the provided patterns.
Packit Service 0535c1
    """
Packit Service 0535c1
    showinterfaces = []
Packit Service 0535c1
Packit Service 0535c1
    for interface in state[Interface.KEY]:
Packit Service 0535c1
        for pattern in patterns:
Packit Service 0535c1
            if fnmatch.fnmatch(interface["name"], pattern):
Packit Service 0535c1
                showinterfaces.append(interface)
Packit Service 0535c1
                break
Packit Service 0535c1
    return showinterfaces
Packit Service 0535c1
Packit Service 0535c1
Packit Service 0535c1
def _get_edited_state(txtstate, suffix, use_yaml):
Packit Service 0535c1
    while True:
Packit Service 0535c1
        txtstate = _run_editor(txtstate, suffix)
Packit Service 0535c1
Packit Service 0535c1
        if txtstate is None:
Packit Service 0535c1
            return None
Packit Service 0535c1
Packit Service 0535c1
        new_state, error = _parse_state(txtstate, use_yaml)
Packit Service 0535c1
Packit Service 0535c1
        if error:
Packit Service 0535c1
            if not _try_edit_again(error):
Packit Service 0535c1
                return None
Packit Service 0535c1
        else:
Packit Service 0535c1
            return new_state
Packit Service 0535c1
Packit Service 0535c1
Packit Service 0535c1
def _run_editor(txtstate, suffix):
Packit Service 0535c1
    editor = os.environ.get("EDITOR", "vi")
Packit Service 0535c1
    with tempfile.NamedTemporaryFile(
Packit Service 0535c1
        suffix=suffix, prefix="nmstate-"
Packit Service 0535c1
    ) as statefile:
Packit Service 0535c1
        statefile.write(txtstate.encode("utf-8"))
Packit Service 0535c1
        statefile.flush()
Packit Service 0535c1
Packit Service 0535c1
        try:
Packit Service 0535c1
            subprocess.check_call([editor, statefile.name])
Packit Service 0535c1
            statefile.seek(0)
Packit Service 0535c1
            return statefile.read()
Packit Service 0535c1
Packit Service 0535c1
        except subprocess.CalledProcessError:
Packit Service 0535c1
            sys.stderr.write("Error running editor, aborting...\n")
Packit Service 0535c1
            return None
Packit Service 0535c1
Packit Service 0535c1
Packit Service 0535c1
def _parse_state(txtstate, parse_yaml):
Packit Service 0535c1
    error = ""
Packit Service 0535c1
    state = {}
Packit Service 0535c1
    if parse_yaml:
Packit Service 0535c1
        try:
Packit Service 0535c1
            state = yaml.load(txtstate, Loader=yaml.SafeLoader)
Packit Service 0535c1
        except yaml.parser.ParserError as e:
Packit Service 0535c1
            error = "Invalid YAML syntax: %s\n" % e
Packit Service 0535c1
        except yaml.parser.ScannerError as e:
Packit Service 0535c1
            error = "Invalid YAML syntax: %s\n" % e
Packit Service 0535c1
    else:
Packit Service 0535c1
        try:
Packit Service 0535c1
            state = json.loads(txtstate)
Packit Service 0535c1
        except ValueError as e:
Packit Service 0535c1
            error = "Invalid JSON syntax: %s\n" % e
Packit Service 0535c1
Packit Service 0535c1
    if not error and Interface.KEY not in state:
Packit Service 0535c1
        # Allow editing routes only.
Packit Service 0535c1
        state[Interface.KEY] = []
Packit Service 0535c1
Packit Service 0535c1
    return state, error
Packit Service 0535c1
Packit Service 0535c1
Packit Service 0535c1
def _try_edit_again(error):
Packit Service 0535c1
    """
Packit Service 0535c1
    Print error and ask for user feedback. Return True, if the state should be
Packit Service 0535c1
    edited again and False otherwise.
Packit Service 0535c1
    """
Packit Service 0535c1
Packit Service 0535c1
    sys.stderr.write("ERROR: " + error)
Packit Service 0535c1
    response = ""
Packit Service 0535c1
    while response not in ("y", "n"):
Packit Service 0535c1
        response = input(
Packit Service 0535c1
            "Try again? [y,n]:\n"
Packit Service 0535c1
            "y - yes, start editor again\n"
Packit Service 0535c1
            "n - no, throw away my changes\n"
Packit Service 0535c1
            "> "
Packit Service 0535c1
        ).lower()
Packit Service 0535c1
        if response == "n":
Packit Service 0535c1
            return False
Packit Service 0535c1
    return True
Packit Service 0535c1
Packit Service 0535c1
Packit Service 0535c1
def print_state(state, use_yaml=False):
Packit Service 0535c1
    state = PrettyState(state)
Packit Service 0535c1
    if use_yaml:
Packit Service 0535c1
        sys.stdout.write(state.yaml)
Packit Service 0535c1
    else:
Packit Service 0535c1
        print(state.json)
Packit Service 0535c1
Packit Service 0535c1
Packit Service 0535c1
def _filter_routes(state, patterns):
Packit Service 0535c1
    """
Packit Service 0535c1
    return the states for all routes from `state` that match at least one
Packit Service 0535c1
    of the provided patterns.
Packit Service 0535c1
    """
Packit Service 0535c1
    routes = {Route.CONFIG: [], Route.RUNNING: []}
Packit Service 0535c1
    for route_type in (Route.RUNNING, Route.CONFIG):
Packit Service 0535c1
        for route in state.get(Route.KEY, {}).get(route_type, []):
Packit Service 0535c1
            for pattern in patterns:
Packit Service 0535c1
                if fnmatch.fnmatch(route[Route.NEXT_HOP_INTERFACE], pattern):
Packit Service 0535c1
                    routes[route_type].append(route)
Packit Service 0535c1
    return routes
Packit Service 0535c1
Packit Service 0535c1
Packit Service 0535c1
def _filter_route_rule(state):
Packit Service 0535c1
    """
Packit Service 0535c1
    return the rules for state's route rule that match the table_id of the
Packit Service 0535c1
    filtered route of state by interface
Packit Service 0535c1
    """
Packit Service 0535c1
    route_rules = {RouteRule.CONFIG: []}
Packit Service 0535c1
    table_ids = []
Packit Service 0535c1
    for routes in {Route.CONFIG: [], Route.RUNNING: []}:
Packit Service 0535c1
        for route in state.get(Route.KEY, {}).get(routes, []):
Packit Service 0535c1
            if route.get(Route.TABLE_ID) not in table_ids:
Packit Service 0535c1
                table_ids.append(route.get(Route.TABLE_ID))
Packit Service 0535c1
    for rule in state.get(RouteRule.KEY, {}).get(RouteRule.CONFIG, []):
Packit Service 0535c1
        if rule.get(RouteRule.ROUTE_TABLE) in table_ids:
Packit Service 0535c1
            route_rules[RouteRule.CONFIG].append(rule)
Packit Service 0535c1
    return route_rules