513 lines
18 KiB
Python
513 lines
18 KiB
Python
"""
|
|
Manage jenkins jobs
|
|
|
|
Prerequisites:
|
|
The jenkinsapi module is required https://pypi.python.org/pypi/jenkinsapi
|
|
To install the module using pip:
|
|
sudo pip install jenkinsapi
|
|
|
|
The plugin polls the jenkins server at 'jenkins_url' every 'jenkins_poll_interval' seconds.
|
|
|
|
The results are checked by profanity every 'prof_cb_interval'.
|
|
New failures are reported in the jenkins window, and a desktop notification is sent.
|
|
|
|
The 'prof_remind_interval' specifies the time between reminder notifications of broken builds
|
|
|
|
"""
|
|
|
|
import prof
|
|
import os
|
|
import threading
|
|
import time
|
|
import webbrowser
|
|
import urllib2
|
|
import base64
|
|
import jenkinsapi
|
|
from jenkinsapi.jenkins import Jenkins
|
|
|
|
# Settings
|
|
jenkins_url = "http://localhost:8080"
|
|
username = None
|
|
password = None
|
|
jenkins_poll_interval = 5
|
|
prof_cb_interval = 5
|
|
prof_remind_interval = 10
|
|
enable_notify = True
|
|
enable_remind = True
|
|
|
|
# Global state
|
|
last_state = {}
|
|
STATE_SUCCESS = "SUCCESS"
|
|
STATE_UNSTABLE = "UNSTABLE"
|
|
STATE_FAILURE = "FAILURE"
|
|
STATE_QUEUED = "QUEUED"
|
|
STATE_RUNNING = "RUNNING"
|
|
STATE_NOBUILDS = "NOBUILDS"
|
|
STATE_UNKNOWN = "UNKNOWN"
|
|
|
|
win_tag = "Jenkins"
|
|
|
|
poll_fail = False
|
|
poll_fail_message = None
|
|
|
|
job_list = None
|
|
changes_list = None
|
|
|
|
def _help():
|
|
prof.win_show(win_tag, "Commands:")
|
|
prof.win_show(win_tag, " /jenkins help - Show this help")
|
|
prof.win_show(win_tag, " /jenkins jobs - List all jobs")
|
|
prof.win_show(win_tag, " /jenkins failing - List all failing jobs")
|
|
prof.win_show(win_tag, " /jenkins passing - List all passing jobs")
|
|
prof.win_show(win_tag, " /jenkins unstable - List all unstable jobs")
|
|
prof.win_show(win_tag, " /jenkins build [job] - Trigger build for job")
|
|
prof.win_show(win_tag, " /jenkins open [job] - Open job in browser")
|
|
prof.win_show(win_tag, " /jenkins log [job] - Show the latest build log")
|
|
prof.win_show(win_tag, " /jenkins remind on|off - Enable/disable reminder notifications")
|
|
prof.win_show(win_tag, " /jenkins notify on|off - Enable/disable build notifications")
|
|
prof.win_show(win_tag, " /jenkins settings - Show current settings")
|
|
|
|
def _settings():
|
|
prof.win_show(win_tag, "Jenkins settings:")
|
|
prof.win_show(win_tag, " Jenkins URL : " + jenkins_url)
|
|
prof.win_show(win_tag, " Jenkins poll interval : " + str(jenkins_poll_interval) + " seconds")
|
|
prof.win_show(win_tag, " Profanity update interval : " + str(prof_cb_interval) + " seconds")
|
|
prof.win_show(win_tag, " Reminder interval : " + str(prof_remind_interval) + " seconds")
|
|
prof.win_show(win_tag, " Notifications enabled : " + str(enable_notify))
|
|
prof.win_show(win_tag, " Reminders enabled : " + str(enable_remind))
|
|
|
|
def _safe_remove(jobname, state):
|
|
if jobname in last_state[state]:
|
|
last_state[state].remove(jobname)
|
|
|
|
def _set_state(jobname, state):
|
|
if not jobname in last_state[state]:
|
|
_safe_remove(jobname, STATE_SUCCESS)
|
|
_safe_remove(jobname, STATE_UNSTABLE)
|
|
_safe_remove(jobname, STATE_FAILURE)
|
|
_safe_remove(jobname, STATE_QUEUED)
|
|
_safe_remove(jobname, STATE_RUNNING)
|
|
_safe_remove(jobname, STATE_NOBUILDS)
|
|
_safe_remove(jobname, STATE_UNKNOWN)
|
|
last_state[state].append(jobname)
|
|
return True
|
|
else:
|
|
return False
|
|
|
|
def _open_job_url(url):
|
|
savout = os.dup(1)
|
|
saverr = os.dup(2)
|
|
os.close(1)
|
|
os.close(2)
|
|
os.open(os.devnull, os.O_RDWR)
|
|
try:
|
|
webbrowser.open(url, new=2)
|
|
finally:
|
|
os.dup2(savout, 1)
|
|
os.dup2(saverr, 2)
|
|
|
|
class JobList():
|
|
def __init__(self):
|
|
self.jobs = []
|
|
|
|
def add_job(self, name, build_number, state):
|
|
self.jobs.append((name, build_number, state))
|
|
|
|
def get_jobs(self, in_state=None):
|
|
if not in_state:
|
|
return self.jobs
|
|
else:
|
|
result = []
|
|
for name, build_number, state in self.jobs:
|
|
if state == in_state:
|
|
result.append((name, build_number, state))
|
|
return result
|
|
|
|
def get_job(self, jobname):
|
|
for name, build_number, state in self.jobs:
|
|
if name == jobname:
|
|
return name, build_number
|
|
return None
|
|
|
|
def contains_job(self, jobname):
|
|
for name, build_number, state in self.jobs:
|
|
if name == jobname:
|
|
return True
|
|
return False
|
|
|
|
def num_in_state(self, given_state):
|
|
count = 0
|
|
for name, build_number, state in self.jobs:
|
|
if state == given_state:
|
|
count = count + 1
|
|
return count
|
|
|
|
class JobUpdates():
|
|
def __init__(self):
|
|
self.states = {}
|
|
self.states[STATE_FAILURE] = []
|
|
self.states[STATE_SUCCESS] = []
|
|
self.states[STATE_UNSTABLE] = []
|
|
self.states[STATE_QUEUED] = []
|
|
self.states[STATE_RUNNING] = []
|
|
|
|
def add_update(self, state, name, build_number=None):
|
|
if build_number:
|
|
self.states[state].append((name, build_number))
|
|
else:
|
|
self.states[state].append(name)
|
|
|
|
def get_in_state(self, state):
|
|
return self.states[state]
|
|
|
|
def _process_build(name, build, new_job_list, new_changes_list):
|
|
if build.get_status() == STATE_FAILURE:
|
|
new_job_list.add_job(name, build.get_number(), STATE_FAILURE)
|
|
changed = _set_state(name, STATE_FAILURE)
|
|
if changed:
|
|
new_changes_list.add_update(STATE_FAILURE, name, build.get_number())
|
|
elif build.get_status() == STATE_SUCCESS:
|
|
new_job_list.add_job(name, build.get_number(), STATE_SUCCESS)
|
|
changed = _set_state(name, STATE_SUCCESS)
|
|
if changed:
|
|
new_changes_list.add_update(STATE_SUCCESS, name, build.get_number())
|
|
elif build.get_status() == STATE_UNSTABLE:
|
|
new_job_list.add_job(name, build.get_number(), STATE_UNSTABLE)
|
|
changed = _set_state(name, STATE_UNSTABLE)
|
|
if changed:
|
|
new_changes_list.add_update(STATE_UNSTABLE, name, build.get_number())
|
|
|
|
def _process_queued_or_running(name, job, new_job_list, new_changes_list):
|
|
if job.is_queued():
|
|
new_job_list.add_job(name, None, STATE_QUEUED)
|
|
changed = _set_state(name, STATE_QUEUED)
|
|
if changed:
|
|
new_changes_list.add_update(STATE_QUEUED, name)
|
|
elif job.is_running():
|
|
new_job_list.add_job(name, None, STATE_RUNNING)
|
|
changed = _set_state(name, STATE_RUNNING)
|
|
if changed:
|
|
new_changes_list.add_update(STATE_RUNNING, name)
|
|
|
|
def _jenkins_poll():
|
|
global poll_fail
|
|
global poll_fail_message
|
|
global job_list
|
|
global changes_list
|
|
|
|
job_ac = []
|
|
|
|
while True:
|
|
time.sleep(jenkins_poll_interval)
|
|
try:
|
|
j = Jenkins(jenkins_url, username, password)
|
|
except Exception, e:
|
|
poll_fail = True
|
|
poll_fail_message = str(e)
|
|
else:
|
|
poll_fail = False
|
|
poll_fail_message = None
|
|
new_job_list = JobList()
|
|
new_changes_list = JobUpdates()
|
|
for name, job in j.get_jobs():
|
|
if not job.is_queued_or_running():
|
|
build = job.get_last_build_or_none()
|
|
if build:
|
|
_process_build(name, build, new_job_list, new_changes_list)
|
|
else:
|
|
new_job_list.add_job(name, None, STATE_NOBUILDS)
|
|
else:
|
|
_process_queued_or_running(name, job, new_job_list, new_changes_list)
|
|
if not job_list:
|
|
job_ac.append(name)
|
|
|
|
if not job_list:
|
|
prof.completer_add("/jenkins build", job_ac)
|
|
prof.completer_add("/jenkins open", job_ac)
|
|
prof.completer_add("/jenkins log", job_ac)
|
|
|
|
job_list = new_job_list
|
|
changes_list = new_changes_list
|
|
|
|
def _prof_callback():
|
|
global changes_list
|
|
if poll_fail:
|
|
if not prof.win_exists(win_tag):
|
|
prof.win_create(win_tag, _handle_input)
|
|
prof.win_show(win_tag, "Could not connect to jenkins, see the logs.")
|
|
if poll_fail_message:
|
|
prof.log_warning("Jenkins poll failed: " + str(poll_fail_message))
|
|
else:
|
|
prof.log_warning("Jenkins poll failed")
|
|
elif changes_list:
|
|
for name in changes_list.get_in_state(STATE_QUEUED):
|
|
if not prof.win_exists(win_tag):
|
|
prof.win_create(win_tag, _handle_input)
|
|
prof.win_show_themed(win_tag, None, None, "cyan", name + " " + STATE_QUEUED)
|
|
for name in changes_list.get_in_state(STATE_RUNNING):
|
|
if not prof.win_exists(win_tag):
|
|
prof.win_create(win_tag, _handle_input)
|
|
prof.win_show_themed(win_tag, None, None, "cyan", name + " " + STATE_RUNNING)
|
|
for name, build_number in changes_list.get_in_state(STATE_SUCCESS):
|
|
if not prof.win_exists(win_tag):
|
|
prof.win_create(win_tag, _handle_input)
|
|
prof.win_show_themed(win_tag, None, None, "green", name + " #" + str(build_number) + " " + STATE_SUCCESS)
|
|
if enable_notify:
|
|
prof.notify(name + " " + STATE_SUCCESS, 5000, "Jenkins")
|
|
for name, build_number in changes_list.get_in_state(STATE_UNSTABLE):
|
|
if not prof.win_exists(win_tag):
|
|
prof.win_create(win_tag, _handle_input)
|
|
prof.win_show_themed(win_tag, None, None, "yellow", name + " #" + str(build_number) + " " + STATE_UNSTABLE)
|
|
if enable_notify:
|
|
prof.notify(name + " " + STATE_UNSTABLE, 5000, "Jenkins")
|
|
for name, build_number in changes_list.get_in_state(STATE_FAILURE):
|
|
if not prof.win_exists(win_tag):
|
|
prof.win_create(win_tag, _handle_input)
|
|
prof.win_show_themed(win_tag, None, None, "red", name + " #" + str(build_number) + " " + STATE_FAILURE)
|
|
if enable_notify:
|
|
prof.notify(name + " " + STATE_FAILURE, 5000, "Jenkins")
|
|
|
|
changes_list = None
|
|
|
|
def _handle_input(win, line):
|
|
pass
|
|
|
|
def _list_jobs(jobs):
|
|
for name, build_number, state in jobs:
|
|
if state == STATE_SUCCESS:
|
|
prof.win_show_themed(win_tag, None, None, "green", " " + name + " #" + str(build_number) + " " + STATE_SUCCESS)
|
|
elif state == STATE_UNSTABLE:
|
|
prof.win_show_themed(win_tag, None, None, "yellow", " " + name + " #" + str(build_number) + " " + STATE_UNSTABLE)
|
|
elif state == STATE_FAILURE:
|
|
prof.win_show_themed(win_tag, None, None, "red", " " + name + " #" + str(build_number) + " " + STATE_FAILURE)
|
|
elif state == STATE_NOBUILDS:
|
|
prof.win_show(win_tag, " " + name + ", no builds")
|
|
else:
|
|
prof.win_show_themed(win_tag, None, None, "cyan", " " + name + " " + state)
|
|
|
|
def _build_job(job):
|
|
try:
|
|
request = urllib2.Request(jenkins_url + "/job/" + job + "/build");
|
|
if username:
|
|
basicauth = base64.encodestring("%s:%s" %(username, password)).replace("\n", "")
|
|
request.add_header("Authorization", "Basic %s" %basicauth);
|
|
request.add_data("")
|
|
urllib2.urlopen(request)
|
|
except Exception, e:
|
|
prof.win_show(win_tag, "Failed to build " + job + ", see the logs.")
|
|
prof.log_warning("Failed to build " + job + ": " + str(e))
|
|
else:
|
|
prof.win_show(win_tag, "Build request sent for " + job)
|
|
|
|
def _job_log(job):
|
|
name = job[0]
|
|
build_no = job[1]
|
|
if build_no:
|
|
try:
|
|
response = urllib2.urlopen(jenkins_url + "/job/" + name + "/" + str(build_no) + "/consoleText")
|
|
except Exception, e:
|
|
prof.win_show(win_tag, "Unable to fetch log for " + name + "#" + str(build_no) + ", see the logs.")
|
|
prof.log_warning("Unable to fetch log for " + name + "#" + str(build_no) + ": " + str(e))
|
|
else:
|
|
log_str = "Log for " + name + " #" + str(build_no) + ":\n" + response.read()
|
|
prof.win_show(win_tag, log_str)
|
|
else:
|
|
prof.win_show(win_tag, "No build found for " + name)
|
|
|
|
def _cmd_jenkins(cmd=None, arg=None):
|
|
global enable_remind
|
|
global enable_notify
|
|
|
|
if not prof.win_exists(win_tag):
|
|
prof.win_create(win_tag, _handle_input)
|
|
|
|
prof.win_focus(win_tag)
|
|
|
|
if cmd == "jobs":
|
|
if job_list and job_list.get_jobs():
|
|
prof.win_show(win_tag, "Jobs:")
|
|
_list_jobs(job_list.get_jobs())
|
|
else:
|
|
prof.win_show(win_tag, "No job data yet.")
|
|
elif cmd == "failing":
|
|
if job_list:
|
|
jobs_in_state = job_list.get_jobs(STATE_FAILURE)
|
|
if jobs_in_state:
|
|
prof.win_show(win_tag, "Failing jobs:")
|
|
_list_jobs(jobs_in_state)
|
|
else:
|
|
prof.win_show(win_tag, "No failing jobs.")
|
|
else:
|
|
prof.win_show(win_tag, "No job data yet.")
|
|
elif cmd == "passing":
|
|
if job_list:
|
|
jobs_in_state = job_list.get_jobs(STATE_SUCCESS)
|
|
if jobs_in_state:
|
|
prof.win_show(win_tag, "Passing jobs:")
|
|
_list_jobs(jobs_in_state)
|
|
else:
|
|
prof.win_show(win_tag, "No passing jobs.")
|
|
else:
|
|
prof.win_show(win_tag, "No job data yet.")
|
|
elif cmd == "unstable":
|
|
if job_list:
|
|
jobs_in_state = job_list.get_jobs(STATE_UNSTABLE)
|
|
if jobs_in_state:
|
|
prof.win_show(win_tag, "Unstable jobs:")
|
|
_list_jobs(jobs_in_state)
|
|
else:
|
|
prof.win_show(win_tag, "No unstable jobs.")
|
|
else:
|
|
prof.win_show(win_tag, "No job data yet.")
|
|
elif cmd == "build":
|
|
if not arg:
|
|
prof.win_show(win_tag, "You must supply a job argument.")
|
|
elif job_list and job_list.contains_job(arg):
|
|
_build_job(arg)
|
|
else:
|
|
prof.win_show(win_tag, "No such job: " + arg)
|
|
elif cmd == "open":
|
|
if not arg:
|
|
prof.win_show(win_tag, "You must supply a job argument.")
|
|
elif job_list and job_list.contains_job(arg):
|
|
_open_job_url(jenkins_url + "/job/" + arg)
|
|
else:
|
|
prof.win_show(win_tag, "No such job: " + arg)
|
|
elif cmd == "remind":
|
|
if not arg:
|
|
prof.win_show(win_tag, "You must specify either 'on' or 'off'.")
|
|
elif arg == "on":
|
|
enable_remind = True
|
|
prof.win_show(win_tag, "Reminder notifications enabled.")
|
|
elif arg == "off":
|
|
enable_remind = False
|
|
prof.win_show(win_tag, "Reminder notifications disabled.")
|
|
else:
|
|
prof.win_show(win_tag, "You must specify either 'on' or 'off'.")
|
|
elif cmd == "notify":
|
|
if not arg:
|
|
prof.win_show(win_tag, "You must specify either 'on' or 'off'.")
|
|
elif arg == "on":
|
|
enable_notify = True
|
|
prof.win_show(win_tag, "Build notifications enabled.")
|
|
elif arg == "off":
|
|
enable_notify = False
|
|
prof.win_show(win_tag, "Build notifications disabled.")
|
|
else:
|
|
prof.win_show(win_tag, "You must specify either 'on' or 'off'.")
|
|
elif cmd == "settings":
|
|
_settings()
|
|
elif cmd == "help":
|
|
_help()
|
|
elif cmd == "log":
|
|
if not arg:
|
|
prof.win_show(win_tag, "You must specify a job.")
|
|
else:
|
|
job = job_list.get_job(arg)
|
|
if job:
|
|
_job_log(job)
|
|
else:
|
|
prof.win_show(win_tag, "No job found: " + arg)
|
|
else:
|
|
prof.win_show(win_tag, "Unknown command.")
|
|
|
|
def _remind():
|
|
if enable_remind and job_list:
|
|
notify_string = ""
|
|
failing = job_list.num_in_state(STATE_FAILURE)
|
|
unstable = job_list.num_in_state(STATE_UNSTABLE)
|
|
|
|
if failing == 1:
|
|
notify_string = notify_string + "1 failing build"
|
|
if failing > 1:
|
|
notify_string = notify_string + str(failing) + " failing builds"
|
|
|
|
if failing > 0 and unstable > 0:
|
|
notify_string = notify_string + "\n"
|
|
|
|
if unstable == 1:
|
|
notify_string = notify_string + "1 unstable build"
|
|
if unstable > 1:
|
|
notify_string = notify_string + str(unstable) + " unstable builds"
|
|
|
|
if not notify_string == "":
|
|
prof.notify(notify_string, 5000, "Jenkins")
|
|
|
|
def prof_init(version, status):
|
|
last_state[STATE_SUCCESS] = []
|
|
last_state[STATE_UNSTABLE] = []
|
|
last_state[STATE_FAILURE] = []
|
|
last_state[STATE_QUEUED] = []
|
|
last_state[STATE_RUNNING] = []
|
|
last_state[STATE_NOBUILDS] = []
|
|
last_state[STATE_UNKNOWN] = []
|
|
|
|
jenkins_t = threading.Thread(target=_jenkins_poll)
|
|
jenkins_t.daemon = True;
|
|
jenkins_t.start()
|
|
|
|
prof.register_timed(_prof_callback, prof_cb_interval)
|
|
prof.register_timed(_remind, prof_remind_interval)
|
|
|
|
prof.completer_add(
|
|
"/jenkins", [
|
|
"help",
|
|
"jobs",
|
|
"failing",
|
|
"passing",
|
|
"unstable",
|
|
"build",
|
|
"open",
|
|
"log",
|
|
"remind",
|
|
"notify",
|
|
"settings"
|
|
]
|
|
);
|
|
prof.completer_add(
|
|
"/jenkins remind", [
|
|
"on",
|
|
"off"
|
|
]
|
|
);
|
|
prof.completer_add(
|
|
"/jenkins notify", [
|
|
"on",
|
|
"off"
|
|
]
|
|
);
|
|
|
|
synopsis = [
|
|
"/jenkins jobs|failing|passing|unstable",
|
|
"/jenkins build <job>",
|
|
"/jenkins open <job>",
|
|
"/jenkins log <job>",
|
|
"/jenkins remind on|off",
|
|
"/jenkins notify on|off",
|
|
"/jenkins settings"
|
|
]
|
|
description = "Monitor, run and view Jenkins jobs."
|
|
args = [
|
|
[ "jobs", "List all jobs" ],
|
|
[ "failing", "List all failing jobs" ],
|
|
[ "passing", "List all passing jobs" ],
|
|
[ "unstable", "List all unstable jobs" ],
|
|
[ "build <job>", "Trigger build for job" ],
|
|
[ "open <job>", "Open job in browser" ],
|
|
[ "log <job>", "Show the latest build log for job" ],
|
|
[ "remind on|off", "Enable/disable reminder notifications" ],
|
|
[ "notify on|off", "Enable/disable build notifications" ],
|
|
[ "settings", "Show current settings" ]
|
|
]
|
|
examples = [
|
|
"/kenkins failing",
|
|
"/kenkins build stabber-build",
|
|
"/kenkins remind off"
|
|
]
|
|
|
|
prof.register_command("/jenkins", 0, 2, synopsis, description, args, examples, _cmd_jenkins)
|
|
|
|
def prof_on_start():
|
|
prof.win_create(win_tag, _handle_input)
|
|
prof.win_show(win_tag, "Jenkins plugin started.")
|