jenkinsapi-0.2.29/0000775000175000017500000000000012616521664013332 5ustar salsal00000000000000jenkinsapi-0.2.29/jenkinsapi_tests/0000775000175000017500000000000012616521664016707 5ustar salsal00000000000000jenkinsapi-0.2.29/jenkinsapi_tests/__init__.py0000664000175000017500000000000012616220252020773 0ustar salsal00000000000000jenkinsapi-0.2.29/PKG-INFO0000664000175000017500000002000412616521664014423 0ustar salsal00000000000000Metadata-Version: 1.1 Name: jenkinsapi Version: 0.2.29 Summary: A Python API for accessing resources on a Jenkins continuous-integration server. Home-page: https://github.com/salimfadhley/jenkinsapi Author: Salim Fadhley, Aleksey Maksimov Author-email: salimfadhley@gmail.com, ctpeko3a@gmail.com License: MIT Description: jenkinsapi ========== .. image:: https://badge.fury.io/py/jenkinsapi.png :target: http://badge.fury.io/py/jenkinsapi .. image:: https://travis-ci.org/salimfadhley/jenkinsapi.png?branch=master :target: https://travis-ci.org/salimfadhley/jenkinsapi .. image:: https://landscape.io/github/salimfadhley/jenkinsapi/master/landscape.png :target: https://landscape.io/github/salimfadhley/jenkinsapi .. image:: https://requires.io/github/salimfadhley/jenkinsapi/requirements.png?branch=master :target: https://requires.io/github/salimfadhley/jenkinsapi/requirements/?branch=master :alt: Requirements Status About this library ------------------- 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 many Jenkins oriented tasks easier to automate. This library allows you to automate most common Jenkins operations using Python. * Ability to add/remove/query Jenkins jobs * Ability to execute jobs and: * Query the results of a completed build * Block until jobs are complete or run jobs asyncronously * Get objects representing the latest builds of a job * Work with build artefacts: * Search for artefacts by simple criteria * Install artefacts to custom-specified directory structures * Ability to search for builds by source code revision * Ability to add/remove/query: * Slaves (Webstart and SSH slaves) * Views (including nested views using NestedViews Jenkins plugin) * Credentials (username/password and ssh key) * Username/password auth support for jenkins instances with auth turned on * Ability to script jenkins installation including plugins Python versions --------------- The project have been tested and working on Python 2.7, 3.3 and 3.4. It was tested previously on Python 2.6, so it may work on this version too. Known issues ------------ * Job deletion operations fail unless Cross-Site scripting protection is disabled. For other issues, please refer to the support URL below. Important Links --------------- Support and bug-reports: https://github.com/salimfadhley/jenkinsapi/issues?direction=desc&sort=comments&state=open Project source code: github: https://github.com/salimfadhley/jenkinsapi Project documentation: https://jenkinsapi.readthedocs.org/en/latest/ Releases: http://pypi.python.org/pypi/jenkinsapi Installation ------------- Egg-files for this project are hosted on PyPi. Most Python users should be able to use pip or setuptools to automatically install this project. Using Pip or Setuptools ^^^^^^^^^^^^^^^^^^^^^^^ Most users can do the following: .. code-block:: bash pip install jenkinsapi Or: .. code-block:: bash easy_install jenkinsapi Both of these techniques can be combined with virtualenv to create an application-specific installation. Using your operating-system's package manager ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ Ubuntu users can now use apt to install this package: .. code-block:: bash apt-get install python-jenkinsapi Beware that this technique will get a somewhat older version of Jenkinsapi. Example ------- JenkinsAPI is intended to map the objects in Jenkins (e.g. Builds, Views, Jobs) into easily managed Python objects: .. code-block:: python >>> import jenkinsapi >>> from jenkinsapi.jenkins import Jenkins >>> J = Jenkins('http://localhost:8080') >>> J.version 1.542 >>> J.keys() # Jenkins objects appear to be dict-like, mapping keys (job-names) to ['foo', 'test_jenkinsapi'] >>> J['test_jenkinsapi'] >>> J['test_jenkinsapi'].get_last_good_build() ... More examples available on Github: https://github.com/salimfadhley/jenkinsapi/tree/master/examples Testing ------- If you have installed the test dependencies on your system already, you can run the testsuite with the following command: .. code-block:: bash python setup.py test Otherwise using a virtualenv is recommended. Setuptools will automatically fetch missing test dependencies: .. code-block:: bash virtualenv source .venv/bin/active (venv) python setup.py test Project Contributors -------------------- * Salim Fadhley (sal@stodge.org) * Aleksey Maksimov (ctpeko3a@gmail.com) * Ramon van Alteren (ramon@vanalteren.nl) * Ruslan Lutsenko (ruslan.lutcenko@gmail.com) * Cleber J Santos (cleber@simplesconsultoria.com.br) * William Zhang (jollychang@douban.com) * Victor Garcia (bravejolie@gmail.com) * Bradley Harris (bradley@ninelb.com) * Kyle Rockman (kyle.rockman@mac.com) * Sascha Peilicke (saschpe@gmx.de) * David Johansen (david@makewhat.is) Please do not contact these contributors directly for support questions! Use the GitHub tracker instead. License -------- The MIT License (MIT): Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. Platform: UNKNOWN Classifier: Development Status :: 4 - Beta Classifier: Environment :: Console Classifier: Intended Audience :: Developers Classifier: License :: OSI Approved :: MIT License Classifier: Natural Language :: English Classifier: Operating System :: OS Independent Classifier: Programming Language :: Python :: 2.7 Classifier: Topic :: Software Development :: Testing jenkinsapi-0.2.29/setup.py0000664000175000017500000000337212616521627015050 0ustar salsal00000000000000from setuptools import setup import os PROJECT_ROOT, _ = os.path.split(__file__) REVISION = '0.2.29' PROJECT_NAME = 'JenkinsAPI' PROJECT_AUTHORS = "Salim Fadhley, Aleksey Maksimov" # Please see readme.rst for a complete list of contributors PROJECT_EMAILS = 'salimfadhley@gmail.com, ctpeko3a@gmail.com' PROJECT_URL = "https://github.com/salimfadhley/jenkinsapi" SHORT_DESCRIPTION = 'A Python API for accessing resources on a Jenkins continuous-integration server.' try: DESCRIPTION = open(os.path.join(PROJECT_ROOT, "README.rst")).read() except IOError: DESCRIPTION = SHORT_DESCRIPTION GLOBAL_ENTRY_POINTS = { "console_scripts": ["jenkins_invoke=jenkinsapi.command_line.jenkins_invoke:main", "jenkinsapi_version=jenkinsapi.command_line.jenkinsapi_version:main"] } setup( name=PROJECT_NAME.lower(), version=REVISION, author=PROJECT_AUTHORS, author_email=PROJECT_EMAILS, packages=[ 'jenkinsapi', 'jenkinsapi.utils', 'jenkinsapi.command_line', 'jenkinsapi_tests'], zip_safe=True, include_package_data=False, install_requires=['requests>=2.3.0', 'pytz>=2014.4'], test_suite='nose.collector', tests_require=['mock', 'nose', 'coverage', 'unittest2'], entry_points=GLOBAL_ENTRY_POINTS, url=PROJECT_URL, description=SHORT_DESCRIPTION, long_description=DESCRIPTION, license='MIT', classifiers=[ 'Development Status :: 4 - Beta', 'Environment :: Console', 'Intended Audience :: Developers', 'License :: OSI Approved :: MIT License', 'Natural Language :: English', 'Operating System :: OS Independent', 'Programming Language :: Python :: 2.7', 'Topic :: Software Development :: Testing', ], ) jenkinsapi-0.2.29/README.rst0000664000175000017500000001405412616220252015012 0ustar salsal00000000000000jenkinsapi ========== .. image:: https://badge.fury.io/py/jenkinsapi.png :target: http://badge.fury.io/py/jenkinsapi .. image:: https://travis-ci.org/salimfadhley/jenkinsapi.png?branch=master :target: https://travis-ci.org/salimfadhley/jenkinsapi .. image:: https://landscape.io/github/salimfadhley/jenkinsapi/master/landscape.png :target: https://landscape.io/github/salimfadhley/jenkinsapi .. image:: https://requires.io/github/salimfadhley/jenkinsapi/requirements.png?branch=master :target: https://requires.io/github/salimfadhley/jenkinsapi/requirements/?branch=master :alt: Requirements Status About this library ------------------- 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 many Jenkins oriented tasks easier to automate. This library allows you to automate most common Jenkins operations using Python. * Ability to add/remove/query Jenkins jobs * Ability to execute jobs and: * Query the results of a completed build * Block until jobs are complete or run jobs asyncronously * Get objects representing the latest builds of a job * Work with build artefacts: * Search for artefacts by simple criteria * Install artefacts to custom-specified directory structures * Ability to search for builds by source code revision * Ability to add/remove/query: * Slaves (Webstart and SSH slaves) * Views (including nested views using NestedViews Jenkins plugin) * Credentials (username/password and ssh key) * Username/password auth support for jenkins instances with auth turned on * Ability to script jenkins installation including plugins Python versions --------------- The project have been tested and working on Python 2.7, 3.3 and 3.4. It was tested previously on Python 2.6, so it may work on this version too. Known issues ------------ * Job deletion operations fail unless Cross-Site scripting protection is disabled. For other issues, please refer to the support URL below. Important Links --------------- Support and bug-reports: https://github.com/salimfadhley/jenkinsapi/issues?direction=desc&sort=comments&state=open Project source code: github: https://github.com/salimfadhley/jenkinsapi Project documentation: https://jenkinsapi.readthedocs.org/en/latest/ Releases: http://pypi.python.org/pypi/jenkinsapi Installation ------------- Egg-files for this project are hosted on PyPi. Most Python users should be able to use pip or setuptools to automatically install this project. Using Pip or Setuptools ^^^^^^^^^^^^^^^^^^^^^^^ Most users can do the following: .. code-block:: bash pip install jenkinsapi Or: .. code-block:: bash easy_install jenkinsapi Both of these techniques can be combined with virtualenv to create an application-specific installation. Using your operating-system's package manager ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ Ubuntu users can now use apt to install this package: .. code-block:: bash apt-get install python-jenkinsapi Beware that this technique will get a somewhat older version of Jenkinsapi. Example ------- JenkinsAPI is intended to map the objects in Jenkins (e.g. Builds, Views, Jobs) into easily managed Python objects: .. code-block:: python >>> import jenkinsapi >>> from jenkinsapi.jenkins import Jenkins >>> J = Jenkins('http://localhost:8080') >>> J.version 1.542 >>> J.keys() # Jenkins objects appear to be dict-like, mapping keys (job-names) to ['foo', 'test_jenkinsapi'] >>> J['test_jenkinsapi'] >>> J['test_jenkinsapi'].get_last_good_build() ... More examples available on Github: https://github.com/salimfadhley/jenkinsapi/tree/master/examples Testing ------- If you have installed the test dependencies on your system already, you can run the testsuite with the following command: .. code-block:: bash python setup.py test Otherwise using a virtualenv is recommended. Setuptools will automatically fetch missing test dependencies: .. code-block:: bash virtualenv source .venv/bin/active (venv) python setup.py test Project Contributors -------------------- * Salim Fadhley (sal@stodge.org) * Aleksey Maksimov (ctpeko3a@gmail.com) * Ramon van Alteren (ramon@vanalteren.nl) * Ruslan Lutsenko (ruslan.lutcenko@gmail.com) * Cleber J Santos (cleber@simplesconsultoria.com.br) * William Zhang (jollychang@douban.com) * Victor Garcia (bravejolie@gmail.com) * Bradley Harris (bradley@ninelb.com) * Kyle Rockman (kyle.rockman@mac.com) * Sascha Peilicke (saschpe@gmx.de) * David Johansen (david@makewhat.is) Please do not contact these contributors directly for support questions! Use the GitHub tracker instead. License -------- The MIT License (MIT): Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. jenkinsapi-0.2.29/jenkinsapi.egg-info/0000775000175000017500000000000012616521664017157 5ustar salsal00000000000000jenkinsapi-0.2.29/jenkinsapi.egg-info/top_level.txt0000664000175000017500000000003412616521664021706 0ustar salsal00000000000000jenkinsapi jenkinsapi_tests jenkinsapi-0.2.29/jenkinsapi.egg-info/requires.txt0000664000175000017500000000003512616521664021555 0ustar salsal00000000000000requests>=2.3.0 pytz>=2014.4 jenkinsapi-0.2.29/jenkinsapi.egg-info/SOURCES.txt0000664000175000017500000000211712616521664021044 0ustar salsal00000000000000README.rst setup.cfg setup.py jenkinsapi/__init__.py jenkinsapi/api.py jenkinsapi/artifact.py jenkinsapi/build.py jenkinsapi/config.py jenkinsapi/constants.py jenkinsapi/credential.py jenkinsapi/credentials.py jenkinsapi/custom_exceptions.py jenkinsapi/executor.py jenkinsapi/executors.py jenkinsapi/fingerprint.py jenkinsapi/jenkins.py jenkinsapi/jenkinsbase.py jenkinsapi/job.py jenkinsapi/jobs.py jenkinsapi/mutable_jenkins_thing.py jenkinsapi/node.py jenkinsapi/nodes.py jenkinsapi/plugin.py jenkinsapi/plugins.py jenkinsapi/queue.py jenkinsapi/result.py jenkinsapi/result_set.py jenkinsapi/view.py jenkinsapi/views.py jenkinsapi.egg-info/PKG-INFO jenkinsapi.egg-info/SOURCES.txt jenkinsapi.egg-info/dependency_links.txt jenkinsapi.egg-info/entry_points.txt jenkinsapi.egg-info/requires.txt jenkinsapi.egg-info/top_level.txt jenkinsapi.egg-info/zip-safe jenkinsapi/command_line/__init__.py jenkinsapi/command_line/jenkins_invoke.py jenkinsapi/command_line/jenkinsapi_version.py jenkinsapi/utils/__init__.py jenkinsapi/utils/krb_requester.py jenkinsapi/utils/requester.py jenkinsapi_tests/__init__.pyjenkinsapi-0.2.29/jenkinsapi.egg-info/zip-safe0000664000175000017500000000000112616220334020575 0ustar salsal00000000000000 jenkinsapi-0.2.29/jenkinsapi.egg-info/entry_points.txt0000664000175000017500000000022512616521664022454 0ustar salsal00000000000000[console_scripts] jenkins_invoke = jenkinsapi.command_line.jenkins_invoke:main jenkinsapi_version = jenkinsapi.command_line.jenkinsapi_version:main jenkinsapi-0.2.29/jenkinsapi.egg-info/PKG-INFO0000664000175000017500000002000412616521664020250 0ustar salsal00000000000000Metadata-Version: 1.1 Name: jenkinsapi Version: 0.2.29 Summary: A Python API for accessing resources on a Jenkins continuous-integration server. Home-page: https://github.com/salimfadhley/jenkinsapi Author: Salim Fadhley, Aleksey Maksimov Author-email: salimfadhley@gmail.com, ctpeko3a@gmail.com License: MIT Description: jenkinsapi ========== .. image:: https://badge.fury.io/py/jenkinsapi.png :target: http://badge.fury.io/py/jenkinsapi .. image:: https://travis-ci.org/salimfadhley/jenkinsapi.png?branch=master :target: https://travis-ci.org/salimfadhley/jenkinsapi .. image:: https://landscape.io/github/salimfadhley/jenkinsapi/master/landscape.png :target: https://landscape.io/github/salimfadhley/jenkinsapi .. image:: https://requires.io/github/salimfadhley/jenkinsapi/requirements.png?branch=master :target: https://requires.io/github/salimfadhley/jenkinsapi/requirements/?branch=master :alt: Requirements Status About this library ------------------- 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 many Jenkins oriented tasks easier to automate. This library allows you to automate most common Jenkins operations using Python. * Ability to add/remove/query Jenkins jobs * Ability to execute jobs and: * Query the results of a completed build * Block until jobs are complete or run jobs asyncronously * Get objects representing the latest builds of a job * Work with build artefacts: * Search for artefacts by simple criteria * Install artefacts to custom-specified directory structures * Ability to search for builds by source code revision * Ability to add/remove/query: * Slaves (Webstart and SSH slaves) * Views (including nested views using NestedViews Jenkins plugin) * Credentials (username/password and ssh key) * Username/password auth support for jenkins instances with auth turned on * Ability to script jenkins installation including plugins Python versions --------------- The project have been tested and working on Python 2.7, 3.3 and 3.4. It was tested previously on Python 2.6, so it may work on this version too. Known issues ------------ * Job deletion operations fail unless Cross-Site scripting protection is disabled. For other issues, please refer to the support URL below. Important Links --------------- Support and bug-reports: https://github.com/salimfadhley/jenkinsapi/issues?direction=desc&sort=comments&state=open Project source code: github: https://github.com/salimfadhley/jenkinsapi Project documentation: https://jenkinsapi.readthedocs.org/en/latest/ Releases: http://pypi.python.org/pypi/jenkinsapi Installation ------------- Egg-files for this project are hosted on PyPi. Most Python users should be able to use pip or setuptools to automatically install this project. Using Pip or Setuptools ^^^^^^^^^^^^^^^^^^^^^^^ Most users can do the following: .. code-block:: bash pip install jenkinsapi Or: .. code-block:: bash easy_install jenkinsapi Both of these techniques can be combined with virtualenv to create an application-specific installation. Using your operating-system's package manager ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ Ubuntu users can now use apt to install this package: .. code-block:: bash apt-get install python-jenkinsapi Beware that this technique will get a somewhat older version of Jenkinsapi. Example ------- JenkinsAPI is intended to map the objects in Jenkins (e.g. Builds, Views, Jobs) into easily managed Python objects: .. code-block:: python >>> import jenkinsapi >>> from jenkinsapi.jenkins import Jenkins >>> J = Jenkins('http://localhost:8080') >>> J.version 1.542 >>> J.keys() # Jenkins objects appear to be dict-like, mapping keys (job-names) to ['foo', 'test_jenkinsapi'] >>> J['test_jenkinsapi'] >>> J['test_jenkinsapi'].get_last_good_build() ... More examples available on Github: https://github.com/salimfadhley/jenkinsapi/tree/master/examples Testing ------- If you have installed the test dependencies on your system already, you can run the testsuite with the following command: .. code-block:: bash python setup.py test Otherwise using a virtualenv is recommended. Setuptools will automatically fetch missing test dependencies: .. code-block:: bash virtualenv source .venv/bin/active (venv) python setup.py test Project Contributors -------------------- * Salim Fadhley (sal@stodge.org) * Aleksey Maksimov (ctpeko3a@gmail.com) * Ramon van Alteren (ramon@vanalteren.nl) * Ruslan Lutsenko (ruslan.lutcenko@gmail.com) * Cleber J Santos (cleber@simplesconsultoria.com.br) * William Zhang (jollychang@douban.com) * Victor Garcia (bravejolie@gmail.com) * Bradley Harris (bradley@ninelb.com) * Kyle Rockman (kyle.rockman@mac.com) * Sascha Peilicke (saschpe@gmx.de) * David Johansen (david@makewhat.is) Please do not contact these contributors directly for support questions! Use the GitHub tracker instead. License -------- The MIT License (MIT): Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. Platform: UNKNOWN Classifier: Development Status :: 4 - Beta Classifier: Environment :: Console Classifier: Intended Audience :: Developers Classifier: License :: OSI Approved :: MIT License Classifier: Natural Language :: English Classifier: Operating System :: OS Independent Classifier: Programming Language :: Python :: 2.7 Classifier: Topic :: Software Development :: Testing jenkinsapi-0.2.29/jenkinsapi.egg-info/dependency_links.txt0000664000175000017500000000000112616521664023225 0ustar salsal00000000000000 jenkinsapi-0.2.29/jenkinsapi/0000775000175000017500000000000012616521664015465 5ustar salsal00000000000000jenkinsapi-0.2.29/jenkinsapi/executors.py0000664000175000017500000000214312616220252020045 0ustar salsal00000000000000""" 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.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) yield Executor( executor_url, self.nodename, self.jenkins, index ) jenkinsapi-0.2.29/jenkinsapi/view.py0000664000175000017500000001515012616220252017000 0ustar salsal00000000000000""" Module for jenkinsapi views """ try: from urllib import urlencode except ImportError: # Python3 from urllib.parse import urlencode import logging from jenkinsapi.jenkinsbase import JenkinsBase from jenkinsapi.job import Job from jenkinsapi.custom_exceptions import NotFound log = logging.getLogger(__name__) class View(JenkinsBase): """ View class """ def __init__(self, url, name, jenkins_obj): self.name = name self.jenkins_obj = jenkins_obj JenkinsBase.__init__(self, url) self.deleted = False def __str__(self): return self.name def __getitem__(self, job_name): assert isinstance(job_name, str) api_url = self.python_api_url(self.get_job_url(job_name)) return Job(api_url, job_name, self.jenkins_obj) def __contains__(self, job_name): """ True if view_name is the name of a defined view """ return job_name in self.keys() def delete(self): """ Remove this view object """ url = "%s/doDelete" % self.baseurl self.jenkins_obj.requester.post_and_confirm_status(url, data='') self.jenkins_obj.poll() self.deleted = True def keys(self): return self.get_job_dict().keys() def iteritems(self): try: it = self.get_job_dict().iteritems() except AttributeError: # Python3 it = self.get_job_dict().items() for name, url in it: api_url = self.python_api_url(url) yield name, Job(api_url, name, self.jenkins_obj) def values(self): return [a[1] for a in self.iteritems()] def items(self): return [a for a in self.iteritems()] def _get_jobs(self): if 'jobs' in self._data: for viewdict in self._data["jobs"]: yield viewdict["name"], viewdict["url"] def get_job_dict(self): return dict(self._get_jobs()) def __len__(self): return len(self.get_job_dict().keys()) def get_job_url(self, str_job_name): if str_job_name in self: return self.get_job_dict()[str_job_name] else: # noinspection PyUnboundLocalVariable views_jobs = ", ".join(self.get_job_dict().keys()) raise NotFound("Job %s is not known, available jobs" " in view are: %s" % (str_job_name, views_jobs)) def get_jenkins_obj(self): return self.jenkins_obj def add_job(self, str_job_name, job=None): """ Add job to a view :param str_job_name: name of the job to be added :param job: Job object to be added :return: True if job has been added, False if job already exists or job not known to Jenkins """ if not job: if str_job_name in self.get_job_dict(): log.warn(msg='Job %s is already in the view %s' % (str_job_name, self.name)) return False else: # Since this call can be made from nested view, # which doesn't have any jobs, we can miss existing job # Thus let's create top level Jenkins and ask him # http://jenkins:8080/view/CRT/view/CRT-FB/view/CRT-SCRT-1301/ top_jenkins = self.get_jenkins_obj().get_jenkins_obj_from_url( self.baseurl.split('view/')[0]) if not top_jenkins.has_job(str_job_name): log.error( msg='Job "%s" is not known to Jenkins' % str_job_name) return False else: job = top_jenkins.get_job(str_job_name) log.info(msg='Creating job %s in view %s' % (str_job_name, self.name)) data = { "description": "", "statusFilter": "", "useincluderegex": "on", "includeRegex": "", "columns": [{"stapler-class": "hudson.views.StatusColumn", "kind": "hudson.views.StatusColumn"}, {"stapler-class": "hudson.views.WeatherColumn", "kind": "hudson.views.WeatherColumn"}, {"stapler-class": "hudson.views.JobColumn", "kind": "hudson.views.JobColumn"}, {"stapler-class": "hudson.views.LastSuccessColumn", "kind": "hudson.views.LastSuccessColumn"}, {"stapler-class": "hudson.views.LastFailureColumn", "kind": "hudson.views.LastFailureColumn"}, {"stapler-class": "hudson.views.LastDurationColumn", "kind": "hudson.views.LastDurationColumn"}, {"stapler-class": "hudson.views.BuildButtonColumn", "kind": "hudson.views.BuildButtonColumn"}], "Submit": "OK", } data["name"] = self.name # Add existing jobs (if any) for job_name in self.get_job_dict().keys(): data[job_name] = 'on' # Add new job data[job.name] = 'on' data['json'] = data.copy() data = urlencode(data) self.get_jenkins_obj().requester.post_and_confirm_status( '%s/configSubmit' % self.baseurl, data=data) self.poll() log.debug(msg='Job "%s" has been added to a view "%s"' % (job.name, self.name)) return True def _get_nested_views(self): for viewdict in self._data.get("views", []): yield viewdict["name"], viewdict["url"] def get_nested_view_dict(self): return dict(self._get_nested_views()) def get_config_xml_url(self): return '%s/config.xml' % self.baseurl def get_config(self): """ Return the config.xml from the view """ url = self.get_config_xml_url() response = self.get_jenkins_obj().requester.get_and_confirm_status(url) return response.text def update_config(self, config): """ Update the config.xml to the view """ url = self.get_config_xml_url() try: if isinstance( config, unicode): # pylint: disable=undefined-variable config = str(config) except NameError: # Python3 already a str pass response = self.get_jenkins_obj().requester.post_url( url, params={}, data=config) return response.text @property def views(self): return self.get_jenkins_obj().get_jenkins_obj_from_url( self.baseurl).views jenkinsapi-0.2.29/jenkinsapi/nodes.py0000664000175000017500000000663612616220252017147 0ustar salsal00000000000000""" Module for jenkinsapi nodes """ import logging try: from urllib import urlencode except ImportError: # Python3 from urllib.parse import urlencode from jenkinsapi.node import Node from jenkinsapi.jenkinsbase import JenkinsBase from jenkinsapi.custom_exceptions import JenkinsAPIException from jenkinsapi.custom_exceptions import UnknownNode from jenkinsapi.custom_exceptions import PostRequired log = logging.getLogger(__name__) class Nodes(JenkinsBase): """ Class to hold information on a collection of nodes """ def __init__(self, baseurl, jenkins_obj): """ Handy access to all of the nodes on your Jenkins server """ self.jenkins = jenkins_obj JenkinsBase.__init__(self, baseurl) def get_jenkins_obj(self): return self.jenkins def __str__(self): return 'Nodes @ %s' % self.baseurl def __contains__(self, node_name): return node_name in self.keys() def iterkeys(self): for item in self._data['computer']: yield item['displayName'] def keys(self): return list(self.iterkeys()) def iteritems(self): for item in self._data['computer']: nodename = item['displayName'] if nodename.lower() == 'master': nodeurl = '%s/(%s)' % (self.baseurl, nodename) else: nodeurl = '%s/%s' % (self.baseurl, nodename) try: yield item['displayName'], Node(self.jenkins, nodeurl, nodename, node_dict={}) except Exception: raise JenkinsAPIException('Unable to iterate nodes') def __getitem__(self, nodename): self_as_dict = dict(self.iteritems()) if nodename in self_as_dict: return self_as_dict[nodename] else: raise UnknownNode(nodename) def __len__(self): return len(self.keys()) def __delitem__(self, item): if item in self and item != 'master': url = "%s/doDelete" % self[item].baseurl try: self.jenkins.requester.get_and_confirm_status(url) except PostRequired: # Latest Jenkins requires POST here. GET kept for compatibility self.jenkins.requester.post_and_confirm_status(url, data={}) self.poll() else: if item != 'master': raise KeyError('Node %s does not exist' % item) def __setitem__(self, name, node_dict): if not isinstance(node_dict, dict): raise ValueError('"node_dict" parameter must be a Node dict') if name not in self: self.create_node(name, node_dict) self.poll() def create_node(self, name, node_dict): """ Create a new slave node :param str name: name of slave :param dict node_dict: node dict (See Node class) :return: node obj """ if name in self: return node = Node(jenkins_obj=self.jenkins, baseurl=None, nodename=name, node_dict=node_dict, poll=False) url = ('%s/computer/doCreateItem?%s' % (self.jenkins.baseurl, urlencode(node.get_node_attributes()))) data = {'json': urlencode(node.get_node_attributes())} self.jenkins.requester.post_and_confirm_status(url, data=data) self.poll() return self[name] jenkinsapi-0.2.29/jenkinsapi/credentials.py0000664000175000017500000001202412616220252020320 0ustar salsal00000000000000""" This module implements the Credentials class, which is intended to be a container-like interface for all of the Global credentials defined on a single Jenkins node. """ import logging try: from urllib import urlencode except ImportError: # Python3 from urllib.parse import urlencode from jenkinsapi.credential import Credential from jenkinsapi.credential import UsernamePasswordCredential from jenkinsapi.credential import SSHKeyCredential from jenkinsapi.jenkinsbase import JenkinsBase from jenkinsapi.custom_exceptions import JenkinsAPIException log = logging.getLogger(__name__) class Credentials(JenkinsBase): """ This class provides a container-like API which gives access to all global credentials on a Jenkins node. Returns a list of Credential Objects. """ def __init__(self, baseurl, jenkins_obj): self.baseurl = baseurl self.jenkins = jenkins_obj JenkinsBase.__init__(self, baseurl) self.credentials = self._data['credentials'] def _poll(self, tree=None): url = self.python_api_url(self.baseurl) + '?depth=2' data = self.get_data(url, tree=tree) credentials = data['credentials'] for cred_id, cred_dict in credentials.items(): cred_dict['credential_id'] = cred_id credentials[cred_id] = self._make_credential(cred_dict) return data def __str__(self): return 'Global Credentials @ %s' % self.baseurl def get_jenkins_obj(self): return self.jenkins def __iter__(self): for cred in self.credentials.values(): yield cred.description def __contains__(self, description): return description in self.keys() def iterkeys(self): return self.__iter__() def keys(self): return list(self.iterkeys()) def iteritems(self): for cred in self.credentials.values(): yield cred.description, cred def __getitem__(self, description): for cred in self.credentials.values(): if cred.description == description: return cred raise KeyError('Credential with description "%s" not found' % description) def __len__(self): return len(self.keys()) def __setitem__(self, description, credential): """ Creates Credential in Jenkins using username, password and description Description must be unique in Jenkins instance because it is used to find Credential later. If description already exists - this method is going to update existing Credential :param str description: Credential description :param tuple credential_tuple: (username, password, description) tuple. """ if description not in self: params = credential.get_attributes() url = ( '%s/credential-store/domain/_/createCredentials' % self.jenkins.baseurl ) else: raise JenkinsAPIException('Updating credentials is not supported ' 'by jenkinsapi') try: self.jenkins.requester.post_and_confirm_status( url, params={}, data=urlencode(params) ) except JenkinsAPIException as jae: raise JenkinsAPIException('Latest version of Credentials ' 'plugin is required to be able ' 'to create/update credentials. ' 'Original exception: %s' % str(jae)) self.poll() self.credentials = self._data['credentials'] if description not in self: raise JenkinsAPIException('Problem creating/updating credential.') def get(self, item, default): return self[item] if item in self else default def __delitem__(self, description): params = { 'Submit': 'OK', 'json': {} } url = ('%s/credential-store/domain/_/credential/%s/doDelete' % (self.jenkins.baseurl, self[description].credential_id)) try: self.jenkins.requester.post_and_confirm_status( url, params={}, data=urlencode(params) ) except JenkinsAPIException as jae: raise JenkinsAPIException('Latest version of Credentials ' 'required to be able to create ' 'credentials. Original exception: %s' % str(jae)) self.poll() self.credentials = self._data['credentials'] if description in self: raise JenkinsAPIException('Problem deleting credential.') def _make_credential(self, cred_dict): if cred_dict['typeName'] == 'Username with password': cr = UsernamePasswordCredential(cred_dict) elif cred_dict['typeName'] == 'SSH Username with private key': cr = SSHKeyCredential(cred_dict) else: cr = Credential(cred_dict) return cr jenkinsapi-0.2.29/jenkinsapi/jobs.py0000664000175000017500000001141212616220252016760 0ustar salsal00000000000000""" This module implements the Jobs class, which is intended to be a container-like interface for all of the jobs defined on a single Jenkins server. """ import logging from jenkinsapi.job import Job from jenkinsapi.custom_exceptions import JenkinsAPIException, UnknownJob log = logging.getLogger(__name__) class Jobs(object): """ This class provides a container-like API which gives access to all jobs defined on the Jenkins server. It behaves like a dict in which keys are Job-names and values are actual jenkinsapi.Job objects. """ def __init__(self, jenkins): self.jenkins = jenkins self.jenkins.poll() def __len__(self): return len(self.keys()) def __delitem__(self, job_name): """ Delete a job by name :param job_name: name of a exist job, str """ if job_name in self: try: delete_job_url = self[job_name].get_delete_url() self.jenkins.requester.post_and_confirm_status( delete_job_url, data='some random bytes...' ) except JenkinsAPIException: # Sometimes jenkins throws NPE when removing job # It removes job ok, but it is good to be sure # so we re-try if job was not deleted self.jenkins.poll() if job_name in self: delete_job_url = self[job_name].get_delete_url() self.jenkins.requester.post_and_confirm_status( delete_job_url, data='some random bytes...' ) self.jenkins.poll() def __setitem__(self, key, value): return self.create(key, value) def __getitem__(self, job_name): for row in self.jenkins._data.get('jobs', []): if row['name'] == job_name: return Job( row['url'], row['name'], self.jenkins) raise UnknownJob(job_name) def iteritems(self): """ Get the names & objects for all jobs """ self.jenkins.poll() for row in self.jenkins._data.get('jobs', []): name = row['name'] url = row['url'] yield name, Job(url, name, self.jenkins) def __contains__(self, job_name): """ True if job_name is the name of a defined job """ return job_name in self.keys() def iterkeys(self): """ Get the names of all available views """ for row in self.jenkins._data.get('jobs', []): yield row['name'] def keys(self): """ Return a list of the names of all jobs """ return list(self.iterkeys()) def create(self, job_name, config): """ Create a job :param jobname: name of new job, str :param config: configuration of new job, xml :return: new Job obj """ if job_name in self: return self[job_name] params = {'name': job_name} try: if isinstance( config, unicode): # pylint: disable=undefined-variable config = str(config) except NameError: # Python3 already a str pass self.jenkins.requester.post_xml_and_confirm_status( self.jenkins.get_create_url(), data=config, params=params ) self.jenkins.poll() if job_name not in self: raise JenkinsAPIException('Cannot create job %s' % job_name) return self[job_name] def copy(self, job_name, new_job_name): """ Copy a job :param job_name: name of a exist job, str :param new_job_name: name of new job, str :return: new Job obj """ params = {'name': new_job_name, 'mode': 'copy', 'from': job_name} self.jenkins.requester.post_and_confirm_status( self.jenkins.get_create_url(), params=params, data='') self.jenkins.poll() return self[new_job_name] def rename(self, job_name, new_job_name): """ Rename a job :param job_name: name of a exist job, str :param new_job_name: name of new job, str :return: new Job obj """ params = {'newName': new_job_name} rename_job_url = self[job_name].get_rename_url() self.jenkins.requester.post_and_confirm_status( rename_job_url, params=params, data='') self.jenkins.poll() return self[new_job_name] def build(self, job_name, params): assert isinstance(params, dict) self[job_name].invoke(build_params=params) jenkinsapi-0.2.29/jenkinsapi/job.py0000664000175000017500000005523112616220252016604 0ustar salsal00000000000000""" Module for jenkinsapi Job """ from collections import defaultdict from jenkinsapi.build import Build from jenkinsapi.custom_exceptions import ( NoBuildData, NotConfiguredSCM, NotFound, NotInQueue, NotSupportSCM, UnknownQueueItem, BadParams, ) from jenkinsapi.jenkinsbase import JenkinsBase from jenkinsapi.mutable_jenkins_thing import MutableJenkinsThing from jenkinsapi.queue import QueueItem import json import logging import xml.etree.ElementTree as ET try: import urlparse except ImportError: # Python3 import urllib.parse as urlparse SVN_URL = './scm/locations/hudson.scm.SubversionSCM_-ModuleLocation/remote' GIT_URL = './scm/userRemoteConfigs/hudson.plugins.git.UserRemoteConfig/url' HG_URL = './scm/source' GIT_BRANCH = './scm/branches/hudson.plugins.git.BranchSpec/name' HG_BRANCH = './scm/branch' DEFAULT_HG_BRANCH_NAME = 'default' log = logging.getLogger(__name__) class Job(JenkinsBase, MutableJenkinsThing): """ Represents a jenkins job A job can hold N builds which are the actual execution environments """ def __init__(self, url, name, jenkins_obj): self.name = name self.jenkins = jenkins_obj self._revmap = None self._config = None self._element_tree = None self._scm_map = { 'hudson.scm.SubversionSCM': 'svn', 'hudson.plugins.git.GitSCM': 'git', 'hudson.plugins.mercurial.MercurialSCM': 'hg', 'hudson.scm.NullSCM': 'NullSCM' } self._scmurlmap = { 'svn': lambda element_tree: list(element_tree.findall(SVN_URL)), 'git': lambda element_tree: list(element_tree.findall(GIT_URL)), 'hg': lambda element_tree: list(element_tree.findall(HG_URL)), None: lambda element_tree: [] } self._scmbranchmap = { 'svn': lambda element_tree: [], 'git': lambda element_tree: list(element_tree.findall(GIT_BRANCH)), 'hg': self._get_hg_branch, None: lambda element_tree: [] } JenkinsBase.__init__(self, url) def __str__(self): return self.name def get_description(self): return self._data["description"] def get_jenkins_obj(self): return self.jenkins # When the name of the hg branch used in the job is default hg branch (i.e. # default), Mercurial plugin doesn't store default branch name in # config XML file of the job. Create XML node corresponding to # default branch def _get_hg_branch(self, element_tree): branches = element_tree.findall(HG_BRANCH) if not branches: hg_default_branch = ET.Element('branch') hg_default_branch.text = DEFAULT_HG_BRANCH_NAME branches.append(hg_default_branch) return branches def poll(self, tree=None): data = super(Job, self).poll(tree=tree) if not tree: self._data = self._add_missing_builds(self._data) else: return data # pylint: disable=E1123 # Unexpected keyword arg 'params' def _add_missing_builds(self, data): """ Query Jenkins to get all builds of the job in the data object. Jenkins API loads the first 100 builds and thus may not contain all builds information. This method checks if all builds are loaded in the data object and updates it with the missing builds if needed. """ if not data.get("builds"): return data # do not call _buildid_for_type here: it would poll and do an infinite # loop oldest_loaded_build_number = data["builds"][-1]["number"] if not self._data['firstBuild']: first_build_number = oldest_loaded_build_number else: first_build_number = self._data["firstBuild"]["number"] all_builds_loaded = (oldest_loaded_build_number == first_build_number) if all_builds_loaded: return data response = self.poll(tree='allBuilds[number,url]') data['builds'] = response['allBuilds'] return data def _get_config_element_tree(self): """ The ElementTree objects creation is unnecessary, it can be a singleton per job """ if self._config is None: self.load_config() if self._element_tree is None: self._element_tree = ET.fromstring(self._config) return self._element_tree def get_build_triggerurl(self, files, build_params=None): if (files and build_params) or (not self.has_params()): # If job has file parameters and non-file parameters - it must be # triggered using "/build", not by "/buildWithParameters" # "/buildWithParameters" will ignore non-file parameters return "%s/build" % self.baseurl return "%s/buildWithParameters" % self.baseurl @staticmethod def _mk_json_from_build_parameters(build_params, file_params=None): """ Build parameters must be submitted in a particular format Key-Value pairs would be far too simple, no no! Watch and read on and behold! """ assert isinstance( build_params, dict), 'Build parameters must be a dict' build_p = [{'name': k, 'value': str(v)} for k, v in sorted(build_params.items())] out = {'parameter': build_p} if file_params: file_p = [{'name': k, 'file': k} for k in file_params.keys()] out['parameter'].extend(file_p) if len(out['parameter']) == 1: out['parameter'] = out['parameter'][0] return out @staticmethod def mk_json_from_build_parameters(build_params, file_params=None): json_structure = Job._mk_json_from_build_parameters( build_params, file_params ) json_structure['statusCode'] = "303" json_structure['redirectTo'] = "." return json.dumps(json_structure) def invoke(self, securitytoken=None, block=False, build_params=None, cause=None, files=None, delay=5): assert isinstance(block, bool) if build_params and (not self.has_params()): raise BadParams("This job does not support parameters") params = {} # Via Get string if securitytoken: params['token'] = securitytoken # Either copy the params dict or make a new one. build_params = build_params and dict( build_params.items()) or {} # Via POSTed JSON url = self.get_build_triggerurl(files, build_params) if cause: build_params['cause'] = cause # Build require params as form fields # and as Json. data = { 'json': self.mk_json_from_build_parameters( build_params, files) } data.update(build_params) response = self.jenkins.requester.post_and_confirm_status( url, data=data, params=params, files=files, valid=[200, 201, 303], allow_redirects=False ) redirect_url = response.headers['location'] if not redirect_url.startswith("%s/queue/item" % self.jenkins.baseurl): if files: raise ValueError('Builds with file parameters are not ' 'supported by this jenkinsapi version. ' 'Please use previous version.') else: raise ValueError("Not a Queue URL: %s" % redirect_url) qi = QueueItem(redirect_url, self.jenkins) if block: qi.block_until_complete(delay=delay) return qi def _buildid_for_type(self, buildtype): """Gets a buildid for a given type of build""" KNOWNBUILDTYPES = [ "lastStableBuild", "lastSuccessfulBuild", "lastBuild", "lastCompletedBuild", "firstBuild", "lastFailedBuild"] assert buildtype in KNOWNBUILDTYPES, ('Unknown build info type: %s' % buildtype) data = self.poll(tree='%s[number]' % buildtype) if not data.get(buildtype): raise NoBuildData(buildtype) return data[buildtype]["number"] def get_first_buildnumber(self): """ Get the numerical ID of the first build. """ return self._buildid_for_type("firstBuild") def get_last_stable_buildnumber(self): """ Get the numerical ID of the last stable build. """ return self._buildid_for_type("lastStableBuild") def get_last_good_buildnumber(self): """ Get the numerical ID of the last good build. """ return self._buildid_for_type("lastSuccessfulBuild") def get_last_failed_buildnumber(self): """ Get the numerical ID of the last good build. """ return self._buildid_for_type(buildtype="lastFailedBuild") def get_last_buildnumber(self): """ Get the numerical ID of the last build. """ return self._buildid_for_type("lastBuild") def get_last_completed_buildnumber(self): """ Get the numerical ID of the last complete build. """ return self._buildid_for_type("lastCompletedBuild") def get_build_dict(self): builds = self.poll(tree='builds[number,url]') if not builds: raise NoBuildData(repr(self)) builds = self._add_missing_builds(builds) builds = builds['builds'] last_build = self.poll(tree='lastBuild[number,url]')['lastBuild'] if builds and last_build and \ builds[0]['number'] != last_build['number']: builds = [last_build] + builds # FIXME SO how is this supposed to work if build is false-y? # I don't think that builds *can* be false here, so I don't # understand the test above. return dict((build["number"], build["url"]) for build in builds) def get_revision_dict(self): """ Get dictionary of all revisions with a list of buildnumbers (int) that used that particular revision """ revs = defaultdict(list) if 'builds' not in self._data: raise NoBuildData(repr(self)) for buildnumber in self.get_build_ids(): revs[self.get_build(buildnumber) .get_revision()].append(buildnumber) return revs def get_build_ids(self): """ Return a sorted list of all good builds as ints. """ return reversed(sorted(self.get_build_dict().keys())) def get_next_build_number(self): """ Return the next build number that Jenkins will assign. """ return self._data.get('nextBuildNumber', 0) def get_last_stable_build(self): """ Get the last stable build """ bn = self.get_last_stable_buildnumber() return self.get_build(bn) def get_last_good_build(self): """ Get the last good build """ bn = self.get_last_good_buildnumber() return self.get_build(bn) def get_last_build(self): """ Get the last build """ bn = self.get_last_buildnumber() return self.get_build(bn) def get_first_build(self): bn = self.get_first_buildnumber() return self.get_build(bn) def get_last_build_or_none(self): """ Get the last build or None if there is no builds """ try: return self.get_last_build() except NoBuildData: return None def get_last_completed_build(self): """ Get the last build regardless of status """ bn = self.get_last_completed_buildnumber() return self.get_build(bn) def get_buildnumber_for_revision(self, revision, refresh=False): """ :param revision: subversion revision to look for, int :param refresh: boolean, whether or not to refresh the revision -> buildnumber map :return: list of buildnumbers, [int] """ if self.get_scm_type() == 'svn' and not isinstance(revision, int): revision = int(revision) if self._revmap is None or refresh: self._revmap = self.get_revision_dict() try: return self._revmap[revision] except KeyError: raise NotFound("Couldn't find a build with that revision") def get_build(self, buildnumber): assert isinstance(buildnumber, int) url = self.get_build_dict()[buildnumber] return Build(url, buildnumber, job=self) def get_build_metadata(self, buildnumber): """ Get the build metadata for a given build number. For large builds with tons of tests, this method is faster than get_build by returning less data. """ assert isinstance(buildnumber, int) url = self.get_build_dict()[buildnumber] return Build(url, buildnumber, job=self, depth=0) def __getitem__(self, buildnumber): return self.get_build(buildnumber) def __len__(self): return len(self.get_build_dict()) def is_queued_or_running(self): return self.is_queued() or self.is_running() def is_queued(self): data = self.poll(tree='inQueue') return data.get('inQueue', False) def get_queue_item(self): """ Return a QueueItem if this object is in a queue, otherwise raise an exception """ if not self.is_queued(): raise UnknownQueueItem() return QueueItem(self.jenkins, **self._data['queueItem']) def is_running(self): # self.poll() try: build = self.get_last_build_or_none() if build is not None: return build.is_running() except NoBuildData: log.info( "No build info available for %s, assuming not running.", str(self)) return False def get_config(self): '''Returns the config.xml from the job''' response = self.jenkins.requester.get_and_confirm_status( "%(baseurl)s/config.xml" % self.__dict__) return response.text def load_config(self): self._config = self.get_config() def get_scm_type(self): element_tree = self._get_config_element_tree() scm_class = element_tree.find('scm').get('class') scm = self._scm_map.get(scm_class) if not scm: raise NotSupportSCM( 'SCM class "%s" not supported by API for job "%s"' % (scm_class, self.name)) if scm == 'NullSCM': raise NotConfiguredSCM( 'SCM is not configured for job "%s"' % self.name) return scm def get_scm_url(self): """ Get list of project SCM urls For some SCM's jenkins allow to configure and use number of SCM url's : return: list of SCM urls """ element_tree = self._get_config_element_tree() scm = self.get_scm_type() scm_url_list = [scm_url.text for scm_url in self._scmurlmap[ scm](element_tree)] return scm_url_list def get_scm_branch(self): """ Get list of SCM branches : return: list of SCM branches """ element_tree = self._get_config_element_tree() scm = self.get_scm_type() return [scm_branch.text for scm_branch in self._scmbranchmap[scm](element_tree)] def modify_scm_branch(self, new_branch, old_branch=None): """ Modify SCM ("Source Code Management") branch name for configured job. :param new_branch : new repository branch name to set. If job has multiple branches configured and "old_branch" not provided - method will allways modify first url. :param old_branch (optional): exact value of branch name to be replaced. For some SCM's jenkins allow set multiple branches per job this parameter intended to indicate which branch need to be modified """ element_tree = self._get_config_element_tree() scm = self.get_scm_type() scm_branch_list = self._scmbranchmap[scm](element_tree) if scm_branch_list and not old_branch: scm_branch_list[0].text = new_branch self.update_config(ET.tostring(element_tree)) else: for scm_branch in scm_branch_list: if scm_branch.text == old_branch: scm_branch.text = new_branch self.update_config(ET.tostring(element_tree)) def modify_scm_url(self, new_source_url, old_source_url=None): """ Modify SCM ("Source Code Management") url for configured job. :param new_source_url : new repository url to set. If job has multiple repositories configured and "old_source_url" not provided - method will allways modify first url. :param old_source_url (optional): for some SCM's jenkins allows settting multiple repositories per job this parameter intended to indicate which repository need to be modified """ element_tree = self._get_config_element_tree() scm = self.get_scm_type() scm_url_list = self._scmurlmap[scm](element_tree) if scm_url_list and not old_source_url: scm_url_list[0].text = new_source_url self.update_config(ET.tostring(element_tree)) else: for scm_url in scm_url_list: if scm_url.text == old_source_url: scm_url.text = new_source_url self.update_config(ET.tostring(element_tree)) def get_config_xml_url(self): return '%s/config.xml' % self.baseurl def update_config(self, config, full_response=False): """ Update the config.xml to the job Also refresh the ElementTree object since the config has changed :param full_response (optional): if True, it will return the full response object instead of just the response text. Useful for debugging and validation workflows. """ url = self.get_config_xml_url() try: if isinstance( config, unicode): # pylint: disable=undefined-variable config = str(config) except NameError: # Python3 already a str pass response = self.jenkins.requester.post_url(url, params={}, data=config) self._element_tree = ET.fromstring(config) if full_response: return response return response.text def get_downstream_jobs(self): """ Get all the possible downstream jobs :return List of Job """ downstream_jobs = [] try: for j in self._data['downstreamProjects']: downstream_jobs.append( self.get_jenkins_obj()[j['name']]) except KeyError: return [] return downstream_jobs def get_downstream_job_names(self): """ Get all the possible downstream job names :return List of String """ downstream_jobs = [] try: for j in self._data['downstreamProjects']: downstream_jobs.append(j['name']) except KeyError: return [] return downstream_jobs def get_upstream_job_names(self): """ Get all the possible upstream job names :return List of String """ upstream_jobs = [] try: for j in self._data['upstreamProjects']: upstream_jobs.append(j['name']) except KeyError: return [] return upstream_jobs def get_upstream_jobs(self): """ Get all the possible upstream jobs :return List of Job """ upstream_jobs = [] try: for j in self._data['upstreamProjects']: upstream_jobs.append(self.get_jenkins_obj().get_job(j['name'])) except KeyError: return [] return upstream_jobs def is_enabled(self): data = self.poll(tree='color') return data.get('color', None) != 'disabled' def disable(self): '''Disable job''' url = "%s/disable" % self.baseurl return self.get_jenkins_obj().requester.post_url(url, data='') def enable(self): '''Enable job''' url = "%s/enable" % self.baseurl return self.get_jenkins_obj().requester.post_url(url, data='') def delete_from_queue(self): """ Delete a job from the queue only if it's enqueued :raise NotInQueue if the job is not in the queue """ if not self.is_queued(): raise NotInQueue() queue_id = self._data['queueItem']['id'] url = urlparse.urljoin(self.get_jenkins_obj().get_queue().baseurl, 'cancelItem?id=%s' % queue_id) self.get_jenkins_obj().requester.post_and_confirm_status(url, data='') return True def get_params(self): """ Get the parameters for this job. Format varies by parameter type. Here is an example string parameter: { 'type': 'StringParameterDefinition', 'description': 'Parameter description', 'defaultParameterValue': {'value': 'default value'}, 'name': 'FOO_BAR' } """ places = ['actions', 'property'] found_definitions = False for place in places: if found_definitions: return actions = (x for x in self._data[place] if x is not None) for action in actions: try: for param in action['parameterDefinitions']: found_definitions = True yield param except KeyError: continue def get_params_list(self): """ Gets the list of parameter names for this job. """ return [param['name'] for param in self.get_params()] def has_params(self): """ If job has parameters, returns True, else False """ return any("parameterDefinitions" in a for a in ( self._data["actions"] or self._data["property"]) if a) def has_queued_build(self, build_params): """Returns True if a build with build_params is currently queued.""" queue = self.jenkins.get_queue() queued_builds = queue.get_queue_items_for_job(self.name) for build in queued_builds: if build.get_parameters() == build_params: return True return False jenkinsapi-0.2.29/jenkinsapi/node.py0000664000175000017500000002300112616220252016745 0ustar salsal00000000000000""" Module for jenkinsapi Node class """ from jenkinsapi.jenkinsbase import JenkinsBase from jenkinsapi.custom_exceptions import PostRequired from jenkinsapi.custom_exceptions import JenkinsAPIException import json import logging try: from urllib import quote as urlquote except ImportError: # Python3 from urllib.parse import quote as urlquote log = logging.getLogger(__name__) class Node(JenkinsBase): """ Class to hold information on nodes that are attached as slaves to the master jenkins instance """ def __init__(self, jenkins_obj, baseurl, nodename, node_dict, poll=True): """ Init a node object by providing all relevant pointers to it :param jenkins_obj: ref to the jenkins obj :param baseurl: basic url for querying information on a node If url is not set - object will construct it itself. This is useful when node is being created and not exists in Jenkins yet :param nodename: hostname of the node :param dict node_dict: Dict with node parameters as described below :param bool poll: set to False if node does not exist or automatic refresh from Jenkins is not required. Default is True. If baseurl parameter is set to None - poll parameter will be set to False JNLP Node: { 'num_executors': int, 'node_description': str, 'remote_fs': str, 'labels': str, 'exclusive': bool } SSH Node: { 'num_executors': int, 'node_description': str, 'remote_fs': str, 'labels': str, 'exclusive': bool, 'host': str, 'port': int 'credential_description': str, 'jvm_options': str, 'java_path': str, 'prefix_start_slave_cmd': str, 'suffix_start_slave_cmd': str 'max_num_retries': int, 'retry_wait_time': int, 'retention': str ('Always' or 'OnDemand') 'ondemand_delay': int (only for OnDemand retention) 'ondemand_idle_delay': int (only for OnDemand retention) 'env': [ { 'key':'TEST', 'value':'VALUE' }, { 'key':'TEST2', 'value':'value2' } ] } :return: None :return: Node obj """ self.name = nodename self.jenkins = jenkins_obj if not baseurl: poll = False baseurl = '%s/computer/%s' % (self.jenkins.baseurl, self.name) JenkinsBase.__init__(self, baseurl, poll=poll) self.node_attributes = node_dict def get_node_attributes(self): """ Gets node attributes as dict Used by Nodes object when node is created :return: Node attributes dict formatted for Jenkins API request to create node """ na = self.node_attributes if not na.get('credential_description', False): # If credentials description is not present - we will create # JNLP node launcher = {'stapler-class': 'hudson.slaves.JNLPLauncher'} else: try: credential = self.jenkins.credentials[ na['credential_description'] ] except KeyError: raise JenkinsAPIException('Credential with description "%s"' ' not found' % na['credential_descr']) retries = na['max_num_retries'] if 'max_num_retries' in na else '' re_wait = na['retry_wait_time'] if 'retry_wait_time' in na else '' launcher = { 'stapler-class': 'hudson.plugins.sshslaves.SSHLauncher', '$class': 'hudson.plugins.sshslaves.SSHLauncher', 'host': na['host'], 'port': na['port'], 'credentialsId': credential.credential_id, 'jvmOptions': na['jvm_options'], 'javaPath': na['java_path'], 'prefixStartSlaveCmd': na['prefix_start_slave_cmd'], 'suffixStartSlaveCmd': na['suffix_start_slave_cmd'], 'maxNumRetries': retries, 'retryWaitTime': re_wait } retention = { 'stapler-class': 'hudson.slaves.RetentionStrategy$Always', '$class': 'hudson.slaves.RetentionStrategy$Always' } if 'retention' in na and na['retention'].lower() == 'ondemand': retention = { 'stapler-class': 'hudson.slaves.RetentionStrategy$Demand', '$class': 'hudson.slaves.RetentionStrategy$Demand', 'inDemandDelay': na['ondemand_delay'], 'idleDelay': na['ondemand_idle_delay'] } if 'env' in na: node_props = { 'stapler-class-bag': 'true', 'hudson-slaves-EnvironmentVariablesNodeProperty': { 'env': na['env'] } } else: node_props = { 'stapler-class-bag': 'true' } params = { 'name': self.name, 'type': 'hudson.slaves.DumbSlave$DescriptorImpl', 'json': json.dumps({ 'name': self.name, 'nodeDescription': na['node_description'], 'numExecutors': na['num_executors'], 'remoteFS': na['remote_fs'], 'labelString': na['labels'], 'mode': 'EXCLUSIVE' if na['exclusive'] else 'NORMAL', 'retentionStrategy': retention, 'type': 'hudson.slaves.DumbSlave', 'nodeProperties': node_props, 'launcher': launcher }) } return params def get_jenkins_obj(self): return self.jenkins def __str__(self): return self.name def is_online(self): return not self.poll(tree='offline')['offline'] def is_temporarily_offline(self): return self.poll(tree='temporarilyOffline')['temporarilyOffline'] def is_jnlpagent(self): return self._data['jnlpAgent'] def is_idle(self): return self._data['idle'] def set_online(self): """ Set node online. Before change state verify client state: if node set 'offline' but 'temporarilyOffline' is not set - client has connection problems and AssertionError raised. If after run node state has not been changed raise AssertionError. """ self.poll() # Before change state check if client is connected if self._data['offline'] and not self._data['temporarilyOffline']: raise AssertionError("Node is offline and not marked as " "temporarilyOffline, check client " "connection: offline = %s, " "temporarilyOffline = %s" % (self._data['offline'], self._data['temporarilyOffline'])) elif self._data['offline'] and self._data['temporarilyOffline']: self.toggle_temporarily_offline() if self._data['offline']: raise AssertionError("The node state is still offline, " "check client connection:" " offline = %s, " "temporarilyOffline = %s" % (self._data['offline'], self._data['temporarilyOffline'])) def set_offline(self, message="requested from jenkinsapi"): """ Set node offline. If after run node state has not been changed raise AssertionError. : param message: optional string explain why you are taking this node offline """ if not self._data['offline']: self.toggle_temporarily_offline(message) data = self.poll(tree='offline,temporarilyOffline') if not data['offline']: raise AssertionError("The node state is still online:" + "offline = %s , temporarilyOffline = %s" % (data['offline'], data['temporarilyOffline'])) def toggle_temporarily_offline(self, message="requested from jenkinsapi"): """ Switches state of connected node (online/offline) and set 'temporarilyOffline' property (True/False) Calling the same method again will bring node status back. :param message: optional string can be used to explain why you are taking this node offline """ initial_state = self.is_temporarily_offline() url = self.baseurl + \ "/toggleOffline?offlineMessage=" + urlquote(message) try: html_result = self.jenkins.requester.get_and_confirm_status(url) except PostRequired: html_result = self.jenkins.requester.post_and_confirm_status( url, data={}) self.poll() log.debug(html_result) state = self.is_temporarily_offline() if initial_state == state: raise AssertionError( "The node state has not changed: temporarilyOffline = %s" % state) jenkinsapi-0.2.29/jenkinsapi/queue.py0000664000175000017500000001110212616220252017143 0ustar salsal00000000000000""" Queue module for jenkinsapi """ from requests import HTTPError from jenkinsapi.jenkinsbase import JenkinsBase from jenkinsapi.custom_exceptions import UnknownQueueItem, NotBuiltYet import logging import time log = logging.getLogger(__name__) class Queue(JenkinsBase): """ Class that represents the Jenkins queue """ def __init__(self, baseurl, jenkins_obj): """ Init the Jenkins queue object :param baseurl: basic url for the queue :param jenkins_obj: ref to the jenkins obj """ self.jenkins = jenkins_obj JenkinsBase.__init__(self, baseurl) def __str__(self): return self.baseurl def get_jenkins_obj(self): return self.jenkins def iteritems(self): for item in self._data['items']: queue_id = item['id'] item_baseurl = "%s/item/%i" % (self.baseurl, queue_id) yield item['id'], QueueItem(baseurl=item_baseurl, jenkins_obj=self.jenkins) def iterkeys(self): for item in self._data['items']: yield item['id'] def itervalues(self): for item in self._data['items']: yield QueueItem(self.jenkins, **item) def keys(self): return list(self.iterkeys()) def values(self): return list(self.itervalues()) def __len__(self): return len(self._data['items']) def __getitem__(self, item_id): self_as_dict = dict(self.iteritems()) if item_id in self_as_dict: return self_as_dict[item_id] else: raise UnknownQueueItem(item_id) def _get_queue_items_for_job(self, job_name): for item in self._data["items"]: if item['task']['name'] == job_name: yield QueueItem(self.get_queue_item_url(item), jenkins_obj=self.jenkins) def get_queue_items_for_job(self, job_name): return list(self._get_queue_items_for_job(job_name)) def get_queue_item_url(self, item): return "%s/item/%i" % (self.baseurl, item["id"]) def delete_item(self, queue_item): self.delete_item_by_id(queue_item.queue_id) def delete_item_by_id(self, item_id): deleteurl = '%s/cancelItem?id=%s' % (self.baseurl, item_id) self.get_jenkins_obj().requester.post_url(deleteurl) class QueueItem(JenkinsBase): """An individual item in the queue """ def __init__(self, baseurl, jenkins_obj): self.jenkins = jenkins_obj JenkinsBase.__init__(self, baseurl) @property def queue_id(self): return self._data['id'] @property def name(self): return self._data['task']['name'] def get_jenkins_obj(self): return self.jenkins def get_job(self): """ Return the job associated with this queue item """ return self.jenkins[self._data['task']['name']] def get_parameters(self): """returns parameters of queue item""" actions = self._data.get('actions', []) for action in actions: if isinstance(action, dict) and 'parameters' in action: parameters = action['parameters'] return dict([(x['name'], x.get('value', None)) for x in parameters]) return [] def __repr__(self): return "<%s.%s %s>" % (self.__class__.__module__, self.__class__.__name__, str(self)) def __str__(self): return "%s Queue #%i" % (self.name, self.queue_id) def get_build(self): build_number = self.get_build_number() job_name = self.get_job_name() return self.jenkins[job_name][build_number] def block_until_complete(self, delay=5): build = self.block_until_building(delay) return build.block_until_complete(delay=delay) def block_until_building(self, delay=5): while True: try: self.poll() return self.get_build() except (NotBuiltYet, HTTPError): time.sleep(delay) continue def is_running(self): """Return True if this queued item is running. """ try: return self.get_build().is_running() except NotBuiltYet: return False def get_build_number(self): try: return self._data['executable']['number'] except KeyError: raise NotBuiltYet() def get_job_name(self): try: return self._data['task']['name'] except KeyError: raise NotBuiltYet() jenkinsapi-0.2.29/jenkinsapi/command_line/0000775000175000017500000000000012616521664020112 5ustar salsal00000000000000jenkinsapi-0.2.29/jenkinsapi/command_line/jenkins_invoke.py0000664000175000017500000000527412616220252023475 0ustar salsal00000000000000""" jenkinsapi class for invoking Jenkins """ import os import sys import logging import optparse from jenkinsapi import jenkins log = logging.getLogger(__name__) class JenkinsInvoke(object): """ JenkinsInvoke object implements class to call from command line """ @classmethod def mkparser(cls): parser = optparse.OptionParser() DEFAULT_BASEURL = os.environ.get( "JENKINS_URL", "http://localhost/jenkins") parser.help_text = "Execute a number of jenkins jobs on the server of your choice." + \ " Optionally block until the jobs are complete." parser.add_option("-J", "--jenkinsbase", dest="baseurl", help="Base URL for the Jenkins server, default is %s" % DEFAULT_BASEURL, type="str", default=DEFAULT_BASEURL) parser.add_option('--username', '-u', dest='username', help="Username for jenkins authentification", type='str', default=None) parser.add_option('--password', '-p', dest='password', help="password for jenkins user auth", type='str', default=None) parser.add_option("-b", "--block", dest="block", action="store_true", default=False, help="Block until each of the jobs is complete.") parser.add_option("-t", "--token", dest="token", help="Optional security token.", default=None) return parser @classmethod def main(cls): parser = cls.mkparser() options, args = parser.parse_args() try: assert len(args) > 0, "Need to specify at least one job name" except AssertionError as err: log.critical(err.message) parser.print_help() sys.exit(1) invoker = cls(options, args) invoker() def __init__(self, options, jobs): self.options = options self.jobs = jobs self.api = self._get_api( baseurl=options.baseurl, username=options.username, password=options.password) def _get_api(self, baseurl, username, password): return jenkins.Jenkins(baseurl, username, password) def __call__(self): for job in self.jobs: self.invokejob( job, block=self.options.block, token=self.options.token) def invokejob(self, jobname, block, token): assert isinstance(block, bool) assert isinstance(jobname, str) assert token is None or isinstance(token, str) job = self.api.get_job(jobname) job.invoke(securitytoken=token, block=block) def main(): logging.basicConfig() logging.getLogger("").setLevel(logging.INFO) JenkinsInvoke.main() jenkinsapi-0.2.29/jenkinsapi/command_line/__init__.py0000664000175000017500000000005312616220252022206 0ustar salsal00000000000000""" __init__,py for commandline module """ jenkinsapi-0.2.29/jenkinsapi/command_line/jenkinsapi_version.py0000664000175000017500000000027712616220252024357 0ustar salsal00000000000000""" jenkinsapi.command_line.jenkinsapi_version """ from jenkinsapi import __version__ as version import sys def main(): sys.stdout.write(version) if __name__ == '__main__': main() jenkinsapi-0.2.29/jenkinsapi/plugin.py0000664000175000017500000000077612616220252017334 0ustar salsal00000000000000""" Module for jenkinsapi Plugin """ class Plugin(object): """ Plugin class """ def __init__(self, plugin_dict): assert isinstance(plugin_dict, dict) self.__dict__ = plugin_dict def __eq__(self, other): return self.__dict__ == other.__dict__ def __str__(self): return self.shortName def __repr__(self): return "<%s.%s %s>" % ( self.__class__.__module__, self.__class__.__name__, str(self) ) jenkinsapi-0.2.29/jenkinsapi/constants.py0000664000175000017500000000064012616220252020040 0ustar salsal00000000000000""" Constants for jenkinsapi """ import re STATUS_FAIL = "FAIL" STATUS_ERROR = "ERROR" STATUS_ABORTED = "ABORTED" STATUS_REGRESSION = "REGRESSION" STATUS_SUCCESS = "SUCCESS" STATUS_FIXED = "FIXED" STATUS_PASSED = "PASSED" RESULTSTATUS_FAILURE = "FAILURE" RESULTSTATUS_FAILED = "FAILED" RESULTSTATUS_SKIPPED = "SKIPPED" STR_RE_SPLIT_VIEW = "(.*)/view/([^/]*)/?" RE_SPLIT_VIEW_URL = re.compile(STR_RE_SPLIT_VIEW) jenkinsapi-0.2.29/jenkinsapi/utils/0000775000175000017500000000000012616521664016625 5ustar salsal00000000000000jenkinsapi-0.2.29/jenkinsapi/utils/krb_requester.py0000664000175000017500000000263512616220252022047 0ustar salsal00000000000000""" Kerberos aware Requester """ from jenkinsapi.utils.requester import Requester from requests_kerberos import HTTPKerberosAuth, OPTIONAL # pylint: disable=W0222 class KrbRequester(Requester): """ A class which carries out HTTP requests with Kerberos/GSSAPI authentication. """ def __init__(self, ssl_verify=None, baseurl=None, mutual_auth=OPTIONAL): """ :param ssl_verify: flag indicating if server certificate in HTTPS requests should be verified :param baseurl: Jenkins' base URL :param mutual_auth: type of mutual authentication, use one of REQUIRED, OPTIONAL or DISABLED from requests_kerberos package """ args = {} if ssl_verify: args["ssl_verify"] = ssl_verify if baseurl: args["baseurl"] = baseurl super(KrbRequester, self).__init__(**args) self.mutual_auth = mutual_auth def get_request_dict( self, params=None, data=None, files=None, headers=None, **kwargs): req_dict = super(KrbRequester, self).get_request_dict(params=params, data=data, files=files, headers=headers) if self.mutual_auth: auth = HTTPKerberosAuth(self.mutual_auth) else: auth = HTTPKerberosAuth() req_dict['auth'] = auth return req_dict jenkinsapi-0.2.29/jenkinsapi/utils/requester.py0000664000175000017500000001251612616220252021210 0ustar salsal00000000000000""" Module for jenkinsapi requester (which is a wrapper around python-requests) """ import requests try: import urlparse except ImportError: # Python3 import urllib.parse as urlparse from jenkinsapi.custom_exceptions import JenkinsAPIException, PostRequired # import logging # # these two lines enable debugging at httplib level (requests->urllib3->httplib) # # you will see the REQUEST, including HEADERS and DATA, and RESPONSE with HEADERS but without DATA. # # the only thing missing will be the response.body which is not logged. # import httplib # httplib.HTTPConnection.debuglevel = 1 # logging.basicConfig() # you need to initialize logging, otherwise you will not see anything from requests # logging.getLogger().setLevel(logging.DEBUG) # requests_log = logging.getLogger("requests.packages.urllib3") # requests_log.setLevel(logging.DEBUG) # requests_log.propagate = True 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. """ VALID_STATUS_CODES = [200, ] def __init__( self, username=None, password=None, ssl_verify=True, baseurl=None): if username: assert password, 'Cannot set a username without a password!' self.base_scheme = urlparse.urlsplit( baseurl).scheme if baseurl else None self.username = username self.password = password self.ssl_verify = ssl_verify def get_request_dict( self, params=None, data=None, files=None, headers=None, **kwargs): requestKwargs = kwargs 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 requestKwargs['verify'] = self.ssl_verify if data: # It may seem odd, but some Jenkins operations require posting # an empty string. requestKwargs['data'] = data if files: requestKwargs['files'] = files return requestKwargs def _update_url_scheme(self, url): """ Updates scheme of given url to the one used in Jenkins baseurl. """ if self.base_scheme and not url.startswith("%s://" % self.base_scheme): url_split = urlparse.urlsplit(url) url = urlparse.urlunsplit( [ self.base_scheme, url_split.netloc, url_split.path, url_split.query, url_split.fragment ] ) return url def get_url(self, url, params=None, headers=None, allow_redirects=True): requestKwargs = self.get_request_dict( params=params, headers=headers, allow_redirects=allow_redirects) return requests.get(self._update_url_scheme(url), **requestKwargs) def post_url(self, url, params=None, data=None, files=None, headers=None, allow_redirects=True): requestKwargs = self.get_request_dict( params=params, data=data, files=files, headers=headers, allow_redirects=allow_redirects) return requests.post(self._update_url_scheme(url), **requestKwargs) def post_xml_and_confirm_status( self, url, params=None, data=None, valid=None): headers = {'Content-Type': 'text/xml'} return self.post_and_confirm_status( url, params=params, data=data, headers=headers, valid=valid) def post_and_confirm_status( self, url, params=None, data=None, files=None, headers=None, valid=None, allow_redirects=True): valid = valid or self.VALID_STATUS_CODES if not headers and not files: headers = {'Content-Type': 'application/x-www-form-urlencoded'} assert data is not None, "Post messages must have data" response = self.post_url( url, params, data, files, headers, allow_redirects) if response.status_code not in valid: raise JenkinsAPIException('Operation failed. url={0}, data={1}, headers={2}, status={3}, text={4}'.format( response.url, data, headers, response.status_code, response.text.encode('UTF-8'))) return response def get_and_confirm_status( self, url, params=None, headers=None, valid=None): valid = valid or self.VALID_STATUS_CODES response = self.get_url(url, params, headers) if response.status_code not in valid: if response.status_code == 405: # POST required raise PostRequired('POST required for url {0}'.format(url)) else: raise JenkinsAPIException('Operation failed. url={0}, headers={1}, status={2}, text={3}'.format( response.url, headers, response.status_code, response.text.encode('UTF-8'))) return response jenkinsapi-0.2.29/jenkinsapi/utils/__init__.py0000664000175000017500000000004212616220252020717 0ustar salsal00000000000000""" Module __init__ for utils """ jenkinsapi-0.2.29/jenkinsapi/jenkinsbase.py0000664000175000017500000000672612616220252020333 0ustar salsal00000000000000""" Module for JenkinsBase class """ import ast import pprint import logging from jenkinsapi import config from jenkinsapi.custom_exceptions import JenkinsAPIException class JenkinsBase(object): """ This appears to be the base object that all other jenkins objects are inherited from """ RETRY_ATTEMPTS = 1 def __repr__(self): return """<%s.%s %s>""" % (self.__class__.__module__, self.__class__.__name__, str(self)) def __str__(self): raise NotImplementedError def __init__(self, baseurl, poll=True): """ Initialize a jenkins connection """ self._data = None self.baseurl = self.strip_trailing_slash(baseurl) if poll: self.poll() def get_jenkins_obj(self): raise NotImplementedError( 'Please implement this method on %s' % self.__class__.__name__) 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 @classmethod def strip_trailing_slash(cls, url): while url.endswith('/'): url = url[:-1] return url def poll(self, tree=None): data = self._poll(tree=tree) if 'jobs' in data: data['jobs'] = self.resolve_job_folders(data['jobs']) if not tree: self._data = data else: return data def _poll(self, tree=None): url = self.python_api_url(self.baseurl) return self.get_data(url, tree=tree) def get_data(self, url, params=None, tree=None): requester = self.get_jenkins_obj().requester if tree: if not params: params = {'tree': tree} else: params.update({'tree': tree}) response = requester.get_url(url, params) if response.status_code != 200: logging.error('Failed request at %s with params: %s %s', url, params, tree if tree else '') response.raise_for_status() try: return ast.literal_eval(response.text) except Exception: logging.exception('Inappropriate content found at %s', url) raise JenkinsAPIException('Cannot parse %s' % response.content) def pprint(self): """ Print out all the data in this object for debugging. """ pprint.pprint(self._data) def resolve_job_folders(self, jobs): for job in list(jobs): if 'color' not in job.keys(): jobs.remove(job) jobs += self.process_job_folder(job) return jobs def process_job_folder(self, folder): data = self.get_data(self.python_api_url(folder['url'])) result = [] for job in data.get('jobs', []): if 'color' not in job.keys(): result += self.process_job_folder(job) else: result.append(job) return result @classmethod def python_api_url(cls, url): if url.endswith(config.JENKINS_API): return url else: if url.endswith(r"/"): fmt = "%s%s" else: fmt = "%s/%s" return fmt % (url, config.JENKINS_API) jenkinsapi-0.2.29/jenkinsapi/mutable_jenkins_thing.py0000664000175000017500000000047512616220252022375 0ustar salsal00000000000000""" Module for MutableJenkinsThing """ class MutableJenkinsThing(object): """ A mixin for certain mutable objects which can be renamed and deleted. """ def get_delete_url(self): return '%s/doDelete' % self.baseurl def get_rename_url(self): return '%s/doRename' % self.baseurl jenkinsapi-0.2.29/jenkinsapi/plugins.py0000664000175000017500000000326212616220252017510 0ustar salsal00000000000000""" jenkinsapi plugins """ from __future__ import print_function import logging from jenkinsapi.plugin import Plugin from jenkinsapi.jenkinsbase import JenkinsBase from jenkinsapi.custom_exceptions import UnknownPlugin log = logging.getLogger(__name__) class Plugins(JenkinsBase): """ Plugins class for jenkinsapi """ def __init__(self, url, jenkins_obj): self.jenkins_obj = jenkins_obj JenkinsBase.__init__(self, url) # print('DEBUG: Plugins._data=', self._data) def get_jenkins_obj(self): return self.jenkins_obj def _poll(self, tree=None): return self.get_data(self.baseurl, tree=tree) def keys(self): return self.get_plugins_dict().keys() __iter__ = keys def iteritems(self): return self._get_plugins() def values(self): return [a[1] for a in self.iteritems()] def _get_plugins(self): if 'plugins' in self._data: for p_dict in self._data["plugins"]: yield p_dict["shortName"], Plugin(p_dict) def get_plugins_dict(self): return dict(self._get_plugins()) def __len__(self): return len(self.get_plugins_dict().keys()) def __getitem__(self, plugin_name): try: return self.get_plugins_dict()[plugin_name] except KeyError: raise UnknownPlugin(plugin_name) def __contains__(self, plugin_name): """ True if plugin_name is the name of a defined plugin """ return plugin_name in self.keys() def __str__(self): plugins = [plugin["shortName"] for plugin in self._data.get("plugins", [])] return str(sorted(plugins)) jenkinsapi-0.2.29/jenkinsapi/build.py0000664000175000017500000003567112616220252017137 0ustar salsal00000000000000""" A jenkins build represents a single execution of a Jenkins Job. Builds can be thought of as the second level of the jenkins heirarchy beneath Jobs. Builds can have state, such as whether they are running or not. They can also have outcomes, such as wether they passed or failed. Build objects can be associated with Results and Artifacts.g """ import time import pytz import logging import warnings import datetime from time import sleep from jenkinsapi import config from jenkinsapi.artifact import Artifact from jenkinsapi.result_set import ResultSet from jenkinsapi.jenkinsbase import JenkinsBase from jenkinsapi.constants import STATUS_SUCCESS from jenkinsapi.custom_exceptions import NoResults from jenkinsapi.custom_exceptions import JenkinsAPIException try: from urllib import quote except ImportError: # Python3 from urllib.parse import quote log = logging.getLogger(__name__) class Build(JenkinsBase): """ Represents a jenkins build, executed in context of a job. """ STR_TOTALCOUNT = "totalCount" STR_TPL_NOTESTS_ERR = ("%s has status %s, and does not have " "any test results") def __init__(self, url, buildno, job, depth=1): """ depth=1 is for backward compatibility consideration About depth, the deeper it is, the more build data you get back. If depth=0 is sufficient for you, don't go up to 1. See section 'Depth control' of https://wiki.jenkins-ci.org/display/JENKINS/Remote+access+API """ assert isinstance(buildno, int) self.buildno = buildno self.job = job self.depth = depth JenkinsBase.__init__(self, url) def _poll(self, tree=None): # For build's we need more information for downstream and # upstream builds so we override the poll to get at the extra # data for build objects url = self.python_api_url(self.baseurl) return self.get_data(url, params={'depth': self.depth}, tree=tree) def __str__(self): return self._data['fullDisplayName'] @property def name(self): return str(self) def get_description(self): return self._data["description"] def get_number(self): return self._data["number"] def get_status(self): return self._data["result"] def get_slave(self): return self._data["builtOn"] def get_revision(self): vcs = self._data['changeSet']['kind'] or 'git' return getattr(self, '_get_%s_rev' % vcs, lambda: None)() def get_revision_branch(self): vcs = self._data['changeSet']['kind'] or 'git' return getattr(self, '_get_%s_rev_branch' % vcs, lambda: None)() def get_changeset_items(self): """ Returns a list of changeSet items. Each item has structure as in following example: { "affectedPaths": [ "content/rcm/v00-rcm-xccdf.xml" ], "author" : { "absoluteUrl": "http://jenkins_url/user/username79", "fullName": "username" }, "commitId": "3097", "timestamp": 1414398423091, "date": "2014-10-27T08:27:03.091288Z", "msg": "commit message", "paths": [{ "editType": "edit", "file": "/some/path/of/changed_file" }], "revision": 3097, "user": "username" } """ if 'items' in self._data['changeSet']: return self._data['changeSet']['items'] else: return [] def _get_svn_rev(self): warnings.warn( "This untested function may soon be removed from Jenkinsapi " "(get_svn_rev).") maxRevision = 0 for repoPathSet in self._data["changeSet"]["revisions"]: maxRevision = max(repoPathSet["revision"], maxRevision) return maxRevision def _get_git_rev(self): # Sometimes we have None as part of actions. Filter those actions # which have lastBuiltRevision in them _actions = [x for x in self._data['actions'] if x and "lastBuiltRevision" in x] if len(_actions) > 0: return _actions[0]["lastBuiltRevision"]["SHA1"] return None def _get_hg_rev(self): warnings.warn( "This untested function may soon be removed from Jenkinsapi " "(_get_hg_rev).") return [x['mercurialNodeName'] for x in self._data['actions'] if 'mercurialNodeName' in x][0] def _get_svn_rev_branch(self): raise NotImplementedError('_get_svn_rev_branch is not yet implemented') def _get_git_rev_branch(self): # Sometimes we have None as part of actions. Filter those actions # which have lastBuiltRevision in them _actions = [x for x in self._data['actions'] if x and "lastBuiltRevision" in x] return _actions[0]["lastBuiltRevision"]["branch"] def _get_hg_rev_branch(self): raise NotImplementedError('_get_hg_rev_branch is not yet implemented') def get_duration(self): return datetime.timedelta(milliseconds=self._data["duration"]) def get_artifacts(self): data = self.poll(tree='artifacts[relativePath,fileName]') for afinfo in data["artifacts"]: url = "%s/artifact/%s" % (self.baseurl, quote(afinfo["relativePath"])) af = Artifact(afinfo["fileName"], url, self) yield af def get_artifact_dict(self): return dict( (af.filename, af) for af in self.get_artifacts() ) def get_upstream_job_name(self): """ Get the upstream job name if it exist, None otherwise :return: String or None """ try: return self.get_actions()['causes'][0]['upstreamProject'] except KeyError: return None def get_upstream_job(self): """ Get the upstream job object if it exist, None otherwise :return: Job or None """ if self.get_upstream_job_name(): return self.get_jenkins_obj().get_job(self.get_upstream_job_name()) else: return None def get_upstream_build_number(self): """ Get the upstream build number if it exist, None otherwise :return: int or None """ try: return int(self.get_actions()['causes'][0]['upstreamBuild']) except KeyError: return None def get_upstream_build(self): """ Get the upstream build if it exist, None otherwise :return Build or None """ upstream_job = self.get_upstream_job() if upstream_job: return upstream_job.get_build(self.get_upstream_build_number()) else: return None def get_master_job_name(self): """ Get the master job name if it exist, None otherwise :return: String or None """ try: return self.get_actions()['parameters'][0]['value'] except KeyError: return None def get_master_job(self): """ Get the master job object if it exist, None otherwise :return: Job or None """ warnings.warn( "This untested function may soon be removed from Jenkinsapi " "(get_master_job).") if self.get_master_job_name(): return self.get_jenkins_obj().get_job(self.get_master_job_name()) else: return None def get_master_build_number(self): """ Get the master build number if it exist, None otherwise :return: int or None """ warnings.warn( "This untested function may soon be removed from Jenkinsapi " "(get_master_build_number).") try: return int(self.get_actions()['parameters'][1]['value']) except KeyError: return None def get_master_build(self): """ Get the master build if it exist, None otherwise :return Build or None """ warnings.warn( "This untested function may soon be removed from Jenkinsapi " "(get_master_build).") master_job = self.get_master_job() if master_job: return master_job.get_build(self.get_master_build_number()) else: return None def get_downstream_jobs(self): """ Get the downstream jobs for this build :return List of jobs or None """ warnings.warn( "This untested function may soon be removed from Jenkinsapi " "(get_downstream_jobs).") downstream_jobs = [] try: for job_name in self.get_downstream_job_names(): downstream_jobs.append( self.get_jenkins_obj().get_job(job_name)) return downstream_jobs except (IndexError, KeyError): return [] def get_downstream_job_names(self): """ Get the downstream job names for this build :return List of string or None """ downstream_job_names = self.job.get_downstream_job_names() downstream_names = [] try: fingerprints = self._data["fingerprint"] for fingerprint in fingerprints: for job_usage in fingerprint['usage']: if job_usage['name'] in downstream_job_names: downstream_names.append(job_usage['name']) return downstream_names except (IndexError, KeyError): return [] def get_downstream_builds(self): """ Get the downstream builds for this build :return List of Build or None """ downstream_job_names = self.get_downstream_job_names() downstream_builds = [] try: fingerprints = self._data["fingerprint"] for fingerprint in fingerprints: for job_usage in fingerprint['usage']: if job_usage['name'] in downstream_job_names: job = self.get_jenkins_obj().get_job(job_usage['name']) for job_range in job_usage['ranges']['ranges']: for build_id in range(job_range['start'], job_range['end']): downstream_builds.append( job.get_build(build_id)) return downstream_builds except (IndexError, KeyError): return [] def get_matrix_runs(self): """ For a matrix job, get the individual builds for each matrix configuration :return: Generator of Build """ if 'runs' in self._data: for rinfo in self._data['runs']: number = rinfo['number'] if number == self._data['number']: yield Build(rinfo['url'], number, self.job) def is_running(self): """ Return a bool if running. """ data = self.poll(tree='building') return data.get('building', False) def block(self): while self.is_running(): time.sleep(1) def is_good(self): """ Return a bool, true if the build was good. If the build is still running, return False. """ return (not self.is_running()) and \ self._data["result"] == STATUS_SUCCESS def block_until_complete(self, delay=15): assert isinstance(delay, int) count = 0 while self.is_running(): total_wait = delay * count log.info( msg="Waited %is for %s #%s to complete" % (total_wait, self.job.name, self.name)) sleep(delay) count += 1 def get_jenkins_obj(self): return self.job.get_jenkins_obj() def get_result_url(self): """ Return the URL for the object which provides the job's result summary. """ url_tpl = r"%stestReport/%s" return url_tpl % (self._data["url"], config.JENKINS_API) def get_resultset(self): """ Obtain detailed results for this build. """ result_url = self.get_result_url() if self.STR_TOTALCOUNT not in self.get_actions(): raise NoResults( "%s does not have any published results" % str(self)) buildstatus = self.get_status() if not self.get_actions()[self.STR_TOTALCOUNT]: raise NoResults( self.STR_TPL_NOTESTS_ERR % (str(self), buildstatus)) obj_results = ResultSet(result_url, build=self) return obj_results def has_resultset(self): """ Return a boolean, true if a result set is available. false if not. """ return self.STR_TOTALCOUNT in self.get_actions() def get_actions(self): all_actions = {} for dct_action in self._data["actions"]: if dct_action is None: continue all_actions.update(dct_action) return all_actions def get_causes(self): ''' Returns a list of causes. There can be multiple causes lists and some of the can be empty. For instance, when a build is manually aborted, Jenkins could add an empty causes list to the actions dict. Empty ones are ignored. ''' all_causes = [] for dct_action in self._data["actions"]: if dct_action is None: continue if 'causes' in dct_action and dct_action['causes']: all_causes.extend(dct_action['causes']) return all_causes def get_timestamp(self): ''' Returns build timestamp in UTC ''' # Java timestamps are given in miliseconds since the epoch start! naive_timestamp = datetime.datetime( *time.gmtime(self._data['timestamp'] / 1000.0)[:6]) return pytz.utc.localize(naive_timestamp) def get_console(self): """ Return the current state of the text console. """ url = "%s/consoleText" % self.baseurl content = self.job.jenkins.requester.get_url(url).content # This check was made for Python 3.x # In this version content is a bytes string # By contract this function must return string if isinstance(content, str): return content elif isinstance(content, bytes): return content.decode('ISO-8859-1') else: raise JenkinsAPIException('Unknown content type for console') def stop(self): """ Stops the build execution if it's running :return boolean True if succeded False otherwise or the build is not running """ if self.is_running(): url = "%s/stop" % self.baseurl self.job.jenkins.requester.post_and_confirm_status(url, data='') return True return False jenkinsapi-0.2.29/jenkinsapi/artifact.py0000664000175000017500000001066212616220252017626 0ustar salsal00000000000000""" Artifacts can be used to represent data created as a side-effect of running a Jenkins build. Artifacts are files which are associated with a single build. A build can have any number of artifacts associated with it. This module provides a class called Artifact which allows you to download objects from the server and also access them as a stream. """ import os import logging import hashlib from jenkinsapi.fingerprint import Fingerprint from jenkinsapi.custom_exceptions import ArtifactBroken log = logging.getLogger(__name__) class Artifact(object): """ Represents a single Jenkins artifact, usually some kind of file generated as a by-product of executing a Jenkins build. """ def __init__(self, filename, url, build): self.filename = filename self.url = url self.build = build def save(self, fspath, strict_validation=False): """ Save the artifact to an explicit path. The containing directory must exist. Returns a reference to the file which has just been writen to. :param fspath: full pathname including the filename, str :return: filepath """ log.info(msg="Saving artifact @ %s to %s" % (self.url, fspath)) if not fspath.endswith(self.filename): log.warn( msg="Attempt to change the filename of artifact %s on save." % self.filename) if os.path.exists(fspath): if self.build: try: if self._verify_download(fspath, strict_validation): log.info( msg="Local copy of %s is already up to date." % self.filename) return fspath except ArtifactBroken: log.warning("Jenkins artifact could not be identified.") else: log.info("This file did not originate from Jenkins, " "so cannot check.") else: log.info("Local file is missing, downloading new.") filepath = self._do_download(fspath) self._verify_download(filepath, strict_validation) return fspath def get_jenkins_obj(self): return self.build.get_jenkins_obj() def get_data(self): """ Grab the text of the artifact """ response = self.get_jenkins_obj().requester.get_and_confirm_status( self.url) return response.content def _do_download(self, fspath): """ Download the the artifact to a path. """ with open(fspath, "wb") as out: out.write(self.get_data()) return fspath def _verify_download(self, fspath, strict_validation): """ Verify that a downloaded object has a valid fingerprint. """ local_md5 = self._md5sum(fspath) baseurl = self.build.job.jenkins.baseurl fp = Fingerprint( baseurl, local_md5, self.build.job.jenkins) valid = fp.validate_for_build( os.path.basename(fspath), self.build.job.name, self.build.buildno) if not valid or (fp.unknown and strict_validation): # strict = 404 as invalid raise ArtifactBroken("Artifact %s seems to be broken, check %s" % (local_md5, baseurl)) return True def _md5sum(self, fspath, chunksize=2 ** 20): """ A MD5 hashing function intended to produce the same results as that used by Jenkins. """ md5 = hashlib.md5() try: with open(fspath, 'rb') as f: for chunk in iter(lambda: f.read(chunksize), ''): if chunk: md5.update(chunk) else: break except: raise return md5.hexdigest() def save_to_dir(self, dirpath, strict_validation=False): """ Save the artifact to a folder. The containing directory must exist, but use the artifact's default filename. """ assert os.path.exists(dirpath) assert os.path.isdir(dirpath) outputfilepath = os.path.join(dirpath, self.filename) return self.save(outputfilepath, strict_validation) def __repr__(self): """ Produce a handy repr-string. """ return """<%s.%s %s>""" % (self.__class__.__module__, self.__class__.__name__, self.url) jenkinsapi-0.2.29/jenkinsapi/__init__.py0000664000175000017500000000442312616220252017566 0ustar salsal00000000000000""" 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 (and It's predecessor Hudson) are fantastic projects - 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. This library can help you: * Query the test-results of a completed build * Get a objects representing the latest builds of a job * Search for artefacts by simple criteria * Block until jobs are complete * 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 Installing JenkinsAPI ===================== Egg-files for this project are hosted on PyPi. Most Python users should be able to use pip or distribute to automatically install this project. Most users can do the following: easy_install jenkinsapi If you'd like to install in multi-version mode: easy_install -m jenkinsapi Project Authors =============== * Salim Fadhley (sal@stodge.org) * Ramon van Alteren (ramon@vanalteren.nl) * Ruslan Lutsenko (ruslan.lutcenko@gmail.com) Current code lives on github: https://github.com/salimfadhley/jenkinsapi """ import sys from jenkinsapi import ( # Modules command_line, utils, # Files 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", "executors", "executor", "fingerprint", "jenkins", "jenkinsbase", "job", "node", "result_set", "result", "view" ] __docformat__ = "epytext" # In case of jenkinsapi is not installed in 'develop' mode __version__ = '99.99.99' try: import pkg_resources __version__ = pkg_resources.working_set.by_key['jenkinsapi'].version except ImportError: pass except KeyError: pass jenkinsapi-0.2.29/jenkinsapi/jenkins.py0000664000175000017500000003006612616220252017472 0ustar salsal00000000000000""" Module for jenkinsapi Jenkins object """ try: import urlparse from urllib import quote as urlquote except ImportError: # Python3 import urllib.parse as urlparse from urllib.parse import quote as urlquote import logging from jenkinsapi import config from jenkinsapi.credentials import Credentials from jenkinsapi.executors import Executors from jenkinsapi.job import Job from jenkinsapi.jobs import Jobs from jenkinsapi.view import View from jenkinsapi.nodes import Nodes from jenkinsapi.plugins import Plugins from jenkinsapi.views import Views from jenkinsapi.queue import Queue from jenkinsapi.fingerprint import Fingerprint from jenkinsapi.jenkinsbase import JenkinsBase from jenkinsapi.utils.requester import Requester from jenkinsapi.custom_exceptions import UnknownJob log = logging.getLogger(__name__) class Jenkins(JenkinsBase): """ Represents a jenkins environment. """ def __init__( self, baseurl, username=None, password=None, requester=None, lazy=False, ssl_verify=True): """ :param baseurl: baseurl for jenkins instance including port, str :param username: username for jenkins auth, str :param password: password for jenkins auth, str :return: a Jenkins obj """ self.username = username self.password = password self.requester = requester or Requester( username, password, baseurl=baseurl, ssl_verify=ssl_verify) self.lazy = lazy JenkinsBase.__init__(self, baseurl, poll=not lazy) def _poll_if_needed(self): if self.lazy and self._data is None: self.poll() def _clone(self): return Jenkins(self.baseurl, username=self.username, password=self.password, requester=self.requester) def base_server_url(self): if config.JENKINS_API in self.baseurl: return self.baseurl[:-(len(config.JENKINS_API))] else: return self.baseurl def validate_fingerprint(self, id_): obj_fingerprint = Fingerprint(self.baseurl, id_, jenkins_obj=self) obj_fingerprint.validate() log.info(msg="Jenkins says %s is valid" % id_) # def reload(self): # '''Try and reload the configuration from disk''' # self.requester.get_url("%(baseurl)s/reload" % self.__dict__) def get_artifact_data(self, id_): obj_fingerprint = Fingerprint(self.baseurl, id_, jenkins_obj=self) obj_fingerprint.validate() return obj_fingerprint.get_info() def validate_fingerprint_for_build(self, digest, filename, job, build): obj_fingerprint = Fingerprint(self.baseurl, digest, jenkins_obj=self) return obj_fingerprint.validate_for_build(filename, job, build) def get_jenkins_obj(self): return self def get_jenkins_obj_from_url(self, url): return Jenkins(url, self.username, self.password, self.requester) def get_create_url(self): # This only ever needs to work on the base object return '%s/createItem' % self.baseurl def get_nodes_url(self): # This only ever needs to work on the base object return '%s/computer' % self.baseurl @property def jobs(self): return Jobs(self) def get_jobs(self): """ Fetch all the build-names on this Jenkins server. """ jobs = self.poll(tree='jobs[name,url,color]')['jobs'] for info in jobs: yield info["name"], \ Job(info["url"], info["name"], jenkins_obj=self) def get_jobs_info(self): """ Get the jobs information :return url, name """ jobs = self.poll(tree='jobs[name,url,color]')['jobs'] for info in jobs: yield info["url"], info["name"] def get_job(self, jobname): """ Get a job by name :param jobname: name of the job, str :return: Job obj """ return self.jobs[jobname] def has_job(self, jobname): """ Does a job by the name specified exist :param jobname: string :return: boolean """ return jobname in self.jobs def create_job(self, jobname, xml): """ Create a job alternatively you can create job using Jobs object: self.jobs['job_name'] = config :param jobname: name of new job, str :param config: configuration of new job, xml :return: new Job obj """ return self.jobs.create(jobname, xml) def copy_job(self, jobname, newjobname): return self.jobs.copy(jobname, newjobname) def build_job(self, jobname, params=None): """ Invoke a build by job name :param jobname: name of exist job, str :param params: the job params, dict :return: none """ self[jobname].invoke(build_params=params or {}) def delete_job(self, jobname): """ Delete a job by name :param jobname: name of a exist job, str :return: new jenkins_obj """ del self.jobs[jobname] def rename_job(self, jobname, newjobname): """ Rename a job :param jobname: name of a exist job, str :param newjobname: name of new job, str :return: new Job obj """ return self.jobs.rename(jobname, newjobname) def iterkeys(self): jobs = self.poll(tree='jobs[name,color,url]')['jobs'] for info in 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()] # This is a function alias we retain for historical compatibility get_jobs_list = keys def __str__(self): return "Jenkins server at %s" % self.baseurl @property def views(self): return Views(self) def get_view_by_url(self, str_view_url): # for nested view str_view_name = str_view_url.split('/view/')[-1].replace('/', '') return View(str_view_url, str_view_name, jenkins_obj=self) def delete_view_by_url(self, str_url): url = "%s/doDelete" % str_url self.requester.post_and_confirm_status(url, data='') self.poll() return self def __getitem__(self, jobname): """ Get a job by name :param jobname: name of job, str :return: Job obj """ # We have to ask for 'color' here because folder resolution # relies on it jobs = self.poll(tree='jobs[name,url,color]')['jobs'] for info in jobs: if info["name"] == jobname: return Job(info["url"], info["name"], jenkins_obj=self) raise UnknownJob(jobname) def __len__(self): jobs = self.poll(tree='jobs[name,color,url]')['jobs'] return len(jobs) def __contains__(self, jobname): """ Does a job by the name specified exist :param jobname: string :return: boolean """ return jobname in self.jobs def __delitem__(self, job_name): del self.jobs[job_name] def get_node(self, nodename): """Get a node object for a specific node""" return self.get_nodes()[nodename] def get_node_url(self, nodename=""): """Return the url for nodes""" url = urlparse.urljoin( self.base_server_url(), 'computer/%s' % urlquote(nodename)) return url def get_queue_url(self): url = "%s/%s" % (self.base_server_url(), 'queue') return url def get_queue(self): queue_url = self.get_queue_url() return Queue(queue_url, self) def get_nodes(self): url = self.get_nodes_url() return Nodes(url, self) @property def nodes(self): return self.get_nodes() def has_node(self, nodename): """ Does a node by the name specified exist :param nodename: string, hostname :return: boolean """ self.poll() return nodename in self.nodes 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" del self.nodes[nodename] def create_node(self, name, num_executors=2, node_description=None, remote_fs='/var/lib/jenkins', labels=None, exclusive=False): """ Create a new JNLP slave node by name. To create SSH node, please see description in Node class :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_dict = { 'num_executors': num_executors, 'node_description': node_description, 'remote_fs': remote_fs, 'labels': labels, 'exclusive': exclusive } return self.nodes.create_node(name, node_dict) def get_plugins_url(self, depth): # This only ever needs to work on the base object return '%s/pluginManager/api/python?depth=%i' % (self.baseurl, depth) def install_plugin(self, plugin): plugin = str(plugin) if '@' not in plugin or len(plugin.split('@')) != 2: usage_err = ('argument must be a string like ' '"plugin-name@version", not "{0}"') usage_err = usage_err.format(plugin) raise ValueError(usage_err) payload = ' ' payload = payload.format(plugin) url = '%s/pluginManager/installNecessaryPlugins' % (self.baseurl,) return self.requester.post_xml_and_confirm_status( url, data=payload) def install_plugins(self, plugin_list, restart=False): for plugin in plugin_list: self.install_plugin(plugin) if restart: self.safe_restart() def safe_restart(self): """ restarts jenkins when no jobs are running """ # NB: unlike other methods, the value of resp.status_code # here can be 503 even when everything is normal url = '%s/safeRestart' % (self.baseurl,) valid = self.requester.VALID_STATUS_CODES + [503] resp = self.requester.post_and_confirm_status(url, data='', valid=valid) return resp def get_plugins(self, depth=1): url = self.get_plugins_url(depth=depth) return Plugins(url, self) 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) def get_master_data(self): url = '%s/computer/api/python' % self.baseurl return self.get_data(url) @property def version(self): """ Return version number of Jenkins """ response = self.requester.get_and_confirm_status(self.baseurl) version_key = 'X-Jenkins' return response.headers.get(version_key, '0.0') def get_credentials(self): """ Return credentials """ url = '%s/credential-store/domain/_/' % self.baseurl return Credentials(url, self) @property def credentials(self): return self.get_credentials() jenkinsapi-0.2.29/jenkinsapi/views.py0000664000175000017500000000640712616220252017170 0ustar salsal00000000000000""" Module for jenkinsapi Views """ import logging import json from jenkinsapi.view import View log = logging.getLogger(__name__) class Views(object): """ An abstraction on a Jenkins object's views """ LIST_VIEW = 'hudson.model.ListView' NESTED_VIEW = 'hudson.plugins.nested_view.NestedView' MY_VIEW = 'hudson.model.MyView' DASHBOARD_VIEW = 'hudson.plugins.view.dashboard.Dashboard' PIPELINE_VIEW = ('au.com.centrumsystems.hudson.' 'plugin.buildpipeline.BuildPipelineView') def __init__(self, jenkins): self.jenkins = jenkins def __len__(self): return len(self.keys()) def __delitem__(self, view_name): if view_name == 'All': raise ValueError('Cannot delete this view: %s' % view_name) if view_name in self: self[view_name].delete() self.jenkins.poll() def __setitem__(self, view_name, job_names_list): new_view = self.create(view_name) if isinstance(job_names_list, str): job_names_list = [job_names_list] for job_name in job_names_list: if not new_view.add_job(job_name): # Something wrong - delete view del self[new_view] raise TypeError('Job %s does not exist in Jenkins' % job_name) def __getitem__(self, view_name): self.jenkins.poll() for row in self.jenkins._data.get('views', []): if row['name'] == view_name: return View(row['url'], row['name'], self.jenkins) def iteritems(self): """ Get the names & objects for all views """ self.jenkins.poll() for row in self.jenkins._data.get('views', []): name = row['name'] url = row['url'] yield name, View(url, name, self.jenkins) def __contains__(self, view_name): """ True if view_name is the name of a defined view """ return view_name in self.keys() def iterkeys(self): """ Get the names of all available views """ self.jenkins.poll() for row in self.jenkins._data.get('views', []): yield row['name'] def keys(self): """ Return a list of the names of all views """ return list(self.iterkeys()) def create(self, view_name, view_type=LIST_VIEW): """ Create a view :param view_name: name of new view, str :param person: Person name (to create personal view), str :return: new View obj or None if view was not created """ log.info(msg='Creating "%s" view "%s"' % (view_type, view_name)) if view_name in self: log.warn(msg='View "%s" already exists' % view_name) return self[view_name] url = '%s/createView' % self.jenkins.baseurl headers = {'Content-Type': 'application/x-www-form-urlencoded'} data = { "name": view_name, "mode": view_type, "Submit": "OK", "json": json.dumps({"name": view_name, "mode": view_type}) } self.jenkins.requester.post_and_confirm_status( url, data=data, headers=headers) self.jenkins.poll() return self[view_name] jenkinsapi-0.2.29/jenkinsapi/executor.py0000664000175000017500000000330112616220252017657 0ustar salsal00000000000000""" 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""" return self.poll(tree='progress')['progress'] def get_number(self): """ Get Executor number. """ return self.poll(tree='number')['number'] def is_idle(self): """ Returns Boolean: whether Executor is idle or not. """ return self.poll(tree='idle')['idle'] def likely_stuck(self): """ Returns Boolean: whether Executor is likely stuck or not. """ return self.poll(tree='likelyStuck')['likelyStuck'] def get_current_executable(self): """ Returns the current Queue.Task this executor is running. """ return self.poll(tree='currentExecutable')['currentExecutable'] jenkinsapi-0.2.29/jenkinsapi/config.py0000664000175000017500000000011712616220252017270 0ustar salsal00000000000000""" Jenkins configuration """ JENKINS_API = r"api/python" LOAD_TIMEOUT = 30 jenkinsapi-0.2.29/jenkinsapi/credential.py0000664000175000017500000001433212616220252020141 0ustar salsal00000000000000""" Module for jenkinsapi Credential class """ import logging log = logging.getLogger(__name__) class Credential(object): """ Base abstract class for credentials Credentials returned from Jenkins don't hold any sensitive information, so there is nothing useful can be done with existing credentials besides attaching them to Nodes or other objects. You can create concrete Credential instance: UsernamePasswordCredential or SSHKeyCredential by passing credential's description and credential dict. Each class expects specific credential dict, see below. """ # pylint: disable=unused-argument def __init__(self, cred_dict): """ Create credential :param str description: as Jenkins doesn't allow human friendly names for credentials and makes "displayName" itself, there is no way to find credential later, this field is used to distinguish between credentials :param dict cred_dict: dict containing credential information """ self.credential_id = cred_dict.get('credential_id', '') self.description = cred_dict['description'] self.fullname = cred_dict.get('fullName', '') self.displayname = cred_dict.get('displayName', '') def __str__(self): return self.description def get_attributes(self): pass class UsernamePasswordCredential(Credential): """ Username and password credential Constructor expects following dict: { 'credential_id': str, Automatically set by jenkinsapi 'displayName': str, Automatically set by Jenkins 'fullName': str, Automatically set by Jenkins 'typeName': str, Automatically set by Jenkins 'description': str, 'userName': str, 'password': str } When creating credential via jenkinsapi automatic fields not need to be in dict """ def __init__(self, cred_dict): super(UsernamePasswordCredential, self).__init__(cred_dict) if 'typeName' in cred_dict: username = cred_dict['displayName'].split('/')[0] else: username = cred_dict['userName'] self.username = username self.password = cred_dict.get('password', None) def get_attributes(self): """ Used by Credentials object to create credential in Jenkins """ c_class = ( 'com.cloudbees.plugins.credentials.impl.' 'UsernamePasswordCredentialsImpl' ) c_id = '' if self.credential_id is None else self.credential_id return { 'stapler-class': c_class, 'Submit': 'OK', 'json': { '': '1', 'credentials': { 'stapler-class': c_class, 'id': c_id, 'username': self.username, 'password': self.password, 'description': self.description } } } class SSHKeyCredential(Credential): """ SSH key credential Constructr expects following dict: { 'credential_id': str, Automatically set by jenkinsapi 'displayName': str, Automatically set by Jenkins 'fullName': str, Automatically set by Jenkins 'typeName': str, Automatically set by Jenkins 'description': str, 'userName': str, 'passphrase': str, SSH key passphrase, 'private_key': str Private SSH key } private_key value is parsed to find type of credential to create: private_key starts with - the value is private key itself private_key starts with / the value is a path to key private_key starts with ~ the value is a key from ~/.ssh When creating credential via jenkinsapi automatic fields not need to be in dict """ def __init__(self, cred_dict): super(SSHKeyCredential, self).__init__(cred_dict) if 'typeName' in cred_dict: username = cred_dict['displayName'].split(' ')[0] else: username = cred_dict['userName'] self.username = username self.passphrase = cred_dict.get('passphrase', '') if 'private_key' not in cred_dict or cred_dict['private_key'] is None: self.key_type = -1 self.key_value = None elif cred_dict['private_key'].startswith('-'): self.key_type = 0 self.key_value = cred_dict['private_key'] elif cred_dict['private_key'].startswith('/'): self.key_type = 1 self.key_value = cred_dict['private_key'] elif cred_dict['private_key'].startswith('~'): self.key_type = 2 self.key_value = cred_dict['private_key'] else: raise ValueError('Invalid private_key value') def get_attributes(self): """ Used by Credentials object to create credential in Jenkins """ base_class = ( 'com.cloudbees.jenkins.plugins.sshcredentials.' 'impl.BasicSSHUserPrivateKey' ) if self.key_type == 0: c_class = base_class + '$DirectEntryPrivateKeySource' elif self.key_type == 1: c_class = base_class + '$FileOnMasterPrivateKeySource' elif self.key_type == 2: c_class = base_class + '$UsersPrivateKeySource' else: c_class = None attrs = { 'value': self.key_type, 'privateKey': self.key_value, 'stapler-class': c_class } c_id = '' if self.credential_id is None else self.credential_id return { 'stapler-class': c_class, 'Submit': 'OK', 'json': { '': '1', 'credentials': { 'scope': 'GLOBAL', 'id': c_id, 'username': self.username, 'description': self.description, 'privateKeySource': attrs, 'passphrase': self.passphrase, 'stapler-class': base_class, '$class': base_class } } } jenkinsapi-0.2.29/jenkinsapi/api.py0000664000175000017500000002172612616220252016605 0ustar salsal00000000000000""" 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 they have simple string arguments. """ import os import time import logging try: from urllib import parse as urlparse except ImportError: # Python3 from urllib2 import urlparse from jenkinsapi import constants from jenkinsapi.jenkins import Jenkins from jenkinsapi.artifact import Artifact from jenkinsapi.custom_exceptions import ArtifactsMissing, TimeOut, BadURL log = logging.getLogger(__name__) def get_latest_test_results(jenkinsurl, jobname, username=None, password=None, ssl_verify=True): """ A convenience function to fetch down the very latest test results from a jenkins job. """ latestbuild = get_latest_build(jenkinsurl, jobname, username=username, password=password, ssl_verify=ssl_verify) res = latestbuild.get_resultset() return res def get_latest_build(jenkinsurl, jobname, username=None, password=None, ssl_verify=True): """ A convenience function to fetch down the very latest test results from a jenkins job. """ jenkinsci = Jenkins(jenkinsurl, username=username, password=password, ssl_verify=ssl_verify) job = jenkinsci[jobname] return job.get_last_build() def get_latest_complete_build(jenkinsurl, jobname, username=None, password=None, ssl_verify=True): """ A convenience function to fetch down the very latest test results from a jenkins job. """ jenkinsci = Jenkins(jenkinsurl, username=username, password=password, ssl_verify=ssl_verify) job = jenkinsci[jobname] return job.get_last_completed_build() def get_build(jenkinsurl, jobname, build_no, username=None, password=None, ssl_verify=True): """ A convenience function to fetch down the test results from a jenkins job by build number. """ jenkinsci = Jenkins(jenkinsurl, username=username, password=password, ssl_verify=ssl_verify) job = jenkinsci[jobname] return job.get_build(build_no) def get_artifacts(jenkinsurl, jobid=None, build_no=None, username=None, password=None, ssl_verify=True): """ Find all the artifacts for the latest build of a job. """ jenkinsci = Jenkins(jenkinsurl, username=username, password=password, ssl_verify=ssl_verify) job = jenkinsci[jobid] if build_no: build = job.get_build(build_no) else: build = job.get_last_good_build() artifacts = build.get_artifact_dict() log.info(msg="Found %i artifacts in '%s'" % (len(artifacts.keys()), build_no)) return artifacts def search_artifacts(jenkinsurl, jobid, artifact_ids=None, username=None, password=None, ssl_verify=True): """ Search the entire history of a jenkins job for a list of artifact names. If same_build is true then ensure that all artifacts come from the same build of the job """ if len(artifact_ids) == 0 or artifact_ids is None: return [] jenkinsci = Jenkins(jenkinsurl, username=username, password=password, ssl_verify=ssl_verify) job = jenkinsci[jobid] build_ids = job.get_build_ids() for build_id in build_ids: build = job.get_build(build_id) artifacts = build.get_artifact_dict() if set(artifact_ids).issubset(set(artifacts.keys())): return dict((a, artifacts[a]) for a in artifact_ids) missing_artifacts = set(artifact_ids) - set(artifacts.keys()) log.debug(msg="Artifacts %s missing from %s #%i" % (", ".join(missing_artifacts), jobid, build_id)) # noinspection PyUnboundLocalVariable raise ArtifactsMissing(missing_artifacts) def grab_artifact(jenkinsurl, jobid, artifactid, targetdir, username=None, password=None, strict_validation=False, ssl_verify=True): """ Convenience method to find the latest good version of an artifact and save it to a target directory. Directory is made automatically if not exists. """ artifacts = get_artifacts(jenkinsurl, jobid, username=username, password=password, ssl_verify=ssl_verify) artifact = artifacts[artifactid] if not os.path.exists(targetdir): os.makedirs(targetdir) artifact.save_to_dir(targetdir, strict_validation) def block_until_complete(jenkinsurl, jobs, maxwait=12000, interval=30, raise_on_timeout=True, username=None, password=None, ssl_verify=True): """ Wait until all of the jobs in the list are complete. """ assert maxwait > 0 assert maxwait > interval assert interval > 0 obj_jenkins = Jenkins(jenkinsurl, username=username, password=password, ssl_verify=ssl_verify) obj_jobs = [obj_jenkins[jid] for jid in jobs] for time_left in range(maxwait, 0, -interval): still_running = [j for j in obj_jobs if j.is_queued_or_running()] if not still_running: return str_still_running = ", ".join('"%s"' % str(a) for a in still_running) log.warn(msg="Waiting for jobs %s to complete. Will wait another %is" % (str_still_running, time_left)) time.sleep(interval) if raise_on_timeout: # noinspection PyUnboundLocalVariable raise TimeOut("Waited too long for these jobs to complete: %s" % str_still_running) def get_view_from_url(url, username=None, password=None, ssl_verify=True): """ Factory method """ matched = constants.RE_SPLIT_VIEW_URL.search(url) if not matched: raise BadURL("Cannot parse URL %s" % url) jenkinsurl, view_name = matched.groups() jenkinsci = Jenkins(jenkinsurl, username=username, password=password, ssl_verify=ssl_verify) return jenkinsci.views[view_name] def get_nested_view_from_url(url, username=None, password=None, ssl_verify=True): """ Returns View based on provided URL. Convenient for nested views. """ matched = constants.RE_SPLIT_VIEW_URL.search(url) if not matched: raise BadURL("Cannot parse URL %s" % url) jenkinsci = Jenkins(matched.group(0), username=username, password=password, ssl_verify=ssl_verify) return jenkinsci.get_view_by_url(url) def install_artifacts(artifacts, dirstruct, installdir, basestaticurl, strict_validation=False): """ Install the artifacts. """ assert basestaticurl.endswith("/"), "Basestaticurl should end with /" installed = [] for reldir, artifactnames in dirstruct.items(): destdir = os.path.join(installdir, reldir) if not os.path.exists(destdir): log.warn(msg="Making install directory %s" % destdir) os.makedirs(destdir) else: assert os.path.isdir(destdir) for artifactname in artifactnames: destpath = os.path.abspath(os.path.join(destdir, artifactname)) if artifactname in artifacts.keys(): # The artifact must be loaded from jenkins theartifact = artifacts[artifactname] else: # It's probably a static file, # we can get it from the static collection staticurl = urlparse.urljoin(basestaticurl, artifactname) theartifact = Artifact(artifactname, staticurl, None) theartifact.save(destpath, strict_validation) installed.append(destpath) return installed def search_artifact_by_regexp(jenkinsurl, jobid, artifactRegExp, username=None, password=None, ssl_verify=True): ''' Search the entire history of a hudson job for a build which has an artifact whose name matches a supplied regular expression. Return only that artifact. @param jenkinsurl: The base URL of the jenkins server @param jobid: The name of the job we are to search through @param artifactRegExp: A compiled regular expression object (not a re-string) @param username: Jenkins login user name, optional @param password: Jenkins login password, optional ''' job = Jenkins(jenkinsurl, username=username, password=password, ssl_verify=ssl_verify) j = job[jobid] build_ids = j.get_build_ids() for build_id in build_ids: build = j.get_build(build_id) artifacts = build.get_artifact_dict() try: it = artifacts.iteritems() except AttributeError: # Python3 it = artifacts.items() for name, art in it: md_match = artifactRegExp.search(name) if md_match: return art raise ArtifactsMissing() jenkinsapi-0.2.29/jenkinsapi/fingerprint.py0000664000175000017500000000745112616220252020362 0ustar salsal00000000000000""" Module for jenkinsapi Fingerprint """ from jenkinsapi.jenkinsbase import JenkinsBase from jenkinsapi.custom_exceptions import ArtifactBroken import re import requests import logging log = logging.getLogger(__name__) class Fingerprint(JenkinsBase): """ Represents a jenkins fingerprint on a single artifact file ?? """ RE_MD5 = re.compile("^([0-9a-z]{32})$") def __init__(self, baseurl, id_, jenkins_obj): logging.basicConfig() self.jenkins_obj = jenkins_obj assert self.RE_MD5.search(id_), ("%s does not look like " "a valid id" % id_) url = "%s/fingerprint/%s/" % (baseurl, id_) JenkinsBase.__init__(self, url, poll=False) self.id_ = id_ self.unknown = False # Previously uninitialized in ctor def get_jenkins_obj(self): return self.jenkins_obj def __str__(self): return self.id_ def valid(self): """ Return True / False if valid. If returns True, self.unknown is set to either True or False, and can be checked if we have positive validity (fingerprint known at server) or negative validity (fingerprint not known at server, but not really an error). """ try: self.poll() self.unknown = False except requests.exceptions.HTTPError as err: # We can't really say anything about the validity of # fingerprints not found -- but the artifact can still # exist, so it is not possible to definitely say they are # valid or not. # The response object is of type: requests.models.Response # extract the status code from it response_obj = err.response if response_obj.status_code == 404: self.unknown = True return True else: return False return True def validate_for_build(self, filename, job, build): if not self.valid(): log.info("Unknown to jenkins.") return False if self.unknown: # not request error, but unknown to jenkins return True if not self._data["original"] is None: if self._data["original"]["name"] == job: if self._data["original"]["number"] == build: return True if self._data["fileName"] != filename: log.info( msg="Filename from jenkins (%s) did not match provided (%s)" % (self._data["fileName"], filename)) return False for usage_item in self._data["usage"]: if usage_item["name"] == job: for range_ in usage_item["ranges"]["ranges"]: if range_["start"] <= build <= range_["end"]: msg = ("This artifact was generated by %s " "between build %i and %i" % (job, range_["start"], range_["end"])) log.info(msg=msg) return True return False def validate(self): try: assert self.valid() except AssertionError: raise ArtifactBroken( "Artifact %s seems to be broken, check %s" % (self.id_, self.baseurl)) except requests.exceptions.HTTPError: raise ArtifactBroken( "Unable to validate artifact id %s using %s" % (self.id_, self.baseurl)) return True def get_info(self): """ Returns a tuple of build-name, build# and artifact filename for a good build. """ self.poll() return self._data["original"]["name"], \ self._data["original"]["number"], self._data["fileName"] jenkinsapi-0.2.29/jenkinsapi/result.py0000664000175000017500000000115212616220252017341 0ustar salsal00000000000000""" Module for jenkinsapi Result """ class Result(object): """ Result class """ def __init__(self, **kwargs): self.__dict__.update(kwargs) def __str__(self): return "%s %s %s" % (self.className, self.name, self.status) def __repr__(self): module_name = self.__class__.__module__ class_name = self.__class__.__name__ self_str = str(self) return "<%s.%s %s>" % (module_name, class_name, self_str) def identifier(self): """ Calculate an ID for this object. """ return "%s.%s" % (self.className, self.name) jenkinsapi-0.2.29/jenkinsapi/custom_exceptions.py0000664000175000017500000000470612616220252021606 0ustar salsal00000000000000"""Module for custom_exceptions. Where possible we try to throw exceptions with non-generic, meaningful names. """ class JenkinsAPIException(Exception): """Base class for all errors """ pass class NotFound(JenkinsAPIException): """Resource cannot be found """ pass class ArtifactsMissing(NotFound): """Cannot find a build with all of the required artifacts. """ pass class UnknownJob(KeyError, NotFound): """Jenkins does not recognize the job requested. """ pass class UnknownView(KeyError, NotFound): """Jenkins does not recognize the view requested. """ pass class UnknownNode(KeyError, NotFound): """Jenkins does not recognize the node requested. """ pass class UnknownQueueItem(KeyError, NotFound): """Jenkins does not recognize the requested queue item """ pass class UnknownPlugin(KeyError, NotFound): """Jenkins does not recognize the plugin requested. """ pass class NoBuildData(NotFound): """A job has no build data. """ pass class NotBuiltYet(NotFound): """A job has no build data. """ pass class ArtifactBroken(JenkinsAPIException): """An artifact is broken, wrong """ pass class TimeOut(JenkinsAPIException): """Some jobs have taken too long to complete. """ pass class NoResults(JenkinsAPIException): """A build did not publish any results. """ pass class FailedNoResults(NoResults): """A build did not publish any results because it failed """ pass class BadURL(ValueError, JenkinsAPIException): """A URL appears to be broken """ pass class NotAuthorized(JenkinsAPIException): """Not Authorized to access resource""" # Usually thrown when we get a 403 returned pass class NotSupportSCM(JenkinsAPIException): """ It's a SCM that does not supported by current version of jenkinsapi """ pass class NotConfiguredSCM(JenkinsAPIException): """It's a job that doesn't have configured SCM """ pass class NotInQueue(JenkinsAPIException): """It's a job that is not in the queue """ pass class PostRequired(JenkinsAPIException): """Method requires POST and not GET """ pass class BadParams(JenkinsAPIException): """Invocation was given bad or inappropriate params """ pass class AlreadyExists(JenkinsAPIException): """ Method requires POST and not GET """ pass jenkinsapi-0.2.29/jenkinsapi/result_set.py0000664000175000017500000000273312616220252020222 0ustar salsal00000000000000""" Module for jenkinsapi ResultSet """ from jenkinsapi.jenkinsbase import JenkinsBase from jenkinsapi.result import Result class ResultSet(JenkinsBase): """ Represents a result from a completed Jenkins run. """ def __init__(self, url, build): """ Init a resultset :param url: url for a build, str :param build: build obj """ self.build = build JenkinsBase.__init__(self, url) def get_jenkins_obj(self): return self.build.job.get_jenkins_obj() def __str__(self): return "Test Result for %s" % str(self.build) @property def name(self): return str(self) def keys(self): return [a[0] for a in self.iteritems()] def items(self): return [a for a in self.iteritems()] def iteritems(self): for suite in self._data.get("suites", []): for case in suite["cases"]: result = Result(**case) yield result.identifier(), result for report_set in self._data.get("childReports", []): if report_set["result"]: for suite in report_set["result"]["suites"]: for case in suite["cases"]: result = Result(**case) yield result.identifier(), result def __len__(self): return len(self.items()) def __getitem__(self, key): self_as_dict = dict(self.iteritems()) return self_as_dict[key] jenkinsapi-0.2.29/setup.cfg0000664000175000017500000000040212616521664015147 0ustar salsal00000000000000[build_sphinx] source-dir = doc/source build-dir = doc/build all_files = 1 [upload_sphinx] upload-dir = doc/build/html [nosetests] detailed-errors = 1 with-coverage = 1 cover-package = jenkinsapi [egg_info] tag_date = 0 tag_svn_revision = 0 tag_build =