diff options
author | Paul Sokolovsky <paul.sokolovsky@linaro.org> | 2013-09-03 15:33:21 +0300 |
---|---|---|
committer | Paul Sokolovsky <paul.sokolovsky@linaro.org> | 2013-09-03 15:33:21 +0300 |
commit | 7388665787d494588beb370ae48e3aefcc2968a6 (patch) | |
tree | 87d05debc6129fcfa636cfc153e73b19e8e77eb8 /utils | |
parent | ff4532f067b48b3b6821fe41332c3854500294ab (diff) | |
parent | 21c7586331929a6a2c4eefa9b806bd79a437f2d8 (diff) |
Add proof-of-concept command line client for Android Build service.
Diffstat (limited to 'utils')
-rw-r--r-- | utils/cmdline-client/README | 62 | ||||
-rwxr-xr-x | utils/cmdline-client/android-build-client | 199 |
2 files changed, 261 insertions, 0 deletions
diff --git a/utils/cmdline-client/README b/utils/cmdline-client/README new file mode 100644 index 0000000..8306b32 --- /dev/null +++ b/utils/cmdline-client/README @@ -0,0 +1,62 @@ +Basic android-build.linaro.org command-line client +================================================== + +This tool provides proof-of-concept command-line client for Linaro Android +Build Jenkins master (android-build.linaro.org), as an alternative for +existing Web UI interface. One particular feature the command-line client +has and Web UI lacks is support for creating completely private builds (such +builds are not availabel via Web UI at all). At the same time, this tool is +so far in the proof of concept stage and provides only basic job management +actions (namely, create a job and schedule its build). If you find this tool +useful, please share your comments and suggestion using this bugtracker: + +https://bugs.launchpad.net/linaro-android-infrastructure + +to help its improvement and evolution. + + +Quick start +----------- + +1. You should have access to Jenkins at: + +http://android-build.linaro.org/jenkins/ + +(Generally available for Linaro Android team members). + +2. Download android-build-client tool. As it is written to depend only +on Python standard library, an easy way to get it is to download seperate +file via Launchpad BZR viewer: + +wget <fill in link once branch lands> +chmod +x android-build-client + +3. Look up your Jenkins API token by visiting +https://android-build.linaro.org/jenkins/me/configure and clicking +"Show API Token..." button. + +4. Run: + +./android-build-client authorize + +Enter yous Jenkins username and API token. Note that this will cache these +credentials in your home dir, so use this only on your personal well-protected +workstation. The alternative is to use --user switch and input API key +interactively. + +5. To create a new job, prepare an Android job config (see +https://wiki.linaro.org/Platform/Android/LinaroAndroidBuildService for more +info) in a file. Run: + +./android-build-client create <job_name> <job_config_file> + +Note that job name should conform to Android Build Service naming +converntions, see documentation link above for more info. To create a job +private for a particular group, pass --private=<group> switch to create command. + +6. To schedule a build, run: + +./android-build-client build <job_name> + + +Run ./android-build-client --help to see all options. diff --git a/utils/cmdline-client/android-build-client b/utils/cmdline-client/android-build-client new file mode 100755 index 0000000..b236fc5 --- /dev/null +++ b/utils/cmdline-client/android-build-client @@ -0,0 +1,199 @@ +#!/usr/bin/env python +############################################################################### +# Copyright (c) 2013 Linaro +# All rights reserved. This program and the accompanying materials +# are made available under the terms of the Eclipse Public License v1.0 +# which accompanies this distribution, and is available at +# http://www.eclipse.org/legal/epl-v10.html +############################################################################### + +import base64 +import json +import os +import sys +import urllib2 +import optparse +import getpass +from xml.dom import minidom + +PRIVATE_ACL = """\ + <hudson.security.AuthorizationMatrixProperty> + <permission>hudson.model.Item.Build:%(group)s</permission> + <permission>hudson.model.Item.Configure:%(group)s</permission> + <permission>hudson.model.Item.Read:%(group)s</permission> + <permission>hudson.model.Item.Cancel:%(group)s</permission> + <permission>hudson.model.Item.Discover:%(group)s</permission> + </hudson.security.AuthorizationMatrixProperty> +""" + + +class Jenkins(object): + + def __init__(self, base_url, username, passwd): + self.base = base_url + self.auth_headers = { + 'Authorization': 'Basic %s' % ( + base64.b64encode('%s:%s' % (username, passwd)))} + self.csrf = None + + def jenkins_rest(self, jenkins_path, data=None, extra_headers=None): + """Make an authenticated request to jenkins. + + @param jenkins_path: The path on the Jenkins instance to make the + request to. + @param data: Data to include in the request (if this is not None the + request will be a POST). + @param extra_headers: A dictionary of extra headers that will passed in + addition to Authorization. + @raises urllib2.HTTPError: If the response is not a HTTP 200. + @returns: the body of the response. + """ + headers = self.auth_headers.copy() + if extra_headers: + headers.update(extra_headers) + url = self.base + jenkins_path + print url + req = urllib2.Request( + url, data, headers) + resp = urllib2.urlopen(req) + data = resp.read() + resp.close() + return data + + def get_csrf_headers(self): + if self.csrf is None: + try: + crumb_data = self.jenkins_rest('crumbIssuer/api/json') + data = json.loads(crumb_data) + self.csrf = {data['crumbRequestField']: data['crumb']} + except urllib2.HTTPError: + # Ignore errors in case CSRF protection is not enabled + self.csrf = {} + return self.csrf + + def jenkins_rest_post(self, jenkins_path, data, extra_headers=None): + if extra_headers is None: + extra_headers = {} + extra_headers.update(self.get_csrf_headers()) + return self.jenkins_rest(jenkins_path, data, extra_headers) + + def get_job_config(self, job_name): + return self.jenkins_rest('job/' + job_name + '/config.xml') + + def post_config(self, url, config_xml): + return self.jenkins_rest_post(url, config_xml, + {'Content-Type': 'text/xml'}) + + def set_job_config(self, job_name, config_xml): + return self.post_config('job/' + job_name + '/config.xml', config_xml) + + def create_job(self, job_name, config_xml): + return self.post_config('createItem?name=' + job_name, config_xml) + + def build_job(self, job_name): + self.jenkins_rest_post( + 'job/' + job_name + '/buildWithParameters?delay=0sec', '') + + +def job2user_group(job_group): + if job_group == "linaro-android": + return "linaro-android-builders" + return job_group + + +def error(msg): + print >>sys.stderr, msg + sys.exit(1) + + +def main(): + global options + optparser = optparse.OptionParser(usage="%prog authorize|create|build <args>...") + optparser.add_option("--url", + default="https://android-build.linaro.org/jenkins", + help="Jenkins base url, default: %default") + optparser.add_option("--user", + help="Jenkins username, default: $USER") + optparser.add_option("--apikey-file", metavar="FILE", + help="File holding Jenkins API key") + optparser.add_option("--cred-file", metavar="FILE", + help="File holding Jenkins username:API key pair") + optparser.add_option("--private", metavar="GROUP", + help="Create private job accessible to GROUP") + + options, args = optparser.parse_args(sys.argv[1:]) + if len(args) < 1: + optparser.error("Wrong number of arguments") + + config_dir = os.path.expanduser("~/.config/android-build-client") + + password = None + if options.cred_file: + options.user, password = open(options.cred_file).read().strip().split(":") + elif options.apikey_file: + password = open(options.passwd_file).read().strip() + elif os.path.exists(config_dir + "/cred"): + options.user, password = open(config_dir + "/cred").read().strip().split(":") + print "INFO: Using cached authorization for: %s" % options.user + elif args[0] != "authorize": + password = getpass.getpass("API Token:") + + if options.url[-1] != '/': + options.url += '/' + + j = Jenkins(options.url, options.user, password) + + if args[0] == "authorize": + if len(args) != 1: + optparser.error("Usage: authorize") + import getpass + user = raw_input("Enter user name: ") + while True: + key = getpass.getpass("API key: ") + if len(key) == 32: + break + print "This does not look like Jenkisn API key, please try again" + try: + os.makedirs(config_dir) + except OSError: + pass + f = open(config_dir + "/cred", "w") + print >>f, "%s:%s" % (user, key) + f.close() + print "Credentials cached for future use" + elif args[0] == "create": + if len(args) != 3: + optparser.error("Usage: create <job_name> <build_config_file>") + job_group, job_subname = args[1].split("/") + template = j.get_job_config("template_" + job2user_group(job_group)) + dom = minidom.parseString(template) + nodes = dom.getElementsByTagName("hudson.model.StringParameterDefinition") + assert len(nodes) == 1 + n = nodes[0].getElementsByTagName("defaultValue")[0] + build_config = open(args[2]).read() + n.childNodes[0].data = base64.encodestring(build_config) + + if options.private: + n = dom.getElementsByTagName("properties")[0] + c = n.getElementsByTagName("hudson.security.AuthorizationMatrixProperty")[0] + acl_dom = minidom.parseString(PRIVATE_ACL % {"group": options.private}) + n.replaceChild(acl_dom.documentElement, c) + + job_conf = dom.toxml() + jenkins_job = args[1].replace("/", "_") + try: + j.create_job(jenkins_job, job_conf) + except urllib2.HTTPError: + error("Error creating job '%s' (job exists?)" % jenkins_job) + print "Job created successfully: %s/job/%s/" % (options.url, jenkins_job) + elif args[0] == "build": + if len(args) != 2: + optparser.error("Usage: build <job_name>") + j.build_job(args[1].replace("/", "_")) + print "Build queued" + else: + optparser.error("Unknown command '%s'" % args[0]) + + +if __name__ == "__main__": + main() |