From 6a5505afea3fe33e32b5a839535b59ec2dc16023 Mon Sep 17 00:00:00 2001 From: salimfadhley Date: Wed, 5 Jun 2013 01:30:41 +0100 Subject: [PATCH] Interim commit - I'm part way through refactoring all the networking stuff. Posts are not yet working. --- README.rst | 9 +- examples/create_a_job.py | 27 +++++ jenkinsapi/api.py | 4 + jenkinsapi/jenkins.py | 110 +++++++-------------- jenkinsapi/jenkinsbase.py | 66 ++++--------- jenkinsapi/utils/requester.py | 41 ++++++++ jenkinsapi_tests/systests/__init__.py | 2 +- jenkinsapi_tests/unittests/test_jenkins.py | 22 +++++ misc/jenkinsapi.sublime-project | 2 +- setup.py | 2 +- 10 files changed, 155 insertions(+), 130 deletions(-) create mode 100644 examples/create_a_job.py create mode 100644 jenkinsapi/utils/requester.py create mode 100644 jenkinsapi_tests/unittests/test_jenkins.py diff --git a/README.rst b/README.rst index fa02d934..9c36fca2 100644 --- a/README.rst +++ b/README.rst @@ -9,9 +9,9 @@ jenkinsapi About this library ------------------- -Jenkins is the market leading continuous integration system, originally created by Kohsuke Kawaguchi. This API makes Jenkins even easier to use by providing an easy to use conventional python interface. +Jenkins is the market leading continuous integration system, originally created by Kohsuke Kawaguchi. -Jenkins (and It's predecessor Hudson) are useful projects for automating common development tasks (e.g. unit-testing, production batches) - but they are somewhat Java-centric. Thankfully the designers have provided an excellent and complete REST interface. This library wraps up that interface as more conventional python objects in order to make most Jenkins oriented tasks simpler. +Jenkins (and It's predecessor Hudson) are useful projects for automating common development tasks (e.g. unit-testing, production batches) - but they are somewhat Java-centric. Thankfully the designers have provided an excellent and complete REST interface. This library wraps up that interface as more conventional python objects in order to make many Jenkins oriented tasks easier to automate. This library can help you: @@ -22,7 +22,8 @@ This library can help you: * Install artefacts to custom-specified directory structures * username/password auth support for jenkins instances with auth turned on * Ability to search for builds by subversion revision - * Ability to add/remove/query jenkins slaves + * Ability to add/remove/query Jenkins slaves + * Ability to add/remove/modify Jenkins views Important Links ---------------- @@ -57,7 +58,7 @@ JenkinsAPI is intended to map the objects in Jenkins (e.g. Builds, Views, Jobs) >>> import jenkinsapi >>> from jenkinsapi.jenkins import Jenkins >>> J = Jenkins('http://localhost:8080') - >>> J.keys() + >>> J.keys() # Jenkins objects appear to be dict-like, mapping keys (job-names) to ['foo', 'test_jenkinsapi'] >>> J['test_jenkinsapi'] diff --git a/examples/create_a_job.py b/examples/create_a_job.py new file mode 100644 index 00000000..1f053ed0 --- /dev/null +++ b/examples/create_a_job.py @@ -0,0 +1,27 @@ +from jenkinsapi.jenkins import Jenkins +J = Jenkins('http://localhost:8080') + +EMPTY_JOB_CONFIG = '''\ + + + + + false + + + true + false + false + false + + false + + + + +''' + +new_job = J.create_job(jobname='foo_job', config=EMPTY_JOB_CONFIG) + +j= J['foo_job'] +print j \ No newline at end of file diff --git a/jenkinsapi/api.py b/jenkinsapi/api.py index dbf6fb5f..26e156e0 100644 --- a/jenkinsapi/api.py +++ b/jenkinsapi/api.py @@ -1,3 +1,7 @@ +""" +This module is a collection of helpful, high-level functions for automating common tasks. +Many of these functions were designed to be exposed to the command-line, hence the have simple string arguments. +""" from jenkinsapi.artifact import Artifact from jenkinsapi import constants from jenkinsapi.jenkins import Jenkins diff --git a/jenkinsapi/jenkins.py b/jenkinsapi/jenkins.py index 222a0cbd..b624a4f1 100644 --- a/jenkinsapi/jenkins.py +++ b/jenkinsapi/jenkins.py @@ -1,18 +1,22 @@ -from jenkinsapi.exceptions import UnknownJob, NotAuthorized -from jenkinsapi.fingerprint import Fingerprint -from jenkinsapi.jenkinsbase import JenkinsBase -from jenkinsapi.job import Job -from jenkinsapi.node import Node -from jenkinsapi.queue import Queue -from jenkinsapi.view import View -from jenkinsapi import config -from utils.urlopener import mkurlopener, mkopener, NoAuto302Handler -import cookielib -import logging import time import urllib import urllib2 +import logging import urlparse +import requests +import StringIO +import cookielib +from utils.urlopener import mkurlopener, mkopener, NoAuto302Handler + +from jenkinsapi import config +from jenkinsapi.job import Job +from jenkinsapi.node import Node +from jenkinsapi.queue import Queue +from jenkinsapi.view import View +from jenkinsapi.fingerprint import Fingerprint +from jenkinsapi.jenkinsbase import JenkinsBase +from jenkinsapi.utils.requester import Requester +from jenkinsapi.exceptions import UnknownJob, NotAuthorized try: import json @@ -32,84 +36,29 @@ class Jenkins(JenkinsBase): """ Represents a jenkins environment. """ - def __init__(self, baseurl, username=None, password=None, proxyhost=None, proxyport=None, proxyuser=None, proxypass=None, formauth=False, krbauth=False): + def __init__(self, baseurl, username=None, password=None, requester=None): """ - :param baseurl: baseurl for jenkins instance including port, str :param username: username for jenkins auth, str :param password: password for jenkins auth, str - :param proxyhost: proxyhostname, str - :param proxyport: proxyport, int - :param proxyuser: proxyusername for proxy auth, str - :param proxypass: proxypassword for proxyauth, str :return: a Jenkins obj """ self.username = username self.password = password - self.proxyhost = proxyhost - self.proxyport = proxyport - self.proxyuser = proxyuser - self.proxypass = proxypass - JenkinsBase.__init__(self, baseurl, formauth=formauth, krbauth=krbauth) + self.requester = requester or Requester(username, password) + JenkinsBase.__init__(self, baseurl) def _clone(self): - return Jenkins(self.baseurl, username=self.username, - password=self.password, proxyhost=self.proxyhost, - proxyport=self.proxyport, proxyuser=self.proxyuser, - proxypass=self.proxypass, formauth=self.formauth, krbauth=self.krbauth) - - def get_proxy_auth(self): - return self.proxyhost, self.proxyport, self.proxyuser, self.proxypass - - def get_jenkins_auth(self): - return self.username, self.password, self.baseurl - - def get_auth(self): - auth_args = [] - auth_args.extend(self.get_jenkins_auth()) - auth_args.extend(self.get_proxy_auth()) - log.debug("auth_args: %s" % auth_args) - return auth_args + return Jenkins(self.baseurl, username=self.username, password=self.password, requester=self.requester) def get_base_server_url(self): return self.baseurl[:-(len(config.JENKINS_API))] - def get_opener(self): - if self.formauth: - return self.get_login_opener() - if self.krbauth: - return self.get_krb_opener() - return mkurlopener(*self.get_auth()) - - def get_login_opener(self): - hdrs = [] - if getattr(self, '_cookies', False): - mcj = cookielib.MozillaCookieJar() - for c in self._cookies: - mcj.set_cookie(c) - hdrs.append(urllib2.HTTPCookieProcessor(mcj)) - return mkopener(*hdrs) - def get_krb_opener(self): if not mkkrbopener: raise NotImplementedError('JenkinsAPI was installed without Kerberos support.') return mkkrbopener(self.baseurl) - def login(self): - formdata = dict(j_username=self.username, j_password=self.password, - remember_me=True, form='/') - formdata.update(dict(json=json.dumps(formdata), Submit='log in')) - formdata = urllib.urlencode(formdata) - - loginurl = urlparse.urljoin(self.baseurl, 'j_acegi_security_check') - mcj = cookielib.MozillaCookieJar() - cookiehandler = urllib2.HTTPCookieProcessor(mcj) - - urlopen = mkopener(NoAuto302Handler, cookiehandler) - res = urlopen(loginurl, data=formdata) - self._cookies = [c for c in mcj] - return res.getcode() == 302 - def validate_fingerprint(self, id): obj_fingerprint = Fingerprint(self.baseurl, id, jenkins_obj=self) obj_fingerprint.validate() @@ -184,10 +133,8 @@ def create_job(self, jobname, config): :return: new Job obj """ headers = {'Content-Type': 'text/xml'} - qs = urllib.urlencode({'name': jobname}) - url = urlparse.urljoin(self.baseurl, "createItem?%s" % qs) - request = urllib2.Request(url, config, headers) - self.post_data(request, None) + params = {'name': jobname} + self.requester.hit_url(self.baseurl, data=config, params=params, headers=headers) newjk = self._clone() return newjk.get_job(jobname) @@ -230,13 +177,22 @@ def rename_job(self, jobname, newjobname): newjk = self._clone() return newjk.get_job(newjobname) - def iteritems(self): - return self.get_jobs() - def iterkeys(self): for info in self._data["jobs"]: yield info["name"] + def iteritems(self): + """ + :param return: An iterator of pairs. Each pair will be (job name, Job object) + """ + return self.get_jobs() + + def items(self): + """ + :param return: A list of pairs. Each pair will be (job name, Job object) + """ + return list(self.get_jobs()) + def keys(self): return [ a for a in self.iterkeys() ] diff --git a/jenkinsapi/jenkinsbase.py b/jenkinsapi/jenkinsbase.py index 18137f8f..da103cb7 100644 --- a/jenkinsapi/jenkinsbase.py +++ b/jenkinsapi/jenkinsbase.py @@ -24,31 +24,40 @@ def print_data(self): def __str__(self): raise NotImplemented - def __init__(self, baseurl, poll=True, formauth=False, krbauth=False): + def __init__(self, baseurl, poll=True): """ Initialize a jenkins connection """ self.baseurl = baseurl - self.formauth = formauth - self.krbauth = krbauth - if poll and not self.formauth: + if poll: try: self.poll() - except urllib2.HTTPError, hte: + except urllib2.HTTPError, hte: #TODO: Wrong exception log.exception(hte) log.warn( "Failed to connect to %s" % baseurl ) raise + def __eq__(self, other): + """ + Return true if the other object represents a connection to the same server + """ + if not isinstance(other, self.__class__): + return False + if not other.baseurl == self.baseurl: + return False + return True + def poll(self): self._data = self._poll() def _poll(self): url = self.python_api_url(self.baseurl) - return retry_function(self.RETRY_ATTEMPTS , self.get_data, url) - - def get_jenkins_obj(self): - """Not implemented, abstract method implemented by child classes""" - raise NotImplemented("Abstract method, implemented by child classes") + requester = self.get_jenkins_obj().requester + content = retry_function(self.RETRY_ATTEMPTS , requester.hit_url, url) + try: + return eval(content) + except SyntaxError: + log.exception('Inappropriate content found at %s' % url) @classmethod def python_api_url(cls, url): @@ -59,39 +68,4 @@ def python_api_url(cls, url): fmt="%s%s" else: fmt = "%s/%s" - return fmt % (url, config.JENKINS_API) - - def get_data(self, url): - """ - Find out how to connect, and then grab the data. - """ - fn_urlopen = self.get_jenkins_obj().get_opener() - try: - stream = fn_urlopen(url) - result = eval(stream.read()) - except urllib2.HTTPError, e: - if e.code == 404: - raise - log.exception("Error reading %s" % url) - raise - return result - - def post_data(self, url, content): - try: - urlopen = self.get_jenkins_obj().get_opener() - result = urlopen(url, data=content).read().strip() - except urllib2.HTTPError: - log.exception("Error post data %s" % url) - raise - return result - - def hit_url(self, url, params = None): - fn_urlopen = self.get_jenkins_obj().get_opener() - try: - if params: stream = fn_urlopen( url, urllib.urlencode(params) ) - else: stream = fn_urlopen( url ) - html_result = stream.read() - except urllib2.HTTPError: - log.exception("Error reading %s" % url) - raise - return html_result + return fmt % (url, config.JENKINS_API) \ No newline at end of file diff --git a/jenkinsapi/utils/requester.py b/jenkinsapi/utils/requester.py new file mode 100644 index 00000000..6683dbe3 --- /dev/null +++ b/jenkinsapi/utils/requester.py @@ -0,0 +1,41 @@ +import StringIO +import requests + +class Requester(object): + + """ + A class which carries out HTTP requests. You can replace this class with one of your own implementation if you require + some other way to access Jenkins. + + This default class can handle simple authentication only. + """ + + def __init__(self, username=None, password=None): + if username: + assert password, 'Cannot set a username without a password!' + + self.username = None + self.password = None + + def hit_url(self, url, params=None, data=None, headers=None): + requestKwargs = {} + if self.username: + requestKwargs['auth'] = (self.username, self.password) + + if params: + assert isinstance(params, dict), 'Params must be a dict, got %s' % repr(params) + requestKwargs['params'] = params + + if headers: + assert isinstance(headers, dict), 'headers must be a dict, got %s' % repr(headers) + requestKwargs['headers'] = headers + + if data: + requestKwargs['data'] = data + response = requests.post(url, **requestKwargs) + else: + response = requests.get(url, **requestKwargs) + + import ipdb; ipdb.set_trace() + + return response.text diff --git a/jenkinsapi_tests/systests/__init__.py b/jenkinsapi_tests/systests/__init__.py index d1f2514b..fb476097 100644 --- a/jenkinsapi_tests/systests/__init__.py +++ b/jenkinsapi_tests/systests/__init__.py @@ -55,4 +55,4 @@ def setUpPackage(): def tearDownPackage(): - launcher.stop() + launcher.stop() \ No newline at end of file diff --git a/jenkinsapi_tests/unittests/test_jenkins.py b/jenkinsapi_tests/unittests/test_jenkins.py new file mode 100644 index 00000000..8f287793 --- /dev/null +++ b/jenkinsapi_tests/unittests/test_jenkins.py @@ -0,0 +1,22 @@ +import mock +import unittest +import datetime + +from jenkinsapi.jenkins import Jenkins + +class TestJenkins(unittest.TestCase): + + DATA = {} + + @mock.patch.object(Jenkins, '_poll') + def setUp(self, _poll): + _poll.return_value = self.DATA + self.J = Jenkins('http://localhost:8080', username='foouser', password='foopassword') + + def testClone(self): + JJ = self.J._clone() + self.assertNotEquals(id(JJ), id(self.J)) + self.assertEquals(JJ, self.J) + +if __name__ == '__main__': + unittest.main() \ No newline at end of file diff --git a/misc/jenkinsapi.sublime-project b/misc/jenkinsapi.sublime-project index dcb7c2fe..b8d238ce 100644 --- a/misc/jenkinsapi.sublime-project +++ b/misc/jenkinsapi.sublime-project @@ -24,7 +24,7 @@ { "name":"Virtualenv 2.7", - "working_dir": "${project_path:${folder}}/src", + //"working_dir": "${project_path:${folder}}/src", "cmd": [ "${project_path}/bin/python2.7", diff --git a/setup.py b/setup.py index cf541a06..e173e8b5 100644 --- a/setup.py +++ b/setup.py @@ -25,7 +25,7 @@ packages=['jenkinsapi', 'jenkinsapi.utils', 'jenkinsapi.command_line', 'jenkinsapi_tests'], zip_safe=True, include_package_data=False, - install_requires=[], + install_requires=['requests==1.2.3'], test_suite='jenkinsapi_tests', tests_require=['mock', 'nose'], extras_require={