├── .env ├── .github └── workflows │ ├── docker-image.yml │ ├── markdown-lint.yml │ └── python.yml ├── .gitignore ├── CHANGELOG.md ├── CODE_OF_CONDUCT.md ├── CONTRIBUTING.md ├── LICENSE ├── MAINTENANCE.md ├── MANIFEST.in ├── README.md ├── SECURITY.md ├── ci ├── .vale.ini ├── Dockerfile ├── Dockerfile-CI ├── config │ └── configuration.py ├── docker-compose.ci.yml ├── docker-compose.yml ├── env │ ├── netbox.env │ ├── postgres.env │ ├── redis-cache.env │ └── redis.env ├── reports │ └── .gitkeep └── styles │ └── config │ └── vocabularies │ └── Base │ ├── accept.txt │ └── reject.txt ├── netbox_slm ├── __init__.py ├── admin.py ├── api │ ├── __init__.py │ ├── serializers.py │ ├── urls.py │ └── views.py ├── filtersets.py ├── forms │ ├── __init__.py │ ├── software_license.py │ ├── software_product.py │ ├── software_product_installation.py │ └── software_product_version.py ├── migrations │ ├── 0001_initial.py │ ├── 0002_alter_softwareproduct_manufacturer.py │ ├── 0003_auto_20220407_1209.py │ ├── 0004_auto_20220415_0705.py │ ├── 0005_add_software_license.py │ ├── 0006_softwarelicense_stored_location_url.py │ ├── 0007_softwareproductinstallation_cluster.py │ ├── 0008_software_release_types_and_more.py │ ├── 0009_softwareproductversion_description.py │ ├── 0010_softwarelicense_spdx_expression.py │ └── __init__.py ├── models.py ├── navigation.py ├── tables.py ├── template_content.py ├── templates │ └── netbox_slm │ │ ├── installations_card_include.html │ │ ├── softwarelicense.html │ │ ├── softwareproduct.html │ │ ├── softwareproductinstallation.html │ │ └── softwareproductversion.html ├── templatetags │ └── __init__.py ├── tests │ ├── __init__.py │ ├── base.py │ ├── test_functional.py │ └── test_models.py ├── urls.py └── views │ ├── __init__.py │ ├── software_license.py │ ├── software_product.py │ ├── software_product_installation.py │ └── software_product_version.py ├── pyproject.toml ├── sonar-project.properties └── start-netbox.sh /.env: -------------------------------------------------------------------------------- 1 | COMPOSE_PATH_SEPARATOR=: 2 | COMPOSE_FILE=ci/docker-compose.yml:ci/docker-compose.ci.yml 3 | COMPOSE_PROJECT_NAME=netbox-docker 4 | -------------------------------------------------------------------------------- /.github/workflows/docker-image.yml: -------------------------------------------------------------------------------- 1 | --- 2 | name: Docker Image CI 3 | 4 | on: 5 | push: 6 | branches: 7 | - master 8 | pull_request: 9 | types: [opened, synchronize, reopened] 10 | 11 | jobs: 12 | build: 13 | runs-on: ubuntu-latest 14 | steps: 15 | - uses: actions/checkout@v4 16 | with: 17 | fetch-depth: 0 18 | 19 | - name: Build the Docker image 20 | run: docker compose build --no-cache 21 | 22 | - name: Verify the Docker image 23 | # smoke test by checking if the migrations for app netbox_slm can be displayed 24 | run: docker compose run netbox sh -c "/opt/netbox/venv/bin/python manage.py showmigrations netbox_slm" 25 | 26 | - name: Run Django tests with coverage for netbox_slm 27 | run: | 28 | chmod go+w ci/reports 29 | docker compose run netbox sh -c "\ 30 | /opt/netbox/venv/bin/coverage run --source='netbox_slm' manage.py test netbox_slm &&\ 31 | /opt/netbox/venv/bin/coverage report --fail-under=0 &&\ 32 | /opt/netbox/venv/bin/coverage xml -o /ci/reports/coverage.xml" 33 | sed -i "s/\/opt\/netbox\/netbox<\/source>/<\/source>/" ci/reports/coverage.xml 34 | timeout-minutes: 6 35 | 36 | - name: Sonar scan 37 | if: env.SONAR_TOKEN != null 38 | uses: SonarSource/sonarqube-scan-action@v6 39 | env: 40 | SONAR_TOKEN: ${{ secrets.SONAR_TOKEN }} 41 | -------------------------------------------------------------------------------- /.github/workflows/markdown-lint.yml: -------------------------------------------------------------------------------- 1 | --- 2 | name: Markdown lint 3 | 4 | on: 5 | push: 6 | branches: 7 | - master 8 | pull_request: 9 | types: [opened, synchronize, reopened] 10 | 11 | jobs: 12 | vale: 13 | runs-on: ubuntu-latest 14 | steps: 15 | - uses: actions/checkout@v4 16 | 17 | - uses: errata-ai/vale-action@v2.1.1 18 | with: 19 | fail_on_error: true 20 | filter_mode: nofilter 21 | vale_flags: "--glob=*.md --config=ci/.vale.ini" 22 | -------------------------------------------------------------------------------- /.github/workflows/python.yml: -------------------------------------------------------------------------------- 1 | --- 2 | name: Python 3 | 4 | on: 5 | push: 6 | branches: 7 | - master 8 | pull_request: 9 | types: [opened, synchronize, reopened] 10 | 11 | jobs: 12 | build: 13 | runs-on: ubuntu-latest 14 | strategy: 15 | matrix: 16 | include: 17 | - python-version: 3.11 18 | - python-version: 3.12 19 | - python-version: 3.13 20 | steps: 21 | - uses: actions/checkout@v4 22 | 23 | - name: Set up Python 24 | uses: actions/setup-python@v5 25 | with: 26 | python-version: ${{ matrix.python-version }} 27 | 28 | - name: Install ci tools 29 | run: pip install .[ci] 30 | 31 | - name: Run ruff lint 32 | run: ruff check netbox_slm 33 | 34 | - name: Run ruff format 35 | run: ruff format --check netbox_slm 36 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | **/netbox 2 | **/netbox-docker 3 | **/*.pyc 4 | **/__pycache__ 5 | **/.idea 6 | **/.vscode 7 | **/dist 8 | **/netbox_slm.egg-info 9 | ci/docker-compose.override.yml 10 | ci/reports 11 | out/production 12 | *.iml 13 | build/ 14 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | 3 | ## [Unreleased] 4 | 5 | ## [1.8.2](https://github.com/ICTU/netbox_slm/releases/tag/1.8.2) - 2025-06-02 6 | 7 | ### Fixed 8 | 9 | * Permission check for adding software product installations (#70) 10 | 11 | ## [1.8.1](https://github.com/ICTU/netbox_slm/releases/tag/1.8.1) - 2025-04-11 12 | 13 | ### Fixed 14 | 15 | * Filtering on foreign objects (#65) 16 | 17 | ## [1.8.0](https://github.com/ICTU/netbox_slm/releases/tag/1.8.0) - 2025-02-25 18 | 19 | ### Added 20 | 21 | * Toggle to show plugin as top level menu item (#42) 22 | * Bulk import and bulk edit (#4) 23 | * Changelog with backdated changes (#51) 24 | * List filtering (#54) 25 | * Show installations on native objects (#49) 26 | * SPDX license expressions (#47) 27 | 28 | ### Changed 29 | 30 | * Support NetBox v4.2.3 (#51) 31 | * Render objects consistently (#58) 32 | 33 | ### Fixed 34 | 35 | * Model detail view listing of linked objects (#55) 36 | * Search link of installations table cells (#48) 37 | 38 | ## [1.7.0](https://github.com/ICTU/netbox_slm/releases/tag/1.7.0) - 2024-07-09 39 | 40 | ### Changed 41 | 42 | * Support NetBox v4.0.6 (#38) 43 | 44 | ### Fixed 45 | 46 | * Hidden plugin data when not logged in (#37) 47 | 48 | ### Removed 49 | 50 | * Broken bulk import and bulk edit (#4) 51 | 52 | ## [1.6.0](https://github.com/ICTU/netbox_slm/releases/tag/1.6.0) - 2024-02-13 53 | 54 | ### Added 55 | 56 | * Additional `SoftwareProductVersion` fields (#10) 57 | * Additional `SoftwareLicense` fields (#26) 58 | 59 | ### Changed 60 | 61 | * Support NetBox v3.7.2 (#35) 62 | 63 | ## [1.5](https://github.com/ICTU/netbox_slm/releases/tag/1.5) - 2023-10-04 64 | 65 | ### Added 66 | 67 | * Hyperlink option for `SoftwareLicense.stored_location` (#27) 68 | 69 | ### Changed 70 | 71 | * Update docs and imports for newer NetBox versions (#23) 72 | * Support NetBox v3.6.1 (#21) 73 | * Allow linking `SoftwareProductInstallation` to `Cluster` (#17) 74 | 75 | ### Fixed 76 | 77 | * Date picker for start and expiration dates (#22) 78 | 79 | ## [1.4](https://github.com/ICTU/netbox_slm/releases/tag/1.4) - 2023-06-30 80 | 81 | ### Added 82 | 83 | * License information for software installations (#8) 84 | 85 | ### Changed 86 | 87 | * Replace `NetBoxModelCSVForm` with `NetBoxModelImportForm` (#18) 88 | 89 | ## [1.3](https://github.com/ICTU/netbox_slm/releases/tag/1.3) - 2023-04-19 90 | 91 | ### Added 92 | 93 | * Set up continuous integration (#3) 94 | 95 | ### Changed 96 | 97 | * Support NetBox v3.4.7 (#14) 98 | 99 | ### Fixed 100 | 101 | * Unify API serialization (#12) 102 | 103 | ## [1.2](https://github.com/ICTU/netbox_slm/releases/tag/1.2) - 2022-05-19 104 | 105 | * Fixed filtering of relevant versions when adding a new installation 106 | * Manufacturer is now required for the add software product form 107 | 108 | ## [1.1](https://github.com/ICTU/netbox_slm/releases/tag/1.1) - 2022-05-03 109 | 110 | * Fixed search filter 111 | -------------------------------------------------------------------------------- /CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | # Contributor Covenant Code of Conduct 2 | 3 | ## Our Pledge 4 | 5 | We as members, contributors, and leaders pledge to make participation in our 6 | community a harassment-free experience for everyone, regardless of age, body 7 | size, visible or invisible disability, ethnicity, sex characteristics, gender 8 | identity and expression, level of experience, education, socio-economic status, 9 | nationality, personal appearance, race, caste, color, religion, or sexual 10 | identity and orientation. 11 | 12 | We pledge to act and interact in ways that contribute to an open, welcoming, 13 | diverse, inclusive, and healthy community. 14 | 15 | ## Our Standards 16 | 17 | Examples of behavior that contributes to a positive environment for our 18 | community include: 19 | 20 | * Demonstrating empathy and kindness toward other people 21 | * Being respectful of differing opinions, viewpoints, and experiences 22 | * Giving and gracefully accepting constructive feedback 23 | * Accepting responsibility and apologizing to those affected by our mistakes, 24 | and learning from the experience 25 | * Focusing on what is best not just for us as individuals, but for the overall 26 | community 27 | 28 | Examples of unacceptable behavior include: 29 | 30 | * The use of sexualized language or imagery, and sexual attention or advances of 31 | any kind 32 | * Trolling, insulting or derogatory comments, and personal or political attacks 33 | * Public or private harassment 34 | * Publishing others' private information, such as a physical or email address, 35 | without their explicit permission 36 | * Other conduct which could reasonably be considered inappropriate in a 37 | professional setting 38 | 39 | ## Enforcement Responsibilities 40 | 41 | Community leaders are responsible for clarifying and enforcing our standards of 42 | acceptable behavior and will take appropriate and fair corrective action in 43 | response to any behavior that they deem inappropriate, threatening, offensive, 44 | or harmful. 45 | 46 | Community leaders have the right and responsibility to remove, edit, or reject 47 | comments, commits, code, wiki edits, issues, and other contributions that are 48 | not aligned to this Code of Conduct, and will communicate reasons for moderation 49 | decisions when appropriate. 50 | 51 | ## Scope 52 | 53 | This Code of Conduct applies within all community spaces, and also applies when 54 | an individual is officially representing the community in public spaces. 55 | Examples of representing our community include using an official e-mail address, 56 | posting via an official social media account, or acting as an appointed 57 | representative at an online or offline event. 58 | 59 | ## Enforcement 60 | 61 | Instances of abusive, harassing, or otherwise unacceptable behavior may be 62 | reported to the community leaders responsible for enforcement by 63 | [opening a new issue in this repository's issue tracker][open issue]. 64 | All complaints will be reviewed and investigated promptly and fairly. 65 | 66 | All community leaders are obligated to respect the privacy and security of the 67 | reporter of any incident. 68 | 69 | ## Enforcement Guidelines 70 | 71 | Community leaders will follow these Community Impact Guidelines in determining 72 | the consequences for any action they deem in violation of this Code of Conduct: 73 | 74 | ### 1. Correction 75 | 76 | **Community Impact**: Use of inappropriate language or other behavior deemed 77 | unprofessional or unwelcome in the community. 78 | 79 | **Consequence**: A private, written warning from community leaders, providing 80 | clarity around the nature of the violation and an explanation of why the 81 | behavior was inappropriate. A public apology may be requested. 82 | 83 | ### 2. Warning 84 | 85 | **Community Impact**: A violation through a single incident or series of 86 | actions. 87 | 88 | **Consequence**: A warning with consequences for continued behavior. No 89 | interaction with the people involved, including unsolicited interaction with 90 | those enforcing the Code of Conduct, for a specified period of time. This 91 | includes avoiding interactions in community spaces as well as external channels 92 | like social media. Violating these terms may lead to a temporary or permanent 93 | ban. 94 | 95 | ### 3. Temporary Ban 96 | 97 | **Community Impact**: A serious violation of community standards, including 98 | sustained inappropriate behavior. 99 | 100 | **Consequence**: A temporary ban from any sort of interaction or public 101 | communication with the community for a specified period of time. No public or 102 | private interaction with the people involved, including unsolicited interaction 103 | with those enforcing the Code of Conduct, is allowed during this period. 104 | Violating these terms may lead to a permanent ban. 105 | 106 | ### 4. Permanent Ban 107 | 108 | **Community Impact**: Demonstrating a pattern of violation of community 109 | standards, including sustained inappropriate behavior, harassment of an 110 | individual, or aggression toward or disparagement of classes of individuals. 111 | 112 | **Consequence**: A permanent ban from any sort of public interaction within the 113 | community. 114 | 115 | ## Attribution 116 | 117 | This Code of Conduct is adapted from the [Contributor Covenant][homepage], 118 | version 2.1, available at 119 | [https://www.contributor-covenant.org/version/2/1/code_of_conduct.html][v2.1]. 120 | 121 | Community Impact Guidelines were inspired by 122 | [Mozilla's code of conduct enforcement ladder][Mozilla CoC]. 123 | 124 | For answers to common questions about this code of conduct, see the FAQ at 125 | [https://www.contributor-covenant.org/faq][FAQ]. Translations are available at 126 | [https://www.contributor-covenant.org/translations][translations]. 127 | 128 | [homepage]: https://www.contributor-covenant.org 129 | [v2.1]: https://www.contributor-covenant.org/version/2/1/code_of_conduct.html 130 | [Mozilla CoC]: https://github.com/mozilla/diversity 131 | [FAQ]: https://www.contributor-covenant.org/faq 132 | [translations]: https://www.contributor-covenant.org/translations 133 | [open issue]: https://github.com/ICTU/netbox_slm/issues/new 134 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # NetBox SLM contributing guidelines 2 | 3 | ## What do I need to know to help? 4 | 5 | If you are looking to help to with a code contribution our project uses, you can find inspiration in the [issue list](https://github.com/ICTU/netbox_slm/issues/). 6 | Additionally, review the [README](./README.md) document and [NetBox Plugin Development](https://docs.netbox.dev/en/stable/plugins/development/) docs. 7 | 8 | ## How do I make a contribution? 9 | 10 | Never made an open source contribution before? Wondering how contributions work in the in our project? Here's a quick rundown! 11 | 12 | 1. Find an issue that you are interested in addressing or a feature that you would like to add. 13 | 2. Fork the repository associated with the issue to your local GitHub organization. This means that you will have a copy of the repository under **your-GitHub-username/repository-name**. 14 | 3. Clone the repository to your local machine using `git clone https://github.com/github-username/repository-name.git`. 15 | 4. Create a new branch for your fix using `git checkout -b branch-name-here`. 16 | 5. Make the appropriate changes for the issue you are trying to address or the feature that you want to add. 17 | 6. Use git `add insert-paths-of-changed-files-here` to add the file contents of the changed files to the "snapshot" git uses to manage the state of the project, also known as the index. 18 | 7. Use `git commit -m "Insert a short message of the changes made here"` to store the contents of the index with a descriptive message. 19 | 8. Push the changes to the remote repository using `git push origin branch-name-here`. 20 | 9. Submit a pull request to the upstream repository. 21 | 10. Title the pull request with a short description of the changes made and the issue or bug number associated with your change. For example, you can title an issue like so "Added more log outputting to resolve #4352". 22 | 11. In the description of the pull request, explain the changes that you made, any issues you think exist with the pull request you made, and any questions you have for the maintainer. It's OK if your pull request is not perfect (no pull request is), the reviewer will be able to help you fix any problems and improve it! 23 | 12. Wait for the pull request to be reviewed by a maintainer. 24 | 13. Make changes to the pull request if the reviewing maintainer recommends them. 25 | 14. Celebrate your success after your pull request is merged! 26 | 27 | ## Where can I go for help? 28 | 29 | If you need help, you can ask questions by [opening a new issue in this repository's issue tracker](https://github.com/ICTU/netbox_slm/issues/new). 30 | 31 | ## What does the Code of Conduct mean for me? 32 | 33 | Our [Code of Conduct](./CODE_OF_CONDUCT.md) means that you are responsible for treating everyone on the project with respect and courtesy regardless of their identity. 34 | If you are the victim of any inappropriate behavior or comments as described in our Code of Conduct, we are here for you and will do the best to ensure that the abuser is reprimanded appropriately, per our code. 35 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, 10 | and distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by 13 | the copyright owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all 16 | other entities that control, are controlled by, or are under common 17 | control with that entity. For the purposes of this definition, 18 | "control" means (i) the power, direct or indirect, to cause the 19 | direction or management of such entity, whether by contract or 20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 21 | outstanding shares, or (iii) beneficial ownership of such entity. 22 | 23 | "You" (or "Your") shall mean an individual or Legal Entity 24 | exercising permissions granted by this License. 25 | 26 | "Source" form shall mean the preferred form for making modifications, 27 | including but not limited to software source code, documentation 28 | source, and configuration files. 29 | 30 | "Object" form shall mean any form resulting from mechanical 31 | transformation or translation of a Source form, including but 32 | not limited to compiled object code, generated documentation, 33 | and conversions to other media types. 34 | 35 | "Work" shall mean the work of authorship, whether in Source or 36 | Object form, made available under the License, as indicated by a 37 | copyright notice that is included in or attached to the work 38 | (an example is provided in the Appendix below). 39 | 40 | "Derivative Works" shall mean any work, whether in Source or Object 41 | form, that is based on (or derived from) the Work and for which the 42 | editorial revisions, annotations, elaborations, or other modifications 43 | represent, as a whole, an original work of authorship. For the purposes 44 | of this License, Derivative Works shall not include works that remain 45 | separable from, or merely link (or bind by name) to the interfaces of, 46 | the Work and Derivative Works thereof. 47 | 48 | "Contribution" shall mean any work of authorship, including 49 | the original version of the Work and any modifications or additions 50 | to that Work or Derivative Works thereof, that is intentionally 51 | submitted to Licensor for inclusion in the Work by the copyright owner 52 | or by an individual or Legal Entity authorized to submit on behalf of 53 | the copyright owner. For the purposes of this definition, "submitted" 54 | means any form of electronic, verbal, or written communication sent 55 | to the Licensor or its representatives, including but not limited to 56 | communication on electronic mailing lists, source code control systems, 57 | and issue tracking systems that are managed by, or on behalf of, the 58 | Licensor for the purpose of discussing and improving the Work, but 59 | excluding communication that is conspicuously marked or otherwise 60 | designated in writing by the copyright owner as "Not a Contribution." 61 | 62 | "Contributor" shall mean Licensor and any individual or Legal Entity 63 | on behalf of whom a Contribution has been received by Licensor and 64 | subsequently incorporated within the Work. 65 | 66 | 2. Grant of Copyright License. Subject to the terms and conditions of 67 | this License, each Contributor hereby grants to You a perpetual, 68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 69 | copyright license to reproduce, prepare Derivative Works of, 70 | publicly display, publicly perform, sublicense, and distribute the 71 | Work and such Derivative Works in Source or Object form. 72 | 73 | 3. Grant of Patent License. Subject to the terms and conditions of 74 | this License, each Contributor hereby grants to You a perpetual, 75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 76 | (except as stated in this section) patent license to make, have made, 77 | use, offer to sell, sell, import, and otherwise transfer the Work, 78 | where such license applies only to those patent claims licensable 79 | by such Contributor that are necessarily infringed by their 80 | Contribution(s) alone or by combination of their Contribution(s) 81 | with the Work to which such Contribution(s) was submitted. If You 82 | institute patent litigation against any entity (including a 83 | cross-claim or counterclaim in a lawsuit) alleging that the Work 84 | or a Contribution incorporated within the Work constitutes direct 85 | or contributory patent infringement, then any patent licenses 86 | granted to You under this License for that Work shall terminate 87 | as of the date such litigation is filed. 88 | 89 | 4. Redistribution. You may reproduce and distribute copies of the 90 | Work or Derivative Works thereof in any medium, with or without 91 | modifications, and in Source or Object form, provided that You 92 | meet the following conditions: 93 | 94 | (a) You must give any other recipients of the Work or 95 | Derivative Works a copy of this License; and 96 | 97 | (b) You must cause any modified files to carry prominent notices 98 | stating that You changed the files; and 99 | 100 | (c) You must retain, in the Source form of any Derivative Works 101 | that You distribute, all copyright, patent, trademark, and 102 | attribution notices from the Source form of the Work, 103 | excluding those notices that do not pertain to any part of 104 | the Derivative Works; and 105 | 106 | (d) If the Work includes a "NOTICE" text file as part of its 107 | distribution, then any Derivative Works that You distribute must 108 | include a readable copy of the attribution notices contained 109 | within such NOTICE file, excluding those notices that do not 110 | pertain to any part of the Derivative Works, in at least one 111 | of the following places: within a NOTICE text file distributed 112 | as part of the Derivative Works; within the Source form or 113 | documentation, if provided along with the Derivative Works; or, 114 | within a display generated by the Derivative Works, if and 115 | wherever such third-party notices normally appear. The contents 116 | of the NOTICE file are for informational purposes only and 117 | do not modify the License. You may add Your own attribution 118 | notices within Derivative Works that You distribute, alongside 119 | or as an addendum to the NOTICE text from the Work, provided 120 | that such additional attribution notices cannot be construed 121 | as modifying the License. 122 | 123 | You may add Your own copyright statement to Your modifications and 124 | may provide additional or different license terms and conditions 125 | for use, reproduction, or distribution of Your modifications, or 126 | for any such Derivative Works as a whole, provided Your use, 127 | reproduction, and distribution of the Work otherwise complies with 128 | the conditions stated in this License. 129 | 130 | 5. Submission of Contributions. Unless You explicitly state otherwise, 131 | any Contribution intentionally submitted for inclusion in the Work 132 | by You to the Licensor shall be under the terms and conditions of 133 | this License, without any additional terms or conditions. 134 | Notwithstanding the above, nothing herein shall supersede or modify 135 | the terms of any separate license agreement you may have executed 136 | with Licensor regarding such Contributions. 137 | 138 | 6. Trademarks. This License does not grant permission to use the trade 139 | names, trademarks, service marks, or product names of the Licensor, 140 | except as required for reasonable and customary use in describing the 141 | origin of the Work and reproducing the content of the NOTICE file. 142 | 143 | 7. Disclaimer of Warranty. Unless required by applicable law or 144 | agreed to in writing, Licensor provides the Work (and each 145 | Contributor provides its Contributions) on an "AS IS" BASIS, 146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 147 | implied, including, without limitation, any warranties or conditions 148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 149 | PARTICULAR PURPOSE. You are solely responsible for determining the 150 | appropriateness of using or redistributing the Work and assume any 151 | risks associated with Your exercise of permissions under this License. 152 | 153 | 8. Limitation of Liability. In no event and under no legal theory, 154 | whether in tort (including negligence), contract, or otherwise, 155 | unless required by applicable law (such as deliberate and grossly 156 | negligent acts) or agreed to in writing, shall any Contributor be 157 | liable to You for damages, including any direct, indirect, special, 158 | incidental, or consequential damages of any character arising as a 159 | result of this License or out of the use or inability to use the 160 | Work (including but not limited to damages for loss of goodwill, 161 | work stoppage, computer failure or malfunction, or any and all 162 | other commercial damages or losses), even if such Contributor 163 | has been advised of the possibility of such damages. 164 | 165 | 9. Accepting Warranty or Additional Liability. While redistributing 166 | the Work or Derivative Works thereof, You may choose to offer, 167 | and charge a fee for, acceptance of support, warranty, indemnity, 168 | or other liability obligations and/or rights consistent with this 169 | License. However, in accepting such obligations, You may act only 170 | on Your own behalf and on Your sole responsibility, not on behalf 171 | of any other Contributor, and only if You agree to indemnify, 172 | defend, and hold each Contributor harmless for any liability 173 | incurred by, or claims asserted against, such Contributor by reason 174 | of your accepting any such warranty or additional liability. 175 | 176 | END OF TERMS AND CONDITIONS 177 | 178 | APPENDIX: How to apply the Apache License to your work. 179 | 180 | To apply the Apache License to your work, attach the following 181 | boilerplate notice, with the fields enclosed by brackets "[]" 182 | replaced with your own identifying information. (Don't include 183 | the brackets!) The text should be enclosed in the appropriate 184 | comment syntax for the file format. We also recommend that a 185 | file or class name and description of purpose be included on the 186 | same "printed page" as the copyright notice for easier 187 | identification within third-party archives. 188 | 189 | Copyright [yyyy] [name of copyright owner] 190 | 191 | Licensed under the Apache License, Version 2.0 (the "License"); 192 | you may not use this file except in compliance with the License. 193 | You may obtain a copy of the License at 194 | 195 | http://www.apache.org/licenses/LICENSE-2.0 196 | 197 | Unless required by applicable law or agreed to in writing, software 198 | distributed under the License is distributed on an "AS IS" BASIS, 199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 200 | See the License for the specific language governing permissions and 201 | limitations under the License. 202 | -------------------------------------------------------------------------------- /MAINTENANCE.md: -------------------------------------------------------------------------------- 1 | # Maintenance related tasks 2 | 3 | 4 | ## Version upgrade workflow 5 | 6 | 1. Update version spec in `netbox_slm/__init__.py` and `sonar-project.properties` 7 | 1. Check for any runtime errors and warnings in the `netbox-*` container logs 8 | 1. Create new version tag on GitHub, following semantic versioning as: `MAJOR.MINOR.PATCH` 9 | 1. Update the `CHANGELOG.md` with new version information and move `[Unreleased]` items to new version section 10 | 1. Build the package: `python -m build` 11 | 1. Upload the distributions to PyPI: `twine upload --skip-existing dist/*` 12 | 13 | 14 | ## Developer Guide (local installation) 15 | 16 | *Follow the steps below on your local system to run NetBox and the 17 | `netbox_slm` plugin in developer mode* 18 | 19 | ### Setup 20 | 21 | The goal below is to run all NetBox components in Docker and run a local 22 | NetBox Django copy with auto-reload to develop the plugin pointing to 23 | the PostgreSQL and Redis container instances, basically ignoring the 24 | NetBox docker runtime server. 25 | 26 | ### Steps 27 | 28 | from your projects directory clone the NetBox repository 29 | 30 | ```shell 31 | $ git clone https://github.com/netbox-community/netbox 32 | $ cd netbox 33 | ``` 34 | 35 | install the virtual environment 36 | 37 | ```shell 38 | $ pipenv shell 39 | $ pipenv install 40 | ``` 41 | 42 | create and edit `netbox/configuration.py` (based on the template file) add these lines at the end of the file; 43 | 44 | ```python 45 | DEBUG = True 46 | SECRET_KEY = 'dummy' 47 | PLUGINS = [ 48 | 'netbox_slm', 49 | ] 50 | ``` 51 | 52 | The NetBox installation above will be used to run Django management 53 | commands like `runserver`, `makemigrations` and `migrate`, which will be 54 | explained in the next steps below; 55 | 56 | from your projects directory clone the `netbox_slm` repository 57 | 58 | ```shell 59 | $ git clone https://github.com/ICTU/netbox_slm 60 | $ cd netbox_slm 61 | $ ./start-netbox.sh 62 | ``` 63 | 64 | This will start NetBox locally (requires Docker) and forward the Redis 65 | and PostgreSQL ports to the localhost (make sure there’s no processes 66 | using these ports or change the Dockerfile(s) accordingly) 67 | 68 | Note, you can also start and stop NetBox by hand: 69 | 70 | ```shell 71 | $ cd netbox-docker 72 | $ docker compose up -d 73 | ``` 74 | 75 | or stop the stack with 76 | 77 | ```shell 78 | $ docker compose down 79 | ``` 80 | 81 | #### to start fresh: 82 | 83 | ```shell 84 | $ docker compose down 85 | $ docker volume rm netbox-docker_netbox-postgres-data # et cetera 86 | $ docker compose up -d --force-recreate 87 | ``` 88 | 89 | this will require you to re-run the migrate commando's for `netbox_slm`, see further down below 90 | 91 | Go back to the NetBox `configuration.py` file and update the PostgreSQL and 92 | Redis connection strings (username, password) to the ones the NetBox 93 | Docker backend is using, for example (using default user and passwords 94 | from the NetBox Docker example): 95 | 96 | ```python 97 | # PostgreSQL database configuration. See the Django documentation for a complete list of available parameters: 98 | # https://docs.djangoproject.com/en/stable/ref/settings/#databases 99 | DATABASE = { 100 | 'NAME': 'netbox', # Database name 101 | 'USER': 'netbox', # PostgreSQL username 102 | 'PASSWORD': 'J5brHrAXFLQSif0K', # PostgreSQL password 103 | 'HOST': 'localhost', # Database server 104 | 'PORT': '', # Database port (leave blank for default) 105 | 'CONN_MAX_AGE': 300, # Max database connection age 106 | } 107 | 108 | # Redis database settings. Redis is used for caching and for queuing background tasks such as webhook events. A separate 109 | # configuration exists for each. Full connection details are required in both sections, and it is strongly recommended 110 | # to use two separate database IDs. 111 | REDIS = { 112 | 'tasks': { 113 | 'HOST': 'localhost', 114 | 'PORT': 6379, 115 | # Comment out `HOST` and `PORT` lines and uncomment the following if using Redis Sentinel 116 | # 'SENTINELS': [('mysentinel.redis.example.com', 6379)], 117 | # 'SENTINEL_SERVICE': 'netbox', 118 | 'PASSWORD': 'H733Kdjndks81', 119 | 'DATABASE': 0, 120 | 'SSL': False, 121 | # Set this to True to skip TLS certificate verification 122 | # This can expose the connection to attacks, be careful 123 | # 'INSECURE_SKIP_TLS_VERIFY': False, 124 | }, 125 | 'caching': { 126 | 'HOST': 'localhost', 127 | 'PORT': 6379, 128 | # Comment out `HOST` and `PORT` lines and uncomment the following if using Redis Sentinel 129 | # 'SENTINELS': [('mysentinel.redis.example.com', 6379)], 130 | # 'SENTINEL_SERVICE': 'netbox', 131 | 'PASSWORD': 'H733Kdjndks81', 132 | 'DATABASE': 1, 133 | 'SSL': False, 134 | # Set this to True to skip TLS certificate verification 135 | # This can expose the connection to attacks, be careful 136 | # 'INSECURE_SKIP_TLS_VERIFY': False, 137 | } 138 | } 139 | ``` 140 | 141 | Now you can run commands from the NetBox repository like this; 142 | 143 | ```shell 144 | $ cd netbox/netbox 145 | $ export PYTHONPATH=../../netbox_slm/netbox_slm/ # or with the pipenv activated run `python3 setup.py develop` from the netbox_slm directory 146 | $ python3 manage.py migrate netbox_slm 147 | $ python3 manage.py runserver 8001 148 | ``` 149 | 150 | Visit http://127.0.0.1:8001 in the browser to see the auto reloading version of the NetBox UI. 151 | Port 8000 is taken by the Docker ran variant. 152 | -------------------------------------------------------------------------------- /MANIFEST.in: -------------------------------------------------------------------------------- 1 | include LICENSE 2 | include README.md 3 | include SECURITY.md 4 | graft netbox_slm 5 | prune netbox_slm/tests 6 | global-exclude *.py[cod] 7 | global-exclude __pycache__ 8 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # NetBox SLM 2 | 3 |

