├── .gitignore ├── Dockerfile ├── LICENSE ├── MANIFEST.in ├── Makefile ├── README.md ├── cf_api ├── __init__.py ├── __version__.py ├── deploy_blue_green.py ├── deploy_manifest.py ├── deploy_service.py ├── deploy_space.py ├── dropsonde_util.py ├── exceptions.py ├── logs_util.py ├── pb2dict.py ├── routes_util.py └── ssh_util.py ├── docs ├── .gitignore ├── Makefile ├── api.rst ├── conf.py ├── examples.rst ├── index.rst └── manifest.yml ├── examples ├── authenticate_with_authorization_code.py ├── authenticate_with_client_credentials.py ├── authenticate_with_refresh_token.py ├── cf_apps_core.py ├── cf_apps_simple.py ├── cf_create_service.py ├── cf_login.py ├── cf_logs_core.py ├── cf_logs_simple.py ├── cf_orgs.py ├── cf_push_blue_green.py ├── cf_push_core.py ├── cf_push_simple.py ├── cf_spaces.py ├── extend_cloud_controller.py └── find_app_by_route.py ├── requirements-dev.txt ├── requirements.txt ├── setup.py ├── test ├── mock_cc.py ├── test_all.py └── test_cf_api.py └── version.txt /.gitignore: -------------------------------------------------------------------------------- 1 | .idea/ 2 | dist/ 3 | build/ 4 | src/ 5 | env/ 6 | env3/ 7 | __pycache__/ 8 | .DS_Store 9 | *.pyc 10 | *.egg-info 11 | *.swp 12 | *.swo 13 | .env 14 | tags 15 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM alpine:3.11 2 | COPY requirements.txt /requirements.txt 3 | COPY requirements-dev.txt /requirements-dev.txt 4 | RUN apk --update --no-cache add \ 5 | openssl \ 6 | ca-certificates \ 7 | python3 \ 8 | python2 \ 9 | git \ 10 | make \ 11 | musl-dev \ 12 | linux-headers \ 13 | python3-dev \ 14 | python2-dev \ 15 | py2-pip \ 16 | openssl-dev \ 17 | libffi-dev \ 18 | gcc \ 19 | g++ && \ 20 | pip2 --no-cache-dir install -r /requirements.txt && \ 21 | pip2 --no-cache-dir install -r /requirements-dev.txt && \ 22 | python3 -m pip --no-cache-dir install -r /requirements.txt && \ 23 | python3 -m pip --no-cache-dir install -r /requirements-dev.txt && \ 24 | python3 -m pip --no-cache-dir install twine 25 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | 2 | Apache License 3 | Version 2.0, January 2004 4 | http://www.apache.org/licenses/ 5 | 6 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 7 | 8 | 1. Definitions. 9 | 10 | "License" shall mean the terms and conditions for use, reproduction, 11 | and distribution as defined by Sections 1 through 9 of this document. 12 | 13 | "Licensor" shall mean the copyright owner or entity authorized by 14 | the copyright owner that is granting the License. 15 | 16 | "Legal Entity" shall mean the union of the acting entity and all 17 | other entities that control, are controlled by, or are under common 18 | control with that entity. For the purposes of this definition, 19 | "control" means (i) the power, direct or indirect, to cause the 20 | direction or management of such entity, whether by contract or 21 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 22 | outstanding shares, or (iii) beneficial ownership of such entity. 23 | 24 | "You" (or "Your") shall mean an individual or Legal Entity 25 | exercising permissions granted by this License. 26 | 27 | "Source" form shall mean the preferred form for making modifications, 28 | including but not limited to software source code, documentation 29 | source, and configuration files. 30 | 31 | "Object" form shall mean any form resulting from mechanical 32 | transformation or translation of a Source form, including but 33 | not limited to compiled object code, generated documentation, 34 | and conversions to other media types. 35 | 36 | "Work" shall mean the work of authorship, whether in Source or 37 | Object form, made available under the License, as indicated by a 38 | copyright notice that is included in or attached to the work 39 | (an example is provided in the Appendix below). 40 | 41 | "Derivative Works" shall mean any work, whether in Source or Object 42 | form, that is based on (or derived from) the Work and for which the 43 | editorial revisions, annotations, elaborations, or other modifications 44 | represent, as a whole, an original work of authorship. For the purposes 45 | of this License, Derivative Works shall not include works that remain 46 | separable from, or merely link (or bind by name) to the interfaces of, 47 | the Work and Derivative Works thereof. 48 | 49 | "Contribution" shall mean any work of authorship, including 50 | the original version of the Work and any modifications or additions 51 | to that Work or Derivative Works thereof, that is intentionally 52 | submitted to Licensor for inclusion in the Work by the copyright owner 53 | or by an individual or Legal Entity authorized to submit on behalf of 54 | the copyright owner. For the purposes of this definition, "submitted" 55 | means any form of electronic, verbal, or written communication sent 56 | to the Licensor or its representatives, including but not limited to 57 | communication on electronic mailing lists, source code control systems, 58 | and issue tracking systems that are managed by, or on behalf of, the 59 | Licensor for the purpose of discussing and improving the Work, but 60 | excluding communication that is conspicuously marked or otherwise 61 | designated in writing by the copyright owner as "Not a Contribution." 62 | 63 | "Contributor" shall mean Licensor and any individual or Legal Entity 64 | on behalf of whom a Contribution has been received by Licensor and 65 | subsequently incorporated within the Work. 66 | 67 | 2. Grant of Copyright License. Subject to the terms and conditions of 68 | this License, each Contributor hereby grants to You a perpetual, 69 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 70 | copyright license to reproduce, prepare Derivative Works of, 71 | publicly display, publicly perform, sublicense, and distribute the 72 | Work and such Derivative Works in Source or Object form. 73 | 74 | 3. Grant of Patent License. Subject to the terms and conditions of 75 | this License, each Contributor hereby grants to You a perpetual, 76 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 77 | (except as stated in this section) patent license to make, have made, 78 | use, offer to sell, sell, import, and otherwise transfer the Work, 79 | where such license applies only to those patent claims licensable 80 | by such Contributor that are necessarily infringed by their 81 | Contribution(s) alone or by combination of their Contribution(s) 82 | with the Work to which such Contribution(s) was submitted. If You 83 | institute patent litigation against any entity (including a 84 | cross-claim or counterclaim in a lawsuit) alleging that the Work 85 | or a Contribution incorporated within the Work constitutes direct 86 | or contributory patent infringement, then any patent licenses 87 | granted to You under this License for that Work shall terminate 88 | as of the date such litigation is filed. 89 | 90 | 4. Redistribution. You may reproduce and distribute copies of the 91 | Work or Derivative Works thereof in any medium, with or without 92 | modifications, and in Source or Object form, provided that You 93 | meet the following conditions: 94 | 95 | (a) You must give any other recipients of the Work or 96 | Derivative Works a copy of this License; and 97 | 98 | (b) You must cause any modified files to carry prominent notices 99 | stating that You changed the files; and 100 | 101 | (c) You must retain, in the Source form of any Derivative Works 102 | that You distribute, all copyright, patent, trademark, and 103 | attribution notices from the Source form of the Work, 104 | excluding those notices that do not pertain to any part of 105 | the Derivative Works; and 106 | 107 | (d) If the Work includes a "NOTICE" text file as part of its 108 | distribution, then any Derivative Works that You distribute must 109 | include a readable copy of the attribution notices contained 110 | within such NOTICE file, excluding those notices that do not 111 | pertain to any part of the Derivative Works, in at least one 112 | of the following places: within a NOTICE text file distributed 113 | as part of the Derivative Works; within the Source form or 114 | documentation, if provided along with the Derivative Works; or, 115 | within a display generated by the Derivative Works, if and 116 | wherever such third-party notices normally appear. The contents 117 | of the NOTICE file are for informational purposes only and 118 | do not modify the License. You may add Your own attribution 119 | notices within Derivative Works that You distribute, alongside 120 | or as an addendum to the NOTICE text from the Work, provided 121 | that such additional attribution notices cannot be construed 122 | as modifying the License. 123 | 124 | You may add Your own copyright statement to Your modifications and 125 | may provide additional or different license terms and conditions 126 | for use, reproduction, or distribution of Your modifications, or 127 | for any such Derivative Works as a whole, provided Your use, 128 | reproduction, and distribution of the Work otherwise complies with 129 | the conditions stated in this License. 130 | 131 | 5. Submission of Contributions. Unless You explicitly state otherwise, 132 | any Contribution intentionally submitted for inclusion in the Work 133 | by You to the Licensor shall be under the terms and conditions of 134 | this License, without any additional terms or conditions. 135 | Notwithstanding the above, nothing herein shall supersede or modify 136 | the terms of any separate license agreement you may have executed 137 | with Licensor regarding such Contributions. 138 | 139 | 6. Trademarks. This License does not grant permission to use the trade 140 | names, trademarks, service marks, or product names of the Licensor, 141 | except as required for reasonable and customary use in describing the 142 | origin of the Work and reproducing the content of the NOTICE file. 143 | 144 | 7. Disclaimer of Warranty. Unless required by applicable law or 145 | agreed to in writing, Licensor provides the Work (and each 146 | Contributor provides its Contributions) on an "AS IS" BASIS, 147 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 148 | implied, including, without limitation, any warranties or conditions 149 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 150 | PARTICULAR PURPOSE. You are solely responsible for determining the 151 | appropriateness of using or redistributing the Work and assume any 152 | risks associated with Your exercise of permissions under this License. 153 | 154 | 8. Limitation of Liability. In no event and under no legal theory, 155 | whether in tort (including negligence), contract, or otherwise, 156 | unless required by applicable law (such as deliberate and grossly 157 | negligent acts) or agreed to in writing, shall any Contributor be 158 | liable to You for damages, including any direct, indirect, special, 159 | incidental, or consequential damages of any character arising as a 160 | result of this License or out of the use or inability to use the 161 | Work (including but not limited to damages for loss of goodwill, 162 | work stoppage, computer failure or malfunction, or any and all 163 | other commercial damages or losses), even if such Contributor 164 | has been advised of the possibility of such damages. 165 | 166 | 9. Accepting Warranty or Additional Liability. While redistributing 167 | the Work or Derivative Works thereof, You may choose to offer, 168 | and charge a fee for, acceptance of support, warranty, indemnity, 169 | or other liability obligations and/or rights consistent with this 170 | License. However, in accepting such obligations, You may act only 171 | on Your own behalf and on Your sole responsibility, not on behalf 172 | of any other Contributor, and only if You agree to indemnify, 173 | defend, and hold each Contributor harmless for any liability 174 | incurred by, or claims asserted against, such Contributor by reason 175 | of your accepting any such warranty or additional liability. 176 | 177 | END OF TERMS AND CONDITIONS 178 | 179 | APPENDIX: How to apply the Apache License to your work. 180 | 181 | To apply the Apache License to your work, attach the following 182 | boilerplate notice, with the fields enclosed by brackets "[]" 183 | replaced with your own identifying information. (Don't include 184 | the brackets!) The text should be enclosed in the appropriate 185 | comment syntax for the file format. We also recommend that a 186 | file or class name and description of purpose be included on the 187 | same "printed page" as the copyright notice for easier 188 | identification within third-party archives. 189 | 190 | Copyright [yyyy] [name of copyright owner] 191 | 192 | Licensed under the Apache License, Version 2.0 (the "License"); 193 | you may not use this file except in compliance with the License. 194 | You may obtain a copy of the License at 195 | 196 | http://www.apache.org/licenses/LICENSE-2.0 197 | 198 | Unless required by applicable law or agreed to in writing, software 199 | distributed under the License is distributed on an "AS IS" BASIS, 200 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 201 | See the License for the specific language governing permissions and 202 | limitations under the License. 203 | -------------------------------------------------------------------------------- /MANIFEST.in: -------------------------------------------------------------------------------- 1 | include requirements.txt 2 | include version.txt 3 | include README.md 4 | include LICENSE 5 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | SHELL := /bin/sh 2 | DOCKERIMAGE := python-cf-api:latest 3 | PY2BIN := python2 4 | PY3BIN := python3 5 | 6 | .PHONY: test 7 | test: 8 | make docker-run CMD='\ 9 | $(PY2BIN) -m nose2 -v && \ 10 | $(PY3BIN) -m nose2 -v' 11 | 12 | .PHONY: deploy 13 | deploy: test 14 | make docker-run CMD='\ 15 | rm -r dist/ || : && \ 16 | $(PY3BIN) setup.py sdist && \ 17 | $(PY3BIN) -m twine upload \ 18 | --verbose \ 19 | --repository-url https://upload.pypi.org/legacy/ \ 20 | --username '$(PYPI_USERNAME)' \ 21 | --password '$(PYPI_PASSWORD)' \ 22 | dist/cf_api-*' 23 | 24 | .PHONY: docker-image 25 | docker-image: 26 | docker build -t $(DOCKERIMAGE) . 27 | 28 | .PHONY: docker-run 29 | docker-run: docker-image 30 | docker run \ 31 | --rm -it \ 32 | -v $(PWD):/src \ 33 | -w /src \ 34 | $(DOCKERIMAGE) \ 35 | /bin/sh -c '$(CMD)' 36 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Python Cloud Foundry API Client 2 | 3 | This module provides a pure Python interface to the Cloud Foundry APIs. 4 | 5 | ## Installation 6 | 7 | You can install from PIP 8 | 9 | `pip install cf-api` 10 | 11 | or view it on [PyPI](https://pypi.python.org/pypi/cf_api). 12 | 13 | ## Documentation 14 | 15 | See the docs at [https://cf-api.readthedocs.io/en/latest/](https://cf-api.readthedocs.io/en/latest/) or in the [./docs](docs) directory and the [./examples](examples) directory. 16 | 17 | ## Versioning 18 | 19 | *Version 1.x* 20 | - Support both Python 2.7/3.6-3.8 21 | - Remove `cf_api.dropsonde` module in favor of the `dropsonde` module. 22 | - Add CF API version 3 support 23 | - Add `Dockerfile` example 24 | 25 | *Version 0.x* 26 | - Supports Python 2.7 27 | 28 | ## Getting Started 29 | 30 | The following examples should be enough to get you started using this library. 31 | 32 | ```python 33 | # Initializing the Cloud Controller client 34 | 35 | from getpass import getpass 36 | import cf_api 37 | import json 38 | 39 | cloud_controller = 'https://api.yourcloudfoundry.com' 40 | deploy_client_id = 'cf' 41 | deploy_client_secret = '' 42 | verify_ssl = True 43 | username = 'youser' 44 | password = getpass('Password: ').strip() 45 | 46 | cc = cf_api.new_cloud_controller( 47 | cloud_controller, 48 | client_id=deploy_client_id, 49 | client_secret=deploy_client_secret, 50 | username=username, 51 | password=password, 52 | ).set_verify_ssl(verify_ssl) 53 | 54 | 55 | # List all organizations 56 | req = cc.organizations() 57 | res = req.get() 58 | orgs = res.resources 59 | for r in orgs: 60 | print('org', r.guid, r.name) 61 | 62 | 63 | # List all spaces 64 | res = cc.spaces().get() 65 | spaces = res.resources 66 | for r in spaces: 67 | print('space', r.guid, r.name) 68 | 69 | 70 | # List all applications 71 | 72 | res = cc.apps().get() 73 | apps = res.resources 74 | for r in apps: 75 | print('app', r.guid, r.name) 76 | 77 | 78 | # Find an app by it's org/space/name 79 | 80 | org_name = 'your_org' 81 | space_name = 'your_space' 82 | app_name = 'your_app' 83 | 84 | # find your org by name 85 | res = cc.organizations().get_by_name(org_name) 86 | # you can access the first array resource using the `resource` attribute 87 | your_org = res.resource 88 | 89 | # find your space by name within your org 90 | res = cc.request(your_org.spaces_url).get_by_name(space_name) 91 | your_space = res.resource 92 | 93 | # find your app by name within your space 94 | res = cc.request(your_space.apps_url).get_by_name(app_name) 95 | your_app = res.resource 96 | print('your_app', your_app) 97 | 98 | 99 | # Find an app by it's GUID 100 | # 101 | # Note that this same pattern applies to all Cloud Controller resources 102 | # 103 | 104 | res = cc.apps(your_app.guid).get() 105 | # you can also use the `resource` attribute to access a response with a 106 | # non-array result 107 | your_same_app = res.resource 108 | print('your_same_app', your_same_app) 109 | 110 | 111 | # Find a stack by name 112 | your_stack = 'some_stack' 113 | res = cc.stacks().get_by_name(your_stack) 114 | stack = res.resource 115 | 116 | 117 | # Create an app 118 | your_buildpack = 'some_buildpack' 119 | command = 'python server.py' 120 | res = cc.apps().set_params( 121 | name=app_name, 122 | space_guid=your_space.guid, 123 | stack_guid=stack.guid, 124 | buildpack=your_buildpack, 125 | command=command, 126 | health_check_type='port', 127 | health_check_timeout=60, 128 | instances=2, 129 | memory=512, 130 | disk_quota=512 131 | ).post() 132 | print('new app', res.data) 133 | 134 | 135 | # Upload the bits for an app 136 | my_zipfile = '/tmp/app.zip' 137 | with open(my_zipfile, 'r') as f: 138 | res = cc.apps(your_app.guid, 'bits')\ 139 | .set_query(async='true')\ 140 | .add_field('resources', json.dumps([]))\ 141 | .add_file('application', 'application.zip', f, 'application/zip')\ 142 | .put() 143 | print(res.data) 144 | ``` 145 | 146 | ## Running in Docker 147 | 148 | To get start running `cf_api` in Docker, just build the provided [Dockerfile](./Dockerfile) 149 | 150 | ``` 151 | you@yourhost:~/python-cf-api$ docker build -t python-cf-api:latest . 152 | ``` 153 | 154 | and run it using the following syntax. 155 | 156 | ``` 157 | you@yourhost:~/python-cf-api$ docker run --rm -it -v $PWD:/src -w /src python-cf-api:latest python3 158 | Python 3.8.1 159 | [GCC 9.2.0] on linux 160 | Type "help", "copyright", "credits" or "license" for more information. 161 | >>> import cf_api 162 | >>> # play with it here 163 | ``` 164 | 165 | ## Using CF API version 3 166 | 167 | The following example shows how to use the Cloud Foundry version 3 API. 168 | 169 | ```python 170 | import cf_api 171 | 172 | cc = cf_api.new_cloud_controller() 173 | req = cc.v3.apps() 174 | res = req.get() 175 | print(res.resource.guid) 176 | ``` 177 | 178 | - The `cc.v3` attribute returns a `CloudController` instance that is configured 179 | to wrap requests and responses in V3 compatible classes, namely `V3CloudControllerRequest` 180 | and `V3CloudControllerResponse`. These objects work similarly to their v2 counterparts, 181 | `CloudControllerRequest` and `CloudControllerResponse`. 182 | - The `V3CloudControllerResponse` provides `resource` and `resources` which return 183 | an instance or list of instances of `V3Resource` objects which support the 184 | common API object keys such as `name`, `guid`, `space_guid`, and `org_guid`, etc. 185 | - The `cc.v3.get_all_resources()` function supports both v2 and v3 pagination. 186 | - The `cc.v3.request()` function supports _both_ relative URLs and absolute URLs, 187 | for example `/v3/apps` and `http://localhost/v3/apps`, respectively. See `request()` 188 | function documentation for more information. 189 | 190 | ## Environment Variables 191 | 192 | The library is also configurable via environment variables. 193 | 194 | | Variable | Description | 195 | | --- | --- | 196 | | `PYTHON_CF_URL` | This is the cloud controller base URL. **Do not include a trailing slash on the URL.** 197 | | `PYTHON_CF_CLIENT_ID` | This is the UAA client ID the library should use. 198 | | `PYTHON_CF_CLIENT_SECRET` | This is the UAA client secret the library should use. 199 | | `PYTHON_CF_IGNORE_SSL` | This indicates whether to verify SSL certs. Default is false. Set to `true` to ignore SSL verification. 200 | | `CF_DOCKER_PASSWORD` | This variable optionally provides the Docker user's password if a docker image is being used. This variable is not necessarily required to use a docker image. 201 | 202 | An example library usage with these variables set would look like this: 203 | 204 | ```python 205 | # env vars might be set as follows 206 | # PYTHON_CF_URL=https://api.cloudfoundry.com 207 | # PYTHON_CF_CLIENT_ID=my_client_id 208 | # PYTHON_CF_CLIENT_SECRET=my_client_secret 209 | 210 | import cf_api 211 | 212 | # no args are required when the above env vars are detected 213 | cc = cf_api.new_cloud_controller() 214 | res = cc.apps().get() 215 | # ... 216 | 217 | # the same principle applies to new_uaa() 218 | uaa = cf_api.new_uaa() 219 | # ... 220 | ``` 221 | 222 | ## Log in with Cloud Foundry Authorization Code 223 | 224 | The following functions may be used to implement login with Cloud Foundry via Authorization Codes. 225 | 226 | The function `get_openid_connect_url()` shows how to build UAA URL to which the user can be 227 | redirected in order to log in. 228 | 229 | The function `verify_code()` can be used when the user successfully logs in and UAA redirects back 230 | to redirect_uri with the `code` attached. Pass the code and original redirect_uri into this function 231 | in order to get the OAuth2 Token and to also verify the signature of the JWT. 232 | 233 | This particular example applies to OpenID Connect. 234 | 235 | ```python 236 | import cf_api 237 | 238 | cc = 'https://api.yourcloudfoundry.com' 239 | client_id = 'yourclient' 240 | client_secret = 'yoursecret' 241 | response_type = 'code' 242 | 243 | def get_openid_connect_url(redirect_uri): 244 | return cf_api\ 245 | .new_uaa(cc=cc, client_id=client_id, client_secret=client_secret, no_auth=True)\ 246 | .authorization_code_url(response_type, scope='openid', redirect_uri=redirect_uri) 247 | 248 | 249 | def verify_code(code, redirect_uri): 250 | uaa = cf_api.new_uaa(cc=cc, client_id=client_id, client_secret=client_secret, no_auth=True) 251 | res = uaa.authorization_code(code, response_type, redirect_uri) 252 | data = res.data 253 | uaa.verify_token(data['id_token'], audience=uaa.client_id) 254 | return data 255 | ``` 256 | 257 | ## Deploy an Application 258 | 259 | The `cf_api.deploy_manifest` module may be used to deploy a Cloud Foundry app. The 260 | following snippet demonstrates the usage for deploying an app. 261 | 262 | ```bash 263 | cd path/to/your/project 264 | python -m cf_api.deploy_manifest \ 265 | --cloud-controller https://api.yourcloudfoundry.com \ 266 | -u youser -o yourg -s yourspace \ 267 | -m manifest.yml -v -w 268 | # For the CLI usage of deploy_manifest, you may also set 269 | # the CF_REFRESH_TOKEN environment variable as a substitute 270 | # for collecting username and password 271 | ``` 272 | 273 | This module may also be used programmatically. 274 | 275 | ```python 276 | from __future__ import print_function 277 | import cf_api 278 | from getpass import getpass 279 | from cf_api.deploy_manifest import Deploy 280 | 281 | cc = cf_api.new_cloud_controller( 282 | 'https://api.yourcloudfoundry.com', 283 | username='youruser', 284 | password=getpass().strip(), 285 | client_id='cf', 286 | client_secret='', 287 | verify_ssl=True 288 | ) 289 | 290 | manifest_filename = 'path/to/manifest.yml' 291 | 292 | apps = Deploy.parse_manifest(manifest_filename, cc) 293 | 294 | for app in apps: 295 | app.set_debug(True) 296 | app.set_org_and_space('yourorg', 'yourspace') 297 | print (app.push()) 298 | # print (app.destroy(destroy_routes=True)) 299 | ``` 300 | 301 | ## Deploy a Service 302 | 303 | The `cf_api.deploy_service` module may be used to deploy a Cloud Foundry service to a space. The 304 | following snippet demonstrates the usage for deploying a service. 305 | 306 | ```bash 307 | cd path/to/your/project 308 | python -m cf_api.deploy_service \ 309 | --cloud-controller https://api.yourcloudfoundry.com \ 310 | -u youser -o yourg -s yourspace \ 311 | --name your-custom-service-name --service-name cf-service-type \ 312 | --service-plan cf-service-type-plan -v -w 313 | ``` 314 | 315 | This module may also be used programmatically. 316 | 317 | ```python 318 | from __future__ import print_function 319 | import cf_api 320 | from getpass import getpass 321 | from cf_api.deploy_service import DeployService 322 | 323 | cc = cf_api.new_cloud_controller( 324 | 'https://api.yourcloudfoundry.com', 325 | username='youruser', 326 | password=getpass().strip(), 327 | client_id='cf', 328 | client_secret='', 329 | verify_ssl=True 330 | ) 331 | 332 | service = DeployService(cc)\ 333 | .set_debug(True)\ 334 | .set_org_and_space('yourorg', 'yourspace') 335 | 336 | result = service.create('my-custom-db', 'database-service', 'small-database-plan') 337 | print(result) 338 | ``` 339 | 340 | ## Query a Space 341 | 342 | The `cf_api.deploy_space` module provides a convenience interface for working with Cloud Foundry 343 | spaces. The module provides read-only (i.e. GET requests only) support for the Cloud Controller API 344 | endpoints scoped to a specific space i.e. /v2/spaces//(routes|service_instances|apps). 345 | The following snippet demonstrates the usage for listing apps for in a space. 346 | 347 | ```bash 348 | cd path/to/your/project 349 | python -m cf_api.deploy_space \ 350 | --cloud-controller https://api.yourcloudfoundry.com \ 351 | -u youser -o yourg -s yourspace apps 352 | ``` 353 | 354 | This module may also be used programmatically. 355 | 356 | ```python 357 | from __future__ import print_function 358 | import cf_api 359 | from getpass import getpass 360 | from cf_api.deploy_space import Space 361 | 362 | cc = cf_api.new_cloud_controller( 363 | 'https://api.yourcloudfoundry.com', 364 | username='youruser', 365 | password=getpass().strip(), 366 | client_id='cf', 367 | client_secret='', 368 | verify_ssl=True 369 | ) 370 | 371 | space = Space(cc, org_name='yourorg', space_name='yourspace') 372 | 373 | # create the space 374 | space.create() 375 | 376 | # destroy the space 377 | space.destroy() 378 | 379 | # make a Cloud Controller request within the space 380 | apps_in_the_space = space.request('apps').get() 381 | 382 | # deploys an application to this space 383 | space.deploy_manifest('/path/to/manifest.yml') # push the app 384 | space.wait_manifest('/path/to/manifest.yml') # wait for the app to start 385 | space.destroy_manifest('/path/to/manifest.yml') # destroy the app 386 | 387 | app = space.get_app_by_name('yourappname') # find an application by its name within the space 388 | 389 | # deploy a service in this space 390 | space.get_deploy_service().create('my-custom-db', 'database-service', 'small-database-plan') 391 | service = space.get_service_instance_by_name('my-custom-db') # find a service by its name within the space 392 | ``` 393 | 394 | ## Tail Application Logs 395 | 396 | The `cf_api.logs_util` module may be used to tail Cloud Foundry application logs. Both 397 | `recentlogs` and `stream` modes are supported. The following snippet demonstrates the usage for 398 | listing recent logs and tailing app logs simultaneously. 399 | 400 | ```bash 401 | cd path/to/your/project 402 | python -m cf_api.logs_util \ 403 | --cloud-controller https://api.yourcloudfoundry.com \ 404 | -u youser -o yourg -s yourspace -a yourapp \ 405 | -r -t 406 | ``` 407 | 408 | This module may also be used programmatically. 409 | 410 | ```python 411 | from __future__ import print_function 412 | import cf_api 413 | from getpass import getpass 414 | from cf_api import dropsonde_util 415 | 416 | cc = cf_api.new_cloud_controller( 417 | 'https://api.yourcloudfoundry.com', 418 | username='youruser', 419 | password=getpass().strip(), 420 | client_id='cf', 421 | client_secret='', 422 | verify_ssl=True, 423 | init_doppler=True 424 | ) 425 | 426 | app_guid = 'your-app-guid' 427 | app = cc.apps(app_guid).get().resource 428 | 429 | # get recent logs 430 | logs = cc.doppler.apps(app.guid, 'recentlogs').get().multipart 431 | 432 | # convert log envelopes from protobuf to dict 433 | logs = [dropsonde_util.parse_envelope_protobuf(log) for log in logs] 434 | 435 | print(logs) 436 | 437 | # stream logs 438 | ws = cc.doppler.ws_request('apps', app.guid, 'stream') 439 | try: 440 | ws.connect() 441 | ws.watch(lambda m: print(dropsonde_util.parse_envelope_protobuf(m))) 442 | except Exception as e: 443 | print(e) 444 | finally: 445 | ws.close() 446 | ``` 447 | -------------------------------------------------------------------------------- /cf_api/__version__.py: -------------------------------------------------------------------------------- 1 | import re, os 2 | with open(os.path.join(os.path.dirname(__file__), '../version.txt')) as f: 3 | __version__ = f.read().strip() 4 | __semver__ = re.split('[ab]', __version__, 1)[0] 5 | -------------------------------------------------------------------------------- /cf_api/deploy_blue_green.py: -------------------------------------------------------------------------------- 1 | import os 2 | from .deploy_manifest import log 3 | from . import exceptions as exc 4 | 5 | 6 | class BlueGreen(object): 7 | """This class orchestrates a Blue-Green deployment in the style of the 8 | Autopilot CF CLI plugin. 9 | """ 10 | 11 | def __init__(self, 12 | space, 13 | manifest, 14 | verbose=True, 15 | wait_kwargs=None, 16 | **kwargs): 17 | """Initializes the deployment 18 | 19 | Args: 20 | space (cf_api.deploy_space.Space): 21 | The space to which the application should be deployed 22 | manifest (cf_api.deploy_manifest.Deploy): 23 | The manifest of the application to be deployed 24 | verbose (bool): 25 | Whether the deployment should be verbose in its output 26 | wait_kwargs (dict|None): 27 | Arguments to pass to the application ``wait_for_app_start`` 28 | function when waiting for the application to start 29 | """ 30 | self.space = space 31 | self.manifest = manifest 32 | self.verbose = verbose 33 | self.venerable_name = '-'.join([self.app_name, 'venerable']) 34 | self.venerable_manifest = self.manifest.clone(self.venerable_name) 35 | self.app = None 36 | self.venerable_app = None 37 | self.wait_kwargs = wait_kwargs or {} 38 | 39 | @property 40 | def cc(self): 41 | return self.space.cc 42 | 43 | @property 44 | def app_name(self): 45 | return self.manifest.name 46 | 47 | @classmethod 48 | def parse_manifest(cls, space, manifest_filename, **kwargs): 49 | """Parses a deployment manifest and creates a BlueGreen instance 50 | for each application in the manifest. 51 | 52 | Args: 53 | space (cf_api.deploy_space.Space): 54 | space to which the manifest should be deployed 55 | manifest_filename (str): 56 | application manifest to be deployed 57 | **kwargs (dict): 58 | passed into the BlueGreen constructor 59 | 60 | Returns: 61 | list[BlueGreen] 62 | """ 63 | space.set_debug(kwargs.get('verbose')) 64 | manifests = space.get_deploy_manifest(manifest_filename) 65 | return [BlueGreen(space, manifest, **kwargs) for manifest in manifests] 66 | 67 | def log(self, *args): 68 | if self.verbose: 69 | return log(*args) 70 | 71 | def _load_apps(self): 72 | self._load_app() 73 | self._load_venerable_app() 74 | 75 | def _load_app(self): 76 | try: 77 | self.app = self.space.get_app_by_name(self.app_name) 78 | except exc.ResponseException as e: 79 | self.app = None 80 | if 404 != e.code: 81 | raise 82 | 83 | def _load_venerable_app(self): 84 | try: 85 | self.venerable_app = self.space.get_app_by_name( 86 | self.venerable_name) 87 | except exc.ResponseException as e: 88 | self.venerable_app = None 89 | if 404 != e.code: 90 | raise 91 | 92 | def _rename_app(self): 93 | self._load_app() 94 | if self.app: 95 | self._load_venerable_app() 96 | if self.venerable_app: 97 | raise exc.InvalidStateException( 98 | 'attempting to rename app to venerable, but venerable ' 99 | 'already exists', 409) 100 | return self.cc.apps(self.app.guid)\ 101 | .set_params(name=self.venerable_name).put().data 102 | self._load_apps() 103 | return None 104 | 105 | def _destroy_venerable_app(self): 106 | self._load_venerable_app() 107 | if self.venerable_app: 108 | self._load_app() 109 | if not self.app: 110 | raise exc.InvalidStateException( 111 | 'attempting to destroy venerable app, but no app will take' 112 | ' it\'s place! aborting...', 409) 113 | return self.venerable_manifest.destroy(destroy_routes=False) 114 | self._load_apps() 115 | return None 116 | 117 | def wait_and_cleanup(self): 118 | """Waits for the new application to start and then destroys the old 119 | version of the app. 120 | """ 121 | self.log('Waiting for app to start...') 122 | self.manifest.wait_for_app_start( 123 | tailing=self.verbose, **self.wait_kwargs) 124 | self.log('OK') 125 | self.log('Destroying venerable...') 126 | self._load_venerable_app() 127 | if self.venerable_app: 128 | self._destroy_venerable_app() 129 | self.log('OK') 130 | 131 | def deploy_app(self): 132 | """Deploys the new application 133 | """ 134 | self.log('Checking apps...') 135 | self._load_apps() 136 | self.log('OK') 137 | 138 | if self.venerable_app: 139 | if self.app: 140 | self.log('Leftover venerable detected with replacement! ' 141 | 'Deleting...') 142 | self._destroy_venerable_app() 143 | self.log('OK') 144 | else: 145 | self.log('Leftover venerable detected with no replacement! ' 146 | 'Aborting...') 147 | raise exc.InvalidStateException( 148 | 'Leftover venerable detected! Rename it and try again.', 149 | 409) 150 | 151 | if self.app: 152 | self.log('Renaming app to venerable...') 153 | self._rename_app() 154 | self.log('OK') 155 | 156 | self.manifest.push() 157 | 158 | def deploy(self): 159 | """Deploy the new application, wait for it to start, then clean up the 160 | old application. 161 | """ 162 | self.deploy_app() 163 | self.wait_and_cleanup() 164 | 165 | 166 | def main(): 167 | import argparse 168 | from getpass import getpass 169 | from .deploy_space import Space 170 | import cf_api 171 | args = argparse.ArgumentParser() 172 | args.add_argument('--cloud-controller', required=True) 173 | args.add_argument('-u', '--user') 174 | args.add_argument('-o', '--org', required=True) 175 | args.add_argument('-s', '--space', required=True) 176 | args.add_argument('-m', '--manifest', required=True) 177 | args = args.parse_args() 178 | 179 | kwargs = dict( 180 | client_id='cf', 181 | client_secret='', 182 | ) 183 | if args.user: 184 | kwargs['username'] = args.user 185 | kwargs['password'] = getpass() 186 | else: 187 | kwargs['refresh_token'] = os.getenv('CF_REFRESH_TOKEN', '') 188 | 189 | cc = cf_api.new_cloud_controller(args.cloud_controller, **kwargs) 190 | space = Space(cc, org_name=args.org, space_name=args.space).set_debug(True) 191 | for manifest in space.deploy_blue_green(args.manifest): 192 | pass 193 | for manifest in space.wait_blue_green(args.manifest): 194 | pass 195 | 196 | 197 | if '__main__' == __name__: 198 | main() 199 | -------------------------------------------------------------------------------- /cf_api/deploy_service.py: -------------------------------------------------------------------------------- 1 | from __future__ import print_function 2 | import os 3 | import sys 4 | import time 5 | import cf_api 6 | import argparse 7 | from getpass import getpass 8 | from . import exceptions as exc 9 | 10 | 11 | class DeployService(object): 12 | """This class provides a basic interface to create and destroy services. 13 | Note you MUST set a space in which to operate before you can do anything 14 | with an instance of this class. See `set_org_and_space()` or 15 | `set_space_guid()` for more info on setting the space. 16 | """ 17 | 18 | _debug = False 19 | 20 | _org = None 21 | _space = None 22 | 23 | _service = None 24 | _service_plan = None 25 | 26 | _service_instance = None 27 | 28 | def __init__(self, cc): 29 | """Initializes a service deployment object 30 | 31 | Args: 32 | cc (cf_api.CloudController): an initialized instance of 33 | CloudController client 34 | """ 35 | self._cc = cc 36 | self._service_plan = {} 37 | 38 | def set_org_and_space(self, org_name, space_name): 39 | """Sets the org and space to be used in this service deployment 40 | 41 | Args: 42 | org_name (str): name of the organization 43 | space_name (str): name of the space 44 | 45 | Returns: 46 | DeployService: self 47 | """ 48 | res = self._cc.organizations().get_by_name(org_name) 49 | self._org = res.resource 50 | 51 | res = self._cc.request(self._org.spaces_url).get_by_name(space_name) 52 | self._space = res.resource 53 | return self 54 | 55 | def set_space_guid(self, space_guid): 56 | """Sets the guid of the space to be used in this deployment 57 | 58 | Args: 59 | space_guid (str): guid of the space 60 | 61 | Returns: 62 | DeployService: self 63 | """ 64 | res = self._cc.spaces(space_guid).get() 65 | self._space = res.resource 66 | 67 | res = self._cc.request(self._space.organization_url).get() 68 | self._org = res.resource 69 | return self 70 | 71 | def set_org_and_space_dicts(self, org_dict, space_dict): 72 | """Sets the internal org / space settings using existing resource dicts 73 | where this service will be deployed. 74 | 75 | Args: 76 | org_dict (dict|Resource): service instance's org dict to be used 77 | internally 78 | space_dict (dict|Resource): service instance's space dict to be 79 | used internally 80 | 81 | Returns: 82 | DeployService: self 83 | """ 84 | self._space = space_dict 85 | self._org = org_dict 86 | return self 87 | 88 | def set_debug(self, debug): 89 | """Sets a debug flag on whether this client should print debug messages 90 | 91 | Args: 92 | debug (bool) 93 | 94 | Returns: 95 | DeployService: self 96 | """ 97 | self._debug = debug 98 | return self 99 | 100 | def log(self, *args): 101 | if self._debug: 102 | sys.stdout.write(' '.join([str(a) for a in args]) + '\n') 103 | sys.stdout.flush() 104 | 105 | def _get_service_instance(self, name, no_cache=False): 106 | """Fetches the service instance object based on the given name. This 107 | method caches the service instance object by default; set no_cache=True 108 | in order to fetch a fresh service instance object. 109 | 110 | Args: 111 | name (str): user defined name of the service instance 112 | 113 | Keyword Args: 114 | no_cache (bool): skip the cache and re-fetch the service instance 115 | 116 | Returns: 117 | service_instance (cf_api.Resource): the service object 118 | """ 119 | self._assert_space() 120 | 121 | if self._service_instance and not no_cache: 122 | return self._service_instance 123 | res = self._cc.request(self._space.service_instances_url)\ 124 | .get_by_name(name) 125 | self._service_instance = res.resource 126 | return self._service_instance 127 | 128 | def _get_service(self, service_name): 129 | """Fetches the service provider details by its name 130 | 131 | Args: 132 | service_name (str): service type name as seen in marketplace 133 | 134 | Returns: 135 | cf_api.Resource: the service provider object 136 | """ 137 | if self._service: 138 | return self._service 139 | res = self._cc.services().get_by_name(service_name, name='label') 140 | self._service = res.resource 141 | return self._service 142 | 143 | def _get_service_plan(self, service_name, service_plan_name): 144 | """Fetches a service plan's details 145 | 146 | Args: 147 | service_name (str): service type name as seen in marketplace 148 | service_plan_name (str): service plan name within the given service 149 | 150 | Returns: 151 | service_plan (cf_api.Resource): the service plan object 152 | """ 153 | self._assert_space() 154 | key = ' / '.join([service_name, service_plan_name]) 155 | if key in self._service_plan: 156 | return self._service_plan[key] 157 | self._get_service(service_name) 158 | service_plan_url = self._service['entity']['service_plans_url'] 159 | res = self._cc.request(service_plan_url).get() 160 | for plan in res.resources: 161 | if service_plan_name == plan['entity']['name']: 162 | self._service_plan[key] = plan 163 | break 164 | return self._service_plan[key] 165 | 166 | def _get_last_operation(self, name): 167 | """Looks up the last operation state for the service. 168 | 169 | Args: 170 | name (str): the name of the service to be checked 171 | 172 | Returns: 173 | str: `state` string from the `last_operation` details 174 | """ 175 | self._get_service_instance(name, no_cache=True) 176 | lo = self._service_instance['entity']['last_operation'] 177 | return lo['state'] 178 | 179 | def _assert_space(self): 180 | if not self._space: 181 | raise exc.InvalidStateException('Space is required', 500) 182 | 183 | def create(self, name, service_name, service_plan_name, 184 | tags=None, parameters=None): 185 | """Creates a service with the user defined name (name), 186 | service type (service_name), and service plan name (service_plan_name). 187 | 188 | Args: 189 | name (str): user defined service name 190 | service_name (str): type of service to be created 191 | service_plan_name (str): plan of the service to be create 192 | 193 | Returns: 194 | cf_api.Resource: the created or existing service object 195 | """ 196 | self._assert_space() 197 | 198 | service_instance = self._get_service_instance(name) 199 | if service_instance: 200 | return service_instance 201 | 202 | service_plan = self._get_service_plan(service_name, service_plan_name) 203 | 204 | if not service_plan: 205 | raise exc.NotFoundException('Service plan not found', 404) 206 | 207 | body = dict( 208 | name=name, 209 | service_plan_guid=service_plan.guid, 210 | space_guid=self._space.guid 211 | ) 212 | if tags is not None: 213 | body['tags'] = tags 214 | if parameters is not None: 215 | body['parameters'] = parameters 216 | 217 | res = self._cc.service_instances() \ 218 | .set_query(accepts_incomplete='true') \ 219 | .set_params(**body).post() 220 | return res.resource 221 | 222 | def destroy(self, name): 223 | """Destroys a service with the user defined name 224 | 225 | Args: 226 | name (str): user defined name of the service 227 | 228 | Returns: 229 | cf_api.Resource: deleted service instance 230 | """ 231 | self._assert_space() 232 | 233 | service_instance = self._get_service_instance(name) 234 | if service_instance: 235 | lastop = service_instance.last_operation 236 | if 'delete' == lastop['type']: 237 | return service_instance 238 | return self._cc \ 239 | .service_instances(service_instance.guid) \ 240 | .set_query(accepts_incomplete='true') \ 241 | .delete() 242 | return None 243 | 244 | def is_provisioned(self, name): 245 | """Checks if the service is provisioned 246 | 247 | Args: 248 | name (str): user defined name of the service 249 | 250 | Returns: 251 | bool 252 | """ 253 | self._assert_space() 254 | 255 | return self._get_last_operation(name) == 'succeeded' 256 | 257 | def is_deprovisioned(self, name): 258 | """Checks if the service is de-provisioned 259 | 260 | Args: 261 | name (str): user defined name of the service 262 | 263 | Returns: 264 | bool 265 | """ 266 | self._assert_space() 267 | 268 | try: 269 | return not self._get_service_instance(name, no_cache=True) 270 | except Exception as e: 271 | print(str(e)) 272 | return True 273 | 274 | def is_provision_state(self, name, state): 275 | """Checks if the service with `name` is in the given `state` 276 | 277 | Args: 278 | name (str): user defined service name 279 | state (str): allowed values are `provisioned` or `deprovisioned` 280 | 281 | Returns: 282 | bool 283 | """ 284 | self._assert_space() 285 | 286 | if state not in ['provisioned', 'deprovisioned']: 287 | raise exc.InvalidArgsException( 288 | 'Invalid service state {0}'.format(state), 500) 289 | 290 | res = self._cc.uaa.refresh_token() 291 | self._cc.update_tokens(res) 292 | 293 | if 'provisioned' == state: 294 | return self.is_provisioned(name) 295 | elif 'deprovisioned' == state: 296 | return self.is_deprovisioned(name) 297 | else: 298 | return False 299 | 300 | def wait_service(self, name, state, timeout=300, interval=30): 301 | """Waits for the service with `name` to enter the `state` within the 302 | given `timeout`, while checking on the `interval`. This method WILL 303 | block until it's the service is in the desired state, or the timeout 304 | has passed. 305 | 306 | Args: 307 | name (str): user defined service name 308 | state (str): allowed values are `provisioned` or `deprovisioned` 309 | timeout (int=300): units in seconds 310 | interval (int=30): units in seconds 311 | """ 312 | self._assert_space() 313 | 314 | t = int(time.time()) 315 | while True: 316 | if self.is_provision_state(name, state): 317 | return 318 | elif 'deprovisioned' == state and self.is_provisioned(name): 319 | raise exc.InvalidStateException( 320 | 'Service {0} does not appear to be deprovisioning.' 321 | .format(name), 500) 322 | elif 'provisioned' == state and self.is_deprovisioned(name): 323 | raise exc.InvalidStateException( 324 | 'Service {0} does not appear to be provisioning.' 325 | .format(name), 500) 326 | 327 | if int(time.time()) - t > timeout: 328 | raise exc.TimeoutException( 329 | 'Service {0} provisioning timed out'.format(name), 500) 330 | 331 | lo = self._service_instance['entity']['last_operation'] 332 | self.log( 333 | 'waiting for service', self._org.name, '/', self._space.name, 334 | self._service_instance.name, lo['type'], lo['state'], 335 | lo['description']) 336 | 337 | time.sleep(interval) 338 | 339 | 340 | if '__main__' == __name__: 341 | 342 | def get_status(args): 343 | if args.provisioned: 344 | status = 'provisioned' 345 | elif args.deprovisioned: 346 | status = 'deprovisioned' 347 | else: 348 | status = None 349 | if 'create' == args.action: 350 | status = 'provisioned' 351 | elif 'destroy' == args.action: 352 | status = 'deprovisioned' 353 | return status 354 | 355 | def main(): 356 | args = argparse.ArgumentParser( 357 | description='This tool deploys a service to a Cloud Foundry ' 358 | 'org/space in the same manner as ' 359 | '`cf create-service\'') 360 | args.add_argument( 361 | '--cloud-controller', dest='cloud_controller', required=True, 362 | help='The Cloud Controller API endpoint ' 363 | '(excluding leading slashes)') 364 | args.add_argument( 365 | '-u', '--user', dest='user', 366 | help='The user to use for the deployment') 367 | args.add_argument( 368 | '-o', '--org', dest='org', required=True, 369 | help='The organization to which the service will be deployed') 370 | args.add_argument( 371 | '-s', '--space', dest='space', required=True, 372 | help='The space to which the service will be deployed') 373 | args.add_argument( 374 | '--skip-ssl', dest='skip_ssl', action='store_true', 375 | help='Indicates to skip SSL cert verification') 376 | args.add_argument( 377 | '--name', dest='name', required=True, 378 | help='User defined service name to be deployed') 379 | args.add_argument( 380 | '--service-name', dest='service_name', required=True, 381 | help='Service type to be deployed') 382 | args.add_argument( 383 | '--service-plan', dest='service_plan', required=True, 384 | help='Service plan to be deployed') 385 | args.add_argument( 386 | '-a', '--action', dest='action', choices=['create', 'destroy'], 387 | help='Service action to be executed. Only `create\' and ' 388 | '`destroy\' are supported values') 389 | args.add_argument( 390 | '-w', '--wait', dest='wait', default=False, action='store_true', 391 | help='Indicates to wait until the service is ' 392 | 'created before exiting') 393 | args.add_argument( 394 | '-v', '--verbose', dest='verbose', 395 | default=False, action='store_true', 396 | help='Indicates that verbose logging will be enabled') 397 | args.add_argument( 398 | '--provisioned', dest='provisioned', 399 | default=False, action='store_true', 400 | help='Used with --wait. Indicates to wait until the service ' 401 | 'is provisioned') 402 | args.add_argument( 403 | '--deprovisioned', dest='deprovisioned', 404 | default=False, action='store_true', 405 | help='Used with --wait. Indicates to wait until the service ' 406 | 'is provisioned') 407 | args.add_argument( 408 | '-t', '--timeout', dest='timeout', 409 | type=int, default=300, 410 | help='Sets a number of seconds to allow before timing out ' 411 | 'the deployment execution') 412 | args = args.parse_args() 413 | 414 | if args.user: 415 | username = args.user 416 | password = getpass('Password: ').strip() 417 | refresh_token = None 418 | else: 419 | username = None 420 | password = None 421 | refresh_token = os.getenv('CF_REFRESH_TOKEN') 422 | 423 | cc = cf_api.new_cloud_controller( 424 | args.cloud_controller, 425 | username=username, 426 | password=password, 427 | refresh_token=refresh_token, 428 | client_id='cf', 429 | client_secret='', 430 | verify_ssl=not args.skip_ssl, 431 | ) 432 | 433 | service_name = args.name 434 | service = DeployService(cc)\ 435 | .set_debug(args.verbose)\ 436 | .set_org_and_space(args.org, args.space) 437 | 438 | res = None 439 | status = get_status(args) 440 | if 'create' == args.action: 441 | res = service.create( 442 | service_name, 443 | args.service_name, 444 | args.service_plan 445 | ) 446 | 447 | elif 'destroy' == args.action: 448 | try: 449 | res = service.destroy( 450 | service_name 451 | ) 452 | except Exception as e: 453 | service.log(str(e)) 454 | return 455 | 456 | if res is not None: 457 | service.log(res) 458 | 459 | if status is not None and args.wait: 460 | service.wait_service(service_name, status, args.timeout) 461 | 462 | main() 463 | -------------------------------------------------------------------------------- /cf_api/deploy_space.py: -------------------------------------------------------------------------------- 1 | from __future__ import print_function 2 | import json 3 | from . import deploy_manifest 4 | from . import deploy_service 5 | from . import exceptions as exc 6 | 7 | 8 | class Space(object): 9 | """This class provides support for working with a particular space. It 10 | mainly provides convenience functions for deploying, fetching, and 11 | destroying the space, apps, and services. 12 | """ 13 | 14 | _org = None 15 | _space = None 16 | _space_name = None 17 | 18 | _debug = False 19 | 20 | def __init__(self, 21 | cc, 22 | org_name=None, 23 | org_guid=None, 24 | space_name=None, 25 | space_guid=None, 26 | is_debug=None): 27 | self.cc = cc 28 | if space_guid: 29 | self.set_space_guid(space_guid) 30 | elif org_guid: 31 | self.set_org_guid(org_guid) 32 | elif org_name and space_name: 33 | self.set_org(org_name).set_space(space_name) 34 | elif org_name: 35 | self.set_org(org_name) 36 | 37 | if is_debug is not None: 38 | self.set_debug(is_debug) 39 | 40 | @property 41 | def space(self): 42 | """Returns the currently set space 43 | """ 44 | if not self._space: 45 | if not self._space_name: 46 | raise exc.InvalidStateException('Space is not set.', 500) 47 | else: 48 | self.set_space(self._space_name) 49 | return self._space 50 | 51 | @property 52 | def org(self): 53 | """Returns the currently set org 54 | """ 55 | if not self._org: 56 | raise exc.InvalidStateException('Org is not set.', 500) 57 | return self._org 58 | 59 | def set_org(self, org_name): 60 | """Sets the organization name for this space 61 | 62 | Args: 63 | org_name (str): name of the organization 64 | 65 | Returns: 66 | space (Space): self 67 | """ 68 | res = self.cc.organizations().get_by_name(org_name) 69 | self._org = res.resource 70 | if self._org is None: 71 | raise exc.InvalidStateException('Org not found.', 404) 72 | return self 73 | 74 | def set_space(self, space_name): 75 | """Sets the space name 76 | 77 | Args: 78 | space_name (str): name of the space 79 | 80 | Returns: 81 | space (Space): self 82 | """ 83 | if not self._org: 84 | raise exc.InvalidStateException( 85 | 'Org is required to set the space name.', 500) 86 | res = self.cc.request(self._org.spaces_url).get_by_name(space_name) 87 | self._space = res.resource 88 | self._space_name = space_name 89 | return self 90 | 91 | def set_org_guid(self, org_guid): 92 | """Sets and loads the organization by the given GUID 93 | """ 94 | res = self.cc.organizations(org_guid).get() 95 | self._org = res.resource 96 | return self 97 | 98 | def set_space_guid(self, space_guid): 99 | """Sets the GUID of the space to be used in this deployment 100 | 101 | Args: 102 | space_guid (str): guid of the space 103 | 104 | Returns: 105 | self (Space) 106 | """ 107 | res = self.cc.spaces(space_guid).get() 108 | self._space = res.resource 109 | 110 | res = self.cc.request(self._space.organization_url).get() 111 | self._org = res.resource 112 | return self 113 | 114 | def set_debug(self, debug): 115 | """Sets a debug flag on whether this client should print debug messages 116 | 117 | Args: 118 | debug (bool) 119 | 120 | Returns: 121 | self (Space) 122 | """ 123 | self._debug = debug 124 | return self 125 | 126 | def request(self, *urls): 127 | """Creates a request object with a base url (i.e. /v2/spaces/) 128 | """ 129 | return self.cc.request(self._space['metadata']['url'], *urls) 130 | 131 | def create(self, **params): 132 | """Creates the space 133 | 134 | Keyword Args: 135 | params: HTTP body args for the space create endpoint 136 | """ 137 | if not self._space: 138 | res = self.cc.spaces().set_params( 139 | name=self._space_name, 140 | organization_guid=self._org.guid, 141 | **params 142 | ).post() 143 | 144 | self._space = res.resource 145 | 146 | return self._space 147 | 148 | def destroy(self, destroy_routes=False): 149 | """Destroys the space, and, optionally, any residual routes existing in 150 | the space. 151 | 152 | Keyword Args: 153 | destroy_routes (bool): indicates if to destroy routes 154 | """ 155 | if not self._space: 156 | raise exc.InvalidStateException( 157 | 'No space specified. Can\'t destroy.', 500) 158 | 159 | route_results = [] 160 | if destroy_routes: 161 | for r in self.get_routes(): 162 | res = self.cc.routes(r.guid).delete() 163 | route_results.append(res.data) 164 | 165 | res = self.cc.spaces(self._space.guid).delete() 166 | self._space = None 167 | return res.resource, route_results 168 | 169 | def get_deploy_manifest(self, manifest_filename): 170 | """Parses the manifest deployment list and sets the org and space to be 171 | used in deployment. 172 | """ 173 | self._assert_space() 174 | app_deploys = deploy_manifest.Deploy\ 175 | .parse_manifest(manifest_filename, self.cc) 176 | return [d.set_org_and_space_dicts(self._org, self._space) 177 | .set_debug(self._debug) for d in app_deploys] 178 | 179 | def get_deploy_service(self): 180 | """Returns a service deployment client with the org and space to be 181 | used in deployment. 182 | """ 183 | self._assert_space() 184 | return deploy_service.DeployService(self.cc)\ 185 | .set_debug(self._debug)\ 186 | .set_org_and_space_dicts(self._org, self._space) 187 | 188 | def deploy_manifest(self, manifest_filename, **kwargs): 189 | """Deploys all apps in the given app manifest into this space. 190 | 191 | Args: 192 | manifest_filename (str): app manifest filename to be deployed 193 | """ 194 | return [m.push(**kwargs) 195 | for m in self.get_deploy_manifest(manifest_filename)] 196 | 197 | def wait_manifest(self, manifest_filename, interval=20, timeout=300, 198 | tailing=False): 199 | """Waits for an app to start given a manifest filename. 200 | 201 | Args: 202 | manifest_filename (str): app manifest filename to be waited on 203 | 204 | Keyword Args: 205 | interval (int): how often to check if the app has started 206 | timeout (int): how long to wait for the app to start 207 | """ 208 | app_deploys = self.get_deploy_manifest(manifest_filename) 209 | deploy_manifest.Deploy.wait_for_apps_start( 210 | app_deploys, interval, timeout, tailing=tailing) 211 | 212 | def destroy_manifest(self, manifest_filename, destroy_routes=False): 213 | """Destroys all apps in the given app manifest in this space. 214 | 215 | Args: 216 | manifest_filename (str): app manifest filename to be destroyed 217 | 218 | Keyword Args: 219 | destroy_routes (bool): indicates whether to destroy routes 220 | """ 221 | return [m.destroy(destroy_routes) 222 | for m in self.get_deploy_manifest(manifest_filename)] 223 | 224 | def get_blue_green(self, manifest_filename, interval=20, timeout=300, 225 | tailing=None, **kwargs): 226 | """Parses the manifest and searches for ``app_name``, returning an 227 | instance of the BlueGreen deployer object. 228 | 229 | Args: 230 | manifest_filename (str) 231 | interval (int) 232 | timeout (int) 233 | tailing (bool) 234 | **kwargs (dict): are passed along to the BlueGreen constructor 235 | 236 | Returns: 237 | list[cf_api.deploy_blue_green.BlueGreen] 238 | """ 239 | from .deploy_blue_green import BlueGreen 240 | if tailing is not None: 241 | kwargs['verbose'] = tailing 242 | elif 'verbose' not in kwargs: 243 | kwargs['verbose'] = self._debug 244 | kwargs['wait_kwargs'] = {'interval': interval, 'timeout': timeout} 245 | return BlueGreen.parse_manifest(self, manifest_filename, **kwargs) 246 | 247 | def deploy_blue_green(self, manifest_filename, **kwargs): 248 | """Deploys the application from the given manifest using the 249 | BlueGreen deployment strategy 250 | 251 | Args: 252 | manifest_filename (str) 253 | **kwargs (dict): are passed along to self.get_blue_green 254 | 255 | Returns: 256 | list 257 | """ 258 | return [m.deploy_app() 259 | for m in self.get_blue_green(manifest_filename, **kwargs)] 260 | 261 | def wait_blue_green(self, manifest_filename, **kwargs): 262 | """Waits for the application to start, from the given manifest using 263 | the BlueGreen deployment strategy 264 | 265 | Args: 266 | manifest_filename (str) 267 | **kwargs (dict): are passed along to self.get_blue_green 268 | 269 | Returns: 270 | list 271 | """ 272 | return [m.wait_and_cleanup() 273 | for m in self.get_blue_green(manifest_filename, **kwargs)] 274 | 275 | def get_service_instance_by_name(self, name): 276 | """Searches the space for a service instance with the name 277 | """ 278 | res = self.cc.request(self._space.service_instances_url)\ 279 | .get_by_name(name) 280 | return res.resource 281 | 282 | def get_app_by_name(self, name): 283 | """Searches the space for an app with the name 284 | """ 285 | res = self.cc.request(self._space.apps_url)\ 286 | .get_by_name(name) 287 | return res.resource 288 | 289 | def get_routes(self, host=None): 290 | """Searches the space for routes 291 | """ 292 | req = self.cc.spaces(self._space.guid, 'routes') 293 | res = req.get_by_name(host, 'host') if host else req.get() 294 | return res.resources 295 | 296 | def _assert_space(self): 297 | if not self._space: 298 | raise exc.InvalidStateException('No space is set.', 500) 299 | 300 | 301 | if '__main__' == __name__: 302 | import argparse 303 | import __init__ as cf_api 304 | from getpass import getpass 305 | 306 | def main(): 307 | args = argparse.ArgumentParser( 308 | description='This tool performs Cloud Controller API requests ' 309 | 'on behalf of a user in a given org/space. It may ' 310 | 'be used to look up space specific resources such ' 311 | 'as apps and services. It returns only the raw ' 312 | 'JSON response from the Cloud Controller.') 313 | args.add_argument( 314 | '--cloud-controller', dest='cloud_controller', required=True, 315 | help='The Cloud Controller API endpoint ' 316 | '(excluding leading slashes)') 317 | args.add_argument( 318 | '-u', '--user', dest='user', required=True, 319 | help='The user used to authenticate. This may be omitted ' 320 | 'if --client-id and --client-secret have sufficient ' 321 | 'authorization to perform the desired request without a ' 322 | 'user\'s permission') 323 | args.add_argument( 324 | '-o', '--org', dest='org', required=True, 325 | help='The organization to be accessed') 326 | args.add_argument( 327 | '-s', '--space', dest='space', required=True, 328 | help='The space to be accessed') 329 | args.add_argument( 330 | '--client-id', dest='client_id', default='cf', 331 | help='Used to set a custom client ID') 332 | args.add_argument( 333 | '--client-secret', dest='client_secret', default='', 334 | help='Secret corresponding to --client-id') 335 | args.add_argument( 336 | '--skip-ssl', dest='skip_ssl', action='store_true', 337 | help='Indicates to skip SSL cert verification.') 338 | args.add_argument( 339 | '--show-org', dest='show_org', action='store_true', 340 | help='Indicates to show the organization set in --org/-o') 341 | args.add_argument( 342 | '--list-all', dest='list_all', action='store_true', 343 | help='Indicates to get all pages of resources matching the given ' 344 | 'URL') 345 | args.add_argument( 346 | '--pretty', dest='pretty_print', action='store_true', 347 | help='Indicates to pretty-print the resulting JSON') 348 | args.add_argument( 349 | 'url', nargs='?', 350 | help='The URL to be accessed relative to the space URL. This value' 351 | ' will be appended to the space URL indicated by -o and -s ' 352 | '(i.e. /spaces//)') 353 | args = args.parse_args() 354 | 355 | cc = cf_api.new_cloud_controller( 356 | args.cloud_controller, 357 | username=args.user, 358 | password=getpass().strip() if args.user is not None else None, 359 | client_id=args.client_id, 360 | client_secret=args.client_secret, 361 | verify_ssl=not args.skip_ssl, 362 | init_doppler=True, 363 | ) 364 | 365 | space = Space( 366 | cc, 367 | org_name=args.org, 368 | space_name=args.space, 369 | is_debug=True 370 | ) 371 | 372 | dumps_kwargs = {} 373 | if args.pretty_print: 374 | dumps_kwargs['indent'] = 4 375 | 376 | if args.url: 377 | req = space.request(args.url) 378 | if not args.list_all: 379 | return print(req.get().text) 380 | else: 381 | res = cc.get_all_resources(req) 382 | elif args.show_org: 383 | res = space.org 384 | else: 385 | res = space.space 386 | 387 | return print(json.dumps(res, **dumps_kwargs)) 388 | 389 | main() 390 | -------------------------------------------------------------------------------- /cf_api/dropsonde_util.py: -------------------------------------------------------------------------------- 1 | from __future__ import print_function 2 | import six 3 | import json 4 | from copy import copy 5 | from datetime import datetime 6 | from google.protobuf.descriptor import FieldDescriptor 7 | from dropsonde.pb.envelope_pb2 import Envelope 8 | from .pb2dict import TYPE_CALLABLE_MAP, protobuf_to_dict 9 | 10 | 11 | def _json_encoder(o): 12 | if isinstance(o, six.binary_type): 13 | return o.decode('utf-8') 14 | elif isinstance(o, six.string_types): 15 | return str(o) 16 | else: 17 | return o.__dict__ 18 | 19 | 20 | def parse_envelope_protobuf(pbstr): 21 | """Parses a protocol buffers string into a dictionary representing a 22 | Dropsonde Envelope protobuf message type 23 | 24 | Args: 25 | pbstr (six.binary_type): protocol buffers string 26 | 27 | Returns: 28 | dict 29 | """ 30 | env = Envelope() 31 | env.ParseFromString(pbstr) 32 | env = protobuf_to_dict(env) 33 | return env 34 | 35 | 36 | def format_unixnano(unixnano): 37 | """Formats an integer unix timestamp from nanoseconds to a user readable 38 | string 39 | 40 | Args: 41 | unixnano (int): integer unix timestamp in nanoseconds 42 | Returns: 43 | formatted_time (str) 44 | """ 45 | return datetime.fromtimestamp(int(unixnano / 1e6) / 1000.0)\ 46 | .strftime('%Y-%m-%d %H:%M:%S.%f') 47 | 48 | 49 | def render_log_http_start_stop(m): 50 | """Formats an HttpStartStop protobuf event type 51 | 52 | Args: 53 | m (DopplerEnvelope): envelope object 54 | 55 | Returns: 56 | str: string message that may be printed 57 | """ 58 | if not isinstance(m, DopplerEnvelope): 59 | m = DopplerEnvelope(m) 60 | hss = m['httpStartStop'] 61 | return ' '.join([ 62 | format_unixnano(m['timestamp']), 63 | ': ', 64 | str((hss['stopTimestamp'] - hss['startTimestamp']) / float(1e6)), 65 | 'ms', 66 | format_unixnano(hss['startTimestamp']), 67 | str(m.app_id), 68 | hss['peerType'], 69 | hss['method'], 70 | str(hss.get('uri', None)) 71 | ]) 72 | 73 | 74 | class DopplerEnvelope(dict): 75 | """Utility class for parsing Doppler Envelope protocol buffers messages 76 | and accessing their members 77 | """ 78 | def __init__(self, pbstr): 79 | super(DopplerEnvelope, self).__init__(**pbstr) 80 | self['eventType'] = Envelope.EventType.Name(self['eventType']) 81 | 82 | def __getattr__(self, item): 83 | return self.get(item, None) 84 | 85 | def __str__(self): 86 | """Builds a string log message from this class 87 | """ 88 | return ''.join([ 89 | '[ ', 90 | ' - '.join([ 91 | str(self['eventType']), 92 | str(self['origin']), 93 | str(self['deployment']), 94 | format_unixnano(self['timestamp'])]), 95 | ' ]: ', 96 | self.message 97 | ]) 98 | 99 | def __repr__(self): 100 | return self.__str__() 101 | 102 | def is_event_type(self, *event_type): 103 | """Checks if the event type is one of the passed in arguments 104 | 105 | Args: 106 | event_type (tuple[str]): HttpStartStop, LogMessage, CounterEvent, 107 | ContainerEvent, ValueMetric 108 | 109 | Returns: 110 | bool 111 | """ 112 | return self['eventType'] in event_type 113 | 114 | @property 115 | def message(self): 116 | """String message representing this envelope customized for several 117 | event types 118 | """ 119 | if self.is_event_type('HttpStartStop'): 120 | return str(render_log_http_start_stop(self)) 121 | elif self.is_event_type('LogMessage'): 122 | msg = self.get('logMessage', {}).get('message', '') 123 | try: 124 | return msg.decode('utf-8') 125 | except: 126 | return str(msg) 127 | elif self.is_event_type('ValueMetric'): 128 | return json.dumps(self['valueMetric']) 129 | elif self.is_event_type('ContainerMetric'): 130 | return json.dumps(self['containerMetric']) 131 | elif self.is_event_type('CounterEvent'): 132 | return json.dumps(self['counterEvent']) 133 | else: 134 | return json.dumps(self, default=_json_encoder) 135 | 136 | @property 137 | def request_id(self): 138 | """Fetches the request UUID converting it from the UUID envelope 139 | message type. Currently this is only applicable to the HttpStartStop 140 | event type 141 | """ 142 | if self.is_event_type('HttpStartStop'): 143 | return get_uuid_string( 144 | **self['httpStartStop'].get('requestId', {})) 145 | else: 146 | return None 147 | 148 | @property 149 | def app_id(self): 150 | """Fetches the application UUID converting it from the UUID envelope 151 | message type. Currently this is only applicable to the HttpStartStop 152 | and LogMessage event types. 153 | """ 154 | if self.is_event_type('HttpStartStop'): 155 | return get_uuid_string( 156 | **self['httpStartStop'].get('applicationId', {})) 157 | else: 158 | return self.get('logMessage', {}).get('app_id', None) 159 | 160 | @staticmethod 161 | def wrap(pbstr): 162 | """Parses a protobuf string into an Envelope dictionary 163 | 164 | Args: 165 | pbstr (str) 166 | 167 | Returns: 168 | DopplerEnvelope|None: if a falsy value is passed in, None is 169 | returned, else a DopplerEnvelope is returned. 170 | """ 171 | if not pbstr: 172 | return None 173 | if isinstance(pbstr, (six.text_type, six.binary_type)): 174 | pbstr = parse_envelope_protobuf(pbstr) 175 | return DopplerEnvelope(pbstr) 176 | -------------------------------------------------------------------------------- /cf_api/exceptions.py: -------------------------------------------------------------------------------- 1 | from requests_factory import APIException 2 | from requests_factory import ResponseException 3 | from requests_factory import InvalidStateException 4 | 5 | 6 | class CFException(APIException): 7 | """Base class of all exceptions used in this library 8 | """ 9 | pass 10 | 11 | 12 | class UnavailableException(CFException): 13 | """Indicates that the requested Cloud Foundry component is unavailable 14 | """ 15 | pass 16 | 17 | 18 | class InvalidArgsException(CFException): 19 | """Indicates that invalid arguments have been provided to the function 20 | """ 21 | pass 22 | 23 | 24 | class NotFoundException(CFException): 25 | """Indicates that the requested object cannot be found 26 | """ 27 | pass 28 | 29 | 30 | class TimeoutException(CFException): 31 | """Indicates that an operation has timed out 32 | """ 33 | pass 34 | -------------------------------------------------------------------------------- /cf_api/logs_util.py: -------------------------------------------------------------------------------- 1 | from __future__ import print_function 2 | import os 3 | import sys 4 | import threading 5 | from uuid import uuid4 6 | from .dropsonde_util import DopplerEnvelope 7 | 8 | 9 | class TailThread(object): 10 | def __init__(self, doppler, app_guid, render_log=None): 11 | self.ws = doppler.ws_request('apps', app_guid, 'stream') 12 | self.thread = threading.Thread(target=self.run) 13 | self.is_terminated = False 14 | 15 | def _render_log(msg): 16 | d = DopplerEnvelope.wrap(msg) 17 | sys.stdout.write(''.join([str(d), '\n'])) 18 | sys.stdout.flush() 19 | 20 | self.render_log = render_log or _render_log 21 | 22 | def start(self): 23 | self.ws.connect() 24 | self.thread.start() 25 | return self 26 | 27 | def terminate(self): 28 | self.ws.close() 29 | self.thread.join() 30 | self.is_terminated = True 31 | return self 32 | 33 | def run(self): 34 | try: 35 | self.ws.watch(self.render_log) 36 | except Exception as e: 37 | print(e) 38 | 39 | 40 | if __name__ == "__main__": 41 | import argparse 42 | import cf_api 43 | from getpass import getpass 44 | 45 | def main(): 46 | 47 | args = argparse.ArgumentParser( 48 | description='This tool performs operations against the Cloud ' 49 | 'Foundry Doppler logging service. It can tail a ' 50 | 'specific application\'s logs, fetch recent logs, or ' 51 | 'read directly from the firehose.') 52 | args.add_argument( 53 | '--cloud-controller', dest='cloud_controller', required=True, 54 | help='The Cloud Controller API endpoint ' 55 | '(excluding leading slashes)') 56 | args.add_argument( 57 | '-u', '--user', dest='user', default=None, 58 | help='The user used to authenticate. This may be omitted ' 59 | 'if --client-id and --client-secret have sufficient ' 60 | 'authorization to perform the desired operation without a ' 61 | 'user\'s permission') 62 | args.add_argument( 63 | '-o', '--org', dest='org', default=None, 64 | help='The organization to be accessed') 65 | args.add_argument( 66 | '-s', '--space', dest='space', default=None, 67 | help='The space to be accessed') 68 | args.add_argument( 69 | '-a', '--app', dest='app', default=None, 70 | help='The application whose logs will be accessed') 71 | args.add_argument( 72 | '-r', '--recent', dest='recent_logs', action='store_true', 73 | help='Indicates to fetch the recent logs from the application') 74 | args.add_argument( 75 | '-t', '--tail', dest='tail_logs', action='store_true', 76 | help='Indicates to tail the logs from the application') 77 | args.add_argument( 78 | '-f', '--firehose', dest='firehose', default=None, 79 | help='Indicates to connect to the Cloud Foundry firehose. ' 80 | 'The value of this option should be a unique ' 81 | 'user-defined subscription ID that represents your logging ' 82 | 'session. Note that you must set a custom --client-id and ' 83 | '--client-secret that is authorized to access the ' 84 | 'firehose') 85 | args.add_argument( 86 | '-e', '--event-types', dest='event_types', default='', 87 | help='') 88 | args.add_argument( 89 | '--client-id', dest='client_id', default='cf', 90 | help='Used to set a custom client ID. This is required to ' 91 | 'use the --firehose. Scope should include ' 92 | '`doppler.firehose\'') 93 | args.add_argument( 94 | '--client-secret', dest='client_secret', default='', 95 | help='Secret corresponding to --client-id') 96 | args.add_argument( 97 | '--skip-ssl', dest='skip_ssl', action='store_true', 98 | help='Indicates to skip SSL cert verification') 99 | args = args.parse_args() 100 | 101 | event_types = args.event_types.split(',') 102 | 103 | def render_log(msg): 104 | d = DopplerEnvelope.wrap(msg) 105 | if args.event_types and not d.is_event_type(*event_types): 106 | return 107 | sys.stdout.write(''.join([str(d), '\n'])) 108 | sys.stdout.flush() 109 | 110 | cc = cf_api.new_cloud_controller( 111 | args.cloud_controller, 112 | username=args.user, 113 | password=getpass().strip() if args.user is not None else None, 114 | client_id=args.client_id, 115 | client_secret=args.client_secret, 116 | verify_ssl=not args.skip_ssl, 117 | init_doppler=True, 118 | refresh_token=os.getenv('CF_REFRESH_TOKEN'), 119 | ) 120 | 121 | if args.firehose: 122 | print('*' * 40, 'firehose', '*' * 40) 123 | subscription_id = '-'.join([args.firehose, str(uuid4())]) 124 | ws = cc.doppler.ws_request('firehose', subscription_id) 125 | try: 126 | ws.connect() 127 | ws.watch(render_log) 128 | except Exception as e: 129 | print(e) 130 | finally: 131 | ws.close() 132 | 133 | else: 134 | if not args.org or not args.space or not args.app: 135 | raise Exception('Org, space, and app are required') 136 | 137 | from . import deploy_space 138 | space = deploy_space.Space( 139 | cc, 140 | org_name=args.org, 141 | space_name=args.space, 142 | is_debug=True 143 | ) 144 | 145 | app = space.get_app_by_name(args.app) 146 | 147 | if args.recent_logs: 148 | print('*' * 40, 'recent logs', '*' * 40) 149 | logs = cc.doppler.apps(app.guid, 'recentlogs').get() 150 | for part in logs.multipart: 151 | render_log(part) 152 | 153 | if args.tail_logs: 154 | print('*' * 40, 'streaming logs', '*' * 40) 155 | ws = cc.doppler.ws_request('apps', app.guid, 'stream') 156 | try: 157 | ws.connect() 158 | ws.watch(render_log) 159 | except Exception as e: 160 | print(e) 161 | finally: 162 | ws.close() 163 | 164 | main() 165 | -------------------------------------------------------------------------------- /cf_api/pb2dict.py: -------------------------------------------------------------------------------- 1 | import six 2 | import datetime 3 | 4 | from google.protobuf.message import Message 5 | from google.protobuf.descriptor import FieldDescriptor 6 | from google.protobuf.timestamp_pb2 import Timestamp 7 | 8 | __all__ = ["protobuf_to_dict", "TYPE_CALLABLE_MAP", "dict_to_protobuf", 9 | "REVERSE_TYPE_CALLABLE_MAP"] 10 | 11 | Timestamp_type_name = 'Timestamp' 12 | 13 | 14 | def datetime_to_timestamp(dt): 15 | ts = Timestamp() 16 | ts.FromDatetime(dt) 17 | return ts 18 | 19 | 20 | def timestamp_to_datetime(ts): 21 | dt = ts.ToDatetime() 22 | return dt 23 | 24 | 25 | EXTENSION_CONTAINER = '___X' 26 | 27 | 28 | TYPE_CALLABLE_MAP = { 29 | FieldDescriptor.TYPE_DOUBLE: float, 30 | FieldDescriptor.TYPE_FLOAT: float, 31 | FieldDescriptor.TYPE_INT32: int, 32 | FieldDescriptor.TYPE_INT64: int if six.PY3 else six.integer_types[1], 33 | FieldDescriptor.TYPE_UINT32: int, 34 | FieldDescriptor.TYPE_UINT64: int if six.PY3 else six.integer_types[1], 35 | FieldDescriptor.TYPE_SINT32: int, 36 | FieldDescriptor.TYPE_SINT64: int if six.PY3 else six.integer_types[1], 37 | FieldDescriptor.TYPE_FIXED32: int, 38 | FieldDescriptor.TYPE_FIXED64: int if six.PY3 else six.integer_types[1], 39 | FieldDescriptor.TYPE_SFIXED32: int, 40 | FieldDescriptor.TYPE_SFIXED64: int if six.PY3 else six.integer_types[1], 41 | FieldDescriptor.TYPE_BOOL: bool, 42 | FieldDescriptor.TYPE_STRING: six.text_type, 43 | FieldDescriptor.TYPE_BYTES: six.binary_type, 44 | FieldDescriptor.TYPE_ENUM: int, 45 | } 46 | 47 | 48 | def repeated(type_callable): 49 | return lambda value_list: [type_callable(value) for value in value_list] 50 | 51 | 52 | def enum_label_name(field, value, lowercase_enum_lables=False): 53 | label = field.enum_type.values_by_number[int(value)].name 54 | label = label.lower() if lowercase_enum_lables else label 55 | return label 56 | 57 | 58 | def _is_map_entry(field): 59 | return (field.type == FieldDescriptor.TYPE_MESSAGE and 60 | field.message_type.has_options and 61 | field.message_type.GetOptions().map_entry) 62 | 63 | 64 | def protobuf_to_dict(pb, type_callable_map=TYPE_CALLABLE_MAP, use_enum_labels=False, 65 | including_default_value_fields=False, lowercase_enum_lables=False): 66 | result_dict = {} 67 | extensions = {} 68 | for field, value in pb.ListFields(): 69 | if field.message_type and field.message_type.has_options and field.message_type.GetOptions().map_entry: 70 | result_dict[field.name] = dict() 71 | value_field = field.message_type.fields_by_name['value'] 72 | type_callable = _get_field_value_adaptor( 73 | pb, value_field, type_callable_map, 74 | use_enum_labels, including_default_value_fields, 75 | lowercase_enum_lables) 76 | for k, v in value.items(): 77 | result_dict[field.name][k] = type_callable(v) 78 | continue 79 | type_callable = _get_field_value_adaptor(pb, field, type_callable_map, 80 | use_enum_labels, including_default_value_fields, 81 | lowercase_enum_lables) 82 | if field.label == FieldDescriptor.LABEL_REPEATED: 83 | type_callable = repeated(type_callable) 84 | 85 | if field.is_extension: 86 | extensions[str(field.number)] = type_callable(value) 87 | continue 88 | 89 | result_dict[field.name] = type_callable(value) 90 | 91 | # Serialize default value if including_default_value_fields is True. 92 | if including_default_value_fields: 93 | for field in pb.DESCRIPTOR.fields: 94 | # Singular message fields and oneof fields will not be affected. 95 | if (( 96 | field.label != FieldDescriptor.LABEL_REPEATED and 97 | field.cpp_type == FieldDescriptor.CPPTYPE_MESSAGE) or 98 | field.containing_oneof): 99 | continue 100 | if field.name in result_dict: 101 | # Skip the field which has been serailized already. 102 | continue 103 | if _is_map_entry(field): 104 | result_dict[field.name] = {} 105 | else: 106 | result_dict[field.name] = field.default_value 107 | 108 | if extensions: 109 | result_dict[EXTENSION_CONTAINER] = extensions 110 | return result_dict 111 | 112 | 113 | def _get_field_value_adaptor(pb, field, type_callable_map=TYPE_CALLABLE_MAP, use_enum_labels=False, 114 | including_default_value_fields=False, lowercase_enum_lables=False): 115 | 116 | if field.message_type and field.message_type.name == Timestamp_type_name: 117 | return timestamp_to_datetime 118 | if field.type == FieldDescriptor.TYPE_MESSAGE: 119 | # recursively encode protobuf sub-message 120 | return lambda pb: protobuf_to_dict( 121 | pb, type_callable_map=type_callable_map, 122 | use_enum_labels=use_enum_labels, 123 | including_default_value_fields=including_default_value_fields, 124 | lowercase_enum_lables=lowercase_enum_lables, 125 | ) 126 | 127 | if use_enum_labels and field.type == FieldDescriptor.TYPE_ENUM: 128 | return lambda value: enum_label_name(field, value, lowercase_enum_lables) 129 | 130 | if field.type in type_callable_map: 131 | return type_callable_map[field.type] 132 | 133 | raise TypeError("Field %s.%s has unrecognised type id %d" % ( 134 | pb.__class__.__name__, field.name, field.type)) 135 | 136 | 137 | REVERSE_TYPE_CALLABLE_MAP = { 138 | } 139 | 140 | 141 | def dict_to_protobuf(pb_klass_or_instance, values, type_callable_map=REVERSE_TYPE_CALLABLE_MAP, 142 | strict=True, ignore_none=False): 143 | """Populates a protobuf model from a dictionary. 144 | :param pb_klass_or_instance: a protobuf message class, or an protobuf instance 145 | :type pb_klass_or_instance: a type or instance of a subclass of google.protobuf.message.Message 146 | :param dict values: a dictionary of values. Repeated and nested values are 147 | fully supported. 148 | :param dict type_callable_map: a mapping of protobuf types to callables for setting 149 | values on the target instance. 150 | :param bool strict: complain if keys in the map are not fields on the message. 151 | :param bool strict: ignore None-values of fields, treat them as empty field 152 | :param bool strict: when false: accept enums both in lowercase and uppercase 153 | """ 154 | if isinstance(pb_klass_or_instance, Message): 155 | instance = pb_klass_or_instance 156 | else: 157 | instance = pb_klass_or_instance() 158 | return _dict_to_protobuf(instance, values, type_callable_map, strict, ignore_none) 159 | 160 | 161 | def _get_field_mapping(pb, dict_value, strict): 162 | field_mapping = [] 163 | for key, value in dict_value.items(): 164 | if key == EXTENSION_CONTAINER: 165 | continue 166 | if key not in pb.DESCRIPTOR.fields_by_name: 167 | if strict: 168 | raise KeyError("%s does not have a field called %s" % (pb, key)) 169 | continue 170 | field_mapping.append((pb.DESCRIPTOR.fields_by_name[key], value, getattr(pb, key, None))) 171 | 172 | for ext_num, ext_val in dict_value.get(EXTENSION_CONTAINER, {}).items(): 173 | try: 174 | ext_num = int(ext_num) 175 | except ValueError: 176 | raise ValueError("Extension keys must be integers.") 177 | if ext_num not in pb._extensions_by_number: 178 | if strict: 179 | raise KeyError("%s does not have a extension with number %s. Perhaps you forgot to import it?" % (pb, key)) 180 | continue 181 | ext_field = pb._extensions_by_number[ext_num] 182 | pb_val = None 183 | pb_val = pb.Extensions[ext_field] 184 | field_mapping.append((ext_field, ext_val, pb_val)) 185 | 186 | return field_mapping 187 | 188 | 189 | def _dict_to_protobuf(pb, value, type_callable_map, strict, ignore_none): 190 | fields = _get_field_mapping(pb, value, strict) 191 | 192 | for field, input_value, pb_value in fields: 193 | if ignore_none and input_value is None: 194 | continue 195 | if field.label == FieldDescriptor.LABEL_REPEATED: 196 | if field.message_type and field.message_type.has_options and field.message_type.GetOptions().map_entry: 197 | value_field = field.message_type.fields_by_name['value'] 198 | for key, value in input_value.items(): 199 | if value_field.cpp_type == FieldDescriptor.CPPTYPE_MESSAGE: 200 | _dict_to_protobuf(getattr(pb, field.name)[key], value, type_callable_map, strict, ignore_none) 201 | else: 202 | getattr(pb, field.name)[key] = value 203 | continue 204 | for item in input_value: 205 | if field.type == FieldDescriptor.TYPE_MESSAGE: 206 | m = pb_value.add() 207 | _dict_to_protobuf(m, item, type_callable_map, strict, ignore_none) 208 | elif field.type == FieldDescriptor.TYPE_ENUM and isinstance(item, six.string_types): 209 | pb_value.append(_string_to_enum(field, item, strict)) 210 | else: 211 | pb_value.append(item) 212 | continue 213 | if isinstance(input_value, datetime.datetime): 214 | input_value = datetime_to_timestamp(input_value) 215 | # Instead of setattr we need to use CopyFrom for composite fields 216 | # Otherwise we will get AttributeError: Assignment not allowed to composite field "field name" in protocol message object 217 | getattr(pb, field.name).CopyFrom(input_value) 218 | continue 219 | elif field.type == FieldDescriptor.TYPE_MESSAGE: 220 | _dict_to_protobuf(pb_value, input_value, type_callable_map, strict, ignore_none) 221 | continue 222 | 223 | if field.type in type_callable_map: 224 | input_value = type_callable_map[field.type](input_value) 225 | 226 | if field.is_extension: 227 | pb.Extensions[field] = input_value 228 | continue 229 | 230 | if field.type == FieldDescriptor.TYPE_ENUM and isinstance(input_value, six.string_types): 231 | input_value = _string_to_enum(field, input_value, strict) 232 | 233 | setattr(pb, field.name, input_value) 234 | 235 | return pb 236 | 237 | 238 | def _string_to_enum(field, input_value, strict=False): 239 | try: 240 | input_value = field.enum_type.values_by_name[input_value].number 241 | except KeyError: 242 | if strict: 243 | raise KeyError("`%s` is not a valid value for field `%s`" % (input_value, field.name)) 244 | else: 245 | return _string_to_enum(field, input_value.upper(), strict=True) 246 | return input_value 247 | 248 | 249 | def get_field_names_and_options(pb): 250 | """ 251 | Return a tuple of field names and options. 252 | """ 253 | desc = pb.DESCRIPTOR 254 | 255 | for field in desc.fields: 256 | field_name = field.name 257 | options_dict = {} 258 | if field.has_options: 259 | options = field.GetOptions() 260 | for subfield, value in options.ListFields(): 261 | options_dict[subfield.name] = value 262 | yield field, field_name, options_dict 263 | 264 | 265 | class FieldsMissing(ValueError): 266 | pass 267 | 268 | 269 | def validate_dict_for_required_pb_fields(pb, dic): 270 | """ 271 | Validate that the dictionary has all the required fields for creating a protobuffer object 272 | from pb class. If a field is missing, raise FieldsMissing. 273 | In order to mark a field as optional, add [(is_optional) = true] to the field. 274 | Take a look at the tests for an example. 275 | """ 276 | missing_fields = [] 277 | for field, field_name, field_options in get_field_names_and_options(pb): 278 | if not field_options.get('is_optional', False) and field_name not in dic: 279 | missing_fields.append(field_name) 280 | if missing_fields: 281 | raise FieldsMissing('Missing fields: {}'.format(', '.join(missing_fields))) 282 | 283 | -------------------------------------------------------------------------------- /cf_api/routes_util.py: -------------------------------------------------------------------------------- 1 | import os 2 | import re 3 | import json 4 | import cf_api 5 | from . import exceptions as exc 6 | 7 | try: 8 | from urllib.parse import urlparse 9 | except ImportError: 10 | from urlparse import urlparse 11 | 12 | _app_to_fqdns = {} 13 | _url_to_routes = {} 14 | _name_to_domains = {} 15 | _domain_to_names = {} 16 | _url_to_app = {} 17 | _app_to_route_fqdns = {} 18 | 19 | 20 | def get_default_fqdn(cc, app_id=None, app_resource=None): 21 | """Gets the default fully qualified domain name for an application. 22 | 23 | Args: 24 | cc (cf_api.CloudController) 25 | app_id (str): app GUID 26 | app_resource (cf_api.Resource): app Resource object 27 | """ 28 | 29 | if not isinstance(cc, cf_api.CloudController): 30 | raise exc.InvalidArgsException( 31 | 'cc must be an instance of cf_api.CloudController', 500) 32 | 33 | if app_resource: 34 | if not isinstance(app_resource, cf_api.Resource): 35 | raise exc.InvalidArgsException( 36 | 'app_resource must be an instance of cf_api.Resource', 500) 37 | app_id = app_resource.guid 38 | 39 | if not app_id: 40 | raise exc.InvalidStateException( 41 | 'app_id OR app_resource is required', 500) 42 | 43 | if app_id in _app_to_fqdns: 44 | return _app_to_fqdns[app_id] 45 | 46 | if not app_resource: 47 | res = cc.apps(app_id).get() 48 | app_resource = res.resource 49 | 50 | res = cc.request(app_resource.routes_url).get() 51 | route = res.resource 52 | if not route: 53 | raise exc.NotFoundException( 54 | 'No route found for app {0}'.format(app_resource.name), 404) 55 | 56 | res = cc.request(route['entity']['domain_url']).get() 57 | domain = res.resource 58 | if not domain: 59 | raise exc.NotFoundException( 60 | 'No domain found for route {0}'.format(route['entity']['host']), 61 | 404) 62 | 63 | _app_to_fqdns[app_id] = '.'.join([route['entity']['host'], domain.name]) 64 | 65 | return _app_to_fqdns[app_id] 66 | 67 | 68 | def get_app_fqdns(cc, app_id, use_cache=True): 69 | """Get all FQDNs for and app id. 70 | 71 | Args: 72 | cc (cf_api.CloudController) 73 | app_id (str) 74 | use_cache (bool) 75 | 76 | Returns: 77 | list 78 | """ 79 | if not isinstance(cc, cf_api.CloudController): 80 | raise exc.InvalidArgsException( 81 | 'cc must be an instance of cf_api.CloudController', 500) 82 | 83 | global _app_to_route_fqdns 84 | if app_id in _app_to_route_fqdns: 85 | return _app_to_route_fqdns[app_id] 86 | 87 | res = cc.apps(app_id, 'routes').get() 88 | if res.has_error: 89 | raise exc.ResponseException(res.text, res.response.status_code) 90 | 91 | fqdns = [] 92 | for route in res.resources: 93 | if route.domain_guid in _domain_to_names and use_cache: 94 | domain = _domain_to_names[route.domain_guid] 95 | else: 96 | res = cc.request(route.domain_url).get() 97 | domain = res.resource 98 | _domain_to_names[route.domain_guid] = domain 99 | 100 | fqdns.append('.'.join([route.host, domain.name])) 101 | 102 | _app_to_route_fqdns[app_id] = fqdns 103 | 104 | return fqdns 105 | 106 | 107 | def decompose_route(url): 108 | """Decomposes a URL into the parts relevant to looking up a route with the 109 | Cloud Controller. 110 | 111 | Args: 112 | url (str): some URL to an app hosted in Cloud Foundry 113 | 114 | Returns: 115 | tuple[str]: four parts, 'host', 'domain', 'port', 'path'. Note that 116 | 'host' is the first dot-segment of the hostname and 'domain' is the 117 | remainder of the hostname 118 | """ 119 | if not re.search('^https?://', url): 120 | url = '://'.join(['https', url]) 121 | 122 | url_parts = urlparse(url) 123 | 124 | domain_parts = url_parts.hostname.split('.') 125 | if len(domain_parts) <= 2: 126 | raise exc.InvalidArgsException('URL does not contain a subdomain', 400) 127 | 128 | host, domain = domain_parts[0], '.'.join(domain_parts[1:]) 129 | return host, domain, url_parts.port, url_parts.path 130 | 131 | 132 | def compose_route(host, domain, port, path): 133 | """Builds a string representation of the route components. 134 | 135 | Args: 136 | host (str): the first dot-segment of the hostname 137 | domain (str): the remainder of the hostname after the first dot-segment 138 | port (int|str): port number if applicable. If it's a Falsy value, then 139 | it will be ignored 140 | path (str): if there's no specific path, then just set '/' 141 | 142 | Returns: 143 | str: the form is 'host.domain(:port)?/(path)?' 144 | """ 145 | netloc = ['.'.join([host, domain])] 146 | if port: 147 | netloc.append(str(port)) 148 | return '/'.join([':'.join(netloc), path]) 149 | 150 | 151 | def get_route_str(url): 152 | return compose_route(*decompose_route(url)) 153 | 154 | 155 | def get_route_from_url(cc, url, 156 | use_cache=True, 157 | ignore_path=False, 158 | ignore_port=False, 159 | require_one=True): 160 | """Gets all routes that are associated with the given URL. 161 | 162 | Args: 163 | cc (cf_api.CloudController): initialized Cloud Controller instance 164 | url (str): some URL to an app hosted in Cloud Foundry 165 | use_cache (bool): use the internal caching mechanism to speed up 166 | route/domain lookups 167 | ignore_port (bool): indicates to ignore the URL port when looking up 168 | the route 169 | ignore_path (bool): indicates to ignore the URL path when looking up 170 | the route 171 | require_one (bool): assert that there is only one route belonging to 172 | this URL and throw an error if there is more than one, else returns 173 | that one directly 174 | 175 | Returns: 176 | cf_api.Resource|list[cf_api.Resource]: If require_one=False, then a 177 | list is returned, else the route resource object is returned 178 | """ 179 | if not isinstance(cc, cf_api.CloudController): 180 | raise exc.InvalidArgsException( 181 | 'cc must be an instance of cf_api.CloudController', 500) 182 | 183 | host, domain, port, path = decompose_route(url) 184 | url = compose_route(host, domain, port, path) 185 | 186 | global _url_to_routes 187 | if url in _url_to_routes and use_cache: 188 | routes = _url_to_routes[url] 189 | 190 | else: 191 | global _name_to_domains 192 | if domain in _name_to_domains and use_cache: 193 | domain_guid = _name_to_domains[domain] 194 | else: 195 | res = cc.shared_domains().get_by_name(domain) 196 | if not res.resource: 197 | res = cc.private_domains().get_by_name(domain) 198 | if not res.resource: 199 | raise exc.NotFoundException( 200 | 'No domain found for name {0}'.format(domain), 404) 201 | domain_guid = res.resource.guid 202 | _name_to_domains[domain] = domain_guid 203 | 204 | route_search = ['host', host, 'domain_guid', domain_guid] 205 | if not ignore_path and '/' != path: 206 | route_search.extend(['path', path]) 207 | if not ignore_port and port: 208 | route_search.extend(['port', port]) 209 | 210 | res = cc.routes().search(*route_search) 211 | if not res.resource: 212 | raise exc.NotFoundException( 213 | 'No route found for host {0}'.format(host), 404) 214 | 215 | routes = res.resources 216 | if not ignore_path and not ignore_port: 217 | _url_to_routes[url] = routes 218 | 219 | if require_one: 220 | if len(routes) != 1: 221 | raise exc.InvalidStateException( 222 | 'More than one route was found when one was required', 500) 223 | return routes[0] 224 | 225 | return routes 226 | 227 | 228 | def get_route_apps_from_url(cc, url, use_cache=True, require_one=True, 229 | started_only=True): 230 | """Get apps belonging to a URL hosted in Cloud Foundry. If you set 231 | require_one=True and start_only=False, then be aware that if there is an 232 | app in the STOPPED state in addition to the STARTED state attached to the 233 | underlying route, you'll get an exception. 234 | 235 | Args: 236 | cc (cf_api.CloudController): initialized Cloud Controller instance 237 | url (str): some URL to an app hosted in Cloud Foundry 238 | use_cache (bool): use the internal caching mechanism to speed up 239 | route/domain lookups 240 | require_one (bool): assert that there is only one app belonging to this 241 | URL and throw an error if there is more than one, else returns that 242 | one directly 243 | started_only (bool): indicates to limit the search to apps with the 244 | state of 'STARTED' 245 | 246 | Returns: 247 | cf_api.Resource|list[cf_api.Resource]: If require_one=False, then a 248 | list is returned, else the app resource object is returned 249 | """ 250 | global _url_to_app 251 | url_key = get_route_str(url) 252 | 253 | if url_key in _url_to_app and use_cache: 254 | apps = _url_to_app[url_key] 255 | 256 | else: 257 | route = get_route_from_url(cc, url, use_cache=use_cache, 258 | require_one=require_one) 259 | res = cc.request(route.apps_url).get() 260 | apps = res.resources 261 | _url_to_app[url_key] = apps 262 | 263 | if started_only: 264 | apps = [r for r in apps if 'STARTED' == r.state] 265 | 266 | if require_one: 267 | if len(apps) != 1: 268 | raise exc.InvalidStateException( 269 | 'More than one app was found when one was required', 500) 270 | return apps[0] 271 | 272 | return apps 273 | 274 | 275 | if '__main__' == __name__: 276 | def main(): 277 | import argparse 278 | from getpass import getpass 279 | 280 | args = argparse.ArgumentParser( 281 | description='This tool performs a reverse lookup of a Cloud ' 282 | 'Foundry route or an application based on a given URL ' 283 | 'belonging to that route or application. It accepts ' 284 | 'multiple URLs and returns a JSON object with keys ' 285 | 'corresponding to the sanitized URLs passed in and ' 286 | 'values showing the requested routes/apps. By ' 287 | 'default it looks up routes.') 288 | args.add_argument( 289 | '--cloud-controller', dest='cloud_controller', required=True, 290 | help='The Cloud Controller API endpoint ' 291 | '(excluding leading slashes)') 292 | args.add_argument( 293 | '-u', '--user', dest='user', 294 | help='The user used to authenticate. This may be omitted ' 295 | 'if --client-id and --client-secret have sufficient ' 296 | 'authorization to perform the desired request without a ' 297 | 'user\'s permission') 298 | args.add_argument( 299 | '--client-id', dest='client_id', default='cf', 300 | help='Used to set a custom client ID') 301 | args.add_argument( 302 | '--client-secret', dest='client_secret', default='', 303 | help='Secret corresponding to --client-id') 304 | args.add_argument( 305 | '--skip-ssl', dest='skip_ssl', action='store_true', 306 | help='Indicates to skip SSL cert verification') 307 | args.add_argument( 308 | '--show-apps', dest='show_apps', action='store_true', 309 | help='Lookup the apps related to the given URL. ' 310 | 'By default it looks up the routes') 311 | args.add_argument( 312 | '--ignore-path', dest='ignore_path', action='store_true', 313 | help='Indicates to ignore the path when looking up the app/route') 314 | args.add_argument( 315 | '--ignore-port', dest='ignore_port', action='store_true', 316 | help='Indicates to ignore the port when looking up the app/route') 317 | args.add_argument( 318 | '--no-require-one', dest='no_require_one', action='store_true', 319 | help='Indicates that there may be more than one route. By default ' 320 | 'this tool expects one route and will throw an error if ' 321 | 'there is more than one') 322 | args.add_argument( 323 | 'url', nargs='+', 324 | help='URLs to be looked up to find their routes/apps') 325 | args = args.parse_args() 326 | 327 | if args.user: 328 | username = args.user 329 | password = getpass().strip() 330 | refresh_token = None 331 | else: 332 | username = None 333 | password = None 334 | refresh_token = os.getenv('CF_REFRESH_TOKEN') 335 | 336 | cc = cf_api.new_cloud_controller( 337 | args.cloud_controller, 338 | username=username, 339 | password=password, 340 | refresh_token=refresh_token, 341 | client_id=args.client_id, 342 | client_secret=args.client_secret, 343 | verify_ssl=not args.skip_ssl 344 | ) 345 | 346 | results = {} 347 | for url in args.url: 348 | if args.show_apps: 349 | results[get_route_str(url)] = get_route_apps_from_url( 350 | cc, url, require_one=not args.no_require_one) 351 | else: 352 | results[get_route_str(url)] = get_route_from_url( 353 | cc, url, 354 | ignore_path=args.ignore_path, 355 | ignore_port=args.ignore_port, 356 | require_one=not args.no_require_one) 357 | 358 | print(json.dumps(results, indent=4)) 359 | 360 | main() 361 | -------------------------------------------------------------------------------- /cf_api/ssh_util.py: -------------------------------------------------------------------------------- 1 | from __future__ import print_function 2 | import os 3 | from paramiko import SSHClient 4 | from paramiko.client import MissingHostKeyPolicy 5 | from . import exceptions as exc 6 | import cf_api 7 | 8 | 9 | class ProxyPolicy(MissingHostKeyPolicy): 10 | def __init__(self, *args, **kwargs): 11 | proxy = kwargs['proxy'] 12 | del kwargs['proxy'] 13 | super(ProxyPolicy, self).__init__(*args, **kwargs) 14 | self.fingerprint = proxy.fingerprint 15 | if 22 != proxy.port: 16 | self.host = ''.join(['[', proxy.host, ']:', str(proxy.port)]) 17 | else: 18 | self.host = proxy.host 19 | 20 | def missing_host_key(self, client, hostname, key): 21 | fingerprint = ':'.join([ 22 | '{0:#0{1}x}'.format(i, 4).replace('0x', '') 23 | for i in list(bytearray(key.get_fingerprint()))]) 24 | if self.host == hostname and self.fingerprint == fingerprint: 25 | return True 26 | raise Exception('Unknown host key fingerprint') 27 | 28 | 29 | class SSHSession(object): 30 | def __init__(self, ssh_proxy, app_guid, instance_index, password=None): 31 | self.ssh_proxy = ssh_proxy 32 | self.app_guid = app_guid 33 | self.instance_index = instance_index 34 | self.password = password 35 | self.client = SSHClient() 36 | self.client.set_missing_host_key_policy(ProxyPolicy(proxy=ssh_proxy)) 37 | 38 | @property 39 | def username(self): 40 | return ''.join(['cf:', str(self.app_guid), 41 | '/', str(self.instance_index)]) 42 | 43 | def authenticate(self): 44 | self.password = self.ssh_proxy.uaa.one_time_password( 45 | self.ssh_proxy.client_id) 46 | return self 47 | 48 | def open(self, **kwargs): 49 | if self.password is None: 50 | raise exc.InvalidStateException( 51 | 'Can\'t open ssh session without a password. ' 52 | 'Please authenticate first.') 53 | kwargs['username'] = self.username 54 | kwargs['password'] = self.password 55 | kwargs['port'] = self.ssh_proxy.port 56 | self.client.connect( 57 | self.ssh_proxy.host, 58 | **kwargs 59 | ) 60 | 61 | def close(self): 62 | self.client.close() 63 | 64 | def execute(self, command): 65 | return self.client.exec_command(command) 66 | 67 | 68 | if '__main__' == __name__: 69 | def main(): 70 | import sys 71 | import argparse 72 | 73 | args = argparse.ArgumentParser() 74 | args.add_argument('--cloud-controller', required=True) 75 | args.add_argument('--guid', required=True) 76 | args.add_argument('-i', dest='index', required=True) 77 | args.add_argument('-c', '--command', dest='command', required=True) 78 | args.add_argument('--stderr', action='store_true', required=False) 79 | args = args.parse_args() 80 | rt = os.getenv('CF_REFRESH_TOKEN') 81 | cc = cf_api.new_cloud_controller( 82 | args.cloud_controller, 83 | refresh_token=rt, 84 | client_id='cf', 85 | client_secret='' 86 | ) 87 | ssh = SSHSession(cc.ssh_proxy, args.guid, args.index) 88 | ssh.authenticate() 89 | 90 | try: 91 | ssh.open(allow_agent=False, look_for_keys=False) 92 | si, so, se = ssh.execute(args.command) 93 | for line in so: 94 | sys.stdout.write(line) 95 | if args.stderr: 96 | for line in se: 97 | sys.stderr.write(line) 98 | finally: 99 | ssh.close() 100 | 101 | main() 102 | -------------------------------------------------------------------------------- /docs/.gitignore: -------------------------------------------------------------------------------- 1 | _build 2 | *.pyc 3 | -------------------------------------------------------------------------------- /docs/Makefile: -------------------------------------------------------------------------------- 1 | # Minimal makefile for Sphinx documentation 2 | # 3 | 4 | # You can set these variables from the command line. 5 | SPHINXOPTS = 6 | SPHINXBUILD = sphinx-build 7 | SPHINXPROJ = cf-api 8 | SOURCEDIR = . 9 | BUILDDIR = _build 10 | 11 | # Put it first so that "make" without argument is like "make help". 12 | help: 13 | @$(SPHINXBUILD) -M help "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O) 14 | 15 | .PHONY: help Makefile 16 | 17 | # Catch-all target: route all unknown targets to Sphinx using the new 18 | # "make mode" option. $(O) is meant as a shortcut for $(SPHINXOPTS). 19 | %: Makefile 20 | @$(SPHINXBUILD) -M $@ "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O) -------------------------------------------------------------------------------- /docs/api.rst: -------------------------------------------------------------------------------- 1 | .. _api: 2 | 3 | API 4 | === 5 | 6 | cf_api 7 | ------ 8 | 9 | .. module:: cf_api 10 | 11 | .. autoclass:: CloudController 12 | :members: 13 | 14 | .. autoclass:: CloudControllerRequest 15 | :members: 16 | 17 | .. autoclass:: CloudControllerResponse 18 | :members: 19 | 20 | .. autoclass:: UAA 21 | :members: 22 | 23 | .. autoclass:: Resource 24 | :members: 25 | 26 | .. autoclass:: JWT 27 | :members: 28 | 29 | .. autoclass:: Doppler 30 | :members: 31 | 32 | .. autoclass:: CFInfo 33 | :members: 34 | 35 | .. autoclass:: SSHProxy 36 | :members: 37 | 38 | .. autofunction:: new_uaa 39 | 40 | .. autofunction:: new_cloud_controller 41 | 42 | .. autofunction:: new_doppler 43 | 44 | cf_api.deploy_manifest 45 | ---------------------- 46 | 47 | .. automodule:: cf_api.deploy_manifest 48 | 49 | .. autoclass:: Deploy 50 | :members: 51 | 52 | cf_api.deploy_service 53 | --------------------- 54 | 55 | .. automodule:: cf_api.deploy_service 56 | 57 | .. autoclass:: DeployService 58 | :members: 59 | 60 | cf_api.deploy_space 61 | ------------------- 62 | 63 | .. automodule:: cf_api.deploy_space 64 | 65 | .. autoclass:: Space 66 | :members: 67 | 68 | cf_api.deploy_blue_green 69 | ------------------------ 70 | 71 | .. automodule:: cf_api.deploy_blue_green 72 | 73 | .. autoclass:: BlueGreen 74 | :members: 75 | 76 | cf_api.dropsonde_util 77 | --------------------- 78 | 79 | .. automodule:: cf_api.dropsonde_util 80 | :members: 81 | 82 | cf_api.exceptions 83 | ----------------- 84 | 85 | .. automodule:: cf_api.exceptions 86 | :members: 87 | 88 | cf_api.logs_util 89 | ---------------- 90 | 91 | .. automodule:: cf_api.logs_util 92 | :members: 93 | 94 | cf_api.routes_util 95 | ------------------ 96 | 97 | .. automodule:: cf_api.routes_util 98 | :members: 99 | 100 | cf_api.ssh_util 101 | --------------- 102 | 103 | .. automodule:: cf_api.ssh_util 104 | :members: 105 | -------------------------------------------------------------------------------- /docs/conf.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # 3 | # cf-api documentation build configuration file, created by 4 | # sphinx-quickstart on Wed Dec 6 10:27:19 2017. 5 | # 6 | # This file is execfile()d with the current directory set to its 7 | # containing dir. 8 | # 9 | # Note that not all possible configuration values are present in this 10 | # autogenerated file. 11 | # 12 | # All configuration values have a default; values that are commented out 13 | # serve to show the default. 14 | 15 | # If extensions (or modules to document with autodoc) are in another directory, 16 | # add these directories to sys.path here. If the directory is relative to the 17 | # documentation root, use os.path.abspath to make it absolute, like shown here. 18 | # 19 | import re 20 | import os 21 | import sys 22 | sys.path.insert(0, os.path.abspath('..')) 23 | from cf_api import __version__ 24 | 25 | 26 | # -- General configuration ------------------------------------------------ 27 | 28 | # If your documentation needs a minimal Sphinx version, state it here. 29 | # 30 | # needs_sphinx = '1.0' 31 | 32 | # Add any Sphinx extension module names here, as strings. They can be 33 | # extensions coming with Sphinx (named 'sphinx.ext.*') or your custom 34 | # ones. 35 | extensions = [ 36 | 'sphinx.ext.autodoc', 37 | 'sphinx.ext.coverage', 38 | 'sphinx.ext.viewcode', 39 | ] 40 | 41 | # Add any paths that contain templates here, relative to this directory. 42 | templates_path = ['_templates'] 43 | 44 | # The suffix(es) of source filenames. 45 | # You can specify multiple suffix as a list of string: 46 | # 47 | # source_suffix = ['.rst', '.md'] 48 | source_suffix = '.rst' 49 | 50 | # The master toctree document. 51 | master_doc = 'index' 52 | 53 | # General information about the project. 54 | project = u'cf-api' 55 | copyright = u'2017' 56 | author = u'Adam Jaso, Jr.' 57 | 58 | # The version info for the project you're documenting, acts as replacement for 59 | # |version| and |release|, also used in various other places throughout the 60 | # built documents. 61 | # 62 | # The short X.Y version. 63 | version = __version__.__semver__ 64 | # The full version, including alpha/beta/rc tags. 65 | release = __version__.__version__ 66 | 67 | # The language for content autogenerated by Sphinx. Refer to documentation 68 | # for a list of supported languages. 69 | # 70 | # This is also used if you do content translation via gettext catalogs. 71 | # Usually you set "language" from the command line for these cases. 72 | language = None 73 | 74 | # List of patterns, relative to source directory, that match files and 75 | # directories to ignore when looking for source files. 76 | # This patterns also effect to html_static_path and html_extra_path 77 | exclude_patterns = ['_build', 'Thumbs.db', '.DS_Store'] 78 | 79 | # The name of the Pygments (syntax highlighting) style to use. 80 | pygments_style = 'sphinx' 81 | 82 | # If true, `todo` and `todoList` produce output, else they produce nothing. 83 | todo_include_todos = False 84 | 85 | 86 | # -- Options for HTML output ---------------------------------------------- 87 | 88 | # The theme to use for HTML and HTML Help pages. See the documentation for 89 | # a list of builtin themes. 90 | # 91 | html_theme = 'alabaster' 92 | 93 | # Theme options are theme-specific and customize the look and feel of a theme 94 | # further. For a list of options available for each theme, see the 95 | # documentation. 96 | # 97 | html_theme_options = { 98 | 'logo_name': True, 99 | } 100 | 101 | # Add any paths that contain custom static files (such as style sheets) here, 102 | # relative to this directory. They are copied after the builtin static files, 103 | # so a file named "default.css" will overwrite the builtin "default.css". 104 | html_static_path = ['_static'] 105 | 106 | # Custom sidebar templates, must be a dictionary that maps document names 107 | # to template names. 108 | # 109 | # This is required for the alabaster theme 110 | # refs: http://alabaster.readthedocs.io/en/latest/installation.html#sidebars 111 | html_sidebars = { 112 | '**': [ 113 | 'globaltoc.html', 114 | 'searchbox.html', 115 | ] 116 | } 117 | 118 | 119 | # -- Options for HTMLHelp output ------------------------------------------ 120 | 121 | # Output file base name for HTML help builder. 122 | htmlhelp_basename = 'cf-apidoc' 123 | 124 | 125 | # -- Options for LaTeX output --------------------------------------------- 126 | 127 | latex_elements = { 128 | # The paper size ('letterpaper' or 'a4paper'). 129 | # 130 | # 'papersize': 'letterpaper', 131 | 132 | # The font size ('10pt', '11pt' or '12pt'). 133 | # 134 | # 'pointsize': '10pt', 135 | 136 | # Additional stuff for the LaTeX preamble. 137 | # 138 | # 'preamble': '', 139 | 140 | # Latex figure (float) alignment 141 | # 142 | # 'figure_align': 'htbp', 143 | } 144 | 145 | # Grouping the document tree into LaTeX files. List of tuples 146 | # (source start file, target name, title, 147 | # author, documentclass [howto, manual, or own class]). 148 | latex_documents = [ 149 | (master_doc, 'cf-api.tex', u'cf-api Documentation', 150 | u'Adam Jaso, Jr.', 'manual'), 151 | ] 152 | 153 | 154 | # -- Options for manual page output --------------------------------------- 155 | 156 | # One entry per manual page. List of tuples 157 | # (source start file, name, description, authors, manual section). 158 | man_pages = [ 159 | (master_doc, 'cf-api', u'cf-api Documentation', 160 | [author], 1) 161 | ] 162 | 163 | 164 | # -- Options for Texinfo output ------------------------------------------- 165 | 166 | # Grouping the document tree into Texinfo files. List of tuples 167 | # (source start file, target name, title, author, 168 | # dir menu entry, description, category) 169 | texinfo_documents = [ 170 | (master_doc, 'cf-api', u'cf-api Documentation', 171 | author, 'cf-api', 'One line description of project.', 172 | 'Miscellaneous'), 173 | ] 174 | 175 | 176 | 177 | -------------------------------------------------------------------------------- /docs/examples.rst: -------------------------------------------------------------------------------- 1 | .. _examples: 2 | 3 | Examples 4 | ======== 5 | 6 | The following examples demonstrate the use cases of this library. 7 | 8 | Initializing the Cloud Controller client 9 | ---------------------------------------- 10 | 11 | Create a new cloud controller client 12 | 13 | .. code-block:: python 14 | 15 | import cf_api 16 | 17 | cc = cf_api.new_cloud_controller( 18 | 'https://api.yourcloudfoundry.com', 19 | client_id='cf', 20 | client_secret='', 21 | username='myuser', 22 | password='uaapassword', 23 | ) 24 | 25 | print(cc) 26 | 27 | This code authenticates the user with UAA using the given client and user 28 | credentials and, if authentication is successful, returns a Cloud Controller 29 | request builder. 30 | 31 | Making API requests with the Cloud Controller client 32 | ---------------------------------------------------- 33 | 34 | To make a request to the Cloud Controller API 35 | 36 | .. code-block:: python 37 | 38 | import cf_api 39 | 40 | cc = cf_api.new_cloud_controller(...) 41 | 42 | request = cc.request('apps') 43 | response = request.get() 44 | 45 | print(response.data) 46 | 47 | This code uses a cloud controller client object to create a new request for 48 | the Cloud Controller API endpoint ``/v2/apps``, execute that request as an 49 | HTTP GET request and print out a dictionary representation of the response. 50 | 51 | .. note:: 52 | 53 | Observe that the ``cc.request()`` method returns a 54 | :class:`~cf_api.CloudControllerRequest` object and does **NOT** execute the 55 | request. This allows the user to choose which HTTP method they want to execute. 56 | 57 | The Request Object 58 | ------------------ 59 | 60 | Since the ``cc`` variable is an instance of the 61 | :class:`~cf_api.CloudController` class, it is a "RequestFactory" instance, 62 | which provides a method, :meth:`~cf_api.CloudController.request`, that produces 63 | a "Request" object that is preset with the HTTP headers and base url from 64 | the "RequestFactory" that produced it. This "Request" object has 65 | several methods named for the HTTP verbs such as 66 | :meth:`~cf_api.CloudControllerRequest.get` for GET, 67 | :meth:`~cf_api.CloudControllerRequest.post` for POST, 68 | :meth:`~cf_api.CloudControllerRequest.delete` for DELETE. You can set any query 69 | parameters, body parameters, or headers on this "Request" object and the 70 | parent "RequestFactory" will not be modified. 71 | 72 | The :class:`~cf_api.CloudController` class provides many methods that are 73 | convenience methods named after their respective Cloud Controller API 74 | endpoints, such as :meth:`~cf_api.CloudController.organizations`, 75 | :meth:`~cf_api.CloudController.spaces`, and 76 | :meth:`~cf_api.CloudController.apps` which simply invoke 77 | :meth:`~cf_api.CloudController.request` with single arguments of 78 | ``organizations``, ``spaces``, and ``apps``. This 79 | :meth:`~cf_api.CloudController.request` method accepts a list of strings which 80 | are joined to make a URL which is joined to the base URL of the 81 | "RequestFactory", like so: 82 | 83 | .. code-block:: python 84 | 85 | req = cc.request('organizations', org_guid, 'spaces') 86 | 87 | The above example produces a "Request" object with a URL set to 88 | ``cloud_controller_url/v2/organizations//spaces``. 89 | 90 | An equivalent way to represent the above URL is as follows 91 | 92 | .. code-block:: python 93 | 94 | req = cc.organizations(org_guid, 'spaces') 95 | 96 | Executing the request object produces a 97 | :class:`~cf_api.CloudControllerResponse` object. To execute the request as an 98 | HTTP GET request, do 99 | 100 | .. code-block:: python 101 | 102 | res = req.get() 103 | 104 | Searching with the API endpoints 105 | ++++++++++++++++++++++++++++++++ 106 | 107 | The ``v2`` Cloud Controller API supports the same query syntax for every 108 | endpoint it provides. This query syntax is 109 | ``q=:``. 110 | The :class:`~cf_api.CloudControllerRequest` class provides a function, 111 | :meth:`~cf_api.CloudControllerRequest.search`, that accepts a list of string 112 | arguments in the form 113 | 114 | .. code-block:: python 115 | 116 | org_name = 'myorg' 117 | space_guid = 'xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx' 118 | res = req.search('name', org_name, 'space_guid', space_guid) 119 | 120 | This syntax sets the internal query parameters as follows 121 | ``q=name:&q=space_guid:``. 122 | 123 | Further query parameters may also be set using the keyword arguments, like so 124 | 125 | .. code-block:: python 126 | 127 | org_name = 'myorg' 128 | res = req.search('name', org_name, **{'results-per-page': 10}) 129 | 130 | As searches by ``q=name:`` are quite common, a further convenience 131 | function is provided in the :meth:`~cf_api.CloudControllerRequest.get_by_name` 132 | method, which is used like so 133 | 134 | .. code-block:: python 135 | 136 | org_name = 'myorg' 137 | res = req.get_by_name(org_name) 138 | 139 | Debugging a request 140 | +++++++++++++++++++ 141 | 142 | The request object also exposes a method, 143 | :meth:`~cf_api.CloudControllerRequest.get_requests_args`, which, when invoked, 144 | returns the exact parameters that will be passed to the ``python-requests`` 145 | module to execute the Cloud Controller API request. The result is a tuple 146 | containing: 147 | 148 | 1. the ``python-requests`` function that will be used (chosen based on the 149 | HTTP method set in the request object, i.e. ``requests.get``, etc) 150 | 2. the relative URL that will be invoked 151 | 3. the keyword arguments that will be passed into the ``python-requests`` 152 | function. 153 | 154 | .. note:: 155 | 156 | The HTTP method **MUST** be set in order for this function to work 157 | correctly. You may get an exception if the method is not set! Often 158 | times the method is not set until the request is invoked with one of 159 | the functions that maps to the HTTP verbs (i.e. ``.get()`` or ``.post()``) 160 | and so, in order to view the request exactly as it would be executed, 161 | you must set the request method prior to invoking this function, using 162 | the :meth:`~cf_api.CloudControllerRequest.set_method` function. 163 | 164 | .. code-block:: python 165 | 166 | print(req.set_query(...)\ 167 | .set_header(...)\ 168 | .set_params(...)\ 169 | .set_method('POST')\ 170 | .get_requests_args()) 171 | 172 | The Response Object 173 | ------------------- 174 | 175 | The response object from an executed API request object is an instance of 176 | :class:`~cf_api.CloudControllerResponse` class. This class has a few members 177 | worth noting. 178 | 179 | To access the response data as a simple :meth:`~dict`, use the 180 | :attr:`~cf_api.CloudControllerResponse.data` attribute, like so 181 | 182 | .. code-block:: python 183 | 184 | res = req.get() 185 | print(res.data) 186 | 187 | This :attr:`~cf_api.CloudControllerResponse.data` attribute internally checks 188 | if the request was successful (HTTP 200 range) and returns a simple 189 | :class:`~dict` if successful, or throws an :class:`~exceptions.Exception` 190 | if there was an error. 191 | 192 | You can check if an error occurred using 193 | :attr:`~cf_api.CloudControllerResponse.has_error`. You can get the error 194 | code and message using :attr:`~cf_api.CloudControllerResponse.error_code` and 195 | :attr:`~cf_api.CloudControllerResponse.error_message`. 196 | 197 | The ``v2`` Cloud Controller API returns all its data types in the same 198 | :class:`~cf_api.Resource` format, whether a list of results or a single result 199 | object. 200 | 201 | If you're searching for a single org by name using, for example, the 202 | ``organizations`` endpoint, you can reference the first result (assuming there 203 | is only one result) like so 204 | 205 | .. code-block:: python 206 | 207 | org_name = 'myorg' 208 | req = cc.organizations().search('name', org_name) 209 | res = req.get() 210 | my_org = res.resource 211 | 212 | This works when requesting an organization by ID as well, like so 213 | 214 | .. code-block:: python 215 | 216 | org_guid = 'xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx' 217 | req = cc.organizations(org_guid) 218 | res = req.get() 219 | my_org = res.resource 220 | 221 | If your request returns a list of resources, then you can iterate over them 222 | using the :attr:`~cf_api.CloudControllerResponse.resources` attribute, like so 223 | 224 | .. code-block:: python 225 | 226 | res = cc.organizations().get() 227 | for org in res.resources: 228 | print(org) 229 | 230 | If you want to access the raw response, use the 231 | :attr:`~cf_api.CloudControllerResponse.raw_data` attribute to get a simple dict. 232 | Accessing this attribute does not throw an error. 233 | 234 | If you want to access the underlying ``python-requests`` Response object, you 235 | can use the :attr:`~cf_api.CloudControllerResponse.response` attribute. 236 | 237 | Getting all pages of a given resource list 238 | ------------------------------------------ 239 | 240 | To retrieve a paginated list of items, for example, a list of organizations. 241 | 242 | .. code-block:: python 243 | 244 | import cf_api 245 | 246 | cc = cf_api.new_cloud_controller(...) 247 | req = cc.organizations() 248 | orgs = cc.get_all_resources(req) 249 | 250 | .. note:: 251 | 252 | Note that you **MUST NOT** execute HTTP GET method on the request. 253 | You must pass the prepared request object into the 254 | :meth:`~cf_api.CloudController.get_all_resources` method. This 255 | ``get_all_resources()`` method will internally execute an HTTP GET 256 | and store the results while ``next_url`` attribute is set on the 257 | response. Once there is no ``next_url`` the ``get_all_resources()`` method 258 | will return the aggregated list of resources. 259 | 260 | Further examples 261 | ================ 262 | 263 | The following examples implement some common use cases for this library. 264 | 265 | You can find the code for these examples in the ``examples`` directory. 266 | 267 | Logging in 268 | ---------- 269 | 270 | The following examples demonstrate usage of the four grant types for 271 | authentication with UAA. 272 | 273 | Grant type: password 274 | ++++++++++++++++++++ 275 | 276 | .. literalinclude:: ../examples/cf_login.py 277 | :language: python 278 | 279 | Grant type: authorization code 280 | ++++++++++++++++++++++++++++++ 281 | 282 | .. literalinclude:: ../examples/authenticate_with_authorization_code.py 283 | :language: python 284 | 285 | Grant type: client credentials 286 | ++++++++++++++++++++++++++++++ 287 | 288 | .. literalinclude:: ../examples/authenticate_with_client_credentials.py 289 | :language: python 290 | 291 | Grant type: refresh token 292 | +++++++++++++++++++++++++ 293 | 294 | .. literalinclude:: ../examples/authenticate_with_refresh_token.py 295 | :language: python 296 | 297 | Listing Organizations 298 | --------------------- 299 | 300 | Think ``cf orgs``. 301 | 302 | .. literalinclude:: ../examples/cf_orgs.py 303 | :language: python 304 | 305 | Listing Spaces 306 | -------------- 307 | 308 | Think ``cf spaces``. 309 | 310 | .. literalinclude:: ../examples/cf_spaces.py 311 | :language: python 312 | 313 | Listing Applications 314 | -------------------- 315 | 316 | Think ``cf apps``. 317 | 318 | This example shows how to list applications in a space using the standard 319 | functions in :mod:`~cf_api` 320 | 321 | .. literalinclude:: ../examples/cf_apps_core.py 322 | :language: python 323 | 324 | This example shows how to list applications in a space using the 325 | :mod:`~cf_api.deploy_space.Space` helper class. 326 | 327 | .. literalinclude:: ../examples/cf_apps_simple.py 328 | :language: python 329 | 330 | Deploying Applications in a space with a manifest (``cf push``) 331 | --------------------------------------------------------------- 332 | 333 | This example shows how to deploy an application using the standard functions 334 | in :mod:`~cf_api` 335 | 336 | .. literalinclude:: ../examples/cf_push_core.py 337 | :language: python 338 | 339 | This example shows how to deploy an applcation using the 340 | :class:`~cf_api.deploy_space.Space` helper class. 341 | 342 | .. literalinclude:: ../examples/cf_push_simple.py 343 | :language: python 344 | 345 | Deploying Applications in a space with a manifest with no downtime 346 | ------------------------------------------------------------------ 347 | 348 | This example uses the :class:`~cf_api.deploy_blue_green.BlueGreen` 349 | helper class. 350 | 351 | .. literalinclude:: ../examples/cf_push_blue_green.py 352 | :language: python 353 | 354 | Creating a service in a space 355 | ----------------------------- 356 | 357 | .. literalinclude:: ../examples/cf_create_service.py 358 | :language: python 359 | 360 | Tailing application logs 361 | ------------------------ 362 | 363 | This example shows how to tail an application's logs using the standard 364 | functions in :mod:`~cf_api` 365 | 366 | .. literalinclude:: ../examples/cf_logs_core.py 367 | :language: python 368 | 369 | This example shows how to deploy an applcation using the 370 | :mod:`~cf_api.deploy_space.Space` helper class. 371 | 372 | .. literalinclude:: ../examples/cf_logs_simple.py 373 | :language: python 374 | 375 | Looking up an application by its FQDN 376 | -------------------------------------- 377 | 378 | If you have a CF app's domain name and you're not sure where it lives you can 379 | find it using this example. 380 | 381 | .. literalinclude:: ../examples/find_app_by_route.py 382 | :language: python 383 | -------------------------------------------------------------------------------- /docs/index.rst: -------------------------------------------------------------------------------- 1 | .. cf-api documentation master file, created by 2 | sphinx-quickstart on Wed Dec 6 10:27:19 2017. 3 | You can adapt this file completely to your liking, but it should at least 4 | contain the root `toctree` directive. 5 | 6 | Welcome to cf-api's documentation! 7 | ================================== 8 | 9 | .. toctree:: 10 | :maxdepth: 2 11 | 12 | api 13 | examples 14 | 15 | The cf-api library provides a pure Python interface to the Cloud Foundry APIs. Supported features include the following: 16 | 17 | - `Authenticated Cloud Controller HTTP request builder `_ 18 | - `UAA OAuth2 implementations for all grant types `_ 19 | - `Support for deploying Cloud Foundry applications from an application manifest `_ 20 | - `Helper for deploying Cloud Foundry services `_ 21 | - `Helper for accessing resources within a given Cloud Foundry space `_ 22 | - `Authenticated Doppler websocket client `_ 23 | - `Authenticated application instance SSH client `_ 24 | 25 | Authenticated Cloud Controller HTTP request builder 26 | --------------------------------------------------- 27 | 28 | The Cloud Controller API contains a great number of possible endpoints and requires OAuth2 authentication. Both of which 29 | make a "full" implementation of the APIs challenging. 30 | 31 | Therefore, in the interest of maintainability and conciseness, this library does *not* support a 32 | "Python function for every endpoint" scheme, but rather provides the user an HTTP request builder object with which to 33 | construct and send all the HTTP path, headers, parameters, etc that a given Cloud Controller endpoint requires. 34 | 35 | Additionally, the library provides a functionality to handle the UAA OAuth2 authentication internally and return an authenticated 36 | request builder object, from which other authenticated HTTP requests may be constructed. 37 | 38 | Learn more `here `_. 39 | 40 | UAA OAuth2 implementations for all grant types 41 | ---------------------------------------------- 42 | 43 | The Cloud Foundry UAA endpoints support most (if not all) of the OAuth2 grant types, and this library provides two possible 44 | ways of accessing UAA. 45 | 46 | - A set of specially implemented functions to handle the authentication 47 | - An HTTP request object builder alike to the Cloud Controller request object builder 48 | 49 | The ``password``, ``authorization_code`` (including ``code`` and ``implicit``), ``client_credentials``, and ``refresh_token`` 50 | grant types are supported. 51 | 52 | Learn more `here `_. 53 | 54 | Support for deploying Cloud Foundry applications from an application manifest 55 | ----------------------------------------------------------------------------- 56 | 57 | Using the Cloud Controller API request builder, this library implements most of the Cloud Foundry application manifest 58 | YAML configuration parameters. This provides a Python interface to fully deploy (or delete) Cloud Foundry applications using the same 59 | manifest as used with ``cf push``. 60 | 61 | Learn more `here `_. 62 | 63 | Helper for deploying Cloud Foundry services 64 | ------------------------------------------- 65 | 66 | Using the Cloud Controller API request builder, this library implements a Python interface that simplifies creating a service. This 67 | functionality removes the tedium of looking up the service by name, looking up the plan by name, looking up the service instance by name 68 | checking if the service exists already, and then building the request to create (or destroy) it. 69 | 70 | Learn more `here `_. 71 | 72 | Helper for accessing resources within a given Cloud Foundry space 73 | ----------------------------------------------------------------- 74 | 75 | This is a helper class that makes requests relative to a given Cloud Foundry space. This is useful when you need to interact 76 | with a specific Cloud Foundry space and don't want to pass around space guid when searching for entities in the space 77 | (i.e. apps, service instances, routes, etc.) 78 | 79 | Learn more `here `_. 80 | 81 | Authenticated Doppler websocket client 82 | -------------------------------------- 83 | 84 | This provides a simple Websocket client to the Doppler API and allows handles the parsing CF protobuf log messages. This can be useful 85 | for either monitoring application logs (i.e. ``cf logs``) or subscribing to the main loggregator firehose. 86 | 87 | Learn more `here `_. 88 | 89 | Authenticated application instance SSH client 90 | --------------------------------------------- 91 | 92 | This provides basic SSH authentication and a session with the shell of a Cloud Foundry application instance. 93 | 94 | Learn more `here `_. 95 | -------------------------------------------------------------------------------- /docs/manifest.yml: -------------------------------------------------------------------------------- 1 | name: cf-api-docs 2 | path: _build/html 3 | stack: cflinuxfs2 4 | disk_quota: 256M 5 | memory: 256M 6 | buildpack: https://github.com/cloudfoundry/staticfile-buildpack 7 | -------------------------------------------------------------------------------- /examples/authenticate_with_authorization_code.py: -------------------------------------------------------------------------------- 1 | """Log with UAA web login page (using grant_type "authorization_code") 2 | 3 | The use case for this grant type is for websites that want to 4 | "Log in with UAA". 5 | 6 | This example is more of snippets, since it requires redirecting to UAA and 7 | then receiving the authorization code on your web server... 8 | """ 9 | from __future__ import print_function 10 | import os 11 | import sys 12 | import json 13 | import cf_api 14 | from webbrowser import open_new 15 | from BaseHTTPServer import HTTPServer, BaseHTTPRequestHandler 16 | 17 | 18 | PORT = int(os.getenv('PORT', 8080)) 19 | 20 | 21 | def browser_authorize(auth_uri): 22 | """Opens the UAA login page in the default web browser to allow the user 23 | to login, then waits for UAA to redirect back to http://localhost:8080, 24 | and then, then captures the authorization code and verifies it with UAA, 25 | and finally displays the login info. 26 | """ 27 | 28 | # open the UAA login page in the web browser 29 | open_new(auth_uri) 30 | 31 | class CodeHandler(BaseHTTPRequestHandler): 32 | def do_GET(self): 33 | self.send_response(200) 34 | self.send_header('Content-Type', 'text/html') 35 | self.end_headers() 36 | self.wfile.write(""" 37 | 38 | 39 | 40 | Hi... and bye! (you may close this window) 41 | 42 | """) 43 | parts = self.path.split('=') 44 | if len(parts) < 2: 45 | raise Exception('invalid response {0}'.format(self.path)) 46 | auth_code = parts[1] 47 | self.server.result = auth_code 48 | 49 | # create a server to handle the redirected authorization code from UAA 50 | server = HTTPServer(('', PORT), CodeHandler) 51 | 52 | # this method waits for a single HTTP request and then shuts down the 53 | # server 54 | server.handle_request() 55 | 56 | return server.result 57 | 58 | 59 | print('----------') 60 | cloud_controller_url = raw_input('cloud controller url: ').strip() 61 | client_id = 'test-client-id' 62 | client_secret = 'test-client-secret' 63 | 64 | print('----------') 65 | print('Redirecting to UAA...') 66 | # we create an instance of the cloud controller, but tell it to NOT authorize 67 | # with UAA. 68 | cc_noauth = cf_api.new_cloud_controller( 69 | cloud_controller_url, 70 | client_id=client_id, 71 | client_secret=client_secret, 72 | no_auth=True 73 | ) 74 | 75 | # we use noauth client to create the redirect URI 76 | uaa_uri = cc_noauth.uaa.authorization_code_url('code') 77 | 78 | # get the authorization code by logging in at the web browser, receiving 79 | # the redirect, and extracting the authorization code 80 | code = browser_authorize(uaa_uri) 81 | print('authorization code: ' + str(code)) 82 | 83 | print('----------') 84 | print('Verifying authorization code...') 85 | # we create a UAA authenticated client using the authorization code by passing 86 | # in the "authorization_code" keyword argument 87 | cc = cf_api.new_cloud_controller( 88 | cloud_controller_url, 89 | client_id=client_id, 90 | client_secret=client_secret, 91 | authorization_code=dict( 92 | code=code, 93 | response_type='code', 94 | ) 95 | ) 96 | print('Login OK!') 97 | 98 | print('----------') 99 | access_token = cc.uaa.get_access_token() 100 | refresh_token = cc.uaa.get_refresh_token() 101 | print('access_token: ' + access_token.to_string() + '\n') 102 | print('refresh_token: ' + refresh_token.to_string() + '\n') 103 | print('user_id: ' + access_token.user_id + '\n') 104 | print('user_name: ' + access_token.user_name + '\n') 105 | print('access_token_data:') 106 | json.dump(access_token.attrs, sys.stdout, indent=2) 107 | print() 108 | -------------------------------------------------------------------------------- /examples/authenticate_with_client_credentials.py: -------------------------------------------------------------------------------- 1 | """Log in with UAA client credentials (using grant_type "client_credentials") 2 | """ 3 | from __future__ import print_function 4 | import sys 5 | import json 6 | import cf_api 7 | from getpass import getpass 8 | 9 | 10 | print('----------') 11 | # cloud_controller_url = 'https://api.changeme.com' 12 | cloud_controller_url = raw_input('cloud controller url: ').strip() 13 | client_id = raw_input('client id: ').strip() 14 | client_secret = getpass('client secret: ').strip() 15 | 16 | print('----------') 17 | print('Authenticating with UAA...') 18 | cc = cf_api.new_cloud_controller( 19 | cloud_controller_url, 20 | client_id=client_id, 21 | client_secret=client_secret, 22 | ) 23 | print('Login OK!') 24 | 25 | print('----------') 26 | access_token = cc.uaa.get_access_token() 27 | print('access_token: ' + str(access_token) + '\n') 28 | print('access_token_data:') 29 | json.dump(access_token.attrs, sys.stdout, indent=2) 30 | print() 31 | -------------------------------------------------------------------------------- /examples/authenticate_with_refresh_token.py: -------------------------------------------------------------------------------- 1 | """Log in with UAA refresh token (using grant_type "refresh_token") 2 | 3 | The if the user has CF CLI installed this script checks for 4 | ``~/.cf/config.json`` and reads the refresh token from there, otherwise, 5 | the user is asked to enter client credentials and the refresh token 6 | """ 7 | from __future__ import print_function 8 | import os 9 | import sys 10 | import json 11 | import cf_api 12 | from getpass import getpass 13 | 14 | 15 | cloud_controller_url = raw_input('cloud controller url: ').strip() 16 | config_file = os.path.expanduser('~/.cf/config.json') 17 | print('----------') 18 | if os.path.isfile(config_file): 19 | print('Loading refresh token from ~/.cf/config.json ...') 20 | with open(config_file) as f: 21 | config = json.load(f) 22 | refresh_token = config['RefreshToken'] 23 | client_id = 'cf' 24 | client_secret = '' 25 | else: 26 | client_id = raw_input('client id: ').strip() 27 | client_secret = getpass('client secret: ').strip() 28 | refresh_token = raw_input('refresh token: ').strip() 29 | 30 | print('----------') 31 | print('Authenticating with UAA...') 32 | cc = cf_api.new_cloud_controller( 33 | cloud_controller_url, 34 | client_id=client_id, 35 | client_secret=client_secret, 36 | refresh_token=refresh_token, 37 | ) 38 | 39 | print('----------') 40 | access_token = cc.uaa.get_access_token() 41 | refresh_token = cc.uaa.get_refresh_token() 42 | print('access_token: ' + access_token.to_string() + '\n') 43 | print('refresh_token: ' + refresh_token.to_string() + '\n') 44 | print('user_id: ' + access_token.user_id + '\n') 45 | print('user_name: ' + access_token.user_name + '\n') 46 | print('access_token_data:') 47 | json.dump(access_token.attrs, sys.stdout, indent=2) 48 | print() 49 | -------------------------------------------------------------------------------- /examples/cf_apps_core.py: -------------------------------------------------------------------------------- 1 | """Searches for apps in a space in an organization on the Cloud Controller API 2 | """ 3 | from __future__ import print_function 4 | import sys 5 | import json 6 | import cf_api 7 | from getpass import getpass 8 | 9 | 10 | print('----------') 11 | # cloud_controller_url = 'https://api.changeme.com' 12 | cloud_controller_url = raw_input('cloud controller url: ').strip() 13 | username = raw_input('username: ').strip() 14 | password = getpass('password: ').strip() 15 | 16 | print('----------') 17 | print('Authenticating with UAA...') 18 | cc = cf_api.new_cloud_controller( 19 | cloud_controller_url, 20 | client_id='cf', # the ``cf`` command uses this client and the secret below 21 | client_secret='', 22 | username=username, 23 | password=password, 24 | ) 25 | print('Login OK!') 26 | print('----------') 27 | 28 | organization_name = raw_input('organization name: ').strip() 29 | # see http://apidocs.cloudfoundry.org/280/organizations/list_all_organizations.html 30 | # for an explanation of the query parameters 31 | print('Searching for organization "{0}"...'.format(organization_name)) 32 | req = cc.request('organizations').set_query(q='name:' + organization_name) 33 | res = req.get() 34 | 35 | print(str(res.response.status_code) + ' ' + res.response.reason) 36 | print('----------') 37 | 38 | if res.has_error: 39 | print(str(res.error_code) + ': ' + str(res.error_message)) 40 | sys.exit(1) 41 | 42 | space_name = raw_input('space name: ').strip() 43 | # see http://apidocs.cloudfoundry.org/280/spaces/list_all_spaces.html 44 | # for an explanation of the query parameters 45 | print('Searching for space "{0}"...'.format(space_name)) 46 | spaces_url = res.resource.spaces_url 47 | req = cc.request(spaces_url).set_query(q='name:' + space_name) 48 | res = req.get() 49 | 50 | print(str(res.response.status_code) + ' ' + res.response.reason) 51 | print('----------') 52 | 53 | if res.has_error: 54 | print(str(res.error_code) + ': ' + str(res.error_message)) 55 | sys.exit(1) 56 | 57 | print('Searching for apps in "{0} / {1}"...'.format( 58 | organization_name, space_name)) 59 | first_space = res.resource 60 | space_apps_url = first_space.apps_url 61 | req = cc.request(space_apps_url) 62 | res = cc.get_all_resources(req) 63 | 64 | print('----------') 65 | json.dump(res, sys.stdout, indent=2) 66 | print() 67 | -------------------------------------------------------------------------------- /examples/cf_apps_simple.py: -------------------------------------------------------------------------------- 1 | """Searches for apps in a space in an organization on the Cloud Controller API 2 | """ 3 | from __future__ import print_function 4 | import sys 5 | import json 6 | import cf_api 7 | from cf_api.deploy_space import Space 8 | from getpass import getpass 9 | 10 | 11 | print('----------') 12 | # cloud_controller_url = 'https://api.changeme.com' 13 | cloud_controller_url = raw_input('cloud controller url: ').strip() 14 | username = raw_input('username: ').strip() 15 | password = getpass('password: ').strip() 16 | 17 | print('----------') 18 | print('Authenticating with UAA...') 19 | cc = cf_api.new_cloud_controller( 20 | cloud_controller_url, 21 | client_id='cf', # the ``cf`` command uses this client and the secret below 22 | client_secret='', 23 | username=username, 24 | password=password, 25 | ) 26 | print('Login OK!') 27 | 28 | print('----------') 29 | org_name = raw_input('organization name: ').strip() 30 | space_name = raw_input('space name: ').strip() 31 | print('Looking up "{0} / {1}"...'.format(org_name, space_name)) 32 | space = Space(cc, org_name=org_name, space_name=space_name) 33 | print('Found space!') 34 | 35 | print('----------') 36 | print('Searching for apps in "{0} / {1}"...'.format(org_name, space_name)) 37 | apps = space.request('apps').get().data 38 | print('Found apps!') 39 | 40 | print('----------') 41 | json.dump(apps, sys.stdout, indent=2) 42 | print() 43 | -------------------------------------------------------------------------------- /examples/cf_create_service.py: -------------------------------------------------------------------------------- 1 | """Searches for service instances in a space in an organization on the 2 | Cloud Controller API 3 | """ 4 | from __future__ import print_function 5 | import sys 6 | import json 7 | import cf_api 8 | from cf_api.deploy_space import Space 9 | from getpass import getpass 10 | 11 | 12 | print('----------') 13 | # cloud_controller_url = 'https://api.changeme.com' 14 | cloud_controller_url = raw_input('cloud controller url: ').strip() 15 | username = raw_input('username: ').strip() 16 | password = getpass('password: ').strip() 17 | 18 | print('----------') 19 | print('Authenticating with UAA...') 20 | cc = cf_api.new_cloud_controller( 21 | cloud_controller_url, 22 | client_id='cf', # the ``cf`` command uses this client and the secret below 23 | client_secret='', 24 | username=username, 25 | password=password, 26 | ) 27 | print('Login OK!') 28 | print('----------') 29 | 30 | org_name = raw_input('organization name: ').strip() 31 | space_name = raw_input('space name: ').strip() 32 | 33 | space = Space(cc, org_name=org_name, space_name=space_name) 34 | 35 | service_name = raw_input('service type: ').strip() 36 | service_plan = raw_input('service plan: ').strip() 37 | service_instance_name = raw_input('service name: ').strip() 38 | service = space.get_deploy_service() 39 | res = service.create(service_instance_name, service_name, service_plan) 40 | print('----------') 41 | json.dump(res, sys.stdout, indent=2) 42 | print() 43 | -------------------------------------------------------------------------------- /examples/cf_login.py: -------------------------------------------------------------------------------- 1 | """Log in with your UAA user credentials (using grant_type "password") 2 | 3 | This example mimics the CF login action and uses the same "cf" client that 4 | the CF CLI uses. 5 | """ 6 | from __future__ import print_function 7 | import sys 8 | import json 9 | import cf_api 10 | from getpass import getpass 11 | 12 | 13 | print('----------') 14 | # cloud_controller_url = 'https://api.changeme.com' 15 | cloud_controller_url = raw_input('cloud controller url: ').strip() 16 | username = raw_input('username: ').strip() 17 | password = getpass('password: ').strip() 18 | 19 | print('----------') 20 | print('Authenticating with UAA...') 21 | cc = cf_api.new_cloud_controller( 22 | cloud_controller_url, 23 | client_id='cf', # the ``cf`` command uses this client and the secret below 24 | client_secret='', 25 | username=username, 26 | password=password, 27 | ) 28 | print('Login OK!') 29 | 30 | print('----------') 31 | access_token = cc.uaa.get_access_token() 32 | refresh_token = cc.uaa.get_refresh_token() 33 | print('access_token: ' + access_token.to_string() + '\n') 34 | print('refresh_token: ' + refresh_token.to_string() + '\n') 35 | print('user_id: ' + access_token.user_id + '\n') 36 | print('user_name: ' + access_token.user_name + '\n') 37 | print('access_token_data:') 38 | json.dump(access_token.attrs, sys.stdout, indent=2) 39 | print() 40 | -------------------------------------------------------------------------------- /examples/cf_logs_core.py: -------------------------------------------------------------------------------- 1 | """Tails application logs like ``cf logs`` 2 | 3 | This example shows how to use the core :module:`~cf_api` module to tail 4 | the logs of an application. 5 | """ 6 | from __future__ import print_function 7 | import sys 8 | from getpass import getpass 9 | import cf_api 10 | from cf_api.dropsonde_util import DopplerEnvelope 11 | 12 | 13 | print('----------') 14 | # cloud_controller_url = 'https://api.changeme.com' 15 | cloud_controller_url = raw_input('cloud controller url: ').strip() 16 | username = raw_input('username: ').strip() 17 | password = getpass('password: ').strip() 18 | 19 | print('----------') 20 | print('Authenticating with UAA...') 21 | cc = cf_api.new_cloud_controller( 22 | cloud_controller_url, 23 | client_id='cf', # the ``cf`` command uses this client and the secret below 24 | client_secret='', 25 | username=username, 26 | password=password, 27 | ) 28 | print('Login OK!') 29 | 30 | print('----------') 31 | org_name = raw_input('organization name: ').strip() 32 | res = cc.organizations().get_by_name(org_name) 33 | print(str(res.response.status_code) + ' ' + res.response.reason) 34 | if res.has_error: 35 | print(str(res.error_code) + ': ' + str(res.error_message)) 36 | sys.exit(1) 37 | 38 | print('----------') 39 | space_name = raw_input('space name: ').strip() 40 | res = cc.request(res.resource.spaces_url).get_by_name(space_name) 41 | print(str(res.response.status_code) + ' ' + res.response.reason) 42 | if res.has_error: 43 | print(str(res.error_code) + ': ' + str(res.error_message)) 44 | sys.exit(1) 45 | 46 | print('----------') 47 | app_name = raw_input('app name: ').strip() 48 | res = cc.request(res.resource.apps_url).get_by_name(app_name) 49 | print(str(res.response.status_code) + ' ' + res.response.reason) 50 | if res.has_error: 51 | print(str(res.error_code) + ': ' + str(res.error_message)) 52 | sys.exit(1) 53 | 54 | print('----------') 55 | websocket = cc.doppler.ws_request('apps', res.resource.guid, 'stream') 56 | websocket.connect() 57 | print('Connected and tailing logs for "{0}" in "{1} / {2}"!'.format( 58 | org_name, space_name, app_name)) 59 | 60 | print('----------') 61 | 62 | def render_log(msg): 63 | d = DopplerEnvelope.wrap(msg) 64 | sys.stdout.write(''.join([str(d), '\n'])) 65 | sys.stdout.flush() 66 | 67 | websocket.watch(render_log) 68 | -------------------------------------------------------------------------------- /examples/cf_logs_simple.py: -------------------------------------------------------------------------------- 1 | """Tails application logs like ``cf logs`` 2 | """ 3 | from __future__ import print_function 4 | import sys 5 | import cf_api 6 | from getpass import getpass 7 | from cf_api.dropsonde_util import DopplerEnvelope 8 | from cf_api.deploy_space import Space 9 | 10 | 11 | print('----------') 12 | # cloud_controller_url = 'https://api.changeme.com' 13 | cloud_controller_url = raw_input('cloud controller url: ').strip() 14 | username = raw_input('username: ').strip() 15 | password = getpass('password: ').strip() 16 | 17 | print('----------') 18 | print('Authenticating with UAA...') 19 | cc = cf_api.new_cloud_controller( 20 | cloud_controller_url, 21 | client_id='cf', # the ``cf`` command uses this client and the secret below 22 | client_secret='', 23 | username=username, 24 | password=password, 25 | ) 26 | print('Login OK!') 27 | 28 | print('----------') 29 | org_name = raw_input('organization name: ').strip() 30 | space_name = raw_input('space name: ').strip() 31 | print('Looking up org space "{0} / {1}"...'.format(org_name, space_name)) 32 | space = Space(cc, org_name=org_name, space_name=space_name, is_debug=True) 33 | print('Found space!') 34 | 35 | print('----------') 36 | app_name = raw_input('app name: ').strip() 37 | print('Looking up app "{0}" in "{1} / {2}"...' 38 | .format(app_name, org_name, space_name)) 39 | app = space.get_app_by_name(app_name) 40 | print('Found app!') 41 | 42 | print('----------') 43 | print('Connecting to app log stream "{0}"...'.format(app_name)) 44 | websocket = cc.doppler.ws_request('apps', app.guid, 'stream') 45 | websocket.connect() 46 | print('Connected and tailing logs for "{0}" in "{1} / {2}"!'.format( 47 | org_name, space_name, app_name)) 48 | 49 | print('----------') 50 | 51 | def render_log(msg): 52 | d = DopplerEnvelope.wrap(msg) 53 | sys.stdout.write(''.join([str(d), '\n'])) 54 | sys.stdout.flush() 55 | 56 | websocket.watch(render_log) 57 | -------------------------------------------------------------------------------- /examples/cf_orgs.py: -------------------------------------------------------------------------------- 1 | """Searches for an organization by name on the Cloud Controller API 2 | """ 3 | from __future__ import print_function 4 | import sys 5 | import json 6 | import cf_api 7 | from getpass import getpass 8 | 9 | 10 | print('----------') 11 | # cloud_controller_url = 'https://api.changeme.com' 12 | cloud_controller_url = raw_input('cloud controller url: ').strip() 13 | username = raw_input('username: ').strip() 14 | password = getpass('password: ').strip() 15 | 16 | print('----------') 17 | print('Authenticating with UAA...') 18 | cc = cf_api.new_cloud_controller( 19 | cloud_controller_url, 20 | client_id='cf', # the ``cf`` command uses this client and the secret below 21 | client_secret='', 22 | username=username, 23 | password=password, 24 | ) 25 | print('Login OK!') 26 | print('----------') 27 | 28 | print('Searching for organizations...') 29 | req = cc.request('organizations') 30 | resources_list = cc.get_all_resources(req) 31 | 32 | print('----------') 33 | json.dump(resources_list, sys.stdout, indent=2) 34 | print() 35 | -------------------------------------------------------------------------------- /examples/cf_push_blue_green.py: -------------------------------------------------------------------------------- 1 | """Runs a Blue Green deploy of a Cloud Foundry application using a manifest 2 | """ 3 | from __future__ import print_function 4 | import os 5 | import sys 6 | import json 7 | import cf_api 8 | from cf_api.deploy_manifest import Deploy 9 | from cf_api.deploy_space import Space 10 | from getpass import getpass 11 | 12 | 13 | print('----------') 14 | # cloud_controller_url = 'https://api.changeme.com' 15 | cloud_controller_url = raw_input('cloud controller url: ').strip() 16 | username = raw_input('username: ').strip() 17 | password = getpass('password: ').strip() 18 | 19 | print('----------') 20 | print('Authenticating with UAA...') 21 | cc = cf_api.new_cloud_controller( 22 | cloud_controller_url, 23 | client_id='cf', # the ``cf`` command uses this client and the secret below 24 | client_secret='', 25 | username=username, 26 | password=password, 27 | ) 28 | print('Login OK!') 29 | 30 | print('----------') 31 | org_name = raw_input('organization name: ').strip() 32 | space_name = raw_input('space name: ').strip() 33 | print('Looking up "{0} / {1}"...'.format(org_name, space_name)) 34 | space = Space(cc, org_name=org_name, space_name=space_name, is_debug=True) 35 | print('Found space!') 36 | 37 | print('----------') 38 | manifest_path = raw_input('manifest path: ').strip() 39 | manifest_path = os.path.abspath(manifest_path) 40 | 41 | space.deploy_blue_green(manifest_path) 42 | print('Deployed {0} successfully!'.format(app_name)) 43 | -------------------------------------------------------------------------------- /examples/cf_push_core.py: -------------------------------------------------------------------------------- 1 | """Deploys a Cloud Foundry application using a manifest 2 | """ 3 | from __future__ import print_function 4 | import os 5 | import sys 6 | import json 7 | import cf_api 8 | from cf_api.deploy_manifest import Deploy 9 | from getpass import getpass 10 | 11 | 12 | print('----------') 13 | # cloud_controller_url = 'https://api.changeme.com' 14 | cloud_controller_url = raw_input('cloud controller url: ').strip() 15 | username = raw_input('username: ').strip() 16 | password = getpass('password: ').strip() 17 | 18 | print('----------') 19 | print('Authenticating with UAA...') 20 | cc = cf_api.new_cloud_controller( 21 | cloud_controller_url, 22 | client_id='cf', # the ``cf`` command uses this client and the secret below 23 | client_secret='', 24 | username=username, 25 | password=password, 26 | ) 27 | print('Login OK!') 28 | print('----------') 29 | 30 | organization_name = raw_input('organization name: ').strip() 31 | # see http://apidocs.cloudfoundry.org/280/organizations/list_all_organizations.html 32 | # for an explanation of the query parameters 33 | print('Searching for organization "{0}"...'.format(organization_name)) 34 | req = cc.request('organizations').set_query(q='name:' + organization_name) 35 | res = req.get() 36 | print(str(res.response.status_code) + ' ' + res.response.reason) 37 | print('----------') 38 | if res.has_error: 39 | print(str(res.error_code) + ': ' + str(res.error_message)) 40 | sys.exit(1) 41 | 42 | space_name = raw_input('space name: ').strip() 43 | # see http://apidocs.cloudfoundry.org/280/spaces/list_all_spaces.html 44 | # for an explanation of the query parameters 45 | print('Searching for space...') 46 | spaces_url = res.resource.spaces_url 47 | req = cc.request(spaces_url).set_query(q='name:' + space_name) 48 | res = req.get() 49 | print(str(res.response.status_code) + ' ' + res.response.reason) 50 | print('----------') 51 | if res.has_error: 52 | print(str(res.error_code) + ': ' + str(res.error_message)) 53 | sys.exit(1) 54 | 55 | manifest_path = raw_input('manifest path: ').strip() 56 | manifest_path = os.path.abspath(manifest_path) 57 | 58 | app_entries = Deploy.parse_manifest(manifest_path, cc) 59 | for app_entry in app_entries: 60 | app_entry.set_org_and_space(organization_name, space_name) 61 | app_entry.set_debug(True) 62 | app_entry.push() 63 | app_entry.wait_for_app_start(tailing=True) 64 | 65 | print('Deployed {0} apps successfully!'.format(len(app_entries))) 66 | -------------------------------------------------------------------------------- /examples/cf_push_simple.py: -------------------------------------------------------------------------------- 1 | """Deploys a Cloud Foundry application using a manifest 2 | """ 3 | from __future__ import print_function 4 | import os 5 | import sys 6 | import json 7 | import cf_api 8 | from cf_api.deploy_manifest import Deploy 9 | from cf_api.deploy_space import Space 10 | from getpass import getpass 11 | 12 | 13 | print('----------') 14 | # cloud_controller_url = 'https://api.changeme.com' 15 | cloud_controller_url = raw_input('cloud controller url: ').strip() 16 | username = raw_input('username: ').strip() 17 | password = getpass('password: ').strip() 18 | 19 | print('----------') 20 | print('Authenticating with UAA...') 21 | cc = cf_api.new_cloud_controller( 22 | cloud_controller_url, 23 | client_id='cf', # the ``cf`` command uses this client and the secret below 24 | client_secret='', 25 | username=username, 26 | password=password, 27 | ) 28 | print('Login OK!') 29 | 30 | print('----------') 31 | org_name = raw_input('organization name: ').strip() 32 | space_name = raw_input('space name: ').strip() 33 | print('Looking up "{0} / {1}"...'.format(org_name, space_name)) 34 | space = Space(cc, org_name=org_name, space_name=space_name, is_debug=True) 35 | print('Found space!') 36 | 37 | print('----------') 38 | manifest_path = raw_input('manifest path: ').strip() 39 | manifest_path = os.path.abspath(manifest_path) 40 | 41 | app_entries = space.get_deploy_manifest(manifest_path) 42 | for app_entry in app_entries: 43 | app_entry.push() 44 | app_entry.wait_for_app_start(tailing=True) 45 | 46 | print('Deployed {0} apps successfully!'.format(len(app_entries))) 47 | -------------------------------------------------------------------------------- /examples/cf_spaces.py: -------------------------------------------------------------------------------- 1 | """Searches for spaces in an organization by name on the Cloud Controller API 2 | """ 3 | from __future__ import print_function 4 | import sys 5 | import json 6 | import cf_api 7 | from getpass import getpass 8 | 9 | 10 | print('----------') 11 | # cloud_controller_url = 'https://api.changeme.com' 12 | cloud_controller_url = raw_input('cloud controller url: ').strip() 13 | username = raw_input('username: ').strip() 14 | password = getpass('password: ').strip() 15 | 16 | print('----------') 17 | print('Authenticating with UAA...') 18 | cc = cf_api.new_cloud_controller( 19 | cloud_controller_url, 20 | client_id='cf', # the ``cf`` command uses this client and the secret below 21 | client_secret='', 22 | username=username, 23 | password=password, 24 | ) 25 | print('Login OK!') 26 | 27 | print('----------') 28 | organization_name = raw_input('organization name: ').strip() 29 | # see http://apidocs.cloudfoundry.org/280/organizations/list_all_organizations.html 30 | # for an explanation of the query parameters 31 | print('Searching for organization "{0}"...'.format(organization_name)) 32 | req = cc.request('organizations').set_query(q='name:' + organization_name) 33 | res = req.get() 34 | print(str(res.response.status_code) + ' ' + res.response.reason) 35 | if res.has_error: 36 | print(str(res.error_code) + ': ' + str(res.error_message)) 37 | sys.exit(1) 38 | 39 | print('----------') 40 | print('Searching for spaces in "{0}"...'.format(organization_name)) 41 | first_org = res.resource 42 | org_spaces_url = first_org.spaces_url 43 | req = cc.request(org_spaces_url) 44 | resources_list = cc.get_all_resources(req) 45 | 46 | print('----------') 47 | json.dump(resources_list, sys.stdout, indent=2) 48 | print() 49 | -------------------------------------------------------------------------------- /examples/extend_cloud_controller.py: -------------------------------------------------------------------------------- 1 | from __future__ import print_function 2 | import cf_api 3 | from getpass import getpass 4 | 5 | 6 | class MyUAA(cf_api.UAA): 7 | pass 8 | 9 | 10 | class MyCloudController(cf_api.CloudController): 11 | pass 12 | 13 | 14 | print('----------') 15 | # cloud_controller_url = 'https://api.changeme.com' 16 | cloud_controller_url = raw_input('cloud controller url: ').strip() 17 | username = raw_input('username: ').strip() 18 | password = getpass('password: ').strip() 19 | 20 | print('----------') 21 | print('Authenticating with UAA...') 22 | cc = MyCloudController.new_instance( 23 | base_url=cloud_controller_url, 24 | username=username, 25 | password=password, 26 | client_id='cf', 27 | client_secret='', 28 | uaa_class=MyUAA, 29 | ) 30 | print('Login OK!') 31 | 32 | print('----------') 33 | print('cc isinstance of MyCloudController?', isinstance(cc, MyCloudController)) 34 | print('cc.uaa isinstance of MyUAA?', isinstance(cc.uaa, MyUAA)) 35 | print() 36 | -------------------------------------------------------------------------------- /examples/find_app_by_route.py: -------------------------------------------------------------------------------- 1 | """Look up an application resource by it's route. 2 | (i.e. host.domain(:port)?(/path)? ) 3 | """ 4 | from __future__ import print_function 5 | import sys 6 | import cf_api 7 | import json 8 | from cf_api import routes_util 9 | from getpass import getpass 10 | 11 | 12 | print('----------') 13 | # cloud_controller_url = 'https://api.changeme.com' 14 | cloud_controller_url = raw_input('cloud controller url: ').strip() 15 | username = raw_input('username: ').strip() 16 | password = getpass('password: ').strip() 17 | 18 | print('----------') 19 | print('Authenticating with UAA...') 20 | cc = cf_api.new_cloud_controller( 21 | cloud_controller_url, 22 | client_id='cf', # the ``cf`` command uses this client and the secret below 23 | client_secret='', 24 | username=username, 25 | password=password, 26 | ) 27 | print('Login OK!') 28 | 29 | print('----------') 30 | route_url = raw_input('route url: ').strip() # myapp.changme.com:4443/v2 31 | 32 | print('----------') 33 | apps = routes_util.get_route_apps_from_url(cc, route_url) 34 | 35 | print('----------') 36 | json.dump(apps, sys.stdout, indent=2) 37 | print() 38 | -------------------------------------------------------------------------------- /requirements-dev.txt: -------------------------------------------------------------------------------- 1 | nose2 2 | responses 3 | -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | six 2 | PyYAML 3 | pycrypto 4 | PyJWT>=1.4.2,<2.0.0 5 | protobuf>=3.3.0 6 | requests-factory>=0.1.0,<1.0.0 7 | paramiko 8 | dropsonde 9 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | from setuptools import setup 2 | from os import path 3 | 4 | with open(path.join(path.dirname(__file__), 'requirements.txt')) as f: 5 | reqs = [l for l in f.read().strip().split('\n') if not l.startswith('-')] 6 | 7 | with open(path.join(path.dirname(__file__), 'version.txt')) as f: 8 | __version__ = f.read().strip() 9 | 10 | setup( 11 | name='cf_api', 12 | version=__version__, 13 | description='Python Interface for Cloud Foundry APIs', 14 | long_description=open('README.md').read(), 15 | long_description_content_type="text/markdown", 16 | license='Apache License Version 2.0', 17 | author='Adam Jaso', 18 | author_email='ajaso@hsdp.io', 19 | packages=['cf_api'], 20 | package_dir={ 21 | 'cf_api': 'cf_api', 22 | }, 23 | install_requires=reqs, 24 | url='https://github.com/hsdp/python-cf-api', 25 | ) 26 | -------------------------------------------------------------------------------- /test/mock_cc.py: -------------------------------------------------------------------------------- 1 | import os 2 | import six 3 | import json 4 | import time 5 | import jwt 6 | import responses 7 | from requests_factory import RequestFactory 8 | from uuid import uuid4 9 | 10 | cc_api_url = os.getenv('CC_API_URL', 'http://localhost') 11 | uaa_api_url = os.getenv('UAA_API_URL', 'http://localhost') 12 | 13 | 14 | relations_tree = { 15 | 'apps': [ 16 | 'routes', 17 | 'summary', 18 | 'service_bindings', 19 | 'env', 20 | 'permissions', 21 | 'instances', 22 | 'restage', 23 | ], 24 | 'buildpacks': [ 25 | 'bits', 26 | ], 27 | 'domains': [ 28 | 'spaces' 29 | ], 30 | 'events': [], 31 | 'info': [], 32 | 'jobs': [], 33 | 'quota_definitions': [], 34 | 'organizations': [ 35 | 'auditors', 36 | 'billing_managers', 37 | 'managers', 38 | 'private_domains', 39 | 'users', 40 | 'domains', 41 | 'services', 42 | 'space_quota_definitions', 43 | 'spaces', 44 | 'instance_usage', 45 | 'memory_usage', 46 | 'user_roles', 47 | ], 48 | 'private_domains': [ 49 | 'shared_organizations' 50 | ], 51 | 'resource_match': [], 52 | 'routes': [ 53 | 'apps', 54 | 'route_mappings', 55 | ], 56 | 'routes/reserved/domain': [ 57 | 'host', 58 | ], 59 | 'route_mappings': [], 60 | 'config/running_security_groups': [], 61 | 'config/staging_security_groups': [], 62 | 'security_groups': [ 63 | 'spaces', 64 | 'staging_spaces', 65 | ], 66 | 'service_bindings': [], 67 | 'service_brokers': [], 68 | 'service_instances': [ 69 | 'routes', 70 | 'service_bindings', 71 | 'service_keys', 72 | 'permissions', 73 | ], 74 | 'service_keys': [], 75 | 'service_plan_visibilities': [], 76 | 'service_plan': [ 77 | 'service_instances', 78 | ], 79 | 'services': [ 80 | 'service_plans', 81 | ], 82 | 'shared_domains': [], 83 | 'space_quota_definitions': [ 84 | 'spaces' 85 | ], 86 | 'spaces': [ 87 | 'auditors', 88 | 'developers', 89 | 'managers', 90 | 'security_groups', 91 | 'staging_security_groups', 92 | 'unmapped_routes', 93 | 'summary', 94 | 'apps', 95 | 'auditors', 96 | 'developers', 97 | 'domains', 98 | 'events', 99 | 'routes', 100 | 'security_groups', 101 | 'staging_security_groups', 102 | 'service_instances', 103 | 'services', 104 | 'user_roles', 105 | ], 106 | 'stacks': [], 107 | 'users': [ 108 | 'audited_organizations', 109 | 'audited_spaces', 110 | 'billing_managed_organizations', 111 | 'managed_organizations', 112 | 'managed_spaces', 113 | 'organizations', 114 | ], 115 | } 116 | 117 | relations_tree_v3 = { 118 | 'apps': { 119 | 'relationships': [ 120 | 'space', 121 | ], 122 | 'links': [ 123 | 'space', 124 | 'processes', 125 | 'packages', 126 | 'route_mappings', 127 | 'environment_variables', 128 | 'droplets', 129 | 'tasks', 130 | ] 131 | }, 132 | 'builds': { 133 | 'toplevel': [ 134 | 'package', 135 | 'droplet', 136 | ], 137 | 'links': [ 138 | 'build', 139 | 'app', 140 | ], 141 | }, 142 | 'buildpacks': { 143 | 'links': [ 144 | 'upload', 145 | ] 146 | }, 147 | 'domains': { 148 | 'relationships': [ 149 | 'organizations', 150 | ], 151 | 'links': [ 152 | 'organization', 153 | ], 154 | }, 155 | 'droplets': { 156 | 'links': [ 157 | 'package', 158 | 'app', 159 | ] 160 | }, 161 | 'isolation_segments': { 162 | 'links': [ 163 | 'organizations', 164 | ], 165 | }, 166 | 'jobs': {}, 167 | 'organizations': { 168 | 'links': [ 169 | 'domains', 170 | ], 171 | }, 172 | 'packages': {}, 173 | 'processes': { 174 | 'relationships': [ 175 | 'app', 176 | 'revision', 177 | ], 178 | 'links': [ 179 | 'app', 180 | 'space', 181 | 'stats' 182 | ] 183 | }, 184 | 'routes': { 185 | 'relationships': [ 186 | 'space', 187 | 'domain', 188 | ], 189 | 'links': [ 190 | 'space', 191 | 'domain', 192 | 'destinations', 193 | ], 194 | }, 195 | 'service_instances': { 196 | 'relationships': [ 197 | 'space', 198 | ], 199 | 'links': [ 200 | 'space', 201 | ], 202 | }, 203 | 'spaces': { 204 | 'relationships': [ 205 | 'organization', 206 | ], 207 | 'links': [ 208 | 'organization', 209 | ], 210 | }, 211 | 'stacks': {}, 212 | 'tasks': { 213 | 'relationships': [ 214 | 'app', 215 | ], 216 | 'links': [ 217 | 'app', 218 | 'droplet', 219 | ], 220 | }, 221 | } 222 | 223 | 224 | cc_v2_info = { 225 | "token_endpoint": uaa_api_url, 226 | "app_ssh_endpoint": "ssh.cf.com:2222", 227 | "app_ssh_host_key_fingerprint": "00:00:00:00:00:00:00:00:00:00:00:00:00:00:00:00", 228 | "app_ssh_oauth_client": "ssh-client-id", 229 | "doppler_logging_endpoint": "wss://doppler.cf.com:4443" 230 | } 231 | 232 | 233 | uaa_oauth_token = { 234 | 'access_token': 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJmb28iLCJ1c2VyX2lkIjoiMmIxNjM0MzctM2VkZC00ZDEwLTgwOGItNDRmZDIzODJhOTU4IiwiZXhwIjoxOTkxODY1ODQxfQ.SMrHg7o9Mv9_hT8GIrG8Rao5CPHumnOPO-KWD8BRz4k', 235 | 'refresh_token': 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJmb28iLCJ1c2VyX2lkIjoiMmIxNjM0MzctM2VkZC00ZDEwLTgwOGItNDRmZDIzODJhOTU4IiwiZXhwIjoxOTkxODY1ODQxfQ.SMrHg7o9Mv9_hT8GIrG8Rao5CPHumnOPO-KWD8BRz4k', 236 | } 237 | 238 | 239 | def make_uaa_oauth_token(ttl): 240 | secret = str(uuid4()) 241 | payload = { 242 | 'iss': 'foo', 243 | 'exp': int(time.time() + ttl), 244 | 'user_id': str(uuid4()), 245 | } 246 | atoken = six.text_type(jwt.encode(payload, secret, algorithm='HS256'), 'utf-8') 247 | rtoken = six.text_type(jwt.encode(payload, secret, algorithm='HS256'), 'utf-8') 248 | return { 249 | 'access_token': atoken, 250 | 'refresh_token': rtoken, 251 | } 252 | 253 | 254 | def make_response(version, endpoint): 255 | if version == 3: 256 | return make_response_v3(version, endpoint) 257 | else: 258 | return make_response_v2(version, endpoint) 259 | 260 | 261 | def make_response_v2(version, endpoint): 262 | uuid = str(uuid4()) 263 | version = 'v' + str(version) 264 | name = 'my-name-' + uuid 265 | entity = { 266 | 'name': name, 267 | 'host': name, 268 | 'label': name, 269 | 'status': 'STARTED', 270 | } 271 | for relation in relations_tree[endpoint]: 272 | if relation.endswith('s'): 273 | relation_ = relation[:-1] 274 | entity[relation_ + '_guid'] = str(uuid4()) 275 | entity[relation + '_url'] = '/'.join([ 276 | cc_api_url, version, endpoint, uuid, relation]) 277 | res = { 278 | 'metadata': { 279 | 'guid': uuid, 280 | 'url': '/'.join([cc_api_url, version, endpoint, uuid]) 281 | }, 282 | 'entity': entity 283 | } 284 | return res 285 | 286 | 287 | def make_response_v3(version, endpoint): 288 | uuid = str(uuid4()) 289 | version = 'v' + str(version) 290 | name = 'my-name-' + uuid 291 | res = { 292 | 'guid': uuid, 293 | 'name': name, 294 | 'state': 'STARTED', 295 | 'stack': 'my-stack', 296 | 'relationships': {}, 297 | 'links': { 298 | 'self': { 299 | 'href': '/'.join([cc_api_url, version, endpoint, uuid]), 300 | }, 301 | }, 302 | } 303 | for relation in relations_tree_v3[endpoint]['links']: 304 | if relation.endswith('s') and not relation.endswith('ss'): 305 | link = '/'.join([cc_api_url, version, endpoint, uuid, relation]) 306 | else: 307 | link = '/'.join([cc_api_url, version, relation + 's', uuid]) 308 | res['links'][relation] = {'href': link} 309 | for relation in relations_tree_v3[endpoint]['relationships']: 310 | res['relationships'][relation] = {'data': {'guid': uuid}} 311 | return res 312 | 313 | 314 | def make_response_list(version, endpoint, n, **extras): 315 | res = { 316 | 'resources': [ 317 | make_response(version, endpoint) 318 | for i in range(n) 319 | ] 320 | } 321 | res.update(extras) 322 | return res 323 | 324 | 325 | def make_error(version, status): 326 | if version == 3: 327 | return { 328 | 'errors': [ 329 | { 330 | 'code': status, 331 | 'title': 'CF-ErrorCode', 332 | 'detail': 'an error occurred ({0})'.format(status) 333 | } 334 | ], 335 | } 336 | else: 337 | return { 338 | 'error_code': str(status), 339 | 'error_description': 'an error occurred ({0})'.format(status), 340 | } 341 | 342 | 343 | def prepare_request(cc, method, endpoint, guid1=None, relation=None, guid2=None, status=200, version=2, n=1, body=None, next_url_path=None): 344 | url = [endpoint] 345 | if status < 200 or status >= 300: 346 | res = make_error(version, status) 347 | elif body: 348 | res = body 349 | elif guid1 is None and relation is None and guid2 is None: 350 | res = make_response_list(version, endpoint, n) 351 | elif guid1 is not None and relation is not None and guid2 is None: 352 | url.extend([guid1, relation]) 353 | res = make_response_list(version, endpoint, n) 354 | elif guid1 is not None and relation is None and guid2 is None: 355 | url.append(guid1) 356 | res = make_response(version, endpoint) 357 | elif guid1 is not None and relation is not None and guid2 is not None: 358 | url.extend([guid1, relation, guid2]) 359 | res = make_response(version, endpoint) 360 | else: 361 | raise Exception('invalid arguments') 362 | if next_url_path is not None: 363 | if version == 3: 364 | res['pagination'] = { 365 | 'next': { 366 | 'href': '/'.join([cc_api_url, next_url_path]) 367 | } 368 | } 369 | else: 370 | res['next_url'] = next_url_path 371 | if isinstance(cc, RequestFactory): 372 | req = cc.request(*url).set_method(method) 373 | responses.add(method.upper(), req.base_url, body=json.dumps(res), status=status) 374 | return req 375 | else: 376 | if version is not None: 377 | url.insert(0, 'v' + str(version)) 378 | url.insert(0, cc) 379 | url = '/'.join(url) 380 | responses.add(method.upper(), url, body=json.dumps(res), status=status) 381 | return None 382 | 383 | -------------------------------------------------------------------------------- /test/test_all.py: -------------------------------------------------------------------------------- 1 | from cf_api.deploy_blue_green import BlueGreen 2 | from cf_api.deploy_manifest import Deploy 3 | from cf_api.deploy_service import DeployService 4 | from cf_api.deploy_space import Space 5 | from cf_api import dropsonde_util 6 | from cf_api import logs_util 7 | from cf_api import exceptions 8 | from cf_api import routes_util 9 | from cf_api import ssh_util 10 | -------------------------------------------------------------------------------- /test/test_cf_api.py: -------------------------------------------------------------------------------- 1 | import six 2 | import time 3 | import json 4 | import responses 5 | import cf_api 6 | import functools 7 | from mock_cc import (prepare_request, cc_api_url, uaa_api_url, cc_v2_info, 8 | uaa_oauth_token, make_uaa_oauth_token, make_response_list) 9 | from unittest import TestCase 10 | from uuid import uuid4 11 | 12 | 13 | def setup_request(method, endpoint, guid1=None, relation=None, guid2=None, **kwargs): 14 | 15 | def decorator(func): 16 | 17 | @responses.activate 18 | def wrap(self): 19 | req = prepare_request(self.cc, method, endpoint, guid1, relation, guid2, **kwargs) 20 | return func(self, req) 21 | 22 | return wrap 23 | 24 | return decorator 25 | 26 | 27 | class CloudControllerRequest(TestCase): 28 | def setUp(self): 29 | self.cc = cf_api.CloudController(cc_api_url) 30 | 31 | @setup_request('GET', 'apps') 32 | def test_search(self, req): 33 | res = req.search('name', 'foo') 34 | self.assertIsInstance(res, cf_api.CloudControllerResponse) 35 | self.assertIsInstance(res.resources, list) 36 | self.assertListEqual(req.query, [('q', 'name:foo')]) 37 | 38 | @setup_request('GET', 'apps') 39 | def test_get_by_name(self, req): 40 | res = req.get_by_name('foo') 41 | self.assertIsInstance(res, cf_api.CloudControllerResponse) 42 | self.assertIsInstance(res.resources, list) 43 | self.assertListEqual(req.query, [('q', 'name:foo')]) 44 | res = req.get_by_name('bar', 'label') 45 | self.assertIsInstance(res, cf_api.CloudControllerResponse) 46 | self.assertIsInstance(res.resources, list) 47 | self.assertListEqual(req.query, [('q', 'label:bar')]) 48 | 49 | 50 | class V3CloudControllerResponse(TestCase): 51 | def setUp(self): 52 | self.cc = cf_api.CloudController(cc_api_url).v3 53 | 54 | @setup_request('GET', 'apps', status=400, version=3) 55 | def test_error_message(self, req): 56 | res = req.get() 57 | self.assertIsInstance(res, cf_api.V3CloudControllerResponse) 58 | self.assertIsInstance(res.error_message[0], six.string_types) 59 | self.assertEqual(res.error_message[0], 60 | 'CF-ErrorCode: an error occurred (400)') 61 | 62 | @setup_request('GET', 'apps', status=400, version=3) 63 | def test_error_code(self, req): 64 | res = req.get() 65 | self.assertIsInstance(res, cf_api.V3CloudControllerResponse) 66 | self.assertIsInstance(res.error_code[0], six.string_types) 67 | self.assertEqual(res.error_code[0], '400') 68 | 69 | @setup_request('GET', 'apps', version=3) 70 | def test_resource(self, req): 71 | res = req.get() 72 | self.assertIsInstance(res.resource, cf_api.V3Resource) 73 | 74 | @setup_request('GET', 'apps', version=3, n=2) 75 | def test_resources(self, req): 76 | res = req.get() 77 | self.assertIsInstance(res.resources, list) 78 | self.assertEqual(len(res.resources), 2) 79 | self.assertIsInstance(res.resources[0], cf_api.V3Resource) 80 | 81 | @setup_request('GET', 'apps', version=3, next_url_path='v3/apps?page=2') 82 | def test_next_url(self, req): 83 | res = req.get() 84 | next_url = cc_api_url + '/v3/apps?page=2' 85 | self.assertEqual(res.next_url, next_url) 86 | 87 | 88 | class CloudControllerResponse(TestCase): 89 | def setUp(self): 90 | self.cc = cf_api.CloudController(cc_api_url) 91 | 92 | @setup_request('GET', 'apps', status=400) 93 | def test_error_message(self, req): 94 | res = req.search('name', 'foo') 95 | self.assertIsInstance(res, cf_api.CloudControllerResponse) 96 | self.assertIsInstance(res.error_message, six.string_types) 97 | 98 | @setup_request('GET', 'apps', status=400) 99 | def test_error_code(self, req): 100 | res = req.search('name', 'foo') 101 | self.assertIsInstance(res, cf_api.CloudControllerResponse) 102 | self.assertIsInstance(res.error_code, six.string_types) 103 | self.assertEqual(res.error_code, '400') 104 | 105 | @setup_request('GET', 'apps') 106 | def test_resources(self, req): 107 | res = req.search('name', 'foo') 108 | self.assertIsInstance(res, cf_api.CloudControllerResponse) 109 | self.assertIsInstance(res.resources, list) 110 | self.assertIsInstance(res.resource, cf_api.Resource) 111 | 112 | @setup_request('GET', 'apps') 113 | def test_first_of_many_resources(self, req): 114 | res = req.search('name', 'foo') 115 | self.assertIsInstance(res, cf_api.CloudControllerResponse) 116 | self.assertIsInstance(res.resources, list) 117 | self.assertIsInstance(res.resource, cf_api.Resource) 118 | 119 | @setup_request('GET', 'apps', 'guid') 120 | def test_first_of_many_resources(self, req): 121 | res = req.search('name', 'foo') 122 | self.assertIsInstance(res, cf_api.CloudControllerResponse) 123 | self.assertIsInstance(res.resource, cf_api.Resource) 124 | 125 | 126 | class Resource(TestCase): 127 | def setUp(self): 128 | self.cc = cf_api.CloudController(cc_api_url) 129 | 130 | @setup_request('GET', 'apps') 131 | def test_guid(self, req): 132 | res = req.get() 133 | self.assertIsInstance(res, cf_api.CloudControllerResponse) 134 | self.assertIsInstance(res.resource.guid, six.string_types) 135 | 136 | @setup_request('GET', 'apps') 137 | def test_name(self, req): 138 | res = req.get() 139 | self.assertIsInstance(res, cf_api.CloudControllerResponse) 140 | self.assertIsInstance(res.resource.name, six.string_types) 141 | 142 | @setup_request('GET', 'apps') 143 | def test_label(self, req): 144 | res = req.get() 145 | self.assertIsInstance(res, cf_api.CloudControllerResponse) 146 | self.assertIsInstance(res.resource.label, six.string_types) 147 | 148 | @setup_request('GET', 'apps') 149 | def test_status(self, req): 150 | res = req.get() 151 | self.assertIsInstance(res, cf_api.CloudControllerResponse) 152 | self.assertIsInstance(res.resource.status, six.string_types) 153 | 154 | 155 | class V3Resource(TestCase): 156 | def setUp(self): 157 | self.cc = cf_api.CloudController(cc_api_url).v3 158 | 159 | @setup_request('GET', 'apps', version=3) 160 | def test_guid(self, req): 161 | res = req.get() 162 | self.assertIsInstance(res.resource.guid, six.string_types) 163 | 164 | @setup_request('GET', 'apps', version=3) 165 | def test_name(self, req): 166 | res = req.get() 167 | self.assertIsInstance(res.resource.name, six.string_types) 168 | 169 | @setup_request('GET', 'apps', version=3) 170 | def test_space_guid(self, req): 171 | res = req.get() 172 | self.assertEqual(res.resource.space_guid, res.resource.guid) 173 | 174 | @setup_request('GET', 'spaces', version=3) 175 | def test_org_guid(self, req): 176 | res = req.get() 177 | self.assertEqual(res.resource.org_guid, res.resource.guid) 178 | 179 | @setup_request('GET', 'apps', version=3) 180 | def test_href(self, req): 181 | res = req.get() 182 | href = '/'.join([cc_api_url, 'v3/apps', res.resource.guid]) 183 | self.assertEqual(res.resource.href, href) 184 | 185 | @setup_request('GET', 'spaces', version=3) 186 | def test_org_guid(self, req): 187 | res = req.get() 188 | href = '/'.join([cc_api_url, 'v3/organizations', res.resource.guid]) 189 | self.assertEqual(res.resource.organization_url, href) 190 | 191 | @setup_request('GET', 'apps', version=3) 192 | def test_state(self, req): 193 | res = req.get() 194 | self.assertEqual(res.resource.state, 'STARTED') 195 | 196 | 197 | class NewUAA(TestCase): 198 | def setUp(self): 199 | prepare_request(cc_api_url, 'GET', 'info', body=cc_v2_info) 200 | prepare_request(uaa_api_url, 'POST', 'oauth/token', body=uaa_oauth_token, version=None) 201 | 202 | @responses.activate 203 | def test_oauth_client_credentials_grant(self): 204 | cf_api.new_uaa( 205 | cc_api_url, 206 | client_id='abc', 207 | client_secret='', 208 | ) 209 | 210 | @responses.activate 211 | def test_oauth_password_grant(self): 212 | cf_api.new_uaa( 213 | cc_api_url, 214 | client_id='abc', 215 | client_secret='', 216 | username='foo', 217 | password='bar', 218 | ) 219 | 220 | @responses.activate 221 | def test_oauth_refresh_token(self): 222 | cf_api.new_uaa( 223 | cc_api_url, 224 | client_id='abc', 225 | client_secret='', 226 | refresh_token=uaa_oauth_token['refresh_token'], 227 | ) 228 | 229 | @responses.activate 230 | def test_oauth_access_token(self): 231 | cf_api.new_uaa( 232 | cc_api_url, 233 | client_id='abc', 234 | client_secret='', 235 | access_token=uaa_oauth_token['refresh_token'], 236 | ) 237 | 238 | @responses.activate 239 | def test_no_auth(self): 240 | uaa = cf_api.new_uaa( 241 | cc_api_url, 242 | client_id='abc', 243 | client_secret='', 244 | no_auth=True, 245 | ) 246 | self.assertIsNone(uaa.get_access_token()) 247 | self.assertIsNone(uaa.get_refresh_token()) 248 | 249 | @responses.activate 250 | def test_oauth_authorization_code_url(self): 251 | uaa = cf_api.new_uaa( 252 | cc_api_url, 253 | client_id='abc', 254 | client_secret='', 255 | no_auth=True, 256 | ) 257 | self.assertIsNone(uaa.get_access_token()) 258 | self.assertIsNone(uaa.get_refresh_token()) 259 | url = uaa.authorization_code_url('code', 'cloud_controller.read', '{0}/success'.format(cc_api_url)) 260 | self.assertEqual(url, '{0}/oauth/authorize?response_type=code&client_id=abc&response_type=code&client_id=abc&scope=cloud_controller.read&redirect_uri=http%3A%2F%2Flocalhost%2Fsuccess'.format(cc_api_url)) 261 | 262 | 263 | class UAA(TestCase): 264 | def test_set_client_credentials_no_basic_auth(self): 265 | uaa = cf_api.UAA(cc_api_url) 266 | uaa.set_client_credentials('abc', '123') 267 | self.assertNotIn('authorization', uaa.headers) 268 | 269 | def test_set_client_credentials_basic_auth(self): 270 | uaa = cf_api.UAA(cc_api_url) 271 | uaa.set_client_credentials('abc', '123', set_basic_auth=True) 272 | self.assertIn('authorization', uaa.headers) 273 | self.assertEqual(uaa.get_header('authorization'), 'Basic YWJjOjEyMw==') 274 | 275 | 276 | class NewCloudController(TestCase): 277 | def setUp(self): 278 | prepare_request(cc_api_url, 'GET', 'info', body=cc_v2_info) 279 | prepare_request(uaa_api_url, 'POST', 'oauth/token', body=uaa_oauth_token, version=None) 280 | 281 | @responses.activate 282 | def test_new_cloud_controller(self): 283 | cc = cf_api.new_cloud_controller( 284 | cc_api_url, 285 | client_id='abc', 286 | client_secret='', 287 | verify_ssl=False, 288 | ) 289 | self.assertIsInstance(cc, cf_api.CloudController) 290 | self.assertIsInstance(cc.info, cf_api.CFInfo) 291 | self.assertIsInstance(cc.uaa, cf_api.UAA) 292 | self.assertFalse(cc.verify_ssl) 293 | self.assertFalse(cc.uaa.verify_ssl) 294 | 295 | @responses.activate 296 | def test_custom_cloud_controller(self): 297 | 298 | class MyCC(cf_api.CloudController): 299 | pass 300 | 301 | cc = cf_api.new_cloud_controller( 302 | cc_api_url, 303 | client_id='abc', 304 | client_secret='', 305 | verify_ssl=False, 306 | cloud_controller_class=MyCC, 307 | ) 308 | self.assertIsInstance(cc, MyCC) 309 | self.assertIsInstance(cc.info, cf_api.CFInfo) 310 | self.assertIsInstance(cc.uaa, cf_api.UAA) 311 | self.assertFalse(cc.verify_ssl) 312 | self.assertFalse(cc.uaa.verify_ssl) 313 | 314 | @responses.activate 315 | def test_no_auth(self): 316 | cc = cf_api.new_cloud_controller( 317 | cc_api_url, 318 | client_id='abc', 319 | client_secret='', 320 | verify_ssl=False, 321 | no_auth=True, 322 | ) 323 | self.assertIsNone(cc.uaa.get_access_token()) 324 | self.assertIsNone(cc.uaa.get_refresh_token()) 325 | 326 | 327 | class RefreshTokens(TestCase): 328 | @responses.activate 329 | def test_refresh_tokens_callback(self): 330 | orig_token = make_uaa_oauth_token(2) 331 | refreshed_token = make_uaa_oauth_token(2) 332 | self.assertNotEqual(orig_token['access_token'], refreshed_token['access_token']) 333 | prepare_request(cc_api_url, 'GET', 'info', body=cc_v2_info) 334 | prepare_request(uaa_api_url, 'POST', 'oauth/token', body=orig_token, version=None) 335 | prepare_request(cc_api_url, 'GET', 'apps') 336 | prepare_request(uaa_api_url, 'POST', 'oauth/token', body=refreshed_token, version=None) 337 | prepare_request(cc_api_url, 'GET', 'apps') 338 | 339 | cc = cf_api.new_cloud_controller( 340 | cc_api_url, 341 | client_id='abc', 342 | client_secret='', 343 | verify_ssl=False, 344 | ) 345 | cc.set_refresh_tokens_callback() 346 | app = cc.apps().get().resource 347 | self.assertIsInstance(app, cf_api.Resource) 348 | self.assertEqual(cc.uaa.get_access_token().to_string(), 349 | orig_token['access_token']) 350 | time.sleep(2) 351 | app = cc.apps().get().resource 352 | self.assertIsInstance(app, cf_api.Resource) 353 | self.assertEqual(cc.uaa.get_access_token().to_string(), 354 | refreshed_token['access_token']) 355 | 356 | 357 | class CloudController(TestCase): 358 | def setUp(self): 359 | prepare_request(cc_api_url, 'GET', 'info', 360 | body=cc_v2_info) 361 | prepare_request(uaa_api_url, 'POST', 'oauth/token', 362 | body=uaa_oauth_token, version=None) 363 | 364 | @responses.activate 365 | def test_get_all_resources(self): 366 | prepare_request(cc_api_url, 'GET', 'apps', 367 | body=make_response_list(2, 'apps', 1, 368 | next_url='apps')) 369 | prepare_request(cc_api_url, 'GET', 'apps', 370 | body=make_response_list(2, 'apps', 1)) 371 | 372 | cc = cf_api.new_cloud_controller( 373 | cc_api_url, 374 | client_id='abc', 375 | client_secret='', 376 | verify_ssl=False, 377 | ) 378 | req = cc.apps() 379 | apps = cc.get_all_resources(req) 380 | self.assertIsInstance(apps, list) 381 | self.assertIsInstance(apps[0], cf_api.Resource) 382 | self.assertEqual(2, len(apps)) 383 | 384 | @responses.activate 385 | def test_set_version(self): 386 | cc = cf_api.new_cloud_controller( 387 | cc_api_url, 388 | client_id='abc', 389 | client_secret='', 390 | verify_ssl=False, 391 | ) 392 | cc.set_version(3) 393 | req = cc.request('apps') 394 | self.assertEqual('{0}/v3/apps'.format(cc_api_url), req.base_url) 395 | -------------------------------------------------------------------------------- /version.txt: -------------------------------------------------------------------------------- 1 | 1.0.0 2 | --------------------------------------------------------------------------------