Added authentification and node classes
authorRamon van Alteren <ramon@vanalteren.nl>
Tue, 3 Jan 2012 16:47:22 +0000 (17:47 +0100)
committerRamon van Alteren <ramon@vanalteren.nl>
Tue, 3 Jan 2012 16:47:22 +0000 (17:47 +0100)
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
pyjenkinsci/node.py
pyjenkinsci/utils/urlopener.py

index 76cc6b8..1b9ccf8 100644 (file)
@@ -7,6 +7,9 @@ from pyjenkinsci.exceptions import UnknownJob
 from utils.urlopener import mkurlopener\r
 import logging\r
 import time\r
+import urllib2\r
+import urllib\r
+import json\r
 \r
 log = logging.getLogger(__name__)\r
 \r
@@ -14,7 +17,20 @@ class Jenkins(JenkinsBase):
     """\r
     Represents a jenkins environment.\r
     """\r
-    def __init__(self, baseurl, proxyhost=None, proxyport=None, proxyuser=None, proxypass=None):\r
+    def __init__(self, baseurl, username=None, password=None, proxyhost=None, proxyport=None, proxyuser=None, proxypass=None):\r
+        """\r
+\r
+        :param baseurl: baseurl for jenkins instance including port, str\r
+        :param username: username for jenkins auth, str\r
+        :param password: password for jenkins auth, str\r
+        :param proxyhost: proxyhostname, str\r
+        :param proxyport: proxyport, int\r
+        :param proxyuser: proxyusername for proxy auth, str\r
+        :param proxypass: proxypassword for proxyauth, str\r
+        :return: a Jenkins obj\r
+        """\r
+        self.username = username\r
+        self.password = password\r
         self.proxyhost = proxyhost\r
         self.proxyport = proxyport\r
         self.proxyuser = proxyuser\r
@@ -22,10 +38,21 @@ class Jenkins(JenkinsBase):
         JenkinsBase.__init__( self, baseurl )\r
 \r
     def get_proxy_auth(self):\r
-        return (self.proxyhost, self.proxyport, self.proxyuser, self.proxypass)\r
+        return self.proxyhost, self.proxyport, self.proxyuser, self.proxypass\r
+\r
+    def get_jenkins_auth(self):\r
+        return self.username, self.password, self.baseurl\r
+\r
+    def get_auth(self):\r
+        auth_args = []\r
+        auth_args.extend(self.get_jenkins_auth())\r
+        auth_args.extend(self.get_proxy_auth())\r
+        log.debug("args: %s" % auth_args)\r
+        return auth_args\r
+\r
 \r
     def get_opener( self ):\r
-        return mkurlopener(*self.get_proxy_auth())\r
+        return mkurlopener(*self.get_auth())\r
 \r
     def validate_fingerprint( self, id ):\r
         obj_fingerprint = Fingerprint(self.baseurl, id, jenkins_obj=self)\r
@@ -99,7 +126,10 @@ class Jenkins(JenkinsBase):
     def get_node_dict(self):\r
         """Get registered slave nodes on this instance"""\r
         url = self.python_api_url(self.get_node_url())\r
-        return dict(self.get_data(url))\r
+        node_dict = dict(self.get_data(url))\r
+        return dict(\r
+            (node['displayName'], self.python_api_url(self.get_node_url(node['displayName'])))\r
+                for node in node_dict['computer'])\r
 \r
     def get_node(self, nodename):\r
         """Get a node object for a specific node"""\r
@@ -110,3 +140,81 @@ class Jenkins(JenkinsBase):
         """Return the url for nodes"""\r
         url = "%(baseurl)s/computer/%(nodename)s" % {'baseurl': self.baseurl, 'nodename': nodename}\r
         return url\r
