解析和 Return Jenkins 控制台输出

Parse and Return Jenkins Console Output

在 Jenkins 中,我想为失败的主机名解析 ansible 剧本“Play Recap”输出部分。我想将信息放入电子邮件或其他通知中。这也可以用来启动另一个 Jenkins 作业。

我目前正在提交一份 ansible-playbook 作为 jenkins 作业,以便在多个系统中部署软件。我正在使用 Jenkins 管道脚本,这是正确应用 sshagent 所必需的。

pipeline {
    agent any
    options {
        ansiColor('xterm')
    }
    stages {
        stage("setup environment") {
            steps {
                deleteDir()
            } //steps
        } //stage - setup environment
        stage("clone the repo") {
            environment {
                GIT_SSH_COMMAND = "ssh -o StrictHostKeyChecking=no"
            } //environment
            steps {
                sshagent(['my_git']) {
                    sh "git clone ssh://git@github.com/~usr/ansible.git" 
                } //sshagent
            } //steps
        } //stage - clone the repo
        stage("run ansible playbook") {
            steps {
                sshagent (credentials: ['apps']) {
                    withEnv(['ANSIBLE_CONFIG=ansible.cfg']) {
                        dir('ansible') {
                            ansiblePlaybook(
                                becomeUser: null, 
                                colorized: true, 
                                credentialsId: 'apps',
                                disableHostKeyChecking: true,
                                forks: 50,
                                hostKeyChecking: false,
                                inventory: 'hosts', 
                                limit: 'production:&*generic', 
                                playbook: 'demo_play.yml', 
                                sudoUser: null,
                                extras: '-vvvvv'
                            ) //ansiblePlaybook
                        } //dir
                    } //withEnv
                } //sshagent
            } //steps
        } //stage - run ansible playbook
    } //stages
    post {
        failure { 
            emailext body: "Please go to ${env.BUILD_URL}/consoleText for more details.",
            recipientProviders: [[$class: 'DevelopersRecipientProvider'], [$class: 'RequesterRecipientProvider']],
            subject: "${env.JOB_NAME}",
            to: 'our.dev.team@gmail.com',
            attachLog: true
            
            office365ConnectorSend message:"A production system appears to be unreachable.",
                status:"Failed",
                color:"f00000",
                factDefinitions: [[name: "Credentials ID", template: "apps"],
                                  [name: "Build Duration", template: "${currentBuild.durationString}"],
                                  [name: "Full Name", template: "${currentBuild.fullDisplayName}"]],
                webhookUrl:'https://outlook.office.com/webhook/[really long alphanumeric key]/IncomingWebhook/[another super-long alphanumeric key]'
        } //failure
    } //post
} //pipeline

有几个 Jenkins 插件可以解析控制台输出,但是 none 可以让我捕获和使用文本。我看过 log-parser and text finder

我唯一的线索是使用 groovy 编写脚本。
https://devops.stackexchange.com/questions/5363/jenkins-groovy-to-parse-console-output-and-mark-build-failure

控制台输出中的“Play Recap”示例是:

PLAY RECAP **************************************************************************************************************************************************
some.host.name     : ok=25   changed=2    unreachable=0    failed=1    skipped=2    rescued=0    ignored=0
some.ip.address    : ok=22   changed=2    unreachable=0    failed=0    skipped=1    rescued=0    ignored=0

我正在尝试获取每个失败主机的列表或分隔字符串。虽然,在列表的情况下,我需要弄清楚如何发送多个通知。

如果有人能帮助我完成完整的解决方案,我将非常感谢你的帮助。

Q: "Parse the ansible playbook 'Play Recap' output section."

A:使用json回调并用jq解析输出。例如

shell> ANSIBLE_STDOUT_CALLBACK=json ansible-playbook pb.yml | jq .stats

