Added ability to query slaves for executors.
authorDavid Johansen <david@makewhat.is>
Tue, 29 Oct 2013 02:29:17 +0000 (19:29 -0700)
committerDavid Johansen <djohansen@twitter.com>
Tue, 29 Oct 2013 02:29:43 +0000 (19:29 -0700)
  - can pull list of executor objects for
    each slave
  - added unit tests for new code.

jenkinsapi/__init__.py
jenkinsapi/executor.py [new file with mode: 0644]
jenkinsapi/executors.py [new file with mode: 0644]
jenkinsapi/jenkins.py
jenkinsapi_tests/unittests/test_executors.py [new file with mode: 0644]

index f6810dae602e6f73eb4ec81f070231957a45168c..9829b5aed901eb15f9b417629088111e20e11cd7 100644 (file)
@@ -52,13 +52,13 @@ from jenkinsapi import (
     utils,
 
     # Files
-    api, artifact, build, config, constants, custom_exceptions, fingerprint,
+    api, artifact, build, config, constants, custom_exceptions, fingerprint, executors, executor,
     jenkins, jenkinsbase, job, node, result_set, result, view
 )
 
 __all__ = [
     "command_line", "utils",
-    "api", "artifact", "build", "config", "constants", "custom_exceptions", "fingerprint",
-    "jenkins", "jenkinsbase", "job", "node", "result_set", "result", "view"
+    "api", "artifact", "build", "config", "constants", "custom_exceptions", "executors", "executor",
+    "fingerprint", "jenkins", "jenkinsbase", "job", "node", "result_set", "result", "view"
 ]
 __docformat__ = "epytext"
diff --git a/jenkinsapi/executor.py b/jenkinsapi/executor.py
new file mode 100644 (file)
index 0000000..531713c
--- /dev/null
@@ -0,0 +1,68 @@
+"""
+Module for jenkinsapi Executer class
+"""
+
+from jenkinsapi.jenkinsbase import JenkinsBase
+import logging
+
+log = logging.getLogger(__name__)
+
+
+class Executor(JenkinsBase):
+    """
+    Class to hold information on nodes that are attached as slaves to the
+    master jenkins instance
+    """
+
+    def __init__(self, baseurl, nodename, jenkins_obj, number):
+        """
+        Init a node object by providing all relevant pointers to it
+        :param baseurl: basic url for querying information on a node
+        :param nodename: hostname of the node
+        :param jenkins_obj: ref to the jenkins obj
+        :return: Node obj
+        """
+        self.nodename = nodename
+        self.number = number
+        self.jenkins = jenkins_obj
+        self.baseurl = baseurl
+        JenkinsBase.__init__(self, baseurl)
+
+    def __str__(self):
+        return '%s %s' % (self.nodename, self.number)
+
+    def get_jenkins_obj(self):
+        return self.jenkins
+
+    def get_progress(self):
+        """Returns percentage"""
+        self.poll()
+        return self._data['progress']
+
+    def get_number(self):
+        """
+        Get Executor number.
+        """
+        self.poll()
+        return self._data['']
+
+    def is_idle(self):
+        """
+        Returns Boolean: whether Executor is idle or not.
+        """
+        self.poll()
+        return self._data['idle']
+
+    def likely_stuck(self):
+        """
+        Returns Boolean: whether Executor is likely stuck or not.
+        """
+        self.poll()
+        return self._data['likelyStuck']
+
+    def get_current_executable(self):
+        """
+        Returns the current Queue.Task this executor is running.
+        """
+        self.poll()
+        return self._data['currentExecutable']
diff --git a/jenkinsapi/executors.py b/jenkinsapi/executors.py
new file mode 100644 (file)
index 0000000..5199214
--- /dev/null
@@ -0,0 +1,44 @@
+"""
+This module implements the Executors class, which is intended to be a
+container-like interface for all of the executors defined on a single
+Jenkins node.
+"""
+import logging
+from jenkinsapi.executor import Executor
+from jenkinsapi.custom_exceptions import JenkinsAPIException
+from jenkinsapi.jenkinsbase import JenkinsBase
+
+log = logging.getLogger(__name__)
+
+
+class Executors(JenkinsBase):
+    """
+    This class provides a container-like API which gives
+    access to all executors on a Jenkins node.
+
+    Returns a list of Executor Objects.
+    """
+    def __init__(self, baseurl, nodename, jenkins):
+        self.nodename = nodename
+        self.jenkins = jenkins
+        JenkinsBase.__init__(self, baseurl)
+        self.count = self._data['numExecutors']
+
+    def __str__(self):
+        return 'Executors @ %s' % self.baseurl
+
+    def get_jenkins_obj(self):
+        return self.jenkins
+
+    def __iter__(self):
+        for index in range(self.count):
+            executor_url = '%s/executors/%s' % (self.baseurl, index)
+            try:
+                yield Executor(
+                    executor_url,
+                    self.nodename,
+                    self.jenkins,
+                    index
+                )
+            except JenkinsAPIException as exe:
+                log.error("Error querying Executors: %s", (exe,))
index 13c4e20b4dfc81e04dad396981e3bc4fe0a83b9b..0a7216dcc9065dd3dcb619a442082cde02fc0a7a 100644 (file)
@@ -9,6 +9,7 @@ import logging
 import urlparse
 
 from jenkinsapi import config
