Tuesday, May 31, 2016

script for jenkins build failure notification

#!/usr/bin/env python
# coding: utf-8
import re
import subprocess
import shlex
import os
import argparse, sys, collections

import urllib2
import json
import httplib
import time, datetime


# Just server address for all builds, otherwise only for given list of views
# place in jenkins
# get total number of failed builds
# run the job daily
# Create email groups
# Arguments: A. comma separated total number of failed builds - increasing order of builds, B. comma separated email addresses corresponding to the build-number groups




# usage: ./p4_create_user.py user_id manager_id group_id location_id : 4 required arguments


script_objective = '''
Purpose of the script:
Send email to different group of email addresses based on total number of failed builds
'''

# Message display when there are not enough arguments on command line
if len(sys.argv) < 2:
    print "Please run the script with argument -h for help"
    exit(0)

# argument parsing:
parser = argparse.ArgumentParser(formatter_class=argparse.RawTextHelpFormatter,
                                         add_help=True, description = '', epilog = script_objective)

requiredNamed = parser.add_argument_group('required named arguments')

requiredNamed.add_argument('-view_urls', nargs = '?', const = '', help = 'Comma separated Jenkins view(s)/server url', required = True)
requiredNamed.add_argument('-build_nums', nargs = '?', const = '', help = 'Comma separated build mumbers for mapping with email groups', required = True)
requiredNamed.add_argument('-emails', nargs = '?', const = '', help = 'Semicolon separated email groups for mapping with build groups. Emails in a group are comma separated', required = True)

args = parser.parse_args()

view_urls = args.view_urls
build_nums = args.build_nums # group = its number or more
emails = args.emails

email_domain = '@ruckuswireless.com'

email_from = 'releng-team' + email_domain

email_to = 'gopal.singh' + email_domain
email_cc = ''
#email_to = email_from
#email_cc = 'releng-team' + email_domain

email_bcc = ''
subject = 'Build failure notification'


email_msg = '''

-Jenkins build failed

'''

jenkins_server = 'http://jenkins.video54.local'

# Jenkins class
INFO         = 'api/json'
SCRIPT_OBJECTIVE = '''
Purpose of the script: Detects totals builds, failed builds, and automation failures.
'''

class JenkinsException(Exception): pass

class Jenkins(object):
    def __init__(self, url, username=None, password=None):
        server_split = (self.fix_server_url(url)).split('/')
self.server = server_split[0] + '//' + server_split[2] + '/'
self.auth = None

    def fix_server_url(self, urls): # urls may be comma separated server, view, job, or build urls
url = urls.split(',')[0]
if url[-1] != '/':
   url = url + '/'
return url

    def fix_urls(self, urls): # urls may be comma separated server, view, job, or build urls
url_str = ''
for url in urls.split(','):
   if url[-1] != '/':
url_str += url + '/,'
   else:
url_str += url + ','

return url_str.rstrip(',')

    def get_jenkins_server_url(self): # extract server url from list of comma separated urls
        server_url = (self.server).split('')      
        return server_url
   
    def jenkins_open(self, req): # req = HTTP request using urllib2
try:
   if self.auth:
req.add_header('Authorization', self.auth)
   return urllib2.urlopen(req).read()
except urllib2.HTTPError, e:
   # Jenkins's funky authentication means its highly impossible to distinguish errors.
   if e.code in [401, 403, 500]:
raise JenkinsException('Error in request. Possibly authentication failed [%s]'%(e.code))

    def get_info(self, url = None): # get_info gets api/json value for the server url (if server url is provided as jenkinsurl)
if url is None:
   url = self.server
try:
   return json.loads(self.jenkins_open(urllib2.Request(url + INFO)))
except urllib2.HTTPError:
   raise JenkinsException("Error communicating with server[%s]"%url)
except httplib.BadStatusLine:
   raise JenkinsException("Error communicating with server[%s]"%url)
except ValueError:
   raise JenkinsException("Could not parse JSON info for server[%s]"%url)
except:
   raise JenkinsException("Possibly wrong url[%s]"%url)

    @staticmethod
    def valid_view(info_str): # checks if the given url is a view url and has associated jobs
if 'description' in info_str and 'jobs' in info_str and (len(info_str['jobs']) > 0):
   return True