NetBox SLM is a plugin for lifecycle management of software components, including versions and installations.

4 | 5 |
6 | PyPi 7 | Stars Badge 8 | Forks Badge 9 | Pull Requests Badge 10 | Issues Badge 11 | GitHub contributors 12 | License Badge 13 |
14 | 15 | 16 | ## Installation Guide 17 | 18 | When using the Docker version of NetBox, first follow the `netbox-docker` [quickstart](https://github.com/netbox-community/netbox-docker#quickstart) instructions to clone the `netbox-docker` repository and set up the ``docker-compose.override.yml``. 19 | 20 | Note that this plugin is only tested against a single NetBox version at this time, see [Dockerfile-CI](https://github.com/ICTU/netbox_slm/blob/master/ci/Dockerfile-CI). 21 | 22 | Next, follow these instructions (based on the NetBox docker variant [instructions](https://github.com/netbox-community/netbox-docker/wiki/Configuration#custom-configuration-files)) to install the NetBox SLM plugin: 23 | 24 | 1. Add ``netbox_slm`` to the ``PLUGINS`` list in 25 | ``configuration/plugins.py``. 26 | 2. Create a ``plugin_requirements.txt`` with ``netbox-slm`` as 27 | contents. 28 | 3. Create a ``Dockerfile-SLM`` with contents: 29 | 30 | ```dockerfile 31 | FROM netboxcommunity/netbox:vX.Y.Z 32 | 33 | COPY ../pyproject.toml /tmp/ 34 | RUN uv pip install -r /tmp/pyproject.toml 35 | ``` 36 | 37 | 4. Create a ``docker-compose.override.yml`` with contents: 38 | 39 | ```yaml 40 | version: "3.7" 41 | services: 42 | netbox: 43 | ports: 44 | - "8000:8080" 45 | build: 46 | context: . 47 | dockerfile: Dockerfile-SLM 48 | image: netbox:slm 49 | netbox-worker: 50 | image: netbox:slm 51 | netbox-housekeeping: 52 | image: netbox:slm 53 | ``` 54 | 55 | Now, build the image: ``docker compose build --no-cache`` 56 | 57 | And finally, run NetBox with the SLM plugin: ``docker compose up`` 58 | 59 | 60 | ## Get in touch 61 | 62 | Point of contact for this repository is [Mart Visser](https://github.com/MartVisser), who can be reached by [opening a new issue in this repository's issue tracker](https://github.com/ICTU/netbox_slm/issues/new). 63 | -------------------------------------------------------------------------------- /SECURITY.md: -------------------------------------------------------------------------------- 1 | # NetBox SLM Security Policy 2 | 3 | ## Current status 4 | 5 | NetBox SLM is a plugin for [NetBox](https://github.com/netbox-community/netbox/) with mostly default configuration. 6 | In order to stay up to date, monitor NetBox security findings and update accordingly. 7 | 8 | ## Supported Versions 9 | 10 | Only the latest version of NetBox SLM is currently being supported with security updates. 11 | The intention is to keep the plugin compatible with the most recent NetBox version(s), there is no incentive to patch older tags. 12 | 13 | ## Reporting a Vulnerability 14 | 15 | You can privately [report a vulnerability issue in this repository's issue tracker](https://github.com/ICTU/netbox_slm/security/advisories/new). 16 | The aim is to get back to you within 24 hours, with a confirmation of the issue and a brief action plan or a request for more information. 17 | -------------------------------------------------------------------------------- /ci/.vale.ini: -------------------------------------------------------------------------------- 1 | StylesPath = styles 2 | 3 | MinAlertLevel = suggestion 4 | Vocab = Base 5 | 6 | Packages = proselint 7 | 8 | [*.md] 9 | BasedOnStyles = Vale, proselint 10 | 11 | [ci/styles/**] 12 | BasedOnStyles = 13 | -------------------------------------------------------------------------------- /ci/Dockerfile: -------------------------------------------------------------------------------- 1 | ARG PYTHON_VERSION=3.12 2 | FROM python:${PYTHON_VERSION}-alpine AS compile-image 3 | # Python version build arg is not actually supported by NetBox container image, only used for maintenance convenience 4 | 5 | WORKDIR /build 6 | RUN python3 -m venv /opt/netbox/venv &&\ 7 | source /opt/netbox/venv/bin/activate 8 | 9 | COPY ../ /build 10 | 11 | RUN pip install -U .[build] &&\ 12 | python -m build &&\ 13 | pip install --no-index /build 14 | 15 | FROM netboxcommunity/netbox:v4.2.3 16 | ARG PYTHON_VERSION=3.12 17 | 18 | COPY --from=compile-image /opt/netbox/venv/lib/python${PYTHON_VERSION}/site-packages/netbox_slm /opt/netbox/venv/lib/python${PYTHON_VERSION}/site-packages/netbox_slm 19 | -------------------------------------------------------------------------------- /ci/Dockerfile-CI: -------------------------------------------------------------------------------- 1 | FROM netboxcommunity/netbox:v4.2.3 2 | ARG PYTHON_VERSION=3.12 3 | 4 | COPY ../pyproject.toml /tmp/ 5 | RUN uv pip install -r /tmp/pyproject.toml --extra ci 6 | -------------------------------------------------------------------------------- /ci/config/configuration.py: -------------------------------------------------------------------------------- 1 | """ 2 | Minimal NetBox config for NetBox SLM plugin testing, docs: https://netbox.readthedocs.io/en/stable/configuration/ 3 | Based on https://github.com/netbox-community/netbox/blob/v4.2.3/netbox/netbox/configuration_testing.py 4 | """ 5 | from os import environ 6 | 7 | ALLOWED_HOSTS = ['*'] 8 | 9 | DATABASE = { 10 | 'NAME': environ.get('DB_NAME', 'netbox'), 11 | 'USER': environ.get('DB_USER', ''), 12 | 'PASSWORD': environ.get('DB_PASSWORD', ''), 13 | 'HOST': environ.get('DB_HOST', 'localhost'), 14 | 'PORT': environ.get('DB_PORT', ''), 15 | 'CONN_MAX_AGE': int(environ.get('DB_CONN_MAX_AGE', '300')), 16 | } 17 | 18 | DEBUG = environ.get('DEBUG', 'False').lower() == 'true' 19 | DEVELOPER = environ.get('DEVELOPER', 'False').lower() == 'true' 20 | 21 | PLUGINS = ["netbox_slm"] 22 | 23 | REDIS = { 24 | 'tasks': { 25 | 'HOST': environ.get('REDIS_HOST', 'localhost'), 26 | 'PORT': int(environ.get('REDIS_PORT', 6379)), 27 | 'PASSWORD': environ.get('REDIS_PASSWORD', ''), 28 | 'DATABASE': int(environ.get('REDIS_DATABASE', 0)), 29 | 'SSL': environ.get('REDIS_SSL', 'False').lower() == 'true', 30 | }, 31 | 'caching': { 32 | 'HOST': environ.get('REDIS_CACHE_HOST', environ.get('REDIS_HOST', 'localhost')), 33 | 'PORT': int(environ.get('REDIS_CACHE_PORT', environ.get('REDIS_PORT', 6379))), 34 | 'PASSWORD': environ.get('REDIS_CACHE_PASSWORD', environ.get('REDIS_PASSWORD', '')), 35 | 'DATABASE': int(environ.get('REDIS_CACHE_DATABASE', 1)), 36 | 'SSL': environ.get('REDIS_CACHE_SSL', environ.get('REDIS_SSL', 'False')).lower() == 'true', 37 | }, 38 | } 39 | 40 | SECRET_KEY = 'dummydummydummydummydummydummydummydummydummydummy' 41 | -------------------------------------------------------------------------------- /ci/docker-compose.ci.yml: -------------------------------------------------------------------------------- 1 | --- 2 | services: 3 | netbox: 4 | ports: 5 | - "8001:8080" 6 | build: 7 | context: .. 8 | dockerfile: ci/Dockerfile-CI 9 | image: netbox:slm 10 | environment: 11 | - COVERAGE_FILE=/tmp/.coverage 12 | healthcheck: 13 | disable: true 14 | volumes: 15 | - ./config:/etc/netbox/config:ro 16 | - ./reports:/ci/reports:rw 17 | - ../netbox_slm:/opt/netbox/netbox/netbox_slm:ro 18 | netbox-worker: 19 | image: netbox:slm 20 | depends_on: 21 | - netbox 22 | healthcheck: 23 | disable: true 24 | volumes: 25 | - ./config:/etc/netbox/config:ro 26 | - ../netbox_slm:/opt/netbox/netbox/netbox_slm:ro 27 | netbox-housekeeping: 28 | image: netbox:slm 29 | depends_on: 30 | - netbox 31 | healthcheck: 32 | disable: true 33 | postgres: 34 | ports: 35 | - "5432:5432" 36 | redis: 37 | ports: 38 | - "6379:6379" 39 | -------------------------------------------------------------------------------- /ci/docker-compose.yml: -------------------------------------------------------------------------------- 1 | --- 2 | # based on https://github.com/netbox-community/netbox-docker/blob/3.2.0/docker-compose.yml 3 | services: 4 | netbox: &netbox 5 | image: netboxcommunity/netbox:${VERSION-v4.2.3} 6 | depends_on: 7 | - postgres 8 | - redis 9 | - redis-cache 10 | env_file: env/netbox.env 11 | user: "unit:root" 12 | healthcheck: 13 | test: curl -f http://localhost:8080/login/ || exit 1 14 | start_period: 90s 15 | timeout: 3s 16 | interval: 15s 17 | volumes: 18 | - ./config:/etc/netbox/config:ro 19 | - netbox-media-files:/opt/netbox/netbox/media:rw 20 | - netbox-reports-files:/opt/netbox/netbox/reports:rw 21 | - netbox-scripts-files:/opt/netbox/netbox/scripts:rw 22 | netbox-worker: 23 | <<: *netbox 24 | depends_on: 25 | netbox: 26 | condition: service_healthy 27 | command: 28 | - /opt/netbox/venv/bin/python 29 | - /opt/netbox/netbox/manage.py 30 | - rqworker 31 | healthcheck: 32 | test: ps -aux | grep -v grep | grep -q rqworker || exit 1 33 | start_period: 20s 34 | timeout: 3s 35 | interval: 15s 36 | netbox-housekeeping: 37 | <<: *netbox 38 | depends_on: 39 | netbox: 40 | condition: service_healthy 41 | command: 42 | - /opt/netbox/housekeeping.sh 43 | healthcheck: 44 | test: ps -aux | grep -v grep | grep -q housekeeping || exit 1 45 | start_period: 20s 46 | timeout: 3s 47 | interval: 15s 48 | 49 | # postgres 50 | postgres: 51 | image: postgres:17-alpine 52 | healthcheck: 53 | test: pg_isready -q -t 2 -d $$POSTGRES_DB -U $$POSTGRES_USER 54 | start_period: 20s 55 | timeout: 30s 56 | interval: 10s 57 | retries: 5 58 | env_file: env/postgres.env 59 | volumes: 60 | - netbox-postgres-data:/var/lib/postgresql/data 61 | 62 | # redis 63 | redis: 64 | image: valkey/valkey:8.0-alpine 65 | command: 66 | - sh 67 | - -c 68 | - valkey-server --appendonly yes --requirepass $$REDIS_PASSWORD 69 | healthcheck: &redis-healthcheck 70 | test: '[ $$(valkey-cli --pass "$${REDIS_PASSWORD}" ping) = ''PONG'' ]' 71 | start_period: 5s 72 | timeout: 3s 73 | interval: 1s 74 | retries: 5 75 | env_file: env/redis.env 76 | volumes: 77 | - netbox-redis-data:/data 78 | redis-cache: 79 | image: valkey/valkey:8.0-alpine 80 | command: 81 | - sh 82 | - -c 83 | - valkey-server --requirepass $$REDIS_PASSWORD 84 | healthcheck: *redis-healthcheck 85 | env_file: env/redis-cache.env 86 | volumes: 87 | - netbox-redis-cache-data:/data 88 | 89 | volumes: 90 | netbox-media-files: 91 | driver: local 92 | netbox-postgres-data: 93 | driver: local 94 | netbox-redis-cache-data: 95 | driver: local 96 | netbox-redis-data: 97 | driver: local 98 | netbox-reports-files: 99 | driver: local 100 | netbox-scripts-files: 101 | driver: local 102 | -------------------------------------------------------------------------------- /ci/env/netbox.env: -------------------------------------------------------------------------------- 1 | CORS_ORIGIN_ALLOW_ALL=True 2 | DB_HOST=postgres 3 | DB_NAME=netbox 4 | DB_PASSWORD=J5brHrAXFLQSif0K 5 | DB_USER=netbox 6 | EMAIL_FROM=netbox@bar.com 7 | EMAIL_PASSWORD= 8 | EMAIL_PORT=25 9 | EMAIL_SERVER=localhost 10 | EMAIL_SSL_CERTFILE= 11 | EMAIL_SSL_KEYFILE= 12 | EMAIL_TIMEOUT=5 13 | EMAIL_USERNAME=netbox 14 | # EMAIL_USE_SSL and EMAIL_USE_TLS are mutually exclusive, i.e. they can't both be `true`! 15 | EMAIL_USE_SSL=false 16 | EMAIL_USE_TLS=false 17 | GRAPHQL_ENABLED=true 18 | HOUSEKEEPING_INTERVAL=86400 19 | MAX_PAGE_SIZE=1000 20 | MEDIA_ROOT=/opt/netbox/netbox/media 21 | METRICS_ENABLED=false 22 | NAPALM_PASSWORD= 23 | NAPALM_TIMEOUT=10 24 | NAPALM_USERNAME= 25 | REDIS_CACHE_DATABASE=1 26 | REDIS_CACHE_HOST=redis-cache 27 | REDIS_CACHE_INSECURE_SKIP_TLS_VERIFY=false 28 | REDIS_CACHE_PASSWORD=t4Ph722qJ5QHeQ1qfu36 29 | REDIS_CACHE_SSL=false 30 | REDIS_DATABASE=0 31 | REDIS_HOST=redis 32 | REDIS_INSECURE_SKIP_TLS_VERIFY=false 33 | REDIS_PASSWORD=H733Kdjndks81 34 | REDIS_SSL=false 35 | RELEASE_CHECK_URL=https://api.github.com/repos/netbox-community/netbox/releases 36 | SECRET_KEY=r8OwDznj!!dci#P9ghmRfdu1Ysxm0AiPeDCQhKE+N_rClfWNj 37 | SKIP_SUPERUSER=false 38 | SUPERUSER_API_TOKEN=0123456789abcdef0123456789abcdef01234567 39 | SUPERUSER_EMAIL=admin@example.com 40 | SUPERUSER_NAME=admin 41 | SUPERUSER_PASSWORD=admin 42 | WEBHOOKS_ENABLED=true 43 | -------------------------------------------------------------------------------- /ci/env/postgres.env: -------------------------------------------------------------------------------- 1 | POSTGRES_DB=netbox 2 | POSTGRES_PASSWORD=J5brHrAXFLQSif0K 3 | POSTGRES_USER=netbox 4 | -------------------------------------------------------------------------------- /ci/env/redis-cache.env: -------------------------------------------------------------------------------- 1 | REDIS_PASSWORD=t4Ph722qJ5QHeQ1qfu36 2 | -------------------------------------------------------------------------------- /ci/env/redis.env: -------------------------------------------------------------------------------- 1 | REDIS_PASSWORD=H733Kdjndks81 2 | -------------------------------------------------------------------------------- /ci/reports/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ICTU/netbox_slm/9e87afcde7780b4b92af9cb7e6e5ff3f7cbbf13f/ci/reports/.gitkeep -------------------------------------------------------------------------------- /ci/styles/config/vocabularies/Base/accept.txt: -------------------------------------------------------------------------------- 1 | Anchore 2 | APIs 3 | Caddy 4 | Checkmarx 5 | Cobertura 6 | Dependabot 7 | Docker-composition 8 | Dockerfile 9 | DTDs 10 | ESLint 11 | Gravatar 12 | Jira 13 | JMeter 14 | JUnit 15 | NCover 16 | Nginx 17 | OJAudit 18 | OpenShift 19 | PDFs 20 | Pydantic 21 | Robocop 22 | Snyk 23 | Trello 24 | Trivy 25 | UUIDs 26 | Wekan 27 | [Hh]ostname 28 | [Uu]nmerged 29 | asyncio 30 | autoformatting 31 | breakpoint 32 | clearable 33 | cloc 34 | discoverability 35 | donut 36 | errored 37 | favicon 38 | fixme 39 | hostnames? 40 | hotspots? 41 | lookback 42 | misconfigured 43 | mypy 44 | namespace 45 | npm 46 | parameterizable 47 | phpldapadmin 48 | [Pp]erformancetest 49 | severities 50 | sparkline 51 | subfolders 52 | submenus 53 | suppressions 54 | todo 55 | tooltips? 56 | tracebacks? 57 | unencrypted 58 | unicode 59 | unmerged 60 | upvotes 61 | url 62 | xml 63 | NetBox 64 | NetBox SLM 65 | Mart 66 | Visser 67 | -------------------------------------------------------------------------------- /ci/styles/config/vocabularies/Base/reject.txt: -------------------------------------------------------------------------------- 1 | docker composition 2 | Docker composition 3 | docker-composition 4 | -------------------------------------------------------------------------------- /netbox_slm/__init__.py: -------------------------------------------------------------------------------- 1 | """ 2 | Copyright 2022-2025 ICTU 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | """ 16 | 17 | from netbox.plugins import PluginConfig 18 | 19 | __version__ = "1.8.2" 20 | 21 | 22 | class SLMConfig(PluginConfig): 23 | name = "netbox_slm" 24 | verbose_name = "Software Lifecycle Management" 25 | version = __version__ 26 | description = "Software Lifecycle Management Netbox Plugin." 27 | author = "ICTU" 28 | author_email = "open-source-projects@ictu.nl" 29 | base_url = "slm" 30 | required_settings = [] 31 | default_settings = { 32 | "top_level_menu": True, 33 | "link_cluster_installations": "right", 34 | "link_device_installations": "right", 35 | "link_virtualmachine_installations": "right", 36 | } 37 | 38 | 39 | config = SLMConfig 40 | -------------------------------------------------------------------------------- /netbox_slm/admin.py: -------------------------------------------------------------------------------- 1 | from django.apps import apps 2 | from django.conf import settings 3 | from django.contrib import admin 4 | 5 | if settings.DEBUG: 6 | for model in apps.get_app_config("netbox_slm").get_models(): 7 | admin.site.register(model) 8 | -------------------------------------------------------------------------------- /netbox_slm/api/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ICTU/netbox_slm/9e87afcde7780b4b92af9cb7e6e5ff3f7cbbf13f/netbox_slm/api/__init__.py -------------------------------------------------------------------------------- /netbox_slm/api/serializers.py: -------------------------------------------------------------------------------- 1 | from rest_framework.serializers import SerializerMethodField, HyperlinkedIdentityField 2 | 3 | from netbox.api.serializers import NetBoxModelSerializer 4 | from netbox_slm.models import SoftwareProduct, SoftwareProductVersion, SoftwareProductInstallation, SoftwareLicense 5 | 6 | 7 | class SoftwareLicenseSerializer(NetBoxModelSerializer): 8 | display = SerializerMethodField() 9 | url = HyperlinkedIdentityField(view_name="plugins-api:netbox_slm-api:softwarelicense-detail") 10 | 11 | class Meta: 12 | model = SoftwareLicense 13 | fields = ( 14 | "id", 15 | "display", 16 | "url", 17 | "name", 18 | "description", 19 | "type", 20 | "spdx_expression", 21 | "stored_location", 22 | "stored_location_url", 23 | "start_date", 24 | "expiration_date", 25 | "support", 26 | "license_amount", 27 | "software_product", 28 | "version", 29 | "installation", 30 | "tags", 31 | "comments", 32 | "custom_field_data", 33 | "created", 34 | "last_updated", 35 | ) 36 | brief_fields = ("id", "display", "url", "name", "description") 37 | 38 | def get_display(self, obj): 39 | return f"{obj}" 40 | 41 | 42 | class SoftwareProductSerializer(NetBoxModelSerializer): 43 | display = SerializerMethodField() 44 | url = HyperlinkedIdentityField(view_name="plugins-api:netbox_slm-api:softwareproduct-detail") 45 | 46 | class Meta: 47 | model = SoftwareProduct 48 | fields = ( 49 | "id", 50 | "display", 51 | "url", 52 | "name", 53 | "description", 54 | "manufacturer", 55 | "description", 56 | "tags", 57 | "comments", 58 | "custom_field_data", 59 | "created", 60 | "last_updated", 61 | ) 62 | brief_fields = ("id", "display", "url", "name", "description") 63 | 64 | def get_display(self, obj): 65 | return f"{obj}" 66 | 67 | 68 | class SoftwareProductInstallationSerializer(NetBoxModelSerializer): 69 | display = SerializerMethodField() 70 | url = HyperlinkedIdentityField(view_name="plugins-api:netbox_slm-api:softwareproductinstallation-detail") 71 | 72 | class Meta: 73 | model = SoftwareProductInstallation 74 | fields = ( 75 | "id", 76 | "display", 77 | "url", 78 | "device", 79 | "virtualmachine", 80 | "cluster", 81 | "software_product", 82 | "version", 83 | "tags", 84 | "comments", 85 | "custom_field_data", 86 | "created", 87 | "last_updated", 88 | ) 89 | brief_fields = ("id", "display", "url") 90 | 91 | def get_display(self, obj): 92 | return f"{obj}" 93 | 94 | 95 | class SoftwareProductVersionSerializer(NetBoxModelSerializer): 96 | display = SerializerMethodField() 97 | url = HyperlinkedIdentityField(view_name="plugins-api:netbox_slm-api:softwareproductversion-detail") 98 | 99 | class Meta: 100 | model = SoftwareProductVersion 101 | fields = ( 102 | "id", 103 | "display", 104 | "url", 105 | "name", 106 | "description", 107 | "release_date", 108 | "documentation_url", 109 | "end_of_support", 110 | "filename", 111 | "file_checksum", 112 | "file_link", 113 | "release_type", 114 | "software_product", 115 | "tags", 116 | "comments", 117 | "custom_field_data", 118 | "created", 119 | "last_updated", 120 | ) 121 | brief_fields = ("id", "display", "url", "name", "description") 122 | 123 | def get_display(self, obj): 124 | return f"{obj}" 125 | -------------------------------------------------------------------------------- /netbox_slm/api/urls.py: -------------------------------------------------------------------------------- 1 | from netbox.api.routers import NetBoxRouter 2 | from netbox_slm.api.views import ( 3 | NetboxSLMRootView, 4 | SoftwareProductViewSet, 5 | SoftwareProductVersionViewSet, 6 | SoftwareProductInstallationViewSet, 7 | SoftwareLicenseViewSet, 8 | ) 9 | 10 | router = NetBoxRouter() 11 | router.APIRootView = NetboxSLMRootView 12 | 13 | router.register("softwareproducts", SoftwareProductViewSet) 14 | router.register("softwareproductversions", SoftwareProductVersionViewSet) 15 | router.register("softwareproductinstallations", SoftwareProductInstallationViewSet) 16 | router.register("softwarelicenses", SoftwareLicenseViewSet) 17 | 18 | urlpatterns = router.urls 19 | -------------------------------------------------------------------------------- /netbox_slm/api/views.py: -------------------------------------------------------------------------------- 1 | from rest_framework.routers import APIRootView 2 | 3 | from netbox.api.viewsets import NetBoxModelViewSet 4 | from netbox_slm.api.serializers import ( 5 | SoftwareProductSerializer, 6 | SoftwareProductVersionSerializer, 7 | SoftwareProductInstallationSerializer, 8 | SoftwareLicenseSerializer, 9 | ) 10 | from netbox_slm.filtersets import ( 11 | SoftwareProductFilterSet, 12 | SoftwareProductVersionFilterSet, 13 | SoftwareProductInstallationFilterSet, 14 | SoftwareLicenseFilterSet, 15 | ) 16 | from netbox_slm.models import ( 17 | SoftwareProduct, 18 | SoftwareProductVersion, 19 | SoftwareProductInstallation, 20 | SoftwareLicense, 21 | ) 22 | 23 | 24 | class NetboxSLMRootView(APIRootView): 25 | def get_view_name(self): 26 | return "NetboxSLM" 27 | 28 | 29 | class SoftwareProductViewSet(NetBoxModelViewSet): 30 | queryset = SoftwareProduct.objects.all() 31 | serializer_class = SoftwareProductSerializer 32 | filterset_class = SoftwareProductFilterSet 33 | 34 | 35 | class SoftwareProductVersionViewSet(NetBoxModelViewSet): 36 | queryset = SoftwareProductVersion.objects.all() 37 | serializer_class = SoftwareProductVersionSerializer 38 | filterset_class = SoftwareProductVersionFilterSet 39 | 40 | 41 | class SoftwareProductInstallationViewSet(NetBoxModelViewSet): 42 | queryset = SoftwareProductInstallation.objects.all() 43 | serializer_class = SoftwareProductInstallationSerializer 44 | filterset_class = SoftwareProductInstallationFilterSet 45 | 46 | 47 | class SoftwareLicenseViewSet(NetBoxModelViewSet): 48 | queryset = SoftwareLicense.objects.all() 49 | serializer_class = SoftwareLicenseSerializer 50 | filterset_class = SoftwareLicenseFilterSet 51 | -------------------------------------------------------------------------------- /netbox_slm/filtersets.py: -------------------------------------------------------------------------------- 1 | from django.db.models import Q 2 | from django_filters import CharFilter, ModelMultipleChoiceFilter, MultipleChoiceFilter 3 | 4 | from dcim.models import Device, Manufacturer 5 | from netbox.filtersets import NetBoxModelFilterSet 6 | from netbox_slm.models import ( 7 | SoftwareProduct, 8 | SoftwareProductVersion, 9 | SoftwareProductInstallation, 10 | SoftwareLicense, 11 | SoftwareReleaseTypes, 12 | ) 13 | from virtualization.models import Cluster, VirtualMachine 14 | 15 | 16 | class SoftwareProductFilterSet(NetBoxModelFilterSet): 17 | """Filter capabilities for SoftwareProduct instances.""" 18 | 19 | name = CharFilter(lookup_expr="icontains") 20 | description = CharFilter(lookup_expr="icontains") 21 | 22 | manufacturer_id = ModelMultipleChoiceFilter(queryset=Manufacturer.objects.all()) 23 | 24 | class Meta: 25 | model = SoftwareProduct 26 | fields = tuple() 27 | 28 | def search(self, queryset, name, value): 29 | """Perform the filtered search.""" 30 | if not value.strip(): 31 | return queryset 32 | qs_filter = ( 33 | Q(name__icontains=value) 34 | | Q(description__icontains=value) 35 | | Q(manufacturer__name__icontains=value) 36 | | Q(comments__icontains=value) 37 | ) 38 | return queryset.filter(qs_filter) 39 | 40 | 41 | class SoftwareProductVersionFilterSet(NetBoxModelFilterSet): 42 | """Filter capabilities for SoftwareProductVersion instances.""" 43 | 44 | name = CharFilter(lookup_expr="icontains") 45 | description = CharFilter(lookup_expr="icontains") 46 | filename = CharFilter(lookup_expr="icontains") 47 | 48 | release_type = MultipleChoiceFilter(choices=SoftwareReleaseTypes.choices) 49 | 50 | manufacturer_id = ModelMultipleChoiceFilter( 51 | field_name="software_product__manufacturer", 52 | queryset=Manufacturer.objects.all(), 53 | ) 54 | 55 | software_product_id = ModelMultipleChoiceFilter( 56 | queryset=SoftwareProduct.objects.all(), 57 | label="Software Product", 58 | ) 59 | 60 | class Meta: 61 | model = SoftwareProductVersion 62 | fields = tuple() 63 | 64 | def search(self, queryset, name, value): 65 | """Perform the filtered search.""" 66 | if not value.strip(): 67 | return queryset 68 | qs_filter = ( 69 | Q(name__icontains=value) 70 | | Q(description__icontains=value) 71 | | Q(software_product__name__icontains=value) 72 | | Q(software_product__manufacturer__name__icontains=value) 73 | | Q(comments__icontains=value) 74 | ) 75 | return queryset.filter(qs_filter) 76 | 77 | 78 | class SoftwareProductInstallationFilterSet(NetBoxModelFilterSet): 79 | """Filter capabilities for SoftwareProductInstallation instances.""" 80 | 81 | device_id = ModelMultipleChoiceFilter(queryset=Device.objects.all()) 82 | virtualmachine_id = ModelMultipleChoiceFilter( 83 | queryset=VirtualMachine.objects.all(), 84 | label="Virtual Machine", 85 | ) 86 | cluster_id = ModelMultipleChoiceFilter(queryset=Cluster.objects.all()) 87 | software_product_id = ModelMultipleChoiceFilter( 88 | queryset=SoftwareProduct.objects.all(), 89 | label="Software Product", 90 | ) 91 | version_id = ModelMultipleChoiceFilter(queryset=SoftwareProductVersion.objects.all()) 92 | 93 | class Meta: 94 | model = SoftwareProductInstallation 95 | fields = tuple() 96 | 97 | def search(self, queryset, name, value): 98 | """Perform the filtered search.""" 99 | if not value.strip(): 100 | return queryset 101 | qs_filter = ( 102 | Q(software_product__name__icontains=value) 103 | | Q(software_product__manufacturer__name__icontains=value) 104 | | Q(version__name__icontains=value) 105 | | Q(comments__icontains=value) 106 | ) 107 | return queryset.filter(qs_filter) 108 | 109 | 110 | class SoftwareLicenseFilterSet(NetBoxModelFilterSet): 111 | """Filter capabilities for SoftwareLicense instances.""" 112 | 113 | name = CharFilter(lookup_expr="icontains") 114 | description = CharFilter(lookup_expr="icontains") 115 | type = CharFilter(lookup_expr="icontains") 116 | spdx_expression = CharFilter(lookup_expr="icontains", label="SPDX expression") 117 | stored_location = CharFilter(lookup_expr="icontains") 118 | 119 | software_product_id = ModelMultipleChoiceFilter( 120 | queryset=SoftwareProduct.objects.all(), 121 | label="Software Product", 122 | ) 123 | version_id = ModelMultipleChoiceFilter(queryset=SoftwareProductVersion.objects.all()) 124 | installation_id = ModelMultipleChoiceFilter(queryset=SoftwareProductInstallation.objects.all()) 125 | 126 | class Meta: 127 | model = SoftwareLicense 128 | fields = ("support",) 129 | 130 | def search(self, queryset, name, value): 131 | """Perform the filtered search.""" 132 | if not value.strip(): 133 | return queryset 134 | qs_filter = ( 135 | Q(name__icontains=value) 136 | | Q(description__icontains=value) 137 | | Q(software_product__name__icontains=value) 138 | | Q(version__name__icontains=value) 139 | | Q(installation__device__name__icontains=value) 140 | | Q(installation__virtualmachine__name__icontains=value) 141 | | Q(installation__cluster__name__icontains=value) 142 | | Q(comments__icontains=value) 143 | ) 144 | return queryset.filter(qs_filter) 145 | -------------------------------------------------------------------------------- /netbox_slm/forms/__init__.py: -------------------------------------------------------------------------------- 1 | from .software_license import ( 2 | SoftwareLicenseForm, 3 | SoftwareLicenseFilterForm, 4 | SoftwareLicenseBulkImportForm, 5 | SoftwareLicenseBulkEditForm, 6 | ) 7 | from .software_product import ( 8 | SoftwareProductForm, 9 | SoftwareProductFilterForm, 10 | SoftwareProductBulkImportForm, 11 | SoftwareProductBulkEditForm, 12 | ) 13 | from .software_product_installation import ( 14 | SoftwareProductInstallationForm, 15 | SoftwareProductInstallationFilterForm, 16 | SoftwareProductInstallationBulkImportForm, 17 | SoftwareProductInstallationBulkEditForm, 18 | ) 19 | from .software_product_version import ( 20 | SoftwareProductVersionForm, 21 | SoftwareProductVersionFilterForm, 22 | SoftwareProductVersionBulkImportForm, 23 | SoftwareProductVersionBulkEditForm, 24 | ) 25 | -------------------------------------------------------------------------------- /netbox_slm/forms/software_license.py: -------------------------------------------------------------------------------- 1 | from django.forms import CharField, DateField, ChoiceField, IntegerField, NullBooleanField 2 | from django.urls import reverse_lazy 3 | 4 | from netbox.forms import NetBoxModelForm, NetBoxModelFilterSetForm, NetBoxModelImportForm, NetBoxModelBulkEditForm 5 | from netbox_slm.models import ( 6 | SoftwareProduct, 7 | SoftwareProductVersion, 8 | SoftwareProductInstallation, 9 | SoftwareLicense, 10 | spdx_license_names, 11 | ) 12 | from utilities.forms.constants import BOOLEAN_WITH_BLANK_CHOICES 13 | from utilities.forms.fields import ( 14 | CommentField, 15 | DynamicModelChoiceField, 16 | TagFilterField, 17 | LaxURLField, 18 | DynamicModelMultipleChoiceField, 19 | ) 20 | from utilities.forms.rendering import FieldSet 21 | from utilities.forms.widgets import APISelect, DatePicker 22 | 23 | 24 | class SoftwareLicenseForm(NetBoxModelForm): 25 | comments = CommentField() 26 | 27 | spdx_expression = ChoiceField(required=False, choices=spdx_license_names(), label="SPDX expression") 28 | stored_location_url = LaxURLField(required=False) 29 | start_date = DateField(required=False, widget=DatePicker()) 30 | expiration_date = DateField(required=False, widget=DatePicker()) 31 | 32 | software_product = DynamicModelChoiceField( 33 | queryset=SoftwareProduct.objects.all(), 34 | required=True, 35 | label="Software Product", 36 | ) 37 | version = DynamicModelChoiceField( 38 | queryset=SoftwareProductVersion.objects.all(), 39 | required=False, 40 | widget=APISelect(attrs={"data-url": reverse_lazy("plugins-api:netbox_slm-api:softwareproductversion-list")}), 41 | query_params=dict(software_product="$software_product"), 42 | ) 43 | installation = DynamicModelChoiceField( 44 | queryset=SoftwareProductInstallation.objects.all(), 45 | required=False, 46 | widget=APISelect( 47 | attrs={"data-url": reverse_lazy("plugins-api:netbox_slm-api:softwareproductinstallation-list")} 48 | ), 49 | query_params=dict(software_product="$software_product"), 50 | ) 51 | 52 | class Meta: 53 | model = SoftwareLicense 54 | fields = ( 55 | "name", 56 | "description", 57 | "software_product", 58 | "type", 59 | "spdx_expression", 60 | "stored_location", 61 | "stored_location_url", 62 | "start_date", 63 | "expiration_date", 64 | "support", 65 | "license_amount", 66 | "version", 67 | "installation", 68 | "tags", 69 | "comments", 70 | ) 71 | 72 | 73 | class SoftwareLicenseFilterForm(NetBoxModelFilterSetForm): 74 | model = SoftwareLicense 75 | fieldsets = ( 76 | FieldSet("q", "filter_id", "tag"), 77 | FieldSet( 78 | "name", 79 | "description", 80 | "type", 81 | "spdx_expression", 82 | "stored_location", 83 | "support", 84 | "software_product_id", 85 | "version_id", 86 | "installation_id", 87 | ), 88 | ) 89 | selector_fields = ("q", "filter_id", "name") 90 | 91 | tag = TagFilterField(model) 92 | 93 | name = CharField(required=False) 94 | description = CharField(required=False) 95 | type = CharField(required=False) 96 | spdx_expression = CharField(required=False, label="SPDX expression") 97 | stored_location = CharField(required=False) 98 | support = ChoiceField(required=False, choices=BOOLEAN_WITH_BLANK_CHOICES) 99 | 100 | software_product_id = DynamicModelMultipleChoiceField( 101 | queryset=SoftwareProduct.objects.all(), 102 | required=False, 103 | label="Software Product", 104 | ) 105 | version_id = DynamicModelMultipleChoiceField( 106 | queryset=SoftwareProductVersion.objects.all(), 107 | required=False, 108 | label="Version", 109 | ) 110 | installation_id = DynamicModelMultipleChoiceField( 111 | queryset=SoftwareProductInstallation.objects.all(), 112 | required=False, 113 | label="Installation", 114 | ) 115 | 116 | 117 | class SoftwareLicenseBulkImportForm(NetBoxModelImportForm): 118 | support = NullBooleanField(required=False, help_text="Support (values other than 'True' and 'False' are ignored)") 119 | 120 | class Meta: 121 | model = SoftwareLicense 122 | fields = ( 123 | "name", 124 | "description", 125 | "software_product", 126 | "type", 127 | "spdx_expression", 128 | "stored_location", 129 | "start_date", 130 | "expiration_date", 131 | "support", 132 | "license_amount", 133 | "version", 134 | "installation", 135 | "tags", 136 | ) 137 | 138 | 139 | class SoftwareLicenseBulkEditForm(NetBoxModelBulkEditForm): 140 | model = SoftwareLicense 141 | fieldsets = ( 142 | FieldSet( 143 | "type", 144 | "spdx_expression", 145 | "stored_location", 146 | "stored_location_url", 147 | "start_date", 148 | "expiration_date", 149 | "support", 150 | "license_amount", 151 | "software_product", 152 | "version", 153 | "installation", 154 | ), 155 | ) 156 | nullable_fields = ( 157 | "type", 158 | "spdx_expression", 159 | "stored_location", 160 | "stored_location_url", 161 | "start_date", 162 | "expiration_date", 163 | "support", 164 | "license_amount", 165 | "version", 166 | "installation", 167 | ) 168 | 169 | tag = TagFilterField(model) 170 | comments = CommentField() 171 | 172 | type = CharField(required=False) 173 | spdx_expression = ChoiceField(required=False, choices=spdx_license_names(), label="SPDX expression") 174 | stored_location = CharField(required=False) 175 | stored_location_url = LaxURLField(required=False) 176 | start_date = DateField(required=False, widget=DatePicker()) 177 | expiration_date = DateField(required=False, widget=DatePicker()) 178 | support = ChoiceField(required=False, choices=BOOLEAN_WITH_BLANK_CHOICES) 179 | license_amount = IntegerField(required=False, min_value=0) 180 | 181 | software_product = DynamicModelChoiceField( 182 | queryset=SoftwareProduct.objects.all(), 183 | required=False, 184 | label="Software Product", 185 | ) 186 | version = DynamicModelChoiceField( 187 | queryset=SoftwareProductVersion.objects.all(), 188 | required=False, 189 | widget=APISelect(attrs={"data-url": reverse_lazy("plugins-api:netbox_slm-api:softwareproductversion-list")}), 190 | query_params=dict(software_product="$software_product"), 191 | ) 192 | installation = DynamicModelChoiceField( 193 | queryset=SoftwareProductInstallation.objects.all(), 194 | required=False, 195 | widget=APISelect( 196 | attrs={"data-url": reverse_lazy("plugins-api:netbox_slm-api:softwareproductinstallation-list")} 197 | ), 198 | query_params=dict(software_product="$software_product"), 199 | ) 200 | -------------------------------------------------------------------------------- /netbox_slm/forms/software_product.py: -------------------------------------------------------------------------------- 1 | from django.forms import CharField 2 | 3 | from dcim.models import Manufacturer 4 | from netbox.forms import NetBoxModelForm, NetBoxModelFilterSetForm, NetBoxModelImportForm, NetBoxModelBulkEditForm 5 | from netbox_slm.models import SoftwareProduct 6 | from utilities.forms.fields import ( 7 | CommentField, 8 | DynamicModelChoiceField, 9 | TagFilterField, 10 | DynamicModelMultipleChoiceField, 11 | ) 12 | from utilities.forms.rendering import FieldSet 13 | 14 | 15 | class SoftwareProductForm(NetBoxModelForm): 16 | comments = CommentField() 17 | 18 | manufacturer = DynamicModelChoiceField( 19 | queryset=Manufacturer.objects.all(), 20 | required=True, 21 | ) 22 | 23 | class Meta: 24 | model = SoftwareProduct 25 | fields = ( 26 | "name", 27 | "description", 28 | "manufacturer", 29 | "tags", 30 | "comments", 31 | ) 32 | 33 | 34 | class SoftwareProductFilterForm(NetBoxModelFilterSetForm): 35 | model = SoftwareProduct 36 | fieldsets = ( 37 | FieldSet("q", "filter_id", "tag"), 38 | FieldSet("name", "description", "manufacturer_id"), 39 | ) 40 | selector_fields = ("q", "filter_id", "name") 41 | 42 | tag = TagFilterField(model) 43 | 44 | name = CharField(required=False) 45 | description = CharField(required=False) 46 | manufacturer_id = DynamicModelMultipleChoiceField( 47 | queryset=Manufacturer.objects.all(), 48 | required=False, 49 | label="Manufacturer", 50 | ) 51 | 52 | 53 | class SoftwareProductBulkImportForm(NetBoxModelImportForm): 54 | class Meta: 55 | model = SoftwareProduct 56 | fields = ( 57 | "name", 58 | "description", 59 | "manufacturer", 60 | "tags", 61 | ) 62 | 63 | 64 | class SoftwareProductBulkEditForm(NetBoxModelBulkEditForm): 65 | model = SoftwareProduct 66 | fieldsets = (FieldSet("manufacturer"),) 67 | nullable_fields = ("manufacturer", "comments") 68 | 69 | tag = TagFilterField(model) 70 | comments = CommentField() 71 | 72 | manufacturer = DynamicModelChoiceField( 73 | queryset=Manufacturer.objects.all(), 74 | required=False, 75 | ) 76 | -------------------------------------------------------------------------------- /netbox_slm/forms/software_product_installation.py: -------------------------------------------------------------------------------- 1 | from django.forms import ValidationError 2 | from django.urls import reverse_lazy 3 | 4 | from dcim.models import Device 5 | from netbox.forms import NetBoxModelForm, NetBoxModelFilterSetForm, NetBoxModelImportForm, NetBoxModelBulkEditForm 6 | from netbox_slm.models import SoftwareProductInstallation, SoftwareProduct, SoftwareProductVersion 7 | from utilities.forms.fields import ( 8 | CommentField, 9 | DynamicModelChoiceField, 10 | TagFilterField, 11 | DynamicModelMultipleChoiceField, 12 | ) 13 | from utilities.forms.rendering import FieldSet 14 | from utilities.forms.widgets import APISelect 15 | from virtualization.models import Cluster, VirtualMachine 16 | 17 | 18 | class SoftwareProductInstallationForm(NetBoxModelForm): 19 | comments = CommentField() 20 | 21 | device = DynamicModelChoiceField(queryset=Device.objects.all(), required=False) 22 | virtualmachine = DynamicModelChoiceField( 23 | queryset=VirtualMachine.objects.all(), 24 | required=False, 25 | label="Virtual Machine", 26 | ) 27 | cluster = DynamicModelChoiceField(queryset=Cluster.objects.all(), required=False) 28 | software_product = DynamicModelChoiceField( 29 | queryset=SoftwareProduct.objects.all(), 30 | required=True, 31 | label="Software Product", 32 | ) 33 | version = DynamicModelChoiceField( 34 | queryset=SoftwareProductVersion.objects.all(), 35 | required=True, 36 | widget=APISelect(attrs={"data-url": reverse_lazy("plugins-api:netbox_slm-api:softwareproductversion-list")}), 37 | query_params=dict(software_product="$software_product"), 38 | ) 39 | 40 | class Meta: 41 | model = SoftwareProductInstallation 42 | fields = ( 43 | "device", 44 | "virtualmachine", 45 | "cluster", 46 | "software_product", 47 | "version", 48 | "tags", 49 | "comments", 50 | ) 51 | 52 | def clean_version(self): 53 | version = self.cleaned_data["version"] 54 | software_product = self.cleaned_data["software_product"] 55 | if version not in software_product.softwareproductversion_set.all(): 56 | raise ValidationError( 57 | f"Version '{version}' doesn't exist on {software_product}, make sure you've " 58 | f"selected a compatible version or first select the software product." 59 | ) 60 | return version 61 | 62 | 63 | class SoftwareProductInstallationFilterForm(NetBoxModelFilterSetForm): 64 | model = SoftwareProductInstallation 65 | fieldsets = ( 66 | FieldSet("q", "filter_id", "tag"), 67 | FieldSet("device_id", "virtualmachine_id", "cluster_id", "software_product_id", "version_id"), 68 | ) 69 | selector_fields = ("filter_id", "q") 70 | 71 | tag = TagFilterField(model) 72 | 73 | device_id = DynamicModelMultipleChoiceField( 74 | queryset=Device.objects.all(), 75 | required=False, 76 | label="Device", 77 | ) 78 | virtualmachine_id = DynamicModelMultipleChoiceField( 79 | queryset=VirtualMachine.objects.all(), 80 | required=False, 81 | label="Virtual Machine", 82 | ) 83 | cluster_id = DynamicModelMultipleChoiceField( 84 | queryset=Cluster.objects.all(), 85 | required=False, 86 | label="Cluster", 87 | ) 88 | software_product_id = DynamicModelMultipleChoiceField( 89 | queryset=SoftwareProduct.objects.all(), 90 | required=False, 91 | label="Software Product", 92 | ) 93 | version_id = DynamicModelMultipleChoiceField( 94 | queryset=SoftwareProductVersion.objects.all(), 95 | required=False, 96 | label="Version", 97 | ) 98 | 99 | 100 | class SoftwareProductInstallationBulkImportForm(NetBoxModelImportForm): 101 | class Meta: 102 | model = SoftwareProductInstallation 103 | fields = ( 104 | "device", 105 | "virtualmachine", 106 | "cluster", 107 | "software_product", 108 | "version", 109 | "tags", 110 | ) 111 | 112 | 113 | class SoftwareProductInstallationBulkEditForm(NetBoxModelBulkEditForm): 114 | model = SoftwareProductInstallation 115 | fieldsets = (FieldSet("device", "virtualmachine", "cluster", "software_product", "version"),) 116 | nullable_fields = ("device", "virtualmachine", "cluster", "comments") 117 | 118 | tag = TagFilterField(model) 119 | comments = CommentField() 120 | 121 | device = DynamicModelChoiceField(queryset=Device.objects.all(), required=False) 122 | virtualmachine = DynamicModelChoiceField( 123 | queryset=VirtualMachine.objects.all(), 124 | required=False, 125 | label="Virtual Machine", 126 | ) 127 | cluster = DynamicModelChoiceField(queryset=Cluster.objects.all(), required=False) 128 | software_product = DynamicModelChoiceField( 129 | queryset=SoftwareProduct.objects.all(), 130 | required=False, 131 | label="Software Product", 132 | ) 133 | version = DynamicModelChoiceField( 134 | queryset=SoftwareProductVersion.objects.all(), 135 | required=False, 136 | widget=APISelect(attrs={"data-url": reverse_lazy("plugins-api:netbox_slm-api:softwareproductversion-list")}), 137 | query_params=dict(software_product="$software_product"), 138 | ) 139 | -------------------------------------------------------------------------------- /netbox_slm/forms/software_product_version.py: -------------------------------------------------------------------------------- 1 | from django.forms import DateField, CharField, MultipleChoiceField 2 | from django.urls import reverse_lazy 3 | 4 | from dcim.models import Manufacturer 5 | from netbox.forms import NetBoxModelForm, NetBoxModelFilterSetForm, NetBoxModelImportForm, NetBoxModelBulkEditForm 6 | from netbox_slm.models import SoftwareProduct, SoftwareProductVersion, SoftwareReleaseTypes 7 | from utilities.forms.fields import ( 8 | CommentField, 9 | DynamicModelChoiceField, 10 | TagFilterField, 11 | DynamicModelMultipleChoiceField, 12 | ) 13 | from utilities.forms.rendering import FieldSet 14 | from utilities.forms.widgets import APISelect, DatePicker 15 | 16 | 17 | class SoftwareProductVersionForm(NetBoxModelForm): 18 | comments = CommentField() 19 | 20 | release_date = DateField(required=False, widget=DatePicker()) 21 | end_of_support = DateField(required=False, widget=DatePicker()) 22 | 23 | software_product = DynamicModelChoiceField( 24 | queryset=SoftwareProduct.objects.all(), 25 | required=True, 26 | label="Software Product", 27 | widget=APISelect(attrs={"data-url": reverse_lazy("plugins-api:netbox_slm-api:softwareproduct-list")}), 28 | ) 29 | 30 | class Meta: 31 | model = SoftwareProductVersion 32 | fields = ( 33 | "name", 34 | "description", 35 | "software_product", 36 | "release_date", 37 | "documentation_url", 38 | "end_of_support", 39 | "filename", 40 | "file_checksum", 41 | "file_link", 42 | "release_type", 43 | "tags", 44 | "comments", 45 | ) 46 | 47 | 48 | class SoftwareProductVersionFilterForm(NetBoxModelFilterSetForm): 49 | model = SoftwareProductVersion 50 | fieldsets = ( 51 | FieldSet("q", "filter_id", "tag"), 52 | FieldSet("name", "description", "filename", "release_type", "manufacturer_id", "software_product_id"), 53 | ) 54 | selector_fields = ("q", "filter_id", "name") 55 | 56 | tag = TagFilterField(model) 57 | 58 | name = CharField(required=False) 59 | description = CharField(required=False) 60 | filename = CharField(required=False) 61 | 62 | release_type = MultipleChoiceField(required=False, choices=SoftwareReleaseTypes.choices) 63 | 64 | manufacturer_id = DynamicModelMultipleChoiceField( 65 | queryset=Manufacturer.objects.all(), 66 | required=False, 67 | label="Manufacturer", 68 | ) 69 | software_product_id = DynamicModelMultipleChoiceField( 70 | queryset=SoftwareProduct.objects.all(), 71 | required=False, 72 | label="Software Product", 73 | ) 74 | 75 | 76 | class SoftwareProductVersionBulkImportForm(NetBoxModelImportForm): 77 | release_type = CharField(help_text=f"Release type (possible values: {SoftwareReleaseTypes.values})") 78 | 79 | class Meta: 80 | model = SoftwareProductVersion 81 | fields = ( 82 | "name", 83 | "description", 84 | "software_product", 85 | "release_date", 86 | "end_of_support", 87 | "filename", 88 | "file_checksum", 89 | "release_type", 90 | "tags", 91 | ) 92 | 93 | 94 | class SoftwareProductVersionBulkEditForm(NetBoxModelBulkEditForm): 95 | model = SoftwareProductVersion 96 | fieldsets = ( 97 | FieldSet( 98 | "release_date", 99 | "documentation_url", 100 | "end_of_support", 101 | "filename", 102 | "file_checksum", 103 | "file_link", 104 | "release_type", 105 | "software_product", 106 | ), 107 | ) 108 | nullable_fields = ("release_date", "documentation_url", "end_of_support", "filename", "file_checksum", "file_link") 109 | 110 | tag = TagFilterField(model) 111 | comments = CommentField() 112 | 113 | release_date = DateField(required=False, widget=DatePicker()) 114 | documentation_url = CharField(required=False) 115 | end_of_support = DateField(required=False, widget=DatePicker()) 116 | filename = CharField(required=False) 117 | file_checksum = CharField(required=False) 118 | file_link = CharField(required=False) 119 | 120 | release_type = MultipleChoiceField(required=False, choices=SoftwareReleaseTypes.choices) 121 | 122 | software_product = DynamicModelChoiceField( 123 | queryset=SoftwareProduct.objects.all(), 124 | required=False, 125 | label="Software Product", 126 | ) 127 | -------------------------------------------------------------------------------- /netbox_slm/migrations/0001_initial.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 3.2.8 on 2021-12-17 10:11 2 | 3 | import django.core.serializers.json 4 | from django.db import migrations, models 5 | import django.db.models.deletion 6 | import taggit.managers 7 | 8 | 9 | class Migration(migrations.Migration): 10 | 11 | initial = True 12 | 13 | dependencies = [ 14 | ('virtualization', '0023_virtualmachine_natural_ordering'), 15 | ('extras', '0062_clear_secrets_changelog'), 16 | ('dcim', '0133_port_colors'), 17 | ] 18 | 19 | operations = [ 20 | migrations.CreateModel( 21 | name='SoftwareProduct', 22 | fields=[ 23 | ('created', models.DateField(auto_now_add=True, null=True)), 24 | ('last_updated', models.DateTimeField(auto_now=True, null=True)), 25 | ('custom_field_data', models.JSONField(blank=True, default=dict, encoder=django.core.serializers.json.DjangoJSONEncoder)), 26 | ('id', models.BigAutoField(primary_key=True, serialize=False)), 27 | ('name', models.CharField(max_length=128)), 28 | ('description', models.CharField(blank=True, max_length=255, null=True)), 29 | ('manufacturer', models.ForeignKey(on_delete=django.db.models.deletion.PROTECT, related_name='software_products', to='dcim.manufacturer')), 30 | ('tags', taggit.managers.TaggableManager(through='extras.TaggedItem', to='extras.Tag')), 31 | ], 32 | options={ 33 | 'abstract': False, 34 | }, 35 | ), 36 | migrations.CreateModel( 37 | name='SoftwareProductVersion', 38 | fields=[ 39 | ('created', models.DateField(auto_now_add=True, null=True)), 40 | ('last_updated', models.DateTimeField(auto_now=True, null=True)), 41 | ('custom_field_data', models.JSONField(blank=True, default=dict, encoder=django.core.serializers.json.DjangoJSONEncoder)), 42 | ('id', models.BigAutoField(primary_key=True, serialize=False)), 43 | ('name', models.CharField(max_length=64)), 44 | ('software_product', models.ForeignKey(on_delete=django.db.models.deletion.PROTECT, related_name='softwareproduct_versions', to='netbox_slm.softwareproduct')), 45 | ('tags', taggit.managers.TaggableManager(through='extras.TaggedItem', to='extras.Tag')), 46 | ], 47 | options={ 48 | 'abstract': False, 49 | }, 50 | ), 51 | migrations.CreateModel( 52 | name='SoftwareProductInstallation', 53 | fields=[ 54 | ('created', models.DateField(auto_now_add=True, null=True)), 55 | ('last_updated', models.DateTimeField(auto_now=True, null=True)), 56 | ('custom_field_data', models.JSONField(blank=True, default=dict, encoder=django.core.serializers.json.DjangoJSONEncoder)), 57 | ('id', models.BigAutoField(primary_key=True, serialize=False)), 58 | ('device', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.PROTECT, related_name='softwareproduct_installations', to='dcim.device')), 59 | ('software_product', models.ForeignKey(on_delete=django.db.models.deletion.PROTECT, related_name='software_products', to='netbox_slm.softwareproduct')), 60 | ('tags', taggit.managers.TaggableManager(through='extras.TaggedItem', to='extras.Tag')), 61 | ('version', models.ForeignKey(on_delete=django.db.models.deletion.PROTECT, related_name='softwareproduct_versions', to='netbox_slm.softwareproductversion')), 62 | ('virtualmachine', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.PROTECT, related_name='softwareproduct_installations', to='virtualization.virtualmachine')), 63 | ], 64 | options={ 65 | 'abstract': False, 66 | }, 67 | ), 68 | ] 69 | -------------------------------------------------------------------------------- /netbox_slm/migrations/0002_alter_softwareproduct_manufacturer.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 3.2.9 on 2022-01-24 08:05 2 | 3 | from django.db import migrations, models 4 | import django.db.models.deletion 5 | 6 | 7 | class Migration(migrations.Migration): 8 | 9 | dependencies = [ 10 | ('dcim', '0133_port_colors'), 11 | ('netbox_slm', '0001_initial'), 12 | ] 13 | 14 | operations = [ 15 | migrations.AlterField( 16 | model_name='softwareproduct', 17 | name='manufacturer', 18 | field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.PROTECT, related_name='software_products', to='dcim.manufacturer'), 19 | ), 20 | ] 21 | -------------------------------------------------------------------------------- /netbox_slm/migrations/0003_auto_20220407_1209.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 3.2.9 on 2022-04-07 12:09 2 | 3 | from django.db import migrations, models 4 | 5 | 6 | class Migration(migrations.Migration): 7 | 8 | dependencies = [ 9 | ('netbox_slm', '0002_alter_softwareproduct_manufacturer'), 10 | ] 11 | 12 | operations = [ 13 | migrations.AlterField( 14 | model_name='softwareproduct', 15 | name='created', 16 | field=models.DateTimeField(auto_now_add=True, null=True), 17 | ), 18 | migrations.AlterField( 19 | model_name='softwareproduct', 20 | name='id', 21 | field=models.BigAutoField(auto_created=True, primary_key=True, serialize=False), 22 | ), 23 | migrations.AlterField( 24 | model_name='softwareproductinstallation', 25 | name='created', 26 | field=models.DateTimeField(auto_now_add=True, null=True), 27 | ), 28 | migrations.AlterField( 29 | model_name='softwareproductinstallation', 30 | name='id', 31 | field=models.BigAutoField(auto_created=True, primary_key=True, serialize=False), 32 | ), 33 | migrations.AlterField( 34 | model_name='softwareproductversion', 35 | name='created', 36 | field=models.DateTimeField(auto_now_add=True, null=True), 37 | ), 38 | migrations.AlterField( 39 | model_name='softwareproductversion', 40 | name='id', 41 | field=models.BigAutoField(auto_created=True, primary_key=True, serialize=False), 42 | ), 43 | ] 44 | -------------------------------------------------------------------------------- /netbox_slm/migrations/0004_auto_20220415_0705.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 3.2.9 on 2022-04-15 07:05 2 | 3 | from django.db import migrations, models 4 | import django.db.models.deletion 5 | 6 | 7 | class Migration(migrations.Migration): 8 | 9 | dependencies = [ 10 | ('dcim', '0153_created_datetimefield'), 11 | ('virtualization', '0029_created_datetimefield'), 12 | ('netbox_slm', '0003_auto_20220407_1209'), 13 | ] 14 | 15 | operations = [ 16 | migrations.AlterField( 17 | model_name='softwareproduct', 18 | name='manufacturer', 19 | field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.PROTECT, to='dcim.manufacturer'), 20 | ), 21 | migrations.AlterField( 22 | model_name='softwareproductinstallation', 23 | name='device', 24 | field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.PROTECT, to='dcim.device'), 25 | ), 26 | migrations.AlterField( 27 | model_name='softwareproductinstallation', 28 | name='software_product', 29 | field=models.ForeignKey(on_delete=django.db.models.deletion.PROTECT, to='netbox_slm.softwareproduct'), 30 | ), 31 | migrations.AlterField( 32 | model_name='softwareproductinstallation', 33 | name='version', 34 | field=models.ForeignKey(on_delete=django.db.models.deletion.PROTECT, to='netbox_slm.softwareproductversion'), 35 | ), 36 | migrations.AlterField( 37 | model_name='softwareproductinstallation', 38 | name='virtualmachine', 39 | field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.PROTECT, to='virtualization.virtualmachine'), 40 | ), 41 | migrations.AlterField( 42 | model_name='softwareproductversion', 43 | name='software_product', 44 | field=models.ForeignKey(on_delete=django.db.models.deletion.PROTECT, to='netbox_slm.softwareproduct'), 45 | ), 46 | ] 47 | -------------------------------------------------------------------------------- /netbox_slm/migrations/0005_add_software_license.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 4.1.8 on 2023-06-27 08:10 2 | 3 | from django.db import migrations, models 4 | import django.db.models.deletion 5 | import taggit.managers 6 | import utilities.json 7 | 8 | 9 | class Migration(migrations.Migration): 10 | 11 | dependencies = [ 12 | ('extras', '0084_staging'), 13 | ('netbox_slm', '0004_auto_20220415_0705'), 14 | ] 15 | 16 | operations = [ 17 | migrations.AlterField( 18 | model_name='softwareproduct', 19 | name='custom_field_data', 20 | field=models.JSONField(blank=True, default=dict, encoder=utilities.json.CustomFieldJSONEncoder), 21 | ), 22 | migrations.AlterField( 23 | model_name='softwareproductinstallation', 24 | name='custom_field_data', 25 | field=models.JSONField(blank=True, default=dict, encoder=utilities.json.CustomFieldJSONEncoder), 26 | ), 27 | migrations.AlterField( 28 | model_name='softwareproductversion', 29 | name='custom_field_data', 30 | field=models.JSONField(blank=True, default=dict, encoder=utilities.json.CustomFieldJSONEncoder), 31 | ), 32 | migrations.CreateModel( 33 | name='SoftwareLicense', 34 | fields=[ 35 | ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False)), 36 | ('created', models.DateTimeField(auto_now_add=True, null=True)), 37 | ('last_updated', models.DateTimeField(auto_now=True, null=True)), 38 | ('custom_field_data', models.JSONField(blank=True, default=dict, encoder=utilities.json.CustomFieldJSONEncoder)), 39 | ('name', models.CharField(max_length=128)), 40 | ('description', models.CharField(blank=True, max_length=255, null=True)), 41 | ('type', models.CharField(max_length=128)), 42 | ('stored_location', models.CharField(blank=True, max_length=255, null=True)), 43 | ('start_date', models.DateField(blank=True, null=True)), 44 | ('expiration_date', models.DateField(blank=True, null=True)), 45 | ('installation', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.PROTECT, to='netbox_slm.softwareproductinstallation')), 46 | ('software_product', models.ForeignKey(on_delete=django.db.models.deletion.PROTECT, to='netbox_slm.softwareproduct')), 47 | ('tags', taggit.managers.TaggableManager(through='extras.TaggedItem', to='extras.Tag')), 48 | ('version', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.PROTECT, to='netbox_slm.softwareproductversion')), 49 | ], 50 | options={ 51 | 'abstract': False, 52 | }, 53 | ), 54 | ] 55 | -------------------------------------------------------------------------------- /netbox_slm/migrations/0006_softwarelicense_stored_location_url.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 4.2.5 on 2023-09-22 00:34 2 | 3 | from django.db import migrations 4 | import netbox_slm.models 5 | 6 | 7 | class Migration(migrations.Migration): 8 | 9 | dependencies = [ 10 | ('netbox_slm', '0005_add_software_license'), 11 | ] 12 | 13 | operations = [ 14 | migrations.AddField( 15 | model_name='softwarelicense', 16 | name='stored_location_url', 17 | field=netbox_slm.models.LaxURLField(blank=True, max_length=1024, null=True), 18 | ), 19 | ] 20 | -------------------------------------------------------------------------------- /netbox_slm/migrations/0007_softwareproductinstallation_cluster.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 4.2.5 on 2023-10-04 10:07 2 | 3 | from django.db import migrations, models 4 | import django.db.models.deletion 5 | 6 | 7 | class Migration(migrations.Migration): 8 | 9 | dependencies = [ 10 | ('virtualization', '0036_virtualmachine_config_template'), 11 | ('netbox_slm', '0006_softwarelicense_stored_location_url'), 12 | ] 13 | 14 | operations = [ 15 | migrations.AddField( 16 | model_name='softwareproductinstallation', 17 | name='cluster', 18 | field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.PROTECT, to='virtualization.cluster'), 19 | ), 20 | migrations.AlterField( 21 | model_name='softwarelicense', 22 | name='installation', 23 | field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, to='netbox_slm.softwareproductinstallation'), 24 | ), 25 | migrations.AddConstraint( 26 | model_name='softwareproductinstallation', 27 | constraint=models.CheckConstraint(check=models.Q(models.Q(('cluster__isnull', True), ('device__isnull', False), ('virtualmachine__isnull', True)), models.Q(('cluster__isnull', True), ('device__isnull', True), ('virtualmachine__isnull', False)), models.Q(('cluster__isnull', False), ('device__isnull', True), ('virtualmachine__isnull', True)), _connector='OR'), name='netbox_slm_softwareproductinstallation_platform', violation_error_message='Installation requires exactly one platform destination.'), 28 | ), 29 | ] 30 | -------------------------------------------------------------------------------- /netbox_slm/migrations/0008_software_release_types_and_more.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 4.2.9 on 2024-02-07 21:01 2 | 3 | from django.db import migrations, models 4 | import netbox_slm.models 5 | 6 | 7 | class Migration(migrations.Migration): 8 | 9 | dependencies = [ 10 | ('netbox_slm', '0007_softwareproductinstallation_cluster'), 11 | ] 12 | 13 | operations = [ 14 | migrations.AddField( 15 | model_name='softwarelicense', 16 | name='comments', 17 | field=models.TextField(blank=True), 18 | ), 19 | migrations.AddField( 20 | model_name='softwarelicense', 21 | name='license_amount', 22 | field=models.PositiveIntegerField(blank=True, default=None, null=True), 23 | ), 24 | migrations.AddField( 25 | model_name='softwarelicense', 26 | name='support', 27 | field=models.BooleanField(blank=True, default=None, null=True), 28 | ), 29 | migrations.AddField( 30 | model_name='softwareproduct', 31 | name='comments', 32 | field=models.TextField(blank=True), 33 | ), 34 | migrations.AddField( 35 | model_name='softwareproductinstallation', 36 | name='comments', 37 | field=models.TextField(blank=True), 38 | ), 39 | migrations.AddField( 40 | model_name='softwareproductversion', 41 | name='comments', 42 | field=models.TextField(blank=True), 43 | ), 44 | migrations.AddField( 45 | model_name='softwareproductversion', 46 | name='documentation_url', 47 | field=netbox_slm.models.LaxURLField(blank=True, max_length=1024, null=True), 48 | ), 49 | migrations.AddField( 50 | model_name='softwareproductversion', 51 | name='end_of_support', 52 | field=models.DateField(blank=True, null=True), 53 | ), 54 | migrations.AddField( 55 | model_name='softwareproductversion', 56 | name='file_checksum', 57 | field=models.CharField(blank=True, max_length=128, null=True), 58 | ), 59 | migrations.AddField( 60 | model_name='softwareproductversion', 61 | name='file_link', 62 | field=netbox_slm.models.LaxURLField(blank=True, max_length=1024, null=True), 63 | ), 64 | migrations.AddField( 65 | model_name='softwareproductversion', 66 | name='filename', 67 | field=models.CharField(blank=True, max_length=64, null=True), 68 | ), 69 | migrations.AddField( 70 | model_name='softwareproductversion', 71 | name='release_date', 72 | field=models.DateField(blank=True, null=True), 73 | ), 74 | migrations.AddField( 75 | model_name='softwareproductversion', 76 | name='release_type', 77 | field=models.CharField(default='S', max_length=3), 78 | ), 79 | ] 80 | -------------------------------------------------------------------------------- /netbox_slm/migrations/0009_softwareproductversion_description.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 5.1.5 on 2025-02-17 21:35 2 | 3 | from django.db import migrations, models 4 | 5 | 6 | class Migration(migrations.Migration): 7 | 8 | dependencies = [ 9 | ("netbox_slm", "0008_software_release_types_and_more"), 10 | ] 11 | 12 | operations = [ 13 | migrations.AddField( 14 | model_name="softwareproductversion", 15 | name="description", 16 | field=models.CharField(blank=True, max_length=255, null=True), 17 | ), 18 | ] 19 | -------------------------------------------------------------------------------- /netbox_slm/migrations/0010_softwarelicense_spdx_expression.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 5.1.5 on 2025-02-24 18:03 2 | 3 | import netbox_slm.models 4 | from django.db import migrations, models 5 | 6 | 7 | class Migration(migrations.Migration): 8 | 9 | dependencies = [ 10 | ("netbox_slm", "0009_softwareproductversion_description"), 11 | ] 12 | 13 | operations = [ 14 | migrations.AddField( 15 | model_name="softwarelicense", 16 | name="spdx_expression", 17 | field=models.CharField( 18 | blank=True, 19 | max_length=64, 20 | null=True, 21 | validators=[netbox_slm.models.validate_spdx_expression], 22 | ), 23 | ), 24 | ] 25 | -------------------------------------------------------------------------------- /netbox_slm/migrations/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ICTU/netbox_slm/9e87afcde7780b4b92af9cb7e6e5ff3f7cbbf13f/netbox_slm/migrations/__init__.py -------------------------------------------------------------------------------- /netbox_slm/models.py: -------------------------------------------------------------------------------- 1 | from django.core.exceptions import ValidationError 2 | from django.db import models 3 | from django.urls import reverse 4 | from django.utils.html import format_html, urlencode 5 | from license_expression import Licensing, get_spdx_licensing 6 | 7 | from netbox.models import NetBoxModel 8 | from utilities.querysets import RestrictedQuerySet 9 | from utilities.validators import EnhancedURLValidator 10 | 11 | spdx_licensing: Licensing = get_spdx_licensing() 12 | 13 | 14 | class LaxURLField(models.URLField): 15 | """ 16 | NetBox Custom Field approach, based on utilities.forms.fields.LaxURLField 17 | Overriding default_validators is needed, as they are always added 18 | """ 19 | 20 | default_validators = [EnhancedURLValidator()] 21 | 22 | 23 | class SoftwareProduct(NetBoxModel): 24 | name = models.CharField(max_length=128) 25 | comments = models.TextField(blank=True) 26 | 27 | description = models.CharField(max_length=255, null=True, blank=True) 28 | manufacturer = models.ForeignKey(to="dcim.Manufacturer", on_delete=models.PROTECT, null=True, blank=True) 29 | 30 | objects = RestrictedQuerySet.as_manager() 31 | 32 | def __str__(self): 33 | return self.name 34 | 35 | def get_absolute_url(self): 36 | return reverse("plugins:netbox_slm:softwareproduct", kwargs={"pk": self.pk}) 37 | 38 | def get_installation_count(self): 39 | count = SoftwareProductInstallation.objects.filter(software_product_id=self.pk).count() 40 | query_string = urlencode(dict(software_product_id=self.pk)) 41 | search_target = reverse("plugins:netbox_slm:softwareproductinstallation_list") 42 | # Can be composed directly with reverse(query=) in Django 5.2, see https://code.djangoproject.com/ticket/25582 43 | return format_html(f"{count}") if count else "0" 44 | 45 | 46 | class SoftwareReleaseTypes(models.TextChoices): 47 | ALPHA = "A", "Alpha" 48 | BETA = "B", "Beta" 49 | RELEASE_CANDIDATE = "RC", "Release candidate" 50 | STABLE = "S", "Stable release" 51 | 52 | 53 | class SoftwareProductVersion(NetBoxModel): 54 | name = models.CharField(max_length=64) 55 | comments = models.TextField(blank=True) 56 | 57 | description = models.CharField(max_length=255, null=True, blank=True) 58 | release_date = models.DateField(null=True, blank=True) 59 | documentation_url = LaxURLField(max_length=1024, null=True, blank=True) 60 | end_of_support = models.DateField(null=True, blank=True) 61 | filename = models.CharField(max_length=64, null=True, blank=True) 62 | file_checksum = models.CharField(max_length=128, null=True, blank=True) 63 | file_link = LaxURLField(max_length=1024, null=True, blank=True) 64 | 65 | release_type = models.CharField( 66 | max_length=3, 67 | choices=SoftwareReleaseTypes.choices, 68 | default=SoftwareReleaseTypes.STABLE, 69 | ) 70 | 71 | software_product = models.ForeignKey( 72 | to="netbox_slm.SoftwareProduct", 73 | on_delete=models.PROTECT, 74 | ) 75 | 76 | objects = RestrictedQuerySet.as_manager() 77 | 78 | def __str__(self): 79 | return self.name 80 | 81 | def get_absolute_url(self): 82 | return reverse("plugins:netbox_slm:softwareproductversion", kwargs={"pk": self.pk}) 83 | 84 | def get_installation_count(self): 85 | count = SoftwareProductInstallation.objects.filter(version_id=self.pk).count() 86 | query_string = urlencode(dict(version_id=self.pk)) 87 | search_target = reverse("plugins:netbox_slm:softwareproductinstallation_list") 88 | # Can be composed directly with reverse(query=) in Django 5.2, see https://code.djangoproject.com/ticket/25582 89 | return format_html(f"{count}") if count else "0" 90 | 91 | 92 | class SoftwareProductInstallation(NetBoxModel): 93 | comments = models.TextField(blank=True) 94 | 95 | device = models.ForeignKey(to="dcim.Device", on_delete=models.PROTECT, null=True, blank=True) 96 | virtualmachine = models.ForeignKey( 97 | to="virtualization.VirtualMachine", on_delete=models.PROTECT, null=True, blank=True 98 | ) 99 | cluster = models.ForeignKey(to="virtualization.Cluster", on_delete=models.PROTECT, null=True, blank=True) 100 | software_product = models.ForeignKey(to="netbox_slm.SoftwareProduct", on_delete=models.PROTECT) 101 | version = models.ForeignKey(to="netbox_slm.SoftwareProductVersion", on_delete=models.PROTECT) 102 | 103 | objects = RestrictedQuerySet.as_manager() 104 | 105 | def __str__(self): 106 | return f"{self.pk} ({self.platform})" 107 | 108 | class Meta: 109 | constraints = [ 110 | models.CheckConstraint( 111 | name="%(app_label)s_%(class)s_platform", 112 | check=( 113 | models.Q(device__isnull=False, virtualmachine__isnull=True, cluster__isnull=True) 114 | | models.Q(device__isnull=True, virtualmachine__isnull=False, cluster__isnull=True) 115 | | models.Q(device__isnull=True, virtualmachine__isnull=True, cluster__isnull=False) 116 | ), 117 | violation_error_message="Installation requires exactly one platform destination.", 118 | ) 119 | ] 120 | 121 | def get_absolute_url(self): 122 | return reverse("plugins:netbox_slm:softwareproductinstallation", kwargs={"pk": self.pk}) 123 | 124 | @property 125 | def platform(self): 126 | return self.device or self.virtualmachine or self.cluster 127 | 128 | def render_type(self): 129 | if self.device: 130 | return "device" 131 | if self.virtualmachine: 132 | return "virtualmachine" 133 | return "cluster" 134 | 135 | 136 | def spdx_license_names(): 137 | names = [(item[0], item[0]) for item in spdx_licensing.known_symbols.items() if not item[1].is_exception] 138 | names.sort() 139 | names.insert(0, ("", "---------")) # set default value 140 | return names 141 | 142 | 143 | def validate_spdx_expression(value): 144 | expression_info = spdx_licensing.validate(value) 145 | if expression_info.errors: 146 | raise ValidationError(f"{value} is not a known SPDX license expression") 147 | 148 | 149 | class SoftwareLicense(NetBoxModel): 150 | name = models.CharField(max_length=128) 151 | comments = models.TextField(blank=True) 152 | 153 | description = models.CharField(max_length=255, null=True, blank=True) 154 | type = models.CharField(max_length=128) 155 | spdx_expression = models.CharField(max_length=64, null=True, blank=True, validators=[validate_spdx_expression]) 156 | stored_location = models.CharField(max_length=255, null=True, blank=True) 157 | stored_location_url = LaxURLField(max_length=1024, null=True, blank=True) 158 | start_date = models.DateField(null=True, blank=True) 159 | expiration_date = models.DateField(null=True, blank=True) 160 | support = models.BooleanField(default=None, null=True, blank=True) 161 | license_amount = models.PositiveIntegerField(default=None, null=True, blank=True) 162 | 163 | software_product = models.ForeignKey(to="netbox_slm.SoftwareProduct", on_delete=models.PROTECT) 164 | version = models.ForeignKey(to="netbox_slm.SoftwareProductVersion", on_delete=models.PROTECT, null=True, blank=True) 165 | installation = models.ForeignKey( 166 | to="netbox_slm.SoftwareProductInstallation", on_delete=models.SET_NULL, null=True, blank=True 167 | ) 168 | 169 | objects = RestrictedQuerySet.as_manager() 170 | 171 | def __str__(self): 172 | return self.name 173 | 174 | def get_absolute_url(self): 175 | return reverse("plugins:netbox_slm:softwarelicense", kwargs={"pk": self.pk}) 176 | 177 | @property 178 | def stored_location_txt(self): 179 | if self.stored_location_url and not self.stored_location: 180 | return "Link" 181 | return self.stored_location 182 | -------------------------------------------------------------------------------- /netbox_slm/navigation.py: -------------------------------------------------------------------------------- 1 | from netbox.plugins import PluginMenuButton, PluginMenuItem, PluginMenu, get_plugin_config 2 | from . import SLMConfig 3 | 4 | slm_items = ( 5 | PluginMenuItem( 6 | link="plugins:netbox_slm:softwareproduct_list", 7 | link_text="Software Products", 8 | permissions=["netbox_slm.add_softwareproduct"], 9 | buttons=( 10 | PluginMenuButton( 11 | "plugins:netbox_slm:softwareproduct_add", 12 | "Add", 13 | "mdi mdi-plus-thick", 14 | permissions=["netbox_slm.add_softwareproduct"], 15 | ), 16 | PluginMenuButton( 17 | "plugins:netbox_slm:softwareproduct_bulk_import", 18 | "Import", 19 | "mdi mdi-upload", 20 | permissions=["netbox_slm.import_softwareproduct"], 21 | ), 22 | ), 23 | ), 24 | PluginMenuItem( 25 | link="plugins:netbox_slm:softwareproductversion_list", 26 | link_text="Versions", 27 | permissions=["netbox_slm.add_softwareproductversion"], 28 | buttons=( 29 | PluginMenuButton( 30 | "plugins:netbox_slm:softwareproductversion_add", 31 | "Add", 32 | "mdi mdi-plus-thick", 33 | permissions=["netbox_slm.add_softwareproductversion"], 34 | ), 35 | PluginMenuButton( 36 | "plugins:netbox_slm:softwareproductversion_bulk_import", 37 | "Import", 38 | "mdi mdi-upload", 39 | permissions=["netbox_slm.import_softwareproductversion"], 40 | ), 41 | ), 42 | ), 43 | PluginMenuItem( 44 | link="plugins:netbox_slm:softwareproductinstallation_list", 45 | link_text="Installations", 46 | permissions=["netbox_slm.add_softwareproductinstallation"], 47 | buttons=( 48 | PluginMenuButton( 49 | "plugins:netbox_slm:softwareproductinstallation_add", 50 | "Add", 51 | "mdi mdi-plus-thick", 52 | permissions=["netbox_slm.add_softwareproductinstallation"], 53 | ), 54 | PluginMenuButton( 55 | "plugins:netbox_slm:softwareproductinstallation_bulk_import", 56 | "Import", 57 | "mdi mdi-upload", 58 | permissions=["netbox_slm.import_softwareproductinstallation"], 59 | ), 60 | ), 61 | ), 62 | PluginMenuItem( 63 | link="plugins:netbox_slm:softwarelicense_list", 64 | link_text="Licenses", 65 | permissions=["netbox_slm.add_softwarelicense"], 66 | buttons=( 67 | PluginMenuButton( 68 | "plugins:netbox_slm:softwarelicense_add", 69 | "Add", 70 | "mdi mdi-plus-thick", 71 | permissions=["netbox_slm.add_softwarelicense"], 72 | ), 73 | PluginMenuButton( 74 | "plugins:netbox_slm:softwarelicense_bulk_import", 75 | "Import", 76 | "mdi mdi-upload", 77 | permissions=["netbox_slm.import_softwarelicense"], 78 | ), 79 | ), 80 | ), 81 | ) 82 | 83 | if get_plugin_config("netbox_slm", "top_level_menu"): 84 | menu = PluginMenu( 85 | label="Software Lifecycle", 86 | groups=((SLMConfig.verbose_name, slm_items),), 87 | icon_class="mdi mdi-content-save", 88 | ) 89 | else: 90 | # auto imported by default PluginConfig.menu_items = navigation.menu_items 91 | menu_items = slm_items 92 | -------------------------------------------------------------------------------- /netbox_slm/tables.py: -------------------------------------------------------------------------------- 1 | import django_tables2 as tables 2 | from django.db.models import Count, F, Value 3 | 4 | from netbox.tables import NetBoxTable, ToggleColumn, columns 5 | from netbox_slm.models import SoftwareProduct, SoftwareProductVersion, SoftwareProductInstallation, SoftwareLicense 6 | 7 | 8 | class SoftwareProductTable(NetBoxTable): 9 | """Table for displaying SoftwareProduct objects.""" 10 | 11 | pk = ToggleColumn() 12 | name = tables.LinkColumn() 13 | manufacturer = tables.Column(accessor="manufacturer", linkify=True) 14 | installations = tables.Column(accessor="get_installation_count") 15 | 16 | tags = columns.TagColumn(url_name="plugins:netbox_slm:softwareproduct_list") 17 | 18 | class Meta(NetBoxTable.Meta): 19 | model = SoftwareProduct 20 | fields = ( 21 | "pk", 22 | "name", 23 | "description", 24 | "manufacturer", 25 | "installations", 26 | "tags", 27 | ) 28 | default_columns = ( 29 | "pk", 30 | "name", 31 | "manufacturer", 32 | "installations", 33 | "tags", 34 | ) 35 | 36 | def order_installations(self, queryset, is_descending): 37 | queryset = queryset.annotate(count=Count("softwareproductinstallation__id")).order_by( 38 | ("-" if is_descending else "") + "count" 39 | ) 40 | return queryset, True 41 | 42 | 43 | class SoftwareProductVersionTable(NetBoxTable): 44 | """Table for displaying SoftwareProductVersion objects.""" 45 | 46 | pk = ToggleColumn() 47 | name = tables.LinkColumn() 48 | software_product = tables.Column(accessor="software_product", verbose_name="Software Product", linkify=True) 49 | manufacturer = tables.Column(accessor="software_product__manufacturer", linkify=True) 50 | installations = tables.Column(accessor="get_installation_count") 51 | 52 | tags = columns.TagColumn(url_name="plugins:netbox_slm:softwareproductversion_list") 53 | 54 | class Meta(NetBoxTable.Meta): 55 | model = SoftwareProductVersion 56 | fields = ( 57 | "pk", 58 | "name", 59 | "description", 60 | "software_product", 61 | "manufacturer", 62 | "release_date", 63 | "end_of_support", 64 | "release_type", 65 | "installations", 66 | "tags", 67 | ) 68 | default_columns = ( 69 | "pk", 70 | "name", 71 | "manufacturer", 72 | "software_product", 73 | "release_date", 74 | "installations", 75 | "tags", 76 | ) 77 | 78 | def order_installations(self, queryset, is_descending): 79 | queryset = queryset.annotate(count=Count("softwareproductinstallation__id")).order_by( 80 | ("-" if is_descending else "") + "count" 81 | ) 82 | return queryset, True 83 | 84 | 85 | class SoftwareProductInstallationTable(NetBoxTable): 86 | """Table for displaying SoftwareProductInstallation objects.""" 87 | 88 | pk = ToggleColumn() 89 | platform = tables.Column(accessor="platform", linkify=True) 90 | type = tables.Column(accessor="render_type") 91 | manufacturer = tables.Column(accessor="software_product__manufacturer", linkify=True) 92 | software_product = tables.Column(accessor="software_product", verbose_name="Software Product", linkify=True) 93 | version = tables.Column(accessor="version", linkify=True) 94 | 95 | tags = columns.TagColumn(url_name="plugins:netbox_slm:softwareproductinstallation_list") 96 | 97 | class Meta(NetBoxTable.Meta): 98 | model = SoftwareProductInstallation 99 | fields = ( 100 | "pk", 101 | "platform", 102 | "type", 103 | "manufacturer", 104 | "software_product", 105 | "version", 106 | "tags", 107 | ) 108 | default_columns = ( 109 | "pk", 110 | "platform", 111 | "type", 112 | "manufacturer", 113 | "software_product", 114 | "version", 115 | "tags", 116 | ) 117 | 118 | def order_platform(self, queryset, is_descending): 119 | device_annotate = queryset.filter(device__isnull=False).annotate(platform_value=F("device__name")) 120 | vm_annotate = queryset.filter(virtualmachine__isnull=False).annotate(platform_value=F("virtualmachine__name")) 121 | cluster_annotate = queryset.filter(cluster__isnull=False).annotate(platform_value=F("cluster__name")) 122 | queryset_union = device_annotate.union(vm_annotate).union(cluster_annotate) 123 | return queryset_union.order_by(f"{'-' if is_descending else ''}platform_value"), True 124 | 125 | def order_type(self, queryset, is_descending): 126 | device_annotate = queryset.filter(device__isnull=False).annotate(render_type=Value("device")) 127 | vm_annotate = queryset.filter(virtualmachine__isnull=False).annotate(render_type=Value("virtualmachine")) 128 | cluster_annotate = queryset.filter(cluster__isnull=False).annotate(render_type=Value("cluster")) 129 | queryset_union = device_annotate.union(vm_annotate).union(cluster_annotate) 130 | return queryset_union.order_by(f"{'-' if is_descending else ''}render_type"), True 131 | 132 | 133 | class SoftwareLicenseTable(NetBoxTable): 134 | """Table for displaying SoftwareLicense objects.""" 135 | 136 | pk = ToggleColumn() 137 | name = tables.LinkColumn() 138 | 139 | type = tables.Column() 140 | spdx_expression = tables.Column(verbose_name="SPDX expression") 141 | stored_location = tables.Column(accessor="stored_location_txt", linkify=lambda record: record.stored_location_url) 142 | 143 | manufacturer = tables.Column(accessor="software_product__manufacturer", linkify=True) 144 | software_product = tables.Column(accessor="software_product", verbose_name="Software Product", linkify=True) 145 | version = tables.Column(accessor="version", linkify=True) 146 | installation = tables.Column(accessor="installation", linkify=True) 147 | 148 | tags = columns.TagColumn(url_name="plugins:netbox_slm:softwarelicense_list") 149 | 150 | class Meta(NetBoxTable.Meta): 151 | model = SoftwareLicense 152 | fields = ( 153 | "pk", 154 | "name", 155 | "description", 156 | "type", 157 | "spdx_expression", 158 | "stored_location", 159 | "start_date", 160 | "expiration_date", 161 | "manufacturer", 162 | "software_product", 163 | "version", 164 | "installation", 165 | "support", 166 | "license_amount", 167 | "tags", 168 | ) 169 | default_columns = ( 170 | "pk", 171 | "name", 172 | "type", 173 | "manufacturer", 174 | "software_product", 175 | "installation", 176 | "expiration_date", 177 | "tags", 178 | ) 179 | 180 | def render_installation(self, **kwargs): 181 | return f"{kwargs['record'].installation.platform}" 182 | -------------------------------------------------------------------------------- /netbox_slm/template_content.py: -------------------------------------------------------------------------------- 1 | from netbox.plugins import PluginTemplateExtension, get_plugin_config 2 | 3 | installations = dict( 4 | device=get_plugin_config("netbox_slm", "link_device_installations"), 5 | cluster=get_plugin_config("netbox_slm", "link_cluster_installations"), 6 | virtualmachine=get_plugin_config("netbox_slm", "link_virtualmachine_installations"), 7 | ) 8 | 9 | 10 | class InstallationsCard(PluginTemplateExtension): 11 | models = ["dcim.device", "virtualization.cluster", "virtualization.virtualmachine"] 12 | 13 | def get_object_name(self): 14 | object_model_meta = self.context["object"]._meta # one of the self.models defined above 15 | return object_model_meta.model_name 16 | 17 | def render_card(self): 18 | return self.render( 19 | "netbox_slm/installations_card_include.html", 20 | extra_context={ 21 | "search_obj": self.get_object_name(), 22 | }, 23 | ) 24 | 25 | def left_page(self): 26 | return self.render_card() if installations[self.get_object_name()] == "left" else "" 27 | 28 | def right_page(self): 29 | return self.render_card() if installations[self.get_object_name()] == "right" else "" 30 | 31 | def full_width_page(self): 32 | return self.render_card() if installations[self.get_object_name()] == "full" else "" 33 | 34 | 35 | # auto imported by default PluginConfig.template_extensions = template_content.template_extensions 36 | template_extensions = [InstallationsCard] 37 | -------------------------------------------------------------------------------- /netbox_slm/templates/netbox_slm/installations_card_include.html: -------------------------------------------------------------------------------- 1 | {% load helpers %} 2 | 3 |
4 |

