├── .gitattributes ├── .gitignore ├── LICENSE ├── MANIFEST.in ├── NOTICE ├── README.md ├── bin ├── anc-policy ├── create-new-pxgrid-account ├── endpoint-query-all ├── matrix-query-all ├── profiles-query-all ├── px-publish ├── px-subscribe ├── session-query-all ├── session-query-by-ip ├── sgacls-query-all ├── sgts-query-all ├── sxp-query-bindings ├── system-query-all └── user-groups-query ├── pxgrid_util ├── __init__.py ├── _version.py ├── config.py ├── create_account_config.py ├── pxgrid.py ├── stomp.py └── ws_stomp.py ├── sample_macs ├── sample_mac_addrs_1000.txt └── sample_mac_addrs_10000.txt ├── setup.cfg ├── setup.py └── versioneer.py /.gitattributes: -------------------------------------------------------------------------------- 1 | pxgrid_util/_version.py export-subst 2 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Byte-compiled / optimized / DLL files 2 | __pycache__/ 3 | *.py[cod] 4 | *$py.class 5 | 6 | # macOS 7 | .DS_Store 8 | 9 | # C extensions 10 | *.so 11 | 12 | # Distribution / packaging 13 | .Python 14 | build/ 15 | develop-eggs/ 16 | dist/ 17 | downloads/ 18 | eggs/ 19 | .eggs/ 20 | lib/ 21 | lib64/ 22 | parts/ 23 | sdist/ 24 | var/ 25 | wheels/ 26 | pip-wheel-metadata/ 27 | share/python-wheels/ 28 | *.egg-info/ 29 | .installed.cfg 30 | *.egg 31 | MANIFEST 32 | 33 | # PyInstaller 34 | # Usually these files are written by a python script from a template 35 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 36 | *.manifest 37 | *.spec 38 | 39 | # Installer logs 40 | pip-log.txt 41 | pip-delete-this-directory.txt 42 | 43 | # Unit test / coverage reports 44 | htmlcov/ 45 | .tox/ 46 | .nox/ 47 | .coverage 48 | .coverage.* 49 | .cache 50 | nosetests.xml 51 | coverage.xml 52 | *.cover 53 | *.py,cover 54 | .hypothesis/ 55 | .pytest_cache/ 56 | 57 | # Translations 58 | *.mo 59 | *.pot 60 | 61 | # Django stuff: 62 | *.log 63 | local_settings.py 64 | db.sqlite3 65 | db.sqlite3-journal 66 | 67 | # Flask stuff: 68 | instance/ 69 | .webassets-cache 70 | 71 | # Scrapy stuff: 72 | .scrapy 73 | 74 | # Sphinx documentation 75 | docs/_build/ 76 | 77 | # PyBuilder 78 | target/ 79 | 80 | # Jupyter Notebook 81 | .ipynb_checkpoints 82 | 83 | # IPython 84 | profile_default/ 85 | ipython_config.py 86 | 87 | # pyenv 88 | .python-version 89 | 90 | # pipenv 91 | # According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. 92 | # However, in case of collaboration, if having platform-specific dependencies or dependencies 93 | # having no cross-platform support, pipenv may install dependencies that don't work, or not 94 | # install all needed dependencies. 95 | #Pipfile.lock 96 | 97 | # PEP 582; used by e.g. github.com/David-OConnor/pyflow 98 | __pypackages__/ 99 | 100 | # Celery stuff 101 | celerybeat-schedule 102 | celerybeat.pid 103 | 104 | # SageMath parsed files 105 | *.sage.py 106 | 107 | # Environments 108 | .env 109 | .venv 110 | env/ 111 | venv/ 112 | ENV/ 113 | env.bak/ 114 | venv.bak/ 115 | 116 | # Spyder project settings 117 | .spyderproject 118 | .spyproject 119 | 120 | # Rope project settings 121 | .ropeproject 122 | 123 | # mkdocs documentation 124 | /site 125 | 126 | # mypy 127 | .mypy_cache/ 128 | .dmypy.json 129 | dmypy.json 130 | 131 | # Pyre type checker 132 | .pyre/ 133 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, 10 | and distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by 13 | the copyright owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all 16 | other entities that control, are controlled by, or are under common 17 | control with that entity. For the purposes of this definition, 18 | "control" means (i) the power, direct or indirect, to cause the 19 | direction or management of such entity, whether by contract or 20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 21 | outstanding shares, or (iii) beneficial ownership of such entity. 22 | 23 | "You" (or "Your") shall mean an individual or Legal Entity 24 | exercising permissions granted by this License. 25 | 26 | "Source" form shall mean the preferred form for making modifications, 27 | including but not limited to software source code, documentation 28 | source, and configuration files. 29 | 30 | "Object" form shall mean any form resulting from mechanical 31 | transformation or translation of a Source form, including but 32 | not limited to compiled object code, generated documentation, 33 | and conversions to other media types. 34 | 35 | "Work" shall mean the work of authorship, whether in Source or 36 | Object form, made available under the License, as indicated by a 37 | copyright notice that is included in or attached to the work 38 | (an example is provided in the Appendix below). 39 | 40 | "Derivative Works" shall mean any work, whether in Source or Object 41 | form, that is based on (or derived from) the Work and for which the 42 | editorial revisions, annotations, elaborations, or other modifications 43 | represent, as a whole, an original work of authorship. For the purposes 44 | of this License, Derivative Works shall not include works that remain 45 | separable from, or merely link (or bind by name) to the interfaces of, 46 | the Work and Derivative Works thereof. 47 | 48 | "Contribution" shall mean any work of authorship, including 49 | the original version of the Work and any modifications or additions 50 | to that Work or Derivative Works thereof, that is intentionally 51 | submitted to Licensor for inclusion in the Work by the copyright owner 52 | or by an individual or Legal Entity authorized to submit on behalf of 53 | the copyright owner. For the purposes of this definition, "submitted" 54 | means any form of electronic, verbal, or written communication sent 55 | to the Licensor or its representatives, including but not limited to 56 | communication on electronic mailing lists, source code control systems, 57 | and issue tracking systems that are managed by, or on behalf of, the 58 | Licensor for the purpose of discussing and improving the Work, but 59 | excluding communication that is conspicuously marked or otherwise 60 | designated in writing by the copyright owner as "Not a Contribution." 61 | 62 | "Contributor" shall mean Licensor and any individual or Legal Entity 63 | on behalf of whom a Contribution has been received by Licensor and 64 | subsequently incorporated within the Work. 65 | 66 | 2. Grant of Copyright License. Subject to the terms and conditions of 67 | this License, each Contributor hereby grants to You a perpetual, 68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 69 | copyright license to reproduce, prepare Derivative Works of, 70 | publicly display, publicly perform, sublicense, and distribute the 71 | Work and such Derivative Works in Source or Object form. 72 | 73 | 3. Grant of Patent License. Subject to the terms and conditions of 74 | this License, each Contributor hereby grants to You a perpetual, 75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 76 | (except as stated in this section) patent license to make, have made, 77 | use, offer to sell, sell, import, and otherwise transfer the Work, 78 | where such license applies only to those patent claims licensable 79 | by such Contributor that are necessarily infringed by their 80 | Contribution(s) alone or by combination of their Contribution(s) 81 | with the Work to which such Contribution(s) was submitted. If You 82 | institute patent litigation against any entity (including a 83 | cross-claim or counterclaim in a lawsuit) alleging that the Work 84 | or a Contribution incorporated within the Work constitutes direct 85 | or contributory patent infringement, then any patent licenses 86 | granted to You under this License for that Work shall terminate 87 | as of the date such litigation is filed. 88 | 89 | 4. Redistribution. You may reproduce and distribute copies of the 90 | Work or Derivative Works thereof in any medium, with or without 91 | modifications, and in Source or Object form, provided that You 92 | meet the following conditions: 93 | 94 | (a) You must give any other recipients of the Work or 95 | Derivative Works a copy of this License; and 96 | 97 | (b) You must cause any modified files to carry prominent notices 98 | stating that You changed the files; and 99 | 100 | (c) You must retain, in the Source form of any Derivative Works 101 | that You distribute, all copyright, patent, trademark, and 102 | attribution notices from the Source form of the Work, 103 | excluding those notices that do not pertain to any part of 104 | the Derivative Works; and 105 | 106 | (d) If the Work includes a "NOTICE" text file as part of its 107 | distribution, then any Derivative Works that You distribute must 108 | include a readable copy of the attribution notices contained 109 | within such NOTICE file, excluding those notices that do not 110 | pertain to any part of the Derivative Works, in at least one 111 | of the following places: within a NOTICE text file distributed 112 | as part of the Derivative Works; within the Source form or 113 | documentation, if provided along with the Derivative Works; or, 114 | within a display generated by the Derivative Works, if and 115 | wherever such third-party notices normally appear. The contents 116 | of the NOTICE file are for informational purposes only and 117 | do not modify the License. You may add Your own attribution 118 | notices within Derivative Works that You distribute, alongside 119 | or as an addendum to the NOTICE text from the Work, provided 120 | that such additional attribution notices cannot be construed 121 | as modifying the License. 122 | 123 | You may add Your own copyright statement to Your modifications and 124 | may provide additional or different license terms and conditions 125 | for use, reproduction, or distribution of Your modifications, or 126 | for any such Derivative Works as a whole, provided Your use, 127 | reproduction, and distribution of the Work otherwise complies with 128 | the conditions stated in this License. 129 | 130 | 5. Submission of Contributions. Unless You explicitly state otherwise, 131 | any Contribution intentionally submitted for inclusion in the Work 132 | by You to the Licensor shall be under the terms and conditions of 133 | this License, without any additional terms or conditions. 134 | Notwithstanding the above, nothing herein shall supersede or modify 135 | the terms of any separate license agreement you may have executed 136 | with Licensor regarding such Contributions. 137 | 138 | 6. Trademarks. This License does not grant permission to use the trade 139 | names, trademarks, service marks, or product names of the Licensor, 140 | except as required for reasonable and customary use in describing the 141 | origin of the Work and reproducing the content of the NOTICE file. 142 | 143 | 7. Disclaimer of Warranty. Unless required by applicable law or 144 | agreed to in writing, Licensor provides the Work (and each 145 | Contributor provides its Contributions) on an "AS IS" BASIS, 146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 147 | implied, including, without limitation, any warranties or conditions 148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 149 | PARTICULAR PURPOSE. You are solely responsible for determining the 150 | appropriateness of using or redistributing the Work and assume any 151 | risks associated with Your exercise of permissions under this License. 152 | 153 | 8. Limitation of Liability. In no event and under no legal theory, 154 | whether in tort (including negligence), contract, or otherwise, 155 | unless required by applicable law (such as deliberate and grossly 156 | negligent acts) or agreed to in writing, shall any Contributor be 157 | liable to You for damages, including any direct, indirect, special, 158 | incidental, or consequential damages of any character arising as a 159 | result of this License or out of the use or inability to use the 160 | Work (including but not limited to damages for loss of goodwill, 161 | work stoppage, computer failure or malfunction, or any and all 162 | other commercial damages or losses), even if such Contributor 163 | has been advised of the possibility of such damages. 164 | 165 | 9. Accepting Warranty or Additional Liability. While redistributing 166 | the Work or Derivative Works thereof, You may choose to offer, 167 | and charge a fee for, acceptance of support, warranty, indemnity, 168 | or other liability obligations and/or rights consistent with this 169 | License. However, in accepting such obligations, You may act only 170 | on Your own behalf and on Your sole responsibility, not on behalf 171 | of any other Contributor, and only if You agree to indemnify, 172 | defend, and hold each Contributor harmless for any liability 173 | incurred by, or claims asserted against, such Contributor by reason 174 | of your accepting any such warranty or additional liability. 175 | 176 | END OF TERMS AND CONDITIONS 177 | 178 | APPENDIX: How to apply the Apache License to your work. 179 | 180 | To apply the Apache License to your work, attach the following 181 | boilerplate notice, with the fields enclosed by brackets "[]" 182 | replaced with your own identifying information. (Don't include 183 | the brackets!) The text should be enclosed in the appropriate 184 | comment syntax for the file format. We also recommend that a 185 | file or class name and description of purpose be included on the 186 | same "printed page" as the copyright notice for easier 187 | identification within third-party archives. 188 | 189 | Copyright [yyyy] [name of copyright owner] 190 | 191 | Licensed under the Apache License, Version 2.0 (the "License"); 192 | you may not use this file except in compliance with the License. 193 | You may obtain a copy of the License at 194 | 195 | http://www.apache.org/licenses/LICENSE-2.0 196 | 197 | Unless required by applicable law or agreed to in writing, software 198 | distributed under the License is distributed on an "AS IS" BASIS, 199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 200 | See the License for the specific language governing permissions and 201 | limitations under the License. 202 | -------------------------------------------------------------------------------- /MANIFEST.in: -------------------------------------------------------------------------------- 1 | include versioneer.py 2 | include pxgrid_util/_version.py 3 | -------------------------------------------------------------------------------- /NOTICE: -------------------------------------------------------------------------------- 1 | Portions Copyright (c) 2021 Cisco Systems, Inc. and/or its affiliates 2 | 3 | This project includes software developed at Cisco Systems, Inc. and/or its affiliates. 4 | 5 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | 2 | [![published](https://static.production.devnetcloud.com/codeexchange/assets/images/devnet-published.svg)](https://developer.cisco.com/codeexchange/github/repo/cisco-pxgrid/python-advanced-examples) 3 | 4 | # pxGrid Python Advanced Examples 5 | 6 | 7 | 8 | 9 | 10 | - [pxGrid Python Advanced Examples](#pxgrid-python-advanced-examples) 11 | - [Introduction](#introduction) 12 | - [Before Running Samples](#before-running-samples) 13 | - [Description Of Samples](#description-of-samples) 14 | - [Sample Invocations](#sample-invocations) 15 | - [`px-publish`](#px-publish) 16 | - [`px-subscribe`](#px-subscribe) 17 | - [Using password authentication plus server public cert](#using-password-authentication-plus-server-public-cert) 18 | - [Ignoring server cert check](#ignoring-server-cert-check) 19 | - [Subscribing for sessions ignoring server cert check](#subscribing-for-sessions-ignoring-server-cert-check) 20 | - [`session-query-all`](#session-query-all) 21 | - [`sgacls-query-all`](#sgacls-query-all) 22 | - [anc-policy](#anc-policy) 23 | - [Get the policies you have defined in your deployment](#get-the-policies-you-have-defined-in-your-deployment) 24 | - [Get all endpoints with an ANC policy, including the applied policy](#get-all-endpoints-with-an-anc-policy-including-the-applied-policy) 25 | - [Get the policy applied to a specific endpoint, by MAC address](#get-the-policy-applied-to-a-specific-endpoint-by-mac-address) 26 | - [Apply policy by MAC address](#apply-policy-by-mac-address) 27 | - [Clear policy by MAC address](#clear-policy-by-mac-address) 28 | - [Apply policy by IP address](#apply-policy-by-ip-address) 29 | - [Clear policy by IP address](#clear-policy-by-ip-address) 30 | - [To Generate pxGrid Certificates From ISE](#to-generate-pxgrid-certificates-from-ise) 31 | 32 | 33 | 34 | ## Introduction 35 | 36 | This repository contains the source code for a number of advanced pxGrid examples written in python. The code is based on extending a set of examples found in [https://github.com/cisco-pxgrid/pxgrid-rest-ws](https://github.com/cisco-pxgrid/pxgrid-rest-ws). Please note that what was initially common code has diverged from the code in that repository. 37 | 38 | All the examples here are based on exercising the pxGrid 2.0 services defined at [https://github.com/cisco-pxgrid/pxgrid-rest-ws/wiki/pxGrid-Provider](https://github.com/cisco-pxgrid/pxgrid-rest-ws/wiki/pxGrid-Provider). However, not all services will necessarily be exercised, but examples will be added over time. 39 | 40 | Basic pxGrid setup on ISE is **not** covered here. Some instructions are provided for creating a suitable client cert for use with the Python examples. 41 | 42 | Each sample script, when able to run successfully, produces JSON to STDOUT, allowing processing by tools such as [jq](https://stedolan.github.io/jq/) or any other that ingests JSON. For example, using the `session-query-all` example combined with [jq](https://stedolan.github.io/jq/) to extract an array of client MAC addresses for currently attached sessions: 43 | 44 | ``` 45 | $ session-query-all 46 | --host HOSTNAME \ 47 | -n NODENAME \ 48 | -w NODESECRET \ 49 | --insecure | \ 50 | jq -C '[ .sessions[] | select(.state == "STARTED") | .macAddress ]' 51 | [ 52 | "00:50:56:94:39:9F", 53 | "00:50:56:94:6F:90", 54 | "00:50:56:94:91:67", 55 | "00:50:56:94:AA:9B", 56 | "00:50:56:94:D8:7E", 57 | "00:50:56:94:DF:7D", 58 | "00:50:56:94:E4:31", 59 | "00:50:56:94:E7:97" 60 | ] 61 | ``` 62 | 63 | 64 | ## Before Running Samples 65 | 66 | All the examples may be installed using `pip`, making the examples available in your environment. 67 | 68 | 1. Have **Python 3.8 or later** available on your system 69 | 2. Optionally (but strongly recommended) create a virtual environment 70 | 2. Install the examples and support module using pip: 71 | 72 | pip3 install pxgrid-util 73 | 74 | 75 | ## Description Of Samples 76 | 77 | There are several simple test scripts, listend below. 78 | 79 | | Script Name | Description | 80 | |:--|:--| 81 | | `anc-policy` | Download ANC policies, endpoints with ANC policies applied, and apply ANC policy | 82 | | `create-new-pxgrid-account` | Create a simple password authentication pxGrid client if you have an ISE admin username and password | 83 | | `matrix-query-all` | Download all cells of the TrustSec policy matrix | 84 | | `profiles-query-all` | Download all ISE Profiler profiles | 85 | | `px-publish` | Simple utility to publish a simple message to a custom service and topic. More of a template to copy. | 86 | | `px-subscribe` | General purpose utility to display details on multiple services and to allow subscriptions to topics of named services | 87 | | `session-query-all` | Download all current sessions | 88 | | `session-query-by-ip` | Perform a query on the session topic using a given IP address | 89 | | `sgacls-query-all` | Download all current SG-ACL definitions | 90 | | `sgts-query-all`| Download all SGT definitions | 91 | | `sxp-query-bindings` | Download all SXP bindings | 92 | | `system-query-all` | Download performance or health metrics from an ISE installation | 93 | | `user-groups-query` | Query for the groups associated with users authenticated to ISE | 94 | 95 | Each script has, at minimum, a set of shared options relating to pxGrid node name, shared secrets, cert parameters, etc. These common options are: 96 | 97 | ``` 98 | -h, --help show this help message and exit 99 | -a HOSTNAME, --hostname HOSTNAME 100 | pxGrid controller host name (multiple ok) 101 | --port PORT pxGrid controller port (default 8910) 102 | -n NODENAME, --NODENAME NODENAME 103 | Client node name 104 | -w PASSWORD, --password PASSWORD 105 | Password (optional) 106 | -d DESCRIPTION, --description DESCRIPTION 107 | Description (optional) 108 | -c CLIENTCERT, --clientcert CLIENTCERT 109 | Client certificate chain pem filename (optional) 110 | -k CLIENTKEY, --clientkey CLIENTKEY 111 | Client key filename (optional) 112 | -p CLIENTKEYPASSWORD, --clientkeypassword CLIENTKEYPASSWORD 113 | Client key password (optional) 114 | -s SERVERCERT, --servercert SERVERCERT 115 | Server certificates pem filename 116 | --insecure Allow insecure server connections when using SSL 117 | -v, --verbose Verbose output 118 | ``` 119 | 120 | 121 | ## Sample Invocations 122 | 123 | Note that most of the examples below focus on using pxGrid 2.0 **without certs**, enabled by the command line option `--insecure`. This is for simplicity. Please refer to pxGrid 2.0 documentation on DevNet or to the [basic examples repo](ttps://github.com/cisco-pxgrid/pxgrid-rest-ws) for examples of how to run with certs. 124 | 125 | Also, not all example scripts will be demonstrated here. 126 | 127 | 128 | ### `px-publish` 129 | 130 | This example uses `--insecure`. 131 | 132 | ``` 133 | $ px-publish \ 134 | --insecure \ 135 | -a ise-3-2.hareshaw.net \ 136 | -w **************** \ 137 | -n producer \ 138 | --service com.cisco.einarnn.special \ 139 | --topic customTopic \ 140 | --verbose 141 | 2023-08-27 21:08:38,587:pxgrid_util.pxgrid:DEBUG:account_activate 142 | 2023-08-27 21:08:38,587:pxgrid_util.pxgrid:DEBUG:send_rest_request AccountActivate 143 | 2023-08-27 21:08:38,644:pxgrid_util.pxgrid:DEBUG:service_register com.cisco.einarnn.special 144 | 2023-08-27 21:08:38,644:pxgrid_util.pxgrid:DEBUG:send_rest_request ServiceRegister 145 | 2023-08-27 21:08:38,788:__main__:DEBUG:[service_register_response] { 146 | 2023-08-27 21:08:38,788:__main__:DEBUG:[service_register_response] "id": "b96aa465-5252-41cd-a961-c43ab0c46475", 147 | 2023-08-27 21:08:38,788:__main__:DEBUG:[service_register_response] "reregisterTimeMillis": 300000 148 | 2023-08-27 21:08:38,788:__main__:DEBUG:[service_register_response] } 149 | 2023-08-27 21:08:38,788:pxgrid_util.pxgrid:DEBUG:service_lookup com.cisco.einarnn.special 150 | 2023-08-27 21:08:38,788:pxgrid_util.pxgrid:DEBUG:send_rest_request ServiceLookup 151 | 2023-08-27 21:08:38,821:__main__:DEBUG:service lookup response: 152 | 2023-08-27 21:08:38,821:__main__:DEBUG: { 153 | 2023-08-27 21:08:38,822:__main__:DEBUG: "services": [ 154 | 2023-08-27 21:08:38,822:__main__:DEBUG: { 155 | 2023-08-27 21:08:38,822:__main__:DEBUG: "name": "com.cisco.einarnn.special", 156 | 2023-08-27 21:08:38,822:__main__:DEBUG: "nodeName": "producer", 157 | 2023-08-27 21:08:38,822:__main__:DEBUG: "properties": { 158 | 2023-08-27 21:08:38,822:__main__:DEBUG: "customTopic": "/topic/com.cisco.einarnn.special", 159 | 2023-08-27 21:08:38,822:__main__:DEBUG: "wsPubsubService": "com.cisco.ise.pubsub" 160 | 2023-08-27 21:08:38,822:__main__:DEBUG: } 161 | 2023-08-27 21:08:38,822:__main__:DEBUG: } 162 | 2023-08-27 21:08:38,822:__main__:DEBUG: ] 163 | 2023-08-27 21:08:38,822:__main__:DEBUG: } 164 | 2023-08-27 21:08:38,822:pxgrid_util.pxgrid:DEBUG:service_lookup com.cisco.ise.pubsub 165 | 2023-08-27 21:08:38,822:pxgrid_util.pxgrid:DEBUG:send_rest_request ServiceLookup 166 | 2023-08-27 21:08:38,868:pxgrid_util.pxgrid:DEBUG:get_access_secret ~ise-pubsub-ise-3-2 167 | 2023-08-27 21:08:38,868:pxgrid_util.pxgrid:DEBUG:send_rest_request AccessSecret 168 | 2023-08-27 21:08:38,897:__main__:DEBUG:[default_publisher_loop] starting subscription to /topic/com.cisco.einarnn.special at wss://ise-3-2.hareshaw.net:8910/pxgrid/ise/pubsub 169 | 2023-08-27 21:08:38,897:__main__:DEBUG:[default_publish_loop] opening web socket and stomp 170 | 2023-08-27 21:08:38,897:__main__:DEBUG:[default_publish_loop] connect websocket 171 | 2023-08-27 21:08:38,897:pxgrid_util.ws_stomp:DEBUG:WebSocket Connect, ws_url=wss://ise-3-2.hareshaw.net:8910/pxgrid/ise/pubsub 172 | 2023-08-27 21:08:38,945:__main__:DEBUG:[default_publish_loop] connect STOMP node ~ise-pubsub-ise-3-2 173 | 2023-08-27 21:08:38,946:pxgrid_util.ws_stomp:DEBUG:STOMP CONNECT host=~ise-pubsub-ise-3-2 174 | 2023-08-27 21:08:38,946:pxgrid_util.stomp:DEBUG:write 175 | 2023-08-27 21:08:38,946:pxgrid_util.ws_stomp:DEBUG:stomp_connect completed 176 | 2023-08-27 21:08:39,947:pxgrid_util.ws_stomp:DEBUG:STOMP SEND topic=/topic/com.cisco.einarnn.special 177 | 2023-08-27 21:08:39,948:pxgrid_util.stomp:DEBUG:write 178 | 2023-08-27 21:08:39,949:pxgrid_util.ws_stomp:DEBUG:stomp_send completed 179 | 2023-08-27 21:08:39,949:__main__:DEBUG:[default_publish_loop] message published to node ~ise-pubsub-ise-3-2, topic /topic/com.cisco.einarnn.special 180 | ... 181 | ``` 182 | 183 | 184 | ### `px-subscribe` 185 | 186 | #### Using password authentication plus server public cert 187 | 188 | The option `--insecure` isn't passed here as we provide a server cert. 189 | 190 | ``` 191 | $ px-subscribe \ 192 | -a your.server.fqdn \ 193 | -n NODENAME \ 194 | -s /path/to/ise/public/server.cer \ 195 | -w NODESECRET \ 196 | --services 197 | [ 198 | { 199 | "services": [ 200 | { 201 | "name": "com.cisco.ise.mdm", 202 | "NODENAME": "ise-admin-tl-enn-ise-1", 203 | "properties": { 204 | "endpointTopic": "/topic/com.cisco.ise.mdm.endpoint", 205 | "restBaseURL": "https://your.server.fqdn:8910/pxgrid/mdm/bd", 206 | "restBaseUrl": "https://your.server.fqdn:8910/pxgrid/ise/mdm", 207 | "wsPubsubService": "com.cisco.ise.pubsub" 208 | } 209 | } 210 | ] 211 | }, 212 | ...etc... 213 | ] 214 | ``` 215 | 216 | 217 | 218 | #### Ignoring server cert check 219 | 220 | Please note that this is **_unsafe for production_**: 221 | 222 | ``` 223 | $ px-subscribe \ 224 | -a your.server.fqdn \ 225 | -n NODENAME \ 226 | -w NODESECRET \ 227 | --services \ 228 | --insecure 229 | [ 230 | { 231 | "services": [ 232 | { 233 | "name": "com.cisco.ise.mdm", 234 | "NODENAME": "ise-admin-tl-enn-ise-1", 235 | "properties": { 236 | "endpointTopic": "/topic/com.cisco.ise.mdm.endpoint", 237 | "restBaseURL": "https://your.server.fqdn:8910/pxgrid/mdm/bd", 238 | "restBaseUrl": "https://your.server.fqdn:8910/pxgrid/ise/mdm", 239 | "wsPubsubService": "com.cisco.ise.pubsub" 240 | } 241 | } 242 | ] 243 | }, 244 | ...etc... 245 | ``` 246 | 247 | #### Subscribing for sessions ignoring server cert check 248 | 249 | Please note that this is **_unsafe for production_**: 250 | 251 | ``` 252 | $ px-subscribe \ 253 | -a your.server.fqdn \ 254 | -n NODENAME \ 255 | -w NODESECRET \ 256 | --service com.cisco.ise.session \ 257 | --topic sessionTopic 258 | 2020-03-31 09:45:13,980:stomp:DEBUG:write 259 | 2020-03-31 09:45:13,980:ws_stomp:DEBUG:stomp_connect completed 260 | 2020-03-31 09:45:13,980:stomp:DEBUG:write 261 | 2020-03-31 09:45:13,981:ws_stomp:DEBUG:stomp_subscribe completed 262 | 2020-03-31 09:45:14,014:stomp:DEBUG:parse 263 | 2020-03-31 09:45:14,014:stomp:DEBUG:parse frame content: 264 | 2020-03-31 09:45:14,014:ws_stomp:DEBUG:STOMP CONNECTED version=1.2 265 | ``` 266 | 267 | ### `session-query-all` 268 | 269 | Using password authentication plus server public cert: 270 | 271 | ``` 272 | session-query-all \ 273 | -a your.server.fqdn \ 274 | -n NODENAME \ 275 | -s /path/to/ise/public/server/cert 276 | -w NODESECRET 277 | {"sessions":[]} 278 | ``` 279 | 280 | ### `sgacls-query-all` 281 | 282 | Using password authentication plus server public cert: 283 | 284 | ``` 285 | $ sgacls-query-all \ 286 | --host HOSTNAME \ 287 | -n NODENAME \ 288 | -w NODESECRET \ 289 | --insecure | \ 290 | jq -C . 291 | { 292 | "securityGroupAcls": [ 293 | { 294 | "id": "8dfd0610-6e9a-11ea-8892-626791db3907", 295 | "name": "DOPE_00001", 296 | "description": "DOPE Test SGACL DOPE_00001", 297 | "ipVersion": "IPV4", 298 | "acl": "permit tcp dst range 1 10 \ndeny ip\n", 299 | "generationId": "0" 300 | }, 301 | ...etc... 302 | ``` 303 | 304 | ### anc-policy 305 | 306 | #### Get the policies you have defined in your deployment 307 | 308 | ``` 309 | anc-policy \ 310 | -a your.server.fqdn \ 311 | -n NODENAME \ 312 | -w NODESECRET \ 313 | --insecure \ 314 | --get-anc-policies 315 | ``` 316 | 317 | #### Get all endpoints with an ANC policy, including the applied policy 318 | 319 | ``` 320 | anc-policy \ 321 | -a your.server.fqdn \ 322 | -n NODENAME \ 323 | -w NODESECRET \ 324 | --insecure \ 325 | --get-anc-endpoints 326 | ``` 327 | 328 | #### Get the policy applied to a specific endpoint, by MAC address 329 | 330 | ``` 331 | anc-policy \ 332 | -a your.server.fqdn \ 333 | -n NODENAME \ 334 | -w NODESECRET \ 335 | --insecure \ 336 | --get-anc-policy-by-mac \ 337 | --anc-mac-address 02:42:0A:14:04:23 338 | ``` 339 | 340 | #### Apply policy by MAC address 341 | 342 | The MAC address specified does not need to be for an active session. 343 | 344 | ``` 345 | anc-policy \ 346 | -a your.server.fqdn \ 347 | -n NODENAME \ 348 | -w NODESECRET \ 349 | --insecure \ 350 | --apply-anc-policy-by-mac \ 351 | --anc-mac-address 02:42:0A:14:04:23 \ 352 | --anc-policy YOUR_POLICY 353 | ``` 354 | 355 | #### Clear policy by MAC address 356 | 357 | The MAC address specified does not need to be for an active session. 358 | 359 | ``` 360 | anc-policy \ 361 | -a your.server.fqdn \ 362 | -n NODENAME \ 363 | -w NODESECRET \ 364 | --insecure \ 365 | --clear-anc-policy-by-mac \ 366 | --anc-mac-address 02:42:0A:14:04:23 367 | ``` 368 | 369 | #### Apply policy by IP address 370 | 371 | Note that ISE will map from the IP address to the MAC address of an **active** session for this command. 372 | 373 | ``` 374 | anc-policy \ 375 | -a your.server.fqdn \ 376 | -n NODENAME \ 377 | -w NODESECRET \ 378 | --insecure \ 379 | --apply-anc-policy-by-ip \ 380 | --anc-ip-address 1.2.3.4 \ 381 | --anc-policy YOUR_POLICY 382 | ``` 383 | 384 | #### Clear policy by IP address 385 | 386 | Note that ISE will map from the IP address to the MAC address of an **active** session for this command. 387 | 388 | ``` 389 | anc-policy \ 390 | -a your.server.fqdn \ 391 | -n NODENAME \ 392 | -w NODESECRET \ 393 | --insecure \ 394 | --clear-anc-policy-by-ip \ 395 | --anc-ip-address 1.2.3.4 396 | ``` 397 | 398 | 399 | ## To Generate pxGrid Certificates From ISE 400 | 401 | If you wish to mutual cert-based authentication: 402 | 403 | - Navigate to ISE Admin GUI via any web browser and authorized login 404 | - Navigate to Administration -> pxGrid Services 405 | - Click on the Certificates tab 406 | - Fill in the form as follows: 407 | - I want to: **Generate a single certificate (without a certificate signing request)** 408 | - Common Name (CN): {fill in any name} 409 | - Certificate Download Format: Certificate in Privacy Enhanced Electronic Mail (PEM) format, key in PKCS8 PEM format (including certificate chain) 410 | - Certificate Password: {fill in a password} 411 | - Confirm Password: {fill in the same password as above} 412 | - Click the 'Create' button. A zip file should download to your machine 413 | - Extract the downloaded file. 414 | 415 | -------------------------------------------------------------------------------- /bin/anc-policy: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | # 3 | # Copyright (c) 2021 Cisco Systems, Inc. and/or its affiliates 4 | # 5 | from pxgrid_util import PXGridControl 6 | from pxgrid_util import Config 7 | from pxgrid_util import create_override_url 8 | import urllib.request 9 | import base64 10 | import time 11 | import logging 12 | import json 13 | import sys 14 | import asyncio 15 | import aiohttp 16 | 17 | 18 | logger = logging.getLogger(__name__) 19 | 20 | 21 | def query(config, secret, url, payload): 22 | handler = urllib.request.HTTPSHandler(context=config.ssl_context) 23 | opener = urllib.request.build_opener(handler) 24 | rest_request = urllib.request.Request(url=url, data=str.encode(payload)) 25 | rest_request.add_header('Content-Type', 'application/json') 26 | rest_request.add_header('Accept', 'application/json') 27 | b64 = base64.b64encode((config.node_name + ':' + secret).encode()).decode() 28 | rest_request.add_header('Authorization', 'Basic ' + b64) 29 | rest_response = opener.open(rest_request) 30 | return rest_response.read().decode() 31 | 32 | 33 | # parallelism 34 | NUM_COROUTINES = 20 35 | 36 | # queue for all the MAC addresses to apply policy to 37 | mac_queue = asyncio.Queue() 38 | 39 | 40 | async def anc_applier( 41 | q=None, 42 | config=None, 43 | secret=None, 44 | url=None, 45 | policy=None): 46 | 47 | assert q is not None 48 | assert config is not None 49 | assert secret is not None 50 | assert url is not None 51 | assert policy is not None 52 | 53 | # what're we doing? 54 | while not q.empty(): 55 | 56 | # create a session 57 | auth = aiohttp.BasicAuth(config.node_name, secret) 58 | conn = aiohttp.TCPConnector(ssl=config.ssl_context) 59 | async with aiohttp.ClientSession(auth=auth, connector=conn) as session: 60 | 61 | # pull from the queue until empty 62 | while not q.empty(): 63 | m = await q.get() 64 | payload = json.dumps({ 65 | 'macAddress': m, 66 | 'policyName': policy, 67 | }) 68 | 69 | # applies the ANC policy 70 | response = await session.post( 71 | url=url, 72 | headers={ 73 | "Accept": "application/json", 74 | "Content-Type": "application/json", 75 | }, 76 | data=payload, 77 | ) 78 | 79 | # tell the world how cool we are 80 | logger.info('Applied policy %s to %s, response = %d', policy, m, response.status) 81 | 82 | 83 | async def apply_anc_policy_to_macs( 84 | config=None, 85 | secret=None, 86 | url=None, 87 | policy=None, 88 | mac_address_file=None, 89 | coroutines=NUM_COROUTINES): 90 | 91 | assert config is not None 92 | assert secret is not None 93 | assert url is not None 94 | assert policy is not None 95 | assert mac_address_file is not None 96 | 97 | # what're we doing? 98 | logger.info( 99 | 'Applying policy %s, reading MAC addresses from %s, using %d coroutines', 100 | policy, 101 | mac_address_file, 102 | coroutines) 103 | 104 | # enqueue all the MAC addresses 105 | with open(mac_address_file, 'r') as macs: 106 | for m in macs.readlines(): 107 | m = m[:-1] 108 | mac_queue.put_nowait(m) 109 | 110 | # start all the coroutines 111 | anc_tasks = [ 112 | asyncio.create_task(anc_applier( 113 | q=mac_queue, 114 | config=config, 115 | secret=secret, 116 | url=url, 117 | policy=policy)) 118 | for t in range(0, coroutines) 119 | ] 120 | 121 | # wait for the workers to be done 122 | await asyncio.gather(*anc_tasks) 123 | 124 | 125 | def apply_bulk_anc_policy_by_mac(config, secret, url, bulk_policy, bulk_mac_addrs_file): 126 | ''' 127 | Apply ANC policy in bulk using `asyncio` techniques. 128 | ''' 129 | asyncio.get_event_loop().run_until_complete( 130 | apply_anc_policy_to_macs( 131 | config=config, 132 | secret=secret, 133 | url=url, 134 | policy=bulk_policy, 135 | mac_address_file=bulk_mac_addrs_file, 136 | coroutines=NUM_COROUTINES, 137 | )) 138 | 139 | 140 | if __name__ == '__main__': 141 | config = Config() 142 | 143 | # 144 | # verbose logging if configured 145 | # 146 | if config.verbose: 147 | handler = logging.StreamHandler() 148 | handler.setFormatter(logging.Formatter('%(asctime)s:%(name)s:%(levelname)s:%(message)s')) 149 | logger.addHandler(handler) 150 | logger.setLevel(logging.DEBUG) 151 | 152 | # and set for stomp and ws_stomp modules also 153 | for modname in ['pxgrid_util.stomp', 'pxgrid_util.ws_stomp', 'pxgrid_util.pxgrid']: 154 | s_logger = logging.getLogger(modname) 155 | handler.setFormatter(logging.Formatter('%(asctime)s:%(name)s:%(levelname)s:%(message)s')) 156 | s_logger.addHandler(handler) 157 | s_logger.setLevel(logging.DEBUG) 158 | 159 | pxgrid = PXGridControl(config=config) 160 | 161 | while pxgrid.account_activate()['accountState'] != 'ENABLED': 162 | time.sleep(60) 163 | 164 | # lookup for session service 165 | service_lookup_response = pxgrid.service_lookup('com.cisco.ise.config.anc') 166 | service = service_lookup_response['services'][0] 167 | node_name = service['nodeName'] 168 | 169 | 170 | secret = pxgrid.get_access_secret(node_name)['secret'] 171 | logger.info('Using access secret %s', secret) 172 | payload = {} 173 | bulk_policy = None 174 | bulk_mac_addrs_file = None 175 | 176 | if config.get_anc_endpoints: 177 | url = service['properties']['restBaseUrl'] + '/getEndpoints' 178 | 179 | elif config.get_anc_policies: 180 | url = service['properties']['restBaseUrl'] + '/getPolicies' 181 | 182 | elif config.apply_anc_policy: 183 | assert config.anc_policy 184 | assert config.anc_mac_address 185 | url = service['properties']['restBaseUrl'] + '/applyEndpointPolicy' 186 | payload['macAddress'] = config.anc_mac_address 187 | payload['policyName'] = config.anc_policy 188 | 189 | elif config.apply_anc_policy_by_mac: 190 | assert config.anc_policy 191 | assert config.anc_mac_address 192 | url = service['properties']['restBaseUrl'] + '/applyEndpointByMacAddress' 193 | payload['macAddress'] = config.anc_mac_address 194 | payload['policyName'] = config.anc_policy 195 | 196 | elif config.apply_anc_policy_by_mac_bulk: 197 | assert config.anc_policy 198 | url = service['properties']['restBaseUrl'] + '/applyEndpointByMacAddress' 199 | bulk_policy = config.anc_policy 200 | bulk_mac_addrs_file = config.apply_anc_policy_by_mac_bulk 201 | 202 | elif config.apply_anc_policy_by_ip: 203 | assert config.anc_policy 204 | assert config.anc_ip_address 205 | url = service['properties']['restBaseUrl'] + '/applyEndpointByIpAddress' 206 | payload['ipAddress'] = config.anc_ip_address 207 | payload['policyName'] = config.anc_policy 208 | 209 | elif config.clear_anc_policy_by_mac: 210 | assert config.anc_mac_address 211 | url = service['properties']['restBaseUrl'] + '/clearEndpointByMacAddress' 212 | payload['macAddress'] = config.anc_mac_address 213 | 214 | elif config.clear_anc_policy_by_ip: 215 | assert config.anc_ip_address 216 | url = service['properties']['restBaseUrl'] + '/clearEndpointByIpAddress' 217 | payload['ipAddress'] = config.anc_ip_address 218 | 219 | elif config.create_anc_policy: 220 | assert config.anc_policy_action 221 | url = service['properties']['restBaseUrl'] + '/createPolicy' 222 | payload['name'] = config.create_anc_policy 223 | payload['actions'] = [ config.anc_policy_action.__str__() ] 224 | 225 | elif config.delete_anc_policy: 226 | url = service['properties']['restBaseUrl'] + '/deletePolicyByName' 227 | payload['name'] = config.delete_anc_policy 228 | 229 | elif config.get_anc_policy_by_mac: 230 | assert config.anc_mac_address 231 | url = service['properties']['restBaseUrl'] + '/getEndpointByMacAddress' 232 | payload['macAddress'] = config.anc_mac_address 233 | 234 | else: 235 | logger.debug('no valid options for getting, applying or removing ANC policy') 236 | sys.exit(1) 237 | 238 | # log url to see what we get via discovery 239 | logger.info('Using URL %s', url) 240 | 241 | # check to see if we need to override the URL 242 | if config.discovery_override: 243 | url = create_override_url(config, url) 244 | 245 | # make the request!! 246 | if not bulk_policy: 247 | payload = json.dumps(payload) 248 | logger.info('payload = %s', payload) 249 | resp = query(config, secret, url, payload) 250 | if len(resp) != 0: 251 | print(json.dumps(json.loads(resp), indent=2, sort_keys=True)) 252 | else: 253 | print('{}') 254 | else: 255 | apply_bulk_anc_policy_by_mac(config, secret, url, bulk_policy, bulk_mac_addrs_file) 256 | 257 | -------------------------------------------------------------------------------- /bin/create-new-pxgrid-account: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | # 3 | # Copyright (c) 2021 Cisco Systems, Inc. and/or its affiliates 4 | # 5 | from __future__ import print_function 6 | 7 | import logging 8 | import json 9 | import base64 10 | import urllib.request 11 | import sys 12 | import errno 13 | from pxgrid_util import CreateAccountConfig 14 | 15 | 16 | def eprint(*args, **kwargs): 17 | print(*args, file=sys.stderr, **kwargs) 18 | 19 | 20 | logger = logging.getLogger(__name__) 21 | 22 | 23 | if __name__ == '__main__': 24 | config = CreateAccountConfig() 25 | 26 | # verbose logging if configured 27 | if config.verbose: 28 | handler = logging.StreamHandler() 29 | handler.setFormatter(logging.Formatter('%(asctime)s:%(name)s:%(levelname)s:%(message)s')) 30 | logger.addHandler(handler) 31 | logger.setLevel(logging.DEBUG) 32 | 33 | 34 | # build an opener 35 | handler = urllib.request.HTTPSHandler(context=config.ssl_context) 36 | opener = urllib.request.build_opener(handler) 37 | 38 | 39 | ## 1. AccountCreate 40 | logger.info('AccountCreate, nodename=%s', config.nodename) 41 | rest_request = urllib.request.Request( 42 | url='https://{}:{}{}'.format( 43 | config.hostname, 44 | 8910, 45 | '/pxgrid/control/AccountCreate'), 46 | method='POST', 47 | data=json.dumps({'nodeName': config.nodename}).encode()) 48 | rest_request.add_header('Content-Type', 'application/json') 49 | rest_request.add_header('Accept', 'application/json') 50 | try: 51 | rest_response = opener.open(rest_request) 52 | except urllib.error.HTTPError as e: 53 | logger.error('Failed to create account, code=%d, reason=%s', e.code, e.reason) 54 | sys.exit(errno.EINVAL) 55 | account_details = json.loads(rest_response.read().decode()) 56 | 57 | 58 | ## 2. AccountActivate -- with node name and password from previous request 59 | logger.info( 60 | 'AccountActivate, nodename=%s, password=%s', 61 | account_details['nodeName'], 62 | account_details['password']) 63 | rest_request = urllib.request.Request( 64 | url='https://{}:{}{}'.format( 65 | config.hostname, 66 | 8910, 67 | '/pxgrid/control/AccountActivate'), 68 | method='POST', 69 | data=json.dumps({'description': config.description}).encode()) 70 | rest_request.add_header('Content-Type', 'application/json') 71 | rest_request.add_header('Accept', 'application/json') 72 | b64 = base64.b64encode('{}:{}'.format( 73 | account_details['userName'], 74 | account_details['password']).encode()).decode() 75 | rest_request.add_header('Authorization', 'Basic ' + b64) 76 | try: 77 | rest_response = opener.open(rest_request) 78 | except urllib.error.HTTPError as e: 79 | logger.error('Failed to activate account, code=%d, reason=%s', e.code, e.reason) 80 | sys.exit(errno.EINVAL) 81 | account_activate = json.loads(rest_response.read().decode()) 82 | 83 | 84 | ## 3. PUT /ers/config/pxgridNode/name/{name}/approve 85 | logger.info( 86 | 'Account Approve, nodename=%s, password=%s', 87 | account_details['nodeName'], 88 | account_details['password']) 89 | rest_request = urllib.request.Request( 90 | url='https://{}:{}{}'.format( 91 | config.hostname, 92 | 9060, 93 | '/ers/config/pxgridNode/name/{}/approve'.format( 94 | account_details['nodeName'])), 95 | method='PUT', 96 | data=json.dumps({}).encode()) 97 | rest_request.add_header('Content-Type', 'application/json') 98 | rest_request.add_header('Accept', 'application/json') 99 | rest_request.add_header('ERS-Media-Type', 'pxgrid.pxgridnode.1.0') 100 | b64 = base64.b64encode('{}:{}'.format(config.username, config.password).encode()).decode() 101 | rest_request.add_header('Authorization', 'Basic ' + b64) 102 | try: 103 | rest_response = opener.open(rest_request) 104 | except urllib.error.HTTPError as e: 105 | logger.error('Failed to approve account, code=%d, reason=%s', e.code, e.reason) 106 | sys.exit(errno.EINVAL) 107 | 108 | 109 | # if we got a 204 now, we're done 110 | if rest_response.status == 204: 111 | details = { 112 | 'nodeName': account_details['nodeName'], 113 | 'password': account_details['password'], 114 | 'hostname': config.hostname, 115 | } 116 | print(json.dumps(details, indent=2, sort_keys=True)) 117 | else: 118 | logger.error('\nApologies, this utility doesn\'t have complete diagnostics or error recovery :(') 119 | logger.error('You probably need to debug the ISE installation you wre talking to!\n') 120 | -------------------------------------------------------------------------------- /bin/endpoint-query-all: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | # 3 | # Copyright (c) 2021 Cisco Systems, Inc. and/or its affiliates 4 | # 5 | from pxgrid_util import PXGridControl 6 | from pxgrid_util import Config 7 | from pxgrid_util import create_override_url 8 | import urllib.request 9 | import base64 10 | import time 11 | import logging 12 | import json 13 | 14 | logger = logging.getLogger(__name__) 15 | 16 | 17 | def query(config, secret, url, payload): 18 | handler = urllib.request.HTTPSHandler(context=config.ssl_context) 19 | opener = urllib.request.build_opener(handler) 20 | rest_request = urllib.request.Request(url=url, data=str.encode(payload)) 21 | rest_request.add_header('Content-Type', 'application/json') 22 | rest_request.add_header('Accept', 'application/json') 23 | b64 = base64.b64encode((config.node_name + ':' + secret).encode()).decode() 24 | rest_request.add_header('Authorization', 'Basic ' + b64) 25 | rest_response = opener.open(rest_request) 26 | return rest_response.read().decode() 27 | 28 | 29 | if __name__ == '__main__': 30 | config = Config() 31 | 32 | # 33 | # verbose logging if configured 34 | # 35 | if config.verbose: 36 | handler = logging.StreamHandler() 37 | handler.setFormatter(logging.Formatter('%(asctime)s:%(name)s:%(levelname)s:%(message)s')) 38 | logger.addHandler(handler) 39 | logger.setLevel(logging.DEBUG) 40 | 41 | # and set for stomp and ws_stomp modules also 42 | for modname in ['pxgrid_util.stomp', 'pxgrid_util.ws_stomp', 'pxgrid_util.pxgrid']: 43 | s_logger = logging.getLogger(modname) 44 | handler.setFormatter(logging.Formatter('%(asctime)s:%(name)s:%(levelname)s:%(message)s')) 45 | s_logger.addHandler(handler) 46 | s_logger.setLevel(logging.DEBUG) 47 | 48 | pxgrid = PXGridControl(config=config) 49 | 50 | while pxgrid.account_activate()['accountState'] != 'ENABLED': 51 | time.sleep(60) 52 | 53 | # lookup for endpoint service 54 | service_lookup_response = pxgrid.service_lookup('com.cisco.ise.endpoint') 55 | service = service_lookup_response['services'][0] 56 | node_name = service['nodeName'] 57 | url = service['properties']['restBaseUrl'] + '/getEndpoints' 58 | 59 | # log url to see what we get via discovery 60 | logger.info('Using URL %s', url) 61 | 62 | # check to see if we need to override the URL 63 | if config.discovery_override: 64 | url = create_override_url(config, url) 65 | 66 | secret = pxgrid.get_access_secret(node_name)['secret'] 67 | logger.info('Using access secret %s', secret) 68 | if config.start_timestamp: 69 | payload = { 70 | 'startTimestamp': config.start_timestamp 71 | } 72 | resp = query(config, secret, url, json.dumps(payload)) 73 | else: 74 | resp = query(config, secret, url, '{}') 75 | print(json.dumps(json.loads(resp), indent=2, sort_keys=True)) 76 | -------------------------------------------------------------------------------- /bin/matrix-query-all: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | # 3 | # Copyright (c) 2021 Cisco Systems, Inc. and/or its affiliates 4 | # 5 | from pxgrid_util import PXGridControl 6 | from pxgrid_util import Config 7 | from pxgrid_util import create_override_url 8 | import urllib.request 9 | import base64 10 | import time 11 | import logging 12 | import json 13 | 14 | logger = logging.getLogger(__name__) 15 | 16 | 17 | def query(config, secret, url, payload): 18 | handler = urllib.request.HTTPSHandler(context=config.ssl_context) 19 | opener = urllib.request.build_opener(handler) 20 | rest_request = urllib.request.Request(url=url, data=str.encode(payload)) 21 | rest_request.add_header('Content-Type', 'application/json') 22 | rest_request.add_header('Accept', 'application/json') 23 | b64 = base64.b64encode((config.node_name + ':' + secret).encode()).decode() 24 | rest_request.add_header('Authorization', 'Basic ' + b64) 25 | rest_response = opener.open(rest_request) 26 | return rest_response.read().decode() 27 | 28 | 29 | if __name__ == '__main__': 30 | config = Config() 31 | 32 | # 33 | # verbose logging if configured 34 | # 35 | if config.verbose: 36 | handler = logging.StreamHandler() 37 | handler.setFormatter(logging.Formatter('%(asctime)s:%(name)s:%(levelname)s:%(message)s')) 38 | logger.addHandler(handler) 39 | logger.setLevel(logging.DEBUG) 40 | 41 | # and set for stomp and ws_stomp modules also 42 | for modname in ['pxgrid_util.stomp', 'pxgrid_util.ws_stomp', 'pxgrid_util.pxgrid']: 43 | s_logger = logging.getLogger(modname) 44 | handler.setFormatter(logging.Formatter('%(asctime)s:%(name)s:%(levelname)s:%(message)s')) 45 | s_logger.addHandler(handler) 46 | s_logger.setLevel(logging.DEBUG) 47 | 48 | pxgrid = PXGridControl(config=config) 49 | 50 | while pxgrid.account_activate()['accountState'] != 'ENABLED': 51 | time.sleep(60) 52 | 53 | # lookup for session service 54 | service_lookup_response = pxgrid.service_lookup('com.cisco.ise.config.trustsec') 55 | service = service_lookup_response['services'][0] 56 | node_name = service['nodeName'] 57 | url = service['properties']['restBaseUrl'] + '/getEgressPolicies' 58 | 59 | # log url to see what we get via discovery 60 | logger.info('Using URL %s', url) 61 | 62 | # check to see if we need to override the URL 63 | if config.discovery_override: 64 | url = create_override_url(config, url) 65 | 66 | secret = pxgrid.get_access_secret(node_name)['secret'] 67 | logger.info('Using access secret %s', secret) 68 | resp = query(config, secret, url, '{}') 69 | print(json.dumps(json.loads(resp), indent=2, sort_keys=True)) 70 | 71 | -------------------------------------------------------------------------------- /bin/profiles-query-all: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | # 3 | # Copyright (c) 2021 Cisco Systems, Inc. and/or its affiliates 4 | # 5 | from pxgrid_util import PXGridControl 6 | from pxgrid_util import Config 7 | from pxgrid_util import create_override_url 8 | import urllib.request 9 | import base64 10 | import time 11 | import logging 12 | import json 13 | 14 | logger = logging.getLogger(__name__) 15 | 16 | 17 | def query(config, secret, url, payload): 18 | handler = urllib.request.HTTPSHandler(context=config.ssl_context) 19 | opener = urllib.request.build_opener(handler) 20 | rest_request = urllib.request.Request(url=url, data=str.encode(payload)) 21 | rest_request.add_header('Content-Type', 'application/json') 22 | rest_request.add_header('Accept', 'application/json') 23 | b64 = base64.b64encode((config.node_name + ':' + secret).encode()).decode() 24 | rest_request.add_header('Authorization', 'Basic ' + b64) 25 | rest_response = opener.open(rest_request) 26 | return rest_response.read().decode() 27 | 28 | 29 | if __name__ == '__main__': 30 | config = Config() 31 | 32 | # 33 | # verbose logging if configured 34 | # 35 | if config.verbose: 36 | handler = logging.StreamHandler() 37 | handler.setFormatter(logging.Formatter('%(asctime)s:%(name)s:%(levelname)s:%(message)s')) 38 | logger.addHandler(handler) 39 | logger.setLevel(logging.DEBUG) 40 | 41 | # and set for stomp and ws_stomp modules also 42 | for modname in ['pxgrid_util.stomp', 'pxgrid_util.ws_stomp', 'pxgrid_util.pxgrid']: 43 | s_logger = logging.getLogger(modname) 44 | handler.setFormatter(logging.Formatter('%(asctime)s:%(name)s:%(levelname)s:%(message)s')) 45 | s_logger.addHandler(handler) 46 | s_logger.setLevel(logging.DEBUG) 47 | 48 | pxgrid = PXGridControl(config=config) 49 | 50 | while pxgrid.account_activate()['accountState'] != 'ENABLED': 51 | time.sleep(60) 52 | 53 | # lookup for session service 54 | service_lookup_response = pxgrid.service_lookup('com.cisco.ise.config.profiler') 55 | service = service_lookup_response['services'][0] 56 | node_name = service['nodeName'] 57 | url = service['properties']['restBaseUrl'] + '/getProfiles' 58 | 59 | # log url to see what we get via discovery 60 | logger.info('Using URL %s', url) 61 | 62 | # check to see if we need to override the URL 63 | if config.discovery_override: 64 | url = create_override_url(config, url) 65 | 66 | secret = pxgrid.get_access_secret(node_name)['secret'] 67 | logger.info('Using access secret %s', secret) 68 | resp = query(config, secret, url, '{}') 69 | print(json.dumps(json.loads(resp), indent=2, sort_keys=True)) 70 | 71 | -------------------------------------------------------------------------------- /bin/px-publish: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | # 3 | # Copyright (c) 2021 Cisco Systems, Inc. and/or its affiliates 4 | # 5 | from io import StringIO 6 | from pxgrid_util import PXGridControl 7 | from pxgrid_util import Config 8 | from pxgrid_util import create_override_url 9 | import asyncio 10 | from asyncio.tasks import FIRST_COMPLETED 11 | import json 12 | import sys 13 | import time 14 | import logging 15 | import threading 16 | import hashlib 17 | from websockets import ConnectionClosed 18 | from websockets.exceptions import WebSocketException 19 | from pxgrid_util import WebSocketStomp 20 | from signal import SIGINT, SIGTERM 21 | 22 | 23 | # 24 | # the global logger 25 | # 26 | logger = logging.getLogger(__name__) 27 | 28 | 29 | async def default_service_reregister_loop(config, pxgrid, service_id, reregister_delay): 30 | ''' 31 | Simple custom service reregistration to keep things alive. 32 | ''' 33 | try: 34 | while True: 35 | await asyncio.sleep(reregister_delay) 36 | try: 37 | resp = pxgrid.service_reregister(service_id) 38 | logger.debug( 39 | '[default_service_reregister_loop] service reregister response %s', 40 | json.dumps(resp)) 41 | except Exception as e: 42 | logger.debug( 43 | '[default_service_reregister_loop] failed to reregister, Exception: %s', 44 | e.__str__()) 45 | 46 | # pull service back to check 47 | service_lookup_response = pxgrid.service_lookup(config.service) 48 | service = service_lookup_response['services'][0] 49 | debug_text = json.dumps(resp, indent=2, sort_keys=True) 50 | for debug_line in debug_text.splitlines(): 51 | logger.debug('[default_publish_loop] service_register_response %s', debug_line) 52 | 53 | except asyncio.CancelledError as e: 54 | logger.debug('[default_service_reregister_loop] reregister loop cancelled') 55 | 56 | 57 | async def default_publish_loop(config, secret, pubsub_node_name, ws_url, topic): 58 | ''' 59 | Simple publish loop just to send some canned data. 60 | ''' 61 | if config.discovery_override: 62 | logger.info('[default_publish_loop] overriding original URL %s', ws_url) 63 | ws_url = create_override_url(config, ws_url) 64 | logger.info('[default_publish_loop] new URL %s', ws_url) 65 | 66 | logger.debug('[default_publisher_loop] starting subscription to %s at %s', topic, ws_url) 67 | 68 | logger.debug('[default_publish_loop] opening web socket and stomp') 69 | ws = WebSocketStomp( 70 | ws_url, 71 | config.node_name, 72 | secret, 73 | config.ssl_context, 74 | # ping_interval=None) 75 | ping_interval=config.ws_ping_interval) 76 | 77 | try: 78 | logger.debug('[default_publish_loop] connect websocket') 79 | await ws.connect() 80 | logger.debug('[default_publish_loop] connect STOMP node %s', pubsub_node_name) 81 | await ws.stomp_connect(pubsub_node_name) 82 | except Exception as e: 83 | logger.debug('[default_publish_loop] failed to connect, Exception: %s', e.__str__()) 84 | return 85 | try: 86 | count = 0 87 | while True: 88 | 89 | await asyncio.sleep(1.0) 90 | count += 1 91 | message = { 92 | 'count': count, 93 | 'data': 'cool and froody', 94 | } 95 | try: 96 | await ws.stomp_send(topic, json.dumps(message)) 97 | except Exception as e: 98 | logger.debug( 99 | '[default_publish_loop] Exception: %s', 100 | e.__str__()) 101 | logger.debug( 102 | '[default_publish_loop] message published to node %s, topic %s', 103 | pubsub_node_name, 104 | topic) 105 | sys.stdout.flush() 106 | except asyncio.CancelledError as e: 107 | pass 108 | except WebSocketException as e: 109 | logger.debug( 110 | '[default_publish_loop] WebSocketException: %s', 111 | e.__str__()) 112 | return 113 | 114 | logger.debug('[default_publish_loop] shutting down publisher...') 115 | await ws.stomp_disconnect('123') 116 | await asyncio.sleep(2.0) 117 | await ws.disconnect() 118 | 119 | 120 | if __name__ == '__main__': 121 | 122 | # 123 | # this will parse all the CLI options 124 | # 125 | config = Config() 126 | 127 | # 128 | # verbose logging if configured 129 | # 130 | if config.verbose: 131 | handler = logging.StreamHandler() 132 | handler.setFormatter(logging.Formatter('%(asctime)s:%(name)s:%(levelname)s:%(message)s')) 133 | logger.addHandler(handler) 134 | logger.setLevel(logging.DEBUG) 135 | 136 | # and set for stomp and ws_stomp modules also 137 | for modname in ['pxgrid_util.stomp', 'pxgrid_util.ws_stomp', 'pxgrid_util.pxgrid']: 138 | s_logger = logging.getLogger(modname) 139 | handler.setFormatter(logging.Formatter('%(asctime)s:%(name)s:%(levelname)s:%(message)s')) 140 | s_logger.addHandler(handler) 141 | s_logger.setLevel(logging.DEBUG) 142 | 143 | # 144 | # we need a hostname and both a service name and a topic short name; 145 | # note that other checks may also fail! 146 | # 147 | if not config.hostname: 148 | print("No hostname!") 149 | sys.exit(0) 150 | if not config.service: 151 | print('Need a service to register and name pub-sub STOMP topic') 152 | sys.exit(1) 153 | if not config.topic: 154 | print('Need a topic short name to register') 155 | sys.exit(1) 156 | 157 | # 158 | # if we have met the basic criteria then we can move forward and set up 159 | # the px grid control object 160 | # 161 | pxgrid = PXGridControl(config=config) 162 | 163 | # 164 | # in case we need to go appropve in the ISE UI 165 | # 166 | while pxgrid.account_activate()['accountState'] != 'ENABLED': 167 | time.sleep(60) 168 | 169 | # 170 | # register a custom service 171 | # 172 | properties = { 173 | 'wsPubsubService': 'com.cisco.ise.pubsub', 174 | f'{config.topic}': f'/topic/{config.service}', 175 | # 'restBaseUrl': 'https://localhost', 176 | # 'bulkDownload': 'bulkDownload', 177 | } 178 | resp = pxgrid.service_register(config.service, properties) 179 | debug_text = json.dumps(resp, indent=2, sort_keys=True) 180 | for debug_line in debug_text.splitlines(): 181 | logger.debug('[service_register_response] %s', debug_line) 182 | 183 | # 184 | # setup periodic service reregistration as a task 185 | # 186 | reregister_task = asyncio.ensure_future( 187 | default_service_reregister_loop( 188 | config, 189 | pxgrid, 190 | resp['id'], 191 | config.reregister_delay, 192 | )) 193 | 194 | # 195 | # now lookup service and topic details for the service we just registered 196 | # 197 | service_lookup_response = pxgrid.service_lookup(config.service) 198 | slr_string = json.dumps(service_lookup_response, indent=2, sort_keys=True) 199 | logger.debug('service lookup response:') 200 | for s in slr_string.splitlines(): 201 | logger.debug(' %s', s) 202 | service = service_lookup_response['services'][0] 203 | pubsub_service_name = service['properties']['wsPubsubService'] 204 | try: 205 | topic = service['properties'][config.topic] 206 | except KeyError as e: 207 | logger.debug('invalid topic %s', config.topic) 208 | possible_topics = [ 209 | k for k in service['properties'].keys() 210 | if k != 'wsPubsubService' and k != 'restBaseUrl' and k != 'restBaseURL' 211 | ] 212 | logger.debug('possible topic handles: %s', ', '.join(possible_topics)) 213 | sys.exit(1) 214 | 215 | # 216 | # lookup the pubsub service 217 | # 218 | service_lookup_response = pxgrid.service_lookup(pubsub_service_name) 219 | 220 | # 221 | # just use the first pubsub service node returned (there is randomness) 222 | # 223 | pubsub_service = service_lookup_response['services'][0] 224 | pubsub_node_name = pubsub_service['nodeName'] 225 | secret = pxgrid.get_access_secret(pubsub_node_name)['secret'] 226 | ws_url = pubsub_service['properties']['wsUrl'] 227 | 228 | # 229 | # setup the publishing loop 230 | # 231 | main_task = asyncio.ensure_future( 232 | default_publish_loop( 233 | config, 234 | secret, 235 | pubsub_node_name, 236 | ws_url, 237 | topic, 238 | )) 239 | 240 | # 241 | # setup sigint/sigterm handlers 242 | # 243 | def signal_handlers(): 244 | main_task.cancel() 245 | reregister_task.cancel() 246 | loop = asyncio.get_event_loop() 247 | loop.add_signal_handler(SIGINT, signal_handlers) 248 | loop.add_signal_handler(SIGTERM, signal_handlers) 249 | 250 | # 251 | # finally, get going!! 252 | # 253 | try: 254 | loop.run_until_complete(main_task) 255 | except: 256 | pass 257 | -------------------------------------------------------------------------------- /bin/px-subscribe: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | # 3 | # Copyright (c) 2021 Cisco Systems, Inc. and/or its affiliates 4 | # 5 | from pxgrid_util import PXGridControl 6 | from pxgrid_util import Config 7 | from pxgrid_util import create_override_url 8 | import asyncio 9 | from asyncio.tasks import FIRST_COMPLETED 10 | import json 11 | import sys 12 | import time 13 | import logging 14 | import threading 15 | import hashlib 16 | from websockets import ConnectionClosed 17 | from websockets.exceptions import WebSocketException 18 | from pxgrid_util import WebSocketStomp 19 | from signal import SIGINT, SIGTERM 20 | 21 | 22 | # 23 | # the global logger 24 | # 25 | logger = logging.getLogger(__name__) 26 | 27 | 28 | # 29 | # lock for deduplicating session events received 30 | # 31 | dedup_lock = threading.Lock() 32 | 33 | 34 | # 35 | # dictionary for storing event keys in 36 | # 37 | # TODO: this really needs a cleaner to remove old events 38 | # 39 | event_keys = {} 40 | 41 | 42 | # 43 | # Definitions of ISE pxGrid 2.0 service names valid when this script was was 44 | # written or updated. 45 | # 46 | SERVICE_NAMES = [ 47 | "com.cisco.endpoint.asset", 48 | "com.cisco.endpoint", 49 | "com.cisco.ise.config.anc", 50 | "com.cisco.ise.config.anc", 51 | "com.cisco.ise.config.profiler", 52 | "com.cisco.ise.config.trustsec", 53 | "com.cisco.ise.mdm", 54 | "com.cisco.ise.pubsub", 55 | "com.cisco.ise.radius", 56 | "com.cisco.ise.session", 57 | "com.cisco.ise.sxp", 58 | "com.cisco.ise.system", 59 | "com.cisco.ise.trustsec", 60 | ] 61 | 62 | 63 | async def future_read_message(ws, future): 64 | try: 65 | message = await ws.stomp_read_message() 66 | future.set_result(message) 67 | except ConnectionClosed: 68 | logger.debug('Websocket connection closed') 69 | 70 | 71 | async def default_subscription_loop(config, secret, pubsub_node_name, ws_url, topic): 72 | ''' 73 | Simple subscription loop just to display whatever events arrive. 74 | ''' 75 | if config.discovery_override: 76 | logger.info('Overriding original URL %s', ws_url) 77 | ws_url = create_override_url(config, ws_url) 78 | logger.info('New URL %s', ws_url) 79 | 80 | logger.debug('default_subscription_loop: starting subscription to %s at %s', topic, ws_url) 81 | ws = WebSocketStomp( 82 | ws_url, 83 | config.node_name, 84 | secret, 85 | config.ssl_context, 86 | ping_interval=config.ws_ping_interval) 87 | try: 88 | await ws.connect() 89 | await ws.stomp_connect(pubsub_node_name) 90 | await ws.stomp_subscribe(topic) 91 | except Exception as e: 92 | logger.debug('Failed to connect, Exception: %s', e.__str__()) 93 | return 94 | try: 95 | while True: 96 | message = json.loads(await ws.stomp_read_message()) 97 | logger.debug('[%s] message received', pubsub_node_name) 98 | print(json.dumps(message, indent=2, sort_keys=True), file=sys.stdout) 99 | sys.stdout.flush() 100 | except asyncio.CancelledError as e: 101 | pass 102 | except WebSocketException as e: 103 | logger.debug('WebSocketException: %s', e.__str__()) 104 | return 105 | logger.debug('shutting down listener...') 106 | await ws.stomp_disconnect('123') 107 | await asyncio.sleep(2.0) 108 | await ws.disconnect() 109 | 110 | 111 | async def connect_only_loop(config, secret, pubsub_node_name, ws_url, topic): 112 | ''' 113 | Simple subscription loop just to display whatever events arrive. 114 | ''' 115 | if config.discovery_override: 116 | logger.info('Overriding original URL %s', ws_url) 117 | ws_url = create_override_url(config, ws_url) 118 | logger.info('New URL %s', ws_url) 119 | 120 | logger.debug('connect_only_loop: starting subscription to %s at %s', topic, ws_url) 121 | ws = WebSocketStomp( 122 | ws_url, config.node_name, secret, config.ssl_context, 123 | ping_interval=config.ws_ping_interval) 124 | try: 125 | await ws.connect() 126 | await ws.stomp_connect(pubsub_node_name) 127 | except Exception as e: 128 | logger.debug('Failed to connect, Exception: %s', e.__str__()) 129 | return 130 | try: 131 | while True: 132 | message = json.loads(await ws.stomp_read_message()) 133 | logger.debug('[%s] message received', pubsub_node_name) 134 | print(json.dumps(message, indent=2, sort_keys=True), file=sys.stdout) 135 | sys.stdout.flush() 136 | except asyncio.CancelledError as e: 137 | pass 138 | except WebSocketException as e: 139 | logger.debug('WebSocketException: %s', e.__str__()) 140 | return 141 | logger.debug('shutting down listener...') 142 | await ws.stomp_disconnect('123') 143 | await asyncio.sleep(2.0) 144 | await ws.disconnect() 145 | 146 | 147 | async def session_dedup_loop(config, secret, pubsub_node_name, ws_url, topic): 148 | ''' 149 | Subscription loop specifically for ISE pxGrid sessionTopic events. The 150 | logic for de-duplication is based around callingStationId, timestamp and 151 | event content. Multiple events may have the same callimgStationId and 152 | timestamp, but attribute changes, like profiling determining the operating 153 | system for a device, may result in events that have the same timestamp but 154 | different contents. 155 | 156 | The algorithm in this routine takes this into account, and will "de- 157 | duplicate" the events (i.e. tell you when a duplicate event arrived). It 158 | uses MD5 (for speed) on a key-sorted dump of the event (which ensures that 159 | duplicate events are detected by the hash digest differing.) 160 | ''' 161 | if config.discovery_override: 162 | logger.info('Overriding original URL %s', ws_url) 163 | ws_url = create_override_url(config, ws_url) 164 | logger.info('New URL %s', ws_url) 165 | 166 | logger.debug('session_dedup_loop: starting subscription to %s at %s', topic, ws_url) 167 | assert topic == '/topic/com.cisco.ise.session', '%s is not the sessionTopic' 168 | 169 | ws = WebSocketStomp( 170 | ws_url, config.node_name, secret, config.ssl_context, 171 | ping_interval=config.ws_ping_interval) 172 | try: 173 | await ws.connect() 174 | await ws.stomp_connect(pubsub_node_name) 175 | await ws.stomp_subscribe(topic) 176 | except Exception as e: 177 | logger.debug('Failed to connect, Exception: %s', e.__str__()) 178 | return 179 | try: 180 | while True: 181 | message = json.loads(await ws.stomp_read_message()) 182 | logger.debug('[%s] message received', pubsub_node_name) 183 | with dedup_lock: 184 | for s in message['sessions']: 185 | event_text = json.dumps(s, indent=2, sort_keys=True) 186 | event_hash = hashlib.md5(event_text.encode()).hexdigest() 187 | event_key = '{}:{}:{}'.format( 188 | s['callingStationId'], s['timestamp'], event_hash) 189 | if event_keys.get(event_key): 190 | event_keys[event_key]['count'] = event_keys[event_key]['count'] + 1 191 | print('duplicate mac:timestamp:hash event, count {}'.format( 192 | event_keys[event_key]['count'])) 193 | print(' --> {}'.format(ws_url)) 194 | else: 195 | event_keys[event_key] = {} 196 | event_keys[event_key]['count'] = 1 197 | event_keys[event_key]['time'] = time.time() 198 | event_keys[event_key]['event'] = event_text 199 | event_keys[event_key]['md5'] = event_hash 200 | print('{}\nevent from {}'.format('-' * 75, ws_url)) 201 | print(json.dumps(s, indent=2, sort_keys=True)) 202 | sys.stdout.flush() 203 | except asyncio.CancelledError as e: 204 | pass 205 | except WebSocketException as e: 206 | logger.debug('WebSocketException: %s', e.__str__()) 207 | return 208 | logger.debug('shutting down listener...') 209 | await ws.stomp_disconnect('123') 210 | await asyncio.sleep(2.0) 211 | await ws.disconnect() 212 | 213 | 214 | # subscribe to topic on ALL service nodes returned 215 | async def run_subscribe_all(task_list): 216 | logger.debug('run_subscribe_all') 217 | if len(task_list) > 0: 218 | try: 219 | return await asyncio.gather(*task_list) 220 | except asyncio.CancelledError as e: 221 | for t in task_list: 222 | t.cancel() 223 | return await asyncio.gather(*task_list) 224 | 225 | 226 | if __name__ == '__main__': 227 | 228 | # 229 | # this will parse all the CLI options, and there **must** be EITHER 230 | # a '--services' OR '--subscribe' 231 | # 232 | config = Config() 233 | 234 | # 235 | # verbose logging if configured 236 | # 237 | if config.verbose: 238 | handler = logging.StreamHandler() 239 | handler.setFormatter(logging.Formatter('%(asctime)s:%(name)s:%(levelname)s:%(message)s')) 240 | logger.addHandler(handler) 241 | logger.setLevel(logging.DEBUG) 242 | 243 | # and set for stomp and ws_stomp modules also 244 | for modname in ['pxgrid_util.stomp', 'pxgrid_util.ws_stomp', 'pxgrid_util.pxgrid']: 245 | s_logger = logging.getLogger(modname) 246 | handler.setFormatter(logging.Formatter('%(asctime)s:%(name)s:%(levelname)s:%(message)s')) 247 | s_logger.addHandler(handler) 248 | s_logger.setLevel(logging.DEBUG) 249 | 250 | 251 | # 252 | # if we jst have a request for services and no hostname, we can only 253 | # list out the services we know about 254 | # 255 | if config.services and (not config.hostname): 256 | print("Known services:") 257 | for service in sorted(SERVICE_NAMES): 258 | print(' %s' % service) 259 | sys.exit(0) 260 | 261 | # 262 | # if we at least have a hostname, we can move forward and set up the 263 | # px grid control object and look at either deeper service discovery 264 | # or just subscribing to what we're asked to subscribe to 265 | # 266 | pxgrid = PXGridControl(config=config) 267 | 268 | # 269 | # in case we need to go appropve in the ISE UI 270 | # 271 | while pxgrid.account_activate()['accountState'] != 'ENABLED': 272 | time.sleep(60) 273 | 274 | # lookup for session service 275 | if config.services: 276 | slr_responses = [] 277 | for service in SERVICE_NAMES: 278 | service_lookup_response = pxgrid.service_lookup(service) 279 | slr_responses.append(service_lookup_response) 280 | 281 | # 282 | # log for debug 283 | # 284 | slr_string = json.dumps(service_lookup_response, indent=2, sort_keys=True) 285 | logger.debug('service %s lookup response:', service) 286 | slr_string = json.dumps(service_lookup_response, indent=2, sort_keys=True) 287 | logger.debug('service lookup response:') 288 | for s in slr_string.splitlines(): 289 | logger.debug(' %s', s) 290 | 291 | # 292 | # dump all services as a json array pretty-printed 293 | # 294 | print(json.dumps(slr_responses, indent=2, sort_keys=True)) 295 | sys.exit(0) 296 | 297 | # get the details of a specific service and then exit 298 | if config.service_details: 299 | 300 | # first, the basic service 301 | service_lookup_response = pxgrid.service_lookup(config.service_details) 302 | print(json.dumps(service_lookup_response, indent=2, sort_keys=True)) 303 | 304 | # check if any of tje services have a "wsPubsubService", and, if so, 305 | # also list out those services 306 | if "services" in service_lookup_response: 307 | topics = [] 308 | for s in service_lookup_response['services']: 309 | pubsub_service = s['properties'].get('wsPubsubService') 310 | if pubsub_service: 311 | for p, v in s['properties'].items(): 312 | if 'topic' in p.lower(): 313 | topics.append({p: v, 'wsPubsubService': pubsub_service}) 314 | break 315 | 316 | # lookup the pubsub service if there is one 317 | pubsub_slr = pxgrid.service_lookup(pubsub_service) 318 | if pubsub_slr: 319 | print(json.dumps(pubsub_slr, indent=2, sort_keys=True)) 320 | 321 | # now exit 322 | sys.exit(0) 323 | 324 | # if we drop through to here, we must be subscribing, so do some initial 325 | # checks to make sure we have enough parameters 326 | if config.service is None or config.topic is None: 327 | logger.error('must have a service and a topic!') 328 | sys.exit(1) 329 | 330 | # 331 | # now subscribe 332 | # 333 | service_lookup_response = pxgrid.service_lookup(config.service) 334 | slr_string = json.dumps(service_lookup_response, indent=2, sort_keys=True) 335 | logger.debug('service lookup response:') 336 | for s in slr_string.splitlines(): 337 | logger.debug(' %s', s) 338 | service = service_lookup_response['services'][0] 339 | pubsub_service_name = service['properties']['wsPubsubService'] 340 | try: 341 | topic = service['properties'][config.topic] 342 | except KeyError as e: 343 | logger.debug('invald topic %s', config.topic) 344 | possible_topics = [k for k in service['properties'].keys() if k != 'wsPubsubService' and k != 'restBaseUrl' and k != 'restBaseURL'] 345 | logger.debug('possible topic handles: %s', ', '.join(possible_topics)) 346 | sys.exit(1) 347 | 348 | # lookup the pubsub service 349 | service_lookup_response = pxgrid.service_lookup(pubsub_service_name) 350 | 351 | # select the subscription loop 352 | subscription_loop = default_subscription_loop 353 | if config.session_dedup: 354 | subscription_loop = session_dedup_loop 355 | if config.connect_only: 356 | subscription_loop = connect_only_loop 357 | 358 | if not config.subscribe_all: 359 | 360 | # just subscribe to first pubsub service node returned 361 | pubsub_service = service_lookup_response['services'][0] 362 | pubsub_node_name = pubsub_service['nodeName'] 363 | secret = pxgrid.get_access_secret(pubsub_node_name)['secret'] 364 | ws_url = pubsub_service['properties']['wsUrl'] 365 | 366 | loop = asyncio.get_event_loop() 367 | main_task = asyncio.ensure_future(subscription_loop(config, secret, pubsub_node_name, ws_url, topic)) 368 | loop.add_signal_handler(SIGINT, main_task.cancel) 369 | loop.add_signal_handler(SIGTERM, main_task.cancel) 370 | try: 371 | loop.run_until_complete(main_task) 372 | except: 373 | pass 374 | 375 | else: 376 | 377 | # create all subscription tasks 378 | subscriber_tasks = [] 379 | loop = asyncio.get_event_loop() 380 | for pubsub_service in service_lookup_response['services']: 381 | pubsub_node_name = pubsub_service['nodeName'] 382 | secret = pxgrid.get_access_secret(pubsub_node_name)['secret'] 383 | ws_url = pubsub_service['properties']['wsUrl'] 384 | logger.debug('creating task to subscribe to %s', ws_url) 385 | task = asyncio.ensure_future(subscription_loop(config, secret, pubsub_node_name, ws_url, topic)) 386 | subscriber_tasks.append(task) 387 | 388 | # create the run all task and graceful termination handling 389 | try: 390 | logger.debug('Create run all task') 391 | run_all_task = asyncio.ensure_future(run_subscribe_all(subscriber_tasks)) 392 | logger.debug('Add signal handlers to run all task') 393 | loop.add_signal_handler(SIGINT, run_all_task.cancel) 394 | loop.add_signal_handler(SIGTERM, run_all_task.cancel) 395 | loop.run_until_complete(run_all_task) 396 | except: 397 | pass 398 | -------------------------------------------------------------------------------- /bin/session-query-all: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | # 3 | # Copyright (c) 2021 Cisco Systems, Inc. and/or its affiliates 4 | # 5 | from pxgrid_util import PXGridControl 6 | from pxgrid_util import Config 7 | from pxgrid_util import create_override_url 8 | import urllib.request 9 | import base64 10 | import time 11 | import logging 12 | import json 13 | 14 | logger = logging.getLogger(__name__) 15 | 16 | 17 | def query(config, secret, url, payload): 18 | handler = urllib.request.HTTPSHandler(context=config.ssl_context) 19 | opener = urllib.request.build_opener(handler) 20 | rest_request = urllib.request.Request(url=url, data=str.encode(payload)) 21 | rest_request.add_header('Content-Type', 'application/json') 22 | rest_request.add_header('Accept', 'application/json') 23 | b64 = base64.b64encode((config.node_name + ':' + secret).encode()).decode() 24 | rest_request.add_header('Authorization', 'Basic ' + b64) 25 | rest_response = opener.open(rest_request) 26 | return rest_response.read().decode() 27 | 28 | 29 | if __name__ == '__main__': 30 | config = Config() 31 | 32 | # 33 | # verbose logging if configured 34 | # 35 | if config.verbose: 36 | handler = logging.StreamHandler() 37 | handler.setFormatter(logging.Formatter('%(asctime)s:%(name)s:%(levelname)s:%(message)s')) 38 | logger.addHandler(handler) 39 | logger.setLevel(logging.DEBUG) 40 | 41 | # and set for stomp and ws_stomp modules also 42 | for modname in ['pxgrid_util.stomp', 'pxgrid_util.ws_stomp', 'pxgrid_util.pxgrid']: 43 | s_logger = logging.getLogger(modname) 44 | handler.setFormatter(logging.Formatter('%(asctime)s:%(name)s:%(levelname)s:%(message)s')) 45 | s_logger.addHandler(handler) 46 | s_logger.setLevel(logging.DEBUG) 47 | 48 | pxgrid = PXGridControl(config=config) 49 | 50 | while pxgrid.account_activate()['accountState'] != 'ENABLED': 51 | time.sleep(60) 52 | 53 | # lookup for session service 54 | service_lookup_response = pxgrid.service_lookup('com.cisco.ise.session') 55 | service = service_lookup_response['services'][0] 56 | node_name = service['nodeName'] 57 | url = service['properties']['restBaseUrl'] + '/getSessions' 58 | 59 | # log url to see what we get via discovery 60 | logger.info('Using URL %s', url) 61 | 62 | # check to see if we need to override the URL 63 | if config.discovery_override: 64 | url = create_override_url(config, url) 65 | 66 | secret = pxgrid.get_access_secret(node_name)['secret'] 67 | logger.info('Using access secret %s', secret) 68 | if config.start_timestamp: 69 | payload = { 70 | 'startTimestamp': config.start_timestamp 71 | } 72 | resp = query(config, secret, url, json.dumps(payload)) 73 | else: 74 | resp = query(config, secret, url, '{}') 75 | print(json.dumps(json.loads(resp), indent=2, sort_keys=True)) 76 | -------------------------------------------------------------------------------- /bin/session-query-by-ip: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | # 3 | # Copyright (c) 2021 Cisco Systems, Inc. and/or its affiliates 4 | # 5 | from pxgrid_util import PXGridControl 6 | from pxgrid_util import Config 7 | from pxgrid_util import create_override_url 8 | import urllib.request 9 | import base64 10 | import time 11 | import logging 12 | import json 13 | 14 | logger = logging.getLogger(__name__) 15 | 16 | 17 | def query(config, secret, url, payload): 18 | handler = urllib.request.HTTPSHandler(context=config.ssl_context) 19 | opener = urllib.request.build_opener(handler) 20 | rest_request = urllib.request.Request(url=url, data=str.encode(payload)) 21 | rest_request.add_header('Content-Type', 'application/json') 22 | rest_request.add_header('Accept', 'application/json') 23 | b64 = base64.b64encode((config.node_name + ':' + secret).encode()).decode() 24 | rest_request.add_header('Authorization', 'Basic ' + b64) 25 | rest_response = opener.open(rest_request) 26 | return rest_response.read().decode() 27 | 28 | 29 | if __name__ == '__main__': 30 | config = Config() 31 | 32 | # 33 | # verbose logging if configured 34 | # 35 | if config.verbose: 36 | handler = logging.StreamHandler() 37 | handler.setFormatter(logging.Formatter('%(asctime)s:%(name)s:%(levelname)s:%(message)s')) 38 | logger.addHandler(handler) 39 | logger.setLevel(logging.DEBUG) 40 | 41 | # and set for stomp and ws_stomp modules also 42 | for modname in ['pxgrid_util.stomp', 'pxgrid_util.ws_stomp', 'pxgrid_util.pxgrid']: 43 | s_logger = logging.getLogger(modname) 44 | handler.setFormatter(logging.Formatter('%(asctime)s:%(name)s:%(levelname)s:%(message)s')) 45 | s_logger.addHandler(handler) 46 | s_logger.setLevel(logging.DEBUG) 47 | 48 | pxgrid = PXGridControl(config=config) 49 | 50 | while pxgrid.account_activate()['accountState'] != 'ENABLED': 51 | time.sleep(60) 52 | 53 | # lookup for session service 54 | service_lookup_response = pxgrid.service_lookup('com.cisco.ise.session') 55 | service = service_lookup_response['services'][0] 56 | node_name = service['nodeName'] 57 | url = service['properties']['restBaseUrl'] + '/getSessionByIpAddress' 58 | 59 | # log url to see what we get via discovery 60 | logger.info('Using URL %s', url) 61 | 62 | # check to see if we need to override the URL 63 | if config.discovery_override: 64 | url = create_override_url(config, url) 65 | 66 | secret = pxgrid.get_access_secret(node_name)['secret'] 67 | 68 | if not config.ip: 69 | ip = input('Enter IP address: ') 70 | else: 71 | ip = config.ip 72 | resp = query(config, secret, url, '{ "ipAddress": "%s" }' % ip) 73 | print(json.dumps(json.loads(resp), indent=2, sort_keys=True)) 74 | -------------------------------------------------------------------------------- /bin/sgacls-query-all: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | # 3 | # Copyright (c) 2021 Cisco Systems, Inc. and/or its affiliates 4 | # 5 | from pxgrid_util import PXGridControl 6 | from pxgrid_util import Config 7 | from pxgrid_util import create_override_url 8 | import urllib.request 9 | import base64 10 | import time 11 | import logging 12 | import json 13 | 14 | logger = logging.getLogger(__name__) 15 | 16 | 17 | def query(config, secret, url, payload): 18 | handler = urllib.request.HTTPSHandler(context=config.ssl_context) 19 | opener = urllib.request.build_opener(handler) 20 | rest_request = urllib.request.Request(url=url, data=str.encode(payload)) 21 | rest_request.add_header('Content-Type', 'application/json') 22 | rest_request.add_header('Accept', 'application/json') 23 | b64 = base64.b64encode((config.node_name + ':' + secret).encode()).decode() 24 | rest_request.add_header('Authorization', 'Basic ' + b64) 25 | rest_response = opener.open(rest_request) 26 | return rest_response.read().decode() 27 | 28 | 29 | if __name__ == '__main__': 30 | config = Config() 31 | 32 | # 33 | # verbose logging if configured 34 | # 35 | if config.verbose: 36 | handler = logging.StreamHandler() 37 | handler.setFormatter(logging.Formatter('%(asctime)s:%(name)s:%(levelname)s:%(message)s')) 38 | logger.addHandler(handler) 39 | logger.setLevel(logging.DEBUG) 40 | 41 | # and set for stomp and ws_stomp modules also 42 | for modname in ['pxgrid_util.stomp', 'pxgrid_util.ws_stomp', 'pxgrid_util.pxgrid']: 43 | s_logger = logging.getLogger(modname) 44 | handler.setFormatter(logging.Formatter('%(asctime)s:%(name)s:%(levelname)s:%(message)s')) 45 | s_logger.addHandler(handler) 46 | s_logger.setLevel(logging.DEBUG) 47 | 48 | pxgrid = PXGridControl(config=config) 49 | 50 | while pxgrid.account_activate()['accountState'] != 'ENABLED': 51 | time.sleep(60) 52 | 53 | # lookup for session service 54 | service_lookup_response = pxgrid.service_lookup('com.cisco.ise.config.trustsec') 55 | service = service_lookup_response['services'][0] 56 | node_name = service['nodeName'] 57 | url = service['properties']['restBaseUrl'] + '/getSecurityGroupAcls' 58 | 59 | # log url to see what we get via discovery 60 | logger.info('Using URL %s', url) 61 | 62 | # check to see if we need to override the URL 63 | if config.discovery_override: 64 | url = create_override_url(config, url) 65 | 66 | secret = pxgrid.get_access_secret(node_name)['secret'] 67 | logger.info('Using access secret %s', secret) 68 | resp = query(config, secret, url, '{}') 69 | print(json.dumps(json.loads(resp), indent=2, sort_keys=True)) 70 | 71 | -------------------------------------------------------------------------------- /bin/sgts-query-all: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | # 3 | # Copyright (c) 2021 Cisco Systems, Inc. and/or its affiliates 4 | # 5 | from pxgrid_util import PXGridControl 6 | from pxgrid_util import Config 7 | from pxgrid_util import create_override_url 8 | import urllib.request 9 | import base64 10 | import time 11 | import logging 12 | import json 13 | 14 | logger = logging.getLogger(__name__) 15 | 16 | 17 | def query(config, secret, url, payload): 18 | handler = urllib.request.HTTPSHandler(context=config.ssl_context) 19 | opener = urllib.request.build_opener(handler) 20 | rest_request = urllib.request.Request(url=url, data=str.encode(payload)) 21 | rest_request.add_header('Content-Type', 'application/json') 22 | rest_request.add_header('Accept', 'application/json') 23 | b64 = base64.b64encode((config.node_name + ':' + secret).encode()).decode() 24 | rest_request.add_header('Authorization', 'Basic ' + b64) 25 | rest_response = opener.open(rest_request) 26 | return rest_response.read().decode() 27 | 28 | 29 | if __name__ == '__main__': 30 | config = Config() 31 | 32 | # 33 | # verbose logging if configured 34 | # 35 | if config.verbose: 36 | handler = logging.StreamHandler() 37 | handler.setFormatter(logging.Formatter('%(asctime)s:%(name)s:%(levelname)s:%(message)s')) 38 | logger.addHandler(handler) 39 | logger.setLevel(logging.DEBUG) 40 | 41 | # and set for stomp and ws_stomp modules also 42 | for modname in ['pxgrid_util.stomp', 'pxgrid_util.ws_stomp', 'pxgrid_util.pxgrid']: 43 | s_logger = logging.getLogger(modname) 44 | handler.setFormatter(logging.Formatter('%(asctime)s:%(name)s:%(levelname)s:%(message)s')) 45 | s_logger.addHandler(handler) 46 | s_logger.setLevel(logging.DEBUG) 47 | 48 | pxgrid = PXGridControl(config=config) 49 | 50 | while pxgrid.account_activate()['accountState'] != 'ENABLED': 51 | time.sleep(60) 52 | 53 | # lookup for session service 54 | service_lookup_response = pxgrid.service_lookup('com.cisco.ise.config.trustsec') 55 | service = service_lookup_response['services'][0] 56 | node_name = service['nodeName'] 57 | url = service['properties']['restBaseUrl'] + '/getSecurityGroups' 58 | 59 | # log url to see what we get via discovery 60 | logger.info('Using URL %s', url) 61 | 62 | # check to see if we need to override the URL 63 | if config.discovery_override: 64 | url = create_override_url(config, url) 65 | 66 | secret = pxgrid.get_access_secret(node_name)['secret'] 67 | logger.info('Using access secret %s', secret) 68 | resp = query(config, secret, url, '{}') 69 | print(json.dumps(json.loads(resp), indent=2, sort_keys=True)) 70 | -------------------------------------------------------------------------------- /bin/sxp-query-bindings: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | # 3 | # Copyright (c) 2021 Cisco Systems, Inc. and/or its affiliates 4 | # 5 | from pxgrid_util import PXGridControl 6 | from pxgrid_util import Config 7 | from pxgrid_util import create_override_url 8 | import urllib.request 9 | import base64 10 | import time 11 | import logging 12 | import json 13 | 14 | logger = logging.getLogger(__name__) 15 | 16 | 17 | def query(config, secret, url, payload): 18 | handler = urllib.request.HTTPSHandler(context=config.ssl_context) 19 | opener = urllib.request.build_opener(handler) 20 | rest_request = urllib.request.Request(url=url, data=str.encode(payload)) 21 | rest_request.add_header('Content-Type', 'application/json') 22 | rest_request.add_header('Accept', 'application/json') 23 | b64 = base64.b64encode((config.node_name + ':' + secret).encode()).decode() 24 | rest_request.add_header('Authorization', 'Basic ' + b64) 25 | rest_response = opener.open(rest_request) 26 | return rest_response.read().decode() 27 | 28 | 29 | if __name__ == '__main__': 30 | config = Config() 31 | 32 | # 33 | # verbose logging if configured 34 | # 35 | if config.verbose: 36 | handler = logging.StreamHandler() 37 | handler.setFormatter(logging.Formatter('%(asctime)s:%(name)s:%(levelname)s:%(message)s')) 38 | logger.addHandler(handler) 39 | logger.setLevel(logging.DEBUG) 40 | 41 | # and set for stomp and ws_stomp modules also 42 | for modname in ['pxgrid_util.stomp', 'pxgrid_util.ws_stomp', 'pxgrid_util.pxgrid']: 43 | s_logger = logging.getLogger(modname) 44 | handler.setFormatter(logging.Formatter('%(asctime)s:%(name)s:%(levelname)s:%(message)s')) 45 | s_logger.addHandler(handler) 46 | s_logger.setLevel(logging.DEBUG) 47 | 48 | pxgrid = PXGridControl(config=config) 49 | 50 | while pxgrid.account_activate()['accountState'] != 'ENABLED': 51 | time.sleep(60) 52 | 53 | # lookup for session service 54 | service_lookup_response = pxgrid.service_lookup('com.cisco.ise.sxp') 55 | service = service_lookup_response['services'][0] 56 | node_name = service['nodeName'] 57 | url = service['properties']['restBaseUrl'] + '/getBindings' 58 | 59 | # log url to see what we get via discovery 60 | logger.info('Using URL %s', url) 61 | 62 | # check to see if we need to override the URL 63 | if config.discovery_override: 64 | url = create_override_url(config, url) 65 | 66 | secret = pxgrid.get_access_secret(node_name)['secret'] 67 | logger.info('Using access secret %s', secret) 68 | if config.start_timestamp: 69 | payload = { 70 | 'startTimestamp': config.start_timestamp 71 | } 72 | resp = query(config, secret, url, json.dumps(payload)) 73 | else: 74 | resp = query(config, secret, url, '{}') 75 | print(json.dumps(json.loads(resp), indent=2, sort_keys=True)) 76 | -------------------------------------------------------------------------------- /bin/system-query-all: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | # 3 | # Copyright (c) 2021 Cisco Systems, Inc. and/or its affiliates 4 | # 5 | from pxgrid_util import PXGridControl 6 | from pxgrid_util import Config 7 | from pxgrid_util import create_override_url 8 | from pxgrid_util import query 9 | import urllib.request 10 | import base64 11 | import time 12 | import logging 13 | import json 14 | 15 | logger = logging.getLogger(__name__) 16 | 17 | if __name__ == '__main__': 18 | config = Config() 19 | 20 | # custom local options set to require at least one of the options 21 | g = config.parser.add_mutually_exclusive_group(required=True) 22 | g.add_argument( 23 | '--get-system-health', action='store_true', 24 | help='Get all system health objects') 25 | g.add_argument( 26 | '--get-system-performance', action='store_true', 27 | help='Get all system performance objects') 28 | 29 | # as we've added custom arguments, trigger parsing explicitly 30 | config.parse_args() 31 | 32 | # 33 | # verbose logging if configured 34 | # 35 | if config.verbose: 36 | handler = logging.StreamHandler() 37 | handler.setFormatter(logging.Formatter('%(asctime)s:%(name)s:%(levelname)s:%(message)s')) 38 | logger.addHandler(handler) 39 | logger.setLevel(logging.DEBUG) 40 | 41 | # and set for stomp and ws_stomp modules also 42 | for modname in ['pxgrid_util.stomp', 'pxgrid_util.ws_stomp', 'pxgrid_util.pxgrid']: 43 | s_logger = logging.getLogger(modname) 44 | handler.setFormatter(logging.Formatter('%(asctime)s:%(name)s:%(levelname)s:%(message)s')) 45 | s_logger.addHandler(handler) 46 | s_logger.setLevel(logging.DEBUG) 47 | 48 | pxgrid = PXGridControl(config=config) 49 | 50 | while pxgrid.account_activate()['accountState'] != 'ENABLED': 51 | time.sleep(60) 52 | 53 | # lookup for session service 54 | service_lookup_response = pxgrid.service_lookup('com.cisco.ise.system') 55 | service = service_lookup_response['services'][0] 56 | node_name = service['nodeName'] 57 | 58 | # health or performance? 59 | if config.config.get_system_performance: 60 | url = service['properties']['restBaseUrl'] + '/getPerformances' 61 | else: 62 | url = service['properties']['restBaseUrl'] + '/getHealths' 63 | 64 | # log url to see what we get via discovery 65 | logger.info('Using URL %s', url) 66 | 67 | # check to see if we need to override the URL 68 | if config.discovery_override: 69 | url = create_override_url(config, url) 70 | 71 | secret = pxgrid.get_access_secret(node_name)['secret'] 72 | logger.info('Using access secret %s', secret) 73 | if config.start_timestamp: 74 | payload = { 75 | 'startTimestamp': config.start_timestamp 76 | } 77 | resp = query(config, secret, url, json.dumps(payload)) 78 | else: 79 | resp = query(config, secret, url, '{}') 80 | print(json.dumps(json.loads(resp), indent=2, sort_keys=True)) 81 | -------------------------------------------------------------------------------- /bin/user-groups-query: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | # 3 | # Copyright (c) 2021 Cisco Systems, Inc. and/or its affiliates 4 | # 5 | from pxgrid_util import PXGridControl 6 | from pxgrid_util import Config 7 | from pxgrid_util import create_override_url 8 | import urllib.request 9 | import base64 10 | import time 11 | import logging 12 | import json 13 | 14 | logger = logging.getLogger(__name__) 15 | 16 | 17 | def query(config, secret, url, payload): 18 | handler = urllib.request.HTTPSHandler(context=config.ssl_context) 19 | opener = urllib.request.build_opener(handler) 20 | rest_request = urllib.request.Request(url=url, data=str.encode(payload)) 21 | rest_request.add_header('Content-Type', 'application/json') 22 | rest_request.add_header('Accept', 'application/json') 23 | b64 = base64.b64encode((config.node_name + ':' + secret).encode()).decode() 24 | rest_request.add_header('Authorization', 'Basic ' + b64) 25 | rest_response = opener.open(rest_request) 26 | return rest_response.read().decode() 27 | 28 | 29 | if __name__ == '__main__': 30 | config = Config() 31 | 32 | # 33 | # verbose logging if configured 34 | # 35 | if config.verbose: 36 | handler = logging.StreamHandler() 37 | handler.setFormatter(logging.Formatter('%(asctime)s:%(name)s:%(levelname)s:%(message)s')) 38 | logger.addHandler(handler) 39 | logger.setLevel(logging.DEBUG) 40 | 41 | # and set for stomp and ws_stomp modules also 42 | for modname in ['pxgrid_util.stomp', 'pxgrid_util.ws_stomp', 'pxgrid_util.pxgrid']: 43 | s_logger = logging.getLogger(modname) 44 | handler.setFormatter(logging.Formatter('%(asctime)s:%(name)s:%(levelname)s:%(message)s')) 45 | s_logger.addHandler(handler) 46 | s_logger.setLevel(logging.DEBUG) 47 | 48 | pxgrid = PXGridControl(config=config) 49 | 50 | while pxgrid.account_activate()['accountState'] != 'ENABLED': 51 | time.sleep(60) 52 | 53 | # lookup for session service 54 | service_lookup_response = pxgrid.service_lookup('com.cisco.ise.session') 55 | service = service_lookup_response['services'][0] 56 | node_name = service['nodeName'] 57 | url = service['properties']['restBaseUrl'] + '/getUserGroups' 58 | 59 | # log url to see what we get via discovery 60 | logger.info('Using URL %s', url) 61 | 62 | # check to see if we need to override the URL 63 | if config.discovery_override: 64 | url = create_override_url(config, url) 65 | 66 | secret = pxgrid.get_access_secret(node_name)['secret'] 67 | logger.info('Using access secret %s', secret) 68 | if config.start_timestamp: 69 | payload = { 70 | 'startTimestamp': config.start_timestamp 71 | } 72 | resp = query(config, secret, url, json.dumps(payload)) 73 | else: 74 | resp = query(config, secret, url, '{}') 75 | print(json.dumps(json.loads(resp), indent=2, sort_keys=True)) 76 | -------------------------------------------------------------------------------- /pxgrid_util/__init__.py: -------------------------------------------------------------------------------- 1 | 2 | from . import _version 3 | import logging 4 | 5 | __version__ = _version.get_versions()['version'] 6 | 7 | import urllib 8 | import base64 9 | from urllib.parse import urlparse 10 | from .config import Config 11 | from .create_account_config import CreateAccountConfig 12 | from .pxgrid import PXGridControl 13 | from .ws_stomp import WebSocketStomp 14 | 15 | 16 | logger = logging.getLogger(__name__) 17 | 18 | 19 | def create_override_url(config: Config, discovered_url: str) -> str: 20 | logger.info('Overriding discovered service host with %s', config.discovery_override) 21 | o = urlparse(discovered_url) 22 | netloc_array = o.netloc.split(':') 23 | new_netloc = config.discovery_override 24 | if len(netloc_array) > 1: 25 | new_netloc += ':' + netloc_array[1] 26 | o = o._replace(netloc=new_netloc) 27 | new_url = o.geturl() 28 | logger.info('New URL %s', new_url) 29 | return new_url 30 | 31 | 32 | def query(config, secret, url, payload): 33 | handler = urllib.request.HTTPSHandler(context=config.ssl_context) 34 | opener = urllib.request.build_opener(handler) 35 | rest_request = urllib.request.Request(url=url, data=str.encode(payload)) 36 | rest_request.add_header('Content-Type', 'application/json') 37 | rest_request.add_header('Accept', 'application/json') 38 | b64 = base64.b64encode((config.node_name + ':' + secret).encode()).decode() 39 | rest_request.add_header('Authorization', 'Basic ' + b64) 40 | rest_response = opener.open(rest_request) 41 | return rest_response.read().decode() 42 | 43 | 44 | -------------------------------------------------------------------------------- /pxgrid_util/_version.py: -------------------------------------------------------------------------------- 1 | 2 | # This file helps to compute a version number in source trees obtained from 3 | # git-archive tarball (such as those provided by githubs download-from-tag 4 | # feature). Distribution tarballs (built by setup.py sdist) and build 5 | # directories (produced by setup.py build) will contain a much shorter file 6 | # that just contains the computed version number. 7 | 8 | # This file is released into the public domain. Generated by 9 | # versioneer-0.20 (https://github.com/python-versioneer/python-versioneer) 10 | 11 | """Git implementation of _version.py.""" 12 | 13 | import errno 14 | import os 15 | import re 16 | import subprocess 17 | import sys 18 | 19 | 20 | def get_keywords(): 21 | """Get the keywords needed to look up the version information.""" 22 | # these strings will be replaced by git during git-archive. 23 | # setup.py/versioneer.py will grep for the variable names, so they must 24 | # each be defined on a line of their own. _version.py will just call 25 | # get_keywords(). 26 | git_refnames = " (HEAD -> main, tag: v0.7.0)" 27 | git_full = "d623caa5c6188263c25bfe5adebb128528e3ca32" 28 | git_date = "2025-06-06 14:46:53 +0100" 29 | keywords = {"refnames": git_refnames, "full": git_full, "date": git_date} 30 | return keywords 31 | 32 | 33 | class VersioneerConfig: # pylint: disable=too-few-public-methods 34 | """Container for Versioneer configuration parameters.""" 35 | 36 | 37 | def get_config(): 38 | """Create, populate and return the VersioneerConfig() object.""" 39 | # these strings are filled in when 'setup.py versioneer' creates 40 | # _version.py 41 | cfg = VersioneerConfig() 42 | cfg.VCS = "git" 43 | cfg.style = "git-describe" 44 | cfg.tag_prefix = "v" 45 | cfg.parentdir_prefix = "None" 46 | cfg.versionfile_source = "pxgrid_util/_version.py" 47 | cfg.verbose = False 48 | return cfg 49 | 50 | 51 | class NotThisMethod(Exception): 52 | """Exception raised if a method is not valid for the current scenario.""" 53 | 54 | 55 | LONG_VERSION_PY = {} 56 | HANDLERS = {} 57 | 58 | 59 | def register_vcs_handler(vcs, method): # decorator 60 | """Create decorator to mark a method as the handler of a VCS.""" 61 | def decorate(f): 62 | """Store f in HANDLERS[vcs][method].""" 63 | if vcs not in HANDLERS: 64 | HANDLERS[vcs] = {} 65 | HANDLERS[vcs][method] = f 66 | return f 67 | return decorate 68 | 69 | 70 | # pylint:disable=too-many-arguments,consider-using-with # noqa 71 | def run_command(commands, args, cwd=None, verbose=False, hide_stderr=False, 72 | env=None): 73 | """Call the given command(s).""" 74 | assert isinstance(commands, list) 75 | process = None 76 | for command in commands: 77 | try: 78 | dispcmd = str([command] + args) 79 | # remember shell=False, so use git.cmd on windows, not just git 80 | process = subprocess.Popen([command] + args, cwd=cwd, env=env, 81 | stdout=subprocess.PIPE, 82 | stderr=(subprocess.PIPE if hide_stderr 83 | else None)) 84 | break 85 | except EnvironmentError: 86 | e = sys.exc_info()[1] 87 | if e.errno == errno.ENOENT: 88 | continue 89 | if verbose: 90 | print("unable to run %s" % dispcmd) 91 | print(e) 92 | return None, None 93 | else: 94 | if verbose: 95 | print("unable to find command, tried %s" % (commands,)) 96 | return None, None 97 | stdout = process.communicate()[0].strip().decode() 98 | if process.returncode != 0: 99 | if verbose: 100 | print("unable to run %s (error)" % dispcmd) 101 | print("stdout was %s" % stdout) 102 | return None, process.returncode 103 | return stdout, process.returncode 104 | 105 | 106 | def versions_from_parentdir(parentdir_prefix, root, verbose): 107 | """Try to determine the version from the parent directory name. 108 | 109 | Source tarballs conventionally unpack into a directory that includes both 110 | the project name and a version string. We will also support searching up 111 | two directory levels for an appropriately named parent directory 112 | """ 113 | rootdirs = [] 114 | 115 | for _ in range(3): 116 | dirname = os.path.basename(root) 117 | if dirname.startswith(parentdir_prefix): 118 | return {"version": dirname[len(parentdir_prefix):], 119 | "full-revisionid": None, 120 | "dirty": False, "error": None, "date": None} 121 | rootdirs.append(root) 122 | root = os.path.dirname(root) # up a level 123 | 124 | if verbose: 125 | print("Tried directories %s but none started with prefix %s" % 126 | (str(rootdirs), parentdir_prefix)) 127 | raise NotThisMethod("rootdir doesn't start with parentdir_prefix") 128 | 129 | 130 | @register_vcs_handler("git", "get_keywords") 131 | def git_get_keywords(versionfile_abs): 132 | """Extract version information from the given file.""" 133 | # the code embedded in _version.py can just fetch the value of these 134 | # keywords. When used from setup.py, we don't want to import _version.py, 135 | # so we do it with a regexp instead. This function is not used from 136 | # _version.py. 137 | keywords = {} 138 | try: 139 | with open(versionfile_abs, "r") as fobj: 140 | for line in fobj: 141 | if line.strip().startswith("git_refnames ="): 142 | mo = re.search(r'=\s*"(.*)"', line) 143 | if mo: 144 | keywords["refnames"] = mo.group(1) 145 | if line.strip().startswith("git_full ="): 146 | mo = re.search(r'=\s*"(.*)"', line) 147 | if mo: 148 | keywords["full"] = mo.group(1) 149 | if line.strip().startswith("git_date ="): 150 | mo = re.search(r'=\s*"(.*)"', line) 151 | if mo: 152 | keywords["date"] = mo.group(1) 153 | except EnvironmentError: 154 | pass 155 | return keywords 156 | 157 | 158 | @register_vcs_handler("git", "keywords") 159 | def git_versions_from_keywords(keywords, tag_prefix, verbose): 160 | """Get version information from git keywords.""" 161 | if "refnames" not in keywords: 162 | raise NotThisMethod("Short version file found") 163 | date = keywords.get("date") 164 | if date is not None: 165 | # Use only the last line. Previous lines may contain GPG signature 166 | # information. 167 | date = date.splitlines()[-1] 168 | 169 | # git-2.2.0 added "%cI", which expands to an ISO-8601 -compliant 170 | # datestamp. However we prefer "%ci" (which expands to an "ISO-8601 171 | # -like" string, which we must then edit to make compliant), because 172 | # it's been around since git-1.5.3, and it's too difficult to 173 | # discover which version we're using, or to work around using an 174 | # older one. 175 | date = date.strip().replace(" ", "T", 1).replace(" ", "", 1) 176 | refnames = keywords["refnames"].strip() 177 | if refnames.startswith("$Format"): 178 | if verbose: 179 | print("keywords are unexpanded, not using") 180 | raise NotThisMethod("unexpanded keywords, not a git-archive tarball") 181 | refs = {r.strip() for r in refnames.strip("()").split(",")} 182 | # starting in git-1.8.3, tags are listed as "tag: foo-1.0" instead of 183 | # just "foo-1.0". If we see a "tag: " prefix, prefer those. 184 | TAG = "tag: " 185 | tags = {r[len(TAG):] for r in refs if r.startswith(TAG)} 186 | if not tags: 187 | # Either we're using git < 1.8.3, or there really are no tags. We use 188 | # a heuristic: assume all version tags have a digit. The old git %d 189 | # expansion behaves like git log --decorate=short and strips out the 190 | # refs/heads/ and refs/tags/ prefixes that would let us distinguish 191 | # between branches and tags. By ignoring refnames without digits, we 192 | # filter out many common branch names like "release" and 193 | # "stabilization", as well as "HEAD" and "master". 194 | tags = {r for r in refs if re.search(r'\d', r)} 195 | if verbose: 196 | print("discarding '%s', no digits" % ",".join(refs - tags)) 197 | if verbose: 198 | print("likely tags: %s" % ",".join(sorted(tags))) 199 | for ref in sorted(tags): 200 | # sorting will prefer e.g. "2.0" over "2.0rc1" 201 | if ref.startswith(tag_prefix): 202 | r = ref[len(tag_prefix):] 203 | # Filter out refs that exactly match prefix or that don't start 204 | # with a number once the prefix is stripped (mostly a concern 205 | # when prefix is '') 206 | if not re.match(r'\d', r): 207 | continue 208 | if verbose: 209 | print("picking %s" % r) 210 | return {"version": r, 211 | "full-revisionid": keywords["full"].strip(), 212 | "dirty": False, "error": None, 213 | "date": date} 214 | # no suitable tags, so version is "0+unknown", but full hex is still there 215 | if verbose: 216 | print("no suitable tags, using unknown + full revision id") 217 | return {"version": "0+unknown", 218 | "full-revisionid": keywords["full"].strip(), 219 | "dirty": False, "error": "no suitable tags", "date": None} 220 | 221 | 222 | @register_vcs_handler("git", "pieces_from_vcs") 223 | def git_pieces_from_vcs(tag_prefix, root, verbose, runner=run_command): 224 | """Get version from 'git describe' in the root of the source tree. 225 | 226 | This only gets called if the git-archive 'subst' keywords were *not* 227 | expanded, and _version.py hasn't already been rewritten with a short 228 | version string, meaning we're inside a checked out source tree. 229 | """ 230 | GITS = ["git"] 231 | if sys.platform == "win32": 232 | GITS = ["git.cmd", "git.exe"] 233 | 234 | _, rc = runner(GITS, ["rev-parse", "--git-dir"], cwd=root, 235 | hide_stderr=True) 236 | if rc != 0: 237 | if verbose: 238 | print("Directory %s not under git control" % root) 239 | raise NotThisMethod("'git rev-parse --git-dir' returned error") 240 | 241 | # if there is a tag matching tag_prefix, this yields TAG-NUM-gHEX[-dirty] 242 | # if there isn't one, this yields HEX[-dirty] (no NUM) 243 | describe_out, rc = runner(GITS, ["describe", "--tags", "--dirty", 244 | "--always", "--long", 245 | "--match", "%s*" % tag_prefix], 246 | cwd=root) 247 | # --long was added in git-1.5.5 248 | if describe_out is None: 249 | raise NotThisMethod("'git describe' failed") 250 | describe_out = describe_out.strip() 251 | full_out, rc = runner(GITS, ["rev-parse", "HEAD"], cwd=root) 252 | if full_out is None: 253 | raise NotThisMethod("'git rev-parse' failed") 254 | full_out = full_out.strip() 255 | 256 | pieces = {} 257 | pieces["long"] = full_out 258 | pieces["short"] = full_out[:7] # maybe improved later 259 | pieces["error"] = None 260 | 261 | branch_name, rc = runner(GITS, ["rev-parse", "--abbrev-ref", "HEAD"], 262 | cwd=root) 263 | # --abbrev-ref was added in git-1.6.3 264 | if rc != 0 or branch_name is None: 265 | raise NotThisMethod("'git rev-parse --abbrev-ref' returned error") 266 | branch_name = branch_name.strip() 267 | 268 | if branch_name == "HEAD": 269 | # If we aren't exactly on a branch, pick a branch which represents 270 | # the current commit. If all else fails, we are on a branchless 271 | # commit. 272 | branches, rc = runner(GITS, ["branch", "--contains"], cwd=root) 273 | # --contains was added in git-1.5.4 274 | if rc != 0 or branches is None: 275 | raise NotThisMethod("'git branch --contains' returned error") 276 | branches = branches.split("\n") 277 | 278 | # Remove the first line if we're running detached 279 | if "(" in branches[0]: 280 | branches.pop(0) 281 | 282 | # Strip off the leading "* " from the list of branches. 283 | branches = [branch[2:] for branch in branches] 284 | if "master" in branches: 285 | branch_name = "master" 286 | elif not branches: 287 | branch_name = None 288 | else: 289 | # Pick the first branch that is returned. Good or bad. 290 | branch_name = branches[0] 291 | 292 | pieces["branch"] = branch_name 293 | 294 | # parse describe_out. It will be like TAG-NUM-gHEX[-dirty] or HEX[-dirty] 295 | # TAG might have hyphens. 296 | git_describe = describe_out 297 | 298 | # look for -dirty suffix 299 | dirty = git_describe.endswith("-dirty") 300 | pieces["dirty"] = dirty 301 | if dirty: 302 | git_describe = git_describe[:git_describe.rindex("-dirty")] 303 | 304 | # now we have TAG-NUM-gHEX or HEX 305 | 306 | if "-" in git_describe: 307 | # TAG-NUM-gHEX 308 | mo = re.search(r'^(.+)-(\d+)-g([0-9a-f]+)$', git_describe) 309 | if not mo: 310 | # unparseable. Maybe git-describe is misbehaving? 311 | pieces["error"] = ("unable to parse git-describe output: '%s'" 312 | % describe_out) 313 | return pieces 314 | 315 | # tag 316 | full_tag = mo.group(1) 317 | if not full_tag.startswith(tag_prefix): 318 | if verbose: 319 | fmt = "tag '%s' doesn't start with prefix '%s'" 320 | print(fmt % (full_tag, tag_prefix)) 321 | pieces["error"] = ("tag '%s' doesn't start with prefix '%s'" 322 | % (full_tag, tag_prefix)) 323 | return pieces 324 | pieces["closest-tag"] = full_tag[len(tag_prefix):] 325 | 326 | # distance: number of commits since tag 327 | pieces["distance"] = int(mo.group(2)) 328 | 329 | # commit: short hex revision ID 330 | pieces["short"] = mo.group(3) 331 | 332 | else: 333 | # HEX: no tags 334 | pieces["closest-tag"] = None 335 | count_out, rc = runner(GITS, ["rev-list", "HEAD", "--count"], cwd=root) 336 | pieces["distance"] = int(count_out) # total number of commits 337 | 338 | # commit date: see ISO-8601 comment in git_versions_from_keywords() 339 | date = runner(GITS, ["show", "-s", "--format=%ci", "HEAD"], cwd=root)[0].strip() 340 | # Use only the last line. Previous lines may contain GPG signature 341 | # information. 342 | date = date.splitlines()[-1] 343 | pieces["date"] = date.strip().replace(" ", "T", 1).replace(" ", "", 1) 344 | 345 | return pieces 346 | 347 | 348 | def plus_or_dot(pieces): 349 | """Return a + if we don't already have one, else return a .""" 350 | if "+" in pieces.get("closest-tag", ""): 351 | return "." 352 | return "+" 353 | 354 | 355 | def render_pep440(pieces): 356 | """Build up version string, with post-release "local version identifier". 357 | 358 | Our goal: TAG[+DISTANCE.gHEX[.dirty]] . Note that if you 359 | get a tagged build and then dirty it, you'll get TAG+0.gHEX.dirty 360 | 361 | Exceptions: 362 | 1: no tags. git_describe was just HEX. 0+untagged.DISTANCE.gHEX[.dirty] 363 | """ 364 | if pieces["closest-tag"]: 365 | rendered = pieces["closest-tag"] 366 | if pieces["distance"] or pieces["dirty"]: 367 | rendered += plus_or_dot(pieces) 368 | rendered += "%d.g%s" % (pieces["distance"], pieces["short"]) 369 | if pieces["dirty"]: 370 | rendered += ".dirty" 371 | else: 372 | # exception #1 373 | rendered = "0+untagged.%d.g%s" % (pieces["distance"], 374 | pieces["short"]) 375 | if pieces["dirty"]: 376 | rendered += ".dirty" 377 | return rendered 378 | 379 | 380 | def render_pep440_branch(pieces): 381 | """TAG[[.dev0]+DISTANCE.gHEX[.dirty]] . 382 | 383 | The ".dev0" means not master branch. Note that .dev0 sorts backwards 384 | (a feature branch will appear "older" than the master branch). 385 | 386 | Exceptions: 387 | 1: no tags. 0[.dev0]+untagged.DISTANCE.gHEX[.dirty] 388 | """ 389 | if pieces["closest-tag"]: 390 | rendered = pieces["closest-tag"] 391 | if pieces["distance"] or pieces["dirty"]: 392 | if pieces["branch"] != "master": 393 | rendered += ".dev0" 394 | rendered += plus_or_dot(pieces) 395 | rendered += "%d.g%s" % (pieces["distance"], pieces["short"]) 396 | if pieces["dirty"]: 397 | rendered += ".dirty" 398 | else: 399 | # exception #1 400 | rendered = "0" 401 | if pieces["branch"] != "master": 402 | rendered += ".dev0" 403 | rendered += "+untagged.%d.g%s" % (pieces["distance"], 404 | pieces["short"]) 405 | if pieces["dirty"]: 406 | rendered += ".dirty" 407 | return rendered 408 | 409 | 410 | def render_pep440_pre(pieces): 411 | """TAG[.post0.devDISTANCE] -- No -dirty. 412 | 413 | Exceptions: 414 | 1: no tags. 0.post0.devDISTANCE 415 | """ 416 | if pieces["closest-tag"]: 417 | rendered = pieces["closest-tag"] 418 | if pieces["distance"]: 419 | rendered += ".post0.dev%d" % pieces["distance"] 420 | else: 421 | # exception #1 422 | rendered = "0.post0.dev%d" % pieces["distance"] 423 | return rendered 424 | 425 | 426 | def render_pep440_post(pieces): 427 | """TAG[.postDISTANCE[.dev0]+gHEX] . 428 | 429 | The ".dev0" means dirty. Note that .dev0 sorts backwards 430 | (a dirty tree will appear "older" than the corresponding clean one), 431 | but you shouldn't be releasing software with -dirty anyways. 432 | 433 | Exceptions: 434 | 1: no tags. 0.postDISTANCE[.dev0] 435 | """ 436 | if pieces["closest-tag"]: 437 | rendered = pieces["closest-tag"] 438 | if pieces["distance"] or pieces["dirty"]: 439 | rendered += ".post%d" % pieces["distance"] 440 | if pieces["dirty"]: 441 | rendered += ".dev0" 442 | rendered += plus_or_dot(pieces) 443 | rendered += "g%s" % pieces["short"] 444 | else: 445 | # exception #1 446 | rendered = "0.post%d" % pieces["distance"] 447 | if pieces["dirty"]: 448 | rendered += ".dev0" 449 | rendered += "+g%s" % pieces["short"] 450 | return rendered 451 | 452 | 453 | def render_pep440_post_branch(pieces): 454 | """TAG[.postDISTANCE[.dev0]+gHEX[.dirty]] . 455 | 456 | The ".dev0" means not master branch. 457 | 458 | Exceptions: 459 | 1: no tags. 0.postDISTANCE[.dev0]+gHEX[.dirty] 460 | """ 461 | if pieces["closest-tag"]: 462 | rendered = pieces["closest-tag"] 463 | if pieces["distance"] or pieces["dirty"]: 464 | rendered += ".post%d" % pieces["distance"] 465 | if pieces["branch"] != "master": 466 | rendered += ".dev0" 467 | rendered += plus_or_dot(pieces) 468 | rendered += "g%s" % pieces["short"] 469 | if pieces["dirty"]: 470 | rendered += ".dirty" 471 | else: 472 | # exception #1 473 | rendered = "0.post%d" % pieces["distance"] 474 | if pieces["branch"] != "master": 475 | rendered += ".dev0" 476 | rendered += "+g%s" % pieces["short"] 477 | if pieces["dirty"]: 478 | rendered += ".dirty" 479 | return rendered 480 | 481 | 482 | def render_pep440_old(pieces): 483 | """TAG[.postDISTANCE[.dev0]] . 484 | 485 | The ".dev0" means dirty. 486 | 487 | Exceptions: 488 | 1: no tags. 0.postDISTANCE[.dev0] 489 | """ 490 | if pieces["closest-tag"]: 491 | rendered = pieces["closest-tag"] 492 | if pieces["distance"] or pieces["dirty"]: 493 | rendered += ".post%d" % pieces["distance"] 494 | if pieces["dirty"]: 495 | rendered += ".dev0" 496 | else: 497 | # exception #1 498 | rendered = "0.post%d" % pieces["distance"] 499 | if pieces["dirty"]: 500 | rendered += ".dev0" 501 | return rendered 502 | 503 | 504 | def render_git_describe(pieces): 505 | """TAG[-DISTANCE-gHEX][-dirty]. 506 | 507 | Like 'git describe --tags --dirty --always'. 508 | 509 | Exceptions: 510 | 1: no tags. HEX[-dirty] (note: no 'g' prefix) 511 | """ 512 | if pieces["closest-tag"]: 513 | rendered = pieces["closest-tag"] 514 | if pieces["distance"]: 515 | rendered += "-%d-g%s" % (pieces["distance"], pieces["short"]) 516 | else: 517 | # exception #1 518 | rendered = pieces["short"] 519 | if pieces["dirty"]: 520 | rendered += "-dirty" 521 | return rendered 522 | 523 | 524 | def render_git_describe_long(pieces): 525 | """TAG-DISTANCE-gHEX[-dirty]. 526 | 527 | Like 'git describe --tags --dirty --always -long'. 528 | The distance/hash is unconditional. 529 | 530 | Exceptions: 531 | 1: no tags. HEX[-dirty] (note: no 'g' prefix) 532 | """ 533 | if pieces["closest-tag"]: 534 | rendered = pieces["closest-tag"] 535 | rendered += "-%d-g%s" % (pieces["distance"], pieces["short"]) 536 | else: 537 | # exception #1 538 | rendered = pieces["short"] 539 | if pieces["dirty"]: 540 | rendered += "-dirty" 541 | return rendered 542 | 543 | 544 | def render(pieces, style): 545 | """Render the given version pieces into the requested style.""" 546 | if pieces["error"]: 547 | return {"version": "unknown", 548 | "full-revisionid": pieces.get("long"), 549 | "dirty": None, 550 | "error": pieces["error"], 551 | "date": None} 552 | 553 | if not style or style == "default": 554 | style = "pep440" # the default 555 | 556 | if style == "pep440": 557 | rendered = render_pep440(pieces) 558 | elif style == "pep440-branch": 559 | rendered = render_pep440_branch(pieces) 560 | elif style == "pep440-pre": 561 | rendered = render_pep440_pre(pieces) 562 | elif style == "pep440-post": 563 | rendered = render_pep440_post(pieces) 564 | elif style == "pep440-post-branch": 565 | rendered = render_pep440_post_branch(pieces) 566 | elif style == "pep440-old": 567 | rendered = render_pep440_old(pieces) 568 | elif style == "git-describe": 569 | rendered = render_git_describe(pieces) 570 | elif style == "git-describe-long": 571 | rendered = render_git_describe_long(pieces) 572 | else: 573 | raise ValueError("unknown style '%s'" % style) 574 | 575 | return {"version": rendered, "full-revisionid": pieces["long"], 576 | "dirty": pieces["dirty"], "error": None, 577 | "date": pieces.get("date")} 578 | 579 | 580 | def get_versions(): 581 | """Get version information or return default if unable to do so.""" 582 | # I am in _version.py, which lives at ROOT/VERSIONFILE_SOURCE. If we have 583 | # __file__, we can work backwards from there to the root. Some 584 | # py2exe/bbfreeze/non-CPython implementations don't do __file__, in which 585 | # case we can only use expanded keywords. 586 | 587 | cfg = get_config() 588 | verbose = cfg.verbose 589 | 590 | try: 591 | return git_versions_from_keywords(get_keywords(), cfg.tag_prefix, 592 | verbose) 593 | except NotThisMethod: 594 | pass 595 | 596 | try: 597 | root = os.path.realpath(__file__) 598 | # versionfile_source is the relative path from the top of the source 599 | # tree (where the .git directory might live) to this file. Invert 600 | # this to find the root from __file__. 601 | for _ in cfg.versionfile_source.split('/'): 602 | root = os.path.dirname(root) 603 | except NameError: 604 | return {"version": "0+unknown", "full-revisionid": None, 605 | "dirty": None, 606 | "error": "unable to find root of source tree", 607 | "date": None} 608 | 609 | try: 610 | pieces = git_pieces_from_vcs(cfg.tag_prefix, root, verbose) 611 | return render(pieces, cfg.style) 612 | except NotThisMethod: 613 | pass 614 | 615 | try: 616 | if cfg.parentdir_prefix: 617 | return versions_from_parentdir(cfg.parentdir_prefix, root, verbose) 618 | except NotThisMethod: 619 | pass 620 | 621 | return {"version": "0+unknown", "full-revisionid": None, 622 | "dirty": None, 623 | "error": "unable to compute version", "date": None} 624 | -------------------------------------------------------------------------------- /pxgrid_util/config.py: -------------------------------------------------------------------------------- 1 | import argparse 2 | import ssl 3 | import enum 4 | 5 | 6 | class AncPolicyType(enum.Enum): 7 | quarantine = 'QUARANTINE' 8 | shutdown = 'SHUT_DOWN' 9 | port_bounce = 'PORT_BOUNCE' 10 | reauthenticate = 'RE_AUTHENTICATE' 11 | 12 | def __str__(self): 13 | return self.value 14 | 15 | def ensure_parsed(func): 16 | def wrapper(instance): 17 | instance.parse_args() 18 | return func(instance) 19 | return wrapper 20 | 21 | 22 | class Config: 23 | args_parsed = False 24 | 25 | def __init__(self): 26 | self.__ssl_context = None 27 | 28 | # 29 | # Options that apply to all clients. `parser` is now member variable 30 | # so that users can extend the parser in their own scripts with their 31 | # own options and explicitly invoke `parse` 32 | # 33 | self.parser = argparse.ArgumentParser() 34 | self.parser.add_argument( 35 | '-a', '--hostname', 36 | help='pxGrid controller host name (multiple ok)', 37 | action='append') 38 | self.parser.add_argument( 39 | '--port', 40 | help='pxGrid port (default 8910)', 41 | default=8910) 42 | self.parser.add_argument( 43 | '-n', '--nodename', 44 | help='Client node name') 45 | self.parser.add_argument( 46 | '-w', '--password', 47 | help='Password (optional)') 48 | self.parser.add_argument( 49 | '-d', '--description', 50 | help='Description (optional)') 51 | self.parser.add_argument( 52 | '-c', '--clientcert', 53 | help='Client certificate chain pem filename (optional)') 54 | self.parser.add_argument( 55 | '-k', '--clientkey', 56 | help='Client key filename (optional)') 57 | self.parser.add_argument( 58 | '-p', '--clientkeypassword', 59 | help='Client key password (optional)') 60 | self.parser.add_argument( 61 | '-s', '--servercert', 62 | help='Server certificates pem filename') 63 | self.parser.add_argument( 64 | '--insecure', action='store_true', 65 | help='Allow insecure server connections when using SSL') 66 | self.parser.add_argument( 67 | '--discovery-override', type=str, 68 | help='Override pxGrid service discovery (for test environments without proper DNS)') 69 | self.parser.add_argument( 70 | '-v', '--verbose', action='store_true', 71 | help='Verbose output') 72 | 73 | # 74 | # Options that apply to `px-subscribe` only 75 | # 76 | self.parser.add_argument( 77 | '--ws-ping-interval', type=float, 78 | default=20.0, 79 | help='WebSocket ping interval in seconds (float)') 80 | self.parser.add_argument( 81 | '--service', type=str, 82 | help='Service name') 83 | self.parser.add_argument( 84 | '--topic', type=str, 85 | help='Topic to subscribe to') 86 | self.parser.add_argument( 87 | '--subscribe', action='store_true', 88 | help='set up a subscription') 89 | self.parser.add_argument( 90 | '--subscribe-all', action='store_true', 91 | help='subscribe to ALL nodes discovered') 92 | 93 | # optionally select a subscriber that is not the default one 94 | g = self.parser.add_mutually_exclusive_group() 95 | g.add_argument( 96 | '--connect-only', action='store_true', 97 | help='connect to pxGrid brokers only, no subscription') 98 | g.add_argument( 99 | '--session-dedup', action='store_true', 100 | help='run the sessionTopic de-duplicating subscriber') 101 | 102 | self.parser.add_argument( 103 | '--services', action='store_true', 104 | help='List out supported services') 105 | self.parser.add_argument( 106 | '--service-details', type=str, 107 | help='List out details of a specific service') 108 | 109 | # 110 | # Options that apply to populating session directory queries 111 | # 112 | self.parser.add_argument( 113 | '--ip', type=str, 114 | help='Optional IP address for queries') 115 | self.parser.add_argument( 116 | '--start-timestamp', type=str, 117 | help='Optional startTimestamp for queries') 118 | 119 | # 120 | # Options for getting, applying and clearing ANC policies via 121 | # `anc-policy` 122 | # 123 | g = self.parser.add_mutually_exclusive_group() 124 | g.add_argument( 125 | '--get-anc-endpoints', action='store_true', 126 | help='Get endpoints with ANC policies') 127 | g.add_argument( 128 | '--get-anc-policy-by-mac', action='store_true', 129 | help='Get endpoint\'s ANC policy by MAC address') 130 | g.add_argument( 131 | '--get-anc-policies', action='store_true', 132 | help='Get all ANC policies') 133 | g.add_argument( 134 | '--apply-anc-policy-by-mac', action='store_true', 135 | help='Apply named ANC policy by endpoint MAC address') 136 | g.add_argument( 137 | '--apply-anc-policy-by-mac-bulk', type=str, 138 | help='Bulk-apply named ANC policy by endpoint MAC addresses in flat file') 139 | g.add_argument( 140 | '--apply-anc-policy-by-ip', action='store_true', 141 | help='Apply named ANC policy by endpoint IP address') 142 | g.add_argument( 143 | '--apply-anc-policy', action='store_true', 144 | help='Apply named ANC policy by whatever parameters provided if there are enough') 145 | g.add_argument( 146 | '--clear-anc-policy-by-mac', action='store_true', 147 | help='Clear ANC policy by endpoint MAC address') 148 | g.add_argument( 149 | '--clear-anc-policy-by-ip', action='store_true', 150 | help='Clear ANC policy by endpoint IP address') 151 | g.add_argument( 152 | '--create-anc-policy', type=str, 153 | help='Create named ANC policy') 154 | g.add_argument( 155 | '--delete-anc-policy', type=str, 156 | help='Delete named ANC policy') 157 | 158 | # anc parameters 159 | self.parser.add_argument( 160 | '--anc-policy', type=str, 161 | help='Optional ANC policy name') 162 | self.parser.add_argument( 163 | '--anc-mac-address', type=str, 164 | help='Optional MAC address for ANC policies') 165 | self.parser.add_argument( 166 | '--anc-ip-address', type=str, 167 | help='Optional IP address for ANC policies') 168 | self.parser.add_argument( 169 | '--anc-nas-ip-address', type=str, 170 | help='Optional NAS IP address for ANC policies') 171 | self.parser.add_argument( 172 | '--anc-policy-action', type=AncPolicyType, 173 | choices=list(AncPolicyType)) 174 | 175 | # publishing parameters 176 | self.parser.add_argument( 177 | '--publish-delay', type=float, default=1.0, 178 | help='delay between custom event publishes') 179 | self.parser.add_argument( 180 | '--reregister-delay', type=float, default=1.0, 181 | help='delay between custom service reregistrations') 182 | 183 | def parse_args(self): 184 | ''' 185 | Call this function after you have 186 | ''' 187 | if not self.args_parsed: 188 | self.config = self.parser.parse_args() 189 | self.args_parsed = True 190 | 191 | @property 192 | @ensure_parsed 193 | def subscribe(self): 194 | return self.config.subscribe 195 | 196 | @property 197 | @ensure_parsed 198 | def subscribe_all(self): 199 | return self.config.subscribe_all 200 | 201 | @property 202 | @ensure_parsed 203 | def connect_only(self): 204 | return self.config.connect_only 205 | 206 | @property 207 | @ensure_parsed 208 | def session_dedup(self): 209 | return self.config.session_dedup 210 | 211 | @property 212 | @ensure_parsed 213 | def ws_ping_interval(self): 214 | return self.config.ws_ping_interval 215 | 216 | @property 217 | @ensure_parsed 218 | def services(self): 219 | return self.config.services 220 | 221 | @property 222 | @ensure_parsed 223 | def service_details(self): 224 | return self.config.service_details 225 | 226 | @property 227 | @ensure_parsed 228 | def verbose(self): 229 | return self.config.verbose 230 | 231 | @property 232 | @ensure_parsed 233 | def hostname(self): 234 | return self.config.hostname 235 | 236 | @property 237 | @ensure_parsed 238 | def port(self): 239 | return self.config.port 240 | 241 | @property 242 | @ensure_parsed 243 | def node_name(self): 244 | return self.config.nodename 245 | 246 | @property 247 | @ensure_parsed 248 | def password(self): 249 | if self.config.password is not None: 250 | return self.config.password 251 | else: 252 | return '' 253 | 254 | @property 255 | @ensure_parsed 256 | def discovery_override(self): 257 | return self.config.discovery_override 258 | 259 | @property 260 | @ensure_parsed 261 | def service(self): 262 | return self.config.service 263 | 264 | @property 265 | @ensure_parsed 266 | def topic(self): 267 | return self.config.topic 268 | 269 | @property 270 | @ensure_parsed 271 | def ip(self): 272 | return self.config.ip 273 | 274 | @property 275 | @ensure_parsed 276 | def start_timestamp(self): 277 | return self.config.start_timestamp 278 | 279 | @property 280 | @ensure_parsed 281 | def get_anc_endpoints(self): 282 | return self.config.get_anc_endpoints 283 | 284 | @property 285 | @ensure_parsed 286 | def get_anc_policy_by_mac(self): 287 | return self.config.get_anc_policy_by_mac 288 | 289 | @property 290 | @ensure_parsed 291 | def get_anc_policies(self): 292 | return self.config.get_anc_policies 293 | 294 | @property 295 | @ensure_parsed 296 | def apply_anc_policy(self): 297 | return self.config.apply_anc_policy 298 | 299 | @property 300 | @ensure_parsed 301 | def apply_anc_policy_by_mac(self): 302 | return self.config.apply_anc_policy_by_mac 303 | 304 | @property 305 | @ensure_parsed 306 | def apply_anc_policy_by_mac_bulk(self): 307 | return self.config.apply_anc_policy_by_mac_bulk 308 | 309 | @property 310 | @ensure_parsed 311 | def apply_anc_policy_by_ip(self): 312 | return self.config.apply_anc_policy_by_ip 313 | 314 | @property 315 | @ensure_parsed 316 | def clear_anc_policy_by_mac(self): 317 | return self.config.clear_anc_policy_by_mac 318 | 319 | @property 320 | @ensure_parsed 321 | def clear_anc_policy_by_ip(self): 322 | return self.config.clear_anc_policy_by_ip 323 | 324 | @property 325 | @ensure_parsed 326 | def create_anc_policy(self): 327 | return self.config.create_anc_policy 328 | 329 | @property 330 | @ensure_parsed 331 | def delete_anc_policy(self): 332 | return self.config.delete_anc_policy 333 | 334 | @property 335 | @ensure_parsed 336 | def anc_mac_address(self): 337 | return self.config.anc_mac_address 338 | 339 | @property 340 | @ensure_parsed 341 | def anc_policy(self): 342 | return self.config.anc_policy 343 | 344 | @property 345 | @ensure_parsed 346 | def anc_ip_address(self): 347 | return self.config.anc_ip_address 348 | 349 | @property 350 | @ensure_parsed 351 | def anc_nas_ip_address(self): 352 | return self.config.anc_nas_ip_address 353 | 354 | @property 355 | @ensure_parsed 356 | def anc_policy_action(self): 357 | return self.config.anc_policy_action 358 | 359 | @property 360 | @ensure_parsed 361 | def publish_delay(self): 362 | return self.config.publish_delay 363 | 364 | @property 365 | @ensure_parsed 366 | def reregister_delay(self): 367 | return self.config.reregister_delay 368 | 369 | @property 370 | @ensure_parsed 371 | def description(self): 372 | return self.config.description 373 | 374 | @property 375 | @ensure_parsed 376 | def ssl_context(self): 377 | if self.__ssl_context == None: 378 | self.__ssl_context = ssl.create_default_context() 379 | if self.config.clientcert is not None: 380 | self.__ssl_context.load_cert_chain( 381 | certfile=self.config.clientcert, 382 | keyfile=self.config.clientkey, 383 | password=self.config.clientkeypassword) 384 | if self.config.servercert: 385 | self.__ssl_context.load_verify_locations(cafile=self.config.servercert) 386 | elif self.config.insecure: 387 | self.__ssl_context.check_hostname = False 388 | self.__ssl_context.verify_mode = ssl.CERT_NONE 389 | return self.__ssl_context 390 | -------------------------------------------------------------------------------- /pxgrid_util/create_account_config.py: -------------------------------------------------------------------------------- 1 | import argparse 2 | import ssl 3 | 4 | 5 | class CreateAccountConfig: 6 | def __init__(self): 7 | self.__ssl_context = None 8 | parser = argparse.ArgumentParser() 9 | parser.add_argument( 10 | '-a', '--hostname', required=True, 11 | help='ISE PAN host name') 12 | parser.add_argument( 13 | '--username', required=True, 14 | help='ISE username') 15 | parser.add_argument( 16 | '--password', required=True, 17 | help='ISE password') 18 | parser.add_argument( 19 | '--nodename', required=True, 20 | help='Client node name to create and approve') 21 | parser.add_argument( 22 | '--description', type=str, 23 | default='pxGrid Client', 24 | help='Optional description for the pxGrid client/node') 25 | 26 | g = parser.add_mutually_exclusive_group() 27 | g.add_argument( 28 | '-s', '--servercert', 29 | help='Server certificates pem filename') 30 | g.add_argument( 31 | '--insecure', action='store_true', 32 | help='Allow insecure server connections when using SSL') 33 | 34 | parser.add_argument( 35 | '-v', '--verbose', action='store_true', 36 | help='Verbose output') 37 | self.config = parser.parse_args() 38 | 39 | @property 40 | def hostname(self): 41 | return self.config.hostname 42 | 43 | @property 44 | def username(self): 45 | return self.config.username 46 | 47 | @property 48 | def password(self): 49 | return self.config.password 50 | 51 | @property 52 | def nodename(self): 53 | return self.config.nodename 54 | 55 | @property 56 | def description(self): 57 | return self.config.description 58 | 59 | @property 60 | def servercert(self): 61 | return self.config.servercert 62 | 63 | @property 64 | def insecure(self): 65 | return self.config.insecure 66 | 67 | @property 68 | def verbose(self): 69 | return self.config.verbose 70 | 71 | @property 72 | def ssl_context(self): 73 | if self.__ssl_context == None: 74 | self.__ssl_context = ssl.create_default_context() 75 | if self.config.servercert: 76 | self.__ssl_context.load_verify_locations(cafile=self.config.servercert) 77 | elif self.config.insecure: 78 | self.__ssl_context.check_hostname = False 79 | self.__ssl_context.verify_mode = ssl.CERT_NONE 80 | return self.__ssl_context 81 | -------------------------------------------------------------------------------- /pxgrid_util/pxgrid.py: -------------------------------------------------------------------------------- 1 | import base64 2 | import json 3 | import urllib.request 4 | import logging 5 | 6 | logger = logging.getLogger(__name__) 7 | 8 | 9 | class PXGridControl: 10 | def __init__(self, config): 11 | self.config = config 12 | 13 | def send_rest_request(self, url_suffix, payload): 14 | logger.debug('send_rest_request %s', url_suffix) 15 | url = 'https://{}:{}/pxgrid/control/{}'.format( 16 | self.config.hostname[0], 17 | self.config.port, 18 | url_suffix) 19 | json_string = json.dumps(payload) 20 | handler = urllib.request.HTTPSHandler(context=self.config.ssl_context) 21 | opener = urllib.request.build_opener(handler) 22 | rest_request = urllib.request.Request( 23 | url=url, data=str.encode(json_string)) 24 | rest_request.add_header('Content-Type', 'application/json') 25 | rest_request.add_header('Accept', 'application/json') 26 | username_password = '%s:%s' % (self.config.node_name, self.config.password) 27 | b64 = base64.b64encode(username_password.encode()).decode() 28 | rest_request.add_header('Authorization', 'Basic ' + b64) 29 | rest_response = opener.open(rest_request) 30 | response = rest_response.read().decode() 31 | return json.loads(response) 32 | 33 | def account_activate(self): 34 | logger.debug('account_activate') 35 | payload = {} 36 | if self.config.description is not None: 37 | payload['description'] = self.config.description 38 | return self.send_rest_request('AccountActivate', payload) 39 | 40 | def service_lookup(self, service_name): 41 | logger.debug('service_lookup %s', service_name) 42 | payload = {'name': service_name} 43 | return self.send_rest_request('ServiceLookup', payload) 44 | 45 | def service_register(self, service_name, properties): 46 | logger.debug('service_register %s', service_name) 47 | payload = {'name': service_name, 'properties': properties} 48 | return self.send_rest_request('ServiceRegister', payload) 49 | 50 | def service_reregister(self, service_id): 51 | logger.debug('service_reregister %s', service_id) 52 | payload = {'id': service_id} 53 | return self.send_rest_request('ServiceReregister', payload) 54 | 55 | def service_unregister(self, service_id): 56 | logger.debug('service_unregister %s', service_id) 57 | payload = {'id': service_id} 58 | return self.send_rest_request('ServiceUnregister', payload) 59 | 60 | def get_access_secret(self, peer_node_name): 61 | logger.debug('get_access_secret %s', peer_node_name) 62 | payload = {'peerNodeName': peer_node_name} 63 | return self.send_rest_request('AccessSecret', payload) 64 | -------------------------------------------------------------------------------- /pxgrid_util/stomp.py: -------------------------------------------------------------------------------- 1 | import io 2 | import logging 3 | 4 | logger = logging.getLogger(__name__) 5 | 6 | 7 | class StompFrame: 8 | def __init__(self): 9 | self.headers = {} 10 | self.command = None 11 | self.content = None 12 | 13 | def get_command(self): 14 | return self.command 15 | 16 | def set_command(self, command): 17 | self.command = command 18 | 19 | def get_content(self): 20 | return self.content 21 | 22 | def set_content(self, content): 23 | self.content = content 24 | 25 | def get_header(self, key): 26 | return self.headers[key] 27 | 28 | def set_header(self, key, value): 29 | self.headers[key] = value 30 | 31 | def write(self, out): 32 | logger.debug('write') 33 | out.write(self.command) 34 | out.write('\n') 35 | for key in self.headers: 36 | out.write(key) 37 | out.write(':') 38 | out.write(self.headers[key]) 39 | out.write('\n') 40 | out.write('\n') 41 | if self.content is not None: 42 | out.write(self.content) 43 | out.write('\0') 44 | 45 | @staticmethod 46 | def parse(input): 47 | logger.debug('parse') 48 | frame = StompFrame() 49 | frame.command = input.readline().rstrip('\r\n') 50 | for line in input: 51 | line = line.rstrip('\r\n') 52 | if line == '': 53 | break 54 | (name, value) = line.split(':') 55 | frame.headers[name] = value 56 | frame.content = input.read()[:-1] 57 | logger.debug('parse frame content: %s', frame.content) 58 | return frame 59 | -------------------------------------------------------------------------------- /pxgrid_util/ws_stomp.py: -------------------------------------------------------------------------------- 1 | import base64 2 | import websockets 3 | from io import StringIO 4 | from .stomp import StompFrame 5 | import logging 6 | 7 | logger = logging.getLogger(__name__) 8 | 9 | class WebSocketStomp: 10 | def __init__(self, ws_url, user, password, ssl_ctx, ping_interval=20.0): 11 | self.ws_url = ws_url 12 | self.user = user 13 | self.password = password 14 | self.ssl_ctx = ssl_ctx 15 | self.ping_interval = ping_interval 16 | self.ws = None 17 | 18 | async def connect(self): 19 | logger.debug('WebSocket Connect, ws_url=%s', self.ws_url) 20 | b64 = base64.b64encode( 21 | (self.user + ':' + self.password).encode()).decode() 22 | self.ws = await websockets.connect( 23 | uri=self.ws_url, 24 | ping_interval=self.ping_interval, 25 | additional_headers={ 26 | 'Authorization': 'Basic ' + b64}, 27 | ssl=self.ssl_ctx) 28 | 29 | async def stomp_connect(self, hostname): 30 | logger.debug('STOMP CONNECT host=%s', hostname) 31 | frame = StompFrame() 32 | frame.set_command("CONNECT") 33 | frame.set_header('accept-version', '1.2') 34 | frame.set_header('host', hostname) 35 | out = StringIO() 36 | frame.write(out) 37 | await self.ws.send(out.getvalue().encode('utf-8')) 38 | logger.debug('stomp_connect completed') 39 | 40 | async def stomp_subscribe(self, topic): 41 | logger.debug('STOMP SUBSCRIBE topic=%s', topic) 42 | frame = StompFrame() 43 | frame.set_command("SUBSCRIBE") 44 | frame.set_header('destination', topic) 45 | frame.set_header('id', 'my-id') 46 | out = StringIO() 47 | frame.write(out) 48 | await self.ws.send(out.getvalue().encode('utf-8')) 49 | logger.debug('stomp_subscribe completed') 50 | 51 | async def stomp_send(self, topic, message): 52 | logger.debug('STOMP SEND topic=' + topic) 53 | frame = StompFrame() 54 | frame.set_command("SEND") 55 | frame.set_header('destination', topic) 56 | frame.set_header('content-length', str(len(message))) 57 | frame.set_content(message) 58 | out = StringIO() 59 | frame.write(out) 60 | await self.ws.send(out.getvalue().encode('utf-8')) 61 | logger.debug('stomp_send completed') 62 | 63 | # only returns for MESSAGE 64 | async def stomp_read_message(self): 65 | while True: 66 | message = await self.ws.recv() 67 | s_in = StringIO(message.decode('utf-8')) 68 | stomp = StompFrame.parse(s_in) 69 | if stomp.get_command() == 'MESSAGE': 70 | return stomp.get_content() 71 | elif stomp.get_command() == 'CONNECTED': 72 | version = stomp.get_header('version') 73 | logger.debug('STOMP CONNECTED version=' + version) 74 | elif stomp.get_command() == 'RECEIPT': 75 | receipt = stomp.get_header('receipt-id') 76 | logger.debug('STOMP RECEIPT id=' + receipt) 77 | elif stomp.get_command() == 'ERROR': 78 | logger.debug('STOMP ERROR content=' + stomp.get_content()) 79 | pass 80 | logger.debug('stomp_read_message completed') 81 | 82 | async def stomp_disconnect(self, receipt=None): 83 | logger.debug('STOMP DISCONNECT receipt=' + receipt) 84 | frame = StompFrame() 85 | frame.set_command("DISCONNECT") 86 | if receipt is not None: 87 | frame.set_header('receipt', receipt) 88 | out = StringIO() 89 | frame.write(out) 90 | await self.ws.send(out.getvalue().encode('utf-8')) 91 | 92 | async def disconnect(self): 93 | await self.ws.close() 94 | 95 | def is_open(self): 96 | return self.ws.open 97 | -------------------------------------------------------------------------------- /sample_macs/sample_mac_addrs_1000.txt: -------------------------------------------------------------------------------- 1 | 72:0e:a8:f1:b6:94 2 | 51:7a:10:0e:6e:a1 3 | 24:a4:95:28:08:e4 4 | f7:cc:af:09:5b:b5 5 | fa:5d:f9:7b:88:93 6 | 91:81:48:13:cf:64 7 | 88:cd:90:46:e4:d6 8 | cc:0e:99:83:36:ce 9 | 7e:38:03:b5:c5:6d 10 | e1:f9:7a:b4:36:43 11 | 20:c2:4e:c3:e8:17 12 | d8:a1:b2:e4:63:7d 13 | 8b:c3:e2:69:f6:ee 14 | cb:2b:37:28:fd:99 15 | ee:fc:67:97:85:03 16 | 6b:ea:6a:a8:b2:df 17 | c5:6d:5b:03:13:30 18 | 1d:d6:7d:0b:75:68 19 | f9:fc:73:9a:0a:63 20 | 85:6d:31:c6:4c:9e 21 | 5c:64:f9:b0:b9:92 22 | 67:47:73:0b:5e:4f 23 | ad:3e:73:8b:b0:22 24 | 63:5d:6a:35:d1:be 25 | 8d:b8:35:40:b7:5d 26 | fe:c3:38:56:16:ae 27 | ab:e0:0a:ed:3b:0c 28 | 06:1b:dd:90:12:4f 29 | fe:1d:3c:77:26:be 30 | 28:27:e8:85:e3:3f 31 | fd:bf:1a:9c:c0:bd 32 | 53:01:db:89:d5:da 33 | 80:76:2c:fc:3c:a3 34 | ea:3b:f9:2e:e0:42 35 | 36:16:31:98:33:b0 36 | 3f:30:f9:39:a6:27 37 | 4b:ed:1c:24:2f:4e 38 | cc:dd:6d:db:ad:9d 39 | 57:3e:9a:da:71:2d 40 | 52:ec:3d:c8:15:09 41 | 7f:53:0b:0d:97:bb 42 | 64:03:a6:41:cd:31 43 | 32:96:e1:f8:5f:3a 44 | 57:d1:22:a2:94:16 45 | 12:9f:02:58:ba:3e 46 | 2a:a6:77:23:55:ea 47 | 71:cb:a0:ad:d6:41 48 | c4:dc:da:61:49:a3 49 | 73:28:77:1b:90:79 50 | a5:ec:c9:82:1d:83 51 | e0:9d:d1:cf:33:c6 52 | 4d:33:89:84:2a:c1 53 | 20:8b:52:93:a8:67 54 | d5:48:89:9b:cc:49 55 | de:37:cf:be:13:7a 56 | b5:2a:65:83:46:76 57 | 1e:02:1d:f1:ce:b0 58 | 02:51:d0:5a:c2:d4 59 | 34:d4:c4:a4:ee:1a 60 | de:1f:81:15:09:80 61 | e9:d5:d0:23:40:5b 62 | 7d:a6:1f:1e:78:9e 63 | 21:d3:d9:a1:4e:9f 64 | f2:41:d4:2a:c1:c7 65 | 93:4b:ac:31:36:0a 66 | 92:c7:70:3e:48:93 67 | 0a:6b:7b:c9:2e:e4 68 | d9:0b:f3:08:66:db 69 | 5d:bd:09:29:22:7a 70 | 44:36:da:21:a5:44 71 | 4f:32:bb:d0:c8:94 72 | 54:46:0d:79:ad:c9 73 | fc:42:d7:cc:d6:8d 74 | f6:95:de:3a:aa:ca 75 | c2:ff:b2:63:70:7b 76 | 08:95:a5:88:d8:e3 77 | 63:bf:cf:2b:cd:17 78 | 4c:66:41:45:87:06 79 | bb:f6:56:b9:98:d0 80 | 79:7b:14:7a:60:53 81 | 77:1f:f5:66:e9:23 82 | 33:03:34:28:e3:e3 83 | 17:1a:40:94:bf:b0 84 | 2f:42:a2:b1:5e:4f 85 | 28:11:e5:a5:37:0d 86 | bd:ca:19:71:83:33 87 | e7:bf:c7:2f:a5:98 88 | ed:dc:7c:b3:43:8e 89 | e9:6c:eb:f6:b8:ea 90 | 37:88:fd:83:b6:bf 91 | 85:c4:49:49:ce:d2 92 | b2:2e:89:9e:54:97 93 | a6:ac:22:6b:f5:04 94 | f3:7f:f5:4e:2b:7f 95 | 61:3e:e0:d6:d2:ae 96 | cb:1f:d2:47:bf:b0 97 | b5:fa:55:06:62:73 98 | 74:de:2c:9c:27:cf 99 | 3a:bd:55:0b:59:eb 100 | 2b:23:89:b9:ac:16 101 | b4:b4:72:fe:8f:aa 102 | ea:89:cd:1b:a9:ac 103 | 6c:3a:41:eb:f4:d3 104 | a5:99:b0:a6:bf:5c 105 | 8a:fb:37:e5:25:f6 106 | 7f:c6:52:e7:f2:ae 107 | fc:c9:b2:3d:e4:39 108 | 10:0b:ff:ef:ec:6d 109 | d0:70:a3:85:cc:e7 110 | b3:09:fd:60:6c:25 111 | 8a:35:0f:1c:e4:ab 112 | b5:ab:53:6c:36:f1 113 | 70:8d:a9:35:b3:93 114 | bd:c0:4c:7a:62:ee 115 | af:54:83:dd:27:87 116 | 42:c8:7e:db:2c:34 117 | 1d:bb:8f:8a:2c:44 118 | 95:d2:de:7d:1d:78 119 | d5:a8:8a:ed:51:b7 120 | 14:8a:fd:4a:c3:85 121 | e9:40:a4:92:2f:d1 122 | db:5c:98:53:a4:6b 123 | cc:c7:ed:8c:f0:f6 124 | 14:d6:3f:bf:92:c8 125 | 31:bb:93:39:b2:6c 126 | 86:d9:e9:56:84:a3 127 | cb:45:5f:e7:18:52 128 | 63:af:cf:35:44:0f 129 | 92:31:9a:8c:93:d2 130 | e6:22:62:d4:03:33 131 | d2:f6:b1:7f:08:b7 132 | d2:31:88:d1:43:5e 133 | 77:9f:63:c2:88:cd 134 | d5:b6:cc:15:a4:d4 135 | 8a:35:c9:3f:d9:61 136 | ca:78:00:ea:3a:8d 137 | 34:ad:1e:67:e2:1c 138 | 7b:75:63:98:53:c3 139 | 70:bb:ae:b1:05:4d 140 | b8:68:56:19:5d:87 141 | d6:16:5a:ed:ff:49 142 | 4a:ed:38:b5:b1:a7 143 | 2f:8b:17:1e:3a:27 144 | f7:b6:56:fa:af:6c 145 | a3:e7:a0:23:67:ef 146 | 64:c6:0f:dd:2c:2a 147 | a8:73:6f:47:c6:03 148 | b6:82:be:9c:cf:4f 149 | cb:1b:ad:44:aa:0f 150 | db:9d:6f:a2:80:f6 151 | 1e:50:24:c5:cd:1d 152 | 51:c0:c8:a0:28:56 153 | 95:03:99:1f:9e:07 154 | ae:51:87:24:26:d6 155 | a8:d9:d8:46:f5:7b 156 | a8:c7:80:09:9c:61 157 | f5:ee:ce:5d:7e:c9 158 | 12:b8:5b:50:82:8f 159 | 93:4b:73:03:08:cd 160 | 11:a7:48:cd:69:c3 161 | 1d:93:4c:05:34:89 162 | 33:4e:aa:b2:57:76 163 | d4:55:a4:6d:61:be 164 | dd:72:2e:dc:e0:3d 165 | cc:09:74:ad:13:bf 166 | 38:8b:92:44:41:fb 167 | eb:b4:bc:ba:17:a9 168 | af:e8:cd:46:ad:53 169 | 0f:ce:6e:fc:0d:b5 170 | 21:b6:03:9b:33:4b 171 | 20:56:e6:76:1d:96 172 | df:43:24:35:41:9c 173 | ad:ac:3d:41:fd:0f 174 | 07:5a:5a:d7:a2:6a 175 | c5:ed:f0:34:6d:df 176 | a9:58:4f:6a:30:2d 177 | 9c:79:15:5b:2e:98 178 | 8c:3f:e7:7b:55:53 179 | 6b:cc:e5:44:6e:06 180 | 66:b8:f5:e4:64:cb 181 | be:18:a3:89:0e:4a 182 | 65:e1:01:cf:85:e8 183 | dd:b1:4b:3c:55:e0 184 | 9e:ea:50:d3:de:4d 185 | dc:79:88:ee:34:24 186 | 9d:12:38:6b:1b:ed 187 | 1a:36:13:e8:83:39 188 | 37:13:e1:eb:9b:fa 189 | 84:cc:7d:6c:ac:1f 190 | fc:8a:b1:22:0f:59 191 | 64:f9:ac:9d:20:d8 192 | e7:19:c4:51:72:27 193 | 68:09:20:e8:cb:44 194 | 7c:41:1b:cf:b9:14 195 | 2a:8e:0f:cd:97:a3 196 | 03:1d:dc:97:61:7c 197 | 2e:ff:d2:4a:17:cd 198 | 48:23:92:9a:5e:89 199 | 8f:9b:b5:31:ad:2d 200 | c6:4f:f8:f4:df:6b 201 | 17:a6:0f:34:a0:fa 202 | 31:c9:93:31:6a:de 203 | d6:8a:32:a1:87:8b 204 | 5f:27:ee:fd:58:7b 205 | 31:cc:d4:87:18:23 206 | 5d:d2:05:e2:3f:ad 207 | ae:a6:71:48:d1:eb 208 | ce:56:79:f8:e3:11 209 | 06:5d:e9:29:29:4b 210 | 96:9f:fd:50:5e:74 211 | cd:e7:e4:a8:30:b6 212 | 72:b3:f4:df:a2:ff 213 | b5:72:b7:43:78:97 214 | b5:ea:2e:67:2f:dc 215 | 00:d5:96:00:7f:0d 216 | 7f:dc:18:ca:63:37 217 | 04:87:bf:8c:1c:c0 218 | 59:bb:c6:7a:73:b3 219 | 16:7a:ee:57:4e:32 220 | 22:30:cf:ab:7e:54 221 | ff:cb:db:22:2e:f9 222 | 32:a0:bd:74:ff:d4 223 | 79:5b:22:72:28:93 224 | 0b:51:51:9e:09:b8 225 | 00:27:7b:b9:cc:e2 226 | 34:4d:f1:7b:78:86 227 | f1:ee:88:c5:c5:e0 228 | 38:8f:ec:af:be:b5 229 | 5e:23:88:eb:e7:4a 230 | 80:97:9d:a4:57:3d 231 | 6e:79:42:03:ff:94 232 | 6a:40:94:c9:fa:19 233 | f8:89:9f:cc:ef:d2 234 | 8a:51:27:17:3d:b4 235 | 71:4f:b5:e4:c6:c6 236 | f6:f9:78:50:aa:b4 237 | 44:f6:9d:66:b5:91 238 | bf:e7:53:09:e3:11 239 | 83:fb:48:ff:74:ea 240 | 65:93:20:9c:93:02 241 | ea:e1:eb:16:fd:27 242 | 2f:09:83:62:bc:b9 243 | 47:02:13:5a:26:f4 244 | 62:76:de:f9:a4:2f 245 | b5:c0:ee:51:ee:d3 246 | b7:41:9e:67:07:6b 247 | 8b:37:3d:11:bc:d6 248 | 15:ec:02:78:ea:42 249 | 65:11:01:be:fa:e3 250 | 94:5f:3d:3b:a2:20 251 | 00:38:7f:da:6a:a3 252 | 92:bd:3d:86:ef:b3 253 | 52:9b:26:84:34:ca 254 | 37:f3:fe:de:6d:a1 255 | 66:40:50:aa:46:22 256 | 4d:80:f5:da:ba:a3 257 | 5b:0d:a0:2b:e6:4e 258 | 18:2b:b9:d2:9a:bd 259 | f5:20:9b:29:d8:f3 260 | 7d:6f:71:7a:4f:8e 261 | 05:27:d1:41:b7:1a 262 | 7c:87:34:15:1a:00 263 | 0e:4b:2b:cc:fe:90 264 | 1f:00:7a:47:b8:1b 265 | 83:b2:13:ac:b7:5c 266 | ad:ca:c8:6b:0f:3e 267 | 38:06:46:1b:2c:d0 268 | 38:2b:3f:3d:f5:61 269 | 83:77:a9:33:da:92 270 | df:27:46:c1:64:e1 271 | 81:43:9e:c0:09:30 272 | a5:c0:de:1f:3f:91 273 | 92:1f:f4:07:0c:e2 274 | 22:df:14:fc:85:e4 275 | e5:38:80:79:b5:94 276 | a7:52:55:2e:df:9c 277 | 79:75:4e:b6:93:40 278 | 58:76:a7:30:7d:fd 279 | b0:be:00:fa:14:fe 280 | bb:aa:8d:ab:cc:56 281 | 12:ea:7a:e8:85:0a 282 | 5c:35:f8:1d:19:5b 283 | 66:fe:6a:55:27:d9 284 | 68:f7:fd:20:3b:13 285 | d5:6d:73:ae:eb:bf 286 | 9f:ee:ad:0f:84:f4 287 | e7:d0:f5:d1:3d:c0 288 | 92:69:4a:81:f6:52 289 | 53:14:0a:ac:68:da 290 | d8:fe:7f:96:dd:7e 291 | ee:39:95:6a:7f:1a 292 | 5e:1e:79:cb:14:0a 293 | 3c:a6:fb:09:a2:2a 294 | f6:5f:76:05:1a:c0 295 | d3:4b:43:55:5a:12 296 | 79:ab:29:b9:1d:0e 297 | 22:2d:37:19:3e:ff 298 | e6:1b:49:cf:a6:b5 299 | c5:d9:5a:c4:1a:ab 300 | 26:4b:4c:be:37:06 301 | 5f:bf:b9:1c:77:d0 302 | 95:41:f1:29:a1:8a 303 | 1d:8e:02:15:79:cb 304 | d1:27:97:02:db:93 305 | 88:9b:33:06:7a:7e 306 | 3e:b4:48:46:bc:03 307 | a5:7e:c8:84:a3:b5 308 | 6d:03:a0:76:b2:dc 309 | 47:69:e2:44:12:1e 310 | 59:4a:32:1f:b2:7a 311 | 3b:6a:19:95:c5:9b 312 | 19:aa:ff:9c:4a:d4 313 | 20:cd:49:84:0c:6e 314 | 4c:3c:7f:87:74:f5 315 | c7:23:4e:63:e6:b9 316 | fb:83:33:0d:57:bb 317 | 92:ee:37:b5:8f:50 318 | 52:36:ef:4d:f8:ec 319 | c0:59:f5:91:e0:b1 320 | 0b:62:a9:e6:63:b8 321 | e2:76:1f:53:a0:7b 322 | 2e:bf:84:3f:cf:26 323 | 8c:4f:c0:0e:9f:8d 324 | 80:97:85:84:cd:22 325 | ea:13:71:0c:f0:e7 326 | ae:0f:1a:4a:97:5f 327 | 3e:17:5a:c9:c3:18 328 | 1f:61:25:3e:fe:3c 329 | 99:21:87:2b:71:e0 330 | e5:2d:aa:d9:5b:d4 331 | 4e:14:7c:8c:e4:a0 332 | 71:0b:c2:3d:32:eb 333 | 26:62:6a:53:23:dc 334 | 0b:1e:27:33:37:84 335 | 59:42:8b:2c:c8:73 336 | 0d:c3:7e:66:5b:ce 337 | b7:b2:0f:58:e2:50 338 | 27:f4:ee:d5:52:ce 339 | a3:b5:d8:f2:23:1c 340 | 46:ec:74:51:f8:9e 341 | 84:4d:5e:a6:b1:39 342 | 14:3c:1c:7c:43:90 343 | 51:f3:9c:3c:39:3a 344 | 40:75:07:3f:50:e7 345 | 37:b7:25:36:b5:c4 346 | 52:01:fe:a5:66:55 347 | 32:8a:fd:48:aa:97 348 | 09:88:d0:0b:14:dc 349 | 70:2e:ed:81:9a:65 350 | 75:d5:70:32:50:62 351 | d4:f9:d1:f2:ea:13 352 | e7:56:39:ed:1e:f0 353 | 2f:ae:dc:6f:7e:bb 354 | a5:b2:db:a4:77:2f 355 | 03:f6:ac:85:50:32 356 | 87:14:6c:1d:ea:fa 357 | f9:78:2e:c4:3a:0b 358 | 37:5c:01:eb:4c:f8 359 | 0a:f9:d0:99:39:32 360 | 8e:79:06:18:1b:d8 361 | 25:e5:e7:92:2a:b2 362 | 85:5e:2b:e9:27:71 363 | 5f:e0:17:d8:9f:ae 364 | 79:59:5e:2e:16:d6 365 | cc:b1:c5:d7:fd:42 366 | 91:35:ea:a0:80:35 367 | 57:e5:2d:fa:58:c8 368 | cb:df:5e:5c:88:c2 369 | 69:8a:00:db:1d:7b 370 | 72:f5:78:66:e3:34 371 | 4c:a1:91:ec:87:19 372 | 11:7f:56:bf:21:25 373 | 81:41:57:f3:ee:92 374 | b4:5d:25:9d:d8:82 375 | cf:28:02:28:d4:35 376 | 53:11:0e:5b:e2:57 377 | ef:0a:b5:f1:30:39 378 | c0:f3:47:4d:f3:1b 379 | e1:ec:d7:05:eb:e5 380 | 98:bd:a4:39:5b:02 381 | 5c:3f:1f:ef:3c:7d 382 | 08:dc:90:5f:a6:a7 383 | 86:87:c2:a1:0b:b8 384 | d8:57:5f:7f:a2:f3 385 | 33:b4:51:06:fd:ad 386 | 50:ff:d5:0a:40:f2 387 | 4e:70:a0:03:84:64 388 | 33:3f:3c:a9:21:3f 389 | 70:ac:b9:f1:17:98 390 | 77:9a:72:fb:77:5d 391 | bc:5a:2a:2f:51:c9 392 | 9d:a7:bb:61:af:a4 393 | eb:cf:01:3f:9a:5f 394 | 65:73:05:62:28:a8 395 | 36:eb:84:7e:e5:de 396 | f2:9e:00:9d:e2:68 397 | 54:3c:a5:bb:45:78 398 | 57:02:e3:35:22:ea 399 | 64:4a:72:f7:14:03 400 | 54:1b:d8:cd:c8:bd 401 | 81:55:c1:a4:7e:8f 402 | 89:33:00:3b:0e:dd 403 | a4:23:f3:c6:33:4f 404 | 62:5c:66:e7:0e:58 405 | 13:5e:19:55:3c:cc 406 | 51:07:c4:ef:65:85 407 | 9f:ac:44:2c:57:70 408 | 8a:b3:77:5c:a2:34 409 | 18:30:af:03:7b:3f 410 | 28:61:d4:e6:7d:df 411 | fe:43:b3:f5:81:b2 412 | df:d6:19:77:9c:cb 413 | 56:55:74:e8:b4:e8 414 | 6b:81:92:27:01:d5 415 | 45:a5:a0:3a:b3:4b 416 | 5b:44:b3:2e:c8:ac 417 | d0:9b:02:3c:b3:27 418 | 04:2d:46:41:96:cd 419 | d3:9f:6d:2a:f7:a0 420 | 59:d9:6a:f1:f8:f5 421 | 98:94:5a:28:59:10 422 | 3b:1f:ef:a5:40:17 423 | 96:06:7e:15:cb:ae 424 | e3:fa:cb:33:db:78 425 | 20:8d:a4:24:e5:32 426 | 88:fb:a3:ee:6a:85 427 | 00:80:ba:5c:0f:26 428 | 29:cd:5c:f6:17:d6 429 | 66:b7:03:45:d3:42 430 | 32:a9:39:3b:c8:e8 431 | d1:57:ad:99:fb:6f 432 | 21:7a:e4:4e:0c:6c 433 | 42:32:bf:40:54:0f 434 | 6f:66:a4:da:97:79 435 | b5:c2:77:af:64:a5 436 | fe:41:3b:c8:d3:0b 437 | db:6d:c2:07:51:d6 438 | 11:80:f2:9c:b1:4e 439 | fa:e9:19:58:d8:c6 440 | de:5d:94:ab:8a:c9 441 | 5f:1f:93:16:22:e8 442 | db:04:9e:2f:19:ae 443 | b6:93:60:ae:ed:ac 444 | 41:f5:34:b0:a8:02 445 | 81:43:d3:fd:66:20 446 | eb:3e:d5:9a:d8:c1 447 | 72:1b:ad:b0:bd:af 448 | 64:9f:e7:99:13:2d 449 | d9:fe:8a:8b:c3:ec 450 | a3:fb:c1:47:35:25 451 | 5c:f8:5d:e6:bd:df 452 | 91:19:02:24:31:af 453 | 3e:42:13:f9:f0:c8 454 | 62:16:7b:12:50:1e 455 | 43:b4:84:54:3e:a1 456 | be:2a:51:92:ed:9b 457 | 28:8e:7b:44:05:40 458 | 63:a8:b0:5b:7a:14 459 | 4e:d0:af:35:c1:d6 460 | 13:64:36:25:0a:81 461 | 05:23:d3:cd:4f:3d 462 | 21:0c:18:6a:30:ef 463 | 06:a1:4f:64:df:0a 464 | 68:27:42:4f:24:dd 465 | 3d:4d:09:d0:80:ad 466 | a1:db:3e:53:be:56 467 | 6c:20:31:23:cd:7a 468 | d0:6f:08:b6:1e:61 469 | b6:61:5d:8d:8b:30 470 | d6:ce:e5:d7:98:1c 471 | e2:de:1d:98:56:bd 472 | 32:32:af:b4:d6:04 473 | ef:d3:50:a8:da:e5 474 | 91:87:7b:0e:19:7a 475 | 85:ee:ae:30:5a:e7 476 | 00:cf:df:05:4e:41 477 | ba:93:2f:75:a6:68 478 | af:cb:22:4b:fa:92 479 | 6f:c7:f2:c5:bc:9b 480 | 47:e8:cb:a4:e2:38 481 | 61:27:9d:75:0f:6b 482 | 84:80:94:16:6f:6e 483 | 2b:c6:07:28:71:02 484 | 45:43:7d:5d:05:3a 485 | 0c:a3:ce:26:c8:6d 486 | d7:43:99:c3:5d:df 487 | a5:47:ad:a1:e2:44 488 | 4d:c7:4a:4a:cd:12 489 | 99:c0:08:a2:ac:10 490 | 33:77:65:b2:c6:11 491 | 8e:f5:09:1a:5e:41 492 | 09:45:91:9f:06:00 493 | 3f:80:e3:df:58:69 494 | 67:29:26:2b:3c:ff 495 | 54:f4:54:e5:e4:88 496 | 14:c5:9f:c3:7c:47 497 | 3d:f5:1c:fd:a7:8a 498 | 65:02:ce:41:51:45 499 | 80:79:aa:8d:83:bf 500 | ac:6d:04:4a:f4:67 501 | fc:17:dd:62:64:9d 502 | 30:c9:e1:6d:d6:5e 503 | a1:48:0a:1a:0c:8d 504 | f2:7b:b6:a2:7b:57 505 | a4:2e:a7:6d:92:92 506 | 0e:4c:bd:d0:98:d8 507 | 93:bd:82:25:8e:b8 508 | 66:d5:86:00:f0:d3 509 | 1e:80:03:6d:6e:ab 510 | 47:51:31:73:5e:1e 511 | a0:95:49:19:b0:56 512 | 0e:16:44:28:cd:40 513 | 4d:64:1d:74:38:72 514 | 27:a4:b7:87:29:a8 515 | f6:12:21:57:8a:be 516 | 68:bd:cb:cf:f1:f6 517 | 1c:c4:ab:00:95:8c 518 | 9f:89:1b:b0:fd:75 519 | 23:3b:d1:1f:31:12 520 | bb:40:d4:bd:69:b0 521 | e7:ad:b2:b9:a6:22 522 | 12:1b:76:e1:65:b8 523 | 21:23:95:2a:7e:72 524 | 4f:0c:df:ac:14:0f 525 | dc:2b:3b:6e:a9:25 526 | 85:1f:8e:ce:46:f5 527 | 49:49:1a:97:08:df 528 | 37:91:d0:73:69:c4 529 | 9d:7d:c2:d7:f3:59 530 | 6e:c8:be:9d:76:9c 531 | 7c:d7:57:ce:2c:07 532 | e7:b4:3a:3f:20:3a 533 | 59:6c:56:9e:5e:9e 534 | 23:aa:c0:12:c8:95 535 | 55:ec:1d:37:86:b5 536 | 60:90:0c:ad:3f:23 537 | 03:aa:d0:63:19:fd 538 | 94:dd:0e:27:c2:b7 539 | b5:b8:84:65:b4:08 540 | a6:bd:94:83:a4:84 541 | d8:9b:73:d8:ad:4e 542 | 8c:c0:db:90:bd:2c 543 | 16:7e:a4:17:d2:5d 544 | 17:ba:30:50:6f:c4 545 | 76:41:20:ed:aa:b1 546 | f3:73:92:7e:5d:34 547 | c0:3e:50:f4:69:4b 548 | 49:8f:93:07:67:24 549 | 03:8d:dd:15:a3:cf 550 | 2e:1a:8d:3e:ba:e1 551 | da:ca:ba:b0:8f:bc 552 | 3e:60:8b:4c:eb:24 553 | 05:1e:79:a1:ee:ee 554 | 45:c3:40:2c:f2:62 555 | 27:0a:52:97:33:b5 556 | b4:e2:1d:02:24:8c 557 | 0e:bd:a3:fc:72:3e 558 | ea:da:15:a9:3b:b8 559 | 5d:93:aa:b8:ff:5b 560 | c0:20:35:85:d9:60 561 | 10:42:85:d0:d8:84 562 | 34:fc:1c:38:2e:e5 563 | f8:26:1d:7e:99:ef 564 | 25:28:e1:86:bb:4b 565 | bd:1e:8c:6d:ee:d1 566 | 34:98:42:b7:b6:c5 567 | 9d:ca:6c:26:d7:6d 568 | 2a:97:f9:98:0c:c9 569 | 64:92:84:55:81:37 570 | e1:ec:56:0a:12:f0 571 | c6:d5:8b:fb:2e:6d 572 | d0:14:1d:1d:68:53 573 | 53:f5:89:32:fe:3c 574 | c7:18:6a:cd:d2:ef 575 | 0f:e2:a3:b7:e6:a9 576 | 65:a6:de:16:e5:8a 577 | fa:0a:9c:fa:18:7a 578 | 3b:b6:e6:45:e7:b1 579 | bd:82:1d:c0:37:0a 580 | db:14:9e:bd:5d:01 581 | 33:57:10:aa:9c:7a 582 | 2b:8e:4a:65:fb:cd 583 | b6:8b:5d:fd:06:7a 584 | 4c:fe:fb:0e:3f:3e 585 | 58:28:c4:54:b5:64 586 | be:36:45:19:c5:7a 587 | cd:0d:5a:24:57:dc 588 | ed:6b:36:3b:9c:5b 589 | bb:c2:c5:aa:c5:3a 590 | 2d:22:8d:a4:d9:f4 591 | cf:e6:9f:a0:2e:3d 592 | d3:9d:62:81:4f:45 593 | ed:6e:be:d8:22:64 594 | c2:c1:2f:29:ec:35 595 | f7:a3:56:89:05:4c 596 | 6d:c8:cb:04:eb:be 597 | 11:ef:9d:79:42:72 598 | 6c:98:4d:ca:c2:3d 599 | f8:dc:1b:b1:f8:8b 600 | 89:d4:ab:8a:de:21 601 | 14:e0:89:9f:a4:9e 602 | d5:e5:b0:9e:f2:cf 603 | 5f:87:5b:a1:f8:18 604 | 30:26:14:90:af:72 605 | 80:e3:36:fa:86:47 606 | cd:17:c2:46:6c:84 607 | b0:a8:f2:9d:89:01 608 | de:02:cb:22:ea:d3 609 | e9:08:77:6f:78:48 610 | ee:7b:ca:3c:4e:72 611 | 28:e7:aa:86:ab:95 612 | dd:9c:73:23:c0:98 613 | 7f:30:77:4d:48:da 614 | 10:af:30:1f:ae:7c 615 | 5f:ae:7a:c3:8f:31 616 | 44:72:bb:6a:7c:70 617 | 23:d1:c4:ec:d0:d1 618 | e1:0a:31:bf:56:bd 619 | bd:48:64:0e:81:ce 620 | fa:55:0f:6c:dc:d2 621 | 32:c6:47:aa:ab:0b 622 | 5c:f5:db:d5:6c:f1 623 | d1:98:24:6a:c6:15 624 | 66:0b:b8:ff:9c:17 625 | f2:eb:95:af:b8:52 626 | 33:38:16:2b:3a:49 627 | ff:df:33:ca:48:3b 628 | 95:8a:18:ce:85:28 629 | 4c:1c:e2:01:4c:84 630 | 3e:86:46:04:7f:0d 631 | 69:01:57:3a:12:d1 632 | 5e:d3:57:1d:e5:d6 633 | 4c:8e:d5:3e:0c:b5 634 | 00:3b:5e:71:dd:3c 635 | 6f:aa:70:36:73:65 636 | 9b:5d:29:a3:2b:74 637 | a9:c8:8b:fe:49:e7 638 | ed:12:72:bc:08:21 639 | 32:cc:bb:9f:21:6e 640 | 6c:22:1f:2c:1d:f9 641 | c3:62:cd:91:58:3e 642 | 4f:fa:a0:66:76:31 643 | c7:fa:e8:70:0e:05 644 | d0:c0:53:ea:fe:13 645 | 9e:60:a6:ed:3f:b3 646 | 14:28:9e:7c:4e:ae 647 | f1:fe:44:76:3c:2c 648 | 93:33:76:e8:d2:af 649 | 1a:69:51:34:37:68 650 | 33:3e:e5:e3:ac:bd 651 | 85:50:b1:1c:e8:ec 652 | f8:f0:90:5e:2b:9f 653 | 14:1a:ba:8a:b3:9c 654 | 94:d4:df:1d:c8:1e 655 | d6:1b:f9:3f:5c:d2 656 | c2:50:65:bb:cc:5e 657 | 68:00:8b:2b:95:cf 658 | 71:69:8b:43:b8:2b 659 | 3a:9e:59:d8:03:4d 660 | 69:9a:98:ce:15:69 661 | ee:29:ef:84:37:1a 662 | ad:a7:c5:9b:96:9d 663 | b4:24:63:9f:c8:53 664 | 9e:29:ab:f7:a3:53 665 | 22:b3:dc:ff:d5:f3 666 | 8e:a1:78:65:48:c7 667 | 3d:2e:bc:81:fc:15 668 | b9:13:fb:a4:48:af 669 | 69:02:00:e1:2a:cf 670 | fa:6d:09:69:a7:35 671 | 10:7e:49:4b:27:16 672 | ec:c5:90:91:1d:a5 673 | aa:f5:41:09:e0:36 674 | f6:11:e3:dc:88:a9 675 | a2:7d:86:fc:06:e2 676 | 3d:b8:53:33:e2:86 677 | 26:10:c6:90:2a:4d 678 | ae:61:86:b4:0a:1f 679 | 14:58:c3:bf:3b:5c 680 | 92:43:3f:d0:a3:20 681 | 79:2b:6f:61:43:1c 682 | f6:3a:f6:e2:c3:1b 683 | df:bc:85:f7:75:18 684 | 20:81:04:f2:8a:bf 685 | bd:a4:c2:e3:df:56 686 | 3a:46:59:1d:fd:72 687 | bd:ab:c9:c4:b5:4c 688 | 90:2f:a5:55:d3:a9 689 | 9a:97:ca:33:bf:96 690 | 0f:a3:42:20:94:07 691 | b9:22:0f:68:a4:a3 692 | ca:81:4e:f9:dc:e0 693 | 43:e8:36:39:68:1a 694 | 99:c6:df:25:0c:0c 695 | 06:07:7f:fa:85:ad 696 | ea:65:be:47:3f:f1 697 | 32:c6:82:5b:52:b2 698 | 44:30:08:0a:09:f5 699 | 3b:68:1e:d7:29:8e 700 | 14:04:88:c0:46:c9 701 | c5:61:8b:0b:e9:39 702 | 44:b3:4b:e0:c3:28 703 | 9d:db:75:f1:62:b4 704 | 09:d4:be:0b:36:0a 705 | 0a:03:33:28:ec:3c 706 | d4:3e:dc:2c:c3:a4 707 | 86:bb:96:43:64:10 708 | 94:7d:00:3a:9e:16 709 | 6d:96:76:89:cf:85 710 | d3:26:cb:55:e3:d8 711 | 3e:24:43:ad:13:ca 712 | ad:ab:8e:d5:ca:2d 713 | d0:78:4c:c9:99:78 714 | 56:52:8e:3e:2f:26 715 | ed:ef:01:4f:e6:32 716 | 5a:71:a9:99:d4:50 717 | 0f:91:39:8c:61:7e 718 | ab:23:50:14:5d:c1 719 | c6:6c:a4:a2:f8:c5 720 | a2:7a:0a:39:64:69 721 | e2:ea:44:45:63:cf 722 | a3:03:50:1f:6d:01 723 | 4d:d7:c2:06:53:d5 724 | e8:1d:b5:cf:e2:a3 725 | f7:53:22:64:76:53 726 | cd:62:db:1a:18:ad 727 | b7:a8:32:2a:7e:d4 728 | 65:c7:9e:91:eb:33 729 | fa:f7:6d:49:14:33 730 | 84:00:da:6c:4b:63 731 | 04:be:a4:73:34:36 732 | 58:ed:3d:6e:9c:39 733 | 23:9e:3b:ec:ef:a5 734 | 64:ec:da:30:ce:f4 735 | 1e:32:20:85:ac:9d 736 | eb:a3:1e:bd:0d:1e 737 | 94:87:b9:3b:df:c2 738 | 99:dc:af:bc:f9:a8 739 | 48:b9:95:ea:61:db 740 | aa:4a:0d:e9:b6:6b 741 | 90:91:1d:d0:d2:54 742 | 99:c6:3a:67:23:16 743 | ec:51:85:8d:6d:a6 744 | 29:35:87:9c:d0:6e 745 | 66:65:24:82:69:ad 746 | d5:ac:92:77:1a:83 747 | aa:99:aa:d3:09:be 748 | 8e:a1:95:62:5c:17 749 | b5:12:ad:25:d1:32 750 | 8c:e5:5b:b8:e9:25 751 | 28:8d:b7:3f:b8:81 752 | cd:3e:2d:2d:f0:82 753 | d7:a8:73:d3:a8:3b 754 | 8d:c3:24:4f:1f:55 755 | 53:dc:22:60:2f:98 756 | 12:66:2f:d9:32:e5 757 | 50:8e:21:05:cb:81 758 | f4:f2:06:7a:98:57 759 | 04:bd:d8:39:4a:40 760 | e0:e0:42:b6:7e:01 761 | e4:8f:2e:55:a1:67 762 | 95:26:ca:2d:85:09 763 | f8:9c:ef:bf:65:a0 764 | f2:23:42:cb:93:71 765 | c8:92:8a:6a:3c:ce 766 | 31:4d:3c:6f:e0:45 767 | 37:af:a5:c4:3c:fe 768 | 3b:f2:1c:59:d6:86 769 | 20:d6:25:9d:9b:21 770 | 5e:78:6b:e0:69:b7 771 | 63:7b:a3:5a:d6:d3 772 | d0:26:23:41:54:15 773 | 62:ea:e8:a9:9f:31 774 | 2d:47:9a:bf:d4:b0 775 | 45:89:da:4b:cd:cc 776 | 7b:e5:b2:70:5e:1a 777 | a5:70:50:30:d9:c0 778 | ee:78:7c:78:e2:2f 779 | 0f:06:ac:be:66:c0 780 | 94:e1:54:02:cc:90 781 | 86:c3:b8:39:cc:a4 782 | 23:24:b1:3c:51:28 783 | 92:00:c1:2d:1f:43 784 | 89:49:da:b1:6c:1e 785 | 36:95:c7:48:92:ce 786 | 30:0f:9a:fc:03:96 787 | 6e:83:39:97:f7:93 788 | 2c:59:4c:08:36:9b 789 | 02:a3:30:58:00:86 790 | 99:10:80:15:18:ec 791 | 6e:61:c2:ae:66:85 792 | dd:38:6e:2a:e0:7c 793 | 6b:0e:d0:cf:9f:c2 794 | fe:ff:c5:f6:58:8e 795 | f3:87:0d:e2:2f:d1 796 | 07:23:e0:0a:87:ba 797 | 2a:26:bd:00:d2:cc 798 | 9d:d6:f7:ff:19:9c 799 | 92:20:f1:ab:79:b9 800 | c0:82:5a:cf:c0:0b 801 | a9:e5:c0:da:5f:90 802 | 4b:f9:ab:6e:89:ec 803 | 36:22:c0:d4:2c:d6 804 | 20:0a:a4:96:9c:3b 805 | 2b:f3:b0:6a:50:a6 806 | 7e:f3:4a:b6:f0:27 807 | f2:8f:cf:b4:c7:4e 808 | fd:a2:c4:60:2c:6d 809 | 21:76:63:0a:5a:89 810 | fc:2c:f5:9e:40:d6 811 | 95:e5:c3:0e:2b:90 812 | 22:89:3c:b2:de:55 813 | 38:c3:9a:1b:6b:56 814 | b3:97:ad:5c:92:6a 815 | 65:f9:7c:98:86:c1 816 | e4:57:6f:69:f7:88 817 | 31:aa:71:8d:16:dc 818 | 28:14:1f:6c:8e:2c 819 | 2d:c7:26:f6:75:0b 820 | ef:f2:29:16:9a:dc 821 | 69:3c:31:ef:4c:a0 822 | e7:1d:b3:63:77:7b 823 | 3b:5b:9f:3a:b1:f8 824 | 3a:ef:67:2e:a1:a0 825 | db:ac:6f:16:fa:94 826 | 8a:b4:39:7c:68:d2 827 | 1d:e3:05:ca:ca:d8 828 | af:d7:d9:15:d1:5b 829 | a7:5c:2d:be:f9:6f 830 | 74:84:37:f7:8f:e9 831 | e2:26:5b:98:89:99 832 | 0f:db:99:ff:bd:88 833 | dd:0b:1b:ad:2f:4f 834 | e3:b7:92:63:11:a8 835 | 96:f3:85:26:b3:5c 836 | c3:45:b4:57:d7:b1 837 | a1:98:f7:8b:67:6b 838 | ae:ad:86:14:c3:3f 839 | 81:fb:2f:f5:e5:0d 840 | 08:ad:a7:e8:77:a0 841 | 00:79:6c:2d:37:0f 842 | 64:f3:c8:67:d3:74 843 | 78:14:84:0c:65:de 844 | 00:2a:a6:79:55:39 845 | 12:e3:2f:f9:9e:ea 846 | 50:c5:11:ce:03:b7 847 | 34:49:21:20:1b:f2 848 | 56:fe:13:9d:d9:17 849 | 98:a0:e5:ad:38:29 850 | a6:89:fd:79:a3:d0 851 | f9:59:cf:5a:ea:3b 852 | b1:60:f7:16:87:c4 853 | 8b:a4:1a:5c:6f:1c 854 | 5a:e8:10:6d:bd:ea 855 | c1:0a:dd:1a:cc:c9 856 | a2:06:8f:05:60:b3 857 | 90:3d:ae:a9:c2:9d 858 | 9b:b8:b0:55:77:f3 859 | 0f:64:4b:f8:12:a4 860 | 60:ee:3f:89:ca:30 861 | b6:8d:65:58:b9:b6 862 | 63:56:b1:c5:ce:7f 863 | 98:04:a6:d6:3e:d9 864 | c7:b8:8e:f8:6e:24 865 | ce:1b:c2:3d:9f:f2 866 | 1b:80:be:36:c2:5f 867 | 8c:54:2f:c9:26:fc 868 | 3b:9f:a5:9f:ad:24 869 | 0d:a9:f2:a7:a7:40 870 | 2c:52:4c:0a:9c:77 871 | b4:8b:be:60:18:ad 872 | 13:ed:c6:c7:73:db 873 | 74:6a:90:15:b9:1a 874 | b0:a5:28:71:29:33 875 | b5:f9:25:40:3e:18 876 | 2b:f0:2d:94:2f:4b 877 | 13:01:a1:54:01:71 878 | 39:4e:5b:c4:09:b2 879 | f3:97:e9:1d:75:fc 880 | 19:21:0a:a4:d7:3f 881 | 3e:57:b7:08:22:b0 882 | 0d:ac:9a:5c:0d:88 883 | 25:36:bd:3b:4c:2c 884 | be:8c:81:01:78:2c 885 | f4:a5:be:42:b4:c2 886 | cb:7b:c8:98:7d:5e 887 | 8a:b3:fb:22:ee:e8 888 | ba:e6:63:b7:4f:b3 889 | 85:8c:4f:9f:f9:b6 890 | 63:c4:ef:b9:c3:5c 891 | 9a:6b:f9:04:ca:e1 892 | d5:6f:57:cc:55:e8 893 | 01:d2:1f:b8:cf:9e 894 | 48:1c:92:18:e5:f0 895 | 46:54:10:82:9a:b3 896 | 09:03:87:50:e3:0a 897 | a7:0c:8b:8e:12:d8 898 | d2:42:52:38:86:4f 899 | ec:cb:4f:9b:d8:eb 900 | 15:18:3f:92:1b:87 901 | 64:6c:3d:68:ee:86 902 | b1:30:d7:9c:b2:20 903 | ab:ef:97:6a:6d:1f 904 | db:63:4b:5f:5e:4f 905 | 9f:77:3e:0a:70:b5 906 | f5:52:3f:76:8a:3c 907 | 28:4d:be:d5:21:0c 908 | 50:29:35:8b:6d:71 909 | 8b:0a:5e:3b:56:1f 910 | 6b:1b:f1:fe:29:47 911 | 97:0e:31:cd:49:23 912 | 81:15:43:30:72:e6 913 | fd:10:bb:63:bd:61 914 | f4:42:e4:25:53:6a 915 | 5e:5b:bf:2f:23:de 916 | 56:5f:cc:0f:7d:59 917 | d4:d9:2a:6d:1a:b6 918 | 11:70:91:0b:3c:06 919 | a5:05:47:c9:88:87 920 | e9:b0:86:9a:b0:31 921 | f0:d3:56:75:ee:0f 922 | af:af:3b:09:26:ca 923 | 5a:d8:2b:cd:99:92 924 | ee:3b:54:29:ae:47 925 | 34:36:59:0b:98:ba 926 | 68:3a:83:db:0c:ad 927 | fb:d8:ed:8a:9f:41 928 | 22:9c:f0:05:3e:d4 929 | e2:6f:9e:4f:58:50 930 | 31:10:f0:ef:23:c1 931 | 46:da:95:97:59:6c 932 | c5:d1:a3:35:7a:c6 933 | b4:b5:14:c0:cd:a0 934 | d0:ae:14:4e:9b:60 935 | ad:ef:c9:c8:ff:10 936 | 50:47:4b:2e:dc:4d 937 | d9:26:11:69:67:04 938 | 11:33:dd:9e:c9:cb 939 | c0:ad:9e:bf:cf:22 940 | ce:d6:85:2c:89:a4 941 | bf:c7:6d:d6:ef:a2 942 | 49:74:f7:3d:de:d3 943 | 34:40:29:ae:28:a9 944 | 2b:0f:21:98:af:2c 945 | aa:56:ce:25:e1:8c 946 | 79:8d:c3:6b:e5:b3 947 | ee:4b:37:c4:0d:4b 948 | 5e:ef:68:a3:71:d4 949 | 4c:b4:39:dd:37:48 950 | 17:a2:1a:21:4b:e0 951 | 38:65:d0:1d:32:14 952 | 2d:45:bb:c4:ec:0c 953 | 00:51:77:22:d4:92 954 | ae:41:bd:d2:5e:56 955 | 3b:18:97:e8:13:86 956 | bf:ec:4c:bc:aa:85 957 | b1:e0:45:97:2a:ca 958 | fd:7d:41:d0:a9:cd 959 | 6f:0d:2b:66:25:3a 960 | 8e:b5:e5:4e:96:32 961 | 81:f9:24:80:3a:28 962 | 63:fc:13:9d:7e:2e 963 | a2:a1:80:0b:84:40 964 | 40:8d:81:65:07:df 965 | aa:94:12:d4:61:c4 966 | c4:08:cc:6f:33:6f 967 | 6e:6f:33:7b:6f:57 968 | 84:b1:66:96:70:ba 969 | 0a:b2:71:b6:ae:e8 970 | f2:f8:8e:22:8e:f1 971 | c2:c1:a5:40:c6:7f 972 | 3f:82:93:2a:a1:02 973 | 91:2a:69:3b:a4:d5 974 | 5c:cc:24:3d:70:3c 975 | be:bf:cb:08:d2:79 976 | fc:bf:89:3e:26:cd 977 | 14:2c:6a:8d:e9:a9 978 | d0:69:92:de:1a:45 979 | ef:85:74:34:5a:7b 980 | 49:09:23:6b:b8:8d 981 | 4b:a6:7e:93:d4:1e 982 | a2:f5:3d:0b:9f:d2 983 | b5:5b:2b:5c:30:2a 984 | e5:38:35:7c:9a:a5 985 | 8d:e3:7f:a1:af:b8 986 | 9f:5b:cf:1a:4e:d3 987 | 95:78:33:55:15:40 988 | 62:30:aa:22:b2:12 989 | ec:23:54:5f:3c:a9 990 | d7:a7:99:d1:fc:d4 991 | 1c:93:77:b4:91:08 992 | b4:b0:b2:0b:4a:92 993 | 2a:4e:6b:26:e2:06 994 | 0e:b0:10:8d:57:8e 995 | 69:9c:d9:d8:3b:32 996 | 9b:b6:6b:5e:95:cb 997 | ec:c2:2e:88:1e:28 998 | ba:bc:ef:7f:ed:47 999 | f5:03:a2:0f:1a:e0 1000 | be:cf:20:bd:63:b9 1001 | -------------------------------------------------------------------------------- /setup.cfg: -------------------------------------------------------------------------------- 1 | [metadata] 2 | description_file = README.md 3 | 4 | [bdist_wheel] 5 | universal=1 6 | 7 | # See the docstring in versioneer.py for instructions. Note that you must 8 | # re-run 'versioneer.py setup' after changing this section, and commit the 9 | # resulting files. 10 | 11 | [versioneer] 12 | VCS = git 13 | style = pep440 14 | versionfile_source = pxgrid_util/_version.py 15 | tag_prefix = v 16 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | # 2 | # Copyright (c) 2021 Cisco and/or its affiliates 3 | # 4 | import os 5 | import sys 6 | import versioneer 7 | from setuptools import setup 8 | 9 | 10 | # 11 | # hack to provent incorrect shebang substitution 12 | # 13 | import re 14 | from distutils.command import build_scripts 15 | build_scripts.first_line_re = re.compile(b'^should not match$') 16 | 17 | 18 | __author__ = "Einar Nilsen-Nygaard" 19 | __author_email__ = "einarnn@cisco.com" 20 | __copyright__ = "Copyright (c) 2025 Cisco and/or its affiliates." 21 | __license__ = "Apache 2.0" 22 | 23 | 24 | if (sys.version_info.major == 2) or (sys.version_info.major == 3 and sys.version_info.minor < 8): 25 | print ("Sorry, Python < 3.8 is not supported") 26 | exit() 27 | 28 | 29 | def read(fname): 30 | return open(os.path.join(os.path.dirname(__file__), fname)).read() 31 | 32 | 33 | setup( 34 | name='pxgrid_util', 35 | version=versioneer.get_version(), 36 | cmdclass=versioneer.get_cmdclass(), 37 | description=('A utility library and example scripts for Cisco pxGrid 2.0'), 38 | long_description='A utility library and example scripts for Cisco pxGrid 2.0', 39 | packages = ['pxgrid_util'], 40 | scripts=[ 41 | 'bin/anc-policy', 42 | 'bin/create-new-pxgrid-account', 43 | 'bin/matrix-query-all', 44 | 'bin/profiles-query-all', 45 | 'bin/px-publish', 46 | 'bin/px-subscribe', 47 | 'bin/endpoint-query-all', 48 | 'bin/session-query-all', 49 | 'bin/session-query-by-ip', 50 | 'bin/sgacls-query-all', 51 | 'bin/sgts-query-all', 52 | 'bin/sxp-query-bindings', 53 | 'bin/system-query-all', 54 | 'bin/user-groups-query', 55 | ], 56 | author=__author__, 57 | author_email=__author_email__, 58 | license=__license__ + "; " + __copyright__, 59 | url='https://github.com/cisco-pxgrid/python-advanced-examples', 60 | download_url='https://github.com/cisco-pxgrid/python-advanced-examples', 61 | install_requires=[ 62 | 'websockets>=15.0.1', 63 | 'aiohttp>=3.8.5', 64 | ], 65 | include_package_data=True, 66 | platforms=["Posix; OS X; Windows"], 67 | keywords=['ISE', 'pxGrid'], 68 | python_requires='>=3.8', 69 | classifiers=[ 70 | 'Development Status :: 4 - Beta', 71 | 'Intended Audience :: Developers', 72 | 'Natural Language :: English', 73 | 'License :: OSI Approved :: Apache Software License', 74 | 'Programming Language :: Python :: 3.8', 75 | 'Programming Language :: Python :: 3.9', 76 | ], 77 | ) 78 | --------------------------------------------------------------------------------