else:
   return False

    @staticmethod
    def valid_job(info_str): # checks if the given url is a job url
if 'builds' in info_str and (len(info_str['builds']) > 0):
   return True
else:
   return False

    @staticmethod
    def valid_build(info_str): # checks if the given url is a view url and has associated jobs
if 'number' in info_str:
   return True
else:
   return False

   
    def get_consecutive_failed_builds_in_job(self, job_url, max_builds_count=None):
jenkins_info = self.get_info(job_url)
builds_list = []
        try:
   first_build = jenkins_info['firstBuild']['number']
except:
   print 'No valid build found: for: ' + job_url
   return 0, job_url
last_build = jenkins_info['lastBuild']['number']
        build_count = 0
for build_num in reversed(range(first_build, last_build + 1)): # range is reversed to start from the latest build
   try:
                build_url = job_url + str(build_num) + '/'
                jenkins_info = self.get_info(url=build_url)
if jenkins_info['result'] == 'SUCCESS': break
if jenkins_info['result'] == 'FAILURE':
   builds_list.append(build_url)                              
                    build_count += 1
   if max_builds_count is not None and build_count >= max_builds_count:
break
   except: continue
return build_count, builds_list

    def get_consecutive_failed_builds_in_views(self, urls_list, max_builds_count=None): # the list of view urls should be comma separated string
        total_failed_builds = []      
list_urls = self.fix_urls(urls_list).split(',')
for url in list_urls:
   try:          
jenkins_info = self.get_info(url)
   except:  
print 'Not a valid view: ' + url
continue
   if self.valid_view(jenkins_info):
failed_builds = []
for job in jenkins_info['jobs']:
                    num, urls = self.get_consecutive_failed_builds_in_job(job['url'], max_builds_count)
                    if num > 0:  
failed_builds.append([job['url'], num])
   print 'Processing JOB: ' + job['url'] + ' ... Failed Builds: ' + str(num)
       total_failed_builds.append({url: failed_builds})  
   else: continue # continue to process next url if the current one is not a valid view url  
return total_failed_builds      

def update_email_data(build_data, build_nums, view_urls, emails):
    email_data = []  
    for x in build_data:
        job_data = []                
        for y in x.values()[0]: # x.values()[0] => list of job and corresponding failed builds, i.e.: [[u'http://jenkins.video54.local/job/ML_DAILY_SCG_AUTO_INIT/', 2]]        
            view_email_groups = select_view_email_groups(x.keys()[0], view_urls, emails) # x.keys()[0] => view_url          
            view_email_groups = fix_builds_email_group(build_nums, view_email_groups)          
            email_group = select_email_group(build_nums, view_email_groups, y[1]) # y[1] => number of failed builds for the job y[0]              
            job_data.append(y + [email_group])
email_data.append({x.keys()[0]:job_data})
    return email_data  
               
def runcmd(cmd):
    cmd =  shlex.split(cmd)
    try:
proc = subprocess.Popen(cmd, stdout=subprocess.PIPE, stderr=subprocess.PIPE)
out, error = proc.communicate()
return out + error
    except:
        msg = 'Invalid Command: ' + str(cmd)
print msg
return msg

def parse_buildnums_emails(build_nums, emails):
    list_buildnums = build_nums.split(',')
    a = [int(x) for x in list_buildnums]  
    list_emails = emails.split(':')
    pair = dict(zip(a, list_emails))
    return collections.OrderedDict(sorted(pair.items()))

def parse_views_emails(build_nums, emails):
    list_buildnums = build_nums.split(',')
    a = [int(x) for x in list_buildnums]  
    list_emails = emails.split(':')
    pair = dict(zip(a, list_emails))
    return collections.OrderedDict(sorted(pair.items()))

def select_email_group(build_nums, view_email_groups, failed_builds): # email group will be selected for if number of failed builds >= key
    #view_email_groups = fix_builds_email_group(build_nums, view_email_groups)  
    email_dict = parse_buildnums_emails(build_nums, view_email_groups)
    email_group = None  
    #email_group = email_dict.items()[0][1]
    for key, value in email_dict.iteritems():
        if failed_builds >= key:
   email_group = value
    return email_group


