From 32ae55ef135a94de445dfb761c223bbbb5db1c4c Mon Sep 17 00:00:00 2001 From: Ramon van Alteren Date: Tue, 3 Jan 2012 17:47:22 +0100 Subject: [PATCH] Added authentification and node classes In order to add nodes, I need to be able to authenticate myself against jenkins Added additional code to the opener to allow authentication using a custom AuthHandler Jenkins uses BasicAuth without a challenge, so I had to implement a small subclass which I pulled from stackoverflow. Node class + methods on Jenkins class added Ability to add and remove nodes, check for presence and basic data --- pyjenkinsci/jenkins.py | 116 +++++++++++++++++++++++++++++++-- pyjenkinsci/node.py | 108 +++--------------------------- pyjenkinsci/utils/urlopener.py | 88 ++++++++++++++++++------- 3 files changed, 186 insertions(+), 126 deletions(-) diff --git a/pyjenkinsci/jenkins.py b/pyjenkinsci/jenkins.py index 76cc6b8..1b9ccf8 100644 --- a/pyjenkinsci/jenkins.py +++ b/pyjenkinsci/jenkins.py @@ -7,6 +7,9 @@ from pyjenkinsci.exceptions import UnknownJob from utils.urlopener import mkurlopener import logging import time +import urllib2 +import urllib +import json log = logging.getLogger(__name__) @@ -14,7 +17,20 @@ class Jenkins(JenkinsBase): """ Represents a jenkins environment. """ - def __init__(self, baseurl, proxyhost=None, proxyport=None, proxyuser=None, proxypass=None): + def __init__(self, baseurl, username=None, password=None, proxyhost=None, proxyport=None, proxyuser=None, proxypass=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 @@ -22,10 +38,21 @@ class Jenkins(JenkinsBase): JenkinsBase.__init__( self, baseurl ) def get_proxy_auth(self): - return (self.proxyhost, self.proxyport, self.proxyuser, self.proxypass) + 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("args: %s" % auth_args) + return auth_args + def get_opener( self ): - return mkurlopener(*self.get_proxy_auth()) + return mkurlopener(*self.get_auth()) def validate_fingerprint( self, id ): obj_fingerprint = Fingerprint(self.baseurl, id, jenkins_obj=self) @@ -99,7 +126,10 @@ class Jenkins(JenkinsBase): def get_node_dict(self): """Get registered slave nodes on this instance""" url = self.python_api_url(self.get_node_url()) - return dict(self.get_data(url)) + node_dict = dict(self.get_data(url)) + return dict( + (node['displayName'], self.python_api_url(self.get_node_url(node['displayName']))) + for node in node_dict['computer']) def get_node(self, nodename): """Get a node object for a specific node""" @@ -110,3 +140,81 @@ class Jenkins(JenkinsBase): """Return the url for nodes""" url = "%(baseurl)s/computer/%(nodename)s" % {'baseurl': self.baseurl, 'nodename': nodename} return url + + def has_node(self, nodename): + """ + Does a node by the name specified exist + :param nodename: string, hostname + :return: boolean + """ + return nodename in self.get_node_dict() + + def delete_node(self, nodename): + """ + Remove a node from the managed slave list + Please note that you cannot remove the master node + + :param nodename: string holding a hostname + :return: None + """ + assert self.has_node(nodename), "This node: %s is not registered as a slave" % nodename + assert nodename != "master", "you cannot delete the master node" + url = "%s/doDelete" % self.get_node_url(nodename) + fn_urlopen = self.get_jenkins_obj().get_opener() + try: + stream = fn_urlopen(url) + html_result = stream.read() + except urllib2.HTTPError, e: + log.debug("Error reading %s" % url) + raise + return not self.has_node(nodename) + + def create_node(self, name, num_executors=2, node_description=None, + remote_fs='/var/lib/jenkins', labels=None, exclusive=False): + """ + Create a new slave node by name. + + :param name: fqdn of slave, str + :param num_executors: number of executors, int + :param node_description: a freetext field describing the node + :param remote_fs: jenkins path, str + :param labels: labels to associate with slave, str + :param exclusive: tied to specific job, boolean + :return: node obj + """ + NODE_TYPE = 'hudson.slaves.DumbSlave$DescriptorImpl' + MODE = 'NORMAL' + if self.has_node(name): + return Node(nodename=name, baseurl=self.get_node_url(nodename=name), jenkins_obj=self) + if exclusive: + MODE = 'EXCLUSIVE' + params = { + 'name' : name, + 'type' : NODE_TYPE, + 'json' : json.dumps ({ + 'name' : name, + 'nodeDescription' : node_description, + 'numExecutors' : num_executors, + 'remoteFS' : remote_fs, + 'labelString' : labels, + 'mode' : MODE, + 'type' : NODE_TYPE, + 'retentionStrategy' : { 'stapler-class' : 'hudson.slaves.RetentionStrategy$Always' }, + 'nodeProperties' : { 'stapler-class-bag' : 'true' }, + 'launcher' : { 'stapler-class' : 'hudson.slaves.JNLPLauncher' } + }) + } + url = "%(nodeurl)s/doCreateItem?%(params)s" % { + 'nodeurl': self.get_node_url(), + 'params': urllib.urlencode(params) + } + print url + fn_urlopen = self.get_jenkins_obj().get_opener() + try: + stream = fn_urlopen(url) + html_result = stream.read() + except urllib2.HTTPError, e: + log.debug("Error reading %s" % url) + log.exception(e) + raise + return Node(nodename=name, baseurl=self.get_node_url(nodename=name), jenkins_obj=self) diff --git a/pyjenkinsci/node.py b/pyjenkinsci/node.py index 332c4db..7c0a00c 100644 --- a/pyjenkinsci/node.py +++ b/pyjenkinsci/node.py @@ -29,105 +29,15 @@ class Node(JenkinsBase): def __str__(self): return self.id() + def get_node_data(self): + return self._data + def is_online(self): return not self._data['offline'] -#NODES = 'computer/api/json' -#CREATE_NODE = 'computer/doCreateItem?%s' -#DELETE_NODE = 'computer/%(name)s/doDelete' -#NODE_INFO = 'computer/%(name)s/api/json?depth=0' -#NODE_TYPE = 'hudson.slaves.DumbSlave$DescriptorImpl' -# -#def get_nodes(self): -# ''' -# Get a list of nodes registered on the jenkins instance -# returns a list of node dictionaries -# ''' -# try: -# response = self.jenkins_open(urllib2.Request(self.server + NODES%locals())) -# if response: -# return json.loads(response)['computer'] -# else: -# raise JenkinsException('Cannot get nodes information') -# except urllib2.HTTPError: -# raise JenkinsException('Cannot get nodes information') -# except ValueError: -# raise JenkinsException("Could not parse JSON info for nodes information") -# -#def get_node_info(self, name): -# ''' -# Get node information dictionary -# -# :param name: Node name, ``str`` -# :returns: Dictionary of node info, ``dict`` -# ''' -# try: -# response = self.jenkins_open(urllib2.Request(self.server + NODE_INFO%locals())) -# if response: -# return json.loads(response) -# else: -# raise JenkinsException('node[%s] does not exist'%name) -# except urllib2.HTTPError: -# raise JenkinsException('node[%s] does not exist'%name) -# except ValueError: -# raise JenkinsException("Could not parse JSON info for node[%s]"%name) -# -#def node_exists(self, name): -# ''' -# :param name: Name of Jenkins node, ``str`` -# :returns: ``True`` if Jenkins node exists -# ''' -# try: -# self.get_node_info(name) -# return True -# except JenkinsException: -# return False -# -#def delete_node(self, name): -# ''' -# Delete Jenkins node permanently. -# -# :param name: Name of Jenkins node, ``str`` -# ''' -# self.get_node_info(name) -# self.jenkins_open(urllib2.Request(self.server + DELETE_NODE%locals(), '')) -# if self.node_exists(name): -# raise JenkinsException('delete[%s] failed'%(name)) -# -#def create_node(self, name, numExecutors=2, nodeDescription=None, -# remoteFS='/var/lib/jenkins', labels=None, exclusive=False): -# ''' -# :param name: name of node to create, ``str`` -# :param numExecutors: number of executors for node, ``int`` -# :param nodeDescription: Description of node, ``str`` -# :param remoteFS: Remote filesystem location to use, ``str`` -# :param labels: Labels to associate with node, ``str`` -# :param exclusive: Use this node for tied jobs only, ``bool`` -# ''' -# if self.node_exists(name): -# raise JenkinsException('node[%s] already exists'%(name)) -# -# mode = 'NORMAL' -# if exclusive: -# mode = 'EXCLUSIVE' -# -# params = { -# 'name' : name, -# 'type' : NODE_TYPE, -# 'json' : json.dumps ({ -# 'name' : name, -# 'nodeDescription' : nodeDescription, -# 'numExecutors' : numExecutors, -# 'remoteFS' : remoteFS, -# 'labelString' : labels, -# 'mode' : mode, -# 'type' : NODE_TYPE, -# 'retentionStrategy' : { 'stapler-class' : 'hudson.slaves.RetentionStrategy$Always' }, -# 'nodeProperties' : { 'stapler-class-bag' : 'true' }, -# 'launcher' : { 'stapler-class' : 'hudson.slaves.JNLPLauncher' } -# }) -# } -# -# self.jenkins_open(urllib2.Request(self.server + CREATE_NODE%urllib.urlencode(params))) -# if not self.node_exists(name): -# raise JenkinsException('create[%s] failed'%(name)) + def is_jnlpagent(self): + return self._data['jnlpAgent'] + + def is_idle(self): + return self._data['idle'] + diff --git a/pyjenkinsci/utils/urlopener.py b/pyjenkinsci/utils/urlopener.py index b3739df..e6b676f 100644 --- a/pyjenkinsci/utils/urlopener.py +++ b/pyjenkinsci/utils/urlopener.py @@ -1,37 +1,79 @@ import urllib2 +import urlparse +import base64 import logging log = logging.getLogger( __name__ ) -DEFAULT_PROXYPORT = 80 -DEFAULT_PROXY_PASS = "Research123" -DEFAULT_PROXY_USER = "wsa_oblicqs_dev" +class PreemptiveBasicAuthHandler(urllib2.BaseHandler): -def mkurlopener( proxyhost, proxyport, proxyuser, proxypass ): - if not proxyhost: - return urllib2.urlopen - else: - if proxyport is None: - proxyport = DEFAULT_PROXYPORT + def __init__(self, password_mgr=None): + if password_mgr is None: + password_mgr = urllib2.HTTPPasswordMgrWithDefaultRealm() + self.passwd = password_mgr + self.add_password = self.passwd.add_password - if proxypass is None: - proxypass = DEFAULT_PROXY_PASS + def http_request(self,req): + uri = req.get_full_url() + user, pw = self.passwd.find_user_password(None,uri) + log.debug('ADDING REQUEST HEADER for uri (%s): %s:%s' % (uri,user,pw)) + if pw is None: return req + raw = "%s:%s" % (user, pw) + auth = 'Basic %s' % base64.b64encode(raw).strip() + req.add_unredirected_header('Authorization', auth) + return req - if proxyuser is None: - proxyuser = DEFAULT_PROXY_USER +def mkurlopener( jenkinsuser, jenkinspass, jenkinsurl, proxyhost, proxyport, proxyuser, proxypass ): + handlers = [] + for handler in get_jenkins_auth_handler(jenkinsuser=jenkinsuser, jenkinspass=jenkinspass, jenkinsurl=jenkinsurl): + handlers.append(handler) + for handler in get_proxy_handler(proxyhost, proxyport, proxyuser, proxypass): + handlers.append(handler) + opener = urllib2.build_opener(*handlers) + return opener.open - assert type( proxyport ) == int, "Proxy port should be an int, got %s" % repr( proxyport ) - assert type( proxypass ) == str, "Proxy password should be a sting, got %s" % repr( proxypass ) - assert type( proxyuser ) == str, "Proxy username should be a string, got %s" % repr( proxyuser ) +def get_jenkins_auth_handler(jenkinsuser, jenkinspass, jenkinsurl): + """ + Get a basic authentification handler for jenkins + :param jenkinsuser: jenkins username, str + :param jenkinspass: jenkins password, str + :param jenkinsurl: jenkins base url, str + :return: a list of handlers + """ + for param in jenkinsuser, jenkinspass, jenkinsurl: + if param is None: + return [] + assert type(jenkinsuser) == str, "Jenkins username should be a string, got %s" % repr(jenkinsuser) + assert type(jenkinspass) == str, "Jenkins password should be a string, git %s" % repr(jenkinspass) +# hostname = urlparse.urlsplit(jenkinsurl).hostname + handler = PreemptiveBasicAuthHandler() + handler.add_password(None, jenkinsurl, jenkinsuser, jenkinspass) + log.debug('Adding BasicAuthHandler: url:%s, user:%s,' % (jenkinsurl, jenkinsuser)) + return [ handler ] - proxy_spec = { 'http': 'http://%s:%i/' % (proxyhost, proxyport), - 'https': 'http://%s:%i/' % (proxyhost, proxyport) } +def get_proxy_handler(proxyhost, proxyport, proxyuser, proxypass): + """ + Get a configured handler for a proxy - proxy_handler = urllib2.ProxyHandler( proxy_spec ) - proxy_auth_handler = urllib2.HTTPBasicAuthHandler() - proxy_auth_handler.add_password( None, proxyhost, proxyuser, proxypass ) + :param proxyhost: proxy hostname, str + :param proxyport: proxy port, int + :param proxyuser: proxy username, str + :param proxypass: proxy password, str + :return: list of handlers + """ + for param in proxyhost, proxyport, proxyuser, proxypass: + if param is None: + return [] + assert type( proxyport ) == int, "Proxy port should be an int, got %s" % repr( proxyport ) + assert type( proxypass ) == str, "Proxy password should be a sting, got %s" % repr( proxypass ) + assert type( proxyuser ) == str, "Proxy username should be a string, got %s" % repr( proxyuser ) - opener = urllib2.build_opener(proxy_handler, proxy_auth_handler) + proxy_spec = { 'http': 'http://%s:%i/' % (proxyhost, proxyport), + 'https': 'http://%s:%i/' % (proxyhost, proxyport) } + + proxy_handler = urllib2.ProxyHandler( proxy_spec ) + proxy_auth_handler = urllib2.HTTPBasicAuthHandler() + proxy_auth_handler.add_password( None, proxyhost, proxyuser, proxypass ) + return [proxy_handler, proxy_auth_handler] - return opener.open -- 2.34.1