├── .git-commit-template.txt ├── .gitignore ├── .travis.yml ├── CONTRIBUTING.md ├── LICENSE ├── MANIFEST.in ├── Makefile ├── README.rst ├── RELEASE ├── SUPPORT.md ├── conftest.py ├── docs ├── apidocs │ ├── icontrol.rst │ └── modules.rst ├── conf.py └── index.rst ├── f5-icontrol-rest-dist ├── Docker │ ├── redhat │ │ ├── 6 │ │ │ ├── Dockerfile │ │ │ └── build-rpms.sh │ │ └── 7 │ │ │ ├── Dockerfile │ │ │ └── build-rpms.sh │ └── ubuntu │ │ └── 14.04 │ │ ├── Dockerfile │ │ └── build-debs.sh ├── deb_dist │ └── stdeb.cfg └── scripts │ └── package.sh ├── icontrol ├── __init__.py ├── authtoken.py ├── exceptions.py ├── session.py └── test │ ├── __init__.py │ ├── functional │ ├── __init__.py │ ├── dummy-ca-cert.pem │ └── test_session.py │ └── unit │ ├── __init__.py │ └── test_session.py ├── requirements.docs.txt ├── requirements.test.txt ├── setup.cfg ├── setup.py └── tox.ini /.git-commit-template.txt: -------------------------------------------------------------------------------- 1 | # Headline or summary of the issue you are fixing 2 | 3 | # Using the Fixes # will close the issue on commit to repo 4 | Issues: 5 | Fixes # 6 | 7 | # Describe the issue that this change addresses 8 | Problem: 9 | 10 | # Describe the change itself and why you made the changes you did 11 | Analysis: 12 | 13 | # Describe the tests you ran and/or created to test this change 14 | Tests: 15 | 16 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | ### added by GMW 2 | .venv 3 | .project 4 | .pydevproject 5 | .settings/* 6 | .idea/* 7 | *.py[cod] 8 | 9 | ### Additional virtual environment files 10 | bin/ 11 | pyvenv.cfg 12 | 13 | ### added with RM's Makefile 14 | localutils/** 15 | agent/f5/bigip/pycontrol/wsdl/** 16 | 17 | ### auto-added by Github 18 | 19 | # C extensions 20 | *.so 21 | 22 | # Packages 23 | *.egg 24 | *.egg-info 25 | dist 26 | build 27 | eggs 28 | parts 29 | var 30 | sdist 31 | develop-eggs 32 | .installed.cfg 33 | lib 34 | lib64 35 | __pycache__ 36 | 37 | # Installer logs 38 | pip-log.txt 39 | 40 | # Unit test / coverage reports 41 | .coverage 42 | .tox 43 | nosetests.xml 44 | htmlcov/* 45 | 46 | # Translations 47 | *.mo 48 | 49 | # Emacs files 50 | *~ 51 | *\#* 52 | 53 | # Vim files 54 | *.swp 55 | 56 | # Locally built RPMS 57 | rpms 58 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | sudo: true 2 | env: 3 | global: 4 | - DIST_REPO="f5-icontrol-rest-dist" 5 | services: 6 | - docker 7 | language: python 8 | matrix: 9 | include: 10 | - python: 2.7 11 | env: TOXENV=py27-unit 12 | - python: 3.4 13 | env: TOXENV=py34-unit 14 | - python: 3.5 15 | env: TOXENV=py35-unit 16 | - env: TOXENV=flake 17 | before_install: 18 | - git config --global user.email "OpenStack_TravisCI@f5.com" 19 | - git config --global user.name "Travis F5 Openstack" 20 | install: 21 | - pip install -r requirements.test.txt 22 | - pip install -r requirements.docs.txt 23 | script: 24 | - tox 25 | - if [[ $TRAVIS_PYTHON_VERSION == 2.7 ]]; then ${DIST_REPO}/scripts/package.sh "redhat" "7"; fi 26 | - if [[ $TRAVIS_PYTHON_VERSION == 2.7 ]]; then ${DIST_REPO}/scripts/package.sh "ubuntu" "14.04"; fi 27 | - if [[ $TRAVIS_PYTHON_VERSION == 2.7 ]]; then sudo chown -R travis:travis ${DIST_REPO}/rpms/build; fi 28 | - if [[ $TRAVIS_PYTHON_VERSION == 2.7 ]]; then sudo chown travis:travis ${DIST_REPO}/deb_dist/*.deb; fi 29 | before_deploy: PKG_VERSION=$(python -c "import icontrol; print(icontrol.__version__)") 30 | deploy: 31 | - provider: releases 32 | api_key: 33 | secure: Nc4NrVZqv5L0Ss8UQh0Glx8AKkL1NfClcvmOrg6kyj+jqnZYbqVbWN7Od8K9tp9pD613mQ86RL8vDV25wxEQY/Z0MidKuWqmM7PkriCe6ZXYhO2qB/jTouWr6ucv2xz4CZE2HRV3gJMKT6zFzKUZD2zUcojoIfzOM1KK2ggohEzkXXzpCtXFbVRB5B5WpMJ5+MQKBKGAUIF2RCiMiVRkqIfEg9dVziNNsSfXOjq5zelEIx3ePj/9/1OCrcjIpdp1SDc7soM79JDcmTyUkeUiczrZyXqw9972wI3zYDTAK/cDyPv/DzHF+N4jLOlI8j1lx8u/tgukAN98x2PqLhkxnezz1wGX17UDyQEvhCXs94+dhB20QulVfjCz+t2xaFozsC5z2C22ogRNIWtIg65x/Uj8YyO1AqNG8gmEYHErlQQGUcdyWhoApvAEF7BGFRZzlyWRBpxu3m4LKPRhahF1g3qOqVt3SHNG2uF4zJOJf+8MBG07A0QNhWCfre5sszsxUaiaePjUrABAgRijU2MPnOwaEzfnPoFH6j40CrpYhVQRn1v97e8Nk547gErIaAoATeJ4XDhAOXWj8qZrN9XBRtnu0AqVL59JtpeORAT5PWjM2bU5gUYk6CuBofdVOVo7qBWFL0nwu8hUneD5ur6t2v6TinktW4zheNNHQUFsH/4= 34 | file: 35 | - ${DIST_REPO}/rpms/build/f5-icontrol-rest-${PKG_VERSION}-1.el7.noarch.rpm 36 | - ${DIST_REPO}/deb_dist/python-f5-icontrol-rest_${PKG_VERSION}-1_1404_all.deb 37 | skip_cleanup: true 38 | overwrite: true 39 | on: 40 | repo: F5Networks/f5-icontrol-rest-python 41 | tags: true 42 | python: 2.7 43 | - provider: pypi 44 | user: $PYPI_USER 45 | password: $PYPI_PASSWORD 46 | on: 47 | all_branches: true 48 | tags: true 49 | python: 2.7 50 | notifications: 51 | slack: 52 | rooms: 53 | - f5openstackdev:$SLACK_PROJECT_TOKEN 54 | - f5openstackdev:$SLACK_BUILD_STATUS_TOKEN 55 | on_success: change 56 | on_failure: always 57 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | 16 | 17 | # Contributing Guide for f5-icontrol-rest-python 18 | If you have found this that means you you want to help us out. Thanks in advance for lending a hand! This guide should get you up and running quickly and make it easy for you to contribute. If we don't answer your questions here and you want to help or just say hi, shoot us an email at f5-icontrol-rest-python@f5.com. 19 | 20 | ## Issues 21 | Creating issues is good, creating good issues is even better. Filing meaningful bug reports with lots of information in them helps us figure out what to fix when and how it impacts our users. We like bugs because it means people are using our code, and we like fixing them even more. 22 | 23 | ## Pull Requests 24 | If you are submitting a pull request, you need to make sure that you have done a few things first. 25 | 26 | * If a corresponding issue doesn't exist, file one. 27 | * Make sure you have tested your code because we are going to do that when when you make your PR. You don't want 28 | _The Hat_ because your request fails unit tests. 29 | * Clean up your git history; no one wants to see 75 commits for one issue. 30 | * Use our [commit template](.git-commit-template.txt) 31 | * Use our pull request template. 32 | 33 | ``` 34 | @ 35 | #### What issues does this address? 36 | Fixes # 37 | WIP # 38 | ... 39 | 40 | #### What's this change do? 41 | 42 | #### Where should the reviewer start? 43 | 44 | #### Any background context? 45 | ``` 46 | 47 | ## Testing 48 | Creating tests is pretty straightforward and we need you to help us keep ensure the quality of our code. We write both our unit tests and functional tests using [pytest](http://pytest.org). We know it is extra work to write these tests but the maintainers and consumers of this code appreciate the effort and writing the tests is pretty easy. 49 | 50 | Running the tests is even easier: 51 | 52 | ```shell 53 | $ py.test --cov ./ --cov-report=html 54 | $ open htmlcov/index.html 55 | ``` 56 | 57 | If you are running our functional tests you will need a real BIG-IP® to run them against, but you can get one of those pretty easily in [Amazon EC2](https://aws.amazon.com/marketplace/pp/B00JL3UASY/ref=srh_res_product_title?ie=UTF8&sr=0-10&qid=1449332167461). 58 | 59 | ## License 60 | 61 | ### Apache V2.0 62 | Licensed under the Apache License, Version 2.0 (the "License"); 63 | you may not use this file except in compliance with the License. 64 | You may obtain a copy of the License at 65 | 66 | http://www.apache.org/licenses/LICENSE-2.0 67 | 68 | Unless required by applicable law or agreed to in writing, software 69 | distributed under the License is distributed on an "AS IS" BASIS, 70 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 71 | See the License for the specific language governing permissions and 72 | limitations under the License. 73 | 74 | ### Contributor License Agreement 75 | Individuals or business entities who contribute to this project must have completed and submitted the [F5 Contributor License Agreement](http://f5-openstack-docs.readthedocs.org/en/latest/cla_landing.html) to Openstack_CLA@f5.com prior to their code submission being included in this project. 76 | 77 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, 10 | and distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by 13 | the copyright owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all 16 | other entities that control, are controlled by, or are under common 17 | control with that entity. For the purposes of this definition, 18 | "control" means (i) the power, direct or indirect, to cause the 19 | direction or management of such entity, whether by contract or 20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 21 | outstanding shares, or (iii) beneficial ownership of such entity. 22 | 23 | "You" (or "Your") shall mean an individual or Legal Entity 24 | exercising permissions granted by this License. 25 | 26 | "Source" form shall mean the preferred form for making modifications, 27 | including but not limited to software source code, documentation 28 | source, and configuration files. 29 | 30 | "Object" form shall mean any form resulting from mechanical 31 | transformation or translation of a Source form, including but 32 | not limited to compiled object code, generated documentation, 33 | and conversions to other media types. 34 | 35 | "Work" shall mean the work of authorship, whether in Source or 36 | Object form, made available under the License, as indicated by a 37 | copyright notice that is included in or attached to the work 38 | (an example is provided in the Appendix below). 39 | 40 | "Derivative Works" shall mean any work, whether in Source or Object 41 | form, that is based on (or derived from) the Work and for which the 42 | editorial revisions, annotations, elaborations, or other modifications 43 | represent, as a whole, an original work of authorship. For the purposes 44 | of this License, Derivative Works shall not include works that remain 45 | separable from, or merely link (or bind by name) to the interfaces of, 46 | the Work and Derivative Works thereof. 47 | 48 | "Contribution" shall mean any work of authorship, including 49 | the original version of the Work and any modifications or additions 50 | to that Work or Derivative Works thereof, that is intentionally 51 | submitted to Licensor for inclusion in the Work by the copyright owner 52 | or by an individual or Legal Entity authorized to submit on behalf of 53 | the copyright owner. For the purposes of this definition, "submitted" 54 | means any form of electronic, verbal, or written communication sent 55 | to the Licensor or its representatives, including but not limited to 56 | communication on electronic mailing lists, source code control systems, 57 | and issue tracking systems that are managed by, or on behalf of, the 58 | Licensor for the purpose of discussing and improving the Work, but 59 | excluding communication that is conspicuously marked or otherwise 60 | designated in writing by the copyright owner as "Not a Contribution." 61 | 62 | "Contributor" shall mean Licensor and any individual or Legal Entity 63 | on behalf of whom a Contribution has been received by Licensor and 64 | subsequently incorporated within the Work. 65 | 66 | 2. Grant of Copyright License. Subject to the terms and conditions of 67 | this License, each Contributor hereby grants to You a perpetual, 68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 69 | copyright license to reproduce, prepare Derivative Works of, 70 | publicly display, publicly perform, sublicense, and distribute the 71 | Work and such Derivative Works in Source or Object form. 72 | 73 | 3. Grant of Patent License. Subject to the terms and conditions of 74 | this License, each Contributor hereby grants to You a perpetual, 75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 76 | (except as stated in this section) patent license to make, have made, 77 | use, offer to sell, sell, import, and otherwise transfer the Work, 78 | where such license applies only to those patent claims licensable 79 | by such Contributor that are necessarily infringed by their 80 | Contribution(s) alone or by combination of their Contribution(s) 81 | with the Work to which such Contribution(s) was submitted. If You 82 | institute patent litigation against any entity (including a 83 | cross-claim or counterclaim in a lawsuit) alleging that the Work 84 | or a Contribution incorporated within the Work constitutes direct 85 | or contributory patent infringement, then any patent licenses 86 | granted to You under this License for that Work shall terminate 87 | as of the date such litigation is filed. 88 | 89 | 4. Redistribution. You may reproduce and distribute copies of the 90 | Work or Derivative Works thereof in any medium, with or without 91 | modifications, and in Source or Object form, provided that You 92 | meet the following conditions: 93 | 94 | (a) You must give any other recipients of the Work or 95 | Derivative Works a copy of this License; and 96 | 97 | (b) You must cause any modified files to carry prominent notices 98 | stating that You changed the files; and 99 | 100 | (c) You must retain, in the Source form of any Derivative Works 101 | that You distribute, all copyright, patent, trademark, and 102 | attribution notices from the Source form of the Work, 103 | excluding those notices that do not pertain to any part of 104 | the Derivative Works; and 105 | 106 | (d) If the Work includes a "NOTICE" text file as part of its 107 | distribution, then any Derivative Works that You distribute must 108 | include a readable copy of the attribution notices contained 109 | within such NOTICE file, excluding those notices that do not 110 | pertain to any part of the Derivative Works, in at least one 111 | of the following places: within a NOTICE text file distributed 112 | as part of the Derivative Works; within the Source form or 113 | documentation, if provided along with the Derivative Works; or, 114 | within a display generated by the Derivative Works, if and 115 | wherever such third-party notices normally appear. The contents 116 | of the NOTICE file are for informational purposes only and 117 | do not modify the License. You may add Your own attribution 118 | notices within Derivative Works that You distribute, alongside 119 | or as an addendum to the NOTICE text from the Work, provided 120 | that such additional attribution notices cannot be construed 121 | as modifying the License. 122 | 123 | You may add Your own copyright statement to Your modifications and 124 | may provide additional or different license terms and conditions 125 | for use, reproduction, or distribution of Your modifications, or 126 | for any such Derivative Works as a whole, provided Your use, 127 | reproduction, and distribution of the Work otherwise complies with 128 | the conditions stated in this License. 129 | 130 | 5. Submission of Contributions. Unless You explicitly state otherwise, 131 | any Contribution intentionally submitted for inclusion in the Work 132 | by You to the Licensor shall be under the terms and conditions of 133 | this License, without any additional terms or conditions. 134 | Notwithstanding the above, nothing herein shall supersede or modify 135 | the terms of any separate license agreement you may have executed 136 | with Licensor regarding such Contributions. 137 | 138 | 6. Trademarks. This License does not grant permission to use the trade 139 | names, trademarks, service marks, or product names of the Licensor, 140 | except as required for reasonable and customary use in describing the 141 | origin of the Work and reproducing the content of the NOTICE file. 142 | 143 | 7. Disclaimer of Warranty. Unless required by applicable law or 144 | agreed to in writing, Licensor provides the Work (and each 145 | Contributor provides its Contributions) on an "AS IS" BASIS, 146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 147 | implied, including, without limitation, any warranties or conditions 148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 149 | PARTICULAR PURPOSE. You are solely responsible for determining the 150 | appropriateness of using or redistributing the Work and assume any 151 | risks associated with Your exercise of permissions under this License. 152 | 153 | 8. Limitation of Liability. In no event and under no legal theory, 154 | whether in tort (including negligence), contract, or otherwise, 155 | unless required by applicable law (such as deliberate and grossly 156 | negligent acts) or agreed to in writing, shall any Contributor be 157 | liable to You for damages, including any direct, indirect, special, 158 | incidental, or consequential damages of any character arising as a 159 | result of this License or out of the use or inability to use the 160 | Work (including but not limited to damages for loss of goodwill, 161 | work stoppage, computer failure or malfunction, or any and all 162 | other commercial damages or losses), even if such Contributor 163 | has been advised of the possibility of such damages. 164 | 165 | 9. Accepting Warranty or Additional Liability. While redistributing 166 | the Work or Derivative Works thereof, You may choose to offer, 167 | and charge a fee for, acceptance of support, warranty, indemnity, 168 | or other liability obligations and/or rights consistent with this 169 | License. However, in accepting such obligations, You may act only 170 | on Your own behalf and on Your sole responsibility, not on behalf 171 | of any other Contributor, and only if You agree to indemnify, 172 | defend, and hold each Contributor harmless for any liability 173 | incurred by, or claims asserted against, such Contributor by reason 174 | of your accepting any such warranty or additional liability. 175 | 176 | END OF TERMS AND CONDITIONS 177 | 178 | APPENDIX: How to apply the Apache License to your work. 179 | 180 | To apply the Apache License to your work, attach the following 181 | boilerplate notice, with the fields enclosed by brackets "{}" 182 | replaced with your own identifying information. (Don't include 183 | the brackets!) The text should be enclosed in the appropriate 184 | comment syntax for the file format. We also recommend that a 185 | file or class name and description of purpose be included on the 186 | same "printed page" as the copyright notice for easier 187 | identification within third-party archives. 188 | 189 | Copyright {yyyy} {name of copyright owner} 190 | 191 | Licensed under the Apache License, Version 2.0 (the "License"); 192 | you may not use this file except in compliance with the License. 193 | You may obtain a copy of the License at 194 | 195 | http://www.apache.org/licenses/LICENSE-2.0 196 | 197 | Unless required by applicable law or agreed to in writing, software 198 | distributed under the License is distributed on an "AS IS" BASIS, 199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 200 | See the License for the specific language governing permissions and 201 | limitations under the License. 202 | 203 | -------------------------------------------------------------------------------- /MANIFEST.in: -------------------------------------------------------------------------------- 1 | include README.md 2 | 3 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | PROJECT_DIR := $(shell pwd) 2 | VERSION := $(shell cat VERSION|tr -d '\n';) 3 | RELEASE := $(shell cat RELEASE|tr -d '\n';) 4 | 5 | default: source 6 | 7 | source: 8 | (python setup.py sdist; \ 9 | rm -rf MANIFEST; \ 10 | ) 11 | 12 | clean: clean-debs clean-rpms clean-source 13 | rm -rf *.egg-info *~ 14 | 15 | clean-debs: 16 | find . -name "*.pyc" -exec rm -rf {} \; 17 | rm -f MANIFEST 18 | rm -f build/f5-bigip-common_*.deb 19 | ( \ 20 | rm -rf deb_dist; \ 21 | rm -rf build; \ 22 | ) 23 | 24 | clean-rpms: 25 | find . -name "*.pyc" -exec rm -rf {} \; 26 | rm -f MANIFEST 27 | rm -rf f5-bigip-common* 28 | rm -f build/f5-bigip-common-*.rpm 29 | ( \ 30 | rm -rf dist; \ 31 | rm -rf build; \ 32 | ) 33 | 34 | clean-source: 35 | rm -rf build/*.tar.gz 36 | rm -rf common/*.tar.gz 37 | rm -rf common/dist 38 | -------------------------------------------------------------------------------- /README.rst: -------------------------------------------------------------------------------- 1 | f5-icontrol-rest-python 2 | ======================= 3 | 4 | **Note this repo is archived and is no longer maintained.** 5 | 6 | |Build Status| |Documentation Status| |slack badge| 7 | 8 | Introduction 9 | ------------ 10 | 11 | This generic python library allows programs and other modules to 12 | interact with the BIG-IP® iControl® REST API. 13 | 14 | **If you want to use python to automate BIG-IP® devices via the REST API, use the F5 SDK** (`F5Networks/f5-common-python `_). 15 | 16 | Installation 17 | ------------ 18 | 19 | Using Pip 20 | ````````` 21 | 22 | .. code:: bash 23 | 24 | $ pip install f5-icontrol-rest 25 | 26 | 27 | Installing directly from GitHub 28 | ``````````````````````````````` 29 | 30 | **NOTE:** The example below installs the package at release v0.1.0. Omitting the version number will install the package from the current state of the default branch. 31 | 32 | .. code:: shell 33 | 34 | $ pip install git+ssh://git@github.com/F5Networks/f5-icontrol-rest@v0.1.0` 35 | 36 | 37 | Configuration 38 | ------------- 39 | N/A 40 | 41 | Usage 42 | ----- 43 | 44 | .. code:: python 45 | 46 | from icontrol.session import iControlRESTSession 47 | icr_session = iControlRESTSession('myuser', 'mypass') 48 | icr_session.get( 49 | 'https://bigip.example.com/mgmt/tm/ltm/nat', 50 | name='mynat', 51 | partition='Common') 52 | 53 | 54 | Documentation 55 | ------------- 56 | 57 | See `Documentation `_. 58 | 59 | For Developers: 60 | --------------- 61 | 62 | Filing Issues 63 | ````````````` 64 | 65 | If you find an issue we would love to hear about it. Please let us know 66 | by filing an issue in this repository and tell us as much as you can 67 | about what you found and how you found it. 68 | 69 | Contributing 70 | ```````````` 71 | 72 | See `Contributing `_. 73 | 74 | Build 75 | ````` 76 | 77 | To make a PyPI package: 78 | 79 | .. code:: bash 80 | 81 | $ python setup.py sdist 82 | 83 | 84 | Test 85 | ```` 86 | Before you open a pull request, your code must have passing `pytest `__ unit tests. In addition, you should include a set of functional tests written to use a real BIG-IP® for testing. Information on how to run our set of tests is included below. 87 | 88 | Unit Tests 89 | ~~~~~~~~~~ 90 | 91 | We use pytest for our unit tests. 92 | 93 | 1. If you haven't already, install the required test packages listed in requirements.test.txt in your virtual 94 | environment. 95 | 96 | .. code:: shell 97 | 98 | $ pip install -r requirements.test.txt 99 | 100 | 101 | 2. Run the tests and produce a coverage report. The ``--cov-report=html`` 102 | will create a ``htmlcov/`` directory that you can view in your browser to see the missing lines of code. 103 | 104 | .. code:: shell 105 | 106 | $ py.test --cov ./icontrol --cov-report=html 107 | $ open htmlcov/index.html 108 | 109 | 110 | Style Checks 111 | ~~~~~~~~~~~~ 112 | We use the hacking module for our style checks (installed as part of 113 | step 1 in the Unit Test section). 114 | 115 | .. code:: shell 116 | 117 | $ flake8 ./ 118 | 119 | Copyright 120 | --------- 121 | Copyright 2015-2016 F5 Networks Inc. 122 | 123 | Support 124 | ------- 125 | See `Support `_. 126 | 127 | License 128 | ------- 129 | 130 | Apache V2.0 131 | ``````````` 132 | Licensed under the Apache License, Version 2.0 (the "License"); you may 133 | not use this file except in compliance with the License. You may obtain 134 | a copy of the License at 135 | 136 | http://www.apache.org/licenses/LICENSE-2.0 137 | 138 | Unless required by applicable law or agreed to in writing, software 139 | distributed under the License is distributed on an "AS IS" BASIS, 140 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 141 | See the License for the specific language governing permissions and 142 | limitations under the License. 143 | 144 | Contributor License Agreement 145 | ````````````````````````````` 146 | Individuals or business entities who contribute to this project must have completed and submitted the `F5 Contributor License Agreement `__ to Openstack\_CLA@f5.com prior to their code submission being included in this project. 147 | 148 | 149 | .. |Build Status| image:: https://travis-ci.org/F5Networks/f5-icontrol-rest-python.svg?branch=develop 150 | :target: https://travis-ci.org/F5Networks/f5-icontrol-rest-python 151 | .. |Documentation Status| image:: https://readthedocs.org/projects/icontrol/badge/?version=latest 152 | :target: http://icontrol.readthedocs.org/en/latest/?badge=latest 153 | .. |slack badge| image:: https://f5-openstack-slack.herokuapp.com/badge.svg 154 | :target: https://f5-openstack-slack.herokuapp.com/ 155 | :alt: Slack 156 | 157 | -------------------------------------------------------------------------------- /RELEASE: -------------------------------------------------------------------------------- 1 | 1 2 | -------------------------------------------------------------------------------- /SUPPORT.md: -------------------------------------------------------------------------------- 1 | Maintenance and F5 Technical Support of the F5 code is provided only if the 2 | software (i) is unmodified; and (ii) has been marked as F5 Supported in 3 | SOL80012344, (https://support.f5.com/kb/en-us/solutions/public/k/80/sol80012344.html). 4 | Support will only be provided to customers who have an existing support contract, 5 | purchased separately, subject to F5’s support policies available at 6 | http://www.f5.com/about/guidelines-policies/ and http://askf5.com. -------------------------------------------------------------------------------- /conftest.py: -------------------------------------------------------------------------------- 1 | # Copyright 2015-2016 F5 Networks Inc. 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # 7 | # http://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | # See the License for the specific language governing permissions and 13 | # limitations under the License. 14 | # 15 | 16 | from icontrol.session import iControlRESTSession 17 | 18 | import pytest 19 | 20 | 21 | def pytest_addoption(parser): 22 | parser.addoption("--bigip", action="store", default='localhost', 23 | help="BIG-IP hostname or IP address") 24 | parser.addoption("--username", action="store", help="BIG-IP REST username", 25 | default="admin") 26 | parser.addoption("--port", action="store", help="BIG-IP port", 27 | default='443') 28 | parser.addoption("--password", action="store", help="BIG-IP REST password", 29 | default="admin") 30 | 31 | parser.addoption("--release", action="store", 32 | help="TMOS version, in dotted format, eg. 12.0.0", 33 | default='11.6.0') 34 | 35 | # These are optional; if not specified, tests skip. 36 | parser.addoption("--nonadmin-username", action="store", 37 | help="BIG-IP REST username for non-admin user", 38 | default=None) 39 | parser.addoption("--nonadmin-password", action="store", 40 | help="BIG-IP REST password for non-admin user", 41 | default=None) 42 | parser.addoption("--ca-bundle", action="store", 43 | help="CA bundle that verifies the BIG-IP certificate", 44 | default=None) 45 | 46 | 47 | def pytest_generate_tests(metafunc): 48 | assert metafunc.config.option.bigip 49 | 50 | 51 | @pytest.fixture 52 | def opt_bigip(request): 53 | return request.config.getoption("--bigip") 54 | 55 | 56 | @pytest.fixture 57 | def opt_username(request): 58 | return request.config.getoption("--username") 59 | 60 | 61 | @pytest.fixture 62 | def opt_password(request): 63 | return request.config.getoption("--password") 64 | 65 | 66 | @pytest.fixture 67 | def opt_port(request): 68 | return request.config.getoption("--port") 69 | 70 | 71 | @pytest.fixture 72 | def opt_nonadmin_username(request): 73 | return request.config.getoption("--nonadmin-username") 74 | 75 | 76 | @pytest.fixture 77 | def opt_nonadmin_password(request): 78 | return request.config.getoption("--nonadmin-password") 79 | 80 | 81 | @pytest.fixture 82 | def opt_ca_bundle(request): 83 | return request.config.getoption("--ca-bundle") 84 | 85 | 86 | @pytest.fixture 87 | def ICR(opt_bigip, opt_username, opt_password): 88 | icr = iControlRESTSession(opt_username, opt_password) 89 | return icr 90 | 91 | 92 | @pytest.fixture 93 | def opt_release(request): 94 | return request.config.getoption("--release") 95 | 96 | 97 | @pytest.fixture 98 | def GET_URL(opt_bigip, opt_port): 99 | url = 'https://' + opt_bigip + ':' + opt_port + '/mgmt/tm/ltm/nat/' 100 | return url 101 | 102 | 103 | @pytest.fixture 104 | def POST_URL(opt_bigip, opt_port): 105 | url = 'https://' + opt_bigip + ':' + opt_port + '/mgmt/tm/ltm/nat/' 106 | return url 107 | 108 | 109 | @pytest.fixture 110 | def FAKE_URL(opt_bigip, opt_port): 111 | fake_url = 'https://' + opt_bigip + ':' + opt_port + '/mgmt/tm/bogus/' 112 | return fake_url 113 | 114 | 115 | @pytest.fixture 116 | def BASE_URL(opt_bigip, opt_port): 117 | return 'https://' + opt_bigip + ':' + opt_port + '/mgmt/tm/' 118 | -------------------------------------------------------------------------------- /docs/apidocs/icontrol.rst: -------------------------------------------------------------------------------- 1 | icontrol package 2 | ================ 3 | 4 | Submodules 5 | ---------- 6 | 7 | 8 | icontrol.authtoken module 9 | -------------------------- 10 | 11 | .. automodule:: icontrol.authtoken 12 | :members: 13 | :undoc-members: 14 | :show-inheritance: 15 | 16 | 17 | icontrol.exceptions module 18 | -------------------------- 19 | 20 | .. automodule:: icontrol.exceptions 21 | :members: 22 | :undoc-members: 23 | :show-inheritance: 24 | 25 | 26 | icontrol.session module 27 | ----------------------- 28 | 29 | .. automodule:: icontrol.session 30 | :members: 31 | :undoc-members: 32 | :show-inheritance: 33 | 34 | 35 | Module contents 36 | --------------- 37 | 38 | .. automodule:: icontrol 39 | :members: 40 | :undoc-members: 41 | :show-inheritance: 42 | -------------------------------------------------------------------------------- /docs/apidocs/modules.rst: -------------------------------------------------------------------------------- 1 | icontrol 2 | ======== 3 | 4 | .. toctree:: 5 | :maxdepth: 4 6 | 7 | icontrol 8 | -------------------------------------------------------------------------------- /docs/conf.py: -------------------------------------------------------------------------------- 1 | # Copyright 2015-2016 F5 Networks Inc. 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # 7 | # http://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | # See the License for the specific language governing permissions and 13 | # limitations under the License. 14 | 15 | # -*- coding: utf-8 -*- 16 | # 17 | # f5-icontrol-rest documentation build configuration file, created by 18 | # sphinx-quickstart on Wed Jan 13 16:34:27 2016. 19 | # 20 | # This file is execfile()d with the current directory set to its 21 | # containing dir. 22 | # 23 | # Note that not all possible configuration values are present in this 24 | # autogenerated file. 25 | # 26 | # All configuration values have a default; values that are commented out 27 | # serve to show the default. 28 | 29 | import os 30 | import sys 31 | 32 | # If extensions (or modules to document with autodoc) are in another directory, 33 | # add these directories to sys.path here. If the directory is relative to the 34 | # documentation root, use os.path.abspath to make it absolute, like shown here. 35 | sys.path.insert(0, os.path.abspath('..')) 36 | 37 | from icontrol import __version__ as version 38 | 39 | # -- General configuration ------------------------------------------------ 40 | 41 | # If your documentation needs a minimal Sphinx version, state it here. 42 | # needs_sphinx = '1.0' 43 | 44 | # Add any Sphinx extension module names here, as strings. They can be 45 | # extensions coming with Sphinx (named 'sphinx.ext.*') or your custom 46 | # ones. 47 | extensions = [ 48 | 'sphinx.ext.autodoc', 49 | 'sphinx.ext.autosummary', 50 | 'sphinx.ext.intersphinx' 51 | ] 52 | 53 | intersphinx_mapping = { 54 | 'requests': ('http://docs.python-requests.org/en/latest/', None) 55 | } 56 | 57 | # Add any paths that contain templates here, relative to this directory. 58 | templates_path = ['_templates'] 59 | 60 | # The suffix(es) of source filenames. 61 | # You can specify multiple suffix as a list of string: 62 | 63 | source_suffix = '.rst' 64 | 65 | # The encoding of source files. 66 | # source_encoding = 'utf-8-sig' 67 | 68 | # The master toctree document. 69 | master_doc = 'index' 70 | 71 | # General information about the project. 72 | project = u'f5-icontrol-rest' 73 | copyright = u'2016, F5 Networks' 74 | author = u'F5 Networks' 75 | 76 | # The version info for the project you're documenting, acts as replacement for 77 | # |version| and |release|, also used in various other places throughout the 78 | # built documents. 79 | # 80 | # The short X.Y version. 81 | version = version 82 | # The full version, including alpha/beta/rc tags. 83 | release = version 84 | 85 | # The language for content autogenerated by Sphinx. Refer to documentation 86 | # for a list of supported languages. 87 | # 88 | # This is also used if you do content translation via gettext catalogs. 89 | # Usually you set "language" from the command line for these cases. 90 | language = None 91 | 92 | # There are two options for replacing |today|: either, you set today to some 93 | # non-false value, then it is used: 94 | # today = '' 95 | # Else, today_fmt is used as the format for a strftime call. 96 | # today_fmt = '%B %d, %Y' 97 | 98 | # List of patterns, relative to source directory, that match files and 99 | # directories to ignore when looking for source files. 100 | exclude_patterns = ['_build'] 101 | 102 | # The reST default role (used for this markup: `text`) to use for all 103 | # documents. 104 | # default_role = None 105 | 106 | # If true, '()' will be appended to :func: etc. cross-reference text. 107 | # add_function_parentheses = True 108 | 109 | # If true, the current module name will be prepended to all description 110 | # unit titles (such as .. function::). 111 | # add_module_names = True 112 | 113 | # If true, sectionauthor and moduleauthor directives will be shown in the 114 | # output. They are ignored by default. 115 | # show_authors = False 116 | 117 | # The name of the Pygments (syntax highlighting) style to use. 118 | pygments_style = 'sphinx' 119 | 120 | # A list of ignored prefixes for module index sorting. 121 | # modindex_common_prefix = [] 122 | 123 | # If true, keep warnings as "system message" paragraphs in the built documents. 124 | # keep_warnings = False 125 | 126 | # If true, `todo` and `todoList` produce output, else they produce nothing. 127 | todo_include_todos = False 128 | 129 | 130 | # -- Options for HTML output ---------------------------------------------- 131 | 132 | # The theme to use for HTML and HTML Help pages. See the documentation for 133 | # a list of builtin themes. 134 | # html_theme = 'alabaster' 135 | html_theme = "sphinx_rtd_theme" 136 | # html_theme_path = [sphinx_rtd_theme.get_html_theme_path()] 137 | 138 | # Theme options are theme-specific and customize the look and feel of a theme 139 | # further. For a list of options available for each theme, see the 140 | # documentation. 141 | # html_theme_options = {} 142 | 143 | # Add any paths that contain custom themes here, relative to this directory. 144 | # html_theme_path = [] 145 | 146 | # The name for this set of Sphinx documents. If None, it defaults to 147 | # " v documentation". 148 | # html_title = None 149 | 150 | # A shorter title for the navigation bar. Default is the same as html_title. 151 | # html_short_title = None 152 | 153 | # The name of an image file (relative to this directory) to place at the top 154 | # of the sidebar. 155 | # html_logo = None 156 | 157 | # The name of an image file (within the static path) to use as favicon of the 158 | # docs. This file should be a Windows icon file (.ico) being 16x16 or 32x32 159 | # pixels large. 160 | # html_favicon = None 161 | 162 | # Add any paths that contain custom static files (such as style sheets) here, 163 | # relative to this directory. They are copied after the builtin static files, 164 | # so a file named "default.css" will overwrite the builtin "default.css". 165 | html_static_path = ['_static'] 166 | 167 | # Add any extra paths that contain custom files (such as robots.txt or 168 | # .htaccess) here, relative to this directory. These files are copied 169 | # directly to the root of the documentation. 170 | # html_extra_path = [] 171 | 172 | # If not '', a 'Last updated on:' timestamp is inserted at every page bottom, 173 | # using the given strftime format. 174 | # html_last_updated_fmt = '%b %d, %Y' 175 | 176 | # If true, SmartyPants will be used to convert quotes and dashes to 177 | # typographically correct entities. 178 | # html_use_smartypants = True 179 | 180 | # Custom sidebar templates, maps document names to template names. 181 | # html_sidebars = {} 182 | 183 | # Additional templates that should be rendered to pages, maps page names to 184 | # template names. 185 | # html_additional_pages = {} 186 | 187 | # If false, no module index is generated. 188 | # html_domain_indices = True 189 | 190 | # If false, no index is generated. 191 | # html_use_index = True 192 | 193 | # If true, the index is split into individual pages for each letter. 194 | # html_split_index = False 195 | 196 | # If true, links to the reST sources are added to the pages. 197 | # html_show_sourcelink = True 198 | 199 | # If true, "Created using Sphinx" is shown in the HTML footer. Default is True. 200 | # html_show_sphinx = True 201 | 202 | # If true, "(C) Copyright ..." is shown in the HTML footer. Default is True. 203 | # html_show_copyright = True 204 | 205 | # If true, an OpenSearch description file will be output, and all pages will 206 | # contain a tag referring to it. The value of this option must be the 207 | # base URL from which the finished HTML is served. 208 | # html_use_opensearch = '' 209 | 210 | # This is the file name suffix for HTML files (e.g. ".xhtml"). 211 | # html_file_suffix = None 212 | 213 | # Language to be used for generating the HTML full-text search index. 214 | # Sphinx supports the following languages: 215 | # 'da', 'de', 'en', 'es', 'fi', 'fr', 'hu', 'it', 'ja' 216 | # 'nl', 'no', 'pt', 'ro', 'ru', 'sv', 'tr' 217 | # html_search_language = 'en' 218 | 219 | # A dictionary with options for the search language support, empty by default. 220 | # Now only 'ja' uses this config value 221 | # html_search_options = {'type': 'default'} 222 | 223 | # The name of a javascript file (relative to the configuration directory) that 224 | # implements a search results scorer. If empty, the default will be used. 225 | # html_search_scorer = 'scorer.js' 226 | 227 | # Output file base name for HTML help builder. 228 | htmlhelp_basename = 'f5-icontrol-restdoc' 229 | 230 | # -- Options for LaTeX output --------------------------------------------- 231 | 232 | latex_elements = { 233 | # The paper size ('letterpaper' or 'a4paper'). 234 | # 'papersize': 'letterpaper', 235 | 236 | # The font size ('10pt', '11pt' or '12pt'). 237 | # 'pointsize': '10pt', 238 | 239 | # Additional stuff for the LaTeX preamble. 240 | # 'preamble': '', 241 | 242 | # Latex figure (float) alignment 243 | # 'figure_align': 'htbp', 244 | } 245 | 246 | # Grouping the document tree into LaTeX files. List of tuples 247 | # (source start file, target name, title, 248 | # author, documentclass [howto, manual, or own class]). 249 | latex_documents = [ 250 | (master_doc, 'f5-icontrol-rest.tex', u'f5-icontrol-rest Documentation', 251 | u'F5 Networks', 'manual'), 252 | ] 253 | 254 | # The name of an image file (relative to this directory) to place at the top of 255 | # the title page. 256 | # latex_logo = None 257 | 258 | # For "manual" documents, if this is true, then toplevel headings are parts, 259 | # not chapters. 260 | # latex_use_parts = False 261 | 262 | # If true, show page references after internal links. 263 | # latex_show_pagerefs = False 264 | 265 | # If true, show URL addresses after external links. 266 | # latex_show_urls = False 267 | 268 | # Documents to append as an appendix to all manuals. 269 | # latex_appendices = [] 270 | 271 | # If false, no module index is generated. 272 | # latex_domain_indices = True 273 | 274 | 275 | # -- Options for manual page output --------------------------------------- 276 | 277 | # One entry per manual page. List of tuples 278 | # (source start file, name, description, authors, manual section). 279 | man_pages = [ 280 | (master_doc, 'icontrol', u'icontrol Documentation', 281 | [author], 1) 282 | ] 283 | 284 | # If true, show URL addresses after external links. 285 | # man_show_urls = False 286 | 287 | 288 | # -- Options for Texinfo output ------------------------------------------- 289 | 290 | # Grouping the document tree into Texinfo files. List of tuples 291 | # (source start file, target name, title, author, 292 | # dir menu entry, description, category) 293 | texinfo_documents = [ 294 | (master_doc, 'icontrol', u'icontrol Documentation', 295 | author, 'icontrol', 'One line description of project.', 296 | 'Miscellaneous'), 297 | ] 298 | 299 | # Documents to append as an appendix to all manuals. 300 | # texinfo_appendices = [] 301 | 302 | # If false, no module index is generated. 303 | # texinfo_domain_indices = True 304 | 305 | # How to display URL addresses: 'footnote', 'no', or 'inline'. 306 | # texinfo_show_urls = 'footnote' 307 | 308 | # If true, do not generate a @detailmenu in the "Top" node's menu. 309 | # texinfo_no_detailmenu = False 310 | -------------------------------------------------------------------------------- /docs/index.rst: -------------------------------------------------------------------------------- 1 | .. f5-icontrol-rest documentation master file, created by 2 | sphinx-quickstart on Wed Jan 13 16:34:27 2016. 3 | You can adapt this file completely to your liking, but it should at least 4 | contain the root `toctree` directive. 5 | 6 | f5-icontrol-rest-python 7 | ======================= 8 | 9 | .. raw:: html 10 | 11 | 12 | 13 | Overview 14 | -------- 15 | The F5 Networks® :mod:`icontrol` module is used to send commands to the `BIGIP® 16 | iControl® REST API `_. 17 | The library maintains a HTTP session (which is a :class:`requests.Session`) and 18 | does URL validation and logging. 19 | 20 | Installation 21 | ------------ 22 | 23 | Using Pip 24 | +++++++++ 25 | 26 | .. code-block:: bash 27 | 28 | $ pip install f5-icontrol-rest 29 | 30 | 31 | GitHub 32 | ++++++ 33 | 34 | `F5Networks/f5-icontrol-rest-python `_ 35 | 36 | Examples 37 | -------- 38 | .. code-block:: python 39 | 40 | from icontrol.session import iControlRESTSession 41 | icr_session = iControlRESTSession('myuser', 'mypass') 42 | 43 | # GET to https://bigip.example.com/mgmt/tm/ltm/nat/~Common~mynat 44 | icr_session.get( 45 | 'https://bigip.example.com/mgmt/tm/ltm/nat', 46 | name='mynat', 47 | partition='Common') 48 | 49 | # GET to https://bigip.example.com/mgmt/tm/ltm/nat 50 | icr_session.get('https://bigip.example.com/mgmt/tm/ltm/nat') 51 | 52 | # POST with json data 53 | icr_session.post('https://bigip.example.com/mgmt/tm/ltm/nat', \ 54 | json={'name': 'myname', 'partition': 'Common'}) 55 | 56 | 57 | Module Documentation 58 | -------------------- 59 | 60 | .. toctree:: 61 | :maxdepth: 4 62 | 63 | apidocs/modules 64 | 65 | 66 | License 67 | ------- 68 | Apache V2.0 69 | +++++++++++ 70 | Licensed under the Apache License, Version 2.0 (the "License"); 71 | you may not use this file except in compliance with the License. 72 | You may obtain a copy of the License at 73 | 74 | http://www.apache.org/licenses/LICENSE-2.0 75 | 76 | Unless required by applicable law or agreed to in writing, software 77 | distributed under the License is distributed on an "AS IS" BASIS, 78 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 79 | See the License for the specific language governing permissions and 80 | limitations under the License. 81 | 82 | Contributor License Agreement 83 | +++++++++++++++++++++++++++++ 84 | Individuals or business entities who contribute to this project must have completed and submitted the `F5® Contributor License Agreement `_ to Openstack_CLA@f5.com prior to their code submission being included in this project. 85 | 86 | -------------------------------------------------------------------------------- /f5-icontrol-rest-dist/Docker/redhat/6/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM centos:6 2 | 3 | RUN yum update -y && yum install rpm-build make tar python-setuptools -y 4 | 5 | COPY ./build-rpms.sh / 6 | -------------------------------------------------------------------------------- /f5-icontrol-rest-dist/Docker/redhat/6/build-rpms.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash -ex 2 | 3 | if [ $# -eq 1 ]; then 4 | SRC_DIR=$1 5 | elif [ $# -eq 0 ]; then 6 | SCRIPTNAME="`readlink --canonicalize $0`" 7 | SRC_DIR="`dirname "$SCRIPTNAME"`/../../../.." 8 | SRC_DIR="`readlink --canonicalize $SRC_DIR`" 9 | else 10 | echo "Error: Cound not deduce SRC_DIR, exiting" >&2 11 | fi 12 | 13 | PKG_NAME=f5-icontrol-rest 14 | DIST_DIR="${PKG_NAME}-dist" 15 | RPMBUILD_DIR="rpmbuild" 16 | DEST_DIR="${SRC_DIR}/${DIST_DIR}" 17 | 18 | # Deduce the DIST name from "rpm --showrc" 19 | getdist() { 20 | rpm --showrc | while read arg1 arg2 arg3; do 21 | case $arg1 in 22 | -[1-9]*:) 23 | #echo found valid arg1: arg2: $arg2, arg3: $arg3 24 | case $arg2 in 25 | dist) 26 | #echo found valid arg: arg3: $arg3 27 | #echo DIST=$arg3 28 | echo $arg3 29 | ;; 30 | esac 31 | esac 32 | done 33 | } 34 | DIST="`getdist`" 35 | DISTDIR="`echo $DIST | tr -d '.'`" 36 | 37 | echo "Building ${PKG_NAME} RPM packages..." 38 | buildroot=$(mktemp -d /tmp/${PKG_NAME}.XXXXX) 39 | 40 | cp -R $SRC_DIR/* ${buildroot} 41 | 42 | pushd ${buildroot} 43 | python setup.py build bdist_rpm --rpm-base rpmbuild --release=1$DIST 44 | 45 | echo "%_topdir ${buildroot}/rpmbuild" > ~/.rpmmacros 46 | 47 | python setup.py bdist_rpm --spec-only --dist-dir rpmbuild/SPECS --release=1$DIST 48 | 49 | rpmbuild -ba rpmbuild/SPECS/${PKG_NAME}.spec 50 | 51 | # Use DIST specific subdirectories 52 | install -d "${DEST_DIR}/rpms/build/$DISTDIR/RPMS" 53 | install rpmbuild/RPMS/*/*.rpm "${DEST_DIR}/rpms/build/$DISTDIR/RPMS/" 54 | install -d "${DEST_DIR}/rpms/build/$DISTDIR/SRPMS" 55 | install rpmbuild/SRPMS/*.rpm "${DEST_DIR}/rpms/build/$DISTDIR/SRPMS/" 56 | 57 | popd 58 | 59 | #rm -rf ${buildroot} 60 | 61 | -------------------------------------------------------------------------------- /f5-icontrol-rest-dist/Docker/redhat/7/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM centos:7 2 | 3 | RUN yum update -y && yum install rpm-build make python-setuptools -y 4 | 5 | COPY ./build-rpms.sh / 6 | -------------------------------------------------------------------------------- /f5-icontrol-rest-dist/Docker/redhat/7/build-rpms.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash -ex 2 | 3 | if [ $# -eq 1 ]; then 4 | SRC_DIR=$1 5 | elif [ $# -eq 0 ]; then 6 | SCRIPTNAME="`readlink --canonicalize $0`" 7 | SRC_DIR="`dirname "$SCRIPTNAME"`/../../../.." 8 | SRC_DIR="`readlink --canonicalize $SRC_DIR`" 9 | else 10 | echo "Error: Cound not deduce SRC_DIR, exiting" >&2 11 | fi 12 | 13 | PKG_NAME=f5-icontrol-rest 14 | DIST_DIR="${PKG_NAME}-dist" 15 | RPMBUILD_DIR="rpmbuild" 16 | DEST_DIR="${SRC_DIR}/${DIST_DIR}" 17 | 18 | # Deduce the DIST name from "rpm --showrc" 19 | getdist() { 20 | rpm --showrc | while read arg1 arg2 arg3; do 21 | case $arg1 in 22 | -[1-9]*:) 23 | #echo found valid arg1: arg2: $arg2, arg3: $arg3 24 | case $arg2 in 25 | dist) 26 | #echo found valid arg: arg3: $arg3 27 | #echo DIST=$arg3 28 | echo $arg3 29 | ;; 30 | esac 31 | esac 32 | done 33 | } 34 | DIST="`getdist`" 35 | DISTDIR="`echo $DIST | tr -d '.'`" 36 | 37 | echo "Building ${PKG_NAME} RPM packages..." 38 | buildroot=$(mktemp -d /tmp/${PKG_NAME}.XXXXX) 39 | 40 | cp -R $SRC_DIR/* ${buildroot} 41 | 42 | pushd ${buildroot} 43 | python setup.py build bdist_rpm --rpm-base rpmbuild --release=1$DIST 44 | 45 | echo "%_topdir ${buildroot}/rpmbuild" > ~/.rpmmacros 46 | 47 | python setup.py bdist_rpm --spec-only --dist-dir rpmbuild/SPECS --release=1$DIST 48 | 49 | rpmbuild -ba rpmbuild/SPECS/${PKG_NAME}.spec 50 | 51 | # Use DIST specific subdirectories 52 | install -d "${DEST_DIR}/rpms/build/$DISTDIR/RPMS" 53 | install rpmbuild/RPMS/*/*.rpm "${DEST_DIR}/rpms/build/$DISTDIR/RPMS/" 54 | install -d "${DEST_DIR}/rpms/build/$DISTDIR/SRPMS" 55 | install rpmbuild/SRPMS/*.rpm "${DEST_DIR}/rpms/build/$DISTDIR/SRPMS/" 56 | 57 | popd 58 | 59 | #rm -rf ${buildroot} 60 | 61 | -------------------------------------------------------------------------------- /f5-icontrol-rest-dist/Docker/ubuntu/14.04/Dockerfile: -------------------------------------------------------------------------------- 1 | # Dockerfile 2 | FROM ubuntu:trusty 3 | 4 | RUN apt-get update && apt-get install -y \ 5 | python-stdeb \ 6 | fakeroot \ 7 | python-all 8 | 9 | COPY ./build-debs.sh / 10 | 11 | -------------------------------------------------------------------------------- /f5-icontrol-rest-dist/Docker/ubuntu/14.04/build-debs.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash -ex 2 | 3 | SRC_DIR=$1 4 | pushd $SRC_DIR 5 | PKG_VERSION=$(python -c "import icontrol; print(icontrol.__version__)") 6 | 7 | PKG_NAME="f5-icontrol-rest" 8 | 9 | TMP_DIST="/var/deb_dist" 10 | OS_VERSION="1404" 11 | DIST_DIR="f5-icontrol-rest-dist/deb_dist" 12 | 13 | echo "Building ${PKG_NAME} debian packages..." 14 | 15 | cp -R "${SRC_DIR}/${DIST_DIR}/stdeb.cfg" . 16 | cp -R "${SRC_DIR}/${DIST_DIR}" ${TMP_DIST} 17 | 18 | python setup.py --command-packages=stdeb.command sdist_dsc --dist-dir=${TMP_DIST} 19 | pushd "${TMP_DIST}/${PKG_NAME}-${PKG_VERSION}" 20 | dpkg-buildpackage -rfakeroot -uc -us 21 | popd; popd 22 | 23 | pkg="python-${PKG_NAME}_${PKG_VERSION}-1_all.deb" 24 | cp "${TMP_DIST}/${pkg}" "${SRC_DIR}/${DIST_DIR}/${pkg%%_all.deb}_${OS_VERSION}_all.deb" 25 | -------------------------------------------------------------------------------- /f5-icontrol-rest-dist/deb_dist/stdeb.cfg: -------------------------------------------------------------------------------- 1 | [DEFAULT] 2 | Depends: 3 | python-requests(>= 2.5.0) 4 | -------------------------------------------------------------------------------- /f5-icontrol-rest-dist/scripts/package.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash -ex 2 | 3 | OS_TYPE=$1 4 | OS_VERSION=$2 5 | PKG_NAME="f5-icontrol-rest" 6 | DIST_DIR="${PKG_NAME}-dist" 7 | 8 | BUILD_CONTAINER=${OS_TYPE}${OS_VERSION}-${PKG_NAME}-builder 9 | WORKING_DIR="/var/wdir" 10 | 11 | if [[ ${OS_TYPE} == "redhat" ]]; then 12 | PKG_TYPE="rpms" 13 | elif [[ ${OS_TYPE} == "ubuntu" ]]; then 14 | PKG_TYPE="debs" 15 | else 16 | echo "Unsupported target OS (${OS_TYPE})" 17 | exit 1 18 | fi 19 | 20 | docker build -t ${BUILD_CONTAINER} ${DIST_DIR}/Docker/${OS_TYPE}/${OS_VERSION} 21 | docker run --privileged --rm -v $(pwd):${WORKING_DIR} ${BUILD_CONTAINER} /bin/bash /build-${PKG_TYPE}.sh "${WORKING_DIR}" 22 | 23 | exit 0 24 | -------------------------------------------------------------------------------- /icontrol/__init__.py: -------------------------------------------------------------------------------- 1 | __version__ = "1.3.17" 2 | -------------------------------------------------------------------------------- /icontrol/authtoken.py: -------------------------------------------------------------------------------- 1 | # Copyright 2015-2016 F5 Networks Inc. 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # 7 | # http://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | # See the License for the specific language governing permissions and 13 | # limitations under the License. 14 | """A requests-compatible system for BIG-IP token-based authentication. 15 | 16 | BIG-IP only allows users with the Administrator role to authenticate to 17 | iControl using HTTP Basic auth. Non-Administrator users can use the 18 | token-based authentication scheme described at: 19 | 20 | https://devcentral.f5.com/wiki/icontrol.authentication_with_the_f5_rest_api.ashx 21 | 22 | Use this module with requests to automatically get a new token, and attach 23 | :class:`requests.Session` object, so that it is used to authenticate future 24 | requests. 25 | 26 | Instead of using this module directly, it is easiest to enable it by passing 27 | a ``token=True`` argument when creating the 28 | :class:`.iControlRESTSession`: 29 | 30 | >>> iCRS = iControlRESTSession('bob', 'secret', token=True) 31 | """ 32 | 33 | import requests 34 | import time 35 | import logging 36 | 37 | from icontrol.exceptions import iControlUnexpectedHTTPError 38 | from icontrol.exceptions import InvalidScheme 39 | from requests.auth import AuthBase 40 | from requests.auth import HTTPBasicAuth 41 | 42 | try: 43 | # Python 3 44 | from urllib.parse import urlsplit 45 | except ImportError: 46 | # Python 2 47 | from urlparse import urlsplit 48 | 49 | 50 | class iControlRESTTokenAuth(AuthBase): 51 | """Acquire and renew BigIP iControl REST authentication tokens. 52 | 53 | :param str username: The username on BigIP 54 | :param str password: The password for username on BigIP 55 | :param str login_provider_name: The name of the login provider that \ 56 | BigIP should consult when creating the token. 57 | :param str verify: The path to a CA bundle containing the \ 58 | CA certificate for SSL validation 59 | :param proxies: A dict of proxy information for Requests to utilize \ 60 | on this connection to the BigIP 61 | 62 | If ``username`` is configured locally on the BigIP, 63 | ``login_provider_name`` should be ``"tmos"`` (default). Otherwise 64 | (for example, ``username`` is configured on LDAP that BigIP consults), 65 | consult BigIP documentation or your system administrator for the value 66 | of ``login_provider_name``. 67 | """ 68 | def __init__(self, username, password, login_provider_name='tmos', 69 | verify=False, auth_provider=None, proxies=None): 70 | self.username = username 71 | self.password = password 72 | self.login_provider_name = login_provider_name 73 | self.proxies = proxies 74 | self.token = None 75 | self.expiration = None 76 | self.attempts = 0 77 | self.verify = verify 78 | self.auth_provider = auth_provider 79 | # We don't actually do auth at this point because we don't have a 80 | # hostname to authenticate to. 81 | 82 | def _check_token_validity(self): 83 | if not self.token: 84 | return False 85 | if self.expiration and time.time() > self.expiration: 86 | return False 87 | return True 88 | 89 | def get_auth_providers(self, netloc): 90 | """BIG-IQ specific query for auth providers 91 | 92 | BIG-IP doesn't really need this because BIG-IP's multiple auth providers 93 | seem to handle fallthrough just fine. BIG-IQ on the other hand, needs to 94 | have its auth provider specified if you're using one of the non-default 95 | ones. 96 | 97 | :param netloc: 98 | :return: 99 | """ 100 | url = "https://%s/info/system?null" % (netloc) 101 | 102 | response = requests.get(url, verify=self.verify, proxies=self.proxies) 103 | if not response.ok or not hasattr(response, "json"): 104 | error_message = '%s Unexpected Error: %s for uri: %s Text: %r' %\ 105 | (response.status_code, 106 | response.reason, 107 | response.url, 108 | response.text) 109 | raise iControlUnexpectedHTTPError(error_message, response=response) 110 | respJson = response.json() 111 | result = respJson['providers'] 112 | return result 113 | 114 | def get_new_token(self, netloc): 115 | """Get a new token from BIG-IP and store it internally. 116 | 117 | Throws relevant exception if it fails to get a new token. 118 | 119 | This method will be called automatically if a request is attempted 120 | but there is no authentication token, or the authentication token 121 | is expired. It is usually not necessary for users to call it, but 122 | it can be called if it is known that the authentication token has 123 | been invalidated by other means. 124 | """ 125 | logger = logging.getLogger(__name__) 126 | login_body = { 127 | 'username': self.username, 128 | 'password': self.password, 129 | } 130 | 131 | if self.auth_provider: 132 | if self.auth_provider == 'local': 133 | login_body['loginProviderName'] = 'local' 134 | elif self.auth_provider == 'tmos': 135 | login_body['loginProviderName'] = 'tmos' 136 | elif self.auth_provider not in ['none', 'default']: 137 | providers = self.get_auth_providers(netloc) 138 | for provider in providers: 139 | if self.auth_provider in provider['link']: 140 | login_body['loginProviderName'] = provider['name'] 141 | break 142 | elif self.auth_provider == provider['name']: 143 | login_body['loginProviderName'] = provider['name'] 144 | break 145 | else: 146 | if self.login_provider_name == 'tmos': 147 | login_body['loginProviderName'] = self.login_provider_name 148 | 149 | login_url = "https://%s/mgmt/shared/authn/login" % (netloc) 150 | 151 | response = requests.post( 152 | login_url, 153 | json=login_body, 154 | verify=self.verify, 155 | auth=HTTPBasicAuth(self.username, self.password), 156 | proxies=self.proxies, 157 | ) 158 | self.attempts += 1 159 | if not response.ok or not hasattr(response, "json"): 160 | error_message = '%s Unexpected Error: %s for uri: %s Text: %r' %\ 161 | (response.status_code, 162 | response.reason, 163 | response.url, 164 | response.text) 165 | raise iControlUnexpectedHTTPError(error_message, 166 | response=response) 167 | respJson = response.json() 168 | 169 | token = self._get_token_from_response(respJson) 170 | created_bigip = self._get_last_update_micros(token) 171 | 172 | try: 173 | expiration_bigip = self._get_expiration_micros( 174 | token, created_bigip 175 | ) 176 | except (KeyError, ValueError): 177 | error_message = \ 178 | '%s Unparseable Response: %s for uri: %s Text: %r' %\ 179 | (response.status_code, 180 | response.reason, 181 | response.url, 182 | response.text) 183 | raise iControlUnexpectedHTTPError(error_message, 184 | response=response) 185 | 186 | try: 187 | self.expiration = self._get_token_expiration_time( 188 | created_bigip, expiration_bigip 189 | ) 190 | logger.debug("Wait for 1 sec after login...") 191 | time.sleep(1) 192 | except iControlUnexpectedHTTPError: 193 | error_message = \ 194 | '%s Token already expired: %s for uri: %s Text: %r' % \ 195 | (response.status_code, 196 | time.ctime(expiration_bigip), 197 | response.url, 198 | response.text) 199 | raise iControlUnexpectedHTTPError(error_message, 200 | response=response) 201 | 202 | def _get_expiration_micros(self, token, created_bigip=None): 203 | if 'expirationMicros' in token: 204 | expiration = token['expirationMicros'] 205 | expiration_bigip = int(expiration) / 1000000.0 206 | elif 'timeout' in token: 207 | expiration_bigip = created_bigip + token['timeout'] 208 | else: 209 | raise iControlUnexpectedHTTPError 210 | return expiration_bigip 211 | 212 | def _get_token_expiration_time(self, created_bigip, expiration_bigip): 213 | req_start_time = time.time() 214 | 215 | # Set our token expiration time. 216 | # The expirationMicros field is when BIG-IP will expire the token 217 | # relative to its local clock. To avoid issues caused by incorrect 218 | # clocks or network latency, we'll compute an expiration time that is 219 | # referenced to our local clock, and expires slightly before the token 220 | # should actually expire on BIG-IP 221 | # Reference to our clock: compute for how long this token is valid as 222 | # the difference between when it expires and when it was created, 223 | # according to BIG-IP. 224 | if expiration_bigip < created_bigip: 225 | raise iControlUnexpectedHTTPError 226 | 227 | valid_duration = expiration_bigip - created_bigip 228 | # Assign new expiration time that is 1 minute earlier than BIG-IP's 229 | # expiration time, as long as that would still be at least a minute in 230 | # the future. This should account for clock skew between us and 231 | # BIG-IP. By default tokens last for 60 minutes so getting one every 232 | # 59 minutes instead of 60 is harmless. 233 | if valid_duration > 120.0: 234 | valid_duration -= 60.0 235 | return req_start_time + valid_duration 236 | 237 | def _get_last_update_micros(self, token): 238 | try: 239 | last_updated = token['lastUpdateMicros'] 240 | created_bigip = int(last_updated) / 1000000.0 241 | except (KeyError, ValueError): 242 | raise iControlUnexpectedHTTPError( 243 | "lastUpdateMicros field was not found in the response" 244 | ) 245 | return created_bigip 246 | 247 | def _get_token_from_response(self, respJson): 248 | try: 249 | token = respJson['token'] 250 | self.token = token['token'] 251 | except KeyError: 252 | raise iControlUnexpectedHTTPError( 253 | "Token field not found in the response" 254 | ) 255 | return token 256 | 257 | def __call__(self, request): 258 | if not self._check_token_validity(): 259 | scheme, netloc, path, _, _ = urlsplit(request.url) 260 | if scheme != "https": 261 | raise InvalidScheme(scheme) 262 | self.get_new_token(netloc) 263 | request.headers['X-F5-Auth-Token'] = self.token 264 | return request 265 | -------------------------------------------------------------------------------- /icontrol/exceptions.py: -------------------------------------------------------------------------------- 1 | # Copyright 2015-2016 F5 Networks Inc. 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # 7 | # http://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | # See the License for the specific language governing permissions and 13 | # limitations under the License. 14 | """Exceptions that can be emitted by the icontrol package.""" 15 | 16 | from requests import HTTPError 17 | 18 | 19 | class iControlUnexpectedHTTPError(HTTPError): 20 | # The Status Code was in the range 207-399 21 | pass 22 | 23 | 24 | class BigIPInvalidURL(Exception): 25 | # Some component to be incorporated into the uri is illegal 26 | pass 27 | 28 | 29 | class InvalidScheme(BigIPInvalidURL): 30 | # The only acceptable scheme is https 31 | pass 32 | 33 | 34 | class InvalidBigIP_ICRURI(BigIPInvalidURL): 35 | # This must contain the servername/address and /mgmt/tm/ 36 | pass 37 | 38 | 39 | class InvalidPrefixCollection(BigIPInvalidURL): 40 | # Must not start with '/' because it's relative to the icr_uri 41 | # must end with a '/' since there may be names or suffixes 42 | # following and they are relative, to the prefix 43 | pass 44 | 45 | 46 | class InvalidInstanceNameOrFolder(BigIPInvalidURL): 47 | # instance names and partitions must not contain the '~' or '/' chars 48 | pass 49 | 50 | 51 | class InvalidSuffixCollection(BigIPInvalidURL): 52 | # must start with a '/' since there may be a partition or name before it 53 | pass 54 | 55 | 56 | class InvalidURIComponentPart(BigIPInvalidURL): 57 | # When a consumer gives the subPath of a uri, the partition should be 58 | # included as well 59 | pass 60 | -------------------------------------------------------------------------------- /icontrol/session.py: -------------------------------------------------------------------------------- 1 | # Copyright 2019 F5 Networks Inc. 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # 7 | # http://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | # See the License for the specific language governing permissions and 13 | # limitations under the License. 14 | """A BigIP-RESTServer URI handler. REST-APIs use it on the :mod:`requests` 15 | library. 16 | 17 | Use this module to make calls to a BigIP-REST server. It will handle: 18 | 19 | #. URI Sanitization uri's produced by this module are checked to ensure 20 | compliance with the BigIP-REST server interface 21 | 22 | #. Session Construction -- the :class:`iControlRESTSession` wraps a 23 | :class:`requests.Session` object. 24 | 25 | #. Logging -- pre- and post- request state is logged. 26 | 27 | #. Exception generation -- Errors in URL construction generate 28 | :class:`BigIPInvalidURL` subclasses; unexpected HTTP status codes raise 29 | :class:`iControlUnexpectedHTTPError`. 30 | 31 | The core functionality of the module is implemented via the 32 | :class:`iControlRESTSession` class. Calls to its' HTTP-methods are checked, 33 | pre-logged, submitted, and post-logged. 34 | 35 | There are 2 modes of operation "full_uri", and "uri_as_parts", toggled by the 36 | `uri_as_parts` boolean keyword param that can be passed to methods. It defaults 37 | to `False`. Use `uri_as_parts` when you want to leverage the full 38 | functionality of this library, and have it construct your uri for you. 39 | Example Use in `uri_as_parts` mode: 40 | 41 | >>> iCRS = iControlRESTSession('jrandomhacker', 'insecure') 42 | >>> iCRS.get('https://192.168.1.1/mgmt/tm/ltm/nat/', \ 43 | partition='Common', name='VALIDNAME', uri_as_parts=True) 44 | 45 | In `full_uri` mode: 46 | 47 | >>> iCRS.get('https://192.168.1.1/mgmt/tm/ltm/nat/~Common~VALIDNAME') 48 | 49 | NOTE: If used via the :mod:`f5-common-python` library the typical mode is 50 | "full_uri" since that library binds uris to Python objects. 51 | 52 | Available functions: 53 | 54 | - iCRS.{get, post, put, delete, patch}: requests.Session.VERB wrappers 55 | - decorate_HTTP_verb_method: this function preps, logs, and handles requests 56 | against the BigIP REST Server, by pre- and post- processing the above methods. 57 | 58 | """ 59 | 60 | from icontrol import __version__ as version 61 | from icontrol.authtoken import iControlRESTTokenAuth 62 | from icontrol.exceptions import iControlUnexpectedHTTPError 63 | from icontrol.exceptions import InvalidBigIP_ICRURI 64 | from icontrol.exceptions import InvalidInstanceNameOrFolder 65 | from icontrol.exceptions import InvalidPrefixCollection 66 | from icontrol.exceptions import InvalidScheme 67 | from icontrol.exceptions import InvalidSuffixCollection 68 | from icontrol.exceptions import InvalidURIComponentPart 69 | from six import iteritems 70 | 71 | import functools 72 | import logging 73 | import requests 74 | import urllib3 75 | 76 | 77 | try: 78 | import json 79 | except ImportError: 80 | import simplejson as json 81 | 82 | try: 83 | # Python 3 84 | from urllib.parse import urlsplit 85 | except ImportError: 86 | # Python 2 87 | from urlparse import urlsplit 88 | 89 | 90 | BOOLEANS_TRUE = frozenset(('y', 'yes', 'on', '1', 'true', 't', 1, 1.0, True)) 91 | BOOLEANS_FALSE = frozenset(('n', 'no', 'off', '0', 'false', 'f', 0, 0.0, False)) 92 | BOOLEANS = BOOLEANS_TRUE.union(BOOLEANS_FALSE) 93 | 94 | 95 | def _validate_icruri(base_uri): 96 | # The icr_uri should specify https, the server name/address, and the path 97 | # to the REST-or-tm management interface "/mgmt/tm/" 98 | scheme, netloc, path, _, _ = urlsplit(base_uri) 99 | if scheme != 'https': 100 | raise InvalidScheme(scheme) 101 | 102 | if path.startswith('/mgmt/tm/'): 103 | # Most of the time this is BIG-IP 104 | sub_path = path[9:] 105 | elif path.startswith('/mgmt/cm/'): 106 | # This can also be in iWorkflow or BIG-IQ 107 | sub_path = path[9:] 108 | elif path.startswith('/mgmt/ap/'): 109 | # This can also be in BIG-IQ 110 | sub_path = path[9:] 111 | elif path.startswith('/mgmt/shared/'): 112 | # This can be iWorkflow or BIG-IQ 113 | sub_path = path[13:] 114 | else: 115 | error_message = "The path must start with either '/mgmt/tm/'," \ 116 | "'/mgmt/cm/', or '/mgmt/shared/'! But it's:" \ 117 | " '%s'" % path 118 | raise InvalidBigIP_ICRURI(error_message) 119 | return _validate_prefix_collections(sub_path) 120 | 121 | 122 | def _validate_prefix_collections(prefix_collections): 123 | # The prefix collections are everything in the URI after /mgmt/tm/ and 124 | # before the 'partition' It must not start with '/' because it's relative 125 | # to the /mgmt/tm REST management path, and it must end with '/' since the 126 | # subequent components expect to be addressed relative to it. 127 | # Additionally the first '/' delimited component of the prefix collection 128 | # must be an "organizing collection". See the REST users guide: 129 | # https://devcentral.f5.com/d/icontrol-rest-user-guide-version-1150 130 | if not prefix_collections.endswith('/'): 131 | error_message =\ 132 | "prefix_collections path element must end with '/', but it's: %s"\ 133 | % prefix_collections 134 | raise InvalidPrefixCollection(error_message) 135 | return True 136 | 137 | 138 | def _validate_name_partition_subpath(element): 139 | # '/' and '~' are illegal characters in most cases, however there are 140 | # few exceptions (GTM Regions endpoint being one of them where the 141 | # validation of name should not apply. 142 | if element == '': 143 | return True 144 | if '~' in element: 145 | error_message =\ 146 | "instance names and partitions cannot contain '~', but it's: %s"\ 147 | % element 148 | raise InvalidInstanceNameOrFolder(error_message) 149 | elif '/' in element: 150 | error_message =\ 151 | "instance names and partitions cannot contain '/', but it's: %s"\ 152 | % element 153 | raise InvalidInstanceNameOrFolder(error_message) 154 | return True 155 | 156 | 157 | def _validate_suffix_collections(suffix_collections): 158 | # These collections must start with '/' since they may come after a name 159 | # and/or partition and I do not know whether '~partition~name/' is a legal 160 | # ending for a URI. 161 | # The suffix must not endwith '/' as it is the last component that can 162 | # be appended to the URI path. 163 | if not suffix_collections.startswith('/'): 164 | error_message =\ 165 | "suffix_collections path element must start with '/', but" \ 166 | " it's: %s" % suffix_collections 167 | raise InvalidSuffixCollection(error_message) 168 | if suffix_collections.endswith('/'): 169 | error_message =\ 170 | "suffix_collections path element must not end with '/', but" \ 171 | " it's: %s" % suffix_collections 172 | raise InvalidSuffixCollection(error_message) 173 | return True 174 | 175 | 176 | def _validate_uri_parts( 177 | base_uri, partition, name, sub_path, suffix_collections, 178 | **kwargs): 179 | # Apply the above validators to the correct components. 180 | _validate_icruri(base_uri) 181 | _validate_name_partition_subpath(partition) 182 | if not kwargs.get('transform_name', False): 183 | _validate_name_partition_subpath(name) 184 | if not kwargs.get('transform_subpath', False): 185 | _validate_name_partition_subpath(sub_path) 186 | if suffix_collections: 187 | _validate_suffix_collections(suffix_collections) 188 | return True 189 | 190 | 191 | def generate_bigip_uri(base_uri, partition, name, sub_path, suffix, **kwargs): 192 | '''(str, str, str) --> str 193 | 194 | This function checks the supplied elements to see if each conforms to 195 | the specification for the appropriate part of the URI. These validations 196 | are conducted by the helper function _validate_uri_parts. 197 | After validation the parts are assembled into a valid BigIP REST URI 198 | string which is then submitted with appropriate metadata. 199 | 200 | >>> generate_bigip_uri('https://0.0.0.0/mgmt/tm/ltm/nat/', \ 201 | 'CUSTOMER1', 'nat52', params={'a':1}) 202 | 'https://0.0.0.0/mgmt/tm/ltm/nat/~CUSTOMER1~nat52' 203 | >>> generate_bigip_uri('https://0.0.0.0/mgmt/tm/ltm/nat/', \ 204 | 'CUSTOMER1', 'nat52', params={'a':1}, suffix='/wacky') 205 | 'https://0.0.0.0/mgmt/tm/ltm/nat/~CUSTOMER1~nat52/wacky' 206 | >>> generate_bigip_uri('https://0.0.0.0/mgmt/tm/ltm/nat/', '', '', \ 207 | params={'a':1}, suffix='/thwocky') 208 | 'https://0.0.0.0/mgmt/tm/ltm/nat/thwocky' 209 | 210 | ::Warning: There are cases where '/' and '~' characters are valid in the 211 | object name or subPath. This is indicated by passing the 'transform_name' or 'transform_subpath' boolean 212 | respectively as True. By default this is set to False. 213 | ''' 214 | 215 | _validate_uri_parts(base_uri, partition, name, sub_path, suffix, 216 | **kwargs) 217 | 218 | if kwargs.get('transform_name', False): 219 | if name != '': 220 | name = name.replace('/', '~') 221 | if kwargs.get('transform_subpath', False): 222 | if sub_path != '': 223 | sub_path = sub_path.replace('/', '~') 224 | if partition != '': 225 | partition = '~' + partition 226 | else: 227 | if sub_path: 228 | msg = 'When giving the subPath component include partition ' \ 229 | 'as well.' 230 | raise InvalidURIComponentPart(msg) 231 | if sub_path != '' and partition != '': 232 | sub_path = '~' + sub_path 233 | if name != '' and partition != '': 234 | name = '~' + name 235 | tilded_partition_and_instance = partition + sub_path + name 236 | if suffix and not tilded_partition_and_instance: 237 | suffix = suffix.lstrip('/') 238 | 239 | REST_uri = base_uri + tilded_partition_and_instance + suffix 240 | return REST_uri 241 | 242 | 243 | def decorate_HTTP_verb_method(method): 244 | """Prepare and Post-Process HTTP VERB method for BigIP-RESTServer request. 245 | 246 | This function decorates all of the HTTP VERB methods in the 247 | iControlRESTSession class. It provides the core logic for this module. 248 | If necessary it validates and assembles a uri from parts with a call to 249 | `generate_bigip_uri`. 250 | 251 | Then it: 252 | 253 | 1. pre-logs the details of the request 254 | 2. submits the request 255 | 3. logs the response, included expected status codes 256 | 4. raises exceptions for unexpected status codes. (i.e. not doc'd as BigIP 257 | RESTServer codes.) 258 | """ 259 | @functools.wraps(method) 260 | def wrapper(self, RIC_base_uri, **kwargs): 261 | partition = kwargs.pop('partition', '') 262 | sub_path = kwargs.pop('subPath', '') 263 | suffix = kwargs.pop('suffix', '') 264 | identifier, kwargs = _unique_resource_identifier_from_kwargs(**kwargs) 265 | uri_as_parts = kwargs.pop('uri_as_parts', False) 266 | transform_name = kwargs.pop('transform_name', False) 267 | transform_subpath = kwargs.pop('transform_subpath', False) 268 | if uri_as_parts: 269 | REST_uri = generate_bigip_uri(RIC_base_uri, partition, identifier, 270 | sub_path, suffix, 271 | transform_name=transform_name, 272 | transform_subpath=transform_subpath, 273 | **kwargs) 274 | else: 275 | REST_uri = RIC_base_uri 276 | pre_message = "%s WITH uri: %s AND suffix: %s AND kwargs: %s" %\ 277 | (method.__name__, REST_uri, suffix, kwargs) 278 | 279 | logger = logging.getLogger(__name__) 280 | logger.debug(pre_message) 281 | response = method(self, REST_uri, **kwargs) 282 | post_message =\ 283 | "RESPONSE::STATUS: %s Content-Type: %s Content-Encoding:"\ 284 | " %s Text: %r" % (response.status_code, 285 | response.headers.get('Content-Type', None), 286 | response.headers.get('Content-Encoding', None), 287 | response.text) 288 | logger.debug(post_message) 289 | if response.status_code not in range(200, 207): 290 | error_message = '%s Unexpected Error: %s for uri: %s Text: %r' %\ 291 | (response.status_code, 292 | response.reason, 293 | response.url, 294 | response.text) 295 | raise iControlUnexpectedHTTPError(error_message, response=response) 296 | return response 297 | return wrapper 298 | 299 | 300 | def _unique_resource_identifier_from_kwargs(**kwargs): 301 | """Chooses an identifier given different choices 302 | 303 | The unique identifier in BIG-IP's REST API at the time of this writing 304 | is called 'name'. This is in contrast to the unique identifier that is 305 | used by iWorkflow and BIG-IQ which at some times is 'name' and other 306 | times is 'uuid'. 307 | 308 | For example, in iWorkflow, there consider this URI 309 | 310 | * https://10.2.2.3/mgmt/cm/cloud/tenants/{0}/services/iapp 311 | 312 | Then consider this iWorkflow URI 313 | 314 | * https://localhost/mgmt/cm/cloud/connectors/local/{0} 315 | 316 | In the first example, the identifier, {0}, is what we would normally 317 | consider a name. For example, "tenant1". In the second example though, 318 | the value is expected to be what we would normally consider to be a 319 | UUID. For example, '244bd478-374e-4eb2-8c73-6e46d7112604'. 320 | 321 | This method only tries to rectify the problem of which to use. 322 | 323 | I believe there might be some change that the two can appear together, 324 | although I have not yet experienced it. If it is possible, I believe it 325 | would happen in BIG-IQ/iWorkflow land where the UUID and Name both have 326 | significance. That's why I deliberately prefer the UUID when it exists 327 | in the parameters sent to the URL. 328 | 329 | :param kwargs: 330 | :return: 331 | """ 332 | name = kwargs.pop('name', '') 333 | uuid = kwargs.pop('uuid', '') 334 | id = kwargs.pop('id', '') 335 | if uuid: 336 | return uuid, kwargs 337 | elif id: 338 | # Used for /mgmt/cm/system/authn/providers/tmos on BIG-IP 339 | return id, kwargs 340 | else: 341 | return name, kwargs 342 | 343 | 344 | class iControlRESTSession(object): 345 | """Represents a :class:`requests.Session` that communicates with a BigIP. 346 | 347 | Instantiate one of these when you want to communicate with a BigIP-REST 348 | Server, it will handle BigIP-specific details of the uri's. In the 349 | f5-common-python library, an :class:`iControlRESTSession` is instantiated 350 | during BigIP instantiation and associated with it as an attribute of the 351 | BigIP (a compositional vs. inheritable association). 352 | 353 | Objects instantiated from this class provide an HTTP 1.1 style session, via 354 | the :class:`requests.Session` object, and HTTP-methods that are specialized 355 | to the BigIP-RESTServer interface. 356 | 357 | Pass ``token=True`` in ``**kwargs`` to use token-based authentication. 358 | This is required for users that do not have the Administrator role on 359 | BigIP. 360 | """ 361 | def __init__(self, username, password, **kwargs): 362 | """Instantiation associated with requests.Session via composition. 363 | 364 | All transactions are Trust On First Use (TOFU) to the BigIP device, 365 | since no PKI exists for this purpose in general, hence the 366 | "disable_warnings" statement. 367 | 368 | Attributes: 369 | username (str): The user to connect with. 370 | password (str): The password of the user. 371 | timeout (int): The timeout, in seconds, to wait before closing 372 | the session. 373 | token (bool|str): True or False, specifying whether to use token 374 | authentication or not. 375 | token_to_use (str): String containing the token itself to use. 376 | This is particularly useful in situations where you want to 377 | mimic the behavior of a browser insofar as storing the token 378 | in a cookie and retrieving it for use "later". This is used 379 | in situations such as automation tools to prevent token 380 | abuse on the BIG-IP. There is a limit that users may not go 381 | beyond when creating tokens and their re-use is an attempt 382 | to mitigate this scenario. 383 | user_agent (str): A string to append to the user agent header 384 | that is sent during a session. 385 | verify (str): The path to a CA bundle containing the CA 386 | certificate for SSL validation 387 | auth_provider: String specifying the specific auth provider to 388 | authenticate the username/password against. If this argument 389 | is specified, the `token` argument is ignored. This keyword 390 | implies that token based authentication is used. The strings 391 | "none" and "default" are reserved words that imply no specific 392 | auth provider is to be used; the system will default to one. 393 | On BIG-IQ systems, the value 'local' can be used to refer to 394 | local user authentication. 395 | """ 396 | 397 | # Used for holding debug information 398 | self._debug_output = [] 399 | self._debug = False 400 | 401 | verify = kwargs.pop('verify', False) 402 | timeout = kwargs.pop('timeout', 30) 403 | proxies = kwargs.pop('proxies', {}) 404 | token_auth = kwargs.pop('token', None) 405 | user_agent = kwargs.pop('user_agent', None) 406 | token_to_use = kwargs.pop('token_to_use', None) 407 | auth_provider = kwargs.pop('auth_provider', None) 408 | 409 | if kwargs: 410 | raise TypeError('Unexpected **kwargs: %r' % kwargs) 411 | 412 | try: 413 | requests.packages.urllib3.disable_warnings() 414 | except AttributeError: 415 | try: 416 | urllib3.disable_warnings(urllib3.exceptions.InsecureRequestWarning) 417 | except Exception: 418 | pass 419 | 420 | # Compose with a Session obj 421 | self.session = requests.Session() 422 | 423 | # Configure with passed parameters 424 | self.session.timeout = timeout 425 | 426 | # Configure with proxy parameters 427 | self.session.proxies = proxies 428 | 429 | # Handle token-based auth. 430 | if token_to_use: 431 | self.session.auth = iControlRESTTokenAuth('admin', 'admin', proxies=proxies) 432 | self.session.auth.token = token_to_use 433 | else: 434 | if auth_provider: 435 | self.session.auth = iControlRESTTokenAuth( 436 | username, password, auth_provider=auth_provider, verify=verify, proxies=proxies 437 | ) 438 | else: 439 | if token_auth is True: 440 | self.session.auth = iControlRESTTokenAuth( 441 | username, password, verify=verify, proxies=proxies 442 | ) 443 | elif token_auth: 444 | # Truthy but not true: non-default loginAuthProvider 445 | self.session.auth = iControlRESTTokenAuth( 446 | username, password, token_auth, verify=verify, proxies=proxies 447 | ) 448 | else: 449 | self.session.auth = (username, password) 450 | 451 | # Set state as indicated by ancestral code. 452 | self.session.verify = verify 453 | self.session.headers.update({'Content-Type': 'application/json'}) 454 | 455 | # Add a user agent for this library and any specified UA 456 | self.append_user_agent('f5-icontrol-rest-python/' + version) 457 | if user_agent: 458 | self.append_user_agent(user_agent) 459 | 460 | @property 461 | def debug(self): 462 | return self._debug 463 | 464 | @debug.setter 465 | def debug(self, value): 466 | if value in BOOLEANS: 467 | self._debug = value 468 | 469 | @property 470 | def debug_output(self): 471 | return self._debug_output 472 | 473 | @decorate_HTTP_verb_method 474 | def delete(self, uri, **kwargs): 475 | """Sends a HTTP DELETE command to the BIGIP REST Server. 476 | 477 | Use this method to send a DELETE command to the BIGIP. When calling 478 | this method with the optional arguments ``name`` and ``partition`` 479 | as part of ``**kwargs`` they will be added to the ``uri`` passed 480 | in separated by ~ to create a proper BIGIP REST API URL for objects. 481 | 482 | All other parameters passed in as ``**kwargs`` are passed directly 483 | to the :meth:`requests.Session.delete` 484 | 485 | :param uri: A HTTP URI 486 | :type uri: str 487 | :param name: The object name that will be appended to the uri 488 | :type name: str 489 | :arg partition: The partition name that will be appened to the uri 490 | :type partition: str 491 | :param \**kwargs: The :meth:`reqeusts.Session.delete` optional params 492 | """ 493 | args1 = get_request_args(kwargs) 494 | args2 = get_send_args(kwargs) 495 | if 'timeout' not in args2: 496 | args2['timeout'] = self.session.timeout 497 | req = requests.Request('DELETE', uri, **args1) 498 | prepared = self.session.prepare_request(req) 499 | if self.debug: 500 | self._debug_output.append(debug_prepared_request(prepared)) 501 | return self.session.send(prepared, **args2) 502 | 503 | @decorate_HTTP_verb_method 504 | def get(self, uri, **kwargs): 505 | """Sends a HTTP GET command to the BIGIP REST Server. 506 | 507 | Use this method to send a GET command to the BIGIP. When calling 508 | this method with the optional arguments ``name`` and ``partition`` 509 | as part of ``**kwargs`` they will be added to the ``uri`` passed 510 | in separated by ~ to create a proper BIGIP REST API URL for objects. 511 | 512 | All other parameters passed in as ``**kwargs`` are passed directly 513 | to the :meth:`requests.Session.get` 514 | 515 | :param uri: A HTTP URI 516 | :type uri: str 517 | :param name: The object name that will be appended to the uri 518 | :type name: str 519 | :arg partition: The partition name that will be appened to the uri 520 | :type partition: str 521 | :param \**kwargs: The :meth:`reqeusts.Session.get` optional params 522 | """ 523 | args1 = get_request_args(kwargs) 524 | args2 = get_send_args(kwargs) 525 | if 'timeout' not in args2: 526 | args2['timeout'] = self.session.timeout 527 | req = requests.Request('GET', uri, **args1) 528 | prepared = self.session.prepare_request(req) 529 | if self.debug: 530 | self._debug_output.append(debug_prepared_request(prepared)) 531 | return self.session.send(prepared, **args2) 532 | 533 | @decorate_HTTP_verb_method 534 | def patch(self, uri, data=None, **kwargs): 535 | """Sends a HTTP PATCH command to the BIGIP REST Server. 536 | 537 | Use this method to send a PATCH command to the BIGIP. When calling 538 | this method with the optional arguments ``name`` and ``partition`` 539 | as part of ``**kwargs`` they will be added to the ``uri`` passed 540 | in separated by ~ to create a proper BIGIP REST API URL for objects. 541 | 542 | All other parameters passed in as ``**kwargs`` are passed directly 543 | to the :meth:`requests.Session.patch` 544 | 545 | :param uri: A HTTP URI 546 | :type uri: str 547 | :param data: The data to be sent with the PATCH command 548 | :type data: str 549 | :param name: The object name that will be appended to the uri 550 | :type name: str 551 | :arg partition: The partition name that will be appened to the uri 552 | :type partition: str 553 | :param \**kwargs: The :meth:`reqeusts.Session.patch` optional params 554 | """ 555 | args1 = get_request_args(kwargs) 556 | args2 = get_send_args(kwargs) 557 | if 'timeout' not in args2: 558 | args2['timeout'] = self.session.timeout 559 | req = requests.Request('PATCH', uri, data=data, **args1) 560 | prepared = self.session.prepare_request(req) 561 | if self.debug: 562 | self._debug_output.append(debug_prepared_request(prepared)) 563 | return self.session.send(prepared, **args2) 564 | 565 | @decorate_HTTP_verb_method 566 | def post(self, uri, data=None, json=None, **kwargs): 567 | """Sends a HTTP POST command to the BIGIP REST Server. 568 | 569 | Use this method to send a POST command to the BIGIP. When calling 570 | this method with the optional arguments ``name`` and ``partition`` 571 | as part of ``**kwargs`` they will be added to the ``uri`` passed 572 | in separated by ~ to create a proper BIGIP REST API URL for objects. 573 | 574 | All other parameters passed in as ``**kwargs`` are passed directly 575 | to the :meth:`requests.Session.post` 576 | 577 | :param uri: A HTTP URI 578 | :type uri: str 579 | :param data: The data to be sent with the POST command 580 | :type data: str 581 | :param json: The JSON data to be sent with the POST command 582 | :type json: dict 583 | :param name: The object name that will be appended to the uri 584 | :type name: str 585 | :arg partition: The partition name that will be appened to the uri 586 | :type partition: str 587 | :param \**kwargs: The :meth:`reqeusts.Session.post` optional params 588 | """ 589 | args1 = get_request_args(kwargs) 590 | args2 = get_send_args(kwargs) 591 | if 'timeout' not in args2: 592 | args2['timeout'] = self.session.timeout 593 | req = requests.Request('POST', uri, data=data, json=json, **args1) 594 | prepared = self.session.prepare_request(req) 595 | if self.debug: 596 | self._debug_output.append(debug_prepared_request(prepared)) 597 | return self.session.send(prepared, **args2) 598 | 599 | @decorate_HTTP_verb_method 600 | def put(self, uri, data=None, **kwargs): 601 | """Sends a HTTP PUT command to the BIGIP REST Server. 602 | 603 | Use this method to send a PUT command to the BIGIP. When calling 604 | this method with the optional arguments ``name`` and ``partition`` 605 | as part of ``**kwargs`` they will be added to the ``uri`` passed 606 | in separated by ~ to create a proper BIGIP REST API URL for objects. 607 | 608 | All other parameters passed in as ``**kwargs`` are passed directly 609 | to the :meth:`requests.Session.put` 610 | 611 | :param uri: A HTTP URI 612 | :type uri: str 613 | :param data: The data to be sent with the PUT command 614 | :type data: str 615 | :param json: The JSON data to be sent with the PUT command 616 | :type json: dict 617 | :param name: The object name that will be appended to the uri 618 | :type name: str 619 | :arg partition: The partition name that will be appended to the uri 620 | :type partition: str 621 | :param **kwargs: The :meth:`reqeusts.Session.put` optional params 622 | """ 623 | args1 = get_request_args(kwargs) 624 | args2 = get_send_args(kwargs) 625 | if 'timeout' not in args2: 626 | args2['timeout'] = self.session.timeout 627 | req = requests.Request('PUT', uri, data=data, **args1) 628 | prepared = self.session.prepare_request(req) 629 | if self.debug: 630 | self._debug_output.append(debug_prepared_request(prepared)) 631 | return self.session.send(prepared, **args2) 632 | 633 | def append_user_agent(self, user_agent): 634 | """Append text to the User-Agent header for the request. 635 | 636 | Use this method to update the User-Agent header by appending the 637 | given string to the session's User-Agent header separated by a space. 638 | 639 | :param user_agent: A string to append to the User-Agent header 640 | :type user_agent: str 641 | """ 642 | old_ua = self.session.headers.get('User-Agent', '') 643 | ua = old_ua + ' ' + user_agent 644 | self.session.headers['User-Agent'] = ua.strip() 645 | 646 | @property 647 | def token(self): 648 | """Convenience wrapper around returning the current token 649 | 650 | Returns: 651 | result (str): The current token being sent in session headers. 652 | """ 653 | return self.session.auth.token 654 | 655 | @token.setter 656 | def token(self, value): 657 | """Convenience wrapper around overwriting the current token 658 | 659 | Useful in situations where you have an existing iControlRESTSession 660 | object which you want to set a new token on. This token could have 661 | been read from a stored value for example. 662 | """ 663 | self.session.auth.token = value 664 | 665 | 666 | def debug_prepared_request(request): 667 | result = "curl -k -X {0} {1}".format(request.method.upper(), request.url) 668 | for k, v in iteritems(request.headers): 669 | result = result + " -H '{0}: {1}'".format(k, v) 670 | if any(v == 'application/json' for k, v in iteritems(request.headers)): 671 | if request.body: 672 | kwargs = json.loads(request.body.decode('utf-8')) 673 | result = result + " -d '" + json.dumps(kwargs, sort_keys=True) + "'" 674 | return result 675 | 676 | 677 | def get_send_args(kwargs): 678 | result = [] 679 | for arg in ['stream', 'timeout', 'verify', 'cert', 'proxies']: 680 | result.append((arg, kwargs.pop(arg, None))) 681 | result = dict([(k, v) for k, v in result if v is not None]) 682 | return result 683 | 684 | 685 | def get_request_args(kwargs): 686 | result = [] 687 | for arg in ['headers', 'files', 'data', 'json', 'params', 'auth', 'cookies', 'hooks']: 688 | result.append((arg, kwargs.pop(arg, None))) 689 | result = dict([(k, v) for k, v in result if v is not None]) 690 | return result 691 | -------------------------------------------------------------------------------- /icontrol/test/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/F5Networks/f5-icontrol-rest-python/3fee4a4599e903cce1abfe232e6ffb74d7085b64/icontrol/test/__init__.py -------------------------------------------------------------------------------- /icontrol/test/functional/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/F5Networks/f5-icontrol-rest-python/3fee4a4599e903cce1abfe232e6ffb74d7085b64/icontrol/test/functional/__init__.py -------------------------------------------------------------------------------- /icontrol/test/functional/dummy-ca-cert.pem: -------------------------------------------------------------------------------- 1 | -----BEGIN CERTIFICATE----- 2 | MIIB/jCCAWegAwIBAgIJAM75/+ozh02xMA0GCSqGSIb3DQEBCwUAMBgxFjAUBgNV 3 | BAMMDUR1bW15IENBIGNlcnQwHhcNMTcwNTMwMTg0ODA0WhcNMTgwNTMwMTg0ODA0 4 | WjAYMRYwFAYDVQQDDA1EdW1teSBDQSBjZXJ0MIGfMA0GCSqGSIb3DQEBAQUAA4GN 5 | ADCBiQKBgQC3TgHeGBejjHnZXa1knjzbrFqnlIJQ0Q8PvQtgPRO/+CNlki6sRBVV 6 | IiBalUikIGEg7gNDo9iXMt4lOsBRL5NdRTjRSTzKNWf6c5HYKNJhEK+pTA46WCYH 7 | mvTK589yeP1b8ZKMn3/BJLJEPMMs0ooNF3q8NjJr1aDeqCmCoFuhPwIDAQABo1Aw 8 | TjAdBgNVHQ4EFgQUrGeCKisC4RNJbip5xGuhuE0B5c0wHwYDVR0jBBgwFoAUrGeC 9 | KisC4RNJbip5xGuhuE0B5c0wDAYDVR0TBAUwAwEB/zANBgkqhkiG9w0BAQsFAAOB 10 | gQCE4z2kMU57xCRZ4T3ABEDaI+tZnJ/MlC+oq1UqlLBG+RPjjA2dq91/6fhTMArD 11 | J+OsV+1nXFtYDWTTAagEB60XenTy2xS8m2lQXYOJjxyauLBP1AvbslyW9W6b0kuu 12 | TrtsPaM0Qj+vQ0MYTgtk+ZIhAg+e79klGnCNT7BAH8ehdw== 13 | -----END CERTIFICATE----- 14 | -------------------------------------------------------------------------------- /icontrol/test/functional/test_session.py: -------------------------------------------------------------------------------- 1 | # Copyright 2019 F5 Networks Inc. 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # 7 | # http://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | # See the License for the specific language governing permissions and 13 | # limitations under the License. 14 | 15 | '''This test suite attempts to test the things that a real BIGIP device is 16 | required for that unit testing cannot test. For example the unit tests can 17 | cover the case in which the beginning of the URL is correct up to the 18 | collection object i.e. https://192.168.1.1/mgmt/tm/ It cannot test that 19 | the collection objects that are after that are correct 20 | i.e https://192.168.1.1/mgmt/tm/boguscollection 21 | ''' 22 | 23 | from distutils.version import LooseVersion 24 | from icontrol.authtoken import iControlRESTTokenAuth 25 | from icontrol.session import iControlRESTSession 26 | from requests.exceptions import HTTPError 27 | from requests.exceptions import SSLError 28 | 29 | import os 30 | import pytest 31 | import time 32 | 33 | 34 | @pytest.fixture 35 | def modules(): 36 | result = [ 37 | 'am', 'afm', 'apm', 'asm', 'avr', 'fps', 'gtm', 'ilx', 38 | 'lc', 'ltm', 'pem', 'sam', 'swg', 'vcmp' 39 | ] 40 | return result 41 | 42 | 43 | @pytest.fixture(autouse=True) 44 | def skip_module_missing(request, modules, opt_bigip, opt_username, opt_password, opt_port): 45 | if request.node.get_marker('skip_module_missing'): 46 | marker = request.node.get_marker('skip_module_missing').args[0] 47 | if marker in modules: 48 | try: 49 | from f5.bigip import ManagementRoot 50 | except ImportError: 51 | pytest.skip('Skipping test because I cannot determine if "{0}" is not provisioned'.format(marker)) 52 | mgmt = ManagementRoot(opt_bigip, opt_username, opt_password, port=opt_port, token=True) 53 | provision = mgmt.tm.sys.provision 54 | resource = getattr(provision, marker) 55 | resource = resource.load() 56 | result = resource.attrs 57 | if str(result['level']) == 'none': 58 | pytest.skip('Skipping test because "{0}" is not provisioned'.format(marker)) 59 | 60 | 61 | nat_data = { 62 | 'name': 'foo', 63 | 'partition': 'Common', 64 | 'originatingAddress': '192.168.1.1', 65 | 'translationAddress': '192.168.2.1', 66 | } 67 | 68 | topology_data = { 69 | 'name': 'ldns: subnet 192.168.110.0/24 server: subnet 192.168.100.0/24' 70 | } 71 | 72 | iapp_templ_data = { 73 | "name": "test_templ", 74 | "partition": "Common", 75 | "actions": { 76 | "definition": 77 | { 78 | "implementation": '''tmsh::create { 79 | ltm pool /Common/test_serv.app/test_pool 80 | load-balancing-mode least-connections-node 81 | members replace-all-with {128.0.0.2:8080{address 128.0.0.2}} 82 | }''', 83 | "presentation": "" 84 | } 85 | } 86 | } 87 | 88 | 89 | iapp_serv_data = { 90 | "name": "test_serv", 91 | "partition": "Common", 92 | "template": "/Common/test_templ" 93 | } 94 | 95 | 96 | iapp_templ_data_subpath_v11 = { 97 | "name": "test_templ_subpath", 98 | "partition": "Common", 99 | "actions": { 100 | "definition": 101 | { 102 | "implementation": '''tmsh::create { net vlan v102 } 103 | tmsh::create { net self self.v102 address 192.168.1.5/24 vlan v102 } 104 | tmsh::create { gtm datacenter dc1 } 105 | tmsh::create { auth partition part1 } 106 | tmsh::cd { /part1 } 107 | tmsh::create { ltm virtual v1 destination 192.168.1.100:80 } 108 | tmsh::cd { /Common } 109 | tmsh::create { gtm server ltm11 addresses add { 192.168.1.5 } datacenter dc1 110 | virtual-servers replace-all-with { /part1/v1 { destination 192.168.1.100:80 } } } 111 | tmsh::cd { /part1 } 112 | tmsh::create { gtm pool p1 members replace-all-with { /Common/ltm11:/part1/v1 } }''', 113 | "presentation": "" 114 | } 115 | } 116 | } 117 | 118 | 119 | iapp_serv_data_subpath = { 120 | "name": "test_serv_subpath", 121 | "partition": "Common", 122 | "template": "/Common/test_templ_subpath" 123 | } 124 | 125 | 126 | @pytest.fixture 127 | def setup_subpath(request, ICR, BASE_URL): 128 | app_templ_url = BASE_URL + 'sys/application/template/' 129 | app_serv_url = BASE_URL + 'sys/application/service/' 130 | 131 | def teardown_iapp(): 132 | try: 133 | ICR.delete( 134 | app_serv_url, uri_as_parts=True, 135 | name='test_serv', partition='Common', 136 | subPath='test_serv.app') 137 | except Exception: 138 | pass 139 | 140 | try: 141 | ICR.delete( 142 | app_templ_url, uri_as_parts=True, 143 | name='test_templ', partition='Common') 144 | except Exception: 145 | pass 146 | 147 | teardown_iapp() 148 | ICR.post(app_templ_url, json=iapp_templ_data) 149 | try: 150 | ICR.post(app_serv_url, json=iapp_serv_data) 151 | except HTTPError as ex: 152 | # The creation of an iapp service does cause a 404 error in bigip 153 | # versions up to but excluding 12.0 154 | if ex.response.status_code == 404: 155 | pass 156 | request.addfinalizer(teardown_iapp) 157 | return app_serv_url 158 | 159 | 160 | @pytest.fixture 161 | def setup_subpath_alt(request, ICR, BASE_URL): 162 | app_templ_url = BASE_URL + 'sys/application/template/' 163 | app_serv_url = BASE_URL + 'sys/application/service/' 164 | 165 | def teardown_iapp(): 166 | try: 167 | ICR.delete( 168 | app_serv_url, uri_as_parts=True, 169 | name='test_serv_subpath', partition='Common', 170 | subPath='test_serv_subpath.app') 171 | except Exception: 172 | pass 173 | 174 | try: 175 | ICR.delete( 176 | app_templ_url, uri_as_parts=True, 177 | name='test_templ_subpath', partition='Common') 178 | except Exception: 179 | pass 180 | 181 | teardown_iapp() 182 | ICR.post(app_templ_url, json=iapp_templ_data_subpath_v11) 183 | try: 184 | ICR.post(app_serv_url, json=iapp_serv_data_subpath) 185 | except HTTPError as ex: 186 | # The creation of an iapp service does cause a 404 error in bigip 187 | # versions up to but excluding 12.0 188 | if ex.response.status_code == 404: 189 | pass 190 | request.addfinalizer(teardown_iapp) 191 | return app_serv_url 192 | 193 | 194 | def teardown_nat(request, icr, url, name, partition): 195 | '''Remove the nat object that we create during a test ''' 196 | def teardown(): 197 | icr.delete(url, uri_as_parts=True, name=name, partition=partition) 198 | request.addfinalizer(teardown) 199 | 200 | 201 | def teardown_topology(request, icr, url, name): 202 | """Remove the topology object that we create during a test.""" 203 | def teardown(): 204 | icr.delete(url, uri_as_parts=True, transform_name=True, name=name) 205 | request.addfinalizer(teardown) 206 | 207 | 208 | def invalid_url(func, url): 209 | '''Reusable test to make sure that we get 404 for invalid URL ''' 210 | with pytest.raises(HTTPError) as err: 211 | func(url) 212 | return (err.value.response.status_code == 404 and 213 | 'Unexpected Error: Not Found for uri: ' + url 214 | in str(err.value)) 215 | 216 | 217 | def invalid_credentials(user, password, url): 218 | '''Reusable test to make sure that we get 401 for invalid creds ''' 219 | icr = iControlRESTSession(user, password) 220 | with pytest.raises(HTTPError) as err: 221 | icr.get(url) 222 | return (err.value.response.status_code == 401 and 223 | '401 Client Error: F5 Authorization Required' in str(err.value)) 224 | 225 | 226 | def invalid_token_credentials(user, password, url): 227 | '''Reusable test to make sure that we get 401 for invalid token creds ''' 228 | icr = iControlRESTSession(user, password, token=True) 229 | with pytest.raises(HTTPError) as err: 230 | icr.get(url) 231 | return (err.value.response.status_code == 401 and 232 | 'Authentication required!' in str(err.value)) 233 | 234 | 235 | def test_get_with_subpath(setup_subpath, ICR, BASE_URL): 236 | # The iapp creates a pool. We should be able to get that pool with subPath 237 | app_serv_url = setup_subpath 238 | res = ICR.get( 239 | app_serv_url, name='test_serv', 240 | partition='Common', subPath='test_serv.app') 241 | assert res.status_code == 200 242 | pool_uri = BASE_URL + 'ltm/pool/' 243 | pool_res = ICR.get( 244 | pool_uri, name='test_pool', 245 | partition='Common', subPath='test_serv.app') 246 | assert pool_res.status_code == 200 247 | data = pool_res.json() 248 | assert data['items'][0]['subPath'] == 'test_serv.app' 249 | assert data['items'][0]['name'] == 'test_pool' 250 | 251 | 252 | @pytest.mark.skipif( 253 | LooseVersion(pytest.config.getoption('--release')) >= LooseVersion( 254 | '12.0.0'), 255 | reason='No GTM Pool type, introduced in 12.0+' 256 | ) 257 | def test_get_with_subpath_transform(setup_subpath_alt, ICR, BASE_URL): 258 | app_serv_url = setup_subpath_alt 259 | res = ICR.get( 260 | app_serv_url, name='test_serv_subpath', 261 | partition='Common', subPath='test_serv_subpath.app') 262 | assert res.status_code == 200 263 | pool_uri = BASE_URL + 'gtm/pool/~part1~p1/members/' 264 | poolmem_res = ICR.get(pool_uri, name='v1', partition='Common', subPath='ltm11:/part1') 265 | assert poolmem_res.status_code == 200 266 | data = poolmem_res.json() 267 | assert data['items'][0]['name'] == 'v1' 268 | assert data['items'][0]['subPath'] == 'ltm11:/part1' 269 | 270 | 271 | def test_get(ICR, GET_URL): 272 | '''Test a GET request to a valid url 273 | 274 | Pass: Returns a 200 with proper json 275 | ''' 276 | response = ICR.get(GET_URL) 277 | assert response.status_code == 200 278 | assert response.json() 279 | 280 | 281 | def test_get_invalid_url(ICR, FAKE_URL): 282 | '''Test a GET to an invalid URL. 283 | 284 | Pass: Returns a 404 with a proper message 285 | ''' 286 | assert invalid_url(ICR.get, FAKE_URL) 287 | 288 | 289 | def test_post(request, ICR, POST_URL): 290 | '''Test a POST request to a valid url 291 | 292 | Pass: Returns a 200 and the json object is set correctly 293 | ''' 294 | teardown_nat( 295 | request, ICR, POST_URL, nat_data['name'], nat_data['partition']) 296 | response = ICR.post(POST_URL, json=nat_data) 297 | response_data = response.json() 298 | assert response.status_code == 200 299 | assert(response_data['name'] == nat_data['name']) 300 | assert(response_data['partition'] == nat_data['partition']) 301 | assert(response_data['originatingAddress'] == 302 | nat_data['originatingAddress']) 303 | assert(response_data['translationAddress'] == 304 | nat_data['translationAddress']) 305 | 306 | 307 | def test_post_invalid_url(ICR, FAKE_URL): 308 | '''Test a POST request to an invalid url. 309 | 310 | Pass: Returns a 404 with a proper message 311 | ''' 312 | assert invalid_url(ICR.post, FAKE_URL) 313 | 314 | 315 | def test_put(request, ICR, POST_URL): 316 | '''Test a PUT request to a valid url. 317 | 318 | Pass: Returns a 200 and the json object is set correctly 319 | ''' 320 | data = {'originatingAddress': '192.168.1.50'} 321 | teardown_nat( 322 | request, ICR, POST_URL, nat_data['name'], nat_data['partition']) 323 | ICR.post(POST_URL, json=nat_data) 324 | response = ICR.put( 325 | POST_URL, 326 | name=nat_data['name'], 327 | partition=nat_data['partition'], 328 | uri_as_parts=True, 329 | json=data) 330 | response_data = response.json() 331 | assert response.status_code == 200 332 | assert response_data['originatingAddress'] == data['originatingAddress'] 333 | assert response_data['name'] == nat_data['name'] 334 | assert response_data['partition'] == nat_data['partition'] 335 | assert response_data['translationAddress'] == \ 336 | nat_data['translationAddress'] 337 | 338 | 339 | def test_put_invalid_url(ICR, FAKE_URL): 340 | '''Test a PUT request to an invalid url. 341 | 342 | Pass: Return a 404 with a proper error message 343 | ''' 344 | assert invalid_url(ICR.put, FAKE_URL) 345 | 346 | 347 | def test_patch(request, ICR, POST_URL): 348 | '''Test a PATCH request to a valid url. 349 | 350 | Pass: Returns a 200 and the json object is set correctly 351 | ''' 352 | data = {'originatingAddress': '192.168.1.50'} 353 | teardown_nat( 354 | request, ICR, POST_URL, nat_data['name'], nat_data['partition']) 355 | ICR.post(POST_URL, json=nat_data) 356 | response = ICR.patch( 357 | POST_URL, 358 | name=nat_data['name'], 359 | partition=nat_data['partition'], 360 | uri_as_parts=True, 361 | json=data) 362 | response_data = response.json() 363 | assert response.status_code == 200 364 | assert response_data['originatingAddress'] == data['originatingAddress'] 365 | assert response_data['name'] == nat_data['name'] 366 | assert response_data['partition'] == nat_data['partition'] 367 | assert response_data['translationAddress'] == \ 368 | nat_data['translationAddress'] 369 | 370 | 371 | def test_patch_invalid_url(ICR, FAKE_URL): 372 | '''Test a PATCH request to an invalid url. 373 | 374 | Pass: Return a 404 with a proper error message 375 | ''' 376 | assert invalid_url(ICR.patch, FAKE_URL) 377 | 378 | 379 | def test_delete(request, ICR, POST_URL): 380 | '''Test a DELETE request to a valid url. 381 | 382 | Pass: Return a 200 and the json is empty. Subsequent GET returns a 404 383 | error because the object is no longer found. 384 | ''' 385 | ICR.post(POST_URL, json=nat_data) 386 | response = ICR.delete( 387 | POST_URL, 388 | name=nat_data['name'], 389 | partition=nat_data['partition'], 390 | uri_as_parts=True) 391 | assert response.status_code == 200 392 | with pytest.raises(ValueError): 393 | response.json() 394 | 395 | with pytest.raises(HTTPError) as err: 396 | ICR.get( 397 | POST_URL, 398 | name=nat_data['name'], 399 | partition=nat_data['partition'], 400 | uri_as_parts=True) 401 | assert err.value.response.status_code == 404 402 | 403 | 404 | def test_delete_invalid_url(ICR, FAKE_URL): 405 | '''Test a DELETE request to an invalid url. 406 | 407 | Pass: Return a 404 with a proper error message 408 | ''' 409 | assert invalid_url(ICR.delete, FAKE_URL) 410 | 411 | 412 | def test_invalid_user(opt_password, GET_URL): 413 | '''Test login with an invalid username and valid password 414 | 415 | Pass: Returns 401 with authorization required message 416 | ''' 417 | invalid_credentials('fakeuser', opt_password, GET_URL) 418 | 419 | 420 | def test_invalid_password(opt_username, GET_URL): 421 | '''Test login with a valid username and invalid password 422 | 423 | Pass: Returns 401 with authorization required message 424 | ''' 425 | invalid_credentials(opt_username, 'fakepassword', GET_URL) 426 | 427 | 428 | @pytest.mark.skipif( 429 | LooseVersion(pytest.config.getoption('--release')) == LooseVersion( 430 | '11.5.4'), 431 | reason='Endpoint does not exist in 11.5.4' 432 | ) 433 | def test_token_auth(opt_username, opt_password, GET_URL): 434 | icr = iControlRESTSession(opt_username, opt_password, token=True) 435 | response = icr.get(GET_URL) 436 | assert response.status_code == 200 437 | 438 | 439 | @pytest.mark.skipif( 440 | LooseVersion(pytest.config.getoption('--release')) == LooseVersion( 441 | '11.5.4'), 442 | reason='Endpoint does not exist in 11.5.4' 443 | ) 444 | def test_token_auth_twice(opt_username, opt_password, GET_URL): 445 | icr = iControlRESTSession(opt_username, opt_password, token=True) 446 | assert icr.session.auth.attempts == 0 447 | response = icr.get(GET_URL) 448 | assert response.status_code == 200 449 | assert icr.session.auth.attempts == 1 450 | response = icr.get(GET_URL) 451 | assert response.status_code == 200 452 | # This token should still be valid, so we should reuse it. 453 | assert icr.session.auth.attempts == 1 454 | 455 | 456 | @pytest.mark.skipif( 457 | LooseVersion(pytest.config.getoption('--release')) == LooseVersion( 458 | '11.5.4'), 459 | reason='Endpoint does not exist in 11.5.4' 460 | ) 461 | def test_token_auth_expired(opt_username, opt_password, GET_URL): 462 | icr = iControlRESTSession(opt_username, opt_password, token=True) 463 | assert icr.session.auth.attempts == 0 464 | response = icr.get(GET_URL) 465 | assert response.status_code == 200 466 | assert icr.session.auth.attempts == 1 467 | assert icr.session.auth.expiration >= time.time() 468 | 469 | # Artificially expire the token 470 | icr.session.auth.expiration = time.time() - 1.0 471 | 472 | # Since token is expired, we should get a new one. 473 | response = icr.get(GET_URL) 474 | assert response.status_code == 200 475 | assert icr.session.auth.attempts == 2 476 | 477 | 478 | @pytest.mark.skipif( 479 | LooseVersion(pytest.config.getoption('--release')) == LooseVersion( 480 | '11.5.4'), 481 | reason='Endpoint does not exist in 11.5.4' 482 | ) 483 | def test_token_invalid_user(opt_password, GET_URL): 484 | invalid_token_credentials('fakeuser', opt_password, GET_URL) 485 | 486 | 487 | @pytest.mark.skipif( 488 | LooseVersion(pytest.config.getoption('--release')) == LooseVersion( 489 | '11.5.4'), 490 | reason='Endpoint does not exist in 11.5.4' 491 | ) 492 | def test_token_invalid_password(opt_username, GET_URL): 493 | invalid_token_credentials(opt_username, 'fakepassword', GET_URL) 494 | 495 | 496 | # You must configure a user that has a non-admin role in a partition for 497 | # test_nonadmin tests to be effective. For instance: 498 | # 499 | # auth user bob { 500 | # description bob 501 | # encrypted-password $6$LsSnHp7J$AIJ2IC8kS.YDrrn/sH6BsxQ... 502 | # partition Common 503 | # partition-access { 504 | # bobspartition { 505 | # role operator 506 | # } 507 | # } 508 | # shell tmsh 509 | # } 510 | # 511 | # Then instantiate with --nonadmin-username=bob --nonadmin-password=changeme 512 | def test_nonadmin_token_auth(opt_nonadmin_username, opt_nonadmin_password, 513 | GET_URL): 514 | if not opt_nonadmin_username or not opt_nonadmin_password: 515 | pytest.skip("No non-admin username/password configured") 516 | icr = iControlRESTSession(opt_nonadmin_username, 517 | opt_nonadmin_password, 518 | token=True) 519 | response = icr.get(GET_URL) 520 | assert response.status_code == 200 521 | 522 | 523 | def test_nonadmin_token_auth_invalid_password(opt_nonadmin_username, 524 | GET_URL): 525 | if not opt_nonadmin_username: 526 | pytest.skip("No non-admin username/password configured") 527 | invalid_token_credentials(opt_nonadmin_username, 528 | 'fakepassword', 529 | GET_URL) 530 | 531 | 532 | def test_nonadmin_token_auth_invalid_username(opt_nonadmin_password, 533 | GET_URL): 534 | if not opt_nonadmin_password: 535 | pytest.skip("No non-admin username/password configured") 536 | invalid_token_credentials('fakeuser', 537 | opt_nonadmin_password, 538 | GET_URL) 539 | 540 | 541 | @pytest.mark.skipif( 542 | LooseVersion(pytest.config.getoption('--release')) > LooseVersion('12.0.0'), 543 | reason='Issue with spaces in the name parameter has been resolved post ' 544 | '12.1.x, therefore another test needs running' 545 | ) 546 | @pytest.mark.skip_module_missing('gtm') 547 | def test_get_special_name_11_x_12_0(request, ICR, BASE_URL): 548 | """Get the object with '/' characters in name 549 | 550 | Due to a bug name kwarg needs to have space in front of "ldns" and 551 | "server" key words when using GET method. We also need to catch and 552 | ignore 404 response to POST due to a bug with topology creation in 11.5.4 553 | """ 554 | 555 | ending = 'gtm/topology/' 556 | topology_url = BASE_URL + ending 557 | load_name = ' ldns: subnet 192.168.110.0/24 server: subnet ' \ 558 | '192.168.100.0/24' 559 | teardown_topology(request, ICR, topology_url, load_name) 560 | try: 561 | ICR.post(topology_url, json=topology_data) 562 | 563 | except HTTPError as err: 564 | if err.response.status_code == 404: 565 | pass 566 | else: 567 | raise 568 | 569 | response = ICR.get(topology_url, uri_as_parts=True, transform_name=True, 570 | name=load_name) 571 | assert response.status_code == 200 572 | data = response.json() 573 | assert data['name'] == load_name 574 | assert data['kind'] == 'tm:gtm:topology:topologystate' 575 | 576 | 577 | @pytest.mark.skipif( 578 | LooseVersion(pytest.config.getoption('--release')) < LooseVersion( 579 | '12.1.0'), 580 | reason='Issue with paces in the name parameter has been resolved in ' 581 | '12.1.x and up, any lower version will fail this test otherwise' 582 | ) 583 | @pytest.mark.skip_module_missing('gtm') 584 | def test_get_special_name_12_1(request, ICR, BASE_URL): 585 | """Get the object with '/' characters in name 586 | 587 | Since the blank space issue was fixed in 12.1.0, 588 | this test had to change. 589 | """ 590 | 591 | ending = 'gtm/topology/' 592 | topology_url = BASE_URL + ending 593 | load_name = 'ldns: subnet 192.168.110.0/24 server: subnet ' \ 594 | '192.168.100.0/24' 595 | teardown_topology(request, ICR, topology_url, load_name) 596 | try: 597 | ICR.post(topology_url, json=topology_data) 598 | 599 | except HTTPError as err: 600 | if err.response.status_code == 404: 601 | pass 602 | else: 603 | raise 604 | 605 | response = ICR.get(topology_url, uri_as_parts=True, transform_name=True, 606 | name=load_name) 607 | assert response.status_code == 200 608 | data = response.json() 609 | assert data['name'] == load_name 610 | assert data['kind'] == 'tm:gtm:topology:topologystate' 611 | 612 | 613 | @pytest.mark.skipif( 614 | LooseVersion(pytest.config.getoption('--release')) < LooseVersion('12.1.0'), 615 | reason='GTM must be provisioned for this test' 616 | ) 617 | @pytest.mark.skip_module_missing('gtm') 618 | def test_delete_special_name(request, ICR, BASE_URL): 619 | """Test a DELETE request to a valid url. 620 | 621 | Pass: Return a 200 and the json is empty. Subsequent GET returns a 404 622 | error because the object is no longer found. 623 | """ 624 | ending = 'gtm/topology/' 625 | topology_url = BASE_URL + ending 626 | 627 | try: 628 | ICR.post(topology_url, json=topology_data) 629 | 630 | except HTTPError as err: 631 | if err.response.status_code == 404: 632 | pass 633 | else: 634 | raise 635 | 636 | response = ICR.delete( 637 | topology_url, 638 | name=topology_data['name'], 639 | uri_as_parts=True, 640 | transform_name=True) 641 | assert response.status_code == 200 642 | with pytest.raises(ValueError): 643 | response.json() 644 | 645 | with pytest.raises(HTTPError) as err: 646 | ICR.get( 647 | topology_url, 648 | name=topology_data['name'], 649 | uri_as_parts=True, 650 | transform_name=True) 651 | assert err.value.response.status_code == 404 652 | 653 | 654 | def test_ssl_verify(opt_username, opt_password, GET_URL, opt_ca_bundle): 655 | """Test connection with a trusted certificate""" 656 | if not opt_ca_bundle: 657 | pytest.skip("No CA bundle configured") 658 | icr = iControlRESTSession(opt_username, opt_password, 659 | token=True, verify=opt_ca_bundle) 660 | icr.get(GET_URL) 661 | 662 | 663 | def test_ssl_verify_fail(opt_username, opt_password, GET_URL): 664 | """Test connection with an untrusted certificate""" 665 | dir_path = os.path.dirname(os.path.realpath(__file__)) 666 | ca_bundle = '%s/dummy-ca-cert.pem' % dir_path 667 | icr = iControlRESTSession(opt_username, opt_password, 668 | verify=ca_bundle) 669 | with pytest.raises(SSLError) as excinfo: 670 | icr.get(GET_URL) 671 | assert 'certificate verify failed' in str(excinfo.value) 672 | 673 | 674 | def test_get_token_ssl_verify_fail(opt_username, opt_password, opt_bigip, opt_port): 675 | """Test token retrival with an untrusted certificate""" 676 | dir_path = os.path.dirname(os.path.realpath(__file__)) 677 | ca_bundle = '%s/dummy-ca-cert.pem' % dir_path 678 | icr = iControlRESTTokenAuth(opt_username, opt_password, 679 | verify=ca_bundle) 680 | with pytest.raises(SSLError) as excinfo: 681 | icr.get_new_token('{0}:{1}'.format(opt_bigip, opt_port)) 682 | assert 'certificate verify failed' in str(excinfo.value) 683 | 684 | 685 | def test_using_stashed_tokens(GET_URL, opt_bigip, opt_username, opt_password): 686 | icr1 = iControlRESTSession(opt_username, opt_password, token='tmos') 687 | icr2 = iControlRESTSession(opt_username, opt_password, token='tmos') 688 | 689 | # Trigger token creation 690 | icr1.get(GET_URL) 691 | icr2.get(GET_URL) 692 | 693 | # Ensure we have two completely different sessions here 694 | assert icr1.token != icr2.token 695 | 696 | # Ensure that both of them are valid 697 | response = icr1.get(GET_URL) 698 | assert response.status_code == 200 699 | assert response.json() 700 | response = icr2.get(GET_URL) 701 | assert response.status_code == 200 702 | assert response.json() 703 | 704 | # Overwrite one session with another. This is illustrating the behavior 705 | # one might see when loading a cookie from disk. 706 | icr1.token = icr2.token 707 | 708 | # Ensure we indeed overwrote the tokens 709 | assert icr1.token == icr2.token 710 | 711 | # Recheck to make sure that all web requests still work 712 | response = icr1.get(GET_URL) 713 | assert response.status_code == 200 714 | assert response.json() 715 | response = icr2.get(GET_URL) 716 | assert response.status_code == 200 717 | assert response.json() 718 | 719 | # Create new object with no token data 720 | icr3 = iControlRESTSession(opt_username, opt_password, token='tmos') 721 | assert icr3.token is None 722 | 723 | # Give token to new session 724 | icr3.token = icr2.token 725 | 726 | # Ensure new object can talk 727 | response = icr1.get(GET_URL) 728 | assert response.status_code == 200 729 | assert response.json() 730 | 731 | # Ensure new object did not get new token but used existing one 732 | assert icr3.token == icr2.token 733 | 734 | # Provide the token via object instantiation 735 | icr4 = iControlRESTSession( 736 | opt_username, opt_password, token='tmos', 737 | token_to_use=icr2.token 738 | ) 739 | 740 | # Ensure the token was actually given 741 | assert icr4.token == icr2.token 742 | 743 | # Ensure the provided token works 744 | response = icr4.get(GET_URL) 745 | assert response.status_code == 200 746 | assert response.json() 747 | 748 | 749 | def test_using_tmos_token(GET_URL, opt_bigip, opt_username, opt_password): 750 | icr1 = iControlRESTSession(opt_username, opt_password, token='tmos') 751 | response = icr1.get(GET_URL) 752 | assert response.status_code == 200 753 | assert response.json() 754 | 755 | 756 | def test_using_tmos_auth_provider(GET_URL, opt_bigip, opt_username, opt_password): 757 | icr1 = iControlRESTSession(opt_username, opt_password, auth_provider='tmos') 758 | response = icr1.get(GET_URL) 759 | assert response.status_code == 200 760 | assert response.json() 761 | 762 | 763 | def test_debug_tracing(request, POST_URL, GET_URL, opt_bigip, opt_username, opt_password): 764 | icr1 = iControlRESTSession(opt_username, opt_password, auth_provider='tmos') 765 | icr1.debug = True 766 | icr1.get(GET_URL) 767 | response = icr1.post(POST_URL, json=nat_data) 768 | response.json() 769 | teardown_nat(request, icr1, POST_URL, nat_data['name'], nat_data['partition']) 770 | assert len(icr1.debug_output) > 0 771 | -------------------------------------------------------------------------------- /icontrol/test/unit/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/F5Networks/f5-icontrol-rest-python/3fee4a4599e903cce1abfe232e6ffb74d7085b64/icontrol/test/unit/__init__.py -------------------------------------------------------------------------------- /icontrol/test/unit/test_session.py: -------------------------------------------------------------------------------- 1 | # Copyright 2019 F5 Networks Inc. 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # 7 | # http://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | # See the License for the specific language governing permissions and 13 | # limitations under the License. 14 | 15 | import mock 16 | import pytest 17 | import requests 18 | 19 | from icontrol import __version__ as VERSION 20 | from icontrol import session 21 | 22 | UA = 'f5-icontrol-rest-python/%s' % VERSION 23 | 24 | 25 | @pytest.fixture() 26 | def iCRS(): 27 | fake_iCRS = session.iControlRESTSession('admin', 'admin') 28 | fake_iCRS.session = mock.MagicMock() 29 | req = requests.PreparedRequest() 30 | req.prepare(method='post', url='https://0.0.0.0/mgmt/tm/root/RESTiface/') 31 | req.body = '{"foo": "bar"}' 32 | fake_iCRS.session.prepare_request.return_value = req 33 | mock_response = mock.MagicMock() 34 | mock_response.status_code = 200 35 | fake_iCRS.session.send.return_value = mock_response 36 | return fake_iCRS 37 | 38 | 39 | @pytest.fixture() 40 | def iCRSBytes(): 41 | fake_iCRS = session.iControlRESTSession('admin', 'admin') 42 | fake_iCRS.session = mock.MagicMock() 43 | req = requests.PreparedRequest() 44 | req.prepare(method='post', url='https://0.0.0.0/mgmt/tm/root/RESTiface/') 45 | req.body = b'{"foo": "bar"}' 46 | fake_iCRS.session.prepare_request.return_value = req 47 | mock_response = mock.MagicMock() 48 | mock_response.status_code = 200 49 | fake_iCRS.session.send.return_value = mock_response 50 | return fake_iCRS 51 | 52 | 53 | @pytest.fixture() 54 | def uparts(): 55 | parts_dict = {'base_uri': 'https://0.0.0.0/mgmt/tm/root/RESTiface/', 56 | 'partition': 'BIGCUSTOMER', 57 | 'name': 'foobar1', 58 | 'sub_path': '', 59 | 'suffix': '/members/m1', 60 | 'transform_name': False, 61 | 'transform_subpath': False} 62 | return parts_dict 63 | 64 | 65 | @pytest.fixture() 66 | def transform_name(): 67 | parts_dict = {'base_uri': 'https://0.0.0.0/mgmt/tm/root/RESTiface/', 68 | 'partition': 'BIGCUSTOMER', 69 | 'name': 'foobar1: 1.1.1.1/24 bar1: /Common/DC1', 70 | 'sub_path': '', 71 | 'suffix': '/members/m1', 72 | 'transform_name': True, 73 | 'transform_subpath': False} 74 | return parts_dict 75 | 76 | 77 | @pytest.fixture() 78 | def uparts_with_subpath(): 79 | parts_dict = {'base_uri': 'https://0.0.0.0/mgmt/tm/root/RESTiface/', 80 | 'partition': 'BIGCUSTOMER', 81 | 'name': 'foobar1', 82 | 'sub_path': 'sp', 83 | 'suffix': '/members/m1', 84 | 'transform_name': False, 85 | 'transform_subpath': False} 86 | return parts_dict 87 | 88 | 89 | @pytest.fixture() 90 | def transform_name_w_subpath(): 91 | parts_dict = {'base_uri': 'https://0.0.0.0/mgmt/tm/root/RESTiface/', 92 | 'partition': 'BIGCUSTOMER', 93 | 'name': 'foobar1: 1.1.1.1/24 bar1: /Common/DC1', 94 | 'sub_path': 'ltm:/sp', 95 | 'suffix': '/members/m1', 96 | 'transform_name': True, 97 | 'transform_subpath': True} 98 | return parts_dict 99 | 100 | 101 | @pytest.fixture() 102 | def uparts_shared(): 103 | parts_dict = {'base_uri': 'https://0.0.0.0/mgmt/shared/root/RESTiface/', 104 | 'partition': 'BIGCUSTOMER', 105 | 'name': 'foobar1', 106 | 'sub_path': '', 107 | 'suffix': '/members/m1'} 108 | return parts_dict 109 | 110 | 111 | @pytest.fixture() 112 | def uparts_cm(): 113 | parts_dict = {'base_uri': 'https://0.0.0.0/mgmt/cm/root/RESTiface/', 114 | 'partition': 'BIGCUSTOMER', 115 | 'name': 'foobar1', 116 | 'sub_path': '', 117 | 'suffix': '/members/m1'} 118 | return parts_dict 119 | 120 | 121 | # Test invalid args 122 | def test_iCRS_with_invalid_construction(): 123 | with pytest.raises(TypeError) as UTE: 124 | session.iControlRESTSession('admin', 'admin', what='foble') 125 | assert str(UTE.value) == "Unexpected **kwargs: {'what': 'foble'}" 126 | 127 | 128 | # Test uri component validation 129 | def test_incorrect_uri_construction_bad_scheme(uparts): 130 | uparts['base_uri'] = 'hryttps://0.0.0.0/mgmt/tm/root/RESTiface/' 131 | with pytest.raises(session.InvalidScheme) as IS: 132 | session.generate_bigip_uri(**uparts) 133 | assert str(IS.value) == 'hryttps' 134 | 135 | 136 | def test_incorrect_uri_construction_bad_mgmt_path(uparts): 137 | uparts['base_uri'] = 'https://0.0.0.0/magmt/tm/root/RESTiface' 138 | with pytest.raises(session.InvalidBigIP_ICRURI) as IR: 139 | session.generate_bigip_uri(**uparts) 140 | assert "But it's: '/magmt/tm/root/RESTiface'" in str(IR.value) 141 | 142 | 143 | def test_incorrect_uri_construction_bad_base_nonslash_last(uparts): 144 | uparts['base_uri'] = 'https://0.0.0.0/mgmt/tm/root/RESTiface' 145 | with pytest.raises(session.InvalidPrefixCollection) as IR: 146 | session.generate_bigip_uri(**uparts) 147 | test_value = "prefix_collections path element must end with '/', but" +\ 148 | " it's: root/RESTiface" 149 | assert str(IR.value) == test_value 150 | 151 | 152 | def test_incorrect_uri_construction_illegal_slash_partition_char(uparts): 153 | uparts['partition'] = 'spam/ham' 154 | with pytest.raises(session.InvalidInstanceNameOrFolder) as II: 155 | session.generate_bigip_uri(**uparts) 156 | test_value = "instance names and partitions cannot contain '/', but" +\ 157 | " it's: %s" % uparts['partition'] 158 | assert str(II.value) == test_value 159 | 160 | 161 | def test_incorrect_uri_construction_illegal_tilde_partition_char(uparts): 162 | uparts['partition'] = 'spam~ham' 163 | with pytest.raises(session.InvalidInstanceNameOrFolder) as II: 164 | session.generate_bigip_uri(**uparts) 165 | test_value = "instance names and partitions cannot contain '~', but" +\ 166 | " it's: %s" % uparts['partition'] 167 | assert str(II.value) == test_value 168 | 169 | 170 | def test_incorrect_uri_construction_illegal_suffix_nonslash_first(uparts): 171 | uparts['suffix'] = 'ham' 172 | with pytest.raises(session.InvalidSuffixCollection) as II: 173 | session.generate_bigip_uri(**uparts) 174 | test_value = "suffix_collections path element must start with '/', but " +\ 175 | "it's: %s" % uparts['suffix'] 176 | assert str(II.value) == test_value 177 | 178 | 179 | def test_incorrect_uri_construction_illegal_suffix_slash_last(uparts): 180 | uparts['suffix'] = '/ham/' 181 | with pytest.raises(session.InvalidSuffixCollection) as II: 182 | session.generate_bigip_uri(**uparts) 183 | test_value = "suffix_collections path element must not end with '/', " +\ 184 | "but it's: %s" % uparts['suffix'] 185 | assert str(II.value) == test_value 186 | 187 | 188 | # Test uri construction 189 | def test_correct_uri_construction_partitionless(uparts): 190 | uparts['partition'] = '' 191 | uri = session.generate_bigip_uri(**uparts) 192 | assert uri == 'https://0.0.0.0/mgmt/tm/root/RESTiface/foobar1/members/m1' 193 | 194 | 195 | def test_correct_uri_construction_partitionless_subpath(uparts_with_subpath): 196 | uparts_with_subpath['partition'] = '' 197 | with pytest.raises(session.InvalidURIComponentPart) as IC: 198 | session.generate_bigip_uri(**uparts_with_subpath) 199 | assert str(IC.value) == \ 200 | 'When giving the subPath component include partition as well.' 201 | 202 | 203 | def test_correct_uri_construction_nameless(uparts): 204 | uparts['name'] = '' 205 | uri = session.generate_bigip_uri(**uparts) 206 | assert uri ==\ 207 | "https://0.0.0.0/mgmt/tm/root/RESTiface/~BIGCUSTOMER/members/m1" 208 | 209 | 210 | def test_correct_uri_construction_nameless_subpath(uparts_with_subpath): 211 | uparts_with_subpath['name'] = '' 212 | uri = session.generate_bigip_uri(**uparts_with_subpath) 213 | assert uri ==\ 214 | "https://0.0.0.0/mgmt/tm/root/RESTiface/~BIGCUSTOMER~sp/members/m1" 215 | 216 | 217 | def test_correct_uri_construction_partitionless_and_nameless(uparts): 218 | uparts['partition'] = '' 219 | uparts['name'] = '' 220 | uri = session.generate_bigip_uri(**uparts) 221 | assert uri == "https://0.0.0.0/mgmt/tm/root/RESTiface/members/m1" 222 | 223 | 224 | def test_correct_uri_construction_partitionless_and_nameless_subpath( 225 | uparts_with_subpath): 226 | uparts_with_subpath['partition'] = '' 227 | uparts_with_subpath['name'] = '' 228 | with pytest.raises(session.InvalidURIComponentPart) as IC: 229 | session.generate_bigip_uri(**uparts_with_subpath) 230 | assert str(IC.value) == \ 231 | 'When giving the subPath component include partition as well.' 232 | 233 | 234 | def test_correct_uri_construction_partition_name_and_suffixless(uparts): 235 | uparts['partition'] = '' 236 | uparts['name'] = '' 237 | uparts['suffix'] = '' 238 | uri = session.generate_bigip_uri(**uparts) 239 | assert uri == "https://0.0.0.0/mgmt/tm/root/RESTiface/" 240 | 241 | 242 | def test_correct_uri_construction_partition_name_and_suffixless_subpath( 243 | uparts_with_subpath): 244 | uparts_with_subpath['partition'] = '' 245 | uparts_with_subpath['name'] = '' 246 | uparts_with_subpath['suffix'] = '' 247 | with pytest.raises(session.InvalidURIComponentPart) as IC: 248 | session.generate_bigip_uri(**uparts_with_subpath) 249 | assert str(IC.value) == \ 250 | 'When giving the subPath component include partition as well.' 251 | 252 | 253 | def test_correct_uri_construction_partitionless_and_suffixless(uparts): 254 | uparts['partition'] = '' 255 | uparts['suffix'] = '' 256 | uri = session.generate_bigip_uri(**uparts) 257 | assert uri == 'https://0.0.0.0/mgmt/tm/root/RESTiface/foobar1' 258 | 259 | 260 | def test_correct_uri_construction_partitionless_and_suffixless_subpath( 261 | uparts_with_subpath): 262 | uparts_with_subpath['partition'] = '' 263 | uparts_with_subpath['suffix'] = '' 264 | with pytest.raises(session.InvalidURIComponentPart) as IC: 265 | session.generate_bigip_uri(**uparts_with_subpath) 266 | assert str(IC.value) == \ 267 | 'When giving the subPath component include partition as well.' 268 | 269 | 270 | def test_correct_uri_construction_nameless_and_suffixless(uparts): 271 | uparts['name'] = '' 272 | uparts['suffix'] = '' 273 | uri = session.generate_bigip_uri(**uparts) 274 | assert uri == 'https://0.0.0.0/mgmt/tm/root/RESTiface/~BIGCUSTOMER' 275 | 276 | 277 | def test_correct_uri_construction_nameless_and_suffixless_subpath( 278 | uparts_with_subpath): 279 | uparts_with_subpath['name'] = '' 280 | uparts_with_subpath['suffix'] = '' 281 | uri = session.generate_bigip_uri(**uparts_with_subpath) 282 | assert uri == 'https://0.0.0.0/mgmt/tm/root/RESTiface/~BIGCUSTOMER~sp' 283 | 284 | 285 | def test_correct_uri_construction_partitionless_transform_name(transform_name): 286 | transform_name['partition'] = '' 287 | uri = session.generate_bigip_uri(**transform_name) 288 | assert uri == \ 289 | 'https://0.0.0.0/mgmt/tm/root/RESTiface/foobar1: ' \ 290 | '1.1.1.1~24 bar1: ~Common~DC1/members/m1' 291 | 292 | 293 | def test_correct_uri_transformed_partitionless_subpath( 294 | transform_name_w_subpath): 295 | transform_name_w_subpath['partition'] = '' 296 | with pytest.raises(session.InvalidURIComponentPart) as IC: 297 | session.generate_bigip_uri(**transform_name_w_subpath) 298 | assert str(IC.value) == \ 299 | 'When giving the subPath component include partition as well.' 300 | 301 | 302 | def test_correct_uri_transformed_nameless(transform_name): 303 | transform_name['name'] = '' 304 | uri = session.generate_bigip_uri(**transform_name) 305 | assert uri ==\ 306 | "https://0.0.0.0/mgmt/tm/root/RESTiface/~BIGCUSTOMER/members/m1" 307 | 308 | 309 | def test_correct_uri_transformed_nameless_subpath(transform_name_w_subpath): 310 | transform_name_w_subpath['name'] = '' 311 | uri = session.generate_bigip_uri(**transform_name_w_subpath) 312 | assert uri ==\ 313 | "https://0.0.0.0/mgmt/tm/root/RESTiface/~BIGCUSTOMER~ltm:~sp/members/m1" 314 | 315 | 316 | def test_correct_uri_transformed_partitionless_and_nameless(transform_name): 317 | transform_name['partition'] = '' 318 | transform_name['name'] = '' 319 | uri = session.generate_bigip_uri(**transform_name) 320 | assert uri == "https://0.0.0.0/mgmt/tm/root/RESTiface/members/m1" 321 | 322 | 323 | def test_correct_uri_transformed_partitionless_and_nameless_subpath( 324 | transform_name_w_subpath): 325 | transform_name_w_subpath['partition'] = '' 326 | transform_name_w_subpath['name'] = '' 327 | with pytest.raises(session.InvalidURIComponentPart) as IC: 328 | session.generate_bigip_uri(**transform_name_w_subpath) 329 | assert str(IC.value) == \ 330 | 'When giving the subPath component include partition as well.' 331 | 332 | 333 | def test_correct_uri_transformed_partition_name_and_suffixless(transform_name): 334 | transform_name['partition'] = '' 335 | transform_name['name'] = '' 336 | transform_name['suffix'] = '' 337 | uri = session.generate_bigip_uri(**transform_name) 338 | assert uri == "https://0.0.0.0/mgmt/tm/root/RESTiface/" 339 | 340 | 341 | def test_correct_uri_transformed_partition_name_and_suffixless_subpath( 342 | transform_name_w_subpath): 343 | transform_name_w_subpath['partition'] = '' 344 | transform_name_w_subpath['name'] = '' 345 | transform_name_w_subpath['suffix'] = '' 346 | with pytest.raises(session.InvalidURIComponentPart) as IC: 347 | session.generate_bigip_uri(**transform_name_w_subpath) 348 | assert str(IC.value) == \ 349 | 'When giving the subPath component include partition as well.' 350 | 351 | 352 | def test_correct_uri_transformed_partitionless_and_suffixless(transform_name): 353 | transform_name['partition'] = '' 354 | transform_name['suffix'] = '' 355 | uri = session.generate_bigip_uri(**transform_name) 356 | assert uri == \ 357 | 'https://0.0.0.0/mgmt/tm/root/RESTiface/foobar1: ' \ 358 | '1.1.1.1~24 bar1: ~Common~DC1' 359 | 360 | 361 | def test_correct_uri_transformed_partitionless_and_suffixless_subpath( 362 | transform_name_w_subpath): 363 | transform_name_w_subpath['partition'] = '' 364 | transform_name_w_subpath['suffix'] = '' 365 | with pytest.raises(session.InvalidURIComponentPart) as IC: 366 | session.generate_bigip_uri(**transform_name_w_subpath) 367 | assert str(IC.value) == \ 368 | 'When giving the subPath component include partition as well.' 369 | 370 | 371 | def test_correct_uri_transformed_nameless_and_suffixless(transform_name): 372 | transform_name['name'] = '' 373 | transform_name['suffix'] = '' 374 | uri = session.generate_bigip_uri(**transform_name) 375 | assert uri == 'https://0.0.0.0/mgmt/tm/root/RESTiface/~BIGCUSTOMER' 376 | 377 | 378 | def test_correct_uri_transformed_nameless_and_suffixless_subpath( 379 | transform_name_w_subpath): 380 | transform_name_w_subpath['name'] = '' 381 | transform_name_w_subpath['suffix'] = '' 382 | uri = session.generate_bigip_uri(**transform_name_w_subpath) 383 | assert uri == 'https://0.0.0.0/mgmt/tm/root/RESTiface/~BIGCUSTOMER~ltm:~sp' 384 | 385 | 386 | def test_correct_uri_construction_mgmt_shared(uparts_shared): 387 | uparts_shared['name'] = '' 388 | uparts_shared['suffix'] = '' 389 | uri = session.generate_bigip_uri(**uparts_shared) 390 | assert uri == 'https://0.0.0.0/mgmt/shared/root/RESTiface/~BIGCUSTOMER' 391 | 392 | 393 | def test_correct_uri_construction_mgmt_cm(uparts_cm): 394 | uparts_cm['name'] = '' 395 | uparts_cm['suffix'] = '' 396 | uri = session.generate_bigip_uri(**uparts_cm) 397 | assert uri == 'https://0.0.0.0/mgmt/cm/root/RESTiface/~BIGCUSTOMER' 398 | 399 | 400 | # Test exception handling 401 | def test_wrapped_delete_success(iCRS, uparts): 402 | iCRS.delete(uparts['base_uri'], partition='AFN', name='AIN', uri_as_parts=True) 403 | assert isinstance(iCRS.session.prepare_request.call_args[0][0], requests.Request) 404 | assert iCRS.session.prepare_request.call_args[0][0].url == 'https://0.0.0.0/mgmt/tm/root/RESTiface/~AFN~AIN' 405 | 406 | 407 | def test_wrapped_delete_207_fail(iCRS, uparts): 408 | iCRS.session.send.return_value.status_code = 207 409 | with pytest.raises(session.iControlUnexpectedHTTPError) as ex: 410 | iCRS.delete(uparts['base_uri'], partition='A_FOLDER_NAME', name='AN_INSTANCE_NAME') 411 | assert str(ex.value).startswith('207 Unexpected Error: ') 412 | 413 | 414 | def test_wrapped_get_success(iCRS, uparts): 415 | iCRS.get(uparts['base_uri'], partition='AFN', name='AIN', uri_as_parts=True) 416 | assert isinstance(iCRS.session.prepare_request.call_args[0][0], requests.Request) 417 | assert iCRS.session.prepare_request.call_args[0][0].url == 'https://0.0.0.0/mgmt/tm/root/RESTiface/~AFN~AIN' 418 | 419 | 420 | def test_wrapped_get_success_with_suffix(iCRS, uparts): 421 | iCRS.get(uparts['base_uri'], partition='AFN', name='AIN', suffix=uparts['suffix'], uri_as_parts=True) 422 | assert isinstance(iCRS.session.prepare_request.call_args[0][0], requests.Request) 423 | assert iCRS.session.prepare_request.call_args[0][0].url == 'https://0.0.0.0/mgmt/tm/root/RESTiface/~AFN~AIN/members/m1' 424 | 425 | 426 | def test_wrapped_get_207_fail(iCRS, uparts): 427 | iCRS.session.send.return_value.status_code = 207 428 | with pytest.raises(session.iControlUnexpectedHTTPError) as ex: 429 | iCRS.get(uparts['base_uri'], partition='A_FOLDER_NAME', name='AN_INSTANCE_NAME') 430 | assert str(ex.value).startswith('207 Unexpected Error: ') 431 | 432 | 433 | def test_wrapped_patch_success(iCRS, uparts): 434 | iCRS.patch(uparts['base_uri'], partition='AFN', name='AIN', uri_as_parts=True) 435 | assert isinstance(iCRS.session.prepare_request.call_args[0][0], requests.Request) 436 | assert iCRS.session.prepare_request.call_args[0][0].url == 'https://0.0.0.0/mgmt/tm/root/RESTiface/~AFN~AIN' 437 | assert iCRS.session.prepare_request.call_args[0][0].data == [] 438 | 439 | 440 | def test_wrapped_patch_207_fail(iCRS, uparts): 441 | iCRS.session.send.return_value.status_code = 207 442 | with pytest.raises(session.iControlUnexpectedHTTPError) as ex: 443 | iCRS.patch(uparts['base_uri'], partition='A_FOLDER_NAME', name='AN_INSTANCE_NAME') 444 | assert str(ex.value).startswith('207 Unexpected Error: ') 445 | 446 | 447 | def test_wrapped_put_207_fail(iCRS, uparts): 448 | iCRS.session.send.return_value.status_code = 207 449 | with pytest.raises(session.iControlUnexpectedHTTPError) as ex: 450 | iCRS.put(uparts['base_uri'], partition='A_FOLDER_NAME', name='AN_INSTANCE_NAME') 451 | assert str(ex.value).startswith('207 Unexpected Error: ') 452 | 453 | 454 | def test_wrapped_post_207_fail(iCRS, uparts): 455 | iCRS.session.send.return_value.status_code = 207 456 | with pytest.raises(session.iControlUnexpectedHTTPError) as ex: 457 | iCRS.post(uparts['base_uri'], partition='A_FOLDER_NAME', name='AN_INSTANCE_NAME') 458 | assert str(ex.value).startswith('207 Unexpected Error: ') 459 | 460 | 461 | def test_wrapped_post_success(iCRS, uparts): 462 | iCRS.post(uparts['base_uri'], partition='AFN', name='AIN', uri_as_parts=True) 463 | assert isinstance(iCRS.session.prepare_request.call_args[0][0], requests.Request) 464 | assert iCRS.session.prepare_request.call_args[0][0].url == 'https://0.0.0.0/mgmt/tm/root/RESTiface/~AFN~AIN' 465 | assert iCRS.session.prepare_request.call_args[0][0].data == [] 466 | assert iCRS.session.prepare_request.call_args[0][0].json is None 467 | 468 | 469 | def test_wrapped_post_success_with_data(iCRS, uparts): 470 | iCRS.post(uparts['base_uri'], partition='AFN', name='AIN', data={'a': 1}, uri_as_parts=True) 471 | assert isinstance(iCRS.session.prepare_request.call_args[0][0], requests.Request) 472 | assert iCRS.session.prepare_request.call_args[0][0].url == 'https://0.0.0.0/mgmt/tm/root/RESTiface/~AFN~AIN' 473 | assert iCRS.session.prepare_request.call_args[0][0].data == {'a': 1} 474 | assert iCRS.session.prepare_request.call_args[0][0].json is None 475 | 476 | 477 | def test_wrapped_post_success_with_json(iCRS, uparts): 478 | iCRS.post(uparts['base_uri'], partition='AFN', name='AIN', json='{"a": 1}', uri_as_parts=True) 479 | assert isinstance(iCRS.session.prepare_request.call_args[0][0], requests.Request) 480 | assert iCRS.session.prepare_request.call_args[0][0].url == 'https://0.0.0.0/mgmt/tm/root/RESTiface/~AFN~AIN' 481 | assert iCRS.session.prepare_request.call_args[0][0].data == [] 482 | assert iCRS.session.prepare_request.call_args[0][0].json == '{"a": 1}' 483 | 484 | 485 | def test_wrapped_post_success_with_json_and_data(iCRS, uparts): 486 | iCRS.post(uparts['base_uri'], partition='AFN', name='AIN', data={'a': 1}, json='{"a": 1}', uri_as_parts=True) 487 | assert isinstance(iCRS.session.prepare_request.call_args[0][0], requests.Request) 488 | assert iCRS.session.prepare_request.call_args[0][0].url == 'https://0.0.0.0/mgmt/tm/root/RESTiface/~AFN~AIN' 489 | assert iCRS.session.prepare_request.call_args[0][0].data == {'a': 1} 490 | assert iCRS.session.prepare_request.call_args[0][0].json == '{"a": 1}' 491 | 492 | 493 | def test_wrapped_post_success_with_json_and_data_bytestring(iCRSBytes, uparts): 494 | iCRSBytes.post(uparts['base_uri'], partition='AFN', name='AIN', data={'a': 1}, json='{"a": 1}', uri_as_parts=True) 495 | assert isinstance(iCRSBytes.session.prepare_request.call_args[0][0], requests.Request) 496 | assert iCRSBytes.session.prepare_request.call_args[0][0].url == 'https://0.0.0.0/mgmt/tm/root/RESTiface/~AFN~AIN' 497 | assert iCRSBytes.session.prepare_request.call_args[0][0].data == {'a': 1} 498 | assert iCRSBytes.session.prepare_request.call_args[0][0].json == '{"a": 1}' 499 | 500 | 501 | def test_wrapped_put_success(iCRS, uparts): 502 | iCRS.put(uparts['base_uri'], partition='AFN', name='AIN', uri_as_parts=True) 503 | assert isinstance(iCRS.session.prepare_request.call_args[0][0], requests.Request) 504 | assert iCRS.session.prepare_request.call_args[0][0].url == 'https://0.0.0.0/mgmt/tm/root/RESTiface/~AFN~AIN' 505 | assert iCRS.session.prepare_request.call_args[0][0].data == [] 506 | 507 | 508 | def test_wrapped_put_success_with_data(iCRS, uparts): 509 | iCRS.put(uparts['base_uri'], partition='AFN', name='AIN', data={'b': 2}, uri_as_parts=True) 510 | assert isinstance(iCRS.session.prepare_request.call_args[0][0], requests.Request) 511 | assert iCRS.session.prepare_request.call_args[0][0].url == 'https://0.0.0.0/mgmt/tm/root/RESTiface/~AFN~AIN' 512 | assert iCRS.session.prepare_request.call_args[0][0].data == {'b': 2} 513 | 514 | 515 | def test___init__user_agent(): 516 | icrs = session.iControlRESTSession('admin', 'admin') 517 | assert UA in icrs.session.headers['User-Agent'] 518 | 519 | 520 | def test__append_user_agent(): 521 | icrs = session.iControlRESTSession('admin', 'admin') 522 | icrs.append_user_agent('test-user-agent/1.1.1') 523 | assert icrs.session.headers['User-Agent'].endswith('test-user-agent/1.1.1') 524 | assert UA in icrs.session.headers['User-Agent'] 525 | 526 | 527 | def test_append_user_agent_empty_start(): 528 | icrs = session.iControlRESTSession('admin', 'admin') 529 | icrs.session.headers['User-Agent'] = '' 530 | icrs.append_user_agent('test-agent') 531 | assert icrs.session.headers['User-Agent'] == 'test-agent' 532 | 533 | 534 | def test___init__with_additional_user_agent(): 535 | icrs = session.iControlRESTSession( 536 | 'admin', 537 | 'admin', 538 | user_agent='test-agent/1.2.3' 539 | ) 540 | assert icrs.session.headers['User-Agent'].endswith('test-agent/1.2.3') 541 | assert 'f5-icontrol-rest-python' in icrs.session.headers['User-Agent'] 542 | 543 | 544 | def test__init__without_verify(): 545 | icrs = session.iControlRESTSession('test_name', 'test_pw', token=True) 546 | assert icrs.session.verify is False 547 | assert icrs.session.auth.verify is False 548 | 549 | 550 | def test__init__with_verify(): 551 | icrs = session.iControlRESTSession( 552 | 'test_name', 'test_pw', token=True, verify='/path/to/cert' 553 | ) 554 | assert icrs.session.verify is '/path/to/cert' 555 | assert icrs.session.auth.verify is '/path/to/cert' 556 | -------------------------------------------------------------------------------- /requirements.docs.txt: -------------------------------------------------------------------------------- 1 | -e . 2 | 3 | sphinx >= 1.3.4 4 | -------------------------------------------------------------------------------- /requirements.test.txt: -------------------------------------------------------------------------------- 1 | # Install setup.py install_requires packages 2 | -e . 3 | 4 | # Test Requirements 5 | flake8==2.6.2 6 | pep8==1.7.0 7 | pyflakes==1.2.3 8 | mccabe==0.5.2 9 | mock==1.3.0 10 | pytest==2.9.1 11 | pytest-cov==2.2.1 12 | git+https://github.com/F5Networks/pytest-symbols.git 13 | python-coveralls==2.7.0 14 | pyOpenSSL>=17.5.0 15 | requests-mock==1.1.0 16 | tox 17 | six 18 | q 19 | -------------------------------------------------------------------------------- /setup.cfg: -------------------------------------------------------------------------------- 1 | [bdist_rpm] 2 | requires = python-six >= 1.9.0 3 | python-six < 2.0.0 4 | python-requests >= 2.5.0 5 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | 3 | # Copyright 2015-2016 F5 Networks Inc. 4 | # 5 | # Licensed under the Apache License, Version 2.0 (the "License"); 6 | # you may not use this file except in compliance with the License. 7 | # You may obtain a copy of the License at 8 | # 9 | # http://www.apache.org/licenses/LICENSE-2.0 10 | # 11 | # Unless required by applicable law or agreed to in writing, software 12 | # distributed under the License is distributed on an "AS IS" BASIS, 13 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | # See the License for the specific language governing permissions and 15 | # limitations under the License. 16 | # 17 | 18 | from setuptools import setup 19 | 20 | import icontrol 21 | 22 | 23 | setup(name='f5-icontrol-rest', 24 | description='F5 BIG-IP iControl REST API client', 25 | license='Apache License, Version 2.0', 26 | version=icontrol.__version__, 27 | author='F5 Networks', 28 | author_email='f5-icontrol-rest-python@f5.com', 29 | url='https://github.com/F5Networks/f5-icontrol-rest-python', 30 | keywords=['F5', 'icontrol', 'rest', 'api', 'bigip'], 31 | install_requires=['requests >= 2.5.0, < 3'], 32 | py_modules=[ 33 | 'icontrol.session', 34 | ], 35 | packages=['icontrol'], 36 | classifiers=[ 37 | 'Development Status :: 5 - Production/Stable', 38 | 'License :: OSI Approved :: Apache Software License', 39 | 'Operating System :: OS Independent', 40 | 'Programming Language :: Python', 41 | 'Intended Audience :: System Administrators', 42 | ] 43 | ) 44 | -------------------------------------------------------------------------------- /tox.ini: -------------------------------------------------------------------------------- 1 | [tox] 2 | envlist = 3 | py{26,27,33,34,35}-bip-v{11.5.4,11.6.0,11.6.1,12.0.0,12.1.0,12.1.1,12.1.2,13.0.0,13.1.0} 4 | py{26,27,33,34,35}-unit 5 | default-unit 6 | default-flake 7 | flake 8 | 9 | [testenv] 10 | basepython = 11 | py26: python2.6 12 | py27: python2.7 13 | py33: python3.3 14 | py34: python3.4 15 | py35: python3.5 16 | default-unit: python 17 | default-flake: python 18 | flake: python 19 | deps = 20 | -rrequirements.test.txt 21 | commands = 22 | py{26,27,35}-bip-v11.5.4: py.test --bigip localhost --port 10443 -s -vv --release 11.5.4 {posargs} 23 | py{26,27,35}-bip-v11.6.0: py.test --bigip localhost --port 10443 -s -vv --release 11.6.0 {posargs} 24 | py{26,27,35}-bip-v11.6.1: py.test --bigip localhost --port 10443 -s -vv --release 11.6.1 {posargs} 25 | py{26,27,35}-bip-v12.0.0: py.test --bigip localhost --port 10443 -s -vv --release 12.0.0 {posargs} 26 | py{26,27,35}-bip-v12.1.0: py.test --bigip localhost --port 10443 -s -vv --release 12.1.0 {posargs} 27 | py{26,27,35}-bip-v12.1.1: py.test --bigip localhost --port 10443 -s -vv --release 12.1.1 {posargs} 28 | py{26,27,35}-bip-v12.1.2: py.test --bigip localhost --port 10443 -s -vv --release 12.1.2 {posargs} 29 | py{26,27,35}-bip-v13.0.0: py.test --bigip localhost --port 10443 -s -vv --release 13.0.0 {posargs} 30 | py{26,27,35}-bip-v13.1.0: py.test --bigip localhost --port 10443 -s -vv --release 13.1.0 {posargs} 31 | py{26,27,35}-unit: py.test -k "not /functional/" -vv --cov {posargs} 32 | default-unit: py.test -k "not /functional/" -vv --cov {posargs} 33 | default-flake: flake8 34 | flake: flake8 35 | 36 | [flake8] 37 | exclude = docs/conf.py,docs/userguide/code_example.py,docs/conf.py,.tox,.git,__pycache__,build,*.pyc,docs,devtools,*.tmpl 38 | ignore = E226,W503,E123,H304 39 | max-line-length = 160 40 | 41 | [pycodestyle] 42 | exclude = docs/conf.py,docs/userguide/code_example.py,docs/conf.py,.tox,.git,__pycache__,build,*.pyc,docs,devtools,*.tmpl 43 | ignore = E226,W503,E123,H304 44 | max-line-length = 160 45 | --------------------------------------------------------------------------------