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