Merge lp:~pfalcon/linaro-android-build-tools/xpath-jobs into lp:linaro-android-build-tools

Proposed by Paul Sokolovsky
Status: Merged
Merged at revision: 461
Proposed branch: lp:~pfalcon/linaro-android-build-tools/xpath-jobs
Merge into: lp:linaro-android-build-tools
Diff against target: 264 lines (+172/-15)
5 files modified
utils/mangle-jobs/mangle-jobs (+52/-6)
utils/mangle-jobs/mangle_helper.py (+16/-9)
utils/mangle-jobs/set-build-expiration.mangle (+17/-0)
utils/query-jobs/README (+20/-0)
utils/query-jobs/query-jobs (+67/-0)
To merge this branch: bzr merge lp:~pfalcon/linaro-android-build-tools/xpath-jobs
Reviewer Review Type Date Requested Status
Deepti B. Kalakeri Pending
Linaro Infrastructure Pending
Review via email: mp+103638@code.launchpad.net

Description of the change

query-job allows to execute an XPath query across all Jenkins config, something which we really need to have a global overview of our Jenkins job sets.

To post a comment you must log in.
Revision history for this message
Paul Sokolovsky (pfalcon) wrote :

Please see README in the patch for usage examples.

444. By Paul Sokolovsky

Remove stray copy&paste content.

445. By Paul Sokolovsky

Add a helper function to add/replace a node easily.

446. By Paul Sokolovsky

Solve long-standing issue with showing XML changes for review.

Well, we can't operate on raw Jenkins XML files, because it appears to write
it in different styles in different circumstances, so we always get some kind
of spurious changes. All XML-specific diff tools I saw are not human-friendly
(none I tested shows context for example), not helping with the task of easy
sanity review. So, instead normalize both old and new XMLs to common format,
and diff that.

447. By Paul Sokolovsky

Polish metavar name in --help output.

448. By Paul Sokolovsky

Add mangle script to set expiration policy for a job.

449. By Paul Sokolovsky

