~linaro-automation/linaro-android-build-tools/trunk

« back to all changes in this revision

Viewing changes to utils/mangle-jobs/mangle-jobs

  • Committer: Paul Sokolovsky
  • Date: 2013-12-10 17:07:20 UTC
  • Revision ID: paul.sokolovsky@linaro.org-20131210170720-vwx83ujrxk2itejn
Most of utils migrated to https://git.linaro.org/infrastructure/linaro-jenkins-tools.git

Show diffs side-by-side

added added

removed removed

Lines of Context:
1
 
#!/usr/bin/python
2
 
"""Helper to mass-edit jobs in jenkins.
3
 
 
4
 
"""
5
 
 
6
 
###############################################################################
7
 
# Copyright (c) 2011 Linaro
8
 
# All rights reserved. This program and the accompanying materials
9
 
# are made available under the terms of the Eclipse Public License v1.0
10
 
# which accompanies this distribution, and is available at
11
 
# http://www.eclipse.org/legal/epl-v10.html
12
 
###############################################################################
13
 
 
14
 
import base64
15
 
from contextlib import nested
16
 
import json
17
 
import os
18
 
import sys
19
 
import copy
20
 
import re
21
 
from tempfile import NamedTemporaryFile
22
 
import urllib2
23
 
import optparse
24
 
import getpass
25
 
from xml.dom import minidom
26
 
 
27
 
from lxml.etree import fromstring, tostring
28
 
 
29
 
 
30
 
optparser = optparse.OptionParser(usage="%prog <mangle script>")
31
 
optparser.add_option("--url", default="http://localhost:8080/jenkins/",
32
 
                     help="Jenkins base url, default: %default")
33
 
optparser.add_option("--user",
34
 
                     help="Jenkins username")
35
 
optparser.add_option("--passwd-file", metavar="FILE",
36
 
                     help="File holding Jenkins password")
37
 
optparser.add_option("--really", action="store_true",
38
 
                     help="Actually perform changes")
39
 
optparser.add_option("--filter-jobname",
40
 
                     help="Process only jobs matching regex pattern")
41
 
optparser.add_option("--limit", type="int", default=-1,
42
 
                     help="Change at most LIMIT jobs")
43
 
optparser.add_option("--file",
44
 
                     help="Process a file instead of all jobs on a remote server")
45
 
 
46
 
options, args = optparser.parse_args(sys.argv[1:])
47
 
if len(args) != 1:
48
 
    optparser.error("Wrong number of arguments")
49
 
 
50
 
d = {}
51
 
execfile(args[0], d, d)
52
 
mangler = d['mangle']
53
 
 
54
 
password = None
55
 
if options.passwd_file:
56
 
    password = open(options.passwd_file).read().strip()
57
 
elif not options.file:
58
 
    password = getpass.getpass("Password/API Token:")
59
 
 
60
 
if options.url[-1] != '/':
61
 
    options.url += '/'
62
 
 
63
 
auth_headers = {
64
 
    'Authorization': 'Basic %s' % (
65
 
        base64.encodestring('%s:%s' % (options.user, password))[:-1],),
66
 
    }
67
 
 
68
 
def _authJenkins(jenkins_path, data=None, extra_headers=None):
69
 
    """Make an authenticated request to jenkins.
70
 
 
71
 
    @param jenkins_path: The path on the Jenkins instance to make the request
72
 
        to.
73
 
    @param data: Data to include in the request (if this is not None the
74
 
        request will be a POST).
75
 
    @param extra_headers: A dictionary of extra headers that will passed in
76
 
        addition to Authorization.
77
 
    @raises urllib2.HTTPError: If the response is not a HTTP 200.
78
 
    @returns: the body of the response.
79
 
    """
80
 
    headers = auth_headers.copy()
81
 
    if extra_headers:
82
 
        headers.update(extra_headers)
83
 
    req = urllib2.Request(
84
 
        options.url + jenkins_path, data, headers)
85
 
    resp = urllib2.urlopen(req)
86
 
    return resp.read()
87
 
 
88
 
def getJobConfig(job_name):
89
 
    return _authJenkins('job/' + job_name + '/config.xml')
90
 
 
91
 
def postConfig(url, configXml, extra_headers=None):
92
 
    headers = {'Content-Type': 'text/xml', }
93
 
    if extra_headers is not None:
94
 
        headers.update(extra_headers)
95
 
    _authJenkins(url, configXml, headers)
96
 
 
97
 
def render_xml(tree):
98
 
    # Render XML to exact dialect used by Jenkins
99
 
    # This involves some dirty magic
100
 
    text = tostring(tree, xml_declaration=True, encoding='UTF-8')
101
 
    # Roundtrip via minidom, this takes care of encoding " as entities
102
 
    tree2 = minidom.parseString(text)
103
 
    text = tree2.toxml('UTF-8')
104
 
 
105
 
    # expand empty tags
