├── .gitignore ├── LICENSE.md ├── README.md ├── SECURITY.md ├── ansible-tasks ├── collect-proxmox-lxc.yml └── collect-proxmox-vm.yml ├── awx-proxmox-add-vm-disk.yml ├── awx-proxmox-clone-lxc-and-set-resources.yml ├── awx-proxmox-clone-vm-and-set-resources.yml ├── awx-proxmox-remove-lxc.yml ├── awx-proxmox-remove-vm-disk.yml ├── awx-proxmox-remove-vm.yml ├── awx-proxmox-resize-lxc-disk.yml ├── awx-proxmox-resize-vm-disk.yml ├── awx-proxmox-set-ipconfig0.yml ├── awx-proxmox-set-netif.yml ├── awx-proxmox-start-lxc.yml ├── awx-proxmox-start-vm.yml ├── awx-proxmox-stop-lxc.yml ├── awx-proxmox-stop-vm.yml ├── awx-update-dns.yml ├── conf.d └── netbox_setup_objects.yml-sample ├── docs ├── configure-awx-aap.md ├── configure-flask-application.md ├── images │ ├── awx-ansible-build-ee.png │ ├── awx-create-project.png │ ├── awx-default-inventory.png │ ├── awx-execution-environment-tree.png │ ├── awx-execution-environment-yml.png │ ├── awx-execution-environment.png │ ├── awx-netbox-proxmox-credentials.png │ ├── awx-netbox-proxmox-creds.png │ ├── awx-netbox-proxmox-template-clone-resources-edit.png │ ├── awx-proxmox-clone-vm-and-set-resources.png │ ├── awx-scm-credential-new.png │ ├── netbox-awx-webhooks.png │ ├── netbox-favicon.png │ ├── netbox-light-favicon.png │ ├── netbox-proxmox-event-rules-awx.png │ ├── netbox-proxmox-flask-app-webhook.png │ ├── netbox-proxmox-vm-create-and-set-resources-awx-webhook.png │ ├── proxmox-cluster-nodes-edit.png │ ├── proxmox-cluster-nodes-saved.png │ ├── proxmox-disk-storage-volume.png │ ├── proxmox-node.png │ ├── proxmox-public-ssh-key.png │ ├── proxmox-remove-vm-awx.png │ ├── proxmox-resize-vm-disk-awx-event-rule.png │ ├── proxmox-set-ip-config-and-ssh-key-awx-event-rule.png │ ├── proxmox-start-vm-awx.png │ ├── proxmox-stop-vm-awx.png │ ├── proxmox-vm-active-awx-event-rule.png │ ├── proxmox-vm-add-disk-awx-event-rule.png │ ├── proxmox-vm-add-disk-awx.png │ ├── proxmox-vm-add-disk.png │ ├── proxmox-vm-assign-ip-address-awx.png │ ├── proxmox-vm-configure-ipconfig0-and-ssh-key-awx.png │ ├── proxmox-vm-create-and-set-resources-awx-event-rule.png │ ├── proxmox-vm-created.png │ ├── proxmox-vm-delete-disk.png │ ├── proxmox-vm-deleted.png │ ├── proxmox-vm-offline-awx-event-rule.png │ ├── proxmox-vm-remove-awx-event-rule.png │ ├── proxmox-vm-remove-disk-awx-event-rule.png │ ├── proxmox-vm-remove-disk-awx.png │ ├── proxmox-vm-resize-disk-awx.png │ ├── proxmox-vm-resize-disk.png │ ├── proxmox-vm-resize-os-disk-awx-event-rule.png │ ├── proxmox-vm-started.png │ ├── proxmox-vm-stopped.png │ ├── proxmox-vm-storage-edit.png │ ├── proxmox-vm-storage-saved.png │ ├── proxmox-vm-storage.png │ ├── proxmox-vm-template.png │ ├── proxmox-vm-templates-edit.png │ ├── proxmox-vm-templates-saved.png │ ├── proxmox-vm-update-network-config.png │ ├── proxmox-vm-updated.png │ └── proxmox-vmid.png ├── index.md ├── netbox-customization.md ├── netbox-event-rules-and-webhooks-awx-aap.md ├── netbox-event-rules-and-webhooks-flask.md ├── netbox-ipam.md ├── netbox-key-and-permissions.md ├── proxmox-api-user-and-key.md ├── proxmox-discover-vm-and-lxc.md ├── proxmox-lxc-templates.md ├── proxmox-vm-templates.md ├── stylesheets │ └── extra.css └── usage.md ├── legacy ├── README.TXT ├── netbox-proxmox-discover-vms.yml ├── setup-netbox-custom-vm-objects.yml └── setup-netbox-objects.yml ├── mkdocs.yml ├── netbox-event-driven-automation-flask-app ├── app.py ├── app_config.yml-sample ├── helpers │ └── netbox_proxmox.py └── requirements.txt ├── requirements.txt ├── setup ├── configure_ansible_automation.py ├── helpers │ ├── ansible_automation_awx.py │ ├── ansible_automation_awx_manager.py │ ├── netbox_objects.py │ └── netbox_proxmox_api.py ├── netbox-discovery-tool.py ├── netbox_setup_objects_and_custom_fields.py ├── netbox_setup_webhook_and_event_rules.py └── requirements.txt └── templates └── bind9 └── zone-template.j2 /.gitignore: -------------------------------------------------------------------------------- 1 | # Byte-compiled / optimized / DLL files 2 | __pycache__/ 3 | *.py[cod] 4 | *$py.class 5 | 6 | # C extensions 7 | *.so 8 | 9 | # Distribution / packaging 10 | .Python 11 | build/ 12 | develop-eggs/ 13 | dist/ 14 | downloads/ 15 | eggs/ 16 | .eggs/ 17 | lib/ 18 | lib64/ 19 | parts/ 20 | sdist/ 21 | var/ 22 | wheels/ 23 | share/python-wheels/ 24 | *.egg-info/ 25 | .installed.cfg 26 | *.egg 27 | MANIFEST 28 | 29 | # PyInstaller 30 | # Usually these files are written by a python script from a template 31 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 32 | *.manifest 33 | *.spec 34 | 35 | # Installer logs 36 | pip-log.txt 37 | pip-delete-this-directory.txt 38 | 39 | # Unit test / coverage reports 40 | htmlcov/ 41 | .tox/ 42 | .nox/ 43 | .coverage 44 | .coverage.* 45 | .cache 46 | nosetests.xml 47 | coverage.xml 48 | *.cover 49 | *.py,cover 50 | .hypothesis/ 51 | .pytest_cache/ 52 | cover/ 53 | 54 | # Translations 55 | *.mo 56 | *.pot 57 | 58 | # Django stuff: 59 | *.log 60 | local_settings.py 61 | db.sqlite3 62 | db.sqlite3-journal 63 | 64 | # Flask stuff: 65 | instance/ 66 | .webassets-cache 67 | 68 | # Scrapy stuff: 69 | .scrapy 70 | 71 | # Sphinx documentation 72 | docs/_build/ 73 | 74 | # PyBuilder 75 | .pybuilder/ 76 | target/ 77 | 78 | # Jupyter Notebook 79 | .ipynb_checkpoints 80 | 81 | # IPython 82 | profile_default/ 83 | ipython_config.py 84 | 85 | # pyenv 86 | # For a library or package, you might want to ignore these files since the code is 87 | # intended to run in multiple environments; otherwise, check them in: 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 | # poetry 98 | # Similar to Pipfile.lock, it is generally recommended to include poetry.lock in version control. 99 | # This is especially recommended for binary packages to ensure reproducibility, and is more 100 | # commonly ignored for libraries. 101 | # https://python-poetry.org/docs/basic-usage/#commit-your-poetrylock-file-to-version-control 102 | #poetry.lock 103 | 104 | # pdm 105 | # Similar to Pipfile.lock, it is generally recommended to include pdm.lock in version control. 106 | #pdm.lock 107 | # pdm stores project-wide configurations in .pdm.toml, but it is recommended to not include it 108 | # in version control. 109 | # https://pdm.fming.dev/latest/usage/project/#working-with-version-control 110 | .pdm.toml 111 | .pdm-python 112 | .pdm-build/ 113 | 114 | # PEP 582; used by e.g. github.com/David-OConnor/pyflow and github.com/pdm-project/pdm 115 | __pypackages__/ 116 | 117 | # Celery stuff 118 | celerybeat-schedule 119 | celerybeat.pid 120 | 121 | # SageMath parsed files 122 | *.sage.py 123 | 124 | # Environments 125 | .env 126 | .venv 127 | env/ 128 | venv/ 129 | ENV/ 130 | env.bak/ 131 | venv.bak/ 132 | 133 | # Spyder project settings 134 | .spyderproject 135 | .spyproject 136 | 137 | # Rope project settings 138 | .ropeproject 139 | 140 | # mkdocs documentation 141 | /site 142 | 143 | # mypy 144 | .mypy_cache/ 145 | .dmypy.json 146 | dmypy.json 147 | 148 | # Pyre type checker 149 | .pyre/ 150 | 151 | # pytype static type analyzer 152 | .pytype/ 153 | 154 | # Cython debug symbols 155 | cython_debug/ 156 | 157 | # PyCharm 158 | # JetBrains specific template is maintained in a separate JetBrains.gitignore that can 159 | # be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore 160 | # and can be added to the global gitignore or merged into this file. For a more nuclear 161 | # option (not recommended) you can uncomment the following to ignore the entire idea folder. 162 | #.idea/ 163 | 164 | # MacOS stuff 165 | .DS_Store 166 | 167 | venv/ 168 | secrets.yml 169 | vms.yml 170 | vms.yml.* 171 | 172 | inventory 173 | tests/ 174 | 175 | app_config.yml 176 | netbox_setup_custom_fields.yml 177 | netbox_importer.py* 178 | netbox_setup_objects.yml 179 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 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 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # netbox-proxmox-automation 2 | 3 | ## Clone and step into the repo 4 | 5 | ``` 6 | # git clone https://github.com/netboxlabs/netbox-proxmox-automation/ 7 | # cd netbox-proxmox-automation 8 | ``` 9 | 10 | ## Install required python packages 11 | 12 | ``` 13 | # python3 -m venv venv 14 | # source venv/bin/activate 15 | (venv) # pip install -r requirements.txt 16 | ``` 17 | 18 | ## Run mkdocs 19 | 20 | ``` 21 | (venv) # mkdocs serve 22 | INFO - Building documentation... 23 | INFO - Cleaning site directory 24 | INFO - The following pages exist in the docs directory, but are not included in the "nav" configuration: 25 | - Administration Console/console-overview.md 26 | - NetBox Cloud/getting-started-with-nbc.md 27 | INFO - Documentation built in 0.75 seconds 28 | INFO - [13:37:39] Watching paths for changes: 'docs', 'mkdocs.yml' 29 | INFO - [13:37:39] Serving on http://127.0.0.1:8000/ 30 | ``` 31 | 32 | ## :warning: 33 | 34 | If you see errors like this... 35 | 36 | > ERROR - Config value 'theme': Unrecognised theme name: 'material'. The available installed themes are: mkdocs, readthedocs 37 | > ERROR - Config value 'markdown_extensions': Failed to load extension 'pymdownx.tabbed'. 38 | > ModuleNotFoundError: No module named 'pymdownx' 39 | 40 | Try uninstalling `mkdocs` from your package manager, (e.g. `brew uninstall mkdocs`) and just using the version installed by `pip`. It seems that `mkdocs` doesn't like it when you've installed it using different methods. 41 | 42 | # What's New in 1.2.0 43 | - Adds LXC support 44 | - Adds the ability to define 'vmid' for both VM and LXC, rather than taking a default value from Proxmox 45 | - Adds "discovery" of Proxmox VM and LXC disks and auto-creation of VM disk objects in NetBox 46 | - Adds rudimentary Proxmox VM and LXC discovery through convenience script 47 | - Adds AWX initial setup through convenience script (uses awxkit) 48 | - Convenience script changes to accommodate LXC requirements 49 | - Can dynamically build webhooks and event rules from current AWX state through convenience script 50 | - Adds customization changes for LXC-specific requirements 51 | 52 | # Developers 53 | - Nate Patwardhan <npatwardhan@netboxlabs.com> 54 | 55 | # Known Issues / Roadmap 56 | 57 | ## Known Issues 58 | - *Only* supports SCSI disk types (this is possibly fine as Proxmox predomininantly provisions disks as SCSI) 59 | - Does not currently support Proxmox VM creation to a Proxmox cluster, but is only node-based 60 | 61 | ## Roadmap -- Delivery 62 | - Integration with NetBox Discovery/Assurance 63 | - DNS update support (requires NetBox `netbox-dns` plugin) 64 | - Maybe evolve into to a NetBox plugin for Proxmox 65 | -------------------------------------------------------------------------------- /SECURITY.md: -------------------------------------------------------------------------------- 1 | # Security Policy 2 | 3 | ## Reporting a Vulnerability 4 | 5 | Please send any suspected vulnerability report to security@netboxlabs.com 6 | -------------------------------------------------------------------------------- /ansible-tasks/collect-proxmox-lxc.yml: -------------------------------------------------------------------------------- 1 | - name: Discover all existing virtual machines on node 2 | community.general.proxmox_vm_info: 3 | api_host: "{{ proxmox_env_info.api_host | default(omit) }}" 4 | api_user: "{{ proxmox_env_info.api_user | default(omit) }}" 5 | api_token_id: "{{ proxmox_env_info.api_token_id | default(omit) }}" 6 | api_token_secret: "{{ proxmox_env_info.api_token_secret | default(omit) }}" 7 | node: "{{ proxmox_env_info.node }}" 8 | register: proxmox_discovered_lxc 9 | 10 | - name: Discover / Collect Proxmox LXC 11 | set_fact: 12 | collected_proxmox_lxc: "{{ collected_proxmox_lxc | combine({item.name: item.vmid}) }}" 13 | loop: "{{ proxmox_discovered_lxc.proxmox_vms }}" 14 | when: not item.template and item.type == 'lxc' 15 | 16 | -------------------------------------------------------------------------------- /ansible-tasks/collect-proxmox-vm.yml: -------------------------------------------------------------------------------- 1 | - name: Discover all existing virtual machines on node 2 | community.general.proxmox_vm_info: 3 | api_host: "{{ proxmox_env_info.api_host | default(omit) }}" 4 | api_user: "{{ proxmox_env_info.api_user | default(omit) }}" 5 | api_token_id: "{{ proxmox_env_info.api_token_id | default(omit) }}" 6 | api_token_secret: "{{ proxmox_env_info.api_token_secret | default(omit) }}" 7 | node: "{{ proxmox_env_info.node }}" 8 | register: proxmox_discovered_vm 9 | 10 | - name: Discover / Collect Proxmox VMs 11 | set_fact: 12 | collected_proxmox_vm: "{{ collected_proxmox_vm | combine({item.name: item.vmid}) }}" 13 | loop: "{{ proxmox_discovered_vm.proxmox_vms }}" 14 | when: not item.template and item.type == 'qemu' 15 | 16 | -------------------------------------------------------------------------------- /awx-proxmox-add-vm-disk.yml: -------------------------------------------------------------------------------- 1 | - name: "Proxmox: add VM disk" 2 | connection: local 3 | hosts: all 4 | gather_facts: False 5 | 6 | vars: 7 | collected_proxmox_vm: {} 8 | collected_proxmox_lxc: {} 9 | 10 | tasks: 11 | - name: Include Proxmox VM discovery 12 | include_tasks: "ansible-tasks/collect-proxmox-vm.yml" 13 | 14 | - name: Include Proxmox LXC discovery 15 | include_tasks: "ansible-tasks/collect-proxmox-lxc.yml" 16 | 17 | - name: Add VM disk 18 | community.general.proxmox_disk: 19 | api_user: "{{ proxmox_env_info.api_user }}" 20 | api_token_id: "{{ proxmox_env_info.api_token_id }}" 21 | api_token_secret: "{{ proxmox_env_info.api_token_secret }}" 22 | api_host: "{{ proxmox_env_info.api_host }}" 23 | name: "{{ vm_config['name'] }}" 24 | disk: "{{ vm_config['add_disk'] }}" 25 | size: "{{ (vm_config['add_disk_size']|int / 1024) | round | int }}" 26 | storage: "{{ vm_config['storage_volume'] }}" 27 | backup: false 28 | ssd: false 29 | create: regular 30 | state: present 31 | when: vm_config['name'] in collected_proxmox_vm 32 | register: vm_add_disk_job 33 | async: 180 34 | poll: 0 35 | 36 | - name: Wait for VM disk add process to finish 37 | async_status: 38 | jid: "{{ vm_add_disk_job.ansible_job_id }}" 39 | when: vm_config['name'] in collected_proxmox_vm 40 | register: _ujobs_alias_vc_0 41 | until: _ujobs_alias_vc_0.finished 42 | retries: 60 43 | delay: 10 44 | 45 | -------------------------------------------------------------------------------- /awx-proxmox-clone-lxc-and-set-resources.yml: -------------------------------------------------------------------------------- 1 | - name: "Proxmox: Clone LXC and set resources" 2 | connection: local 3 | hosts: all 4 | gather_facts: False 5 | 6 | vars: 7 | collected_proxmox_vm: {} 8 | collected_proxmox_lxc: {} 9 | 10 | tasks: 11 | - name: Include Proxmox VM discovery 12 | include_tasks: "ansible-tasks/collect-proxmox-vm.yml" 13 | 14 | - name: Include Proxmox LXC discovery 15 | include_tasks: "ansible-tasks/collect-proxmox-lxc.yml" 16 | 17 | - name: SHOW US vmconfig 18 | debug: 19 | msg: "vm_config {{ vm_config }}" 20 | 21 | - name: Show us NetBox config 22 | debug: 23 | msg: "NetBox config {{ netbox_env_info }}" 24 | 25 | - name: Show us Proxmox config 26 | debug: 27 | msg: "Proxmox config {{ proxmox_env_info }}" 28 | 29 | - name: Show us id of template (integer) 30 | debug: 31 | msg: "ID of template (integer) {{ vm_config['template'] | int }}" 32 | 33 | - name: Create LXC (Container) from a template 34 | community.general.proxmox: 35 | node: "{{ proxmox_env_info.node }}" 36 | api_user: "{{ proxmox_env_info.api_user }}" 37 | api_token_id: "{{ proxmox_env_info.api_token_id }}" 38 | api_token_secret: "{{ proxmox_env_info.api_token_secret }}" 39 | api_host: "{{ proxmox_env_info.api_host }}" 40 | api_port: "{{ proxmox_env_info.api_port }}" 41 | ostemplate: "{{ vm_config ['template'] }}" 42 | #vmid: "{{ vm_config['template'] }}" 43 | vmid: "{{ vm_config['vmid'] | int | default(omit) }}" 44 | hostname: "{{ vm_config['hostname'] }}" 45 | cores: "{{ vm_config['cpus'] | int | default(1) }}" 46 | cpus: "{{ vm_config['cpus'] | int | default(1) }}" 47 | memory: "{{ vm_config['memory'] | default(512) }}" 48 | pubkey: "{{ vm_config['pubkey']}}" 49 | clone_type: "full" 50 | storage: "{{ vm_config['storage'] }}" 51 | #onboot: "{{ vm_config_create[item].auto_start }}" 52 | unprivileged: true 53 | state: present 54 | timeout: 120 55 | when: not vm_config['hostname'] in collected_proxmox_lxc and not vm_config['hostname'] in collected_proxmox_vm 56 | register: create_lxc_job 57 | async: 180 58 | poll: 0 59 | 60 | - name: Wait for LXC cloning processes to finish 61 | async_status: 62 | jid: "{{ create_lxc_job.ansible_job_id }}" 63 | register: _jobs_alias_vc_0 64 | until: _jobs_alias_vc_0.finished 65 | retries: 60 66 | delay: 5 67 | when: not vm_config['hostname'] in collected_proxmox_lxc 68 | 69 | - name: Show us LXC settings 70 | debug: 71 | msg: "Show LXC settings: {{ create_lxc_job }}" 72 | 73 | - name: Include Proxmox LXC discovery post-provisioning 74 | include_tasks: "ansible-tasks/collect-proxmox-lxc.yml" 75 | 76 | - name: "Discover information for newly-created Proxmox LXC" 77 | community.general.proxmox_vm_info: 78 | api_host: "{{ proxmox_env_info.api_host | default(omit) }}" 79 | api_user: "{{ proxmox_env_info.api_user | default(omit) }}" 80 | api_token_id: "{{ proxmox_env_info.api_token_id | default(omit) }}" 81 | api_token_secret: "{{ proxmox_env_info.api_token_secret | default(omit) }}" 82 | node: "{{ proxmox_env_info.node }}" 83 | name: "{{ vm_config['hostname'] }}" 84 | type: lxc 85 | config: current 86 | register: new_proxmox_lxc 87 | 88 | - name: "Newly-created Proxmox LXC information" 89 | debug: 90 | msg: "Newly-created Proxmox LXC information: {{ new_proxmox_lxc}}" 91 | 92 | - name: "Update Proxmox vmid for LXC in NetBox" 93 | netbox.netbox.netbox_virtual_machine: 94 | netbox_url: "{{ netbox_env_info.api_proto }}://{{ netbox_env_info.api_host }}:{{ netbox_env_info.api_port}}" 95 | netbox_token: "{{ netbox_env_info.api_token }}" 96 | data: 97 | name: "{{ vm_config['hostname'] }}" 98 | custom_fields: 99 | proxmox_vmid: "{{ new_proxmox_lxc.proxmox_vms[0].vmid }}" 100 | state: present 101 | when: new_proxmox_lxc is defined and new_proxmox_lxc.proxmox_vms is defined and new_proxmox_lxc.proxmox_vms | length > 0 and vm_config['vmid'] is defined and vm_config['vmid'] == "None" 102 | 103 | - name: "Update Proxmox storage for LXC in NetBox" 104 | netbox.netbox.netbox_virtual_disk: 105 | netbox_url: "{{ netbox_env_info.api_proto }}://{{ netbox_env_info.api_host }}:{{ netbox_env_info.api_port}}" 106 | netbox_token: "{{ netbox_env_info.api_token }}" 107 | data: 108 | virtual_machine: "{{ new_proxmox_lxc.proxmox_vms[0].name }}" 109 | name: rootfs 110 | size: "{{ new_proxmox_lxc.proxmox_vms[0].maxdisk | human_readable(unit='M') | regex_replace('\\s+[M][Bb]$', '') | int | string }}" 111 | custom_fields: 112 | proxmox_disk_storage_volume: "{{ vm_config['storage'] }}" 113 | state: present 114 | when: new_proxmox_lxc is defined and new_proxmox_lxc.proxmox_vms is defined and new_proxmox_lxc.proxmox_vms | length > 0 115 | -------------------------------------------------------------------------------- /awx-proxmox-clone-vm-and-set-resources.yml: -------------------------------------------------------------------------------- 1 | - name: "Proxmox: Clone VM and set VM resources" 2 | connection: local 3 | hosts: all 4 | gather_facts: False 5 | 6 | vars: 7 | collected_proxmox_vm: {} 8 | collected_proxmox_lxc: {} 9 | 10 | tasks: 11 | - name: Include Proxmox VM discovery 12 | include_tasks: "ansible-tasks/collect-proxmox-vm.yml" 13 | 14 | - name: Include Proxmox LXC discovery 15 | include_tasks: "ansible-tasks/collect-proxmox-lxc.yml" 16 | 17 | - name: SHOW US vmconfig 18 | debug: 19 | msg: "vm_config {{ vm_config }}" 20 | 21 | - name: Show us NetBox config 22 | debug: 23 | msg: "NetBox config {{ netbox_env_info }}" 24 | 25 | - name: Show us Proxmox config 26 | debug: 27 | msg: "Proxmox config {{ proxmox_env_info }}" 28 | 29 | - name: Show us id of template (integer) 30 | debug: 31 | msg: "ID of template (integer) {{ vm_config['template'] | int }}" 32 | 33 | - name: Create VM from a full clone of template 34 | community.general.proxmox_kvm: 35 | node: "{{ proxmox_env_info.node }}" 36 | api_user: "{{ proxmox_env_info.api_user }}" 37 | api_token_id: "{{ proxmox_env_info.api_token_id }}" 38 | api_token_secret: "{{ proxmox_env_info.api_token_secret }}" 39 | api_host: "{{ proxmox_env_info.api_host }}" 40 | api_port: "{{ proxmox_env_info.api_port }}" 41 | clone: "{{ vm_config['template'] }}" 42 | vmid: "{{ vm_config['template'] }}" 43 | name: "{{ vm_config['name'] }}" 44 | newid: "{{ vm_config['vmid'] | int | default(omit) }}" 45 | agent: 'enabled=1' 46 | storage: "{{ vm_config['storage'] }}" 47 | #onboot: "{{ vm_config_create[item].auto_start }}" 48 | state: present 49 | timeout: 120 50 | when: not vm_config['name'] in collected_proxmox_vm and not vm_config['name'] in collected_proxmox_lxc 51 | register: clone_vm_job 52 | async: 180 53 | poll: 0 54 | 55 | - name: Wait for VM cloning processes to finish 56 | async_status: 57 | jid: "{{ clone_vm_job.ansible_job_id }}" 58 | register: _jobs_alias_vc_0 59 | until: _jobs_alias_vc_0.finished 60 | retries: 60 61 | delay: 5 62 | when: not vm_config['name'] in collected_proxmox_vm 63 | 64 | - name: Update Proxmox VMs resource settings 65 | community.general.proxmox_kvm: 66 | node: "{{ proxmox_env_info.node }}" 67 | api_user: "{{ proxmox_env_info.api_user }}" 68 | api_token_id: "{{ proxmox_env_info.api_token_id }}" 69 | api_token_secret: "{{ proxmox_env_info.api_token_secret }}" 70 | api_host: "{{ proxmox_env_info.api_host }}" 71 | name: "{{ vm_config['name'] }}" 72 | cores: "{{ vm_config['vcpus'] | int }}" 73 | vcpus: "{{ vm_config['vcpus'] | int }}" 74 | memory: "{{ vm_config['memory'] }}" 75 | update: true 76 | register: update_vms_settings_job 77 | async: 180 78 | poll: 0 79 | 80 | - name: Wait for VM settings updates processes to finish 81 | async_status: 82 | jid: "{{ update_vms_settings_job.ansible_job_id }}" 83 | register: _ujobs_alias_vc_0 84 | until: _ujobs_alias_vc_0.finished 85 | retries: 60 86 | delay: 10 87 | 88 | - name: "Discover information for newly-created Proxmox VM" 89 | community.general.proxmox_vm_info: 90 | api_host: "{{ proxmox_env_info.api_host | default(omit) }}" 91 | api_user: "{{ proxmox_env_info.api_user | default(omit) }}" 92 | api_token_id: "{{ proxmox_env_info.api_token_id | default(omit) }}" 93 | api_token_secret: "{{ proxmox_env_info.api_token_secret | default(omit) }}" 94 | node: "{{ proxmox_env_info.node }}" 95 | name: "{{ vm_config['name'] }}" 96 | register: new_proxmox_vm 97 | 98 | - name: Show us new VM info 99 | debug: 100 | msg: "New VM info: {{ new_proxmox_vm }}" 101 | 102 | - name: "Update Proxmox vmid for VM in NetBox" 103 | netbox.netbox.netbox_virtual_machine: 104 | netbox_url: "{{ netbox_env_info.api_proto }}://{{ netbox_env_info.api_host }}:{{ netbox_env_info.api_port}}" 105 | netbox_token: "{{ netbox_env_info.api_token }}" 106 | data: 107 | name: "{{ new_proxmox_vm.proxmox_vms[0].name }}" 108 | custom_fields: 109 | proxmox_vmid: "{{ new_proxmox_vm.proxmox_vms[0].vmid }}" 110 | state: present 111 | when: new_proxmox_vm is defined and new_proxmox_vm.proxmox_vms is defined and new_proxmox_vm.proxmox_vms | length > 0 and vm_config['vmid'] is defined and vm_config['vmid'] == "None" 112 | 113 | # ok: [localhost] => { 114 | # "msg": "New VM info: {'changed': False, 'proxmox_vms': [{'maxcpu': 1, 'status': 'stopped', 'netout': 0, 'vmid': 104, 'diskwrite': 0, 'id': 'qemu/104', 'uptime': 0, 'node': 'proxmox-ve', 'disk': 0, 'maxdisk': 2361393152, 'type': 'qemu', 'maxmem': 268435456, 'template': False, 'mem': 0, 'diskread': 0, 'netin': 0, 'cpu': 0, 'name': 'test2', 'cpus': 1, 'serial': 1}], 'failed': False}" 115 | #} 116 | # - name: Show human-readable disk size from newly-created VM 117 | # debug: 118 | # msg: "human readable disk size: {{ new_proxmox_vm.proxmox_vms[0].maxdisk | human_readable(unit='M') }} ||| {{ new_proxmox_vm.proxmox_vms[0].maxdisk | human_readable(unit='M') | regex_replace('\\s+[M][Bb]$', '') | int | string }} ||| {{ new_proxmox_vm.proxmox_vms[0].maxdisk | human_readable(unit='M') | regex_replace('\\s+[M][Bb]$', '') | int | string | type_debug }}" 119 | 120 | - name: "Update Proxmox storage for VM in NetBox" 121 | netbox.netbox.netbox_virtual_disk: 122 | netbox_url: "{{ netbox_env_info.api_proto }}://{{ netbox_env_info.api_host }}:{{ netbox_env_info.api_port}}" 123 | netbox_token: "{{ netbox_env_info.api_token }}" 124 | data: 125 | virtual_machine: "{{ new_proxmox_vm.proxmox_vms[0].name }}" 126 | name: scsi0 127 | size: "{{ new_proxmox_vm.proxmox_vms[0].maxdisk | human_readable(unit='M') | regex_replace('\\s+[M][Bb]$', '') | int | string }}" 128 | custom_fields: 129 | proxmox_disk_storage_volume: "{{ vm_config['storage'] }}" 130 | state: present 131 | when: new_proxmox_vm is defined and new_proxmox_vm.proxmox_vms is defined and new_proxmox_vm.proxmox_vms | length > 0 132 | -------------------------------------------------------------------------------- /awx-proxmox-remove-lxc.yml: -------------------------------------------------------------------------------- 1 | - name: "Proxmox: LXC Removal" 2 | connection: local 3 | hosts: all 4 | gather_facts: False 5 | 6 | vars: 7 | collected_proxmox_vm: {} 8 | collected_proxmox_lxc: {} 9 | 10 | tasks: 11 | - name: Include Proxmox VM discovery 12 | include_tasks: "ansible-tasks/collect-proxmox-vm.yml" 13 | 14 | - name: Include Proxmox LXC discovery 15 | include_tasks: "ansible-tasks/collect-proxmox-lxc.yml" 16 | 17 | - name: Stop specified LXC 18 | community.general.proxmox: 19 | #node: "{{ proxmox_env_info.node }}" 20 | api_user: "{{ proxmox_env_info.api_user }}" 21 | api_token_id: "{{ proxmox_env_info.api_token_id }}" 22 | api_token_secret: "{{ proxmox_env_info.api_token_secret }}" 23 | api_host: "{{ proxmox_env_info.api_host }}" 24 | #name: "{{ vm_config['name'] }}" 25 | vmid: "{{ vm_config['vmid'] }}" 26 | state: stopped 27 | force: true 28 | when: (vm_config['hostname'] in collected_proxmox_lxc) and (collected_proxmox_lxc[vm_config['hostname']] | int == vm_config['vmid'] | int) 29 | register: stop_vm_job 30 | async: 180 31 | poll: 0 32 | 33 | - name: Wait for LXC stop process to finish 34 | async_status: 35 | jid: "{{ stop_vm_job.ansible_job_id }}" 36 | when: vm_config['hostname'] in collected_proxmox_lxc 37 | register: _sjobs_alias_vc_0 38 | until: _sjobs_alias_vc_0.finished 39 | retries: 100 40 | delay: 10 41 | 42 | - name: "Proxmox: Remove specified LXC" 43 | community.general.proxmox: 44 | node: "{{ proxmox_env_info.node }}" 45 | api_user: "{{ proxmox_env_info.api_user }}" 46 | api_token_id: "{{ proxmox_env_info.api_token_id }}" 47 | api_token_secret: "{{ proxmox_env_info.api_token_secret }}" 48 | api_host: "{{ proxmox_env_info.api_host }}" 49 | vmid: "{{ vm_config['vmid'] }}" 50 | #vmid: "{{ nb_remove_vms[item] }}" 51 | state: absent 52 | force: true 53 | when: (vm_config['hostname'] in collected_proxmox_lxc) and (collected_proxmox_lxc[vm_config['hostname']] | int == vm_config['vmid'] | int) 54 | register: remove_vms_job 55 | async: 180 56 | poll: 0 57 | 58 | - name: "Proxmox: Wait for LXC removal" 59 | async_status: 60 | jid: "{{ remove_vms_job.ansible_job_id }}" 61 | when: vm_config['hostname'] in collected_proxmox_lxc 62 | register: _rjobs_alias_vc_0 63 | until: _rjobs_alias_vc_0.finished 64 | retries: 100 65 | delay: 5 66 | 67 | -------------------------------------------------------------------------------- /awx-proxmox-remove-vm-disk.yml: -------------------------------------------------------------------------------- 1 | - name: "Proxmox: remove VM disk" 2 | connection: local 3 | hosts: all 4 | gather_facts: False 5 | 6 | vars: 7 | collected_proxmox_vm: {} 8 | collected_proxmox_lxc: {} 9 | 10 | tasks: 11 | - name: Include Proxmox VM discovery 12 | include_tasks: "ansible-tasks/collect-proxmox-vm.yml" 13 | 14 | - name: Include Proxmox LXC discovery 15 | include_tasks: "ansible-tasks/collect-proxmox-lxc.yml" 16 | 17 | - name: Remove VM disk 18 | community.general.proxmox_disk: 19 | api_user: "{{ proxmox_env_info.api_user }}" 20 | api_token_id: "{{ proxmox_env_info.api_token_id }}" 21 | api_token_secret: "{{ proxmox_env_info.api_token_secret }}" 22 | api_host: "{{ proxmox_env_info.api_host }}" 23 | name: "{{ vm_config['name'] }}" 24 | disk: "{{ vm_config['remove_disk'] }}" 25 | state: absent 26 | when: vm_config['name'] in collected_proxmox_vm 27 | register: vm_remove_disk_job 28 | async: 180 29 | poll: 0 30 | 31 | - name: Wait for VM disk removal process to finish 32 | async_status: 33 | jid: "{{ vm_remove_disk_job.ansible_job_id }}" 34 | when: vm_config['name'] in collected_proxmox_vm 35 | register: _ujobs_alias_vc_0 36 | until: _ujobs_alias_vc_0.finished 37 | retries: 60 38 | delay: 10 39 | 40 | -------------------------------------------------------------------------------- /awx-proxmox-remove-vm.yml: -------------------------------------------------------------------------------- 1 | - name: "Proxmox: VM Removal" 2 | connection: local 3 | hosts: all 4 | gather_facts: False 5 | 6 | vars: 7 | collected_proxmox_vm: {} 8 | collected_proxmox_lxc: {} 9 | 10 | tasks: 11 | - name: Include Proxmox VM discovery 12 | include_tasks: "ansible-tasks/collect-proxmox-vm.yml" 13 | 14 | - name: Include Proxmox LXC discovery 15 | include_tasks: "ansible-tasks/collect-proxmox-lxc.yml" 16 | 17 | - name: Stop specified VM 18 | community.general.proxmox_kvm: 19 | #node: "{{ proxmox_env_info.node }}" 20 | api_user: "{{ proxmox_env_info.api_user }}" 21 | api_token_id: "{{ proxmox_env_info.api_token_id }}" 22 | api_token_secret: "{{ proxmox_env_info.api_token_secret }}" 23 | api_host: "{{ proxmox_env_info.api_host }}" 24 | #name: "{{ vm_config['name'] }}" 25 | vmid: "{{ vm_config['vmid'] }}" 26 | state: stopped 27 | force: true 28 | when: vm_config['name'] in collected_proxmox_vm and collected_proxmox_vm[vm_config['name']] | int == vm_config['vmid'] | int 29 | register: stop_vm_job 30 | async: 180 31 | poll: 0 32 | 33 | - name: Wait for VM stop process to finish 34 | async_status: 35 | jid: "{{ stop_vm_job.ansible_job_id }}" 36 | when: vm_config['name'] in collected_proxmox_vm 37 | register: _sjobs_alias_vc_0 38 | until: _sjobs_alias_vc_0.finished 39 | retries: 100 40 | delay: 10 41 | 42 | - name: "Proxmox: Remove specified VM" 43 | community.general.proxmox_kvm: 44 | node: "{{ proxmox_env_info.node }}" 45 | api_user: "{{ proxmox_env_info.api_user }}" 46 | api_token_id: "{{ proxmox_env_info.api_token_id }}" 47 | api_token_secret: "{{ proxmox_env_info.api_token_secret }}" 48 | api_host: "{{ proxmox_env_info.api_host }}" 49 | name: "{{ vm_config['name'] }}" 50 | #vmid: "{{ nb_remove_vms[item] }}" 51 | state: absent 52 | force: true 53 | when: vm_config['name'] in collected_proxmox_vm 54 | register: remove_vms_job 55 | async: 180 56 | poll: 0 57 | 58 | - name: "Proxmox: Wait for VM removal" 59 | async_status: 60 | jid: "{{ remove_vms_job.ansible_job_id }}" 61 | when: vm_config['name'] in collected_proxmox_vm 62 | register: _rjobs_alias_vc_0 63 | until: _rjobs_alias_vc_0.finished 64 | retries: 100 65 | delay: 5 66 | 67 | -------------------------------------------------------------------------------- /awx-proxmox-resize-lxc-disk.yml: -------------------------------------------------------------------------------- 1 | - name: "Proxmox: resize rootfs" 2 | connection: local 3 | hosts: all 4 | gather_facts: False 5 | 6 | vars: 7 | collected_proxmox_vm: {} 8 | collected_proxmox_lxc: {} 9 | 10 | tasks: 11 | - name: Include Proxmox VM discovery 12 | include_tasks: "ansible-tasks/collect-proxmox-vm.yml" 13 | 14 | - name: Include Proxmox LXC discovery 15 | include_tasks: "ansible-tasks/collect-proxmox-lxc.yml" 16 | 17 | - name: "Grow LXC disk" 18 | community.general.proxmox: 19 | node: "{{ proxmox_env_info.node }}" 20 | api_user: "{{ proxmox_env_info.api_user }}" 21 | api_token_id: "{{ proxmox_env_info.api_token_id }}" 22 | api_token_secret: "{{ proxmox_env_info.api_token_secret }}" 23 | api_host: "{{ proxmox_env_info.api_host }}" 24 | #hostname: "{{ vm_config['hostname'] }}" 25 | vmid: "{{ collected_proxmox_lxc[vm_config['name']] | int }}" 26 | disk: "{{ vm_config['storage_volume'] }}:{{ (vm_config['resize_disk_size']|int / 1024) | round | int }}" 27 | state: present 28 | update: true 29 | when: vm_config['name'] in collected_proxmox_lxc 30 | register: grow_lxc_disk_status 31 | async: 180 32 | poll: 0 33 | 34 | - name: Wait for VM settings updates processes to finish 35 | async_status: 36 | jid: "{{ grow_lxc_disk_status.ansible_job_id }}" 37 | when: vm_config['name'] in collected_proxmox_lxc 38 | register: _ujobs_alias_vc_0 39 | until: _ujobs_alias_vc_0.finished 40 | retries: 60 41 | delay: 10 42 | 43 | 44 | -------------------------------------------------------------------------------- /awx-proxmox-resize-vm-disk.yml: -------------------------------------------------------------------------------- 1 | - name: "Proxmox: resize scsi0" 2 | connection: local 3 | hosts: all 4 | gather_facts: False 5 | 6 | vars: 7 | collected_proxmox_vm: {} 8 | collected_proxmox_lxc: {} 9 | 10 | tasks: 11 | - name: Include Proxmox VM discovery 12 | include_tasks: "ansible-tasks/collect-proxmox-vm.yml" 13 | 14 | - name: Include Proxmox LXC discovery 15 | include_tasks: "ansible-tasks/collect-proxmox-lxc.yml" 16 | 17 | - name: Grow VM disk 18 | community.general.proxmox_disk: 19 | api_user: "{{ proxmox_env_info.api_user }}" 20 | api_token_id: "{{ proxmox_env_info.api_token_id }}" 21 | api_token_secret: "{{ proxmox_env_info.api_token_secret }}" 22 | api_host: "{{ proxmox_env_info.api_host }}" 23 | name: "{{ vm_config['name'] }}" 24 | disk: "{{ vm_config['resize_disk'] }}" 25 | size: "{{ (vm_config['resize_disk_size']|int / 1024) | round | int }}G" 26 | state: resized 27 | when: vm_config['name'] in collected_proxmox_vm 28 | register: vm_update_disk_size 29 | async: 180 30 | poll: 0 31 | 32 | - name: Wait for VM disk updates processes to finish 33 | async_status: 34 | jid: "{{ vm_update_disk_size.ansible_job_id }}" 35 | when: vm_config['name'] in collected_proxmox_vm 36 | register: _ujobs_alias_vc_0 37 | until: _ujobs_alias_vc_0.finished 38 | retries: 60 39 | delay: 10 40 | 41 | -------------------------------------------------------------------------------- /awx-proxmox-set-ipconfig0.yml: -------------------------------------------------------------------------------- 1 | - name: "Proxmox: set ipconfig0" 2 | connection: local 3 | hosts: all 4 | gather_facts: False 5 | 6 | vars: 7 | collected_proxmox_vm: {} 8 | collected_proxmox_lxc: {} 9 | 10 | tasks: 11 | - name: Include Proxmox VM discovery 12 | include_tasks: "ansible-tasks/collect-proxmox-vm.yml" 13 | 14 | - name: Include Proxmox LXC discovery 15 | include_tasks: "ansible-tasks/collect-proxmox-lxc.yml" 16 | 17 | - name: "Update VM settings (ipconfig0, ssh key)" 18 | community.general.proxmox_kvm: 19 | node: "{{ proxmox_env_info.node }}" 20 | api_user: "{{ proxmox_env_info.api_user }}" 21 | api_token_id: "{{ proxmox_env_info.api_token_id }}" 22 | api_token_secret: "{{ proxmox_env_info.api_token_secret }}" 23 | api_host: "{{ proxmox_env_info.api_host }}" 24 | #name: "{{ vm_config['name'] }}" 25 | vmid: "{{ collected_proxmox_vm[vm_config['name']] }}" 26 | sshkeys: "{{ vm_config['ssh_key'] }}" 27 | ipconfig: 28 | #ipconfig0: "ip={{ item.ip }},gw={{ item.ip.split('.')[:3] | join('.') }}.{{ item.gw if item.gw is defined else gw_last_quad }}" 29 | ipconfig0: "ip={{ vm_config['ip'] }},gw={{ vm_config['ip'].split('.')[:3] | join('.') }}.1" 30 | update: true 31 | when: vm_config['name'] in collected_proxmox_vm 32 | register: update_vm_settings_job 33 | async: 180 34 | poll: 0 35 | 36 | - name: Wait for VM settings updates processes to finish 37 | async_status: 38 | jid: "{{ update_vm_settings_job.ansible_job_id }}" 39 | when: vm_config['name'] in collected_proxmox_vm 40 | register: _ujobs_alias_vc_0 41 | until: _ujobs_alias_vc_0.finished 42 | retries: 60 43 | delay: 10 44 | 45 | -------------------------------------------------------------------------------- /awx-proxmox-set-netif.yml: -------------------------------------------------------------------------------- 1 | - name: "Proxmox: set netif (LXC)" 2 | connection: local 3 | hosts: all 4 | gather_facts: False 5 | 6 | vars: 7 | collected_proxmox_vm: {} 8 | collected_proxmox_lxc: {} 9 | 10 | tasks: 11 | - name: Include Proxmox VM discovery 12 | include_tasks: "ansible-tasks/collect-proxmox-vm.yml" 13 | 14 | - name: Include Proxmox LXC discovery 15 | include_tasks: "ansible-tasks/collect-proxmox-lxc.yml" 16 | 17 | - name: "Update LXC settings (netif)" 18 | community.general.proxmox: 19 | node: "{{ proxmox_env_info.node }}" 20 | api_user: "{{ proxmox_env_info.api_user }}" 21 | api_token_id: "{{ proxmox_env_info.api_token_id }}" 22 | api_token_secret: "{{ proxmox_env_info.api_token_secret }}" 23 | api_host: "{{ proxmox_env_info.api_host }}" 24 | hostname: "{{ vm_config['hostname'] }}" 25 | vmid: "{{ collected_proxmox_lxc[vm_config['hostname']] | int }}" 26 | netif: 27 | #ipconfig0: "ip={{ item.ip }},gw={{ item.ip.split('.')[:3] | join('.') }}.{{ item.gw if item.gw is defined else gw_last_quad }}" 28 | net0: "name=eth0,ip={{ vm_config['ip'] }},gw={{ vm_config['ip'].split('.')[:3] | join('.') }}.1,bridge=vmbr0,firewall=1" 29 | update: true 30 | when: vm_config['hostname'] in collected_proxmox_lxc 31 | register: update_lxc_settings_job 32 | async: 180 33 | poll: 0 34 | 35 | - name: Wait for VM settings updates processes to finish 36 | async_status: 37 | jid: "{{ update_lxc_settings_job.ansible_job_id }}" 38 | when: vm_config['hostname'] in collected_proxmox_lxc 39 | register: _ujobs_alias_vc_0 40 | until: _ujobs_alias_vc_0.finished 41 | retries: 60 42 | delay: 10 43 | 44 | -------------------------------------------------------------------------------- /awx-proxmox-start-lxc.yml: -------------------------------------------------------------------------------- 1 | - name: "Proxmox: start LXC" 2 | connection: local 3 | hosts: all 4 | gather_facts: False 5 | 6 | vars: 7 | collected_proxmox_vm: {} 8 | collected_proxmox_lxc: {} 9 | 10 | tasks: 11 | - name: Include Proxmox VM discovery 12 | include_tasks: "ansible-tasks/collect-proxmox-vm.yml" 13 | 14 | - name: Include Proxmox LXC discovery 15 | include_tasks: "ansible-tasks/collect-proxmox-lxc.yml" 16 | 17 | - name: Show collected VM 18 | debug: 19 | msg: "Collected VM: {{ collected_proxmox_lxc }}" 20 | 21 | - name: Show collected LXC 22 | debug: 23 | msg: "Collected LXC: {{ collected_proxmox_lxc }}" 24 | 25 | - name: Show start key 26 | debug: 27 | msg: "start key {{ vm_config['vmid'] }} ||| {{ collected_proxmox_lxc[vm_config['hostname']] }} ||| {{ vm_config['vmid'] | int == collected_proxmox_lxc[vm_config['hostname']] | int }}" 28 | 29 | - name: Start specified LXC 30 | community.general.proxmox: 31 | #node: "{{ proxmox_env_info.node }}" 32 | api_user: "{{ proxmox_env_info.api_user }}" 33 | api_token_id: "{{ proxmox_env_info.api_token_id }}" 34 | api_token_secret: "{{ proxmox_env_info.api_token_secret }}" 35 | api_host: "{{ proxmox_env_info.api_host }}" 36 | #name: "{{ vm_config['name'] }}" 37 | vmid: "{{ vm_config['vmid'] }}" 38 | state: started 39 | when: (vm_config['hostname'] in collected_proxmox_lxc) and (collected_proxmox_lxc[vm_config['hostname']] | int == vm_config['vmid'] | int) 40 | register: start_vm_job 41 | async: 180 42 | poll: 0 43 | 44 | - name: Wait for LXC start process to finish 45 | async_status: 46 | jid: "{{ start_vm_job.ansible_job_id }}" 47 | when: vm_config['hostname'] in collected_proxmox_lxc 48 | register: _sjobs_alias_vc_0 49 | until: _sjobs_alias_vc_0.finished 50 | retries: 100 51 | delay: 10 52 | 53 | - name: Discover Proxmox started LXC on node 54 | community.general.proxmox_vm_info: 55 | api_host: "{{ proxmox_env_info.api_host | default(omit) }}" 56 | api_user: "{{ proxmox_env_info.api_user | default(omit) }}" 57 | api_token_id: "{{ proxmox_env_info.api_token_id | default(omit) }}" 58 | api_token_secret: "{{ proxmox_env_info.api_token_secret | default(omit) }}" 59 | node: "{{ proxmox_env_info.node }}" 60 | #name: "{{ vm_config['name'] }}" 61 | vmid: "{{ vm_config['vmid'] }}" 62 | config: "current" 63 | register: proxmox_started_vm 64 | 65 | - name: Show started LXC info 66 | debug: 67 | msg: "started vm info: {{ proxmox_started_vm }}" 68 | 69 | # - name: Wait for SSH on VMs to become available 70 | # wait_for: host="{{ nb_vm_interface_assigned_ip[item] | regex_replace('/.*$', '') }}" port=22 delay=10 timeout=300 71 | # with_items: "{{ proxmox_all_vm_interfaces }}" 72 | # when: proxmox_all_vm_interfaces[item].primary 73 | 74 | -------------------------------------------------------------------------------- /awx-proxmox-start-vm.yml: -------------------------------------------------------------------------------- 1 | - name: "Proxmox: start VM" 2 | connection: local 3 | hosts: all 4 | gather_facts: False 5 | 6 | vars: 7 | collected_proxmox_vm: {} 8 | collected_proxmox_lxc: {} 9 | 10 | tasks: 11 | - name: Include Proxmox VM discovery 12 | include_tasks: "ansible-tasks/collect-proxmox-vm.yml" 13 | 14 | - name: Include Proxmox LXC discovery 15 | include_tasks: "ansible-tasks/collect-proxmox-lxc.yml" 16 | 17 | - name: Start specified VM 18 | community.general.proxmox_kvm: 19 | #node: "{{ proxmox_env_info.node }}" 20 | api_user: "{{ proxmox_env_info.api_user }}" 21 | api_token_id: "{{ proxmox_env_info.api_token_id }}" 22 | api_token_secret: "{{ proxmox_env_info.api_token_secret }}" 23 | api_host: "{{ proxmox_env_info.api_host }}" 24 | #name: "{{ vm_config['name'] }}" 25 | vmid: "{{ vm_config['vmid'] }}" 26 | state: started 27 | when: vm_config['name'] in collected_proxmox_vm and collected_proxmox_vm[vm_config['name']] | int == vm_config['vmid'] | int 28 | register: start_vm_job 29 | async: 180 30 | poll: 0 31 | 32 | - name: Wait for VM start process to finish 33 | async_status: 34 | jid: "{{ start_vm_job.ansible_job_id }}" 35 | when: vm_config['name'] in collected_proxmox_vm 36 | register: _sjobs_alias_vc_0 37 | until: _sjobs_alias_vc_0.finished 38 | retries: 100 39 | delay: 10 40 | 41 | - name: Discover Proxmox started VM on node 42 | community.general.proxmox_vm_info: 43 | api_host: "{{ proxmox_env_info.api_host | default(omit) }}" 44 | api_user: "{{ proxmox_env_info.api_user | default(omit) }}" 45 | api_token_id: "{{ proxmox_env_info.api_token_id | default(omit) }}" 46 | api_token_secret: "{{ proxmox_env_info.api_token_secret | default(omit) }}" 47 | node: "{{ proxmox_env_info.node }}" 48 | #name: "{{ vm_config['name'] }}" 49 | vmid: "{{ vm_config['vmid'] }}" 50 | config: "current" 51 | register: proxmox_started_vm 52 | 53 | - name: Show started VM info 54 | debug: 55 | msg: "started vm info: {{ proxmox_started_vm }}" 56 | 57 | # - name: Wait for SSH on VMs to become available 58 | # wait_for: host="{{ nb_vm_interface_assigned_ip[item] | regex_replace('/.*$', '') }}" port=22 delay=10 timeout=300 59 | # with_items: "{{ proxmox_all_vm_interfaces }}" 60 | # when: proxmox_all_vm_interfaces[item].primary 61 | 62 | -------------------------------------------------------------------------------- /awx-proxmox-stop-lxc.yml: -------------------------------------------------------------------------------- 1 | - name: "Proxmox: stop LXC" 2 | connection: local 3 | hosts: all 4 | gather_facts: False 5 | 6 | vars: 7 | collected_proxmox_vm: {} 8 | collected_proxmox_lxc: {} 9 | 10 | tasks: 11 | - name: Include Proxmox VM discovery 12 | include_tasks: "ansible-tasks/collect-proxmox-vm.yml" 13 | 14 | - name: Include Proxmox LXC discovery 15 | include_tasks: "ansible-tasks/collect-proxmox-lxc.yml" 16 | 17 | - name: Stop specified LXC 18 | community.general.proxmox: 19 | #node: "{{ proxmox_env_info.node }}" 20 | api_user: "{{ proxmox_env_info.api_user }}" 21 | api_token_id: "{{ proxmox_env_info.api_token_id }}" 22 | api_token_secret: "{{ proxmox_env_info.api_token_secret }}" 23 | api_host: "{{ proxmox_env_info.api_host }}" 24 | #name: "{{ vm_config['name'] }}" 25 | vmid: "{{ vm_config['vmid'] }}" 26 | state: stopped 27 | force: true 28 | when: (vm_config['hostname'] in collected_proxmox_lxc) and (collected_proxmox_lxc[vm_config['hostname']] | int == vm_config['vmid'] | int) 29 | register: stop_vm_job 30 | async: 180 31 | poll: 0 32 | 33 | - name: Wait for LXC stop process to finish 34 | async_status: 35 | jid: "{{ stop_vm_job.ansible_job_id }}" 36 | when: vm_config['hostname'] in collected_proxmox_lxc 37 | register: _sjobs_alias_vc_0 38 | until: _sjobs_alias_vc_0.finished 39 | retries: 100 40 | delay: 10 41 | -------------------------------------------------------------------------------- /awx-proxmox-stop-vm.yml: -------------------------------------------------------------------------------- 1 | - name: "Proxmox: stop VM" 2 | connection: local 3 | hosts: all 4 | gather_facts: False 5 | 6 | vars: 7 | collected_proxmox_vm: {} 8 | collected_proxmox_lxc: {} 9 | 10 | tasks: 11 | - name: Include Proxmox VM discovery 12 | include_tasks: "ansible-tasks/collect-proxmox-vm.yml" 13 | 14 | - name: Include Proxmox LXC discovery 15 | include_tasks: "ansible-tasks/collect-proxmox-lxc.yml" 16 | 17 | - name: Stop specified VM 18 | community.general.proxmox_kvm: 19 | #node: "{{ proxmox_env_info.node }}" 20 | api_user: "{{ proxmox_env_info.api_user }}" 21 | api_token_id: "{{ proxmox_env_info.api_token_id }}" 22 | api_token_secret: "{{ proxmox_env_info.api_token_secret }}" 23 | api_host: "{{ proxmox_env_info.api_host }}" 24 | #name: "{{ vm_config['name'] }}" 25 | vmid: "{{ vm_config['vmid'] }}" 26 | state: stopped 27 | force: true 28 | when: vm_config['name'] in collected_proxmox_vm and collected_proxmox_vm[vm_config['name']] | int == vm_config['vmid'] | int 29 | register: stop_vm_job 30 | async: 180 31 | poll: 0 32 | 33 | - name: Wait for VM stop process to finish 34 | async_status: 35 | jid: "{{ stop_vm_job.ansible_job_id }}" 36 | when: vm_config['name'] in collected_proxmox_vm 37 | register: _sjobs_alias_vc_0 38 | until: _sjobs_alias_vc_0.finished 39 | retries: 100 40 | delay: 10 41 | 42 | -------------------------------------------------------------------------------- /awx-update-dns.yml: -------------------------------------------------------------------------------- 1 | - name: "Proxmox: DNS" 2 | connection: local 3 | hosts: all 4 | gather_facts: False 5 | 6 | vars: 7 | collected_dns_integrations: [] 8 | dns_zone_origin: null 9 | dns_zone_ttl: 0 10 | collected_soa: {} 11 | collected_ns: [] 12 | collected_mx: [] 13 | collected_rr: [] 14 | 15 | tasks: 16 | # - name: Show DNS stuff 17 | # debug: 18 | # msg: "DNS stuff zone: {{ dns_zone }} ||| soa: {{ dns_soa }} ||| dns integrations: {{ dns_integrations }} -> {{ dns_integrations | length }}" 19 | 20 | - name: Collected DNS integrations 21 | set_fact: 22 | collected_dns_integrations: "{{ collected_dns_integrations + [ item ] }}" 23 | loop: "{{ dns_integrations }}" 24 | 25 | # - name: show dns integrations 26 | # debug: 27 | # msg: "hey: {{ collected_dns_integrations }} ||| {{ collected_dns_integrations | length }}" 28 | 29 | - name: Set DNS zone origin 30 | set_fact: 31 | dns_zone_origin: "{{ dns_zone['name'] }}" 32 | when: dns_integrations | length > 0 33 | 34 | - name: Set DNS zone TTL 35 | set_fact: 36 | dns_zone_ttl: "{{ dns_soa['default_ttl'] }}" 37 | when: dns_integrations | length > 0 38 | 39 | - name: Set SOA entries 40 | set_fact: 41 | collected_soa: 42 | expire: "{{ dns_soa['soa_expire'] }}" 43 | minimum: "{{ dns_soa['soa_minimum'] }}" 44 | mname: "{{ dns_soa['soa_mname'] }}" 45 | refresh: "{{ dns_soa['soa_refresh'] }}" 46 | retry: "{{ dns_soa['soa_retry'] }}" 47 | rname: "{{ dns_soa['soa_rname'] }}" 48 | serial: "{{ dns_soa['soa_serial'] }}" 49 | ttl: "{{ dns_soa['soa_ttl'] }}" 50 | when: dns_integrations | length > 0 51 | 52 | - name: Collect NS 53 | set_fact: 54 | collected_ns: "{{ collected_ns + [ item.name ] }}" 55 | loop: "{{ dns_ns }}" 56 | when: dns_integrations | length > 0 57 | 58 | - name: Get DNS records for zone by id 59 | local_action: 60 | module: uri 61 | url: "{{ netbox_env_info.api_proto }}://{{ netbox_env_info.api_host }}:{{ netbox_env_info.api_port}}/api/plugins/netbox-dns/records/?zone_id={{ dns_zone['id'] }}" 62 | method: GET 63 | headers: 64 | Authorization: "Token {{ netbox_env_info.api_token }}" 65 | Accept: application/json 66 | Content-Type: application/json 67 | status_code: 200 68 | validate_certs: no 69 | register: dns_records 70 | when: dns_integrations | length > 0 71 | 72 | - name: Collect all MX records 73 | set_fact: 74 | collected_mx: "{{ collected_mx + [ item.value ] }}" 75 | loop: "{{ dns_records.json.results }}" 76 | when: item.type == 'MX' 77 | 78 | - name: Collect DNS entries 79 | set_fact: 80 | collected_rr: "{{ collected_rr + [{'name': item.name, 'value': item.value, 'type': item.type, 'ttl': item.ttl or ''}] }}" 81 | loop: "{{ dns_records.json.results }}" 82 | when: item.type not in ['SOA', 'MX', 'NS'] 83 | 84 | - name: Show DNS zone origin 85 | debug: 86 | msg: "DNS zone origin: {{ dns_zone_origin }}" 87 | when: dns_integrations | length > 0 88 | 89 | - name: Show DNS zone TTL 90 | debug: 91 | msg: "DNS zone TTL: {{ dns_zone_ttl }}" 92 | when: dns_integrations | length > 0 93 | 94 | - name: Show collected SOA 95 | debug: 96 | msg: "collected: {{ collected_soa }}" 97 | when: dns_integrations | length > 0 98 | 99 | - name: Show collected NS 100 | debug: 101 | msg: "collected: {{ collected_ns }}" 102 | when: dns_integrations | length > 0 103 | 104 | - name: Show collected MX 105 | debug: 106 | msg: "collected: {{ collected_mx }}" 107 | when: dns_integrations | length > 0 108 | 109 | - name: Show collected DNS entries 110 | debug: 111 | msg: "collected DNS entries: {{ collected_rr }}" 112 | when: dns_integrations | length > 0 113 | 114 | - name: Show expanded template for DNS zone 115 | debug: 116 | msg: "{{ lookup('ansible.builtin.template', './templates/bind9/zone-template.j2') }}" 117 | 118 | -------------------------------------------------------------------------------- /conf.d/netbox_setup_objects.yml-sample: -------------------------------------------------------------------------------- 1 | proxmox_api_config: 2 | api_host: proxmox-ip-or-hostname 3 | api_port: 8006 4 | api_user: proxmox_api_user 5 | api_token_id: name_of_proxmox_api_token 6 | api_token_secret: proxmox_api_secret_token 7 | verify_ssl: false 8 | netbox_api_config: 9 | api_proto: http # or https 10 | api_host: name or ip of NetBox host 11 | api_port: 8000 12 | api_token: netbox_api_secret_token 13 | verify_ssl: false # or true, up to you 14 | proxmox: 15 | cluster_name: proxmox-ve 16 | netbox: 17 | cluster_role: Proxmox 18 | vm_role: "Proxmox VM" 19 | lxc_role: "Proxmox LXC" 20 | automation_type: choices are ansible_automation or flask_application 21 | ansible_automation: 22 | host: name or ip of AWX/Tower/AAP host 23 | http_proto: http or https 24 | http_port: 80 or whatever 25 | ssl_verify: false # or true 26 | username: awx_user # should have permissions to view both projects and templates 27 | password: awx_password 28 | settings: 29 | project: 30 | name: name-of-your-project # default: netbox-proxmox-ee-test1 31 | scm_type: your-scm-type # Optional. Default: git 32 | scm_url: your-scm-url # Optional. Default: https://github.com/netboxlabs/netbox-proxmox-automation.git 33 | scm_branch: main # Optional. Default: main 34 | inventory: 35 | name: your-inventory # Default: Demo Inventory 36 | hosts: 37 | name: your-defined-host # Default: localhost 38 | flask_application: 39 | host: name or ip of where Flask application is running 40 | http_proto: http or https 41 | http_port: 9000 or whatever 42 | ssl_verify: false # or true 43 | netbox_webhook_name: "netbox-proxmox-webhook" 44 | -------------------------------------------------------------------------------- /docs/configure-awx-aap.md: -------------------------------------------------------------------------------- 1 | # Configure AWX/Tower/AAP 2 | 3 | *You only need to do this configuration step if you intend to use AWX/Tower/AAP to handle your Proxmox automation.* 4 | 5 | Certainly, you do not need to do Ansible automation by using webhooks and event rules (triggering) in NetBox. [This weblog](https://netboxlabs.com/blog/getting-started-with-network-automation-netbox-ansible/) shows you how you can use [Ansible](https://www.ansible.com/) with NetBox, as network source of truth, to induce changes in your environment -- by facilitating automation from any client on your network. In that example, you'd be able to run `ansible-playbook`, alongside a dynamic inventory (NetBox) to induce automation, and from there you could add Proxmox VM automation. 6 | 7 | However, many other NetBox users want to use NetBox as NSoT (network source of truth) to facilitate their Proxmox VM automation. Changes to (Proxmox) VMs in NetBox will result in automation being kicked off, in this case via [AWX](https://github.com/ansible/awx), or perhaps for a Red Hat commercial customer, through [Tower/AAP](https://www.redhat.com/en/solutions/it-automation?sc_cid=7015Y000003sm3kQAA&gad_source=1&gclid=CjwKCAiAl4a6BhBqEiwAqvrqugh1f-1RfeP-NQxOKYhSbwJqUPVqGqR1A0ScrGMdNhLUbdTayU-EOhoCg00QAvD_BwE&gclsrc=aw.ds). By using webhooks and event rules in NetBox, AWX/Tower/AAP are more than capable of inducing Proxmox automation. In fact, using AWX/Tower/AAP is the preferred method for large environments -- where Proxmox VM deployment might be a part of an underlying CI/CD process. 8 | 9 | For those who are unfamiliar, AWX is the upstream (community, i.e. free) version of AAP. Functionally, AWX works the same way as Tower/AAP, but without the commercial support. AWX is an excellent alternative as you work through NetBox/Proxmox automation, but it can be a heavy lift when configuring AWX for the first time. This section talks through the steps you'll need to be able to run AWX and to begin your Proxmox VM automation journey with NetBox. 10 | 11 | ### Installing AWX with docker-compose 12 | 13 | AWX/Tower/AAP are typically installed in an environment where Kuberenetes (k8s) is available. However, should you have Docker/docker-compose running on your local system, you should be able to install AWX [this way](https://github.com/ansible/awx/blob/devel/tools/docker-compose/README.md). 14 | 15 | Once you have installed AWX/Tower/AAP in your environment, and are able to login, as an 'admin' user through the UI, you can start configuring AWX/Tower/AAP to facilitate your Proxmox VM automation. *Note that you can add whatever user(s)/group(s) that you want to AWX, but make sure that whatever user(s)/group(s) you add to AWX have the appropriate permissions to manage the following.* 16 | 17 | #### Create Github (or your Git of choice) Credential in AWX 18 | 19 | 1. Login to the AWX UI 20 | 2. Navigate to Resources > Credentials 21 | 3. Create a new credential, as specified below, of type 'Source Control'. Call it whatever you want, then make sure to copy and paste your SSH private key and (if required) SSH passphrase here. 22 | 23 | ![AWX image git credential image](./images/awx-scm-credential-new.png) 24 | 25 | #### Select Inventory in AWX 26 | 27 | Navigate to Resources > Inventories. 'Demo Inventory' should be sufficient for `netbox-promox-automation`. 28 | 29 | ![AWX default inventory image](./images/awx-default-inventory.png) 30 | 31 | #### Create Execution Environment in AWX 32 | 33 | Typically, when `ansible` or `ansible-playbook` is/are executed from the command line, this is done via a Python3 `venv`. However, with AWX, there is no such capability to interact with a command line to leverage `venv` to do a `pip install` of NetBox/Python module dependencies. 34 | 35 | As a result, you will need to use an [Execution Environment](https://ansible.readthedocs.io/projects/awx/en/latest/userguide/execution_environments.html) in AWX. Your Execution Environment is a container image that will include all of the (NetBox/Python) module dependencies that you'll need to facilitate Proxmox automation, and this container image will live in your container registry of choice. 36 | 37 | *You (probably) only need to create an Exection Environment once for `netbox-proxmox-automation` with AWX/Tower/AAP.* 38 | 39 | In the end, your Execution Environment should look like this in AWX. 40 | 41 | ![AWX Execution Environment image](./images/awx-execution-environment.png) 42 | 43 | To create your Execution Environment, you need to do the following. 44 | 45 | 1. On your AWX host, create a directory structure like this. 46 | 47 | ![AWX Execution Environment directory tree image](./images/awx-execution-environment-tree.png) 48 | 49 | 2. On your AWX host, create and change to the directory where your Execution Environment will live: `mkdir -p /home/ubuntu/awx/execution-environments/ubuntu-env1; cd /home/ubuntu/awx/execution-environments/ubuntu-env1` 50 | 51 | 3. On your AWX host, setup a Python 3 `venv`: `cd /home/ubuntu/awx/execution-environments/ubuntu-env1 ; python3 -m venv venv ; source venv/bin/activate` 52 | 53 | 4. On your AWX host, create a file called `execution-environment.yml`. It should look like the following. 54 | 55 | ![AWX Execution Environment config file image](./images/awx-execution-environment-yml.png) 56 | 57 | Note that `execution-environment.yml` contains two requirement lines. One is for Ansible Galaxy, and the other is for Python 3. 58 | 59 | 5. Create `dependencies/requirements.txt` and `dependencies/requirements.yml`. 60 | 61 | `requirements.txt` will include the Python 3 module dependencies. 62 | 63 | ``` 64 | (venv) shell$ mkdir dependencies 65 | 66 | (venv) shell$ cat dependencies/requirements.txt 67 | ansible 68 | ansible-core 69 | ansible-runner 70 | bcrypt 71 | certifi 72 | cffi 73 | charset-normalizer 74 | cryptography 75 | dnspython 76 | docutils 77 | idna 78 | Jinja2 79 | lockfile 80 | MarkupSafe 81 | packaging 82 | paramiko 83 | pexpect 84 | prompt-toolkit 85 | proxmoxer 86 | ptyprocess 87 | pycparser 88 | PyNaCl 89 | pynetbox 90 | python-daemon 91 | PyYAML 92 | questionary 93 | requests 94 | resolvelib 95 | six 96 | urllib3 97 | wcwidth 98 | ``` 99 | 100 | `requirements.yml` will define the Ansible collections to include. In this case we want to include the awx collection, the community.general collection (where the Proxmox Ansible modules live), and the netbox.netbox collection, which will interface with the NetBox API. 101 | 102 | ``` 103 | (venv) shell$ mkdir dependencies 104 | 105 | (venv) shell$ cat dependencies/requirements.yml 106 | --- 107 | collections: 108 | - awx.awx 109 | - community.general 110 | - netbox.netbox 111 | ``` 112 | 113 | 6. Finally, build the new Execution Environment image, and push to your container registry, which in this case lives on localhost (local to AWX) 114 | 115 | ![AWX Build Execution Environment commands image](./images/awx-ansible-build-ee.png) 116 | 117 | Once you have built your Execution Environment, which is based on the default AWX Execution Environment, you can proceed with Proxmox automation in AWX. 118 | 119 | ### Automating AWX NetBox/Proxmox Configuration 120 | 121 | There exists a convenience script in `netbox-proxmox-automation` that's called `./setup/configure_ansible_automation.py`, and it uses `awxkit` to manage objects in AWX. It leverages the configuration that you wrote based on the sample configuration called `./conf.d/netbox_setup_objects.yml-sample`, and will save you an *immense* amount of time. 122 | 123 | Usage (inital setup): 124 | 125 | ``` 126 | shell$ cd /path/to/netbox-proxmox-automation/setup 127 | 128 | shell$ deactivate 129 | 130 | shell$ rm -rf venv 131 | 132 | shell$ python3 -m venv venv 133 | 134 | shell$ source venv/bin/activate 135 | 136 | shell$ pip install -r requirements.txt 137 | ``` 138 | 139 | Then make sure that you've made a copy of `../conf.d/netbox_setup_objects.yml-sample` and seasoned it to taste before proceeding. 140 | 141 | Usage (creation) of AWX objects that will be used for NetBox/Proxmox automation: `./configure_ansible_automation.py create --config /path/to/netbox-setup-objects.yml` 142 | 143 | Usage (removal) of *all* AWX objects that would be used for NetBox/Proxmox automation (*proceed with caution!*): `./configure_ansible_automation.py destroy --config /path/to/netbox-setup-objects.yml` 144 | 145 | ### Manual AWX NetBox/Proxmox Configuration 146 | 147 | *This is not required if you have been able to run the convenience script, `./configure_ansible_automation.py`.* 148 | 149 | #### Add NetBox and Proxmox Credential Types to AWX 150 | 151 | Navigate to Administration > Credential Types in AWX, and create a credential type called 'NetBox Proxmox Creds'. 152 | 153 | ![AWX Netbox Proxmox Creds image](./images/awx-netbox-proxmox-creds.png) 154 | 155 | Input configuration should include the following: 156 | 157 | ``` 158 | fields: 159 | - id: proxmox_api_host 160 | type: string 161 | label: Proxmox API Host 162 | - id: proxmox_api_user 163 | type: string 164 | label: Proxmox API User 165 | - id: proxmox_api_user_token 166 | type: string 167 | label: Proxmox API Token ID 168 | - id: proxmox_node 169 | type: string 170 | label: Proxmox Node 171 | - id: proxmox_api_token_secret 172 | type: string 173 | label: Proxmox API Token 174 | secret: true 175 | - id: netbox_api_proto 176 | type: string 177 | label: NetBox HTTP Protocol 178 | - id: netbox_api_host 179 | type: string 180 | label: NetBox API host 181 | - id: netbox_api_port 182 | type: string 183 | label: NetBox API port 184 | - id: netbox_api_token 185 | type: string 186 | label: NetBox API token 187 | secret: true 188 | required: 189 | - proxmox_api_host 190 | - proxmox_api_user 191 | - proxmox_api_user_token 192 | - proxmox_node 193 | - proxmox_api_token_secret 194 | - netbox_api_host 195 | - netbox_api_port 196 | - netbox_api_proto 197 | - netbox_api_token 198 | ``` 199 | 200 | Injector configuration should include the following (yes, `extra_vars` is required, as is `netbox_env_info` and `proxmox_env_info`): 201 | 202 | ``` 203 | extra_vars: 204 | netbox_env_info: 205 | api_host: '{{ netbox_api_host }}' 206 | api_port: '{{ netbox_api_port }}' 207 | api_proto: '{{ netbox_api_proto }}' 208 | api_token: '{{ netbox_api_token }}' 209 | proxmox_env_info: 210 | node: '{{ proxmox_node }}' 211 | api_host: '{{ proxmox_api_host }}' 212 | api_user: '{{ proxmox_api_user }}' 213 | api_token_id: '{{ proxmox_api_user_token }}' 214 | api_token_secret: '{{ proxmox_api_token_secret }}' 215 | ``` 216 | 217 | #### Add NetBox/Proxmox Credentials to AWX 218 | 219 | Navigate to Resources > Credentials in AWX, and create a credential called 'NetBox Proxmox Credentials'. 220 | 221 | ![NetBox Proxmox Credentials Image](./images/awx-netbox-proxmox-credentials.png) 222 | 223 | #### Add Project to AWX 224 | 225 | Navigate to Resources > Projects in AWX, and create a new Project called 'netbox-proxmox-ee-test1'. 226 | 227 | ![NetBox Proxmox Create Project image](./images/awx-create-project.png) 228 | 229 | Click the 'Sync' button in the AWX UI to ensure that git synchronization is successful. If this step is *not* successful, then *do not proceed* -- as you troubleshoot. Otherwise, proceed. 230 | 231 | #### Add (job) Templates to AWX 232 | 233 | In AWX, a (job) template provides a web accessible means of triggering automation, i.e. via a webhook. Each (job) template represents an Ansible playbook -- each Ansible playbook represents a file that was synchronized from git when you created the project in AWX -- where the playbook will perform Proxmox automation. 234 | 235 | For example, when you have defined a Proxmox VM in NetBox (alongside its default resources), you can use `awx-clone-vm-and-set-resources.yml` to automate the cloning of a VM and setting its resources in Proxmox. 236 | 237 | ![NetBox Proxmox clone vm and set resources image](./images/awx-proxmox-clone-vm-and-set-resources.png) 238 | 239 | When you create *any* template in AWX for Proxmox automation, you will need to set 'Prompt on launch' to true (tick checkbox) for both 'Credentials' and 'Variables', as shown below. 240 | 241 | ![NetBox Proxmox clone vm and set resources edit image](./images/awx-netbox-proxmox-template-clone-resources-edit.png) 242 | 243 | `netbox-proxmox-ansible` provides a series of Ansible playbooks that you can use to create fully-functioning Proxmox VMs based on their desired configuration states in NetBox. You will need to create a (job) template for each playbook in AWX. 244 | 245 | `netbox-proxmox-automation` implements the following Ansible playbooks. 246 | 247 | | Ansible playbook | Purpose | 248 | | --- | --- | 249 | | awx-proxmox-add-vm-disk.yml | Adds a disk to Proxmox VM (*not* scsi0, which is the OS disk) | 250 | | awx-proxmox-clone-vm-and-set-resources.yml | Clones a Proxmox VM template to a Proxmox VM and sets resources like vcpu and memory | 251 | | awx-proxmox-remove-vm-disk.yml | Removes a non-OS disk (i.e. not scsi0) from a Proxmox VM | 252 | | awx-proxmox-remove-vm.yml | Removes a Proxmox VM | 253 | | awx-proxmox-resize-vm-disk.yml | Resizes a Proxmox VM disk | 254 | | awx-proxmox-set-ipconfig0.yml | Sets ipconfig0 for Proxmox VM and adds ssh key| 255 | | awx-proxmox-start-vm.yml | Starts Proxmox VM | 256 | | awx-proxmox-stop-vm.yml | Stops Proxmox VM | 257 | 258 | -------------------------------------------------------------------------------- /docs/configure-flask-application.md: -------------------------------------------------------------------------------- 1 | ## Configure Flask Application (Python) 2 | 3 | *You only need to do this configuration step if you intend to use the Flask application to handle your Proxmox automation.* 4 | 5 | Open a shell on your local system. *Do not* run these commands as the 'root' user. The following commands should run on MacOS, Linux, and UNIX-like systems; you will need to run these commands during initial installation or upgrades of `netbox-proxmox-automation`. 6 | 7 | ``` 8 | shell$ cd /path/to/netbox-proxmox-automation/netbox-event-driven-automation-flask-app 9 | 10 | shell$ deactivate # this will fail if there is no configured venv 11 | 12 | shell$ rm -rf venv 13 | 14 | shell$ python3 -m venv venv 15 | 16 | shell$ source venv/bin/activate 17 | 18 | (venv) shell$ pip install -r requirements.txt # this will install all of the dependencies 19 | ``` 20 | 21 | To leave `venv`, simply type 'deactivate'. 22 | 23 | ``` 24 | (venv) shell$ deactivate 25 | shell$ 26 | ``` 27 | 28 | With each usage of `netbox-proxmox-automation`, make sure that you enter `venv` before running any Ansible commands. Else this automation will not work. 29 | 30 | ``` 31 | shell$ cd /path/to/netbox-proxmox-automation/netbox-event-driven-automation-flask-app 32 | 33 | shell$ source venv/bin/activate 34 | 35 | (venv) shell$ # <--- this is the desired result 36 | ``` 37 | 38 | When in `venv`, you will need to create `app_config.yml`. 39 | 40 | ``` 41 | (venv) shell$ cd /path/to/netbox-proxmox-automation/netbox-event-driven-automation-flask-app 42 | 43 | (venv) shell$ cp -pi app_config.yml-sample app_config.yml 44 | ``` 45 | 46 | Then season `app_config.yml` to taste. When you are ready to test your Flask application, do this: 47 | 48 | ``` 49 | (venv) shell$ flask run -h 0.0.0.0 -p 8000 --debug 50 | * Debug mode: on 51 | WARNING: This is a development server. Do not use it in a production deployment. Use a production WSGI server instead. 52 | * Running on all addresses (0.0.0.0) 53 | * Running on http://127.0.0.1:8000 54 | * Running on http://X.X.X.X:8000 55 | Press CTRL+C to quit 56 | * Restarting with stat 57 | * Debugger is active! 58 | * Debugger PIN: XXX-XXX-XXX 59 | ``` 60 | 61 | The above `flask` command will start the Flask application on port 8000 (or whatever you specify with the `-p` argument) and will bind on the IP address (or IP addresses) that were specified with the `-h` argument. In this case, we used 0.0.0.0 with the `-h` argument, so the Flask application will listen on all interfaces. The `--debug` argument indicates that we will run a single-threaded web service and that we will show output to stdout. 62 | 63 | *You will want to use `gunicorn.py` or some other WSGI server to run the Flask application in production.* This is documented [here](https://flask.palletsprojects.com/en/stable/deploying/gunicorn/), but here's an annotated version of how to use gunicorn to run netbox-event-driven-automation-flask-app. 64 | 65 | 1. Make sure that you are in the netbox-event-driven-automation-flask-app directory: `cd /path/to/netbox-proxmox-automation/netbox-event-driven-automation-flask-app` 66 | 67 | 2. Leave `venv`: `deactivate` 68 | 69 | 3. Remove existing `venv`: `rm -rf venv` 70 | 71 | 4. Create new `venv`: `python3 -m venv venv` 72 | 73 | 5. Enter new `venv`: `source venv/bin/activate` 74 | 75 | 6. `pip` install requirements: `pip install -r requirements.txt` 76 | 77 | 7. Run the application: `gunicorn -w 4 -b 0.0.0.0:9000 'app:app'`, or season this command to taste for how you want to bind, including your preferred port number for listening. 78 | 79 | You can then start using `netbox-event-driven-automation-flask-app` from NetBox! 80 | 81 | Granted, there are myriad ways to do this, including using NGINX to proxy connections to this Flask application, but that's an exercise that's left up to the reader for the time being. 82 | 83 | 84 | 85 | 86 | -------------------------------------------------------------------------------- /docs/images/awx-ansible-build-ee.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/netboxlabs/netbox-proxmox-automation/125affc4819e5dc8e1fc2b20950dc2d38c44d957/docs/images/awx-ansible-build-ee.png -------------------------------------------------------------------------------- /docs/images/awx-create-project.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/netboxlabs/netbox-proxmox-automation/125affc4819e5dc8e1fc2b20950dc2d38c44d957/docs/images/awx-create-project.png -------------------------------------------------------------------------------- /docs/images/awx-default-inventory.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/netboxlabs/netbox-proxmox-automation/125affc4819e5dc8e1fc2b20950dc2d38c44d957/docs/images/awx-default-inventory.png -------------------------------------------------------------------------------- /docs/images/awx-execution-environment-tree.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/netboxlabs/netbox-proxmox-automation/125affc4819e5dc8e1fc2b20950dc2d38c44d957/docs/images/awx-execution-environment-tree.png -------------------------------------------------------------------------------- /docs/images/awx-execution-environment-yml.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/netboxlabs/netbox-proxmox-automation/125affc4819e5dc8e1fc2b20950dc2d38c44d957/docs/images/awx-execution-environment-yml.png -------------------------------------------------------------------------------- /docs/images/awx-execution-environment.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/netboxlabs/netbox-proxmox-automation/125affc4819e5dc8e1fc2b20950dc2d38c44d957/docs/images/awx-execution-environment.png -------------------------------------------------------------------------------- /docs/images/awx-netbox-proxmox-credentials.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/netboxlabs/netbox-proxmox-automation/125affc4819e5dc8e1fc2b20950dc2d38c44d957/docs/images/awx-netbox-proxmox-credentials.png -------------------------------------------------------------------------------- /docs/images/awx-netbox-proxmox-creds.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/netboxlabs/netbox-proxmox-automation/125affc4819e5dc8e1fc2b20950dc2d38c44d957/docs/images/awx-netbox-proxmox-creds.png -------------------------------------------------------------------------------- /docs/images/awx-netbox-proxmox-template-clone-resources-edit.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/netboxlabs/netbox-proxmox-automation/125affc4819e5dc8e1fc2b20950dc2d38c44d957/docs/images/awx-netbox-proxmox-template-clone-resources-edit.png -------------------------------------------------------------------------------- /docs/images/awx-proxmox-clone-vm-and-set-resources.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/netboxlabs/netbox-proxmox-automation/125affc4819e5dc8e1fc2b20950dc2d38c44d957/docs/images/awx-proxmox-clone-vm-and-set-resources.png -------------------------------------------------------------------------------- /docs/images/awx-scm-credential-new.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/netboxlabs/netbox-proxmox-automation/125affc4819e5dc8e1fc2b20950dc2d38c44d957/docs/images/awx-scm-credential-new.png -------------------------------------------------------------------------------- /docs/images/netbox-awx-webhooks.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/netboxlabs/netbox-proxmox-automation/125affc4819e5dc8e1fc2b20950dc2d38c44d957/docs/images/netbox-awx-webhooks.png -------------------------------------------------------------------------------- /docs/images/netbox-favicon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/netboxlabs/netbox-proxmox-automation/125affc4819e5dc8e1fc2b20950dc2d38c44d957/docs/images/netbox-favicon.png -------------------------------------------------------------------------------- /docs/images/netbox-light-favicon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/netboxlabs/netbox-proxmox-automation/125affc4819e5dc8e1fc2b20950dc2d38c44d957/docs/images/netbox-light-favicon.png -------------------------------------------------------------------------------- /docs/images/netbox-proxmox-event-rules-awx.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/netboxlabs/netbox-proxmox-automation/125affc4819e5dc8e1fc2b20950dc2d38c44d957/docs/images/netbox-proxmox-event-rules-awx.png -------------------------------------------------------------------------------- /docs/images/netbox-proxmox-flask-app-webhook.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/netboxlabs/netbox-proxmox-automation/125affc4819e5dc8e1fc2b20950dc2d38c44d957/docs/images/netbox-proxmox-flask-app-webhook.png -------------------------------------------------------------------------------- /docs/images/netbox-proxmox-vm-create-and-set-resources-awx-webhook.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/netboxlabs/netbox-proxmox-automation/125affc4819e5dc8e1fc2b20950dc2d38c44d957/docs/images/netbox-proxmox-vm-create-and-set-resources-awx-webhook.png -------------------------------------------------------------------------------- /docs/images/proxmox-cluster-nodes-edit.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/netboxlabs/netbox-proxmox-automation/125affc4819e5dc8e1fc2b20950dc2d38c44d957/docs/images/proxmox-cluster-nodes-edit.png -------------------------------------------------------------------------------- /docs/images/proxmox-cluster-nodes-saved.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/netboxlabs/netbox-proxmox-automation/125affc4819e5dc8e1fc2b20950dc2d38c44d957/docs/images/proxmox-cluster-nodes-saved.png -------------------------------------------------------------------------------- /docs/images/proxmox-disk-storage-volume.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/netboxlabs/netbox-proxmox-automation/125affc4819e5dc8e1fc2b20950dc2d38c44d957/docs/images/proxmox-disk-storage-volume.png -------------------------------------------------------------------------------- /docs/images/proxmox-node.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/netboxlabs/netbox-proxmox-automation/125affc4819e5dc8e1fc2b20950dc2d38c44d957/docs/images/proxmox-node.png -------------------------------------------------------------------------------- /docs/images/proxmox-public-ssh-key.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/netboxlabs/netbox-proxmox-automation/125affc4819e5dc8e1fc2b20950dc2d38c44d957/docs/images/proxmox-public-ssh-key.png -------------------------------------------------------------------------------- /docs/images/proxmox-remove-vm-awx.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/netboxlabs/netbox-proxmox-automation/125affc4819e5dc8e1fc2b20950dc2d38c44d957/docs/images/proxmox-remove-vm-awx.png -------------------------------------------------------------------------------- /docs/images/proxmox-resize-vm-disk-awx-event-rule.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/netboxlabs/netbox-proxmox-automation/125affc4819e5dc8e1fc2b20950dc2d38c44d957/docs/images/proxmox-resize-vm-disk-awx-event-rule.png -------------------------------------------------------------------------------- /docs/images/proxmox-set-ip-config-and-ssh-key-awx-event-rule.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/netboxlabs/netbox-proxmox-automation/125affc4819e5dc8e1fc2b20950dc2d38c44d957/docs/images/proxmox-set-ip-config-and-ssh-key-awx-event-rule.png -------------------------------------------------------------------------------- /docs/images/proxmox-start-vm-awx.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/netboxlabs/netbox-proxmox-automation/125affc4819e5dc8e1fc2b20950dc2d38c44d957/docs/images/proxmox-start-vm-awx.png -------------------------------------------------------------------------------- /docs/images/proxmox-stop-vm-awx.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/netboxlabs/netbox-proxmox-automation/125affc4819e5dc8e1fc2b20950dc2d38c44d957/docs/images/proxmox-stop-vm-awx.png -------------------------------------------------------------------------------- /docs/images/proxmox-vm-active-awx-event-rule.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/netboxlabs/netbox-proxmox-automation/125affc4819e5dc8e1fc2b20950dc2d38c44d957/docs/images/proxmox-vm-active-awx-event-rule.png -------------------------------------------------------------------------------- /docs/images/proxmox-vm-add-disk-awx-event-rule.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/netboxlabs/netbox-proxmox-automation/125affc4819e5dc8e1fc2b20950dc2d38c44d957/docs/images/proxmox-vm-add-disk-awx-event-rule.png -------------------------------------------------------------------------------- /docs/images/proxmox-vm-add-disk-awx.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/netboxlabs/netbox-proxmox-automation/125affc4819e5dc8e1fc2b20950dc2d38c44d957/docs/images/proxmox-vm-add-disk-awx.png -------------------------------------------------------------------------------- /docs/images/proxmox-vm-add-disk.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/netboxlabs/netbox-proxmox-automation/125affc4819e5dc8e1fc2b20950dc2d38c44d957/docs/images/proxmox-vm-add-disk.png -------------------------------------------------------------------------------- /docs/images/proxmox-vm-assign-ip-address-awx.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/netboxlabs/netbox-proxmox-automation/125affc4819e5dc8e1fc2b20950dc2d38c44d957/docs/images/proxmox-vm-assign-ip-address-awx.png -------------------------------------------------------------------------------- /docs/images/proxmox-vm-configure-ipconfig0-and-ssh-key-awx.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/netboxlabs/netbox-proxmox-automation/125affc4819e5dc8e1fc2b20950dc2d38c44d957/docs/images/proxmox-vm-configure-ipconfig0-and-ssh-key-awx.png -------------------------------------------------------------------------------- /docs/images/proxmox-vm-create-and-set-resources-awx-event-rule.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/netboxlabs/netbox-proxmox-automation/125affc4819e5dc8e1fc2b20950dc2d38c44d957/docs/images/proxmox-vm-create-and-set-resources-awx-event-rule.png -------------------------------------------------------------------------------- /docs/images/proxmox-vm-created.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/netboxlabs/netbox-proxmox-automation/125affc4819e5dc8e1fc2b20950dc2d38c44d957/docs/images/proxmox-vm-created.png -------------------------------------------------------------------------------- /docs/images/proxmox-vm-delete-disk.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/netboxlabs/netbox-proxmox-automation/125affc4819e5dc8e1fc2b20950dc2d38c44d957/docs/images/proxmox-vm-delete-disk.png -------------------------------------------------------------------------------- /docs/images/proxmox-vm-deleted.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/netboxlabs/netbox-proxmox-automation/125affc4819e5dc8e1fc2b20950dc2d38c44d957/docs/images/proxmox-vm-deleted.png -------------------------------------------------------------------------------- /docs/images/proxmox-vm-offline-awx-event-rule.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/netboxlabs/netbox-proxmox-automation/125affc4819e5dc8e1fc2b20950dc2d38c44d957/docs/images/proxmox-vm-offline-awx-event-rule.png -------------------------------------------------------------------------------- /docs/images/proxmox-vm-remove-awx-event-rule.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/netboxlabs/netbox-proxmox-automation/125affc4819e5dc8e1fc2b20950dc2d38c44d957/docs/images/proxmox-vm-remove-awx-event-rule.png -------------------------------------------------------------------------------- /docs/images/proxmox-vm-remove-disk-awx-event-rule.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/netboxlabs/netbox-proxmox-automation/125affc4819e5dc8e1fc2b20950dc2d38c44d957/docs/images/proxmox-vm-remove-disk-awx-event-rule.png -------------------------------------------------------------------------------- /docs/images/proxmox-vm-remove-disk-awx.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/netboxlabs/netbox-proxmox-automation/125affc4819e5dc8e1fc2b20950dc2d38c44d957/docs/images/proxmox-vm-remove-disk-awx.png -------------------------------------------------------------------------------- /docs/images/proxmox-vm-resize-disk-awx.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/netboxlabs/netbox-proxmox-automation/125affc4819e5dc8e1fc2b20950dc2d38c44d957/docs/images/proxmox-vm-resize-disk-awx.png -------------------------------------------------------------------------------- /docs/images/proxmox-vm-resize-disk.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/netboxlabs/netbox-proxmox-automation/125affc4819e5dc8e1fc2b20950dc2d38c44d957/docs/images/proxmox-vm-resize-disk.png -------------------------------------------------------------------------------- /docs/images/proxmox-vm-resize-os-disk-awx-event-rule.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/netboxlabs/netbox-proxmox-automation/125affc4819e5dc8e1fc2b20950dc2d38c44d957/docs/images/proxmox-vm-resize-os-disk-awx-event-rule.png -------------------------------------------------------------------------------- /docs/images/proxmox-vm-started.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/netboxlabs/netbox-proxmox-automation/125affc4819e5dc8e1fc2b20950dc2d38c44d957/docs/images/proxmox-vm-started.png -------------------------------------------------------------------------------- /docs/images/proxmox-vm-stopped.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/netboxlabs/netbox-proxmox-automation/125affc4819e5dc8e1fc2b20950dc2d38c44d957/docs/images/proxmox-vm-stopped.png -------------------------------------------------------------------------------- /docs/images/proxmox-vm-storage-edit.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/netboxlabs/netbox-proxmox-automation/125affc4819e5dc8e1fc2b20950dc2d38c44d957/docs/images/proxmox-vm-storage-edit.png -------------------------------------------------------------------------------- /docs/images/proxmox-vm-storage-saved.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/netboxlabs/netbox-proxmox-automation/125affc4819e5dc8e1fc2b20950dc2d38c44d957/docs/images/proxmox-vm-storage-saved.png -------------------------------------------------------------------------------- /docs/images/proxmox-vm-storage.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/netboxlabs/netbox-proxmox-automation/125affc4819e5dc8e1fc2b20950dc2d38c44d957/docs/images/proxmox-vm-storage.png -------------------------------------------------------------------------------- /docs/images/proxmox-vm-template.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/netboxlabs/netbox-proxmox-automation/125affc4819e5dc8e1fc2b20950dc2d38c44d957/docs/images/proxmox-vm-template.png -------------------------------------------------------------------------------- /docs/images/proxmox-vm-templates-edit.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/netboxlabs/netbox-proxmox-automation/125affc4819e5dc8e1fc2b20950dc2d38c44d957/docs/images/proxmox-vm-templates-edit.png -------------------------------------------------------------------------------- /docs/images/proxmox-vm-templates-saved.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/netboxlabs/netbox-proxmox-automation/125affc4819e5dc8e1fc2b20950dc2d38c44d957/docs/images/proxmox-vm-templates-saved.png -------------------------------------------------------------------------------- /docs/images/proxmox-vm-update-network-config.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/netboxlabs/netbox-proxmox-automation/125affc4819e5dc8e1fc2b20950dc2d38c44d957/docs/images/proxmox-vm-update-network-config.png -------------------------------------------------------------------------------- /docs/images/proxmox-vm-updated.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/netboxlabs/netbox-proxmox-automation/125affc4819e5dc8e1fc2b20950dc2d38c44d957/docs/images/proxmox-vm-updated.png -------------------------------------------------------------------------------- /docs/images/proxmox-vmid.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/netboxlabs/netbox-proxmox-automation/125affc4819e5dc8e1fc2b20950dc2d38c44d957/docs/images/proxmox-vmid.png -------------------------------------------------------------------------------- /docs/index.md: -------------------------------------------------------------------------------- 1 | # NetBox Proxmox Automation 2 | 3 | [NetBox](https://github.com/netbox-community/netbox) is the world's most popular Network Source of Truth (NSoT) with tens of thousands of installations globally. It is used extensively for documenting/modeling networks (network devices, virtual machines, etc), and also provides a great IPAM solution. [Proxmox](https://www.proxmox.com/en/) is a freely available virtualization technology that allows you to deploy virtual machines (VMs) at scale, and perhaps in a clustered configuration. Proxmox has approximately [900,000 hosts and more than 130,000 users in its open source community](https://www.proxmox.com/en/about/press-releases/proxmox-virtual-environment-8-1). 4 | 5 | When you think of the challenges of a widely used network documentation solution and a widely used virtualization technology, this implementation is an integration between virtual machine documentation (NetBox) and the automation of Proxmox virtual machine (VM) and container (LXC) deployment and configuration. 6 | 7 | This automation handles creation, removal, and changes of/to Proxmox VMs *and* LXCs. The underlying automation uses [webhooks](https://demo.netbox.dev/static/docs/additional-features/webhooks/) and [event rules](https://netboxlabs.com/docs/netbox/en/stable/features/event-rules/) in NetBox. When you induce a change in NetBox, this will set the desired VM state(s) in Proxmox. 8 | 9 | When you create/update/delete VM objects (for Proxmox VM) in NetBox, the following will take place in Proxmox: 10 | 11 | - when you create a VM object in NetBox (name, status == Staged, VM Type == Virtual Machine, chosen Proxmox VM template name), this will clone a VM in Proxmox of the same name, from the defined template 12 | - when you add a SSH key to a NetBox VM object (status == Staged), a SSH key will be added to the VM settings in Proxmox 13 | - when you add a primary IP address to a NetBox VM object (status == Staged), this will update the VM settings in Proxmox for ipconfig0 14 | - when you add or resize VM disks (scsi0 - scsiN) for a NetBox VM object (status == Staged), this will: 15 | - resize scsi0 on the Proxmox VM to the size that was defined in NetBox 16 | - create scsi1 - scsiN on the Proxmox VM and set them to their specified sizes 17 | - resize scsi1 - scsiN on the Proxmox VM and resize them to their specified sizes (*NOTE: Proxmox does not allow you to shrink disks!*) 18 | - when you remove a disk or disks from a NetBox VM object, this will remove the corresponding disks from the Proxmox VM (*NOTE: this does not include scsi0 as that is the OS disk*) 19 | 20 | Further: 21 | 22 | - when you set a VM's state to 'Active' in NetBox, this will start a VM in Proxmox 23 | - when you set a VM's state to 'Offline' in NetBox, this still stop a VM in Proxmox 24 | - when you remove a VM from NetBox, this will stop and remove a VM in Proxmox. 25 | 26 | When you create/update/delete VM objects (for Proxmox LXC) in NetBox, the following will take place in Proxmox: 27 | 28 | - when you create a VM object in NetBox (name, status == Staged, VM Type == LXC Container, chosen Proxmox VM template name, defined public SSH key), this will clone a LXC in Proxmox of the same name, from the defined template, and will also set the public SSH key (*NOTE: In LXC, you can only set the public SSH key once, and that's during the cloning process!*) 29 | - when you add a primary IP address to a NetBox VM object (status == Staged), this will update the VM settings in Proxmox for netif for the LXC 30 | - when you add or resize the VM disk (rootfs) for a NetBox VM object (status == Staged), this will: 31 | - resize rootfs on the Proxmox LXC to the size that was defined in NetBox 32 | 33 | Further: 34 | 35 | - when you set a VM's state to 'Active' in NetBox, this will start a LXC in Proxmox 36 | - when you set a VM's state to 'Offline' in NetBox, this still stop a LXC in Proxmox 37 | - when you remove a VM from NetBox, this will stop and remove a LXC in Proxmox. 38 | -------------------------------------------------------------------------------- /docs/netbox-customization.md: -------------------------------------------------------------------------------- 1 | # NetBox Customization 2 | 3 | You will need to do some customization to NetBox to define the underlying Proxmox VM configuration(s). This section covers the custom field choices and custom fields that you'll need to create in NetBox -- in order for you to facilitate automation. 4 | 5 | ### Custom Field Choices (for Proxmox VM and LXC automation) 6 | 7 | You will need to perform some customization in NetBox before you can start automating Proxmox VMs and LXCs. 8 | 9 | #### Automated NetBox Objects and the Creation of Custom Field Choices and Custom Fields 10 | 11 | `netbox-proxmox-automation` version 1.1.0 and newer ships with a convenience script, `netbox_setup_objects_and_custom_fields.py`, that when used alongside a configuration file of your choice, will greatly simplify this process. In the case of AWX/Tower/AAP, `netbox_setup_objects_and_custom_fields.py` will query your AWX/Tower/AAP instance for project and template(s) information; this information will then be used to create the corresponding webhooks and event rules in NetBox. 12 | 13 | There exists a sample configuration file called `netbox_setup_objects.yml-sample` under the conf.d directory of this git repository. Copy this file to a location of your choice, and season it to taste. In the end you should have a configuration that looks something like this. 14 | 15 | ``` 16 | proxmox_api_config: 17 | api_host: proxmox-ip-or-hostname 18 | api_port: 8006 19 | api_user: proxmox_api_user 20 | api_token_id: name_of_proxmox_api_token 21 | api_token_secret: proxmox_api_secret_token 22 | verify_ssl: false 23 | netbox_api_config: 24 | api_proto: http # or https 25 | api_host: name or ip of NetBox host 26 | api_port: 8000 27 | api_token: netbox_api_secret_token 28 | verify_ssl: false # or true, up to you 29 | proxmox: 30 | cluster_name: proxmox-ve 31 | netbox: 32 | cluster_role: Proxmox 33 | vm_role: "Proxmox VM" 34 | ``` 35 | 36 | Usage: 37 | 38 | ``` 39 | shell$ cd setup 40 | 41 | shell$ pwd 42 | /some/path/netbox-proxmox-automation/setup 43 | 44 | shell$ python3 -m venv venv 45 | 46 | shell$ source venv/bin/activate 47 | 48 | (venv) shell$ pip install -r requirements.txt 49 | 50 | (venv) shell$ ./netbox_setup_objects_and_custom_fields.py --config /path/to/your/configuration.yml 51 | ``` 52 | 53 | Then verify that everything has been created. In the end, you should have Custom Fields for the following. 54 | 55 | - proxmox_node: 56 | - Object types: Virtual Machine 57 | - Label: Proxmox node 58 | - Group name: Proxmox (common) 59 | - Type: Selection 60 | - proxmox_vm_type: 61 | - Object types: Virtual Machine 62 | - Label: Proxmox VM Type 63 | - Group name: Proxmox (common) 64 | - Type: Selection 65 | - proxmox_vmid: 66 | - Object types: Virtual Machine 67 | - Label: Proxmox Virtual machine ID (vmid) 68 | - Group name: Proxmox (common) 69 | - Type: Text 70 | - proxmox_vm_storage: 71 | - Object types: Virtual Machine 72 | - Label: Proxmox VM Storage 73 | - Group name: Proxmox (common) 74 | - Type: Selection 75 | - proxmox_public_ssh_key: 76 | - Object types: Virtual Machine 77 | - Label: Proxmox public SSH key 78 | - Group name: Proxmox (common) 79 | - Type: Text (long) 80 | - proxmox_lxc_templates: 81 | - Object types: Virtual Machine 82 | - Label: Proxmox LXC Templates 83 | - Group name: Proxmox LXC 84 | - Type: Selection 85 | - proxmox_disk_storage_volume: 86 | - Object types: Virtual Disk 87 | - Label: Proxmox Disk Storage Volume 88 | - Group name: Proxmox VM 89 | - Type: Selection 90 | - proxmox_vm_templates: 91 | - Object types: Virtual Machine 92 | - Label: Proxmox VM Templates 93 | - Group name: Proxmox VM 94 | - Type: Selection 95 | 96 | And you have Custom Field Choices for the following: 97 | 98 | - proxmox-cluster-nodes: used by `proxmox_node` custom field 99 | - Choices: available Proxmox nodes 100 | - Default: first "discovered" Proxmox node 101 | - proxmox-lxc-templates: used by `proxmox_lxc_templates` custom field 102 | - Choices: "discovered" available Proxmox LXC images 103 | - Default: first "discovered" Proxmox LXC image 104 | - proxmox-vm-storage: used by `proxmox_vm_storage` custom field 105 | - Choices: "discovered" Proxmox storage volumes 106 | - Default: first "discovered" Proxmox storage volume 107 | - proxmox-vm-templates: used by `proxmox_vm_templates` custom field 108 | - Choices: "discovered" Proxmox VM templates 109 | - Default: first "discovered" Proxmox VM template, based on lowest discovered vmid 110 | - proxmox-vm-type: used by `proxmox_vm_type` 111 | - Choices: Virtual Machine, LXC Container 112 | - Default: Virtual Machine 113 | 114 | 115 | -------------------------------------------------------------------------------- /docs/netbox-event-rules-and-webhooks-awx-aap.md: -------------------------------------------------------------------------------- 1 | # Initial Configuration: NetBox Event Rules and Webhooks (AWX/Tower/AAP) 2 | 3 | There are two key components to automating Proxmox VM management in NetBox. 4 | 5 | 1. webhooks 6 | 2. event rules 7 | 8 | A webhook in NetBox will consume the payload of data from an event rule. An event rule announces changes to an object type inside of NetBox (in this case, a Virtual Machine and its related object types) -- then sends the payload of data around those changes to a webhook. The webhook will handle the Proxmox automation(s) as you've defined it/them. 9 | 10 | For the sake of automation, every event rule that you create in NetBox requires either a Webhook or a Script. 11 | 12 | Regardless of whether you are using a Flask (or other) application for Proxmox automation, or you are using AWX/Tower/AAP, this automation should trigger anytime that a Proxmox VM is changed in NetBox such that: 13 | 14 | - a Proxmox VM has been created in NetBox with a status of 'Staged' 15 | - a Proxmox VM in NetBox (with a status of 'Staged') has a changed network configuration 16 | - a Proxmox VM in NetBox (with a status of 'Staged') adds new disks 17 | - a Proxmox VM in NetBox (with a status of 'Staged') has a changed disk configuration 18 | - a Proxmox VM in NetBox has been set to a status of 'Active' 19 | - a Proxmox VM in NetBox has been set to a status of 'Offline' 20 | - a Proxmox VM in NetBox has been removed 21 | 22 | 23 | ### AWX/Tower/AAP 24 | 25 | As noted earlier, AWX/Tower/AAP will perform Proxmox automation through separate (job) templates. This section walks you through how (NetBox) webhooks and (NetBox) event rules are handled by AWX. 26 | 27 | #### Automated Webhook and Event Rules Configuration 28 | 29 | If you'd prefer to manually create the webhook and event rules in NetBox, you can skip to the next section. Otherwise, proceed with the following to automate the creation of the webhook and event rules in NetBox. 30 | 31 | `netbox-proxmox-automation` version 1.1.0 and newer ships with a convenience script, `netbox_setup_webhook_and_event_rules.py`, that when used alongside a configuration file of your choice, will greatly simplify this process. In the case of AWX/Tower/AAP, `netbox_setup_webhook_and_event_rules.py` will query your AWX/Tower/AAP instance for project and template(s) information; this information will then be used to create the corresponding webhooks and event rules in NetBox. 32 | 33 | There exists a sample configuration file called `netbox_setup_objects.yml-sample` under the conf.d directory of this git repository. Copy this file to a location of your choice, and season it to taste. In the end you should have a configuration that looks something like this. 34 | 35 | ``` 36 | proxmox_api_config: 37 | api_host: proxmox-ip-or-hostname 38 | api_port: 8006 39 | api_user: proxmox_api_user 40 | api_token_id: name_of_proxmox_api_token 41 | api_token_secret: proxmox_api_secret_token 42 | verify_ssl: false 43 | netbox_api_config: 44 | api_proto: http # or https 45 | api_host: name or ip of NetBox host 46 | api_port: 8000 47 | api_token: netbox_api_secret_token 48 | verify_ssl: false # or true, up to you 49 | proxmox: 50 | cluster_name: proxmox-ve 51 | netbox: 52 | cluster_role: Proxmox 53 | vm_role: "Proxmox VM" 54 | automation_type: ansible_automation 55 | ansible_automation: 56 | host: name or ip of AWX/Tower/AAP host 57 | http_proto: http or https 58 | http_port: 80 or whatever 59 | ssl_verify: false # or true 60 | username: awx_user # should have permissions to view both projects and templates 61 | password: awx_password 62 | project_name: netbox-proxmox-ee-test1 # or whatever you named your project 63 | ``` 64 | 65 | Usage: 66 | 67 | ``` 68 | shell$ cd setup 69 | 70 | shell$ pwd 71 | /some/path/netbox-proxmox-automation/setup 72 | 73 | shell$ python3 -m venv venv 74 | 75 | shell$ source venv/bin/activate 76 | 77 | (venv) shell$ pip install -r requirements.txt 78 | 79 | (venv) shell$ ./netbox_setup_webhook_and_event_rules.py --config /path/to/your/configuration.yml 80 | ``` 81 | 82 | Then verify that everything has been created. 83 | -------------------------------------------------------------------------------- /docs/netbox-event-rules-and-webhooks-flask.md: -------------------------------------------------------------------------------- 1 | # Initial Configuration: NetBox Event Rules and Webhooks (Flask application) 2 | 3 | There are two key components to automating Proxmox VM management in NetBox. 4 | 5 | 1. webhooks 6 | 2. event rules 7 | 8 | A webhook in NetBox will consume the payload of data from an event rule. An event rule announces changes to an object type inside of NetBox (in this case, a Virtual Machine and its related object types) -- then sends the payload of data around those changes to a webhook. The webhook will handle the Proxmox automation(s) as you've defined it/them. 9 | 10 | For the sake of automation, every event rule that you create in NetBox requires either a Webhook or a Script. 11 | 12 | Regardless of whether you are using a Flask (or other) application for Proxmox automation, or you are using AWX/Tower/AAP, this automation should trigger anytime that a Proxmox VM is changed in NetBox such that: 13 | 14 | - a Proxmox VM has been created in NetBox with a status of 'Staged' 15 | - a Proxmox VM in NetBox (with a status of 'Staged') has a changed network configuration 16 | - a Proxmox VM in NetBox (with a status of 'Staged') adds new disks 17 | - a Proxmox VM in NetBox (with a status of 'Staged') has a changed disk configuration 18 | - a Proxmox VM in NetBox has been set to a status of 'Active' 19 | - a Proxmox VM in NetBox has been set to a status of 'Offline' 20 | - a Proxmox VM in NetBox has been removed 21 | 22 | 23 | ### Flask Application 24 | 25 | As noted [here](#initial-configuration-flask-application-python), you will need to have a running Flask application *before* you can start handling events (i.e. object changes) inside of NetBox. 26 | 27 | #### Automated Webhook and Event Rules Configuration 28 | 29 | If you'd prefer to manually create the webhook and event rules in NetBox, you can skip to the next section. Otherwise, proceed with the following to automate the creation of the webhook and event rules in NetBox. 30 | 31 | `netbox-proxmox-automation` version 1.1.0 and newer ships with a convenience script, `netbox_setup_webhook_and_event_rules.py`, that when used alongside a configuration file of your choice, will greatly simplify this process. 32 | 33 | There exists a sample configuration file called `netbox_setup_objects.yml-sample` under the conf.d directory of this git repository. Copy this file to a location of your choice, and season it to taste. In the end you should have a configuration that looks something like this. 34 | 35 | ``` 36 | proxmox_api_config: 37 | api_host: proxmox-ip-or-hostname 38 | api_port: 8006 39 | api_user: proxmox_api_user 40 | api_token_id: name_of_proxmox_api_token 41 | api_token_secret: proxmox_api_secret_token 42 | verify_ssl: false 43 | netbox_api_config: 44 | api_proto: http # or https 45 | api_host: name or ip of NetBox host 46 | api_port: 8000 47 | api_token: netbox_api_secret_token 48 | verify_ssl: false # or true, up to you 49 | proxmox: 50 | cluster_name: proxmox-ve 51 | netbox: 52 | cluster_role: Proxmox 53 | vm_role: "Proxmox VM" 54 | automation_type: flask_application 55 | flask_application: 56 | host: name or ip of where Flask application is running 57 | http_proto: http or https 58 | http_port: 9000 or whatever 59 | ssl_verify: false # or true 60 | netbox_webhook_name: "netbox-proxmox-webhook" 61 | ``` 62 | 63 | Usage: 64 | 65 | ``` 66 | shell$ cd setup 67 | 68 | shell$ pwd 69 | /some/path/netbox-proxmox-automation/setup 70 | 71 | shell$ python3 -m venv venv 72 | 73 | shell$ source venv/bin/activate 74 | 75 | (venv) shell$ pip install -r requirements.txt 76 | 77 | (venv) shell$ ./netbox_setup_webhook_and_event_rules.py --config /path/to/your/configuration.yml 78 | ``` 79 | 80 | Then verify that everything has been created. In the end, you should see a singular webhook and a series of event rules. 81 | 82 | -------------------------------------------------------------------------------- /docs/netbox-ipam.md: -------------------------------------------------------------------------------- 1 | # NetBox IPAM 2 | 3 | Given the heirarchical nature of NetBox, you will need to create the following objects before using `netbox-proxmox-automation` automation. You should refer to the [NetBox planning guide](https://netboxlabs.com/docs/netbox/en/stable/getting-started/planning/) to address these dependencies before proceeding with `netbox-proxmox-automation`. 4 | 5 | Using NetBox's IPAM is a *requirement* of `netbox-proxmox-automation`. This is because `netbox-proxmox-automation` is going to either assign a defined IP address to a specified interface (or interfaces) on a Proxmox VM, or it's going to request an available IP address from NetBox's IPAM -- and assign the requested IP address to an interface (or interfaces) on a Proxmox VM. 6 | 7 | Ahead of using this automation, make sure to create the following IPAM-related objects in NetBox: 8 | 9 | - IPAM > RIRs 10 | - IPAM > Aggregates (relate each aggregate to an RIR) 11 | - IPAM > Prefixes (use containers and set Active state for each active prefix) 12 | -------------------------------------------------------------------------------- /docs/netbox-key-and-permissions.md: -------------------------------------------------------------------------------- 1 | # NetBox Key and Permissions 2 | 3 | ## Initial Configuration: NetBox API user + key 4 | 5 | It is recommended that you do *not* create an API token for the NetBox 'admin' user. Instead, create a new user in NetBox; then create a new permission for that API user -- that has sufficient read/write/modify permissions to modify the following object types in NetBox, at a minimum: 6 | 7 | - Devices (for VM cluster(s) hardware, if used) 8 | - Interfaces (devices and VMs) 9 | - VMs (groups, clusters, VMs) 10 | - VM disks 11 | 12 | ### Create NetBox User + Group 13 | 14 | In the NetBox UI: 15 | 16 | 1. Navigate to Admin > Users 17 | 2. Create a new user called `api_user`, or a user of your choice 18 | 3. Navigate to Admin > Groups 19 | 4. Create a new group called `api_user_group`, or a group of your choice 20 | 5. Navigate to Admin > Users, select `api_user` (or the user that you created), click the Edit button, and associate `api_user` with the group that you just created. 21 | 22 | ### Create NetBox Permissions 23 | 24 | In the Netbox UI: 25 | 26 | 1. Navigate to Admin > Permissions 27 | 2. Create a new permission called `api_user_permissions` (or whatever you want to call it) and ensure that this permission has read/write/update/delete rights for the object types, at a minimum, noted above. Associate the user and/or group with the permission that you've created. 28 | 29 | ### Create NetBox API Token 30 | 31 | While it is possible to use passwords with the Netbox Ansible collection, `netbox-proxmox-automation` does not allow this behavior. Instead a NetBox API token is required. 32 | 33 | In the NetBox UI: 34 | 35 | 1. Navigate to Admin > API Tokens 36 | 2. Add a new token, associating it with `api_user`, with the following characteristics: Write enabled (you can select other characteristics if you wish) 37 | 38 | Once you've created a NetBox API token, store it some place safe in the meantime; (most) NetBox installations will obscure the API token once it's been created. 39 | -------------------------------------------------------------------------------- /docs/proxmox-api-user-and-key.md: -------------------------------------------------------------------------------- 1 | # Proxmox API user + key 2 | 3 | While the Proxmox implementation that's part of the Ansible community.general collection allows you to use passwords when doing Proxmox automation, `netbox-proxmox-automation` does not allow this behavior. 4 | 5 | It is recommended that you do *not* create an API token for the Proxmox 'root' user. Instead, create an `api_user` and an API token. Then assign the required permissions to the `api_user`. This procedure uses a combination of the Proxmox UI and the command line. You need to be able to access Proxmox via and UI and SSH and become the 'root' user. You will need to create an `api_user` in Proxmox, an API token, and set the requisite permissions in Proxmox so that the `api_user` can: 6 | 7 | - Connect to Proxmox through the API 8 | - Create, remove, modify Proxmox VMs 9 | - Access (CRUD) Proxmox storage 10 | 11 | 1. Create `api_user` via the Proxmox UI 12 | - Login to the Proxmox UI (typically as root@pam or as a user with the equivalent permissions) 13 | - Navigate to 'Datacenter' in top left corner of the UI 14 | - Expand the 'Permissions' pane in the center of the UI 15 | - Navigate to Permissions > Users in the center of the UI 16 | - Click Users 17 | - Click Add 18 | - Make sure that the 'Advanced' checkbox is checked 19 | - Create a user called `api_user` (or whatever you want to call it) 20 | - The realm should be set to `pve` or 'Proxmox VE Authentication server' (i.e. do not create a user account and use PAM for authentication) 21 | - Group should be set to `admins` 22 | - Make sure that 'enabled' is checked 23 | - Make sure that 'expire' is set to 'never' 24 | - Do *not* set a password for the user, unless it is required 25 | - Click OK 26 | 2. Create `api_token` via the Proxmox UI 27 | - Navigate to Permissions > API Tokens in the center of the UI 28 | - Click the Add button 29 | - Select User from the drop down menu 30 | - Give the token ID a name 31 | - Uncheck Privilege Separation 32 | - Make sure that Expire is set to 'never' 33 | - Click OK 34 | - This is the *only* time that your Proxmox API token will be shown in clear text. Please store it in a safe space, as it will be required when configuring `secrets.yml` in the next section of this document. 35 | 3. Login to the Proxmox node via SSH 36 | - Become root: `sudo su -` 37 | - Add `api_user` to the correct role. For example: `pveum acl modify / -user api_user@pve -role Administrator` 38 | - If 'Administrator' is too broad a role, you can show which roles might be more amenable permissions wise: `pvesh get /access/roles --output-format yaml`. Then select a more appropriate role for `api_user`, with the understanding that `api_user` needs full access to manage VMs and CRUD access to the underlying storage. 39 | 40 | If you want to do everything, as noted above, on the Proxmox (SSH) command line, as root, the procedure would look like this: 41 | 42 | ``` 43 | proxmox-ve-shell# pveum user add api_user@pve --comment "Proxmox API User" --enable 1 --groups admin --expire 0 # create api_user@pve, enabled, non expiring, assigned to admin group 44 | 45 | proxmox-ve-shell# pveum user token add api_user@pve api_user_token -privsep 0 # create API token for api_user with the name of api_user_token, and disable privilege separation 46 | 47 | proxmox-ve-shell# pveum acl modify / -user api_user@pve -role Administrator # allow api_user@pve to access everything -- given Administrator role rights 48 | ``` 49 | -------------------------------------------------------------------------------- /docs/proxmox-discover-vm-and-lxc.md: -------------------------------------------------------------------------------- 1 | # Discover Proxmox VMs and LXCs and Adding Objects to Netbox 2 | 3 | Starting with `netbox-proxmox-automation` 1.2.0, you are able to "discover" Proxmox VMs and LXCs and automatically create the related Virtual Machine objects in NetBox. This is done by using a Proxmox API token to query the virtualization inventory, and based on VM category (Virtual Machines and LXC Containers), collecting everything that we know about virtualization objects in Proxmox -- then creating the related VM objects in NetBox. 4 | 5 | *Note that you must follow the steps in [this document](./netbox-customization.md) before running this convenience script!* 6 | 7 | `./setup/netbox-discovery-tool.py` was created so that you can document everything that you have in Proxmox in NetBox, but with the understanding that your future pattern will be to induce changes in Proxmox from the "intent" (i.e. desired state) that you've set in NetBox. 8 | `./setup/netbox-discovery-tool.py` was not created to implement a perpetual "discovery" process against the inventory that lives in Proxmox. It's meant to be a starting point in your automation journey. 9 | 10 | `netbox-proxmox-automation` has a sample config file under the `conf.d` directory that's called `netbox_setup_objects.yml-sample`. Make a copy of `netbox_setup_objects.yml-sample` to the location of your choice. Then run `./setup/netbox-discovery-tool.py` as follows. 11 | 12 | ``` 13 | shell$ cd /path/to/netbox-proxmox-automation/setup 14 | 15 | shell$ deactivate 16 | 17 | shell$ python3 -m venv venv 18 | 19 | shell$ source venv/bin/activate 20 | 21 | shell$ pip install -r requirements.txt 22 | ``` 23 | 24 | ## To Discover Proxmox VMs 25 | 26 | Follow the steps above, then: `./netbox-discovery-tool.py vm --config /path/to/your-config.yml` 27 | 28 | ## To Discover Proxmox LXCs 29 | 30 | Follow the steps above, then: `./netbox-discovery-tool.py lxc --config /path/to/your-config.yml` 31 | -------------------------------------------------------------------------------- /docs/proxmox-lxc-templates.md: -------------------------------------------------------------------------------- 1 | # Proxmox LXC Templates 2 | 3 | `netbox-proxmox-automation` is intended to make your life as simple as possible. Once you have a working Proxmox node (or cluster), have provisioned a Proxmox API token (the permission is able to manage both VMs and storage), a NetBox instance, a NetBox API token, the entire process of managing Proxmox VMs via NetBox involves these simple requirements. 4 | 5 | 1. You have defined a webhook that will be used to facilitate your automation 6 | 2. You have defined an event rule that uses the webhook in Step 1 -- for automating Proxmox VM operations based on VM state in NetBox 7 | 3. You have a web application that handles events via webhooks 8 | 9 | - [netbox-event-driven-automation-flask-app](https://github.com/netboxlabs/netbox-proxmox-automation/tree/main/netbox-event-driven-automation-flask-app) is a web application that you can use to facilitate Proxmox automation by handling event rules from NetBox. 10 | 11 | *-or-* 12 | 13 | - You are running AWX and have created (job) templates to handle events via webhooks 14 | 15 | 16 | ## Initial Configuration: Working with LXC (Linux Container) in Proxmox 17 | 18 | This is documented [here](https://pve.proxmox.com/wiki/Linux_Container). *Before you can use `netbox-proxmox-automation`, you must have downloaded at least one LXC container, as documented in the previous link, on your Proxmox node.* 19 | 20 | -------------------------------------------------------------------------------- /docs/proxmox-vm-templates.md: -------------------------------------------------------------------------------- 1 | # Proxmox VM Templates 2 | 3 | `netbox-proxmox-automation` is intended to make your life as simple as possible. Once you have a working Proxmox node (or cluster), have provisioned a Proxmox API token (the permission is able to manage both VMs and storage), a NetBox instance, a NetBox API token, the entire process of managing Proxmox VMs via NetBox involves these simple requirements. 4 | 5 | 1. You have defined a webhook that will be used to facilitate your automation 6 | 2. You have defined an event rule that uses the webhook in Step 1 -- for automating Proxmox VM operations based on VM state in NetBox 7 | 3. You have a web application that handles events via webhooks 8 | 9 | - [netbox-event-driven-automation-flask-app](https://github.com/netboxlabs/netbox-proxmox-automation/tree/main/netbox-event-driven-automation-flask-app) is a web application that you can use to facilitate Proxmox automation by handling event rules from NetBox. 10 | 11 | *-or-* 12 | 13 | - You are running AWX and have created (job) templates to handle events via webhooks 14 | 15 | 16 | ## Initial Configuration: Creating Proxmox VM templates from (cloud-init) images 17 | 18 | For Proxmox Virtual Machine automation, `netbox-proxmox-automation` *only* supports cloud-init images. The dynamic nature of Proxmox VM automation requires this implementation to be able to set things, during the Proxmox VM provisioning process, like network configuration, hostnames, and more. While it's *possible* that `netbox-proxmox-automation` *might* support your existing Proxmox VM templates, it's *highly* recommended that you follow the procedure below -- for the best results. 19 | 20 | As a cloud-init image is sufficient "bare bones", meaning that there is not broad network or SSH key or package configuration(s), this allows us to have total flexibility in the way that this automation takes a *desired* Proxmox VM state from NetBox and generates anticipated changes to VMs -- in Proxmox. 21 | 22 | This process is [well documented](https://pve.proxmox.com/wiki/Cloud-Init_Support) by the Proxmox team. In the end it comes down to: 23 | - logging into your Proxmox node(s) and running these commands as the 'root' user, or as a user who has adequate permissions to modify Proxmox VMs and the underlying storage 24 | - downloading a cloud image 25 | - following the documented process in the previous link 26 | - ensuring that your cloud-init image has included `qemu-guest-agent`; `qemu-guest-agent` is *required* for the discovery of network interfaces/settings and underlying platform information 27 | - converting your cloud-init image to a Proxmox VM template 28 | 29 | The automated VM cloning and configuration process will handle IP allocation/configuration, host naming, ssh key configuration and more. The default user for an Ubuntu cloud image is always 'ubuntu'; please refer to the documentation (about default users) for other cloud images. 30 | 31 | The first step before modifying cloud-init images, obviously, is to download the cloud-init images. Here's an example that will download the jammy, focal, and noble releases of Ubuntu, in parallel, as root, to a Proxmox node. 32 | 33 | ``` 34 | proxmox-ve-node# cd /root 35 | 36 | proxmox-ve-node# mkdir -p cloud-images/ubuntu 37 | 38 | proxmox-ve-node# cd cloud-images/ubuntu 39 | 40 | proxmox-ve-node# for r in jammy focal noble; do wget "https://cloud-images.ubuntu.com/${r}/current/${r}-server-cloudimg-amd64.img" & done 41 | ``` 42 | 43 | Then wait for the cloud-init images to download. Once the downloads have completed, you might want to take backups of the original cloud-init images -- as we will proceed with modifying these cloud-init images slightly before converting them to Proxmox VM templates. Taking backups of the original cloud-init images is helpful should you ever need to revert any customization you did before converting the cloud-init images into Proxmox VM templates. Run this, again, as 'root' on the proxmox-node of your choice. 44 | 45 | ``` 46 | proxmox-ve-node# cd /root/cloud-images/ubuntu 47 | 48 | proxmox-ve-node# for img in `ls -1 *img`; do cp -pi $img $img.$(date +%Y-%m-%d); done 49 | ``` 50 | 51 | Now let's start preparing our cloud-init images so that we can convert them to Proxmox VM templates. We'll use the Ubuntu ('jammy') cloud image to illustrate this process. You can use whatever cloud image you want to implement this process. 52 | 53 | You will need to install the `virt-customize` and `virt-sysprep` tools on your Proxmox node, as the 'root' user. These are part of the `guestfs-tools` package, which might or might not have been installed when you installed Proxmox initially. 54 | 55 | ``` 56 | proxmox-ve-node# apt-get update ; apt-get install guestfs-tools 57 | ``` 58 | 59 | Let's start working on the Ubuntu ('jammy') cloud-init image, as the 'root' user on our Proxmox node. You'll need to use `virt-customize` for this step. We are going to install `qemu-guest-agent` on the Ubuntu ('jammy') cloud-init image: `virt-customize -a jammy-server-cloudimg-amd64.img --install qemu-guest-agent` 60 | 61 | Output should be something like this if things have succeeded. 62 | 63 | ``` 64 | [ 0.0] Examining the guest ... 65 | [ 10.5] Setting a random seed 66 | virt-customize: warning: random seed could not be set for this type of 67 | guest 68 | [ 10.5] Setting the machine ID in /etc/machine-id 69 | [ 10.5] Installing packages: qemu-guest-agent 70 | [ 32.8] Finishing off 71 | ``` 72 | 73 | Run `virt-sysprep` to start resetting the cloud-init image -- ahead of cloning it to a Proxmox VM later on. 74 | 75 | ``` 76 | proxmox-ve-node# virt-sysprep -a jammy-server-cloudimg-amd64.img 77 | [ 0.0] Examining the guest ... 78 | [ 3.7] Performing "abrt-data" ... 79 | [ 3.7] Performing "backup-files" ... 80 | [ 4.2] Performing "bash-history" ... 81 | 82 | ... etc ... 83 | 84 | [ 4.7] Performing "utmp" ... 85 | [ 4.7] Performing "yum-uuid" ... 86 | [ 4.7] Performing "customize" ... 87 | [ 4.7] Setting a random seed 88 | virt-sysprep: warning: random seed could not be set for this type of guest 89 | [ 4.8] Setting the machine ID in /etc/machine-id 90 | [ 4.8] Performing "lvm-uuids" ... 91 | ``` 92 | 93 | Notice how `virt-sysprep` set `/etc/machine-id`. We don't want that -- as the same machine-id will carry over to all Proxmox VMs when they are cloned. Therefore, we need to truncate `/etc/machine-id` so that it will be automatically created as each Proxmox VM is provisioned. 94 | 95 | ``` 96 | proxmox-ve-node# virt-customize -a jammy-server-cloudimg-amd64.img --truncate /etc/machine-id 97 | [ 0.0] Examining the guest ... 98 | [ 3.8] Setting a random seed 99 | virt-customize: warning: random seed could not be set for this type of 100 | guest 101 | [ 3.8] Truncating: /etc/machine-id 102 | [ 3.8] Finishing off 103 | ``` 104 | 105 | Now we are ready to create a Proxmox VM from the Ubuntu ('jammy') cloud-init image -- that we have modified. This breaks down into two (high-level) steps: 106 | 1. Create a Proxmox VM, with a unique id, with various configuration options 107 | 2. Convert the Proxmox VM into a Proxmox VM template 108 | 109 | First, create the Proxmox VM, with a unique id, and configure its attributes. We tend to use unique ids >= 9000 for Proxmox VM templates, but you do as you will. *Note that you cannot use duplicate VM ids in Proxmox.* You will need to run the `qm` command, as the 'root' user, on your Proxmox node, to configure the following Proxmox VM attributes: 110 | 111 | - create the Proxmox VM 112 | - import the cloud-init image to the Proxmox VM 113 | - set the SCSI (disk) hardware attributes for the Proxmox VM root disk 114 | - map an IDE disk to the cloud-init image (this will be used as a CD-ROM) 115 | - define a boot disk for the Proxmox VM 116 | - define a serial port such that the Proxmox VM is accessible through the Proxmox console 117 | - set the QEMU agent to be enabled such that you can access various information from `qemu-guest-agent` when the Proxmox VM is running 118 | 119 | Regarding where you store the Ubuntu ('jammy') cloud-init image, you likely have options between faster and slower disks on your Proxmox nodes. It's recommended that you store the Ubuntu ('jammy') cloud-init image on faster disks; this will lead to better VM cloning performance. Let's see which disks are available to us in Proxmox; in this case an SSD comprises our root volume, which is called 'local-lvm'. There is a slower spinning drive configuration that's called 'pve-hdd'. 120 | 121 | ``` 122 | proxmox-ve-node# pvesh get /storage --output-format yaml 123 | --- 124 | - content: images,rootdir 125 | digest: 0b59487c0e528e7eabc9293079ac26389ac1b91b 126 | storage: local-lvm 127 | thinpool: data 128 | type: lvmthin 129 | vgname: pve 130 | - content: iso,backup,vztmpl 131 | digest: 0b59487c0e528e7eabc9293079ac26389ac1b91b 132 | path: /var/lib/vz 133 | storage: local 134 | type: dir 135 | - content: rootdir,images 136 | digest: 0b59487c0e528e7eabc9293079ac26389ac1b91b 137 | nodes: proxmox-ve 138 | shared: 0 139 | storage: pve-hdd 140 | type: lvm 141 | vgname: pve-hdd 142 | ``` 143 | 144 | As noted, 'local-lvm' is our SSD storage, so let's use that. We'll need to keep track of the name 'local-lvm' as it's required when running the `qm` commands. 145 | 146 | Here's the procedure to create a Proxmox VM from the Ubuntu ('jammy') cloud-init image. 147 | 148 | ``` 149 | proxmox-ve-node# qm create 9000 --name jammy-server-cloudimg-amd64-template --ostype l26 --cpu cputype=host --cores 1 --sockets 1 --memory 1024 --net0 virtio,bridge=vmbr0 150 | 151 | proxmox-ve-node# # qm list | grep jammy 152 | 9000 jammy-server-cloudimg-amd64-template stopped 1024 0.00 0 153 | 154 | 155 | proxmox-ve-node# qm importdisk 9000 jammy-server-cloudimg-amd64.img local-lvm -format qcow2 156 | importing disk 'jammy-server-cloudimg-amd64.img' to VM 9001 ... 157 | format 'qcow2' is not supported by the target storage - using 'raw' instead 158 | Logical volume "vm-9001-disk-1" created. 159 | transferred 0.0 B of 2.2 GiB (0.00%) 160 | transferred 22.5 MiB of 2.2 GiB (1.00%) 161 | 162 | ... etc ... 163 | 164 | transferred 2.2 GiB of 2.2 GiB (99.64%) 165 | transferred 2.2 GiB of 2.2 GiB (100.00%) 166 | Successfully imported disk as 'unused0:local-lvm:vm-9000-disk-0' 167 | 168 | 169 | proxmox-ve-node# qm set 9000 --scsihw virtio-scsi-pci --scsi0 local-lvm:vm-9000-disk-0 170 | update VM 9000: -scsi0 local-lvm:vm-9000-disk-0 -scsihw virtio-scsi-pci 171 | 172 | 173 | proxmox-ve-node# qm set 9000 --ide2 local-lvm:cloudinit 174 | update VM 9000: -ide2 local-lvm:cloudinit 175 | Logical volume "vm-9000-cloudinit" created. 176 | ide2: successfully created disk 'local-lvm:vm-9000-cloudinit,media=cdrom' 177 | generating cloud-init ISO 178 | 179 | 180 | proxmox-ve-node# qm set 9000 --boot c --bootdisk scsi0 181 | update VM 9000: -boot c -bootdisk scsi0 182 | 183 | 184 | proxmox-ve-node# qm set 9000 --serial0 socket --vga serial0 185 | update VM 9000: -serial0 socket -vga serial0 186 | 187 | 188 | proxmox-ve-node# qm set 9000 --agent enabled=1 189 | update VM 9000: -agent enabled=1 190 | ``` 191 | 192 | Second, convert the Proxmox VM into a template. You can then use this Proxmox VM template in your `netbox-proxmox-automation` automation. 193 | 194 | *Note that this cannot be undone!* 195 | 196 | ``` 197 | proxmox-ve-node# qm template 9000 198 | Renamed "vm-9000-disk-0" to "base-9000-disk-0" in volume group "pve" 199 | Logical volume pve/base-9000-disk-0 changed. 200 | ``` 201 | 202 | You should now be able to use your Proxmox VM template, with a VM id (vmid) of 9000 (or whatever you chose) in your `netbox-proxmox-automation` automation. 203 | -------------------------------------------------------------------------------- /docs/stylesheets/extra.css: -------------------------------------------------------------------------------- 1 | [data-md-color-scheme="nbl-light"] { 2 | color-scheme: light; 3 | 4 | --md-default-fg-color--light: #001423b3; 5 | 6 | --md-primary-fg-color: #00857d; 7 | --md-primary-fg-color--light: #00d9be; 8 | --md-primary-fg-color--dark: #00a595; 9 | --md-primary-bg-color: #fff; 10 | --md-primary-bg-color--light: #ffffffb3; 11 | 12 | --md-accent-fg-color: #00d9be; 13 | --md-accent-fg-color--transparent: #00d9be1a; 14 | --md-accent-bg-color: #ffffff; 15 | --md-accent-bg-color--light: #ffffffb3; 16 | 17 | --md-typeset-color: #000000de; 18 | --md-typeset-a-color: #00a595; 19 | } 20 | 21 | [data-md-color-scheme="nbl-dark"] { 22 | color-scheme: dark; 23 | 24 | --md-default-bg-color: #001423; 25 | --md-default-fg-color--light: #ffffff; 26 | 27 | --md-primary-fg-color: #00857d; 28 | --md-primary-fg-color--light: #00d9be; 29 | --md-primary-fg-color--dark: #00a595; 30 | --md-primary-bg-color: #fff; 31 | --md-primary-bg-color--light: #ffffffb3; 32 | 33 | --md-accent-fg-color: #00d9be; 34 | --md-accent-fg-color--transparent: #00d9be1a; 35 | --md-accent-bg-color: #ffffff; 36 | --md-accent-bg-color--light: #ffffffb3; 37 | 38 | --md-typeset-color: #ffffff; 39 | --md-typeset-a-color: #00ccb8; 40 | 41 | --md-code-fg-color: #ffffff; 42 | --md-code-bg-color: #272a35; 43 | --md-code-hl-name-color: #ffffff; 44 | --md-code-hl-string-color: #ffffff; 45 | --md-code-hl-punctuation-color:#ffffffb3; 46 | --md-code-hl-operator-color: #ffffffb3; 47 | --md-code-hl-comment-color: #ffffff; 48 | --md-code-hl-generic-color: #ffffff; 49 | --md-code-hl-variable-color: #ffffff; 50 | 51 | 52 | --md-admonition-fg-color: #ffffff; 53 | --md-admonition-bg-color: #272a35; 54 | } -------------------------------------------------------------------------------- /docs/usage.md: -------------------------------------------------------------------------------- 1 | # Usage 2 | 3 | `netbox-proxmox-automation` implements two automation use cases: 4 | 5 | 1. NetBox webhooks and event rules will use Ansible AWX/Tower/AAP to facilitate Proxmox automation 6 | 2. NetBox webhooks and event rules will use a Flask application to facilitate Proxmox automation 7 | 8 | ## What this implementation *is* 9 | 10 | `netbox-proxmox-automation` is an implementation where you define your *desired* (Proxmox) VM states in NetBox. Your desired VM state in NetBox then gets synchronized to Proxmox. 11 | 12 | For Virtual Machine automation, `netbox-proxmox-automation` uses cloud-init images to induce VM changes on Proxmox based on the *desired* state in NetBox. Proxmox is highly conducive to using cloud-init images -- when said cloud-init images are converted to templates. You can define items like ssh keys and network configurations in Proxmox by way of using cloud-init images, and cloud-init will cascade these settings into your Proxmox VMs: *dynamically*. Further, Proxmox has a comprehensive API -- you can define VM resources, plus disk configurations and more -- which makes it easy for you to automate your desired Proxmox VM states with little effort. 13 | 14 | Almost always these cloud-init images will be Debian or Debian-derived images (e.g. Debian or Ubuntu), RHEL-derived images (e.g. Rocky Linux), or maybe even Windows-based cloud-init images. *(Windows cloud-init images are currently un-tested.)* While you should be able to use a cloud-init image of choice with this automation, and due to the uncertain future of RHEL-derived Linuxes, *only* Ubuntu/Debian cloud images (cloud-init) are supported for the time being. We welcome any reports around other cloud-init images, and will address this functionality as we are able. 15 | 16 | For LXC automation, `netbox-proxmox-automation` uses LXC (container images) on Proxmox to induce LXC changes on Proxmox based on the *desired* state in NetBox. 17 | 18 | NetBox models VMs in an intuitive way. You can define roles for VMs, such as for Proxmox, and from there you can define both VM state (Staged, Active, Offline, etc) and other resources like vcpus, memory, network configuration, VM disks, and more (through customizations in NetBox). 19 | 20 | This automation is based on the premise(s) that: 21 | 22 | 1. You are using Python (version 3) 23 | 2. You are using NetBox 4.1.0 or newer (NetBox 3.7.x should also work) 24 | 3. You have a running Proxmox instance or cluster 25 | 4. You have a running [AWX](https://github.com/ansible/awx) instance or are running [your own web application](https://github.com/netboxlabs/netbox-proxmox-automation/tree/main/netbox-event-driven-automation-flask-app) to handle webhooks and event rules 26 | 5. You have converted a cloud-init image to a Proxmox VM template 27 | 6. Your Promox VM template(s) has/have qemu-guest-agent installed, and that qemu-guest-agent has been enabled via cloud-init 28 | 7. You have access to the NetBox and Proxmox APIs (via API tokens, respectively) 29 | 8. Your NetBox API token and its underlying privileges can create, modify, and delete objects in NetBox 30 | 9. Your Proxmox API token and its underlying privileges can both manage VMs and storage (query, create, delete, etc) 31 | 32 | ## What this implementation *is not* 33 | 34 | `netbox-proxmox-automation` is not currently a NetBox plugin, but this may change. 35 | 36 | [ProxBox](https://github.com/netdevopsbr/netbox-proxbox) is a neat implementation of pulling information from Proxmox into NetBox. ProxBox has its place, most certainly, but what it does is *not* the aim of `netbox-proxmox-automation`. 37 | 38 | Further, `netbox-proxmox-automation` does *not* deploy a future state for any Proxmox VM. For example, let's say you deploy a Proxmox VM called 'vm1' and it's intended to be a(n) LDAP server. The scope of `netbox-proxmox-automation` is to ensure that each VM that you document/model and deploy to Proxmox has a consistent baseline configuration, from which you can take future automation steps. NetBox will document/model your *desired* Proxmox VM state, but additional automation states like package installations and configurations are left up to further automation that you might choose to do once your vitual machine is running in Proxmox. As `netbox-proxmox-automation` is free for you to use and/or modify, you are welcome to introduce subsequent automations to `netbox-proxmox-automation` in your own environment. 39 | 40 | -------------------------------------------------------------------------------- /legacy/README.TXT: -------------------------------------------------------------------------------- 1 | Simply put, don't use this stuff. It's just parked here and will be reimagined at some point in time. 2 | -------------------------------------------------------------------------------- /legacy/netbox-proxmox-discover-vms.yml: -------------------------------------------------------------------------------- 1 | - name: "NetBox-Proxmox VM Manager" 2 | connection: local 3 | hosts: localhost 4 | gather_facts: False 5 | 6 | vars: 7 | proxmox_discovered_running_vms_configuration: [] 8 | proxmox_discovered_stopped_vms_configuration: [] 9 | proxmox_discovered_running_vms_networking: [] 10 | proxmox_discovered_running_vms_network_interface_mapping: [] 11 | proxmox_discovered_running_vms: [] 12 | proxmox_discovered_stopped_vms: [] 13 | proxmox_discovered_vms_configurations_raw: [] 14 | proxmox_vm_disk_names: {} 15 | proxmox_vm_collected_disks: [] 16 | proxmox_vm_collected_disks_gb: [] 17 | proxmox_vm_collected_disks_mb: [] 18 | proxmox_vm_collected_disks_all: [] 19 | 20 | pm_to_nb_status_mapping: 21 | 'stopped': 'Offline' 22 | 'running': 'Active' 23 | 24 | tasks: 25 | - name: Collect secrets 26 | include_vars: 27 | file: secrets.yml 28 | name: secrets_cfg 29 | 30 | - name: Read Proxmox VM Configuration 31 | include_vars: 32 | file: vms.yml 33 | name: proxmox_vm_cfg 34 | 35 | - name: "Proxmox: Discover all VMs" 36 | community.general.proxmox_vm_info: 37 | api_host: "{{ secrets_cfg.proxmox.api_host | default(omit) }}" 38 | api_user: "{{ secrets_cfg.proxmox.api_user | default(omit) }}" 39 | api_token_id: "{{ secrets_cfg.proxmox.api_token_id | default(omit) }}" 40 | api_token_secret: "{{ secrets_cfg.proxmox.api_token_secret | default(omit) }}" 41 | config: current 42 | register: proxmox_discovered_vms 43 | 44 | - name: "Proxmox: Collect all running VMs (excluding templates)" 45 | set_fact: 46 | proxmox_discovered_running_vms: "{{ proxmox_discovered_running_vms | combine({item.name: item.vmid}) }}" 47 | loop: "{{ proxmox_discovered_vms.proxmox_vms }}" 48 | when: not item.template and item.status == "running" 49 | 50 | - name: "Proxmox: Collect all stopped VMs (excluding templates)" 51 | set_fact: 52 | proxmox_discovered_stopped_vms: "{{ proxmox_discovered_stopped_vms | combine({item.name: item.vmid}) }}" 53 | loop: "{{ proxmox_discovered_vms.proxmox_vms }}" 54 | when: not item.template and item.status == "stopped" 55 | 56 | - name: "Proxmox: Discover running VM configurations by vmid" 57 | community.general.proxmox_vm_info: 58 | api_host: "{{ secrets_cfg.proxmox.api_host | default(omit) }}" 59 | api_user: "{{ secrets_cfg.proxmox.api_user | default(omit) }}" 60 | api_token_id: "{{ secrets_cfg.proxmox.api_token_id | default(omit) }}" 61 | api_token_secret: "{{ secrets_cfg.proxmox.api_token_secret | default(omit) }}" 62 | vmid: "{{ proxmox_discovered_running_vms[item] }}" 63 | network: true 64 | config: current 65 | with_items: "{{ proxmox_discovered_running_vms }}" 66 | register: proxmox_discovered_vms_cfg 67 | 68 | - name: "Proxmox: Collect Proxmox running VM configurations" 69 | set_fact: 70 | proxmox_discovered_running_vms_configuration: "{{ proxmox_discovered_running_vms_configuration + [ {'name': item.proxmox_vms[0].name, 'vmid': item.proxmox_vms[0].vmid, 'node': item.proxmox_vms[0].node, 'status': item.proxmox_vms[0].status, 'vcpus': item.proxmox_vms[0].cpus, 'memory': item.proxmox_vms[0].maxmem, 'disk': item.proxmox_vms[0].maxdisk, 'network': item.proxmox_vms[0].network} ] }}" 71 | loop: "{{ proxmox_discovered_vms_cfg.results }}" 72 | 73 | - name: "Proxmox: Discover stopped VM configurations by vmid" 74 | community.general.proxmox_vm_info: 75 | api_host: "{{ secrets_cfg.proxmox.api_host | default(omit) }}" 76 | api_user: "{{ secrets_cfg.proxmox.api_user | default(omit) }}" 77 | api_token_id: "{{ secrets_cfg.proxmox.api_token_id | default(omit) }}" 78 | api_token_secret: "{{ secrets_cfg.proxmox.api_token_secret | default(omit) }}" 79 | vmid: "{{ proxmox_discovered_stopped_vms[item] }}" 80 | config: current 81 | with_items: "{{ proxmox_discovered_stopped_vms }}" 82 | register: proxmox_discovered_stopped_vms_cfg 83 | 84 | - name: "Proxmox: Collect Proxmox stopped VM configurations" 85 | set_fact: 86 | proxmox_discovered_stopped_vms_configuration: "{{ proxmox_discovered_stopped_vms_configuration + [ {'name': item.proxmox_vms[0].name, 'vmid': item.proxmox_vms[0].vmid, 'node': item.proxmox_vms[0].node, 'status': item.proxmox_vms[0].status, 'vcpus': item.proxmox_vms[0].cpus, 'memory': item.proxmox_vms[0].maxmem, 'disk': item.proxmox_vms[0].maxdisk} ] }}" 87 | loop: "{{ proxmox_discovered_stopped_vms_cfg.results }}" 88 | 89 | - name: "Proxmox: Collect all networking information for VM(s)" 90 | set_fact: 91 | proxmox_discovered_running_vms_networking: "{{ proxmox_discovered_running_vms_networking + [ { 'name': item.0.name, 'interface': item.1.name, 'mac-address': item.1['hardware-address'], 'ip-addresses': item.1['ip-addresses'] } ] }}" 92 | loop: "{{ proxmox_discovered_running_vms_configuration | subelements('network') }}" 93 | when: item.1.name != "lo" and item.1.name != "docker0" and not item.1.name | regex_search('^br\-') # Exclude loopback and docker interface(s) 94 | 95 | - name: "Proxmox: Collect network interface mappings for VM(s)" 96 | set_fact: 97 | proxmox_discovered_running_vms_network_interface_mapping: "{{ proxmox_discovered_running_vms_network_interface_mapping + [ { 'name': item.0.name, 'type': item.1['ip-address-type'], 'interface': item.0.interface, 'mac-address': item.0['mac-address'], 'ip': item.1['ip-address'] | string + '/' + item.1['prefix'] | string } ]}}" 98 | loop: "{{ proxmox_discovered_running_vms_networking | subelements('ip-addresses') }}" 99 | when: item.1['ip-address-type'] == "ipv4" # Only collect ipv4 addresses for this example 100 | 101 | # Collect Proxmox VM disks 102 | - name: "Proxmox: Discover VM (raw) disk configurations" 103 | set_fact: 104 | proxmox_discovered_vms_configurations_raw: "{{ proxmox_discovered_vms_configurations_raw + [ item.1.config | ansible.utils.keep_keys(target=['scsi', 'name'], matching_parameter= 'starts_with') ] }}" 105 | loop: "{{ proxmox_discovered_vms_cfg.results | subelements('proxmox_vms') }}" 106 | 107 | - name: Gather Proxmox disk names from collected output 108 | set_fact: 109 | proxmox_vm_disk_names: "{{ proxmox_vm_disk_names | combine({item.name: {'name': item.name, 'disks': item.keys() | map('regex_search', '^scsi[0-9]+') | select('string') | list}}) }}" 110 | loop: "{{ proxmox_discovered_vms_configurations_raw }}" 111 | 112 | - name: Categorize collected Proxmox disk names 113 | set_fact: 114 | proxmox_vm_collected_disks: "{{ proxmox_vm_collected_disks + [ {'name': item.0.name, 'disk_name': item.1, 'disk_config': (proxmox_discovered_vms_configurations_raw | selectattr('name', 'equalto', item.0.name) | first)[item.1]} ] }}" 115 | loop: "{{ proxmox_vm_disk_names | subelements('disks') }}" 116 | when: proxmox_discovered_vms_configurations_raw | selectattr('name', 'equalto', item.0.name) 117 | 118 | - name: Categorize collected Proxmox disk names by disk size in GB 119 | set_fact: 120 | proxmox_vm_collected_disks_gb: "{{ proxmox_vm_collected_disks_gb + [ {'name': item.name, 'disk_name': item.disk_name, 'disk_size': item.disk_config | regex_replace('^.*,size=([0-9]+)G$', '\\1') } ] }}" 121 | loop: "{{ proxmox_vm_collected_disks }}" 122 | when: item.disk_config | regex_search(',size=\d+G$') 123 | 124 | - name: Categorize collected Proxmox disk names by disk size in MB 125 | set_fact: 126 | proxmox_vm_collected_disks_mb: "{{ proxmox_vm_collected_disks_mb + [ {'name': item.name, 'disk_name': item.disk_name, 'disk_size': (item.disk_config | regex_replace('^.*,size=([0-9]+)M$', '\\1') | int / 1024) | int } ] }}" 127 | loop: "{{ proxmox_vm_collected_disks }}" 128 | when: item.disk_config | regex_search(',size=\d+M$') 129 | 130 | - name: Merge collected Proxmox VM disk information 131 | set_fact: 132 | proxmox_vm_collected_disks_all: "{{ proxmox_vm_collected_disks_gb + proxmox_vm_collected_disks_mb }}" 133 | 134 | - name: "NetBox: Add VM entries for running Proxmox VM(s)" 135 | netbox.netbox.netbox_virtual_machine: 136 | netbox_url: "{{ secrets_cfg.netbox.api_proto }}://{{ secrets_cfg.netbox.api_host }}:{{ secrets_cfg.netbox.api_port}}" 137 | netbox_token: "{{ secrets_cfg.netbox.api_token }}" 138 | data: 139 | name: "{{ item.name }}" 140 | status: "{{ pm_to_nb_status_mapping[item.status] }}" 141 | tenant: "{{ vm_config[item.name].tenant if vm_config[item.name].tenant is defined else proxmox_vm_cfg.default_tenant }}" 142 | virtual_machine_role: "{{ proxmox_vm_cfg.default_vm_device_role }}" 143 | site: "{{ vm_config[item.name].site if vm_config[item.name].site is defined else proxmox_vm_cfg.default_site }}" 144 | cluster: "{{ vm_config[item.name].vm_cluster if vm_config[item.name].vm_cluster is defined else proxmox_vm_cfg.default_vm_cluster }}" 145 | vcpus: "{{ item.vcpus | int }}" 146 | memory: "{{ (item.memory / (1024*1024)) | float | int }}" 147 | custom_fields: 148 | proxmox_vmid: "{{ item.vmid | int }}" 149 | proxmox_node: "{{ item.node }}" 150 | state: present 151 | loop: "{{ proxmox_discovered_running_vms_configuration }}" 152 | 153 | - name: "Proxmox: VM create interface(s)" 154 | netbox.netbox.netbox_vm_interface: 155 | netbox_url: "{{ secrets_cfg.netbox.api_proto }}://{{ secrets_cfg.netbox.api_host }}:{{ secrets_cfg.netbox.api_port}}" 156 | netbox_token: "{{ secrets_cfg.netbox.api_token }}" 157 | data: 158 | virtual_machine: "{{ item.name }}" 159 | name: "{{ item.interface }}" 160 | mac_address: "{{ item['mac-address']}}" 161 | state: present 162 | with_items: "{{ proxmox_discovered_running_vms_network_interface_mapping }}" 163 | 164 | - name: "Proxmox: VM assign IP address(es)" 165 | netbox.netbox.netbox_ip_address: 166 | netbox_url: "{{ secrets_cfg.netbox.api_proto }}://{{ secrets_cfg.netbox.api_host }}:{{ secrets_cfg.netbox.api_port}}" 167 | netbox_token: "{{ secrets_cfg.netbox.api_token }}" 168 | data: 169 | address: "{{ item.ip }}" 170 | status: Active 171 | assigned_object: 172 | virtual_machine: "{{ item.name }}" 173 | name: "{{ item.interface }}" 174 | state: present 175 | with_items: "{{ proxmox_discovered_running_vms_network_interface_mapping }}" 176 | 177 | - name: "NetBox: Update VM(s) primary_ip4" 178 | netbox.netbox.netbox_virtual_machine: 179 | netbox_url: "{{ secrets_cfg.netbox.api_proto }}://{{ secrets_cfg.netbox.api_host }}:{{ secrets_cfg.netbox.api_port}}" 180 | netbox_token: "{{ secrets_cfg.netbox.api_token }}" 181 | data: 182 | name: "{{ item.name }}" 183 | primary_ip4: "{{ item.config.ipconfig0.split(',')[0] | regex_replace('^ip=', '')}}" 184 | state: present 185 | loop: "{{ proxmox_discovered_vms.proxmox_vms }}" 186 | when: not item.template and item.status == "running" 187 | 188 | - name: "NetBox: Add VM entries for stopped Proxmox VM(s)" 189 | netbox.netbox.netbox_virtual_machine: 190 | netbox_url: "{{ secrets_cfg.netbox.api_proto }}://{{ secrets_cfg.netbox.api_host }}:{{ secrets_cfg.netbox.api_port}}" 191 | netbox_token: "{{ secrets_cfg.netbox.api_token }}" 192 | data: 193 | name: "{{ item.name }}" 194 | status: "{{ pm_to_nb_status_mapping[item.status] }}" 195 | tenant: "{{ vm_config[item.name].tenant if vm_config[item.name].tenant is defined else proxmox_vm_cfg.default_tenant }}" 196 | virtual_machine_role: "{{ proxmox_vm_cfg.default_vm_device_role }}" 197 | site: "{{ vm_config[item.name].site if vm_config[item.name].site is defined else proxmox_vm_cfg.default_site }}" 198 | cluster: "{{ vm_config[item.name].vm_cluster if vm_config[item.name].vm_cluster is defined else proxmox_vm_cfg.default_vm_cluster }}" 199 | vcpus: "{{ item.vcpus | int }}" 200 | memory: "{{ (item.memory / (1024*1024)) | float | int }}" 201 | custom_fields: 202 | proxmox_vmid: "{{ item.vmid | int }}" 203 | proxmox_node: "{{ item.node }}" 204 | state: present 205 | loop: "{{ proxmox_discovered_stopped_vms_configuration }}" 206 | 207 | - name: "NetBox: Add collected disks for Proxmox VMs" 208 | netbox.netbox.netbox_virtual_disk: 209 | netbox_url: "{{ secrets_cfg.netbox.api_proto }}://{{ secrets_cfg.netbox.api_host }}:{{ secrets_cfg.netbox.api_port}}" 210 | netbox_token: "{{ secrets_cfg.netbox.api_token }}" 211 | data: 212 | virtual_machine: "{{ item.name }}" 213 | name: "{{ item.disk_name }}" 214 | size: "{{ item.disk_size }}" 215 | state: present 216 | loop: "{{ proxmox_vm_collected_disks_all }}" 217 | 218 | - name: Find VMs in Netbox 219 | local_action: 220 | module: uri 221 | url: "{{ secrets_cfg.netbox.api_proto }}://{{ secrets_cfg.netbox.api_host }}:{{ secrets_cfg.netbox.api_port}}/api/virtualization/virtual-machines/" 222 | method: GET 223 | headers: 224 | Authorization: "Token {{ secrets_cfg.netbox.api_token }}" 225 | status_code: 200 226 | validate_certs: no 227 | register: vms 228 | 229 | - name: "NetBox: Remove VM(s) that no longer exist in Proxmox" 230 | netbox.netbox.netbox_virtual_machine: 231 | netbox_url: "{{ secrets_cfg.netbox.api_proto }}://{{ secrets_cfg.netbox.api_host }}:{{ secrets_cfg.netbox.api_port}}" 232 | netbox_token: "{{ secrets_cfg.netbox.api_token }}" 233 | data: 234 | name: "{{ item.name }}" 235 | state: absent 236 | loop: "{{ vms.json.results }}" 237 | when: (proxmox_discovered_running_vms[item.name] is not defined) and (proxmox_discovered_stopped_vms[item.name] is not defined) 238 | 239 | -------------------------------------------------------------------------------- /legacy/setup-netbox-custom-vm-objects.yml: -------------------------------------------------------------------------------- 1 | - name: "NetBox-Proxmox VM Manager" 2 | connection: local 3 | hosts: localhost 4 | gather_facts: False 5 | 6 | vars: 7 | collected_proxmox_vm_templates: {} 8 | proxmox_vm_create: [] 9 | proxmox_vm_delete: [] 10 | proxmox_all_vm_interfaces: {} 11 | proxmox_vm_interfaces_prefix: [] 12 | proxmox_vm_interfaces_ip: [] 13 | nb_vm_created: {} 14 | nb_vm_interface_assigned_ip: {} 15 | nb_remove_vms: {} 16 | proxmox_ipconfig_dict: {} 17 | gw_last_quad: 1 18 | 19 | tasks: 20 | - name: Collect secrets 21 | include_vars: 22 | file: secrets.yml 23 | name: secrets_cfg 24 | 25 | - name: Read Proxmox VM Configuration 26 | include_vars: 27 | file: vms.yml 28 | name: proxmox_vm_cfg 29 | 30 | - name: Init dict(s) 31 | set_fact: 32 | vm_config: {} 33 | vm_config_interfaces: {} 34 | vm_config_remove: {} 35 | the_fact: {} 36 | nb_selected_vm: '' 37 | nb_selected_interface: '' 38 | nb_ipconfig_key_name: '' 39 | proxmox_vm_templates: [] 40 | 41 | - name: "Proxmox: Discover all existing virtual machines" 42 | community.general.proxmox_vm_info: 43 | api_host: "{{ secrets_cfg.proxmox.api_host | default(omit) }}" 44 | api_user: "{{ secrets_cfg.proxmox.api_user | default(omit) }}" 45 | api_token_id: "{{ secrets_cfg.proxmox.api_token_id | default(omit) }}" 46 | api_token_secret: "{{ secrets_cfg.proxmox.api_token_secret | default(omit) }}" 47 | #node: "{{ secrets_cfg.proxmox.node }}" 48 | register: proxmox_discovered_vms 49 | 50 | - name: "Proxmox: Collect Proxmox VM Templates" 51 | set_fact: 52 | collected_proxmox_vm_templates: "{{ collected_proxmox_vm_templates | combine({item.name: True}) }}" 53 | loop: "{{ proxmox_discovered_vms.proxmox_vms }}" 54 | when: item.template 55 | 56 | - name: Set Proxmox VM collected templates 57 | set_fact: 58 | proxmox_vm_templates: "{{ proxmox_vm_templates + [ item + ',' + item ] }}" 59 | with_items: "{{ collected_proxmox_vm_templates }}" 60 | 61 | - name: "NetBox: Create Proxmox VM template choices" 62 | netbox.netbox.netbox_custom_field_choice_set: 63 | netbox_url: "{{ secrets_cfg.netbox.api_proto }}://{{ secrets_cfg.netbox.api_host }}:{{ secrets_cfg.netbox.api_port}}" 64 | netbox_token: "{{ secrets_cfg.netbox.api_token }}" 65 | data: 66 | name: "Proxmox VM Templates" 67 | description: "Proxmox VM Templates" 68 | extra_choices: "{{ proxmox_vm_templates }}" 69 | 70 | - name: "NetBox: Create Proxmox VM template custom field of type selection" 71 | netbox.netbox.netbox_custom_field: 72 | netbox_url: "{{ secrets_cfg.netbox.api_proto }}://{{ secrets_cfg.netbox.api_host }}:{{ secrets_cfg.netbox.api_port}}" 73 | netbox_token: "{{ secrets_cfg.netbox.api_token }}" 74 | data: 75 | name: "proxmox_vm_template" 76 | label: "Proxmox VM Template" 77 | group_name: "Proxmox" 78 | required: true 79 | content_types: 80 | - virtualization.virtualmachine 81 | type: select 82 | choice_set: "Proxmox VM Templates" 83 | -------------------------------------------------------------------------------- /legacy/setup-netbox-objects.yml: -------------------------------------------------------------------------------- 1 | - name: "NetBox-Proxmox VM Manager" 2 | connection: local 3 | hosts: localhost 4 | gather_facts: False 5 | 6 | vars: 7 | collected_proxmox_vms: {} 8 | proxmox_vm_create: [] 9 | proxmox_vm_delete: [] 10 | proxmox_all_vm_interfaces: {} 11 | proxmox_vm_interfaces_prefix: [] 12 | proxmox_vm_interfaces_ip: [] 13 | nb_vm_created: {} 14 | nb_vm_interface_assigned_ip: {} 15 | nb_remove_vms: {} 16 | proxmox_ipconfig_dict: {} 17 | gw_last_quad: 1 18 | 19 | tasks: 20 | - name: Collect secrets 21 | include_vars: 22 | file: secrets.yml 23 | name: secrets_cfg 24 | 25 | - name: Read Proxmox VM Configuration 26 | include_vars: 27 | file: vms.yml 28 | name: proxmox_vm_cfg 29 | 30 | - name: Init dict(s) 31 | set_fact: 32 | vm_config: {} 33 | vm_config_interfaces: {} 34 | vm_config_remove: {} 35 | the_fact: {} 36 | nb_selected_vm: '' 37 | nb_selected_interface: '' 38 | nb_ipconfig_key_name: '' 39 | 40 | - name: "NetBox: Configure base objects from defaults" 41 | include_tasks: "ansible-tasks/netbox/netbox-object-defaults.yml" 42 | 43 | - name: "NetBox: Configure base objects from vm settings" 44 | include_tasks: "ansible-tasks/netbox/netbox-vm-object-defaults.yml" 45 | 46 | - name: "NetBox: Configure base objects for VM clusters" 47 | include_tasks: "ansible-tasks/netbox/netbox-vm-cluster-objects.yml" 48 | 49 | - name: "Proxmox: Discover VMs" 50 | include_tasks: "ansible-tasks/proxmox/discover-vms.yml" 51 | 52 | - name: "NetBox: Check Prefix and IP settings for Proxmox VMs" 53 | include_tasks: "ansible-tasks/netbox/prefix-ip-checker.yml" 54 | -------------------------------------------------------------------------------- /mkdocs.yml: -------------------------------------------------------------------------------- 1 | site_name: NetBox Labs Documentation 2 | theme: 3 | name: material 4 | #custom_dir: overrides 5 | favicon: images/netbox-favicon.png 6 | logo: images/netbox-light-favicon.png 7 | features: 8 | - navigation.tracking 9 | - navigation.instant 10 | - navigation.tabs 11 | - navigation.tabs.sticky 12 | - navigation.top 13 | - navigation.indexes 14 | - navigation.sections 15 | - navigation.prune 16 | - search.suggest 17 | # - toc.integrate 18 | - content.code.copy 19 | - content.code.select 20 | palette: 21 | - scheme: nbl-light 22 | toggle: 23 | icon: material/lightbulb 24 | name: Switch to dark mode 25 | - scheme: nbl-dark 26 | toggle: 27 | icon: material/lightbulb-outline 28 | name: Switch to light mode 29 | extra_css: 30 | - stylesheets/extra.css 31 | markdown_extensions: 32 | - abbr 33 | - pymdownx.tabbed: 34 | alternate_style: true 35 | - pymdownx.snippets 36 | - pymdownx.highlight 37 | - pymdownx.inlinehilite 38 | - pymdownx.caret 39 | - pymdownx.mark 40 | - pymdownx.tilde 41 | - pymdownx.superfences 42 | - admonition 43 | - pymdownx.details 44 | - attr_list 45 | - md_in_html 46 | - toc: 47 | toc_depth: 3 48 | - pymdownx.superfences: 49 | custom_fences: 50 | - name: mermaid 51 | class: mermaid 52 | format: !!python/name:pymdownx.superfences.fence_code_format 53 | plugins: 54 | - search 55 | nav: 56 | - Home: index.md 57 | - Usage: usage.md 58 | - Proxmox VM Templates: proxmox-vm-templates.md 59 | - Proxmox LXC Templates: proxmox-lxc-templates.md 60 | - NetBox IPAM: netbox-ipam.md 61 | - NetBox API Key and Permissions: netbox-key-and-permissions.md 62 | - NetBox Customization: netbox-customization.md 63 | - Discover Proxmox VMs and LXCs: proxmox-discover-vm-and-lxc.md 64 | - Proxmox API User and Key: proxmox-api-user-and-key.md 65 | - Configure Flask Application: configure-flask-application.md 66 | - Configure AWX or Tower/AAP: configure-awx-aap.md 67 | - NetBox Event Rules and Webhooks (Flask): netbox-event-rules-and-webhooks-flask.md 68 | - NetBox Event Rules and Webhooks (AWX/Tower/AAP): netbox-event-rules-and-webhooks-awx-aap.md 69 | extra: 70 | social: 71 | - icon: fontawesome/solid/globe 72 | link: https://netboxlabs.com 73 | - icon: octicons/mail-24 74 | link: "mailto:support@netboxlabs.com" 75 | analytics: 76 | provider: google 77 | property: G-Q107GMDTJM 78 | copyright: Copyright © 2024-2025 NetBox Labs 79 | -------------------------------------------------------------------------------- /netbox-event-driven-automation-flask-app/app.py: -------------------------------------------------------------------------------- 1 | import logging 2 | import json 3 | import yaml 4 | 5 | from datetime import datetime 6 | 7 | # adapted from: https://majornetwork.net/2019/10/webhook-listener-for-netbox/ 8 | 9 | from helpers.netbox_proxmox import NetBoxProxmoxHelper, NetBoxProxmoxHelperVM, NetBoxProxmoxHelperLXC 10 | 11 | from flask import Flask, Response, request, jsonify 12 | from flask_restx import Api, Resource, fields 13 | 14 | VERSION = '1.2.0' 15 | 16 | app_config_file = 'app_config.yml' 17 | 18 | with open(app_config_file) as yaml_cfg: 19 | try: 20 | app_config = yaml.safe_load(yaml_cfg) 21 | except yaml.YAMLError as exc: 22 | print(exc) 23 | 24 | if not 'netbox_webhook_name' in app_config: 25 | raise ValueError(f"'netbox_webhook_name' missing in {app_config_file}") 26 | 27 | app = Flask(__name__) 28 | api = Api(app, version=VERSION, title="NetBox-Proxmox Webhook Listener", 29 | description="NetBox-Proxmox Webhook Listener") 30 | ns = api.namespace(app_config['netbox_webhook_name']) 31 | 32 | # set debug (enabled/disabled) 33 | DEBUG = False 34 | 35 | if app.debug: 36 | DEBUG = True 37 | 38 | 39 | APP_NAME = "netbox-proxmox-webhook-listener" 40 | 41 | logger = logging.getLogger(APP_NAME) 42 | logger.setLevel(logging.INFO) 43 | formatter = logging.Formatter("%(asctime)s %(name)s %(levelname)s: %(message)s") 44 | file_logging = logging.FileHandler("{}.log".format(APP_NAME)) 45 | file_logging.setFormatter(formatter) 46 | logger.addHandler(file_logging) 47 | 48 | webhook_request = api.model("Webhook request from NetBox", { 49 | 'username': fields.String, 50 | 'data': fields.Raw(description="Object data from NetBox"), 51 | 'event': fields.String, 52 | 'timestamp': fields.String, 53 | 'model': fields.String, 54 | 'request_id': fields.String, 55 | }) 56 | 57 | # For session logging, c/o sol1 58 | session = { 59 | 'name': "netbox-webhook-flask-app", 60 | 'version': VERSION, 61 | 'version_lastrun': VERSION, 62 | 'server_start': "", 63 | 'status': { 64 | 'requests': 0, 65 | 'last_called': "" 66 | }, 67 | } 68 | 69 | 70 | @ns.route("/status/", methods=['GET']) 71 | class WebhookListener(Resource): 72 | @ns.expect(webhook_request) 73 | 74 | def get(self): 75 | _session = session.copy() 76 | _session['version_lastrun'] = VERSION 77 | _session['status']['requests'] += 1 78 | _session['status']['last_called'] = datetime.now() 79 | sanitized_full_path = request.full_path.replace('\r\n', '').replace('\n', '') 80 | sanitized_remote_addr = request.remote_addr.replace('\r\n', '').replace('\n', '') if request.remote_addr else 'Unknown' 81 | sanitized_data = request.get_data(as_text=True).replace('\r\n', '').replace('\n', '') if request.get_data() else '' 82 | logger.info(f"{sanitized_full_path}, {sanitized_remote_addr}, Status request with data {sanitized_data}") 83 | return jsonify(_session) 84 | 85 | 86 | # For handling event rules 87 | @ns.route("/") 88 | class WebhookListener(Resource): 89 | @ns.expect(webhook_request) 90 | def post(self): 91 | try: 92 | webhook_json_data = request.json 93 | except: 94 | webhook_json_data = {} 95 | 96 | sanitized_data = json.dumps(webhook_json_data).replace('\n', '').replace('\r', '') 97 | logger.info("User-provided data: {}".format(sanitized_data)) 98 | 99 | if not webhook_json_data or "model" not in webhook_json_data or "event" not in webhook_json_data: 100 | return {"result":"invalid input"}, 400 101 | 102 | results = (500, {'result': 'Default error message (obviously something has gone wrong)'}) 103 | 104 | if DEBUG: 105 | print(f"INCOMING DATA FOR WEBHOOK {webhook_json_data['event']} --> {webhook_json_data['model']}\n", json.dumps(webhook_json_data, indent=4)) 106 | 107 | if webhook_json_data['model'] == 'virtualmachine': 108 | if not 'proxmox_node' in webhook_json_data['data']['custom_fields']: 109 | results = 500, {'result': 'Missing proxmox_node in custom_fields'} 110 | 111 | proxmox_node = webhook_json_data['data']['custom_fields']['proxmox_node'] 112 | 113 | if webhook_json_data['data']['custom_fields']['proxmox_vm_type'] == 'vm': 114 | tc = NetBoxProxmoxHelperVM(app_config, proxmox_node, DEBUG) 115 | 116 | if webhook_json_data['data']['status']['value'] == 'staged': 117 | if webhook_json_data['event'] == 'created': 118 | results = tc.proxmox_clone_vm(webhook_json_data) 119 | elif webhook_json_data['event'] == 'updated': 120 | results = tc.proxmox_update_vm_vcpus_and_memory(webhook_json_data) 121 | 122 | if webhook_json_data['data']['primary_ip'] and webhook_json_data['data']['primary_ip']['address']: 123 | results = tc.proxmox_set_ipconfig0(webhook_json_data) 124 | 125 | if 'proxmox_public_ssh_key' in webhook_json_data['data']['custom_fields'] and webhook_json_data['data']['custom_fields']['proxmox_public_ssh_key']: 126 | results = tc.proxmox_set_ssh_public_key(webhook_json_data) 127 | elif webhook_json_data['event'] == 'deleted': 128 | results = tc.proxmox_delete_vm(webhook_json_data) 129 | elif webhook_json_data['event'] == 'updated': 130 | if webhook_json_data['data']['status']['value'] == 'offline': 131 | results = tc.proxmox_stop_vm(webhook_json_data) 132 | elif webhook_json_data['data']['status']['value'] == 'active': 133 | results = tc.proxmox_start_vm(webhook_json_data) 134 | else: 135 | results = (500, {'result': f"Unknown value {webhook_json_data['data']['status']['value']}"}) 136 | elif webhook_json_data['event'] == 'deleted': 137 | results = tc.proxmox_delete_vm(webhook_json_data) 138 | elif webhook_json_data['data']['custom_fields']['proxmox_vm_type'] == 'lxc': 139 | tc = NetBoxProxmoxHelperLXC(app_config, proxmox_node, DEBUG) 140 | 141 | if webhook_json_data['data']['status']['value'] == 'staged': 142 | if DEBUG: 143 | print(f"LXC STAGED INPUT {webhook_json_data['data']}", webhook_json_data['event']) 144 | 145 | if webhook_json_data['event'] == 'created': 146 | results = tc.proxmox_create_lxc(webhook_json_data) 147 | elif webhook_json_data['event'] == 'updated': 148 | if webhook_json_data['data']['primary_ip'] and webhook_json_data['data']['primary_ip']['address']: 149 | results = tc.proxmox_lxc_set_net0(webhook_json_data) 150 | 151 | if (webhook_json_data['snapshots']['prechange']['vcpus'] != webhook_json_data['snapshots']['postchange']['vcpus']) or (webhook_json_data['snapshots']['prechange']['memory'] != webhook_json_data['snapshots']['postchange']['memory']): 152 | results = tc.proxmox_update_lxc_vpus_and_memory(webhook_json_data) 153 | else: 154 | results = (200, {'result': 'No resources to change'}) 155 | elif webhook_json_data['event'] == 'deleted': 156 | results = tc.proxmox_delete_lxc(webhook_json_data) 157 | elif webhook_json_data['event'] == 'updated': 158 | if webhook_json_data['data']['status']['value'] == 'offline': 159 | results = tc.proxmox_stop_lxc(webhook_json_data) 160 | elif webhook_json_data['data']['status']['value'] == 'active': 161 | results = tc.proxmox_start_lxc(webhook_json_data) 162 | else: 163 | results = (500, {'result': f"Unknown value {webhook_json_data['data']['status']['value']}"}) 164 | elif webhook_json_data['event'] == 'deleted': 165 | results = tc.proxmox_delete_lxc(webhook_json_data) 166 | else: 167 | results = (500, {'result': f"Unknown event: {webhook_json_data['event']}"}) 168 | elif webhook_json_data['model'] == 'virtualdisk': 169 | results = 500, {'result': 'Something has gone wrong with virtualdisk management'} 170 | is_lxc = False 171 | 172 | if webhook_json_data['data']['name'] == 'rootfs': 173 | is_lxc = True 174 | 175 | if DEBUG: 176 | print("HERE VIRTUALDISK", is_lxc) 177 | 178 | tcall = NetBoxProxmoxHelper(app_config, None, DEBUG) 179 | proxmox_node = tcall.netbox_get_proxmox_node_from_vm_id(webhook_json_data['data']['virtual_machine']['id']) 180 | 181 | if is_lxc: 182 | if DEBUG: 183 | print("change disk lxc") 184 | 185 | if webhook_json_data['event'] == 'updated': 186 | if webhook_json_data['snapshots']['prechange']['size'] != webhook_json_data['snapshots']['postchange']['size']: 187 | tc = NetBoxProxmoxHelperLXC(app_config, proxmox_node, DEBUG) 188 | results = tc.proxmox_lxc_resize_disk(webhook_json_data) 189 | elif webhook_json_data['event'] == 'deleted': 190 | results = 200, {'result': 'All good'} 191 | else: 192 | tc = NetBoxProxmoxHelperVM(app_config, proxmox_node, DEBUG) 193 | 194 | if webhook_json_data['event'] == 'created': 195 | results = tc.proxmox_add_disk(webhook_json_data) 196 | elif webhook_json_data['event'] == 'updated': 197 | results = tc.proxmox_resize_disk(webhook_json_data) 198 | elif webhook_json_data['event'] == 'deleted': 199 | results = tc.proxmox_delete_disk(webhook_json_data) 200 | 201 | if DEBUG: 202 | print("RAW RESULTS", results) 203 | 204 | response = Response( 205 | json.dumps(results[1]), 206 | status = results[0], 207 | mimetype = 'application/json' 208 | ) 209 | 210 | return response.status_code, {'result': response.json['result']} 211 | 212 | 213 | if __name__ == "__main__": 214 | app.run(host="0.0.0.0") 215 | -------------------------------------------------------------------------------- /netbox-event-driven-automation-flask-app/app_config.yml-sample: -------------------------------------------------------------------------------- 1 | netbox_webhook_name: "netbox-proxmox-webhook" 2 | 3 | proxmox_api_config: 4 | api_host: proxmox-ip-or-hostname 5 | api_port: 8006 6 | api_user: proxmox_api_user 7 | api_token_id: name_of_proxmox_api_token 8 | api_token_secret: proxmox_api_secret_token 9 | verify_ssl: false 10 | netbox_api_config: 11 | api_proto: http # or https 12 | api_host: name or ip of NetBox host 13 | api_port: 8000 14 | api_token: netbox_api_secret_token 15 | verify_ssl: false # or true, up to you 16 | 17 | -------------------------------------------------------------------------------- /netbox-event-driven-automation-flask-app/requirements.txt: -------------------------------------------------------------------------------- 1 | aniso8601==9.0.1 2 | ansible-runner==2.4.0 3 | attrs==24.2.0 4 | blinker==1.8.2 5 | certifi==2024.7.4 6 | charset-normalizer==3.4.0 7 | click==8.1.7 8 | Flask==3.0.3 9 | flask-restx==1.3.0 10 | gunicorn==23.0.0 11 | idna==3.10 12 | importlib-metadata==6.2.1 13 | importlib_resources==6.4.5 14 | itsdangerous==2.2.0 15 | Jinja2==3.1.6 16 | jsonschema==4.23.0 17 | jsonschema-specifications==2024.10.1 18 | lockfile==0.12.2 19 | MarkupSafe==3.0.2 20 | packaging==24.1 21 | pexpect==4.9.0 22 | proxmoxer==2.2.0 23 | ptyprocess==0.7.0 24 | pynetbox==7.4.1 25 | python-daemon==3.1.0 26 | pytz==2024.2 27 | PyYAML==6.0.2 28 | referencing==0.35.1 29 | requests==2.32.3 30 | rpds-py==0.20.1 31 | six==1.16.0 32 | urllib3==2.2.3 33 | Werkzeug==3.1.1 34 | zipp==3.20.2 35 | -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | babel==2.16.0 2 | certifi==2024.8.30 3 | charset-normalizer==3.4.0 4 | click==8.1.7 5 | colorama==0.4.6 6 | ghp-import==2.1.0 7 | idna==3.10 8 | Jinja2==3.1.6 9 | Markdown==3.7 10 | MarkupSafe==3.0.2 11 | mergedeep==1.3.4 12 | mkdocs==1.6.1 13 | mkdocs-get-deps==0.2.0 14 | mkdocs-material==9.5.46 15 | mkdocs-material-extensions==1.3.1 16 | packaging==24.2 17 | paginate==0.5.7 18 | pathspec==0.12.1 19 | platformdirs==4.3.6 20 | proxmoxer==2.2.0 21 | Pygments==2.18.0 22 | pymdown-extensions==10.12 23 | pynetbox==7.4.1 24 | python-dateutil==2.9.0.post0 25 | PyYAML==6.0.2 26 | pyyaml_env_tag==0.1 27 | regex==2024.11.6 28 | requests==2.32.3 29 | six==1.16.0 30 | urllib3==2.2.3 31 | watchdog==6.0.0 32 | -------------------------------------------------------------------------------- /setup/configure_ansible_automation.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | 3 | import os, sys, re 4 | import argparse 5 | import yaml 6 | 7 | from helpers.ansible_automation_awx_manager import AnsibleAutomationAWXManager 8 | 9 | 10 | def get_arguments(): 11 | # Initialize the parser 12 | parser = argparse.ArgumentParser(description="Command-line Parsing for AWX (Tower/AAP) Object Creations") 13 | 14 | sub_parser = parser.add_subparsers(dest='action_type', 15 | required=True, 16 | description='Ansible Automation actions', 17 | help='additional help') 18 | 19 | aa_create = sub_parser.add_parser('create', help='create objects help action') 20 | aa_create.add_argument("--config", required=True, help="YAML file containing the configuration") 21 | 22 | aa_destroy = sub_parser.add_parser('destroy', help='destroy objects help action') 23 | aa_destroy.add_argument("--config", required=True, help="YAML file containing the configuration") 24 | 25 | # Parse the arguments 26 | args = parser.parse_args() 27 | 28 | # Return the parsed arguments 29 | return args 30 | 31 | 32 | def main(): 33 | args = get_arguments() 34 | app_config_file = args.config 35 | 36 | default_organization = 'Default' 37 | default_inventory = 'Default Inventory' 38 | 39 | default_host_name = 'localhost' 40 | default_host_var_data = "---\nansible_connection: local\nansible_python_interpreter: '{{ ansible_playbook_python }}'" 41 | 42 | default_execution_environment = 'netbox-proxmox-exec-env' 43 | default_execution_environment_image = 'localhost:5000/awx/ee/exec-env-test1:1.0.0' 44 | default_execution_environment_pull = 'Missing' 45 | 46 | default_project = 'netbox-proxmox-ee-test1' 47 | default_scm_type = 'git' 48 | default_scm_url = 'https://github.com/netboxlabs/netbox-proxmox-automation.git' 49 | default_scm_branch = 'main' 50 | 51 | credential_type = 'NetBox Proxmox Credential Type' 52 | credential_name = 'NetBox Proxmox Credentials Configuration' 53 | 54 | with open(app_config_file) as yaml_cfg: 55 | try: 56 | app_config = yaml.safe_load(yaml_cfg) 57 | except yaml.YAMLError as exc: 58 | raise ValueError(exc) 59 | except IOError as ioe: 60 | raise ValueError(ioe) 61 | 62 | if not 'ansible_automation' in app_config: 63 | raise ValueError(f"Missing 'ansible_automation' section in {app_config_file}") 64 | 65 | if not 'settings' in app_config['ansible_automation']: 66 | raise ValueError(f"Missing 'settings' in 'ansible_automation' section of {app_config_file}") 67 | 68 | 69 | # Set common variables 70 | org_name = default_organization 71 | if 'organization' in app_config['ansible_automation']['settings']: 72 | org_name = app_config['ansible_automation']['settings']['organization'] 73 | 74 | inventory_name = default_inventory 75 | if 'inventory' in app_config['ansible_automation']['settings']: 76 | if 'name' in app_config['ansible_automation']['settings']['inventory']: 77 | inventory_name = app_config['ansible_automation']['settings']['inventory']['name'] 78 | 79 | host_name = default_host_name 80 | host_var_data = default_host_var_data 81 | if 'hosts' in app_config['ansible_automation']['settings']: 82 | if 'name' in app_config['ansible_automation']['settings']['hosts']: 83 | host_name = app_config['ansible_automation']['settings']['hosts']['name'] 84 | 85 | if 'var_data' in app_config['ansible_automation']['settings']['hosts']: 86 | host_var_data = app_config['ansible_automation']['settings']['hosts']['var_data'] 87 | 88 | ee_name = default_execution_environment 89 | ee_image_name = default_execution_environment_image 90 | if 'execution_environment' in app_config['ansible_automation']['settings']: 91 | if 'name' in app_config['ansible_automation']['settings']['execution_environment']: 92 | ee_name = app_config['ansible_automation']['settings']['execution_environment']['name'] 93 | 94 | if 'image' in app_config['ansible_automation']['settings']['execution_environment']: 95 | ee_image_name = app_config['ansible_automation']['settings']['execution_environment']['image'] 96 | 97 | project_name = default_project 98 | if 'project' in app_config['ansible_automation']['settings']: 99 | if 'name' in app_config['ansible_automation']['settings']['project']: 100 | project_name = app_config['ansible_automation']['settings']['project']['name'] 101 | 102 | scm_type = default_scm_type 103 | if 'project' in app_config['ansible_automation']['settings']: 104 | if 'scm_type' in app_config['ansible_automation']['settings']['project']: 105 | scm_type = app_config['ansible_automation']['settings']['project']['scm_type'] 106 | 107 | scm_url = default_scm_url 108 | if 'project' in app_config['ansible_automation']['settings']: 109 | if 'scm_url' in app_config['ansible_automation']['settings']['project']: 110 | scm_url = app_config['ansible_automation']['settings']['project']['scm_url'] 111 | 112 | scm_branch = default_scm_branch 113 | if 'project' in app_config['ansible_automation']['settings']: 114 | if 'scm_branch' in app_config['ansible_automation']['settings']['project']: 115 | scm_branch = app_config['ansible_automation']['settings']['project']['scm_branch'] 116 | # End set common variables 117 | 118 | 119 | aam = AnsibleAutomationAWXManager(app_config) 120 | 121 | if args.action_type == 'create': 122 | aam.create_organization(org_name) 123 | 124 | aam.create_inventory(inventory_name) 125 | 126 | aam.create_host(host_name, host_var_data) 127 | 128 | aam.create_execution_environment(ee_name, ee_image_name) 129 | 130 | aam.create_project(project_name, scm_type, scm_url, scm_branch) 131 | 132 | project_playbooks = aam.get_playbooks() 133 | project_playbooks = [x for x in project_playbooks if x.startswith('awx-')] 134 | #print("project playbooks", project_playbooks) 135 | 136 | if not project_playbooks: 137 | print("I can't find any project playbooks. Therefore I cannot create any templates. Exiting.") 138 | sys.exit(1) 139 | 140 | aam.create_credential_type(credential_type) 141 | 142 | aam.create_credential(credential_name) 143 | 144 | for project_playbook in project_playbooks: 145 | aam.create_job_template(project_playbook) 146 | 147 | for created_job_template_item in aam.created_job_templates: 148 | aam.create_job_template_credential(created_job_template_item['id']) 149 | elif args.action_type == 'destroy': 150 | aam.get_project(project_name) 151 | 152 | aam.get_job_templates_for_project() 153 | 154 | if not aam.job_templates: 155 | print(f"Unable to find any job templates for project {project_name}!") 156 | sys.exit(1) 157 | 158 | collected_credentials = {} 159 | for project_template in aam.job_templates: 160 | related_credentials = aam.get_object_by_id('credentials', project_template.get_related('credentials').results[0]['id']) 161 | 162 | if related_credentials: 163 | related_credentials_type = aam.get_object_by_id('credential_types', related_credentials.get_related('credential_type')['id']) 164 | collected_credentials[related_credentials['name']] = related_credentials_type['name'] 165 | 166 | aam.delete_job_template(project_template) 167 | 168 | aam.delete_project() 169 | 170 | if credential_name in collected_credentials: 171 | aam.delete_credential(credential_name) 172 | 173 | if collected_credentials[credential_name] == credential_type: 174 | aam.delete_credential_type(credential_type) 175 | 176 | if host_name != default_host_name: 177 | aam.delete_host(host_name) 178 | 179 | if inventory_name != default_inventory: 180 | aam.delete_inventory(inventory_name) 181 | 182 | sys.exit(0) 183 | 184 | 185 | if __name__ == '__main__': 186 | main() 187 | -------------------------------------------------------------------------------- /setup/helpers/ansible_automation_awx.py: -------------------------------------------------------------------------------- 1 | import os, sys, re 2 | 3 | from awxkit import api, config, utils 4 | from awxkit.api import ApiV2, job_templates, projects 5 | from awxkit.api.resources import resources 6 | 7 | 8 | class AnsibleAutomationAWX: 9 | def __init__(self, cfg_data = {}): 10 | self.cfg_data = cfg_data['ansible_automation'] 11 | self.proxmox_cfg_data = cfg_data['proxmox_api_config'] 12 | self.netbox_cfg_data = cfg_data['netbox_api_config'] 13 | 14 | aa_url = f"{self.cfg_data['http_proto']}://{self.cfg_data['host']}:{str(self.cfg_data['http_port'])}/" 15 | 16 | if not aa_url.endswith('/'): 17 | aa_url += '/' 18 | 19 | aa_user = self.cfg_data['username'] 20 | aa_pass = self.cfg_data['password'] 21 | 22 | self.aa_base_url = aa_url 23 | 24 | config.base_url = self.aa_base_url 25 | config.credentials = utils.PseudoNamespace( 26 | {'default': 27 | {'username': aa_user, 'password': aa_pass} 28 | } 29 | ) 30 | 31 | connection = api.Api() 32 | connection.load_session().get() 33 | self.api_v2 = connection.available_versions.v2.get() 34 | 35 | 36 | def get_object_by_name(self, method_name = None, obj_name = None): 37 | method = getattr(self.api_v2, method_name) 38 | get_obj = method.get(name=obj_name)['results'] 39 | 40 | if get_obj: 41 | return get_obj[0] 42 | 43 | return {} 44 | 45 | 46 | def get_object_by_id(self, method_name = None, obj_id = None): 47 | method = getattr(self.api_v2, method_name) 48 | get_obj = method.get(id=obj_id)['results'] 49 | 50 | if get_obj: 51 | return get_obj[0] 52 | 53 | return {} 54 | 55 | 56 | def get_object_id(self, method_name = None, obj_name = None): 57 | object_id = 0 58 | 59 | found_obj = self.get_object_by_name(method_name, obj_name) 60 | 61 | if found_obj: 62 | object_id = found_obj['id'] 63 | 64 | return object_id 65 | 66 | 67 | def get_objects_by_kwargs(self, method_name = None, **kwargs): 68 | method = getattr(self.api_v2, method_name) 69 | get_obj = method.get(**kwargs)['results'] 70 | 71 | if get_obj: 72 | return get_obj 73 | 74 | return {} 75 | 76 | 77 | def create_object(self, method_name = None, obj_name = None, payload = {}): 78 | got_obj = self.get_object_by_name(method_name, obj_name) 79 | 80 | if not got_obj: 81 | method = getattr(self.api_v2, method_name) 82 | got_obj = method.post(payload) 83 | 84 | return got_obj 85 | 86 | 87 | def delete_object_by_name(self, method_name = None, obj_name = None): 88 | del_obj = self.get_object_by_name(method_name, obj_name) 89 | 90 | if not del_obj: 91 | return False 92 | 93 | try: 94 | return self.delete_object(del_obj) 95 | except: 96 | return False 97 | 98 | 99 | def delete_object(self, the_object = None): 100 | if not the_object: 101 | return False 102 | 103 | try: 104 | the_object.delete() 105 | return True 106 | except: 107 | return False 108 | 109 | 110 | 111 | -------------------------------------------------------------------------------- /setup/helpers/netbox_objects.py: -------------------------------------------------------------------------------- 1 | import pynetbox 2 | 3 | class Netbox: 4 | 5 | def _sanitize_value(self, key, value): 6 | # Mask sensitive fields 7 | sensitive_keys = {'password', 'token', 'secret'} 8 | if key in sensitive_keys: 9 | return '***' 10 | elif isinstance(value, dict): 11 | return {k: self._sanitize_value(k, v) for k, v in value.items()} 12 | elif isinstance(value, list): 13 | return [self._sanitize_value(key, v) for v in value] 14 | return value 15 | 16 | def _sanitize_payload(self): 17 | # Return a sanitized version of the payload 18 | return {key: self._sanitize_value(key, value) for key, value in self.payload.items()} 19 | def __init__(self, url, token, payload) -> None: 20 | # NetBox API details 21 | self.netbox_url = url 22 | self.netbox_token = token 23 | self.payload = payload 24 | self.object_type = None 25 | self.obj = None 26 | self.required_fields = [] 27 | self.init_api() 28 | 29 | 30 | def init_api(self): 31 | # Initialize pynetbox API connection 32 | self.nb = pynetbox.api(self.netbox_url, token=self.netbox_token) 33 | 34 | 35 | def findBy(self, key): 36 | self.obj = self.object_type.get(**{key: self.payload[key]}) 37 | 38 | @property 39 | def hasRequired(self): 40 | missing = [] 41 | for key in self.required_fields: 42 | if key not in self.payload: 43 | missing.append(key) 44 | if missing: 45 | print(f"missing required fields {', '.join(missing)}") 46 | return False 47 | else: 48 | return True 49 | 50 | def createOrUpdate(self): 51 | # If object exists see if we need to update it 52 | if self.obj: 53 | # Do we need to save? 54 | updated = False 55 | 56 | for key, value in self.payload.items(): 57 | if isinstance(value, dict): 58 | if hasattr(self.obj, key): 59 | child_key = next(iter(value)) 60 | child_value = value[child_key] 61 | if not hasattr(self.obj, key) or not hasattr(getattr(self.obj, key), child_key) or getattr(getattr(self.obj, key), child_key) != child_value: 62 | setattr(self.obj, key, value) 63 | updated = True 64 | print(f"Updated field '{key}' successfully.") 65 | else: 66 | if getattr(self.obj, key) != value: 67 | setattr(self.obj, key, value) 68 | updated = True 69 | print(f"Updated field '{key}' successfully.") 70 | if updated: 71 | self.obj.save() 72 | # TODO: error handling here 73 | print(f"Object updated successfully with sanitized payload: '{self._sanitize_payload()}'.") 74 | else: 75 | print(f"No changes detected for sanitized payload: '{self._sanitize_payload()}'.") 76 | # If the object doesn't exist then create it 77 | else: 78 | if self.hasRequired: 79 | self.object_type.create(self.payload) 80 | if 'name' in self.payload: 81 | print(f"Object (has required) '{self.payload['name']}' created successfully.") 82 | self.findBy('name') 83 | 84 | 85 | class NetBoxDeviceRoles(Netbox): 86 | def __init__(self, url, token, payload, find_key = 'name') -> None: 87 | # Initialize the Netbox superclass with URL and token 88 | super().__init__(url, token, payload) 89 | self.object_type = self.nb.dcim.device_roles 90 | self.required_fields = [ 91 | "name", 92 | "slug", 93 | "vm_role" 94 | ] 95 | self.find_key = find_key 96 | self.findBy(self.find_key) 97 | self.createOrUpdate() 98 | 99 | 100 | class NetBoxTags(Netbox): 101 | def __init__(self, url, token, payload, find_key = 'name') -> None: 102 | # Initialize the Netbox superclass with URL and token 103 | super().__init__(url, token, payload) 104 | self.object_type = self.nb.extras.tags 105 | self.required_fields = [ 106 | "name", 107 | "slug" 108 | ] 109 | self.find_key = find_key 110 | self.findBy(self.find_key) 111 | self.createOrUpdate() 112 | 113 | 114 | class NetboxCustomFields(Netbox): 115 | def __init__(self, url, token, payload, find_key = 'name') -> None: 116 | # Initialize the Netbox superclass with URL and token 117 | super().__init__(url, token, payload) 118 | self.object_type = self.nb.extras.custom_fields 119 | self.required_fields = [ 120 | "weight", 121 | "filter_logic", 122 | "search_weight", 123 | "object_types", 124 | "type", 125 | "name", 126 | ] 127 | self.find_key = find_key 128 | self.findBy(self.find_key) 129 | self.createOrUpdate() 130 | 131 | 132 | class NetboxCustomFieldChoiceSets(Netbox): 133 | def __init__(self, url, token, payload, find_key = 'name') -> None: 134 | # Initialize the Netbox superclass with URL and token 135 | super().__init__(url, token, payload) 136 | self.object_type = self.nb.extras.custom_field_choice_sets 137 | self.required_fields = [ 138 | "name", 139 | "extra_choices", 140 | 141 | ] 142 | self.find_key = find_key 143 | self.findBy(self.find_key) 144 | self.createOrUpdate() 145 | 146 | 147 | class NetboxClusterTypes(Netbox): 148 | def __init__(self, url, token, payload, find_key = 'name') -> None: 149 | # Initialize the Netbox superclass with URL and token 150 | super().__init__(url, token, payload) 151 | self.object_type = self.nb.virtualization.cluster_types 152 | self.required_fields = [ 153 | "name", 154 | "slug", 155 | ] 156 | self.find_key = find_key 157 | self.findBy(self.find_key) 158 | self.createOrUpdate() 159 | 160 | 161 | class NetboxClusters(Netbox): 162 | def __init__(self, url, token, payload, find_key = 'name') -> None: 163 | # Initialize the Netbox superclass with URL and token 164 | super().__init__(url, token, payload) 165 | self.object_type = self.nb.virtualization.clusters 166 | self.required_fields = [ 167 | "name", 168 | "type", 169 | "status", 170 | ] 171 | self.find_key = find_key 172 | self.findBy(self.find_key) 173 | self.createOrUpdate() 174 | 175 | 176 | class NetboxVirtualMachines(Netbox): 177 | def __init__(self, url, token, payload, find_key = 'name') -> None: 178 | # Initialize the Netbox superclass with URL and token 179 | super().__init__(url, token, payload) 180 | self.object_type = self.nb.virtualization.virtual_machines 181 | self.required_fields = [ 182 | "name", 183 | "cluster", 184 | "status" 185 | ] 186 | self.find_key = find_key 187 | self.findBy(self.find_key) 188 | self.createOrUpdate() 189 | 190 | 191 | class NetboxVirtualMachineInterface(Netbox): 192 | def __init__(self, url, token, payload, find_key = 'name') -> None: 193 | # Initialize the Netbox superclass with URL and token 194 | super().__init__(url, token, payload) 195 | 196 | """ 197 | self.object_type = self.nb.virtualization.interfaces 198 | self.required_fields = [ 199 | "name", 200 | "virtual_machine" 201 | ] 202 | self.find_key = find_key 203 | self.findBy(self.find_key) 204 | self.createOrUpdate() 205 | """ 206 | 207 | self.object_type = self.nb.virtualization.interfaces 208 | nb_vm_int = self.object_type.create(payload) 209 | 210 | 211 | class NetboxIPAddresses(Netbox): 212 | def __init__(self, url, token, payload, find_key = 'name') -> None: 213 | # Initialize the Netbox superclass with URL and token 214 | super().__init__(url, token, payload) 215 | self.object_type = self.nb.ipam.ip_addresses 216 | self.required_fields = [ 217 | "address", 218 | "status", 219 | ] 220 | self.find_key = find_key 221 | self.findBy(self.find_key) 222 | self.createOrUpdate() 223 | 224 | 225 | class NetboxWebhooks(Netbox): 226 | def __init__(self, url, token, payload, find_key = 'name') -> None: 227 | # Initialize the Netbox superclass with URL and token 228 | super().__init__(url, token, payload) 229 | self.object_type = self.nb.extras.webhooks 230 | self.required_fields = [ 231 | 'name', 232 | 'ssl_verification', 233 | 'http_method', 234 | 'http_content_type', 235 | 'payload_url', 236 | 'additional_headers', 237 | ] 238 | self.find_key = find_key 239 | self.findBy(self.find_key) 240 | self.createOrUpdate() 241 | 242 | 243 | class NetboxEventRules(Netbox): 244 | def __init__(self, url, token, payload, find_key = 'name') -> None: 245 | # Initialize the Netbox superclass with URL and token 246 | super().__init__(url, token, payload) 247 | self.object_type = self.nb.extras.event_rules 248 | self.required_fields = [ 249 | "name", 250 | "enabled", 251 | "object_types", 252 | "event_types", 253 | "action_type", 254 | "action_object_type", 255 | "action_object_id", 256 | "conditions" 257 | ] 258 | self.find_key = find_key 259 | self.findBy(self.find_key) 260 | self.createOrUpdate() 261 | -------------------------------------------------------------------------------- /setup/netbox-discovery-tool.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | 3 | import os, sys, re 4 | import argparse 5 | import yaml 6 | import json 7 | import pynetbox 8 | import proxmoxer 9 | 10 | from helpers.netbox_proxmox_api import NetBoxProxmoxAPIHelper 11 | from helpers.netbox_objects import Netbox, NetBoxTags, NetBoxDeviceRoles, NetboxClusterTypes, NetboxClusters, NetboxVirtualMachines, NetboxVirtualMachineInterface, NetboxIPAddresses 12 | 13 | nb_obj = None 14 | 15 | proxmox_to_netbox_vm_status_mappings = { 16 | False: 'offline', 17 | True: 'active' 18 | } 19 | 20 | 21 | def get_arguments(): 22 | # Initialize the parser 23 | parser = argparse.ArgumentParser(description="Import Netbox and Proxmox Configurations") 24 | 25 | # Add arguments for URL and Token 26 | sub_parser = parser.add_subparsers(dest='virt_type', 27 | required=True, 28 | description='Proxmox virtualization types', 29 | help='additional help') 30 | 31 | vms_action = sub_parser.add_parser('vm', help='vm help action') 32 | vms_action.add_argument("--config", required=True, help="YAML file containing the configuration") 33 | 34 | lxc_action = sub_parser.add_parser('lxc', help='lxc help action') 35 | lxc_action.add_argument("--config", required=True, help="YAML file containing the configuration") 36 | 37 | # Parse the arguments 38 | args = parser.parse_args() 39 | 40 | # Return the parsed arguments 41 | return args 42 | 43 | 44 | def __netbox_make_slug(in_str): 45 | return re.sub(r'\W+', '-', in_str).lower() 46 | 47 | 48 | def netbox_get_vms(nb_obj = None): 49 | nb_vm_info = {} 50 | 51 | try: 52 | for each_nb_vm in list(nb_obj.nb.virtualization.virtual_machines.all()): 53 | each_nb_vm_info = dict(each_nb_vm) 54 | 55 | if not each_nb_vm_info['name'] in nb_vm_info: 56 | nb_vm_info[each_nb_vm_info['name']] = {} 57 | 58 | nb_vm_info[each_nb_vm_info['name']]['is_proxmox_vm'] = False 59 | 60 | if each_nb_vm_info['custom_fields']['proxmox_vmid']: 61 | nb_vm_info[each_nb_vm_info['name']]['is_proxmox_vm'] = True 62 | nb_vm_info[each_nb_vm_info['name']]['id'] = int(each_nb_vm_info['custom_fields']['proxmox_vmid']) 63 | except pynetbox.RequestError as e: 64 | raise ValueError(e, e.error) 65 | 66 | return nb_vm_info 67 | 68 | 69 | def netbox_create_proxmox_discovered_object_tags(nb_obj = None, tag_name = None): 70 | try: 71 | nb_obj.nb.extras.tags.create([{'name': tag_name, 'slug': __netbox_make_slug(tag_name)}]) 72 | except pynetbox.RequestError as e: 73 | raise ValueError(e, e.error) 74 | 75 | 76 | def netbox_create_vm(nb_url = None, nb_api_token = None, proxmox_cluster_name = None, vm_configuration = {}, vm_name = None, vm_role_id = 0, tag_id = 0): 77 | try: 78 | create_vm_config = { 79 | 'name': vm_name, 80 | 'cluster': dict(NetboxClusters(nb_url, nb_api_token, {'name': proxmox_cluster_name}).obj)['id'], 81 | 'vcpus': str(vm_configuration['vcpus']), 82 | 'memory': vm_configuration['memory'], 83 | 'role': vm_role_id, 84 | 'status': proxmox_to_netbox_vm_status_mappings[vm_configuration['running']] 85 | } 86 | 87 | if tag_id > 0: 88 | create_vm_config['tags'] = [str(tag_id)] 89 | 90 | if not 'custom_fields' in create_vm_config: 91 | create_vm_config['custom_fields'] = {} 92 | 93 | if proxmox_to_netbox_vm_status_mappings[vm_configuration['running']]: 94 | if 'node' in vm_configuration: 95 | create_vm_config['custom_fields']['proxmox_node'] = vm_configuration['node'] 96 | 97 | if 'public_ssh_key' in vm_configuration: 98 | create_vm_config['custom_fields']['proxmox_public_ssh_key'] = vm_configuration['public_ssh_key'] 99 | 100 | if 'storage' in vm_configuration: 101 | create_vm_config['custom_fields']['proxmox_vm_storage'] = vm_configuration['storage'] 102 | 103 | if 'vmid' in vm_configuration: 104 | create_vm_config['custom_fields']['proxmox_vmid'] = vm_configuration['vmid'] 105 | 106 | create_vm_config['custom_fields']['proxmox_vm_type'] = 'vm' 107 | if 'is_lxc' in vm_configuration: 108 | create_vm_config['custom_fields']['proxmox_vm_type'] = 'lxc' 109 | 110 | # Don't take the default template (jammy, currently) for dicovered VM and LXC 111 | create_vm_config['custom_fields']['proxmox_vm_templates'] = '' 112 | 113 | nb_created_vm = NetboxVirtualMachines(nb_url, nb_api_token, create_vm_config) 114 | nb_created_vm_id = dict(nb_created_vm.obj)['id'] 115 | 116 | if 'network_interfaces' in vm_configuration: 117 | for network_interface in vm_configuration['network_interfaces']: 118 | network_interface_id = __netbox_create_vm_network_interface(nb_url, nb_api_token, nb_created_vm_id, network_interface, vm_configuration['network_interfaces'][network_interface]['mac-address']) 119 | 120 | for ip_address_entry in vm_configuration['network_interfaces'][network_interface]['ip-addresses']: 121 | ip_address_id = __netbox_vm_network_interface_assign_ip_address(nb_url, nb_api_token, network_interface_id, ip_address_entry['ip-address']) 122 | 123 | if network_interface == 'eth0' or network_interface == 'net0': 124 | if not ip_address_entry['ip-address'].endswith('/64'): 125 | __netbox_vm_network_interface_assign_primary_ip4_address(vm_name, network_interface_id, ip_address_entry['ip-address']) 126 | 127 | if 'disks' in vm_configuration: 128 | for vm_disk in vm_configuration['disks']: 129 | __netbox_vm_create_disk(vm_name, vm_disk['disk_name'], vm_disk['disk_size'], vm_disk['proxmox_disk_storage_volume']) 130 | except pynetbox.RequestError as e: 131 | raise ValueError(e, e.error) 132 | 133 | 134 | def __netbox_create_vm_network_interface(nb_url = None, nb_api_token = None, netbox_vm_id = 0, vm_network_interface_name = None, vm_network_interface_mac_address = None): 135 | try: 136 | print(f"Going to create network interface {vm_network_interface_name} on {netbox_vm_id}") 137 | 138 | nb_vm_create_interface_payload = { 139 | 'virtual_machine': str(netbox_vm_id), 140 | 'name': vm_network_interface_name, 141 | 'mac_address': vm_network_interface_mac_address 142 | } 143 | 144 | #nb_create_vm_interface = NetboxVirtualMachineInterface(nb_url, nb_api_token, nb_vm_create_interface_payload) 145 | #nb_create_vm_interface_id = dict(nb_create_vm_interface.obj)['id'] 146 | 147 | nb_create_vm_interface = nb_obj.nb.virtualization.interfaces.get(virtual_machine_id = netbox_vm_id, name = vm_network_interface_name, mac_address = vm_network_interface_mac_address) 148 | 149 | if not nb_create_vm_interface: 150 | nb_create_vm_interface = nb_obj.nb.virtualization.interfaces.create(nb_vm_create_interface_payload) 151 | 152 | nb_create_vm_interface_id = dict(nb_create_vm_interface)['id'] 153 | 154 | return nb_create_vm_interface_id 155 | except pynetbox.RequestError as e: 156 | raise ValueError(e, e.error) 157 | 158 | 159 | def __netbox_vm_network_interface_assign_ip_address(nb_url = None, nb_api_token = None, netbox_vm_network_interface_id = 0, ip_address = None): 160 | try: 161 | print(f"Going to assign IP address {ip_address} to {netbox_vm_network_interface_id}") 162 | 163 | nb_assign_ip_address_payload = { 164 | 'address': ip_address, 165 | 'status': 'active', 166 | 'assigned_object_type': 'virtualization.vminterface', 167 | 'assigned_object_id': str(netbox_vm_network_interface_id) 168 | } 169 | 170 | nb_assign_ip_address = NetboxIPAddresses(nb_url, nb_api_token, nb_assign_ip_address_payload, 'address') 171 | 172 | return nb_assign_ip_address 173 | except pynetbox.RequestError as e: 174 | raise ValueError(e, e.error) 175 | 176 | 177 | def __netbox_vm_network_interface_assign_primary_ip4_address(vm_name = None, primary_network_interface_id = 0, ip_address = None): 178 | try: 179 | print(f"Setting primary network interface on VM {vm_name} to {primary_network_interface_id} {ip_address}") 180 | 181 | assigned_vm_obj = nb_obj.nb.virtualization.virtual_machines.get(name=vm_name) 182 | 183 | if assigned_vm_obj: 184 | found_ip_address = nb_obj.nb.ipam.ip_addresses.get(address = ip_address) 185 | assigned_vm_obj.primary_ip4 = dict(found_ip_address)['id'] 186 | assigned_vm_obj.save() 187 | except pynetbox.RequestError as e: 188 | raise ValueError(e, e.error) 189 | 190 | 191 | def __netbox_vm_create_disk(vm_name = None, disk_name = None, disk_size = 0, proxmox_disk_storage_volume = None): 192 | try: 193 | print(f"Adding virtual disk {disk_name} (vol: {proxmox_disk_storage_volume}) VM {vm_name}") 194 | 195 | assigned_vm_obj = nb_obj.nb.virtualization.virtual_machines.get(name=vm_name) 196 | assigned_disk_obj = nb_obj.nb.virtualization.virtual_disks.get(name=disk_name, virtual_machine=vm_name) 197 | 198 | if assigned_vm_obj and not assigned_disk_obj: 199 | nb_vm_id = dict(assigned_vm_obj)['id'] 200 | 201 | virtual_disks_payload = { 202 | 'virtual_machine': { 203 | 'name': vm_name, 204 | 'id': nb_vm_id 205 | }, 206 | 'name': disk_name, 207 | 'size': int(disk_size), 208 | 'custom_fields': { 209 | 'proxmox_disk_storage_volume': proxmox_disk_storage_volume 210 | } 211 | } 212 | 213 | created_vm_disk = nb_obj.nb.virtualization.virtual_disks.create(virtual_disks_payload) 214 | created_vm_disk_id = dict(created_vm_disk)['id'] 215 | 216 | return created_vm_disk_id 217 | except pynetbox.RequestError as e: 218 | raise ValueError(e, e.error) 219 | 220 | 221 | def main(): 222 | global nb_obj 223 | 224 | args = get_arguments() 225 | app_config_file = args.config 226 | 227 | with open(app_config_file) as yaml_cfg: 228 | try: 229 | app_config = yaml.safe_load(yaml_cfg) 230 | except yaml.YAMLError as exc: 231 | raise ValueError(exc) 232 | except IOError as ioe: 233 | raise ValueError(ioe) 234 | 235 | nb_url = f"{app_config['netbox_api_config']['api_proto']}://{app_config['netbox_api_config']['api_host']}:{str(app_config['netbox_api_config']['api_port'])}/" 236 | nb_obj = Netbox(nb_url, app_config['netbox_api_config']['api_token'], None) 237 | 238 | # Collect all NetBox VMs, and for Proxmox VMs: VMIDs 239 | all_nb_vms = netbox_get_vms(nb_obj) 240 | 241 | all_nb_vms_ids = {} 242 | 243 | for all_nb_vm in all_nb_vms: 244 | all_nb_vms_ids[all_nb_vms[all_nb_vm]['id']] = all_nb_vm 245 | 246 | pm = NetBoxProxmoxAPIHelper(app_config) 247 | 248 | if args.virt_type == 'vm': 249 | device_role_id = dict(NetBoxDeviceRoles(nb_url, app_config['netbox_api_config']['api_token'], {'name': app_config['netbox']['vm_role'], 'slug': __netbox_make_slug(app_config['netbox']['vm_role']), 'vm_role': True, 'color': 'ffbf00'}).obj)['id'] 250 | 251 | proxmox_vm_configurations = pm.proxmox_get_vms_configurations() 252 | 253 | for proxmox_vm_configuration in proxmox_vm_configurations: 254 | if not proxmox_vm_configuration in all_nb_vms and not pm.proxmox_vms[proxmox_vm_configuration]['vmid'] in all_nb_vms_ids: 255 | nbt_vm_discovered_id = dict(NetBoxTags(nb_url, app_config['netbox_api_config']['api_token'], {'name': 'proxmox-vm-discovered', 'slug': __netbox_make_slug('proxmox-vm-discovered'), 'color': 'aa1409'}).obj)['id'] 256 | else: 257 | nbt_vm_discovered_id = 0 258 | 259 | netbox_create_vm(nb_url, app_config['netbox_api_config']['api_token'], app_config['proxmox']['cluster_name'], proxmox_vm_configurations[proxmox_vm_configuration], proxmox_vm_configuration, device_role_id, nbt_vm_discovered_id) 260 | elif args.virt_type == 'lxc': 261 | device_role_id = dict(NetBoxDeviceRoles(nb_url, app_config['netbox_api_config']['api_token'], {'name': app_config['netbox']['lxc_role'], 'slug': __netbox_make_slug(app_config['netbox']['lxc_role']), 'vm_role': True, 'color': 'ff9800'}).obj)['id'] 262 | 263 | proxmox_lxc_configurations = pm.proxmox_get_lxc_configurations() 264 | 265 | for proxmox_lxc_configuration in proxmox_lxc_configurations: 266 | if not proxmox_lxc_configuration in all_nb_vms and not pm.proxmox_lxc[proxmox_lxc_configuration]['vmid'] in all_nb_vms_ids: 267 | nbt_lxc_discovered_id = dict(NetBoxTags(nb_url, app_config['netbox_api_config']['api_token'], {'name': 'proxmox-lxc-discovered', 'slug': __netbox_make_slug('proxmox-lxc-discovered'), 'color': 'f44336'}).obj)['id'] 268 | else: 269 | nbt_lxc_discovered_id = 0 270 | 271 | netbox_create_vm(nb_url, app_config['netbox_api_config']['api_token'], app_config['proxmox']['cluster_name'], proxmox_lxc_configurations[proxmox_lxc_configuration], proxmox_lxc_configuration, device_role_id, nbt_lxc_discovered_id) 272 | else: 273 | print(f"Unknown virtualizaton type {args.virt_type}") 274 | sys.exit(1) 275 | 276 | sys.exit(0) 277 | 278 | 279 | if __name__ == "__main__": 280 | main() 281 | -------------------------------------------------------------------------------- /setup/netbox_setup_objects_and_custom_fields.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | 3 | import os, sys, re 4 | import argparse 5 | import yaml 6 | import json 7 | import pynetbox 8 | 9 | # adapted from sol1 implementation 10 | from helpers.netbox_objects import NetboxCustomFields, NetboxCustomFieldChoiceSets, NetboxClusterTypes, NetboxClusters 11 | 12 | from helpers.netbox_proxmox_api import NetBoxProxmoxAPIHelper 13 | 14 | 15 | def get_arguments(): 16 | # Initialize the parser 17 | parser = argparse.ArgumentParser(description="Import Netbox and Proxmox Configurations") 18 | 19 | # Add arguments for URL and Token 20 | parser.add_argument("--config", required=True, help="YAML file containing the configuration") 21 | 22 | # Parse the arguments 23 | args = parser.parse_args() 24 | 25 | # Return the parsed arguments 26 | return args 27 | 28 | 29 | def __netbox_make_slug(in_str): 30 | return re.sub(r'\W+', '-', in_str).lower() 31 | 32 | 33 | def create_custom_field_choice_sets_proxmox_vm_templates(proxmox_api_obj): 34 | proxmox_api_obj.proxmox_get_vm_templates() 35 | extra_choices = [] 36 | 37 | for k, v in proxmox_api_obj.proxmox_vm_templates.items(): 38 | extra_choices.append([str(k), v]) 39 | 40 | ncfcs = NetboxCustomFieldChoiceSets(netbox_url, netbox_api_token, {'name': 'proxmox-vm-templates', 'extra_choices': extra_choices}) 41 | return dict(ncfcs.obj)['id'] 42 | 43 | 44 | def create_custom_field_choice_sets_proxmox_vm_storage(proxmox_api_obj): 45 | proxmox_api_obj.proxmox_get_vm_storage_volumes() 46 | print(proxmox_api_obj.proxmox_storage_volumes) 47 | 48 | extra_choices = [] 49 | 50 | for psv in proxmox_api_obj.proxmox_storage_volumes: 51 | extra_choices.append([psv, psv]) 52 | 53 | ncfcs = NetboxCustomFieldChoiceSets(netbox_url, netbox_api_token, {'name': 'proxmox-vm-storage', 'extra_choices': extra_choices}) 54 | return dict(ncfcs.obj)['id'] 55 | 56 | 57 | def create_custom_field_choice_sets_proxmox_lxc_templates(proxmox_api_obj): 58 | extra_choices = [] 59 | 60 | for proxmox_node in proxmox_api_obj.proxmox_nodes: 61 | proxmox_api_obj.proxmox_get_lxc_templates(proxmox_node) 62 | 63 | proxmox_lxc_templates = proxmox_api_obj.proxmox_lxc_templates 64 | 65 | if proxmox_lxc_templates: 66 | for psv in proxmox_lxc_templates: 67 | extra_choices.append([psv, proxmox_lxc_templates[psv]]) 68 | 69 | ncfcs = NetboxCustomFieldChoiceSets(netbox_url, netbox_api_token, {'name': 'proxmox-lxc-templates', 'extra_choices': extra_choices}) 70 | return dict(ncfcs.obj)['id'] 71 | 72 | return 0 73 | 74 | 75 | def create_custom_field_choice_sets_proxmox_vm_cluster_nodes(proxmox_api_obj): 76 | extra_choices = [] 77 | 78 | # get proxmox nodes 79 | #proxmox_api_obj.get_proxmox_nodes() 80 | proxmox_cluster_nodes = proxmox_api_obj.proxmox_nodes 81 | 82 | for pcn in proxmox_cluster_nodes: 83 | extra_choices.append([pcn, pcn]) 84 | 85 | ncfcs = NetboxCustomFieldChoiceSets(netbox_url, netbox_api_token, {'name': 'proxmox-cluster-nodes', 'extra_choices': extra_choices}) 86 | return dict(ncfcs.obj)['id'] 87 | 88 | 89 | def create_custom_field_choice_sets_proxmox_vm_type(proxmox_api_obj): 90 | extra_choices = [] 91 | 92 | proxmox_vm_types = { 93 | 'vm': 'Virtual Machine', 94 | 'lxc': 'LXC Container' 95 | } 96 | 97 | for proxmox_vm_type in proxmox_vm_types: 98 | extra_choices.append([proxmox_vm_type, proxmox_vm_types[proxmox_vm_type]]) 99 | 100 | ncfcs = NetboxCustomFieldChoiceSets(netbox_url, netbox_api_token, {'name': 'proxmox-vm-type', 'extra_choices': extra_choices}) 101 | return dict(ncfcs.obj)['id'] 102 | 103 | 104 | def create_custom_field(netbox_url=None, netbox_api_token=None, name=None, label=None, choice_set_id=0, default=None): 105 | weight = 100 106 | description = '' 107 | 108 | if name in ['proxmox_lxc_templates']: 109 | group_name = 'Proxmox LXC' 110 | elif name in ['proxmox_node', 'proxmox_vmid', 'proxmox_vm_storage', 'proxmox_vm_type', 'proxmox_public_ssh_key']: 111 | group_name = 'Proxmox (common)' 112 | 113 | if name == 'proxmox_vm_type': 114 | weight = 200 115 | elif name == 'proxmox_vmid': 116 | weight = 300 117 | elif name == 'proxmox_vm_storage': 118 | weight = 400 119 | elif name == 'proxmox_public_ssh_key': 120 | weight = 500 121 | else: 122 | group_name = 'Proxmox VM' 123 | 124 | if name in ['proxmox_node', 'proxmox_vm_storage', 'proxmox_vm_templates', 'proxmox_lxc_templates', 'proxmox_vm_type']: 125 | object_types = ['virtualization.virtualmachine'] 126 | input_type = {'value': 'select', 'label': 'Selection'} 127 | elif name in ['proxmox_disk_storage_volume']: 128 | object_types = ['virtualization.virtualdisk'] 129 | input_type = {'value': 'select', 'label': 'Selection'} 130 | elif name in ['proxmox_public_ssh_key']: 131 | object_types = ['virtualization.virtualmachine'] 132 | input_type = {'value': 'longtext', 'label': 'Text (long)'} 133 | description = 'For LXC (containers), you MUST add a SSH public key BEFORE you create the VM in NetBox' 134 | elif name in ['proxmox_vmid']: 135 | object_types = ['virtualization.virtualmachine'] 136 | input_type = {'value': 'text', 'label': 'Text'} 137 | elif name in ['proxmox_is_lxc_container']: 138 | object_types = ['virtualization.virtualmachine'] 139 | input_type = {'value': "boolean", 'label': 'Boolean (true/false)'} 140 | 141 | if input_type['value'] == 'select': 142 | nbcf = NetboxCustomFields(netbox_url, netbox_api_token, 143 | {'weight': weight, 144 | #'filter_logic': {'value': 'loose', 'label': 'Loose'}, 145 | 'filter_logic': 'disabled', 146 | 'search_weight': 1000, 147 | 'object_types': object_types, 148 | 'type': input_type['value'], 149 | 'group_name': group_name, 150 | 'name': name, 151 | 'label': label, 152 | 'choice_set': choice_set_id, 153 | 'default': default}) 154 | elif input_type['value'] == 'text': 155 | nbcf = NetboxCustomFields(netbox_url, netbox_api_token, 156 | {'weight': weight, 157 | 'filter_logic': 'disabled', 158 | 'search_weight': 1000, 159 | 'object_types': object_types, 160 | 'type': input_type['value'], 161 | 'group_name': group_name, 162 | 'name': name, 163 | 'label': label}) 164 | elif input_type['value'] == 'longtext': 165 | nbcf = NetboxCustomFields(netbox_url, netbox_api_token, 166 | {'weight': weight, 167 | 'filter_logic': 'disabled', 168 | 'search_weight': 1000, 169 | 'object_types': object_types, 170 | 'type': input_type, 171 | 'group_name': group_name, 172 | 'name': name, 173 | 'label': label, 174 | 'description': description}) 175 | elif input_type['value'] == 'boolean': 176 | nbcf = NetboxCustomFields(netbox_url, netbox_api_token, 177 | {'weight': weight, 178 | 'filter_logic': 'disabled', 179 | 'search_weight': 1000, 180 | 'object_types': object_types, 181 | 'type': input_type, 182 | 'group_name': group_name, 183 | 'name': name, 184 | 'label': label, 185 | 'required': True, 186 | 'default': False, 187 | 'label': label}) 188 | 189 | return dict(nbcf.obj)['id'] 190 | 191 | 192 | if __name__ == "__main__": 193 | default_netbox_proxmox_name = 'Proxmox' 194 | default_netbox_cluster_role = default_netbox_proxmox_name 195 | default_netbox_cluster_name = 'proxmox-ve' 196 | default_netbox_vm_role = 'Proxmox VM' 197 | default_netbox_lxc_role = 'Proxmox LXC' 198 | 199 | args = get_arguments() 200 | 201 | app_config_file = args.config 202 | 203 | with open(app_config_file) as yaml_cfg: 204 | try: 205 | app_config = yaml.safe_load(yaml_cfg) 206 | except yaml.YAMLError as exc: 207 | raise ValueError(exc) 208 | except IOError as ioe: 209 | raise ValueError(ioe) 210 | 211 | netbox_url = f"{app_config['netbox_api_config']['api_proto']}://{app_config['netbox_api_config']['api_host']}:{app_config['netbox_api_config']['api_port']}/" 212 | netbox_api_token = f"{app_config['netbox_api_config']['api_token']}" 213 | 214 | # init NetBox Proxmox API integration 215 | p = NetBoxProxmoxAPIHelper(app_config) 216 | 217 | # setup defaults and override from config values later 218 | proxmox_cluster_name = default_netbox_cluster_name 219 | vm_cluster_role = default_netbox_cluster_role 220 | vm_role = default_netbox_vm_role 221 | lxc_role = default_netbox_lxc_role 222 | 223 | if 'proxmox' in app_config: 224 | if 'cluster_name' in app_config['proxmox']: 225 | proxmox_cluster_name = app_config['proxmox']['cluster_name'] 226 | 227 | if 'netbox' in app_config: 228 | if 'cluster_role' in app_config['netbox']: 229 | vm_cluster_role = app_config['netbox']['cluster_role'] 230 | 231 | if 'vm_role' in app_config['netbox']: 232 | vm_role = app_config['netbox']['vm_role'] 233 | 234 | if 'lxc_role' in app_config['netbox']: 235 | lxc_role = app_config['netbox']['lxc_role'] 236 | 237 | # vm clusters and types 238 | nbct = NetboxClusterTypes(netbox_url, netbox_api_token, {'name': vm_cluster_role, 'slug': __netbox_make_slug(vm_cluster_role)}) 239 | netbox_cluster_type_id = dict(nbct.obj)['id'] 240 | 241 | nbc = NetboxClusters(netbox_url, netbox_api_token, {'name': proxmox_cluster_name, 'type': netbox_cluster_type_id, 'status': 'active'}) 242 | netbox_cluster_id = dict(nbc.obj)['id'] 243 | 244 | # custom field choice sets 245 | netbox_field_choice_sets_templates_id = create_custom_field_choice_sets_proxmox_vm_templates(p) 246 | 247 | netbox_field_choice_sets_vm_storage_volumes_id = create_custom_field_choice_sets_proxmox_vm_storage(p) 248 | 249 | netbox_field_choice_sets_lxc_templates_id = create_custom_field_choice_sets_proxmox_lxc_templates(p) 250 | 251 | netbox_field_choice_sets_proxmox_nodes_id = create_custom_field_choice_sets_proxmox_vm_cluster_nodes(p) 252 | 253 | netbox_field_choice_sets_proxmox_vm_types_id = create_custom_field_choice_sets_proxmox_vm_type(p) 254 | 255 | # custom fields 256 | 257 | # VM template id 258 | custom_field_template_id = create_custom_field(netbox_url, netbox_api_token, 'proxmox_vm_templates', 'Proxmox VM Templates', netbox_field_choice_sets_templates_id, str(min(p.proxmox_vm_templates.keys()))) 259 | 260 | # VM proxmox node id 261 | custom_field_proxmox_node_id = create_custom_field(netbox_url, netbox_api_token, 'proxmox_node', 'Proxmox node', netbox_field_choice_sets_proxmox_nodes_id, p.proxmox_nodes[0]) 262 | 263 | # proxmox_vmid 264 | custom_field_proxmox_vm_id = create_custom_field(netbox_url, netbox_api_token, 'proxmox_vmid', 'Proxmox Virtual machine ID (vmid)') 265 | 266 | # proxmox_public_ssh_key 267 | custom_field_proxmox_public_ssh_key_id = create_custom_field(netbox_url, netbox_api_token, 'proxmox_public_ssh_key', 'Proxmox public SSH key') 268 | 269 | # proxmox_disk_storage_volume 270 | custom_field_proxmox_disk_storage_volume_id = create_custom_field(netbox_url, netbox_api_token, 'proxmox_disk_storage_volume', 'Proxmox Disk Storage Volume', netbox_field_choice_sets_vm_storage_volumes_id, p.proxmox_storage_volumes[0]) 271 | 272 | # proxmox_vm_storage 273 | custom_field_proxmox_disk_storage_volume_id = create_custom_field(netbox_url, netbox_api_token, 'proxmox_vm_storage', 'Proxmox VM Storage', netbox_field_choice_sets_vm_storage_volumes_id, p.proxmox_storage_volumes[0]) 274 | 275 | # proxmox_vm_type 276 | custom_field_proxmox_vm_type = create_custom_field(netbox_url, netbox_api_token, 'proxmox_vm_type', 'Proxmox VM Type', netbox_field_choice_sets_proxmox_vm_types_id, 'vm') 277 | 278 | # proxmox_lxc_templates 279 | if netbox_field_choice_sets_lxc_templates_id > 0: 280 | #custom_field_proxmox_disk_storage_volume_id = create_custom_field(netbox_url, netbox_api_token, 'proxmox_lxc_templates', 'Proxmox LXC Templates', netbox_field_choice_sets_lxc_templates_id, str(min(p.proxmox_lxc_templates.keys()))) 281 | if len(p.proxmox_lxc_templates.keys()): 282 | custom_field_proxmox_disk_storage_volume_id = create_custom_field(netbox_url, netbox_api_token, 'proxmox_lxc_templates', 'Proxmox LXC Templates', netbox_field_choice_sets_lxc_templates_id, str(min(p.proxmox_lxc_templates.keys()))) 283 | else: 284 | custom_field_proxmox_disk_storage_volume_id = create_custom_field(netbox_url, netbox_api_token, 'proxmox_lxc_templates', 'Proxmox LXC Templates', netbox_field_choice_sets_lxc_templates_id, "") 285 | -------------------------------------------------------------------------------- /setup/requirements.txt: -------------------------------------------------------------------------------- 1 | awxkit==24.6.1 2 | babel==2.16.0 3 | certifi==2024.8.30 4 | charset-normalizer==3.4.0 5 | click==8.1.7 6 | colorama==0.4.6 7 | ghp-import==2.1.0 8 | idna==3.10 9 | Jinja2==3.1.6 10 | Markdown==3.7 11 | MarkupSafe==3.0.2 12 | mergedeep==1.3.4 13 | mkdocs==1.6.1 14 | mkdocs-get-deps==0.2.0 15 | mkdocs-material==9.5.46 16 | mkdocs-material-extensions==1.3.1 17 | packaging==24.2 18 | paginate==0.5.7 19 | pathspec==0.12.1 20 | platformdirs==4.3.6 21 | proxmoxer==2.2.0 22 | Pygments==2.18.0 23 | pymdown-extensions==10.12 24 | pynetbox==7.4.1 25 | python-dateutil==2.9.0.post0 26 | PyYAML==6.0.2 27 | pyyaml_env_tag==0.1 28 | regex==2024.11.6 29 | requests==2.32.3 30 | six==1.16.0 31 | urllib3==2.2.3 32 | watchdog==6.0.0 33 | -------------------------------------------------------------------------------- /templates/bind9/zone-template.j2: -------------------------------------------------------------------------------- 1 | ; 2 | ; Auto generated -- DO NOT EDIT! 3 | ; 4 | $ORIGIN {{ dns_zone_origin }}. 5 | $TTL {{ dns_zone_ttl }} 6 | @ IN SOA {{ collected_soa['mname'] }} {{ collected_soa['rname'] }} ( 7 | {{ collected_soa['serial'] }} ; serial, today's date + today's serial # 8 | {{ collected_soa['refresh'] }} ; refresh, seconds 9 | {{ collected_soa['retry'] }} ; retry, seconds 10 | {{ collected_soa['expire'] }} ; expire,seconds 11 | {{ collected_soa['minimum'] }} ) ; minimum TTL, seconds 12 | 13 | ; 14 | {% for ns in collected_ns %} 15 | NS {{ ns }}. 16 | {% endfor %} 17 | 18 | ; 19 | 20 | {% for mx in collected_mx %} 21 | MX {{ mx }}. 22 | {% endfor %} 23 | 24 | ; 25 | ; DNS entries 26 | ; 27 | {% for dns_entry in collected_rr|sort(attribute='name') %} 28 | {% if dns_entry['type'] != 'PTR' %} 29 | {{ "{:<24} {:<10} {:<8} {}".format(dns_entry['name'], dns_entry['ttl'], dns_entry['type'], dns_entry['value']) }} 30 | {% else %} 31 | {{ "{:<4} {:<10} {:<8} {}".format(dns_entry['name'], dns_entry['ttl'], dns_entry['type'], dns_entry['value']) }} 32 | {% endif %} 33 | {% endfor %} 34 | ; 35 | ; end DNS entries 36 | ; 37 | 38 | --------------------------------------------------------------------------------