+\r
+    def has_node(self, nodename):\r
+        """\r
+        Does a node by the name specified exist\r
+        :param nodename: string, hostname\r
+        :return: boolean\r
+        """\r
+        return nodename in self.get_node_dict()\r
+\r
+    def delete_node(self, nodename):\r
+        """\r
+        Remove a node from the managed slave list\r
+        Please note that you cannot remove the master node\r
+\r
+        :param nodename: string holding a hostname\r
+        :return: None\r
+        """\r
+        assert self.has_node(nodename), "This node: %s is not registered as a slave" % nodename\r
+        assert nodename != "master", "you cannot delete the master node"\r
+        url = "%s/doDelete" % self.get_node_url(nodename)\r
+        fn_urlopen = self.get_jenkins_obj().get_opener()\r
+        try:\r
+            stream = fn_urlopen(url)\r
+            html_result = stream.read()\r
+        except urllib2.HTTPError, e:\r
+            log.debug("Error reading %s" % url)\r
+            raise\r
+        return not self.has_node(nodename)\r
+\r
+    def create_node(self, name, num_executors=2, node_description=None,\r
+                    remote_fs='/var/lib/jenkins', labels=None, exclusive=False):\r
+        """\r
+        Create a new slave node by name.\r
+\r
+        :param name: fqdn of slave, str\r
+        :param num_executors: number of executors, int\r
+        :param node_description: a freetext field describing the node\r
+        :param remote_fs: jenkins path, str\r
+        :param labels: labels to associate with slave, str\r
+        :param exclusive: tied to specific job, boolean\r
+        :return: node obj\r
+        """\r
+        NODE_TYPE   = 'hudson.slaves.DumbSlave$DescriptorImpl'\r
+        MODE = 'NORMAL'\r
+        if self.has_node(name):\r
+            return Node(nodename=name, baseurl=self.get_node_url(nodename=name), jenkins_obj=self)\r
+        if exclusive:\r
+            MODE = 'EXCLUSIVE'\r
+        params = {\r
+            'name' : name,\r
+            'type' : NODE_TYPE,\r
+            'json' : json.dumps ({\r
+                'name'            : name,\r
+                'nodeDescription' : node_description,\r
+                'numExecutors'    : num_executors,\r
+                'remoteFS'        : remote_fs,\r
+                'labelString'     : labels,\r
+                'mode'            : MODE,\r
+                'type'            : NODE_TYPE,\r
+                'retentionStrategy' : { 'stapler-class'  : 'hudson.slaves.RetentionStrategy$Always' },\r
+                'nodeProperties'    : { 'stapler-class-bag' : 'true' },\r
+                'launcher'          : { 'stapler-class' : 'hudson.slaves.JNLPLauncher' }\r
+            })\r
+        }\r
+        url = "%(nodeurl)s/doCreateItem?%(params)s" % {\r
+            'nodeurl': self.get_node_url(),\r
+            'params': urllib.urlencode(params)\r
+        }\r
+        print url\r
+        fn_urlopen = self.get_jenkins_obj().get_opener()\r
+        try:\r
+            stream = fn_urlopen(url)\r
+            html_result = stream.read()\r
+        except urllib2.HTTPError, e:\r
+            log.debug("Error reading %s" % url)\r
+            log.exception(e)\r
+            raise\r
+        return Node(nodename=name, baseurl=self.get_node_url(nodename=name), jenkins_obj=self)\r
index 332c4db..7c0a00c 100644 (file)
@@ -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']
+
index b3739df..e6b676f 100644 (file)
@@ -1,37 +1,79 @@
 import urllib2\r
+import urlparse\r
+import base64\r
 \r
 import logging\r
 \r
 log = logging.getLogger( __name__ )\r
 \r
-DEFAULT_PROXYPORT = 80\r
-DEFAULT_PROXY_PASS = "Research123"\r
-DEFAULT_PROXY_USER = "wsa_oblicqs_dev"\r
+class PreemptiveBasicAuthHandler(urllib2.BaseHandler):\r
 \r
-def mkurlopener( proxyhost, proxyport, proxyuser, proxypass ):\r
-    if not proxyhost:\r
-        return urllib2.urlopen\r
-    else:\r
-        if proxyport is None:\r
-            proxyport = DEFAULT_PROXYPORT\r
+        def __init__(self, password_mgr=None):\r
+                if password_mgr is None:\r
+                        password_mgr = urllib2.HTTPPasswordMgrWithDefaultRealm()\r
+                self.passwd = password_mgr\r
+                self.add_password = self.passwd.add_password\r
 \r
-        if proxypass is None:\r
-            proxypass = DEFAULT_PROXY_PASS\r
+        def http_request(self,req):\r
+                uri = req.get_full_url()\r
+                user, pw = self.passwd.find_user_password(None,uri)\r
+                log.debug('ADDING REQUEST HEADER for uri (%s): %s:%s' % (uri,user,pw))\r
+                if pw is None: return req\r
+                raw = "%s:%s" % (user, pw)\r
+                auth = 'Basic %s' % base64.b64encode(raw).strip()\r
+                req.add_unredirected_header('Authorization', auth)\r
+                return req\r
 \r
