Compare commits
13 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 1980fd04ba | |||
| d06d0c76c2 | |||
| 9244328da8 | |||
| 70489c3dac | |||
| 2a3ee1c8af | |||
| 8a01cb4755 | |||
| d85b409662 | |||
| 9dfea6c795 | |||
| 29cb46963c | |||
| ca7c5f77a4 | |||
| 9f28113573 | |||
| f4920cbee6 | |||
| c1b0a7084b |
@@ -0,0 +1,34 @@
|
|||||||
|
name: Dev
|
||||||
|
|
||||||
|
on:
|
||||||
|
workflow_dispatch:
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
build:
|
||||||
|
runs-on: build-htz-01
|
||||||
|
steps:
|
||||||
|
- name: Checkout
|
||||||
|
uses: actions/checkout@v4
|
||||||
|
with:
|
||||||
|
fetch-depth: 0
|
||||||
|
|
||||||
|
- name: Build and push Docker image
|
||||||
|
run: |
|
||||||
|
docker build -t cr.jdbnet.co.uk/public/ipam:dev \
|
||||||
|
--build-arg VERSION=dev \
|
||||||
|
.
|
||||||
|
docker push cr.jdbnet.co.uk/public/ipam:dev
|
||||||
|
|
||||||
|
deploy:
|
||||||
|
name: Deploy to Kubernetes
|
||||||
|
needs: release
|
||||||
|
runs-on: k3s-internal-htz-01
|
||||||
|
steps:
|
||||||
|
- name: Checkout
|
||||||
|
uses: actions/checkout@v4
|
||||||
|
with:
|
||||||
|
fetch-depth: 0
|
||||||
|
|
||||||
|
- name: Deploy to Kubernetes
|
||||||
|
run: |
|
||||||
|
sudo kubectl replace -f deployment-dev.yml --grace-period=60 --force
|
||||||
@@ -0,0 +1,60 @@
|
|||||||
|
name: Release
|
||||||
|
|
||||||
|
on:
|
||||||
|
pull_request:
|
||||||
|
branches:
|
||||||
|
- main
|
||||||
|
types: [closed]
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
release:
|
||||||
|
if: github.event.pull_request.merged == true && startsWith(github.head_ref, 'v')
|
||||||
|
runs-on: build-htz-01
|
||||||
|
steps:
|
||||||
|
- name: Checkout
|
||||||
|
uses: actions/checkout@v4
|
||||||
|
with:
|
||||||
|
fetch-depth: 0
|
||||||
|
|
||||||
|
- name: Extract Version
|
||||||
|
id: get_version
|
||||||
|
run: echo "VERSION=${{ github.head_ref }}" >> $GITHUB_OUTPUT
|
||||||
|
|
||||||
|
- name: Generate Changelog
|
||||||
|
id: changelog
|
||||||
|
uses: https://github.com/metcalfc/changelog-generator@v4.6.2
|
||||||
|
with:
|
||||||
|
myToken: ${{ secrets.GITHUB_TOKEN }}
|
||||||
|
|
||||||
|
- name: Build and push Docker image
|
||||||
|
run: |
|
||||||
|
VERSION=${{ steps.get_version.outputs.VERSION }}
|
||||||
|
docker build -t cr.jdbnet.co.uk/public/ipam:$VERSION \
|
||||||
|
-t cr.jdbnet.co.uk/public/ipam:latest \
|
||||||
|
--build-arg VERSION=$VERSION \
|
||||||
|
.
|
||||||
|
docker push cr.jdbnet.co.uk/public/ipam:$VERSION
|
||||||
|
docker push cr.jdbnet.co.uk/public/ipam:latest
|
||||||
|
|
||||||
|
- name: Create Gitea Release
|
||||||
|
uses: https://gitea.com/actions/gitea-release-action@v1
|
||||||
|
with:
|
||||||
|
tag_name: ${{ steps.get_version.outputs.VERSION }}
|
||||||
|
name: ${{ steps.get_version.outputs.VERSION }}
|
||||||
|
body: ${{ steps.changelog.outputs.changelog }}
|
||||||
|
draft: false
|
||||||
|
prerelease: false
|
||||||
|
|
||||||
|
deploy:
|
||||||
|
name: Deploy to Kubernetes
|
||||||
|
needs: release
|
||||||
|
runs-on: k3s-internal-htz-01
|
||||||
|
steps:
|
||||||
|
- name: Checkout
|
||||||
|
uses: actions/checkout@v4
|
||||||
|
with:
|
||||||
|
fetch-depth: 0
|
||||||
|
|
||||||
|
- name: Deploy to Kubernetes
|
||||||
|
run: |
|
||||||
|
sudo kubectl replace -f deployment-prod.yml --grace-period=60 --force
|
||||||
@@ -1,72 +0,0 @@
|
|||||||
name: Release Please
|
|
||||||
|
|
||||||
on:
|
|
||||||
push:
|
|
||||||
branches:
|
|
||||||
- main
|
|
||||||
|
|
||||||
permissions:
|
|
||||||
contents: write
|
|
||||||
pull-requests: write
|
|
||||||
packages: write
|
|
||||||
|
|
||||||
jobs:
|
|
||||||
release-please:
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
outputs:
|
|
||||||
release_created: ${{ steps.release.outputs.release_created }}
|
|
||||||
steps:
|
|
||||||
- uses: googleapis/release-please-action@v4
|
|
||||||
id: release
|
|
||||||
with:
|
|
||||||
manifest-file: .release-please-manifest.json
|
|
||||||
config-file: .release-please-config.json
|
|
||||||
|
|
||||||
- name: Checkout
|
|
||||||
if: ${{ steps.release.outputs.release_created }}
|
|
||||||
uses: actions/checkout@v4
|
|
||||||
|
|
||||||
- name: Set up Docker Buildx
|
|
||||||
if: ${{ steps.release.outputs.release_created }}
|
|
||||||
uses: docker/setup-buildx-action@v3
|
|
||||||
|
|
||||||
- name: Log in to GitHub Container Registry
|
|
||||||
if: ${{ steps.release.outputs.release_created }}
|
|
||||||
uses: docker/login-action@v3
|
|
||||||
with:
|
|
||||||
registry: ghcr.io
|
|
||||||
username: ${{ github.actor }}
|
|
||||||
password: ${{ secrets.GITHUB_TOKEN }}
|
|
||||||
|
|
||||||
- name: Read version
|
|
||||||
if: ${{ steps.release.outputs.release_created }}
|
|
||||||
id: version
|
|
||||||
run: |
|
|
||||||
VERSION=$(cat VERSION)
|
|
||||||
echo "version=$VERSION" >> $GITHUB_OUTPUT
|
|
||||||
echo "VERSION=$VERSION" >> $GITHUB_ENV
|
|
||||||
|
|
||||||
- name: Build and push Docker image
|
|
||||||
if: ${{ steps.release.outputs.release_created }}
|
|
||||||
uses: docker/build-push-action@v6
|
|
||||||
with:
|
|
||||||
context: .
|
|
||||||
push: true
|
|
||||||
tags: |
|
|
||||||
ghcr.io/jdb-net/ipam:${{ env.VERSION }}
|
|
||||||
ghcr.io/jdb-net/ipam:latest
|
|
||||||
build-args: |
|
|
||||||
VERSION=${{ env.VERSION }}
|
|
||||||
|
|
||||||
deploy:
|
|
||||||
name: Deploy to Kubernetes
|
|
||||||
needs: release-please
|
|
||||||
if: ${{ needs.release-please.outputs.release_created }}
|
|
||||||
runs-on: [ k3s-internal-htz-01 ]
|
|
||||||
steps:
|
|
||||||
- name: Checkout
|
|
||||||
uses: actions/checkout@v4
|
|
||||||
|
|
||||||
- name: Apply manifests
|
|
||||||
run: |
|
|
||||||
sudo kubectl replace -f deployment.yml --grace-period=60 --force
|
|
||||||
@@ -1,54 +0,0 @@
|
|||||||
{
|
|
||||||
"packages": {
|
|
||||||
".": {
|
|
||||||
"release-type": "simple",
|
|
||||||
"version-file": "VERSION",
|
|
||||||
"extra-files": [
|
|
||||||
"CHANGELOG.md"
|
|
||||||
],
|
|
||||||
"changelog-sections": [
|
|
||||||
{
|
|
||||||
"type": "feat",
|
|
||||||
"section": "Features"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"type": "fix",
|
|
||||||
"section": "Bug Fixes"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"type": "refactor",
|
|
||||||
"section": "Refactoring"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"type": "style",
|
|
||||||
"section": "Style Changes"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"type": "perf",
|
|
||||||
"section": "Performance Improvements"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"type": "docs",
|
|
||||||
"section": "Documentation"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"type": "test",
|
|
||||||
"section": "Tests"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"type": "build",
|
|
||||||
"section": "Build System"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"type": "ci",
|
|
||||||
"section": "CI/CD"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"type": "chore",
|
|
||||||
"section": "Chores"
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"$schema": "https://raw.githubusercontent.com/googleapis/release-please/main/schemas/config.json"
|
|
||||||
}
|
|
||||||
@@ -1,3 +0,0 @@
|
|||||||
{
|
|
||||||
".": "1.9.1"
|
|
||||||
}
|
|
||||||
-179
@@ -1,179 +0,0 @@
|
|||||||
# Changelog
|
|
||||||
|
|
||||||
## [1.9.1](https://github.com/JDB-NET/ipam/compare/v1.9.0...v1.9.1) (2025-12-29)
|
|
||||||
|
|
||||||
|
|
||||||
### Bug Fixes
|
|
||||||
|
|
||||||
* :bug: device page dictionary ([83c1b21](https://github.com/JDB-NET/ipam/commit/83c1b21c04163e22c25831ee8064f3cd5ea2c99d))
|
|
||||||
|
|
||||||
## [1.9.0](https://github.com/JDB-NET/ipam/compare/v1.8.0...v1.9.0) (2025-12-27)
|
|
||||||
|
|
||||||
|
|
||||||
### Features
|
|
||||||
|
|
||||||
* :sparkles: api rate limiting ([e316a16](https://github.com/JDB-NET/ipam/commit/e316a1638661e023a23b4b164fc2c773cd2f7e2a))
|
|
||||||
* :sparkles: custom fields by device or subnet ([b23cda4](https://github.com/JDB-NET/ipam/commit/b23cda48af575b92a16be2c211e2b2ebb9008a56))
|
|
||||||
* :sparkles: ip address history ([21042b7](https://github.com/JDB-NET/ipam/commit/21042b7fd701ecf025ba6d9407137d05156d884a))
|
|
||||||
* :sparkles: ip address notes/descriptions ([8b001a0](https://github.com/JDB-NET/ipam/commit/8b001a047b263501300213d40a474b19e976cabb))
|
|
||||||
* :sparkles: log api usage to audit log ([e028f96](https://github.com/JDB-NET/ipam/commit/e028f9610cb09c5551594a32fada43e7078a2a73))
|
|
||||||
* :sparkles: two factor authentication ([5037c1b](https://github.com/JDB-NET/ipam/commit/5037c1b57823a59ed4dfda6dc3a16d570cde47bd))
|
|
||||||
* :sparkles: vlan management ([c7350ae](https://github.com/JDB-NET/ipam/commit/c7350aeb1f5b3ac471e40a351c66f9a82b70bdf4))
|
|
||||||
|
|
||||||
|
|
||||||
### Bug Fixes
|
|
||||||
|
|
||||||
* :bug: 2fa verification ([53dc19a](https://github.com/JDB-NET/ipam/commit/53dc19a549ca4255d22cc9812c6e7e83e1b76697))
|
|
||||||
|
|
||||||
|
|
||||||
### Refactoring
|
|
||||||
|
|
||||||
* :art: auto save custom fields ([7e1c4b1](https://github.com/JDB-NET/ipam/commit/7e1c4b126e0d45010b17af7be64f31c85e33d76d))
|
|
||||||
* :art: minify ([9106799](https://github.com/JDB-NET/ipam/commit/91067994bac7688b95e8c71a69b14967f640407e))
|
|
||||||
|
|
||||||
|
|
||||||
### Style Changes
|
|
||||||
|
|
||||||
* :lipstick: backup code button ([181e2b2](https://github.com/JDB-NET/ipam/commit/181e2b2ca53b2c7b99899f54bef028a3d9ac30eb))
|
|
||||||
|
|
||||||
## [1.8.0](https://github.com/JDB-NET/ipam/compare/v1.7.0...v1.8.0) (2025-12-23)
|
|
||||||
|
|
||||||
|
|
||||||
### Features
|
|
||||||
|
|
||||||
* :sparkles: get next available ip by api ([64ae4be](https://github.com/JDB-NET/ipam/commit/64ae4be6d5997ff0b16ff5232237d38f2fec5b64))
|
|
||||||
|
|
||||||
|
|
||||||
### Bug Fixes
|
|
||||||
|
|
||||||
* :bug: global search missing from devices ([283c445](https://github.com/JDB-NET/ipam/commit/283c445263b7dc992448d907e682e53b7720b610))
|
|
||||||
|
|
||||||
|
|
||||||
### Build System
|
|
||||||
|
|
||||||
* :rocket: redeploy ([d7fcffd](https://github.com/JDB-NET/ipam/commit/d7fcffd4b5598b682dede864ba526b1257584f6a))
|
|
||||||
|
|
||||||
## [1.7.0](https://github.com/JDB-NET/ipam/compare/v1.6.1...v1.7.0) (2025-12-05)
|
|
||||||
|
|
||||||
|
|
||||||
### Features
|
|
||||||
|
|
||||||
* :sparkles: add devices by tag page ([9c0e6d0](https://github.com/JDB-NET/ipam/commit/9c0e6d035c8dda68281b2bfe2b7a61802353f7a7))
|
|
||||||
|
|
||||||
|
|
||||||
### Bug Fixes
|
|
||||||
|
|
||||||
* :bug: invalidate cache when device type is added ([47208b3](https://github.com/JDB-NET/ipam/commit/47208b31eed51f0cf0d7c8c411093bda1c84cf1b))
|
|
||||||
* :bug: invalidate linked cache ([8242e9d](https://github.com/JDB-NET/ipam/commit/8242e9d758ef19030b516e4a51f0cfb556f4e5ba))
|
|
||||||
|
|
||||||
## [1.6.1](https://github.com/JDB-NET/ipam/compare/v1.6.0...v1.6.1) (2025-12-05)
|
|
||||||
|
|
||||||
|
|
||||||
### Bug Fixes
|
|
||||||
|
|
||||||
* :bug: invalidate subnet cache when device is deleted ([286bf4b](https://github.com/JDB-NET/ipam/commit/286bf4b665e6352dea7b14753f080fa5cabb7926))
|
|
||||||
|
|
||||||
## [1.6.0](https://github.com/JDB-NET/ipam/compare/v1.5.1...v1.6.0) (2025-12-05)
|
|
||||||
|
|
||||||
|
|
||||||
### Features
|
|
||||||
|
|
||||||
* :sparkles: backup and restore ([707846b](https://github.com/JDB-NET/ipam/commit/707846bb3c717df9223ea7103e29efc6e671e16d))
|
|
||||||
* :sparkles: bulk operations ([2163be8](https://github.com/JDB-NET/ipam/commit/2163be8f79b579e38944a689915a18d5c35f8d3a))
|
|
||||||
* :sparkles: global search ([3e8965d](https://github.com/JDB-NET/ipam/commit/3e8965de6f19b3b382e236b08df685401205f356))
|
|
||||||
* :sparkles: in memory cache ([3a9250f](https://github.com/JDB-NET/ipam/commit/3a9250f5b0c14bfc6a807fe2948bbc852a652047))
|
|
||||||
* :sparkles: subnet utilisation stats ([f98e92d](https://github.com/JDB-NET/ipam/commit/f98e92da062640d47bec3516def0efde3aebd058))
|
|
||||||
* :sparkles: update available notification ([730b870](https://github.com/JDB-NET/ipam/commit/730b8701db81f5e03760a25209baeab2f81116fa))
|
|
||||||
|
|
||||||
|
|
||||||
### Refactoring
|
|
||||||
|
|
||||||
* :art: database indexing and optimisation ([47f68fd](https://github.com/JDB-NET/ipam/commit/47f68fd27cf62d0e0d2af55089bc0556043c12ff))
|
|
||||||
* :art: header link to github releases ([61e3200](https://github.com/JDB-NET/ipam/commit/61e320020724e437d8a607e7341b12b2fe6f794d))
|
|
||||||
* :art: improved audit log filtering ([f016598](https://github.com/JDB-NET/ipam/commit/f0165985fc194fd3a3e460b52447a5511908ed91))
|
|
||||||
* :art: js ([1d9209a](https://github.com/JDB-NET/ipam/commit/1d9209a714a6d0b7d1901b6e3470f5265e0171a6))
|
|
||||||
* :art: tidy nav bar ([69588d6](https://github.com/JDB-NET/ipam/commit/69588d6518571d8de55c718c14176bb78cb19ee1))
|
|
||||||
|
|
||||||
|
|
||||||
### CI/CD
|
|
||||||
|
|
||||||
* :rocket: include all commit types ([f6795f5](https://github.com/JDB-NET/ipam/commit/f6795f52815a2d599840c8ed83c99ad690a046c8))
|
|
||||||
|
|
||||||
## [1.5.1](https://github.com/JDB-NET/ipam/compare/v1.5.0...v1.5.1) (2025-12-04)
|
|
||||||
|
|
||||||
|
|
||||||
### Bug Fixes
|
|
||||||
|
|
||||||
* :bug: audit log on mobile ([6f01c99](https://github.com/JDB-NET/ipam/commit/6f01c9956f4a31414a082a779eb493735df0b8e6))
|
|
||||||
|
|
||||||
## [1.5.0](https://github.com/JDB-NET/ipam/compare/v1.4.2...v1.5.0) (2025-11-21)
|
|
||||||
|
|
||||||
|
|
||||||
### Features
|
|
||||||
|
|
||||||
* :sparkles: device tags ([ad1e576](https://github.com/JDB-NET/ipam/commit/ad1e576da42bf90c59347f7f7a4cce13c6842204))
|
|
||||||
|
|
||||||
## [1.4.2](https://github.com/JDB-NET/ipam/compare/v1.4.1...v1.4.2) (2025-11-08)
|
|
||||||
|
|
||||||
|
|
||||||
### Bug Fixes
|
|
||||||
|
|
||||||
* :bug: ensure all fields are updated by api ([5c1ad03](https://github.com/JDB-NET/ipam/commit/5c1ad039904b2c8c8629242b5558b03da5ad782c))
|
|
||||||
|
|
||||||
## [1.4.1](https://github.com/JDB-NET/ipam/compare/v1.4.0...v1.4.1) (2025-11-06)
|
|
||||||
|
|
||||||
|
|
||||||
### Bug Fixes
|
|
||||||
|
|
||||||
* :bug: pagination no longer gets out of control ([80b6de3](https://github.com/JDB-NET/ipam/commit/80b6de395fc4ddb4e7cd3ece89b423af2667d298))
|
|
||||||
* :bug: styling of admin and users pages ([d56e064](https://github.com/JDB-NET/ipam/commit/d56e0647f74fba1db1f504e02364406691ede9f3))
|
|
||||||
|
|
||||||
## [1.4.0](https://github.com/JDB-NET/ipam/compare/v1.3.0...v1.4.0) (2025-11-06)
|
|
||||||
|
|
||||||
|
|
||||||
### Features
|
|
||||||
|
|
||||||
* :sparkles: full api integration ([c53472c](https://github.com/JDB-NET/ipam/commit/c53472c5d760e28e53a737cb0546e85c9a422d15))
|
|
||||||
|
|
||||||
## [1.3.0](https://github.com/JDB-NET/ipam/compare/v1.2.0...v1.3.0) (2025-11-06)
|
|
||||||
|
|
||||||
|
|
||||||
### Features
|
|
||||||
|
|
||||||
* :sparkles: role based access control ([3bf2697](https://github.com/JDB-NET/ipam/commit/3bf269701030bc1f14a48c5af488286c424dbfa7))
|
|
||||||
|
|
||||||
## [1.2.0](https://github.com/JDB-NET/ipam/compare/v1.1.1...v1.2.0) (2025-11-06)
|
|
||||||
|
|
||||||
|
|
||||||
### Features
|
|
||||||
|
|
||||||
* :sparkles: added the ability to create/edit/remove device types ([d68eefc](https://github.com/JDB-NET/ipam/commit/d68eefcf0cc4a59cda9cedb3e126d974ee45d2ad))
|
|
||||||
|
|
||||||
|
|
||||||
### Bug Fixes
|
|
||||||
|
|
||||||
* :bug: missing button classes ([f93fa15](https://github.com/JDB-NET/ipam/commit/f93fa155eb5d6c9ff4ed19f332c3ad6fff328d31))
|
|
||||||
|
|
||||||
## [1.1.1](https://github.com/JDB-NET/ipam/compare/v1.1.0...v1.1.1) (2025-11-01)
|
|
||||||
|
|
||||||
|
|
||||||
### Bug Fixes
|
|
||||||
|
|
||||||
* :bug: image name ([de123fa](https://github.com/JDB-NET/ipam/commit/de123fafd40d97ea6e545bd8dd1d3a812e2a709f))
|
|
||||||
|
|
||||||
## [1.1.0](https://github.com/JDB-NET/ipam/compare/v1.0.0...v1.1.0) (2025-11-01)
|
|
||||||
|
|
||||||
|
|
||||||
### Features
|
|
||||||
|
|
||||||
* Added icon on login button. Closes [#1](https://github.com/JDB-NET/ipam/issues/1) ([6e068b6](https://github.com/JDB-NET/ipam/commit/6e068b672592f7d23ca66a0a6189b5763d89a698))
|
|
||||||
* Added light mode up to admin ([38c8402](https://github.com/JDB-NET/ipam/commit/38c840251f03c8f1e1a2c407efa77621df70ce2f))
|
|
||||||
* Rack stuff now complete ([5d220d3](https://github.com/JDB-NET/ipam/commit/5d220d354df83db8b2bfbf8e2c87bd78ba91f6e5))
|
|
||||||
|
|
||||||
|
|
||||||
### Bug Fixes
|
|
||||||
|
|
||||||
* Back buttons now hidden on mobile ([40a7a2f](https://github.com/JDB-NET/ipam/commit/40a7a2f2d58f6c89a7e7e74908c088e7eddf966a))
|
|
||||||
* Corrected image in deployment ([9ecd492](https://github.com/JDB-NET/ipam/commit/9ecd492065fcd226d274f8e343d401437e1c8de8))
|
|
||||||
* Fixed back button on device page ([9734e4d](https://github.com/JDB-NET/ipam/commit/9734e4df0b27461867393c132991f9e2ec907de4))
|
|
||||||
* Fixed database initialisation and dropped to 1 worker ([7cd6a0f](https://github.com/JDB-NET/ipam/commit/7cd6a0f96d8dc20743603d55498d8c1af8069690))
|
|
||||||
@@ -1,6 +1,9 @@
|
|||||||
FROM python:3.13-slim
|
FROM python:3.13-slim
|
||||||
|
LABEL org.opencontainers.image.vendor="JDB-NET"
|
||||||
WORKDIR /app
|
WORKDIR /app
|
||||||
COPY . /app
|
COPY . /app
|
||||||
|
ARG VERSION=unknown
|
||||||
|
ENV VERSION=${VERSION}
|
||||||
RUN pip install -r requirements.txt
|
RUN pip install -r requirements.txt
|
||||||
RUN apt-get update && apt-get install -y curl mariadb-client-compat
|
RUN apt-get update && apt-get install -y curl mariadb-client-compat
|
||||||
RUN rm -rf /var/lib/apt/lists/*
|
RUN rm -rf /var/lib/apt/lists/*
|
||||||
|
|||||||
@@ -40,7 +40,7 @@ docker run -d \
|
|||||||
-e SECRET_KEY=your_secret_key \
|
-e SECRET_KEY=your_secret_key \
|
||||||
-e NAME="Your Organisation" \
|
-e NAME="Your Organisation" \
|
||||||
-e LOGO_PNG="https://example.com/logo.png" \
|
-e LOGO_PNG="https://example.com/logo.png" \
|
||||||
ghcr.io/jdb-net/ipam:latest
|
cr.jdbnet.co.uk/public/ipam:latest
|
||||||
```
|
```
|
||||||
|
|
||||||
### Docker Compose
|
### Docker Compose
|
||||||
@@ -48,7 +48,7 @@ docker run -d \
|
|||||||
```yaml
|
```yaml
|
||||||
services:
|
services:
|
||||||
ipam:
|
ipam:
|
||||||
image: ghcr.io/jdb-net/ipam:latest
|
image: cr.jdbnet.co.uk/public/ipam:latest
|
||||||
container_name: ipam
|
container_name: ipam
|
||||||
restart: unless-stopped
|
restart: unless-stopped
|
||||||
ports:
|
ports:
|
||||||
@@ -247,7 +247,7 @@ spec:
|
|||||||
spec:
|
spec:
|
||||||
containers:
|
containers:
|
||||||
- name: ipam
|
- name: ipam
|
||||||
image: ghcr.io/jdb-net/ipam:latest
|
image: cr.jdbnet.co.uk/public/ipam:latest
|
||||||
ports:
|
ports:
|
||||||
- containerPort: 5000
|
- containerPort: 5000
|
||||||
env:
|
env:
|
||||||
|
|||||||
@@ -27,23 +27,17 @@ limiter = Limiter(
|
|||||||
|
|
||||||
@app.context_processor
|
@app.context_processor
|
||||||
def inject_env_vars():
|
def inject_env_vars():
|
||||||
version = 'unknown'
|
version = os.environ.get('VERSION', 'unknown')
|
||||||
try:
|
|
||||||
version_file = os.path.join(os.path.dirname(os.path.abspath(__file__)), 'VERSION')
|
|
||||||
if os.path.exists(version_file):
|
|
||||||
with open(version_file, 'r') as f:
|
|
||||||
version = f.read().strip()
|
|
||||||
except Exception:
|
|
||||||
pass
|
|
||||||
|
|
||||||
# Import has_permission from routes after routes are registered
|
# Import has_permission and is_feature_enabled from routes after routes are registered
|
||||||
from routes import has_permission
|
from routes import has_permission, is_feature_enabled
|
||||||
|
|
||||||
return {
|
return {
|
||||||
'NAME': os.environ.get('NAME', 'JDB-NET'),
|
'NAME': os.environ.get('NAME', 'JDB-NET'),
|
||||||
'LOGO_PNG': os.environ.get('LOGO_PNG', 'https://assets.s3.jdbnet.co.uk/logo/128x128.png'),
|
'LOGO_PNG': os.environ.get('LOGO_PNG', 'https://assets.s3.jdbnet.co.uk/logo/128x128.png'),
|
||||||
'VERSION': version,
|
'VERSION': version,
|
||||||
'has_permission': has_permission
|
'has_permission': has_permission,
|
||||||
|
'is_feature_enabled': is_feature_enabled
|
||||||
}
|
}
|
||||||
|
|
||||||
register_routes(app, limiter)
|
register_routes(app, limiter)
|
||||||
|
|||||||
@@ -350,6 +350,32 @@ def init_db(app=None):
|
|||||||
if not cursor.fetchone():
|
if not cursor.fetchone():
|
||||||
cursor.execute('ALTER TABLE Subnet ADD COLUMN vlan_notes TEXT DEFAULT NULL')
|
cursor.execute('ALTER TABLE Subnet ADD COLUMN vlan_notes TEXT DEFAULT NULL')
|
||||||
|
|
||||||
|
# Create FeatureFlags table
|
||||||
|
cursor.execute('''
|
||||||
|
CREATE TABLE IF NOT EXISTS FeatureFlags (
|
||||||
|
id INTEGER PRIMARY KEY AUTO_INCREMENT,
|
||||||
|
feature_key VARCHAR(255) NOT NULL UNIQUE,
|
||||||
|
enabled BOOLEAN DEFAULT TRUE,
|
||||||
|
description TEXT,
|
||||||
|
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
updated_at DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP
|
||||||
|
)
|
||||||
|
''')
|
||||||
|
|
||||||
|
# Initialize default feature flags
|
||||||
|
default_features = [
|
||||||
|
('racks', True, 'Enable rack management functionality'),
|
||||||
|
('ip_address_notes', True, 'Enable IP address notes/descriptions editing on subnet page'),
|
||||||
|
('device_tags', True, 'Enable device tagging functionality'),
|
||||||
|
('bulk_operations', True, 'Enable bulk operations for devices and IPs')
|
||||||
|
]
|
||||||
|
|
||||||
|
for feature_key, enabled, description in default_features:
|
||||||
|
cursor.execute('SELECT id FROM FeatureFlags WHERE feature_key = %s', (feature_key,))
|
||||||
|
if not cursor.fetchone():
|
||||||
|
cursor.execute('INSERT INTO FeatureFlags (feature_key, enabled, description) VALUES (%s, %s, %s)',
|
||||||
|
(feature_key, enabled, description))
|
||||||
|
|
||||||
# Define all permissions with categories
|
# Define all permissions with categories
|
||||||
permissions = [
|
permissions = [
|
||||||
# View permissions
|
# View permissions
|
||||||
|
|||||||
@@ -15,7 +15,7 @@ spec:
|
|||||||
spec:
|
spec:
|
||||||
containers:
|
containers:
|
||||||
- name: ipam
|
- name: ipam
|
||||||
image: ghcr.io/jdb-net/ipam:latest
|
image: cr.jdbnet.co.uk/public/ipam:dev
|
||||||
imagePullPolicy: Always
|
imagePullPolicy: Always
|
||||||
ports:
|
ports:
|
||||||
- containerPort: 5000
|
- containerPort: 5000
|
||||||
@@ -0,0 +1,64 @@
|
|||||||
|
apiVersion: apps/v1
|
||||||
|
kind: Deployment
|
||||||
|
metadata:
|
||||||
|
name: ipam
|
||||||
|
namespace: ipam
|
||||||
|
spec:
|
||||||
|
replicas: 1
|
||||||
|
selector:
|
||||||
|
matchLabels:
|
||||||
|
app: ipam
|
||||||
|
template:
|
||||||
|
metadata:
|
||||||
|
labels:
|
||||||
|
app: ipam
|
||||||
|
spec:
|
||||||
|
containers:
|
||||||
|
- name: ipam
|
||||||
|
image: cr.jdbnet.co.uk/public/ipam:latest
|
||||||
|
imagePullPolicy: Always
|
||||||
|
ports:
|
||||||
|
- containerPort: 5000
|
||||||
|
name: "ipam"
|
||||||
|
env:
|
||||||
|
- name: SECRET_KEY
|
||||||
|
value: "41TbN7v5peFLZPrdwSCc64J3mjmiUk5fkVWsmb2m"
|
||||||
|
- name: MYSQL_HOST
|
||||||
|
value: "10.10.25.4"
|
||||||
|
- name: MYSQL_USER
|
||||||
|
value: "ipam"
|
||||||
|
- name: MYSQL_PASSWORD
|
||||||
|
value: "WXPmo05sGCfjGe"
|
||||||
|
- name: MYSQL_DATABASE
|
||||||
|
value: "ipam"
|
||||||
|
---
|
||||||
|
apiVersion: v1
|
||||||
|
kind: Service
|
||||||
|
metadata:
|
||||||
|
name: ipam-ingress-service
|
||||||
|
namespace: ipam
|
||||||
|
spec:
|
||||||
|
selector:
|
||||||
|
app: ipam
|
||||||
|
ports:
|
||||||
|
- protocol: TCP
|
||||||
|
port: 80
|
||||||
|
targetPort: 5000
|
||||||
|
---
|
||||||
|
apiVersion: networking.k8s.io/v1
|
||||||
|
kind: Ingress
|
||||||
|
metadata:
|
||||||
|
name: ipam-ingress
|
||||||
|
namespace: ipam
|
||||||
|
spec:
|
||||||
|
rules:
|
||||||
|
- host: ipam.jdb143.uk
|
||||||
|
http:
|
||||||
|
paths:
|
||||||
|
- pathType: Prefix
|
||||||
|
path: "/"
|
||||||
|
backend:
|
||||||
|
service:
|
||||||
|
name: ipam-ingress-service
|
||||||
|
port:
|
||||||
|
number: 80
|
||||||
@@ -75,6 +75,24 @@ def permission_required(permission_name):
|
|||||||
return decorated_function
|
return decorated_function
|
||||||
return decorator
|
return decorator
|
||||||
|
|
||||||
|
def is_feature_enabled(feature_key, conn=None):
|
||||||
|
"""Check if a feature flag is enabled"""
|
||||||
|
close_conn = False
|
||||||
|
if conn is None:
|
||||||
|
from flask import current_app
|
||||||
|
conn = get_db_connection(current_app)
|
||||||
|
close_conn = True
|
||||||
|
|
||||||
|
try:
|
||||||
|
cursor = conn.cursor()
|
||||||
|
cursor.execute('SELECT enabled FROM FeatureFlags WHERE feature_key = %s', (feature_key,))
|
||||||
|
result = cursor.fetchone()
|
||||||
|
# Default to True if feature flag doesn't exist (backward compatibility)
|
||||||
|
return result[0] if result else True
|
||||||
|
finally:
|
||||||
|
if close_conn:
|
||||||
|
conn.close()
|
||||||
|
|
||||||
def get_user_from_api_key(api_key):
|
def get_user_from_api_key(api_key):
|
||||||
"""Get user from API key"""
|
"""Get user from API key"""
|
||||||
from flask import current_app
|
from flask import current_app
|
||||||
@@ -687,6 +705,11 @@ def prewarm_cache(app):
|
|||||||
cursor.execute('''SELECT DeviceIPAddress.id as device_ip_id, IPAddress.ip FROM DeviceIPAddress JOIN IPAddress ON DeviceIPAddress.ip_id = IPAddress.id WHERE DeviceIPAddress.device_id = %s''', (device_id,))
|
cursor.execute('''SELECT DeviceIPAddress.id as device_ip_id, IPAddress.ip FROM DeviceIPAddress JOIN IPAddress ON DeviceIPAddress.ip_id = IPAddress.id WHERE DeviceIPAddress.device_id = %s''', (device_id,))
|
||||||
device_ips = [{'device_ip_id': row[0], 'ip': row[1]} for row in cursor.fetchall()]
|
device_ips = [{'device_ip_id': row[0], 'ip': row[1]} for row in cursor.fetchall()]
|
||||||
|
|
||||||
|
# Get tags for device (only if tags feature is enabled)
|
||||||
|
tags_enabled = is_feature_enabled('device_tags', conn=conn)
|
||||||
|
device_tags = []
|
||||||
|
all_tags = []
|
||||||
|
if tags_enabled:
|
||||||
cursor.execute('''
|
cursor.execute('''
|
||||||
SELECT t.id, t.name, t.color
|
SELECT t.id, t.name, t.color
|
||||||
FROM DeviceTag dt
|
FROM DeviceTag dt
|
||||||
@@ -1026,10 +1049,13 @@ def register_routes(app, limiter=None):
|
|||||||
tag_filter = request.args.get('tag')
|
tag_filter = request.args.get('tag')
|
||||||
|
|
||||||
with get_db_connection(current_app) as conn:
|
with get_db_connection(current_app) as conn:
|
||||||
|
# Check if device tags feature is enabled
|
||||||
|
tags_enabled = is_feature_enabled('device_tags', conn=conn)
|
||||||
|
|
||||||
cursor = conn.cursor()
|
cursor = conn.cursor()
|
||||||
|
|
||||||
# Base device query
|
# Base device query
|
||||||
if tag_filter:
|
if tag_filter and tags_enabled:
|
||||||
cursor.execute('''
|
cursor.execute('''
|
||||||
SELECT DISTINCT d.id, d.name, dt.icon_class
|
SELECT DISTINCT d.id, d.name, dt.icon_class
|
||||||
FROM Device d
|
FROM Device d
|
||||||
@@ -1051,8 +1077,10 @@ def register_routes(app, limiter=None):
|
|||||||
for row in cursor.fetchall():
|
for row in cursor.fetchall():
|
||||||
device_ips.setdefault(row[0], []).append((row[1], row[2]))
|
device_ips.setdefault(row[0], []).append((row[1], row[2]))
|
||||||
|
|
||||||
# Get tags for each device
|
# Get tags for each device (only if tags feature is enabled)
|
||||||
device_tags = {}
|
device_tags = {}
|
||||||
|
all_tag_names = []
|
||||||
|
if tags_enabled:
|
||||||
for device in devices:
|
for device in devices:
|
||||||
cursor.execute('''
|
cursor.execute('''
|
||||||
SELECT t.id, t.name, t.color
|
SELECT t.id, t.name, t.color
|
||||||
@@ -1153,7 +1181,11 @@ def register_routes(app, limiter=None):
|
|||||||
cursor.execute('''SELECT DeviceIPAddress.id as device_ip_id, IPAddress.ip FROM DeviceIPAddress JOIN IPAddress ON DeviceIPAddress.ip_id = IPAddress.id WHERE DeviceIPAddress.device_id = %s''', (device_id,))
|
cursor.execute('''SELECT DeviceIPAddress.id as device_ip_id, IPAddress.ip FROM DeviceIPAddress JOIN IPAddress ON DeviceIPAddress.ip_id = IPAddress.id WHERE DeviceIPAddress.device_id = %s''', (device_id,))
|
||||||
device_ips = [{'device_ip_id': row[0], 'ip': row[1]} for row in cursor.fetchall()]
|
device_ips = [{'device_ip_id': row[0], 'ip': row[1]} for row in cursor.fetchall()]
|
||||||
|
|
||||||
# Get device tags
|
# Get device tags (only if tags feature is enabled)
|
||||||
|
tags_enabled = is_feature_enabled('device_tags', conn=conn)
|
||||||
|
device_tags = []
|
||||||
|
all_tags = []
|
||||||
|
if tags_enabled:
|
||||||
cursor.execute('''
|
cursor.execute('''
|
||||||
SELECT t.id, t.name, t.color
|
SELECT t.id, t.name, t.color
|
||||||
FROM DeviceTag dt
|
FROM DeviceTag dt
|
||||||
@@ -1338,6 +1370,9 @@ def register_routes(app, limiter=None):
|
|||||||
tag_id = request.form['tag_id']
|
tag_id = request.form['tag_id']
|
||||||
from flask import current_app
|
from flask import current_app
|
||||||
with get_db_connection(current_app) as conn:
|
with get_db_connection(current_app) as conn:
|
||||||
|
# Check if device tags feature is enabled
|
||||||
|
if not is_feature_enabled('device_tags', conn=conn):
|
||||||
|
abort(404)
|
||||||
cursor = conn.cursor()
|
cursor = conn.cursor()
|
||||||
cursor.execute('SELECT name FROM Device WHERE id = %s', (device_id,))
|
cursor.execute('SELECT name FROM Device WHERE id = %s', (device_id,))
|
||||||
device = cursor.fetchone()
|
device = cursor.fetchone()
|
||||||
@@ -1369,6 +1404,9 @@ def register_routes(app, limiter=None):
|
|||||||
tag_id = request.form['tag_id']
|
tag_id = request.form['tag_id']
|
||||||
from flask import current_app
|
from flask import current_app
|
||||||
with get_db_connection(current_app) as conn:
|
with get_db_connection(current_app) as conn:
|
||||||
|
# Check if device tags feature is enabled
|
||||||
|
if not is_feature_enabled('device_tags', conn=conn):
|
||||||
|
abort(404)
|
||||||
cursor = conn.cursor()
|
cursor = conn.cursor()
|
||||||
cursor.execute('SELECT name FROM Device WHERE id = %s', (device_id,))
|
cursor.execute('SELECT name FROM Device WHERE id = %s', (device_id,))
|
||||||
device = cursor.fetchone()
|
device = cursor.fetchone()
|
||||||
@@ -1448,11 +1486,14 @@ def register_routes(app, limiter=None):
|
|||||||
subnet_dict['vlan_id'] = vlan_row[0]
|
subnet_dict['vlan_id'] = vlan_row[0]
|
||||||
subnet_dict['vlan_description'] = vlan_row[1]
|
subnet_dict['vlan_description'] = vlan_row[1]
|
||||||
subnet_dict['vlan_notes'] = vlan_row[2]
|
subnet_dict['vlan_notes'] = vlan_row[2]
|
||||||
|
# Check if IP address notes feature is enabled
|
||||||
|
ip_notes_enabled = is_feature_enabled('ip_address_notes', conn=conn)
|
||||||
return render_with_user('subnet.html', subnet=subnet_dict,
|
return render_with_user('subnet.html', subnet=subnet_dict,
|
||||||
ip_addresses=cached_result['ip_addresses'],
|
ip_addresses=cached_result['ip_addresses'],
|
||||||
utilization=cached_result['utilization'],
|
utilization=cached_result['utilization'],
|
||||||
custom_fields=custom_fields,
|
custom_fields=custom_fields,
|
||||||
can_edit_subnet=has_permission('edit_subnet'))
|
can_edit_subnet=has_permission('edit_subnet'),
|
||||||
|
ip_notes_enabled=ip_notes_enabled)
|
||||||
|
|
||||||
from flask import current_app
|
from flask import current_app
|
||||||
with get_db_connection(current_app) as conn:
|
with get_db_connection(current_app) as conn:
|
||||||
@@ -1525,11 +1566,16 @@ def register_routes(app, limiter=None):
|
|||||||
}
|
}
|
||||||
# Cache for 3 hours
|
# Cache for 3 hours
|
||||||
cache.set(cache_key, result, ttl=10800)
|
cache.set(cache_key, result, ttl=10800)
|
||||||
|
|
||||||
|
# Check if IP address notes feature is enabled
|
||||||
|
ip_notes_enabled = is_feature_enabled('ip_address_notes', conn=conn)
|
||||||
|
|
||||||
return render_with_user('subnet.html', subnet=subnet_dict,
|
return render_with_user('subnet.html', subnet=subnet_dict,
|
||||||
ip_addresses=ip_addresses_with_device,
|
ip_addresses=ip_addresses_with_device,
|
||||||
utilization=utilization_stats,
|
utilization=utilization_stats,
|
||||||
custom_fields=custom_fields,
|
custom_fields=custom_fields,
|
||||||
can_edit_subnet=has_permission('edit_subnet'))
|
can_edit_subnet=has_permission('edit_subnet'),
|
||||||
|
ip_notes_enabled=ip_notes_enabled)
|
||||||
|
|
||||||
@app.route('/add_subnet', methods=['POST'])
|
@app.route('/add_subnet', methods=['POST'])
|
||||||
@permission_required('add_subnet')
|
@permission_required('add_subnet')
|
||||||
@@ -1654,6 +1700,19 @@ def register_routes(app, limiter=None):
|
|||||||
cached_result = None
|
cached_result = None
|
||||||
|
|
||||||
if cached_result is not None:
|
if cached_result is not None:
|
||||||
|
# Always fetch feature flags fresh (they might have changed)
|
||||||
|
from flask import current_app
|
||||||
|
with get_db_connection(current_app) as conn:
|
||||||
|
cursor = conn.cursor()
|
||||||
|
cursor.execute('SELECT feature_key, enabled, description FROM FeatureFlags ORDER BY feature_key')
|
||||||
|
feature_flags = []
|
||||||
|
for row in cursor.fetchall():
|
||||||
|
feature_flags.append({
|
||||||
|
'key': row[0],
|
||||||
|
'enabled': bool(row[1]),
|
||||||
|
'description': row[2]
|
||||||
|
})
|
||||||
|
cached_result['feature_flags'] = feature_flags
|
||||||
return render_with_user('admin.html', **cached_result)
|
return render_with_user('admin.html', **cached_result)
|
||||||
|
|
||||||
from flask import current_app
|
from flask import current_app
|
||||||
@@ -1701,16 +1760,55 @@ def register_routes(app, limiter=None):
|
|||||||
'total': total_ips
|
'total': total_ips
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
|
# Get feature flags (inside the connection context)
|
||||||
|
cursor.execute('SELECT feature_key, enabled, description FROM FeatureFlags ORDER BY feature_key')
|
||||||
|
feature_flags = []
|
||||||
|
for row in cursor.fetchall():
|
||||||
|
feature_flags.append({
|
||||||
|
'key': row[0],
|
||||||
|
'enabled': bool(row[1]),
|
||||||
|
'description': row[2]
|
||||||
|
})
|
||||||
|
|
||||||
result_data = {
|
result_data = {
|
||||||
'subnets': subnets,
|
'subnets': subnets,
|
||||||
'can_add_subnet': has_permission('add_subnet'),
|
'can_add_subnet': has_permission('add_subnet'),
|
||||||
'can_edit_subnet': has_permission('edit_subnet'),
|
'can_edit_subnet': has_permission('edit_subnet'),
|
||||||
'can_delete_subnet': has_permission('delete_subnet')
|
'can_delete_subnet': has_permission('delete_subnet'),
|
||||||
|
'feature_flags': feature_flags
|
||||||
}
|
}
|
||||||
# Cache for 3 hours
|
# Cache for 3 hours
|
||||||
cache.set(cache_key, result_data, ttl=10800)
|
cache.set(cache_key, result_data, ttl=10800)
|
||||||
|
|
||||||
return render_with_user('admin.html', **result_data)
|
return render_with_user('admin.html', **result_data)
|
||||||
|
|
||||||
|
@app.route('/admin/feature_flags', methods=['POST'])
|
||||||
|
@permission_required('manage_users')
|
||||||
|
def update_feature_flags():
|
||||||
|
"""Update feature flags"""
|
||||||
|
user_name = get_current_user_name()
|
||||||
|
from flask import current_app
|
||||||
|
with get_db_connection(current_app) as conn:
|
||||||
|
cursor = conn.cursor()
|
||||||
|
# Get all feature flags
|
||||||
|
cursor.execute('SELECT feature_key FROM FeatureFlags')
|
||||||
|
all_features = [row[0] for row in cursor.fetchall()]
|
||||||
|
|
||||||
|
# Update each feature flag based on form data
|
||||||
|
for feature_key in all_features:
|
||||||
|
enabled = request.form.get(f'feature_{feature_key}') == 'on'
|
||||||
|
cursor.execute('UPDATE FeatureFlags SET enabled = %s WHERE feature_key = %s',
|
||||||
|
(enabled, feature_key))
|
||||||
|
logging.info(f"User {user_name} {'enabled' if enabled else 'disabled'} feature '{feature_key}'")
|
||||||
|
|
||||||
|
conn.commit()
|
||||||
|
|
||||||
|
# Clear admin cache to refresh feature flags
|
||||||
|
cache.clear('admin')
|
||||||
|
|
||||||
|
return redirect(url_for('admin'))
|
||||||
|
|
||||||
@app.route('/api-docs')
|
@app.route('/api-docs')
|
||||||
@permission_required('view_admin')
|
@permission_required('view_admin')
|
||||||
def api_docs():
|
def api_docs():
|
||||||
@@ -2047,6 +2145,9 @@ def register_routes(app, limiter=None):
|
|||||||
def tags():
|
def tags():
|
||||||
from flask import current_app
|
from flask import current_app
|
||||||
with get_db_connection(current_app) as conn:
|
with get_db_connection(current_app) as conn:
|
||||||
|
# Check if device tags feature is enabled
|
||||||
|
if not is_feature_enabled('device_tags', conn=conn):
|
||||||
|
abort(404)
|
||||||
cursor = conn.cursor()
|
cursor = conn.cursor()
|
||||||
error = None
|
error = None
|
||||||
|
|
||||||
@@ -2504,7 +2605,7 @@ def register_routes(app, limiter=None):
|
|||||||
@app.route('/check_update')
|
@app.route('/check_update')
|
||||||
@login_required
|
@login_required
|
||||||
def check_update():
|
def check_update():
|
||||||
"""Check for available updates from GitHub (cached for 3 hours)"""
|
"""Check for available updates from Gitea (cached for 3 hours)"""
|
||||||
cache_key = 'check_update'
|
cache_key = 'check_update'
|
||||||
|
|
||||||
# Check cache first
|
# Check cache first
|
||||||
@@ -2513,15 +2614,11 @@ def register_routes(app, limiter=None):
|
|||||||
return jsonify(cached_result)
|
return jsonify(cached_result)
|
||||||
|
|
||||||
try:
|
try:
|
||||||
# Get current version
|
# Get current version from environment
|
||||||
version_file = os.path.join(os.path.dirname(os.path.abspath(__file__)), 'VERSION')
|
current_version = os.environ.get('VERSION', 'unknown')
|
||||||
current_version = 'unknown'
|
|
||||||
if os.path.exists(version_file):
|
|
||||||
with open(version_file, 'r') as f:
|
|
||||||
current_version = f.read().strip()
|
|
||||||
|
|
||||||
# Fetch latest release from GitHub
|
# Fetch latest release from Gitea
|
||||||
response = requests.get('https://api.github.com/repos/JDB-NET/ipam/releases/latest', timeout=5)
|
response = requests.get('https://git.jdbnet.co.uk/api/v1/repos/jamie/ipam/releases/latest', timeout=5)
|
||||||
if response.status_code != 200:
|
if response.status_code != 200:
|
||||||
return jsonify({'error': 'Failed to fetch release information'}), 500
|
return jsonify({'error': 'Failed to fetch release information'}), 500
|
||||||
|
|
||||||
@@ -3324,6 +3421,9 @@ def register_routes(app, limiter=None):
|
|||||||
def racks():
|
def racks():
|
||||||
from flask import current_app
|
from flask import current_app
|
||||||
with get_db_connection(current_app) as conn:
|
with get_db_connection(current_app) as conn:
|
||||||
|
# Check if racks feature is enabled
|
||||||
|
if not is_feature_enabled('racks', conn=conn):
|
||||||
|
abort(404)
|
||||||
cursor = conn.cursor(dictionary=True)
|
cursor = conn.cursor(dictionary=True)
|
||||||
cursor.execute('SELECT * FROM Rack')
|
cursor.execute('SELECT * FROM Rack')
|
||||||
racks = cursor.fetchall()
|
racks = cursor.fetchall()
|
||||||
@@ -3343,6 +3443,10 @@ def register_routes(app, limiter=None):
|
|||||||
@permission_required('add_rack')
|
@permission_required('add_rack')
|
||||||
def add_rack():
|
def add_rack():
|
||||||
from flask import current_app
|
from flask import current_app
|
||||||
|
with get_db_connection(current_app) as conn:
|
||||||
|
# Check if racks feature is enabled
|
||||||
|
if not is_feature_enabled('racks', conn=conn):
|
||||||
|
abort(404)
|
||||||
if request.method == 'POST':
|
if request.method == 'POST':
|
||||||
name = request.form['name']
|
name = request.form['name']
|
||||||
site = request.form['site']
|
site = request.form['site']
|
||||||
@@ -3364,6 +3468,9 @@ def register_routes(app, limiter=None):
|
|||||||
from flask import current_app, request
|
from flask import current_app, request
|
||||||
side = request.args.get('side', 'front')
|
side = request.args.get('side', 'front')
|
||||||
with get_db_connection(current_app) as conn:
|
with get_db_connection(current_app) as conn:
|
||||||
|
# Check if racks feature is enabled
|
||||||
|
if not is_feature_enabled('racks', conn=conn):
|
||||||
|
abort(404)
|
||||||
cursor = conn.cursor(dictionary=True)
|
cursor = conn.cursor(dictionary=True)
|
||||||
cursor.execute('SELECT * FROM Rack WHERE id = %s', (rack_id,))
|
cursor.execute('SELECT * FROM Rack WHERE id = %s', (rack_id,))
|
||||||
rack = cursor.fetchone()
|
rack = cursor.fetchone()
|
||||||
@@ -3404,6 +3511,9 @@ def register_routes(app, limiter=None):
|
|||||||
user_name = get_current_user_name()
|
user_name = get_current_user_name()
|
||||||
from flask import current_app
|
from flask import current_app
|
||||||
with get_db_connection(current_app) as conn:
|
with get_db_connection(current_app) as conn:
|
||||||
|
# Check if racks feature is enabled
|
||||||
|
if not is_feature_enabled('racks', conn=conn):
|
||||||
|
abort(404)
|
||||||
cursor = conn.cursor(dictionary=True)
|
cursor = conn.cursor(dictionary=True)
|
||||||
cursor.execute('SELECT height_u FROM Rack WHERE id = %s', (rack_id,))
|
cursor.execute('SELECT height_u FROM Rack WHERE id = %s', (rack_id,))
|
||||||
rack = cursor.fetchone()
|
rack = cursor.fetchone()
|
||||||
@@ -3464,6 +3574,9 @@ def register_routes(app, limiter=None):
|
|||||||
user_name = get_current_user_name()
|
user_name = get_current_user_name()
|
||||||
from flask import current_app
|
from flask import current_app
|
||||||
with get_db_connection(current_app) as conn:
|
with get_db_connection(current_app) as conn:
|
||||||
|
# Check if racks feature is enabled
|
||||||
|
if not is_feature_enabled('racks', conn=conn):
|
||||||
|
abort(404)
|
||||||
cursor = conn.cursor(dictionary=True)
|
cursor = conn.cursor(dictionary=True)
|
||||||
cursor.execute('SELECT height_u FROM Rack WHERE id = %s', (rack_id,))
|
cursor.execute('SELECT height_u FROM Rack WHERE id = %s', (rack_id,))
|
||||||
rack = cursor.fetchone()
|
rack = cursor.fetchone()
|
||||||
@@ -3521,6 +3634,9 @@ def register_routes(app, limiter=None):
|
|||||||
user_name = get_current_user_name()
|
user_name = get_current_user_name()
|
||||||
from flask import current_app
|
from flask import current_app
|
||||||
with get_db_connection(current_app) as conn:
|
with get_db_connection(current_app) as conn:
|
||||||
|
# Check if racks feature is enabled
|
||||||
|
if not is_feature_enabled('racks', conn=conn):
|
||||||
|
abort(404)
|
||||||
cursor = conn.cursor(dictionary=True)
|
cursor = conn.cursor(dictionary=True)
|
||||||
cursor.execute('SELECT device_id, nonnet_device_name, position_u, side FROM RackDevice WHERE id = %s', (rack_device_id,))
|
cursor.execute('SELECT device_id, nonnet_device_name, position_u, side FROM RackDevice WHERE id = %s', (rack_device_id,))
|
||||||
rd = cursor.fetchone()
|
rd = cursor.fetchone()
|
||||||
@@ -3545,6 +3661,9 @@ def register_routes(app, limiter=None):
|
|||||||
user_name = get_current_user_name()
|
user_name = get_current_user_name()
|
||||||
from flask import current_app
|
from flask import current_app
|
||||||
with get_db_connection(current_app) as conn:
|
with get_db_connection(current_app) as conn:
|
||||||
|
# Check if racks feature is enabled
|
||||||
|
if not is_feature_enabled('racks', conn=conn):
|
||||||
|
abort(404)
|
||||||
cursor = conn.cursor()
|
cursor = conn.cursor()
|
||||||
cursor.execute('SELECT name FROM Rack WHERE id = %s', (rack_id,))
|
cursor.execute('SELECT name FROM Rack WHERE id = %s', (rack_id,))
|
||||||
rack_name = cursor.fetchone()
|
rack_name = cursor.fetchone()
|
||||||
@@ -3559,6 +3678,9 @@ def register_routes(app, limiter=None):
|
|||||||
def export_rack_csv(rack_id):
|
def export_rack_csv(rack_id):
|
||||||
from flask import current_app
|
from flask import current_app
|
||||||
with get_db_connection(current_app) as conn:
|
with get_db_connection(current_app) as conn:
|
||||||
|
# Check if racks feature is enabled
|
||||||
|
if not is_feature_enabled('racks', conn=conn):
|
||||||
|
abort(404)
|
||||||
cursor = conn.cursor(dictionary=True)
|
cursor = conn.cursor(dictionary=True)
|
||||||
cursor.execute('SELECT * FROM Rack WHERE id = %s', (rack_id,))
|
cursor.execute('SELECT * FROM Rack WHERE id = %s', (rack_id,))
|
||||||
rack = cursor.fetchone()
|
rack = cursor.fetchone()
|
||||||
@@ -4274,6 +4396,9 @@ def register_routes(app, limiter=None):
|
|||||||
"""Get all racks"""
|
"""Get all racks"""
|
||||||
from flask import current_app
|
from flask import current_app
|
||||||
with get_db_connection(current_app) as conn:
|
with get_db_connection(current_app) as conn:
|
||||||
|
# Check if racks feature is enabled
|
||||||
|
if not is_feature_enabled('racks', conn=conn):
|
||||||
|
return jsonify({'error': 'Racks feature is disabled'}), 404
|
||||||
cursor = conn.cursor(dictionary=True)
|
cursor = conn.cursor(dictionary=True)
|
||||||
cursor.execute('SELECT id, name, site, height_u FROM Rack ORDER BY site, name')
|
cursor.execute('SELECT id, name, site, height_u FROM Rack ORDER BY site, name')
|
||||||
racks = cursor.fetchall()
|
racks = cursor.fetchall()
|
||||||
@@ -4300,6 +4425,9 @@ def register_routes(app, limiter=None):
|
|||||||
"""Get a specific rack"""
|
"""Get a specific rack"""
|
||||||
from flask import current_app
|
from flask import current_app
|
||||||
with get_db_connection(current_app) as conn:
|
with get_db_connection(current_app) as conn:
|
||||||
|
# Check if racks feature is enabled
|
||||||
|
if not is_feature_enabled('racks', conn=conn):
|
||||||
|
return jsonify({'error': 'Racks feature is disabled'}), 404
|
||||||
cursor = conn.cursor(dictionary=True)
|
cursor = conn.cursor(dictionary=True)
|
||||||
cursor.execute('SELECT id, name, site, height_u FROM Rack WHERE id = %s', (rack_id,))
|
cursor.execute('SELECT id, name, site, height_u FROM Rack WHERE id = %s', (rack_id,))
|
||||||
rack = cursor.fetchone()
|
rack = cursor.fetchone()
|
||||||
@@ -4337,6 +4465,9 @@ def register_routes(app, limiter=None):
|
|||||||
return jsonify({'error': 'height_u must be greater than zero'}), 400
|
return jsonify({'error': 'height_u must be greater than zero'}), 400
|
||||||
|
|
||||||
with get_db_connection(current_app) as conn:
|
with get_db_connection(current_app) as conn:
|
||||||
|
# Check if racks feature is enabled
|
||||||
|
if not is_feature_enabled('racks', conn=conn):
|
||||||
|
return jsonify({'error': 'Racks feature is disabled'}), 404
|
||||||
cursor = conn.cursor()
|
cursor = conn.cursor()
|
||||||
cursor.execute('INSERT INTO Rack (name, site, height_u) VALUES (%s, %s, %s)', (name, site, height_u))
|
cursor.execute('INSERT INTO Rack (name, site, height_u) VALUES (%s, %s, %s)', (name, site, height_u))
|
||||||
rack_id = cursor.lastrowid
|
rack_id = cursor.lastrowid
|
||||||
@@ -4351,6 +4482,9 @@ def register_routes(app, limiter=None):
|
|||||||
"""Delete a rack"""
|
"""Delete a rack"""
|
||||||
from flask import current_app
|
from flask import current_app
|
||||||
with get_db_connection(current_app) as conn:
|
with get_db_connection(current_app) as conn:
|
||||||
|
# Check if racks feature is enabled
|
||||||
|
if not is_feature_enabled('racks', conn=conn):
|
||||||
|
return jsonify({'error': 'Racks feature is disabled'}), 404
|
||||||
cursor = conn.cursor()
|
cursor = conn.cursor()
|
||||||
cursor.execute('SELECT name FROM Rack WHERE id = %s', (rack_id,))
|
cursor.execute('SELECT name FROM Rack WHERE id = %s', (rack_id,))
|
||||||
rack = cursor.fetchone()
|
rack = cursor.fetchone()
|
||||||
@@ -4394,6 +4528,9 @@ def register_routes(app, limiter=None):
|
|||||||
return jsonify({'error': 'device_id must be an integer'}), 400
|
return jsonify({'error': 'device_id must be an integer'}), 400
|
||||||
|
|
||||||
with get_db_connection(current_app) as conn:
|
with get_db_connection(current_app) as conn:
|
||||||
|
# Check if racks feature is enabled
|
||||||
|
if not is_feature_enabled('racks', conn=conn):
|
||||||
|
return jsonify({'error': 'Racks feature is disabled'}), 404
|
||||||
cursor = conn.cursor(dictionary=True)
|
cursor = conn.cursor(dictionary=True)
|
||||||
cursor.execute('SELECT name, height_u FROM Rack WHERE id = %s', (rack_id,))
|
cursor.execute('SELECT name, height_u FROM Rack WHERE id = %s', (rack_id,))
|
||||||
rack = cursor.fetchone()
|
rack = cursor.fetchone()
|
||||||
@@ -4454,6 +4591,9 @@ def register_routes(app, limiter=None):
|
|||||||
"""Remove a device from a rack"""
|
"""Remove a device from a rack"""
|
||||||
from flask import current_app
|
from flask import current_app
|
||||||
with get_db_connection(current_app) as conn:
|
with get_db_connection(current_app) as conn:
|
||||||
|
# Check if racks feature is enabled
|
||||||
|
if not is_feature_enabled('racks', conn=conn):
|
||||||
|
return jsonify({'error': 'Racks feature is disabled'}), 404
|
||||||
cursor = conn.cursor(dictionary=True)
|
cursor = conn.cursor(dictionary=True)
|
||||||
cursor.execute('''
|
cursor.execute('''
|
||||||
SELECT rd.device_id, rd.nonnet_device_name, rd.position_u, rd.side,
|
SELECT rd.device_id, rd.nonnet_device_name, rd.position_u, rd.side,
|
||||||
@@ -4748,6 +4888,9 @@ def register_routes(app, limiter=None):
|
|||||||
"""Get all tags"""
|
"""Get all tags"""
|
||||||
from flask import current_app
|
from flask import current_app
|
||||||
with get_db_connection(current_app) as conn:
|
with get_db_connection(current_app) as conn:
|
||||||
|
# Check if device tags feature is enabled
|
||||||
|
if not is_feature_enabled('device_tags', conn=conn):
|
||||||
|
return jsonify({'error': 'Device tags feature is disabled'}), 404
|
||||||
cursor = conn.cursor(dictionary=True)
|
cursor = conn.cursor(dictionary=True)
|
||||||
cursor.execute('SELECT id, name, color, description, created_at FROM Tag ORDER BY name')
|
cursor.execute('SELECT id, name, color, description, created_at FROM Tag ORDER BY name')
|
||||||
tags = cursor.fetchall()
|
tags = cursor.fetchall()
|
||||||
@@ -4761,6 +4904,11 @@ def register_routes(app, limiter=None):
|
|||||||
@api_permission_required('add_tag')
|
@api_permission_required('add_tag')
|
||||||
def api_add_tag():
|
def api_add_tag():
|
||||||
"""Create a new tag"""
|
"""Create a new tag"""
|
||||||
|
from flask import current_app
|
||||||
|
with get_db_connection(current_app) as conn:
|
||||||
|
# Check if device tags feature is enabled
|
||||||
|
if not is_feature_enabled('device_tags', conn=conn):
|
||||||
|
return jsonify({'error': 'Device tags feature is disabled'}), 404
|
||||||
data = request.get_json()
|
data = request.get_json()
|
||||||
if not data or 'name' not in data:
|
if not data or 'name' not in data:
|
||||||
return jsonify({'error': 'Tag name is required'}), 400
|
return jsonify({'error': 'Tag name is required'}), 400
|
||||||
@@ -4794,6 +4942,9 @@ def register_routes(app, limiter=None):
|
|||||||
"""Get a specific tag"""
|
"""Get a specific tag"""
|
||||||
from flask import current_app
|
from flask import current_app
|
||||||
with get_db_connection(current_app) as conn:
|
with get_db_connection(current_app) as conn:
|
||||||
|
# Check if device tags feature is enabled
|
||||||
|
if not is_feature_enabled('device_tags', conn=conn):
|
||||||
|
return jsonify({'error': 'Device tags feature is disabled'}), 404
|
||||||
cursor = conn.cursor(dictionary=True)
|
cursor = conn.cursor(dictionary=True)
|
||||||
cursor.execute('SELECT id, name, color, description, created_at FROM Tag WHERE id = %s', (tag_id,))
|
cursor.execute('SELECT id, name, color, description, created_at FROM Tag WHERE id = %s', (tag_id,))
|
||||||
tag = cursor.fetchone()
|
tag = cursor.fetchone()
|
||||||
@@ -4815,6 +4966,11 @@ def register_routes(app, limiter=None):
|
|||||||
@api_permission_required('edit_tag')
|
@api_permission_required('edit_tag')
|
||||||
def api_update_tag(tag_id):
|
def api_update_tag(tag_id):
|
||||||
"""Update a tag"""
|
"""Update a tag"""
|
||||||
|
from flask import current_app
|
||||||
|
with get_db_connection(current_app) as conn:
|
||||||
|
# Check if device tags feature is enabled
|
||||||
|
if not is_feature_enabled('device_tags', conn=conn):
|
||||||
|
return jsonify({'error': 'Device tags feature is disabled'}), 404
|
||||||
data = request.get_json()
|
data = request.get_json()
|
||||||
if not data:
|
if not data:
|
||||||
return jsonify({'error': 'Request body is required'}), 400
|
return jsonify({'error': 'Request body is required'}), 400
|
||||||
@@ -4868,6 +5024,9 @@ def register_routes(app, limiter=None):
|
|||||||
"""Delete a tag"""
|
"""Delete a tag"""
|
||||||
from flask import current_app
|
from flask import current_app
|
||||||
with get_db_connection(current_app) as conn:
|
with get_db_connection(current_app) as conn:
|
||||||
|
# Check if device tags feature is enabled
|
||||||
|
if not is_feature_enabled('device_tags', conn=conn):
|
||||||
|
return jsonify({'error': 'Device tags feature is disabled'}), 404
|
||||||
cursor = conn.cursor()
|
cursor = conn.cursor()
|
||||||
cursor.execute('SELECT name FROM Tag WHERE id = %s', (tag_id,))
|
cursor.execute('SELECT name FROM Tag WHERE id = %s', (tag_id,))
|
||||||
tag = cursor.fetchone()
|
tag = cursor.fetchone()
|
||||||
@@ -4889,6 +5048,9 @@ def register_routes(app, limiter=None):
|
|||||||
"""Get tags for a specific device"""
|
"""Get tags for a specific device"""
|
||||||
from flask import current_app
|
from flask import current_app
|
||||||
with get_db_connection(current_app) as conn:
|
with get_db_connection(current_app) as conn:
|
||||||
|
# Check if device tags feature is enabled
|
||||||
|
if not is_feature_enabled('device_tags', conn=conn):
|
||||||
|
return jsonify({'error': 'Device tags feature is disabled'}), 404
|
||||||
cursor = conn.cursor(dictionary=True)
|
cursor = conn.cursor(dictionary=True)
|
||||||
cursor.execute('SELECT id, name FROM Device WHERE id = %s', (device_id,))
|
cursor.execute('SELECT id, name FROM Device WHERE id = %s', (device_id,))
|
||||||
if not cursor.fetchone():
|
if not cursor.fetchone():
|
||||||
@@ -4908,6 +5070,11 @@ def register_routes(app, limiter=None):
|
|||||||
@api_permission_required('assign_device_tag')
|
@api_permission_required('assign_device_tag')
|
||||||
def api_assign_device_tag(device_id):
|
def api_assign_device_tag(device_id):
|
||||||
"""Assign a tag to a device"""
|
"""Assign a tag to a device"""
|
||||||
|
from flask import current_app
|
||||||
|
with get_db_connection(current_app) as conn:
|
||||||
|
# Check if device tags feature is enabled
|
||||||
|
if not is_feature_enabled('device_tags', conn=conn):
|
||||||
|
return jsonify({'error': 'Device tags feature is disabled'}), 404
|
||||||
data = request.get_json()
|
data = request.get_json()
|
||||||
if not data or 'tag_id' not in data:
|
if not data or 'tag_id' not in data:
|
||||||
return jsonify({'error': 'tag_id is required'}), 400
|
return jsonify({'error': 'tag_id is required'}), 400
|
||||||
@@ -4946,6 +5113,9 @@ def register_routes(app, limiter=None):
|
|||||||
"""Remove a tag from a device"""
|
"""Remove a tag from a device"""
|
||||||
from flask import current_app
|
from flask import current_app
|
||||||
with get_db_connection(current_app) as conn:
|
with get_db_connection(current_app) as conn:
|
||||||
|
# Check if device tags feature is enabled
|
||||||
|
if not is_feature_enabled('device_tags', conn=conn):
|
||||||
|
return jsonify({'error': 'Device tags feature is disabled'}), 404
|
||||||
cursor = conn.cursor()
|
cursor = conn.cursor()
|
||||||
cursor.execute('SELECT name FROM Device WHERE id = %s', (device_id,))
|
cursor.execute('SELECT name FROM Device WHERE id = %s', (device_id,))
|
||||||
device = cursor.fetchone()
|
device = cursor.fetchone()
|
||||||
@@ -5167,13 +5337,22 @@ def register_routes(app, limiter=None):
|
|||||||
def bulk_operations():
|
def bulk_operations():
|
||||||
from flask import current_app
|
from flask import current_app
|
||||||
with get_db_connection(current_app) as conn:
|
with get_db_connection(current_app) as conn:
|
||||||
|
# Check if bulk operations feature is enabled
|
||||||
|
if not is_feature_enabled('bulk_operations', conn=conn):
|
||||||
|
abort(404)
|
||||||
cursor = conn.cursor()
|
cursor = conn.cursor()
|
||||||
cursor.execute('SELECT id, name FROM Device ORDER BY name')
|
cursor.execute('SELECT id, name FROM Device ORDER BY name')
|
||||||
devices = cursor.fetchall()
|
devices = cursor.fetchall()
|
||||||
cursor.execute('SELECT id, name, cidr, site FROM Subnet ORDER BY site, name')
|
cursor.execute('SELECT id, name, cidr, site FROM Subnet ORDER BY site, name')
|
||||||
subnets = cursor.fetchall()
|
subnets = cursor.fetchall()
|
||||||
|
|
||||||
|
# Get tags only if device tags feature is enabled
|
||||||
|
tags_enabled = is_feature_enabled('device_tags', conn=conn)
|
||||||
|
tags = []
|
||||||
|
if tags_enabled:
|
||||||
cursor.execute('SELECT id, name FROM Tag ORDER BY name')
|
cursor.execute('SELECT id, name FROM Tag ORDER BY name')
|
||||||
tags = cursor.fetchall()
|
tags = cursor.fetchall()
|
||||||
|
|
||||||
cursor.execute('SELECT id, name FROM DeviceType ORDER BY name')
|
cursor.execute('SELECT id, name FROM DeviceType ORDER BY name')
|
||||||
device_types = cursor.fetchall()
|
device_types = cursor.fetchall()
|
||||||
return render_with_user('bulk_operations.html',
|
return render_with_user('bulk_operations.html',
|
||||||
@@ -5189,6 +5368,11 @@ def register_routes(app, limiter=None):
|
|||||||
@app.route('/bulk/assign_ips', methods=['POST'])
|
@app.route('/bulk/assign_ips', methods=['POST'])
|
||||||
@permission_required('add_device_ip')
|
@permission_required('add_device_ip')
|
||||||
def bulk_assign_ips():
|
def bulk_assign_ips():
|
||||||
|
from flask import current_app
|
||||||
|
with get_db_connection(current_app) as conn:
|
||||||
|
# Check if bulk operations feature is enabled
|
||||||
|
if not is_feature_enabled('bulk_operations', conn=conn):
|
||||||
|
abort(404)
|
||||||
device_id = request.form['device_id']
|
device_id = request.form['device_id']
|
||||||
ip_ids = request.form.getlist('ip_ids[]')
|
ip_ids = request.form.getlist('ip_ids[]')
|
||||||
user_name = get_current_user_name()
|
user_name = get_current_user_name()
|
||||||
@@ -5268,6 +5452,11 @@ def register_routes(app, limiter=None):
|
|||||||
@app.route('/bulk/create_devices', methods=['POST'])
|
@app.route('/bulk/create_devices', methods=['POST'])
|
||||||
@permission_required('add_device')
|
@permission_required('add_device')
|
||||||
def bulk_create_devices():
|
def bulk_create_devices():
|
||||||
|
from flask import current_app
|
||||||
|
with get_db_connection(current_app) as conn:
|
||||||
|
# Check if bulk operations feature is enabled
|
||||||
|
if not is_feature_enabled('bulk_operations', conn=conn):
|
||||||
|
abort(404)
|
||||||
device_names = request.form.get('device_names', '').strip().split('\n')
|
device_names = request.form.get('device_names', '').strip().split('\n')
|
||||||
device_type_id = int(request.form.get('device_type', 1))
|
device_type_id = int(request.form.get('device_type', 1))
|
||||||
user_name = get_current_user_name()
|
user_name = get_current_user_name()
|
||||||
@@ -5298,6 +5487,14 @@ def register_routes(app, limiter=None):
|
|||||||
@app.route('/bulk/assign_tags', methods=['POST'])
|
@app.route('/bulk/assign_tags', methods=['POST'])
|
||||||
@permission_required('assign_device_tag')
|
@permission_required('assign_device_tag')
|
||||||
def bulk_assign_tags():
|
def bulk_assign_tags():
|
||||||
|
from flask import current_app
|
||||||
|
with get_db_connection(current_app) as conn:
|
||||||
|
# Check if bulk operations feature is enabled
|
||||||
|
if not is_feature_enabled('bulk_operations', conn=conn):
|
||||||
|
abort(404)
|
||||||
|
# Check if device tags feature is enabled
|
||||||
|
if not is_feature_enabled('device_tags', conn=conn):
|
||||||
|
abort(404)
|
||||||
device_ids = request.form.getlist('device_ids[]')
|
device_ids = request.form.getlist('device_ids[]')
|
||||||
tag_ids = request.form.getlist('tag_ids[]')
|
tag_ids = request.form.getlist('tag_ids[]')
|
||||||
user_name = get_current_user_name()
|
user_name = get_current_user_name()
|
||||||
@@ -5344,6 +5541,11 @@ def register_routes(app, limiter=None):
|
|||||||
@app.route('/bulk/export_subnets', methods=['POST'])
|
@app.route('/bulk/export_subnets', methods=['POST'])
|
||||||
@permission_required('export_subnet_csv')
|
@permission_required('export_subnet_csv')
|
||||||
def bulk_export_subnets():
|
def bulk_export_subnets():
|
||||||
|
from flask import current_app
|
||||||
|
with get_db_connection(current_app) as conn:
|
||||||
|
# Check if bulk operations feature is enabled
|
||||||
|
if not is_feature_enabled('bulk_operations', conn=conn):
|
||||||
|
abort(404)
|
||||||
subnet_ids = request.form.getlist('subnet_ids[]')
|
subnet_ids = request.form.getlist('subnet_ids[]')
|
||||||
from flask import current_app
|
from flask import current_app
|
||||||
output = StringIO()
|
output = StringIO()
|
||||||
|
|||||||
@@ -21,7 +21,7 @@ document.addEventListener('DOMContentLoaded', function() {
|
|||||||
latestVersionEl.textContent = 'v' + data.latest_version;
|
latestVersionEl.textContent = 'v' + data.latest_version;
|
||||||
|
|
||||||
// Set compare link (current version to latest version)
|
// Set compare link (current version to latest version)
|
||||||
compareLink.href = `https://github.com/JDB-NET/ipam/compare/v${data.current_version}...v${data.latest_version}`;
|
compareLink.href = `https://git.jdbnet.co.uk/jamie/ipam/compare/v${data.current_version}...v${data.latest_version}`;
|
||||||
|
|
||||||
// Show toast
|
// Show toast
|
||||||
toast.classList.remove('hidden');
|
toast.classList.remove('hidden');
|
||||||
|
|||||||
Vendored
+1
-1
@@ -1 +1 @@
|
|||||||
document.addEventListener("DOMContentLoaded",function(){let t=sessionStorage.getItem("update-toast-dismissed");!t&&fetch("/check_update").then(t=>t.json()).then(t=>{if(t.update_available){let e=document.getElementById("update-toast"),n=document.getElementById("toast-current-version"),s=document.getElementById("toast-latest-version"),a=document.getElementById("toast-compare-link"),o=document.getElementById("toast-close");n.textContent="v"+t.current_version,s.textContent="v"+t.latest_version,a.href=`https://github.com/JDB-NET/ipam/compare/v${t.current_version}...v${t.latest_version}`,e.classList.remove("hidden"),o.addEventListener("click",function(){e.classList.add("hidden"),sessionStorage.setItem("update-toast-dismissed","true")})}}).catch(t=>{console.error("Error checking for updates:",t)})});
|
document.addEventListener("DOMContentLoaded",function(){let t=sessionStorage.getItem("update-toast-dismissed");!t&&fetch("/check_update").then(t=>t.json()).then(t=>{if(t.update_available){let e=document.getElementById("update-toast"),n=document.getElementById("toast-current-version"),s=document.getElementById("toast-latest-version"),a=document.getElementById("toast-compare-link"),d=document.getElementById("toast-close");n.textContent="v"+t.current_version,s.textContent="v"+t.latest_version,a.href=`https://git.jdbnet.co.uk/jamie/ipam/compare/v${t.current_version}...v${t.latest_version}`,e.classList.remove("hidden"),d.addEventListener("click",function(){e.classList.add("hidden"),sessionStorage.setItem("update-toast-dismissed","true")})}}).catch(t=>{console.error("Error checking for updates:",t)})});
|
||||||
+35
-1
@@ -42,7 +42,7 @@
|
|||||||
</div>
|
</div>
|
||||||
<i class="fas fa-chevron-right text-gray-400"></i>
|
<i class="fas fa-chevron-right text-gray-400"></i>
|
||||||
</a>
|
</a>
|
||||||
{% if has_permission('view_tags') %}
|
{% if has_permission('view_tags') and is_feature_enabled('device_tags') %}
|
||||||
<a href="/tags" class="bg-gray-200 dark:bg-zinc-800 hover:bg-gray-300 dark:hover:bg-zinc-700 p-6 rounded-lg shadow-md flex items-center justify-between transition-colors">
|
<a href="/tags" class="bg-gray-200 dark:bg-zinc-800 hover:bg-gray-300 dark:hover:bg-zinc-700 p-6 rounded-lg shadow-md flex items-center justify-between transition-colors">
|
||||||
<div class="flex items-center space-x-4">
|
<div class="flex items-center space-x-4">
|
||||||
<i class="fas fa-tags text-3xl text-gray-600 dark:text-gray-400"></i>
|
<i class="fas fa-tags text-3xl text-gray-600 dark:text-gray-400"></i>
|
||||||
@@ -167,6 +167,40 @@
|
|||||||
</div>
|
</div>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<!-- Feature Flags Section -->
|
||||||
|
{% if has_permission('manage_users') %}
|
||||||
|
<div class="bg-gray-200 dark:bg-zinc-800 p-6 rounded-lg shadow-md mt-8">
|
||||||
|
<h2 class="text-2xl font-bold mb-6">Feature Flags</h2>
|
||||||
|
<form action="/admin/feature_flags" method="POST">
|
||||||
|
<div class="space-y-4">
|
||||||
|
{% for flag in feature_flags %}
|
||||||
|
<div class="flex items-center justify-between p-4 bg-gray-300 dark:bg-zinc-700 rounded-lg">
|
||||||
|
<div class="flex-1">
|
||||||
|
<label for="feature_{{ flag.key }}" class="text-lg font-semibold cursor-pointer">
|
||||||
|
{{ flag.key|replace('_', ' ')|title }}
|
||||||
|
</label>
|
||||||
|
{% if flag.description %}
|
||||||
|
<p class="text-sm text-gray-600 dark:text-gray-400 mt-1">{{ flag.description }}</p>
|
||||||
|
{% endif %}
|
||||||
|
</div>
|
||||||
|
<label class="relative inline-flex items-center cursor-pointer">
|
||||||
|
<input type="checkbox" name="feature_{{ flag.key }}" id="feature_{{ flag.key }}"
|
||||||
|
class="sr-only peer" {% if flag.enabled %}checked{% endif %}>
|
||||||
|
<div class="w-11 h-6 bg-gray-600 peer-focus:outline-none peer-focus:ring-4 peer-focus:ring-gray-800 rounded-full peer peer-checked:after:translate-x-full peer-checked:after:border-white after:content-[''] after:absolute after:top-[2px] after:left-[2px] after:bg-white after:border-gray-300 after:border after:rounded-full after:h-5 after:w-5 after:transition-all peer-checked:bg-gray-500 dark:peer-checked:bg-zinc-600"></div>
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
{% endfor %}
|
||||||
|
</div>
|
||||||
|
<div class="flex justify-end mt-6">
|
||||||
|
<button type="submit" class="bg-gray-300 hover:bg-gray-400 dark:bg-zinc-700 dark:hover:bg-zinc-600 hover:cursor-pointer px-6 py-2 rounded-lg font-medium">
|
||||||
|
Save Changes
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|||||||
@@ -22,7 +22,9 @@
|
|||||||
<div class="flex space-x-4">
|
<div class="flex space-x-4">
|
||||||
<button onclick="showTab('assign-ips')" id="tab-assign-ips" class="tab-btn px-4 py-2 font-medium border-b-2 border-gray-600 text-gray-900 dark:text-gray-100 hover:cursor-pointer">Bulk IP Assignment</button>
|
<button onclick="showTab('assign-ips')" id="tab-assign-ips" class="tab-btn px-4 py-2 font-medium border-b-2 border-gray-600 text-gray-900 dark:text-gray-100 hover:cursor-pointer">Bulk IP Assignment</button>
|
||||||
<button onclick="showTab('create-devices')" id="tab-create-devices" class="tab-btn px-4 py-2 font-medium border-b-2 border-transparent text-gray-500 hover:text-gray-700 dark:hover:text-gray-300 hover:cursor-pointer">Bulk Device Creation</button>
|
<button onclick="showTab('create-devices')" id="tab-create-devices" class="tab-btn px-4 py-2 font-medium border-b-2 border-transparent text-gray-500 hover:text-gray-700 dark:hover:text-gray-300 hover:cursor-pointer">Bulk Device Creation</button>
|
||||||
|
{% if is_feature_enabled('device_tags') %}
|
||||||
<button onclick="showTab('assign-tags')" id="tab-assign-tags" class="tab-btn px-4 py-2 font-medium border-b-2 border-transparent text-gray-500 hover:text-gray-700 dark:hover:text-gray-300 hover:cursor-pointer">Bulk Tag Assignment</button>
|
<button onclick="showTab('assign-tags')" id="tab-assign-tags" class="tab-btn px-4 py-2 font-medium border-b-2 border-transparent text-gray-500 hover:text-gray-700 dark:hover:text-gray-300 hover:cursor-pointer">Bulk Tag Assignment</button>
|
||||||
|
{% endif %}
|
||||||
<button onclick="showTab('export')" id="tab-export" class="tab-btn px-4 py-2 font-medium border-b-2 border-transparent text-gray-500 hover:text-gray-700 dark:hover:text-gray-300 hover:cursor-pointer">Bulk Export</button>
|
<button onclick="showTab('export')" id="tab-export" class="tab-btn px-4 py-2 font-medium border-b-2 border-transparent text-gray-500 hover:text-gray-700 dark:hover:text-gray-300 hover:cursor-pointer">Bulk Export</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -94,6 +96,7 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Bulk Tag Assignment -->
|
<!-- Bulk Tag Assignment -->
|
||||||
|
{% if is_feature_enabled('device_tags') %}
|
||||||
<div id="panel-assign-tags" class="tab-panel hidden bg-gray-200 dark:bg-zinc-800 p-6 rounded-lg shadow-md">
|
<div id="panel-assign-tags" class="tab-panel hidden bg-gray-200 dark:bg-zinc-800 p-6 rounded-lg shadow-md">
|
||||||
{% if can_assign_device_tag %}
|
{% if can_assign_device_tag %}
|
||||||
<h2 class="text-2xl font-bold mb-4">Bulk Tag Assignment</h2>
|
<h2 class="text-2xl font-bold mb-4">Bulk Tag Assignment</h2>
|
||||||
@@ -123,6 +126,7 @@
|
|||||||
<p class="text-gray-500">You don't have permission to assign tags to devices.</p>
|
<p class="text-gray-500">You don't have permission to assign tags to devices.</p>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
</div>
|
</div>
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
<!-- Bulk Export -->
|
<!-- Bulk Export -->
|
||||||
<div id="panel-export" class="tab-panel hidden bg-gray-200 dark:bg-zinc-800 p-6 rounded-lg shadow-md">
|
<div id="panel-export" class="tab-panel hidden bg-gray-200 dark:bg-zinc-800 p-6 rounded-lg shadow-md">
|
||||||
|
|||||||
@@ -115,6 +115,7 @@
|
|||||||
{% endif %}
|
{% endif %}
|
||||||
|
|
||||||
<!-- Tags Section -->
|
<!-- Tags Section -->
|
||||||
|
{% if is_feature_enabled('device_tags') %}
|
||||||
<div class="tags-section mb-6">
|
<div class="tags-section mb-6">
|
||||||
<h3 class="text-lg font-bold mb-2">Tags:</h3>
|
<h3 class="text-lg font-bold mb-2">Tags:</h3>
|
||||||
<div class="flex flex-wrap gap-2 mb-4">
|
<div class="flex flex-wrap gap-2 mb-4">
|
||||||
@@ -155,6 +156,7 @@
|
|||||||
</form>
|
</form>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
</div>
|
</div>
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
<form action="/update_device_description" method="POST" class="mb-6 mt-4">
|
<form action="/update_device_description" method="POST" class="mb-6 mt-4">
|
||||||
<input type="hidden" name="device_id" value="{{ device.id }}">
|
<input type="hidden" name="device_id" value="{{ device.id }}">
|
||||||
|
|||||||
@@ -16,14 +16,16 @@
|
|||||||
<h1 class="text-3xl font-bold mb-6 text-center">Device Manager</h1>
|
<h1 class="text-3xl font-bold mb-6 text-center">Device Manager</h1>
|
||||||
<div class="flex flex-row justify-center gap-4 mb-6 flex-wrap">
|
<div class="flex flex-row justify-center gap-4 mb-6 flex-wrap">
|
||||||
<a href="/add_device" class="text-center bg-gray-200 hover:bg-gray-400 dark:bg-zinc-700 dark:hover:bg-zinc-600 px-4 py-2 rounded-lg">Add New Device</a>
|
<a href="/add_device" class="text-center bg-gray-200 hover:bg-gray-400 dark:bg-zinc-700 dark:hover:bg-zinc-600 px-4 py-2 rounded-lg">Add New Device</a>
|
||||||
|
{% if is_feature_enabled('bulk_operations') %}
|
||||||
<a href="/bulk" class="text-center bg-gray-200 hover:bg-gray-400 dark:bg-zinc-700 dark:hover:bg-zinc-600 px-4 py-2 rounded-lg">Bulk Operations</a>
|
<a href="/bulk" class="text-center bg-gray-200 hover:bg-gray-400 dark:bg-zinc-700 dark:hover:bg-zinc-600 px-4 py-2 rounded-lg">Bulk Operations</a>
|
||||||
|
{% endif %}
|
||||||
<a href="/device_type_stats" class="text-center bg-gray-200 hover:bg-gray-400 dark:bg-zinc-700 dark:hover:bg-zinc-600 px-4 py-2 rounded-lg">View Device Stats</a>
|
<a href="/device_type_stats" class="text-center bg-gray-200 hover:bg-gray-400 dark:bg-zinc-700 dark:hover:bg-zinc-600 px-4 py-2 rounded-lg">View Device Stats</a>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Filters Section -->
|
<!-- Filters Section -->
|
||||||
<div class="mb-6 space-y-4">
|
<div class="mb-6 space-y-4">
|
||||||
<!-- Tag Filter -->
|
<!-- Tag Filter -->
|
||||||
{% if all_tag_names %}
|
{% if is_feature_enabled('device_tags') and all_tag_names %}
|
||||||
<div class="flex items-center space-x-2">
|
<div class="flex items-center space-x-2">
|
||||||
<label class="text-sm font-medium">Filter by tag:</label>
|
<label class="text-sm font-medium">Filter by tag:</label>
|
||||||
<select id="tag-filter" class="border p-2 rounded-lg bg-gray-200 dark:bg-zinc-800 border-gray-600">
|
<select id="tag-filter" class="border p-2 rounded-lg bg-gray-200 dark:bg-zinc-800 border-gray-600">
|
||||||
@@ -67,6 +69,7 @@
|
|||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
<!-- Tags -->
|
<!-- Tags -->
|
||||||
|
{% if is_feature_enabled('device_tags') %}
|
||||||
{% set tags = device_tags.get(device.id, []) %}
|
{% set tags = device_tags.get(device.id, []) %}
|
||||||
{% if tags %}
|
{% if tags %}
|
||||||
<div class="flex flex-wrap gap-1 mt-2">
|
<div class="flex flex-wrap gap-1 mt-2">
|
||||||
@@ -78,6 +81,7 @@
|
|||||||
{% endfor %}
|
{% endfor %}
|
||||||
</div>
|
</div>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
{% endif %}
|
||||||
</a>
|
</a>
|
||||||
</li>
|
</li>
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
|
|||||||
@@ -4,7 +4,7 @@
|
|||||||
<img src="{{ LOGO_PNG }}" alt="Logo" class="h-8 rounded">
|
<img src="{{ LOGO_PNG }}" alt="Logo" class="h-8 rounded">
|
||||||
<span class="text-2xl font-bold text-white whitespace-nowrap">{{ NAME }} IPAM</span>
|
<span class="text-2xl font-bold text-white whitespace-nowrap">{{ NAME }} IPAM</span>
|
||||||
</a>
|
</a>
|
||||||
<a href="https://github.com/JDB-NET/ipam/releases" target="_blank" rel="noopener noreferrer" class="text-sm font-normal text-gray-300 hover:text-gray-100 -ml-1 mt-3">v{{ VERSION }}</a>
|
<a href="https://git.jdbnet.co.uk/jamie/ipam/releases" target="_blank" rel="noopener noreferrer" class="text-sm font-normal text-gray-300 hover:text-gray-100 -ml-1 mt-3">v{{ VERSION }}</a>
|
||||||
</div>
|
</div>
|
||||||
<div class="hidden lg:flex items-center justify-center absolute left-1/2" style="transform: translateX(calc(-50% + 1.5rem));">
|
<div class="hidden lg:flex items-center justify-center absolute left-1/2" style="transform: translateX(calc(-50% + 1.5rem));">
|
||||||
<form action="/search" method="GET" class="flex items-center space-x-2">
|
<form action="/search" method="GET" class="flex items-center space-x-2">
|
||||||
@@ -29,7 +29,7 @@
|
|||||||
<span>Devices</span>
|
<span>Devices</span>
|
||||||
</a>
|
</a>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
{% if has_permission('view_racks') %}
|
{% if has_permission('view_racks') and is_feature_enabled('racks') %}
|
||||||
<a href="/racks" class="text-gray-200 hover:text-gray-400 font-medium flex items-center gap-2">
|
<a href="/racks" class="text-gray-200 hover:text-gray-400 font-medium flex items-center gap-2">
|
||||||
<i class="fas fa-th"></i>
|
<i class="fas fa-th"></i>
|
||||||
<span>Racks</span>
|
<span>Racks</span>
|
||||||
@@ -86,7 +86,7 @@
|
|||||||
<span>Devices</span>
|
<span>Devices</span>
|
||||||
</a>
|
</a>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
{% if has_permission('view_racks') %}
|
{% if has_permission('view_racks') and is_feature_enabled('racks') %}
|
||||||
<a href="/racks" class="block px-6 py-2 text-gray-200 hover:text-gray-400 font-medium flex items-center gap-2">
|
<a href="/racks" class="block px-6 py-2 text-gray-200 hover:text-gray-400 font-medium flex items-center gap-2">
|
||||||
<i class="fas fa-th"></i>
|
<i class="fas fa-th"></i>
|
||||||
<span>Racks</span>
|
<span>Racks</span>
|
||||||
|
|||||||
@@ -251,6 +251,7 @@
|
|||||||
<td class="text-left align-top hidden sm:table-cell desc-col">
|
<td class="text-left align-top hidden sm:table-cell desc-col">
|
||||||
{% set device_desc = ip[4] if ip[4] else '' %}
|
{% set device_desc = ip[4] if ip[4] else '' %}
|
||||||
{% set ip_notes = ip[5] if ip|length > 5 and ip[5] else '' %}
|
{% set ip_notes = ip[5] if ip|length > 5 and ip[5] else '' %}
|
||||||
|
{% if ip_notes_enabled %}
|
||||||
{% set combined_desc = '' %}
|
{% set combined_desc = '' %}
|
||||||
{% if device_desc %}
|
{% if device_desc %}
|
||||||
{% set combined_desc = device_desc %}
|
{% set combined_desc = device_desc %}
|
||||||
@@ -267,6 +268,10 @@
|
|||||||
{% else %}
|
{% else %}
|
||||||
<textarea readonly rows="1" class="border border-gray-600 rounded w-full cursor-pointer p-2" style="overflow: hidden; resize: none;">{{ combined_desc }}</textarea>
|
<textarea readonly rows="1" class="border border-gray-600 rounded w-full cursor-pointer p-2" style="overflow: hidden; resize: none;">{{ combined_desc }}</textarea>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
{% else %}
|
||||||
|
{# IP notes disabled - show only device description, read-only #}
|
||||||
|
<textarea readonly rows="1" class="border border-gray-600 rounded w-full cursor-pointer p-2 bg-gray-200 dark:bg-zinc-800" style="overflow: hidden; resize: none;">{{ device_desc }}</textarea>
|
||||||
|
{% endif %}
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
|
|||||||
Reference in New Issue
Block a user