Search code examples
juniperjunos-automationpyez

Is there a way to convert juniper "json" or "xml" config to "set" or "show" config?


We use juniper hardware with junos version 15. In this version we can export our config as "json" or "xml" which we want to use to edit it with our automation tooling. Importing however is only possible in "set" or "show" format.

Is there a tool to convert "json" or "xml" format to "set" or "show" format? I can only find converters between "show" and "set".

We can't upgrade to version 16 where the import of "json" would be possible.


Solution

  • Here's a script I made at work, throw it in your bin and you can it via providing a filename or piping output. This assumes linux or mac so the os.isatty function works, but the logic can work anywhere:

    usage demo:

    person@laptop ~ > head router.cfg
    ## Last commit: 2021-04-20 21:21:39 UTC by vit
    version 15.1X12.2;
    groups {
        BACKBONE-PORT {
            interfaces {
                <*> {
                    mtu 9216;
                    unit <*> {
                        family inet {
                            mtu 9150;
    person@laptop ~ > convert.py router.cfg | head
    set groups BACKBONE-PORT interfaces <*> mtu 9216
    set groups BACKBONE-PORT interfaces <*> unit <*> family inet mtu 9150
    set groups BACKBONE-PORT interfaces <*> unit <*> family inet6 mtu 9150
    set groups BACKBONE-PORT interfaces <*> unit <*> family mpls maximum-labels 5
    <... output removed... >
    

    convert.py:

    #!/usr/bin/env python3
    # Class that attempts to parse out Juniper JSON into set format
    #   I think it works? still testing
    #
    #   TODO: 
    #      accumulate annotations and provide them as commands at the end. Will be weird as annotations have to be done after an edit command
    from argparse import ArgumentParser, RawTextHelpFormatter
    import sys, os, re
    
    class TokenStack():
        def __init__(self):
            self._tokens = []
    
        def push(self, token):
            self._tokens.append(token)
    
        def pop(self):
            if not self._tokens:
                return None
            item = self._tokens[-1]
            self._tokens = self._tokens[:-1]
            return item
    
        def peek(self):
            if not self._tokens:
                return None
            return self._tokens[-1]
    
        def __str__(self):
            return " ".join(self._tokens)
    
        def __repr__(self):
            return " ".join(self._tokens)
    
    def main():
        # get file
        a = ArgumentParser(prog="convert_jpr_json",
                description="This program takes in Juniper style JSON (blah { format) and prints it in a copy pastable display set format",
                epilog=f"Either supply with a filename or pipe config contents into this program and it'll print out the display set view.\nEx:\n{B}convert_jpr_json <FILENAME>\ncat <FILENAME> | convert_jpr_json{WHITE}",
                formatter_class=RawTextHelpFormatter)
        a.add_argument('file', help="juniper config in JSON format", nargs="?")
        args = a.parse_args()
        if not args.file and os.isatty(0):
            a.print_help()
            die("Please supply filename or provide piped input")
        file_contents = None
        if args.file:
            try:
                file_contents = open(args.file, "r").readlines()
            except IOError as e:
                die(f"Issue opening file {args.file}: {e}")
                print(output_text)
        else:
            file_contents = sys.stdin.readlines()
    
        tokens = TokenStack()
        in_comment = False
        new_config = []
    
        for line_num, line in enumerate(file_contents):
            if line.startswith("version ") or len(line) == 0:
                continue
            token = re.sub(r"^(.+?)#+[^\"]*$", r"\1", line.strip())
            token = token.strip()
            if (any(token.startswith(_) for _ in ["!", "#"])):
                # annotations currently not supported
                continue
        
            if token.startswith("/*"):
                # we're in a comment now until the next token (this will break if a multiline comment with # style { happens, but hopefully no-one is that dumb
                in_comment = True
                continue
        
            if "inactive: " in token:
                token = token.split("inactive: ")[1]
                new_config.append(f"deactivate {tokens} {token}")
            if token[-1] == "{":
                in_comment = False
                tokens.push(token.strip("{ "))
            elif token[-1] == "}":
                if not tokens.pop():
                    die("Invalid json supplied: unmatched closing } encountered on line " + f"{line_num}")
            elif token[-1] == ";":
                new_config.append(f"set {tokens} {token[:-1]}")
        if tokens.peek():
            print(tokens)
            die("Unbalanced JSON: expected closing }, but encountered EOF")
        print("\n".join(new_config))
    
    def die(msg): print(f"\n{B}{RED}FATAL ERROR{WHITE}: {msg}"); exit(1)
    RED = "\033[31m"; GREEN = "\033[32m"; YELLOW = "\033[33m"; B = "\033[1m"; WHITE = "\033[0m"
    if __name__ == "__main__": main()