106
 
    text = re.sub(r"<([-A-Za-z.]+)/>", "<\\1></\\1>", text)
107
 
 
108
 
    # Some CR noise should be entities
109
 
    text = text.replace("\r", "&#xd;")
110
 
 
111
 
    # Finally, munge xml decl
112
 
    line1, rest = text.split("><", 1)
113
 
    line1 = line1.replace('"', "'")
114
 
    r = line1 + ">\n<" + rest
115
 
 
116
 
    return r
117
 
 
118
 
def show_diff(old, new):
119
 
    with nested(NamedTemporaryFile(), NamedTemporaryFile()) as (a, b):
120
 
        a.write(old)
121
 
        b.write(new)
122
 
        a.flush(); b.flush()
123
 
        os.system('diff -u %s %s' % (a.name, b.name))
124
 
    print
125
 
 
126
 
def indent_tree(elem, level=0):
127
 
    "Indent XML tree for pretty-printing"
128
 
    i = "\n" + level*"  "
129
 
    if len(elem):
130
 
        if not elem.text or not elem.text.strip():
131
 
            elem.text = i + "  "
132
 
        if not elem.tail or not elem.tail.strip():
133
 
            elem.tail = i
134
 
        for elem in elem:
135
 
            indent_tree(elem, level+1)
136
 
        if not elem.tail or not elem.tail.strip():
137
 
            elem.tail = i
138
 
    else:
139
 
        if level and (not elem.tail or not elem.tail.strip()):
140
 
            elem.tail = i
141
 
 
142
 
def normalize2text(tree):
143
 
    """Return normalized text representation of XML tree, suitable for
144
 
    diffing with normal diff tool."""
145
 
    normalized = copy.deepcopy(tree)
146
 
    indent_tree(normalized)
147
 
    return tostring(normalized)
148
 
 
149
 
def match_job_name(job_name):
150
 
    "Check if job name matches filters which may be specified on command line."
151
 
    if not options.filter_jobname:
152
 
        return True
153
 
    neg = False
154
 
    r = options.filter_jobname
155
 
    if r[0] == "-":
156
 
        neg = True
157
 
        r = r[1:]
158
 
    return bool(re.search(r, job_name)) ^ neg
159
 
 
160
 
def get_csrf_token():
161
 
    try:
162
 
        crumb_data = _authJenkins('crumbIssuer/api/xml')
163
 
    except urllib2.HTTPError:
164
 
        # Ignore errors for android-build which provides no crumb.
165
 
        return None
166
 
    tree = minidom.parseString(crumb_data)
167
 
    crumb_tag = tree.getElementsByTagName('crumb')[0]
168
 
    field_tag = tree.getElementsByTagName('crumbRequestField')[0]
169
 
    crumb = str(crumb_tag.firstChild.wholeText)
170
 
    field = str(field_tag.firstChild.wholeText)
171
 
    return (field, crumb)
172
 
 
173
 
def process_remote_jenkins():
174
 
    jobs = json.load(urllib2.urlopen(options.url + 'api/json?tree=jobs[name]'))
175
 
    names = [job['name'] for job in jobs['jobs']]
176
 
    names = [name for name in names if name == 'blank' or '_' in name]
177
 
    limit = options.limit
178
 
 
179
 
    csrf_token = get_csrf_token()
180
 
    if csrf_token is None:
181
 
        extra_headers = None
182
 
    else:
183
 
        extra_headers = { csrf_token[0]: csrf_token[1], }
184
 
 
185
 
    for name in names:
186
 
        if not match_job_name(name):
187
 
            continue
188
 
        if limit == 0:
189
 
            break
190
 
        limit -= 1
191
 
        print "Processing:" + name
192
 
        sys.stdout.flush()
193
 
        org_text = getJobConfig(name)
194
 
        tree = fromstring(org_text)
195
 
        org_normalized = normalize2text(tree)
196
 
 
197
 
        if mangler(tree) == False:
198
 
            continue
199
 
 
200
 
        if not options.really:
201
 
            new_normalized = normalize2text(tree)
202
 
            show_diff(org_normalized, new_normalized)
203
 
        else:
204
 
            new_text = render_xml(tree)
205
 
            if type(new_text) == type(u""):
206
 
                new_text = new_text.encode("utf8")
207
 
            postConfig(str('job/' + name + '/config.xml'), new_text,
208
 
                       extra_headers)
209
 
 
210
 
 
211
 
def main():
212
 
    if options.file:
213
 
        text = open(options.file).read()
214
 
        tree = fromstring(text)
215
 
 
216
 
        org_normalized = normalize2text(tree)
217
 
        mangler(tree)
218
 
        new_normalized = normalize2text(tree)
219
 
        show_diff(org_normalized, new_normalized)
220
 
    else:
221
 
        process_remote_jenkins()
222
 
 
223
 
if __name__ == "__main__":
224
 
    main()