diff --git a/.copywrite.hcl b/.copywrite.hcl
deleted file mode 100644
index bdf3892..0000000
--- a/.copywrite.hcl
+++ /dev/null
@@ -1,21 +0,0 @@
-# NOTE: This file is for HashiCorp specific licensing automation and can be deleted after creating a new repo with this template.
-schema_version = 1
-
-project {
- license = "MPL-2.0"
- copyright_year = 2021
-
- header_ignore = [
- # examples used within documentation (prose)
- "examples/**",
-
- # GitHub issue template configuration
- ".github/ISSUE_TEMPLATE/*.yml",
-
- # golangci-lint tooling configuration
- ".golangci.yml",
-
- # GoReleaser tooling configuration
- ".goreleaser.yml",
- ]
-}
diff --git a/.gitea/workflows/ci.yaml b/.gitea/workflows/ci.yaml
new file mode 100644
index 0000000..4fdefc6
--- /dev/null
+++ b/.gitea/workflows/ci.yaml
@@ -0,0 +1,33 @@
+name: CI
+
+on:
+ push:
+ branches: [main]
+ pull_request:
+ branches: [main]
+
+jobs:
+ build:
+ runs-on: ubuntu-latest
+ steps:
+ - uses: actions/checkout@v4
+
+ - uses: actions/setup-go@v5
+ with:
+ go-version-file: go.mod
+
+ - name: Build
+ run: go build -v ./...
+
+ - name: Lint
+ uses: golangci/golangci-lint-action@v6
+ with:
+ version: latest
+
+ - name: Test
+ run: go test -race -v ./internal/...
+
+ - name: Check go generate
+ run: |
+ go generate ./...
+ git diff --exit-code || (echo "go generate produced changes; please run 'go generate ./...' and commit" && exit 1)
diff --git a/.gitea/workflows/endpoint-sync.yaml b/.gitea/workflows/endpoint-sync.yaml
new file mode 100644
index 0000000..bed4721
--- /dev/null
+++ b/.gitea/workflows/endpoint-sync.yaml
@@ -0,0 +1,22 @@
+name: Endpoint Sync Check
+
+on:
+ schedule:
+ - cron: '0 9 * * 1'
+ push:
+ paths:
+ - 'openapi.yaml'
+ workflow_dispatch:
+
+jobs:
+ check-drift:
+ runs-on: ubuntu-latest
+ steps:
+ - uses: actions/checkout@v4
+
+ - uses: actions/setup-go@v5
+ with:
+ go-version-file: go.mod
+
+ - name: Check endpoint drift
+ run: go run ./scripts/check-endpoint-drift.go
diff --git a/.gitea/workflows/release.yaml b/.gitea/workflows/release.yaml
new file mode 100644
index 0000000..7261aa1
--- /dev/null
+++ b/.gitea/workflows/release.yaml
@@ -0,0 +1,49 @@
+name: Release
+
+on:
+ push:
+ tags:
+ - 'v*'
+
+permissions:
+ contents: write
+
+jobs:
+ release:
+ runs-on: ubuntu-latest
+ steps:
+ - uses: actions/checkout@v4
+ with:
+ fetch-depth: 0
+
+ - uses: actions/setup-go@v5
+ with:
+ go-version-file: go.mod
+
+ - name: Import GPG key
+ id: import_gpg
+ uses: crazy-max/ghaction-import-gpg@v6
+ with:
+ gpg_private_key: ${{ secrets.GPG_PRIVATE_KEY }}
+ passphrase: ${{ secrets.PASSPHRASE }}
+
+ - name: Check endpoint drift
+ run: go run ./scripts/check-endpoint-drift.go
+
+ - name: Run GoReleaser
+ uses: goreleaser/goreleaser-action@v6
+ with:
+ args: release --clean
+ env:
+ GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
+ GPG_FINGERPRINT: ${{ steps.import_gpg.outputs.fingerprint }}
+
+ - name: Create Gitea release
+ if: success()
+ run: |
+ TAG="${GITHUB_REF#refs/tags/}"
+ curl -s -X POST \
+ -H "Authorization: token ${{ secrets.GITEA_TOKEN }}" \
+ -H "Content-Type: application/json" \
+ -d "{\"tag_name\": \"${TAG}\", \"name\": \"${TAG}\", \"body\": \"Release ${TAG} — see GitHub mirror for artifacts.\"}" \
+ "${{ github.server_url }}/api/v1/repos/${{ github.repository }}/releases"
diff --git a/.gitea/workflows/version-check.yaml b/.gitea/workflows/version-check.yaml
new file mode 100644
index 0000000..853da54
--- /dev/null
+++ b/.gitea/workflows/version-check.yaml
@@ -0,0 +1,25 @@
+name: Version Check
+
+on:
+ pull_request:
+ paths:
+ - 'main.go'
+ - 'internal/**'
+
+jobs:
+ check-version:
+ runs-on: ubuntu-latest
+ steps:
+ - uses: actions/checkout@v4
+ with:
+ fetch-depth: 0
+
+ - name: Check version bump
+ run: |
+ BASE_VERSION=$(git show origin/${{ github.base_ref }}:main.go | grep -oP 'version string = "\K[^"]+')
+ HEAD_VERSION=$(grep -oP 'version string = "\K[^"]+' main.go)
+ if [ "$BASE_VERSION" = "$HEAD_VERSION" ]; then
+ echo "::warning::Version in main.go has not been bumped (still ${HEAD_VERSION}). Consider updating it for this release."
+ else
+ echo "Version bumped: ${BASE_VERSION} → ${HEAD_VERSION}"
+ fi
diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS
deleted file mode 100644
index 922ee27..0000000
--- a/.github/CODEOWNERS
+++ /dev/null
@@ -1 +0,0 @@
-* @hashicorp/terraform-devex
diff --git a/.github/CODE_OF_CONDUCT.md b/.github/CODE_OF_CONDUCT.md
deleted file mode 100644
index 0c8b092..0000000
--- a/.github/CODE_OF_CONDUCT.md
+++ /dev/null
@@ -1,5 +0,0 @@
-# Code of Conduct
-
-HashiCorp Community Guidelines apply to you when interacting with the community here on GitHub and contributing code.
-
-Please read the full text at https://www.hashicorp.com/community-guidelines
diff --git a/.github/ISSUE_TEMPLATE/bug_report.md b/.github/ISSUE_TEMPLATE/bug_report.md
deleted file mode 100644
index dd84ea7..0000000
--- a/.github/ISSUE_TEMPLATE/bug_report.md
+++ /dev/null
@@ -1,38 +0,0 @@
----
-name: Bug report
-about: Create a report to help us improve
-title: ''
-labels: ''
-assignees: ''
-
----
-
-**Describe the bug**
-A clear and concise description of what the bug is.
-
-**To Reproduce**
-Steps to reproduce the behavior:
-1. Go to '...'
-2. Click on '....'
-3. Scroll down to '....'
-4. See error
-
-**Expected behavior**
-A clear and concise description of what you expected to happen.
-
-**Screenshots**
-If applicable, add screenshots to help explain your problem.
-
-**Desktop (please complete the following information):**
- - OS: [e.g. iOS]
- - Browser [e.g. chrome, safari]
- - Version [e.g. 22]
-
-**Smartphone (please complete the following information):**
- - Device: [e.g. iPhone6]
- - OS: [e.g. iOS8.1]
- - Browser [e.g. stock browser, safari]
- - Version [e.g. 22]
-
-**Additional context**
-Add any other context about the problem here.
diff --git a/.github/ISSUE_TEMPLATE/feature_request.md b/.github/ISSUE_TEMPLATE/feature_request.md
deleted file mode 100644
index bbcbbe7..0000000
--- a/.github/ISSUE_TEMPLATE/feature_request.md
+++ /dev/null
@@ -1,20 +0,0 @@
----
-name: Feature request
-about: Suggest an idea for this project
-title: ''
-labels: ''
-assignees: ''
-
----
-
-**Is your feature request related to a problem? Please describe.**
-A clear and concise description of what the problem is. Ex. I'm always frustrated when [...]
-
-**Describe the solution you'd like**
-A clear and concise description of what you want to happen.
-
-**Describe alternatives you've considered**
-A clear and concise description of any alternative solutions or features you've considered.
-
-**Additional context**
-Add any other context or screenshots about the feature request here.
diff --git a/.github/dependabot.yml b/.github/dependabot.yml
deleted file mode 100644
index af374dd..0000000
--- a/.github/dependabot.yml
+++ /dev/null
@@ -1,8 +0,0 @@
-# See GitHub's documentation for more information on this file:
-# https://docs.github.com/en/code-security/supply-chain-security/keeping-your-dependencies-updated-automatically/configuration-options-for-dependency-updates
-version: 2
-updates:
- - package-ecosystem: "gomod"
- directory: "/"
- schedule:
- interval: "daily"
diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml
deleted file mode 100644
index e08c110..0000000
--- a/.github/workflows/release.yml
+++ /dev/null
@@ -1,41 +0,0 @@
-# Terraform Provider release workflow.
-name: Release
-
-# This GitHub action creates a release when a tag that matches the pattern
-# "v*" (e.g. v0.1.0) is created.
-on:
- push:
- tags:
- - 'v*'
-
-# Releases need permissions to read and write the repository contents.
-# GitHub considers creating releases and uploading assets as writing contents.
-permissions:
- contents: write
-
-jobs:
- goreleaser:
- runs-on: ubuntu-latest
- steps:
- - uses: actions/checkout@c85c95e3d7251135ab7dc9ce3241c5835cc595a9 # v3.5.3
- with:
- # Allow goreleaser to access older tag information.
- fetch-depth: 0
- - uses: actions/setup-go@93397bea11091df50f3d7e59dc26a7711a8bcfbe # v4.1.0
- with:
- go-version-file: 'go.mod'
- cache: true
- - name: Import GPG key
- uses: crazy-max/ghaction-import-gpg@72b6676b71ab476b77e676928516f6982eef7a41 # v5.3.0
- id: import_gpg
- with:
- gpg_private_key: ${{ secrets.GPG_PRIVATE_KEY }}
- passphrase: ${{ secrets.PASSPHRASE }}
- - name: Run GoReleaser
- uses: goreleaser/goreleaser-action@336e29918d653399e599bfca99fadc1d7ffbc9f7 # v4.3.0
- with:
- args: release --clean
- env:
- # GitHub sets the GITHUB_TOKEN secret automatically.
- GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
- GPG_FINGERPRINT: ${{ steps.import_gpg.outputs.fingerprint }}
diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml
deleted file mode 100644
index af0b6fe..0000000
--- a/.github/workflows/test.yml
+++ /dev/null
@@ -1,81 +0,0 @@
-# Terraform Provider testing workflow.
-name: Tests
-
-# This GitHub action runs your tests for each pull request and push.
-# Optionally, you can turn it on using a schedule for regular testing.
-on:
- pull_request:
- paths-ignore:
- - 'README.md'
- push:
- paths-ignore:
- - 'README.md'
-
-# Testing only needs permissions to read the repository contents.
-permissions:
- contents: read
-
-jobs:
- # Ensure project builds before running testing matrix
- build:
- name: Build
- runs-on: ubuntu-latest
- timeout-minutes: 5
- steps:
- - uses: actions/checkout@c85c95e3d7251135ab7dc9ce3241c5835cc595a9 # v3.5.3
- - uses: actions/setup-go@93397bea11091df50f3d7e59dc26a7711a8bcfbe # v4.1.0
- with:
- go-version-file: 'go.mod'
- cache: true
- - run: go mod download
- - run: go build -v .
- - name: Run linters
- uses: golangci/golangci-lint-action@639cd343e1d3b897ff35927a75193d57cfcba299 # v3.6.0
- with:
- version: latest
-
- generate:
- runs-on: ubuntu-latest
- steps:
- - uses: actions/checkout@c85c95e3d7251135ab7dc9ce3241c5835cc595a9 # v3.5.3
- - uses: actions/setup-go@93397bea11091df50f3d7e59dc26a7711a8bcfbe # v4.1.0
- with:
- go-version-file: 'go.mod'
- cache: true
- - run: go generate ./...
- - name: git diff
- run: |
- git diff --compact-summary --exit-code || \
- (echo; echo "Unexpected difference in directories after code generation. Run 'go generate ./...' command and commit."; exit 1)
-
- # Run acceptance tests in a matrix with Terraform CLI versions
- test:
- name: Terraform Provider Acceptance Tests
- needs: build
- runs-on: ubuntu-latest
- timeout-minutes: 15
- strategy:
- fail-fast: false
- matrix:
- # list whatever Terraform versions here you would like to support
- terraform:
- - '1.0.*'
- - '1.1.*'
- - '1.2.*'
- - '1.3.*'
- - '1.4.*'
- steps:
- - uses: actions/checkout@c85c95e3d7251135ab7dc9ce3241c5835cc595a9 # v3.5.3
- - uses: actions/setup-go@93397bea11091df50f3d7e59dc26a7711a8bcfbe # v4.1.0
- with:
- go-version-file: 'go.mod'
- cache: true
- - uses: hashicorp/setup-terraform@633666f66e0061ca3b725c73b2ec20cd13a8fdd1 # v2.0.3
- with:
- terraform_version: ${{ matrix.terraform }}
- terraform_wrapper: false
- - run: go mod download
- - env:
- TF_ACC: "1"
- run: go test -v -cover ./internal/provider/
- timeout-minutes: 10
diff --git a/.gitignore b/.gitignore
index fd3ad8e..44b9c97 100644
--- a/.gitignore
+++ b/.gitignore
@@ -31,5 +31,8 @@ website/vendor
!command/test-fixtures/**/*.tfstate
!command/test-fixtures/**/.terraform/
+# Local test directory
+test/
+
# Keep windows files with windows line endings
*.winfile eol=crlf
diff --git a/.golangci.yml b/.golangci.yml
index 223cf95..52d0a87 100644
--- a/.golangci.yml
+++ b/.golangci.yml
@@ -1,5 +1,3 @@
-# Visit https://golangci-lint.run/ for usage documentation
-# and information on other useful linters
issues:
max-per-linter: 0
max-same-issues: 0
@@ -9,13 +7,11 @@ linters:
enable:
- durationcheck
- errcheck
- - exportloopref
- forcetypeassert
- godot
- gofmt
- gosimple
- ineffassign
- - makezero
- misspell
- nilerr
- predeclared
@@ -24,4 +20,4 @@ linters:
- unconvert
- unparam
- unused
- - vet
\ No newline at end of file
+ - vet
diff --git a/.goreleaser.yml b/.goreleaser.yml
index 9bb0aa7..09d8f6c 100644
--- a/.goreleaser.yml
+++ b/.goreleaser.yml
@@ -1,60 +1,66 @@
-# Visit https://goreleaser.com for documentation on how to customize this
-# behavior.
+version: 2
+
before:
hooks:
- # this is just an example and not a requirement for provider building/publishing
- go mod tidy
+
builds:
-- env:
- # goreleaser does not work with CGO, it could also complicate
- # usage by users in CI/CD systems like Terraform Cloud where
- # they are unable to install libraries.
- - CGO_ENABLED=0
- mod_timestamp: '{{ .CommitTimestamp }}'
- flags:
- - -trimpath
- ldflags:
- - '-s -w -X main.version={{.Version}} -X main.commit={{.Commit}}'
- goos:
- - freebsd
- - windows
- - linux
- - darwin
- goarch:
- - amd64
- - '386'
- - arm
- - arm64
- ignore:
- - goos: darwin
- goarch: '386'
- binary: '{{ .ProjectName }}_v{{ .Version }}'
+ - env:
+ - CGO_ENABLED=0
+ mod_timestamp: '{{ .CommitTimestamp }}'
+ flags:
+ - -trimpath
+ ldflags:
+ - '-s -w -X main.version={{.Version}} -X main.commit={{.Commit}}'
+ goos:
+ - freebsd
+ - windows
+ - linux
+ - darwin
+ goarch:
+ - amd64
+ - '386'
+ - arm
+ - arm64
+ ignore:
+ - goos: darwin
+ goarch: '386'
+ binary: '{{ .ProjectName }}_v{{ .Version }}'
+
archives:
-- format: zip
- name_template: '{{ .ProjectName }}_{{ .Version }}_{{ .Os }}_{{ .Arch }}'
+ - format: zip
+ name_template: '{{ .ProjectName }}_{{ .Version }}_{{ .Os }}_{{ .Arch }}'
+
checksum:
extra_files:
- glob: 'terraform-registry-manifest.json'
name_template: '{{ .ProjectName }}_{{ .Version }}_manifest.json'
name_template: '{{ .ProjectName }}_{{ .Version }}_SHA256SUMS'
algorithm: sha256
+
signs:
- artifacts: checksum
args:
- # if you are using this in a GitHub action or some other automated pipeline, you
- # need to pass the batch flag to indicate its not interactive.
- "--batch"
- "--local-user"
- - "{{ .Env.GPG_FINGERPRINT }}" # set this environment variable for your signing key
+ - "{{ .Env.GPG_FINGERPRINT }}"
- "--output"
- "${signature}"
- "--detach-sign"
- "${artifact}"
+
release:
+ github:
+ owner: EZSCALE
+ name: terraform-provider-virtfusion
extra_files:
- glob: 'terraform-registry-manifest.json'
name_template: '{{ .ProjectName }}_{{ .Version }}_manifest.json'
- # If you want to manually examine the release before its live, uncomment this line:
- # draft: true
+
changelog:
- skip: true
+ sort: asc
+ filters:
+ exclude:
+ - '^docs:'
+ - '^test:'
+ - '^ci:'
diff --git a/GNUmakefile b/GNUmakefile
index 7771cd6..1a87b16 100644
--- a/GNUmakefile
+++ b/GNUmakefile
@@ -1,6 +1,25 @@
-default: testacc
+default: build
+
+.PHONY: build
+build:
+ go build -v ./...
+
+.PHONY: test
+test:
+ go test -race -v ./internal/...
-# Run acceptance tests
.PHONY: testacc
testacc:
TF_ACC=1 go test ./... -v $(TESTARGS) -timeout 120m
+
+.PHONY: lint
+lint:
+ golangci-lint run
+
+.PHONY: generate
+generate:
+ go generate ./...
+
+.PHONY: drift-check
+drift-check:
+ go run ./scripts/check-endpoint-drift.go
diff --git a/endpoint-manifest.json b/endpoint-manifest.json
new file mode 100644
index 0000000..f588bca
--- /dev/null
+++ b/endpoint-manifest.json
@@ -0,0 +1,506 @@
+[
+ {
+ "method": "GET",
+ "path": "/backups/server/{serverId}",
+ "summary": "Retrieve a server backups",
+ "tag": "Backups"
+ },
+ {
+ "method": "GET",
+ "path": "/compute/hypervisors",
+ "summary": "Retrieve hypervisors",
+ "tag": "Hypervisors"
+ },
+ {
+ "method": "GET",
+ "path": "/compute/hypervisors/{hypervisorId}",
+ "summary": "Retrive a Hypervisor",
+ "tag": "Hypervisors"
+ },
+ {
+ "method": "GET",
+ "path": "/compute/hypervisors/groups",
+ "summary": "Retrieve hypervisor groups",
+ "tag": "Hypervisor Groups"
+ },
+ {
+ "method": "GET",
+ "path": "/compute/hypervisors/groups/{hypervisorGroupId}",
+ "summary": "Retrieve a hypervisor group",
+ "tag": "Hypervisor Groups"
+ },
+ {
+ "method": "GET",
+ "path": "/compute/hypervisors/groups/{hypervisorGroupId}/resources",
+ "summary": "Retrieve a hypervisor groups resources",
+ "tag": "Hypervisor Groups"
+ },
+ {
+ "method": "GET",
+ "path": "/connect",
+ "summary": "Test connection",
+ "tag": "General"
+ },
+ {
+ "method": "GET",
+ "path": "/connectivity/ipblocks",
+ "summary": "Retrieve IP blocks",
+ "tag": "IP Blocks"
+ },
+ {
+ "method": "GET",
+ "path": "/connectivity/ipblocks/{blockId}",
+ "summary": "Retrieve an IP block",
+ "tag": "IP Blocks"
+ },
+ {
+ "method": "POST",
+ "path": "/connectivity/ipblocks/{blockId}/ipv4",
+ "summary": "Add an IPv4 range to an IP block",
+ "tag": "IP Blocks"
+ },
+ {
+ "method": "GET",
+ "path": "/dns/services/{serviceId}",
+ "summary": "Retrieve a DNS service",
+ "tag": "DNS"
+ },
+ {
+ "method": "GET",
+ "path": "/media/iso/{isoId}",
+ "summary": "Retrieve an ISO",
+ "tag": "Media"
+ },
+ {
+ "method": "GET",
+ "path": "/media/templates/fromServerPackageSpec/{serverPackageId}",
+ "summary": "Retrieve operating system templates that are available for a package",
+ "tag": "Media"
+ },
+ {
+ "method": "GET",
+ "path": "/packages",
+ "summary": "Retrieve packages",
+ "tag": "Packages"
+ },
+ {
+ "method": "GET",
+ "path": "/packages/{packageId}",
+ "summary": "Retrieve a packge",
+ "tag": "Packages"
+ },
+ {
+ "method": "GET",
+ "path": "/queue/{queueId}",
+ "summary": "Retrieve a queue item",
+ "tag": "Queue & Tasks"
+ },
+ {
+ "method": "PUT",
+ "path": "/selfService/access/byUserExtRelationId/{extRelationId}",
+ "summary": "Modify user access",
+ "tag": "Self Service/External Relational ID"
+ },
+ {
+ "method": "DELETE",
+ "path": "/selfService/credit/{creditId}",
+ "summary": "Cancel credit that was applied to a user",
+ "tag": "Self Service"
+ },
+ {
+ "method": "POST",
+ "path": "/selfService/credit/byUserExtRelationId/{extRelationId}",
+ "summary": "Add credit to user",
+ "tag": "Self Service/External Relational ID"
+ },
+ {
+ "method": "GET",
+ "path": "/selfService/currencies",
+ "summary": "Retrieve currencies",
+ "tag": "Self Service"
+ },
+ {
+ "method": "DELETE",
+ "path": "/selfService/hourlyGroupProfile/{profileId}/byUserExtRelationId/{extRelationId}",
+ "summary": "Remove hourly group profile from a user",
+ "tag": "Self Service/External Relational ID"
+ },
+ {
+ "method": "POST",
+ "path": "/selfService/hourlyGroupProfile/byUserExtRelationId/{extRelationId}",
+ "summary": "Add an hourly group profile to a user",
+ "tag": "Self Service/External Relational ID"
+ },
+ {
+ "method": "PUT",
+ "path": "/selfService/hourlyResourcePack/byUserExtRelationId/{extRelationId}",
+ "summary": "Set an hourly resource pack",
+ "tag": "Self Service/External Relational ID"
+ },
+ {
+ "method": "GET",
+ "path": "/selfService/hourlyStats/byUserExtRelationId/{extRelationId}",
+ "summary": "Retrieve hourly statistics",
+ "tag": "Self Service/External Relational ID"
+ },
+ {
+ "method": "GET",
+ "path": "/selfService/report/byUserExtRelationId/{extRelationId}",
+ "summary": "Generate a report",
+ "tag": "Self Service/External Relational ID"
+ },
+ {
+ "method": "DELETE",
+ "path": "/selfService/resourceGroupProfile/{profileId}/byUserExtRelationId/{extRelationId}",
+ "summary": "Remove resource group from a user",
+ "tag": "Self Service/External Relational ID"
+ },
+ {
+ "method": "POST",
+ "path": "/selfService/resourceGroupProfile/byUserExtRelationId/{extRelationId}",
+ "summary": "Add a resource group profile to a user",
+ "tag": "Self Service/External Relational ID"
+ },
+ {
+ "method": "DELETE",
+ "path": "/selfService/resourcePack/{packId}",
+ "summary": "Delete a user resource pack",
+ "tag": "Self Service"
+ },
+ {
+ "method": "GET",
+ "path": "/selfService/resourcePack/{packId}",
+ "summary": "Retrieve a user resource pack",
+ "tag": "Self Service"
+ },
+ {
+ "method": "PUT",
+ "path": "/selfService/resourcePack/{packId}",
+ "summary": "Modify user resource pack",
+ "tag": "Self Service"
+ },
+ {
+ "method": "POST",
+ "path": "/selfService/resourcePack/byUserExtRelationId/{extRelationId}",
+ "summary": "Add a resource pack to a user",
+ "tag": "Self Service/External Relational ID"
+ },
+ {
+ "method": "DELETE",
+ "path": "/selfService/resourcePackServers/{packId}",
+ "summary": "Delete all servers attached to a pack ID",
+ "tag": "Self Service"
+ },
+ {
+ "method": "POST",
+ "path": "/selfService/resourcePackServers/{packId}/suspend",
+ "summary": "Suspend all servers assigned to a reosurce pack",
+ "tag": "Self Service"
+ },
+ {
+ "method": "POST",
+ "path": "/selfService/resourcePackServers/{packId}/unsuspend",
+ "summary": "Unsuspend all servers assigned to a reosurce pack",
+ "tag": "Self Service"
+ },
+ {
+ "method": "GET",
+ "path": "/selfService/usage/byUserExtRelationId/{extRelationId}",
+ "summary": "Retrieve a users usage",
+ "tag": "Self Service/External Relational ID"
+ },
+ {
+ "method": "GET",
+ "path": "/servers",
+ "summary": "Retrieve servers",
+ "tag": "Servers"
+ },
+ {
+ "method": "POST",
+ "path": "/servers",
+ "summary": "Create a server",
+ "tag": "Servers"
+ },
+ {
+ "method": "DELETE",
+ "path": "/servers/{serverId}",
+ "summary": "Delete a server",
+ "tag": "Servers"
+ },
+ {
+ "method": "GET",
+ "path": "/servers/{serverId}",
+ "summary": "Retrieve a server",
+ "tag": "Servers"
+ },
+ {
+ "method": "PUT",
+ "path": "/servers/{serverId}/backups/plan/{planId}",
+ "summary": "Add, remove or modify a backup plan",
+ "tag": "Servers"
+ },
+ {
+ "method": "POST",
+ "path": "/servers/{serverId}/build",
+ "summary": "Build a server",
+ "tag": "Servers"
+ },
+ {
+ "method": "POST",
+ "path": "/servers/{serverId}/customXML",
+ "summary": "Set custom XML",
+ "tag": "Servers"
+ },
+ {
+ "method": "GET",
+ "path": "/servers/{serverId}/firewall/{interface}",
+ "summary": "Retrieve firewall",
+ "tag": "Servers/Network/Firewall"
+ },
+ {
+ "method": "POST",
+ "path": "/servers/{serverId}/firewall/{interface}/disable",
+ "summary": "Disable firewall",
+ "tag": "Servers/Network/Firewall"
+ },
+ {
+ "method": "POST",
+ "path": "/servers/{serverId}/firewall/{interface}/enable",
+ "summary": "Enable firewall",
+ "tag": "Servers/Network/Firewall"
+ },
+ {
+ "method": "POST",
+ "path": "/servers/{serverId}/firewall/{interface}/rules",
+ "summary": "Apply firewall rulesets",
+ "tag": "Servers/Network/Firewall"
+ },
+ {
+ "method": "DELETE",
+ "path": "/servers/{serverId}/ipv4",
+ "summary": "Remove an array of IPv4 addresses",
+ "tag": "Servers/Network"
+ },
+ {
+ "method": "POST",
+ "path": "/servers/{serverId}/ipv4",
+ "summary": "Add an array of IPv4 addresses",
+ "tag": "Servers/Network"
+ },
+ {
+ "method": "POST",
+ "path": "/servers/{serverId}/ipv4Qty",
+ "summary": "Add a quantity of IPv4 addresses",
+ "tag": "Servers/Network"
+ },
+ {
+ "method": "PUT",
+ "path": "/servers/{serverId}/modify/cpuCores",
+ "summary": "Modify CPU cores",
+ "tag": "Servers"
+ },
+ {
+ "method": "PUT",
+ "path": "/servers/{serverId}/modify/cpuThrottle",
+ "summary": "Throttle a servers CPU",
+ "tag": "Servers"
+ },
+ {
+ "method": "PUT",
+ "path": "/servers/{serverId}/modify/memory",
+ "summary": "Modify memory",
+ "tag": "Servers"
+ },
+ {
+ "method": "PUT",
+ "path": "/servers/{serverId}/modify/name",
+ "summary": "Modify name",
+ "tag": "Servers"
+ },
+ {
+ "method": "PUT",
+ "path": "/servers/{serverId}/modify/traffic",
+ "summary": "Modify primary traffic allowance",
+ "tag": "Servers/Network/Traffic"
+ },
+ {
+ "method": "DELETE",
+ "path": "/servers/{serverId}/networkWhitelist",
+ "summary": "Remove an address from the whitelist",
+ "tag": "Servers/Network"
+ },
+ {
+ "method": "POST",
+ "path": "/servers/{serverId}/networkWhitelist",
+ "summary": "Add an address to the whitelist",
+ "tag": "Servers/Network"
+ },
+ {
+ "method": "PUT",
+ "path": "/servers/{serverId}/owner/{newOwnerId}",
+ "summary": "Change owner",
+ "tag": "Servers"
+ },
+ {
+ "method": "PUT",
+ "path": "/servers/{serverId}/package/{packageId}",
+ "summary": "Change a server package",
+ "tag": "Servers"
+ },
+ {
+ "method": "POST",
+ "path": "/servers/{serverId}/power/boot",
+ "summary": "Boot a server",
+ "tag": "Servers/Power"
+ },
+ {
+ "method": "POST",
+ "path": "/servers/{serverId}/power/poweroff",
+ "summary": "Poweroff a server",
+ "tag": "Servers/Power"
+ },
+ {
+ "method": "POST",
+ "path": "/servers/{serverId}/power/restart",
+ "summary": "Restart a server",
+ "tag": "Servers/Power"
+ },
+ {
+ "method": "POST",
+ "path": "/servers/{serverId}/power/shutdown",
+ "summary": "Shutdown a server",
+ "tag": "Servers/Power"
+ },
+ {
+ "method": "POST",
+ "path": "/servers/{serverId}/resetPassword",
+ "summary": "Reset a server password",
+ "tag": "Servers"
+ },
+ {
+ "method": "POST",
+ "path": "/servers/{serverId}/suspend",
+ "summary": "Suspend a server",
+ "tag": "Servers"
+ },
+ {
+ "method": "GET",
+ "path": "/servers/{serverId}/templates",
+ "summary": "Retrieve OS templates available to a server",
+ "tag": "Servers"
+ },
+ {
+ "method": "GET",
+ "path": "/servers/{serverId}/traffic",
+ "summary": "Retrieve a servers traffic statistics",
+ "tag": "Servers"
+ },
+ {
+ "method": "GET",
+ "path": "/servers/{serverId}/traffic/blocks",
+ "summary": "Retrieve a servers traffic blocks",
+ "tag": "Servers/Network/Traffic"
+ },
+ {
+ "method": "POST",
+ "path": "/servers/{serverId}/traffic/blocks",
+ "summary": "Add a traffic block to a server",
+ "tag": "Servers/Network/Traffic"
+ },
+ {
+ "method": "DELETE",
+ "path": "/servers/{serverId}/traffic/blocks/{blockId}",
+ "summary": "Remove a traffic block from a server",
+ "tag": "Servers/Network/Traffic"
+ },
+ {
+ "method": "POST",
+ "path": "/servers/{serverId}/unsuspend",
+ "summary": "Unsuspend a server",
+ "tag": "Servers"
+ },
+ {
+ "method": "GET",
+ "path": "/servers/{serverId}/vnc",
+ "summary": "Retrive VNC details",
+ "tag": "Servers"
+ },
+ {
+ "method": "POST",
+ "path": "/servers/{serverId}/vnc",
+ "summary": "Enable or disable VNC",
+ "tag": "Servers"
+ },
+ {
+ "method": "GET",
+ "path": "/servers/user/{userId}",
+ "summary": "Retrieve a users servers",
+ "tag": "Servers"
+ },
+ {
+ "method": "POST",
+ "path": "/ssh_keys",
+ "summary": "Add an SSH key to a user account",
+ "tag": "SSH Keys"
+ },
+ {
+ "method": "DELETE",
+ "path": "/ssh_keys/{keyId}",
+ "summary": "Delete an SSH key from a user",
+ "tag": "SSH Keys"
+ },
+ {
+ "method": "GET",
+ "path": "/ssh_keys/{keyId}",
+ "summary": "Retrieve an SSH key",
+ "tag": "SSH Keys"
+ },
+ {
+ "method": "GET",
+ "path": "/ssh_keys/user/{userId}",
+ "summary": "Retrieve a users SSH keys",
+ "tag": "SSH Keys"
+ },
+ {
+ "method": "POST",
+ "path": "/users",
+ "summary": "Create a user",
+ "tag": "Users"
+ },
+ {
+ "method": "POST",
+ "path": "/users/{extRelationId}/authenticationTokens",
+ "summary": "Generate a set of login tokens",
+ "tag": "Users/External Rel ID & Rel Str"
+ },
+ {
+ "method": "DELETE",
+ "path": "/users/{extRelationId}/byExtRelation",
+ "summary": "Delete a user",
+ "tag": "Users/External Rel ID & Rel Str"
+ },
+ {
+ "method": "GET",
+ "path": "/users/{extRelationId}/byExtRelation",
+ "summary": "Retrieve a user",
+ "tag": "Users/External Rel ID & Rel Str"
+ },
+ {
+ "method": "PUT",
+ "path": "/users/{extRelationId}/byExtRelation",
+ "summary": "Modify a user",
+ "tag": "Users/External Rel ID & Rel Str"
+ },
+ {
+ "method": "POST",
+ "path": "/users/{extRelationId}/byExtRelation/resetPassword",
+ "summary": "Change a user passowrd",
+ "tag": "Users/External Rel ID & Rel Str"
+ },
+ {
+ "method": "POST",
+ "path": "/users/{extRelationId}/serverAuthenticationTokens/{serverId}",
+ "summary": "Generate a set of loging tokens using a server ID",
+ "tag": "Users/External Rel ID & Rel Str"
+ }
+]
diff --git a/examples/data-sources/scaffolding_example/data-source.tf b/examples/data-sources/scaffolding_example/data-source.tf
deleted file mode 100644
index a852489..0000000
--- a/examples/data-sources/scaffolding_example/data-source.tf
+++ /dev/null
@@ -1,3 +0,0 @@
-data "scaffolding_example" "example" {
- configurable_attribute = "some-value"
-}
diff --git a/examples/data-sources/virtfusion_hypervisors/data-source.tf b/examples/data-sources/virtfusion_hypervisors/data-source.tf
new file mode 100644
index 0000000..797b363
--- /dev/null
+++ b/examples/data-sources/virtfusion_hypervisors/data-source.tf
@@ -0,0 +1,5 @@
+data "virtfusion_hypervisors" "all" {}
+
+output "hypervisor_names" {
+ value = [for h in data.virtfusion_hypervisors.all.hypervisors : h.name]
+}
diff --git a/examples/data-sources/virtfusion_packages/data-source.tf b/examples/data-sources/virtfusion_packages/data-source.tf
new file mode 100644
index 0000000..829d3c6
--- /dev/null
+++ b/examples/data-sources/virtfusion_packages/data-source.tf
@@ -0,0 +1,5 @@
+data "virtfusion_packages" "all" {}
+
+output "package_names" {
+ value = [for p in data.virtfusion_packages.all.packages : p.name]
+}
diff --git a/examples/data-sources/virtfusion_server/data-source.tf b/examples/data-sources/virtfusion_server/data-source.tf
new file mode 100644
index 0000000..e71c18b
--- /dev/null
+++ b/examples/data-sources/virtfusion_server/data-source.tf
@@ -0,0 +1,7 @@
+data "virtfusion_server" "example" {
+ id = 1
+}
+
+output "server_name" {
+ value = data.virtfusion_server.example.name
+}
diff --git a/examples/provider/provider.tf b/examples/provider/provider.tf
index 7c7800a..76cbf9c 100644
--- a/examples/provider/provider.tf
+++ b/examples/provider/provider.tf
@@ -1,4 +1,9 @@
provider "virtfusion" {
- endpoint = "example.com"
- api_token = "myapikey"
+ endpoint = "https://cp.example.com"
+ api_token = var.virtfusion_api_token
+}
+
+variable "virtfusion_api_token" {
+ type = string
+ sensitive = true
}
diff --git a/examples/resources/virtfusion_build/resource.tf b/examples/resources/virtfusion_build/resource.tf
deleted file mode 100644
index 945313f..0000000
--- a/examples/resources/virtfusion_build/resource.tf
+++ /dev/null
@@ -1,10 +0,0 @@
-resource "virtfusion_build" "node1" {
- server_id = virtfusion_server.node1.id
- name = "node1-demo"
- hostname = "node1.example.com"
- osid = 1
- vnc = true
- ipv6 = true
- ssh_keys = [virtfusion_ssh.dummy_key.id]
- email = true
-}
diff --git a/examples/resources/virtfusion_server/resource.tf b/examples/resources/virtfusion_server/resource.tf
index 57659ea..3ba782c 100644
--- a/examples/resources/virtfusion_server/resource.tf
+++ b/examples/resources/virtfusion_server/resource.tf
@@ -12,3 +12,7 @@ resource "virtfusion_server" "node1" {
storage_profile = 1
network_profile = 1
}
+
+output "server_id" {
+ value = virtfusion_server.node1.id
+}
diff --git a/examples/resources/virtfusion_server_build/resource.tf b/examples/resources/virtfusion_server_build/resource.tf
new file mode 100644
index 0000000..041447d
--- /dev/null
+++ b/examples/resources/virtfusion_server_build/resource.tf
@@ -0,0 +1,10 @@
+resource "virtfusion_server_build" "node1_build" {
+ server_id = virtfusion_server.node1.id
+ name = "my-server"
+ hostname = "my-server.example.com"
+ osid = 1
+ vnc = true
+ ipv6 = false
+ ssh_keys = [virtfusion_ssh_key.mykey.id]
+ email = false
+}
diff --git a/examples/resources/virtfusion_ssh/resource.tf b/examples/resources/virtfusion_ssh/resource.tf
deleted file mode 100644
index 09f6b12..0000000
--- a/examples/resources/virtfusion_ssh/resource.tf
+++ /dev/null
@@ -1,9 +0,0 @@
-resource "virtfusion_ssh" "dummy_key" {
- # This is what is displayed in the UI on the SSH keys page.
- name = "dummy_key"
-
- public_key = "ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAACAQCRM5gzj6BpVbTEZ8XX5meQOC9X+znTMCQbXTfdqm9IP3HY2JbqH+yfCBWSsLpXim6WvsYtfkAhrtrkdmaX66Wn1uo6XvARwi/5D1VRTM94vwoitJb0rne4OorpwGIGCpDIi1iRA/ERIbAIQpw/2PJfm7q+fEj9TS+n/MzYOOmwTaKPEJ8+wHwXbjcSNoBQmEPonafbQKQN5PXe5rwnTNAqJWhGPHqF2t7lvZy+m7Sl7X1vUVlw+7iZzOVm9iDXmUInc8A0kz18l/O+4ELhRxxzjmSX5/KkN0GG7wS7CHlq9MS2741MS6p0ZNMgTT/04RfsY5JXoOa1gCeAdnXQST9ylvBd6hXubV95lRM8AXAhEJFHpa0Xn1gHMJ4F0cjjvmBIDx39QztuYsNJPk8veBBQwhOzhnJ3Zh2IYTQD+Mwu5yUrJzUt7ia8X5fhjbrYlfUgdH+siBbvJRzyXwnZdHArher55U4xPCJO4qRrFr72Jn+WGzkcY53oLnW5K3NnPaYViCJD2BgJZU1YF8oA3RyEG+2GS7Ksqs2nXXlZ1c+RXLUXM0pxDrwqvYrE3Ae+O/PtZ0cqpesyjxDfH/R2cj86jjdEi7S8nhgkumHwkoac8LCJnoAeC9S7sxmI99VBHcNwCazx3ZL2UAI3Ik/DQBZXcCPXw9MfY25SyQwEYftMKw== dummy_key"
-
- # This is the user ID that the key will be associated with.
- user_id = 1
-}
\ No newline at end of file
diff --git a/examples/resources/virtfusion_ssh_key/resource.tf b/examples/resources/virtfusion_ssh_key/resource.tf
new file mode 100644
index 0000000..0d7085e
--- /dev/null
+++ b/examples/resources/virtfusion_ssh_key/resource.tf
@@ -0,0 +1,9 @@
+resource "virtfusion_ssh_key" "mykey" {
+ user_id = 1
+ name = "my-ssh-key"
+ public_key = "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIExample user@example.com"
+}
+
+output "ssh_key_id" {
+ value = virtfusion_ssh_key.mykey.id
+}
diff --git a/examples/resources/virtfusion_user/resource.tf b/examples/resources/virtfusion_user/resource.tf
new file mode 100644
index 0000000..82db8c4
--- /dev/null
+++ b/examples/resources/virtfusion_user/resource.tf
@@ -0,0 +1,5 @@
+resource "virtfusion_user" "customer1" {
+ name = "John Doe"
+ email = "john@example.com"
+ ext_relation_id = "cust-12345"
+}
diff --git a/go.mod b/go.mod
index 3e61493..e5272a5 100644
--- a/go.mod
+++ b/go.mod
@@ -1,63 +1,71 @@
module terraform-provider-virtfusion
-go 1.19
+go 1.23
require (
- github.com/hashicorp/terraform-plugin-docs v0.16.0
- github.com/hashicorp/terraform-plugin-framework v1.3.5
+ github.com/hashicorp/terraform-plugin-docs v0.19.4
+ github.com/hashicorp/terraform-plugin-framework v1.13.0
)
require (
+ github.com/BurntSushi/toml v1.2.1 // indirect
+ github.com/Kunde21/markdownfmt/v3 v3.1.0 // indirect
github.com/Masterminds/goutils v1.1.1 // indirect
- github.com/Masterminds/semver/v3 v3.1.1 // indirect
- github.com/Masterminds/sprig/v3 v3.2.2 // indirect
- github.com/ProtonMail/go-crypto v0.0.0-20230217124315-7d5c6f04bbb8 // indirect
- github.com/apparentlymart/go-textseg/v13 v13.0.0 // indirect
+ github.com/Masterminds/semver/v3 v3.2.0 // indirect
+ github.com/Masterminds/sprig/v3 v3.2.3 // indirect
+ github.com/ProtonMail/go-crypto v1.1.0-alpha.2 // indirect
+ github.com/apparentlymart/go-textseg/v15 v15.0.0 // indirect
github.com/armon/go-radix v1.0.0 // indirect
github.com/bgentry/speakeasy v0.1.0 // indirect
- github.com/cloudflare/circl v1.3.3 // indirect
- github.com/fatih/color v1.13.0 // indirect
- github.com/golang/protobuf v1.5.3 // indirect
- github.com/google/uuid v1.3.0 // indirect
+ github.com/bmatcuk/doublestar/v4 v4.6.1 // indirect
+ github.com/cloudflare/circl v1.3.7 // indirect
+ github.com/fatih/color v1.16.0 // indirect
+ github.com/golang/protobuf v1.5.4 // indirect
+ github.com/google/uuid v1.6.0 // indirect
+ github.com/hashicorp/cli v1.1.6 // indirect
github.com/hashicorp/errwrap v1.1.0 // indirect
github.com/hashicorp/go-checkpoint v0.5.0 // indirect
github.com/hashicorp/go-cleanhttp v0.5.2 // indirect
github.com/hashicorp/go-hclog v1.5.0 // indirect
github.com/hashicorp/go-multierror v1.1.1 // indirect
- github.com/hashicorp/go-plugin v1.4.10 // indirect
+ github.com/hashicorp/go-plugin v1.6.2 // indirect
github.com/hashicorp/go-uuid v1.0.3 // indirect
- github.com/hashicorp/go-version v1.6.0 // indirect
- github.com/hashicorp/hc-install v0.5.2 // indirect
- github.com/hashicorp/terraform-exec v0.18.1 // indirect
- github.com/hashicorp/terraform-json v0.17.1 // indirect
- github.com/hashicorp/terraform-plugin-go v0.18.0 // indirect
+ github.com/hashicorp/go-version v1.7.0 // indirect
+ github.com/hashicorp/hc-install v0.7.0 // indirect
+ github.com/hashicorp/terraform-exec v0.21.0 // indirect
+ github.com/hashicorp/terraform-json v0.22.1 // indirect
+ github.com/hashicorp/terraform-plugin-go v0.25.0 // indirect
github.com/hashicorp/terraform-plugin-log v0.9.0 // indirect
- github.com/hashicorp/terraform-registry-address v0.2.1 // indirect
+ github.com/hashicorp/terraform-registry-address v0.2.3 // indirect
github.com/hashicorp/terraform-svchost v0.1.1 // indirect
- github.com/hashicorp/yamux v0.0.0-20181012175058-2f1d1f20f75d // indirect
- github.com/huandu/xstrings v1.3.2 // indirect
- github.com/imdario/mergo v0.3.13 // indirect
+ github.com/hashicorp/yamux v0.1.1 // indirect
+ github.com/huandu/xstrings v1.3.3 // indirect
+ github.com/imdario/mergo v0.3.15 // indirect
github.com/mattn/go-colorable v0.1.13 // indirect
- github.com/mattn/go-isatty v0.0.16 // indirect
- github.com/mitchellh/cli v1.1.5 // indirect
+ github.com/mattn/go-isatty v0.0.20 // indirect
+ github.com/mattn/go-runewidth v0.0.9 // indirect
github.com/mitchellh/copystructure v1.2.0 // indirect
github.com/mitchellh/go-testing-interface v1.14.1 // indirect
github.com/mitchellh/reflectwalk v1.0.2 // indirect
github.com/oklog/run v1.0.0 // indirect
github.com/posener/complete v1.2.3 // indirect
- github.com/russross/blackfriday v1.6.0 // indirect
github.com/shopspring/decimal v1.3.1 // indirect
github.com/spf13/cast v1.5.0 // indirect
- github.com/vmihailenco/msgpack/v5 v5.3.5 // indirect
+ github.com/vmihailenco/msgpack/v5 v5.4.1 // indirect
github.com/vmihailenco/tagparser/v2 v2.0.0 // indirect
- github.com/zclconf/go-cty v1.13.2 // indirect
- golang.org/x/crypto v0.14.0 // indirect
+ github.com/yuin/goldmark v1.7.1 // indirect
+ github.com/yuin/goldmark-meta v1.1.0 // indirect
+ github.com/zclconf/go-cty v1.14.4 // indirect
+ go.abhg.dev/goldmark/frontmatter v0.2.0 // indirect
+ golang.org/x/crypto v0.26.0 // indirect
golang.org/x/exp v0.0.0-20230626212559-97b1e661b5df // indirect
- golang.org/x/mod v0.11.0 // indirect
- golang.org/x/net v0.17.0 // indirect
- golang.org/x/sys v0.13.0 // indirect
- golang.org/x/text v0.13.0 // indirect
- google.golang.org/genproto v0.0.0-20230410155749-daa745c078e1 // indirect
- google.golang.org/grpc v1.56.1 // indirect
- google.golang.org/protobuf v1.31.0 // indirect
+ golang.org/x/mod v0.17.0 // indirect
+ golang.org/x/net v0.28.0 // indirect
+ golang.org/x/sys v0.24.0 // indirect
+ golang.org/x/text v0.17.0 // indirect
+ google.golang.org/genproto/googleapis/rpc v0.0.0-20240814211410-ddb44dafa142 // indirect
+ google.golang.org/grpc v1.67.1 // indirect
+ google.golang.org/protobuf v1.35.1 // indirect
+ gopkg.in/yaml.v2 v2.3.0 // indirect
+ gopkg.in/yaml.v3 v3.0.1 // indirect
)
diff --git a/go.sum b/go.sum
index 70f2a1d..0b87309 100644
--- a/go.sum
+++ b/go.sum
@@ -1,45 +1,60 @@
+dario.cat/mergo v1.0.0 h1:AGCNq9Evsj31mOgNPcLyXc+4PNABt905YmuqPYYpBWk=
+dario.cat/mergo v1.0.0/go.mod h1:uNxQE+84aUszobStD9th8a29P2fMDhsBdgRYvZOxGmk=
+github.com/BurntSushi/toml v1.2.1 h1:9F2/+DoOYIOksmaJFPw1tGFy1eDnIJXg+UHjuD8lTak=
+github.com/BurntSushi/toml v1.2.1/go.mod h1:CxXYINrC8qIiEnFrOxCa7Jy5BFHlXnUU2pbicEuybxQ=
+github.com/Kunde21/markdownfmt/v3 v3.1.0 h1:KiZu9LKs+wFFBQKhrZJrFZwtLnCCWJahL+S+E/3VnM0=
+github.com/Kunde21/markdownfmt/v3 v3.1.0/go.mod h1:tPXN1RTyOzJwhfHoon9wUr4HGYmWgVxSQN6VBJDkrVc=
github.com/Masterminds/goutils v1.1.1 h1:5nUrii3FMTL5diU80unEVvNevw1nH4+ZV4DSLVJLSYI=
github.com/Masterminds/goutils v1.1.1/go.mod h1:8cTjp+g8YejhMuvIA5y2vz3BpJxksy863GQaJW2MFNU=
-github.com/Masterminds/semver/v3 v3.1.1 h1:hLg3sBzpNErnxhQtUy/mmLR2I9foDujNK030IGemrRc=
-github.com/Masterminds/semver/v3 v3.1.1/go.mod h1:VPu/7SZ7ePZ3QOrcuXROw5FAcLl4a0cBrbBpGY/8hQs=
-github.com/Masterminds/sprig/v3 v3.2.1/go.mod h1:UoaO7Yp8KlPnJIYWTFkMaqPUYKTfGFPhxNuwnnxkKlk=
-github.com/Masterminds/sprig/v3 v3.2.2 h1:17jRggJu518dr3QaafizSXOjKYp94wKfABxUmyxvxX8=
-github.com/Masterminds/sprig/v3 v3.2.2/go.mod h1:UoaO7Yp8KlPnJIYWTFkMaqPUYKTfGFPhxNuwnnxkKlk=
-github.com/Microsoft/go-winio v0.5.2 h1:a9IhgEQBCUEk6QCdml9CiJGhAws+YwffDHEMp1VMrpA=
-github.com/ProtonMail/go-crypto v0.0.0-20230217124315-7d5c6f04bbb8 h1:wPbRQzjjwFc0ih8puEVAOFGELsn1zoIIYdxvML7mDxA=
-github.com/ProtonMail/go-crypto v0.0.0-20230217124315-7d5c6f04bbb8/go.mod h1:I0gYDMZ6Z5GRU7l58bNFSkPTFN6Yl12dsUlAZ8xy98g=
-github.com/acomagu/bufpipe v1.0.4 h1:e3H4WUzM3npvo5uv95QuJM3cQspFNtFBzvJ2oNjKIDQ=
-github.com/apparentlymart/go-textseg/v13 v13.0.0 h1:Y+KvPE1NYz0xl601PVImeQfFyEy6iT90AvPUL1NNfNw=
-github.com/apparentlymart/go-textseg/v13 v13.0.0/go.mod h1:ZK2fH7c4NqDTLtiYLvIkEghdlcqw7yxLeM89kiTRPUo=
-github.com/armon/go-radix v0.0.0-20180808171621-7fddfc383310/go.mod h1:ufUuZ+zHj4x4TnLV4JWEpy2hxWSpsRywHrMgIH9cCH8=
+github.com/Masterminds/semver/v3 v3.2.0 h1:3MEsd0SM6jqZojhjLWWeBY+Kcjy9i6MQAeY7YgDP83g=
+github.com/Masterminds/semver/v3 v3.2.0/go.mod h1:qvl/7zhW3nngYb5+80sSMF+FG2BjYrf8m9wsX0PNOMQ=
+github.com/Masterminds/sprig/v3 v3.2.3 h1:eL2fZNezLomi0uOLqjQoN6BfsDD+fyLtgbJMAj9n6YA=
+github.com/Masterminds/sprig/v3 v3.2.3/go.mod h1:rXcFaZ2zZbLRJv/xSysmlgIM1u11eBaRMhvYXJNkGuM=
+github.com/Microsoft/go-winio v0.6.1 h1:9/kr64B9VUZrLm5YYwbGtUJnMgqWVOdUAXu6Migciow=
+github.com/Microsoft/go-winio v0.6.1/go.mod h1:LRdKpFKfdobln8UmuiYcKPot9D2v6svN5+sAH+4kjUM=
+github.com/ProtonMail/go-crypto v1.1.0-alpha.2 h1:bkyFVUP+ROOARdgCiJzNQo2V2kiB97LyUpzH9P6Hrlg=
+github.com/ProtonMail/go-crypto v1.1.0-alpha.2/go.mod h1:rA3QumHc/FZ8pAHreoekgiAbzpNsfQAosU5td4SnOrE=
+github.com/apparentlymart/go-textseg/v15 v15.0.0 h1:uYvfpb3DyLSCGWnctWKGj857c6ew1u1fNQOlOtuGxQY=
+github.com/apparentlymart/go-textseg/v15 v15.0.0/go.mod h1:K8XmNZdhEBkdlyDdvbmmsvpAG721bKi0joRfFdHIWJ4=
github.com/armon/go-radix v1.0.0 h1:F4z6KzEeeQIMeLFa97iZU6vupzoecKdU5TX24SNppXI=
github.com/armon/go-radix v1.0.0/go.mod h1:ufUuZ+zHj4x4TnLV4JWEpy2hxWSpsRywHrMgIH9cCH8=
github.com/bgentry/speakeasy v0.1.0 h1:ByYyxL9InA1OWqxJqqp2A5pYHUrCiAL6K3J+LKSsQkY=
github.com/bgentry/speakeasy v0.1.0/go.mod h1:+zsyZBPWlz7T6j88CTgSN5bM796AkVf0kBD4zp0CCIs=
-github.com/bwesterb/go-ristretto v1.2.0/go.mod h1:fUIoIZaG73pV5biE2Blr2xEzDoMj7NFEuV9ekS419A0=
-github.com/cloudflare/circl v1.1.0/go.mod h1:prBCrKB9DV4poKZY1l9zBXg2QJY7mvgRvtMxxK7fi4I=
-github.com/cloudflare/circl v1.3.3 h1:fE/Qz0QdIGqeWfnwq0RE0R7MI51s0M2E4Ga9kq5AEMs=
-github.com/cloudflare/circl v1.3.3/go.mod h1:5XYMA4rFBvNIrhs50XuiBJ15vF2pZn4nnUKZrLbUZFA=
+github.com/bmatcuk/doublestar/v4 v4.6.1 h1:FH9SifrbvJhnlQpztAx++wlkk70QBf0iBWDwNy7PA4I=
+github.com/bmatcuk/doublestar/v4 v4.6.1/go.mod h1:xBQ8jztBU6kakFMg+8WGxn0c6z1fTSPVIjEY1Wr7jzc=
+github.com/bufbuild/protocompile v0.4.0 h1:LbFKd2XowZvQ/kajzguUp2DC9UEIQhIq77fZZlaQsNA=
+github.com/bufbuild/protocompile v0.4.0/go.mod h1:3v93+mbWn/v3xzN+31nwkJfrEpAUwp+BagBSZWx+TP8=
+github.com/cloudflare/circl v1.3.7 h1:qlCDlTPz2n9fu58M0Nh1J/JzcFpfgkFHHX3O35r5vcU=
+github.com/cloudflare/circl v1.3.7/go.mod h1:sRTcRWXGLrKw6yIGJ+l7amYJFfAXbZG0kBSc8r4zxgA=
+github.com/cyphar/filepath-securejoin v0.2.4 h1:Ugdm7cg7i6ZK6x3xDF1oEu1nfkyfH53EtKeQYTC3kyg=
+github.com/cyphar/filepath-securejoin v0.2.4/go.mod h1:aPGpWjXOXUn2NCNjFvBE6aRxGGx79pTxQpKOJNYHHl4=
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/emirpasic/gods v1.18.1 h1:FXtiHYKDGKCW2KzwZKx0iC0PQmdlorYgdFG9jPXJ1Bc=
-github.com/fatih/color v1.7.0/go.mod h1:Zm6kSWBoL9eyXnKyktHP6abPY2pDugNf5KwzbycvMj4=
-github.com/fatih/color v1.13.0 h1:8LOYc1KYPPmyKMuN8QV2DNRWNbLo6LZ0iLs8+mlH53w=
+github.com/emirpasic/gods v1.18.1/go.mod h1:8tpGGwCnJ5H4r6BWwaV6OrWmMoPhUl5jm/FMNAnJvWQ=
github.com/fatih/color v1.13.0/go.mod h1:kLAiJbzzSOZDVNGyDpeOxJ47H46qBXwg5ILebYFFOfk=
+github.com/fatih/color v1.16.0 h1:zmkK9Ngbjj+K0yRhTVONQh1p/HknKYSlNT+vZCzyokM=
+github.com/fatih/color v1.16.0/go.mod h1:fL2Sau1YI5c0pdGEVCbKQbLXB6edEj1ZgiY4NijnWvE=
github.com/frankban/quicktest v1.14.3 h1:FJKSZTDHjyhriyC81FLQ0LY93eSai0ZyR/ZIkd3ZUKE=
-github.com/go-git/gcfg v1.5.0 h1:Q5ViNfGF8zFgyJWPqYwA7qGFoMTEiBmdlkcfRmpIMa4=
-github.com/go-git/go-billy/v5 v5.4.1 h1:Uwp5tDRkPr+l/TnbHOQzp+tmJfLceOlbVucgpTz8ix4=
-github.com/go-git/go-git/v5 v5.6.1 h1:q4ZRqQl4pR/ZJHc1L5CFjGA1a10u76aV1iC+nh+bHsk=
-github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk=
-github.com/golang/protobuf v1.5.3 h1:KhyjKVUg7Usr/dYsdSqoFveMYd5ko72D+zANwlG1mmg=
-github.com/golang/protobuf v1.5.3/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY=
-github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
-github.com/google/go-cmp v0.5.9 h1:O2Tfq5qg4qc4AmwVlvv0oLiVAGB7enBSJ2x2DqQFi38=
+github.com/frankban/quicktest v1.14.3/go.mod h1:mgiwOwqx65TmIk1wJ6Q7wvnVMocbUorkibMOrVTHZps=
+github.com/go-git/gcfg v1.5.1-0.20230307220236-3a3c6141e376 h1:+zs/tPmkDkHx3U66DAb0lQFJrpS6731Oaa12ikc+DiI=
+github.com/go-git/gcfg v1.5.1-0.20230307220236-3a3c6141e376/go.mod h1:an3vInlBmSxCcxctByoQdvwPiA7DTK7jaaFDBTtu0ic=
+github.com/go-git/go-billy/v5 v5.5.0 h1:yEY4yhzCDuMGSv83oGxiBotRzhwhNr8VZyphhiu+mTU=
+github.com/go-git/go-billy/v5 v5.5.0/go.mod h1:hmexnoNsr2SJU1Ju67OaNz5ASJY3+sHgFRpCtpDCKow=
+github.com/go-git/go-git/v5 v5.12.0 h1:7Md+ndsjrzZxbddRDZjF14qK+NN56sy6wkqaVrjZtys=
+github.com/go-git/go-git/v5 v5.12.0/go.mod h1:FTM9VKtnI2m65hNI/TenDDDnUf2Q9FHnXYjuz9i5OEY=
+github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da h1:oI5xCqsCo564l8iNU+DwB5epxmsaqB+rhGL0m5jtYqE=
+github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=
+github.com/golang/protobuf v1.5.4 h1:i7eJL8qZTpSEXOPTxNKhASYpMn+8e5Q6AdndVa1dWek=
+github.com/golang/protobuf v1.5.4/go.mod h1:lnTiLA8Wa4RWRcIUkrtSVa5nRhsEGBg48fD6rSs7xps=
+github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI=
+github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
github.com/google/uuid v1.1.1/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
-github.com/google/uuid v1.1.2/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
-github.com/google/uuid v1.3.0 h1:t6JiXgmwXMjEs8VusXIJk2BXHsn+wx8BZdTaoZ5fu7I=
-github.com/google/uuid v1.3.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
+github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
+github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
+github.com/hashicorp/cli v1.1.6 h1:CMOV+/LJfL1tXCOKrgAX0uRKnzjj/mpmqNXloRSy2K8=
+github.com/hashicorp/cli v1.1.6/go.mod h1:MPon5QYlgjjo0BSoAiN0ESeT5fRzDjVRp+uioJ0piz4=
github.com/hashicorp/errwrap v1.0.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4=
github.com/hashicorp/errwrap v1.1.0 h1:OxrOeh75EUXMY8TBjag2fzXGZ40LB6IKw45YeGUDY2I=
github.com/hashicorp/errwrap v1.1.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4=
@@ -53,56 +68,59 @@ github.com/hashicorp/go-hclog v1.5.0/go.mod h1:W4Qnvbt70Wk/zYJryRzDRU/4r0kIg0PVH
github.com/hashicorp/go-multierror v1.0.0/go.mod h1:dHtQlpGsu+cZNNAkkCN/P3hoUDHhCYQXV3UM06sGGrk=
github.com/hashicorp/go-multierror v1.1.1 h1:H5DkEtf6CXdFp0N0Em5UCwQpXMWke8IA0+lD48awMYo=
github.com/hashicorp/go-multierror v1.1.1/go.mod h1:iw975J/qwKPdAO1clOe2L8331t/9/fmwbPZ6JB6eMoM=
-github.com/hashicorp/go-plugin v1.4.10 h1:xUbmA4jC6Dq163/fWcp8P3JuHilrHHMLNRxzGQJ9hNk=
-github.com/hashicorp/go-plugin v1.4.10/go.mod h1:6/1TEzT0eQznvI/gV2CM29DLSkAK/e58mUWKVsPaph0=
+github.com/hashicorp/go-plugin v1.6.2 h1:zdGAEd0V1lCaU0u+MxWQhtSDQmahpkwOun8U8EiRVog=
+github.com/hashicorp/go-plugin v1.6.2/go.mod h1:CkgLQ5CZqNmdL9U9JzM532t8ZiYQ35+pj3b1FD37R0Q=
github.com/hashicorp/go-uuid v1.0.0/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/bN7x4byOro=
github.com/hashicorp/go-uuid v1.0.3 h1:2gKiV6YVmrJ1i2CKKa9obLvRieoRGviZFL26PcT/Co8=
github.com/hashicorp/go-uuid v1.0.3/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/bN7x4byOro=
-github.com/hashicorp/go-version v1.6.0 h1:feTTfFNnjP967rlCxM/I9g701jU+RN74YKx2mOkIeek=
-github.com/hashicorp/go-version v1.6.0/go.mod h1:fltr4n8CU8Ke44wwGCBoEymUuxUHl09ZGVZPK5anwXA=
-github.com/hashicorp/hc-install v0.5.2 h1:SfwMFnEXVVirpwkDuSF5kymUOhrUxrTq3udEseZdOD0=
-github.com/hashicorp/hc-install v0.5.2/go.mod h1:9QISwe6newMWIfEiXpzuu1k9HAGtQYgnSH8H9T8wmoI=
-github.com/hashicorp/terraform-exec v0.18.1 h1:LAbfDvNQU1l0NOQlTuudjczVhHj061fNX5H8XZxHlH4=
-github.com/hashicorp/terraform-exec v0.18.1/go.mod h1:58wg4IeuAJ6LVsLUeD2DWZZoc/bYi6dzhLHzxM41980=
-github.com/hashicorp/terraform-json v0.17.1 h1:eMfvh/uWggKmY7Pmb3T85u86E2EQg6EQHgyRwf3RkyA=
-github.com/hashicorp/terraform-json v0.17.1/go.mod h1:Huy6zt6euxaY9knPAFKjUITn8QxUFIe9VuSzb4zn/0o=
-github.com/hashicorp/terraform-plugin-docs v0.16.0 h1:UmxFr3AScl6Wged84jndJIfFccGyBZn52KtMNsS12dI=
-github.com/hashicorp/terraform-plugin-docs v0.16.0/go.mod h1:M3ZrlKBJAbPMtNOPwHicGi1c+hZUh7/g0ifT/z7TVfA=
-github.com/hashicorp/terraform-plugin-framework v1.3.5 h1:FJ6s3CVWVAxlhiF/jhy6hzs4AnPHiflsp9KgzTGl1wo=
-github.com/hashicorp/terraform-plugin-framework v1.3.5/go.mod h1:2gGDpWiTI0irr9NSTLFAKlTi6KwGti3AoU19rFqU30o=
-github.com/hashicorp/terraform-plugin-go v0.18.0 h1:IwTkOS9cOW1ehLd/rG0y+u/TGLK9y6fGoBjXVUquzpE=
-github.com/hashicorp/terraform-plugin-go v0.18.0/go.mod h1:l7VK+2u5Kf2y+A+742GX0ouLut3gttudmvMgN0PA74Y=
+github.com/hashicorp/go-version v1.7.0 h1:5tqGy27NaOTB8yJKUZELlFAS/LTKJkrmONwQKeRZfjY=
+github.com/hashicorp/go-version v1.7.0/go.mod h1:fltr4n8CU8Ke44wwGCBoEymUuxUHl09ZGVZPK5anwXA=
+github.com/hashicorp/hc-install v0.7.0 h1:Uu9edVqjKQxxuD28mR5TikkKDd/p55S8vzPC1659aBk=
+github.com/hashicorp/hc-install v0.7.0/go.mod h1:ELmmzZlGnEcqoUMKUuykHaPCIR1sYLYX+KSggWSKZuA=
+github.com/hashicorp/terraform-exec v0.21.0 h1:uNkLAe95ey5Uux6KJdua6+cv8asgILFVWkd/RG0D2XQ=
+github.com/hashicorp/terraform-exec v0.21.0/go.mod h1:1PPeMYou+KDUSSeRE9szMZ/oHf4fYUmB923Wzbq1ICg=
+github.com/hashicorp/terraform-json v0.22.1 h1:xft84GZR0QzjPVWs4lRUwvTcPnegqlyS7orfb5Ltvec=
+github.com/hashicorp/terraform-json v0.22.1/go.mod h1:JbWSQCLFSXFFhg42T7l9iJwdGXBYV8fmmD6o/ML4p3A=
+github.com/hashicorp/terraform-plugin-docs v0.19.4 h1:G3Bgo7J22OMtegIgn8Cd/CaSeyEljqjH3G39w28JK4c=
+github.com/hashicorp/terraform-plugin-docs v0.19.4/go.mod h1:4pLASsatTmRynVzsjEhbXZ6s7xBlUw/2Kt0zfrq8HxA=
+github.com/hashicorp/terraform-plugin-framework v1.13.0 h1:8OTG4+oZUfKgnfTdPTJwZ532Bh2BobF4H+yBiYJ/scw=
+github.com/hashicorp/terraform-plugin-framework v1.13.0/go.mod h1:j64rwMGpgM3NYXTKuxrCnyubQb/4VKldEKlcG8cvmjU=
+github.com/hashicorp/terraform-plugin-go v0.25.0 h1:oi13cx7xXA6QciMcpcFi/rwA974rdTxjqEhXJjbAyks=
+github.com/hashicorp/terraform-plugin-go v0.25.0/go.mod h1:+SYagMYadJP86Kvn+TGeV+ofr/R3g4/If0O5sO96MVw=
github.com/hashicorp/terraform-plugin-log v0.9.0 h1:i7hOA+vdAItN1/7UrfBqBwvYPQ9TFvymaRGZED3FCV0=
github.com/hashicorp/terraform-plugin-log v0.9.0/go.mod h1:rKL8egZQ/eXSyDqzLUuwUYLVdlYeamldAHSxjUFADow=
-github.com/hashicorp/terraform-registry-address v0.2.1 h1:QuTf6oJ1+WSflJw6WYOHhLgwUiQ0FrROpHPYFtwTYWM=
-github.com/hashicorp/terraform-registry-address v0.2.1/go.mod h1:BSE9fIFzp0qWsJUUyGquo4ldV9k2n+psif6NYkBRS3Y=
+github.com/hashicorp/terraform-registry-address v0.2.3 h1:2TAiKJ1A3MAkZlH1YI/aTVcLZRu7JseiXNRHbOAyoTI=
+github.com/hashicorp/terraform-registry-address v0.2.3/go.mod h1:lFHA76T8jfQteVfT7caREqguFrW3c4MFSPhZB7HHgUM=
github.com/hashicorp/terraform-svchost v0.1.1 h1:EZZimZ1GxdqFRinZ1tpJwVxxt49xc/S52uzrw4x0jKQ=
github.com/hashicorp/terraform-svchost v0.1.1/go.mod h1:mNsjQfZyf/Jhz35v6/0LWcv26+X7JPS+buii2c9/ctc=
-github.com/hashicorp/yamux v0.0.0-20181012175058-2f1d1f20f75d h1:kJCB4vdITiW1eC1vq2e6IsrXKrZit1bv/TDYFGMp4BQ=
-github.com/hashicorp/yamux v0.0.0-20181012175058-2f1d1f20f75d/go.mod h1:+NfK9FKeTrX5uv1uIXGdwYDTeHna2qgaIlx54MXqjAM=
-github.com/huandu/xstrings v1.3.1/go.mod h1:y5/lhBue+AyNmUVz9RLU9xbLR0o4KIIExikq4ovT0aE=
-github.com/huandu/xstrings v1.3.2 h1:L18LIDzqlW6xN2rEkpdV8+oL/IXWJ1APd+vsdYy4Wdw=
-github.com/huandu/xstrings v1.3.2/go.mod h1:y5/lhBue+AyNmUVz9RLU9xbLR0o4KIIExikq4ovT0aE=
+github.com/hashicorp/yamux v0.1.1 h1:yrQxtgseBDrq9Y652vSRDvsKCJKOUD+GzTS4Y0Y8pvE=
+github.com/hashicorp/yamux v0.1.1/go.mod h1:CtWFDAQgb7dxtzFs4tWbplKIe2jSi3+5vKbgIO0SLnQ=
+github.com/huandu/xstrings v1.3.3 h1:/Gcsuc1x8JVbJ9/rlye4xZnVAbEkGauT8lbebqcQws4=
+github.com/huandu/xstrings v1.3.3/go.mod h1:y5/lhBue+AyNmUVz9RLU9xbLR0o4KIIExikq4ovT0aE=
github.com/imdario/mergo v0.3.11/go.mod h1:jmQim1M+e3UYxmgPu/WyfjB3N3VflVyUjjjwH0dnCYA=
-github.com/imdario/mergo v0.3.13 h1:lFzP57bqS/wsqKssCGmtLAb8A0wKjLGrve2q3PPVcBk=
-github.com/imdario/mergo v0.3.13/go.mod h1:4lJ1jqUDcsbIECGy0RUJAXNIhg+6ocWgb1ALK2O4oXg=
+github.com/imdario/mergo v0.3.15 h1:M8XP7IuFNsqUx6VPK2P9OSmsYsI/YFaGil0uD21V3dM=
+github.com/imdario/mergo v0.3.15/go.mod h1:WBLT9ZmE3lPoWsEzCh9LPo3TiwVN+ZKEjmz+hD27ysY=
github.com/jbenet/go-context v0.0.0-20150711004518-d14ea06fba99 h1:BQSFePA1RWJOlocH6Fxy8MmwDt+yVQYULKfN0RoTN8A=
-github.com/jhump/protoreflect v1.6.0 h1:h5jfMVslIg6l29nsMs0D8Wj17RDVdNYti0vDN/PZZoE=
+github.com/jbenet/go-context v0.0.0-20150711004518-d14ea06fba99/go.mod h1:1lJo3i6rXxKeerYnT8Nvf0QmHCRC1n8sfWVwXF2Frvo=
+github.com/jhump/protoreflect v1.15.1 h1:HUMERORf3I3ZdX05WaQ6MIpd/NJ434hTp5YiKgfCL6c=
+github.com/jhump/protoreflect v1.15.1/go.mod h1:jD/2GMKKE6OqX8qTjhADU1e6DShO+gavG9e0Q693nKo=
github.com/kevinburke/ssh_config v1.2.0 h1:x584FjTGwHzMwvHx18PXxbBVzfnxogHaAReU4gf13a4=
+github.com/kevinburke/ssh_config v1.2.0/go.mod h1:CT57kijsi8u/K/BOFA39wgDQJ9CxiF4nAY/ojJ6r6mM=
github.com/kr/pretty v0.3.0 h1:WgNl7dwNpEZ6jJ9k1snq4pZsg7DOEN8hP9Xw0Tsjwk0=
+github.com/kr/pretty v0.3.0/go.mod h1:640gp4NfQd8pI5XOwp5fnNeVWj67G7CFk/SaSQn7NBk=
github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
-github.com/mattn/go-colorable v0.0.9/go.mod h1:9vuHe8Xs5qXnSaW/c/ABM9alt+Vo+STaOChaDxuIBZU=
+github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
github.com/mattn/go-colorable v0.1.9/go.mod h1:u6P/XSegPjTcexA+o6vUJrdnUu04hMope9wVRipJSqc=
github.com/mattn/go-colorable v0.1.12/go.mod h1:u5H1YNBxpqRaxsYJYSkiCWKzEfiAb1Gb520KVy5xxl4=
github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA=
github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg=
-github.com/mattn/go-isatty v0.0.3/go.mod h1:M+lRXTBqGeGNdLjl/ufCoiOlB5xdOkqRJdNxMWT7Zi4=
github.com/mattn/go-isatty v0.0.12/go.mod h1:cbi8OIDigv2wuxKPP5vlRcQ1OAZbq2CE4Kysco4FUpU=
github.com/mattn/go-isatty v0.0.14/go.mod h1:7GGIvUiUoEMVVmxf/4nioHXj79iQHKdU27kJ6hsGG94=
-github.com/mattn/go-isatty v0.0.16 h1:bq3VjFmv/sOjHtdEhmkEV4x1AJtvUvOJ2PFAZ5+peKQ=
github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM=
-github.com/mitchellh/cli v1.1.5 h1:OxRIeJXpAMztws/XHlN2vu6imG5Dpq+j61AzAX5fLng=
-github.com/mitchellh/cli v1.1.5/go.mod h1:v8+iFts2sPIKUV1ltktPXMCC8fumSKFItNcD2cLtRR4=
+github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
+github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
+github.com/mattn/go-runewidth v0.0.9 h1:Lm995f3rfxdpd6TSmuVCHVb/QhupuXlYr8sCI/QdE+0=
+github.com/mattn/go-runewidth v0.0.9/go.mod h1:H031xJmbD/WCDINGzjvQ9THkh0rPKHF+m2gUSrubnMI=
github.com/mitchellh/copystructure v1.0.0/go.mod h1:SNtv71yrdKgLRyLFxmLdkAbkKEFWgYaq1OVrnRcwhnw=
github.com/mitchellh/copystructure v1.2.0 h1:vpKXTN4ewci03Vljg/q9QvCGUDttBOGBIa15WveJJGw=
github.com/mitchellh/copystructure v1.2.0/go.mod h1:qLl+cE2AmVv+CoeAwDPye/v+N2HKCj9FbZEVFJRxO9s=
@@ -114,19 +132,20 @@ github.com/mitchellh/reflectwalk v1.0.2/go.mod h1:mSTlrgnPZtwu0c4WaC2kGObEpuNDbx
github.com/oklog/run v1.0.0 h1:Ru7dDtJNOyC66gQ5dQmaCa0qIsAUFY3sFpK1Xk8igrw=
github.com/oklog/run v1.0.0/go.mod h1:dlhp/R75TPv97u0XWUtDeV/lRKWPKSdTuV0TZvrmrQA=
github.com/pjbgf/sha1cd v0.3.0 h1:4D5XXmUUBUl/xQ6IjCkEAbqXskkq/4O7LmGn0AqMDs4=
+github.com/pjbgf/sha1cd v0.3.0/go.mod h1:nZ1rrWOcGJ5uZgEEVL1VUM9iRQiZvWdbZjkKyFzPPsI=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
-github.com/posener/complete v1.1.1/go.mod h1:em0nMJCgc9GFtwrmVmEMR/ZL6WyhyjMBndrE9hABlRI=
github.com/posener/complete v1.2.3 h1:NP0eAhjcjImqslEwo/1hq7gpajME0fTLTezBKDqfXqo=
github.com/posener/complete v1.2.3/go.mod h1:WZIdtGGp+qx0sLrYKtIRAruyNpv6hFCicSgv7Sy7s/s=
-github.com/rogpeppe/go-internal v1.6.1 h1:/FiVV8dS/e+YqF2JvO3yXRFbBLTIuSDkuC7aBOAvL+k=
-github.com/russross/blackfriday v1.6.0 h1:KqfZb0pUVN2lYqZUYRddxF4OR8ZMURnJIG5Y3VRLtww=
-github.com/russross/blackfriday v1.6.0/go.mod h1:ti0ldHuxg49ri4ksnFxlkCfN+hvslNlmVHqNRXXJNAY=
-github.com/sergi/go-diff v1.2.0 h1:XU+rvMAioB0UC3q1MFrIQy4Vo5/4VsRDQQXHsEya6xQ=
+github.com/rogpeppe/go-internal v1.12.0 h1:exVL4IDcn6na9z1rAb56Vxr+CgyK3nn3O+epU5NdKM8=
+github.com/rogpeppe/go-internal v1.12.0/go.mod h1:E+RYuTGaKKdloAfM02xzb0FW3Paa99yedzYV+kq4uf4=
+github.com/sergi/go-diff v1.3.2-0.20230802210424-5b0b94c5c0d3 h1:n661drycOFuPLCN3Uc8sB6B/s6Z4t2xvBgU1htSHuq8=
+github.com/sergi/go-diff v1.3.2-0.20230802210424-5b0b94c5c0d3/go.mod h1:A0bzQcvG0E7Rwjx0REVgAGH58e96+X0MeOfepqsbeW4=
github.com/shopspring/decimal v1.2.0/go.mod h1:DKyhrW/HYNuLGql+MJL6WCR6knT2jwCFRcu2hWCYk4o=
github.com/shopspring/decimal v1.3.1 h1:2Usl1nmF/WZucqkFZhnfFYxxxu8LG21F6nPQBE5gKV8=
github.com/shopspring/decimal v1.3.1/go.mod h1:DKyhrW/HYNuLGql+MJL6WCR6knT2jwCFRcu2hWCYk4o=
-github.com/skeema/knownhosts v1.1.0 h1:Wvr9V0MxhjRbl3f9nMnKnFfiWTJmtECJ9Njkea3ysW0=
+github.com/skeema/knownhosts v1.2.2 h1:Iug2P4fLmDw9f41PB6thxUkNUkJzB5i+1/exaj40L3A=
+github.com/skeema/knownhosts v1.2.2/go.mod h1:xYbVRSPxqBZFrdmDyMmsOs+uX1UZC3nTN3ThzgDxUwo=
github.com/spf13/cast v1.3.1/go.mod h1:Qx5cxh0v+4UWYiBimWS+eyWzqEqokIECu5etghLkUJE=
github.com/spf13/cast v1.5.0 h1:rj3WzYc11XZaIZMPKmwP96zkFEnnAmV8s6XbB2aY32w=
github.com/spf13/cast v1.5.0/go.mod h1:SpXXQ5YoyJw6s3/6cMTQuxvgRl3PCJiyaX9p6b155UU=
@@ -134,63 +153,86 @@ github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+
github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4=
github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA=
-github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
-github.com/stretchr/testify v1.7.2 h1:4jaiDzPyXQvSd7D0EjG45355tLlV3VOECpq10pLC+8s=
github.com/stretchr/testify v1.7.2/go.mod h1:R6va5+xMeoiuVRoj+gSkQ7d3FALtqAAGI1FQKckRals=
-github.com/vmihailenco/msgpack/v5 v5.3.5 h1:5gO0H1iULLWGhs2H5tbAHIZTV8/cYafcFOr9znI5mJU=
-github.com/vmihailenco/msgpack/v5 v5.3.5/go.mod h1:7xyJ9e+0+9SaZT0Wt1RGleJXzli6Q/V5KbhBonMG9jc=
+github.com/stretchr/testify v1.8.3 h1:RP3t2pwF7cMEbC1dqtB6poj3niw/9gnV4Cjg5oW5gtY=
+github.com/stretchr/testify v1.8.3/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo=
+github.com/vmihailenco/msgpack/v5 v5.4.1 h1:cQriyiUvjTwOHg8QZaPihLWeRAAVoCpE00IUPn0Bjt8=
+github.com/vmihailenco/msgpack/v5 v5.4.1/go.mod h1:GaZTsDaehaPpQVyxrf5mtQlH+pc21PIudVV/E3rRQok=
github.com/vmihailenco/tagparser/v2 v2.0.0 h1:y09buUbR+b5aycVFQs/g70pqKVZNBmxwAhO7/IwNM9g=
github.com/vmihailenco/tagparser/v2 v2.0.0/go.mod h1:Wri+At7QHww0WTrCBeu4J6bNtoV6mEfg5OIWRZA9qds=
github.com/xanzy/ssh-agent v0.3.3 h1:+/15pJfg/RsTxqYcX6fHqOXZwwMP+2VyYWJeWM2qQFM=
-github.com/zclconf/go-cty v1.13.2 h1:4GvrUxe/QUDYuJKAav4EYqdM47/kZa672LwmXFmEKT0=
-github.com/zclconf/go-cty v1.13.2/go.mod h1:YKQzy/7pZ7iq2jNFzy5go57xdxdWoLLpaEp4u238AE0=
+github.com/xanzy/ssh-agent v0.3.3/go.mod h1:6dzNDKs0J9rVPHPhaGCukekBHKqfl+L3KghI1Bc68Uw=
+github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY=
+github.com/yuin/goldmark v1.7.1 h1:3bajkSilaCbjdKVsKdZjZCLBNPL9pYzrCakKaf4U49U=
+github.com/yuin/goldmark v1.7.1/go.mod h1:uzxRWxtg69N339t3louHJ7+O03ezfj6PlliRlaOzY1E=
+github.com/yuin/goldmark-meta v1.1.0 h1:pWw+JLHGZe8Rk0EGsMVssiNb/AaPMHfSRszZeUeiOUc=
+github.com/yuin/goldmark-meta v1.1.0/go.mod h1:U4spWENafuA7Zyg+Lj5RqK/MF+ovMYtBvXi1lBb2VP0=
+github.com/zclconf/go-cty v1.14.4 h1:uXXczd9QDGsgu0i/QFR/hzI5NYCHLf6NQw/atrbnhq8=
+github.com/zclconf/go-cty v1.14.4/go.mod h1:VvMs5i0vgZdhYawQNq5kePSpLAoz8u1xvZgrPIxfnZE=
+go.abhg.dev/goldmark/frontmatter v0.2.0 h1:P8kPG0YkL12+aYk2yU3xHv4tcXzeVnN+gU0tJ5JnxRw=
+go.abhg.dev/goldmark/frontmatter v0.2.0/go.mod h1:XqrEkZuM57djk7zrlRUB02x8I5J0px76YjkOzhB4YlU=
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
-golang.org/x/crypto v0.0.0-20200414173820-0848c9571904/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
-golang.org/x/crypto v0.0.0-20200820211705-5c72a883971a/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
-golang.org/x/crypto v0.14.0 h1:wBqGXzWJW6m1XrIKlAH0Hs1JJ7+9KBwnIO8v66Q9cHc=
-golang.org/x/crypto v0.14.0/go.mod h1:MVFd36DqK4CsrnJYDkBA3VC4m2GkXAM0PvzMCn4JQf4=
+golang.org/x/crypto v0.3.0/go.mod h1:hebNnKkNXi2UzZN1eVRvBB7co0a+JxK6XbPiWVs/3J4=
+golang.org/x/crypto v0.26.0 h1:RrRspgV4mU+YwB4FYnuBoKsUapNIL5cohGAmSH3azsw=
+golang.org/x/crypto v0.26.0/go.mod h1:GY7jblb9wI+FOo5y8/S2oY4zWP07AkOJ4+jxCqdqn54=
golang.org/x/exp v0.0.0-20230626212559-97b1e661b5df h1:UA2aFVmmsIlefxMk29Dp2juaUSth8Pyn3Tq5Y5mJGME=
golang.org/x/exp v0.0.0-20230626212559-97b1e661b5df/go.mod h1:FXUEEKJgO7OQYeo8N01OfiKP8RXMtf6e8aTskBGqWdc=
-golang.org/x/mod v0.11.0 h1:bUO06HqtnRcc/7l71XBe4WcqTZ+3AH1J59zWDDwLKgU=
-golang.org/x/mod v0.11.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
-golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
+golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4=
+golang.org/x/mod v0.17.0 h1:zY54UmvipHiNd+pm+m0x9KhZ9hl1/7QNMyxXbc6ICqA=
+golang.org/x/mod v0.17.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c=
+golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
-golang.org/x/net v0.17.0 h1:pVaXccu2ozPjCXewfr1S7xza/zcXTity9cCdXQYSjIM=
-golang.org/x/net v0.17.0/go.mod h1:NxSsAGuq816PNPmqtQdLE42eU2Fs7NoRIZrHJAlaCOE=
+golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c=
+golang.org/x/net v0.2.0/go.mod h1:KqCZLdyyvdV855qA2rE3GC2aiw5xGR5TEjj8smXukLY=
+golang.org/x/net v0.28.0 h1:a9JDOJc5GMUJ0+UDqmLT86WiEy7iWyIhz8gz8E4e5hE=
+golang.org/x/net v0.28.0/go.mod h1:yqtgsTWOOnlGLG9GFRrK3++bGOUEkNBoHZc8MEDWPNg=
+golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
+golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
+golang.org/x/sync v0.8.0 h1:3NFvSEYkUoMifnESzZl15y791HH1qU2xm6eCJU5ZPXQ=
+golang.org/x/sync v0.8.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
-golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200116001909-b77594299b42/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200223170610-d5e6a3e2c0ae/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20210927094055-39ccf1dd6fa6/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
-golang.org/x/sys v0.0.0-20211007075335-d3039528d8ac/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220503163025-988cb79eb6c6/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
+golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
+golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
-golang.org/x/sys v0.13.0 h1:Af8nKPmuFypiUBjVoU9V20FiaFXOcuZI21p0ycVYYGE=
-golang.org/x/sys v0.13.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
+golang.org/x/sys v0.2.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
+golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
+golang.org/x/sys v0.24.0 h1:Twjiwq9dn6R1fQcyiK+wQyHWfaz/BJB+YIpzU/Cv3Xg=
+golang.org/x/sys v0.24.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
+golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
+golang.org/x/term v0.2.0/go.mod h1:TVmDHMZPmdnySmBfhjOoOdhjzdE1h4u1VwSiw2l1Nuc=
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
-golang.org/x/text v0.13.0 h1:ablQoSUd0tRdKxZewP80B+BaqeKJuVhuRxj/dkrun3k=
-golang.org/x/text v0.13.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE=
+golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
+golang.org/x/text v0.4.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
+golang.org/x/text v0.17.0 h1:XtiM5bkSOt+ewxlOE/aE/AKEHibwj/6gvWMl9Rsh0Qc=
+golang.org/x/text v0.17.0/go.mod h1:BuEKDfySbSR4drPmRPG/7iBdf8hvFMuRexcpahXilzY=
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
-golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
-google.golang.org/genproto v0.0.0-20230410155749-daa745c078e1 h1:KpwkzHKEF7B9Zxg18WzOa7djJ+Ha5DzthMyZYQfEn2A=
-google.golang.org/genproto v0.0.0-20230410155749-daa745c078e1/go.mod h1:nKE/iIaLqn2bQwXBg8f1g2Ylh6r5MN5CmZvuzZCgsCU=
-google.golang.org/grpc v1.56.1 h1:z0dNfjIl0VpaZ9iSVjA6daGatAYwPGstTjt5vkRMFkQ=
-google.golang.org/grpc v1.56.1/go.mod h1:I9bI3vqKfayGqPUAwGdOSu7kt6oIJLixfffKrpXqQ9s=
-google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw=
-google.golang.org/protobuf v1.26.0/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc=
-google.golang.org/protobuf v1.31.0 h1:g0LDEJHgrBl9N9r17Ru3sqWhkIx2NB67okBHPwC7hs8=
-google.golang.org/protobuf v1.31.0/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I=
+golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
+golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc=
+golang.org/x/tools v0.21.1-0.20240508182429-e35e4ccd0d2d h1:vU5i/LfpvrRCpgM/VPfJLg5KjxD3E+hfT1SH+d9zLwg=
+golang.org/x/tools v0.21.1-0.20240508182429-e35e4ccd0d2d/go.mod h1:aiJjzUbINMkxbQROHiO6hDPo2LHcIPhhQsa9DLh0yGk=
+golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
+google.golang.org/genproto/googleapis/rpc v0.0.0-20240814211410-ddb44dafa142 h1:e7S5W7MGGLaSu8j3YjdezkZ+m1/Nm0uRVRMEMGk26Xs=
+google.golang.org/genproto/googleapis/rpc v0.0.0-20240814211410-ddb44dafa142/go.mod h1:UqMtugtsSgubUsoxbuAoiCXvqvErP7Gf0so0mK9tHxU=
+google.golang.org/grpc v1.67.1 h1:zWnc1Vrcno+lHZCOofnIMvycFcc0QRGIzm9dhnDX68E=
+google.golang.org/grpc v1.67.1/go.mod h1:1gLDyUQU7CTLJI90u3nXZ9ekeghjeM7pTDZlqFNg2AA=
+google.golang.org/protobuf v1.35.1 h1:m3LfL6/Ca+fqnjnlqQXNpFPABW1UD7mjh8KO2mKFytA=
+google.golang.org/protobuf v1.35.1/go.mod h1:9fA7Ob0pmnwhb644+1+CVWFRbNajQ6iRojtC/QF5bRE=
+gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/warnings.v0 v0.1.2 h1:wFXVbFY8DY5/xOe1ECiWdKCzZlxgshcYVNkBHstARME=
+gopkg.in/warnings.v0 v0.1.2/go.mod h1:jksf8JmL6Qr/oQM2OXTHunEvvTAsrWBLb6OOjuVWRNI=
gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
+gopkg.in/yaml.v2 v2.3.0 h1:clyUAQHOM3G0M3f5vQj7LuJrETvjVot3Z5el9nffUtU=
gopkg.in/yaml.v2 v2.3.0/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
-gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
-gopkg.in/yaml.v3 v3.0.0/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
diff --git a/internal/client/client.go b/internal/client/client.go
new file mode 100644
index 0000000..29c2988
--- /dev/null
+++ b/internal/client/client.go
@@ -0,0 +1,71 @@
+// Copyright (c) EZSCALE.
+// SPDX-License-Identifier: MPL-2.0
+
+package client
+
+import (
+ "fmt"
+ "net/http"
+ "net/url"
+ "strings"
+ "time"
+)
+
+// Client is the VirtFusion API client.
+type Client struct {
+ BaseURL string
+ Token string
+ HTTPClient *http.Client
+}
+
+// New creates a new VirtFusion API client.
+// The endpoint can be a hostname (e.g. "cp.example.com") or a full URL
+// (e.g. "https://cp.example.com" or "https://cp.example.com/api/v1").
+func New(endpoint, token string) (*Client, error) {
+ if endpoint == "" {
+ return nil, fmt.Errorf("endpoint is required")
+ }
+ if token == "" {
+ return nil, fmt.Errorf("api_token is required")
+ }
+
+ baseURL := normalizeEndpoint(endpoint)
+
+ return &Client{
+ BaseURL: baseURL,
+ Token: token,
+ HTTPClient: &http.Client{
+ Timeout: 60 * time.Second,
+ },
+ }, nil
+}
+
+// normalizeEndpoint takes a user-provided endpoint and returns a full base URL
+// ending with /api/v1. Supports:
+// - "cp.example.com" → "https://cp.example.com/api/v1"
+// - "https://cp.example.com" → "https://cp.example.com/api/v1"
+// - "https://cp.example.com/api/v1" → "https://cp.example.com/api/v1"
+func normalizeEndpoint(endpoint string) string {
+ endpoint = strings.TrimRight(endpoint, "/")
+
+ // If no scheme, add https://
+ if !strings.Contains(endpoint, "://") {
+ endpoint = "https://" + endpoint
+ }
+
+ // Parse to validate
+ u, err := url.Parse(endpoint)
+ if err != nil {
+ // Fall back to simple construction
+ return endpoint + "/api/v1"
+ }
+
+ // If path already ends with /api/v1, use as-is
+ if strings.HasSuffix(u.Path, "/api/v1") {
+ return u.String()
+ }
+
+ // Otherwise append /api/v1
+ u.Path = strings.TrimRight(u.Path, "/") + "/api/v1"
+ return u.String()
+}
diff --git a/internal/client/errors.go b/internal/client/errors.go
new file mode 100644
index 0000000..dca7a84
--- /dev/null
+++ b/internal/client/errors.go
@@ -0,0 +1,34 @@
+// Copyright (c) EZSCALE.
+// SPDX-License-Identifier: MPL-2.0
+
+package client
+
+import "fmt"
+
+// APIError represents an error returned by the VirtFusion API.
+type APIError struct {
+ StatusCode int
+ Status string
+ Body string
+ Errors map[string][]string
+}
+
+func (e *APIError) Error() string {
+ if len(e.Errors) > 0 {
+ return fmt.Sprintf("VirtFusion API error %d (%s): %v", e.StatusCode, e.Status, e.Errors)
+ }
+ if e.Body != "" {
+ return fmt.Sprintf("VirtFusion API error %d (%s): %s", e.StatusCode, e.Status, e.Body)
+ }
+ return fmt.Sprintf("VirtFusion API error %d (%s)", e.StatusCode, e.Status)
+}
+
+// IsNotFound returns true if the error is a 404 Not Found response.
+func (e *APIError) IsNotFound() bool {
+ return e.StatusCode == 404
+}
+
+// IsValidationError returns true if the error is a 422 Unprocessable Entity response.
+func (e *APIError) IsValidationError() bool {
+ return e.StatusCode == 422
+}
diff --git a/internal/client/request.go b/internal/client/request.go
new file mode 100644
index 0000000..786dd83
--- /dev/null
+++ b/internal/client/request.go
@@ -0,0 +1,192 @@
+// Copyright (c) EZSCALE.
+// SPDX-License-Identifier: MPL-2.0
+
+package client
+
+import (
+ "bytes"
+ "context"
+ "encoding/json"
+ "fmt"
+ "io"
+ "net/http"
+ "net/url"
+ "strings"
+)
+
+// paginatedResponse is the envelope returned by VirtFusion's Laravel-style pagination.
+type paginatedResponse struct {
+ CurrentPage int `json:"current_page"`
+ LastPage int `json:"last_page"`
+ Data json.RawMessage `json:"data"`
+}
+
+// Get performs a GET request to the given path.
+func (c *Client) Get(ctx context.Context, path string) (json.RawMessage, error) {
+ return c.doRequest(ctx, http.MethodGet, path, nil)
+}
+
+// GetAllPages fetches all pages from a paginated endpoint and returns
+// a synthetic JSON response with all items merged into a single "data" array.
+// If the response is not paginated (no last_page field or single page), it
+// returns the original response unchanged.
+func (c *Client) GetAllPages(ctx context.Context, path string) (json.RawMessage, error) {
+ firstRaw, err := c.Get(ctx, path)
+ if err != nil {
+ return nil, err
+ }
+
+ var page paginatedResponse
+ if err := json.Unmarshal(firstRaw, &page); err != nil || page.LastPage == 0 {
+ // Not a paginated response — return as-is.
+ return firstRaw, nil
+ }
+
+ if page.LastPage <= 1 {
+ return firstRaw, nil
+ }
+
+ // Collect data arrays from all pages.
+ allItems, err := flattenJSONArray(page.Data)
+ if err != nil {
+ return nil, fmt.Errorf("parsing page 1 data: %w", err)
+ }
+
+ sep := "&"
+ if !strings.Contains(path, "?") {
+ sep = "?"
+ }
+
+ for p := 2; p <= page.LastPage; p++ {
+ pageRaw, err := c.Get(ctx, fmt.Sprintf("%s%spage=%d", path, sep, p))
+ if err != nil {
+ return nil, fmt.Errorf("fetching page %d: %w", p, err)
+ }
+
+ var pageResp paginatedResponse
+ if err := json.Unmarshal(pageRaw, &pageResp); err != nil {
+ return nil, fmt.Errorf("parsing page %d: %w", p, err)
+ }
+
+ items, err := flattenJSONArray(pageResp.Data)
+ if err != nil {
+ return nil, fmt.Errorf("parsing page %d data: %w", p, err)
+ }
+ allItems = append(allItems, items...)
+ }
+
+ mergedData, err := json.Marshal(allItems)
+ if err != nil {
+ return nil, fmt.Errorf("marshaling merged data: %w", err)
+ }
+
+ // Build a response that looks like {"data": [...all items...]} so
+ // existing list response types (e.g. ServerListResponse) unmarshal correctly.
+ result, err := json.Marshal(map[string]json.RawMessage{"data": mergedData})
+ if err != nil {
+ return nil, fmt.Errorf("marshaling merged response: %w", err)
+ }
+ return result, nil
+}
+
+// flattenJSONArray unmarshals a JSON array into individual raw messages.
+func flattenJSONArray(raw json.RawMessage) ([]json.RawMessage, error) {
+ var items []json.RawMessage
+ if err := json.Unmarshal(raw, &items); err != nil {
+ return nil, err
+ }
+ return items, nil
+}
+
+// Post performs a POST request to the given path with the given body.
+func (c *Client) Post(ctx context.Context, path string, body interface{}) (json.RawMessage, error) {
+ return c.doRequest(ctx, http.MethodPost, path, body)
+}
+
+// Put performs a PUT request to the given path with the given body.
+func (c *Client) Put(ctx context.Context, path string, body interface{}) (json.RawMessage, error) {
+ return c.doRequest(ctx, http.MethodPut, path, body)
+}
+
+// Delete performs a DELETE request to the given path.
+func (c *Client) Delete(ctx context.Context, path string) (json.RawMessage, error) {
+ return c.doRequest(ctx, http.MethodDelete, path, nil)
+}
+
+// DeleteWithBody performs a DELETE request with a JSON body.
+func (c *Client) DeleteWithBody(ctx context.Context, path string, body interface{}) (json.RawMessage, error) {
+ return c.doRequest(ctx, http.MethodDelete, path, body)
+}
+
+func (c *Client) doRequest(ctx context.Context, method, path string, body interface{}) (json.RawMessage, error) {
+ // Split path from query string before joining, since url.JoinPath
+ // escapes '?' as '%3F' when it appears in the path.
+ pathPart := path
+ queryPart := ""
+ if idx := strings.IndexByte(path, '?'); idx >= 0 {
+ pathPart = path[:idx]
+ queryPart = path[idx:]
+ }
+
+ fullURL, err := url.JoinPath(c.BaseURL, pathPart)
+ if err != nil {
+ return nil, fmt.Errorf("building URL: %w", err)
+ }
+ fullURL += queryPart
+
+ var bodyReader io.Reader
+ if body != nil {
+ jsonBody, err := json.Marshal(body)
+ if err != nil {
+ return nil, fmt.Errorf("marshaling request body: %w", err)
+ }
+ bodyReader = bytes.NewReader(jsonBody)
+ }
+
+ req, err := http.NewRequestWithContext(ctx, method, fullURL, bodyReader)
+ if err != nil {
+ return nil, fmt.Errorf("creating request: %w", err)
+ }
+
+ req.Header.Set("Authorization", "Bearer "+c.Token)
+ req.Header.Set("Accept", "application/json")
+ if body != nil {
+ req.Header.Set("Content-Type", "application/json")
+ }
+
+ resp, err := c.HTTPClient.Do(req)
+ if err != nil {
+ return nil, fmt.Errorf("executing request: %w", err)
+ }
+ defer resp.Body.Close()
+
+ // 204 No Content is a success with no body
+ if resp.StatusCode == http.StatusNoContent {
+ return nil, nil
+ }
+
+ respBody, err := io.ReadAll(resp.Body)
+ if err != nil {
+ return nil, fmt.Errorf("reading response body: %w", err)
+ }
+
+ if resp.StatusCode >= 400 {
+ apiErr := &APIError{
+ StatusCode: resp.StatusCode,
+ Status: resp.Status,
+ Body: string(respBody),
+ }
+
+ // Try to parse validation errors
+ var errResp struct {
+ Errors map[string][]string `json:"errors"`
+ }
+ if json.Unmarshal(respBody, &errResp) == nil && len(errResp.Errors) > 0 {
+ apiErr.Errors = errResp.Errors
+ }
+
+ return nil, apiErr
+ }
+
+ return json.RawMessage(respBody), nil
+}
diff --git a/internal/client/types.go b/internal/client/types.go
new file mode 100644
index 0000000..09ed03b
--- /dev/null
+++ b/internal/client/types.go
@@ -0,0 +1,516 @@
+// Copyright (c) EZSCALE.
+// SPDX-License-Identifier: MPL-2.0
+
+package client
+
+// ServerCreateRequest represents the request body for creating a server.
+type ServerCreateRequest struct {
+ PackageID int64 `json:"packageId"`
+ UserID int64 `json:"userId"`
+ HypervisorID int64 `json:"hypervisorId"`
+ Ipv4 *int64 `json:"ipv4,omitempty"`
+ Storage *int64 `json:"storage,omitempty"`
+ Memory *int64 `json:"memory,omitempty"`
+ CPUCores *int64 `json:"cpuCores,omitempty"`
+ Traffic *int64 `json:"traffic,omitempty"`
+ NetworkSpeedInbound *int64 `json:"networkSpeedInbound,omitempty"`
+ NetworkSpeedOutbound *int64 `json:"networkSpeedOutbound,omitempty"`
+ StorageProfile *int64 `json:"storageProfile,omitempty"`
+ NetworkProfile *int64 `json:"networkProfile,omitempty"`
+ DryRun *bool `json:"dryRun,omitempty"`
+ AdditionalStorage1 *int64 `json:"additionalStorage1,omitempty"`
+ AdditionalStorage1Profile *int64 `json:"additionalStorage1Profile,omitempty"`
+ AdditionalStorage2 *int64 `json:"additionalStorage2,omitempty"`
+ AdditionalStorage2Profile *int64 `json:"additionalStorage2Profile,omitempty"`
+}
+
+// ServerResponse represents the response from the API for server operations.
+type ServerResponse struct {
+ Data ServerData `json:"data"`
+}
+
+// ServerData represents a server in the API.
+// The API returns "ownerId" (not "userId") and nests CPU/memory/storage
+// under settings.resources and cpu.cores for detailed (single) responses.
+// List responses use a flatter structure with "owner" as an int.
+type ServerData struct {
+ ID int64 `json:"id"`
+ UUID string `json:"uuid"`
+ Name string `json:"name"`
+ Hostname string `json:"hostname"`
+ OwnerID int64 `json:"ownerId"`
+ HypervisorID int64 `json:"hypervisorId"`
+ Suspended bool `json:"suspended"`
+
+ // Nested objects present in detailed (single-server) responses.
+ CPU *ServerCPU `json:"cpu,omitempty"`
+ Settings *ServerSettings `json:"settings,omitempty"`
+ Traffic interface{} `json:"traffic,omitempty"`
+}
+
+// ServerCPU represents CPU info from the detailed server response.
+type ServerCPU struct {
+ Cores int64 `json:"cores"`
+}
+
+// ServerSettings holds nested settings from the detailed server response.
+type ServerSettings struct {
+ Resources *ServerSettingsResources `json:"resources,omitempty"`
+}
+
+// ServerSettingsResources holds resource allocations from server settings.
+type ServerSettingsResources struct {
+ Memory int64 `json:"memory"`
+ Storage int64 `json:"storage"`
+ Traffic int64 `json:"traffic"`
+ CPUCores int64 `json:"cpuCores"`
+}
+
+// ServerListResponse represents a list of servers.
+type ServerListResponse struct {
+ Data []ServerData `json:"data"`
+}
+
+// ServerBuildRequest represents the request body for building a server.
+type ServerBuildRequest struct {
+ Name string `json:"name"`
+ Hostname string `json:"hostname,omitempty"`
+ OperatingSystemID int64 `json:"operatingSystemId"`
+ VNC bool `json:"vnc"`
+ Ipv6 bool `json:"ipv6"`
+ SSHKeys []int64 `json:"sshKeys,omitempty"`
+ Email bool `json:"email"`
+}
+
+// SSHKeyCreateRequest represents the request body for creating an SSH key.
+type SSHKeyCreateRequest struct {
+ UserID int64 `json:"userId"`
+ Name string `json:"name"`
+ PublicKey string `json:"publicKey"`
+}
+
+// SSHKeyResponse represents the response from the API for SSH key operations.
+type SSHKeyResponse struct {
+ Data SSHKeyData `json:"data"`
+}
+
+// SSHKeyData represents an SSH key in the API.
+type SSHKeyData struct {
+ ID int64 `json:"id"`
+ Name string `json:"name"`
+ Type string `json:"type"`
+ PublicKey string `json:"publicKey"`
+ Enabled bool `json:"enabled"`
+ UserID int64 `json:"userId"`
+ CreatedAt string `json:"created"`
+ UpdatedAt string `json:"updated"`
+}
+
+// SSHKeyListResponse represents a list of SSH keys.
+type SSHKeyListResponse struct {
+ Data []SSHKeyData `json:"data"`
+}
+
+// UserCreateRequest represents the request body for creating a user.
+type UserCreateRequest struct {
+ Name string `json:"name"`
+ Email string `json:"email"`
+ ExtRelationID string `json:"extRelationId"`
+}
+
+// UserResponse represents the response from the API for user operations.
+type UserResponse struct {
+ Data UserData `json:"data"`
+}
+
+// UserData represents a user in the API.
+type UserData struct {
+ ID int64 `json:"id"`
+ Name string `json:"name"`
+ Email string `json:"email"`
+ ExtRelationID string `json:"extRelationId"`
+ Enabled bool `json:"enabled"`
+ CreatedAt string `json:"created"`
+ UpdatedAt string `json:"updated"`
+}
+
+// UserModifyRequest represents the request body for modifying a user.
+type UserModifyRequest struct {
+ Name string `json:"name,omitempty"`
+ Email string `json:"email,omitempty"`
+}
+
+// HypervisorResponse represents the response from the API for hypervisor operations.
+type HypervisorResponse struct {
+ Data HypervisorData `json:"data"`
+}
+
+// HypervisorData represents a hypervisor in the API.
+type HypervisorData struct {
+ ID int64 `json:"id"`
+ Name string `json:"name"`
+ Type string `json:"type"`
+ Hostname string `json:"hostname"`
+ Enabled bool `json:"enabled"`
+}
+
+// HypervisorListResponse represents a list of hypervisors.
+type HypervisorListResponse struct {
+ Data []HypervisorData `json:"data"`
+}
+
+// HypervisorGroupResponse represents the response from the API for hypervisor group operations.
+type HypervisorGroupResponse struct {
+ Data HypervisorGroupData `json:"data"`
+}
+
+// HypervisorGroupData represents a hypervisor group in the API.
+type HypervisorGroupData struct {
+ ID int64 `json:"id"`
+ Name string `json:"name"`
+ Enabled bool `json:"enabled"`
+}
+
+// HypervisorGroupListResponse represents a list of hypervisor groups.
+type HypervisorGroupListResponse struct {
+ Data []HypervisorGroupData `json:"data"`
+}
+
+// PackageResponse represents the response from the API for package operations.
+type PackageResponse struct {
+ Data PackageData `json:"data"`
+}
+
+// PackageData represents a package in the API.
+// The API uses "primaryStorage", "primaryNetworkSpeedIn/Out" etc.
+type PackageData struct {
+ ID int64 `json:"id"`
+ Name string `json:"name"`
+ Enabled bool `json:"enabled"`
+ CPUCores int64 `json:"cpuCores"`
+ Memory int64 `json:"memory"`
+ Storage int64 `json:"primaryStorage"`
+ Traffic int64 `json:"traffic"`
+ NetworkSpeedInbound int64 `json:"primaryNetworkSpeedIn"`
+ NetworkSpeedOutbound int64 `json:"primaryNetworkSpeedOut"`
+ Ipv4 int64 `json:"ipv4"`
+ StorageProfile int64 `json:"primaryStorageProfile"`
+ NetworkProfile int64 `json:"primaryNetworkProfile"`
+}
+
+// PackageListResponse represents a list of packages.
+type PackageListResponse struct {
+ Data []PackageData `json:"data"`
+}
+
+// IPBlockResponse represents the response from the API for IP block operations.
+type IPBlockResponse struct {
+ Data IPBlockData `json:"data"`
+}
+
+// IPBlockData represents an IP block in the API.
+// The API nests gateway/netmask under "ipv4" and returns "type" as an int.
+type IPBlockData struct {
+ ID int64 `json:"id"`
+ Name string `json:"name"`
+ Type int64 `json:"type"`
+ IPv4 IPBlockIPv4 `json:"ipv4"`
+ Enabled bool `json:"enabled"`
+}
+
+// IPBlockIPv4 represents the IPv4 section of an IP block.
+type IPBlockIPv4 struct {
+ Gateway string `json:"gateway"`
+ Netmask string `json:"netmask"`
+}
+
+// IPBlockListResponse represents a list of IP blocks.
+type IPBlockListResponse struct {
+ Data []IPBlockData `json:"data"`
+}
+
+// FirewallResponse represents the response from the API for firewall operations.
+type FirewallResponse struct {
+ Data FirewallData `json:"data"`
+}
+
+// FirewallData represents firewall data in the API.
+type FirewallData struct {
+ Enabled bool `json:"enabled"`
+ Rules []FirewallRule `json:"rules"`
+}
+
+// FirewallRule represents a single firewall rule.
+type FirewallRule struct {
+ Action string `json:"action"`
+ Direction string `json:"direction"`
+ Protocol string `json:"protocol"`
+ Port string `json:"port"`
+ IP string `json:"ip"`
+}
+
+// FirewallSetRulesRequest represents the request body for setting firewall rules.
+type FirewallSetRulesRequest struct {
+ Rules []FirewallRule `json:"rules"`
+}
+
+// NetworkWhitelistRequest represents the request body for adding a network whitelist entry.
+type NetworkWhitelistRequest struct {
+ IP string `json:"ip"`
+}
+
+// TrafficBlockRequest represents the request body for adding a traffic block.
+type TrafficBlockRequest struct {
+ Type string `json:"type"`
+}
+
+// TrafficBlockResponse represents the response for traffic block operations.
+type TrafficBlockResponse struct {
+ Data TrafficBlockData `json:"data"`
+}
+
+// TrafficBlockData represents a traffic block.
+type TrafficBlockData struct {
+ ID int64 `json:"id"`
+ Type string `json:"type"`
+}
+
+// TrafficBlockListResponse represents a list of traffic blocks.
+type TrafficBlockListResponse struct {
+ Data []TrafficBlockData `json:"data"`
+}
+
+// TrafficResponse represents server traffic data.
+type TrafficResponse struct {
+ Data TrafficData `json:"data"`
+}
+
+// TrafficData represents traffic usage data.
+type TrafficData struct {
+ Used int64 `json:"used"`
+ Limit int64 `json:"limit"`
+}
+
+// IPBlockRangeRequest represents the request body for adding an IPv4 range.
+type IPBlockRangeRequest struct {
+ StartIP string `json:"startIp"`
+ EndIP string `json:"endIp"`
+ Gateway string `json:"gateway"`
+ Netmask string `json:"netmask"`
+}
+
+// ServerModifyNameRequest represents the request body for modifying a server name.
+type ServerModifyNameRequest struct {
+ Name string `json:"name"`
+}
+
+// ServerModifyCPURequest represents the request body for modifying server CPU.
+type ServerModifyCPURequest struct {
+ CPUCores int64 `json:"cpuCores"`
+}
+
+// ServerModifyMemoryRequest represents the request body for modifying server memory.
+type ServerModifyMemoryRequest struct {
+ Memory int64 `json:"memory"`
+}
+
+// ServerModifyTrafficRequest represents the request body for modifying server traffic.
+type ServerModifyTrafficRequest struct {
+ Traffic int64 `json:"traffic"`
+}
+
+// ServerModifyCPUThrottleRequest represents the request body for modifying CPU throttle.
+type ServerModifyCPUThrottleRequest struct {
+ Percentage int64 `json:"percentage"`
+}
+
+// ServerCustomXMLRequest represents the request body for setting custom XML.
+type ServerCustomXMLRequest struct {
+ XML string `json:"xml"`
+}
+
+// VNCResponse represents the response for VNC operations.
+type VNCResponse struct {
+ Data VNCData `json:"data"`
+}
+
+// VNCData represents VNC connection data.
+type VNCData struct {
+ URL string `json:"url"`
+}
+
+// BackupResponse represents the response for backup operations.
+type BackupResponse struct {
+ Data []BackupData `json:"data"`
+}
+
+// BackupData represents a backup entry.
+type BackupData struct {
+ ID int64 `json:"id"`
+ Type string `json:"type"`
+ CreatedAt string `json:"created"`
+}
+
+// DNSServiceResponse represents the response for DNS service operations.
+type DNSServiceResponse struct {
+ Data DNSServiceData `json:"data"`
+}
+
+// DNSServiceData represents a DNS service.
+type DNSServiceData struct {
+ ID int64 `json:"id"`
+ Name string `json:"name"`
+ Type string `json:"type"`
+}
+
+// ISOResponse represents the response for ISO operations.
+type ISOResponse struct {
+ Data ISOData `json:"data"`
+}
+
+// ISOData represents an ISO.
+type ISOData struct {
+ ID int64 `json:"id"`
+ Name string `json:"name"`
+}
+
+// QueueResponse represents the response for queue operations.
+type QueueResponse struct {
+ Data QueueData `json:"data"`
+}
+
+// QueueData represents a queue item.
+type QueueData struct {
+ ID int64 `json:"id"`
+ Status string `json:"status"`
+ Action string `json:"action"`
+ CreatedAt string `json:"created"`
+}
+
+// TemplateResponse represents the response for template operations.
+type TemplateResponse struct {
+ Data []TemplateData `json:"data"`
+}
+
+// TemplateData represents a template.
+type TemplateData struct {
+ ID int64 `json:"id"`
+ Name string `json:"name"`
+}
+
+// SelfServiceCreditRequest represents the request body for adding credit.
+type SelfServiceCreditRequest struct {
+ Amount float64 `json:"amount"`
+ CurrencyCode string `json:"currencyCode"`
+ UserID int64 `json:"userId"`
+}
+
+// SelfServiceCreditResponse represents the response for credit operations.
+type SelfServiceCreditResponse struct {
+ Data SelfServiceCreditData `json:"data"`
+}
+
+// SelfServiceCreditData represents credit data.
+type SelfServiceCreditData struct {
+ ID int64 `json:"id"`
+ Amount float64 `json:"amount"`
+}
+
+// SelfServiceResourcePackRequest represents the request body for resource packs.
+type SelfServiceResourcePackRequest struct {
+ Name string `json:"name"`
+ UserID int64 `json:"userId"`
+ PackID int64 `json:"packId"`
+}
+
+// SelfServiceResourcePackResponse represents the response for resource pack operations.
+type SelfServiceResourcePackResponse struct {
+ Data SelfServiceResourcePackData `json:"data"`
+}
+
+// SelfServiceResourcePackData represents a resource pack.
+type SelfServiceResourcePackData struct {
+ ID int64 `json:"id"`
+ Name string `json:"name"`
+ UserID int64 `json:"userId"`
+ PackID int64 `json:"packId"`
+}
+
+// CurrencyResponse represents the response for currency operations.
+type CurrencyResponse struct {
+ Data []CurrencyData `json:"data"`
+}
+
+// CurrencyData represents a currency.
+type CurrencyData struct {
+ ID int64 `json:"id"`
+ Code string `json:"code"`
+ Name string `json:"name"`
+}
+
+// ServerIPv4AddRequest represents the request body for adding IPv4 to a server.
+type ServerIPv4AddRequest struct {
+ Quantity int64 `json:"quantity,omitempty"`
+}
+
+// AuthTokenResponse represents the response for auth token generation.
+type AuthTokenResponse struct {
+ Data AuthTokenData `json:"data"`
+}
+
+// AuthTokenData represents an auth token.
+type AuthTokenData struct {
+ Token string `json:"token"`
+ URL string `json:"url"`
+}
+
+// PasswordResetResponse represents the response for password reset operations.
+type PasswordResetResponse struct {
+ Data PasswordResetData `json:"data"`
+}
+
+// PasswordResetData represents password reset data.
+type PasswordResetData struct {
+ Password string `json:"password"`
+}
+
+// HypervisorGroupResourcesResponse represents the response for hypervisor group resources.
+// The API returns a paginated array of per-hypervisor resource entries.
+type HypervisorGroupResourcesResponse struct {
+ Data []HypervisorGroupResourceEntry `json:"data"`
+}
+
+// HypervisorGroupResourceEntry represents a single hypervisor's resources within a group.
+type HypervisorGroupResourceEntry struct {
+ Hypervisor HypervisorData `json:"hypervisor"`
+ Resources HypervisorGroupResourceDetail `json:"resources"`
+}
+
+// HypervisorGroupResourceDetail represents the resource metrics for a hypervisor.
+type HypervisorGroupResourceDetail struct {
+ Memory ResourceMetric `json:"memory"`
+ CPUCores ResourceMetric `json:"cpuCores"`
+ LocalStorage ResourceMetric `json:"localStorage"`
+}
+
+// ResourceMetric represents a resource metric with max/allocated/free values.
+type ResourceMetric struct {
+ Max int64 `json:"max"`
+ Allocated int64 `json:"allocated"`
+ Free int64 `json:"free"`
+}
+
+// SelfServiceHourlyStatsResponse represents the response for hourly stats.
+type SelfServiceHourlyStatsResponse struct {
+ Data interface{} `json:"data"`
+}
+
+// SelfServiceReportResponse represents the response for self-service reports.
+type SelfServiceReportResponse struct {
+ Data interface{} `json:"data"`
+}
+
+// SelfServiceUsageResponse represents the response for self-service usage.
+type SelfServiceUsageResponse struct {
+ Data interface{} `json:"data"`
+}
diff --git a/internal/provider/data_source_dns_service.go b/internal/provider/data_source_dns_service.go
new file mode 100644
index 0000000..b27f63b
--- /dev/null
+++ b/internal/provider/data_source_dns_service.go
@@ -0,0 +1,105 @@
+// Copyright (c) EZSCALE.
+// SPDX-License-Identifier: MPL-2.0
+
+package provider
+
+import (
+ "context"
+ "encoding/json"
+ "fmt"
+
+ "terraform-provider-virtfusion/internal/client"
+
+ "github.com/hashicorp/terraform-plugin-framework/datasource"
+ "github.com/hashicorp/terraform-plugin-framework/datasource/schema"
+ "github.com/hashicorp/terraform-plugin-framework/types"
+)
+
+var (
+ _ datasource.DataSource = &DNSServiceDataSource{}
+ _ datasource.DataSourceWithConfigure = &DNSServiceDataSource{}
+)
+
+// NewDNSServiceDataSource returns a new DNS service data source.
+func NewDNSServiceDataSource() datasource.DataSource {
+ return &DNSServiceDataSource{}
+}
+
+// DNSServiceDataSource defines the data source implementation.
+type DNSServiceDataSource struct {
+ client *client.Client
+}
+
+// DNSServiceDataSourceModel describes the data source data model.
+type DNSServiceDataSourceModel struct {
+ ID types.Int64 `tfsdk:"id"`
+ Name types.String `tfsdk:"name"`
+ Type types.String `tfsdk:"type"`
+}
+
+func (d *DNSServiceDataSource) Metadata(_ context.Context, req datasource.MetadataRequest, resp *datasource.MetadataResponse) {
+ resp.TypeName = req.ProviderTypeName + "_dns_service"
+}
+
+func (d *DNSServiceDataSource) Schema(_ context.Context, _ datasource.SchemaRequest, resp *datasource.SchemaResponse) {
+ resp.Schema = schema.Schema{
+ MarkdownDescription: "Fetches a single VirtFusion DNS service by ID.",
+ Attributes: map[string]schema.Attribute{
+ "id": schema.Int64Attribute{
+ MarkdownDescription: "The DNS service ID.",
+ Required: true,
+ },
+ "name": schema.StringAttribute{
+ MarkdownDescription: "The DNS service name.",
+ Computed: true,
+ },
+ "type": schema.StringAttribute{
+ MarkdownDescription: "The DNS service type.",
+ Computed: true,
+ },
+ },
+ }
+}
+
+func (d *DNSServiceDataSource) Configure(_ context.Context, req datasource.ConfigureRequest, resp *datasource.ConfigureResponse) {
+ if req.ProviderData == nil {
+ return
+ }
+
+ c, ok := req.ProviderData.(*client.Client)
+ if !ok {
+ resp.Diagnostics.AddError(
+ "Unexpected Data Source Configure Type",
+ fmt.Sprintf("Expected *client.Client, got: %T.", req.ProviderData),
+ )
+ return
+ }
+
+ d.client = c
+}
+
+func (d *DNSServiceDataSource) Read(ctx context.Context, req datasource.ReadRequest, resp *datasource.ReadResponse) {
+ var data DNSServiceDataSourceModel
+ resp.Diagnostics.Append(req.Config.Get(ctx, &data)...)
+ if resp.Diagnostics.HasError() {
+ return
+ }
+
+ rawResp, err := d.client.Get(ctx, fmt.Sprintf("/dns/services/%d", data.ID.ValueInt64()))
+ if err != nil {
+ resp.Diagnostics.AddError("Error Reading DNS Service", err.Error())
+ return
+ }
+
+ var dnsResp client.DNSServiceResponse
+ if err := json.Unmarshal(rawResp, &dnsResp); err != nil {
+ resp.Diagnostics.AddError("Error Parsing DNS Service Response", err.Error())
+ return
+ }
+
+ data.ID = types.Int64Value(dnsResp.Data.ID)
+ data.Name = types.StringValue(dnsResp.Data.Name)
+ data.Type = types.StringValue(dnsResp.Data.Type)
+
+ resp.Diagnostics.Append(resp.State.Set(ctx, &data)...)
+}
diff --git a/internal/provider/data_source_hypervisor.go b/internal/provider/data_source_hypervisor.go
new file mode 100644
index 0000000..21a7c35
--- /dev/null
+++ b/internal/provider/data_source_hypervisor.go
@@ -0,0 +1,117 @@
+// Copyright (c) EZSCALE.
+// SPDX-License-Identifier: MPL-2.0
+
+package provider
+
+import (
+ "context"
+ "encoding/json"
+ "fmt"
+
+ "terraform-provider-virtfusion/internal/client"
+
+ "github.com/hashicorp/terraform-plugin-framework/datasource"
+ "github.com/hashicorp/terraform-plugin-framework/datasource/schema"
+ "github.com/hashicorp/terraform-plugin-framework/types"
+)
+
+var (
+ _ datasource.DataSource = &HypervisorDataSource{}
+ _ datasource.DataSourceWithConfigure = &HypervisorDataSource{}
+)
+
+// NewHypervisorDataSource returns a new hypervisor data source.
+func NewHypervisorDataSource() datasource.DataSource {
+ return &HypervisorDataSource{}
+}
+
+// HypervisorDataSource defines the data source implementation.
+type HypervisorDataSource struct {
+ client *client.Client
+}
+
+// HypervisorDataSourceModel describes the data source data model.
+type HypervisorDataSourceModel struct {
+ ID types.Int64 `tfsdk:"id"`
+ Name types.String `tfsdk:"name"`
+ Type types.String `tfsdk:"type"`
+ Hostname types.String `tfsdk:"hostname"`
+ Enabled types.Bool `tfsdk:"enabled"`
+}
+
+func (d *HypervisorDataSource) Metadata(_ context.Context, req datasource.MetadataRequest, resp *datasource.MetadataResponse) {
+ resp.TypeName = req.ProviderTypeName + "_hypervisor"
+}
+
+func (d *HypervisorDataSource) Schema(_ context.Context, _ datasource.SchemaRequest, resp *datasource.SchemaResponse) {
+ resp.Schema = schema.Schema{
+ MarkdownDescription: "Fetches a single VirtFusion hypervisor by ID.",
+ Attributes: map[string]schema.Attribute{
+ "id": schema.Int64Attribute{
+ MarkdownDescription: "The hypervisor ID.",
+ Required: true,
+ },
+ "name": schema.StringAttribute{
+ MarkdownDescription: "The hypervisor name.",
+ Computed: true,
+ },
+ "type": schema.StringAttribute{
+ MarkdownDescription: "The hypervisor type.",
+ Computed: true,
+ },
+ "hostname": schema.StringAttribute{
+ MarkdownDescription: "The hypervisor hostname.",
+ Computed: true,
+ },
+ "enabled": schema.BoolAttribute{
+ MarkdownDescription: "Whether the hypervisor is enabled.",
+ Computed: true,
+ },
+ },
+ }
+}
+
+func (d *HypervisorDataSource) Configure(_ context.Context, req datasource.ConfigureRequest, resp *datasource.ConfigureResponse) {
+ if req.ProviderData == nil {
+ return
+ }
+
+ c, ok := req.ProviderData.(*client.Client)
+ if !ok {
+ resp.Diagnostics.AddError(
+ "Unexpected Data Source Configure Type",
+ fmt.Sprintf("Expected *client.Client, got: %T.", req.ProviderData),
+ )
+ return
+ }
+
+ d.client = c
+}
+
+func (d *HypervisorDataSource) Read(ctx context.Context, req datasource.ReadRequest, resp *datasource.ReadResponse) {
+ var data HypervisorDataSourceModel
+ resp.Diagnostics.Append(req.Config.Get(ctx, &data)...)
+ if resp.Diagnostics.HasError() {
+ return
+ }
+
+ rawResp, err := d.client.Get(ctx, fmt.Sprintf("/compute/hypervisors/%d", data.ID.ValueInt64()))
+ if err != nil {
+ resp.Diagnostics.AddError("Error Reading Hypervisor", err.Error())
+ return
+ }
+
+ var hypervisorResp client.HypervisorResponse
+ if err := json.Unmarshal(rawResp, &hypervisorResp); err != nil {
+ resp.Diagnostics.AddError("Error Parsing Hypervisor Response", err.Error())
+ return
+ }
+
+ data.ID = types.Int64Value(hypervisorResp.Data.ID)
+ data.Name = types.StringValue(hypervisorResp.Data.Name)
+ data.Type = types.StringValue(hypervisorResp.Data.Type)
+ data.Hostname = types.StringValue(hypervisorResp.Data.Hostname)
+ data.Enabled = types.BoolValue(hypervisorResp.Data.Enabled)
+
+ resp.Diagnostics.Append(resp.State.Set(ctx, &data)...)
+}
diff --git a/internal/provider/data_source_hypervisor_group.go b/internal/provider/data_source_hypervisor_group.go
new file mode 100644
index 0000000..4be969f
--- /dev/null
+++ b/internal/provider/data_source_hypervisor_group.go
@@ -0,0 +1,105 @@
+// Copyright (c) EZSCALE.
+// SPDX-License-Identifier: MPL-2.0
+
+package provider
+
+import (
+ "context"
+ "encoding/json"
+ "fmt"
+
+ "terraform-provider-virtfusion/internal/client"
+
+ "github.com/hashicorp/terraform-plugin-framework/datasource"
+ "github.com/hashicorp/terraform-plugin-framework/datasource/schema"
+ "github.com/hashicorp/terraform-plugin-framework/types"
+)
+
+var (
+ _ datasource.DataSource = &HypervisorGroupDataSource{}
+ _ datasource.DataSourceWithConfigure = &HypervisorGroupDataSource{}
+)
+
+// NewHypervisorGroupDataSource returns a new hypervisor group data source.
+func NewHypervisorGroupDataSource() datasource.DataSource {
+ return &HypervisorGroupDataSource{}
+}
+
+// HypervisorGroupDataSource defines the data source implementation.
+type HypervisorGroupDataSource struct {
+ client *client.Client
+}
+
+// HypervisorGroupDataSourceModel describes the data source data model.
+type HypervisorGroupDataSourceModel struct {
+ ID types.Int64 `tfsdk:"id"`
+ Name types.String `tfsdk:"name"`
+ Enabled types.Bool `tfsdk:"enabled"`
+}
+
+func (d *HypervisorGroupDataSource) Metadata(_ context.Context, req datasource.MetadataRequest, resp *datasource.MetadataResponse) {
+ resp.TypeName = req.ProviderTypeName + "_hypervisor_group"
+}
+
+func (d *HypervisorGroupDataSource) Schema(_ context.Context, _ datasource.SchemaRequest, resp *datasource.SchemaResponse) {
+ resp.Schema = schema.Schema{
+ MarkdownDescription: "Fetches a single VirtFusion hypervisor group by ID.",
+ Attributes: map[string]schema.Attribute{
+ "id": schema.Int64Attribute{
+ MarkdownDescription: "The hypervisor group ID.",
+ Required: true,
+ },
+ "name": schema.StringAttribute{
+ MarkdownDescription: "The hypervisor group name.",
+ Computed: true,
+ },
+ "enabled": schema.BoolAttribute{
+ MarkdownDescription: "Whether the hypervisor group is enabled.",
+ Computed: true,
+ },
+ },
+ }
+}
+
+func (d *HypervisorGroupDataSource) Configure(_ context.Context, req datasource.ConfigureRequest, resp *datasource.ConfigureResponse) {
+ if req.ProviderData == nil {
+ return
+ }
+
+ c, ok := req.ProviderData.(*client.Client)
+ if !ok {
+ resp.Diagnostics.AddError(
+ "Unexpected Data Source Configure Type",
+ fmt.Sprintf("Expected *client.Client, got: %T.", req.ProviderData),
+ )
+ return
+ }
+
+ d.client = c
+}
+
+func (d *HypervisorGroupDataSource) Read(ctx context.Context, req datasource.ReadRequest, resp *datasource.ReadResponse) {
+ var data HypervisorGroupDataSourceModel
+ resp.Diagnostics.Append(req.Config.Get(ctx, &data)...)
+ if resp.Diagnostics.HasError() {
+ return
+ }
+
+ rawResp, err := d.client.Get(ctx, fmt.Sprintf("/compute/hypervisors/groups/%d", data.ID.ValueInt64()))
+ if err != nil {
+ resp.Diagnostics.AddError("Error Reading Hypervisor Group", err.Error())
+ return
+ }
+
+ var groupResp client.HypervisorGroupResponse
+ if err := json.Unmarshal(rawResp, &groupResp); err != nil {
+ resp.Diagnostics.AddError("Error Parsing Hypervisor Group Response", err.Error())
+ return
+ }
+
+ data.ID = types.Int64Value(groupResp.Data.ID)
+ data.Name = types.StringValue(groupResp.Data.Name)
+ data.Enabled = types.BoolValue(groupResp.Data.Enabled)
+
+ resp.Diagnostics.Append(resp.State.Set(ctx, &data)...)
+}
diff --git a/internal/provider/data_source_hypervisor_group_resources.go b/internal/provider/data_source_hypervisor_group_resources.go
new file mode 100644
index 0000000..5379f35
--- /dev/null
+++ b/internal/provider/data_source_hypervisor_group_resources.go
@@ -0,0 +1,119 @@
+// Copyright (c) EZSCALE.
+// SPDX-License-Identifier: MPL-2.0
+
+package provider
+
+import (
+ "context"
+ "encoding/json"
+ "fmt"
+
+ "terraform-provider-virtfusion/internal/client"
+
+ "github.com/hashicorp/terraform-plugin-framework/datasource"
+ "github.com/hashicorp/terraform-plugin-framework/datasource/schema"
+ "github.com/hashicorp/terraform-plugin-framework/types"
+)
+
+var (
+ _ datasource.DataSource = &HypervisorGroupResourcesDataSource{}
+ _ datasource.DataSourceWithConfigure = &HypervisorGroupResourcesDataSource{}
+)
+
+// NewHypervisorGroupResourcesDataSource returns a new hypervisor group resources data source.
+func NewHypervisorGroupResourcesDataSource() datasource.DataSource {
+ return &HypervisorGroupResourcesDataSource{}
+}
+
+// HypervisorGroupResourcesDataSource defines the data source implementation.
+type HypervisorGroupResourcesDataSource struct {
+ client *client.Client
+}
+
+// HypervisorGroupResourcesDataSourceModel describes the data source data model.
+type HypervisorGroupResourcesDataSourceModel struct {
+ ID types.Int64 `tfsdk:"id"`
+ Results types.Int64 `tfsdk:"results"`
+ CPUCores types.Int64 `tfsdk:"cpu_cores"`
+ Memory types.Int64 `tfsdk:"memory"`
+ Storage types.Int64 `tfsdk:"storage"`
+}
+
+func (d *HypervisorGroupResourcesDataSource) Metadata(_ context.Context, req datasource.MetadataRequest, resp *datasource.MetadataResponse) {
+ resp.TypeName = req.ProviderTypeName + "_hypervisor_group_resources"
+}
+
+func (d *HypervisorGroupResourcesDataSource) Schema(_ context.Context, _ datasource.SchemaRequest, resp *datasource.SchemaResponse) {
+ resp.Schema = schema.Schema{
+ MarkdownDescription: "Fetches resource information for a VirtFusion hypervisor group.",
+ Attributes: map[string]schema.Attribute{
+ "id": schema.Int64Attribute{
+ MarkdownDescription: "The hypervisor group ID.",
+ Required: true,
+ },
+ "results": resultsSchemaAttribute(),
+ "cpu_cores": schema.Int64Attribute{
+ MarkdownDescription: "The number of CPU cores available in the group.",
+ Computed: true,
+ },
+ "memory": schema.Int64Attribute{
+ MarkdownDescription: "The amount of memory available in the group.",
+ Computed: true,
+ },
+ "storage": schema.Int64Attribute{
+ MarkdownDescription: "The amount of storage available in the group.",
+ Computed: true,
+ },
+ },
+ }
+}
+
+func (d *HypervisorGroupResourcesDataSource) Configure(_ context.Context, req datasource.ConfigureRequest, resp *datasource.ConfigureResponse) {
+ if req.ProviderData == nil {
+ return
+ }
+
+ c, ok := req.ProviderData.(*client.Client)
+ if !ok {
+ resp.Diagnostics.AddError(
+ "Unexpected Data Source Configure Type",
+ fmt.Sprintf("Expected *client.Client, got: %T.", req.ProviderData),
+ )
+ return
+ }
+
+ d.client = c
+}
+
+func (d *HypervisorGroupResourcesDataSource) Read(ctx context.Context, req datasource.ReadRequest, resp *datasource.ReadResponse) {
+ var data HypervisorGroupResourcesDataSourceModel
+ resp.Diagnostics.Append(req.Config.Get(ctx, &data)...)
+ if resp.Diagnostics.HasError() {
+ return
+ }
+
+ rawResp, err := d.client.GetAllPages(ctx, fmt.Sprintf("/compute/hypervisors/groups/%d/resources?%s", data.ID.ValueInt64(), resultsQueryParam(data.Results)))
+ if err != nil {
+ resp.Diagnostics.AddError("Error Reading Hypervisor Group Resources", err.Error())
+ return
+ }
+
+ var resourcesResp client.HypervisorGroupResourcesResponse
+ if err := json.Unmarshal(rawResp, &resourcesResp); err != nil {
+ resp.Diagnostics.AddError("Error Parsing Hypervisor Group Resources Response", err.Error())
+ return
+ }
+
+ // Aggregate resource totals across all hypervisors in the group.
+ var totalCPU, totalMemory, totalStorage int64
+ for _, entry := range resourcesResp.Data {
+ totalCPU += entry.Resources.CPUCores.Max
+ totalMemory += entry.Resources.Memory.Max
+ totalStorage += entry.Resources.LocalStorage.Max
+ }
+ data.CPUCores = types.Int64Value(totalCPU)
+ data.Memory = types.Int64Value(totalMemory)
+ data.Storage = types.Int64Value(totalStorage)
+
+ resp.Diagnostics.Append(resp.State.Set(ctx, &data)...)
+}
diff --git a/internal/provider/data_source_hypervisor_groups.go b/internal/provider/data_source_hypervisor_groups.go
new file mode 100644
index 0000000..fb97b09
--- /dev/null
+++ b/internal/provider/data_source_hypervisor_groups.go
@@ -0,0 +1,121 @@
+// Copyright (c) EZSCALE.
+// SPDX-License-Identifier: MPL-2.0
+
+package provider
+
+import (
+ "context"
+ "encoding/json"
+ "fmt"
+
+ "terraform-provider-virtfusion/internal/client"
+
+ "github.com/hashicorp/terraform-plugin-framework/datasource"
+ "github.com/hashicorp/terraform-plugin-framework/datasource/schema"
+ "github.com/hashicorp/terraform-plugin-framework/types"
+)
+
+var (
+ _ datasource.DataSource = &HypervisorGroupsDataSource{}
+ _ datasource.DataSourceWithConfigure = &HypervisorGroupsDataSource{}
+)
+
+// NewHypervisorGroupsDataSource returns a new hypervisor groups data source.
+func NewHypervisorGroupsDataSource() datasource.DataSource {
+ return &HypervisorGroupsDataSource{}
+}
+
+// HypervisorGroupsDataSource defines the data source implementation.
+type HypervisorGroupsDataSource struct {
+ client *client.Client
+}
+
+// HypervisorGroupsDataSourceModel describes the data source data model.
+type HypervisorGroupsDataSourceModel struct {
+ Results types.Int64 `tfsdk:"results"`
+ Groups []HypervisorGroupItemModel `tfsdk:"groups"`
+}
+
+// HypervisorGroupItemModel describes a single hypervisor group in the list.
+type HypervisorGroupItemModel struct {
+ ID types.Int64 `tfsdk:"id"`
+ Name types.String `tfsdk:"name"`
+ Enabled types.Bool `tfsdk:"enabled"`
+}
+
+func (d *HypervisorGroupsDataSource) Metadata(_ context.Context, req datasource.MetadataRequest, resp *datasource.MetadataResponse) {
+ resp.TypeName = req.ProviderTypeName + "_hypervisor_groups"
+}
+
+func (d *HypervisorGroupsDataSource) Schema(_ context.Context, _ datasource.SchemaRequest, resp *datasource.SchemaResponse) {
+ resp.Schema = schema.Schema{
+ MarkdownDescription: "Fetches all VirtFusion hypervisor groups.",
+ Attributes: map[string]schema.Attribute{
+ "results": resultsSchemaAttribute(),
+ "groups": schema.ListNestedAttribute{
+ MarkdownDescription: "List of hypervisor groups.",
+ Computed: true,
+ NestedObject: schema.NestedAttributeObject{
+ Attributes: map[string]schema.Attribute{
+ "id": schema.Int64Attribute{
+ MarkdownDescription: "The hypervisor group ID.",
+ Computed: true,
+ },
+ "name": schema.StringAttribute{
+ MarkdownDescription: "The hypervisor group name.",
+ Computed: true,
+ },
+ "enabled": schema.BoolAttribute{
+ MarkdownDescription: "Whether the hypervisor group is enabled.",
+ Computed: true,
+ },
+ },
+ },
+ },
+ },
+ }
+}
+
+func (d *HypervisorGroupsDataSource) Configure(_ context.Context, req datasource.ConfigureRequest, resp *datasource.ConfigureResponse) {
+ if req.ProviderData == nil {
+ return
+ }
+
+ c, ok := req.ProviderData.(*client.Client)
+ if !ok {
+ resp.Diagnostics.AddError(
+ "Unexpected Data Source Configure Type",
+ fmt.Sprintf("Expected *client.Client, got: %T.", req.ProviderData),
+ )
+ return
+ }
+
+ d.client = c
+}
+
+func (d *HypervisorGroupsDataSource) Read(ctx context.Context, req datasource.ReadRequest, resp *datasource.ReadResponse) {
+ var data HypervisorGroupsDataSourceModel
+
+ rawResp, err := d.client.GetAllPages(ctx, fmt.Sprintf("/compute/hypervisors/groups?%s", resultsQueryParam(data.Results)))
+ if err != nil {
+ resp.Diagnostics.AddError("Error Reading Hypervisor Groups", err.Error())
+ return
+ }
+
+ var listResp client.HypervisorGroupListResponse
+ if err := json.Unmarshal(rawResp, &listResp); err != nil {
+ resp.Diagnostics.AddError("Error Parsing Hypervisor Groups Response", err.Error())
+ return
+ }
+
+ data.Groups = make([]HypervisorGroupItemModel, len(listResp.Data))
+ for i, g := range listResp.Data {
+ data.Groups[i] = HypervisorGroupItemModel{
+ ID: types.Int64Value(g.ID),
+ Name: types.StringValue(g.Name),
+ Enabled: types.BoolValue(g.Enabled),
+ }
+ }
+
+ resp.Diagnostics.Append(resp.State.Set(ctx, &data)...)
+}
diff --git a/internal/provider/data_source_hypervisors.go b/internal/provider/data_source_hypervisors.go
new file mode 100644
index 0000000..676cab2
--- /dev/null
+++ b/internal/provider/data_source_hypervisors.go
@@ -0,0 +1,133 @@
+// Copyright (c) EZSCALE.
+// SPDX-License-Identifier: MPL-2.0
+
+package provider
+
+import (
+ "context"
+ "encoding/json"
+ "fmt"
+
+ "terraform-provider-virtfusion/internal/client"
+
+ "github.com/hashicorp/terraform-plugin-framework/datasource"
+ "github.com/hashicorp/terraform-plugin-framework/datasource/schema"
+ "github.com/hashicorp/terraform-plugin-framework/types"
+)
+
+var (
+ _ datasource.DataSource = &HypervisorsDataSource{}
+ _ datasource.DataSourceWithConfigure = &HypervisorsDataSource{}
+)
+
+// NewHypervisorsDataSource returns a new hypervisors data source.
+func NewHypervisorsDataSource() datasource.DataSource {
+ return &HypervisorsDataSource{}
+}
+
+// HypervisorsDataSource defines the data source implementation.
+type HypervisorsDataSource struct {
+ client *client.Client
+}
+
+// HypervisorsDataSourceModel describes the data source data model.
+type HypervisorsDataSourceModel struct {
+ Results types.Int64 `tfsdk:"results"`
+ Hypervisors []HypervisorItemModel `tfsdk:"hypervisors"`
+}
+
+// HypervisorItemModel describes a single hypervisor in the list.
+type HypervisorItemModel struct {
+ ID types.Int64 `tfsdk:"id"`
+ Name types.String `tfsdk:"name"`
+ Type types.String `tfsdk:"type"`
+ Hostname types.String `tfsdk:"hostname"`
+ Enabled types.Bool `tfsdk:"enabled"`
+}
+
+func (d *HypervisorsDataSource) Metadata(_ context.Context, req datasource.MetadataRequest, resp *datasource.MetadataResponse) {
+ resp.TypeName = req.ProviderTypeName + "_hypervisors"
+}
+
+func (d *HypervisorsDataSource) Schema(_ context.Context, _ datasource.SchemaRequest, resp *datasource.SchemaResponse) {
+ resp.Schema = schema.Schema{
+ MarkdownDescription: "Fetches all VirtFusion hypervisors.",
+ Attributes: map[string]schema.Attribute{
+ "results": resultsSchemaAttribute(),
+ "hypervisors": schema.ListNestedAttribute{
+ MarkdownDescription: "List of hypervisors.",
+ Computed: true,
+ NestedObject: schema.NestedAttributeObject{
+ Attributes: map[string]schema.Attribute{
+ "id": schema.Int64Attribute{
+ MarkdownDescription: "The hypervisor ID.",
+ Computed: true,
+ },
+ "name": schema.StringAttribute{
+ MarkdownDescription: "The hypervisor name.",
+ Computed: true,
+ },
+ "type": schema.StringAttribute{
+ MarkdownDescription: "The hypervisor type.",
+ Computed: true,
+ },
+ "hostname": schema.StringAttribute{
+ MarkdownDescription: "The hypervisor hostname.",
+ Computed: true,
+ },
+ "enabled": schema.BoolAttribute{
+ MarkdownDescription: "Whether the hypervisor is enabled.",
+ Computed: true,
+ },
+ },
+ },
+ },
+ },
+ }
+}
+
+func (d *HypervisorsDataSource) Configure(_ context.Context, req datasource.ConfigureRequest, resp *datasource.ConfigureResponse) {
+ if req.ProviderData == nil {
+ return
+ }
+
+ c, ok := req.ProviderData.(*client.Client)
+ if !ok {
+ resp.Diagnostics.AddError(
+ "Unexpected Data Source Configure Type",
+ fmt.Sprintf("Expected *client.Client, got: %T.", req.ProviderData),
+ )
+ return
+ }
+
+ d.client = c
+}
+
+func (d *HypervisorsDataSource) Read(ctx context.Context, req datasource.ReadRequest, resp *datasource.ReadResponse) {
+ var data HypervisorsDataSourceModel
+
+ rawResp, err := d.client.GetAllPages(ctx, fmt.Sprintf("/compute/hypervisors?%s", resultsQueryParam(data.Results)))
+ if err != nil {
+ resp.Diagnostics.AddError("Error Reading Hypervisors", err.Error())
+ return
+ }
+
+ var listResp client.HypervisorListResponse
+ if err := json.Unmarshal(rawResp, &listResp); err != nil {
+ resp.Diagnostics.AddError("Error Parsing Hypervisors Response", err.Error())
+ return
+ }
+
+ data.Hypervisors = make([]HypervisorItemModel, len(listResp.Data))
+ for i, h := range listResp.Data {
+ data.Hypervisors[i] = HypervisorItemModel{
+ ID: types.Int64Value(h.ID),
+ Name: types.StringValue(h.Name),
+ Type: types.StringValue(h.Type),
+ Hostname: types.StringValue(h.Hostname),
+ Enabled: types.BoolValue(h.Enabled),
+ }
+ }
+
+ resp.Diagnostics.Append(resp.State.Set(ctx, &data)...)
+}
diff --git a/internal/provider/data_source_ip_block.go b/internal/provider/data_source_ip_block.go
new file mode 100644
index 0000000..0e05945
--- /dev/null
+++ b/internal/provider/data_source_ip_block.go
@@ -0,0 +1,123 @@
+// Copyright (c) EZSCALE.
+// SPDX-License-Identifier: MPL-2.0
+
+package provider
+
+import (
+ "context"
+ "encoding/json"
+ "fmt"
+
+ "terraform-provider-virtfusion/internal/client"
+
+ "github.com/hashicorp/terraform-plugin-framework/datasource"
+ "github.com/hashicorp/terraform-plugin-framework/datasource/schema"
+ "github.com/hashicorp/terraform-plugin-framework/types"
+)
+
+var (
+ _ datasource.DataSource = &IPBlockDataSource{}
+ _ datasource.DataSourceWithConfigure = &IPBlockDataSource{}
+)
+
+// NewIPBlockDataSource returns a new IP block data source.
+func NewIPBlockDataSource() datasource.DataSource {
+ return &IPBlockDataSource{}
+}
+
+// IPBlockDataSource defines the data source implementation.
+type IPBlockDataSource struct {
+ client *client.Client
+}
+
+// IPBlockDataSourceModel describes the data source data model.
+type IPBlockDataSourceModel struct {
+ ID types.Int64 `tfsdk:"id"`
+ Name types.String `tfsdk:"name"`
+ Type types.Int64 `tfsdk:"type"`
+ Gateway types.String `tfsdk:"gateway"`
+ Netmask types.String `tfsdk:"netmask"`
+ Enabled types.Bool `tfsdk:"enabled"`
+}
+
+func (d *IPBlockDataSource) Metadata(_ context.Context, req datasource.MetadataRequest, resp *datasource.MetadataResponse) {
+ resp.TypeName = req.ProviderTypeName + "_ip_block"
+}
+
+func (d *IPBlockDataSource) Schema(_ context.Context, _ datasource.SchemaRequest, resp *datasource.SchemaResponse) {
+ resp.Schema = schema.Schema{
+ MarkdownDescription: "Fetches a single VirtFusion IP block by ID.",
+ Attributes: map[string]schema.Attribute{
+ "id": schema.Int64Attribute{
+ MarkdownDescription: "The IP block ID.",
+ Required: true,
+ },
+ "name": schema.StringAttribute{
+ MarkdownDescription: "The IP block name.",
+ Computed: true,
+ },
+ "type": schema.Int64Attribute{
+ MarkdownDescription: "The IP block type (4 = IPv4, 6 = IPv6).",
+ Computed: true,
+ },
+ "gateway": schema.StringAttribute{
+ MarkdownDescription: "The IPv4 gateway address.",
+ Computed: true,
+ },
+ "netmask": schema.StringAttribute{
+ MarkdownDescription: "The IPv4 netmask.",
+ Computed: true,
+ },
+ "enabled": schema.BoolAttribute{
+ MarkdownDescription: "Whether the IP block is enabled.",
+ Computed: true,
+ },
+ },
+ }
+}
+
+func (d *IPBlockDataSource) Configure(_ context.Context, req datasource.ConfigureRequest, resp *datasource.ConfigureResponse) {
+ if req.ProviderData == nil {
+ return
+ }
+
+ c, ok := req.ProviderData.(*client.Client)
+ if !ok {
+ resp.Diagnostics.AddError(
+ "Unexpected Data Source Configure Type",
+ fmt.Sprintf("Expected *client.Client, got: %T.", req.ProviderData),
+ )
+ return
+ }
+
+ d.client = c
+}
+
+func (d *IPBlockDataSource) Read(ctx context.Context, req datasource.ReadRequest, resp *datasource.ReadResponse) {
+ var data IPBlockDataSourceModel
+ resp.Diagnostics.Append(req.Config.Get(ctx, &data)...)
+ if resp.Diagnostics.HasError() {
+ return
+ }
+
+ rawResp, err := d.client.Get(ctx, fmt.Sprintf("/connectivity/ipblocks/%d", data.ID.ValueInt64()))
+ if err != nil {
+ resp.Diagnostics.AddError("Error Reading IP Block", err.Error())
+ return
+ }
+
+ var blockResp client.IPBlockResponse
+ if err := json.Unmarshal(rawResp, &blockResp); err != nil {
+ resp.Diagnostics.AddError("Error Parsing IP Block Response", err.Error())
+ return
+ }
+
+ data.ID = types.Int64Value(blockResp.Data.ID)
+ data.Name = types.StringValue(blockResp.Data.Name)
+ data.Type = types.Int64Value(blockResp.Data.Type)
+ data.Gateway = types.StringValue(blockResp.Data.IPv4.Gateway)
+ data.Netmask = types.StringValue(blockResp.Data.IPv4.Netmask)
+ data.Enabled = types.BoolValue(blockResp.Data.Enabled)
+
+ resp.Diagnostics.Append(resp.State.Set(ctx, &data)...)
+}
diff --git a/internal/provider/data_source_ip_blocks.go b/internal/provider/data_source_ip_blocks.go
new file mode 100644
index 0000000..bbc1856
--- /dev/null
+++ b/internal/provider/data_source_ip_blocks.go
@@ -0,0 +1,139 @@
+// Copyright (c) EZSCALE.
+// SPDX-License-Identifier: MPL-2.0
+
+package provider
+
+import (
+ "context"
+ "encoding/json"
+ "fmt"
+
+ "terraform-provider-virtfusion/internal/client"
+
+ "github.com/hashicorp/terraform-plugin-framework/datasource"
+ "github.com/hashicorp/terraform-plugin-framework/datasource/schema"
+ "github.com/hashicorp/terraform-plugin-framework/types"
+)
+
+var (
+ _ datasource.DataSource = &IPBlocksDataSource{}
+ _ datasource.DataSourceWithConfigure = &IPBlocksDataSource{}
+)
+
+// NewIPBlocksDataSource returns a new IP blocks data source.
+func NewIPBlocksDataSource() datasource.DataSource {
+ return &IPBlocksDataSource{}
+}
+
+// IPBlocksDataSource defines the data source implementation.
+type IPBlocksDataSource struct {
+ client *client.Client
+}
+
+// IPBlocksDataSourceModel describes the data source data model.
+type IPBlocksDataSourceModel struct {
+ Results types.Int64 `tfsdk:"results"`
+ IPBlocks []IPBlockItemModel `tfsdk:"ip_blocks"`
+}
+
+// IPBlockItemModel describes a single IP block in the list.
+type IPBlockItemModel struct {
+ ID types.Int64 `tfsdk:"id"`
+ Name types.String `tfsdk:"name"`
+ Type types.Int64 `tfsdk:"type"`
+ Gateway types.String `tfsdk:"gateway"`
+ Netmask types.String `tfsdk:"netmask"`
+ Enabled types.Bool `tfsdk:"enabled"`
+}
+
+func (d *IPBlocksDataSource) Metadata(_ context.Context, req datasource.MetadataRequest, resp *datasource.MetadataResponse) {
+ resp.TypeName = req.ProviderTypeName + "_ip_blocks"
+}
+
+func (d *IPBlocksDataSource) Schema(_ context.Context, _ datasource.SchemaRequest, resp *datasource.SchemaResponse) {
+ resp.Schema = schema.Schema{
+ MarkdownDescription: "Fetches all VirtFusion IP blocks.",
+ Attributes: map[string]schema.Attribute{
+ "results": resultsSchemaAttribute(),
+ "ip_blocks": schema.ListNestedAttribute{
+ MarkdownDescription: "List of IP blocks.",
+ Computed: true,
+ NestedObject: schema.NestedAttributeObject{
+ Attributes: map[string]schema.Attribute{
+ "id": schema.Int64Attribute{
+ MarkdownDescription: "The IP block ID.",
+ Computed: true,
+ },
+ "name": schema.StringAttribute{
+ MarkdownDescription: "The IP block name.",
+ Computed: true,
+ },
+ "type": schema.Int64Attribute{
+ MarkdownDescription: "The IP block type (4 = IPv4, 6 = IPv6).",
+ Computed: true,
+ },
+ "gateway": schema.StringAttribute{
+ MarkdownDescription: "The IPv4 gateway address.",
+ Computed: true,
+ },
+ "netmask": schema.StringAttribute{
+ MarkdownDescription: "The IPv4 netmask.",
+ Computed: true,
+ },
+ "enabled": schema.BoolAttribute{
+ MarkdownDescription: "Whether the IP block is enabled.",
+ Computed: true,
+ },
+ },
+ },
+ },
+ },
+ }
+}
+
+func (d *IPBlocksDataSource) Configure(_ context.Context, req datasource.ConfigureRequest, resp *datasource.ConfigureResponse) {
+ if req.ProviderData == nil {
+ return
+ }
+
+ c, ok := req.ProviderData.(*client.Client)
+ if !ok {
+ resp.Diagnostics.AddError(
+ "Unexpected Data Source Configure Type",
+ fmt.Sprintf("Expected *client.Client, got: %T.", req.ProviderData),
+ )
+ return
+ }
+
+ d.client = c
+}
+
+func (d *IPBlocksDataSource) Read(ctx context.Context, req datasource.ReadRequest, resp *datasource.ReadResponse) {
+ var data IPBlocksDataSourceModel
+
+ rawResp, err := d.client.GetAllPages(ctx, fmt.Sprintf("/connectivity/ipblocks?%s", resultsQueryParam(data.Results)))
+ if err != nil {
+ resp.Diagnostics.AddError("Error Reading IP Blocks", err.Error())
+ return
+ }
+
+ var listResp client.IPBlockListResponse
+ if err := json.Unmarshal(rawResp, &listResp); err != nil {
+ resp.Diagnostics.AddError("Error Parsing IP Blocks Response", err.Error())
+ return
+ }
+
+ data.IPBlocks = make([]IPBlockItemModel, len(listResp.Data))
+ for i, b := range listResp.Data {
+ data.IPBlocks[i] = IPBlockItemModel{
+ ID: types.Int64Value(b.ID),
+ Name: types.StringValue(b.Name),
+ Type: types.Int64Value(b.Type),
+ Gateway: types.StringValue(b.IPv4.Gateway),
+ Netmask: types.StringValue(b.IPv4.Netmask),
+ Enabled: types.BoolValue(b.Enabled),
+ }
+ }
+
+ resp.Diagnostics.Append(resp.State.Set(ctx, &data)...)
+}
diff --git a/internal/provider/data_source_iso.go b/internal/provider/data_source_iso.go
new file mode 100644
index 0000000..3a74aa9
--- /dev/null
+++ b/internal/provider/data_source_iso.go
@@ -0,0 +1,99 @@
+// Copyright (c) EZSCALE.
+// SPDX-License-Identifier: MPL-2.0
+
+package provider
+
+import (
+ "context"
+ "encoding/json"
+ "fmt"
+
+ "terraform-provider-virtfusion/internal/client"
+
+ "github.com/hashicorp/terraform-plugin-framework/datasource"
+ "github.com/hashicorp/terraform-plugin-framework/datasource/schema"
+ "github.com/hashicorp/terraform-plugin-framework/types"
+)
+
+var (
+ _ datasource.DataSource = &ISODataSource{}
+ _ datasource.DataSourceWithConfigure = &ISODataSource{}
+)
+
+// NewISODataSource returns a new ISO data source.
+func NewISODataSource() datasource.DataSource {
+ return &ISODataSource{}
+}
+
+// ISODataSource defines the data source implementation.
+type ISODataSource struct {
+ client *client.Client
+}
+
+// ISODataSourceModel describes the data source data model.
+type ISODataSourceModel struct {
+ ID types.Int64 `tfsdk:"id"`
+ Name types.String `tfsdk:"name"`
+}
+
+func (d *ISODataSource) Metadata(_ context.Context, req datasource.MetadataRequest, resp *datasource.MetadataResponse) {
+ resp.TypeName = req.ProviderTypeName + "_iso"
+}
+
+func (d *ISODataSource) Schema(_ context.Context, _ datasource.SchemaRequest, resp *datasource.SchemaResponse) {
+ resp.Schema = schema.Schema{
+ MarkdownDescription: "Fetches a single VirtFusion ISO by ID.",
+ Attributes: map[string]schema.Attribute{
+ "id": schema.Int64Attribute{
+ MarkdownDescription: "The ISO ID.",
+ Required: true,
+ },
+ "name": schema.StringAttribute{
+ MarkdownDescription: "The ISO name.",
+ Computed: true,
+ },
+ },
+ }
+}
+
+func (d *ISODataSource) Configure(_ context.Context, req datasource.ConfigureRequest, resp *datasource.ConfigureResponse) {
+ if req.ProviderData == nil {
+ return
+ }
+
+ c, ok := req.ProviderData.(*client.Client)
+ if !ok {
+ resp.Diagnostics.AddError(
+ "Unexpected Data Source Configure Type",
+ fmt.Sprintf("Expected *client.Client, got: %T.", req.ProviderData),
+ )
+ return
+ }
+
+ d.client = c
+}
+
+func (d *ISODataSource) Read(ctx context.Context, req datasource.ReadRequest, resp *datasource.ReadResponse) {
+ var data ISODataSourceModel
+ resp.Diagnostics.Append(req.Config.Get(ctx, &data)...)
+ if resp.Diagnostics.HasError() {
+ return
+ }
+
+ rawResp, err := d.client.Get(ctx, fmt.Sprintf("/media/iso/%d", data.ID.ValueInt64()))
+ if err != nil {
+ resp.Diagnostics.AddError("Error Reading ISO", err.Error())
+ return
+ }
+
+ var isoResp client.ISOResponse
+ if err := json.Unmarshal(rawResp, &isoResp); err != nil {
+ resp.Diagnostics.AddError("Error Parsing ISO Response", err.Error())
+ return
+ }
+
+ data.ID = types.Int64Value(isoResp.Data.ID)
+ data.Name = types.StringValue(isoResp.Data.Name)
+
+ resp.Diagnostics.Append(resp.State.Set(ctx, &data)...)
+}
diff --git a/internal/provider/data_source_package.go b/internal/provider/data_source_package.go
new file mode 100644
index 0000000..6468b29
--- /dev/null
+++ b/internal/provider/data_source_package.go
@@ -0,0 +1,147 @@
+// Copyright (c) EZSCALE.
+// SPDX-License-Identifier: MPL-2.0
+
+package provider
+
+import (
+ "context"
+ "encoding/json"
+ "fmt"
+
+ "terraform-provider-virtfusion/internal/client"
+
+ "github.com/hashicorp/terraform-plugin-framework/datasource"
+ "github.com/hashicorp/terraform-plugin-framework/datasource/schema"
+ "github.com/hashicorp/terraform-plugin-framework/types"
+)
+
+var (
+ _ datasource.DataSource = &PackageDataSource{}
+ _ datasource.DataSourceWithConfigure = &PackageDataSource{}
+)
+
+// NewPackageDataSource returns a new package data source.
+func NewPackageDataSource() datasource.DataSource {
+ return &PackageDataSource{}
+}
+
+// PackageDataSource defines the data source implementation.
+type PackageDataSource struct {
+ client *client.Client
+}
+
+// PackageDataSourceModel describes the data source data model.
+type PackageDataSourceModel struct {
+ ID types.Int64 `tfsdk:"id"`
+ Name types.String `tfsdk:"name"`
+ Enabled types.Bool `tfsdk:"enabled"`
+ CPUCores types.Int64 `tfsdk:"cpu_cores"`
+ Memory types.Int64 `tfsdk:"memory"`
+ Storage types.Int64 `tfsdk:"storage"`
+ Traffic types.Int64 `tfsdk:"traffic"`
+ NetworkSpeedInbound types.Int64 `tfsdk:"network_speed_inbound"`
+ NetworkSpeedOutbound types.Int64 `tfsdk:"network_speed_outbound"`
+ Ipv4 types.Int64 `tfsdk:"ipv4"`
+}
+
+func (d *PackageDataSource) Metadata(_ context.Context, req datasource.MetadataRequest, resp *datasource.MetadataResponse) {
+ resp.TypeName = req.ProviderTypeName + "_package"
+}
+
+func (d *PackageDataSource) Schema(_ context.Context, _ datasource.SchemaRequest, resp *datasource.SchemaResponse) {
+ resp.Schema = schema.Schema{
+ MarkdownDescription: "Fetches a single VirtFusion package by ID.",
+ Attributes: map[string]schema.Attribute{
+ "id": schema.Int64Attribute{
+ MarkdownDescription: "The package ID.",
+ Required: true,
+ },
+ "name": schema.StringAttribute{
+ MarkdownDescription: "The package name.",
+ Computed: true,
+ },
+ "enabled": schema.BoolAttribute{
+ MarkdownDescription: "Whether the package is enabled.",
+ Computed: true,
+ },
+ "cpu_cores": schema.Int64Attribute{
+ MarkdownDescription: "The number of CPU cores in the package.",
+ Computed: true,
+ },
+ "memory": schema.Int64Attribute{
+ MarkdownDescription: "The amount of memory in the package.",
+ Computed: true,
+ },
+ "storage": schema.Int64Attribute{
+ MarkdownDescription: "The amount of storage in the package.",
+ Computed: true,
+ },
+ "traffic": schema.Int64Attribute{
+ MarkdownDescription: "The traffic limit in the package.",
+ Computed: true,
+ },
+ "network_speed_inbound": schema.Int64Attribute{
+ MarkdownDescription: "The inbound network speed in the package.",
+ Computed: true,
+ },
+ "network_speed_outbound": schema.Int64Attribute{
+ MarkdownDescription: "The outbound network speed in the package.",
+ Computed: true,
+ },
+ "ipv4": schema.Int64Attribute{
+ MarkdownDescription: "The number of IPv4 addresses in the package.",
+ Computed: true,
+ },
+ },
+ }
+}
+
+func (d *PackageDataSource) Configure(_ context.Context, req datasource.ConfigureRequest, resp *datasource.ConfigureResponse) {
+ if req.ProviderData == nil {
+ return
+ }
+
+ c, ok := req.ProviderData.(*client.Client)
+ if !ok {
+ resp.Diagnostics.AddError(
+ "Unexpected Data Source Configure Type",
+ fmt.Sprintf("Expected *client.Client, got: %T.", req.ProviderData),
+ )
+ return
+ }
+
+ d.client = c
+}
+
+func (d *PackageDataSource) Read(ctx context.Context, req datasource.ReadRequest, resp *datasource.ReadResponse) {
+ var data PackageDataSourceModel
+ resp.Diagnostics.Append(req.Config.Get(ctx, &data)...)
+ if resp.Diagnostics.HasError() {
+ return
+ }
+
+ rawResp, err := d.client.Get(ctx, fmt.Sprintf("/packages/%d", data.ID.ValueInt64()))
+ if err != nil {
+ resp.Diagnostics.AddError("Error Reading Package", err.Error())
+ return
+ }
+
+ var pkgResp client.PackageResponse
+ if err := json.Unmarshal(rawResp, &pkgResp); err != nil {
+ resp.Diagnostics.AddError("Error Parsing Package Response", err.Error())
+ return
+ }
+
+ data.ID = types.Int64Value(pkgResp.Data.ID)
+ data.Name = types.StringValue(pkgResp.Data.Name)
+ data.Enabled = types.BoolValue(pkgResp.Data.Enabled)
+ data.CPUCores = types.Int64Value(pkgResp.Data.CPUCores)
+ data.Memory = types.Int64Value(pkgResp.Data.Memory)
+ data.Storage = types.Int64Value(pkgResp.Data.Storage)
+ data.Traffic = types.Int64Value(pkgResp.Data.Traffic)
+ data.NetworkSpeedInbound = types.Int64Value(pkgResp.Data.NetworkSpeedInbound)
+ data.NetworkSpeedOutbound = types.Int64Value(pkgResp.Data.NetworkSpeedOutbound)
+ data.Ipv4 = types.Int64Value(pkgResp.Data.Ipv4)
+
+ resp.Diagnostics.Append(resp.State.Set(ctx, &data)...)
+}
diff --git a/internal/provider/data_source_package_templates.go b/internal/provider/data_source_package_templates.go
new file mode 100644
index 0000000..c847da0
--- /dev/null
+++ b/internal/provider/data_source_package_templates.go
@@ -0,0 +1,124 @@
+// Copyright (c) EZSCALE.
+// SPDX-License-Identifier: MPL-2.0
+
+package provider
+
+import (
+ "context"
+ "encoding/json"
+ "fmt"
+
+ "terraform-provider-virtfusion/internal/client"
+
+ "github.com/hashicorp/terraform-plugin-framework/datasource"
+ "github.com/hashicorp/terraform-plugin-framework/datasource/schema"
+ "github.com/hashicorp/terraform-plugin-framework/types"
+)
+
+var (
+ _ datasource.DataSource = &PackageTemplatesDataSource{}
+ _ datasource.DataSourceWithConfigure = &PackageTemplatesDataSource{}
+)
+
+// NewPackageTemplatesDataSource returns a new package templates data source.
+func NewPackageTemplatesDataSource() datasource.DataSource {
+ return &PackageTemplatesDataSource{}
+}
+
+// PackageTemplatesDataSource defines the data source implementation.
+type PackageTemplatesDataSource struct {
+ client *client.Client
+}
+
+// PackageTemplatesDataSourceModel describes the data source data model.
+type PackageTemplatesDataSourceModel struct {
+ PackageID types.Int64 `tfsdk:"package_id"`
+ Results types.Int64 `tfsdk:"results"`
+ Templates []PackageTemplateItemModel `tfsdk:"templates"`
+}
+
+// PackageTemplateItemModel describes a single template in the list.
+type PackageTemplateItemModel struct {
+ ID types.Int64 `tfsdk:"id"`
+ Name types.String `tfsdk:"name"`
+}
+
+func (d *PackageTemplatesDataSource) Metadata(_ context.Context, req datasource.MetadataRequest, resp *datasource.MetadataResponse) {
+ resp.TypeName = req.ProviderTypeName + "_package_templates"
+}
+
+func (d *PackageTemplatesDataSource) Schema(_ context.Context, _ datasource.SchemaRequest, resp *datasource.SchemaResponse) {
+ resp.Schema = schema.Schema{
+ MarkdownDescription: "Fetches templates available for a VirtFusion server package.",
+ Attributes: map[string]schema.Attribute{
+ "package_id": schema.Int64Attribute{
+ MarkdownDescription: "The package ID to fetch templates for.",
+ Required: true,
+ },
+ "results": resultsSchemaAttribute(),
+ "templates": schema.ListNestedAttribute{
+ MarkdownDescription: "List of templates available for the package.",
+ Computed: true,
+ NestedObject: schema.NestedAttributeObject{
+ Attributes: map[string]schema.Attribute{
+ "id": schema.Int64Attribute{
+ MarkdownDescription: "The template ID.",
+ Computed: true,
+ },
+ "name": schema.StringAttribute{
+ MarkdownDescription: "The template name.",
+ Computed: true,
+ },
+ },
+ },
+ },
+ },
+ }
+}
+
+func (d *PackageTemplatesDataSource) Configure(_ context.Context, req datasource.ConfigureRequest, resp *datasource.ConfigureResponse) {
+ if req.ProviderData == nil {
+ return
+ }
+
+ c, ok := req.ProviderData.(*client.Client)
+ if !ok {
+ resp.Diagnostics.AddError(
+ "Unexpected Data Source Configure Type",
+ fmt.Sprintf("Expected *client.Client, got: %T.", req.ProviderData),
+ )
+ return
+ }
+
+ d.client = c
+}
+
+func (d *PackageTemplatesDataSource) Read(ctx context.Context, req datasource.ReadRequest, resp *datasource.ReadResponse) {
+ var data PackageTemplatesDataSourceModel
+ resp.Diagnostics.Append(req.Config.Get(ctx, &data)...)
+ if resp.Diagnostics.HasError() {
+ return
+ }
+
+ rawResp, err := d.client.GetAllPages(ctx, fmt.Sprintf("/media/templates/fromServerPackageSpec/%d?%s", data.PackageID.ValueInt64(), resultsQueryParam(data.Results)))
+ if err != nil {
+ resp.Diagnostics.AddError("Error Reading Package Templates", err.Error())
+ return
+ }
+
+ var templateResp client.TemplateResponse
+ if err := json.Unmarshal(rawResp, &templateResp); err != nil {
+ resp.Diagnostics.AddError("Error Parsing Package Templates Response", err.Error())
+ return
+ }
+
+ data.Templates = make([]PackageTemplateItemModel, len(templateResp.Data))
+ for i, t := range templateResp.Data {
+ data.Templates[i] = PackageTemplateItemModel{
+ ID: types.Int64Value(t.ID),
+ Name: types.StringValue(t.Name),
+ }
+ }
+
+ resp.Diagnostics.Append(resp.State.Set(ctx, &data)...)
+}
diff --git a/internal/provider/data_source_packages.go b/internal/provider/data_source_packages.go
new file mode 100644
index 0000000..b377229
--- /dev/null
+++ b/internal/provider/data_source_packages.go
@@ -0,0 +1,163 @@
+// Copyright (c) EZSCALE.
+// SPDX-License-Identifier: MPL-2.0
+
+package provider
+
+import (
+ "context"
+ "encoding/json"
+ "fmt"
+
+ "terraform-provider-virtfusion/internal/client"
+
+ "github.com/hashicorp/terraform-plugin-framework/datasource"
+ "github.com/hashicorp/terraform-plugin-framework/datasource/schema"
+ "github.com/hashicorp/terraform-plugin-framework/types"
+)
+
+var (
+ _ datasource.DataSource = &PackagesDataSource{}
+ _ datasource.DataSourceWithConfigure = &PackagesDataSource{}
+)
+
+// NewPackagesDataSource returns a new packages data source.
+func NewPackagesDataSource() datasource.DataSource {
+ return &PackagesDataSource{}
+}
+
+// PackagesDataSource defines the data source implementation.
+type PackagesDataSource struct {
+ client *client.Client
+}
+
+// PackagesDataSourceModel describes the data source data model.
+type PackagesDataSourceModel struct {
+ Results types.Int64 `tfsdk:"results"`
+ Packages []PackageItemModel `tfsdk:"packages"`
+}
+
+// PackageItemModel describes a single package in the list.
+type PackageItemModel struct {
+ ID types.Int64 `tfsdk:"id"`
+ Name types.String `tfsdk:"name"`
+ Enabled types.Bool `tfsdk:"enabled"`
+ CPUCores types.Int64 `tfsdk:"cpu_cores"`
+ Memory types.Int64 `tfsdk:"memory"`
+ Storage types.Int64 `tfsdk:"storage"`
+ Traffic types.Int64 `tfsdk:"traffic"`
+ NetworkSpeedInbound types.Int64 `tfsdk:"network_speed_inbound"`
+ NetworkSpeedOutbound types.Int64 `tfsdk:"network_speed_outbound"`
+ Ipv4 types.Int64 `tfsdk:"ipv4"`
+}
+
+func (d *PackagesDataSource) Metadata(_ context.Context, req datasource.MetadataRequest, resp *datasource.MetadataResponse) {
+ resp.TypeName = req.ProviderTypeName + "_packages"
+}
+
+func (d *PackagesDataSource) Schema(_ context.Context, _ datasource.SchemaRequest, resp *datasource.SchemaResponse) {
+ resp.Schema = schema.Schema{
+ MarkdownDescription: "Fetches all VirtFusion packages.",
+ Attributes: map[string]schema.Attribute{
+ "results": resultsSchemaAttribute(),
+ "packages": schema.ListNestedAttribute{
+ MarkdownDescription: "List of packages.",
+ Computed: true,
+ NestedObject: schema.NestedAttributeObject{
+ Attributes: map[string]schema.Attribute{
+ "id": schema.Int64Attribute{
+ MarkdownDescription: "The package ID.",
+ Computed: true,
+ },
+ "name": schema.StringAttribute{
+ MarkdownDescription: "The package name.",
+ Computed: true,
+ },
+ "enabled": schema.BoolAttribute{
+ MarkdownDescription: "Whether the package is enabled.",
+ Computed: true,
+ },
+ "cpu_cores": schema.Int64Attribute{
+ MarkdownDescription: "The number of CPU cores in the package.",
+ Computed: true,
+ },
+ "memory": schema.Int64Attribute{
+ MarkdownDescription: "The amount of memory in the package.",
+ Computed: true,
+ },
+ "storage": schema.Int64Attribute{
+ MarkdownDescription: "The amount of storage in the package.",
+ Computed: true,
+ },
+ "traffic": schema.Int64Attribute{
+ MarkdownDescription: "The traffic limit in the package.",
+ Computed: true,
+ },
+ "network_speed_inbound": schema.Int64Attribute{
+ MarkdownDescription: "The inbound network speed in the package.",
+ Computed: true,
+ },
+ "network_speed_outbound": schema.Int64Attribute{
+ MarkdownDescription: "The outbound network speed in the package.",
+ Computed: true,
+ },
+ "ipv4": schema.Int64Attribute{
+ MarkdownDescription: "The number of IPv4 addresses in the package.",
+ Computed: true,
+ },
+ },
+ },
+ },
+ },
+ }
+}
+
+func (d *PackagesDataSource) Configure(_ context.Context, req datasource.ConfigureRequest, resp *datasource.ConfigureResponse) {
+ if req.ProviderData == nil {
+ return
+ }
+
+ c, ok := req.ProviderData.(*client.Client)
+ if !ok {
+ resp.Diagnostics.AddError(
+ "Unexpected Data Source Configure Type",
+ fmt.Sprintf("Expected *client.Client, got: %T.", req.ProviderData),
+ )
+ return
+ }
+
+ d.client = c
+}
+
+func (d *PackagesDataSource) Read(ctx context.Context, req datasource.ReadRequest, resp *datasource.ReadResponse) {
+ var data PackagesDataSourceModel
+
+ rawResp, err := d.client.GetAllPages(ctx, fmt.Sprintf("/packages?%s", resultsQueryParam(data.Results)))
+ if err != nil {
+ resp.Diagnostics.AddError("Error Reading Packages", err.Error())
+ return
+ }
+
+ var listResp client.PackageListResponse
+ if err := json.Unmarshal(rawResp, &listResp); err != nil {
+ resp.Diagnostics.AddError("Error Parsing Packages Response", err.Error())
+ return
+ }
+
+ data.Packages = make([]PackageItemModel, len(listResp.Data))
+ for i, p := range listResp.Data {
+ data.Packages[i] = PackageItemModel{
+ ID: types.Int64Value(p.ID),
+ Name: types.StringValue(p.Name),
+ Enabled: types.BoolValue(p.Enabled),
+ CPUCores: types.Int64Value(p.CPUCores),
+ Memory: types.Int64Value(p.Memory),
+ Storage: types.Int64Value(p.Storage),
+ Traffic: types.Int64Value(p.Traffic),
+ NetworkSpeedInbound: types.Int64Value(p.NetworkSpeedInbound),
+ NetworkSpeedOutbound: types.Int64Value(p.NetworkSpeedOutbound),
+ Ipv4: types.Int64Value(p.Ipv4),
+ }
+ }
+
+ resp.Diagnostics.Append(resp.State.Set(ctx, &data)...)
+}
diff --git a/internal/provider/data_source_queue_item.go b/internal/provider/data_source_queue_item.go
new file mode 100644
index 0000000..3674eb1
--- /dev/null
+++ b/internal/provider/data_source_queue_item.go
@@ -0,0 +1,111 @@
+// Copyright (c) EZSCALE.
+// SPDX-License-Identifier: MPL-2.0
+
+package provider
+
+import (
+ "context"
+ "encoding/json"
+ "fmt"
+
+ "terraform-provider-virtfusion/internal/client"
+
+ "github.com/hashicorp/terraform-plugin-framework/datasource"
+ "github.com/hashicorp/terraform-plugin-framework/datasource/schema"
+ "github.com/hashicorp/terraform-plugin-framework/types"
+)
+
+var (
+ _ datasource.DataSource = &QueueItemDataSource{}
+ _ datasource.DataSourceWithConfigure = &QueueItemDataSource{}
+)
+
+// NewQueueItemDataSource returns a new queue item data source.
+func NewQueueItemDataSource() datasource.DataSource {
+ return &QueueItemDataSource{}
+}
+
+// QueueItemDataSource defines the data source implementation.
+type QueueItemDataSource struct {
+ client *client.Client
+}
+
+// QueueItemDataSourceModel describes the data source data model.
+type QueueItemDataSourceModel struct {
+ ID types.Int64 `tfsdk:"id"`
+ Status types.String `tfsdk:"status"`
+ Action types.String `tfsdk:"action"`
+ CreatedAt types.String `tfsdk:"created_at"`
+}
+
+func (d *QueueItemDataSource) Metadata(_ context.Context, req datasource.MetadataRequest, resp *datasource.MetadataResponse) {
+ resp.TypeName = req.ProviderTypeName + "_queue_item"
+}
+
+func (d *QueueItemDataSource) Schema(_ context.Context, _ datasource.SchemaRequest, resp *datasource.SchemaResponse) {
+ resp.Schema = schema.Schema{
+ MarkdownDescription: "Fetches a single VirtFusion queue item by ID.",
+ Attributes: map[string]schema.Attribute{
+ "id": schema.Int64Attribute{
+ MarkdownDescription: "The queue item ID.",
+ Required: true,
+ },
+ "status": schema.StringAttribute{
+ MarkdownDescription: "The queue item status.",
+ Computed: true,
+ },
+ "action": schema.StringAttribute{
+ MarkdownDescription: "The queue item action.",
+ Computed: true,
+ },
+ "created_at": schema.StringAttribute{
+ MarkdownDescription: "The creation timestamp.",
+ Computed: true,
+ },
+ },
+ }
+}
+
+func (d *QueueItemDataSource) Configure(_ context.Context, req datasource.ConfigureRequest, resp *datasource.ConfigureResponse) {
+ if req.ProviderData == nil {
+ return
+ }
+
+ c, ok := req.ProviderData.(*client.Client)
+ if !ok {
+ resp.Diagnostics.AddError(
+ "Unexpected Data Source Configure Type",
+ fmt.Sprintf("Expected *client.Client, got: %T.", req.ProviderData),
+ )
+ return
+ }
+
+ d.client = c
+}
+
+func (d *QueueItemDataSource) Read(ctx context.Context, req datasource.ReadRequest, resp *datasource.ReadResponse) {
+ var data QueueItemDataSourceModel
+ resp.Diagnostics.Append(req.Config.Get(ctx, &data)...)
+ if resp.Diagnostics.HasError() {
+ return
+ }
+
+ rawResp, err := d.client.Get(ctx, fmt.Sprintf("/queue/%d", data.ID.ValueInt64()))
+ if err != nil {
+ resp.Diagnostics.AddError("Error Reading Queue Item", err.Error())
+ return
+ }
+
+ var queueResp client.QueueResponse
+ if err := json.Unmarshal(rawResp, &queueResp); err != nil {
+ resp.Diagnostics.AddError("Error Parsing Queue Item Response", err.Error())
+ return
+ }
+
+ data.ID = types.Int64Value(queueResp.Data.ID)
+ data.Status = types.StringValue(queueResp.Data.Status)
+ data.Action = types.StringValue(queueResp.Data.Action)
+ data.CreatedAt = types.StringValue(queueResp.Data.CreatedAt)
+
+ resp.Diagnostics.Append(resp.State.Set(ctx, &data)...)
+}
diff --git a/internal/provider/data_source_self_service_currencies.go b/internal/provider/data_source_self_service_currencies.go
new file mode 100644
index 0000000..c304fad
--- /dev/null
+++ b/internal/provider/data_source_self_service_currencies.go
@@ -0,0 +1,121 @@
+// Copyright (c) EZSCALE.
+// SPDX-License-Identifier: MPL-2.0
+
+package provider
+
+import (
+ "context"
+ "encoding/json"
+ "fmt"
+
+ "terraform-provider-virtfusion/internal/client"
+
+ "github.com/hashicorp/terraform-plugin-framework/datasource"
+ "github.com/hashicorp/terraform-plugin-framework/datasource/schema"
+ "github.com/hashicorp/terraform-plugin-framework/types"
+)
+
+var (
+ _ datasource.DataSource = &SelfServiceCurrenciesDataSource{}
+ _ datasource.DataSourceWithConfigure = &SelfServiceCurrenciesDataSource{}
+)
+
+// NewSelfServiceCurrenciesDataSource returns a new self-service currencies data source.
+func NewSelfServiceCurrenciesDataSource() datasource.DataSource {
+ return &SelfServiceCurrenciesDataSource{}
+}
+
+// SelfServiceCurrenciesDataSource defines the data source implementation.
+type SelfServiceCurrenciesDataSource struct {
+ client *client.Client
+}
+
+// SelfServiceCurrenciesDataSourceModel describes the data source data model.
+type SelfServiceCurrenciesDataSourceModel struct {
+ Results types.Int64 `tfsdk:"results"`
+ Currencies []CurrencyItemModel `tfsdk:"currencies"`
+}
+
+// CurrencyItemModel describes a single currency in the list.
+type CurrencyItemModel struct {
+ ID types.Int64 `tfsdk:"id"`
+ Code types.String `tfsdk:"code"`
+ Name types.String `tfsdk:"name"`
+}
+
+func (d *SelfServiceCurrenciesDataSource) Metadata(_ context.Context, req datasource.MetadataRequest, resp *datasource.MetadataResponse) {
+ resp.TypeName = req.ProviderTypeName + "_self_service_currencies"
+}
+
+func (d *SelfServiceCurrenciesDataSource) Schema(_ context.Context, _ datasource.SchemaRequest, resp *datasource.SchemaResponse) {
+ resp.Schema = schema.Schema{
+ MarkdownDescription: "Fetches all available VirtFusion self-service currencies.",
+ Attributes: map[string]schema.Attribute{
+ "results": resultsSchemaAttribute(),
+ "currencies": schema.ListNestedAttribute{
+ MarkdownDescription: "List of available currencies.",
+ Computed: true,
+ NestedObject: schema.NestedAttributeObject{
+ Attributes: map[string]schema.Attribute{
+ "id": schema.Int64Attribute{
+ MarkdownDescription: "The currency ID.",
+ Computed: true,
+ },
+ "code": schema.StringAttribute{
+ MarkdownDescription: "The currency code.",
+ Computed: true,
+ },
+ "name": schema.StringAttribute{
+ MarkdownDescription: "The currency name.",
+ Computed: true,
+ },
+ },
+ },
+ },
+ },
+ }
+}
+
+func (d *SelfServiceCurrenciesDataSource) Configure(_ context.Context, req datasource.ConfigureRequest, resp *datasource.ConfigureResponse) {
+ if req.ProviderData == nil {
+ return
+ }
+
+ c, ok := req.ProviderData.(*client.Client)
+ if !ok {
+ resp.Diagnostics.AddError(
+ "Unexpected Data Source Configure Type",
+ fmt.Sprintf("Expected *client.Client, got: %T.", req.ProviderData),
+ )
+ return
+ }
+
+ d.client = c
+}
+
+func (d *SelfServiceCurrenciesDataSource) Read(ctx context.Context, req datasource.ReadRequest, resp *datasource.ReadResponse) {
+ var data SelfServiceCurrenciesDataSourceModel
+
+ rawResp, err := d.client.GetAllPages(ctx, fmt.Sprintf("/selfService/currencies?%s", resultsQueryParam(data.Results)))
+ if err != nil {
+ resp.Diagnostics.AddError("Error Reading Self-Service Currencies", err.Error())
+ return
+ }
+
+ var currencyResp client.CurrencyResponse
+ if err := json.Unmarshal(rawResp, ¤cyResp); err != nil {
+ resp.Diagnostics.AddError("Error Parsing Self-Service Currencies Response", err.Error())
+ return
+ }
+
+ data.Currencies = make([]CurrencyItemModel, len(currencyResp.Data))
+ for i, c := range currencyResp.Data {
+ data.Currencies[i] = CurrencyItemModel{
+ ID: types.Int64Value(c.ID),
+ Code: types.StringValue(c.Code),
+ Name: types.StringValue(c.Name),
+ }
+ }
+
+ resp.Diagnostics.Append(resp.State.Set(ctx, &data)...)
+}
diff --git a/internal/provider/data_source_self_service_hourly_stats.go b/internal/provider/data_source_self_service_hourly_stats.go
new file mode 100644
index 0000000..aaa34d6
--- /dev/null
+++ b/internal/provider/data_source_self_service_hourly_stats.go
@@ -0,0 +1,96 @@
+// Copyright (c) EZSCALE.
+// SPDX-License-Identifier: MPL-2.0
+
+package provider
+
+import (
+ "context"
+ "fmt"
+
+ "terraform-provider-virtfusion/internal/client"
+
+ "github.com/hashicorp/terraform-plugin-framework/datasource"
+ "github.com/hashicorp/terraform-plugin-framework/datasource/schema"
+ "github.com/hashicorp/terraform-plugin-framework/types"
+)
+
+var (
+ _ datasource.DataSource = &SelfServiceHourlyStatsDataSource{}
+ _ datasource.DataSourceWithConfigure = &SelfServiceHourlyStatsDataSource{}
+)
+
+// NewSelfServiceHourlyStatsDataSource returns a new self-service hourly stats data source.
+func NewSelfServiceHourlyStatsDataSource() datasource.DataSource {
+ return &SelfServiceHourlyStatsDataSource{}
+}
+
+// SelfServiceHourlyStatsDataSource defines the data source implementation.
+type SelfServiceHourlyStatsDataSource struct {
+ client *client.Client
+}
+
+// SelfServiceHourlyStatsDataSourceModel describes the data source data model.
+type SelfServiceHourlyStatsDataSourceModel struct {
+ UserID types.Int64 `tfsdk:"user_id"`
+ GroupID types.Int64 `tfsdk:"group_id"`
+ StatsJSON types.String `tfsdk:"stats_json"`
+}
+
+func (d *SelfServiceHourlyStatsDataSource) Metadata(_ context.Context, req datasource.MetadataRequest, resp *datasource.MetadataResponse) {
+ resp.TypeName = req.ProviderTypeName + "_self_service_hourly_stats"
+}
+
+func (d *SelfServiceHourlyStatsDataSource) Schema(_ context.Context, _ datasource.SchemaRequest, resp *datasource.SchemaResponse) {
+ resp.Schema = schema.Schema{
+ MarkdownDescription: "Fetches VirtFusion self-service hourly stats for a user and group.",
+ Attributes: map[string]schema.Attribute{
+ "user_id": schema.Int64Attribute{
+ MarkdownDescription: "The user ID to fetch hourly stats for.",
+ Required: true,
+ },
+ "group_id": schema.Int64Attribute{
+ MarkdownDescription: "The group ID to fetch hourly stats for.",
+ Required: true,
+ },
+ "stats_json": schema.StringAttribute{
+ MarkdownDescription: "The raw JSON response containing hourly stats.",
+ Computed: true,
+ },
+ },
+ }
+}
+
+func (d *SelfServiceHourlyStatsDataSource) Configure(_ context.Context, req datasource.ConfigureRequest, resp *datasource.ConfigureResponse) {
+ if req.ProviderData == nil {
+ return
+ }
+
+ c, ok := req.ProviderData.(*client.Client)
+ if !ok {
+ resp.Diagnostics.AddError(
+ "Unexpected Data Source Configure Type",
+ fmt.Sprintf("Expected *client.Client, got: %T.", req.ProviderData),
+ )
+ return
+ }
+
+ d.client = c
+}
+
+func (d *SelfServiceHourlyStatsDataSource) Read(ctx context.Context, req datasource.ReadRequest, resp *datasource.ReadResponse) {
+ var data SelfServiceHourlyStatsDataSourceModel
+ resp.Diagnostics.Append(req.Config.Get(ctx, &data)...)
+ if resp.Diagnostics.HasError() {
+ return
+ }
+
+ rawResp, err := d.client.Get(ctx, fmt.Sprintf("/selfService/hourlyStats/byUser/%d/group/%d", data.UserID.ValueInt64(), data.GroupID.ValueInt64()))
+ if err != nil {
+ resp.Diagnostics.AddError("Error Reading Self-Service Hourly Stats", err.Error())
+ return
+ }
+
+ data.StatsJSON = types.StringValue(string(rawResp))
+
+ resp.Diagnostics.Append(resp.State.Set(ctx, &data)...)
+}
diff --git a/internal/provider/data_source_self_service_report.go b/internal/provider/data_source_self_service_report.go
new file mode 100644
index 0000000..270faaa
--- /dev/null
+++ b/internal/provider/data_source_self_service_report.go
@@ -0,0 +1,96 @@
+// Copyright (c) EZSCALE.
+// SPDX-License-Identifier: MPL-2.0
+
+package provider
+
+import (
+ "context"
+ "fmt"
+
+ "terraform-provider-virtfusion/internal/client"
+
+ "github.com/hashicorp/terraform-plugin-framework/datasource"
+ "github.com/hashicorp/terraform-plugin-framework/datasource/schema"
+ "github.com/hashicorp/terraform-plugin-framework/types"
+)
+
+var (
+ _ datasource.DataSource = &SelfServiceReportDataSource{}
+ _ datasource.DataSourceWithConfigure = &SelfServiceReportDataSource{}
+)
+
+// NewSelfServiceReportDataSource returns a new self-service report data source.
+func NewSelfServiceReportDataSource() datasource.DataSource {
+ return &SelfServiceReportDataSource{}
+}
+
+// SelfServiceReportDataSource defines the data source implementation.
+type SelfServiceReportDataSource struct {
+ client *client.Client
+}
+
+// SelfServiceReportDataSourceModel describes the data source data model.
+type SelfServiceReportDataSourceModel struct {
+ UserID types.Int64 `tfsdk:"user_id"`
+ GroupID types.Int64 `tfsdk:"group_id"`
+ ReportJSON types.String `tfsdk:"report_json"`
+}
+
+func (d *SelfServiceReportDataSource) Metadata(_ context.Context, req datasource.MetadataRequest, resp *datasource.MetadataResponse) {
+ resp.TypeName = req.ProviderTypeName + "_self_service_report"
+}
+
+func (d *SelfServiceReportDataSource) Schema(_ context.Context, _ datasource.SchemaRequest, resp *datasource.SchemaResponse) {
+ resp.Schema = schema.Schema{
+ MarkdownDescription: "Fetches a VirtFusion self-service report for a user and group.",
+ Attributes: map[string]schema.Attribute{
+ "user_id": schema.Int64Attribute{
+ MarkdownDescription: "The user ID to fetch the report for.",
+ Required: true,
+ },
+ "group_id": schema.Int64Attribute{
+ MarkdownDescription: "The group ID to fetch the report for.",
+ Required: true,
+ },
+ "report_json": schema.StringAttribute{
+ MarkdownDescription: "The raw JSON response containing the report.",
+ Computed: true,
+ },
+ },
+ }
+}
+
+func (d *SelfServiceReportDataSource) Configure(_ context.Context, req datasource.ConfigureRequest, resp *datasource.ConfigureResponse) {
+ if req.ProviderData == nil {
+ return
+ }
+
+ c, ok := req.ProviderData.(*client.Client)
+ if !ok {
+ resp.Diagnostics.AddError(
+ "Unexpected Data Source Configure Type",
+ fmt.Sprintf("Expected *client.Client, got: %T.", req.ProviderData),
+ )
+ return
+ }
+
+ d.client = c
+}
+
+func (d *SelfServiceReportDataSource) Read(ctx context.Context, req datasource.ReadRequest, resp *datasource.ReadResponse) {
+ var data SelfServiceReportDataSourceModel
+ resp.Diagnostics.Append(req.Config.Get(ctx, &data)...)
+ if resp.Diagnostics.HasError() {
+ return
+ }
+
+ rawResp, err := d.client.Get(ctx, fmt.Sprintf("/selfService/report/byUser/%d/group/%d", data.UserID.ValueInt64(), data.GroupID.ValueInt64()))
+ if err != nil {
+ resp.Diagnostics.AddError("Error Reading Self-Service Report", err.Error())
+ return
+ }
+
+ data.ReportJSON = types.StringValue(string(rawResp))
+
+ resp.Diagnostics.Append(resp.State.Set(ctx, &data)...)
+}
diff --git a/internal/provider/data_source_self_service_resource_pack.go b/internal/provider/data_source_self_service_resource_pack.go
new file mode 100644
index 0000000..6f1d521
--- /dev/null
+++ b/internal/provider/data_source_self_service_resource_pack.go
@@ -0,0 +1,111 @@
+// Copyright (c) EZSCALE.
+// SPDX-License-Identifier: MPL-2.0
+
+package provider
+
+import (
+ "context"
+ "encoding/json"
+ "fmt"
+
+ "terraform-provider-virtfusion/internal/client"
+
+ "github.com/hashicorp/terraform-plugin-framework/datasource"
+ "github.com/hashicorp/terraform-plugin-framework/datasource/schema"
+ "github.com/hashicorp/terraform-plugin-framework/types"
+)
+
+var (
+ _ datasource.DataSource = &SelfServiceResourcePackDataSource{}
+ _ datasource.DataSourceWithConfigure = &SelfServiceResourcePackDataSource{}
+)
+
+// NewSelfServiceResourcePackDataSource returns a new self-service resource pack data source.
+func NewSelfServiceResourcePackDataSource() datasource.DataSource {
+ return &SelfServiceResourcePackDataSource{}
+}
+
+// SelfServiceResourcePackDataSource defines the data source implementation.
+type SelfServiceResourcePackDataSource struct {
+ client *client.Client
+}
+
+// SelfServiceResourcePackDataSourceModel describes the data source data model.
+type SelfServiceResourcePackDataSourceModel struct {
+ ID types.Int64 `tfsdk:"id"`
+ Name types.String `tfsdk:"name"`
+ UserID types.Int64 `tfsdk:"user_id"`
+ PackID types.Int64 `tfsdk:"pack_id"`
+}
+
+func (d *SelfServiceResourcePackDataSource) Metadata(_ context.Context, req datasource.MetadataRequest, resp *datasource.MetadataResponse) {
+ resp.TypeName = req.ProviderTypeName + "_self_service_resource_pack"
+}
+
+func (d *SelfServiceResourcePackDataSource) Schema(_ context.Context, _ datasource.SchemaRequest, resp *datasource.SchemaResponse) {
+ resp.Schema = schema.Schema{
+ MarkdownDescription: "Fetches a single VirtFusion self-service resource pack by ID.",
+ Attributes: map[string]schema.Attribute{
+ "id": schema.Int64Attribute{
+ MarkdownDescription: "The resource pack ID.",
+ Required: true,
+ },
+ "name": schema.StringAttribute{
+ MarkdownDescription: "The resource pack name.",
+ Computed: true,
+ },
+ "user_id": schema.Int64Attribute{
+ MarkdownDescription: "The user ID associated with the resource pack.",
+ Computed: true,
+ },
+ "pack_id": schema.Int64Attribute{
+ MarkdownDescription: "The pack ID associated with the resource pack.",
+ Computed: true,
+ },
+ },
+ }
+}
+
+func (d *SelfServiceResourcePackDataSource) Configure(_ context.Context, req datasource.ConfigureRequest, resp *datasource.ConfigureResponse) {
+ if req.ProviderData == nil {
+ return
+ }
+
+ c, ok := req.ProviderData.(*client.Client)
+ if !ok {
+ resp.Diagnostics.AddError(
+ "Unexpected Data Source Configure Type",
+ fmt.Sprintf("Expected *client.Client, got: %T.", req.ProviderData),
+ )
+ return
+ }
+
+ d.client = c
+}
+
+func (d *SelfServiceResourcePackDataSource) Read(ctx context.Context, req datasource.ReadRequest, resp *datasource.ReadResponse) {
+ var data SelfServiceResourcePackDataSourceModel
+ resp.Diagnostics.Append(req.Config.Get(ctx, &data)...)
+ if resp.Diagnostics.HasError() {
+ return
+ }
+
+ rawResp, err := d.client.Get(ctx, fmt.Sprintf("/selfService/resourcePack/%d", data.ID.ValueInt64()))
+ if err != nil {
+ resp.Diagnostics.AddError("Error Reading Self-Service Resource Pack", err.Error())
+ return
+ }
+
+ var packResp client.SelfServiceResourcePackResponse
+ if err := json.Unmarshal(rawResp, &packResp); err != nil {
+ resp.Diagnostics.AddError("Error Parsing Self-Service Resource Pack Response", err.Error())
+ return
+ }
+
+ data.ID = types.Int64Value(packResp.Data.ID)
+ data.Name = types.StringValue(packResp.Data.Name)
+ data.UserID = types.Int64Value(packResp.Data.UserID)
+ data.PackID = types.Int64Value(packResp.Data.PackID)
+
+ resp.Diagnostics.Append(resp.State.Set(ctx, &data)...)
+}
diff --git a/internal/provider/data_source_self_service_usage.go b/internal/provider/data_source_self_service_usage.go
new file mode 100644
index 0000000..a9d8052
--- /dev/null
+++ b/internal/provider/data_source_self_service_usage.go
@@ -0,0 +1,96 @@
+// Copyright (c) EZSCALE.
+// SPDX-License-Identifier: MPL-2.0
+
+package provider
+
+import (
+ "context"
+ "fmt"
+
+ "terraform-provider-virtfusion/internal/client"
+
+ "github.com/hashicorp/terraform-plugin-framework/datasource"
+ "github.com/hashicorp/terraform-plugin-framework/datasource/schema"
+ "github.com/hashicorp/terraform-plugin-framework/types"
+)
+
+var (
+ _ datasource.DataSource = &SelfServiceUsageDataSource{}
+ _ datasource.DataSourceWithConfigure = &SelfServiceUsageDataSource{}
+)
+
+// NewSelfServiceUsageDataSource returns a new self-service usage data source.
+func NewSelfServiceUsageDataSource() datasource.DataSource {
+ return &SelfServiceUsageDataSource{}
+}
+
+// SelfServiceUsageDataSource defines the data source implementation.
+type SelfServiceUsageDataSource struct {
+ client *client.Client
+}
+
+// SelfServiceUsageDataSourceModel describes the data source data model.
+type SelfServiceUsageDataSourceModel struct {
+ UserID types.Int64 `tfsdk:"user_id"`
+ GroupID types.Int64 `tfsdk:"group_id"`
+ UsageJSON types.String `tfsdk:"usage_json"`
+}
+
+func (d *SelfServiceUsageDataSource) Metadata(_ context.Context, req datasource.MetadataRequest, resp *datasource.MetadataResponse) {
+ resp.TypeName = req.ProviderTypeName + "_self_service_usage"
+}
+
+func (d *SelfServiceUsageDataSource) Schema(_ context.Context, _ datasource.SchemaRequest, resp *datasource.SchemaResponse) {
+ resp.Schema = schema.Schema{
+ MarkdownDescription: "Fetches VirtFusion self-service usage data for a user and group.",
+ Attributes: map[string]schema.Attribute{
+ "user_id": schema.Int64Attribute{
+ MarkdownDescription: "The user ID to fetch usage data for.",
+ Required: true,
+ },
+ "group_id": schema.Int64Attribute{
+ MarkdownDescription: "The group ID to fetch usage data for.",
+ Required: true,
+ },
+ "usage_json": schema.StringAttribute{
+ MarkdownDescription: "The raw JSON response containing usage data.",
+ Computed: true,
+ },
+ },
+ }
+}
+
+func (d *SelfServiceUsageDataSource) Configure(_ context.Context, req datasource.ConfigureRequest, resp *datasource.ConfigureResponse) {
+ if req.ProviderData == nil {
+ return
+ }
+
+ c, ok := req.ProviderData.(*client.Client)
+ if !ok {
+ resp.Diagnostics.AddError(
+ "Unexpected Data Source Configure Type",
+ fmt.Sprintf("Expected *client.Client, got: %T.", req.ProviderData),
+ )
+ return
+ }
+
+ d.client = c
+}
+
+func (d *SelfServiceUsageDataSource) Read(ctx context.Context, req datasource.ReadRequest, resp *datasource.ReadResponse) {
+ var data SelfServiceUsageDataSourceModel
+ resp.Diagnostics.Append(req.Config.Get(ctx, &data)...)
+ if resp.Diagnostics.HasError() {
+ return
+ }
+
+ rawResp, err := d.client.Get(ctx, fmt.Sprintf("/selfService/usage/byUser/%d/group/%d", data.UserID.ValueInt64(), data.GroupID.ValueInt64()))
+ if err != nil {
+ resp.Diagnostics.AddError("Error Reading Self-Service Usage", err.Error())
+ return
+ }
+
+ data.UsageJSON = types.StringValue(string(rawResp))
+
+ resp.Diagnostics.Append(resp.State.Set(ctx, &data)...)
+}
diff --git a/internal/provider/data_source_server.go b/internal/provider/data_source_server.go
new file mode 100644
index 0000000..4117035
--- /dev/null
+++ b/internal/provider/data_source_server.go
@@ -0,0 +1,156 @@
+// Copyright (c) EZSCALE.
+// SPDX-License-Identifier: MPL-2.0
+
+package provider
+
+import (
+ "context"
+ "encoding/json"
+ "fmt"
+
+ "terraform-provider-virtfusion/internal/client"
+
+ "github.com/hashicorp/terraform-plugin-framework/datasource"
+ "github.com/hashicorp/terraform-plugin-framework/datasource/schema"
+ "github.com/hashicorp/terraform-plugin-framework/types"
+)
+
+var _ datasource.DataSource = &ServerDataSource{}
+var _ datasource.DataSourceWithConfigure = &ServerDataSource{}
+
+// NewServerDataSource returns a new data source for reading a single server.
+func NewServerDataSource() datasource.DataSource {
+ return &ServerDataSource{}
+}
+
+// ServerDataSource defines the data source implementation.
+type ServerDataSource struct {
+ client *client.Client
+}
+
+// ServerDataSourceModel describes the data source data model.
+type ServerDataSourceModel struct {
+ ID types.Int64 `tfsdk:"id"`
+ UUID types.String `tfsdk:"uuid"`
+ Name types.String `tfsdk:"name"`
+ Hostname types.String `tfsdk:"hostname"`
+ OwnerID types.Int64 `tfsdk:"owner_id"`
+ HypervisorID types.Int64 `tfsdk:"hypervisor_id"`
+ Suspended types.Bool `tfsdk:"suspended"`
+ CPUCores types.Int64 `tfsdk:"cpu_cores"`
+ Memory types.Int64 `tfsdk:"memory"`
+ Storage types.Int64 `tfsdk:"storage"`
+}
+
+func (d *ServerDataSource) Metadata(_ context.Context, req datasource.MetadataRequest, resp *datasource.MetadataResponse) {
+ resp.TypeName = req.ProviderTypeName + "_server"
+}
+
+func (d *ServerDataSource) Schema(_ context.Context, _ datasource.SchemaRequest, resp *datasource.SchemaResponse) {
+ resp.Schema = schema.Schema{
+ MarkdownDescription: "Use this data source to read a single VirtFusion server by ID.",
+ Attributes: map[string]schema.Attribute{
+ "id": schema.Int64Attribute{
+ MarkdownDescription: "The server ID.",
+ Required: true,
+ },
+ "uuid": schema.StringAttribute{
+ MarkdownDescription: "The server UUID.",
+ Computed: true,
+ },
+ "name": schema.StringAttribute{
+ MarkdownDescription: "The server display name.",
+ Computed: true,
+ },
+ "hostname": schema.StringAttribute{
+ MarkdownDescription: "The server hostname.",
+ Computed: true,
+ },
+ "owner_id": schema.Int64Attribute{
+ MarkdownDescription: "The owner (user) ID who owns the server.",
+ Computed: true,
+ },
+ "hypervisor_id": schema.Int64Attribute{
+ MarkdownDescription: "The hypervisor ID where the server is hosted.",
+ Computed: true,
+ },
+ "suspended": schema.BoolAttribute{
+ MarkdownDescription: "Whether the server is suspended.",
+ Computed: true,
+ },
+ "cpu_cores": schema.Int64Attribute{
+ MarkdownDescription: "The number of CPU cores.",
+ Computed: true,
+ },
+ "memory": schema.Int64Attribute{
+ MarkdownDescription: "The memory size in MB.",
+ Computed: true,
+ },
+ "storage": schema.Int64Attribute{
+ MarkdownDescription: "The storage size in GB.",
+ Computed: true,
+ },
+ },
+ }
+}
+
+func (d *ServerDataSource) Configure(_ context.Context, req datasource.ConfigureRequest, resp *datasource.ConfigureResponse) {
+ if req.ProviderData == nil {
+ return
+ }
+
+ c, ok := req.ProviderData.(*client.Client)
+ if !ok {
+ resp.Diagnostics.AddError(
+ "Unexpected Data Source Configure Type",
+ fmt.Sprintf("Expected *client.Client, got: %T. Please report this issue to the provider developers.", req.ProviderData),
+ )
+ return
+ }
+
+ d.client = c
+}
+
+func (d *ServerDataSource) Read(ctx context.Context, req datasource.ReadRequest, resp *datasource.ReadResponse) {
+ var data ServerDataSourceModel
+ resp.Diagnostics.Append(req.Config.Get(ctx, &data)...)
+ if resp.Diagnostics.HasError() {
+ return
+ }
+
+ result, err := d.client.Get(ctx, fmt.Sprintf("/servers/%d", data.ID.ValueInt64()))
+ if err != nil {
+ resp.Diagnostics.AddError("Error Reading Server", err.Error())
+ return
+ }
+
+ var serverResp client.ServerResponse
+ if err := json.Unmarshal(result, &serverResp); err != nil {
+ resp.Diagnostics.AddError("Error Parsing Server Response", err.Error())
+ return
+ }
+
+ s := serverResp.Data
+ data.ID = types.Int64Value(s.ID)
+ data.UUID = types.StringValue(s.UUID)
+ data.Name = types.StringValue(s.Name)
+ data.Hostname = types.StringValue(s.Hostname)
+ data.OwnerID = types.Int64Value(s.OwnerID)
+ data.HypervisorID = types.Int64Value(s.HypervisorID)
+ data.Suspended = types.BoolValue(s.Suspended)
+
+ // Extract nested resource values from the detailed server response.
+ var cpuCores, memory, storage int64
+ if s.CPU != nil {
+ cpuCores = s.CPU.Cores
+ }
+ if s.Settings != nil && s.Settings.Resources != nil {
+ memory = s.Settings.Resources.Memory
+ storage = s.Settings.Resources.Storage
+ }
+ data.CPUCores = types.Int64Value(cpuCores)
+ data.Memory = types.Int64Value(memory)
+ data.Storage = types.Int64Value(storage)
+
+ resp.Diagnostics.Append(resp.State.Set(ctx, &data)...)
+}
diff --git a/internal/provider/data_source_server_backups.go b/internal/provider/data_source_server_backups.go
new file mode 100644
index 0000000..811ac29
--- /dev/null
+++ b/internal/provider/data_source_server_backups.go
@@ -0,0 +1,144 @@
+// Copyright (c) EZSCALE.
+// SPDX-License-Identifier: MPL-2.0
+
+package provider
+
+import (
+ "context"
+ "encoding/json"
+ "fmt"
+
+ "terraform-provider-virtfusion/internal/client"
+
+ "github.com/hashicorp/terraform-plugin-framework/attr"
+ "github.com/hashicorp/terraform-plugin-framework/datasource"
+ "github.com/hashicorp/terraform-plugin-framework/datasource/schema"
+ "github.com/hashicorp/terraform-plugin-framework/types"
+)
+
+var _ datasource.DataSource = &ServerBackupsDataSource{}
+var _ datasource.DataSourceWithConfigure = &ServerBackupsDataSource{}
+
+// NewServerBackupsDataSource returns a new data source for listing server backups.
+func NewServerBackupsDataSource() datasource.DataSource {
+ return &ServerBackupsDataSource{}
+}
+
+// ServerBackupsDataSource defines the data source implementation.
+type ServerBackupsDataSource struct {
+ client *client.Client
+}
+
+// ServerBackupsDataSourceModel describes the data source data model.
+type ServerBackupsDataSourceModel struct {
+ ServerID types.Int64 `tfsdk:"server_id"`
+ Results types.Int64 `tfsdk:"results"`
+ Backups types.List `tfsdk:"backups"`
+}
+
+func (d *ServerBackupsDataSource) Metadata(_ context.Context, req datasource.MetadataRequest, resp *datasource.MetadataResponse) {
+ resp.TypeName = req.ProviderTypeName + "_server_backups"
+}
+
+func (d *ServerBackupsDataSource) Schema(_ context.Context, _ datasource.SchemaRequest, resp *datasource.SchemaResponse) {
+ resp.Schema = schema.Schema{
+ MarkdownDescription: "Use this data source to list backups for a VirtFusion server.",
+ Attributes: map[string]schema.Attribute{
+ "server_id": schema.Int64Attribute{
+ MarkdownDescription: "The server ID to list backups for.",
+ Required: true,
+ },
+ "results": resultsSchemaAttribute(),
+ "backups": schema.ListNestedAttribute{
+ MarkdownDescription: "The list of backups.",
+ Computed: true,
+ NestedObject: schema.NestedAttributeObject{
+ Attributes: map[string]schema.Attribute{
+ "id": schema.Int64Attribute{
+ MarkdownDescription: "The backup ID.",
+ Computed: true,
+ },
+ "type": schema.StringAttribute{
+ MarkdownDescription: "The backup type.",
+ Computed: true,
+ },
+ "created_at": schema.StringAttribute{
+ MarkdownDescription: "The backup creation timestamp.",
+ Computed: true,
+ },
+ },
+ },
+ },
+ },
+ }
+}
+
+func (d *ServerBackupsDataSource) Configure(_ context.Context, req datasource.ConfigureRequest, resp *datasource.ConfigureResponse) {
+ if req.ProviderData == nil {
+ return
+ }
+
+ c, ok := req.ProviderData.(*client.Client)
+ if !ok {
+ resp.Diagnostics.AddError(
+ "Unexpected Data Source Configure Type",
+ fmt.Sprintf("Expected *client.Client, got: %T. Please report this issue to the provider developers.", req.ProviderData),
+ )
+ return
+ }
+
+ d.client = c
+}
+
+func (d *ServerBackupsDataSource) Read(ctx context.Context, req datasource.ReadRequest, resp *datasource.ReadResponse) {
+ var data ServerBackupsDataSourceModel
+ resp.Diagnostics.Append(req.Config.Get(ctx, &data)...)
+ if resp.Diagnostics.HasError() {
+ return
+ }
+
+ result, err := d.client.GetAllPages(ctx, fmt.Sprintf("/backups/server/%d?%s", data.ServerID.ValueInt64(), resultsQueryParam(data.Results)))
+ if err != nil {
+ resp.Diagnostics.AddError("Error Reading Server Backups", err.Error())
+ return
+ }
+
+ var backupsResp client.BackupResponse
+ if err := json.Unmarshal(result, &backupsResp); err != nil {
+ resp.Diagnostics.AddError("Error Parsing Server Backups Response", err.Error())
+ return
+ }
+
+ backupAttrTypes := map[string]attr.Type{
+ "id": types.Int64Type,
+ "type": types.StringType,
+ "created_at": types.StringType,
+ }
+
+ backupObjects := make([]attr.Value, len(backupsResp.Data))
+ for i, b := range backupsResp.Data {
+ obj, diags := types.ObjectValue(
+ backupAttrTypes,
+ map[string]attr.Value{
+ "id": types.Int64Value(b.ID),
+ "type": types.StringValue(b.Type),
+ "created_at": types.StringValue(b.CreatedAt),
+ },
+ )
+ resp.Diagnostics.Append(diags...)
+ if resp.Diagnostics.HasError() {
+ return
+ }
+ backupObjects[i] = obj
+ }
+
+ backupsList, diags := types.ListValue(types.ObjectType{AttrTypes: backupAttrTypes}, backupObjects)
+ resp.Diagnostics.Append(diags...)
+ if resp.Diagnostics.HasError() {
+ return
+ }
+
+ data.Backups = backupsList
+
+ resp.Diagnostics.Append(resp.State.Set(ctx, &data)...)
+}
diff --git a/internal/provider/data_source_server_firewall.go b/internal/provider/data_source_server_firewall.go
new file mode 100644
index 0000000..d024bd1
--- /dev/null
+++ b/internal/provider/data_source_server_firewall.go
@@ -0,0 +1,166 @@
+// Copyright (c) EZSCALE.
+// SPDX-License-Identifier: MPL-2.0
+
+package provider
+
+import (
+ "context"
+ "encoding/json"
+ "fmt"
+
+ "terraform-provider-virtfusion/internal/client"
+
+ "github.com/hashicorp/terraform-plugin-framework/attr"
+ "github.com/hashicorp/terraform-plugin-framework/datasource"
+ "github.com/hashicorp/terraform-plugin-framework/datasource/schema"
+ "github.com/hashicorp/terraform-plugin-framework/types"
+)
+
+var _ datasource.DataSource = &ServerFirewallDataSource{}
+var _ datasource.DataSourceWithConfigure = &ServerFirewallDataSource{}
+
+// NewServerFirewallDataSource returns a new data source for reading server firewall information.
+func NewServerFirewallDataSource() datasource.DataSource {
+ return &ServerFirewallDataSource{}
+}
+
+// ServerFirewallDataSource defines the data source implementation.
+type ServerFirewallDataSource struct {
+ client *client.Client
+}
+
+// ServerFirewallDataSourceModel describes the data source data model.
+type ServerFirewallDataSourceModel struct {
+ ServerID types.Int64 `tfsdk:"server_id"`
+ InterfaceName types.String `tfsdk:"interface_name"`
+ Enabled types.Bool `tfsdk:"enabled"`
+ Rules types.List `tfsdk:"rules"`
+}
+
+func (d *ServerFirewallDataSource) Metadata(_ context.Context, req datasource.MetadataRequest, resp *datasource.MetadataResponse) {
+ resp.TypeName = req.ProviderTypeName + "_server_firewall"
+}
+
+func (d *ServerFirewallDataSource) Schema(_ context.Context, _ datasource.SchemaRequest, resp *datasource.SchemaResponse) {
+ resp.Schema = schema.Schema{
+ MarkdownDescription: "Use this data source to read firewall information for a VirtFusion server.",
+ Attributes: map[string]schema.Attribute{
+ "server_id": schema.Int64Attribute{
+ MarkdownDescription: "The server ID to read firewall information for.",
+ Required: true,
+ },
+ "interface_name": schema.StringAttribute{
+ MarkdownDescription: "The network interface name. Defaults to `eth0`.",
+ Optional: true,
+ Computed: true,
+ },
+ "enabled": schema.BoolAttribute{
+ MarkdownDescription: "Whether the firewall is enabled.",
+ Computed: true,
+ },
+ "rules": schema.ListNestedAttribute{
+ MarkdownDescription: "The firewall rules.",
+ Computed: true,
+ NestedObject: schema.NestedAttributeObject{
+ Attributes: map[string]schema.Attribute{
+ "action": schema.StringAttribute{
+ MarkdownDescription: "The action for the rule (e.g. `accept`, `drop`).",
+ Computed: true,
+ },
+ "direction": schema.StringAttribute{
+ MarkdownDescription: "The direction for the rule (e.g. `in`, `out`).",
+ Computed: true,
+ },
+ "protocol": schema.StringAttribute{
+ MarkdownDescription: "The protocol for the rule (e.g. `tcp`, `udp`).",
+ Computed: true,
+ },
+ "port": schema.StringAttribute{
+ MarkdownDescription: "The port or port range for the rule.",
+ Computed: true,
+ },
+ "ip": schema.StringAttribute{
+ MarkdownDescription: "The IP address or CIDR for the rule.",
+ Computed: true,
+ },
+ },
+ },
+ },
+ },
+ }
+}
+
+func (d *ServerFirewallDataSource) Configure(_ context.Context, req datasource.ConfigureRequest, resp *datasource.ConfigureResponse) {
+ if req.ProviderData == nil {
+ return
+ }
+
+ c, ok := req.ProviderData.(*client.Client)
+ if !ok {
+ resp.Diagnostics.AddError(
+ "Unexpected Data Source Configure Type",
+ fmt.Sprintf("Expected *client.Client, got: %T. Please report this issue to the provider developers.", req.ProviderData),
+ )
+ return
+ }
+
+ d.client = c
+}
+
+func (d *ServerFirewallDataSource) Read(ctx context.Context, req datasource.ReadRequest, resp *datasource.ReadResponse) {
+ var data ServerFirewallDataSourceModel
+ resp.Diagnostics.Append(req.Config.Get(ctx, &data)...)
+ if resp.Diagnostics.HasError() {
+ return
+ }
+
+ // Default interface_name to "eth0" if not set.
+ interfaceName := "eth0"
+ if !data.InterfaceName.IsNull() && !data.InterfaceName.IsUnknown() && data.InterfaceName.ValueString() != "" {
+ interfaceName = data.InterfaceName.ValueString()
+ }
+ data.InterfaceName = types.StringValue(interfaceName)
+
+ result, err := d.client.Get(ctx, fmt.Sprintf("/servers/%d/firewall/%s", data.ServerID.ValueInt64(), interfaceName))
+ if err != nil {
+ resp.Diagnostics.AddError("Error Reading Server Firewall", err.Error())
+ return
+ }
+
+ var fwResp client.FirewallResponse
+ if err := json.Unmarshal(result, &fwResp); err != nil {
+ resp.Diagnostics.AddError("Error Parsing Server Firewall Response", err.Error())
+ return
+ }
+
+ data.Enabled = types.BoolValue(fwResp.Data.Enabled)
+
+ ruleObjects := make([]attr.Value, len(fwResp.Data.Rules))
+ for i, rule := range fwResp.Data.Rules {
+ obj, diags := types.ObjectValue(
+ firewallRuleAttrTypes(),
+ map[string]attr.Value{
+ "action": types.StringValue(rule.Action),
+ "direction": types.StringValue(rule.Direction),
+ "protocol": types.StringValue(rule.Protocol),
+ "port": types.StringValue(rule.Port),
+ "ip": types.StringValue(rule.IP),
+ },
+ )
+ resp.Diagnostics.Append(diags...)
+ if resp.Diagnostics.HasError() {
+ return
+ }
+ ruleObjects[i] = obj
+ }
+
+ rulesList, diags := types.ListValue(types.ObjectType{AttrTypes: firewallRuleAttrTypes()}, ruleObjects)
+ resp.Diagnostics.Append(diags...)
+ if resp.Diagnostics.HasError() {
+ return
+ }
+
+ data.Rules = rulesList
+
+ resp.Diagnostics.Append(resp.State.Set(ctx, &data)...)
+}
diff --git a/internal/provider/data_source_server_templates.go b/internal/provider/data_source_server_templates.go
new file mode 100644
index 0000000..94b0f43
--- /dev/null
+++ b/internal/provider/data_source_server_templates.go
@@ -0,0 +1,138 @@
+// Copyright (c) EZSCALE.
+// SPDX-License-Identifier: MPL-2.0
+
+package provider
+
+import (
+ "context"
+ "encoding/json"
+ "fmt"
+
+ "terraform-provider-virtfusion/internal/client"
+
+ "github.com/hashicorp/terraform-plugin-framework/attr"
+ "github.com/hashicorp/terraform-plugin-framework/datasource"
+ "github.com/hashicorp/terraform-plugin-framework/datasource/schema"
+ "github.com/hashicorp/terraform-plugin-framework/types"
+)
+
+var _ datasource.DataSource = &ServerTemplatesDataSource{}
+var _ datasource.DataSourceWithConfigure = &ServerTemplatesDataSource{}
+
+// NewServerTemplatesDataSource returns a new data source for listing server templates.
+func NewServerTemplatesDataSource() datasource.DataSource {
+ return &ServerTemplatesDataSource{}
+}
+
+// ServerTemplatesDataSource defines the data source implementation.
+type ServerTemplatesDataSource struct {
+ client *client.Client
+}
+
+// ServerTemplatesDataSourceModel describes the data source data model.
+type ServerTemplatesDataSourceModel struct {
+ ServerID types.Int64 `tfsdk:"server_id"`
+ Results types.Int64 `tfsdk:"results"`
+ Templates types.List `tfsdk:"templates"`
+}
+
+func (d *ServerTemplatesDataSource) Metadata(_ context.Context, req datasource.MetadataRequest, resp *datasource.MetadataResponse) {
+ resp.TypeName = req.ProviderTypeName + "_server_templates"
+}
+
+func (d *ServerTemplatesDataSource) Schema(_ context.Context, _ datasource.SchemaRequest, resp *datasource.SchemaResponse) {
+ resp.Schema = schema.Schema{
+ MarkdownDescription: "Use this data source to list available templates for a VirtFusion server.",
+ Attributes: map[string]schema.Attribute{
+ "server_id": schema.Int64Attribute{
+ MarkdownDescription: "The server ID to list templates for.",
+ Required: true,
+ },
+ "results": resultsSchemaAttribute(),
+ "templates": schema.ListNestedAttribute{
+ MarkdownDescription: "The list of available templates.",
+ Computed: true,
+ NestedObject: schema.NestedAttributeObject{
+ Attributes: map[string]schema.Attribute{
+ "id": schema.Int64Attribute{
+ MarkdownDescription: "The template ID.",
+ Computed: true,
+ },
+ "name": schema.StringAttribute{
+ MarkdownDescription: "The template name.",
+ Computed: true,
+ },
+ },
+ },
+ },
+ },
+ }
+}
+
+func (d *ServerTemplatesDataSource) Configure(_ context.Context, req datasource.ConfigureRequest, resp *datasource.ConfigureResponse) {
+ if req.ProviderData == nil {
+ return
+ }
+
+ c, ok := req.ProviderData.(*client.Client)
+ if !ok {
+ resp.Diagnostics.AddError(
+ "Unexpected Data Source Configure Type",
+ fmt.Sprintf("Expected *client.Client, got: %T. Please report this issue to the provider developers.", req.ProviderData),
+ )
+ return
+ }
+
+ d.client = c
+}
+
+func (d *ServerTemplatesDataSource) Read(ctx context.Context, req datasource.ReadRequest, resp *datasource.ReadResponse) {
+ var data ServerTemplatesDataSourceModel
+ resp.Diagnostics.Append(req.Config.Get(ctx, &data)...)
+ if resp.Diagnostics.HasError() {
+ return
+ }
+
+ result, err := d.client.GetAllPages(ctx, fmt.Sprintf("/servers/%d/templates?%s", data.ServerID.ValueInt64(), resultsQueryParam(data.Results)))
+ if err != nil {
+ resp.Diagnostics.AddError("Error Reading Server Templates", err.Error())
+ return
+ }
+
+ var templateResp client.TemplateResponse
+ if err := json.Unmarshal(result, &templateResp); err != nil {
+ resp.Diagnostics.AddError("Error Parsing Server Templates Response", err.Error())
+ return
+ }
+
+ templateAttrTypes := map[string]attr.Type{
+ "id": types.Int64Type,
+ "name": types.StringType,
+ }
+
+ templateObjects := make([]attr.Value, len(templateResp.Data))
+ for i, t := range templateResp.Data {
+ obj, diags := types.ObjectValue(
+ templateAttrTypes,
+ map[string]attr.Value{
+ "id": types.Int64Value(t.ID),
+ "name": types.StringValue(t.Name),
+ },
+ )
+ resp.Diagnostics.Append(diags...)
+ if resp.Diagnostics.HasError() {
+ return
+ }
+ templateObjects[i] = obj
+ }
+
+ templatesList, diags := types.ListValue(types.ObjectType{AttrTypes: templateAttrTypes}, templateObjects)
+ resp.Diagnostics.Append(diags...)
+ if resp.Diagnostics.HasError() {
+ return
+ }
+
+ data.Templates = templatesList
+
+ resp.Diagnostics.Append(resp.State.Set(ctx, &data)...)
+}
diff --git a/internal/provider/data_source_server_traffic.go b/internal/provider/data_source_server_traffic.go
new file mode 100644
index 0000000..055496f
--- /dev/null
+++ b/internal/provider/data_source_server_traffic.go
@@ -0,0 +1,102 @@
+// Copyright (c) EZSCALE.
+// SPDX-License-Identifier: MPL-2.0
+
+package provider
+
+import (
+ "context"
+ "encoding/json"
+ "fmt"
+
+ "terraform-provider-virtfusion/internal/client"
+
+ "github.com/hashicorp/terraform-plugin-framework/datasource"
+ "github.com/hashicorp/terraform-plugin-framework/datasource/schema"
+ "github.com/hashicorp/terraform-plugin-framework/types"
+)
+
+var _ datasource.DataSource = &ServerTrafficDataSource{}
+var _ datasource.DataSourceWithConfigure = &ServerTrafficDataSource{}
+
+// NewServerTrafficDataSource returns a new data source for reading server traffic.
+func NewServerTrafficDataSource() datasource.DataSource {
+ return &ServerTrafficDataSource{}
+}
+
+// ServerTrafficDataSource defines the data source implementation.
+type ServerTrafficDataSource struct {
+ client *client.Client
+}
+
+// ServerTrafficDataSourceModel describes the data source data model.
+type ServerTrafficDataSourceModel struct {
+ ServerID types.Int64 `tfsdk:"server_id"`
+ Used types.Int64 `tfsdk:"used"`
+ Limit types.Int64 `tfsdk:"limit"`
+}
+
+func (d *ServerTrafficDataSource) Metadata(_ context.Context, req datasource.MetadataRequest, resp *datasource.MetadataResponse) {
+ resp.TypeName = req.ProviderTypeName + "_server_traffic"
+}
+
+func (d *ServerTrafficDataSource) Schema(_ context.Context, _ datasource.SchemaRequest, resp *datasource.SchemaResponse) {
+ resp.Schema = schema.Schema{
+ MarkdownDescription: "Use this data source to read traffic usage for a VirtFusion server.",
+ Attributes: map[string]schema.Attribute{
+ "server_id": schema.Int64Attribute{
+ MarkdownDescription: "The server ID to read traffic for.",
+ Required: true,
+ },
+ "used": schema.Int64Attribute{
+ MarkdownDescription: "The amount of traffic used.",
+ Computed: true,
+ },
+ "limit": schema.Int64Attribute{
+ MarkdownDescription: "The traffic limit.",
+ Computed: true,
+ },
+ },
+ }
+}
+
+func (d *ServerTrafficDataSource) Configure(_ context.Context, req datasource.ConfigureRequest, resp *datasource.ConfigureResponse) {
+ if req.ProviderData == nil {
+ return
+ }
+
+ c, ok := req.ProviderData.(*client.Client)
+ if !ok {
+ resp.Diagnostics.AddError(
+ "Unexpected Data Source Configure Type",
+ fmt.Sprintf("Expected *client.Client, got: %T. Please report this issue to the provider developers.", req.ProviderData),
+ )
+ return
+ }
+
+ d.client = c
+}
+
+func (d *ServerTrafficDataSource) Read(ctx context.Context, req datasource.ReadRequest, resp *datasource.ReadResponse) {
+ var data ServerTrafficDataSourceModel
+ resp.Diagnostics.Append(req.Config.Get(ctx, &data)...)
+ if resp.Diagnostics.HasError() {
+ return
+ }
+
+ result, err := d.client.Get(ctx, fmt.Sprintf("/servers/%d/traffic", data.ServerID.ValueInt64()))
+ if err != nil {
+ resp.Diagnostics.AddError("Error Reading Server Traffic", err.Error())
+ return
+ }
+
+ var trafficResp client.TrafficResponse
+ if err := json.Unmarshal(result, &trafficResp); err != nil {
+ resp.Diagnostics.AddError("Error Parsing Server Traffic Response", err.Error())
+ return
+ }
+
+ data.Used = types.Int64Value(trafficResp.Data.Used)
+ data.Limit = types.Int64Value(trafficResp.Data.Limit)
+
+ resp.Diagnostics.Append(resp.State.Set(ctx, &data)...)
+}
diff --git a/internal/provider/data_source_server_traffic_blocks.go b/internal/provider/data_source_server_traffic_blocks.go
new file mode 100644
index 0000000..81baf39
--- /dev/null
+++ b/internal/provider/data_source_server_traffic_blocks.go
@@ -0,0 +1,138 @@
+// Copyright (c) EZSCALE.
+// SPDX-License-Identifier: MPL-2.0
+
+package provider
+
+import (
+ "context"
+ "encoding/json"
+ "fmt"
+
+ "terraform-provider-virtfusion/internal/client"
+
+ "github.com/hashicorp/terraform-plugin-framework/attr"
+ "github.com/hashicorp/terraform-plugin-framework/datasource"
+ "github.com/hashicorp/terraform-plugin-framework/datasource/schema"
+ "github.com/hashicorp/terraform-plugin-framework/types"
+)
+
+var _ datasource.DataSource = &ServerTrafficBlocksDataSource{}
+var _ datasource.DataSourceWithConfigure = &ServerTrafficBlocksDataSource{}
+
+// NewServerTrafficBlocksDataSource returns a new data source for listing server traffic blocks.
+func NewServerTrafficBlocksDataSource() datasource.DataSource {
+ return &ServerTrafficBlocksDataSource{}
+}
+
+// ServerTrafficBlocksDataSource defines the data source implementation.
+type ServerTrafficBlocksDataSource struct {
+ client *client.Client
+}
+
+// ServerTrafficBlocksDataSourceModel describes the data source data model.
+type ServerTrafficBlocksDataSourceModel struct {
+ ServerID types.Int64 `tfsdk:"server_id"`
+ Results types.Int64 `tfsdk:"results"`
+ Blocks types.List `tfsdk:"blocks"`
+}
+
+func (d *ServerTrafficBlocksDataSource) Metadata(_ context.Context, req datasource.MetadataRequest, resp *datasource.MetadataResponse) {
+ resp.TypeName = req.ProviderTypeName + "_server_traffic_blocks"
+}
+
+func (d *ServerTrafficBlocksDataSource) Schema(_ context.Context, _ datasource.SchemaRequest, resp *datasource.SchemaResponse) {
+ resp.Schema = schema.Schema{
+ MarkdownDescription: "Use this data source to list traffic blocks for a VirtFusion server.",
+ Attributes: map[string]schema.Attribute{
+ "server_id": schema.Int64Attribute{
+ MarkdownDescription: "The server ID to list traffic blocks for.",
+ Required: true,
+ },
+ "results": resultsSchemaAttribute(),
+ "blocks": schema.ListNestedAttribute{
+ MarkdownDescription: "The list of traffic blocks.",
+ Computed: true,
+ NestedObject: schema.NestedAttributeObject{
+ Attributes: map[string]schema.Attribute{
+ "id": schema.Int64Attribute{
+ MarkdownDescription: "The traffic block ID.",
+ Computed: true,
+ },
+ "type": schema.StringAttribute{
+ MarkdownDescription: "The traffic block type.",
+ Computed: true,
+ },
+ },
+ },
+ },
+ },
+ }
+}
+
+func (d *ServerTrafficBlocksDataSource) Configure(_ context.Context, req datasource.ConfigureRequest, resp *datasource.ConfigureResponse) {
+ if req.ProviderData == nil {
+ return
+ }
+
+ c, ok := req.ProviderData.(*client.Client)
+ if !ok {
+ resp.Diagnostics.AddError(
+ "Unexpected Data Source Configure Type",
+ fmt.Sprintf("Expected *client.Client, got: %T. Please report this issue to the provider developers.", req.ProviderData),
+ )
+ return
+ }
+
+ d.client = c
+}
+
+func (d *ServerTrafficBlocksDataSource) Read(ctx context.Context, req datasource.ReadRequest, resp *datasource.ReadResponse) {
+ var data ServerTrafficBlocksDataSourceModel
+ resp.Diagnostics.Append(req.Config.Get(ctx, &data)...)
+ if resp.Diagnostics.HasError() {
+ return
+ }
+
+ result, err := d.client.GetAllPages(ctx, fmt.Sprintf("/servers/%d/traffic/blocks?%s", data.ServerID.ValueInt64(), resultsQueryParam(data.Results)))
+ if err != nil {
+ resp.Diagnostics.AddError("Error Reading Server Traffic Blocks", err.Error())
+ return
+ }
+
+ var blocksResp client.TrafficBlockListResponse
+ if err := json.Unmarshal(result, &blocksResp); err != nil {
+ resp.Diagnostics.AddError("Error Parsing Server Traffic Blocks Response", err.Error())
+ return
+ }
+
+ blockAttrTypes := map[string]attr.Type{
+ "id": types.Int64Type,
+ "type": types.StringType,
+ }
+
+ blockObjects := make([]attr.Value, len(blocksResp.Data))
+ for i, b := range blocksResp.Data {
+ obj, diags := types.ObjectValue(
+ blockAttrTypes,
+ map[string]attr.Value{
+ "id": types.Int64Value(b.ID),
+ "type": types.StringValue(b.Type),
+ },
+ )
+ resp.Diagnostics.Append(diags...)
+ if resp.Diagnostics.HasError() {
+ return
+ }
+ blockObjects[i] = obj
+ }
+
+ blocksList, diags := types.ListValue(types.ObjectType{AttrTypes: blockAttrTypes}, blockObjects)
+ resp.Diagnostics.Append(diags...)
+ if resp.Diagnostics.HasError() {
+ return
+ }
+
+ data.Blocks = blocksList
+
+ resp.Diagnostics.Append(resp.State.Set(ctx, &data)...)
+}
diff --git a/internal/provider/data_source_server_vnc.go b/internal/provider/data_source_server_vnc.go
new file mode 100644
index 0000000..2eff6c6
--- /dev/null
+++ b/internal/provider/data_source_server_vnc.go
@@ -0,0 +1,97 @@
+// Copyright (c) EZSCALE.
+// SPDX-License-Identifier: MPL-2.0
+
+package provider
+
+import (
+ "context"
+ "encoding/json"
+ "fmt"
+
+ "terraform-provider-virtfusion/internal/client"
+
+ "github.com/hashicorp/terraform-plugin-framework/datasource"
+ "github.com/hashicorp/terraform-plugin-framework/datasource/schema"
+ "github.com/hashicorp/terraform-plugin-framework/types"
+)
+
+var _ datasource.DataSource = &ServerVNCDataSource{}
+var _ datasource.DataSourceWithConfigure = &ServerVNCDataSource{}
+
+// NewServerVNCDataSource returns a new data source for reading server VNC information.
+func NewServerVNCDataSource() datasource.DataSource {
+ return &ServerVNCDataSource{}
+}
+
+// ServerVNCDataSource defines the data source implementation.
+type ServerVNCDataSource struct {
+ client *client.Client
+}
+
+// ServerVNCDataSourceModel describes the data source data model.
+type ServerVNCDataSourceModel struct {
+ ServerID types.Int64 `tfsdk:"server_id"`
+ URL types.String `tfsdk:"url"`
+}
+
+func (d *ServerVNCDataSource) Metadata(_ context.Context, req datasource.MetadataRequest, resp *datasource.MetadataResponse) {
+ resp.TypeName = req.ProviderTypeName + "_server_vnc"
+}
+
+func (d *ServerVNCDataSource) Schema(_ context.Context, _ datasource.SchemaRequest, resp *datasource.SchemaResponse) {
+ resp.Schema = schema.Schema{
+ MarkdownDescription: "Use this data source to read VNC connection information for a VirtFusion server.",
+ Attributes: map[string]schema.Attribute{
+ "server_id": schema.Int64Attribute{
+ MarkdownDescription: "The server ID to read VNC information for.",
+ Required: true,
+ },
+ "url": schema.StringAttribute{
+ MarkdownDescription: "The VNC connection URL.",
+ Computed: true,
+ Sensitive: true,
+ },
+ },
+ }
+}
+
+func (d *ServerVNCDataSource) Configure(_ context.Context, req datasource.ConfigureRequest, resp *datasource.ConfigureResponse) {
+ if req.ProviderData == nil {
+ return
+ }
+
+ c, ok := req.ProviderData.(*client.Client)
+ if !ok {
+ resp.Diagnostics.AddError(
+ "Unexpected Data Source Configure Type",
+ fmt.Sprintf("Expected *client.Client, got: %T. Please report this issue to the provider developers.", req.ProviderData),
+ )
+ return
+ }
+
+ d.client = c
+}
+
+func (d *ServerVNCDataSource) Read(ctx context.Context, req datasource.ReadRequest, resp *datasource.ReadResponse) {
+ var data ServerVNCDataSourceModel
+ resp.Diagnostics.Append(req.Config.Get(ctx, &data)...)
+ if resp.Diagnostics.HasError() {
+ return
+ }
+
+ result, err := d.client.Get(ctx, fmt.Sprintf("/servers/%d/vnc", data.ServerID.ValueInt64()))
+ if err != nil {
+ resp.Diagnostics.AddError("Error Reading Server VNC", err.Error())
+ return
+ }
+
+ var vncResp client.VNCResponse
+ if err := json.Unmarshal(result, &vncResp); err != nil {
+ resp.Diagnostics.AddError("Error Parsing Server VNC Response", err.Error())
+ return
+ }
+
+ data.URL = types.StringValue(vncResp.Data.URL)
+
+ resp.Diagnostics.Append(resp.State.Set(ctx, &data)...)
+}
diff --git a/internal/provider/data_source_servers.go b/internal/provider/data_source_servers.go
new file mode 100644
index 0000000..4fd88d0
--- /dev/null
+++ b/internal/provider/data_source_servers.go
@@ -0,0 +1,177 @@
+// Copyright (c) EZSCALE.
+// SPDX-License-Identifier: MPL-2.0
+
+package provider
+
+import (
+ "context"
+ "encoding/json"
+ "fmt"
+
+ "terraform-provider-virtfusion/internal/client"
+
+ "github.com/hashicorp/terraform-plugin-framework/attr"
+ "github.com/hashicorp/terraform-plugin-framework/datasource"
+ "github.com/hashicorp/terraform-plugin-framework/datasource/schema"
+ "github.com/hashicorp/terraform-plugin-framework/types"
+)
+
+var _ datasource.DataSource = &ServersDataSource{}
+var _ datasource.DataSourceWithConfigure = &ServersDataSource{}
+
+// NewServersDataSource returns a new data source for listing all servers.
+func NewServersDataSource() datasource.DataSource {
+ return &ServersDataSource{}
+}
+
+// ServersDataSource defines the data source implementation.
+type ServersDataSource struct {
+ client *client.Client
+}
+
+// ServersDataSourceModel describes the data source data model.
+type ServersDataSourceModel struct {
+ Results types.Int64 `tfsdk:"results"`
+ Servers types.List `tfsdk:"servers"`
+}
+
+func (d *ServersDataSource) Metadata(_ context.Context, req datasource.MetadataRequest, resp *datasource.MetadataResponse) {
+ resp.TypeName = req.ProviderTypeName + "_servers"
+}
+
+func (d *ServersDataSource) Schema(_ context.Context, _ datasource.SchemaRequest, resp *datasource.SchemaResponse) {
+ resp.Schema = schema.Schema{
+ MarkdownDescription: "Use this data source to list all VirtFusion servers.",
+ Attributes: map[string]schema.Attribute{
+ "results": resultsSchemaAttribute(),
+ "servers": schema.ListNestedAttribute{
+ MarkdownDescription: "The list of servers.",
+ Computed: true,
+ NestedObject: schema.NestedAttributeObject{
+ Attributes: serverDataSourceSchemaAttributes(),
+ },
+ },
+ },
+ }
+}
+
+func (d *ServersDataSource) Configure(_ context.Context, req datasource.ConfigureRequest, resp *datasource.ConfigureResponse) {
+ if req.ProviderData == nil {
+ return
+ }
+
+ c, ok := req.ProviderData.(*client.Client)
+ if !ok {
+ resp.Diagnostics.AddError(
+ "Unexpected Data Source Configure Type",
+ fmt.Sprintf("Expected *client.Client, got: %T. Please report this issue to the provider developers.", req.ProviderData),
+ )
+ return
+ }
+
+ d.client = c
+}
+
+func (d *ServersDataSource) Read(ctx context.Context, req datasource.ReadRequest, resp *datasource.ReadResponse) {
+ var data ServersDataSourceModel
+ resp.Diagnostics.Append(req.Config.Get(ctx, &data)...)
+ if resp.Diagnostics.HasError() {
+ return
+ }
+
+ result, err := d.client.GetAllPages(ctx, fmt.Sprintf("/servers?%s", resultsQueryParam(data.Results)))
+ if err != nil {
+ resp.Diagnostics.AddError("Error Reading Servers", err.Error())
+ return
+ }
+
+ var listResp client.ServerListResponse
+ if err := json.Unmarshal(result, &listResp); err != nil {
+ resp.Diagnostics.AddError("Error Parsing Servers Response", err.Error())
+ return
+ }
+
+ serverObjects := make([]attr.Value, len(listResp.Data))
+ for i, s := range listResp.Data {
+ obj, diags := types.ObjectValue(
+ serverDataSourceAttrTypes(),
+ serverDataToAttrValues(s),
+ )
+ resp.Diagnostics.Append(diags...)
+ if resp.Diagnostics.HasError() {
+ return
+ }
+ serverObjects[i] = obj
+ }
+
+ serversList, diags := types.ListValue(types.ObjectType{AttrTypes: serverDataSourceAttrTypes()}, serverObjects)
+ resp.Diagnostics.Append(diags...)
+ if resp.Diagnostics.HasError() {
+ return
+ }
+
+ data.Servers = serversList
+
+ resp.Diagnostics.Append(resp.State.Set(ctx, &data)...)
+}
+
+// serverDataSourceSchemaAttributes returns the schema attributes for a server object
+// used in list data sources.
+func serverDataSourceSchemaAttributes() map[string]schema.Attribute {
+ return map[string]schema.Attribute{
+ "id": schema.Int64Attribute{
+ MarkdownDescription: "The server ID.",
+ Computed: true,
+ },
+ "uuid": schema.StringAttribute{
+ MarkdownDescription: "The server UUID.",
+ Computed: true,
+ },
+ "name": schema.StringAttribute{
+ MarkdownDescription: "The server display name.",
+ Computed: true,
+ },
+ "hostname": schema.StringAttribute{
+ MarkdownDescription: "The server hostname.",
+ Computed: true,
+ },
+ "owner_id": schema.Int64Attribute{
+ MarkdownDescription: "The owner (user) ID who owns the server.",
+ Computed: true,
+ },
+ "hypervisor_id": schema.Int64Attribute{
+ MarkdownDescription: "The hypervisor ID where the server is hosted.",
+ Computed: true,
+ },
+ "suspended": schema.BoolAttribute{
+ MarkdownDescription: "Whether the server is suspended.",
+ Computed: true,
+ },
+ }
+}
+
+// serverDataSourceAttrTypes returns the attribute types for a server object.
+func serverDataSourceAttrTypes() map[string]attr.Type {
+ return map[string]attr.Type{
+ "id": types.Int64Type,
+ "uuid": types.StringType,
+ "name": types.StringType,
+ "hostname": types.StringType,
+ "owner_id": types.Int64Type,
+ "hypervisor_id": types.Int64Type,
+ "suspended": types.BoolType,
+ }
+}
+
+// serverDataToAttrValues converts a client.ServerData to a map of attribute values.
+func serverDataToAttrValues(s client.ServerData) map[string]attr.Value {
+ return map[string]attr.Value{
+ "id": types.Int64Value(s.ID),
+ "uuid": types.StringValue(s.UUID),
+ "name": types.StringValue(s.Name),
+ "hostname": types.StringValue(s.Hostname),
+ "owner_id": types.Int64Value(s.OwnerID),
+ "hypervisor_id": types.Int64Value(s.HypervisorID),
+ "suspended": types.BoolValue(s.Suspended),
+ }
+}
diff --git a/internal/provider/data_source_servers_by_user.go b/internal/provider/data_source_servers_by_user.go
new file mode 100644
index 0000000..450c8e8
--- /dev/null
+++ b/internal/provider/data_source_servers_by_user.go
@@ -0,0 +1,121 @@
+// Copyright (c) EZSCALE.
+// SPDX-License-Identifier: MPL-2.0
+
+package provider
+
+import (
+ "context"
+ "encoding/json"
+ "fmt"
+
+ "terraform-provider-virtfusion/internal/client"
+
+ "github.com/hashicorp/terraform-plugin-framework/attr"
+ "github.com/hashicorp/terraform-plugin-framework/datasource"
+ "github.com/hashicorp/terraform-plugin-framework/datasource/schema"
+ "github.com/hashicorp/terraform-plugin-framework/types"
+)
+
+var _ datasource.DataSource = &ServersByUserDataSource{}
+var _ datasource.DataSourceWithConfigure = &ServersByUserDataSource{}
+
+// NewServersByUserDataSource returns a new data source for listing servers by user.
+func NewServersByUserDataSource() datasource.DataSource {
+ return &ServersByUserDataSource{}
+}
+
+// ServersByUserDataSource defines the data source implementation.
+type ServersByUserDataSource struct {
+ client *client.Client
+}
+
+// ServersByUserDataSourceModel describes the data source data model.
+type ServersByUserDataSourceModel struct {
+ UserID types.Int64 `tfsdk:"user_id"`
+ Results types.Int64 `tfsdk:"results"`
+ Servers types.List `tfsdk:"servers"`
+}
+
+func (d *ServersByUserDataSource) Metadata(_ context.Context, req datasource.MetadataRequest, resp *datasource.MetadataResponse) {
+ resp.TypeName = req.ProviderTypeName + "_servers_by_user"
+}
+
+func (d *ServersByUserDataSource) Schema(_ context.Context, _ datasource.SchemaRequest, resp *datasource.SchemaResponse) {
+ resp.Schema = schema.Schema{
+ MarkdownDescription: "Use this data source to list all VirtFusion servers owned by a specific user.",
+ Attributes: map[string]schema.Attribute{
+ "user_id": schema.Int64Attribute{
+ MarkdownDescription: "The user ID to filter servers by.",
+ Required: true,
+ },
+ "results": resultsSchemaAttribute(),
+ "servers": schema.ListNestedAttribute{
+ MarkdownDescription: "The list of servers owned by the user.",
+ Computed: true,
+ NestedObject: schema.NestedAttributeObject{
+ Attributes: serverDataSourceSchemaAttributes(),
+ },
+ },
+ },
+ }
+}
+
+func (d *ServersByUserDataSource) Configure(_ context.Context, req datasource.ConfigureRequest, resp *datasource.ConfigureResponse) {
+ if req.ProviderData == nil {
+ return
+ }
+
+ c, ok := req.ProviderData.(*client.Client)
+ if !ok {
+ resp.Diagnostics.AddError(
+ "Unexpected Data Source Configure Type",
+ fmt.Sprintf("Expected *client.Client, got: %T. Please report this issue to the provider developers.", req.ProviderData),
+ )
+ return
+ }
+
+ d.client = c
+}
+
+func (d *ServersByUserDataSource) Read(ctx context.Context, req datasource.ReadRequest, resp *datasource.ReadResponse) {
+ var data ServersByUserDataSourceModel
+ resp.Diagnostics.Append(req.Config.Get(ctx, &data)...)
+ if resp.Diagnostics.HasError() {
+ return
+ }
+
+ result, err := d.client.GetAllPages(ctx, fmt.Sprintf("/servers/user/%d?%s", data.UserID.ValueInt64(), resultsQueryParam(data.Results)))
+ if err != nil {
+ resp.Diagnostics.AddError("Error Reading Servers By User", err.Error())
+ return
+ }
+
+ var listResp client.ServerListResponse
+ if err := json.Unmarshal(result, &listResp); err != nil {
+ resp.Diagnostics.AddError("Error Parsing Servers Response", err.Error())
+ return
+ }
+
+ serverObjects := make([]attr.Value, len(listResp.Data))
+ for i, s := range listResp.Data {
+ obj, diags := types.ObjectValue(
+ serverDataSourceAttrTypes(),
+ serverDataToAttrValues(s),
+ )
+ resp.Diagnostics.Append(diags...)
+ if resp.Diagnostics.HasError() {
+ return
+ }
+ serverObjects[i] = obj
+ }
+
+ serversList, diags := types.ListValue(types.ObjectType{AttrTypes: serverDataSourceAttrTypes()}, serverObjects)
+ resp.Diagnostics.Append(diags...)
+ if resp.Diagnostics.HasError() {
+ return
+ }
+
+ data.Servers = serversList
+
+ resp.Diagnostics.Append(resp.State.Set(ctx, &data)...)
+}
diff --git a/internal/provider/data_source_ssh_key.go b/internal/provider/data_source_ssh_key.go
new file mode 100644
index 0000000..a0d8437
--- /dev/null
+++ b/internal/provider/data_source_ssh_key.go
@@ -0,0 +1,135 @@
+// Copyright (c) EZSCALE.
+// SPDX-License-Identifier: MPL-2.0
+
+package provider
+
+import (
+ "context"
+ "encoding/json"
+ "fmt"
+
+ "terraform-provider-virtfusion/internal/client"
+
+ "github.com/hashicorp/terraform-plugin-framework/datasource"
+ "github.com/hashicorp/terraform-plugin-framework/datasource/schema"
+ "github.com/hashicorp/terraform-plugin-framework/types"
+)
+
+var (
+ _ datasource.DataSource = &SSHKeyDataSource{}
+ _ datasource.DataSourceWithConfigure = &SSHKeyDataSource{}
+)
+
+// NewSSHKeyDataSource returns a new SSH key data source.
+func NewSSHKeyDataSource() datasource.DataSource {
+ return &SSHKeyDataSource{}
+}
+
+// SSHKeyDataSource defines the data source implementation.
+type SSHKeyDataSource struct {
+ client *client.Client
+}
+
+// SSHKeyDataSourceModel describes the data source data model.
+type SSHKeyDataSourceModel struct {
+ ID types.Int64 `tfsdk:"id"`
+ Name types.String `tfsdk:"name"`
+ Type types.String `tfsdk:"type"`
+ PublicKey types.String `tfsdk:"public_key"`
+ Enabled types.Bool `tfsdk:"enabled"`
+ UserID types.Int64 `tfsdk:"user_id"`
+ CreatedAt types.String `tfsdk:"created_at"`
+ UpdatedAt types.String `tfsdk:"updated_at"`
+}
+
+func (d *SSHKeyDataSource) Metadata(_ context.Context, req datasource.MetadataRequest, resp *datasource.MetadataResponse) {
+ resp.TypeName = req.ProviderTypeName + "_ssh_key"
+}
+
+func (d *SSHKeyDataSource) Schema(_ context.Context, _ datasource.SchemaRequest, resp *datasource.SchemaResponse) {
+ resp.Schema = schema.Schema{
+ MarkdownDescription: "Fetches a single VirtFusion SSH key by ID.",
+ Attributes: map[string]schema.Attribute{
+ "id": schema.Int64Attribute{
+ MarkdownDescription: "The SSH key ID.",
+ Required: true,
+ },
+ "name": schema.StringAttribute{
+ MarkdownDescription: "The SSH key name.",
+ Computed: true,
+ },
+ "type": schema.StringAttribute{
+ MarkdownDescription: "The SSH key type.",
+ Computed: true,
+ },
+ "public_key": schema.StringAttribute{
+ MarkdownDescription: "The public key content.",
+ Computed: true,
+ },
+ "enabled": schema.BoolAttribute{
+ MarkdownDescription: "Whether the SSH key is enabled.",
+ Computed: true,
+ },
+ "user_id": schema.Int64Attribute{
+ MarkdownDescription: "The ID of the user who owns this SSH key.",
+ Computed: true,
+ },
+ "created_at": schema.StringAttribute{
+ MarkdownDescription: "The creation timestamp.",
+ Computed: true,
+ },
+ "updated_at": schema.StringAttribute{
+ MarkdownDescription: "The last update timestamp.",
+ Computed: true,
+ },
+ },
+ }
+}
+
+func (d *SSHKeyDataSource) Configure(_ context.Context, req datasource.ConfigureRequest, resp *datasource.ConfigureResponse) {
+ if req.ProviderData == nil {
+ return
+ }
+
+ c, ok := req.ProviderData.(*client.Client)
+ if !ok {
+ resp.Diagnostics.AddError(
+ "Unexpected Data Source Configure Type",
+ fmt.Sprintf("Expected *client.Client, got: %T.", req.ProviderData),
+ )
+ return
+ }
+
+ d.client = c
+}
+
+func (d *SSHKeyDataSource) Read(ctx context.Context, req datasource.ReadRequest, resp *datasource.ReadResponse) {
+ var data SSHKeyDataSourceModel
+ resp.Diagnostics.Append(req.Config.Get(ctx, &data)...)
+ if resp.Diagnostics.HasError() {
+ return
+ }
+
+ rawResp, err := d.client.Get(ctx, fmt.Sprintf("/ssh_keys/%d", data.ID.ValueInt64()))
+ if err != nil {
+ resp.Diagnostics.AddError("Error Reading SSH Key", err.Error())
+ return
+ }
+
+ var sshKeyResp client.SSHKeyResponse
+ if err := json.Unmarshal(rawResp, &sshKeyResp); err != nil {
+ resp.Diagnostics.AddError("Error Parsing SSH Key Response", err.Error())
+ return
+ }
+
+ data.ID = types.Int64Value(sshKeyResp.Data.ID)
+ data.Name = types.StringValue(sshKeyResp.Data.Name)
+ data.Type = types.StringValue(sshKeyResp.Data.Type)
+ data.PublicKey = types.StringValue(sshKeyResp.Data.PublicKey)
+ data.Enabled = types.BoolValue(sshKeyResp.Data.Enabled)
+ data.UserID = types.Int64Value(sshKeyResp.Data.UserID)
+ data.CreatedAt = types.StringValue(sshKeyResp.Data.CreatedAt)
+ data.UpdatedAt = types.StringValue(sshKeyResp.Data.UpdatedAt)
+
+ resp.Diagnostics.Append(resp.State.Set(ctx, &data)...)
+}
diff --git a/internal/provider/data_source_ssh_keys_by_user.go b/internal/provider/data_source_ssh_keys_by_user.go
new file mode 100644
index 0000000..1d50202
--- /dev/null
+++ b/internal/provider/data_source_ssh_keys_by_user.go
@@ -0,0 +1,142 @@
+// Copyright (c) EZSCALE.
+// SPDX-License-Identifier: MPL-2.0
+
+package provider
+
+import (
+ "context"
+ "encoding/json"
+ "fmt"
+
+ "terraform-provider-virtfusion/internal/client"
+
+ "github.com/hashicorp/terraform-plugin-framework/datasource"
+ "github.com/hashicorp/terraform-plugin-framework/datasource/schema"
+ "github.com/hashicorp/terraform-plugin-framework/types"
+)
+
+var (
+ _ datasource.DataSource = &SSHKeysByUserDataSource{}
+ _ datasource.DataSourceWithConfigure = &SSHKeysByUserDataSource{}
+)
+
+// NewSSHKeysByUserDataSource returns a new SSH keys by user data source.
+func NewSSHKeysByUserDataSource() datasource.DataSource {
+ return &SSHKeysByUserDataSource{}
+}
+
+// SSHKeysByUserDataSource defines the data source implementation.
+type SSHKeysByUserDataSource struct {
+ client *client.Client
+}
+
+// SSHKeysByUserDataSourceModel describes the data source data model.
+type SSHKeysByUserDataSourceModel struct {
+ UserID types.Int64 `tfsdk:"user_id"`
+ Results types.Int64 `tfsdk:"results"`
+ SSHKeys []SSHKeyByUserItemModel `tfsdk:"ssh_keys"`
+}
+
+// SSHKeyByUserItemModel describes a single SSH key in the list.
+type SSHKeyByUserItemModel struct {
+ ID types.Int64 `tfsdk:"id"`
+ Name types.String `tfsdk:"name"`
+ Type types.String `tfsdk:"type"`
+ PublicKey types.String `tfsdk:"public_key"`
+ Enabled types.Bool `tfsdk:"enabled"`
+}
+
+func (d *SSHKeysByUserDataSource) Metadata(_ context.Context, req datasource.MetadataRequest, resp *datasource.MetadataResponse) {
+ resp.TypeName = req.ProviderTypeName + "_ssh_keys_by_user"
+}
+
+func (d *SSHKeysByUserDataSource) Schema(_ context.Context, _ datasource.SchemaRequest, resp *datasource.SchemaResponse) {
+ resp.Schema = schema.Schema{
+ MarkdownDescription: "Fetches all SSH keys for a VirtFusion user.",
+ Attributes: map[string]schema.Attribute{
+ "user_id": schema.Int64Attribute{
+ MarkdownDescription: "The user ID to fetch SSH keys for.",
+ Required: true,
+ },
+ "results": resultsSchemaAttribute(),
+ "ssh_keys": schema.ListNestedAttribute{
+ MarkdownDescription: "List of SSH keys belonging to the user.",
+ Computed: true,
+ NestedObject: schema.NestedAttributeObject{
+ Attributes: map[string]schema.Attribute{
+ "id": schema.Int64Attribute{
+ MarkdownDescription: "The SSH key ID.",
+ Computed: true,
+ },
+ "name": schema.StringAttribute{
+ MarkdownDescription: "The SSH key name.",
+ Computed: true,
+ },
+ "type": schema.StringAttribute{
+ MarkdownDescription: "The SSH key type.",
+ Computed: true,
+ },
+ "public_key": schema.StringAttribute{
+ MarkdownDescription: "The public key content.",
+ Computed: true,
+ },
+ "enabled": schema.BoolAttribute{
+ MarkdownDescription: "Whether the SSH key is enabled.",
+ Computed: true,
+ },
+ },
+ },
+ },
+ },
+ }
+}
+
+func (d *SSHKeysByUserDataSource) Configure(_ context.Context, req datasource.ConfigureRequest, resp *datasource.ConfigureResponse) {
+ if req.ProviderData == nil {
+ return
+ }
+
+ c, ok := req.ProviderData.(*client.Client)
+ if !ok {
+ resp.Diagnostics.AddError(
+ "Unexpected Data Source Configure Type",
+ fmt.Sprintf("Expected *client.Client, got: %T.", req.ProviderData),
+ )
+ return
+ }
+
+ d.client = c
+}
+
+func (d *SSHKeysByUserDataSource) Read(ctx context.Context, req datasource.ReadRequest, resp *datasource.ReadResponse) {
+ var data SSHKeysByUserDataSourceModel
+ resp.Diagnostics.Append(req.Config.Get(ctx, &data)...)
+ if resp.Diagnostics.HasError() {
+ return
+ }
+
+ rawResp, err := d.client.GetAllPages(ctx, fmt.Sprintf("/ssh_keys/user/%d?%s", data.UserID.ValueInt64(), resultsQueryParam(data.Results)))
+ if err != nil {
+ resp.Diagnostics.AddError("Error Reading SSH Keys By User", err.Error())
+ return
+ }
+
+ var listResp client.SSHKeyListResponse
+ if err := json.Unmarshal(rawResp, &listResp); err != nil {
+ resp.Diagnostics.AddError("Error Parsing SSH Keys Response", err.Error())
+ return
+ }
+
+ data.SSHKeys = make([]SSHKeyByUserItemModel, len(listResp.Data))
+ for i, k := range listResp.Data {
+ data.SSHKeys[i] = SSHKeyByUserItemModel{
+ ID: types.Int64Value(k.ID),
+ Name: types.StringValue(k.Name),
+ Type: types.StringValue(k.Type),
+ PublicKey: types.StringValue(k.PublicKey),
+ Enabled: types.BoolValue(k.Enabled),
+ }
+ }
+
+ resp.Diagnostics.Append(resp.State.Set(ctx, &data)...)
+}
diff --git a/internal/provider/data_source_user.go b/internal/provider/data_source_user.go
new file mode 100644
index 0000000..0492567
--- /dev/null
+++ b/internal/provider/data_source_user.go
@@ -0,0 +1,128 @@
+// Copyright (c) EZSCALE.
+// SPDX-License-Identifier: MPL-2.0
+
+package provider
+
+import (
+ "context"
+ "encoding/json"
+ "fmt"
+
+ "terraform-provider-virtfusion/internal/client"
+
+ "github.com/hashicorp/terraform-plugin-framework/datasource"
+ "github.com/hashicorp/terraform-plugin-framework/datasource/schema"
+ "github.com/hashicorp/terraform-plugin-framework/types"
+)
+
+var (
+ _ datasource.DataSource = &UserDataSource{}
+ _ datasource.DataSourceWithConfigure = &UserDataSource{}
+)
+
+// NewUserDataSource returns a new user data source.
+func NewUserDataSource() datasource.DataSource {
+ return &UserDataSource{}
+}
+
+// UserDataSource defines the data source implementation.
+type UserDataSource struct {
+ client *client.Client
+}
+
+// UserDataSourceModel describes the data source data model.
+type UserDataSourceModel struct {
+ ExtRelationID types.String `tfsdk:"ext_relation_id"`
+ ID types.Int64 `tfsdk:"id"`
+ Name types.String `tfsdk:"name"`
+ Email types.String `tfsdk:"email"`
+ Enabled types.Bool `tfsdk:"enabled"`
+ CreatedAt types.String `tfsdk:"created_at"`
+ UpdatedAt types.String `tfsdk:"updated_at"`
+}
+
+func (d *UserDataSource) Metadata(_ context.Context, req datasource.MetadataRequest, resp *datasource.MetadataResponse) {
+ resp.TypeName = req.ProviderTypeName + "_user"
+}
+
+func (d *UserDataSource) Schema(_ context.Context, _ datasource.SchemaRequest, resp *datasource.SchemaResponse) {
+ resp.Schema = schema.Schema{
+ MarkdownDescription: "Fetches a VirtFusion user by external relation ID.",
+ Attributes: map[string]schema.Attribute{
+ "ext_relation_id": schema.StringAttribute{
+ MarkdownDescription: "The external relation ID of the user.",
+ Required: true,
+ },
+ "id": schema.Int64Attribute{
+ MarkdownDescription: "The numeric ID of the user.",
+ Computed: true,
+ },
+ "name": schema.StringAttribute{
+ MarkdownDescription: "The user name.",
+ Computed: true,
+ },
+ "email": schema.StringAttribute{
+ MarkdownDescription: "The user email address.",
+ Computed: true,
+ },
+ "enabled": schema.BoolAttribute{
+ MarkdownDescription: "Whether the user is enabled.",
+ Computed: true,
+ },
+ "created_at": schema.StringAttribute{
+ MarkdownDescription: "The creation timestamp.",
+ Computed: true,
+ },
+ "updated_at": schema.StringAttribute{
+ MarkdownDescription: "The last update timestamp.",
+ Computed: true,
+ },
+ },
+ }
+}
+
+func (d *UserDataSource) Configure(_ context.Context, req datasource.ConfigureRequest, resp *datasource.ConfigureResponse) {
+ if req.ProviderData == nil {
+ return
+ }
+
+ c, ok := req.ProviderData.(*client.Client)
+ if !ok {
+ resp.Diagnostics.AddError(
+ "Unexpected Data Source Configure Type",
+ fmt.Sprintf("Expected *client.Client, got: %T.", req.ProviderData),
+ )
+ return
+ }
+
+ d.client = c
+}
+
+func (d *UserDataSource) Read(ctx context.Context, req datasource.ReadRequest, resp *datasource.ReadResponse) {
+ var data UserDataSourceModel
+ resp.Diagnostics.Append(req.Config.Get(ctx, &data)...)
+ if resp.Diagnostics.HasError() {
+ return
+ }
+
+ rawResp, err := d.client.Get(ctx, fmt.Sprintf("/users/%s/byExtRelation", data.ExtRelationID.ValueString()))
+ if err != nil {
+ resp.Diagnostics.AddError("Error Reading User", err.Error())
+ return
+ }
+
+ var userResp client.UserResponse
+ if err := json.Unmarshal(rawResp, &userResp); err != nil {
+ resp.Diagnostics.AddError("Error Parsing User Response", err.Error())
+ return
+ }
+
+ data.ID = types.Int64Value(userResp.Data.ID)
+ data.Name = types.StringValue(userResp.Data.Name)
+ data.Email = types.StringValue(userResp.Data.Email)
+ data.Enabled = types.BoolValue(userResp.Data.Enabled)
+ data.CreatedAt = types.StringValue(userResp.Data.CreatedAt)
+ data.UpdatedAt = types.StringValue(userResp.Data.UpdatedAt)
+
+ resp.Diagnostics.Append(resp.State.Set(ctx, &data)...)
+}
diff --git a/internal/provider/provider.go b/internal/provider/provider.go
index 7ff2a2d..200a3bb 100644
--- a/internal/provider/provider.go
+++ b/internal/provider/provider.go
@@ -1,141 +1,181 @@
-// Copyright (c) HashiCorp, Inc.
+// Copyright (c) EZSCALE.
// SPDX-License-Identifier: MPL-2.0
package provider
import (
"context"
+ "fmt"
+ "os"
+
+ "terraform-provider-virtfusion/internal/client"
+
"github.com/hashicorp/terraform-plugin-framework/datasource"
+ dsschema "github.com/hashicorp/terraform-plugin-framework/datasource/schema"
"github.com/hashicorp/terraform-plugin-framework/provider"
"github.com/hashicorp/terraform-plugin-framework/provider/schema"
"github.com/hashicorp/terraform-plugin-framework/resource"
"github.com/hashicorp/terraform-plugin-framework/types"
- "net/http"
- "net/url"
- "os"
- "path"
)
-// Ensure VirtfusionProvider satisfies various provider interfaces.
+const defaultResultsPerPage int64 = 300
+
+// resultsSchemaAttribute returns the standard "results" schema attribute for list data sources.
+func resultsSchemaAttribute() dsschema.Int64Attribute {
+ return dsschema.Int64Attribute{
+ MarkdownDescription: "Maximum number of results to return. Defaults to 300.",
+ Optional: true,
+ }
+}
+
+// resultsQueryParam returns the query parameter string for the results limit.
+// If results is null/unknown, defaults to defaultResultsPerPage.
+func resultsQueryParam(results types.Int64) string {
+ n := defaultResultsPerPage
+ if !results.IsNull() && !results.IsUnknown() {
+ n = results.ValueInt64()
+ }
+ return fmt.Sprintf("results=%d", n)
+}
+
var _ provider.Provider = &VirtfusionProvider{}
// VirtfusionProvider defines the provider implementation.
type VirtfusionProvider struct {
- // version is set to the provider version on release, "dev" when the
- // provider is built and ran locally, and "test" when running acceptance
- // testing.
version string
}
-// ScaffoldingProviderModel describes the provider data model.
-type ScaffoldingProviderModel struct {
+// VirtfusionProviderModel describes the provider data model.
+type VirtfusionProviderModel struct {
Endpoint types.String `tfsdk:"endpoint"`
ApiToken types.String `tfsdk:"api_token"`
}
-func (p *VirtfusionProvider) Metadata(ctx context.Context, req provider.MetadataRequest, resp *provider.MetadataResponse) {
+func (p *VirtfusionProvider) Metadata(_ context.Context, _ provider.MetadataRequest, resp *provider.MetadataResponse) {
resp.TypeName = "virtfusion"
resp.Version = p.version
}
-func (p *VirtfusionProvider) Schema(ctx context.Context, req provider.SchemaRequest, resp *provider.SchemaResponse) {
+func (p *VirtfusionProvider) Schema(_ context.Context, _ provider.SchemaRequest, resp *provider.SchemaResponse) {
resp.Schema = schema.Schema{
+ MarkdownDescription: "The VirtFusion provider allows managing VirtFusion virtualization platform resources.",
Attributes: map[string]schema.Attribute{
"endpoint": schema.StringAttribute{
- MarkdownDescription: "The endpoint to use for API requests.",
- Required: true,
+ MarkdownDescription: "The VirtFusion API endpoint. Can be a hostname (e.g. `cp.example.com`) or full URL (e.g. `https://cp.example.com/api/v1`). Can also be set with the `VIRTFUSION_ENDPOINT` environment variable.",
+ Optional: true,
},
"api_token": schema.StringAttribute{
- MarkdownDescription: "The API token to use for API requests.",
- Required: true,
+ MarkdownDescription: "The API token for authentication. Can also be set with the `VIRTFUSION_API_TOKEN` environment variable.",
+ Optional: true,
+ Sensitive: true,
},
},
}
}
func (p *VirtfusionProvider) Configure(ctx context.Context, req provider.ConfigureRequest, resp *provider.ConfigureResponse) {
- // Check environment variables
- apiToken := os.Getenv("VIRTFUSION_API_TOKEN")
- endpoint := os.Getenv("VIRTFUSION_ENDPOINT")
-
- var data ScaffoldingProviderModel
-
+ var data VirtfusionProviderModel
resp.Diagnostics.Append(req.Config.Get(ctx, &data)...)
-
if resp.Diagnostics.HasError() {
return
}
- // Configuration values are now available.
- // if data.Endpoint.IsNull() { /* ... */ }
+ // Environment variables as fallback
+ endpoint := os.Getenv("VIRTFUSION_ENDPOINT")
+ apiToken := os.Getenv("VIRTFUSION_API_TOKEN")
- if data.Endpoint.ValueString() != "" {
+ // Config values override env vars
+ if !data.Endpoint.IsNull() && data.Endpoint.ValueString() != "" {
endpoint = data.Endpoint.ValueString()
}
-
- if data.ApiToken.ValueString() != "" {
+ if !data.ApiToken.IsNull() && data.ApiToken.ValueString() != "" {
apiToken = data.ApiToken.ValueString()
}
- if apiToken == "" {
- resp.Diagnostics.AddError(
- "Missing API Token Configuration",
- "While configuring the provider, the API token was not found in "+
- "the VIRTFUSION_API_TOKEN environment variable or provider "+
- "configuration block api_token attribute.",
- )
- // Not returning early allows the logic to collect all errors.
- }
-
if endpoint == "" {
resp.Diagnostics.AddError(
"Missing Endpoint Configuration",
- "While configuring the provider, the endpoint was not found in "+
- "the VIRTFUSION_ENDPOINT environment variable or provider "+
- "configuration block endpoint attribute.",
+ "The VirtFusion endpoint was not found in the VIRTFUSION_ENDPOINT environment variable or provider configuration block endpoint attribute.",
)
- // Not returning early allows the logic to collect all errors.
+ }
+ if apiToken == "" {
+ resp.Diagnostics.AddError(
+ "Missing API Token Configuration",
+ "The API token was not found in the VIRTFUSION_API_TOKEN environment variable or provider configuration block api_token attribute.",
+ )
+ }
+ if resp.Diagnostics.HasError() {
+ return
}
- customTransport := &CustomTransport{
- Transport: http.DefaultTransport,
- BaseURL: &url.URL{Scheme: "https", Host: endpoint, Path: "/api/v1"},
- Token: apiToken,
+ c, err := client.New(endpoint, apiToken)
+ if err != nil {
+ resp.Diagnostics.AddError("Failed to Create Client", err.Error())
+ return
}
- // Example client configuration for data sources and resources
- client := &http.Client{
- Transport: customTransport,
- }
-
- resp.DataSourceData = client
- resp.ResourceData = client
+ resp.DataSourceData = c
+ resp.ResourceData = c
}
-func (p *VirtfusionProvider) Resources(ctx context.Context) []func() resource.Resource {
+func (p *VirtfusionProvider) Resources(_ context.Context) []func() resource.Resource {
return []func() resource.Resource{
- NewVirtfusionServerResource,
- NewVirtfusionServerBuildResource,
- NewVirtfusionSSHResource,
+ NewServerResource,
+ NewServerBuildResource,
+ NewSSHKeyResource,
+ NewUserResource,
+ NewServerFirewallResource,
+ NewServerNetworkWhitelistResource,
+ NewServerIPv4Resource,
+ NewServerTrafficBlockResource,
+ NewIPBlockRangeResource,
+ NewSelfServiceCreditResource,
+ NewSelfServiceResourcePackResource,
+ NewSelfServiceHourlyGroupProfileResource,
+ NewSelfServiceResourceGroupProfileResource,
+ NewServerPowerActionResource,
+ NewServerPasswordResetResource,
+ NewUserPasswordResetResource,
+ NewUserAuthTokenResource,
+ NewUserServerAuthTokenResource,
+ NewSelfServicePackServersActionResource,
+ NewSelfServiceHourlyResourcePackResource,
}
}
-func (p *VirtfusionProvider) DataSources(ctx context.Context) []func() datasource.DataSource {
- return []func() datasource.DataSource{}
-}
-
-type CustomTransport struct {
- Transport http.RoundTripper
- BaseURL *url.URL
- Token string
-}
-
-func (c *CustomTransport) RoundTrip(req *http.Request) (*http.Response, error) {
- req.Header.Add("Authorization", "Bearer "+c.Token)
- req.URL.Scheme = c.BaseURL.Scheme
- req.URL.Host = c.BaseURL.Host
- req.URL.Path = path.Join(c.BaseURL.Path, req.URL.Path)
- return c.Transport.RoundTrip(req)
+func (p *VirtfusionProvider) DataSources(_ context.Context) []func() datasource.DataSource {
+ return []func() datasource.DataSource{
+ NewHypervisorDataSource,
+ NewHypervisorsDataSource,
+ NewHypervisorGroupDataSource,
+ NewHypervisorGroupsDataSource,
+ NewHypervisorGroupResourcesDataSource,
+ NewServerDataSource,
+ NewServersDataSource,
+ NewServersByUserDataSource,
+ NewServerTemplatesDataSource,
+ NewServerTrafficDataSource,
+ NewServerTrafficBlocksDataSource,
+ NewServerVNCDataSource,
+ NewServerBackupsDataSource,
+ NewServerFirewallDataSource,
+ NewPackageDataSource,
+ NewPackagesDataSource,
+ NewPackageTemplatesDataSource,
+ NewIPBlockDataSource,
+ NewIPBlocksDataSource,
+ NewSSHKeyDataSource,
+ NewSSHKeysByUserDataSource,
+ NewUserDataSource,
+ NewDNSServiceDataSource,
+ NewISODataSource,
+ NewQueueItemDataSource,
+ NewSelfServiceCurrenciesDataSource,
+ NewSelfServiceResourcePackDataSource,
+ NewSelfServiceHourlyStatsDataSource,
+ NewSelfServiceReportDataSource,
+ NewSelfServiceUsageDataSource,
+ }
}
func New(version string) func() provider.Provider {
diff --git a/internal/provider/resource_ip_block_range.go b/internal/provider/resource_ip_block_range.go
new file mode 100644
index 0000000..2693230
--- /dev/null
+++ b/internal/provider/resource_ip_block_range.go
@@ -0,0 +1,162 @@
+// Copyright (c) EZSCALE.
+// SPDX-License-Identifier: MPL-2.0
+
+package provider
+
+import (
+ "context"
+ "fmt"
+
+ "terraform-provider-virtfusion/internal/client"
+
+ "github.com/hashicorp/terraform-plugin-framework/resource"
+ "github.com/hashicorp/terraform-plugin-framework/resource/schema"
+ "github.com/hashicorp/terraform-plugin-framework/resource/schema/int64planmodifier"
+ "github.com/hashicorp/terraform-plugin-framework/resource/schema/planmodifier"
+ "github.com/hashicorp/terraform-plugin-framework/resource/schema/stringplanmodifier"
+ "github.com/hashicorp/terraform-plugin-framework/types"
+)
+
+// Ensure provider-defined types fully satisfy framework interfaces.
+var (
+ _ resource.Resource = &IPBlockRangeResource{}
+ _ resource.ResourceWithConfigure = &IPBlockRangeResource{}
+)
+
+// NewIPBlockRangeResource creates a new IP block range resource.
+func NewIPBlockRangeResource() resource.Resource {
+ return &IPBlockRangeResource{}
+}
+
+// IPBlockRangeResource defines the resource implementation.
+type IPBlockRangeResource struct {
+ client *client.Client
+}
+
+// IPBlockRangeResourceModel describes the resource data model.
+type IPBlockRangeResourceModel struct {
+ ID types.String `tfsdk:"id"`
+ IPBlockID types.Int64 `tfsdk:"ip_block_id"`
+ StartIP types.String `tfsdk:"start_ip"`
+ EndIP types.String `tfsdk:"end_ip"`
+ Gateway types.String `tfsdk:"gateway"`
+ Netmask types.String `tfsdk:"netmask"`
+}
+
+func (r *IPBlockRangeResource) Metadata(_ context.Context, req resource.MetadataRequest, resp *resource.MetadataResponse) {
+ resp.TypeName = req.ProviderTypeName + "_ip_block_range"
+}
+
+func (r *IPBlockRangeResource) Schema(_ context.Context, _ resource.SchemaRequest, resp *resource.SchemaResponse) {
+ resp.Schema = schema.Schema{
+ MarkdownDescription: "Adds an IPv4 address range to a VirtFusion IP block. This is a create-only resource — ranges cannot be deleted via the API.",
+ Attributes: map[string]schema.Attribute{
+ "id": schema.StringAttribute{
+ MarkdownDescription: "The identifier of the IP block range.",
+ Computed: true,
+ PlanModifiers: []planmodifier.String{
+ stringplanmodifier.UseStateForUnknown(),
+ },
+ },
+ "ip_block_id": schema.Int64Attribute{
+ MarkdownDescription: "The ID of the IP block to add the range to.",
+ Required: true,
+ PlanModifiers: []planmodifier.Int64{
+ int64planmodifier.RequiresReplace(),
+ },
+ },
+ "start_ip": schema.StringAttribute{
+ MarkdownDescription: "The starting IP address of the range.",
+ Required: true,
+ PlanModifiers: []planmodifier.String{
+ stringplanmodifier.RequiresReplace(),
+ },
+ },
+ "end_ip": schema.StringAttribute{
+ MarkdownDescription: "The ending IP address of the range.",
+ Required: true,
+ PlanModifiers: []planmodifier.String{
+ stringplanmodifier.RequiresReplace(),
+ },
+ },
+ "gateway": schema.StringAttribute{
+ MarkdownDescription: "The gateway address for the range.",
+ Required: true,
+ PlanModifiers: []planmodifier.String{
+ stringplanmodifier.RequiresReplace(),
+ },
+ },
+ "netmask": schema.StringAttribute{
+ MarkdownDescription: "The netmask for the range.",
+ Required: true,
+ PlanModifiers: []planmodifier.String{
+ stringplanmodifier.RequiresReplace(),
+ },
+ },
+ },
+ }
+}
+
+func (r *IPBlockRangeResource) Configure(_ context.Context, req resource.ConfigureRequest, resp *resource.ConfigureResponse) {
+ if req.ProviderData == nil {
+ return
+ }
+
+ c, ok := req.ProviderData.(*client.Client)
+ if !ok {
+ resp.Diagnostics.AddError(
+ "Unexpected Resource Configure Type",
+ fmt.Sprintf("Expected *client.Client, got: %T. Please report this issue to the provider developers.", req.ProviderData),
+ )
+ return
+ }
+
+ r.client = c
+}
+
+func (r *IPBlockRangeResource) Create(ctx context.Context, req resource.CreateRequest, resp *resource.CreateResponse) {
+ var data IPBlockRangeResourceModel
+ resp.Diagnostics.Append(req.Plan.Get(ctx, &data)...)
+ if resp.Diagnostics.HasError() {
+ return
+ }
+
+ rangeReq := client.IPBlockRangeRequest{
+ StartIP: data.StartIP.ValueString(),
+ EndIP: data.EndIP.ValueString(),
+ Gateway: data.Gateway.ValueString(),
+ Netmask: data.Netmask.ValueString(),
+ }
+
+ apiPath := fmt.Sprintf("/connectivity/ipblocks/%d/ipv4", data.IPBlockID.ValueInt64())
+ _, err := r.client.Post(ctx, apiPath, rangeReq)
+ if err != nil {
+ resp.Diagnostics.AddError(
+ "Error Creating IP Block Range",
+ fmt.Sprintf("Could not create IP block range on block %d: %s", data.IPBlockID.ValueInt64(), err),
+ )
+ return
+ }
+
+ // Generate a composite ID since the API does not return one.
+ data.ID = types.StringValue(fmt.Sprintf("%d/%s-%s", data.IPBlockID.ValueInt64(), data.StartIP.ValueString(), data.EndIP.ValueString()))
+
+ resp.Diagnostics.Append(resp.State.Set(ctx, &data)...)
+}
+
+func (r *IPBlockRangeResource) Read(ctx context.Context, req resource.ReadRequest, resp *resource.ReadResponse) {
+ var data IPBlockRangeResourceModel
+ resp.Diagnostics.Append(req.State.Get(ctx, &data)...)
+ if resp.Diagnostics.HasError() {
+ return
+ }
+ resp.Diagnostics.Append(resp.State.Set(ctx, &data)...)
+}
+
+func (r *IPBlockRangeResource) Update(_ context.Context, _ resource.UpdateRequest, _ *resource.UpdateResponse) {
+ // All attributes require replacement — updates are never called.
+}
+
+func (r *IPBlockRangeResource) Delete(_ context.Context, _ resource.DeleteRequest, _ *resource.DeleteResponse) {
+ // No delete API — removing from state only.
+}
diff --git a/internal/provider/resource_self_service_credit.go b/internal/provider/resource_self_service_credit.go
new file mode 100644
index 0000000..dd3b1af
--- /dev/null
+++ b/internal/provider/resource_self_service_credit.go
@@ -0,0 +1,173 @@
+// Copyright (c) EZSCALE.
+// SPDX-License-Identifier: MPL-2.0
+
+package provider
+
+import (
+ "context"
+ "encoding/json"
+ "errors"
+ "fmt"
+
+ "terraform-provider-virtfusion/internal/client"
+
+ "github.com/hashicorp/terraform-plugin-framework/resource"
+ "github.com/hashicorp/terraform-plugin-framework/resource/schema"
+ "github.com/hashicorp/terraform-plugin-framework/resource/schema/float64planmodifier"
+ "github.com/hashicorp/terraform-plugin-framework/resource/schema/int64planmodifier"
+ "github.com/hashicorp/terraform-plugin-framework/resource/schema/planmodifier"
+ "github.com/hashicorp/terraform-plugin-framework/resource/schema/stringplanmodifier"
+ "github.com/hashicorp/terraform-plugin-framework/types"
+)
+
+// Ensure provider-defined types fully satisfy framework interfaces.
+var (
+ _ resource.Resource = &SelfServiceCreditResource{}
+ _ resource.ResourceWithConfigure = &SelfServiceCreditResource{}
+)
+
+// NewSelfServiceCreditResource creates a new self-service credit resource.
+func NewSelfServiceCreditResource() resource.Resource {
+ return &SelfServiceCreditResource{}
+}
+
+// SelfServiceCreditResource defines the resource implementation.
+type SelfServiceCreditResource struct {
+ client *client.Client
+}
+
+// SelfServiceCreditResourceModel describes the resource data model.
+type SelfServiceCreditResourceModel struct {
+ ID types.Int64 `tfsdk:"id"`
+ Amount types.Float64 `tfsdk:"amount"`
+ CurrencyCode types.String `tfsdk:"currency_code"`
+ UserID types.Int64 `tfsdk:"user_id"`
+}
+
+func (r *SelfServiceCreditResource) Metadata(_ context.Context, req resource.MetadataRequest, resp *resource.MetadataResponse) {
+ resp.TypeName = req.ProviderTypeName + "_self_service_credit"
+}
+
+func (r *SelfServiceCreditResource) Schema(_ context.Context, _ resource.SchemaRequest, resp *resource.SchemaResponse) {
+ resp.Schema = schema.Schema{
+ MarkdownDescription: "Manages self-service credit in VirtFusion. Deleting this resource cancels the credit.",
+ Attributes: map[string]schema.Attribute{
+ "id": schema.Int64Attribute{
+ MarkdownDescription: "The identifier of the credit entry.",
+ Computed: true,
+ PlanModifiers: []planmodifier.Int64{
+ int64planmodifier.UseStateForUnknown(),
+ },
+ },
+ "amount": schema.Float64Attribute{
+ MarkdownDescription: "The credit amount.",
+ Required: true,
+ PlanModifiers: []planmodifier.Float64{
+ float64planmodifier.RequiresReplace(),
+ },
+ },
+ "currency_code": schema.StringAttribute{
+ MarkdownDescription: "The currency code (e.g. `USD`, `EUR`).",
+ Required: true,
+ PlanModifiers: []planmodifier.String{
+ stringplanmodifier.RequiresReplace(),
+ },
+ },
+ "user_id": schema.Int64Attribute{
+ MarkdownDescription: "The ID of the user to add credit to.",
+ Required: true,
+ PlanModifiers: []planmodifier.Int64{
+ int64planmodifier.RequiresReplace(),
+ },
+ },
+ },
+ }
+}
+
+func (r *SelfServiceCreditResource) Configure(_ context.Context, req resource.ConfigureRequest, resp *resource.ConfigureResponse) {
+ if req.ProviderData == nil {
+ return
+ }
+
+ c, ok := req.ProviderData.(*client.Client)
+ if !ok {
+ resp.Diagnostics.AddError(
+ "Unexpected Resource Configure Type",
+ fmt.Sprintf("Expected *client.Client, got: %T. Please report this issue to the provider developers.", req.ProviderData),
+ )
+ return
+ }
+
+ r.client = c
+}
+
+func (r *SelfServiceCreditResource) Create(ctx context.Context, req resource.CreateRequest, resp *resource.CreateResponse) {
+ var data SelfServiceCreditResourceModel
+ resp.Diagnostics.Append(req.Plan.Get(ctx, &data)...)
+ if resp.Diagnostics.HasError() {
+ return
+ }
+
+ creditReq := client.SelfServiceCreditRequest{
+ Amount: data.Amount.ValueFloat64(),
+ CurrencyCode: data.CurrencyCode.ValueString(),
+ UserID: data.UserID.ValueInt64(),
+ }
+
+ respBody, err := r.client.Post(ctx, "/selfService/credit", creditReq)
+ if err != nil {
+ resp.Diagnostics.AddError(
+ "Error Creating Credit",
+ fmt.Sprintf("Could not create credit for user %d: %s", data.UserID.ValueInt64(), err),
+ )
+ return
+ }
+
+ var creditResp client.SelfServiceCreditResponse
+ if err := json.Unmarshal(respBody, &creditResp); err != nil {
+ resp.Diagnostics.AddError(
+ "Error Parsing Response",
+ fmt.Sprintf("Could not parse credit response: %s", err),
+ )
+ return
+ }
+
+ data.ID = types.Int64Value(creditResp.Data.ID)
+
+ resp.Diagnostics.Append(resp.State.Set(ctx, &data)...)
+}
+
+func (r *SelfServiceCreditResource) Read(ctx context.Context, req resource.ReadRequest, resp *resource.ReadResponse) {
+ var data SelfServiceCreditResourceModel
+ resp.Diagnostics.Append(req.State.Get(ctx, &data)...)
+ if resp.Diagnostics.HasError() {
+ return
+ }
+ resp.Diagnostics.Append(resp.State.Set(ctx, &data)...)
+}
+
+func (r *SelfServiceCreditResource) Update(_ context.Context, _ resource.UpdateRequest, _ *resource.UpdateResponse) {
+ // All attributes require replacement — updates are never called.
+}
+
+func (r *SelfServiceCreditResource) Delete(ctx context.Context, req resource.DeleteRequest, resp *resource.DeleteResponse) {
+ var data SelfServiceCreditResourceModel
+ resp.Diagnostics.Append(req.State.Get(ctx, &data)...)
+ if resp.Diagnostics.HasError() {
+ return
+ }
+
+ apiPath := fmt.Sprintf("/selfService/credit/%d", data.ID.ValueInt64())
+ _, err := r.client.Delete(ctx, apiPath)
+ if err != nil {
+ var apiErr *client.APIError
+ if errors.As(err, &apiErr) && apiErr.IsNotFound() {
+ return
+ }
+ resp.Diagnostics.AddError(
+ "Error Cancelling Credit",
+ fmt.Sprintf("Could not cancel credit %d: %s", data.ID.ValueInt64(), err),
+ )
+ return
+ }
+}
diff --git a/internal/provider/resource_self_service_hourly_group_profile.go b/internal/provider/resource_self_service_hourly_group_profile.go
new file mode 100644
index 0000000..9713feb
--- /dev/null
+++ b/internal/provider/resource_self_service_hourly_group_profile.go
@@ -0,0 +1,172 @@
+// Copyright (c) EZSCALE.
+// SPDX-License-Identifier: MPL-2.0
+
+package provider
+
+import (
+ "context"
+ "errors"
+ "fmt"
+
+ "terraform-provider-virtfusion/internal/client"
+
+ "github.com/hashicorp/terraform-plugin-framework/resource"
+ "github.com/hashicorp/terraform-plugin-framework/resource/schema"
+ "github.com/hashicorp/terraform-plugin-framework/resource/schema/int64planmodifier"
+ "github.com/hashicorp/terraform-plugin-framework/resource/schema/planmodifier"
+ "github.com/hashicorp/terraform-plugin-framework/resource/schema/stringplanmodifier"
+ "github.com/hashicorp/terraform-plugin-framework/types"
+)
+
+// Ensure provider-defined types fully satisfy framework interfaces.
+var (
+ _ resource.Resource = &SelfServiceHourlyGroupProfileResource{}
+ _ resource.ResourceWithConfigure = &SelfServiceHourlyGroupProfileResource{}
+)
+
+// NewSelfServiceHourlyGroupProfileResource creates a new self-service hourly group profile resource.
+func NewSelfServiceHourlyGroupProfileResource() resource.Resource {
+ return &SelfServiceHourlyGroupProfileResource{}
+}
+
+// SelfServiceHourlyGroupProfileResource defines the resource implementation.
+type SelfServiceHourlyGroupProfileResource struct {
+ client *client.Client
+}
+
+// SelfServiceHourlyGroupProfileResourceModel describes the resource data model.
+type SelfServiceHourlyGroupProfileResourceModel struct {
+ ID types.String `tfsdk:"id"`
+ UserID types.Int64 `tfsdk:"user_id"`
+ GroupID types.Int64 `tfsdk:"group_id"`
+ ProfileID types.Int64 `tfsdk:"profile_id"`
+}
+
+func (r *SelfServiceHourlyGroupProfileResource) Metadata(_ context.Context, req resource.MetadataRequest, resp *resource.MetadataResponse) {
+ resp.TypeName = req.ProviderTypeName + "_self_service_hourly_group_profile"
+}
+
+func (r *SelfServiceHourlyGroupProfileResource) Schema(_ context.Context, _ resource.SchemaRequest, resp *resource.SchemaResponse) {
+ resp.Schema = schema.Schema{
+ MarkdownDescription: "Manages a self-service hourly group profile assignment in VirtFusion.",
+ Attributes: map[string]schema.Attribute{
+ "id": schema.StringAttribute{
+ MarkdownDescription: "The composite identifier of the hourly group profile (userId/groupId/profileId).",
+ Computed: true,
+ PlanModifiers: []planmodifier.String{
+ stringplanmodifier.UseStateForUnknown(),
+ },
+ },
+ "user_id": schema.Int64Attribute{
+ MarkdownDescription: "The ID of the user.",
+ Required: true,
+ PlanModifiers: []planmodifier.Int64{
+ int64planmodifier.RequiresReplace(),
+ },
+ },
+ "group_id": schema.Int64Attribute{
+ MarkdownDescription: "The ID of the hypervisor group.",
+ Required: true,
+ PlanModifiers: []planmodifier.Int64{
+ int64planmodifier.RequiresReplace(),
+ },
+ },
+ "profile_id": schema.Int64Attribute{
+ MarkdownDescription: "The ID of the hourly profile.",
+ Required: true,
+ PlanModifiers: []planmodifier.Int64{
+ int64planmodifier.RequiresReplace(),
+ },
+ },
+ },
+ }
+}
+
+func (r *SelfServiceHourlyGroupProfileResource) Configure(_ context.Context, req resource.ConfigureRequest, resp *resource.ConfigureResponse) {
+ if req.ProviderData == nil {
+ return
+ }
+
+ c, ok := req.ProviderData.(*client.Client)
+ if !ok {
+ resp.Diagnostics.AddError(
+ "Unexpected Resource Configure Type",
+ fmt.Sprintf("Expected *client.Client, got: %T. Please report this issue to the provider developers.", req.ProviderData),
+ )
+ return
+ }
+
+ r.client = c
+}
+
+func (r *SelfServiceHourlyGroupProfileResource) Create(ctx context.Context, req resource.CreateRequest, resp *resource.CreateResponse) {
+ var data SelfServiceHourlyGroupProfileResourceModel
+ resp.Diagnostics.Append(req.Plan.Get(ctx, &data)...)
+ if resp.Diagnostics.HasError() {
+ return
+ }
+
+ profileReq := map[string]int64{
+ "userId": data.UserID.ValueInt64(),
+ "groupId": data.GroupID.ValueInt64(),
+ "profileId": data.ProfileID.ValueInt64(),
+ }
+
+ _, err := r.client.Post(ctx, "/selfService/hourlyGroupProfile", profileReq)
+ if err != nil {
+ resp.Diagnostics.AddError(
+ "Error Creating Hourly Group Profile",
+ fmt.Sprintf("Could not create hourly group profile for user %d: %s", data.UserID.ValueInt64(), err),
+ )
+ return
+ }
+
+ // Generate a composite ID.
+ data.ID = types.StringValue(fmt.Sprintf("%d/%d/%d", data.UserID.ValueInt64(), data.GroupID.ValueInt64(), data.ProfileID.ValueInt64()))
+
+ resp.Diagnostics.Append(resp.State.Set(ctx, &data)...)
+}
+
+func (r *SelfServiceHourlyGroupProfileResource) Read(ctx context.Context, req resource.ReadRequest, resp *resource.ReadResponse) {
+ var data SelfServiceHourlyGroupProfileResourceModel
+ resp.Diagnostics.Append(req.State.Get(ctx, &data)...)
+ if resp.Diagnostics.HasError() {
+ return
+ }
+ resp.Diagnostics.Append(resp.State.Set(ctx, &data)...)
+}
+
+func (r *SelfServiceHourlyGroupProfileResource) Update(_ context.Context, _ resource.UpdateRequest, _ *resource.UpdateResponse) {
+ // All attributes require replacement — updates are never called.
+}
+
+func (r *SelfServiceHourlyGroupProfileResource) Delete(ctx context.Context, req resource.DeleteRequest, resp *resource.DeleteResponse) {
+ var data SelfServiceHourlyGroupProfileResourceModel
+ resp.Diagnostics.Append(req.State.Get(ctx, &data)...)
+ if resp.Diagnostics.HasError() {
+ return
+ }
+
+ apiPath := fmt.Sprintf("/selfService/hourlyGroupProfile/%d/%d/%d",
+ data.UserID.ValueInt64(),
+ data.GroupID.ValueInt64(),
+ data.ProfileID.ValueInt64(),
+ )
+ _, err := r.client.Delete(ctx, apiPath)
+ if err != nil {
+ var apiErr *client.APIError
+ if errors.As(err, &apiErr) && apiErr.IsNotFound() {
+ return
+ }
+ resp.Diagnostics.AddError(
+ "Error Deleting Hourly Group Profile",
+ fmt.Sprintf("Could not delete hourly group profile for user %d, group %d, profile %d: %s",
+ data.UserID.ValueInt64(),
+ data.GroupID.ValueInt64(),
+ data.ProfileID.ValueInt64(),
+ err,
+ ),
+ )
+ return
+ }
+}
diff --git a/internal/provider/resource_self_service_hourly_resource_pack.go b/internal/provider/resource_self_service_hourly_resource_pack.go
new file mode 100644
index 0000000..2fb97b7
--- /dev/null
+++ b/internal/provider/resource_self_service_hourly_resource_pack.go
@@ -0,0 +1,186 @@
+// Copyright (c) EZSCALE.
+// SPDX-License-Identifier: MPL-2.0
+
+package provider
+
+import (
+ "context"
+ "fmt"
+
+ "terraform-provider-virtfusion/internal/client"
+
+ "github.com/hashicorp/terraform-plugin-framework/path"
+ "github.com/hashicorp/terraform-plugin-framework/resource"
+ "github.com/hashicorp/terraform-plugin-framework/resource/schema"
+ "github.com/hashicorp/terraform-plugin-framework/resource/schema/int64planmodifier"
+ "github.com/hashicorp/terraform-plugin-framework/resource/schema/planmodifier"
+ "github.com/hashicorp/terraform-plugin-framework/resource/schema/stringplanmodifier"
+ "github.com/hashicorp/terraform-plugin-framework/types"
+)
+
+// Ensure provider-defined types fully satisfy framework interfaces.
+var (
+ _ resource.Resource = &SelfServiceHourlyResourcePackResource{}
+ _ resource.ResourceWithConfigure = &SelfServiceHourlyResourcePackResource{}
+)
+
+// NewSelfServiceHourlyResourcePackResource creates a new self-service hourly resource pack resource.
+func NewSelfServiceHourlyResourcePackResource() resource.Resource {
+ return &SelfServiceHourlyResourcePackResource{}
+}
+
+// SelfServiceHourlyResourcePackResource defines the resource implementation.
+type SelfServiceHourlyResourcePackResource struct {
+ client *client.Client
+}
+
+// SelfServiceHourlyResourcePackResourceModel describes the resource data model.
+type SelfServiceHourlyResourcePackResourceModel struct {
+ ID types.String `tfsdk:"id"`
+ UserID types.Int64 `tfsdk:"user_id"`
+ GroupID types.Int64 `tfsdk:"group_id"`
+ ResourcePackID types.Int64 `tfsdk:"resource_pack_id"`
+}
+
+func (r *SelfServiceHourlyResourcePackResource) Metadata(_ context.Context, req resource.MetadataRequest, resp *resource.MetadataResponse) {
+ resp.TypeName = req.ProviderTypeName + "_self_service_hourly_resource_pack"
+}
+
+func (r *SelfServiceHourlyResourcePackResource) Schema(_ context.Context, _ resource.SchemaRequest, resp *resource.SchemaResponse) {
+ resp.Schema = schema.Schema{
+ MarkdownDescription: "Sets the hourly resource pack for a user and group in VirtFusion self-service. Changing any attribute forces recreation.",
+ Attributes: map[string]schema.Attribute{
+ "id": schema.StringAttribute{
+ MarkdownDescription: "The composite identifier for this hourly resource pack assignment.",
+ Computed: true,
+ PlanModifiers: []planmodifier.String{
+ stringplanmodifier.UseStateForUnknown(),
+ },
+ },
+ "user_id": schema.Int64Attribute{
+ MarkdownDescription: "The ID of the user.",
+ Required: true,
+ PlanModifiers: []planmodifier.Int64{
+ int64planmodifier.RequiresReplace(),
+ },
+ },
+ "group_id": schema.Int64Attribute{
+ MarkdownDescription: "The ID of the group.",
+ Required: true,
+ PlanModifiers: []planmodifier.Int64{
+ int64planmodifier.RequiresReplace(),
+ },
+ },
+ "resource_pack_id": schema.Int64Attribute{
+ MarkdownDescription: "The ID of the resource pack.",
+ Required: true,
+ PlanModifiers: []planmodifier.Int64{
+ int64planmodifier.RequiresReplace(),
+ },
+ },
+ },
+ }
+}
+
+func (r *SelfServiceHourlyResourcePackResource) Configure(_ context.Context, req resource.ConfigureRequest, resp *resource.ConfigureResponse) {
+ if req.ProviderData == nil {
+ return
+ }
+
+ c, ok := req.ProviderData.(*client.Client)
+ if !ok {
+ resp.Diagnostics.AddError(
+ "Unexpected Resource Configure Type",
+ fmt.Sprintf("Expected *client.Client, got: %T. Please report this issue to the provider developers.", req.ProviderData),
+ )
+ return
+ }
+
+ r.client = c
+}
+
+func (r *SelfServiceHourlyResourcePackResource) Create(ctx context.Context, req resource.CreateRequest, resp *resource.CreateResponse) {
+ var data SelfServiceHourlyResourcePackResourceModel
+ resp.Diagnostics.Append(req.Plan.Get(ctx, &data)...)
+ if resp.Diagnostics.HasError() {
+ return
+ }
+
+ apiPath := fmt.Sprintf("/selfService/hourlyResourcePack/byUser/%d/group/%d/resourcePack/%d",
+ data.UserID.ValueInt64(), data.GroupID.ValueInt64(), data.ResourcePackID.ValueInt64())
+ _, err := r.client.Put(ctx, apiPath, nil)
+ if err != nil {
+ resp.Diagnostics.AddError(
+ "Error Setting Hourly Resource Pack",
+ fmt.Sprintf("Could not set hourly resource pack (user=%d, group=%d, resource_pack=%d): %s",
+ data.UserID.ValueInt64(), data.GroupID.ValueInt64(), data.ResourcePackID.ValueInt64(), err),
+ )
+ return
+ }
+
+ data.ID = types.StringValue(fmt.Sprintf("%d-%d-%d", data.UserID.ValueInt64(), data.GroupID.ValueInt64(), data.ResourcePackID.ValueInt64()))
+
+ resp.Diagnostics.Append(resp.State.Set(ctx, &data)...)
+}
+
+func (r *SelfServiceHourlyResourcePackResource) Read(ctx context.Context, req resource.ReadRequest, resp *resource.ReadResponse) {
+ var data SelfServiceHourlyResourcePackResourceModel
+ resp.Diagnostics.Append(req.State.Get(ctx, &data)...)
+ if resp.Diagnostics.HasError() {
+ return
+ }
+
+ // Return stored state as-is. The API does not provide a direct read endpoint for this assignment.
+ resp.Diagnostics.Append(resp.State.Set(ctx, &data)...)
+}
+
+func (r *SelfServiceHourlyResourcePackResource) Update(ctx context.Context, req resource.UpdateRequest, resp *resource.UpdateResponse) {
+ // All attributes have RequiresReplace, so Update should never be called.
+ var data SelfServiceHourlyResourcePackResourceModel
+ resp.Diagnostics.Append(req.Plan.Get(ctx, &data)...)
+ if resp.Diagnostics.HasError() {
+ return
+ }
+
+ resp.Diagnostics.Append(resp.State.Set(ctx, &data)...)
+}
+
+func (r *SelfServiceHourlyResourcePackResource) Delete(_ context.Context, _ resource.DeleteRequest, _ *resource.DeleteResponse) {
+ // No-op: removing the hourly resource pack assignment from state only.
+}
+
+// ValidateConfig validates the resource configuration.
+func (r *SelfServiceHourlyResourcePackResource) ValidateConfig(ctx context.Context, req resource.ValidateConfigRequest, resp *resource.ValidateConfigResponse) {
+ var data SelfServiceHourlyResourcePackResourceModel
+ resp.Diagnostics.Append(req.Config.Get(ctx, &data)...)
+ if resp.Diagnostics.HasError() {
+ return
+ }
+
+ // Validate user_id is positive.
+ if !data.UserID.IsNull() && !data.UserID.IsUnknown() && data.UserID.ValueInt64() <= 0 {
+ resp.Diagnostics.AddAttributeError(
+ path.Root("user_id"),
+ "Invalid User ID",
+ "user_id must be a positive integer.",
+ )
+ }
+
+ // Validate group_id is positive.
+ if !data.GroupID.IsNull() && !data.GroupID.IsUnknown() && data.GroupID.ValueInt64() <= 0 {
+ resp.Diagnostics.AddAttributeError(
+ path.Root("group_id"),
+ "Invalid Group ID",
+ "group_id must be a positive integer.",
+ )
+ }
+
+ // Validate resource_pack_id is positive.
+ if !data.ResourcePackID.IsNull() && !data.ResourcePackID.IsUnknown() && data.ResourcePackID.ValueInt64() <= 0 {
+ resp.Diagnostics.AddAttributeError(
+ path.Root("resource_pack_id"),
+ "Invalid Resource Pack ID",
+ "resource_pack_id must be a positive integer.",
+ )
+ }
+}
diff --git a/internal/provider/resource_self_service_pack_servers_action.go b/internal/provider/resource_self_service_pack_servers_action.go
new file mode 100644
index 0000000..d2b0480
--- /dev/null
+++ b/internal/provider/resource_self_service_pack_servers_action.go
@@ -0,0 +1,204 @@
+// Copyright (c) EZSCALE.
+// SPDX-License-Identifier: MPL-2.0
+
+package provider
+
+import (
+ "context"
+ "fmt"
+ "time"
+
+ "terraform-provider-virtfusion/internal/client"
+
+ "github.com/hashicorp/terraform-plugin-framework/path"
+ "github.com/hashicorp/terraform-plugin-framework/resource"
+ "github.com/hashicorp/terraform-plugin-framework/resource/schema"
+ "github.com/hashicorp/terraform-plugin-framework/resource/schema/mapplanmodifier"
+ "github.com/hashicorp/terraform-plugin-framework/resource/schema/planmodifier"
+ "github.com/hashicorp/terraform-plugin-framework/resource/schema/stringplanmodifier"
+ "github.com/hashicorp/terraform-plugin-framework/types"
+)
+
+// Ensure provider-defined types fully satisfy framework interfaces.
+var (
+ _ resource.Resource = &SelfServicePackServersActionResource{}
+ _ resource.ResourceWithConfigure = &SelfServicePackServersActionResource{}
+)
+
+// NewSelfServicePackServersActionResource creates a new self-service pack servers action resource.
+func NewSelfServicePackServersActionResource() resource.Resource {
+ return &SelfServicePackServersActionResource{}
+}
+
+// SelfServicePackServersActionResource defines the resource implementation.
+type SelfServicePackServersActionResource struct {
+ client *client.Client
+}
+
+// SelfServicePackServersActionResourceModel describes the resource data model.
+type SelfServicePackServersActionResourceModel struct {
+ ID types.String `tfsdk:"id"`
+ PackID types.Int64 `tfsdk:"pack_id"`
+ Action types.String `tfsdk:"action"`
+ Triggers types.Map `tfsdk:"triggers"`
+}
+
+func (r *SelfServicePackServersActionResource) Metadata(_ context.Context, req resource.MetadataRequest, resp *resource.MetadataResponse) {
+ resp.TypeName = req.ProviderTypeName + "_self_service_pack_servers_action"
+}
+
+func (r *SelfServicePackServersActionResource) Schema(_ context.Context, _ resource.SchemaRequest, resp *resource.SchemaResponse) {
+ resp.Schema = schema.Schema{
+ MarkdownDescription: "Performs an action on all servers in a self-service resource pack. This is a trigger-style resource — the action is executed on create and can be re-triggered by changing the `triggers` attribute.",
+ Attributes: map[string]schema.Attribute{
+ "id": schema.StringAttribute{
+ MarkdownDescription: "The identifier for this pack servers action.",
+ Computed: true,
+ PlanModifiers: []planmodifier.String{
+ stringplanmodifier.UseStateForUnknown(),
+ },
+ },
+ "pack_id": schema.Int64Attribute{
+ MarkdownDescription: "The ID of the resource pack.",
+ Required: true,
+ },
+ "action": schema.StringAttribute{
+ MarkdownDescription: "The action to perform on the pack servers. Must be one of: `suspend`, `unsuspend`, `delete`.",
+ Required: true,
+ },
+ "triggers": schema.MapAttribute{
+ MarkdownDescription: "A map of arbitrary strings that, when changed, will cause the action to be re-executed. Works like `triggers` in `terraform_data`.",
+ ElementType: types.StringType,
+ Optional: true,
+ PlanModifiers: []planmodifier.Map{
+ mapplanmodifier.RequiresReplace(),
+ },
+ },
+ },
+ }
+}
+
+func (r *SelfServicePackServersActionResource) Configure(_ context.Context, req resource.ConfigureRequest, resp *resource.ConfigureResponse) {
+ if req.ProviderData == nil {
+ return
+ }
+
+ c, ok := req.ProviderData.(*client.Client)
+ if !ok {
+ resp.Diagnostics.AddError(
+ "Unexpected Resource Configure Type",
+ fmt.Sprintf("Expected *client.Client, got: %T. Please report this issue to the provider developers.", req.ProviderData),
+ )
+ return
+ }
+
+ r.client = c
+}
+
+func (r *SelfServicePackServersActionResource) Create(ctx context.Context, req resource.CreateRequest, resp *resource.CreateResponse) {
+ var data SelfServicePackServersActionResourceModel
+ resp.Diagnostics.Append(req.Plan.Get(ctx, &data)...)
+ if resp.Diagnostics.HasError() {
+ return
+ }
+
+ action := data.Action.ValueString()
+ packID := data.PackID.ValueInt64()
+
+ switch action {
+ case "suspend":
+ apiPath := fmt.Sprintf("/selfService/resourcePack/%d/servers/suspend", packID)
+ _, err := r.client.Post(ctx, apiPath, nil)
+ if err != nil {
+ resp.Diagnostics.AddError(
+ "Error Suspending Pack Servers",
+ fmt.Sprintf("Could not suspend servers for resource pack %d: %s", packID, err),
+ )
+ return
+ }
+ case "unsuspend":
+ apiPath := fmt.Sprintf("/selfService/resourcePack/%d/servers/unsuspend", packID)
+ _, err := r.client.Post(ctx, apiPath, nil)
+ if err != nil {
+ resp.Diagnostics.AddError(
+ "Error Unsuspending Pack Servers",
+ fmt.Sprintf("Could not unsuspend servers for resource pack %d: %s", packID, err),
+ )
+ return
+ }
+ case "delete":
+ apiPath := fmt.Sprintf("/selfService/resourcePack/%d/servers", packID)
+ _, err := r.client.Delete(ctx, apiPath)
+ if err != nil {
+ resp.Diagnostics.AddError(
+ "Error Deleting Pack Servers",
+ fmt.Sprintf("Could not delete servers for resource pack %d: %s", packID, err),
+ )
+ return
+ }
+ }
+
+ data.ID = types.StringValue(fmt.Sprintf("%d-%s-%d", packID, action, time.Now().UnixNano()))
+
+ resp.Diagnostics.Append(resp.State.Set(ctx, &data)...)
+}
+
+func (r *SelfServicePackServersActionResource) Read(ctx context.Context, req resource.ReadRequest, resp *resource.ReadResponse) {
+ var data SelfServicePackServersActionResourceModel
+ resp.Diagnostics.Append(req.State.Get(ctx, &data)...)
+ if resp.Diagnostics.HasError() {
+ return
+ }
+
+ // Return stored state as-is for trigger-style resources.
+ resp.Diagnostics.Append(resp.State.Set(ctx, &data)...)
+}
+
+func (r *SelfServicePackServersActionResource) Update(ctx context.Context, req resource.UpdateRequest, resp *resource.UpdateResponse) {
+ var data SelfServicePackServersActionResourceModel
+ resp.Diagnostics.Append(req.Plan.Get(ctx, &data)...)
+ if resp.Diagnostics.HasError() {
+ return
+ }
+
+ resp.Diagnostics.Append(resp.State.Set(ctx, &data)...)
+}
+
+func (r *SelfServicePackServersActionResource) Delete(_ context.Context, _ resource.DeleteRequest, _ *resource.DeleteResponse) {
+ // No-op: pack server actions are not reversible. Removing from state only.
+}
+
+// ValidateConfig validates the resource configuration.
+func (r *SelfServicePackServersActionResource) ValidateConfig(ctx context.Context, req resource.ValidateConfigRequest, resp *resource.ValidateConfigResponse) {
+ var data SelfServicePackServersActionResourceModel
+ resp.Diagnostics.Append(req.Config.Get(ctx, &data)...)
+ if resp.Diagnostics.HasError() {
+ return
+ }
+
+ // Validate pack_id is positive.
+ if !data.PackID.IsNull() && !data.PackID.IsUnknown() && data.PackID.ValueInt64() <= 0 {
+ resp.Diagnostics.AddAttributeError(
+ path.Root("pack_id"),
+ "Invalid Pack ID",
+ "pack_id must be a positive integer.",
+ )
+ }
+
+ // Validate action is one of the allowed values.
+ if !data.Action.IsNull() && !data.Action.IsUnknown() {
+ action := data.Action.ValueString()
+ validActions := map[string]bool{
+ "suspend": true,
+ "unsuspend": true,
+ "delete": true,
+ }
+ if !validActions[action] {
+ resp.Diagnostics.AddAttributeError(
+ path.Root("action"),
+ "Invalid Pack Servers Action",
+ fmt.Sprintf("action must be one of: suspend, unsuspend, delete. Got: %q", action),
+ )
+ }
+ }
+}
diff --git a/internal/provider/resource_self_service_resource_group_profile.go b/internal/provider/resource_self_service_resource_group_profile.go
new file mode 100644
index 0000000..8f4ddc7
--- /dev/null
+++ b/internal/provider/resource_self_service_resource_group_profile.go
@@ -0,0 +1,206 @@
+// Copyright (c) EZSCALE.
+// SPDX-License-Identifier: MPL-2.0
+
+package provider
+
+import (
+ "context"
+ "fmt"
+
+ "terraform-provider-virtfusion/internal/client"
+
+ "github.com/hashicorp/terraform-plugin-framework/path"
+ "github.com/hashicorp/terraform-plugin-framework/resource"
+ "github.com/hashicorp/terraform-plugin-framework/resource/schema"
+ "github.com/hashicorp/terraform-plugin-framework/resource/schema/int64planmodifier"
+ "github.com/hashicorp/terraform-plugin-framework/resource/schema/planmodifier"
+ "github.com/hashicorp/terraform-plugin-framework/resource/schema/stringplanmodifier"
+ "github.com/hashicorp/terraform-plugin-framework/types"
+)
+
+// Ensure provider-defined types fully satisfy framework interfaces.
+var (
+ _ resource.Resource = &SelfServiceResourceGroupProfileResource{}
+ _ resource.ResourceWithConfigure = &SelfServiceResourceGroupProfileResource{}
+)
+
+// NewSelfServiceResourceGroupProfileResource creates a new self-service resource group profile resource.
+func NewSelfServiceResourceGroupProfileResource() resource.Resource {
+ return &SelfServiceResourceGroupProfileResource{}
+}
+
+// SelfServiceResourceGroupProfileResource defines the resource implementation.
+type SelfServiceResourceGroupProfileResource struct {
+ client *client.Client
+}
+
+// SelfServiceResourceGroupProfileResourceModel describes the resource data model.
+type SelfServiceResourceGroupProfileResourceModel struct {
+ ID types.String `tfsdk:"id"`
+ UserID types.Int64 `tfsdk:"user_id"`
+ GroupID types.Int64 `tfsdk:"group_id"`
+ ProfileID types.Int64 `tfsdk:"profile_id"`
+}
+
+func (r *SelfServiceResourceGroupProfileResource) Metadata(_ context.Context, req resource.MetadataRequest, resp *resource.MetadataResponse) {
+ resp.TypeName = req.ProviderTypeName + "_self_service_resource_group_profile"
+}
+
+func (r *SelfServiceResourceGroupProfileResource) Schema(_ context.Context, _ resource.SchemaRequest, resp *resource.SchemaResponse) {
+ resp.Schema = schema.Schema{
+ MarkdownDescription: "Associates a resource group profile with a user in VirtFusion self-service. Changing any attribute forces recreation of the association.",
+ Attributes: map[string]schema.Attribute{
+ "id": schema.StringAttribute{
+ MarkdownDescription: "The composite identifier for this resource group profile association.",
+ Computed: true,
+ PlanModifiers: []planmodifier.String{
+ stringplanmodifier.UseStateForUnknown(),
+ },
+ },
+ "user_id": schema.Int64Attribute{
+ MarkdownDescription: "The ID of the user.",
+ Required: true,
+ PlanModifiers: []planmodifier.Int64{
+ int64planmodifier.RequiresReplace(),
+ },
+ },
+ "group_id": schema.Int64Attribute{
+ MarkdownDescription: "The ID of the resource group.",
+ Required: true,
+ PlanModifiers: []planmodifier.Int64{
+ int64planmodifier.RequiresReplace(),
+ },
+ },
+ "profile_id": schema.Int64Attribute{
+ MarkdownDescription: "The ID of the profile.",
+ Required: true,
+ PlanModifiers: []planmodifier.Int64{
+ int64planmodifier.RequiresReplace(),
+ },
+ },
+ },
+ }
+}
+
+func (r *SelfServiceResourceGroupProfileResource) Configure(_ context.Context, req resource.ConfigureRequest, resp *resource.ConfigureResponse) {
+ if req.ProviderData == nil {
+ return
+ }
+
+ c, ok := req.ProviderData.(*client.Client)
+ if !ok {
+ resp.Diagnostics.AddError(
+ "Unexpected Resource Configure Type",
+ fmt.Sprintf("Expected *client.Client, got: %T. Please report this issue to the provider developers.", req.ProviderData),
+ )
+ return
+ }
+
+ r.client = c
+}
+
+func (r *SelfServiceResourceGroupProfileResource) Create(ctx context.Context, req resource.CreateRequest, resp *resource.CreateResponse) {
+ var data SelfServiceResourceGroupProfileResourceModel
+ resp.Diagnostics.Append(req.Plan.Get(ctx, &data)...)
+ if resp.Diagnostics.HasError() {
+ return
+ }
+
+ body := map[string]int64{
+ "userId": data.UserID.ValueInt64(),
+ "groupId": data.GroupID.ValueInt64(),
+ "profileId": data.ProfileID.ValueInt64(),
+ }
+
+ _, err := r.client.Post(ctx, "/selfService/resourceGroupProfile", body)
+ if err != nil {
+ resp.Diagnostics.AddError(
+ "Error Creating Resource Group Profile Association",
+ fmt.Sprintf("Could not create resource group profile association (user=%d, group=%d, profile=%d): %s",
+ data.UserID.ValueInt64(), data.GroupID.ValueInt64(), data.ProfileID.ValueInt64(), err),
+ )
+ return
+ }
+
+ data.ID = types.StringValue(fmt.Sprintf("%d-%d-%d", data.UserID.ValueInt64(), data.GroupID.ValueInt64(), data.ProfileID.ValueInt64()))
+
+ resp.Diagnostics.Append(resp.State.Set(ctx, &data)...)
+}
+
+func (r *SelfServiceResourceGroupProfileResource) Read(ctx context.Context, req resource.ReadRequest, resp *resource.ReadResponse) {
+ var data SelfServiceResourceGroupProfileResourceModel
+ resp.Diagnostics.Append(req.State.Get(ctx, &data)...)
+ if resp.Diagnostics.HasError() {
+ return
+ }
+
+ // Return stored state as-is. The API does not provide a direct read endpoint for this association.
+ resp.Diagnostics.Append(resp.State.Set(ctx, &data)...)
+}
+
+func (r *SelfServiceResourceGroupProfileResource) Update(ctx context.Context, req resource.UpdateRequest, resp *resource.UpdateResponse) {
+ // All attributes have RequiresReplace, so Update should never be called.
+ var data SelfServiceResourceGroupProfileResourceModel
+ resp.Diagnostics.Append(req.Plan.Get(ctx, &data)...)
+ if resp.Diagnostics.HasError() {
+ return
+ }
+
+ resp.Diagnostics.Append(resp.State.Set(ctx, &data)...)
+}
+
+func (r *SelfServiceResourceGroupProfileResource) Delete(ctx context.Context, req resource.DeleteRequest, resp *resource.DeleteResponse) {
+ var data SelfServiceResourceGroupProfileResourceModel
+ resp.Diagnostics.Append(req.State.Get(ctx, &data)...)
+ if resp.Diagnostics.HasError() {
+ return
+ }
+
+ apiPath := fmt.Sprintf("/selfService/resourceGroupProfile/%d/%d/%d",
+ data.UserID.ValueInt64(), data.GroupID.ValueInt64(), data.ProfileID.ValueInt64())
+ _, err := r.client.Delete(ctx, apiPath)
+ if err != nil {
+ resp.Diagnostics.AddError(
+ "Error Deleting Resource Group Profile Association",
+ fmt.Sprintf("Could not delete resource group profile association (user=%d, group=%d, profile=%d): %s",
+ data.UserID.ValueInt64(), data.GroupID.ValueInt64(), data.ProfileID.ValueInt64(), err),
+ )
+ return
+ }
+}
+
+// ValidateConfig validates the resource configuration.
+func (r *SelfServiceResourceGroupProfileResource) ValidateConfig(ctx context.Context, req resource.ValidateConfigRequest, resp *resource.ValidateConfigResponse) {
+ var data SelfServiceResourceGroupProfileResourceModel
+ resp.Diagnostics.Append(req.Config.Get(ctx, &data)...)
+ if resp.Diagnostics.HasError() {
+ return
+ }
+
+ // Validate user_id is positive.
+ if !data.UserID.IsNull() && !data.UserID.IsUnknown() && data.UserID.ValueInt64() <= 0 {
+ resp.Diagnostics.AddAttributeError(
+ path.Root("user_id"),
+ "Invalid User ID",
+ "user_id must be a positive integer.",
+ )
+ }
+
+ // Validate group_id is positive.
+ if !data.GroupID.IsNull() && !data.GroupID.IsUnknown() && data.GroupID.ValueInt64() <= 0 {
+ resp.Diagnostics.AddAttributeError(
+ path.Root("group_id"),
+ "Invalid Group ID",
+ "group_id must be a positive integer.",
+ )
+ }
+
+ // Validate profile_id is positive.
+ if !data.ProfileID.IsNull() && !data.ProfileID.IsUnknown() && data.ProfileID.ValueInt64() <= 0 {
+ resp.Diagnostics.AddAttributeError(
+ path.Root("profile_id"),
+ "Invalid Profile ID",
+ "profile_id must be a positive integer.",
+ )
+ }
+}
diff --git a/internal/provider/resource_self_service_resource_pack.go b/internal/provider/resource_self_service_resource_pack.go
new file mode 100644
index 0000000..83002dc
--- /dev/null
+++ b/internal/provider/resource_self_service_resource_pack.go
@@ -0,0 +1,215 @@
+// Copyright (c) EZSCALE.
+// SPDX-License-Identifier: MPL-2.0
+
+package provider
+
+import (
+ "context"
+ "encoding/json"
+ "errors"
+ "fmt"
+
+ "terraform-provider-virtfusion/internal/client"
+
+ "github.com/hashicorp/terraform-plugin-framework/resource"
+ "github.com/hashicorp/terraform-plugin-framework/resource/schema"
+ "github.com/hashicorp/terraform-plugin-framework/resource/schema/int64planmodifier"
+ "github.com/hashicorp/terraform-plugin-framework/resource/schema/planmodifier"
+ "github.com/hashicorp/terraform-plugin-framework/types"
+)
+
+// Ensure provider-defined types fully satisfy framework interfaces.
+var (
+ _ resource.Resource = &SelfServiceResourcePackResource{}
+ _ resource.ResourceWithConfigure = &SelfServiceResourcePackResource{}
+)
+
+// NewSelfServiceResourcePackResource creates a new self-service resource pack resource.
+func NewSelfServiceResourcePackResource() resource.Resource {
+ return &SelfServiceResourcePackResource{}
+}
+
+// SelfServiceResourcePackResource defines the resource implementation.
+type SelfServiceResourcePackResource struct {
+ client *client.Client
+}
+
+// SelfServiceResourcePackResourceModel describes the resource data model.
+type SelfServiceResourcePackResourceModel struct {
+ ID types.Int64 `tfsdk:"id"`
+ Name types.String `tfsdk:"name"`
+ UserID types.Int64 `tfsdk:"user_id"`
+ PackID types.Int64 `tfsdk:"pack_id"`
+}
+
+func (r *SelfServiceResourcePackResource) Metadata(_ context.Context, req resource.MetadataRequest, resp *resource.MetadataResponse) {
+ resp.TypeName = req.ProviderTypeName + "_self_service_resource_pack"
+}
+
+func (r *SelfServiceResourcePackResource) Schema(_ context.Context, _ resource.SchemaRequest, resp *resource.SchemaResponse) {
+ resp.Schema = schema.Schema{
+ MarkdownDescription: "Manages a self-service resource pack in VirtFusion.",
+ Attributes: map[string]schema.Attribute{
+ "id": schema.Int64Attribute{
+ MarkdownDescription: "The identifier of the resource pack.",
+ Computed: true,
+ PlanModifiers: []planmodifier.Int64{
+ int64planmodifier.UseStateForUnknown(),
+ },
+ },
+ "name": schema.StringAttribute{
+ MarkdownDescription: "The name of the resource pack.",
+ Required: true,
+ },
+ "user_id": schema.Int64Attribute{
+ MarkdownDescription: "The ID of the user who owns the resource pack.",
+ Required: true,
+ },
+ "pack_id": schema.Int64Attribute{
+ MarkdownDescription: "The ID of the pack.",
+ Required: true,
+ },
+ },
+ }
+}
+
+func (r *SelfServiceResourcePackResource) Configure(_ context.Context, req resource.ConfigureRequest, resp *resource.ConfigureResponse) {
+ if req.ProviderData == nil {
+ return
+ }
+
+ c, ok := req.ProviderData.(*client.Client)
+ if !ok {
+ resp.Diagnostics.AddError(
+ "Unexpected Resource Configure Type",
+ fmt.Sprintf("Expected *client.Client, got: %T. Please report this issue to the provider developers.", req.ProviderData),
+ )
+ return
+ }
+
+ r.client = c
+}
+
+func (r *SelfServiceResourcePackResource) Create(ctx context.Context, req resource.CreateRequest, resp *resource.CreateResponse) {
+ var data SelfServiceResourcePackResourceModel
+ resp.Diagnostics.Append(req.Plan.Get(ctx, &data)...)
+ if resp.Diagnostics.HasError() {
+ return
+ }
+
+ packReq := client.SelfServiceResourcePackRequest{
+ Name: data.Name.ValueString(),
+ UserID: data.UserID.ValueInt64(),
+ PackID: data.PackID.ValueInt64(),
+ }
+
+ respBody, err := r.client.Post(ctx, "/selfService/resourcePack", packReq)
+ if err != nil {
+ resp.Diagnostics.AddError(
+ "Error Creating Resource Pack",
+ fmt.Sprintf("Could not create resource pack: %s", err),
+ )
+ return
+ }
+
+ var packResp client.SelfServiceResourcePackResponse
+ if err := json.Unmarshal(respBody, &packResp); err != nil {
+ resp.Diagnostics.AddError(
+ "Error Parsing Response",
+ fmt.Sprintf("Could not parse resource pack response: %s", err),
+ )
+ return
+ }
+
+ data.ID = types.Int64Value(packResp.Data.ID)
+
+ resp.Diagnostics.Append(resp.State.Set(ctx, &data)...)
+}
+
+func (r *SelfServiceResourcePackResource) Read(ctx context.Context, req resource.ReadRequest, resp *resource.ReadResponse) {
+ var data SelfServiceResourcePackResourceModel
+ resp.Diagnostics.Append(req.State.Get(ctx, &data)...)
+ if resp.Diagnostics.HasError() {
+ return
+ }
+
+ apiPath := fmt.Sprintf("/selfService/resourcePack/%d", data.ID.ValueInt64())
+ respBody, err := r.client.Get(ctx, apiPath)
+ if err != nil {
+ var apiErr *client.APIError
+ if errors.As(err, &apiErr) && apiErr.IsNotFound() {
+ // Resource no longer exists, remove from state.
+ resp.State.RemoveResource(ctx)
+ return
+ }
+
+ resp.Diagnostics.AddError(
+ "Error Reading Resource Pack",
+ fmt.Sprintf("Could not read resource pack %d: %s", data.ID.ValueInt64(), err),
+ )
+ return
+ }
+
+ var packResp client.SelfServiceResourcePackResponse
+ if err := json.Unmarshal(respBody, &packResp); err != nil {
+ resp.Diagnostics.AddError(
+ "Error Parsing Response",
+ fmt.Sprintf("Could not parse resource pack response: %s", err),
+ )
+ return
+ }
+
+ data.Name = types.StringValue(packResp.Data.Name)
+ data.UserID = types.Int64Value(packResp.Data.UserID)
+ data.PackID = types.Int64Value(packResp.Data.PackID)
+
+ resp.Diagnostics.Append(resp.State.Set(ctx, &data)...)
+}
+
+func (r *SelfServiceResourcePackResource) Update(ctx context.Context, req resource.UpdateRequest, resp *resource.UpdateResponse) {
+ var data SelfServiceResourcePackResourceModel
+ resp.Diagnostics.Append(req.Plan.Get(ctx, &data)...)
+ if resp.Diagnostics.HasError() {
+ return
+ }
+
+ packReq := client.SelfServiceResourcePackRequest{
+ Name: data.Name.ValueString(),
+ UserID: data.UserID.ValueInt64(),
+ PackID: data.PackID.ValueInt64(),
+ }
+
+ apiPath := fmt.Sprintf("/selfService/resourcePack/%d", data.ID.ValueInt64())
+ _, err := r.client.Put(ctx, apiPath, packReq)
+ if err != nil {
+ resp.Diagnostics.AddError(
+ "Error Updating Resource Pack",
+ fmt.Sprintf("Could not update resource pack %d: %s", data.ID.ValueInt64(), err),
+ )
+ return
+ }
+
+ resp.Diagnostics.Append(resp.State.Set(ctx, &data)...)
+}
+
+func (r *SelfServiceResourcePackResource) Delete(ctx context.Context, req resource.DeleteRequest, resp *resource.DeleteResponse) {
+ var data SelfServiceResourcePackResourceModel
+ resp.Diagnostics.Append(req.State.Get(ctx, &data)...)
+ if resp.Diagnostics.HasError() {
+ return
+ }
+
+ apiPath := fmt.Sprintf("/selfService/resourcePack/%d", data.ID.ValueInt64())
+ _, err := r.client.Delete(ctx, apiPath)
+ if err != nil {
+ var apiErr *client.APIError
+ if errors.As(err, &apiErr) && apiErr.IsNotFound() {
+ return
+ }
+ resp.Diagnostics.AddError(
+ "Error Deleting Resource Pack",
+ fmt.Sprintf("Could not delete resource pack %d: %s", data.ID.ValueInt64(), err),
+ )
+ return
+ }
+}
diff --git a/internal/provider/resource_server.go b/internal/provider/resource_server.go
new file mode 100644
index 0000000..2c15246
--- /dev/null
+++ b/internal/provider/resource_server.go
@@ -0,0 +1,637 @@
+// Copyright (c) EZSCALE.
+// SPDX-License-Identifier: MPL-2.0
+
+package provider
+
+import (
+ "context"
+ "encoding/json"
+ "errors"
+ "fmt"
+ "strconv"
+
+ "terraform-provider-virtfusion/internal/client"
+
+ "github.com/hashicorp/terraform-plugin-framework/path"
+ "github.com/hashicorp/terraform-plugin-framework/resource"
+ "github.com/hashicorp/terraform-plugin-framework/resource/schema"
+ "github.com/hashicorp/terraform-plugin-framework/resource/schema/booldefault"
+ "github.com/hashicorp/terraform-plugin-framework/resource/schema/int64default"
+ "github.com/hashicorp/terraform-plugin-framework/resource/schema/int64planmodifier"
+ "github.com/hashicorp/terraform-plugin-framework/resource/schema/planmodifier"
+ "github.com/hashicorp/terraform-plugin-framework/resource/schema/stringplanmodifier"
+ "github.com/hashicorp/terraform-plugin-framework/types"
+)
+
+// Ensure provider defined types fully satisfy framework interfaces.
+var (
+ _ resource.Resource = &ServerResource{}
+ _ resource.ResourceWithConfigure = &ServerResource{}
+ _ resource.ResourceWithImportState = &ServerResource{}
+)
+
+// NewServerResource returns a new server resource.
+func NewServerResource() resource.Resource {
+ return &ServerResource{}
+}
+
+// ServerResource defines the resource implementation.
+type ServerResource struct {
+ client *client.Client
+}
+
+// ServerResourceModel describes the resource data model.
+type ServerResourceModel struct {
+ // Computed
+ ID types.Int64 `tfsdk:"id"`
+ UUID types.String `tfsdk:"uuid"`
+ Hostname types.String `tfsdk:"hostname"`
+
+ // Required (create)
+ PackageID types.Int64 `tfsdk:"package_id"`
+ UserID types.Int64 `tfsdk:"user_id"`
+ HypervisorID types.Int64 `tfsdk:"hypervisor_id"`
+
+ // Optional (create)
+ Ipv4 types.Int64 `tfsdk:"ipv4"`
+ Storage types.Int64 `tfsdk:"storage"`
+ Memory types.Int64 `tfsdk:"memory"`
+ Cores types.Int64 `tfsdk:"cores"`
+ Traffic types.Int64 `tfsdk:"traffic"`
+ InboundNetworkSpeed types.Int64 `tfsdk:"inbound_network_speed"`
+ OutboundNetworkSpeed types.Int64 `tfsdk:"outbound_network_speed"`
+ StorageProfile types.Int64 `tfsdk:"storage_profile"`
+ NetworkProfile types.Int64 `tfsdk:"network_profile"`
+ DryRun types.Bool `tfsdk:"dry_run"`
+ AdditionalStorage1 types.Int64 `tfsdk:"additional_storage_1"`
+ AdditionalStorage1Profile types.Int64 `tfsdk:"additional_storage_1_profile"`
+ AdditionalStorage2 types.Int64 `tfsdk:"additional_storage_2"`
+ AdditionalStorage2Profile types.Int64 `tfsdk:"additional_storage_2_profile"`
+
+ // Optional (update-only)
+ Name types.String `tfsdk:"name"`
+ CPUThrottle types.Int64 `tfsdk:"cpu_throttle"`
+ VNCEnabled types.Bool `tfsdk:"vnc_enabled"`
+ Suspended types.Bool `tfsdk:"suspended"`
+ BackupPlanID types.Int64 `tfsdk:"backup_plan_id"`
+ CustomXML types.String `tfsdk:"custom_xml"`
+ OwnerUserID types.Int64 `tfsdk:"owner_user_id"`
+}
+
+func (r *ServerResource) Metadata(_ context.Context, req resource.MetadataRequest, resp *resource.MetadataResponse) {
+ resp.TypeName = req.ProviderTypeName + "_server"
+}
+
+func (r *ServerResource) Schema(_ context.Context, _ resource.SchemaRequest, resp *resource.SchemaResponse) {
+ resp.Schema = schema.Schema{
+ MarkdownDescription: "Manages a VirtFusion server.",
+ Attributes: map[string]schema.Attribute{
+ // Computed
+ "id": schema.Int64Attribute{
+ MarkdownDescription: "The server ID.",
+ Computed: true,
+ PlanModifiers: []planmodifier.Int64{
+ int64planmodifier.UseStateForUnknown(),
+ },
+ },
+ "uuid": schema.StringAttribute{
+ MarkdownDescription: "The server UUID.",
+ Computed: true,
+ PlanModifiers: []planmodifier.String{
+ stringplanmodifier.UseStateForUnknown(),
+ },
+ },
+ "hostname": schema.StringAttribute{
+ MarkdownDescription: "The server hostname.",
+ Computed: true,
+ PlanModifiers: []planmodifier.String{
+ stringplanmodifier.UseStateForUnknown(),
+ },
+ },
+
+ // Required
+ "package_id": schema.Int64Attribute{
+ MarkdownDescription: "The package ID for the server.",
+ Required: true,
+ },
+ "user_id": schema.Int64Attribute{
+ MarkdownDescription: "The user ID who owns the server.",
+ Required: true,
+ },
+ "hypervisor_id": schema.Int64Attribute{
+ MarkdownDescription: "The hypervisor ID where the server will be created.",
+ Required: true,
+ },
+
+ // Optional (create)
+ "ipv4": schema.Int64Attribute{
+ MarkdownDescription: "Number of IPv4 addresses to assign. Defaults to 1.",
+ Optional: true,
+ Computed: true,
+ Default: int64default.StaticInt64(1),
+ },
+ "storage": schema.Int64Attribute{
+ MarkdownDescription: "Storage size override in GB.",
+ Optional: true,
+ },
+ "memory": schema.Int64Attribute{
+ MarkdownDescription: "Memory size override in MB.",
+ Optional: true,
+ },
+ "cores": schema.Int64Attribute{
+ MarkdownDescription: "Number of CPU cores override.",
+ Optional: true,
+ },
+ "traffic": schema.Int64Attribute{
+ MarkdownDescription: "Traffic limit override in GB.",
+ Optional: true,
+ },
+ "inbound_network_speed": schema.Int64Attribute{
+ MarkdownDescription: "Inbound network speed override in Mbps.",
+ Optional: true,
+ },
+ "outbound_network_speed": schema.Int64Attribute{
+ MarkdownDescription: "Outbound network speed override in Mbps.",
+ Optional: true,
+ },
+ "storage_profile": schema.Int64Attribute{
+ MarkdownDescription: "Storage profile ID.",
+ Optional: true,
+ },
+ "network_profile": schema.Int64Attribute{
+ MarkdownDescription: "Network profile ID.",
+ Optional: true,
+ },
+ "dry_run": schema.BoolAttribute{
+ MarkdownDescription: "If true, validates the request without creating the server.",
+ Optional: true,
+ Computed: true,
+ Default: booldefault.StaticBool(false),
+ },
+ "additional_storage_1": schema.Int64Attribute{
+ MarkdownDescription: "Additional storage 1 size in GB.",
+ Optional: true,
+ },
+ "additional_storage_1_profile": schema.Int64Attribute{
+ MarkdownDescription: "Additional storage 1 profile ID.",
+ Optional: true,
+ },
+ "additional_storage_2": schema.Int64Attribute{
+ MarkdownDescription: "Additional storage 2 size in GB.",
+ Optional: true,
+ },
+ "additional_storage_2_profile": schema.Int64Attribute{
+ MarkdownDescription: "Additional storage 2 profile ID.",
+ Optional: true,
+ },
+
+ // Optional (update-only)
+ "name": schema.StringAttribute{
+ MarkdownDescription: "The server display name.",
+ Optional: true,
+ Computed: true,
+ PlanModifiers: []planmodifier.String{
+ stringplanmodifier.UseStateForUnknown(),
+ },
+ },
+ "cpu_throttle": schema.Int64Attribute{
+ MarkdownDescription: "CPU throttle percentage (0-100).",
+ Optional: true,
+ },
+ "vnc_enabled": schema.BoolAttribute{
+ MarkdownDescription: "Whether VNC is enabled on the server.",
+ Optional: true,
+ },
+ "suspended": schema.BoolAttribute{
+ MarkdownDescription: "Whether the server is suspended.",
+ Optional: true,
+ },
+ "backup_plan_id": schema.Int64Attribute{
+ MarkdownDescription: "Backup plan ID. Set to 0 to remove the backup plan.",
+ Optional: true,
+ },
+ "custom_xml": schema.StringAttribute{
+ MarkdownDescription: "Custom XML configuration for the server.",
+ Optional: true,
+ },
+ "owner_user_id": schema.Int64Attribute{
+ MarkdownDescription: "The user ID to transfer ownership to.",
+ Optional: true,
+ },
+ },
+ }
+}
+
+func (r *ServerResource) Configure(_ context.Context, req resource.ConfigureRequest, resp *resource.ConfigureResponse) {
+ if req.ProviderData == nil {
+ return
+ }
+
+ c, ok := req.ProviderData.(*client.Client)
+ if !ok {
+ resp.Diagnostics.AddError(
+ "Unexpected Resource Configure Type",
+ fmt.Sprintf("Expected *client.Client, got: %T. Please report this issue to the provider developers.", req.ProviderData),
+ )
+ return
+ }
+
+ r.client = c
+}
+
+func (r *ServerResource) Create(ctx context.Context, req resource.CreateRequest, resp *resource.CreateResponse) {
+ var plan ServerResourceModel
+ resp.Diagnostics.Append(req.Plan.Get(ctx, &plan)...)
+ if resp.Diagnostics.HasError() {
+ return
+ }
+
+ // Build the create request from plan values.
+ createReq := client.ServerCreateRequest{
+ PackageID: plan.PackageID.ValueInt64(),
+ UserID: plan.UserID.ValueInt64(),
+ HypervisorID: plan.HypervisorID.ValueInt64(),
+ }
+
+ if !plan.Ipv4.IsNull() && !plan.Ipv4.IsUnknown() {
+ v := plan.Ipv4.ValueInt64()
+ createReq.Ipv4 = &v
+ }
+ if !plan.Storage.IsNull() && !plan.Storage.IsUnknown() {
+ v := plan.Storage.ValueInt64()
+ createReq.Storage = &v
+ }
+ if !plan.Memory.IsNull() && !plan.Memory.IsUnknown() {
+ v := plan.Memory.ValueInt64()
+ createReq.Memory = &v
+ }
+ if !plan.Cores.IsNull() && !plan.Cores.IsUnknown() {
+ v := plan.Cores.ValueInt64()
+ createReq.CPUCores = &v
+ }
+ if !plan.Traffic.IsNull() && !plan.Traffic.IsUnknown() {
+ v := plan.Traffic.ValueInt64()
+ createReq.Traffic = &v
+ }
+ if !plan.InboundNetworkSpeed.IsNull() && !plan.InboundNetworkSpeed.IsUnknown() {
+ v := plan.InboundNetworkSpeed.ValueInt64()
+ createReq.NetworkSpeedInbound = &v
+ }
+ if !plan.OutboundNetworkSpeed.IsNull() && !plan.OutboundNetworkSpeed.IsUnknown() {
+ v := plan.OutboundNetworkSpeed.ValueInt64()
+ createReq.NetworkSpeedOutbound = &v
+ }
+ if !plan.StorageProfile.IsNull() && !plan.StorageProfile.IsUnknown() {
+ v := plan.StorageProfile.ValueInt64()
+ createReq.StorageProfile = &v
+ }
+ if !plan.NetworkProfile.IsNull() && !plan.NetworkProfile.IsUnknown() {
+ v := plan.NetworkProfile.ValueInt64()
+ createReq.NetworkProfile = &v
+ }
+ if !plan.DryRun.IsNull() && !plan.DryRun.IsUnknown() {
+ v := plan.DryRun.ValueBool()
+ createReq.DryRun = &v
+ }
+ if !plan.AdditionalStorage1.IsNull() && !plan.AdditionalStorage1.IsUnknown() {
+ v := plan.AdditionalStorage1.ValueInt64()
+ createReq.AdditionalStorage1 = &v
+ }
+ if !plan.AdditionalStorage1Profile.IsNull() && !plan.AdditionalStorage1Profile.IsUnknown() {
+ v := plan.AdditionalStorage1Profile.ValueInt64()
+ createReq.AdditionalStorage1Profile = &v
+ }
+ if !plan.AdditionalStorage2.IsNull() && !plan.AdditionalStorage2.IsUnknown() {
+ v := plan.AdditionalStorage2.ValueInt64()
+ createReq.AdditionalStorage2 = &v
+ }
+ if !plan.AdditionalStorage2Profile.IsNull() && !plan.AdditionalStorage2Profile.IsUnknown() {
+ v := plan.AdditionalStorage2Profile.ValueInt64()
+ createReq.AdditionalStorage2Profile = &v
+ }
+
+ // Create the server.
+ rawResp, err := r.client.Post(ctx, "/servers", createReq)
+ if err != nil {
+ resp.Diagnostics.AddError("Error Creating Server", err.Error())
+ return
+ }
+
+ // Parse the create response.
+ var serverResp client.ServerResponse
+ if err := json.Unmarshal(rawResp, &serverResp); err != nil {
+ resp.Diagnostics.AddError("Error Parsing Server Response", err.Error())
+ return
+ }
+
+ // Set computed values from the response.
+ plan.ID = types.Int64Value(serverResp.Data.ID)
+ plan.UUID = types.StringValue(serverResp.Data.UUID)
+ plan.Hostname = types.StringValue(serverResp.Data.Hostname)
+
+ // If name was not set in the plan, use the name from the API response.
+ if plan.Name.IsNull() || plan.Name.IsUnknown() {
+ plan.Name = types.StringValue(serverResp.Data.Name)
+ }
+
+ // After creation, apply update-only attributes if they are set.
+ serverID := serverResp.Data.ID
+
+ // Apply name if explicitly set in the plan.
+ if !plan.Name.IsNull() && !plan.Name.IsUnknown() && plan.Name.ValueString() != serverResp.Data.Name {
+ _, err := r.client.Put(ctx, fmt.Sprintf("/servers/%d/modify/name", serverID), client.ServerModifyNameRequest{
+ Name: plan.Name.ValueString(),
+ })
+ if err != nil {
+ resp.Diagnostics.AddError("Error Setting Server Name", err.Error())
+ return
+ }
+ }
+
+ // Apply CPU throttle if set.
+ if !plan.CPUThrottle.IsNull() && !plan.CPUThrottle.IsUnknown() {
+ _, err := r.client.Put(ctx, fmt.Sprintf("/servers/%d/modify/cpuThrottle", serverID), client.ServerModifyCPUThrottleRequest{
+ Percentage: plan.CPUThrottle.ValueInt64(),
+ })
+ if err != nil {
+ resp.Diagnostics.AddError("Error Setting CPU Throttle", err.Error())
+ return
+ }
+ }
+
+ // Apply VNC if set.
+ if !plan.VNCEnabled.IsNull() && !plan.VNCEnabled.IsUnknown() && plan.VNCEnabled.ValueBool() {
+ _, err := r.client.Post(ctx, fmt.Sprintf("/servers/%d/vnc", serverID), nil)
+ if err != nil {
+ resp.Diagnostics.AddError("Error Enabling VNC", err.Error())
+ return
+ }
+ }
+
+ // Apply suspended if set to true.
+ if !plan.Suspended.IsNull() && !plan.Suspended.IsUnknown() && plan.Suspended.ValueBool() {
+ _, err := r.client.Post(ctx, fmt.Sprintf("/servers/%d/suspend", serverID), nil)
+ if err != nil {
+ resp.Diagnostics.AddError("Error Suspending Server", err.Error())
+ return
+ }
+ }
+
+ // Apply backup plan if set.
+ if !plan.BackupPlanID.IsNull() && !plan.BackupPlanID.IsUnknown() {
+ _, err := r.client.Put(ctx, fmt.Sprintf("/servers/%d/backups/plan/%d", serverID, plan.BackupPlanID.ValueInt64()), nil)
+ if err != nil {
+ resp.Diagnostics.AddError("Error Setting Backup Plan", err.Error())
+ return
+ }
+ }
+
+ // Apply custom XML if set.
+ if !plan.CustomXML.IsNull() && !plan.CustomXML.IsUnknown() {
+ _, err := r.client.Post(ctx, fmt.Sprintf("/servers/%d/customXML", serverID), client.ServerCustomXMLRequest{
+ XML: plan.CustomXML.ValueString(),
+ })
+ if err != nil {
+ resp.Diagnostics.AddError("Error Setting Custom XML", err.Error())
+ return
+ }
+ }
+
+ // Apply owner change if set and different from the creating user.
+ if !plan.OwnerUserID.IsNull() && !plan.OwnerUserID.IsUnknown() && plan.OwnerUserID.ValueInt64() != plan.UserID.ValueInt64() {
+ _, err := r.client.Put(ctx, fmt.Sprintf("/servers/%d/owner/%d", serverID, plan.OwnerUserID.ValueInt64()), nil)
+ if err != nil {
+ resp.Diagnostics.AddError("Error Changing Server Owner", err.Error())
+ return
+ }
+ }
+
+ resp.Diagnostics.Append(resp.State.Set(ctx, &plan)...)
+}
+
+func (r *ServerResource) Read(ctx context.Context, req resource.ReadRequest, resp *resource.ReadResponse) {
+ var state ServerResourceModel
+ resp.Diagnostics.Append(req.State.Get(ctx, &state)...)
+ if resp.Diagnostics.HasError() {
+ return
+ }
+
+ rawResp, err := r.client.Get(ctx, fmt.Sprintf("/servers/%d", state.ID.ValueInt64()))
+ if err != nil {
+ var apiErr *client.APIError
+ if errors.As(err, &apiErr) && apiErr.IsNotFound() {
+ resp.State.RemoveResource(ctx)
+ return
+ }
+ resp.Diagnostics.AddError("Error Reading Server", err.Error())
+ return
+ }
+
+ var serverResp client.ServerResponse
+ if err := json.Unmarshal(rawResp, &serverResp); err != nil {
+ resp.Diagnostics.AddError("Error Parsing Server Response", err.Error())
+ return
+ }
+
+ // Map API response to state model.
+ s := serverResp.Data
+ state.ID = types.Int64Value(s.ID)
+ state.UUID = types.StringValue(s.UUID)
+ state.Hostname = types.StringValue(s.Hostname)
+ state.Name = types.StringValue(s.Name)
+ state.HypervisorID = types.Int64Value(s.HypervisorID)
+
+ // Map optional create attributes from the nested API response if they were set in state.
+ if !state.Storage.IsNull() && s.Settings != nil && s.Settings.Resources != nil {
+ state.Storage = types.Int64Value(s.Settings.Resources.Storage)
+ }
+ if !state.Memory.IsNull() && s.Settings != nil && s.Settings.Resources != nil {
+ state.Memory = types.Int64Value(s.Settings.Resources.Memory)
+ }
+ if !state.Cores.IsNull() && s.CPU != nil {
+ state.Cores = types.Int64Value(s.CPU.Cores)
+ }
+ if !state.Traffic.IsNull() && s.Settings != nil && s.Settings.Resources != nil {
+ state.Traffic = types.Int64Value(s.Settings.Resources.Traffic)
+ }
+ // NetworkSpeed and profiles are not returned by the detail API; preserve from state.
+
+ if !state.Suspended.IsNull() {
+ state.Suspended = types.BoolValue(s.Suspended)
+ }
+
+ resp.Diagnostics.Append(resp.State.Set(ctx, &state)...)
+}
+
+func (r *ServerResource) Update(ctx context.Context, req resource.UpdateRequest, resp *resource.UpdateResponse) {
+ var plan, state ServerResourceModel
+ resp.Diagnostics.Append(req.Plan.Get(ctx, &plan)...)
+ resp.Diagnostics.Append(req.State.Get(ctx, &state)...)
+ if resp.Diagnostics.HasError() {
+ return
+ }
+
+ serverID := state.ID.ValueInt64()
+
+ // Preserve computed values from state.
+ plan.ID = state.ID
+ plan.UUID = state.UUID
+ plan.Hostname = state.Hostname
+
+ // Name change.
+ if !plan.Name.Equal(state.Name) {
+ _, err := r.client.Put(ctx, fmt.Sprintf("/servers/%d/modify/name", serverID), client.ServerModifyNameRequest{
+ Name: plan.Name.ValueString(),
+ })
+ if err != nil {
+ resp.Diagnostics.AddError("Error Modifying Server Name", err.Error())
+ return
+ }
+ }
+
+ // CPU cores change.
+ if !plan.Cores.Equal(state.Cores) && !plan.Cores.IsNull() {
+ _, err := r.client.Put(ctx, fmt.Sprintf("/servers/%d/modify/cpuCores", serverID), client.ServerModifyCPURequest{
+ CPUCores: plan.Cores.ValueInt64(),
+ })
+ if err != nil {
+ resp.Diagnostics.AddError("Error Modifying Server CPU Cores", err.Error())
+ return
+ }
+ }
+
+ // Memory change.
+ if !plan.Memory.Equal(state.Memory) && !plan.Memory.IsNull() {
+ _, err := r.client.Put(ctx, fmt.Sprintf("/servers/%d/modify/memory", serverID), client.ServerModifyMemoryRequest{
+ Memory: plan.Memory.ValueInt64(),
+ })
+ if err != nil {
+ resp.Diagnostics.AddError("Error Modifying Server Memory", err.Error())
+ return
+ }
+ }
+
+ // Traffic change.
+ if !plan.Traffic.Equal(state.Traffic) && !plan.Traffic.IsNull() {
+ _, err := r.client.Put(ctx, fmt.Sprintf("/servers/%d/modify/traffic", serverID), client.ServerModifyTrafficRequest{
+ Traffic: plan.Traffic.ValueInt64(),
+ })
+ if err != nil {
+ resp.Diagnostics.AddError("Error Modifying Server Traffic", err.Error())
+ return
+ }
+ }
+
+ // CPU throttle change.
+ if !plan.CPUThrottle.Equal(state.CPUThrottle) && !plan.CPUThrottle.IsNull() {
+ _, err := r.client.Put(ctx, fmt.Sprintf("/servers/%d/modify/cpuThrottle", serverID), client.ServerModifyCPUThrottleRequest{
+ Percentage: plan.CPUThrottle.ValueInt64(),
+ })
+ if err != nil {
+ resp.Diagnostics.AddError("Error Modifying CPU Throttle", err.Error())
+ return
+ }
+ }
+
+ // VNC toggle.
+ if !plan.VNCEnabled.Equal(state.VNCEnabled) && !plan.VNCEnabled.IsNull() {
+ _, err := r.client.Post(ctx, fmt.Sprintf("/servers/%d/vnc", serverID), nil)
+ if err != nil {
+ resp.Diagnostics.AddError("Error Toggling VNC", err.Error())
+ return
+ }
+ }
+
+ // Suspended change.
+ if !plan.Suspended.Equal(state.Suspended) && !plan.Suspended.IsNull() {
+ if plan.Suspended.ValueBool() {
+ _, err := r.client.Post(ctx, fmt.Sprintf("/servers/%d/suspend", serverID), nil)
+ if err != nil {
+ resp.Diagnostics.AddError("Error Suspending Server", err.Error())
+ return
+ }
+ } else {
+ _, err := r.client.Post(ctx, fmt.Sprintf("/servers/%d/unsuspend", serverID), nil)
+ if err != nil {
+ resp.Diagnostics.AddError("Error Unsuspending Server", err.Error())
+ return
+ }
+ }
+ }
+
+ // Backup plan change.
+ if !plan.BackupPlanID.Equal(state.BackupPlanID) {
+ planID := int64(0)
+ if !plan.BackupPlanID.IsNull() {
+ planID = plan.BackupPlanID.ValueInt64()
+ }
+ _, err := r.client.Put(ctx, fmt.Sprintf("/servers/%d/backups/plan/%d", serverID, planID), nil)
+ if err != nil {
+ resp.Diagnostics.AddError("Error Modifying Backup Plan", err.Error())
+ return
+ }
+ }
+
+ // Custom XML change.
+ if !plan.CustomXML.Equal(state.CustomXML) && !plan.CustomXML.IsNull() {
+ _, err := r.client.Post(ctx, fmt.Sprintf("/servers/%d/customXML", serverID), client.ServerCustomXMLRequest{
+ XML: plan.CustomXML.ValueString(),
+ })
+ if err != nil {
+ resp.Diagnostics.AddError("Error Setting Custom XML", err.Error())
+ return
+ }
+ }
+
+ // Owner change.
+ if !plan.OwnerUserID.Equal(state.OwnerUserID) && !plan.OwnerUserID.IsNull() {
+ _, err := r.client.Put(ctx, fmt.Sprintf("/servers/%d/owner/%d", serverID, plan.OwnerUserID.ValueInt64()), nil)
+ if err != nil {
+ resp.Diagnostics.AddError("Error Changing Server Owner", err.Error())
+ return
+ }
+ }
+
+ // Package change.
+ if !plan.PackageID.Equal(state.PackageID) {
+ _, err := r.client.Put(ctx, fmt.Sprintf("/servers/%d/package/%d", serverID, plan.PackageID.ValueInt64()), nil)
+ if err != nil {
+ resp.Diagnostics.AddError("Error Changing Server Package", err.Error())
+ return
+ }
+ }
+
+ resp.Diagnostics.Append(resp.State.Set(ctx, &plan)...)
+}
+
+func (r *ServerResource) Delete(ctx context.Context, req resource.DeleteRequest, resp *resource.DeleteResponse) {
+ var state ServerResourceModel
+ resp.Diagnostics.Append(req.State.Get(ctx, &state)...)
+ if resp.Diagnostics.HasError() {
+ return
+ }
+
+ _, err := r.client.Delete(ctx, fmt.Sprintf("/servers/%d?delay=0", state.ID.ValueInt64()))
+ if err != nil {
+ var apiErr *client.APIError
+ if errors.As(err, &apiErr) && apiErr.IsNotFound() {
+ // Already deleted, nothing to do.
+ return
+ }
+ resp.Diagnostics.AddError("Error Deleting Server", err.Error())
+ return
+ }
+}
+
+func (r *ServerResource) ImportState(ctx context.Context, req resource.ImportStateRequest, resp *resource.ImportStateResponse) {
+ id, err := strconv.ParseInt(req.ID, 10, 64)
+ if err != nil {
+ resp.Diagnostics.AddError(
+ "Invalid Import ID",
+ fmt.Sprintf("Could not parse server ID %q as an integer: %s", req.ID, err),
+ )
+ return
+ }
+
+ resp.Diagnostics.Append(resp.State.SetAttribute(ctx, path.Root("id"), types.Int64Value(id))...)
+}
diff --git a/internal/provider/resource_server_build.go b/internal/provider/resource_server_build.go
new file mode 100644
index 0000000..ff7860e
--- /dev/null
+++ b/internal/provider/resource_server_build.go
@@ -0,0 +1,266 @@
+// Copyright (c) EZSCALE.
+// SPDX-License-Identifier: MPL-2.0
+
+package provider
+
+import (
+ "context"
+ "errors"
+ "fmt"
+
+ "terraform-provider-virtfusion/internal/client"
+
+ "github.com/hashicorp/terraform-plugin-framework/path"
+ "github.com/hashicorp/terraform-plugin-framework/resource"
+ "github.com/hashicorp/terraform-plugin-framework/resource/schema"
+ "github.com/hashicorp/terraform-plugin-framework/resource/schema/booldefault"
+ "github.com/hashicorp/terraform-plugin-framework/resource/schema/boolplanmodifier"
+ "github.com/hashicorp/terraform-plugin-framework/resource/schema/int64planmodifier"
+ "github.com/hashicorp/terraform-plugin-framework/resource/schema/listplanmodifier"
+ "github.com/hashicorp/terraform-plugin-framework/resource/schema/planmodifier"
+ "github.com/hashicorp/terraform-plugin-framework/resource/schema/stringplanmodifier"
+ "github.com/hashicorp/terraform-plugin-framework/types"
+)
+
+// Ensure provider-defined types fully satisfy framework interfaces.
+var (
+ _ resource.Resource = &ServerBuildResource{}
+ _ resource.ResourceWithConfigure = &ServerBuildResource{}
+)
+
+// NewServerBuildResource creates a new server build resource.
+func NewServerBuildResource() resource.Resource {
+ return &ServerBuildResource{}
+}
+
+// ServerBuildResource defines the resource implementation.
+type ServerBuildResource struct {
+ client *client.Client
+}
+
+// ServerBuildResourceModel describes the resource data model.
+type ServerBuildResourceModel struct {
+ ID types.String `tfsdk:"id"`
+ ServerID types.Int64 `tfsdk:"server_id"`
+ Name types.String `tfsdk:"name"`
+ Hostname types.String `tfsdk:"hostname"`
+ OSID types.Int64 `tfsdk:"osid"`
+ VNC types.Bool `tfsdk:"vnc"`
+ Ipv6 types.Bool `tfsdk:"ipv6"`
+ SSHKeys types.List `tfsdk:"ssh_keys"`
+ Email types.Bool `tfsdk:"email"`
+}
+
+func (r *ServerBuildResource) Metadata(_ context.Context, req resource.MetadataRequest, resp *resource.MetadataResponse) {
+ resp.TypeName = req.ProviderTypeName + "_server_build"
+}
+
+func (r *ServerBuildResource) Schema(_ context.Context, _ resource.SchemaRequest, resp *resource.SchemaResponse) {
+ resp.Schema = schema.Schema{
+ MarkdownDescription: "Builds a VirtFusion server with an operating system. This is a one-time operation — once a server is built, it stays built.",
+ Attributes: map[string]schema.Attribute{
+ "id": schema.StringAttribute{
+ MarkdownDescription: "The identifier of the server build (same as server_id).",
+ Computed: true,
+ PlanModifiers: []planmodifier.String{
+ stringplanmodifier.UseStateForUnknown(),
+ },
+ },
+ "server_id": schema.Int64Attribute{
+ MarkdownDescription: "The ID of the server to build.",
+ Required: true,
+ PlanModifiers: []planmodifier.Int64{
+ int64planmodifier.RequiresReplace(),
+ },
+ },
+ "name": schema.StringAttribute{
+ MarkdownDescription: "The name for the server build.",
+ Required: true,
+ PlanModifiers: []planmodifier.String{
+ stringplanmodifier.RequiresReplace(),
+ },
+ },
+ "osid": schema.Int64Attribute{
+ MarkdownDescription: "The operating system ID to install.",
+ Required: true,
+ PlanModifiers: []planmodifier.Int64{
+ int64planmodifier.RequiresReplace(),
+ },
+ },
+ "hostname": schema.StringAttribute{
+ MarkdownDescription: "The hostname for the server.",
+ Optional: true,
+ PlanModifiers: []planmodifier.String{
+ stringplanmodifier.RequiresReplace(),
+ },
+ },
+ "vnc": schema.BoolAttribute{
+ MarkdownDescription: "Whether to enable VNC access.",
+ Optional: true,
+ Computed: true,
+ Default: booldefault.StaticBool(false),
+ PlanModifiers: []planmodifier.Bool{
+ boolplanmodifier.RequiresReplace(),
+ },
+ },
+ "ipv6": schema.BoolAttribute{
+ MarkdownDescription: "Whether to enable IPv6.",
+ Optional: true,
+ Computed: true,
+ Default: booldefault.StaticBool(false),
+ PlanModifiers: []planmodifier.Bool{
+ boolplanmodifier.RequiresReplace(),
+ },
+ },
+ "ssh_keys": schema.ListAttribute{
+ MarkdownDescription: "List of SSH key IDs to add to the server.",
+ Optional: true,
+ ElementType: types.Int64Type,
+ PlanModifiers: []planmodifier.List{
+ listplanmodifier.RequiresReplace(),
+ },
+ },
+ "email": schema.BoolAttribute{
+ MarkdownDescription: "Whether to send a notification email after build.",
+ Optional: true,
+ Computed: true,
+ Default: booldefault.StaticBool(false),
+ PlanModifiers: []planmodifier.Bool{
+ boolplanmodifier.RequiresReplace(),
+ },
+ },
+ },
+ }
+}
+
+func (r *ServerBuildResource) Configure(_ context.Context, req resource.ConfigureRequest, resp *resource.ConfigureResponse) {
+ if req.ProviderData == nil {
+ return
+ }
+
+ c, ok := req.ProviderData.(*client.Client)
+ if !ok {
+ resp.Diagnostics.AddError(
+ "Unexpected Resource Configure Type",
+ fmt.Sprintf("Expected *client.Client, got: %T. Please report this issue to the provider developers.", req.ProviderData),
+ )
+ return
+ }
+
+ r.client = c
+}
+
+func (r *ServerBuildResource) Create(ctx context.Context, req resource.CreateRequest, resp *resource.CreateResponse) {
+ var data ServerBuildResourceModel
+ resp.Diagnostics.Append(req.Plan.Get(ctx, &data)...)
+ if resp.Diagnostics.HasError() {
+ return
+ }
+
+ // Build the API request body.
+ buildReq := client.ServerBuildRequest{
+ Name: data.Name.ValueString(),
+ OperatingSystemID: data.OSID.ValueInt64(),
+ VNC: data.VNC.ValueBool(),
+ Ipv6: data.Ipv6.ValueBool(),
+ Email: data.Email.ValueBool(),
+ }
+
+ if !data.Hostname.IsNull() && !data.Hostname.IsUnknown() {
+ buildReq.Hostname = data.Hostname.ValueString()
+ }
+
+ // Convert ssh_keys from types.List to []int64.
+ if !data.SSHKeys.IsNull() && !data.SSHKeys.IsUnknown() {
+ var sshKeys []int64
+ resp.Diagnostics.Append(data.SSHKeys.ElementsAs(ctx, &sshKeys, false)...)
+ if resp.Diagnostics.HasError() {
+ return
+ }
+ buildReq.SSHKeys = sshKeys
+ }
+
+ apiPath := fmt.Sprintf("/servers/%d/build", data.ServerID.ValueInt64())
+ _, err := r.client.Post(ctx, apiPath, buildReq)
+ if err != nil {
+ resp.Diagnostics.AddError(
+ "Error Building Server",
+ fmt.Sprintf("Could not build server %d: %s", data.ServerID.ValueInt64(), err),
+ )
+ return
+ }
+
+ // Set the ID to the server_id.
+ data.ID = types.StringValue(fmt.Sprintf("%d", data.ServerID.ValueInt64()))
+
+ resp.Diagnostics.Append(resp.State.Set(ctx, &data)...)
+}
+
+func (r *ServerBuildResource) Read(ctx context.Context, req resource.ReadRequest, resp *resource.ReadResponse) {
+ var data ServerBuildResourceModel
+ resp.Diagnostics.Append(req.State.Get(ctx, &data)...)
+ if resp.Diagnostics.HasError() {
+ return
+ }
+
+ // Verify the server still exists.
+ apiPath := fmt.Sprintf("/servers/%d", data.ServerID.ValueInt64())
+ _, err := r.client.Get(ctx, apiPath)
+ if err != nil {
+ var apiErr *client.APIError
+ if errors.As(err, &apiErr) && apiErr.IsNotFound() {
+ // Server no longer exists, remove from state.
+ resp.State.RemoveResource(ctx)
+ return
+ }
+
+ resp.Diagnostics.AddError(
+ "Error Reading Server",
+ fmt.Sprintf("Could not read server %d: %s", data.ServerID.ValueInt64(), err),
+ )
+ return
+ }
+
+ resp.Diagnostics.Append(resp.State.Set(ctx, &data)...)
+}
+
+func (r *ServerBuildResource) Update(ctx context.Context, req resource.UpdateRequest, resp *resource.UpdateResponse) {
+ var data ServerBuildResourceModel
+ resp.Diagnostics.Append(req.Plan.Get(ctx, &data)...)
+ if resp.Diagnostics.HasError() {
+ return
+ }
+
+ resp.Diagnostics.Append(resp.State.Set(ctx, &data)...)
+}
+
+func (r *ServerBuildResource) Delete(_ context.Context, _ resource.DeleteRequest, _ *resource.DeleteResponse) {
+ // No-op: building a server is not reversible. Removing from state only.
+}
+
+// ValidateConfig validates the resource configuration.
+func (r *ServerBuildResource) ValidateConfig(ctx context.Context, req resource.ValidateConfigRequest, resp *resource.ValidateConfigResponse) {
+ var data ServerBuildResourceModel
+ resp.Diagnostics.Append(req.Config.Get(ctx, &data)...)
+ if resp.Diagnostics.HasError() {
+ return
+ }
+
+ // Validate server_id is positive.
+ if !data.ServerID.IsNull() && !data.ServerID.IsUnknown() && data.ServerID.ValueInt64() <= 0 {
+ resp.Diagnostics.AddAttributeError(
+ path.Root("server_id"),
+ "Invalid Server ID",
+ "server_id must be a positive integer.",
+ )
+ }
+
+ // Validate osid is positive.
+ if !data.OSID.IsNull() && !data.OSID.IsUnknown() && data.OSID.ValueInt64() <= 0 {
+ resp.Diagnostics.AddAttributeError(
+ path.Root("osid"),
+ "Invalid OS ID",
+ "osid must be a positive integer.",
+ )
+ }
+}
diff --git a/internal/provider/resource_server_firewall.go b/internal/provider/resource_server_firewall.go
new file mode 100644
index 0000000..fcaad4a
--- /dev/null
+++ b/internal/provider/resource_server_firewall.go
@@ -0,0 +1,317 @@
+// Copyright (c) EZSCALE.
+// SPDX-License-Identifier: MPL-2.0
+
+package provider
+
+import (
+ "context"
+ "encoding/json"
+ "errors"
+ "fmt"
+
+ "terraform-provider-virtfusion/internal/client"
+
+ "github.com/hashicorp/terraform-plugin-framework/attr"
+ "github.com/hashicorp/terraform-plugin-framework/diag"
+ "github.com/hashicorp/terraform-plugin-framework/resource"
+ "github.com/hashicorp/terraform-plugin-framework/resource/schema"
+ "github.com/hashicorp/terraform-plugin-framework/resource/schema/planmodifier"
+ "github.com/hashicorp/terraform-plugin-framework/resource/schema/stringdefault"
+ "github.com/hashicorp/terraform-plugin-framework/resource/schema/stringplanmodifier"
+ "github.com/hashicorp/terraform-plugin-framework/types"
+)
+
+var (
+ _ resource.Resource = &ServerFirewallResource{}
+ _ resource.ResourceWithConfigure = &ServerFirewallResource{}
+)
+
+// NewServerFirewallResource returns a new resource for managing server firewalls.
+func NewServerFirewallResource() resource.Resource {
+ return &ServerFirewallResource{}
+}
+
+// ServerFirewallResource defines the resource implementation.
+type ServerFirewallResource struct {
+ client *client.Client
+}
+
+// ServerFirewallResourceModel describes the resource data model.
+type ServerFirewallResourceModel struct {
+ ID types.String `tfsdk:"id"`
+ ServerID types.Int64 `tfsdk:"server_id"`
+ InterfaceName types.String `tfsdk:"interface_name"`
+ Rules types.List `tfsdk:"rules"`
+}
+
+// FirewallRuleModel describes a single firewall rule.
+type FirewallRuleModel struct {
+ Action types.String `tfsdk:"action"`
+ Direction types.String `tfsdk:"direction"`
+ Protocol types.String `tfsdk:"protocol"`
+ Port types.String `tfsdk:"port"`
+ IP types.String `tfsdk:"ip"`
+}
+
+func (r *ServerFirewallResource) Metadata(_ context.Context, req resource.MetadataRequest, resp *resource.MetadataResponse) {
+ resp.TypeName = req.ProviderTypeName + "_server_firewall"
+}
+
+func (r *ServerFirewallResource) Schema(_ context.Context, _ resource.SchemaRequest, resp *resource.SchemaResponse) {
+ resp.Schema = schema.Schema{
+ MarkdownDescription: "Manages a VirtFusion server firewall.",
+ Attributes: map[string]schema.Attribute{
+ "id": schema.StringAttribute{
+ MarkdownDescription: "Composite identifier in the format `server_id/interface_name`.",
+ Computed: true,
+ PlanModifiers: []planmodifier.String{
+ stringplanmodifier.UseStateForUnknown(),
+ },
+ },
+ "server_id": schema.Int64Attribute{
+ MarkdownDescription: "The ID of the server.",
+ Required: true,
+ },
+ "interface_name": schema.StringAttribute{
+ MarkdownDescription: "The network interface name. Defaults to `eth0`.",
+ Optional: true,
+ Computed: true,
+ Default: stringdefault.StaticString("eth0"),
+ },
+ "rules": schema.ListNestedAttribute{
+ MarkdownDescription: "The firewall rules.",
+ Optional: true,
+ NestedObject: schema.NestedAttributeObject{
+ Attributes: map[string]schema.Attribute{
+ "action": schema.StringAttribute{
+ MarkdownDescription: "The action for the rule (e.g. `accept`, `drop`).",
+ Required: true,
+ },
+ "direction": schema.StringAttribute{
+ MarkdownDescription: "The direction for the rule (e.g. `in`, `out`).",
+ Required: true,
+ },
+ "protocol": schema.StringAttribute{
+ MarkdownDescription: "The protocol for the rule (e.g. `tcp`, `udp`).",
+ Required: true,
+ },
+ "port": schema.StringAttribute{
+ MarkdownDescription: "The port or port range for the rule.",
+ Required: true,
+ },
+ "ip": schema.StringAttribute{
+ MarkdownDescription: "The IP address or CIDR for the rule.",
+ Required: true,
+ },
+ },
+ },
+ },
+ },
+ }
+}
+
+func (r *ServerFirewallResource) Configure(_ context.Context, req resource.ConfigureRequest, resp *resource.ConfigureResponse) {
+ if req.ProviderData == nil {
+ return
+ }
+
+ c, ok := req.ProviderData.(*client.Client)
+ if !ok {
+ resp.Diagnostics.AddError(
+ "Unexpected Resource Configure Type",
+ fmt.Sprintf("Expected *client.Client, got: %T.", req.ProviderData),
+ )
+ return
+ }
+
+ r.client = c
+}
+
+func (r *ServerFirewallResource) Create(ctx context.Context, req resource.CreateRequest, resp *resource.CreateResponse) {
+ var data ServerFirewallResourceModel
+ resp.Diagnostics.Append(req.Plan.Get(ctx, &data)...)
+ if resp.Diagnostics.HasError() {
+ return
+ }
+
+ serverID := data.ServerID.ValueInt64()
+ iface := data.InterfaceName.ValueString()
+
+ // Enable the firewall
+ _, err := r.client.Post(ctx, fmt.Sprintf("/servers/%d/firewall/%s/enable", serverID, iface), nil)
+ if err != nil {
+ resp.Diagnostics.AddError("Error enabling server firewall", err.Error())
+ return
+ }
+
+ // Set rules if provided
+ rules, diags := r.extractRules(ctx, data)
+ resp.Diagnostics.Append(diags...)
+ if resp.Diagnostics.HasError() {
+ return
+ }
+
+ if len(rules) > 0 {
+ rulesReq := client.FirewallSetRulesRequest{Rules: rules}
+ _, err = r.client.Post(ctx, fmt.Sprintf("/servers/%d/firewall/%s/rules", serverID, iface), rulesReq)
+ if err != nil {
+ resp.Diagnostics.AddError("Error setting firewall rules", err.Error())
+ return
+ }
+ }
+
+ data.ID = types.StringValue(fmt.Sprintf("%d/%s", serverID, iface))
+
+ resp.Diagnostics.Append(resp.State.Set(ctx, &data)...)
+}
+
+func (r *ServerFirewallResource) Read(ctx context.Context, req resource.ReadRequest, resp *resource.ReadResponse) {
+ var data ServerFirewallResourceModel
+ resp.Diagnostics.Append(req.State.Get(ctx, &data)...)
+ if resp.Diagnostics.HasError() {
+ return
+ }
+
+ serverID := data.ServerID.ValueInt64()
+ iface := data.InterfaceName.ValueString()
+
+ result, err := r.client.Get(ctx, fmt.Sprintf("/servers/%d/firewall/%s", serverID, iface))
+ if err != nil {
+ var apiErr *client.APIError
+ if errors.As(err, &apiErr) && apiErr.IsNotFound() {
+ resp.State.RemoveResource(ctx)
+ return
+ }
+ resp.Diagnostics.AddError("Error reading server firewall", err.Error())
+ return
+ }
+
+ var fwResp client.FirewallResponse
+ if err := json.Unmarshal(result, &fwResp); err != nil {
+ resp.Diagnostics.AddError("Error parsing firewall response", err.Error())
+ return
+ }
+
+ // If the firewall is not enabled, remove from state
+ if !fwResp.Data.Enabled {
+ resp.State.RemoveResource(ctx)
+ return
+ }
+
+ data.ID = types.StringValue(fmt.Sprintf("%d/%s", serverID, iface))
+
+ // Map API rules to the model
+ ruleObjects := make([]attr.Value, len(fwResp.Data.Rules))
+ for i, rule := range fwResp.Data.Rules {
+ ruleObj, diags := types.ObjectValue(
+ firewallRuleAttrTypes(),
+ map[string]attr.Value{
+ "action": types.StringValue(rule.Action),
+ "direction": types.StringValue(rule.Direction),
+ "protocol": types.StringValue(rule.Protocol),
+ "port": types.StringValue(rule.Port),
+ "ip": types.StringValue(rule.IP),
+ },
+ )
+ resp.Diagnostics.Append(diags...)
+ if resp.Diagnostics.HasError() {
+ return
+ }
+ ruleObjects[i] = ruleObj
+ }
+
+ rulesList, diags := types.ListValue(types.ObjectType{AttrTypes: firewallRuleAttrTypes()}, ruleObjects)
+ resp.Diagnostics.Append(diags...)
+ if resp.Diagnostics.HasError() {
+ return
+ }
+ data.Rules = rulesList
+
+ resp.Diagnostics.Append(resp.State.Set(ctx, &data)...)
+}
+
+func (r *ServerFirewallResource) Update(ctx context.Context, req resource.UpdateRequest, resp *resource.UpdateResponse) {
+ var data ServerFirewallResourceModel
+ resp.Diagnostics.Append(req.Plan.Get(ctx, &data)...)
+ if resp.Diagnostics.HasError() {
+ return
+ }
+
+ serverID := data.ServerID.ValueInt64()
+ iface := data.InterfaceName.ValueString()
+
+ rules, diags := r.extractRules(ctx, data)
+ resp.Diagnostics.Append(diags...)
+ if resp.Diagnostics.HasError() {
+ return
+ }
+
+ rulesReq := client.FirewallSetRulesRequest{Rules: rules}
+ _, err := r.client.Post(ctx, fmt.Sprintf("/servers/%d/firewall/%s/rules", serverID, iface), rulesReq)
+ if err != nil {
+ resp.Diagnostics.AddError("Error updating firewall rules", err.Error())
+ return
+ }
+
+ data.ID = types.StringValue(fmt.Sprintf("%d/%s", serverID, iface))
+
+ resp.Diagnostics.Append(resp.State.Set(ctx, &data)...)
+}
+
+func (r *ServerFirewallResource) Delete(ctx context.Context, req resource.DeleteRequest, resp *resource.DeleteResponse) {
+ var data ServerFirewallResourceModel
+ resp.Diagnostics.Append(req.State.Get(ctx, &data)...)
+ if resp.Diagnostics.HasError() {
+ return
+ }
+
+ serverID := data.ServerID.ValueInt64()
+ iface := data.InterfaceName.ValueString()
+
+ _, err := r.client.Post(ctx, fmt.Sprintf("/servers/%d/firewall/%s/disable", serverID, iface), nil)
+ if err != nil {
+ var apiErr *client.APIError
+ if errors.As(err, &apiErr) && apiErr.IsNotFound() {
+ return
+ }
+ resp.Diagnostics.AddError("Error disabling server firewall", err.Error())
+ }
+}
+
+// extractRules converts the rules list from the model into client.FirewallRule slice.
+func (r *ServerFirewallResource) extractRules(ctx context.Context, data ServerFirewallResourceModel) ([]client.FirewallRule, diag.Diagnostics) {
+ var diags diag.Diagnostics
+
+ if data.Rules.IsNull() || data.Rules.IsUnknown() {
+ return nil, diags
+ }
+
+ var ruleModels []FirewallRuleModel
+ diags.Append(data.Rules.ElementsAs(ctx, &ruleModels, false)...)
+ if diags.HasError() {
+ return nil, diags
+ }
+
+ rules := make([]client.FirewallRule, len(ruleModels))
+ for i, rm := range ruleModels {
+ rules[i] = client.FirewallRule{
+ Action: rm.Action.ValueString(),
+ Direction: rm.Direction.ValueString(),
+ Protocol: rm.Protocol.ValueString(),
+ Port: rm.Port.ValueString(),
+ IP: rm.IP.ValueString(),
+ }
+ }
+ return rules, diags
+}
+
+// firewallRuleAttrTypes returns the attribute types for a firewall rule object.
+func firewallRuleAttrTypes() map[string]attr.Type {
+ return map[string]attr.Type{
+ "action": types.StringType,
+ "direction": types.StringType,
+ "protocol": types.StringType,
+ "port": types.StringType,
+ "ip": types.StringType,
+ }
+}
diff --git a/internal/provider/resource_server_ipv4.go b/internal/provider/resource_server_ipv4.go
new file mode 100644
index 0000000..fc7fd99
--- /dev/null
+++ b/internal/provider/resource_server_ipv4.go
@@ -0,0 +1,160 @@
+// Copyright (c) EZSCALE.
+// SPDX-License-Identifier: MPL-2.0
+
+package provider
+
+import (
+ "context"
+ "errors"
+ "fmt"
+
+ "terraform-provider-virtfusion/internal/client"
+
+ "github.com/hashicorp/terraform-plugin-framework/resource"
+ "github.com/hashicorp/terraform-plugin-framework/resource/schema"
+ "github.com/hashicorp/terraform-plugin-framework/resource/schema/int64default"
+ "github.com/hashicorp/terraform-plugin-framework/resource/schema/int64planmodifier"
+ "github.com/hashicorp/terraform-plugin-framework/resource/schema/planmodifier"
+ "github.com/hashicorp/terraform-plugin-framework/resource/schema/stringplanmodifier"
+ "github.com/hashicorp/terraform-plugin-framework/types"
+)
+
+var (
+ _ resource.Resource = &ServerIPv4Resource{}
+ _ resource.ResourceWithConfigure = &ServerIPv4Resource{}
+)
+
+// NewServerIPv4Resource returns a new resource for managing server IPv4 addresses.
+func NewServerIPv4Resource() resource.Resource {
+ return &ServerIPv4Resource{}
+}
+
+// ServerIPv4Resource defines the resource implementation.
+type ServerIPv4Resource struct {
+ client *client.Client
+}
+
+// ServerIPv4ResourceModel describes the resource data model.
+type ServerIPv4ResourceModel struct {
+ ID types.String `tfsdk:"id"`
+ ServerID types.Int64 `tfsdk:"server_id"`
+ Quantity types.Int64 `tfsdk:"quantity"`
+}
+
+func (r *ServerIPv4Resource) Metadata(_ context.Context, req resource.MetadataRequest, resp *resource.MetadataResponse) {
+ resp.TypeName = req.ProviderTypeName + "_server_ipv4"
+}
+
+func (r *ServerIPv4Resource) Schema(_ context.Context, _ resource.SchemaRequest, resp *resource.SchemaResponse) {
+ resp.Schema = schema.Schema{
+ MarkdownDescription: "Adds IPv4 addresses to a VirtFusion server.",
+ Attributes: map[string]schema.Attribute{
+ "id": schema.StringAttribute{
+ MarkdownDescription: "Resource identifier.",
+ Computed: true,
+ PlanModifiers: []planmodifier.String{
+ stringplanmodifier.UseStateForUnknown(),
+ },
+ },
+ "server_id": schema.Int64Attribute{
+ MarkdownDescription: "The ID of the server. Changing this forces a new resource to be created.",
+ Required: true,
+ PlanModifiers: []planmodifier.Int64{
+ int64planmodifier.RequiresReplace(),
+ },
+ },
+ "quantity": schema.Int64Attribute{
+ MarkdownDescription: "The number of IPv4 addresses to add. Defaults to `1`. Changing this forces a new resource to be created.",
+ Optional: true,
+ Computed: true,
+ Default: int64default.StaticInt64(1),
+ PlanModifiers: []planmodifier.Int64{
+ int64planmodifier.RequiresReplace(),
+ },
+ },
+ },
+ }
+}
+
+func (r *ServerIPv4Resource) Configure(_ context.Context, req resource.ConfigureRequest, resp *resource.ConfigureResponse) {
+ if req.ProviderData == nil {
+ return
+ }
+
+ c, ok := req.ProviderData.(*client.Client)
+ if !ok {
+ resp.Diagnostics.AddError(
+ "Unexpected Resource Configure Type",
+ fmt.Sprintf("Expected *client.Client, got: %T.", req.ProviderData),
+ )
+ return
+ }
+
+ r.client = c
+}
+
+func (r *ServerIPv4Resource) Create(ctx context.Context, req resource.CreateRequest, resp *resource.CreateResponse) {
+ var data ServerIPv4ResourceModel
+ resp.Diagnostics.Append(req.Plan.Get(ctx, &data)...)
+ if resp.Diagnostics.HasError() {
+ return
+ }
+
+ serverID := data.ServerID.ValueInt64()
+ quantity := data.Quantity.ValueInt64()
+
+ var body interface{}
+ if quantity > 1 {
+ body = client.ServerIPv4AddRequest{
+ Quantity: quantity,
+ }
+ }
+
+ _, err := r.client.Post(ctx, fmt.Sprintf("/servers/%d/ipv4", serverID), body)
+ if err != nil {
+ resp.Diagnostics.AddError("Error adding IPv4 to server", err.Error())
+ return
+ }
+
+ data.ID = types.StringValue(fmt.Sprintf("%d/ipv4/%d", serverID, quantity))
+
+ resp.Diagnostics.Append(resp.State.Set(ctx, &data)...)
+}
+
+func (r *ServerIPv4Resource) Read(ctx context.Context, req resource.ReadRequest, resp *resource.ReadResponse) {
+ var data ServerIPv4ResourceModel
+ resp.Diagnostics.Append(req.State.Get(ctx, &data)...)
+ if resp.Diagnostics.HasError() {
+ return
+ }
+
+ // No dedicated read endpoint; return stored state as-is.
+ resp.Diagnostics.Append(resp.State.Set(ctx, &data)...)
+}
+
+func (r *ServerIPv4Resource) Update(_ context.Context, _ resource.UpdateRequest, resp *resource.UpdateResponse) {
+ // All attributes have RequiresReplace, so Update should never be called.
+ resp.Diagnostics.AddError(
+ "Update Not Supported",
+ "All attributes of virtfusion_server_ipv4 require replacement. This function should not be called.",
+ )
+}
+
+func (r *ServerIPv4Resource) Delete(ctx context.Context, req resource.DeleteRequest, resp *resource.DeleteResponse) {
+ var data ServerIPv4ResourceModel
+ resp.Diagnostics.Append(req.State.Get(ctx, &data)...)
+ if resp.Diagnostics.HasError() {
+ return
+ }
+
+ serverID := data.ServerID.ValueInt64()
+
+ _, err := r.client.Delete(ctx, fmt.Sprintf("/servers/%d/ipv4", serverID))
+ if err != nil {
+ var apiErr *client.APIError
+ if errors.As(err, &apiErr) && apiErr.IsNotFound() {
+ return
+ }
+ resp.Diagnostics.AddError("Error removing IPv4 from server", err.Error())
+ }
+}
diff --git a/internal/provider/resource_server_network_whitelist.go b/internal/provider/resource_server_network_whitelist.go
new file mode 100644
index 0000000..85e9094
--- /dev/null
+++ b/internal/provider/resource_server_network_whitelist.go
@@ -0,0 +1,158 @@
+// Copyright (c) EZSCALE.
+// SPDX-License-Identifier: MPL-2.0
+
+package provider
+
+import (
+ "context"
+ "errors"
+ "fmt"
+
+ "terraform-provider-virtfusion/internal/client"
+
+ "github.com/hashicorp/terraform-plugin-framework/resource"
+ "github.com/hashicorp/terraform-plugin-framework/resource/schema"
+ "github.com/hashicorp/terraform-plugin-framework/resource/schema/int64planmodifier"
+ "github.com/hashicorp/terraform-plugin-framework/resource/schema/planmodifier"
+ "github.com/hashicorp/terraform-plugin-framework/resource/schema/stringplanmodifier"
+ "github.com/hashicorp/terraform-plugin-framework/types"
+)
+
+var (
+ _ resource.Resource = &ServerNetworkWhitelistResource{}
+ _ resource.ResourceWithConfigure = &ServerNetworkWhitelistResource{}
+)
+
+// NewServerNetworkWhitelistResource returns a new resource for managing server network whitelist entries.
+func NewServerNetworkWhitelistResource() resource.Resource {
+ return &ServerNetworkWhitelistResource{}
+}
+
+// ServerNetworkWhitelistResource defines the resource implementation.
+type ServerNetworkWhitelistResource struct {
+ client *client.Client
+}
+
+// ServerNetworkWhitelistResourceModel describes the resource data model.
+type ServerNetworkWhitelistResourceModel struct {
+ ID types.String `tfsdk:"id"`
+ ServerID types.Int64 `tfsdk:"server_id"`
+ IP types.String `tfsdk:"ip"`
+}
+
+func (r *ServerNetworkWhitelistResource) Metadata(_ context.Context, req resource.MetadataRequest, resp *resource.MetadataResponse) {
+ resp.TypeName = req.ProviderTypeName + "_server_network_whitelist"
+}
+
+func (r *ServerNetworkWhitelistResource) Schema(_ context.Context, _ resource.SchemaRequest, resp *resource.SchemaResponse) {
+ resp.Schema = schema.Schema{
+ MarkdownDescription: "Manages a VirtFusion server network whitelist entry.",
+ Attributes: map[string]schema.Attribute{
+ "id": schema.StringAttribute{
+ MarkdownDescription: "Composite identifier in the format `server_id/ip`.",
+ Computed: true,
+ PlanModifiers: []planmodifier.String{
+ stringplanmodifier.UseStateForUnknown(),
+ },
+ },
+ "server_id": schema.Int64Attribute{
+ MarkdownDescription: "The ID of the server. Changing this forces a new resource to be created.",
+ Required: true,
+ PlanModifiers: []planmodifier.Int64{
+ int64planmodifier.RequiresReplace(),
+ },
+ },
+ "ip": schema.StringAttribute{
+ MarkdownDescription: "The IP address to whitelist. Changing this forces a new resource to be created.",
+ Required: true,
+ PlanModifiers: []planmodifier.String{
+ stringplanmodifier.RequiresReplace(),
+ },
+ },
+ },
+ }
+}
+
+func (r *ServerNetworkWhitelistResource) Configure(_ context.Context, req resource.ConfigureRequest, resp *resource.ConfigureResponse) {
+ if req.ProviderData == nil {
+ return
+ }
+
+ c, ok := req.ProviderData.(*client.Client)
+ if !ok {
+ resp.Diagnostics.AddError(
+ "Unexpected Resource Configure Type",
+ fmt.Sprintf("Expected *client.Client, got: %T.", req.ProviderData),
+ )
+ return
+ }
+
+ r.client = c
+}
+
+func (r *ServerNetworkWhitelistResource) Create(ctx context.Context, req resource.CreateRequest, resp *resource.CreateResponse) {
+ var data ServerNetworkWhitelistResourceModel
+ resp.Diagnostics.Append(req.Plan.Get(ctx, &data)...)
+ if resp.Diagnostics.HasError() {
+ return
+ }
+
+ serverID := data.ServerID.ValueInt64()
+ ip := data.IP.ValueString()
+
+ body := client.NetworkWhitelistRequest{
+ IP: ip,
+ }
+
+ _, err := r.client.Post(ctx, fmt.Sprintf("/servers/%d/networkWhitelist", serverID), body)
+ if err != nil {
+ resp.Diagnostics.AddError("Error adding network whitelist entry", err.Error())
+ return
+ }
+
+ data.ID = types.StringValue(fmt.Sprintf("%d/%s", serverID, ip))
+
+ resp.Diagnostics.Append(resp.State.Set(ctx, &data)...)
+}
+
+func (r *ServerNetworkWhitelistResource) Read(ctx context.Context, req resource.ReadRequest, resp *resource.ReadResponse) {
+ var data ServerNetworkWhitelistResourceModel
+ resp.Diagnostics.Append(req.State.Get(ctx, &data)...)
+ if resp.Diagnostics.HasError() {
+ return
+ }
+
+ // No dedicated read endpoint; return stored state as-is.
+ resp.Diagnostics.Append(resp.State.Set(ctx, &data)...)
+}
+
+func (r *ServerNetworkWhitelistResource) Update(_ context.Context, _ resource.UpdateRequest, resp *resource.UpdateResponse) {
+ // All attributes have RequiresReplace, so Update should never be called.
+ resp.Diagnostics.AddError(
+ "Update Not Supported",
+ "All attributes of virtfusion_server_network_whitelist require replacement. This function should not be called.",
+ )
+}
+
+func (r *ServerNetworkWhitelistResource) Delete(ctx context.Context, req resource.DeleteRequest, resp *resource.DeleteResponse) {
+ var data ServerNetworkWhitelistResourceModel
+ resp.Diagnostics.Append(req.State.Get(ctx, &data)...)
+ if resp.Diagnostics.HasError() {
+ return
+ }
+
+ serverID := data.ServerID.ValueInt64()
+
+ body := client.NetworkWhitelistRequest{
+ IP: data.IP.ValueString(),
+ }
+
+ _, err := r.client.DeleteWithBody(ctx, fmt.Sprintf("/servers/%d/networkWhitelist", serverID), body)
+ if err != nil {
+ var apiErr *client.APIError
+ if errors.As(err, &apiErr) && apiErr.IsNotFound() {
+ return
+ }
+ resp.Diagnostics.AddError("Error removing network whitelist entry", err.Error())
+ }
+}
diff --git a/internal/provider/resource_server_password_reset.go b/internal/provider/resource_server_password_reset.go
new file mode 100644
index 0000000..151fced
--- /dev/null
+++ b/internal/provider/resource_server_password_reset.go
@@ -0,0 +1,199 @@
+// Copyright (c) EZSCALE.
+// SPDX-License-Identifier: MPL-2.0
+
+package provider
+
+import (
+ "context"
+ "encoding/json"
+ "fmt"
+ "time"
+
+ "terraform-provider-virtfusion/internal/client"
+
+ "github.com/hashicorp/terraform-plugin-framework/path"
+ "github.com/hashicorp/terraform-plugin-framework/resource"
+ "github.com/hashicorp/terraform-plugin-framework/resource/schema"
+ "github.com/hashicorp/terraform-plugin-framework/resource/schema/mapplanmodifier"
+ "github.com/hashicorp/terraform-plugin-framework/resource/schema/planmodifier"
+ "github.com/hashicorp/terraform-plugin-framework/resource/schema/stringplanmodifier"
+ "github.com/hashicorp/terraform-plugin-framework/types"
+)
+
+// Ensure provider-defined types fully satisfy framework interfaces.
+var (
+ _ resource.Resource = &ServerPasswordResetResource{}
+ _ resource.ResourceWithConfigure = &ServerPasswordResetResource{}
+)
+
+// NewServerPasswordResetResource creates a new server password reset resource.
+func NewServerPasswordResetResource() resource.Resource {
+ return &ServerPasswordResetResource{}
+}
+
+// ServerPasswordResetResource defines the resource implementation.
+type ServerPasswordResetResource struct {
+ client *client.Client
+}
+
+// ServerPasswordResetResourceModel describes the resource data model.
+type ServerPasswordResetResourceModel struct {
+ ID types.String `tfsdk:"id"`
+ ServerID types.Int64 `tfsdk:"server_id"`
+ User types.String `tfsdk:"user"`
+ Password types.String `tfsdk:"password"`
+ Triggers types.Map `tfsdk:"triggers"`
+}
+
+func (r *ServerPasswordResetResource) Metadata(_ context.Context, req resource.MetadataRequest, resp *resource.MetadataResponse) {
+ resp.TypeName = req.ProviderTypeName + "_server_password_reset"
+}
+
+func (r *ServerPasswordResetResource) Schema(_ context.Context, _ resource.SchemaRequest, resp *resource.SchemaResponse) {
+ resp.Schema = schema.Schema{
+ MarkdownDescription: "Resets the password for a VirtFusion server. This is a trigger-style resource — the reset is executed on create and can be re-triggered by changing the `triggers` attribute.",
+ Attributes: map[string]schema.Attribute{
+ "id": schema.StringAttribute{
+ MarkdownDescription: "The identifier for this password reset.",
+ Computed: true,
+ PlanModifiers: []planmodifier.String{
+ stringplanmodifier.UseStateForUnknown(),
+ },
+ },
+ "server_id": schema.Int64Attribute{
+ MarkdownDescription: "The ID of the server to reset the password for.",
+ Required: true,
+ },
+ "user": schema.StringAttribute{
+ MarkdownDescription: "The user to reset the password for. Must be `root` (Linux) or `Administrator` (Windows).",
+ Required: true,
+ },
+ "password": schema.StringAttribute{
+ MarkdownDescription: "The new password generated by the reset operation.",
+ Computed: true,
+ Sensitive: true,
+ PlanModifiers: []planmodifier.String{
+ stringplanmodifier.UseStateForUnknown(),
+ },
+ },
+ "triggers": schema.MapAttribute{
+ MarkdownDescription: "A map of arbitrary strings that, when changed, will cause the password reset to be re-executed. Works like `triggers` in `terraform_data`.",
+ ElementType: types.StringType,
+ Optional: true,
+ PlanModifiers: []planmodifier.Map{
+ mapplanmodifier.RequiresReplace(),
+ },
+ },
+ },
+ }
+}
+
+func (r *ServerPasswordResetResource) Configure(_ context.Context, req resource.ConfigureRequest, resp *resource.ConfigureResponse) {
+ if req.ProviderData == nil {
+ return
+ }
+
+ c, ok := req.ProviderData.(*client.Client)
+ if !ok {
+ resp.Diagnostics.AddError(
+ "Unexpected Resource Configure Type",
+ fmt.Sprintf("Expected *client.Client, got: %T. Please report this issue to the provider developers.", req.ProviderData),
+ )
+ return
+ }
+
+ r.client = c
+}
+
+func (r *ServerPasswordResetResource) Create(ctx context.Context, req resource.CreateRequest, resp *resource.CreateResponse) {
+ var data ServerPasswordResetResourceModel
+ resp.Diagnostics.Append(req.Plan.Get(ctx, &data)...)
+ if resp.Diagnostics.HasError() {
+ return
+ }
+
+ body := map[string]string{
+ "user": data.User.ValueString(),
+ }
+
+ apiPath := fmt.Sprintf("/servers/%d/resetPassword", data.ServerID.ValueInt64())
+ rawResp, err := r.client.Post(ctx, apiPath, body)
+ if err != nil {
+ resp.Diagnostics.AddError(
+ "Error Resetting Server Password",
+ fmt.Sprintf("Could not reset password for server %d: %s", data.ServerID.ValueInt64(), err),
+ )
+ return
+ }
+
+ // Parse the response for the new password.
+ if rawResp != nil {
+ var passResp client.PasswordResetResponse
+ if jsonErr := json.Unmarshal(rawResp, &passResp); jsonErr == nil && passResp.Data.Password != "" {
+ data.Password = types.StringValue(passResp.Data.Password)
+ } else {
+ data.Password = types.StringValue("")
+ }
+ } else {
+ data.Password = types.StringValue("")
+ }
+
+ data.ID = types.StringValue(fmt.Sprintf("%d-%d", data.ServerID.ValueInt64(), time.Now().UnixNano()))
+
+ resp.Diagnostics.Append(resp.State.Set(ctx, &data)...)
+}
+
+func (r *ServerPasswordResetResource) Read(ctx context.Context, req resource.ReadRequest, resp *resource.ReadResponse) {
+ var data ServerPasswordResetResourceModel
+ resp.Diagnostics.Append(req.State.Get(ctx, &data)...)
+ if resp.Diagnostics.HasError() {
+ return
+ }
+
+ // Return stored state as-is for trigger-style resources.
+ resp.Diagnostics.Append(resp.State.Set(ctx, &data)...)
+}
+
+func (r *ServerPasswordResetResource) Update(ctx context.Context, req resource.UpdateRequest, resp *resource.UpdateResponse) {
+ var data ServerPasswordResetResourceModel
+ resp.Diagnostics.Append(req.Plan.Get(ctx, &data)...)
+ if resp.Diagnostics.HasError() {
+ return
+ }
+
+ resp.Diagnostics.Append(resp.State.Set(ctx, &data)...)
+}
+
+func (r *ServerPasswordResetResource) Delete(_ context.Context, _ resource.DeleteRequest, _ *resource.DeleteResponse) {
+ // No-op: password resets are not reversible. Removing from state only.
+}
+
+// ValidateConfig validates the resource configuration.
+func (r *ServerPasswordResetResource) ValidateConfig(ctx context.Context, req resource.ValidateConfigRequest, resp *resource.ValidateConfigResponse) {
+ var data ServerPasswordResetResourceModel
+ resp.Diagnostics.Append(req.Config.Get(ctx, &data)...)
+ if resp.Diagnostics.HasError() {
+ return
+ }
+
+ // Validate server_id is positive.
+ if !data.ServerID.IsNull() && !data.ServerID.IsUnknown() && data.ServerID.ValueInt64() <= 0 {
+ resp.Diagnostics.AddAttributeError(
+ path.Root("server_id"),
+ "Invalid Server ID",
+ "server_id must be a positive integer.",
+ )
+ }
+
+ // Validate user is one of the allowed values.
+ if !data.User.IsNull() && !data.User.IsUnknown() {
+ user := data.User.ValueString()
+ if user != "root" && user != "Administrator" {
+ resp.Diagnostics.AddAttributeError(
+ path.Root("user"),
+ "Invalid User",
+ fmt.Sprintf("user must be either \"root\" or \"Administrator\". Got: %q", user),
+ )
+ }
+ }
+}
diff --git a/internal/provider/resource_server_power_action.go b/internal/provider/resource_server_power_action.go
new file mode 100644
index 0000000..ca669ef
--- /dev/null
+++ b/internal/provider/resource_server_power_action.go
@@ -0,0 +1,179 @@
+// Copyright (c) EZSCALE.
+// SPDX-License-Identifier: MPL-2.0
+
+package provider
+
+import (
+ "context"
+ "fmt"
+ "time"
+
+ "terraform-provider-virtfusion/internal/client"
+
+ "github.com/hashicorp/terraform-plugin-framework/path"
+ "github.com/hashicorp/terraform-plugin-framework/resource"
+ "github.com/hashicorp/terraform-plugin-framework/resource/schema"
+ "github.com/hashicorp/terraform-plugin-framework/resource/schema/mapplanmodifier"
+ "github.com/hashicorp/terraform-plugin-framework/resource/schema/planmodifier"
+ "github.com/hashicorp/terraform-plugin-framework/resource/schema/stringplanmodifier"
+ "github.com/hashicorp/terraform-plugin-framework/types"
+)
+
+// Ensure provider-defined types fully satisfy framework interfaces.
+var (
+ _ resource.Resource = &ServerPowerActionResource{}
+ _ resource.ResourceWithConfigure = &ServerPowerActionResource{}
+)
+
+// NewServerPowerActionResource creates a new server power action resource.
+func NewServerPowerActionResource() resource.Resource {
+ return &ServerPowerActionResource{}
+}
+
+// ServerPowerActionResource defines the resource implementation.
+type ServerPowerActionResource struct {
+ client *client.Client
+}
+
+// ServerPowerActionResourceModel describes the resource data model.
+type ServerPowerActionResourceModel struct {
+ ID types.String `tfsdk:"id"`
+ ServerID types.Int64 `tfsdk:"server_id"`
+ Action types.String `tfsdk:"action"`
+ Triggers types.Map `tfsdk:"triggers"`
+}
+
+func (r *ServerPowerActionResource) Metadata(_ context.Context, req resource.MetadataRequest, resp *resource.MetadataResponse) {
+ resp.TypeName = req.ProviderTypeName + "_server_power_action"
+}
+
+func (r *ServerPowerActionResource) Schema(_ context.Context, _ resource.SchemaRequest, resp *resource.SchemaResponse) {
+ resp.Schema = schema.Schema{
+ MarkdownDescription: "Performs a power action on a VirtFusion server. This is a trigger-style resource — the action is executed on create and can be re-triggered by changing the `triggers` attribute.",
+ Attributes: map[string]schema.Attribute{
+ "id": schema.StringAttribute{
+ MarkdownDescription: "The identifier for this power action.",
+ Computed: true,
+ PlanModifiers: []planmodifier.String{
+ stringplanmodifier.UseStateForUnknown(),
+ },
+ },
+ "server_id": schema.Int64Attribute{
+ MarkdownDescription: "The ID of the server to perform the power action on.",
+ Required: true,
+ },
+ "action": schema.StringAttribute{
+ MarkdownDescription: "The power action to perform. Must be one of: `boot`, `shutdown`, `restart`, `poweroff`.",
+ Required: true,
+ },
+ "triggers": schema.MapAttribute{
+ MarkdownDescription: "A map of arbitrary strings that, when changed, will cause the power action to be re-executed. Works like `triggers` in `terraform_data`.",
+ ElementType: types.StringType,
+ Optional: true,
+ PlanModifiers: []planmodifier.Map{
+ mapplanmodifier.RequiresReplace(),
+ },
+ },
+ },
+ }
+}
+
+func (r *ServerPowerActionResource) Configure(_ context.Context, req resource.ConfigureRequest, resp *resource.ConfigureResponse) {
+ if req.ProviderData == nil {
+ return
+ }
+
+ c, ok := req.ProviderData.(*client.Client)
+ if !ok {
+ resp.Diagnostics.AddError(
+ "Unexpected Resource Configure Type",
+ fmt.Sprintf("Expected *client.Client, got: %T. Please report this issue to the provider developers.", req.ProviderData),
+ )
+ return
+ }
+
+ r.client = c
+}
+
+func (r *ServerPowerActionResource) Create(ctx context.Context, req resource.CreateRequest, resp *resource.CreateResponse) {
+ var data ServerPowerActionResourceModel
+ resp.Diagnostics.Append(req.Plan.Get(ctx, &data)...)
+ if resp.Diagnostics.HasError() {
+ return
+ }
+
+ apiPath := fmt.Sprintf("/servers/%d/power/%s", data.ServerID.ValueInt64(), data.Action.ValueString())
+ _, err := r.client.Post(ctx, apiPath, nil)
+ if err != nil {
+ resp.Diagnostics.AddError(
+ "Error Performing Server Power Action",
+ fmt.Sprintf("Could not perform power action %q on server %d: %s", data.Action.ValueString(), data.ServerID.ValueInt64(), err),
+ )
+ return
+ }
+
+ data.ID = types.StringValue(fmt.Sprintf("%d-%s-%d", data.ServerID.ValueInt64(), data.Action.ValueString(), time.Now().UnixNano()))
+
+ resp.Diagnostics.Append(resp.State.Set(ctx, &data)...)
+}
+
+func (r *ServerPowerActionResource) Read(ctx context.Context, req resource.ReadRequest, resp *resource.ReadResponse) {
+ var data ServerPowerActionResourceModel
+ resp.Diagnostics.Append(req.State.Get(ctx, &data)...)
+ if resp.Diagnostics.HasError() {
+ return
+ }
+
+ // Return stored state as-is for trigger-style resources.
+ resp.Diagnostics.Append(resp.State.Set(ctx, &data)...)
+}
+
+func (r *ServerPowerActionResource) Update(ctx context.Context, req resource.UpdateRequest, resp *resource.UpdateResponse) {
+ var data ServerPowerActionResourceModel
+ resp.Diagnostics.Append(req.Plan.Get(ctx, &data)...)
+ if resp.Diagnostics.HasError() {
+ return
+ }
+
+ resp.Diagnostics.Append(resp.State.Set(ctx, &data)...)
+}
+
+func (r *ServerPowerActionResource) Delete(_ context.Context, _ resource.DeleteRequest, _ *resource.DeleteResponse) {
+ // No-op: power actions are not reversible. Removing from state only.
+}
+
+// ValidateConfig validates the resource configuration.
+func (r *ServerPowerActionResource) ValidateConfig(ctx context.Context, req resource.ValidateConfigRequest, resp *resource.ValidateConfigResponse) {
+ var data ServerPowerActionResourceModel
+ resp.Diagnostics.Append(req.Config.Get(ctx, &data)...)
+ if resp.Diagnostics.HasError() {
+ return
+ }
+
+ // Validate server_id is positive.
+ if !data.ServerID.IsNull() && !data.ServerID.IsUnknown() && data.ServerID.ValueInt64() <= 0 {
+ resp.Diagnostics.AddAttributeError(
+ path.Root("server_id"),
+ "Invalid Server ID",
+ "server_id must be a positive integer.",
+ )
+ }
+
+ // Validate action is one of the allowed values.
+ if !data.Action.IsNull() && !data.Action.IsUnknown() {
+ action := data.Action.ValueString()
+ validActions := map[string]bool{
+ "boot": true,
+ "shutdown": true,
+ "restart": true,
+ "poweroff": true,
+ }
+ if !validActions[action] {
+ resp.Diagnostics.AddAttributeError(
+ path.Root("action"),
+ "Invalid Power Action",
+ fmt.Sprintf("action must be one of: boot, shutdown, restart, poweroff. Got: %q", action),
+ )
+ }
+ }
+}
diff --git a/internal/provider/resource_server_traffic_block.go b/internal/provider/resource_server_traffic_block.go
new file mode 100644
index 0000000..38eae69
--- /dev/null
+++ b/internal/provider/resource_server_traffic_block.go
@@ -0,0 +1,163 @@
+// Copyright (c) EZSCALE.
+// SPDX-License-Identifier: MPL-2.0
+
+package provider
+
+import (
+ "context"
+ "encoding/json"
+ "errors"
+ "fmt"
+
+ "terraform-provider-virtfusion/internal/client"
+
+ "github.com/hashicorp/terraform-plugin-framework/resource"
+ "github.com/hashicorp/terraform-plugin-framework/resource/schema"
+ "github.com/hashicorp/terraform-plugin-framework/resource/schema/int64planmodifier"
+ "github.com/hashicorp/terraform-plugin-framework/resource/schema/planmodifier"
+ "github.com/hashicorp/terraform-plugin-framework/resource/schema/stringplanmodifier"
+ "github.com/hashicorp/terraform-plugin-framework/types"
+)
+
+// Ensure provider-defined types fully satisfy framework interfaces.
+var (
+ _ resource.Resource = &ServerTrafficBlockResource{}
+ _ resource.ResourceWithConfigure = &ServerTrafficBlockResource{}
+)
+
+// NewServerTrafficBlockResource creates a new server traffic block resource.
+func NewServerTrafficBlockResource() resource.Resource {
+ return &ServerTrafficBlockResource{}
+}
+
+// ServerTrafficBlockResource defines the resource implementation.
+type ServerTrafficBlockResource struct {
+ client *client.Client
+}
+
+// ServerTrafficBlockResourceModel describes the resource data model.
+type ServerTrafficBlockResourceModel struct {
+ ID types.Int64 `tfsdk:"id"`
+ ServerID types.Int64 `tfsdk:"server_id"`
+ Type types.String `tfsdk:"type"`
+}
+
+func (r *ServerTrafficBlockResource) Metadata(_ context.Context, req resource.MetadataRequest, resp *resource.MetadataResponse) {
+ resp.TypeName = req.ProviderTypeName + "_server_traffic_block"
+}
+
+func (r *ServerTrafficBlockResource) Schema(_ context.Context, _ resource.SchemaRequest, resp *resource.SchemaResponse) {
+ resp.Schema = schema.Schema{
+ MarkdownDescription: "Manages a traffic block on a VirtFusion server.",
+ Attributes: map[string]schema.Attribute{
+ "id": schema.Int64Attribute{
+ MarkdownDescription: "The identifier of the traffic block.",
+ Computed: true,
+ PlanModifiers: []planmodifier.Int64{
+ int64planmodifier.UseStateForUnknown(),
+ },
+ },
+ "server_id": schema.Int64Attribute{
+ MarkdownDescription: "The ID of the server to add the traffic block to.",
+ Required: true,
+ PlanModifiers: []planmodifier.Int64{
+ int64planmodifier.RequiresReplace(),
+ },
+ },
+ "type": schema.StringAttribute{
+ MarkdownDescription: "The type of traffic block (e.g. `inbound` or `outbound`).",
+ Required: true,
+ PlanModifiers: []planmodifier.String{
+ stringplanmodifier.RequiresReplace(),
+ },
+ },
+ },
+ }
+}
+
+func (r *ServerTrafficBlockResource) Configure(_ context.Context, req resource.ConfigureRequest, resp *resource.ConfigureResponse) {
+ if req.ProviderData == nil {
+ return
+ }
+
+ c, ok := req.ProviderData.(*client.Client)
+ if !ok {
+ resp.Diagnostics.AddError(
+ "Unexpected Resource Configure Type",
+ fmt.Sprintf("Expected *client.Client, got: %T. Please report this issue to the provider developers.", req.ProviderData),
+ )
+ return
+ }
+
+ r.client = c
+}
+
+func (r *ServerTrafficBlockResource) Create(ctx context.Context, req resource.CreateRequest, resp *resource.CreateResponse) {
+ var data ServerTrafficBlockResourceModel
+ resp.Diagnostics.Append(req.Plan.Get(ctx, &data)...)
+ if resp.Diagnostics.HasError() {
+ return
+ }
+
+ blockReq := client.TrafficBlockRequest{
+ Type: data.Type.ValueString(),
+ }
+
+ apiPath := fmt.Sprintf("/servers/%d/traffic/blocks", data.ServerID.ValueInt64())
+ respBody, err := r.client.Post(ctx, apiPath, blockReq)
+ if err != nil {
+ resp.Diagnostics.AddError(
+ "Error Creating Traffic Block",
+ fmt.Sprintf("Could not create traffic block on server %d: %s", data.ServerID.ValueInt64(), err),
+ )
+ return
+ }
+
+ var blockResp client.TrafficBlockResponse
+ if err := json.Unmarshal(respBody, &blockResp); err != nil {
+ resp.Diagnostics.AddError(
+ "Error Parsing Response",
+ fmt.Sprintf("Could not parse traffic block response: %s", err),
+ )
+ return
+ }
+
+ data.ID = types.Int64Value(blockResp.Data.ID)
+
+ resp.Diagnostics.Append(resp.State.Set(ctx, &data)...)
+}
+
+func (r *ServerTrafficBlockResource) Read(ctx context.Context, req resource.ReadRequest, resp *resource.ReadResponse) {
+ var data ServerTrafficBlockResourceModel
+ resp.Diagnostics.Append(req.State.Get(ctx, &data)...)
+ if resp.Diagnostics.HasError() {
+ return
+ }
+ resp.Diagnostics.Append(resp.State.Set(ctx, &data)...)
+}
+
+func (r *ServerTrafficBlockResource) Update(_ context.Context, _ resource.UpdateRequest, _ *resource.UpdateResponse) {
+ // All attributes require replacement — updates are never called.
+}
+
+func (r *ServerTrafficBlockResource) Delete(ctx context.Context, req resource.DeleteRequest, resp *resource.DeleteResponse) {
+ var data ServerTrafficBlockResourceModel
+ resp.Diagnostics.Append(req.State.Get(ctx, &data)...)
+ if resp.Diagnostics.HasError() {
+ return
+ }
+
+ apiPath := fmt.Sprintf("/servers/%d/traffic/blocks/%d", data.ServerID.ValueInt64(), data.ID.ValueInt64())
+ _, err := r.client.Delete(ctx, apiPath)
+ if err != nil {
+ var apiErr *client.APIError
+ if errors.As(err, &apiErr) && apiErr.IsNotFound() {
+ return
+ }
+ resp.Diagnostics.AddError(
+ "Error Deleting Traffic Block",
+ fmt.Sprintf("Could not delete traffic block %d on server %d: %s", data.ID.ValueInt64(), data.ServerID.ValueInt64(), err),
+ )
+ return
+ }
+}
diff --git a/internal/provider/resource_ssh_key.go b/internal/provider/resource_ssh_key.go
new file mode 100644
index 0000000..30f82a0
--- /dev/null
+++ b/internal/provider/resource_ssh_key.go
@@ -0,0 +1,226 @@
+// Copyright (c) EZSCALE.
+// SPDX-License-Identifier: MPL-2.0
+
+package provider
+
+import (
+ "context"
+ "encoding/json"
+ "errors"
+ "fmt"
+ "strconv"
+
+ "terraform-provider-virtfusion/internal/client"
+
+ "github.com/hashicorp/terraform-plugin-framework/path"
+ "github.com/hashicorp/terraform-plugin-framework/resource"
+ "github.com/hashicorp/terraform-plugin-framework/resource/schema"
+ "github.com/hashicorp/terraform-plugin-framework/resource/schema/int64planmodifier"
+ "github.com/hashicorp/terraform-plugin-framework/resource/schema/planmodifier"
+ "github.com/hashicorp/terraform-plugin-framework/resource/schema/stringplanmodifier"
+ "github.com/hashicorp/terraform-plugin-framework/types"
+)
+
+// Ensure provider-defined types fully satisfy framework interfaces.
+var (
+ _ resource.Resource = &SSHKeyResource{}
+ _ resource.ResourceWithConfigure = &SSHKeyResource{}
+ _ resource.ResourceWithImportState = &SSHKeyResource{}
+)
+
+// NewSSHKeyResource creates a new SSH key resource.
+func NewSSHKeyResource() resource.Resource {
+ return &SSHKeyResource{}
+}
+
+// SSHKeyResource defines the resource implementation.
+type SSHKeyResource struct {
+ client *client.Client
+}
+
+// SSHKeyResourceModel describes the resource data model.
+type SSHKeyResourceModel struct {
+ ID types.Int64 `tfsdk:"id"`
+ UserID types.Int64 `tfsdk:"user_id"`
+ Name types.String `tfsdk:"name"`
+ PublicKey types.String `tfsdk:"public_key"`
+}
+
+func (r *SSHKeyResource) Metadata(_ context.Context, req resource.MetadataRequest, resp *resource.MetadataResponse) {
+ resp.TypeName = req.ProviderTypeName + "_ssh_key"
+}
+
+func (r *SSHKeyResource) Schema(_ context.Context, _ resource.SchemaRequest, resp *resource.SchemaResponse) {
+ resp.Schema = schema.Schema{
+ MarkdownDescription: "Manages a VirtFusion SSH key.",
+ Attributes: map[string]schema.Attribute{
+ "id": schema.Int64Attribute{
+ MarkdownDescription: "The ID of the SSH key.",
+ Computed: true,
+ PlanModifiers: []planmodifier.Int64{
+ int64planmodifier.UseStateForUnknown(),
+ },
+ },
+ "user_id": schema.Int64Attribute{
+ MarkdownDescription: "The ID of the user who owns this SSH key.",
+ Required: true,
+ PlanModifiers: []planmodifier.Int64{
+ int64planmodifier.RequiresReplace(),
+ },
+ },
+ "name": schema.StringAttribute{
+ MarkdownDescription: "The name of the SSH key.",
+ Required: true,
+ PlanModifiers: []planmodifier.String{
+ stringplanmodifier.RequiresReplace(),
+ },
+ },
+ "public_key": schema.StringAttribute{
+ MarkdownDescription: "The public key content.",
+ Required: true,
+ PlanModifiers: []planmodifier.String{
+ stringplanmodifier.RequiresReplace(),
+ },
+ },
+ },
+ }
+}
+
+func (r *SSHKeyResource) Configure(_ context.Context, req resource.ConfigureRequest, resp *resource.ConfigureResponse) {
+ if req.ProviderData == nil {
+ return
+ }
+
+ c, ok := req.ProviderData.(*client.Client)
+ if !ok {
+ resp.Diagnostics.AddError(
+ "Unexpected Resource Configure Type",
+ fmt.Sprintf("Expected *client.Client, got: %T. Please report this issue to the provider developers.", req.ProviderData),
+ )
+ return
+ }
+
+ r.client = c
+}
+
+func (r *SSHKeyResource) Create(ctx context.Context, req resource.CreateRequest, resp *resource.CreateResponse) {
+ var data SSHKeyResourceModel
+ resp.Diagnostics.Append(req.Plan.Get(ctx, &data)...)
+ if resp.Diagnostics.HasError() {
+ return
+ }
+
+ createReq := client.SSHKeyCreateRequest{
+ UserID: data.UserID.ValueInt64(),
+ Name: data.Name.ValueString(),
+ PublicKey: data.PublicKey.ValueString(),
+ }
+
+ respBody, err := r.client.Post(ctx, "/ssh_keys", createReq)
+ if err != nil {
+ resp.Diagnostics.AddError(
+ "Error Creating SSH Key",
+ fmt.Sprintf("Could not create SSH key: %s", err),
+ )
+ return
+ }
+
+ var sshKeyResp client.SSHKeyResponse
+ if err := json.Unmarshal(respBody, &sshKeyResp); err != nil {
+ resp.Diagnostics.AddError(
+ "Error Parsing Response",
+ fmt.Sprintf("Could not parse SSH key response: %s", err),
+ )
+ return
+ }
+
+ data.ID = types.Int64Value(sshKeyResp.Data.ID)
+
+ resp.Diagnostics.Append(resp.State.Set(ctx, &data)...)
+}
+
+func (r *SSHKeyResource) Read(ctx context.Context, req resource.ReadRequest, resp *resource.ReadResponse) {
+ var data SSHKeyResourceModel
+ resp.Diagnostics.Append(req.State.Get(ctx, &data)...)
+ if resp.Diagnostics.HasError() {
+ return
+ }
+
+ apiPath := fmt.Sprintf("/ssh_keys/%d", data.ID.ValueInt64())
+ respBody, err := r.client.Get(ctx, apiPath)
+ if err != nil {
+ var apiErr *client.APIError
+ if errors.As(err, &apiErr) && apiErr.IsNotFound() {
+ // SSH key no longer exists, remove from state.
+ resp.State.RemoveResource(ctx)
+ return
+ }
+
+ resp.Diagnostics.AddError(
+ "Error Reading SSH Key",
+ fmt.Sprintf("Could not read SSH key %d: %s", data.ID.ValueInt64(), err),
+ )
+ return
+ }
+
+ var sshKeyResp client.SSHKeyResponse
+ if err := json.Unmarshal(respBody, &sshKeyResp); err != nil {
+ resp.Diagnostics.AddError(
+ "Error Parsing Response",
+ fmt.Sprintf("Could not parse SSH key response: %s", err),
+ )
+ return
+ }
+
+ // Update state from API response.
+ data.Name = types.StringValue(sshKeyResp.Data.Name)
+
+ resp.Diagnostics.Append(resp.State.Set(ctx, &data)...)
+}
+
+func (r *SSHKeyResource) Update(ctx context.Context, req resource.UpdateRequest, resp *resource.UpdateResponse) {
+ var data SSHKeyResourceModel
+ resp.Diagnostics.Append(req.Plan.Get(ctx, &data)...)
+ if resp.Diagnostics.HasError() {
+ return
+ }
+
+ resp.Diagnostics.Append(resp.State.Set(ctx, &data)...)
+}
+
+func (r *SSHKeyResource) Delete(ctx context.Context, req resource.DeleteRequest, resp *resource.DeleteResponse) {
+ var data SSHKeyResourceModel
+ resp.Diagnostics.Append(req.State.Get(ctx, &data)...)
+ if resp.Diagnostics.HasError() {
+ return
+ }
+
+ apiPath := fmt.Sprintf("/ssh_keys/%d", data.ID.ValueInt64())
+ _, err := r.client.Delete(ctx, apiPath)
+ if err != nil {
+ var apiErr *client.APIError
+ if errors.As(err, &apiErr) && apiErr.IsNotFound() {
+ // Already deleted, nothing to do.
+ return
+ }
+
+ resp.Diagnostics.AddError(
+ "Error Deleting SSH Key",
+ fmt.Sprintf("Could not delete SSH key %d: %s", data.ID.ValueInt64(), err),
+ )
+ return
+ }
+}
+
+func (r *SSHKeyResource) ImportState(ctx context.Context, req resource.ImportStateRequest, resp *resource.ImportStateResponse) {
+ id, err := strconv.ParseInt(req.ID, 10, 64)
+ if err != nil {
+ resp.Diagnostics.AddError(
+ "Invalid Import ID",
+ fmt.Sprintf("Could not parse SSH key ID %q as integer: %s", req.ID, err),
+ )
+ return
+ }
+
+ resp.Diagnostics.Append(resp.State.SetAttribute(ctx, path.Root("id"), types.Int64Value(id))...)
+}
diff --git a/internal/provider/resource_user.go b/internal/provider/resource_user.go
new file mode 100644
index 0000000..52f667f
--- /dev/null
+++ b/internal/provider/resource_user.go
@@ -0,0 +1,218 @@
+// Copyright (c) EZSCALE.
+// SPDX-License-Identifier: MPL-2.0
+
+package provider
+
+import (
+ "context"
+ "encoding/json"
+ "errors"
+ "fmt"
+
+ "terraform-provider-virtfusion/internal/client"
+
+ "github.com/hashicorp/terraform-plugin-framework/path"
+ "github.com/hashicorp/terraform-plugin-framework/resource"
+ "github.com/hashicorp/terraform-plugin-framework/resource/schema"
+ "github.com/hashicorp/terraform-plugin-framework/resource/schema/planmodifier"
+ "github.com/hashicorp/terraform-plugin-framework/resource/schema/stringplanmodifier"
+ "github.com/hashicorp/terraform-plugin-framework/types"
+)
+
+var (
+ _ resource.Resource = &UserResource{}
+ _ resource.ResourceWithConfigure = &UserResource{}
+ _ resource.ResourceWithImportState = &UserResource{}
+)
+
+// NewUserResource returns a new resource for managing VirtFusion users.
+func NewUserResource() resource.Resource {
+ return &UserResource{}
+}
+
+// UserResource defines the resource implementation.
+type UserResource struct {
+ client *client.Client
+}
+
+// UserResourceModel describes the resource data model.
+type UserResourceModel struct {
+ ID types.Int64 `tfsdk:"id"`
+ Name types.String `tfsdk:"name"`
+ Email types.String `tfsdk:"email"`
+ ExtRelationID types.String `tfsdk:"ext_relation_id"`
+ Enabled types.Bool `tfsdk:"enabled"`
+}
+
+func (r *UserResource) Metadata(_ context.Context, req resource.MetadataRequest, resp *resource.MetadataResponse) {
+ resp.TypeName = req.ProviderTypeName + "_user"
+}
+
+func (r *UserResource) Schema(_ context.Context, _ resource.SchemaRequest, resp *resource.SchemaResponse) {
+ resp.Schema = schema.Schema{
+ MarkdownDescription: "Manages a VirtFusion user.",
+ Attributes: map[string]schema.Attribute{
+ "id": schema.Int64Attribute{
+ MarkdownDescription: "The numeric ID of the user.",
+ Computed: true,
+ },
+ "name": schema.StringAttribute{
+ MarkdownDescription: "The name of the user.",
+ Required: true,
+ },
+ "email": schema.StringAttribute{
+ MarkdownDescription: "The email address of the user.",
+ Required: true,
+ },
+ "ext_relation_id": schema.StringAttribute{
+ MarkdownDescription: "The external relation ID used to look up the user. Changing this forces a new resource to be created.",
+ Required: true,
+ PlanModifiers: []planmodifier.String{
+ stringplanmodifier.RequiresReplace(),
+ },
+ },
+ "enabled": schema.BoolAttribute{
+ MarkdownDescription: "Whether the user is enabled.",
+ Computed: true,
+ },
+ },
+ }
+}
+
+func (r *UserResource) Configure(_ context.Context, req resource.ConfigureRequest, resp *resource.ConfigureResponse) {
+ if req.ProviderData == nil {
+ return
+ }
+
+ c, ok := req.ProviderData.(*client.Client)
+ if !ok {
+ resp.Diagnostics.AddError(
+ "Unexpected Resource Configure Type",
+ fmt.Sprintf("Expected *client.Client, got: %T.", req.ProviderData),
+ )
+ return
+ }
+
+ r.client = c
+}
+
+func (r *UserResource) Create(ctx context.Context, req resource.CreateRequest, resp *resource.CreateResponse) {
+ var data UserResourceModel
+ resp.Diagnostics.Append(req.Plan.Get(ctx, &data)...)
+ if resp.Diagnostics.HasError() {
+ return
+ }
+
+ body := client.UserCreateRequest{
+ Name: data.Name.ValueString(),
+ Email: data.Email.ValueString(),
+ ExtRelationID: data.ExtRelationID.ValueString(),
+ }
+
+ result, err := r.client.Post(ctx, "/users", body)
+ if err != nil {
+ resp.Diagnostics.AddError("Error creating user", err.Error())
+ return
+ }
+
+ var userResp client.UserResponse
+ if err := json.Unmarshal(result, &userResp); err != nil {
+ resp.Diagnostics.AddError("Error parsing user response", err.Error())
+ return
+ }
+
+ data.ID = types.Int64Value(userResp.Data.ID)
+ data.Name = types.StringValue(userResp.Data.Name)
+ data.Email = types.StringValue(userResp.Data.Email)
+ data.ExtRelationID = types.StringValue(userResp.Data.ExtRelationID)
+ data.Enabled = types.BoolValue(userResp.Data.Enabled)
+
+ resp.Diagnostics.Append(resp.State.Set(ctx, &data)...)
+}
+
+func (r *UserResource) Read(ctx context.Context, req resource.ReadRequest, resp *resource.ReadResponse) {
+ var data UserResourceModel
+ resp.Diagnostics.Append(req.State.Get(ctx, &data)...)
+ if resp.Diagnostics.HasError() {
+ return
+ }
+
+ result, err := r.client.Get(ctx, fmt.Sprintf("/users/%s/byExtRelation", data.ExtRelationID.ValueString()))
+ if err != nil {
+ var apiErr *client.APIError
+ if errors.As(err, &apiErr) && apiErr.IsNotFound() {
+ resp.State.RemoveResource(ctx)
+ return
+ }
+ resp.Diagnostics.AddError("Error reading user", err.Error())
+ return
+ }
+
+ var userResp client.UserResponse
+ if err := json.Unmarshal(result, &userResp); err != nil {
+ resp.Diagnostics.AddError("Error parsing user response", err.Error())
+ return
+ }
+
+ data.ID = types.Int64Value(userResp.Data.ID)
+ data.Name = types.StringValue(userResp.Data.Name)
+ data.Email = types.StringValue(userResp.Data.Email)
+ data.ExtRelationID = types.StringValue(userResp.Data.ExtRelationID)
+ data.Enabled = types.BoolValue(userResp.Data.Enabled)
+
+ resp.Diagnostics.Append(resp.State.Set(ctx, &data)...)
+}
+
+func (r *UserResource) Update(ctx context.Context, req resource.UpdateRequest, resp *resource.UpdateResponse) {
+ var data UserResourceModel
+ resp.Diagnostics.Append(req.Plan.Get(ctx, &data)...)
+ if resp.Diagnostics.HasError() {
+ return
+ }
+
+ body := client.UserModifyRequest{
+ Name: data.Name.ValueString(),
+ Email: data.Email.ValueString(),
+ }
+
+ result, err := r.client.Put(ctx, fmt.Sprintf("/users/%s/byExtRelation", data.ExtRelationID.ValueString()), body)
+ if err != nil {
+ resp.Diagnostics.AddError("Error updating user", err.Error())
+ return
+ }
+
+ var userResp client.UserResponse
+ if err := json.Unmarshal(result, &userResp); err != nil {
+ resp.Diagnostics.AddError("Error parsing user response", err.Error())
+ return
+ }
+
+ data.ID = types.Int64Value(userResp.Data.ID)
+ data.Name = types.StringValue(userResp.Data.Name)
+ data.Email = types.StringValue(userResp.Data.Email)
+ data.ExtRelationID = types.StringValue(userResp.Data.ExtRelationID)
+ data.Enabled = types.BoolValue(userResp.Data.Enabled)
+
+ resp.Diagnostics.Append(resp.State.Set(ctx, &data)...)
+}
+
+func (r *UserResource) Delete(ctx context.Context, req resource.DeleteRequest, resp *resource.DeleteResponse) {
+ var data UserResourceModel
+ resp.Diagnostics.Append(req.State.Get(ctx, &data)...)
+ if resp.Diagnostics.HasError() {
+ return
+ }
+
+ _, err := r.client.Delete(ctx, fmt.Sprintf("/users/%s/byExtRelation", data.ExtRelationID.ValueString()))
+ if err != nil {
+ var apiErr *client.APIError
+ if errors.As(err, &apiErr) && apiErr.IsNotFound() {
+ return
+ }
+ resp.Diagnostics.AddError("Error deleting user", err.Error())
+ }
+}
+
+func (r *UserResource) ImportState(ctx context.Context, req resource.ImportStateRequest, resp *resource.ImportStateResponse) {
+ resource.ImportStatePassthroughID(ctx, path.Root("ext_relation_id"), req, resp)
+}
diff --git a/internal/provider/resource_user_auth_token.go b/internal/provider/resource_user_auth_token.go
new file mode 100644
index 0000000..9cbbfc9
--- /dev/null
+++ b/internal/provider/resource_user_auth_token.go
@@ -0,0 +1,170 @@
+// Copyright (c) EZSCALE.
+// SPDX-License-Identifier: MPL-2.0
+
+package provider
+
+import (
+ "context"
+ "encoding/json"
+ "fmt"
+ "time"
+
+ "terraform-provider-virtfusion/internal/client"
+
+ "github.com/hashicorp/terraform-plugin-framework/resource"
+ "github.com/hashicorp/terraform-plugin-framework/resource/schema"
+ "github.com/hashicorp/terraform-plugin-framework/resource/schema/mapplanmodifier"
+ "github.com/hashicorp/terraform-plugin-framework/resource/schema/planmodifier"
+ "github.com/hashicorp/terraform-plugin-framework/resource/schema/stringplanmodifier"
+ "github.com/hashicorp/terraform-plugin-framework/types"
+)
+
+// Ensure provider-defined types fully satisfy framework interfaces.
+var (
+ _ resource.Resource = &UserAuthTokenResource{}
+ _ resource.ResourceWithConfigure = &UserAuthTokenResource{}
+)
+
+// NewUserAuthTokenResource creates a new user auth token resource.
+func NewUserAuthTokenResource() resource.Resource {
+ return &UserAuthTokenResource{}
+}
+
+// UserAuthTokenResource defines the resource implementation.
+type UserAuthTokenResource struct {
+ client *client.Client
+}
+
+// UserAuthTokenResourceModel describes the resource data model.
+type UserAuthTokenResourceModel struct {
+ ID types.String `tfsdk:"id"`
+ ExtRelationID types.String `tfsdk:"ext_relation_id"`
+ Token types.String `tfsdk:"token"`
+ URL types.String `tfsdk:"url"`
+ Triggers types.Map `tfsdk:"triggers"`
+}
+
+func (r *UserAuthTokenResource) Metadata(_ context.Context, req resource.MetadataRequest, resp *resource.MetadataResponse) {
+ resp.TypeName = req.ProviderTypeName + "_user_auth_token"
+}
+
+func (r *UserAuthTokenResource) Schema(_ context.Context, _ resource.SchemaRequest, resp *resource.SchemaResponse) {
+ resp.Schema = schema.Schema{
+ MarkdownDescription: "Generates an authentication token for a VirtFusion user. This is a trigger-style resource — the token is generated on create and can be re-generated by changing the `triggers` attribute.",
+ Attributes: map[string]schema.Attribute{
+ "id": schema.StringAttribute{
+ MarkdownDescription: "The identifier for this auth token generation.",
+ Computed: true,
+ PlanModifiers: []planmodifier.String{
+ stringplanmodifier.UseStateForUnknown(),
+ },
+ },
+ "ext_relation_id": schema.StringAttribute{
+ MarkdownDescription: "The external relation ID of the user to generate the auth token for.",
+ Required: true,
+ },
+ "token": schema.StringAttribute{
+ MarkdownDescription: "The generated authentication token.",
+ Computed: true,
+ Sensitive: true,
+ PlanModifiers: []planmodifier.String{
+ stringplanmodifier.UseStateForUnknown(),
+ },
+ },
+ "url": schema.StringAttribute{
+ MarkdownDescription: "The authentication URL for the generated token.",
+ Computed: true,
+ PlanModifiers: []planmodifier.String{
+ stringplanmodifier.UseStateForUnknown(),
+ },
+ },
+ "triggers": schema.MapAttribute{
+ MarkdownDescription: "A map of arbitrary strings that, when changed, will cause the auth token to be re-generated. Works like `triggers` in `terraform_data`.",
+ ElementType: types.StringType,
+ Optional: true,
+ PlanModifiers: []planmodifier.Map{
+ mapplanmodifier.RequiresReplace(),
+ },
+ },
+ },
+ }
+}
+
+func (r *UserAuthTokenResource) Configure(_ context.Context, req resource.ConfigureRequest, resp *resource.ConfigureResponse) {
+ if req.ProviderData == nil {
+ return
+ }
+
+ c, ok := req.ProviderData.(*client.Client)
+ if !ok {
+ resp.Diagnostics.AddError(
+ "Unexpected Resource Configure Type",
+ fmt.Sprintf("Expected *client.Client, got: %T. Please report this issue to the provider developers.", req.ProviderData),
+ )
+ return
+ }
+
+ r.client = c
+}
+
+func (r *UserAuthTokenResource) Create(ctx context.Context, req resource.CreateRequest, resp *resource.CreateResponse) {
+ var data UserAuthTokenResourceModel
+ resp.Diagnostics.Append(req.Plan.Get(ctx, &data)...)
+ if resp.Diagnostics.HasError() {
+ return
+ }
+
+ apiPath := fmt.Sprintf("/users/%s/authenticationTokens", data.ExtRelationID.ValueString())
+ rawResp, err := r.client.Post(ctx, apiPath, nil)
+ if err != nil {
+ resp.Diagnostics.AddError(
+ "Error Generating User Auth Token",
+ fmt.Sprintf("Could not generate auth token for user with ext_relation_id %q: %s", data.ExtRelationID.ValueString(), err),
+ )
+ return
+ }
+
+ // Parse the response for the token and URL.
+ if rawResp != nil {
+ var tokenResp client.AuthTokenResponse
+ if jsonErr := json.Unmarshal(rawResp, &tokenResp); jsonErr == nil {
+ data.Token = types.StringValue(tokenResp.Data.Token)
+ data.URL = types.StringValue(tokenResp.Data.URL)
+ } else {
+ data.Token = types.StringValue("")
+ data.URL = types.StringValue("")
+ }
+ } else {
+ data.Token = types.StringValue("")
+ data.URL = types.StringValue("")
+ }
+
+ data.ID = types.StringValue(fmt.Sprintf("%s-%d", data.ExtRelationID.ValueString(), time.Now().UnixNano()))
+
+ resp.Diagnostics.Append(resp.State.Set(ctx, &data)...)
+}
+
+func (r *UserAuthTokenResource) Read(ctx context.Context, req resource.ReadRequest, resp *resource.ReadResponse) {
+ var data UserAuthTokenResourceModel
+ resp.Diagnostics.Append(req.State.Get(ctx, &data)...)
+ if resp.Diagnostics.HasError() {
+ return
+ }
+
+ // Return stored state as-is for trigger-style resources.
+ resp.Diagnostics.Append(resp.State.Set(ctx, &data)...)
+}
+
+func (r *UserAuthTokenResource) Update(ctx context.Context, req resource.UpdateRequest, resp *resource.UpdateResponse) {
+ var data UserAuthTokenResourceModel
+ resp.Diagnostics.Append(req.Plan.Get(ctx, &data)...)
+ if resp.Diagnostics.HasError() {
+ return
+ }
+
+ resp.Diagnostics.Append(resp.State.Set(ctx, &data)...)
+}
+
+func (r *UserAuthTokenResource) Delete(_ context.Context, _ resource.DeleteRequest, _ *resource.DeleteResponse) {
+ // No-op: auth tokens cannot be revoked via this resource. Removing from state only.
+}
diff --git a/internal/provider/resource_user_password_reset.go b/internal/provider/resource_user_password_reset.go
new file mode 100644
index 0000000..93c3a2e
--- /dev/null
+++ b/internal/provider/resource_user_password_reset.go
@@ -0,0 +1,137 @@
+// Copyright (c) EZSCALE.
+// SPDX-License-Identifier: MPL-2.0
+
+package provider
+
+import (
+ "context"
+ "fmt"
+ "time"
+
+ "terraform-provider-virtfusion/internal/client"
+
+ "github.com/hashicorp/terraform-plugin-framework/resource"
+ "github.com/hashicorp/terraform-plugin-framework/resource/schema"
+ "github.com/hashicorp/terraform-plugin-framework/resource/schema/mapplanmodifier"
+ "github.com/hashicorp/terraform-plugin-framework/resource/schema/planmodifier"
+ "github.com/hashicorp/terraform-plugin-framework/resource/schema/stringplanmodifier"
+ "github.com/hashicorp/terraform-plugin-framework/types"
+)
+
+// Ensure provider-defined types fully satisfy framework interfaces.
+var (
+ _ resource.Resource = &UserPasswordResetResource{}
+ _ resource.ResourceWithConfigure = &UserPasswordResetResource{}
+)
+
+// NewUserPasswordResetResource creates a new user password reset resource.
+func NewUserPasswordResetResource() resource.Resource {
+ return &UserPasswordResetResource{}
+}
+
+// UserPasswordResetResource defines the resource implementation.
+type UserPasswordResetResource struct {
+ client *client.Client
+}
+
+// UserPasswordResetResourceModel describes the resource data model.
+type UserPasswordResetResourceModel struct {
+ ID types.String `tfsdk:"id"`
+ ExtRelationID types.String `tfsdk:"ext_relation_id"`
+ Triggers types.Map `tfsdk:"triggers"`
+}
+
+func (r *UserPasswordResetResource) Metadata(_ context.Context, req resource.MetadataRequest, resp *resource.MetadataResponse) {
+ resp.TypeName = req.ProviderTypeName + "_user_password_reset"
+}
+
+func (r *UserPasswordResetResource) Schema(_ context.Context, _ resource.SchemaRequest, resp *resource.SchemaResponse) {
+ resp.Schema = schema.Schema{
+ MarkdownDescription: "Resets the password for a VirtFusion user by external relation ID. This is a trigger-style resource — the reset is executed on create and can be re-triggered by changing the `triggers` attribute.",
+ Attributes: map[string]schema.Attribute{
+ "id": schema.StringAttribute{
+ MarkdownDescription: "The identifier for this password reset.",
+ Computed: true,
+ PlanModifiers: []planmodifier.String{
+ stringplanmodifier.UseStateForUnknown(),
+ },
+ },
+ "ext_relation_id": schema.StringAttribute{
+ MarkdownDescription: "The external relation ID of the user to reset the password for.",
+ Required: true,
+ },
+ "triggers": schema.MapAttribute{
+ MarkdownDescription: "A map of arbitrary strings that, when changed, will cause the password reset to be re-executed. Works like `triggers` in `terraform_data`.",
+ ElementType: types.StringType,
+ Optional: true,
+ PlanModifiers: []planmodifier.Map{
+ mapplanmodifier.RequiresReplace(),
+ },
+ },
+ },
+ }
+}
+
+func (r *UserPasswordResetResource) Configure(_ context.Context, req resource.ConfigureRequest, resp *resource.ConfigureResponse) {
+ if req.ProviderData == nil {
+ return
+ }
+
+ c, ok := req.ProviderData.(*client.Client)
+ if !ok {
+ resp.Diagnostics.AddError(
+ "Unexpected Resource Configure Type",
+ fmt.Sprintf("Expected *client.Client, got: %T. Please report this issue to the provider developers.", req.ProviderData),
+ )
+ return
+ }
+
+ r.client = c
+}
+
+func (r *UserPasswordResetResource) Create(ctx context.Context, req resource.CreateRequest, resp *resource.CreateResponse) {
+ var data UserPasswordResetResourceModel
+ resp.Diagnostics.Append(req.Plan.Get(ctx, &data)...)
+ if resp.Diagnostics.HasError() {
+ return
+ }
+
+ apiPath := fmt.Sprintf("/users/%s/byExtRelation/resetPassword", data.ExtRelationID.ValueString())
+ _, err := r.client.Post(ctx, apiPath, nil)
+ if err != nil {
+ resp.Diagnostics.AddError(
+ "Error Resetting User Password",
+ fmt.Sprintf("Could not reset password for user with ext_relation_id %q: %s", data.ExtRelationID.ValueString(), err),
+ )
+ return
+ }
+
+ data.ID = types.StringValue(fmt.Sprintf("%s-%d", data.ExtRelationID.ValueString(), time.Now().UnixNano()))
+
+ resp.Diagnostics.Append(resp.State.Set(ctx, &data)...)
+}
+
+func (r *UserPasswordResetResource) Read(ctx context.Context, req resource.ReadRequest, resp *resource.ReadResponse) {
+ var data UserPasswordResetResourceModel
+ resp.Diagnostics.Append(req.State.Get(ctx, &data)...)
+ if resp.Diagnostics.HasError() {
+ return
+ }
+
+ // Return stored state as-is for trigger-style resources.
+ resp.Diagnostics.Append(resp.State.Set(ctx, &data)...)
+}
+
+func (r *UserPasswordResetResource) Update(ctx context.Context, req resource.UpdateRequest, resp *resource.UpdateResponse) {
+ var data UserPasswordResetResourceModel
+ resp.Diagnostics.Append(req.Plan.Get(ctx, &data)...)
+ if resp.Diagnostics.HasError() {
+ return
+ }
+
+ resp.Diagnostics.Append(resp.State.Set(ctx, &data)...)
+}
+
+func (r *UserPasswordResetResource) Delete(_ context.Context, _ resource.DeleteRequest, _ *resource.DeleteResponse) {
+ // No-op: password resets are not reversible. Removing from state only.
+}
diff --git a/internal/provider/resource_user_server_auth_token.go b/internal/provider/resource_user_server_auth_token.go
new file mode 100644
index 0000000..98cc149
--- /dev/null
+++ b/internal/provider/resource_user_server_auth_token.go
@@ -0,0 +1,194 @@
+// Copyright (c) EZSCALE.
+// SPDX-License-Identifier: MPL-2.0
+
+package provider
+
+import (
+ "context"
+ "encoding/json"
+ "fmt"
+ "time"
+
+ "terraform-provider-virtfusion/internal/client"
+
+ "github.com/hashicorp/terraform-plugin-framework/path"
+ "github.com/hashicorp/terraform-plugin-framework/resource"
+ "github.com/hashicorp/terraform-plugin-framework/resource/schema"
+ "github.com/hashicorp/terraform-plugin-framework/resource/schema/mapplanmodifier"
+ "github.com/hashicorp/terraform-plugin-framework/resource/schema/planmodifier"
+ "github.com/hashicorp/terraform-plugin-framework/resource/schema/stringplanmodifier"
+ "github.com/hashicorp/terraform-plugin-framework/types"
+)
+
+// Ensure provider-defined types fully satisfy framework interfaces.
+var (
+ _ resource.Resource = &UserServerAuthTokenResource{}
+ _ resource.ResourceWithConfigure = &UserServerAuthTokenResource{}
+)
+
+// NewUserServerAuthTokenResource creates a new user server auth token resource.
+func NewUserServerAuthTokenResource() resource.Resource {
+ return &UserServerAuthTokenResource{}
+}
+
+// UserServerAuthTokenResource defines the resource implementation.
+type UserServerAuthTokenResource struct {
+ client *client.Client
+}
+
+// UserServerAuthTokenResourceModel describes the resource data model.
+type UserServerAuthTokenResourceModel struct {
+ ID types.String `tfsdk:"id"`
+ ExtRelationID types.String `tfsdk:"ext_relation_id"`
+ ServerID types.Int64 `tfsdk:"server_id"`
+ Token types.String `tfsdk:"token"`
+ URL types.String `tfsdk:"url"`
+ Triggers types.Map `tfsdk:"triggers"`
+}
+
+func (r *UserServerAuthTokenResource) Metadata(_ context.Context, req resource.MetadataRequest, resp *resource.MetadataResponse) {
+ resp.TypeName = req.ProviderTypeName + "_user_server_auth_token"
+}
+
+func (r *UserServerAuthTokenResource) Schema(_ context.Context, _ resource.SchemaRequest, resp *resource.SchemaResponse) {
+ resp.Schema = schema.Schema{
+ MarkdownDescription: "Generates a server-scoped authentication token for a VirtFusion user. This is a trigger-style resource — the token is generated on create and can be re-generated by changing the `triggers` attribute.",
+ Attributes: map[string]schema.Attribute{
+ "id": schema.StringAttribute{
+ MarkdownDescription: "The identifier for this server auth token generation.",
+ Computed: true,
+ PlanModifiers: []planmodifier.String{
+ stringplanmodifier.UseStateForUnknown(),
+ },
+ },
+ "ext_relation_id": schema.StringAttribute{
+ MarkdownDescription: "The external relation ID of the user to generate the server auth token for.",
+ Required: true,
+ },
+ "server_id": schema.Int64Attribute{
+ MarkdownDescription: "The ID of the server to scope the auth token to.",
+ Required: true,
+ },
+ "token": schema.StringAttribute{
+ MarkdownDescription: "The generated server authentication token.",
+ Computed: true,
+ Sensitive: true,
+ PlanModifiers: []planmodifier.String{
+ stringplanmodifier.UseStateForUnknown(),
+ },
+ },
+ "url": schema.StringAttribute{
+ MarkdownDescription: "The authentication URL for the generated server token.",
+ Computed: true,
+ PlanModifiers: []planmodifier.String{
+ stringplanmodifier.UseStateForUnknown(),
+ },
+ },
+ "triggers": schema.MapAttribute{
+ MarkdownDescription: "A map of arbitrary strings that, when changed, will cause the server auth token to be re-generated. Works like `triggers` in `terraform_data`.",
+ ElementType: types.StringType,
+ Optional: true,
+ PlanModifiers: []planmodifier.Map{
+ mapplanmodifier.RequiresReplace(),
+ },
+ },
+ },
+ }
+}
+
+func (r *UserServerAuthTokenResource) Configure(_ context.Context, req resource.ConfigureRequest, resp *resource.ConfigureResponse) {
+ if req.ProviderData == nil {
+ return
+ }
+
+ c, ok := req.ProviderData.(*client.Client)
+ if !ok {
+ resp.Diagnostics.AddError(
+ "Unexpected Resource Configure Type",
+ fmt.Sprintf("Expected *client.Client, got: %T. Please report this issue to the provider developers.", req.ProviderData),
+ )
+ return
+ }
+
+ r.client = c
+}
+
+func (r *UserServerAuthTokenResource) Create(ctx context.Context, req resource.CreateRequest, resp *resource.CreateResponse) {
+ var data UserServerAuthTokenResourceModel
+ resp.Diagnostics.Append(req.Plan.Get(ctx, &data)...)
+ if resp.Diagnostics.HasError() {
+ return
+ }
+
+ apiPath := fmt.Sprintf("/users/%s/serverAuthenticationTokens/%d", data.ExtRelationID.ValueString(), data.ServerID.ValueInt64())
+ rawResp, err := r.client.Post(ctx, apiPath, nil)
+ if err != nil {
+ resp.Diagnostics.AddError(
+ "Error Generating User Server Auth Token",
+ fmt.Sprintf("Could not generate server auth token for user %q on server %d: %s", data.ExtRelationID.ValueString(), data.ServerID.ValueInt64(), err),
+ )
+ return
+ }
+
+ // Parse the response for the token and URL.
+ if rawResp != nil {
+ var tokenResp client.AuthTokenResponse
+ if jsonErr := json.Unmarshal(rawResp, &tokenResp); jsonErr == nil {
+ data.Token = types.StringValue(tokenResp.Data.Token)
+ data.URL = types.StringValue(tokenResp.Data.URL)
+ } else {
+ data.Token = types.StringValue("")
+ data.URL = types.StringValue("")
+ }
+ } else {
+ data.Token = types.StringValue("")
+ data.URL = types.StringValue("")
+ }
+
+ data.ID = types.StringValue(fmt.Sprintf("%s-%d-%d", data.ExtRelationID.ValueString(), data.ServerID.ValueInt64(), time.Now().UnixNano()))
+
+ resp.Diagnostics.Append(resp.State.Set(ctx, &data)...)
+}
+
+func (r *UserServerAuthTokenResource) Read(ctx context.Context, req resource.ReadRequest, resp *resource.ReadResponse) {
+ var data UserServerAuthTokenResourceModel
+ resp.Diagnostics.Append(req.State.Get(ctx, &data)...)
+ if resp.Diagnostics.HasError() {
+ return
+ }
+
+ // Return stored state as-is for trigger-style resources.
+ resp.Diagnostics.Append(resp.State.Set(ctx, &data)...)
+}
+
+func (r *UserServerAuthTokenResource) Update(ctx context.Context, req resource.UpdateRequest, resp *resource.UpdateResponse) {
+ var data UserServerAuthTokenResourceModel
+ resp.Diagnostics.Append(req.Plan.Get(ctx, &data)...)
+ if resp.Diagnostics.HasError() {
+ return
+ }
+
+ resp.Diagnostics.Append(resp.State.Set(ctx, &data)...)
+}
+
+func (r *UserServerAuthTokenResource) Delete(_ context.Context, _ resource.DeleteRequest, _ *resource.DeleteResponse) {
+ // No-op: server auth tokens cannot be revoked via this resource. Removing from state only.
+}
+
+// ValidateConfig validates the resource configuration.
+func (r *UserServerAuthTokenResource) ValidateConfig(ctx context.Context, req resource.ValidateConfigRequest, resp *resource.ValidateConfigResponse) {
+ var data UserServerAuthTokenResourceModel
+ resp.Diagnostics.Append(req.Config.Get(ctx, &data)...)
+ if resp.Diagnostics.HasError() {
+ return
+ }
+
+ // Validate server_id is positive.
+ if !data.ServerID.IsNull() && !data.ServerID.IsUnknown() && data.ServerID.ValueInt64() <= 0 {
+ resp.Diagnostics.AddAttributeError(
+ path.Root("server_id"),
+ "Invalid Server ID",
+ "server_id must be a positive integer.",
+ )
+ }
+}
diff --git a/internal/provider/virtfusion_server_build_resource.go b/internal/provider/virtfusion_server_build_resource.go
deleted file mode 100644
index ad528a6..0000000
--- a/internal/provider/virtfusion_server_build_resource.go
+++ /dev/null
@@ -1,285 +0,0 @@
-// Copyright (c) HashiCorp, Inc.
-// SPDX-License-Identifier: MPL-2.0
-
-package provider
-
-import (
- "bytes"
- "context"
- "encoding/json"
- "fmt"
- "github.com/hashicorp/terraform-plugin-framework/resource/schema/booldefault"
- "github.com/hashicorp/terraform-plugin-framework/types"
- "io"
- "io/ioutil"
- "net/http"
-
- "github.com/hashicorp/terraform-plugin-framework/path"
- "github.com/hashicorp/terraform-plugin-framework/resource"
- "github.com/hashicorp/terraform-plugin-framework/resource/schema"
-)
-
-// Ensure provider defined types fully satisfy framework interfaces.
-var _ resource.Resource = &VirtfusionServerBuildResource{}
-var _ resource.ResourceWithImportState = &VirtfusionServerBuildResource{}
-
-func NewVirtfusionServerBuildResource() resource.Resource {
- return &VirtfusionServerBuildResource{}
-}
-
-// VirtfusionServerBuildResource defines the resource implementation.
-type VirtfusionServerBuildResource struct {
- client *http.Client
-}
-
-type VirtfusionServerBuildResourceModel struct {
- ServerId int64 `tfsdk:"server_id"`
- Name string `tfsdk:"name" json:"name"`
- Hostname string `tfsdk:"hostname" json:"hostname"`
- Osid int64 `tfsdk:"osid" json:"operatingSystemId"`
- Vnc bool `tfsdk:"vnc" json:"vnc"`
- Ipv6 bool `tfsdk:"ipv6" json:"ipv6"`
- SshKeys []int64 `tfsdk:"ssh_keys" json:"sshKeys"`
- Email bool `tfsdk:"email" json:"email"`
-}
-
-func (r *VirtfusionServerBuildResource) Metadata(ctx context.Context, req resource.MetadataRequest, resp *resource.MetadataResponse) {
- resp.TypeName = req.ProviderTypeName + "_build"
-}
-
-func (r *VirtfusionServerBuildResource) Schema(ctx context.Context, req resource.SchemaRequest, resp *resource.SchemaResponse) {
- resp.Schema = schema.Schema{
- // This description is used by the documentation generator and the language server.
- MarkdownDescription: "Virtfusion Server Build Resource",
-
- Attributes: map[string]schema.Attribute{
- "server_id": schema.Int64Attribute{
- MarkdownDescription: "Server ID",
- Required: true,
- },
- "name": schema.StringAttribute{
- MarkdownDescription: "Server Name",
- Required: true,
- },
- "hostname": schema.StringAttribute{
- MarkdownDescription: "Server Hostname",
- Optional: true,
- },
- "osid": schema.Int64Attribute{
- MarkdownDescription: "Server Operating System ID",
- Required: true,
- },
- "vnc": schema.BoolAttribute{
- MarkdownDescription: "Server VNC",
- Optional: true,
- Computed: true,
- Default: booldefault.StaticBool(false),
- },
- "ipv6": schema.BoolAttribute{
- MarkdownDescription: "Server IPv6",
- Optional: true,
- Computed: true,
- Default: booldefault.StaticBool(false),
- },
- "ssh_keys": schema.ListAttribute{
- MarkdownDescription: "Server SSH Keys IDs",
- ElementType: types.Int64Type,
- Optional: true,
- },
- "email": schema.BoolAttribute{
- MarkdownDescription: "Server Email",
- Optional: true,
- Computed: true,
- Default: booldefault.StaticBool(false),
- },
- },
- }
-}
-
-func (r *VirtfusionServerBuildResource) Configure(ctx context.Context, req resource.ConfigureRequest, resp *resource.ConfigureResponse) {
- // Prevent panic if the provider has not been configured.
- if req.ProviderData == nil {
- return
- }
-
- client, ok := req.ProviderData.(*http.Client)
-
- if !ok {
- resp.Diagnostics.AddError(
- "Unexpected Resource Configure Type",
- fmt.Sprintf("Expected *http.Client, got: %T. Please report this issue to the provider developers.", req.ProviderData),
- )
-
- return
- }
-
- r.client = client
-}
-
-func (r *VirtfusionServerBuildResource) Create(ctx context.Context, req resource.CreateRequest, resp *resource.CreateResponse) {
- var data VirtfusionServerBuildResourceModel
-
- // Read Terraform plan data into the model
- resp.Diagnostics.Append(req.Plan.Get(ctx, &data)...)
-
- if resp.Diagnostics.HasError() {
- return
- }
-
- createReq := VirtfusionServerBuildResourceModel{
- Name: data.Name,
- Hostname: data.Hostname,
- Osid: data.Osid,
- Vnc: data.Vnc,
- Ipv6: data.Ipv6,
- SshKeys: data.SshKeys,
- Email: data.Email,
- }
-
- httpReqBody, err := json.Marshal(createReq)
-
- if err != nil {
- resp.Diagnostics.AddError(
- "Unable to Create Resource",
- "An unexpected error occurred while creating the resource create request. "+
- "Please report this issue to the provider developers.\n\n"+
- "JSON Error: "+err.Error(),
- )
-
- return
- }
-
- httpReq, err := http.NewRequest("POST", fmt.Sprintf("/servers/%d/build", data.ServerId), bytes.NewBuffer(httpReqBody))
-
- if err != nil {
- resp.Diagnostics.AddError(
- "Failed to Create Request",
- fmt.Sprintf("Failed to create a new HTTP request: %s", err.Error()),
- )
- return
- }
-
- // Add any additional headers (Content-Type, etc.)
- httpReq.Header.Set("Content-Type", "application/json")
-
- httpResponse, err := r.client.Do(httpReq)
- if err != nil {
- resp.Diagnostics.AddError(
- "Failed to Execute Request",
- fmt.Sprintf("Failed to execute HTTP request: %s", err.Error()),
- )
- return
- }
-
- defer func(Body io.ReadCloser) {
- err := Body.Close()
- if err != nil {
- resp.Diagnostics.AddError(
- "Failed to Close Request",
- fmt.Sprintf("Failed to close HTTP request: %s", err.Error()),
- )
- return
- }
- }(httpResponse.Body)
-
- if httpResponse.StatusCode == 422 {
- responseBody, err := ioutil.ReadAll(httpResponse.Body)
- if err != nil {
- resp.Diagnostics.AddError(
- "Failed to Read Response",
- fmt.Sprintf("Failed to read HTTP response body: %s", err.Error()),
- )
- return
- }
-
- var errorResponse map[string]interface{}
- err = json.Unmarshal(responseBody, &errorResponse)
- if err != nil {
- resp.Diagnostics.AddError(
- "Failed to Parse Error Response",
- fmt.Sprintf("Failed to parse HTTP response body: %s", err.Error()),
- )
- return
- }
-
- if errors, exists := errorResponse["errors"]; exists {
- resp.Diagnostics.AddError(
- "Server Returned Errors",
- fmt.Sprintf("Errors from server: %v", errors),
- )
- }
-
- return
- }
-
- if httpResponse.StatusCode != 200 {
- resp.Diagnostics.AddError(
- "Failed to Create Resource",
- fmt.Sprintf("Failed to create resource: %s", httpResponse.Status),
- )
- return
- }
-
- resp.Diagnostics.Append(resp.State.Set(ctx, &data)...)
-}
-
-func (r *VirtfusionServerBuildResource) Read(ctx context.Context, req resource.ReadRequest, resp *resource.ReadResponse) {
- var data VirtfusionServerBuildResourceModel
-
- // Read Terraform prior state data into the model
- resp.Diagnostics.Append(req.State.Get(ctx, &data)...)
-
- if resp.Diagnostics.HasError() {
- return
- }
-
- // If applicable, this is a great opportunity to initialize any necessary
- // provider client data and make a call using it.
- // httpResp, err := r.client.Do(httpReq)
- // if err != nil {
- // resp.Diagnostics.AddError("Client Error", fmt.Sprintf("Unable to read example, got error: %s", err))
- // return
- // }
-
- // Save updated data into Terraform state
- resp.Diagnostics.Append(resp.State.Set(ctx, &data)...)
-}
-
-func (r *VirtfusionServerBuildResource) Update(ctx context.Context, req resource.UpdateRequest, resp *resource.UpdateResponse) {
- var data VirtfusionServerBuildResourceModel
-
- // Read Terraform plan data into the model
- resp.Diagnostics.Append(req.Plan.Get(ctx, &data)...)
-
- if resp.Diagnostics.HasError() {
- return
- }
-
- // If applicable, this is a great opportunity to initialize any necessary
- // provider client data and make a call using it.
- // httpResp, err := r.client.Do(httpReq)
- // if err != nil {
- // resp.Diagnostics.AddError("Client Error", fmt.Sprintf("Unable to update example, got error: %s", err))
- // return
- // }
-
- // Save updated data into Terraform state
- resp.Diagnostics.Append(resp.State.Set(ctx, &data)...)
-}
-
-func (r *VirtfusionServerBuildResource) Delete(ctx context.Context, req resource.DeleteRequest, resp *resource.DeleteResponse) {
- var data VirtfusionServerBuildResourceModel
-
- // Read Terraform prior state data into the model
- resp.Diagnostics.Append(req.State.Get(ctx, &data)...)
-
- if resp.Diagnostics.HasError() {
- return
- }
-
- resp.Diagnostics.Append(resp.State.Set(ctx, &data)...)
-}
-
-func (r *VirtfusionServerBuildResource) ImportState(ctx context.Context, req resource.ImportStateRequest, resp *resource.ImportStateResponse) {
- resource.ImportStatePassthroughID(ctx, path.Root("id"), req, resp)
-}
diff --git a/internal/provider/virtfusion_server_resource.go b/internal/provider/virtfusion_server_resource.go
deleted file mode 100644
index 0dbf8f6..0000000
--- a/internal/provider/virtfusion_server_resource.go
+++ /dev/null
@@ -1,374 +0,0 @@
-// Copyright (c) HashiCorp, Inc.
-// SPDX-License-Identifier: MPL-2.0
-
-package provider
-
-import (
- "bytes"
- "context"
- "encoding/json"
- "fmt"
- "github.com/hashicorp/terraform-plugin-framework/resource/schema/int64default"
- "github.com/hashicorp/terraform-plugin-framework/types"
- "io"
- "io/ioutil"
- "net/http"
-
- "github.com/hashicorp/terraform-plugin-framework/path"
- "github.com/hashicorp/terraform-plugin-framework/resource"
- "github.com/hashicorp/terraform-plugin-framework/resource/schema"
-)
-
-// Ensure provider defined types fully satisfy framework interfaces.
-var _ resource.Resource = &VirtfusionServerResource{}
-var _ resource.ResourceWithImportState = &VirtfusionServerResource{}
-
-func NewVirtfusionServerResource() resource.Resource {
- return &VirtfusionServerResource{}
-}
-
-// VirtfusionServerResource defines the resource implementation.
-type VirtfusionServerResource struct {
- client *http.Client
-}
-
-// VirtfusionServerResourceModel describes the resource data model.
-type VirtfusionServerResourceModel struct {
- PackageId *int64 `tfsdk:"package_id" json:"packageId,omitempty"`
- UserId *int64 `tfsdk:"user_id" json:"userId,omitempty"`
- HypervisorId *int64 `tfsdk:"hypervisor_id" json:"hypervisorId,omitempty"`
- Ipv4 *int64 `tfsdk:"ipv4" json:"ipv4,omitempty"`
- Storage *int64 `tfsdk:"storage" json:"storage,omitempty"`
- Memory *int64 `tfsdk:"memory" json:"memory,omitempty"`
- Cores *int64 `tfsdk:"cores" json:"cpuCores,omitempty"`
- Traffic *int64 `tfsdk:"traffic" json:"traffic,omitempty"`
- InboundNetworkSpeed *int64 `tfsdk:"inbound_network_speed" json:"networkSpeedInbound,omitempty"`
- OutboundNetworkSpeed *int64 `tfsdk:"outbound_network_speed" json:"networkSpeedOutbound,omitempty"`
- StorageProfile *int64 `tfsdk:"storage_profile" json:"storageProfile,omitempty"`
- NetworkProfile *int64 `tfsdk:"network_profile" json:"networkProfile,omitempty"`
- Id types.Int64 `tfsdk:"id" json:"id"`
-}
-
-func (r *VirtfusionServerResource) Metadata(ctx context.Context, req resource.MetadataRequest, resp *resource.MetadataResponse) {
- resp.TypeName = req.ProviderTypeName + "_server"
-}
-
-func (r *VirtfusionServerResource) Schema(ctx context.Context, req resource.SchemaRequest, resp *resource.SchemaResponse) {
- resp.Schema = schema.Schema{
- // This description is used by the documentation generator and the language server.
- MarkdownDescription: "Virtfusion Server Resource",
-
- Attributes: map[string]schema.Attribute{
- "package_id": schema.Int64Attribute{
- MarkdownDescription: "Package ID",
- Required: true,
- },
- "user_id": schema.Int64Attribute{
- MarkdownDescription: "User ID",
- Required: true,
- },
- "hypervisor_id": schema.Int64Attribute{
- MarkdownDescription: "Hypervisor Group ID",
- Required: true,
- },
- "ipv4": schema.Int64Attribute{
- MarkdownDescription: "IPv4 Addresses to assign. Omit to use the default of 1 IPv4.",
- Optional: true,
- Computed: true,
- Default: int64default.StaticInt64(1),
- },
- "storage": schema.Int64Attribute{
- MarkdownDescription: "Primary storage size in GB. Omit to use the default storage size from the package.",
- Optional: true,
- },
- "memory": schema.Int64Attribute{
- MarkdownDescription: "How much memory to allocate in MB. Omit to use the default memory size from the package.",
- Optional: true,
- },
- "cores": schema.Int64Attribute{
- MarkdownDescription: "How many cores to allocate. Omit to use the default core count from the package.",
- Optional: true,
- },
- "traffic": schema.Int64Attribute{
- MarkdownDescription: "How much traffic to allocate in GB. Omit to use the default traffic size from the package. 0=Unlimited",
- Optional: true,
- },
- "inbound_network_speed": schema.Int64Attribute{
- MarkdownDescription: "Inbound network speed in kB/s. Omit to use the default inbound network speed from the package.",
- Optional: true,
- },
- "outbound_network_speed": schema.Int64Attribute{
- MarkdownDescription: "Outbound network speed in kB/s. Omit to use the default outbound network speed from the package.",
- Optional: true,
- },
- "storage_profile": schema.Int64Attribute{
- MarkdownDescription: "Storage profile ID. Omit to use the default storage profile from the package.",
- Optional: true,
- },
- "network_profile": schema.Int64Attribute{
- MarkdownDescription: "Network profile ID. Omit to use the default network profile from the package.",
- Optional: true,
- },
- "id": schema.Int64Attribute{
- MarkdownDescription: "Server ID",
- Computed: true,
- },
- },
- }
-}
-
-func (r *VirtfusionServerResource) Configure(ctx context.Context, req resource.ConfigureRequest, resp *resource.ConfigureResponse) {
- // Prevent panic if the provider has not been configured.
- if req.ProviderData == nil {
- return
- }
-
- client, ok := req.ProviderData.(*http.Client)
-
- if !ok {
- resp.Diagnostics.AddError(
- "Unexpected Resource Configure Type",
- fmt.Sprintf("Expected *http.Client, got: %T. Please report this issue to the provider developers.", req.ProviderData),
- )
-
- return
- }
-
- r.client = client
-}
-
-func (r *VirtfusionServerResource) Create(ctx context.Context, req resource.CreateRequest, resp *resource.CreateResponse) {
- var data VirtfusionServerResourceModel
-
- // Read Terraform plan data into the model
- resp.Diagnostics.Append(req.Plan.Get(ctx, &data)...)
-
- if resp.Diagnostics.HasError() {
- return
- }
-
- createReq := VirtfusionServerResourceModel{
- PackageId: data.PackageId,
- UserId: data.UserId,
- HypervisorId: data.HypervisorId,
- Ipv4: data.Ipv4,
- Storage: data.Storage,
- Traffic: data.Traffic,
- Memory: data.Memory,
- Cores: data.Cores,
- InboundNetworkSpeed: data.InboundNetworkSpeed,
- OutboundNetworkSpeed: data.OutboundNetworkSpeed,
- StorageProfile: data.StorageProfile,
- NetworkProfile: data.NetworkProfile,
- }
-
- httpReqBody, err := json.Marshal(createReq)
-
- if err != nil {
- resp.Diagnostics.AddError(
- "Unable to Create Resource",
- "An unexpected error occurred while creating the resource create request. "+
- "Please report this issue to the provider developers.\n\n"+
- "JSON Error: "+err.Error(),
- )
-
- return
- }
-
- httpReq, err := http.NewRequest("POST", "/servers", bytes.NewBuffer(httpReqBody))
-
- if err != nil {
- resp.Diagnostics.AddError(
- "Failed to Create Request",
- fmt.Sprintf("Failed to create a new HTTP request: %s", err.Error()),
- )
- return
- }
-
- // Add any additional headers (Content-Type, etc.)
- httpReq.Header.Set("Content-Type", "application/json")
-
- httpResponse, err := r.client.Do(httpReq)
- if err != nil {
- resp.Diagnostics.AddError(
- "Failed to Execute Request",
- fmt.Sprintf("Failed to execute HTTP request: %s", err.Error()),
- )
- return
- }
-
- defer func(Body io.ReadCloser) {
- err := Body.Close()
- if err != nil {
- resp.Diagnostics.AddError(
- "Failed to Close Request",
- fmt.Sprintf("Failed to close HTTP request: %s", err.Error()),
- )
- return
- }
- }(httpResponse.Body)
-
- if httpResponse.StatusCode == 422 {
- responseBody, err := ioutil.ReadAll(httpResponse.Body)
- if err != nil {
- resp.Diagnostics.AddError(
- "Failed to Read Response",
- fmt.Sprintf("Failed to read HTTP response body: %s", err.Error()),
- )
- return
- }
-
- var errorResponse map[string]interface{}
- err = json.Unmarshal(responseBody, &errorResponse)
- if err != nil {
- resp.Diagnostics.AddError(
- "Failed to Parse Error Response",
- fmt.Sprintf("Failed to parse HTTP response body: %s", err.Error()),
- )
- return
- }
-
- if errors, exists := errorResponse["errors"]; exists {
- resp.Diagnostics.AddError(
- "Server Returned Errors",
- fmt.Sprintf("Errors from server: %v", errors),
- )
- }
-
- return
- }
-
- if httpResponse.StatusCode != 201 {
- resp.Diagnostics.AddError(
- "Failed to Create Resource",
- fmt.Sprintf("Failed to create resource: %s", httpResponse.Status),
- )
- return
- }
-
- responseBody, err := ioutil.ReadAll(httpResponse.Body)
-
- if err != nil {
- resp.Diagnostics.AddError(
- "Failed to Read Response",
- fmt.Sprintf("Failed to read HTTP response body: %s", err.Error()),
- )
- return
- }
-
- type ResponseData struct {
- Data struct {
- Id int64 `json:"id"`
- Uuid string `json:"uuid"`
- Name string `json:"name"`
- } `json:"data"`
- }
-
- var responseData ResponseData
-
- // Unmarshal the JSON response
- err = json.Unmarshal(responseBody, &responseData)
-
- if err != nil {
- resp.Diagnostics.AddError(
- "Failed to Parse Response",
- fmt.Sprintf("Failed to parse HTTP response body: %s", err.Error()),
- )
- return
- }
-
- // Update the Terraform state with the server ID
- data.Id = types.Int64Value(responseData.Data.Id)
-
- resp.Diagnostics.Append(resp.State.Set(ctx, &data)...)
-}
-
-func (r *VirtfusionServerResource) Read(ctx context.Context, req resource.ReadRequest, resp *resource.ReadResponse) {
- var data VirtfusionServerResourceModel
-
- // Read Terraform prior state data into the model
- resp.Diagnostics.Append(req.State.Get(ctx, &data)...)
-
- if resp.Diagnostics.HasError() {
- return
- }
-
- // If applicable, this is a great opportunity to initialize any necessary
- // provider client data and make a call using it.
- // httpResp, err := r.client.Do(httpReq)
- // if err != nil {
- // resp.Diagnostics.AddError("Client Error", fmt.Sprintf("Unable to read example, got error: %s", err))
- // return
- // }
-
- // Save updated data into Terraform state
- resp.Diagnostics.Append(resp.State.Set(ctx, &data)...)
-}
-
-func (r *VirtfusionServerResource) Update(ctx context.Context, req resource.UpdateRequest, resp *resource.UpdateResponse) {
- var data VirtfusionServerResourceModel
-
- // Read Terraform plan data into the model
- resp.Diagnostics.Append(req.Plan.Get(ctx, &data)...)
-
- if resp.Diagnostics.HasError() {
- return
- }
-
- // If applicable, this is a great opportunity to initialize any necessary
- // provider client data and make a call using it.
- // httpResp, err := r.client.Do(httpReq)
- // if err != nil {
- // resp.Diagnostics.AddError("Client Error", fmt.Sprintf("Unable to update example, got error: %s", err))
- // return
- // }
-
- // Save updated data into Terraform state
- resp.Diagnostics.Append(resp.State.Set(ctx, &data)...)
-}
-
-func (r *VirtfusionServerResource) Delete(ctx context.Context, req resource.DeleteRequest, resp *resource.DeleteResponse) {
- var data VirtfusionServerResourceModel
-
- // Read Terraform prior state data into the model
- resp.Diagnostics.Append(req.State.Get(ctx, &data)...)
-
- if resp.Diagnostics.HasError() {
- return
- }
-
- httpReq, err := http.NewRequest("DELETE", fmt.Sprintf("/servers/%d?delay=0", data.Id.ValueInt64()), bytes.NewBuffer([]byte{}))
- if err != nil {
- resp.Diagnostics.AddError(
- "Failed to Create Request",
- fmt.Sprintf("Failed to create a new HTTP request: %s", err.Error()),
- )
- return
- }
-
- // Add any additional headers (Content-Type, etc.)
- httpReq.Header.Set("Content-Type", "application/json")
-
- httpResponse, err := r.client.Do(httpReq)
- if err != nil {
- resp.Diagnostics.AddError(
- "Failed to Execute Request",
- fmt.Sprintf("Failed to execute HTTP request: %s", err.Error()),
- )
- return
- }
-
- if httpResponse.StatusCode != 204 {
- resp.Diagnostics.AddError(
- "Failed to Delete Resource",
- fmt.Sprintf("Failed to delete resource: %s", httpResponse.Status),
- )
- return
- }
-
- resp.Diagnostics.Append(resp.State.Set(ctx, &data)...)
-}
-
-func (r *VirtfusionServerResource) ImportState(ctx context.Context, req resource.ImportStateRequest, resp *resource.ImportStateResponse) {
- resource.ImportStatePassthroughID(ctx, path.Root("id"), req, resp)
-}
diff --git a/internal/provider/virtfusion_ssh_resource.go b/internal/provider/virtfusion_ssh_resource.go
deleted file mode 100644
index c1034dd..0000000
--- a/internal/provider/virtfusion_ssh_resource.go
+++ /dev/null
@@ -1,332 +0,0 @@
-// Copyright (c) HashiCorp, Inc.
-// SPDX-License-Identifier: MPL-2.0
-
-package provider
-
-import (
- "bytes"
- "context"
- "encoding/json"
- "fmt"
- "github.com/hashicorp/terraform-plugin-framework/path"
- "github.com/hashicorp/terraform-plugin-framework/resource"
- "github.com/hashicorp/terraform-plugin-framework/resource/schema"
- "github.com/hashicorp/terraform-plugin-framework/types"
- "io"
- "net/http"
-)
-
-// Ensure provider defined types fully satisfy framework interfaces.
-var _ resource.Resource = &VirtfusionSSHResource{}
-var _ resource.ResourceWithImportState = &VirtfusionSSHResource{}
-
-func NewVirtfusionSSHResource() resource.Resource {
- return &VirtfusionSSHResource{}
-}
-
-// VirtfusionSSHResource defines the resource implementation.
-type VirtfusionSSHResource struct {
- client *http.Client
-}
-
-// VirtfusionSSHResourceModel describes the resource data model.
-type VirtfusionSSHResourceModel struct {
- UserId *int64 `tfsdk:"user_id" json:"userId"`
- Name *string `tfsdk:"name" json:"name"`
- PublicKey *string `tfsdk:"public_key" json:"publicKey"`
- Id types.Int64 `tfsdk:"id" json:"id,omitempty"`
-}
-
-func (r *VirtfusionSSHResource) Metadata(ctx context.Context, req resource.MetadataRequest, resp *resource.MetadataResponse) {
- resp.TypeName = req.ProviderTypeName + "_ssh"
-}
-
-func (r *VirtfusionSSHResource) Schema(ctx context.Context, req resource.SchemaRequest, resp *resource.SchemaResponse) {
- resp.Schema = schema.Schema{
- // This description is used by the documentation generator and the language server.
- MarkdownDescription: "Virtfusion SSH Resource",
-
- Attributes: map[string]schema.Attribute{
- "user_id": schema.Int64Attribute{
- Description: "User ID",
- Required: true,
- },
- "name": schema.StringAttribute{
- Description: "Key Name",
- Required: true,
- },
- "public_key": schema.StringAttribute{
- Description: "Public Key",
- Required: true,
- },
- "id": schema.Int64Attribute{
- Description: "SSH Key ID",
- Computed: true,
- },
- },
- }
-}
-
-func (r *VirtfusionSSHResource) Configure(ctx context.Context, req resource.ConfigureRequest, resp *resource.ConfigureResponse) {
- // Prevent panic if the provider has not been configured.
- if req.ProviderData == nil {
- return
- }
-
- client, ok := req.ProviderData.(*http.Client)
-
- if !ok {
- resp.Diagnostics.AddError(
- "Unexpected Resource Configure Type",
- fmt.Sprintf("Expected *http.Client, got: %T. Please report this issue to the provider developers.", req.ProviderData),
- )
-
- return
- }
-
- r.client = client
-}
-
-func (r *VirtfusionSSHResource) Create(ctx context.Context, req resource.CreateRequest, resp *resource.CreateResponse) {
- var data VirtfusionSSHResourceModel
-
- // Read Terraform plan data into the model
- resp.Diagnostics.Append(req.Plan.Get(ctx, &data)...)
-
- if resp.Diagnostics.HasError() {
- return
- }
-
- createReq := VirtfusionSSHResourceModel{
- UserId: data.UserId,
- Name: data.Name,
- PublicKey: data.PublicKey,
- }
-
- // Convert the model to JSON
- jsonReq, err := json.Marshal(createReq)
-
- if err != nil {
- resp.Diagnostics.AddError(
- "Failed to marshal request body",
- fmt.Sprintf("Failed to marshal request body: %s", err.Error()),
- )
- return
- }
-
- httpReq, err := r.client.Post("/ssh_keys", "application/json", bytes.NewBuffer(jsonReq))
-
- if err != nil {
- resp.Diagnostics.AddError(
- "Request failed",
- fmt.Sprintf("Request failed: %s", err.Error()),
- )
- return
- }
-
- defer func(Body io.ReadCloser) {
- err := Body.Close()
- if err != nil {
- resp.Diagnostics.AddError(
- "Failed to close response body",
- fmt.Sprintf("Failed to close response body: %s", err.Error()),
- )
- }
- }(httpReq.Body)
-
- if httpReq.StatusCode != 201 {
-
- if httpReq.StatusCode == 422 {
- responseBody, _ := io.ReadAll(httpReq.Body)
- var errorResponse map[string]interface{}
- err = json.Unmarshal(responseBody, &errorResponse)
- if errors, exists := errorResponse["errors"]; exists {
- resp.Diagnostics.AddError(
- "Failed to create SSH key",
- fmt.Sprintf("Errors from server: %v", errors),
- )
-
- return
- }
- }
-
- resp.Diagnostics.AddError(
- "Invalid Request",
- fmt.Sprintf("Failed to create SSH key: %s", httpReq.Status),
- )
- return
- }
-
- // Read the response body into the model. The response is expected to be a JSON object with the body of the created
- // ssh key within the `data` field. The `data` field is a JSON object with the ssh key data.
- responseBody, err := io.ReadAll(httpReq.Body)
-
- type ResponseData struct {
- Data struct {
- Id int64 `json:"id"`
- Name string `json:"name"`
- Type string `json:"type"`
- CreatedAt string `json:"createdAt"`
- } `json:"data"`
- }
-
- var responseData ResponseData
-
- // Unmarshal the response body into the model
- err = json.Unmarshal(responseBody, &responseData)
-
- if err != nil {
- resp.Diagnostics.AddError(
- "Failed to unmarshal response body",
- fmt.Sprintf("Failed to unmarshal response body: %s", err.Error()),
- )
- return
- }
-
- data.Id = types.Int64Value(responseData.Data.Id)
- data.Name = &responseData.Data.Name
-
- resp.Diagnostics.Append(resp.State.Set(ctx, &data)...)
-}
-
-func (r *VirtfusionSSHResource) Read(ctx context.Context, req resource.ReadRequest, resp *resource.ReadResponse) {
- var data VirtfusionSSHResourceModel
-
- // Read Terraform prior state data into the model
- resp.Diagnostics.Append(req.State.Get(ctx, &data)...)
-
- if resp.Diagnostics.HasError() {
- return
- }
-
- httpReq, err := http.NewRequest("GET", fmt.Sprintf("/ssh_keys/%d", data.Id.ValueInt64()), nil)
-
- if err != nil {
- resp.Diagnostics.AddError(
- "Failed to Create Request",
- fmt.Sprintf("Failed to create a new HTTP request: %s", err.Error()),
- )
- return
- }
-
- // If the resource returns a 404, then the resource has been deleted. Return an empty state.
- httpResponse, err := r.client.Do(httpReq)
- defer func(Body io.ReadCloser) {
- err := Body.Close()
- if err != nil {
- resp.Diagnostics.AddError(
- "Failed to close response body",
- fmt.Sprintf("Failed to close response body: %s", err.Error()),
- )
- }
- }(httpResponse.Body)
-
- if err != nil {
- resp.Diagnostics.AddError(
- "Failed to Execute Request",
- fmt.Sprintf("Failed to execute HTTP request: %s", err.Error()),
- )
- return
- }
-
- if httpResponse.StatusCode == 404 {
- resp.State.RemoveResource(ctx)
- return
- }
-
- var responseData struct {
- Data struct {
- Id int64 `json:"id"`
- Name string `json:"name"`
- Type string `json:"type"`
- Enabled bool `json:"enabled"`
- CreatedAt string `json:"created"`
- UpdatedAt string `json:"updated"`
- PublicKeyHash string `json:"publicKey"`
- } `json:"data"`
- }
-
- err = json.NewDecoder(httpResponse.Body).Decode(&responseData)
-
- if err != nil {
- resp.Diagnostics.AddError(
- "Failed to decode response body",
- fmt.Sprintf("Failed to decode response body: %s", err.Error()),
- )
- return
- }
-
- data.Name = &responseData.Data.Name
-
- // Save updated data into Terraform state
- resp.Diagnostics.Append(resp.State.Set(ctx, &data)...)
-}
-
-func (r *VirtfusionSSHResource) Update(ctx context.Context, req resource.UpdateRequest, resp *resource.UpdateResponse) {
- var data VirtfusionSSHResourceModel
-
- // Read Terraform plan data into the model
- resp.Diagnostics.Append(req.Plan.Get(ctx, &data)...)
-
- if resp.Diagnostics.HasError() {
- return
-
- }
-
- // Save updated data into Terraform state
- resp.Diagnostics.Append(resp.State.Set(ctx, &data)...)
-}
-
-func (r *VirtfusionSSHResource) Delete(ctx context.Context, req resource.DeleteRequest, resp *resource.DeleteResponse) {
- var data VirtfusionSSHResourceModel
-
- // Read Terraform prior state data into the model
- resp.Diagnostics.Append(req.State.Get(ctx, &data)...)
-
- if resp.Diagnostics.HasError() {
- return
- }
-
- httpReq, err := http.NewRequest("DELETE", fmt.Sprintf("/ssh_keys/%d", data.Id.ValueInt64()), nil)
- if err != nil {
- resp.Diagnostics.AddError(
- "Failed to Create Request",
- fmt.Sprintf("Failed to create a new HTTP request: %s", err.Error()),
- )
- return
- }
-
- // Add any additional headers (Content-Type, etc.)
- httpReq.Header.Set("Content-Type", "application/json")
-
- httpResponse, err := r.client.Do(httpReq)
- if err != nil {
- resp.Diagnostics.AddError(
- "Failed to Execute Request",
- fmt.Sprintf("Failed to execute HTTP request: %s", err.Error()),
- )
- return
- }
-
- if httpResponse.StatusCode != 204 {
- resp.Diagnostics.AddError(
- "Failed to Delete Resource",
- fmt.Sprintf("Failed to delete resource: %s", httpResponse.Status),
- )
- return
- }
-
- if err != nil {
- resp.Diagnostics.AddError(
- "Request failed",
- fmt.Sprintf("Request failed: %s", err.Error()),
- )
- return
- }
-
- resp.Diagnostics.Append(resp.State.Set(ctx, &data)...)
-}
-
-func (r *VirtfusionSSHResource) ImportState(ctx context.Context, req resource.ImportStateRequest, resp *resource.ImportStateResponse) {
- resource.ImportStatePassthroughID(ctx, path.Root("id"), req, resp)
-}
diff --git a/main.go b/main.go
index decc29a..df25b71 100644
--- a/main.go
+++ b/main.go
@@ -1,4 +1,4 @@
-// Copyright (c) HashiCorp, Inc.
+// Copyright (c) EZSCALE.
// SPDX-License-Identifier: MPL-2.0
package main
@@ -15,21 +15,19 @@ import (
// Run "go generate" to format example terraform files and generate the docs for the registry/website
-// If you do not have terraform installed, you can remove the formatting command, but its suggested to
+// If you do not have terraform installed, you can remove the formatting command, but it is suggested to
// ensure the documentation is formatted properly.
//go:generate terraform fmt -recursive ./examples/
// Run the docs generation tool, check its repository for more information on how it works and how docs
// can be customized.
-//go:generate go run github.com/hashicorp/terraform-plugin-docs/cmd/tfplugindocs
+//go:generate go run github.com/hashicorp/terraform-plugin-docs/cmd/tfplugindocs generate -provider-name virtfusion
var (
- // these will be set by the goreleaser configuration
+ // These will be set by the goreleaser configuration
// to appropriate values for the compiled binary.
- version string = "0.0.3"
-
- // goreleaser can pass other information to the main package, such as the specific commit
// https://goreleaser.com/cookbooks/using-main.version/
+ version string = "dev"
)
func main() {
@@ -39,13 +37,11 @@ func main() {
flag.Parse()
opts := providerserver.ServeOpts{
- // TODO: Update this string with the published name of your provider.
Address: "registry.terraform.io/EZSCALE/virtfusion",
Debug: debug,
}
err := providerserver.Serve(context.Background(), provider.New(version), opts)
-
if err != nil {
log.Fatal(err.Error())
}
diff --git a/openapi.yaml b/openapi.yaml
new file mode 100644
index 0000000..caa19fc
--- /dev/null
+++ b/openapi.yaml
@@ -0,0 +1,8309 @@
+openapi: 3.0.1
+info:
+ title: VirtFusion Global API
+ description: >-
+ You can use this API to access all Administrator API endpoints, such as the
+ server API to deploy and manage servers, or the system API to configure and
+ manage the system.
+
+
+ The API is organized around REST. All requests should be made over SSL. All
+ request and response bodies, including errors, are encoded in JSON.
+
+
+ Endpoint https://cp.domain.com/api/v1
+ version: 1.0.0
+tags:
+ - name: General
+ - name: Hypervisors
+ - name: Hypervisor Groups
+ - name: Servers
+ - name: Servers/Network
+ - name: Servers/Network/Firewall
+ - name: Servers/Network/Traffic
+ - name: Servers/Power
+ - name: IP Blocks
+ - name: Backups
+ - name: DNS
+ - name: Media
+ - name: Packages
+ - name: Queue & Tasks
+ - name: SSH Keys
+ - name: Users
+ - name: Users/External Rel ID & Rel Str
+ - name: Self Service
+ - name: Self Service/External Relational ID
+paths:
+ /connect:
+ get:
+ summary: Test connection
+ deprecated: false
+ description: ''
+ tags:
+ - General
+ parameters: []
+ responses:
+ '200':
+ description: ''
+ content:
+ application/json:
+ schema:
+ type: object
+ properties: {}
+ example: []
+ headers: {}
+ '401':
+ $ref: '#/components/responses/401'
+ description: ''
+ security:
+ - bearer: []
+ /compute/hypervisors/groups:
+ get:
+ summary: Retrieve hypervisor groups
+ deprecated: false
+ description: ''
+ tags:
+ - Hypervisor Groups
+ parameters:
+ - name: results
+ in: query
+ description: >-
+ Number of results to return. Range between 1 and 200. Defaults to
+ 20.
+ required: false
+ example: 20
+ schema:
+ type: integer
+ responses:
+ '200':
+ description: ''
+ content:
+ application/json:
+ schema:
+ type: object
+ properties: {}
+ example:
+ current_page: 1
+ data:
+ - id: 1
+ name: Default
+ label: null
+ description: Default hypervisor group
+ distributionType: 5
+ enabled: true
+ default: true
+ created: '2024-03-12T22:21:32+00:00'
+ updated: '2024-04-12T20:56:04+00:00'
+ - id: 2
+ name: Test 1
+ label: null
+ description: null
+ distributionType: 13
+ enabled: true
+ default: false
+ created: '2024-10-08T13:23:28+00:00'
+ updated: '2024-10-08T13:23:42+00:00'
+ - id: 3
+ name: Test 2
+ label: null
+ description: null
+ distributionType: 5
+ enabled: true
+ default: false
+ created: '2024-10-12T21:12:33+00:00'
+ updated: '2024-10-12T21:14:18+00:00'
+ first_page_url: >-
+ https://192.168.3.11/api/v1/compute/hypervisors/groups?results=20&page=1
+ from: 1
+ last_page: 1
+ last_page_url: >-
+ https://192.168.3.11/api/v1/compute/hypervisors/groups?results=20&page=1
+ links:
+ - url: null
+ label: '« Previous'
+ active: false
+ - url: >-
+ https://192.168.3.11/api/v1/compute/hypervisors/groups?results=20&page=1
+ label: '1'
+ active: true
+ - url: null
+ label: Next »
+ active: false
+ next_page_url: null
+ path: https://192.168.3.11/api/v1/compute/hypervisors/groups
+ per_page: 20
+ prev_page_url: null
+ to: 3
+ total: 3
+ headers: {}
+ '401':
+ $ref: '#/components/responses/401'
+ description: ''
+ security:
+ - bearer: []
+ /compute/hypervisors/groups/{hypervisorGroupId}:
+ get:
+ summary: Retrieve a hypervisor group
+ deprecated: false
+ description: ''
+ tags:
+ - Hypervisor Groups
+ parameters:
+ - name: hypervisorGroupId
+ in: path
+ description: A valid hypervisor group ID as shown in VirtFusion.
+ required: true
+ example: 1
+ schema:
+ type: integer
+ responses:
+ '200':
+ description: ''
+ content:
+ application/json:
+ schema:
+ type: object
+ properties: {}
+ example:
+ data:
+ id: 1
+ name: Default
+ label: null
+ description: Default hypervisor group
+ distributionType: 5
+ enabled: true
+ default: true
+ created: '2024-03-12T22:21:32+00:00'
+ updated: '2024-04-12T20:56:04+00:00'
+ headers: {}
+ '401':
+ $ref: '#/components/responses/401'
+ description: ''
+ security:
+ - bearer: []
+ /compute/hypervisors/groups/{hypervisorGroupId}/resources:
+ get:
+ summary: Retrieve a hypervisor groups resources
+ deprecated: false
+ description: ''
+ tags:
+ - Hypervisor Groups
+ parameters:
+ - name: hypervisorGroupId
+ in: path
+ description: A valid hypervisor group ID as shown in VirtFusion.
+ required: true
+ example: 1
+ schema:
+ type: integer
+ - name: results
+ in: query
+ description: >-
+ Number of results to return. Range between 1 and 200. Defaults to
+ 20.
+ required: false
+ example: 20
+ schema:
+ type: integer
+ responses:
+ '200':
+ description: ''
+ content:
+ application/json:
+ schema:
+ type: object
+ properties: {}
+ example:
+ current_page: 1
+ data:
+ - hypervisor:
+ id: 1
+ name: PHV 1 (RED)
+ enabled: true
+ prohibit: false
+ accept: false
+ commissioned: true
+ resources:
+ servers:
+ units: '#'
+ max: 0
+ allocated: 1
+ free: -1
+ percent: null
+ memory:
+ units: MB
+ max: 6004
+ allocated: 4096
+ free: 1908
+ percent: 68.2
+ cpuCores:
+ units: '#'
+ max: 4
+ allocated: 3
+ free: 1
+ percent: 75
+ localStorage:
+ enabled: 1
+ name: Local (Default mountpoint)
+ storageType: 0
+ units: GB
+ max: 90
+ allocated: 25
+ free: 65
+ percent: 27.8
+ otherStorage: []
+ network:
+ total:
+ ipv4:
+ free: 470
+ - hypervisor:
+ id: 2
+ name: PHV 2 (BLUE)
+ enabled: true
+ prohibit: false
+ accept: false
+ commissioned: true
+ resources:
+ servers:
+ units: '#'
+ max: 0
+ allocated: 1
+ free: -1
+ percent: null
+ memory:
+ units: MB
+ max: 10000
+ allocated: 1024
+ free: 8976
+ percent: 10.2
+ cpuCores:
+ units: '#'
+ max: 28
+ allocated: 1
+ free: 27
+ percent: 3.6
+ localStorage:
+ enabled: 1
+ name: Local (Default mountpoint)
+ storageType: 0
+ units: GB
+ max: 150
+ allocated: 10
+ free: 140
+ percent: 6.7
+ otherStorage: []
+ network:
+ total:
+ ipv4:
+ free: 470
+ - hypervisor:
+ id: 3
+ name: BHV 9
+ enabled: true
+ prohibit: false
+ accept: false
+ commissioned: true
+ resources:
+ servers:
+ units: '#'
+ max: 0
+ allocated: 2
+ free: -2
+ percent: null
+ memory:
+ units: MB
+ max: 27913
+ allocated: 2048
+ free: 25865
+ percent: 7.3
+ cpuCores:
+ units: '#'
+ max: 64
+ allocated: 2
+ free: 62
+ percent: 3.1
+ localStorage:
+ enabled: 1
+ name: Local (Default mountpoint)
+ storageType: 0
+ units: GB
+ max: 400
+ allocated: 20
+ free: 380
+ percent: 5
+ otherStorage: []
+ network:
+ total:
+ ipv4:
+ free: 470
+ - hypervisor:
+ id: 4
+ name: BHV 8
+ enabled: true
+ prohibit: false
+ accept: false
+ commissioned: true
+ resources:
+ servers:
+ units: '#'
+ max: 0
+ allocated: 0
+ free: 0
+ percent: null
+ memory:
+ units: MB
+ max: 27913
+ allocated: 0
+ free: 27913
+ percent: 0
+ cpuCores:
+ units: '#'
+ max: 16
+ allocated: 0
+ free: 16
+ percent: 0
+ localStorage:
+ enabled: 1
+ name: Local (Default mountpoint)
+ storageType: 0
+ units: GB
+ max: 1000
+ allocated: 0
+ free: 1000
+ percent: 0
+ otherStorage: []
+ network:
+ total:
+ ipv4:
+ free: 0
+ - hypervisor:
+ id: 8
+ name: BHV 3
+ enabled: true
+ prohibit: false
+ accept: false
+ commissioned: true
+ resources:
+ servers:
+ units: '#'
+ max: 0
+ allocated: 2
+ free: -2
+ percent: null
+ memory:
+ units: MB
+ max: 27913
+ allocated: 2048
+ free: 25865
+ percent: 7.3
+ cpuCores:
+ units: '#'
+ max: 120
+ allocated: 3
+ free: 117
+ percent: 2.5
+ localStorage:
+ enabled: 1
+ name: Local (Default mountpoint)
+ storageType: 1
+ units: GB
+ max: 2000
+ allocated: 20
+ free: 1980
+ percent: 1
+ otherStorage: []
+ network:
+ total:
+ ipv4:
+ free: 470
+ - hypervisor:
+ id: 9
+ name: BHV 4
+ enabled: true
+ prohibit: false
+ accept: false
+ commissioned: true
+ resources:
+ servers:
+ units: '#'
+ max: 0
+ allocated: 0
+ free: 0
+ percent: null
+ memory:
+ units: MB
+ max: 13684
+ allocated: 0
+ free: 13684
+ percent: 0
+ cpuCores:
+ units: '#'
+ max: 4
+ allocated: 0
+ free: 4
+ percent: 0
+ localStorage:
+ enabled: 1
+ name: Local (Default mountpoint)
+ storageType: 0
+ units: GB
+ max: 1000
+ allocated: 0
+ free: 1000
+ percent: 0
+ otherStorage: []
+ network:
+ total:
+ ipv4:
+ free: 0
+ - hypervisor:
+ id: 10
+ name: BHV 5
+ enabled: true
+ prohibit: false
+ accept: false
+ commissioned: true
+ resources:
+ servers:
+ units: '#'
+ max: 0
+ allocated: 0
+ free: 0
+ percent: null
+ memory:
+ units: MB
+ max: 13684
+ allocated: 0
+ free: 13684
+ percent: 0
+ cpuCores:
+ units: '#'
+ max: 4
+ allocated: 0
+ free: 4
+ percent: 0
+ localStorage:
+ enabled: 1
+ name: Local (Default mountpoint)
+ storageType: 0
+ units: GB
+ max: 1000
+ allocated: 0
+ free: 1000
+ percent: 0
+ otherStorage: []
+ network:
+ total:
+ ipv4:
+ free: 0
+ - hypervisor:
+ id: 11
+ name: BHV 6
+ enabled: true
+ prohibit: false
+ accept: false
+ commissioned: true
+ resources:
+ servers:
+ units: '#'
+ max: 0
+ allocated: 1
+ free: -1
+ percent: null
+ memory:
+ units: MB
+ max: 27913
+ allocated: 1024
+ free: 26889
+ percent: 3.7
+ cpuCores:
+ units: '#'
+ max: 16
+ allocated: 1
+ free: 15
+ percent: 6.3
+ localStorage:
+ enabled: 1
+ name: Local (Default mountpoint)
+ storageType: 0
+ units: GB
+ max: 1000
+ allocated: 10
+ free: 990
+ percent: 1
+ otherStorage: []
+ network:
+ total:
+ ipv4:
+ free: 478
+ - hypervisor:
+ id: 12
+ name: BHV 7
+ enabled: true
+ prohibit: false
+ accept: false
+ commissioned: true
+ resources:
+ servers:
+ units: '#'
+ max: 0
+ allocated: 1
+ free: -1
+ percent: null
+ memory:
+ units: MB
+ max: 27913
+ allocated: 1024
+ free: 26889
+ percent: 3.7
+ cpuCores:
+ units: '#'
+ max: 16
+ allocated: 1
+ free: 15
+ percent: 6.3
+ localStorage:
+ enabled: 1
+ name: Local (Default mountpoint)
+ storageType: 0
+ units: GB
+ max: 1000
+ allocated: 10
+ free: 990
+ percent: 1
+ otherStorage: []
+ network:
+ total:
+ ipv4:
+ free: 478
+ - hypervisor:
+ id: 13
+ name: Ceph Hypervisor 1
+ enabled: true
+ prohibit: false
+ accept: false
+ commissioned: true
+ resources:
+ servers:
+ units: '#'
+ max: 0
+ allocated: 5
+ free: -5
+ percent: null
+ memory:
+ units: MB
+ max: 24000
+ allocated: 7168
+ free: 16832
+ percent: 29.9
+ cpuCores:
+ units: '#'
+ max: 64
+ allocated: 8
+ free: 56
+ percent: 12.5
+ localStorage:
+ enabled: 1
+ name: Local (Default mountpoint)
+ storageType: 1
+ units: GB
+ max: 100
+ allocated: 50
+ free: 50
+ percent: 50
+ otherStorage:
+ - id: 1
+ name: Ceph RBD
+ enabled: 0
+ path: null
+ units: GB
+ storageType: 2
+ isDatastore: true
+ max: 10000
+ allocated: 35
+ free: 9965
+ percent: 0.4
+ - id: 4
+ name: Ceph FS
+ enabled: 0
+ path: null
+ units: GB
+ storageType: 2
+ isDatastore: true
+ max: 558349385
+ allocated: 5
+ free: 558349380
+ percent: 0
+ network:
+ total:
+ ipv4:
+ free: 503
+ - hypervisor:
+ id: 14
+ name: Ceph Hypervisor 2
+ enabled: true
+ prohibit: false
+ accept: false
+ commissioned: true
+ resources:
+ servers:
+ units: '#'
+ max: 0
+ allocated: 3
+ free: -3
+ percent: null
+ memory:
+ units: MB
+ max: 24000
+ allocated: 3072
+ free: 20928
+ percent: 12.8
+ cpuCores:
+ units: '#'
+ max: 64
+ allocated: 3
+ free: 61
+ percent: 4.7
+ localStorage:
+ enabled: 1
+ name: Local (Default mountpoint)
+ storageType: 1
+ units: GB
+ max: 1000
+ allocated: 10
+ free: 990
+ percent: 1
+ otherStorage:
+ - id: 2
+ name: Ceph RBD
+ enabled: 0
+ path: null
+ units: GB
+ storageType: 2
+ isDatastore: true
+ max: 10000
+ allocated: 10
+ free: 9990
+ percent: 0.1
+ - id: 3
+ name: Ceph EC
+ enabled: 0
+ path: null
+ units: GB
+ storageType: 2
+ isDatastore: true
+ max: 13333333
+ allocated: 10
+ free: 13333323
+ percent: 0
+ network:
+ total:
+ ipv4:
+ free: 33
+ first_page_url: >-
+ https://192.168.3.11/api/v1/compute/hypervisors/groups/1/resources?page=1
+ from: 1
+ last_page: 1
+ last_page_url: >-
+ https://192.168.3.11/api/v1/compute/hypervisors/groups/1/resources?page=1
+ links:
+ - url: null
+ label: '« Previous'
+ active: false
+ - url: >-
+ https://192.168.3.11/api/v1/compute/hypervisors/groups/1/resources?page=1
+ label: '1'
+ active: true
+ - url: null
+ label: Next »
+ active: false
+ next_page_url: null
+ path: >-
+ https://192.168.3.11/api/v1/compute/hypervisors/groups/1/resources
+ per_page: 20
+ prev_page_url: null
+ to: 11
+ total: 11
+ headers: {}
+ '401':
+ $ref: '#/components/responses/401'
+ description: ''
+ security:
+ - bearer: []
+ /servers/{serverId}/firewall/{interface}/disable:
+ post:
+ summary: Disable firewall
+ deprecated: false
+ description: ''
+ tags:
+ - Servers/Network/Firewall
+ parameters:
+ - name: serverId
+ in: path
+ description: A valid server ID as shown in VirtFusion.
+ required: true
+ example: 69
+ schema:
+ type: integer
+ - name: interface
+ in: path
+ description: primary or secondary.
+ required: true
+ example: primary
+ schema:
+ type: string
+ - name: sync
+ in: query
+ description: >-
+ Synchronise and apply the defined rules. true|false Defaults to
+ false.
+ required: false
+ example: 'true'
+ schema:
+ type: boolean
+ responses:
+ '200':
+ description: ''
+ content:
+ application/octet-stream:
+ schema:
+ type: object
+ properties: {}
+ headers: {}
+ '401':
+ $ref: '#/components/responses/401'
+ description: ''
+ security:
+ - bearer: []
+ /servers/{serverId}/firewall/{interface}/enable:
+ post:
+ summary: Enable firewall
+ deprecated: false
+ description: ''
+ tags:
+ - Servers/Network/Firewall
+ parameters:
+ - name: serverId
+ in: path
+ description: A valid server ID as shown in VirtFusion.
+ required: true
+ example: 69
+ schema:
+ type: integer
+ - name: interface
+ in: path
+ description: primary or secondary.
+ required: true
+ example: primary
+ schema:
+ type: string
+ - name: sync
+ in: query
+ description: >-
+ Synchronise and apply the defined rules. true|false Defaults to
+ false.
+ required: false
+ example: 'true'
+ schema:
+ type: boolean
+ responses:
+ '200':
+ description: ''
+ content:
+ application/octet-stream:
+ schema:
+ type: object
+ properties: {}
+ headers: {}
+ '401':
+ $ref: '#/components/responses/401'
+ description: ''
+ security:
+ - bearer: []
+ /servers/{serverId}/firewall/{interface}:
+ get:
+ summary: Retrieve firewall
+ deprecated: false
+ description: ''
+ tags:
+ - Servers/Network/Firewall
+ parameters:
+ - name: serverId
+ in: path
+ description: A valid server ID as shown in VirtFusion.
+ required: true
+ example: 69
+ schema:
+ type: integer
+ - name: interface
+ in: path
+ description: primary or secondary.
+ required: true
+ example: primary
+ schema:
+ type: string
+ - name: sync
+ in: query
+ description: >-
+ Synchronise and apply the defined rules. true|false Defaults to
+ false.
+ required: false
+ example: 'true'
+ schema:
+ type: boolean
+ responses:
+ '200':
+ description: ''
+ content:
+ application/octet-stream:
+ schema:
+ type: object
+ properties: {}
+ example: |-
+ {
+ "data": {
+ "enabled": true,
+ "rules": []
+ }
+ }
+ headers: {}
+ '401':
+ $ref: '#/components/responses/401'
+ description: ''
+ security:
+ - bearer: []
+ /servers/{serverId}/firewall/{interface}/rules:
+ post:
+ summary: Apply firewall rulesets
+ deprecated: false
+ description: ''
+ tags:
+ - Servers/Network/Firewall
+ parameters:
+ - name: serverId
+ in: path
+ description: A valid server ID as shown in VirtFusion.
+ required: true
+ example: 69
+ schema:
+ type: integer
+ - name: interface
+ in: path
+ description: primary or secondary.
+ required: true
+ example: primary
+ schema:
+ type: string
+ - name: sync
+ in: query
+ description: >-
+ Synchronise and apply the defined rules. true|false Defaults to
+ false.
+ required: false
+ example: 'true'
+ schema:
+ type: boolean
+ requestBody:
+ content:
+ application/json:
+ schema:
+ type: object
+ properties:
+ rulesets:
+ type: array
+ items:
+ type: integer
+ description: >-
+ An array of ruleset IDs. All existing rules will be flushed
+ and the new rules applied. An empty array will flush all
+ rules.
+ required:
+ - rulesets
+ example:
+ rulesets:
+ - 1
+ - 2
+ - 5
+ responses:
+ '201':
+ description: ''
+ content:
+ application/octet-stream:
+ schema:
+ type: object
+ properties: {}
+ headers: {}
+ '401':
+ $ref: '#/components/responses/401'
+ description: ''
+ security:
+ - bearer: []
+ /servers/{serverId}/traffic/blocks:
+ post:
+ summary: Add a traffic block to a server
+ deprecated: false
+ description: ''
+ tags:
+ - Servers/Network/Traffic
+ parameters:
+ - name: serverId
+ in: path
+ description: A valid server ID as shown in VirtFusion.
+ required: true
+ example: 69
+ schema:
+ type: integer
+ requestBody:
+ content:
+ application/json:
+ schema:
+ type: object
+ properties:
+ month:
+ type: integer
+ description: >-
+ The numeric month as returned by the GET request
+ (available).
+ amount:
+ type: integer
+ description: An amount of traffic in GB.
+ required:
+ - month
+ - amount
+ example:
+ month: 2
+ amount: 100
+ responses:
+ '201':
+ description: ''
+ content:
+ application/octet-stream:
+ schema:
+ type: object
+ properties: {}
+ headers: {}
+ '401':
+ $ref: '#/components/responses/401'
+ description: ''
+ security:
+ - bearer: []
+ get:
+ summary: Retrieve a servers traffic blocks
+ deprecated: false
+ description: ''
+ tags:
+ - Servers/Network/Traffic
+ parameters:
+ - name: serverId
+ in: path
+ description: A valid server ID as shown in VirtFusion.
+ required: true
+ example: 69
+ schema:
+ type: integer
+ requestBody:
+ content:
+ application/json:
+ schema:
+ type: object
+ properties:
+ month:
+ type: integer
+ description: >-
+ The numeric month as returned by the GET request
+ (available).
+ amount:
+ type: integer
+ description: An amount of traffic in GB.
+ required:
+ - month
+ - amount
+ example: ''
+ responses:
+ '200':
+ description: ''
+ content:
+ application/json:
+ schema:
+ type: object
+ properties: {}
+ example:
+ data:
+ assigned:
+ - id: 2
+ current: false
+ month: 2
+ traffic: 100
+ start: '2025-02-20 00:00:00'
+ end: '2025-03-19 23:59:59'
+ added: '2025-01-20T15:08:15.000000Z'
+ available:
+ total: 25
+ current:
+ month: 1
+ start: '2025-01-20 00:00:00'
+ end: '2025-02-19 23:59:59'
+ months:
+ '1':
+ month: 1
+ start: '2025-01-20 00:00:00'
+ end: '2025-02-19 23:59:59'
+ '2':
+ month: 2
+ start: '2025-02-20 00:00:00'
+ end: '2025-03-19 23:59:59'
+ '3':
+ month: 3
+ start: '2025-03-20 00:00:00'
+ end: '2025-04-19 23:59:59'
+ '4':
+ month: 4
+ start: '2025-04-20 00:00:00'
+ end: '2025-05-19 23:59:59'
+ '5':
+ month: 5
+ start: '2025-05-20 00:00:00'
+ end: '2025-06-19 23:59:59'
+ '6':
+ month: 6
+ start: '2025-06-20 00:00:00'
+ end: '2025-07-19 23:59:59'
+ '7':
+ month: 7
+ start: '2025-07-20 00:00:00'
+ end: '2025-08-19 23:59:59'
+ '8':
+ month: 8
+ start: '2025-08-20 00:00:00'
+ end: '2025-09-19 23:59:59'
+ '9':
+ month: 9
+ start: '2025-09-20 00:00:00'
+ end: '2025-10-19 23:59:59'
+ '10':
+ month: 10
+ start: '2025-10-20 00:00:00'
+ end: '2025-11-19 23:59:59'
+ '11':
+ month: 11
+ start: '2025-11-20 00:00:00'
+ end: '2025-12-19 23:59:59'
+ '12':
+ month: 12
+ start: '2025-12-20 00:00:00'
+ end: '2026-01-19 23:59:59'
+ '13':
+ month: 13
+ start: '2026-01-20 00:00:00'
+ end: '2026-02-19 23:59:59'
+ '14':
+ month: 14
+ start: '2026-02-20 00:00:00'
+ end: '2026-03-19 23:59:59'
+ '15':
+ month: 15
+ start: '2026-03-20 00:00:00'
+ end: '2026-04-19 23:59:59'
+ '16':
+ month: 16
+ start: '2026-04-20 00:00:00'
+ end: '2026-05-19 23:59:59'
+ '17':
+ month: 17
+ start: '2026-05-20 00:00:00'
+ end: '2026-06-19 23:59:59'
+ '18':
+ month: 18
+ start: '2026-06-20 00:00:00'
+ end: '2026-07-19 23:59:59'
+ '19':
+ month: 19
+ start: '2026-07-20 00:00:00'
+ end: '2026-08-19 23:59:59'
+ '20':
+ month: 20
+ start: '2026-08-20 00:00:00'
+ end: '2026-09-19 23:59:59'
+ '21':
+ month: 21
+ start: '2026-09-20 00:00:00'
+ end: '2026-10-19 23:59:59'
+ '22':
+ month: 22
+ start: '2026-10-20 00:00:00'
+ end: '2026-11-19 23:59:59'
+ '23':
+ month: 23
+ start: '2026-11-20 00:00:00'
+ end: '2026-12-19 23:59:59'
+ '24':
+ month: 24
+ start: '2026-12-20 00:00:00'
+ end: '2027-01-19 23:59:59'
+ '25':
+ month: 25
+ start: '2027-01-20 00:00:00'
+ end: '2027-02-19 23:59:59'
+ headers: {}
+ '401':
+ $ref: '#/components/responses/401'
+ description: ''
+ security:
+ - bearer: []
+ /servers/{serverId}/traffic/blocks/{blockId}:
+ delete:
+ summary: Remove a traffic block from a server
+ deprecated: false
+ description: ''
+ tags:
+ - Servers/Network/Traffic
+ parameters:
+ - name: serverId
+ in: path
+ description: A valid server ID as shown in VirtFusion.
+ required: true
+ example: 69
+ schema:
+ type: integer
+ - name: blockId
+ in: path
+ description: >-
+ ID of an assigned traffic block as returned by the GET request
+ (assigned).
+ required: true
+ example: '1'
+ schema:
+ type: string
+ requestBody:
+ content:
+ application/json:
+ schema:
+ type: object
+ properties:
+ month:
+ type: integer
+ description: >-
+ The numeric month as returned by the GET request
+ (available).
+ amount:
+ type: integer
+ description: An amount of traffic in GB.
+ required:
+ - month
+ - amount
+ example:
+ month: 2
+ amount: 100
+ responses:
+ '204':
+ description: ''
+ content:
+ application/octet-stream:
+ schema:
+ type: object
+ properties: {}
+ headers: {}
+ '401':
+ $ref: '#/components/responses/401'
+ description: ''
+ security:
+ - bearer: []
+ /servers/{serverId}/modify/traffic:
+ put:
+ summary: Modify primary traffic allowance
+ deprecated: false
+ description: ''
+ tags:
+ - Servers/Network/Traffic
+ parameters:
+ - name: serverId
+ in: path
+ description: A valid server ID as shown in VirtFusion.
+ required: true
+ example: 69
+ schema:
+ type: integer
+ requestBody:
+ content:
+ application/json:
+ schema:
+ type: object
+ properties:
+ traffic:
+ type: string
+ description: Range of 0 - 999999999
+ required:
+ - traffic
+ example:
+ traffic: 1000
+ responses:
+ '201':
+ description: ''
+ content:
+ application/octet-stream:
+ schema:
+ type: object
+ properties: {}
+ headers: {}
+ '401':
+ $ref: '#/components/responses/401'
+ description: ''
+ security:
+ - bearer: []
+ /servers/{serverId}/networkWhitelist:
+ post:
+ summary: Add an address to the whitelist
+ deprecated: false
+ description: ''
+ tags:
+ - Servers/Network
+ parameters:
+ - name: serverId
+ in: path
+ description: A valid server ID as shown in VirtFusion.
+ required: true
+ example: 9
+ schema:
+ type: integer
+ requestBody:
+ content:
+ application/json:
+ schema:
+ type: object
+ properties:
+ interface:
+ type: string
+ description: Primary or secondary.
+ ip:
+ type: string
+ description: IPv4 or IPv6 address.
+ cidr:
+ type: integer
+ description: IPv4 or IPv6 CIDR.
+ required:
+ - interface
+ - ip
+ - cidr
+ example:
+ interface: primary
+ ip: 10.0.0.10
+ cidr: 32
+ responses:
+ '201':
+ description: ''
+ content:
+ application/octet-stream:
+ schema:
+ type: object
+ properties: {}
+ headers: {}
+ '401':
+ $ref: '#/components/responses/401'
+ description: ''
+ security:
+ - bearer: []
+ delete:
+ summary: Remove an address from the whitelist
+ deprecated: false
+ description: ''
+ tags:
+ - Servers/Network
+ parameters:
+ - name: serverId
+ in: path
+ description: A valid server ID as shown in VirtFusion.
+ required: true
+ example: 9
+ schema:
+ type: integer
+ requestBody:
+ content:
+ application/json:
+ schema:
+ type: object
+ properties:
+ interface:
+ type: string
+ description: Primary or secondary.
+ ip:
+ type: string
+ description: IPv4 or IPv6 address.
+ required:
+ - interface
+ - ip
+ example:
+ interface: primary
+ ip: 10.0.0.10
+ responses:
+ '204':
+ description: ''
+ content:
+ application/octet-stream:
+ schema:
+ type: object
+ properties: {}
+ headers: {}
+ '401':
+ $ref: '#/components/responses/401'
+ description: ''
+ security:
+ - bearer: []
+ /servers/{serverId}/ipv4Qty:
+ post:
+ summary: Add a quantity of IPv4 addresses
+ deprecated: false
+ description: ''
+ tags:
+ - Servers/Network
+ parameters:
+ - name: serverId
+ in: path
+ description: A valid server ID as shown in VirtFusion.
+ required: true
+ example: 9
+ schema:
+ type: integer
+ requestBody:
+ content:
+ application/json:
+ schema:
+ type: object
+ properties:
+ interface:
+ type: string
+ description: Primary or secondary.
+ quantity:
+ type: integer
+ description: Number of IPv4 addresses.
+ required:
+ - interface
+ - quantity
+ example:
+ interface: primary
+ quantity: 2
+ responses:
+ '200':
+ description: ''
+ content:
+ application/json:
+ schema:
+ type: object
+ properties: {}
+ example:
+ data:
+ - 192.168.4.36
+ - 192.168.4.37
+ headers: {}
+ '401':
+ $ref: '#/components/responses/401'
+ description: ''
+ security:
+ - bearer: []
+ /servers/{serverId}/ipv4:
+ post:
+ summary: Add an array of IPv4 addresses
+ deprecated: false
+ description: ''
+ tags:
+ - Servers/Network
+ parameters:
+ - name: serverId
+ in: path
+ description: A valid server ID as shown in VirtFusion.
+ required: true
+ example: 9
+ schema:
+ type: integer
+ requestBody:
+ content:
+ application/json:
+ schema:
+ type: object
+ properties:
+ ip:
+ type: array
+ items:
+ type: string
+ required:
+ - ip
+ example:
+ ip:
+ - 10.100.0.10
+ - 10.100.0.11
+ responses:
+ '204':
+ description: ''
+ content:
+ application/octet-stream:
+ schema:
+ type: object
+ properties: {}
+ headers: {}
+ '401':
+ $ref: '#/components/responses/401'
+ description: ''
+ security:
+ - bearer: []
+ delete:
+ summary: Remove an array of IPv4 addresses
+ deprecated: false
+ description: ''
+ tags:
+ - Servers/Network
+ parameters:
+ - name: serverId
+ in: path
+ description: A valid server ID as shown in VirtFusion.
+ required: true
+ example: 9
+ schema:
+ type: integer
+ requestBody:
+ content:
+ application/json:
+ schema:
+ type: object
+ properties:
+ ip:
+ type: array
+ items:
+ type: string
+ description: Valid IPv4 addresses.
+ description: Valid IPv4 addresses.
+ required:
+ - ip
+ example:
+ ip:
+ - 10.100.0.10
+ - 10.100.0.11
+ responses:
+ '204':
+ description: ''
+ content:
+ application/octet-stream:
+ schema:
+ type: object
+ properties: {}
+ headers: {}
+ '401':
+ $ref: '#/components/responses/401'
+ description: ''
+ security:
+ - bearer: []
+ /servers/{serverId}/power/boot:
+ post:
+ summary: Boot a server
+ deprecated: false
+ description: ''
+ tags:
+ - Servers/Power
+ parameters:
+ - name: serverId
+ in: path
+ description: A valid server ID as shown in VirtFusion.
+ required: true
+ example: 69
+ schema:
+ type: integer
+ responses:
+ '200':
+ description: ''
+ content:
+ application/json:
+ schema:
+ type: object
+ properties: {}
+ example:
+ data:
+ queueId: 171
+ headers: {}
+ '401':
+ $ref: '#/components/responses/401'
+ description: ''
+ security:
+ - bearer: []
+ /servers/{serverId}/power/shutdown:
+ post:
+ summary: Shutdown a server
+ deprecated: false
+ description: ''
+ tags:
+ - Servers/Power
+ parameters:
+ - name: serverId
+ in: path
+ description: A valid server ID as shown in VirtFusion.
+ required: true
+ example: 69
+ schema:
+ type: integer
+ responses:
+ '200':
+ description: ''
+ content:
+ application/json:
+ schema:
+ type: object
+ properties: {}
+ example:
+ data:
+ queueId: 171
+ headers: {}
+ '401':
+ $ref: '#/components/responses/401'
+ description: ''
+ security:
+ - bearer: []
+ /servers/{serverId}/power/restart:
+ post:
+ summary: Restart a server
+ deprecated: false
+ description: ''
+ tags:
+ - Servers/Power
+ parameters:
+ - name: serverId
+ in: path
+ description: A valid server ID as shown in VirtFusion.
+ required: true
+ example: 69
+ schema:
+ type: integer
+ responses:
+ '200':
+ description: ''
+ content:
+ application/json:
+ schema:
+ type: object
+ properties: {}
+ example:
+ data:
+ queueId: 171
+ headers: {}
+ '401':
+ $ref: '#/components/responses/401'
+ description: ''
+ security:
+ - bearer: []
+ /servers/{serverId}/power/poweroff:
+ post:
+ summary: Poweroff a server
+ deprecated: false
+ description: ''
+ tags:
+ - Servers/Power
+ parameters:
+ - name: serverId
+ in: path
+ description: A valid server ID as shown in VirtFusion.
+ required: true
+ example: 69
+ schema:
+ type: integer
+ responses:
+ '200':
+ description: ''
+ content:
+ application/json:
+ schema:
+ type: object
+ properties: {}
+ example:
+ data:
+ queueId: 171
+ headers: {}
+ '401':
+ $ref: '#/components/responses/401'
+ description: ''
+ security:
+ - bearer: []
+ /servers/{serverId}:
+ get:
+ summary: Retrieve a server
+ deprecated: false
+ description: ''
+ tags:
+ - Servers
+ parameters:
+ - name: serverId
+ in: path
+ description: A valid server ID as shown in VirtFusion.
+ required: true
+ example: 1
+ schema:
+ type: integer
+ - name: remoteState
+ in: query
+ description: Return the remote state of the server.
+ required: false
+ example: 'false'
+ schema:
+ type: boolean
+ responses:
+ '200':
+ description: ''
+ content:
+ application/json:
+ schema:
+ type: object
+ properties: {}
+ example:
+ data:
+ id: 69
+ ownerId: 1
+ hypervisorId: 6
+ arch: 1
+ name: Elliptical Way
+ selfService: 0
+ selfServiceSettings: []
+ hostname: null
+ commissionStatus: 3
+ uuid: b9fd9092-7200-4a24-96d4-76aedd664274
+ state: complete
+ migratable: true
+ timezone: _default
+ migrateLevel: 0
+ deleteLevel: 0
+ configLevel: 0
+ backupLevel: 0
+ elevated: false
+ elevateId: null
+ elevate: false
+ destroyable: true
+ rebuild: false
+ suspended: false
+ protected: false
+ buildFailed: false
+ primaryNetworkDhcp4: false
+ primaryNetworkDhcp6: false
+ built: '2025-01-15T15:00:49+00:00'
+ created: '2024-12-06T21:25:58+00:00'
+ updated: '2025-01-15T23:17:49+00:00'
+ traffic:
+ public:
+ countMethod: 1
+ currentPeriod:
+ start: '2025-01-06T00:00:00.000000Z'
+ end: '2025-02-05T23:59:59.999999Z'
+ limit: 20000
+ settings:
+ osTemplateInstall: true
+ osTemplateInstallId: 49
+ encryptedPassword: >-
+ eyJpdiI6IkNtT0ZmUEQ4Q2ZuNW5yUWVXZUcvWWc9PSIsInZhbHVlIjoibHJmMTNHZXpqV3lneFUrZEZ3eThSWEZVbVk5TDlBYTJQbXpPbFRvcmd0OD0iLCJtYWMiOiI1NTNhMGVmNzBlZWViZWI3NjkyMmYzYmM3NWFjMDY3ZTlmZmE4ZDE3NDI2YzliMjY0ODM4YzQzMDViMWY3MTI1IiwidGFnIjoiIn0=
+ backupPlan: null
+ uefi: false
+ uefiType: 0
+ cloudInit: true
+ cloudInitType: 1
+ config:
+ cloud.init:
+ on.network:
+ netplan_routes_v4: true
+ netplan_routes_v6: true
+ on.network.libvirtrouted:
+ netplan_routes_v4: true
+ netplan_routes_v6: true
+ on.all:
+ user.data:
+ runcmd:
+ - >-
+ DEBIAN_FRONTEND=noninteractive /usr/bin/apt-get
+ --option=Dpkg::Options::=--force-confold
+ --option=Dpkg::options::=--force-unsafe-io
+ --assume-yes --quiet update
+ - >-
+ DEBIAN_FRONTEND=noninteractive /usr/bin/apt-get
+ --option=Dpkg::Options::=--force-confold
+ --option=Dpkg::options::=--force-unsafe-io
+ --assume-yes --quiet install qemu-guest-agent
+ - /usr/bin/systemctl enable qemu-guest-agent
+ - /usr/bin/systemctl start qemu-guest-agent
+ - >-
+ DEBIAN_FRONTEND=noninteractive /usr/bin/apt-get
+ --option=Dpkg::Options::=--force-confold
+ --option=Dpkg::options::=--force-unsafe-io
+ --assume-yes --quiet dist-upgrade
+ on.password:
+ user.data:
+ runcmd:
+ - >-
+ /usr/bin/sed -i "s/#PermitRootLogin
+ prohibit-password/PermitRootLogin yes/g"
+ /etc/ssh/sshd_config
+ - >-
+ /usr/bin/sed -i "s/PasswordAuthentication
+ no/PasswordAuthentication yes/g"
+ /etc/ssh/sshd_config
+ - /usr/bin/systemctl restart sshd
+ on.sshkey:
+ user.data: []
+ userConfig: []
+ bootOrder:
+ - hd
+ - cdrom
+ tpmType: 0
+ networkBoot: false
+ bootMenu: 1
+ customISO: 1
+ securityDriver: 3
+ memBalloon:
+ model: 1
+ autoDeflate: 0
+ freePageReporting: 0
+ hyperv:
+ enabled: false
+ passthrough: false
+ relaxed: 0
+ vapic: 0
+ spinlocks: 0
+ vpindex: 0
+ runtime: 0
+ synic: 0
+ stimer: 0
+ reset: 0
+ vendorId: 0
+ frequencies: 0
+ reenlightenment: 0
+ tlbflush: 0
+ ipi: 0
+ evmcs: 0
+ vendorIdValue: KVM VM
+ spinlocksValue: 8191
+ clockEnabled: 0
+ extended:
+ cpuFlags:
+ topoext: '1'
+ svm: '1'
+ vmx: '1'
+ machineType: inherit
+ pciPorts: 16
+ resources:
+ memory: 1024
+ storage: 11
+ traffic: 20000
+ cpuCores: 2
+ cpu:
+ cores: 2
+ type: inherit
+ typeExact: host-model
+ shares: 1024
+ throttle: 0
+ topology:
+ enabled: false
+ sockets: 1
+ cores: 1
+ threads: 1
+ dies: 1
+ customXML:
+ domain:
+ xml: ''
+ enabled: false
+ os:
+ xml: ''
+ enabled: false
+ devices:
+ xml: ''
+ enabled: false
+ features:
+ xml: ''
+ enabled: false
+ clock:
+ xml: ''
+ enabled: false
+ cpuTune:
+ xml: ''
+ enabled: false
+ qemuCommandline: []
+ qemuAgent:
+ os:
+ screen: >-
+ iVBORw0KGgoAAAANSUhEUgAAAJYAAABTCAAAAABYT6E5AAAABGdBTUEAALGPC/xhBQAAACBjSFJNAAB6JgAAgIQAAPoAAACA6AAAdTAAAOpgAAA6mAAAF3CculE8AAAAAmJLR0QA/4ePzL8AAAAHdElNRQfpARESACl1ShbSAAAB0klEQVRo3u3VS2/TQBAA4NlZr9eP+N04DmmKqxZFBQRC6oX/wN9G4s6l0ENFKQQoSVw7deT4xaFpRTkgkNKIivksreWd9e5oVqsFIH+N/UmUrdrrz9//tMaMxK8rSaEgAICGGnAAuGpAv+sy8f3wUYnBttibW2Wz33X9qfayOJhVXvydx8nzYLDMjcfZTrZTdH1tlHXxQbWXx1PhGD23lYVymD7LDHt7st7SKWGlxSguWRvtvq70yWjqST8YnmilCxB9GzdD8zwcnZrClKGZIna81jzMpWE97OfKWH0DArj24shac7VYlJuJNy9qgd4ZDOczYaXmhcLNeXQKgyJBP80VN40mbmp1vixYkMu5lXU/gK1Vjc+PwVwY1dNj++Ndb+u/ga0eAAbXr1UA2E0XA3Z18m4N/3mW/0UkIu4Aoio6jAP05E3EtRGYcJABbjgn5dVRrTpJ6G0t+btIme1e5IbNB2VwVn8qpB4Xvfe6G791TjabFla5Y1iG7etfz3WXMVW1jGU7kBKeMDBs4H1w1c9qteFqMdaqDTZc1AtE1mDVqnUN2HC+NW5FC20/Kctmw0ndQ7fuZ0IIIYQQQgghhBBCCCGEEEIIIYQQQgghhNwTPwDlF4AYGPA7/gAAACV0RVh0ZGF0ZTpjcmVhdGUAMjAyNS0wMS0xN1QxODowMDo0MSswMDowMA+ZFQkAAAAldEVYdGRhdGU6bW9kaWZ5ADIwMjUtMDEtMTdUMTg6MDA6NDErMDA6MDB+xK21AAAAAElFTkSuQmCC
+ media:
+ isoMounted: false
+ isoType: local
+ isoName: ''
+ isoFilename: ''
+ isoUrl: ''
+ isoDownload: false
+ backupPlan:
+ id: null
+ name: null
+ vnc:
+ ip: 192.168.4.2
+ port: 5903
+ enabled: false
+ network:
+ interfaces:
+ - id: 69
+ order: 1
+ enabled: true
+ tag: 4618706442
+ name: ens3
+ type: public
+ driver: 1
+ processQueues: null
+ mac: 00:E7:FB:01:87:14
+ ipv4ToMac: null
+ ipv6ToMac: null
+ inTrafficCount: true
+ outTrafficCount: false
+ inAverage: 0
+ inPeak: 0
+ inBurst: 0
+ outAverage: 0
+ outPeak: 0
+ outBurst: 0
+ ipFilter: false
+ vlans: []
+ ipFilterType: null
+ portIsolated: false
+ ipv4_resolver_1: 1
+ ipv4_resolver_2: 2
+ ipv6_resolver_1: 1
+ ipv6_resolver_2: 2
+ networkProfile: 0
+ dhcpV4: 0
+ dhcpV6: 0
+ firewallEnabled: false
+ hypervisorNetwork: 6
+ isNat: false
+ nat: false
+ firewall: []
+ hypervisorConnectivity:
+ id: 6
+ type: simpleBridge
+ bridge: br0
+ mtu: null
+ primary: true
+ default: true
+ ipWhitelist: []
+ actions: []
+ ipv4:
+ - id: 33
+ order: 1
+ enabled: true
+ blockId: 1
+ address: 192.168.4.32
+ gateway: 192.168.4.1
+ netmask: 255.255.254.0
+ resolver1: 8.8.8.8
+ resolver2: 8.8.4.4
+ rdns: null
+ mac: null
+ - id: 36
+ order: 2
+ enabled: true
+ blockId: 1
+ address: 192.168.4.35
+ gateway: 192.168.4.1
+ netmask: 255.255.254.0
+ resolver1: 8.8.8.8
+ resolver2: 8.8.4.4
+ rdns: null
+ mac: null
+ ipv6:
+ - id: 502
+ block:
+ id: 5
+ name: V6 For BHV 1,3
+ order: 1
+ enabled: true
+ addresses: []
+ addressesDetailed: []
+ subnet: '2001:db8:abcd:12:2::'
+ cidr: 80
+ exhausted: false
+ gateway: 2001:db8:abcd:12::1
+ resolver1: 2001:4860:4860::8888
+ resolver2: 2001:4860:4860::8844
+ routeNet: false
+ - id: 505
+ block:
+ id: 5
+ name: V6 For BHV 1,3
+ order: 1
+ enabled: true
+ addresses: []
+ addressesDetailed: []
+ subnet: '2001:db8:abcd:12:5::'
+ cidr: 80
+ exhausted: false
+ gateway: 2001:db8:abcd:12::1
+ resolver1: 2001:4860:4860::8888
+ resolver2: 2001:4860:4860::8844
+ routeNet: false
+ - id: 506
+ block:
+ id: 5
+ name: V6 For BHV 1,3
+ order: 1
+ enabled: true
+ addresses: []
+ addressesDetailed: []
+ subnet: '2001:db8:abcd:12:6::'
+ cidr: 80
+ exhausted: false
+ gateway: 2001:db8:abcd:12::1
+ resolver1: 2001:4860:4860::8888
+ resolver2: 2001:4860:4860::8844
+ routeNet: false
+ - id: 507
+ block:
+ id: 5
+ name: V6 For BHV 1,3
+ order: 1
+ enabled: true
+ addresses: []
+ addressesDetailed: []
+ subnet: '2001:db8:abcd:12:7::'
+ cidr: 80
+ exhausted: false
+ gateway: 2001:db8:abcd:12::1
+ resolver1: 2001:4860:4860::8888
+ resolver2: 2001:4860:4860::8844
+ routeNet: false
+ - id: 508
+ block:
+ id: 5
+ name: V6 For BHV 1,3
+ order: 1
+ enabled: true
+ addresses: []
+ addressesDetailed: []
+ subnet: '2001:db8:abcd:12:8::'
+ cidr: 80
+ exhausted: false
+ gateway: 2001:db8:abcd:12::1
+ resolver1: 2001:4860:4860::8888
+ resolver2: 2001:4860:4860::8844
+ routeNet: false
+ - id: 509
+ block:
+ id: 5
+ name: V6 For BHV 1,3
+ order: 1
+ enabled: true
+ addresses: []
+ addressesDetailed: []
+ subnet: '2001:db8:abcd:12:9::'
+ cidr: 80
+ exhausted: false
+ gateway: 2001:db8:abcd:12::1
+ resolver1: 2001:4860:4860::8888
+ resolver2: 2001:4860:4860::8844
+ routeNet: false
+ secondaryInterfaces:
+ - id: 4
+ enabled: true
+ order: 1
+ tag: 3933491695
+ name: eth1
+ type: private
+ driver: 1
+ processQueues: null
+ mac: 00:F0:4A:C6:3F:08
+ ipv4ToMac: null
+ ipv6ToMac: null
+ inTrafficCount: true
+ outTrafficCount: false
+ inAverage: 0
+ inPeak: 0
+ inBurst: 0
+ outAverage: 0
+ outPeak: 0
+ outBurst: 0
+ ipFilter: true
+ vlans: []
+ ipFilterType: 4-6
+ portIsolated: false
+ ipv4_resolver_1: 1
+ ipv4_resolver_2: 2
+ ipv6_resolver_1: 1
+ ipv6_resolver_2: 2
+ networkProfile: 0
+ dhcpV4: 0
+ dhcpV6: 0
+ firewallEnabled: false
+ hypervisorNetwork: 6
+ isNat: false
+ nat: false
+ firewall: []
+ hypervisorConnectivity:
+ id: 6
+ type: simpleBridge
+ bridge: br0
+ mtu: null
+ primary: true
+ default: true
+ ipWhitelist: []
+ actions: []
+ ipv4:
+ - id: 34
+ order: 1
+ enabled: true
+ address: 192.168.4.33
+ gateway: 192.168.4.1
+ netmask: 255.255.254.0
+ resolver1: 8.8.8.8
+ resolver2: 8.8.4.4
+ rdns: null
+ mac: null
+ - id: 35
+ order: 2
+ enabled: true
+ address: 192.168.4.34
+ gateway: 192.168.4.1
+ netmask: 255.255.254.0
+ resolver1: 8.8.8.8
+ resolver2: 8.8.4.4
+ rdns: null
+ mac: null
+ ipv6:
+ - id: 503
+ block:
+ id: 5
+ name: V6 For BHV 1,3
+ order: 1
+ enabled: true
+ addresses: []
+ addressesDetailed: []
+ subnet: '2001:db8:abcd:12:3::'
+ cidr: 80
+ exhausted: false
+ gateway: 2001:db8:abcd:12::1
+ resolver1: 2001:4860:4860::8888
+ resolver2: 2001:4860:4860::8844
+ routeNet: false
+ - id: 504
+ block:
+ id: 5
+ name: V6 For BHV 1,3
+ order: 1
+ enabled: true
+ addresses: []
+ addressesDetailed: []
+ subnet: '2001:db8:abcd:12:4::'
+ cidr: 80
+ exhausted: false
+ gateway: 2001:db8:abcd:12::1
+ resolver1: 2001:4860:4860::8888
+ resolver2: 2001:4860:4860::8844
+ routeNet: false
+ storage:
+ - _id: 80
+ id: 1
+ cache: null
+ bus: null
+ capacity: 11
+ drive: a
+ datastoreDiskId: null
+ filesystem: null
+ iops:
+ read: null
+ write: null
+ bytes:
+ read: null
+ write: null
+ type: qcow2
+ profile: 1
+ status: 3
+ enabled: true
+ primary: true
+ created: '2024-12-06T21:25:58+00:00'
+ updated: '2025-01-07T22:26:16+00:00'
+ datastore: []
+ name: b9fd9092-7200-4a24-96d4-76aedd664274_1
+ filename: b9fd9092-7200-4a24-96d4-76aedd664274_1.img
+ hypervisorStorageId: null
+ local: true
+ locationType: mountpoint
+ path: /home/vf-data/disk
+ hypervisorAssets: []
+ hypervisor:
+ id: 6
+ ip: 192.168.4.2
+ hostname: null
+ port: 8892
+ maintenance: false
+ groupId: 2
+ group:
+ name: Test
+ icon: null
+ timezone: Europe/London
+ forceIPv6: false
+ vncListenType: 1
+ displayName: null
+ cpuSet: null
+ nfType: 4
+ backupStorageType: 2
+ defaultDiskType: inherit
+ defaultDiskCacheType: inherit
+ defaultCPU: inherit
+ defaultMachineType: inherit
+ created: '2024-03-30T09:53:38+00:00'
+ updated: '2024-12-06T21:25:54+00:00'
+ name: BHV 1
+ dataDir: /home/vf-data
+ resources:
+ servers:
+ units: '#'
+ max: 0
+ allocated: 5
+ free: -5
+ percent: null
+ memory:
+ units: MB
+ max: 29419
+ allocated: 7168
+ free: 22251
+ percent: 24.4
+ cpuCores:
+ units: '#'
+ max: 128
+ allocated: 6
+ free: 122
+ percent: 4.7
+ localStorage:
+ enabled: 1
+ name: Local (Default mountpoint)
+ storageType: 1
+ units: GB
+ max: 1000
+ allocated: 141
+ free: 859
+ percent: 14.1
+ otherStorage: []
+ owner:
+ id: 1
+ admin: true
+ extRelationId: null
+ name: Jon Doe
+ email: jon@doe.com
+ timezone: Europe/London
+ suspended: false
+ twoFactorAuth: false
+ created: '2024-03-12T22:22:09+00:00'
+ updated: '2025-01-15T11:01:18+00:00'
+ sshKeys:
+ - id: 1
+ ownerId: 1
+ type: OpenSSH
+ name: Test Key
+ public: >-
+ ssh-rsa
+ AAAAB3NzaC1yc2EAAAADAQABAAACAQC+JdL4fWELBWGAknSu0PwVpDDOlORxy9z7eVnZphZXBzYLMnux+ZogVLns6+O6NDE8JmWvP9RIg3SIga7RDOkW9UCdLzRu0jF2ALL7CK1huo1Ih0PDM9ZbFDy2Fd7a4DTvUX6923fQyW0PWRtyL11R4c9NUqzejKp5kW8vHfPQjzwb1hGIKvkSYkI0Auq4JJhlvjjnoK7Z8t5mpDrVfNTrVqevPgsW5Xwnq8R+02XywrY+Q/wnpxDs4Ujb2aA61A0x5J0xcZQpTQHoJNj77J3VmPI7Ry7Q8hPbTSLGZbN+gODr0lOaL5TdbvM3bnus5JvoqgRoszzPcTiNMZAe3v9UM8hiXise54b8rsc2M9MQ4olPu7TrROZbcw+9q4m6cV+dfVU/NRFkf27YRa4oZNKehHsMiupDyoISgSl4qSB8YXAWsX03oC/gzpB2YJIqEL1Y/SmKYEhgr0cplkvGZy6C/Q9cJHyHlMPtEBPexgcjXC9QrDZ4n2cmde3TuSRMctawcat7Nuq08C8fGHaGHr8iAeage3o/ODVOt0rhBu69PknzQeVBdlwK3+p1dH6PnMzNNBhWyNZT/NqB2eS6K8lYpOQ47byXPwYsRLvStUjpZRdikOT7D31T5g8FwOThQ+6WX+xfMD7CSLsSKCn/FhlinbVbG2IhCLH3B30Akw5bUw==
+ enabled: true
+ created: '2024-03-13T20:28:32+00:00'
+ sharedUsers: []
+ tasks:
+ active: false
+ lastOn: '2025-01-15 15:00:49'
+ actions:
+ pending: []
+ remoteState: false
+ headers: {}
+ '401':
+ $ref: '#/components/responses/401'
+ description: ''
+ security:
+ - bearer: []
+ delete:
+ summary: Delete a server
+ deprecated: false
+ description: ''
+ tags:
+ - Servers
+ parameters:
+ - name: serverId
+ in: path
+ description: A valid server ID as shown in VirtFusion.
+ required: true
+ example: 9
+ schema:
+ type: integer
+ - name: delay
+ in: query
+ description: >-
+ How many minutes the system should wait before deleting the server.
+ (0-43800)
+ required: false
+ example: 5
+ schema:
+ type: integer
+ responses:
+ '204':
+ description: ''
+ content:
+ application/octet-stream:
+ schema:
+ type: object
+ properties: {}
+ headers: {}
+ '401':
+ $ref: '#/components/responses/401'
+ description: ''
+ security:
+ - bearer: []
+ /servers/{serverId}/backups/plan/{planId}:
+ put:
+ summary: Add, remove or modify a backup plan
+ deprecated: false
+ description: ''
+ tags:
+ - Servers
+ parameters:
+ - name: serverId
+ in: path
+ description: A valid server ID as shown in VirtFusion.
+ required: true
+ example: 9
+ schema:
+ type: integer
+ - name: planId
+ in: path
+ description: >-
+ A valid backup plan ID as shown in VirtFusion. A value of 0 (zero)
+ will remove the plan.
+ required: true
+ example: 0
+ schema:
+ type: integer
+ responses:
+ '201':
+ description: ''
+ content:
+ application/octet-stream:
+ schema:
+ type: object
+ properties: {}
+ headers: {}
+ '401':
+ $ref: '#/components/responses/401'
+ description: ''
+ security:
+ - bearer: []
+ /servers/{serverId}/build:
+ post:
+ summary: Build a server
+ deprecated: false
+ description: ''
+ tags:
+ - Servers
+ parameters:
+ - name: serverId
+ in: path
+ description: A valid server ID as shown in VirtFusion.
+ required: true
+ example: 9
+ schema:
+ type: integer
+ requestBody:
+ content:
+ application/json:
+ schema:
+ type: object
+ properties:
+ operatingSystemId:
+ type: integer
+ description: A valid operating system template ID.
+ name:
+ type: string
+ description: Server name.
+ hostname:
+ type: string
+ description: Server Hostname.
+ sshKeys:
+ type: array
+ items:
+ type: integer
+ description: An array of SSH keys.
+ vnc:
+ type: boolean
+ description: Enable/disable.
+ ipv6:
+ type: boolean
+ description: Enable/disable.
+ email:
+ type: boolean
+ description: Enable/disable.
+ swap:
+ type: number
+ description: Values of 256, 512, 768, 1, 1.5, 2, 3, 4, 5,6 8
+ required:
+ - operatingSystemId
+ example:
+ operatingSystemId: 1
+ name: server 1
+ hostname: server1.domain.com
+ sshKeys:
+ - 1
+ - 2
+ - 3
+ - 4
+ vnc: false
+ ipv6: false
+ swap: 512
+ email: true
+ responses:
+ '200':
+ description: ''
+ content:
+ application/json:
+ schema:
+ type: object
+ properties: {}
+ example:
+ data:
+ id: 9
+ ownerId: 3
+ hypervisorId: 6
+ arch: 1
+ name: server 1
+ selfService: 0
+ selfServiceSettings: []
+ hostname: server1.domain.com
+ commissionStatus: 1
+ uuid: 5de5a89b-b707-41bf-a051-7af1a4e67795
+ state: queued
+ migratable: true
+ timezone: _default
+ migrateLevel: 0
+ deleteLevel: 0
+ configLevel: 1
+ backupLevel: 0
+ elevated: false
+ elevateId: null
+ elevate: false
+ destroyable: true
+ rebuild: false
+ suspended: false
+ protected: false
+ buildFailed: false
+ primaryNetworkDhcp4: false
+ primaryNetworkDhcp6: false
+ built: '2024-11-29T19:32:17+00:00'
+ created: '2024-04-11T17:22:19+00:00'
+ updated: '2025-01-20T13:32:44+00:00'
+ traffic:
+ public:
+ countMethod: 1
+ currentPeriod:
+ start: '2025-01-11T00:00:00.000000Z'
+ end: '2025-02-10T23:59:59.999999Z'
+ limit: 200
+ settings:
+ osTemplateInstall: true
+ osTemplateInstallId: 1
+ encryptedPassword: >-
+ eyJpdiI6IjVsdWVBMzNNWnVXZzlYMjhlTUMzSXc9PSIsInZhbHVlIjoiT2E3SDNmVTVCOW1GK1RCd0h6YjZwZnIva1ZHbU9rQU1VL1hsQSthcUVRYz0iLCJtYWMiOiIzMzdmNjkxOTcwMjkxYmM2ZmNlMjgyMzdkMTQzMDY2OWY1ZTBlYjExYzA1MjdjMzZmMTU1ZTVlMGFiMWY2ZmJlIiwidGFnIjoiIn0=
+ backupPlan: null
+ uefi: false
+ uefiType: 0
+ cloudInit: true
+ cloudInitType: 1
+ config:
+ cloud.init:
+ on.all:
+ user.data:
+ runcmd:
+ - >-
+ DEBIAN_FRONTEND=noninteractive /usr/bin/apt-get
+ --option=Dpkg::Options::=--force-confold
+ --option=Dpkg::options::=--force-unsafe-io
+ --assume-yes --quiet update
+ - >-
+ DEBIAN_FRONTEND=noninteractive /usr/bin/apt-get
+ --option=Dpkg::Options::=--force-confold
+ --option=Dpkg::options::=--force-unsafe-io
+ --assume-yes --quiet dist-upgrade
+ on.password:
+ user.data: []
+ on.sshkey:
+ user.data: []
+ on.allpre:
+ user.data: []
+ on.allpost:
+ user.data: []
+ on.network: []
+ on.network.libvirtrouted: []
+ userConfig: []
+ bootOrder:
+ - hd
+ - cdrom
+ tpmType: 0
+ networkBoot: false
+ bootMenu: 1
+ customISO: 1
+ securityDriver: 3
+ memBalloon:
+ model: 1
+ autoDeflate: 0
+ freePageReporting: 0
+ hyperv:
+ enabled: false
+ passthrough: false
+ relaxed: 0
+ vapic: 0
+ spinlocks: 0
+ vpindex: 0
+ runtime: 0
+ synic: 0
+ stimer: 0
+ reset: 0
+ vendorId: 0
+ frequencies: 0
+ reenlightenment: 0
+ tlbflush: 0
+ ipi: 0
+ evmcs: 0
+ vendorIdValue: KVM VM
+ spinlocksValue: 8191
+ clockEnabled: 0
+ extended:
+ cpuFlags:
+ topoext: '1'
+ svm: '1'
+ vmx: '1'
+ machineType: inherit
+ pciPorts: 16
+ resources:
+ memory: 2048
+ storage: 10
+ traffic: 200
+ cpuCores: 1
+ decryptedPassword: uv1dmfUUaENhNpbrGUwD
+ cpu:
+ cores: 1
+ type: inherit
+ typeExact: host-model
+ shares: 1024
+ throttle: 0
+ topology:
+ enabled: false
+ sockets: 1
+ cores: 1
+ threads: 1
+ dies: 1
+ customXML:
+ domain:
+ xml: ''
+ enabled: false
+ os:
+ xml: ''
+ enabled: false
+ devices:
+ xml: ''
+ enabled: false
+ features:
+ xml: ''
+ enabled: false
+ clock:
+ xml: ''
+ enabled: false
+ cpuTune:
+ xml: ''
+ enabled: false
+ qemuCommandline: []
+ qemuAgent:
+ os:
+ screen: >-
+ iVBORw0KGgoAAAANSUhEUgAAAJYAAABTCAAAAABYT6E5AAAABGdBTUEAALGPC/xhBQAAACBjSFJNAAB6JgAAgIQAAPoAAACA6AAAdTAAAOpgAAA6mAAAF3CculE8AAAAAmJLR0QA/4ePzL8AAAAHdElNRQfpARESACeS8jvVAAAI2klEQVRo3u2W+XMjxRXHX3fPfWhG1+i0ZMmXbK3t9dq7bFg2ARKgSFWSn5If8+/lx1xFhSpCQSWppVhYWKB2F+y1JV+yLI1kndbcnR98LBACJGxCUjWfH3r6eN39puf1dx7A/yboiy2EvsHiv+wY+vL2CP2jQ+irjP8TbpP5fiCJtm4BAEBclUURuzFxotnnBqkgZ3uQcCXncs75WAy5oHiKA1x8/NTd+mW8LN0+WIJl+UpdfJH91XSEZZfw8Kq4kpEzKRNeOq50JzMvNEupVCET1QXlWr8qzsbL6Yimt4Wft18YFyPVLUyfrlu43YOupbpGjFOxY6p/enjK+1uFiEvkhK4olLZygQ3RFiCRU9GV6bEfYYPMItUzquWC1QgOE+t4jIKnfVzoc+VllXnSJojBZ/0EkzkdAADiC5dzEKgVljxtp+B7u2j/jldf0YkQAMIIEAA6a6CzKuAvzkJftdjnrjWCy4P+v4PMiWogYJEiCiJKcpRhPRYvH0M8PyKEpAF7qgvxEqpET0uZdDw1kxitNmDej8/qs8XJdXupX+36PEuxgOcHPGb8dTNAJQcBEhnis/jK8SqT4qvl0TzKEOzT8myQcCp9lsELHYoUwOmILfqYQtFhPACJeFwgsMwvXkt0K9ulZqR+utB1pMLuTGv6D4WPQJzOR+SP5eHGvZHjxCIuX0nWb7mDZnKYzX/Argz6aE7e28jCas2ouavxMY7UZqXF41t75JjfnrlmR3aym1cb0380nln4iIoeqVTu+2TkBFOJibaoLh3EttYOBten94rd0SIF+28wx/3ojd1MjuWn6+oeuv5YxPqpiFZ+d1oKHP7Zjy0ibcfrVEhhuP5ZA0XNoQNCGmEX+R4JxH6kn6qxmk5NjAOB7BtDbT/IqZpeGzpKYGdc9zHNrW/bvE13ZpBSSzhil9PGNBiJXD8YBRm9obEI92MtzjoxtF7iOELqt5k3YbabOuwn27pBEWwCQBIAgOGRDgAgAgBAAqCogs6KZQbIVKpgQASMtB6bikhiVMMkDyAZBT4mFjQgaSEDEMsCaLiQTRvGVIZTzyJEOo8U+Un4ZwDw2R6QhMRZhRVnzkdljTurMQvjsmGMpffLD5eJyzXSZq6Rv3PjzUrWVKIdV+Pyo6XtOGcS+niZC47y629n2zOmJWfFzOl7cw9yGXmi3FvT4ny2EWTvFE90nLEblqEi20/sbXyw0L3y+gGsY5dr5FrZw/w7a03IZG38kfTcVqF+e0dqGW97r2xXYmKz/OlueeGw0oS/stfrpLiV9GVpemTiKvVVijYLrHrEcO12lulIzYEhD9yHk07U5IaPcde+54+YftTeNNSj/gFnW3Z7pr/XT7HKfcVvY3u4qb3Lb8aVelRMJd3+qUSd/BHLLgW+SmFzio02pAMqq76yz890XQFPbNJq00BwJG2ojExHGwnkpImnTwAAgJeqAHr5QlYIYITh24LZiw+EgMCliqW1Swu99Hl75tusSSpsOnZzKxZkYZBOpNg17Tj949rK6Lqf68C8khZvdmZJKlcYzbHRuDAmr4g8X4JYOYpzsmqgCf9M74dWWVZjyrr7/PECGlFIGLKsawiKXFJ4dnfNzfr+CBbUqHSrUZDXmlWa1LT+17tFmR/UjODALaamu9Tfv7IzdxeUnp3d5lb9rWDl0407tJrbXH0kiaksy9uvJfLuSoNGZu7rp1nZjR/+BfEeXH3u8YHgjTVlzmbm644xFxDekUfiZum+IEeda17wG7T8aKXuz916Q5aTuhawPTqiiAKiAAgoAKDz4vyJFAcoscVo8S4b2AwIjsNhSx2KE3kM8lgdE4It1uYDDMhFHpJtBp8ifiJ5GAU+9YHxo2NPcnxEJc/iLKMFPOvgpYnUO3KkgeBzNsypd0E61Xss79qCLXt4ggD5X6/y1WE5zgdLFIu5GTtHutySuSznkonsSTXpFA6Bd1wO4rE+F7ipTAdEfWITzBA78PJuQivac2U0vnZQMRnHdvURO8b6CKKJrU6r64goOfFdLJj7DMqK5uziTtUJlrkWXc1OJ4xxtf9MoqDNCgUxUTCfHVJ1dUEMKuxaC9YJUzrSyY031Qr6ZKpfYoXu1IYo1o14JhlP9gbpR+pLbcFn28lFNKX3Np0T5sUh7/SJnfXb7YXl+1or2S7G1j/J1yWLWeJUOf5Z6v1kqzwdkbaslvvCgHcHxMp57U5Ex+QnU3l1K78guGytqGtV245PbY+NuffVqSKmv66n7ICmGvaLlH+EskHSFCdyN3JKbBb8oZAKuBaM82MBbIc/YMvWjQf+gRH4kjQ5aeHbbsOX/CabICOX4U8UT28ntrM9xWTGgtouOorfD07EBCYr7Ye95+2OzVltMY57VBj0Zn1mxIwTAXInLkqM1ZOgVzLdQcniXS44LjdjJ5GRfmLHMd2DDAFIAoh8tJwvFNKz5zIL7IXeKsLFB4+IDNEh+STpi3MgKQAAYFwItwwAGs8DiOhc3aPcxZ8EACJnCh9/EkQKAIAhAyg8gHKpIjc/nt9+9RNWHOzM7pTt6WNEpWgPRE/yWS8/+T0kNrq8M44OVW8Ajmn3+GsTgkbABUjYTTbr0Z9+Jh0YvcXGWD1+QDfeXZzceIdwZIw/YF5uprenTVfNdyZC5LfqzU72w0b+Rh33JJQatDOb27klK7ASd9WpoPTnuPvqh6mD0m7hdRrAy0f4SOIGbwXdmsvdc997cHfHsGosexXYLcvtOX5KTrGMxwvbPSzh0cTxIeaZ4AvM5ogVSN+hVo2YAiPUvKG7Q+HEmwQO9bdPfQLM8MDBDBOYJu37+0ApZzZp/xB3BQ72rclol1Lqyfsnkw5Azez4Rx5nHvLHsXkK9uMnwgoAQM5SRrF0mUriRQm+E/9qDooxAACq9q6+NWXN3FlqKm6w++UlKVxI3Rf66T9rfKP1txwiP5sQ8bl6sl1gV8vikWh/+RW/n4wbqWOGgCW6rIOWrS1sf/clQ0JCQkJCQkJCQkJCQkJCQkJCQkJCQkJCQkJCQkJCQp4afwdRMMFLNhfN2wAAACV0RVh0ZGF0ZTpjcmVhdGUAMjAyNS0wMS0xN1QxODowMDozOSswMDowMDazUncAAAAldEVYdGRhdGU6bW9kaWZ5ADIwMjUtMDEtMTdUMTg6MDA6MzkrMDA6MDBH7urLAAAAAElFTkSuQmCC
+ media:
+ isoMounted: false
+ isoType: local
+ isoName: ''
+ isoFilename: ''
+ isoUrl: ''
+ isoDownload: false
+ backupPlan:
+ id: null
+ name: null
+ vnc:
+ ip: 192.168.4.2
+ port: 5901
+ enabled: false
+ network:
+ interfaces:
+ - id: 9
+ order: 1
+ enabled: true
+ tag: 4238114467
+ name: eth0
+ type: public
+ driver: 1
+ processQueues: null
+ mac: 00:C3:BA:23:37:B3
+ ipv4ToMac: null
+ ipv6ToMac: null
+ inTrafficCount: true
+ outTrafficCount: false
+ inAverage: 0
+ inPeak: 0
+ inBurst: 0
+ outAverage: 0
+ outPeak: 0
+ outBurst: 0
+ ipFilter: true
+ vlans: []
+ ipFilterType: '4'
+ portIsolated: false
+ ipv4_resolver_1: 1
+ ipv4_resolver_2: 2
+ ipv6_resolver_1: 1
+ ipv6_resolver_2: 2
+ networkProfile: 0
+ dhcpV4: 0
+ dhcpV6: 0
+ firewallEnabled: false
+ hypervisorNetwork: 6
+ isNat: false
+ nat: false
+ firewall: []
+ hypervisorConnectivity:
+ id: 6
+ type: simpleBridge
+ bridge: br0
+ mtu: null
+ primary: true
+ default: true
+ ipWhitelist:
+ - id: 1
+ type: 4
+ ip: 100.100.100.100
+ mask: 32
+ - id: 2
+ type: 4
+ ip: 10.0.0.10
+ mask: 32
+ actions: []
+ ipv4:
+ - id: 22
+ order: 1
+ enabled: true
+ blockId: 1
+ address: 192.168.4.21
+ gateway: 192.168.4.1
+ netmask: 255.255.254.0
+ resolver1: 8.8.8.8
+ resolver2: 8.8.4.4
+ rdns: null
+ mac: null
+ - id: 37
+ order: 2
+ enabled: true
+ blockId: 1
+ address: 192.168.4.36
+ gateway: 192.168.4.1
+ netmask: 255.255.254.0
+ resolver1: 8.8.8.8
+ resolver2: 8.8.4.4
+ rdns: null
+ mac: null
+ - id: 38
+ order: 3
+ enabled: true
+ blockId: 1
+ address: 192.168.4.37
+ gateway: 192.168.4.1
+ netmask: 255.255.254.0
+ resolver1: 8.8.8.8
+ resolver2: 8.8.4.4
+ rdns: null
+ mac: null
+ ipv6: []
+ secondaryInterfaces: []
+ storage:
+ - _id: 11
+ id: 1
+ cache: null
+ bus: null
+ capacity: 10
+ drive: a
+ datastoreDiskId: null
+ filesystem: null
+ iops:
+ read: null
+ write: null
+ bytes:
+ read: null
+ write: null
+ type: qcow2
+ profile: 0
+ status: 3
+ enabled: true
+ primary: true
+ created: '2024-04-11T17:22:19+00:00'
+ updated: '2024-04-11T17:22:19+00:00'
+ datastore: []
+ name: 5de5a89b-b707-41bf-a051-7af1a4e67795_1
+ filename: 5de5a89b-b707-41bf-a051-7af1a4e67795_1.img
+ hypervisorStorageId: null
+ local: true
+ locationType: mountpoint
+ path: /home/vf-data/disk
+ hypervisorAssets: []
+ hypervisor:
+ id: 6
+ ip: 192.168.4.2
+ hostname: null
+ port: 8892
+ maintenance: false
+ groupId: 2
+ group:
+ name: Test
+ icon: null
+ timezone: Europe/London
+ forceIPv6: false
+ vncListenType: 1
+ displayName: null
+ cpuSet: null
+ nfType: 4
+ backupStorageType: 2
+ defaultDiskType: inherit
+ defaultDiskCacheType: inherit
+ defaultCPU: inherit
+ defaultMachineType: inherit
+ created: '2024-03-30T09:53:38+00:00'
+ updated: '2024-12-06T21:25:54+00:00'
+ name: BHV 1
+ dataDir: /home/vf-data
+ resources:
+ servers:
+ units: '#'
+ max: 0
+ allocated: 5
+ free: -5
+ percent: null
+ memory:
+ units: MB
+ max: 29419
+ allocated: 7168
+ free: 22251
+ percent: 24.4
+ cpuCores:
+ units: '#'
+ max: 128
+ allocated: 6
+ free: 122
+ percent: 4.7
+ localStorage:
+ enabled: 1
+ name: Local (Default mountpoint)
+ storageType: 1
+ units: GB
+ max: 1000
+ allocated: 141
+ free: 859
+ percent: 14.1
+ otherStorage: []
+ owner:
+ id: 3
+ admin: false
+ extRelationId: 1
+ name: jon Doe
+ email: jon@doe.com
+ timezone: Europe/London
+ suspended: false
+ twoFactorAuth: false
+ created: '2025-01-20T12:48:20+00:00'
+ updated: '2025-01-20T13:00:38+00:00'
+ sshKeys: []
+ sharedUsers: []
+ tasks:
+ active: true
+ lastOn: '2024-11-29 19:32:17'
+ actions:
+ pending: []
+ headers: {}
+ '401':
+ $ref: '#/components/responses/401'
+ description: ''
+ security:
+ - bearer: []
+ /servers/{serverId}/package/{packageId}:
+ put:
+ summary: Change a server package
+ deprecated: false
+ description: ''
+ tags:
+ - Servers
+ parameters:
+ - name: serverId
+ in: path
+ description: A valid server ID as shown in VirtFusion.
+ required: true
+ example: 9
+ schema:
+ type: integer
+ - name: packageId
+ in: path
+ description: A valid package ID as shown in VirtFusion.
+ required: true
+ example: 1
+ schema:
+ type: integer
+ requestBody:
+ content:
+ application/json:
+ schema:
+ type: object
+ properties:
+ backupPlan:
+ type: boolean
+ cpu:
+ type: boolean
+ memory:
+ type: boolean
+ primaryDiskReadIOPS:
+ type: boolean
+ primaryDiskReadThroughput:
+ type: boolean
+ primaryDiskSize:
+ type: boolean
+ primaryDiskWriteIOPS:
+ type: boolean
+ primaryDiskWriteThroughput:
+ type: boolean
+ primaryNetworkInboundSpeed:
+ type: boolean
+ primaryNetworkOutboundSpeed:
+ type: boolean
+ primaryNetworkTraffic:
+ type: boolean
+ example:
+ backupPlan: true
+ cpu: true
+ memory: true
+ primaryDiskReadIOPS: false
+ primaryDiskReadThroughput: false
+ primaryDiskSize: true
+ primaryDiskWriteIOPS: true
+ primaryDiskWriteThroughput: true
+ primaryNetworkInboundSpeed: true
+ primaryNetworkOutboundSpeed: true
+ primaryNetworkTraffic: true
+ responses:
+ '200':
+ description: ''
+ content:
+ application/json:
+ schema:
+ type: object
+ properties: {}
+ example:
+ info:
+ - CPU cores not updated. It matches the current value
+ - >-
+ primary disk not updated. It either matches or is lower than
+ the current value
+ - traffic not updated. It matches the current value
+ - >-
+ primary network speed inbound not updated. It matches the
+ current value
+ - >-
+ primary network speed outbound not updated. It matches the
+ current value
+ - write IOPS not updated. It matches the current value
+ - write bytes/sec not updated. It matches the current value
+ headers: {}
+ '401':
+ $ref: '#/components/responses/401'
+ description: ''
+ security:
+ - bearer: []
+ /servers:
+ post:
+ summary: Create a server
+ deprecated: false
+ description: ''
+ tags:
+ - Servers
+ parameters:
+ - name: dryRun
+ in: query
+ description: >-
+ Test to see if a server can be created without actual creation.
+ true|false Defaults to false.
+ required: false
+ example: 'false'
+ schema:
+ type: boolean
+ requestBody:
+ content:
+ application/json:
+ schema:
+ type: object
+ properties:
+ packageId:
+ type: integer
+ description: A valid package ID.
+ userId:
+ type: integer
+ description: A valid user ID.
+ hypervisorId:
+ type: integer
+ description: A valid hypervisor group ID.
+ ipv4:
+ type: integer
+ description: Number of IPv4 addresses.
+ storage:
+ type: integer
+ description: Number of GB primary storage.
+ traffic:
+ type: integer
+ description: Number of GB traffic (0=unlimited).
+ memory:
+ type: integer
+ description: Number of MB memory.
+ cpuCores:
+ type: integer
+ description: Number of CPU cores.
+ networkSpeedInbound:
+ type: integer
+ description: Inbound network speed (kB/s).
+ networkSpeedOutbound:
+ type: integer
+ description: Outbound network speed (kB/s).
+ storageProfile:
+ type: integer
+ description: Storage profile ID.
+ networkProfile:
+ type: integer
+ description: Network profile ID.
+ firewallRulesets:
+ type: array
+ items:
+ type: integer
+ description: >-
+ Array of firewall rulesets. This will override package
+ settings. A value of -1 will force no rulesets to be
+ applied.
+ hypervisorAssetGroups:
+ type: array
+ items:
+ type: integer
+ description: >-
+ Array of hypervisor asset groups. This will override package
+ settings. A value of -1 will force no groups to be applied.
+ additionalStorage1Enable:
+ type: boolean
+ description: Enable/disable additional storage 1.
+ additionalStorage2Enable:
+ type: boolean
+ description: Enable/disable additional storage 2.
+ additionalStorage1Profile:
+ type: integer
+ description: Additional storage 1 profile ID.
+ additionalStorage2Profile:
+ type: integer
+ description: Additional storage 2 profile ID.
+ additionalStorage1Capacity:
+ type: integer
+ description: Number of GB additional storage 1 capacity.
+ additionalStorage2Capacity:
+ type: integer
+ description: Number of GB additional storage 2 capacity.
+ required:
+ - packageId
+ - userId
+ - hypervisorId
+ example:
+ packageId: 1
+ userId: 1
+ hypervisorId: 1
+ ipv4: 1
+ storage: 20
+ traffic: 20
+ memory: 512
+ cpuCores: 5
+ networkSpeedInbound: 200
+ networkSpeedOutbound: 400
+ storageProfile: 1
+ networkProfile: 1
+ firewallRulesets:
+ - 1
+ - 2
+ hypervisorAssetGroups:
+ - 3
+ - 4
+ additionalStorage1Enable: true
+ additionalStorage2Enable: false
+ additionalStorage1Profile: 1
+ additionalStorage2Profile: 2
+ additionalStorage1Capacity: 10
+ additionalStorage2Capacity: 20
+ responses:
+ '201':
+ description: ''
+ content:
+ application/json:
+ schema:
+ type: object
+ properties: {}
+ example:
+ data:
+ id: 70
+ ownerId: 1
+ hypervisorId: 14
+ arch: 1
+ name: ''
+ selfService: 0
+ selfServiceSettings: []
+ hostname: null
+ commissionStatus: 0
+ uuid: ab68e20a-211f-4b90-99f1-8ee9068c81de
+ state: allocated
+ migratable: true
+ timezone: _default
+ migrateLevel: 0
+ deleteLevel: 0
+ configLevel: 0
+ backupLevel: 0
+ elevated: false
+ elevateId: null
+ elevate: false
+ destroyable: true
+ rebuild: false
+ suspended: false
+ protected: false
+ buildFailed: false
+ primaryNetworkDhcp4: false
+ primaryNetworkDhcp6: false
+ built: null
+ created: '2025-01-20T14:00:47+00:00'
+ updated: '2025-01-20T14:00:47+00:00'
+ traffic:
+ public:
+ countMethod: 1
+ currentPeriod:
+ start: '2025-01-20T00:00:00.000000Z'
+ end: '2025-02-19T23:59:59.999999Z'
+ limit: 20
+ settings:
+ osTemplateInstall: true
+ osTemplateInstallId: 0
+ encryptedPassword: >-
+ eyJpdiI6IkF5L05USXR3OGRNMm80NVFpMXhaVnc9PSIsInZhbHVlIjoiZ0JtclcxSFhoeEdEOGJPa1J6cTVteTllOTh5YU1xM3ViUGphSS9qUTFPMD0iLCJtYWMiOiI3MWFmYzhkY2Y4ZTkxNmNjZWFhZDgzMjZlMjIwZGFhYTg2YTU2OThmYzdjN2MwYzZjNzZhNDBmZTE2MDY4MTc5IiwidGFnIjoiIn0=
+ backupPlan: null
+ uefi: false
+ uefiType: 0
+ cloudInit: true
+ cloudInitType: 1
+ config: []
+ userConfig: []
+ bootOrder:
+ - hd
+ - cdrom
+ tpmType: 0
+ networkBoot: false
+ bootMenu: 1
+ customISO: 1
+ securityDriver: 3
+ memBalloon:
+ model: 1
+ autoDeflate: 0
+ freePageReporting: 0
+ hyperv:
+ enabled: false
+ passthrough: false
+ relaxed: 0
+ vapic: 0
+ spinlocks: 0
+ vpindex: 0
+ runtime: 0
+ synic: 0
+ stimer: 0
+ reset: 0
+ vendorId: 0
+ frequencies: 0
+ reenlightenment: 0
+ tlbflush: 0
+ ipi: 0
+ evmcs: 0
+ vendorIdValue: WIN KVM
+ spinlocksValue: 8191
+ clockEnabled: 0
+ extended:
+ cpuFlags:
+ topoext: '1'
+ svm: '1'
+ vmx: '1'
+ machineType: inherit
+ pciPorts: 16
+ resources:
+ memory: 512
+ storage: 20
+ traffic: 20
+ cpuCores: 5
+ cpu:
+ cores: 5
+ type: inherit
+ typeExact: host-model
+ shares: 1024
+ throttle: 0
+ topology:
+ enabled: false
+ sockets: 1
+ cores: 5
+ threads: 1
+ dies: 1
+ customXML:
+ domain:
+ xml: ''
+ enabled: false
+ os:
+ xml: ''
+ enabled: false
+ devices:
+ xml: ''
+ enabled: false
+ features:
+ xml: ''
+ enabled: false
+ clock:
+ xml: ''
+ enabled: false
+ cpuTune:
+ xml: ''
+ enabled: false
+ qemuCommandline: []
+ qemuAgent:
+ os:
+ screen: >-
+ iVBORw0KGgoAAAANSUhEUgAAAWgAAAEQCAYAAACdlO55AAAAAXNSR0IArs4c6QAAAHhlWElmTU0AKgAAAAgABAEaAAUAAAABAAAAPgEbAAUAAAABAAAARgEoAAMAAAABAAIAAIdpAAQAAAABAAAATgAAAAAAAABIAAAAAQAAAEgAAAABAAOgAQADAAAAAQABAACgAgAEAAAAAQAAAWigAwAEAAAAAQAAARAAAAAAoXChbAAAAAlwSFlzAAALEwAACxMBAJqcGAAAIOZJREFUeAHtnQe4HUXZx+eWAAYTiCQQEpUaFVSIigoK2LBrKAbsgkBARSw8Sn3UPKISPj4ExQpiw4Y1VvR7VFBEMfoIIiK9SC+GIgFCknu+/S++65zN7rnn3Jy9O3P2N3lOtszuzju/d+5/Z2dndobOPffcloswtFojbuutz3Rz5ix1U6bcF2EOcia3Wu6CsV3d4a3T3MWjOyaRQ7kDItxsrUyMXuhaQ/+XLFdFmIG8yS134JkHuZOO/B834+4ZbijKvxw/TypjNyb+WeBc65JkfcyPjG5duRlzw+64kePdie4o1xobTvIVXTbaDE5yQIAABCAAgRAJINAhegWbIAABCCQEEGiKAQQgAIFACSDQgToGsyAAAQgg0JQBCEAAAoESQKADdQxmQQACEECgKQMQgAAEAiWAQAfqGMyCAAQggEBTBiAAAQgESgCBDtQxmAUBCEAAgaYMQAACEAiUAAIdqGMwCwIQgAACTRmAAAQgECgBBDpQx2AWBCAAAQSaMgABCEAgUAIIdKCOwSwIQAACCDRlAAIQgECgBBDoQB2DWRCAAAQQaMoABCAAgUAJINCBOgazIAABCCDQlAEIQAACgRJAoAN1DGZBAAIQQKApAxCAAAQCJYBAB+oYzIIABCCAQFMGIAABCARKAIEO1DGYBQEIQACBpgxAAAIQCJQAAh2oYzALAhCAAAJNGYAABCAQKAEEOlDHYBYEIAABBJoyAAEIQCBQAgh0oI7BLAhAAAIINGUAAhCAQKAEEOhAHYNZEIAABBBoygAEIACBQAkg0IE6BrMgAAEIINCUAQhAAAKBEkCgA3UMZkEAAhBAoCkDEIAABAIlgEAH6hjMggAEIIBAUwYgAAEIBEoAgQ7UMZgFAQhAAIGmDEAAAhAIlAACHahjMAsCEIAAAk0ZgAAEIBAoAQQ6UMdgFgQgAAEEmjIAAQhAIFACCHSgjsEsCEAAAgg0ZQACEIBAoAQQ6EAdg1kQgAAEEGjKAAQgAIFACSDQgToGsyAAAQgg0JQBCEAAAoESQKADdQxmQQACEECgKQMQgAAEAiWAQAfqGMyCAAQggEBTBiAAAQgESgCBDtQxmAUBCEAAgaYMQAACEAiUAAIdqGMwCwIQgAACTRmAAAQgECgBBDpQx2AWBCAAAQSaMgABCEAgUAIIdKCOwSwIQAACCDRlAAIQgECgBBDoQB2DWRCAAAQQaMoABCAAgUAJINCBOgazIAABCCDQlAEIQAACgRJAoAN1DGZBAAIQQKApAxCAAAQCJYBAB+oYzIIABCCAQFMGIAABCARKAIEO1DGYBQEIQACBpgxAAAIQCJQAAh2oYzALAhCAAAJNGYAABCAQKAEEOlDHYBYEIAABBJoyAAEIQCBQAgh0oI7BLAhAAAIINGUAAhCAQKAEEOhAHYNZEIAABBBoygAEIACBQAkg0IE6BrMgAAEIINCUAQhAAAKBEkCgA3UMZkEAAhBAoCkDEIAABAIlgEAH6hjMggAEIIBAUwYgAAEIBEoAgQ7UMZgFAQhAAIGmDEAAAhAIlAACHahjMAsCEIAAAk0ZgAAEIBAoAQQ6UMdgFgQgAAEEmjIAAQhAIFACCHSgjsEsCEAAAgg0ZQACEIBAoAQQ6EAdg1kQgAAERkGwNoFzzmm5r3997f2V7mk5d0frCne9O9a1hmckSQ1VmtykXLy1JknmoiQrq5NlksEBCOddfZ479P5D3Xqt9QbAQypjK1yrdUOyHJt070xNUjx90lONK0EEusBfl13mJl+gUzvuTP4/p8AidoVC4Fp3jdOPsO4EpiWXQKA7c6SJozMfYiEAAQjURgCBrg09CUMAAhDoTACB7syHWAhAAAK1EUCga0NPwhCAAAQ6E0CgO/MhFgIQgEBtBBDo2tCTMAQgAIHOBBDoznyIhQAEIFAbAQS6NvQkDAEIQKAzAQS6Mx9iIQABCNRGgJGEk4h+6tSp7slPfrKbNm2au+qqq9yNN97YVeqbbbaZ23777d0DDzzg/v73v7v777+/8LwpU6a4DTbYII0bGxtzK1asaDtuZGTEyQaFBx980K1erSHY7WH69OnuKU95inv0ox/tbrrpJnfllVcWHtd+lnPd2pg/r9O2n59OxylOTFqt4uHkysvQUPvQ+TVr1rhVq1alv6JrF52j48RM7IpCL/bqGo961KOyy8i3sikf5E9dV0HHyIfrr79+tq1zeklX17AyoIuojKisWPDznS9Do6Ojmc0rV650Dz/8sJ3GsiIC1KArAutfdvfdd3d//etf3b///W+3bNky96tf/cr985//dMuXL3fvfve70z86/3itz5gxw33pS19yt912W/r79a9/7S688EJ33333uauvvtq94Q1vyJ/i3vrWt6bxOubee+918+fPbztmt912y+J/8pOftMUtWLDAXXfddel5F1xwgfvFL36R3gxk88knn5zeVNpOSDYmYuNf/vKXNA3Zd/jhh+cv2bb9tre9LbNXeer022abbdrO9TduuOGGtc6VMElglL/vfOc7btNNN/VPSVkUpSeBe+ihh9zNN9/svvrVr7Zx6cXe5z//+W02vf3tb29L3zYuuuii7DiVhw984APZ9llnnZUe1ku6T3/607Pzlb8XvOAFlpR77GMfm/KwfP/rX//Kbvg6SDZa3N/+9rfsPFaqI4BAV8c2vfIHP/hBd+6557oddtjBDQ+345bAnXrqqcl3P9q/zLTddts5/QEccMABac3UN1E1QYmRzvn+97/vVKspCqppffazn12r5mjHPuEJT7BVd8ghh7ilS5e6LbfcMttnK6rBHXHEEc7EwPZP1EY9PaiWrp/VBO2adSxVY1y4cKG79NJL3ZOe9KSuTJDdc+bMcW9+85vTm+bGG2/c1Xn+Qddff7275557sl2ve93rsnVb0ZOMb9M//vEPi0qXvg/bIjps3H333U61Xws777yzrbo99tgjW9eK8unH++u6wRKqJ9CuGNWn16gUnv3sZ7vFixdnwqxHcNWk1XTgh9e+9rXuuc99brpLgisxnDt3bnaIanuqPetc/3F07733Tmvg2YG5Ff1BLVq0KLd37c33vOc9mZDrkflPf/qT+8Mf/tD2+L/nnns6PQko9NPGta0p3qPmiEsuuaTw9/Of/9zdcsstxSfm9oq9xPiaa65pa1KYNWuWKxJJna4as9LWTfOKK65oa0pR09NLX/rSXCouZTeevaq5W3jOc56T1mBtW0vdOCyo7HzjG9+wzdJlN5zkXwu77LKLra4l0IpQTd+Cf+xvf/tb282yQgIIdIVwP/GJT2TCJ5F9+ctfnjY7PO5xj3Ovec1r2lL+6Ec/mm6/6U1vcs94xjOyOP0hzJs3z+mPQ00WeiS9/fbbs3jdAGbPnp1t51dOOOEEJ/EpC5tvvrlTbdiCHpef9axnOQmGmj38YDWoftvop1G2fuutt7odd9yx8CeuanroJuiJ5qlPfarbdttt06caNX9YeNWrXmWrbUs1xShtPQWpRqtmAgmhhXzNU/u7sdd/ctKTkW7Ufth3332zzd/97nfOtzWLyK10k+7555+fnaVKhIUXvehFtpotrQlETUBbbbVVtv83v/lNts5KdQQQ6IrYPuYxj3F+4f/kJz+Ztutacmqe+MIXvpDWij/96U+7U045JY3yxVmio1qd/ugsSLDf//7322b6Mk9ty2VBdpx00kll0Wmbo18rP/bYY91hhx3mtthiC6ea6ZFHHumOO+44p1r2eeedl16n3zaWGldxxGXJd2UtT0pKteFugp5k/Ed8/2VfN+fbMfKl/6LYF2jZ4tvzta99zU5b56Uv0DNnzkxvVmpOKbrRqwyrmctuzkpctXndMAjVExitPolmpqBarx++9a1v+ZvpelHzg2ppFtQu7Iuz7VfN6zOf+Uwqztr3xCc+0aIKl/vvv7/74he/WBin3g9//OMf0xq6DlAt6VOf+lT60+P8Oeec47785S+nNxK7QBU22rXLlo9//OPTF6xF8bLxQx/6UFFU6T4106jG+MpXvjI7RoJdFCROaubQOWpDV01bwmZBTSb50I291mxx1FFHpac/85nPdFtvvbW79tpr25o39DLTbw7Jp+Vvd5Pu73//+7SpzN6JKH9+fvQiV08K6u2hdmg9vfkCraYetWUTqieAQFfEOC/Qfk2pU5KqyVjQW/SioBqvrmdNE/m0dI7EVU0p1qVKgv7e97636HLpy0j90W6yySZt8RJ+/VR7Pvroo92JJ56YxvfLxrbEutiQgBUF5bXboBuVWFh3RP+8slrhQQcd5PQrCmr7/tznPlcU5bqxVzdbE2hdRLVoNUv5zRs/+9nPehLE8dJV7V/t49bLR+LrvyBWenfeeWfWtq52aNqfC11c+U6aOCpCrNqWH/Lbfpy/bn1eta+oX6wdq14anYK6zB1//PHZIep/XSbQ6usskVdTSNnLtiVLljjVxBX6ZWNm3CSvFImz+pf3Wgv//Oc/n7Zn+70xes2KaqMSSwtq0lLt1b8J9rN5w9Lxmzn08vd5z3ueRblf/vKX6c92qI19p512sk3HC8IMReUr7SpSeXLNSUB9lf2g2qxqJX7QyybVTPQHYbVADWDRfgU9rhaF9dZbry1OAlsU1H9ZL/Qkzgp6mZYPejmlmrNeFp555plpbU4vxPT4L7Hw/zD1aP+Vr3wlHWTTLxvz9pRtq/eF37TiH9fLgAk16aibmW5+arZQP3P1Cdd7AL9d2b++ek/oieUd73hH1u9ZzRNqElFf9qLQi72qRVvetFRfZwuyKd9n3eKKlt2mK4G2fuh6aWpBL7PVg8e/6eiFsR8QaJ9GtevUoCvimxdN9Zn1g2rA+sNXe+/ll1/uNDhEwYRa669+9avTmq3W/aABA34t0D/HP049DXSsxKQsqBeIbhyqxamZQ/2CL7744nRwinpz6HHXgrVD+umtq4127fGWatZRu2fRT6LSbXjXu96Vtrdq5KNehOolmJ40yoRW11U/djXxqJYpUVfQjU192F/2spel2/n/erH3m9/8ZpuP/EFI3/3ud9v6LefTyW93m65fg/avof264ak85CsUOk7l2u9F5J/Lev8JIND9Z5pe8Y477nB62WLh0EMPdXvttVe6qbf+6i3hv9zTyEKFn/70p+lS/6kp4Xvf+17bceqe9+EPfzg7RiO7OtVo9AenEWhlQU0hFtTjQ93QTPzVtcqvXam2qdBvGy390Jca1adeLRYk0meccUY66Mb2TWSp2nlZt7Uqmjdko3yZf8rTfj3NKeimrhGv+dCprOWPZXvdCSDQ686w9ArvfOc7s5qRRPkHP/hBOkBCNRNfZHUBdcNT0OOu/8cqgVQvAT1Oa/SZalQahWdBgqqbQacgUbnrrrsKD9GQbqsV6oD3ve996bGqJasHiZpmLNijdr9sVK1UIlH0s0d+S1vNPUXH2b6yQSZ2fr+WeuJRjdqChkd/7GMfs81s2au9YpoPnYQ7f6xt95JuUS3aBFrX89ft+n7ZtH0sqyNAG3R1bNO2PLUnSoytS5O6UeWDjlG7nwV1v/vhD3+Y9dLQC0brsWHHaGlNJP6+onX1BpFIF3W1kwirl4bfE2HDDTd0+WHEGnmn7nYW+mGj2r7zPUfs+laLt20tO31vw79p+ef0e101ywMPPDAdVajmIAU1I+m7HPnQi7268Ur8/eHv+aaP/PXLtrtNVwKt77dYsKYu2y4SaGrQRmdyltSgK+asEYJqu9Tw2vzLLNVS3/jGN7qPfOQjbVboReHTnva0tGam0WMSBXtpY0N599lnn/Rcv6eHX5POtxNKXMv+uNQbQddTT4Z80HUkFHpZ6I9km6iNebvy6Wlb+S1q/yw61vZ1c107dl2XepLxBwvp5qunpV5C3l7512860rWqaN7w083XoNWkIfYW5G/52YK2rSnO9rGslsBQ8rj2X49Um1Zfr95qjSSd+s9MPlqzNGmrva+v1z755FbyqN/XS6YXU+8LdaFSjVhtv9129tfgCPtpEIM/zLjfViodDVZR/2n9cZb1xc6na/ZpWbWN+bTZjpPAtMTsfn5yaSi53pgbdseNHO9OdEe51lhS/4xS3f7rT5o4/sui8jV7O95rQvokpn6TEZSO3y+32zQn08ZubeI4CMROgCaO2D2I/RCAwMASQKAH1rVkDAIQiJ0AAh27B7EfAhAYWAII9MC6loxBAAKxE0CgY/cg9kMAAgNLgF4cA+Baf1ZnfYsh/22KsngNSLEBNEWzfGv0o7oEqq912YwlGlih7oMK6kPrzziuc/2P2esafr9tQ69BKfaFvPwx6rZnQSMei7oYKn0b4JE/387tddkpX/61/Fmw/f2y0x+h6cf1ul6WRv46/Uwzf2226yFADboe7n1NVV8ls9mWi7rI6StsFu8PRlE/Z9tfNKGAPoRv8bvuumuhzZpt3I7R0h/Fpu8I+3EacVcU8jNX2zEvfvGL287X1/aKQtFM1zqulxnE89ftlC//WPVn9/No63bDU59w2aeblQV961lfqdOv7POudqyWGiBi1+201Aw9hMEigEAPlj8nnBt9hKnoc6S6oL68VzREXcPP/U9R6uNB/iwxGqlmIyB1naLvZei7xxq8Y8Gfufrggw+23elSwqaZ0DsFf4i6at8TmUF8vHx1St+PEzcN+tFQf43WtKAav9k1WUPULW2WcRFAoOPyV6XW6lsQRd/AKEu0aJYRfdvBmiv03WV/qqZeZq7WNzrs63+Wvmx7y1veYpuVLcfLV1nCqk1r+jAN69cnZP1wwAEHOH0tcF2Dvvesp6SiX36o+Lqmxfn1E0Cg6/dBMBaolqwJYrsJqgUWiaU+UeoLq/+VNtWw/YlRlY4/tZM/c7W+n21t2749hxxyiL/Z9/Vu81WUsL5qp29m6zvaqoXr+yYW1NbvT7Zr+3td6uuFZbOb65sphMEigEAPlj/XOTf66p3fTFB2wQULFrhZs2Zl0f5HdvTtawv6QJM+m2nBF+hOM1f7tVi9+LSgc8raw+2YdVl2m69u0li9enXbYf5EB20RbECghAACXQKmqbtVg9SkquMFX0D1WO/XvF/4whdmLwsl3PosqgWbuVrbCxcutN3pl/6sOUSznPhz8uk4fzqqKmvR3eYrM9xb0bRZP/7xj9Ov0l144YXuRz/6URarz8n240tw+mTssmXLCn/6NjVhsAgg0IPlzwnnRj0pLGg+wte//vW2udZSH4V/yUteku3Xt5D1aUyr6aopwxdRv5lDJ1kt2m/e8Geu9l8OauYPCd23v/3tLL1uXhZmB/ew0mu+8pfWzU3zNr7iFa9Ip9Ly433h9/dPZF03uaKfdTWcyDU5J0wCCHSYfpmwVRLHfCjalz/mmGOOaZtr7uMf/7jbaKON8oel23oRaP2n1ff27LPPTpsx1DXNgl6K2cvCXmauVp9fv7eHat/qO63Jai1U9bKw13yZPd0sNd+jzenYzfEcAwERQKAHoByot4QFE0Xb1tLfl580wI5Td7gjjjjCNt3s2bPd/Pnzs21bkTD7s3Do2uqxoA/s77HHHnaY08vCvffeO9v2a9GdZq7eb7/90olr7UTZpGsvXbrUdqVLv4beFjHBjYnmy09O80yqH7jmmtQLQdmu/tAKG2+8cdtThX9eL+vq867eIEU/f37JXq7JseESQKDD9U3XlpkI6AS9uJMY+GHOnDnZZqfRbaqtFk1zlJ2crEiENRu2H2bOnJnOlO3v07ovovnpm8pmrvabN+x6Rdfv98vCiebLbNRSNxINTNHM1xokc8opp7S1Q5f1M/evMd66RokWzWyufdbENN41iI+HAAIdj69KLfV7SagdUsJg7ZHz5s1zfvvneCPXNOrQr5HnE/UFVE0P+ZFt/g1ALwu33Xbb9BKdJkC1qZ0kurvsskuWpIZt569f1lskO2mCKxPNV6fklHe1FVtQXggQ6IUAAt0LrUCPVS8Kf8Se2n+XL1+eTqul2pz/PQsN3+4UNPz7hBNOKDxENdk999wzizv99NPTdmq1Vdtv9913z+LV9u2PLPSbOewgX7j9G4nEXwJn17Wlb796d4w3stDSGW8G8XXJl6VxxhlnpDOPa4JdDShR048/ArNoTkhNLWYzk+eXRUPb1Ysjf5xtq52bMFgEEOgB8KdqZhrA4Af94W+55Zb+rvSbDhLV8cKSJUvSx/T8cfnBI2eddVb+kHQU3aWXXprt183CBpxo5up87dyaPvIDRDSBqWYczwd/ZnK9LNx///3zhxRua2Si2oeLfuq1Yjbq5F7z5Seo60uU586dmw6Rtzg9uSxevNg2s6VuYkU2ad/mm2+eHeevlB2f97d/DutxEkCg4/TbWlafdtppae1WM07ng/oQqxlBM4X77dX+LOD+ORJRNXXkgz9ARLU29e0tCr6I6mWhXpopdJq5Wseohm6hSCQVp37Gd911lx3mdtttt2w9v+LPYJ2Ps201maxLvvJfDrTrapCKmmgkzLoJqbnn5ptvTqO7sUsHdnucpdnr8XYey3AJMKt3gW+qmtW7IKlKdmnAgsRONUx9Ca2oJlpJwlwUAj0QYFbv8WGNjn8IR8RGQO2f+hEgAIG4CdDEEbf/sB4CEBhgAgj0ADuXrEEAAnETQKDj9h/WQwACA0wAgR5g55I1CEAgbgIIdNz+w3oIQGCACSDQA+xcsgYBCMRNAIGO239YDwEIDDABBHqAnUvWIACBuAkg0HH7D+shAIEBJoBAD7BzyRoEIBA3AYZ6F/hv0SLn9tmnIKLKXS3n/jy2k1vcWuwuG90uSWntqauqTL6Sa7c008thSVbOT5arKklisi+679n7uWOWHO02unejAfCQytgtruWSAu8uT35JIZzEQO1wfNgIdAGj6dOH3PTpBRFV7kq+qnbL2AZu/dZcNzS6VZLSoAj0VNdKPqk5EPlJBGzarGlui5Et3IyhGW5ocvWsgtInv4wm/lk/0WatR5+hChjVe0luYvXyJ3UIQAACpQQQ6FI0REAAAhColwACXS9/UocABCBQSgCBLkVDBAQgAIF6CSDQ9fIndQhAAAKlBBDoUjREQAACEKiXAAJdL39ShwAEIFBKAIEuRUMEBCAAgXoJIND18id1CEAAAqUEEOhSNERAAAIQqJcAAl0vf1KHAAQgUEoAgS5FQwQEIACBegkg0PXyJ3UIQAACpQQQ6FI0REAAAhColwACXS9/UocABCBQSgCBLkVDBAQgAIF6CSDQ9fIndQhAAAKlBBDoUjREQAACEKiXAAJdL39ShwAEIFBKAIEuRUMEBCAAgXoJIND18id1CEAAAqUEEOhSNERAAAIQqJcAAl0vf1KHAAQgUEoAgS5FQwQEIACBegkg0PXyJ3UIQAACpQQQ6FI0REAAAhColwACXS9/UocABCBQSgCBLkVDBAQgAIF6CSDQ9fIndQhAAAKlBBDoUjREQAACEKiXAAJdL39ShwAEIFBKAIEuRUMEBCAAgXoJIND18id1CEAAAqUEEOhSNERAAAIQqJcAAl0vf1KHAAQgUEoAgS5FQwQEIACBegkg0PXyJ3UIQAACpQQQ6FI0REAAAhColwACXS9/UocABCBQSgCBLkVDBAQgAIF6CSDQ9fIndQhAAAKlBEZLY4KOGHJDQzLQlr0Y2+rl4Ek9VpaZdbasyoAUX1UXt+s+4iTbGoilspSUuvTfQGRogHIif6hcyzu2pn0xh9E1a9ZEaL8cMOZWr2q5VaseMb/VpaIND4+4kRE9OOgaIYUxN+xG3JTEJN01q7RubGzMjY21XKs1ViEA5UDOqTKNCs0vuvRIkps1Lbcy+fdw8q9KHxUl3/99SYbc6qRS0OUfT/8N6OsV5Q/lZU3yzyX+cclfVOxhaPsd5kfonUf+NNabco8bnXJfUpvuTgRWrlzpFu6zl1t08CI3e7NNA/Ndyz3Q2tDd6ma7h4Y2qOyPX0V22bI/u5P/91R3/XXXJukkeypTGhWtG5PfiuQXWjFruRUrV7jVq/9zh08sHDckqGYsn+E2vW1TNzImcYs9JN4fXp38/dyUuOfB2DPzH/uH3G2tme5Ot0l4RW4ChEevu/6GCZwWximtlt0hu/tjefihIbfLzjOTWve8JANzw8iEZ8XURCi38barWr3z/hVu6pVXueHLr3DDEudKmyLkG/NTVTmawHWTR67hlUnme7xv3O0e+TeBFAM+RTdpFYQBCa3bE78mvwEIoyMj3YnbAOTVrU7aDoZH9BfZ41/lIGTey8OYdGk0gaFf5QLtJRzSqorA6v/8QrJrkm0ZTm6ej7TZTnLCFSY3luRoUJptAqzaVOg5Lg0BCEAgIgIIdETOwlQIQKBZBBDoZvmb3EIAAhERQKAjchamQgACzSKAQDfL3+QWAhCIiAACHZGzMBUCEGgWAQS6Wf4mtxCAQEQEEOiInIWpEIBAswgg0M3yN7mFAAQiIoBAR+QsTIUABJpFAIFulr/JLQQgEBEBBDoiZ2EqBCDQLAIIdLP8TW4hAIGICCDQETkLUyEAgWYRQKCb5W9yCwEIREQAgY7IWZgKAQg0iwAC3Sx/k1sIQCAiAgh0RM7CVAhAoFkEEOhm+ZvcQgACERFAoCNyFqZCAALNIoBAN8vf5BYCEIiIAAIdkbMwFQIQaBYBBLpZ/ia3EIBARAQQ6IichakQgECzCCDQzfI3uYUABCIigEBH5CxMhQAEmkUAgW6Wv8ktBCAQEQEEOiJnYSoEINAsAgh0s/xNbiEAgYgIINAROQtTIQCBZhFAoJvlb3ILAQhERACBjshZmAoBCDSLAALdLH+TWwhAICICCHREzsJUCECgWQQQ6Gb5m9xCAAIREUCgI3IWpkIAAs0igEA3y9/kFgIQiIgAAh2RszAVAhBoFgEEuln+JrcQgEBEBBDoiJyFqRCAQLMIINDN8je5hQAEIiKAQEfkLEyFAASaRQCBbpa/yS0EIBARAQQ6ImdhKgQg0CwCCHSz/E1uIQCBiAgg0BE5C1MhAIFmEUCgm+VvcgsBCEREAIGOyFmYCgEINIsAAt0sf5NbCEAgIgIIdETOwlQIQKBZBBDoZvmb3EIAAhERQKAjchamQgACzSKAQDfL3+QWAhCIiAACHZGzMBUCEGgWAQS6Wf4mtxCAQEQEEOiInIWpEIBAswgg0M3yN7mFAAQiIoBAR+QsTIUABJpFAIFulr/JLQQgEBEBBDoiZ2EqBCDQLAIIdLP8TW4hAIGICCDQETkLUyEAgWYRQKCb5W9yCwEIREQAgY7IWZgKAQg0iwAC3Sx/k1sIQCAiAv8PwMJP2Mn0f2kAAAAASUVORK5CYII=
+ media:
+ isoMounted: false
+ isoType: local
+ isoName: ''
+ isoFilename: ''
+ isoUrl: ''
+ isoDownload: false
+ backupPlan:
+ id: null
+ name: null
+ vnc:
+ ip: 192.168.30.6
+ port: 5904
+ enabled: false
+ network:
+ interfaces:
+ - id: 70
+ order: 1
+ enabled: true
+ tag: 6927490480
+ name: eth0
+ type: public
+ driver: null
+ processQueues: null
+ mac: 00:BA:76:AB:DF:4E
+ ipv4ToMac: null
+ ipv6ToMac: null
+ inTrafficCount: true
+ outTrafficCount: false
+ inAverage: 200
+ inPeak: 0
+ inBurst: 0
+ outAverage: 400
+ outPeak: 0
+ outBurst: 0
+ ipFilter: true
+ vlans: []
+ ipFilterType: '4'
+ portIsolated: false
+ ipv4_resolver_1: 1
+ ipv4_resolver_2: 2
+ ipv6_resolver_1: 1
+ ipv6_resolver_2: 2
+ networkProfile: 0
+ dhcpV4: 0
+ dhcpV6: 0
+ firewallEnabled: false
+ hypervisorNetwork: 14
+ isNat: false
+ nat: false
+ firewall: []
+ hypervisorConnectivity:
+ id: 14
+ type: simpleBridge
+ bridge: br0
+ mtu: null
+ primary: true
+ default: true
+ ipWhitelist: []
+ actions: []
+ ipv4:
+ - id: 520
+ order: 1
+ enabled: true
+ blockId: 3
+ address: 192.168.30.207
+ gateway: 192.168.30.1
+ netmask: 255.255.255.0
+ resolver1: 8.8.8.8
+ resolver2: 8.8.4.4
+ rdns: null
+ mac: null
+ ipv6: []
+ secondaryInterfaces: []
+ storage:
+ - _id: 81
+ id: 1
+ cache: null
+ bus: null
+ capacity: 20
+ drive: a
+ datastoreDiskId: null
+ filesystem: null
+ iops:
+ read: null
+ write: null
+ bytes:
+ read: null
+ write: null
+ type: qcow2
+ profile: 1
+ status: 3
+ enabled: true
+ primary: true
+ created: '2025-01-20T14:00:47+00:00'
+ updated: '2025-01-20T14:00:47+00:00'
+ datastore: []
+ name: ab68e20a-211f-4b90-99f1-8ee9068c81de_1
+ filename: ab68e20a-211f-4b90-99f1-8ee9068c81de_1.img
+ hypervisorStorageId: null
+ local: true
+ locationType: mountpoint
+ path: /home/vf-data/disk
+ - _id: 82
+ id: 2
+ cache: null
+ bus: null
+ capacity: 10
+ drive: b
+ datastoreDiskId: null
+ filesystem: null
+ iops:
+ read: null
+ write: null
+ bytes:
+ read: null
+ write: null
+ type: qcow2
+ profile: 0
+ status: 1
+ enabled: false
+ primary: false
+ created: '2025-01-20T14:00:47+00:00'
+ updated: '2025-01-20T14:00:47+00:00'
+ datastore: []
+ name: ab68e20a-211f-4b90-99f1-8ee9068c81de_2
+ filename: ab68e20a-211f-4b90-99f1-8ee9068c81de_2.img
+ hypervisorStorageId: null
+ local: true
+ locationType: mountpoint
+ path: /home/vf-data/disk
+ hypervisorAssets: []
+ hypervisor:
+ id: 14
+ ip: 192.168.30.6
+ hostname: null
+ port: 8892
+ maintenance: false
+ groupId: 1
+ group:
+ name: Default
+ icon: null
+ timezone: Europe/London
+ forceIPv6: false
+ vncListenType: 1
+ displayName: null
+ cpuSet: null
+ nfType: 4
+ backupStorageType: 2
+ defaultDiskType: inherit
+ defaultDiskCacheType: inherit
+ defaultCPU: inherit
+ defaultMachineType: inherit
+ created: '2024-05-14T11:19:04+00:00'
+ updated: '2024-06-28T21:22:01+00:00'
+ name: Ceph Hypervisor 2
+ dataDir: /home/vf-data
+ resources:
+ servers:
+ units: '#'
+ max: 0
+ allocated: 4
+ free: -4
+ percent: null
+ memory:
+ units: MB
+ max: 24000
+ allocated: 3584
+ free: 20416
+ percent: 14.9
+ cpuCores:
+ units: '#'
+ max: 64
+ allocated: 8
+ free: 56
+ percent: 12.5
+ localStorage:
+ enabled: 1
+ name: Local (Default mountpoint)
+ storageType: 1
+ units: GB
+ max: 1000
+ allocated: 40
+ free: 960
+ percent: 4
+ otherStorage:
+ - id: 2
+ name: Ceph RBD
+ enabled: 0
+ path: null
+ units: GB
+ storageType: 2
+ isDatastore: true
+ max: 10000
+ allocated: 10
+ free: 9990
+ percent: 0.1
+ - id: 3
+ name: Ceph EC
+ enabled: 0
+ path: null
+ units: GB
+ storageType: 2
+ isDatastore: true
+ max: 13333333
+ allocated: 10
+ free: 13333323
+ percent: 0
+ owner:
+ id: 1
+ admin: true
+ extRelationId: null
+ name: Jon Doe
+ email: jon@doe.com
+ timezone: Europe/London
+ suspended: false
+ twoFactorAuth: false
+ created: '2024-03-12T22:22:09+00:00'
+ updated: '2025-01-15T11:01:18+00:00'
+ sshKeys: []
+ sharedUsers: []
+ tasks:
+ active: false
+ lastOn: null
+ actions:
+ pending:
+ - id: 19
+ action: Create HDD (sdb)
+ requires:
+ - boot
+ - restart
+ - shutdown
+ - poweroff
+ collected: false
+ complete: false
+ failed: false
+ payload:
+ disk:
+ id: 82
+ disk_storage_id: null
+ created: '2025-01-20T14:00:47+00:00'
+ headers: {}
+ '401':
+ $ref: '#/components/responses/401'
+ description: ''
+ '422':
+ description: ''
+ content:
+ application/json:
+ schema:
+ type: object
+ properties: {}
+ example:
+ errors:
+ - Invalid or disabled firewall ruleset
+ headers: {}
+ security:
+ - bearer: []
+ get:
+ summary: Retrieve servers
+ deprecated: false
+ description: ''
+ tags:
+ - Servers
+ parameters:
+ - name: type
+ in: query
+ description: simple or full. Defaults to simple.
+ required: false
+ example: simple
+ schema:
+ type: string
+ - name: results
+ in: query
+ description: >-
+ Number of results to return. Range between 1 and 200. Defaults to
+ 20.
+ required: false
+ example: 20
+ schema:
+ type: integer
+ - name: hypervisorId
+ in: query
+ description: >-
+ Filter by hypervisor ID. Specify multiple with
+ hypervisorId[]=1&hypervisorId[]=2 etc...
+ required: false
+ schema:
+ type: integer
+ responses:
+ '200':
+ description: ''
+ content:
+ application/json:
+ schema:
+ type: object
+ properties: {}
+ example:
+ current_page: 1
+ data:
+ - id: 5
+ uuid: 1fb4b391-b360-40e7-8fe1-5b024c7508ac
+ name: Avaricious Trade
+ commissioned: 3
+ owner: 1
+ hypervisorId: 7
+ suspended: false
+ protected: false
+ updated: '2024-04-02T10:15:10+00:00'
+ created: '2024-03-30T14:41:27+00:00'
+ - id: 8
+ uuid: 82c37680-bf8f-4712-854f-31428933703f
+ name: PDNS
+ commissioned: 3
+ owner: 1
+ hypervisorId: 3
+ suspended: false
+ protected: false
+ updated: '2024-04-13T22:02:04+00:00'
+ created: '2024-04-09T11:33:43+00:00'
+ - id: 9
+ uuid: 5de5a89b-b707-41bf-a051-7af1a4e67795
+ name: server 1
+ commissioned: 2
+ owner: 3
+ hypervisorId: 6
+ suspended: false
+ protected: false
+ updated: '2025-01-20T14:13:50+00:00'
+ created: '2024-04-11T17:22:19+00:00'
+ - id: 10
+ uuid: 71178184-7554-406f-80b8-0c1d7ffcfd49
+ name: Respectful Exit
+ commissioned: 3
+ owner: 1
+ hypervisorId: 6
+ suspended: false
+ protected: false
+ updated: '2024-05-13T08:16:00+00:00'
+ created: '2024-04-23T11:50:58+00:00'
+ - id: 11
+ uuid: ffed8ddb-c758-41ff-8380-abb1377dfb38
+ name: Ubuntu Test
+ commissioned: 0
+ owner: 1
+ hypervisorId: 7
+ suspended: false
+ protected: false
+ updated: '2024-05-02T18:33:20+00:00'
+ created: '2024-04-25T20:18:57+00:00'
+ - id: 19
+ uuid: c77ce40f-0226-43ca-b000-c9b7fe143dc7
+ name: Metallic National
+ commissioned: 3
+ owner: 1
+ hypervisorId: 2
+ suspended: false
+ protected: false
+ updated: '2024-05-02T18:34:27+00:00'
+ created: '2024-05-02T10:36:37+00:00'
+ - id: 20
+ uuid: 785aaddd-b08b-448b-9486-baf29cd3c0f8
+ name: Rubbery Daughter
+ commissioned: 3
+ owner: 1
+ hypervisorId: 7
+ suspended: false
+ protected: false
+ updated: '2024-10-07T21:32:34+00:00'
+ created: '2024-05-03T10:05:41+00:00'
+ - id: 22
+ uuid: 5a7e3d49-0fdf-4cfa-bb14-864f3ca0e79a
+ name: Frightening Clock
+ commissioned: 3
+ owner: 1
+ hypervisorId: 7
+ suspended: false
+ protected: false
+ updated: '2024-06-08T08:30:15+00:00'
+ created: '2024-05-03T10:35:36+00:00'
+ - id: 23
+ uuid: b1f6efb6-22a1-4d0a-b043-17d0ccfce4b2
+ name: Backup Test
+ commissioned: 3
+ owner: 1
+ hypervisorId: 6
+ suspended: false
+ protected: false
+ updated: '2024-05-14T15:29:37+00:00'
+ created: '2024-05-04T07:30:10+00:00'
+ - id: 26
+ uuid: 5c681c72-6828-4fa3-8011-ced2502384e6
+ name: Ceph Test 1
+ commissioned: 3
+ owner: 1
+ hypervisorId: 13
+ suspended: false
+ protected: false
+ updated: '2024-05-14T11:42:08+00:00'
+ created: '2024-05-14T10:57:56+00:00'
+ - id: 27
+ uuid: 8cb75e06-caae-47f5-9bf2-3ea1d341d10e
+ name: OVS BHV 6
+ commissioned: 3
+ owner: 1
+ hypervisorId: 11
+ suspended: false
+ protected: false
+ updated: '2024-05-17T13:25:10+00:00'
+ created: '2024-05-16T16:56:12+00:00'
+ - id: 28
+ uuid: 3a63170a-2350-422d-8cfb-449ed6940414
+ name: OVS BHV 7
+ commissioned: 3
+ owner: 1
+ hypervisorId: 12
+ suspended: false
+ protected: false
+ updated: '2024-05-17T13:25:04+00:00'
+ created: '2024-05-16T18:13:44+00:00'
+ - id: 29
+ uuid: f24aebac-016c-4139-afcf-5dbfeda54fc8
+ name: OVS BHV 1
+ commissioned: 3
+ owner: 1
+ hypervisorId: 6
+ suspended: false
+ protected: false
+ updated: '2024-05-17T13:25:00+00:00'
+ created: '2024-05-17T11:25:13+00:00'
+ - id: 30
+ uuid: 67486d4d-d974-45c3-a680-980bc84635d8
+ name: Test 10
+ commissioned: 3
+ owner: 1
+ hypervisorId: 1
+ suspended: false
+ protected: false
+ updated: '2024-06-07T16:41:45+00:00'
+ created: '2024-06-07T12:03:00+00:00'
+ - id: 36
+ uuid: a3df9e3c-893e-4f42-ad90-cf34df155589
+ name: Frail Text
+ commissioned: 3
+ owner: 1
+ hypervisorId: 13
+ suspended: false
+ protected: false
+ updated: '2024-06-28T21:25:57+00:00'
+ created: '2024-06-28T13:39:55+00:00'
+ - id: 37
+ uuid: a3b2e9f8-9b5c-44a3-bcb6-bbadf9bd83e2
+ name: Stark Brown
+ commissioned: 3
+ owner: 1
+ hypervisorId: 13
+ suspended: false
+ protected: false
+ updated: '2024-08-23T20:15:25+00:00'
+ created: '2024-06-28T21:36:23+00:00'
+ - id: 38
+ uuid: 8c6f63d1-ec53-4e1a-a52f-d50f03b05c70
+ name: ''
+ commissioned: 0
+ owner: 1
+ hypervisorId: 14
+ suspended: false
+ protected: false
+ updated: '2024-08-23T20:17:42+00:00'
+ created: '2024-08-23T20:17:42+00:00'
+ - id: 39
+ uuid: 539bff72-f6cd-4260-96f1-b7523fd890c5
+ name: Thorny Impression
+ commissioned: 3
+ owner: 1
+ hypervisorId: 14
+ suspended: false
+ protected: false
+ updated: '2024-08-23T20:20:32+00:00'
+ created: '2024-08-23T20:18:39+00:00'
+ - id: 40
+ uuid: ce445459-c716-4f88-a7c6-a0ffd29eb9b2
+ name: Present Charge
+ commissioned: 2
+ owner: 1
+ hypervisorId: 14
+ suspended: false
+ protected: false
+ updated: '2024-08-23T20:57:22+00:00'
+ created: '2024-08-23T20:56:04+00:00'
+ - id: 41
+ uuid: 6fce272f-c6ea-45bd-bf24-d4d357d9a788
+ name: CP Test
+ commissioned: 3
+ owner: 1
+ hypervisorId: 13
+ suspended: false
+ protected: false
+ updated: '2024-08-27T11:10:48+00:00'
+ created: '2024-08-27T11:09:54+00:00'
+ first_page_url: https://192.168.3.11/api/v1/servers?page=1
+ from: 1
+ last_page: 2
+ last_page_url: https://192.168.3.11/api/v1/servers?page=2
+ links:
+ - url: null
+ label: '« Previous'
+ active: false
+ - url: https://192.168.3.11/api/v1/servers?page=1
+ label: '1'
+ active: true
+ - url: https://192.168.3.11/api/v1/servers?page=2
+ label: '2'
+ active: false
+ - url: https://192.168.3.11/api/v1/servers?page=2
+ label: Next »
+ active: false
+ next_page_url: https://192.168.3.11/api/v1/servers?page=2
+ path: https://192.168.3.11/api/v1/servers
+ per_page: 20
+ prev_page_url: null
+ to: 20
+ total: 27
+ headers: {}
+ '401':
+ $ref: '#/components/responses/401'
+ description: ''
+ security:
+ - bearer: []
+ /servers/{serverId}/modify/name:
+ put:
+ summary: Modify name
+ deprecated: false
+ description: ''
+ tags:
+ - Servers
+ parameters:
+ - name: serverId
+ in: path
+ description: A valid server ID as shown in VirtFusion.
+ required: true
+ example: 69
+ schema:
+ type: integer
+ requestBody:
+ content:
+ application/json:
+ schema:
+ type: object
+ properties:
+ name:
+ type: string
+ description: The new name of the server.
+ required:
+ - name
+ example:
+ name: Server 1
+ responses:
+ '201':
+ description: ''
+ content:
+ application/octet-stream:
+ schema:
+ type: object
+ properties: {}
+ headers: {}
+ '401':
+ $ref: '#/components/responses/401'
+ description: ''
+ security:
+ - bearer: []
+ /servers/{serverId}/resetPassword:
+ post:
+ summary: Reset a server password
+ deprecated: false
+ description: ''
+ tags:
+ - Servers
+ parameters:
+ - name: serverId
+ in: path
+ description: A valid server ID as shown in VirtFusion.
+ required: true
+ example: 69
+ schema:
+ type: integer
+ requestBody:
+ content:
+ application/json:
+ schema:
+ type: object
+ properties:
+ user:
+ type: string
+ description: Either root or Administrator.
+ sendMail:
+ type: boolean
+ description: >-
+ Optional (default true) Email the password to the user.
+ (true|false).
+ required:
+ - user
+ example:
+ user: root
+ sendMail: true
+ responses:
+ '200':
+ description: ''
+ content:
+ application/json:
+ schema:
+ type: object
+ properties: {}
+ example:
+ data:
+ queueId: 176
+ expectedPassword: l1LMzm2JGhWYdjjn8JkC
+ headers: {}
+ '401':
+ $ref: '#/components/responses/401'
+ description: ''
+ security:
+ - bearer: []
+ /servers/user/{userId}:
+ get:
+ summary: Retrieve a users servers
+ deprecated: false
+ description: ''
+ tags:
+ - Servers
+ parameters:
+ - name: userId
+ in: path
+ description: A valid user ID as shown in VirtFusion.
+ required: true
+ example: 3
+ schema:
+ type: integer
+ responses:
+ '200':
+ description: ''
+ content:
+ application/json:
+ schema:
+ type: object
+ properties: {}
+ example:
+ data:
+ - id: 9
+ ownerId: 3
+ hypervisorId: 6
+ name: server 1
+ hostname: server1.domain.com
+ commissionStatus: 2
+ uuid: 5de5a89b-b707-41bf-a051-7af1a4e67795
+ state: failed
+ rebuild: false
+ suspended: false
+ protected: false
+ buildFailed: false
+ backup_level: 0
+ backup_plan: null
+ os:
+ screen: >-
+ iVBORw0KGgoAAAANSUhEUgAAAJYAAABTCAAAAABYT6E5AAAABGdBTUEAALGPC/xhBQAAACBjSFJNAAB6JgAAgIQAAPoAAACA6AAAdTAAAOpgAAA6mAAAF3CculE8AAAAAmJLR0QA/4ePzL8AAAAHdElNRQfpARESACeS8jvVAAAI2klEQVRo3u2W+XMjxRXHX3fPfWhG1+i0ZMmXbK3t9dq7bFg2ARKgSFWSn5If8+/lx1xFhSpCQSWppVhYWKB2F+y1JV+yLI1kndbcnR98LBACJGxCUjWfH3r6eN39puf1dx7A/yboiy2EvsHiv+wY+vL2CP2jQ+irjP8TbpP5fiCJtm4BAEBclUURuzFxotnnBqkgZ3uQcCXncs75WAy5oHiKA1x8/NTd+mW8LN0+WIJl+UpdfJH91XSEZZfw8Kq4kpEzKRNeOq50JzMvNEupVCET1QXlWr8qzsbL6Yimt4Wft18YFyPVLUyfrlu43YOupbpGjFOxY6p/enjK+1uFiEvkhK4olLZygQ3RFiCRU9GV6bEfYYPMItUzquWC1QgOE+t4jIKnfVzoc+VllXnSJojBZ/0EkzkdAADiC5dzEKgVljxtp+B7u2j/jldf0YkQAMIIEAA6a6CzKuAvzkJftdjnrjWCy4P+v4PMiWogYJEiCiJKcpRhPRYvH0M8PyKEpAF7qgvxEqpET0uZdDw1kxitNmDej8/qs8XJdXupX+36PEuxgOcHPGb8dTNAJQcBEhnis/jK8SqT4qvl0TzKEOzT8myQcCp9lsELHYoUwOmILfqYQtFhPACJeFwgsMwvXkt0K9ulZqR+utB1pMLuTGv6D4WPQJzOR+SP5eHGvZHjxCIuX0nWb7mDZnKYzX/Argz6aE7e28jCas2ouavxMY7UZqXF41t75JjfnrlmR3aym1cb0380nln4iIoeqVTu+2TkBFOJibaoLh3EttYOBten94rd0SIF+28wx/3ojd1MjuWn6+oeuv5YxPqpiFZ+d1oKHP7Zjy0ibcfrVEhhuP5ZA0XNoQNCGmEX+R4JxH6kn6qxmk5NjAOB7BtDbT/IqZpeGzpKYGdc9zHNrW/bvE13ZpBSSzhil9PGNBiJXD8YBRm9obEI92MtzjoxtF7iOELqt5k3YbabOuwn27pBEWwCQBIAgOGRDgAgAgBAAqCogs6KZQbIVKpgQASMtB6bikhiVMMkDyAZBT4mFjQgaSEDEMsCaLiQTRvGVIZTzyJEOo8U+Un4ZwDw2R6QhMRZhRVnzkdljTurMQvjsmGMpffLD5eJyzXSZq6Rv3PjzUrWVKIdV+Pyo6XtOGcS+niZC47y629n2zOmJWfFzOl7cw9yGXmi3FvT4ny2EWTvFE90nLEblqEi20/sbXyw0L3y+gGsY5dr5FrZw/w7a03IZG38kfTcVqF+e0dqGW97r2xXYmKz/OlueeGw0oS/stfrpLiV9GVpemTiKvVVijYLrHrEcO12lulIzYEhD9yHk07U5IaPcde+54+YftTeNNSj/gFnW3Z7pr/XT7HKfcVvY3u4qb3Lb8aVelRMJd3+qUSd/BHLLgW+SmFzio02pAMqq76yz890XQFPbNJq00BwJG2ojExHGwnkpImnTwAAgJeqAHr5QlYIYITh24LZiw+EgMCliqW1Swu99Hl75tusSSpsOnZzKxZkYZBOpNg17Tj949rK6Lqf68C8khZvdmZJKlcYzbHRuDAmr4g8X4JYOYpzsmqgCf9M74dWWVZjyrr7/PECGlFIGLKsawiKXFJ4dnfNzfr+CBbUqHSrUZDXmlWa1LT+17tFmR/UjODALaamu9Tfv7IzdxeUnp3d5lb9rWDl0407tJrbXH0kiaksy9uvJfLuSoNGZu7rp1nZjR/+BfEeXH3u8YHgjTVlzmbm644xFxDekUfiZum+IEeda17wG7T8aKXuz916Q5aTuhawPTqiiAKiAAgoAKDz4vyJFAcoscVo8S4b2AwIjsNhSx2KE3kM8lgdE4It1uYDDMhFHpJtBp8ifiJ5GAU+9YHxo2NPcnxEJc/iLKMFPOvgpYnUO3KkgeBzNsypd0E61Xss79qCLXt4ggD5X6/y1WE5zgdLFIu5GTtHutySuSznkonsSTXpFA6Bd1wO4rE+F7ipTAdEfWITzBA78PJuQivac2U0vnZQMRnHdvURO8b6CKKJrU6r64goOfFdLJj7DMqK5uziTtUJlrkWXc1OJ4xxtf9MoqDNCgUxUTCfHVJ1dUEMKuxaC9YJUzrSyY031Qr6ZKpfYoXu1IYo1o14JhlP9gbpR+pLbcFn28lFNKX3Np0T5sUh7/SJnfXb7YXl+1or2S7G1j/J1yWLWeJUOf5Z6v1kqzwdkbaslvvCgHcHxMp57U5Ex+QnU3l1K78guGytqGtV245PbY+NuffVqSKmv66n7ICmGvaLlH+EskHSFCdyN3JKbBb8oZAKuBaM82MBbIc/YMvWjQf+gRH4kjQ5aeHbbsOX/CabICOX4U8UT28ntrM9xWTGgtouOorfD07EBCYr7Ye95+2OzVltMY57VBj0Zn1mxIwTAXInLkqM1ZOgVzLdQcniXS44LjdjJ5GRfmLHMd2DDAFIAoh8tJwvFNKz5zIL7IXeKsLFB4+IDNEh+STpi3MgKQAAYFwItwwAGs8DiOhc3aPcxZ8EACJnCh9/EkQKAIAhAyg8gHKpIjc/nt9+9RNWHOzM7pTt6WNEpWgPRE/yWS8/+T0kNrq8M44OVW8Ajmn3+GsTgkbABUjYTTbr0Z9+Jh0YvcXGWD1+QDfeXZzceIdwZIw/YF5uprenTVfNdyZC5LfqzU72w0b+Rh33JJQatDOb27klK7ASd9WpoPTnuPvqh6mD0m7hdRrAy0f4SOIGbwXdmsvdc997cHfHsGosexXYLcvtOX5KTrGMxwvbPSzh0cTxIeaZ4AvM5ogVSN+hVo2YAiPUvKG7Q+HEmwQO9bdPfQLM8MDBDBOYJu37+0ApZzZp/xB3BQ72rclol1Lqyfsnkw5Azez4Rx5nHvLHsXkK9uMnwgoAQM5SRrF0mUriRQm+E/9qDooxAACq9q6+NWXN3FlqKm6w++UlKVxI3Rf66T9rfKP1txwiP5sQ8bl6sl1gV8vikWh/+RW/n4wbqWOGgCW6rIOWrS1sf/clQ0JCQkJCQkJCQkJCQkJCQkJCQkJCQkJCQkJCQkJCQp4afwdRMMFLNhfN2wAAACV0RVh0ZGF0ZTpjcmVhdGUAMjAyNS0wMS0xN1QxODowMDozOSswMDowMDazUncAAAAldEVYdGRhdGU6bW9kaWZ5ADIwMjUtMDEtMTdUMTg6MDA6MzkrMDA6MDBH7urLAAAAAElFTkSuQmCC
+ server_info:
+ show: false
+ icon: null
+ name: null
+ label: null
+ vnc:
+ expose_details: true
+ ip: 192.168.4.2
+ hostname: null
+ port: 5901
+ enabled: 0
+ resources:
+ memory: 2048
+ storage: 10
+ traffic: 200
+ cpuCores: 1
+ cpu_model: null
+ network:
+ interfaces:
+ - order: 1
+ enabled: true
+ tag: 4238114467
+ name: eth0
+ mac: 00:C3:BA:23:37:B3
+ inAverage: 0
+ inPeak: 0
+ inBurst: 0
+ outAverage: 0
+ outPeak: 0
+ outBurst: 0
+ isNat: false
+ ipv4:
+ - order: 1
+ enabled: true
+ address: 192.168.4.21
+ gateway: 192.168.4.1
+ netmask: 255.255.254.0
+ resolver1: 8.8.8.8
+ resolver2: 8.8.4.4
+ - order: 2
+ enabled: true
+ address: 192.168.4.36
+ gateway: 192.168.4.1
+ netmask: 255.255.254.0
+ resolver1: 8.8.8.8
+ resolver2: 8.8.4.4
+ - order: 3
+ enabled: true
+ address: 192.168.4.37
+ gateway: 192.168.4.1
+ netmask: 255.255.254.0
+ resolver1: 8.8.8.8
+ resolver2: 8.8.4.4
+ ipv6: []
+ config:
+ uefi: false
+ bootOrder:
+ - hd
+ - cdrom
+ media:
+ isoMounted: false
+ isoName: ''
+ headers: {}
+ '401':
+ $ref: '#/components/responses/401'
+ description: ''
+ security:
+ - bearer: []
+ /servers/{serverId}/templates:
+ get:
+ summary: Retrieve OS templates available to a server
+ deprecated: false
+ description: ''
+ tags:
+ - Servers
+ parameters:
+ - name: serverId
+ in: path
+ description: A valid server ID as shown in VirtFusion.
+ required: true
+ example: 69
+ schema:
+ type: integer
+ responses:
+ '200':
+ description: ''
+ content:
+ application/json:
+ schema:
+ type: object
+ properties: {}
+ example:
+ data:
+ - name: Debian
+ description: >-
+ Debian GNU/Linux, is a Linux distribution composed of free
+ and open-source software, developed by the
+ community-supported Debian Project.
+ icon: debian_logo.png
+ templates:
+ - id: 8
+ name: Debian
+ version: 11 (Bullseye)
+ variant: Minimal
+ arch: 1
+ description: >-
+ Minimal installation with limited packages. New
+ packages are easily installed using Advanced Package
+ Tool (APT), the main command-line package manager for
+ Debian.
+ icon: debian_logo.png
+ eol: false
+ eol_date: '2024-03-12 00:00:00'
+ eol_warning: false
+ deploy_type: 1
+ vnc: false
+ type: linux
+ - id: 46
+ name: Debian
+ version: 12 (Bookworm)
+ variant: Minimal
+ arch: 1
+ description: >-
+ Minimal installation with limited packages. New
+ packages are easily installed using Advanced Package
+ Tool (APT), the main command-line package manager for
+ Debian.
+ icon: debian_logo.png
+ eol: false
+ eol_date: '2024-04-23 00:00:00'
+ eol_warning: false
+ deploy_type: 1
+ vnc: false
+ type: linux
+ - id: 56
+ name: Debian
+ version: 12 (Bookworm)
+ variant: Test
+ arch: 1
+ description: >-
+ Minimal installation with limited packages. New
+ packages are easily installed using Advanced Package
+ Tool (APT), the main command-line package manager for
+ Debian.
+ icon: debian_logo.png
+ eol: false
+ eol_date: '2024-04-23 00:00:00'
+ eol_warning: false
+ deploy_type: 1
+ vnc: true
+ type: linux
+ id: 1
+ - name: CentOS
+ description: >-
+ The CentOS Linux distribution is a stable, predictable,
+ manageable and reproducible platform derived from the
+ sources of Red Hat Enterprise Linux (RHEL).
+ icon: centos_logo.png
+ templates:
+ - id: 1
+ name: CentOS
+ version: '7'
+ variant: Minimal
+ arch: 1
+ description: >-
+ Minimal installation with limited packages. New
+ packages are easily installed using Yum, the main
+ command-line package manager for CentOS.
+ icon: centos_logo.png
+ eol: false
+ eol_date: '2024-03-12 00:00:00'
+ eol_warning: false
+ deploy_type: 1
+ vnc: false
+ type: linux
+ - id: 2
+ name: CentOS Stream
+ version: '9'
+ variant: Minimal
+ arch: 1
+ description: >-
+ Base installation with limited packages. New packages
+ are easily installed using DNF (yum), the main
+ command-line package manager for CentOS.
+ icon: centos_logo.png
+ eol: false
+ eol_date: '2024-03-12 00:00:00'
+ eol_warning: false
+ deploy_type: 1
+ vnc: false
+ type: linux
+ id: 2
+ - name: Rocky Linux
+ description: >-
+ Rocky Linux is a community enterprise operating system
+ designed to be 100% bug-for-bug compatible with America's
+ top enterprise Linux distribution now that its downstream
+ partner has shifted direction. It is under intensive
+ development by the community. Rocky Linux is led by
+ Gregory Kurtzer, founder of the CentOS project.
+ icon: rocky_linux_logo.png
+ templates:
+ - id: 7
+ name: Rocky Linux
+ version: '8'
+ variant: Minimal
+ arch: 1
+ description: >-
+ Minimal installation with limited packages. New
+ packages are easily installed using DNF (yum), the
+ main command-line package manager for Rocky Linux.
+ icon: rocky_linux_logo.png
+ eol: false
+ eol_date: '2024-03-12 00:00:00'
+ eol_warning: false
+ deploy_type: 1
+ vnc: false
+ type: linux
+ - id: 13
+ name: Rocky Linux
+ version: '9'
+ variant: Minimal
+ arch: 1
+ description: >-
+ Minimal installation with limited packages. New
+ packages are easily installed using DNF (yum), the
+ main command-line package manager for Rocky Linux.
+ icon: rocky_linux_logo.png
+ eol: false
+ eol_date: '2024-03-12 00:00:00'
+ eol_warning: false
+ deploy_type: 1
+ vnc: false
+ type: linux
+ id: 3
+ - name: AlmaLinux
+ description: >-
+ AlmaLinux OS is an open-source, community-driven project
+ that intends provide and alternative to the CentOS Stable
+ release. AlmaLinux is an OS that is 1:1 binary compatible
+ with RHEL® 8 and a global collaborative of the developer
+ community, industry, academia and research which build
+ upon this technology to empower humanity.
+ icon: almalinux_logo.png
+ templates:
+ - id: 6
+ name: AlmaLinux
+ version: '8'
+ variant: Minimal
+ arch: 1
+ description: >-
+ Minimal installation with limited packages. New
+ packages are easily installed using DNF (yum), the
+ main command-line package manager for AlmaLinux.
+ icon: almalinux_logo.png
+ eol: false
+ eol_date: '2024-03-12 00:00:00'
+ eol_warning: false
+ deploy_type: 1
+ vnc: false
+ type: linux
+ - id: 12
+ name: ARM -> AlmaLinux
+ version: '9'
+ variant: Latest
+ arch: 1
+ description: >-
+ Latest version with base packages. New packages are
+ easily installed using DNF (yum), the main
+ command-line package manager for AlmaLinux.
+ icon: almalinux_logo.png
+ eol: false
+ eol_date: '2024-03-12 00:00:00'
+ eol_warning: false
+ deploy_type: 1
+ vnc: false
+ type: linux
+ id: 4
+ - name: Ubuntu
+ description: >-
+ The most popular server Linux in the cloud and data
+ centre, you can rely on Ubuntu Server and its five years
+ of guaranteed free upgrades.
+ icon: ubuntu_logo.png
+ templates:
+ - id: 3
+ name: Ubuntu Server
+ version: 20.04 LTS (Focal Fossa)
+ variant: Minimal
+ arch: 1
+ description: >-
+ Minimal installation with limited packages. New
+ packages are easily installed using Advanced Package
+ Tool (APT), the main command-line package manager for
+ Ubuntu.
+ icon: ubuntu_logo.png
+ eol: false
+ eol_date: '2024-03-12 00:00:00'
+ eol_warning: false
+ deploy_type: 1
+ vnc: false
+ type: linux
+ - id: 4
+ name: Ubuntu Server
+ version: 18.04 LTS (Bionic Beaver)
+ variant: Minimal
+ arch: 1
+ description: >-
+ Minimal installation with limited packages. New
+ packages are easily installed using Advanced Package
+ Tool (APT), the main command-line package manager for
+ Ubuntu.
+ icon: ubuntu_logo.png
+ eol: false
+ eol_date: '2024-03-12 00:00:00'
+ eol_warning: false
+ deploy_type: 1
+ vnc: false
+ type: linux
+ - id: 9
+ name: Ubuntu Server
+ version: 22.04 LTS (Jammy Jellyfish)
+ variant: Minimal
+ arch: 1
+ description: >-
+ Minimal installation with limited packages. New
+ packages are easily installed using Advanced Package
+ Tool (APT), the main command-line package manager for
+ Ubuntu.
+ icon: ubuntu_logo.png
+ eol: false
+ eol_date: '2024-03-12 00:00:00'
+ eol_warning: false
+ deploy_type: 1
+ vnc: false
+ type: linux
+ - id: 49
+ name: Ubuntu Server
+ version: 24.04 LTS (Noble Numbat)
+ variant: Minimal
+ arch: 1
+ description: >-
+ Minimal installation with limited packages. New
+ packages are easily installed using Advanced Package
+ Tool (APT), the main command-line package manager for
+ Ubuntu.
+ icon: ubuntu_logo.png
+ eol: false
+ eol_date: '2024-04-25 00:00:00'
+ eol_warning: false
+ deploy_type: 1
+ vnc: false
+ type: linux
+ id: 5
+ - name: Fedora
+ description: >-
+ Fedora Server is a powerful, flexible operating system
+ that includes the best and latest datacenter technologies.
+ It puts you in control of all your infrastructure and
+ services.
+ icon: fedora_logo.png
+ templates:
+ - id: 11
+ name: Fedora
+ version: '37'
+ variant: Minimal
+ arch: 1
+ description: >-
+ Minimal installation with limited packages. New
+ packages are easily installed using DNF (yum), the
+ main command-line package manager for Fedora.
+ icon: fedora_logo.png
+ eol: false
+ eol_date: '2024-03-12 00:00:00'
+ eol_warning: false
+ deploy_type: 1
+ vnc: false
+ type: linux
+ - id: 14
+ name: Fedora
+ version: '38'
+ variant: Minimal
+ arch: 1
+ description: >-
+ Minimal installation with limited packages. New
+ packages are easily installed using DNF (yum), the
+ main command-line package manager for Fedora.
+ icon: fedora_logo.png
+ eol: false
+ eol_date: '2024-03-12 00:00:00'
+ eol_warning: false
+ deploy_type: 1
+ vnc: false
+ type: linux
+ - id: 15
+ name: Fedora
+ version: '39'
+ variant: Minimal
+ arch: 1
+ description: >-
+ Minimal installation with limited packages. New
+ packages are easily installed using DNF (yum), the
+ main command-line package manager for Fedora.
+ icon: fedora_logo.png
+ eol: false
+ eol_date: '2024-03-12 00:00:00'
+ eol_warning: false
+ deploy_type: 1
+ vnc: false
+ type: linux
+ - id: 59
+ name: Fedora
+ version: '41'
+ variant: Minimal
+ arch: 1
+ description: >-
+ Minimal installation with limited packages. New
+ packages are easily installed using DNF (yum), the
+ main command-line package manager for Fedora.
+ icon: fedora_logo.png
+ eol: false
+ eol_date: '2024-12-18 00:00:00'
+ eol_warning: false
+ deploy_type: 1
+ vnc: false
+ type: linux
+ id: 6
+ - name: FreeBSD
+ description: >-
+ FreeBSD is an operating system used to power modern
+ servers, desktops, and embedded platforms. A large
+ community has continually developed it for more than
+ thirty years. Its advanced networking, security, and
+ storage features have made FreeBSD the platform of choice
+ for many of the busiest web sites and most pervasive
+ embedded networking and storage devices.
+ icon: freebsd_logo.png
+ templates:
+ - id: 52
+ name: FreeBSD
+ version: '13.3'
+ variant: Minimal
+ arch: 1
+ description: Minimal installation with limited packages.
+ icon: freebsd_logo.png
+ eol: false
+ eol_date: '2024-05-15 00:00:00'
+ eol_warning: false
+ deploy_type: 1
+ vnc: false
+ type: unix
+ - id: 53
+ name: FreeBSD
+ version: '14.0'
+ variant: Minimal
+ arch: 1
+ description: Minimal installation with limited packages.
+ icon: freebsd_logo.png
+ eol: false
+ eol_date: '2024-05-15 00:00:00'
+ eol_warning: false
+ deploy_type: 1
+ vnc: false
+ type: unix
+ - id: 55
+ name: FreeBSD
+ version: '14.2'
+ variant: Minimal
+ arch: 1
+ description: Minimal installation with limited packages.
+ icon: freebsd_logo.png
+ eol: false
+ eol_date: '2024-10-20 00:00:00'
+ eol_warning: false
+ deploy_type: 1
+ vnc: true
+ type: unix
+ - id: 58
+ name: FreeBSD
+ version: '13.2'
+ variant: Minimal
+ arch: 1
+ description: Minimal installation with limited packages.
+ icon: freebsd_logo.png
+ eol: false
+ eol_date: '2024-12-10 00:00:00'
+ eol_warning: false
+ deploy_type: 1
+ vnc: false
+ type: unix
+ id: 7
+ - name: Other
+ description: ''
+ icon: linux_logo.png
+ templates:
+ - id: 5
+ name: openSUSE
+ version: Leap 15
+ variant: Minimal
+ arch: 1
+ description: >-
+ openSUSE is a project that serves to promote the use
+ of free and open-source software.
Minimal
+ installation with limited packages. New packages are
+ easily installed using Zypper, the main command-line
+ package manager for openSUSE.
+ icon: opensuse_logo.png
+ eol: false
+ eol_date: '2024-03-12 00:00:00'
+ eol_warning: false
+ deploy_type: 1
+ vnc: false
+ type: linux
+ id: 0
+ headers: {}
+ '401':
+ $ref: '#/components/responses/401'
+ description: ''
+ security:
+ - bearer: []
+ /servers/{serverId}/suspend:
+ post:
+ summary: Suspend a server
+ deprecated: false
+ description: ''
+ tags:
+ - Servers
+ parameters:
+ - name: serverId
+ in: path
+ description: A valid server ID as shown in VirtFusion.
+ required: true
+ example: 69
+ schema:
+ type: integer
+ responses:
+ '204':
+ description: ''
+ content:
+ application/octet-stream:
+ schema:
+ type: object
+ properties: {}
+ headers: {}
+ '401':
+ $ref: '#/components/responses/401'
+ description: ''
+ security:
+ - bearer: []
+ /servers/{serverId}/modify/cpuThrottle:
+ put:
+ summary: Throttle a servers CPU
+ deprecated: false
+ description: ''
+ tags:
+ - Servers
+ parameters:
+ - name: serverId
+ in: path
+ description: A valid server ID as shown in VirtFusion.
+ required: true
+ example: 69
+ schema:
+ type: integer
+ - name: sync
+ in: query
+ description: >-
+ Synchronise and apply the defined percentage. true|false Defaults to
+ false.
+ required: false
+ example: 'true'
+ schema:
+ type: boolean
+ requestBody:
+ content:
+ application/json:
+ schema:
+ type: object
+ properties:
+ percent:
+ type: integer
+ description: The percentage the CPU should be throttled (0-99).
+ required:
+ - percent
+ example:
+ percent: 50
+ responses:
+ '201':
+ description: ''
+ content:
+ application/octet-stream:
+ schema:
+ type: object
+ properties: {}
+ headers: {}
+ '401':
+ $ref: '#/components/responses/401'
+ description: ''
+ security:
+ - bearer: []
+ /servers/{serverId}/traffic:
+ get:
+ summary: Retrieve a servers traffic statistics
+ deprecated: false
+ description: ''
+ tags:
+ - Servers
+ parameters:
+ - name: serverId
+ in: path
+ description: A valid server ID as shown in VirtFusion.
+ required: true
+ example: 69
+ schema:
+ type: integer
+ responses:
+ '200':
+ description: ''
+ content:
+ application/json:
+ schema:
+ type: object
+ properties: {}
+ example:
+ data:
+ monthly:
+ - month: 2
+ start: '2025-01-06 00:00:00'
+ end: '2025-02-05 23:59:59'
+ rx: 1847110337
+ tx: 1270421
+ total: 1848380758
+ limit: 20000
+ blocks:
+ - id: 2
+ traffic: 100
+ - month: 1
+ start: '2024-12-06 00:00:00'
+ end: '2025-01-05 23:59:59'
+ rx: 5650592916
+ tx: 42336801
+ total: 5692929717
+ limit: 20000
+ blocks: []
+ headers: {}
+ '401':
+ $ref: '#/components/responses/401'
+ description: ''
+ security:
+ - bearer: []
+ /servers/{serverId}/unsuspend:
+ post:
+ summary: Unsuspend a server
+ deprecated: false
+ description: ''
+ tags:
+ - Servers
+ parameters:
+ - name: serverId
+ in: path
+ description: A valid server ID as shown in VirtFusion.
+ required: true
+ example: 69
+ schema:
+ type: integer
+ responses:
+ '204':
+ description: ''
+ content:
+ application/octet-stream:
+ schema:
+ type: object
+ properties: {}
+ headers: {}
+ '401':
+ $ref: '#/components/responses/401'
+ description: ''
+ security:
+ - bearer: []
+ /servers/{serverId}/vnc:
+ post:
+ summary: Enable or disable VNC
+ deprecated: false
+ description: ''
+ tags:
+ - Servers
+ parameters:
+ - name: serverId
+ in: path
+ description: A valid server ID as shown in VirtFusion.
+ required: true
+ example: 69
+ schema:
+ type: integer
+ requestBody:
+ content:
+ application/json:
+ schema:
+ type: object
+ properties:
+ action:
+ type: string
+ enum:
+ - enable
+ - disable
+ required:
+ - action
+ example:
+ action: enable
+ responses:
+ '200':
+ description: ''
+ content:
+ application/json:
+ schema:
+ type: object
+ properties: {}
+ example:
+ data:
+ vnc:
+ ip: 192.168.4.2
+ hostname: null
+ port: 5903
+ password: ZNYonJeU
+ wss:
+ token: 69316231-d34a-4d36-b754-ffd3253df96d
+ url: /vnc/?token=69316231-d34a-4d36-b754-ffd3253df96d
+ enabled: false
+ queueId: null
+ headers: {}
+ '401':
+ $ref: '#/components/responses/401'
+ description: ''
+ security:
+ - bearer: []
+ get:
+ summary: Retrive VNC details
+ deprecated: false
+ description: ''
+ tags:
+ - Servers
+ parameters:
+ - name: serverId
+ in: path
+ description: A valid server ID as shown in VirtFusion.
+ required: true
+ example: 69
+ schema:
+ type: integer
+ responses:
+ '200':
+ description: ''
+ content:
+ application/json:
+ schema:
+ type: object
+ properties: {}
+ example:
+ data:
+ vnc:
+ ip: 192.168.4.2
+ hostname: null
+ port: 5903
+ password: ZNYonJeU
+ wss:
+ token: 69316231-d34a-4d36-b754-ffd3253df96d
+ url: /vnc/?token=69316231-d34a-4d36-b754-ffd3253df96d
+ enabled: false
+ headers: {}
+ '401':
+ $ref: '#/components/responses/401'
+ description: ''
+ security:
+ - bearer: []
+ /servers/{serverId}/owner/{newOwnerId}:
+ put:
+ summary: Change owner
+ deprecated: false
+ description: ''
+ tags:
+ - Servers
+ parameters:
+ - name: serverId
+ in: path
+ description: A valid server ID as shown in VirtFusion.
+ required: true
+ example: 9
+ schema:
+ type: integer
+ - name: newOwnerId
+ in: path
+ description: A vailid user ID as shown in VirtFusion.
+ required: true
+ schema:
+ type: integer
+ responses:
+ '201':
+ description: ''
+ content:
+ application/octet-stream:
+ schema:
+ type: object
+ properties: {}
+ headers: {}
+ '401':
+ $ref: '#/components/responses/401'
+ description: ''
+ security:
+ - bearer: []
+ /servers/{serverId}/modify/memory:
+ put:
+ summary: Modify memory
+ deprecated: false
+ description: ''
+ tags:
+ - Servers
+ parameters:
+ - name: serverId
+ in: path
+ description: A valid server ID as shown in VirtFusion.
+ required: true
+ example: 69
+ schema:
+ type: integer
+ requestBody:
+ content:
+ application/json:
+ schema:
+ type: object
+ properties:
+ memory:
+ type: integer
+ description: The new memory value in MB.
+ minimum: 256
+ example: 1024
+ required:
+ - memory
+ example:
+ memory: 1024
+ responses:
+ '201':
+ description: ''
+ content:
+ application/octet-stream:
+ schema:
+ type: object
+ properties: {}
+ headers: {}
+ '401':
+ $ref: '#/components/responses/401'
+ description: ''
+ security:
+ - bearer: []
+ /servers/{serverId}/modify/cpuCores:
+ put:
+ summary: Modify CPU cores
+ deprecated: false
+ description: ''
+ tags:
+ - Servers
+ parameters:
+ - name: serverId
+ in: path
+ description: A valid server ID as shown in VirtFusion.
+ required: true
+ example: 69
+ schema:
+ type: integer
+ requestBody:
+ content:
+ application/json:
+ schema:
+ type: object
+ properties:
+ cores:
+ type: integer
+ description: The new core value.
+ minimum: 1
+ maximum: 600
+ example: 4
+ required:
+ - cores
+ example:
+ cores: 4
+ responses:
+ '201':
+ description: ''
+ content:
+ application/octet-stream:
+ schema:
+ type: object
+ properties: {}
+ headers: {}
+ '401':
+ $ref: '#/components/responses/401'
+ description: ''
+ security:
+ - bearer: []
+ /servers/{serverId}/customXML:
+ post:
+ summary: Set custom XML
+ deprecated: false
+ description: ''
+ tags:
+ - Servers
+ parameters:
+ - name: serverId
+ in: path
+ description: A valid server ID as shown in VirtFusion.
+ required: true
+ example: 69
+ schema:
+ type: integer
+ requestBody:
+ content:
+ application/json:
+ schema:
+ type: object
+ properties:
+ domain:
+ type: string
+ os:
+ type: string
+ devices:
+ type: string
+ features:
+ type: string
+ clock:
+ type: string
+ cpuTune:
+ type: string
+ domainEnabled:
+ type: boolean
+ osEnabled:
+ type: boolean
+ devicesEnabled:
+ type: boolean
+ featuresEnabled:
+ type: boolean
+ clockEnabled:
+ type: boolean
+ cpuTuneEnabled:
+ type: boolean
+ example:
+ domain:
+ os:
+ devices:
+ features:
+ clock:
+ cpuTune:
+ domainEnabled: true
+ osEnabled: true
+ devicesEnabled: true
+ featuresEnabled: true
+ clockEnabled: true
+ cpuTuneEnabled: true
+ responses:
+ '200':
+ description: ''
+ content:
+ application/json:
+ schema:
+ type: object
+ properties: {}
+ example: ''
+ headers: {}
+ '401':
+ $ref: '#/components/responses/401'
+ description: ''
+ security:
+ - bearer: []
+ /compute/hypervisors:
+ get:
+ summary: Retrieve hypervisors
+ deprecated: false
+ description: ''
+ tags:
+ - Hypervisors
+ parameters:
+ - name: results
+ in: query
+ description: >-
+ Number of results to return. Range between 1 and 200. Defaults to
+ 20.
+ required: false
+ example: 20
+ schema:
+ type: integer
+ responses:
+ '200':
+ description: ''
+ content:
+ application/json:
+ schema:
+ type: object
+ properties: {}
+ example:
+ current_page: 1
+ data:
+ - id: 1
+ commissioned: 3
+ ip: 192.168.4.10
+ ipAlt: null
+ hostname: null
+ port: 8892
+ sshPort: 22
+ name: PHV 1 (RED)
+ maintenance: false
+ enabled: true
+ nfType: 4
+ group:
+ id: 1
+ name: Default
+ description: Default hypervisor group
+ default: true
+ enabled: true
+ distributionType: 5
+ created: '2024-03-12T22:21:32+00:00'
+ updated: '2024-04-12T20:56:04+00:00'
+ encryptedToken: >-
+ eyJpdiI6Ik1Ua29ZSGp0QThxWVZhellzL2VTU3c9PSIsInZhbHVlIjoiNzc1eGdMMzFPUFpFZVpIbytzMDc1NzRsUHRJVnFTWFpKWS9WamJIaVJVMVZkSFZjZVM1YVB3bnlQeGt4eEhVamhrWGF4SnNqQVFES010Y3owUmJneTR4a05oRkp1R08xVXI1eHcvQ3NsbW5qU0dpUWhZbnFUMWYrTHM5L2NoZmhUQm9nRnV4b2Y0dENGLy9vanVDMnkwTG1mNXBYM1JVcE5TNWRCSGkvZS9qVEFsSWx5WXdXOU1wajIwam1DV1d4aUNXMUNGMThFNXI5THM4VWFmYnRFNkx3VHFaV3o3M0VVaEZXSHo0TVdKc0xSemJYVExUWEVlZHM0ZVNoUkk0ZEI2QnAySlVESVU2R0JDcWJMeG9YRUhIM0Vad2w2VHNGcFQ3R1BkbU1TbzU3V2JzbEJFNlUvSW90eGxNZkdqRjVmMGx6TTRIWEttYVA0Ti9JQkEwQURrWTRPL2k4VFJsNjhFTHh3UW1wSGMzUkxibEtDeDdlK2tOekQxVkh0bzhsWXY1RkxxaWRkSFBEQlNvM1l2akxqNitickp1TzR0ekhTbmdVSG5VUE5tMGh1WFJuejhscFpSS2dLcE1ZaS9NUlRKdnNUS0wzYWlDYjB1MVJhcmk4OEJoZURNQ3JROE5WcTZTdzV0Si9UeDhwMTFLK3lZV0NDdzB5b2NBZFhsM0hYMDJPMHlXS1g1MmxhNWdrOTRTSDJHbWNvODNuOUswMHJpYTVBL0YwRW9BVndsMllIdW95ZjBhZXdLUTRSR0xBelBVekViTCtKaG8wSGxPR1NOWmNSaXpxQ1hBUVdsdE9HMUhtc2YrRU14WkhOaUVVeWhXRlB2amtRRXkxZjY0cm85ekxVYWE1QU5zdlJDK2N6YmZrNHNOWk4xSTZXbUhxYklLTmgraTZFWHM9IiwibWFjIjoiOTY2ZmJkNzJkNzZmNmZmYTQzM2U4NDQzMDdhYTAzOWZhNTM0M2I1MDQyYWUwYzQ1ZGIyZTRlOGEwM2M0MTRhYiIsInRhZyI6IiJ9
+ maxServers: 0
+ maxCpu: 4
+ maxMemory: 6004
+ networks:
+ - id: 1
+ type: simpleBridge
+ bridge: br0
+ primary: true
+ default: true
+ created: '2024-03-12T22:37:15+00:00'
+ updated: '2024-03-12T22:37:40+00:00'
+ storage: []
+ created: '2024-03-12T22:37:15+00:00'
+ updated: '2024-05-10T11:27:52+00:00'
+ - id: 2
+ commissioned: 3
+ ip: 192.168.4.9
+ ipAlt: null
+ hostname: null
+ port: 8892
+ sshPort: 22
+ name: PHV 2 (BLUE)
+ maintenance: false
+ enabled: true
+ nfType: 4
+ group:
+ id: 1
+ name: Default
+ description: Default hypervisor group
+ default: true
+ enabled: true
+ distributionType: 5
+ created: '2024-03-12T22:21:32+00:00'
+ updated: '2024-04-12T20:56:04+00:00'
+ encryptedToken: >-
+ eyJpdiI6IjgyR2FZZmJwalVDYUxBR2hMdXdTNmc9PSIsInZhbHVlIjoiSFA1Tm44VzdZMUdZSXNZSTNUdmF5dGo5WjIrZEJlU04xQlVIOEZnUzc3dFZRY0ltc3pLdTZ2SFdkUXlQWUF5UEVaZUE3dXVKQXV6ajZvUTZiY0lBQmlnVllvRDZMSDBYK0ZMV0d5dzRTZlYwaDFRcllsNEdGaTljQnpnbEg5Umt6SmdBZ3ZQL0RneTZEUHhKanFGZU9hSWtvR29lYVlLdzk5NTJNZE1hbExSaWtuMkE4cTVaSGxSbWlJZ3pHejhFWnFxbEltNUcrSXVIdE4rQW9ET2R0M0MrK0RHOXNhOFFuVEw1R2k4eEpDNmZiNWJPVS9NL2xrVk40eG93NzQxaTRFN0pBR0FEL2JTclIvd2xWM3JkbnltZGhrc0xkUzV0SGtKNVoyU2JFY2M0S3dyVXEzS256b1ZHOVRvSWlmNm9OT0d6TktEWUduaVBHT2VHaFpqakU2SjFhU2lqTUZPeVdRN3dWSjhnakVQYkFiVmpCK05ja3BVU3FxakNjUUEzRHl3WUZweFJuQ0FBVkR2eTcxLzVVR2ZPNHU2bDJGRTJ6bVkrZ201akZXT0JIeHByK2VQVmMwUEJ5aDM0TWI0RmViakprM0phVXRVMFUvU1Y5M0FCRTBORm1aNWtGUklRbW51Y010NWIzUE02Vno3SkU2MVk4WTUwRy9QUndTZEgwWmRiLzhiV0w1c0ZsNkRMZkNycUlabWQrQ0F5cnYxamgxcWZjNjk3NXlMZHNMdnZqZkhKdG1sY2VLVmFPUTI3OTJ4UVdGLzF4SE83Y0N5dEhNNUhSQWhoZ29uUXMvR3dGeHVUMlRjc3dYZkVrYVVUMWZVQjhZOVRBT1RXYk84bFpqbGZ5RGQ2Rncvb2lQbVh0djBnSHUwSWJKbnAzQmQrL1VIK1JOK2N4NE09IiwibWFjIjoiNGUyNTIxYTIyNDBjZmRiYmEzNmQ5NTc4MGZmMTU4ZjgwN2Q2OTQzYzFhYTgxODVmYzkyMmU3YWNiOGRmNWZjZSIsInRhZyI6IiJ9
+ maxServers: 0
+ maxCpu: 28
+ maxMemory: 10000
+ networks:
+ - id: 2
+ type: simpleBridge
+ bridge: br0
+ primary: true
+ default: true
+ created: '2024-03-16T19:31:43+00:00'
+ updated: '2024-03-16T19:32:46+00:00'
+ storage: []
+ created: '2024-03-16T19:31:43+00:00'
+ updated: '2024-04-26T16:41:51+00:00'
+ - id: 3
+ commissioned: 3
+ ip: 192.168.4.12
+ ipAlt: null
+ hostname: null
+ port: 8892
+ sshPort: 22
+ name: BHV 9
+ maintenance: false
+ enabled: true
+ nfType: 4
+ group:
+ id: 1
+ name: Default
+ description: Default hypervisor group
+ default: true
+ enabled: true
+ distributionType: 5
+ created: '2024-03-12T22:21:32+00:00'
+ updated: '2024-04-12T20:56:04+00:00'
+ encryptedToken: >-
+ eyJpdiI6IlZ6MFN4dnlvQm9DTXNsaDM3YWg1aUE9PSIsInZhbHVlIjoiamlrbEhzRlY5d0RRaUxMYkZIUEV2UHh5eVJVMHEwOGQ2QkVIS0tydW82Um15bGFPVHJQbmUzMDMvbGxyZkEyR0MwT1JUR3ZNaDUvZWE5UFYwOExOU2xKNDhUa2g5VnNqQ2NoamptNnp3dmY0VVhzSXEwTEsxWDRwMDdtMyttdmp2UVRHOFJsb3Q3VkpKWEw0N3JmelAxNWZGWTVRQ1lCWHFpM3N5anFnaDNlcVFWazV6ci9Fem9xQTYrYXpNeUw0a3Jobm85aFRweCsxQVNoOWJrVktveWczYm5CL3VyNVhqWHlFbEpRYUtINzhwMCtEN3N4aEEwdTQ0YzdSbVhqQk9BTDVkN3F0aDFHWngwQU1iNDJKT1BRT25LYjZacklPM1llL2hQRWJab1l5QVdTQUtiVkZXcXROZC9xOWxLdzROTUprRkUxWkNjY2l3TnIzYXk5YiswNkhIQTlKejI3YXhPc2xXRklETmtYRkNNWlIzT1RHZkZTVGMvY1lra2JaemNQcW8vUEFLbEROS3dJQkorSVNUeTJESzZtV2tUV0Q0Nk5QVWRvUnJUbWhkVFlwZmphNXZXanFUTi9SbnVacTJXUzhYZW8zby95RG9jVWJDT25UMHU3dVZSN1UrS3RxRFhlM3diYkhxL1g3OXZIdmwzUzhCZmpjN3ZpMkhlRlNSMmNPMzduektWRGpYOFQ2UktIQjdnaEVoZy95MmZYK0c1dTZOemZ0VXpxbHpneVlndkp0anNuN2Y4bXlScXhoWFQrai9yL2wrWWhLNGlGWGVhc09iSEQrRzYrOThCY1czUTdnd0pOTFdSZ05uNUU5QUZPVmtHOENBRDljOFN1UjBteUdacmZYZWdtM2RodXg4dEx5cExGZVZ0TTNrQTVVTFlTZU0vZ28xLzA9IiwibWFjIjoiNzdmZTc5MDI2NjY3OTc2NzhmMjJiMWY3ZGNjNGI4YmNkZTNiNzE0MmNlODdkYzNlNzIxNDU5NmVlMjJmODNhYSIsInRhZyI6IiJ9
+ maxServers: 0
+ maxCpu: 64
+ maxMemory: 27913
+ networks:
+ - id: 3
+ type: simpleBridge
+ bridge: br0
+ primary: true
+ default: true
+ created: '2024-03-29T20:10:22+00:00'
+ updated: '2024-04-09T11:32:14+00:00'
+ storage: []
+ created: '2024-03-29T20:10:22+00:00'
+ updated: '2024-04-16T10:44:21+00:00'
+ - id: 4
+ commissioned: 3
+ ip: 192.168.4.11
+ ipAlt: null
+ hostname: null
+ port: 8892
+ sshPort: 22
+ name: BHV 8
+ maintenance: false
+ enabled: true
+ nfType: 4
+ group:
+ id: 1
+ name: Default
+ description: Default hypervisor group
+ default: true
+ enabled: true
+ distributionType: 5
+ created: '2024-03-12T22:21:32+00:00'
+ updated: '2024-04-12T20:56:04+00:00'
+ encryptedToken: >-
+ eyJpdiI6ImpjS3JqNWhlOHl6Rm5RUk80WkRVQkE9PSIsInZhbHVlIjoiUXNSVEhXMWhJM2hrZ2gza3hBVGtQUjJMenp4bThNL2d0ajYyUGNTQ2s2TjMxZEZ3ZVJVYzQxbUhjSXZMd2greWY5MEl3ajFLK3RrL1BpSzdZbCtzRkpObisvNktxanpZZzBORFpHRUdnN2t3cmN2YXJBbnhQbWRkQ0FUVkozQTVwanlYZHJxVlF2Y3VBOWVBb21FdytUT3Z0cUdrMTU0V3YxM0w1VUh2NER4VkpESngvK0kwNmp6eHFVSDloSWxEd2t4aHV4UnM2c1kyRFRjTDJ5TXJ0dFZPRjZNL3YxKzFpODlzRHFtak5PRm1pVDFBdHJwNGhqNEZiV05Fd1c2OWlkeWxuTWdUT1Z1STE4MjFIMnBoaDA1WWhmSitFMFdnZGdqZ0lZSlBzODd5Y3hDVzNCYWFwSHlHV1hDU0lXZDJKS3RsYWN4VU9EUmJqeTM3Vy9RSXFKTGxkRnZlMnhjWm5mekRrd0VZVjEzeVFhMDFGazJKbXNOVTdCUzdTcW9PbzFsdkEzZE9nK0k4dW5ndXB6OTJDbUFWZk5hU1JveExMSGFzMnNsQzFUSzRDSHVaQkkvR2JseFFwT1BDTWcvZjlqaU5IdDJPMnRTRjJrVmxuYjJzeDRBT2NTNjl5V2hVc3c3UC9ucG11UFozZjErOFovcXVZQlJHSkt0RWJrcVFvY2NyMDdDZ0JFOE5SVklJa0loSVIvSStwVWh6b0twV1NLT0tOaXF3N1EvQkJUTmhYRit3eFR2VGxMc3NMSDVSUWtyMlZCck43ZkFYVXMrVEpoVXhHSVJlN2pMOENnL1R4SzVIeHkrWkVWKzlhRmN5RWdmK1IzaGpJZ0VtbUMvTzJvRnI2TW93RmpyNXlITXB3TGpteGZ6NmZpUEh4NlNVMytsd0hsS0kxRDA9IiwibWFjIjoiNWFkMTQ4YmQ1MGJjMTE3N2QzN2JhOGJkOTM1NzBlNjIzYjFkM2UyMjkxYjI3OWJmMTkxMmVmYjcxNzEwZTNkNCIsInRhZyI6IiJ9
+ maxServers: 0
+ maxCpu: 16
+ maxMemory: 27913
+ networks:
+ - id: 4
+ type: macVTap
+ bridge: eth0
+ primary: true
+ default: true
+ created: '2024-03-29T20:10:42+00:00'
+ updated: '2024-03-29T20:10:42+00:00'
+ storage: []
+ created: '2024-03-29T20:10:42+00:00'
+ updated: '2024-04-16T10:44:21+00:00'
+ - id: 6
+ commissioned: 3
+ ip: 192.168.4.2
+ ipAlt: 123.123.123.121
+ hostname: null
+ port: 8892
+ sshPort: 22
+ name: BHV 1
+ maintenance: false
+ enabled: true
+ nfType: 4
+ group:
+ id: 2
+ name: Test
+ description: null
+ default: false
+ enabled: true
+ distributionType: 13
+ created: '2024-10-08T13:23:28+00:00'
+ updated: '2024-10-08T13:23:42+00:00'
+ encryptedToken: >-
+ eyJpdiI6ImQ0M1hybkc5bDJaQ2IrakJFUGZ0MEE9PSIsInZhbHVlIjoiNW1ETjRDeGMxZDlDOWx3M1ZmRkFBZG84dGRwbEZVRVlwc0JJT202UXhHekJ0cS9wNWpIUjJiUzJhaG0ySCs4aHpLakF2RnNJTjVPZ09hN0ttcEg4bkJrdjRqd2pxNHlPcTBaNnBDZ2VwTllUNzNnRTdBUlVHM245VDVhWkxhYkZ5MnRmQXZJMjZkQWxkV3BmbVdZNWt0clR2UlNGTEZES0kzaksyd2xmZFJITlRRMU0yUkp2WkNRU0lYUlRkWWF3NWxxY1cyUFo1WFByczZIak4wMnl4VmdSbUs0b0RjVWNacmZmcDM1VTgyWlo1OGYydnBXVGNOdHZIRXA3YnNIZFIzTXJiUmorcVExc216R3VyeTQ0U2JTeDZZbzlBcHN2MFNyczlNZGZPM1M5K2FuUVQrNVc1UHVuTEZUSC8yMi9FWHVOYUphblNPVnZsQ2RhVGdrSE5zczlyTEF6QTM5cTBrci8yRy9DM204NWJxOUZBZjdhTmRFZnc1UWcwVUM1L3dveGpvb2tleEd2eVF0amlSN2VRYzdlS3kzQUtMRVk2WkFma25aVjN5OWRURDZFN3JnWU5UVDRkTjRWc0Rib1JIdXAwSlZQTVViWHJTNlIrbFBvb1M0MHVOOEU4VGlBdGZ4dVI0V1BwS3dnempYSC94bmRXdHdET2FEUGZHVXptNzFQQjM3UnRwbWtSQW1wY2xscGxTTmRvZzJpQ0pEWXdoYWRGTk03aytWbGJyVUh3ZGliVWN1NGVEM1lRNjFVV3oxYkNOL0NJUFUycVlQQ212S0NsYitIK2p5QkxwdUk2bk4vL0l6QktraCtqR2lnYkYzNU1IazBPVG42RGlCVkVBV2JKcGI4My9lNDl4N29vQUNJempjaGtlQU1tVWlHZDBDUFkwVU1lTmM9IiwibWFjIjoiMTk1MWEwNGQyYzdiOGM5ZTQ4NDBjZWI2N2JjYTJjYmFhNThhOGUyMDE1MTdiZDdmN2E2YjE4MjZmZjk1OTcxYiIsInRhZyI6IiJ9
+ maxServers: 0
+ maxCpu: 128
+ maxMemory: 29419
+ networks:
+ - id: 6
+ type: simpleBridge
+ bridge: br0
+ primary: true
+ default: true
+ created: '2024-03-30T09:53:38+00:00'
+ updated: '2025-01-15T13:31:56+00:00'
+ - id: 17
+ type: lvBridgeOVS
+ bridge: bhv1
+ primary: false
+ default: false
+ created: '2024-05-17T11:25:57+00:00'
+ updated: '2024-05-17T11:25:57+00:00'
+ storage: []
+ created: '2024-03-30T09:53:38+00:00'
+ updated: '2024-12-06T21:25:54+00:00'
+ first_page_url: >-
+ https://192.168.3.11/api/v1/compute/hypervisors?results=5&page=1
+ from: 1
+ last_page: 3
+ last_page_url: >-
+ https://192.168.3.11/api/v1/compute/hypervisors?results=5&page=3
+ links:
+ - url: null
+ label: '« Previous'
+ active: false
+ - url: >-
+ https://192.168.3.11/api/v1/compute/hypervisors?results=5&page=1
+ label: '1'
+ active: true
+ - url: >-
+ https://192.168.3.11/api/v1/compute/hypervisors?results=5&page=2
+ label: '2'
+ active: false
+ - url: >-
+ https://192.168.3.11/api/v1/compute/hypervisors?results=5&page=3
+ label: '3'
+ active: false
+ - url: >-
+ https://192.168.3.11/api/v1/compute/hypervisors?results=5&page=2
+ label: Next »
+ active: false
+ next_page_url: >-
+ https://192.168.3.11/api/v1/compute/hypervisors?results=5&page=2
+ path: https://192.168.3.11/api/v1/compute/hypervisors
+ per_page: 5
+ prev_page_url: null
+ to: 5
+ total: 14
+ headers: {}
+ '401':
+ $ref: '#/components/responses/401'
+ description: ''
+ security:
+ - bearer: []
+ /compute/hypervisors/{hypervisorId}:
+ get:
+ summary: Retrive a Hypervisor
+ deprecated: false
+ description: ''
+ tags:
+ - Hypervisors
+ parameters:
+ - name: hypervisorId
+ in: path
+ description: A valid hypervisor ID as shown in VirtFusion.
+ required: true
+ example: 1
+ schema:
+ type: integer
+ responses:
+ '200':
+ description: ''
+ content:
+ application/json:
+ schema:
+ type: object
+ properties: {}
+ example:
+ data:
+ id: 1
+ commissioned: 3
+ ip: 192.168.4.10
+ ipAlt: null
+ hostname: null
+ port: 8892
+ sshPort: 22
+ name: PHV 1 (RED)
+ maintenance: false
+ enabled: true
+ nfType: 4
+ group:
+ id: 1
+ name: Default
+ description: Default hypervisor group
+ default: true
+ enabled: true
+ distributionType: 5
+ created: '2024-03-12T22:21:32+00:00'
+ updated: '2024-04-12T20:56:04+00:00'
+ encryptedToken: >-
+ eyJpdiI6Ik1Ua29ZSGp0QThxWVZhellzL2VTU3c9PSIsInZhbHVlIjoiNzc1eGdMMzFPUFpFZVpIbytzMDc1NzRsUHRJVnFTWFpKWS9WamJIaVJVMVZkSFZjZVM1YVB3bnlQeGt4eEhVamhrWGF4SnNqQVFES010Y3owUmJneTR4a05oRkp1R08xVXI1eHcvQ3NsbW5qU0dpUWhZbnFUMWYrTHM5L2NoZmhUQm9nRnV4b2Y0dENGLy9vanVDMnkwTG1mNXBYM1JVcE5TNWRCSGkvZS9qVEFsSWx5WXdXOU1wajIwam1DV1d4aUNXMUNGMThFNXI5THM4VWFmYnRFNkx3VHFaV3o3M0VVaEZXSHo0TVdKc0xSemJYVExUWEVlZHM0ZVNoUkk0ZEI2QnAySlVESVU2R0JDcWJMeG9YRUhIM0Vad2w2VHNGcFQ3R1BkbU1TbzU3V2JzbEJFNlUvSW90eGxNZkdqRjVmMGx6TTRIWEttYVA0Ti9JQkEwQURrWTRPL2k4VFJsNjhFTHh3UW1wSGMzUkxibEtDeDdlK2tOekQxVkh0bzhsWXY1RkxxaWRkSFBEQlNvM1l2akxqNitickp1TzR0ekhTbmdVSG5VUE5tMGh1WFJuejhscFpSS2dLcE1ZaS9NUlRKdnNUS0wzYWlDYjB1MVJhcmk4OEJoZURNQ3JROE5WcTZTdzV0Si9UeDhwMTFLK3lZV0NDdzB5b2NBZFhsM0hYMDJPMHlXS1g1MmxhNWdrOTRTSDJHbWNvODNuOUswMHJpYTVBL0YwRW9BVndsMllIdW95ZjBhZXdLUTRSR0xBelBVekViTCtKaG8wSGxPR1NOWmNSaXpxQ1hBUVdsdE9HMUhtc2YrRU14WkhOaUVVeWhXRlB2amtRRXkxZjY0cm85ekxVYWE1QU5zdlJDK2N6YmZrNHNOWk4xSTZXbUhxYklLTmgraTZFWHM9IiwibWFjIjoiOTY2ZmJkNzJkNzZmNmZmYTQzM2U4NDQzMDdhYTAzOWZhNTM0M2I1MDQyYWUwYzQ1ZGIyZTRlOGEwM2M0MTRhYiIsInRhZyI6IiJ9
+ maxServers: 0
+ maxCpu: 4
+ maxMemory: 6004
+ created: '2024-03-12T22:37:15+00:00'
+ updated: '2024-05-10T11:27:52+00:00'
+ networks:
+ - id: 1
+ type: simpleBridge
+ bridge: br0
+ primary: true
+ default: true
+ created: '2024-03-12T22:37:15+00:00'
+ updated: '2024-03-12T22:37:40+00:00'
+ headers: {}
+ '401':
+ $ref: '#/components/responses/401'
+ description: ''
+ security:
+ - bearer: []
+ /connectivity/ipblocks/{blockId}/ipv4:
+ post:
+ summary: Add an IPv4 range to an IP block
+ deprecated: false
+ description: ''
+ tags:
+ - IP Blocks
+ parameters:
+ - name: blockId
+ in: path
+ description: A valid IPv4 block ID as shown in VirtFusion.
+ required: true
+ example: 1
+ schema:
+ type: integer
+ requestBody:
+ content:
+ application/json:
+ schema:
+ type: object
+ properties:
+ type:
+ type: string
+ description: Must be set to range.
+ start:
+ type: string
+ description: Start of IPv4 range.
+ end:
+ type: string
+ description: End of IPv4 range.
+ required:
+ - type
+ - start
+ - end
+ example:
+ type: range
+ start: 192.168.1.2
+ end: 192.168.1.10
+ responses:
+ '204':
+ description: ''
+ content:
+ text/css:
+ schema:
+ type: object
+ properties: {}
+ headers: {}
+ '401':
+ $ref: '#/components/responses/401'
+ description: ''
+ security:
+ - bearer: []
+ /connectivity/ipblocks:
+ get:
+ summary: Retrieve IP blocks
+ deprecated: false
+ description: ''
+ tags:
+ - IP Blocks
+ parameters:
+ - name: results
+ in: query
+ description: >-
+ Number of results to return. Range between 1 and 200. Defaults to
+ 20.
+ required: false
+ schema:
+ type: integer
+ responses:
+ '200':
+ description: ''
+ content:
+ application/json:
+ schema:
+ type: object
+ properties: {}
+ example:
+ current_page: 1
+ data:
+ - id: 1
+ type: 4
+ name: 192.168.4.0/23
+ ipv4:
+ gateway: 192.168.4.1
+ netmask: 255.255.254.0
+ resolvers:
+ primary: 8.8.8.8
+ secondary: 8.8.4.4
+ total: 521
+ usedTotal: 21
+ freeTotal: 500
+ ipv6:
+ gateway: null
+ resolvers:
+ primary: null
+ secondary: null
+ subnet: null
+ from: 48
+ to: 64
+ restricted: []
+ total: 0
+ generatedTotal: 0
+ usedTotal: 0
+ freeTotal: 0
+ freeGenerated: 0
+ blacklistedTotal: 0
+ rdnsType: 0
+ rdnsZoneId: null
+ networkProfile: 0
+ routeBlock: null
+ dhcp: 1
+ enabled: true
+ created: '2024-03-12T22:40:23+00:00'
+ updated: '2024-12-06T21:53:15+00:00'
+ - id: 2
+ type: 6
+ name: PDNS TEST
+ ipv4:
+ gateway: null
+ netmask: null
+ resolvers:
+ primary: null
+ secondary: null
+ total: 0
+ usedTotal: 0
+ freeTotal: 0
+ ipv6:
+ gateway: 2a03:3a61:a1::1
+ resolvers:
+ primary: 2001:4860:4860::8888
+ secondary: 2001:4860:4860::8844
+ subnet: '2a03:3a61:a1::'
+ from: 48
+ to: 64
+ restricted: []
+ total: 65535
+ generatedTotal: 300
+ usedTotal: 0
+ freeTotal: 65535
+ freeGenerated: 300
+ blacklistedTotal: 0
+ rdnsType: 2
+ rdnsZoneId: 1
+ networkProfile: 0
+ routeBlock: null
+ dhcp: 1
+ enabled: true
+ created: '2024-04-26T11:41:41+00:00'
+ updated: '2024-12-31T10:23:33+00:00'
+ - id: 3
+ type: 4
+ name: 192.168.30.200-240
+ ipv4:
+ gateway: 192.168.30.1
+ netmask: 255.255.255.0
+ resolvers:
+ primary: 8.8.8.8
+ secondary: 8.8.4.4
+ total: 41
+ usedTotal: 8
+ freeTotal: 33
+ ipv6:
+ gateway: null
+ resolvers:
+ primary: null
+ secondary: null
+ subnet: null
+ from: 48
+ to: 64
+ restricted: []
+ total: 0
+ generatedTotal: 0
+ usedTotal: 0
+ freeTotal: 0
+ freeGenerated: 0
+ blacklistedTotal: 0
+ rdnsType: 0
+ rdnsZoneId: null
+ networkProfile: 0
+ routeBlock: null
+ dhcp: 1
+ enabled: true
+ created: '2024-05-14T10:43:52+00:00'
+ updated: '2024-05-14T10:44:25+00:00'
+ - id: 4
+ type: 4
+ name: 10.1.1.0/24
+ ipv4:
+ gateway: null
+ netmask: 255.255.255.255
+ resolvers:
+ primary: 8.8.8.8
+ secondary: 8.8.4.4
+ total: 0
+ usedTotal: 0
+ freeTotal: 0
+ ipv6:
+ gateway: null
+ resolvers:
+ primary: null
+ secondary: null
+ subnet: null
+ from: 48
+ to: 64
+ restricted: []
+ total: 0
+ generatedTotal: 0
+ usedTotal: 0
+ freeTotal: 0
+ freeGenerated: 0
+ blacklistedTotal: 0
+ rdnsType: 0
+ rdnsZoneId: null
+ networkProfile: 0
+ routeBlock: null
+ dhcp: 1
+ enabled: true
+ created: '2024-05-16T18:11:03+00:00'
+ updated: '2024-05-17T13:22:04+00:00'
+ - id: 5
+ type: 6
+ name: V6 For BHV 1,3
+ ipv4:
+ gateway: null
+ netmask: null
+ resolvers:
+ primary: null
+ secondary: null
+ total: 0
+ usedTotal: 0
+ freeTotal: 0
+ ipv6:
+ gateway: 2001:db8:abcd:12::1
+ resolvers:
+ primary: 2001:4860:4860::8888
+ secondary: 2001:4860:4860::8844
+ subnet: '2001:db8:abcd:12::'
+ from: 64
+ to: 80
+ restricted: []
+ total: 65535
+ generatedTotal: 1100
+ usedTotal: 9
+ freeTotal: 65526
+ freeGenerated: 1091
+ blacklistedTotal: 0
+ rdnsType: 0
+ rdnsZoneId: null
+ networkProfile: 0
+ routeBlock: null
+ dhcp: 1
+ enabled: true
+ created: '2024-09-19T17:23:05+00:00'
+ updated: '2024-12-06T21:23:55+00:00'
+ first_page_url: https://192.168.3.11/api/v1/connectivity/ipblocks?page=1
+ from: 1
+ last_page: 1
+ last_page_url: https://192.168.3.11/api/v1/connectivity/ipblocks?page=1
+ links:
+ - url: null
+ label: '« Previous'
+ active: false
+ - url: https://192.168.3.11/api/v1/connectivity/ipblocks?page=1
+ label: '1'
+ active: true
+ - url: null
+ label: Next »
+ active: false
+ next_page_url: null
+ path: https://192.168.3.11/api/v1/connectivity/ipblocks
+ per_page: 20
+ prev_page_url: null
+ to: 5
+ total: 5
+ headers: {}
+ '401':
+ $ref: '#/components/responses/401'
+ description: ''
+ security:
+ - bearer: []
+ /connectivity/ipblocks/{blockId}:
+ get:
+ summary: Retrieve an IP block
+ deprecated: false
+ description: ''
+ tags:
+ - IP Blocks
+ parameters:
+ - name: blockId
+ in: path
+ description: A valid IP block ID as shown in VirtFusion.
+ required: true
+ example: 1
+ schema:
+ type: integer
+ responses:
+ '200':
+ description: ''
+ content:
+ application/json:
+ schema:
+ type: object
+ properties: {}
+ example:
+ data:
+ id: 1
+ type: 4
+ name: 192.168.4.0/23
+ ipv4:
+ gateway: 192.168.4.1
+ netmask: 255.255.254.0
+ resolvers:
+ primary: 8.8.8.8
+ secondary: 8.8.4.4
+ total: 521
+ usedTotal: 21
+ freeTotal: 500
+ ipv6:
+ gateway: null
+ resolvers:
+ primary: null
+ secondary: null
+ subnet: null
+ from: 48
+ to: 64
+ restricted: []
+ total: 0
+ generatedTotal: 0
+ usedTotal: 0
+ freeTotal: 0
+ freeGenerated: 0
+ blacklistedTotal: 0
+ rdnsType: 0
+ rdnsZoneId: null
+ networkProfile: 0
+ routeBlock: null
+ dhcp: 1
+ enabled: true
+ created: '2024-03-12T22:40:23+00:00'
+ updated: '2024-12-06T21:53:15+00:00'
+ headers: {}
+ '401':
+ $ref: '#/components/responses/401'
+ description: ''
+ security:
+ - bearer: []
+ /backups/server/{serverId}:
+ get:
+ summary: Retrieve a server backups
+ deprecated: false
+ description: ''
+ tags:
+ - Backups
+ parameters:
+ - name: serverId
+ in: path
+ description: A valid server ID as shown in VirtFusion.
+ required: true
+ example: 1
+ schema:
+ type: integer
+ responses:
+ '200':
+ description: ''
+ content:
+ application/json:
+ schema:
+ type: object
+ properties: {}
+ example:
+ data:
+ - id: 42
+ serverId: 202
+ storage:
+ id: 5
+ name: Backup Server 1
+ enabled: true
+ deleting: false
+ restoring: false
+ progress: false
+ complete: true
+ deleteAfter: null
+ created: '2022-03-03T20:25:01+00:00'
+ updated: '2022-03-03T20:26:01+00:00'
+ - id: 49
+ serverId: 202
+ storage:
+ id: 5
+ name: Backup Server 1
+ enabled: true
+ deleting: false
+ restoring: false
+ progress: false
+ complete: true
+ deleteAfter: null
+ created: '2022-03-04T20:25:01+00:00'
+ updated: '2022-03-04T20:26:01+00:00'
+ headers: {}
+ '401':
+ $ref: '#/components/responses/401'
+ description: ''
+ security:
+ - bearer: []
+ /dns/services/{serviceId}:
+ get:
+ summary: Retrieve a DNS service
+ deprecated: false
+ description: ''
+ tags:
+ - DNS
+ parameters:
+ - name: serviceId
+ in: path
+ description: A valid DNS service ID as shown in VirtFusion.
+ required: true
+ example: '1'
+ schema:
+ type: string
+ responses:
+ '200':
+ description: ''
+ content:
+ application/json:
+ schema:
+ type: object
+ properties: {}
+ example:
+ data:
+ id: 4
+ type: 1
+ name: ClouDNS
+ username: '456754'
+ url: https://api.cloudns.net
+ ip: null
+ port: 443
+ password: >-
+ eyJpdiI6IjVUOU11S09KNmFtNnlqLzRzR0FYd1E9PSIsInZhbHVlIjoiS01SNjdhbEt1TzFVMHM0Nk1lY2Z0bnl5cUJJUDlxeUF0VXdtTTUwWW41QT0iLCJtYWMiOiI4NTBlNzFhNzJmNTkwMTA1ODQ0MjU4OTUzNjM0MzAxN2QwYzY5OTdiMTgzNDg3ZGFjMmU5NjE0Y2E3YTE1NWVjIiwidGFnIjoiIn0=
+ config: {}
+ subAccount: false
+ capabilities: 1
+ enabled: true
+ created: '2022-02-11T11:55:49+00:00'
+ updated: '2022-02-14T22:45:43+00:00'
+ headers: {}
+ '401':
+ $ref: '#/components/responses/401'
+ description: ''
+ security:
+ - bearer: []
+ /media/iso/{isoId}:
+ get:
+ summary: Retrieve an ISO
+ deprecated: false
+ description: ''
+ tags:
+ - Media
+ parameters:
+ - name: isoId
+ in: path
+ description: A valid ISO ID as shown in VirtFusion.
+ required: true
+ example: '1'
+ schema:
+ type: string
+ responses:
+ '200':
+ description: ''
+ content:
+ application/json:
+ schema:
+ type: object
+ properties: {}
+ example:
+ data:
+ id: 1
+ name: Deb Arch
+ description: null
+ arch: 2
+ url: >-
+ https://cdimage.debian.org/debian-cd/current/arm64/iso-cd/debian-12.5.0-arm64-netinst.iso
+ filename: deb-arc
+ enabled: true
+ config: '[]'
+ global: true
+ download: true
+ users: []
+ created: '2024-03-13T09:34:54+00:00'
+ updated: '2024-04-01T20:34:05+00:00'
+ headers: {}
+ '401':
+ $ref: '#/components/responses/401'
+ description: ''
+ security:
+ - bearer: []
+ /media/templates/fromServerPackageSpec/{serverPackageId}:
+ get:
+ summary: Retrieve operating system templates that are available for a package
+ deprecated: false
+ description: ''
+ tags:
+ - Media
+ parameters:
+ - name: serverPackageId
+ in: path
+ description: A valid server package ID as shown in VirtFusion.
+ required: true
+ example: 1
+ schema:
+ type: integer
+ responses:
+ '200':
+ description: ''
+ content:
+ application/json:
+ schema:
+ type: object
+ properties: {}
+ example:
+ data:
+ - name: Debian
+ description: >-
+ Debian GNU/Linux, is a Linux distribution composed of free
+ and open-source software, developed by the
+ community-supported Debian Project.
+ icon: debian_logo.png
+ templates:
+ - id: 8
+ name: Debian
+ version: 11 (Bullseye)
+ variant: Minimal
+ arch: 1
+ description: >-
+ Minimal installation with limited packages. New
+ packages are easily installed using Advanced Package
+ Tool (APT), the main command-line package manager for
+ Debian.
+ icon: debian_logo.png
+ eol: false
+ eol_date: '2024-03-12 00:00:00'
+ eol_warning: false
+ deploy_type: 1
+ vnc: false
+ type: linux
+ - id: 44
+ name: Debian
+ version: 12 (Bookworm)
+ variant: null
+ arch: 2
+ description: >-
+ Minimal installation with limited packages. New
+ packages are easily installed using Advanced Package
+ Tool (APT), the main command-line package manager for
+ Debian.
+ icon: debian_logo.png
+ eol: false
+ eol_date: '2024-04-02 00:00:00'
+ eol_warning: false
+ deploy_type: 1
+ vnc: false
+ type: linux
+ - id: 46
+ name: Debian
+ version: 12 (Bookworm)
+ variant: Minimal
+ arch: 1
+ description: >-
+ Minimal installation with limited packages. New
+ packages are easily installed using Advanced Package
+ Tool (APT), the main command-line package manager for
+ Debian.
+ icon: debian_logo.png
+ eol: false
+ eol_date: '2024-04-23 00:00:00'
+ eol_warning: false
+ deploy_type: 1
+ vnc: false
+ type: linux
+ - id: 56
+ name: Debian
+ version: 12 (Bookworm)
+ variant: Test
+ arch: 1
+ description: >-
+ Minimal installation with limited packages. New
+ packages are easily installed using Advanced Package
+ Tool (APT), the main command-line package manager for
+ Debian.
+ icon: debian_logo.png
+ eol: false
+ eol_date: '2024-04-23 00:00:00'
+ eol_warning: false
+ deploy_type: 1
+ vnc: true
+ type: linux
+ id: 1
+ - name: CentOS
+ description: >-
+ The CentOS Linux distribution is a stable, predictable,
+ manageable and reproducible platform derived from the
+ sources of Red Hat Enterprise Linux (RHEL).
+ icon: centos_logo.png
+ templates:
+ - id: 1
+ name: CentOS
+ version: '7'
+ variant: Minimal
+ arch: 1
+ description: >-
+ Minimal installation with limited packages. New
+ packages are easily installed using Yum, the main
+ command-line package manager for CentOS.
+ icon: centos_logo.png
+ eol: false
+ eol_date: '2024-03-12 00:00:00'
+ eol_warning: false
+ deploy_type: 1
+ vnc: false
+ type: linux
+ - id: 2
+ name: CentOS Stream
+ version: '9'
+ variant: Minimal
+ arch: 1
+ description: >-
+ Base installation with limited packages. New packages
+ are easily installed using DNF (yum), the main
+ command-line package manager for CentOS.
+ icon: centos_logo.png
+ eol: false
+ eol_date: '2024-03-12 00:00:00'
+ eol_warning: false
+ deploy_type: 1
+ vnc: false
+ type: linux
+ id: 2
+ - name: Rocky Linux
+ description: >-
+ Rocky Linux is a community enterprise operating system
+ designed to be 100% bug-for-bug compatible with America's
+ top enterprise Linux distribution now that its downstream
+ partner has shifted direction. It is under intensive
+ development by the community. Rocky Linux is led by
+ Gregory Kurtzer, founder of the CentOS project.
+ icon: rocky_linux_logo.png
+ templates:
+ - id: 7
+ name: Rocky Linux
+ version: '8'
+ variant: Minimal
+ arch: 1
+ description: >-
+ Minimal installation with limited packages. New
+ packages are easily installed using DNF (yum), the
+ main command-line package manager for Rocky Linux.
+ icon: rocky_linux_logo.png
+ eol: false
+ eol_date: '2024-03-12 00:00:00'
+ eol_warning: false
+ deploy_type: 1
+ vnc: false
+ type: linux
+ - id: 13
+ name: Rocky Linux
+ version: '9'
+ variant: Minimal
+ arch: 1
+ description: >-
+ Minimal installation with limited packages. New
+ packages are easily installed using DNF (yum), the
+ main command-line package manager for Rocky Linux.
+ icon: rocky_linux_logo.png
+ eol: false
+ eol_date: '2024-03-12 00:00:00'
+ eol_warning: false
+ deploy_type: 1
+ vnc: false
+ type: linux
+ - id: 40
+ name: Rocky Linux
+ version: '9'
+ variant: ''
+ arch: 2
+ description: >-
+ Minimal installation with limited packages. New
+ packages are easily installed using DNF (yum), the
+ main command-line package manager for Rocky Linux.
+ icon: rocky_linux_logo.png
+ eol: false
+ eol_date: '2024-03-28 00:00:00'
+ eol_warning: false
+ deploy_type: 1
+ vnc: false
+ type: linux
+ id: 3
+ - name: AlmaLinux
+ description: >-
+ AlmaLinux OS is an open-source, community-driven project
+ that intends provide and alternative to the CentOS Stable
+ release. AlmaLinux is an OS that is 1:1 binary compatible
+ with RHEL® 8 and a global collaborative of the developer
+ community, industry, academia and research which build
+ upon this technology to empower humanity.
+ icon: almalinux_logo.png
+ templates:
+ - id: 6
+ name: AlmaLinux
+ version: '8'
+ variant: Minimal
+ arch: 1
+ description: >-
+ Minimal installation with limited packages. New
+ packages are easily installed using DNF (yum), the
+ main command-line package manager for AlmaLinux.
+ icon: almalinux_logo.png
+ eol: false
+ eol_date: '2024-03-12 00:00:00'
+ eol_warning: false
+ deploy_type: 1
+ vnc: false
+ type: linux
+ - id: 12
+ name: ARM -> AlmaLinux
+ version: '9'
+ variant: Latest
+ arch: 1
+ description: >-
+ Latest version with base packages. New packages are
+ easily installed using DNF (yum), the main
+ command-line package manager for AlmaLinux.
+ icon: almalinux_logo.png
+ eol: false
+ eol_date: '2024-03-12 00:00:00'
+ eol_warning: false
+ deploy_type: 1
+ vnc: false
+ type: linux
+ - id: 39
+ name: AlmaLinux
+ version: '9'
+ variant: null
+ arch: 2
+ description: >-
+ Minimal installation with limited packages. New
+ packages are easily installed using DNF (yum), the
+ main command-line package manager for AlmaLinux.
+ icon: almalinux_logo.png
+ eol: false
+ eol_date: '2024-03-28 00:00:00'
+ eol_warning: false
+ deploy_type: 1
+ vnc: false
+ type: linux
+ id: 4
+ - name: Ubuntu
+ description: >-
+ The most popular server Linux in the cloud and data
+ centre, you can rely on Ubuntu Server and its five years
+ of guaranteed free upgrades.
+ icon: ubuntu_logo.png
+ templates:
+ - id: 3
+ name: Ubuntu Server
+ version: 20.04 LTS (Focal Fossa)
+ variant: Minimal
+ arch: 1
+ description: >-
+ Minimal installation with limited packages. New
+ packages are easily installed using Advanced Package
+ Tool (APT), the main command-line package manager for
+ Ubuntu.
+ icon: ubuntu_logo.png
+ eol: false
+ eol_date: '2024-03-12 00:00:00'
+ eol_warning: false
+ deploy_type: 1
+ vnc: false
+ type: linux
+ - id: 4
+ name: Ubuntu Server
+ version: 18.04 LTS (Bionic Beaver)
+ variant: Minimal
+ arch: 1
+ description: >-
+ Minimal installation with limited packages. New
+ packages are easily installed using Advanced Package
+ Tool (APT), the main command-line package manager for
+ Ubuntu.
+ icon: ubuntu_logo.png
+ eol: false
+ eol_date: '2024-03-12 00:00:00'
+ eol_warning: false
+ deploy_type: 1
+ vnc: false
+ type: linux
+ - id: 9
+ name: Ubuntu Server
+ version: 22.04 LTS (Jammy Jellyfish)
+ variant: Minimal
+ arch: 1
+ description: >-
+ Minimal installation with limited packages. New
+ packages are easily installed using Advanced Package
+ Tool (APT), the main command-line package manager for
+ Ubuntu.
+ icon: ubuntu_logo.png
+ eol: false
+ eol_date: '2024-03-12 00:00:00'
+ eol_warning: false
+ deploy_type: 1
+ vnc: false
+ type: linux
+ - id: 41
+ name: Ubuntu
+ version: 22.04 LTS (Jammy Jellyfish)
+ variant: ''
+ arch: 2
+ description: >-
+ Minimal installation with limited packages. New
+ packages are easily installed using Advanced Package
+ Tool (APT), the main command-line package manager for
+ Ubuntu.
+ icon: ubuntu_logo.png
+ eol: false
+ eol_date: '2024-03-28 00:00:00'
+ eol_warning: false
+ deploy_type: 1
+ vnc: false
+ type: linux
+ - id: 49
+ name: Ubuntu Server
+ version: 24.04 LTS (Noble Numbat)
+ variant: Minimal
+ arch: 1
+ description: >-
+ Minimal installation with limited packages. New
+ packages are easily installed using Advanced Package
+ Tool (APT), the main command-line package manager for
+ Ubuntu.
+ icon: ubuntu_logo.png
+ eol: false
+ eol_date: '2024-04-25 00:00:00'
+ eol_warning: false
+ deploy_type: 1
+ vnc: false
+ type: linux
+ - id: 51
+ name: Ubuntu
+ version: 24.04 LTS (Noble Numbat)
+ variant: null
+ arch: 2
+ description: >-
+ Minimal installation with limited packages. New
+ packages are easily installed using Advanced Package
+ Tool (APT), the main command-line package manager for
+ Ubuntu.
+ icon: ubuntu_logo.png
+ eol: false
+ eol_date: '2024-05-02 00:00:00'
+ eol_warning: false
+ deploy_type: 1
+ vnc: false
+ type: linux
+ id: 5
+ - name: Fedora
+ description: >-
+ Fedora Server is a powerful, flexible operating system
+ that includes the best and latest datacenter technologies.
+ It puts you in control of all your infrastructure and
+ services.
+ icon: fedora_logo.png
+ templates:
+ - id: 11
+ name: Fedora
+ version: '37'
+ variant: Minimal
+ arch: 1
+ description: >-
+ Minimal installation with limited packages. New
+ packages are easily installed using DNF (yum), the
+ main command-line package manager for Fedora.
+ icon: fedora_logo.png
+ eol: false
+ eol_date: '2024-03-12 00:00:00'
+ eol_warning: false
+ deploy_type: 1
+ vnc: false
+ type: linux
+ - id: 14
+ name: Fedora
+ version: '38'
+ variant: Minimal
+ arch: 1
+ description: >-
+ Minimal installation with limited packages. New
+ packages are easily installed using DNF (yum), the
+ main command-line package manager for Fedora.
+ icon: fedora_logo.png
+ eol: false
+ eol_date: '2024-03-12 00:00:00'
+ eol_warning: false
+ deploy_type: 1
+ vnc: false
+ type: linux
+ - id: 15
+ name: Fedora
+ version: '39'
+ variant: Minimal
+ arch: 1
+ description: >-
+ Minimal installation with limited packages. New
+ packages are easily installed using DNF (yum), the
+ main command-line package manager for Fedora.
+ icon: fedora_logo.png
+ eol: false
+ eol_date: '2024-03-12 00:00:00'
+ eol_warning: false
+ deploy_type: 1
+ vnc: false
+ type: linux
+ - id: 59
+ name: Fedora
+ version: '41'
+ variant: Minimal
+ arch: 1
+ description: >-
+ Minimal installation with limited packages. New
+ packages are easily installed using DNF (yum), the
+ main command-line package manager for Fedora.
+ icon: fedora_logo.png
+ eol: false
+ eol_date: '2024-12-18 00:00:00'
+ eol_warning: false
+ deploy_type: 1
+ vnc: false
+ type: linux
+ id: 6
+ - name: FreeBSD
+ description: >-
+ FreeBSD is an operating system used to power modern
+ servers, desktops, and embedded platforms. A large
+ community has continually developed it for more than
+ thirty years. Its advanced networking, security, and
+ storage features have made FreeBSD the platform of choice
+ for many of the busiest web sites and most pervasive
+ embedded networking and storage devices.
+ icon: freebsd_logo.png
+ templates:
+ - id: 52
+ name: FreeBSD
+ version: '13.3'
+ variant: Minimal
+ arch: 1
+ description: Minimal installation with limited packages.
+ icon: freebsd_logo.png
+ eol: false
+ eol_date: '2024-05-15 00:00:00'
+ eol_warning: false
+ deploy_type: 1
+ vnc: false
+ type: unix
+ - id: 53
+ name: FreeBSD
+ version: '14.0'
+ variant: Minimal
+ arch: 1
+ description: Minimal installation with limited packages.
+ icon: freebsd_logo.png
+ eol: false
+ eol_date: '2024-05-15 00:00:00'
+ eol_warning: false
+ deploy_type: 1
+ vnc: false
+ type: unix
+ - id: 55
+ name: FreeBSD
+ version: '14.2'
+ variant: Minimal
+ arch: 1
+ description: Minimal installation with limited packages.
+ icon: freebsd_logo.png
+ eol: false
+ eol_date: '2024-10-20 00:00:00'
+ eol_warning: false
+ deploy_type: 1
+ vnc: true
+ type: unix
+ - id: 58
+ name: FreeBSD
+ version: '13.2'
+ variant: Minimal
+ arch: 1
+ description: Minimal installation with limited packages.
+ icon: freebsd_logo.png
+ eol: false
+ eol_date: '2024-12-10 00:00:00'
+ eol_warning: false
+ deploy_type: 1
+ vnc: false
+ type: unix
+ id: 7
+ - name: Other
+ description: ''
+ icon: linux_logo.png
+ templates:
+ - id: 5
+ name: openSUSE
+ version: Leap 15
+ variant: Minimal
+ arch: 1
+ description: >-
+ openSUSE is a project that serves to promote the use
+ of free and open-source software.
Minimal
+ installation with limited packages. New packages are
+ easily installed using Zypper, the main command-line
+ package manager for openSUSE.
+ icon: opensuse_logo.png
+ eol: false
+ eol_date: '2024-03-12 00:00:00'
+ eol_warning: false
+ deploy_type: 1
+ vnc: false
+ type: linux
+ - id: 36
+ name: openSUSE
+ version: Leap 15.6
+ variant: ''
+ arch: 2
+ description: >-
+ openSUSE is a project that serves to promote the use
+ of free and open-source software.
Minimal
+ installation with limited packages. New packages are
+ easily installed using Zypper, the main command-line
+ package manager for openSUSE.
+ icon: opensuse_logo.png
+ eol: false
+ eol_date: '2024-03-14 00:00:00'
+ eol_warning: false
+ deploy_type: 1
+ vnc: false
+ type: linux
+ id: 0
+ headers: {}
+ '401':
+ $ref: '#/components/responses/401'
+ description: ''
+ security:
+ - bearer: []
+ /packages:
+ get:
+ summary: Retrieve packages
+ deprecated: false
+ description: ''
+ tags:
+ - Packages
+ parameters: []
+ responses:
+ '200':
+ description: ''
+ content:
+ application/json:
+ schema:
+ type: object
+ properties: {}
+ example:
+ data:
+ - id: 1
+ name: Test
+ description: null
+ enabled: true
+ memory: 1024
+ primaryStorage: 10
+ traffic: 200
+ cpuCores: 1
+ primaryNetworkSpeedIn: 0
+ primaryNetworkSpeedOut: 0
+ primaryDiskType: inherit
+ backupPlanId: 0
+ primaryStorageReadBytesSec: null
+ primaryStorageWriteBytesSec: null
+ primaryStorageReadIopsSec: null
+ primaryStorageWriteIopsSec: null
+ primaryStorageProfile: 1
+ primaryNetworkProfile: 0
+ created: '2024-03-12T22:41:31.000000Z'
+ - id: 2
+ name: Test Only
+ description: null
+ enabled: true
+ memory: 1024
+ primaryStorage: 10
+ traffic: 200
+ cpuCores: 1
+ primaryNetworkSpeedIn: 0
+ primaryNetworkSpeedOut: 0
+ primaryDiskType: inherit
+ backupPlanId: 0
+ primaryStorageReadBytesSec: null
+ primaryStorageWriteBytesSec: null
+ primaryStorageReadIopsSec: null
+ primaryStorageWriteIopsSec: null
+ primaryStorageProfile: 0
+ primaryNetworkProfile: 0
+ created: '2024-06-28T12:36:16.000000Z'
+ - id: 3
+ name: BASIC
+ description: null
+ enabled: true
+ memory: 1024
+ primaryStorage: 10
+ traffic: 20000
+ cpuCores: 2
+ primaryNetworkSpeedIn: 0
+ primaryNetworkSpeedOut: 0
+ primaryDiskType: inherit
+ backupPlanId: 0
+ primaryStorageReadBytesSec: null
+ primaryStorageWriteBytesSec: null
+ primaryStorageReadIopsSec: null
+ primaryStorageWriteIopsSec: null
+ primaryStorageProfile: 1
+ primaryNetworkProfile: 0
+ created: '2024-10-12T15:54:54.000000Z'
+ headers: {}
+ '401':
+ $ref: '#/components/responses/401'
+ description: ''
+ security:
+ - bearer: []
+ /packages/{packageId}:
+ get:
+ summary: Retrieve a packge
+ deprecated: false
+ description: ''
+ tags:
+ - Packages
+ parameters:
+ - name: packageId
+ in: path
+ description: A valid package ID as shown in VirtFusion.
+ required: true
+ example: 1
+ schema:
+ type: integer
+ responses:
+ '200':
+ description: ''
+ content:
+ application/json:
+ schema:
+ type: object
+ properties: {}
+ example:
+ data:
+ id: 1
+ name: Test
+ description: null
+ enabled: true
+ memory: 1024
+ primaryStorage: 10
+ traffic: 200
+ cpuCores: 1
+ primaryNetworkSpeedIn: 0
+ primaryNetworkSpeedOut: 0
+ primaryDiskType: inherit
+ backupPlanId: 0
+ primaryStorageReadBytesSec: null
+ primaryStorageWriteBytesSec: null
+ primaryStorageReadIopsSec: null
+ primaryStorageWriteIopsSec: null
+ primaryStorageProfile: 1
+ primaryNetworkProfile: 0
+ created: '2024-03-12T22:41:31.000000Z'
+ headers: {}
+ '401':
+ $ref: '#/components/responses/401'
+ description: ''
+ security:
+ - bearer: []
+ /queue/{queueId}:
+ get:
+ summary: Retrieve a queue item
+ deprecated: false
+ description: ''
+ tags:
+ - Queue & Tasks
+ parameters:
+ - name: queueId
+ in: path
+ description: A valid queue ID as shown in VirtFusion.
+ required: true
+ example: 158
+ schema:
+ type: integer
+ responses:
+ '200':
+ description: ''
+ content:
+ application/json:
+ schema:
+ type: object
+ properties: {}
+ example:
+ data:
+ id: 158
+ jobId: '852'
+ job: App\Jobs\Server\KVM\Build
+ hypervisorId: 6
+ serverId: 69
+ action: build_server
+ queue: default
+ started: '2025-01-15T15:00:26+00:00'
+ updated: '2025-01-15T15:00:49+00:00'
+ finished: '2025-01-15T15:00:49+00:00'
+ failed: false
+ progress: 100
+ errors:
+ exception:
+ stringable: false
+ errors: []
+ type: null
+ trace: null
+ message: null
+ primaryActions:
+ - type: server.get.status
+ dataType: object
+ data:
+ success: true
+ version: '{{VERSION}}'
+ setOpts:
+ failOnVersionCheck: true
+ failOnDisasterRecovery: true
+ createDirStructure: true
+ writeXMLConfiguration: false
+ failOnCustomXML: false
+ failOnPriorityXML: false
+ failOnElevateXML: true
+ actions:
+ createDirStructure:
+ requested: true
+ output: server directory structure set. No action required
+ msg: null
+ success: true
+ statusTree:
+ disasterRecoveryActive: false
+ customXML: false
+ priorityXML: false
+ elevateXML: false
+ created: '2025-01-15T15:00:26+00:00'
+ updated: '2025-01-15T15:00:26+00:00'
+ - type: server.config.dhcp
+ dataType: object
+ data:
+ system:
+ success: true
+ commandline: []
+ data: []
+ created: '2025-01-15T15:00:26+00:00'
+ updated: '2025-01-15T15:00:26+00:00'
+ - type: server.os.template.exists
+ dataType: object
+ data:
+ success: false
+ remote:
+ info:
+ url: >-
+ https://cloud-images.ubuntu.com/noble/current/noble-server-cloudimg-amd64.img
+ content_type: application/octet-stream
+ http_code: 200
+ header_size: 255
+ request_size: 176
+ filetime: -1
+ ssl_verify_result: 20
+ redirect_count: 0
+ total_time: 0.070349
+ namelookup_time: 0.016706
+ connect_time: 0.03088
+ pretransfer_time: 0.054076
+ size_upload: 0
+ size_download: 0
+ speed_download: 0
+ speed_upload: 0
+ download_content_length: 609856512
+ upload_content_length: 0
+ starttransfer_time: 0.070305
+ redirect_time: 0
+ redirect_url: ''
+ primary_ip: 185.125.190.37
+ certinfo: []
+ primary_port: 443
+ local_ip: 192.168.4.2
+ local_port: 34728
+ http_version: 2
+ protocol: 2
+ ssl_verifyresult: 0
+ scheme: HTTPS
+ appconnect_time_us: 54015
+ connect_time_us: 30880
+ namelookup_time_us: 16706
+ pretransfer_time_us: 54076
+ redirect_time_us: 0
+ starttransfer_time_us: 70305
+ total_time_us: 70349
+ effective_method: HEAD
+ exitCode: 0
+ error: null
+ completed: true
+ created: '2025-01-15T15:00:27+00:00'
+ updated: '2025-01-15T15:00:27+00:00'
+ - type: server.os.template.download
+ dataType: object
+ data:
+ system:
+ success: true
+ errors: []
+ commandline: []
+ data:
+ - sourceUrl: >-
+ https://cloud-images.ubuntu.com/noble/current/noble-server-cloudimg-amd64.img
+ sourceDecompress: ''
+ destinationPath: >-
+ /home/vf-data/os/template/ubuntu-noble-server-cloudimg-amd64-2024-04-25.qcow2
+ exitCode: null
+ pid: null
+ finished: false
+ error: null
+ errorOutput: null
+ success: 0
+ updated: 1736953227
+ created: '2025-01-15T15:00:27+00:00'
+ updated: '2025-01-15T15:00:27+00:00'
+ - type: server.os.template.download.check
+ dataType: object
+ data:
+ success: true
+ filesize: 609856512
+ remote: |-
+ {
+ "sourceUrl": "https:\/\/cloud-images.ubuntu.com\/noble\/current\/noble-server-cloudimg-amd64.img",
+ "sourceDecompress": "",
+ "destinationPath": "\/home\/vf-data\/os\/template\/ubuntu-noble-server-cloudimg-amd64-2024-04-25.qcow2",
+ "exitCode": 0,
+ "pid": 387093,
+ "finished": true,
+ "error": null,
+ "errorOutput": null,
+ "success": true,
+ "updated": 1736953235,
+ "decompressOutput": null
+ }
+ created: '2025-01-15T15:00:35+00:00'
+ updated: '2025-01-15T15:00:35+00:00'
+ - type: server.create.ci
+ dataType: object
+ data:
+ network:
+ version: 2
+ ethernets:
+ ens3:
+ match:
+ macaddress: 00:e7:fb:01:87:14
+ addresses:
+ - 192.168.4.32/23
+ - 192.168.4.35/23
+ gateway4: 192.168.4.1
+ nameservers:
+ addresses:
+ - 8.8.8.8
+ - 8.8.4.4
+ routes:
+ - to: 192.168.4.1
+ via: 0.0.0.0
+ scope: link
+ ens4:
+ match:
+ macaddress: 00:f0:4a:c6:3f:08
+ addresses:
+ - 192.168.4.33/23
+ - 192.168.4.34/23
+ gateway4: 192.168.4.1
+ nameservers:
+ addresses:
+ - 8.8.8.8
+ - 8.8.4.4
+ user:
+ timezone: Europe/London
+ ssh_pwauth: false
+ users:
+ - name: root
+ ssh-authorized-keys:
+ - >-
+ ssh-rsa
+ AAAAB3NzaC1yc2EAAAADAQABAAACAQC+JdL4fWELBWGAknSu0PwVpDDOlORxy9z7eVnZphZXBzYLMnux+ZogVLns6+O6NDE8JmWvP9RIg3SIga7RDOkW9UCdLzRu0jF2ALL7CK1huo1Ih0PDM9ZbFDy2Fd7a4DTvUX6923fQyW0PWRtyL11R4c9NUqzejKp5kW8vHfPQjzwb1hGIKvkSYkI0Auq4JJhlvjjnoK7Z8t5mpDrVfNTrVqevPgsW5Xwnq8R+02XywrY+Q/wnpxDs3Wjb2aA61A0x5J0xcZQpTQHoJNj77J3VmPI7Ry7Q8hPbTSLGZbN+gODr0lOaL5TdbvM3bnus5JvoqgRoszzPcTiNMZAe3v9UM8hiXise54b8rsc2M9MQ4olPu7TrROZbcw+9q4m6cV+dfVU/NRFkf27YRa4oZNKehHsMiupDyoISgSl4qSB8YXAWsX03oC/gzpB2YJIqEL1Y/SmKYEhgr0cplkvGZy6C/Q9cJHyHlMPtEBPexgcjXC9QrVK4n2cmde3TuSRMctawcat7Nuq08C8fGHaGHr8iAeage3o/ODVOt0rhBu69PknzQeVBdlwK3+p1dH6PnMzNNBhWyNZT/NqB2eS6K8lYpOQ47byXPwYsRLvStUjpZRdikOT7D31T5g8FwOThQ+6WX+xfMD7CSLsSKCn/FhlinbVbG2IhCLH3B30Akw5bUw==
+ hashed_passwd: ''
+ lock_passwd: true
+ runcmd:
+ - >-
+ DEBIAN_FRONTEND=noninteractive /usr/bin/apt-get
+ --option=Dpkg::Options::=--force-confold
+ --option=Dpkg::options::=--force-unsafe-io
+ --assume-yes --quiet update
+ - >-
+ DEBIAN_FRONTEND=noninteractive /usr/bin/apt-get
+ --option=Dpkg::Options::=--force-confold
+ --option=Dpkg::options::=--force-unsafe-io
+ --assume-yes --quiet install qemu-guest-agent
+ - /usr/bin/systemctl enable qemu-guest-agent
+ - /usr/bin/systemctl start qemu-guest-agent
+ - >-
+ DEBIAN_FRONTEND=noninteractive /usr/bin/apt-get
+ --option=Dpkg::Options::=--force-confold
+ --option=Dpkg::options::=--force-unsafe-io
+ --assume-yes --quiet dist-upgrade
+ meta:
+ instance-id: b9fd9092-7200-4a24-96d4-76aedd664274
+ local-hostname: elliptical-way
+ created: '2025-01-15T15:00:35+00:00'
+ updated: '2025-01-15T15:00:35+00:00'
+ - type: server.disk.create.os
+ dataType: object
+ data:
+ system:
+ success: true
+ commandline:
+ - result:
+ success: true
+ exitOnZero: false
+ command: >-
+ 'virsh' 'destroy'
+ 'b9fd9092-7200-4a24-96d4-76aedd664274'
+ exit_code: 1
+ pid: 387131
+ started: 1736953235.403454
+ env:
+ PATH: >-
+ /usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin
+ timeout: 180
+ output: ''
+ error: >-
+ error: failed to get domain
+ 'b9fd9092-7200-4a24-96d4-76aedd664274'
+ - command: >-
+ qemu-img info
+ '/home/vf-data/os/template/ubuntu-noble-server-cloudimg-amd64-2024-04-25.qcow2'
+ | grep -v grep | grep -w "file format:" | awk '{
+ print $3 }'
+ exit_code: 0
+ output: qcow2
+ error: ''
+ - command: >-
+ 'cloud-localds'
+ '/home/vf-data/server/b9fd9092-7200-4a24-96d4-76aedd664274/cloud-drive.img'
+ '--network-config=/home/vf-data/server/b9fd9092-7200-4a24-96d4-76aedd664274/network-config-v2.yaml'
+ '/home/vf-data/server/b9fd9092-7200-4a24-96d4-76aedd664274/user-data.yaml'
+ '/home/vf-data/server/b9fd9092-7200-4a24-96d4-76aedd664274/meta-data.yaml'
+ exit_code: 0
+ output: ''
+ error: ''
+ data:
+ success: true
+ forkData:
+ status: true
+ errors: []
+ commandline: []
+ output:
+ - sourcePath: >-
+ /home/vf-data/os/template/ubuntu-noble-server-cloudimg-amd64-2024-04-25.qcow2
+ destinationPath: >-
+ /home/vf-data/disk/b9fd9092-7200-4a24-96d4-76aedd664274_1.img
+ convertProcess:
+ - qemu-img
+ - convert
+ - '-f'
+ - qcow2
+ - '-O'
+ - qcow2
+ - >-
+ /home/vf-data/os/template/ubuntu-noble-server-cloudimg-amd64-2024-04-25.qcow2
+ - >-
+ /home/vf-data/disk/b9fd9092-7200-4a24-96d4-76aedd664274_1.img
+ resizeProcess:
+ - qemu-img
+ - resize
+ - '-f'
+ - qcow2
+ - >-
+ /home/vf-data/disk/b9fd9092-7200-4a24-96d4-76aedd664274_1.img
+ - 11G
+ resizeProcessPid: null
+ convertProcessPid: null
+ finished: false
+ convertProcessOutput: null
+ resizeProcessOutput: null
+ convertProcessExitCode: null
+ resizeProcessExitCode: null
+ convertProcessError: null
+ resizeProcessError: null
+ error: null
+ success: false
+ updated: 1736953238
+ abort: false
+ error: null
+ errorException: null
+ created: '2025-01-15T15:00:38+00:00'
+ updated: '2025-01-15T15:00:38+00:00'
+ - type: server.os.install.check
+ dataType: object
+ data:
+ success: true
+ sourceFilesize: 609856512
+ destinationFilesize: 1832517808
+ remote:
+ sourcePath: >-
+ /home/vf-data/os/template/ubuntu-noble-server-cloudimg-amd64-2024-04-25.qcow2
+ destinationPath: >-
+ /home/vf-data/disk/b9fd9092-7200-4a24-96d4-76aedd664274_1.img
+ convertProcess:
+ - qemu-img
+ - convert
+ - '-f'
+ - qcow2
+ - '-O'
+ - qcow2
+ - >-
+ /home/vf-data/os/template/ubuntu-noble-server-cloudimg-amd64-2024-04-25.qcow2
+ - >-
+ /home/vf-data/disk/b9fd9092-7200-4a24-96d4-76aedd664274_1.img
+ resizeProcess:
+ - qemu-img
+ - resize
+ - '-f'
+ - qcow2
+ - >-
+ /home/vf-data/disk/b9fd9092-7200-4a24-96d4-76aedd664274_1.img
+ - 11G
+ resizeProcessPid: 387270
+ convertProcessPid: 387168
+ finished: true
+ convertProcessOutput: ''
+ resizeProcessOutput: |
+ Image resized.
+ convertProcessExitCode: 0
+ resizeProcessExitCode: 0
+ convertProcessError: null
+ resizeProcessError: null
+ error: null
+ success: true
+ updated: 1736953244
+ created: '2025-01-15T15:00:44+00:00'
+ updated: '2025-01-15T15:00:44+00:00'
+ - type: server.vnc.disable
+ dataType: object
+ data:
+ system:
+ success: true
+ commandline:
+ - command: '''/usr/sbin/ufw'' ''deny'' ''5903'''
+ exit_code: 0
+ output: |-
+ Skipping adding existing rule
+ Skipping adding existing rule (v6)
+ error: ''
+ data: []
+ created: '2025-01-15T15:00:45+00:00'
+ updated: '2025-01-15T15:00:45+00:00'
+ - type: server.boot
+ dataType: object
+ data:
+ system:
+ success: true
+ errors: []
+ commandline:
+ - success: true
+ exitOnZero: false
+ command: >-
+ 'virsh' '-q' 'domstate'
+ 'b9fd9092-7200-4a24-96d4-76aedd664274'
+ exit_code: 1
+ pid: 387301
+ started: 1736953245.165795
+ env:
+ PATH: >-
+ /usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin
+ timeout: 60
+ output: ''
+ error: >-
+ error: failed to get domain
+ 'b9fd9092-7200-4a24-96d4-76aedd664274'
+ - success: true
+ exitOnZero: true
+ command: >-
+ 'virsh' 'create'
+ '/home/vf-data/server/b9fd9092-7200-4a24-96d4-76aedd664274/server.xml'
+ exit_code: 0
+ pid: 387303
+ started: 1736953245.190122
+ env:
+ PATH: >-
+ /usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin
+ timeout: 360
+ output: >-
+ Domain 'b9fd9092-7200-4a24-96d4-76aedd664274'
+ created from
+ /home/vf-data/server/b9fd9092-7200-4a24-96d4-76aedd664274/server.xml
+ error: ''
+ - result:
+ success: true
+ exitOnZero: true
+ command: >-
+ 'virsh' 'attach-disk'
+ 'b9fd9092-7200-4a24-96d4-76aedd664274'
+ '/home/vf-data/server/b9fd9092-7200-4a24-96d4-76aedd664274/cloud-drive.img'
+ 'sdx' '--mode' 'readonly'
+ exit_code: 0
+ pid: 387457
+ started: 1736953246.67152
+ env:
+ PATH: >-
+ /usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin
+ timeout: 60
+ output: Disk attached successfully
+ error: ''
+ data:
+ - - filter_list: null
+ filter_apply: null
+ filter_apply_success: true
+ filter_apply_error: false
+ filter_apply_error_trace: false
+ filter_apply_cli: null
+ filter_apply_code: null
+ tmp_filter_1: >-
+ /home/vf-data/server/b9fd9092-7200-4a24-96d4-76aedd664274/networkFilter-3933491695.xml
+ tmp_filter_2: >-
+ /home/vf-data/server/b9fd9092-7200-4a24-96d4-76aedd664274/networkFilter-3933491695.xml-tmp
+ sha1: bd9ce80d8372e025e5de8757ec63c042986a48fa
+ sha1_last: bd9ce80d8372e025e5de8757ec63c042986a48fa
+ native:
+ primary: []
+ secondary:
+ sha1: 801b6632cbb50f2c8c6dd15037ba9c9d4e03cf50
+ sha1_last: 801b6632cbb50f2c8c6dd15037ba9c9d4e03cf50
+ created: '2025-01-15T15:00:46+00:00'
+ updated: '2025-01-15T15:00:46+00:00'
+ - type: server.config.statistics
+ dataType: object
+ data: []
+ created: '2025-01-15T15:00:48+00:00'
+ updated: '2025-01-15T15:00:48+00:00'
+ subActions: []
+ headers: {}
+ '401':
+ $ref: '#/components/responses/401'
+ description: ''
+ security:
+ - bearer: []
+ /ssh_keys:
+ post:
+ summary: Add an SSH key to a user account
+ deprecated: false
+ description: ''
+ tags:
+ - SSH Keys
+ parameters: []
+ requestBody:
+ content:
+ application/json:
+ schema:
+ type: object
+ properties:
+ userId:
+ type: integer
+ name:
+ type: string
+ publicKey:
+ type: string
+ required:
+ - userId
+ - name
+ - publicKey
+ example:
+ userId: 1
+ name: Key 1
+ publicKey: >-
+ ssh-rsa
+ AAAAB3NzaC1yc2EAAAADAQABAAABAQDF6O4Evybdywpi6PImTE5aJ75+5OpJKyd2QR2LSl0bVxhZjQOqN/4msCp/UjUpFDSeC1SQXeKQb4o7OZ7bUC8k2JbNxnArsYSGi/XhqczKOX/uYOMA/V8gb1e+uishQSzjYrneC0PufFYwNGStjYf0QXCsgQcYLsHbjV2g9j0FhVYxj5endy7Z1K1RMP7IzF5lh3KgtbqKhdJ8XK1fqXCcPHxEuAzjq7G2W+I9xOs8GqftxYGS4XAiOe7YLKfWM00dUdYMJ81R8lZFj5UzP0MOT9qxPNBNiB0MEQX8hc0+2nQdaQYkg8mbCJQxhT9Cr0rXyYdbaNnYWIJql3SVgigJ
+ responses:
+ '201':
+ description: ''
+ content:
+ application/json:
+ schema:
+ type: object
+ properties: {}
+ example:
+ data:
+ id: 2
+ name: Key 1
+ type: OpenSSH
+ createdAt: '2025-01-20T12:16:23.000000Z'
+ headers: {}
+ '401':
+ $ref: '#/components/responses/401'
+ description: ''
+ security:
+ - bearer: []
+ /ssh_keys/{keyId}:
+ delete:
+ summary: Delete an SSH key from a user
+ deprecated: false
+ description: ''
+ tags:
+ - SSH Keys
+ parameters:
+ - name: keyId
+ in: path
+ description: A valid SSH key ID as shown in VirtFusion.
+ required: true
+ example: 2
+ schema:
+ type: integer
+ responses:
+ '204':
+ description: ''
+ content:
+ application/octet-stream:
+ schema:
+ type: object
+ properties: {}
+ headers: {}
+ '401':
+ $ref: '#/components/responses/401'
+ description: ''
+ security:
+ - bearer: []
+ get:
+ summary: Retrieve an SSH key
+ deprecated: false
+ description: ''
+ tags:
+ - SSH Keys
+ parameters:
+ - name: keyId
+ in: path
+ description: A valid SSH key ID as shown in VirtFusion.
+ required: true
+ example: 1
+ schema:
+ type: integer
+ responses:
+ '200':
+ description: ''
+ content:
+ application/json:
+ schema:
+ type: object
+ properties: {}
+ example:
+ data:
+ id: 1
+ name: MY SSH Key
+ publicKey: >-
+ ssh-rsa
+ AAAAB3NzaC1yc2EAAAADAQABAAACAQC+JdL4fWELBWGAknSu0PwVpDDOlORxy9z7eVnZphZXBzYLMnux+ZogVLns6+O6NDE8JmWvP9RIg3SIga7RDOkW9UCdLzRu0jF2ALL7CK1huo1Ih0PDM9ZbFDy2Fd7a4DTvUX6923fQyW0PWRtyL11R4c9NUqzejKp5kW8vHfPQjzwb1hGIKvkSYkI0Auq4JJhlvjjnoK7Z8t5mpDrVfNTrVqevPgsW5Xwnq8R+02XywrY+Q/wnpxDs4Ujb2aA61A0x5J0xcZQpTQHoJNj77J3VmPI7Ry7Q8hPbTSLGZbN+gODr0lOaL5TdbvM3bnus5JvoqgRoszzPcTiAQZAe3v9UM8hiXise54b8rsc2M9MQ4olPu7TrROZbcw+9q4m6cV+dfVU/NRFkf27YRa4oZNKehHsMiupDyoISgSl4qSB8YXAWsX03oC/gzpB2YJIqEL1Y/SmKYEhgr0cplkvGZy6C/Q9cJHyHlMPtEBPexgcjXC9QrVK4n2cmde3TuSRMctawcat7Nuq08C8fGHaGHr8iAeage3o/ODVOt0rhBu69PknzQeVBdlwK3+p1dH6PnMzNNBhWyNZT/NqB2eS6K8lYpOQ47byXPwYsRLvStUjpZRdikOT7D31T5g8FwOThQ+6WX+xfMD7CSLsSKCn/FhlinbVbG2IhCLH3B30Akw5bUw==
+ type: OpenSSH
+ enabled: true
+ created: '2024-03-13T20:28:32+00:00'
+ updated: '2024-03-13T20:28:32+00:00'
+ headers: {}
+ '401':
+ $ref: '#/components/responses/401'
+ description: ''
+ security:
+ - bearer: []
+ /ssh_keys/user/{userId}:
+ get:
+ summary: Retrieve a users SSH keys
+ deprecated: false
+ description: ''
+ tags:
+ - SSH Keys
+ parameters:
+ - name: userId
+ in: path
+ description: A valid user ID as shown in VirtFusion.
+ required: true
+ example: 1
+ schema:
+ type: integer
+ responses:
+ '200':
+ description: ''
+ content:
+ application/json:
+ schema:
+ type: object
+ properties: {}
+ example:
+ data:
+ - id: 1
+ name: My SSH Key
+ publicKey: >-
+ ssh-rsa
+ AAAAB3NzaC1yc2EAAAADAQABAAACAQC+JdL4fWELBWGAknSu0PwVpDDOlORxy9z7eVnZphZXBzYLMnux+ZogVLns6+O6NDE8JmWvP9RIg3SIga7RDOkW9UCdLzRu0jF2ALL7CK1huo1Ih0PDM9ZbFDy2Fd7a4DTvUX6923fQyW0PWRtyL11R4c9NUqzejKp5kW8vHfPQjzwb1hGIKvkSYkI0Auq4JJhlvjjnoK7Z8t5mpDrVfNTrVqevPgsW5Xwnq8R+02XywrY+Q/wnpxDs4Ujb2aA61A0x5J0xcRTpTQHoJNj77J3VmPI7Ry7Q8hPbTSLGZbN+gODr0lOaL5TdbvM3bnus5JvoqgRoszzPcTiNMZAe3v9UM8hiXise54b8rsc2M9MQ4olPu7TrROZbcw+9q4m6cV+dfVU/NRFkf27YRa4oZNKehHsMiupDyoISgSl4qSB8YXAWsX03oC/gzpB2YJIqEL1Y/SmKYEhgr0cplkvGZy6C/Q9cJHyHlMPtEBPexgcjXC9QrVK4n2cmde3TuSRMctawcat7Nuq08C8fGHaGHr8iAeage3o/ODVOt0rhBu69PknzQeVBdlwK3+p1dH6PnMzNNBhWyNZT/NqB2eS6K8lYpOQ47byXPwYsRLvStUjpZRdikOT7D31T5g8FwOThQ+6WX+xfMD7CSLsSKCn/FhlinbVbG2IhCLH3B30Akw5bUw==
+ type: OpenSSH
+ enabled: true
+ created: '2024-03-13T20:28:32+00:00'
+ updated: '2024-03-13T20:28:32+00:00'
+ headers: {}
+ '401':
+ $ref: '#/components/responses/401'
+ description: ''
+ security:
+ - bearer: []
+ /users/{extRelationId}/byExtRelation:
+ delete:
+ summary: Delete a user
+ deprecated: false
+ description: ''
+ tags:
+ - Users/External Rel ID & Rel Str
+ parameters:
+ - name: extRelationId
+ in: path
+ description: A valid external relational ID as shown in VirtFusion.
+ required: true
+ example: '1'
+ schema:
+ type: string
+ - name: relStr
+ in: query
+ description: ''
+ required: false
+ example: 'true'
+ schema:
+ type: boolean
+ default: false
+ responses:
+ '204':
+ description: ''
+ content:
+ application/octet-stream:
+ schema:
+ type: object
+ properties: {}
+ headers: {}
+ '401':
+ $ref: '#/components/responses/401'
+ description: ''
+ security:
+ - bearer: []
+ put:
+ summary: Modify a user
+ deprecated: false
+ description: ''
+ tags:
+ - Users/External Rel ID & Rel Str
+ parameters:
+ - name: extRelationId
+ in: path
+ description: A valid external relational ID as shown in VirtFusion.
+ required: true
+ example: '1'
+ schema:
+ type: string
+ - name: relStr
+ in: query
+ description: ''
+ required: false
+ schema:
+ type: boolean
+ default: false
+ requestBody:
+ content:
+ application/json:
+ schema:
+ type: object
+ properties:
+ name:
+ type: string
+ description: Full name of the user.
+ email:
+ type: string
+ description: Email address of the user.
+ selfService:
+ type: integer
+ description: >-
+ default disabled) 0 = disabled, 1 = hourly, 2 = resource
+ packs, 3 = hourly & resource packs.
+ selfServiceHourlyCredit:
+ type: boolean
+ description: >-
+ Enable/disable credit balance billing for hourly self
+ service. (true|false).
+ selfServiceHourlyGroupProfiles:
+ type: array
+ items:
+ type: integer
+ description: >-
+ (default none) array of self service hourly group profile
+ ids.
+ selfServiceResourceGroupProfiles:
+ type: array
+ items:
+ type: integer
+ description: >-
+ (default none) array of self service resource group profile
+ ids.
+ selfServiceHourlyResourcePack:
+ type: integer
+ description: (default none) ID of an hourly self service resource pack.
+ enabled:
+ type: boolean
+ description: >-
+ (default false) Email the access credentials to the user.
+ (true|false).
+ example:
+ name: jon Doe
+ email: jon@doe.com
+ selfService: 3
+ selfServiceHourlyCredit: true
+ selfServiceHourlyGroupProfiles:
+ - 1
+ - 2
+ - 3
+ selfServiceResourceGroupProfiles:
+ - 4
+ - 5
+ - 6
+ selfServiceHourlyResourcePack: 1
+ enabled: true
+ responses:
+ '201':
+ description: ''
+ content:
+ application/json:
+ schema:
+ type: object
+ properties: {}
+ example:
+ data:
+ name: jon Doe
+ email: jon@doe.com
+ selfService: 3
+ enabled: true
+ headers: {}
+ '401':
+ $ref: '#/components/responses/401'
+ description: ''
+ security:
+ - bearer: []
+ get:
+ summary: Retrieve a user
+ deprecated: false
+ description: ''
+ tags:
+ - Users/External Rel ID & Rel Str
+ parameters:
+ - name: extRelationId
+ in: path
+ description: A valid external relational ID as shown in VirtFusion.
+ required: true
+ example: '1'
+ schema:
+ type: string
+ - name: relStr
+ in: query
+ description: ''
+ required: false
+ schema:
+ type: boolean
+ default: false
+ responses:
+ '200':
+ description: ''
+ content:
+ application/json:
+ schema:
+ type: object
+ properties: {}
+ example:
+ data:
+ id: 3
+ admin: false
+ extRelationId: 1
+ selfService: 3
+ selfServiceHourlyGroupProfiles: []
+ selfServiceResourceGroupProfiles: []
+ selfServiceHourlyResourcePack: null
+ name: jon Doe
+ email: jon@doe.com
+ timezone: Europe/London
+ suspended: false
+ twoFactorAuth: false
+ created: '2025-01-20T12:48:20.000000Z'
+ updated: '2025-01-20T13:00:38.000000Z'
+ headers: {}
+ '401':
+ $ref: '#/components/responses/401'
+ description: ''
+ security:
+ - bearer: []
+ /users/{extRelationId}/authenticationTokens:
+ post:
+ summary: Generate a set of login tokens
+ deprecated: false
+ description: ''
+ tags:
+ - Users/External Rel ID & Rel Str
+ parameters:
+ - name: extRelationId
+ in: path
+ description: A valid external relational ID as shown in VirtFusion.
+ required: true
+ example: '1'
+ schema:
+ type: string
+ - name: relStr
+ in: query
+ description: ''
+ required: false
+ schema:
+ type: boolean
+ default: false
+ responses:
+ '200':
+ description: ''
+ content:
+ application/json:
+ schema:
+ type: object
+ properties: {}
+ example:
+ data:
+ authentication:
+ tokens:
+ '1': >-
+ zYpEXpWEeXR4LfogW3xIomIJS5YW8woOjo18h9st6Sh23ReeTEeQNI1RSQWXYv1AImtQzFm0CLrn6Ve8VtIP3MfDnoRWHxQ334UU
+ '2': >-
+ RGzuQDFt0KsWgPozaTZDpuXy3aSsbj6VHWbz4JrhGoj0ZOvaGHUcXM6WGeGuNgfTUPLcy0SYMNJWmI1idC8uR88ZSs00XRnEtbG9
+ endpoint: /token_authenticate
+ endpoint_complete: >-
+ /token_authenticate/?1=zYpEXpWEeXR4LfogW3xIomIJS5YW8woOjo18h9st6Sh23ReeTEeQNI1RSQWXYv1AImtQzFm0CLrn6Ve8VtIP3MfDnoRWHxQ334UU&2=RGzuQDFt0KsWgPozaTZDpuXy3aSsbj6VHWbz4JrhGoj0ZOvaGHUcXM6WGeGuNgfTUPLcy0SYMNJWmI1idC8uR88ZSs00XRnEtbG9
+ expiry:
+ ttl: 60
+ expires: '2025-01-20T12:49:52.170943Z'
+ headers: {}
+ '401':
+ $ref: '#/components/responses/401'
+ description: ''
+ security:
+ - bearer: []
+ /users/{extRelationId}/serverAuthenticationTokens/{serverId}:
+ post:
+ summary: Generate a set of loging tokens using a server ID
+ deprecated: false
+ description: ''
+ tags:
+ - Users/External Rel ID & Rel Str
+ parameters:
+ - name: extRelationId
+ in: path
+ description: A valid external relational ID as shown in VirtFusion.
+ required: true
+ example: '1'
+ schema:
+ type: string
+ - name: serverId
+ in: path
+ description: A valid server ID as shown in VirtFusion.
+ required: true
+ example: 9
+ schema:
+ type: integer
+ - name: relStr
+ in: query
+ description: ''
+ required: false
+ schema:
+ type: boolean
+ default: false
+ responses:
+ '200':
+ description: ''
+ content:
+ application/json:
+ schema:
+ type: object
+ properties: {}
+ example:
+ data:
+ authentication:
+ tokens:
+ '1': >-
+ oIGBk2qEYTXKMGbaDVbpRFqwQC57Rzl5zWKhwQkgDbRBeXSTH865Bvv0Fm8oY6b0xYpH22xbLAKarOAy28PnToxRu5InfmkIHmo0
+ '2': >-
+ WwiZ9XwqKM5jNGgCsCsUD4B6DDxAKeolJu3dBN7lsK1uGDVvElvfH77sDyukRIzTbbEI6fggKBXuSYRaYc5FqMab4L6PB0QcOxr9
+ endpoint: /token_authenticate
+ endpoint_complete: >-
+ /token_authenticate/?1=oIGBk2qEYTXKMGbaDVbpRFqwQC57Rzl5zWKhwQkgDbRBeXSTH865Bvv0Fm8oY6b0xYpH22xbLAKarOAy28PnToxRu5InfmkIHmo0&2=WwiZ9XwqKM5jNGgCsCsUD4B6DDxAKeolJu3dBN7lsK1uGDVvElvfH77sDyukRIzTbbEI6fggKBXuSYRaYc5FqMab4L6PB0QcOxr9
+ expiry:
+ ttl: 60
+ expires: '2025-01-20T12:52:59.761522Z'
+ headers: {}
+ '401':
+ $ref: '#/components/responses/401'
+ description: ''
+ security:
+ - bearer: []
+ /users/{extRelationId}/byExtRelation/resetPassword:
+ post:
+ summary: Change a user passowrd
+ deprecated: false
+ description: ''
+ tags:
+ - Users/External Rel ID & Rel Str
+ parameters:
+ - name: extRelationId
+ in: path
+ description: A valid external relational ID as shown in VirtFusion.
+ required: true
+ example: '1'
+ schema:
+ type: string
+ - name: relStr
+ in: query
+ description: ''
+ required: false
+ schema:
+ type: boolean
+ default: false
+ responses:
+ '201':
+ description: ''
+ content:
+ application/json:
+ schema:
+ type: object
+ properties: {}
+ example:
+ data:
+ email: jon@doe.com
+ password: zD2VqFKO554tdfWKOmGhw
+ headers: {}
+ '401':
+ $ref: '#/components/responses/401'
+ description: ''
+ security:
+ - bearer: []
+ /users:
+ post:
+ summary: Create a user
+ deprecated: false
+ description: ''
+ tags:
+ - Users
+ parameters: []
+ requestBody:
+ content:
+ application/json:
+ schema:
+ type: object
+ properties:
+ name:
+ type: string
+ description: Full name of the user.
+ email:
+ type: string
+ description: Email address of the user.
+ extRelationId:
+ type: integer
+ description: Relation ID.
+ relStr:
+ type: string
+ description: Relational string.
+ selfService:
+ type: integer
+ description: >-
+ (default disabled) 0 = disabled, 1 = hourly, 2 = resource
+ packs, 3 = hourly & resource packs.
+ selfServiceHourlyCredit:
+ type: boolean
+ description: ' Enable/disable credit balance billing for hourly self service. (true|false).'
+ selfServiceHourlyGroupProfiles:
+ type: array
+ items:
+ type: integer
+ description: >-
+ (default none) array of self service hourly group profile
+ ids.
+ selfServiceResourceGroupProfiles:
+ type: array
+ items:
+ type: integer
+ description: ' (default none) array of self service resource group profile ids.'
+ selfServiceHourlyResourcePack:
+ type: integer
+ description: ' (default none) ID of an hourly self service resource pack.'
+ sendMail:
+ type: boolean
+ description: >-
+ (default false) Email the access credentials to the user.
+ (true|false).
+ required:
+ - name
+ - email
+ example:
+ name: Jon Doe
+ email: jon@doe.com
+ extRelationId: 1
+ selfService: 3
+ selfServiceHourlyCredit: true
+ selfServiceHourlyGroupProfiles:
+ - 1
+ - 2
+ - 3
+ selfServiceResourceGroupProfiles:
+ - 4
+ - 5
+ - 6
+ selfServiceHourlyResourcePack: 1
+ sendMail: false
+ responses:
+ '201':
+ description: ''
+ content:
+ application/json:
+ schema:
+ type: object
+ properties: {}
+ example:
+ data:
+ id: 2
+ admin: false
+ extRelationId: 1
+ selfService: 3
+ selfServiceHourlyGroupProfiles: []
+ selfServiceResourceGroupProfiles: []
+ selfServiceHourlyResourcePack: null
+ name: Jon Doe
+ email: jon@doe.com
+ timezone: Europe/London
+ suspended: false
+ twoFactorAuth: false
+ created: '2025-01-20T12:41:28.000000Z'
+ updated: '2025-01-20T12:41:28.000000Z'
+ password: 0hPZSAmj8Tgq1noGoenxpxlC9xf1tc
+ headers: {}
+ '401':
+ $ref: '#/components/responses/401'
+ description: ''
+ security:
+ - bearer: []
+ /selfService/credit/byUserExtRelationId/{extRelationId}:
+ post:
+ summary: Add credit to user
+ deprecated: false
+ description: ''
+ tags:
+ - Self Service/External Relational ID
+ parameters:
+ - name: extRelationId
+ in: path
+ description: A valid external relational ID as shown in VirtFusion.
+ required: true
+ schema:
+ type: string
+ - name: relStr
+ in: query
+ description: ''
+ required: false
+ example: 'true'
+ schema:
+ type: boolean
+ requestBody:
+ content:
+ application/json:
+ schema:
+ type: object
+ properties:
+ tokens:
+ type: number
+ description: A numeric token value.
+ reference_1:
+ type: integer
+ description: ' An optional reference number. Max 64-bit integer.'
+ reference_2:
+ type: string
+ description: An optional reference in string format. Max 1000 character.
+ required:
+ - tokens
+ example:
+ tokens: 100
+ reference_1: 400
+ reference_2: This is a string reference with a 1000 character limit.
+ responses:
+ '201':
+ description: ''
+ content:
+ application/json:
+ schema:
+ type: object
+ properties: {}
+ example:
+ data:
+ id: 2
+ headers: {}
+ '401':
+ $ref: '#/components/responses/401'
+ description: ''
+ security:
+ - bearer: []
+ /selfService/hourlyGroupProfile/byUserExtRelationId/{extRelationId}:
+ post:
+ summary: Add an hourly group profile to a user
+ deprecated: false
+ description: ''
+ tags:
+ - Self Service/External Relational ID
+ parameters:
+ - name: extRelationId
+ in: path
+ description: A valid external relational ID as shown in VirtFusion.
+ required: true
+ example: '1'
+ schema:
+ type: string
+ - name: relStr
+ in: query
+ description: ''
+ required: false
+ example: 'true'
+ schema:
+ type: boolean
+ requestBody:
+ content:
+ application/json:
+ schema:
+ type: object
+ properties:
+ profileId:
+ type: integer
+ description: ID of an hourly group profile.
+ required:
+ - profileId
+ example:
+ profileId: 1
+ responses:
+ '204':
+ description: ''
+ content:
+ application/octet-stream:
+ schema:
+ type: object
+ properties: {}
+ headers: {}
+ '401':
+ $ref: '#/components/responses/401'
+ description: ''
+ security:
+ - bearer: []
+ /selfService/resourceGroupProfile/byUserExtRelationId/{extRelationId}:
+ post:
+ summary: Add a resource group profile to a user
+ deprecated: false
+ description: ''
+ tags:
+ - Self Service/External Relational ID
+ parameters:
+ - name: extRelationId
+ in: path
+ description: A valid external relational ID as shown in VirtFusion.
+ required: true
+ example: '1'
+ schema:
+ type: string
+ - name: relStr
+ in: query
+ description: ''
+ required: false
+ example: 'true'
+ schema:
+ type: boolean
+ requestBody:
+ content:
+ application/json:
+ schema:
+ type: object
+ properties:
+ profileId:
+ type: integer
+ description: ID a resource group profile.
+ required:
+ - profileId
+ example:
+ profileId: 1
+ responses:
+ '204':
+ description: ''
+ content:
+ application/octet-stream:
+ schema:
+ type: object
+ properties: {}
+ headers: {}
+ '401':
+ $ref: '#/components/responses/401'
+ description: ''
+ security:
+ - bearer: []
+ /selfService/resourcePack/byUserExtRelationId/{extRelationId}:
+ post:
+ summary: Add a resource pack to a user
+ deprecated: false
+ description: ''
+ tags:
+ - Self Service/External Relational ID
+ parameters:
+ - name: extRelationId
+ in: path
+ description: A valid external relational ID as shown in VirtFusion.
+ required: true
+ example: '1'
+ schema:
+ type: string
+ - name: relStr
+ in: query
+ description: ''
+ required: false
+ example: 'true'
+ schema:
+ type: boolean
+ requestBody:
+ content:
+ application/json:
+ schema:
+ type: object
+ properties:
+ packId:
+ type: integer
+ description: ID of a resource pack.
+ enabled:
+ type: boolean
+ description: Enable the pack. true|false defaults too true.
+ required:
+ - packId
+ - enabled
+ example:
+ packId: 1
+ enabled: true
+ responses:
+ '201':
+ description: ''
+ content:
+ application/json:
+ schema:
+ type: object
+ properties: {}
+ example:
+ data:
+ id: 17
+ headers: {}
+ '401':
+ $ref: '#/components/responses/401'
+ description: ''
+ security:
+ - bearer: []
+ /selfService/hourlyStats/byUserExtRelationId/{extRelationId}:
+ get:
+ summary: Retrieve hourly statistics
+ deprecated: false
+ description: ''
+ tags:
+ - Self Service/External Relational ID
+ parameters:
+ - name: extRelationId
+ in: path
+ description: A valid external relational ID as shown in VirtFusion.
+ required: true
+ example: '1'
+ schema:
+ type: string
+ - name: period[]
+ in: query
+ description: 'Example: period[]=YYYY-MM-DD&period[]=YYYY-MM-D'
+ required: false
+ example: YYYY-MM-DD
+ schema:
+ type: string
+ - name: range
+ in: query
+ description: range=m
+ required: false
+ example: m
+ schema:
+ type: string
+ - name: relStr
+ in: query
+ description: ''
+ required: false
+ example: 'true'
+ schema:
+ type: boolean
+ responses:
+ '200':
+ description: ''
+ content:
+ application/json:
+ schema:
+ type: object
+ properties: {}
+ example:
+ data:
+ periodId: 0
+ period: January 2025
+ previousPeriod: December 2024
+ nextPeriod: February 2025
+ monthlyTotal:
+ hours: 0
+ value: '0.00'
+ tokens: false
+ servers: 0
+ credit:
+ value: 0
+ currency:
+ code: ''
+ prefix: ''
+ suffix: ''
+ value: 0
+ currentValue: 0
+ headers: {}
+ '401':
+ $ref: '#/components/responses/401'
+ description: ''
+ security:
+ - bearer: []
+ /selfService/access/byUserExtRelationId/{extRelationId}:
+ put:
+ summary: Modify user access
+ deprecated: false
+ description: ''
+ tags:
+ - Self Service/External Relational ID
+ parameters:
+ - name: extRelationId
+ in: path
+ description: A valid external relational ID as shown in VirtFusion.
+ required: true
+ example: '1'
+ schema:
+ type: string
+ - name: relStr
+ in: query
+ description: ''
+ required: false
+ example: 'true'
+ schema:
+ type: boolean
+ requestBody:
+ content:
+ application/json:
+ schema:
+ type: object
+ properties:
+ syncToProfiles:
+ type: boolean
+ description: >-
+ true|false Default false. If true, the self service access
+ level will be set based on profiles.
+ required:
+ - syncToProfiles
+ example:
+ syncToProfiles: true
+ responses:
+ '204':
+ description: ''
+ content:
+ application/octet-stream:
+ schema:
+ type: object
+ properties: {}
+ headers: {}
+ '401':
+ $ref: '#/components/responses/401'
+ description: ''
+ security:
+ - bearer: []
+ /selfService/hourlyGroupProfile/{profileId}/byUserExtRelationId/{extRelationId}:
+ delete:
+ summary: Remove hourly group profile from a user
+ deprecated: false
+ description: ''
+ tags:
+ - Self Service/External Relational ID
+ parameters:
+ - name: profileId
+ in: path
+ description: ID of a hourly group profile.
+ required: true
+ example: 1
+ schema:
+ type: integer
+ - name: extRelationId
+ in: path
+ description: A valid external relational ID as shown in VirtFusion.
+ required: true
+ example: '1'
+ schema:
+ type: string
+ - name: relStr
+ in: query
+ description: ''
+ required: false
+ example: 'true'
+ schema:
+ type: boolean
+ responses:
+ '204':
+ description: ''
+ content:
+ application/octet-stream:
+ schema:
+ type: object
+ properties: {}
+ headers: {}
+ '401':
+ $ref: '#/components/responses/401'
+ description: ''
+ security:
+ - bearer: []
+ /selfService/resourceGroupProfile/{profileId}/byUserExtRelationId/{extRelationId}:
+ delete:
+ summary: Remove resource group from a user
+ deprecated: false
+ description: ''
+ tags:
+ - Self Service/External Relational ID
+ parameters:
+ - name: profileId
+ in: path
+ description: ID of a hourly group profile.
+ required: true
+ example: 1
+ schema:
+ type: integer
+ - name: extRelationId
+ in: path
+ description: A valid external relational ID as shown in VirtFusion.
+ required: true
+ example: '1'
+ schema:
+ type: string
+ - name: relStr
+ in: query
+ description: ''
+ required: false
+ example: 'true'
+ schema:
+ type: boolean
+ responses:
+ '204':
+ description: ''
+ content:
+ application/octet-stream:
+ schema:
+ type: object
+ properties: {}
+ headers: {}
+ '401':
+ $ref: '#/components/responses/401'
+ description: ''
+ security:
+ - bearer: []
+ /selfService/report/byUserExtRelationId/{extRelationId}:
+ get:
+ summary: Generate a report
+ deprecated: false
+ description: ''
+ tags:
+ - Self Service/External Relational ID
+ parameters:
+ - name: extRelationId
+ in: path
+ description: A valid external relational ID as shown in VirtFusion.
+ required: true
+ example: '1'
+ schema:
+ type: string
+ - name: period
+ in: query
+ description: >-
+ A single period in the range of 0-24 (0 being the currently defined
+ month in the self service settings | optional and will default to
+ the current month if not defined).
+ required: false
+ example: '0'
+ schema:
+ type: string
+ - name: currency
+ in: query
+ description: >-
+ A three letter currency code that is defined in the self service
+ settings. (optional and will default to the user defined currency if
+ not defined).
+ required: false
+ example: USD
+ schema:
+ type: string
+ - name: relStr
+ in: query
+ description: ''
+ required: false
+ example: 'true'
+ schema:
+ type: boolean
+ responses:
+ '200':
+ description: ''
+ content:
+ application/json:
+ schema:
+ type: object
+ properties: {}
+ example:
+ data:
+ usage:
+ servers: []
+ serversTotal:
+ hours: false
+ value: false
+ tokens: false
+ hourConversionRate: false
+ monthlyTotal:
+ hours: false
+ value: false
+ tokens: false
+ addonsTotal:
+ hours: 0
+ value: 0
+ tokens: false
+ taxStatus: 3
+ success: false
+ history: '0'
+ breakdown: true
+ term: January 2025
+ previousTerm: December 2024
+ nextTerm: February 2025
+ period:
+ ymd: '2025-01-01'
+ start: '2025-01-01T00:00:00+00:00'
+ end: '2025-01-31T00:00:00+00:00'
+ showHourlyRate: false
+ showMonthlyRate: false
+ currency:
+ prefix: ''
+ suffix: ''
+ code: ''
+ currentValue: 0
+ value: 0
+ default:
+ prefix: ''
+ suffix: ''
+ code: ''
+ limits:
+ success: true
+ packs: []
+ headers: {}
+ '401':
+ $ref: '#/components/responses/401'
+ description: ''
+ security:
+ - bearer: []
+ /selfService/hourlyResourcePack/byUserExtRelationId/{extRelationId}:
+ put:
+ summary: Set an hourly resource pack
+ deprecated: false
+ description: ''
+ tags:
+ - Self Service/External Relational ID
+ parameters:
+ - name: extRelationId
+ in: path
+ description: A valid external relational ID as shown in VirtFusion.
+ required: true
+ example: '1'
+ schema:
+ type: string
+ - name: relStr
+ in: query
+ description: ''
+ required: false
+ example: 'true'
+ schema:
+ type: boolean
+ requestBody:
+ content:
+ application/json:
+ schema:
+ type: object
+ properties:
+ packId:
+ type: integer
+ description: ID of an hourly resource pack.
+ required:
+ - packId
+ example:
+ packId: 1
+ responses:
+ '204':
+ description: ''
+ content:
+ application/octet-stream:
+ schema:
+ type: object
+ properties: {}
+ headers: {}
+ '401':
+ $ref: '#/components/responses/401'
+ description: ''
+ security:
+ - bearer: []
+ /selfService/usage/byUserExtRelationId/{extRelationId}:
+ get:
+ summary: Retrieve a users usage
+ deprecated: false
+ description: ''
+ tags:
+ - Self Service/External Relational ID
+ parameters:
+ - name: extRelationId
+ in: path
+ description: A valid external relational ID as shown in VirtFusion.
+ required: true
+ example: '1'
+ schema:
+ type: string
+ - name: period[]
+ in: query
+ description: Array of periods or a single period. (YYYY-MM-DD).
+ required: false
+ example: '2025-01-01'
+ schema:
+ type: string
+ - name: range
+ in: query
+ description: >-
+ Length of period. Defaults to 1 month. Possible values d = day, w =
+ week, 2w = 2 weeks, 3w = 3 weeks, m = month.
+ required: false
+ example: m
+ schema:
+ type: string
+ - name: relStr
+ in: query
+ description: ''
+ required: false
+ example: 'true'
+ schema:
+ type: boolean
+ responses:
+ '200':
+ description: ''
+ content:
+ application/json:
+ schema:
+ type: object
+ properties: {}
+ example:
+ data:
+ user:
+ id: 3
+ relationalId: 1
+ currency: null
+ timezone: Europe/London
+ name: jon Doe
+ email: jon@doe.com
+ usageServers:
+ hours: 0
+ token: 0
+ tokenReal: 0
+ usageServersBillable:
+ hours: 0
+ token: 0
+ tokenReal: 0
+ usageAddons:
+ hours: 0
+ token: 0
+ tokenReal: 0
+ usageAddonsBillable:
+ hours: 0
+ token: 0
+ tokenReal: 0
+ periods:
+ - period: '2025-01-01'
+ range: month
+ start: '2025-01-01T00:00:00+00:00'
+ end: '2025-01-31T23:59:59+00:00'
+ timezone: UTC
+ currentPeriod: true
+ hoursInMonthPeriod: 744
+ monthToHourRate: 730
+ monthToHourRateType: 1
+ days: 31
+ hours: 744
+ minutes: 44640
+ seconds: 2678400
+ usageServers:
+ hours: 0
+ token: 0
+ tokenReal: 0
+ usageServersBillable:
+ hours: 0
+ token: 0
+ tokenReal: 0
+ usageAddons:
+ hours: 0
+ token: 0
+ tokenReal: 0
+ usageAddonsBillable:
+ hours: 0
+ token: 0
+ tokenReal: 0
+ servers: []
+ headers: {}
+ '401':
+ $ref: '#/components/responses/401'
+ description: ''
+ security:
+ - bearer: []
+ /selfService/credit/{creditId}:
+ delete:
+ summary: Cancel credit that was applied to a user
+ deprecated: false
+ description: ''
+ tags:
+ - Self Service
+ parameters:
+ - name: creditId
+ in: path
+ description: A valid credit ID.
+ required: true
+ example: 1
+ schema:
+ type: integer
+ responses:
+ '204':
+ description: ''
+ content:
+ application/octet-stream:
+ schema:
+ type: object
+ properties: {}
+ headers: {}
+ '401':
+ $ref: '#/components/responses/401'
+ description: ''
+ security:
+ - bearer: []
+ /selfService/resourcePackServers/{packId}:
+ delete:
+ summary: Delete all servers attached to a pack ID
+ deprecated: false
+ description: ''
+ tags:
+ - Self Service
+ parameters:
+ - name: packId
+ in: path
+ description: ID of a resource pack.
+ required: true
+ example: 1
+ schema:
+ type: integer
+ - name: delay
+ in: query
+ description: The delay in minutes. Defaults to 30 (0 - 43800).
+ required: false
+ example: 30
+ schema:
+ type: integer
+ responses:
+ '204':
+ description: ''
+ content:
+ application/octet-stream:
+ schema:
+ type: object
+ properties: {}
+ headers: {}
+ '401':
+ $ref: '#/components/responses/401'
+ description: ''
+ security:
+ - bearer: []
+ /selfService/resourcePack/{packId}:
+ delete:
+ summary: Delete a user resource pack
+ deprecated: false
+ description: ''
+ tags:
+ - Self Service
+ parameters:
+ - name: packId
+ in: path
+ description: ID of a resource pack.
+ required: true
+ example: 1
+ schema:
+ type: integer
+ - name: disable
+ in: query
+ description: >-
+ Disable the pack if it can't be deleted. true|false Defaults to
+ false.
+ required: false
+ example: 'true'
+ schema:
+ type: boolean
+ responses:
+ '204':
+ description: ''
+ content:
+ application/octet-stream:
+ schema:
+ type: object
+ properties: {}
+ headers: {}
+ '401':
+ $ref: '#/components/responses/401'
+ description: ''
+ security:
+ - bearer: []
+ get:
+ summary: Retrieve a user resource pack
+ deprecated: false
+ description: ''
+ tags:
+ - Self Service
+ parameters:
+ - name: packId
+ in: path
+ description: ID of a resource pack.
+ required: true
+ example: 1
+ schema:
+ type: integer
+ - name: withServers
+ in: query
+ description: include a list of assigned servers. true|false Defaults to false.
+ required: false
+ example: 'true'
+ schema:
+ type: boolean
+ responses:
+ '200':
+ description: ''
+ content:
+ application/json:
+ schema:
+ type: object
+ properties: {}
+ example:
+ data:
+ type: pack
+ id: 18
+ pid: 9
+ label: null
+ name: Pack 2 · 2 / 4096 / 250
+ limits:
+ total_servers: 2
+ total_memory: 4096
+ total_storage: 200
+ total_cpu: 24
+ total_traffic: 1000000
+ max_memory: 4096
+ max_storage: 10
+ max_cpu: 8
+ max_traffic: 500000
+ used:
+ servers: 0
+ memory: 0
+ storage: 0
+ cpu: 0
+ traffic: 0
+ usage:
+ servers:
+ t: 2
+ u: 0
+ f: 2
+ p: 0
+ l: true
+ memory:
+ t: 4096
+ u: 0
+ f: 4096
+ p: 0
+ l: true
+ storage:
+ t: 200
+ u: 0
+ f: 200
+ p: 0
+ l: true
+ cpu:
+ t: 24
+ u: 0
+ p: 0
+ f: 24
+ l: true
+ traffic:
+ t: 1000000
+ u: 0
+ f: 1000000
+ p: 0
+ l: true
+ headers: {}
+ '401':
+ $ref: '#/components/responses/401'
+ description: ''
+ security:
+ - bearer: []
+ put:
+ summary: Modify user resource pack
+ deprecated: false
+ description: ''
+ tags:
+ - Self Service
+ parameters:
+ - name: packId
+ in: path
+ description: ID of a resource pack.
+ required: true
+ example: 1
+ schema:
+ type: integer
+ requestBody:
+ content:
+ application/json:
+ schema:
+ type: object
+ properties:
+ enabled:
+ type: boolean
+ required:
+ - enabled
+ example:
+ enabled: true
+ responses:
+ '204':
+ description: ''
+ content:
+ application/octet-stream:
+ schema:
+ type: object
+ properties: {}
+ headers: {}
+ '401':
+ $ref: '#/components/responses/401'
+ description: ''
+ security:
+ - bearer: []
+ /selfService/currencies:
+ get:
+ summary: Retrieve currencies
+ deprecated: false
+ description: ''
+ tags:
+ - Self Service
+ parameters: []
+ responses:
+ '200':
+ description: ''
+ content:
+ application/json:
+ schema:
+ type: object
+ properties: {}
+ example:
+ data:
+ - id: 11
+ code: USD
+ value: '0.0100000000'
+ prefix: $
+ suffix: null
+ default: true
+ enabled: true
+ - id: 12
+ code: GBP
+ value: '0.0200000000'
+ prefix: £
+ suffix: null
+ default: false
+ enabled: true
+ headers: {}
+ '401':
+ $ref: '#/components/responses/401'
+ description: ''
+ security:
+ - bearer: []
+ /selfService/resourcePackServers/{packId}/suspend:
+ post:
+ summary: Suspend all servers assigned to a reosurce pack
+ deprecated: false
+ description: ''
+ tags:
+ - Self Service
+ parameters:
+ - name: packId
+ in: path
+ description: ID of a resource pack.
+ required: true
+ example: 1
+ schema:
+ type: integer
+ responses:
+ '204':
+ description: ''
+ content:
+ application/octet-stream:
+ schema:
+ type: object
+ properties: {}
+ headers: {}
+ '401':
+ $ref: '#/components/responses/401'
+ description: ''
+ security:
+ - bearer: []
+ /selfService/resourcePackServers/{packId}/unsuspend:
+ post:
+ summary: Unsuspend all servers assigned to a reosurce pack
+ deprecated: false
+ description: ''
+ tags:
+ - Self Service
+ parameters:
+ - name: packId
+ in: path
+ description: ID of a resource pack.
+ required: true
+ example: 1
+ schema:
+ type: integer
+ responses:
+ '204':
+ description: ''
+ content:
+ application/octet-stream:
+ schema:
+ type: object
+ properties: {}
+ headers: {}
+ '401':
+ $ref: '#/components/responses/401'
+ description: ''
+ security:
+ - bearer: []
+components:
+ schemas: {}
+ responses:
+ '401':
+ description: ''
+ content:
+ application/octet-stream:
+ schema:
+ type: object
+ properties: {}
+ examples:
+ '401':
+ summary: '401'
+ value: 401 Unauthorized
+ securitySchemes:
+ bearer:
+ type: http
+ scheme: bearer
+servers:
+ - url: https://cp.domain.com/api/v1
+ description: Example URL
+security:
+ - bearer: []
diff --git a/scripts/check-endpoint-drift.go b/scripts/check-endpoint-drift.go
new file mode 100644
index 0000000..828a3da
--- /dev/null
+++ b/scripts/check-endpoint-drift.go
@@ -0,0 +1,154 @@
+//go:build ignore
+
+// check-endpoint-drift compares the current OpenAPI spec against the endpoint manifest
+// and reports any added or removed endpoints. Exit 0 if no drift, exit 1 if drift detected.
+package main
+
+import (
+ "encoding/json"
+ "fmt"
+ "os"
+ "path/filepath"
+ "runtime"
+ "sort"
+ "strings"
+
+ "gopkg.in/yaml.v3"
+)
+
+type Endpoint struct {
+ Method string `json:"method" yaml:"-"`
+ Path string `json:"path" yaml:"-"`
+ Summary string `json:"summary" yaml:"-"`
+ Tag string `json:"tag" yaml:"-"`
+}
+
+func endpointKey(e Endpoint) string {
+ return e.Method + " " + e.Path
+}
+
+type OpenAPISpec struct {
+ Paths map[string]map[string]struct {
+ Summary string `yaml:"summary"`
+ Tags []string `yaml:"tags"`
+ } `yaml:"paths"`
+}
+
+func extractEndpoints(specPath string) ([]Endpoint, error) {
+ data, err := os.ReadFile(specPath)
+ if err != nil {
+ return nil, fmt.Errorf("reading spec: %w", err)
+ }
+
+ var spec OpenAPISpec
+ if err := yaml.Unmarshal(data, &spec); err != nil {
+ return nil, fmt.Errorf("parsing spec: %w", err)
+ }
+
+ validMethods := map[string]bool{
+ "get": true, "post": true, "put": true, "delete": true, "patch": true,
+ }
+
+ var endpoints []Endpoint
+ for path, methods := range spec.Paths {
+ for method, details := range methods {
+ if !validMethods[strings.ToLower(method)] {
+ continue
+ }
+ tag := "Untagged"
+ if len(details.Tags) > 0 {
+ tag = details.Tags[0]
+ }
+ endpoints = append(endpoints, Endpoint{
+ Method: strings.ToUpper(method),
+ Path: path,
+ Summary: details.Summary,
+ Tag: tag,
+ })
+ }
+ }
+
+ sort.Slice(endpoints, func(i, j int) bool {
+ if endpoints[i].Path != endpoints[j].Path {
+ return endpoints[i].Path < endpoints[j].Path
+ }
+ return endpoints[i].Method < endpoints[j].Method
+ })
+
+ return endpoints, nil
+}
+
+func main() {
+ // Resolve paths relative to this script's location
+ _, filename, _, _ := runtime.Caller(0)
+ rootDir := filepath.Dir(filepath.Dir(filename))
+
+ specPath := filepath.Join(rootDir, "openapi.yaml")
+ manifestPath := filepath.Join(rootDir, "endpoint-manifest.json")
+
+ current, err := extractEndpoints(specPath)
+ if err != nil {
+ fmt.Fprintf(os.Stderr, "Error extracting endpoints: %v\n", err)
+ os.Exit(1)
+ }
+
+ manifestData, err := os.ReadFile(manifestPath)
+ if err != nil {
+ fmt.Fprintf(os.Stderr, "Error: endpoint-manifest.json not found. Copy it from the MCP repo first.\n")
+ os.Exit(1)
+ }
+
+ var manifest []Endpoint
+ if err := json.Unmarshal(manifestData, &manifest); err != nil {
+ fmt.Fprintf(os.Stderr, "Error parsing manifest: %v\n", err)
+ os.Exit(1)
+ }
+
+ manifestKeys := make(map[string]bool)
+ for _, e := range manifest {
+ manifestKeys[endpointKey(e)] = true
+ }
+ currentKeys := make(map[string]bool)
+ for _, e := range current {
+ currentKeys[endpointKey(e)] = true
+ }
+
+ var added, removed []Endpoint
+ for _, e := range current {
+ if !manifestKeys[endpointKey(e)] {
+ added = append(added, e)
+ }
+ }
+ for _, e := range manifest {
+ if !currentKeys[endpointKey(e)] {
+ removed = append(removed, e)
+ }
+ }
+
+ if len(added) == 0 && len(removed) == 0 {
+ fmt.Printf("No endpoint drift detected. %d endpoints match the manifest.\n", len(current))
+ os.Exit(0)
+ }
+
+ fmt.Println("Endpoint drift detected!")
+ fmt.Println()
+
+ if len(added) > 0 {
+ fmt.Printf("New endpoints (%d):\n", len(added))
+ for _, e := range added {
+ fmt.Printf(" + %s %s — %s [%s]\n", e.Method, e.Path, e.Summary, e.Tag)
+ }
+ fmt.Println()
+ }
+
+ if len(removed) > 0 {
+ fmt.Printf("Removed endpoints (%d):\n", len(removed))
+ for _, e := range removed {
+ fmt.Printf(" - %s %s — %s [%s]\n", e.Method, e.Path, e.Summary, e.Tag)
+ }
+ fmt.Println()
+ }
+
+ fmt.Println("Update endpoint-manifest.json from the MCP repo to resolve drift.")
+ os.Exit(1)
+}
diff --git a/tools/tools.go b/tools/tools.go
index 867d3a2..c41f614 100644
--- a/tools/tools.go
+++ b/tools/tools.go
@@ -1,4 +1,4 @@
-// Copyright (c) HashiCorp, Inc.
+// Copyright (c) EZSCALE.
// SPDX-License-Identifier: MPL-2.0
//go:build tools