def fix_view_email_groups(view_urls, emails):
    views = view_urls.split(',')
    emails_group = emails.split('|')
    for num in range(len(views)):
        if num < len(emails_group): # use last provided emails group      
            if emails_group[num] == '': emails_group[num] = emails_group[num - 1]    
        else:    
       emails_group.append(emails_group[num - 1])
    return zip(views, emails_group)  

def fix_builds_email_group(build_nums, build_email_groups):
    build_group = build_nums.split(',')    
    email_group = build_email_groups.split(':')
    for num in range(len(build_group)):
        if num < len(email_group): # use last provided emails group      
            if email_group[num] == '': email_group[num] = email_group[num - 1]    
        else:    
       email_group.append(email_group[num - 1])
    return ':'.join(email_group)
       

def select_view_email_groups(view_url, view_urls, emails): # gives view corresponding email groups
    pair_list = fix_view_email_groups(view_urls, emails)  
    view_email_groups = ''  
    for x in pair_list:
        if x[0] == view_url:  
   view_email_groups = x[1]
    return view_email_groups      

def get_failed_builds(views_list, build_duration):
    pass

def verify_email_address(receivers):
    error_message = 'One or more email address is invalid'
    valid_emails = []
    invalid_emails = []
    for email_address in (''.join(receivers.split())).split(','): # Taken care off white spaces in the specified emails list
match = re.match('^[_a-z0-9-\.]+(\.[_a-z0-9-]+)*@[a-z0-9-]+(\.[a-z0-9-]+)*(\.[a-z]{2,4})$', email_address)
if match == None:
   invalid_emails += [email_address]
else:
   valid_emails += [email_address]
    if len(invalid_emails) > 0:
print(error_message)
raise ValueError(error_message)

    return valid_emails    

def send_email(email_from, email_to, email_subject, email_message, email_cc='', email_bcc=''):
    sendmail_location = runcmd('which sendmail').strip() # strip() used to trim line feed
    p = os.popen('%s -t' % sendmail_location, 'w')
    p.write('From: %s\n' % email_from)
    p.write('To: %s\n' % email_to)
    if email_cc is not '':
p.write('Cc: %s\n' % email_cc)
    if email_bcc is not '':
p.write('Bcc: %s\n' % email_bcc)
    p.write('Subject: %s\n' % email_subject)
    p.write('\n') # blank line separating headers from body
    p.write(email_message)
    status = p.close()
    if status is None: status = 'OK'
    print "Sendmail exit status: ", str(status)
    return status

def send_verified_email():
    global email_from, email_to, email_cc, email_bcc, email_msg, subject
    status = None  
    # send email  
    valid_emails_to = ','.join(verify_email_address(email_to)) # sendmail does not need list
    # Validate CC and BCC addresses
    if email_cc is not '':
        valid_emails_cc = ','.join(verify_email_address(email_cc))
    else:
        valid_emails_cc = ''
    if email_bcc is not '':    
        valid_emails_bcc = ','.join(verify_email_address(email_bcc))
    else:
        valid_emails_bcc = ''
    if len(valid_emails_to) > 0:
        send_email(email_from, valid_emails_to, subject, email_msg, email_cc=valid_emails_cc, email_bcc=valid_emails_bcc)
    else:
        print 'No valid email specified'
status = 'Error'

def complete_email_send():
    print '\n' # print line feed for cleaner nessage  
    status = None  
    status = get_failed_builds(views_list, build_duration)
    if status is None:
        status  = send_verified_email()
    return status    

def main():
    global build_nums, view_urls, emails, subject, email_msg
    jenkins = Jenkins('http://jenkins.video54.local/')    
    failed_job_data = jenkins.get_consecutive_failed_builds_in_views(view_urls)
    email_data = update_email_data(failed_job_data, build_nums, view_urls, emails)  
    print "Failed job and Email data: " + str(email_data)
    for x in email_data:
for y in x.values()[0]:
   if y[2] is not None:
email_to = y[2]
email_msg = str(y[1]) + ' continuous builds failed for the job: ' + y[0]
send_verified_email()
   else:
print 'No valid email to notify: ' + email_msg
if __name__ == '__main__':
    main()
   





No comments:

Post a Comment