+from jenkinsapi.executors import Executors
 from jenkinsapi.job import Job
 from jenkinsapi.jobs import Jobs
 from jenkinsapi.node import Node
@@ -307,3 +308,7 @@ class Jenkins(JenkinsBase):
 
     def has_plugin(self, plugin_name):
         return plugin_name in self.get_plugins()
+
+    def get_executors(self, nodename):
+        url = '%s/computer/%s' % (self.baseurl, nodename)
+        return Executors(url, nodename, self)
diff --git a/jenkinsapi_tests/unittests/test_executors.py b/jenkinsapi_tests/unittests/test_executors.py
new file mode 100644 (file)
index 0000000..453721f
--- /dev/null
@@ -0,0 +1,323 @@
+import mock
+import types
+import unittest
+
+from jenkinsapi.jenkins import Jenkins
+from jenkinsapi.executors import Executors
+from jenkinsapi.executor import Executor
+
+
+class TestExecutors(unittest.TestCase):
+
+    DATAM = {
+        'assignedLabels': [{}],
+        'description': None,
+        'jobs': [],
+        'mode': 'NORMAL',
+        'nodeDescription': 'the master Jenkins node',
+        'nodeName': '',
+        'numExecutors': 2,
+        'overallLoad': {},
+        'primaryView': {'name': 'All', 'url': 'http://localhost:8080/'},
+        'quietingDown': False,
+        'slaveAgentPort': 0,
+        'unlabeledLoad': {},
+        'useCrumbs': False,
+        'useSecurity': False,
+        'views': [
+            {'name': 'All', 'url': 'http://localhost:8080/'},
+            {'name': 'BigMoney', 'url': 'http://localhost:8080/view/BigMoney/'}
+        ]
+    }
+
+    DATA0 = {
+        "actions": [
+        ],
+        "displayName": "host0.host.com",
+        "executors": [
+            {},
+            {},
+            {},
+            {},
+            {},
+            {},
+            {},
+            {}
+        ],
+        "icon": "computer.png",
+        "idle": False,
+        "jnlpAgent": True,
+        "launchSupported": False,
+        "loadStatistics": {
+
+        },
+        "manualLaunchAllowed": True,
+        "monitorData": {
+            "hudson.node_monitors.SwapSpaceMonitor": {
+                "availablePhysicalMemory": 8462417920,
+                "availableSwapSpace": 0,
+                "totalPhysicalMemory": 75858042880,
+                "totalSwapSpace": 0
+            },
+            "hudson.node_monitors.ArchitectureMonitor": "Linux (amd64)",
+            "hudson.node_monitors.ResponseTimeMonitor": {
+                "average": 2
+            },
+            "hudson.node_monitors.TemporarySpaceMonitor": {
+                "path": "/tmp",
+                "size": 430744551424
+            },
+            "hudson.node_monitors.DiskSpaceMonitor": {
+                "path": "/data/jenkins",
+                "size": 1214028627968
+            },
+            "hudson.node_monitors.ClockMonitor": {
+                "diff": 1
+            }
+        },
+        "numExecutors": 8,
+        "offline": False,
+        "offlineCause": None,
+        "offlineCauseReason": "",
+        "oneOffExecutors": [
+            {},
+            {}
+        ],
+        "temporarilyOffline": False
+    }
+
+    DATA1 = {
+        "actions": [
+        ],
+        "displayName": "host1.host.com",
+        "executors": [
+            {},
+            {}
+        ],
+        "icon": "computer.png",
+        "idle": False,
+        "jnlpAgent": True,
+        "launchSupported": False,
+        "loadStatistics": {
+        },
+        "manualLaunchAllowed": True,
+        "monitorData": {
+            "hudson.node_monitors.SwapSpaceMonitor": {
+                "availablePhysicalMemory": 8462417920,
+                "availableSwapSpace": 0,
+                "totalPhysicalMemory": 75858042880,
+                "totalSwapSpace": 0
+            },
+            "hudson.node_monitors.ArchitectureMonitor": "Linux (amd64)",
+            "hudson.node_monitors.ResponseTimeMonitor": {
+                "average": 2
+            },
+            "hudson.node_monitors.TemporarySpaceMonitor": {
+                "path": "/tmp",
+                "size": 430744551424
+            },
+            "hudson.node_monitors.DiskSpaceMonitor": {
+                "path": "/data/jenkins",
+                "size": 1214028627968
+            },
+            "hudson.node_monitors.ClockMonitor": {
+                "diff": 1
+            }
+        },
+        "numExecutors": 2,
+        "offline": False,
+        "offlineCause": None,
+        "offlineCauseReason": "",
+        "oneOffExecutors": [
+            {},
+            {}
+        ],
+        "temporarilyOffline": False
+    }
+
+    DATA2 = {
+        "actions": [
+        ],
+        "displayName": "host2.host.com",
+        "executors": [
+            {},
+            {},
+            {},
+            {}
+        ],
+        "icon": "computer.png",
+        "idle": False,
+        "jnlpAgent": True,
+        "launchSupported": False,
+        "loadStatistics": {
+        },
+        "manualLaunchAllowed": True,
+        "monitorData": {
+            "hudson.node_monitors.SwapSpaceMonitor": {
+                "availablePhysicalMemory": 8462417920,
+                "availableSwapSpace": 0,
+                "totalPhysicalMemory": 75858042880,
+                "totalSwapSpace": 0
+            },
+            "hudson.node_monitors.ArchitectureMonitor": "Linux (amd64)",
+            "hudson.node_monitors.ResponseTimeMonitor": {
+                "average": 2
+            },
+            "hudson.node_monitors.TemporarySpaceMonitor": {
+                "path": "/tmp",
+                "size": 430744551424
+            },
+            "hudson.node_monitors.DiskSpaceMonitor": {
+                "path": "/data/jenkins",
+                "size": 1214028627968
+            },
+            "hudson.node_monitors.ClockMonitor": {
+                "diff": 1
+            }
+        },
+        "numExecutors": 4,
+        "offline": False,
+        "offlineCause": None,
+        "offlineCauseReason": "",
+        "oneOffExecutors": [
+            {},
+            {}
+        ],
+        "temporarilyOffline": False
+    }
+
+    DATA3 = {
+        "actions": [
+        ],
+        "displayName": "host3.host.com",
+        "executors": [
+            {}
+        ],
+        "icon": "computer.png",
+        "idle": False,
+        "jnlpAgent": True,
+        "launchSupported": False,
+        "loadStatistics": {},
+        "manualLaunchAllowed": True,
+        "monitorData": {
+            "hudson.node_monitors.SwapSpaceMonitor": {
+                "availablePhysicalMemory": 8462417920,
+                "availableSwapSpace": 0,
+                "totalPhysicalMemory": 75858042880,
+                "totalSwapSpace": 0
+            },
+            "hudson.node_monitors.ArchitectureMonitor": "Linux (amd64)",
+            "hudson.node_monitors.ResponseTimeMonitor": {
+                "average": 2
+            },
+            "hudson.node_monitors.TemporarySpaceMonitor": {
+                "path": "/tmp",
+                "size": 430744551424
+            },
+            "hudson.node_monitors.DiskSpaceMonitor": {
+                "path": "/data/jenkins",
+                "size": 1214028627968
+            },
+            "hudson.node_monitors.ClockMonitor": {
+                "diff": 1
+            }
+        },
+        "numExecutors": 1,
+        "offline": False,
+        "offlineCause": None,
+        "offlineCauseReason": "",
+        "oneOffExecutors": [
+            {},
+            {}
+        ],
+        "temporarilyOffline": False
+    }
+
+    EXEC0 = {
+        "currentExecutable": {
+            "number": 4168,
+            "url": "http://localhost:8080/job/testjob/4168/"
+        },
+        "currentWorkUnit": {},
+        "idle": False,
+        "likelyStuck": False,
+        "number": 0,
+        "progress": 48
+    }
+
+    EXEC1 = {
+        "currentExecutable": None,
+        "currentWorkUnit": None,
+        "idle": True,
+        "likelyStuck": False,
+        "number": 0,
+        "progress": -1
+    }
+
+    @mock.patch.object(Jenkins, '_poll')
+    def setUp(self, _poll_jenkins):
+        _poll_jenkins.return_value = self.DATAM
+        self.J = Jenkins('http://localhost:8080')
+
+    def testRepr(self):
+        # Can we produce a repr string for this object
+        assert repr(self.J)
+
+    def testCheckURL(self):
+        self.assertEquals(self.J.baseurl, 'http://localhost:8080')
+
+    @mock.patch.object(Executors, '_poll')
+    @mock.patch.object(Executor, '_poll')
+    def testGetExecutors(self, _poll_executor, _poll_executors):
+        _poll_executor.return_value = self.EXEC0
+        _poll_executors.return_value = self.DATA3
+        exec_info = self.J.get_executors(self.DATA3['displayName'])
+
+        self.assertIsInstance(exec_info, object)
+        self.assertIsInstance(repr(exec_info), str)
+
+        for e in exec_info:
+            self.assertEquals(e.get_progress(), 48, 'Should return 48 %')
+
+    @mock.patch.object(Executors, '_poll')
+    @mock.patch.object(Executor, '_poll')
+    def testis_idle(self, _poll_executor, _poll_executors):
+        _poll_executor.return_value = self.EXEC1
+        _poll_executors.return_value = self.DATA3
+        exec_info = self.J.get_executors('host3.host.com')
+
+        self.assertIsInstance(exec_info, object)
+        for e in exec_info:
+            self.assertEquals(e.get_progress(), -1, 'Should return 48 %')
+            self.assertEquals(e.is_idle(), True, 'Should return True')
+            self.assertEquals(
+                repr(e),
+                '<jenkinsapi.executor.Executor host3.host.com 0>'
+            )
+
+    @mock.patch.object(Executor, '_poll')
+    def test_likely_stuck(self, _poll_executor):
+        _poll_executor.return_value = self.EXEC0
+        baseurl = 'http://localhost:8080/computer/host0.host.com/executors/0'
+        nodename = 'host0.host.com'
+        single_executer = Executor(baseurl, nodename, self.J, '0')
+        self.assertEquals(single_executer.likely_stuck(), False)
+
+    @mock.patch.object(Executor, '_poll')
+    def test_get_current_executable(self, _poll_executor):
+        _poll_executor.return_value = self.EXEC0
+        baseurl = 'http://localhost:8080/computer/host0.host.com/executors/0'
+        nodename = 'host0.host.com'
+        single_executer = Executor(baseurl, nodename, self.J, '0')
+        self.assertEquals(
+            single_executer.get_current_executable()['number'],
+            4168
+        )
+        self.assertEquals(
+            single_executer.get_current_executable()['url'],
+            'http://localhost:8080/job/testjob/4168/'
+        )
+
+
+if __name__ == '__main__':
+    unittest.main()