-        if proxyuser is None:\r
-            proxyuser = DEFAULT_PROXY_USER\r
+def mkurlopener( jenkinsuser, jenkinspass, jenkinsurl, proxyhost, proxyport, proxyuser, proxypass ):\r
+    handlers = []\r
+    for handler in get_jenkins_auth_handler(jenkinsuser=jenkinsuser, jenkinspass=jenkinspass, jenkinsurl=jenkinsurl):\r
+        handlers.append(handler)\r
+    for handler in get_proxy_handler(proxyhost, proxyport, proxyuser, proxypass):\r
+        handlers.append(handler)\r
+    opener = urllib2.build_opener(*handlers)\r
+    return opener.open\r
 \r
-        assert type( proxyport ) == int, "Proxy port should be an int, got %s" % repr( proxyport )\r
-        assert type( proxypass ) == str, "Proxy password should be a sting, got %s" % repr( proxypass )\r
-        assert type( proxyuser ) == str, "Proxy username should be a string, got %s" % repr( proxyuser )\r
+def get_jenkins_auth_handler(jenkinsuser, jenkinspass, jenkinsurl):\r
+    """\r
+    Get a basic authentification handler for jenkins\r
+    :param jenkinsuser: jenkins username, str\r
+    :param jenkinspass: jenkins password, str\r
+    :param jenkinsurl: jenkins base url, str\r
+    :return: a list of handlers\r
+    """\r
+    for param in jenkinsuser, jenkinspass, jenkinsurl:\r
+        if param is None:\r
+            return []\r
+    assert type(jenkinsuser) == str, "Jenkins username should be a string, got %s" % repr(jenkinsuser)\r
+    assert type(jenkinspass) == str, "Jenkins password should be a string, git %s" % repr(jenkinspass)\r
+#    hostname = urlparse.urlsplit(jenkinsurl).hostname\r
+    handler = PreemptiveBasicAuthHandler()\r
+    handler.add_password(None, jenkinsurl, jenkinsuser, jenkinspass)\r
+    log.debug('Adding BasicAuthHandler: url:%s, user:%s,' % (jenkinsurl, jenkinsuser))\r
+    return [ handler ]\r
 \r
-        proxy_spec = { 'http': 'http://%s:%i/' % (proxyhost, proxyport),\r
-                       'https': 'http://%s:%i/' % (proxyhost, proxyport) }\r
+def get_proxy_handler(proxyhost, proxyport, proxyuser, proxypass):\r
+    """\r
+    Get a configured handler for a proxy\r
 \r
-        proxy_handler = urllib2.ProxyHandler( proxy_spec )\r
-        proxy_auth_handler = urllib2.HTTPBasicAuthHandler()\r
-        proxy_auth_handler.add_password( None, proxyhost, proxyuser, proxypass )\r
+    :param proxyhost: proxy hostname, str\r
+    :param proxyport: proxy port, int\r
+    :param proxyuser: proxy username, str\r
+    :param proxypass: proxy password, str\r
+    :return: list of handlers\r
+    """\r
+    for param in proxyhost, proxyport, proxyuser, proxypass:\r
+        if param is None:\r
+            return []\r
+    assert type( proxyport ) == int, "Proxy port should be an int, got %s" % repr( proxyport )\r
+    assert type( proxypass ) == str, "Proxy password should be a sting, got %s" % repr( proxypass )\r
+    assert type( proxyuser ) == str, "Proxy username should be a string, got %s" % repr( proxyuser )\r
 \r
-        opener = urllib2.build_opener(proxy_handler, proxy_auth_handler)\r
+    proxy_spec = { 'http': 'http://%s:%i/' % (proxyhost, proxyport),\r
+                   'https': 'http://%s:%i/' % (proxyhost, proxyport) }\r
+\r
+    proxy_handler = urllib2.ProxyHandler( proxy_spec )\r
+    proxy_auth_handler = urllib2.HTTPBasicAuthHandler()\r
+    proxy_auth_handler.add_password( None, proxyhost, proxyuser, proxypass )\r
+    return [proxy_handler, proxy_auth_handler]\r
 \r
-        return opener.open\r