5 | Installations 6 | {% if perms.netbox_slm.add_softwareproductinstallation %} 7 | 12 | {% endif %} 13 |

14 | {% if search_obj == "cluster" %} 15 | {% htmx_table "plugins:netbox_slm:softwareproductinstallation_list" cluster_id=object.pk %} 16 | {% elif search_obj == "device" %} 17 | {% htmx_table "plugins:netbox_slm:softwareproductinstallation_list" device_id=object.pk %} 18 | {% elif search_obj == "virtualmachine" %} 19 | {% htmx_table "plugins:netbox_slm:softwareproductinstallation_list" virtualmachine_id=object.pk %} 20 | {% endif %} 21 |
22 | -------------------------------------------------------------------------------- /netbox_slm/templates/netbox_slm/softwarelicense.html: -------------------------------------------------------------------------------- 1 | {% extends 'generic/object.html' %} 2 | {% load buttons %} 3 | {% load static %} 4 | {% load helpers %} 5 | {% load plugins %} 6 | 7 | {% block content %} 8 |
9 |
10 |
11 |
12 | Software License 13 |
14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | {% if object.stored_location_url %} 46 | 47 | {% else %} 48 | 49 | {% endif %} 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 |
Name{{ object.name }}
Description{{ object.description }}
Type{{ object.type }}
SPDX expression{{ object.spdx_expression }}
Software Product{{ object.software_product|linkify }}
Version{{ object.version|linkify }}
Installation{{ object.installation|linkify }}
Stored location{{ object.stored_location_txt }}{{ object.stored_location_txt }}
Start date{{ object.start_date }}
Expiration date{{ object.expiration_date }}
Support{{ object.support }}
License amount{{ object.license_amount }}
68 |
69 | {% include 'inc/panels/custom_fields.html' %} 70 | {% include 'inc/panels/tags.html' %} 71 | {% include 'inc/panels/comments.html' %} 72 | {% plugin_left_page object %} 73 |
74 |
75 |
76 |
77 | {% plugin_full_width_page object %} 78 |
79 |
80 | {% endblock %} 81 | -------------------------------------------------------------------------------- /netbox_slm/templates/netbox_slm/softwareproduct.html: -------------------------------------------------------------------------------- 1 | {% extends 'generic/object.html' %} 2 | {% load buttons %} 3 | {% load static %} 4 | {% load helpers %} 5 | {% load plugins %} 6 | 7 | {% block content %} 8 |
9 |
10 |
11 |
12 | Software Product 13 |
14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 38 | 39 | 40 | 41 | 50 | 51 | 52 | 53 | 62 | 63 |
Name{{ object.name }}
Description{{ object.description }}
Manufacturer{{ object.manufacturer|linkify }}
Versions 30 | {% for version in object.softwareproductversion_set.all %} 31 | 32 | {{ version }} 33 | 34 | {% empty %} 35 | None 36 | {% endfor %} 37 |
Installations 42 | {% for installation in object.softwareproductinstallation_set.all %} 43 | 44 | {{ installation }} 45 | 46 | {% empty %} 47 | None 48 | {% endfor %} 49 |
Licenses 54 | {% for license in object.softwarelicense_set.all %} 55 | 56 | {{ license }} 57 | 58 | {% empty %} 59 | None 60 | {% endfor %} 61 |
64 |
65 | {% include 'inc/panels/custom_fields.html' %} 66 | {% include 'inc/panels/tags.html' %} 67 | {% include 'inc/panels/comments.html' %} 68 | {% plugin_left_page object %} 69 |
70 |
71 |
72 |
73 | {% plugin_full_width_page object %} 74 |
75 |
76 | {% endblock %} 77 | -------------------------------------------------------------------------------- /netbox_slm/templates/netbox_slm/softwareproductinstallation.html: -------------------------------------------------------------------------------- 1 | {% extends 'generic/object.html' %} 2 | {% load buttons %} 3 | {% load static %} 4 | {% load helpers %} 5 | {% load plugins %} 6 | 7 | {% block content %} 8 |
9 |
10 |
11 |
12 | Software Product Installation 13 |
14 | 15 | {% if object.device %} 16 | 17 | 18 | 19 | 20 | {% elif object.virtualmachine %} 21 | 22 | 23 | 24 | 25 | {% else %} 26 | 27 | 28 | 29 | 30 | {% endif %} 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 50 | 51 |
Device{{ object.device|linkify }}
Virtual Machine{{ object.virtualmachine|linkify }}
Cluster{{ object.cluster|linkify }}
Software Product{{ object.software_product|linkify }}
Version{{ object.version|linkify }}
Licenses 42 | {% for license in object.softwarelicense_set.all %} 43 | 44 | {{ license }} 45 | 46 | {% empty %} 47 | None 48 | {% endfor %} 49 |
52 |
53 | {% include 'inc/panels/custom_fields.html' %} 54 | {% include 'inc/panels/tags.html' %} 55 | {% include 'inc/panels/comments.html' %} 56 | {% plugin_left_page object %} 57 |
58 |
59 |
60 |
61 | {% plugin_full_width_page object %} 62 |
63 |
64 | {% endblock %} 65 | -------------------------------------------------------------------------------- /netbox_slm/templates/netbox_slm/softwareproductversion.html: -------------------------------------------------------------------------------- 1 | {% extends 'generic/object.html' %} 2 | {% load buttons %} 3 | {% load static %} 4 | {% load helpers %} 5 | {% load plugins %} 6 | 7 | {% block content %} 8 |
9 |
10 |
11 |
12 | Software Product Version 13 |
14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | {% if object.documentation_url %} 38 | 39 | {% else %} 40 | 41 | {% endif %} 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | {% if object.file_link %} 50 | 51 | {% else %} 52 | 53 | {% endif %} 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 74 | 75 | 76 | 77 | 86 | 87 |
Name{{ object.name }}
Description{{ object.description }}
Manufacturer{{ object.software_product.manufacturer|linkify }}
Software Product{{ object.software_product|linkify }}
Release date{{ object.release_date }}
Documentation url{{ object.documentation_url }}{{ object.documentation_url }}
End of support{{ object.end_of_support }}
Filename{{ object.filename }}{{ object.filename }}
File checksum{{ object.file_checksum }}
Release type{{ object.get_release_type_display }}
Installations 66 | {% for installation in object.softwareproductinstallation_set.all %} 67 | 68 | {{ installation }} 69 | 70 | {% empty %} 71 | None 72 | {% endfor %} 73 |
Licenses 78 | {% for license in object.softwarelicense_set.all %} 79 | 80 | {{ license }} 81 | 82 | {% empty %} 83 | None 84 | {% endfor %} 85 |
88 |
89 | {% include 'inc/panels/custom_fields.html' %} 90 | {% include 'inc/panels/tags.html' %} 91 | {% include 'inc/panels/comments.html' %} 92 | {% plugin_left_page object %} 93 |
94 |
95 |
96 |
97 | {% plugin_full_width_page object %} 98 |
99 |
100 | {% endblock %} 101 | -------------------------------------------------------------------------------- /netbox_slm/templatetags/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ICTU/netbox_slm/9e87afcde7780b4b92af9cb7e6e5ff3f7cbbf13f/netbox_slm/templatetags/__init__.py -------------------------------------------------------------------------------- /netbox_slm/tests/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ICTU/netbox_slm/9e87afcde7780b4b92af9cb7e6e5ff3f7cbbf13f/netbox_slm/tests/__init__.py -------------------------------------------------------------------------------- /netbox_slm/tests/base.py: -------------------------------------------------------------------------------- 1 | from django.test import TestCase 2 | 3 | from dcim.models import Device, DeviceRole, DeviceType, Manufacturer, Site 4 | from netbox_slm.models import SoftwareProduct, SoftwareProductVersion, SoftwareProductInstallation, SoftwareLicense 5 | from virtualization.models import Cluster, ClusterType, VirtualMachine 6 | 7 | 8 | class SlmBaseTestCase(TestCase): 9 | p_name: str = "test product" 10 | v_name: str = "test version" 11 | l_name: str = "test license" 12 | test_url: str = "https://github.com/ICTU/netbox_slm" 13 | vm: VirtualMachine 14 | software_product: SoftwareProduct 15 | software_product_version: SoftwareProductVersion 16 | software_product_installation: SoftwareProductInstallation 17 | 18 | @classmethod 19 | def setUpClass(cls): 20 | super().setUpClass() 21 | manufacturer = Manufacturer.objects.create(name="test manufacturer") 22 | device_type = DeviceType.objects.create(model="test device type", manufacturer=manufacturer) 23 | device_role = DeviceRole.objects.create(name="test device role") 24 | site = Site.objects.create(name="test site") 25 | cls.device = Device.objects.create(name="test device", device_type=device_type, role=device_role, site=site) 26 | cls.vm = VirtualMachine.objects.create(name="test VM") 27 | cluster_type = ClusterType.objects.create(name="test cluster type") 28 | cls.cluster = Cluster.objects.create(name="test cluster", type=cluster_type) 29 | 30 | cls.software_product = SoftwareProduct.objects.create(name=cls.p_name) 31 | cls.software_product_version = SoftwareProductVersion.objects.create( 32 | name=cls.v_name, software_product=cls.software_product 33 | ) 34 | cls.software_product_installation = SoftwareProductInstallation.objects.create( 35 | virtualmachine=cls.vm, software_product=cls.software_product, version=cls.software_product_version 36 | ) 37 | cls.software_license = SoftwareLicense.objects.create( 38 | name=cls.l_name, 39 | software_product=cls.software_product, 40 | version=cls.software_product_version, 41 | installation=cls.software_product_installation, 42 | stored_location_url=cls.test_url, 43 | ) 44 | 45 | @classmethod 46 | def tearDownClass(cls): 47 | SoftwareLicense.objects.all().delete() 48 | SoftwareProductInstallation.objects.all().delete() 49 | SoftwareProductVersion.objects.all().delete() 50 | SoftwareProduct.objects.all().delete() 51 | Cluster.objects.all().delete() 52 | ClusterType.objects.all().delete() 53 | VirtualMachine.objects.all().delete() 54 | Device.objects.all().delete() 55 | Site.objects.all().delete() 56 | DeviceRole.objects.all().delete() 57 | DeviceType.objects.all().delete() 58 | Manufacturer.objects.all().delete() 59 | super().tearDownClass() 60 | -------------------------------------------------------------------------------- /netbox_slm/tests/test_functional.py: -------------------------------------------------------------------------------- 1 | from unittest.mock import patch 2 | 3 | from django.contrib.auth import get_user_model 4 | 5 | from .base import SlmBaseTestCase 6 | 7 | 8 | class FunctionalTestCase(SlmBaseTestCase): 9 | """Functional test cases that require a client and user with permissions""" 10 | 11 | def setUp(self): 12 | test_user, _ = get_user_model().objects.get_or_create(username="test", is_superuser=True) 13 | self.client.force_login(test_user) 14 | 15 | @classmethod 16 | def tearDownClass(cls): 17 | get_user_model().objects.all().delete() 18 | super().tearDownClass() 19 | 20 | def test_template_content(self): 21 | cluster_response = self.client.get(f"/virtualization/clusters/{self.cluster.pk}/") 22 | self.assertContains(cluster_response, f' Add an installation") 24 | 25 | device_response = self.client.get(f"/dcim/devices/{self.device.pk}/") 26 | self.assertContains(device_response, f' Add an installation") 28 | 29 | vm_response = self.client.get(f"/virtualization/virtual-machines/{self.vm.pk}/") 30 | self.assertTemplateUsed(vm_response, "netbox_slm/installations_card_include.html") 31 | self.assertContains(vm_response, f' Add an installation") 45 | 46 | def test_setting_full_content(self): 47 | # self.settings(PLUGINS_CONFIG=dict(netbox_slm=dict(link_cluster_installations="full"))) 48 | with patch.dict("netbox_slm.template_content.installations", {"cluster": "full"}): 49 | cluster_response = self.client.get(f"/virtualization/clusters/{self.cluster.pk}/") 50 | self.assertContains(cluster_response, f' Add an installation") 52 | -------------------------------------------------------------------------------- /netbox_slm/tests/test_models.py: -------------------------------------------------------------------------------- 1 | from .base import SlmBaseTestCase 2 | 3 | 4 | class ModelTestCase(SlmBaseTestCase): 5 | """Test basic model functionality and custom overrides""" 6 | 7 | def test_model_name(self): 8 | self.assertEqual(self.p_name, str(self.software_product)) 9 | self.assertEqual(self.v_name, str(self.software_product_version)) 10 | self.assertTrue(str(self.software_product_installation)[0].isdigit()) # starts with PK 11 | self.assertEqual(self.l_name, str(self.software_license)) 12 | 13 | def test_absolute_url(self): 14 | self.assertEqual( 15 | f"/plugins/slm/software-products/{self.software_product.pk}/", self.software_product.get_absolute_url() 16 | ) 17 | self.assertEqual( 18 | f"/plugins/slm/versions/{self.software_product_version.pk}/", 19 | self.software_product_version.get_absolute_url(), 20 | ) 21 | self.assertEqual( 22 | f"/plugins/slm/installations/{self.software_product_installation.pk}/", 23 | self.software_product_installation.get_absolute_url(), 24 | ) 25 | self.assertEqual(f"/plugins/slm/licenses/{self.software_license.pk}/", self.software_license.get_absolute_url()) 26 | 27 | def test_get_installation_count(self): 28 | self.assertEqual( 29 | f"1", 30 | self.software_product.get_installation_count(), 31 | ) 32 | self.assertEqual( 33 | f"1", 34 | self.software_product_version.get_installation_count(), 35 | ) 36 | 37 | def test_product_installation_methods(self): 38 | self.assertEqual("virtualmachine", self.software_product_installation.render_type()) 39 | self.assertEqual(self.vm, self.software_product_installation.platform) 40 | 41 | self.software_product_installation.virtualmachine = None 42 | self.software_product_installation.device = self.device 43 | self.software_product_installation.save() 44 | self.assertEqual("device", self.software_product_installation.render_type()) 45 | self.assertEqual(self.device, self.software_product_installation.platform) 46 | 47 | self.software_product_installation.device = None 48 | self.software_product_installation.cluster = self.cluster 49 | self.software_product_installation.save() 50 | self.assertEqual("cluster", self.software_product_installation.render_type()) 51 | self.assertEqual(self.cluster, self.software_product_installation.platform) 52 | 53 | def test_software_license_stored_location_txt(self): 54 | self.assertEqual("Link", self.software_license.stored_location_txt) 55 | 56 | self.software_license.stored_location = "GitHub" 57 | self.software_license.save() 58 | self.assertEqual("GitHub", self.software_license.stored_location_txt) 59 | -------------------------------------------------------------------------------- /netbox_slm/urls.py: -------------------------------------------------------------------------------- 1 | from django.urls import path, include 2 | 3 | from netbox_slm import views # noqa F401 4 | from utilities.urls import get_model_urls 5 | 6 | urlpatterns = [ 7 | path("software-products/", include(get_model_urls("netbox_slm", "softwareproduct", detail=False))), 8 | path("software-products//", include(get_model_urls("netbox_slm", "softwareproduct"))), 9 | path("versions/", include(get_model_urls("netbox_slm", "softwareproductversion", detail=False))), 10 | path("versions//", include(get_model_urls("netbox_slm", "softwareproductversion"))), 11 | path("installations/", include(get_model_urls("netbox_slm", "softwareproductinstallation", detail=False))), 12 | path("installations//", include(get_model_urls("netbox_slm", "softwareproductinstallation"))), 13 | path("licenses/", include(get_model_urls("netbox_slm", "softwarelicense", detail=False))), 14 | path("licenses//", include(get_model_urls("netbox_slm", "softwarelicense"))), 15 | ] 16 | -------------------------------------------------------------------------------- /netbox_slm/views/__init__.py: -------------------------------------------------------------------------------- 1 | from .software_license import ( 2 | SoftwareLicenseListView, 3 | SoftwareLicenseView, 4 | SoftwareLicenseEditView, 5 | SoftwareLicenseDeleteView, 6 | SoftwareLicenseBulkDeleteView, 7 | SoftwareLicenseBulkImportView, 8 | ) 9 | from .software_product import ( 10 | SoftwareProductListView, 11 | SoftwareProductView, 12 | SoftwareProductEditView, 13 | SoftwareProductDeleteView, 14 | SoftwareProductBulkDeleteView, 15 | SoftwareProductBulkImportView, 16 | ) 17 | from .software_product_installation import ( 18 | SoftwareProductInstallationListView, 19 | SoftwareProductInstallationView, 20 | SoftwareProductInstallationEditView, 21 | SoftwareProductInstallationDeleteView, 22 | SoftwareProductInstallationBulkDeleteView, 23 | SoftwareProductInstallationBulkImportView, 24 | ) 25 | from .software_product_version import ( 26 | SoftwareProductVersionListView, 27 | SoftwareProductVersionView, 28 | SoftwareProductVersionEditView, 29 | SoftwareProductVersionDeleteView, 30 | SoftwareProductVersionBulkDeleteView, 31 | SoftwareProductVersionBulkImportView, 32 | ) 33 | -------------------------------------------------------------------------------- /netbox_slm/views/software_license.py: -------------------------------------------------------------------------------- 1 | from netbox.views import generic 2 | from netbox_slm import filtersets, forms, tables 3 | from netbox_slm.models import SoftwareLicense 4 | from utilities.views import register_model_view 5 | 6 | 7 | @register_model_view(SoftwareLicense, "list", path="", detail=False) 8 | class SoftwareLicenseListView(generic.ObjectListView): 9 | queryset = SoftwareLicense.objects.all() 10 | filterset = filtersets.SoftwareLicenseFilterSet 11 | filterset_form = forms.SoftwareLicenseFilterForm 12 | table = tables.SoftwareLicenseTable 13 | 14 | 15 | @register_model_view(SoftwareLicense) 16 | class SoftwareLicenseView(generic.ObjectView): 17 | queryset = SoftwareLicense.objects.all() 18 | 19 | 20 | @register_model_view(SoftwareLicense, "add", detail=False) 21 | @register_model_view(SoftwareLicense, "edit") 22 | class SoftwareLicenseEditView(generic.ObjectEditView): 23 | queryset = SoftwareLicense.objects.all() 24 | form = forms.SoftwareLicenseForm 25 | 26 | 27 | @register_model_view(SoftwareLicense, "delete") 28 | class SoftwareLicenseDeleteView(generic.ObjectDeleteView): 29 | queryset = SoftwareLicense.objects.all() 30 | 31 | 32 | @register_model_view(SoftwareLicense, "bulk_import", detail=False) 33 | class SoftwareLicenseBulkImportView(generic.BulkImportView): 34 | queryset = SoftwareLicense.objects.all() 35 | model_form = forms.SoftwareLicenseBulkImportForm 36 | 37 | 38 | @register_model_view(SoftwareLicense, "bulk_edit", path="edit", detail=False) 39 | class SoftwareLicenseBulkEditView(generic.BulkEditView): 40 | queryset = SoftwareLicense.objects.all() 41 | filterset = filtersets.SoftwareLicenseFilterSet 42 | table = tables.SoftwareLicenseTable 43 | form = forms.SoftwareLicenseBulkEditForm 44 | 45 | 46 | @register_model_view(SoftwareLicense, "bulk_delete", path="delete", detail=False) 47 | class SoftwareLicenseBulkDeleteView(generic.BulkDeleteView): 48 | queryset = SoftwareLicense.objects.all() 49 | filterset = filtersets.SoftwareLicenseFilterSet 50 | table = tables.SoftwareLicenseTable 51 | -------------------------------------------------------------------------------- /netbox_slm/views/software_product.py: -------------------------------------------------------------------------------- 1 | from netbox.views import generic 2 | from netbox_slm import filtersets, forms, tables 3 | from netbox_slm.models import SoftwareProduct 4 | from utilities.views import register_model_view 5 | 6 | 7 | @register_model_view(SoftwareProduct, "list", path="", detail=False) 8 | class SoftwareProductListView(generic.ObjectListView): 9 | queryset = SoftwareProduct.objects.all() 10 | filterset = filtersets.SoftwareProductFilterSet 11 | filterset_form = forms.SoftwareProductFilterForm 12 | table = tables.SoftwareProductTable 13 | 14 | 15 | @register_model_view(SoftwareProduct) 16 | class SoftwareProductView(generic.ObjectView): 17 | queryset = SoftwareProduct.objects.all() 18 | 19 | def get_extra_context(self, request, instance): 20 | versions = instance.softwareproductversion_set.all() 21 | return {"versions": versions} 22 | 23 | 24 | @register_model_view(SoftwareProduct, "add", detail=False) 25 | @register_model_view(SoftwareProduct, "edit") 26 | class SoftwareProductEditView(generic.ObjectEditView): 27 | queryset = SoftwareProduct.objects.all() 28 | form = forms.SoftwareProductForm 29 | 30 | 31 | @register_model_view(SoftwareProduct, "delete") 32 | class SoftwareProductDeleteView(generic.ObjectDeleteView): 33 | queryset = SoftwareProduct.objects.all() 34 | 35 | 36 | @register_model_view(SoftwareProduct, "bulk_import", detail=False) 37 | class SoftwareProductBulkImportView(generic.BulkImportView): 38 | queryset = SoftwareProduct.objects.all() 39 | model_form = forms.SoftwareProductBulkImportForm 40 | 41 | 42 | @register_model_view(SoftwareProduct, "bulk_edit", path="edit", detail=False) 43 | class SoftwareProductBulkEditView(generic.BulkEditView): 44 | queryset = SoftwareProduct.objects.all() 45 | filterset = filtersets.SoftwareProductFilterSet 46 | table = tables.SoftwareProductTable 47 | form = forms.SoftwareProductBulkEditForm 48 | 49 | 50 | @register_model_view(SoftwareProduct, "bulk_delete", path="delete", detail=False) 51 | class SoftwareProductBulkDeleteView(generic.BulkDeleteView): 52 | queryset = SoftwareProduct.objects.all() 53 | filterset = filtersets.SoftwareProductFilterSet 54 | table = tables.SoftwareProductTable 55 | -------------------------------------------------------------------------------- /netbox_slm/views/software_product_installation.py: -------------------------------------------------------------------------------- 1 | from netbox.views import generic 2 | from netbox_slm import filtersets, forms, tables 3 | from netbox_slm.models import SoftwareProductInstallation 4 | from utilities.views import register_model_view 5 | 6 | 7 | @register_model_view(SoftwareProductInstallation, "list", path="", detail=False) 8 | class SoftwareProductInstallationListView(generic.ObjectListView): 9 | queryset = SoftwareProductInstallation.objects.all() 10 | filterset = filtersets.SoftwareProductInstallationFilterSet 11 | filterset_form = forms.SoftwareProductInstallationFilterForm 12 | table = tables.SoftwareProductInstallationTable 13 | 14 | 15 | @register_model_view(SoftwareProductInstallation) 16 | class SoftwareProductInstallationView(generic.ObjectView): 17 | queryset = SoftwareProductInstallation.objects.all() 18 | 19 | 20 | @register_model_view(SoftwareProductInstallation, "add", detail=False) 21 | @register_model_view(SoftwareProductInstallation, "edit") 22 | class SoftwareProductInstallationEditView(generic.ObjectEditView): 23 | queryset = SoftwareProductInstallation.objects.all() 24 | form = forms.SoftwareProductInstallationForm 25 | 26 | 27 | @register_model_view(SoftwareProductInstallation, "delete") 28 | class SoftwareProductInstallationDeleteView(generic.ObjectDeleteView): 29 | queryset = SoftwareProductInstallation.objects.all() 30 | 31 | 32 | @register_model_view(SoftwareProductInstallation, "bulk_import", detail=False) 33 | class SoftwareProductInstallationBulkImportView(generic.BulkImportView): 34 | queryset = SoftwareProductInstallation.objects.all() 35 | model_form = forms.SoftwareProductInstallationBulkImportForm 36 | 37 | 38 | @register_model_view(SoftwareProductInstallation, "bulk_edit", path="edit", detail=False) 39 | class SoftwareProductInstallationBulkEditView(generic.BulkEditView): 40 | queryset = SoftwareProductInstallation.objects.all() 41 | filterset = filtersets.SoftwareProductInstallationFilterSet 42 | table = tables.SoftwareProductInstallationTable 43 | form = forms.SoftwareProductInstallationBulkEditForm 44 | 45 | 46 | @register_model_view(SoftwareProductInstallation, "bulk_delete", path="delete", detail=False) 47 | class SoftwareProductInstallationBulkDeleteView(generic.BulkDeleteView): 48 | queryset = SoftwareProductInstallation.objects.all() 49 | filterset = filtersets.SoftwareProductInstallationFilterSet 50 | table = tables.SoftwareProductInstallationTable 51 | -------------------------------------------------------------------------------- /netbox_slm/views/software_product_version.py: -------------------------------------------------------------------------------- 1 | from netbox.views import generic 2 | from netbox_slm import filtersets, forms, tables 3 | from netbox_slm.models import SoftwareProductVersion 4 | from utilities.views import register_model_view 5 | 6 | 7 | @register_model_view(SoftwareProductVersion, "list", path="", detail=False) 8 | class SoftwareProductVersionListView(generic.ObjectListView): 9 | queryset = SoftwareProductVersion.objects.all() 10 | filterset = filtersets.SoftwareProductVersionFilterSet 11 | filterset_form = forms.SoftwareProductVersionFilterForm 12 | table = tables.SoftwareProductVersionTable 13 | 14 | 15 | @register_model_view(SoftwareProductVersion) 16 | class SoftwareProductVersionView(generic.ObjectView): 17 | queryset = SoftwareProductVersion.objects.all() 18 | 19 | def get_extra_context(self, request, instance): 20 | installation_count = instance.get_installation_count() 21 | return {"installations": installation_count} 22 | 23 | 24 | @register_model_view(SoftwareProductVersion, "add", detail=False) 25 | @register_model_view(SoftwareProductVersion, "edit") 26 | class SoftwareProductVersionEditView(generic.ObjectEditView): 27 | queryset = SoftwareProductVersion.objects.all() 28 | form = forms.SoftwareProductVersionForm 29 | 30 | 31 | @register_model_view(SoftwareProductVersion, "delete") 32 | class SoftwareProductVersionDeleteView(generic.ObjectDeleteView): 33 | queryset = SoftwareProductVersion.objects.all() 34 | 35 | 36 | @register_model_view(SoftwareProductVersion, "bulk_import", detail=False) 37 | class SoftwareProductVersionBulkImportView(generic.BulkImportView): 38 | queryset = SoftwareProductVersion.objects.all() 39 | model_form = forms.SoftwareProductVersionBulkImportForm 40 | 41 | 42 | @register_model_view(SoftwareProductVersion, "bulk_edit", path="edit", detail=False) 43 | class SoftwareProductVersionBulkEditView(generic.BulkEditView): 44 | queryset = SoftwareProductVersion.objects.all() 45 | filterset = filtersets.SoftwareProductVersionFilterSet 46 | table = tables.SoftwareProductVersionTable 47 | form = forms.SoftwareProductVersionBulkEditForm 48 | 49 | 50 | @register_model_view(SoftwareProductVersion, "bulk_delete", path="delete", detail=False) 51 | class SoftwareProductVersionBulkDeleteView(generic.BulkDeleteView): 52 | queryset = SoftwareProductVersion.objects.all() 53 | filterset = filtersets.SoftwareProductVersionFilterSet 54 | table = tables.SoftwareProductVersionTable 55 | -------------------------------------------------------------------------------- /pyproject.toml: -------------------------------------------------------------------------------- 1 | [build-system] 2 | requires = ["setuptools"] 3 | build-backend = "setuptools.build_meta" 4 | 5 | [project] 6 | name = "netbox-slm" 7 | dynamic = ["version"] 8 | description = "Software Lifecycle Management Netbox Plugin" 9 | readme = "README.md" 10 | requires-python = ">=3.11" 11 | license = { text = "Apache-2.0" } 12 | authors = [ 13 | {name = "ICTU", email = "open-source-projects@ictu.nl"}, 14 | ] 15 | classifiers = [ 16 | "Development Status :: 5 - Production/Stable", 17 | "Environment :: Plugins", 18 | "Environment :: Web Environment", 19 | "Framework :: Django", 20 | "Intended Audience :: Developers", 21 | "Intended Audience :: System Administrators", 22 | "License :: OSI Approved :: Apache Software License", 23 | "Operating System :: OS Independent", 24 | "Programming Language :: Python :: 3", 25 | "Programming Language :: Python :: 3 :: Only", 26 | "Programming Language :: Python :: 3.11", 27 | "Programming Language :: Python :: 3.12", 28 | "Programming Language :: Python :: 3.13", 29 | "Programming Language :: Python", 30 | "Topic :: Internet :: WWW/HTTP", 31 | "Topic :: Internet :: WWW/HTTP :: Dynamic Content", 32 | ] 33 | dependencies = [ 34 | "license-expression == 30.4.1", 35 | ] 36 | 37 | [project.optional-dependencies] 38 | build = [ 39 | "build == 1.2.2.post1", 40 | "setuptools == 75.8.0", 41 | "twine == 6.1.0", 42 | ] 43 | ci = [ 44 | "coverage == 7.6.11", 45 | "ruff == 0.9.6", 46 | ] 47 | 48 | [project.urls] 49 | Homepage = "https://github.com/ICTU/netbox_slm/" 50 | Changelog = "https://github.com/ICTU/netbox_slm/blob/master/CHANGELOG.md" 51 | Issues = "https://github.com/ICTU/netbox_slm/issues/" 52 | 53 | [tool.setuptools.dynamic] 54 | version = {attr = "netbox_slm.__version__"} 55 | 56 | [tool.setuptools.packages.find] 57 | include = ["netbox_slm"] 58 | 59 | [tool.ruff] 60 | line-length = 120 61 | target-version = "py312" 62 | src = ["netbox_slm"] 63 | 64 | [tool.ruff.lint] 65 | select = [ 66 | "E", # pycodestyle 67 | "F", # Pyflakes 68 | "UP", # pyupgrade 69 | "B", # flake8-bugbear 70 | "SIM", # flake8-simplify 71 | ] 72 | 73 | [tool.ruff.lint.per-file-ignores] 74 | "__init__.py" = [ 75 | "D104", # https://beta.ruff.rs/docs/rules/#pydocstyle-d - don't require doc strings in __init__.py files 76 | "F401", # https://beta.ruff.rs/docs/rules/#pyflakes-f - don't complain about unused imports in __init__.py files 77 | ] 78 | "netbox_slm/migrations/*.py" = [ 79 | "E501", # https://beta.ruff.rs/docs/rules/line-too-long/ - don't check on Django generated migration files 80 | ] 81 | 82 | [tool.ruff.format] 83 | exclude = ["netbox_slm/migrations/*.py"] 84 | -------------------------------------------------------------------------------- /sonar-project.properties: -------------------------------------------------------------------------------- 1 | # Project metadata 2 | sonar.organization=ictu 3 | sonar.projectKey=ICTU_netbox_slm 4 | sonar.projectName=netbox-slm 5 | sonar.projectVersion=1.8.2 6 | 7 | # Path is relative to the sonar-project.properties file 8 | sonar.sources=netbox_slm 9 | sonar.exclusions=netbox_slm/tests/** 10 | sonar.tests=netbox_slm/tests 11 | sonar.python.version=3.11, 3.12, 3.13 12 | 13 | # Python test reports 14 | sonar.python.coverage.reportPaths=/ci/reports/coverage.xml 15 | -------------------------------------------------------------------------------- /start-netbox.sh: -------------------------------------------------------------------------------- 1 | git clone -b release https://github.com/netbox-community/netbox-docker.git 2 | cd netbox-docker 3 | tee docker-compose.override.yml <