我在解决这个问题时遇到了一些 'gotchas'。

  1. 我可以访问 ansible 插件输出的唯一成功方法是拉取原始日志文件。 def log = currentBuild.rawBuild.getLog(100) 在这种情况下,我只提取了最后 100 行,因为我只是在寻找 Play Recap 框。此方法需要特殊权限。控制台日志将显示错误并提供一个 link 允许功能的位置。

  2. ansible 输出不应着色。 colorized: false 彩色输出很难解析。 'console log' 不会显示彩色标记,但是如果您查看 'consoleText' 就会看到它。

  3. 使用正则表达式时,您很可能会有一个不可序列化的 matcher 对象。要在 Jenkins 中使用它,可能需要将其放置在标记为 @NonCPS 的函数中,以阻止 Jenkins 尝试序列化该对象。我需要这个的结果好坏参半,所以我没有完全理解在哪里需要它。

  4. 正则表达式语句对我来说是比较难的部分之一。我想出了一个通用语句,可以针对不同的场景轻松修改,例如失败或无法访问。我在 groovy 中使用 'slashy-style' 正则表达式也更幸运,它在语句的两端放置了一个正斜杠,不需要任何类型的引号。您会注意到 'failed' 部分是不同的 failed=([1-9]|[1-9][0-9]),因此它只匹配失败为非零的语句。

/([0-9a-zA-Z\.\-]+)(?=[ ]*:[ ]*ok=([0-9]|[1-9][0-9])[ ]*changed=([0-9]|[1-9][0-9])[ ]*unreachable=([0-9]|[1-9][0-9])[ ]*failed=([1-9]|[1-9][0-9]))/

这是我想出的完整管道代码。

pipeline {
    agent any
    options {
        ansiColor('xterm')
    }
    stages {
        stage("setup environment") {
            steps {
                deleteDir()
            } //steps
        } //stage - setup environment
        stage("clone the repo") {
            environment {
                GIT_SSH_COMMAND = "ssh -o StrictHostKeyChecking=no"
            } //environment
            steps {
                sshagent(['my_git']) {
                    sh "git clone ssh://git@github.com/~usr/ansible.git" 
                } //sshagent
            } //steps
        } //stage - clone the repo
        stage("run ansible playbook") {
            steps {
                sshagent (credentials: ['apps']) {
                    withEnv(['ANSIBLE_CONFIG=ansible.cfg']) {
                        dir('ansible') {
                            ansiblePlaybook(
                                becomeUser: null, 
                                colorized: false, 
                                credentialsId: 'apps',
                                disableHostKeyChecking: true,
                                forks: 50,
                                hostKeyChecking: false,
                                inventory: 'hosts', 
                                limit: 'production:&*generic', 
                                playbook: 'demo_play.yml', 
                                sudoUser: null,
                                extras: '-vvvvv'
                            ) //ansiblePlaybook
                        } //dir
                    } //withEnv
                } //sshagent
            } //steps
        } //stage - run ansible playbook
    } //stages
    post {
        failure {
            script {
                problem_hosts = get_the_hostnames()
            }
            emailext body: "${problem_hosts} has failed.  Please go to ${env.BUILD_URL}/consoleText for more details.",
            recipientProviders: [[$class: 'DevelopersRecipientProvider'], [$class: 'RequesterRecipientProvider']],
            subject: "${env.JOB_NAME}",
            to: 'our.dev.team@gmail.com',
            attachLog: true
            
            office365ConnectorSend message:"${problem_hosts} has failed.",
                status:"Failed",
                color:"f00000",
                factDefinitions: [[name: "Credentials ID", template: "apps"],
                                  [name: "Build Duration", template: "${currentBuild.durationString}"],
                                  [name: "Full Name", template: "${currentBuild.fullDisplayName}"]],
                webhookUrl:'https://outlook.office.com/webhook/[really long alphanumeric key]/IncomingWebhook/[another super-long alphanumeric key]'
        } //failure
    } //post
} //pipeline

//@NonCPS
def get_the_hostnames() {
    // Get the last 100 lines of the log
    def log = currentBuild.rawBuild.getLog(100)
    print log
    // GREP the log for the failed hostnames
    def matches = log =~ /([0-9a-zA-Z\.\-]+)(?=[ ]*:[ ]*ok=([0-9]|[1-9][0-9])[ ]*changed=([0-9]|[1-9][0-9])[ ]*unreachable=([0-9]|[1-9][0-9])[ ]*failed=([1-9]|[1-9][0-9]))/
    def hostnames = null
    // if any matches occurred
    if (matches) {
        // iterate over the matches
        for (int i = 0; i < matches.size(); i++) {
            // if there is a name, concatenate it
            // else populate it
            if (hostnames?.trim()) {
                hostnames = hostnames + " " + matches[i]
            } else {
                hostnames = matches[i][0]
            } // if/else
        } // for
    } // if
    if (!hostnames?.trim()) {
        hostnames = "No hostnames identified."
    }
    return hostnames
}