Forum Discussion

Ken_Norris's avatar
3 years ago

Cisco IOS-XR backups

Hello,

It does not look like there is a Cisco IOS-XR config module to add for IOS-XR device backups.  Maybe I'm missing something?  Is there a way to do this with just groovy script maybe?  Any other work arounds?  TIA for any help.

  • Anonymous's avatar
    Anonymous

    Assuming you've looked in the exchange and found nothing. Yes, it's pretty easy to get it to work. You'd do something like this (JSCH) or like this (Expect) to log into the device, issue the commands you need to grab the config, then use println() to output the config text to stdout. The main difference is that JSCH might take a bit of work to enter enable mode (it's built more for Linux systems than network gear, IMO). Expect would be more of a back and forth between your script and the device, so you can program in the cues and responses to get the config(s) you want.

  • We have a config source in our portal that does this.  I think it's built off an older IOS config source from awhile back that we modified.  Here it is in 2 posts for you.

    AD part

     

    /*******************************************************************************
     * © 2007-2019 - LogicMonitor, Inc.  All rights reserved.
     ******************************************************************************/
    import com.santaba.agent.groovyapi.expect.Expect

    def host = hostProps.get("system.hostname")
    def teln = hostProps.get("configsource.use.telnet").toString().toBoolean()
    def user = hostProps.get("ssh.user") ?: hostProps.get("config.user")
    def pass = hostProps.get("ssh.pass") ?: hostProps.get("config.pass")
    def epas = hostProps.get("ssh.enable.pass") ?: hostProps.get("config.enable.pass") ?: pass
    def admn = hostProps.get("ssh.admin") ?: "false"
    def port = teln ? (hostProps.get("configsource.telnet.port") ?: 23) : (hostProps.get("ssh.port") ?: 22)

    // Basic mode is used for devices that can't handle terminal or enable commands.
    def basicMode = hostProps.get("system.sysinfo").contains("Cisco Firepower Threat Defense")

    // These are the commands we check are supported.
    def desiredCommands = ["running-config", "startup-config", "version", "inventory"]

    // The command list can be overwritten via this property.
    if(hostProps["configsource.desired.commands"]) {
        desiredCommands = hostProps["configsource.desired.commands"].split(",").collect { it.trim() }
    }

    def cli
    def timeout = 60
    def success = false
    def error = ""

    // We remove common characters used to decorate a terminal.
    def termClean = /(.?\[\?7h)/
    def rawPrompt = /(?-m)[^\n\s]*[>#$]\s*$/

    // CLI based approaches fail often. We make up to 10 attempts to connect and retrieve a single byte of data.
    def retries = 0
    while(retries < 10 && !success) {
        try {
            cli = teln ? Expect.open(host, port.toInteger(), timeout) : Expect.open(host, port.toInteger(), user, pass, timeout)
            cli.expect(".")
            success = true
        }
         catch (ex) {
            sleep(1000)
            error += "[Connection Attempt ${retries}] ${ex.message}\n"
        }

        retries++
    }

    if(success) {
        try {
            if(teln) {
                // Telnet sometimes doesn't require a user to be sent. Only do so if requested.
                cli.expect(["[Uu]?sername:", "[Pp]?assword:"] as String[])

                if(cli.matched().toLowerCase().contains("name")) {
                    cli.send("${user}\n")
                    cli.expect("[Pp]?assword:")
                }

                cli.send("${pass}\n")
            }

            // At this point we are ready to check for a prompt, but don't know if the device has finished sending data.
            // We wait until the data has been stable for 100 ms. Staggered/Delayed data is reasonably common.
            def outputStability = 0
            def lastLength = -1

            while(outputStability < 100) {
                outputStability += (cli.stdout().length() == lastLength) ? 1 : -outputStability
                lastLength = cli.stdout().length()
                sleep(10)
            }
            
            // Now the data is stable, the standard output should have everything we need to gather a prompt.
            def stdPrompt = cli.stdout() =~ rawPrompt.bitwiseNegate()
            def prompt = ""

            if(stdPrompt.size()) {
                // We've got our best guess at a prompt at this point. Clean it and get it ready for line matching.
                prompt = "^${termClean}*.?${stdPrompt[0].trim().replaceAll(termClean,'').drop(1).trim().replaceAll(/[.*+?^()|\[\]\\{}$]/, '\\\\$0')}"
                // Consume the buffer expecting the prompt so we know we have a clean buffer to work with for future matches.
                cli.expect(prompt)
            }
            else {
                // We didn't find a prompt in the standard output. Use expect to wait for one.
                cli.expect(rawPrompt)
                prompt = "^.?${cli.matched().replaceAll(termClean,'').trim().replaceAll(/[.*+?^()|\[\]\\{}$]/, '\\\\$0')}"
            }

            if(!basicMode) {
                // The enable command will elevate the prompt if possible.
                cli.send("enable\n")
                cli.expect(["(?i)password:", "${prompt[0..-2]}[>#\$]", prompt] as String[])

                if(cli.matched().toLowerCase().contains("password")) {
                    cli.send("${epas}\n")
                    cli.expect(rawPrompt)
                }

                // The enable command may have changed the prompt. We've got to get it again.
                prompt = "^${cli.matched().replaceAll(termClean,'').trim().replaceAll(/[.*+?^()|\[\]\\{}$]/, '\\\\$0')}"
                
                // We aren't in basic mode so try and configure the terminal to avoid pagination with both variations of the width and length commands.
                ["width", "length"].each { dimension ->
                    cli.send("terminal ${dimension} 0\n")
                    cli.expect(prompt)
                    if(cli.before().trim().readLines().size() > 1) {
                        cli.send("screen-${dimension} 0\n")
                        cli.expect(prompt)
                    }
                }

                if(admn.toLowerCase().contains("true")) {
                    // Attempt to enter admin mode if requested.
                    cli.send("admin\n")
                    cli.expect(rawPrompt)
                    prompt = "^.?${cli.matched().replaceAll(termClean,'').trim().replaceAll(/[.*+?^()|\[\]\\{}$]/, '\\\\$0')}"
                }
            }

            // Execute the show command for this instance, adding a space for each page we may need to postpone a prompt being returned.
            def maxPages = 50
            cli.send("show ?${' ' * maxPages}\n")

            // As we sometimes need to paginate the output, we will concatenate the output in a buffer.
            def buffer = ""

            // We loop throught all potential pages up to the limit set by "maxPages" (a default of 50 was double the ammount needed for anything in testing).
            for(int i = 0; i < maxPages; i++) {
                cli.expect([prompt, "\\s?--More--\\s?"] as String[])

                // Attempt to clean the output of unwanted information, specifically backspace characters and the --More-- decoration.
                buffer += cli.before().replaceAll("(\b+\\s*\b+)|(\\s?--More--\\s?)","")

                if(cli.matched().contains("--More--")) {
                    // We hit a page break. Send many new line characters to greatly reduce the chance of an early prompt being returned (one is not reliable).
                    cli.send("${' ' * maxPages}\n")
                } else {
                    // We matched the prompt and have to assume we've gotten everything. We are unable to continue pagination even with more new line characters.
                    break
                }
            }

            def output = buffer.trim()

            output.eachLine{ line->
                desiredCommands.each { comamnd->
                    def tokens = line.trim().tokenize()
                    if(tokens[0] == comamnd) {
                        println "${tokens[0]}##${tokens[0]}##${tokens[1..-1].join(" ")}"
                    }
                }
            }
        } catch (ex) {
            error += "[Collecting] ${ex.message}\n"
            success = false
        }

        try {
            // Add an extra exit if we might be in privilege mode.
            cli.send("exit\n" * (basicMode ? 2 : 1))
            cli.close()
        }
        catch(ex) {
            error += "[Closing] ${ex.message}\n"
            success = false
        }
    }

    if(!success) {
        error += cli ? "--- CLI Output ---\n${cli.stdout()}\n" : ""
        error += cli ? "--- CLI Base64 ---\n${cli.stdout().bytes.encodeBase64().toString()}\n" : ""
        println "--- Error Logs ---\n${error}"
        //new File("../logs/Cisco_IOS_Mini_AD ${host.replaceAll('[^a-zA-Z0-9-_\\.]', '_')}.txt").write(error)
        return 1
    }

    return 0

  • Data Collection

    /*******************************************************************************
     * © 2007-2019 - LogicMonitor, Inc.  All rights reserved.
     ******************************************************************************/
    import com.santaba.agent.groovyapi.expect.Expect

    def host = hostProps.get("system.hostname")
    def teln = hostProps.get("configsource.use.telnet").toString().toBoolean()
    def user = hostProps.get("ssh.user") ?: hostProps.get("config.user")
    def pass = hostProps.get("ssh.pass") ?: hostProps.get("config.pass")
    def epas = hostProps.get("ssh.enable.pass") ?: hostProps.get("config.enable.pass") ?: pass
    def admn = hostProps.get("ssh.admin") ?: "false"
    def port = teln ? (hostProps.get("configsource.telnet.port") ?: 23) : (hostProps.get("ssh.port") ?: 22)
    def wild = instanceProps.get("wildvalue")

    // Basic mode is used for devices that can't handle terminal or enable commands.
    def basicMode = hostProps.get("system.sysinfo").contains("Cisco Firepower Threat Defense")

    def cli
    def timeout = 60
    def success = false
    def error = ""

    // We remove common characters used to decorate a terminal.
    def termClean = /(.?\[\?7h)/
    def rawPrompt = /(?-m)[^\n\s]*[>#$]\s*$/

    // CLI based approaches fail often. We make up to 10 attempts to connect and retrieve a single byte of data.
    def retries = 0
    while(retries < 10 && !success) {
        try {
            cli = teln ? Expect.open(host, port.toInteger(), timeout) : Expect.open(host, port.toInteger(), user, pass, timeout)
            cli.expect(".")
            success = true
        }
         catch (ex) {
            sleep(1000)
            error += "[Connection Attempt ${retries}] ${ex.message}\n"
        }

        retries++
    }

    if(success) {
        try {        
            if(teln) {
                // Telnet sometimes doesn't require a user to be sent. Only do so if requested.
                cli.expect(["[Uu]?sername:", "[Pp]?assword:"] as String[])

                if(cli.matched().toLowerCase().contains("name")) {
                    cli.send("${user}\n")
                    cli.expect("[Pp]?assword:")
                }

                cli.send("${pass}\n")
            }

            // At this point we are ready to check for a prompt, but don't know if the device has finished sending data.
            // We wait until the data has been stable for 100 ms. Staggered/Delayed data is reasonably common.
            def outputStability = 0
            def lastLength = -1

            while(outputStability < 100) {
                outputStability += (cli.stdout().length() == lastLength) ? 1 : -outputStability
                lastLength = cli.stdout().length()
                sleep(10)
            }

            // Now the data is stable, the standard output should have everything we need to gather a prompt.
            def stdPrompt = cli.stdout() =~ rawPrompt.bitwiseNegate()
            def prompt = ""

            if(stdPrompt.size()) {
                // We've got our best guess at a prompt at this point. Clean it and get it ready for line matching.
                prompt = "^${termClean}*.?${stdPrompt[0].trim().replaceAll(termClean,'').drop(1).trim().replaceAll(/[.*+?^()|\[\]\\{}$]/, '\\\\$0')}"
                // Consume the buffer expecting the prompt so we know we have a clean buffer to work with for future matches.
                cli.expect(prompt)
            }
            else {
                // We didn't find a prompt in the standard output. Use expect to wait for one.
                cli.expect(rawPrompt)
                prompt = "^.?${cli.matched().replaceAll(termClean,'').trim().replaceAll(/[.*+?^()|\[\]\\{}$]/, '\\\\$0')}"
            }

            if(!basicMode) {
                // The enable command will elevate the prompt if possible.
                cli.send("enable\n")
                cli.expect(["(?i)password:", "${prompt[0..-2]}[>#\$]", prompt] as String[])

                if(cli.matched().toLowerCase().contains("password")) {
                    cli.send("${epas}\n")
                    cli.expect(rawPrompt)
                }

                // The enable command may have changed the prompt. We've got to get it again.
                prompt = "^${cli.matched().replaceAll(termClean,'').trim().replaceAll(/[.*+?^()|\[\]\\{}$]/, '\\\\$0')}"

                // We aren't in basic mode so try and configure the terminal to avoid pagination with both variations of the width and length commands.
                ["width", "length"].each { dimension ->
                    cli.send("terminal ${dimension} 0\n")
                    cli.expect(prompt)
                    if(cli.before().trim().readLines().size() > 1) {
                        cli.send("screen-${dimension} 0\n")
                        cli.expect(prompt)
                    }
                }

                if(admn.toLowerCase().contains("true")) {
                    // Attempt to enter admin mode if requested.
                    cli.send("admin\n")
                    cli.expect(rawPrompt)
                    prompt = "^.?${cli.matched().replaceAll(termClean,'').trim().replaceAll(/[.*+?^()|\[\]\\{}$]/, '\\\\$0')}"
                }
            }

            // Execute the show command for this instance, adding a space for each page we may need to postpone a prompt being returned.
            def maxPages = 50
            cli.send("show ${wild}${' ' * maxPages}\n")

            // As we sometimes need to paginate the output, we will concatenate the output in a buffer.
            def buffer = ""

            // We loop throught all potential pages up to the limit set by "maxPages" (a default of 50 was double the ammount needed for anything in testing).
            for(int i = 0; i < maxPages; i++) {
                cli.expect([prompt, "\\s?--More--\\s?"] as String[])

                // Attempt to clean the output of unwanted information, specifically backspace characters and the --More-- decoration.
                buffer += cli.before().replaceAll("(\b+\\s*\b+)|(\\s?--More--\\s?)","")

                if(cli.matched().contains("--More--")) {
                    // We hit a page break. Send many new line characters to greatly reduce the chance of an early prompt being returned (one is not reliable).
                    cli.send("${' ' * maxPages}\n")
                } else {
                    // We matched the prompt and have to assume we've gotten everything. We are unable to continue pagination even with more new line characters.
                    break
                }
            }

            def output = buffer.trim()

            // Print the output of the show command if the input was valid. Remove the last line (the prompt).
            if(!output.toLowerCase().contains("invalid input detected")) {
                println output.readLines().drop(1).join('\n').trim()
            }
        } catch (ex) {
            error += "[Collecting] ${ex.message}\n"
            success = false
        }

        try {
            // Add an extra exit if we might be in privilege mode.
            cli.send("exit\n" * (basicMode ? 2 : 1))
            cli.close()
        }
        catch(ex) {
            error += "[Closing] ${ex.message}\n"
            success = false
        }
    }

    if(!success) {
        error += cli ? "--- CLI Output ---\n${cli.stdout()}\n" : ""
        error += cli ? "--- CLI Base64 ---\n${cli.stdout().bytes.encodeBase64().toString()}\n" : ""
        println "--- Error Logs ---\n${error}"
        new File("../logs/Cisco_IOS_Mini_Collection ${host.replaceAll('[^a-zA-Z0-9-_\\.]', '_')}.txt").write(error)
        return 1
    }

    return 0