pax_global_header00006660000000000000000000000064131652306720014517gustar00rootroot0000000000000052 comment=02c2b97d4769424cb8a3c8009871ad97d5779f07 ansible-tower-cli-3.2.0/000077500000000000000000000000001316523067200150415ustar00rootroot00000000000000ansible-tower-cli-3.2.0/.coveragerc000066400000000000000000000004301316523067200171570ustar00rootroot00000000000000[run] source = tower_cli/ omit = tower_cli/compat.py branch = True [html] directory = .coverage-html/ [report] exclude_lines = # Enable the standard pragma. pragma: no cover # Don't complain about missing debug-only code: def __repr__ show_missing = True ansible-tower-cli-3.2.0/.gitignore000066400000000000000000000015371316523067200170370ustar00rootroot00000000000000# Python *.py[co] __pycache__ build # Emacs *~ .\#* # RPM MANIFEST dist rpm-build # Eclipse/PyDev .project .pydevproject # PyCharm .idea #IntelliJ IDEA *.iml # Mac OS X .DS_Store # manpage build docs/man/man3/* # Sublime Text *.sublime-project *.sublime-workspace # docsite docsite/latest/rst/modules docsite/latest/*.html docsite/latest/_static/*.gif docsite/latest/_static/*.png docsite/latest/_static/websupport.js docsite/latest/searchindex.js docsite/latest/htmlout # deb building debian/ # Vim swap files *.swp *.swo # coverage .coverage-html/ .coverage # tox .tox # jenkins cover-html coverage.xml results.xml # ignore personal configs .tower_cli.cfg # ignore python egg *.egg-info/ # Sphinx docs_wip/build/* # tower-cli v1 build tower_cli_v1/ bin/tower-cli-v1 setup_v1.py # tower-cli v2 build tower_cli_v2/ bin/tower-cli-v2 setup_v2.py ansible-tower-cli-3.2.0/.travis.yml000066400000000000000000000006041316523067200171520ustar00rootroot00000000000000sudo: false language: python python: - "2.7" - "3.3" - "3.4" - "3.5" - "nightly" matrix: allow_failures: - python: nightly install: - pip install flake8 - pip install tox-travis - pip install coveralls before_script: if [[ $TRAVIS_PYTHON_VERSION != '2.6' ]]; then flake8 . --max-line-length=120; fi script: - tox after_success: coveralls ansible-tower-cli-3.2.0/HISTORY.rst000066400000000000000000000217501316523067200167410ustar00rootroot00000000000000Release History =============== 3.2.0 (2017-10-04) ------------------ *General:* - Officially support using tower_cli as a python library. - Major documentation updates. From 3.2.0 docs are hosted on http://tower-cli.readthedocs.io. - Added project_update and inventory_update resources to allow canceling and deleting. *Updates from Tower 3.2:* - Migrated to API V2. All API calls will start with `/api/v2` instead of `/api/v1`. - Made inventory_source an external resource and remove the old relationship to its associated group. Remove launching inventory updates from group resource. - Added credential_type resource and significantly modified credential resource to reveal user-defined credentials feature of Tower 3.2. - Added job template extra credential (dis)association to reveal extra_credential field of 3.2 job templates. - Removed all source-specific inventory source fields and replaced them with a `credential` field. - Updated inventory resource fields to reveal smart inventory and insights integration features of Tower 3.2. - Added `list_fact` and `insights` commands to host resource to reveal smart inventory and insights integration features of Tower 3.2. - Added `instance` and `instance_group` resources to reveal instance/instance group feature of Tower 3.2. - Enabled (dis)associating instance groups to(from) organization, job_template and inventory resources to reveal instance/instance group feature of Tower 3.2. - Added support for Tower 3.2 SCM inventory sources. - Updated job_template resource fields to reveal changes in Tower 3.2, including `--diff` mode feature. - Updated job resource launch command to reveal changes in Tower 3.2, including `--diff` mode feature. - Updated ad_hoc resource fields to reveal changes in Tower 3.2, including `--diff` mode feature. Specifically, changed name of `--become` of `launch` command into `--become-enabled`. *Deprecated features:* - Removed permission resource. - Disabled launching a job using the jobs endpoint. - Removed scan jobs in favor of new job fact cache. - Removed Rackspace options. - Remove outdated association function for project’s organization. *Reflected from 3.1.8:* - Include method of installing with alias tower-cli-v2 - Fix bug of incomplete role membership lookup, preventing granting of roles. - Combine click parameters from multiple base classes in metaclass. - Fix unicode bug in human display format. - Add new page_size parameter to list view. - Add scm_update_cache_timeout field to project resource. - Begin process to deprecate python 2.6. 3.1.7 (2017-08-07) ------------------ - Follow up 3.1.6 by duplicating exceptions.py to support `import tower_cli.utils.exceptions` syntax. 3.1.6 (2017-07-18) ------------------ - Fix a usage compatibility issue for Ansible Tower modules. 3.1.5 (2017-07-12) ------------------ - Major code base file structure refactor. Now all click-related logics are moved to `tower_cli/cli/` directory, and `exceptions.py` as well as `compat.py` are moved out of utils directory into base directory. - Categorize help text options for resource action commands (like `update`) to increase readability. - Behavior change of workflow schema command. Now schema will both create new nodes and delete existing nodes when needed to make the resulting workflow topology exactly the same as described in schema file. - Add command `job_template callback` to enable conducting provisioning callback via Tower CLI. - Add new format option to just echo id. - Expand some resource fields, including hipchat rooms for notification template and allow_simultaneous for job templates. - Lookup related inventory sources with "starts with" logic if its name is not fully qualified. - Fixed a python 3.5 compatibility issue that causes job monitor traceback. - Minor typo and help text updates. 3.1.4 (2017-06-07) ------------------ - Support resource copy subcommand. - Support auth-token-based authentication for Tower CLI requests. - Support managing workflow roles, labels and notifications via Tower CLI. - Several fixes on RPM spec file. - Name change from 'foreman' to 'satellite6' in credential kind choices. - Fixed a bug where creating job templates with --extra-vars did not work after 3.1.0 upgrade. - Fixed traceback when launching job with --use-job-endpoint. - Enhanced json library usage to prevent traceback when using earlier python 2.6 versions. - Prevent throwing unnecessary warning when reading from global configuration file. 3.1.3 (2017-03-22) ------------------ - Fixed a bug where extra_vars were dropped in some commands. 3.1.2 (2017-03-21) ------------------ - Fixed a bug where global flags are not added to some commands. 3.1.1 (2017-03-13) ------------------ - Fixed a bug which blocks named resources from using runtime configure settings. - Fixed a bug in 3.1.0 which sometimes causes traceback when `pk` value is given. 3.1.0 (2017-03-09) ------------------ - Improved job monitoring functionality to enable standard out streaming, which displays real-time job output on command line. - Added workflow, workflow_job and node endpoints to manipulate workflow graph and manage workflow job resources. Reflecting workflows feature of Tower 3.1. - Added settings command to manage Tower settings via Tower CLI. Reflecting Configure Tower in Tower (CTiT) feature of Tower 3.1. - Included timeout option to certain unified job template resources. Reflecting job timeout feature of Tower 3.1. - Added unicode support to extra_vars and variable types. - Several minor bug fixes to improve user experience. 3.0.3 (2017-02-07) ------------------ - Expose custom inventory script resource to the user - Include tests and docs in the release tarball - Added job template skip_tags prompting support - Added job template callback support 3.0.2 (2016-12-08) ------------------ - Enable configuring tower-cli via environment variables 3.0.1 (2016-09-22) ------------------ - Added custom SSL certificate support 3.0.0 (2016-08-05) ------------------ - Added text indicator for resource change - Allow hosts, inventory, and groups to use variables from the command line and denote a file by starting with "@" - Added resource role for tower3.0 and permission for previous tower versions - Added notification templates - Added labels - Added description display option - Added deprecation warnings - Help text upgrades - Give indication of "changed" apart from color - New credential fields to support openstack-v2, networking and azure - New options for inventory source/group. Add implicit resource inventory script. - credential updates (no longer require user/team) - Added support for system auditors - projects (do not post to organizations/N/projects) - prompt-for JT fields + job launch options (allow blank inventory too) - Update the POST protocol for associate and disassociate actions - New job launch option for backwards compatibility - New tower-cli option to display tower-cli version - Enhanced debug log format (support multi-line debug log) 2.3.2 (2016-07-21) ------------------ - Add RPM specfile and Makefile - Tower compatibility fixes - Allow scan JTs as an option for "job_type" - Add ability to create group as subgroup of another group - Add YAML output format against JSON and humanized output formats - Add SSL corner case error handling and suggestion - Allow resource disassociation with "null" 2.3.1 (2015-12-10) ------------------ - Fixed bug affecting force-on-exists and fail_on_found options - Changed extra_vars behavior to be more compliant by re-parsing vars, even when only one source exists - Fixed group modify bug, avoid sending unwanted fields in modify requests 2.3.0 (2015-10-20) ------------------ - Fixed an issue where the settings file could be world readable - Added the ability to associate a project with an organization - Added setting "verify\_ssl" to disallow insecure connections - Added support for additional cloud credentials - Exposed additional options for a cloud inventory source - Combined " launch-time extra\_vars" with " job\_template extra\_vars" for older Tower versions - Changed the extra\_vars parameters to align with Ansible parameter handling - Added the ability to run ad hoc commands - Included more detail when displaying job information - Added an example bash script to demonstrate tower-cli usage 2.1.1 (2015-01-27) ------------------ - Added tests for Python versions 2.6 through 3.4 - Added shields for github README - Added job\_tags on job launches - Added option for project local path 2.1.0 (2015-01-21) ------------------ - Added the ability to customize the set of fields used as options for a resource - Expanded monitoring capability to include projects and inventory sources - Added support for new job\_template job launch endpoint 2.0.2 (2014-10-02) ------------------ - Added ability to set local scope for config file - Expanded credential resource to allow options for cloud credentials 2.0.1 (2014-07-18) ------------------ - Updated README and error text 2.0.0 (2014-07-15) ------------------ - Pluggable resource architecture built around click ansible-tower-cli-3.2.0/LICENSE000066400000000000000000000240411316523067200160470ustar00rootroot00000000000000Apache License Version 2.0, January 2004 http://www.apache.org/licenses/ TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 1. Definitions. "License" shall mean the terms and conditions for use, reproduction, and distribution as defined by Sections 1 through 9 of this document. "Licensor" shall mean the copyright owner or entity authorized by the copyright owner that is granting the License. "Legal Entity" shall mean the union of the acting entity and all other entities that control, are controlled by, or are under common control with that entity. For the purposes of this definition, "control" means (i) the power, direct or indirect, to cause the direction or management of such entity, whether by contract or otherwise, or (ii) ownership of fifty percent (50%) or more of the outstanding shares, or (iii) beneficial ownership of such entity. "You" (or "Your") shall mean an individual or Legal Entity exercising permissions granted by this License. "Source" form shall mean the preferred form for making modifications, including but not limited to software source code, documentation source, and configuration files. "Object" form shall mean any form resulting from mechanical transformation or translation of a Source form, including but not limited to compiled object code, generated documentation, and conversions to other media types. "Work" shall mean the work of authorship, whether in Source or Object form, made available under the License, as indicated by a copyright notice that is included in or attached to the work (an example is provided in the Appendix below). "Derivative Works" shall mean any work, whether in Source or Object form, that is based on (or derived from) the Work and for which the editorial revisions, annotations, elaborations, or other modifications represent, as a whole, an original work of authorship. For the purposes of this License, Derivative Works shall not include works that remain separable from, or merely link (or bind by name) to the interfaces of, the Work and Derivative Works thereof. "Contribution" shall mean any work of authorship, including the original version of the Work and any modifications or additions to that Work or Derivative Works thereof, that is intentionally submitted to Licensor for inclusion in the Work by the copyright owner or by an individual or Legal Entity authorized to submit on behalf of the copyright owner. For the purposes of this definition, "submitted" means any form of electronic, verbal, or written communication sent to the Licensor or its representatives, including but not limited to communication on electronic mailing lists, source code control systems, and issue tracking systems that are managed by, or on behalf of, the Licensor for the purpose of discussing and improving the Work, but excluding communication that is conspicuously marked or otherwise designated in writing by the copyright owner as "Not a Contribution." "Contributor" shall mean Licensor and any individual or Legal Entity on behalf of whom a Contribution has been received by Licensor and subsequently incorporated within the Work. 2. Grant of Copyright License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable copyright license to reproduce, prepare Derivative Works of, publicly display, publicly perform, sublicense, and distribute the Work and such Derivative Works in Source or Object form. 3. Grant of Patent License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable (except as stated in this section) patent license to make, have made, use, offer to sell, sell, import, and otherwise transfer the Work, where such license applies only to those patent claims licensable by such Contributor that are necessarily infringed by their Contribution(s) alone or by combination of their Contribution(s) with the Work to which such Contribution(s) was submitted. If You institute patent litigation against any entity (including a cross-claim or counterclaim in a lawsuit) alleging that the Work or a Contribution incorporated within the Work constitutes direct or contributory patent infringement, then any patent licenses granted to You under this License for that Work shall terminate as of the date such litigation is filed. 4. Redistribution. You may reproduce and distribute copies of the Work or Derivative Works thereof in any medium, with or without modifications, and in Source or Object form, provided that You meet the following conditions: You must give any other recipients of the Work or Derivative Works a copy of this License; and You must cause any modified files to carry prominent notices stating that You changed the files; and You must retain, in the Source form of any Derivative Works that You distribute, all copyright, patent, trademark, and attribution notices from the Source form of the Work, excluding those notices that do not pertain to any part of the Derivative Works; and If the Work includes a "NOTICE" text file as part of its distribution, then any Derivative Works that You distribute must include a readable copy of the attribution notices contained within such NOTICE file, excluding those notices that do not pertain to any part of the Derivative Works, in at least one of the following places: within a NOTICE text file distributed as part of the Derivative Works; within the Source form or documentation, if provided along with the Derivative Works; or, within a display generated by the Derivative Works, if and wherever such third-party notices normally appear. The contents of the NOTICE file are for informational purposes only and do not modify the License. You may add Your own attribution notices within Derivative Works that You distribute, alongside or as an addendum to the NOTICE text from the Work, provided that such additional attribution notices cannot be construed as modifying the License. You may add Your own copyright statement to Your modifications and may provide additional or different license terms and conditions for use, reproduction, or distribution of Your modifications, or for any such Derivative Works as a whole, provided Your use, reproduction, and distribution of the Work otherwise complies with the conditions stated in this License. 5. Submission of Contributions. Unless You explicitly state otherwise, any Contribution intentionally submitted for inclusion in the Work by You to the Licensor shall be under the terms and conditions of this License, without any additional terms or conditions. Notwithstanding the above, nothing herein shall supersede or modify the terms of any separate license agreement you may have executed with Licensor regarding such Contributions. 6. Trademarks. This License does not grant permission to use the trade names, trademarks, service marks, or product names of the Licensor, except as required for reasonable and customary use in describing the origin of the Work and reproducing the content of the NOTICE file. 7. Disclaimer of Warranty. Unless required by applicable law or agreed to in writing, Licensor provides the Work (and each Contributor provides its Contributions) on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied, including, without limitation, any warranties or conditions of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A PARTICULAR PURPOSE. You are solely responsible for determining the appropriateness of using or redistributing the Work and assume any risks associated with Your exercise of permissions under this License. 8. Limitation of Liability. In no event and under no legal theory, whether in tort (including negligence), contract, or otherwise, unless required by applicable law (such as deliberate and grossly negligent acts) or agreed to in writing, shall any Contributor be liable to You for damages, including any direct, indirect, special, incidental, or consequential damages of any character arising as a result of this License or out of the use or inability to use the Work (including but not limited to damages for loss of goodwill, work stoppage, computer failure or malfunction, or any and all other commercial damages or losses), even if such Contributor has been advised of the possibility of such damages. 9. Accepting Warranty or Additional Liability. While redistributing the Work or Derivative Works thereof, You may choose to offer, and charge a fee for, acceptance of support, warranty, indemnity, or other liability obligations and/or rights consistent with this License. However, in accepting such obligations, You may act only on Your own behalf and on Your sole responsibility, not on behalf of any other Contributor, and only if You agree to indemnify, defend, and hold each Contributor harmless for any liability incurred by, or claims asserted against, such Contributor by reason of your accepting any such warranty or additional liability. END OF TERMS AND CONDITIONS APPENDIX: How to apply the Apache License to your work To apply the Apache License to your work, attach the following boilerplate notice, with the fields enclosed by brackets "[]" replaced with your own identifying information. (Don't include the brackets!) The text should be enclosed in the appropriate comment syntax for the file format. We also recommend that a file or class name and description of purpose be included on the same "printed page" as the copyright notice for easier identification within third-party archives. Copyright [yyyy] [name of copyright owner] Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. ansible-tower-cli-3.2.0/MANIFEST.in000066400000000000000000000004101316523067200165720ustar00rootroot00000000000000# ---------------------- # -- Package Metadata -- # ---------------------- include LICENSE include README.rst include HISTORY.rst include VERSION include tower_cli/VERSION include requirements.txt recursive-include docs * recursive-include tests * include tox.ini ansible-tower-cli-3.2.0/Makefile000066400000000000000000000032111316523067200164760ustar00rootroot00000000000000VERSION = $(shell cat tower_cli/VERSION) DISTS = el6 el7 DIST_SUFFIX_el6 = DIST_SUFFIX_el7 = .centos MOCK_CFG_el6 = epel-6-x86_64 MOCK_CFG_el7 = epel-7-x86_64 .PHONY: all clean $(DISTS) .DEFAULT_GOAL = all define RPM_DIST_RULE $(eval $(1)_SRPM=rpm-build/ansible-tower-cli-$(VERSION)-1.$(1)$(DIST_SUFFIX_$(1)).src.rpm) $(eval $(1)_RPM=rpm-build/ansible-tower-cli-$(VERSION)-1.$(1)$(DIST_SUFFIX_$(1)).noarch.rpm) SRPMS += $($(1)_SRPM) RPMS += $($(1)_RPM) $($(1)_SRPM): rpm-build/.exists dist/ansible-tower-cli-$(VERSION).tar.gz packaging/rpm/ansible-tower-cli.spec mock -r $(MOCK_CFG_$(1)) --buildsrpm --spec packaging/rpm/ansible-tower-cli.spec --sources dist/ --resultdir rpm-build $($(1)_RPM): $($(1)_SRPM) mock -r $(MOCK_CFG_$(1)) --rebuild $($(1)_SRPM) --resultdir rpm-build $(1): $($(1)_RPM) endef $(foreach DIST, $(DISTS), $(eval $(call RPM_DIST_RULE,$(DIST)))) all: $(RPMS) clean: rm -rf dist rm -rf ansible_tower_cli.egg-info rm -rf rpm-build dist/ansible-tower-cli-$(VERSION).tar.gz: bin/tower-cli HISTORY.rst LICENSE MANIFEST.in README.rst VERSION requirements.txt setup.py setup.cfg @python setup.py sdist rpm-build/.exists: mkdir -p rpm-build touch rpm-build/.exists # For devel convenience install: sudo rm -rf dist/ build/ sudo python setup.py install clean_v2: rm -rf tower_cli_v2 rm -rf ansible_tower_cli_v2.egg-info rm -rf setup_v2.py rm -f bin/tower-cli-v2 setup_v2.py: cp -R tower_cli tower_cli_v2/ cp bin/tower-cli bin/tower-cli-v2 cp setup.py setup_v2.py python version_swap.py prep_v2: setup_v2.py install_v2: setup_v2.py sudo rm -rf dist/ build/ sudo python setup_v2.py install v2-refresh: clean_v2 install_v2 ansible-tower-cli-3.2.0/README.rst000066400000000000000000000007721316523067200165360ustar00rootroot00000000000000|Build Status| |Coverage Status| |Version| |Downloads| |License| |Supported Python Versions| Welcome to tower-cli ==================== **tower-cli** is a command line tool for Ansible Tower. It allows Tower commands to be easily run from the Unix command line. It can also be used as a client library for other python apps, or as a reference for others developing API interactions with Tower's REST API. For more information, please refer to our official docs hosted at http://tower-cli.readthedocs.io. ansible-tower-cli-3.2.0/bin/000077500000000000000000000000001316523067200156115ustar00rootroot00000000000000ansible-tower-cli-3.2.0/bin/tower-cli000077500000000000000000000016111316523067200174430ustar00rootroot00000000000000#!/usr/bin/env python # Copyright 2013-2014, Ansible, Inc. # Luke Sneeringer # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. import click from tower_cli.cli.base import TowerCLI if __name__ == '__main__': cli = TowerCLI( params=[click.Option(param_decls=['--version'], is_flag=True, help='Display tower-cli version.')], ) cli() ansible-tower-cli-3.2.0/docs/000077500000000000000000000000001316523067200157715ustar00rootroot00000000000000ansible-tower-cli-3.2.0/docs/Makefile000066400000000000000000000011451316523067200174320ustar00rootroot00000000000000# Minimal makefile for Sphinx documentation # # You can set these variables from the command line. SPHINXOPTS = SPHINXBUILD = python -msphinx SPHINXPROJ = tower_cli SOURCEDIR = source BUILDDIR = build # Put it first so that "make" without argument is like "make help". help: @$(SPHINXBUILD) -M help "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O) .PHONY: help Makefile # Catch-all target: route all unknown targets to Sphinx using the new # "make mode" option. $(O) is meant as a shortcut for $(SPHINXOPTS). %: Makefile @$(SPHINXBUILD) -M $@ "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O)ansible-tower-cli-3.2.0/docs/README.md000066400000000000000000000033761316523067200172610ustar00rootroot00000000000000This README documentation walks you through the process of generating a local doc site for Tower CLI. Before the actual generating process, make sure you have cloned Tower CLI from github and checked out the right version tag you want to generate docs from. Also, we use [graphviz](http://www.graphviz.org/) for some graph generations in doc. Make sure to install graphviz. For example, on OS X: ``` $ brew install graphviz ``` It is always suggested you generate docs in a python virtual environment to prevent any dependency conflicts. ``` $ virtualenv docs ``` And ``` source docs/bin/activate ``` to activate the virtual environment. In the newly created empty virtual environment, install [sphinx](http://www.sphinx-doc.org/en/stable/), our doc generating engine. ``` $ sudo pip install sphinx ``` Sphinx walks through an existing python package's source code tree to generate its documentation. so make sure tower CLI is installed also. ``` $ cd $ make install ``` Then, under `docs/` directory, `mkdir build` to create the subdirectory for hosting the local doc site. Also, under `docs/source`, `mkdir _static` and `mkdir _templates` which are necessary placeholders for doc site compilation. In Tower CLI, each resource has a lot of fields that need to be grouped and documented as `.rst`-formatted tables. we provide a script, `docs/source/api_ref/generate_tables.py`, to auto-generate all tables. In order to run the script, `cd docs/source/api_ref/` and ``` $ python generate_tables.py ``` Now that all documentation source scripts are ready, navigate to `docs/` directory and generate the doc site by ``` $ make html ``` Finally, in web browser, navigate to `file:///docs/build/html/index.html` and see the site for yourself. ansible-tower-cli-3.2.0/docs/source/000077500000000000000000000000001316523067200172715ustar00rootroot00000000000000ansible-tower-cli-3.2.0/docs/source/CONTRIBUTING.rst000066400000000000000000000133411316523067200217340ustar00rootroot00000000000000Contributor's Guide =================== All kinds of contributions are more than welcomed. You can help make tower CLI better by reporting bugs, come up with feature ideas or, even further, help maintainers out by making pull requests. Make sure you follow the rules below when contributing and you are ready to roll ;) Bug Reports ----------- Reporting bugs is highly valuable to us. For flexibility, we do not provide issue templates, but describe the issue as specific as possible to make it easier and faster for us to hunt down the issue. - First check existing issues to see if it has already been created, if it has, giving issue description a "thumbs up". - Mark the issue with 'bug' label. - Be sure to mention Tower backend version, Tower CLI version and python interpreter version when the bug occurs. - Copy-paste the detailed usage (code snippet when using as python library and command when using as CLI) and error message if possible. Feature Requests ---------------- We welcome all sorts of feature ideas, but note, it may be scheduled for a future release rather than the next one, please be patient while we process your request. We will ping you on github once the feature is implemented. - Mark the issue with 'enhancement' label. Architecture Overview --------------------- All available Tower CLI resources descent from abstract class ``tower_cli.models.base.BaseResource``, which provides two fundamental methods, ``read`` and ``write``. ``read`` wraps around a GET method to the specified resource, while ``write`` wraps around a POST or PATCH on condition. Most public resource APIs, like ``create`` or ``list``, are essentially using a combination of ``read`` and ``write`` to communicate with Tower REST APIs. .. autoclass:: tower_cli.models.base.BaseResource :members: read, write Here is the detailed class hierarchy from ``tower_cli.models.base.BaseResource`` to all specific Tower resources: .. inheritance-diagram:: tower_cli.models.base tower_cli.models.fields tower_cli.resources.ad_hoc tower_cli.resources.credential_type tower_cli.resources.credential tower_cli.resources.group tower_cli.resources.host tower_cli.resources.instance_group tower_cli.resources.instance tower_cli.resources.inventory_script tower_cli.resources.inventory_source tower_cli.resources.inventory_update tower_cli.resources.inventory tower_cli.resources.job_template tower_cli.resources.job tower_cli.resources.label tower_cli.resources.node tower_cli.resources.notification_template tower_cli.resources.organization tower_cli.resources.project tower_cli.resources.project_update tower_cli.resources.role tower_cli.resources.schedule tower_cli.resources.setting tower_cli.resources.team tower_cli.resources.unified_job tower_cli.resources.user tower_cli.resources.workflow_job tower_cli.resources.workflow :parts: 0 Details of each Tower CLI resource module are available under ``tower_cli/resources/``. Some root-level modules under ``tower_cli/`` folder are of great importance. Specifically, ``api.py`` contains details of the API client Tower CLI used to make HTTP(S) requests using `requests `_, and ``conf.py`` is used to define and initialize singleton setting object ``tower_cli.conf.settings``. On the other hand, ``tower_cli/cli/`` folder contains code that extends tower_cli from a python library into a full- fledged command-line interface. We use `click `_ as the CLI engine. .. inheritance-diagram:: tower_cli.cli.action tower_cli.cli.base tower_cli.cli.resource tower_cli.cli.misc tower_cli.cli.types :parts: 0 Code Contributions ------------------ Setting up development environment and playing around with Tower CLI is quite straight-forward, here is the usual development procedure: 1. Branch out a local issue branch from the correct base branch. 2. Create an empty virtual environment. 3. Code. 4. Run ``make install`` to install development Tower CLI and all its dependency to virtual environment. 5. Manually test on your bug fix/feature and modify until manual tests pass. 6. Run ``sudo pip install tox``. Then at the root directory of Tower CLI repository, run ``sudo tox .`` to run flake8 verify and unit test against all supported python versions. Run and modify until all flake8 checks and unit tests pass. 7. Commit, push to local fork and make pull request targeting the correct base branch. 8. Wait for a maintainer to either approve and merge the pull request, or update pull request according to feedback comment. Some points to keep in mind when developing: - Target the correct branch. Currently we use branch 'v1' to track 3.1.x versions and 'master' to track 3.2.0 and beyond. - Consider all API versions. Currently 3.1.x versions are exclusively used for API v1 and 3.2.0 and beyond are exclusively used for API v2, that means if you fixed a bug for 3.1.8, switch to 3.2.0 and see if the same or similar bug exists and needs similar fix. - Consider python 2/3 compatibility, make good use of ``six`` and ``tower_cli.compat``. - Consider docs update. Whenever a new resource, new resource public method or new resource field is added, inspect the docs folder and make all necessary updates before committing. Whenever ``HISTORY.rst`` at base directory changes replace ``docs/source/HISTORY.rst`` with the latest version. - Adhere to the flake8 specifications when developing, the only exception we allow is the maximum line length, which is 120 characters rather than 79. - Be pythonic by using meaningful names and clear structures. Make code self-explanatory rather than adding excessive comments. - Be test-driven. Although not mandatory, please try keeping test coverage at least the same as before, we appreciate it if you can increase our test coverage in your pull requests. ansible-tower-cli-3.2.0/docs/source/HISTORY.rst000066400000000000000000000225211316523067200211660ustar00rootroot00000000000000Release History =============== 3.2.0 (2017-10-04) ------------------ *General:* - Officially support using tower_cli as a python library. - Major documentation updates. From 3.2.0 docs are hosted on http://tower-cli.readthedocs.io. - Added project_update and inventory_update resources to allow canceling and deleting. *Updates from Tower 3.2:* - Migrated to API V2. All API calls will start with `/api/v2` instead of `/api/v1`. - Made inventory_source an external resource and remove the old relationship as a group. associate. Remove launching inventory updates from group resource. - Added credential_type resource and significantly modified credential resource to reveal user-defined credentials feature of Tower 3.2. - Added job template extra credential (dis)association to reveal extra_credential field of 3.2 job templates. - Removed all source-specific inventory source fields and replaced them with a `credential` field. - Updated inventory resource fields to reveal smart inventory and insights integration features of Tower 3.2. - Added `list_fact` and `insights` commands to host resource to reveal smart inventory and insights integration features of Tower 3.2. - Added `instance` and `instance_group` resources to reveal instance/instance group feature of Tower 3.2. - Enabled (dis)associating instance groups to(from) organization, job_template and inventory resources to reveal instance/instance group feature of Tower 3.2. - Added support for Tower 3.2 SCM inventory sources. - Updated job_template resource fields to reveal changes in Tower 3.2, including `--diff` mode feature. - Updated job resource launch command to reveal changes in Tower 3.2, including `--diff` mode feature. - Updated ad_hoc resource fields to reveal changes in Tower 3.2, including `--diff` mode feature. Specifically, changed name of `--become` of `launch` command into `--become-enabled`. *Deprecated features:* - Removed permission resource. - Disabled launching a job using the jobs endpoint. - Removed scan jobs in favor of new job fact cache. - Removed Rackspace options. - Old association function for project’s organization. *Reflected from 3.1.8:* - Fix bug of incomplete role membership lookup, preventing granting of roles. - Combine click parameters from multiple base classes in metaclass. - Fix unicode bug in human display format. - Add new page_size parameter to list view. - Add scm_update_cache_timeout field to project resource. - Begin process to deprecate python 2.6. 3.1.8 (2017-08-21) ------------------ - Fix bug of incomplete role membership lookup, preventing granting of roles - New method of installing tower-cli-v1, specific to API v1 use - Combine click parameters from multiple base classes in metaclass - Fix unicode bug in human display format - Add new page_size parameter to list view - Add scm_update_cache_timeout field to project resource - Begin process to deprecate python 2.6 3.1.7 (2017-08-07) ------------------ - Follow up 3.1.6 by duplicating exceptions.py to support `import tower_cli.utils.exceptions` syntax. 3.1.6 (2017-07-18) ------------------ - Fix a usage compatibility issue for Ansible Tower modules. 3.1.5 (2017-07-12) ------------------ - Major code base file structure refactor. Now all click-related logics are moved to `tower_cli/cli/` directory, and `exceptions.py` as well as `compat.py` are moved out of utils directory into base directory. - Categorize help text options for resource action commands (like `update`) to increase readability. - Behavior change of workflow schema command. Now schema will both create new nodes and delete existing nodes when needed to make the resulting workflow topology exactly the same as described in schema file. - Add command `job_template callback` to enable conducting provisioning callback via Tower CLI. - Add new format option to just echo id. - Expand some resource fields, including hipchat rooms for notification template and allow_simultaneous for job templates. - Lookup related inventory sources with "starts with" logic if its name is not fully qualified. - Fixed a python 3.5 compatibility issue that causes job monitor traceback. - Minor typo and help text updates. 3.1.4 (2017-06-07) ------------------ - Support resource copy subcommand. - Support auth-token-based authentication for Tower CLI requests. - Support managing workflow roles, labels and notifications via Tower CLI. - Several fixes on RPM spec file. - Name change from 'foreman' to 'satellite6' in credential kind choices. - Fixed a bug where creating job templates with --extra-vars did not work after 3.1.0 upgrade. - Fixed traceback when launching job with --use-job-endpoint. - Enhanced json library usage to prevent traceback when using earlier python 2.6 versions. - Prevent throwing unnecessary warning when reading from global configuration file. 3.1.3 (2017-03-22) ------------------ - Fixed a bug where extra_vars were dropped in some commands. 3.1.2 (2017-03-21) ------------------ - Fixed a bug where global flags are not added to some commands. 3.1.1 (2017-03-13) ------------------ - Fixed a bug which blocks named resources from using runtime configure settings. - Fixed a bug in 3.1.0 which sometimes causes traceback when `pk` value is given. 3.1.0 (2017-03-09) ------------------ - Improved job monitoring functionality to enable standard out streaming, which displays real-time job output on command line. - Added workflow, workflow_job and node endpoints to manipulate workflow graph and manage workflow job resources. Reflecting workflows feature of Tower 3.1. - Added settings command to manage Tower settings via Tower CLI. Reflecting Configure Tower in Tower (CTiT) feature of Tower 3.1. - Included timeout option to certain unified job template resources. Reflecting job timeout feature of Tower 3.1. - Added unicode support to extra_vars and variable types. - Several minor bug fixes to improve user experience. 3.0.3 (2017-02-07) ------------------ - Expose custom inventory script resource to the user - Include tests and docs in the release tarball - Added job template skip_tags prompting support - Added job template callback support 3.0.2 (2016-12-08) ------------------ - Enable configuring tower-cli via environment variables 3.0.1 (2016-09-22) ------------------ - Added custom SSL certificate support 3.0.0 (2016-08-05) ------------------ - Added text indicator for resource change - Allow hosts, inventory, and groups to use variables from the command line and denote a file by starting with "@" - Added resource role for tower3.0 and permission for previous tower versions - Added notification templates - Added labels - Added description display option - Added deprecation warnings - Help text upgrades - Give indication of "changed" apart from color - New credential fields to support openstack-v2, networking and azure - New options for inventory source/group. Add implicit resource inventory script. - credential updates (no longer require user/team) - Added support for system auditors - projects (do not post to organizations/N/projects) - prompt-for JT fields + job launch options (allow blank inventory too) - Update the POST protocol for associate and disassociate actions - New job launch option for backwards compatibility - New tower-cli option to display tower-cli version - Enhanced debug log format (support multi-line debug log) 2.3.2 (2016-07-21) ------------------ - Add RPM specfile and Makefile - Tower compatibility fixes - Allow scan JTs as an option for "job_type" - Add ability to create group as subgroup of another group - Add YAML output format against JSON and humanized output formats - Add SSL corner case error handling and suggestion - Allow resource disassociation with "null" 2.3.1 (2015-12-10) ------------------ - Fixed bug affecting force-on-exists and fail_on_found options - Changed extra_vars behavior to be more compliant by re-parsing vars, even when only one source exists - Fixed group modify bug, avoid sending unwanted fields in modify requests 2.3.0 (2015-10-20) ------------------ - Fixed an issue where the settings file could be world readable - Added the ability to associate a project with an organization - Added setting "verify\_ssl" to disallow insecure connections - Added support for additional cloud credentials - Exposed additional options for a cloud inventory source - Combined " launch-time extra\_vars" with " job\_template extra\_vars" for older Tower versions - Changed the extra\_vars parameters to align with Ansible parameter handling - Added the ability to run ad hoc commands - Included more detail when displaying job information - Added an example bash script to demonstrate tower-cli usage 2.1.1 (2015-01-27) ------------------ - Added tests for Python versions 2.6 through 3.4 - Added shields for github README - Added job\_tags on job launches - Added option for project local path 2.1.0 (2015-01-21) ------------------ - Added the ability to customize the set of fields used as options for a resource - Expanded monitoring capability to include projects and inventory sources - Added support for new job\_template job launch endpoint 2.0.2 (2014-10-02) ------------------ - Added ability to set local scope for config file - Expanded credential resource to allow options for cloud credentials 2.0.1 (2014-07-18) ------------------ - Updated README and error text 2.0.0 (2014-07-15) ------------------ - Pluggable resource architecture built around click ansible-tower-cli-3.2.0/docs/source/api_ref/000077500000000000000000000000001316523067200206765ustar00rootroot00000000000000ansible-tower-cli-3.2.0/docs/source/api_ref/conf.rst000066400000000000000000000136221316523067200223610ustar00rootroot00000000000000.. _api-ref-conf: Configuration ============= In Tower CLI, there are a number of configuration settings available to users. These settings are mostly used to set up connection details to Tower backend, like hostname of Tower backend and user name/password used for authentication; some are also used for other purposes, like toggle on/off colored stdout. Here is a list of all available Tower CLI settings: +------------------+-------------------------------------------------------+------------------------------------------------------------------------------------------------------------------------------------------------------------+ | **Key** | **Value Type / Value Default** | **Description** | +==================+=======================================================+============================================================================================================================================================+ | ``color`` | Boolean/'true' | Whether to use colored output for highlighting or not. | +------------------+-------------------------------------------------------+------------------------------------------------------------------------------------------------------------------------------------------------------------+ |``format`` | String with options ('human', 'json', 'yaml')/'human' | Output format. The "human" format is intended for humans reading output on the CLI; the "json" and "yaml" formats provide more data. [CLI use only] | +------------------+-------------------------------------------------------+------------------------------------------------------------------------------------------------------------------------------------------------------------+ |``host`` | String/'127.0.0.1' | The location of the Ansible Tower host. HTTPS is assumed as the protocol unless "http://" is explicitly provided. | +------------------+-------------------------------------------------------+------------------------------------------------------------------------------------------------------------------------------------------------------------+ |``password`` | String/'' | Password to use to authenticate to Ansible Tower. | +------------------+-------------------------------------------------------+------------------------------------------------------------------------------------------------------------------------------------------------------------+ |``username`` | String/'' | Username to use to authenticate to Ansible Tower. | +------------------+-------------------------------------------------------+------------------------------------------------------------------------------------------------------------------------------------------------------------+ |``verify_ssl`` | Boolean/'true' | Whether to force verified SSL connections. | +------------------+-------------------------------------------------------+------------------------------------------------------------------------------------------------------------------------------------------------------------+ |``verbose`` | Boolean/'false' | Whether to show information about requests being made. | +------------------+-------------------------------------------------------+------------------------------------------------------------------------------------------------------------------------------------------------------------+ |``description_on``| Boolean/'false' | Whether to show description in human-formatted output. [CLI use only] | +------------------+-------------------------------------------------------+------------------------------------------------------------------------------------------------------------------------------------------------------------+ |``certificate`` | String/'' | Path to a custom certificate file that will be used throughout the command. Ignored if `--insecure` flag if set in command or `verify_ssl` is set to false | +------------------+-------------------------------------------------------+------------------------------------------------------------------------------------------------------------------------------------------------------------+ |``use_token`` | Boolean/'false' | Whether to use token-based authentication. | +------------------+-------------------------------------------------------+------------------------------------------------------------------------------------------------------------------------------------------------------------+ **Note:** Some settings are marked as 'CLI use only', this means although users are free to set values to those settings, those settings only affect CLI but not API usage. .. autoclass:: tower_cli.conf.Settings :members: runtime_values ansible-tower-cli-3.2.0/docs/source/api_ref/exceptions.rst000066400000000000000000000003571316523067200236160ustar00rootroot00000000000000Exceptions ========== APIs of ``tower_cli`` raise exceptions defined in ``tower_cli.exceptions`` module. Check raise list of resource public method documentation for possible exceptions. .. automodule:: tower_cli.exceptions :members: ansible-tower-cli-3.2.0/docs/source/api_ref/generate_tables.py000066400000000000000000000070201316523067200243730ustar00rootroot00000000000000import os import click from tower_cli import get_resource from tower_cli.cli import types from collections import OrderedDict try: unicode except NameError: unicode = str basestring = str ATTR_TO_SHOW = [ 'name', 'type', 'help_text', 'read_only', 'unique', 'filterable', 'required', ] CLI_TYPE_MAPPING = { unicode: 'String', str: 'String', types.Related: (lambda x: 'Resource ' + str(getattr(x, 'resource_name', 'unknown'))), click.Choice: (lambda x: 'Choices: ' + ','.join(getattr(x, 'choices', []))) } def convert_type(attr_val): if attr_val in CLI_TYPE_MAPPING: return CLI_TYPE_MAPPING[attr_val] \ if isinstance(CLI_TYPE_MAPPING[attr_val], str) \ else CLI_TYPE_MAPPING[attr_val](attr_val) elif type(attr_val) in CLI_TYPE_MAPPING: return CLI_TYPE_MAPPING[type(attr_val)] \ if isinstance(CLI_TYPE_MAPPING[type(attr_val)], str) \ else CLI_TYPE_MAPPING[type(attr_val)](attr_val) try: return attr_val.__name__ except Exception: return str(attr_val) def convert_to_str(field, attr, attr_val): if attr == 'help_text' and not attr_val: return 'The %s field.' % field.name elif attr == 'type': return convert_type(attr_val) elif not isinstance(attr_val, basestring): if attr_val in (True, False, None): return str(attr_val) else: return 'TODO' return attr_val def get_content(res_name): res = get_resource(res_name) content = OrderedDict() for field in res.fields: for attr in ATTR_TO_SHOW: content.setdefault(attr, []) attr_val = getattr(field, attr, 'N/A') attr_val = convert_to_str(field, attr, attr_val) content[attr].append(attr_val) return content def render_table(content): delimiter = ['+'] titles = ['|'] values = [] for attr_name in content: column_len = max(len(attr_name), max([len(x) for x in content[attr_name]])) + 1 delimiter.append('-' * column_len + '+') titles.append(attr_name + ' ' * (column_len - len(attr_name)) + '|') for i in range(len(content[attr_name])): val = content[attr_name][i] if len(values) <= i: values.append(['|']) values[i].append(val + ' ' * (column_len - len(val)) + '|') delimiter = ''.join(delimiter) titles = ''.join(titles) values = [''.join(x) for x in values] table = [delimiter] table.append(titles) table.append(delimiter.replace('-', '=')) for val in values: table.append(val) table.append(delimiter) return '\n' + '\n'.join(table) + '\n\n' def insert_table(rst_path, table): insert_checkpnt = '.. \n' with open(rst_path) as f: file_content = f.read() start = file_content.find(insert_checkpnt) + len(insert_checkpnt) end = file_content.rfind(insert_checkpnt) if start >= 0 and end >= 0: with open(rst_path, 'w') as f: f.write(file_content[: start] + table + file_content[end:]) def process_resource(res_name, rst_path): content = get_content(res_name) table = render_table(content) insert_table(rst_path, table) def main(): for root, dirs, files in os.walk('resources/'): for file_ in files: if not file_.endswith('.rst'): continue process_resource(file_[: -len('.rst')], os.path.join(root, file_)) if __name__ == '__main__': main() ansible-tower-cli-3.2.0/docs/source/api_ref/index.rst000066400000000000000000000104071316523067200225410ustar00rootroot00000000000000API Reference ============= **NOTE:** This API documentation assumes you are using 3.2.0 and higher versions of ansible-tower-cli. If you are using a lower version than 3.2.0, there is no guarantee API usages in this documentation would work as expected. Introduction ------------ Like Tower UI, Tower CLI is a client talking to multiple REST services of Tower backend, but via Python script or UNIX command line. Thus the usage of Tower CLI's APIs are pretty straight-forward: get a resource corresponding to its counterpart in Tower backend, and call public methods of that resource, which in term requests specific REST endpoint and fetch/render response JSON. Here is a simple example of creating a new organization using Tower CLI in Python: .. code-block:: python from tower_cli import get_resource from tower_cli.exceptions import Found from tower_cli.conf import settings with settings.runtime_values(username='user', password='pass'): try: res = get_resource('organization') new_org = res.create(name='foo', description='bar', fail_on_found=True) except Found: print('This organization already exists.') assert isinstance(new_org, dict) print(new_org['id']) The above example shows the pattern for most Tower CLI API use cases, which is composed of 3 parts: runtime configuration, fetch resource and invoke its public methods, and exception handling. Tower CLI needs a set of configurations to function properly, all configuration settings are stored in singleton object ``tower_cli.conf.settings``, which provides a public context manager ``runtime_values`` to temporary override settings on file with temporary runtime values. see more about Tower CLI configurations in 'Configuration' section. Most of the resources listed at Tower's endpoint `/api/v2/` have client-side proxy classes in Tower CLI. The two main ways of getting resource proxies in Tower CLI are: .. code-block:: python from tower_cli import get_resource res = get_resource('') and .. code-block:: python import tower_cli.resources..Resource as res = () A typical resource in Tower CLI has 2 components: fields and public methods. Resource fields can be seen as wrappers around actual resource fields exposed by Tower REST API. They are generally used by public methods to create and modify resources and filter when searching for specific resources; Public methods are the actual wrappers around querying Tower REST APIs, they can be used both for general CRUD operations against Tower resources, like delete a user, and for specific tasks like launching an ad hoc command, monitoring a job run or constructing a workflow graph from script. In the table of contents below, all available Tower CLI resources are listed, the documentation for each of them all follow the same structure: a 'Description' section which gives an introduction to the resource; a 'Fields Table' section which lists all available fields of that resource; and a 'API Specification' section, which expands the usage detail of every available public method. Note most public methods have a keyword argument ``**kwargs``. This argument basically contains and only contains resource fields, unless specified. Any usage errors or connection exceptions are thrown as subclasses of ``tower_cli.exceptions.TowerCLIError``, see 'Exceptions' section below for details. Table of Contents ----------------- .. toctree:: :maxdepth: 1 :caption: Environment Setup conf.rst exceptions.rst .. toctree:: :maxdepth: 1 :caption: Resource List resources/ad_hoc.rst resources/credential_type.rst resources/credential.rst resources/group.rst resources/host.rst resources/instance_group.rst resources/instance.rst resources/inventory_script.rst resources/inventory_source.rst resources/inventory_update.rst resources/inventory.rst resources/job_template.rst resources/job.rst resources/label.rst resources/node.rst resources/notification_template.rst resources/organization.rst resources/project.rst resources/project_update.rst resources/role.rst resources/schedule.rst resources/setting.rst resources/team.rst resources/user.rst resources/workflow_job.rst resources/workflow.rst ansible-tower-cli-3.2.0/docs/source/api_ref/resources/000077500000000000000000000000001316523067200227105ustar00rootroot00000000000000ansible-tower-cli-3.2.0/docs/source/api_ref/resources/ad_hoc.rst000066400000000000000000000075561316523067200246740ustar00rootroot00000000000000Ad Hoc Commands =============== Description ----------- This resource is used for managing and executing ad hoc commands via Tower. While the rest CRUD operations follow the common usage pattern, an ad hoc command resource cannot be created via the normal way of calling ``create``, but only be created on-the-fly via ``launch``. Fields Table ------------ ..
+----------------+--------------------+---------------------------+----------+-------+-----------+---------+ |name |type |help_text |read_only |unique |filterable |required | +================+====================+===========================+==========+=======+===========+=========+ |job_explanation |String |The job_explanation field. |False |False |True |False | +----------------+--------------------+---------------------------+----------+-------+-----------+---------+ |created |String |The created field. |False |False |True |False | +----------------+--------------------+---------------------------+----------+-------+-----------+---------+ |status |String |The status field. |False |False |True |False | +----------------+--------------------+---------------------------+----------+-------+-----------+---------+ |elapsed |String |The elapsed field. |False |False |True |False | +----------------+--------------------+---------------------------+----------+-------+-----------+---------+ |job_type |Choices: run,check |The job_type field. |False |False |True |True | +----------------+--------------------+---------------------------+----------+-------+-----------+---------+ |inventory |Resource inventory |The inventory field. |False |False |True |True | +----------------+--------------------+---------------------------+----------+-------+-----------+---------+ |limit |String |The limit field. |False |False |True |False | +----------------+--------------------+---------------------------+----------+-------+-----------+---------+ |credential |Resource credential |The credential field. |False |False |True |True | +----------------+--------------------+---------------------------+----------+-------+-----------+---------+ |module_name |String |The module_name field. |False |False |True |False | +----------------+--------------------+---------------------------+----------+-------+-----------+---------+ |module_args |String |The module_args field. |False |False |True |False | +----------------+--------------------+---------------------------+----------+-------+-----------+---------+ |forks |int |The forks field. |False |False |True |False | +----------------+--------------------+---------------------------+----------+-------+-----------+---------+ |verbosity |mapped_choice |The verbosity field. |False |False |True |False | +----------------+--------------------+---------------------------+----------+-------+-----------+---------+ |become_enabled |bool |The become_enabled field. |False |False |True |False | +----------------+--------------------+---------------------------+----------+-------+-----------+---------+ |diff_mode |bool |The diff_mode field. |False |False |True |False | +----------------+--------------------+---------------------------+----------+-------+-----------+---------+ ..
API Specification ----------------- .. autoclass:: tower_cli.resources.ad_hoc.Resource :members: cancel, delete, get, launch, list, monitor, relaunch, status, wait ansible-tower-cli-3.2.0/docs/source/api_ref/resources/credential.rst000066400000000000000000000043341316523067200255600ustar00rootroot00000000000000Credential ========== Description ----------- This resource is used for managing credential resources in Tower. Fields Table ------------ ..
+----------------+-------------------------+---------------------------+----------+-------+-----------+---------+ |name |type |help_text |read_only |unique |filterable |required | +================+=========================+===========================+==========+=======+===========+=========+ |name |String |The name field. |False |True |True |True | +----------------+-------------------------+---------------------------+----------+-------+-----------+---------+ |description |String |The description field. |False |False |True |False | +----------------+-------------------------+---------------------------+----------+-------+-----------+---------+ |user |Resource user |The user field. |False |False |True |False | +----------------+-------------------------+---------------------------+----------+-------+-----------+---------+ |team |Resource team |The team field. |False |False |True |False | +----------------+-------------------------+---------------------------+----------+-------+-----------+---------+ |organization |Resource organization |The organization field. |False |False |True |False | +----------------+-------------------------+---------------------------+----------+-------+-----------+---------+ |credential_type |Resource credential_type |The credential_type field. |False |False |True |True | +----------------+-------------------------+---------------------------+----------+-------+-----------+---------+ |inputs |structured_input |The inputs field. |False |False |True |False | +----------------+-------------------------+---------------------------+----------+-------+-----------+---------+ ..
API Specification ----------------- .. autoclass:: tower_cli.resources.credential.Resource :members: copy, create, delete, get, list, modify ansible-tower-cli-3.2.0/docs/source/api_ref/resources/credential_type.rst000066400000000000000000000105461316523067200266230ustar00rootroot00000000000000Credential Type =============== Description ----------- This resource is used for managing credential type resources in Tower. Fields Table ------------ ..
+-----------------+------------------------------------------+-------------------------------------------------------------------------------------------------------------------------------------------------------------------------+----------+-------+-----------+---------+ |name |type |help_text |read_only |unique |filterable |required | +=================+==========================================+=========================================================================================================================================================================+==========+=======+===========+=========+ |name |String |The name field. |False |True |True |True | +-----------------+------------------------------------------+-------------------------------------------------------------------------------------------------------------------------------------------------------------------------+----------+-------+-----------+---------+ |description |String |The description field. |False |False |True |False | +-----------------+------------------------------------------+-------------------------------------------------------------------------------------------------------------------------------------------------------------------------+----------+-------+-----------+---------+ |kind |Choices: ssh,vault,net,scm,cloud,insights |The type of credential type being added. Valid options are: ssh, vault, net, scm, cloud and insights. Note only cloud and net can be used for creating credential types. |False |False |True |True | +-----------------+------------------------------------------+-------------------------------------------------------------------------------------------------------------------------------------------------------------------------+----------+-------+-----------+---------+ |managed_by_tower |bool |Indicating if the credential type is a tower built-in type. |True |False |True |False | +-----------------+------------------------------------------+-------------------------------------------------------------------------------------------------------------------------------------------------------------------------+----------+-------+-----------+---------+ |inputs |structured_input |The inputs field. |False |False |True |False | +-----------------+------------------------------------------+-------------------------------------------------------------------------------------------------------------------------------------------------------------------------+----------+-------+-----------+---------+ |injectors |structured_input |The injectors field. |False |False |True |False | +-----------------+------------------------------------------+-------------------------------------------------------------------------------------------------------------------------------------------------------------------------+----------+-------+-----------+---------+ ..
API Specification ----------------- .. autoclass:: tower_cli.resources.credential_type.Resource :members: copy, create, delete, get, modify ansible-tower-cli-3.2.0/docs/source/api_ref/resources/group.rst000066400000000000000000000032731316523067200246030ustar00rootroot00000000000000Group ===== Description ----------- This resource is used for managing group resources in Tower. It can also associate/disassociate one group to/from another group. Fields Table ------------ ..
+------------+-------------------+-------------------------------------------+----------+-------+-----------+---------+ |name |type |help_text |read_only |unique |filterable |required | +============+===================+===========================================+==========+=======+===========+=========+ |name |String |The name field. |False |True |True |True | +------------+-------------------+-------------------------------------------+----------+-------+-----------+---------+ |description |String |The description field. |False |False |True |False | +------------+-------------------+-------------------------------------------+----------+-------+-----------+---------+ |inventory |Resource inventory |The inventory field. |False |False |True |True | +------------+-------------------+-------------------------------------------+----------+-------+-----------+---------+ |variables |variables |Group variables, use "@" to get from file. |False |False |True |False | +------------+-------------------+-------------------------------------------+----------+-------+-----------+---------+ ..
API Specification ----------------- .. autoclass:: tower_cli.resources.group.Resource :members: copy, create, delete, get, modify, associate, disassociate, list ansible-tower-cli-3.2.0/docs/source/api_ref/resources/host.rst000066400000000000000000000043761316523067200244310ustar00rootroot00000000000000Host ==== Description ----------- This resource is used for managing host resources in Tower. It can also associate/disassociate a group to/from a host. Fields Table ------------ ..
+-------------------+-------------------+------------------------------------------+----------+-------+-----------+---------+ |name |type |help_text |read_only |unique |filterable |required | +===================+===================+==========================================+==========+=======+===========+=========+ |name |String |The name field. |False |True |True |True | +-------------------+-------------------+------------------------------------------+----------+-------+-----------+---------+ |description |String |The description field. |False |False |True |False | +-------------------+-------------------+------------------------------------------+----------+-------+-----------+---------+ |inventory |Resource inventory |The inventory field. |False |False |True |True | +-------------------+-------------------+------------------------------------------+----------+-------+-----------+---------+ |enabled |bool |The enabled field. |False |False |True |False | +-------------------+-------------------+------------------------------------------+----------+-------+-----------+---------+ |variables |variables |Host variables, use "@" to get from file. |False |False |True |False | +-------------------+-------------------+------------------------------------------+----------+-------+-----------+---------+ |insights_system_id |String |The insights_system_id field. |False |False |True |False | +-------------------+-------------------+------------------------------------------+----------+-------+-----------+---------+ ..
API Specification ----------------- .. autoclass:: tower_cli.resources.host.Resource :members: copy, create, delete, get, list, modify, associate, disassociate, insights, list_facts ansible-tower-cli-3.2.0/docs/source/api_ref/resources/instance.rst000066400000000000000000000032501316523067200252460ustar00rootroot00000000000000Instance ======== Description ----------- This resource is used for managing instance resources in Tower. Note since instances are read-only in Tower, only ``get`` and ``list`` methods are available for this resource. Fields Table ------------ ..
+------------------+-------+-----------------------------+----------+-------+-----------+---------+ |name |type |help_text |read_only |unique |filterable |required | +==================+=======+=============================+==========+=======+===========+=========+ |uuid |String |The uuid field. |False |False |True |False | +------------------+-------+-----------------------------+----------+-------+-----------+---------+ |hostname |String |The hostname field. |False |False |True |False | +------------------+-------+-----------------------------+----------+-------+-----------+---------+ |version |String |The version field. |False |False |True |False | +------------------+-------+-----------------------------+----------+-------+-----------+---------+ |capacity |int |The capacity field. |False |False |True |False | +------------------+-------+-----------------------------+----------+-------+-----------+---------+ |consumed_capacity |int |The consumed_capacity field. |False |False |True |False | +------------------+-------+-----------------------------+----------+-------+-----------+---------+ ..
API Specification ----------------- .. autoclass:: tower_cli.resources.instance.Resource :members: get, list ansible-tower-cli-3.2.0/docs/source/api_ref/resources/instance_group.rst000066400000000000000000000024661316523067200264720ustar00rootroot00000000000000Instance Group ============== Description ----------- This resource is used for managing instance group resources in Tower. Note since instance groups are read-only in Tower, only ``get`` and ``list`` methods are available for this resource. Fields Table ------------ ..
+------------------+-------+-----------------------------+----------+-------+-----------+---------+ |name |type |help_text |read_only |unique |filterable |required | +==================+=======+=============================+==========+=======+===========+=========+ |name |String |The name field. |False |False |True |False | +------------------+-------+-----------------------------+----------+-------+-----------+---------+ |capacity |int |The capacity field. |False |False |True |False | +------------------+-------+-----------------------------+----------+-------+-----------+---------+ |consumed_capacity |int |The consumed_capacity field. |False |False |True |False | +------------------+-------+-----------------------------+----------+-------+-----------+---------+ ..
API Specification ----------------- .. autoclass:: tower_cli.resources.instance_group.Resource :members: get, list ansible-tower-cli-3.2.0/docs/source/api_ref/resources/inventory.rst000066400000000000000000000052771316523067200255120ustar00rootroot00000000000000Inventory ========= Description ----------- This resource is used for managing inventory resources in Tower. Fields Table ------------ ..
+--------------------+----------------------+----------------------------------------------------+----------+-------+-----------+---------+ |name |type |help_text |read_only |unique |filterable |required | +====================+======================+====================================================+==========+=======+===========+=========+ |name |String |The name field. |False |True |True |True | +--------------------+----------------------+----------------------------------------------------+----------+-------+-----------+---------+ |description |String |The description field. |False |False |True |False | +--------------------+----------------------+----------------------------------------------------+----------+-------+-----------+---------+ |organization |Resource organization |The organization field. |False |False |True |True | +--------------------+----------------------+----------------------------------------------------+----------+-------+-----------+---------+ |variables |variables |Inventory variables, use "@" to get from file. |False |False |True |False | +--------------------+----------------------+----------------------------------------------------+----------+-------+-----------+---------+ |kind |Choices: ,smart |The kind field. Cannot be modified after created. |False |False |True |False | +--------------------+----------------------+----------------------------------------------------+----------+-------+-----------+---------+ |host_filter |String |The host_filter field. Only useful when kind=smart. |False |False |True |False | +--------------------+----------------------+----------------------------------------------------+----------+-------+-----------+---------+ |insights_credential |Resource credential |The insights_credential field. |False |False |True |False | +--------------------+----------------------+----------------------------------------------------+----------+-------+-----------+---------+ ..
API Specification ----------------- .. autoclass:: tower_cli.resources.inventory.Resource :members: copy, create, delete, get, list, modify, associate_ig, disassociate_ig, batch_update ansible-tower-cli-3.2.0/docs/source/api_ref/resources/inventory_script.rst000066400000000000000000000042451316523067200270700ustar00rootroot00000000000000Inventory Script ================ Description ----------- This resource is used for managing inventory script resources in Tower. Fields Table ------------ ..
+-------------+----------------------+----------------------------------------------------------------------------------------+----------+-------+-----------+---------+ |name |type |help_text |read_only |unique |filterable |required | +=============+======================+========================================================================================+==========+=======+===========+=========+ |name |String |The name field. |False |True |True |True | +-------------+----------------------+----------------------------------------------------------------------------------------+----------+-------+-----------+---------+ |description |String |The description field. |False |False |True |False | +-------------+----------------------+----------------------------------------------------------------------------------------+----------+-------+-----------+---------+ |script |variables |Script code to fetch inventory, prefix with "@" to use contents of file for this field. |False |False |True |True | +-------------+----------------------+----------------------------------------------------------------------------------------+----------+-------+-----------+---------+ |organization |Resource organization |The organization field. |False |False |True |True | +-------------+----------------------+----------------------------------------------------------------------------------------+----------+-------+-----------+---------+ ..
API Specification ----------------- .. autoclass:: tower_cli.resources.inventory_script.Resource :members: copy, create, delete, get, list, modify ansible-tower-cli-3.2.0/docs/source/api_ref/resources/inventory_source.rst000066400000000000000000000202111316523067200270530ustar00rootroot00000000000000Inventory Source ================ Description ----------- This resource is used for managing and executing inventory sources via Tower. Note inventory updates are triggered via ``update`` method. Fields Table ------------ ..
+-------------------------+----------------------------------------------------------------------------------------+-------------------------------------------+----------+-------+-----------+---------+ |name |type |help_text |read_only |unique |filterable |required | +=========================+========================================================================================+===========================================+==========+=======+===========+=========+ |name |String |The name field. |False |True |True |True | +-------------------------+----------------------------------------------------------------------------------------+-------------------------------------------+----------+-------+-----------+---------+ |description |String |The description field. |False |False |True |False | +-------------------------+----------------------------------------------------------------------------------------+-------------------------------------------+----------+-------+-----------+---------+ |inventory |Resource inventory |The inventory field. |False |False |True |True | +-------------------------+----------------------------------------------------------------------------------------+-------------------------------------------+----------+-------+-----------+---------+ |source |Choices: ,file,scm,ec2,vmware,gce,azure,azure_rm,openstack,satellite6,cloudforms,custom |The type of inventory source in use. |False |False |True |True | +-------------------------+----------------------------------------------------------------------------------------+-------------------------------------------+----------+-------+-----------+---------+ |credential |Resource credential |The credential field. |False |False |True |False | +-------------------------+----------------------------------------------------------------------------------------+-------------------------------------------+----------+-------+-----------+---------+ |source_vars |String |The source_vars field. |False |False |True |False | +-------------------------+----------------------------------------------------------------------------------------+-------------------------------------------+----------+-------+-----------+---------+ |timeout |int |The timeout field (in seconds). |False |False |True |False | +-------------------------+----------------------------------------------------------------------------------------+-------------------------------------------+----------+-------+-----------+---------+ |source_project |Resource project |Use project files as source for inventory. |False |False |True |False | +-------------------------+----------------------------------------------------------------------------------------+-------------------------------------------+----------+-------+-----------+---------+ |source_path |String |File in SCM Project to use as source. |False |False |True |False | +-------------------------+----------------------------------------------------------------------------------------+-------------------------------------------+----------+-------+-----------+---------+ |update_on_project_update |bool |The update_on_project_update field. |False |False |True |False | +-------------------------+----------------------------------------------------------------------------------------+-------------------------------------------+----------+-------+-----------+---------+ |source_regions |String |The source_regions field. |False |False |True |False | +-------------------------+----------------------------------------------------------------------------------------+-------------------------------------------+----------+-------+-----------+---------+ |instance_filters |String |The instance_filters field. |False |False |True |False | +-------------------------+----------------------------------------------------------------------------------------+-------------------------------------------+----------+-------+-----------+---------+ |group_by |String |The group_by field. |False |False |True |False | +-------------------------+----------------------------------------------------------------------------------------+-------------------------------------------+----------+-------+-----------+---------+ |source_script |Resource inventory_script |The source_script field. |False |False |True |False | +-------------------------+----------------------------------------------------------------------------------------+-------------------------------------------+----------+-------+-----------+---------+ |overwrite |bool |The overwrite field. |False |False |True |False | +-------------------------+----------------------------------------------------------------------------------------+-------------------------------------------+----------+-------+-----------+---------+ |overwrite_vars |bool |The overwrite_vars field. |False |False |True |False | +-------------------------+----------------------------------------------------------------------------------------+-------------------------------------------+----------+-------+-----------+---------+ |update_on_launch |bool |The update_on_launch field. |False |False |True |False | +-------------------------+----------------------------------------------------------------------------------------+-------------------------------------------+----------+-------+-----------+---------+ |update_cache_timeout |int |The update_cache_timeout field. |False |False |True |False | +-------------------------+----------------------------------------------------------------------------------------+-------------------------------------------+----------+-------+-----------+---------+ ..
API Specification ----------------- .. autoclass:: tower_cli.resources.inventory_source.Resource :members: copy, create, delete, get, list, modify, monitor, status, update, wait ansible-tower-cli-3.2.0/docs/source/api_ref/resources/inventory_update.rst000066400000000000000000000073041316523067200270450ustar00rootroot00000000000000Inventory Update ================ Description ----------- This resource is used for managing and executing inventory updates via Tower. Fields Table ------------ ..
+-----------------+----------------------------------------------------------------------------------------+----------------------------+----------+-------+-----------+---------+ |name |type |help_text |read_only |unique |filterable |required | +=================+========================================================================================+============================+==========+=======+===========+=========+ |inventory_source |Resource inventory_source |The inventory_source field. |False |False |True |True | +-----------------+----------------------------------------------------------------------------------------+----------------------------+----------+-------+-----------+---------+ |name |String |The name field. |True |False |True |False | +-----------------+----------------------------------------------------------------------------------------+----------------------------+----------+-------+-----------+---------+ |launch_type |Choices: manual,relaunch,relaunch,callback,scheduled,dependency,workflow,sync,scm |The launch_type field. |True |False |True |True | +-----------------+----------------------------------------------------------------------------------------+----------------------------+----------+-------+-----------+---------+ |status |Choices: new,pending,waiting,running,successful,failed,error,canceled |The status field. |True |False |True |True | +-----------------+----------------------------------------------------------------------------------------+----------------------------+----------+-------+-----------+---------+ |job_explanation |String |The job_explanation field. |True |False |True |False | +-----------------+----------------------------------------------------------------------------------------+----------------------------+----------+-------+-----------+---------+ |created |String |The created field. |True |False |True |False | +-----------------+----------------------------------------------------------------------------------------+----------------------------+----------+-------+-----------+---------+ |elapsed |String |The elapsed field. |True |False |True |False | +-----------------+----------------------------------------------------------------------------------------+----------------------------+----------+-------+-----------+---------+ |source |Choices: ,file,scm,ec2,vmware,gce,azure,azure_rm,openstack,satellite6,cloudforms,custom |The source field. |False |False |True |True | +-----------------+----------------------------------------------------------------------------------------+----------------------------+----------+-------+-----------+---------+ ..
API Specification ----------------- .. autoclass:: tower_cli.resources.inventory_update.Resource :members: cancel, delete, get, list, monitor, relaunch, status, wait ansible-tower-cli-3.2.0/docs/source/api_ref/resources/job.rst000066400000000000000000000035531316523067200242220ustar00rootroot00000000000000Job === Description ----------- This resource is used for managing jobs and launching job templates via Tower. Note for historical purposes, launching a job template is linked to job, rather than job template. Fields Table ------------ ..
+----------------+----------------------+---------------------------+----------+-------+-----------+---------+ |name |type |help_text |read_only |unique |filterable |required | +================+======================+===========================+==========+=======+===========+=========+ |job_template |Resource job_template |The job_template field. |False |False |True |False | +----------------+----------------------+---------------------------+----------+-------+-----------+---------+ |job_explanation |String |The job_explanation field. |False |False |True |False | +----------------+----------------------+---------------------------+----------+-------+-----------+---------+ |created |String |The created field. |False |False |True |False | +----------------+----------------------+---------------------------+----------+-------+-----------+---------+ |status |String |The status field. |False |False |True |False | +----------------+----------------------+---------------------------+----------+-------+-----------+---------+ |elapsed |String |The elapsed field. |False |False |True |False | +----------------+----------------------+---------------------------+----------+-------+-----------+---------+ ..
API Specification ----------------- .. autoclass:: tower_cli.resources.job.Resource :members: cancel, delete, get, launch, list, monitor, relaunch, status, stdout, wait ansible-tower-cli-3.2.0/docs/source/api_ref/resources/job_template.rst000066400000000000000000000432421316523067200261140ustar00rootroot00000000000000Job Template ============ Description ----------- This resource is used for managing job template resources in Tower. It is also responsible to associate/disassociate labels and notification templates to/from an existing job template. There is yet another custom command, ``survey``, used for getting survey specification of a job template. Fields Table ------------ ..
+-------------------------+--------------------+----------------------------------------------------------------------------------------------------------------------------------------------------------------+----------+-------+-----------+---------+ |name |type |help_text |read_only |unique |filterable |required | +=========================+====================+================================================================================================================================================================+==========+=======+===========+=========+ |name |String |The name field. |False |True |True |True | +-------------------------+--------------------+----------------------------------------------------------------------------------------------------------------------------------------------------------------+----------+-------+-----------+---------+ |description |String |The description field. |False |False |True |False | +-------------------------+--------------------+----------------------------------------------------------------------------------------------------------------------------------------------------------------+----------+-------+-----------+---------+ |job_type |Choices: run,check |The job_type field. |False |False |True |False | +-------------------------+--------------------+----------------------------------------------------------------------------------------------------------------------------------------------------------------+----------+-------+-----------+---------+ |inventory |Resource inventory |The inventory field. |False |False |True |False | +-------------------------+--------------------+----------------------------------------------------------------------------------------------------------------------------------------------------------------+----------+-------+-----------+---------+ |project |Resource project |The project field. |False |False |True |True | +-------------------------+--------------------+----------------------------------------------------------------------------------------------------------------------------------------------------------------+----------+-------+-----------+---------+ |playbook |String |The playbook field. |False |False |True |True | +-------------------------+--------------------+----------------------------------------------------------------------------------------------------------------------------------------------------------------+----------+-------+-----------+---------+ |credential |Resource credential |The credential field. |False |False |True |False | +-------------------------+--------------------+----------------------------------------------------------------------------------------------------------------------------------------------------------------+----------+-------+-----------+---------+ |vault_credential |Resource credential |The vault_credential field. |False |False |True |False | +-------------------------+--------------------+----------------------------------------------------------------------------------------------------------------------------------------------------------------+----------+-------+-----------+---------+ |forks |int |The forks field. |False |False |True |False | +-------------------------+--------------------+----------------------------------------------------------------------------------------------------------------------------------------------------------------+----------+-------+-----------+---------+ |limit |String |The limit field. |False |False |True |False | +-------------------------+--------------------+----------------------------------------------------------------------------------------------------------------------------------------------------------------+----------+-------+-----------+---------+ |verbosity |mapped_choice |The verbosity field. |False |False |True |False | +-------------------------+--------------------+----------------------------------------------------------------------------------------------------------------------------------------------------------------+----------+-------+-----------+---------+ |extra_vars |variables |Extra variables used by Ansible in YAML or key=value format. Use @ to get YAML from a file. |False |False |True |False | +-------------------------+--------------------+----------------------------------------------------------------------------------------------------------------------------------------------------------------+----------+-------+-----------+---------+ |job_tags |String |The job_tags field. |False |False |True |False | +-------------------------+--------------------+----------------------------------------------------------------------------------------------------------------------------------------------------------------+----------+-------+-----------+---------+ |force_handlers |bool |The force_handlers field. |False |False |True |False | +-------------------------+--------------------+----------------------------------------------------------------------------------------------------------------------------------------------------------------+----------+-------+-----------+---------+ |skip_tags |String |The skip_tags field. |False |False |True |False | +-------------------------+--------------------+----------------------------------------------------------------------------------------------------------------------------------------------------------------+----------+-------+-----------+---------+ |start_at_task |String |The start_at_task field. |False |False |True |False | +-------------------------+--------------------+----------------------------------------------------------------------------------------------------------------------------------------------------------------+----------+-------+-----------+---------+ |timeout |int |The amount of time (in seconds) to run before the task is canceled. |False |False |True |False | +-------------------------+--------------------+----------------------------------------------------------------------------------------------------------------------------------------------------------------+----------+-------+-----------+---------+ |use_fact_cache |bool |If enabled, Tower will act as an Ansible Fact Cache Plugin; persisting facts at the end of a playbook run to the database and caching facts for use by Ansible. |False |False |True |False | +-------------------------+--------------------+----------------------------------------------------------------------------------------------------------------------------------------------------------------+----------+-------+-----------+---------+ |host_config_key |String |Allow Provisioning Callbacks using this host config key |False |False |True |False | +-------------------------+--------------------+----------------------------------------------------------------------------------------------------------------------------------------------------------------+----------+-------+-----------+---------+ |ask_diff_mode_on_launch |bool |Ask diff mode on launch. |False |False |True |False | +-------------------------+--------------------+----------------------------------------------------------------------------------------------------------------------------------------------------------------+----------+-------+-----------+---------+ |ask_variables_on_launch |bool |Prompt user for extra_vars on launch. |False |False |True |False | +-------------------------+--------------------+----------------------------------------------------------------------------------------------------------------------------------------------------------------+----------+-------+-----------+---------+ |ask_limit_on_launch |bool |Prompt user for host limits on launch. |False |False |True |False | +-------------------------+--------------------+----------------------------------------------------------------------------------------------------------------------------------------------------------------+----------+-------+-----------+---------+ |ask_tags_on_launch |bool |Prompt user for job tags on launch. |False |False |True |False | +-------------------------+--------------------+----------------------------------------------------------------------------------------------------------------------------------------------------------------+----------+-------+-----------+---------+ |ask_skip_tags_on_launch |bool |Prompt user for tags to skip on launch. |False |False |True |False | +-------------------------+--------------------+----------------------------------------------------------------------------------------------------------------------------------------------------------------+----------+-------+-----------+---------+ |ask_job_type_on_launch |bool |Prompt user for job type on launch. |False |False |True |False | +-------------------------+--------------------+----------------------------------------------------------------------------------------------------------------------------------------------------------------+----------+-------+-----------+---------+ |ask_verbosity_on_launch |bool |Prompt user for verbosity on launch. |False |False |True |False | +-------------------------+--------------------+----------------------------------------------------------------------------------------------------------------------------------------------------------------+----------+-------+-----------+---------+ |ask_inventory_on_launch |bool |Prompt user for inventory on launch. |False |False |True |False | +-------------------------+--------------------+----------------------------------------------------------------------------------------------------------------------------------------------------------------+----------+-------+-----------+---------+ |ask_credential_on_launch |bool |Prompt user for machine credential on launch. |False |False |True |False | +-------------------------+--------------------+----------------------------------------------------------------------------------------------------------------------------------------------------------------+----------+-------+-----------+---------+ |survey_enabled |bool |Prompt user for job type on launch. |False |False |True |False | +-------------------------+--------------------+----------------------------------------------------------------------------------------------------------------------------------------------------------------+----------+-------+-----------+---------+ |become_enabled |bool |The become_enabled field. |False |False |True |False | +-------------------------+--------------------+----------------------------------------------------------------------------------------------------------------------------------------------------------------+----------+-------+-----------+---------+ |diff_mode |bool |If enabled, textual changes made to any templated files on the host are shown in the standard output. |False |False |True |False | +-------------------------+--------------------+----------------------------------------------------------------------------------------------------------------------------------------------------------------+----------+-------+-----------+---------+ |allow_simultaneous |bool |The allow_simultaneous field. |False |False |True |False | +-------------------------+--------------------+----------------------------------------------------------------------------------------------------------------------------------------------------------------+----------+-------+-----------+---------+ |survey_spec |variables |On write commands, perform extra POST to the survey_spec endpoint. |False |False |True |False | +-------------------------+--------------------+----------------------------------------------------------------------------------------------------------------------------------------------------------------+----------+-------+-----------+---------+ ..
API Specification ----------------- .. autoclass:: tower_cli.resources.job_template.Resource :members: copy, create, delete, get, list, modify, survey, associate_label, disassociate_label, associate_notification_template, disassociate_notification_template, associate_credential, disassociate_credential, associate_ig, disassociate_ig, callback ansible-tower-cli-3.2.0/docs/source/api_ref/resources/label.rst000066400000000000000000000020151316523067200245170ustar00rootroot00000000000000Label ===== Description ----------- This resource is used for managing label resources in Tower. Fields Table ------------ ..
+-------------+----------------------+------------------------+----------+-------+-----------+---------+ |name |type |help_text |read_only |unique |filterable |required | +=============+======================+========================+==========+=======+===========+=========+ |name |String |The name field. |False |True |True |True | +-------------+----------------------+------------------------+----------+-------+-----------+---------+ |organization |Resource organization |The organization field. |False |False |True |True | +-------------+----------------------+------------------------+----------+-------+-----------+---------+ ..
API Specification ----------------- .. autoclass:: tower_cli.resources.label.Resource :members: copy, create, get, list, modify ansible-tower-cli-3.2.0/docs/source/api_ref/resources/node.rst000066400000000000000000000054701316523067200243750ustar00rootroot00000000000000Workflow Node ============= Description ----------- This resource is used for managing workflow job template nodes in Tower. It can also used for building workflow topology by associating/disassociating nodes. Fields Table ------------ ..
+----------------------+--------------------+---------------------------------+----------+-------+-----------+---------+ |name |type |help_text |read_only |unique |filterable |required | +======================+====================+=================================+==========+=======+===========+=========+ |workflow_job_template |Resource workflow |The workflow_job_template field. |False |False |True |True | +----------------------+--------------------+---------------------------------+----------+-------+-----------+---------+ |unified_job_template |String |The unified_job_template field. |False |False |True |False | +----------------------+--------------------+---------------------------------+----------+-------+-----------+---------+ |inventory |Resource inventory |The inventory field. |False |False |True |False | +----------------------+--------------------+---------------------------------+----------+-------+-----------+---------+ |credential |Resource credential |The credential field. |False |False |True |False | +----------------------+--------------------+---------------------------------+----------+-------+-----------+---------+ |job_type |String |The job_type field. |False |False |True |False | +----------------------+--------------------+---------------------------------+----------+-------+-----------+---------+ |job_tags |String |The job_tags field. |False |False |True |False | +----------------------+--------------------+---------------------------------+----------+-------+-----------+---------+ |skip_tags |String |The skip_tags field. |False |False |True |False | +----------------------+--------------------+---------------------------------+----------+-------+-----------+---------+ |limit |String |The limit field. |False |False |True |False | +----------------------+--------------------+---------------------------------+----------+-------+-----------+---------+ ..
API Specification ----------------- .. autoclass:: tower_cli.resources.node.Resource :members: copy, create, delete, get, list, modify, associate_always_node, disassociate_always_node, associate_failure_node, disassociate_failure_node, associate_success_node, disassociate_success_node ansible-tower-cli-3.2.0/docs/source/api_ref/resources/notification_template.rst000066400000000000000000000425231316523067200300310ustar00rootroot00000000000000Notification Template ===================== Description ----------- This resource is used for managing notification templates in Tower. Note most resource fields, like ``username`` and ``host`` are effective based on ``notification_type``. For example, providing ``service_key`` when creating an email notification template is not effective as it will be discarded. Fields Table ------------ ..
+---------------------------+----------------------------------------------------------+-----------------------------------------------------------------------------------------------------------------------------+----------+-------+-----------+---------+ |name |type |help_text |read_only |unique |filterable |required | +===========================+==========================================================+=============================================================================================================================+==========+=======+===========+=========+ |name |String |The name field. |False |True |True |True | +---------------------------+----------------------------------------------------------+-----------------------------------------------------------------------------------------------------------------------------+----------+-------+-----------+---------+ |description |String |The description field. |False |False |True |False | +---------------------------+----------------------------------------------------------+-----------------------------------------------------------------------------------------------------------------------------+----------+-------+-----------+---------+ |organization |Resource organization |The organization field. |False |False |True |False | +---------------------------+----------------------------------------------------------+-----------------------------------------------------------------------------------------------------------------------------+----------+-------+-----------+---------+ |notification_type |Choices: email,slack,twilio,pagerduty,hipchat,webhook,irc |The notification_type field. |False |False |True |True | +---------------------------+----------------------------------------------------------+-----------------------------------------------------------------------------------------------------------------------------+----------+-------+-----------+---------+ |notification_configuration |file |The notification configuration field. Note providing this field would disable all notification-configuration-related fields. |False |False |True |False | +---------------------------+----------------------------------------------------------+-----------------------------------------------------------------------------------------------------------------------------+----------+-------+-----------+---------+ |username |String |[email]The username. |False |False |True |False | +---------------------------+----------------------------------------------------------+-----------------------------------------------------------------------------------------------------------------------------+----------+-------+-----------+---------+ |sender |String |[email]The sender. |False |False |True |False | +---------------------------+----------------------------------------------------------+-----------------------------------------------------------------------------------------------------------------------------+----------+-------+-----------+---------+ |recipients |String |[email]The recipients. |False |False |True |False | +---------------------------+----------------------------------------------------------+-----------------------------------------------------------------------------------------------------------------------------+----------+-------+-----------+---------+ |use_tls |BOOL |[email]The tls trigger. |False |False |True |False | +---------------------------+----------------------------------------------------------+-----------------------------------------------------------------------------------------------------------------------------+----------+-------+-----------+---------+ |host |String |[email]The host. |False |False |True |False | +---------------------------+----------------------------------------------------------+-----------------------------------------------------------------------------------------------------------------------------+----------+-------+-----------+---------+ |use_ssl |BOOL |[email/irc]The ssl trigger. |False |False |True |False | +---------------------------+----------------------------------------------------------+-----------------------------------------------------------------------------------------------------------------------------+----------+-------+-----------+---------+ |password |String |[email/irc]The password. |False |False |True |False | +---------------------------+----------------------------------------------------------+-----------------------------------------------------------------------------------------------------------------------------+----------+-------+-----------+---------+ |port |INT |[email/irc]The email port. |False |False |True |False | +---------------------------+----------------------------------------------------------+-----------------------------------------------------------------------------------------------------------------------------+----------+-------+-----------+---------+ |channels |String |[slack]The channel. |False |False |True |False | +---------------------------+----------------------------------------------------------+-----------------------------------------------------------------------------------------------------------------------------+----------+-------+-----------+---------+ |token |String |[slack/pagerduty/hipchat]The token. |False |False |True |False | +---------------------------+----------------------------------------------------------+-----------------------------------------------------------------------------------------------------------------------------+----------+-------+-----------+---------+ |account_token |String |[twilio]The account token. |False |False |True |False | +---------------------------+----------------------------------------------------------+-----------------------------------------------------------------------------------------------------------------------------+----------+-------+-----------+---------+ |from_number |String |[twilio]The source phone number. |False |False |True |False | +---------------------------+----------------------------------------------------------+-----------------------------------------------------------------------------------------------------------------------------+----------+-------+-----------+---------+ |to_numbers |String |[twilio]The destination SMS numbers. |False |False |True |False | +---------------------------+----------------------------------------------------------+-----------------------------------------------------------------------------------------------------------------------------+----------+-------+-----------+---------+ |account_sid |String |[twilioThe account sid. |False |False |True |False | +---------------------------+----------------------------------------------------------+-----------------------------------------------------------------------------------------------------------------------------+----------+-------+-----------+---------+ |subdomain |String |[pagerduty]The subdomain. |False |False |True |False | +---------------------------+----------------------------------------------------------+-----------------------------------------------------------------------------------------------------------------------------+----------+-------+-----------+---------+ |service_key |String |[pagerduty]The API service/integration key. |False |False |True |False | +---------------------------+----------------------------------------------------------+-----------------------------------------------------------------------------------------------------------------------------+----------+-------+-----------+---------+ |client_name |String |[pagerduty]The client identifier. |False |False |True |False | +---------------------------+----------------------------------------------------------+-----------------------------------------------------------------------------------------------------------------------------+----------+-------+-----------+---------+ |message_from |String |[hipchat]The label to be shown with notification. |False |False |True |False | +---------------------------+----------------------------------------------------------+-----------------------------------------------------------------------------------------------------------------------------+----------+-------+-----------+---------+ |api_url |String |[hipchat]The api url. |False |False |True |False | +---------------------------+----------------------------------------------------------+-----------------------------------------------------------------------------------------------------------------------------+----------+-------+-----------+---------+ |color |Choices: yellow,green,red,purple,gray,random |[hipchat]The notification color. |False |False |True |False | +---------------------------+----------------------------------------------------------+-----------------------------------------------------------------------------------------------------------------------------+----------+-------+-----------+---------+ |rooms |String |[hipchat]Rooms to send notification to. Use multiple flags to send to multiple rooms, ex --rooms=A --rooms=B |False |False |True |False | +---------------------------+----------------------------------------------------------+-----------------------------------------------------------------------------------------------------------------------------+----------+-------+-----------+---------+ |notify |String |[hipchat]The notify channel trigger. |False |False |True |False | +---------------------------+----------------------------------------------------------+-----------------------------------------------------------------------------------------------------------------------------+----------+-------+-----------+---------+ |url |String |[webhook]The target URL. |False |False |True |False | +---------------------------+----------------------------------------------------------+-----------------------------------------------------------------------------------------------------------------------------+----------+-------+-----------+---------+ |headers |file |[webhook]The http headers. |False |False |True |False | +---------------------------+----------------------------------------------------------+-----------------------------------------------------------------------------------------------------------------------------+----------+-------+-----------+---------+ |server |String |[irc]Server address. |False |False |True |False | +---------------------------+----------------------------------------------------------+-----------------------------------------------------------------------------------------------------------------------------+----------+-------+-----------+---------+ |nickname |String |[irc]The irc nick. |False |False |True |False | +---------------------------+----------------------------------------------------------+-----------------------------------------------------------------------------------------------------------------------------+----------+-------+-----------+---------+ |target |String |[irc]The distination channels or users. |False |False |True |False | +---------------------------+----------------------------------------------------------+-----------------------------------------------------------------------------------------------------------------------------+----------+-------+-----------+---------+ ..
API Specification ----------------- .. autoclass:: tower_cli.resources.notification_template.Resource :members: copy, create, delete, get, list, modify ansible-tower-cli-3.2.0/docs/source/api_ref/resources/organization.rst000066400000000000000000000021161316523067200261460ustar00rootroot00000000000000Organization ============ Description ----------- This resource is used for managing organization resources in Tower. It can also perform some associations/disassociations. Fields Table ------------ ..
+------------+-------+-----------------------+----------+-------+-----------+---------+ |name |type |help_text |read_only |unique |filterable |required | +============+=======+=======================+==========+=======+===========+=========+ |name |String |The name field. |False |True |True |True | +------------+-------+-----------------------+----------+-------+-----------+---------+ |description |String |The description field. |False |False |True |False | +------------+-------+-----------------------+----------+-------+-----------+---------+ ..
API Specification ----------------- .. autoclass:: tower_cli.resources.organization.Resource :members: copy, create, delete, get, list, modify, associate, disassociate, associate_admin, disassociate_admin, associate_ig, disassociate_ig ansible-tower-cli-3.2.0/docs/source/api_ref/resources/project.rst000066400000000000000000000112431316523067200251110ustar00rootroot00000000000000Project ======= Description ----------- This resource is used for managing and executing projects via Tower. Note project updates are triggered via ``update`` method. Fields Table ------------ ..
+-------------------------+----------------------+---------------------------------------------------------+----------+-------+-----------+---------+ |name |type |help_text |read_only |unique |filterable |required | +=========================+======================+=========================================================+==========+=======+===========+=========+ |name |String |The name field. |False |True |True |True | +-------------------------+----------------------+---------------------------------------------------------+----------+-------+-----------+---------+ |description |String |The description field. |False |False |True |False | +-------------------------+----------------------+---------------------------------------------------------+----------+-------+-----------+---------+ |organization |Resource organization |The organization field. |False |False |True |False | +-------------------------+----------------------+---------------------------------------------------------+----------+-------+-----------+---------+ |scm_type |mapped_choice |The scm_type field. |False |False |True |True | +-------------------------+----------------------+---------------------------------------------------------+----------+-------+-----------+---------+ |scm_url |String |The scm_url field. |False |False |True |False | +-------------------------+----------------------+---------------------------------------------------------+----------+-------+-----------+---------+ |local_path |String |For manual projects, the server playbook directory name. |False |False |True |False | +-------------------------+----------------------+---------------------------------------------------------+----------+-------+-----------+---------+ |scm_branch |String |The scm_branch field. |False |False |True |False | +-------------------------+----------------------+---------------------------------------------------------+----------+-------+-----------+---------+ |scm_credential |Resource credential |The scm_credential field. |False |False |True |False | +-------------------------+----------------------+---------------------------------------------------------+----------+-------+-----------+---------+ |scm_clean |bool |The scm_clean field. |False |False |True |False | +-------------------------+----------------------+---------------------------------------------------------+----------+-------+-----------+---------+ |scm_delete_on_update |bool |The scm_delete_on_update field. |False |False |True |False | +-------------------------+----------------------+---------------------------------------------------------+----------+-------+-----------+---------+ |scm_update_on_launch |bool |The scm_update_on_launch field. |False |False |True |False | +-------------------------+----------------------+---------------------------------------------------------+----------+-------+-----------+---------+ |scm_update_cache_timeout |int |The scm_update_cache_timeout field. |False |False |True |False | +-------------------------+----------------------+---------------------------------------------------------+----------+-------+-----------+---------+ |job_timeout |int |The timeout field (in seconds). |False |False |True |False | +-------------------------+----------------------+---------------------------------------------------------+----------+-------+-----------+---------+ ..
API Specification ----------------- .. autoclass:: tower_cli.resources.project.Resource :members: copy, create, delete, get, list, modify, monitor, status, stdout, update, wait ansible-tower-cli-3.2.0/docs/source/api_ref/resources/project_update.rst000066400000000000000000000075721316523067200264650ustar00rootroot00000000000000Project Update ============== Description ----------- This resource is used for managing and executing project updates via Tower. Fields Table ------------ ..
+----------------+----------------------------------------------------------------------------------+---------------------------+----------+-------+-----------+---------+ |name |type |help_text |read_only |unique |filterable |required | +================+==================================================================================+===========================+==========+=======+===========+=========+ |project |Resource project |The project field. |False |False |True |True | +----------------+----------------------------------------------------------------------------------+---------------------------+----------+-------+-----------+---------+ |name |String |The name field. |True |False |True |False | +----------------+----------------------------------------------------------------------------------+---------------------------+----------+-------+-----------+---------+ |launch_type |Choices: manual,relaunch,relaunch,callback,scheduled,dependency,workflow,sync,scm |The launch_type field. |True |False |True |True | +----------------+----------------------------------------------------------------------------------+---------------------------+----------+-------+-----------+---------+ |status |Choices: new,pending,waiting,running,successful,failed,error,canceled |The status field. |True |False |True |True | +----------------+----------------------------------------------------------------------------------+---------------------------+----------+-------+-----------+---------+ |job_type |Choices: run,check |The job_type field. |True |False |True |True | +----------------+----------------------------------------------------------------------------------+---------------------------+----------+-------+-----------+---------+ |job_explanation |String |The job_explanation field. |True |False |True |False | +----------------+----------------------------------------------------------------------------------+---------------------------+----------+-------+-----------+---------+ |created |String |The created field. |True |False |True |False | +----------------+----------------------------------------------------------------------------------+---------------------------+----------+-------+-----------+---------+ |elapsed |String |The elapsed field. |True |False |True |False | +----------------+----------------------------------------------------------------------------------+---------------------------+----------+-------+-----------+---------+ |scm_type |mapped_choice |The scm_type field. |False |False |True |True | +----------------+----------------------------------------------------------------------------------+---------------------------+----------+-------+-----------+---------+ ..
API Specification ----------------- .. autoclass:: tower_cli.resources.project_update.Resource :members: cancel, delete, get, list, monitor, relaunch, status, wait ansible-tower-cli-3.2.0/docs/source/api_ref/resources/role.rst000066400000000000000000000122761316523067200244130ustar00rootroot00000000000000RBAC Role ========= Description ----------- This resource is used for managing RBAC roles in Tower. More importantly, it can be used for granting roles to and revoke roles from a user or team. Fields Table ------------ ..
+--------------+------------------------------------------------------------+--------------------------------------------------------------+----------+-------+-----------+---------+ |name |type |help_text |read_only |unique |filterable |required | +==============+============================================================+==============================================================+==========+=======+===========+=========+ |user |Resource user |The user field. |False |False |True |False | +--------------+------------------------------------------------------------+--------------------------------------------------------------+----------+-------+-----------+---------+ |team |Resource team |The team that receives the permissions specified by the role. |False |False |True |False | +--------------+------------------------------------------------------------+--------------------------------------------------------------+----------+-------+-----------+---------+ |type |Choices: admin,read,member,execute,adhoc,update,use,auditor |The type of permission that the role controls. |False |False |True |False | +--------------+------------------------------------------------------------+--------------------------------------------------------------+----------+-------+-----------+---------+ |resource_name |String |The resource_name field. |False |False |True |False | +--------------+------------------------------------------------------------+--------------------------------------------------------------+----------+-------+-----------+---------+ |resource_type |String |The resource_type field. |False |False |True |False | +--------------+------------------------------------------------------------+--------------------------------------------------------------+----------+-------+-----------+---------+ |target_team |Resource team |The team that the role acts on. |False |False |True |False | +--------------+------------------------------------------------------------+--------------------------------------------------------------+----------+-------+-----------+---------+ |inventory |Resource inventory |The inventory field. |False |False |True |False | +--------------+------------------------------------------------------------+--------------------------------------------------------------+----------+-------+-----------+---------+ |job_template |Resource job_template |The job_template field. |False |False |True |False | +--------------+------------------------------------------------------------+--------------------------------------------------------------+----------+-------+-----------+---------+ |credential |Resource credential |The credential field. |False |False |True |False | +--------------+------------------------------------------------------------+--------------------------------------------------------------+----------+-------+-----------+---------+ |organization |Resource organization |The organization field. |False |False |True |False | +--------------+------------------------------------------------------------+--------------------------------------------------------------+----------+-------+-----------+---------+ |project |Resource project |The project field. |False |False |True |False | +--------------+------------------------------------------------------------+--------------------------------------------------------------+----------+-------+-----------+---------+ |workflow |Resource workflow |The workflow field. |False |False |True |False | +--------------+------------------------------------------------------------+--------------------------------------------------------------+----------+-------+-----------+---------+ ..
API Specification ----------------- .. autoclass:: tower_cli.resources.role.Resource :members: copy, get, list, grant, revoke ansible-tower-cli-3.2.0/docs/source/api_ref/resources/schedule.rst000066400000000000000000000100601316523067200252330ustar00rootroot00000000000000Schedule ======== Description ----------- This resource is used for managing schedule resources in Tower. Fields Table ------------ ..
+---------------------+--------------------------+-----------------------------------------------------------------------------------------+----------+-------+-----------+---------+ |name |type |help_text |read_only |unique |filterable |required | +=====================+==========================+=========================================================================================+==========+=======+===========+=========+ |name |String |The name field. |False |True |True |True | +---------------------+--------------------------+-----------------------------------------------------------------------------------------+----------+-------+-----------+---------+ |description |String |The description field. |False |False |True |False | +---------------------+--------------------------+-----------------------------------------------------------------------------------------+----------+-------+-----------+---------+ |job_template |Resource job_template |The job_template field. |False |False |True |False | +---------------------+--------------------------+-----------------------------------------------------------------------------------------+----------+-------+-----------+---------+ |inventory_source |Resource inventory_source |The inventory_source field. |False |False |True |False | +---------------------+--------------------------+-----------------------------------------------------------------------------------------+----------+-------+-----------+---------+ |project |Resource project |The project field. |False |False |True |False | +---------------------+--------------------------+-----------------------------------------------------------------------------------------+----------+-------+-----------+---------+ |unified_job_template |int |Integer used to display unified job template in result, Do not use it for create/modify. |False |False |True |False | +---------------------+--------------------------+-----------------------------------------------------------------------------------------+----------+-------+-----------+---------+ |enabled |BOOL |Whether this schedule will be used. |False |False |True |False | +---------------------+--------------------------+-----------------------------------------------------------------------------------------+----------+-------+-----------+---------+ |rrule |String |Schedule rules specifications which is less than 255 characters. |False |False |True |False | +---------------------+--------------------------+-----------------------------------------------------------------------------------------+----------+-------+-----------+---------+ |extra_data |variables |Extra data for schedule rules in the form of a .json file. |False |False |True |False | +---------------------+--------------------------+-----------------------------------------------------------------------------------------+----------+-------+-----------+---------+ ..
API Specification ----------------- .. autoclass:: tower_cli.resources.schedule.Resource :members: copy, create, delete, get, list, modify ansible-tower-cli-3.2.0/docs/source/api_ref/resources/setting.rst000066400000000000000000000013241316523067200251170ustar00rootroot00000000000000Tower Configuration =================== Description ----------- This resource is used for managing tower configurations in Tower. Fields Table ------------ ..
+------+----------+-----------------+----------+-------+-----------+---------+ |name |type |help_text |read_only |unique |filterable |required | +======+==========+=================+==========+=======+===========+=========+ |value |variables |The value field. |False |False |True |True | +------+----------+-----------------+----------+-------+-----------+---------+ ..
API Specification ----------------- .. autoclass:: tower_cli.resources.setting.Resource :members: copy, get, list, modify ansible-tower-cli-3.2.0/docs/source/api_ref/resources/team.rst000066400000000000000000000024031316523067200243670ustar00rootroot00000000000000Team ==== Description ----------- This resource is used for managing teams and their users in Tower. Fields Table ------------ ..
+-------------+----------------------+------------------------+----------+-------+-----------+---------+ |name |type |help_text |read_only |unique |filterable |required | +=============+======================+========================+==========+=======+===========+=========+ |name |String |The name field. |False |True |True |True | +-------------+----------------------+------------------------+----------+-------+-----------+---------+ |organization |Resource organization |The organization field. |False |False |True |True | +-------------+----------------------+------------------------+----------+-------+-----------+---------+ |description |String |The description field. |False |False |True |False | +-------------+----------------------+------------------------+----------+-------+-----------+---------+ ..
API Specification ----------------- .. autoclass:: tower_cli.resources.team.Resource :members: copy, create, delete, get, list, modify, associate, disassociate ansible-tower-cli-3.2.0/docs/source/api_ref/resources/user.rst000066400000000000000000000037151316523067200244260ustar00rootroot00000000000000User ==== Description ----------- This resource is used for managing users in Tower. Fields Table ------------ ..
+------------------+-------+-----------------------------+----------+-------+-----------+---------+ |name |type |help_text |read_only |unique |filterable |required | +==================+=======+=============================+==========+=======+===========+=========+ |username |String |The username field. |False |True |True |True | +------------------+-------+-----------------------------+----------+-------+-----------+---------+ |password |String |The password field. |False |False |True |False | +------------------+-------+-----------------------------+----------+-------+-----------+---------+ |email |String |The email field. |False |True |True |True | +------------------+-------+-----------------------------+----------+-------+-----------+---------+ |first_name |String |The first_name field. |False |False |True |False | +------------------+-------+-----------------------------+----------+-------+-----------+---------+ |last_name |String |The last_name field. |False |False |True |False | +------------------+-------+-----------------------------+----------+-------+-----------+---------+ |is_superuser |bool |The is_superuser field. |False |False |True |False | +------------------+-------+-----------------------------+----------+-------+-----------+---------+ |is_system_auditor |bool |The is_system_auditor field. |False |False |True |False | +------------------+-------+-----------------------------+----------+-------+-----------+---------+ ..
API Specification ----------------- .. autoclass:: tower_cli.resources.user.Resource :members: copy, create, delete, get, list, modify ansible-tower-cli-3.2.0/docs/source/api_ref/resources/workflow.rst000066400000000000000000000147741316523067200253310ustar00rootroot00000000000000Workflow Job Template ===================== Description ----------- This resource is used for managing workflow job template resources in Tower. It is also responsible for associating/disassociating labels and notification templates to/from an existing job template. There is yet another 2 custom commands, ``survey``, used for getting survey specification of a workflow job template, and ``schema``, used for build workflow topology via YAML/JSON content. Workflow Schema --------------- Workflow ``schema`` is a handy API to bulk-create or bulk-update a workflow node network. The schema is a JSON- or YAML-formatted string defining the hierarchy structure that connects the nodes. Names, as well as other valid parameters for node creation, are acceptable inside of the node's entry inside the schema definition. Links must be declared as a list under a key that starts with "success", "failure", or "always". The following is an example of a valid YAML-formatted schema definition. :: """ - job_template: Hello world failure: - inventory_source: AWS servers (AWS servers - 42) success: - project: Ansible Examples always: - job_template: Echo variable success: - job_template: Scan localhost """ The workflow schema feature populates the workflow node network based on the hierarchy structure. Before creating each node, it attempts to find an existing node with the specified properties in that location in the tree, and will not create a new node if it exists. Also, if an existing node has no correspondence in the schema, the entire sub-tree based on that node will be deleted. Thus, after running the schema command, the resulting workflow node network topology will always be exactly the same as what is specified in the given schema file. To continue with the previous example, subsequent invocations of: :: wfjt.schema('workflow1', '') should not change the network of ``workflow1``, since schema detail is unchanged. However :: wfjt.schema('workflow1', '') will modify node network topology of ``workflow1`` to exactly the same as what is specified in the new schema spec. Fields Table ------------ ..
+-------------------+----------------------+-----------------------------------------------------------------------------------------------------------------------------------------------------------+----------+-------+-----------+---------+ |name |type |help_text |read_only |unique |filterable |required | +===================+======================+===========================================================================================================================================================+==========+=======+===========+=========+ |name |String |The name field. |False |True |True |True | +-------------------+----------------------+-----------------------------------------------------------------------------------------------------------------------------------------------------------+----------+-------+-----------+---------+ |description |String |The description field. |False |False |True |False | +-------------------+----------------------+-----------------------------------------------------------------------------------------------------------------------------------------------------------+----------+-------+-----------+---------+ |extra_vars |variables |Extra variables used by Ansible in YAML or key=value format. Use @ to get YAML from a file. Use the option multiple times to add multiple extra variables. |False |False |True |False | +-------------------+----------------------+-----------------------------------------------------------------------------------------------------------------------------------------------------------+----------+-------+-----------+---------+ |organization |Resource organization |The organization field. |False |False |True |False | +-------------------+----------------------+-----------------------------------------------------------------------------------------------------------------------------------------------------------+----------+-------+-----------+---------+ |survey_enabled |bool |Prompt user for job type on launch. |False |False |True |False | +-------------------+----------------------+-----------------------------------------------------------------------------------------------------------------------------------------------------------+----------+-------+-----------+---------+ |allow_simultaneous |bool |The allow_simultaneous field. |False |False |True |False | +-------------------+----------------------+-----------------------------------------------------------------------------------------------------------------------------------------------------------+----------+-------+-----------+---------+ |survey_spec |variables |On write commands, perform extra POST to the survey_spec endpoint. |False |False |True |False | +-------------------+----------------------+-----------------------------------------------------------------------------------------------------------------------------------------------------------+----------+-------+-----------+---------+ ..
API Specification ----------------- .. autoclass:: tower_cli.resources.workflow.Resource :members: copy, create, delete, get, list, modify, survey, associate_label, associate_notification_template, disassociate_notification_template, associate_label, disassociate_label, schema ansible-tower-cli-3.2.0/docs/source/api_ref/resources/workflow_job.rst000066400000000000000000000032471316523067200261540ustar00rootroot00000000000000Workflow Job ============ Description ----------- This resource is used for managing workflow jobs and launching workflow job templates via Tower. Fields Table ------------ ..
+----------------------+------------------+---------------------------------+----------+-------+-----------+---------+ |name |type |help_text |read_only |unique |filterable |required | +======================+==================+=================================+==========+=======+===========+=========+ |workflow_job_template |Resource workflow |The workflow_job_template field. |False |False |True |True | +----------------------+------------------+---------------------------------+----------+-------+-----------+---------+ |extra_vars |variables |The extra_vars field. |False |False |True |False | +----------------------+------------------+---------------------------------+----------+-------+-----------+---------+ |created |String |The created field. |False |False |True |False | +----------------------+------------------+---------------------------------+----------+-------+-----------+---------+ |status |String |The status field. |False |False |True |False | +----------------------+------------------+---------------------------------+----------+-------+-----------+---------+ ..
API Specification ----------------- .. autoclass:: tower_cli.resources.workflow_job.Resource :members: cancel, delete, get, launch, list, monitor, relaunch, status, wait ansible-tower-cli-3.2.0/docs/source/cli_ref/000077500000000000000000000000001316523067200206745ustar00rootroot00000000000000ansible-tower-cli-3.2.0/docs/source/cli_ref/index.rst000066400000000000000000000206201316523067200225350ustar00rootroot00000000000000CLI Reference ============= CLI invocation generally follows this format: .. code:: bash $ tower-cli {resource} {action} ... The "resource" is a type of object within Tower (a noun), such as ``user``, ``organization``, ``job_template``, etc.; resource names are always singular in Tower CLI (so it is ``tower-cli user``, never ``tower-cli users``). The "action" is the thing you want to do (a verb). Most Tower CLI resources have the following actions--\ ``get``, ``list``, ``create``, ``modify``, and ``delete``--and have options corresponding to fields on the object in Tower. Some examples: .. code:: bash # List all users. $ tower-cli user list # List all non-superusers $ tower-cli user list --is-superuser=false # Get the user with the ID of 42. $ tower-cli user get 42 # Get the user with the given username. $ tower-cli user get --username=guido # Create a new user. $ tower-cli user create --username=guido --first-name=Guido \ --last-name="Van Rossum" --email=guido@python.org \ --password=password1234 # Modify an existing user. # This would modify the first name of the user with the ID of "42" to "Guido". $ tower-cli user modify 42 --first-name=Guido # Modify an existing user, lookup by username. # This would use "username" as the lookup, and modify the first name. # Which fields are used as lookups vary by resource, but are generally # the resource's name. $ tower-cli user modify --username=guido --first-name=Guido # Delete a user. $ tower-cli user delete 42 # Launch a job. $ tower-cli job launch --job-template=144 # Monitor a job. $ tower-cli job monitor 95 When in doubt, help is available! .. code:: bash $ tower-cli --help # help $ tower-cli user --help # resource specific help $ tower-cli user create --help # command specific help In specific, ``tower-cli --help`` lists all available resources in the current version of Tower CLI: .. code:: bash $ tower-cli --help Usage: tower-cli [OPTIONS] COMMAND [ARGS]... Options: --version Display tower-cli version. --help Show this message and exit. Commands: ad_hoc Launch commands based on playbook given at... config Read or write tower-cli configuration. credential Manage credentials within Ansible Tower. credential_type Manage credential types within Ansible Tower. group Manage groups belonging to an inventory. host Manage hosts belonging to a group within an... instance Check instances within Ansible Tower. instance_group Check instance groups within Ansible Tower. inventory Manage inventory within Ansible Tower. inventory_script Manage inventory scripts within Ansible... inventory_source Manage inventory sources within Ansible... job Launch or monitor jobs. job_template Manage job templates. label Manage labels within Ansible Tower. node Manage nodes inside of a workflow job... notification_template Manage notification templates within Ansible... organization Manage organizations within Ansible Tower. project Manage projects within Ansible Tower. role Add and remove users/teams from roles. schedule Manage schedules within Ansible Tower. setting Manage settings within Ansible Tower. team Manage teams within Ansible Tower. user Manage users within Ansible Tower. version Display version information. workflow Manage workflow job templates. workflow_job Launch or monitor workflow jobs. and ``tower-cli {resource} --help`` lists all available actions: .. code:: bash $ tower-cli user --help Usage: tower-cli user [OPTIONS] COMMAND [ARGS]... Manage users within Ansible Tower. Options: --help Show this message and exit. Commands: copy Copy a user. create Create a user. delete Remove the given user. get Return one and exactly one user. list Return a list of users. modify Modify an already existing user. and ``tower-cli {resource} {action} --help`` shows details of the usage of this action: .. code:: bash $ tower-cli user create --help Usage: tower-cli user create [OPTIONS] Create a user. Fields in the resource's --identity tuple are used for a lookup; if a match is found, then no-op (unless --force-on-exists is set) but do not fail (unless --fail-on-found is set). Field Options: --username TEXT [REQUIRED] The username field. --password TEXT The password field. --email TEXT [REQUIRED] The email field. --first-name TEXT The first_name field. --last-name TEXT The last_name field. --is-superuser BOOLEAN The is_superuser field. --is-system-auditor BOOLEAN The is_system_auditor field. Local Options: --fail-on-found If used, return an error if a matching record already exists. [default: False] --force-on-exists If used, if a match is found on unique fields, other fields will be updated to the provided values. If False, a match causes the request to be a no-op. [default: False] Global Options: --use-token Turn on Tower's token-based authentication. Set config use_token to make this permanent. --certificate TEXT Path to a custom certificate file that will be used throughout the command. Overwritten by --insecure flag if set. --insecure Turn off insecure connection warnings. Set config verify_ssl to make this permanent. --description-on Show description in human-formatted output. -v, --verbose Show information about requests being made. -f, --format [human|json|yaml|id] Output format. The "human" format is intended for humans reading output on the CLI; the "json" and "yaml" formats provide more data, and "id" echos the object id only. -p, --tower-password TEXT Password to use to authenticate to Ansible Tower. This will take precedence over a password provided to `tower config`, if any. -u, --tower-username TEXT Username to use to authenticate to Ansible Tower. This will take precedence over a username provided to `tower config`, if any. -h, --tower-host TEXT The location of the Ansible Tower host. HTTPS is assumed as the protocol unless "http://" is explicitly provided. This will take precedence over a host provided to `tower config`, if any. Other Options: --help Show this message and exit. There are generally 3 categories of options for each action to take: field options, local options and global options. Field options can be seen as wrappers around actual resource fields exposed by Tower REST API. They are generally used to create and modify resources and filter when searching for specific resources; local options are action-specific options, they provide fine-grained modification of the behavior of a resource action. for example, ``--fail-on-found`` option of a ``create`` action will fail the command if a matching record already exists in Tower backend; global options are used to set runtime configuration settings, functioning the same way as context manager ``tower_cli.conf.Settings.runtime_values`` in :ref:`api-ref-conf`. ansible-tower-cli-3.2.0/docs/source/conf.py000066400000000000000000000131331316523067200205710ustar00rootroot00000000000000# -*- coding: utf-8 -*- # # tower_cli documentation build configuration file, created by # sphinx-quickstart on Tue Aug 8 11:31:31 2017. # # This file is execfile()d with the current directory set to its # containing dir. # # Note that not all possible configuration values are present in this # autogenerated file. # # All configuration values have a default; values that are commented out # serve to show the default. # If extensions (or modules to document with autodoc) are in another directory, # add these directories to sys.path here. If the directory is relative to the # documentation root, use os.path.abspath to make it absolute, like shown here. # import os # import sys # sys.path.insert(0, os.path.abspath('../../')) # -- General configuration ------------------------------------------------ # If your documentation needs a minimal Sphinx version, state it here. # # needs_sphinx = '1.0' # Add any Sphinx extension module names here, as strings. They can be # extensions coming with Sphinx (named 'sphinx.ext.*') or your custom # ones. extensions = [ 'sphinx.ext.autodoc', 'sphinx.ext.inheritance_diagram', 'sphinx.ext.intersphinx', 'sphinx.ext.todo', 'sphinx.ext.mathjax', 'sphinx.ext.ifconfig', 'sphinx.ext.viewcode', 'sphinx.ext.githubpages' ] # Add any paths that contain templates here, relative to this directory. templates_path = ['_templates'] # The suffix(es) of source filenames. # You can specify multiple suffix as a list of string: # # source_suffix = ['.rst', '.md'] source_suffix = '.rst' # The master toctree document. master_doc = 'index' # General information about the project. project = u'tower-cli' copyright = u'2017, Ansible by Red Hat' author = u'Alan Rominger, Luke Sneeringer, Aaron Tan' # The version info for the project you're documenting, acts as replacement for # |version| and |release|, also used in various other places throughout the # built documents. # # The short X.Y version. version = u'' # The full version, including alpha/beta/rc tags. release = u'' # The language for content autogenerated by Sphinx. Refer to documentation # for a list of supported languages. # # This is also used if you do content translation via gettext catalogs. # Usually you set "language" from the command line for these cases. language = None # List of patterns, relative to source directory, that match files and # directories to ignore when looking for source files. # This patterns also effect to html_static_path and html_extra_path exclude_patterns = [] # The name of the Pygments (syntax highlighting) style to use. pygments_style = 'sphinx' # If true, `todo` and `todoList` produce output, else they produce nothing. todo_include_todos = True # -- Options for HTML output ---------------------------------------------- # The theme to use for HTML and HTML Help pages. See the documentation for # a list of builtin themes. # html_theme = 'classic' # Theme options are theme-specific and customize the look and feel of a theme # further. For a list of options available for each theme, see the # documentation. # # html_theme_options = {} # Add any paths that contain custom static files (such as style sheets) here, # relative to this directory. They are copied after the builtin static files, # so a file named "default.css" will overwrite the builtin "default.css". html_static_path = ['_static'] # Custom sidebar templates, must be a dictionary that maps document names # to template names. # # This is required for the alabaster theme # refs: http://alabaster.readthedocs.io/en/latest/installation.html#sidebars ''' html_sidebars = { '**': [ 'about.html', 'navigation.html', 'relations.html', # needs 'show_related': True theme option to display 'searchbox.html', 'donate.html', ] } ''' # -- Options for HTMLHelp output ------------------------------------------ # Output file base name for HTML help builder. htmlhelp_basename = 'tower_clidoc' # -- Options for LaTeX output --------------------------------------------- latex_elements = { # The paper size ('letterpaper' or 'a4paper'). # # 'papersize': 'letterpaper', # The font size ('10pt', '11pt' or '12pt'). # # 'pointsize': '10pt', # Additional stuff for the LaTeX preamble. # # 'preamble': '', # Latex figure (float) alignment # # 'figure_align': 'htbp', } # Grouping the document tree into LaTeX files. List of tuples # (source start file, target name, title, # author, documentclass [howto, manual, or own class]). latex_documents = [ (master_doc, 'tower_cli.tex', u'tower\\_cli Documentation', u'Alan Rominger, Luke Sneeringer, Aaron Tan', 'manual'), ] # -- Options for manual page output --------------------------------------- # One entry per manual page. List of tuples # (source start file, name, description, authors, manual section). man_pages = [ (master_doc, 'tower_cli', u'tower_cli Documentation', [author], 1) ] # -- Options for Texinfo output ------------------------------------------- # Grouping the document tree into Texinfo files. List of tuples # (source start file, target name, title, author, # dir menu entry, description, category) texinfo_documents = [ (master_doc, 'tower_cli', u'tower_cli Documentation', author, 'tower_cli', 'One line description of project.', 'Miscellaneous'), ] # Example configuration for intersphinx: refer to the Python standard library. intersphinx_mapping = {'https://docs.python.org/': None} def setup(app): from sphinx.ext.autodoc import between app.connect('autodoc-process-docstring', between(r'=====API DOCS=====', keepempty=True, what=["method"])) ansible-tower-cli-3.2.0/docs/source/index.rst000066400000000000000000000023411316523067200211320ustar00rootroot00000000000000Ansible Tower CLI ================= **tower-cli** is a command line tool for Ansible Tower. It allows Tower commands to be easily run from the Unix command line. It can also be used as a client library for other python apps, or as a reference for others developing API interactions with Tower's REST API. About Tower ----------- `Ansible Tower `__ is a GUI and REST interface for Ansible that supercharges it by adding RBAC, centralized logging, autoscaling/provisioning callbacks, graphical inventory editing, and more. Tower is free to use for up to 30 days or 10 nodes. Beyond this, `a license is required `__. Capabilities ------------ This command line tool sends commands to the Tower API. It is capable of retrieving, creating, modifying, and deleting most resources within Tower. A few potential uses include: - Launching playbook runs (for instance, from Jenkins, TeamCity, Bamboo, etc) - Checking on job statuses - Rapidly creating objects like organizations, users, teams, and more Table of Contents ----------------- .. toctree:: :maxdepth: 2 install.rst quickstart.rst api_ref/index.rst cli_ref/index.rst CONTRIBUTING.rst HISTORY.rst ansible-tower-cli-3.2.0/docs/source/install.rst000066400000000000000000000025221316523067200214720ustar00rootroot00000000000000Installation ============ Install from Package Managers ----------------------------- Tower CLI is available as a package on `PyPI `__. The preferred way to install is through pip: .. code:: bash $ pip install ansible-tower-cli Build from Source ----------------- ansible-tower-cli may also be consumed and built directly from source. .. code:: bash $ git clone https://github.com/ansible/tower-cli.git Then, inside ``tower_cli`` directory, run .. code:: bash $ make install and follow the instructions. If you are not familar with ansible-tower-cli's dependency tree, we suggested building source in a fresh `virtual environment `__ to prevent any dependency conflict. Install the Right Version ------------------------- REST API of Ansible Tower is versioned, and each API version is supported by a subset, rather than all, of ansible-tower-cli versions. Make sure you are pairing your Tower backend with a right version of ansible-tower-cli, specifically: - If you are using Tower 3.2.0 and beyond, API v2 is available, you should use ansible-tower-cli 3.2.0 and beyond. - If you are using a Tower version lower than 3.2.0, only API v1 is available, you should use ansible-tower-cli versions lower than 3.2.0. ansible-tower-cli-3.2.0/docs/source/quickstart.rst000066400000000000000000000070531316523067200222220ustar00rootroot00000000000000Quick Start =========== This chapter walks you through the general process of setting up and using Tower CLI. It starts with CLI usage and ends with API usage. For details, please see API and CLI references in subsequent chapters. It is assumed you have a Tower backend available to talk to and Tower CLI installed. Please see 'Installation' chapter for instructions on installing Tower CLI. First of all, make sure you know the name of the Tower backend, like ``tower.example.com``, as well as the username/password set of a user in that Tower backend, like ``user/pass``. These are connection information necessary for Tower CLI to communicate to Tower. With these prerequisites, run .. code:: bash $ tower-cli config host tower.example.com $ tower-cli config username user $ tower-cli config password pass The first Tower CLI command, ``tower-cli config``. writes the connection informations to a configuration file (``~/.tower-cli.cfg`` in this case), and subsequent commands and API calls will read this file, extract connection information and talk to Tower as the specified user. See details of Tower CLI configuration in API reference and ``tower-cli config --help``. Then, use Tower CLI to actually control your Tower backend. The CRUD operations against almost every Tower resource can be done using Tower CLI. Suppose we want to see the available job templates to choose for running: .. code:: bash $ tower-cli job_template list A command-line-formatted table would show up, giving general summary of (maybe part of) the available job templates. Note the actual HTTP(S) response is in JSON format, you can choose to see the JSON response itself instead using ``--format`` flag. .. code:: bash $ tower-cli job_template list --format json Other than normal resource CRUD operations, Tower CLI can be used to launch and monitor executable resources like job templates and projects. Suppose we have had the ID of the job template we want to execute from the previous ``list`` call, we can launch the job template by: .. code:: bash $ tower-cli job launch -J --monitor This command will POST to Tower backend to launch the job template to be executed, and monitor the triggered job run by dumping job stdout in real-time, just as what Tower UI does. The best CLI help you can get is from ``--help`` option. Each Tower CLI command is guaranteed to have a ``--help`` option instructing the command hierarchy and detailed usage like command format the meaning of each available command option. Use ``--help`` whenever you have questions about a Tower CLI command. Under the hood, Tower CLI is composed of an API engine and a wrapper layer around it to make it a CLI. Using API of Tower CLI gives you finer-grained control and makes it easy to integrate Tower CLI into your python scripts. The usage of Tower CLI's API is two-phased: get resource and call its API. First you get the type of resource you want to interact with. .. code:: python import tower_cli res = tower_cli.get_resource('job_template') Due to legacy reasons, we use a non-traditional way of importing resource class, ``tower_cli.get_resource``. Alternatively, you can use the old way by using import alias: .. code:: python import tower_cli.resources.job_template import Resource as JobTemplate res = JobTemplate() Then, interaction with Tower would be as easy as straight-forward resource public method calls, like .. code:: python jt_list = res.list() tower_cli.get_resource('job').launch(job_template=1, monitor=True) More API usage can be found in API reference. ansible-tower-cli-3.2.0/docs_deprecated/000077500000000000000000000000001316523067200201515ustar00rootroot00000000000000ansible-tower-cli-3.2.0/docs_deprecated/CONFIG_CMD_OPTIONS.md000066400000000000000000000101721316523067200233370ustar00rootroot00000000000000## key-value options available for `tower-cli config ` command | *Key* | *Value Type/Default* | *Description* | |------------------|-------------------------------------------------------|------------------------------------------------------------------------------------------------------------------------------------------------------------| | `color` | Boolean/'true' | Whether to use colored output for highlighting or not. | | `format` | String with options ('human', 'json', 'yaml')/'human' | Output format. The "human" format is intended for humans reading output on the CLI; the "json" and "yaml" formats provide more data. | | `host` | String/'127.0.0.1' | The location of the Ansible Tower host. HTTPS is assumed as the protocol unless "http://" is explicitly provided. | | `password` | String/'' | Password to use to authenticate to Ansible Tower. | | `username` | String/'' | Username to use to authenticate to Ansible Tower. | | `verify_ssl` | Boolean/'true' | Whether to force verified SSL connections. | | `verbose` | Boolean/'false' | Whether to show information about requests being made. | | `description_on` | Boolean/'false' | Whether to show description in human-formatted output. | | `certificate` | String/'' | Path to a custom certificate file that will be used throughout the command. Ignored if `--insecure` flag if set in command or `verify_ssl` is set to false | | `use_token` | Boolean/'false' | Whether to use token-based authentication. | ## Environment Variables All of the above options can also be set using environment variables. The default behavior is to allow environment variables to override your tower-cli.cfg settings, but they will not override config values that are passed in on the command line at runtime. Below is a table of the available environment variables. ## Variable Mapping | *Environment Variable* | *Tower Config Key* | |------------------------|--------------------| | `TOWER_COLOR` | `color` | | `TOWER_FORMAT` | `format` | | `TOWER_HOST` | `host` | | `TOWER_PASSWORD` | `password` | | `TOWER_USERNAME` | `username` | | `TOWER_VERIFY_SSL` | `verify_ssl` | | `TOWER_VERBOSE` | `verbose` | | `TOWER_DESCRIPTION_ON` | `description_on` | | `TOWER_CERTIFICATE` | `certificate` | | `TOWER_USE_TOKEN` | `use_token` | ## Notes * Under the hood we use the SSL functionality of requests, however the current requests version has checkings on a deprecated SSL certificate field `commonName` (deprecated by RFC 2818). In order to prevent any related usage issues, please make sure to add `subjectAltName` field to your own certificate in use. We will update help docs as soon as changes are made on the requests side. ansible-tower-cli-3.2.0/docs_deprecated/NOTIFICATION_TEMPLATE_MANAGEMENT.md000066400000000000000000000106551316523067200253570ustar00rootroot00000000000000## Introduction - What Notification Templates Are Starting with Ansible Tower 3.0, users can create and associate notification templates to job templates. Whenever a job template undergoes a successful/unsuccessful job run, all its related notification templates will send out notifications to corresponding notification receiver, indicating the status of job run. ## Managing Notification Templates with tower-cli To see the commands available for notification templates, see `tower-cli notification_template --help`. Within a specific command, get the help text with `tower-cli notification_template --help`. The arguments for all notification template commands follow the same pattern, although not all arguments are mandatory for all commands. The structure follows the following pattern: ``` tower-cli role ``` Notification templates suppport all typical CRUD operations that control other resources through tower-cli: `get`, `list`, `create`, `modify` and `delete`. On the other hand, uses can use new command `tower-cli job_template associate_notification` and `tower-cli job_tempate disassociate_notification` to (dis)associate an existing notification to/from a job template. ### Basic Operations CRUD operations on notification templates are basically the same as that of typicall existing resources, e.g. `inventory`. There are, however, some points due to the nature of notification templates that needs to be careful with: * There are two options in `create`, `--job-template` and `--status`, which controls the create mode: providing these options will create a new notification template and associate it with the specified job template. Notification templates will be created isolatedly if these options are not provided. You can find more information in Tower's official documentation. * When looking at the help text of certain notification template commands, note a large portion of the available options are prefixed by a label like `[slack]`. These are special configuration-related options which are composing elements of a notification template's destination configuration. You can find more information about those options in Tower's official documentations. These options plus `--notification-configuration` option form configuration-related options. * Configuration-related options are *not* functioning options in `get`, `list` and `delete`, meaning they will be ignored under these commands even provided. * The label prefixing configuration-related options indicate the type of notification destination this option is used for. When creating a new notification template with certain destination type (controlled by `--notification-type` option), *all non-default* related configuration-related options must be provided. * When modifying an existing notification template, not every configuration-related option has to be provided(but encryped fields must, even you are not changing it!). But if this modification modifies destination type, *all non-default* related configuration-related options must be provided. * `--notification-configuration` option provides a json file specifying the configuration details. All other configuration-related options will be ignored if `--notification-configuration`` is provided. ### Detailed Example ```bash # Create a notification template in isolation. tower-cli notification_template create --name foo --description bar --notification-type slack --channels a --channels b --token hey --organization Default # Create a notification template under an existing job template. tower-cli notification_template create --name foo --description bar --notification-type slack --channels a --channels b --token hey --job-template 5 --organization Default # Get exactly one notification template. tower-cli notification_template get --name foo # Get a list of notification templates under certain criteria. tower-cli notification_template list --notification-type irc # Modify an existing notification. tower-cli notification_template modify --description foo --token hi 17 # Delete an existing notification template. tower-cli notification_template delete --name foo # Associate a job template with an existing notification template. tower-cli job_template associate_notification_template --job-template 5 --notification-template 3 # Disassociate a job template with an existing notification template. tower-cli job_template disassociate_notification_template --job-template 5 --notification-template 3 ``` ansible-tower-cli-3.2.0/docs_deprecated/ROLE_MANAGEMENT.md000066400000000000000000000074261316523067200227410ustar00rootroot00000000000000## Introduction - What Roles Are Starting with Ansible Tower 3.0, roles are the objects used to manage permissions to various resources within Tower. Each role represents: - A type of permission like "use", "update", or "admin" - A resource that this permission applies to, like an inventory or credential This is "Role Based Access Control" or RBAC. Each role may have several users associated with it, where each of the users gains the specified type of permission. Teams may also be associated with a role, in which case all users who are members of the team receive the specified type of permission. ## Managing Roles with tower-cli To see the commands available for roles, see `tower-cli roles`. Within a specific command, get the help text with `tower-cli roles list --help`. The arguments for all role commands follow the same pattern, although not all arguments are mandatory for all commands. The structure follows the following pattern: ``` tower-cli role --type --user/team --resource ``` Roles do not have the typical CRUD operations that control other resources through tower-cli. Roles can not be deleted or created on their own, because they are tied to the resource that they reference. The next section covers what the possible actions are. ### Basic Operations The primary use case for roles is adding or removing users and teams from roles. In the following example, a user is added to the project "use" role. ``` tower-cli role grant --type use --user test_user --project test_project ``` In the above command "test_user" is the username of a user to receive the new permission, "test_project" is the name of the project they are receiving permission for, and "use" is the type of permission they are receiving. Specifically, this allows test_user to use test_project in a job template. In a similar fashion, to remove the user from that role: ``` tower-cli role revoke --type use --user test_user --project test_project ``` To list the roles on that project: ``` tower-cli role list --project test_project ``` ### Detailed Example The following commands will create an inventory and user and demonstrate the different role commands on them. ```bash # Create the inventory and list its roles tower-cli inventory create --name 'test_inventory' --organization 'Default' tower-cli role list --inventory 'test_inventory' tower-cli role get --type 'use' --inventory 'test_inventory' # Create a user, give access to the inventory and take it away tower-cli user create --username 'test_user' --password 'pa$$' --email 'user@example.com' tower-cli role grant --type 'use' --user 'test_user' --inventory 'test_inventory' tower-cli role list --user 'test_user' --type 'use' tower-cli role revoke --type 'use' --user 'test_user' --inventory 'test_inventory' # Create a team, give access to the inventory and take it away tower-cli team create --name 'test_team' --organization 'Default' tower-cli role grant --type 'use' --team 'test_team' --inventory 'test_inventory' tower-cli role list --team 'test_team' --type 'use' tower-cli role revoke --type 'use' --team 'test_team' --inventory 'test_inventory' ``` ### Organization and Team Roles For assigning users to teams and organizations, include the team or organization flag, and it will be acted on as the resource. Note that teams can be either the resource or the role grantee, depending of whether the `--team` or the `--target-team` flag is used. The following example appoints `test_user` to the member role of a team and of an organization. ```bash tower-cli role grant --user 'test_user' ---target-team 'test_team' --type 'member' tower-cli role grant --organization 'Default' --user 'test_user' --type 'member' ``` These commands are redundant with the tower-cli organization and team `associate` and `disassociate` commands. ansible-tower-cli-3.2.0/docs_deprecated/SURVEYS.md000066400000000000000000000051521316523067200216560ustar00rootroot00000000000000# How to manage surveys of job templates and workflows through tower-cli This feature is added in v3.1.0, and v3.1.3 or higher, at least, is recommended. get help: `tower-cli job_template survey --help` ## View a Survey The name of the job template is known ("survey jt" in this example), and the survey definition is desired. `tower-cli job_template survey --name="survey jt"` Example output, this is the survey spec: ```json { "description": "", "spec": [ { "required": true, "min": null, "default": "v3.1.3", "max": null, "question_description": "enter a version of tower-cli to install", "choices": "v3.0.0\nv3.0.1\nv3.0.2\nv3.1.0\nv3.1.1\nv3.1.2\nv3.1.3", "new_question": true, "variable": "version", "question_name": "Tower-cli version", "type": "multiplechoice" }, { "required": true, "min": null, "default": "click\ncolorama\nrequests\nsix\nPyYAML", "max": null, "question_description": "which package requirements would you like to install/check", "choices": "click\ncolorama\nrequests\nsix\nPyYAML", "new_question": true, "variable": "packages", "question_name": "Package requirements", "type": "multiselect" } ], "name": "" } ``` ## Save a survey Use the job template `modify` command to do this. In order to create a _functional_ survey you must do two things: - Save the survey spec - use the `--survey-spec` option - Enable the survey - use the `--survey-enabled` option Example of enabling the survey on a job template: ``` tower-cli job_template modify --name="hello world infinity" --survey-enabled=true ``` The `--survey-spec` option can get the spec from a file by prepending the `@` character. If this character is not used, it is assumed that you are giving the JSON data in-line. ### Copy a survey to another template The following example saves a survey spec to a file, and then uploads that survey spec to another job template. ```bash # Save the survey spec to file in local directory tower-cli job_template survey --name="survey jt" > s.json # Upload that survey to another job template tower-cli job_template modify --name="another jt" --survey-spec=@s.json --survey-enabled=true ``` The next example using one line to do the same thing on the command line. ``` tower-cli job_template modify --name="another jt" --survey-spec="$(tower-cli job_template survey --name='survey jt')" --survey-enabled=true ``` ## Workflows Workflows can also have surveys and follow the same pattern. Example: `tower-cli workflow survey --name="workflow with survey"` ansible-tower-cli-3.2.0/docs_deprecated/WORKFLOWS.md000066400000000000000000000135171316523067200220770ustar00rootroot00000000000000These docs show how to populate an example workflow in Tower and how to automate the creation or copying of workflows. ## Normal CRUD Workflows and workflow nodes can be managed as normal tower-cli resources. ### Workflow Creation Create an empty workflow: ``` tower-cli workflow create --name="workflow1" --organization="Default" --description="example description" ``` Check the existing workflows with the standard list or get commands. ``` tower-cli workflow list --description-on ``` ### Node Creation Create nodes inside the workflow These all become "root" nodes and will spawn jobs on workflow launch without any dependencies on other nodes. These commands create 2 root nodes. ``` tower-cli node create -W workflow1 --job-template="Hello World" tower-cli node create -W workflow1 --job-template="Hello World" ``` List the nodes inside of the workflow ``` tower-cli node list -W workflow1 ``` ### Node Association From the list command, you can find the ids of nodes you want to link `assocate_success_node` will cause the 2nd node to run on success of the first node. The following command causes node 2 to run on the event of successful completion of node 1. ``` tower-cli node assocate_success_node 1 2 ``` This operation is only possible when node 2 is a root node. See the Tower documentation for the limitations on the types of node connections allowed. Auto-creation of the success node, only knowing the parent node id: ``` tower-cli node assocate_success_node 8 --job-template="Hello world" ``` Corresponding disassociate commands are also available. Disassociating a node from another node will make it a root node. ## Node Network Bulk Management To print out a YAML representation of the nodes of a workflow, you can use the following command. JSON format is also allowed. ``` tower-cli workflow schema workflow1 ``` Here, "workflow1" is the name of the workflow. ### Creating/updating a Schema Definition To bulk-create or buld-update a workflow node network, use the workflow schema command. The schema is JSON or YAML content, and can be passed in the CLI argument, or pointed to a file. The schema is passed as a second positional argument, where the first argument references the workflow. ``` tower-cli workflow schema workflow2 @schema.yml ``` The schema can be provided without using a file: ``` tower-cli workflow schema workflow2 '[{"job_template": "Hello world"}]' ``` The Schema definition defines the hierarchy structure that connects the nodes. Names, as well as other valid parameters for node creation, are acceptable inside of the node's entry inside the schema definition. Links must be declared as a list under a key that starts with "success", "failure", or "always". The following is an example of a valid schema definition. Example `schema.yml` file: ```yaml - job_template: Hello world failure: - inventory_source: AWS servers (AWS servers - 42) success: - project: Ansible Examples always: - job_template: Echo variable success: - job_template: Scan localhost ``` This style may be useful to apply in a script to create a workflow network with a tower-cli command after the constituent resources like the job templates and projects were created by preceding tower-cli commands. ### Differences with Machine Formatted Schemas After writing a workflow schema, you may notice differences in how tower-cli subsequently echos the schema definition back to you. The following is what tower-cli might return after writing the above example. ```yaml - failure_nodes: - inventory_source: 42 job_template: 45 success_nodes: - always_nodes: - job_template: 55 success_nodes: - job_template: 44 project: 40 ``` Note that the root node data starts with "failure_nodes", instead of the name of the job template. This will not impact functionality, and manually changing the order will not impact functionality either. Although this format is harder to read, it does the same thing as the previous schema. The ability to both echo and create schemas can be used to copy the contents of one workflow to another. As an example, consider 2 workflows. The first has a name "workflow1", and has its node network populated. The second is named "workflow2" and is empty. The following commands will copy the structure from the first to the second. ```bash tower-cli workflow schema workflow1 > schema.yml tower-cli workflow schema workflow2 @schema.yml ``` ### Idempotence The workflow schema feature populates the workflow node network based on the hierarchy structure. Before creating each node, it attempts to find an existing node with the specified properties in that location in the tree, and will not create a new node if it exists. Also, if an existing node has no correspondence in the schema, the entire sub-tree based on that node will be deleted. Thus, after running the schema command, the resulting workflow topology will always be exactly the same as what is specified in the given schema file. To continue with the previous example, subsequent invocations of: ```bash tower-cli workflow schema workflow2 @schema.yml tower-cli workflow schema workflow2 @schema.yml ``` should not change the network of workflow2, since `schema.yml` file itself remains unchanged. However ```bash tower-cli workflow schema workflow2 @new_schema.yml ``` will modify topology of workflow2 to exactly the same as what is specified in `new_schema.yml`. ## Launching Workflow Jobs Use the workflow_job resource to launch workflow jobs. This supports the use of extra_vars, which can contain answers to survey questions. The `--monitor` and `--wait` flag are available to poll the server until workflow job reaches a completed status. The `--monitor` option will print rolling updates of the jobs that ran as part of the workflow. Here is an example of using those features: ``` tower-cli workflow_job launch -W "echo Hello World" -e a=1 --monitor ``` ansible-tower-cli-3.2.0/docs_deprecated/examples/000077500000000000000000000000001316523067200217675ustar00rootroot00000000000000ansible-tower-cli-3.2.0/docs_deprecated/examples/README.md000066400000000000000000000040441316523067200232500ustar00rootroot00000000000000## Example Commands Examples here are intended to give concrete examples of how the CLI can be used in an automated way. It can also help with testing or the defining of feature requests. Expect the setup script to take up to 2 minutes to run. Most of this time is waiting for the project source control to sync the examples from github to the tower server. ### Setup You should have a version of tower running and configured in the CLI in order to run any scripts or commands here. With your specific data, that can done by the following commands: ```bash $ tower-cli config host tower.example.com $ tower-cli config username leeroyjenkins $ tower-cli config password myPassw0rd ``` Jobs demonstrated in the script do not connect to another machine, and do not require valid machine credentials, so tower-cli config information should be all the unique information necessary. ### Create Fake Data You may want to reference the [fake data creator](https://github.com/ansible/tower-cli/blob/master/docs/examples/fake_data_creator.sh) for examples on how to create different types of resources. If you want to run the script, which auto-populates your Tower server with a small set of fake data, run the following: ```bash # Populate the server with fake data and run test jobs $ cd docs/examples/ $ source fake_data_creator.sh ``` ### Cleaning Up The teardown script removes all of the objects that the CLI can easily remove. This is not a perfect cleanup, but it performs well enough to get the system ready to run the fake data creator script again. ```bash # Delete the data that was created (with some exceptions) $ source fake_data_teardown.sh ``` ### Warnings It is strongly suggested that you only run these scripts on testing versions of an Ansible Tower host in order to avoid unintended naming conflicts. ### Python Module Use Example This bash script example borrows fake data elements from the [tower populator script](https://github.com/jsmartin/tower_populator). The tower_populator script provides an example of how to use the tower-cli python modules. ansible-tower-cli-3.2.0/docs_deprecated/examples/data/000077500000000000000000000000001316523067200227005ustar00rootroot00000000000000ansible-tower-cli-3.2.0/docs_deprecated/examples/data/schema_a.yml000066400000000000000000000013411316523067200251620ustar00rootroot00000000000000# Branch that has deeply nested content removed going to schema B - job_template: workflow JT failure_nodes: - inventory_source: tower script1 (workflow demo success_nodes: - project: Ansible Examples always_nodes: - job_template: workflow JT success_nodes: - job_template: workflow JT # Branch that is modified going to schema B - job_template: workflow JT2 failure_nodes: - job_template: workflow JT2 - job_template: workflow JT success_nodes: - job_template: workflow JT2 - job_template: workflow JT # Branch that is deleted going to schema B - project: Ansible Examples always_nodes: - inventory_source: tower script1 (workflow demo - inventory_source: tower script2 (workflow demo ansible-tower-cli-3.2.0/docs_deprecated/examples/data/schema_b.yml000066400000000000000000000010241316523067200251610ustar00rootroot00000000000000# Branch that has deeply nested content removed going to schema B - job_template: workflow JT failure_nodes: - inventory_source: tower script1 (workflow demo success_nodes: - project: Ansible Examples always_nodes: - job_template: workflow JT # Branch that is modified going to schema B - job_template: workflow JT2 failure_nodes: - inventory_source: tower script1 (workflow demo - inventory_source: tower script2 (workflow demo success_nodes: - project: Ansible Examples - job_template: workflow JT ansible-tower-cli-3.2.0/docs_deprecated/examples/data/schema_simple.yml000066400000000000000000000003371316523067200262370ustar00rootroot00000000000000- job_template: workflow JT failure: - inventory_source: tower script1 (workflow demo success: - project: Ansible Examples always: - job_template: workflow JT success: - job_template: workflow JTansible-tower-cli-3.2.0/docs_deprecated/examples/data/schema_tiny.yml000066400000000000000000000000331316523067200257220ustar00rootroot00000000000000- job_template: workflow JTansible-tower-cli-3.2.0/docs_deprecated/examples/fake_data_creator.sh000066400000000000000000000314721316523067200257500ustar00rootroot00000000000000# Copyright 2015, Ansible, Inc. # Alan Rominger and others # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. # # This bash script can populate an instance of Ansible Tower with # fake data, using the command line interface (tower-cli) to do so. echo " " echo " == Tower-CLI DATA FAKER == " echo " Setting up fake data for tower-cli testing" echo " " echo "Tower-CLI DATA FAKER: reading config settings" hostval=$(tower-cli config host) USER_OUTPUT=$(tower-cli config username) userval=$(echo $USER_OUTPUT| cut -d' ' -f 2) passwordval=$(tower-cli config password) DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd )" if [[ $userval == "username: " ]] || [[ $passwordval == "password: " ]] then echo "WARNING: Configuration has not been fully set"; echo " You will want to run the $ tower-cli config "; echo " command for host, username, and password "; return; fi echo " current configuration settings:" echo $hostval echo $userval echo "Tower-CLI DATA FAKER: creating orgs and teams" # Data regarding Hyrule Ventures was taken from # https://github.com/jsmartin/tower_populator tower-cli organization create --name="Default" tower-cli organization create --name="Hyrule Ventures" --description="Mining Rupees Daily" tower-cli team create --name="Ops" --organization=Default --description="The Ops Team" tower-cli team create --name="QA" --organization=Default --description="Assures quality of software" tower-cli team create --name="Dev" --organization=Default --description="Develops software" tower-cli organization create --name="Bio Inc" --description="Medical services" tower-cli team create --name="Tech Services" --organization="Bio Inc" --description="Helps customers with problems" tower-cli team create --name="Engineering" --organization="Bio Inc" --description="Does tech things" echo "Tower-CLI DATA FAKER: adding projects (--wait flag waits for SCM update)" # The Hyrulian playbooks configure servers on the planet of Hyrule, # and the project containing these playbooks belongs to their organization tower-cli project create --name="Hyrulian Playbooks" --description="Configures all the servers in Hyrule." --scm-type=git --scm-url="https://github.com/jsmartin/tower-demo-example-simple" --organization="Hyrule Ventures" --wait # Generic examples tower-cli project create --name="Ansible Examples" --description="Some example roles and playbooks" --scm-type=git --scm-url="https://github.com/ansible/ansible-examples" --organization "Default" --wait tower-cli project create --name sample_playbooks --organization "Default" --scm-type git --scm-url https://github.com/AlanCoding/permission-testing-playbooks.git --wait tower-cli project create --name="Inventory file examples" --organization "Default" --scm-type git --scm-url https://github.com/AlanCoding/Ansible-inventory-file-examples.git --wait echo "Tower-CLI DATA FAKER: creating users" # The Hyrule Ventures team tower-cli user create --username="link" --password="password" --email=asdf@asdf.com --first-name=Link --last-name=Smith tower-cli organization associate --organization="Hyrule Ventures" --user=link tower-cli team associate --team=Ops --user=link tower-cli user create --username="gdorf" --password="password" --email=asdf@asdf.com --first-name=Geoff --last-name=Smith tower-cli organization associate --organization="Hyrule Ventures" --user=gdorf tower-cli team associate --team=QA --user=gdorf tower-cli user create --username="zelda" --password="password" --email=asdf@asdf.com --first-name=Zelda --last-name=Smith tower-cli organization associate --organization="Hyrule Ventures" --user=zelda tower-cli team associate --team=Dev --user=zelda # The Bio Inc team tower-cli user create --username="sherlock" --password="password" --email=asdf@asdf.com --first-name=Sherlock --last-name=Holmes tower-cli organization associate --organization="Bio Inc" --user=sherlock tower-cli team associate --team="Tech Services" --user=sherlock tower-cli user create --username="jack" --password="password" --email=asdf@asdf.com --first-name=Jack --last-name=Black tower-cli organization associate --organization="Bio Inc" --user=jack tower-cli team associate --team=Engineering --user=jack # Users not belonging to an organization tower-cli user create --username="rshinra" --password="password" --email=asdf4@asdf.com --first-name=Rufus --last-name=Shinra # Examples of associating a user with different organizations echo " associating a user with an organization" tower-cli organization associate --organization="Bio Inc" --user="rshinra" echo " disassociating a user with an organization" tower-cli organization disassociate --organization="Bio Inc" --user="rshinra" # key taken from http://phpseclib.sourceforge.net/rsa/examples.html machine_cred_inputs="username: root ssh_key_data: | -----BEGIN RSA PRIVATE KEY----- MIICXAIBAAKBgQCqGKukO1De7zhZj6+H0qtjTkVxwTCpvKe4eCZ0FPqri0cb2JZfXJ/DgYSF6vUp wmJG8wVQZKjeGcjDOL5UlsuusFncCzWBQ7RKNUSesmQRMSGkVb1/3j+skZ6UtW+5u09lHNsj6tQ5 1s1SPrCBkedbNf0Tp0GbMJDyR4e9T04ZZwIDAQABAoGAFijko56+qGyN8M0RVyaRAXz++xTqHBLh 3tx4VgMtrQ+WEgCjhoTwo23KMBAuJGSYnRmoBZM3lMfTKevIkAidPExvYCdm5dYq3XToLkkLv5L2 pIIVOFMDG+KESnAFV7l2c+cnzRMW0+b6f8mR1CJzZuxVLL6Q02fvLi55/mbSYxECQQDeAw6fiIQX GukBI4eMZZt4nscy2o12KyYner3VpoeE+Np2q+Z3pvAMd/aNzQ/W9WaI+NRfcxUJrmfPwIGm63il AkEAxCL5HQb2bQr4ByorcMWm/hEP2MZzROV73yF41hPsRC9m66KrheO9HPTJuo3/9s5p+sqGxOlF L0NDt4SkosjgGwJAFklyR1uZ/wPJjj611cdBcztlPdqoxssQGnh85BzCj/u3WqBpE2vjvyyvyI5k X6zk7S0ljKtt2jny2+00VsBerQJBAJGC1Mg5Oydo5NwD6BiROrPxGo2bpTbu/fhrT8ebHkTz2epl U9VQQSQzY1oZMVX8i1m5WUTLPz2yLJIBQVdXqhMCQBGoiuSoSjafUhV7i1cEGpb88h5NBYZzWXGZ 37sJ5QsW+sJyoNde3xH8vdXhzU7eT82D6X/scw9RZz+/6rCJ4p0= -----END RSA PRIVATE KEY-----" echo "Tower-CLI DATA FAKER: creating credentials" # Example credentials for cloud and machine tower-cli credential create --name="SSH example" --user=$userval --inputs="$machine_cred_inputs" --credential-type="Machine" tower-cli credential create --name="blank SSH" --user=$userval --inputs="{}" --credential-type="Machine" tower-cli credential create --name="vault password" --user=$userval --inputs="vault_password: password" --credential-type="Vault" tower-cli credential create --name="AWS creds" --team=Ops --credential-type="Amazon Web Services" --inputs='{"username": "your_username", "password": "password"}' # Two users who can become the other to escalate a task tower-cli credential create --credential-type="Machine" --name=user1 --inputs='{"username": "user1", "password": "pass1", "become_method": "su", "become_username": "user2"}' --user=$userval tower-cli credential create --credential-type="Machine" --name=user2 --inputs='{"username": "user2", "password": "pass1", "become_method": "su", "become_username": "user1"}' --user=$userval echo "Tower-CLI DATA FAKER: creating inventories and groups" # Basic localhost examples tower-cli inventory create --name=localhost --description="local machine" --organization=Default --variables="@$DIR/variables.yml" tower-cli host create --name="127.0.0.1" --description="this is a manually created host" --inventory="localhost" --variables="@$DIR/variables.yml" # Corporate example uses localhost with special vars for testing tower-cli inventory create --name=Production --description="Production Machines" --organization="Hyrule Ventures" --variables="@$DIR/variables.yml" # Example of creating a cloud inventory source, with some configurables tower-cli inventory_source create --name=EC2 --credential="AWS creds" --source=ec2 --description="EC2 hosts" --inventory=Production --overwrite=true --source-regions="us-east-1" --overwrite-vars=false --source-vars="foo: bar" example_script="#!/usr/bin/env python import json inventory = {'_meta': {'hostvars': {'foobar': {}}}, 'ungrouped': {'hosts': ['foobar']}} print json.dumps(inventory)" # Inventory examples with custom scripts stored in Tower tower-cli inventory create --name="Custom script inventory" --description="this is an inventory that contains a custom inventory source" --organization="Bio Inc" --variables="@$DIR/variables.yml" # The script can also be obtained from a file using the "@" character tower-cli inventory_script create --name="foobar inventory script" --script="$example_script" --organization="Bio Inc" tower-cli inventory_source create --name="fetch foobar" --source-script="foobar inventory script" --inventory="Custom script inventory" --source="custom" # This will actually run the script, which fetches the host "foobar" tower-cli inventory_source update "fetch foobar" --monitor # Observe the new host "foobar" tower-cli host list --inventory="Custom script inventory" # Examples of nested groups, associating and managing hosts/groups tower-cli inventory create --name="tower-cli manual examples" --organization="Default" --variables="@$DIR/variables.yml" tower-cli group create --name=web --inventory="tower-cli manual examples" tower-cli host create --name="10.42.0.6" --inventory="tower-cli manual examples" tower-cli host create --name="10.42.0.7" --inventory="tower-cli manual examples" tower-cli host create --name="10.42.0.8" --inventory="tower-cli manual examples" tower-cli host create --name="10.42.0.9" --inventory="tower-cli manual examples" tower-cli host create --name="10.42.0.10" --inventory="tower-cli manual examples" # these include databases and web servers, with hosts in the web server group tower-cli group create --name="databases" --inventory="tower-cli manual examples" # Setting up the web servers, associate hosts with the group tower-cli group create --name="web servers" --inventory="tower-cli manual examples" tower-cli host create --name="server.example1.com" --inventory="tower-cli manual examples" tower-cli host associate --host="server.example1.com" --group="web servers" tower-cli host create --name="server.example2.com" --inventory="tower-cli manual examples" tower-cli host associate --host="server.example2.com" --group="web servers" # Example of inventory contents sourced from a project tower-cli inventory create --name="tower-cli SCM inventory example" --organization="Default" --variables="@$DIR/variables.yml" # Uses an example taken from the official Ansible docs tower-cli inventory_source create --name="project-based source" --inventory="tower-cli SCM inventory example" --source="scm" --source-project="Inventory file examples" --source-path="official/inventory.ini" --overwrite-vars=true tower-cli inventory_source update "project-based source" --monitor # Have a look at the group structure from this import tower-cli group list --inventory="tower-cli SCM inventory example" echo "Tower-CLI DATA FAKER: create job templates" # Hello world example, including different credentials # note that since we have set "connection: local", the credentials do not matter. tower-cli job_template create --name="Hello World Debug" --description="debug statement" --inventory=localhost --credential=user1 --project=sample_playbooks --playbook=debug.yml tower-cli job_template create --name="Hello World" --description="echo statement" --inventory=localhost --credential=user1 --project=sample_playbooks --playbook=helloworld.yml tower-cli job_template create --name="Hello World as user2" --description="echo statement with user2 credentials" --inventory=localhost --credential=user2 --project=sample_playbooks --playbook=helloworld.yml # Example from Hyrule data set tower-cli job_template create --name=Apache --description="Confgure Apache servers" --inventory="tower-cli manual examples" --project="Hyrulian Playbooks" --playbook="site.yml" --credential="SSH example" --job-type=run --verbosity=verbose --forks=5 echo "Tower-CLI DATA FAKER: run a job, check status, cancel, and run with monitoring" # Launch job without monitoring tower-cli job launch --job-template="Hello World Debug" --job-explanation="launched by example script" # Note that these only work because there are no other completed jobs from that template # If that is not true, you need to run "job list" and then cancel with the ID tower-cli job status --job-template="Hello World Debug" --status="running" tower-cli job cancel --job-template="Hello World Debug" --status="running" # Note that delete is different from cancel. # With delete, we remove the record of this job's run. For instance: # tower-cli job delete {pk} # launch a job with monitoring turned on tower-cli job launch --job-template="Hello World Debug" --monitor --job-explanation="launched by example script" echo "Tower-CLI DATA FAKER: displaying jobs that have run via the fake data script" tower-cli job list --job-template="Hello World Debug" ansible-tower-cli-3.2.0/docs_deprecated/examples/inventory_script_example.py000077500000000000000000000005071316523067200275020ustar00rootroot00000000000000#!/usr/bin/env python import json inv = { '_meta': { 'hostvars': {} }, 'hosts': [] } for num in range(0, 3): host = u"host-%0.2d" % num inv['hosts'].append(host) inv['_meta']['hostvars'][host] = dict(ansible_ssh_host='127.0.0.1', ansible_connection='local') print(json.dumps(inv, indent=2)) ansible-tower-cli-3.2.0/docs_deprecated/examples/teardown_script.sh000066400000000000000000000037501316523067200255370ustar00rootroot00000000000000# in the current verion of tower-cli, we can't delete the records of job # runs and ad hoc commands because there is no "name" identifier that # we can use to automatically look them up. echo "Tower-CLI DATA FAKER: deleting job templates" tower-cli job_template delete --name="Hello World" tower-cli job_template delete --name="Hello World Debug" tower-cli job_template delete --name="Hello World as user2" tower-cli job_template delete --name="Apache" echo "Tower-CLI DATA FAKER: deleting inventories" tower-cli inventory delete --name=localhost # For complex inventories, we only bother deleting the inventory resource # and the groups and hosts contained within should go with it tower-cli inventory delete --name=Production tower-cli inventory delete --name=Testing tower-cli inventory delete --name=QA echo "Tower-CLI DATA FAKER: deleting credentials" tower-cli credential delete --name=user1 tower-cli credential delete --name=user2 tower-cli credential delete --name="EC2 SSH" tower-cli credential delete --name="Local SSH" tower-cli credential delete --name="AWS creds" echo "Tower-CLI DATA FAKER: deleting users" tower-cli user delete --username="rshinra" tower-cli user delete --username="link" tower-cli user delete --username="gdorf" tower-cli user delete --username="zelda" tower-cli user delete --username="sherlock" tower-cli user delete --username="jack" echo "Tower-CLI DATA FAKER: deleting the projects" tower-cli project delete --name sample_playbooks tower-cli project delete --name="Hyrulian Playbooks" tower-cli project delete --name="Ansible Examples" echo "Tower-CLI DATA FAKER: deleting orgs and teams" # Teams do not automatically go away when their organization is deleted # so we must delete them all tower-cli team delete --name Ops tower-cli team delete --name QA tower-cli team delete --name Dev tower-cli team delete --name Engineering tower-cli team delete --name "Tech Services" tower-cli organization delete --name="Hyrule Ventures" tower-cli organization delete --name="Bio Inc" ansible-tower-cli-3.2.0/docs_deprecated/examples/variables.yml000066400000000000000000000000621316523067200244600ustar00rootroot00000000000000--- connection: local ansible_ssh_host: 127.0.0.1 ansible-tower-cli-3.2.0/docs_deprecated/examples/workflow_demo.sh000066400000000000000000000057361316523067200252140ustar00rootroot00000000000000# Copyright 2017, Ansible by Red Hat # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. # # This bash script can populate an instance of Ansible Tower with # fake data, using the command line interface (tower-cli) to do so. echo " " echo " == Tower-CLI WORKFLOW DEMO == " echo " " echo "current location: ${BASH_SOURCE%/*}" tower-cli organization create --name="Default" tower-cli project create --name="Ansible Examples" --scm-type=git --scm-url="https://github.com/ansible/ansible-examples" --wait tower-cli project create --name="Ansible Examples2" --scm-type=git --scm-url="https://github.com/ansible/ansible-examples" --wait tower-cli inventory create --name="workflow demo" --organization=Default tower-cli inventory_script create --name="example inv script" --organization="Default" --script="@${BASH_SOURCE%/*}/inventory_script_example.py" # Note that these 2 groups need to have distinct names without one starting with the other tower-cli group create --name="tower script1" --inventory="workflow demo" --source="custom" --source-script="example inv script" tower-cli group create --name="tower script2" --inventory="workflow demo" --source="custom" --source-script="example inv script" tower-cli inventory create --name="localhost" tower-cli host create --inventory="localhost" --name="localhost" --variables='{"connection": "local", "ansible_host": "localhost"}' tower-cli credential create --name="Local SSH" --kind=ssh --organization="Default" --ssh-key-data="${BASH_SOURCE%/*}/insecure_private_key" tower-cli job_template create --name="workflow JT" --inventory="localhost" --machine-credential="Local SSH" --project="Ansible Examples" --playbook="language_features/loop_nested.yml" tower-cli job_template create --name="workflow JT2" --inventory="localhost" --machine-credential="Local SSH" --project="Ansible Examples" --playbook="language_features/loop_nested.yml" # Create the WFJT resource itself tower-cli workflow create --name="workflow demo" --organization="Default" echo '' echo ' -- application of simple schema --' tower-cli workflow schema "workflow demo" "@${BASH_SOURCE%/*}/data/schema_simple.yml" echo '' echo ' -- application of tiny schema --' tower-cli workflow schema "workflow demo" "@${BASH_SOURCE%/*}/data/schema_tiny.yml" echo '' echo ' -- application of schema a --' tower-cli workflow schema "workflow demo" "@${BASH_SOURCE%/*}/data/schema_a.yml" echo '' echo ' -- application of schema a --' tower-cli workflow schema "workflow demo" "@${BASH_SOURCE%/*}/data/schema_b.yml" ansible-tower-cli-3.2.0/hacking/000077500000000000000000000000001316523067200164455ustar00rootroot00000000000000ansible-tower-cli-3.2.0/hacking/README.md000066400000000000000000000012241316523067200177230ustar00rootroot00000000000000'Hacking' directory tools ========================= Env-setup --------- The 'env-setup' script modifies your environment to allow you to run tower-cli from a git checkout using python 2.6+. First, set up your environment to run from the checkout: $ source ./hacking/env-setup You will need some basic prerequisites installed. If you do not already have them and do not wish to install them from your operating system package manager, you can install them from pip $ easy_install pip # if pip is not already available $ pip install -r requirements.txt From there, follow tower-cli instructions on docs.ansible.com as normal. ansible-tower-cli-3.2.0/hacking/env-setup000066400000000000000000000047021316523067200203210ustar00rootroot00000000000000# usage: source hacking/env-setup [-q] # modifies environment for running Ansible Tower CLI from checkout # Default values for shell variables we use PYTHONPATH=${PYTHONPATH-""} PATH=${PATH-""} MANPATH=${MANPATH-""} PYTHON=$(which python 2>/dev/null || which python3 2>/dev/null) PYTHON_BIN=${PYTHON_BIN-$PYTHON} verbosity=${1-info} # Defaults to `info' if unspecified if [ "$verbosity" = -q ]; then verbosity=silent fi # When run using source as directed, $0 gets set to bash, so we must use $BASH_SOURCE if [ -n "$BASH_SOURCE" ] ; then HACKING_DIR=$(dirname "$BASH_SOURCE") elif [ $(basename -- "$0") = "env-setup" ]; then HACKING_DIR=$(dirname "$0") # Works with ksh93 but not pdksh, have to eval to keep ash happy... elif [ -n "$KSH_VERSION" ] && echo $KSH_VERSION | grep -qv '^@(#)PD KSH'; then eval "HACKING_DIR=\$(dirname \"\${.sh.file}\")" else HACKING_DIR="$PWD/hacking" fi # The below is an alternative to readlink -fn which doesn't exist on OS X # Source: http://stackoverflow.com/a/1678636 FULL_PATH=$($PYTHON_BIN -c "import os; print(os.path.realpath('$HACKING_DIR'))") export ANSIBLE_HOME="$(dirname "$FULL_PATH")" PREFIX_PYTHONPATH="$ANSIBLE_HOME/lib" PREFIX_PATH="$ANSIBLE_HOME/bin" PREFIX_MANPATH="$ANSIBLE_HOME/docs/man" expr "$PYTHONPATH" : "${PREFIX_PYTHONPATH}.*" > /dev/null || export PYTHONPATH="$PREFIX_PYTHONPATH:$PYTHONPATH" expr "$PATH" : "${PREFIX_PATH}.*" > /dev/null || export PATH="$PREFIX_PATH:$PATH" expr "$MANPATH" : "${PREFIX_MANPATH}.*" > /dev/null || export MANPATH="$PREFIX_MANPATH:$MANPATH" # # Generate egg_info so that pkg_resources works # # Do the work in a function so we don't repeat ourselves later gen_egg_info() { if [ -e "$PREFIX_PYTHONPATH/ansible_tower_cli.egg-info" ] ; then \rm -rf "$PREFIX_PYTHONPATH/ansible_tower_cli.egg-info" fi $PYTHON_BIN setup.py egg_info } if [ "$ANSIBLE_HOME" != "$PWD" ] ; then current_dir="$PWD" else current_dir="$ANSIBLE_HOME" fi ( cd "$ANSIBLE_HOME" if [ "$verbosity" = silent ] ; then gen_egg_info > /dev/null 2>&1 find . -type f -name "*.pyc" -exec rm -f {} \; > /dev/null 2>&1 else gen_egg_info find . -type f -name "*.pyc" -exec rm -f {} \; fi cd "$current_dir" ) if [ "$verbosity" != silent ] ; then cat <<- EOF Setting up Ansible Tower CLI to run out of checkout... PATH=$PATH PYTHONPATH=$PYTHONPATH MANPATH=$MANPATH Remember, you may wish to specify your host file with -i Done! EOF fi ansible-tower-cli-3.2.0/hacking/env-setup.fish000066400000000000000000000035421316523067200212520ustar00rootroot00000000000000#!/usr/bin/env fish # usage: . ./hacking/env-setup [-q] # modifies environment for running Ansible Tower CLI from checkout set HACKING_DIR (dirname (status -f)) set FULL_PATH (python -c "import os; print(os.path.realpath('$HACKING_DIR'))") set ANSIBLE_HOME (dirname $FULL_PATH) set PREFIX_PYTHONPATH $ANSIBLE_HOME/lib set PREFIX_PATH $ANSIBLE_HOME/bin set PREFIX_MANPATH $ANSIBLE_HOME/docs/man # set quiet flag if set -q argv switch $argv case '-q' '--quiet' set QUIET "true" case '*' end end # Set PYTHONPATH if not set -q PYTHONPATH set -gx PYTHONPATH $PREFIX_PYTHONPATH else switch PYTHONPATH case "$PREFIX_PYTHONPATH*" case "*" if not [ $QUIET ] echo "Appending PYTHONPATH" end set -gx PYTHONPATH "$PREFIX_PYTHONPATH:$PYTHONPATH" end end # Set PATH if not contains $PREFIX_PATH $PATH set -gx PATH $PREFIX_PATH $PATH end # Set MANPATH if not contains $PREFIX_MANPATH $MANPATH if not set -q MANPATH set -gx MANPATH $PREFIX_MANPATH: else set -gx MANPATH $PREFIX_MANPATH $MANPATH end end set -gx ANSIBLE_LIBRARY $ANSIBLE_HOME/library # Generate egg_info so that pkg_resources works pushd $ANSIBLE_HOME if [ $QUIET ] python setup.py -q egg_info else python setup.py egg_info end if test -e $PREFIX_PYTHONPATH/ansible*.egg-info rm -r $PREFIX_PYTHONPATH/ansible*.egg-info end mv ansible*egg-info $PREFIX_PYTHONPATH find . -type f -name "*.pyc" -delete popd if not [ $QUIET ] echo "" echo "Setting up Ansible Tower CLI to run out of checkout..." echo "" echo "PATH=$PATH" echo "PYTHONPATH=$PYTHONPATH" echo "ANSIBLE_LIBRARY=$ANSIBLE_LIBRARY" echo "MANPATH=$MANPATH" echo "" echo "Remember, you may wish to specify your host file with -i" echo "" echo "Done!" echo "" end set -e QUIET ansible-tower-cli-3.2.0/packaging/000077500000000000000000000000001316523067200167655ustar00rootroot00000000000000ansible-tower-cli-3.2.0/packaging/rpm/000077500000000000000000000000001316523067200175635ustar00rootroot00000000000000ansible-tower-cli-3.2.0/packaging/rpm/ansible-tower-cli.spec000066400000000000000000000036361316523067200237670ustar00rootroot00000000000000%{!?python_sitelib: %define python_sitelib %(%{__python} -c "from distutils.sysconfig import get_python_lib; print get_python_lib()")} %global srcname tower-cli Name: ansible-%{srcname} Version: 3.2.0 Release: 2%{?dist} Summary: Commandline interface for Ansible Tower Group: Development/Tools License: ASL 2.0 URL: https://github.com/ansible/tower-cli Source0: https://pypi.python.org/packages/f7/7d/ee885225933bb498c34e2ad704194ed188662489a4ca4936a2d82248b6e8/%{srcname}-%{version}.tar.gz BuildRoot: %{_tmppath}/%{name}-%{version}-%{release}-root-%(%{__id_u} -n) BuildRequires: python-devel BuildRequires: python-setuptools BuildArch: noarch Requires: python-six >= 1.7.2 Requires: PyYAML >= 3.10 Requires: python-requests >= 2.3.0 Requires: python-click >= 2.1 %if 0%{?rhel} == 6 Requires: python-importlib >= 1.0.2 Requires: python-ordereddict >= 1.1 Requires: python-simplejson >= 3.5.3 %endif %description ansible-tower-cli is a command line tool for Ansible Tower. It allows Tower commands to be easily run from the Unix command line. It can also be used as a client library for other python apps, or as a reference for others developing API interactions with Tower's REST API. %prep %setup -q -n %{srcname}-%{version} %build %{__python} setup.py build %install rm -rf $RPM_BUILD_ROOT %{__python} setup.py install -O1 --skip-build --root $RPM_BUILD_ROOT mkdir -p $RPM_BUILD_ROOT/etc/tower touch $RPM_BUILD_ROOT/etc/tower/tower_cli.cfg %clean rm -rf $RPM_BUILD_ROOT %files %defattr(-,root,root,-) %doc LICENSE README.rst HISTORY.rst %config(noreplace) %ghost %{_sysconfdir}/tower/tower_cli.cfg %{python_sitelib}/ansible* %{python_sitelib}/tower_cli* %{_bindir}/* %changelog * Sun Mar 26 2017 Satoru SATOH - 3.1.3-2 - Fix the source archive name * Fri Jun 17 2016 Bill Nottingham - 2.3.1-1 - initial spec file ansible-tower-cli-3.2.0/requirements.txt000066400000000000000000000002251316523067200203240ustar00rootroot00000000000000click>=2.1 colorama>=0.3.1 requests>=2.3.0 six>=1.7.2 PyYAML>=3.10 # === Python < 2.7 === # importlib>=1.0.2 # ordereddict>=1.1 # simplejson>=3.5.3 ansible-tower-cli-3.2.0/setup.cfg000066400000000000000000000002621316523067200166620ustar00rootroot00000000000000[nosetests] # don't wrap stdout/stderr nocapture=1 # coverage with-coverage=1 cover-branches=1 cover-erase=1 cover-package=tower_cli cover-html=1 cover-html-dir=.coverage-html/ ansible-tower-cli-3.2.0/setup.py000066400000000000000000000127621316523067200165630ustar00rootroot00000000000000#!/usr/bin/env python # Copyright 2013-2015, Ansible, Inc. # Michael DeHaan # Luke Sneeringer # and others # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. import re import sys from distutils.core import setup from setuptools import find_packages from setuptools.command.test import test as TestCommand pkg_name = 'tower_cli' class Tox(TestCommand): """The test command should install and then run tox. Based on http://tox.readthedocs.org/en/latest/example/basic.html """ user_options = [('tox-args=', 'a', "Arguments to pass to tox")] def initialize_options(self): TestCommand.initialize_options(self) self.tox_args = "" def finalize_options(self): TestCommand.finalize_options(self) self.test_args = [] self.test_suite = True def run_tests(self): import tox # Import here, because outside eggs aren't loaded. import shlex sys.exit(tox.cmdline(args=shlex.split(self.tox_args))) def parse_requirements(filename): """Parse out a list of requirements from the given requirements requirements file. """ reqs = [] version_spec_in_play = None # Iterate over each line in the requirements file. for line in open(filename, 'r').read().strip().split('\n'): # Sanity check: Is this an empty line? # If so, do nothing. if not line.strip(): continue # If this is just a plain requirement (not a comment), then # add it to the requirements list. if not line.startswith('#'): reqs.append(line) continue # "Header" comments take the form of "=== Python {op} {version} ===", # and make the requirement only matter for those versions. # If this line is a header comment, parse it. match = re.search(r'^# === [Pp]ython (?P[<>=]{1,2}) ' r'(?P[\d])\.(?P[\d]+) ===[\s]*$', line) if match: version_spec_in_play = match.groupdict() for key in ('major', 'minor'): version_spec_in_play[key] = int(version_spec_in_play[key]) continue # If this is a comment that otherwise looks like a package, then it # should be a package applying only to the current version spec. # # We can identify something that looks like a package by a lack # of any spaces. if ' ' not in line[1:].strip() and version_spec_in_play: package = line[1:].strip() # Sanity check: Is our version of Python one of the ones currently # in play? op = version_spec_in_play['op'] vspec = (version_spec_in_play['major'], version_spec_in_play['minor']) if '=' in op and sys.version_info[0:2] == vspec: reqs.append(package) elif '>' in op and sys.version_info[0:2] > vspec: reqs.append(package) elif '<' in op and sys.version_info[0:2] < vspec: reqs.append(package) # Okay, we should have an entire list of requirements now. return reqs def combine_files(*args): """returns a string of all the strings in *args combined together, with two line breaks between them""" file_contents = [] for filename in args: with open(filename, 'r') as f: file_contents.append(f.read()) return "\n\n".join(file_contents) setup( # Basic metadata name='ansible-%s' % pkg_name.replace('_', '-'), version=open('tower_cli/VERSION').read().strip(), author='Luke Sneeringer', author_email='lsneeringer@ansible.com', url='https://github.com/ansible/tower-cli', # Additional information description='A CLI tool for Ansible Tower.', long_description=combine_files('README.rst', 'HISTORY.rst'), license='Apache 2.0', # How to do the install install_requires=parse_requirements('requirements.txt'), provides=[ pkg_name, ], scripts=[ 'bin/%s' % pkg_name.replace('_', '-'), ], packages=find_packages(exclude=['tests']), # How to do the tests tests_require=['tox'], cmdclass={'test': Tox}, # Data files package_data={ pkg_name: ['VERSION'], }, # PyPI metadata. classifiers=[ 'Development Status :: 5 - Production/Stable', 'Environment :: Console', 'Intended Audience :: Developers', 'Intended Audience :: System Administrators', 'License :: OSI Approved :: Apache Software License', 'Operating System :: MacOS :: MacOS X', 'Operating System :: POSIX :: Linux', 'Programming Language :: Python', 'Programming Language :: Python :: 2.6', 'Programming Language :: Python :: 2.7', 'Programming Language :: Python :: 3.3', 'Programming Language :: Python :: 3.4', 'Programming Language :: Python :: 3.5', 'Topic :: System :: Software Distribution', 'Topic :: System :: Systems Administration', ], ) ansible-tower-cli-3.2.0/tests/000077500000000000000000000000001316523067200162035ustar00rootroot00000000000000ansible-tower-cli-3.2.0/tests/__init__.py000066400000000000000000000000001316523067200203020ustar00rootroot00000000000000ansible-tower-cli-3.2.0/tests/compat.py000066400000000000000000000023501316523067200200400ustar00rootroot00000000000000# Copyright 2015, Ansible, Inc. # Luke Sneeringer # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. # Import mock from the unittest module in the stdlib if it's there # (Python 3.3+), otherwise import the pip package, which is required # in Python 2. # Import unittest2 if we have it (required on Python 2.6), # otherwise just use the stdlib unittest. try: import unittest2 as unittest # NOQA except ImportError: # Python >= 2.7 import unittest # NOQA try: from unittest import mock # NOQA except ImportError: # Python < 3.3 import mock # NOQA # Import the correct class used for when a file is opened. try: from io import TextIOWrapper except ImportError: # Python < 3 TextIOWrapper = file # NOQA ansible-tower-cli-3.2.0/tests/requirements.txt000066400000000000000000000001751316523067200214720ustar00rootroot00000000000000coverage>=3.7.1 fauxquests>=1.1 nose>=1.3.3 # === Python < 3.3 === # mock>=1.0.1 # === Python < 2.7 === # unittest2>=0.5.1 ansible-tower-cli-3.2.0/tests/runtests.py000066400000000000000000000017511316523067200204500ustar00rootroot00000000000000# Copyright 2015, Ansible, Inc. # Luke Sneeringer # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. import os import sys from tests.compat import unittest # Ensure that the tests directory is part of our Python path. APP_ROOT = os.path.realpath(os.path.dirname(__file__) + '/../') sys.path.append(APP_ROOT) # Find tests. def load_tests(loader, standard_tests, throwaway): return loader.discover('tests') # Run the tests. if __name__ == '__main__': unittest.main() ansible-tower-cli-3.2.0/tests/test__init.py000066400000000000000000000020271316523067200207170ustar00rootroot00000000000000# Copyright 2015, Ansible, Inc. # Luke Sneeringer # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. import tower_cli from tests.compat import unittest class GetResourceTests(unittest.TestCase): """Establish that the `tower_cli.get_resource` method works in the way that it should. """ def test_get_resource(self): for res in ('credential', 'group', 'host', 'inventory', 'job_template', 'job', 'organization', 'project', 'team', 'user'): tower_cli.get_resource(res) ansible-tower-cli-3.2.0/tests/test_api.py000066400000000000000000000261331316523067200203720ustar00rootroot00000000000000# Copyright 2015, Ansible, Inc. # Luke Sneeringer # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. import json from datetime import datetime as dt, timedelta import requests from requests.sessions import Session from tower_cli.api import APIResponse, client, TowerTokenAuth,\ TOWER_DATETIME_FMT from tower_cli import exceptions as exc from tower_cli.conf import settings from tower_cli.utils import debug from tower_cli.utils.data_structures import OrderedDict from tower_cli.constants import CUR_API_VERSION from tests.compat import unittest, mock import click REQUESTS_ERRORS = [requests.exceptions.ConnectionError, requests.exceptions.SSLError] class ClientTests(unittest.TestCase): """A set of tests to ensure that the API Client class works in the way that we expect. """ def test_prefix_implicit_https(self): """Establish that the prefix property returns the appropriate URL prefix given a host with no specified protocol. """ with settings.runtime_values(host='33.33.33.33'): self.assertEqual(client.prefix, 'https://33.33.33.33/api/%s/' % CUR_API_VERSION) def test_prefix_explicit_protocol(self): """Establish that the prefix property returns the appropriate URL prefix and don't clobber over an explicit protocol. """ with settings.runtime_values(host='bogus://33.33.33.33/'): self.assertEqual(client.prefix, 'bogus://33.33.33.33/api/%s/' % CUR_API_VERSION) def test_request_ok(self): """Establish that a request that returns a valid JSON response returns without incident and comes back as an APIResponse. """ with client.test_mode as t: t.register_json('/ping/', {'status': 'ok'}) r = client.get('/ping/') # Establish that our response is an APIResponse and that our # JSONification method returns back an ordered dict. self.assertIsInstance(r, APIResponse) self.assertIsInstance(r.json(), OrderedDict) # Establish that our headers have expected auth. request = r.request self.assertEqual(request.headers['Authorization'], 'Basic bWVhZ2FuOlRoaXMgaXMgdGhlIGJlc3Qgd2luZS4=') # Make sure the content matches what we expect. self.assertEqual(r.json(), {'status': 'ok'}) def test_request_post(self): """Establish that on a POST request, we encode the provided data to JSON automatically. """ with client.test_mode as t: t.register_json('/ping/', {'status': 'ok'}, method='POST') r = client.post('/ping/', {'payload': 'this is my payload.'}) # Establish that our request has the expected payload, and # is sent using an application/json content type. headers = r.request.headers self.assertEqual(headers['Content-Type'], 'application/json') self.assertEqual(r.request.body, '{"payload": "this is my payload."}') def test_connection_ssl_error(self): """Establish that if we get a ConnectionError or an SSLError back from requests, that we deal with it nicely. """ for ErrorType in REQUESTS_ERRORS: with settings.runtime_values(verbose=False, host='https://foo.co'): with mock.patch.object(Session, 'request') as req: req.side_effect = ErrorType with self.assertRaises(exc.ConnectionError): client.get('/ping/') def test_connection_ssl_error_verbose(self): """Establish that if we get a ConnectionError or an SSLError back from requests, that we deal with it nicely, and additionally print the internal error if verbose is True. """ for ErrorType in REQUESTS_ERRORS: with settings.runtime_values(verbose=True, host='https://foo.co'): with mock.patch.object(Session, 'request') as req: req.side_effect = ErrorType with mock.patch.object(debug, 'log') as dlog: with self.assertRaises(exc.ConnectionError): client.get('/ping/') self.assertEqual(dlog.call_count, 5) def test_server_error(self): """Establish that server errors raise the ServerError exception as expected. """ with client.test_mode as t: t.register('/ping/', 'ERRORED!!', status_code=500) with self.assertRaises(exc.ServerError): client.get('/ping/') def test_auth_error(self): """Establish that authentication errors raise the AuthError exception. """ with client.test_mode as t: t.register('/ping/', 'ERRORED!!', status_code=401) with self.assertRaises(exc.AuthError): client.get('/ping/') def test_forbidden_error(self): """Establish that forbidden errors raise the ForbiddenError exception. """ with client.test_mode as t: t.register('/ping/', 'ERRORED!!', status_code=403) with self.assertRaises(exc.Forbidden): client.get('/ping/') def test_not_found_error(self): """Establish that authentication errors raise the NotFound exception. """ with client.test_mode as t: t.register('/ping/', 'ERRORED!!', status_code=404) with self.assertRaises(exc.NotFound): client.get('/ping/') def test_method_not_allowed_error(self): """Establish that authentication errors raise the MethodNotAllowed exception. """ with client.test_mode as t: t.register('/ping/', 'ERRORED!!', status_code=405) with self.assertRaises(exc.MethodNotAllowed): client.get('/ping/') def test_bad_request_error(self): """Establish that other errors not covered above raise the BadRequest exception. """ with client.test_mode as t: t.register('/ping/', "I'm a teapot!", status_code=418) with self.assertRaises(exc.BadRequest): client.get('/ping/') def test_insecure_connection(self): """Establish that the --insecure flag will cause the program to call request with verify=False. """ with mock.patch('requests.sessions.Session.request') as g: mock_response = type('statobj', (), {})() # placeholder object mock_response.status_code = 200 g.return_value = mock_response with client.test_mode as t: t.register('/ping/', "I'm a teapot!", status_code=200) with settings.runtime_values(verify_ssl=False): client.get('/ping/') g.assert_called_once_with( # The pont is to assure verify=False below 'GET', mock.ANY, allow_redirects=True, auth=(mock.ANY, mock.ANY), verify=False ) def test_http_contradiction_error(self): """Establish that commands can not be ran with verify_ssl set to false and an http connection.""" with settings.runtime_values( host='http://33.33.33.33', verify_ssl=True): with self.assertRaises(exc.TowerCLIError): client.prefix def test_failed_suggestion_protocol(self): """Establish that if connection fails and protocol not given, tower-cli suggests that to the user.""" with settings.runtime_values(verbose=False, host='foo.co'): with mock.patch.object(Session, 'request') as req: req.side_effect = requests.exceptions.SSLError with mock.patch.object(click, 'secho') as secho: with self.assertRaises(exc.ConnectionError): client.get('/ping/') self.assertTrue(secho.called) class TowerTokenAuthTests(unittest.TestCase): def setUp(self): class Req(object): def __init__(self): self.headers = {} self.auth = TowerTokenAuth('alice', 'pass', client) self.req = Req() self.expires = dt.utcnow() def test_reading_valid_token(self): self.expires += timedelta(hours=1) expires = self.expires.strftime(TOWER_DATETIME_FMT) with mock.patch('six.moves.builtins.open', new_callable=mock.mock_open()): with mock.patch('tower_cli.api.json.load', return_value={'token': 'foobar', 'expires': expires}): self.auth(self.req) self.assertEqual(self.req.headers['Authorization'], 'Token foobar') def test_reading_invalid_token(self): self.expires += timedelta(hours=1) expires = self.expires.strftime(TOWER_DATETIME_FMT) with mock.patch('six.moves.builtins.open', new_callable=mock.mock_open()): with mock.patch('tower_cli.api.json.load', return_value="invalid"): with client.test_mode as t: t.register('/authtoken/', json.dumps({'token': 'barfoo', 'expires': expires}), status_code=200, method='POST') self.auth(self.req) self.assertEqual(self.req.headers['Authorization'], 'Token barfoo') def test_reading_expired_token(self): self.expires += timedelta(hours=-1) expires = self.expires.strftime(TOWER_DATETIME_FMT) with mock.patch('six.moves.builtins.open', new_callable=mock.mock_open()): with mock.patch('tower_cli.api.json.load', return_value={'token': 'foobar', 'expires': expires}): with client.test_mode as t: t.register('/authtoken/', json.dumps({'token': 'barfoo', 'expires': expires}), status_code=200, method='POST') self.auth(self.req) self.assertEqual(self.req.headers['Authorization'], 'Token barfoo') def test_reading_invalid_token_from_server(self): self.expires += timedelta(hours=-1) expires = self.expires.strftime(TOWER_DATETIME_FMT) with mock.patch('six.moves.builtins.open', new_callable=mock.mock_open()): with mock.patch('tower_cli.api.json.load', return_value={'token': 'foobar', 'expires': expires}): with client.test_mode as t: with self.assertRaises(exc.AuthError): t.register('/authtoken/', json.dumps({'invalid': 'invalid'}), status_code=200, method='POST') self.auth(self.req) ansible-tower-cli-3.2.0/tests/test_cli_action.py000066400000000000000000000050261316523067200217230ustar00rootroot00000000000000# Copyright 2015, Ansible, Inc. # Luke Sneeringer # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. import click from click.testing import CliRunner from tower_cli.cli.action import ActionSubcommand from tests.compat import unittest CATEGORIZED_OUTPUT = """Usage: foo [OPTIONS] Field Options: --bar TEXT foobar Local Options: --foo TEXT foobar Global Options: --tower-host TEXT foobar Other Options: --help Show this message and exit. """ class ActionCommandTests(unittest.TestCase): """A set of tests to ensure that the tower_cli Command class works in the way we expect. """ def setUp(self): self.runner = CliRunner() def test_dash_dash_help(self): """Establish that no_args_is_help causes the help to be printed, and an exit. """ # Create a command with which to test. @click.command(no_args_is_help=True, cls=ActionSubcommand) @click.argument('parrot') def foo(parrot): click.echo(parrot) # Establish that this command echos if called with echo. self.assertEqual(self.runner.invoke(foo, ['bar']).output, 'bar\n') # Establish that this command sends help if called with nothing. result = self.runner.invoke(foo) self.assertIn('--help', result.output) self.assertIn('Show this message and exit.\n', result.output) def test_categorize_options(self): """Establish that options in help text are correctly categorized. """ @click.command(cls=ActionSubcommand) @click.option('--foo', help='foobar') @click.option('--bar', help='[FIELD]foobar') @click.option('--tower-host', help='foobar') def foo(): pass result = self.runner.invoke(foo) self.assertEqual(result.output, CATEGORIZED_OUTPUT) @click.command(cls=ActionSubcommand, add_help_option=False) def bar(): pass result = self.runner.invoke(bar) self.assertEqual(result.output, 'Usage: bar [OPTIONS]\n') ansible-tower-cli-3.2.0/tests/test_cli_misc.py000066400000000000000000000322551316523067200214050ustar00rootroot00000000000000# Copyright 2015, Ansible, Inc. # Luke Sneeringer # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. import os.path import stat import warnings import click from click.testing import CliRunner import requests import tower_cli from tower_cli.api import client from tower_cli.cli.misc import config, version, _echo_setting from tower_cli.conf import settings from tests.compat import unittest, mock class VersionTests(unittest.TestCase): """A set of tests to ensure that the version command runs in the way that we expect. """ def setUp(self): self.runner = CliRunner() def test_version_command(self): """Establish that the version command returns the output we expect. """ # Set up output from the /config/ endpoint in Tower and # invoke the command. with client.test_mode as t: t.register_json('/config/', {'version': '4.21'}) result = self.runner.invoke(version) # Verify that we got the output we expected. self.assertEqual(result.exit_code, 0) self.assertEqual( result.output.strip(), 'Tower CLI %s\nAnsible Tower 4.21' % tower_cli.__version__, ) def test_cannot_connect(self): """Establish that the version command gives a nice error in cases where it cannot connect to Tower. """ with mock.patch.object(client, 'get') as get: get.side_effect = requests.exceptions.RequestException result = self.runner.invoke(version) self.assertEqual(result.exit_code, 1) self.assertIn('Could not connect to Ansible Tower.', result.output) class ConfigTests(unittest.TestCase): """Establish that the `tower-cli config` command works in the way that we expect. """ def setUp(self): self.runner = CliRunner() def test_no_arguments(self): """Establish that if `tower-cli config` is called with no arguments, that we print out the current configuration. """ # Invoke the command. with settings.runtime_values(username='meagan', verbose=False, password='This is the best wine.'): result = self.runner.invoke(config) # Ensure that we got a 0 exit status self.assertEqual(result.exit_code, 0) # Ensure that the output looks correct. self.assertIn('username: meagan', result.output) self.assertIn('password: This is the best wine.', result.output) self.assertIn('verbose: False', result.output) def test_key_and_no_value(self): """Establish that if we are given a key and no value, that the setting's value is printed. """ with settings.runtime_values(password='This is the best wine.'): result = self.runner.invoke(config, ['password']) self.assertEqual(result.exit_code, 0) self.assertEqual(result.output.strip(), 'password: This is the best wine.') def test_write_setting(self): """Establish that if we attempt to write a valid setting, that the parser's write method is run. """ # Invoke the command, but trap the file-write at the end # so we don't plow over real things. mock_open = mock.mock_open() filename = os.path.expanduser('~/.tower_cli.cfg') with mock.patch('tower_cli.cli.misc.open', mock_open, create=True): with mock.patch.object(os, 'chmod') as chmod: result = self.runner.invoke(config, ['username', 'luke']) chmod.assert_called_once_with(filename, int('0600', 8)) # Ensure that the command completed successfully. self.assertEqual(result.exit_code, 0) self.assertEqual(result.output.strip(), 'Configuration updated successfully.') # Ensure that the output seems to be correct. self.assertIn(mock.call(os.path.expanduser('~/.tower_cli.cfg'), 'w'), mock_open.mock_calls) self.assertIn(mock.call().write('username = luke\n'), mock_open.mock_calls) def test_permissions_warning(self): """Warn user if configuration file permissions can not be set """ # Try to set permissions on file that does not exist, expecting warning mock_open = mock.mock_open() filename = '.tower_cli.cfg' with mock.patch('tower_cli.cli.misc.open', mock_open, create=True): with mock.patch.object(os, 'chmod') as chmod: chmod.side_effect = OSError with mock.patch.object(warnings, 'warn') as warn: result = self.runner.invoke( config, ['username', 'luke', '--scope=local']) warn.assert_called_once_with(mock.ANY, UserWarning) chmod.assert_called_once_with( filename, stat.S_IRUSR | stat.S_IWUSR) # Ensure that the command completed successfully. self.assertEqual(result.exit_code, 0) self.assertEqual(result.output.strip(), 'Configuration updated successfully.') def test_write_global_setting(self): """Establish that if we attempt to write a valid setting, that the parser's write method is run. """ # Invoke the command, but trap the file-write at the end # so we don't plow over real things. filename = '/etc/tower/tower_cli.cfg' mock_open = mock.mock_open() with mock.patch('tower_cli.cli.misc.open', mock_open, create=True): with mock.patch.object(os.path, 'isdir') as isdir: with mock.patch.object(os, 'chmod') as chmod: isdir.return_value = True result = self.runner.invoke( config, ['username', 'luke', '--scope=global'], ) isdir.assert_called_once_with('/etc/tower/') chmod.assert_called_once_with(filename, int('0600', 8)) # Ensure that the command completed successfully. self.assertEqual(result.exit_code, 0) self.assertEqual(result.output.strip(), 'Configuration updated successfully.') # Ensure that the output seems to be correct. self.assertIn(mock.call('/etc/tower/tower_cli.cfg', 'w'), mock_open.mock_calls) self.assertIn(mock.call().write('username = luke\n'), mock_open.mock_calls) def test_write_local_setting(self): """Establish that if we attempt to write a valid setting locally, that the correct parser's write method is run. """ # Invoke the command, but trap the file-write at the end # so we don't plow over real things. mock_open = mock.mock_open() with mock.patch('tower_cli.cli.misc.open', mock_open, create=True): with mock.patch.object(os, 'chmod') as chmod: result = self.runner.invoke( config, ['username', 'meagan', '--scope=local'], ) filename = ".tower_cli.cfg" chmod.assert_called_once_with(filename, int('0600', 8)) # Ensure that the command completed successfully. self.assertEqual(result.exit_code, 0) self.assertEqual(result.output.strip(), 'Configuration updated successfully.') # Ensure that the output seems to be correct. self.assertIn(mock.call('.tower_cli.cfg', 'w'), mock_open.mock_calls) self.assertIn(mock.call().write('username = meagan\n'), mock_open.mock_calls) def test_unset(self): """Establish that calling `tower-cli config --unset` works in the way that we expect. """ # Invoke the command, but trap the file-write at the end # so we don't plow over real things. mock_open = mock.mock_open() with mock.patch('tower_cli.cli.misc.open', mock_open, create=True): with mock.patch.object(os, 'chmod'): result = self.runner.invoke(config, ['username', '--unset']) # Ensure that the command completed successfully. self.assertEqual(result.exit_code, 0) self.assertEqual(result.output.strip(), 'Configuration updated successfully.') # Ensure that the output seems to be correct. self.assertNotIn(mock.call().write('username = luke\n'), mock_open.mock_calls) def test_error_invalid_key(self): """Establish that if `tower-cli config` is sent an invalid key, that we raise an exception. """ result = self.runner.invoke(config, ['bogus']) self.assertEqual(result.exit_code, 1) self.assertEqual(result.output.strip(), 'Error: Invalid configuration option "bogus".') def test_error_value_and_unset(self): """Establish that if `tower-cli config` is called with both a value and the --unset flag, that we raise an exception. """ result = self.runner.invoke(config, ['host', '127.0.0.1', '--unset']) self.assertEqual(result.exit_code, 2) self.assertEqual(result.output.strip(), 'Error: Cannot provide both a value and --unset.') def test_error_no_global_config_file(self): """Establish that if no global config file exists, that tower-cli does not attempt to create it. """ with mock.patch.object(os.path, 'isdir') as isdir: isdir.return_value = False result = self.runner.invoke(config, ['host', 'foo', '--scope=global']) isdir.assert_called_once_with('/etc/tower/') self.assertEqual(result.exit_code, 1) self.assertEqual(result.output.strip(), 'Error: /etc/tower/ does not exist, and this ' 'command cowardly declines to create it.') class SupportTests(unittest.TestCase): """Establish that support functions in this module work in the way that we expect. """ def test_echo_setting(self): """Establish that the `echo_setting` method works in the way that we expect. """ with settings.runtime_values(host='20.12.4.21'): with mock.patch.object(click, 'secho') as secho: _echo_setting('host') self.assertEqual(secho.mock_calls, [ mock.call('host: ', fg='magenta', bold=True, nl=False), mock.call('20.12.4.21', fg='white', bold=True), ]) class DeprecationTests(unittest.TestCase): """Establish any deprecation notices are sent with a command if they are expected. """ def setUp(self): self.runner = CliRunner() def test_write_global_setting_deprecated(self): """Establish that if we attempt to write a valid setting, that the parser's write method is run. """ # Invoke the command, but trap the file-write at the end # so we don't plow over real things. mock_open = mock.mock_open() warning_text = 'The `--global` option is deprecated and will be '\ 'removed. Use `--scope=global` to get the same effect.' with mock.patch('tower_cli.cli.misc.open', mock_open, create=True): with mock.patch.object(os.path, 'isdir') as isdir: with mock.patch.object(os, 'chmod'): with mock.patch.object(warnings, 'warn') as warn: isdir.return_value = True result = self.runner.invoke( config, ['username', 'meagan', '--global'], ) warn.assert_called_once_with(warning_text, DeprecationWarning) self.assertEqual(warn.mock_calls[0][1][1], DeprecationWarning) isdir.assert_called_once_with('/etc/tower/') # Ensure that the command completed successfully. self.assertEqual(result.exit_code, 0) self.assertEqual('Configuration updated successfully.', result.output.strip()) # Ensure that the output seems to be correct. self.assertIn(mock.call('/etc/tower/tower_cli.cfg', 'w'), mock_open.mock_calls) self.assertIn(mock.call().write('username = meagan\n'), mock_open.mock_calls) ansible-tower-cli-3.2.0/tests/test_cli_resource.py000066400000000000000000000407551316523067200223050ustar00rootroot00000000000000# -*- coding: utf-8 -*- import json import yaml import click from tower_cli import models, resources from tower_cli.cli.resource import ResSubcommand from tower_cli.cli.types import StructuredInput from tower_cli.conf import settings from tests.compat import unittest, mock try: basestring except NameError: basestring = None class SubcommandTests(unittest.TestCase): """A set of tests for establishing that the Subcommand class created on the basis of a Reosurce class works in the way we expect. """ def setUp(self): """Install a resource instance sufficient for testing common things with subcommands. """ class BasicResource(models.Resource): endpoint = '/basic/' name = models.Field(unique=True) self.resource = BasicResource() self.command = ResSubcommand(self.resource) def test_command_instance(self): """Establish that the command based on a resource is, in fact, a click MultiCommand. """ # Assert that it is a click command, and that it has the commands # available on the resource. self.assertIsInstance(self.command, click.MultiCommand) def test_list_commands(self): """Establish that the `list_commands` method for the command corresponds to the commands available on the resource. """ self.assertEqual(set(self.resource.commands), set(self.command.list_commands(None))) def test_get_command(self): """Establish that the `get_command` method returns the appropriate resource method wrapped as a click command. """ list_command = self.command.get_command(None, 'list') # Ensure that this is a click command. self.assertIsInstance(list_command, click.core.Command) # Ensure that this command has an option corresponding to the # "name" unique field. self.assertEqual(list_command.params[0].name, 'name') self.assertIn('--name', list_command.params[0].opts) def test_get_command_error(self): """Establish that if `get_command` is called against a command that does not actually exist on the resource, that null value is returned. """ self.assertEqual(self.command.get_command(None, 'bogus'), None) def test_command_with_pk(self): """Establish that the `get_command` method appropriately adds a primary key argument if the method has a "pk" positional argument. """ # Create a resource with an appropriate command. class PKResource(models.BaseResource): endpoint = '/pkr/' @resources.command def my_method(self, pk): pass # Get the command version of my_method. my_method = ResSubcommand(PKResource()).get_command(None, 'my_method') # Establish that the `my_method` command does, in fact, have a PK # click argument attached. self.assertEqual(my_method.params[-1].name, 'pk') def test_use_fields_as_options_false(self): """Establish that the `use_fields_as_options` attribute is honored if set to False. """ # Create a resource with a command that doesn't expect its # fields to become options. class NoOptResource(models.BaseResource): endpoint = '/nor/' f1 = models.Field() f2 = models.Field() @resources.command(use_fields_as_options=False) def noopt(self): pass # Make the resource into a command, and get the noopt subcommand. noopt = ResSubcommand(NoOptResource()).get_command(None, 'noopt') # Establish that the noopt command does NOT have fields as options. self.assertFalse(any([o.name == 'f1' for o in noopt.params])) self.assertFalse(any([o.name == 'f2' for o in noopt.params])) def test_use_fields_as_options_enumerated(self): """Establish that the `use_fields_as_options` attribute is honored if set to a tuple containing a subset of fields. """ # Create a resource with a command that doesn't expect its # fields to become options. class NoOptResource(models.BaseResource): endpoint = '/nor/' f1 = models.Field() f2 = models.Field() @resources.command(use_fields_as_options=('f2',)) def noopt(self): pass # Make the resource into a command, and get the noopt subcommand. noopt = ResSubcommand(NoOptResource()).get_command(None, 'noopt') # Establish that the noopt command does NOT have fields as options. self.assertFalse(any([o.name == 'f1' for o in noopt.params])) self.assertTrue(any([o.name == 'f2' for o in noopt.params])) def test_fields_not_options(self): """Establish that a field which is not an option is not made into an option for commands. """ # Create a resource with a field that is an option and another # field that isn't. class NoOptionResource(models.Resource): endpoint = '/nor/' yes = models.Field() no = models.Field(is_option=False) # Make the resource into a command, and get a reasonably-arbitrary # subcommand. cmd = ResSubcommand(NoOptionResource()).get_command(None, 'list') # Establish that "yes" is an option on the command and "no" is not. self.assertTrue(any([o.name == 'yes' for o in cmd.params])) self.assertFalse(any([o.name == 'no' for o in cmd.params])) def test_field_explicit_key(self): """Establish that if a field is given an explicit key, that they key is used for the field name instead of the implicit name. """ # Create a resource with a field that has an explicit key. class ExplicitKeyResource(models.Resource): endpoint = '/ekr/' option_name = models.Field('internal_name') # Make the resource into a command, and get a reasonably-arbitrary # subcommand. cmd = ResSubcommand(ExplicitKeyResource()).get_command(None, 'get') # Establish that the field has an option of --option-name, and # a name of internal_name. opt = cmd.params[0] self.assertEqual(opt.name, 'internal_name') self.assertEqual(opt.opts, ['--option-name']) def test_field_help_text_has_prefix(self): """Establish that resource field help text is properly prefixed. """ class FieldHelpTextResource(models.Resource): endpoint = '/foobar/' option_name = models.Field('internal_name', help_text='foobar', required=False) cmd = ResSubcommand(FieldHelpTextResource()).get_command(None, 'get') opt = cmd.params[0] self.assertEqual(opt.help, '[FIELD]foobar') def test_field_help_text_has_suffix_for_structured_input(self): """Establish that resource field help text is properly suffixed if field type is StructuredInput. """ class FieldHelpTextResource(models.Resource): endpoint = '/foobar/' option_name = models.Field('internal_name', type=StructuredInput(), help_text='foobar', required=False) cmd = ResSubcommand(FieldHelpTextResource()).get_command(None, 'get') opt = cmd.params[0] self.assertEqual(opt.help.endswith(' Use @ to get JSON or YAML from a file.'), True) def test_docstring_replacement_an(self): """Establish that for resources with names beginning with vowels, that the automatic docstring replacement is grammatically correct. """ # Create a resource with an approriate name. class Oreo(models.Resource): resource_name = 'Oreo cookie' # COOOOOOKIES!!!! endpoint = '/oreo/' # Get the Oreo resource's create method. create = ResSubcommand(Oreo()).get_command(None, 'create') self.assertIn('Create an Oreo cookie', create.help) def test_docstring_replacement_y(self): """Establish that for resources with names ending in y, that plural replacement is correct. """ # Create a resource with an approriate name. class Oreo(models.Resource): resource_name = 'telephony' endpoint = '/telephonies/' # Get the Oreo resource's create method. create = ResSubcommand(Oreo()).get_command(None, 'list') self.assertIn('list of telephonies', create.help) def test_echo_method(self): """Establish that the _echo_method subcommand class works in the way we expect. """ func = self.command._echo_method(lambda: {'foo': 'bar'}) with mock.patch.object(click, 'secho') as secho: with settings.runtime_values(format='json'): func() secho.assert_called_once_with(json.dumps({'foo': 'bar'}, indent=2)) def test_echo_method_format_freezer(self): """Establish that the _echo_method subcommand class respects format_freezer attribute of inner method. """ def inner_func(): return {'foo': 'bar'} inner_func.format_freezer = 'json' func = self.command._echo_method(inner_func) with mock.patch.object(click, 'secho') as secho: with settings.runtime_values(format='human'): func() secho.assert_called_once_with(json.dumps({'foo': 'bar'}, indent=2)) def test_echo_method_changed_false(self): """Establish that the _echo_method subcommand decorator works in the way we expect if we get an unchanged designation. """ func = self.command._echo_method(lambda: {'changed': False}) with mock.patch.object(click, 'secho') as secho: with settings.runtime_values(format='json', color=True): func() answer = json.dumps({'changed': False}, indent=2) secho.assert_called_once_with(answer, fg='green') def test_echo_method_changed_true(self): """Establish that the _echo_method subcommand decorator works in the way we expect if we get an changed designation. """ func = self.command._echo_method(lambda: {'changed': True}) with mock.patch.object(click, 'secho') as secho: with settings.runtime_values(format='json', color=True): func() answer = json.dumps({'changed': True}, indent=2) secho.assert_called_once_with(answer, fg='yellow') def test_echo_method_yaml_formatted(self): """Establish that the `_echo_method` properly returns YAML formatting when it gets back a list of objects. """ func = self.command._echo_method(lambda: {'foo': 'bar'}) with mock.patch.object(click, 'secho') as secho: with settings.runtime_values(format='yaml'): func() secho.assert_called_once_with(yaml.safe_dump({'foo': 'bar'}, indent=2, allow_unicode=True, default_flow_style=False)) def test_echo_method_human_formatted(self): """Establish that the `_echo_method` properly returns human formatting when it gets back a list of objects. """ func = self.command._echo_method(lambda: {'results': [ {'id': 1, 'name': 'Durham, NC'}, {'id': 2, 'name': 'Austin, TX'}, ]}) with mock.patch.object(click, 'secho') as secho: with settings.runtime_values(format='human'): func() output = secho.mock_calls[-1][1][0] self.assertIn('1 Durham, NC', output) self.assertIn('2 Austin, TX', output) def test_unicode_human_formatting(self): value = 'unicode ❤ ☀ ☆ ☂' data = { 'count': 1, 'results': [ {'id': 1, 'name': 'ascii'}, { 'id': 42, 'name': '' } ] } if basestring: # Python < 3 data['results'][1]['name'] = value.decode('utf-8') else: data['results'][1]['name'] = value output = self.command._format_human(data) assert value in output def test_echo_method_human_formatted_changed(self): """Establish that if there is a change and no id is returned, we print a generic OK message. """ func = self.command._echo_method(lambda: {'changed': False}) with mock.patch.object(click, 'secho') as secho: with settings.runtime_values(format='human'): func() output = secho.mock_calls[-1][1][0] self.assertEqual(output, 'OK. (changed: false)') def test_echo_method_human_formatted_no_records(self): """Establish that if there are no records sent to the human formatter, that it prints a terse message to that effect. """ func = self.command._echo_method(lambda: {'results': []}) with mock.patch.object(click, 'secho') as secho: with settings.runtime_values(format='human'): func() output = secho.mock_calls[-1][1][0] self.assertEqual(output, 'No records found.') def test_echo_method_human_formatted_single_result(self): """Establish that a single result sent to the human formatter shows a table with a single row as expected. """ f = self.command._echo_method(lambda: {'id': 1, 'name': 'Durham, NC'}) with mock.patch.object(click, 'secho') as secho: with settings.runtime_values(format='human'): f() output = secho.mock_calls[-1][1][0] self.assertIn('1 Durham, NC', output) def test_echo_method_human_boolean_formatting(self): """Establish that booleans are formatted right-aligned, lower-cased in human output. """ func = self.command._echo_method(lambda: {'results': [ {'id': 1, 'name': 'Durham, NC'}, {'id': 2, 'name': True}, ]}) with mock.patch.object(click, 'secho') as secho: with settings.runtime_values(format='human'): func() output = secho.mock_calls[-1][1][0] self.assertIn('1 Durham, NC', output) self.assertIn('2 true', output) def test_echo_method_human_pagination(self): """Establish that pagination works in human formatting, and it prints the way we expect. """ func = self.command._echo_method(lambda: {'results': [ {'id': 1, 'name': 'Durham, NC'}, {'id': 2, 'name': True}, ], 'next': 3, 'count': 10, 'previous': 1}) with mock.patch.object(click, 'secho') as secho: with settings.runtime_values(format='human'): func() output = secho.mock_calls[-1][1][0] self.assertIn('(Page 2 of 5.)', output) def test_echo_method_human_pagination_last_page(self): """Establish that pagination works in human formatting, and it prints the way we expect on the final page.. """ func = self.command._echo_method(lambda: {'results': [ {'id': 1, 'name': 'Durham, NC'}, ], 'next': None, 'count': 3, 'previous': 1}) with mock.patch.object(click, 'secho') as secho: with settings.runtime_values(format='human'): func() output = secho.mock_calls[-1][1][0] self.assertIn('(Page 2 of 2.)', output) def test_echo_method_human_custom_output(self): """Establish that a custom dictionary with no ID is made into a table and printed as expected. """ func = self.command._echo_method(lambda: {'foo': 'bar', 'spam': 'eggs'}) with mock.patch.object(click, 'secho') as secho: with settings.runtime_values(format='human'): func() output = secho.mock_calls[-1][1][0] self.assertIn('foo', output) self.assertIn('spam', output) self.assertIn('bar', output) self.assertIn('eggs', output) def test_echo_id(self): for input_format in [{'id': 5}, {'count': 1, 'results': [{'id': 5}]}]: func = self.command._echo_method(lambda: input_format) with mock.patch.object(click, 'secho') as secho: with settings.runtime_values(format='id'): func() output = secho.mock_calls[-1][1][0] self.assertEqual('5', output) ansible-tower-cli-3.2.0/tests/test_cli_types.py000066400000000000000000000170531316523067200216150ustar00rootroot00000000000000# Copyright 2015, Ansible, Inc. # Luke Sneeringer # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. import os.path from six.moves import StringIO import click from tower_cli import exceptions as exc from tower_cli.api import client from tower_cli.cli import types from tower_cli import get_resource from tests.compat import unittest, mock class FileTests(unittest.TestCase): """A set of tests establishing that the tower_cli subclass of click.File works in the way we expect. """ def test_convert_file_object(self): """Establish that if we receive a file-like object to the convert method, that it is passed through without action. """ sio = StringIO('The cat is trying to eat my goldfish crackers.') f = types.File('r') self.assertEqual(sio, f.convert(sio, 'myfile', None)) def test_convert_expanduser(self): """Establish that if a filename is specified with a user home directory shortcut, that it is expanded appropriately. """ f = types.File('f') with mock.patch.object(click.File, 'convert') as convert: f.convert('~/my_file.txt', 'myfile', None) convert.assert_called_with(os.path.expanduser('~/my_file.txt'), 'myfile', None) def test_variables_file(self): """Establish that file with variables is opened in this type.""" f = types.Variables() with mock.patch.object(click.File, 'convert') as convert: convert.return_value = "foo: bar" foo_converted = f.convert('@foobar.yml', 'myfile', None) convert.assert_called_once_with("foobar.yml", 'myfile', None) self.assertEqual(foo_converted, 'foo: bar') def test_variables_no_file(self): """Establish that plain variables are passed as-is.""" f = types.Variables() foo_converted = f.convert('foo: barz', 'myfile', None) self.assertEqual(foo_converted, 'foo: barz') def test_variables_backup_option(self): """Establish that non-string input is protected against.""" f = types.Variables() foo_converted = f.convert(54, 'myfile', None) self.assertEqual(foo_converted, 54) class StructuredInputTests(unittest.TestCase): """A set of tests to establish that the JSONFile class works in the way that we expect. """ def test_deserialize_valid_input(self): s = '{"foo": "bar"}' p = click.Option(('name', '-n')) f = types.StructuredInput() f_converted = f.convert(s, p, None) self.assertEqual(f_converted, {'foo': 'bar'}) def test_invalid_json_raises_exception(self): s = 'not valid{"foo":}' p = click.Option(('name', '-n')) f = types.StructuredInput() with self.assertRaises(exc.UsageError): f.convert(s, p, None) class MappedChoiceTests(unittest.TestCase): """A set of tests to establish that the MappedChoice class works in the way that we expect. """ def test_convert(self): """Establish that the convert method converts from the value provided to the user to the internal value, and calls the superclass method. """ mc = types.MappedChoice({'foo': 'bar', 'spam': 'eggs'}) self.assertEqual(mc.convert('bar', 'myopt', None), 'foo') self.assertEqual(mc.convert('eggs', 'myopt', None), 'spam') class RelatedTests(unittest.TestCase): """A set of tests to establish that the Related class works in the way that we expect. """ def setUp(self): self.related = types.Related('user') def test_convert_none(self): """Establish that attempting to convert None just returns None, as required by click. """ self.assertEqual(self.related.convert(None, 'mything', None), None) def test_convert_null_no_record(self): """Establish 'null' passes through as-is""" self.assertEqual(self.related.convert('null', 'mything', None), 'null') def test_convert_int(self): """Establish that if we get an integer sent to the convert method, that it's passed through with no action taken on it (idempotency). """ self.assertEqual(self.related.convert(42, 'mything', None), 42) def test_convert_number_as_string(self): """Establish that if we get a string value that looks like an integer, that we pass it through with no action taken (except to convert it to a true integer). """ self.assertEqual(self.related.convert('42', 'mything', None), 42) def test_convert_by_lookup(self): """Establish that if we get a string value, we do a lookup against the resource's identity, and return back the ID that we get back in response. """ p = click.Option(('name', '-n')) with client.test_mode as t: t.register_json('/users/?username=meagan', { 'count': 1, 'results': [{'id': 42}], }) self.assertEqual(self.related.convert('meagan', p, None), 42) def test_convert_by_lookup_error(self): """Establish that if doing a foreign key lookup fails, that we raise an appropriate exception. """ p = click.Option(('name', '-n')) with client.test_mode as t: t.register_json('/users/?username=meagan', {}, status_code=404) with self.assertRaises(exc.RelatedError): self.related.convert('meagan', p, None) def test_convert_by_lookup_multi(self): """Establish that if doing a foreign key lookup returns multiple results, that we return a special error. """ p = click.Option(('name', '-n')) with client.test_mode as t: t.register_json('/users/?username=meagan', { 'count': 2, 'results': [{'id': 42}, {'id': 84}], }) with self.assertRaises(exc.MultipleRelatedError): self.assertEqual(self.related.convert('meagan', p, None), 42) def test_get_metavar(self): """Establish that the metavar ends up being what we expect, which is the resource name, but in uppercase. """ self.assertEqual(self.related.get_metavar(None), 'USER') class GeneralTests(unittest.TestCase): """A set of tests that are not specific to any type. """ def test_type_name(self): """Establish that custom types are equipped with __name__ field and the upstream click.Choice's issue of having no __name__ field is properly dealt with. """ names = [ 'ad_hoc', 'credential', 'group', 'host', 'inventory', 'inventory_source', 'job', 'job_template', 'organization', 'project', 'team', 'user' ] for res_name in names: res = get_resource(res_name) self.assertEqual(type(res.fields), type([])) self.assertEqual(str(res.fields).startswith('[ # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. import click import os import os.path import stat import warnings from six.moves import StringIO from tower_cli.conf import Parser, Settings, runtime_context_manager from tests.compat import unittest, mock class SettingsTests(unittest.TestCase): """A set of tests to establish that our settings object works in the way that we expect. """ def test_error_etc_awx(self): """Establish that if /etc/tower/ exists but isn't readable, that we properly catch it and whine about it. """ with mock.patch.object(os, 'getcwd') as getcwd: getcwd.return_value = os.path.expanduser('~') with mock.patch.object(warnings, 'warn') as warn: with mock.patch.object(os.path, 'isdir') as isdir: isdir.return_value = True with mock.patch.object(os, 'listdir') as listdir: listdir.side_effect = OSError settings = Settings() settings warn.assert_called_once_with( '/etc/tower/ is present, but not readable with ' 'current permissions. Any settings defined in ' '/etc/tower/tower_cli.cfg will not be honored.', RuntimeWarning, ) class ParserTests(unittest.TestCase): """A set of tests to establish that our Parser subclass works in the way that we expect. """ def test_parser_read_with_header(self): """Establish that the parser can read settings with a standard header. """ parser = Parser() read_file_method = getattr(parser, 'read_file', parser.readfp) read_file_method(StringIO('[general]\nfoo: bar\n')) self.assertEqual(parser.get('general', 'foo'), 'bar') def test_parser_read_without_header(self): """Establish that the parser can read settings without a standard header, and that "general" is then implied. """ parser = Parser() read_file_method = getattr(parser, 'read_file', parser.readfp) read_file_method(StringIO('foo: bar')) self.assertEqual(parser.get('general', 'foo'), 'bar') def test_file_permission_warning(self): """Warn file permissions may expose credentials """ with mock.patch.object(warnings, 'warn') as warn: with mock.patch.object(os.path, 'isfile') as isfile: with mock.patch.object(os, 'stat') as os_stat: isfile.return_value = True mock_stat = type('statobj', (), {})() # placeholder object mock_stat.st_mode = stat.S_IRUSR | stat.S_IWUSR | \ stat.S_IROTH os_stat.return_value = mock_stat # readable to others parser = Parser() read_file_method = getattr(parser, 'read_file', parser.readfp) read_file_method(StringIO('[general]\nfoo: bar\n')) warn.assert_called_once_with(mock.ANY, RuntimeWarning) # Also run with acceptable permissions, verify that no warning issued with mock.patch.object(warnings, 'warn') as warn: with mock.patch.object(os.path, 'isfile') as isfile: with mock.patch.object(os, 'stat') as os_stat: isfile.return_value = True mock_stat = type('statobj', (), {})() # placeholder object mock_stat.st_mode = stat.S_IRUSR | stat.S_IWUSR os_stat.return_value = mock_stat # not readable to others parser = Parser() read_file_method = getattr(parser, 'read_file', parser.readfp) read_file_method(StringIO('[general]\nfoo: bar\n')) assert not warn.called class ConfigFromEnvironmentTests(unittest.TestCase): """A set of tests that ensure the environment from config parsing works as intended. """ def test_no_override(self): """Establish that the environment variables do not override explicitly passed in values.""" settings = Settings() with mock.patch.dict(os.environ, {'TOWER_HOST': 'myhost'}): with settings.runtime_values(host='yourhost'): self.assertEqual(settings.host, 'yourhost') def test_read_from_env(self): """Establish that the environment variables are correctly parsed and the values are set in the settings.""" settings = Settings() mock_env = {'TOWER_HOST': 'myhost', 'TOWER_PASSWORD': 'mypass', 'TOWER_USERNAME': 'myuser', 'TOWER_VERIFY_SSL': 'False'} with mock.patch.dict(os.environ, mock_env): with settings.runtime_values(): self.assertEqual(settings.host, 'myhost') self.assertEqual(settings.username, 'myuser') self.assertEqual(settings.password, 'mypass') self.assertEqual(settings.verify_ssl, False) class CommandTests(unittest.TestCase): """Establish that the @command decorator works as I expect, with and without a call signature. """ def test_command_with_call_signature(self): """Establish that if we call the @click.command decorator with a call signature, that it decorates the function appropriately. """ # Define a command @click.command() def foo(): pass # Ensure that it's a command. self.assertIsInstance(foo, click.core.Command) def test_runtime_context_manager(self): """Test that the kwargs from settings are removed before running the command from the resource itself. """ def foo(**kwargs): self.assertEqual(kwargs, {}) foo = runtime_context_manager(foo) foo(tower_username='foobear') ansible-tower-cli-3.2.0/tests/test_exceptions.py000066400000000000000000000023021316523067200217720ustar00rootroot00000000000000# Copyright 2015, Ansible, Inc. # Luke Sneeringer # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. from six.moves import StringIO from tower_cli.exceptions import TowerCLIError from tests.compat import unittest class ExceptionTests(unittest.TestCase): """A set of tests to ensure that our exception classes all work in the way that we expect. """ def test_show(self): """Establish that the show method will properly route to an alternate file. """ sio = StringIO() ex = TowerCLIError('Fe fi fo fum; I smell the blood of an Englishman.') ex.show(file=sio) sio.seek(0) self.assertIn('Fe fi fo fum;', sio.read()) ansible-tower-cli-3.2.0/tests/test_models_base.py000066400000000000000000000660551316523067200221050ustar00rootroot00000000000000# Copyright 2015, Ansible, Inc. # Luke Sneeringer # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. import json from six.moves import StringIO from tower_cli import models, resources, exceptions as exc from tower_cli.api import client from tower_cli.utils import debug from tower_cli.constants import CUR_API_VERSION from tests.compat import unittest, mock class ResourceMetaTests(unittest.TestCase): """A set of tests to establish that the ResourceMeta metaclass works in the way we expect. """ def test_commands(self): """Establish that commands are appropriately classified within the resource, and that the stock commands are not present on a BaseResource subclass. """ # Create the resource. class MyResource(models.BaseResource): endpoint = '/bogus/' @resources.command def foo(self): pass @resources.command def bar(self): pass def boring_method(self): pass # Establish that the commands are present on the resource where # we expect, and that the defined methods are still plain methods. # # Note: We can use something like types.FunctionType or # types.UnboundMethodType to test against directly, but using a # regular method is preferable because of differences between # the type internals in Python 2 vs. Python 3. # # By just getting the desirable control type from another method # on the resource, we are ensuring that it "just matches" regardless # of which version of Python is in use. self.assertIsInstance(MyResource.foo, type(MyResource.boring_method)) self.assertIsInstance(MyResource.bar, type(MyResource.boring_method)) self.assertEqual(set(MyResource.commands), set(['foo', 'bar', 'list', 'delete', 'get'])) def test_inherited_commands(self): """Establish that the stock commands are automatically present on classes inherited from Resource. """ # Create the resource. class MyResource(models.Resource): endpoint = '/bogus/' # Establish it has the commands we expect. self.assertEqual(set(MyResource.commands), set(['create', 'copy', 'modify', 'list', 'get', 'delete'])) def test_subclassed_commands(self): """Establish that commands overridden in subclasses retain their superclass implementation options. """ # Create the subclass resource, overriding a superclass command. class MyResource(models.Resource): endpoint = '/bogus/' @resources.command def list(self, **kwargs): return super(MyResource, self).list(**kwargs) # Establish that it has one of the options added to the # superclass list command. self.assertEqual(MyResource.list.__click_params__, models.Resource.list.__click_params__) def test_multiple_inheritance(self): """ Establish that click decoration from all parent class chains are preserved in a subclass. """ class MyMixin(models.Resource): endpoint = '/bogus/' def list(self, **kwargs): return super(MyMixin, self).list(**kwargs) class MyResource(MyMixin, models.Resource): endpoint = '/bogus/' def list(self, **kwargs): return super(MyResource, self).list(**kwargs) self.assertTrue(hasattr(MyResource.list, '__click_params__')) self.assertEqual(MyResource.list.__click_params__, models.Resource.list.__click_params__) def test_no_duplicate_options_from_inheritance(self): """ Test that metaclass does not duplicate options from multiple parents """ class MyMixin1(models.Resource): endpoint = '/bogus/' class MyMixin2(models.Resource): endpoint = '/boguser/' class MyResource(MyMixin1, MyMixin2): endpoint = '/boguser/' def list(self, **kwargs): return super(MyResource, self).list(**kwargs) self.assertTrue(hasattr(MyResource.list, '__click_params__')) self.assertEqual(MyResource.list.__click_params__, models.Resource.list.__click_params__) def test_fields(self): """Establish that fields are appropriately classified within the resource. """ # Create the resource. class MyResource(models.Resource): endpoint = '/bogus/' foo = models.Field(unique=True) bar = models.Field() # Establish that our fields lists are the length we expect. self.assertEqual(len(MyResource.fields), 2) self.assertEqual(len(MyResource.unique_fields), 1) # Establish that the fields are present in fields. self.assertEqual(MyResource.fields[0].name, 'foo') self.assertEqual(MyResource.fields[1].name, 'bar') self.assertEqual(MyResource.unique_fields, set(['foo'])) def test_error_no_endpoint(self): """Establish that Resource subclasses are required to have an endpoint, and attempting to create one that lacks an endpoint raises TypeError. """ with self.assertRaises(TypeError): class MyResource(models.Resource): pass def test_endpoint_normalization(self): """Establish that the endpoints have leading and trailing slashes added if they are not present on a resource. """ class MyResource(models.Resource): endpoint = 'foo' self.assertEqual(MyResource.endpoint, '/foo/') def test_disabled_property(self): """Establish that disabled_methods of derived classes disable specified attributes derived from base classes. """ class MyRes(models.Resource): endpoint = 'foo' foobar = 'baz' class MyDerivedRes(MyRes): endpoint = 'bar' disabled_methods = set(['foobar']) res = MyDerivedRes() with self.assertRaises(AttributeError): getattr(res, 'foobar') res.foobar = 'hey' self.assertEqual(res.foobar, 'hey') del res.foobar with self.assertRaises(AttributeError): getattr(res, 'foobar') class ResourceTests(unittest.TestCase): """A set of tests to establish that the Resource class works in the way that we expect. """ def setUp(self): # Create a resource class that can be used across this particular # suite. class FooResource(models.Resource): endpoint = '/foo/' name = models.Field(unique=True) description = models.Field(required=False) self.res = FooResource() def test_get(self): """Establish that the Resource class' `get` method works in the way that we expect. """ with client.test_mode as t: t.register_json('/foo/42/', {'id': 42, 'description': 'bar', 'name': 'foo'}) result = self.res.get(42) self.assertEqual(result['id'], 42) self.assertEqual(result['name'], 'foo') def test_list_no_kwargs(self): """Establish that the Resource class' `list` method correctly requests the resource and parses out a list of results. """ with client.test_mode as t: t.register_json('/foo/', {'count': 2, 'results': [ {'id': 1, 'name': 'foo', 'description': 'bar'}, {'id': 2, 'name': 'spam', 'description': 'eggs'}, ], 'next': None, 'previous': None}) result = self.res.list() self.assertEqual(t.requests[0].url, 'https://20.12.4.21/api/%s/foo/' % CUR_API_VERSION) self.assertEqual(result['count'], 2) self.assertEqual(result['results'][0]['id'], 1) def test_list_all_pages(self): """Establish that the Resource class' `list` method correctly accepts the --all-pages flag and checks follow-up pages. """ with client.test_mode as t: # Register the first, second, and third page. t.register_json('/foo/', {'count': 3, 'results': [ {'id': 1, 'name': 'foo', 'description': 'bar'}, ], 'next': '/foo/?page=2', 'previous': None}) t.register_json('/foo/?page=2', {'count': 3, 'results': [ {'id': 2, 'name': 'spam', 'description': 'eggs'}, ], 'next': '/foo/?page=3', 'previous': None}) t.register_json('/foo/?page=3', {'count': 3, 'results': [ {'id': 3, 'name': 'bacon', 'description': 'cheese'}, ], 'next': None, 'previous': None}) # Get the list result = self.res.list(all_pages=True) # Assert that there are three results, and three requests. self.assertEqual(len(t.requests), 3) self.assertEqual(len(result['results']), 3) def test_list_with_page_1_special_case(self): """Establish that the list function works even if the server gives /foo/ as the relative link for page 1. """ with client.test_mode as t: # Register the 2nd page in order to test this. t.register_json('/foo/?page=2', {'count': 2, 'results': [ {'id': 2, 'name': 'spam', 'description': 'eggs'}, ], 'next': None, 'previous': '/foo/'}) # Get the list result = self.res.list(page=2) # Check that the function knows that /foo/ is page 1 self.assertEqual(result['previous'], 1) def test_list_custom_kwargs(self): """Establish that if we pass custom keyword arguments to list, that they are included in the final request. """ with client.test_mode as t: t.register_json('/foo/?bar=baz', {'count': 0, 'results': [], 'next': None, 'previous': None}) self.res.list(query=[('bar', 'baz')]) self.assertTrue(t.requests[0].url.endswith('bar=baz')) def test_list_duplicate_kwarg(self): """Establish that if we attempt to query on the same field twice, that we get an error. """ with client.test_mode as t: with self.assertRaises(exc.BadRequest): self.res.list(name='Batman', query=[('name', 'Robin')]) self.assertEqual(len(t.requests), 0) def test_get_unexpected_zero_results(self): """Establish that if a read method gets 0 results when it should have gotten one or more, that it raises NotFound. """ with client.test_mode as t: t.register_json('/foo/?name=spam', {'count': 0, 'results': []}) with self.assertRaises(exc.NotFound): self.res.get(name='spam') def test_get_no_debug_header(self): """Establish that if get is called with include_debug_header=False, no debug header is issued. """ with mock.patch.object(type(self.res), 'read') as read: with mock.patch.object(debug, 'log') as dlog: read.return_value = {'results': [True]} result = self.res.get(42, include_debug_header=False) self.assertEqual(dlog.call_count, 0) self.assertTrue(result) def test_get_unexpected_multiple_results(self): """Establish that if a read method gets more than one result when it should have gotten one and exactly one, that it raises MultipleResults. """ # Register the response to the request URL. # Note that this response should represent bad data, since name is # generally unique within Tower. This doesn't matter for the purpose # of this test; what's important is that if we expected one and exactly # one result and we get two or more, that we complain in an expected # (and later, handled) way. with client.test_mode as t: t.register_json('/foo/?name=spam', {'count': 2, 'results': [ {'id': 1, 'name': 'spam'}, {'id': 2, 'name': 'spam'}, ], 'next': None, 'previous': None}) with self.assertRaises(exc.MultipleResults): self.res.get(name='spam') def test_list_with_none_kwargs(self): """Establish that if `list` is called with keyword arguments with None values, that these are ignored. This is to ensure that click's eagerness to send None values doesn't cause problems. """ # Register the request and make the call. with client.test_mode as t: t.register_json('/foo/?name=foo', {'count': 1, 'results': [ {'id': 1, 'name': 'foo', 'description': 'bar'}, ], 'next': None, 'previous': None}) self.res.list(name='foo', description=None) self.assertEqual(len(t.requests), 1) # Ensure that there are no other query param arguments other # than `?name=foo` in the request URL. self.assertNotIn('&', t.requests[0].url) self.assertTrue(t.requests[0].url.endswith('?name=foo')) def test_list_with_pagination(self): """Establish that the `list` method returns pages as integers if it is given pages at all. """ with client.test_mode as t: t.register_json('/foo/', {'count': 10, 'results': [ {'id': 1, 'name': 'bar'}, ], 'next': '/api/%s/foo/?page=2' % CUR_API_VERSION, 'previous': None}) result = self.res.list() self.assertEqual(result['next'], 2) def test_reading_with_file(self): """Establish that if we get a file-like object, that it is appropriately read. """ # Note: This is primarily for a case of longer input that belongs # in files (such as SSH RSA/DSA private keys), but in this case we're # using something trivial; we need only provide a proof of concept # to test against. sio = StringIO('bar') with client.test_mode as t: t.register_json('/foo/?name=bar', {'count': 0, 'results': [], 'next': None, 'previous': None}) self.res.list(name=sio) self.assertTrue(t.requests[0].url.endswith('?name=bar')) def test_create(self): """Establish that a standard create call works in the way that we expect. """ with client.test_mode as t: # `create` will attempt to see if the record already exists; # mock this to state that it does not. t.register_json('/foo/?name=bar', {'count': 0, 'results': [], 'next': None, 'previous': None}) t.register_json('/foo/', {'changed': True, 'id': 42}, method='POST') self.res.create(name='bar') self.assertEqual(t.requests[0].method, 'GET') self.assertEqual(t.requests[1].method, 'POST') def test_create_already_existing(self): """Establish that if we attempt to create a record that already exists, that no action ends up being taken. """ with client.test_mode as t: t.register_json('/foo/?name=bar', {'count': 1, 'results': [ {'id': 42, 'name': 'bar'}, ], 'next': None, 'previous': None}) result = self.res.create(name='bar') self.assertEqual(len(t.requests), 1) self.assertFalse(result['changed']) def test_create_missing_required_fields(self): """Establish that if we attempt to create a record and don't specify all required fields, that we raise BadRequest. """ # Create a resource with a required field that isn't the name # field. class BarResource(models.Resource): endpoint = '/bar/' name = models.Field(unique=True) required = models.Field() res = BarResource() # Attempt to write the resource and prove that it fails. with client.test_mode as t: t.register_json('/bar/?name=foo', {'count': 0, 'results': [], 'next': None, 'previous': None}) with self.assertRaises(exc.BadRequest): res.create(name='foo') def test_modify(self): """Establish that the modify method works in the way we expect, given a normal circumstance. """ with client.test_mode as t: t.register_json('/foo/42/', {'id': 42, 'name': 'bar', 'description': 'baz'}) t.register_json('/foo/42/', {'changed': True, 'id': 42}, method='PATCH') result = self.res.modify(42, description='spam') self.assertTrue(result['changed']) self.assertEqual(t.requests[1].body, '{"description": "spam"}') def test_modify_no_changes(self): """Establish that the modify method does not actually attempt a modification if there are no changes. """ with client.test_mode as t: t.register_json('/foo/42/', {'id': 42, 'name': 'bar', 'description': 'baz'}) result = self.res.modify(42, description='baz') self.assertFalse(result['changed']) self.assertEqual(len(t.requests), 1) def test_modify_ignore_kwargs_none(self): """Establish that we ignore keyword arguments set to None when performing writes. """ with client.test_mode as t: t.register_json('/foo/42/', {'id': 42, 'name': 'bar', 'description': 'baz'}) result = self.res.modify(42, name=None, description='baz') self.assertFalse(result['changed']) self.assertEqual(len(t.requests), 1) self.assertNotIn('name', t.requests[0].url) def test_write_file_like_object(self): """Establish that our write method, if it gets a file-like object, correctly reads it and uses the file's value as what it sends. """ sio = StringIO('bar') with client.test_mode as t: t.register_json('/foo/?name=bar', {'count': 1, 'results': [ {'id': 42, 'name': 'bar', 'description': 'baz'}, ], 'next': None, 'previous': None}) result = self.res.modify(name=sio, description='baz') self.assertFalse(result['changed']) self.assertIn('name=bar', t.requests[0].url) def test_write_with_null_field(self): """Establish that a resource with 'null' field is written.""" with client.test_mode as t: t.register_json('/foo/42/', {'id': 42, 'name': 'bar', 'description': 'baz'}, method='GET') t.register_json('/foo/42/', {'name': 'bar', 'id': 42, 'inventory': 'null'}, method='PATCH') self.res.write(42, inventory='null') self.assertEqual(json.loads(t.requests[1].body)['inventory'], None) def test_delete_with_pk(self): """Establish that calling `delete` and providing a primary key works in the way that we expect. """ with client.test_mode as t: t.register('/foo/42/', '', method='DELETE') result = self.res.delete(42) self.assertTrue(result['changed']) def test_delete_without_pk(self): """Establish that calling `delete` with keyword arguments works in the way that we expect. """ with client.test_mode as t: t.register_json('/foo/?name=bar', {'count': 1, 'results': [ {'id': 42, 'name': 'bar', 'description': 'baz'}, ], 'next': None, 'previous': None}) t.register('/foo/42/', '', method='DELETE') result = self.res.delete(name='bar') self.assertEqual(len(t.requests), 2) self.assertTrue(t.requests[1].url.endswith('/foo/42/')) self.assertTrue(result['changed']) def test_delete_with_pk_already_missing(self): """Establish that calling `delete` on a record that does not exist returns back an unchanged response. """ with client.test_mode as t: t.register_json('/foo/42/', '', method='DELETE', status_code=404) result = self.res.delete(42) self.assertFalse(result['changed']) def test_delete_with_pk_already_missing_exc(self): """Establish that calling `delete` on a record that does not exist raises an exception if requested. """ with client.test_mode as t: t.register_json('/foo/42/', '', method='DELETE', status_code=404) with self.assertRaises(exc.NotFound): self.res.delete(42, fail_on_missing=True) def test_delete_without_pk_already_missing(self): """Establish that calling `delete` on a record without a primary key correctly sends back an unchanged response. """ with client.test_mode as t: t.register_json('/foo/?name=bar', {'count': 0, 'results': []}) result = self.res.delete(name='bar') self.assertFalse(result['changed']) def test_delete_without_pk_already_missing_exc(self): """Establish that calling `delete` on a record without a primary key correctly sends back an unchanged response. """ with client.test_mode as t: t.register_json('/foo/?name=bar', {'count': 0, 'results': []}) with self.assertRaises(exc.NotFound): self.res.delete(name='bar', fail_on_missing=True) def test_assoc_already_present(self): """Establish that the _assoc method returns an unchanged status message if it attempts to associate two records that are already associated. """ with client.test_mode as t: t.register_json('/foo/42/bar/?id=84', {'count': 1, 'results': [ {'id': 84}, ], 'next': None, 'previous': None}) result = self.res._assoc('bar', 42, 84) self.assertFalse(result['changed']) def test_assoc_not_already_present(self): """Establish that the _assoc method returns an changed status message and associates objects if appropriate. """ with client.test_mode as t: t.register_json('/foo/42/bar/?id=84', {'count': 0, 'results': []}) t.register_json('/foo/42/bar/', {}, method='POST') result = self.res._assoc('bar', 42, 84) self.assertEqual(json.loads(t.requests[1].body), {'associate': True, 'id': 84}) self.assertTrue(result['changed']) def test_disassoc_not_already_present(self): """Establish that the _disassoc method returns an unchanged status message if it attempts to associate two records that are not associated. """ with client.test_mode as t: t.register_json('/foo/42/bar/?id=84', {'count': 0, 'results': []}) result = self.res._disassoc('bar', 42, 84) self.assertFalse(result['changed']) def test_disassoc_already_present(self): """Establish that the _assoc method returns an changed status message and associates objects if appropriate. """ with client.test_mode as t: t.register_json('/foo/42/bar/?id=84', {'count': 1, 'results': [ {'id': 84}, ], 'next': None, 'previous': None}) t.register_json('/foo/42/bar/', {}, method='POST') result = self.res._disassoc('bar', 42, 84) self.assertEqual(json.loads(t.requests[1].body), {'disassociate': True, 'id': 84}) self.assertTrue(result['changed']) def test_lookup_with_unique_field_not_present(self): """Establish that a if _lookup is invoked without any unique field specified, that BadRequest is raised. """ with client.test_mode: with self.assertRaises(exc.BadRequest): self.res._lookup(description='abcd') def test_lookup_errant_found(self): """Establish that if _lookup is invoked and finds a record when it should not, that an appropriate exception is raised. """ with client.test_mode as t: t.register_json('/foo/?name=bar', {'count': 1, 'results': [ {'id': 42, 'name': 'bar'}, ], 'next': None, 'previous': None}) with self.assertRaises(exc.Found): self.res._lookup(name='bar', fail_on_found=True) class MonitorableResourcesTests(unittest.TestCase): """Estblaish that the MonitorableResource abstract class works in the way that we expect. """ def test_status_not_implemented(self): """Establish that the abstract MonitorableResource's status method raises NotImplementedError. """ with self.assertRaises(NotImplementedError): models.MonitorableResource().status(None) class SurveyResourceTests(unittest.TestCase): """Test methods specific to survey models.""" def setUp(self): self.res = models.SurveyResource() self.res.endpoint = '/job_templates/' def test_survey_no_op(self): with mock.patch.object(models.base.BaseResource, 'write') as w: self.res.modify(name='foobar') w.assert_called_once_with( create_on_missing=False, force_on_exists=True, name='foobar', pk=None) def test_survey_create(self): with mock.patch.object(models.base.BaseResource, 'write') as w: w.return_value = {'id': 42, 'survey_enabled': True} survey_data = {'foobar': 'foo'} with client.test_mode as t: t.register_json( '/job_templates/42/survey_spec/', {}, method='POST' ) self.res.modify(survey_spec=survey_data, verbose=True) self.assertEqual(t.requests[0].body, json.dumps(survey_data)) def test_survey_delete(self): with mock.patch.object(models.base.BaseResource, 'write') as w: w.return_value = {'id': 42, 'survey_enabled': True} with client.test_mode as t: t.register_json( '/job_templates/42/survey_spec/', {}, method='DELETE' ) self.res.modify(survey_spec={}, verbose=True) self.assertEqual(t.requests[0].method, 'DELETE') ansible-tower-cli-3.2.0/tests/test_models_fields.py000066400000000000000000000057511316523067200224350ustar00rootroot00000000000000# Copyright 2015, Ansible, Inc. # Luke Sneeringer # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. from tower_cli import models from tests.compat import unittest class FieldTests(unittest.TestCase): """A set of tests to establish that the base Field class works in the way we expect. """ def test_dunder_lt(self): """Establish that the `__lt__` comparison method on fields works as expected. """ f1 = models.Field() f2 = models.Field() self.assertTrue(f1 < f2) def test_dunder_gt(self): """Establish that the `__gt__` comparison method on fields works in the way we expect. """ f1 = models.Field() f2 = models.Field() self.assertTrue(f2 > f1) def test_help_property_explicit(self): """Establish that an explicitly provided help text is preserved as the field's help. """ f1 = models.Field(help_text='foo bar baz') self.assertEqual(f1.help, 'foo bar baz') def test_help_property_implicit(self): """Establish that a sane implicit help text is provided if none is specified. """ f1 = models.Field() f1.name = 'f1' self.assertEqual(f1.help, 'The f1 field.') def test_flags_standard(self): """Establish that the `flags` property returns what I expect for a run-of-the-mill field. """ f1 = models.Field() self.assertEqual(f1.flags, ['str']) def test_flags_unique_unfilterable(self): """Establish that the `flags` property successfully flags unfilterable and unique flags. """ f1 = models.Field(unique=True, filterable=False) self.assertIn('unique', f1.flags) self.assertIn('not filterable', f1.flags) def test_flags_read_only(self): """Establish that the `flags` property successfully flags read-only flags. """ f = models.Field(read_only=True) self.assertEqual(f.flags, ['str', 'read-only']) def test_flags_not_required(self): """Establish that the `flags` property successfully flags a not-required field. """ f = models.Field(type=int, required=False) self.assertEqual(f.flags, ['int', 'not required']) def test_flags_type(self): """Establish that the flags property successfully shows the correct type name. """ f = models.Field(type=bool) self.assertEqual(f.flags, ['bool']) ansible-tower-cli-3.2.0/tests/test_models_unified_jobs.py000066400000000000000000000077001316523067200236230ustar00rootroot00000000000000# Copyright 2016, Ansible by Red Hat # Alan Rominger # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. import time import click import tower_cli from tower_cli.api import client from tests.compat import unittest, mock def project_update_registration(t): t.register_json('/project_updates/54/', { 'elapsed': 1335024000.0, 'failed': False, 'status': 'successful', }, method='GET') class StandardOutTests(unittest.TestCase): """ Test that standard out lookup and wrapper methods """ def setUp(self): # Representative of a Unified Job Template self.res = tower_cli.get_resource('project') # Representative of a Unified Job self.job_res = tower_cli.get_resource('job') def test_lookup_stdout(self): "Test the method that makes a call to get standard out." with client.test_mode as t: # note: # 'foobar' = 'Zm9vYmFy' via # base64.standard_b64encode('foobar'.encode('ascii')) # but python versions can't all agree on how to trim the string t.register_json( '/project_updates/42/stdout/', {'content': 'Zm9vYmFy'}, method='GET') stdout = self.res.lookup_stdout(42, start_line=0, end_line=1) assert 'foobar' in str(stdout) def test_stdout(self): "Test that printing standard out works with project-like things." with mock.patch.object(type(self.res), 'lookup_stdout') as lookup: with mock.patch.object(type(self.res), 'last_job_data') as job: with mock.patch.object(click, 'echo') as mock_echo: job.return_value = {'id': 42} lookup.return_value = 'foobar' result = self.res.stdout(42) assert not result['changed'] mock_echo.assert_called_once_with('foobar', nl=1) def test_stdout_with_lookup(self): "Test that unified job will be automatically looked up." with mock.patch.object(type(self.job_res), 'lookup_stdout'): with mock.patch.object(type(self.job_res), 'get') as get: with mock.patch.object(click, 'echo'): self.job_res.stdout(pk=None, name="test-proj") get.assert_called_once_with(name="test-proj") def test_call_wait_with_parent(self): "Test auto-lookup of last job is called for wait" with client.test_mode as t: project_update_registration(t) with mock.patch.object(type(self.res), 'last_job_data') as job: job.return_value = {'id': 54} with mock.patch.object(click, 'echo'): with mock.patch.object(time, 'sleep'): self.res.wait(pk=None, parent_pk=42) job.assert_called_once_with(42) def test_call_monitor_with_parent(self): "Test auto-lookup when the monitor method is called" with client.test_mode as t: project_update_registration(t) with mock.patch.object(type(self.res), 'last_job_data') as job: job.return_value = {'id': 54} with mock.patch.object(click, 'echo'): with mock.patch.object(type(self.res), 'wait'): with mock.patch.object(time, 'sleep'): self.res.monitor(pk=None, parent_pk=42) job.assert_called_once_with(42) ansible-tower-cli-3.2.0/tests/test_resources_ad_hoc.py000066400000000000000000000201201316523067200231160ustar00rootroot00000000000000# Copyright 2015, Ansible, Inc. # Alan Rominger # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. import tower_cli from tower_cli.api import client from tower_cli import exceptions as exc from tower_cli.cli.resource import ResSubcommand from tower_cli.constants import CUR_API_VERSION from tests.compat import unittest, mock from tower_cli.conf import settings import json import click class LaunchTests(unittest.TestCase): """A set of tests for ensuring that the ad hoc resource's launch command works in the way we expect. """ def setUp(self): self.res = tower_cli.get_resource('ad_hoc') def test_basic_launch(self): """Establish that we are able to run an ad hoc command. """ with client.test_mode as t: t.register_json('/ad_hoc_commands/42/', {'id': 42}, method='GET') t.register_json('/', { 'ad_hoc_commands': '/api/%s/ad_hoc_commands/' % CUR_API_VERSION }, method='GET') t.register_json('/ad_hoc_commands/', {'id': 42}, method='POST') result = self.res.launch(inventory="foobar", machine_credential=2) self.assertEqual(result, {'changed': True, 'id': 42}) def test_launch_with_become(self): """Establish that we are able use the --become-enabled flag """ with client.test_mode as t: t.register_json('/ad_hoc_commands/42/', {'id': 42}, method='GET') t.register_json('/', { 'ad_hoc_commands': '/api/%s/ad_hoc_commands/' % CUR_API_VERSION }, method='GET') t.register_json('/ad_hoc_commands/', {'id': 42}, method='POST') self.res.launch(inventory="foobar", machine_credential=2, become_enabled=True) self.assertDictContainsSubset( {'become_enabled': True}, json.loads(t.requests[1].body) ) def test_basic_launch_with_echo(self): """Establish that we are able to run an ad hoc command and also print that to the command line without errors. """ with client.test_mode as t: t.register_json('/ad_hoc_commands/42/', {'id': 42}, method='GET') t.register_json('/', { 'ad_hoc_commands': '/api/%s/ad_hoc_commands/' % CUR_API_VERSION }, method='GET') t.register_json( '/ad_hoc_commands/', {'changed': True, 'id': 42, 'inventory': 'foobar', 'credential': 2, 'name': 'ping', 'created': 1234, 'elapsed': 2352, 'status': 'successful', 'module_name': 'command', 'limit': '', }, method='POST' ) result = self.res.launch(inventory="foobar", machine_credential=2) self.assertEqual(result['changed'], True) self.assertEqual(result['id'], 42) f = ResSubcommand(self.res)._echo_method(self.res.launch) with mock.patch.object(click, 'secho'): with settings.runtime_values(format='human'): f(inventory="foobar", machine_credential=2) def test_launch_with_param(self): """Establish that we are able to run an ad hoc command. """ with client.test_mode as t: t.register_json('/ad_hoc_commands/42/', {'id': 42}, method='GET') t.register_json('/', { 'ad_hoc_commands': '/api/%s/ad_hoc_commands/' % CUR_API_VERSION }, method='GET') t.register_json('/ad_hoc_commands/', {'id': 42}, method='POST') result = self.res.launch(inventory="foobar", machine_credential=2, module_args="echo 'hi'") self.assertEqual(result, {'changed': True, 'id': 42}) def test_version_failure(self): """Establish that if the command has failed, that we raise the JobFailure exception. """ with client.test_mode as t: t.register_json('/ad_hoc_commands/42/', {'id': 42}, method='GET') t.register_json('/', {}, method='GET') t.register_json('/ad_hoc_commands/', {'id': 42}, method='POST') with self.assertRaises(exc.TowerCLIError): self.res.launch(inventory=1, machine_credential=2, module_args="echo 'hi'") def test_basic_launch_monitor_option(self): """Establish that we are able to run a command and monitor it, if requested. """ with client.test_mode as t: t.register_json('/ad_hoc_commands/42/', {'id': 42}, method='GET') t.register_json('/', { 'ad_hoc_commands': '/api/%s/ad_hoc_commands/' % CUR_API_VERSION }, method='GET') t.register_json('/ad_hoc_commands/', {'id': 42}, method='POST') with mock.patch.object(type(self.res), 'monitor') as monitor: self.res.launch(inventory=1, machine_credential=2, module_args="echo 'hi'", monitor=True) monitor.assert_called_once_with(42, timeout=None) class StatusTests(unittest.TestCase): """A set of tests to establish that the ad hoc job status command works in the way that we expect. """ def setUp(self): self.res = tower_cli.get_resource('ad_hoc') def test_normal(self): """Establish that the data about an ad hoc command retrieved from the jobs endpoint is provided. """ with client.test_mode as t: t.register_json('/ad_hoc_commands/42/', { 'elapsed': 1335024000.0, 'extra': 'ignored', 'failed': False, 'status': 'successful', }) result = self.res.status(42) self.assertEqual(result, { 'elapsed': 1335024000.0, 'failed': False, 'status': 'successful', }) self.assertEqual(len(t.requests), 1) def test_detailed(self): with client.test_mode as t: t.register_json('/ad_hoc_commands/42/', { 'elapsed': 1335024000.0, 'extra': 'ignored', 'failed': False, 'status': 'successful', }) result = self.res.status(42, detail=True) self.assertEqual(result, { 'elapsed': 1335024000.0, 'extra': 'ignored', 'failed': False, 'status': 'successful', }) self.assertEqual(len(t.requests), 1) class CancelTests(unittest.TestCase): """A set of tasks to establish that the ad hoc cancel command works in the way that we expect. """ def setUp(self): self.res = tower_cli.get_resource('ad_hoc') def test_standard_cancelation(self): """Establish that a standard cancelation command works in the way we expect. """ with client.test_mode as t: t.register('/ad_hoc_commands/42/cancel/', '', method='POST') result = self.res.cancel(42) self.assertTrue( t.requests[0].url.endswith('/ad_hoc_commands/42/cancel/') ) self.assertTrue(result['changed']) def test_cancelation_completed_with_error(self): """Establish that a standard cancelation command works in the way we expect. """ with client.test_mode as t: t.register('/ad_hoc_commands/42/cancel/', '', method='POST', status_code=405) with self.assertRaises(exc.TowerCLIError): self.res.cancel(42, fail_if_not_running=True) ansible-tower-cli-3.2.0/tests/test_resources_credential.py000066400000000000000000000101311316523067200240140ustar00rootroot00000000000000# Copyright 2015, Red Hat, Inc. # Alan Rominger # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. import tower_cli from tower_cli.api import client from tests.compat import unittest, mock class CredentialTests(unittest.TestCase): """A set of tests for ensuring that the credential resource's create command works in the way we expect. """ def setUp(self): self.res = tower_cli.get_resource('credential') def test_create_without_special_fields(self): """Establish that a create without user, team, or credential works""" with mock.patch('tower_cli.models.base.Resource.create') as mock_create: cred_res = tower_cli.get_resource('credential') cred_res.create(name="foobar") mock_create.assert_called_once_with(name="foobar") def test_create_with_special_fields_old(self): """Establish that creating with special fields passes through as-is if the API does not support this use mode.""" with client.test_mode as t: t.register_json('/credentials/', {'actions': {'POST': {}}}, method='OPTIONS') with mock.patch('tower_cli.models.base.Resource.create') as mock_create: cred_res = tower_cli.get_resource('credential') cred_res.create(name="foobar", organization="Foo Ops") mock_create.assert_called_once_with(name="foobar", organization="Foo Ops") def test_create_with_special_fields_new(self): """Establish that creating with special fields uses special no_lookup tool if given special fields and the API supports that use case.""" with client.test_mode as t: t.register_json('/credentials/', {'actions': {'POST': {'organization': 'information'}}}, method='OPTIONS') with mock.patch('tower_cli.models.base.Resource.create') as mock_create: cred_res = tower_cli.get_resource('credential') cred_res.create(name="foobar", organization="Foo Ops") mock_create.assert_called_once_with(name="foobar", organization="Foo Ops") self.assertTrue(cred_res.fields[2].no_lookup) def test_create_with_special_fields_new_functional(self): """Establish that the correct GET data is used with the new method for creating credentials.""" with client.test_mode as t: t.register_json('/credentials/', {'actions': {'POST': {'organization': 'information'}}}, method='OPTIONS') t.register_json('/credentials/', {'count': 0, 'results': [], 'next': None, 'previous': None}, method='GET') t.register_json('/credentials/', {'count': 0, 'results': [], 'next': None, 'previous': None}, method='GET') t.register_json('/credentials/', {'id': 42}, method='POST') cred_res = tower_cli.get_resource('credential') cred_res.create(name="foobar", user=1, credential_type=1) self.assertTrue(cred_res.fields[2].no_lookup) self.assertTrue(cred_res.fields[3].no_lookup) # Verify request data is correct self.assertEqual(len(t.requests), 2) self.assertEqual(t.requests[0].method, 'GET') self.assertEqual(t.requests[1].method, 'POST') self.assertIn('name=foobar', t.requests[0].url) # Make sure special fields not used for GET self.assertTrue('user' not in t.requests[0].url) # Make sure special files are used in actual POST self.assertIn('user', t.requests[1].body) ansible-tower-cli-3.2.0/tests/test_resources_group.py000066400000000000000000000144721316523067200230520ustar00rootroot00000000000000# Copyright 2015, Ansible, Inc. # Luke Sneeringer # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. import tower_cli from tower_cli import models, exceptions as exc from tower_cli.api import client from tests.compat import unittest, mock class GroupTests(unittest.TestCase): """A set of tests to establish that the group resource functions in the way that we expect. """ def setUp(self): self.gr = tower_cli.get_resource('group') def test_set_child_endpoint_id(self): """Test that we can change the endpoint to list children of another group - given the id of parent. """ with client.test_mode as t: t.register_json('/groups/1/', {'id': 1, 'inventory': 1, 'name': 'Foo'}, method='GET') self.gr.set_child_endpoint(1) self.assertEqual(self.gr.endpoint, '/groups/1/children/') def test_set_child_endpoint_name(self): """Test that we can change the endpoint to list children of another group - given the name of parent. """ with client.test_mode as t: t.register_json('/groups/?name=Foo', { 'count': 1, 'results': [{ 'id': 1, 'inventory': 1, 'name': 'Foo', }], 'next': None, 'previous': None, }, method='GET') self.gr.set_child_endpoint("Foo") self.assertEqual(self.gr.endpoint, '/groups/1/children/') def test_create_no_change(self): """Establish that if we try to create a group that already exists, that we return the standard changed: false. """ with mock.patch.object(models.Resource, 'create') as super_create: super_create.return_value = {'changed': False, 'id': 1} with client.test_mode as t: answer = self.gr.create(name='Foo', inventory=1) self.assertEqual(len(t.requests), 0) # This also establishes that the "credential" argument above # was dropped at the superclass method (as it should be). super_create.assert_called_once_with(fail_on_found=False, force_on_exists=False, name='Foo', inventory=1) self.assertFalse(answer['changed']) def test_create_as_child(self): """Establish that if we start the creation process of a child group """ with mock.patch.object(models.Resource, 'create') as super_create: super_create.return_value = {'changed': False, 'id': 1} with mock.patch.object(models.Resource, 'get') as super_get: super_get.return_value = {'id': 2, 'inventory': 1} with client.test_mode as t: answer = self.gr.create(name='Foo', parent=2) self.assertEqual(len(t.requests), 0) super_get.assert_called_once_with(2) super_create.assert_called_once_with( fail_on_found=False, force_on_exists=False, name='Foo', inventory=1, parent=2 ) self.assertFalse(answer['changed']) def test_create_no_inventory_error(self): """Establish that error is thrown when no group/inventory given.""" with self.assertRaises(exc.UsageError): self.gr.create(1) def test_root_and_no_inventory(self): """Establish that if we try to get root groups without specifying an inventory, that we get a usage error. """ with self.assertRaises(exc.UsageError): self.gr.list(root=True, inventory=None) def test_list_root(self): """Establish that getting root groups from the Tower API works in the way that we expect. """ with client.test_mode as t: t.register_json('/inventories/1/root_groups/', { 'count': 2, 'results': [ {'id': 1, 'name': 'Foo', 'inventory': 1}, {'id': 2, 'name': 'Bar', 'inventory': 2}, ], 'next': None, 'previous': None, }) result = self.gr.list(root=True, inventory=1) self.assertEqual(result['results'], [ {'id': 1, 'name': 'Foo', 'inventory': 1}, {'id': 2, 'name': 'Bar', 'inventory': 2}, ]) def test_list_normal_situation(self): """Test that anything not covered by the subclass implementation simply calls the superclass implementation. """ with mock.patch.object(models.Resource, 'list') as super_list: self.gr.list(root=False) super_list.assert_called_once_with() def test_list_under_parent(self): """Establish that listing with a parent specified works.""" with mock.patch('tower_cli.models.base.BaseResource.list') as mock_list: with mock.patch('tower_cli.resources.group.Resource.lookup_with_inventory'): self.gr.list(parent="foo_group") mock_list.assert_called_once_with() def test_associate(self): """Establish that associate commands work.""" with mock.patch('tower_cli.models.base.BaseResource._assoc') as mock_assoc: with mock.patch('tower_cli.resources.group.Resource.lookup_with_inventory') as mock_lookup: mock_lookup.return_value = {'id': 1} self.gr.associate(group=1, parent=2) mock_assoc.assert_called_once_with('children', 1, 1) def test_disassociate(self): """Establish that associate commands work.""" with mock.patch('tower_cli.models.base.BaseResource._disassoc') as mock_assoc: with mock.patch('tower_cli.resources.group.Resource.lookup_with_inventory') as mock_lookup: mock_lookup.return_value = {'id': 1} self.gr.disassociate(group=1, parent=2) mock_assoc.assert_called_once_with('children', 1, 1) ansible-tower-cli-3.2.0/tests/test_resources_host.py000066400000000000000000000071161316523067200226700ustar00rootroot00000000000000# Copyright 2015, Ansible, Inc. # Luke Sneeringer # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. import json import tower_cli from tower_cli.api import client from tests.compat import unittest, mock class HostTests(unittest.TestCase): """Establish that the host resource methods work in the way that we expect. """ def setUp(self): self.host_resource = tower_cli.get_resource('host') def test_associate(self): """Establish that the associate method makes the HTTP requests that we expect. """ with client.test_mode as t: t.register_json('/hosts/42/groups/?id=84', {'count': 0, 'results': []}) t.register_json('/hosts/42/groups/', {}, method='POST') self.host_resource.associate(42, 84) self.assertEqual(t.requests[1].body, json.dumps({'associate': True, 'id': 84})) def test_disassociate(self): """Establish that the associate method makes the HTTP requests that we expect. """ with client.test_mode as t: t.register_json('/hosts/42/groups/?id=84', {'count': 1, 'results': [{'id': 84}], 'next': None, 'previous': None}) t.register_json('/hosts/42/groups/', {}, method='POST') self.host_resource.disassociate(42, 84) self.assertEqual(t.requests[1].body, json.dumps({'disassociate': True, 'id': 84})) def test_list_under_group(self): """Establish that a group flag is converted into query string.""" with mock.patch('tower_cli.models.base.BaseResource.list') as mock_list: self.host_resource.list(group=78) mock_list.assert_called_once_with(query=(('groups__in', 78),)) def test_list_by_host_filter(self): """Establish that a host filter option is converted into query string.""" with mock.patch('tower_cli.models.base.BaseResource.list') as mock_list: self.host_resource.list(host_filter='foobar') mock_list.assert_called_once_with(query=(('host_filter', 'foobar'),)) def test_normal_list(self): """Establish that the group flag doesn't break the normal list.""" with mock.patch('tower_cli.models.base.BaseResource.list') as mock_list: self.host_resource.list(name="foobar") mock_list.assert_called_once_with(name="foobar") def test_list_facts(self): """Establish that the list_facts command runs as it is supposed.""" with client.test_mode as t: t.register_json('/hosts/42/', {'id': 42}) t.register_json('/hosts/42/ansible_facts/', {'foo': 'bar'}) self.assertEqual(self.host_resource.list_facts(pk=42), {'foo': 'bar'}) def test_insights(self): """Establish that the insights command runs as it is supposed.""" with client.test_mode as t: t.register_json('/hosts/42/', {'id': 42}) t.register_json('/hosts/42/insights/', {'foo': 'bar'}) self.assertEqual(self.host_resource.insights(pk=42), {'foo': 'bar'}) ansible-tower-cli-3.2.0/tests/test_resources_inventory.py000066400000000000000000000021331316523067200237420ustar00rootroot00000000000000# Copyright 2017, Ansible by Red Hat # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. import tower_cli from tower_cli.api import client from tests.compat import unittest class InventoryTests(unittest.TestCase): def setUp(self): self.inv_resource = tower_cli.get_resource('inventory') def test_batch_update(self): with client.test_mode as t: t.register_json('/inventories/42/', {'id': 42}) t.register_json('/inventories/42/update_inventory_sources/', ['foo', 'bar'], method='POST') self.assertEqual(self.inv_resource.batch_update(pk=42), ['foo', 'bar']) ansible-tower-cli-3.2.0/tests/test_resources_inventory_source.py000066400000000000000000000145131316523067200253270ustar00rootroot00000000000000# Copyright 2015, Ansible, Inc. # Luke Sneeringer # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. import tower_cli from tower_cli.api import client from tower_cli import exceptions as exc from tower_cli.constants import CUR_API_VERSION from tests.compat import unittest, mock class UpdateTests(unittest.TestCase): """A set of tests to establish that the inventory source resource works in the way that we expect. """ def setUp(self): self.isr = tower_cli.get_resource('inventory_source') def test_cannot_sync(self): """Establish that if we attempt to update an inventory source that cannot be updated, that we raise BadRequest. """ with client.test_mode as t: t.register_json('/inventory_sources/1/update/', {'can_update': False}, method='GET') with self.assertRaises(exc.BadRequest): self.isr.update(1) def test_update(self): """Establish that if we are able to update an inventory source, that the update command does so. """ with client.test_mode as t: t.register_json('/inventory_sources/1/update/', {'can_update': True}, method='GET') t.register_json('/inventory_sources/1/update/', {'inventory_update': 42}, method='POST') answer = self.isr.update(1) self.assertEqual(answer['status'], 'ok') def test_update_with_monitor(self): """Establish that if we call update with the monitor flag, that the monitor method runs. """ with client.test_mode as t: t.register_json('/inventory_sources/1/update/', {'can_update': True}, method='GET') t.register_json('/inventory_sources/1/update/', {'inventory_update': 32}, method='POST') t.register_json('/inventory_sources/1/', {'inventory': 1}, method='GET') with mock.patch.object(type(self.isr), 'monitor') as monitor: self.isr.update(1, monitor=True) monitor.assert_called_once_with(32, parent_pk=1, timeout=None) # Check wait method, following same pattern with mock.patch.object(type(self.isr), 'wait') as wait: self.isr.update(1, wait=True) wait.assert_called_once_with(32, parent_pk=1, timeout=None) class StatusTests(unittest.TestCase): """A set of tests to establish that the inventory_source status command works in the way that we expect. """ def setUp(self): self.res = tower_cli.get_resource('inventory_source') self.detail_uri = '/inventory_sources/1/inventory_updates/42/' def test_normal(self): """Establish that the data about a project update retrieved from the project updates endpoint is provided. """ with client.test_mode as t: t.register_json('/inventory_sources/1/', { 'id': 1, 'related': {'last_update': '/api/%s%s' % (CUR_API_VERSION, self.detail_uri)}, }) t.register_json(self.detail_uri, { 'elapsed': 1335024000.0, 'extra': 'ignored', 'failed': False, 'status': 'successful', }) result = self.res.status(1) self.assertEqual(result, { 'elapsed': 1335024000.0, 'failed': False, 'status': 'successful', }) self.assertEqual(len(t.requests), 2) def test_detailed(self): """Establish that a detailed request is sent back in the way that we expect. """ with client.test_mode as t: t.register_json('/inventory_sources/1/', { 'id': 1, 'related': {'last_update': '/api/%s%s' % (CUR_API_VERSION, self.detail_uri)}, }) t.register_json(self.detail_uri, { 'elapsed': 1335024000.0, 'extra': 'ignored', 'failed': False, 'status': 'successful', }) result = self.res.status(1, detail=True) self.assertEqual(result, { 'elapsed': 1335024000.0, 'extra': 'ignored', 'failed': False, 'status': 'successful', }) self.assertEqual(len(t.requests), 2) def test_currently_running_update(self): """Establish that if an update is currently running, that we see this and send back the appropriate status. """ with client.test_mode as t: t.register_json('/inventory_sources/1/', { 'id': 1, 'related': { 'current_update': '/api/%s%s' % (CUR_API_VERSION, self.detail_uri), 'last_update': '/api/%s%s' % (CUR_API_VERSION, self.detail_uri.replace('42', '41')), }, }) t.register_json(self.detail_uri, { 'elapsed': 1335024000.0, 'extra': 'ignored', 'failed': False, 'status': 'running', }) result = self.res.status(1) self.assertEqual(result, { 'elapsed': 1335024000.0, 'failed': False, 'status': 'running', }) self.assertEqual(len(t.requests), 2) def test_no_updates(self): """Establish that running `status` against a project with no updates raises the error we expect. """ with client.test_mode as t: t.register_json('/inventory_sources/1/', { 'id': 1, 'related': {}, }) with self.assertRaises(exc.NotFound): self.res.status(1) ansible-tower-cli-3.2.0/tests/test_resources_job.py000066400000000000000000000620171316523067200224660ustar00rootroot00000000000000# Copyright 2015, Ansible, Inc. # Luke Sneeringer # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. import json import yaml import time from copy import copy import click import tower_cli from tower_cli.api import client from tower_cli import exceptions as exc from tower_cli.cli.resource import ResSubcommand from tests.compat import unittest, mock from tower_cli.conf import settings # Standard functions used for space and readability # these operate on the test client, t def register_get(t): """ After starting job, the launch method may grab info about the job just launched from this endpoint """ t.register_json('/jobs/42/', { 'id': 42, 'job_template': 1, 'status': 'pending', 'created': 1234, 'elapsed': 0.0, }, method='GET') def standard_registration(t, **kwargs): """ Endpoints common to launching any job with template #1 and is automatically assigned to job #42. kwargs is used to provide extra return fields of job launch""" # A GET to the template endpoint is made to find the extra_vars to combine t.register_json('/job_templates/1/', { 'id': 1, 'name': 'frobnicate', 'related': {'launch': '/job_templates/1/launch/'}, }) register_get(t) # A GET to the launch endpoint is needed to check if # a password prompt is needed t.register_json('/job_templates/1/launch/', {}, method='GET') # A POST to the launch endpoint will launch a job, and we # expect that the tower server will return the job number data = {'id': 42} data.update(kwargs) t.register_json('/job_templates/1/launch/', data, method='POST') def jt_vars_registration(t, extra_vars): """ Endpoints that are needed to get information from job template. This particular combination also entails 1) version of Tower - 2.2.0 2) successful job launch, id=42 3) prompts user for variables on launch """ t.register_json('/job_templates/1/', { 'ask_variables_on_launch': True, 'extra_vars': extra_vars, 'id': 1, 'name': 'frobnicate', 'related': {'launch': '/job_templates/1/launch/'}, }) register_get(t) t.register_json('/config/', {'version': '2.2.0'}, method='GET') t.register_json('/job_templates/1/launch/', {}, method='GET') t.register_json('/job_templates/1/launch/', {'id': 42}, method='POST') class LaunchTests(unittest.TestCase): """A set of tests for ensuring that the job resource's launch command works in the way we expect. """ def setUp(self): self.res = tower_cli.get_resource('job') def test_basic_launch(self): """Establish that we are able to create a job that doesn't require any invocation-time input. """ with client.test_mode as t: standard_registration(t) result = self.res.launch(1) self.assertDictContainsSubset({'changed': True, 'id': 42}, result) def test_basic_launch_with_echo(self): """Establish that we are able to create a job and echo the output to the command line without it breaking. """ with client.test_mode as t: standard_registration(t) result = self.res.launch(1) self.assertDictContainsSubset({'changed': True, 'id': 42}, result) f = ResSubcommand(self.res)._echo_method(self.res.launch) with mock.patch.object(click, 'secho'): with settings.runtime_values(format='human'): f(job_template=1) def test_launch_w_tags(self): """Establish that we are able to create a job and attach tags to it. """ with client.test_mode as t: standard_registration(t) self.res.launch(1, tags="a, b, c") self.assertEqual( json.loads(t.requests[2].body)['job_tags'], 'a, b, c', ) def test_launch_w_tuple_extra_vars(self): """Establish that if the click library gives a tuple, than the job will run normally. """ with client.test_mode as t: standard_registration(t) result = self.res.launch(1, extra_vars=()) self.assertDictContainsSubset({'changed': True, 'id': 42}, result) def test_basic_launch_wait_option(self): """Establish that we are able to create a job that doesn't require any invocation-time input, and that wait is called if requested. """ with client.test_mode as t: standard_registration(t) with mock.patch.object(type(self.res), 'wait') as wait: self.res.launch(1, wait=True) wait.assert_called_once_with(42, timeout=None) def test_extra_vars_at_runtime(self): """Establish that if we should be asking for extra variables at runtime, that we do. """ with client.test_mode as t: # test with JSON job template extra_vars jt_vars_registration(t, '{"spam": "eggs"}') with mock.patch.object(click, 'edit') as edit: edit.return_value = '# Nothing.\nfoo: bar' result = self.res.launch(1, no_input=False) self.assertDictContainsSubset( {"spam": "eggs"}, yaml.load(edit.mock_calls[0][1][0]) ) self.assertDictContainsSubset( {'foo': 'bar'}, json.loads(json.loads(t.requests[2].body)['extra_vars']) ) self.assertDictContainsSubset({'changed': True, 'id': 42}, result) def test_extra_vars_at_runtime_YAML_JT(self): """Establish that if we should be asking for extra variables at runtime, that we do. """ with client.test_mode as t: # test with YAML and comments jt_vars_registration(t, 'spam: eggs\n# comment') with mock.patch.object(click, 'edit') as edit: edit.return_value = '# Nothing.\nfoo: bar' self.res.launch(1, no_input=False) self.assertIn('# comment', edit.mock_calls[0][1][0]) self.assertDictContainsSubset( {"spam": "eggs"}, yaml.load(edit.mock_calls[0][1][0]) ) def test_extra_vars_at_runtime_no_user_data(self): """User launches a job that prompts for variables. User closes editor without adding any text. Establish that we launch the job as-is. """ with client.test_mode as t: # No job template variables jt_vars_registration(t, '') initial = '\n'.join(( '# Specify extra variables (if any) here as YAML.', '# Lines beginning with "#" denote comments.', '', )) with mock.patch.object(click, 'edit') as edit: edit.return_value = initial self.res.launch(1, no_input=False) self.assertEqual(t.requests[2].method, 'POST') self.assertEqual(t.requests[2].body, '{}') def test_job_template_variables_post_24(self): """ Check that in Tower versions past 2.4, it does not include job template variables along with the rest """ with client.test_mode as t: jt_vars_registration(t, 'spam: eggs') t.register_json('/config/', {'version': '2.4'}, method='GET') result = self.res.launch(1, extra_vars=['foo: bar']) response_json = yaml.load(t.requests[2].body) ev_json = yaml.load(response_json['extra_vars']) self.assertTrue('foo' in ev_json) self.assertTrue('spam' not in ev_json) self.assertDictContainsSubset({'changed': True, 'id': 42}, result) def test_extra_vars_at_call_time(self): """Establish that extra variables specified at call time are appropriately specified. """ with client.test_mode as t: t.register_json('/job_templates/1/', { 'id': 1, 'name': 'frobnicate', 'related': {'launch': '/job_templates/1/launch/'}, }) register_get(t) t.register_json('/job_templates/1/launch/', {}, method='GET') t.register_json('/job_templates/1/launch/', {'id': 42}, method='POST') result = self.res.launch(1, extra_vars=['foo: bar']) self.assertDictContainsSubset( {'foo': 'bar'}, json.loads(json.loads(t.requests[2].body)['extra_vars']) ) self.assertDictContainsSubset({'changed': True, 'id': 42}, result) def test_extra_vars_file_at_call_time(self): """Establish that extra variables specified at call time as a file are appropriately specified. """ with client.test_mode as t: t.register_json('/job_templates/1/', { 'id': 1, 'name': 'frobnicate', 'related': {'launch': '/job_templates/1/launch/'}, }) register_get(t) t.register_json('/job_templates/1/launch/', {}, method='GET') t.register_json('/job_templates/1/launch/', {'id': 42}, method='POST') result = self.res.launch(1, extra_vars=['foo: bar']) self.assertDictContainsSubset( {'foo': 'bar'}, json.loads(json.loads(t.requests[2].body)['extra_vars']) ) self.assertDictContainsSubset({'changed': True, 'id': 42}, result) def test_passwords_needed_at_start(self): """Establish that we are able to create a job that doesn't require any invocation-time input. """ with client.test_mode as t: t.register_json('/job_templates/1/', { 'id': 1, 'name': 'frobnicate', 'related': {'launch': '/job_templates/1/launch/'}, }) register_get(t) t.register_json('/job_templates/1/launch/', { 'passwords_needed_to_start': ['foo'], }, method='GET') t.register_json('/job_templates/1/launch/', {'id': 42}, method='POST') with mock.patch('tower_cli.resources.job.getpass') as getpass: getpass.return_value = 'bar' result = self.res.launch(1) getpass.assert_called_once_with('Password for foo: ') self.assertDictContainsSubset({'changed': True, 'id': 42}, result) def test_ignored_fields(self): """Establish that if ignored_fields is returned when launching job, it will be displayed in verbose mode. """ echo_count_with_ignore = 0 echo_count = 0 with client.test_mode as t: standard_registration(t) with settings.runtime_values(verbose=True): with mock.patch.object(click, 'secho') as secho: self.res.launch(job_template=1) echo_count = secho.call_count with client.test_mode as t: standard_registration(t, ignored_fields={'foo': 'bar'}) with settings.runtime_values(verbose=True): with mock.patch.object(click, 'secho') as secho: self.res.launch(job_template=1) echo_count_with_ignore = secho.call_count self.assertEqual(echo_count_with_ignore - echo_count, 2) class StatusTests(unittest.TestCase): """A set of tests to establish that the job status command works in the way that we expect. """ def setUp(self): self.res = tower_cli.get_resource('job') def test_normal(self): """Establish that the data about a job retrieved from the jobs endpoint is provided. """ with client.test_mode as t: t.register_json('/jobs/42/', { 'elapsed': 1335024000.0, 'extra': 'ignored', 'failed': False, 'status': 'successful', }) result = self.res.status(42) self.assertEqual(result, { 'elapsed': 1335024000.0, 'failed': False, 'status': 'successful', }) self.assertEqual(len(t.requests), 1) def test_normal_with_lookup(self): """Establish that the data about job specified by query is returned correctly. """ with client.test_mode as t: t.register_json('/jobs/?name=bar', {"count": 1, "results": [ {"id": 42, "name": "bar", 'elapsed': 1335024000.0, 'extra': 'ignored', 'failed': False, 'status': 'successful', }, ], "next": None, "previous": None}, method='GET') result = self.res.status(name="bar") self.assertEqual(result, { 'elapsed': 1335024000.0, 'failed': False, 'status': 'successful', }) self.assertEqual(len(t.requests), 1) def test_detailed(self): with client.test_mode as t: t.register_json('/jobs/42/', { 'elapsed': 1335024000.0, 'extra': 'ignored', 'failed': False, 'status': 'successful', }) result = self.res.status(42, detail=True) self.assertEqual(result, { 'elapsed': 1335024000.0, 'extra': 'ignored', 'failed': False, 'status': 'successful', }) self.assertEqual(len(t.requests), 1) class MonitorWaitTests(unittest.TestCase): """A set of tests to establish that the job monitor and wait commands works in the way that we expect. """ def setUp(self): self.res = tower_cli.get_resource('job') def test_already_successful(self): """Establish that if we attempt to wait an already successful job, we simply get back the job success report. """ with client.test_mode as t: t.register_json('/jobs/42/', { 'elapsed': 1335024000.0, 'failed': False, 'status': 'successful', }) with mock.patch.object(time, 'sleep') as sleep: result = self.res.wait(42) self.assertEqual(sleep.call_count, 0) self.assertEqual(result['status'], 'successful') def test_already_successful_monitor(self): """Pass-through successful job with monitor method""" with client.test_mode as t: t.register_json('/jobs/42/', { 'elapsed': 1335024000.0, 'failed': False, 'status': 'successful', }) # Test same for monitor with mock.patch.object(time, 'sleep') as sleep: with mock.patch.object(type(self.res), 'wait'): with mock.patch.object(click, 'echo'): result = self.res.monitor(42) self.assertEqual(sleep.call_count, 0) self.assertEqual(result['status'], 'successful') def test_failure(self): """Establish that if the job has failed, that we raise the JobFailure exception. """ with client.test_mode as t: t.register_json('/jobs/42/', { 'elapsed': 1335024000.0, 'failed': True, 'status': 'failed', }) with self.assertRaises(exc.JobFailure): with mock.patch.object(click, 'secho') as secho: with mock.patch('tower_cli.models.base.is_tty') as tty: tty.return_value = True self.res.wait(42) self.assertTrue(secho.call_count >= 1) # Test the same with the monitor method with self.assertRaises(exc.JobFailure): with mock.patch.object(click, 'secho') as secho: with mock.patch('tower_cli.models.base.is_tty') as tty: tty.return_value = True self.res.monitor(42) self.assertTrue(secho.call_count >= 1) def test_failure_non_tty(self): """Establish that if the job has failed, that we raise the JobFailure exception, and also don't print bad things on non-tty outfiles. """ with client.test_mode as t: t.register_json('/jobs/42/', { 'elapsed': 1335024000.0, 'failed': True, 'status': 'failed', }) with self.assertRaises(exc.JobFailure): with mock.patch.object(click, 'echo') as echo: with mock.patch('tower_cli.models.base.is_tty') as tty: tty.return_value = False self.res.wait(42) self.assertTrue(echo.call_count >= 1) def test_waiting(self): """Establish that if the first status call returns a pending job, and the second a success, that both calls are made, and a success finally returned. """ # Set up our data object. data = {'elapsed': 1335024000.0, 'failed': False, 'status': 'pending'} # Register the initial request's response. with client.test_mode as t: t.register_json('/jobs/42/', copy(data)) # Create a way to assign a successful data object to the request. def assign_success(*args): t.clear() t.register_json('/jobs/42/', dict(data, status='successful')) # Make the successful state assignment occur when time.sleep() # is called between requests. with mock.patch.object(time, 'sleep') as sleep: sleep.side_effect = assign_success with mock.patch.object(click, 'secho') as secho: with mock.patch('tower_cli.models.base.is_tty') as tty: tty.return_value = True self.res.wait(42, min_interval=0.21) self.assertTrue(secho.call_count >= 100) # We should have gotten two requests total, to the same URL. self.assertEqual(len(t.requests), 2) self.assertEqual(t.requests[0].url, t.requests[1].url) def test_monitor(self): """Establish that if the first status call returns a pending job, and the second a success, that both calls are made, and a success finally returned. """ # Set up our data object. data = {'elapsed': 1335024000.0, 'failed': False, 'status': 'pending'} # Register the initial request's response. with client.test_mode as t: t.register_json('/jobs/42/', copy(data)) # Create a way to assign a successful data object to the request. def assign_success(*args): t.clear() t.register_json('/jobs/42/', dict(data, status='successful')) # Make the successful state assignment occur when time.sleep() # is called between requests. with mock.patch.object(time, 'sleep') as sleep: sleep.side_effect = assign_success with mock.patch.object(click, 'echo'): with mock.patch.object(type(self.res), 'wait'): with mock.patch.object( type(self.res), 'lookup_stdout'): self.res.monitor(42, min_interval=0.21) # We should have gotten 3 requests total, to the same URL. self.assertEqual(len(t.requests), 3) self.assertEqual(t.requests[0].url, t.requests[1].url) def test_timeout(self): """Establish that the --timeout flag is honored if sent to `tower-cli job wait`. """ # Set up our data object. # This doesn't have to change; it will always be pending # (thus the timeout). data = {'elapsed': 1335024000.0, 'failed': False, 'status': 'pending'} # Mock out the passage of time. with client.test_mode as t: t.register_json('/jobs/42/', copy(data)) with mock.patch.object(click, 'secho') as secho: with self.assertRaises(exc.Timeout): with mock.patch('tower_cli.models.base.is_tty') as tty: tty.return_value = True self.res.wait(42, min_interval=0.21, timeout=0.1) self.assertTrue(secho.call_count >= 1) def test_waiting_not_tty(self): """Establish that the wait command prints more useful output for logging if not connected to a tty. """ # Set up our data object. data = {'elapsed': 1335024000.0, 'failed': False, 'status': 'pending'} # Register the initial request's response. with client.test_mode as t: t.register_json('/jobs/42/', copy(data)) # Create a way to assign a successful data object to the request. def assign_success(*args): t.clear() t.register_json('/jobs/42/', dict(data, status='successful')) # Make the successful state assignment occur when time.sleep() # is called between requests. with mock.patch.object(time, 'sleep') as sleep: sleep.side_effect = assign_success with mock.patch.object(click, 'echo') as echo: with mock.patch('tower_cli.models.base.is_tty') as tty: tty.return_value = False self.res.wait(42, min_interval=0.21) self.assertTrue(echo.call_count >= 1) # We should have gotten two requests total, to the same URL. self.assertEqual(len(t.requests), 2) self.assertEqual(t.requests[0].url, t.requests[1].url) class CancelTests(unittest.TestCase): """A set of tasks to establish that the job cancel command works in the way that we expect. """ def setUp(self): self.res = tower_cli.get_resource('job') def test_standard_cancelation(self): """Establish that a standard cancelation command works in the way we expect. """ with client.test_mode as t: t.register('/jobs/42/cancel/', '', method='POST') result = self.res.cancel(42) self.assertTrue(t.requests[0].url.endswith('/jobs/42/cancel/')) self.assertTrue(result['changed']) def test_cancelation_by_lookup(self): """Establish that a job can be canceled by name or identity """ with client.test_mode as t: t.register_json('/jobs/?name=bar', {"count": 1, "results": [ {"id": 42, "name": "bar"}, ], "next": None, "previous": None}, method='GET') t.register('/jobs/42/cancel/', '', method='POST') result = self.res.cancel(name="bar") self.assertTrue(t.requests[0].url.endswith('/jobs/?name=bar')) self.assertTrue(t.requests[1].url.endswith('/jobs/42/cancel/')) self.assertTrue(result['changed']) def test_cancelation_completed(self): """Establish that a standard cancelation command works in the way we expect. """ with client.test_mode as t: t.register('/jobs/42/cancel/', '', method='POST', status_code=405) result = self.res.cancel(42) self.assertTrue(t.requests[0].url.endswith('/jobs/42/cancel/')) self.assertFalse(result['changed']) def test_cancelation_completed_with_error(self): """Establish that a standard cancelation command works in the way we expect. """ with client.test_mode as t: t.register('/jobs/42/cancel/', '', method='POST', status_code=405) with self.assertRaises(exc.TowerCLIError): self.res.cancel(42, fail_if_not_running=True) class RelaunchTests(unittest.TestCase): """A set of tasks to establish that the job relaunch command works in the way that we expect. """ def setUp(self): self.res = tower_cli.get_resource('job') def test_standard_relaunch(self): """Establish that a standard relaunch command works in the way we expect. """ with client.test_mode as t: data = {'id': 43} t.register_json('/jobs/42/relaunch/', data, method='POST') result = self.res.relaunch(42) self.assertTrue(t.requests[0].url.endswith('/jobs/42/relaunch/')) self.assertTrue(result['changed']) ansible-tower-cli-3.2.0/tests/test_resources_job_template.py000066400000000000000000000237771316523067200243730ustar00rootroot00000000000000# Copyright 2015, Ansible, Inc. # Alan Rominger # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. import tower_cli from tower_cli.api import client from tests.compat import unittest, mock from tower_cli.conf import settings from tower_cli.cli.resource import ResSubcommand import click import json class TemplateTests(unittest.TestCase): """A set of tests for commands operating on the job template """ def setUp(self): self.res = tower_cli.get_resource('job_template') def test_create(self): """Establish that a job template can be created """ with client.test_mode as t: endpoint = '/job_templates/' t.register_json(endpoint, {'count': 0, 'results': [], 'next': None, 'previous': None}, method='GET') t.register_json(endpoint, {'changed': True, 'id': 42}, method='POST') self.res.create(name='bar', job_type='run', inventory=1, project=1, playbook='foobar.yml', credential=1) self.assertEqual(t.requests[0].method, 'GET') self.assertEqual(t.requests[1].method, 'POST') self.assertEqual(len(t.requests), 2) # Check that default job_type will get added when needed with client.test_mode as t: endpoint = '/job_templates/' t.register_json(endpoint, {'count': 0, 'results': [], 'next': None, 'previous': None}, method='GET') t.register_json(endpoint, {'changed': True, 'id': 42}, method='POST') self.res.create(name='bar', inventory=1, project=1, playbook='foobar.yml', credential=1) req_body = json.loads(t.requests[1].body) self.assertIn('job_type', req_body) self.assertEqual(req_body['job_type'], 'run') def test_job_template_create_with_echo(self): """Establish that a job template can be created """ with client.test_mode as t: endpoint = '/job_templates/' t.register_json(endpoint, {'count': 0, 'results': [], 'next': None, 'previous': None}, method='GET') t.register_json(endpoint, {'changed': True, 'id': 42, 'name': 'bar', 'inventory': 1, 'project': 1, 'playbook': 'foobar.yml', 'credential': 1}, method='POST') self.res.create(name='bar', job_type='run', inventory=1, project=1, playbook='foobar.yml', credential=1) f = ResSubcommand(self.res)._echo_method(self.res.create) with mock.patch.object(click, 'secho'): with settings.runtime_values(format='human'): f(name='bar', job_type='run', inventory=1, project=1, playbook='foobar.yml', credential=1) def test_create_w_extra_vars(self): """Establish that a job template can be created and extra varas passed to it """ with client.test_mode as t: endpoint = '/job_templates/' t.register_json(endpoint, {'count': 0, 'results': [], 'next': None, 'previous': None}, method='GET') t.register_json(endpoint, {'changed': True, 'id': 42}, method='POST') self.res.create(name='bar', job_type='run', inventory=1, project=1, playbook='foobar.yml', credential=1, extra_vars=['foo: bar']) self.assertEqual(t.requests[0].method, 'GET') self.assertEqual(t.requests[1].method, 'POST') self.assertEqual(len(t.requests), 2) def test_modify(self): """Establish that a job template can be modified """ with client.test_mode as t: endpoint = '/job_templates/' t.register_json(endpoint, {'count': 1, 'results': [{'id': 1, 'name': 'bar'}], 'next': None, 'previous': None}, method='GET') t.register_json('/job_templates/1/', {'name': 'bar', 'id': 1, 'job_type': 'run'}, method='PATCH') self.res.modify(name='bar', playbook='foobared.yml') self.assertEqual(t.requests[0].method, 'GET') self.assertEqual(t.requests[1].method, 'PATCH') self.assertEqual(len(t.requests), 2) def test_modify_extra_vars(self): """Establish that a job template can be modified """ with client.test_mode as t: endpoint = '/job_templates/' t.register_json(endpoint, {'count': 1, 'results': [{'id': 1, 'name': 'bar'}], 'next': None, 'previous': None}, method='GET') t.register_json('/job_templates/1/', {'name': 'bar', 'id': 1, 'job_type': 'run'}, method='PATCH') self.res.modify(name='bar', extra_vars=["a: 5"]) self.assertEqual(t.requests[0].method, 'GET') self.assertEqual(t.requests[1].method, 'PATCH') self.assertEqual(len(t.requests), 2) def test_associate_label(self): """Establish that the associate method makes the HTTP requests that we expect. """ with client.test_mode as t: t.register_json('/job_templates/42/labels/?id=84', {'count': 0, 'results': []}) t.register_json('/job_templates/42/labels/', {}, method='POST') self.res.associate_label(42, 84) self.assertEqual(t.requests[1].body, json.dumps({'associate': True, 'id': 84})) def test_disassociate_label(self): """Establish that the disassociate method makes the HTTP requests that we expect. """ with client.test_mode as t: t.register_json('/job_templates/42/labels/?id=84', {'count': 1, 'results': [{'id': 84}], 'next': None, 'previous': None}) t.register_json('/job_templates/42/labels/', {}, method='POST') self.res.disassociate_label(42, 84) self.assertEqual(t.requests[1].body, json.dumps({'disassociate': True, 'id': 84})) def test_associate_credential(self): """Establish that the associate method makes the HTTP requests that we expect. """ with client.test_mode as t: t.register_json('/job_templates/42/extra_credentials/?id=84', {'count': 0, 'results': []}) t.register_json('/job_templates/42/extra_credentials/', {}, method='POST') self.res.associate_credential(42, 84) self.assertEqual(t.requests[1].body, json.dumps({'associate': True, 'id': 84})) def test_disassociate_credential(self): """Establish that the disassociate method makes the HTTP requests that we expect. """ with client.test_mode as t: t.register_json('/job_templates/42/extra_credentials/?id=84', {'count': 1, 'results': [{'id': 84}], 'next': None, 'previous': None}) t.register_json('/job_templates/42/extra_credentials/', {}, method='POST') self.res.disassociate_credential(42, 84) self.assertEqual(t.requests[1].body, json.dumps({'disassociate': True, 'id': 84})) def test_associate_notification_template(self): """Establish that a job template should be able to associate itself with an existing notification template. """ with client.test_mode as t: t.register_json('/job_templates/5/notification_templates_any/' '?id=3', {'count': 0, 'results': []}) t.register_json('/job_templates/5/notification_templates_any/', {}, method='POST') self.res.associate_notification_template(5, 3, 'any') self.assertEqual(t.requests[1].body, json.dumps({'associate': True, 'id': 3})) def test_disassociate_notification_template(self): """Establish that a job template should be able to disassociate itself from an associated notification template. """ with client.test_mode as t: t.register_json('/job_templates/5/notification_templates_any/' '?id=3', {'count': 1, 'results': [{'id': 3}]}) t.register_json('/job_templates/5/notification_templates_any/', {}, method='POST') self.res.disassociate_notification_template(5, 3, 'any') self.assertEqual(t.requests[1].body, json.dumps({'disassociate': True, 'id': 3})) def test_callback(self): """Establish that a job template should be able to conduct provisioning callback """ with client.test_mode as t: t.register_json('/job_templates/5/callback/', {'host_config_key': 'foobar'}) t.register_json('/job_templates/5/callback/', {}, method='POST') self.res.callback(5) self.assertEqual(t.requests[1].body, json.dumps({"host_config_key": "foobar"})) ansible-tower-cli-3.2.0/tests/test_resources_label.py000066400000000000000000000044151316523067200227710ustar00rootroot00000000000000# Copyright 2016, Ansible, Inc. # Aaron Tan # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. import tower_cli from tower_cli import exceptions as exc from tower_cli.api import client from tower_cli.cli.resource import ResSubcommand from tests.compat import unittest class LabelTests(unittest.TestCase): """A set of tests to establish that the label resource functions in the way that we expect. """ def setUp(self): self.res = tower_cli.get_resource('label') def test_delete_method_is_disabled(self): """Establish that delete method of a label is properly disabled. """ self.assertEqual(ResSubcommand(self.res).get_command(None, 'delete'), None) def test_create_with_jt(self): """Establish that create a label with job template specified works in the way that we expect. """ with client.test_mode as t: t.register_json('/job_templates/6/', {}) t.register_json('/job_templates/6/labels/?name=foo', {'count': 0, 'results': []}) t.register_json('/job_templates/6/labels/', {'id': 1}, method='POST', status_code=201) t.register_json('/labels/?name=foo&organization=1', {'count': 0, 'results': []}) r = self.res.create(name='foo', organization=1, job_template=6) self.assertEqual(dict(r), {'id': 1, 'changed': True}) t.register_json('/labels/?name=foo&organization=1', {'count': 1, 'results': [{'id': 1}]}) with self.assertRaises(exc.TowerCLIError): self.res.create(name='foo', organization=1, job_template=6, fail_on_found=True) ansible-tower-cli-3.2.0/tests/test_resources_notification_template.py000066400000000000000000000236121316523067200262730ustar00rootroot00000000000000# Copyright 2016, Ansible by RedHat. # Aaron Tan # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. import tower_cli from tower_cli.api import client from tower_cli import exceptions as exc from tests.compat import unittest import json import copy class NotificationTemplateTests(unittest.TestCase): """A set of tests for commands operating on the notification template """ def setUp(self): self.res = tower_cli.get_resource('notification_template') self.endpoint = '/notification_templates/' def test_create_in_isolation(self): """Establish that we can create a notification template in isolation. """ post_body = """ {"name": "foo", "notification_type": "slack", "notification_configuration": {"token": "hey", "channels": ["a", "b"]}, "description": "bar"} """ with client.test_mode as t: t.register_json(self.endpoint+'?name=foo', {'count': 0, 'results': []}) t.register_json(self.endpoint, {'id': 18}, method='POST') self.res.create(name='foo', description='bar', notification_type='slack', channels=('a', 'b'), token='hey') self.assertEqual(json.loads(t.requests[1].body), json.loads(post_body)) def test_create_under_jt(self): """Establish that we can create a notification template under an existing job template. """ post_body = """ {"notification_type": "slack", "description": "bar", "notification_configuration": {"token": "hey", "channels": ["a", "b"]}, "name": "foo"} """ with client.test_mode as t: t.register_json('/job_templates/5/', {'id': 5}) t.register_json(self.endpoint, {'count': 0, 'results': []}, notification_type='slack', description='bar', name='foo') t.register_json('/job_templates/5/notification_templates_any/' '?name=foo', {'count': 0, 'results': []}) t.register_json('/job_templates/5/notification_templates_any/', {'id': 16}, method='POST') self.res.create(name='foo', description='bar', notification_type='slack', channels=('a', 'b'), token='hey', job_template=5) self.assertEqual(json.loads(t.requests[3].body), json.loads(post_body)) def test_create_without_necessary_config_option(self): """Establish that creating a notification template without necessary notification configuration options will trigger exception. """ with client.test_mode as t: t.register_json('/job_templates/5/', {'id': 5}) t.register_json(self.endpoint, {'count': 0, 'results': []}, notification_type='slack', description='bar', name='foo') with self.assertRaises(exc.TowerCLIError) as cm: self.res.create(name='foo', description='bar', notification_type='slack', channels=('a', 'b'), job_template=5) self.assertEqual(cm.exception.message, 'Required config field token not provided.') def test_delete(self): """Establish that we can delete an existing notification template. """ with client.test_mode as t: t.register_json(self.endpoint + '?name=foo', {'count': 1, 'results': [{'id': 13}]}) t.register_json(self.endpoint + '13/', '', method='DELETE') result = self.res.delete(name='foo') self.assertTrue(result['changed']) def test_get(self): """Establish that we can get exactly one notification template. """ with client.test_mode as t: t.register_json(self.endpoint + '?name=foo', {'count': 1, 'results': [{'id': 15}]}) result = self.res.get(name='foo') self.assertEqual(result['id'], 15) def test_list(self): """Establish that we can get a list of notification templates under certain criteria. """ with client.test_mode as t: t.register_json(self.endpoint, {'count': 1, 'results': [{'id': 9}], 'previous': None, 'next': None}, notification_type='irc', page='1') result = self.res.list(notification_type='irc', page=1) self.assertEqual(result['count'], 1) def test_config_fields_disabled_during_read(self): """Establish that configuration-related fields are not used for searching. """ with client.test_mode as t: t.register_json(self.endpoint, {'count': 1, 'results': [{'id': 15}]}) self.res.get(channels=('a', 'b'), name='foo') self.assertTrue(t.requests[0].url.endswith('?name=foo')) def test_modify(self): """Establish that we can modify an existing notification, including primary fields and configuration-related fields. """ nt = { 'id': 17, 'name': 'foo', 'notification_type': 'slack', 'notification_configuration': { 'channels': ['a', 'b'], 'token': 'hello' }, 'description': 'foo' } res = copy.deepcopy(nt) res['description'] = 'bar' r1 = """ {"description": "bar"} """ r3 = """ {"description": "bar", "notification_configuration": {"token": "hi", "channels": ["a", "b"]}, "notification_type": "slack"} """ with client.test_mode as t: t.register_json(self.endpoint + '17/', nt) t.register_json(self.endpoint + '17/', res, method='PATCH') self.res.modify(pk=17, description='bar', token='hi') self.assertEqual(json.loads(t.requests[1].body), json.loads(r1)) self.assertEqual(json.loads(t.requests[3].body), json.loads(r3)) def test_modify_with_notification_type_altered(self): """Establish that modifying an existing notification template to a new notification type must provide all related configuration fields also. """ nt = { 'id': 17, 'name': 'foo', 'notification_type': 'slack', 'notification_configuration': { 'channels': ['a', 'b'], 'token': 'hello' }, 'description': 'bar' } with client.test_mode as t: t.register_json(self.endpoint + '17/', nt) with self.assertRaises(exc.TowerCLIError) as cm: self.res.modify(pk=17, description='bar', notification_type='email', token='hi') self.assertEqual(cm.exception.message, 'Required config field username not provided.') def test_notification_configuration_ignore_configuration_options(self): """Establish that --notification-configuration option would ignore any configuration-related options. """ nc = """ { "channels": [ "ho", "ha", "yoho" ], "token": "jingobells" } """ with client.test_mode as t: t.register_json(self.endpoint, {'count': 0, 'results': []}, name='hey') t.register_json(self.endpoint, {'id': 19}, method='POST') self.res.create(name='hey', notification_type='slack', notification_configuration=nc, username='a', sender='b') self.assertEqual(json.loads(t.requests[1].body) ['notification_configuration'], json.loads(nc)) def test_incorrect_json_format(self): """Establish that incorrect json format would trigger exception. """ nc = """ { "channels": [ "ho", "ha", "yoho" ], "token": "jingobells" """ with self.assertRaises(exc.TowerCLIError) as cm: self.res.create(name='hey', notification_type='slack', notification_configuration=nc) self.assertEqual(cm.exception.message, 'Provided json file format invalid. Please recheck.') def test_encrypted_fields_must_be_given(self): """Establish that encrypted configuration fields must be provided even in modification. """ nt = { 'id': 17, 'name': 'foo', 'notification_type': 'slack', 'notification_configuration': { 'channels': ['a', 'b'], 'token': '$encrypted$' }, 'description': '' } with client.test_mode as t: t.register_json(self.endpoint + '12/', nt) with self.assertRaises(exc.TowerCLIError) as cm: self.res.modify(pk=12, channels=('1',)) self.assertEqual(cm.exception.message, 'Required config field token not provided.') ansible-tower-cli-3.2.0/tests/test_resources_organization.py000066400000000000000000000062021316523067200244120ustar00rootroot00000000000000# Copyright 2015, Ansible, Inc. # Luke Sneeringer # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. import json import tower_cli from tower_cli.api import client from tests.compat import unittest class OrganizationTests(unittest.TestCase): """Establish that the organization resource methods work in the way that we expect. """ def setUp(self): self.org_resource = tower_cli.get_resource('organization') def test_associate(self): """Establish that the associate method makes the HTTP requests that we expect. """ with client.test_mode as t: t.register_json('/organizations/42/users/?id=84', {'count': 0, 'results': []}) t.register_json('/organizations/42/users/', {}, method='POST') self.org_resource.associate(42, 84) self.assertEqual(t.requests[1].body, json.dumps({'associate': True, 'id': 84})) def test_associate_admin(self): """Same associate test, but for creating an admin""" with client.test_mode as t: t.register_json('/organizations/42/admins/?id=84', {'count': 0, 'results': []}) t.register_json('/organizations/42/admins/', {}, method='POST') self.org_resource.associate_admin(42, 84) self.assertEqual(t.requests[1].body, json.dumps({'associate': True, 'id': 84})) def test_disassociate(self): """Establish that the associate method makes the HTTP requests that we expect. """ with client.test_mode as t: t.register_json('/organizations/42/users/?id=84', {'count': 1, 'results': [{'id': 84}], 'next': None, 'previous': None}) t.register_json('/organizations/42/users/', {}, method='POST') self.org_resource.disassociate(42, 84) self.assertEqual(t.requests[1].body, json.dumps({'disassociate': True, 'id': 84})) def test_disassociate_admin(self): """Same disassociate test, but for creating an admin""" with client.test_mode as t: t.register_json('/organizations/42/admins/?id=84', {'count': 1, 'results': [{'id': 84}], 'next': None, 'previous': None}) t.register_json('/organizations/42/admins/', {}, method='POST') self.org_resource.disassociate_admin(42, 84) self.assertEqual(t.requests[1].body, json.dumps({'disassociate': True, 'id': 84})) ansible-tower-cli-3.2.0/tests/test_resources_project.py000066400000000000000000000314141316523067200233570ustar00rootroot00000000000000# Copyright 2015, Ansible, Inc. # Luke Sneeringer # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. import tower_cli from tower_cli.api import client from tower_cli import exceptions as exc from tower_cli.constants import CUR_API_VERSION from tests.compat import unittest, mock class CreateTests(unittest.TestCase): """A set of tests for ensuring that the project resource's create command works in the way we expect. """ def setUp(self): self.res = tower_cli.get_resource('project') def test_create_with_organization(self): """Establish that a project can be created inside of an organization. This uses the --organization flag with the create command. """ with client.test_mode as t: t.register_json('/projects/', {'count': 0, 'results': [], 'next': None, 'previous': None}, method='GET') # OPTIONS check is used to determine how the linkage is done t.register_json('/projects/', {'actions': {'POST': {}}}, method='OPTIONS') t.register_json('/projects/', {'id': 42}, method='POST') # The org endpoint can be used to lookup org pk given org name t.register_json('/organizations/1/', {'id': 1}, method='GET') # This is an endpoint used for project-org association t.register_json( '/organizations/1/projects/', {'count': 0}, method='GET' ) t.register_json( '/organizations/1/projects/', {'changed': True}, method='POST' ) result = self.res.create( name='bar', organization=1, scm_type="git" ) self.assertEqual(len(t.requests), 6) self.assertEqual(t.requests[0].method, 'OPTIONS') self.assertEqual(t.requests[1].method, 'GET') self.assertEqual(t.requests[2].method, 'POST') self.assertEqual(t.requests[3].method, 'GET') self.assertEqual(t.requests[4].method, 'GET') self.assertEqual(t.requests[5].method, 'POST') self.assertDictContainsSubset({'id': 42}, result) def test_create_with_organization_as_flag(self): """Establish organization works as direct API flag when the API allows for this use type.""" with client.test_mode as t: t.register_json('/projects/', {'count': 0, 'results': [], 'next': None, 'previous': None}, method='GET') # OPTIONS check is used to determine how the linkage is done t.register_json( '/projects/', {'actions': {'POST': { 'organization': 'detail'}}}, method='OPTIONS') t.register_json('/projects/', {'id': 42}, method='POST') # The org endpoint can be used to lookup org pk given org name # t.register_json('/organizations/1/', {'id': 1}, method='GET') result = self.res.create( name='bar', organization=1, scm_type="git" ) self.assertEqual(len(t.requests), 3) self.assertEqual(t.requests[0].method, 'OPTIONS') self.assertEqual(t.requests[1].method, 'GET') self.assertEqual(t.requests[2].method, 'POST') self.assertDictContainsSubset({'id': 42}, result) def test_create_without_organization(self): """Establish that a project can be created without giving an organization. This should create a project with no organization. This action uses the /projects/ endpoint """ with client.test_mode as t: endpoint = '/projects/' t.register_json(endpoint, {'count': 0, 'results': [], 'next': None, 'previous': None}, method='GET') t.register_json(endpoint, {'changed': True, 'id': 42}, method='POST') self.res.create(name='bar', scm_type="git") self.assertEqual(t.requests[0].method, 'GET') self.assertEqual(t.requests[1].method, 'POST') self.assertEqual(len(t.requests), 2) def test_create_wait(self): """Establish that a project can be created with the wait flag enabled and still successfully exit and complete. """ with client.test_mode as t: # Endpoints related to creating the resource t.register_json('/projects/', {'count': 0, 'results': [], 'next': None, 'previous': None}, method='GET') t.register_json('/projects/', {'changed': True, 'id': 42}, method='POST') # Endpoints related to waiting for the resource t.register_json( '/projects/42/', { 'status': 'successful', 'related': { 'last_update': '/api/%s/project_updates/21/' % CUR_API_VERSION } }, method='GET' ) t.register_json( '/project_updates/21/', {'id': 21, 'status': 'successful'}, method='GET' ) result = self.res.create(name='bar', scm_type="git", wait=True) self.assertEqual(t.requests[0].method, 'GET') self.assertEqual(t.requests[-1].method, 'GET') self.assertDictContainsSubset({'changed': True}, result) class UpdateTests(unittest.TestCase): """A set of tests for ensuring that the project resource's update command works in the way we expect. """ def setUp(self): self.res = tower_cli.get_resource('project') def test_modify_project(self): """Test modifying project in order to cover the special case that removes the organization from its options. """ with client.test_mode as t: t.register_json('/projects/', {'count': 1, 'results': [{'id': 1, 'name': 'bar'}], 'next': None, 'previous': None}, method='GET') t.register_json('/projects/1/', {'name': 'bar', 'id': 1, 'type': 'project', 'organization': 1}, method='PATCH') self.res.modify(name='bar', scm_type="git") self.assertEqual(t.requests[0].method, 'GET') self.assertEqual(t.requests[1].method, 'PATCH') self.assertEqual(len(t.requests), 2) def test_basic_update(self): """Establish that we are able to create a project update and return the changed status. """ with client.test_mode as t: t.register_json('/projects/1/', { 'id': 1, 'name': 'frobnicate', 'related': {'update': '/api/%s/projects/1/update/' % CUR_API_VERSION}, }) t.register_json('/projects/1/update/', {'can_update': True}, method='GET') t.register_json('/projects/1/update/', {'project_update': 42}, method='POST') result = self.res.update(1) self.assertEqual(result, {'changed': True, 'id': 42}) def test_basic_update_with_monitor(self): """Establish that we are able to create a project update and shift to monitoring if requested. """ with client.test_mode as t: t.register_json('/projects/1/', { 'id': 1, 'name': 'frobnicate', 'related': {'update': '/api/%s/projects/1/update/' % CUR_API_VERSION}, }) t.register_json('/projects/1/update/', {'can_update': True}, method='GET') t.register_json('/projects/1/update/', {'project_update': 42}, method='POST') with mock.patch.object(type(self.res), 'monitor') as monitor: self.res.update(1, monitor=True) monitor.assert_called_once_with(42, parent_pk=1, timeout=None) # Test wait method, which follows same pattern with mock.patch.object(type(self.res), 'wait') as wait: self.res.update(1, wait=True) wait.assert_called_once_with(42, parent_pk=1, timeout=None) def test_cannot_update(self): """Establish that attempting to update a non-updatable project errors out as expected. """ with client.test_mode as t: t.register_json('/projects/1/', { 'id': 1, 'name': 'frobnicate', 'related': {'update': '/api/%s/projects/1/update/' % CUR_API_VERSION}, }) t.register_json('/projects/1/update/', {'can_update': False}, method='GET') with self.assertRaises(exc.CannotStartJob): self.res.update(1) class StatusTests(unittest.TestCase): """A set of tests to establish that the project status command works in the way that we expect. """ def setUp(self): self.res = tower_cli.get_resource('project') def test_normal(self): """Establish that the data about a project update retrieved from the project updates endpoint is provided. """ with client.test_mode as t: t.register_json('/projects/1/', { 'id': 1, 'related': {'last_update': '/api/%s/projects/1/project_updates/42/' % CUR_API_VERSION}, }) t.register_json('/projects/1/project_updates/42/', { 'elapsed': 1335024000.0, 'extra': 'ignored', 'failed': False, 'status': 'successful', }) result = self.res.status(1) self.assertEqual(result, { 'elapsed': 1335024000.0, 'failed': False, 'status': 'successful', }) self.assertEqual(len(t.requests), 2) def test_detailed(self): """Establish that a detailed request is sent back in the way that we expect. """ with client.test_mode as t: t.register_json('/projects/1/', { 'id': 1, 'related': {'last_update': '/api/%s/projects/1/project_updates/42/' % CUR_API_VERSION}, }) t.register_json('/projects/1/project_updates/42/', { 'elapsed': 1335024000.0, 'extra': 'ignored', 'failed': False, 'status': 'successful', }) result = self.res.status(1, detail=True) self.assertEqual(result, { 'elapsed': 1335024000.0, 'extra': 'ignored', 'failed': False, 'status': 'successful', }) self.assertEqual(len(t.requests), 2) def test_currently_running_update(self): """Establish that if an update is currently running, that we see this and send back the appropriate status. """ with client.test_mode as t: t.register_json('/projects/1/', { 'id': 1, 'related': { 'current_update': '/api/%s/projects/1/project_updates/42/' % CUR_API_VERSION, 'last_update': '/api/%s/projects/1/project_updates/41/' % CUR_API_VERSION, }, }) t.register_json('/projects/1/project_updates/42/', { 'elapsed': 1335024000.0, 'extra': 'ignored', 'failed': False, 'status': 'running', }) result = self.res.status(1) self.assertEqual(result, { 'elapsed': 1335024000.0, 'failed': False, 'status': 'running', }) self.assertEqual(len(t.requests), 2) def test_no_updates(self): """Establish that running `status` against a project with no updates raises the error we expect. """ with client.test_mode as t: t.register_json('/projects/1/', { 'id': 1, 'related': {}, }) with self.assertRaises(exc.NotFound): self.res.status(1) ansible-tower-cli-3.2.0/tests/test_resources_role.py000066400000000000000000000250071316523067200226530ustar00rootroot00000000000000# Copyright 2016, Ansible by Red Hat # Alan Rominger # Aaron Tan # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. import tower_cli # used to test static methods from tower_cli.resources.role import Resource as Role from tower_cli import exceptions as exc from tower_cli.conf import settings from tower_cli.cli.resource import ResSubcommand from tower_cli.constants import CUR_API_VERSION from tests.compat import unittest, mock from copy import copy example_role_data = { "id": 1, "type": "role", "url": "/api/%s/roles/1/" % CUR_API_VERSION, "related": {"users": "/api/%s/roles/1/users/" % CUR_API_VERSION, "teams": "/api/%s/roles/1/teams/" % CUR_API_VERSION}, "summary_fields": {}, "name": "System Administrator", "description": "Can manage all aspects of the system"} class RoleUnitTests(unittest.TestCase): """Test role internal helper functions.""" def test_plurals(self): """English words changed from singular to plural""" self.assertEqual(Role.pluralize("inventory"), "inventories") self.assertEqual(Role.pluralize("job_template"), "job_templates") def test_obj_res_team(self): """Test that the input format can be correctly translated into the object & resource data structure for granting the resource role to the object. Do this for teams getting read permission on inventory here.""" obj, obj_type, res, res_type = Role.obj_res( {"team": 3, "inventory": 5, "type": "read", "not_a_res": None, "credential": None, "project": None}) self.assertEqual(obj, 3) self.assertEqual(obj_type, 'team') self.assertEqual(res, 5) self.assertEqual(res_type, 'inventory') def test_obj_res_missing_errors(self): """Testing obj_res method, ability to produce errors here.""" with self.assertRaises(exc.UsageError): obj, obj_type, res, res_type = Role.obj_res( {"inventory": None, "credential": None}) def test_obj_res_too_many_errors(self): """Testing obj_res method, ability to duplicate errors.""" with self.assertRaises(exc.UsageError): obj, obj_type, res, res_type = Role.obj_res( {"inventory": 1, "target_team": 2, "user": 3, "team": 5}) def test_populate_resource_columns(self): """Test function that fills in extra columns""" singleton_output = copy(example_role_data) Role.populate_resource_columns(singleton_output) self.assertIn('resource_name', singleton_output) # Case for non-singleton roles normal_output = copy(example_role_data) normal_output['summary_fields'] = { "resource_name": "Default", "resource_type": "organization", "resource_type_display_name": "Organization"} Role.populate_resource_columns(normal_output) self.assertIn('resource_name', normal_output) def test_data_endpoint_team_no_res(self): """Translation of input args to lookup args, using team""" kwargs = {'team': 2} data, endpoint = Role.data_endpoint(kwargs, ignore=[]) self.assertEqual(endpoint, 'teams/2/roles/') self.assertNotIn('object_id', data) def test_data_endpoint_inventory_ignore(self): """Translation of input args to lookup args, ignoring inventory""" kwargs = {'user': 2, 'type': 'admin', 'inventory': 5} data, endpoint = Role.data_endpoint(kwargs, ignore=['res']) self.assertIn('members__in', data) self.assertEqual(endpoint, '/roles/') class RoleMethodTests(unittest.TestCase): """Test role commands.""" def setUp(self): self.res = tower_cli.get_resource('role') def test_removed_methods(self): """Test that None is returned from removed methods.""" self.assertEqual( ResSubcommand(self.res).get_command(None, 'delete'), None) def test_configure_write_display(self): """Test that output configuration for writing to role works.""" data = copy(example_role_data) kwargs = {'user': 2, 'inventory': 3, 'type': 'admin'} self.res.configure_display(data, kwargs, write=True) self.assertIn('user', data) def test_list_user(self): """Assure that super method is called with right parameters""" with mock.patch( 'tower_cli.models.base.BaseResource.list') as mock_list: mock_list.return_value = {'results': [example_role_data]} self.res.list(user=1) mock_list.assert_called_once_with(members__in=1) def test_list_team(self): """Teams can not be passed as a parameter, check use of sublist""" with mock.patch( 'tower_cli.models.base.BaseResource.list') as mock_list: mock_list.return_value = {'results': []} self.res.list(team=1, inventory=3, type='read') mock_list.assert_called_once_with( object_id=3, role_field='read_role') self.assertEqual(self.res.endpoint, 'teams/1/roles/') def test_list_resource(self): """Listing based on a resource the role applies to""" with mock.patch( 'tower_cli.models.base.BaseResource.list') as mock_list: mock_list.return_value = {'results': []} self.res.list(inventory=3, type='read') mock_list.assert_called_once_with(role_field='read_role') self.assertEqual(self.res.endpoint, 'inventories/3/object_roles/') def test_get_user(self): """Assure that super method is called with right parameters""" with mock.patch( 'tower_cli.models.base.BaseResource.read') as mock_read: mock_read.return_value = {'results': [copy(example_role_data)]} with settings.runtime_values(format='human'): self.res.get(user=1) mock_read.assert_called_once_with( fail_on_multiple_results=True, fail_on_no_results=True, members__in=1, pk=None) def test_get_user_json(self): """Test internal use with json format, no debug""" with mock.patch( 'tower_cli.models.base.BaseResource.read') as mock_read: mock_read.return_value = {'results': [{ 'name': 'arole', 'summary_fields': {}}]} with settings.runtime_values(format='json'): self.res.get(user=1, include_debug_header=False) mock_read.assert_called_once_with( fail_on_multiple_results=True, fail_on_no_results=True, members__in=1, pk=None) def test_grant_user_role(self): """Assure that super method is called granting role""" with mock.patch( 'tower_cli.resources.role.Resource.role_write') as mock_write: kwargs = dict(user=1, type='read', project=3) self.res.grant(**kwargs) mock_write.assert_called_once_with(fail_on_found=False, **kwargs) def test_revoke_user_role(self): """Assure that super method is called revoking role""" with mock.patch( 'tower_cli.resources.role.Resource.role_write') as mock_write: kwargs = dict(user=1, type='read', project=3) self.res.revoke(**kwargs) mock_write.assert_called_once_with(fail_on_found=False, disassociate=True, **kwargs) def test_role_write_user_exists(self): """Simulate granting user permission where they already have it.""" with mock.patch( 'tower_cli.models.base.BaseResource.read') as mock_read: mock_read.return_value = {'results': [copy(example_role_data)], 'count': 1} r = self.res.role_write(user=2, inventory=3, type='admin') self.assertEqual(r['user'], 2) def test_role_write_user_exists_FOF(self): """Simulate granting user permission where they already have it.""" with mock.patch( 'tower_cli.models.base.BaseResource.read') as mock_read: mock_read.return_value = {'results': [copy(example_role_data)], 'count': 1} with mock.patch('tower_cli.api.Client.post'): with self.assertRaises(exc.NotFound): self.res.role_write(user=2, inventory=3, type='admin', fail_on_found=True) def test_role_write_user_does_not_exist(self): """Simulate revoking user permission where they already lack it.""" with mock.patch( 'tower_cli.models.base.BaseResource.read') as mock_read: mock_read.return_value = {'results': [copy(example_role_data)], 'count': 0} r = self.res.role_write(user=2, inventory=3, type='admin', disassociate=True) self.assertEqual(r['user'], 2) def test_role_grant_user(self): """Simulate granting user permission.""" with mock.patch( 'tower_cli.models.base.BaseResource.read') as mock_read: mock_read.return_value = { 'results': [copy(example_role_data)], 'count': 0} with mock.patch('tower_cli.api.Client.post') as mock_post: self.res.role_write(user=2, inventory=3, type='admin') mock_post.assert_called_once_with( 'users/2/roles/', data={'id': 1}) def test_role_revoke_user(self): """Simulate granting user permission.""" with mock.patch( 'tower_cli.models.base.BaseResource.read') as mock_read: mock_read.return_value = { 'results': [copy(example_role_data)], 'count': 1} with mock.patch('tower_cli.api.Client.post') as mock_post: self.res.role_write(user=2, inventory=3, type='admin', disassociate=True) mock_post.assert_called_once_with( 'users/2/roles/', data={'id': 1, 'disassociate': True}) ansible-tower-cli-3.2.0/tests/test_resources_schedule.py000066400000000000000000000070561316523067200235120ustar00rootroot00000000000000# Copyright 2016, Ansible by Red Hat. # Aaron Tan # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. import tower_cli from tower_cli import exceptions as exc from tower_cli.api import client from tower_cli.models.base import Resource from tests.compat import unittest import json CURD_METHODS = ('create', 'modify', 'list', 'get', 'delete') CLICK_ATTRS = ('__click_params__', '_cli_command', '_cli_command_attrs') class ScheduleTests(unittest.TestCase): """A set of tests for commands operating on schedules """ def setUp(self): self.res = tower_cli.get_resource('schedule') self.endpoint = '/schedules/' def test_raises_with_duplicate_unified_jt_fields(self): """Establish that No more than one unified job template fields should be provided. """ with self.assertRaises(exc.UsageError) as cm: self.res.get(job_template=1, project=2) self.assertEqual(cm.exception.message, 'More than one unified job' ' template fields provided, please tighten your ' 'criteria.') def test_raises_with_no_unified_jt_fields_during_creation(self): """Establish that At least one unified job template field should be provided during creation. """ with self.assertRaises(exc.UsageError) as cm: self.res.create(name='hehe') self.assertEqual(cm.exception.message, 'You must provide exactly ' 'one unified job template field during creation.') def test_decorated_methods_have_click_related_attributes(self): """Establish that click-related extra function attributes are passed on to decorated methods. """ for item in CURD_METHODS: method = getattr(self.res, item) for attr in CLICK_ATTRS: self.assertIn(attr, dir(method)) def test_docstring_is_passed(self): """Establish that docstring of the original method are passed on to decorated counterpart. """ parent_res = Resource() for item in CURD_METHODS: method = getattr(self.res, item) parent_method = getattr(parent_res, item) self.assertEqual(method.__doc__, parent_method.__doc__) def test_create_invokes_correct_endpoint(self): """Establish that correct endpoint is invoked according to unified jt field provided during creation. """ post_body = """ {"name": "hehe", "unified_job_template": 5, "rrule": "DTSTART:20160812T200122Z"} """ with client.test_mode as t: t.register_json('/job_templates/5/schedules/?name=hehe', {'count': 0, 'results': []}) t.register_json('/job_templates/5/schedules/', {'id': 5}, method='POST') self.res.create(name='hehe', job_template=5, rrule='DTSTART:20160812T200122Z') self.assertEqual(json.loads(t.requests[1].body), json.loads(post_body)) ansible-tower-cli-3.2.0/tests/test_resources_setting.py000066400000000000000000000276371316523067200234020ustar00rootroot00000000000000# -*- coding: utf-8 -*- # Copyright 2017, Ansible by Red Hat # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. import json import six import tower_cli from tower_cli.api import client from tower_cli import exceptions as exc from tower_cli.utils.data_structures import OrderedDict from tower_cli.cli.resource import ResSubcommand from tower_cli.constants import CUR_API_VERSION from tests.compat import unittest LICENSE_DATA = json.dumps({ "eula_accepted": True, "contact_email": "bobby@example.org", "features": {}, "license_type": "enterprise", "company_name": "Fancy Pants, Inc.", "contact_name": "Bobby Softwares", "license_date": 10000000000, "license_key": "60a888de5a23994c6d1e6406b7fd75c8", "instance_count": 250 }) class SettingTests(unittest.TestCase): """A set of tests to establish that the setting resource functions in the way that we expect. """ def setUp(self): self.res = tower_cli.get_resource('setting') def test_create_method_is_disabled(self): """The delete method is properly disabled.""" self.assertEqual(ResSubcommand(self.res).get_command(None, 'create'), None) def test_delete_method_is_disabled(self): """The create method is properly disabled.""" self.assertEqual(ResSubcommand(self.res).get_command(None, 'delete'), None) def test_list_all(self): """All settings can be listed""" all_settings = OrderedDict({ 'FIRST': 123, 'SECOND': 'foo' }) with client.test_mode as t: t.register_json('/settings/all/', all_settings) r = self.res.list() self.assertEqual( sorted(r['results'], key=lambda k: k['id']), [ {'id': 'FIRST', 'value': 123}, {'id': 'SECOND', 'value': 'foo'} ] ) def test_list_all_by_category(self): """Settings can be listed by category""" system_settings = OrderedDict({'FEATURE_ENABLED': True}) auth_settings = OrderedDict({'SOME_API_KEY': 'ABC123'}) with client.test_mode as t: t.register_json('/settings/system/', system_settings) t.register_json('/settings/authentication/', auth_settings) r = self.res.list(category='system') self.assertEqual( r['results'], [{'id': 'FEATURE_ENABLED', 'value': True}] ) r = self.res.list(category='authentication') self.assertEqual( r['results'], [{'id': 'SOME_API_KEY', 'value': 'ABC123'}] ) def test_list_invalid_category(self): """Settings can only be listed by valid categories""" categories = { 'results': [{ 'url': '/api/%s/settings/all/' % CUR_API_VERSION, 'name': 'All', 'slug': 'all' }, { 'url': '/api/%s/settings/logging/' % CUR_API_VERSION, 'name': 'Logging', 'slug': 'logging' }] } with client.test_mode as t: t.register_json('/settings/', categories) t.register_json('/settings/authentication/', '', status_code=404) with self.assertRaises(exc.NotFound) as e: self.res.list(category='authentication') self.assertEqual( e.exception.message, ('authentication is not a valid category. Choose from ' '[all, logging]') ) def test_get(self): """Individual settings can be retrieved""" all_settings = OrderedDict({'FIRST': 123}) with client.test_mode as t: t.register_json('/settings/all/', all_settings) r = self.res.get('FIRST') self.assertEqual(r, {'id': 'FIRST', 'value': 123}) def test_get_invalid(self): """Invalid setting names throw an error""" all_settings = OrderedDict({'FIRST': 123}) with client.test_mode as t: t.register_json('/settings/all/', all_settings) self.assertRaises(exc.NotFound, self.res.get, 'MISSING') def test_update(self): """A setting's value can be updated""" options = {'actions': {'PUT': {'FIRST': {'type': 'integer'}}}} all_settings = OrderedDict({'FIRST': 123}) patched = OrderedDict({'FIRST': 456}) with client.test_mode as t: t.register_json('/settings/all/', all_settings) t.register_json('/settings/all/', options, method='OPTIONS') t.register_json('/settings/all/', patched, method='PATCH') r = self.res.modify('FIRST', '456') self.assertTrue(r['changed']) request = t.requests[0] self.assertEqual(request.method, 'GET') request = t.requests[1] self.assertEqual(request.method, 'OPTIONS') request = t.requests[2] self.assertEqual(request.method, 'PATCH') self.assertEqual(request.body, json.dumps({'FIRST': 456})) def test_license_update(self): """The software license can be updated""" all_settings = OrderedDict({'LICENSE': {}}) with client.test_mode as t: t.register_json('/settings/all/', all_settings) t.register_json('/config/', all_settings, method='POST') self.res.modify('LICENSE', LICENSE_DATA) request = t.requests[0] self.assertEqual(request.method, 'GET') request = t.requests[1] self.assertEqual(request.method, 'POST') self.assertEqual(json.loads(request.body), json.loads(LICENSE_DATA)) def test_update_with_unicode(self): """A setting's value can be updated with unicode""" new_val = six.u('Iñtërnâtiônàlizætiøn') options = {'actions': {'PUT': {'FIRST': {'type': 'string'}}}} all_settings = OrderedDict({'FIRST': 'FOO'}) patched = OrderedDict({'FIRST': new_val}) with client.test_mode as t: t.register_json('/settings/all/', all_settings) t.register_json('/settings/all/', options, method='OPTIONS') t.register_json('/settings/all/', patched, method='PATCH') r = self.res.modify('FIRST', new_val) self.assertTrue(r['changed']) request = t.requests[0] self.assertEqual(request.method, 'GET') request = t.requests[1] self.assertEqual(request.method, 'OPTIONS') request = t.requests[2] self.assertEqual(request.method, 'PATCH') self.assertEqual(request.body, json.dumps({'FIRST': new_val})) def test_update_with_boolean(self): """A setting's value can be updated with a boolean""" options = {'actions': {'PUT': {'FIRST': {'type': 'boolean'}}}} all_settings = OrderedDict({'FIRST': False}) patched = OrderedDict({'FIRST': True}) with client.test_mode as t: t.register_json('/settings/all/', all_settings) t.register_json('/settings/all/', options, method='OPTIONS') t.register_json('/settings/all/', patched, method='PATCH') r = self.res.modify('FIRST', 'True') self.assertTrue(r['changed']) request = t.requests[0] self.assertEqual(request.method, 'GET') request = t.requests[1] self.assertEqual(request.method, 'OPTIONS') request = t.requests[2] self.assertEqual(request.method, 'PATCH') self.assertEqual(request.body, json.dumps({'FIRST': True})) def test_update_with_list(self): """A setting's value can be updated with a list""" options = {'actions': {'PUT': {'FIRST': {'type': 'list'}}}} all_settings = OrderedDict({'FIRST': []}) patched = OrderedDict({'FIRST': ['abc']}) with client.test_mode as t: t.register_json('/settings/all/', all_settings) t.register_json('/settings/all/', options, method='OPTIONS') t.register_json('/settings/all/', patched, method='PATCH') r = self.res.modify('FIRST', "['abc']") self.assertTrue(r['changed']) request = t.requests[0] self.assertEqual(request.method, 'GET') request = t.requests[1] self.assertEqual(request.method, 'OPTIONS') request = t.requests[2] self.assertEqual(request.method, 'PATCH') self.assertEqual(request.body, json.dumps({'FIRST': ['abc']})) def test_update_with_dict(self): """A setting's value can be updated with a dict""" options = {'actions': {'PUT': {'FIRST': {'type': 'nested object'}}}} all_settings = OrderedDict({'FIRST': []}) patched = OrderedDict({'FIRST': {'abc': 'xyz'}}) with client.test_mode as t: t.register_json('/settings/all/', all_settings) t.register_json('/settings/all/', options, method='OPTIONS') t.register_json('/settings/all/', patched, method='PATCH') r = self.res.modify('FIRST', "{'abc': 'xyz'}") self.assertTrue(r['changed']) request = t.requests[0] self.assertEqual(request.method, 'GET') request = t.requests[1] self.assertEqual(request.method, 'OPTIONS') request = t.requests[2] self.assertEqual(request.method, 'PATCH') self.assertEqual( request.body, json.dumps({'FIRST': {'abc': 'xyz'}}) ) def test_idempotent_updates_ignored(self): """Don't PATCH a setting if the provided value didn't change""" all_settings = OrderedDict({'FIRST': 123}) with client.test_mode as t: t.register_json('/settings/all/', all_settings) r = self.res.modify('FIRST', '123') self.assertFalse(r['changed']) self.assertEqual(len(t.requests), 1) request = t.requests[0] self.assertEqual(request.method, 'GET') def test_encrypted_updates_always_patch(self): """Always PATCH a setting if it's an encrypted one""" options = {'actions': {'PUT': {'SECRET': {'type': 'string'}}}} all_settings = OrderedDict({'SECRET': '$encrypted$'}) patched = OrderedDict({'SECRET': '$encrypted$'}) with client.test_mode as t: t.register_json('/settings/all/', all_settings) t.register_json('/settings/all/', options, method='OPTIONS') t.register_json('/settings/all/', patched, method='PATCH') r = self.res.modify('SECRET', 'SENSITIVE') self.assertTrue(r['changed']) self.assertEqual(len(t.requests), 3) request = t.requests[0] self.assertEqual(request.method, 'GET') request = t.requests[1] self.assertEqual(request.method, 'OPTIONS') request = t.requests[2] self.assertEqual(request.method, 'PATCH') self.assertEqual(request.body, json.dumps({'SECRET': 'SENSITIVE'})) def test_update_invalid_setting_name(self): """A setting must exist to be updated""" all_settings = OrderedDict({'FIRST': 123}) with client.test_mode as t: t.register_json('/settings/all/', all_settings) t.register_json('/settings/all/', all_settings, method='PATCH') self.assertRaises(exc.NotFound, self.res.modify, 'MISSING', 456) ansible-tower-cli-3.2.0/tests/test_resources_team.py000066400000000000000000000040101316523067200226270ustar00rootroot00000000000000# Copyright 2015, Ansible, Inc. # Luke Sneeringer # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. import json import tower_cli from tower_cli.api import client from tests.compat import unittest class TeamTests(unittest.TestCase): """Establish that the team resource methods work in the way that we expect. """ def setUp(self): self.team_resource = tower_cli.get_resource('team') def test_associate(self): """Establish that the associate method makes the HTTP requests that we expect. """ with client.test_mode as t: t.register_json('/teams/42/users/?id=84', {'count': 0, 'results': []}) t.register_json('/teams/42/users/', {}, method='POST') self.team_resource.associate(42, 84) self.assertEqual(t.requests[1].body, json.dumps({'associate': True, 'id': 84})) def test_disassociate(self): """Establish that the associate method makes the HTTP requests that we expect. """ with client.test_mode as t: t.register_json('/teams/42/users/?id=84', {'count': 1, 'results': [{'id': 84}], 'next': None, 'previous': None}) t.register_json('/teams/42/users/', {}, method='POST') self.team_resource.disassociate(42, 84) self.assertEqual(t.requests[1].body, json.dumps({'disassociate': True, 'id': 84})) ansible-tower-cli-3.2.0/tests/test_resources_workflow.py000066400000000000000000000277471316523067200236010ustar00rootroot00000000000000# Copyright 2017 Ansible by Red Hat # Alan Rominger # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. import json import tower_cli from tower_cli.resources.workflow import Resource from tests.compat import unittest, mock EXAMPLE_NODE_LIST = [ { 'id': 1, 'success_nodes': [], 'failure_nodes': [], 'always_nodes': [2], 'unified_job_template': 48, 'summary_fields': { 'unified_job_template': { 'unified_job_type': 'job' } } }, { 'id': 2, 'success_nodes': [], 'failure_nodes': [], 'always_nodes': [], 'unified_job_template': 98, 'summary_fields': { 'unified_job_template': { 'unified_job_type': 'project_update' } } } ] CUR_WORKFLOW = [ { 'id': 1, 'success_nodes': [], 'failure_nodes': [], 'always_nodes': [2, 3], 'workflow_job_template': 1, 'unified_job_template': 1, 'summary_fields': { 'unified_job_template': { 'unified_job_type': 'project_update' } } }, { 'id': 2, 'success_nodes': [4], 'failure_nodes': [5], 'always_nodes': [], 'workflow_job_template': 1, 'unified_job_template': 2, 'summary_fields': { 'unified_job_template': { 'unified_job_type': 'project_update' } } }, { 'id': 3, 'success_nodes': [], 'failure_nodes': [6, 7], 'always_nodes': [], 'workflow_job_template': 1, 'unified_job_template': 3, 'summary_fields': { 'unified_job_template': { 'unified_job_type': 'project_update' } } }, { 'id': 4, 'success_nodes': [], 'failure_nodes': [], 'always_nodes': [], 'workflow_job_template': 1, 'unified_job_template': 4, 'summary_fields': { 'unified_job_template': { 'unified_job_type': 'job' } } }, { 'id': 5, 'success_nodes': [], 'failure_nodes': [], 'always_nodes': [], 'workflow_job_template': 1, 'unified_job_template': 5, 'summary_fields': { 'unified_job_template': { 'unified_job_type': 'job' } } }, { 'id': 6, 'success_nodes': [], 'failure_nodes': [], 'always_nodes': [], 'workflow_job_template': 1, 'unified_job_template': 6, 'summary_fields': { 'unified_job_template': { 'unified_job_type': 'job' } } }, { 'id': 7, 'success_nodes': [], 'failure_nodes': [], 'always_nodes': [], 'workflow_job_template': 1, 'unified_job_template': 7, 'summary_fields': { 'unified_job_template': { 'unified_job_type': 'job' } } }, { 'id': 8, 'success_nodes': [9, 10], 'failure_nodes': [], 'always_nodes': [], 'workflow_job_template': 1, 'unified_job_template': 8, 'summary_fields': { 'unified_job_template': { 'unified_job_type': 'inventory_update' } } }, { 'id': 9, 'success_nodes': [], 'failure_nodes': [], 'always_nodes': [11], 'workflow_job_template': 1, 'unified_job_template': 9, 'summary_fields': { 'unified_job_template': { 'unified_job_type': 'inventory_update' } } }, { 'id': 10, 'success_nodes': [], 'failure_nodes': [], 'always_nodes': [12], 'workflow_job_template': 1, 'unified_job_template': 9, 'summary_fields': { 'unified_job_template': { 'unified_job_type': 'inventory_update' } } }, { 'id': 11, 'success_nodes': [], 'failure_nodes': [], 'always_nodes': [], 'workflow_job_template': 1, 'unified_job_template': 10, 'summary_fields': { 'unified_job_template': { 'unified_job_type': 'job' } } }, { 'id': 12, 'success_nodes': [], 'failure_nodes': [], 'always_nodes': [], 'workflow_job_template': 1, 'unified_job_template': 10, 'summary_fields': { 'unified_job_template': { 'unified_job_type': 'job' } } }, ] UPDATED_WORKFLOW = [ { u'project': 1, u'always': [ { u'project': 2, u'success': [ { u'job_template': 4 } ], u'failure': [ { u'job_template': 5, u'success': [ { u'job_template': 6 }, { u'job_template': 7 } ] } ] } ] }, { u'inventory_source': 8, u'success': [ { u'inventory_source': 9, u'always': [ { u'job_template': 10, } ] }, { u'inventory_source': 9, u'always': [ { u'job_template': 10, } ] } ] } ] UPDATED_RESULT = [ { u'project': 1, u'always_nodes': [ { u'project': 2, u'success_nodes': [ { u'job_template': 4 } ], u'failure_nodes': [ { u'job_template': 5, u'success_nodes': [ { u'job_template': 6 }, { u'job_template': 7 } ] } ] } ] }, { u'inventory_source': 8, u'success_nodes': [ { u'job_template': 9, u'always_nodes': [ { u'job_template': 10, } ] }, { u'job_template': 9, u'always_nodes': [ { u'job_template': 10, } ] } ] } ] class NodeResMock(object): def __init__(self, init_list): self.db = {} self.cur_pk = 0 for node in init_list: self.db[node['id']] = node self.cur_pk = max(self.cur_pk, node['id']) self.create_cnt = 0 self.delete_cnt = 0 self.associate_success_cnt = 0 self.associate_failure_cnt = 0 self.associate_always_cnt = 0 def create(self, **kwargs): self.cur_pk += 1 new_record = { 'success_nodes': [], 'failure_nodes': [], 'always_nodes': [], 'id': self.cur_pk, 'summary_fields': { 'unified_job_template': { 'unified_job_type': 'job' } } } new_record.update(kwargs) self.db[new_record['id']] = new_record self.create_cnt += 1 return new_record def list(self, **kwargs): return { 'count': len(self.db), 'results': list(self.db.values()), 'next': None, 'previous': None } def delete(self, pk=None): if not pk: return self.db.pop(pk, None) for record in self.db.values(): for rel in ['success_nodes', 'failure_nodes', 'always_nodes']: try: record[rel].remove(pk) except ValueError: pass self.delete_cnt += 1 def associate_success_node(self, id, child=None): if not child or id not in self.db: return self.db[id]['success_nodes'].append(child) self.associate_success_cnt += 1 def associate_failure_node(self, id, child=None): if not child or id not in self.db: return self.db[id]['failure_nodes'].append(child) self.associate_failure_cnt += 1 def associate_always_node(self, id, child=None): if not child or id not in self.db: return self.db[id]['always_nodes'].append(child) self.associate_always_cnt += 1 class SchemaTests(unittest.TestCase): """ Tests for the schema file in the tower_cli utils """ def setUp(self): self.res = tower_cli.get_resource('workflow') def test_expected_output(self): """ Test that the expected structure is returned from the method to reorganize workflow nodes in a hierarchy """ output = Resource._workflow_node_structure(EXAMPLE_NODE_LIST) self.assertEqual( output, [ { 'job_template': 48, 'id': 1, 'always_nodes': [ { 'id': 2, 'project': 98 } ] } ] ) def _remove_ids(self, input_list): for node in input_list: node.pop('id', None) for rel in ['success_nodes', 'failure_nodes', 'always_nodes']: if rel in node: self._remove_ids(node[rel]) return input_list def test_schema_update(self): node_res_mock = NodeResMock(CUR_WORKFLOW) with mock.patch('tower_cli.resources.workflow.get_resource', return_value=node_res_mock): res = self._remove_ids(self.res.schema(1, node_network=json.dumps(UPDATED_WORKFLOW))) self.assertEqual(res, UPDATED_RESULT) self.assertEqual(node_res_mock.create_cnt, 6) self.assertEqual(node_res_mock.delete_cnt, 7) self.assertEqual(node_res_mock.associate_success_cnt, 4) self.assertEqual(node_res_mock.associate_failure_cnt, 0) self.assertEqual(node_res_mock.associate_always_cnt, 2) class NodeModelTests(unittest.TestCase): """ Tests for workflow nodes """ def setUp(self): self.res = tower_cli.get_resource('node') def test_translation_into_UJT(self): """ Test application of additional decorator in __getattribute__ """ with mock.patch('tower_cli.models.base.BaseResource.write') as mck: mck.return_value = {'id': 589} mck.__name__ = 'create' self.res.create(workflow_job_template=1, job_template=5) self.assertEqual(mck.call_args[1]['unified_job_template'], 5) self.assertEqual(mck.call_args[1]['workflow_job_template'], 1) ansible-tower-cli-3.2.0/tests/test_resources_workflow_job.py000066400000000000000000000064321316523067200244170ustar00rootroot00000000000000from tower_cli import get_resource from tower_cli.api import client from tests.compat import unittest, mock class WorkflowJobTest(unittest.TestCase): def setUp(self): self.res = get_resource('workflow_job') def test_lookup_stdout(self): with client.test_mode as t: t.register_json('/unified_jobs/?order_by=finished&status__in=successful%2Cfailed%2Cerror', {'count': 2, 'results': [ {'id': 1, 'name': 'Durham, NC'}, {'id': 2, 'name': 'Austin, TX'} ], 'next': None, 'previous': None}) ret = self.res.lookup_stdout() self.assertIn('Durham, NC', ret) self.assertIn('Austin, TX', ret) self.assertEqual(len(ret.split('\n')), 7) def test_lookup_stdout_not_full(self): with client.test_mode as t: t.register_json('/unified_jobs/?order_by=finished&status__in=successful%2Cfailed%2Cerror', {'count': 2, 'results': [ {'id': 1, 'name': 'Durham, NC'}, {'id': 2, 'name': 'Austin, TX'} ], 'next': None, 'previous': None}) ret = self.res.lookup_stdout(full=False) self.assertIn('Durham, NC', ret) self.assertIn('Austin, TX', ret) self.assertEqual(len(ret.split('\n')), 6) def test_lookup_stdout_start_n_end(self): with client.test_mode as t: t.register_json('/unified_jobs/?order_by=finished&status__in=successful%2Cfailed%2Cerror', {'count': 2, 'results': [ {'id': 1, 'name': 'Durham, NC'}, {'id': 2, 'name': 'Austin, TX'} ], 'next': None, 'previous': None}) ret = self.res.lookup_stdout(start_line=3, end_line=4) self.assertIn('Durham, NC', ret) self.assertNotIn('Austin, TX', ret) self.assertEqual(len(ret.split('\n')), 2) def test_launch(self): with client.test_mode as t: t.register_json('/workflow_job_templates/1/launch/', {'id': 1}, method='POST') self.res.launch(workflow_job_template=1) def test_launch_monitor(self): with client.test_mode as t: t.register_json('/workflow_job_templates/1/launch/', {'id': 1}, method='POST') with mock.patch.object(self.res, 'monitor', mock.MagicMock()) as m: self.res.launch(workflow_job_template=1, monitor=True) assert m.called def test_launch_wait(self): with client.test_mode as t: t.register_json('/workflow_job_templates/1/launch/', {'id': 1}, method='POST') with mock.patch.object(self.res, 'wait', mock.MagicMock()) as m: self.res.launch(workflow_job_template=1, wait=True) assert m.called def test_get_attribute(self): self.res.__getattribute__('summary') with self.assertRaises(AttributeError): self.res.__getattribute__('stdout') ansible-tower-cli-3.2.0/tests/test_utils.py000066400000000000000000000032431316523067200207560ustar00rootroot00000000000000# Copyright 2015, Ansible, Inc. # Luke Sneeringer # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. import click from tower_cli.conf import settings from tower_cli.utils import secho from tests.compat import unittest, mock class SechoTests(unittest.TestCase): """Establish that our wrapper around click.secho works in the way that we expect. """ def test_color_true(self): """Establish that when the color setting is true, that color data is not stripped. """ with settings.runtime_values(color=True): with mock.patch.object(click, 'secho') as click_secho: secho('foo bar baz', fg='green') click_secho.assert_called_once_with('foo bar baz', fg='green') def test_color_false(self): """Establish that when the color setting is false, that color data is stripped. """ with settings.runtime_values(color=False): with mock.patch.object(click, 'secho') as click_secho: secho('foo bar baz', fg='green') click_secho.assert_called_once_with('foo bar baz') ansible-tower-cli-3.2.0/tests/test_utils_datastructures.py000066400000000000000000000022101316523067200241040ustar00rootroot00000000000000# Copyright 2015, Ansible, Inc. # Luke Sneeringer # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. from tower_cli.utils.data_structures import OrderedDict from tests.compat import unittest class OrderedDictTests(unittest.TestCase): """A set of tests to ensure that the OrderedDict subclass that tower-cli provides works as expected. """ def test_dunder_repr(self): """Establish that the OrderedDict __repr__ method works in the way we expect. """ d = OrderedDict() d['foo'] = 'spam' d['bar'] = 'eggs' self.assertEqual(repr(d), "{'foo': 'spam', 'bar': 'eggs'}") ansible-tower-cli-3.2.0/tests/test_utils_debug.py000066400000000000000000000062441316523067200221300ustar00rootroot00000000000000# Copyright 2015, Ansible, Inc. # Luke Sneeringer # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. import click from tower_cli.conf import settings from tower_cli.utils import debug from tests.compat import unittest, mock class LogTests(unittest.TestCase): """A set of tests to establish that the log method works in the way that we expect. """ def test_not_verbose_mode(self): """Establish that this method does nothing if we are not in verbose mode. """ with settings.runtime_values(verbose=False): with mock.patch.object(click, 'secho') as secho: debug.log('foo bar baz') self.assertEqual(secho.call_count, 0) def test_header(self): """Establish that a header echoes the expected string, of correct length. """ s = 'Decided all the things.' with mock.patch.object(click, 'secho') as secho: with settings.runtime_values(verbose=True): debug.log(s, header='decision', fg='blue') self.assertEqual(secho.mock_calls[0][1][0], '*** DECISION: Decided all the things. ' '*****************************************') def test_extra_newlines(self): """Establish that extra newlines are correctly applied if they are requested. """ s = 'All your base are belong to us.' with mock.patch.object(click, 'secho') as secho: with settings.runtime_values(verbose=True): debug.log(s, nl=3) self.assertEqual(secho.mock_calls[0][1][0], 'All your base are belong to us.\n\n') def test_multi_lines(self): """Establish that overly long debug messages will be displayed in multiple lines. """ s = ' '.join(['multi-line'] * 30) expected = '\n'.join([ '*** DETAILS: multi-line multi-line multi-line ' 'multi-line multi-line multi-line ', '*** multi-line multi-line multi-line multi-line ' 'multi-line multi-line *********', '*** multi-line multi-line multi-line multi-line ' 'multi-line multi-line *********', '*** multi-line multi-line multi-line multi-line ' 'multi-line multi-line *********', '*** multi-line multi-line multi-line multi-line ' 'multi-line multi-line *********', ]) with mock.patch.object(click, 'secho') as secho: with settings.runtime_values(verbose=True): debug.log(s, header='details') self.assertEqual(secho.mock_calls[0][1][0], expected) ansible-tower-cli-3.2.0/tests/test_utils_parser.py000066400000000000000000000270571316523067200223430ustar00rootroot00000000000000# -*- coding: utf-8 -*- # Copyright 2015, Ansible, Inc. # Alan Rominger # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. import yaml from tower_cli import exceptions as exc from tower_cli.utils import parser from tower_cli.utils.data_structures import OrderedDict from tests.compat import unittest, mock class ParserTests(unittest.TestCase): """A set of tests to establish that the parser methods read files and combine variables in the intended way. """ def test_many_combinations(self): """Combine yaml with json with bare values, check that key:value pairs are preserved at the end.""" adict = {"a": 1} bdict = {"b": 2} ayml = yaml.dump(adict) bjson = yaml.dump(bdict, default_flow_style=True) cyml = "c: 5" result = parser.process_extra_vars([ayml, bjson, cyml]) rdict = yaml.load(result) self.assertEqual(rdict['a'], 1) self.assertEqual(rdict['b'], 2) yaml_w_comment = "a: b\n# comment\nc: d" self.assertEqual( parser.process_extra_vars([yaml_w_comment], force_json=False), yaml_w_comment ) yaml_w_comment = '{a: b,\n# comment\nc: d}' json_text = '{"z":"p"}' self.assertDictContainsSubset( yaml.load(yaml_w_comment), yaml.load(parser.process_extra_vars( [yaml_w_comment, json_text], force_json=False )) ) # Test that it correctly combines a diverse set of YAML yml1 = "a: 1\n# a comment on second line \nb: 2" yml2 = "c: 3" self.assertEqual( yaml.load(parser.process_extra_vars( [yml1, yml2], force_json=False)), {'a': 1, 'b': 2, 'c': 3} ) # make sure it combined them into valid yaml self.assertFalse("{" in parser.process_extra_vars( [yml1, yml2], force_json=False)) def test_precedence(self): """Test that last value is the one that overwrites the others""" adict = {"a": 1} ayml = yaml.dump(adict) a2dict = {"a": 2} a2yml = yaml.dump(a2dict) result = parser.process_extra_vars([ayml, a2yml]) rdict = yaml.load(result) self.assertEqual(rdict['a'], 2) def test_read_from_file(self): """Give it some with '@' and test that it reads from the file""" mock_open = mock.mock_open() with mock.patch('tower_cli.utils.parser.open', mock_open, create=True): manager = mock_open.return_value.__enter__.return_value manager.read.return_value = 'foo: bar' parser.process_extra_vars(["@fake_file1.yml"]) parser.process_extra_vars(["@fake_file2.yml", "@fake_file3.yml"]) # Ensure that "open" was triggered in test self.assertIn(mock.call("fake_file1.yml", 'r'), mock_open.mock_calls) self.assertIn(mock.call("fake_file2.yml", 'r'), mock_open.mock_calls) self.assertIn(mock.call("fake_file3.yml", 'r'), mock_open.mock_calls) def test_parse_error(self): """Given a yaml file with incorrect syntax, throw a warning""" with self.assertRaises(exc.TowerCLIError): parser.process_extra_vars(["mixing: yaml\nwith=keyval"]) with self.assertRaises(exc.TowerCLIError): parser.process_extra_vars(["incorrect == brackets"]) # but accept data if there are just two equals res = parser.process_extra_vars(['password==pa#exp&U=!9Rop']) self.assertEqual(yaml.load(res)['password'], '=pa#exp&U=!9Rop') with self.assertRaises(exc.TowerCLIError): parser.process_extra_vars(["left_param="]) with self.assertRaises(exc.TowerCLIError): parser.process_extra_vars(["incorrect = =brackets"]) # Do not accept _raw_params with self.assertRaises(exc.TowerCLIError): parser.process_extra_vars(["42"]) def test_handling_bad_data(self): """Check robustness of the parser functions in how it handles empty strings, null values, etc.""" # Verify that all parts of the computational chain can handle None return_dict = parser.parse_kv(None) self.assertEqual(return_dict, {}) return_dict = parser.string_to_dict(None) self.assertEqual(return_dict, {}) # Verrify that all parts of computational chain can handle "" return_dict = parser.parse_kv("") self.assertEqual(return_dict, {}) return_dict = parser.string_to_dict("") self.assertEqual(return_dict, {}) # Check that the behavior is what we want if feeding it an int return_dict = parser.parse_kv("foo=5") self.assertEqual(return_dict, {"foo": 5}) # Check that an empty extra_vars list doesn't blow up return_str = parser.process_extra_vars([]) self.assertEqual(return_str, "") return_str = parser.process_extra_vars([""], force_json=False) self.assertEqual(return_str, "") def test_handling_unicode(self): """Verify that unicode strings are correctly parsed and converted to desired python objects""" input_unicode = u"the_user_name='äöü ÄÖÜ'" return_dict = parser.string_to_dict(input_unicode) self.assertEqual(return_dict, {u'the_user_name': u'äöü ÄÖÜ'}) class TestSplitter_Gen(unittest.TestCase): """Set of strings paired with expected output is ran against the parsing functions in this code in order to verrify desired accuracy. SPLIT_DATA is taken from Ansible tests located in: ansible/test/units/parsing/test_splitter.py within the ansible project source. """ SPLIT_DATA = ( (u'a=b', [u'a=b'], {u'a': u'b'}), (u'a="foo bar"', [u'a="foo bar"'], {u'a': u'foo bar'}), (u'a=b c="foo bar"', [u'a=b', u'c="foo bar"'], {u'a': u'b', u'c': u'foo bar'}), (u'a="echo \\"hello world\\"" b=bar', [u'a="echo \\"hello world\\""', u'b=bar'], {u'a': u'echo "hello world"', u'b': u'bar'}), (u'a="multi\nline"', [u'a="multi\nline"'], {u'a': u'multi\nline'}), (u'a="blank\n\nline"', [u'a="blank\n\nline"'], {u'a': u'blank\n\nline'}), (u'a="blank\n\n\nlines"', [u'a="blank\n\n\nlines"'], {u'a': u'blank\n\n\nlines'}), (u'a="a long\nmessage\\\nabout a thing\n"', [u'a="a long\nmessage\\\nabout a thing\n"'], {u'a': u'a long\nmessage\\\nabout a thing\n'}), (u'a="multiline\nmessage1\\\n" b="multiline\nmessage2\\\n"', [u'a="multiline\nmessage1\\\n"', u'b="multiline\nmessage2\\\n"'], {u'a': 'multiline\nmessage1\\\n', u'b': u'multiline\nmessage2\\\n'}), (u'a={{jinja}}', [u'a={{jinja}}'], {u'a': u'{{jinja}}'}), (u'a="{{ jinja }}"', # edited for reduced scope [u'a={{ jinja }}'], {u'a': u'{{ jinja }}'}), (u'a="{{jinja}}"', [u'a="{{jinja}}"'], {u'a': u'{{jinja}}'}), (u'a="{{ jinja }}{{jinja2}}"', # edited for reduced scope [u'a={{ jinja }}{{jinja2}}'], {u'a': u'{{ jinja }}{{jinja2}}'}), (u'a="{{ jinja }}{{jinja2}}"', [u'a="{{ jinja }}{{jinja2}}"'], {u'a': u'{{ jinja }}{{jinja2}}'}), (u'a={{jinja}} b={{jinja2}}', [u'a={{jinja}}', u'b={{jinja2}}'], {u'a': u'{{jinja}}', u'b': u'{{jinja2}}'}), (u'a="{{jinja}}\n" b="{{jinja2}}\n"', [u'a="{{jinja}}\n"', u'b="{{jinja2}}\n"'], {u'a': u'{{jinja}}\n', u'b': u'{{jinja2}}\n'}), ) CUSTOM_DATA = [ ("test=23 site=example.com", {"test": 23, "site": "example.com"}), ('var: value', {"var": "value"}), # key=value ('test=23 key="white space"', {"test": 23, "key": "white space"}), ("test=23 key='white space'", {"test": 23, "key": "white space"}), ('a="[1, 2, 3, 4, 5]" b="white space" ', {"a": [1, 2, 3, 4, 5], "b": 'white space'}), # YAML list ('a: [1, 2, 3, 4, 5]', {'a': [1, 2, 3, 4, 5]}), # JSON list ('{"a": [6,7,8,9]}', {'a': [6, 7, 8, 9]}), ("{'a': True, 'list_thing': [1, 2, 3, 4]}", {'a': True, 'list_thing': [1, 2, 3, 4]}), ("a: [1, 2, 3, 4, 5]\nb: 'white space' ", {"a": [1, 2, 3, 4, 5], "b": 'white space'}), ] # tests that combine two sources into one COMBINATION_DATA = [ (["a: [1, 2, 3, 4, 5]", "b='white space'"], {"a": [1, 2, 3, 4, 5], "b": 'white space'}), (['{"a":3}', "b='white space'"], # json {"a": 3, "b": 'white space'}), ] def test_parse_list(self): """Run tests on the data from Ansible core project.""" for data in self.SPLIT_DATA: self.assertEqual( parser.string_to_dict(data[0], allow_kv=True), data[2]) def test_custom_parse_list(self): """Custom input-output scenario tests.""" for data in self.CUSTOM_DATA: self.assertEqual( parser.string_to_dict(data[0], allow_kv=True), data[1]) def test_combination_parse_list(self): """Custom input-output scenario tests for 2 sources into one.""" for data in self.COMBINATION_DATA: self.assertEqual( yaml.load(parser.process_extra_vars(data[0])), data[1] ) def test_unicode_dump(self): """Test that data is dumped without unicode character marking.""" for data in self.COMBINATION_DATA: string_rep = parser.process_extra_vars(data[0]) self.assertEqual(yaml.load(string_rep), data[1]) assert "python/unicode" not in string_rep assert "\\n" not in string_rep class TestOrderedDump(unittest.TestCase): """Set of tests for testing function ordered_dump.""" CORRECT_OUTPUT = "g: 6\nf: 5\ne: 4\nd: 3\nc: 2\nb: 1\na: 0\n" def test_output_order(self): """Test that ordered_dump perserves the order of OrderedDict.""" ordered_dict = OrderedDict() for i in reversed('abcdefg'): ordered_dict[i] = ord(i) - ord('a') self.assertEqual(parser.ordered_dump(ordered_dict, Dumper=yaml.SafeDumper, default_flow_style=False), self.CORRECT_OUTPUT) def test_mixture(self): """Test to ensure that both dict and OrderedDict can be parsed by ordered_dump.""" ordered_dict = OrderedDict() ordered_dict['a'] = {} ordered_dict['b'] = OrderedDict() for item in ordered_dict.values(): for i in reversed('abcdefg'): item[i] = ord(i) - ord('a') try: parser.ordered_dump(ordered_dict, Dumper=yaml.SafeDumper, default_flow_style=False) except Exception: self.fail("No exceptions should be raised here.") ansible-tower-cli-3.2.0/tower_cli/000077500000000000000000000000001316523067200170305ustar00rootroot00000000000000ansible-tower-cli-3.2.0/tower_cli/VERSION000066400000000000000000000000061316523067200200740ustar00rootroot000000000000003.2.0 ansible-tower-cli-3.2.0/tower_cli/__init__.py000066400000000000000000000022601316523067200211410ustar00rootroot00000000000000# Copyright 2015, Ansible, Inc. # Luke Sneeringer # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. from __future__ import absolute_import, unicode_literals import importlib import os whereami = os.path.realpath(os.path.dirname(__file__)) __version__ = open('%s/VERSION' % whereami, 'r').read().strip() def get_resource(name): """Return an instance of the requested Resource class. Since all of the resource classes are named `Resource`, this provides a slightly cleaner interface for using these classes via. importing rather than through the CLI. """ module = importlib.import_module('tower_cli.resources.%s' % name) return module.Resource() ansible-tower-cli-3.2.0/tower_cli/api.py000066400000000000000000000277001316523067200201610ustar00rootroot00000000000000# Copyright 2015, Ansible, Inc. # Luke Sneeringer # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. import os import contextlib import copy import functools import json import warnings from datetime import datetime as dt from requests.exceptions import ConnectionError, SSLError from requests.sessions import Session from requests.models import Response from requests.packages import urllib3 from requests.auth import AuthBase from tower_cli import exceptions as exc from tower_cli.conf import settings from tower_cli.utils import data_structures, debug, secho from tower_cli.constants import CUR_API_VERSION TOWER_DATETIME_FMT = r'%Y-%m-%dT%H:%M:%S.%fZ' class TowerTokenAuth(AuthBase): def __init__(self, username, password, cli_client): self.username = username self.password = password self.cli_client = cli_client def _acquire_token(self): return self.cli_client._make_request( 'POST', self.cli_client.prefix + 'authtoken/', [], {'data': json.dumps({'username': self.username, 'password': self.password}), 'headers': {'Content-Type': 'application/json'}} ).json() def _get_auth_token(self): filename = os.path.expanduser('~/.tower_cli_token.json') try: with open(filename) as f: token_json = json.load(f) if not isinstance(token_json, dict) or 'token' not in token_json or 'expires' not in token_json or \ dt.utcnow() > dt.strptime(token_json['expires'], TOWER_DATETIME_FMT): raise Exception("Current token expires.") return 'Token ' + token_json['token'] except Exception as e: debug.log('Acquiring and caching auth token due to:\n%s' % str(e), fg='blue', bold=True) token_json = self._acquire_token() if not isinstance(token_json, dict) or 'token' not in token_json or 'expires' not in token_json: raise exc.AuthError('Invalid Tower auth token format: %s' % json.dumps(token_json)) with open(filename, 'w') as f: json.dump(token_json, f) return 'Token ' + token_json['token'] def __call__(self, r): r.headers['Authorization'] = self._get_auth_token() return r class Client(Session): """A class for making HTTP requests to the Ansible Tower API and returning the responses. This functions as a wrapper around [requests][1], and returns its responses; therefore, interact with response objects to this class the same way you would with objects you get back from `requests.get` or similar. [1]: http://docs.python-requests.org/en/latest/ """ def __init__(self): super(Client, self).__init__() for adapter in self.adapters.values(): adapter.max_retries = 3 def _make_request(self, method, url, args, kwargs): # Decide whether to require SSL verification verify_ssl = True if (settings.verify_ssl is False) or hasattr(settings, 'insecure'): verify_ssl = False elif settings.certificate is not None: verify_ssl = settings.certificate # Call the superclass method. try: with warnings.catch_warnings(): warnings.simplefilter( "ignore", urllib3.exceptions.InsecureRequestWarning) return super(Client, self).request( method, url, *args, verify=verify_ssl, **kwargs) except SSLError as ex: # Throw error if verify_ssl not set to false and server # is not using verified certificate. if settings.verbose: debug.log('SSL connection failed:', fg='yellow', bold=True) debug.log(str(ex), fg='yellow', bold=True, nl=2) if not settings.host.startswith('http'): secho('Suggestion: add the correct http:// or ' 'https:// prefix to the host configuration.', fg='blue', bold=True) raise exc.ConnectionError( 'Could not establish a secure connection. ' 'Please add the server to your certificate ' 'authority.\nYou can run this command without verifying SSL ' 'with the --insecure flag, or permanently disable ' 'verification by the config setting:\n\n ' 'tower-cli config verify_ssl false' ) except ConnectionError as ex: # Throw error if server can not be reached. if settings.verbose: debug.log('Cannot connect to Tower:', fg='yellow', bold=True) debug.log(str(ex), fg='yellow', bold=True, nl=2) raise exc.ConnectionError( 'There was a network error of some kind trying to connect ' 'to Tower.\n\nThe most common reason for this is a settings ' 'issue; is your "host" value in `tower-cli config` correct?\n' 'Right now it is: "%s".' % settings.host ) @property def prefix(self): """Return the appropriate URL prefix to prepend to requests, based on the host provided in settings. """ host = settings.host if '://' not in host: host = 'https://%s' % host.strip('/') elif host.startswith('http://') and settings.verify_ssl: raise exc.TowerCLIError( 'Can not verify ssl with non-https protocol. Change the ' 'verify_ssl configuration setting to continue.' ) return '%s/api/%s/' % (host.rstrip('/'), CUR_API_VERSION) @functools.wraps(Session.request) def request(self, method, url, *args, **kwargs): """Make a request to the Ansible Tower API, and return the response. """ # Piece together the full URL. url = '%s%s' % (self.prefix, url.lstrip('/')) # Ansible Tower expects authenticated requests; add the authentication # from settings if it's provided. kwargs.setdefault( 'auth', TowerTokenAuth( settings.username, settings.password, self ) if settings.use_token else (settings.username, settings.password) ) # POST and PUT requests will send JSON by default; make this # the content_type by default. This makes it such that we don't have # to constantly write that in our code, which gets repetitive. headers = kwargs.get('headers', {}) if method.upper() in ('PATCH', 'POST', 'PUT'): headers.setdefault('Content-Type', 'application/json') kwargs['headers'] = headers # If debugging is on, print the URL and data being sent. debug.log('%s %s' % (method, url), fg='blue', bold=True) if method in ('POST', 'PUT', 'PATCH'): debug.log('Data: %s' % kwargs.get('data', {}), fg='blue', bold=True) if method == 'GET' or kwargs.get('params', None): debug.log('Params: %s' % kwargs.get('params', {}), fg='blue', bold=True) debug.log('') # If this is a JSON request, encode the data value. if headers.get('Content-Type', '') == 'application/json': kwargs['data'] = json.dumps(kwargs.get('data', {})) r = self._make_request(method, url, args, kwargs) # Sanity check: Did the server send back some kind of internal error? # If so, bubble this up. if r.status_code >= 500: raise exc.ServerError('The Tower server sent back a server error. ' 'Please try again later.') # Sanity check: Did we fail to authenticate properly? # If so, fail out now; this is always a failure. if r.status_code == 401: raise exc.AuthError('Invalid Tower authentication credentials.') # Sanity check: Did we get a forbidden response, which means that # the user isn't allowed to do this? Report that. if r.status_code == 403: raise exc.Forbidden("You don't have permission to do that.") # Sanity check: Did we get a 404 response? # Requests with primary keys will return a 404 if there is no response, # and we want to consistently trap these. if r.status_code == 404: raise exc.NotFound('The requested object could not be found.') # Sanity check: Did we get a 405 response? # A 405 means we used a method that isn't allowed. Usually this # is a bad request, but it requires special treatment because the # API sends it as a logic error in a few situations (e.g. trying to # cancel a job that isn't running). if r.status_code == 405: raise exc.MethodNotAllowed( "The Tower server says you can't make a request with the " "%s method to that URL (%s)." % (method, url), ) # Sanity check: Did we get some other kind of error? # If so, write an appropriate error message. if r.status_code >= 400: raise exc.BadRequest( 'The Tower server claims it was sent a bad request.\n\n' '%s %s\nParams: %s\nData: %s\n\nResponse: %s' % (method, url, kwargs.get('params', None), kwargs.get('data', None), r.content.decode('utf8')) ) # Django REST Framework intelligently prints API keys in the # order that they are defined in the models and serializer. # # We want to preserve this behavior when it is possible to do so # with minimal effort, because while the order has no explicit meaning, # we make some effort to order keys in a convenient manner. # # To this end, make this response into an APIResponse subclass # (defined below), which has a `json` method that doesn't lose key # order. r.__class__ = APIResponse # Return the response object. return r @property @contextlib.contextmanager def test_mode(self): """Replace the HTTP adapters with a fauxquests.FauxAdapter, which will make the client into a faux client. """ # Import this here, because we don't want to require fauxquests # in order for the app to work. from fauxquests.adapter import FauxAdapter with settings.runtime_values(host='20.12.4.21', username='meagan', password='This is the best wine.', verbose=False, format='json'): adapters = copy.copy(self.adapters) faux_adapter = FauxAdapter( url_pattern=self.prefix.rstrip('/') + '%s', ) try: self.adapters.clear() self.mount('https://', faux_adapter) self.mount('http://', faux_adapter) yield faux_adapter finally: self.adapters = adapters class APIResponse(Response): """A Response subclass which preseves JSON key order (but makes no other changes). """ def json(self, **kwargs): kwargs.setdefault('object_pairs_hook', data_structures.OrderedDict) try: return super(APIResponse, self).json(**kwargs) except Exception: kwargs.pop('object_pairs_hook', None) return super(APIResponse, self).json(**kwargs) client = Client() ansible-tower-cli-3.2.0/tower_cli/cli/000077500000000000000000000000001316523067200175775ustar00rootroot00000000000000ansible-tower-cli-3.2.0/tower_cli/cli/__init__.py000066400000000000000000000000001316523067200216760ustar00rootroot00000000000000ansible-tower-cli-3.2.0/tower_cli/cli/action.py000066400000000000000000000062431316523067200214330ustar00rootroot00000000000000# Copyright 2017, Ansible by Red Hat # Alan Rominger # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. import click from click.formatting import join_options from tower_cli.conf import SETTINGS_PARMS class ActionSubcommand(click.Command): """A Command subclass that adds support for the concept that invocation without arguments assumes `--help`. This code is adapted by taking code from click.MultiCommand and placing it here, to get just the --help functionality and nothing else. """ def __init__(self, name=None, no_args_is_help=True, **kwargs): self.no_args_is_help = no_args_is_help super(ActionSubcommand, self).__init__(name=name, **kwargs) def parse_args(self, ctx, args): """Parse arguments sent to this command. The code for this method is taken from MultiCommand: https://github.com/mitsuhiko/click/blob/master/click/core.py It is Copyright (c) 2014 by Armin Ronacher. See the license: https://github.com/mitsuhiko/click/blob/master/LICENSE """ if not args and self.no_args_is_help and not ctx.resilient_parsing: click.echo(ctx.get_help()) ctx.exit() return super(ActionSubcommand, self).parse_args(ctx, args) def format_options(self, ctx, formatter): """Monkey-patch click's format_options method to support option categorization. """ field_opts = [] global_opts = [] local_opts = [] other_opts = [] for param in self.params: if param.name in SETTINGS_PARMS: opts = global_opts elif getattr(param, 'help', None) and param.help.startswith('[FIELD]'): opts = field_opts param.help = param.help[len('[FIELD]'):] else: opts = local_opts rv = param.get_help_record(ctx) if rv is None: continue else: opts.append(rv) if self.add_help_option: help_options = self.get_help_option_names(ctx) if help_options: other_opts.append([join_options(help_options)[0], 'Show this message and exit.']) if field_opts: with formatter.section('Field Options'): formatter.write_dl(field_opts) if local_opts: with formatter.section('Local Options'): formatter.write_dl(local_opts) if global_opts: with formatter.section('Global Options'): formatter.write_dl(global_opts) if other_opts: with formatter.section('Other Options'): formatter.write_dl(other_opts) ansible-tower-cli-3.2.0/tower_cli/cli/base.py000066400000000000000000000052501316523067200210650ustar00rootroot00000000000000# Copyright 2017, Ansible by Red Hat # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. import os import sys import click import tower_cli from tower_cli import __version__ from tower_cli.utils import secho from tower_cli.cli.resource import ResSubcommand from tower_cli.cli import misc class TowerCLI(click.MultiCommand): """Tower CLI is a command-line interface tool for interacting with [Ansible Tower][1]. It allows basic CRUD operations and job control from the Unix shell. [1]: http://www.ansible.com/tower/ """ def _get_all_res(self): pass def _get_all_misc_cmds(self): pass def list_commands(self, ctx): """Return a list of commands present in the commands and resources folders, but not subcommands. """ answer = set() for filename in os.listdir('%s/%s/' % (tower_cli.whereami, 'resources')): if filename.endswith('.py') and not filename.startswith('_'): res = tower_cli.get_resource(filename[:-3]) if not getattr(res, 'internal', False): answer.add(filename[:-3]) for cmd_name in misc.__all__: answer.add(cmd_name) return sorted(answer) def get_command(self, ctx, name): """Given a command identified by its name, import the appropriate module and return the decorated command. Resources are automatically commands, but if both a resource and a command are defined, the command takes precedence. """ # First, attempt to get a basic command from `tower_cli.api.misc`. if name in misc.__all__: return getattr(misc, name) # No command was found; try to get a resource. try: resource = tower_cli.get_resource(name) return ResSubcommand(resource) except ImportError: pass # Okay, we weren't able to find a command. secho('No such command: %s.' % name, fg='red', bold=True) sys.exit(2) def invoke(self, ctx): if ctx.params.get('version', False): click.echo('Tower CLI %s' % __version__) else: return super(TowerCLI, self).invoke(ctx) ansible-tower-cli-3.2.0/tower_cli/cli/misc.py000066400000000000000000000162431316523067200211120ustar00rootroot00000000000000# Copyright 2017, Ansible by Red Hat # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. import os import stat import warnings import click import six from requests.exceptions import RequestException from tower_cli import __version__, exceptions as exc from tower_cli.api import client from tower_cli.conf import with_global_options, Parser, settings from tower_cli.utils import secho __all__ = ['version', 'config'] @click.command() @with_global_options def version(**kwargs): """Display version information.""" # Print out the current version of Tower CLI. click.echo('Tower CLI %s' % __version__) # Attempt to connect to the Ansible Tower server. # If we succeed, print a version; if not, generate a failure. try: r = client.get('/config/') click.echo('Ansible Tower %s' % r.json()['version']) except RequestException as ex: raise exc.TowerCLIError('Could not connect to Ansible Tower.\n%s' % six.text_type(ex)) def _echo_setting(key): """Echo a setting to the CLI.""" value = getattr(settings, key) secho('%s: ' % key, fg='magenta', bold=True, nl=False) secho( six.text_type(value), bold=True, fg='white' if isinstance(value, six.text_type) else 'cyan', ) # Note: This uses `click.command`, not `tower_cli.utils.decorators.command`, # because we don't want the "global" options that t.u.d.command adds. @click.command() @click.argument('key', required=False) @click.argument('value', required=False) @click.option('global_', '--global', is_flag=True, help='Write this config option to the global configuration. ' 'Probably will require sudo.\n' 'Deprecated: Use `--scope=global` instead.') @click.option('--scope', type=click.Choice(['local', 'user', 'global']), default='user', help='The config file to write. ' '"local" writes to a config file in the local ' 'directory; "user" writes to the home directory,' ' and "global" to a system-wide directory ' '(probably requires sudo).') @click.option('--unset', is_flag=True, help='Remove reference to this configuration option from ' 'the config file.') def config(key=None, value=None, scope='user', global_=False, unset=False): """Read or write tower-cli configuration. `tower config` saves the given setting to the appropriate Tower CLI; either the user's ~/.tower_cli.cfg file, or the /etc/tower/tower_cli.cfg file if --global is used. Writing to /etc/tower/tower_cli.cfg is likely to require heightened permissions (in other words, sudo). """ # If the old-style `global_` option is set, issue a deprecation notice. if global_: scope = 'global' warnings.warn('The `--global` option is deprecated and will be ' 'removed. Use `--scope=global` to get the same effect.', DeprecationWarning) # If no key was provided, print out the current configuration # in play. if not key: seen = set() parser_desc = { 'runtime': 'Runtime options.', 'local': 'Local options (set with `tower-cli config ' '--scope=local`; stored in .tower_cli.cfg of this ' 'directory or a parent)', 'user': 'User options (set with `tower-cli config`; stored in ' '~/.tower_cli.cfg).', 'global': 'Global options (set with `tower-cli config ' '--scope=global`, stored in /etc/tower/tower_cli.cfg).', 'defaults': 'Defaults.', } # Iterate over each parser (English: location we can get settings from) # and print any settings that we haven't already seen. # # We iterate over settings from highest precedence to lowest, so any # seen settings are overridden by the version we iterated over already. click.echo('') for name, parser in zip(settings._parser_names, settings._parsers): # Determine if we're going to see any options in this # parser that get echoed. will_echo = False for option in parser.options('general'): if option in seen: continue will_echo = True # Print a segment header if will_echo: secho('# %s' % parser_desc[name], fg='green', bold=True) # Iterate over each option in the parser and, if we haven't # already seen an option at higher precedence, print it. for option in parser.options('general'): if option in seen: continue _echo_setting(option) seen.add(option) # Print a nice newline, for formatting. if will_echo: click.echo('') return # Sanity check: Is this a valid configuration option? If it's not # a key we recognize, abort. if not hasattr(settings, key): raise exc.TowerCLIError('Invalid configuration option "%s".' % key) # Sanity check: The combination of a value and --unset makes no # sense. if value and unset: raise exc.UsageError('Cannot provide both a value and --unset.') # If a key was provided but no value was provided, then just # print the current value for that key. if key and not value and not unset: _echo_setting(key) return # Okay, so we're *writing* a key. Let's do this. # First, we need the appropriate file. filename = os.path.expanduser('~/.tower_cli.cfg') if scope == 'global': if not os.path.isdir('/etc/tower/'): raise exc.TowerCLIError('/etc/tower/ does not exist, and this ' 'command cowardly declines to create it.') filename = '/etc/tower/tower_cli.cfg' elif scope == 'local': filename = '.tower_cli.cfg' # Read in the appropriate config file, write this value, and save # the result back to the file. parser = Parser() parser.add_section('general') parser.read(filename) if unset: parser.remove_option('general', key) else: parser.set('general', key, value) with open(filename, 'w') as config_file: parser.write(config_file) # Give rw permissions to user only fix for issue number 48 try: os.chmod(filename, stat.S_IRUSR | stat.S_IWUSR) except Exception as e: warnings.warn( 'Unable to set permissions on {0} - {1} '.format(filename, e), UserWarning ) click.echo('Configuration updated successfully.') ansible-tower-cli-3.2.0/tower_cli/cli/resource.py000066400000000000000000000356451316523067200220150ustar00rootroot00000000000000# Copyright 2017, Ansible by Red Hat # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. from __future__ import absolute_import, division import functools import inspect import json import yaml import math import re from copy import copy import six import click from tower_cli.conf import settings, with_global_options from tower_cli.utils import parser, debug, secho from tower_cli.cli.action import ActionSubcommand from tower_cli.exceptions import MultipleRelatedError from tower_cli.cli.types import StructuredInput try: basestring except NameError: basestring = None class ResSubcommand(click.MultiCommand): """A subcommand that implements all command methods on the Resource. """ def __init__(self, resource, *args, **kwargs): self.resource = resource self.resource_name = getattr( resource, 'resource_name', resource.__module__.split('.')[-1] ) self.resource_name = self.resource_name.replace('_', ' ') super(ResSubcommand, self).__init__( *args, help=self.resource.cli_help, **kwargs ) def list_commands(self, ctx): """Return a list of all methods decorated with the @resources.command decorator. """ return self.resource.commands def _auto_help_text(self, help_text): """Given a method with a docstring, convert the docstring to more CLI appropriate wording, and also disambiguate the word "object" on the base class docstrings. """ # Delete API docs if there are any. api_doc_delimiter = '=====API DOCS=====' begin_api_doc = help_text.find(api_doc_delimiter) if begin_api_doc >= 0: end_api_doc = help_text.rfind(api_doc_delimiter) + len(api_doc_delimiter) help_text = help_text[:begin_api_doc] + help_text[end_api_doc:] # Convert the word "object" to the appropriate type of # object being modified (e.g. user, organization). an_prefix = ('a', 'e', 'i', 'o') if not self.resource_name.lower().startswith(an_prefix): help_text = help_text.replace('an object', 'a %s' % self.resource_name) if self.resource_name.lower().endswith('y'): help_text = help_text.replace( 'objects', '%sies' % self.resource_name[:-1], ) help_text = help_text.replace('object', self.resource_name) # Convert some common Python terms to their CLI equivalents. help_text = help_text.replace('keyword argument', 'option') help_text = help_text.replace('raise an exception', 'abort with an error') # Convert keyword arguments specified in docstrings enclosed # by backticks to switches. for match in re.findall(r'`([\w_]+)`', help_text): option = '--%s' % match.replace('_', '-') help_text = help_text.replace('`%s`' % match, option) # Done; return the new help text. return help_text def _echo_method(self, method): """Given a method, return a method that runs the internal method and echos the result. """ @functools.wraps(method) def func(*args, **kwargs): # Echo warning if this method is deprecated. if getattr(method, 'deprecated', False): debug.log('This method is deprecated in Tower 3.0.', header='warning') result = method(*args, **kwargs) # If this was a request that could result in a modification # of data, print it in Ansible coloring. color_info = {} if isinstance(result, dict) and 'changed' in result: if result['changed']: color_info['fg'] = 'yellow' else: color_info['fg'] = 'green' # Piece together the result into the proper format. format = getattr(self, '_format_%s' % (getattr(method, 'format_freezer', None) or settings.format)) output = format(result) # Perform the echo. secho(output, **color_info) return func def _format_json(self, payload): """Convert the payload into a JSON string with proper indentation and return it. """ return json.dumps(payload, indent=2) def _format_yaml(self, payload): """Convert the payload into a YAML string with proper indentation and return it. """ return parser.ordered_dump(payload, Dumper=yaml.SafeDumper, default_flow_style=False) def _format_id(self, payload): """Echos only the id""" if 'id' in payload: return str(payload['id']) if 'results' in payload and payload['count'] == 1: return str(payload['results'][0]['id']) raise MultipleRelatedError( 'Can not use id format when multiple objects are returned.') @staticmethod def get_print_value(data, col): value = data.get(col, 'N/A') is_bool = isinstance(value, bool) if basestring and isinstance(value, basestring) and type(value) is not str: value = value.encode('utf-8') # handle python 2 encoding problem value = '%s' % value if is_bool: value = value.lower() return value def _format_human(self, payload): """Convert the payload into an ASCII table suitable for printing on screen and return it. """ page = None total_pages = None # What are the columns we will show? columns = [field.name for field in self.resource.fields if field.display or settings.description_on and field.name == 'description'] columns.insert(0, 'id') # Save a dictionary-by-name of fields for later use fields_by_name = {} for field in self.resource.fields: fields_by_name[field.name] = field # Sanity check: If there is a "changed" key in our payload # and little else, we print a short message and not a table. # this specifically applies to deletion if 'changed' in payload and 'id' not in payload: return 'OK. (changed: {0})'.format( six.text_type(payload['changed']).lower(), ) # Sanity check: If there is no ID and no results, then this # is unusual output; keep our table formatting, but plow # over the columns-as-keys stuff above. # this originally applied to launch/status/update methods # but it may become deprecated if 'id' not in payload and 'results' not in payload: columns = [i for i in payload.keys()] # Get our raw rows into a standard format. if 'results' in payload: raw_rows = payload['results'] if payload.get('count', 0) > len(payload['results']): prev = payload.get('previous', 0) or 0 page = prev + 1 count = payload['count'] if payload.get('next', None): total_pages = math.ceil(count / len(raw_rows)) else: total_pages = page else: raw_rows = [payload] # If we have no rows to display, return this information # and don't do any further processing. if not raw_rows: return 'No records found.' # Determine the width for each column. widths = {} for col in columns: widths[col] = max( len(col), *[len(self.get_print_value(i, col)) for i in raw_rows] ) fd = fields_by_name.get(col, None) if fd is not None and fd.col_width is not None: widths[col] = fd.col_width # It's possible that the column widths will exceed our terminal # width; if so, reduce column widths accordingly. # TODO: Write this. # Put together the divider row. # This is easy and straightforward: it's simply a table divider # using the widths calculated. divider_row = '' for col in columns: divider_row += '=' * widths[col] + ' ' divider_row.rstrip() # Put together the header row. # This is also easy and straightforward; simply center the # headers (which str.format does for us!). header_row = '' for col in columns: header_row += ('{0:^%d}' % widths[col]).format(col) + ' ' header_row.rstrip() # Piece together each row of data. data_rows = [] for raw_row in raw_rows: data_row = '' for col in columns: template = '{0:%d}' % widths[col] value = self.get_print_value(raw_row, col) # Right-align certain native data types if isinstance(raw_row.get(col, 'N/A'), (bool, int)): template = template.replace('{0:', '{0:>') # Truncate the cell entry if exceeds manually # specified column width limit fd = fields_by_name.get(col, None) if fd is not None and fd.col_width is not None: str_value = template.format(value or '') if len(str_value) > fd.col_width: value = str_value[:fd.col_width] data_row += template.format(value or '') + ' ' data_rows.append(data_row.rstrip()) # Result the resulting table. response = '\n'.join(( divider_row, header_row, divider_row, '\n'.join(data_rows), divider_row, )) if page: response += '(Page %d of %d.)' % (page, total_pages) if payload.get('changed', False): response = 'Resource changed.\n' + response return response def get_command(self, ctx, name): """Retrieve the appropriate method from the Resource, decorate it as a click command, and return that method. """ # Sanity check: Does a method exist corresponding to this # command? If not, None is returned for click to raise # exception. if not hasattr(self.resource, name): return None # Get the method. method = getattr(self.resource, name) # Get any attributes that were given at command-declaration # time. attrs = getattr(method, '_cli_command_attrs', {}) # If the help message comes from the docstring, then # convert it into a message specifically for this resource. help_text = inspect.getdoc(method) attrs['help'] = self._auto_help_text(help_text or '') # On some methods, we ignore the defaults, which are intended # for writing and not reading; process this. ignore_defaults = attrs.pop('ignore_defaults', False) # Wrap the method, such that it outputs its final return # value rather than returning it. new_method = self._echo_method(method) # Soft copy the "__click_params__", if any exist. # This is the internal holding method that the click library # uses to store @click.option and @click.argument directives # before the method is converted into a command. # # Because self._echo_method uses @functools.wraps, this is # actually preserved; the purpose of copying it over is # so we can get our resource fields at the top of the help; # the easiest way to do this is to load them in before the # conversion takes place. (This is a happy result of Armin's # work to get around Python's processing decorators # bottom-to-top.) click_params = getattr(method, '__click_params__', []) new_method.__click_params__ = copy(click_params) new_method = with_global_options(new_method) # Write options based on the fields available on this resource. fao = attrs.pop('use_fields_as_options', True) if fao: for field in reversed(self.resource.fields): if not field.is_option: continue # If we got an iterable rather than a boolean, # then it is a list of fields to use; check for # presence in that list. if not isinstance(fao, bool) and field.name not in fao: continue # Create the initial arguments based on the # option value. If we have a different key to use # (which is what gets routed to the Tower API), # ensure that is the first argument. args = [field.option] if field.key: args.insert(0, field.key) # short name aliases for common flags short_fields = { 'name': 'n', 'description': 'd', 'inventory': 'i', 'extra_vars': 'e' } if field.name in short_fields: args.append('-'+short_fields[field.name]) # Apply the option to the method. option_help = field.help if isinstance(field.type, StructuredInput): option_help += ' Use @ to get JSON or YAML from a file.' if field.required: option_help = '[REQUIRED] ' + option_help elif field.read_only: option_help = '[READ ONLY] ' + option_help option_help = '[FIELD]' + option_help click.option( *args, default=field.default if not ignore_defaults else None, help=option_help, type=field.type, show_default=field.show_default, multiple=field.multiple, is_eager=False )(new_method) # Make a click Command instance using this method # as the callback, and return it. cmd = click.command(name=name, cls=ActionSubcommand, **attrs)(new_method) # If this method has a `pk` positional argument, # then add a click argument for it. code = six.get_function_code(method) if 'pk' in code.co_varnames: click.argument('pk', nargs=1, required=False, type=str, metavar='[ID]')(cmd) # Done; return the command. return cmd ansible-tower-cli-3.2.0/tower_cli/cli/types.py000066400000000000000000000143671316523067200213300ustar00rootroot00000000000000# Copyright 2015, Ansible, Inc. # Luke Sneeringer # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. from __future__ import absolute_import, unicode_literals import os import re import six import click import tower_cli from tower_cli import exceptions as exc from tower_cli.utils import debug from tower_cli.utils.parser import string_to_dict from tower_cli.compat import OrderedDict class File(click.File): """A subclass of click.File that adds `os.path.expanduser`.""" __name__ = 'file' def convert(self, value, param, ctx): if hasattr(value, 'read') or hasattr(value, 'write'): return value value = os.path.expanduser(value) return super(File, self).convert(value, param, ctx) class Variables(click.File): """Allows reading from a file optionally with '@' prefix, otherwise passes through string as-is """ name = 'variables' __name__ = 'variables' def convert(self, value, param, ctx): """Return file content if file, else, return value as-is """ # Protect against corner cases of invalid inputs if not isinstance(value, str): return value if isinstance(value, six.binary_type): value = value.decode('UTF-8') # Read from a file under these cases if value.startswith('@'): filename = os.path.expanduser(value[1:]) file_obj = super(Variables, self).convert(filename, param, ctx) if hasattr(file_obj, 'read'): # Sometimes click.File may return a buffer and not a string return file_obj.read() return file_obj # No file, use given string return value class StructuredInput(Variables): """A subclass of Variables that deserializes JSON/YAML-formatted string/file content into python objects.""" name = 'structured_input' __name__ = 'structured_input' def convert(self, value, param, ctx): s = super(StructuredInput, self).convert(value, param, ctx) try: return string_to_dict(s, allow_kv=False) except Exception: raise exc.UsageError( 'Error loading structured input given by %s parameter. Please ' 'check the validity of your JSON/YAML format.' % param.name ) class MappedChoice(click.Choice): """A subclass of click.Choice that allows a distinction between the choice sent to the method and the choice typed on the CLI. """ __name__ = 'mapped_choice' def __init__(self, choices): # Call the superclass constructor and send it the **values**. # This will make the `click.Choice` things work as expected, since # the values are what we are interested in showing the person using # the command. choices = OrderedDict(choices) super(MappedChoice, self).__init__([i for i in choices.values()]) # Save the keys the MappedChoice instance, and the `convert` method # will convert from values to keys. self.actual_choices = [i for i in choices.keys()] def convert(self, value, param, ctx): """Match against the appropriate choice value using the superclass implementation, and then return the actual choice. """ choice = super(MappedChoice, self).convert(value, param, ctx) ix = self.choices.index(choice) return self.actual_choices[ix] class Related(click.types.ParamType): """A subclass of click.types.ParamType that represents a value related to another resource. """ __name__ = 'related' name = 'related' def __init__(self, resource_name): super(Related, self).__init__() self.resource_name = resource_name def convert(self, value, param, ctx): """Return the appropriate integer value. If a non-integer is provided, attempt a name-based lookup and return the primary key. """ resource = tower_cli.get_resource(self.resource_name) # Ensure that None is passed through without trying to # do anything. if value is None: return None # If we were already given an integer, do nothing. # This ensures that the convert method is idempotent. if isinstance(value, int): return value # Do we have a string that contains only digits? # If so, then convert it to an integer and return it. if re.match(r'^[\d]+$', value): return int(value) # Special case to allow disassociations if value == 'null': return value # Okay, we have a string. Try to do a name-based lookup on the # resource, and return back the ID that we get from that. # # This has the chance of erroring out, which is fine. try: debug.log('The %s field is given as a name; ' 'looking it up.' % param.name, header='details') lookup_data = {resource.identity[-1]: value} rel = resource.get(**lookup_data) except exc.MultipleResults as ex: raise exc.MultipleRelatedError( 'Cannot look up {0} exclusively by name, because multiple {0} ' 'objects exist with that name.\n' 'Please send an ID. You can get the ID for the {0} you want ' 'with:\n' ' tower-cli {0} list --name "{1}"'.format(self.resource_name, value), ) except exc.TowerCLIError as ex: raise exc.RelatedError('Could not get %s. %s' % (self.resource_name, str(ex))) # Done! Return the ID. return rel['id'] def get_metavar(self, param): return self.resource_name.upper() ansible-tower-cli-3.2.0/tower_cli/compat.py000066400000000000000000000023641316523067200206720ustar00rootroot00000000000000# Copyright 2015, Ansible, Inc. # Luke Sneeringer # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. # Import OrderedDict from the standard library if possible, and from # the ordereddict library (required on Python 2.6) otherwise. try: from collections import OrderedDict # NOQA except ImportError: # Python < 2.7 from ordereddict import OrderedDict # NOQA # Import simplejson if we have it (Python 2.6), and use json from the # standard library otherwise. # # Note: Python 2.6 does have a JSON library, but it lacks `object_pairs_hook` # as a keyword argument to `json.loads`, so we still need simplejson on # Python 2.6. import sys if sys.version_info < (2, 7): import simplejson as json # NOQA else: import json # NOQA ansible-tower-cli-3.2.0/tower_cli/conf.py000066400000000000000000000420011316523067200203240ustar00rootroot00000000000000# Copyright 2015, Ansible, Inc. # Luke Sneeringer # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. from __future__ import absolute_import import click import contextlib import copy import os import stat import warnings from functools import wraps import six from six.moves import configparser from six import StringIO __all__ = ['settings', 'with_global_options', 'pop_option'] class Parser(configparser.ConfigParser): """ConfigParser subclass that doesn't strictly require section headers. """ def _read(self, fp, fpname): """Read the configuration from the given file. If the file lacks any section header, add a [general] section header that encompasses the whole thing. """ # Attempt to read the file using the superclass implementation. # # Check the permissions of the file we are considering reading # if the file exists and the permissions expose it to reads from # other users, raise a warning if os.path.isfile(fpname): file_permission = os.stat(fpname) if fpname != '/etc/tower/tower_cli.cfg' and ( (file_permission.st_mode & stat.S_IRGRP) or (file_permission.st_mode & stat.S_IROTH) ): warnings.warn('File {0} readable by group or others.' .format(fpname), RuntimeWarning) # If it doesn't work because there's no section header, then # create a section header and call the superclass implementation # again. try: return configparser.ConfigParser._read(self, fp, fpname) except configparser.MissingSectionHeaderError: fp.seek(0) string = '[general]\n%s' % fp.read() flo = StringIO(string) # flo == file-like object return configparser.ConfigParser._read(self, flo, fpname) class Settings(object): """A class that understands configurations provided to tower-cli through configuration files or runtime parameters. A signleton object ``tower_cli.conf.settings`` will be instantiated and used. The 5 levels of precedence for settings, listing from least to greatest, are: - defaults: Default values provided - global: Contents parsed from .ini-formatted file ``/etc/tower/tower_cli.cfg`` if exists. - user: Contents parsed from .ini-formatted file ``~/.tower_cli.cfg`` if exists. - local: Contents parsed from .ini-formatted file ``.tower_cli.cfg`` if exists in the present working directory or any parent directories. - runtime: keyworded arguements provided by ``settings.runtime_values`` context manager. Note that .ini configuration file should follow the specified format in order to be correctly parsed: .. code-block:: bash [general] = = ... """ _parser_names = ['runtime', 'local', 'user', 'global', 'defaults'] @staticmethod def _new_parser(defaults=None): if defaults: p = Parser(defaults=defaults) else: p = Parser() p.add_section('general') return p def __init__(self): """Create the settings object, and read from appropriate files as well as from `sys.argv`. """ self._cache = {} # Initialize the data dictionary for the default level # precedence (that is, the bottom of the totem pole). defaults = { 'color': 'true', 'format': 'human', 'host': '127.0.0.1', 'password': '', 'username': '', 'verify_ssl': 'true', 'verbose': 'false', 'description_on': 'false', 'certificate': '', 'use_token': 'false', } self._defaults = self._new_parser(defaults=defaults) # If there is a global settings file, initialize it. self._global = self._new_parser() if os.path.isdir('/etc/tower/'): # Sanity check: Try to get a list of files in `/etc/tower/`. # # The default Tower installation caused `/etc/tower/` to have # extremely restrictive permissions, since it has its own user # and group and has a chmod of 0750. # # This makes it very easy for a user to fall into the mistake # of writing a config file under sudo which they then cannot read, # which could lead to difficult-to-troubleshoot situations. # # Therefore, check for that particular problem and give a warning # if we're in that situation. try: os.listdir('/etc/tower/') except OSError: warnings.warn('/etc/tower/ is present, but not readable with ' 'current permissions. Any settings defined in ' '/etc/tower/tower_cli.cfg will not be honored.', RuntimeWarning) # If there is a global settings file for Tower CLI, read in its # contents. self._global.read('/etc/tower/tower_cli.cfg') # Initialize a parser for the user settings file. self._user = self._new_parser() # If there is a user settings file, read it into the parser object. user_filename = os.path.expanduser('~/.tower_cli.cfg') self._user.read(user_filename) # Initialize a parser for the local settings file. self._local = self._new_parser() # If there is a local settings file in the current working directory # or any parent, read it into the parser object. # # As a first step, we need to get each of the parents. cwd = os.getcwd() local_dirs = [] for i in range(0, len(cwd.split('/'))): local_dir = '/'.join(cwd.split('/')[0:i + 1]) if len(local_dir) == 0: local_dir = '/' # Sanity check: if this directory corresponds to our global or # user directory, skip it. if local_dir in (os.path.expanduser('~'), '/etc/tower'): continue # Add this directory to the list. local_dirs.append(local_dir) # Iterate over each potential local config file and attempt to read # it (most won't exist, which is fine). for local_dir in local_dirs: local_filename = '%s/.tower_cli.cfg' % local_dir self._local.read(local_filename) # Put a stubbed runtime parser in. self._runtime = self._new_parser() def __getattr__(self, key): """Return the approprate value, intelligently type-casted in the case of numbers or booleans. """ # Sanity check: Have I cached this value? If so, return that. if key in self._cache: return self._cache[key] # Run through each of the parsers and check for a value. Whenever # we actually find a value, try to determine the correct type for it # and cache and return a value of that type. for parser in self._parsers: # Get the value from this parser; if it's None, then this # key isn't present and we move on to the next one. try: value = parser.get('general', key) except configparser.NoOptionError: continue # We have a value; it may or may not be a string, though, so # try to return it as an int, float, or boolean (in that order) # before falling back to the string value. type_method = ('getint', 'getfloat', 'getboolean') for tm in type_method: try: value = getattr(parser, tm)('general', key) break except ValueError: pass # Write the value to the cache, so we don't have to do this lookup # logic on subsequent requests. self._cache[key] = value return self._cache[key] # If we got here, that means that the attribute wasn't found, and # also that there is no default; raise an exception. raise AttributeError('No setting exists: %s.' % key.lower()) @property def _parsers(self): """Return a tuple of all parsers, in order. This is referenced at runtime, to avoid gleefully ignoring the `runtime_values` context manager. """ return tuple([getattr(self, '_%s' % i) for i in self._parser_names]) def set_or_reset_runtime_param(self, key, value): """Maintains the context of the runtime settings for invoking a command. This should be called by a click.option callback, and only called once for each setting for each command invocation. If the setting exists, it follows that the runtime settings are stale, so the entire runtime settings are reset. """ if self._runtime.has_option('general', key): self._runtime = self._new_parser() if value is None: return settings._runtime.set('general', key.replace('tower_', ''), six.text_type(value)) @contextlib.contextmanager def runtime_values(self, **kwargs): """ =====API DOCS===== Context manager that temporarily override runtime level configurations. :param kwargs: Keyword arguments specifying runtime configuration settings. :type kwargs: arbitrary keyword arguments :returns: N/A :Example: >>> import tower_cli >>> from tower_cli.conf import settings >>> with settings.runtime_values(username='user', password='pass'): >>> print(tower_cli.get_resource('credential').list()) =====API DOCS===== """ kwargs = config_from_environment(kwargs) # Coerce all values to strings (to be coerced back by configparser # later) and defenestrate any None values. for k, v in copy.copy(kwargs).items(): # If the value is None, just get rid of it. if v is None: kwargs.pop(k) continue # Remove these keys from the cache, if they are present. self._cache.pop(k, None) # Coerce values to strings. kwargs[k] = six.text_type(v) # Replace the `self._runtime` INI parser with a new one, using # the context manager's kwargs as the "defaults" (there can never # be anything other than defaults, but that isn't a problem for our # purposes because we're using our own precedence system). # # Ensure that everything is put back to rights at the end of the # context manager call. old_runtime_parser = self._runtime try: self._runtime = Parser(defaults=kwargs) self._runtime.add_section('general') yield self finally: # Revert the runtime configparser object. self._runtime = old_runtime_parser # Remove the keys from the cache again, since the settings # have been reverted. for key in kwargs: self._cache.pop(k, None) def config_from_environment(kwargs): """Read tower-cli config values from the environment if present, being careful not to override config values that were explicitly passed in. """ CONFIG_OPTIONS = ('host', 'username', 'password', 'verify_ssl', 'format', 'color', 'verbose', 'description_on', 'certificate', 'use_token') kwargs = copy.copy(kwargs) for k in CONFIG_OPTIONS: if k not in kwargs or kwargs[k] is None: env = 'TOWER_' + k.upper() v = os.getenv(env, None) if v is not None: kwargs[k] = v return kwargs # The primary way to interact with settings is to simply hit the # already constructed settings object. settings = Settings() def _apply_runtime_setting(ctx, param, value): settings.set_or_reset_runtime_param(param.name, value) SETTINGS_PARMS = set([ 'tower_host', 'tower_password', 'format', 'tower_username', 'verbose', 'description_on', 'insecure', 'certificate', 'use_token' ]) def runtime_context_manager(method): @wraps(method) def method_with_context_managed(*args, **kwargs): # Remove the settings before running the method for key in SETTINGS_PARMS: kwargs.pop(key, None) method(*args, **kwargs) # Destroy the runtime settings settings._runtime = settings._new_parser() return method_with_context_managed def with_global_options(method): """Apply the global options that we desire on every method within tower-cli to the given click command. """ # Create global options for the Tower host, username, and password. # # These are runtime options that will override the configuration file # settings. method = click.option( '-h', '--tower-host', help='The location of the Ansible Tower host. ' 'HTTPS is assumed as the protocol unless "http://" is explicitly ' 'provided. This will take precedence over a host provided to ' '`tower config`, if any.', required=False, callback=_apply_runtime_setting, is_eager=True )(method) method = click.option( '-u', '--tower-username', help='Username to use to authenticate to Ansible Tower. ' 'This will take precedence over a username provided to ' '`tower config`, if any.', required=False, callback=_apply_runtime_setting, is_eager=True )(method) method = click.option( '-p', '--tower-password', help='Password to use to authenticate to Ansible Tower. ' 'This will take precedence over a password provided to ' '`tower config`, if any.', required=False, callback=_apply_runtime_setting, is_eager=True )(method) # Create a global verbose/debug option. method = click.option( '-f', '--format', help='Output format. The "human" format is intended for humans ' 'reading output on the CLI; the "json" and "yaml" formats ' 'provide more data, and "id" echos the object id only.', type=click.Choice(['human', 'json', 'yaml', 'id']), required=False, callback=_apply_runtime_setting, is_eager=True )(method) method = click.option( '-v', '--verbose', default=None, help='Show information about requests being made.', is_flag=True, required=False, callback=_apply_runtime_setting, is_eager=True )(method) method = click.option( '--description-on', default=None, help='Show description in human-formatted output.', is_flag=True, required=False, callback=_apply_runtime_setting, is_eager=True )(method) # Create a global SSL warning option. method = click.option( '--insecure', default=None, help='Turn off insecure connection warnings. Set config verify_ssl ' 'to make this permanent.', is_flag=True, required=False, callback=_apply_runtime_setting, is_eager=True )(method) # Create a custom certificate specification option. method = click.option( '--certificate', default=None, help='Path to a custom certificate file that will be used throughout' ' the command. Overwritten by --insecure flag if set.', required=False, callback=_apply_runtime_setting, is_eager=True )(method) method = click.option( '--use-token', default=None, help='Turn on Tower\'s token-based authentication. Set config' ' use_token to make this permanent.', is_flag=True, required=False, callback=_apply_runtime_setting, is_eager=True )(method) # Manage the runtime settings context method = runtime_context_manager(method) # Okay, we're done adding options; return the method. return method def pop_option(function, name): """ Used to remove an option applied by the @click.option decorator. This is useful for when you want to subclass a decorated resource command and *don't* want all of the options provided by the parent class' implementation. """ for option in getattr(function, '__click_params__', tuple()): if option.name == name: function.__click_params__.remove(option) ansible-tower-cli-3.2.0/tower_cli/constants.py000066400000000000000000000017631316523067200214250ustar00rootroot00000000000000# Copyright 2017, Ansible by Red Hat. # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. CUR_API_VERSION = 'v2' LAUNCH_TYPE_CHOICES = [ 'manual', 'relaunch', 'relaunch', 'callback', 'scheduled', 'dependency', 'workflow', 'sync', 'scm' ] STATUS_CHOICES = [ 'new', 'pending', 'waiting', 'running', 'successful', 'failed', 'error', 'canceled' ] INVENTORY_SOURCE_CHOICES = [ '', 'file', 'scm', 'ec2', 'vmware', 'gce', 'azure', 'azure_rm', 'openstack', 'satellite6', 'cloudforms', 'custom' ] ansible-tower-cli-3.2.0/tower_cli/exceptions.py000066400000000000000000000075621316523067200215750ustar00rootroot00000000000000# Copyright 2015, Ansible, Inc. # Luke Sneeringer # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. import click from click._compat import get_text_stderr class TowerCLIError(click.ClickException): """Base exception class for problems raised within Tower CLI. This class adds coloring to exceptions. """ fg = 'red' bg = None bold = True def show(self, file=None): if file is None: file = get_text_stderr() click.secho('Error: %s' % self.format_message(), file=file, fg=self.fg, bg=self.bg, bold=self.bold) class UsageError(TowerCLIError): """An exception class for reporting usage errors. This uses an exit code of 2 in order to match click (which matters more than following the erstwhile "standard" of using 64). """ exit_code = 2 class BadRequest(TowerCLIError): """An exception class for reporting unexpected error codes from Ansible Tower such that 400 <= code < 500. In theory, we should never, ever get these. """ exit_code = 40 class AuthError(TowerCLIError): """An exception class for reporting when a request failed due to an authorization failure. """ exit_code = 41 class Forbidden(TowerCLIError): """An exception class for reporting when a user doesn't have permission to do something. """ exit_code = 43 class NotFound(TowerCLIError): """An exception class for reporting when a request went through without incident, but the requested content could not be found. """ exit_code = 44 class MethodNotAllowed(BadRequest): """An exception class for sending a request to a URL where the URL doesn't accept that method at all. """ exit_code = 45 class MultipleResults(TowerCLIError): """An exception class for reporting when a request that expected one and exactly one result got more than that. """ exit_code = 49 class ServerError(TowerCLIError): """An exception class for reporting server-side errors which are expected to be ephemeral. """ exit_code = 50 class Found(TowerCLIError): """An exception class for when a record already exists, and we were explicitly told that it shouldn't. """ exit_code = 60 class RelatedError(TowerCLIError): """An exception class for errors where we can't find related objects that we expect to find. """ exit_code = 61 class MultipleRelatedError(RelatedError): """An exception class for errors where we try to find a single related object, and get more than one. """ exit_code = 62 class ValidationError(TowerCLIError): """An exception class for invalid values being sent as option switches to Tower CLI. """ exit_code = 64 class CannotStartJob(TowerCLIError): """An exception class for jobs that cannot be started within Tower for whatever reason. """ exit_code = 97 class Timeout(TowerCLIError): """An exception class for timeouts encountered within Tower CLI, usually for monitoring. """ exit_code = 98 class JobFailure(TowerCLIError): """An exception class for job failures that require error codes within the Tower CLI. """ exit_code = 99 class ConnectionError(TowerCLIError): """An exception class to bubble requests errors more nicely, and communicate connection issues to the user. """ exit_code = 120 ansible-tower-cli-3.2.0/tower_cli/models/000077500000000000000000000000001316523067200203135ustar00rootroot00000000000000ansible-tower-cli-3.2.0/tower_cli/models/__init__.py000066400000000000000000000016531316523067200224310ustar00rootroot00000000000000# Copyright 2015, Ansible, Inc. # Luke Sneeringer # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. # # flake8: noqa from __future__ import absolute_import, unicode_literals from tower_cli.models.base import (Resource, MonitorableResource, ExeResource, BaseResource, SurveyResource, ReadOnlyResource) from tower_cli.models.fields import Field from tower_cli.cli.types import File ansible-tower-cli-3.2.0/tower_cli/models/base.py000066400000000000000000001477361316523067200216210ustar00rootroot00000000000000# Copyright 2015, Ansible, Inc. # Luke Sneeringer # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. from __future__ import absolute_import, division import itertools import json import re import sys import time from copy import copy from base64 import b64decode import six import click from click._compat import isatty as is_tty from tower_cli import resources, exceptions as exc from tower_cli.api import client from tower_cli.conf import settings from tower_cli.models.fields import Field from tower_cli.utils import parser, debug, secho from tower_cli.utils.data_structures import OrderedDict from tower_cli.utils.resource_decorators import disabled_getter, disabled_setter, disabled_deleter class ResourceMeta(type): """Metaclass for the creation of a Model subclass, which pulls fields aside into their appropriate tuple and handles other initialization. """ def __new__(cls, name, bases, attrs): super_new = super(ResourceMeta, cls).__new__ # Mark all `@resources.command` methods as CLI commands. commands = set() for base in bases: base_commands = getattr(base, 'commands', []) commands = commands.union(base_commands) # Read list of deprecated resource methods if present. deprecates = attrs.pop('deprecated_methods', []) for key, value in attrs.items(): if getattr(value, '_cli_command', False): commands.add(key) if key in deprecates: setattr(value, 'deprecated', True) # If this method has been overwritten from the superclass, copy any click options or arguments from # the superclass implementation down to the subclass implementation. if not len(bases): continue cp = [] baseattrs = {} for superclass in bases: super_method = getattr(superclass, key, None) if super_method and getattr(super_method, '_cli_command', False): # Copy the click parameters from the parent method to the child. for param in getattr(super_method, '__click_params__', []): if param not in cp: cp.append(param) # Copy the command attributes from the parent to the child, if the child has not overridden them. for attkey, attval in getattr(super_method, '_cli_command_attrs', {}).items(): baseattrs.setdefault(attkey, attval) if cp: # If subclass method is not decorated as command, but parent # classes do, then make it into a command here if not hasattr(value, '__click_params__'): value.__click_params__ = [] if not hasattr(value, '_cli_command_attrs'): value._cli_command_attrs = {} # Copy all parent click parameters to subclass method here for param in cp: if param not in value.__click_params__: value.__click_params__.append(param) for attkey, attval in baseattrs.items(): value._cli_command_attrs.setdefault(attkey, attval) disabled_methods = attrs.pop('disabled_methods', set()) commands -= disabled_methods attrs['commands'] = sorted(commands) for method in disabled_methods: attrs[method] = property(disabled_getter(method), disabled_setter(method), disabled_deleter(method)) # Sanity check: Only perform remaining initialization for subclasses actual resources, not abstract ones. if attrs.pop('abstract', False): return super_new(cls, name, bases, attrs) # Initialize a new attributes dictionary. newattrs = {} # Iterate over each of the fields and move them into a `fields` list; # port remaining attrs unchanged into newattrs. fields = [] unique_fields = set() for k, v in attrs.items(): if isinstance(v, Field): v.name = k fields.append(v) if v.unique: unique_fields.add(v.name) else: newattrs[k] = v newattrs['fields'] = sorted(fields) newattrs['unique_fields'] = unique_fields # Cowardly refuse to create a Resource with no endpoint (unless it's the base class). if not newattrs.get('endpoint', None): raise TypeError('Resource subclasses must have an `endpoint`.') # Ensure that the endpoint ends in a trailing slash, since we expect this when we build URLs based on it. if isinstance(newattrs['endpoint'], six.string_types): if not newattrs['endpoint'].startswith('/'): newattrs['endpoint'] = '/' + newattrs['endpoint'] if not newattrs['endpoint'].endswith('/'): newattrs['endpoint'] += '/' # Construct the class. return super_new(cls, name, bases, newattrs) class BaseResource(six.with_metaclass(ResourceMeta)): """Abstract class representing resources within the Ansible Tower system, on which actions can be taken. Includes standard create, modify, list, get, and delete methods. Some of these methods are not created as commands, but will be implemented as commands inside of non-abstract child classes. Particularly, create is not a command in this class, but will be for some (but not all) child classes.""" abstract = True # Not inherited. cli_help = '' endpoint = None identity = ('name',) # The basic methods for interacting with a resource are `read`, `write`, # and `delete`; these cover basic CRUD situations and have options # to handle most desired behavior. # # Most likely, `read` and `write` won't see much direct use; rather, # `get` and `list` are wrappers around `read` and `create` and # `modify` are wrappers around `write`. def _pop_none(self, kwargs): """Remove default values (anything where the value is None). click is unfortunately bad at the way it sends through unspecified defaults.""" for key, value in copy(kwargs).items(): # options with multiple=True return a tuple if value is None or value == (): kwargs.pop(key) if hasattr(value, 'read'): kwargs[key] = value.read() def _lookup(self, fail_on_missing=False, fail_on_found=False, include_debug_header=True, **kwargs): """ =====API DOCS===== Attempt to perform a lookup that is expected to return a single result, and return the record. This method is a wrapper around `get` that strips out non-unique keys, and is used internally by `write` and `delete`. :param fail_on_missing: Flag that raise exception if no resource is found. :type fail_on_missing: bool :param fail_on_found: Flag that raise exception if a resource is found. :type fail_on_found: bool :param include_debug_header: Flag determining whether to print debug messages when querying Tower backend. :type include_debug_header: bool :param `**kwargs`: Keyword arguments list of available fields used for searching resource. :returns: A JSON object containing details of the resource returned by Tower backend. :rtype: dict :raises tower_cli.exceptions.BadRequest: When no field are provided in kwargs. :raises tower_cli.exceptions.Found: When a resource is found and fail_on_found flag is on. :raises tower_cli.exceptions.NotFound: When no resource is found and fail_on_missing flag is on. =====API DOCS===== """ read_params = {} for field_name in self.identity: if field_name in kwargs: read_params[field_name] = kwargs[field_name] if 'id' in self.identity and len(self.identity) == 1: return {} if not read_params: raise exc.BadRequest('Cannot reliably determine which record to write. Include an ID or unique ' 'fields.') try: existing_data = self.get(include_debug_header=include_debug_header, **read_params) if fail_on_found: raise exc.Found('A record matching %s already exists, and you requested a failure in that case.' % read_params) return existing_data except exc.NotFound: if fail_on_missing: raise exc.NotFound('A record matching %s does not exist, and you requested a failure in that case.' % read_params) return {} def read(self, pk=None, fail_on_no_results=False, fail_on_multiple_results=False, **kwargs): """ =====API DOCS===== Retrieve and return objects from the Ansible Tower API. :param pk: Primary key of the resource to be read. Tower CLI will only attempt to read that object if ``pk`` is provided (not ``None``). :type pk: int :param fail_on_no_results: Flag that if set, zero results is considered a failure case and raises an exception; otherwise, empty list is returned. (Note: This is always True if a primary key is included.) :type fail_on_no_results: bool :param fail_on_multiple_results: Flag that if set, at most one result is expected, and more results constitutes a failure case. (Note: This is meaningless if a primary key is included, as there can never be multiple results.) :type fail_on_multiple_results: bool :param query: Contains 2-tuples used as query parameters to filter resulting resource objects. :type query: list :param `**kwargs`: Keyword arguements which, all together, will be used as query parameters to filter resulting resource objects. :returns: loaded JSON from Tower backend response body. :rtype: dict :raises tower_cli.exceptions.BadRequest: When 2-tuples in ``query`` overlaps key-value pairs in ``**kwargs``. :raises tower_cli.exceptions.NotFound: When no objects are found and ``fail_on_no_results`` flag is on. :raises tower_cli.exceptions.MultipleResults: When multiple objects are found and ``fail_on_multiple_results`` flag is on. =====API DOCS===== """ # Piece together the URL we will be hitting. url = self.endpoint if pk: url += '%s/' % pk # Pop the query parameter off of the keyword arguments; it will # require special handling (below). queries = kwargs.pop('query', []) # Remove default values (anything where the value is None). self._pop_none(kwargs) # Remove fields that are specifically excluded from lookup for field in self.fields: if field.no_lookup and field.name in kwargs: kwargs.pop(field.name) # If queries were provided, process them. for query in queries: if query[0] in kwargs: raise exc.BadRequest('Attempted to set %s twice.' % query[0].replace('_', '-')) kwargs[query[0]] = query[1] # Make the request to the Ansible Tower API. r = client.get(url, params=kwargs) resp = r.json() # If this was a request with a primary key included, then at the # point that we got a good result, we know that we're done and can # return the result. if pk: # Make the results all look the same, for easier parsing # by other methods. # # Note that the `get` method will effectively undo this operation, # but that's a good thing, because we might use `get` without a # primary key. return {'count': 1, 'results': [resp]} # Did we get zero results back when we shouldn't? # If so, this is an error, and we need to complain. if fail_on_no_results and resp['count'] == 0: raise exc.NotFound('The requested object could not be found.') # Did we get more than one result back? # If so, this is also an error, and we need to complain. if fail_on_multiple_results and resp['count'] >= 2: raise exc.MultipleResults('Expected one result, got %d. Possibly caused by not providing required ' 'fields. Please tighten your criteria.' % resp['count']) # Return the response. return resp def _get_patch_url(self, url, pk): """Overwrite this method to handle specific corner cases to the url passed to PATCH method.""" return url + '%s/' % pk def write(self, pk=None, create_on_missing=False, fail_on_found=False, force_on_exists=True, **kwargs): """ =====API DOCS===== Modify the given object using the Ansible Tower API. :param pk: Primary key of the resource to be read. Tower CLI will only attempt to read that object if ``pk`` is provided (not ``None``). :type pk: int :param create_on_missing: Flag that if set, a new object is created if ``pk`` is not set and objects matching the appropriate unique criteria is not found. :type create_on_missing: bool :param fail_on_found: Flag that if set, the operation fails if an object matching the unique criteria already exists. :type fail_on_found: bool :param force_on_exists: Flag that if set, then if an object is modified based on matching via unique fields (as opposed to the primary key), other fields are updated based on data sent; If unset, then the non-unique values are only written in a creation case. :type force_on_exists: bool :param `**kwargs`: Keyword arguements which, all together, will be used as POST/PATCH body to create/modify the resource object. if ``pk`` is not set, key-value pairs of ``**kwargs`` which are also in resource's identity will be used to lookup existing reosource. :returns: A dictionary combining the JSON output of the resource, as well as two extra fields: "changed", a flag indicating if the resource is created or successfully updated; "id", an integer which is the primary key of the specified object. :rtype: dict :raises tower_cli.exceptions.BadRequest: When required fields are missing in ``**kwargs`` when creating a new resource object. =====API DOCS===== """ existing_data = {} # Remove default values (anything where the value is None). self._pop_none(kwargs) # Determine which record we are writing, if we weren't given a primary key. if not pk: debug.log('Checking for an existing record.', header='details') existing_data = self._lookup( fail_on_found=fail_on_found, fail_on_missing=not create_on_missing, include_debug_header=False, **kwargs ) if existing_data: pk = existing_data['id'] else: # We already know the primary key, but get the existing data. # This allows us to know whether the write made any changes. debug.log('Getting existing record.', header='details') existing_data = self.get(pk) # Sanity check: Are we missing required values? # If we don't have a primary key, then all required values must be set, and if they're not, it's an error. missing_fields = [] for i in self.fields: if i.key not in kwargs and i.name not in kwargs and i.required: missing_fields.append(i.key or i.name) if missing_fields and not pk: raise exc.BadRequest('Missing required fields: %s' % ', '.join(missing_fields).replace('_', '-')) # Sanity check: Do we need to do a write at all? # If `force_on_exists` is False and the record was, in fact, found, then no action is required. if pk and not force_on_exists: debug.log('Record already exists, and --force-on-exists is off; do nothing.', header='decision', nl=2) answer = OrderedDict((('changed', False), ('id', pk))) answer.update(existing_data) return answer # Similarly, if all existing data matches our write parameters, there's no need to do anything. if all([kwargs[k] == existing_data.get(k, None) for k in kwargs.keys()]): debug.log('All provided fields match existing data; do nothing.', header='decision', nl=2) answer = OrderedDict((('changed', False), ('id', pk))) answer.update(existing_data) return answer # Reinsert None for special case of null association for key in kwargs: if kwargs[key] == 'null': kwargs[key] = None # Get the URL and method to use for the write. url = self.endpoint method = 'POST' if pk: url = self._get_patch_url(url, pk) method = 'PATCH' # If debugging is on, print the URL and data being sent. debug.log('Writing the record.', header='details') # Actually perform the write. r = getattr(client, method.lower())(url, data=kwargs) # At this point, we know the write succeeded, and we know that data was changed in the process. answer = OrderedDict((('changed', True), ('id', r.json()['id']))) answer.update(r.json()) return answer @resources.command def delete(self, pk=None, fail_on_missing=False, **kwargs): """Remove the given object. If `fail_on_missing` is True, then the object's not being found is considered a failure; otherwise, a success with no change is reported. =====API DOCS===== Remove the given object. :param pk: Primary key of the resource to be deleted. :type pk: int :param fail_on_missing: Flag that if set, the object's not being found is considered a failure; otherwise, a success with no change is reported. :type fail_on_missing: bool :param `**kwargs`: Keyword arguments used to look up resource object to delete if ``pk`` is not provided. :returns: dictionary of only one field "changed", which is a flag indicating whether the specified resource is successfully deleted. :rtype: dict =====API DOCS===== """ # If we weren't given a primary key, determine which record we're deleting. if not pk: existing_data = self._lookup(fail_on_missing=fail_on_missing, **kwargs) if not existing_data: return {'changed': False} pk = existing_data['id'] # Attempt to delete the record. If it turns out the record doesn't exist, handle the 404 appropriately # (this is an okay response if `fail_on_missing` is False). url = '%s%s/' % (self.endpoint, pk) debug.log('DELETE %s' % url, fg='blue', bold=True) try: client.delete(url) return {'changed': True} except exc.NotFound: if fail_on_missing: raise return {'changed': False} # Convenience wrappers around `read` and `write`: # - read: get, list # - write: create, modify @resources.command(ignore_defaults=True) def get(self, pk=None, **kwargs): """Return one and exactly one object. Lookups may be through a primary key, specified as a positional argument, and/or through filters specified through keyword arguments. If the number of results does not equal one, raise an exception. =====API DOCS===== Retrieve one and exactly one object. :param pk: Primary key of the resource to be read. Tower CLI will only attempt to read *that* object if ``pk`` is provided (not ``None``). :type pk: int :param `**kwargs`: Keyword arguments used to look up resource object to retrieve if ``pk`` is not provided. :returns: loaded JSON of the retrieved resource object. :rtype: dict =====API DOCS===== """ if kwargs.pop('include_debug_header', True): debug.log('Getting the record.', header='details') response = self.read(pk=pk, fail_on_no_results=True, fail_on_multiple_results=True, **kwargs) return response['results'][0] @resources.command(ignore_defaults=True, no_args_is_help=False) @click.option('all_pages', '-a', '--all-pages', is_flag=True, default=False, show_default=True, help='If set, collate all pages of content from the API when returning results.') @click.option('--page', default=1, type=int, show_default=True, help='The page to show. Ignored if --all-pages is sent.') @click.option('--page-size', type=int, show_default=True, required=False, help='Number of records to show. Ignored if --all-pages.') @click.option('-Q', '--query', required=False, nargs=2, multiple=True, help='A key and value to be passed as an HTTP query string key and value to the Tower API.' ' Will be run through HTTP escaping. This argument may be sent multiple times.\n' 'Example: `--query foo bar` would be passed to Tower as ?foo=bar') def list(self, all_pages=False, **kwargs): """Return a list of objects. If one or more filters are provided through keyword arguments, filter the results accordingly. If no filters are provided, return all results. =====API DOCS===== Retrieve a list of objects. :param all_pages: Flag that if set, collect all pages of content from the API when returning results. :type all_pages: bool :param page: The page to show. Ignored if all_pages is set. :type page: int :param query: Contains 2-tuples used as query parameters to filter resulting resource objects. :type query: list :param `**kwargs`: Keyword arguments list of available fields used for searching resource objects. :returns: A JSON object containing details of all resource objects returned by Tower backend. :rtype: dict =====API DOCS===== """ # If the `all_pages` flag is set, then ignore any page that might also be sent. if all_pages: kwargs.pop('page', None) kwargs.pop('page_size', None) # Get the response. debug.log('Getting records.', header='details') response = self.read(**kwargs) # Alter the "next" and "previous" to reflect simple integers, rather than URLs, since this endpoint # just takes integers. for key in ('next', 'previous'): if not response.get(key): continue match = re.search(r'page=(?P[\d]+)', response[key]) if match is None and key == 'previous': response[key] = 1 continue response[key] = int(match.groupdict()['num']) # If we were asked for all pages, keep retrieving pages until we have them all. if all_pages and response['next']: cursor = copy(response) while cursor['next']: cursor = self.list(**dict(kwargs, page=cursor['next'])) response['results'] += cursor['results'] # Done; return the response return response def _assoc(self, url_fragment, me, other): """Associate the `other` record with the `me` record.""" # Get the endpoint for foreign records within this object. url = self.endpoint + '%d/%s/' % (me, url_fragment) # Attempt to determine whether the other record already exists here, for the "changed" moniker. r = client.get(url, params={'id': other}).json() if r['count'] > 0: return {'changed': False} # Send a request adding the other record to this one. r = client.post(url, data={'associate': True, 'id': other}) return {'changed': True} def _disassoc(self, url_fragment, me, other): """Disassociate the `other` record from the `me` record.""" # Get the endpoint for foreign records within this object. url = self.endpoint + '%d/%s/' % (me, url_fragment) # Attempt to determine whether the other record already is absent, for the "changed" moniker. r = client.get(url, params={'id': other}).json() if r['count'] == 0: return {'changed': False} # Send a request removing the foreign record from this one. r = client.post(url, data={'disassociate': True, 'id': other}) return {'changed': True} class Resource(BaseResource): """This is the parent class for all standard resources.""" abstract = True @resources.command @click.option('--fail-on-found', default=False, show_default=True, type=bool, is_flag=True, help='If used, return an error if a matching record already exists.') @click.option('--force-on-exists', default=False, show_default=True, type=bool, is_flag=True, help='If used, if a match is found on unique fields, other fields will be updated ' 'to the provided values. If False, a match causes the request to be a no-op.') def create(self, **kwargs): """Create an object. Fields in the resource's `identity` tuple are used for a lookup; if a match is found, then no-op (unless `force_on_exists` is set) but do not fail (unless `fail_on_found` is set). =====API DOCS===== Create an object. :param fail_on_found: Flag that if set, the operation fails if an object matching the unique criteria already exists. :type fail_on_found: bool :param force_on_exists: Flag that if set, then if a match is found on unique fields, other fields will be updated to the provided values.; If unset, a match causes the request to be a no-op. :type force_on_exists: bool :param `**kwargs`: Keyword arguements which, all together, will be used as POST body to create the resource object. :returns: A dictionary combining the JSON output of the created resource, as well as two extra fields: "changed", a flag indicating if the resource is created successfully; "id", an integer which is the primary key of the created object. :rtype: dict =====API DOCS===== """ return self.write(create_on_missing=True, **kwargs) @resources.command(ignore_defaults=True) def copy(self, pk=None, **kwargs): """Copy an object. Only the ID is used for the lookup. All provided fields are used to override the old data from the copied resource. =====API DOCS===== Copy an object. :param pk: Primary key of the resource object to be copied :type pk: int :param `**kwargs`: Keyword arguments of fields whose given value will override the original value. :returns: loaded JSON of the copied new resource object. :rtype: dict =====API DOCS===== """ orig = self.read(pk, fail_on_no_results=True, fail_on_multiple_results=True) orig = orig['results'][0] # Remove default values (anything where the value is None). self._pop_none(kwargs) newresource = copy(orig) newresource.pop('id') basename = newresource['name'].split('@', 1)[0].strip() newresource['name'] = "%s @ %s" % (basename, time.strftime('%X')) newresource.update(kwargs) return self.write(create_on_missing=True, **newresource) @resources.command(ignore_defaults=True) @click.option('--create-on-missing', default=False, show_default=True, type=bool, is_flag=True, help='If used, and if options rather than a primary key are used to attempt to match a record, ' 'will create the record if it does not exist. This is an alias to `create --force-on-exists`.') def modify(self, pk=None, create_on_missing=False, **kwargs): """Modify an already existing object. Fields in the resource's `identity` tuple can be used in lieu of a primary key for a lookup; in such a case, only other fields are written. To modify unique fields, you must use the primary key for the lookup. =====API DOCS===== Modify an already existing object. :param pk: Primary key of the resource to be modified. :type pk: int :param create_on_missing: Flag that if set, a new object is created if ``pk`` is not set and objects matching the appropriate unique criteria is not found. :type create_on_missing: bool :param `**kwargs`: Keyword arguements which, all together, will be used as PATCH body to modify the resource object. if ``pk`` is not set, key-value pairs of ``**kwargs`` which are also in resource's identity will be used to lookup existing reosource. :returns: A dictionary combining the JSON output of the modified resource, as well as two extra fields: "changed", a flag indicating if the resource is successfully updated; "id", an integer which is the primary key of the updated object. :rtype: dict =====API DOCS===== """ return self.write(pk, create_on_missing=create_on_missing, force_on_exists=True, **kwargs) class ReadOnlyResource(BaseResource): abstract = True disabled_methods = set(['_assoc', '_disassoc', '_get_patch_url', 'delete', 'write']) class MonitorableResource(BaseResource): """A resource that is able to be tied to a running task, such as a job or project, and thus able to be monitored. """ abstract = True # Not inherited. def __init__(self, *args, **kwargs): if not hasattr(self, 'unified_job_type'): self.unified_job_type = self.endpoint return super(MonitorableResource, self).__init__(*args, **kwargs) def status(self, pk, detail=False): """A stub method requesting the status of the resource.""" raise NotImplementedError('This resource does not implement a status method, and must do so.') def last_job_data(self, pk=None, **kwargs): """ Internal utility function for Unified Job Templates. Returns data about the last job run off of that UJT """ ujt = self.get(pk, include_debug_header=True, **kwargs) # Determine the appropriate inventory source update. if 'current_update' in ujt['related']: debug.log('A current job; retrieving it.', header='details') return client.get(ujt['related']['current_update'][7:]).json() elif ujt['related'].get('last_update', None): debug.log('No current job or update exists; retrieving the most recent.', header='details') return client.get(ujt['related']['last_update'][7:]).json() else: raise exc.NotFound('No related jobs or updates exist.') def lookup_stdout(self, pk=None, start_line=None, end_line=None, full=True): """ Internal utility function to return standard out. Requires the pk of a unified job. """ stdout_url = '%s%s/stdout/' % (self.unified_job_type, pk) payload = {'format': 'json', 'content_encoding': 'base64', 'content_format': 'ansi'} if start_line: payload['start_line'] = start_line if end_line: payload['end_line'] = end_line debug.log('Requesting a copy of job standard output', header='details') resp = client.get(stdout_url, params=payload).json() content = b64decode(resp['content']) return content @resources.command @click.option('--start-line', required=False, type=int, help='Line at which to start printing the standard out.') @click.option('--end-line', required=False, type=int, help='Line at which to end printing the standard out.') def stdout(self, pk, start_line=None, end_line=None, **kwargs): """ Print out the standard out of a unified job to the command line. For Projects, print the standard out of most recent update. For Inventory Sources, print standard out of most recent sync. For Jobs, print the job's standard out. For Workflow Jobs, print a status table of its jobs. """ # resource is Unified Job Template if self.unified_job_type != self.endpoint: unified_job = self.last_job_data(pk, **kwargs) pk = unified_job['id'] # resource is Unified Job, but pk not given elif not pk: unified_job = self.get(**kwargs) pk = unified_job['id'] content = self.lookup_stdout(pk, start_line, end_line) if len(content) > 0: click.echo(content, nl=1) return {"changed": False} @resources.command @click.option('--interval', default=0.2, help='Polling interval to refresh content from Tower.') @click.option('--timeout', required=False, type=int, help='If provided, this command (not the job) will time out after the given number of seconds.') def monitor(self, pk, parent_pk=None, timeout=None, interval=0.5, outfile=sys.stdout, **kwargs): """ Stream the standard output from a job, project update, or inventory udpate. =====API DOCS===== Stream the standard output from a job run to stdout. :param pk: Primary key of the job resource object to be monitored. :type pk: int :param parent_pk: Primary key of the unified job template resource object whose latest job run will be monitored if ``pk`` is not set. :type parent_pk: int :param timeout: Number in seconds after which this method wiil time out. :type timeout: float :param interval: Polling interval to refresh content from Tower. :type interval: float :param outfile: Alternative file than stdout to write job stdout to. :type outfile: file :param `**kwargs`: Keyword arguments used to look up job resource object to monitor if ``pk`` is not provided. :returns: A dictionary combining the JSON output of the finished job resource object, as well as two extra fields: "changed", a flag indicating if the job resource object is finished as expected; "id", an integer which is the primary key of the job resource object being monitored. :rtype: dict :raises tower_cli.exceptions.Timeout: When monitor time reaches time out. :raises tower_cli.exceptions.JobFailure: When the job being monitored runs into failure. =====API DOCS===== """ # If we do not have the unified job info, infer it from parent if pk is None: pk = self.last_job_data(parent_pk, **kwargs)['id'] job_endpoint = '%s%s/' % (self.unified_job_type, pk) # Pause until job is in running state self.wait(pk, exit_on=['running', 'successful']) # Loop initialization start = time.time() start_line = 0 result = client.get(job_endpoint).json() click.echo('\033[0;91m------Starting Standard Out Stream------\033[0m', nl=2, file=outfile) # Poll the Ansible Tower instance for status and content, and print standard out to the out file while not result['failed'] and result['status'] != 'successful': result = client.get(job_endpoint).json() # Put the process to sleep briefly. time.sleep(interval) # Make request to get standard out content = self.lookup_stdout(pk, start_line, full=False) # In the first moments of running the job, the standard out # may not be available yet if not content.startswith(b"Waiting for results"): line_count = len(content.splitlines()) start_line += line_count click.echo(content, nl=0) if timeout and time.time() - start > timeout: raise exc.Timeout('Monitoring aborted due to timeout.') # Special final line for closure with workflow jobs if self.endpoint == '/workflow_jobs/': click.echo(self.lookup_stdout(pk, start_line, full=True), nl=1) click.echo('\033[0;91m------End of Standard Out Stream--------\033[0m', nl=2, file=outfile) if result['failed']: raise exc.JobFailure('Job failed.') # Return the job ID and other response data answer = OrderedDict((('changed', True), ('id', pk))) answer.update(result) # Make sure to return ID of resource and not update number relevant for project creation and update if parent_pk: answer['id'] = parent_pk else: answer['id'] = pk return answer @resources.command @click.option('--min-interval', default=1, help='The minimum interval to request an update from Tower.') @click.option('--max-interval', default=30, help='The maximum interval to request an update from Tower.') @click.option('--timeout', required=False, type=int, help='If provided, this command (not the job) will time out after the given number of seconds.') def wait(self, pk, parent_pk=None, min_interval=1, max_interval=30, timeout=None, outfile=sys.stdout, exit_on=['successful'], **kwargs): """ Wait for a running job to finish. Blocks further input until the job completes (whether successfully or unsuccessfully) and a final status can be given. =====API DOCS===== Wait for a job resource object to enter certain status. :param pk: Primary key of the job resource object to wait. :type pk: int :param parent_pk: Primary key of the unified job template resource object whose latest job run will be waited if ``pk`` is not set. :type parent_pk: int :param timeout: Number in seconds after which this method will time out. :type timeout: float :param min_interval: Minimum polling interval to request an update from Tower. :type min_interval: float :param max_interval: Maximum polling interval to request an update from Tower. :type max_interval: float :param outfile: Alternative file than stdout to write job status updates on. :type outfile: file :param exit_on: Job resource object statuses to wait on. :type exit_on: array :param `**kwargs`: Keyword arguments used to look up job resource object to wait if ``pk`` is not provided. :returns: A dictionary combining the JSON output of the status-changed job resource object, as well as two extra fields: "changed", a flag indicating if the job resource object is status-changed as expected; "id", an integer which is the primary key of the job resource object being status-changed. :rtype: dict :raises tower_cli.exceptions.Timeout: When wait time reaches time out. :raises tower_cli.exceptions.JobFailure: When the job being waited on runs into failure. =====API DOCS===== """ # If we do not have the unified job info, infer it from parent if pk is None: pk = self.last_job_data(parent_pk, **kwargs)['id'] job_endpoint = '%s%s/' % (self.unified_job_type, pk) dots = itertools.cycle([0, 1, 2, 3]) longest_string = 0 interval = min_interval start = time.time() # Poll the Ansible Tower instance for status, and print the status to the outfile (usually standard out). # # Note that this is one of the few places where we use `secho` even though we're in a function that might # theoretically be imported and run in Python. This seems fine; outfile can be set to /dev/null and very # much the normal use for this method should be CLI monitoring. result = client.get(job_endpoint).json() last_poll = time.time() timeout_check = 0 while result['status'] not in exit_on: # If the job has failed, we want to raise an Exception for that so we get a non-zero response. if result['failed']: if is_tty(outfile) and not settings.verbose: secho('\r' + ' ' * longest_string + '\n', file=outfile) raise exc.JobFailure('Job failed.') # Sanity check: Have we officially timed out? # The timeout check is incremented below, so this is checking to see if we were timed out as of # the previous iteration. If we are timed out, abort. if timeout and timeout_check - start > timeout: raise exc.Timeout('Monitoring aborted due to timeout.') # If the outfile is a TTY, print the current status. output = '\rCurrent status: %s%s' % (result['status'], '.' * next(dots)) if longest_string > len(output): output += ' ' * (longest_string - len(output)) else: longest_string = len(output) if is_tty(outfile) and not settings.verbose: secho(output, nl=False, file=outfile) # Put the process to sleep briefly. time.sleep(0.2) # Sanity check: Have we reached our timeout? # If we're about to time out, then we need to ensure that we do one last check. # # Note that the actual timeout will be performed at the start of the **next** iteration, # so there's a chance for the job's completion to be noted first. timeout_check = time.time() if timeout and timeout_check - start > timeout: last_poll -= interval # If enough time has elapsed, ask the server for a new status. # # Note that this doesn't actually do a status check every single time; we want the "spinner" to # spin even if we're not actively doing a check. # # So, what happens is that we are "counting down" (actually up) to the next time that we intend # to do a check, and once that time hits, we do the status check as part of the normal cycle. if time.time() - last_poll > interval: result = client.get(job_endpoint).json() last_poll = time.time() interval = min(interval * 1.5, max_interval) # If the outfile is *not* a TTY, print a status update when and only when we make an actual # check to job status. if not is_tty(outfile) or settings.verbose: click.echo('Current status: %s' % result['status'], file=outfile) # Wipe out the previous output if is_tty(outfile) and not settings.verbose: secho('\r' + ' ' * longest_string, file=outfile, nl=False) secho('\r', file=outfile, nl=False) # Return the job ID and other response data answer = OrderedDict((('changed', True), ('id', pk))) answer.update(result) # Make sure to return ID of resource and not update number relevant for project creation and update if parent_pk: answer['id'] = parent_pk else: answer['id'] = pk return answer class ExeResource(MonitorableResource): """Executable resource - defines status and cancel methods""" abstract = True @resources.command @click.option('--detail', is_flag=True, default=False, help='Print more detail.') def status(self, pk=None, detail=False, **kwargs): """Print the current job status. This is used to check a running job. You can look up the job with the same parameters used for a get request. =====API DOCS===== Retrieve the current job status. :param pk: Primary key of the resource to retrieve status from. :type pk: int :param detail: Flag that if set, return the full JSON of the job resource rather than a status summary. :type detail: bool :param `**kwargs`: Keyword arguments used to look up resource object to retrieve status from if ``pk`` is not provided. :returns: full loaded JSON of the specified unified job if ``detail`` flag is on; trimed JSON containing only "elapsed", "failed" and "status" fields of the unified job if ``detail`` flag is off. :rtype: dict =====API DOCS===== """ # Remove default values (anything where the value is None). self._pop_none(kwargs) # Search for the record if pk not given if not pk: job = self.get(include_debug_header=True, **kwargs) # Get the job from Ansible Tower if pk given else: debug.log('Asking for job status.', header='details') finished_endpoint = '%s%s/' % (self.endpoint, pk) job = client.get(finished_endpoint).json() # In most cases, we probably only want to know the status of the job and the amount of time elapsed. # However, if we were asked for verbose information, provide it. if detail: return job # Print just the information we need. return { 'elapsed': job['elapsed'], 'failed': job['failed'], 'status': job['status'], } @resources.command @click.option('--fail-if-not-running', is_flag=True, default=False, help='Fail loudly if the job is not currently running.') def cancel(self, pk=None, fail_if_not_running=False, **kwargs): """Cancel a currently running job. Fails with a non-zero exit status if the job cannot be canceled. You must provide either a pk or parameters in the job's identity. =====API DOCS===== Cancel a currently running job. :param pk: Primary key of the job resource to restart. :type pk: int :param fail_if_not_running: Flag that if set, raise exception if the job resource cannot be canceled. :type fail_if_not_running: bool :param `**kwargs`: Keyword arguments used to look up job resource object to restart if ``pk`` is not provided. :returns: A dictionary of two keys: "status", which is "canceled", and "changed", which indicates if the job resource has been successfully canceled. :rtype: dict :raises tower_cli.exceptions.TowerCLIError: When the job resource cannot be canceled and ``fail_if_not_running`` flag is on. =====API DOCS===== """ # Search for the record if pk not given if not pk: existing_data = self.get(**kwargs) pk = existing_data['id'] cancel_endpoint = '%s%s/cancel/' % (self.endpoint, pk) # Attempt to cancel the job. try: client.post(cancel_endpoint) changed = True except exc.MethodNotAllowed: changed = False if fail_if_not_running: raise exc.TowerCLIError('Job not running.') # Return a success. return {'status': 'canceled', 'changed': changed} @resources.command def relaunch(self, pk=None, **kwargs): """Relaunch a stopped job. Fails with a non-zero exit status if the job cannot be relaunched. You must provide either a pk or parameters in the job's identity. =====API DOCS===== Relaunch a stopped job resource. :param pk: Primary key of the job resource to relaunch. :type pk: int :param `**kwargs`: Keyword arguments used to look up job resource object to relaunch if ``pk`` is not provided. :returns: A dictionary combining the JSON output of the relaunched job resource object, as well as an extra field "changed", a flag indicating if the job resource object is status-changed as expected. :rtype: dict =====API DOCS===== """ # Search for the record if pk not given if not pk: existing_data = self.get(**kwargs) pk = existing_data['id'] relaunch_endpoint = '%s%s/relaunch/' % (self.endpoint, pk) data = {} # Attempt to relaunch the job. answer = {} try: result = client.post(relaunch_endpoint, data=data).json() if 'id' in result: answer.update(result) answer['changed'] = True except exc.MethodNotAllowed: answer['changed'] = False # Return the answer. return answer class SurveyResource(Resource): """Contains utilities and commands common to "job template" models, which take extra_vars and have a survey_spec.""" abstract = True def _survey_endpoint(self, pk): return '{0}{1}/survey_spec/'.format(self.endpoint, pk) def write(self, pk=None, **kwargs): survey_input = kwargs.pop('survey_spec', None) if kwargs.get('extra_vars', None): kwargs['extra_vars'] = parser.process_extra_vars(kwargs['extra_vars']) ret = super(SurveyResource, self).write(pk=pk, **kwargs) if survey_input is not None and ret.get('id', None): if not isinstance(survey_input, dict): survey_input = json.loads(survey_input.strip(' ')) if survey_input == {}: debug.log('Deleting the survey_spec.', header='details') r = client.delete(self._survey_endpoint(ret['id'])) else: debug.log('Saving the survey_spec.', header='details') r = client.post(self._survey_endpoint(ret['id']), data=survey_input) if r.status_code == 200: ret['changed'] = True if survey_input and not ret['survey_enabled']: debug.log('For survey to take effect, set survey_enabled field to True.', header='warning') return ret @resources.command def survey(self, pk=None, **kwargs): """Get the survey_spec for the job template. To write a survey, use the modify command with the --survey-spec parameter. =====API DOCS===== Get the survey specification of a resource object. :param pk: Primary key of the resource to retrieve survey from. Tower CLI will only attempt to read *that* object if ``pk`` is provided (not ``None``). :type pk: int :param `**kwargs`: Keyword arguments used to look up resource object to retrieve survey if ``pk`` is not provided. :returns: loaded JSON of the retrieved survey specification of the resource object. :rtype: dict =====API DOCS===== """ job_template = self.get(pk=pk, **kwargs) if settings.format == 'human': settings.format = 'json' return client.get(self._survey_endpoint(job_template['id'])).json() ansible-tower-cli-3.2.0/tower_cli/models/fields.py000066400000000000000000000066601316523067200221430ustar00rootroot00000000000000# Copyright 2015, Ansible, Inc. # Luke Sneeringer # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. import six _field_counter = 0 class Field(object): """A class representing flags on a given field on a model. This class tracks whether a field is unique, filterable, read-only, etc. """ def __init__(self, key=None, type=six.text_type, default=None, display=True, filterable=True, help_text=None, is_option=True, password=False, read_only=False, required=True, show_default=False, unique=False, multiple=False, no_lookup=False, col_width=None): # Init the name to blank. # What's going on here: This is set by the ResourceMeta metaclass # when the **resource** is instantiated. # Essentially, in any normal situation, it's safe to expect it # to be set and non-empty. self.name = '' # Save properties of this field. self.key = key self.type = type self.display = display self.default = default self.help_text = help_text self.is_option = is_option self.filterable = filterable self.password = password self.read_only = read_only self.required = required self.show_default = show_default self.unique = unique self.multiple = multiple self.no_lookup = no_lookup self.col_width = col_width # If this is a password, display is always off. if self.password: self.display = False # Track the creation history of each field, for sorting reasons. global _field_counter self.number = _field_counter _field_counter += 1 def __lt__(self, other): return self.number < other.number def __gt__(self, other): return self.number > other.number def __repr__(self): return '' % (self.name, ', '.join(self.flags)) @property def flags(self): try: flags_list = [self.type.__name__.replace('unicode', 'str')] except AttributeError: flags_list = [type(self.type).__name__.replace('unicode', 'str')] if self.read_only: flags_list.append('read-only') if self.unique: flags_list.append('unique') if not self.filterable: flags_list.append('not filterable') if not self.required: flags_list.append('not required') return flags_list @property def help(self): """Return the help text that was passed to the constructor, or a sensible default if none was provided. """ if self.help_text: return self.help_text return 'The %s field.' % self.name @property def option(self): """Return the field name as a bash option string (e.g. "--field-name"). """ return '--' + self.name.replace('_', '-') ansible-tower-cli-3.2.0/tower_cli/resources/000077500000000000000000000000001316523067200210425ustar00rootroot00000000000000ansible-tower-cli-3.2.0/tower_cli/resources/__init__.py000066400000000000000000000026771316523067200231670ustar00rootroot00000000000000# Copyright 2015, Ansible, Inc. # Luke Sneeringer # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. import types def command(method=None, **kwargs): """Mark this method as a CLI command. This will only have any meaningful effect in methods that are members of a Resource subclass. """ # Create the actual decorator to be applied. # This is done in such a way to make `@resources.command`, # `@resources.command()`, and `@resources.command(foo='bar')` all work. def actual_decorator(method): method._cli_command = True method._cli_command_attrs = kwargs return method # If we got the method straight-up, apply the decorator and return # the decorated method; otherwise, return the actual decorator for # the Python interpreter to apply. if method and isinstance(method, types.FunctionType): return actual_decorator(method) else: return actual_decorator ansible-tower-cli-3.2.0/tower_cli/resources/ad_hoc.py000066400000000000000000000134051316523067200226340ustar00rootroot00000000000000# Copyright 2015, Ansible, Inc. # Alan Rominger # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. from __future__ import absolute_import, unicode_literals import click from tower_cli import models, resources, exceptions as exc from tower_cli.api import client from tower_cli.cli import types from tower_cli.utils import debug from tower_cli.utils.data_structures import OrderedDict class Resource(models.ExeResource): """A resource for ad hoc commands.""" cli_help = 'Launch commands based on playbook given at runtime.' endpoint = '/ad_hoc_commands/' # Parameters similar to job job_explanation = models.Field(required=False, display=False) created = models.Field(required=False, display=True) status = models.Field(required=False, display=True) elapsed = models.Field(required=False, display=True) # Parameters similar to job_template job_type = models.Field( default='run', display=False, show_default=True, type=click.Choice(['run', 'check']), ) inventory = models.Field(type=types.Related('inventory')) limit = models.Field(required=False, display=False) credential = models.Field(display=False, type=types.Related('credential')) module_name = models.Field(required=False, display=True, default="command", show_default=True) module_args = models.Field(required=False, display=False) forks = models.Field(type=int, required=False, display=False) verbosity = models.Field( display=False, type=types.MappedChoice([ (0, 'default'), (1, 'verbose'), (2, 'more_verbose'), (3, 'debug'), (4, 'connection'), (5, 'winrm'), ]), required=False, ) become_enabled = models.Field(type=bool, required=False, display=False) diff_mode = models.Field(type=bool, required=False, display=False) @resources.command( use_fields_as_options=( 'job_explanation', 'job_type', 'inventory', 'credential', 'module_name', 'module_args', 'forks', 'limit', 'verbosity', 'become_enabled', 'diff_mode', ) ) @click.option('--monitor', is_flag=True, default=False, help='If sent, immediately calls `monitor` on the newly ' 'launched command rather than exiting with a success.') @click.option('--wait', is_flag=True, default=False, help='Monitor the status of the job, but do not print ' 'while job is in progress.') @click.option('--timeout', required=False, type=int, help='If provided with --monitor, this attempt' ' will time out after the given number of seconds. ' 'Does nothing if --monitor is not sent.') def launch(self, monitor=False, wait=False, timeout=None, **kwargs): """Launch a new ad-hoc command. Runs a user-defined command from Ansible Tower, immediately starts it, and returns back an ID in order for its status to be monitored. =====API DOCS===== Launch a new ad-hoc command. :param monitor: Flag that if set, immediately calls ``monitor`` on the newly launched command rather than exiting with a success. :type monitor: bool :param wait: Flag that if set, monitor the status of the job, but do not print while job is in progress. :type wait: bool :param timeout: If provided with ``monitor`` flag set, this attempt will time out after the given number of seconds. :type timeout: int :param `**kwargs`: Fields needed to create and launch an ad hoc command. :returns: Result of subsequent ``monitor`` call if ``monitor`` flag is on; Result of subsequent ``wait`` call if ``wait`` flag is on; dictionary of "id" and "changed" if none of the two flags are on. :rtype: dict :raises tower_cli.exceptions.TowerCLIError: When ad hoc commands are not available in Tower backend. =====API DOCS===== """ # This feature only exists for versions 2.2 and up r = client.get('/') if 'ad_hoc_commands' not in r.json(): raise exc.TowerCLIError('Your host is running an outdated version' 'of Ansible Tower that can not run ' 'ad-hoc commands (2.2 or earlier)') # Pop the None arguments because we have no .write() method in # inheritance chain for this type of resource. This is needed self._pop_none(kwargs) # Actually start the command. debug.log('Launching the ad-hoc command.', header='details') result = client.post(self.endpoint, data=kwargs) command = result.json() command_id = command['id'] # If we were told to monitor the command once it started, then call # monitor from here. if monitor: return self.monitor(command_id, timeout=timeout) elif wait: return self.wait(command_id, timeout=timeout) # Return the command ID and other response data answer = OrderedDict(( ('changed', True), ('id', command_id), )) answer.update(result.json()) return answer ansible-tower-cli-3.2.0/tower_cli/resources/credential.py000066400000000000000000000027261316523067200235350ustar00rootroot00000000000000# Copyright 2015, Ansible, Inc. # Luke Sneeringer # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. from tower_cli import models from tower_cli.cli import types class Resource(models.Resource): """A resource for credentials.""" cli_help = 'Manage credentials within Ansible Tower.' endpoint = '/credentials/' identity = ('organization', 'user', 'team', 'name') name = models.Field(unique=True) description = models.Field(required=False, display=False) # Who owns this credential? user = models.Field(display=False, type=types.Related('user'), required=False, no_lookup=True) team = models.Field(display=False, type=types.Related('team'), required=False, no_lookup=True) organization = models.Field(display=False, type=types.Related('organization'), required=False) credential_type = models.Field(type=types.Related('credential_type')) inputs = models.Field(type=types.StructuredInput(), required=False, display=False) ansible-tower-cli-3.2.0/tower_cli/resources/credential_type.py000066400000000000000000000031431316523067200245700ustar00rootroot00000000000000# Copyright 2017, Ansible by Red Hat. # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. import click from tower_cli import models from tower_cli.cli import types class Resource(models.Resource): """A resource for credential types.""" cli_help = 'Manage credential types within Ansible Tower.' endpoint = '/credential_types/' name = models.Field(unique=True) description = models.Field(required=False, display=False) kind = models.Field( help_text='The type of credential type being added. Valid options are: ssh, vault, net, scm, ' 'cloud and insights. Note only cloud and net can be used for creating credential types.', type=click.Choice(['ssh', 'vault', 'net', 'scm', 'cloud', 'insights']), ) managed_by_tower = models.Field( type=bool, required=False, read_only=True, help_text='Indicating if the credential type is a tower built-in type.') inputs = models.Field( type=types.StructuredInput(), required=False, display=False, ) injectors = models.Field( type=types.StructuredInput(), required=False, display=False, ) ansible-tower-cli-3.2.0/tower_cli/resources/group.py000066400000000000000000000204501316523067200225510ustar00rootroot00000000000000# Copyright 2015, Ansible, Inc. # Luke Sneeringer # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. import click from tower_cli import get_resource, models, resources, exceptions as exc from tower_cli.api import client from tower_cli.cli import types class Resource(models.Resource): """A resource for groups.""" cli_help = 'Manage groups belonging to an inventory.' endpoint = '/groups/' identity = ('inventory', 'name') name = models.Field(unique=True) description = models.Field(required=False, display=False) inventory = models.Field(type=types.Related('inventory')) variables = models.Field(type=types.Variables(), required=False, display=False, help_text='Group variables, use "@" to get from file.') def lookup_with_inventory(self, group, inventory=None): group_res = get_resource('group') if isinstance(group, int) or group.isdigit(): return group_res.get(int(group)) else: return group_res.get(name=group, inventory=inventory) def set_child_endpoint(self, parent, inventory=None): parent_data = self.lookup_with_inventory(parent, inventory) self.endpoint = '/groups/' + str(parent_data['id']) + '/children/' return parent_data @resources.command @click.option('--parent', help='Parent group to nest this one inside of.') def create(self, fail_on_found=False, force_on_exists=False, **kwargs): """Create a group. =====API DOCS===== Create a group. :param parent: Primary key or name of the group which will be the parent of created group. :type parent: str :param fail_on_found: Flag that if set, the operation fails if an object matching the unique criteria already exists. :type fail_on_found: bool :param force_on_exists: Flag that if set, then if a match is found on unique fields, other fields will be updated to the provided values.; If unset, a match causes the request to be a no-op. :type force_on_exists: bool :param `**kwargs`: Keyword arguements which, all together, will be used as POST body to create the resource object. :returns: A dictionary combining the JSON output of the created resource, as well as two extra fields: "changed", a flag indicating if the resource is created successfully; "id", an integer which is the primary key of the created object. :rtype: dict :raises tower_cli.exceptions.UsageError: When inventory is not provided in ``**kwargs`` and ``parent`` is not provided. =====API DOCS===== """ if kwargs.get('parent', None): parent_data = self.set_child_endpoint(parent=kwargs['parent'], inventory=kwargs.get('inventory', None)) kwargs['inventory'] = parent_data['inventory'] elif 'inventory' not in kwargs: raise exc.UsageError('To create a group, you must provide a parent inventory or parent group.') return super(Resource, self).create(fail_on_found=fail_on_found, force_on_exists=force_on_exists, **kwargs) @resources.command(ignore_defaults=True, no_args_is_help=False) @click.option('--root', is_flag=True, default=False, help='Show only root groups (groups with no parent groups) within the given inventory.') @click.option('--parent', help='Parent group to nest this one inside of.') def list(self, root=False, **kwargs): """Return a list of groups. =====API DOCS===== Retrieve a list of groups. :param root: Flag that if set, only root groups of a specific inventory will be listed. :type root: bool :param parent: Primary key or name of the group whose child groups will be listed. :type parent: str :param all_pages: Flag that if set, collect all pages of content from the API when returning results. :type all_pages: bool :param page: The page to show. Ignored if all_pages is set. :type page: int :param query: Contains 2-tuples used as query parameters to filter resulting resource objects. :type query: list :param `**kwargs`: Keyword arguments list of available fields used for searching resource objects. :returns: A JSON object containing details of all resource objects returned by Tower backend. :rtype: dict :raises tower_cli.exceptions.UsageError: When ``root`` flag is on and ``inventory`` is not present in ``**kwargs``. =====API DOCS===== """ # Option to list children of a parent group if kwargs.get('parent', None): self.set_child_endpoint(parent=kwargs['parent'], inventory=kwargs.get('inventory', None)) kwargs.pop('parent') # Sanity check: If we got `--root` and no inventory, that's an error. if root and not kwargs.get('inventory', None): raise exc.UsageError('The --root option requires specifying an inventory also.') # If we are tasked with getting root groups, do that. if root: inventory_id = kwargs['inventory'] r = client.get('/inventories/%d/root_groups/' % inventory_id) return r.json() # Return the superclass implementation. return super(Resource, self).list(**kwargs) @resources.command(use_fields_as_options=False) @click.option('--group', help='The group to move.') @click.option('--parent', help='Destination group to move into.') @click.option('--inventory', type=types.Related('inventory')) def associate(self, group, parent, **kwargs): """Associate this group with the specified group. =====API DOCS===== Associate this group with the specified group. :param group: Primary key or name of the child group to associate. :type group: str :param parent: Primary key or name of the parent group to associate to. :type parent: str :param inventory: Primary key or name of the inventory the association should happen in. :type inventory: str :returns: Dictionary of only one key "changed", which indicates whether the association succeeded. :rtype: dict =====API DOCS===== """ parent_id = self.lookup_with_inventory(parent, kwargs.get('inventory', None))['id'] group_id = self.lookup_with_inventory(group, kwargs.get('inventory', None))['id'] return self._assoc('children', parent_id, group_id) @resources.command(use_fields_as_options=False) @click.option('--group', help='The group to move.') @click.option('--parent', help='Destination group to move into.') @click.option('--inventory', type=types.Related('inventory')) def disassociate(self, group, parent, **kwargs): """Disassociate this group from the specified group. =====API DOCS===== Disassociate this group with the specified group. :param group: Primary key or name of the child group to disassociate. :type group: str :param parent: Primary key or name of the parent group to disassociate from. :type parent: str :param inventory: Primary key or name of the inventory the disassociation should happen in. :type inventory: str :returns: Dictionary of only one key "changed", which indicates whether the disassociation succeeded. :rtype: dict =====API DOCS===== """ parent_id = self.lookup_with_inventory(parent, kwargs.get('inventory', None))['id'] group_id = self.lookup_with_inventory(group, kwargs.get('inventory', None))['id'] return self._disassoc('children', parent_id, group_id) ansible-tower-cli-3.2.0/tower_cli/resources/host.py000066400000000000000000000135141316523067200223750ustar00rootroot00000000000000# Copyright 2015, Ansible, Inc. # Luke Sneeringer # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. import click from tower_cli import models, resources from tower_cli.cli import types from tower_cli.api import client class Resource(models.Resource): """A resource for credentials.""" cli_help = 'Manage hosts belonging to a group within an inventory.' endpoint = '/hosts/' identity = ('inventory', 'name') name = models.Field(unique=True) description = models.Field(required=False, display=False) inventory = models.Field(type=types.Related('inventory')) enabled = models.Field(type=bool, required=False) variables = models.Field(type=types.Variables(), required=False, display=False, help_text='Host variables, use "@" to get from file.') insights_system_id = models.Field(required=False, display=False) @resources.command(use_fields_as_options=False) @click.option('--host', type=types.Related('host')) @click.option('--group', type=types.Related('group')) def associate(self, host, group): """Associate a group with this host. =====API DOCS===== Associate a group with this host. :param host: Primary key or name of the host to be associated. :type host: str :param group: Primary key or name of the group to associate. :type group: str :returns: Dictionary of only one key "changed", which indicates whether the association succeeded. :rtype: dict =====API DOCS===== """ return self._assoc('groups', host, group) @resources.command(use_fields_as_options=False) @click.option('--host', type=types.Related('host')) @click.option('--group', type=types.Related('group')) def disassociate(self, host, group): """Disassociate a group from this host. =====API DOCS===== Disassociate a group from this host. :param host: Primary key or name of the host to be disassociated. :type host: str :param group: Primary key or name of the group to disassociate. :type group: str :returns: Dictionary of only one key "changed", which indicates whether the disassociation succeeded. :rtype: dict =====API DOCS===== """ return self._disassoc('groups', host, group) @resources.command(ignore_defaults=True, no_args_is_help=False) @click.option('--group', type=types.Related('group'), help='List hosts that are children of this group.') @click.option('--host-filter', help='List hosts filtered by this fact search query string.') def list(self, group=None, host_filter=None, **kwargs): """Return a list of hosts. =====API DOCS===== Retrieve a list of hosts. :param group: Primary key or name of the group whose hosts will be listed. :type group: str :param all_pages: Flag that if set, collect all pages of content from the API when returning results. :type all_pages: bool :param page: The page to show. Ignored if all_pages is set. :type page: int :param query: Contains 2-tuples used as query parameters to filter resulting resource objects. :type query: list :param `**kwargs`: Keyword arguments list of available fields used for searching resource objects. :returns: A JSON object containing details of all resource objects returned by Tower backend. :rtype: dict =====API DOCS===== """ if group: kwargs['query'] = kwargs.get('query', ()) + (('groups__in', group),) if host_filter: kwargs['query'] = kwargs.get('query', ()) + (('host_filter', host_filter),) return super(Resource, self).list(**kwargs) @resources.command(ignore_defaults=True) def list_facts(self, pk=None, **kwargs): """Return a JSON object of all available facts of the given host. Note global option --format is not available here, as the output would always be JSON-formatted. =====API DOCS===== List all available facts of the given host. :param pk: Primary key of the target host. :type pk: int :param `**kwargs`: Keyword arguments list of available fields used for searching resource objects. :returns: A JSON object of all available facts of the given host. :rtype: dict =====API DOCS===== """ res = self.get(pk=pk, **kwargs) url = self.endpoint + '%d/%s/' % (res['id'], 'ansible_facts') return client.get(url, params={}).json() list_facts.format_freezer = 'json' @resources.command(ignore_defaults=True) def insights(self, pk=None, **kwargs): """Return a JSON object of host insights. Note global option --format is not available here, as the output would always be JSON-formatted. =====API DOCS===== List host insights. :param pk: Primary key of the target host. :type pk: int :param `**kwargs`: Keyword arguments list of available fields used for searching resource objects. :returns: A JSON object of host insights. :rtype: dict =====API DOCS===== """ res = self.get(pk=pk, **kwargs) url = self.endpoint + '%d/%s/' % (res['id'], 'insights') return client.get(url, params={}).json() insights.format_freezer = 'json' ansible-tower-cli-3.2.0/tower_cli/resources/instance.py000066400000000000000000000020341316523067200232170ustar00rootroot00000000000000# Copyright 2017, Ansible by Red Hat. # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. from tower_cli import models class Resource(models.ReadOnlyResource): """A resource for instances.""" cli_help = 'Check instances within Ansible Tower.' endpoint = '/instances/' uuid = models.Field(required=False, display=False) hostname = models.Field(required=False) version = models.Field(required=False, display=False) capacity = models.Field(type=int, required=False) consumed_capacity = models.Field(type=int, required=False) ansible-tower-cli-3.2.0/tower_cli/resources/instance_group.py000066400000000000000000000016711316523067200244410ustar00rootroot00000000000000# Copyright 2017, Ansible by Red Hat. # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. from tower_cli import models class Resource(models.ReadOnlyResource): """A resource for instance groups.""" cli_help = 'Check instance groups within Ansible Tower.' endpoint = '/instance_groups/' name = models.Field(required=False) capacity = models.Field(type=int, required=False) consumed_capacity = models.Field(type=int, required=False) ansible-tower-cli-3.2.0/tower_cli/resources/inventory.py000066400000000000000000000106321316523067200234530ustar00rootroot00000000000000# Copyright 2015, Ansible, Inc. # Luke Sneeringer # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. import click from tower_cli import models, resources from tower_cli.cli import types from tower_cli.api import client class Resource(models.Resource): """A resource for inventories.""" cli_help = 'Manage inventory within Ansible Tower.' endpoint = '/inventories/' identity = ('organization', 'name') name = models.Field(unique=True) description = models.Field(required=False, display=False) organization = models.Field(type=types.Related('organization')) variables = models.Field(type=types.Variables(), required=False, display=False, help_text='Inventory variables, use "@" to get from file.') kind = models.Field(type=click.Choice(['', 'smart']), required=False, display=False, help_text='The kind field. Cannot be modified after created.') host_filter = models.Field(required=False, display=False, help_text='The host_filter field. Only useful when kind=smart.') insights_credential = models.Field(display=False, required=False, type=types.Related('credential')) @resources.command(ignore_defaults=True) def batch_update(self, pk=None, **kwargs): """Update all related inventory sources of the given inventory. Note global option --format is not available here, as the output would always be JSON-formatted. =====API DOCS===== Update all related inventory sources of the given inventory. :param pk: Primary key of the given inventory. :type pk: int :param `**kwargs`: Keyword arguments list of available fields used for searching resource objects. :returns: A JSON object of update status of the given inventory. :rtype: dict =====API DOCS===== """ res = self.get(pk=pk, **kwargs) url = self.endpoint + '%d/%s/' % (res['id'], 'update_inventory_sources') return client.post(url, data={}).json() batch_update.format_freezer = 'json' @resources.command(use_fields_as_options=False) @click.option('--inventory', type=types.Related('inventory'), required=True) @click.option('--instance-group', type=types.Related('instance_group'), required=True) def associate_ig(self, inventory, instance_group): """Associate an instance group with this inventory. The instance group will be used to run jobs within the inventory. =====API DOCS===== Associate an instance group with this inventory. :param inventory: Primary key or name of the inventory to associate to. :type inventory: str :param instance_group: Primary key or name of the instance group to be associated. :type instance_group: str :returns: Dictionary of only one key "changed", which indicates whether the association succeeded. :rtype: dict =====API DOCS===== """ return self._assoc('instance_groups', inventory, instance_group) @resources.command(use_fields_as_options=False) @click.option('--inventory', type=types.Related('inventory'), required=True) @click.option('--instance-group', type=types.Related('instance_group'), required=True) def disassociate_ig(self, inventory, instance_group): """Disassociate an instance group from this inventory. =====API DOCS===== Disassociate an instance group with this inventory. :param inventory: Primary key or name of the inventory to associate to. :type inventory: str :param instance_group: Primary key or name of the instance group to be associated. :type instance_group: str :returns: Dictionary of only one key "changed", which indicates whether the association succeeded. :rtype: dict =====API DOCS===== """ return self._disassoc('instance_groups', inventory, instance_group) ansible-tower-cli-3.2.0/tower_cli/resources/inventory_script.py000066400000000000000000000024421316523067200250370ustar00rootroot00000000000000# Copyright 2016, Ansible by Red Hat. # Aaron Tan # Alan Rominger # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. from tower_cli import models from tower_cli.cli import types class Resource(models.Resource): """A resource for inventory scripts.""" cli_help = 'Manage inventory scripts within Ansible Tower.' endpoint = '/inventory_scripts/' name = models.Field(unique=True) description = models.Field(required=False, display=False) script = models.Field( type=types.Variables(), display=False, help_text='Script code to fetch inventory, prefix with "@" to ' 'use contents of file for this field.') organization = models.Field(type=types.Related('organization'), display=False) ansible-tower-cli-3.2.0/tower_cli/resources/inventory_source.py000066400000000000000000000172251316523067200250400ustar00rootroot00000000000000# Copyright 2015, Ansible, Inc. # Luke Sneeringer # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. import click from tower_cli import models, resources, exceptions as exc from tower_cli.constants import INVENTORY_SOURCE_CHOICES from tower_cli.api import client from tower_cli.cli import types from tower_cli.utils import debug class Resource(models.Resource, models.MonitorableResource): """A resource for inventory sources.""" cli_help = 'Manage inventory sources within Ansible Tower.' endpoint = '/inventory_sources/' unified_job_type = '/inventory_updates/' identity = ('inventory', 'name') name = models.Field(unique=True) description = models.Field(required=False, display=False) inventory = models.Field(type=types.Related('inventory')) source = models.Field( default=None, help_text='The type of inventory source in use.', type=click.Choice(INVENTORY_SOURCE_CHOICES), ) credential = models.Field(type=types.Related('credential'), required=False, display=False) source_vars = models.Field(required=False, display=False) timeout = models.Field(type=int, required=False, display=False, help_text='The timeout field (in seconds).') # Variables not shared by all cloud providers source_project = models.Field(type=types.Related('project'), required=False, display=False, help_text='Use project files as source for inventory.') source_path = models.Field(required=False, display=False, help_text='File in SCM Project to use as source.') update_on_project_update = models.Field(type=bool, required=False, display=False) source_regions = models.Field(required=False, display=False) instance_filters = models.Field(required=False, display=False) group_by = models.Field(required=False, display=False) source_script = models.Field(type=types.Related('inventory_script'), required=False, display=False) # Boolean variables overwrite = models.Field(type=bool, required=False, display=False) overwrite_vars = models.Field(type=bool, required=False, display=False) update_on_launch = models.Field(type=bool, required=False, display=False) # Only used if update_on_launch is used update_cache_timeout = models.Field(type=int, required=False, display=False) @click.argument('inventory_source', type=types.Related('inventory_source')) @click.option('--monitor', is_flag=True, default=False, help='If sent, immediately calls `monitor` on the newly ' 'launched job rather than exiting with a success.') @click.option('--wait', is_flag=True, default=False, help='Polls server for status, exists when finished.') @click.option('--timeout', required=False, type=int, help='If provided with --monitor, this command (not the job)' ' will time out after the given number of seconds. ' 'Does nothing if --monitor is not sent.') @resources.command(use_fields_as_options=False, no_args_is_help=True) def update(self, inventory_source, monitor=False, wait=False, timeout=None, **kwargs): """Update the given inventory source. =====API DOCS===== Update the given inventory source. :param inventory_source: Primary key or name of the inventory source to be updated. :type inventory_source: str :param monitor: Flag that if set, immediately calls ``monitor`` on the newly launched inventory update rather than exiting with a success. :type monitor: bool :param wait: Flag that if set, monitor the status of the inventory update, but do not print while it is in progress. :type wait: bool :param timeout: If provided with ``monitor`` flag set, this attempt will time out after the given number of seconds. :type timeout: int :param `**kwargs`: Fields used to override underlyingl inventory source fields when creating and launching an inventory update. :returns: Result of subsequent ``monitor`` call if ``monitor`` flag is on; Result of subsequent ``wait`` call if ``wait`` flag is on; dictionary of "status" if none of the two flags are on. :rtype: dict :raises tower_cli.exceptions.BadRequest: When the inventory source cannot be updated. =====API DOCS===== """ # Establish that we are able to update this inventory source # at all. debug.log('Asking whether the inventory source can be updated.', header='details') r = client.get('%s%d/update/' % (self.endpoint, inventory_source)) if not r.json()['can_update']: raise exc.BadRequest('Tower says it cannot run an update against this inventory source.') # Run the update. debug.log('Updating the inventory source.', header='details') r = client.post('%s%d/update/' % (self.endpoint, inventory_source), data={}) inventory_update_id = r.json()['inventory_update'] # If we were told to monitor the project update's status, do so. if monitor or wait: if monitor: result = self.monitor(inventory_update_id, parent_pk=inventory_source, timeout=timeout) elif wait: result = self.wait(inventory_update_id, parent_pk=inventory_source, timeout=timeout) inventory = client.get('/inventory_sources/%d/' % result['inventory_source']).json()['inventory'] result['inventory'] = int(inventory) return result # Done. return { 'id': inventory_update_id, 'status': 'ok' } @resources.command @click.option('--detail', is_flag=True, default=False, help='Print more detail.') def status(self, pk, detail=False, **kwargs): """Print the status of the most recent sync. =====API DOCS===== Retrieve the current inventory update status. :param pk: Primary key of the resource to retrieve status from. :type pk: int :param detail: Flag that if set, return the full JSON of the job resource rather than a status summary. :type detail: bool :param `**kwargs`: Keyword arguments used to look up resource object to retrieve status from if ``pk`` is not provided. :returns: full loaded JSON of the specified unified job if ``detail`` flag is on; trimed JSON containing only "elapsed", "failed" and "status" fields of the unified job if ``detail`` flag is off. :rtype: dict =====API DOCS===== """ # Obtain the most recent inventory sync job = self.last_job_data(pk, **kwargs) # In most cases, we probably only want to know the status of the job # and the amount of time elapsed. However, if we were asked for # verbose information, provide it. if detail: return job # Print just the information we need. return { 'elapsed': job['elapsed'], 'failed': job['failed'], 'status': job['status'], } ansible-tower-cli-3.2.0/tower_cli/resources/inventory_update.py000066400000000000000000000033131316523067200250130ustar00rootroot00000000000000# Copyright 2017, Red Hat, Inc. # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. from __future__ import absolute_import, unicode_literals import click from tower_cli import models from tower_cli.constants import LAUNCH_TYPE_CHOICES, STATUS_CHOICES, INVENTORY_SOURCE_CHOICES from tower_cli.cli import types class Resource(models.ExeResource): """A resource for inventory source updates. """ cli_help = 'Launch or monitor inventory source updates.' endpoint = '/inventory_updates/' inventory_source = models.Field( key='-I', type=types.Related('inventory_source'), required=True, display=True ) name = models.Field(required=False, display=True, read_only=True) launch_type = models.Field( type=click.Choice(LAUNCH_TYPE_CHOICES), read_only=True, display=True ) status = models.Field( type=click.Choice(STATUS_CHOICES), read_only=True ) job_explanation = models.Field(required=False, display=False, read_only=True) created = models.Field(required=False, display=True, read_only=True) elapsed = models.Field(required=False, display=True, read_only=True) source = models.Field( type=click.Choice(INVENTORY_SOURCE_CHOICES), display=True ) ansible-tower-cli-3.2.0/tower_cli/resources/job.py000066400000000000000000000264411316523067200221750ustar00rootroot00000000000000# Copyright 2015, Ansible, Inc. # Luke Sneeringer # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. from __future__ import absolute_import, unicode_literals from getpass import getpass from distutils.version import LooseVersion import click from tower_cli import models, get_resource, resources from tower_cli.api import client from tower_cli.cli import types from tower_cli.utils import debug, parser PROMPT_LIST = ['diff_mode', 'limit', 'tags', 'skip_tags', 'job_type', 'verbosity', 'inventory', 'credential'] class Resource(models.ExeResource): """A resource for jobs. This resource has ordinary list and get methods, but it does not have create or modify. Instead of being created, a job is launched. """ cli_help = 'Launch or monitor jobs.' endpoint = '/jobs/' job_template = models.Field( key='-J', type=types.Related('job_template'), required=False, display=True ) job_explanation = models.Field(required=False, display=False) created = models.Field(required=False, display=True) status = models.Field(required=False, display=True) elapsed = models.Field(required=False, display=True) @resources.command( use_fields_as_options=('job_template', 'job_explanation') ) @click.option('--monitor', is_flag=True, default=False, help='If sent, immediately calls `job monitor` on the newly ' 'launched job rather than exiting with a success.') @click.option('--wait', is_flag=True, default=False, help='Monitor the status of the job, but do not print ' 'while job is in progress.') @click.option('--timeout', required=False, type=int, help='If provided with --monitor, this command (not the job)' ' will time out after the given number of seconds. ' 'Does nothing if --monitor is not sent.') @click.option('--no-input', is_flag=True, default=False, help='Suppress any requests for input.') @click.option('-e', '--extra-vars', required=False, multiple=True, help='yaml format text that contains extra variables ' 'to pass on. Use @ to get these from a file.') @click.option('--diff-mode', type=bool, required=False, help='Specify diff mode for job template to run.') @click.option('--limit', required=False, help='Specify host limit for job template to run.') @click.option('--tags', required=False, help='Specify tagged actions in the playbook to run.') @click.option('--skip-tags', required=False, help='Specify tagged actions in the playbook to ommit.') @click.option('--job-type', required=False, type=click.Choice(['run', 'check']), help='Specify job type for job template to run.') @click.option('--verbosity', type=int, required=False, help='Specify verbosity of the playbook run.') @click.option('--inventory', required=False, type=types.Related('inventory'), help='Specify inventory for job template to run.') @click.option('--credential', required=False, type=types.Related('credential'), help='Specify machine credential for job template to run.') def launch(self, job_template=None, monitor=False, wait=False, timeout=None, no_input=True, extra_vars=None, **kwargs): """Launch a new job based on a job template. Creates a new job in Ansible Tower, immediately starts it, and returns back an ID in order for its status to be monitored. =====API DOCS===== Launch a new job based on a job template. :param job_template: Primary key or name of the job template to launch new job. :type job_template: str :param monitor: Flag that if set, immediately calls ``monitor`` on the newly launched job rather than exiting with a success. :type monitor: bool :param wait: Flag that if set, monitor the status of the job, but do not print while job is in progress. :type wait: bool :param timeout: If provided with ``monitor`` flag set, this attempt will time out after the given number of seconds. :type timeout: int :param no_input: Flag that if set, suppress any requests for input. :type no_input: bool :param extra_vars: yaml formatted texts that contains extra variables to pass on. :type extra_vars: array of strings :param diff_mode: Specify diff mode for job template to run. :type diff_mode: bool :param limit: Specify host limit for job template to run. :type limit: str :param tags: Specify tagged actions in the playbook to run. :type tags: str :param skip_tags: Specify tagged actions in the playbook to ommit. :type skip_tags: str :param job_type: Specify job type for job template to run. :type job_type: str :param verbosity: Specify verbosity of the playbook run. :type verbosity: int :param inventory: Specify machine credential for job template to run. :type inventory: str :param credential: Specify machine credential for job template to run. :type credential: str :returns: Result of subsequent ``monitor`` call if ``monitor`` flag is on; Result of subsequent ``wait`` call if ``wait`` flag is on; Result of subsequent ``status`` call if none of the two flags are on. :rtype: dict =====API DOCS===== """ # Get the job template from Ansible Tower. # This is used as the baseline for starting the job. tags = kwargs.get('tags', None) jt_resource = get_resource('job_template') jt = jt_resource.get(job_template) # Update the job data by adding an automatically-generated job name, # and removing the ID. data = {} if tags: data['job_tags'] = tags # Initialize an extra_vars list that starts with the job template # preferences first, if they exist extra_vars_list = [] if 'extra_vars' in data and len(data['extra_vars']) > 0: # But only do this for versions before 2.3 debug.log('Getting version of Tower.', header='details') r = client.get('/config/') if LooseVersion(r.json()['version']) < LooseVersion('2.4'): extra_vars_list = [data['extra_vars']] # Add the runtime extra_vars to this list if extra_vars: extra_vars_list += list(extra_vars) # accept tuples # If the job template requires prompting for extra variables, # do so (unless --no-input is set). if jt.get('ask_variables_on_launch', False) and not no_input \ and not extra_vars: # If JT extra_vars are JSON, echo them to user as YAML initial = parser.process_extra_vars( [jt['extra_vars']], force_json=False ) initial = '\n'.join(( '# Specify extra variables (if any) here as YAML.', '# Lines beginning with "#" denote comments.', initial, )) extra_vars = click.edit(initial) or '' if extra_vars != initial: extra_vars_list = [extra_vars] # Data is starting out with JT variables, and we only want to # include extra_vars that come from the algorithm here. data.pop('extra_vars', None) # Replace/populate data fields if prompted. modified = set() for resource in PROMPT_LIST: if jt.pop('ask_' + resource + '_on_launch', False) and not no_input: resource_object = kwargs.get(resource, None) if type(resource_object) == types.Related: resource_class = get_resource(resource) resource_object = resource_class.get(resource).pop('id', None) if resource_object is None: debug.log('{0} is asked at launch but not provided'. format(resource), header='warning') elif resource != 'tags': data[resource] = resource_object modified.add(resource) # Dump extra_vars into JSON string for launching job if len(extra_vars_list) > 0: data['extra_vars'] = parser.process_extra_vars( extra_vars_list, force_json=True ) # Create the new job in Ansible Tower. start_data = {} endpoint = '/job_templates/%d/launch/' % jt['id'] if 'extra_vars' in data and len(data['extra_vars']) > 0: start_data['extra_vars'] = data['extra_vars'] if tags: start_data['job_tags'] = data['job_tags'] for resource in PROMPT_LIST: if resource in modified: start_data[resource] = data[resource] # There's a non-trivial chance that we are going to need some # additional information to start the job; in particular, many jobs # rely on passwords entered at run-time. # # If there are any such passwords on this job, ask for them now. debug.log('Asking for information necessary to start the job.', header='details') job_start_info = client.get(endpoint).json() for password in job_start_info.get('passwords_needed_to_start', []): start_data[password] = getpass('Password for %s: ' % password) # Actually start the job. debug.log('Launching the job.', header='details') self._pop_none(kwargs) kwargs.update(start_data) job_started = client.post(endpoint, data=kwargs) # Get the job ID from the result. job_id = job_started.json()['id'] # If returning json indicates any ignored fields, display it in # verbose mode. if job_started.text == '': ignored_fields = {} else: ignored_fields = job_started.json().get('ignored_fields', {}) has_ignored_fields = False for key, value in ignored_fields.items(): if value and value != '{}': if not has_ignored_fields: debug.log('List of ignored fields on the server side:', header='detail') has_ignored_fields = True debug.log('{0}: {1}'.format(key, value)) # Get some information about the running job to print result = self.status(pk=job_id, detail=True) result['changed'] = True # If we were told to monitor the job once it started, then call # monitor from here. if monitor: return self.monitor(job_id, timeout=timeout) elif wait: return self.wait(job_id, timeout=timeout) return result ansible-tower-cli-3.2.0/tower_cli/resources/job_template.py000066400000000000000000000354331316523067200240710ustar00rootroot00000000000000# Copyright 2015, Ansible, Inc. # Luke Sneeringer # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. from __future__ import absolute_import, unicode_literals import click from tower_cli import models, resources from tower_cli.utils import parser from tower_cli.api import client from tower_cli.cli import types class Resource(models.SurveyResource): """A resource for job templates.""" cli_help = 'Manage job templates.' endpoint = '/job_templates/' name = models.Field(unique=True) description = models.Field(required=False, display=False) job_type = models.Field( required=False, display=False, type=click.Choice(['run', 'check']), ) inventory = models.Field(type=types.Related('inventory'), required=False) project = models.Field(type=types.Related('project')) playbook = models.Field() credential = models.Field(display=False, required=False, type=types.Related('credential')) vault_credential = models.Field(type=types.Related('credential'), required=False, display=False) forks = models.Field(type=int, required=False, display=False) limit = models.Field(required=False, display=False) verbosity = models.Field( display=False, type=types.MappedChoice([ (0, 'default'), (1, 'verbose'), (2, 'more_verbose'), (3, 'debug'), (4, 'connection'), (5, 'winrm'), ]), required=False, ) extra_vars = models.Field(type=types.Variables(), required=False, display=False, multiple=True, help_text='Extra variables used by Ansible in YAML or key=value ' 'format. Use @ to get YAML from a file.') job_tags = models.Field(required=False, display=False) force_handlers = models.Field(type=bool, required=False, display=False) skip_tags = models.Field(required=False, display=False) start_at_task = models.Field(required=False, display=False) timeout = models.Field(type=int, required=False, display=False, help_text='The amount of time (in seconds) to run before the task is canceled.') use_fact_cache = models.Field(type=bool, required=False, display=False, help_text='If enabled, Tower will act as an Ansible Fact Cache Plugin;' ' persisting facts at the end of a playbook run to the database' ' and caching facts for use by Ansible.') host_config_key = models.Field( required=False, display=False, help_text='Allow Provisioning Callbacks using this host config key') ask_diff_mode_on_launch = models.Field( type=bool, required=False, display=False, help_text='Ask diff mode on launch.') ask_variables_on_launch = models.Field( type=bool, required=False, display=False, help_text='Prompt user for extra_vars on launch.') ask_limit_on_launch = models.Field( type=bool, required=False, display=False, help_text='Prompt user for host limits on launch.') ask_tags_on_launch = models.Field( type=bool, required=False, display=False, help_text='Prompt user for job tags on launch.') ask_skip_tags_on_launch = models.Field( type=bool, required=False, display=False, help_text='Prompt user for tags to skip on launch.') ask_job_type_on_launch = models.Field( type=bool, required=False, display=False, help_text='Prompt user for job type on launch.') ask_verbosity_on_launch = models.Field( type=bool, required=False, display=False, help_text='Prompt user for verbosity on launch.') ask_inventory_on_launch = models.Field( type=bool, required=False, display=False, help_text='Prompt user for inventory on launch.') ask_credential_on_launch = models.Field( type=bool, required=False, display=False, help_text='Prompt user for machine credential on launch.') survey_enabled = models.Field( type=bool, required=False, display=False, help_text='Prompt user for job type on launch.') become_enabled = models.Field(type=bool, required=False, display=False) diff_mode = models.Field(type=bool, required=False, display=False, help_text='If enabled, textual changes made to any templated files on' ' the host are shown in the standard output.') allow_simultaneous = models.Field(type=bool, required=False, display=False) survey_spec = models.Field( type=types.Variables(), required=False, display=False, help_text='On write commands, perform extra POST to the ' 'survey_spec endpoint.') def write(self, *args, **kwargs): # Provide a default value for job_type, but only in creation of JT if (kwargs.get('create_on_missing', False) and (not kwargs.get('job_type', None))): kwargs['job_type'] = 'run' return super(Resource, self).write(*args, **kwargs) @resources.command(use_fields_as_options=False) @click.option('--job-template', type=types.Related('job_template')) @click.option('--label', type=types.Related('label')) def associate_label(self, job_template, label): """Associate an label with this job template. =====API DOCS===== Associate an label with this job template. :param job_template: The job template to associate to. :type job_template: str :param label: The label to be associated. :type label: str :returns: Dictionary of only one key "changed", which indicates whether the association succeeded. :rtype: dict =====API DOCS===== """ return self._assoc('labels', job_template, label) @resources.command(use_fields_as_options=False) @click.option('--job-template', type=types.Related('job_template')) @click.option('--label', type=types.Related('label')) def disassociate_label(self, job_template, label): """Disassociate an label from this job template. =====API DOCS===== Disassociate an label from this job template. :param job_template: The job template to disassociate from. :type job_template: str :param label: The label to be disassociated. :type label: str :returns: Dictionary of only one key "changed", which indicates whether the disassociation succeeded. :rtype: dict =====API DOCS===== """ return self._disassoc('labels', job_template, label) @resources.command(use_fields_as_options=False) @click.option('--job-template', type=types.Related('job_template')) @click.option('--credential', type=types.Related('credential')) def associate_credential(self, job_template, credential): """Associate a credential with this job template. =====API DOCS===== Associate a credential with this job template. :param job_template: The job template to associate to. :type job_template: str :param credential: The credential to be associated. :type credential: str :returns: Dictionary of only one key "changed", which indicates whether the association succeeded. :rtype: dict =====API DOCS===== """ return self._assoc('extra_credentials', job_template, credential) @resources.command(use_fields_as_options=False) @click.option('--job-template', type=types.Related('job_template')) @click.option('--credential', type=types.Related('credential')) def disassociate_credential(self, job_template, credential): """Disassociate a credential with this job template. =====API DOCS===== Disassociate a credential from this job template. :param job_template: The job template to disassociate fom. :type job_template: str :param credential: The credential to be disassociated. :type credential: str :returns: Dictionary of only one key "changed", which indicates whether the disassociation succeeded. :rtype: dict =====API DOCS===== """ return self._disassoc('extra_credentials', job_template, credential) @resources.command(use_fields_as_options=False) @click.option('--job-template', type=types.Related('job_template')) @click.option('--notification-template', type=types.Related('notification_template')) @click.option('--status', type=click.Choice(['any', 'error', 'success']), required=False, default='any', help='Specify job run status' ' of job template to relate to.') def associate_notification_template(self, job_template, notification_template, status): """Associate a notification template from this job template. =====API DOCS===== Associate a notification template from this job template. :param job_template: The job template to associate to. :type job_template: str :param notification_template: The notification template to be associated. :type notification_template: str :param status: type of notification this notification template should be associated to. :type status: str :returns: Dictionary of only one key "changed", which indicates whether the association succeeded. :rtype: dict =====API DOCS===== """ return self._assoc('notification_templates_%s' % status, job_template, notification_template) @resources.command(use_fields_as_options=False) @click.option('--job-template', type=types.Related('job_template')) @click.option('--notification-template', type=types.Related('notification_template')) @click.option('--status', type=click.Choice(['any', 'error', 'success']), required=False, default='any', help='Specify job run status' ' of job template to relate to.') def disassociate_notification_template(self, job_template, notification_template, status): """Disassociate a notification template from this job template. =====API DOCS===== Disassociate a notification template from this job template. :param job_template: The job template to disassociate from. :type job_template: str :param notification_template: The notification template to be disassociated. :type notification_template: str :param status: type of notification this notification template should be disassociated from. :type status: str :returns: Dictionary of only one key "changed", which indicates whether the disassociation succeeded. :rtype: dict =====API DOCS===== """ return self._disassoc('notification_templates_%s' % status, job_template, notification_template) @resources.command(use_fields_as_options=('extra_vars')) @click.option('--host-config-key', help='Job-template-specific string used to authenticate ' 'host during provisioning callback.') def callback(self, pk=None, host_config_key='', extra_vars=None): """Contact Tower and request a configuration update using this job template. =====API DOCS===== Contact Tower and request a provisioning callback using this job template. :param pk: Primary key of the job template to run provisioning callback against. :type pk: int :param host_config_key: Key string used to authenticate the callback host. :type host_config_key: str :param extra_vars: Extra variables that are passed to provisioning callback. :type extra_vars: array of str :returns: A dictionary of a single key "changed", which indicates whether the provisioning callback is successful. :rtype: dict =====API DOCS===== """ url = self.endpoint + '%s/callback/' % pk if not host_config_key: host_config_key = client.get(url).json()['host_config_key'] post_data = {'host_config_key': host_config_key} if extra_vars: post_data['extra_vars'] = parser.process_extra_vars(list(extra_vars), force_json=True) r = client.post(url, data=post_data, auth=None) if r.status_code == 201: return {'changed': True} @resources.command(use_fields_as_options=False) @click.option('--job-template', type=types.Related('job_template'), required=True) @click.option('--instance-group', type=types.Related('instance_group'), required=True) def associate_ig(self, job_template, instance_group): """Associate an instance group with this job_template. The instance group will be used to run jobs within the job_template. =====API DOCS===== Associate an instance group with this job_template. :param job_template: Primary key or name of the job_template to associate to. :type job_template: str :param instance_group: Primary key or name of the instance group to be associated. :type instance_group: str :returns: Dictionary of only one key "changed", which indicates whether the association succeeded. :rtype: dict =====API DOCS===== """ return self._assoc('instance_groups', job_template, instance_group) @resources.command(use_fields_as_options=False) @click.option('--job-template', type=types.Related('job_template'), required=True) @click.option('--instance-group', type=types.Related('instance_group'), required=True) def disassociate_ig(self, job_template, instance_group): """Disassociate an instance group from this job_template. =====API DOCS===== Disassociate an instance group with this job_template. :param job_template: Primary key or name of the job_template to associate to. :type job_template: str :param instance_group: Primary key or name of the instance group to be associated. :type instance_group: str :returns: Dictionary of only one key "changed", which indicates whether the association succeeded. :rtype: dict =====API DOCS===== """ return self._disassoc('instance_groups', job_template, instance_group) ansible-tower-cli-3.2.0/tower_cli/resources/label.py000066400000000000000000000104161316523067200224750ustar00rootroot00000000000000# Copyright 2016, Ansible by RedHat. # Aaron Tan # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. import click from tower_cli import get_resource, resources, models, exceptions as exc from tower_cli.utils import debug from tower_cli.cli import types class Resource(models.Resource): """A resource for labels.""" cli_help = 'Manage labels within Ansible Tower.' endpoint = '/labels/' name = models.Field(unique=True) organization = models.Field(type=types.Related('organization'), display=False) def __getattribute__(self, name): """Disable inherited methods that cannot be applied to this particular resource. """ if name in ['delete']: raise AttributeError else: return object.__getattribute__(self, name) @resources.command @click.option('--job-template', type=types.Related('job_template'), required=False, help='The job template to relate to.') def create(self, fail_on_found=False, force_on_exists=False, **kwargs): """Create a new label. There are two types of label creation: isolatedly creating a new label and creating a new label under a job template. Here the two types are discriminated by whether to provide --job-template option. Fields in the resource's `identity` tuple are used for a lookup; if a match is found, then no-op (unless `force_on_exists` is set) but do not fail (unless `fail_on_found` is set). =====API DOCS===== Create a label. :param job_template: Primary key or name of the job template for the created label to associate to. :type job_template: str :param fail_on_found: Flag that if set, the operation fails if an object matching the unique criteria already exists. :type fail_on_found: bool :param force_on_exists: Flag that if set, then if a match is found on unique fields, other fields will be updated to the provided values.; If unset, a match causes the request to be a no-op. :type force_on_exists: bool :param `**kwargs`: Keyword arguements which, all together, will be used as POST body to create the resource object. :returns: A dictionary combining the JSON output of the created resource, as well as two extra fields: "changed", a flag indicating if the resource is created successfully; "id", an integer which is the primary key of the created object. :rtype: dict :raises tower_cli.exceptions.TowerCLIError: When the label already exists and ``fail_on_found`` flag is on. =====API DOCS===== """ jt_id = kwargs.pop('job_template', None) old_endpoint = self.endpoint if jt_id is not None: jt = get_resource('job_template') jt.get(pk=jt_id) try: label_id = self.get(name=kwargs.get('name', None), organization=kwargs.get('organization', None))['id'] except exc.NotFound: pass else: if fail_on_found: raise exc.TowerCLIError('Label already exists and fail-on-found is switched on. Please use' ' "associate_label" method of job_template instead.') else: debug.log('Label already exists, associating with job template.', header='details') return jt.associate_label(jt_id, label_id) self.endpoint = '/job_templates/%d/labels/' % jt_id result = super(Resource, self).create(fail_on_found=fail_on_found, force_on_exists=force_on_exists, **kwargs) self.endpoint = old_endpoint return result ansible-tower-cli-3.2.0/tower_cli/resources/node.py000066400000000000000000000225131316523067200223440ustar00rootroot00000000000000# Copyright 2016, Ansible by Red Hat # Alan Rominger # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. from __future__ import absolute_import, unicode_literals from tower_cli import models, resources, exceptions from tower_cli.cli import types from tower_cli.utils.resource_decorators import unified_job_template_options from tower_cli.utils import debug from tower_cli.api import client import click NODE_STANDARD_FIELDS = [ 'unified_job_template', 'inventory', 'credential', 'job_type', 'job_tags', 'skip_tags', 'limit' ] JOB_TYPES = { 'job': 'job_template', 'project_update': 'project', 'inventory_update': 'inventory_source' } class Resource(models.Resource): """A resource for workflow nodes.""" cli_help = 'Manage nodes inside of a workflow job template.' endpoint = '/workflow_job_template_nodes/' identity = ('id',) workflow_job_template = models.Field( key='-W', type=types.Related('workflow')) unified_job_template = models.Field(required=False) inventory = models.Field( type=types.Related('inventory'), required=False, display=False) credential = models.Field( type=types.Related('credential'), required=False, display=False) job_type = models.Field(required=False, display=False) job_tags = models.Field(required=False, display=False) skip_tags = models.Field(required=False, display=False) limit = models.Field(required=False, display=False) def __new__(cls, *args, **kwargs): for attr in ['create', 'modify', 'list']: setattr(cls, attr, unified_job_template_options(getattr(cls, attr))) return super(Resource, cls).__new__(cls, *args, **kwargs) @staticmethod def _forward_rel_name(rel): return '{0}_nodes'.format(rel) @staticmethod def _reverse_rel_name(rel): return 'workflowjobtemplatenodes_{0}'.format(rel) def _parent_filter(self, parent, relationship, **kwargs): """ Returns filtering parameters to limit a search to the children of a particular node by a particular relationship. """ if parent is None or relationship is None: return {} parent_filter_kwargs = {} query_params = ((self._reverse_rel_name(relationship), parent),) parent_filter_kwargs['query'] = query_params if kwargs.get('workflow_job_template', None) is None: parent_data = self.read(pk=parent)['results'][0] parent_filter_kwargs['workflow_job_template'] = parent_data[ 'workflow_job_template'] return parent_filter_kwargs @unified_job_template_options def _get_or_create_child(self, parent, relationship, **kwargs): ujt_pk = kwargs.get('unified_job_template', None) if ujt_pk is None: raise exceptions.BadRequest( 'A child node must be specified by one of the options ' 'unified-job-template, job-template, project, or ' 'inventory-source') kwargs.update(self._parent_filter(parent, relationship, **kwargs)) response = self.read( fail_on_no_results=False, fail_on_multiple_results=False, **kwargs) if len(response['results']) == 0: debug.log('Creating new workflow node.', header='details') return client.post(self.endpoint, data=kwargs).json() else: return response['results'][0] def _assoc_or_create(self, relationship, parent, child, **kwargs): if child is None: child_data = self._get_or_create_child(parent, relationship, **kwargs) return child_data return self._assoc(self._forward_rel_name(relationship), parent, child) @resources.command @unified_job_template_options @click.argument('parent', type=types.Related('node')) @click.argument('child', type=types.Related('node'), required=False) def associate_success_node(self, parent, child=None, **kwargs): """Add a node to run on success. =====API DOCS===== Add a node to run on success. :param parent: Primary key of parent node to associate success node to. :type parent: int :param child: Primary key of child node to be associated. :type child: int :param `**kwargs`: Fields used to create child node if ``child`` is not provided. :returns: Dictionary of only one key "changed", which indicates whether the association succeeded. :rtype: dict =====API DOCS===== """ return self._assoc_or_create('success', parent, child, **kwargs) @resources.command(use_fields_as_options=False) @click.argument('parent', type=types.Related('node')) @click.argument('child', type=types.Related('node')) def disassociate_success_node(self, parent, child): """Remove success node. The resulatant 2 nodes will both become root nodes. =====API DOCS===== Remove success node. :param parent: Primary key of parent node to disassociate success node from. :type parent: int :param child: Primary key of child node to be disassociated. :type child: int :returns: Dictionary of only one key "changed", which indicates whether the disassociation succeeded. :rtype: dict =====API DOCS===== """ return self._disassoc( self._forward_rel_name('success'), parent, child) @resources.command @unified_job_template_options @click.argument('parent', type=types.Related('node')) @click.argument('child', type=types.Related('node'), required=False) def associate_failure_node(self, parent, child=None, **kwargs): """Add a node to run on failure. =====API DOCS===== Add a node to run on failure. :param parent: Primary key of parent node to associate failure node to. :type parent: int :param child: Primary key of child node to be associated. :type child: int :param `**kwargs`: Fields used to create child node if ``child`` is not provided. :returns: Dictionary of only one key "changed", which indicates whether the association succeeded. :rtype: dict =====API DOCS===== """ return self._assoc_or_create('failure', parent, child, **kwargs) @resources.command(use_fields_as_options=False) @click.argument('parent', type=types.Related('node')) @click.argument('child', type=types.Related('node')) def disassociate_failure_node(self, parent, child): """Remove a failure node link. The resulatant 2 nodes will both become root nodes. =====API DOCS===== Remove a failure node link. :param parent: Primary key of parent node to disassociate failure node from. :type parent: int :param child: Primary key of child node to be disassociated. :type child: int :returns: Dictionary of only one key "changed", which indicates whether the disassociation succeeded. :rtype: dict =====API DOCS===== """ return self._disassoc( self._forward_rel_name('failure'), parent, child) @resources.command @unified_job_template_options @click.argument('parent', type=types.Related('node')) @click.argument('child', type=types.Related('node'), required=False) def associate_always_node(self, parent, child=None, **kwargs): """Add a node to always run after the parent is finished. =====API DOCS===== Add a node to always run after the parent is finished. :param parent: Primary key of parent node to associate always node to. :type parent: int :param child: Primary key of child node to be associated. :type child: int :param `**kwargs`: Fields used to create child node if ``child`` is not provided. :returns: Dictionary of only one key "changed", which indicates whether the association succeeded. :rtype: dict =====API DOCS===== """ return self._assoc_or_create('always', parent, child, **kwargs) @resources.command(use_fields_as_options=False) @click.argument('parent', type=types.Related('node')) @click.argument('child', type=types.Related('node')) def disassociate_always_node(self, parent, child): """Remove an always node link. The resultant 2 nodes will both become root nodes. =====API DOCS===== Remove an always node link. :param parent: Primary key of parent node to disassociate always node from. :type parent: int :param child: Primary key of child node to be disassociated. :type child: int :returns: Dictionary of only one key "changed", which indicates whether the disassociation succeeded. :rtype: dict =====API DOCS===== """ return self._disassoc( self._forward_rel_name('always'), parent, child) ansible-tower-cli-3.2.0/tower_cli/resources/notification_template.py000066400000000000000000000475221316523067200260070ustar00rootroot00000000000000# Copyright 2016, Ansible, Inc. # Aaron Tan # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. import click import json import copy from tower_cli import get_resource, models, resources, exceptions as exc from tower_cli.utils import debug from tower_cli.cli import types class Resource(models.Resource): """A resource for notification templates.""" cli_help = 'Manage notification templates within Ansible Tower.' endpoint = '/notification_templates/' # Actual fields name = models.Field(unique=True) description = models.Field(required=False, display=False) organization = models.Field(type=types.Related('organization'), required=False, display=False) notification_type = models.Field( type=click.Choice(['email', 'slack', 'twilio', 'pagerduty', 'hipchat', 'webhook', 'irc']) ) notification_configuration = models.Field( type=models.File('r', lazy=True), required=False, display=False, help_text='The notification configuration field. Note providing this' ' field would disable all notification-configuration-related' ' fields.' ) # Fields that are part of notification_configuration config_fields = ['notification_configuration', 'channels', 'token', 'username', 'sender', 'recipients', 'use_tls', 'host', 'use_ssl', 'password', 'port', 'account_token', 'from_number', 'to_numbers', 'account_sid', 'subdomain', 'service_key', 'client_name', 'message_from', 'api_url', 'color', 'notify', 'rooms', 'url', 'headers', 'server', 'nickname', 'targets'] # Fields that are part of notification_configuration which are categorized # according to notification_type configuration = { 'slack': ['channels', 'token'], 'email': ['username', 'sender', 'recipients', 'use_tls', 'host', 'use_ssl', 'password', 'port'], 'twilio': ['account_token', 'from_number', 'to_numbers', 'account_sid'], 'pagerduty': ['token', 'subdomain', 'service_key', 'client_name'], 'hipchat': ['message_from', 'api_url', 'color', 'token', 'notify', 'rooms'], 'webhook': ['url', 'headers'], 'irc': ['server', 'port', 'use_ssl', 'password', 'nickname', 'targets'] } # Fields which are expected to be json files. json_fields = ['notification_configuration', 'headers'] encrypted_fields = ['password', 'token', 'account_token'] # notification_configuration-related fields. fields with default values # are optional. username = models.Field(required=False, display=False, help_text='[{0}]The username.'.format('email')) sender = models.Field(required=False, display=False, help_text='[{0}]The sender.'.format('email')) recipients = models.Field(required=False, display=False, multiple=True, help_text='[{0}]The recipients.'.format('email')) use_tls = models.Field(required=False, display=False, type=click.BOOL, default=False, help_text='[{0}]The tls trigger.'.format('email')) host = models.Field(required=False, display=False, help_text='[{0}]The host.'.format('email')) use_ssl = models.Field(required=False, display=False, type=click.BOOL, default=False, help_text='[{0}]The ssl trigger.' .format('email/irc')) password = models.Field(required=False, display=False, password=True, help_text='[{0}]The password.'.format('email/irc')) port = models.Field(required=False, display=False, type=click.INT, help_text='[{0}]The email port.'.format('email/irc')) channels = models.Field(required=False, display=False, multiple=True, help_text='[{0}]The channel.'.format('slack')) token = models.Field(required=False, display=False, password=True, help_text='[{0}]The token.'. format('slack/pagerduty/hipchat')) account_token = models.Field(required=False, display=False, password=True, help_text='[{0}]The account token.'. format('twilio')) from_number = models.Field(required=False, display=False, help_text='[{0}]The source phone number.'. format('twilio')) to_numbers = models.Field(required=False, display=False, multiple=True, help_text='[{0}]The destination SMS numbers.'. format('twilio')) account_sid = models.Field(required=False, display=False, help_text='[{0}The account sid.'. format('twilio')) subdomain = models.Field(required=False, display=False, help_text='[{0}]The subdomain.'. format('pagerduty')) service_key = models.Field(required=False, display=False, help_text='[{0}]The API service/integration' ' key.'.format('pagerduty')) client_name = models.Field(required=False, display=False, help_text='[{0}]The client identifier.'. format('pagerduty')) message_from = models.Field(required=False, display=False, help_text='[{0}]The label to be shown with ' 'notification.'.format('hipchat')) api_url = models.Field(required=False, display=False, help_text='[{0}]The api url.'.format('hipchat')) color = models.Field(required=False, display=False, type=click.Choice(['yellow', 'green', 'red', 'purple', 'gray', 'random']), help_text='[{0}]The notification color.'. format('hipchat')) rooms = models.Field(required=False, display=False, default=False, help_text='[{0}]Rooms to send notification to. ' 'Use multiple flags to send to multiple rooms, ex ' '--rooms=A --rooms=B'. format('hipchat'), multiple=True) notify = models.Field(required=False, display=False, default=False, help_text='[{0}]The notify channel trigger.'. format('hipchat')) url = models.Field(required=False, display=False, help_text='[{0}]The target URL.'.format('webhook')) headers = models.Field(required=False, display=False, type=models.File('r', lazy=True), help_text='[{0}]The http headers.'. format('webhook')) server = models.Field(required=False, display=False, help_text='[{0}]Server address.'.format('irc')) nickname = models.Field(required=False, display=False, help_text='[{0}]The irc nick.'.format('irc')) target = models.Field(required=False, display=False, help_text='[{0}]The distination channels or users.' .format('irc')) def _separate(self, kwargs): """Remove None-valued and configuration-related keyworded arguments """ self._pop_none(kwargs) result = {} for field in Resource.config_fields: if field in kwargs: result[field] = kwargs.pop(field) if field in Resource.json_fields: try: data = json.loads(result[field]) result[field] = data except ValueError: raise exc.TowerCLIError('Provided json file format ' 'invalid. Please recheck.') return result def _configuration(self, kwargs, config_item): """Combine configuration-related keyworded arguments into notification_configuration. """ if 'notification_configuration' not in config_item: if 'notification_type' not in kwargs: return nc = kwargs['notification_configuration'] = {} for field in Resource.configuration[kwargs['notification_type']]: if field not in config_item: raise exc.TowerCLIError('Required config field %s not' ' provided.' % field) else: nc[field] = config_item[field] else: kwargs['notification_configuration'] = \ config_item['notification_configuration'] @resources.command @click.option('--job-template', type=types.Related('job_template'), required=False, help='The job template to relate to.') @click.option('--status', type=click.Choice(['error', 'success']), required=False, help='Specify job run status of job ' 'template to relate to.') def create(self, fail_on_found=False, force_on_exists=False, **kwargs): """Create a notification template. All required configuration-related fields (required according to notification_type) must be provided. There are two types of notification template creation: isolatedly creating a new notification template and creating a new notification template under a job template. Here the two types are discriminated by whether to provide --job-template option. --status option controls more specific, job-run-status-related association. Fields in the resource's `identity` tuple are used for a lookup; if a match is found, then no-op (unless `force_on_exists` is set) but do not fail (unless `fail_on_found` is set). =====API DOCS===== Create an object. :param fail_on_found: Flag that if set, the operation fails if an object matching the unique criteria already exists. :type fail_on_found: bool :param force_on_exists: Flag that if set, then if a match is found on unique fields, other fields will be updated to the provided values.; If unset, a match causes the request to be a no-op. :type force_on_exists: bool :param `**kwargs`: Keyword arguements which, all together, will be used as POST body to create the resource object. :returns: A dictionary combining the JSON output of the created resource, as well as two extra fields: "changed", a flag indicating if the resource is created successfully; "id", an integer which is the primary key of the created object. :rtype: dict =====API DOCS===== """ config_item = self._separate(kwargs) jt_id = kwargs.pop('job_template', None) status = kwargs.pop('status', 'any') old_endpoint = self.endpoint if jt_id is not None: jt = get_resource('job_template') jt.get(pk=jt_id) try: nt_id = self.get(**copy.deepcopy(kwargs))['id'] except exc.NotFound: pass else: if fail_on_found: raise exc.TowerCLIError('Notification template already ' 'exists and fail-on-found is ' 'switched on. Please use' ' "associate_notification" method' ' of job_template instead.') else: debug.log('Notification template already exists, ' 'associating with job template.', header='details') return jt.associate_notification_template( jt_id, nt_id, status=status) self.endpoint = '/job_templates/%d/notification_templates_%s/' %\ (jt_id, status) self._configuration(kwargs, config_item) result = super(Resource, self).create(**kwargs) self.endpoint = old_endpoint return result @resources.command def modify(self, pk=None, create_on_missing=False, **kwargs): """Modify an existing notification template. Not all required configuration-related fields (required according to notification_type) should be provided. Fields in the resource's `identity` tuple can be used in lieu of a primary key for a lookup; in such a case, only other fields are written. To modify unique fields, you must use the primary key for the lookup. =====API DOCS===== Modify an already existing object. :param pk: Primary key of the resource to be modified. :type pk: int :param create_on_missing: Flag that if set, a new object is created if ``pk`` is not set and objects matching the appropriate unique criteria is not found. :type create_on_missing: bool :param `**kwargs`: Keyword arguements which, all together, will be used as PATCH body to modify the resource object. if ``pk`` is not set, key-value pairs of ``**kwargs`` which are also in resource's identity will be used to lookup existing reosource. :returns: A dictionary combining the JSON output of the modified resource, as well as two extra fields: "changed", a flag indicating if the resource is successfully updated; "id", an integer which is the primary key of the updated object. :rtype: dict =====API DOCS===== """ # Create the resource if needed. if pk is None and create_on_missing: try: self.get(**copy.deepcopy(kwargs)) except exc.NotFound: return self.create(**kwargs) # Modify everything except notification type and configuration config_item = self._separate(kwargs) notification_type = kwargs.pop('notification_type', None) debug.log('Modify everything except notification type and' ' configuration', header='details') part_result = super(Resource, self).\ modify(pk=pk, create_on_missing=create_on_missing, **kwargs) # Modify notification type and configuration if notification_type is None or \ notification_type == part_result['notification_type']: for item in part_result['notification_configuration']: if item not in config_item or not config_item[item]: to_add = part_result['notification_configuration'][item] if not (to_add == '$encrypted$' and item in Resource.encrypted_fields): config_item[item] = to_add if notification_type is None: kwargs['notification_type'] = part_result['notification_type'] else: kwargs['notification_type'] = notification_type self._configuration(kwargs, config_item) debug.log('Modify notification type and configuration', header='details') result = super(Resource, self).\ modify(pk=pk, create_on_missing=create_on_missing, **kwargs) # Update 'changed' field to give general changed info if 'changed' in result and 'changed' in part_result: result['changed'] = result['changed'] or part_result['changed'] return result @resources.command def delete(self, pk=None, fail_on_missing=False, **kwargs): """Remove the given notification template. Note here configuration-related fields like 'notification_configuration' and 'channels' will not be used even provided. If `fail_on_missing` is True, then the object's not being found is considered a failure; otherwise, a success with no change is reported. =====API DOCS===== Remove the given object. :param pk: Primary key of the resource to be deleted. :type pk: int :param fail_on_missing: Flag that if set, the object's not being found is considered a failure; otherwise, a success with no change is reported. :type fail_on_missing: bool :param `**kwargs`: Keyword arguments used to look up resource object to delete if ``pk`` is not provided. :returns: dictionary of only one field "changed", which is a flag indicating whether the specified resource is successfully deleted. :rtype: dict =====API DOCS===== """ self._separate(kwargs) return super(Resource, self).\ delete(pk=pk, fail_on_missing=fail_on_missing, **kwargs) @resources.command def list(self, all_pages=False, **kwargs): """Return a list of notification templates. Note here configuration-related fields like 'notification_configuration' and 'channels' will not be used even provided. If one or more filters are provided through keyword arguments, filter the results accordingly. If no filters are provided, return all results. =====API DOCS===== Retrieve a list of objects. :param all_pages: Flag that if set, collect all pages of content from the API when returning results. :type all_pages: bool :param page: The page to show. Ignored if all_pages is set. :type page: int :param query: Contains 2-tuples used as query parameters to filter resulting resource objects. :type query: list :param `**kwargs`: Keyword arguments list of available fields used for searching resource objects. :returns: A JSON object containing details of all resource objects returned by Tower backend. :rtype: dict =====API DOCS===== """ self._separate(kwargs) return super(Resource, self).list(all_pages=all_pages, **kwargs) @resources.command def get(self, pk=None, **kwargs): """Return one and exactly one notification template. Note here configuration-related fields like 'notification_configuration' and 'channels' will not be used even provided. Lookups may be through a primary key, specified as a positional argument, and/or through filters specified through keyword arguments. If the number of results does not equal one, raise an exception. =====API DOCS===== Retrieve one and exactly one object. :param pk: Primary key of the resource to be read. Tower CLI will only attempt to read *that* object if ``pk`` is provided (not ``None``). :type pk: int :param `**kwargs`: Keyword arguments used to look up resource object to retrieve if ``pk`` is not provided. :returns: loaded JSON of the retrieved resource object. :rtype: dict =====API DOCS===== """ self._separate(kwargs) return super(Resource, self).get(pk=pk, **kwargs) ansible-tower-cli-3.2.0/tower_cli/resources/organization.py000066400000000000000000000143611316523067200241250ustar00rootroot00000000000000# Copyright 2015, Ansible, Inc. # Luke Sneeringer # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. import click from tower_cli import models, resources from tower_cli.cli import types class Resource(models.Resource): cli_help = 'Manage organizations within Ansible Tower.' endpoint = '/organizations/' deprecated_methods = ['associate_project', 'disassociate_project'] name = models.Field(unique=True) description = models.Field(required=False, display=False) @resources.command(use_fields_as_options=False) @click.option('--organization', type=types.Related('organization'), required=True) @click.option('--user', type=types.Related('user'), required=True) def associate(self, organization, user): """Associate a user with this organization. =====API DOCS===== Associate a user with this organization. :param organization: Primary key or name of the organization to associate to. :type organization: str :param user: Primary key or name of the user to be associated. :type user: str :returns: Dictionary of only one key "changed", which indicates whether the association succeeded. :rtype: dict =====API DOCS===== """ return self._assoc('users', organization, user) @resources.command(use_fields_as_options=False) @click.option('--organization', type=types.Related('organization'), required=True) @click.option('--user', type=types.Related('user'), required=True) def associate_admin(self, organization, user): """Associate an admin with this organization. =====API DOCS===== Associate an admin with this organization. :param organization: Primary key or name of the organization to associate to. :type organization: str :param user: Primary key or name of the user to be associated. :type user: str :returns: Dictionary of only one key "changed", which indicates whether the association succeeded. :rtype: dict =====API DOCS===== """ return self._assoc('admins', organization, user) @resources.command(use_fields_as_options=False) @click.option('--organization', type=types.Related('organization'), required=True) @click.option('--user', type=types.Related('user'), required=True) def disassociate(self, organization, user): """Disassociate a user from this organization. =====API DOCS===== Disassociate a user from this organization. :param organization: Primary key or name of the organization to disassociate from. :type organization: str :param user: Primary key or name of the user to be disassociated. :type user: str :returns: Dictionary of only one key "changed", which indicates whether the disassociation succeeded. :rtype: dict =====API DOCS===== """ return self._disassoc('users', organization, user) @resources.command(use_fields_as_options=False) @click.option('--organization', type=types.Related('organization'), required=True) @click.option('--user', type=types.Related('user'), required=True) def disassociate_admin(self, organization, user): """Disassociate an admin from this organization. =====API DOCS===== Disassociate an admin from this organization. :param organization: Primary key or name of the organization to disassociate from. :type organization: str :param user: Primary key or name of the user to be disassociated. :type user: str :returns: Dictionary of only one key "changed", which indicates whether the disassociation succeeded. :rtype: dict =====API DOCS===== """ return self._disassoc('admins', organization, user) @resources.command(use_fields_as_options=False) @click.option('--organization', type=types.Related('organization'), required=True) @click.option('--instance-group', type=types.Related('instance_group'), required=True) def associate_ig(self, organization, instance_group): """Associate an instance group with this organization. The instance group will be used to run jobs within the organization. =====API DOCS===== Associate an instance group with this organization. :param organization: Primary key or name of the organization to associate to. :type organization: str :param instance_group: Primary key or name of the instance group to be associated. :type instance_group: str :returns: Dictionary of only one key "changed", which indicates whether the association succeeded. :rtype: dict =====API DOCS===== """ return self._assoc('instance_groups', organization, instance_group) @resources.command(use_fields_as_options=False) @click.option('--organization', type=types.Related('organization'), required=True) @click.option('--instance-group', type=types.Related('instance_group'), required=True) def disassociate_ig(self, organization, instance_group): """Disassociate an instance group from this organization. =====API DOCS===== Disassociate an instance group with this organization. :param organization: Primary key or name of the organization to associate to. :type organization: str :param instance_group: Primary key or name of the instance group to be associated. :type instance_group: str :returns: Dictionary of only one key "changed", which indicates whether the association succeeded. :rtype: dict =====API DOCS===== """ return self._disassoc('instance_groups', organization, instance_group) ansible-tower-cli-3.2.0/tower_cli/resources/project.py000066400000000000000000000335011316523067200230640ustar00rootroot00000000000000# Copyright 2015, Ansible, Inc. # Luke Sneeringer # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. import click from tower_cli import models, get_resource, resources, exceptions as exc from tower_cli.api import client from tower_cli.cli import types from tower_cli.utils import debug class Resource(models.Resource, models.MonitorableResource): """A resource for projects.""" cli_help = 'Manage projects within Ansible Tower.' endpoint = '/projects/' unified_job_type = '/project_updates/' name = models.Field(unique=True) description = models.Field(required=False, display=False) organization = models.Field(type=types.Related('organization'), display=False, required=False) scm_type = models.Field( type=types.MappedChoice([ ('', 'manual'), ('git', 'git'), ('hg', 'hg'), ('svn', 'svn'), ('insights', 'insights'), ]), ) scm_url = models.Field(required=False) local_path = models.Field( help_text='For manual projects, the server playbook directory name.', required=False) scm_branch = models.Field(required=False, display=False) scm_credential = models.Field( 'credential', display=False, required=False, type=types.Related('credential'), ) scm_clean = models.Field(type=bool, required=False, display=False) scm_delete_on_update = models.Field(type=bool, required=False, display=False) scm_update_on_launch = models.Field(type=bool, required=False, display=False) scm_update_cache_timeout = models.Field(type=int, required=False, display=False) job_timeout = models.Field(type=int, required=False, display=False, help_text='The timeout field (in seconds).') @resources.command @click.option('--monitor', is_flag=True, default=False, help='If sent, immediately calls `project monitor` on the ' 'project rather than exiting with a success.' 'It polls for status until the SCM is updated.') @click.option('--wait', is_flag=True, default=False, help='Polls server for status, exists when finished.') @click.option('--timeout', required=False, type=int, help='If provided with --monitor, the SCM update' ' will time out after the given number of seconds. ' 'Does nothing if --monitor is not sent.') def create(self, organization=None, monitor=False, wait=False, timeout=None, fail_on_found=False, force_on_exists=False, **kwargs): """Create a new item of resource, with or w/o org. This would be a shared class with user, but it needs the ability to monitor if the flag is set. =====API DOCS===== Create a project and, if related flags are set, monitor or wait the triggered initial project update. :param monitor: Flag that if set, immediately calls ``monitor`` on the newly triggered project update rather than exiting with a success. :type monitor: bool :param wait: Flag that if set, monitor the status of the triggered project update, but do not print while it is in progress. :type wait: bool :param timeout: If provided with ``monitor`` flag set, this attempt will time out after the given number of seconds. :type timeout: bool :param fail_on_found: Flag that if set, the operation fails if an object matching the unique criteria already exists. :type fail_on_found: bool :param force_on_exists: Flag that if set, then if a match is found on unique fields, other fields will be updated to the provided values.; If unset, a match causes the request to be a no-op. :type force_on_exists: bool :param `**kwargs`: Keyword arguements which, all together, will be used as POST body to create the resource object. :returns: A dictionary combining the JSON output of the created resource, as well as two extra fields: "changed", a flag indicating if the resource is created successfully; "id", an integer which is the primary key of the created object. :rtype: dict =====API DOCS===== """ if 'job_timeout' in kwargs and 'timeout' not in kwargs: kwargs['timeout'] = kwargs.pop('job_timeout') post_associate = False if organization: # Processing the organization flag depends on version debug.log('Checking Organization Relationship.', header='details') r = client.options('/projects/') if 'organization' in r.json()['actions']['POST']: kwargs['organization'] = organization else: post_associate = True # First, run the create method, ignoring the organization given answer = super(Resource, self).write( create_on_missing=True, fail_on_found=fail_on_found, force_on_exists=force_on_exists, **kwargs ) project_id = answer['id'] # If an organization is given, associate it here if post_associate: # Get the organization from Tower, will lookup name if needed org_resource = get_resource('organization') org_data = org_resource.get(organization) org_pk = org_data['id'] debug.log("associating the project with its organization", header='details', nl=1) org_resource._assoc('projects', org_pk, project_id) # if the monitor flag is set, wait for the SCM to update if monitor and answer.get('changed', False): return self.monitor(pk=None, parent_pk=project_id, timeout=timeout) elif wait and answer.get('changed', False): return self.wait(pk=None, parent_pk=project_id, timeout=timeout) return answer @resources.command(use_fields_as_options=( 'name', 'description', 'scm_type', 'scm_url', 'local_path', 'scm_branch', 'scm_credential', 'scm_clean', 'scm_delete_on_update', 'scm_update_on_launch', 'job_timeout' )) def modify(self, pk=None, create_on_missing=False, **kwargs): """Modify an already existing. To edit the project's organizations, see help for organizations. Fields in the resource's `identity` tuple can be used in lieu of a primary key for a lookup; in such a case, only other fields are written. To modify unique fields, you must use the primary key for the lookup. =====API DOCS===== Modify an already existing project. :param pk: Primary key of the resource to be modified. :type pk: int :param create_on_missing: Flag that if set, a new object is created if ``pk`` is not set and objects matching the appropriate unique criteria is not found. :type create_on_missing: bool :param `**kwargs`: Keyword arguements which, all together, will be used as PATCH body to modify the resource object. if ``pk`` is not set, key-value pairs of ``**kwargs`` which are also in resource's identity will be used to lookup existing reosource. :returns: A dictionary combining the JSON output of the modified resource, as well as two extra fields: "changed", a flag indicating if the resource is successfully updated; "id", an integer which is the primary key of the updated object. :rtype: dict =====API DOCS===== """ # Associated with issue #52, the organization can't be modified # with the 'modify' command. This would create confusion about # whether its flag is an identifier versus a field to modify. if 'job_timeout' in kwargs and 'timeout' not in kwargs: kwargs['timeout'] = kwargs.pop('job_timeout') return super(Resource, self).write( pk, create_on_missing=create_on_missing, force_on_exists=True, **kwargs ) @resources.command(use_fields_as_options=('name', 'organization')) @click.option('--monitor', is_flag=True, default=False, help='If sent, immediately calls `job monitor` on the newly ' 'launched job rather than exiting with a success.') @click.option('--wait', is_flag=True, default=False, help='Polls server for status, exists when finished.') @click.option('--timeout', required=False, type=int, help='If provided with --monitor, this command (not the job)' ' will time out after the given number of seconds. ' 'Does nothing if --monitor is not sent.') def update(self, pk=None, create_on_missing=False, monitor=False, wait=False, timeout=None, name=None, organization=None): """Trigger a project update job within Ansible Tower. Only meaningful on non-manual projects. =====API DOCS===== Update the given project. :param pk: Primary key of the project to be updated. :type pk: int :param monitor: Flag that if set, immediately calls ``monitor`` on the newly launched project update rather than exiting with a success. :type monitor: bool :param wait: Flag that if set, monitor the status of the project update, but do not print while it is in progress. :type wait: bool :param timeout: If provided with ``monitor`` flag set, this attempt will time out after the given number of seconds. :type timeout: int :param name: Name of the project to be updated if ``pk`` is not set. :type name: str :param organization: Primary key or name of the organization the project to be updated belonging to if ``pk`` is not set. :type organization: str :returns: Result of subsequent ``monitor`` call if ``monitor`` flag is on; Result of subsequent ``wait`` call if ``wait`` flag is on; dictionary of "status" if none of the two flags are on. :rtype: dict :raises tower_cli.exceptions.CannotStartJob: When the project cannot be updated. =====API DOCS===== """ # First, get the appropriate project. # This should be uniquely identified at this point, and if not, then # we just want the error that `get` will throw to bubble up. project = self.get(pk, name=name, organization=organization) pk = project['id'] # Determine whether this project is able to be updated. debug.log('Asking whether the project can be updated.', header='details') result = client.get('/projects/%d/update/' % pk) if not result.json()['can_update']: raise exc.CannotStartJob('Cannot update project.') # Okay, this project can be updated, according to Tower. # Commence the update. debug.log('Updating the project.', header='details') result = client.post('/projects/%d/update/' % pk) project_update_id = result.json()['project_update'] # If we were told to monitor the project update's status, do so. if monitor: return self.monitor(project_update_id, parent_pk=pk, timeout=timeout) elif wait: return self.wait(project_update_id, parent_pk=pk, timeout=timeout) # Return the project update ID. return { 'id': project_update_id, 'changed': True, } @resources.command @click.option('--detail', is_flag=True, default=False, help='Print more detail.') def status(self, pk=None, detail=False, **kwargs): """Print the status of the most recent update. =====API DOCS===== Print the status of the most recent update. :param pk: Primary key of the resource to retrieve status from. :type pk: int :param detail: Flag that if set, return the full JSON of the job resource rather than a status summary. :type detail: bool :param `**kwargs`: Keyword arguments used to look up resource object to retrieve status from if ``pk`` is not provided. :returns: full loaded JSON of the specified unified job if ``detail`` flag is on; trimed JSON containing only "elapsed", "failed" and "status" fields of the unified job if ``detail`` flag is off. :rtype: dict =====API DOCS===== """ # Obtain the most recent project update job = self.last_job_data(pk, **kwargs) # In most cases, we probably only want to know the status of the job # and the amount of time elapsed. However, if we were asked for # verbose information, provide it. if detail: return job # Print just the information we need. return { 'elapsed': job['elapsed'], 'failed': job['failed'], 'status': job['status'], } ansible-tower-cli-3.2.0/tower_cli/resources/project_update.py000066400000000000000000000035731316523067200244340ustar00rootroot00000000000000# Copyright 2017, Red Hat, Inc. # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. from __future__ import absolute_import, unicode_literals import click from tower_cli import models from tower_cli.constants import LAUNCH_TYPE_CHOICES, STATUS_CHOICES from tower_cli.cli import types class Resource(models.ExeResource): """A resource for project updates. """ cli_help = 'Launch or monitor project updates.' endpoint = '/project_updates/' project = models.Field( key='-P', type=types.Related('project'), required=True, display=True ) name = models.Field(required=False, display=True, read_only=True) launch_type = models.Field( type=click.Choice(LAUNCH_TYPE_CHOICES), read_only=True, display=False ) status = models.Field( type=click.Choice(STATUS_CHOICES), read_only=True ) job_type = models.Field( type=click.Choice(['run', 'check']), read_only=True ) job_explanation = models.Field(required=False, display=False, read_only=True) created = models.Field(required=False, display=True, read_only=True) elapsed = models.Field(required=False, display=True, read_only=True) scm_type = models.Field( type=types.MappedChoice([ ('', 'manual'), ('git', 'git'), ('hg', 'hg'), ('svn', 'svn'), ('insights', 'insights'), ]), display=False ) ansible-tower-cli-3.2.0/tower_cli/resources/role.py000066400000000000000000000375131316523067200223660ustar00rootroot00000000000000# Copyright 2016, Ansible by Red Hat # Alan Rominger # Aaron Tan # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. import click from tower_cli import models, resources, exceptions as exc from tower_cli.api import client from tower_cli.cli import types from tower_cli.utils import debug from tower_cli.conf import settings ACTOR_FIELDS = ['user', 'team'] RESOURCE_FIELDS = [ 'target_team', 'credential', 'inventory', 'job_template', 'organization', 'project', 'workflow'] ROLE_TYPES = [ 'admin', 'read', 'member', 'execute', 'adhoc', 'update', 'use', 'auditor'] class Resource(models.Resource): """A resource for managing roles. This resource has ordinary list and get methods, but it roles can not be created or edited, instead, they are automatically generated along with the connected resource. """ cli_help = 'Add and remove users/teams from roles.' endpoint = '/roles/' user = models.Field(type=types.Related('user'), required=False, display=True) team = models.Field( type=types.Related('team'), required=False, display=True, help_text='The team that receives the permissions ' 'specified by the role.') type = models.Field( required=False, display=True, type=click.Choice(ROLE_TYPES), help_text='The type of permission that the role controls.') # These fields are never valid input arguments, # they are only used as columns in output resource_name = models.Field(required=False, display=False) resource_type = models.Field(required=False, display=False) # These are purely resource fields, and are always inputs, # but are only selectively set as output columns target_team = models.Field( type=types.Related('team'), required=False, display=False, help_text='The team that the role acts on.') credential = models.Field(type=types.Related('credential'), required=False, display=False) inventory = models.Field(type=types.Related('inventory'), required=False, display=False) job_template = models.Field(type=types.Related('job_template'), required=False, display=False) credential = models.Field(type=types.Related('credential'), required=False, display=False) organization = models.Field(type=types.Related('organization'), required=False, display=False) project = models.Field(type=types.Related('project'), required=False, display=False) workflow = models.Field(type=types.Related('workflow'), required=False, display=False) def __getattribute__(self, name): """Disable inherited methods that cannot be applied to this particular resource. """ if name in ['create', 'delete', 'modify']: raise AttributeError else: return object.__getattribute__(self, name) @staticmethod def pluralize(kind): if kind == 'inventory': return 'inventories' elif kind == 'workflow': return 'workflow_job_templates' else: return '%ss' % kind @staticmethod def obj_res(data, fail_on=['type', 'obj', 'res']): """ Given some CLI input data, Returns the following and their types: obj - the role grantee res - the resource that the role applies to """ errors = [] if not data.get('type', None) and 'type' in fail_on: errors += ['You must provide a role type to use this command.'] # Find the grantee, and remove them from resource_list obj = None obj_type = None for fd in ACTOR_FIELDS: if data.get(fd, False): if not obj: obj = data[fd] obj_type = fd else: errors += ['You can not give a role to a user ' 'and team at the same time.'] break if not obj and 'obj' in fail_on: errors += ['You must specify either user or ' 'team to use this command.'] # Out of the resource list, pick out available valid resource field res = None res_type = None for fd in RESOURCE_FIELDS: if data.get(fd, False): if not res: res = data[fd] res_type = fd if res_type == 'target_team': res_type = 'team' else: errors += ['You can only give a role to one ' 'type of resource at a time.'] break if not res and 'res' in fail_on: errors += ['You must specify a target resource ' 'to use this command.'] if errors: raise exc.UsageError("\n".join(errors)) return obj, obj_type, res, res_type @classmethod def data_endpoint(cls, in_data, ignore=[]): """ Converts a set of CLI input arguments, `in_data`, into request data and an endpoint that can be used to look up a role or list of roles. Also changes the format of `type` in data to what the server expects for the role model, as it exists in the database. """ obj, obj_type, res, res_type = cls.obj_res(in_data, fail_on=[]) data = {} if 'obj' in ignore: obj = None if 'res' in ignore: res = None # Input fields are not actually present on role model, and all have # to be managed as individual special-cases if obj and obj_type == 'user': data['members__in'] = obj if obj and obj_type == 'team': endpoint = '%s/%s/roles/' % (cls.pluralize(obj_type), obj) if res is not None: # For teams, this is the best lookup we can do # without making the additional request for its member_role data['object_id'] = res elif res: endpoint = '%s/%s/object_roles/' % (cls.pluralize(res_type), res) else: endpoint = '/roles/' if in_data.get('type', False): data['role_field'] = '%s_role' % in_data['type'].lower() return data, endpoint @staticmethod def populate_resource_columns(item_dict): """Operates on item_dict Promotes the resource_name and resource_type fields to the top-level of the serialization so they can be printed as columns. Also makes a copies name field to type, which is a default column.""" item_dict['type'] = item_dict['name'] if len(item_dict['summary_fields']) == 0: # Singleton roles ommit these fields item_dict['resource_name'] = None item_dict['resource_type'] = None else: item_dict['resource_name'] = item_dict[ 'summary_fields']['resource_name'] item_dict['resource_type'] = item_dict[ 'summary_fields']['resource_type'] def set_display_columns(self, set_true=[], set_false=[]): """Add or remove columns from the output.""" for i in range(len(self.fields)): if self.fields[i].name in set_true: self.fields[i].display = True elif self.fields[i].name in set_false: self.fields[i].display = False def configure_display(self, data, kwargs=None, write=False): """Populates columns and sets display attribute as needed. Operates on data.""" if settings.format != 'human': return # This is only used for human format if write: obj, obj_type, res, res_type = self.obj_res(kwargs) data['type'] = kwargs['type'] data[obj_type] = obj data[res_type] = res self.set_display_columns( set_false=['team' if obj_type == 'user' else 'user'], set_true=['target_team' if res_type == 'team' else res_type]) else: self.set_display_columns( set_false=['user', 'team'], set_true=['resource_name', 'resource_type']) if 'results' in data: for i in range(len(data['results'])): self.populate_resource_columns(data['results'][i]) else: self.populate_resource_columns(data) def role_write(self, fail_on_found=False, disassociate=False, **kwargs): """Re-implementation of the parent `write` method specific to roles. Adds a grantee (user or team) to the resource's role.""" # Get the role, using only the resource data data, self.endpoint = self.data_endpoint(kwargs, ignore=['obj']) debug.log('Checking if role exists.', header='details') response = self.read(pk=None, fail_on_no_results=True, fail_on_multiple_results=True, **data) role_data = response['results'][0] role_id = role_data['id'] # Role exists, change display settings to output something self.configure_display(role_data, kwargs, write=True) # Check if user/team has this role # Implictly, force_on_exists is false for roles obj, obj_type, res, res_type = self.obj_res(kwargs) debug.log('Checking if %s already has role.' % obj_type, header='details') data, self.endpoint = self.data_endpoint(kwargs) data['content_type__model'] = res_type.replace('_', '') response = self.read(pk=None, fail_on_no_results=False, fail_on_multiple_results=False, **data) msg = '' if response['count'] > 0 and not disassociate: msg = 'This %s is already a member of the role.' % obj_type elif response['count'] == 0 and disassociate: msg = 'This %s is already a non-member of the role.' % obj_type if msg: role_data['changed'] = False if fail_on_found: raise exc.NotFound(msg) else: debug.log(msg, header='DECISION') return role_data # Add or remove the user/team to the role debug.log('Attempting to %s the %s in this role.' % ( 'remove' if disassociate else 'add', obj_type), header='details') post_data = {'id': role_id} if disassociate: post_data['disassociate'] = True client.post('%s/%s/roles/' % (self.pluralize(obj_type), obj), data=post_data) role_data['changed'] = True return role_data # Command method for roles # TODO: write commands to see access_list for resource @resources.command( use_fields_as_options=ACTOR_FIELDS+RESOURCE_FIELDS+['type']) def list(self, **kwargs): """Return a list of roles. =====API DOCS===== Retrieve a list of objects. :param all_pages: Flag that if set, collect all pages of content from the API when returning results. :type all_pages: bool :param page: The page to show. Ignored if all_pages is set. :type page: int :param query: Contains 2-tuples used as query parameters to filter resulting resource objects. :type query: list :param `**kwargs`: Keyword arguments list of available fields used for searching resource objects. :returns: A JSON object containing details of all resource objects returned by Tower backend. :rtype: dict =====API DOCS===== """ data, self.endpoint = self.data_endpoint(kwargs) r = super(Resource, self).list(**data) # Change display settings and data format for human consumption self.configure_display(r) return r @resources.command( use_fields_as_options=ACTOR_FIELDS+RESOURCE_FIELDS+['type']) def get(self, pk=None, **kwargs): """Get information about a role. =====API DOCS===== Retrieve one and exactly one object. :param pk: Primary key of the resource to be read. Tower CLI will only attempt to read *that* object if ``pk`` is provided (not ``None``). :type pk: int :param `**kwargs`: Keyword arguments used to look up resource object to retrieve if ``pk`` is not provided. :returns: loaded JSON of the retrieved resource object. :rtype: dict =====API DOCS===== """ if kwargs.pop('include_debug_header', True): debug.log('Getting the role record.', header='details') data, self.endpoint = self.data_endpoint(kwargs) response = self.read(pk=pk, fail_on_no_results=True, fail_on_multiple_results=True, **data) item_dict = response['results'][0] self.configure_display(item_dict) return item_dict @resources.command( use_fields_as_options=ACTOR_FIELDS+RESOURCE_FIELDS+['type']) @click.option('--fail-on-found', default=False, show_default=True, type=bool, is_flag=True, help='If used, return an error if the user already has the ' 'role.') def grant(self, fail_on_found=False, **kwargs): """Add a user or a team to a role. Required information: 1) Type of the role 2) Resource of the role, inventory, credential, or any other 3) A user or a team to add to the role =====API DOCS===== Add a user or a team to a role. Required information: * Type of the role. * Resource of the role, inventory, credential, or any other. * A user or a team to add to the role. :param fail_on_found: Flag that if set, the operation fails if a user/team already has the role. :type fail_on_found: bool :param `**kwargs`: The user to be associated and the role to associate. :returns: parsed JSON of role grant. :rtype: dict =====API DOCS===== """ return self.role_write(fail_on_found=fail_on_found, **kwargs) @resources.command( use_fields_as_options=ACTOR_FIELDS+RESOURCE_FIELDS+['type']) @click.option('--fail-on-found', default=False, show_default=True, type=bool, is_flag=True, help='If used, return an error if the user is already ' 'not a member of the role.') def revoke(self, fail_on_found=False, **kwargs): """Remove a user or a team from a role. Required information: 1) Type of the role 2) Resource of the role, inventory, credential, or any other 3) A user or a team to add to the role =====API DOCS===== Remove a user or a team from a role. Required information: * Type of the role. * Resource of the role, inventory, credential, or any other. * A user or a team to add to the role. :param fail_on_found: Flag that if set, the operation fails if a user/team dose not have the role. :type fail_on_found: bool :param `**kwargs`: The user to be disassociated and the role to disassociate. :returns: parsed JSON of role revoke. :rtype: dict =====API DOCS===== """ return self.role_write(fail_on_found=fail_on_found, disassociate=True, **kwargs) ansible-tower-cli-3.2.0/tower_cli/resources/schedule.py000066400000000000000000000134721316523067200232170ustar00rootroot00000000000000# Copyright 2016, Ansible by Red Hat. # Aaron Tan # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. import click from tower_cli import models, exceptions as exc from tower_cli.cli import types UNIFIED_JT = { 'job_template': '/job_templates', 'inventory_source': '/inventory_sources', 'project': '/projects', } CLICK_ATTRS = ('__click_params__', '_cli_command', '_cli_command_attrs') def jt_aggregate(func, is_create=False, has_pk=False): """Decorator to aggregate unified_jt-related fields. Args: func: The CURD method to be decorated. is_create: Boolean flag showing whether this method is create. has_pk: Boolean flag showing whether this method uses pk as argument. Returns: A function with necessary click-related attributes whose keyworded arguments are aggregated. Raises: exc.UsageError: Either more than one unified jt fields are provided, or none is provided when is_create flag is set. """ def helper(kwargs, obj): """The helper function preceding actual function that aggregates unified jt fields. """ unified_job_template = None for item in UNIFIED_JT: if kwargs.get(item, None) is not None: jt_id = kwargs.pop(item) if unified_job_template is None: unified_job_template = (item, jt_id) else: raise exc.UsageError( 'More than one unified job template fields provided, ' 'please tighten your criteria.' ) if unified_job_template is not None: kwargs['unified_job_template'] = unified_job_template[1] obj.identity = tuple(list(obj.identity) + ['unified_job_template']) return '/'.join([UNIFIED_JT[unified_job_template[0]], str(unified_job_template[1]), 'schedules/']) elif is_create: raise exc.UsageError('You must provide exactly one unified job' ' template field during creation.') def decorator_without_pk(obj, *args, **kwargs): old_endpoint = obj.endpoint new_endpoint = helper(kwargs, obj) if is_create: obj.endpoint = new_endpoint result = func(obj, *args, **kwargs) obj.endpoint = old_endpoint return result def decorator_with_pk(obj, pk=None, *args, **kwargs): old_endpoint = obj.endpoint new_endpoint = helper(kwargs, obj) if is_create: obj.endpoint = new_endpoint result = func(obj, pk=pk, *args, **kwargs) obj.endpoint = old_endpoint return result decorator = decorator_with_pk if has_pk else decorator_without_pk for item in CLICK_ATTRS: setattr(decorator, item, getattr(func, item, [])) decorator.__doc__ = func.__doc__ return decorator class Resource(models.Resource): """A resource for schedules.""" cli_help = 'Manage schedules within Ansible Tower.' endpoint = '/schedules/' # General fields. name = models.Field(unique=True) description = models.Field(required=False, display=False) # Unified jt fields. note these fields will only be used during creation. # Plus, one and only one field should be provided. job_template = models.Field(type=types.Related('job_template'), required=False, display=False) inventory_source = models.Field(type=types.Related('inventory_source'), required=False, display=False) project = models.Field(type=types.Related('project'), required=False, display=False) # Schedule-specific fields. unified_job_template = models.Field(required=False, type=int, help_text='Integer used to display' ' unified job template in result, ' 'Do not use it for create/' 'modify.') enabled = models.Field(required=False, type=click.BOOL, default=True, help_text='Whether this schedule will be used.', show_default=True) rrule = models.Field(required=False, display=False, help_text='Schedule rules specifications which is' ' less than 255 characters.') extra_data = models.Field(type=types.Variables(), required=False, display=False, help_text='Extra data for ' 'schedule rules in the form of a .json file.') def _get_patch_url(self, url, pk): urlTokens = url.split('/') if len(urlTokens) > 3: # reconstruct url to prevent a rare corner case where resources # cannot be constructed independently. Open to modification if # API convention changes. url = '/'.join(urlTokens[:1] + urlTokens[-2:]) return super(Resource, self)._get_patch_url(url, pk) Resource.create = jt_aggregate(Resource.create, is_create=True) Resource.delete = jt_aggregate(Resource.delete, has_pk=True) Resource.get = jt_aggregate(Resource.get, has_pk=True) Resource.list = jt_aggregate(Resource.list) Resource.modify = jt_aggregate(Resource.modify, has_pk=True) ansible-tower-cli-3.2.0/tower_cli/resources/setting.py000066400000000000000000000143751316523067200231030ustar00rootroot00000000000000# Copyright 2017, Ansible by Red Hat # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. import ast import json from distutils.util import strtobool import click import six from tower_cli import models, resources, exceptions as exc from tower_cli.api import client from tower_cli.conf import pop_option from tower_cli.cli import types from tower_cli.utils.data_structures import OrderedDict class Resource(models.Resource): """A resource for Tower configurations.""" cli_help = 'Manage settings within Ansible Tower.' custom_category = None value = models.Field(required=True, type=types.Variables()) @resources.command(ignore_defaults=True, no_args_is_help=False) @click.option('category', '-c', '--category', help='If set, filter settings by a specific category') def list(self, **kwargs): """Return a list of objects. =====API DOCS===== Retrieve a list of Tower settings. :param category: The category slug in which to look up indevidual settings. :type category: str :param `**kwargs`: Keyword arguments list of available fields used for searching resource objects. :returns: A JSON object containing details of all resource objects returned by Tower backend. :rtype: dict =====API DOCS===== """ self.custom_category = kwargs.get('category', 'all') try: result = super(Resource, self).list(**kwargs) except exc.NotFound as e: categories = map( lambda category: category['slug'], client.get('/settings/').json()['results'] ) e.message = '%s is not a valid category. Choose from [%s]' % ( kwargs['category'], ', '.join(categories) ) raise e finally: self.custom_category = None return { 'results': [{'id': k, 'value': v} for k, v in result.items()] } @resources.command(use_fields_as_options=False) def get(self, pk): """Return one and exactly one object =====API DOCS===== Return one and exactly one Tower setting. :param pk: Primary key of the Tower setting to retrieve :type pk: int :returns: loaded JSON of the retrieved Tower setting object. :rtype: dict :raises tower_cli.exceptions.NotFound: When no specified Tower setting exists. =====API DOCS===== """ # The Tower API doesn't provide a mechanism for retrieving a single # setting value at a time, so fetch them all and filter try: return next(s for s in self.list()['results'] if s['id'] == pk) except StopIteration: raise exc.NotFound('The requested object could not be found.') @resources.command(use_fields_as_options=False) @click.argument('setting') @click.argument('value', default=None, required=False, type=types.Variables()) def modify(self, setting, value): """Modify an already existing object. Positional argument SETTING is the setting name and VALUE is its value, which can be provided directly or obtained from a file name if prefixed with '@'. =====API DOCS===== Modify an already existing Tower setting. :param setting: The name of the Tower setting to be modified. :type setting: str :param value: The new value of the Tower setting. :type value: str :returns: A dictionary combining the JSON output of the modified resource, as well as two extra fields: "changed", a flag indicating if the resource is successfully updated; "id", an integer which is the primary key of the updated object. :rtype: dict =====API DOCS===== """ prev_value = new_value = self.get(setting)['value'] answer = OrderedDict() encrypted = '$encrypted$' in six.text_type(prev_value) if encrypted or six.text_type(prev_value) != six.text_type(value): if setting == 'LICENSE': r = client.post('/config/', data=self.coerce_type(setting, value)) new_value = r.json() else: r = client.patch( self.endpoint, data={setting: self.coerce_type(setting, value)} ) new_value = r.json()[setting] answer.update(r.json()) changed = encrypted or (prev_value != new_value) answer.update({ 'changed': changed, 'id': setting, 'value': new_value, }) return answer @property def endpoint(self): return '/settings/%s/' % (self.custom_category or 'all') def coerce_type(self, key, value): if key == 'LICENSE': return json.loads(value) r = client.options(self.endpoint) to_type = r.json()['actions']['PUT'].get(key, {}).get('type') if to_type == 'integer': return int(value) elif to_type == 'boolean': return bool(strtobool(value)) elif to_type in ('list', 'nested object'): return ast.literal_eval(value) return value def __getattribute__(self, name): """Disable inherited methods that cannot be applied to this particular resource. """ if name in ['create', 'delete']: raise AttributeError else: return object.__getattribute__(self, name) # Settings don't support pagination, and there's nothing to filter on pop_option(Resource.list, 'all_pages') pop_option(Resource.list, 'page') pop_option(Resource.list, 'query') # Settings don't have a `create` operation pop_option(Resource.modify, 'create_on_missing') ansible-tower-cli-3.2.0/tower_cli/resources/team.py000066400000000000000000000047231316523067200223500ustar00rootroot00000000000000# Copyright 2015, Ansible, Inc. # Luke Sneeringer # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. import click from tower_cli import models, resources from tower_cli.cli import types class Resource(models.Resource): """A resource for teams.""" cli_help = 'Manage teams within Ansible Tower.' endpoint = '/teams/' identity = ('organization', 'name') name = models.Field(unique=True) organization = models.Field(type=types.Related('organization')) description = models.Field(required=False, display=False) @resources.command(use_fields_as_options=False) @click.option('--team', type=types.Related('team')) @click.option('--user', type=types.Related('user')) def associate(self, team, user): """Associate a user with this team. =====API DOCS===== Associate a user with this team. :param team: Primary key or name of the team to associate to. :type team: str :param user: Primary key or name of the user to be associated. :type user: str :returns: Dictionary of only one key "changed", which indicates whether the association succeeded. :rtype: dict =====API DOCS===== """ return self._assoc('users', team, user) @resources.command(use_fields_as_options=False) @click.option('--team', type=types.Related('team')) @click.option('--user', type=types.Related('user')) def disassociate(self, team, user): """Disassociate a user from this team. =====API DOCS===== Disassociate a user from this team. :param organization: Primary key or name of the team to disassociate from. :type organization: str :param user: Primary key or name of the user to be disassociated. :type user: str :returns: Dictionary of only one key "changed", which indicates whether the disassociation succeeded. :rtype: dict """ return self._disassoc('users', team, user) ansible-tower-cli-3.2.0/tower_cli/resources/unified_job.py000066400000000000000000000022551316523067200236750ustar00rootroot00000000000000# Copyright 2017, Ansible by Red Hat # Alan Rominger # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. from tower_cli import models class Resource(models.Resource): """A resource for unified jobs.""" cli_help = 'Combined model of projects, job templates, and others.' endpoint = '/unified_jobs/' internal = True name = models.Field(required=False, display=True, col_width=24) type = models.Field(required=False, display=True, col_width=14) status = models.Field(required=False, display=True, col_width=10) created = models.Field(required=False, display=True, col_width=24) elapsed = models.Field(required=False, display=True, col_width=7) ansible-tower-cli-3.2.0/tower_cli/resources/user.py000066400000000000000000000022321316523067200223710ustar00rootroot00000000000000# Copyright 2015, Ansible, Inc. # Luke Sneeringer # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. from tower_cli import models class Resource(models.Resource): """A resource for users.""" cli_help = 'Manage users within Ansible Tower.' endpoint = '/users/' identity = ('username',) username = models.Field(unique=True) password = models.Field(required=False, display=False) email = models.Field(unique=True) first_name = models.Field(required=False) last_name = models.Field(required=False) is_superuser = models.Field(required=False, type=bool) is_system_auditor = models.Field(required=False, type=bool) ansible-tower-cli-3.2.0/tower_cli/resources/workflow.py000066400000000000000000000406121316523067200232710ustar00rootroot00000000000000# Copyright 2016, Ansible by Red Hat # Alan Rominger # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. from __future__ import absolute_import, unicode_literals from tower_cli import models, resources, get_resource from tower_cli.cli import types from tower_cli.utils.parser import string_to_dict from tower_cli.exceptions import BadRequest from tower_cli.conf import settings from tower_cli.resources.node import NODE_STANDARD_FIELDS, JOB_TYPES import click from collections import deque class TreeNode(object): def __init__(self, data, wfjt, include_id=False): ujt_attrs = list(JOB_TYPES.values()) FK_FIELDS = ujt_attrs + ['inventory', 'credential'] node_attrs = {} attr_names = NODE_STANDARD_FIELDS + ujt_attrs if include_id: attr_names.append('id') for fd in attr_names: if fd not in data: continue if fd in FK_FIELDS and not isinstance(data[fd], int): # Node's template was given by name, do lookup ujt_res = get_resource(fd) ujt_data = ujt_res.get(name=data[fd]) node_attrs[fd] = ujt_data['id'] else: node_attrs[fd] = data[fd] node_attrs['workflow_job_template'] = wfjt for ujt_name in ujt_attrs: if ujt_name not in node_attrs: continue if 'unified_job_template' not in node_attrs: node_attrs['unified_job_template'] = node_attrs.pop(ujt_name) else: raise BadRequest( 'You should not provide more than one of the attributes' ' job_template, project and inventory_source.' ) self.unified_job_template = node_attrs.get('unified_job_template', None) self.node_attrs = node_attrs for rel in ['success_nodes', 'failure_nodes', 'always_nodes']: setattr( self, rel, [TreeNode(x, wfjt, include_id=include_id) for x in data.get(rel, data.get(rel[: -6], []))] ) def create(self, node_res): self.node_attrs['id'] = node_res.create(**self.node_attrs)['id'] queue = deque() queue.append(self) while queue: node = queue.popleft() for rel in ['success_nodes', 'failure_nodes', 'always_nodes']: for sub_node in getattr(node, rel, []): sub_node.node_attrs['id'] = node_res.create(**sub_node.node_attrs)['id'] getattr(node_res, 'associate_%s' % rel[:-1])( node.node_attrs['id'], child=sub_node.node_attrs['id'] ) queue.append(sub_node) def delete(self, node_res): for rel in ['success_nodes', 'failure_nodes', 'always_nodes']: for sub_node in getattr(self, rel, []): sub_node.delete(node_res) node_res.delete(pk=self.node_attrs['id']) def _compare_node_lists(old, new): ''' Investigate two lists of workflow TreeNodes and categorize them. There will be three types of nodes after categorization: 1. Nodes that only exists in the new list. These nodes will later be created recursively. 2. Nodes that only exists in the old list. These nodes will later be deleted recursively. 3. Node pairs that makes an exact match. These nodes will be further investigated. Corresponding nodes of old and new lists will be distinguished by their unified_job_template value. A special case is that both the old and the new lists contain one type of node, say A, and at least one of them contains duplicates. In this case all A nodes in the old list will be categorized as to-be-deleted and all A nodes in the new list will be categorized as to-be-created. ''' to_expand = [] to_delete = [] to_recurse = [] old_records = {} new_records = {} for tree_node in old: old_records.setdefault(tree_node.unified_job_template, []) old_records[tree_node.unified_job_template].append(tree_node) for tree_node in new: new_records.setdefault(tree_node.unified_job_template, []) new_records[tree_node.unified_job_template].append(tree_node) for ujt_id in old_records: if ujt_id not in new_records: to_delete.extend(old_records[ujt_id]) continue old_list = old_records[ujt_id] new_list = new_records.pop(ujt_id) if len(old_list) == 1 and len(new_list) == 1: to_recurse.append((old_list[0], new_list[0])) else: to_delete.extend(old_list) to_expand.extend(new_list) for nodes in new_records.values(): to_expand.extend(nodes) return to_expand, to_delete, to_recurse def _do_update_workflow(existing_roots, updated_roots, node_res): to_expand, to_delete, to_recurse = _compare_node_lists(existing_roots, updated_roots) for node in to_delete: node.delete(node_res) for node in to_expand: node.create(node_res) for old_node, new_node in to_recurse: for rel in ['success_nodes', 'failure_nodes', 'always_nodes']: to_assoc = _do_update_workflow(getattr(old_node, rel, []), getattr(new_node, rel, []), node_res) for sub_node in to_assoc: getattr(node_res, 'associate_%s' % rel[:-1])(old_node.node_attrs['id'], child=sub_node.node_attrs['id']) return to_expand def _update_workflow(existing_roots, updated_roots): # Node resource should be fetched *only once*. node_res = get_resource('node') _do_update_workflow(existing_roots, updated_roots, node_res) class Resource(models.SurveyResource): """A resource for workflow job templates.""" cli_help = 'Manage workflow job templates.' endpoint = '/workflow_job_templates/' unified_job_type = '/workflow_jobs/' name = models.Field(unique=True) description = models.Field(required=False, display=False) extra_vars = models.Field( type=types.Variables(), required=False, display=False, multiple=True, help_text='Extra variables used by Ansible in YAML or key=value ' 'format. Use @ to get YAML from a file. Use the option ' 'multiple times to add multiple extra variables.') organization = models.Field(type=types.Related('organization'), required=False) survey_enabled = models.Field( type=bool, required=False, display=False, help_text='Prompt user for job type on launch.') allow_simultaneous = models.Field(type=bool, required=False, display=False) survey_spec = models.Field( type=types.Variables(), required=False, display=False, help_text='On write commands, perform extra POST to the ' 'survey_spec endpoint.') @staticmethod def _workflow_node_structure(node_results): ''' Takes the list results from the API in `node_results` and translates this data into a dictionary organized in a human-readable heirarchial structure ''' # Build list address translation, and create backlink lists node_list_pos = {} for i, node_result in enumerate(node_results): for rel in ['success', 'failure', 'always']: node_result['{0}_backlinks'.format(rel)] = [] node_list_pos[node_result['id']] = i # Populate backlink lists for node_result in node_results: for rel in ['success', 'failure', 'always']: for sub_node_id in node_result['{0}_nodes'.format(rel)]: j = node_list_pos[sub_node_id] node_results[j]['{0}_backlinks'.format(rel)].append( node_result['id']) # Find the root nodes root_nodes = [] for node_result in node_results: is_root = True for rel in ['success', 'failure', 'always']: if node_result['{0}_backlinks'.format(rel)] != []: is_root = False break if is_root: root_nodes.append(node_result['id']) # Create network dictionary recursively from root nodes def branch_schema(node_id): i = node_list_pos[node_id] node_dict = node_results[i] ret_dict = {"id": node_id} for fd in NODE_STANDARD_FIELDS: val = node_dict.get(fd, None) if val is not None: if fd == 'unified_job_template': job_type = node_dict['summary_fields'][ 'unified_job_template']['unified_job_type'] ujt_key = JOB_TYPES[job_type] ret_dict[ujt_key] = val else: ret_dict[fd] = val for rel in ['success', 'failure', 'always']: sub_node_id_list = node_dict['{0}_nodes'.format(rel)] if len(sub_node_id_list) == 0: continue relationship_name = '{0}_nodes'.format(rel) ret_dict[relationship_name] = [] for sub_node_id in sub_node_id_list: ret_dict[relationship_name].append( branch_schema(sub_node_id)) return ret_dict schema_dict = [] for root_node_id in root_nodes: schema_dict.append(branch_schema(root_node_id)) return schema_dict def _get_schema(self, wfjt_id): """ Returns a dictionary that represents the node network of the workflow job template """ node_res = get_resource('node') node_results = node_res.list(workflow_job_template=wfjt_id, all_pages=True)['results'] return self._workflow_node_structure(node_results) @resources.command(use_fields_as_options=False) @click.argument('wfjt', type=types.Related('workflow')) @click.argument('node_network', type=types.Variables(), required=False) def schema(self, wfjt, node_network=None): """ Convert YAML/JSON content into workflow node objects if node_network param is given. If not, print a YAML representation of the node network. =====API DOCS===== Convert YAML/JSON content into workflow node objects if ``node_network`` param is given. If not, print a YAML representation of the node network. :param wfjt: Primary key or name of the workflow job template to run schema against. :type wfjt: str :param node_network: JSON- or YAML-formatted string representing the topology of the workflow job template be updated to. :type node_network: str :returns: The latest topology (possibly after modification) of the workflow job template. :rtype: dict =====API DOCS===== """ existing_network = self._get_schema(wfjt) if not isinstance(existing_network, list): existing_network = [] if node_network is None: if settings.format == 'human': settings.format = 'yaml' return existing_network if hasattr(node_network, 'read'): node_network = node_network.read() node_network = string_to_dict( node_network, allow_kv=False, require_dict=False) if not isinstance(node_network, list): node_network = [] _update_workflow([TreeNode(x, wfjt, include_id=True) for x in existing_network], [TreeNode(x, wfjt) for x in node_network]) if settings.format == 'human': settings.format = 'yaml' return self._get_schema(wfjt) @resources.command(use_fields_as_options=False) @click.option('--workflow', type=types.Related('workflow')) @click.option('--label', type=types.Related('label')) def associate_label(self, workflow, label): """Associate an label with this workflow. =====API DOCS===== Associate an label with this workflow job template. :param workflow: The workflow job template to associate to. :type workflow: str :param label: The label to be associated. :type label: str :returns: Dictionary of only one key "changed", which indicates whether the association succeeded. :rtype: dict =====API DOCS===== """ return self._assoc('labels', workflow, label) @resources.command(use_fields_as_options=False) @click.option('--workflow', type=types.Related('workflow')) @click.option('--label', type=types.Related('label')) def disassociate_label(self, workflow, label): """Disassociate an label from this workflow. =====API DOCS===== Disassociate an label from this workflow job template. :param workflow: The workflow job template to disassociate from. :type workflow: str :param label: The label to be disassociated. :type label: str :returns: Dictionary of only one key "changed", which indicates whether the disassociation succeeded. :rtype: dict =====API DOCS===== """ return self._disassoc('labels', workflow, label) @resources.command(use_fields_as_options=False) @click.option('--workflow', type=types.Related('workflow')) @click.option('--notification-template', type=types.Related('notification_template')) @click.option('--status', type=click.Choice(['any', 'error', 'success']), required=False, default='any', help='Specify job run status' ' of job template to relate to.') def associate_notification_template(self, workflow, notification_template, status): """Associate a notification template from this workflow. =====API DOCS===== Associate a notification template from this workflow job template. :param workflow: The workflow job template to associate to. :type workflow: str :param notification_template: The notification template to be associated. :type notification_template: str :param status: type of notification this notification template should be associated to. :type status: str :returns: Dictionary of only one key "changed", which indicates whether the association succeeded. :rtype: dict =====API DOCS===== """ return self._assoc('notification_templates_%s' % status, workflow, notification_template) @resources.command(use_fields_as_options=False) @click.option('--workflow', type=types.Related('workflow')) @click.option('--notification-template', type=types.Related('notification_template')) @click.option('--status', type=click.Choice(['any', 'error', 'success']), required=False, default='any', help='Specify job run status' ' of job template to relate to.') def disassociate_notification_template(self, workflow, notification_template, status): """Disassociate a notification template from this workflow. =====API DOCS===== Disassociate a notification template from this workflow job template. :param job_template: The workflow job template to disassociate from. :type job_template: str :param notification_template: The notification template to be disassociated. :type notification_template: str :param status: type of notification this notification template should be disassociated from. :type status: str :returns: Dictionary of only one key "changed", which indicates whether the disassociation succeeded. :rtype: dict =====API DOCS===== """ return self._disassoc('notification_templates_%s' % status, workflow, notification_template) ansible-tower-cli-3.2.0/tower_cli/resources/workflow_job.py000066400000000000000000000144101316523067200241200ustar00rootroot00000000000000# Copyright 2017 Ansible by Red Hat # Alan Rominger # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. import click from tower_cli import models, resources, get_resource from tower_cli.api import client from tower_cli.cli import types from tower_cli.cli.resource import ResSubcommand from tower_cli.utils import debug, parser class Resource(models.ExeResource): """A resource for workflow jobs.""" cli_help = 'Launch or monitor workflow jobs.' endpoint = '/workflow_jobs/' workflow_job_template = models.Field( key='-W', type=types.Related('workflow'), display=True ) extra_vars = models.Field( type=types.Variables(), required=False, display=False, multiple=True ) created = models.Field(required=False, display=True) status = models.Field(required=False, display=True) def __getattribute__(self, attr): """Alias the stdout to `summary` specially for workflow""" if attr == 'summary': return object.__getattribute__(self, 'stdout') elif attr == 'stdout': raise AttributeError return super(Resource, self).__getattribute__(attr) def lookup_stdout(self, pk=None, start_line=None, end_line=None, full=True): """ Internal method that lies to our `monitor` method by returning a scorecard for the workflow job where the standard out would have been expected. """ uj_res = get_resource('unified_job') # Filters # - limit search to jobs spawned as part of this workflow job # - order in the order in which they should add to the list # - only include final job states query_params = (('unified_job_node__workflow_job', pk), ('order_by', 'finished'), ('status__in', 'successful,failed,error')) jobs_list = uj_res.list(all_pages=True, query=query_params) if jobs_list['count'] == 0: return '' return_content = ResSubcommand(uj_res)._format_human(jobs_list) lines = return_content.split('\n') if not full: lines = lines[:-1] N = len(lines) start_range = start_line if start_line is None: start_range = 0 elif start_line > N: start_range = N end_range = end_line if end_line is None or end_line > N: end_range = N lines = lines[start_range:end_range] return_content = '\n'.join(lines) if len(lines) > 0: return_content += '\n' return return_content @resources.command def summary(self): """Placeholder to get swapped out for `stdout`. =====API DOCS===== foobar =====API DOCS===== """ pass @resources.command( use_fields_as_options=('workflow_job_template', 'extra_vars') ) @click.option('--monitor', is_flag=True, default=False, help='If used, immediately calls monitor on the newly ' 'launched workflow job rather than exiting.') @click.option('--wait', is_flag=True, default=False, help='Wait until completion to exit, displaying ' 'placeholder text while in progress.') @click.option('--timeout', required=False, type=int, help='If provided with --monitor, this command (not the job)' ' will time out after the given number of seconds. ' 'Does nothing if --monitor is not sent.') def launch(self, workflow_job_template=None, monitor=False, wait=False, timeout=None, extra_vars=None, **kwargs): """Launch a new workflow job based on a workflow job template. Creates a new workflow job in Ansible Tower, starts it, and returns back an ID in order for its status to be monitored. =====API DOCS===== Launch a new workflow job based on a workflow job template. :param workflow_job_template: Primary key or name of the workflow job template to launch new job. :type workflow_job_template: str :param monitor: Flag that if set, immediately calls ``monitor`` on the newly launched workflow job rather than exiting with a success. :type monitor: bool :param wait: Flag that if set, monitor the status of the workflow job, but do not print while job is in progress. :type wait: bool :param timeout: If provided with ``monitor`` flag set, this attempt will time out after the given number of seconds. :type timeout: int :param extra_vars: yaml formatted texts that contains extra variables to pass on. :type extra_vars: array of strings :param `**kwargs`: Fields needed to create and launch a workflow job. :returns: Result of subsequent ``monitor`` call if ``monitor`` flag is on; Result of subsequent ``wait`` call if ``wait`` flag is on; loaded JSON output of the job launch if none of the two flags are on. :rtype: dict =====API DOCS===== """ if extra_vars is not None and len(extra_vars) > 0: kwargs['extra_vars'] = parser.process_extra_vars(extra_vars) debug.log('Launching the workflow job.', header='details') self._pop_none(kwargs) post_response = client.post('workflow_job_templates/{0}/launch/'.format( workflow_job_template), data=kwargs).json() workflow_job_id = post_response['id'] post_response['changed'] = True if monitor: return self.monitor(workflow_job_id, timeout=timeout) elif wait: return self.wait(workflow_job_id, timeout=timeout) return post_response ansible-tower-cli-3.2.0/tower_cli/utils/000077500000000000000000000000001316523067200201705ustar00rootroot00000000000000ansible-tower-cli-3.2.0/tower_cli/utils/__init__.py000066400000000000000000000022631316523067200223040ustar00rootroot00000000000000# Copyright 2015, Ansible, Inc. # Luke Sneeringer # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. import functools import click from tower_cli.conf import settings from tower_cli import exceptions # NOQA @functools.wraps(click.secho) def secho(message, **kwargs): """A wrapper around click.secho that disables any coloring being used if colors have been disabled. """ # If colors are disabled, remove any color or other style data # from keyword arguments. if not settings.color: for key in ('fg', 'bg', 'bold', 'blink'): kwargs.pop(key, None) # Okay, now call click.secho normally. return click.secho(message, **kwargs) ansible-tower-cli-3.2.0/tower_cli/utils/data_structures.py000066400000000000000000000020051316523067200237530ustar00rootroot00000000000000# Copyright 2015, Ansible, Inc. # Luke Sneeringer # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. from tower_cli import compat class OrderedDict(compat.OrderedDict): """OrderedDict subclass that nonetheless uses the basic dictionary __repr__ method. """ def __repr__(self): """Print a repr that resembles dict's repr, but preserves key order. """ return '{' + ', '.join(['%r: %r' % (k, v) for k, v in self.items()]) + '}' ansible-tower-cli-3.2.0/tower_cli/utils/debug.py000066400000000000000000000040311316523067200216260ustar00rootroot00000000000000# Copyright 2015, Ansible, Inc. # Luke Sneeringer # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. import sys from tower_cli.conf import settings from tower_cli.utils import secho def log(s, header='', file=sys.stderr, nl=1, **kwargs): """Log the given output to stderr if and only if we are in verbose mode. If we are not in verbose mode, this is a no-op. """ # Sanity check: If we are not in verbose mode, this is a no-op. if not settings.verbose: return # Construct multi-line string to stderr if header is provided. if header: word_arr = s.split(' ') multi = [] word_arr.insert(0, '%s:' % header.upper()) i = 0 while i < len(word_arr): to_add = ['***'] count = 3 while count <= 79: count += len(word_arr[i]) + 1 if count <= 79: to_add.append(word_arr[i]) i += 1 if i == len(word_arr): break if i != len(word_arr): count -= len(word_arr[i]) + 1 to_add.append('*' * (78 - count)) multi.append(' '.join(to_add)) s = '\n'.join(multi) lines = len(multi) else: lines = 1 # If `nl` is an int greater than the number of rows of a message, # add the appropriate newlines to the output. if isinstance(nl, int) and nl > lines: s += '\n' * (nl - lines) # Output to stderr. return secho(s, file=file, **kwargs) ansible-tower-cli-3.2.0/tower_cli/utils/exceptions.py000066400000000000000000000077441316523067200227370ustar00rootroot00000000000000# Copyright 2015, Ansible, Inc. # Luke Sneeringer # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. # # NOTE: this is a duplicate of tower_cli/exceptions.py for backward compatibility # with Ansible Tower modules. import click from click._compat import get_text_stderr class TowerCLIError(click.ClickException): """Base exception class for problems raised within Tower CLI. This class adds coloring to exceptions. """ fg = 'red' bg = None bold = True def show(self, file=None): if file is None: file = get_text_stderr() click.secho('Error: %s' % self.format_message(), file=file, fg=self.fg, bg=self.bg, bold=self.bold) class UsageError(TowerCLIError): """An exception class for reporting usage errors. This uses an exit code of 2 in order to match click (which matters more than following the erstwhile "standard" of using 64). """ exit_code = 2 class BadRequest(TowerCLIError): """An exception class for reporting unexpected error codes from Ansible Tower such that 400 <= code < 500. In theory, we should never, ever get these. """ exit_code = 40 class AuthError(TowerCLIError): """An exception class for reporting when a request failed due to an authorization failure. """ exit_code = 41 class Forbidden(TowerCLIError): """An exception class for reporting when a user doesn't have permission to do something. """ exit_code = 43 class NotFound(TowerCLIError): """An exception class for reporting when a request went through without incident, but the requested content could not be found. """ exit_code = 44 class MethodNotAllowed(BadRequest): """An exception class for sending a request to a URL where the URL doesn't accept that method at all. """ exit_code = 45 class MultipleResults(TowerCLIError): """An exception class for reporting when a request that expected one and exactly one result got more than that. """ exit_code = 49 class ServerError(TowerCLIError): """An exception class for reporting server-side errors which are expected to be ephemeral. """ exit_code = 50 class Found(TowerCLIError): """An exception class for when a record already exists, and we were explicitly told that it shouldn't. """ exit_code = 60 class RelatedError(TowerCLIError): """An exception class for errors where we can't find related objects that we expect to find. """ exit_code = 61 class MultipleRelatedError(RelatedError): """An exception class for errors where we try to find a single related object, and get more than one. """ exit_code = 62 class ValidationError(TowerCLIError): """An exception class for invalid values being sent as option switches to Tower CLI. """ exit_code = 64 class CannotStartJob(TowerCLIError): """An exception class for jobs that cannot be started within Tower for whatever reason. """ exit_code = 97 class Timeout(TowerCLIError): """An exception class for timeouts encountered within Tower CLI, usually for monitoring. """ exit_code = 98 class JobFailure(TowerCLIError): """An exception class for job failures that require error codes within the Tower CLI. """ exit_code = 99 class ConnectionError(TowerCLIError): """An exception class to bubble requests errors more nicely, and communicate connection issues to the user. """ exit_code = 120 ansible-tower-cli-3.2.0/tower_cli/utils/parser.py000066400000000000000000000147561316523067200220530ustar00rootroot00000000000000# Copyright 2015, Ansible, Inc. # Alan Rominger # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. import yaml import json import ast import shlex import sys import six from tower_cli import exceptions as exc from tower_cli.utils import debug from tower_cli.utils.data_structures import OrderedDict def parse_kv(var_string): """Similar to the Ansible function of the same name, parses file with a key=value pattern and stores information in a dictionary, but not as fully featured as the corresponding Ansible code.""" return_dict = {} # Output updates dictionaries, so return empty one if no vals in if var_string is None: return {} # Python 2.6 / shlex has problems handling unicode, this is a fix fix_encoding_26 = False if sys.version_info < (2, 7) and '\x00' in shlex.split(u'a')[0]: fix_encoding_26 = True # Also hedge against Click library giving non-string type is_unicode = False if fix_encoding_26 or not isinstance(var_string, str): if isinstance(var_string, six.text_type): var_string = var_string.encode('UTF-8') is_unicode = True else: var_string = str(var_string) # Use shlex library to split string by quotes, whitespace, etc. for token in shlex.split(var_string): # Second part of fix to avoid passing shlex unicode in py2.6 if (is_unicode): token = token.decode('UTF-8') if fix_encoding_26: token = six.text_type(token) # Look for key=value pattern, if not, process as raw parameter if '=' in token: (k, v) = token.split('=', 1) # If '=' are unbalanced, then stop and warn user if len(k) == 0 or len(v) == 0: raise Exception # If possible, convert into python data type, for instance "5"->5 try: return_dict[k] = ast.literal_eval(v) except: return_dict[k] = v else: # scenario where --extra-vars=42, will throw error raise Exception return return_dict def string_to_dict(var_string, allow_kv=True, require_dict=True): """Returns a dictionary given a string with yaml or json syntax. If data is not present in a key: value format, then it return an empty dictionary. Attempts processing string by 3 different methods in order: 1. as JSON 2. as YAML 3. as custom key=value syntax Throws an error if all of these fail in the standard ways.""" # try: # # Accept all valid "key":value types of json # return_dict = json.loads(var_string) # assert type(return_dict) is dict # except (TypeError, AttributeError, ValueError, AssertionError): try: # Accept all JSON and YAML return_dict = yaml.load(var_string) if require_dict: assert type(return_dict) is dict except (AttributeError, yaml.YAMLError, AssertionError): # if these fail, parse by key=value syntax try: assert allow_kv return_dict = parse_kv(var_string) except: raise exc.TowerCLIError( 'failed to parse some of the extra ' 'variables.\nvariables: \n%s' % var_string ) return return_dict def process_extra_vars(extra_vars_list, force_json=True): """Returns a string that is valid JSON or YAML and contains all the variables in every extra_vars_opt inside of extra_vars_list. Args: parse_kv (bool): whether to allow key=value syntax. force_json (bool): if True, always output json. """ # Read from all the different sources and put into dictionary extra_vars = {} extra_vars_yaml = "" for extra_vars_opt in extra_vars_list: # Load file content if necessary if extra_vars_opt.startswith("@"): with open(extra_vars_opt[1:], 'r') as f: extra_vars_opt = f.read() # Convert text markup to a dictionary conservatively opt_dict = string_to_dict(extra_vars_opt, allow_kv=False) else: # Convert text markup to a dictionary liberally opt_dict = string_to_dict(extra_vars_opt, allow_kv=True) # Rolling YAML-based string combination if any(line.startswith("#") for line in extra_vars_opt.split('\n')): extra_vars_yaml += extra_vars_opt + "\n" elif extra_vars_opt != "": extra_vars_yaml += yaml.dump( opt_dict, default_flow_style=False) + "\n" # Combine dictionary with cumulative dictionary extra_vars.update(opt_dict) # Return contents in form of a string if not force_json: try: # Conditions to verify it is safe to return rolling YAML string try_dict = yaml.load(extra_vars_yaml) assert type(try_dict) is dict debug.log('Using unprocessed YAML', header='decision', nl=2) return extra_vars_yaml.rstrip() except: debug.log('Failed YAML parsing, defaulting to JSON', header='decison', nl=2) if extra_vars == {}: return "" return json.dumps(extra_vars, ensure_ascii=False) def ordered_dump(data, Dumper=yaml.Dumper, **kws): """Expand PyYAML's built-in dumper to support parsing OrderedDict. Return a string as parse result of the original data structure, which includes OrderedDict. Args: data: the data structure to be dumped(parsed) which is supposed to contain OrderedDict. Dumper: the yaml serializer to be expanded and used. kws: extra key-value arguments to be passed to yaml.dump. """ class OrderedDumper(Dumper): pass def _dict_representer(dumper, data): return dumper.represent_mapping( yaml.resolver.BaseResolver.DEFAULT_MAPPING_TAG, data.items()) OrderedDumper.add_representer(OrderedDict, _dict_representer) return yaml.dump(data, None, OrderedDumper, **kws) ansible-tower-cli-3.2.0/tower_cli/utils/resource_decorators.py000066400000000000000000000054011316523067200246160ustar00rootroot00000000000000# Copyright 2017 Ansible by Red Hat # Alan Rominger # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. import functools from tower_cli.cli import types import click def unified_job_template_options(method): """ Adds the decorators for all types of unified job templates, and if the non-unified type is specified, converts it into the unified_job_template kwarg. """ jt_dec = click.option( '--job-template', type=types.Related('job_template'), help='Use this job template as unified_job_template field') prj_dec = click.option( '--project', type=types.Related('project'), help='Use this project as unified_job_template field') inv_src_dec = click.option( '--inventory-source', type=types.Related('inventory_source'), help='Use this inventory source as unified_job_template field') def ujt_translation(_method): def _ujt_translation(*args, **kwargs): for fd in ['job_template', 'project', 'inventory_source']: if fd in kwargs and kwargs[fd] is not None: kwargs['unified_job_template'] = kwargs.pop(fd) return _method(*args, **kwargs) return functools.wraps(_method)(_ujt_translation) return ujt_translation( inv_src_dec( prj_dec( jt_dec( method ) ) ) ) # The following three decorators, altogether, are used to disable inherited attributes/methods. # Typically it should be used like this: # # `name = property(disabled_getter('name'), disabled_setter('name'), disabled_deleter('name'))` # def disabled_getter(attr_name): def handler(self): internal_attr_name = '__' + attr_name if hasattr(self, internal_attr_name): return getattr(self, internal_attr_name) raise AttributeError('Inherited attribute %s has been disabled.' % attr_name) return handler def disabled_setter(attr_name): def handler(self, val): internal_attr_name = '__' + attr_name setattr(self, internal_attr_name, val) return handler def disabled_deleter(attr_name): def handler(self): internal_attr_name = '__' + attr_name delattr(self, internal_attr_name) return handler ansible-tower-cli-3.2.0/tox.ini000066400000000000000000000010301316523067200163460ustar00rootroot00000000000000[testenv] commands = nosetests {posargs} deps = -r{toxinidir}/requirements.txt -r{toxinidir}/tests/requirements.txt [testenv:py36] deps = {[testenv]deps} mock [testenv:py35] deps = {[testenv]deps} mock [testenv:py34] deps = {[testenv]deps} mock [testenv:py27] deps = {[testenv]deps} mock [testenv:py26] deps = {[testenv:py27]deps} importlib ordereddict simplejson unittest2 [testenv:flake8] deps = flake8 commands = flake8 {toxinidir} [flake8] max-line-length=120 ansible-tower-cli-3.2.0/version_swap.py000066400000000000000000000013361316523067200201350ustar00rootroot00000000000000import os API_VERSION = 2 package_name = 'tower_cli_v%s' % API_VERSION def convert_file(filename): with open(filename) as f: s = f.read() if package_name in s: raise Exception( 'While attempting to convert %s. ' 'Command has already ran, no need to run again.' % filename ) s = s.replace('tower_cli', package_name) s = s.replace('tower-cli', 'tower-cli-v%s' % API_VERSION) with open(filename, "w") as f: f.write(s) for dname, dirs, files in os.walk(package_name): for fname in files: fpath = os.path.join(dname, fname) convert_file(fpath) convert_file('setup_v%s.py' % API_VERSION) convert_file('bin/tower-cli-v%s' % API_VERSION)