├── .github ├── ISSUE_TEMPLATE.md ├── dependabot.yml ├── stale.yml └── workflows │ ├── build-docs.yml │ ├── python-package.yml │ └── release.yml ├── .gitignore ├── .pre-commit-config.yaml ├── CHANGELOG.md ├── LICENSE ├── Makefile ├── README.rst ├── TODO ├── bin └── pipelint ├── doc ├── .gitignore ├── CONTRIBUTING.md ├── Makefile ├── _static │ ├── jenkins.png │ └── tmp.txt ├── conf.py ├── getting_started.rst ├── index.rst ├── jenkins.png ├── module_reference.rst ├── project_info.rst ├── readme_link.rst ├── ssl_certificate_verification └── submodules │ ├── api.rst │ ├── artifact.rst │ ├── build.rst │ ├── command_line.rst │ ├── credentials.rst │ ├── custom_exceptions.rst │ ├── executors.rst │ ├── fingerprint.rst │ ├── jenkins.rst │ ├── jenkinsbase.rst │ ├── jobs.rst │ ├── label.rst │ ├── mutable_jenkins.rst │ ├── nodes.rst │ ├── plugins.rst │ ├── queue.rst │ ├── results.rst │ ├── utils.rst │ └── views.rst ├── examples ├── __init__.py ├── addjob.xml ├── how_to │ ├── add_command.py │ ├── create_a_job.py │ ├── create_credentials.py │ ├── create_nested_views.py │ ├── create_slave.py │ ├── create_views.py │ ├── delete_all_the_nodes_except_master.py │ ├── get_config.py │ ├── get_plugin_information.py │ ├── get_version_info_from_last_good_build.py │ ├── query_a_build.py │ ├── readme.rst │ ├── search_artifact_by_regexp.py │ ├── search_artifacts.py │ ├── start_parameterized_build.py │ └── use_crumbs.py └── low_level │ ├── copy_a_job.py │ ├── create_a_view_low_level.py │ ├── example_param_build.py │ ├── login_with_auth.py │ ├── post_watcher.py │ └── readme.rst ├── jenkinsapi ├── .gitignore ├── __init__.py ├── api.py ├── artifact.py ├── build.py ├── command_line │ ├── __init__.py │ ├── jenkins_invoke.py │ └── jenkinsapi_version.py ├── config.py ├── constants.py ├── credential.py ├── credentials.py ├── custom_exceptions.py ├── executor.py ├── executors.py ├── fingerprint.py ├── jenkins.py ├── jenkinsbase.py ├── job.py ├── jobs.py ├── label.py ├── mutable_jenkins_thing.py ├── node.py ├── nodes.py ├── plugin.py ├── plugins.py ├── queue.py ├── result.py ├── result_set.py ├── utils │ ├── __init__.py │ ├── crumb_requester.py │ ├── jenkins_launcher.py │ ├── jsonp_to_json.py │ ├── krb_requester.py │ ├── manifest.py │ ├── requester.py │ └── simple_post_logger.py ├── view.py └── views.py ├── jenkinsapi_tests ├── __init__.py ├── conftest.py ├── systests │ ├── __init__.py │ ├── config.xml │ ├── conftest.py │ ├── get-jenkins-war.sh │ ├── jenkins_home.tar.gz │ ├── job_configs.py │ ├── test_authentication.py │ ├── test_credentials.py │ ├── test_crumbs_requester.py │ ├── test_downstream_upstream.py │ ├── test_env_vars.py │ ├── test_executors.py │ ├── test_generate_new_api_token.py │ ├── test_invocation.py │ ├── test_jenkins.py │ ├── test_jenkins_artifacts.py │ ├── test_jenkins_matrix.py │ ├── test_nodes.py │ ├── test_parameterized_builds.py │ ├── test_plugins.py │ ├── test_queue.py │ ├── test_quiet_down.py │ ├── test_restart.py │ ├── test_safe_exit.py │ ├── test_scm.py │ ├── test_views.py │ └── view_configs.py ├── test_utils │ ├── __init__.py │ └── random_strings.py └── unittests │ ├── __init__.py │ ├── configs.py │ ├── test_artifact.py │ ├── test_build.py │ ├── test_build_scm_git.py │ ├── test_executors.py │ ├── test_fingerprint.py │ ├── test_jenkins.py │ ├── test_job.py │ ├── test_job_folders.py │ ├── test_job_get_all_builds.py │ ├── test_job_scm_hg.py │ ├── test_label.py │ ├── test_misc.py │ ├── test_node.py │ ├── test_nodes.py │ ├── test_plugins.py │ ├── test_requester.py │ ├── test_result_set.py │ └── test_view.py ├── pylintrc └── pyproject.toml /.github/ISSUE_TEMPLATE.md: -------------------------------------------------------------------------------- 1 | ##### ISSUE TYPE 2 | 3 | - Bug Report 4 | - Feature Idea 5 | - How to... 6 | 7 | ##### Jenkinsapi VERSION 8 | 9 | ##### Jenkins VERSION 10 | 11 | ##### SUMMARY 12 | 13 | 14 | ##### EXPECTED RESULTS 15 | 16 | 17 | ##### ACTUAL RESULTS 18 | 19 | 20 | ##### USEFUL INFORMATION 21 | 25 | 26 | 27 | ``` 28 | 29 | ``` 30 | -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | # To get started with Dependabot version updates, you'll need to specify which 2 | # package ecosystems to update and where the package manifests are located. 3 | # Please see the documentation for all configuration options: 4 | # https://docs.github.com/code-security/dependabot/dependabot-version-updates/configuration-options-for-the-dependabot.yml-file 5 | 6 | version: 2 7 | updates: 8 | - package-ecosystem: "github-actions" # See documentation for possible values 9 | directory: "/" # Location of package manifests 10 | schedule: 11 | interval: "weekly" 12 | -------------------------------------------------------------------------------- /.github/stale.yml: -------------------------------------------------------------------------------- 1 | # Configuration for probot-stale - https://github.com/probot/stale 2 | 3 | # Number of days of inactivity before an Issue or Pull Request becomes stale 4 | daysUntilStale: 60 5 | 6 | # Number of days of inactivity before an Issue or Pull Request with the stale label is closed. 7 | # Set to false to disable. If disabled, issues still need to be closed manually, but will remain marked as stale. 8 | daysUntilClose: 7 9 | 10 | # Only issues or pull requests with all of these labels are check if stale. Defaults to `[]` (disabled) 11 | onlyLabels: [] 12 | 13 | # Issues or Pull Requests with these labels will never be considered stale. Set to `[]` to disable 14 | exemptLabels: 15 | - pinned 16 | - security 17 | - "[Status] Maybe Later" 18 | - "feature request" 19 | - "help wanted" 20 | - "improvement request" 21 | 22 | # Set to true to ignore issues in a project (defaults to false) 23 | exemptProjects: false 24 | 25 | # Set to true to ignore issues in a milestone (defaults to false) 26 | exemptMilestones: false 27 | 28 | # Set to true to ignore issues with an assignee (defaults to false) 29 | exemptAssignees: false 30 | 31 | # Label to use when marking as stale 32 | staleLabel: stale 33 | 34 | # Comment to post when marking as stale. Set to `false` to disable 35 | markComment: > 36 | This issue has been automatically marked as stale because it has not had 37 | recent activity. It will be closed if no further activity occurs. Thank you 38 | for your contributions. 39 | 40 | # Comment to post when removing the stale label. 41 | # unmarkComment: > 42 | # Your comment here. 43 | 44 | # Comment to post when closing a stale Issue or Pull Request. 45 | closeComment: > 46 | Closed due to inactivity 47 | 48 | # Limit the number of actions per hour, from 1-30. Default is 30 49 | limitPerRun: 30 50 | 51 | # Limit to only `issues` or `pulls` 52 | # only: issues 53 | 54 | # Optionally, specify configuration settings that are specific to just 'issues' or 'pulls': 55 | # pulls: 56 | # daysUntilStale: 30 57 | # markComment: > 58 | # This pull request has been automatically marked as stale because it has not had 59 | # recent activity. It will be closed if no further activity occurs. Thank you 60 | # for your contributions. 61 | 62 | # issues: 63 | # exemptLabels: 64 | # - confirmed 65 | -------------------------------------------------------------------------------- /.github/workflows/build-docs.yml: -------------------------------------------------------------------------------- 1 | name: Sphinx build 2 | 3 | on: # yamllint disable-line rule:truthy 4 | push: 5 | branches: 6 | - master 7 | workflow_dispatch: 8 | 9 | jobs: 10 | sphinx-build: 11 | runs-on: ubuntu-latest 12 | steps: 13 | - uses: actions/checkout@v4 14 | 15 | - name: Install uv 16 | uses: astral-sh/setup-uv@v6 17 | with: 18 | python-version: "3.10" 19 | 20 | - name: Install dependencies 21 | run: | 22 | sudo apt-get update; sudo apt-get install libkrb5-dev gcc 23 | 24 | - name: Build HTML 25 | run: make 26 | working-directory: ./doc 27 | 28 | - name: Upload artifacts 29 | uses: actions/upload-artifact@v4 30 | with: 31 | name: html-docs 32 | path: ./doc/html/ 33 | 34 | - name: Deploy Master Docs to GitHub Pages 35 | if: ${{ github.ref == 'refs/heads/master' }} 36 | uses: peaceiris/actions-gh-pages@v4 37 | with: 38 | github_token: ${{ secrets.GITHUB_TOKEN }} 39 | publish_dir: ./doc/html 40 | publish_branch: gh-pages 41 | keep_files: false 42 | -------------------------------------------------------------------------------- /.github/workflows/python-package.yml: -------------------------------------------------------------------------------- 1 | name: CI_TEST 2 | 3 | on: 4 | push: 5 | branches: [ "master" ] 6 | paths: 7 | - '**.py' 8 | - '**.yml' 9 | pull_request: 10 | branches: [ "master" ] 11 | workflow_dispatch: # allow manual run 12 | 13 | concurrency: 14 | group: ${{ github.workflow }}-${{ github.ref }} 15 | cancel-in-progress: true 16 | 17 | jobs: 18 | build: 19 | runs-on: ubuntu-latest 20 | strategy: 21 | fail-fast: false 22 | matrix: 23 | python-version: ["3.8", "3.9", "3.10", "3.11", "3.12", "3.13"] 24 | token: ["stable"] 25 | 26 | steps: 27 | - name: Harden Runner 28 | uses: step-security/harden-runner@v2.12.0 29 | with: 30 | egress-policy: audit 31 | allowed-endpoints: > 32 | azure.archive.ubuntu.com:80 33 | esm.ubuntu.com:443 34 | files.pythonhosted.org:443 35 | ftp-chi.osuosl.org:443 36 | ftp-nyc.osuosl.org:443 37 | get.jenkins.io:443 38 | github.com:443 39 | api.github.com:443 40 | int.api.stepsecurity.io:443 41 | mirror.xmission.com:443 42 | motd.ubuntu.com:443 43 | packages.microsoft.com:443 44 | ppa.launchpadcontent.net:443 45 | pypi.org:443 46 | updates.jenkins-ci.org:80 47 | updates.jenkins.io:443 48 | mirrors.updates.jenkins.io:443 49 | updates.jenkins.io:80 50 | 51 | - name: Checkout 52 | uses: actions/checkout@v4 53 | 54 | - name: Install uv 55 | uses: astral-sh/setup-uv@v6 56 | with: 57 | python-version: ${{ matrix.python-version }} 58 | 59 | - name: Install python 60 | run: uv python install 61 | 62 | - name: setup java 21 63 | uses: actions/setup-java@v4 64 | with: 65 | java-version: '21' 66 | distribution: 'temurin' 67 | 68 | - name: Install dependencies 69 | run: | 70 | sudo apt-get update; sudo apt-get install libkrb5-dev gcc 71 | 72 | - name: Lint with ruff 73 | run: | 74 | uv run ruff check jenkinsapi/ --output-format full 75 | 76 | - name: Test with pytest 77 | env: 78 | JENKINS_VERSION: ${{ matrix.token }} 79 | run: | 80 | uv run pytest -sv --cov=jenkinsapi --cov-report=term-missing --cov-report=xml jenkinsapi_tests 81 | -------------------------------------------------------------------------------- /.github/workflows/release.yml: -------------------------------------------------------------------------------- 1 | name: Release 2 | 3 | on: 4 | release: 5 | types: [published] 6 | 7 | jobs: 8 | build: 9 | runs-on: ubuntu-latest 10 | steps: 11 | - name: Checkout 12 | uses: actions/checkout@v4 13 | 14 | - name: Install uv 15 | uses: astral-sh/setup-uv@v6 16 | with: 17 | python-version: "3.13" 18 | 19 | - name: Install python 20 | run: uv python install 21 | 22 | - name: build 23 | run: uv build 24 | 25 | - name: Upload artifact 26 | uses: actions/upload-artifact@v4 27 | with: 28 | name: package 29 | path: dist/ 30 | retention-days: 7 31 | if-no-files-found: error 32 | 33 | pypi: 34 | needs: [build] 35 | runs-on: ubuntu-latest 36 | permissions: 37 | id-token: write 38 | environment: 39 | name: pypi-publishing 40 | url: https://pypi.org/project/jenkinsapi/ 41 | steps: 42 | - name: Download artifact 43 | uses: actions/download-artifact@v4 44 | with: 45 | name: package 46 | path: dist 47 | 48 | - name: Show tree 49 | run: tree 50 | 51 | - name: Publish 52 | uses: pypa/gh-action-pypi-publish@release/v1 53 | with: 54 | password: ${{ secrets.PYPI_API_TOKEN }} 55 | 56 | asset: 57 | needs: [build] 58 | runs-on: ubuntu-latest 59 | permissions: 60 | contents: write 61 | steps: 62 | - name: Download artifact 63 | uses: actions/download-artifact@v4 64 | with: 65 | name: package 66 | path: dist 67 | 68 | - name: Show tree 69 | run: tree 70 | 71 | - name: Add release asset 72 | uses: softprops/action-gh-release@da05d552573ad5aba039eaac05058a918a7bf631 73 | with: 74 | tag_name: ${{ github.event.release.tag_name }} 75 | fail_on_unmatched_files: true 76 | files: | 77 | dist/* 78 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .svn 2 | .project 3 | .pydevproject 4 | *.pyc 5 | *.egg-info 6 | /build 7 | /dist 8 | .settings 9 | *.DS_Store 10 | localinstance_files/ 11 | .coverage/ 12 | .coverage 13 | nosetests.xml 14 | coverage*/ 15 | .idea/ 16 | include/ 17 | lib/ 18 | *.egg 19 | .tox/* 20 | *.sw? 21 | /jenkinsapi_tests/systests/coverage.xml 22 | /.cache 23 | /AUTHORS 24 | /ChangeLog 25 | .pytest_cache 26 | .eggs 27 | coverage.xml 28 | .venv/ 29 | .vscode/ 30 | *.war 31 | venv/ 32 | tags 33 | .pytype/ 34 | uv.lock 35 | -------------------------------------------------------------------------------- /.pre-commit-config.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | repos: 3 | - repo: https://github.com/psf/black 4 | rev: 24.10.0 5 | hooks: 6 | - id: black 7 | args: 8 | - --line-length=79 9 | # these folders wont be formatted by black 10 | - --exclude="""\.git | 11 | \.__pycache__| 12 | \.hg| 13 | \.mypy_cache| 14 | \.tox| 15 | \.venv| 16 | _build| 17 | buck-out| 18 | build| 19 | dist""" 20 | language_version: python3 21 | - repo: https://github.com/pre-commit/pre-commit-hooks 22 | rev: v5.0.0 23 | hooks: 24 | - id: end-of-file-fixer 25 | - id: trailing-whitespace 26 | - id: mixed-line-ending 27 | - id: check-byte-order-marker 28 | - id: check-executables-have-shebangs 29 | - id: check-merge-conflict 30 | - id: check-symlinks 31 | - id: check-vcs-permalinks 32 | - id: debug-statements 33 | - id: check-yaml 34 | files: .*\.(yaml|yml)$ 35 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | 3 | ## 3.3.13 4 | 5 | - docs: fix doc and add autobuild 6 | - feature: add launch method to Node class (#849) 7 | - convert to using ruff for linting (#895) 8 | - update harden runner (#894) 9 | - change url to pull jenkins.war file (#893) 10 | - Make sure to filter warnings to errors (#868) 11 | - Revert "dependabot" (#888) 12 | - dependabot (#887) 13 | - makefile: update for uv compatibility (#886) 14 | - update pre-commit versions (#885) 15 | - remove deprecated setup (#884) 16 | - housekeeping: readme, license, pyproject.toml (#881) 17 | - update install commands to use uv (#882) 18 | - ci: update java to 21 (#883) 19 | - convert to uv and pyproject.toml (#879) 20 | - ci: update checkout to v4 (#880) 21 | - ci: remove apt-get update, install, and pip upgrade as unnecessary (#878) 22 | - docs: fix build (#876) 23 | - add support for 3.12 and 3.13 (#877) 24 | - Correctly specify python version (#858) 25 | - Merge pull request #874 from clintonsteiner/fixSeveralCiIssues 26 | - ci: add in missing plugins 27 | - ci: harden runner - set to audit for now 28 | - ci: add mirrors to harden whitelist 29 | - test_build: fix failing tests 30 | - ci: tests fail because jenkins requires java 17 - setup in github action 31 | - makefile: fix py.test according to pytest documentation 32 | - ci: fix issue causing runner to crash in setup 33 | - Merge pull request #853 from clintonsteiner/updateReadme 34 | - Readme Updates: Remove redundant words, move install and example to top 35 | - [pre-commit.ci] auto fixes from pre-commit.com hooks 36 | - fix: jenkinsapi crashing jenkins_is_unavaiable 37 | - [pre-commit.ci] auto fixes from pre-commit.com hooks 38 | - Support for using unicode characters in update_config 39 | - Decode content using the encoding specified in the response 40 | - Fixing plugin dependencies 41 | - Fix console scripts in pyproject.toml 42 | - Added Python typing hints to most of the code 43 | - Bump version 44 | - Move utils into jenkinsapi.utils module 45 | - Remove blank line 46 | - Update Build object docs 47 | - Add pyproject.toml 48 | - Rename Travis and Tox files 49 | - Flake8 fixes 50 | - [pre-commit.ci] auto fixes from pre-commit.com hooks 51 | - Do not call Jenkins site when testing plugins 52 | - Download Jenkins silently and improve plugin downloads 53 | - Fix failing tests and commented ones not working 54 | - Add flake8 to test requirements 55 | - Install test dependencies 56 | - Install krb and pytest-cov in workflow 57 | - Add Github action 58 | - Pyflake8 fixes 59 | - Add pre-commit.ci configuration 60 | - Add debug logs in Queue.block_until_building (#745) 61 | - Fixing retrying logic by introducing a max_retries param (#739) 62 | - Add quiet period parameter to job invoke (#758) 63 | - useCrumbs -> use_crumbs #755 (#756) 64 | - job.py: manage scm data for multibranch pipelines (#742) 65 | 66 | ## 0.3.11 - Oct 31, 2019 67 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2023 PyContribs 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | .PHONY: test lint tox coverage dist 2 | 3 | test: 4 | uv run pytest -sv jenkinsapi_tests 5 | 6 | lint: 7 | uv run pycodestyle 8 | uv run pylint jenkinsapi/*.py 9 | uv run flake8 jenkinsapi/ --count --select=E9,F63,F7,F82 --ignore F821,W503,W504 --show-source --statistics 10 | uv run flake8 jenkinsapi/ --count --exit-zero --max-complexity=10 --max-line-length=79 --statistics 11 | 12 | tox: 13 | tox 14 | 15 | dist: 16 | uv build 17 | 18 | coverage: 19 | uv run pytest -sv --cov=jenkinsapi --cov-report=term-missing --cov-report=xml jenkinsapi_tests 20 | -------------------------------------------------------------------------------- /README.rst: -------------------------------------------------------------------------------- 1 | Jenkinsapi 2 | ========== 3 | 4 | .. image:: https://badge.fury.io/py/jenkinsapi.png 5 | :target: http://badge.fury.io/py/jenkinsapi 6 | 7 | .. image:: https://codecov.io/gh/pycontribs/jenkinsapi/branch/master/graph/badge.svg 8 | :target: https://codecov.io/gh/pycontribs/jenkinsapi 9 | 10 | Installation 11 | ------------ 12 | 13 | .. code-block:: bash 14 | 15 | pip install jenkinsapi 16 | 17 | Important Links 18 | --------------- 19 | * `Documentation `__ 20 | * `Source Code `_ 21 | * `Support and bug-reports `_ 22 | * `Releases `_ 23 | 24 | 25 | About this library 26 | ------------------- 27 | 28 | Jenkins is the market leading continuous integration system. 29 | 30 | Jenkins (and its predecessor Hudson) are useful projects for automating common development tasks (e.g. unit-testing, production batches) - but they are somewhat Java-centric. 31 | 32 | Jenkinsapi makes scripting Jenkins tasks a breeze by wrapping the REST api into familiar python objects. 33 | 34 | Here is a list of some of the most commonly used functionality 35 | 36 | * Add, remove, and query Jenkins jobs 37 | * Control pipeline execution 38 | * Query the results of a completed build 39 | * Block until jobs are complete or run jobs asyncronously 40 | * Get objects representing the latest builds of a job 41 | * Artifact management 42 | * Search for artifacts by simple criteria 43 | * Install artifacts to custom-specified directory structures 44 | * Search for builds by source code revision 45 | * Create, destroy, and monitor 46 | * Build nodes (Webstart and SSH slaves) 47 | * Views (including nested views using NestedViews Jenkins plugin) 48 | * Credentials (username/password and ssh key) 49 | * Authentication support for username and password 50 | * Manage jenkins and plugin installation 51 | 52 | Full library capabilities are outlined in the `Documentation `__ 53 | 54 | Get details of jobs running on Jenkins server 55 | --------------------------------------------- 56 | 57 | .. code-block:: python 58 | 59 | """Get job details of each job that is running on the Jenkins instance""" 60 | def get_job_details(): 61 | # Refer Example #1 for definition of function 'get_server_instance' 62 | server = get_server_instance() 63 | for job_name, job_instance in server.get_jobs(): 64 | print 'Job Name:%s' % (job_instance.name) 65 | print 'Job Description:%s' % (job_instance.get_description()) 66 | print 'Is Job running:%s' % (job_instance.is_running()) 67 | print 'Is Job enabled:%s' % (job_instance.is_enabled()) 68 | 69 | Disable/Enable a Jenkins Job 70 | ---------------------------- 71 | 72 | .. code-block:: python 73 | 74 | def disable_job(): 75 | """Disable a Jenkins job""" 76 | # Refer Example #1 for definition of function 'get_server_instance' 77 | server = get_server_instance() 78 | job_name = 'nightly-build-job' 79 | if (server.has_job(job_name)): 80 | job_instance = server.get_job(job_name) 81 | job_instance.disable() 82 | print 'Name:%s,Is Job Enabled ?:%s' % (job_name,job_instance.is_enabled()) 83 | 84 | Use the call ``job_instance.enable()`` to enable a Jenkins Job. 85 | 86 | 87 | Known issues 88 | ------------ 89 | * Job deletion operations fail unless Cross-Site scripting protection is disabled. 90 | 91 | For other issues, please refer to the `support URL `_ 92 | 93 | Development 94 | ----------- 95 | 96 | * Make sure that you have Java_ installed. Jenkins will be automatically 97 | downloaded and started during tests. 98 | * Create virtual environment for development 99 | * Install package in development mode 100 | 101 | .. code-block:: bash 102 | 103 | uv sync 104 | 105 | * Make your changes, write tests and check your code 106 | 107 | .. code-block:: bash 108 | 109 | uv run pytest -sv 110 | 111 | Python versions 112 | --------------- 113 | 114 | The project has been tested against Python versions: 115 | 116 | * 3.8 - 3.13 117 | 118 | Jenkins versions 119 | ---------------- 120 | 121 | Project tested on both stable (LTS) and latest Jenkins versions. 122 | 123 | Project Contributors 124 | -------------------- 125 | 126 | * Aleksey Maksimov (ctpeko3a@gmail.com) 127 | * Salim Fadhley (sal@stodge.org) 128 | * Ramon van Alteren (ramon@vanalteren.nl) 129 | * Ruslan Lutsenko (ruslan.lutcenko@gmail.com) 130 | * Cleber J Santos (cleber@simplesconsultoria.com.br) 131 | * William Zhang (jollychang@douban.com) 132 | * Victor Garcia (bravejolie@gmail.com) 133 | * Bradley Harris (bradley@ninelb.com) 134 | * Kyle Rockman (kyle.rockman@mac.com) 135 | * Sascha Peilicke (saschpe@gmx.de) 136 | * David Johansen (david@makewhat.is) 137 | * Misha Behersky (bmwant@gmail.com) 138 | * Clinton Steiner (clintonsteiner@gmail.com) 139 | 140 | Please do not contact these contributors directly for support questions! Use the GitHub tracker instead. 141 | 142 | .. _Java: https://www.oracle.com/java/technologies/downloads/#java21 143 | -------------------------------------------------------------------------------- /TODO: -------------------------------------------------------------------------------- 1 | TODO: 2 | 3 | * Clean up the fingerprint code 4 | * Clean up the resultset and results code 5 | * Add support for Jenkins 2.x features 6 | * Improve speed for large Jenkins installations 7 | -------------------------------------------------------------------------------- /bin/pipelint: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | set -eo pipefail 3 | 4 | INPUT=/dev/stdin 5 | if [ -t 0 ]; then 6 | if [ "$#" -ne 1 ]; then 7 | echo "ERROR: Illegal number of parameters." 8 | echo "INFO: Use 'pipefail Jenkinsfile' or 'cat Jenkinsfile | pipefail'" 9 | exit 1 10 | fi 11 | INPUT=$1 12 | fi 13 | # put credentials inside ~/.netrc 14 | # define JENKINS_URL in your user profile 15 | JENKINS_URL=${JENKINS_URL:-http://localhost:8080} 16 | 17 | # failure to get crumb is ignored as this may be diabled on the server side 18 | CRUMB="-H `curl -nfs "$JENKINS_URL/crumbIssuer/api/xml?xpath=concat(//crumbRequestField,%22:%22,//crumb)"`" || CRUMB='' 19 | 20 | # The tee+grep trick assures that the exit code is 0 only if the server replied with "successfully validated" 21 | curl -nfs -X POST $CRUMB -F "jenkinsfile=<-" $JENKINS_URL/pipeline-model-converter/validate <$INPUT \ 22 | | tee >(cat 1>&2) | grep 'successfully validated' >/dev/null 23 | -------------------------------------------------------------------------------- /doc/.gitignore: -------------------------------------------------------------------------------- 1 | /**/html 2 | /docs_html.zip 3 | / html 4 | /build 5 | -------------------------------------------------------------------------------- /doc/CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | Contributing 2 | ============ 3 | 4 | The JenkinsAPI project welcomes contributions via GitHub. Please bear in mind the following guidelines when preparing your pull-request. 5 | 6 | Pre-commit 7 | ---------- 8 | Ensure pre-commit has been setup prior to comitting 9 | 10 | Build the Docs 11 | -------------- 12 | From within doc: make && python -m http.server --directory html 13 | 14 | Python compatibility 15 | -------------------- 16 | 17 | The project currently targets Python 3.8+. 18 | 19 | Code formatting 20 | --------------- 21 | 22 | The project follows strict PEP8 guidelines. Please use a tool like black to format your code before submitting a pull request. Tell black to use 79 characters per line (black -l 79). 23 | 24 | Test Driven Development 25 | ----------------------- 26 | 27 | Please do not submit pull requests without tests. That's really important. Our project is all about test-driven development. It would be embarrasing if our project failed because of a lack of tests! 28 | 29 | You might want to follow a typical test driven development cycle: http://en.wikipedia.org/wiki/Test-driven_development 30 | 31 | Put simply: Write your tests first and only implement features required to make your tests pass. Do not let your implementation get ahead of your tests. 32 | 33 | Features implemented without tests will be removed. Unmaintained features (which break because of changes in Jenkins) will also be removed. 34 | 35 | Check the CI status before comitting 36 | ------------------------------------ 37 | 38 | Project uses Github Actions, please verify that your branch passes all tests before making a pull request. 39 | 40 | Any problems? 41 | ------------- 42 | 43 | If you are stuck on something, please post to the issue tracker. Do not contact the developers directly. 44 | -------------------------------------------------------------------------------- /doc/Makefile: -------------------------------------------------------------------------------- 1 | SPHINXBUILD ?= uv run sphinx-build 2 | SOURCEDIR = . 3 | BUILDDIR = html 4 | 5 | .PHONY: all clean html 6 | 7 | all: clean html 8 | 9 | html: 10 | uv sync --group docs 11 | @$(SPHINXBUILD) -b html "$(SOURCEDIR)" "$(BUILDDIR)" 12 | 13 | clean: 14 | rm -rf ${BUILDDIR}/* 15 | 16 | run: 17 | uv run python -m http.server --directory "${BUILDDIR}" 18 | -------------------------------------------------------------------------------- /doc/_static/jenkins.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pycontribs/jenkinsapi/ffb35b6dca607b9aa5a65d4e49aa1ca76946adbf/doc/_static/jenkins.png -------------------------------------------------------------------------------- /doc/_static/tmp.txt: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pycontribs/jenkinsapi/ffb35b6dca607b9aa5a65d4e49aa1ca76946adbf/doc/_static/tmp.txt -------------------------------------------------------------------------------- /doc/getting_started.rst: -------------------------------------------------------------------------------- 1 | Getting Started 2 | =============== 3 | 4 | JenkinsAPI lets you query the state of a running Jenkins server. It also allows you to change configuration and automate minor tasks on nodes and jobs. 5 | 6 | Installation 7 | ------------- 8 | 9 | .. code-block:: bash 10 | 11 | pip install jenkinsapi 12 | 13 | Example 14 | ------- 15 | 16 | JenkinsAPI is intended to map the objects in Jenkins (e.g. Builds, Views, Jobs) into easily managed Python objects: 17 | 18 | .. code-block:: python 19 | 20 | from jenkinsapi.jenkins import Jenkins 21 | J = Jenkins('http://localhost:8080') 22 | print(J.version) # 1.542 23 | print(J.keys()) # foo, test_jenkinsapi 24 | print(J.get('test_jenkinsapi')) # 25 | print(J.get('test_jenkinsapi').get_last_good_build()) # 26 | 27 | Testing 28 | ------- 29 | 30 | If you have installed the test dependencies on your system already, you can run 31 | the testsuite with the following command: 32 | 33 | .. code-block:: bash 34 | 35 | uv sync 36 | uv run pytest -sv --cov=jenkinsapi --cov-report=term-missing --cov-report=xml jenkinsapi_tests 37 | 38 | Otherwise using a virtualenv is recommended. Setuptools will automatically fetch 39 | missing test dependencies: 40 | 41 | .. code-block:: bash 42 | 43 | uv venv 44 | uv python install 45 | uv run pytest -sv --cov=jenkinsapi --cov-report=term-missing --cov-report=xml jenkinsapi_tests 46 | 47 | Get version of Jenkins 48 | ---------------------- 49 | 50 | .. code-block:: python 51 | 52 | from jenkinsapi.jenkins import Jenkins 53 | 54 | def get_server_instance(): 55 | jenkins_url = 'http://jenkins_host:8080' 56 | server = Jenkins(jenkins_url, username='foouser', password='foopassword') 57 | return server 58 | 59 | if __name__ == '__main__': 60 | print get_server_instance().version 61 | 62 | The above code prints version of Jenkins running on the host *jenkins_host*. 63 | 64 | From Jenkins vesion 1.426 onward one can specify an API token instead of your real password while authenticating the user against Jenkins instance. 65 | 66 | Refer to the the Jenkis wiki page `Authenticating scripted clients `_ for details about how a user can generate an API token. 67 | 68 | Once you have API token you can pass the API token instead of real password while creating an Jenkins server instance using Jenkins API. 69 | 70 | Get details of jobs running on Jenkins server 71 | --------------------------------------------- 72 | 73 | .. code-block:: python 74 | 75 | """Get job details of each job that is running on the Jenkins instance""" 76 | def get_job_details(): 77 | # Refer Example #1 for definition of function 'get_server_instance' 78 | server = get_server_instance() 79 | for job_name, job_instance in server.get_jobs(): 80 | print 'Job Name:%s' % (job_instance.name) 81 | print 'Job Description:%s' % (job_instance.get_description()) 82 | print 'Is Job running:%s' % (job_instance.is_running()) 83 | print 'Is Job enabled:%s' % (job_instance.is_enabled()) 84 | 85 | Disable/Enable a Jenkins Job 86 | ---------------------------- 87 | 88 | .. code-block:: python 89 | 90 | def disable_job(): 91 | """Disable a Jenkins job""" 92 | # Refer Example #1 for definition of function 'get_server_instance' 93 | server = get_server_instance() 94 | job_name = 'nightly-build-job' 95 | if (server.has_job(job_name)): 96 | job_instance = server.get_job(job_name) 97 | job_instance.disable() 98 | print 'Name:%s,Is Job Enabled ?:%s' % (job_name,job_instance.is_enabled()) 99 | 100 | Use the call ``job_instance.enable()`` to enable a Jenkins Job. 101 | 102 | Get Plugin details 103 | ------------------ 104 | 105 | Below chunk of code gets the details of the plugins currently installed in the 106 | Jenkins instance. 107 | 108 | .. code-block:: python 109 | 110 | def get_plugin_details(): 111 | # Refer Example #1 for definition of function 'get_server_instance' 112 | server = get_server_instance() 113 | for plugin in server.get_plugins().values(): 114 | print "Short Name:%s" % (plugin.shortName) 115 | print "Long Name:%s" % (plugin.longName) 116 | print "Version:%s" % (plugin.version) 117 | print "URL:%s" % (plugin.url) 118 | print "Active:%s" % (plugin.active) 119 | print "Enabled:%s" % (plugin.enabled) 120 | 121 | Getting version information from a completed build 122 | -------------------------------------------------- 123 | 124 | This is a typical use of JenkinsAPI - it was the very first use I had in mind when the project was first built. 125 | 126 | In a continuous-integration environment you want to be able to programatically detect the version-control information of the last succsessful build in order to trigger some kind of release process. 127 | 128 | .. code-block:: python 129 | 130 | from jenkinsapi.jenkins import Jenkins 131 | 132 | def getSCMInfroFromLatestGoodBuild(url, jobName, username=None, password=None): 133 | J = Jenkins(url, username, password) 134 | job = J[jobName] 135 | lgb = job.get_last_good_build() 136 | return lgb.get_revision() 137 | 138 | if __name__ == '__main__': 139 | print getSCMInfroFromLatestGoodBuild('http://localhost:8080', 'fooJob') 140 | 141 | When used with the Git source-control system line 20 will print out something like '8b4f4e6f6d0af609bb77f95d8fb82ff1ee2bba0d' - which looks suspiciously like a Git revision number. 142 | -------------------------------------------------------------------------------- /doc/jenkins.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pycontribs/jenkinsapi/ffb35b6dca607b9aa5a65d4e49aa1ca76946adbf/doc/jenkins.png -------------------------------------------------------------------------------- /doc/module_reference.rst: -------------------------------------------------------------------------------- 1 | Module Reference 2 | ================ 3 | 4 | .. toctree:: 5 | :maxdepth: 4 6 | 7 | submodules/api 8 | submodules/artifact 9 | submodules/build 10 | submodules/credentials 11 | submodules/custom_exceptions 12 | submodules/command_line 13 | submodules/executors 14 | submodules/fingerprint 15 | submodules/jenkins 16 | submodules/jenkinsbase 17 | submodules/jobs 18 | submodules/label 19 | submodules/mutable_jenkins 20 | submodules/nodes 21 | submodules/plugins 22 | submodules/utils 23 | submodules/queue 24 | submodules/results 25 | submodules/views 26 | -------------------------------------------------------------------------------- /doc/project_info.rst: -------------------------------------------------------------------------------- 1 | Project Info 2 | ============ 3 | 4 | .. automodule:: jenkinsapi 5 | :members: 6 | :undoc-members: 7 | :show-inheritance: 8 | -------------------------------------------------------------------------------- /doc/readme_link.rst: -------------------------------------------------------------------------------- 1 | .. include:: ../README.rst 2 | -------------------------------------------------------------------------------- /doc/ssl_certificate_verification: -------------------------------------------------------------------------------- 1 | SSL Certificate Verification 2 | ============================ 3 | 4 | There are times, when one would like to skip the SSL certificate verification. 5 | 6 | For instance, the system administrator has set a different and new SSL 7 | certificate to the Jenkins master causing `certificate verify failed` errors: 8 | 9 | SSLError: [Errno 1] _ssl.c:504: error:14090086:SSL routines:SSL3_GET_SERVER_CERTIFICATE:certificate verify failed 10 | 11 | Unfortunately, he's not available right now, and you have to finish your tasks 12 | using the JenkinsAPI library by the end of today. 13 | In these times, skipping the SSL certificate verification can be helpful. 14 | 15 | ***Note***: It's not recommended to disable SSL verifying on a regular basis. 16 | To really fix this, please read 17 | [this post](http://stackoverflow.com/questions/30830901/python-requests-throwing-ssl-errors/30831120#30831120) 18 | at StackOverflow. 19 | 20 | Disabling the SSL verify 21 | ------------------------ 22 | 23 | Before JenkinsAPI v??, if one wants to disable the SSL certificate 24 | verification, she/he will have to pass a `Requester` instance specifying that. 25 | 26 | It may look like this: 27 | 28 | from jenkinsapi.jenkins import Jenkins 29 | username = 'jenkins' 30 | password = 'changeme' 31 | baseurl = 'https://localhost:8443' 32 | jenkins = Jenkins(baseurl, username, password, requester=Requester(username, password, ssl_verify=False)) 33 | 34 | As you can see, the last line is quite long. 35 | Not to mention that we pass the `username` and the `password` variables twice. 36 | 37 | There must be a better way! 38 | 39 | Disabling the SSL verify in JenkinsAPI v?? 40 | ------------------------------------------ 41 | 42 | All these problems are gone thanks to `ssl_verify` argument. 43 | You're not asked to pass a `Requester` instance anymore for just disabling 44 | the SSL certificate verification. 45 | 46 | This is how it looks like: 47 | 48 | jenkins = Jenkins(baseurl, username, password, ssl_verify=False) 49 | 50 | Now, the code is more readable than before. 51 | 52 | Notes 53 | ----- 54 | 55 | If you specify both `ssl_verify` and `requester` arguments, then the 56 | `ssl_verify` argument will be ignored. 57 | 58 | For example: 59 | 60 | jenkins = jenkinsapi.Jenkins(baseurl, username, password, requester=Requester(username, password, ssl_verify=False), ssl_verify=True) 61 | 62 | will do SSL certificate verifying. 63 | -------------------------------------------------------------------------------- /doc/submodules/api.rst: -------------------------------------------------------------------------------- 1 | User API 2 | ======== 3 | 4 | .. automodule:: jenkinsapi.api 5 | :members: 6 | :undoc-members: 7 | :show-inheritance: 8 | -------------------------------------------------------------------------------- /doc/submodules/artifact.rst: -------------------------------------------------------------------------------- 1 | Artifact 2 | ======== 3 | 4 | .. automodule:: jenkinsapi.artifact 5 | :members: 6 | :undoc-members: 7 | :show-inheritance: 8 | -------------------------------------------------------------------------------- /doc/submodules/build.rst: -------------------------------------------------------------------------------- 1 | Build 2 | ===== 3 | 4 | .. automodule:: jenkinsapi.build 5 | :members: 6 | :undoc-members: 7 | :show-inheritance: 8 | -------------------------------------------------------------------------------- /doc/submodules/command_line.rst: -------------------------------------------------------------------------------- 1 | Command\_line 2 | ============= 3 | 4 | Command\_line.jenkins\_invoke module 5 | ----------------------------------------------- 6 | 7 | .. automodule:: jenkinsapi.command_line.jenkins_invoke 8 | :members: 9 | :undoc-members: 10 | :show-inheritance: 11 | -------------------------------------------------------------------------------- /doc/submodules/credentials.rst: -------------------------------------------------------------------------------- 1 | Credentials and Credential 2 | ========================== 3 | 4 | .. contents:: 5 | :depth: 2 6 | :local: 7 | :backlinks: none 8 | :class: this-will-duplicate-information-and-it-is-still-useful-here 9 | 10 | Credentials 11 | ----------- 12 | 13 | .. automodule:: jenkinsapi.credentials 14 | :members: 15 | :undoc-members: 16 | :show-inheritance: 17 | 18 | Credential 19 | ---------- 20 | 21 | .. automodule:: jenkinsapi.credential 22 | :members: 23 | :undoc-members: 24 | :show-inheritance: 25 | -------------------------------------------------------------------------------- /doc/submodules/custom_exceptions.rst: -------------------------------------------------------------------------------- 1 | Custom\_exceptions 2 | ------------------ 3 | 4 | .. automodule:: jenkinsapi.custom_exceptions 5 | :members: 6 | :undoc-members: 7 | :show-inheritance: 8 | -------------------------------------------------------------------------------- /doc/submodules/executors.rst: -------------------------------------------------------------------------------- 1 | Exectors and Executor 2 | ===================== 3 | 4 | .. contents:: 5 | :depth: 2 6 | :local: 7 | :backlinks: none 8 | :class: this-will-duplicate-information-and-it-is-still-useful-here 9 | 10 | Executors 11 | --------- 12 | 13 | .. automodule:: jenkinsapi.executors 14 | :members: 15 | :undoc-members: 16 | :show-inheritance: 17 | 18 | Executor 19 | -------- 20 | 21 | .. automodule:: jenkinsapi.executor 22 | :members: 23 | :undoc-members: 24 | :show-inheritance: 25 | -------------------------------------------------------------------------------- /doc/submodules/fingerprint.rst: -------------------------------------------------------------------------------- 1 | Fingerprint 2 | ----------- 3 | 4 | .. automodule:: jenkinsapi.fingerprint 5 | :members: 6 | :undoc-members: 7 | :show-inheritance: 8 | -------------------------------------------------------------------------------- /doc/submodules/jenkins.rst: -------------------------------------------------------------------------------- 1 | Jenkins 2 | ------- 3 | 4 | .. automodule:: jenkinsapi.jenkins 5 | :members: 6 | :undoc-members: 7 | :show-inheritance: 8 | -------------------------------------------------------------------------------- /doc/submodules/jenkinsbase.rst: -------------------------------------------------------------------------------- 1 | Jenkinsbase 2 | ----------- 3 | 4 | .. automodule:: jenkinsapi.jenkinsbase 5 | :members: 6 | :undoc-members: 7 | :show-inheritance: 8 | -------------------------------------------------------------------------------- /doc/submodules/jobs.rst: -------------------------------------------------------------------------------- 1 | Jobs and job 2 | ============ 3 | 4 | .. contents:: 5 | :depth: 2 6 | :local: 7 | :backlinks: none 8 | :class: this-will-duplicate-information-and-it-is-still-useful-here 9 | 10 | Jobs 11 | ---- 12 | 13 | .. automodule:: jenkinsapi.jobs 14 | :members: 15 | :undoc-members: 16 | :show-inheritance: 17 | 18 | Job 19 | --- 20 | 21 | .. automodule:: jenkinsapi.job 22 | :members: 23 | :undoc-members: 24 | :show-inheritance: 25 | -------------------------------------------------------------------------------- /doc/submodules/label.rst: -------------------------------------------------------------------------------- 1 | Label 2 | ----- 3 | 4 | .. automodule:: jenkinsapi.label 5 | :members: 6 | :undoc-members: 7 | :show-inheritance: 8 | -------------------------------------------------------------------------------- /doc/submodules/mutable_jenkins.rst: -------------------------------------------------------------------------------- 1 | Mutable_jenkins_thing 2 | --------------------- 3 | 4 | .. automodule:: jenkinsapi.mutable_jenkins_thing 5 | :members: 6 | :undoc-members: 7 | :show-inheritance: 8 | -------------------------------------------------------------------------------- /doc/submodules/nodes.rst: -------------------------------------------------------------------------------- 1 | Nodes and node 2 | ============== 3 | 4 | .. contents:: 5 | :depth: 2 6 | :local: 7 | :backlinks: none 8 | :class: this-will-duplicate-information-and-it-is-still-useful-here 9 | 10 | nodes 11 | ----- 12 | 13 | .. automodule:: jenkinsapi.nodes 14 | :members: 15 | :undoc-members: 16 | :show-inheritance: 17 | 18 | node 19 | ---- 20 | 21 | .. automodule:: jenkinsapi.node 22 | :members: 23 | :undoc-members: 24 | :show-inheritance: 25 | -------------------------------------------------------------------------------- /doc/submodules/plugins.rst: -------------------------------------------------------------------------------- 1 | Plugins and Plugin 2 | ================== 3 | 4 | .. contents:: 5 | :depth: 2 6 | :local: 7 | :backlinks: none 8 | :class: this-will-duplicate-information-and-it-is-still-useful-here 9 | 10 | Plugins 11 | ------- 12 | 13 | .. automodule:: jenkinsapi.plugins 14 | :members: 15 | :undoc-members: 16 | :show-inheritance: 17 | 18 | Plugin 19 | ------ 20 | 21 | .. automodule:: jenkinsapi.plugin 22 | :members: 23 | :undoc-members: 24 | :show-inheritance: 25 | -------------------------------------------------------------------------------- /doc/submodules/queue.rst: -------------------------------------------------------------------------------- 1 | Queue 2 | ----- 3 | 4 | .. automodule:: jenkinsapi.queue 5 | :members: 6 | :undoc-members: 7 | :show-inheritance: 8 | -------------------------------------------------------------------------------- /doc/submodules/results.rst: -------------------------------------------------------------------------------- 1 | Result_set and result 2 | ===================== 3 | 4 | .. contents:: 5 | :depth: 2 6 | :local: 7 | :backlinks: none 8 | :class: this-will-duplicate-information-and-it-is-still-useful-here 9 | 10 | Result 11 | ------ 12 | 13 | .. automodule:: jenkinsapi.result 14 | :members: 15 | :undoc-members: 16 | :show-inheritance: 17 | 18 | Result_set 19 | ---------- 20 | 21 | .. automodule:: jenkinsapi.result_set 22 | :members: 23 | :undoc-members: 24 | :show-inheritance: 25 | -------------------------------------------------------------------------------- /doc/submodules/utils.rst: -------------------------------------------------------------------------------- 1 | Utils 2 | ===== 3 | 4 | crumb\_requester module 5 | ---------------------------------------- 6 | 7 | .. automodule:: jenkinsapi.utils.crumb_requester 8 | :members: 9 | :undoc-members: 10 | :show-inheritance: 11 | 12 | jenkins\_launcher module 13 | ----------------------------------------- 14 | 15 | .. automodule:: jenkinsapi.utils.jenkins_launcher 16 | :members: 17 | :undoc-members: 18 | :show-inheritance: 19 | 20 | jsonp\_to\_json module 21 | --------------------------------------- 22 | 23 | .. automodule:: jenkinsapi.utils.jsonp_to_json 24 | :members: 25 | :undoc-members: 26 | :show-inheritance: 27 | 28 | krb\_requester module 29 | -------------------------------------- 30 | 31 | .. automodule:: jenkinsapi.utils.krb_requester 32 | :members: 33 | :undoc-members: 34 | :show-inheritance: 35 | 36 | manifest module 37 | -------------------------------- 38 | 39 | .. automodule:: jenkinsapi.utils.manifest 40 | :members: 41 | :undoc-members: 42 | :show-inheritance: 43 | 44 | requester module 45 | --------------------------------- 46 | 47 | .. automodule:: jenkinsapi.utils.requester 48 | :members: 49 | :undoc-members: 50 | :show-inheritance: 51 | 52 | simple\_post\_logger module 53 | -------------------------------------------- 54 | 55 | .. automodule:: jenkinsapi.utils.simple_post_logger 56 | :members: 57 | :undoc-members: 58 | :show-inheritance: 59 | 60 | Module contents 61 | --------------- 62 | 63 | .. automodule:: jenkinsapi.utils 64 | :members: 65 | :undoc-members: 66 | :show-inheritance: 67 | -------------------------------------------------------------------------------- /doc/submodules/views.rst: -------------------------------------------------------------------------------- 1 | View and Views 2 | ============== 3 | 4 | .. contents:: 5 | :depth: 2 6 | :local: 7 | :backlinks: none 8 | :class: this-will-duplicate-information-and-it-is-still-useful-here 9 | 10 | Views 11 | ----- 12 | 13 | .. automodule:: jenkinsapi.views 14 | :members: 15 | :undoc-members: 16 | :show-inheritance: 17 | 18 | View 19 | ---- 20 | 21 | .. automodule:: jenkinsapi.view 22 | :members: 23 | :undoc-members: 24 | :show-inheritance: 25 | -------------------------------------------------------------------------------- /examples/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pycontribs/jenkinsapi/ffb35b6dca607b9aa5a65d4e49aa1ca76946adbf/examples/__init__.py -------------------------------------------------------------------------------- /examples/addjob.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | false 6 | 7 | 8 | true 9 | false 10 | false 11 | false 12 | 13 | false 14 | 15 | 16 | 17 | 18 | -------------------------------------------------------------------------------- /examples/how_to/add_command.py: -------------------------------------------------------------------------------- 1 | """ 2 | This example shows how to add new command to "Shell" build step 3 | """ 4 | 5 | import xml.etree.ElementTree as et 6 | from jenkinsapi.jenkins import Jenkins 7 | 8 | J = Jenkins("http://localhost:8080") 9 | EMPTY_JOB_CONFIG = """ 10 | 11 | 12 | jkkjjk 13 | 14 | false 15 | 16 | 17 | true 18 | false 19 | false 20 | false 21 | 22 | false 23 | 24 | 25 | 26 | 27 | """ 28 | 29 | jobname = "foo_job" 30 | new_job = J.create_job(jobname, EMPTY_JOB_CONFIG) 31 | new_conf = new_job.get_config() 32 | 33 | root = et.fromstring(new_conf.strip()) 34 | 35 | builders = root.find("builders") 36 | shell = et.SubElement(builders, "hudson.tasks.Shell") 37 | command = et.SubElement(shell, "command") 38 | command.text = "ls" 39 | 40 | print(et.tostring(root)) 41 | J[jobname].update_config(et.tostring(root)) 42 | -------------------------------------------------------------------------------- /examples/how_to/create_a_job.py: -------------------------------------------------------------------------------- 1 | """ 2 | This example shows how to create job from XML file and how to delete job 3 | """ 4 | 5 | from pkg_resources import resource_string 6 | from jenkinsapi.jenkins import Jenkins 7 | 8 | jenkins = Jenkins("http://localhost:8080") 9 | job_name = "foo_job2" 10 | xml = resource_string("examples", "addjob.xml") 11 | 12 | print(xml) 13 | 14 | job = jenkins.create_job(jobname=job_name, xml=xml) 15 | 16 | # Get job from Jenkins by job name 17 | my_job = jenkins[job_name] 18 | print(my_job) 19 | 20 | # Delete job using method in Jenkins class 21 | # 22 | # Another way is to use: 23 | # 24 | # del jenkins[job_name] 25 | jenkins.delete_job(job_name) 26 | -------------------------------------------------------------------------------- /examples/how_to/create_credentials.py: -------------------------------------------------------------------------------- 1 | """ 2 | This example shows how to create credentials 3 | """ 4 | 5 | import logging 6 | from jenkinsapi.jenkins import Jenkins 7 | from jenkinsapi.credential import UsernamePasswordCredential, SSHKeyCredential 8 | 9 | log_level = getattr(logging, "DEBUG") 10 | logging.basicConfig(level=log_level) 11 | logger = logging.getLogger() 12 | 13 | jenkins_url = "http://localhost:8080/" 14 | 15 | jenkins = Jenkins(jenkins_url) 16 | 17 | # Get a list of all global credentials 18 | creds = jenkins.credentials 19 | logging.info(jenkins.credentials.keys()) 20 | 21 | # Create username and password credential 22 | creds_description1 = "My_username_credential" 23 | cred_dict = { 24 | "description": creds_description1, 25 | "userName": "userName", 26 | "password": "password", 27 | } 28 | creds[creds_description1] = UsernamePasswordCredential(cred_dict) 29 | 30 | 31 | # Create ssh key credential that uses private key as a value 32 | # In jenkins credential dialog you need to paste credential 33 | # In your code it is adviced to read it from file 34 | # For simplicity of this example reading key from file is not shown here 35 | def get_private_key_from_file(): 36 | return "-----BEGIN RSA PRIVATE KEY-----" 37 | 38 | 39 | my_private_key = get_private_key_from_file() 40 | 41 | creds_description2 = "My_ssh_cred1" 42 | cred_dict = { 43 | "description": creds_description2, 44 | "userName": "userName", 45 | "passphrase": "", 46 | "private_key": my_private_key, 47 | } 48 | creds[creds_description2] = SSHKeyCredential(cred_dict) 49 | 50 | # Create ssh key credential that uses private key from path on Jenkins server 51 | my_private_key = "/home/jenkins/.ssh/special_key" 52 | 53 | creds_description3 = "My_ssh_cred2" 54 | cred_dict = { 55 | "description": creds_description3, 56 | "userName": "userName", 57 | "passphrase": "", 58 | "private_key": my_private_key, 59 | } 60 | creds[creds_description3] = SSHKeyCredential(cred_dict) 61 | 62 | # Remove credentials 63 | # We use credential description to find specific credential. This is the only 64 | # way to get specific credential from Jenkins via REST API 65 | del creds[creds_description1] 66 | del creds[creds_description2] 67 | del creds[creds_description3] 68 | 69 | # Remove all credentials 70 | for cred_descr in creds.keys(): 71 | del creds[cred_descr] 72 | -------------------------------------------------------------------------------- /examples/how_to/create_nested_views.py: -------------------------------------------------------------------------------- 1 | """ 2 | How to create nested views using NestedViews Jenkins plugin 3 | 4 | This example requires NestedViews plugin to be installed in Jenkins 5 | You need to have at least one job in your Jenkins to see views 6 | """ 7 | 8 | import logging 9 | from pkg_resources import resource_string 10 | from jenkinsapi.views import Views 11 | from jenkinsapi.jenkins import Jenkins 12 | 13 | log_level = getattr(logging, "DEBUG") 14 | logging.basicConfig(level=log_level) 15 | logger = logging.getLogger() 16 | 17 | jenkins_url = "http://127.0.0.1:8080/" 18 | jenkins = Jenkins(jenkins_url) 19 | 20 | job_name = "foo_job2" 21 | xml = resource_string("examples", "addjob.xml") 22 | j = jenkins.create_job(jobname=job_name, xml=xml) 23 | 24 | # Create ListView in main view 25 | logger.info("Attempting to create new nested view") 26 | top_view = jenkins.views.create("TopView", Views.NESTED_VIEW) 27 | logger.info("top_view is %s", top_view) 28 | if top_view is None: 29 | logger.error("View was not created") 30 | else: 31 | logger.info("View has been created") 32 | 33 | print("top_view.views=", top_view.views.keys()) 34 | logger.info("Attempting to create view inside nested view") 35 | sub_view = top_view.views.create("SubView") 36 | if sub_view is None: 37 | logger.info("View was not created") 38 | else: 39 | logger.error("View has been created") 40 | 41 | logger.info("Attempting to delete sub_view") 42 | del top_view.views["SubView"] 43 | if "SubView" in top_view.views: 44 | logger.error("SubView was not deleted") 45 | else: 46 | logger.info("SubView has been deleted") 47 | 48 | # Another way of creating sub view 49 | # This way sub view will have jobs in it 50 | logger.info("Attempting to create view with jobs inside nested view") 51 | top_view.views["SubView"] = job_name 52 | if "SubView" not in top_view.views: 53 | logger.error("View was not created") 54 | else: 55 | logger.info("View has been created") 56 | 57 | logger.info("Attempting to delete sub_view") 58 | del top_view.views["SubView"] 59 | if "SubView" in top_view.views: 60 | logger.error("SubView was not deleted") 61 | else: 62 | logger.info("SubView has been deleted") 63 | 64 | logger.info("Attempting to delete top view") 65 | del jenkins.views["TopView"] 66 | if "TopView" not in jenkins.views: 67 | logger.info("View has been deleted") 68 | else: 69 | logger.error("View was not deleted") 70 | 71 | # Delete job that we created 72 | jenkins.delete_job(job_name) 73 | -------------------------------------------------------------------------------- /examples/how_to/create_slave.py: -------------------------------------------------------------------------------- 1 | """ 2 | How to create slaves/nodes 3 | """ 4 | 5 | import logging 6 | import requests 7 | from jenkinsapi.jenkins import Jenkins 8 | from jenkinsapi.utils.requester import Requester 9 | 10 | requests.packages.urllib3.disable_warnings() 11 | 12 | log_level = getattr(logging, "DEBUG") 13 | logging.basicConfig(level=log_level) 14 | logger = logging.getLogger() 15 | 16 | jenkins_url = "http://localhost:8080/" 17 | username = "default_user" # In case Jenkins requires authentication 18 | password = "default_password" 19 | 20 | jenkins = Jenkins( 21 | jenkins_url, 22 | requester=Requester( 23 | username, password, baseurl=jenkins_url, ssl_verify=False 24 | ), 25 | ) 26 | 27 | # Create JNLP(Java Webstart) slave 28 | node_dict = { 29 | "num_executors": 1, # Number of executors 30 | "node_description": "Test JNLP Node", # Just a user friendly text 31 | "remote_fs": "/tmp", # Remote workspace location 32 | "labels": "my_new_node", # Space separated labels string 33 | "exclusive": True, # Only run jobs assigned to it 34 | } 35 | new_jnlp_node = jenkins.nodes.create_node("My new webstart node", node_dict) 36 | 37 | node_dict = { 38 | "num_executors": 1, 39 | "node_description": "Test SSH Node", 40 | "remote_fs": "/tmp", 41 | "labels": "new_node", 42 | "exclusive": True, 43 | "host": "localhost", # Remote hostname 44 | "port": 22, # Remote post, usually 22 45 | "credential_description": "localhost cred", # Credential to use 46 | # [Mandatory for SSH node!] 47 | # (see Credentials example) 48 | "jvm_options": "-Xmx2000M", # JVM parameters 49 | "java_path": "/bin/java", # Path to java 50 | "prefix_start_slave_cmd": "", 51 | "suffix_start_slave_cmd": "", 52 | "max_num_retries": 0, 53 | "retry_wait_time": 0, 54 | "retention": "OnDemand", # Change to 'Always' for 55 | # immediate slave launch 56 | "ondemand_delay": 1, 57 | "ondemand_idle_delay": 5, 58 | "env": [ # Environment variables 59 | {"key": "TEST", "value": "VALUE"}, 60 | {"key": "TEST2", "value": "value2"}, 61 | ], 62 | } 63 | new_ssh_node = jenkins.nodes.create_node("My new SSH node", node_dict) 64 | 65 | # Take this slave offline 66 | if new_ssh_node.is_online(): 67 | new_ssh_node.toggle_temporarily_offline() 68 | 69 | # Take this slave back online 70 | new_ssh_node.toggle_temporarily_offline() 71 | 72 | # Get a list of all slave names 73 | slave_names = jenkins.nodes.keys() 74 | 75 | # Get Node object 76 | my_node = jenkins.nodes["My new SSH node"] 77 | # Take this slave offline 78 | my_node.set_offline() 79 | 80 | # Delete slaves 81 | del jenkins.nodes["My new webstart node"] 82 | del jenkins.nodes["My new SSH node"] 83 | -------------------------------------------------------------------------------- /examples/how_to/create_views.py: -------------------------------------------------------------------------------- 1 | """ 2 | How to create views 3 | """ 4 | 5 | import logging 6 | from pkg_resources import resource_string 7 | from jenkinsapi.jenkins import Jenkins 8 | 9 | logging.basicConfig(level=logging.INFO) 10 | logger = logging.getLogger() 11 | 12 | jenkins_url = "http://localhost:8080/" 13 | 14 | jenkins = Jenkins(jenkins_url, lazy=True) 15 | 16 | # Create ListView in main view 17 | logger.info("Attempting to create new view") 18 | test_view_name = "SimpleListView" 19 | 20 | # Views object appears as a dictionary of views 21 | if test_view_name not in jenkins.views: 22 | new_view = jenkins.views.create(test_view_name) 23 | if new_view is None: 24 | logger.error("View %s was not created", test_view_name) 25 | else: 26 | logger.info( 27 | "View %s has been created: %s", new_view.name, new_view.baseurl 28 | ) 29 | else: 30 | logger.info("View %s already exists", test_view_name) 31 | 32 | # No error is raised if view already exists 33 | logger.info("Attempting to create view that already exists") 34 | my_view = jenkins.views.create(test_view_name) 35 | 36 | logger.info("Create job and assign it to a view") 37 | job_name = "foo_job2" 38 | xml = resource_string("examples", "addjob.xml") 39 | 40 | my_job = jenkins.create_job(jobname=job_name, xml=xml) 41 | 42 | # add_job supports two parameters: job_name and job object 43 | # passing job object will remove verification calls to Jenkins 44 | my_view.add_job(job_name, my_job) 45 | assert len(my_view) == 1 46 | 47 | logger.info("Attempting to delete view that already exists") 48 | del jenkins.views[test_view_name] 49 | 50 | if test_view_name in jenkins.views: 51 | logger.error("View was not deleted") 52 | else: 53 | logger.info("View has been deleted") 54 | 55 | # No error will be raised when attempting to remove non-existing view 56 | logger.info("Attempting to delete view that does not exist") 57 | del jenkins.views[test_view_name] 58 | 59 | # Create CategorizedJobsView 60 | config = """ 61 | 62 | 63 | 64 | .dev. 65 | Development 66 | 67 | 68 | .hml. 69 | Homologation 70 | 71 | 72 | 73 | """ 74 | view = jenkins.views.create( 75 | "My categorized jobs view", jenkins.views.CATEGORIZED_VIEW, config=config 76 | ) 77 | -------------------------------------------------------------------------------- /examples/how_to/delete_all_the_nodes_except_master.py: -------------------------------------------------------------------------------- 1 | """ 2 | How to delete slaves/nodes 3 | """ 4 | 5 | import logging 6 | from jenkinsapi.jenkins import Jenkins 7 | 8 | logging.basicConfig() 9 | 10 | 11 | j = Jenkins("http://localhost:8080") 12 | 13 | for node_id, _ in j.get_nodes().iteritems(): 14 | if node_id != "master": 15 | print(node_id) 16 | j.delete_node(node_id) 17 | 18 | # Alternative way - this method will not delete 'master' 19 | for node in j.nodes.keys(): 20 | del j.nodes[node] 21 | -------------------------------------------------------------------------------- /examples/how_to/get_config.py: -------------------------------------------------------------------------------- 1 | """ 2 | An example of how to use JenkinsAPI to fetch the config XML of a job. 3 | """ 4 | 5 | from jenkinsapi.jenkins import Jenkins 6 | 7 | jenkins = Jenkins("http://localhost:8080") 8 | jobName = jenkins.keys()[0] # get the first job 9 | 10 | config = jenkins[jobName].get_config() 11 | 12 | print(config) 13 | -------------------------------------------------------------------------------- /examples/how_to/get_plugin_information.py: -------------------------------------------------------------------------------- 1 | """ 2 | Get information about currently installed plugins 3 | """ 4 | 5 | from jenkinsapi.jenkins import Jenkins 6 | 7 | 8 | plugin_name = "subversion" 9 | jenkins = Jenkins("http://localhost:8080") 10 | plugin = jenkins.get_plugins()[plugin_name] 11 | 12 | print(repr(plugin)) 13 | -------------------------------------------------------------------------------- /examples/how_to/get_version_info_from_last_good_build.py: -------------------------------------------------------------------------------- 1 | """ 2 | Extract version information from the latest build. 3 | """ 4 | 5 | from jenkinsapi.jenkins import Jenkins 6 | 7 | 8 | job_name = "foo" 9 | jenkins = Jenkins("http://localhost:8080") 10 | job = jenkins[job_name] 11 | lgb = job.get_last_good_build() 12 | print(lgb.get_revision()) 13 | -------------------------------------------------------------------------------- /examples/how_to/query_a_build.py: -------------------------------------------------------------------------------- 1 | """ 2 | How to get build from job and query that build 3 | """ 4 | 5 | from jenkinsapi.jenkins import Jenkins 6 | 7 | 8 | jenkins = Jenkins("http://localhost:8080") 9 | # Print all jobs in Jenkins 10 | print(jenkins.items()) 11 | 12 | job = jenkins.get_job("foo") 13 | build = job.get_last_build() 14 | print(build) 15 | 16 | mjn = build.get_master_job_name() 17 | print(mjn) 18 | -------------------------------------------------------------------------------- /examples/how_to/readme.rst: -------------------------------------------------------------------------------- 1 | "How To..." examples 2 | ==================== 3 | 4 | This directory contains a set of examples or recipes for common tasks in JenkinsAPI. 5 | 6 | ========================================= ================================================== 7 | Example Description 8 | ----------------------------------------- -------------------------------------------------- 9 | add_command.py create new job and then add shell build step to it 10 | create_a_job.py create new job 11 | create_credentials.py create new credential 12 | create_nested_views.py create nested views using Nested Views Jenkins plugin 13 | create_slave.py create jnlp ans ssh slave 14 | create_views.py create views, assign and delete jobs in views 15 | delete_all_the_nodes.py delete all slaves except master 16 | get_config.py get job configuration XML 17 | get_plugin_infomation.py show information about plugin 18 | get_version_info_from_last_good_build.py get Git revision from last successful build 19 | query_a_build.py get build information 20 | search_artifact_by_regexp.py search for job artifacts using regular expression 21 | search_artifacts.py search for artifacts 22 | start_parameterized_build.py start a build with parameters 23 | use_crumbs.py how to work with CSRF protection enabled in Jenkins 24 | ========================================= ================================================== 25 | -------------------------------------------------------------------------------- /examples/how_to/search_artifact_by_regexp.py: -------------------------------------------------------------------------------- 1 | """ 2 | Search for job artifacts using regexp 3 | """ 4 | 5 | import re 6 | from jenkinsapi.api import search_artifact_by_regexp 7 | 8 | jenkinsurl = "http://localhost:8080" 9 | jobid = "foo" 10 | artifact_regexp = re.compile(r"test1\.txt") # A file name I want. 11 | result = search_artifact_by_regexp(jenkinsurl, jobid, artifact_regexp) 12 | print((repr(result))) 13 | -------------------------------------------------------------------------------- /examples/how_to/search_artifacts.py: -------------------------------------------------------------------------------- 1 | """ 2 | Search for job artifacts 3 | """ 4 | 5 | from jenkinsapi.api import search_artifacts 6 | 7 | jenkinsurl = "http://localhost:8080" 8 | jobid = "foo" 9 | # I need a build that contains all of these 10 | artifact_ids = ["test1.txt", "test2.txt"] 11 | result = search_artifacts(jenkinsurl, jobid, artifact_ids) 12 | print((repr(result))) 13 | -------------------------------------------------------------------------------- /examples/how_to/start_parameterized_build.py: -------------------------------------------------------------------------------- 1 | """ 2 | Start a Parameterized Build 3 | """ 4 | 5 | from jenkinsapi.jenkins import Jenkins 6 | 7 | jenkins = Jenkins("http://localhost:8080") 8 | 9 | params = {"VERSION": "1.2.3", "PYTHON_VER": "2.7"} 10 | 11 | # This will start the job in non-blocking manner 12 | jenkins.build_job("foo", params) 13 | 14 | 15 | # This will start the job and will return a QueueItem object which 16 | # can be used to get build results 17 | job = jenkins["foo"] 18 | qi = job.invoke(build_params=params) 19 | 20 | # Block this script until build is finished 21 | if qi.is_queued() or qi.is_running(): 22 | qi.block_until_complete() 23 | 24 | build = qi.get_build() 25 | print(build) 26 | -------------------------------------------------------------------------------- /examples/how_to/use_crumbs.py: -------------------------------------------------------------------------------- 1 | """ 2 | Example of using CrumbRequester - when CSRF protection is enabled in Jenkins 3 | """ 4 | 5 | from jenkinsapi.jenkins import Jenkins 6 | 7 | jenkins = Jenkins( 8 | "http://localhost:8080", 9 | username="admin", 10 | password="password", 11 | use_crumb=True, 12 | ) 13 | 14 | for job_name in jenkins.jobs: 15 | print(job_name) 16 | -------------------------------------------------------------------------------- /examples/low_level/copy_a_job.py: -------------------------------------------------------------------------------- 1 | """ 2 | A lower-level implementation of copying a job in Jenkins 3 | """ 4 | 5 | import requests 6 | from pkg_resources import resource_string 7 | from jenkinsapi.jenkins import Jenkins 8 | from jenkinsapi_tests.test_utils.random_strings import random_string 9 | 10 | J = Jenkins("http://localhost:8080") 11 | jobName = random_string() 12 | jobName2 = "%s_2" % jobName 13 | 14 | url = "http://localhost:8080/createItem?from=%s&name=%s&mode=copy" % ( 15 | jobName, 16 | jobName2, 17 | ) 18 | 19 | xml = resource_string("examples", "addjob.xml") 20 | j = J.create_job(jobname=jobName, xml=xml) 21 | 22 | 23 | h = {"Content-Type": "application/x-www-form-urlencoded"} 24 | response = requests.post(url, data="dysjsjsjs", headers=h) 25 | print(response.text.encode("UTF-8")) 26 | -------------------------------------------------------------------------------- /examples/low_level/create_a_view_low_level.py: -------------------------------------------------------------------------------- 1 | """ 2 | A low level example: 3 | This is how JenkinsAPI creates views 4 | """ 5 | 6 | import json 7 | import requests 8 | 9 | url = "http://localhost:8080/createView" 10 | 11 | str_view_name = "blahblah123" 12 | params = {} # {'name': str_view_name} 13 | headers = {"Content-Type": "application/x-www-form-urlencoded"} 14 | data = { 15 | "name": str_view_name, 16 | "mode": "hudson.model.ListView", 17 | "Submit": "OK", 18 | "json": json.dumps( 19 | {"name": str_view_name, "mode": "hudson.model.ListView"} 20 | ), 21 | } 22 | # Try 1 23 | result = requests.post(url, params=params, data=data, headers=headers) 24 | print(result.text.encode("UTF-8")) 25 | -------------------------------------------------------------------------------- /examples/low_level/example_param_build.py: -------------------------------------------------------------------------------- 1 | import json 2 | import requests 3 | 4 | 5 | def foo(): 6 | """ 7 | A low level example of how JenkinsAPI runs a parameterized build 8 | """ 9 | toJson = {"parameter": [{"name": "B", "value": "xyz"}]} 10 | url = "http://localhost:8080/job/ddd/build" 11 | # url = 'http://localhost:8000' 12 | headers = {"Content-Type": "application/x-www-form-urlencoded"} 13 | form = {"json": json.dumps(toJson)} 14 | response = requests.post(url, data=form, headers=headers) 15 | print(response.text.encode("UTF-8")) 16 | 17 | 18 | if __name__ == "__main__": 19 | foo() 20 | -------------------------------------------------------------------------------- /examples/low_level/login_with_auth.py: -------------------------------------------------------------------------------- 1 | """ 2 | A lower level example of how we login with authentication 3 | """ 4 | 5 | from jenkinsapi import jenkins 6 | 7 | 8 | J = jenkins.Jenkins("http://localhost:8080", username="sal", password="foobar") 9 | J.poll() 10 | 11 | print(J.items()) 12 | -------------------------------------------------------------------------------- /examples/low_level/post_watcher.py: -------------------------------------------------------------------------------- 1 | """ 2 | Save this file as server.py 3 | >>> python server.py 0.0.0.0 8001 4 | serving on 0.0.0.0:8001 5 | 6 | or simply 7 | 8 | >>> python server.py 9 | Serving on localhost:8000 10 | 11 | You can use this to test GET and POST methods. 12 | """ 13 | 14 | import http.server as SimpleHTTPServer 15 | import socketserver 16 | import logging 17 | import cgi 18 | 19 | PORT = 8081 # <-- change this to be the actual port you want to run on 20 | INTERFACE = "localhost" 21 | 22 | 23 | class ServerHandler(SimpleHTTPServer.SimpleHTTPRequestHandler): 24 | def do_GET(self): 25 | logging.warning("======= GET STARTED =======") 26 | logging.warning(self.headers) 27 | SimpleHTTPServer.SimpleHTTPRequestHandler.do_GET(self) 28 | 29 | def do_POST(self): 30 | logging.warning("======= POST STARTED =======") 31 | logging.warning(self.headers) 32 | form = cgi.FieldStorage( 33 | fp=self.rfile, 34 | headers=self.headers, 35 | environ={ 36 | "REQUEST_METHOD": "POST", 37 | "CONTENT_TYPE": self.headers["Content-Type"], 38 | }, 39 | ) 40 | logging.warning("======= POST VALUES =======") 41 | for item in form.list: 42 | logging.warning(item) 43 | logging.warning("\n") 44 | SimpleHTTPServer.SimpleHTTPRequestHandler.do_GET(self) 45 | 46 | 47 | Handler = ServerHandler 48 | 49 | httpd = socketserver.TCPServer(("", PORT), Handler) 50 | 51 | print( 52 | "Serving at: http://%(interface)s:%(port)s" 53 | % dict(interface=INTERFACE or "localhost", port=PORT) 54 | ) 55 | httpd.serve_forever() 56 | -------------------------------------------------------------------------------- /examples/low_level/readme.rst: -------------------------------------------------------------------------------- 1 | Low-Level Examples 2 | ================== 3 | 4 | These examoples are intended to explain how JenkinsAPI performs certain functions. While developing JenkinsAPI I created a number of small scripts like these in order to figure out the correct way to communicate. Ive retained a number of these as they provide some insights into how the various interfaces that Jenkins provides can be used. 5 | -------------------------------------------------------------------------------- /jenkinsapi/.gitignore: -------------------------------------------------------------------------------- 1 | /__pycache__ 2 | -------------------------------------------------------------------------------- /jenkinsapi/__init__.py: -------------------------------------------------------------------------------- 1 | """ 2 | About this library 3 | ================== 4 | 5 | Jenkins is the market leading continuous integration system, originally created 6 | by Kohsuke Kawaguchi. This API makes Jenkins even easier to use by providing an 7 | easy to use conventional python interface. 8 | 9 | Jenkins (and It's predecessor Hudson) are fantastic projects - but they are 10 | somewhat Java-centric. Thankfully the designers have provided an excellent and 11 | complete REST interface. This library wraps up that interface as more 12 | conventional python objects in order to make most Jenkins oriented 13 | tasks simpler. 14 | 15 | This library can help you: 16 | 17 | * Query the test-results of a completed build 18 | * Get a objects representing the latest builds of a job 19 | * Search for artifacts by simple criteria 20 | * Block until jobs are complete 21 | * Install artifacts to custom-specified directory structures 22 | * username/password auth support for jenkins instances with auth turned on 23 | * Ability to search for builds by subversion revision 24 | * Ability to add/remove/query jenkins slaves 25 | 26 | Installing JenkinsAPI 27 | ===================== 28 | 29 | pip install jenkinsapi 30 | 31 | Project Authors 32 | =============== 33 | 34 | * Salim Fadhley (sal@stodge.org) 35 | * Ramon van Alteren (ramon@vanalteren.nl) 36 | * Ruslan Lutsenko (ruslan.lutcenko@gmail.com) 37 | * Aleksey Maksimov 38 | * Clinton Steiner 39 | 40 | Current code lives on github: https://github.com/pycontribs/jenkinsapi 41 | 42 | """ 43 | 44 | from importlib.metadata import version 45 | 46 | from jenkinsapi import ( 47 | # Modules 48 | command_line, 49 | utils, 50 | # Files 51 | api, 52 | artifact, 53 | build, 54 | config, 55 | constants, 56 | custom_exceptions, 57 | fingerprint, 58 | executors, 59 | executor, 60 | jenkins, 61 | jenkinsbase, 62 | job, 63 | node, 64 | result_set, 65 | result, 66 | view, 67 | ) 68 | 69 | __all__ = [ 70 | "command_line", 71 | "utils", 72 | "api", 73 | "artifact", 74 | "build", 75 | "config", 76 | "constants", 77 | "custom_exceptions", 78 | "executors", 79 | "executor", 80 | "fingerprint", 81 | "jenkins", 82 | "jenkinsbase", 83 | "job", 84 | "node", 85 | "result_set", 86 | "result", 87 | "view", 88 | ] 89 | __docformat__ = "epytext" 90 | __version__ = version("jenkinsapi") 91 | -------------------------------------------------------------------------------- /jenkinsapi/artifact.py: -------------------------------------------------------------------------------- 1 | """ 2 | Artifacts can be used to represent data created as a side-effect of running 3 | a Jenkins build. 4 | 5 | Artifacts are files which are associated with a single build. A build can 6 | have any number of artifacts associated with it. 7 | 8 | This module provides a class called Artifact which allows you to download 9 | objects from the server and also access them as a stream. 10 | """ 11 | 12 | from __future__ import annotations 13 | 14 | import os 15 | import logging 16 | import hashlib 17 | from typing import Any, Literal 18 | 19 | from jenkinsapi.fingerprint import Fingerprint 20 | from jenkinsapi.custom_exceptions import ArtifactBroken 21 | 22 | log = logging.getLogger(__name__) 23 | 24 | 25 | class Artifact(object): 26 | """ 27 | Represents a single Jenkins artifact, usually some kind of file 28 | generated as a by-product of executing a Jenkins build. 29 | """ 30 | 31 | def __init__( 32 | self, 33 | filename: str, 34 | url: str, 35 | build: "Build", 36 | relative_path: str | None = None, 37 | ) -> None: 38 | self.filename: str = filename 39 | self.url: str = url 40 | self.build: "Build" = build 41 | self.relative_path: str | None = relative_path 42 | 43 | def save(self, fspath: str, strict_validation: bool = False) -> str: 44 | """ 45 | Save the artifact to an explicit path. The containing directory must 46 | exist. Returns a reference to the file which has just been writen to. 47 | 48 | :param fspath: full pathname including the filename, str 49 | :return: filepath 50 | """ 51 | log.info(msg="Saving artifact @ %s to %s" % (self.url, fspath)) 52 | if not fspath.endswith(self.filename): 53 | log.warning( 54 | "Attempt to change the filename of artifact %s on save.", 55 | self.filename, 56 | ) 57 | if os.path.exists(fspath): 58 | if self.build: 59 | try: 60 | if self._verify_download(fspath, strict_validation): 61 | log.info( 62 | "Local copy of %s is already up to date.", 63 | self.filename, 64 | ) 65 | return fspath 66 | except ArtifactBroken: 67 | log.warning("Jenkins artifact could not be identified.") 68 | else: 69 | log.info( 70 | "This file did not originate from Jenkins, " 71 | "so cannot check." 72 | ) 73 | else: 74 | log.info("Local file is missing, downloading new.") 75 | filepath = self._do_download(fspath) 76 | self._verify_download(filepath, strict_validation) 77 | return fspath 78 | 79 | def get_jenkins_obj(self) -> Jenkins: 80 | return self.build.get_jenkins_obj() 81 | 82 | def get_data(self) -> Any: 83 | """ 84 | Grab the text of the artifact 85 | """ 86 | response = self.get_jenkins_obj().requester.get_and_confirm_status( 87 | self.url 88 | ) 89 | return response.content 90 | 91 | def _do_download(self, fspath: str) -> str: 92 | """ 93 | Download the the artifact to a path. 94 | """ 95 | data = self.get_jenkins_obj().requester.get_and_confirm_status( 96 | self.url, stream=True 97 | ) 98 | with open(fspath, "wb") as out: 99 | for chunk in data.iter_content(chunk_size=1024): 100 | out.write(chunk) 101 | return fspath 102 | 103 | def _verify_download(self, fspath, strict_validation) -> Literal[True]: 104 | """ 105 | Verify that a downloaded object has a valid fingerprint. 106 | 107 | Returns True if the fingerprint is valid, raises an exception if 108 | the fingerprint is invalid. 109 | """ 110 | local_md5 = self._md5sum(fspath) 111 | baseurl = self.build.job.jenkins.baseurl 112 | fp = Fingerprint(baseurl, local_md5, self.build.job.jenkins) 113 | valid = fp.validate_for_build( 114 | self.filename, self.build.job.get_full_name(), self.build.buildno 115 | ) 116 | if not valid or (fp.unknown and strict_validation): 117 | # strict = 404 as invalid 118 | raise ArtifactBroken( 119 | "Artifact %s seems to be broken, check %s" 120 | % (local_md5, baseurl) 121 | ) 122 | return True 123 | 124 | def _md5sum(self, fspath: str, chunksize: int = 2**20) -> str: 125 | """ 126 | A MD5 hashing function intended to produce the same results as that 127 | used by Jenkins. 128 | """ 129 | md5 = hashlib.md5() 130 | with open(fspath, "rb") as f: 131 | for chunk in iter(lambda: f.read(chunksize), ""): 132 | if chunk: 133 | md5.update(chunk) 134 | else: 135 | break 136 | return md5.hexdigest() 137 | 138 | def save_to_dir( 139 | self, dirpath: str, strict_validation: bool = False 140 | ) -> str: 141 | """ 142 | Save the artifact to a folder. The containing directory must exist, 143 | but use the artifact's default filename. 144 | """ 145 | assert os.path.exists(dirpath) 146 | assert os.path.isdir(dirpath) 147 | outputfilepath: str = os.path.join(dirpath, self.filename) 148 | return self.save(outputfilepath, strict_validation) 149 | 150 | def __repr__(self) -> str: 151 | """ 152 | Produce a handy repr-string. 153 | """ 154 | return """<%s.%s %s>""" % ( 155 | self.__class__.__module__, 156 | self.__class__.__name__, 157 | self.url, 158 | ) 159 | -------------------------------------------------------------------------------- /jenkinsapi/command_line/__init__.py: -------------------------------------------------------------------------------- 1 | """ 2 | __init__,py for commandline module 3 | """ 4 | -------------------------------------------------------------------------------- /jenkinsapi/command_line/jenkins_invoke.py: -------------------------------------------------------------------------------- 1 | """ 2 | jenkinsapi class for invoking Jenkins 3 | """ 4 | 5 | import os 6 | import sys 7 | import logging 8 | import optparse 9 | from jenkinsapi import jenkins 10 | 11 | log = logging.getLogger(__name__) 12 | 13 | 14 | class JenkinsInvoke(object): 15 | """ 16 | JenkinsInvoke object implements class to call from command line 17 | """ 18 | 19 | @classmethod 20 | def mkparser(cls): 21 | parser = optparse.OptionParser() 22 | DEFAULT_BASEURL = os.environ.get( 23 | "JENKINS_URL", "http://localhost/jenkins" 24 | ) 25 | parser.help_text = ( 26 | "Execute a number of jenkins jobs on the server of your choice." 27 | + " Optionally block until the jobs are complete." 28 | ) 29 | parser.add_option( 30 | "-J", 31 | "--jenkinsbase", 32 | dest="baseurl", 33 | help="Base URL for the Jenkins server, default is %s" 34 | % DEFAULT_BASEURL, 35 | type="str", 36 | default=DEFAULT_BASEURL, 37 | ) 38 | parser.add_option( 39 | "--username", 40 | "-u", 41 | dest="username", 42 | help="Username for jenkins authentification", 43 | type="str", 44 | default=None, 45 | ) 46 | parser.add_option( 47 | "--password", 48 | "-p", 49 | dest="password", 50 | help="password for jenkins user auth", 51 | type="str", 52 | default=None, 53 | ) 54 | parser.add_option( 55 | "-b", 56 | "--block", 57 | dest="block", 58 | action="store_true", 59 | default=False, 60 | help="Block until each of the jobs is complete.", 61 | ) 62 | parser.add_option( 63 | "-t", 64 | "--token", 65 | dest="token", 66 | help="Optional security token.", 67 | default=None, 68 | ) 69 | return parser 70 | 71 | @classmethod 72 | def main(cls): 73 | parser = cls.mkparser() 74 | options, args = parser.parse_args() 75 | try: 76 | assert args, "Need to specify at least one job name" 77 | except AssertionError as err: 78 | log.critical(err.message) 79 | parser.print_help() 80 | sys.exit(1) 81 | invoker = cls(options, args) 82 | invoker() 83 | 84 | def __init__(self, options, jobs): 85 | self.options = options 86 | self.jobs = jobs 87 | self.api = self._get_api( 88 | baseurl=options.baseurl, 89 | username=options.username, 90 | password=options.password, 91 | ) 92 | 93 | def _get_api(self, baseurl, username, password): 94 | return jenkins.Jenkins(baseurl, username, password) 95 | 96 | def __call__(self): 97 | for job in self.jobs: 98 | self.invokejob( 99 | job, block=self.options.block, token=self.options.token 100 | ) 101 | 102 | def invokejob(self, jobname, block, token): 103 | assert isinstance(block, bool) 104 | assert isinstance(jobname, str) 105 | assert token is None or isinstance(token, str) 106 | job = self.api.get_job(jobname) 107 | job.invoke(securitytoken=token, block=block) 108 | 109 | 110 | def main(): 111 | logging.basicConfig() 112 | logging.getLogger("").setLevel(logging.INFO) 113 | JenkinsInvoke.main() 114 | -------------------------------------------------------------------------------- /jenkinsapi/command_line/jenkinsapi_version.py: -------------------------------------------------------------------------------- 1 | """ jenkinsapi.command_line.jenkinsapi_version 2 | """ 3 | 4 | import jenkinsapi 5 | import sys 6 | 7 | 8 | def main(): 9 | sys.stdout.write(jenkinsapi.__version__) 10 | 11 | 12 | if __name__ == "__main__": 13 | main() 14 | -------------------------------------------------------------------------------- /jenkinsapi/config.py: -------------------------------------------------------------------------------- 1 | """ 2 | Jenkins configuration 3 | """ 4 | 5 | JENKINS_API = r"api/python" 6 | 7 | LOAD_TIMEOUT = 30 8 | -------------------------------------------------------------------------------- /jenkinsapi/constants.py: -------------------------------------------------------------------------------- 1 | """ 2 | Constants for jenkinsapi 3 | """ 4 | 5 | import re 6 | 7 | STATUS_FAIL = "FAIL" 8 | STATUS_ERROR = "ERROR" 9 | STATUS_ABORTED = "ABORTED" 10 | STATUS_REGRESSION = "REGRESSION" 11 | STATUS_SUCCESS = "SUCCESS" 12 | 13 | STATUS_FIXED = "FIXED" 14 | STATUS_PASSED = "PASSED" 15 | 16 | RESULTSTATUS_FAILURE = "FAILURE" 17 | RESULTSTATUS_FAILED = "FAILED" 18 | RESULTSTATUS_SKIPPED = "SKIPPED" 19 | 20 | STR_RE_SPLIT_VIEW = "(.*)/view/([^/]*)/?" 21 | RE_SPLIT_VIEW_URL = re.compile(STR_RE_SPLIT_VIEW) 22 | -------------------------------------------------------------------------------- /jenkinsapi/custom_exceptions.py: -------------------------------------------------------------------------------- 1 | """Module for custom_exceptions. 2 | 3 | Where possible we try to throw exceptions with non-generic, 4 | meaningful names. 5 | """ 6 | 7 | 8 | class JenkinsAPIException(Exception): 9 | """Base class for all errors""" 10 | 11 | pass 12 | 13 | 14 | class NotFound(JenkinsAPIException): 15 | """Resource cannot be found""" 16 | 17 | pass 18 | 19 | 20 | class ArtifactsMissing(NotFound): 21 | """Cannot find a build with all of the required artifacts.""" 22 | 23 | pass 24 | 25 | 26 | class UnknownJob(KeyError, NotFound): 27 | """Jenkins does not recognize the job requested.""" 28 | 29 | pass 30 | 31 | 32 | class UnknownView(KeyError, NotFound): 33 | """Jenkins does not recognize the view requested.""" 34 | 35 | pass 36 | 37 | 38 | class UnknownNode(KeyError, NotFound): 39 | """Jenkins does not recognize the node requested.""" 40 | 41 | pass 42 | 43 | 44 | class UnknownQueueItem(KeyError, NotFound): 45 | """Jenkins does not recognize the requested queue item""" 46 | 47 | pass 48 | 49 | 50 | class UnknownPlugin(KeyError, NotFound): 51 | """Jenkins does not recognize the plugin requested.""" 52 | 53 | pass 54 | 55 | 56 | class NoBuildData(NotFound): 57 | """A job has no build data.""" 58 | 59 | pass 60 | 61 | 62 | class NotBuiltYet(NotFound): 63 | """A job has no build data.""" 64 | 65 | pass 66 | 67 | 68 | class ArtifactBroken(JenkinsAPIException): 69 | """An artifact is broken, wrong""" 70 | 71 | pass 72 | 73 | 74 | class TimeOut(JenkinsAPIException): 75 | """Some jobs have taken too long to complete.""" 76 | 77 | pass 78 | 79 | 80 | class NoResults(JenkinsAPIException): 81 | """A build did not publish any results.""" 82 | 83 | pass 84 | 85 | 86 | class FailedNoResults(NoResults): 87 | """A build did not publish any results because it failed""" 88 | 89 | pass 90 | 91 | 92 | class BadURL(ValueError, JenkinsAPIException): 93 | """A URL appears to be broken""" 94 | 95 | pass 96 | 97 | 98 | class NotAuthorized(JenkinsAPIException): 99 | """Not Authorized to access resource""" 100 | 101 | # Usually thrown when we get a 403 returned 102 | pass 103 | 104 | 105 | class NotSupportSCM(JenkinsAPIException): 106 | """ 107 | It's a SCM that does not supported by current version of jenkinsapi 108 | """ 109 | 110 | pass 111 | 112 | 113 | class NotConfiguredSCM(JenkinsAPIException): 114 | """It's a job that doesn't have configured SCM""" 115 | 116 | pass 117 | 118 | 119 | class NotInQueue(JenkinsAPIException): 120 | """It's a job that is not in the queue""" 121 | 122 | pass 123 | 124 | 125 | class PostRequired(JenkinsAPIException): 126 | """Method requires POST and not GET""" 127 | 128 | pass 129 | 130 | 131 | class BadParams(JenkinsAPIException): 132 | """Invocation was given bad or inappropriate params""" 133 | 134 | pass 135 | 136 | 137 | class AlreadyExists(JenkinsAPIException): 138 | """ 139 | Method requires POST and not GET 140 | """ 141 | 142 | pass 143 | -------------------------------------------------------------------------------- /jenkinsapi/executor.py: -------------------------------------------------------------------------------- 1 | """ 2 | Module for jenkinsapi Executer class 3 | """ 4 | 5 | from __future__ import annotations 6 | 7 | from jenkinsapi.jenkinsbase import JenkinsBase 8 | import logging 9 | 10 | log = logging.getLogger(__name__) 11 | 12 | 13 | class Executor(JenkinsBase): 14 | """ 15 | Class to hold information on nodes that are attached as slaves to the 16 | master jenkins instance 17 | """ 18 | 19 | def __init__( 20 | self, baseurl: str, nodename: str, jenkins_obj: "Jenkins", number: int 21 | ) -> None: 22 | """ 23 | Init a node object by providing all relevant pointers to it 24 | :param baseurl: basic url for querying information on a node 25 | :param nodename: hostname of the node 26 | :param jenkins_obj: ref to the jenkins obj 27 | :return: Node obj 28 | """ 29 | self.nodename: str = nodename 30 | self.number: int = number 31 | self.jenkins: "Jenkins" = jenkins_obj 32 | self.baseurl: str = baseurl 33 | JenkinsBase.__init__(self, baseurl) 34 | 35 | def __str__(self) -> str: 36 | return f"{self.nodename} {self.number}" 37 | 38 | def get_jenkins_obj(self) -> "Jenkins": 39 | return self.jenkins 40 | 41 | def get_progress(self) -> str: 42 | """Returns percentage""" 43 | return self.poll(tree="progress")["progress"] 44 | 45 | def get_number(self) -> int: 46 | """ 47 | Get Executor number. 48 | """ 49 | return self.poll(tree="number")["number"] 50 | 51 | def is_idle(self) -> bool: 52 | """ 53 | Returns Boolean: whether Executor is idle or not. 54 | """ 55 | return self.poll(tree="idle")["idle"] 56 | 57 | def likely_stuck(self) -> bool: 58 | """ 59 | Returns Boolean: whether Executor is likely stuck or not. 60 | """ 61 | return self.poll(tree="likelyStuck")["likelyStuck"] 62 | 63 | def get_current_executable(self) -> str: 64 | """ 65 | Returns the current Queue.Task this executor is running. 66 | """ 67 | return self.poll(tree="currentExecutable")["currentExecutable"] 68 | -------------------------------------------------------------------------------- /jenkinsapi/executors.py: -------------------------------------------------------------------------------- 1 | """ 2 | This module implements the Executors class, which is intended to be a 3 | container-like interface for all of the executors defined on a single 4 | Jenkins node. 5 | """ 6 | 7 | from __future__ import annotations 8 | 9 | import logging 10 | from typing import Iterator 11 | 12 | from jenkinsapi.executor import Executor 13 | from jenkinsapi.jenkinsbase import JenkinsBase 14 | 15 | log: logging.Logger = logging.getLogger(__name__) 16 | 17 | 18 | class Executors(JenkinsBase): 19 | """ 20 | This class provides a container-like API which gives 21 | access to all executors on a Jenkins node. 22 | 23 | Returns a list of Executor Objects. 24 | """ 25 | 26 | def __init__( 27 | self, baseurl: str, nodename: str, jenkins: "Jenkins" 28 | ) -> None: 29 | self.nodename: str = nodename 30 | self.jenkins: str = jenkins 31 | JenkinsBase.__init__(self, baseurl) 32 | self.count: int = self._data["numExecutors"] 33 | 34 | def __str__(self) -> str: 35 | return f"Executors @ {self.baseurl}" 36 | 37 | def get_jenkins_obj(self) -> "Jenkins": 38 | return self.jenkins 39 | 40 | def __iter__(self) -> Iterator[Executor]: 41 | for index in range(self.count): 42 | executor_url = "%s/executors/%s" % (self.baseurl, index) 43 | yield Executor(executor_url, self.nodename, self.jenkins, index) 44 | -------------------------------------------------------------------------------- /jenkinsapi/fingerprint.py: -------------------------------------------------------------------------------- 1 | """ 2 | Module for jenkinsapi Fingerprint 3 | """ 4 | 5 | from __future__ import annotations 6 | 7 | import re 8 | import logging 9 | from typing import Any 10 | 11 | import requests 12 | 13 | from jenkinsapi.jenkinsbase import JenkinsBase 14 | from jenkinsapi.custom_exceptions import ArtifactBroken 15 | 16 | log: logging.Logger = logging.getLogger(__name__) 17 | 18 | 19 | class Fingerprint(JenkinsBase): 20 | """ 21 | Represents a jenkins fingerprint on a single artifact file ?? 22 | """ 23 | 24 | RE_MD5 = re.compile("^([0-9a-z]{32})$") 25 | 26 | def __init__(self, baseurl: str, id_: str, jenkins_obj: "Jenkins") -> None: 27 | self.jenkins_obj: "Jenkins" = jenkins_obj 28 | assert self.RE_MD5.search(id_), ( 29 | "%s does not look like " "a valid id" % id_ 30 | ) 31 | url: str = f"{baseurl}/fingerprint/{id_}/" 32 | 33 | JenkinsBase.__init__(self, url, poll=False) 34 | self.id_: str = id_ 35 | self.unknown: bool = False # Previously uninitialized in ctor 36 | 37 | def get_jenkins_obj(self) -> "Jenkins": 38 | return self.jenkins_obj 39 | 40 | def __str__(self) -> str: 41 | return self.id_ 42 | 43 | def valid(self) -> bool: 44 | """ 45 | Return True / False if valid. If returns True, self.unknown is 46 | set to either True or False, and can be checked if we have 47 | positive validity (fingerprint known at server) or negative 48 | validity (fingerprint not known at server, but not really an 49 | error). 50 | """ 51 | try: 52 | self.poll() 53 | self.unknown = False 54 | except requests.exceptions.HTTPError as err: 55 | # We can't really say anything about the validity of 56 | # fingerprints not found -- but the artifact can still 57 | # exist, so it is not possible to definitely say they are 58 | # valid or not. 59 | # The response object is of type: requests.models.Response 60 | # extract the status code from it 61 | response_obj: Any = err.response 62 | if response_obj.status_code == 404: 63 | logging.warning( 64 | "MD5 cannot be checked if fingerprints are not enabled" 65 | ) 66 | self.unknown = True 67 | return True 68 | 69 | return False 70 | 71 | return True 72 | 73 | def validate_for_build(self, filename: str, job: str, build: int) -> bool: 74 | if not self.valid(): 75 | log.info("Fingerprint is not known to jenkins.") 76 | return False 77 | if self.unknown: 78 | # not request error, but unknown to jenkins 79 | return True 80 | if self._data["original"] is not None: 81 | if self._data["original"]["name"] == job: 82 | if self._data["original"]["number"] == build: 83 | return True 84 | if self._data["fileName"] != filename: 85 | log.info( 86 | msg="Filename from jenkins (%s) did not match provided (%s)" 87 | % (self._data["fileName"], filename) 88 | ) 89 | return False 90 | for usage_item in self._data["usage"]: 91 | if usage_item["name"] == job: 92 | for range_ in usage_item["ranges"]["ranges"]: 93 | if range_["start"] <= build <= range_["end"]: 94 | msg = ( 95 | "This artifact was generated by %s " 96 | "between build %i and %i" 97 | % ( 98 | job, 99 | range_["start"], 100 | range_["end"], 101 | ) 102 | ) 103 | log.info(msg=msg) 104 | return True 105 | return False 106 | 107 | def validate(self) -> bool: 108 | try: 109 | assert self.valid() 110 | except AssertionError as ae: 111 | raise ArtifactBroken( 112 | "Artifact %s seems to be broken, check %s" 113 | % (self.id_, self.baseurl) 114 | ) from ae 115 | except requests.exceptions.HTTPError: 116 | raise ArtifactBroken( 117 | "Unable to validate artifact id %s using %s" 118 | % (self.id_, self.baseurl) 119 | ) 120 | return True 121 | 122 | def get_info(self): 123 | """ 124 | Returns a tuple of build-name, build# and artifact filename 125 | for a good build. 126 | """ 127 | self.poll() 128 | return ( 129 | self._data["original"]["name"], 130 | self._data["original"]["number"], 131 | self._data["fileName"], 132 | ) 133 | -------------------------------------------------------------------------------- /jenkinsapi/jenkinsbase.py: -------------------------------------------------------------------------------- 1 | """ 2 | Module for JenkinsBase class 3 | """ 4 | 5 | from __future__ import annotations 6 | 7 | import ast 8 | import pprint 9 | import logging 10 | from urllib.parse import quote 11 | from jenkinsapi import config 12 | from jenkinsapi.custom_exceptions import JenkinsAPIException 13 | 14 | logger = logging.getLogger(__name__) 15 | 16 | 17 | class JenkinsBase(object): 18 | """ 19 | This appears to be the base object that all other jenkins objects are 20 | inherited from 21 | """ 22 | 23 | def __repr__(self): 24 | return """<%s.%s %s>""" % ( 25 | self.__class__.__module__, 26 | self.__class__.__name__, 27 | str(self), 28 | ) 29 | 30 | def __str__(self): 31 | raise NotImplementedError 32 | 33 | def __init__(self, baseurl: str, poll: bool = True): 34 | """ 35 | Initialize a jenkins connection 36 | """ 37 | self._data = None 38 | self.baseurl = self.strip_trailing_slash(baseurl) 39 | if poll: 40 | self.poll() 41 | 42 | def get_jenkins_obj(self): 43 | raise NotImplementedError( 44 | "Please implement this method on %s" % self.__class__.__name__ 45 | ) 46 | 47 | def __eq__(self, other) -> bool: 48 | """ 49 | Return true if the other object represents a connection to the 50 | same server 51 | """ 52 | if not isinstance(other, self.__class__): 53 | return False 54 | return other.baseurl == self.baseurl 55 | 56 | @classmethod 57 | def strip_trailing_slash(cls, url: str) -> str: 58 | while url.endswith("/"): 59 | url = url[:-1] 60 | return url 61 | 62 | def poll(self, tree=None): 63 | data = self._poll(tree=tree) 64 | if "jobs" in data: 65 | data["jobs"] = self.resolve_job_folders(data["jobs"]) 66 | if not tree: 67 | self._data = data 68 | 69 | return data 70 | 71 | def _poll(self, tree=None): 72 | url = self.python_api_url(self.baseurl) 73 | return self.get_data(url, tree=tree) 74 | 75 | def get_data(self, url, params=None, tree=None): 76 | requester = self.get_jenkins_obj().requester 77 | if tree: 78 | if not params: 79 | params = {"tree": tree} 80 | else: 81 | params.update({"tree": tree}) 82 | 83 | response = requester.get_url(url, params) 84 | if response.status_code != 200: 85 | logger.error( 86 | "Failed request at %s with params: %s %s", 87 | url, 88 | params, 89 | tree if tree else "", 90 | ) 91 | response.raise_for_status() 92 | try: 93 | return ast.literal_eval(response.text) 94 | except Exception: 95 | logger.exception("Inappropriate content found at %s", url) 96 | raise JenkinsAPIException("Cannot parse %s" % response.content) 97 | 98 | def pprint(self): 99 | """ 100 | Print out all the data in this object for debugging. 101 | """ 102 | pprint.pprint(self._data) 103 | 104 | def resolve_job_folders(self, jobs): 105 | for job in list(jobs): 106 | if "color" not in job.keys(): 107 | jobs.remove(job) 108 | jobs += self.process_job_folder(job, self.baseurl) 109 | 110 | return jobs 111 | 112 | def process_job_folder(self, folder, folder_path): 113 | logger.debug("Processing folder %s in %s", folder["name"], folder_path) 114 | folder_path += "/job/%s" % quote(folder["name"]) 115 | data = self.get_data( 116 | self.python_api_url(folder_path), tree="jobs[name,color]" 117 | ) 118 | 119 | result = [] 120 | for job in data.get("jobs", []): 121 | if "color" not in job.keys(): 122 | result += self.process_job_folder(job, folder_path) 123 | else: 124 | job["url"] = "%s/job/%s" % (folder_path, quote(job["name"])) 125 | result.append(job) 126 | 127 | return result 128 | 129 | @classmethod 130 | def python_api_url(cls, url: str) -> str: 131 | if url.endswith(config.JENKINS_API): 132 | return url 133 | else: 134 | if url.endswith(r"/"): 135 | fmt = "%s%s" 136 | else: 137 | fmt = "%s/%s" 138 | return fmt % (url, config.JENKINS_API) 139 | -------------------------------------------------------------------------------- /jenkinsapi/label.py: -------------------------------------------------------------------------------- 1 | """ 2 | Module for jenkinsapi labels 3 | """ 4 | 5 | from jenkinsapi.jenkinsbase import JenkinsBase 6 | import logging 7 | 8 | log = logging.getLogger(__name__) 9 | 10 | 11 | class Label(JenkinsBase): 12 | """ 13 | Class to hold information on labels that tied to a collection of jobs 14 | """ 15 | 16 | def __init__(self, baseurl, labelname, jenkins_obj): 17 | """ 18 | Init a label object by providing all relevant pointers to it 19 | :param baseurl: basic url for querying information on a node 20 | :param labelname: name of the label 21 | :param jenkins_obj: ref to the jenkins obj 22 | :return: Label obj 23 | """ 24 | self.labelname = labelname 25 | self.jenkins = jenkins_obj 26 | self.baseurl = baseurl 27 | JenkinsBase.__init__(self, baseurl) 28 | 29 | def __str__(self): 30 | return "%s" % (self.labelname) 31 | 32 | def get_jenkins_obj(self): 33 | return self.jenkins 34 | 35 | def is_online(self): 36 | return not self.poll(tree="offline")["offline"] 37 | 38 | def get_tied_jobs(self): 39 | """ 40 | Get a list of jobs. 41 | """ 42 | if self.get_tied_job_names(): 43 | for job in self.get_tied_job_names(): 44 | yield self.get_jenkins_obj().get_job(job["name"]) 45 | 46 | def get_tied_job_names(self): 47 | """ 48 | Get a list of the name of tied jobs. 49 | """ 50 | return self.poll(tree="tiedJobs[name]")["tiedJobs"] 51 | -------------------------------------------------------------------------------- /jenkinsapi/mutable_jenkins_thing.py: -------------------------------------------------------------------------------- 1 | """ 2 | Module for MutableJenkinsThing 3 | """ 4 | 5 | 6 | class MutableJenkinsThing(object): 7 | """ 8 | A mixin for certain mutable objects which can be renamed and deleted. 9 | """ 10 | 11 | def get_delete_url(self) -> str: 12 | return f"{self.baseurl}/doDelete" 13 | 14 | def get_rename_url(self) -> str: 15 | return f"{self.baseurl}/doRename" 16 | -------------------------------------------------------------------------------- /jenkinsapi/plugin.py: -------------------------------------------------------------------------------- 1 | """ 2 | Module for jenkinsapi Plugin 3 | """ 4 | 5 | from __future__ import annotations 6 | 7 | from typing import Union 8 | 9 | 10 | class Plugin(object): 11 | """ 12 | Plugin class 13 | """ 14 | 15 | def __init__(self, plugin_dict: Union[dict, str]) -> None: 16 | if isinstance(plugin_dict, dict): 17 | self.__dict__ = plugin_dict 18 | else: 19 | self.__dict__ = self.to_plugin(plugin_dict) 20 | self.shortName: str = self.__dict__["shortName"] 21 | self.version: str = self.__dict__.get("version", "Unknown") 22 | 23 | def to_plugin(self, plugin_string: str) -> dict: 24 | plugin_string = str(plugin_string) 25 | if "@" not in plugin_string or len(plugin_string.split("@")) != 2: 26 | usage_err: str = ( 27 | "plugin specification must be a string like " 28 | '"plugin-name@version", not "{0}"' 29 | ) 30 | usage_err = usage_err.format(plugin_string) 31 | raise ValueError(usage_err) 32 | 33 | shortName, version = plugin_string.split("@") 34 | return {"shortName": shortName, "version": version} 35 | 36 | def __eq__(self, other) -> bool: 37 | return self.__dict__ == other.__dict__ 38 | 39 | def __str__(self) -> str: 40 | return self.shortName 41 | 42 | def __repr__(self) -> str: 43 | return "<%s.%s %s>" % ( 44 | self.__class__.__module__, 45 | self.__class__.__name__, 46 | str(self), 47 | ) 48 | 49 | def get_attributes(self) -> str: 50 | """ 51 | Used by Plugins object to install plugins in Jenkins 52 | """ 53 | return ' ' % ( 54 | self.shortName, 55 | self.version, 56 | ) 57 | 58 | def is_latest(self, update_center_dict: dict) -> bool: 59 | """ 60 | Used by Plugins object to determine if plugin can be 61 | installed through the update center (when plugin version is 62 | latest version), or must be installed by uploading 63 | the plugin hpi file. 64 | """ 65 | if self.version == "latest": 66 | return True 67 | center_plugin = update_center_dict["plugins"][self.shortName] 68 | return center_plugin["version"] == self.version 69 | 70 | def get_download_link(self, update_center_dict) -> str: 71 | latest_version = update_center_dict["plugins"][self.shortName][ 72 | "version" 73 | ] 74 | latest_url = update_center_dict["plugins"][self.shortName]["url"] 75 | return latest_url.replace( 76 | "/".join((self.shortName, latest_version)), 77 | "/".join((self.shortName, self.version)), 78 | ) 79 | -------------------------------------------------------------------------------- /jenkinsapi/queue.py: -------------------------------------------------------------------------------- 1 | """ 2 | Queue module for jenkinsapi 3 | """ 4 | 5 | from __future__ import annotations 6 | 7 | from typing import Iterator, Tuple 8 | import logging 9 | import time 10 | from requests import HTTPError 11 | from jenkinsapi.jenkinsbase import JenkinsBase 12 | from jenkinsapi.custom_exceptions import UnknownQueueItem, NotBuiltYet 13 | 14 | log: logging.Logger = logging.getLogger(__name__) 15 | 16 | 17 | class Queue(JenkinsBase): 18 | """ 19 | Class that represents the Jenkins queue 20 | """ 21 | 22 | def __init__(self, baseurl: str, jenkins_obj: "Jenkins") -> None: 23 | """ 24 | Init the Jenkins queue object 25 | :param baseurl: basic url for the queue 26 | :param jenkins_obj: ref to the jenkins obj 27 | """ 28 | self.jenkins: "Jenkins" = jenkins_obj 29 | JenkinsBase.__init__(self, baseurl) 30 | 31 | def __str__(self) -> str: 32 | return self.baseurl 33 | 34 | def get_jenkins_obj(self) -> "Jenkins": 35 | return self.jenkins 36 | 37 | def iteritems(self) -> Iterator[Tuple[str, "QueueItem"]]: 38 | for item in self._data["items"]: 39 | queue_id = item["id"] 40 | item_baseurl = "%s/item/%i" % (self.baseurl, queue_id) 41 | yield item["id"], QueueItem( 42 | baseurl=item_baseurl, jenkins_obj=self.jenkins 43 | ) 44 | 45 | def iterkeys(self) -> Iterator[str]: 46 | for item in self._data["items"]: 47 | yield item["id"] 48 | 49 | def itervalues(self) -> Iterator["QueueItem"]: 50 | for item in self._data["items"]: 51 | yield QueueItem(self.jenkins, **item) 52 | 53 | def keys(self) -> list[str]: 54 | return list(self.iterkeys()) 55 | 56 | def values(self) -> list["QueueItem"]: 57 | return list(self.itervalues()) 58 | 59 | def __len__(self) -> int: 60 | return len(self._data["items"]) 61 | 62 | def __getitem__(self, item_id: str) -> "QueueItem": 63 | self_as_dict = dict(self.iteritems()) 64 | if item_id in self_as_dict: 65 | return self_as_dict[item_id] 66 | else: 67 | raise UnknownQueueItem(item_id) 68 | 69 | def _get_queue_items_for_job(self, job_name: str) -> Iterator["QueueItem"]: 70 | for item in self._data["items"]: 71 | if "name" in item["task"] and item["task"]["name"] == job_name: 72 | yield QueueItem( 73 | self.get_queue_item_url(item), jenkins_obj=self.jenkins 74 | ) 75 | 76 | def get_queue_items_for_job(self, job_name: str): 77 | return list(self._get_queue_items_for_job(job_name)) 78 | 79 | def get_queue_item_url(self, item: str) -> str: 80 | return "%s/item/%i" % (self.baseurl, item["id"]) 81 | 82 | def delete_item(self, queue_item: "QueueItem"): 83 | self.delete_item_by_id(queue_item.queue_id) 84 | 85 | def delete_item_by_id(self, item_id: str): 86 | deleteurl: str = "%s/cancelItem?id=%s" % (self.baseurl, item_id) 87 | self.get_jenkins_obj().requester.post_url(deleteurl) 88 | 89 | 90 | class QueueItem(JenkinsBase): 91 | """An individual item in the queue""" 92 | 93 | def __init__(self, baseurl: str, jenkins_obj: "Jenkins") -> None: 94 | self.jenkins: "Jenkins" = jenkins_obj 95 | JenkinsBase.__init__(self, baseurl) 96 | 97 | @property 98 | def queue_id(self): 99 | return self._data["id"] 100 | 101 | @property 102 | def name(self): 103 | return self._data["task"]["name"] 104 | 105 | @property 106 | def why(self): 107 | return self._data.get("why") 108 | 109 | def get_jenkins_obj(self) -> "Jenkins": 110 | return self.jenkins 111 | 112 | def get_job(self) -> "Job": 113 | """ 114 | Return the job associated with this queue item 115 | """ 116 | return self.jenkins.get_job_by_url( 117 | self._data["task"]["url"], 118 | self._data["task"]["name"], 119 | ) 120 | 121 | def get_parameters(self): 122 | """returns parameters of queue item""" 123 | actions = self._data.get("actions", []) 124 | for action in actions: 125 | if isinstance(action, dict) and "parameters" in action: 126 | parameters = action["parameters"] 127 | return dict( 128 | [(x["name"], x.get("value", None)) for x in parameters] 129 | ) 130 | return [] 131 | 132 | def __repr__(self) -> str: 133 | return "<%s.%s %s>" % ( 134 | self.__class__.__module__, 135 | self.__class__.__name__, 136 | str(self), 137 | ) 138 | 139 | def __str__(self) -> str: 140 | return "%s Queue #%i" % (self.name, self.queue_id) 141 | 142 | def get_build(self) -> "Build": 143 | build_number = self.get_build_number() 144 | job = self.get_job() 145 | return job[build_number] 146 | 147 | def block_until_complete(self, delay=5): 148 | build = self.block_until_building(delay) 149 | return build.block_until_complete(delay=delay) 150 | 151 | def block_until_building(self, delay=5): 152 | while True: 153 | try: 154 | self.poll() 155 | return self.get_build() 156 | except NotBuiltYet: 157 | time.sleep(delay) 158 | continue 159 | except HTTPError as http_error: 160 | log.debug(str(http_error)) 161 | time.sleep(delay) 162 | continue 163 | 164 | def is_running(self) -> bool: 165 | """Return True if this queued item is running.""" 166 | try: 167 | return self.get_build().is_running() 168 | except NotBuiltYet: 169 | return False 170 | 171 | def is_queued(self) -> bool: 172 | """Return True if this queued item is queued.""" 173 | try: 174 | self.get_build() 175 | except NotBuiltYet: 176 | return True 177 | else: 178 | return False 179 | 180 | def get_build_number(self) -> int: 181 | try: 182 | return self._data["executable"]["number"] 183 | except (KeyError, TypeError): 184 | raise NotBuiltYet() 185 | 186 | def get_job_name(self) -> str: 187 | try: 188 | return self._data["task"]["name"] 189 | except KeyError: 190 | raise NotBuiltYet() 191 | -------------------------------------------------------------------------------- /jenkinsapi/result.py: -------------------------------------------------------------------------------- 1 | """ 2 | Module for jenkinsapi Result 3 | """ 4 | 5 | 6 | class Result(object): 7 | """ 8 | Result class 9 | """ 10 | 11 | def __init__(self, **kwargs): 12 | self.__dict__.update(kwargs) 13 | 14 | def __str__(self): 15 | return f"{self.className} {self.name} {self.status}" 16 | 17 | def __repr__(self) -> str: 18 | module_name = self.__class__.__module__ 19 | class_name = self.__class__.__name__ 20 | self_str = str(self) 21 | return "<%s.%s %s>" % (module_name, class_name, self_str) 22 | 23 | def identifier(self) -> str: 24 | """ 25 | Calculate an ID for this object. 26 | """ 27 | return f"{self.className}.{self.name}" 28 | -------------------------------------------------------------------------------- /jenkinsapi/result_set.py: -------------------------------------------------------------------------------- 1 | """ 2 | Module for jenkinsapi ResultSet 3 | """ 4 | 5 | from __future__ import annotations 6 | 7 | from jenkinsapi.jenkinsbase import JenkinsBase 8 | from jenkinsapi.result import Result 9 | 10 | 11 | class ResultSet(JenkinsBase): 12 | """ 13 | Represents a result from a completed Jenkins run. 14 | """ 15 | 16 | def __init__(self, url: str, build: "Build") -> None: 17 | """ 18 | Init a resultset 19 | :param url: url for a build, str 20 | :param build: build obj 21 | """ 22 | self.build: "Build" = build 23 | JenkinsBase.__init__(self, url) 24 | 25 | def get_jenkins_obj(self) -> "Jenkins": 26 | return self.build.job.get_jenkins_obj() 27 | 28 | def __str__(self) -> str: 29 | return "Test Result for %s" % str(self.build) 30 | 31 | @property 32 | def name(self): 33 | return str(self) 34 | 35 | def keys(self) -> list[str]: 36 | return [a[0] for a in self.iteritems()] 37 | 38 | def items(self): 39 | return [a for a in self.iteritems()] 40 | 41 | def iteritems(self): 42 | for suite in self._data.get("suites", []): 43 | for case in suite["cases"]: 44 | result = Result(**case) 45 | yield result.identifier(), result 46 | 47 | for report_set in self._data.get("childReports", []): 48 | if report_set["result"]: 49 | for suite in report_set["result"]["suites"]: 50 | for case in suite["cases"]: 51 | result = Result(**case) 52 | yield result.identifier(), result 53 | 54 | def __len__(self): 55 | return len(self.items()) 56 | 57 | def __getitem__(self, key): 58 | self_as_dict = dict(self.iteritems()) 59 | return self_as_dict[key] 60 | -------------------------------------------------------------------------------- /jenkinsapi/utils/__init__.py: -------------------------------------------------------------------------------- 1 | """ 2 | Module __init__ for utils 3 | """ 4 | -------------------------------------------------------------------------------- /jenkinsapi/utils/crumb_requester.py: -------------------------------------------------------------------------------- 1 | # Code from https://github.com/ros-infrastructure/ros_buildfarm 2 | # (c) Open Source Robotics Foundation 3 | import ast 4 | import logging 5 | from jenkinsapi.utils.requester import Requester 6 | 7 | logger = logging.getLogger(__name__) 8 | 9 | 10 | class CrumbRequester(Requester): 11 | """Adapter for Requester inserting the crumb in every request.""" 12 | 13 | def __init__(self, *args, **kwargs): 14 | super(CrumbRequester, self).__init__(*args, **kwargs) 15 | self._baseurl = kwargs["baseurl"] 16 | self._last_crumb_data = None 17 | 18 | def post_url( 19 | self, 20 | url, 21 | params=None, 22 | data=None, 23 | files=None, 24 | headers=None, 25 | allow_redirects=True, 26 | **kwargs, 27 | ): 28 | if self._last_crumb_data: 29 | # first try request with previous crumb if available 30 | response = self._post_url_with_crumb( 31 | self._last_crumb_data, 32 | url, 33 | params, 34 | data, 35 | files, 36 | headers, 37 | allow_redirects, 38 | **kwargs, 39 | ) 40 | # code 403 might indicate that the crumb is not valid anymore 41 | if response.status_code != 403: 42 | return response 43 | 44 | # fetch new crumb (if server has crumbs enabled) 45 | if self._last_crumb_data is not False: 46 | self._last_crumb_data = self._get_crumb_data() 47 | 48 | return self._post_url_with_crumb( 49 | self._last_crumb_data, 50 | url, 51 | params, 52 | data, 53 | files, 54 | headers, 55 | allow_redirects, 56 | **kwargs, 57 | ) 58 | 59 | def _get_crumb_data(self): 60 | response = self.get_url(self._baseurl + "/crumbIssuer/api/python") 61 | if response.status_code in [404]: 62 | logger.warning("The Jenkins master does not require a crumb") 63 | return False 64 | if response.status_code not in [200]: 65 | raise RuntimeError("Failed to fetch crumb: %s" % response.text) 66 | crumb_issuer_response = ast.literal_eval(response.text) 67 | crumb_request_field = crumb_issuer_response["crumbRequestField"] 68 | crumb = crumb_issuer_response["crumb"] 69 | logger.debug("Fetched crumb: %s", crumb) 70 | return {crumb_request_field: crumb} 71 | 72 | def _post_url_with_crumb( 73 | self, 74 | crumb_data, 75 | url, 76 | params, 77 | data, 78 | files, 79 | headers, 80 | allow_redirects, 81 | **kwargs, 82 | ): 83 | if crumb_data: 84 | if headers is None: 85 | headers = crumb_data 86 | else: 87 | headers.update(crumb_data) 88 | 89 | return super(CrumbRequester, self).post_url( 90 | url, params, data, files, headers, allow_redirects, **kwargs 91 | ) 92 | -------------------------------------------------------------------------------- /jenkinsapi/utils/jsonp_to_json.py: -------------------------------------------------------------------------------- 1 | """ 2 | Module for converting jsonp to json. 3 | """ 4 | 5 | 6 | def jsonp_to_json(jsonp): 7 | try: 8 | l_index = jsonp.index("(") + 1 9 | r_index = jsonp.rindex(")") 10 | except ValueError: 11 | print("Input is not in jsonp format.") 12 | return None 13 | 14 | res = jsonp[l_index:r_index] 15 | return res 16 | -------------------------------------------------------------------------------- /jenkinsapi/utils/krb_requester.py: -------------------------------------------------------------------------------- 1 | """ 2 | Kerberos aware Requester 3 | """ 4 | 5 | from jenkinsapi.utils.requester import Requester 6 | from requests_kerberos import HTTPKerberosAuth, OPTIONAL 7 | 8 | 9 | # pylint: disable=W0222 10 | class KrbRequester(Requester): 11 | """ 12 | A class which carries out HTTP requests with Kerberos/GSSAPI 13 | authentication. 14 | """ 15 | 16 | def __init__(self, *args, **kwargs): 17 | """ 18 | :param ssl_verify: flag indicating if server certificate 19 | in HTTPS requests should be verified 20 | :param baseurl: Jenkins' base URL 21 | :param mutual_auth: type of mutual authentication, use one of 22 | REQUIRED, OPTIONAL or DISABLED 23 | from requests_kerberos package 24 | """ 25 | 26 | super(KrbRequester, self).__init__(*args, **kwargs) 27 | self.mutual_auth = ( 28 | kwargs["mutual_auth"] if "mutual_auth" in kwargs else OPTIONAL 29 | ) 30 | 31 | def get_request_dict( 32 | self, params=None, data=None, files=None, headers=None, **kwargs 33 | ): 34 | req_dict = super(KrbRequester, self).get_request_dict( 35 | params=params, data=data, files=files, headers=headers, **kwargs 36 | ) 37 | if self.mutual_auth: 38 | auth = HTTPKerberosAuth(self.mutual_auth) 39 | else: 40 | auth = HTTPKerberosAuth() 41 | 42 | req_dict["auth"] = auth 43 | return req_dict 44 | -------------------------------------------------------------------------------- /jenkinsapi/utils/manifest.py: -------------------------------------------------------------------------------- 1 | """ 2 | This module enables Manifest file parsing. 3 | Copied from 4 | https://chromium.googlesource.com/external/googleappengine/python/+/master 5 | /google/appengine/tools/jarfile.py 6 | """ 7 | 8 | import zipfile 9 | 10 | _MANIFEST_NAME = "META-INF/MANIFEST.MF" 11 | 12 | 13 | class InvalidJarError(Exception): 14 | """ 15 | InvalidJar exception class 16 | """ 17 | 18 | pass 19 | 20 | 21 | class Manifest(object): 22 | """The parsed manifest from a jar file. 23 | Attributes: 24 | main_section: a dict representing the main (first) 25 | section of the manifest. 26 | Each key is a string that is an attribute, such as 27 | 'Manifest-Version', and the corresponding value is a string that 28 | is the value of the attribute, such as '1.0'. 29 | sections: a dict representing the other sections of the manifest. 30 | Each key is a string that is the value of the 'Name' attribute for 31 | the section, and the corresponding value is a dict like the 32 | main_section one, for the other attributes. 33 | """ 34 | 35 | def __init__(self, main_section, sections): 36 | self.main_section = main_section 37 | self.sections = sections 38 | 39 | 40 | def read_manifest(jar_file_name): 41 | """Read and parse the manifest out of the given jar. 42 | Args: 43 | jar_file_name: the name of the jar from which the manifest is to be read. 44 | Returns: 45 | A parsed Manifest object, or None if the jar has no manifest. 46 | Raises: 47 | IOError: if the jar does not exist or cannot be read. 48 | """ 49 | with zipfile.ZipFile(jar_file_name) as jar: 50 | try: 51 | manifest_string = jar.read(_MANIFEST_NAME).decode("UTF-8") 52 | except KeyError: 53 | return None 54 | return _parse_manifest(manifest_string) 55 | 56 | 57 | def _parse_manifest(manifest_string): 58 | """Parse a Manifest object out of the given string. 59 | Args: 60 | manifest_string: a str or unicode that is the manifest contents. 61 | Returns: 62 | A Manifest object parsed out of the string. 63 | Raises: 64 | InvalidJarError: if the manifest is not well-formed. 65 | """ 66 | manifest_string = "\n".join(manifest_string.splitlines()).rstrip("\n") 67 | section_strings = manifest_string.split("\n\n") 68 | parsed_sections = [_parse_manifest_section(s) for s in section_strings] 69 | main_section = parsed_sections[0] 70 | sections = dict() 71 | try: 72 | for entry in parsed_sections[1:]: 73 | sections[entry["Name"]] = entry 74 | except KeyError: 75 | raise InvalidJarError( 76 | "Manifest entry has no Name attribute: %s" % entry 77 | ) 78 | return Manifest(main_section, sections) 79 | 80 | 81 | def _parse_manifest_section(section): 82 | """Parse a dict out of the given manifest section string. 83 | Args: 84 | section: a str or unicode that is the manifest section. 85 | It looks something like this (without the >): 86 | > Name: section-name 87 | > Some-Attribute: some value 88 | > Another-Attribute: another value 89 | Returns: 90 | A dict where the keys are the attributes (here, 'Name', 'Some-Attribute', 91 | 'Another-Attribute'), and the values are the corresponding 92 | attribute values. 93 | Raises: 94 | InvalidJarError: if the manifest section is not well-formed. 95 | """ 96 | section = section.replace("\n ", "") 97 | try: 98 | return dict(line.split(": ", 1) for line in section.split("\n")) 99 | except ValueError: 100 | raise InvalidJarError("Invalid manifest %r" % section) 101 | -------------------------------------------------------------------------------- /jenkinsapi/utils/simple_post_logger.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | 3 | try: 4 | from SimpleHTTPServer import SimpleHTTPRequestHandler 5 | except ImportError: 6 | from http.server import SimpleHTTPRequestHandler 7 | 8 | try: 9 | import SocketServer as socketserver 10 | except ImportError: 11 | import socketserver 12 | 13 | import logging 14 | import cgi 15 | 16 | PORT = 8080 17 | 18 | 19 | class ServerHandler(SimpleHTTPRequestHandler): 20 | def do_GET(self): 21 | logging.error(self.headers) 22 | super().do_GET() 23 | 24 | def do_POST(self): 25 | logging.error(self.headers) 26 | form = cgi.FieldStorage( 27 | fp=self.rfile, 28 | headers=self.headers, 29 | environ={ 30 | "REQUEST_METHOD": "POST", 31 | "CONTENT_TYPE": self.headers["Content-Type"], 32 | }, 33 | ) 34 | for item in form.list: 35 | logging.error(item) 36 | super().do_GET() 37 | 38 | 39 | if __name__ == "__main__": 40 | Handler = ServerHandler 41 | 42 | httpd = socketserver.TCPServer(("", PORT), Handler) 43 | 44 | print("serving at port", PORT) 45 | httpd.serve_forever() 46 | -------------------------------------------------------------------------------- /jenkinsapi/views.py: -------------------------------------------------------------------------------- 1 | """ 2 | Module for jenkinsapi Views 3 | """ 4 | 5 | import logging 6 | import json 7 | from jenkinsapi.view import View 8 | from jenkinsapi.custom_exceptions import JenkinsAPIException 9 | 10 | log = logging.getLogger(__name__) 11 | 12 | 13 | class Views(object): 14 | """ 15 | An abstraction on a Jenkins object's views 16 | """ 17 | 18 | LIST_VIEW = "hudson.model.ListView" 19 | NESTED_VIEW = "hudson.plugins.nested_view.NestedView" 20 | CATEGORIZED_VIEW = ( 21 | "org.jenkinsci.plugins.categorizedview.CategorizedJobsView" 22 | ) 23 | MY_VIEW = "hudson.model.MyView" 24 | DASHBOARD_VIEW = "hudson.plugins.view.dashboard.Dashboard" 25 | PIPELINE_VIEW = ( 26 | "au.com.centrumsystems.hudson." 27 | "plugin.buildpipeline.BuildPipelineView" 28 | ) 29 | 30 | def __init__(self, jenkins): 31 | self.jenkins = jenkins 32 | self._data = None 33 | 34 | def poll(self, tree=None): 35 | self._data = self.jenkins.poll( 36 | tree="views[name,url]" if tree is None else tree 37 | ) 38 | 39 | def __len__(self): 40 | return len(self.keys()) 41 | 42 | def __delitem__(self, view_name): 43 | if view_name == "All": 44 | raise ValueError("Cannot delete this view: %s" % view_name) 45 | 46 | if view_name in self: 47 | self[view_name].delete() 48 | self.poll() 49 | 50 | def __setitem__(self, view_name, job_names_list): 51 | new_view = self.create(view_name) 52 | if isinstance(job_names_list, str): 53 | job_names_list = [job_names_list] 54 | for job_name in job_names_list: 55 | if not new_view.add_job(job_name): 56 | # Something wrong - delete view 57 | del self[new_view] 58 | raise TypeError("Job %s does not exist in Jenkins" % job_name) 59 | 60 | def __getitem__(self, view_name): 61 | self.poll() 62 | for row in self._data.get("views", []): 63 | if row["name"] == view_name: 64 | return View(row["url"], row["name"], self.jenkins) 65 | 66 | raise KeyError("View %s not found" % view_name) 67 | 68 | def iteritems(self): 69 | """ 70 | Get the names & objects for all views 71 | """ 72 | self.poll() 73 | for row in self._data.get("views", []): 74 | name = row["name"] 75 | url = row["url"] 76 | 77 | yield name, View(url, name, self.jenkins) 78 | 79 | def __contains__(self, view_name): 80 | """ 81 | True if view_name is the name of a defined view 82 | """ 83 | return view_name in self.keys() 84 | 85 | def iterkeys(self): 86 | """ 87 | Get the names of all available views 88 | """ 89 | self.poll() 90 | for row in self._data.get("views", []): 91 | yield row["name"] 92 | 93 | def keys(self): 94 | """ 95 | Return a list of the names of all views 96 | """ 97 | return list(self.iterkeys()) 98 | 99 | def create(self, view_name, view_type=LIST_VIEW, config=None): 100 | """ 101 | Create a view 102 | :param view_name: name of new view, str 103 | :param view_type: type of the view, one of the constants in Views, str 104 | :param config: XML configuration of the new view 105 | :return: new View obj or None if view was not created 106 | """ 107 | log.info('Creating "%s" view "%s"', view_type, view_name) 108 | 109 | if view_name in self: 110 | log.warning('View "%s" already exists', view_name) 111 | return self[view_name] 112 | 113 | url = "%s/createView" % self.jenkins.baseurl 114 | 115 | if view_type == self.CATEGORIZED_VIEW: 116 | if not config: 117 | raise JenkinsAPIException( 118 | "Job XML config cannot be empty for CATEGORIZED_VIEW" 119 | ) 120 | 121 | params = {"name": view_name} 122 | 123 | self.jenkins.requester.post_xml_and_confirm_status( 124 | url, data=config, params=params 125 | ) 126 | else: 127 | headers = {"Content-Type": "application/x-www-form-urlencoded"} 128 | data = { 129 | "name": view_name, 130 | "mode": view_type, 131 | "Submit": "OK", 132 | "json": json.dumps({"name": view_name, "mode": view_type}), 133 | } 134 | 135 | self.jenkins.requester.post_and_confirm_status( 136 | url, data=data, headers=headers 137 | ) 138 | 139 | self.poll() 140 | return self[view_name] 141 | -------------------------------------------------------------------------------- /jenkinsapi_tests/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pycontribs/jenkinsapi/ffb35b6dca607b9aa5a65d4e49aa1ca76946adbf/jenkinsapi_tests/__init__.py -------------------------------------------------------------------------------- /jenkinsapi_tests/conftest.py: -------------------------------------------------------------------------------- 1 | import os 2 | import logging 3 | 4 | 5 | logging.basicConfig( 6 | format="%(module)s.%(funcName)s %(levelname)s: %(message)s", 7 | level=logging.INFO, 8 | ) 9 | 10 | level = ( 11 | logging.WARNING 12 | if "LOG_LEVEL" not in os.environ 13 | else os.environ["LOG_LEVEL"].upper().strip() 14 | ) 15 | 16 | modules = [ 17 | "requests.packages.urllib3.connectionpool", 18 | "requests", 19 | "urllib3", 20 | "urllib3.connectionpool", 21 | ] 22 | 23 | for module_name in modules: 24 | logger = logging.getLogger(module_name) 25 | logger.setLevel(level) 26 | -------------------------------------------------------------------------------- /jenkinsapi_tests/systests/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pycontribs/jenkinsapi/ffb35b6dca607b9aa5a65d4e49aa1ca76946adbf/jenkinsapi_tests/systests/__init__.py -------------------------------------------------------------------------------- /jenkinsapi_tests/systests/config.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 1.0 5 | 2 6 | NORMAL 7 | true 8 | 9 | 10 | 11 | ${JENKINS_HOME}/workspace/${ITEM_FULLNAME} 12 | ${ITEM_ROOTDIR}/builds 13 | 14 | 15 | 16 | 17 | 18 | 0 19 | 20 | 21 | 22 | All 23 | false 24 | false 25 | 26 | 27 | 28 | All 29 | 0 30 | 31 | 32 | 33 | 34 | -------------------------------------------------------------------------------- /jenkinsapi_tests/systests/get-jenkins-war.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | #JENKINS_WAR_URL="http://mirrors.jenkins-ci.org/war/latest/jenkins.war" 3 | 4 | if [[ "$#" -ne 3 ]]; then 5 | echo "Usage: $0 jenkins_url path_to_store_jenkins war_filename" 6 | exit 1 7 | fi 8 | 9 | readonly JENKINS_WAR_URL=$1 10 | readonly JENKINS_PATH=$2 11 | readonly WAR_FILENAME=$3 12 | 13 | echo "Downloading $JENKINS_WAR_URL to ${JENKINS_PATH}" 14 | if [[ $(type -t wget) ]]; then wget -O ${JENKINS_PATH}/${WAR_FILENAME} -q $JENKINS_WAR_URL 15 | elif [[ $(type -t curl) ]]; then curl -sSL -o ${JENKINS_PATH}/${WAR_FILENAME} $JENKINS_WAR_URL 16 | else 17 | echo "Could not find wget or curl" 18 | exit 1 19 | fi 20 | 21 | echo "Jenkins downloaded" 22 | -------------------------------------------------------------------------------- /jenkinsapi_tests/systests/jenkins_home.tar.gz: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pycontribs/jenkinsapi/ffb35b6dca607b9aa5a65d4e49aa1ca76946adbf/jenkinsapi_tests/systests/jenkins_home.tar.gz -------------------------------------------------------------------------------- /jenkinsapi_tests/systests/test_authentication.py: -------------------------------------------------------------------------------- 1 | """ 2 | System tests for authentication functionality 3 | """ 4 | 5 | import pytest 6 | from jenkinsapi.utils.requester import Requester 7 | from requests import HTTPError as REQHTTPError 8 | from jenkinsapi.jenkins import Jenkins 9 | 10 | 11 | def test_normal_authentication(jenkins_admin_admin): 12 | # No problem with the righ user/pass 13 | jenkins_user = Jenkins( 14 | jenkins_admin_admin.baseurl, 15 | jenkins_admin_admin.username, 16 | jenkins_admin_admin.password, 17 | ) 18 | 19 | assert jenkins_user is not None 20 | 21 | # We cannot connect if no user/pass 22 | with pytest.raises(REQHTTPError) as http_excep: 23 | Jenkins(jenkins_admin_admin.baseurl) 24 | 25 | assert Requester.AUTH_COOKIE is None 26 | assert http_excep.value.response.status_code == 403 27 | 28 | 29 | # def test_auth_cookie(jenkins_admin_admin): 30 | # initial_cookie_value = None 31 | # final_cookie_value = "JSESSIONID" 32 | # assert initial_cookie_value == Requester.AUTH_COOKIE 33 | # 34 | # jenkins_admin_admin.use_auth_cookie() 35 | # 36 | # result = Requester.AUTH_COOKIE 37 | # assert result is not None 38 | # assert final_cookie_value in result 39 | # 40 | # 41 | # def test_wrongauth_cookie(jenkins_admin_admin): 42 | # initial_cookie_value = None 43 | # assert initial_cookie_value == Requester.AUTH_COOKIE 44 | # 45 | # jenkins_admin_admin.username = "fakeuser" 46 | # jenkins_admin_admin.password = "fakepass" 47 | # 48 | # with pytest.raises(HTTPError) as http_excep: 49 | # jenkins_admin_admin.use_auth_cookie() 50 | # 51 | # assert Requester.AUTH_COOKIE is None 52 | # assert http_excep.value.code == 401 53 | # 54 | # 55 | # def test_verify_cookie_isworking(jenkins_admin_admin): 56 | # initial_cookie_value = None 57 | # final_cookie_value = "JSESSIONID" 58 | # assert initial_cookie_value == Requester.AUTH_COOKIE 59 | # 60 | # # Remove requester user/pass 61 | # jenkins_admin_admin.requester.username = None 62 | # jenkins_admin_admin.requester.password = None 63 | # 64 | # # Verify that we cannot connect 65 | # with pytest.raises(REQHTTPError) as http_excep: 66 | # jenkins_admin_admin.poll() 67 | # 68 | # assert Requester.AUTH_COOKIE is None 69 | # assert http_excep.value.response.status_code == 403 70 | # 71 | # # Retrieve the auth cookie, we can because we 72 | # # have right values for jenkins_admin_admin.username 73 | # # and jenkins_admin_admin.password 74 | # jenkins_admin_admin.use_auth_cookie() 75 | # 76 | # result = Requester.AUTH_COOKIE 77 | # assert result is not None 78 | # assert final_cookie_value in result 79 | # 80 | # # Verify that we can connect even with no requester user/pass 81 | # # If we have the cookie the requester user/pass is not needed 82 | # jenkins_admin_admin.poll() 83 | -------------------------------------------------------------------------------- /jenkinsapi_tests/systests/test_credentials.py: -------------------------------------------------------------------------------- 1 | """ 2 | System tests for `jenkinsapi.jenkins` module. 3 | """ 4 | 5 | import logging 6 | import pytest 7 | from jenkinsapi_tests.test_utils.random_strings import random_string 8 | from jenkinsapi.credentials import Credentials 9 | from jenkinsapi.credentials import UsernamePasswordCredential 10 | from jenkinsapi.credentials import SecretTextCredential 11 | from jenkinsapi.credential import SSHKeyCredential 12 | 13 | log = logging.getLogger(__name__) 14 | 15 | 16 | def test_get_credentials(jenkins): 17 | creds = jenkins.credentials 18 | assert isinstance(creds, Credentials) is True 19 | 20 | 21 | def test_delete_inexistant_credential(jenkins): 22 | with pytest.raises(KeyError): 23 | creds = jenkins.credentials 24 | 25 | del creds[random_string()] 26 | 27 | 28 | def test_create_user_pass_credential(jenkins): 29 | creds = jenkins.credentials 30 | 31 | cred_descr = random_string() 32 | cred_dict = { 33 | "description": cred_descr, 34 | "userName": "userName", 35 | "password": "password", 36 | } 37 | creds[cred_descr] = UsernamePasswordCredential(cred_dict) 38 | 39 | assert cred_descr in creds 40 | 41 | cred = creds[cred_descr] 42 | assert isinstance(cred, UsernamePasswordCredential) is True 43 | assert cred.password == "" 44 | assert cred.description == cred_descr 45 | 46 | del creds[cred_descr] 47 | 48 | 49 | def test_update_user_pass_credential(jenkins): 50 | creds = jenkins.credentials 51 | 52 | cred_descr = random_string() 53 | cred_dict = { 54 | "description": cred_descr, 55 | "userName": "userName", 56 | "password": "password", 57 | } 58 | creds[cred_descr] = UsernamePasswordCredential(cred_dict) 59 | 60 | cred = creds[cred_descr] 61 | cred.userName = "anotheruser" 62 | cred.password = "password2" 63 | 64 | cred = creds[cred_descr] 65 | assert isinstance(cred, UsernamePasswordCredential) is True 66 | assert cred.userName == "anotheruser" 67 | assert cred.password == "password2" 68 | 69 | 70 | def test_create_ssh_credential(jenkins): 71 | creds = jenkins.credentials 72 | 73 | cred_descr = random_string() 74 | cred_dict = { 75 | "description": cred_descr, 76 | "userName": "userName", 77 | "passphrase": "", 78 | "private_key": "-----BEGIN RSA PRIVATE KEY-----", 79 | } 80 | creds[cred_descr] = SSHKeyCredential(cred_dict) 81 | 82 | assert cred_descr in creds 83 | 84 | cred = creds[cred_descr] 85 | assert isinstance(cred, SSHKeyCredential) is True 86 | assert cred.description == cred_descr 87 | 88 | del creds[cred_descr] 89 | 90 | cred_dict = { 91 | "description": cred_descr, 92 | "userName": "userName", 93 | "passphrase": "", 94 | "private_key": "/tmp/key", 95 | } 96 | with pytest.raises(ValueError): 97 | creds[cred_descr] = SSHKeyCredential(cred_dict) 98 | 99 | cred_dict = { 100 | "description": cred_descr, 101 | "userName": "userName", 102 | "passphrase": "", 103 | "private_key": "~/.ssh/key", 104 | } 105 | with pytest.raises(ValueError): 106 | creds[cred_descr] = SSHKeyCredential(cred_dict) 107 | 108 | cred_dict = { 109 | "description": cred_descr, 110 | "userName": "userName", 111 | "passphrase": "", 112 | "private_key": "invalid", 113 | } 114 | with pytest.raises(ValueError): 115 | creds[cred_descr] = SSHKeyCredential(cred_dict) 116 | 117 | 118 | def test_delete_credential(jenkins): 119 | creds = jenkins.credentials 120 | 121 | cred_descr = random_string() 122 | cred_dict = { 123 | "description": cred_descr, 124 | "userName": "userName", 125 | "password": "password", 126 | } 127 | creds[cred_descr] = UsernamePasswordCredential(cred_dict) 128 | 129 | assert cred_descr in creds 130 | del creds[cred_descr] 131 | assert cred_descr not in creds 132 | 133 | 134 | def test_create_secret_text_credential(jenkins): 135 | """ 136 | Tests the creation of a secret text. 137 | """ 138 | creds = jenkins.credentials 139 | 140 | cred_descr = random_string() 141 | cred_dict = {"description": cred_descr, "secret": "newsecret"} 142 | creds[cred_descr] = SecretTextCredential(cred_dict) 143 | 144 | assert cred_descr in creds 145 | cred = creds[cred_descr] 146 | assert isinstance(cred, SecretTextCredential) is True 147 | assert cred.secret is None 148 | assert cred.description == cred_descr 149 | 150 | del creds[cred_descr] 151 | -------------------------------------------------------------------------------- /jenkinsapi_tests/systests/test_crumbs_requester.py: -------------------------------------------------------------------------------- 1 | import io 2 | import time 3 | import json 4 | import logging 5 | import pytest 6 | 7 | from urllib.parse import urljoin 8 | from jenkinsapi.jenkins import Jenkins 9 | from jenkinsapi.utils.crumb_requester import CrumbRequester 10 | from jenkinsapi_tests.test_utils.random_strings import random_string 11 | from jenkinsapi_tests.systests.job_configs import JOB_WITH_FILE 12 | 13 | 14 | log = logging.getLogger(__name__) 15 | 16 | DEFAULT_JENKINS_PORT = 8080 17 | 18 | ENABLE_CRUMBS_CONFIG = { 19 | "hudson-security-csrf-GlobalCrumbIssuerConfiguration": { 20 | "csrf": { 21 | "issuer": { 22 | "value": "0", 23 | "stapler-class": "hudson.security.csrf.DefaultCrumbIssuer", 24 | "$class": "hudson.security.csrf.DefaultCrumbIssuer", 25 | "excludeClientIPFromCrumb": False, 26 | } 27 | } 28 | } 29 | } 30 | 31 | DISABLE_CRUMBS_CONFIG = { 32 | "hudson-security-csrf-GlobalCrumbIssuerConfiguration": {}, 33 | } 34 | 35 | SECURITY_SETTINGS = { 36 | "": "0", 37 | "markupFormatter": { 38 | "stapler-class": "hudson.markup.EscapedMarkupFormatter", 39 | "$class": "hudson.markup.EscapedMarkupFormatter", 40 | }, 41 | "org-jenkinsci-main-modules-sshd-SSHD": { 42 | "port": {"value": "", "type": "disabled"} 43 | }, 44 | "jenkins-CLI": {"enabled": False}, 45 | # This is not required if envinject plugin is not installed 46 | # but since it is installed for test suite - we must have this config 47 | # If this is not present - Jenkins will return error 48 | "org-jenkinsci-plugins-envinject-EnvInjectPluginConfiguration": { 49 | "enablePermissions": False, 50 | "hideInjectedVars": False, 51 | "enableLoadingFromMaster": False, 52 | }, 53 | "jenkins-model-DownloadSettings": {"useBrowser": False}, 54 | "slaveAgentPort": {"value": "", "type": "disable"}, 55 | "agentProtocol": [ 56 | "CLI-connect", 57 | "CLI2-connect", 58 | "JNLP-connect", 59 | "JNLP2-connect", 60 | "JNLP4-connect", 61 | ], 62 | "core:apply": "", 63 | } 64 | 65 | 66 | @pytest.fixture(scope="function") 67 | def crumbed_jenkins(jenkins): 68 | ENABLE_CRUMBS_CONFIG.update(SECURITY_SETTINGS) 69 | DISABLE_CRUMBS_CONFIG.update(SECURITY_SETTINGS) 70 | 71 | jenkins.requester.post_and_confirm_status( 72 | urljoin(jenkins.baseurl, "/configureSecurity/configure"), 73 | data={"Submit": "save", "json": json.dumps(ENABLE_CRUMBS_CONFIG)}, 74 | headers={"Content-Type": "application/x-www-form-urlencoded"}, 75 | ) 76 | log.info("Enabled Jenkins security") 77 | 78 | crumbed = Jenkins( 79 | jenkins.baseurl, requester=CrumbRequester(baseurl=jenkins.baseurl) 80 | ) 81 | 82 | yield crumbed 83 | 84 | crumbed.requester.post_and_confirm_status( 85 | jenkins.baseurl + "/configureSecurity/configure", 86 | data={"Submit": "save", "json": json.dumps(DISABLE_CRUMBS_CONFIG)}, 87 | headers={"Content-Type": "application/x-www-form-urlencoded"}, 88 | ) 89 | log.info("Disabled Jenkins security") 90 | 91 | 92 | def test_invoke_job_with_file(crumbed_jenkins): 93 | file_data = random_string() 94 | param_file = io.BytesIO(file_data.encode("utf-8")) 95 | 96 | job_name = "create1_%s" % random_string() 97 | job = crumbed_jenkins.create_job(job_name, JOB_WITH_FILE) 98 | 99 | assert job.has_params() 100 | assert len(job.get_params_list()) 101 | 102 | job.invoke(block=True, files={"file.txt": param_file}) 103 | 104 | build = job.get_last_build() 105 | while build.is_running(): 106 | time.sleep(0.25) 107 | 108 | artifacts = build.get_artifact_dict() 109 | assert isinstance(artifacts, dict) is True 110 | art_file = artifacts["file.txt"] 111 | assert art_file.get_data().decode("utf-8").strip() == file_data 112 | -------------------------------------------------------------------------------- /jenkinsapi_tests/systests/test_downstream_upstream.py: -------------------------------------------------------------------------------- 1 | """ 2 | System tests for `jenkinsapi.jenkins` module. 3 | """ 4 | 5 | import time 6 | import logging 7 | import pytest 8 | from jenkinsapi.custom_exceptions import NoBuildData 9 | 10 | log = logging.getLogger(__name__) 11 | 12 | JOB_CONFIGS = { 13 | "A": """ 14 | 15 | 16 | 17 | false 18 | 19 | 20 | true 21 | false 22 | false 23 | false 24 | 25 | false 26 | 27 | 28 | 29 | B 30 | 31 | SUCCESS 32 | 0 33 | BLUE 34 | 35 | 36 | 37 | 38 | """, 39 | "B": """ 40 | 41 | 42 | 43 | false 44 | 45 | 46 | true 47 | false 48 | false 49 | false 50 | 51 | false 52 | 53 | 54 | 55 | C 56 | 57 | SUCCESS 58 | 0 59 | BLUE 60 | 61 | 62 | 63 | 64 | """, 65 | "C": """ 66 | 67 | 68 | 69 | false 70 | 71 | 72 | true 73 | false 74 | false 75 | false 76 | 77 | false 78 | 79 | 80 | 81 | """, 82 | } 83 | 84 | DELAY = 10 85 | 86 | 87 | def test_stream_relationship(jenkins): 88 | """ 89 | Can we keep track of the relationships between upstream & downstream jobs? 90 | """ 91 | for job_name, job_config in JOB_CONFIGS.items(): 92 | jenkins.create_job(job_name, job_config) 93 | 94 | time.sleep(1) 95 | 96 | jenkins["A"].invoke() 97 | 98 | for _ in range(10): 99 | try: 100 | jenkins["C"].get_last_completed_buildnumber() > 0 101 | except NoBuildData: 102 | log.info( 103 | "Waiting %i seconds for until the final job has run", DELAY 104 | ) 105 | time.sleep(DELAY) 106 | else: 107 | break 108 | else: 109 | pytest.fail("Jenkins took too long to run these jobs") 110 | 111 | assert jenkins["C"].get_upstream_jobs() == [jenkins["B"]] 112 | assert jenkins["B"].get_upstream_jobs() == [jenkins["A"]] 113 | 114 | assert jenkins["A"].get_downstream_jobs() == [jenkins["B"]] 115 | assert jenkins["B"].get_downstream_jobs() == [jenkins["C"]] 116 | -------------------------------------------------------------------------------- /jenkinsapi_tests/systests/test_env_vars.py: -------------------------------------------------------------------------------- 1 | """ 2 | System tests for `jenkinsapi.jenkins` module. 3 | """ 4 | 5 | import time 6 | 7 | from jenkinsapi_tests.systests.job_configs import JOB_WITH_ENV_VARS 8 | from jenkinsapi_tests.test_utils.random_strings import random_string 9 | 10 | 11 | def test_get_env_vars(jenkins): 12 | job_name = "get_env_vars_create1_%s" % random_string() 13 | job = jenkins.create_job(job_name, JOB_WITH_ENV_VARS) 14 | job.invoke(block=True) 15 | build = job.get_last_build() 16 | while build.is_running(): 17 | time.sleep(0.25) 18 | data = build.get_env_vars() 19 | assert data["key1"] == "value1" 20 | assert data["key2"] == "value2" 21 | -------------------------------------------------------------------------------- /jenkinsapi_tests/systests/test_executors.py: -------------------------------------------------------------------------------- 1 | """ 2 | System tests for `jenkinsapi.jenkins` module. 3 | """ 4 | 5 | import time 6 | import logging 7 | from jenkinsapi_tests.systests.job_configs import LONG_RUNNING_JOB 8 | from jenkinsapi_tests.test_utils.random_strings import random_string 9 | 10 | log = logging.getLogger(__name__) 11 | 12 | 13 | def test_get_executors(jenkins): 14 | node_name = random_string() 15 | node_dict = { 16 | "num_executors": 2, 17 | "node_description": "Test JNLP Node", 18 | "remote_fs": "/tmp", 19 | "labels": "systest_jnlp", 20 | "exclusive": True, 21 | } 22 | jenkins.nodes.create_node(node_name, node_dict) 23 | executors = jenkins.get_executors(node_name) 24 | assert executors.count == 2 25 | 26 | for count, execs in enumerate(executors): 27 | assert count == execs.get_number() 28 | assert execs.is_idle() is True 29 | 30 | 31 | def test_running_executor(jenkins): 32 | node_name = random_string() 33 | node_dict = { 34 | "num_executors": 1, 35 | "node_description": "Test JNLP Node", 36 | "remote_fs": "/tmp", 37 | "labels": "systest_jnlp", 38 | "exclusive": True, 39 | } 40 | jenkins.nodes.create_node(node_name, node_dict) 41 | job_name = "create_%s" % random_string() 42 | job = jenkins.create_job(job_name, LONG_RUNNING_JOB) 43 | qq = job.invoke() 44 | qq.block_until_building() 45 | 46 | if job.is_running() is False: 47 | time.sleep(1) 48 | executors = jenkins.get_executors(node_name) 49 | all_idle = True 50 | for execs in executors: 51 | if execs.is_idle() is False: 52 | all_idle = False 53 | assert execs.get_progress() != -1 54 | assert execs.get_current_executable() == qq.get_build_number() 55 | assert execs.likely_stuck() is False 56 | assert all_idle is True, "Executor should have been triggered." 57 | 58 | 59 | def test_idle_executors(jenkins): 60 | node_name = random_string() 61 | node_dict = { 62 | "num_executors": 1, 63 | "node_description": "Test JNLP Node", 64 | "remote_fs": "/tmp", 65 | "labels": "systest_jnlp", 66 | "exclusive": True, 67 | } 68 | jenkins.nodes.create_node(node_name, node_dict) 69 | executors = jenkins.get_executors(node_name) 70 | 71 | for execs in executors: 72 | assert execs.get_progress() == -1 73 | assert execs.get_current_executable() is None 74 | assert execs.likely_stuck() is False 75 | assert execs.is_idle() is True 76 | -------------------------------------------------------------------------------- /jenkinsapi_tests/systests/test_generate_new_api_token.py: -------------------------------------------------------------------------------- 1 | """ 2 | System tests for generation new api token for logged in user 3 | """ 4 | 5 | import pytest 6 | import logging 7 | from jenkinsapi.utils.crumb_requester import CrumbRequester 8 | 9 | 10 | log = logging.getLogger(__name__) 11 | 12 | 13 | @pytest.mark.generate_new_api_token 14 | def test_generate_new_api_token(jenkins_admin_admin): 15 | jenkins_admin_admin.requester = CrumbRequester( 16 | baseurl=jenkins_admin_admin.baseurl, 17 | username=jenkins_admin_admin.username, 18 | password=jenkins_admin_admin.password, 19 | ) 20 | jenkins_admin_admin.poll() 21 | 22 | new_token = ( 23 | jenkins_admin_admin.generate_new_api_token() 24 | ) # generate new token 25 | log.info("newly generated token: %s", new_token) 26 | assert new_token is not None 27 | -------------------------------------------------------------------------------- /jenkinsapi_tests/systests/test_invocation.py: -------------------------------------------------------------------------------- 1 | """ 2 | System tests for `jenkinsapi.jenkins` module. 3 | """ 4 | 5 | import time 6 | import logging 7 | import pytest 8 | from jenkinsapi.build import Build 9 | from jenkinsapi.queue import QueueItem 10 | from jenkinsapi_tests.test_utils.random_strings import random_string 11 | from jenkinsapi_tests.systests.job_configs import LONG_RUNNING_JOB 12 | from jenkinsapi_tests.systests.job_configs import SHORTISH_JOB, EMPTY_JOB 13 | from jenkinsapi.custom_exceptions import BadParams, NotFound 14 | 15 | 16 | log = logging.getLogger(__name__) 17 | 18 | 19 | def test_invocation_object(jenkins): 20 | job_name = "Acreate_%s" % random_string() 21 | job = jenkins.create_job(job_name, SHORTISH_JOB) 22 | qq = job.invoke() 23 | assert isinstance(qq, QueueItem) 24 | # Let Jenkins catchup 25 | qq.block_until_building() 26 | assert qq.get_build_number() == 1 27 | 28 | 29 | def test_get_block_until_build_running(jenkins): 30 | job_name = "Bcreate_%s" % random_string() 31 | job = jenkins.create_job(job_name, LONG_RUNNING_JOB) 32 | qq = job.invoke() 33 | time.sleep(3) 34 | bn = qq.block_until_building(delay=3).get_number() 35 | assert isinstance(bn, int) 36 | 37 | build = qq.get_build() 38 | assert isinstance(build, Build) 39 | assert build.is_running() 40 | build.stop() 41 | # if we call next line right away - Jenkins have no time to stop job 42 | # so we wait a bit 43 | time.sleep(1) 44 | assert not build.is_running() 45 | console = build.get_console() 46 | assert isinstance(console, str) 47 | assert "Started by user" in console 48 | 49 | 50 | def test_get_block_until_build_complete(jenkins): 51 | job_name = "Ccreate_%s" % random_string() 52 | job = jenkins.create_job(job_name, SHORTISH_JOB) 53 | qq = job.invoke() 54 | qq.block_until_complete() 55 | assert not qq.get_build().is_running() 56 | 57 | 58 | def test_mi_and_get_last_build(jenkins): 59 | job_name = "Dcreate_%s" % random_string() 60 | 61 | job = jenkins.create_job(job_name, SHORTISH_JOB) 62 | 63 | for _ in range(3): 64 | ii = job.invoke() 65 | ii.block_until_complete(delay=2) 66 | 67 | build_number = job.get_last_good_buildnumber() 68 | assert build_number == 3 69 | 70 | build = job.get_build(build_number) 71 | assert isinstance(build, Build) 72 | 73 | build = job.get_build_metadata(build_number) 74 | assert isinstance(build, Build) 75 | 76 | 77 | def test_mi_and_get_build_number(jenkins): 78 | job_name = "Ecreate_%s" % random_string() 79 | 80 | job = jenkins.create_job(job_name, EMPTY_JOB) 81 | 82 | for invocation in range(3): 83 | qq = job.invoke() 84 | qq.block_until_complete(delay=1) 85 | build_number = qq.get_build_number() 86 | assert build_number == invocation + 1 87 | 88 | 89 | def test_mi_and_delete_build(jenkins): 90 | job_name = "Ecreate_%s" % random_string() 91 | 92 | job = jenkins.create_job(job_name, EMPTY_JOB) 93 | 94 | for invocation in range(3): 95 | qq = job.invoke() 96 | qq.block_until_complete(delay=1) 97 | build_number = qq.get_build_number() 98 | assert build_number == invocation + 1 99 | 100 | # Delete build using Job.delete_build 101 | job.get_build(1) 102 | job.delete_build(1) 103 | with pytest.raises(NotFound): 104 | job.get_build(1) 105 | 106 | # Delete build using Job as dictionary of builds 107 | assert isinstance(job[2], Build) 108 | del job[2] 109 | 110 | with pytest.raises(NotFound): 111 | job.get_build(2) 112 | 113 | with pytest.raises(NotFound): 114 | job.delete_build(99) 115 | 116 | 117 | def test_give_params_on_non_parameterized_job(jenkins): 118 | job_name = "Ecreate_%s" % random_string() 119 | job = jenkins.create_job(job_name, EMPTY_JOB) 120 | with pytest.raises(BadParams): 121 | job.invoke(build_params={"foo": "bar", "baz": 99}) 122 | 123 | 124 | def test_keep_build_toggle(jenkins): 125 | job_name = "Ecreate_%s" % random_string() 126 | 127 | job = jenkins.create_job(job_name, EMPTY_JOB) 128 | 129 | qq = job.invoke() 130 | qq.block_until_complete(delay=1) 131 | 132 | build = job.get_last_build() 133 | assert not build.is_kept_forever() 134 | build.toggle_keep() 135 | assert build.is_kept_forever() 136 | 137 | build_number = job.get_last_buildnumber() 138 | job.toggle_keep_build(build_number) 139 | build = job.get_last_build() 140 | assert not build.is_kept_forever() 141 | -------------------------------------------------------------------------------- /jenkinsapi_tests/systests/test_jenkins.py: -------------------------------------------------------------------------------- 1 | """ 2 | System tests for `jenkinsapi.jenkins` module. 3 | """ 4 | 5 | import pytest 6 | from jenkinsapi.job import Job 7 | from jenkinsapi.jobs import Jobs 8 | from jenkinsapi.build import Build 9 | from jenkinsapi.queue import QueueItem 10 | from jenkinsapi_tests.systests.job_configs import EMPTY_JOB 11 | from jenkinsapi_tests.test_utils.random_strings import random_string 12 | from jenkinsapi.custom_exceptions import UnknownJob 13 | 14 | 15 | def job_present(jenkins, name): 16 | jenkins.poll() 17 | assert name in jenkins, "Job %r is absent in jenkins." % name 18 | assert isinstance(jenkins.get_job(name), Job) is True 19 | assert isinstance(jenkins[name], Job) is True 20 | 21 | 22 | def job_absent(jenkins, name): 23 | jenkins.poll() 24 | assert name not in jenkins, "Job %r is present in jenkins." % name 25 | 26 | 27 | def test_create_job(jenkins): 28 | job_name = "create_%s" % random_string() 29 | jenkins.create_job(job_name, EMPTY_JOB) 30 | job_present(jenkins, job_name) 31 | 32 | 33 | def test_create_job_with_plus(jenkins): 34 | job_name = "create+%s" % random_string() 35 | jenkins.create_job(job_name, EMPTY_JOB) 36 | job_present(jenkins, job_name) 37 | job = jenkins[job_name] 38 | assert job_name in job.url 39 | 40 | 41 | def test_create_dup_job(jenkins): 42 | job_name = "create_%s" % random_string() 43 | old_job = jenkins.create_job(job_name, EMPTY_JOB) 44 | job_present(jenkins, job_name) 45 | new_job = jenkins.create_job(job_name, EMPTY_JOB) 46 | assert new_job == old_job 47 | 48 | 49 | def test_get_jobs_info(jenkins): 50 | job_name = "create_%s" % random_string() 51 | job = jenkins.create_job(job_name, EMPTY_JOB) 52 | 53 | jobs_info = list(jenkins.get_jobs_info()) 54 | assert len(jobs_info) == 1 55 | for url, name in jobs_info: 56 | assert url == job.url 57 | assert name == job.name 58 | 59 | 60 | def test_create_job_through_jobs_dict(jenkins): 61 | job_name = "create_%s" % random_string() 62 | jenkins.jobs[job_name] = EMPTY_JOB 63 | job_present(jenkins, job_name) 64 | 65 | 66 | def test_enable_disable_job(jenkins): 67 | job_name = "create_%s" % random_string() 68 | jenkins.create_job(job_name, EMPTY_JOB) 69 | job_present(jenkins, job_name) 70 | 71 | j = jenkins[job_name] 72 | j.invoke(block=True) # run this at least once 73 | # Ensure job begins as enabled 74 | assert j.is_enabled() is True, "An enabled job is reporting incorrectly" 75 | 76 | j.disable() 77 | assert j.is_enabled() is False, "A disabled job is reporting incorrectly" 78 | 79 | j.enable() 80 | assert j.is_enabled() is True, "An enabled job is reporting incorrectly" 81 | 82 | 83 | def test_get_job_and_update_config(jenkins): 84 | job_name = "config_%s" % random_string() 85 | jenkins.create_job(job_name, EMPTY_JOB) 86 | job_present(jenkins, job_name) 87 | config = jenkins[job_name].get_config() 88 | assert config.strip() == EMPTY_JOB.strip() 89 | jenkins[job_name].update_config(EMPTY_JOB) 90 | 91 | 92 | def test_invoke_job(jenkins): 93 | job_name = "create_%s" % random_string() 94 | job = jenkins.create_job(job_name, EMPTY_JOB) 95 | job.invoke(block=True) 96 | assert isinstance(job.get_build(1), Build) 97 | 98 | 99 | def test_invocation_object(jenkins): 100 | job_name = "create_%s" % random_string() 101 | job = jenkins.create_job(job_name, EMPTY_JOB) 102 | ii = job.invoke() 103 | assert isinstance(ii, QueueItem) is True 104 | 105 | 106 | def test_get_jobs_list(jenkins): 107 | job1_name = "first_%s" % random_string() 108 | job2_name = "second_%s" % random_string() 109 | 110 | jenkins.create_job(job1_name, EMPTY_JOB) 111 | jenkins.create_job(job2_name, EMPTY_JOB) 112 | assert len(jenkins.jobs) >= 2 113 | job_list = jenkins.get_jobs_list() 114 | assert [job1_name, job2_name] == job_list 115 | 116 | 117 | def test_get_job(jenkins): 118 | job1_name = "first_%s" % random_string() 119 | 120 | jenkins.create_job(job1_name, EMPTY_JOB) 121 | job = jenkins[job1_name] 122 | assert isinstance(job, Job) is True 123 | assert job.name == job1_name 124 | 125 | 126 | def test_get_jobs(jenkins): 127 | job1_name = "first_%s" % random_string() 128 | job2_name = "second_%s" % random_string() 129 | 130 | jenkins.create_job(job1_name, EMPTY_JOB) 131 | jenkins.create_job(job2_name, EMPTY_JOB) 132 | jobs = jenkins.jobs 133 | assert isinstance(jobs, Jobs) is True 134 | assert len(jobs) >= 2 135 | for job_name, job in jobs.iteritems(): 136 | assert isinstance(job_name, str) is True 137 | assert isinstance(job, Job) is True 138 | 139 | 140 | def test_get_job_that_does_not_exist(jenkins): 141 | with pytest.raises(UnknownJob): 142 | jenkins["doesnot_exist"] 143 | 144 | 145 | def test_has_job(jenkins): 146 | job1_name = "first_%s" % random_string() 147 | jenkins.create_job(job1_name, EMPTY_JOB) 148 | assert jenkins.has_job(job1_name) is True 149 | assert job1_name in jenkins 150 | 151 | 152 | def test_has_no_job(jenkins): 153 | assert jenkins.has_job("doesnt_exist") is False 154 | assert "doesnt_exist" not in jenkins 155 | 156 | 157 | def test_delete_job(jenkins): 158 | job1_name = "delete_me_%s" % random_string() 159 | 160 | jenkins.create_job(job1_name, EMPTY_JOB) 161 | jenkins.delete_job(job1_name) 162 | job_absent(jenkins, job1_name) 163 | 164 | 165 | def test_rename_job(jenkins): 166 | job1_name = "A__%s" % random_string() 167 | job2_name = "B__%s" % random_string() 168 | 169 | jenkins.create_job(job1_name, EMPTY_JOB) 170 | jenkins.rename_job(job1_name, job2_name) 171 | job_absent(jenkins, job1_name) 172 | job_present(jenkins, job2_name) 173 | 174 | 175 | def test_copy_job(jenkins): 176 | template_job_name = "TPL%s" % random_string() 177 | copied_job_name = "CPY%s" % random_string() 178 | 179 | jenkins.create_job(template_job_name, EMPTY_JOB) 180 | j = jenkins.copy_job(template_job_name, copied_job_name) 181 | job_present(jenkins, template_job_name) 182 | job_present(jenkins, copied_job_name) 183 | assert isinstance(j, Job) is True 184 | assert j.name == copied_job_name 185 | 186 | 187 | def test_get_master_data(jenkins): 188 | master_data = jenkins.get_master_data() 189 | assert master_data["totalExecutors"] == 2 190 | 191 | 192 | def test_run_groovy_script(jenkins): 193 | expected_result = "Hello world!" 194 | result = jenkins.run_groovy_script('print "%s"' % expected_result) 195 | assert result.strip() == "Hello world!" 196 | -------------------------------------------------------------------------------- /jenkinsapi_tests/systests/test_jenkins_artifacts.py: -------------------------------------------------------------------------------- 1 | """ 2 | System tests for `jenkinsapi.jenkins` module. 3 | """ 4 | 5 | import os 6 | from posixpath import join 7 | import re 8 | import time 9 | import gzip 10 | import shutil 11 | import tempfile 12 | import logging 13 | from jenkinsapi_tests.systests.job_configs import JOB_WITH_ARTIFACTS 14 | from jenkinsapi_tests.test_utils.random_strings import random_string 15 | 16 | log = logging.getLogger(__name__) 17 | 18 | 19 | def test_artifacts(jenkins): 20 | job_name = "create_%s" % random_string() 21 | job = jenkins.create_job(job_name, JOB_WITH_ARTIFACTS) 22 | job.invoke(block=True) 23 | 24 | build = job.get_last_build() 25 | 26 | while build.is_running(): 27 | time.sleep(1) 28 | 29 | artifacts = build.get_artifact_dict() 30 | assert isinstance(artifacts, dict) is True 31 | 32 | text_artifact = artifacts["out.txt"] 33 | binary_artifact = artifacts["out.gz"] 34 | 35 | tempDir = tempfile.mkdtemp() 36 | 37 | try: 38 | # Verify that we can handle text artifacts 39 | text_artifact.save_to_dir(tempDir, strict_validation=True) 40 | text_file_path = join(tempDir, text_artifact.filename) 41 | assert os.path.exists(text_file_path) 42 | with open(text_file_path, "rb") as f: 43 | read_back_text = f.read().strip() 44 | read_back_text = read_back_text.decode("ascii") 45 | log.info("Text artifact: %s", read_back_text) 46 | assert ( 47 | re.match(r"^PING \S+ \(127.0.0.1\)", read_back_text) 48 | is not None 49 | ) 50 | assert read_back_text.endswith("ms") is True 51 | 52 | # Verify that we can hande binary artifacts 53 | binary_artifact.save_to_dir(tempDir, strict_validation=True) 54 | bin_file_path = join(tempDir, binary_artifact.filename) 55 | assert os.path.exists(bin_file_path) 56 | with gzip.open(bin_file_path, "rb") as f: 57 | read_back_text = f.read().strip() 58 | read_back_text = read_back_text.decode("ascii") 59 | assert ( 60 | re.match(r"^PING \S+ \(127.0.0.1\)", read_back_text) 61 | is not None 62 | ) 63 | assert read_back_text.endswith("ms") is True 64 | finally: 65 | shutil.rmtree(tempDir) 66 | -------------------------------------------------------------------------------- /jenkinsapi_tests/systests/test_jenkins_matrix.py: -------------------------------------------------------------------------------- 1 | """ 2 | System tests for `jenkinsapi.jenkins` module. 3 | """ 4 | 5 | import re 6 | import time 7 | from jenkinsapi_tests.systests.job_configs import MATRIX_JOB 8 | from jenkinsapi_tests.test_utils.random_strings import random_string 9 | 10 | 11 | def test_invoke_matrix_job(jenkins): 12 | job_name = "create_%s" % random_string() 13 | job = jenkins.create_job(job_name, MATRIX_JOB) 14 | queueItem = job.invoke() 15 | queueItem.block_until_complete() 16 | 17 | build = job.get_last_build() 18 | 19 | while build.is_running(): 20 | time.sleep(1) 21 | 22 | set_of_groups = set() 23 | for run in build.get_matrix_runs(): 24 | assert run.get_number() == build.get_number() 25 | assert run.get_upstream_build() == build 26 | match_result = re.search("\xbb (.*) #\\d+$", run.name) 27 | assert match_result is not None 28 | set_of_groups.add(match_result.group(1)) 29 | build.get_master_job_name() 30 | 31 | assert set_of_groups == set(["one", "two", "three"]) 32 | -------------------------------------------------------------------------------- /jenkinsapi_tests/systests/test_nodes.py: -------------------------------------------------------------------------------- 1 | """ 2 | System tests for `jenkinsapi.jenkins` module. 3 | """ 4 | 5 | import logging 6 | import pytest 7 | from jenkinsapi.node import Node 8 | from jenkinsapi.credential import SSHKeyCredential 9 | from jenkinsapi_tests.test_utils.random_strings import random_string 10 | 11 | log = logging.getLogger(__name__) 12 | 13 | TOOL_KEY = "hudson.tasks.Maven$MavenInstallation$DescriptorImpl@Maven 3.0.5" 14 | 15 | 16 | def test_online_offline(jenkins): 17 | """ 18 | Can we flip the online / offline state of the master node. 19 | """ 20 | # Master node name should be case insensitive 21 | # mn0 = jenkins.get_node('MaStEr') 22 | mn = jenkins.get_node("Built-In Node") 23 | # self.assertEqual(mn, mn0) 24 | 25 | mn.set_online() # It should already be online, hence no-op 26 | assert mn.is_online() is True 27 | 28 | mn.set_offline() # We switch that suckah off 29 | mn.set_offline() # This should be a no-op 30 | assert mn.is_online() is False 31 | 32 | mn.set_online() # Switch it back on 33 | assert mn.is_online() is True 34 | 35 | 36 | def test_create_jnlp_node(jenkins): 37 | node_name = random_string() 38 | node_dict = { 39 | "num_executors": 1, 40 | "node_description": "Test JNLP Node", 41 | "remote_fs": "/tmp", 42 | "labels": "systest_jnlp", 43 | "exclusive": True, 44 | "tool_location": [ 45 | { 46 | "key": TOOL_KEY, 47 | "home": "/home/apache-maven-3.0.5/", 48 | }, 49 | ], 50 | } 51 | node = jenkins.nodes.create_node(node_name, node_dict) 52 | assert isinstance(node, Node) is True 53 | 54 | del jenkins.nodes[node_name] 55 | 56 | 57 | def test_create_ssh_node(jenkins): 58 | node_name = random_string() 59 | creds = jenkins.get_credentials() 60 | 61 | cred_descr = random_string() 62 | cred_dict = { 63 | "description": cred_descr, 64 | "userName": "username", 65 | "passphrase": "", 66 | "private_key": "-----BEGIN RSA PRIVATE KEY-----", 67 | } 68 | creds[cred_descr] = SSHKeyCredential(cred_dict) 69 | node_dict = { 70 | "num_executors": 1, 71 | "node_description": "Description %s" % node_name, 72 | "remote_fs": "/tmp", 73 | "labels": node_name, 74 | "exclusive": False, 75 | "host": "127.0.0.1", 76 | "port": 22, 77 | "credential_description": cred_descr, 78 | "jvm_options": "", 79 | "java_path": "", 80 | "prefix_start_slave_cmd": "", 81 | "suffix_start_slave_cmd": "", 82 | "retention": "ondemand", 83 | "ondemand_delay": 0, 84 | "ondemand_idle_delay": 5, 85 | "tool_location": [ 86 | { 87 | "key": TOOL_KEY, 88 | "home": "/home/apache-maven-3.0.5/", 89 | }, 90 | ], 91 | } 92 | node = jenkins.nodes.create_node(node_name, node_dict) 93 | assert isinstance(node, Node) is True 94 | del jenkins.nodes[node_name] 95 | 96 | jenkins.nodes[node_name] = node_dict 97 | assert isinstance(jenkins.nodes[node_name], Node) is True 98 | del jenkins.nodes[node_name] 99 | 100 | 101 | def test_delete_node(jenkins): 102 | node_name = random_string() 103 | node_dict = { 104 | "num_executors": 1, 105 | "node_description": "Test JNLP Node", 106 | "remote_fs": "/tmp", 107 | "labels": "systest_jnlp", 108 | "exclusive": True, 109 | } 110 | jenkins.nodes.create_node(node_name, node_dict) 111 | del jenkins.nodes[node_name] 112 | 113 | with pytest.raises(KeyError): 114 | jenkins.nodes[node_name] 115 | 116 | with pytest.raises(KeyError): 117 | del jenkins.nodes["not_exist"] 118 | 119 | 120 | def test_delete_all_nodes(jenkins): 121 | nodes = jenkins.nodes 122 | 123 | for name in nodes.keys(): 124 | del nodes[name] 125 | 126 | assert len(jenkins.nodes) == 1 127 | 128 | 129 | def test_get_node_labels(jenkins): 130 | node_name = random_string() 131 | node_labels = "LABEL1 LABEL2" 132 | node_dict = { 133 | "num_executors": 1, 134 | "node_description": "Test Node with Labels", 135 | "remote_fs": "/tmp", 136 | "labels": node_labels, 137 | "exclusive": True, 138 | } 139 | node = jenkins.nodes.create_node(node_name, node_dict) 140 | assert node.get_labels() == node_labels 141 | 142 | del jenkins.nodes[node_name] 143 | 144 | 145 | def test_get_executors(jenkins): 146 | node_name = random_string() 147 | node_labels = "LABEL1 LABEL2" 148 | node_dict = { 149 | "num_executors": 1, 150 | "node_description": "Test Node with Labels", 151 | "remote_fs": "/tmp", 152 | "labels": node_labels, 153 | "exclusive": True, 154 | } 155 | node = jenkins.nodes.create_node(node_name, node_dict) 156 | 157 | with pytest.raises(AttributeError): 158 | assert node.get_config_element("executors") == "1" 159 | 160 | assert node.get_config_element("numExecutors") == "1" 161 | 162 | del jenkins.nodes[node_name] 163 | 164 | 165 | def test_set_executors(jenkins): 166 | node_name = random_string() 167 | node_labels = "LABEL1 LABEL2" 168 | node_dict = { 169 | "num_executors": 1, 170 | "node_description": "Test Node with Labels", 171 | "remote_fs": "/tmp", 172 | "labels": node_labels, 173 | "exclusive": True, 174 | } 175 | node = jenkins.nodes.create_node(node_name, node_dict) 176 | 177 | assert node.set_config_element("numExecutors", "5") is None 178 | 179 | assert node.get_config_element("numExecutors") == "5" 180 | 181 | del jenkins.nodes[node_name] 182 | 183 | 184 | def test_set_master_executors(jenkins): 185 | node = jenkins.nodes["Built-In Node"] 186 | 187 | assert node.get_num_executors() == 2 188 | 189 | node.set_num_executors(5) 190 | assert node.get_num_executors() == 5 191 | 192 | node.set_num_executors(2) 193 | 194 | 195 | def test_offline_reason(jenkins): 196 | node_name = random_string() 197 | node_labels = "LABEL1 LABEL2" 198 | node_dict = { 199 | "num_executors": 1, 200 | "node_description": "Test Node with Labels", 201 | "remote_fs": "/tmp", 202 | "labels": node_labels, 203 | "exclusive": True, 204 | } 205 | node = jenkins.nodes.create_node(node_name, node_dict) 206 | 207 | node.toggle_temporarily_offline("test1") 208 | node.poll() 209 | assert node.offline_reason() == "test1" 210 | 211 | node.update_offline_reason("test2") 212 | node.poll() 213 | assert node.offline_reason() == "test2" 214 | 215 | del jenkins.nodes[node_name] 216 | -------------------------------------------------------------------------------- /jenkinsapi_tests/systests/test_parameterized_builds.py: -------------------------------------------------------------------------------- 1 | """ 2 | System tests for `jenkinsapi.jenkins` module. 3 | """ 4 | 5 | import io 6 | import time 7 | 8 | from jenkinsapi_tests.test_utils.random_strings import random_string 9 | from jenkinsapi_tests.systests.job_configs import JOB_WITH_FILE 10 | from jenkinsapi_tests.systests.job_configs import JOB_WITH_FILE_AND_PARAMS 11 | from jenkinsapi_tests.systests.job_configs import JOB_WITH_PARAMETERS 12 | 13 | 14 | def test_invoke_job_with_file(jenkins): 15 | file_data = random_string() 16 | param_file = io.BytesIO(file_data.encode("utf-8")) 17 | 18 | job_name = "create1_%s" % random_string() 19 | job = jenkins.create_job(job_name, JOB_WITH_FILE) 20 | 21 | assert job.has_params() is True 22 | assert len(job.get_params_list()) != 0 23 | 24 | job.invoke(block=True, files={"file.txt": param_file}) 25 | 26 | build = job.get_last_build() 27 | while build.is_running(): 28 | time.sleep(0.25) 29 | 30 | artifacts = build.get_artifact_dict() 31 | assert isinstance(artifacts, dict) is True 32 | art_file = artifacts["file.txt"] 33 | assert art_file.get_data().decode("utf-8").strip() == file_data 34 | 35 | 36 | def test_invoke_job_parameterized(jenkins): 37 | param_B = random_string() 38 | 39 | job_name = "create2_%s" % random_string() 40 | job = jenkins.create_job(job_name, JOB_WITH_PARAMETERS) 41 | job.invoke(block=True, build_params={"B": param_B}) 42 | build = job.get_last_build() 43 | 44 | artifacts = build.get_artifact_dict() 45 | artB = artifacts["b.txt"] 46 | assert artB.get_data().decode("UTF-8", "replace").strip() == param_B 47 | 48 | assert param_B in build.get_console() 49 | 50 | 51 | def test_parameterized_job_build_queuing(jenkins): 52 | """ 53 | Accept multiple builds of parameterized jobs with unique parameters. 54 | """ 55 | job_name = "create_%s" % random_string() 56 | job = jenkins.create_job(job_name, JOB_WITH_PARAMETERS) 57 | 58 | # Latest Jenkins schedules builds to run right away, so remove all 59 | # executors from master node to investigate queue 60 | master = jenkins.nodes["Built-In Node"] 61 | num_executors = master.get_num_executors() 62 | master.set_num_executors(0) 63 | 64 | for i in range(3): 65 | param_B = random_string() 66 | params = {"B": param_B} 67 | job.invoke(build_params=params) 68 | 69 | assert job.has_queued_build(params) is True 70 | 71 | master.set_num_executors(num_executors) 72 | 73 | while job.has_queued_build(params): 74 | time.sleep(0.25) 75 | 76 | build = job.get_last_build() 77 | while build.is_running(): 78 | time.sleep(0.25) 79 | 80 | artifacts = build.get_artifact_dict() 81 | assert isinstance(artifacts, dict) is True 82 | artB = artifacts["b.txt"] 83 | assert artB.get_data().decode("utf-8").strip() == param_B 84 | 85 | assert param_B in build.get_console() 86 | 87 | 88 | def test_parameterized_multiple_builds_get_the_same_queue_item(jenkins): 89 | """ 90 | Multiple attempts to run the same parameterized 91 | build will get the same queue item. 92 | """ 93 | job_name = "create_%s" % random_string() 94 | job = jenkins.create_job(job_name, JOB_WITH_PARAMETERS) 95 | 96 | # Latest Jenkins schedules builds to run right away, so remove all 97 | # executors from master node to investigate queue 98 | master = jenkins.nodes["Built-In Node"] 99 | num_executors = master.get_num_executors() 100 | master.set_num_executors(0) 101 | 102 | for i in range(3): 103 | params = {"B": random_string()} 104 | qq0 = job.invoke(build_params=params) 105 | 106 | qq1 = job.invoke(build_params=params) 107 | assert qq0 == qq1 108 | 109 | master.set_num_executors(num_executors) 110 | 111 | 112 | def test_invoke_job_with_file_and_params(jenkins): 113 | file_data = random_string() 114 | param_data = random_string() 115 | param_file = io.BytesIO(file_data.encode("utf-8")) 116 | 117 | job_name = "create_%s" % random_string() 118 | job = jenkins.create_job(job_name, JOB_WITH_FILE_AND_PARAMS) 119 | 120 | assert job.has_params() is True 121 | assert len(job.get_params_list()) != 0 122 | 123 | qi = job.invoke( 124 | block=True, 125 | files={"file.txt": param_file}, 126 | build_params={"B": param_data}, 127 | ) 128 | 129 | build = qi.get_build() 130 | artifacts = build.get_artifact_dict() 131 | assert isinstance(artifacts, dict) is True 132 | art_file = artifacts["file.txt"] 133 | assert art_file.get_data().decode("utf-8").strip() == file_data 134 | art_param = artifacts["file1.txt"] 135 | assert art_param.get_data().decode("utf-8").strip() == param_data 136 | -------------------------------------------------------------------------------- /jenkinsapi_tests/systests/test_plugins.py: -------------------------------------------------------------------------------- 1 | """ 2 | System tests for `jenkinsapi.plugins` module. 3 | """ 4 | 5 | import logging 6 | import pytest 7 | from jenkinsapi_tests.test_utils.random_strings import random_string 8 | from jenkinsapi.plugin import Plugin 9 | 10 | log = logging.getLogger(__name__) 11 | 12 | 13 | def test_plugin_data(jenkins): 14 | # It takes time to get plugins json from remote 15 | timeout = jenkins.requester.timeout 16 | jenkins.requester.timeout = 60 17 | jenkins.plugins.check_updates_server() 18 | jenkins.requester.timeout = timeout 19 | 20 | assert "workflow-api" in jenkins.plugins 21 | 22 | 23 | def test_get_missing_plugin(jenkins): 24 | plugins = jenkins.get_plugins() 25 | with pytest.raises(KeyError): 26 | plugins["lsdajdaslkjdlkasj"] # this plugin surely does not exist! 27 | 28 | 29 | def test_get_single_plugin(jenkins): 30 | plugins = jenkins.get_plugins() 31 | plugin_name, plugin = next(plugins.iteritems()) 32 | 33 | assert isinstance(plugin_name, str) 34 | assert isinstance(plugin, Plugin) 35 | 36 | 37 | def test_get_single_plugin_depth_2(jenkins): 38 | plugins = jenkins.get_plugins(depth=2) 39 | _, plugin = next(plugins.iteritems()) 40 | 41 | assert isinstance(plugin, Plugin) 42 | 43 | 44 | def test_delete_inexistant_plugin(jenkins): 45 | with pytest.raises(KeyError): 46 | del jenkins.plugins[random_string()] 47 | 48 | 49 | # def test_install_uninstall_plugin(jenkins): 50 | # plugin_name = "suppress-stack-trace" 51 | # 52 | # plugin_dict = { 53 | # "shortName": plugin_name, 54 | # "version": "latest", 55 | # } 56 | # jenkins.plugins[plugin_name] = Plugin(plugin_dict) 57 | # 58 | # assert plugin_name in jenkins.plugins 59 | # 60 | # plugin = jenkins.get_plugins()[plugin_name] 61 | # assert isinstance(plugin, Plugin) 62 | # assert plugin.shortName == plugin_name 63 | # 64 | # del jenkins.plugins[plugin_name] 65 | # assert jenkins.plugins[plugin_name].deleted 66 | # 67 | # 68 | # def test_install_multiple_plugins(jenkins): 69 | # plugin_one_name = "keyboard-shortcuts-plugin" 70 | # plugin_one_version = "latest" 71 | # plugin_one = "@".join((plugin_one_name, plugin_one_version)) 72 | # plugin_two = Plugin( 73 | # {"shortName": "emotional-jenkins-plugin", "version": "latest"} 74 | # ) 75 | # 76 | # assert isinstance(plugin_two, Plugin) 77 | # 78 | # plugin_list = [plugin_one, plugin_two] 79 | # 80 | # jenkins.install_plugins(plugin_list) 81 | # 82 | # assert plugin_one_name in jenkins.plugins 83 | # assert plugin_two.shortName in jenkins.plugins 84 | # 85 | # del jenkins.plugins["keyboard-shortcuts-plugin"] 86 | # del jenkins.plugins["emotional-jenkins-plugin"] 87 | # 88 | # 89 | # def test_downgrade_plugin(jenkins): 90 | # plugin_name = "console-badge" 91 | # plugin_version = "latest" 92 | # plugin = Plugin({"shortName": plugin_name, "version": plugin_version}) 93 | # 94 | # assert isinstance(plugin, Plugin) 95 | # 96 | # # Need to restart when not installing the latest version 97 | # jenkins.install_plugins([plugin]) 98 | # 99 | # installed_plugin = jenkins.plugins[plugin_name] 100 | # 101 | # assert installed_plugin.version == "1.1" 102 | # 103 | # older_plugin = Plugin({"shortName": plugin_name, "version": "1.0"}) 104 | # jenkins.install_plugins( 105 | # [older_plugin], restart=True, wait_for_reboot=True) 106 | # installed_older_plugin = jenkins.plugins[plugin_name] 107 | # 108 | # assert installed_older_plugin.version == "1.0" 109 | # 110 | # del jenkins.plugins[plugin_name] 111 | -------------------------------------------------------------------------------- /jenkinsapi_tests/systests/test_queue.py: -------------------------------------------------------------------------------- 1 | """ 2 | All kinds of testing on Jenkins Queues 3 | """ 4 | 5 | import time 6 | import logging 7 | import pytest 8 | from jenkinsapi.queue import Queue 9 | from jenkinsapi.queue import QueueItem 10 | from jenkinsapi.job import Job 11 | from jenkinsapi_tests.test_utils.random_strings import random_string 12 | from jenkinsapi_tests.systests.job_configs import LONG_RUNNING_JOB 13 | 14 | log = logging.getLogger(__name__) 15 | 16 | 17 | @pytest.fixture(scope="function") 18 | def no_executors(jenkins, request): 19 | master = jenkins.nodes["Built-In Node"] 20 | num_executors = master.get_num_executors() 21 | master.set_num_executors(0) 22 | 23 | def restore(): 24 | master.set_num_executors(num_executors) 25 | 26 | request.addfinalizer(restore) 27 | 28 | return num_executors 29 | 30 | 31 | def test_get_queue(jenkins): 32 | qq = jenkins.get_queue() 33 | assert isinstance(qq, Queue) is True 34 | 35 | 36 | def test_invoke_many_jobs(jenkins, no_executors): 37 | job_names = [random_string() for _ in range(5)] 38 | jobs = [] 39 | 40 | while len(jenkins.get_queue()) != 0: 41 | log.info("Sleeping to get queue empty...") 42 | time.sleep(1) 43 | 44 | for job_name in job_names: 45 | j = jenkins.create_job(job_name, LONG_RUNNING_JOB) 46 | jobs.append(j) 47 | j.invoke() 48 | 49 | assert j.is_queued_or_running() is True 50 | 51 | queue = jenkins.get_queue() 52 | 53 | reprString = repr(queue) 54 | assert queue.baseurl in reprString 55 | assert len(queue) == 5, queue.keys() 56 | assert isinstance(queue[queue.keys()[0]].get_job(), Job) is True 57 | items = queue.get_queue_items_for_job(job_names[2]) 58 | assert isinstance(items, list) is True 59 | assert len(items) == 1 60 | assert isinstance(items[0], QueueItem) is True 61 | assert items[0].get_parameters() == [] 62 | 63 | for _, item in queue.iteritems(): 64 | queue.delete_item(item) 65 | 66 | queue.poll() 67 | 68 | assert len(queue) == 0 69 | 70 | 71 | def test_start_and_stop_long_running_job(jenkins): 72 | job_name = random_string() 73 | j = jenkins.create_job(job_name, LONG_RUNNING_JOB) 74 | j.invoke() 75 | time.sleep(1) 76 | assert j.is_queued_or_running() is True 77 | 78 | while j.is_queued(): 79 | time.sleep(0.5) 80 | 81 | if j.is_running(): 82 | time.sleep(1) 83 | 84 | j.get_first_build().stop() 85 | time.sleep(1) 86 | assert j.is_queued_or_running() is False 87 | 88 | 89 | def test_queueitem_for_why_field(jenkins, no_executors): 90 | job_names = [random_string() for _ in range(2)] 91 | 92 | jobs = [] 93 | for job_name in job_names: 94 | j = jenkins.create_job(job_name, LONG_RUNNING_JOB) 95 | jobs.append(j) 96 | j.invoke() 97 | 98 | queue = jenkins.get_queue() 99 | for _, item in queue.iteritems(): 100 | assert isinstance(item.why, str) is True 101 | 102 | # Clean up after ourselves 103 | for _, item in queue.iteritems(): 104 | queue.delete_item(item) 105 | 106 | 107 | def test_queueitem_from_job(jenkins, no_executors): 108 | job_name = random_string() 109 | j = jenkins.create_job(job_name, LONG_RUNNING_JOB) 110 | j.invoke() 111 | 112 | qi = j.get_queue_item() 113 | assert isinstance(qi, QueueItem) 114 | assert qi.get_job() == j 115 | assert qi.get_job_name() == job_name 116 | assert qi.name == job_name 117 | assert qi.is_queued() 118 | assert not qi.is_running() 119 | -------------------------------------------------------------------------------- /jenkinsapi_tests/systests/test_quiet_down.py: -------------------------------------------------------------------------------- 1 | """ 2 | System tests for setting jenkins in quietDown mode 3 | """ 4 | 5 | import logging 6 | 7 | 8 | log = logging.getLogger(__name__) 9 | 10 | 11 | def test_quiet_down_and_cancel_quiet_down(jenkins): 12 | jenkins.poll() # jenkins should be alive 13 | 14 | jenkins.quiet_down() # put Jenkins in quietDown mode 15 | # is_quieting_down = jenkins.is_quieting_down 16 | assert jenkins.is_quieting_down is True 17 | 18 | jenkins.poll() # jenkins should be alive 19 | 20 | jenkins.cancel_quiet_down() # leave quietDown mode 21 | 22 | # is_quieting_down = jenkins_api['quietingDown'] 23 | assert jenkins.is_quieting_down is False 24 | 25 | jenkins.poll() # jenkins should be alive 26 | -------------------------------------------------------------------------------- /jenkinsapi_tests/systests/test_restart.py: -------------------------------------------------------------------------------- 1 | """ 2 | System tests for restarting jenkins 3 | 4 | NB: this test will be very time consuming because 5 | after restart it will wait for jenkins to boot 6 | """ 7 | 8 | import time 9 | import logging 10 | import pytest 11 | from requests import HTTPError, ConnectionError 12 | 13 | log = logging.getLogger(__name__) 14 | 15 | 16 | def wait_for_restart(jenkins): 17 | wait = 15 18 | count = 0 19 | max_count = 30 20 | success = False 21 | msg = ( 22 | "Jenkins has not restarted yet! (This is try %s of %s, " 23 | "waited %s seconds so far) " 24 | "Sleeping %s seconds and trying again..." 25 | ) 26 | 27 | while count < max_count or not success: 28 | time.sleep(wait) 29 | try: 30 | jenkins.poll() 31 | log.info("Jenkins restarted successfully.") 32 | success = True 33 | break 34 | except HTTPError as ex: 35 | log.info(ex) 36 | except ConnectionError as ex: 37 | log.info(ex) 38 | 39 | log.info(msg, count + 1, max_count, count * wait, wait) 40 | count += 1 41 | 42 | if not success: 43 | msg = ( 44 | "Jenkins did not come back from safe restart! " 45 | "Waited {0} seconds altogether. This " 46 | "failure may cause other failures." 47 | ) 48 | log.critical(msg.format(count * wait)) 49 | pytest.fail(msg) 50 | 51 | 52 | def test_safe_restart_wait(jenkins): 53 | jenkins.poll() # jenkins should be alive 54 | jenkins.safe_restart() # restart and wait for reboot (default) 55 | jenkins.poll() # jenkins should be alive again 56 | 57 | 58 | def test_safe_restart_dont_wait(jenkins): 59 | jenkins.poll() # jenkins should be alive 60 | jenkins.safe_restart(wait_for_reboot=False) 61 | # Jenkins sleeps for 10 seconds before actually restarting 62 | time.sleep(11) 63 | with pytest.raises((HTTPError, ConnectionError)): 64 | # this is a 503: jenkins is still restarting 65 | jenkins.poll() 66 | # the test is now complete, but other tests cannot run until 67 | # jenkins has finished restarted. to avoid cascading failure 68 | # we have to wait for reboot to finish. 69 | wait_for_restart(jenkins) 70 | -------------------------------------------------------------------------------- /jenkinsapi_tests/systests/test_safe_exit.py: -------------------------------------------------------------------------------- 1 | """ 2 | System tests for `jenkinsapi.jenkins` module. 3 | """ 4 | 5 | import time 6 | import logging 7 | from jenkinsapi.build import Build 8 | from jenkinsapi_tests.test_utils.random_strings import random_string 9 | from jenkinsapi_tests.systests.job_configs import LONG_RUNNING_JOB 10 | 11 | 12 | log = logging.getLogger(__name__) 13 | 14 | 15 | def test_safe_exit(jenkins): 16 | job_name = "Bcreate_%s" % random_string() 17 | job = jenkins.create_job(job_name, LONG_RUNNING_JOB) 18 | qq = job.invoke() 19 | time.sleep(3) 20 | bn = qq.block_until_building(delay=3).get_number() 21 | assert isinstance(bn, int) 22 | 23 | build = qq.get_build() 24 | assert isinstance(build, Build) 25 | assert build.is_running() 26 | 27 | # A job is now running and safe_exit should await running jobs 28 | # Call, but wait only for 5 seconds then cancel exit 29 | jenkins.safe_exit(wait_for_exit=False) 30 | time.sleep(5) 31 | 32 | jenkins.cancel_quiet_down() # leave quietDown mode 33 | assert jenkins.is_quieting_down is False 34 | 35 | build.stop() 36 | # if we call next line right away - Jenkins have no time to stop job 37 | # so we wait a bit 38 | while build.is_running(): 39 | time.sleep(0.5) 40 | 41 | console = build.get_console() 42 | assert isinstance(console, str) 43 | assert "Started by user" in console 44 | -------------------------------------------------------------------------------- /jenkinsapi_tests/systests/test_scm.py: -------------------------------------------------------------------------------- 1 | # ''' 2 | # System tests for `jenkinsapi.jenkins` module. 3 | # ''' 4 | # To run unittests on python 2.6 please use unittest2 library 5 | # try: 6 | # import unittest2 as unittest 7 | # except ImportError: 8 | # import unittest 9 | # from jenkinsapi_tests.systests.base import BaseSystemTest 10 | # from jenkinsapi_tests.test_utils.random_strings import random_string 11 | # from jenkinsapi_tests.systests.job_configs import SCM_GIT_JOB 12 | 13 | # # Maybe have a base class for all SCM test activites? 14 | # class TestSCMGit(BaseSystemTest): 15 | # # Maybe it makes sense to move plugin dependencies outside the code. 16 | # # Have a config to dependencies mapping from the launcher can use 17 | # # to install plugins. 18 | # def test_get_revision(self): 19 | # job_name = 'git_%s' % random_string() 20 | # job = self.jenkins.create_job(job_name, SCM_GIT_JOB) 21 | # ii = job.invoke() 22 | # ii.block(until='completed') 23 | # self.assertFalse(ii.is_running()) 24 | # b = ii.get_build() 25 | # try: 26 | # self.assertIsInstance(b.get_revision(), basestring) 27 | # except NameError: 28 | # # Python3 29 | # self.assertIsInstance(b.get_revision(), str) 30 | 31 | # if __name__ == '__main__': 32 | # unittest.main() 33 | -------------------------------------------------------------------------------- /jenkinsapi_tests/systests/test_views.py: -------------------------------------------------------------------------------- 1 | """ 2 | System tests for `jenkinsapi.jenkins` module. 3 | """ 4 | 5 | import logging 6 | from jenkinsapi.view import View 7 | from jenkinsapi.views import Views 8 | from jenkinsapi.job import Job 9 | from jenkinsapi.api import get_view_from_url 10 | from jenkinsapi_tests.systests.job_configs import EMPTY_JOB 11 | from jenkinsapi_tests.systests.view_configs import VIEW_WITH_FILTER_AND_REGEX 12 | from jenkinsapi_tests.test_utils.random_strings import random_string 13 | 14 | log = logging.getLogger(__name__) 15 | 16 | 17 | def create_job(jenkins, job_name="whatever"): 18 | job = jenkins.create_job(job_name, EMPTY_JOB) 19 | return job 20 | 21 | 22 | def test_make_views(jenkins): 23 | view_name = random_string() 24 | assert view_name not in jenkins.views 25 | new_view = jenkins.views.create(view_name) 26 | assert view_name in jenkins.views 27 | assert isinstance(new_view, View) is True 28 | assert view_name == str(new_view) 29 | 30 | # Can we create a view that already exists? 31 | existing = jenkins.views.create(view_name) 32 | assert existing == new_view 33 | 34 | # Can we use the API convenience methods 35 | new_view_1 = get_view_from_url(new_view.baseurl) 36 | assert new_view == new_view_1 37 | 38 | del jenkins.views[view_name] 39 | 40 | 41 | def test_add_job_to_view(jenkins): 42 | job_name = random_string() 43 | create_job(jenkins, job_name) 44 | 45 | view_name = random_string() 46 | assert view_name not in jenkins.views 47 | new_view = jenkins.views.create(view_name) 48 | assert view_name in jenkins.views 49 | assert isinstance(new_view, View) is True 50 | 51 | assert job_name not in new_view 52 | assert new_view.add_job(job_name) is True 53 | assert job_name in new_view 54 | assert isinstance(new_view[job_name], Job) is True 55 | 56 | assert len(new_view) == 1 57 | for j_name, j in new_view.iteritems(): 58 | assert j_name == job_name 59 | assert isinstance(j, Job) is True 60 | 61 | for j in new_view.values(): 62 | assert isinstance(j, Job) is True 63 | 64 | jobs = new_view.items() 65 | assert isinstance(jobs, list) is True 66 | assert isinstance(jobs[0], tuple) is True 67 | 68 | assert new_view.add_job(job_name) is False 69 | assert new_view.add_job("unknown") is False 70 | 71 | del jenkins.views[view_name] 72 | 73 | 74 | def test_create_and_delete_views(jenkins): 75 | view1_name = random_string() 76 | new_view = jenkins.views.create(view1_name) 77 | assert isinstance(new_view, View) is True 78 | assert view1_name in jenkins.views 79 | del jenkins.views[view1_name] 80 | assert view1_name not in jenkins.views 81 | 82 | 83 | def test_create_and_delete_views_by_url(jenkins): 84 | view1_name = random_string() 85 | new_view = jenkins.views.create(view1_name) 86 | assert isinstance(new_view, View) is True 87 | assert view1_name in jenkins.views 88 | 89 | view_url = new_view.baseurl 90 | view_by_url = jenkins.get_view_by_url(view_url) 91 | assert isinstance(view_by_url, View) is True 92 | jenkins.delete_view_by_url(view_url) 93 | 94 | assert view1_name not in jenkins.views 95 | 96 | 97 | def test_delete_view_which_does_not_exist(jenkins): 98 | view1_name = random_string() 99 | assert view1_name not in jenkins.views 100 | del jenkins.views[view1_name] 101 | 102 | 103 | def test_update_view_config(jenkins): 104 | view_name = random_string() 105 | new_view = jenkins.views.create(view_name) 106 | assert isinstance(new_view, View) is True 107 | assert view_name in jenkins.views 108 | 109 | config = jenkins.views[view_name].get_config().strip() 110 | new_view_config = VIEW_WITH_FILTER_AND_REGEX % view_name 111 | assert config != new_view_config 112 | 113 | jenkins.views[view_name].update_config(new_view_config) 114 | config = jenkins.views[view_name].get_config().strip() 115 | assert config == new_view_config 116 | 117 | 118 | def test_make_nested_views(jenkins): 119 | job = create_job(jenkins) 120 | top_view_name = random_string() 121 | sub1_view_name = random_string() 122 | sub2_view_name = random_string() 123 | 124 | assert top_view_name not in jenkins.views 125 | tv = jenkins.views.create(top_view_name, Views.NESTED_VIEW) 126 | assert top_view_name in jenkins.views 127 | assert isinstance(tv, View) is True 128 | 129 | # Empty sub view 130 | sv1 = tv.views.create(sub1_view_name) 131 | assert sub1_view_name in tv.views 132 | assert isinstance(sv1, View) is True 133 | 134 | # Sub view with job in it 135 | tv.views[sub2_view_name] = job.name 136 | assert sub2_view_name in tv.views 137 | sv2 = tv.views[sub2_view_name] 138 | assert isinstance(sv2, View) is True 139 | assert job.name in sv2 140 | 141 | # Can we use the API convenience methods 142 | new_view = get_view_from_url(sv2.baseurl) 143 | assert new_view == sv2 144 | 145 | 146 | def test_add_to_view_after_copy(jenkins): 147 | # This test is for issue #291 148 | job = create_job(jenkins) 149 | new_job_name = random_string() 150 | view_name = random_string() 151 | new_view = jenkins.views.create(view_name) 152 | new_view = jenkins.views[view_name] 153 | new_job = jenkins.copy_job(job.name, new_job_name) 154 | assert new_view.add_job(new_job.name) is True 155 | assert new_job.name in new_view 156 | 157 | 158 | def test_get_job_config(jenkins): 159 | # This test is for issue #301 160 | job = create_job(jenkins) 161 | view_name = random_string() 162 | new_view = jenkins.views.create(view_name) 163 | 164 | assert new_view.add_job(job.name) is True 165 | 166 | assert " 7 | 8 | %s 9 | true 10 | true 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | regex 26 | false 27 | 28 | """.strip() 29 | -------------------------------------------------------------------------------- /jenkinsapi_tests/test_utils/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pycontribs/jenkinsapi/ffb35b6dca607b9aa5a65d4e49aa1ca76946adbf/jenkinsapi_tests/test_utils/__init__.py -------------------------------------------------------------------------------- /jenkinsapi_tests/test_utils/random_strings.py: -------------------------------------------------------------------------------- 1 | import random 2 | import string 3 | 4 | 5 | def random_string(length=10): 6 | return "".join( 7 | random.choice(string.ascii_lowercase) for i in range(length) 8 | ) 9 | 10 | 11 | if __name__ == "__main__": 12 | print(random_string()) 13 | -------------------------------------------------------------------------------- /jenkinsapi_tests/unittests/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pycontribs/jenkinsapi/ffb35b6dca607b9aa5a65d4e49aa1ca76946adbf/jenkinsapi_tests/unittests/__init__.py -------------------------------------------------------------------------------- /jenkinsapi_tests/unittests/test_build_scm_git.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | from . import configs 3 | from jenkinsapi.build import Build 4 | from jenkinsapi.job import Job 5 | 6 | 7 | @pytest.fixture(scope="function") 8 | def jenkins(mocker): 9 | return mocker.MagicMock() 10 | 11 | 12 | @pytest.fixture(scope="function") 13 | def job(monkeypatch, jenkins): 14 | def fake_poll(cls, tree=None): # pylint: disable=unused-argument 15 | return configs.JOB_DATA 16 | 17 | monkeypatch.setattr(Job, "_poll", fake_poll) 18 | 19 | fake_job = Job("http://", "Fake_Job", jenkins) 20 | return fake_job 21 | 22 | 23 | @pytest.fixture(scope="function") 24 | def build(job, monkeypatch): 25 | def fake_poll(cls, tree=None): # pylint: disable=unused-argument 26 | return configs.BUILD_SCM_DATA 27 | 28 | monkeypatch.setattr(Build, "_poll", fake_poll) 29 | 30 | return Build("http://", 97, job) 31 | 32 | 33 | def test_git_scm(build): 34 | """ 35 | Can we extract git build revision data from a build object? 36 | """ 37 | assert isinstance(build.get_revision(), str) 38 | assert build.get_revision() == "7def9ed6e92580f37d00e4980c36c4d36e68f702" 39 | 40 | 41 | def test_git_revision_branch(build): 42 | """ 43 | Can we extract git build branch from a build object? 44 | """ 45 | assert isinstance(build.get_revision_branch(), list) 46 | assert len(build.get_revision_branch()) == 1 47 | assert isinstance(build.get_revision_branch()[0], dict) 48 | assert ( 49 | build.get_revision_branch()[0]["SHA1"] 50 | == "7def9ed6e92580f37d00e4980c36c4d36e68f702" 51 | ) 52 | assert build.get_revision_branch()[0]["name"] == "origin/unstable" 53 | 54 | 55 | def test_git_repo_url(build): 56 | """ 57 | Can we Extract git repo url for a given build 58 | """ 59 | assert isinstance(build.get_repo_url(), str) 60 | assert ( 61 | build.get_repo_url() 62 | == "https://github.com/salimfadhley/jenkinsapi.git" 63 | ) 64 | -------------------------------------------------------------------------------- /jenkinsapi_tests/unittests/test_fingerprint.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | import hashlib 3 | from jenkinsapi.jenkins import Jenkins 4 | from jenkinsapi.jenkinsbase import JenkinsBase 5 | from jenkinsapi.fingerprint import Fingerprint 6 | from jenkinsapi.utils.requester import Requester 7 | from requests.exceptions import HTTPError 8 | 9 | 10 | @pytest.fixture(scope="function") 11 | def jenkins(monkeypatch): 12 | def fake_poll(cls, tree=None): # pylint: disable=unused-argument 13 | return {} 14 | 15 | monkeypatch.setattr(Jenkins, "_poll", fake_poll) 16 | 17 | return Jenkins( 18 | "http://localhost:8080", username="foouser", password="foopassword" 19 | ) 20 | 21 | 22 | @pytest.fixture(scope="module") 23 | def dummy_md5(): 24 | md = hashlib.md5() 25 | md.update("some dummy string".encode("ascii")) 26 | return md.hexdigest() 27 | 28 | 29 | def test_object_creation(jenkins, dummy_md5, monkeypatch): 30 | def fake_poll(cls, tree=None): # pylint: disable=unused-argument 31 | return {} 32 | 33 | monkeypatch.setattr(JenkinsBase, "_poll", fake_poll) 34 | fp_instance = Fingerprint("http://foo:8080", dummy_md5, jenkins) 35 | 36 | assert isinstance(fp_instance, Fingerprint) 37 | assert str(fp_instance) == dummy_md5 38 | assert fp_instance.valid() 39 | 40 | 41 | def test_valid_for_404(jenkins, dummy_md5, monkeypatch): 42 | class FakeResponse(object): 43 | status_code = 404 44 | text = "{}" 45 | 46 | class FakeHTTPError(HTTPError): 47 | def __init__(self): 48 | self.response = FakeResponse() 49 | 50 | def fake_poll(cls, tree=None): # pylint: disable=unused-argument 51 | raise FakeHTTPError() 52 | 53 | monkeypatch.setattr(JenkinsBase, "_poll", fake_poll) 54 | 55 | def fake_get_url( 56 | url, # pylint: disable=unused-argument 57 | params=None, # pylint: disable=unused-argument 58 | headers=None, # pylint: disable=unused-argument 59 | allow_redirects=True, # pylint: disable=unused-argument 60 | stream=False, 61 | ): # pylint: disable=unused-argument 62 | 63 | return FakeResponse() 64 | 65 | monkeypatch.setattr(Requester, "get_url", fake_get_url) 66 | 67 | fingerprint = Fingerprint("http://foo:8080", dummy_md5, jenkins) 68 | assert fingerprint.valid() is True 69 | 70 | 71 | def test_invalid_for_401(jenkins, dummy_md5, monkeypatch): 72 | class FakeResponse(object): 73 | status_code = 401 74 | text = "{}" 75 | 76 | class FakeHTTPError(HTTPError): 77 | def __init__(self): 78 | self.response = FakeResponse() 79 | 80 | def fake_poll(cls, tree=None): # pylint: disable=unused-argument 81 | raise FakeHTTPError() 82 | 83 | monkeypatch.setattr(JenkinsBase, "_poll", fake_poll) 84 | 85 | def fake_get_url( 86 | url, # pylint: disable=unused-argument 87 | params=None, # pylint: disable=unused-argument 88 | headers=None, # pylint: disable=unused-argument 89 | allow_redirects=True, # pylint: disable=unused-argument 90 | stream=False, 91 | ): # pylint: disable=unused-argument 92 | 93 | return FakeResponse() 94 | 95 | monkeypatch.setattr(Requester, "get_url", fake_get_url) 96 | 97 | fingerprint = Fingerprint("http://foo:8080", dummy_md5, jenkins) 98 | assert fingerprint.valid() is not True 99 | -------------------------------------------------------------------------------- /jenkinsapi_tests/unittests/test_label.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | from jenkinsapi.label import Label 3 | 4 | 5 | DATA = { 6 | "actions": [], 7 | "busyExecutors": 0, 8 | "clouds": [], 9 | "description": None, 10 | "idleExecutors": 0, 11 | "loadStatistics": {}, 12 | "name": "jenkins-slave", 13 | "nodes": [], 14 | "offline": True, 15 | "tiedJobs": [ 16 | { 17 | "name": "test_job1", 18 | "url": "http://jtest:8080/job/test_job1/", 19 | "color": "blue", 20 | }, 21 | { 22 | "name": "test_job2", 23 | "url": "http://jtest:8080/job/test_job2/", 24 | "color": "blue", 25 | }, 26 | { 27 | "name": "test_job3", 28 | "url": "http://jtest:8080/job/test_job3/", 29 | "color": "blue", 30 | }, 31 | { 32 | "name": "test_job4", 33 | "url": "http://jtest:8080/job/test_job4/", 34 | "color": "blue", 35 | }, 36 | ], 37 | "totalExecutors": 0, 38 | "propertiesList": [], 39 | } 40 | 41 | DATA_JOB_NAMES = { 42 | "tiedJobs": [ 43 | {"name": "test_job1"}, 44 | {"name": "test_job2"}, 45 | {"name": "test_job3"}, 46 | {"name": "test_job4"}, 47 | ] 48 | } 49 | 50 | DATA_JOBS = [ 51 | { 52 | "url": "http://jtest:8080/job/test_job1/", 53 | "color": "blue", 54 | "name": "test_job1", 55 | }, 56 | { 57 | "url": "http://jtest:8080/job/test_job2/", 58 | "color": "blue", 59 | "name": "test_job2", 60 | }, 61 | { 62 | "url": "http://jtest:8080/job/test_job3/", 63 | "color": "blue", 64 | "name": "test_job3", 65 | }, 66 | { 67 | "url": "http://jtest:8080/job/test_job4/", 68 | "color": "blue", 69 | "name": "test_job4", 70 | }, 71 | ] 72 | 73 | 74 | @pytest.fixture(scope="function") 75 | def label(monkeypatch, mocker): 76 | def fake_poll(cls, tree=None): # pylint: disable=unused-argument 77 | return DATA 78 | 79 | monkeypatch.setattr(Label, "_poll", fake_poll) 80 | jenkins = mocker.MagicMock() 81 | 82 | return Label("http://foo:8080", "jenkins-slave", jenkins) 83 | 84 | 85 | def test_repr(label): 86 | # Can we produce a repr string for this object 87 | repr(label) 88 | 89 | 90 | def test_name(label): 91 | with pytest.raises(AttributeError): 92 | label.id() 93 | assert label.labelname == "jenkins-slave" 94 | 95 | 96 | def test_get_tied_job_names(label): 97 | assert label.get_tied_job_names() == DATA_JOBS 98 | 99 | 100 | def test_online(label): 101 | assert label.is_online() is False 102 | -------------------------------------------------------------------------------- /jenkinsapi_tests/unittests/test_misc.py: -------------------------------------------------------------------------------- 1 | import jenkinsapi 2 | 3 | 4 | def test_jenkinsapi_version(): 5 | """Verify that we can get the jenkinsapi version number from the 6 | package's __version__ property. 7 | """ 8 | version = jenkinsapi.__version__ 9 | # only first two parts must be interger, 1.0.dev5 being a valid version. 10 | parts = [int(x) for x in version.split(".")[0:2]] 11 | for part in parts: 12 | assert part >= 0, "Implausible version number: %r" % version 13 | -------------------------------------------------------------------------------- /jenkinsapi_tests/unittests/test_node.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | from jenkinsapi.node import Node 3 | 4 | 5 | DATA = { 6 | "actions": [], 7 | "displayName": "bobnit", 8 | "executors": [{}], 9 | "icon": "computer.png", 10 | "idle": True, 11 | "jnlpAgent": False, 12 | "launchSupported": True, 13 | "loadStatistics": {}, 14 | "manualLaunchAllowed": True, 15 | "monitorData": { 16 | "hudson.node_monitors.SwapSpaceMonitor": { 17 | "availablePhysicalMemory": 7681417216, 18 | "availableSwapSpace": 12195983360, 19 | "totalPhysicalMemory": 8374497280, 20 | "totalSwapSpace": 12195983360, 21 | }, 22 | "hudson.node_monitors.ArchitectureMonitor": "Linux (amd64)", 23 | "hudson.node_monitors.ResponseTimeMonitor": {"average": 64}, 24 | "hudson.node_monitors.TemporarySpaceMonitor": { 25 | "path": "/tmp", 26 | "size": 250172776448, 27 | }, 28 | "hudson.node_monitors.DiskSpaceMonitor": { 29 | "path": "/home/sal/jenkins", 30 | "size": 170472026112, 31 | }, 32 | "hudson.node_monitors.ClockMonitor": {"diff": 6736}, 33 | }, 34 | "numExecutors": 1, 35 | "offline": False, 36 | "offlineCause": None, 37 | "oneOffExecutors": [], 38 | "temporarilyOffline": False, 39 | } 40 | 41 | 42 | @pytest.fixture(scope="function") 43 | def node(monkeypatch, mocker): 44 | def fake_poll(cls, tree=None): # pylint: disable=unused-argument 45 | return DATA 46 | 47 | monkeypatch.setattr(Node, "_poll", fake_poll) 48 | jenkins = mocker.MagicMock() 49 | 50 | return Node(jenkins, "http://foo:8080", "bobnit", {}) 51 | 52 | 53 | def test_repr(node): 54 | # Can we produce a repr string for this object 55 | repr(node) 56 | 57 | 58 | def test_name(node): 59 | with pytest.raises(AttributeError): 60 | node.id() 61 | assert node.name == "bobnit" 62 | 63 | 64 | def test_online(node): 65 | assert node.is_online() is True 66 | 67 | 68 | def test_available_physical_memory(node): 69 | monitor = DATA["monitorData"]["hudson.node_monitors.SwapSpaceMonitor"] 70 | expected_value = monitor["availablePhysicalMemory"] 71 | assert node.get_available_physical_memory() == expected_value 72 | 73 | 74 | def test_available_swap_space(node): 75 | monitor = DATA["monitorData"]["hudson.node_monitors.SwapSpaceMonitor"] 76 | expected_value = monitor["availableSwapSpace"] 77 | assert node.get_available_swap_space() == expected_value 78 | 79 | 80 | def test_total_physical_memory(node): 81 | monitor = DATA["monitorData"]["hudson.node_monitors.SwapSpaceMonitor"] 82 | expected_value = monitor["totalPhysicalMemory"] 83 | assert node.get_total_physical_memory() == expected_value 84 | 85 | 86 | def test_total_swap_space(node): 87 | monitor = DATA["monitorData"]["hudson.node_monitors.SwapSpaceMonitor"] 88 | expected_value = monitor["totalSwapSpace"] 89 | assert node.get_total_swap_space() == expected_value 90 | 91 | 92 | def test_workspace_path(node): 93 | monitor = DATA["monitorData"]["hudson.node_monitors.DiskSpaceMonitor"] 94 | expected_value = monitor["path"] 95 | assert node.get_workspace_path() == expected_value 96 | 97 | 98 | def test_workspace_size(node): 99 | monitor = DATA["monitorData"]["hudson.node_monitors.DiskSpaceMonitor"] 100 | expected_value = monitor["size"] 101 | assert node.get_workspace_size() == expected_value 102 | 103 | 104 | def test_temp_path(node): 105 | monitor = DATA["monitorData"]["hudson.node_monitors.TemporarySpaceMonitor"] 106 | expected_value = monitor["path"] 107 | assert node.get_temp_path() == expected_value 108 | 109 | 110 | def test_temp_size(node): 111 | monitor = DATA["monitorData"]["hudson.node_monitors.TemporarySpaceMonitor"] 112 | expected_value = monitor["size"] 113 | assert node.get_temp_size() == expected_value 114 | 115 | 116 | def test_architecture(node): 117 | expected_value = DATA["monitorData"][ 118 | "hudson.node_monitors.ArchitectureMonitor" 119 | ] 120 | assert node.get_architecture() == expected_value 121 | 122 | 123 | def test_response_time(node): 124 | monitor = DATA["monitorData"]["hudson.node_monitors.ResponseTimeMonitor"] 125 | expected_value = monitor["average"] 126 | assert node.get_response_time() == expected_value 127 | 128 | 129 | def test_clock_difference(node): 130 | monitor = DATA["monitorData"]["hudson.node_monitors.ClockMonitor"] 131 | expected_value = monitor["diff"] 132 | assert node.get_clock_difference() == expected_value 133 | -------------------------------------------------------------------------------- /jenkinsapi_tests/unittests/test_result_set.py: -------------------------------------------------------------------------------- 1 | import mock 2 | 3 | # To run unittests on python 2.6 please use unittest2 library 4 | try: 5 | import unittest2 as unittest 6 | except ImportError: 7 | import unittest 8 | 9 | from jenkinsapi.result_set import ResultSet 10 | from jenkinsapi.result import Result 11 | 12 | 13 | class TestResultSet(unittest.TestCase): 14 | 15 | DATA = { 16 | "duration": 0.0, 17 | "failCount": 2, 18 | "passCount": 0, 19 | "skipCount": 0, 20 | "suites": [ 21 | { 22 | "cases": [ 23 | { 24 | "age": 1, 25 | "className": ":setup", 31 | "skipped": False, 32 | "status": "FAILED", 33 | "stderr": None, 34 | "stdout": None, 35 | }, 36 | { 37 | "age": 1, 38 | "className": "nose.failure.Failure", 39 | "duration": 0.0, 40 | "errorDetails": "No module named mock", 41 | "errorStackTrace": 'Traceback (most recent call last):\n File "/usr/lib/python2.7/unittest/case.py", line 332, in run\n testMethod()\n File "/usr/lib/python2.7/dist-packages/nose/loader.py", line 390, in loadTestsFromName\n addr.filename, addr.module)\n File "/usr/lib/python2.7/dist-packages/nose/importer.py", line 39, in importFromPath\n return self.importFromDir(dir_path, fqname)\n File "/usr/lib/python2.7/dist-packages/nose/importer.py", line 86, in importFromDir\n mod = load_module(part_fqname, fh, filename, desc)\n File "/var/lib/jenkins/jobs/test_jenkinsapi/workspace/jenkinsapi/src/jenkinsapi_tests/unittests/test_build.py", line 1, in \n import mock\nImportError: No module named mock\n', # noqa 42 | "failedSince": 88, 43 | "name": "runTest", 44 | "skipped": False, 45 | "status": "FAILED", 46 | "stderr": None, 47 | "stdout": None, 48 | }, 49 | ], 50 | "duration": 0.0, 51 | "id": None, 52 | "name": "nosetests", 53 | "stderr": None, 54 | "stdout": None, 55 | "timestamp": None, 56 | } 57 | ], 58 | "childReports": [ 59 | {"child": {"number": 1915, "url": "url1"}, "result": None}, 60 | ], 61 | } 62 | 63 | @mock.patch.object(ResultSet, "_poll") 64 | def setUp(self, _poll): 65 | _poll.return_value = self.DATA 66 | 67 | # def __init__(self, url, build ): 68 | 69 | self.b = mock.MagicMock() # Build object 70 | self.b.__str__.return_value = "FooBuild" 71 | self.rs = ResultSet("http://", self.b) 72 | 73 | def testRepr(self): 74 | # Can we produce a repr string for this object 75 | repr(self.rs) 76 | 77 | def testName(self): 78 | with self.assertRaises(AttributeError): 79 | self.rs.id() 80 | 81 | self.assertEqual(self.rs.name, "Test Result for FooBuild") 82 | 83 | def testBuildComponents(self): 84 | self.assertTrue(self.rs.items()) 85 | for k, v in self.rs.items(): 86 | self.assertIsInstance(k, str) 87 | self.assertIsInstance(v, Result) 88 | self.assertIsInstance(v.identifier(), str) 89 | 90 | 91 | if __name__ == "__main__": 92 | unittest.main() 93 | -------------------------------------------------------------------------------- /pyproject.toml: -------------------------------------------------------------------------------- 1 | [build-system] 2 | requires = ["flit_core >=3.2,<4"] 3 | build-backend = "flit_core.buildapi" 4 | 5 | [project] 6 | name = "jenkinsapi" 7 | version = "0.3.14" 8 | authors = [ 9 | {name = "Salim Fadhley", email = "salimfadhley@gmail.com"}, 10 | {name = "Aleksey Maksimov", email = "ctpeko3a@gmail.com"}, 11 | {name = "Clinton Steiner", email = "clintonsteiner@gmail.com"}, 12 | ] 13 | maintainers = [ 14 | {name = "Aleksey Maksimov", email = "ctpeko3a@gmail.com"}, 15 | {name = "Clinton Steiner", email = "clintonsteiner@gmail.com"}, 16 | ] 17 | description = "A Python API for accessing resources on a Jenkins continuous-integration server." 18 | readme = "README.rst" 19 | license = {text = "MIT license"} 20 | classifiers = [ 21 | "Development Status :: 5 - Production/Stable", 22 | "Environment :: Console", 23 | "Intended Audience :: Developers", 24 | "Intended Audience :: Information Technology", 25 | "Intended Audience :: System Administrators", 26 | "License :: OSI Approved :: MIT License", 27 | "Natural Language :: English", 28 | "Operating System :: OS Independent", 29 | "Programming Language :: Python", 30 | "Programming Language :: Python :: 3", 31 | "Programming Language :: Python :: 3.8", 32 | "Programming Language :: Python :: 3.9", 33 | "Programming Language :: Python :: 3.10", 34 | "Programming Language :: Python :: 3.11", 35 | "Programming Language :: Python :: 3.12", 36 | "Programming Language :: Python :: 3.13", 37 | "Topic :: Software Development :: Testing", 38 | "Topic :: Utilities", 39 | ] 40 | requires-python = ">=3.8" 41 | dependencies = [ 42 | "pytz>=2014.4", 43 | "requests>=2.3.0", 44 | ] 45 | 46 | [tool.setuptools] 47 | packages = ["jenkinsapi", "jenkinsapi_utils", "jenkinsapi_tests"] 48 | include-package-data = false 49 | 50 | [tool.pbr] 51 | warnerrors = "True" 52 | 53 | [project.scripts] 54 | jenkins_invoke = "jenkinsapi.command_line.jenkins_invoke:main" 55 | jenkinsapi_version = "jenkinsapi.command_line.jenkinsapi_version:main" 56 | 57 | [tool.build_sphinx] 58 | source-dir = "doc/source" 59 | build-dir = "doc/build" 60 | all_files = "1" 61 | 62 | [tool.upload_sphinx] 63 | upload-dir = "doc/build/html" 64 | 65 | [tool.distutils.bdist_wheel] 66 | universal = 1 67 | 68 | [tool.pycodestyle] 69 | exclude = ".tox,doc/source/conf.py,build,.venv,.eggs" 70 | max-line-length = "99" 71 | 72 | [dependency-groups] 73 | dev = [ 74 | "pytest-mock>=3.14.0", 75 | "pytest>=8.3.4", 76 | "pytest-cov>=4.0.0", 77 | "pycodestyle>=2.3.1", 78 | "astroid>=1.4.8", 79 | "pylint>=1.7.1", 80 | "tox>=2.3.1", 81 | "mock>=5.1.0", 82 | "codecov>=2.1.13", 83 | "requests-kerberos>=0.15.0", 84 | "ruff>=0.9.6", 85 | ] 86 | docs = [ 87 | "docutils>=0.20.1", 88 | "furo>=2024.8.6", 89 | "myst-parser>=3.0.1", 90 | "pygments>=2.19.1", 91 | "sphinx>=7.1.2", 92 | ] 93 | 94 | [tool.ruff] 95 | line-length = 79 96 | 97 | [tool.ruff.lint] 98 | select = ["E9", "F63", "F7", "F82"] # Equivalent to flake8’s default rules 99 | ignore = ["F821"] #, "W503", "W504" 100 | --------------------------------------------------------------------------------