Skip to content

Commit

Permalink
Interim commit - I'm part way through refactoring all the networking …
Browse files Browse the repository at this point in the history
…stuff.

Posts are not yet working.
  • Loading branch information
salimfadhley committed Jun 5, 2013
1 parent b36d9df commit 6a5505a
Show file tree
Hide file tree
Showing 10 changed files with 155 additions and 130 deletions.
9 changes: 5 additions & 4 deletions README.rst
Original file line number Diff line number Diff line change
Expand Up @@ -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:

Expand All @@ -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
----------------
Expand Down Expand Up @@ -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']
<jenkinsapi.job.Job test_jenkinsapi>
Expand Down
27 changes: 27 additions & 0 deletions examples/create_a_job.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
from jenkinsapi.jenkins import Jenkins
J = Jenkins('http://localhost:8080')

EMPTY_JOB_CONFIG = '''\
<?xml version='1.0' encoding='UTF-8'?>
<project>
<actions/>
<description></description>
<keepDependencies>false</keepDependencies>
<properties/>
<scm class="hudson.scm.NullSCM"/>
<canRoam>true</canRoam>
<disabled>false</disabled>
<blockBuildWhenDownstreamBuilding>false</blockBuildWhenDownstreamBuilding>
<blockBuildWhenUpstreamBuilding>false</blockBuildWhenUpstreamBuilding>
<triggers class="vector"/>
<concurrentBuild>false</concurrentBuild>
<builders/>
<publishers/>
<buildWrappers/>
</project>
'''

new_job = J.create_job(jobname='foo_job', config=EMPTY_JOB_CONFIG)

j= J['foo_job']
print j
4 changes: 4 additions & 0 deletions jenkinsapi/api.py
Original file line number Diff line number Diff line change
@@ -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
Expand Down
110 changes: 33 additions & 77 deletions jenkinsapi/jenkins.py
Original file line number Diff line number Diff line change
@@ -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
Expand All @@ -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()
Expand Down Expand Up @@ -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)

Expand Down Expand Up @@ -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() ]

Expand Down
66 changes: 20 additions & 46 deletions jenkinsapi/jenkinsbase.py
Original file line number Diff line number Diff line change
Expand Up @@ -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):
Expand All @@ -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)
41 changes: 41 additions & 0 deletions jenkinsapi/utils/requester.py
Original file line number Diff line number Diff line change
@@ -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
2 changes: 1 addition & 1 deletion jenkinsapi_tests/systests/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -55,4 +55,4 @@ def setUpPackage():


def tearDownPackage():
launcher.stop()
launcher.stop()
22 changes: 22 additions & 0 deletions jenkinsapi_tests/unittests/test_jenkins.py
Original file line number Diff line number Diff line change
@@ -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()
Loading

0 comments on commit 6a5505a

Please sign in to comment.