Use new diffing algorithm for runs against remote Jenkins too.

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1=== modified file 'utils/mangle-jobs/mangle-jobs'
2--- utils/mangle-jobs/mangle-jobs 2012-03-09 10:31:08 +0000
3+++ utils/mangle-jobs/mangle-jobs 2012-04-26 11:24:21 +0000
4@@ -16,6 +16,7 @@
5 import json
6 import os
7 import sys
8+import copy
9 import re
10 from tempfile import NamedTemporaryFile
11 import urllib2
12@@ -31,6 +32,8 @@
13 help="File holding Jenkins password")
14 optparser.add_option("--really", action="store_true",
15 help="Actually perform changes")
16+optparser.add_option("--filter-jobname",
17+ help="Process only jobs matching regex pattern")
18 optparser.add_option("--limit", type="int", default=-1,
19 help="Change at most LIMIT jobs")
20 optparser.add_option("--file",
21@@ -108,36 +111,79 @@
22 os.system('diff -u %s %s' % (a.name, b.name))
23 print
24
25+def indent_tree(elem, level=0):
26+ "Indent XML tree for pretty-printing"
27+ i = "\n" + level*" "
28+ if len(elem):
29+ if not elem.text or not elem.text.strip():
30+ elem.text = i + " "
31+ if not elem.tail or not elem.tail.strip():
32+ elem.tail = i
33+ for elem in elem:
34+ indent_tree(elem, level+1)
35+ if not elem.tail or not elem.tail.strip():
36+ elem.tail = i
37+ else:
38+ if level and (not elem.tail or not elem.tail.strip()):
39+ elem.tail = i
40+
41+def normalize2text(tree):
42+ """Return normalized text representation of XML tree, suitable for
43+ diffing with normal diff tool."""
44+ normalized = copy.deepcopy(tree)
45+ indent_tree(normalized)
46+ return tostring(normalized)
47+
48+def match_job_name(job_name):
49+ "Check if job name matches filters which may be specified on command line."
50+ if not options.filter_jobname:
51+ return True
52+ neg = False
53+ r = options.filter_jobname
54+ if r[0] == "-":
55+ neg = True
56+ r = r[1:]
57+ return bool(re.search(r, job_name)) ^ neg
58+
59 def process_remote_jenkins():
60 jobs = json.load(urllib2.urlopen('http://localhost:9090/jenkins/api/json?tree=jobs[name]'))
61 names = [job['name'] for job in jobs['jobs']]
62 names = [name for name in names if name == 'blank' or '_' in name]
63 limit = options.limit
64 for name in names:
65+ if not match_job_name(name):
66+ continue
67 if limit == 0:
68 break
69 limit -= 1
70 print "Processing:" + name
71 sys.stdout.flush()
72- text = getJobConfig(name)
73- tree = fromstring(text)
74+ org_text = getJobConfig(name)
75+ tree = fromstring(org_text)
76+ org_normalized = normalize2text(tree)
77+
78 if mangler(tree) == False:
79 continue
80- new_text = render_xml(tree)
81+
82 if not options.really:
83- show_diff(text, new_text)
84+ new_normalized = normalize2text(tree)
85+ show_diff(org_normalized, new_normalized)
86 else:
87+ new_text = render_xml(tree)
88 if type(new_text) == type(u""):
89 new_text = new_text.encode("utf8")
90 postConfig(str('job/' + name + '/config.xml'), new_text)
91
92+
93 def main():
94 if options.file:
95 text = open(options.file).read()
96 tree = fromstring(text)
97+
98+ org_normalized = normalize2text(tree)
99 mangler(tree)
100- new_text = render_xml(tree)
101- show_diff(text, new_text)
102+ new_normalized = normalize2text(tree)
103+ show_diff(org_normalized, new_normalized)
104 else:
105 process_remote_jenkins()
106
107
108=== modified file 'utils/mangle-jobs/mangle_helper.py'
109--- utils/mangle-jobs/mangle_helper.py 2011-12-21 19:48:46 +0000
110+++ utils/mangle-jobs/mangle_helper.py 2012-04-26 11:24:21 +0000
111@@ -1,5 +1,6 @@
112-# Disable Jenkins builtin artifact archiving (part of migration to snapshots.linaro.org)
113 import base64
114+import lxml.etree
115+
116
117 def get_build_config(tree):
118 tag = tree.xpath("/project/properties//defaultValue")
119@@ -14,11 +15,17 @@
120 d[k] = v
121 return d
122
123-def mangle(tree):
124- cfg = get_build_config(tree)
125- if cfg.get("BUILD_TYPE", "build-android") != "build-android":
126- return
127- tags = tree.xpath('//hudson.tasks.ArtifactArchiver')
128- if not tags:
129- return
130- tags[0].getparent().remove(tags[0])
131+
132+def add_or_replace_node(tree, node_xpath, node_text):
133+ new_node = lxml.etree.fromstring(node_text)
134+ nodes = tree.xpath(node_xpath)
135+ assert len(nodes) < 2, "Please use more selective XPath expression"
136+ if nodes:
137+ # First, add new node after existing
138+ nodes[0].addnext(new_node)
139+ # Then, delete the original node
140+ nodes[0].getparent().remove(nodes[0])
141+ else:
142+ parent_xpath, _ = node_xpath.rsplit("/", 1)
143+ parent = tree.xpath(parent_xpath)[0]
144+ parent.append(new_node)
145
146=== added file 'utils/mangle-jobs/set-build-expiration.mangle'
147--- utils/mangle-jobs/set-build-expiration.mangle 1970-01-01 00:00:00 +0000
148+++ utils/mangle-jobs/set-build-expiration.mangle 2012-04-26 11:24:21 +0000
149@@ -0,0 +1,17 @@
150+# Set expiration policy for a job
151+
152+from lxml.etree import fromstring
153+from mangle_helper import *
154+
155+
156+new_node = """\
157+ <logRotator>
158+ <daysToKeep>90</daysToKeep>
159+ <numToKeep>100</numToKeep>
160+ <artifactDaysToKeep>-1</artifactDaysToKeep>
161+ <artifactNumToKeep>-1</artifactNumToKeep>
162+ </logRotator>
163+"""
164+
165+def mangle(tree):
166+ add_or_replace_node(tree, "/project/logRotator", new_node)
167
168=== added directory 'utils/query-jobs'
169=== added file 'utils/query-jobs/README'
170--- utils/query-jobs/README 1970-01-01 00:00:00 +0000
171+++ utils/query-jobs/README 2012-04-26 11:24:21 +0000
172@@ -0,0 +1,20 @@
173+This is a script to query Jenkins job configs, on the level of raw XML files.
174+
175+Run ./query-jobs --help for list of optoins.
176+
177+
178+Example usage:
179+
180+./query-jobs //hudson.triggers.TimerTrigger --filter
181+
182+ List all regularly repeating jobs.
183+
184+./query-jobs //hudson.triggers.TimerTrigger/spec --filter -v
185+
186+ List crontab specs for all regularly repeating jobs.
187+
188+./query-jobs //daysToKeep --filter-not --filter-jobname='-(release|toolchain)'
189+
190+ List daily (not toolchain and not release) jobs which don't have build
191+ expriation configured (i.e. would use up disk space without limit).
192+
193
194=== added file 'utils/query-jobs/query-jobs'
195--- utils/query-jobs/query-jobs 1970-01-01 00:00:00 +0000
196+++ utils/query-jobs/query-jobs 2012-04-26 11:24:21 +0000
197@@ -0,0 +1,67 @@
198+#!/usr/bin/env python
199+import sys
200+import glob
201+import re
202+import optparse
203+
204+from lxml import etree
205+
206+
207+JENKINS_HOME = "/var/lib/jenkins/"
208+
209+def get_xpath(el):
210+ path = []
211+ while el is not None:
212+ path.insert(0, el.tag)
213+ el = el.getparent()
214+ return "/" + "/".join(path)
215+
216+def match_job_name(job_name):
217+ "Check if job name matches filters which may be specified on command line."
218+ if not options.filter_jobname:
219+ return True
220+ neg = False
221+ r = options.filter_jobname
222+ if r[0] == "-":
223+ neg = True
224+ r = r[1:]
225+ return bool(re.search(r, job_name)) ^ neg
226+
227+def process(query):
228+ for fname in glob.glob(options.home + "jobs/*/config.xml"):
229+ job_name = fname.split('/')[-2]
230+ if not match_job_name(job_name):
231+ continue
232+
233+ doc = etree.parse(fname)
234+ results = doc.getroot().xpath(query)
235+ if options.filter and len(results) == 0:
236+ continue
237+ if options.filter_not and len(results) > 0:
238+ continue
239+
240+ if options.verbose:
241+ print "===== %s =====" % (fname if options.show_file else job_name)
242+ else:
243+ print fname if options.show_file else job_name
244+ if options.verbose:
245+ first = True
246+ for el in results:
247+ if not first:
248+ print "-" * 20
249+ print "Element XPath: %s" % get_xpath(el)
250+ print etree.tostring(el)
251+ first = False
252+
253+optparser = optparse.OptionParser(usage="%prog")
254+optparser.add_option("-v", "--verbose", action="store_true", help="Verbose")
255+optparser.add_option("--show-file", action="store_true", help="Show job config path (default is job name)")
256+optparser.add_option("--home", default=JENKINS_HOME, help="Jenkins home dir (%default)")
257+optparser.add_option("--filter-jobname", metavar="REGEX", help="Process only jobs matching regex pattern (prefix with '-' for not matching)")
258+optparser.add_option("--filter", action="store_true", help="Show jobs matching query")
259+optparser.add_option("--filter-not", action="store_true", help="Show jobs not matching query")
260+options, args = optparser.parse_args(sys.argv[1:])
261+if len(args) != 1:
262+ optparser.error("Wrong number of arguments")
263+
264+process(args[0])

Subscribers

People subscribed via source and target branches