0
0
mirror of https://github.com/bpg/terraform-provider-proxmox.git synced 2025-06-29 18:21:10 +00:00

feat: API client cleanup and refactoring (#323)

* cleanup 1

* continue refactoring

* more refactoring

* move VMs under nodes

* move container and other apis under nodes

* cleanups

* enabled revive.exported linter & add comments to exported stuff

* enable godot linter

* enable wsl linter

* enable thelper linter

* enable govet linter

* cleanup after rebase

* cleanup after rebase

* extract SSH ops into a separate interface

* fix linter error

* move ssh code to its own package

* cleaning up VirtualEnvironmentClient receivers

* on the finish line

* not sure what else I forgot... 🤔

* fix ssh connection and upload

* renaming client interfaces

* final cleanups
This commit is contained in:
Pavel Boldyrev 2023-05-25 21:32:51 -04:00 committed by GitHub
parent 23585570ab
commit 1f006aa82b
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
181 changed files with 5488 additions and 5015 deletions

View File

@ -9,6 +9,9 @@ issues:
# Maximum count of issues with the same text. Set to 0 to disable.
# Default is 3.
max-same-issues: 0
include:
- EXC0012
- EXC0014
exclude-rules:
# Exclude duplicate code and function length and complexity checking in test
# files (due to common repeats and long functions in test code)
@ -43,6 +46,11 @@ linters-settings:
funlen:
lines: 80
statements: 60
errcheck:
check-blank: true
ignoretests: true
revive:
exported: ["checkPrivateReceivers"]
linters:
enable-all: true
disable:
@ -53,6 +61,7 @@ linters:
- ifshort
- interfacer
- maligned
- nosnakecase
- rowserrcheck
- scopelint
- structcheck
@ -63,24 +72,16 @@ linters:
- forcetypeassert
- funlen
- gocognit
- govet
# others
- exhaustivestruct
- exhaustruct
- gci
- gochecknoinits
- godot
- goerr113
- gomnd
- grouper
- ireturn
- maintidx
- nlreturn
- nonamedreturns
- nosnakecase
- tagliatelle
- testpackage
- thelper
- varnamelen
- wsl
fast: false

50
proxmox/access/acl.go Normal file
View File

@ -0,0 +1,50 @@
/*
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
*/
package access
import (
"context"
"fmt"
"net/http"
"sort"
"github.com/bpg/terraform-provider-proxmox/proxmox/api"
)
func (c *Client) aclPath() string {
return c.ExpandPath("acl")
}
// GetACL retrieves the access control list.
func (c *Client) GetACL(ctx context.Context) ([]*ACLGetResponseData, error) {
resBody := &ACLGetResponseBody{}
err := c.DoRequest(ctx, http.MethodGet, c.aclPath(), nil, resBody)
if err != nil {
return nil, fmt.Errorf("failed to get access control list: %w", err)
}
if resBody.Data == nil {
return nil, api.ErrNoDataObjectInResponse
}
sort.Slice(resBody.Data, func(i, j int) bool {
return resBody.Data[i].Path < resBody.Data[j].Path
})
return resBody.Data, nil
}
// UpdateACL updates the access control list.
func (c *Client) UpdateACL(ctx context.Context, d *ACLUpdateRequestBody) error {
err := c.DoRequest(ctx, http.MethodPut, c.aclPath(), d, nil)
if err != nil {
return fmt.Errorf("failed to update access control list: %w", err)
}
return nil
}

View File

@ -1,18 +1,20 @@
/* This Source Code Form is subject to the terms of the Mozilla Public
/*
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at https://mozilla.org/MPL/2.0/. */
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
*/
package proxmox
package access
import "github.com/bpg/terraform-provider-proxmox/proxmox/types"
// VirtualEnvironmentACLGetResponseBody contains the body from an access control list response.
type VirtualEnvironmentACLGetResponseBody struct {
Data []*VirtualEnvironmentACLGetResponseData `json:"data,omitempty"`
// ACLGetResponseBody contains the body from an access control list response.
type ACLGetResponseBody struct {
Data []*ACLGetResponseData `json:"data,omitempty"`
}
// VirtualEnvironmentACLGetResponseData contains the data from an access control list response.
type VirtualEnvironmentACLGetResponseData struct {
// ACLGetResponseData contains the data from an access control list response.
type ACLGetResponseData struct {
Path string `json:"path"`
Propagate *types.CustomBool `json:"propagate,omitempty"`
RoleID string `json:"roleid"`
@ -20,8 +22,8 @@ type VirtualEnvironmentACLGetResponseData struct {
UserOrGroupID string `json:"ugid"`
}
// VirtualEnvironmentACLUpdateRequestBody contains the data for an access control list update request.
type VirtualEnvironmentACLUpdateRequestBody struct {
// ACLUpdateRequestBody contains the data for an access control list update request.
type ACLUpdateRequestBody struct {
Delete *types.CustomBool `json:"delete,omitempty" url:"delete,omitempty,int"`
Groups []string `json:"groups,omitempty" url:"groups,omitempty,comma"`
Path string `json:"path" url:"path"`

23
proxmox/access/client.go Normal file
View File

@ -0,0 +1,23 @@
/*
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
*/
package access
import (
"fmt"
"github.com/bpg/terraform-provider-proxmox/proxmox/api"
)
// Client is an interface for performing requests against the Proxmox 'access' API.
type Client struct {
api.Client
}
// ExpandPath expands a path relative to the client's base path.
func (c *Client) ExpandPath(path string) string {
return fmt.Sprintf("access/%s", path)
}

93
proxmox/access/groups.go Normal file
View File

@ -0,0 +1,93 @@
/*
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
*/
package access
import (
"context"
"fmt"
"net/http"
"net/url"
"sort"
"github.com/bpg/terraform-provider-proxmox/proxmox/api"
)
func (c *Client) groupsPath() string {
return c.ExpandPath("groups")
}
func (c *Client) groupPath(id string) string {
return fmt.Sprintf("%s/%s", c.groupsPath(), url.PathEscape(id))
}
// CreateGroup creates an access group.
func (c *Client) CreateGroup(ctx context.Context, d *GroupCreateRequestBody) error {
err := c.DoRequest(ctx, http.MethodPost, c.groupsPath(), d, nil)
if err != nil {
return fmt.Errorf("failed to create access group: %w", err)
}
return nil
}
// DeleteGroup deletes an access group.
func (c *Client) DeleteGroup(ctx context.Context, id string) error {
err := c.DoRequest(ctx, http.MethodDelete, c.groupPath(id), nil, nil)
if err != nil {
return fmt.Errorf("failed to delete access group: %w", err)
}
return nil
}
// GetGroup retrieves an access group.
func (c *Client) GetGroup(ctx context.Context, id string) (*GroupGetResponseData, error) {
resBody := &GroupGetResponseBody{}
err := c.DoRequest(ctx, http.MethodGet, c.groupPath(id), nil, resBody)
if err != nil {
return nil, fmt.Errorf("failed to get access group: %w", err)
}
if resBody.Data == nil {
return nil, api.ErrNoDataObjectInResponse
}
sort.Strings(resBody.Data.Members)
return resBody.Data, nil
}
// ListGroups retrieves a list of access groups.
func (c *Client) ListGroups(ctx context.Context) ([]*GroupListResponseData, error) {
resBody := &GroupListResponseBody{}
err := c.DoRequest(ctx, http.MethodGet, c.groupsPath(), nil, resBody)
if err != nil {
return nil, fmt.Errorf("failed to list access groups: %w", err)
}
if resBody.Data == nil {
return nil, api.ErrNoDataObjectInResponse
}
sort.Slice(resBody.Data, func(i, j int) bool {
return resBody.Data[i].ID < resBody.Data[j].ID
})
return resBody.Data, nil
}
// UpdateGroup updates an access group.
func (c *Client) UpdateGroup(ctx context.Context, id string, d *GroupUpdateRequestBody) error {
err := c.DoRequest(ctx, http.MethodPut, c.groupPath(id), d, nil)
if err != nil {
return fmt.Errorf("failed to update access group: %w", err)
}
return nil
}

View File

@ -0,0 +1,40 @@
/*
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
*/
package access
// GroupCreateRequestBody contains the data for an access group create request.
type GroupCreateRequestBody struct {
Comment *string `json:"comment,omitempty" url:"comment,omitempty"`
ID string `json:"groupid" url:"groupid"`
}
// GroupGetResponseBody contains the body from an access group get response.
type GroupGetResponseBody struct {
Data *GroupGetResponseData `json:"data,omitempty"`
}
// GroupGetResponseData contains the data from an access group get response.
type GroupGetResponseData struct {
Comment *string `json:"comment,omitempty"`
Members []string `json:"members"`
}
// GroupListResponseBody contains the body from an access group list response.
type GroupListResponseBody struct {
Data []*GroupListResponseData `json:"data,omitempty"`
}
// GroupListResponseData contains the data from an access group list response.
type GroupListResponseData struct {
Comment *string `json:"comment,omitempty"`
ID string `json:"groupid"`
}
// GroupUpdateRequestBody contains the data for an access group update request.
type GroupUpdateRequestBody struct {
Comment *string `json:"comment,omitempty" url:"comment,omitempty"`
}

100
proxmox/access/roles.go Normal file
View File

@ -0,0 +1,100 @@
/*
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
*/
package access
import (
"context"
"fmt"
"net/http"
"net/url"
"sort"
"github.com/bpg/terraform-provider-proxmox/proxmox/api"
"github.com/bpg/terraform-provider-proxmox/proxmox/types"
)
func (c *Client) rolesPath() string {
return c.ExpandPath("roles")
}
func (c *Client) rolePath(id string) string {
return fmt.Sprintf("%s/%s", c.rolesPath(), url.PathEscape(id))
}
// CreateRole creates an access role.
func (c *Client) CreateRole(ctx context.Context, d *RoleCreateRequestBody) error {
err := c.DoRequest(ctx, http.MethodPost, c.rolesPath(), d, nil)
if err != nil {
return fmt.Errorf("error creating role: %w", err)
}
return nil
}
// DeleteRole deletes an access role.
func (c *Client) DeleteRole(ctx context.Context, id string) error {
err := c.DoRequest(ctx, http.MethodDelete, c.rolePath(id), nil, nil)
if err != nil {
return fmt.Errorf("error deleting role: %w", err)
}
return nil
}
// GetRole retrieves an access role.
func (c *Client) GetRole(ctx context.Context, id string) (*types.CustomPrivileges, error) {
resBody := &RoleGetResponseBody{}
err := c.DoRequest(ctx, http.MethodGet, c.rolePath(id), nil, resBody)
if err != nil {
return nil, fmt.Errorf("error getting role: %w", err)
}
if resBody.Data == nil {
return nil, api.ErrNoDataObjectInResponse
}
sort.Strings(*resBody.Data)
return resBody.Data, nil
}
// ListRoles retrieves a list of access roles.
func (c *Client) ListRoles(ctx context.Context) ([]*RoleListResponseData, error) {
resBody := &RoleListResponseBody{}
err := c.DoRequest(ctx, http.MethodGet, c.rolesPath(), nil, resBody)
if err != nil {
return nil, fmt.Errorf("error listing roles: %w", err)
}
if resBody.Data == nil {
return nil, api.ErrNoDataObjectInResponse
}
sort.Slice(resBody.Data, func(i, j int) bool {
return resBody.Data[i].ID < resBody.Data[j].ID
})
for i := range resBody.Data {
if resBody.Data[i].Privileges != nil {
sort.Strings(*resBody.Data[i].Privileges)
}
}
return resBody.Data, nil
}
// UpdateRole updates an access role.
func (c *Client) UpdateRole(ctx context.Context, id string, d *RoleUpdateRequestBody) error {
err := c.DoRequest(ctx, http.MethodPut, c.rolePath(id), d, nil)
if err != nil {
return fmt.Errorf("error updating role: %w", err)
}
return nil
}

View File

@ -0,0 +1,37 @@
/*
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
*/
package access
import "github.com/bpg/terraform-provider-proxmox/proxmox/types"
// RoleCreateRequestBody contains the data for an access group create request.
type RoleCreateRequestBody struct {
ID string `json:"roleid" url:"roleid"`
Privileges types.CustomPrivileges `json:"privs" url:"privs,comma"`
}
// RoleGetResponseBody contains the body from an access group get response.
type RoleGetResponseBody struct {
Data *types.CustomPrivileges `json:"data,omitempty"`
}
// RoleListResponseBody contains the body from an access group list response.
type RoleListResponseBody struct {
Data []*RoleListResponseData `json:"data,omitempty"`
}
// RoleListResponseData contains the data from an access group list response.
type RoleListResponseData struct {
ID string `json:"roleid"`
Privileges *types.CustomPrivileges `json:"privs,omitempty"`
Special *types.CustomBool `json:"special,omitempty"`
}
// RoleUpdateRequestBody contains the data for an access group update request.
type RoleUpdateRequestBody struct {
Privileges types.CustomPrivileges `json:"privs" url:"privs,comma"`
}

128
proxmox/access/users.go Normal file
View File

@ -0,0 +1,128 @@
/*
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
*/
package access
import (
"context"
"fmt"
"net/http"
"net/url"
"sort"
"time"
"github.com/bpg/terraform-provider-proxmox/proxmox/api"
"github.com/bpg/terraform-provider-proxmox/proxmox/types"
)
func (c *Client) usersPath() string {
return c.ExpandPath("users")
}
func (c *Client) userPath(id string) string {
return fmt.Sprintf("%s/%s", c.usersPath(), url.PathEscape(id))
}
// ChangeUserPassword changes a user's password.
func (c *Client) ChangeUserPassword(ctx context.Context, id, password string) error {
d := UserChangePasswordRequestBody{
ID: id,
Password: password,
}
err := c.DoRequest(ctx, http.MethodPut, c.ExpandPath("password"), d, nil)
if err != nil {
return fmt.Errorf("error changing user password: %w", err)
}
return nil
}
// CreateUser creates a user.
func (c *Client) CreateUser(ctx context.Context, d *UserCreateRequestBody) error {
err := c.DoRequest(ctx, http.MethodPost, c.usersPath(), d, nil)
if err != nil {
return fmt.Errorf("error creating user: %w", err)
}
return nil
}
// DeleteUser deletes an user.
func (c *Client) DeleteUser(ctx context.Context, id string) error {
err := c.DoRequest(ctx, http.MethodDelete, c.userPath(id), nil, nil)
if err != nil {
return fmt.Errorf("error deleting user: %w", err)
}
return nil
}
// GetUser retrieves a user.
func (c *Client) GetUser(ctx context.Context, id string) (*UserGetResponseData, error) {
resBody := &UserGetResponseBody{}
err := c.DoRequest(ctx, http.MethodGet, c.userPath(id), nil, resBody)
if err != nil {
return nil, fmt.Errorf("error retrieving user: %w", err)
}
if resBody.Data == nil {
return nil, api.ErrNoDataObjectInResponse
}
if resBody.Data.ExpirationDate != nil {
expirationDate := types.CustomTimestamp(time.Time(*resBody.Data.ExpirationDate).UTC())
resBody.Data.ExpirationDate = &expirationDate
}
if resBody.Data.Groups != nil {
sort.Strings(*resBody.Data.Groups)
}
return resBody.Data, nil
}
// ListUsers retrieves a list of users.
func (c *Client) ListUsers(ctx context.Context) ([]*UserListResponseData, error) {
resBody := &UserListResponseBody{}
err := c.DoRequest(ctx, http.MethodGet, c.usersPath(), nil, resBody)
if err != nil {
return nil, fmt.Errorf("error listing users: %w", err)
}
if resBody.Data == nil {
return nil, api.ErrNoDataObjectInResponse
}
sort.Slice(resBody.Data, func(i, j int) bool {
return resBody.Data[i].ID < resBody.Data[j].ID
})
for i := range resBody.Data {
if resBody.Data[i].ExpirationDate != nil {
expirationDate := types.CustomTimestamp(time.Time(*resBody.Data[i].ExpirationDate).UTC())
resBody.Data[i].ExpirationDate = &expirationDate
}
if resBody.Data[i].Groups != nil {
sort.Strings(*resBody.Data[i].Groups)
}
}
return resBody.Data, nil
}
// UpdateUser updates a user.
func (c *Client) UpdateUser(ctx context.Context, id string, d *UserUpdateRequestBody) error {
err := c.DoRequest(ctx, http.MethodPut, c.userPath(id), d, nil)
if err != nil {
return fmt.Errorf("error updating user: %w", err)
}
return nil
}

View File

@ -1,19 +1,21 @@
/* This Source Code Form is subject to the terms of the Mozilla Public
/*
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at https://mozilla.org/MPL/2.0/. */
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
*/
package proxmox
package access
import "github.com/bpg/terraform-provider-proxmox/proxmox/types"
// VirtualEnvironmentUserChangePasswordRequestBody contains the data for a user password change request.
type VirtualEnvironmentUserChangePasswordRequestBody struct {
// UserChangePasswordRequestBody contains the data for a user password change request.
type UserChangePasswordRequestBody struct {
ID string `json:"userid" url:"userid"`
Password string `json:"password" url:"password"`
}
// VirtualEnvironmentUserCreateRequestBody contains the data for an user create request.
type VirtualEnvironmentUserCreateRequestBody struct {
// UserCreateRequestBody contains the data for a user create request.
type UserCreateRequestBody struct {
Comment *string `json:"comment,omitempty" url:"comment,omitempty"`
Email *string `json:"email,omitempty" url:"email,omitempty"`
Enabled *types.CustomBool `json:"enable,omitempty" url:"enable,omitempty,int"`
@ -26,13 +28,13 @@ type VirtualEnvironmentUserCreateRequestBody struct {
Password string `json:"password" url:"password"`
}
// VirtualEnvironmentUserGetResponseBody contains the body from an user get response.
type VirtualEnvironmentUserGetResponseBody struct {
Data *VirtualEnvironmentUserGetResponseData `json:"data,omitempty"`
// UserGetResponseBody contains the body from a user get response.
type UserGetResponseBody struct {
Data *UserGetResponseData `json:"data,omitempty"`
}
// VirtualEnvironmentUserGetResponseData contains the data from an user get response.
type VirtualEnvironmentUserGetResponseData struct {
// UserGetResponseData contains the data from an user get response.
type UserGetResponseData struct {
Comment *string `json:"comment,omitempty"`
Email *string `json:"email,omitempty"`
Enabled *types.CustomBool `json:"enable,omitempty"`
@ -43,13 +45,13 @@ type VirtualEnvironmentUserGetResponseData struct {
LastName *string `json:"lastname,omitempty"`
}
// VirtualEnvironmentUserListResponseBody contains the body from an user list response.
type VirtualEnvironmentUserListResponseBody struct {
Data []*VirtualEnvironmentUserListResponseData `json:"data,omitempty"`
// UserListResponseBody contains the body from a user list response.
type UserListResponseBody struct {
Data []*UserListResponseData `json:"data,omitempty"`
}
// VirtualEnvironmentUserListResponseData contains the data from an user list response.
type VirtualEnvironmentUserListResponseData struct {
// UserListResponseData contains the data from an user list response.
type UserListResponseData struct {
Comment *string `json:"comment,omitempty"`
Email *string `json:"email,omitempty"`
Enabled *types.CustomBool `json:"enable,omitempty"`
@ -61,8 +63,8 @@ type VirtualEnvironmentUserListResponseData struct {
LastName *string `json:"lastname,omitempty"`
}
// VirtualEnvironmentUserUpdateRequestBody contains the data for an user update request.
type VirtualEnvironmentUserUpdateRequestBody struct {
// UserUpdateRequestBody contains the data for an user update request.
type UserUpdateRequestBody struct {
Append *types.CustomBool `json:"append,omitempty" url:"append,omitempty"`
Comment *string `json:"comment,omitempty" url:"comment,omitempty"`
Email *string `json:"email,omitempty" url:"email,omitempty"`

View File

@ -1,8 +1,10 @@
/* This Source Code Form is subject to the terms of the Mozilla Public
/*
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at https://mozilla.org/MPL/2.0/. */
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
*/
package proxmox
package api
import (
"bytes"
@ -14,40 +16,37 @@ import (
"net/url"
"github.com/hashicorp/terraform-plugin-log/tflog"
)
const (
// DefaultRootAccount contains the default username and realm for the root account.
DefaultRootAccount = "root@pam"
"github.com/bpg/terraform-provider-proxmox/utils"
)
// Authenticate authenticates against the specified endpoint.
func (c *VirtualEnvironmentClient) Authenticate(ctx context.Context, reset bool) error {
func (c *client) Authenticate(ctx context.Context, reset bool) error {
if c.authenticationData != nil && !reset {
return nil
}
var reqBody *bytes.Buffer
if c.OTP != nil {
if c.otp != nil {
reqBody = bytes.NewBufferString(fmt.Sprintf(
"username=%s&password=%s&otp=%s",
url.QueryEscape(c.Username),
url.QueryEscape(c.Password),
url.QueryEscape(*c.OTP),
url.QueryEscape(c.username),
url.QueryEscape(c.password),
url.QueryEscape(*c.otp),
))
} else {
reqBody = bytes.NewBufferString(fmt.Sprintf(
"username=%s&password=%s",
url.QueryEscape(c.Username),
url.QueryEscape(c.Password),
url.QueryEscape(c.username),
url.QueryEscape(c.password),
))
}
req, err := http.NewRequestWithContext(
ctx,
http.MethodPost,
fmt.Sprintf("%s/%s/access/ticket", c.Endpoint, basePathJSONAPI),
fmt.Sprintf("%s/%s/access/ticket", c.endpoint, basePathJSONAPI),
reqBody,
)
if err != nil {
@ -64,12 +63,14 @@ func (c *VirtualEnvironmentClient) Authenticate(ctx context.Context, reset bool)
return fmt.Errorf("failed to retrieve authentication response: %w", err)
}
err = c.ValidateResponseCode(res)
defer utils.CloseOrLogError(ctx)(res.Body)
err = c.validateResponseCode(res)
if err != nil {
return err
return fmt.Errorf("failed to authenticate: %w", err)
}
resBody := VirtualEnvironmentAuthenticationResponseBody{}
resBody := AuthenticationResponseBody{}
err = json.NewDecoder(res.Body).Decode(&resBody)
if err != nil {
return fmt.Errorf("failed to decode authentication response, %w", err)
@ -99,10 +100,10 @@ func (c *VirtualEnvironmentClient) Authenticate(ctx context.Context, reset bool)
}
// AuthenticateRequest adds authentication data to a new request.
func (c *VirtualEnvironmentClient) AuthenticateRequest(ctx context.Context, req *http.Request) error {
func (c *client) AuthenticateRequest(ctx context.Context, req *http.Request) error {
err := c.Authenticate(ctx, false)
if err != nil {
return err
return fmt.Errorf("failed to authenticate: %w", err)
}
req.AddCookie(&http.Cookie{
@ -110,7 +111,7 @@ func (c *VirtualEnvironmentClient) AuthenticateRequest(ctx context.Context, req
Value: *c.authenticationData.Ticket,
})
if req.Method != "GET" {
if req.Method != http.MethodGet {
req.Header.Add("CSRFPreventionToken", *c.authenticationData.CSRFPreventionToken)
}

View File

@ -0,0 +1,32 @@
/*
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
*/
package api
import "github.com/bpg/terraform-provider-proxmox/proxmox/types"
// AuthenticationResponseBody contains the body from an authentication response.
type AuthenticationResponseBody struct {
Data *AuthenticationResponseData `json:"data,omitempty"`
}
// AuthenticationResponseCapabilities contains the supported capabilities for a session.
type AuthenticationResponseCapabilities struct {
Access *types.CustomPrivileges `json:"access,omitempty"`
Datacenter *types.CustomPrivileges `json:"dc,omitempty"`
Nodes *types.CustomPrivileges `json:"nodes,omitempty"`
Storage *types.CustomPrivileges `json:"storage,omitempty"`
VMs *types.CustomPrivileges `json:"vms,omitempty"`
}
// AuthenticationResponseData contains the data from an authentication response.
type AuthenticationResponseData struct {
ClusterName *string `json:"clustername,omitempty"`
CSRFPreventionToken *string `json:"CSRFPreventionToken,omitempty"`
Capabilities *AuthenticationResponseCapabilities `json:"cap,omitempty"`
Ticket *string `json:"ticket,omitempty"`
Username string `json:"username"`
}

View File

@ -4,7 +4,7 @@
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
*/
package proxmox
package api
import (
"bytes"
@ -16,19 +16,35 @@ import (
"io"
"net/http"
"net/url"
"runtime"
"strings"
"github.com/google/go-querystring/query"
"github.com/hashicorp/terraform-plugin-log/tflog"
"github.com/hashicorp/terraform-plugin-sdk/v2/helper/logging"
"github.com/bpg/terraform-provider-proxmox/utils"
)
// NewVirtualEnvironmentClient creates and initializes a VirtualEnvironmentClient instance.
func NewVirtualEnvironmentClient(
endpoint, username, password, otp string,
insecure bool, sshUsername string, sshPassword string, sshAgent bool, sshAgentSocket string,
) (*VirtualEnvironmentClient, error) {
const (
basePathJSONAPI = "api2/json"
)
// VirtualEnvironmentClient implements an API client for the Proxmox Virtual Environment API.
type client struct {
endpoint string
insecure bool
otp *string
password string
username string
authenticationData *AuthenticationResponseData
httpClient *http.Client
}
// NewClient creates and initializes a VirtualEnvironmentClient instance.
func NewClient(
endpoint, username, password, otp string, insecure bool,
) (Client, error) {
u, err := url.ParseRequestURI(endpoint)
if err != nil {
return nil, errors.New(
@ -71,43 +87,25 @@ func NewVirtualEnvironmentClient(
InsecureSkipVerify: insecure, //nolint:gosec
},
}
if logging.IsDebugOrHigher() {
transport = logging.NewLoggingHTTPTransport(transport)
}
httpClient := &http.Client{Transport: transport}
if sshUsername == "" {
sshUsername = strings.Split(username, "@")[0]
}
if sshPassword == "" {
sshPassword = password
}
if sshAgent && runtime.GOOS != "linux" && runtime.GOOS != "darwin" && runtime.GOOS != "freebsd" {
return nil, errors.New(
"the ssh agent flag is only supported on POSIX systems, please set it to 'false'" +
" or remove it from your provider configuration",
)
}
return &VirtualEnvironmentClient{
Endpoint: strings.TrimRight(u.String(), "/"),
Insecure: insecure,
OTP: pOTP,
Password: password,
Username: username,
SSHUsername: sshUsername,
SSHPassword: sshPassword,
SSHAgent: sshAgent,
SSHAgentSocket: sshAgentSocket,
httpClient: httpClient,
return &client{
endpoint: strings.TrimRight(u.String(), "/"),
insecure: insecure,
otp: pOTP,
password: password,
username: username,
httpClient: httpClient,
}, nil
}
// DoRequest performs a HTTP request against a JSON API endpoint.
func (c *VirtualEnvironmentClient) DoRequest(
func (c *client) DoRequest(
ctx context.Context,
method, path string,
requestBody, responseBody interface{},
@ -119,7 +117,7 @@ func (c *VirtualEnvironmentClient) DoRequest(
reqBodyType := ""
if requestBody != nil {
multipartData, multipart := requestBody.(*VirtualEnvironmentMultiPartData)
multipartData, multipart := requestBody.(*MultiPartData)
pipedBodyReader, pipedBody := requestBody.(*io.PipeReader)
if multipart {
@ -158,7 +156,7 @@ func (c *VirtualEnvironmentClient) DoRequest(
req, err := http.NewRequestWithContext(
ctx,
method,
fmt.Sprintf("%s/%s/%s", c.Endpoint, basePathJSONAPI, modifiedPath),
fmt.Sprintf("%s/%s/%s", c.endpoint, basePathJSONAPI, modifiedPath),
reqBodyReader,
)
if err != nil {
@ -201,9 +199,9 @@ func (c *VirtualEnvironmentClient) DoRequest(
return fErr
}
defer CloseOrLogError(ctx)(res.Body)
defer utils.CloseOrLogError(ctx)(res.Body)
err = c.ValidateResponseCode(res)
err = c.validateResponseCode(res)
if err != nil {
tflog.Warn(ctx, err.Error())
return err
@ -232,12 +230,21 @@ func (c *VirtualEnvironmentClient) DoRequest(
return nil
}
// ValidateResponseCode ensures that a response is valid.
func (c *VirtualEnvironmentClient) ValidateResponseCode(res *http.Response) error {
// ExpandPath expands the given path to an absolute path.
func (c *client) ExpandPath(path string) string {
return path
}
func (c *client) IsRoot() bool {
return c.username == "root@pam"
}
// validateResponseCode ensures that a response is valid.
func (c *client) validateResponseCode(res *http.Response) error {
if res.StatusCode < 200 || res.StatusCode >= 300 {
status := strings.TrimPrefix(res.Status, fmt.Sprintf("%d ", res.StatusCode))
errRes := &VirtualEnvironmentErrorResponseBody{}
errRes := &ErrorResponseBody{}
err := json.NewDecoder(res.Body).Decode(errRes)
if err == nil && errRes.Errors != nil {

View File

@ -0,0 +1,56 @@
/*
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
*/
package api
import (
"context"
"errors"
"io"
"os"
)
// ErrNoDataObjectInResponse is returned when the server does not include a data object in the response.
var ErrNoDataObjectInResponse = errors.New("the server did not include a data object in the response")
// Client is an interface for performing requests against the Proxmox API.
type Client interface {
// DoRequest performs a request against the Proxmox API.
DoRequest(
ctx context.Context,
method, path string,
requestBody, responseBody interface{},
) error
// ExpandPath expands a path relative to the client's base path.
// For example, if the client is configured for a VM and the
// path is "firewall/options", the returned path will be
// "/nodes/<node>/qemu/<vmid>/firewall/options".
ExpandPath(path string) string
// IsRoot returns true if the client is configured with the root user.
IsRoot() bool
}
// MultiPartData enables multipart uploads in DoRequest.
type MultiPartData struct {
Boundary string
Reader io.Reader
Size *int64
}
// ErrorResponseBody contains the body of an error response.
type ErrorResponseBody struct {
Data *string `json:"data"`
Errors *map[string]string `json:"errors"`
}
// FileUploadRequest is a request for uploading a file.
type FileUploadRequest struct {
ContentType string
FileName string
File *os.File
}

95
proxmox/client.go Normal file
View File

@ -0,0 +1,95 @@
/*
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
*/
package proxmox
import (
"github.com/bpg/terraform-provider-proxmox/proxmox/access"
"github.com/bpg/terraform-provider-proxmox/proxmox/api"
"github.com/bpg/terraform-provider-proxmox/proxmox/cluster"
"github.com/bpg/terraform-provider-proxmox/proxmox/nodes"
"github.com/bpg/terraform-provider-proxmox/proxmox/pools"
"github.com/bpg/terraform-provider-proxmox/proxmox/ssh"
"github.com/bpg/terraform-provider-proxmox/proxmox/storage"
"github.com/bpg/terraform-provider-proxmox/proxmox/version"
)
// Client defines a client interface for the Proxmox Virtual Environment API.
type Client interface {
// Access returns a client for managing access control.
Access() *access.Client
// Cluster returns a client for managing the cluster.
Cluster() *cluster.Client
// Node returns a client for managing resources on a specific node.
Node(nodeName string) *nodes.Client
// Pool returns a client for managing resource pools.
Pool() *pools.Client
// Storage returns a client for managing storage.
Storage() *storage.Client
// Version returns a client for getting the version of the Proxmox Virtual Environment API.
Version() *version.Client
// API returns a lower-lever REST API client.
API() api.Client
// SSH returns a lower-lever SSH client.
SSH() ssh.Client
}
type client struct {
a api.Client
s ssh.Client
}
// NewClient creates a new API client.
func NewClient(a api.Client, s ssh.Client) Client {
return &client{a: a, s: s}
}
// Access returns a client for managing access control.
func (c *client) Access() *access.Client {
return &access.Client{Client: c.a}
}
// Cluster returns a client for managing the cluster.
func (c *client) Cluster() *cluster.Client {
return &cluster.Client{Client: c.a}
}
// Node returns a client for managing resources on a specific node.
func (c *client) Node(nodeName string) *nodes.Client {
return &nodes.Client{Client: c.a, NodeName: nodeName}
}
// Pool returns a client for managing resource pools.
func (c *client) Pool() *pools.Client {
return &pools.Client{Client: c.a}
}
// Storage returns a client for managing storage.
func (c *client) Storage() *storage.Client {
return &storage.Client{Client: c.a}
}
// Version returns a client for getting the version of the Proxmox Virtual Environment API.
func (c *client) Version() *version.Client {
return &version.Client{Client: c.a}
}
// API returns a lower-lever REST API client.
func (c *client) API() api.Client {
return c.a
}
// SSH returns a lower-lever SSH client.s.
func (c *client) SSH() ssh.Client {
return c.s
}

View File

@ -9,19 +9,22 @@ package cluster
import (
"fmt"
"github.com/bpg/terraform-provider-proxmox/proxmox/api"
clusterfirewall "github.com/bpg/terraform-provider-proxmox/proxmox/cluster/firewall"
"github.com/bpg/terraform-provider-proxmox/proxmox/firewall"
"github.com/bpg/terraform-provider-proxmox/proxmox/types"
)
// Client is an interface for accessing the Proxmox cluster API.
type Client struct {
types.Client
api.Client
}
// ExpandPath expands a relative path to a full cluster API path.
func (c *Client) ExpandPath(path string) string {
return fmt.Sprintf("cluster/%s", path)
}
// Firewall returns a client for managing the cluster firewall.
func (c *Client) Firewall() clusterfirewall.API {
return &clusterfirewall.Client{
Client: firewall.Client{Client: c},

View File

@ -11,6 +11,22 @@ import (
"errors"
"fmt"
"net/http"
"sync"
"github.com/hashicorp/terraform-plugin-log/tflog"
"github.com/bpg/terraform-provider-proxmox/proxmox/api"
)
const (
getVMIDStep = 1
)
var (
//nolint:gochecknoglobals
getVMIDCounter = -1
//nolint:gochecknoglobals
getVMIDCounterMutex = &sync.Mutex{}
)
// GetNextID retrieves the next free VM identifier for the cluster.
@ -20,14 +36,61 @@ func (c *Client) GetNextID(ctx context.Context, vmID *int) (*int, error) {
}
resBody := &NextIDResponseBody{}
err := c.DoRequest(ctx, http.MethodGet, "cluster/nextid", reqBody, resBody)
if err != nil {
return nil, fmt.Errorf("error retrieving next VM ID: %w", err)
}
if resBody.Data == nil {
return nil, errors.New("the server did not include a data object in the response")
return nil, api.ErrNoDataObjectInResponse
}
return (*int)(resBody.Data), nil
}
// GetVMID retrieves the next available VM identifier.
func (c *Client) GetVMID(ctx context.Context) (*int, error) {
getVMIDCounterMutex.Lock()
defer getVMIDCounterMutex.Unlock()
if getVMIDCounter < 0 {
nextVMID, err := c.GetNextID(ctx, nil)
if err != nil {
return nil, err
}
if nextVMID == nil {
return nil, errors.New("unable to retrieve the next available VM identifier")
}
getVMIDCounter = *nextVMID + getVMIDStep
tflog.Debug(ctx, "next VM identifier", map[string]interface{}{
"id": *nextVMID,
})
return nextVMID, nil
}
vmID := getVMIDCounter
for vmID <= 2147483637 {
_, err := c.GetNextID(ctx, &vmID)
if err != nil {
vmID += getVMIDStep
continue
}
getVMIDCounter = vmID + getVMIDStep
tflog.Debug(ctx, "next VM identifier", map[string]interface{}{
"id": vmID,
})
return &vmID, nil
}
return nil, errors.New("unable to determine the next available VM identifier")
}

View File

@ -12,6 +12,7 @@ import (
"github.com/bpg/terraform-provider-proxmox/proxmox/firewall"
)
// API is an interface for managing cluster firewall.
type API interface {
firewall.API
SecurityGroup
@ -19,6 +20,7 @@ type API interface {
SecurityGroup(group string) firewall.Rule
}
// Client is an interface for accessing the Proxmox cluster firewall API.
type Client struct {
firewall.Client
}
@ -28,6 +30,7 @@ type groupClient struct {
Group string
}
// SecurityGroup returns a client for managing a specific security group.
func (c *Client) SecurityGroup(group string) firewall.Rule {
// My head really hurts when I'm looking at this code
// I'm not sure if this is the best way to do the required

View File

@ -8,125 +8,39 @@ package firewall
import (
"context"
"encoding/json"
"fmt"
"net/http"
"net/url"
"strconv"
"strings"
"github.com/bpg/terraform-provider-proxmox/proxmox/types"
"github.com/bpg/terraform-provider-proxmox/proxmox/api"
)
// Options is an interface for managing global firewall options.
type Options interface {
SetGlobalOptions(ctx context.Context, d *OptionsPutRequestBody) error
GetGlobalOptions(ctx context.Context) (*OptionsGetResponseData, error)
}
type OptionsPutRequestBody struct {
EBTables *types.CustomBool `json:"ebtables,omitempty" url:"ebtables,omitempty,int"`
Enable *types.CustomBool `json:"enable,omitempty" url:"enable,omitempty,int"`
LogRateLimit *CustomLogRateLimit `json:"log_ratelimit,omitempty" url:"log_ratelimit,omitempty"`
PolicyIn *string `json:"policy_in,omitempty" url:"policy_in,omitempty"`
PolicyOut *string `json:"policy_out,omitempty" url:"policy_out,omitempty"`
}
type CustomLogRateLimit struct {
Enable types.CustomBool `json:"enable,omitempty" url:"enable,omitempty,int"`
Burst *int `json:"burst,omitempty" url:"burst,omitempty,int"`
Rate *string `json:"rate,omitempty" url:"rate,omitempty"`
}
type OptionsGetResponseBody struct {
Data *OptionsGetResponseData `json:"data,omitempty"`
}
type OptionsGetResponseData struct {
EBTables *types.CustomBool `json:"ebtables" url:"ebtables, int"`
Enable *types.CustomBool `json:"enable" url:"enable,int"`
LogRateLimit *CustomLogRateLimit `json:"log_ratelimit" url:"log_ratelimit"`
PolicyIn *string `json:"policy_in" url:"policy_in"`
PolicyOut *string `json:"policy_out" url:"policy_out"`
}
// EncodeValues converts a CustomWatchdogDevice struct to a URL vlaue.
func (r *CustomLogRateLimit) EncodeValues(key string, v *url.Values) error {
var values []string
if r.Enable {
values = append(values, "enable=1")
} else {
values = append(values, "enable=0")
}
if r.Burst != nil {
values = append(values, fmt.Sprintf("burst=%d", *r.Burst))
}
if r.Rate != nil {
values = append(values, fmt.Sprintf("rate=%s", *r.Rate))
}
v.Add(key, strings.Join(values, ","))
return nil
}
func (r *CustomLogRateLimit) UnmarshalJSON(b []byte) error {
var s string
err := json.Unmarshal(b, &s)
if err != nil {
return fmt.Errorf("error unmarshaling json: %w", err)
}
if s == "" {
return nil
}
pairs := strings.Split(s, ",")
for _, p := range pairs {
v := strings.Split(strings.TrimSpace(p), "=")
if len(v) == 1 {
r.Enable = v[0] == "1"
} else if len(v) == 2 {
switch v[0] {
case "enable":
r.Enable = v[1] == "1"
case "burst":
iv, err := strconv.Atoi(v[1])
if err != nil {
return fmt.Errorf("error converting burst to int: %w", err)
}
r.Burst = &iv
case "rate":
r.Rate = &v[1]
}
}
}
return nil
}
// SetGlobalOptions sets the global firewall options.
func (c *Client) SetGlobalOptions(ctx context.Context, d *OptionsPutRequestBody) error {
err := c.DoRequest(ctx, http.MethodPut, "cluster/firewall/options", d, nil)
if err != nil {
return fmt.Errorf("error setting optionss: %w", err)
}
return nil
}
// GetGlobalOptions retrieves the global firewall options.
func (c *Client) GetGlobalOptions(ctx context.Context) (*OptionsGetResponseData, error) {
resBody := &OptionsGetResponseBody{}
err := c.DoRequest(ctx, http.MethodGet, "cluster/firewall/options", nil, resBody)
if err != nil {
return nil, fmt.Errorf("error retrieving options: %w", err)
}
if resBody.Data == nil {
return nil, fmt.Errorf("the server did not include a data object in the response")
return nil, api.ErrNoDataObjectInResponse
}
return resBody.Data, nil

View File

@ -0,0 +1,109 @@
/*
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
*/
package firewall
import (
"encoding/json"
"fmt"
"net/url"
"strconv"
"strings"
"github.com/bpg/terraform-provider-proxmox/proxmox/types"
)
// OptionsPutRequestBody is the request body for the PUT /cluster/firewall/options API call.
type OptionsPutRequestBody struct {
EBTables *types.CustomBool `json:"ebtables,omitempty" url:"ebtables,omitempty,int"`
Enable *types.CustomBool `json:"enable,omitempty" url:"enable,omitempty,int"`
LogRateLimit *CustomLogRateLimit `json:"log_ratelimit,omitempty" url:"log_ratelimit,omitempty"`
PolicyIn *string `json:"policy_in,omitempty" url:"policy_in,omitempty"`
PolicyOut *string `json:"policy_out,omitempty" url:"policy_out,omitempty"`
}
// CustomLogRateLimit is a custom type for the log_ratelimit field of the firewall optionss.
type CustomLogRateLimit struct {
Enable types.CustomBool `json:"enable,omitempty" url:"enable,omitempty,int"`
Burst *int `json:"burst,omitempty" url:"burst,omitempty,int"`
Rate *string `json:"rate,omitempty" url:"rate,omitempty"`
}
// OptionsGetResponseBody is the response body for the GET /cluster/firewall/options API call.
type OptionsGetResponseBody struct {
Data *OptionsGetResponseData `json:"data,omitempty"`
}
// OptionsGetResponseData is the data field of the response body for the GET /cluster/firewall/options API call.
type OptionsGetResponseData struct {
EBTables *types.CustomBool `json:"ebtables" url:"ebtables, int"`
Enable *types.CustomBool `json:"enable" url:"enable,int"`
LogRateLimit *CustomLogRateLimit `json:"log_ratelimit" url:"log_ratelimit"`
PolicyIn *string `json:"policy_in" url:"policy_in"`
PolicyOut *string `json:"policy_out" url:"policy_out"`
}
// EncodeValues converts a CustomWatchdogDevice struct to a URL vlaue.
func (r *CustomLogRateLimit) EncodeValues(key string, v *url.Values) error {
var values []string
if r.Enable {
values = append(values, "enable=1")
} else {
values = append(values, "enable=0")
}
if r.Burst != nil {
values = append(values, fmt.Sprintf("burst=%d", *r.Burst))
}
if r.Rate != nil {
values = append(values, fmt.Sprintf("rate=%s", *r.Rate))
}
v.Add(key, strings.Join(values, ","))
return nil
}
// UnmarshalJSON unmarshals a CustomLogRateLimit struct from JSON.
func (r *CustomLogRateLimit) UnmarshalJSON(b []byte) error {
var s string
err := json.Unmarshal(b, &s)
if err != nil {
return fmt.Errorf("error unmarshaling json: %w", err)
}
if s == "" {
return nil
}
pairs := strings.Split(s, ",")
for _, p := range pairs {
v := strings.Split(strings.TrimSpace(p), "=")
if len(v) == 1 {
r.Enable = v[0] == "1"
} else if len(v) == 2 {
switch v[0] {
case "enable":
r.Enable = v[1] == "1"
case "burst":
iv, err := strconv.Atoi(v[1])
if err != nil {
return fmt.Errorf("error converting burst to int: %w", err)
}
r.Burst = &iv
case "rate":
r.Rate = &v[1]
}
}
}
return nil
}

View File

@ -8,13 +8,15 @@ package firewall
import (
"context"
"errors"
"fmt"
"net/http"
"net/url"
"sort"
"github.com/bpg/terraform-provider-proxmox/proxmox/api"
)
// SecurityGroup is an interface for the Proxmox security group API.
type SecurityGroup interface {
CreateGroup(ctx context.Context, d *GroupCreateRequestBody) error
ListGroups(ctx context.Context) ([]*GroupListResponseData, error)
@ -22,34 +24,6 @@ type SecurityGroup interface {
DeleteGroup(ctx context.Context, group string) error
}
// GroupCreateRequestBody contains the data for a security group create request.
type GroupCreateRequestBody struct {
Group string `json:"group" url:"group"`
Comment *string `json:"comment,omitempty" url:"comment,omitempty"`
Digest *string `json:"digest,omitempty" url:"digest,omitempty"`
}
// GroupListResponseData contains the data from a group list response.
type GroupListResponseData struct {
Comment *string `json:"comment,omitempty" url:"comment,omitempty"`
Group string `json:"group" url:"group"`
Digest string `json:"digest" url:"digest"`
}
// GroupListResponseBody contains the data from a group get response.
type GroupListResponseBody struct {
Data []*GroupListResponseData `json:"data,omitempty"`
}
// GroupUpdateRequestBody contains the data for a group update request.
type GroupUpdateRequestBody struct {
Group string `json:"group" url:"group"`
Comment *string `json:"comment,omitempty" url:"comment,omitempty"`
ReName *string `json:"rename,omitempty" url:"rename,omitempty"`
Digest *string `json:"digest,omitempty" url:"digest,omitempty"`
}
func (c *Client) securityGroupsPath() string {
return "cluster/firewall/groups"
}
@ -60,19 +34,21 @@ func (c *Client) CreateGroup(ctx context.Context, d *GroupCreateRequestBody) err
if err != nil {
return fmt.Errorf("error creating security group: %w", err)
}
return nil
}
// ListGroups retrieve list of security groups.
func (c *Client) ListGroups(ctx context.Context) ([]*GroupListResponseData, error) {
resBody := &GroupListResponseBody{}
err := c.DoRequest(ctx, http.MethodGet, c.securityGroupsPath(), nil, resBody)
if err != nil {
return nil, fmt.Errorf("error retrieving security groups: %w", err)
}
if resBody.Data == nil {
return nil, errors.New("the server did not include a data object in the response")
return nil, api.ErrNoDataObjectInResponse
}
sort.Slice(resBody.Data, func(i, j int) bool {
@ -94,6 +70,7 @@ func (c *Client) UpdateGroup(ctx context.Context, d *GroupUpdateRequestBody) err
if err != nil {
return fmt.Errorf("error updating security group: %w", err)
}
return nil
}
@ -109,5 +86,6 @@ func (c *Client) DeleteGroup(ctx context.Context, group string) error {
if err != nil {
return fmt.Errorf("error deleting security group '%s': %w", group, err)
}
return nil
}

View File

@ -0,0 +1,35 @@
/*
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
*/
package firewall
// GroupCreateRequestBody contains the data for a security group create request.
type GroupCreateRequestBody struct {
Group string `json:"group" url:"group"`
Comment *string `json:"comment,omitempty" url:"comment,omitempty"`
Digest *string `json:"digest,omitempty" url:"digest,omitempty"`
}
// GroupListResponseData contains the data from a group list response.
type GroupListResponseData struct {
Comment *string `json:"comment,omitempty" url:"comment,omitempty"`
Group string `json:"group" url:"group"`
Digest string `json:"digest" url:"digest"`
}
// GroupListResponseBody contains the data from a group get response.
type GroupListResponseBody struct {
Data []*GroupListResponseData `json:"data,omitempty"`
}
// GroupUpdateRequestBody contains the data for a group update request.
type GroupUpdateRequestBody struct {
Group string `json:"group" url:"group"`
Comment *string `json:"comment,omitempty" url:"comment,omitempty"`
ReName *string `json:"rename,omitempty" url:"rename,omitempty"`
Digest *string `json:"digest,omitempty" url:"digest,omitempty"`
}

View File

@ -1,410 +0,0 @@
/*
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
*/
package proxmox
import (
"context"
"errors"
"fmt"
"io"
"mime/multipart"
"net/http"
"net/url"
"os"
"path/filepath"
"sort"
"strings"
"github.com/hashicorp/terraform-plugin-log/tflog"
"golang.org/x/crypto/ssh"
"github.com/pkg/sftp"
)
// GetDatastore retrieves information about a datastore.
/*
Using undocumented API endpoints is not recommended, but sometimes it's the only way to get things done.
$ pvesh get /storage/local
key value
content images,vztmpl,iso,backup,snippets,rootdir
digest 5b65ede80f34631d6039e6922845cfa4abc956be
path /var/lib/vz
shared 0
storage local
type dir
*/
func (c *VirtualEnvironmentClient) GetDatastore(
ctx context.Context,
datastoreID string,
) (*DatastoreGetResponseData, error) {
resBody := &DatastoreGetResponseBody{}
err := c.DoRequest(
ctx,
http.MethodGet,
fmt.Sprintf("storage/%s", url.PathEscape(datastoreID)),
nil,
resBody,
)
if err != nil {
return nil, err
}
return resBody.Data, nil
}
// DeleteDatastoreFile deletes a file in a datastore.
func (c *VirtualEnvironmentClient) DeleteDatastoreFile(
ctx context.Context,
nodeName, datastoreID, volumeID string,
) error {
err := c.DoRequest(
ctx,
http.MethodDelete,
fmt.Sprintf(
"nodes/%s/storage/%s/content/%s",
url.PathEscape(nodeName),
url.PathEscape(datastoreID),
url.PathEscape(volumeID),
),
nil,
nil,
)
if err != nil {
return err
}
return nil
}
// GetDatastoreStatus gets status information for a given datastore.
func (c *VirtualEnvironmentClient) GetDatastoreStatus(
ctx context.Context,
nodeName, datastoreID string,
) (*DatastoreGetStatusResponseData, error) {
resBody := &DatastoreGetStatusResponseBody{}
err := c.DoRequest(
ctx,
http.MethodGet,
fmt.Sprintf(
"nodes/%s/storage/%s/status",
url.PathEscape(nodeName),
url.PathEscape(datastoreID),
),
nil,
resBody,
)
if err != nil {
return nil, err
}
if resBody.Data == nil {
return nil, errors.New("the server did not include a data object in the response")
}
return resBody.Data, nil
}
// ListDatastoreFiles retrieves a list of the files in a datastore.
func (c *VirtualEnvironmentClient) ListDatastoreFiles(
ctx context.Context,
nodeName, datastoreID string,
) ([]*DatastoreFileListResponseData, error) {
resBody := &DatastoreFileListResponseBody{}
err := c.DoRequest(
ctx,
http.MethodGet,
fmt.Sprintf(
"nodes/%s/storage/%s/content",
url.PathEscape(nodeName),
url.PathEscape(datastoreID),
),
nil,
resBody,
)
if err != nil {
return nil, err
}
if resBody.Data == nil {
return nil, errors.New("the server did not include a data object in the response")
}
sort.Slice(resBody.Data, func(i, j int) bool {
return resBody.Data[i].VolumeID < resBody.Data[j].VolumeID
})
return resBody.Data, nil
}
// ListDatastores retrieves a list of nodes.
func (c *VirtualEnvironmentClient) ListDatastores(
ctx context.Context,
nodeName string,
d *DatastoreListRequestBody,
) ([]*DatastoreListResponseData, error) {
resBody := &DatastoreListResponseBody{}
err := c.DoRequest(
ctx,
http.MethodGet,
fmt.Sprintf("nodes/%s/storage", url.PathEscape(nodeName)),
d,
resBody,
)
if err != nil {
return nil, err
}
if resBody.Data == nil {
return nil, errors.New("the server did not include a data object in the response")
}
sort.Slice(resBody.Data, func(i, j int) bool {
return resBody.Data[i].ID < resBody.Data[j].ID
})
return resBody.Data, nil
}
// UploadFileToDatastore uploads a file to a datastore.
func (c *VirtualEnvironmentClient) UploadFileToDatastore(
ctx context.Context,
d *DatastoreUploadRequestBody,
) (*DatastoreUploadResponseBody, error) {
switch d.ContentType {
case "iso", "vztmpl":
tflog.Debug(ctx, "uploading file to datastore using PVE API", map[string]interface{}{
"node_name": d.NodeName,
"datastore_id": d.DatastoreID,
"file_name": d.FileName,
"content_type": d.ContentType,
})
r, w := io.Pipe()
defer func(r *io.PipeReader) {
err := r.Close()
if err != nil {
tflog.Error(ctx, "failed to close pipe reader", map[string]interface{}{
"error": err,
})
}
}(r)
m := multipart.NewWriter(w)
go func() {
defer func(w *io.PipeWriter) {
err := w.Close()
if err != nil {
tflog.Error(ctx, "failed to close pipe writer", map[string]interface{}{
"error": err,
})
}
}(w)
defer func(m *multipart.Writer) {
err := m.Close()
if err != nil {
tflog.Error(ctx, "failed to close multipart writer", map[string]interface{}{
"error": err,
})
}
}(m)
err := m.WriteField("content", d.ContentType)
if err != nil {
tflog.Error(ctx, "failed to write 'content' field", map[string]interface{}{
"error": err,
})
return
}
part, err := m.CreateFormFile("filename", d.FileName)
if err != nil {
return
}
_, err = io.Copy(part, d.File)
if err != nil {
return
}
}()
// We need to store the multipart content in a temporary file to avoid using high amounts of memory.
// This is necessary due to Proxmox VE not supporting chunked transfers in v6.1 and earlier versions.
tempMultipartFile, err := os.CreateTemp("", "multipart")
if err != nil {
return nil, fmt.Errorf("failed to create temporary file: %w", err)
}
tempMultipartFileName := tempMultipartFile.Name()
_, err = io.Copy(tempMultipartFile, r)
if err != nil {
return nil, fmt.Errorf("failed to copy multipart data to temporary file: %w", err)
}
err = tempMultipartFile.Close()
if err != nil {
return nil, fmt.Errorf("failed to close temporary file: %w", err)
}
defer func(name string) {
err := os.Remove(name)
if err != nil {
tflog.Error(ctx, "failed to remove temporary file", map[string]interface{}{
"error": err,
})
}
}(tempMultipartFileName)
// Now that the multipart data is stored in a file, we can go ahead and do a HTTP POST request.
fileReader, err := os.Open(tempMultipartFileName)
if err != nil {
return nil, fmt.Errorf("failed to open temporary file: %w", err)
}
defer func(fileReader *os.File) {
err := fileReader.Close()
if err != nil {
tflog.Error(ctx, "failed to close file reader", map[string]interface{}{
"error": err,
})
}
}(fileReader)
fileInfo, err := fileReader.Stat()
if err != nil {
return nil, fmt.Errorf("failed to get file info: %w", err)
}
fileSize := fileInfo.Size()
reqBody := &VirtualEnvironmentMultiPartData{
Boundary: m.Boundary(),
Reader: fileReader,
Size: &fileSize,
}
resBody := &DatastoreUploadResponseBody{}
err = c.DoRequest(
ctx,
http.MethodPost,
fmt.Sprintf(
"nodes/%s/storage/%s/upload",
url.PathEscape(d.NodeName),
url.PathEscape(d.DatastoreID),
),
reqBody,
resBody,
)
if err != nil {
return nil, err
}
return resBody, nil
default:
// We need to upload all other files using SFTP due to API limitations.
// Hopefully, this will not be required in future releases of Proxmox VE.
tflog.Debug(ctx, "uploading file to datastore using SFTP", map[string]interface{}{
"node_name": d.NodeName,
"datastore_id": d.DatastoreID,
"file_name": d.FileName,
"content_type": d.ContentType,
})
fileInfo, err := d.File.Stat()
if err != nil {
return nil, fmt.Errorf("failed to get file info: %w", err)
}
fileSize := fileInfo.Size()
sshClient, err := c.OpenNodeShell(ctx, d.NodeName)
if err != nil {
return nil, err
}
defer func(sshClient *ssh.Client) {
err := sshClient.Close()
if err != nil {
tflog.Error(ctx, "failed to close SSH client", map[string]interface{}{
"error": err,
})
}
}(sshClient)
datastore, err := c.GetDatastore(ctx, d.DatastoreID)
if err != nil {
return nil, fmt.Errorf("failed to get datastore: %w", err)
}
if datastore.Path == nil || *datastore.Path == "" {
return nil, errors.New("failed to determine the datastore path")
}
remoteFileDir := *datastore.Path
if d.ContentType != "" {
remoteFileDir = filepath.Join(remoteFileDir, d.ContentType)
}
remoteFilePath := strings.ReplaceAll(filepath.Join(remoteFileDir, d.FileName), `\`, `/`)
sftpClient, err := sftp.NewClient(sshClient)
if err != nil {
return nil, fmt.Errorf("failed to create SFTP client: %w", err)
}
defer func(sftpClient *sftp.Client) {
err := sftpClient.Close()
if err != nil {
tflog.Error(ctx, "failed to close SFTP client", map[string]interface{}{
"error": err,
})
}
}(sftpClient)
err = sftpClient.MkdirAll(remoteFileDir)
if err != nil {
return nil, fmt.Errorf("failed to create directory %s: %w", remoteFileDir, err)
}
remoteFile, err := sftpClient.Create(remoteFilePath)
if err != nil {
return nil, fmt.Errorf("failed to create file %s: %w", remoteFilePath, err)
}
defer func(remoteFile *sftp.File) {
err := remoteFile.Close()
if err != nil {
tflog.Error(ctx, "failed to close remote file", map[string]interface{}{
"error": err,
})
}
}(remoteFile)
bytesUploaded, err := remoteFile.ReadFrom(d.File)
if err != nil {
return nil, fmt.Errorf("failed to upload file %s: %w", remoteFilePath, err)
}
if bytesUploaded != fileSize {
return nil, fmt.Errorf("failed to upload file %s: uploaded %d bytes, expected %d bytes",
remoteFilePath, bytesUploaded, fileSize)
}
tflog.Debug(ctx, "uploaded file to datastore", map[string]interface{}{
"remote_file_path": remoteFilePath,
"size": bytesUploaded,
})
return &DatastoreUploadResponseBody{}, nil
}
}

View File

@ -8,13 +8,15 @@ package firewall
import (
"context"
"errors"
"fmt"
"net/http"
"net/url"
"sort"
"github.com/bpg/terraform-provider-proxmox/proxmox/api"
)
// Alias is an interface for managing firewall aliases.
type Alias interface {
CreateAlias(ctx context.Context, d *AliasCreateRequestBody) error
DeleteAlias(ctx context.Context, name string) error
@ -23,83 +25,45 @@ type Alias interface {
UpdateAlias(ctx context.Context, name string, d *AliasUpdateRequestBody) error
}
// AliasCreateRequestBody contains the data for an alias create request.
type AliasCreateRequestBody struct {
Comment *string `json:"comment,omitempty" url:"comment,omitempty"`
Name string `json:"name" url:"name"`
CIDR string `json:"cidr" url:"cidr"`
}
// AliasGetResponseBody contains the body from an alias get response.
type AliasGetResponseBody struct {
Data *AliasGetResponseData `json:"data,omitempty"`
}
// AliasGetResponseData contains the data from an alias get response.
type AliasGetResponseData struct {
Comment *string `json:"comment,omitempty" url:"comment,omitempty"`
Name string `json:"name" url:"name"`
CIDR string `json:"cidr" url:"cidr"`
Digest *string `json:"digest" url:"digest"`
IPVersion int `json:"ipversion" url:"ipversion"`
}
// AliasListResponseBody contains the data from an alias get response.
type AliasListResponseBody struct {
Data []*AliasGetResponseData `json:"data,omitempty"`
}
// AliasUpdateRequestBody contains the data for an alias update request.
type AliasUpdateRequestBody struct {
Comment *string `json:"comment,omitempty" url:"comment,omitempty"`
ReName string `json:"rename" url:"rename"`
CIDR string `json:"cidr" url:"cidr"`
}
func (c *Client) aliasesPath() string {
return c.ExpandPath("firewall/aliases")
}
// CreateAlias create an alias
func (c *Client) aliasPath(name string) string {
return fmt.Sprintf("%s/%s", c.aliasesPath(), url.PathEscape(name))
}
// CreateAlias create an alias.
func (c *Client) CreateAlias(ctx context.Context, d *AliasCreateRequestBody) error {
err := c.DoRequest(ctx, http.MethodPost, c.aliasesPath(), d, nil)
if err != nil {
return fmt.Errorf("error creating alias: %w", err)
}
return nil
}
// DeleteAlias delete an alias
// DeleteAlias delete an alias.
func (c *Client) DeleteAlias(ctx context.Context, name string) error {
err := c.DoRequest(
ctx,
http.MethodDelete,
fmt.Sprintf("%s/%s", c.aliasesPath(), url.PathEscape(name)),
nil,
nil,
)
err := c.DoRequest(ctx, http.MethodDelete, c.aliasPath(name), nil, nil)
if err != nil {
return fmt.Errorf("error deleting alias '%s': %w", name, err)
}
return nil
}
// GetAlias retrieves an alias
// GetAlias retrieves an alias.
func (c *Client) GetAlias(ctx context.Context, name string) (*AliasGetResponseData, error) {
resBody := &AliasGetResponseBody{}
err := c.DoRequest(
ctx,
http.MethodGet,
fmt.Sprintf("%s/%s", c.aliasesPath(), url.PathEscape(name)),
nil,
resBody,
)
err := c.DoRequest(ctx, http.MethodGet, c.aliasPath(name), nil, resBody)
if err != nil {
return nil, fmt.Errorf("error retrieving alias '%s': %w", name, err)
}
if resBody.Data == nil {
return nil, errors.New("the server did not include a data object in the response")
return nil, api.ErrNoDataObjectInResponse
}
return resBody.Data, nil
@ -108,13 +72,14 @@ func (c *Client) GetAlias(ctx context.Context, name string) (*AliasGetResponseDa
// ListAliases retrieves a list of aliases.
func (c *Client) ListAliases(ctx context.Context) ([]*AliasGetResponseData, error) {
resBody := &AliasListResponseBody{}
err := c.DoRequest(ctx, http.MethodGet, c.aliasesPath(), nil, resBody)
if err != nil {
return nil, fmt.Errorf("error retrieving aliases: %w", err)
}
if resBody.Data == nil {
return nil, errors.New("the server did not include a data object in the response")
return nil, api.ErrNoDataObjectInResponse
}
sort.Slice(resBody.Data, func(i, j int) bool {
@ -126,15 +91,10 @@ func (c *Client) ListAliases(ctx context.Context) ([]*AliasGetResponseData, erro
// UpdateAlias updates an alias.
func (c *Client) UpdateAlias(ctx context.Context, name string, d *AliasUpdateRequestBody) error {
err := c.DoRequest(
ctx,
http.MethodPut,
fmt.Sprintf("%s/%s", c.aliasesPath(), url.PathEscape(name)),
d,
nil,
)
err := c.DoRequest(ctx, http.MethodPut, c.aliasPath(name), d, nil)
if err != nil {
return fmt.Errorf("error updating alias '%s': %w", name, err)
}
return nil
}

View File

@ -0,0 +1,40 @@
/*
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
*/
package firewall
// AliasCreateRequestBody contains the data for an alias create request.
type AliasCreateRequestBody struct {
Comment *string `json:"comment,omitempty" url:"comment,omitempty"`
Name string `json:"name" url:"name"`
CIDR string `json:"cidr" url:"cidr"`
}
// AliasGetResponseBody contains the body from an alias get response.
type AliasGetResponseBody struct {
Data *AliasGetResponseData `json:"data,omitempty"`
}
// AliasGetResponseData contains the data from an alias get response.
type AliasGetResponseData struct {
Comment *string `json:"comment,omitempty" url:"comment,omitempty"`
Name string `json:"name" url:"name"`
CIDR string `json:"cidr" url:"cidr"`
Digest *string `json:"digest" url:"digest"`
IPVersion int `json:"ipversion" url:"ipversion"`
}
// AliasListResponseBody contains the data from an alias get response.
type AliasListResponseBody struct {
Data []*AliasGetResponseData `json:"data,omitempty"`
}
// AliasUpdateRequestBody contains the data for an alias update request.
type AliasUpdateRequestBody struct {
Comment *string `json:"comment,omitempty" url:"comment,omitempty"`
ReName string `json:"rename" url:"rename"`
CIDR string `json:"cidr" url:"cidr"`
}

View File

@ -6,8 +6,11 @@
package firewall
import "github.com/bpg/terraform-provider-proxmox/proxmox/types"
import (
"github.com/bpg/terraform-provider-proxmox/proxmox/api"
)
// API is an interface for the Proxmox firewall API.
type API interface {
Alias
IPSet
@ -15,6 +18,7 @@ type API interface {
Options
}
// Client is an interface for accessing the Proxmox firewall API.
type Client struct {
types.Client
api.Client
}

View File

@ -12,15 +12,15 @@ package firewall
import (
"context"
"errors"
"fmt"
"net/http"
"net/url"
"sort"
"github.com/bpg/terraform-provider-proxmox/proxmox/types"
"github.com/bpg/terraform-provider-proxmox/proxmox/api"
)
// IPSet is an interface for managing IP sets.
type IPSet interface {
CreateIPSet(ctx context.Context, d *IPSetCreateRequestBody) error
AddCIDRToIPSet(ctx context.Context, id string, d IPSetGetResponseData) error
@ -31,59 +31,21 @@ type IPSet interface {
ListIPSets(ctx context.Context) ([]*IPSetListResponseData, error)
}
// IPSetListResponseBody contains the data from an IPSet get response.
type IPSetListResponseBody struct {
Data []*IPSetListResponseData `json:"data,omitempty"`
}
// IPSetCreateRequestBody contains the data for an IPSet create request
type IPSetCreateRequestBody struct {
Comment string `json:"comment,omitempty" url:"comment,omitempty"`
Name string `json:"name" url:"name"`
}
// IPSetGetResponseBody contains the body from an IPSet get response.
type IPSetGetResponseBody struct {
Data []*IPSetGetResponseData `json:"data,omitempty"`
}
// IPSetGetResponseData contains the data from an IPSet get response.
type IPSetGetResponseData struct {
CIDR string `json:"cidr" url:"cidr"`
NoMatch *types.CustomBool `json:"nomatch,omitempty" url:"nomatch,omitempty,int"`
Comment *string `json:"comment,omitempty" url:"comment,omitempty"`
}
// IPSetUpdateRequestBody contains the data for an IPSet update request.
type IPSetUpdateRequestBody struct {
ReName string `json:"rename,omitempty" url:"rename,omitempty"`
Comment *string `json:"comment,omitempty" url:"comment,omitempty"`
Name string `json:"name" url:"name"`
}
// IPSetListResponseData contains list of IPSets from
type IPSetListResponseData struct {
Comment *string `json:"comment,omitempty" url:"comment,omitempty"`
Name string `json:"name" url:"name"`
}
// IPSetContent is an array of IPSetGetResponseData.
type IPSetContent []IPSetGetResponseData
func (c *Client) ipsetPath() string {
return c.ExpandPath("firewall/ipset")
}
// CreateIPSet create an IPSet
// CreateIPSet create an IPSet.
func (c *Client) CreateIPSet(ctx context.Context, d *IPSetCreateRequestBody) error {
err := c.DoRequest(ctx, http.MethodPost, c.ipsetPath(), d, nil)
if err != nil {
return fmt.Errorf("error creating IPSet: %w", err)
}
return nil
}
// AddCIDRToIPSet adds IP or Network to IPSet
// AddCIDRToIPSet adds IP or Network to IPSet.
func (c *Client) AddCIDRToIPSet(ctx context.Context, id string, d IPSetGetResponseData) error {
err := c.DoRequest(
ctx,
@ -95,6 +57,7 @@ func (c *Client) AddCIDRToIPSet(ctx context.Context, id string, d IPSetGetRespon
if err != nil {
return fmt.Errorf("error adding CIDR to IPSet: %w", err)
}
return nil
}
@ -104,10 +67,11 @@ func (c *Client) UpdateIPSet(ctx context.Context, d *IPSetUpdateRequestBody) err
if err != nil {
return fmt.Errorf("error updating IPSet: %w", err)
}
return nil
}
// DeleteIPSet delete an IPSet
// DeleteIPSet delete an IPSet.
func (c *Client) DeleteIPSet(ctx context.Context, id string) error {
err := c.DoRequest(
ctx,
@ -119,6 +83,7 @@ func (c *Client) DeleteIPSet(ctx context.Context, id string) error {
if err != nil {
return fmt.Errorf("error deleting IPSet %s: %w", id, err)
}
return nil
}
@ -134,12 +99,14 @@ func (c *Client) DeleteIPSetContent(ctx context.Context, id string, cidr string)
if err != nil {
return fmt.Errorf("error deleting IPSet content %s: %w", id, err)
}
return nil
}
// GetIPSetContent retrieve a list of IPSet content
// GetIPSetContent retrieve a list of IPSet content.
func (c *Client) GetIPSetContent(ctx context.Context, id string) ([]*IPSetGetResponseData, error) {
resBody := &IPSetGetResponseBody{}
err := c.DoRequest(
ctx,
http.MethodGet,
@ -152,7 +119,7 @@ func (c *Client) GetIPSetContent(ctx context.Context, id string) ([]*IPSetGetRes
}
if resBody.Data == nil {
return nil, errors.New("the server did not include a data object in the response")
return nil, api.ErrNoDataObjectInResponse
}
return resBody.Data, nil
@ -161,13 +128,14 @@ func (c *Client) GetIPSetContent(ctx context.Context, id string) ([]*IPSetGetRes
// ListIPSets retrieves list of IPSets.
func (c *Client) ListIPSets(ctx context.Context) ([]*IPSetListResponseData, error) {
resBody := &IPSetListResponseBody{}
err := c.DoRequest(ctx, http.MethodGet, c.ipsetPath(), nil, resBody)
if err != nil {
return nil, fmt.Errorf("error getting IPSet list: %w", err)
}
if resBody.Data == nil {
return nil, errors.New("the server did not include a data object in the response")
return nil, api.ErrNoDataObjectInResponse
}
sort.Slice(resBody.Data, func(i, j int) bool {

View File

@ -0,0 +1,48 @@
/*
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
*/
package firewall
import "github.com/bpg/terraform-provider-proxmox/proxmox/types"
// IPSetListResponseBody contains the data from an IPSet get response.
type IPSetListResponseBody struct {
Data []*IPSetListResponseData `json:"data,omitempty"`
}
// IPSetCreateRequestBody contains the data for an IPSet create request.
type IPSetCreateRequestBody struct {
Comment string `json:"comment,omitempty" url:"comment,omitempty"`
Name string `json:"name" url:"name"`
}
// IPSetGetResponseBody contains the body from an IPSet get response.
type IPSetGetResponseBody struct {
Data []*IPSetGetResponseData `json:"data,omitempty"`
}
// IPSetGetResponseData contains the data from an IPSet get response.
type IPSetGetResponseData struct {
CIDR string `json:"cidr" url:"cidr"`
NoMatch *types.CustomBool `json:"nomatch,omitempty" url:"nomatch,omitempty,int"`
Comment *string `json:"comment,omitempty" url:"comment,omitempty"`
}
// IPSetUpdateRequestBody contains the data for an IPSet update request.
type IPSetUpdateRequestBody struct {
ReName string `json:"rename,omitempty" url:"rename,omitempty"`
Comment *string `json:"comment,omitempty" url:"comment,omitempty"`
Name string `json:"name" url:"name"`
}
// IPSetListResponseData contains list of IPSets from.
type IPSetListResponseData struct {
Comment *string `json:"comment,omitempty" url:"comment,omitempty"`
Name string `json:"name" url:"name"`
}
// IPSetContent is an array of IPSetGetResponseData.
type IPSetContent []IPSetGetResponseData

View File

@ -14,70 +14,46 @@ import (
"github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema"
"github.com/bpg/terraform-provider-proxmox/proxmox/types"
"github.com/bpg/terraform-provider-proxmox/proxmox/api"
)
// Options is an interface for the Proxmox firewall options API.
type Options interface {
GetOptionsID() string
SetOptions(ctx context.Context, d *OptionsPutRequestBody) error
GetOptions(ctx context.Context) (*OptionsGetResponseData, error)
}
type OptionsPutRequestBody struct {
DHCP *types.CustomBool `json:"dhcp,omitempty" url:"dhcp,omitempty,int"`
Enable *types.CustomBool `json:"enable,omitempty" url:"enable,omitempty,int"`
IPFilter *types.CustomBool `json:"ipfilter,omitempty" url:"ipfilter,omitempty,int"`
LogLevelIN *string `json:"log_level_in,omitempty" url:"log_level_in,omitempty"`
LogLevelOUT *string `json:"log_level_out,omitempty" url:"log_level_out,omitempty"`
MACFilter *types.CustomBool `json:"macfilter,omitempty" url:"macfilter,omitempty,int"`
NDP *types.CustomBool `json:"ndp,omitempty" url:"ndp,omitempty,int"`
PolicyIn *string `json:"policy_in,omitempty" url:"policy_in,omitempty"`
PolicyOut *string `json:"policy_out,omitempty" url:"policy_out,omitempty"`
RAdv *types.CustomBool `json:"radv,omitempty" url:"radv,omitempty,int"`
}
type OptionsGetResponseBody struct {
Data *OptionsGetResponseData `json:"data,omitempty"`
}
type OptionsGetResponseData struct {
DHCP *types.CustomBool `json:"dhcp" url:"dhcp,int"`
Enable *types.CustomBool `json:"enable" url:"enable,int"`
IPFilter *types.CustomBool `json:"ipfilter" url:"ipfilter,int"`
LogLevelIN *string `json:"log_level_in" url:"log_level_in"`
LogLevelOUT *string `json:"log_level_out" url:"log_level_out"`
MACFilter *types.CustomBool `json:"macfilter" url:"macfilter,int"`
NDP *types.CustomBool `json:"ndp" url:"ndp,int"`
PolicyIn *string `json:"policy_in" url:"policy_in"`
PolicyOut *string `json:"policy_out" url:"policy_out"`
RAdv *types.CustomBool `json:"radv" url:"radv,int"`
}
func (c *Client) optionsPath() string {
return c.ExpandPath("firewall/options")
}
// GetOptionsID returns the ID of the options object.
func (c *Client) GetOptionsID() string {
return "options-" + strconv.Itoa(schema.HashString(c.optionsPath()))
}
// SetOptions sets the options object.
func (c *Client) SetOptions(ctx context.Context, d *OptionsPutRequestBody) error {
err := c.DoRequest(ctx, http.MethodPut, c.optionsPath(), d, nil)
if err != nil {
return fmt.Errorf("error setting optionss: %w", err)
}
return nil
}
// GetOptions retrieves the options object.
func (c *Client) GetOptions(ctx context.Context) (*OptionsGetResponseData, error) {
resBody := &OptionsGetResponseBody{}
err := c.DoRequest(ctx, http.MethodGet, c.optionsPath(), nil, resBody)
if err != nil {
return nil, fmt.Errorf("error retrieving options: %w", err)
}
if resBody.Data == nil {
return nil, fmt.Errorf("the server did not include a data object in the response")
return nil, api.ErrNoDataObjectInResponse
}
return resBody.Data, nil

View File

@ -0,0 +1,42 @@
/*
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
*/
package firewall
import "github.com/bpg/terraform-provider-proxmox/proxmox/types"
// OptionsPutRequestBody is the request body for the PUT /cluster/firewall/options API call.
type OptionsPutRequestBody struct {
DHCP *types.CustomBool `json:"dhcp,omitempty" url:"dhcp,omitempty,int"`
Enable *types.CustomBool `json:"enable,omitempty" url:"enable,omitempty,int"`
IPFilter *types.CustomBool `json:"ipfilter,omitempty" url:"ipfilter,omitempty,int"`
LogLevelIN *string `json:"log_level_in,omitempty" url:"log_level_in,omitempty"`
LogLevelOUT *string `json:"log_level_out,omitempty" url:"log_level_out,omitempty"`
MACFilter *types.CustomBool `json:"macfilter,omitempty" url:"macfilter,omitempty,int"`
NDP *types.CustomBool `json:"ndp,omitempty" url:"ndp,omitempty,int"`
PolicyIn *string `json:"policy_in,omitempty" url:"policy_in,omitempty"`
PolicyOut *string `json:"policy_out,omitempty" url:"policy_out,omitempty"`
RAdv *types.CustomBool `json:"radv,omitempty" url:"radv,omitempty,int"`
}
// OptionsGetResponseBody is the response body for the GET /cluster/firewall/options API call.
type OptionsGetResponseBody struct {
Data *OptionsGetResponseData `json:"data,omitempty"`
}
// OptionsGetResponseData is the data field of the response body for the GET /cluster/firewall/options API call.
type OptionsGetResponseData struct {
DHCP *types.CustomBool `json:"dhcp" url:"dhcp,int"`
Enable *types.CustomBool `json:"enable" url:"enable,int"`
IPFilter *types.CustomBool `json:"ipfilter" url:"ipfilter,int"`
LogLevelIN *string `json:"log_level_in" url:"log_level_in"`
LogLevelOUT *string `json:"log_level_out" url:"log_level_out"`
MACFilter *types.CustomBool `json:"macfilter" url:"macfilter,int"`
NDP *types.CustomBool `json:"ndp" url:"ndp,int"`
PolicyIn *string `json:"policy_in" url:"policy_in"`
PolicyOut *string `json:"policy_out" url:"policy_out"`
RAdv *types.CustomBool `json:"radv" url:"radv,int"`
}

View File

@ -8,16 +8,17 @@ package firewall
import (
"context"
"errors"
"fmt"
"net/http"
"strconv"
"github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema"
"github.com/bpg/terraform-provider-proxmox/proxmox/api"
"github.com/bpg/terraform-provider-proxmox/proxmox/types"
)
// Rule is an interface for the Proxmox firewall rule API.
type Rule interface {
GetRulesID() string
CreateRule(ctx context.Context, d *RuleCreateRequestBody) error
@ -27,52 +28,7 @@ type Rule interface {
DeleteRule(ctx context.Context, pos int) error
}
// RuleCreateRequestBody contains the data for a firewall rule create request.
type RuleCreateRequestBody struct {
BaseRule
Action string `json:"action" url:"action"`
Type string `json:"type" url:"type"`
Group *string `json:"group,omitempty" url:"group,omitempty"`
}
// RuleGetResponseBody contains the body from a firewall rule get response.
type RuleGetResponseBody struct {
Data *RuleGetResponseData `json:"data,omitempty"`
}
// RuleGetResponseData contains the data from a firewall rule get response.
type RuleGetResponseData struct {
BaseRule
// NOTE: This is `int` in the PVE API docs, but it's actually a string in the response.
Pos string `json:"pos" url:"pos"`
Action string `json:"action" url:"action"`
Type string `json:"type" url:"type"`
}
// RuleListResponseBody contains the data from a firewall rule get response.
type RuleListResponseBody struct {
Data []*RuleListResponseData `json:"data,omitempty"`
}
// RuleListResponseData contains the data from a firewall rule get response.
type RuleListResponseData struct {
Pos int `json:"pos" url:"pos"`
}
// RuleUpdateRequestBody contains the data for a firewall rule update request.
type RuleUpdateRequestBody struct {
BaseRule
Pos *int `json:"pos,omitempty" url:"pos,omitempty"`
Action *string `json:"action,omitempty" url:"action,omitempty"`
Type *string `json:"type,omitempty" url:"type,omitempty"`
Group *string `json:"group,omitempty" url:"group,omitempty"`
}
// BaseRule is the base struct for firewall rules.
type BaseRule struct {
Comment *string `json:"comment,omitempty" url:"comment,omitempty"`
Dest *string `json:"dest,omitempty" url:"dest,omitempty"`
@ -92,6 +48,11 @@ func (c *Client) rulesPath() string {
return c.ExpandPath("firewall/rules")
}
func (c *Client) rulePath(pos int) string {
return fmt.Sprintf("%s/%d", c.rulesPath(), pos)
}
// GetRulesID returns the ID of the rules object.
func (c *Client) GetRulesID() string {
return "rule-" + strconv.Itoa(schema.HashString(c.rulesPath()))
}
@ -102,25 +63,21 @@ func (c *Client) CreateRule(ctx context.Context, d *RuleCreateRequestBody) error
if err != nil {
return fmt.Errorf("error creating firewall rule: %w", err)
}
return nil
}
// GetRule retrieves a firewall rule.
func (c *Client) GetRule(ctx context.Context, pos int) (*RuleGetResponseData, error) {
resBody := &RuleGetResponseBody{}
err := c.DoRequest(
ctx,
http.MethodGet,
fmt.Sprintf("%s/%d", c.rulesPath(), pos),
nil,
resBody,
)
err := c.DoRequest(ctx, http.MethodGet, c.rulePath(pos), nil, resBody)
if err != nil {
return nil, fmt.Errorf("error retrieving firewall rule %d: %w", pos, err)
}
if resBody.Data == nil {
return nil, errors.New("the server did not include a data object in the response")
return nil, api.ErrNoDataObjectInResponse
}
return resBody.Data, nil
@ -129,13 +86,14 @@ func (c *Client) GetRule(ctx context.Context, pos int) (*RuleGetResponseData, er
// ListRules retrieves a list of firewall rules.
func (c *Client) ListRules(ctx context.Context) ([]*RuleListResponseData, error) {
resBody := &RuleListResponseBody{}
err := c.DoRequest(ctx, http.MethodGet, c.rulesPath(), nil, resBody)
if err != nil {
return nil, fmt.Errorf("error retrieving firewall rules: %w", err)
}
if resBody.Data == nil {
return nil, errors.New("the server did not include a data object in the response")
return nil, api.ErrNoDataObjectInResponse
}
return resBody.Data, nil
@ -143,30 +101,20 @@ func (c *Client) ListRules(ctx context.Context) ([]*RuleListResponseData, error)
// UpdateRule updates a firewall rule.
func (c *Client) UpdateRule(ctx context.Context, pos int, d *RuleUpdateRequestBody) error {
err := c.DoRequest(
ctx,
http.MethodPut,
fmt.Sprintf("%s/%d", c.rulesPath(), pos),
d,
nil,
)
err := c.DoRequest(ctx, http.MethodPut, c.rulePath(pos), d, nil)
if err != nil {
return fmt.Errorf("error updating firewall rule %d: %w", pos, err)
}
return nil
}
// DeleteRule deletes a firewall rule.
func (c *Client) DeleteRule(ctx context.Context, pos int) error {
err := c.DoRequest(
ctx,
http.MethodDelete,
fmt.Sprintf("%s/%d", c.rulesPath(), pos),
nil,
nil,
)
err := c.DoRequest(ctx, http.MethodDelete, c.rulePath(pos), nil, nil)
if err != nil {
return fmt.Errorf("error deleting firewall rule %d: %w", pos, err)
}
return nil
}

View File

@ -0,0 +1,53 @@
/*
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
*/
package firewall
// RuleCreateRequestBody contains the data for a firewall rule create request.
type RuleCreateRequestBody struct {
BaseRule
Action string `json:"action" url:"action"`
Type string `json:"type" url:"type"`
Group *string `json:"group,omitempty" url:"group,omitempty"`
}
// RuleGetResponseBody contains the body from a firewall rule get response.
type RuleGetResponseBody struct {
Data *RuleGetResponseData `json:"data,omitempty"`
}
// RuleGetResponseData contains the data from a firewall rule get response.
type RuleGetResponseData struct {
BaseRule
// NOTE: This is `int` in the PVE API docs, but it's actually a string in the response.
Pos string `json:"pos" url:"pos"`
Action string `json:"action" url:"action"`
Type string `json:"type" url:"type"`
}
// RuleListResponseBody contains the data from a firewall rule get response.
type RuleListResponseBody struct {
Data []*RuleListResponseData `json:"data,omitempty"`
}
// RuleListResponseData contains the data from a firewall rule get response.
type RuleListResponseData struct {
Pos int `json:"pos" url:"pos"`
}
// RuleUpdateRequestBody contains the data for a firewall rule update request.
type RuleUpdateRequestBody struct {
BaseRule
Pos *int `json:"pos,omitempty" url:"pos,omitempty"`
Action *string `json:"action,omitempty" url:"action,omitempty"`
Type *string `json:"type,omitempty" url:"type,omitempty"`
Group *string `json:"group,omitempty" url:"group,omitempty"`
}

View File

@ -0,0 +1,51 @@
/*
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
*/
package nodes
import (
"context"
"fmt"
"net/http"
"github.com/bpg/terraform-provider-proxmox/proxmox/api"
)
// DeleteCertificate deletes the custom certificate for a node.
func (c *Client) DeleteCertificate(ctx context.Context, d *CertificateDeleteRequestBody) error {
err := c.DoRequest(ctx, http.MethodDelete, c.ExpandPath("certificates/custom"), d, nil)
if err != nil {
return fmt.Errorf("error deleting certificate: %w", err)
}
return nil
}
// ListCertificates retrieves the list of certificates for a node.
func (c *Client) ListCertificates(ctx context.Context) (*[]CertificateListResponseData, error) {
resBody := &CertificateListResponseBody{}
err := c.DoRequest(ctx, http.MethodGet, c.ExpandPath("certificates/info"), nil, resBody)
if err != nil {
return nil, fmt.Errorf("error retrieving certificate list: %w", err)
}
if resBody.Data == nil {
return nil, api.ErrNoDataObjectInResponse
}
return resBody.Data, nil
}
// UpdateCertificate updates the custom certificate for a node.
func (c *Client) UpdateCertificate(ctx context.Context, d *CertificateUpdateRequestBody) error {
err := c.DoRequest(ctx, http.MethodPost, c.ExpandPath("certificates/custom"), d, nil)
if err != nil {
return fmt.Errorf("error updating certificate: %w", err)
}
return nil
}

View File

@ -1,23 +1,25 @@
/* This Source Code Form is subject to the terms of the Mozilla Public
/*
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at https://mozilla.org/MPL/2.0/. */
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
*/
package proxmox
package nodes
import "github.com/bpg/terraform-provider-proxmox/proxmox/types"
// VirtualEnvironmentCertificateDeleteRequestBody contains the data for a custom certificate delete request.
type VirtualEnvironmentCertificateDeleteRequestBody struct {
// CertificateDeleteRequestBody contains the data for a custom certificate delete request.
type CertificateDeleteRequestBody struct {
Restart *types.CustomBool `json:"restart,omitempty" url:"restart,omitempty,int"`
}
// VirtualEnvironmentCertificateListResponseBody contains the body from a certificate list response.
type VirtualEnvironmentCertificateListResponseBody struct {
Data *[]VirtualEnvironmentCertificateListResponseData `json:"data,omitempty"`
// CertificateListResponseBody contains the body from a certificate list response.
type CertificateListResponseBody struct {
Data *[]CertificateListResponseData `json:"data,omitempty"`
}
// VirtualEnvironmentCertificateListResponseData contains the data from a certificate list response.
type VirtualEnvironmentCertificateListResponseData struct {
// CertificateListResponseData contains the data from a certificate list response.
type CertificateListResponseData struct {
Certificates *string `json:"pem,omitempty"`
FileName *string `json:"filename,omitempty"`
Fingerprint *string `json:"fingerprint,omitempty"`
@ -30,8 +32,8 @@ type VirtualEnvironmentCertificateListResponseData struct {
SubjectAlternativeNames *[]string `json:"san,omitempty"`
}
// VirtualEnvironmentCertificateUpdateRequestBody contains the body for a custom certificate update request.
type VirtualEnvironmentCertificateUpdateRequestBody struct {
// CertificateUpdateRequestBody contains the body for a custom certificate update request.
type CertificateUpdateRequestBody struct {
Certificates string `json:"certificates" url:"certificates"`
Force *types.CustomBool `json:"force,omitempty" url:"force,omitempty,int"`
PrivateKey *string `json:"key,omitempty" url:"key,omitempty"`

43
proxmox/nodes/client.go Normal file
View File

@ -0,0 +1,43 @@
/*
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
*/
package nodes
import (
"fmt"
"net/url"
"github.com/bpg/terraform-provider-proxmox/proxmox/api"
"github.com/bpg/terraform-provider-proxmox/proxmox/nodes/containers"
"github.com/bpg/terraform-provider-proxmox/proxmox/nodes/vms"
)
// Client is an interface for accessing the Proxmox node API.
type Client struct {
api.Client
NodeName string
}
// ExpandPath expands a relative path to a full node API path.
func (c *Client) ExpandPath(path string) string {
return fmt.Sprintf("nodes/%s/%s", url.PathEscape(c.NodeName), path)
}
// Container returns a client for managing a specific container.
func (c *Client) Container(vmID int) *containers.Client {
return &containers.Client{
Client: c,
VMID: vmID,
}
}
// VM returns a client for managing a specific VM.
func (c *Client) VM(vmID int) *vms.Client {
return &vms.Client{
Client: c,
VMID: vmID,
}
}

View File

@ -4,27 +4,37 @@
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
*/
package container
package containers
import (
"fmt"
"net/url"
containerfirewall "github.com/bpg/terraform-provider-proxmox/proxmox/container/firewall"
"github.com/bpg/terraform-provider-proxmox/proxmox/api"
"github.com/bpg/terraform-provider-proxmox/proxmox/firewall"
"github.com/bpg/terraform-provider-proxmox/proxmox/types"
containerfirewall "github.com/bpg/terraform-provider-proxmox/proxmox/nodes/containers/firewall"
)
// Client is an interface for accessing the Proxmox container API.
type Client struct {
types.Client
NodeName string
VMID int
api.Client
VMID int
}
func (c *Client) basePath() string {
return c.Client.ExpandPath("lxc")
}
// ExpandPath expands a relative path to a full container API path.
func (c *Client) ExpandPath(path string) string {
return fmt.Sprintf("nodes/%s/lxc/%d/%s", url.PathEscape(c.NodeName), c.VMID, path)
ep := fmt.Sprintf("%s/%d", c.basePath(), c.VMID)
if path != "" {
ep = fmt.Sprintf("%s/%s", ep, path)
}
return ep
}
// Firewall returns a client for managing the container firewall.
func (c *Client) Firewall() firewall.API {
return &containerfirewall.Client{
Client: firewall.Client{Client: c},

View File

@ -0,0 +1,202 @@
/*
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
*/
package containers
import (
"context"
"fmt"
"net/http"
"strings"
"time"
"github.com/bpg/terraform-provider-proxmox/proxmox/api"
)
// CloneContainer clones a container.
func (c *Client) CloneContainer(ctx context.Context, d *CloneRequestBody) error {
err := c.DoRequest(ctx, http.MethodPost, c.ExpandPath("/clone"), d, nil)
if err != nil {
return fmt.Errorf("error cloning container: %w", err)
}
return nil
}
// CreateContainer creates a container.
func (c *Client) CreateContainer(ctx context.Context, d *CreateRequestBody) error {
err := c.DoRequest(ctx, http.MethodPost, c.basePath(), d, nil)
if err != nil {
return fmt.Errorf("error creating container: %w", err)
}
return nil
}
// DeleteContainer deletes a container.
func (c *Client) DeleteContainer(ctx context.Context) error {
err := c.DoRequest(ctx, http.MethodDelete, c.ExpandPath(""), nil, nil)
if err != nil {
return fmt.Errorf("error deleting container: %w", err)
}
return nil
}
// GetContainer retrieves a container.
func (c *Client) GetContainer(ctx context.Context) (*GetResponseData, error) {
resBody := &GetResponseBody{}
err := c.DoRequest(ctx, http.MethodGet, c.ExpandPath("config"), nil, resBody)
if err != nil {
return nil, fmt.Errorf("error retrieving container: %w", err)
}
if resBody.Data == nil {
return nil, api.ErrNoDataObjectInResponse
}
return resBody.Data, nil
}
// GetContainerStatus retrieves the status for a container.
func (c *Client) GetContainerStatus(ctx context.Context) (*GetStatusResponseData, error) {
resBody := &GetStatusResponseBody{}
err := c.DoRequest(ctx, http.MethodGet, c.ExpandPath("status/current"), nil, resBody)
if err != nil {
return nil, fmt.Errorf("error retrieving container status: %w", err)
}
if resBody.Data == nil {
return nil, api.ErrNoDataObjectInResponse
}
return resBody.Data, nil
}
// RebootContainer reboots a container.
func (c *Client) RebootContainer(ctx context.Context, d *RebootRequestBody) error {
err := c.DoRequest(ctx, http.MethodPost, c.ExpandPath("status/reboot"), d, nil)
if err != nil {
return fmt.Errorf("error rebooting container: %w", err)
}
return nil
}
// ShutdownContainer shuts down a container.
func (c *Client) ShutdownContainer(ctx context.Context, d *ShutdownRequestBody) error {
err := c.DoRequest(ctx, http.MethodPost, c.ExpandPath("status/shutdown"), d, nil)
if err != nil {
return fmt.Errorf("error shutting down container: %w", err)
}
return nil
}
// StartContainer starts a container.
func (c *Client) StartContainer(ctx context.Context) error {
err := c.DoRequest(ctx, http.MethodPost, c.ExpandPath("status/start"), nil, nil)
if err != nil {
return fmt.Errorf("error starting container: %w", err)
}
return nil
}
// StopContainer stops a container immediately.
func (c *Client) StopContainer(ctx context.Context) error {
err := c.DoRequest(ctx, http.MethodPost, c.ExpandPath("status/stop"), nil, nil)
if err != nil {
return fmt.Errorf("error stopping container: %w", err)
}
return nil
}
// UpdateContainer updates a container.
func (c *Client) UpdateContainer(ctx context.Context, d *UpdateRequestBody) error {
err := c.DoRequest(ctx, http.MethodPut, c.ExpandPath("config"), d, nil)
if err != nil {
return fmt.Errorf("error updating container: %w", err)
}
return nil
}
// WaitForContainerState waits for a container to reach a specific state.
func (c *Client) WaitForContainerState(ctx context.Context, state string, timeout int, delay int) error {
state = strings.ToLower(state)
timeDelay := int64(delay)
timeMax := float64(timeout)
timeStart := time.Now()
timeElapsed := timeStart.Sub(timeStart)
for timeElapsed.Seconds() < timeMax {
if int64(timeElapsed.Seconds())%timeDelay == 0 {
data, err := c.GetContainerStatus(ctx)
if err != nil {
return fmt.Errorf("error retrieving container status: %w", err)
}
if data.Status == state {
return nil
}
time.Sleep(1 * time.Second)
}
time.Sleep(200 * time.Millisecond)
timeElapsed = time.Since(timeStart)
if ctx.Err() != nil {
return fmt.Errorf("context error: %w", ctx.Err())
}
}
return fmt.Errorf(
"timeout while waiting for container \"%d\" to enter the state \"%s\"",
c.VMID,
state,
)
}
// WaitForContainerLock waits for a container lock to be released.
func (c *Client) WaitForContainerLock(ctx context.Context, timeout int, delay int, ignoreErrorResponse bool) error {
timeDelay := int64(delay)
timeMax := float64(timeout)
timeStart := time.Now()
timeElapsed := timeStart.Sub(timeStart)
for timeElapsed.Seconds() < timeMax {
if int64(timeElapsed.Seconds())%timeDelay == 0 {
data, err := c.GetContainerStatus(ctx)
if err != nil {
if !ignoreErrorResponse {
return fmt.Errorf("error retrieving container status: %w", err)
}
} else if data.Lock == nil || *data.Lock == "" {
return nil
}
time.Sleep(1 * time.Second)
}
time.Sleep(200 * time.Millisecond)
timeElapsed = time.Since(timeStart)
if ctx.Err() != nil {
return fmt.Errorf("context error: %w", ctx.Err())
}
}
return fmt.Errorf("timeout while waiting for container \"%d\" to become unlocked", c.VMID)
}

View File

@ -0,0 +1,811 @@
/*
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
*/
package containers
import (
"encoding/json"
"fmt"
"net/url"
"strconv"
"strings"
"github.com/bpg/terraform-provider-proxmox/proxmox/types"
)
// CloneRequestBody contains the data for an container clone request.
type CloneRequestBody struct {
BandwidthLimit *int `json:"bwlimit,omitempty" url:"bwlimit,omitempty"`
Description *string `json:"description,omitempty" url:"description,omitempty"`
FullCopy *types.CustomBool `json:"full,omitempty" url:"full,omitempty,int"`
Hostname *string `json:"hostname,omitempty" url:"hostname,omitempty"`
PoolID *string `json:"pool,omitempty" url:"pool,omitempty"`
SnapshotName *string `json:"snapname,omitempty" url:"snapname,omitempty"`
TargetNodeName *string `json:"target,omitempty" url:"target,omitempty"`
TargetStorage *string `json:"storage,omitempty" url:"storage,omitempty"`
VMIDNew int `json:"newid" url:"newid"`
}
// CreateRequestBody contains the data for a user create request.
type CreateRequestBody struct {
BandwidthLimit *float64 `json:"bwlimit,omitempty" url:"bwlimit,omitempty"`
ConsoleEnabled *types.CustomBool `json:"console,omitempty" url:"console,omitempty,int"`
ConsoleMode *string `json:"cmode,omitempty" url:"cmode,omitempty"`
CPUArchitecture *string `json:"arch,omitempty" url:"arch,omitempty"`
CPUCores *int `json:"cores,omitempty" url:"cores,omitempty"`
CPULimit *int `json:"cpulimit,omitempty" url:"cpulimit,omitempty"`
CPUUnits *int `json:"cpuunits,omitempty" url:"cpuunits,omitempty"`
DatastoreID *string `json:"storage,omitempty" url:"storage,omitempty"`
DedicatedMemory *int `json:"memory,omitempty" url:"memory,omitempty"`
Delete []string `json:"delete,omitempty" url:"delete,omitempty"`
Description *string `json:"description,omitempty" url:"description,omitempty"`
DNSDomain *string `json:"searchdomain,omitempty" url:"searchdomain,omitempty"`
DNSServer *string `json:"nameserver,omitempty" url:"nameserver,omitempty"`
Features *CustomFeatures `json:"features,omitempty" url:"features,omitempty"`
Force *types.CustomBool `json:"force,omitempty" url:"force,omitempty,int"`
HookScript *string `json:"hookscript,omitempty" url:"hookscript,omitempty"`
Hostname *string `json:"hostname,omitempty" url:"hostname,omitempty"`
IgnoreUnpackErrors *types.CustomBool `json:"ignore-unpack-errors,omitempty" url:"force,omitempty,int"`
Lock *string `json:"lock,omitempty" url:"lock,omitempty,int"`
MountPoints CustomMountPointArray `json:"mp,omitempty" url:"mp,omitempty,numbered"`
NetworkInterfaces CustomNetworkInterfaceArray `json:"net,omitempty" url:"net,omitempty,numbered"`
OSTemplateFileVolume *string `json:"ostemplate,omitempty" url:"ostemplate,omitempty"`
OSType *string `json:"ostype,omitempty" url:"ostype,omitempty"`
Password *string `json:"password,omitempty" url:"password,omitempty"`
PoolID *string `json:"pool,omitempty" url:"pool,omitempty"`
Protection *types.CustomBool `json:"protection,omitempty" url:"protection,omitempty,int"`
Restore *types.CustomBool `json:"restore,omitempty" url:"restore,omitempty,int"`
RootFS *CustomRootFS `json:"rootfs,omitempty" url:"rootfs,omitempty"`
SSHKeys *CustomSSHKeys `json:"ssh-public-keys,omitempty" url:"ssh-public-keys,omitempty"`
Start *types.CustomBool `json:"start,omitempty" url:"start,omitempty,int"`
StartOnBoot *types.CustomBool `json:"onboot,omitempty" url:"onboot,omitempty,int"`
StartupBehavior *CustomStartupBehavior `json:"startup,omitempty" url:"startup,omitempty"`
Swap *int `json:"swap,omitempty" url:"swap,omitempty"`
Tags *string `json:"tags,omitempty" url:"tags,omitempty"`
Template *types.CustomBool `json:"template,omitempty" url:"template,omitempty,int"`
TTY *int `json:"tty,omitempty" url:"tty,omitempty"`
Unique *types.CustomBool `json:"unique,omitempty" url:"unique,omitempty,int"`
Unprivileged *types.CustomBool `json:"unprivileged,omitempty" url:"unprivileged,omitempty,int"`
VMID *int `json:"vmid,omitempty" url:"vmid,omitempty"`
}
// CustomFeatures contains the values for the "features" property.
type CustomFeatures struct {
FUSE *types.CustomBool `json:"fuse,omitempty" url:"fuse,omitempty,int"`
KeyControl *types.CustomBool `json:"keyctl,omitempty" url:"keyctl,omitempty,int"`
MountTypes *[]string `json:"mount,omitempty" url:"mount,omitempty"`
Nesting *types.CustomBool `json:"nesting,omitempty" url:"nesting,omitempty,int"`
}
// CustomMountPoint contains the values for the "mp[n]" properties.
type CustomMountPoint struct {
ACL *types.CustomBool `json:"acl,omitempty" url:"acl,omitempty,int"`
Backup *types.CustomBool `json:"backup,omitempty" url:"backup,omitempty,int"`
DiskSize *string `json:"size,omitempty" url:"size,omitempty"`
Enabled bool `json:"-" url:"-"`
MountOptions *[]string `json:"mountoptions,omitempty" url:"mountoptions,omitempty"`
MountPoint string `json:"mp" url:"mp"`
Quota *types.CustomBool `json:"quota,omitempty" url:"quota,omitempty,int"`
ReadOnly *types.CustomBool `json:"ro,omitempty" url:"ro,omitempty,int"`
Replicate *types.CustomBool `json:"replicate,omitempty" url:"replicate,omitempty,int"`
Shared *types.CustomBool `json:"shared,omitempty" url:"shared,omitempty,int"`
Volume string `json:"volume" url:"volume"`
}
// CustomMountPointArray is an array of CustomMountPoint.
type CustomMountPointArray []CustomMountPoint
// CustomNetworkInterface contains the values for the "net[n]" properties.
type CustomNetworkInterface struct {
Bridge *string `json:"bridge,omitempty" url:"bridge,omitempty"`
Enabled bool `json:"-" url:"-"`
Firewall *types.CustomBool `json:"firewall,omitempty" url:"firewall,omitempty,int"`
IPv4Address *string `json:"ip,omitempty" url:"ip,omitempty"`
IPv4Gateway *string `json:"gw,omitempty" url:"gw,omitempty"`
IPv6Address *string `json:"ip6,omitempty" url:"ip6,omitempty"`
IPv6Gateway *string `json:"gw6,omitempty" url:"gw6,omitempty"`
MACAddress *string `json:"hwaddr,omitempty" url:"hwaddr,omitempty"`
MTU *int `json:"mtu,omitempty" url:"mtu,omitempty"`
Name string `json:"name" url:"name"`
RateLimit *float64 `json:"rate,omitempty" url:"rate,omitempty"`
Tag *int `json:"tag,omitempty" url:"tag,omitempty"`
Trunks *[]int `json:"trunks,omitempty" url:"trunks,omitempty"`
Type *string `json:"type,omitempty" url:"type,omitempty"`
}
// CustomNetworkInterfaceArray is an array of CustomNetworkInterface.
type CustomNetworkInterfaceArray []CustomNetworkInterface
// CustomRootFS contains the values for the "rootfs" property.
type CustomRootFS struct {
ACL *types.CustomBool `json:"acl,omitempty" url:"acl,omitempty,int"`
Size *types.DiskSize `json:"size,omitempty" url:"size,omitempty"`
MountOptions *[]string `json:"mountoptions,omitempty" url:"mountoptions,omitempty"`
Quota *types.CustomBool `json:"quota,omitempty" url:"quota,omitempty,int"`
ReadOnly *types.CustomBool `json:"ro,omitempty" url:"ro,omitempty,int"`
Replicate *types.CustomBool `json:"replicate,omitempty" url:"replicate,omitempty,int"`
Shared *types.CustomBool `json:"shared,omitempty" url:"shared,omitempty,int"`
Volume string `json:"volume" url:"volume"`
}
// CustomSSHKeys contains the values for the "ssh-public-keys" property.
type CustomSSHKeys []string
// CustomStartupBehavior contains the values for the "startup" property.
type CustomStartupBehavior struct {
Down *int `json:"down,omitempty" url:"down,omitempty"`
Order *int `json:"order,omitempty" url:"order,omitempty"`
Up *int `json:"up,omitempty" url:"up,omitempty"`
}
// GetResponseBody contains the body from a user get response.
type GetResponseBody struct {
Data *GetResponseData `json:"data,omitempty"`
}
// GetResponseData contains the data from a user get response.
type GetResponseData struct {
ConsoleEnabled *types.CustomBool `json:"console,omitempty"`
ConsoleMode *string `json:"cmode,omitempty"`
CPUArchitecture *string `json:"arch,omitempty"`
CPUCores *int `json:"cores,omitempty"`
CPULimit *int `json:"cpulimit,omitempty"`
CPUUnits *int `json:"cpuunits,omitempty"`
DedicatedMemory *int `json:"memory,omitempty"`
Description *string `json:"description,omitempty"`
Digest string `json:"digest"`
DNSDomain *string `json:"searchdomain,omitempty"`
DNSServer *string `json:"nameserver,omitempty"`
Features *CustomFeatures `json:"features,omitempty"`
HookScript *string `json:"hookscript,omitempty"`
Hostname *string `json:"hostname,omitempty"`
Lock *types.CustomBool `json:"lock,omitempty"`
LXCConfiguration *[][2]string `json:"lxc,omitempty"`
MountPoint0 CustomMountPoint `json:"mp0,omitempty"`
MountPoint1 CustomMountPoint `json:"mp1,omitempty"`
MountPoint2 CustomMountPoint `json:"mp2,omitempty"`
MountPoint3 CustomMountPoint `json:"mp3,omitempty"`
NetworkInterface0 *CustomNetworkInterface `json:"net0,omitempty"`
NetworkInterface1 *CustomNetworkInterface `json:"net1,omitempty"`
NetworkInterface2 *CustomNetworkInterface `json:"net2,omitempty"`
NetworkInterface3 *CustomNetworkInterface `json:"net3,omitempty"`
NetworkInterface4 *CustomNetworkInterface `json:"net4,omitempty"`
NetworkInterface5 *CustomNetworkInterface `json:"net5,omitempty"`
NetworkInterface6 *CustomNetworkInterface `json:"net6,omitempty"`
NetworkInterface7 *CustomNetworkInterface `json:"net7,omitempty"`
OSType *string `json:"ostype,omitempty"`
Protection *types.CustomBool `json:"protection,omitempty"`
RootFS *CustomRootFS `json:"rootfs,omitempty"`
StartOnBoot *types.CustomBool `json:"onboot,omitempty"`
StartupBehavior *CustomStartupBehavior `json:"startup,omitempty"`
Swap *int `json:"swap,omitempty"`
Tags *string `json:"tags,omitempty"`
Template *types.CustomBool `json:"template,omitempty"`
TTY *int `json:"tty,omitempty"`
Unprivileged *types.CustomBool `json:"unprivileged,omitempty"`
}
// GetStatusResponseBody contains the body from a container get status response.
type GetStatusResponseBody struct {
Data *GetStatusResponseData `json:"data,omitempty"`
}
// GetStatusResponseData contains the data from a container get status response.
type GetStatusResponseData struct {
CPUCount *float64 `json:"cpus,omitempty"`
Lock *string `json:"lock,omitempty"`
MemoryAllocation *int `json:"maxmem,omitempty"`
Name *string `json:"name,omitempty"`
RootDiskSize *interface{} `json:"maxdisk,omitempty"`
Status string `json:"status,omitempty"`
SwapAllocation *int `json:"maxswap,omitempty"`
Tags *string `json:"tags,omitempty"`
Uptime *int `json:"uptime,omitempty"`
VMID *int `json:"vmid,omitempty"`
}
// RebootRequestBody contains the body for a container reboot request.
type RebootRequestBody struct {
Timeout *int `json:"timeout,omitempty" url:"timeout,omitempty"`
}
// ShutdownRequestBody contains the body for a container shutdown request.
type ShutdownRequestBody struct {
ForceStop *types.CustomBool `json:"forceStop,omitempty" url:"forceStop,omitempty,int"`
Timeout *int `json:"timeout,omitempty" url:"timeout,omitempty"`
}
// UpdateRequestBody contains the data for an user update request.
type UpdateRequestBody CreateRequestBody
// EncodeValues converts a ContainerCustomFeatures struct to a URL value.
func (r *CustomFeatures) EncodeValues(key string, v *url.Values) error {
var values []string
if r.FUSE != nil {
if *r.FUSE {
values = append(values, "fuse=1")
} else {
values = append(values, "fuse=0")
}
}
if r.KeyControl != nil {
if *r.KeyControl {
values = append(values, "keyctl=1")
} else {
values = append(values, "keyctl=0")
}
}
if r.MountTypes != nil {
if len(*r.MountTypes) > 0 {
values = append(values, fmt.Sprintf("mount=%s", strings.Join(*r.MountTypes, ";")))
}
}
if r.Nesting != nil {
if *r.Nesting {
values = append(values, "nesting=1")
} else {
values = append(values, "nesting=0")
}
}
if len(values) > 0 {
v.Add(key, strings.Join(values, ","))
}
return nil
}
// EncodeValues converts a CustomMountPoint struct to a URL value.
func (r *CustomMountPoint) EncodeValues(key string, v *url.Values) error {
var values []string
if r.ACL != nil {
if *r.ACL {
values = append(values, "acl=%d")
} else {
values = append(values, "acl=0")
}
}
if r.Backup != nil {
if *r.Backup {
values = append(values, "backup=1")
} else {
values = append(values, "backup=0")
}
}
if r.DiskSize != nil {
values = append(values, fmt.Sprintf("size=%s", *r.DiskSize))
}
if r.MountOptions != nil {
if len(*r.MountOptions) > 0 {
values = append(values, fmt.Sprintf("mount=%s", strings.Join(*r.MountOptions, ";")))
}
}
values = append(values, fmt.Sprintf("mp=%s", r.MountPoint))
if r.Quota != nil {
if *r.Quota {
values = append(values, "quota=1")
} else {
values = append(values, "quota=0")
}
}
if r.ReadOnly != nil {
if *r.ReadOnly {
values = append(values, "ro=1")
} else {
values = append(values, "ro=0")
}
}
if r.Replicate != nil {
if *r.ReadOnly {
values = append(values, "replicate=1")
} else {
values = append(values, "replicate=0")
}
}
if r.Shared != nil {
if *r.Shared {
values = append(values, "shared=1")
} else {
values = append(values, "shared=0")
}
}
values = append(values, fmt.Sprintf("volume=%s", r.Volume))
if len(values) > 0 {
v.Add(key, strings.Join(values, ","))
}
return nil
}
// EncodeValues converts a CustomMountPointArray array to multiple URL values.
func (r CustomMountPointArray) EncodeValues(
key string,
v *url.Values,
) error {
for i, d := range r {
if err := d.EncodeValues(fmt.Sprintf("%s%d", key, i), v); err != nil {
return fmt.Errorf("failed to encode CustomMountPointArray: %w", err)
}
}
return nil
}
// EncodeValues converts a CustomNetworkInterface struct to a URL value.
func (r *CustomNetworkInterface) EncodeValues(
key string,
v *url.Values,
) error {
var values []string
if r.Bridge != nil {
values = append(values, fmt.Sprintf("bridge=%s", *r.Bridge))
}
if r.Firewall != nil {
if *r.Firewall {
values = append(values, "firewall=1")
} else {
values = append(values, "firewall=0")
}
}
if r.IPv4Address != nil {
values = append(values, fmt.Sprintf("ip=%s", *r.IPv4Address))
}
if r.IPv4Gateway != nil {
values = append(values, fmt.Sprintf("gw=%s", *r.IPv4Gateway))
}
if r.IPv6Address != nil {
values = append(values, fmt.Sprintf("ip6=%s", *r.IPv6Address))
}
if r.IPv6Gateway != nil {
values = append(values, fmt.Sprintf("gw6=%s", *r.IPv6Gateway))
}
if r.MACAddress != nil {
values = append(values, fmt.Sprintf("hwaddr=%s", *r.MACAddress))
}
if r.MTU != nil {
values = append(values, fmt.Sprintf("mtu=%d", *r.MTU))
}
values = append(values, fmt.Sprintf("name=%s", r.Name))
if r.RateLimit != nil {
values = append(values, fmt.Sprintf("rate=%.2f", *r.RateLimit))
}
if r.Tag != nil {
values = append(values, fmt.Sprintf("tag=%d", *r.Tag))
}
if r.Trunks != nil && len(*r.Trunks) > 0 {
sTrunks := make([]string, len(*r.Trunks))
for i, v := range *r.Trunks {
sTrunks[i] = strconv.Itoa(v)
}
values = append(values, fmt.Sprintf("trunks=%s", strings.Join(sTrunks, ";")))
}
if r.Type != nil {
values = append(values, fmt.Sprintf("type=%s", *r.Type))
}
if len(values) > 0 {
v.Add(key, strings.Join(values, ","))
}
return nil
}
// EncodeValues converts a CustomNetworkInterfaceArray array to multiple URL values.
func (r CustomNetworkInterfaceArray) EncodeValues(
key string,
v *url.Values,
) error {
for i, d := range r {
if err := d.EncodeValues(fmt.Sprintf("%s%d", key, i), v); err != nil {
return fmt.Errorf("failed to encode CustomNetworkInterfaceArray: %w", err)
}
}
return nil
}
// EncodeValues converts a CustomRootFS struct to a URL value.
func (r *CustomRootFS) EncodeValues(key string, v *url.Values) error {
var values []string
if r.ACL != nil {
if *r.ACL {
values = append(values, "acl=%d")
} else {
values = append(values, "acl=0")
}
}
if r.Size != nil {
values = append(values, fmt.Sprintf("size=%s", *r.Size))
}
if r.MountOptions != nil {
if len(*r.MountOptions) > 0 {
values = append(values, fmt.Sprintf("mount=%s", strings.Join(*r.MountOptions, ";")))
}
}
if r.Quota != nil {
if *r.Quota {
values = append(values, "quota=1")
} else {
values = append(values, "quota=0")
}
}
if r.ReadOnly != nil {
if *r.ReadOnly {
values = append(values, "ro=1")
} else {
values = append(values, "ro=0")
}
}
if r.Replicate != nil {
if *r.ReadOnly {
values = append(values, "replicate=1")
} else {
values = append(values, "replicate=0")
}
}
if r.Shared != nil {
if *r.Shared {
values = append(values, "shared=1")
} else {
values = append(values, "shared=0")
}
}
values = append(values, fmt.Sprintf("volume=%s", r.Volume))
if len(values) > 0 {
v.Add(key, strings.Join(values, ","))
}
return nil
}
// EncodeValues converts a CustomSSHKeys array to a URL value.
func (r CustomSSHKeys) EncodeValues(key string, v *url.Values) error {
v.Add(key, strings.Join(r, "\n"))
return nil
}
// EncodeValues converts a CustomStartupBehavior struct to a URL value.
func (r *CustomStartupBehavior) EncodeValues(
key string,
v *url.Values,
) error {
var values []string
if r.Down != nil {
values = append(values, fmt.Sprintf("down=%d", *r.Down))
}
if r.Order != nil {
values = append(values, fmt.Sprintf("order=%d", *r.Order))
}
if r.Up != nil {
values = append(values, fmt.Sprintf("up=%d", *r.Up))
}
if len(values) > 0 {
v.Add(key, strings.Join(values, ","))
}
return nil
}
// UnmarshalJSON converts a ContainerCustomFeatures string to an object.
func (r *CustomFeatures) UnmarshalJSON(b []byte) error {
var s string
err := json.Unmarshal(b, &s)
if err != nil {
return fmt.Errorf("unable to unmarshal ContainerCustomFeatures: %w", err)
}
pairs := strings.Split(s, ",")
for _, p := range pairs {
v := strings.Split(strings.TrimSpace(p), "=")
if len(v) == 2 {
switch v[0] {
case "fuse":
bv := types.CustomBool(v[1] == "1")
r.FUSE = &bv
case "keyctl":
bv := types.CustomBool(v[1] == "1")
r.KeyControl = &bv
case "mount":
if v[1] != "" {
a := strings.Split(v[1], ";")
r.MountTypes = &a
} else {
var a []string
r.MountTypes = &a
}
case "nesting":
bv := types.CustomBool(v[1] == "1")
r.Nesting = &bv
}
}
}
return nil
}
// UnmarshalJSON converts a CustomMountPoint string to an object.
func (r *CustomMountPoint) UnmarshalJSON(b []byte) error {
var s string
err := json.Unmarshal(b, &s)
if err != nil {
return fmt.Errorf("unable to unmarshal CustomMountPoint: %w", err)
}
pairs := strings.Split(s, ",")
for _, p := range pairs {
v := strings.Split(strings.TrimSpace(p), "=")
if len(v) == 1 {
r.Volume = v[0]
} else if len(v) == 2 {
switch v[0] {
case "acl":
bv := types.CustomBool(v[1] == "1")
r.ACL = &bv
case "backup":
bv := types.CustomBool(v[1] == "1")
r.Backup = &bv
case "mountoptions":
if v[1] != "" {
a := strings.Split(v[1], ";")
r.MountOptions = &a
} else {
var a []string
r.MountOptions = &a
}
case "mp":
r.MountPoint = v[1]
case "quota":
bv := types.CustomBool(v[1] == "1")
r.Quota = &bv
case "ro":
bv := types.CustomBool(v[1] == "1")
r.ReadOnly = &bv
case "replicate":
bv := types.CustomBool(v[1] == "1")
r.Replicate = &bv
case "shared":
bv := types.CustomBool(v[1] == "1")
r.Shared = &bv
case "size":
r.DiskSize = &v[1]
}
}
}
return nil
}
// UnmarshalJSON converts a CustomNetworkInterface string to an object.
func (r *CustomNetworkInterface) UnmarshalJSON(b []byte) error {
var s string
er := json.Unmarshal(b, &s)
if er != nil {
return fmt.Errorf("unable to unmarshal CustomNetworkInterface: %w", er)
}
pairs := strings.Split(s, ",")
for _, p := range pairs {
v := strings.Split(strings.TrimSpace(p), "=")
//nolint:nestif
if len(v) == 1 {
r.Name = v[0]
} else if len(v) == 2 {
switch v[0] {
case "bridge":
r.Bridge = &v[1]
case "firewall":
bv := types.CustomBool(v[1] == "1")
r.Firewall = &bv
case "gw":
r.IPv4Gateway = &v[1]
case "gw6":
r.IPv6Gateway = &v[1]
case "ip":
r.IPv4Address = &v[1]
case "ip6":
r.IPv6Address = &v[1]
case "hwaddr":
r.MACAddress = &v[1]
case "mtu":
iv, err := strconv.Atoi(v[1])
if err != nil {
return fmt.Errorf("unable to unmarshal 'mtu': %w", err)
}
r.MTU = &iv
case "name":
r.Name = v[1]
case "rate":
fv, err := strconv.ParseFloat(v[1], 64)
if err != nil {
return fmt.Errorf("unable to unmarshal 'rate': %w", err)
}
r.RateLimit = &fv
case "tag":
iv, err := strconv.Atoi(v[1])
if err != nil {
return fmt.Errorf("unable to unmarshal 'tag': %w", err)
}
r.Tag = &iv
case "trunks":
var err error
if v[1] != "" {
trunks := strings.Split(v[1], ";")
a := make([]int, len(trunks))
for ti, tv := range trunks {
a[ti], err = strconv.Atoi(tv)
if err != nil {
return fmt.Errorf("unable to unmarshal 'trunks': %w", err)
}
}
r.Trunks = &a
} else {
var a []int
r.Trunks = &a
}
case "type":
r.Type = &v[1]
}
}
}
return nil
}
// UnmarshalJSON converts a CustomRootFS string to an object.
func (r *CustomRootFS) UnmarshalJSON(b []byte) error {
var s string
err := json.Unmarshal(b, &s)
if err != nil {
return fmt.Errorf("unable to unmarshal CustomRootFS: %w", err)
}
pairs := strings.Split(s, ",")
for _, p := range pairs {
v := strings.Split(strings.TrimSpace(p), "=")
if len(v) == 1 {
r.Volume = v[0]
} else if len(v) == 2 {
switch v[0] {
case "acl":
bv := types.CustomBool(v[1] == "1")
r.ACL = &bv
case "mountoptions":
if v[1] != "" {
a := strings.Split(v[1], ";")
r.MountOptions = &a
} else {
var a []string
r.MountOptions = &a
}
case "quota":
bv := types.CustomBool(v[1] == "1")
r.Quota = &bv
case "ro":
bv := types.CustomBool(v[1] == "1")
r.ReadOnly = &bv
case "replicate":
bv := types.CustomBool(v[1] == "1")
r.Replicate = &bv
case "shared":
bv := types.CustomBool(v[1] == "1")
r.Shared = &bv
case "size":
r.Size = new(types.DiskSize)
err := r.Size.UnmarshalJSON([]byte(v[1]))
if err != nil {
return fmt.Errorf("failed to unmarshal disk size: %w", err)
}
}
}
}
return nil
}
// UnmarshalJSON converts a CustomStartupBehavior string to an object.
func (r *CustomStartupBehavior) UnmarshalJSON(b []byte) error {
var s string
err := json.Unmarshal(b, &s)
if err != nil {
return fmt.Errorf("unable to unmarshal CustomStartupBehavior: %w", err)
}
pairs := strings.Split(s, ",")
for _, p := range pairs {
v := strings.Split(strings.TrimSpace(p), "=")
if len(v) == 2 {
switch v[0] {
case "down":
iv, err := strconv.Atoi(v[1])
if err != nil {
return fmt.Errorf("unable to unmarshal 'down': %w", err)
}
r.Down = &iv
case "order":
iv, err := strconv.Atoi(v[1])
if err != nil {
return fmt.Errorf("unable to unmarshal 'order': %w", err)
}
r.Order = &iv
case "up":
iv, err := strconv.Atoi(v[1])
if err != nil {
return fmt.Errorf("unable to unmarshal 'up': %w", err)
}
r.Up = &iv
}
}
}
return nil
}

View File

@ -10,6 +10,7 @@ import (
"github.com/bpg/terraform-provider-proxmox/proxmox/firewall"
)
// Client is an interface for accessing the Proxmox container firewall API.
type Client struct {
firewall.Client
}

41
proxmox/nodes/dns.go Normal file
View File

@ -0,0 +1,41 @@
/*
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
*/
package nodes
import (
"context"
"fmt"
"net/http"
"github.com/bpg/terraform-provider-proxmox/proxmox/api"
)
// GetDNS retrieves the DNS configuration for a node.
func (c *Client) GetDNS(ctx context.Context) (*DNSGetResponseData, error) {
resBody := &DNSGetResponseBody{}
err := c.DoRequest(ctx, http.MethodGet, c.ExpandPath("dns"), nil, resBody)
if err != nil {
return nil, fmt.Errorf("error retrieving DNS configuration: %w", err)
}
if resBody.Data == nil {
return nil, api.ErrNoDataObjectInResponse
}
return resBody.Data, nil
}
// UpdateDNS updates the DNS configuration for a node.
func (c *Client) UpdateDNS(ctx context.Context, d *DNSUpdateRequestBody) error {
err := c.DoRequest(ctx, http.MethodPut, c.ExpandPath("dns"), d, nil)
if err != nil {
return fmt.Errorf("error updating DNS configuration: %w", err)
}
return nil
}

View File

@ -1,24 +1,26 @@
/* This Source Code Form is subject to the terms of the Mozilla Public
/*
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at https://mozilla.org/MPL/2.0/. */
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
*/
package proxmox
package nodes
// VirtualEnvironmentDNSGetResponseBody contains the body from a DNS get response.
type VirtualEnvironmentDNSGetResponseBody struct {
Data *VirtualEnvironmentDNSGetResponseData `json:"data,omitempty"`
// DNSGetResponseBody contains the body from a DNS get response.
type DNSGetResponseBody struct {
Data *DNSGetResponseData `json:"data,omitempty"`
}
// VirtualEnvironmentDNSGetResponseData contains the data from a DNS get response.
type VirtualEnvironmentDNSGetResponseData struct {
// DNSGetResponseData contains the data from a DNS get response.
type DNSGetResponseData struct {
Server1 *string `json:"dns1,omitempty" url:"dns1,omitempty"`
Server2 *string `json:"dns2,omitempty" url:"dns2,omitempty"`
Server3 *string `json:"dns3,omitempty" url:"dns3,omitempty"`
SearchDomain *string `json:"search,omitempty" url:"search,omitempty"`
}
// VirtualEnvironmentDNSUpdateRequestBody contains the body for a DNS update request.
type VirtualEnvironmentDNSUpdateRequestBody struct {
// DNSUpdateRequestBody contains the body for a DNS update request.
type DNSUpdateRequestBody struct {
Server1 *string `json:"dns1,omitempty" url:"dns1,omitempty"`
Server2 *string `json:"dns2,omitempty" url:"dns2,omitempty"`
Server3 *string `json:"dns3,omitempty" url:"dns3,omitempty"`

41
proxmox/nodes/hosts.go Normal file
View File

@ -0,0 +1,41 @@
/*
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
*/
package nodes
import (
"context"
"fmt"
"net/http"
"github.com/bpg/terraform-provider-proxmox/proxmox/api"
)
// GetHosts retrieves the Hosts configuration for a node.
func (c *Client) GetHosts(ctx context.Context) (*HostsGetResponseData, error) {
resBody := &HostsGetResponseBody{}
err := c.DoRequest(ctx, http.MethodGet, c.ExpandPath("hosts"), nil, resBody)
if err != nil {
return nil, fmt.Errorf("error retrieving hosts configuration: %w", err)
}
if resBody.Data == nil {
return nil, api.ErrNoDataObjectInResponse
}
return resBody.Data, nil
}
// UpdateHosts updates the Hosts configuration for a node.
func (c *Client) UpdateHosts(ctx context.Context, d *HostsUpdateRequestBody) error {
err := c.DoRequest(ctx, http.MethodPost, c.ExpandPath("hosts"), d, nil)
if err != nil {
return fmt.Errorf("error updating hosts configuration: %w", err)
}
return nil
}

View File

@ -0,0 +1,24 @@
/*
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
*/
package nodes
// HostsGetResponseBody contains the body from a hosts get response.
type HostsGetResponseBody struct {
Data *HostsGetResponseData `json:"data,omitempty"`
}
// HostsGetResponseData contains the data from a hosts get response.
type HostsGetResponseData struct {
Data string `json:"data"`
Digest *string `json:"digest,omitempty"`
}
// HostsUpdateRequestBody contains the body for a hosts update request.
type HostsUpdateRequestBody struct {
Data string `json:"data" url:"data"`
Digest *string `json:"digest,omitempty" url:"digest,omitempty"`
}

108
proxmox/nodes/nodes.go Normal file
View File

@ -0,0 +1,108 @@
/*
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
*/
package nodes
import (
"context"
"fmt"
"net/http"
"sort"
"strings"
"github.com/bpg/terraform-provider-proxmox/proxmox/api"
)
// GetIP retrieves the IP address of a node.
func (c *Client) GetIP(ctx context.Context) (string, error) {
networkDevices, err := c.ListNetworkDevices(ctx)
if err != nil {
return "", err
}
nodeAddress := ""
for _, d := range networkDevices {
if d.Address != nil {
nodeAddress = *d.Address
break
}
}
if nodeAddress == "" {
return "", fmt.Errorf("failed to determine the IP address of node \"%s\"", c.NodeName)
}
nodeAddressParts := strings.Split(nodeAddress, "/")
return nodeAddressParts[0], nil
}
// GetTime retrieves the time information for a node.
func (c *Client) GetTime(ctx context.Context) (*GetTimeResponseData, error) {
resBody := &GetTimeResponseBody{}
err := c.DoRequest(ctx, http.MethodGet, c.ExpandPath("time"), nil, resBody)
if err != nil {
return nil, fmt.Errorf("failed to get time information for node \"%s\": %w", c.NodeName, err)
}
if resBody.Data == nil {
return nil, api.ErrNoDataObjectInResponse
}
return resBody.Data, nil
}
// ListNetworkDevices retrieves a list of network devices for a specific nodes.
func (c *Client) ListNetworkDevices(ctx context.Context) ([]*NetworkDeviceListResponseData, error) {
resBody := &NetworkDeviceListResponseBody{}
err := c.DoRequest(ctx, http.MethodGet, c.ExpandPath("network"), nil, resBody)
if err != nil {
return nil, fmt.Errorf("failed to get network devices for node \"%s\": %w", c.NodeName, err)
}
if resBody.Data == nil {
return nil, api.ErrNoDataObjectInResponse
}
sort.Slice(resBody.Data, func(i, j int) bool {
return resBody.Data[i].Priority < resBody.Data[j].Priority
})
return resBody.Data, nil
}
// ListNodes retrieves a list of nodes.
func (c *Client) ListNodes(ctx context.Context) ([]*ListResponseData, error) {
resBody := &ListResponseBody{}
err := c.DoRequest(ctx, http.MethodGet, "nodes", nil, resBody)
if err != nil {
return nil, fmt.Errorf("failed to get nodes: %w", err)
}
if resBody.Data == nil {
return nil, api.ErrNoDataObjectInResponse
}
sort.Slice(resBody.Data, func(i, j int) bool {
return resBody.Data[i].Name < resBody.Data[j].Name
})
return resBody.Data, nil
}
// UpdateTime updates the time on a node.
func (c *Client) UpdateTime(ctx context.Context, d *UpdateTimeRequestBody) error {
err := c.DoRequest(ctx, http.MethodPut, c.ExpandPath("time"), d, nil)
if err != nil {
return fmt.Errorf("failed to update node time: %w", err)
}
return nil
}

View File

@ -0,0 +1,95 @@
/*
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
*/
package nodes
import (
"encoding/json"
"fmt"
"net/url"
"github.com/bpg/terraform-provider-proxmox/proxmox/types"
)
// CustomCommands contains an array of commands to execute.
type CustomCommands []string
// ExecuteRequestBody contains the data for a node execute request.
type ExecuteRequestBody struct {
Commands CustomCommands `json:"commands" url:"commands"`
}
// GetTimeResponseBody contains the body from a node time zone get response.
type GetTimeResponseBody struct {
Data *GetTimeResponseData `json:"data,omitempty"`
}
// GetTimeResponseData contains the data from a node list response.
type GetTimeResponseData struct {
LocalTime types.CustomTimestamp `json:"localtime"`
TimeZone string `json:"timezone"`
UTCTime types.CustomTimestamp `json:"time"`
}
// ListResponseBody contains the body from a node list response.
type ListResponseBody struct {
Data []*ListResponseData `json:"data,omitempty"`
}
// ListResponseData contains the data from a node list response.
type ListResponseData struct {
CPUCount *int `json:"maxcpu,omitempty"`
CPUUtilization *float64 `json:"cpu,omitempty"`
MemoryAvailable *int `json:"maxmem,omitempty"`
MemoryUsed *int `json:"mem,omitempty"`
Name string `json:"node"`
SSLFingerprint *string `json:"ssl_fingerprint,omitempty"`
Status *string `json:"status"`
SupportLevel *string `json:"level,omitempty"`
Uptime *int `json:"uptime"`
}
// NetworkDeviceListResponseBody contains the body from a node network device list response.
type NetworkDeviceListResponseBody struct {
Data []*NetworkDeviceListResponseData `json:"data,omitempty"`
}
// NetworkDeviceListResponseData contains the data from a node network device list response.
type NetworkDeviceListResponseData struct {
Active *types.CustomBool `json:"active,omitempty"`
Address *string `json:"address,omitempty"`
Autostart *types.CustomBool `json:"autostart,omitempty"`
BridgeFD *string `json:"bridge_fd,omitempty"`
BridgePorts *string `json:"bridge_ports,omitempty"`
BridgeSTP *string `json:"bridge_stp,omitempty"`
CIDR *string `json:"cidr,omitempty"`
Exists *types.CustomBool `json:"exists,omitempty"`
Families *[]string `json:"families,omitempty"`
Gateway *string `json:"gateway,omitempty"`
Iface string `json:"iface"`
MethodIPv4 *string `json:"method,omitempty"`
MethodIPv6 *string `json:"method6,omitempty"`
Netmask *string `json:"netmask,omitempty"`
Priority int `json:"priority"`
Type string `json:"type"`
}
// UpdateTimeRequestBody contains the body for a node time update request.
type UpdateTimeRequestBody struct {
TimeZone string `json:"timezone" url:"timezone"`
}
// EncodeValues converts a CustomCommands array to a JSON encoded URL value.
func (r CustomCommands) EncodeValues(key string, v *url.Values) error {
jsonArrayBytes, err := json.Marshal(r)
if err != nil {
return fmt.Errorf("error marshalling CustomCommands array: %w", err)
}
v.Add(key, string(jsonArrayBytes))
return nil
}

280
proxmox/nodes/storage.go Normal file
View File

@ -0,0 +1,280 @@
/*
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
*/
package nodes
import (
"context"
"fmt"
"io"
"mime/multipart"
"net/http"
"net/url"
"os"
"sort"
"github.com/hashicorp/terraform-plugin-log/tflog"
"github.com/bpg/terraform-provider-proxmox/proxmox/api"
)
// DeleteDatastoreFile deletes a file in a datastore.
func (c *Client) DeleteDatastoreFile(
ctx context.Context,
datastoreID, volumeID string,
) error {
err := c.DoRequest(
ctx,
http.MethodDelete,
c.ExpandPath(
fmt.Sprintf(
"storage/%s/content/%s",
url.PathEscape(datastoreID),
url.PathEscape(volumeID),
),
),
nil,
nil,
)
if err != nil {
return fmt.Errorf("error deleting file %s from datastore %s: %w", volumeID, datastoreID, err)
}
return nil
}
// GetDatastoreStatus gets status information for a given datastore.
func (c *Client) GetDatastoreStatus(
ctx context.Context,
datastoreID string,
) (*DatastoreGetStatusResponseData, error) {
resBody := &DatastoreGetStatusResponseBody{}
err := c.DoRequest(
ctx,
http.MethodGet,
c.ExpandPath(
fmt.Sprintf(
"storage/%s/status",
url.PathEscape(datastoreID),
),
),
nil,
resBody,
)
if err != nil {
return nil, fmt.Errorf("error retrieving status for datastore %s: %w", datastoreID, err)
}
if resBody.Data == nil {
return nil, api.ErrNoDataObjectInResponse
}
return resBody.Data, nil
}
// ListDatastoreFiles retrieves a list of the files in a datastore.
func (c *Client) ListDatastoreFiles(
ctx context.Context,
datastoreID string,
) ([]*DatastoreFileListResponseData, error) {
resBody := &DatastoreFileListResponseBody{}
err := c.DoRequest(
ctx,
http.MethodGet,
c.ExpandPath(
fmt.Sprintf(
"storage/%s/content",
url.PathEscape(datastoreID),
),
),
nil,
resBody,
)
if err != nil {
return nil, fmt.Errorf("error retrieving files from datastore %s: %w", datastoreID, err)
}
if resBody.Data == nil {
return nil, api.ErrNoDataObjectInResponse
}
sort.Slice(resBody.Data, func(i, j int) bool {
return resBody.Data[i].VolumeID < resBody.Data[j].VolumeID
})
return resBody.Data, nil
}
// ListDatastores retrieves a list of nodes.
func (c *Client) ListDatastores(
ctx context.Context,
d *DatastoreListRequestBody,
) ([]*DatastoreListResponseData, error) {
resBody := &DatastoreListResponseBody{}
err := c.DoRequest(
ctx,
http.MethodGet,
c.ExpandPath("storage"),
d,
resBody,
)
if err != nil {
return nil, fmt.Errorf("error retrieving datastores: %w", err)
}
if resBody.Data == nil {
return nil, api.ErrNoDataObjectInResponse
}
sort.Slice(resBody.Data, func(i, j int) bool {
return resBody.Data[i].ID < resBody.Data[j].ID
})
return resBody.Data, nil
}
// APIUpload uploads a file to a datastore using the Proxmox API.
func (c *Client) APIUpload(
ctx context.Context,
datastoreID string,
d *api.FileUploadRequest,
) (*DatastoreUploadResponseBody, error) {
tflog.Debug(ctx, "uploading file to datastore using PVE API", map[string]interface{}{
"file_name": d.FileName,
"content_type": d.ContentType,
})
r, w := io.Pipe()
defer func(r *io.PipeReader) {
err := r.Close()
if err != nil {
tflog.Error(ctx, "failed to close pipe reader", map[string]interface{}{
"error": err,
})
}
}(r)
m := multipart.NewWriter(w)
go func() {
defer func(w *io.PipeWriter) {
err := w.Close()
if err != nil {
tflog.Error(ctx, "failed to close pipe writer", map[string]interface{}{
"error": err,
})
}
}(w)
defer func(m *multipart.Writer) {
err := m.Close()
if err != nil {
tflog.Error(ctx, "failed to close multipart writer", map[string]interface{}{
"error": err,
})
}
}(m)
err := m.WriteField("content", d.ContentType)
if err != nil {
tflog.Error(ctx, "failed to write 'content' field", map[string]interface{}{
"error": err,
})
return
}
part, err := m.CreateFormFile("filename", d.FileName)
if err != nil {
return
}
_, err = io.Copy(part, d.File)
if err != nil {
return
}
}()
// We need to store the multipart content in a temporary file to avoid using high amounts of memory.
// This is necessary due to Proxmox VE not supporting chunked transfers in v6.1 and earlier versions.
tempMultipartFile, err := os.CreateTemp("", "multipart")
if err != nil {
return nil, fmt.Errorf("failed to create temporary file: %w", err)
}
tempMultipartFileName := tempMultipartFile.Name()
_, err = io.Copy(tempMultipartFile, r)
if err != nil {
return nil, fmt.Errorf("failed to copy multipart data to temporary file: %w", err)
}
err = tempMultipartFile.Close()
if err != nil {
return nil, fmt.Errorf("failed to close temporary file: %w", err)
}
defer func(name string) {
e := os.Remove(name)
if e != nil {
tflog.Error(ctx, "failed to remove temporary file", map[string]interface{}{
"error": e,
})
}
}(tempMultipartFileName)
// Now that the multipart data is stored in a file, we can go ahead and do an HTTP POST request.
fileReader, err := os.Open(tempMultipartFileName)
if err != nil {
return nil, fmt.Errorf("failed to open temporary file: %w", err)
}
defer func(fileReader *os.File) {
e := fileReader.Close()
if e != nil {
tflog.Error(ctx, "failed to close file reader", map[string]interface{}{
"error": e,
})
}
}(fileReader)
fileInfo, err := fileReader.Stat()
if err != nil {
return nil, fmt.Errorf("failed to get file info: %w", err)
}
fileSize := fileInfo.Size()
reqBody := &api.MultiPartData{
Boundary: m.Boundary(),
Reader: fileReader,
Size: &fileSize,
}
resBody := &DatastoreUploadResponseBody{}
err = c.DoRequest(
ctx,
http.MethodPost,
c.ExpandPath(
fmt.Sprintf(
"storage/%s/upload",
url.PathEscape(datastoreID),
),
),
reqBody,
resBody,
)
if err != nil {
return nil, fmt.Errorf("error uploading file to datastore %s: %w", datastoreID, err)
}
return resBody, nil
}

View File

@ -1,30 +1,15 @@
/* This Source Code Form is subject to the terms of the Mozilla Public
/*
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at https://mozilla.org/MPL/2.0/. */
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
*/
package proxmox
package nodes
import (
"os"
"github.com/bpg/terraform-provider-proxmox/proxmox/types"
)
// DatastoreGetResponseBody contains the body from a datastore get response.
type DatastoreGetResponseBody struct {
Data *DatastoreGetResponseData `json:"data,omitempty"`
}
// DatastoreGetResponseData contains the data from a datastore get response.
type DatastoreGetResponseData struct {
Content types.CustomCommaSeparatedList `json:"content,omitempty" url:"content,omitempty,comma"`
Digest *string `json:"digest,omitempty"`
Path *string `json:"path,omitempty"`
Shared *types.CustomBool `json:"shared,omitempty"`
Storage *string `json:"storage,omitempty"`
Type *string `json:"type,omitempty"`
}
// DatastoreFileListResponseBody contains the body from a datastore content list response.
type DatastoreFileListResponseBody struct {
Data []*DatastoreFileListResponseData `json:"data,omitempty"`
@ -86,15 +71,6 @@ type DatastoreListResponseData struct {
Type string `json:"type,omitempty"`
}
// DatastoreUploadRequestBody contains the body for a datastore upload request.
type DatastoreUploadRequestBody struct {
ContentType string `json:"content,omitempty"`
DatastoreID string `json:"storage,omitempty"`
FileName string `json:"filename,omitempty"`
NodeName string `json:"node,omitempty"`
File *os.File `json:"-"`
}
// DatastoreUploadResponseBody contains the body from a datastore upload response.
type DatastoreUploadResponseBody struct {
UploadID *string `json:"data,omitempty"`

View File

@ -0,0 +1,25 @@
/*
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
*/
package tasks
import (
"fmt"
"github.com/bpg/terraform-provider-proxmox/proxmox/api"
)
// Client is an interface for performing requests against the Proxmox 'tasks' API.
type Client struct {
api.Client
}
// ExpandPath expands a path relative to the client's base path.
func (c *Client) ExpandPath(path string) string {
return c.Client.ExpandPath(
fmt.Sprintf("tasks/%s", path),
)
}

View File

@ -0,0 +1,86 @@
/*
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
*/
package tasks
import (
"context"
"fmt"
"net/http"
"net/url"
"time"
"github.com/bpg/terraform-provider-proxmox/proxmox/api"
)
// GetTaskStatus retrieves the status of a task.
func (c *Client) GetTaskStatus(ctx context.Context, upid string) (*GetTaskStatusResponseData, error) {
resBody := &GetTaskStatusResponseBody{}
err := c.DoRequest(
ctx,
http.MethodGet,
c.ExpandPath(fmt.Sprintf("%s/status", url.PathEscape(upid))),
nil,
resBody,
)
if err != nil {
return nil, fmt.Errorf("error retrievinf task status: %w", err)
}
if resBody.Data == nil {
return nil, api.ErrNoDataObjectInResponse
}
return resBody.Data, nil
}
// WaitForTask waits for a specific task to complete.
func (c *Client) WaitForTask(ctx context.Context, upid string, timeout, delay int) error {
timeDelay := int64(delay)
timeMax := float64(timeout)
timeStart := time.Now()
timeElapsed := timeStart.Sub(timeStart)
for timeElapsed.Seconds() < timeMax {
if int64(timeElapsed.Seconds())%timeDelay == 0 {
status, err := c.GetTaskStatus(ctx, upid)
if err != nil {
return err
}
if status.Status != "running" {
if status.ExitCode != "OK" {
return fmt.Errorf(
"task \"%s\" failed to complete with exit code: %s",
upid,
status.ExitCode,
)
}
return nil
}
time.Sleep(1 * time.Second)
}
time.Sleep(200 * time.Millisecond)
timeElapsed = time.Since(timeStart)
if ctx.Err() != nil {
return fmt.Errorf(
"context error while waiting for task \"%s\" to complete: %w",
upid, ctx.Err(),
)
}
}
return fmt.Errorf(
"timeout while waiting for task \"%s\" to complete",
upid,
)
}

View File

@ -0,0 +1,19 @@
/*
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
*/
package tasks
// GetTaskStatusResponseBody contains the body from a node get task status response.
type GetTaskStatusResponseBody struct {
Data *GetTaskStatusResponseData `json:"data,omitempty"`
}
// GetTaskStatusResponseData contains the data from a node get task status response.
type GetTaskStatusResponseData struct {
PID int `json:"pid,omitempty"`
Status string `json:"status,omitempty"`
ExitCode string `json:"exitstatus,omitempty"`
}

View File

@ -0,0 +1,50 @@
/*
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
*/
package vms
import (
"fmt"
"github.com/bpg/terraform-provider-proxmox/proxmox/api"
"github.com/bpg/terraform-provider-proxmox/proxmox/firewall"
"github.com/bpg/terraform-provider-proxmox/proxmox/nodes/tasks"
vmfirewall "github.com/bpg/terraform-provider-proxmox/proxmox/nodes/vms/firewall"
)
// Client is an interface for accessing the Proxmox VM API.
type Client struct {
api.Client
VMID int
}
func (c *Client) basePath() string {
return c.Client.ExpandPath("qemu")
}
// ExpandPath expands a relative path to a full VM API path.
func (c *Client) ExpandPath(path string) string {
ep := fmt.Sprintf("%s/%d", c.basePath(), c.VMID)
if path != "" {
ep = fmt.Sprintf("%s/%s", ep, path)
}
return ep
}
// Tasks returns a client for managing VM tasks.
func (c *Client) Tasks() *tasks.Client {
return &tasks.Client{
Client: c.Client,
}
}
// Firewall returns a client for managing the VM firewall.
func (c *Client) Firewall() firewall.API {
return &vmfirewall.Client{
Client: firewall.Client{Client: c},
}
}

View File

@ -10,6 +10,7 @@ import (
"github.com/bpg/terraform-provider-proxmox/proxmox/firewall"
)
// Client is an interface for accessing the Proxmox VM firewall API.
type Client struct {
firewall.Client
}

581
proxmox/nodes/vms/vms.go Normal file
View File

@ -0,0 +1,581 @@
/*
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
*/
package vms
import (
"context"
"fmt"
"net"
"net/http"
"strings"
"time"
"github.com/hashicorp/terraform-plugin-log/tflog"
"github.com/bpg/terraform-provider-proxmox/proxmox/api"
)
// CloneVM clones a virtual machine.
func (c *Client) CloneVM(ctx context.Context, retries int, d *CloneRequestBody, timeout int) error {
var err error
resBody := &MoveDiskResponseBody{}
// just a guard in case someone sets retries to 0 unknowingly
if retries <= 0 {
retries = 1
}
for i := 0; i < retries; i++ {
err = c.DoRequest(ctx, http.MethodPost, c.ExpandPath("clone"), d, resBody)
if err != nil {
return fmt.Errorf("error cloning VM: %w", err)
}
if resBody.Data == nil {
return api.ErrNoDataObjectInResponse
}
err = c.Tasks().WaitForTask(ctx, *resBody.Data, timeout, 5)
if err == nil {
return nil
}
time.Sleep(10 * time.Second)
}
if err != nil {
return fmt.Errorf("error waiting for VM clone: %w", err)
}
return nil
}
// CreateVM creates a virtual machine.
func (c *Client) CreateVM(ctx context.Context, d *CreateRequestBody, timeout int) error {
taskID, err := c.CreateVMAsync(ctx, d)
if err != nil {
return err
}
err = c.Tasks().WaitForTask(ctx, *taskID, timeout, 1)
if err != nil {
return fmt.Errorf("error waiting for VM creation: %w", err)
}
return nil
}
// CreateVMAsync creates a virtual machine asynchronously.
func (c *Client) CreateVMAsync(ctx context.Context, d *CreateRequestBody) (*string, error) {
resBody := &CreateResponseBody{}
err := c.DoRequest(ctx, http.MethodPost, c.basePath(), d, resBody)
if err != nil {
return nil, fmt.Errorf("error creating VM: %w", err)
}
if resBody.Data == nil {
return nil, api.ErrNoDataObjectInResponse
}
return resBody.Data, nil
}
// DeleteVM deletes a virtual machine.
func (c *Client) DeleteVM(ctx context.Context) error {
err := c.DoRequest(ctx, http.MethodDelete, c.ExpandPath("?destroy-unreferenced-disks=1&purge=1"), nil, nil)
if err != nil {
return fmt.Errorf("error deleting VM: %w", err)
}
return nil
}
// GetVM retrieves a virtual machine.
func (c *Client) GetVM(ctx context.Context) (*GetResponseData, error) {
resBody := &GetResponseBody{}
err := c.DoRequest(ctx, http.MethodGet, c.ExpandPath("config"), nil, resBody)
if err != nil {
return nil, fmt.Errorf("error retrieving VM: %w", err)
}
if resBody.Data == nil {
return nil, api.ErrNoDataObjectInResponse
}
return resBody.Data, nil
}
// GetVMNetworkInterfacesFromAgent retrieves the network interfaces reported by the QEMU agent.
func (c *Client) GetVMNetworkInterfacesFromAgent(ctx context.Context) (*GetQEMUNetworkInterfacesResponseData, error) {
resBody := &GetQEMUNetworkInterfacesResponseBody{}
err := c.DoRequest(ctx, http.MethodGet, c.ExpandPath("agent/network-get-interfaces"), nil, resBody)
if err != nil {
return nil, fmt.Errorf("error retrieving VM network interfaces from agent: %w", err)
}
if resBody.Data == nil {
return nil, api.ErrNoDataObjectInResponse
}
return resBody.Data, nil
}
// GetVMStatus retrieves the status for a virtual machine.
func (c *Client) GetVMStatus(ctx context.Context) (*GetStatusResponseData, error) {
resBody := &GetStatusResponseBody{}
err := c.DoRequest(ctx, http.MethodGet, c.ExpandPath("status/current"), nil, resBody)
if err != nil {
return nil, fmt.Errorf("error retrieving VM status: %w", err)
}
if resBody.Data == nil {
return nil, api.ErrNoDataObjectInResponse
}
return resBody.Data, nil
}
// MigrateVM migrates a virtual machine.
func (c *Client) MigrateVM(ctx context.Context, d *MigrateRequestBody, timeout int) error {
taskID, err := c.MigrateVMAsync(ctx, d)
if err != nil {
return err
}
err = c.Tasks().WaitForTask(ctx, *taskID, timeout, 5)
if err != nil {
return fmt.Errorf("error waiting for VM migration: %w", err)
}
return nil
}
// MigrateVMAsync migrates a virtual machine asynchronously.
func (c *Client) MigrateVMAsync(ctx context.Context, d *MigrateRequestBody) (*string, error) {
resBody := &MigrateResponseBody{}
err := c.DoRequest(ctx, http.MethodPost, c.ExpandPath("migrate"), d, resBody)
if err != nil {
return nil, fmt.Errorf("error migrating VM: %w", err)
}
if resBody.Data == nil {
return nil, api.ErrNoDataObjectInResponse
}
return resBody.Data, nil
}
// MoveVMDisk moves a virtual machine disk.
func (c *Client) MoveVMDisk(ctx context.Context, d *MoveDiskRequestBody, timeout int) error {
taskID, err := c.MoveVMDiskAsync(ctx, d)
if err != nil {
if strings.Contains(err.Error(), "you can't move to the same storage with same format") {
// if someone tries to move to the same storage, the move is considered to be successful
return nil
}
return err
}
err = c.Tasks().WaitForTask(ctx, *taskID, timeout, 5)
if err != nil {
return fmt.Errorf("error waiting for VM disk move: %w", err)
}
return nil
}
// MoveVMDiskAsync moves a virtual machine disk asynchronously.
func (c *Client) MoveVMDiskAsync(ctx context.Context, d *MoveDiskRequestBody) (*string, error) {
resBody := &MoveDiskResponseBody{}
err := c.DoRequest(ctx, http.MethodPost, c.ExpandPath("move_disk"), d, resBody)
if err != nil {
return nil, fmt.Errorf("error moving VM disk: %w", err)
}
if resBody.Data == nil {
return nil, api.ErrNoDataObjectInResponse
}
return resBody.Data, nil
}
// ListVMs retrieves a list of virtual machines.
func (c *Client) ListVMs(ctx context.Context) ([]*ListResponseData, error) {
resBody := &ListResponseBody{}
err := c.DoRequest(ctx, http.MethodGet, c.basePath(), nil, resBody)
if err != nil {
return nil, fmt.Errorf("error retrieving VMs: %w", err)
}
if resBody.Data == nil {
return nil, api.ErrNoDataObjectInResponse
}
return resBody.Data, nil
}
// RebootVM reboots a virtual machine.
func (c *Client) RebootVM(ctx context.Context, d *RebootRequestBody, timeout int) error {
taskID, err := c.RebootVMAsync(ctx, d)
if err != nil {
return err
}
err = c.Tasks().WaitForTask(ctx, *taskID, timeout, 5)
if err != nil {
return fmt.Errorf("error waiting for VM reboot: %w", err)
}
return nil
}
// RebootVMAsync reboots a virtual machine asynchronously.
func (c *Client) RebootVMAsync(ctx context.Context, d *RebootRequestBody) (*string, error) {
resBody := &RebootResponseBody{}
err := c.DoRequest(ctx, http.MethodPost, c.ExpandPath("status/reboot"), d, resBody)
if err != nil {
return nil, fmt.Errorf("error rebooting VM: %w", err)
}
if resBody.Data == nil {
return nil, api.ErrNoDataObjectInResponse
}
return resBody.Data, nil
}
// ResizeVMDisk resizes a virtual machine disk.
func (c *Client) ResizeVMDisk(ctx context.Context, d *ResizeDiskRequestBody) error {
var err error
tflog.Debug(ctx, "resize disk", map[string]interface{}{
"disk": d.Disk,
"size": d.Size,
})
for i := 0; i < 5; i++ {
err = c.DoRequest(
ctx,
http.MethodPut,
c.ExpandPath("resize"),
d,
nil,
)
if err == nil {
return nil
}
tflog.Debug(ctx, "resize disk failed", map[string]interface{}{
"retry": i,
})
time.Sleep(5 * time.Second)
if ctx.Err() != nil {
return fmt.Errorf("error resizing VM disk: %w", ctx.Err())
}
}
if err != nil {
return fmt.Errorf("error resizing VM disk: %w", err)
}
return nil
}
// ShutdownVM shuts down a virtual machine.
func (c *Client) ShutdownVM(ctx context.Context, d *ShutdownRequestBody, timeout int) error {
taskID, err := c.ShutdownVMAsync(ctx, d)
if err != nil {
return err
}
err = c.Tasks().WaitForTask(ctx, *taskID, timeout, 5)
if err != nil {
return fmt.Errorf("error waiting for VM shutdown: %w", err)
}
return nil
}
// ShutdownVMAsync shuts down a virtual machine asynchronously.
func (c *Client) ShutdownVMAsync(ctx context.Context, d *ShutdownRequestBody) (*string, error) {
resBody := &ShutdownResponseBody{}
err := c.DoRequest(ctx, http.MethodPost, c.ExpandPath("status/shutdown"), d, resBody)
if err != nil {
return nil, fmt.Errorf("error shutting down VM: %w", err)
}
if resBody.Data == nil {
return nil, api.ErrNoDataObjectInResponse
}
return resBody.Data, nil
}
// StartVM starts a virtual machine.
func (c *Client) StartVM(ctx context.Context, timeout int) error {
taskID, err := c.StartVMAsync(ctx)
if err != nil {
return err
}
err = c.Tasks().WaitForTask(ctx, *taskID, timeout, 5)
if err != nil {
return fmt.Errorf("error waiting for VM start: %w", err)
}
return nil
}
// StartVMAsync starts a virtual machine asynchronously.
func (c *Client) StartVMAsync(ctx context.Context) (*string, error) {
resBody := &StartResponseBody{}
err := c.DoRequest(ctx, http.MethodPost, c.ExpandPath("status/start"), nil, resBody)
if err != nil {
return nil, fmt.Errorf("error starting VM: %w", err)
}
if resBody.Data == nil {
return nil, api.ErrNoDataObjectInResponse
}
return resBody.Data, nil
}
// StopVM stops a virtual machine.
func (c *Client) StopVM(ctx context.Context, timeout int) error {
taskID, err := c.StopVMAsync(ctx)
if err != nil {
return err
}
err = c.Tasks().WaitForTask(ctx, *taskID, timeout, 5)
if err != nil {
return fmt.Errorf("error waiting for VM stop: %w", err)
}
return nil
}
// StopVMAsync stops a virtual machine asynchronously.
func (c *Client) StopVMAsync(ctx context.Context) (*string, error) {
resBody := &StopResponseBody{}
err := c.DoRequest(ctx, http.MethodPost, c.ExpandPath("status/stop"), nil, resBody)
if err != nil {
return nil, fmt.Errorf("error stopping VM: %w", err)
}
if resBody.Data == nil {
return nil, api.ErrNoDataObjectInResponse
}
return resBody.Data, nil
}
// UpdateVM updates a virtual machine.
func (c *Client) UpdateVM(ctx context.Context, d *UpdateRequestBody) error {
err := c.DoRequest(ctx, http.MethodPut, c.ExpandPath("config"), d, nil)
if err != nil {
return fmt.Errorf("error updating VM: %w", err)
}
return nil
}
// UpdateVMAsync updates a virtual machine asynchronously.
func (c *Client) UpdateVMAsync(ctx context.Context, d *UpdateRequestBody) (*string, error) {
resBody := &UpdateAsyncResponseBody{}
err := c.DoRequest(ctx, http.MethodPost, c.ExpandPath("config"), d, resBody)
if err != nil {
return nil, fmt.Errorf("error updating VM: %w", err)
}
if resBody.Data == nil {
return nil, api.ErrNoDataObjectInResponse
}
return resBody.Data, nil
}
// WaitForNetworkInterfacesFromVMAgent waits for a virtual machine's QEMU agent to publish the network interfaces.
func (c *Client) WaitForNetworkInterfacesFromVMAgent(
ctx context.Context,
timeout int,
delay int,
waitForIP bool,
) (*GetQEMUNetworkInterfacesResponseData, error) {
timeDelay := int64(delay)
timeMax := float64(timeout)
timeStart := time.Now()
timeElapsed := timeStart.Sub(timeStart)
for timeElapsed.Seconds() < timeMax {
//nolint:nestif
if int64(timeElapsed.Seconds())%timeDelay == 0 {
data, err := c.GetVMNetworkInterfacesFromAgent(ctx)
if err == nil && data != nil && data.Result != nil {
hasAnyGlobalUnicast := false
if waitForIP {
for _, nic := range *data.Result {
if nic.Name == "lo" {
continue
}
if nic.IPAddresses == nil ||
(nic.IPAddresses != nil && len(*nic.IPAddresses) == 0) {
break
}
for _, addr := range *nic.IPAddresses {
if ip := net.ParseIP(addr.Address); ip != nil && ip.IsGlobalUnicast() {
hasAnyGlobalUnicast = true
}
}
}
}
if hasAnyGlobalUnicast {
return data, err
}
}
time.Sleep(1 * time.Second)
}
time.Sleep(200 * time.Millisecond)
timeElapsed = time.Since(timeStart)
if ctx.Err() != nil {
return nil, fmt.Errorf("error waiting for VM network interfaces: %w", ctx.Err())
}
}
return nil, fmt.Errorf(
"timeout while waiting for the QEMU agent on VM \"%d\" to publish the network interfaces",
c.VMID,
)
}
// WaitForNoNetworkInterfacesFromVMAgent waits for a virtual machine's QEMU agent to unpublish the network interfaces.
func (c *Client) WaitForNoNetworkInterfacesFromVMAgent(ctx context.Context, timeout int, delay int) error {
timeDelay := int64(delay)
timeMax := float64(timeout)
timeStart := time.Now()
timeElapsed := timeStart.Sub(timeStart)
for timeElapsed.Seconds() < timeMax {
if int64(timeElapsed.Seconds())%timeDelay == 0 {
_, err := c.GetVMNetworkInterfacesFromAgent(ctx)
if err == nil {
return nil
}
time.Sleep(1 * time.Second)
}
time.Sleep(200 * time.Millisecond)
timeElapsed = time.Since(timeStart)
if ctx.Err() != nil {
return fmt.Errorf("error waiting for VM network interfaces: %w", ctx.Err())
}
}
return fmt.Errorf(
"timeout while waiting for the QEMU agent on VM \"%d\" to unpublish the network interfaces",
c.VMID,
)
}
// WaitForVMConfigUnlock waits for a virtual machine configuration to become unlocked.
func (c *Client) WaitForVMConfigUnlock(ctx context.Context, timeout int, delay int, ignoreErrorResponse bool) error {
timeDelay := int64(delay)
timeMax := float64(timeout)
timeStart := time.Now()
timeElapsed := timeStart.Sub(timeStart)
for timeElapsed.Seconds() < timeMax {
if int64(timeElapsed.Seconds())%timeDelay == 0 {
data, err := c.GetVMStatus(ctx)
if err != nil {
if !ignoreErrorResponse {
return err
}
} else if data.Lock == nil || *data.Lock == "" {
return nil
}
time.Sleep(1 * time.Second)
}
time.Sleep(200 * time.Millisecond)
timeElapsed = time.Since(timeStart)
if ctx.Err() != nil {
return fmt.Errorf("error waiting for VM configuration to become unlocked: %w", ctx.Err())
}
}
return fmt.Errorf("timeout while waiting for VM \"%d\" configuration to become unlocked", c.VMID)
}
// WaitForVMState waits for a virtual machine to reach a specific state.
func (c *Client) WaitForVMState(ctx context.Context, state string, timeout int, delay int) error {
state = strings.ToLower(state)
timeDelay := int64(delay)
timeMax := float64(timeout)
timeStart := time.Now()
timeElapsed := timeStart.Sub(timeStart)
for timeElapsed.Seconds() < timeMax {
if int64(timeElapsed.Seconds())%timeDelay == 0 {
data, err := c.GetVMStatus(ctx)
if err != nil {
return err
}
if data.Status == state {
return nil
}
time.Sleep(1 * time.Second)
}
time.Sleep(200 * time.Millisecond)
timeElapsed = time.Since(timeStart)
if ctx.Err() != nil {
return fmt.Errorf("error waiting for VM state: %w", ctx.Err())
}
}
return fmt.Errorf("timeout while waiting for VM \"%d\" to enter the state \"%s\"", c.VMID, state)
}

View File

@ -4,7 +4,7 @@
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
*/
package proxmox
package vms
import (
"encoding/json"
@ -35,6 +35,7 @@ type CustomAudioDevice struct {
// CustomAudioDevices handles QEMU audio device parameters.
type CustomAudioDevices []CustomAudioDevice
// CustomBoot handles QEMU boot parameters.
type CustomBoot struct {
Order *[]string `json:"order,omitempty" url:"order,omitempty,semicolon"`
}
@ -218,8 +219,8 @@ type CustomWatchdogDevice struct {
Model *string `json:"model" url:"model"`
}
// VirtualEnvironmentVMCloneRequestBody contains the data for an virtual machine clone request.
type VirtualEnvironmentVMCloneRequestBody struct {
// CloneRequestBody contains the data for an virtual machine clone request.
type CloneRequestBody struct {
BandwidthLimit *int `json:"bwlimit,omitempty" url:"bwlimit,omitempty"`
Description *string `json:"description,omitempty" url:"description,omitempty"`
FullCopy *types.CustomBool `json:"full,omitempty" url:"full,omitempty,int"`
@ -232,8 +233,8 @@ type VirtualEnvironmentVMCloneRequestBody struct {
VMIDNew int `json:"newid" url:"newid"`
}
// VirtualEnvironmentVMCreateRequestBody contains the data for a virtual machine create request.
type VirtualEnvironmentVMCreateRequestBody struct {
// CreateRequestBody contains the data for a virtual machine create request.
type CreateRequestBody struct {
ACPI *types.CustomBool `json:"acpi,omitempty" url:"acpi,omitempty,int"`
Agent *CustomAgent `json:"agent,omitempty" url:"agent,omitempty"`
AllowReboot *types.CustomBool `json:"reboot,omitempty" url:"reboot,omitempty,int"`
@ -305,37 +306,38 @@ type VirtualEnvironmentVMCreateRequestBody struct {
WatchdogDevice *CustomWatchdogDevice `json:"watchdog,omitempty" url:"watchdog,omitempty"`
}
type VirtualEnvironmentVMCreateResponseBody struct {
// CreateResponseBody contains the body from a create response.
type CreateResponseBody struct {
Data *string `json:"data,omitempty"`
}
// VirtualEnvironmentVMGetQEMUNetworkInterfacesResponseBody contains the body from a QEMU get network interfaces response.
type VirtualEnvironmentVMGetQEMUNetworkInterfacesResponseBody struct {
Data *VirtualEnvironmentVMGetQEMUNetworkInterfacesResponseData `json:"data,omitempty"`
// GetQEMUNetworkInterfacesResponseBody contains the body from a QEMU get network interfaces response.
type GetQEMUNetworkInterfacesResponseBody struct {
Data *GetQEMUNetworkInterfacesResponseData `json:"data,omitempty"`
}
// VirtualEnvironmentVMGetQEMUNetworkInterfacesResponseData contains the data from a QEMU get network interfaces response.
type VirtualEnvironmentVMGetQEMUNetworkInterfacesResponseData struct {
Result *[]VirtualEnvironmentVMGetQEMUNetworkInterfacesResponseResult `json:"result,omitempty"`
// GetQEMUNetworkInterfacesResponseData contains the data from a QEMU get network interfaces response.
type GetQEMUNetworkInterfacesResponseData struct {
Result *[]GetQEMUNetworkInterfacesResponseResult `json:"result,omitempty"`
}
// VirtualEnvironmentVMGetQEMUNetworkInterfacesResponseResult contains the result from a QEMU get network interfaces response.
type VirtualEnvironmentVMGetQEMUNetworkInterfacesResponseResult struct {
MACAddress string `json:"hardware-address"`
Name string `json:"name"`
Statistics *VirtualEnvironmentVMGetQEMUNetworkInterfacesResponseResultStatistics `json:"statistics,omitempty"`
IPAddresses *[]VirtualEnvironmentVMGetQEMUNetworkInterfacesResponseResultIPAddress `json:"ip-addresses,omitempty"`
// GetQEMUNetworkInterfacesResponseResult contains the result from a QEMU get network interfaces response.
type GetQEMUNetworkInterfacesResponseResult struct {
MACAddress string `json:"hardware-address"`
Name string `json:"name"`
Statistics *GetQEMUNetworkInterfacesResponseResultStatistics `json:"statistics,omitempty"`
IPAddresses *[]GetQEMUNetworkInterfacesResponseResultIPAddress `json:"ip-addresses,omitempty"`
}
// VirtualEnvironmentVMGetQEMUNetworkInterfacesResponseResultIPAddress contains the IP address from a QEMU get network interfaces response.
type VirtualEnvironmentVMGetQEMUNetworkInterfacesResponseResultIPAddress struct {
// GetQEMUNetworkInterfacesResponseResultIPAddress contains the IP address from a QEMU get network interfaces response.
type GetQEMUNetworkInterfacesResponseResultIPAddress struct {
Address string `json:"ip-address"`
Prefix int `json:"prefix"`
Type string `json:"ip-address-type"`
}
// VirtualEnvironmentVMGetQEMUNetworkInterfacesResponseResultStatistics contains the statistics from a QEMU get network interfaces response.
type VirtualEnvironmentVMGetQEMUNetworkInterfacesResponseResultStatistics struct {
// GetQEMUNetworkInterfacesResponseResultStatistics contains the statistics from a QEMU get network interfaces response.
type GetQEMUNetworkInterfacesResponseResultStatistics struct {
RXBytes int `json:"rx-bytes"`
RXDropped int `json:"rx-dropped"`
RXErrors int `json:"rx-errs"`
@ -346,13 +348,13 @@ type VirtualEnvironmentVMGetQEMUNetworkInterfacesResponseResultStatistics struct
TXPackets int `json:"tx-packets"`
}
// VirtualEnvironmentVMGetResponseBody contains the body from a virtual machine get response.
type VirtualEnvironmentVMGetResponseBody struct {
Data *VirtualEnvironmentVMGetResponseData `json:"data,omitempty"`
// GetResponseBody contains the body from a virtual machine get response.
type GetResponseBody struct {
Data *GetResponseData `json:"data,omitempty"`
}
// VirtualEnvironmentVMGetResponseData contains the data from an virtual machine get response.
type VirtualEnvironmentVMGetResponseData struct {
// GetResponseData contains the data from an virtual machine get response.
type GetResponseData struct {
ACPI *types.CustomBool `json:"acpi,omitempty"`
Agent *CustomAgent `json:"agent,omitempty"`
AllowReboot *types.CustomBool `json:"reboot,omitempty"`
@ -486,13 +488,13 @@ type VirtualEnvironmentVMGetResponseData struct {
WatchdogDevice *CustomWatchdogDevice `json:"watchdog,omitempty"`
}
// VirtualEnvironmentVMGetStatusResponseBody contains the body from a VM get status response.
type VirtualEnvironmentVMGetStatusResponseBody struct {
Data *VirtualEnvironmentVMGetStatusResponseData `json:"data,omitempty"`
// GetStatusResponseBody contains the body from a VM get status response.
type GetStatusResponseBody struct {
Data *GetStatusResponseData `json:"data,omitempty"`
}
// VirtualEnvironmentVMGetStatusResponseData contains the data from a VM get status response.
type VirtualEnvironmentVMGetStatusResponseData struct {
// GetStatusResponseData contains the data from a VM get status response.
type GetStatusResponseData struct {
AgentEnabled *types.CustomBool `json:"agent,omitempty"`
CPUCount *float64 `json:"cpus,omitempty"`
Lock *string `json:"lock,omitempty"`
@ -508,33 +510,33 @@ type VirtualEnvironmentVMGetStatusResponseData struct {
VMID *int `json:"vmid,omitempty"`
}
// VirtualEnvironmentVMListResponseBody contains the body from an virtual machine list response.
type VirtualEnvironmentVMListResponseBody struct {
Data []*VirtualEnvironmentVMListResponseData `json:"data,omitempty"`
// ListResponseBody contains the body from a virtual machine list response.
type ListResponseBody struct {
Data []*ListResponseData `json:"data,omitempty"`
}
// VirtualEnvironmentVMListResponseData contains the data from an virtual machine list response.
type VirtualEnvironmentVMListResponseData struct {
// ListResponseData contains the data from an virtual machine list response.
type ListResponseData struct {
Name *string `json:"name,omitempty"`
Tags *string `json:"tags,omitempty"`
VMID int `json:"vmid,omitempty"`
}
// VirtualEnvironmentVMMigrateRequestBody contains the body for a VM migration request.
type VirtualEnvironmentVMMigrateRequestBody struct {
// MigrateRequestBody contains the body for a VM migration request.
type MigrateRequestBody struct {
OnlineMigration *types.CustomBool `json:"online,omitempty" url:"online,omitempty"`
TargetNode string `json:"target" url:"target"`
TargetStorage *string `json:"targetstorage,omitempty" url:"targetstorage,omitempty"`
WithLocalDisks *types.CustomBool `json:"with-local-disks,omitempty" url:"with-local-disks,omitempty,int"`
}
// VirtualEnvironmentVMMigrateResponseBody contains the body from a VM migrate response.
type VirtualEnvironmentVMMigrateResponseBody struct {
// MigrateResponseBody contains the body from a VM migrate response.
type MigrateResponseBody struct {
Data *string `json:"data,omitempty"`
}
// VirtualEnvironmentVMMoveDiskRequestBody contains the body for a VM move disk request.
type VirtualEnvironmentVMMoveDiskRequestBody struct {
// MoveDiskRequestBody contains the body for a VM move disk request.
type MoveDiskRequestBody struct {
BandwidthLimit *int `json:"bwlimit,omitempty" url:"bwlimit,omitempty"`
DeleteOriginalDisk *types.CustomBool `json:"delete,omitempty" url:"delete,omitempty,int"`
Digest *string `json:"digest,omitempty" url:"digest,omitempty"`
@ -543,59 +545,59 @@ type VirtualEnvironmentVMMoveDiskRequestBody struct {
TargetStorageFormat *string `json:"format,omitempty" url:"format,omitempty"`
}
// VirtualEnvironmentVMMoveDiskResponseBody contains the body from a VM move disk response.
type VirtualEnvironmentVMMoveDiskResponseBody struct {
// MoveDiskResponseBody contains the body from a VM move disk response.
type MoveDiskResponseBody struct {
Data *string `json:"data,omitempty"`
}
// VirtualEnvironmentVMRebootRequestBody contains the body for a VM reboot request.
type VirtualEnvironmentVMRebootRequestBody struct {
// RebootRequestBody contains the body for a VM reboot request.
type RebootRequestBody struct {
Timeout *int `json:"timeout,omitempty" url:"timeout,omitempty"`
}
// VirtualEnvironmentVMRebootResponseBody contains the body from a VM reboot response.
type VirtualEnvironmentVMRebootResponseBody struct {
// RebootResponseBody contains the body from a VM reboot response.
type RebootResponseBody struct {
Data *string `json:"data,omitempty"`
}
// VirtualEnvironmentVMResizeDiskRequestBody contains the body for a VM resize disk request.
type VirtualEnvironmentVMResizeDiskRequestBody struct {
// ResizeDiskRequestBody contains the body for a VM resize disk request.
type ResizeDiskRequestBody struct {
Digest *string `json:"digest,omitempty" url:"digest,omitempty"`
Disk string `json:"disk" url:"disk"`
Size types.DiskSize `json:"size" url:"size"`
SkipLock *types.CustomBool `json:"skiplock,omitempty" url:"skiplock,omitempty,int"`
}
// VirtualEnvironmentVMShutdownRequestBody contains the body for a VM shutdown request.
type VirtualEnvironmentVMShutdownRequestBody struct {
// ShutdownRequestBody contains the body for a VM shutdown request.
type ShutdownRequestBody struct {
ForceStop *types.CustomBool `json:"forceStop,omitempty" url:"forceStop,omitempty,int"`
KeepActive *types.CustomBool `json:"keepActive,omitempty" url:"keepActive,omitempty,int"`
SkipLock *types.CustomBool `json:"skipLock,omitempty" url:"skipLock,omitempty,int"`
Timeout *int `json:"timeout,omitempty" url:"timeout,omitempty"`
}
// VirtualEnvironmentVMShutdownResponseBody contains the body from a VM shutdown response.
type VirtualEnvironmentVMShutdownResponseBody struct {
// ShutdownResponseBody contains the body from a VM shutdown response.
type ShutdownResponseBody struct {
Data *string `json:"data,omitempty"`
}
// VirtualEnvironmentVMStartResponseBody contains the body from a VM start response.
type VirtualEnvironmentVMStartResponseBody struct {
// StartResponseBody contains the body from a VM start response.
type StartResponseBody struct {
Data *string `json:"data,omitempty"`
}
// VirtualEnvironmentVMStopResponseBody contains the body from a VM stop response.
type VirtualEnvironmentVMStopResponseBody struct {
// StopResponseBody contains the body from a VM stop response.
type StopResponseBody struct {
Data *string `json:"data,omitempty"`
}
// VirtualEnvironmentVMUpdateAsyncResponseBody contains the body from a VM async update response.
type VirtualEnvironmentVMUpdateAsyncResponseBody struct {
// UpdateAsyncResponseBody contains the body from a VM async update response.
type UpdateAsyncResponseBody struct {
Data *string `json:"data,omitempty"`
}
// VirtualEnvironmentVMUpdateRequestBody contains the data for an virtual machine update request.
type VirtualEnvironmentVMUpdateRequestBody VirtualEnvironmentVMCreateRequestBody
// UpdateRequestBody contains the data for an virtual machine update request.
type UpdateRequestBody CreateRequestBody
// EncodeValues converts a CustomAgent struct to a URL vlaue.
func (r CustomAgent) EncodeValues(key string, v *url.Values) error {
@ -645,9 +647,8 @@ func (r CustomAudioDevice) EncodeValues(key string, v *url.Values) error {
func (r CustomAudioDevices) EncodeValues(key string, v *url.Values) error {
for i, d := range r {
if d.Enabled {
err := d.EncodeValues(fmt.Sprintf("%s%d", key, i), v)
if err != nil {
return err
if err := d.EncodeValues(fmt.Sprintf("%s%d", key, i), v); err != nil {
return fmt.Errorf("unable to encode audio device %d: %w", i, err)
}
}
}
@ -655,6 +656,7 @@ func (r CustomAudioDevices) EncodeValues(key string, v *url.Values) error {
return nil
}
// EncodeValues converts a CustomBoot struct to multiple URL values.
func (r CustomBoot) EncodeValues(key string, v *url.Values) error {
if r.Order != nil && len(*r.Order) > 0 {
v.Add(key, fmt.Sprintf("order=%s", strings.Join(*r.Order, ";")))
@ -665,6 +667,7 @@ func (r CustomBoot) EncodeValues(key string, v *url.Values) error {
// EncodeValues converts a CustomCloudInitConfig struct to multiple URL values.
func (r CustomCloudInitConfig) EncodeValues(_ string, v *url.Values) error {
//nolint:nestif
if r.Files != nil {
var volumes []string
@ -830,6 +833,7 @@ func (r CustomNetworkDevice) EncodeValues(key string, v *url.Values) error {
if r.Tag != nil {
values = append(values, fmt.Sprintf("tag=%d", *r.Tag))
}
if r.MTU != nil {
values = append(values, fmt.Sprintf("mtu=%d", *r.MTU))
}
@ -853,9 +857,8 @@ func (r CustomNetworkDevice) EncodeValues(key string, v *url.Values) error {
func (r CustomNetworkDevices) EncodeValues(key string, v *url.Values) error {
for i, d := range r {
if d.Enabled {
err := d.EncodeValues(fmt.Sprintf("%s%d", key, i), v)
if err != nil {
return err
if err := d.EncodeValues(fmt.Sprintf("%s%d", key, i), v); err != nil {
return fmt.Errorf("failed to encode network device %d: %w", i, err)
}
}
}
@ -889,9 +892,8 @@ func (r CustomNUMADevice) EncodeValues(key string, v *url.Values) error {
// EncodeValues converts a CustomNUMADevices array to multiple URL values.
func (r CustomNUMADevices) EncodeValues(key string, v *url.Values) error {
for i, d := range r {
err := d.EncodeValues(fmt.Sprintf("%s%d", key, i), v)
if err != nil {
return err
if err := d.EncodeValues(fmt.Sprintf("%s%d", key, i), v); err != nil {
return fmt.Errorf("failed to encode NUMA device %d: %w", i, err)
}
}
@ -944,9 +946,8 @@ func (r CustomPCIDevice) EncodeValues(key string, v *url.Values) error {
// EncodeValues converts a CustomPCIDevices array to multiple URL values.
func (r CustomPCIDevices) EncodeValues(key string, v *url.Values) error {
for i, d := range r {
err := d.EncodeValues(fmt.Sprintf("%s%d", key, i), v)
if err != nil {
return err
if err := d.EncodeValues(fmt.Sprintf("%s%d", key, i), v); err != nil {
return fmt.Errorf("failed to encode PCI device %d: %w", i, err)
}
}
@ -1143,11 +1144,10 @@ func (r CustomStorageDevice) EncodeValues(key string, v *url.Values) error {
// EncodeValues converts a CustomStorageDevices array to multiple URL values.
func (r CustomStorageDevices) EncodeValues(_ string, v *url.Values) error {
for i, d := range r {
for s, d := range r {
if d.Enabled {
err := d.EncodeValues(i, v)
if err != nil {
return err
if err := d.EncodeValues(s, v); err != nil {
return fmt.Errorf("error encoding storage device %s: %w", s, err)
}
}
}
@ -1177,9 +1177,8 @@ func (r CustomUSBDevice) EncodeValues(key string, v *url.Values) error {
// EncodeValues converts a CustomUSBDevices array to multiple URL values.
func (r CustomUSBDevices) EncodeValues(key string, v *url.Values) error {
for i, d := range r {
err := d.EncodeValues(fmt.Sprintf("%s%d", key, i), v)
if err != nil {
return err
if err := d.EncodeValues(fmt.Sprintf("%s%d", key, i), v); err != nil {
return fmt.Errorf("error encoding USB device %d: %w", i, err)
}
}
@ -1230,9 +1229,8 @@ func (r CustomVirtualIODevice) EncodeValues(key string, v *url.Values) error {
func (r CustomVirtualIODevices) EncodeValues(key string, v *url.Values) error {
for i, d := range r {
if d.Enabled {
err := d.EncodeValues(fmt.Sprintf("%s%d", key, i), v)
if err != nil {
return err
if err := d.EncodeValues(fmt.Sprintf("%s%d", key, i), v); err != nil {
return fmt.Errorf("error encoding virtual IO device %d: %w", i, err)
}
}
}
@ -1259,9 +1257,8 @@ func (r CustomWatchdogDevice) EncodeValues(key string, v *url.Values) error {
func (r *CustomAgent) UnmarshalJSON(b []byte) error {
var s string
err := json.Unmarshal(b, &s)
if err != nil {
return err
if err := json.Unmarshal(b, &s); err != nil {
return fmt.Errorf("error unmarshalling CustomAgent: %w", err)
}
pairs := strings.Split(s, ",")
@ -1293,9 +1290,8 @@ func (r *CustomAgent) UnmarshalJSON(b []byte) error {
func (r *CustomAudioDevice) UnmarshalJSON(b []byte) error {
var s string
err := json.Unmarshal(b, &s)
if err != nil {
return err
if err := json.Unmarshal(b, &s); err != nil {
return fmt.Errorf("error unmarshalling CustomAudioDevice: %w", err)
}
pairs := strings.Split(s, ",")
@ -1320,8 +1316,7 @@ func (r *CustomAudioDevice) UnmarshalJSON(b []byte) error {
func (r *CustomBoot) UnmarshalJSON(b []byte) error {
var s string
err := json.Unmarshal(b, &s)
if err != nil {
if err := json.Unmarshal(b, &s); err != nil {
return fmt.Errorf("error unmarshalling CustomBoot: %w", err)
}
@ -1332,8 +1327,8 @@ func (r *CustomBoot) UnmarshalJSON(b []byte) error {
if len(v) == 2 {
if v[0] == "order" {
v := strings.Split(strings.TrimSpace(v[1]), ";")
r.Order = &v
o := strings.Split(strings.TrimSpace(v[1]), ";")
r.Order = &o
}
}
}
@ -1345,9 +1340,8 @@ func (r *CustomBoot) UnmarshalJSON(b []byte) error {
func (r *CustomCloudInitFiles) UnmarshalJSON(b []byte) error {
var s string
err := json.Unmarshal(b, &s)
if err != nil {
return err
if err := json.Unmarshal(b, &s); err != nil {
return fmt.Errorf("error unmarshalling CustomCloudInitFiles: %w", err)
}
pairs := strings.Split(s, ",")
@ -1376,9 +1370,8 @@ func (r *CustomCloudInitFiles) UnmarshalJSON(b []byte) error {
func (r *CustomCloudInitIPConfig) UnmarshalJSON(b []byte) error {
var s string
err := json.Unmarshal(b, &s)
if err != nil {
return err
if err := json.Unmarshal(b, &s); err != nil {
return fmt.Errorf("error unmarshalling CustomCloudInitIPConfig: %w", err)
}
pairs := strings.Split(s, ",")
@ -1407,15 +1400,13 @@ func (r *CustomCloudInitIPConfig) UnmarshalJSON(b []byte) error {
func (r *CustomCloudInitSSHKeys) UnmarshalJSON(b []byte) error {
var s string
err := json.Unmarshal(b, &s)
if err != nil {
return err
if err := json.Unmarshal(b, &s); err != nil {
return fmt.Errorf("error unmarshalling CustomCloudInitSSHKeys: %w", err)
}
s, err = url.QueryUnescape(s)
s, err := url.QueryUnescape(s)
if err != nil {
return err
return fmt.Errorf("error unescaping CustomCloudInitSSHKeys: %w", err)
}
if s != "" {
@ -1431,9 +1422,8 @@ func (r *CustomCloudInitSSHKeys) UnmarshalJSON(b []byte) error {
func (r *CustomCPUEmulation) UnmarshalJSON(b []byte) error {
var s string
err := json.Unmarshal(b, &s)
if err != nil {
return err
if err := json.Unmarshal(b, &s); err != nil {
return fmt.Errorf("error unmarshalling CustomCPUEmulation: %w", err)
}
if s == "" {
@ -1475,8 +1465,7 @@ func (r *CustomCPUEmulation) UnmarshalJSON(b []byte) error {
func (r *CustomEFIDisk) UnmarshalJSON(b []byte) error {
var s string
err := json.Unmarshal(b, &s)
if err != nil {
if err := json.Unmarshal(b, &s); err != nil {
return fmt.Errorf("failed to unmarshal CustomEFIDisk: %w", err)
}
@ -1493,6 +1482,7 @@ func (r *CustomEFIDisk) UnmarshalJSON(b []byte) error {
r.FileVolume = v[1]
case "size":
r.Size = new(types.DiskSize)
err := r.Size.UnmarshalJSON([]byte(v[1]))
if err != nil {
return fmt.Errorf("failed to unmarshal disk size: %w", err)
@ -1508,9 +1498,8 @@ func (r *CustomEFIDisk) UnmarshalJSON(b []byte) error {
func (r *CustomNetworkDevice) UnmarshalJSON(b []byte) error {
var s string
err := json.Unmarshal(b, &s)
if err != nil {
return err
if err := json.Unmarshal(b, &s); err != nil {
return fmt.Errorf("failed to unmarshal CustomNetworkDevice: %w", err)
}
pairs := strings.Split(s, ",")
@ -1518,6 +1507,7 @@ func (r *CustomNetworkDevice) UnmarshalJSON(b []byte) error {
for _, p := range pairs {
v := strings.Split(strings.TrimSpace(p), "=")
//nolint:nestif
if len(v) == 2 {
switch v[0] {
case "bridge":
@ -1535,28 +1525,30 @@ func (r *CustomNetworkDevice) UnmarshalJSON(b []byte) error {
case "queues":
iv, err := strconv.Atoi(v[1])
if err != nil {
return err
return fmt.Errorf("failed to parse queues: %w", err)
}
r.Queues = &iv
case "rate":
fv, err := strconv.ParseFloat(v[1], 64)
if err != nil {
return err
return fmt.Errorf("failed to parse rate: %w", err)
}
r.RateLimit = &fv
case "mtu":
iv, err := strconv.Atoi(v[1])
if err != nil {
return err
return fmt.Errorf("failed to parse mtu: %w", err)
}
r.MTU = &iv
case "tag":
iv, err := strconv.Atoi(v[1])
if err != nil {
return err
return fmt.Errorf("failed to parse tag: %w", err)
}
r.Tag = &iv
@ -1567,7 +1559,7 @@ func (r *CustomNetworkDevice) UnmarshalJSON(b []byte) error {
for i, trunk := range trunks {
iv, err := strconv.Atoi(trunk)
if err != nil {
return err
return fmt.Errorf("failed to parse trunk %d: %w", i, err)
}
r.Trunks[i] = iv
@ -1588,9 +1580,8 @@ func (r *CustomNetworkDevice) UnmarshalJSON(b []byte) error {
func (r *CustomPCIDevice) UnmarshalJSON(b []byte) error {
var s string
err := json.Unmarshal(b, &s)
if err != nil {
return err
if err := json.Unmarshal(b, &s); err != nil {
return fmt.Errorf("failed to unmarshal CustomPCIDevice: %w", err)
}
pairs := strings.Split(s, ",")
@ -1627,9 +1618,8 @@ func (r *CustomPCIDevice) UnmarshalJSON(b []byte) error {
func (r *CustomSharedMemory) UnmarshalJSON(b []byte) error {
var s string
err := json.Unmarshal(b, &s)
if err != nil {
return err
if err := json.Unmarshal(b, &s); err != nil {
return fmt.Errorf("failed to unmarshal CustomSharedMemory: %w", err)
}
pairs := strings.Split(s, ",")
@ -1642,10 +1632,11 @@ func (r *CustomSharedMemory) UnmarshalJSON(b []byte) error {
case "name":
r.Name = &v[1]
case "size":
r.Size, err = strconv.Atoi(v[1])
var err error
r.Size, err = strconv.Atoi(v[1])
if err != nil {
return err
return fmt.Errorf("failed to parse shared memory size: %w", err)
}
}
}
@ -1658,9 +1649,8 @@ func (r *CustomSharedMemory) UnmarshalJSON(b []byte) error {
func (r *CustomSMBIOS) UnmarshalJSON(b []byte) error {
var s string
err := json.Unmarshal(b, &s)
if err != nil {
return err
if err := json.Unmarshal(b, &s); err != nil {
return fmt.Errorf("failed to unmarshal CustomSMBIOS: %w", err)
}
pairs := strings.Split(s, ",")
@ -1698,9 +1688,8 @@ func (r *CustomSMBIOS) UnmarshalJSON(b []byte) error {
func (r *CustomStorageDevice) UnmarshalJSON(b []byte) error {
var s string
err := json.Unmarshal(b, &s)
if err != nil {
return err
if err := json.Unmarshal(b, &s); err != nil {
return fmt.Errorf("failed to unmarshal CustomStorageDevice: %w", err)
}
pairs := strings.Split(s, ",")
@ -1708,8 +1697,10 @@ func (r *CustomStorageDevice) UnmarshalJSON(b []byte) error {
for _, p := range pairs {
v := strings.Split(strings.TrimSpace(p), "=")
//nolint:nestif
if len(v) == 1 {
r.FileVolume = v[0]
ext := filepath.Ext(v[0])
if ext != "" {
format := string([]byte(ext)[1:])
@ -1727,28 +1718,28 @@ func (r *CustomStorageDevice) UnmarshalJSON(b []byte) error {
case "mbps_rd":
iv, err := strconv.Atoi(v[1])
if err != nil {
return err
return fmt.Errorf("failed to convert mbps_rd to int: %w", err)
}
r.MaxReadSpeedMbps = &iv
case "mbps_rd_max":
iv, err := strconv.Atoi(v[1])
if err != nil {
return err
return fmt.Errorf("failed to convert mbps_rd_max to int: %w", err)
}
r.BurstableReadSpeedMbps = &iv
case "mbps_wr":
iv, err := strconv.Atoi(v[1])
if err != nil {
return err
return fmt.Errorf("failed to convert mbps_wr to int: %w", err)
}
r.MaxWriteSpeedMbps = &iv
case "mbps_wr_max":
iv, err := strconv.Atoi(v[1])
if err != nil {
return err
return fmt.Errorf("failed to convert mbps_wr_max to int: %w", err)
}
r.BurstableWriteSpeedMbps = &iv
@ -1783,9 +1774,8 @@ func (r *CustomStorageDevice) UnmarshalJSON(b []byte) error {
func (r *CustomVGADevice) UnmarshalJSON(b []byte) error {
var s string
err := json.Unmarshal(b, &s)
if err != nil {
return err
if err := json.Unmarshal(b, &s); err != nil {
return fmt.Errorf("failed to unmarshal CustomVGADevice: %w", err)
}
if s == "" {
@ -1804,7 +1794,7 @@ func (r *CustomVGADevice) UnmarshalJSON(b []byte) error {
case "memory":
m, err := strconv.Atoi(v[1])
if err != nil {
return err
return fmt.Errorf("failed to convert memory to int: %w", err)
}
r.Memory = &m
@ -1821,9 +1811,8 @@ func (r *CustomVGADevice) UnmarshalJSON(b []byte) error {
func (r *CustomWatchdogDevice) UnmarshalJSON(b []byte) error {
var s string
err := json.Unmarshal(b, &s)
if err != nil {
return err
if err := json.Unmarshal(b, &s); err != nil {
return fmt.Errorf("failed to unmarshal CustomWatchdogDevice: %w", err)
}
if s == "" {

View File

@ -4,7 +4,7 @@
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
*/
package proxmox
package vms
import (
"testing"

16
proxmox/pools/client.go Normal file
View File

@ -0,0 +1,16 @@
/*
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
*/
package pools
import (
"github.com/bpg/terraform-provider-proxmox/proxmox/api"
)
// Client is an interface for accessing the Proxmox pools API.
type Client struct {
api.Client
}

87
proxmox/pools/pool.go Normal file
View File

@ -0,0 +1,87 @@
/*
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
*/
package pools
import (
"context"
"fmt"
"net/http"
"net/url"
"sort"
"github.com/bpg/terraform-provider-proxmox/proxmox/api"
)
// CreatePool creates a pool.
func (c *Client) CreatePool(ctx context.Context, d *PoolCreateRequestBody) error {
err := c.DoRequest(ctx, http.MethodPost, "pools", d, nil)
if err != nil {
return fmt.Errorf("error creating pool: %w", err)
}
return nil
}
// DeletePool deletes a pool.
func (c *Client) DeletePool(ctx context.Context, id string) error {
err := c.DoRequest(ctx, http.MethodDelete, fmt.Sprintf("pools/%s", url.PathEscape(id)), nil, nil)
if err != nil {
return fmt.Errorf("error deleting pool: %w", err)
}
return nil
}
// GetPool retrieves a pool.
func (c *Client) GetPool(ctx context.Context, id string) (*PoolGetResponseData, error) {
resBody := &PoolGetResponseBody{}
err := c.DoRequest(ctx, http.MethodGet, fmt.Sprintf("pools/%s", url.PathEscape(id)), nil, resBody)
if err != nil {
return nil, fmt.Errorf("error getting pool: %w", err)
}
if resBody.Data == nil {
return nil, api.ErrNoDataObjectInResponse
}
sort.Slice(resBody.Data.Members, func(i, j int) bool {
return resBody.Data.Members[i].ID < resBody.Data.Members[j].ID
})
return resBody.Data, nil
}
// ListPools retrieves a list of pools.
func (c *Client) ListPools(ctx context.Context) ([]*PoolListResponseData, error) {
resBody := &PoolListResponseBody{}
err := c.DoRequest(ctx, http.MethodGet, "pools", nil, resBody)
if err != nil {
return nil, fmt.Errorf("error listing pools: %w", err)
}
if resBody.Data == nil {
return nil, api.ErrNoDataObjectInResponse
}
sort.Slice(resBody.Data, func(i, j int) bool {
return resBody.Data[i].ID < resBody.Data[j].ID
})
return resBody.Data, nil
}
// UpdatePool updates a pool.
func (c *Client) UpdatePool(ctx context.Context, id string, d *PoolUpdateRequestBody) error {
err := c.DoRequest(ctx, http.MethodPut, fmt.Sprintf("pools/%s", url.PathEscape(id)), d, nil)
if err != nil {
return fmt.Errorf("error updating pool: %w", err)
}
return nil
}

View File

@ -0,0 +1,49 @@
/*
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
*/
package pools
// PoolCreateRequestBody contains the data for a pool create request.
type PoolCreateRequestBody struct {
Comment *string `json:"comment,omitempty" url:"comment,omitempty"`
ID string `json:"groupid" url:"poolid"`
}
// PoolGetResponseBody contains the body from a pool get response.
type PoolGetResponseBody struct {
Data *PoolGetResponseData `json:"data,omitempty"`
}
// PoolGetResponseData contains the data from a pool get response.
type PoolGetResponseData struct {
Comment *string `json:"comment,omitempty"`
Members []VirtualEnvironmentPoolGetResponseMembers `json:"members,omitempty"`
}
// VirtualEnvironmentPoolGetResponseMembers contains the members data from a pool get response.
type VirtualEnvironmentPoolGetResponseMembers struct {
ID string `json:"id"`
Node string `json:"node"`
DatastoreID *string `json:"storage,omitempty"`
Type string `json:"type"`
VMID *int `json:"vmid"`
}
// PoolListResponseBody contains the body from a pool list response.
type PoolListResponseBody struct {
Data []*PoolListResponseData `json:"data,omitempty"`
}
// PoolListResponseData contains the data from a pool list response.
type PoolListResponseData struct {
Comment *string `json:"comment,omitempty"`
ID string `json:"poolid"`
}
// PoolUpdateRequestBody contains the data for an pool update request.
type PoolUpdateRequestBody struct {
Comment *string `json:"comment,omitempty" url:"comment,omitempty"`
}

306
proxmox/ssh/client.go Normal file
View File

@ -0,0 +1,306 @@
/*
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
*/
package ssh
import (
"context"
"errors"
"fmt"
"net"
"os"
"path"
"path/filepath"
"runtime"
"strings"
"github.com/hashicorp/terraform-plugin-log/tflog"
"github.com/pkg/sftp"
"github.com/skeema/knownhosts"
"golang.org/x/crypto/ssh"
"golang.org/x/crypto/ssh/agent"
"github.com/bpg/terraform-provider-proxmox/proxmox/api"
"github.com/bpg/terraform-provider-proxmox/utils"
)
type client struct {
username string
password string
agent bool
agentSocket string
}
// NewClient creates a new SSH client.
func NewClient(username string, password string, agent bool, agentSocket string) (Client, error) {
//goland:noinspection GoBoolExpressions
if agent && runtime.GOOS != "linux" && runtime.GOOS != "darwin" && runtime.GOOS != "freebsd" {
return nil, errors.New(
"the ssh agent flag is only supported on POSIX systems, please set it to 'false'" +
" or remove it from your provider configuration",
)
}
return &client{
username: username,
password: password,
agent: agent,
agentSocket: agentSocket,
}, nil
}
// ExecuteNodeCommands executes commands on a given node.
func (c *client) ExecuteNodeCommands(ctx context.Context, nodeAddress string, commands []string) error {
closeOrLogError := utils.CloseOrLogError(ctx)
sshClient, err := c.openNodeShell(ctx, nodeAddress)
if err != nil {
return err
}
defer closeOrLogError(sshClient)
sshSession, err := sshClient.NewSession()
if err != nil {
return fmt.Errorf("failed to create SSH session: %w", err)
}
defer closeOrLogError(sshSession)
script := strings.Join(commands, " && \\\n")
output, err := sshSession.CombinedOutput(
fmt.Sprintf(
"/bin/bash -c '%s'",
strings.ReplaceAll(script, "'", "'\"'\"'"),
),
)
if err != nil {
return errors.New(string(output))
}
return nil
}
func (c *client) NodeUpload(
ctx context.Context, nodeAddress string, remoteFileDir string,
d *api.FileUploadRequest,
) error {
// We need to upload all other files using SFTP due to API limitations.
// Hopefully, this will not be required in future releases of Proxmox VE.
tflog.Debug(ctx, "uploading file to datastore using SFTP", map[string]interface{}{
"file_name": d.FileName,
"content_type": d.ContentType,
})
fileInfo, err := d.File.Stat()
if err != nil {
return fmt.Errorf("failed to get file info: %w", err)
}
fileSize := fileInfo.Size()
sshClient, err := c.openNodeShell(ctx, nodeAddress)
if err != nil {
return fmt.Errorf("failed to open SSH client: %w", err)
}
defer func(sshClient *ssh.Client) {
e := sshClient.Close()
if e != nil {
tflog.Error(ctx, "failed to close SSH client", map[string]interface{}{
"error": e,
})
}
}(sshClient)
if d.ContentType != "" {
remoteFileDir = filepath.Join(remoteFileDir, d.ContentType)
}
remoteFilePath := strings.ReplaceAll(filepath.Join(remoteFileDir, d.FileName), `\`, `/`)
sftpClient, err := sftp.NewClient(sshClient)
if err != nil {
return fmt.Errorf("failed to create SFTP client: %w", err)
}
defer func(sftpClient *sftp.Client) {
e := sftpClient.Close()
if e != nil {
tflog.Error(ctx, "failed to close SFTP client", map[string]interface{}{
"error": e,
})
}
}(sftpClient)
err = sftpClient.MkdirAll(remoteFileDir)
if err != nil {
return fmt.Errorf("failed to create directory %s: %w", remoteFileDir, err)
}
remoteFile, err := sftpClient.Create(remoteFilePath)
if err != nil {
return fmt.Errorf("failed to create file %s: %w", remoteFilePath, err)
}
defer func(remoteFile *sftp.File) {
e := remoteFile.Close()
if e != nil {
tflog.Error(ctx, "failed to close remote file", map[string]interface{}{
"error": e,
})
}
}(remoteFile)
bytesUploaded, err := remoteFile.ReadFrom(d.File)
if err != nil {
return fmt.Errorf("failed to upload file %s: %w", remoteFilePath, err)
}
if bytesUploaded != fileSize {
return fmt.Errorf("failed to upload file %s: uploaded %d bytes, expected %d bytes",
remoteFilePath, bytesUploaded, fileSize)
}
tflog.Debug(ctx, "uploaded file to datastore", map[string]interface{}{
"remote_file_path": remoteFilePath,
"size": bytesUploaded,
})
return nil
}
// openNodeShell establishes a new SSH connection to a node.
func (c *client) openNodeShell(ctx context.Context, nodeAddress string) (*ssh.Client, error) {
homeDir, err := os.UserHomeDir()
if err != nil {
return nil, fmt.Errorf("failed to determine the home directory: %w", err)
}
sshHost := fmt.Sprintf("%s:22", nodeAddress)
sshPath := path.Join(homeDir, ".ssh")
if _, err = os.Stat(sshPath); os.IsNotExist(err) {
e := os.Mkdir(sshPath, 0o700)
if e != nil {
return nil, fmt.Errorf("failed to create %s: %w", sshPath, e)
}
}
khPath := path.Join(sshPath, "known_hosts")
if _, err = os.Stat(khPath); os.IsNotExist(err) {
e := os.WriteFile(khPath, []byte{}, 0o600)
if e != nil {
return nil, fmt.Errorf("failed to create %s: %w", khPath, e)
}
}
kh, err := knownhosts.New(khPath)
if err != nil {
return nil, fmt.Errorf("failed to read %s: %w", khPath, err)
}
// Create a custom permissive hostkey callback which still errors on hosts
// with changed keys, but allows unknown hosts and adds them to known_hosts
cb := ssh.HostKeyCallback(func(hostname string, remote net.Addr, key ssh.PublicKey) error {
kherr := kh(hostname, remote, key)
if knownhosts.IsHostKeyChanged(kherr) {
return fmt.Errorf("REMOTE HOST IDENTIFICATION HAS CHANGED for host %s! This may indicate a MitM attack", hostname)
}
if knownhosts.IsHostUnknown(kherr) {
f, ferr := os.OpenFile(khPath, os.O_APPEND|os.O_WRONLY, 0o600)
if ferr == nil {
defer utils.CloseOrLogError(ctx)(f)
ferr = knownhosts.WriteKnownHost(f, hostname, remote, key)
}
if ferr == nil {
tflog.Info(ctx, fmt.Sprintf("Added host %s to known_hosts", hostname))
} else {
tflog.Error(ctx, fmt.Sprintf("Failed to add host %s to known_hosts", hostname), map[string]interface{}{
"error": kherr,
})
}
return nil
}
return kherr
})
sshConfig := &ssh.ClientConfig{
User: c.username,
Auth: []ssh.AuthMethod{ssh.Password(c.password)},
HostKeyCallback: cb,
HostKeyAlgorithms: kh.HostKeyAlgorithms(sshHost),
}
tflog.Info(ctx, fmt.Sprintf("Agent is set to %t", c.agent))
var sshClient *ssh.Client
if c.agent {
sshClient, err = c.createSSHClientAgent(ctx, cb, kh, sshHost)
if err != nil {
tflog.Error(ctx, "Failed ssh connection through agent, "+
"falling back to password authentication",
map[string]interface{}{
"error": err,
})
} else {
return sshClient, nil
}
}
sshClient, err = ssh.Dial("tcp", sshHost, sshConfig)
if err != nil {
return nil, fmt.Errorf("failed to dial %s: %w", sshHost, err)
}
tflog.Debug(ctx, "SSH connection established", map[string]interface{}{
"host": sshHost,
"user": c.username,
})
return sshClient, nil
}
// createSSHClientAgent establishes an ssh connection through the agent authentication mechanism.
func (c *client) createSSHClientAgent(
ctx context.Context,
cb ssh.HostKeyCallback,
kh knownhosts.HostKeyCallback,
sshHost string,
) (*ssh.Client, error) {
if c.agentSocket == "" {
return nil, errors.New("failed connecting to SSH agent socket: the socket file is not defined, " +
"authentication will fall back to password")
}
conn, err := net.Dial("unix", c.agentSocket)
if err != nil {
return nil, fmt.Errorf("failed connecting to SSH auth socket '%s': %w", c.agentSocket, err)
}
ag := agent.NewClient(conn)
sshConfig := &ssh.ClientConfig{
User: c.username,
Auth: []ssh.AuthMethod{ssh.PublicKeysCallback(ag.Signers), ssh.Password(c.password)},
HostKeyCallback: cb,
HostKeyAlgorithms: kh.HostKeyAlgorithms(sshHost),
}
sshClient, err := ssh.Dial("tcp", sshHost, sshConfig)
if err != nil {
return nil, fmt.Errorf("failed to dial %s: %w", sshHost, err)
}
tflog.Debug(ctx, "SSH connection established", map[string]interface{}{
"host": sshHost,
"user": c.username,
})
return sshClient, nil
}

View File

@ -0,0 +1,28 @@
/*
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
*/
package ssh
import (
"context"
"github.com/bpg/terraform-provider-proxmox/proxmox/api"
)
// Client is an interface for performing SSH requests against the Proxmox Nodes.
type Client interface {
// ExecuteNodeCommands executes a command on a node.
ExecuteNodeCommands(
ctx context.Context, nodeAddress string,
commands []string,
) error
// NodeUpload uploads a file to a node.
NodeUpload(
ctx context.Context, nodeAddress string,
remoteFileDir string, fileUploadRequest *api.FileUploadRequest,
) error
}

16
proxmox/storage/client.go Normal file
View File

@ -0,0 +1,16 @@
/*
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
*/
package storage
import (
"github.com/bpg/terraform-provider-proxmox/proxmox/api"
)
// Client is an interface for accessing the Proxmox storage API.
type Client struct {
api.Client
}

View File

@ -0,0 +1,54 @@
/*
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
*/
package storage
import (
"context"
"fmt"
"net/http"
"net/url"
)
// GetDatastore retrieves information about a datastore.
/*
Using undocumented API endpoints is not recommended, but sometimes it's the only way to get things done.
$ pvesh get /storage/local
key value
content images,vztmpl,iso,backup,snippets,rootdir
digest 5b65ede80f34631d6039e6922845cfa4abc956be
path /var/lib/vz
shared 0
storage local
type dir
.
*/
func (c *Client) GetDatastore(
ctx context.Context,
datastoreID string,
) (*DatastoreGetResponseData, error) {
resBody := &DatastoreGetResponseBody{}
err := c.DoRequest(
ctx,
http.MethodGet,
fmt.Sprintf("storage/%s", url.PathEscape(datastoreID)),
nil,
resBody,
)
if err != nil {
return nil, fmt.Errorf("error retrieving datastore %s: %w", datastoreID, err)
}
return resBody.Data, nil
}

View File

@ -0,0 +1,26 @@
/*
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
*/
package storage
import (
"github.com/bpg/terraform-provider-proxmox/proxmox/types"
)
// DatastoreGetResponseBody contains the body from a datastore get response.
type DatastoreGetResponseBody struct {
Data *DatastoreGetResponseData `json:"data,omitempty"`
}
// DatastoreGetResponseData contains the data from a datastore get response.
type DatastoreGetResponseData struct {
Content types.CustomCommaSeparatedList `json:"content,omitempty" url:"content,omitempty,comma"`
Digest *string `json:"digest,omitempty"`
Path *string `json:"path,omitempty"`
Shared *types.CustomBool `json:"shared,omitempty"`
Storage *string `json:"storage,omitempty"`
Type *string `json:"type,omitempty"`
}

View File

@ -1,18 +0,0 @@
package types
import "context"
type Client interface {
// DoRequest performs a request against the Proxmox API.
DoRequest(
ctx context.Context,
method, path string,
requestBody, responseBody interface{},
) error
// ExpandPath expands a path relative to the client's base path.
// For example, if the client is configured for a VM and the
// path is "firewall/options", the returned path will be
// "/nodes/<node>/qemu/<vmid>/firewall/options".
ExpandPath(path string) string
}

View File

@ -43,6 +43,7 @@ func (r DiskSize) MarshalJSON() ([]byte, error) {
if err != nil {
return nil, fmt.Errorf("cannot marshal disk size: %w", err)
}
return bytes, nil
}
@ -54,12 +55,13 @@ func (r *DiskSize) UnmarshalJSON(b []byte) error {
if err != nil {
return err
}
*r = DiskSize(size)
return nil
}
// parseDiskSize parses a disk size string into a number of bytes
// parseDiskSize parses a disk size string into a number of bytes.
func parseDiskSize(size *string) (int64, error) {
if size == nil {
return 0, nil
@ -71,6 +73,7 @@ func parseDiskSize(size *string) (int64, error) {
if err != nil {
return -1, fmt.Errorf("cannot parse disk size \"%s\": %w", *size, err)
}
switch strings.ToLower(matches[3]) {
case "k", "kb", "kib":
fsize *= 1024

16
proxmox/version/client.go Normal file
View File

@ -0,0 +1,16 @@
/*
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
*/
package version
import (
"github.com/bpg/terraform-provider-proxmox/proxmox/api"
)
// Client is an interface for accessing the Proxmox version API.
type Client struct {
api.Client
}

View File

@ -0,0 +1,31 @@
/*
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
*/
package version
import (
"context"
"fmt"
"net/http"
"github.com/bpg/terraform-provider-proxmox/proxmox/api"
)
// Version retrieves the version information.
func (c *Client) Version(ctx context.Context) (*ResponseData, error) {
resBody := &ResponseBody{}
err := c.DoRequest(ctx, http.MethodGet, "version", nil, resBody)
if err != nil {
return nil, fmt.Errorf("failed to get version information: %w", err)
}
if resBody.Data == nil {
return nil, api.ErrNoDataObjectInResponse
}
return resBody.Data, nil
}

View File

@ -0,0 +1,20 @@
/*
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
*/
package version
// ResponseBody contains the body from a version response.
type ResponseBody struct {
Data *ResponseData `json:"data,omitempty"`
}
// ResponseData contains the data from a version response.
type ResponseData struct {
Keyboard string `json:"keyboard"`
Release string `json:"release"`
RepositoryID string `json:"repoid"`
Version string `json:"version"`
}

View File

@ -1,41 +0,0 @@
/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at https://mozilla.org/MPL/2.0/. */
package proxmox
import (
"context"
"errors"
"net/http"
"sort"
)
// GetACL retrieves the access control list.
func (c *VirtualEnvironmentClient) GetACL(
ctx context.Context,
) ([]*VirtualEnvironmentACLGetResponseData, error) {
resBody := &VirtualEnvironmentACLGetResponseBody{}
err := c.DoRequest(ctx, http.MethodGet, "access/acl", nil, resBody)
if err != nil {
return nil, err
}
if resBody.Data == nil {
return nil, errors.New("the server did not include a data object in the response")
}
sort.Slice(resBody.Data, func(i, j int) bool {
return resBody.Data[i].Path < resBody.Data[j].Path
})
return resBody.Data, nil
}
// UpdateACL updates the access control list.
func (c *VirtualEnvironmentClient) UpdateACL(
ctx context.Context,
d *VirtualEnvironmentACLUpdateRequestBody,
) error {
return c.DoRequest(ctx, http.MethodPut, "access/acl", d, nil)
}

View File

@ -1,30 +0,0 @@
/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at https://mozilla.org/MPL/2.0/. */
package proxmox
import "github.com/bpg/terraform-provider-proxmox/proxmox/types"
// VirtualEnvironmentAuthenticationResponseBody contains the body from an authentication response.
type VirtualEnvironmentAuthenticationResponseBody struct {
Data *VirtualEnvironmentAuthenticationResponseData `json:"data,omitempty"`
}
// VirtualEnvironmentAuthenticationResponseCapabilities contains the supported capabilities for a session.
type VirtualEnvironmentAuthenticationResponseCapabilities struct {
Access *types.CustomPrivileges `json:"access,omitempty"`
Datacenter *types.CustomPrivileges `json:"dc,omitempty"`
Nodes *types.CustomPrivileges `json:"nodes,omitempty"`
Storage *types.CustomPrivileges `json:"storage,omitempty"`
VMs *types.CustomPrivileges `json:"vms,omitempty"`
}
// VirtualEnvironmentAuthenticationResponseData contains the data from an authentication response.
type VirtualEnvironmentAuthenticationResponseData struct {
ClusterName *string `json:"clustername,omitempty"`
CSRFPreventionToken *string `json:"CSRFPreventionToken,omitempty"`
Capabilities *VirtualEnvironmentAuthenticationResponseCapabilities `json:"cap,omitempty"`
Ticket *string `json:"ticket,omitempty"`
Username string `json:"username"`
}

View File

@ -1,67 +0,0 @@
/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at https://mozilla.org/MPL/2.0/. */
package proxmox
import (
"context"
"errors"
"fmt"
"net/http"
"net/url"
)
// DeleteCertificate deletes the custom certificate for a node.
func (c *VirtualEnvironmentClient) DeleteCertificate(
ctx context.Context,
nodeName string,
d *VirtualEnvironmentCertificateDeleteRequestBody,
) error {
return c.DoRequest(
ctx,
http.MethodDelete,
fmt.Sprintf("nodes/%s/certificates/custom", url.PathEscape(nodeName)),
d,
nil,
)
}
// ListCertificates retrieves the list of certificates for a node.
func (c *VirtualEnvironmentClient) ListCertificates(
ctx context.Context,
nodeName string,
) (*[]VirtualEnvironmentCertificateListResponseData, error) {
resBody := &VirtualEnvironmentCertificateListResponseBody{}
err := c.DoRequest(
ctx,
http.MethodGet,
fmt.Sprintf("nodes/%s/certificates/info", url.PathEscape(nodeName)),
nil,
resBody,
)
if err != nil {
return nil, err
}
if resBody.Data == nil {
return nil, errors.New("the server did not include a data object in the response")
}
return resBody.Data, nil
}
// UpdateCertificate updates the custom certificate for a node.
func (c *VirtualEnvironmentClient) UpdateCertificate(
ctx context.Context,
nodeName string,
d *VirtualEnvironmentCertificateUpdateRequestBody,
) error {
return c.DoRequest(
ctx,
http.MethodPost,
fmt.Sprintf("nodes/%s/certificates/custom", url.PathEscape(nodeName)),
d,
nil,
)
}

View File

@ -1,79 +0,0 @@
/*
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
*/
package proxmox
import (
"io"
"net/http"
"github.com/bpg/terraform-provider-proxmox/proxmox/cluster"
"github.com/bpg/terraform-provider-proxmox/proxmox/container"
"github.com/bpg/terraform-provider-proxmox/proxmox/vm"
)
const (
basePathJSONAPI = "api2/json"
)
// VirtualEnvironmentClient implements an API client for the Proxmox Virtual Environment API.
type VirtualEnvironmentClient struct {
Endpoint string
Insecure bool
OTP *string
Password string
Username string
SSHUsername string
SSHPassword string
SSHAgent bool
SSHAgentSocket string
authenticationData *VirtualEnvironmentAuthenticationResponseData
httpClient *http.Client
}
// VirtualEnvironmentErrorResponseBody contains the body of an error response.
type VirtualEnvironmentErrorResponseBody struct {
Data *string
Errors *map[string]string
}
// VirtualEnvironmentMultiPartData enables multipart uploads in DoRequest.
type VirtualEnvironmentMultiPartData struct {
Boundary string
Reader io.Reader
Size *int64
}
type API interface {
Cluster() *cluster.Client
VM(nodeName string, vmID int) *vm.Client
Container(nodeName string, vmID int) *container.Client
}
func (c *VirtualEnvironmentClient) API() API {
return &client{c}
}
func (c *VirtualEnvironmentClient) ExpandPath(path string) string {
return path
}
type client struct {
c *VirtualEnvironmentClient
}
func (c *client) Cluster() *cluster.Client {
return &cluster.Client{Client: c.c}
}
func (c *client) VM(nodeName string, vmID int) *vm.Client {
return &vm.Client{Client: c.c, NodeName: nodeName, VMID: vmID}
}
func (c *client) Container(nodeName string, vmID int) *container.Client {
return &container.Client{Client: c.c, NodeName: nodeName, VMID: vmID}
}

View File

@ -1,274 +0,0 @@
/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at https://mozilla.org/MPL/2.0/. */
package proxmox
import (
"context"
"errors"
"fmt"
"net/http"
"net/url"
"strings"
"time"
)
// CloneContainer clones a container.
func (c *VirtualEnvironmentClient) CloneContainer(
ctx context.Context,
nodeName string,
vmID int,
d *VirtualEnvironmentContainerCloneRequestBody,
) error {
return c.DoRequest(
ctx,
http.MethodPost,
fmt.Sprintf("nodes/%s/lxc/%d/clone", url.PathEscape(nodeName), vmID),
d,
nil,
)
}
// CreateContainer creates a container.
func (c *VirtualEnvironmentClient) CreateContainer(
ctx context.Context,
nodeName string,
d *VirtualEnvironmentContainerCreateRequestBody,
) error {
return c.DoRequest(ctx, http.MethodPost, fmt.Sprintf("nodes/%s/lxc", url.PathEscape(nodeName)), d, nil)
}
// DeleteContainer deletes a container.
func (c *VirtualEnvironmentClient) DeleteContainer(
ctx context.Context,
nodeName string,
vmID int,
) error {
return c.DoRequest(
ctx,
http.MethodDelete,
fmt.Sprintf("nodes/%s/lxc/%d", url.PathEscape(nodeName), vmID),
nil,
nil,
)
}
// GetContainer retrieves a container.
func (c *VirtualEnvironmentClient) GetContainer(
ctx context.Context,
nodeName string,
vmID int,
) (*VirtualEnvironmentContainerGetResponseData, error) {
resBody := &VirtualEnvironmentContainerGetResponseBody{}
err := c.DoRequest(
ctx,
http.MethodGet,
fmt.Sprintf("nodes/%s/lxc/%d/config", url.PathEscape(nodeName), vmID),
nil,
resBody,
)
if err != nil {
return nil, err
}
if resBody.Data == nil {
return nil, errors.New("the server did not include a data object in the response")
}
return resBody.Data, nil
}
// GetContainerStatus retrieves the status for a container.
func (c *VirtualEnvironmentClient) GetContainerStatus(
ctx context.Context,
nodeName string,
vmID int,
) (*VirtualEnvironmentContainerGetStatusResponseData, error) {
resBody := &VirtualEnvironmentContainerGetStatusResponseBody{}
err := c.DoRequest(
ctx,
http.MethodGet,
fmt.Sprintf("nodes/%s/lxc/%d/status/current", url.PathEscape(nodeName), vmID),
nil,
resBody,
)
if err != nil {
return nil, err
}
if resBody.Data == nil {
return nil, errors.New("the server did not include a data object in the response")
}
return resBody.Data, nil
}
// RebootContainer reboots a container.
func (c *VirtualEnvironmentClient) RebootContainer(
ctx context.Context,
nodeName string,
vmID int,
d *VirtualEnvironmentContainerRebootRequestBody,
) error {
return c.DoRequest(
ctx,
http.MethodPost,
fmt.Sprintf("nodes/%s/lxc/%d/status/reboot", url.PathEscape(nodeName), vmID),
d,
nil,
)
}
// ShutdownContainer shuts down a container.
func (c *VirtualEnvironmentClient) ShutdownContainer(
ctx context.Context,
nodeName string,
vmID int,
d *VirtualEnvironmentContainerShutdownRequestBody,
) error {
return c.DoRequest(
ctx,
http.MethodPost,
fmt.Sprintf("nodes/%s/lxc/%d/status/shutdown", url.PathEscape(nodeName), vmID),
d,
nil,
)
}
// StartContainer starts a container.
func (c *VirtualEnvironmentClient) StartContainer(
ctx context.Context,
nodeName string,
vmID int,
) error {
return c.DoRequest(
ctx,
http.MethodPost,
fmt.Sprintf("nodes/%s/lxc/%d/status/start", url.PathEscape(nodeName), vmID),
nil,
nil,
)
}
// StopContainer stops a container immediately.
func (c *VirtualEnvironmentClient) StopContainer(
ctx context.Context,
nodeName string,
vmID int,
) error {
return c.DoRequest(
ctx,
http.MethodPost,
fmt.Sprintf("nodes/%s/lxc/%d/status/stop", url.PathEscape(nodeName), vmID),
nil,
nil,
)
}
// UpdateContainer updates a container.
func (c *VirtualEnvironmentClient) UpdateContainer(
ctx context.Context,
nodeName string,
vmID int,
d *VirtualEnvironmentContainerUpdateRequestBody,
) error {
return c.DoRequest(
ctx,
http.MethodPut,
fmt.Sprintf("nodes/%s/lxc/%d/config", url.PathEscape(nodeName), vmID),
d,
nil,
)
}
// WaitForContainerState waits for a container to reach a specific state.
//
//nolint:dupl
func (c *VirtualEnvironmentClient) WaitForContainerState(
ctx context.Context,
nodeName string,
vmID int,
state string,
timeout int,
delay int,
) error {
state = strings.ToLower(state)
timeDelay := int64(delay)
timeMax := float64(timeout)
timeStart := time.Now()
timeElapsed := timeStart.Sub(timeStart)
for timeElapsed.Seconds() < timeMax {
if int64(timeElapsed.Seconds())%timeDelay == 0 {
data, err := c.GetContainerStatus(ctx, nodeName, vmID)
if err != nil {
return err
}
if data.Status == state {
return nil
}
time.Sleep(1 * time.Second)
}
time.Sleep(200 * time.Millisecond)
timeElapsed = time.Since(timeStart)
if ctx.Err() != nil {
return ctx.Err()
}
}
return fmt.Errorf(
"timeout while waiting for container \"%d\" to enter the state \"%s\"",
vmID,
state,
)
}
// WaitForContainerLock waits for a container lock to be released.
//
//nolint:dupl
func (c *VirtualEnvironmentClient) WaitForContainerLock(
ctx context.Context,
nodeName string,
vmID int,
timeout int,
delay int,
ignoreErrorResponse bool,
) error {
timeDelay := int64(delay)
timeMax := float64(timeout)
timeStart := time.Now()
timeElapsed := timeStart.Sub(timeStart)
for timeElapsed.Seconds() < timeMax {
if int64(timeElapsed.Seconds())%timeDelay == 0 {
data, err := c.GetContainerStatus(ctx, nodeName, vmID)
if err != nil {
if !ignoreErrorResponse {
return err
}
} else if data.Lock == nil || *data.Lock == "" {
return nil
}
time.Sleep(1 * time.Second)
}
time.Sleep(200 * time.Millisecond)
timeElapsed = time.Since(timeStart)
if ctx.Err() != nil {
return ctx.Err()
}
}
return fmt.Errorf("timeout while waiting for container \"%d\" to become unlocked", vmID)
}

View File

@ -1,812 +0,0 @@
/*
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
*/
package proxmox
import (
"encoding/json"
"fmt"
"net/url"
"strconv"
"strings"
"github.com/bpg/terraform-provider-proxmox/proxmox/types"
)
// VirtualEnvironmentContainerCloneRequestBody contains the data for an container clone request.
type VirtualEnvironmentContainerCloneRequestBody struct {
BandwidthLimit *int `json:"bwlimit,omitempty" url:"bwlimit,omitempty"`
Description *string `json:"description,omitempty" url:"description,omitempty"`
FullCopy *types.CustomBool `json:"full,omitempty" url:"full,omitempty,int"`
Hostname *string `json:"hostname,omitempty" url:"hostname,omitempty"`
PoolID *string `json:"pool,omitempty" url:"pool,omitempty"`
SnapshotName *string `json:"snapname,omitempty" url:"snapname,omitempty"`
TargetNodeName *string `json:"target,omitempty" url:"target,omitempty"`
TargetStorage *string `json:"storage,omitempty" url:"storage,omitempty"`
VMIDNew int `json:"newid" url:"newid"`
}
// VirtualEnvironmentContainerCreateRequestBody contains the data for an user create request.
type VirtualEnvironmentContainerCreateRequestBody struct {
BandwidthLimit *float64 `json:"bwlimit,omitempty" url:"bwlimit,omitempty"`
ConsoleEnabled *types.CustomBool `json:"console,omitempty" url:"console,omitempty,int"`
ConsoleMode *string `json:"cmode,omitempty" url:"cmode,omitempty"`
CPUArchitecture *string `json:"arch,omitempty" url:"arch,omitempty"`
CPUCores *int `json:"cores,omitempty" url:"cores,omitempty"`
CPULimit *int `json:"cpulimit,omitempty" url:"cpulimit,omitempty"`
CPUUnits *int `json:"cpuunits,omitempty" url:"cpuunits,omitempty"`
DatastoreID *string `json:"storage,omitempty" url:"storage,omitempty"`
DedicatedMemory *int `json:"memory,omitempty" url:"memory,omitempty"`
Delete []string `json:"delete,omitempty" url:"delete,omitempty"`
Description *string `json:"description,omitempty" url:"description,omitempty"`
DNSDomain *string `json:"searchdomain,omitempty" url:"searchdomain,omitempty"`
DNSServer *string `json:"nameserver,omitempty" url:"nameserver,omitempty"`
Features *VirtualEnvironmentContainerCustomFeatures `json:"features,omitempty" url:"features,omitempty"`
Force *types.CustomBool `json:"force,omitempty" url:"force,omitempty,int"`
HookScript *string `json:"hookscript,omitempty" url:"hookscript,omitempty"`
Hostname *string `json:"hostname,omitempty" url:"hostname,omitempty"`
IgnoreUnpackErrors *types.CustomBool `json:"ignore-unpack-errors,omitempty" url:"force,omitempty,int"`
Lock *string `json:"lock,omitempty" url:"lock,omitempty,int"`
MountPoints VirtualEnvironmentContainerCustomMountPointArray `json:"mp,omitempty" url:"mp,omitempty,numbered"`
NetworkInterfaces VirtualEnvironmentContainerCustomNetworkInterfaceArray `json:"net,omitempty" url:"net,omitempty,numbered"`
OSTemplateFileVolume *string `json:"ostemplate,omitempty" url:"ostemplate,omitempty"`
OSType *string `json:"ostype,omitempty" url:"ostype,omitempty"`
Password *string `json:"password,omitempty" url:"password,omitempty"`
PoolID *string `json:"pool,omitempty" url:"pool,omitempty"`
Protection *types.CustomBool `json:"protection,omitempty" url:"protection,omitempty,int"`
Restore *types.CustomBool `json:"restore,omitempty" url:"restore,omitempty,int"`
RootFS *VirtualEnvironmentContainerCustomRootFS `json:"rootfs,omitempty" url:"rootfs,omitempty"`
SSHKeys *VirtualEnvironmentContainerCustomSSHKeys `json:"ssh-public-keys,omitempty" url:"ssh-public-keys,omitempty"`
Start *types.CustomBool `json:"start,omitempty" url:"start,omitempty,int"`
StartOnBoot *types.CustomBool `json:"onboot,omitempty" url:"onboot,omitempty,int"`
StartupBehavior *VirtualEnvironmentContainerCustomStartupBehavior `json:"startup,omitempty" url:"startup,omitempty"`
Swap *int `json:"swap,omitempty" url:"swap,omitempty"`
Tags *string `json:"tags,omitempty" url:"tags,omitempty"`
Template *types.CustomBool `json:"template,omitempty" url:"template,omitempty,int"`
TTY *int `json:"tty,omitempty" url:"tty,omitempty"`
Unique *types.CustomBool `json:"unique,omitempty" url:"unique,omitempty,int"`
Unprivileged *types.CustomBool `json:"unprivileged,omitempty" url:"unprivileged,omitempty,int"`
VMID *int `json:"vmid,omitempty" url:"vmid,omitempty"`
}
// VirtualEnvironmentContainerCustomFeatures contains the values for the "features" property.
type VirtualEnvironmentContainerCustomFeatures struct {
FUSE *types.CustomBool `json:"fuse,omitempty" url:"fuse,omitempty,int"`
KeyControl *types.CustomBool `json:"keyctl,omitempty" url:"keyctl,omitempty,int"`
MountTypes *[]string `json:"mount,omitempty" url:"mount,omitempty"`
Nesting *types.CustomBool `json:"nesting,omitempty" url:"nesting,omitempty,int"`
}
// VirtualEnvironmentContainerCustomMountPoint contains the values for the "mp[n]" properties.
type VirtualEnvironmentContainerCustomMountPoint struct {
ACL *types.CustomBool `json:"acl,omitempty" url:"acl,omitempty,int"`
Backup *types.CustomBool `json:"backup,omitempty" url:"backup,omitempty,int"`
DiskSize *string `json:"size,omitempty" url:"size,omitempty"`
Enabled bool `json:"-" url:"-"`
MountOptions *[]string `json:"mountoptions,omitempty" url:"mountoptions,omitempty"`
MountPoint string `json:"mp" url:"mp"`
Quota *types.CustomBool `json:"quota,omitempty" url:"quota,omitempty,int"`
ReadOnly *types.CustomBool `json:"ro,omitempty" url:"ro,omitempty,int"`
Replicate *types.CustomBool `json:"replicate,omitempty" url:"replicate,omitempty,int"`
Shared *types.CustomBool `json:"shared,omitempty" url:"shared,omitempty,int"`
Volume string `json:"volume" url:"volume"`
}
// VirtualEnvironmentContainerCustomMountPointArray is an array of VirtualEnvironmentContainerCustomMountPoint.
type VirtualEnvironmentContainerCustomMountPointArray []VirtualEnvironmentContainerCustomMountPoint
// VirtualEnvironmentContainerCustomNetworkInterface contains the values for the "net[n]" properties.
type VirtualEnvironmentContainerCustomNetworkInterface struct {
Bridge *string `json:"bridge,omitempty" url:"bridge,omitempty"`
Enabled bool `json:"-" url:"-"`
Firewall *types.CustomBool `json:"firewall,omitempty" url:"firewall,omitempty,int"`
IPv4Address *string `json:"ip,omitempty" url:"ip,omitempty"`
IPv4Gateway *string `json:"gw,omitempty" url:"gw,omitempty"`
IPv6Address *string `json:"ip6,omitempty" url:"ip6,omitempty"`
IPv6Gateway *string `json:"gw6,omitempty" url:"gw6,omitempty"`
MACAddress *string `json:"hwaddr,omitempty" url:"hwaddr,omitempty"`
MTU *int `json:"mtu,omitempty" url:"mtu,omitempty"`
Name string `json:"name" url:"name"`
RateLimit *float64 `json:"rate,omitempty" url:"rate,omitempty"`
Tag *int `json:"tag,omitempty" url:"tag,omitempty"`
Trunks *[]int `json:"trunks,omitempty" url:"trunks,omitempty"`
Type *string `json:"type,omitempty" url:"type,omitempty"`
}
// VirtualEnvironmentContainerCustomNetworkInterfaceArray is an array of VirtualEnvironmentContainerCustomNetworkInterface.
type VirtualEnvironmentContainerCustomNetworkInterfaceArray []VirtualEnvironmentContainerCustomNetworkInterface
// VirtualEnvironmentContainerCustomRootFS contains the values for the "rootfs" property.
type VirtualEnvironmentContainerCustomRootFS struct {
ACL *types.CustomBool `json:"acl,omitempty" url:"acl,omitempty,int"`
Size *types.DiskSize `json:"size,omitempty" url:"size,omitempty"`
MountOptions *[]string `json:"mountoptions,omitempty" url:"mountoptions,omitempty"`
Quota *types.CustomBool `json:"quota,omitempty" url:"quota,omitempty,int"`
ReadOnly *types.CustomBool `json:"ro,omitempty" url:"ro,omitempty,int"`
Replicate *types.CustomBool `json:"replicate,omitempty" url:"replicate,omitempty,int"`
Shared *types.CustomBool `json:"shared,omitempty" url:"shared,omitempty,int"`
Volume string `json:"volume" url:"volume"`
}
// VirtualEnvironmentContainerCustomSSHKeys contains the values for the "ssh-public-keys" property.
type VirtualEnvironmentContainerCustomSSHKeys []string
// VirtualEnvironmentContainerCustomStartupBehavior contains the values for the "startup" property.
type VirtualEnvironmentContainerCustomStartupBehavior struct {
Down *int `json:"down,omitempty" url:"down,omitempty"`
Order *int `json:"order,omitempty" url:"order,omitempty"`
Up *int `json:"up,omitempty" url:"up,omitempty"`
}
// VirtualEnvironmentContainerGetResponseBody contains the body from an user get response.
type VirtualEnvironmentContainerGetResponseBody struct {
Data *VirtualEnvironmentContainerGetResponseData `json:"data,omitempty"`
}
// VirtualEnvironmentContainerGetResponseData contains the data from an user get response.
type VirtualEnvironmentContainerGetResponseData struct {
ConsoleEnabled *types.CustomBool `json:"console,omitempty"`
ConsoleMode *string `json:"cmode,omitempty"`
CPUArchitecture *string `json:"arch,omitempty"`
CPUCores *int `json:"cores,omitempty"`
CPULimit *int `json:"cpulimit,omitempty"`
CPUUnits *int `json:"cpuunits,omitempty"`
DedicatedMemory *int `json:"memory,omitempty"`
Description *string `json:"description,omitempty"`
Digest string `json:"digest"`
DNSDomain *string `json:"searchdomain,omitempty"`
DNSServer *string `json:"nameserver,omitempty"`
Features *VirtualEnvironmentContainerCustomFeatures `json:"features,omitempty"`
HookScript *string `json:"hookscript,omitempty"`
Hostname *string `json:"hostname,omitempty"`
Lock *types.CustomBool `json:"lock,omitempty"`
LXCConfiguration *[][2]string `json:"lxc,omitempty"`
MountPoint0 VirtualEnvironmentContainerCustomMountPoint `json:"mp0,omitempty"`
MountPoint1 VirtualEnvironmentContainerCustomMountPoint `json:"mp1,omitempty"`
MountPoint2 VirtualEnvironmentContainerCustomMountPoint `json:"mp2,omitempty"`
MountPoint3 VirtualEnvironmentContainerCustomMountPoint `json:"mp3,omitempty"`
NetworkInterface0 *VirtualEnvironmentContainerCustomNetworkInterface `json:"net0,omitempty"`
NetworkInterface1 *VirtualEnvironmentContainerCustomNetworkInterface `json:"net1,omitempty"`
NetworkInterface2 *VirtualEnvironmentContainerCustomNetworkInterface `json:"net2,omitempty"`
NetworkInterface3 *VirtualEnvironmentContainerCustomNetworkInterface `json:"net3,omitempty"`
NetworkInterface4 *VirtualEnvironmentContainerCustomNetworkInterface `json:"net4,omitempty"`
NetworkInterface5 *VirtualEnvironmentContainerCustomNetworkInterface `json:"net5,omitempty"`
NetworkInterface6 *VirtualEnvironmentContainerCustomNetworkInterface `json:"net6,omitempty"`
NetworkInterface7 *VirtualEnvironmentContainerCustomNetworkInterface `json:"net7,omitempty"`
OSType *string `json:"ostype,omitempty"`
Protection *types.CustomBool `json:"protection,omitempty"`
RootFS *VirtualEnvironmentContainerCustomRootFS `json:"rootfs,omitempty"`
StartOnBoot *types.CustomBool `json:"onboot,omitempty"`
StartupBehavior *VirtualEnvironmentContainerCustomStartupBehavior `json:"startup,omitempty"`
Swap *int `json:"swap,omitempty"`
Tags *string `json:"tags,omitempty"`
Template *types.CustomBool `json:"template,omitempty"`
TTY *int `json:"tty,omitempty"`
Unprivileged *types.CustomBool `json:"unprivileged,omitempty"`
}
// VirtualEnvironmentContainerGetStatusResponseBody contains the body from a container get status response.
type VirtualEnvironmentContainerGetStatusResponseBody struct {
Data *VirtualEnvironmentContainerGetStatusResponseData `json:"data,omitempty"`
}
// VirtualEnvironmentContainerGetStatusResponseData contains the data from a container get status response.
type VirtualEnvironmentContainerGetStatusResponseData struct {
CPUCount *float64 `json:"cpus,omitempty"`
Lock *string `json:"lock,omitempty"`
MemoryAllocation *int `json:"maxmem,omitempty"`
Name *string `json:"name,omitempty"`
RootDiskSize *interface{} `json:"maxdisk,omitempty"`
Status string `json:"status,omitempty"`
SwapAllocation *int `json:"maxswap,omitempty"`
Tags *string `json:"tags,omitempty"`
Uptime *int `json:"uptime,omitempty"`
VMID *int `json:"vmid,omitempty"`
}
// VirtualEnvironmentContainerRebootRequestBody contains the body for a container reboot request.
type VirtualEnvironmentContainerRebootRequestBody struct {
Timeout *int `json:"timeout,omitempty" url:"timeout,omitempty"`
}
// VirtualEnvironmentContainerShutdownRequestBody contains the body for a container shutdown request.
type VirtualEnvironmentContainerShutdownRequestBody struct {
ForceStop *types.CustomBool `json:"forceStop,omitempty" url:"forceStop,omitempty,int"`
Timeout *int `json:"timeout,omitempty" url:"timeout,omitempty"`
}
// VirtualEnvironmentContainerUpdateRequestBody contains the data for an user update request.
type VirtualEnvironmentContainerUpdateRequestBody VirtualEnvironmentContainerCreateRequestBody
// EncodeValues converts a VirtualEnvironmentContainerCustomFeatures struct to a URL vlaue.
func (r VirtualEnvironmentContainerCustomFeatures) EncodeValues(key string, v *url.Values) error {
var values []string
if r.FUSE != nil {
if *r.FUSE {
values = append(values, "fuse=1")
} else {
values = append(values, "fuse=0")
}
}
if r.KeyControl != nil {
if *r.KeyControl {
values = append(values, "keyctl=1")
} else {
values = append(values, "keyctl=0")
}
}
if r.MountTypes != nil {
if len(*r.MountTypes) > 0 {
values = append(values, fmt.Sprintf("mount=%s", strings.Join(*r.MountTypes, ";")))
}
}
if r.Nesting != nil {
if *r.Nesting {
values = append(values, "nesting=1")
} else {
values = append(values, "nesting=0")
}
}
if len(values) > 0 {
v.Add(key, strings.Join(values, ","))
}
return nil
}
// EncodeValues converts a VirtualEnvironmentContainerCustomMountPoint struct to a URL vlaue.
func (r VirtualEnvironmentContainerCustomMountPoint) EncodeValues(key string, v *url.Values) error {
var values []string
if r.ACL != nil {
if *r.ACL {
values = append(values, "acl=%d")
} else {
values = append(values, "acl=0")
}
}
if r.Backup != nil {
if *r.Backup {
values = append(values, "backup=1")
} else {
values = append(values, "backup=0")
}
}
if r.DiskSize != nil {
values = append(values, fmt.Sprintf("size=%s", *r.DiskSize))
}
if r.MountOptions != nil {
if len(*r.MountOptions) > 0 {
values = append(values, fmt.Sprintf("mount=%s", strings.Join(*r.MountOptions, ";")))
}
}
values = append(values, fmt.Sprintf("mp=%s", r.MountPoint))
if r.Quota != nil {
if *r.Quota {
values = append(values, "quota=1")
} else {
values = append(values, "quota=0")
}
}
if r.ReadOnly != nil {
if *r.ReadOnly {
values = append(values, "ro=1")
} else {
values = append(values, "ro=0")
}
}
if r.Replicate != nil {
if *r.ReadOnly {
values = append(values, "replicate=1")
} else {
values = append(values, "replicate=0")
}
}
if r.Shared != nil {
if *r.Shared {
values = append(values, "shared=1")
} else {
values = append(values, "shared=0")
}
}
values = append(values, fmt.Sprintf("volume=%s", r.Volume))
if len(values) > 0 {
v.Add(key, strings.Join(values, ","))
}
return nil
}
// EncodeValues converts a VirtualEnvironmentContainerCustomMountPointArray array to multiple URL values.
func (r VirtualEnvironmentContainerCustomMountPointArray) EncodeValues(
key string,
v *url.Values,
) error {
for i, d := range r {
err := d.EncodeValues(fmt.Sprintf("%s%d", key, i), v)
if err != nil {
return err
}
}
return nil
}
// EncodeValues converts a VirtualEnvironmentContainerCustomNetworkInterface struct to a URL vlaue.
func (r VirtualEnvironmentContainerCustomNetworkInterface) EncodeValues(
key string,
v *url.Values,
) error {
var values []string
if r.Bridge != nil {
values = append(values, fmt.Sprintf("bridge=%s", *r.Bridge))
}
if r.Firewall != nil {
if *r.Firewall {
values = append(values, "firewall=1")
} else {
values = append(values, "firewall=0")
}
}
if r.IPv4Address != nil {
values = append(values, fmt.Sprintf("ip=%s", *r.IPv4Address))
}
if r.IPv4Gateway != nil {
values = append(values, fmt.Sprintf("gw=%s", *r.IPv4Gateway))
}
if r.IPv6Address != nil {
values = append(values, fmt.Sprintf("ip6=%s", *r.IPv6Address))
}
if r.IPv6Gateway != nil {
values = append(values, fmt.Sprintf("gw6=%s", *r.IPv6Gateway))
}
if r.MACAddress != nil {
values = append(values, fmt.Sprintf("hwaddr=%s", *r.MACAddress))
}
if r.MTU != nil {
values = append(values, fmt.Sprintf("mtu=%d", *r.MTU))
}
values = append(values, fmt.Sprintf("name=%s", r.Name))
if r.RateLimit != nil {
values = append(values, fmt.Sprintf("rate=%.2f", *r.RateLimit))
}
if r.Tag != nil {
values = append(values, fmt.Sprintf("tag=%d", *r.Tag))
}
if r.Trunks != nil && len(*r.Trunks) > 0 {
sTrunks := make([]string, len(*r.Trunks))
for i, v := range *r.Trunks {
sTrunks[i] = strconv.Itoa(v)
}
values = append(values, fmt.Sprintf("trunks=%s", strings.Join(sTrunks, ";")))
}
if r.Type != nil {
values = append(values, fmt.Sprintf("type=%s", *r.Type))
}
if len(values) > 0 {
v.Add(key, strings.Join(values, ","))
}
return nil
}
// EncodeValues converts a VirtualEnvironmentContainerCustomNetworkInterfaceArray array to multiple URL values.
func (r VirtualEnvironmentContainerCustomNetworkInterfaceArray) EncodeValues(
key string,
v *url.Values,
) error {
for i, d := range r {
err := d.EncodeValues(fmt.Sprintf("%s%d", key, i), v)
if err != nil {
return err
}
}
return nil
}
// EncodeValues converts a VirtualEnvironmentContainerCustomRootFS struct to a URL vlaue.
func (r VirtualEnvironmentContainerCustomRootFS) EncodeValues(key string, v *url.Values) error {
var values []string
if r.ACL != nil {
if *r.ACL {
values = append(values, "acl=%d")
} else {
values = append(values, "acl=0")
}
}
if r.Size != nil {
values = append(values, fmt.Sprintf("size=%s", *r.Size))
}
if r.MountOptions != nil {
if len(*r.MountOptions) > 0 {
values = append(values, fmt.Sprintf("mount=%s", strings.Join(*r.MountOptions, ";")))
}
}
if r.Quota != nil {
if *r.Quota {
values = append(values, "quota=1")
} else {
values = append(values, "quota=0")
}
}
if r.ReadOnly != nil {
if *r.ReadOnly {
values = append(values, "ro=1")
} else {
values = append(values, "ro=0")
}
}
if r.Replicate != nil {
if *r.ReadOnly {
values = append(values, "replicate=1")
} else {
values = append(values, "replicate=0")
}
}
if r.Shared != nil {
if *r.Shared {
values = append(values, "shared=1")
} else {
values = append(values, "shared=0")
}
}
values = append(values, fmt.Sprintf("volume=%s", r.Volume))
if len(values) > 0 {
v.Add(key, strings.Join(values, ","))
}
return nil
}
// EncodeValues converts a VirtualEnvironmentContainerCustomSSHKeys array to a URL vlaue.
func (r VirtualEnvironmentContainerCustomSSHKeys) EncodeValues(key string, v *url.Values) error {
v.Add(key, strings.Join(r, "\n"))
return nil
}
// EncodeValues converts a VirtualEnvironmentContainerCustomStartupBehavior struct to a URL vlaue.
func (r VirtualEnvironmentContainerCustomStartupBehavior) EncodeValues(
key string,
v *url.Values,
) error {
var values []string
if r.Down != nil {
values = append(values, fmt.Sprintf("down=%d", *r.Down))
}
if r.Order != nil {
values = append(values, fmt.Sprintf("order=%d", *r.Order))
}
if r.Up != nil {
values = append(values, fmt.Sprintf("up=%d", *r.Up))
}
if len(values) > 0 {
v.Add(key, strings.Join(values, ","))
}
return nil
}
// UnmarshalJSON converts a VirtualEnvironmentContainerCustomFeatures string to an object.
func (r *VirtualEnvironmentContainerCustomFeatures) UnmarshalJSON(b []byte) error {
var s string
err := json.Unmarshal(b, &s)
if err != nil {
return err
}
pairs := strings.Split(s, ",")
for _, p := range pairs {
v := strings.Split(strings.TrimSpace(p), "=")
if len(v) == 2 {
switch v[0] {
case "fuse":
bv := types.CustomBool(v[1] == "1")
r.FUSE = &bv
case "keyctl":
bv := types.CustomBool(v[1] == "1")
r.KeyControl = &bv
case "mount":
if v[1] != "" {
a := strings.Split(v[1], ";")
r.MountTypes = &a
} else {
var a []string
r.MountTypes = &a
}
case "nesting":
bv := types.CustomBool(v[1] == "1")
r.Nesting = &bv
}
}
}
return nil
}
// UnmarshalJSON converts a VirtualEnvironmentContainerCustomMountPoint string to an object.
func (r *VirtualEnvironmentContainerCustomMountPoint) UnmarshalJSON(b []byte) error {
var s string
err := json.Unmarshal(b, &s)
if err != nil {
return err
}
pairs := strings.Split(s, ",")
for _, p := range pairs {
v := strings.Split(strings.TrimSpace(p), "=")
if len(v) == 1 {
r.Volume = v[0]
} else if len(v) == 2 {
switch v[0] {
case "acl":
bv := types.CustomBool(v[1] == "1")
r.ACL = &bv
case "backup":
bv := types.CustomBool(v[1] == "1")
r.Backup = &bv
case "mountoptions":
if v[1] != "" {
a := strings.Split(v[1], ";")
r.MountOptions = &a
} else {
var a []string
r.MountOptions = &a
}
case "mp":
r.MountPoint = v[1]
case "quota":
bv := types.CustomBool(v[1] == "1")
r.Quota = &bv
case "ro":
bv := types.CustomBool(v[1] == "1")
r.ReadOnly = &bv
case "replicate":
bv := types.CustomBool(v[1] == "1")
r.Replicate = &bv
case "shared":
bv := types.CustomBool(v[1] == "1")
r.Shared = &bv
case "size":
r.DiskSize = &v[1]
}
}
}
return nil
}
// UnmarshalJSON converts a VirtualEnvironmentContainerCustomNetworkInterface string to an object.
func (r *VirtualEnvironmentContainerCustomNetworkInterface) UnmarshalJSON(b []byte) error {
var s string
err := json.Unmarshal(b, &s)
if err != nil {
return err
}
pairs := strings.Split(s, ",")
for _, p := range pairs {
v := strings.Split(strings.TrimSpace(p), "=")
if len(v) == 1 {
r.Name = v[0]
} else if len(v) == 2 {
switch v[0] {
case "bridge":
r.Bridge = &v[1]
case "firewall":
bv := types.CustomBool(v[1] == "1")
r.Firewall = &bv
case "gw":
r.IPv4Gateway = &v[1]
case "gw6":
r.IPv6Gateway = &v[1]
case "ip":
r.IPv4Address = &v[1]
case "ip6":
r.IPv6Address = &v[1]
case "hwaddr":
r.MACAddress = &v[1]
case "mtu":
iv, err := strconv.Atoi(v[1])
if err != nil {
return err
}
r.MTU = &iv
case "name":
r.Name = v[1]
case "rate":
fv, err := strconv.ParseFloat(v[1], 64)
if err != nil {
return err
}
r.RateLimit = &fv
case "tag":
iv, err := strconv.Atoi(v[1])
if err != nil {
return err
}
r.Tag = &iv
case "trunks":
if v[1] != "" {
trunks := strings.Split(v[1], ";")
a := make([]int, len(trunks))
for ti, tv := range trunks {
a[ti], err = strconv.Atoi(tv)
if err != nil {
return err
}
}
r.Trunks = &a
} else {
var a []int
r.Trunks = &a
}
case "type":
r.Type = &v[1]
}
}
}
return nil
}
// UnmarshalJSON converts a VirtualEnvironmentContainerCustomRootFS string to an object.
func (r *VirtualEnvironmentContainerCustomRootFS) UnmarshalJSON(b []byte) error {
var s string
err := json.Unmarshal(b, &s)
if err != nil {
return err
}
pairs := strings.Split(s, ",")
for _, p := range pairs {
v := strings.Split(strings.TrimSpace(p), "=")
if len(v) == 1 {
r.Volume = v[0]
} else if len(v) == 2 {
switch v[0] {
case "acl":
bv := types.CustomBool(v[1] == "1")
r.ACL = &bv
case "mountoptions":
if v[1] != "" {
a := strings.Split(v[1], ";")
r.MountOptions = &a
} else {
var a []string
r.MountOptions = &a
}
case "quota":
bv := types.CustomBool(v[1] == "1")
r.Quota = &bv
case "ro":
bv := types.CustomBool(v[1] == "1")
r.ReadOnly = &bv
case "replicate":
bv := types.CustomBool(v[1] == "1")
r.Replicate = &bv
case "shared":
bv := types.CustomBool(v[1] == "1")
r.Shared = &bv
case "size":
r.Size = new(types.DiskSize)
err := r.Size.UnmarshalJSON([]byte(v[1]))
if err != nil {
return fmt.Errorf("failed to unmarshal disk size: %w", err)
}
}
}
}
return nil
}
// UnmarshalJSON converts a VirtualEnvironmentContainerCustomStartupBehavior string to an object.
func (r *VirtualEnvironmentContainerCustomStartupBehavior) UnmarshalJSON(b []byte) error {
var s string
err := json.Unmarshal(b, &s)
if err != nil {
return err
}
pairs := strings.Split(s, ",")
for _, p := range pairs {
v := strings.Split(strings.TrimSpace(p), "=")
if len(v) == 2 {
switch v[0] {
case "down":
iv, err := strconv.Atoi(v[1])
if err != nil {
return err
}
r.Down = &iv
case "order":
iv, err := strconv.Atoi(v[1])
if err != nil {
return err
}
r.Order = &iv
case "up":
iv, err := strconv.Atoi(v[1])
if err != nil {
return err
}
r.Up = &iv
}
}
}
return nil
}

View File

@ -1,46 +0,0 @@
/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at https://mozilla.org/MPL/2.0/. */
package proxmox
import (
"context"
"errors"
"fmt"
"net/http"
"net/url"
)
// GetDNS retrieves the DNS configuration for a node.
func (c *VirtualEnvironmentClient) GetDNS(
ctx context.Context,
nodeName string,
) (*VirtualEnvironmentDNSGetResponseData, error) {
resBody := &VirtualEnvironmentDNSGetResponseBody{}
err := c.DoRequest(
ctx,
http.MethodGet,
fmt.Sprintf("nodes/%s/dns", url.PathEscape(nodeName)),
nil,
resBody,
)
if err != nil {
return nil, err
}
if resBody.Data == nil {
return nil, errors.New("the server did not include a data object in the response")
}
return resBody.Data, nil
}
// UpdateDNS updates the DNS configuration for a node.
func (c *VirtualEnvironmentClient) UpdateDNS(
ctx context.Context,
nodeName string,
d *VirtualEnvironmentDNSUpdateRequestBody,
) error {
return c.DoRequest(ctx, http.MethodPut, fmt.Sprintf("nodes/%s/dns", url.PathEscape(nodeName)), d, nil)
}

View File

@ -1,83 +0,0 @@
/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at https://mozilla.org/MPL/2.0/. */
package proxmox
import (
"context"
"errors"
"fmt"
"net/http"
"net/url"
"sort"
)
// CreateGroup creates an access group.
func (c *VirtualEnvironmentClient) CreateGroup(
ctx context.Context,
d *VirtualEnvironmentGroupCreateRequestBody,
) error {
return c.DoRequest(ctx, http.MethodPost, "access/groups", d, nil)
}
// DeleteGroup deletes an access group.
func (c *VirtualEnvironmentClient) DeleteGroup(ctx context.Context, id string) error {
return c.DoRequest(ctx, http.MethodDelete, fmt.Sprintf("access/groups/%s", url.PathEscape(id)), nil, nil)
}
// GetGroup retrieves an access group.
func (c *VirtualEnvironmentClient) GetGroup(
ctx context.Context,
id string,
) (*VirtualEnvironmentGroupGetResponseData, error) {
resBody := &VirtualEnvironmentGroupGetResponseBody{}
err := c.DoRequest(
ctx,
http.MethodGet,
fmt.Sprintf("access/groups/%s", url.PathEscape(id)),
nil,
resBody,
)
if err != nil {
return nil, err
}
if resBody.Data == nil {
return nil, errors.New("the server did not include a data object in the response")
}
sort.Strings(resBody.Data.Members)
return resBody.Data, nil
}
// ListGroups retrieves a list of access groups.
func (c *VirtualEnvironmentClient) ListGroups(
ctx context.Context,
) ([]*VirtualEnvironmentGroupListResponseData, error) {
resBody := &VirtualEnvironmentGroupListResponseBody{}
err := c.DoRequest(ctx, http.MethodGet, "access/groups", nil, resBody)
if err != nil {
return nil, err
}
if resBody.Data == nil {
return nil, errors.New("the server did not include a data object in the response")
}
sort.Slice(resBody.Data, func(i, j int) bool {
return resBody.Data[i].ID < resBody.Data[j].ID
})
return resBody.Data, nil
}
// UpdateGroup updates an access group.
func (c *VirtualEnvironmentClient) UpdateGroup(
ctx context.Context,
id string,
d *VirtualEnvironmentGroupUpdateRequestBody,
) error {
return c.DoRequest(ctx, http.MethodPut, fmt.Sprintf("access/groups/%s", url.PathEscape(id)), d, nil)
}

View File

@ -1,38 +0,0 @@
/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at https://mozilla.org/MPL/2.0/. */
package proxmox
// VirtualEnvironmentGroupCreateRequestBody contains the data for an access group create request.
type VirtualEnvironmentGroupCreateRequestBody struct {
Comment *string `json:"comment,omitempty" url:"comment,omitempty"`
ID string `json:"groupid" url:"groupid"`
}
// VirtualEnvironmentGroupGetResponseBody contains the body from an access group get response.
type VirtualEnvironmentGroupGetResponseBody struct {
Data *VirtualEnvironmentGroupGetResponseData `json:"data,omitempty"`
}
// VirtualEnvironmentGroupGetResponseData contains the data from an access group get response.
type VirtualEnvironmentGroupGetResponseData struct {
Comment *string `json:"comment,omitempty"`
Members []string `json:"members"`
}
// VirtualEnvironmentGroupListResponseBody contains the body from an access group list response.
type VirtualEnvironmentGroupListResponseBody struct {
Data []*VirtualEnvironmentGroupListResponseData `json:"data,omitempty"`
}
// VirtualEnvironmentGroupListResponseData contains the data from an access group list response.
type VirtualEnvironmentGroupListResponseData struct {
Comment *string `json:"comment,omitempty"`
ID string `json:"groupid"`
}
// VirtualEnvironmentGroupUpdateRequestBody contains the data for an access group update request.
type VirtualEnvironmentGroupUpdateRequestBody struct {
Comment *string `json:"comment,omitempty" url:"comment,omitempty"`
}

View File

@ -1,46 +0,0 @@
/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at https://mozilla.org/MPL/2.0/. */
package proxmox
import (
"context"
"errors"
"fmt"
"net/http"
"net/url"
)
// GetHosts retrieves the Hosts configuration for a node.
func (c *VirtualEnvironmentClient) GetHosts(
ctx context.Context,
nodeName string,
) (*VirtualEnvironmentHostsGetResponseData, error) {
resBody := &VirtualEnvironmentHostsGetResponseBody{}
err := c.DoRequest(
ctx,
http.MethodGet,
fmt.Sprintf("nodes/%s/hosts", url.PathEscape(nodeName)),
nil,
resBody,
)
if err != nil {
return nil, err
}
if resBody.Data == nil {
return nil, errors.New("the server did not include a data object in the response")
}
return resBody.Data, nil
}
// UpdateHosts updates the Hosts configuration for a node.
func (c *VirtualEnvironmentClient) UpdateHosts(
ctx context.Context,
nodeName string,
d *VirtualEnvironmentHostsUpdateRequestBody,
) error {
return c.DoRequest(ctx, http.MethodPost, fmt.Sprintf("nodes/%s/hosts", url.PathEscape(nodeName)), d, nil)
}

View File

@ -1,22 +0,0 @@
/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at https://mozilla.org/MPL/2.0/. */
package proxmox
// VirtualEnvironmentHostsGetResponseBody contains the body from a hosts get response.
type VirtualEnvironmentHostsGetResponseBody struct {
Data *VirtualEnvironmentHostsGetResponseData `json:"data,omitempty"`
}
// VirtualEnvironmentHostsGetResponseData contains the data from a hosts get response.
type VirtualEnvironmentHostsGetResponseData struct {
Data string `json:"data"`
Digest *string `json:"digest,omitempty"`
}
// VirtualEnvironmentHostsUpdateRequestBody contains the body for a hosts update request.
type VirtualEnvironmentHostsUpdateRequestBody struct {
Data string `json:"data" url:"data"`
Digest *string `json:"digest,omitempty" url:"digest,omitempty"`
}

View File

@ -1,379 +0,0 @@
/*
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
*/
package proxmox
import (
"context"
"errors"
"fmt"
"net"
"net/http"
"net/url"
"os"
"path"
"sort"
"strings"
"time"
"github.com/hashicorp/terraform-plugin-log/tflog"
"github.com/skeema/knownhosts"
"golang.org/x/crypto/ssh"
"golang.org/x/crypto/ssh/agent"
)
// ExecuteNodeCommands executes commands on a given node.
func (c *VirtualEnvironmentClient) ExecuteNodeCommands(
ctx context.Context,
nodeName string,
commands []string,
) error {
closeOrLogError := CloseOrLogError(ctx)
sshClient, err := c.OpenNodeShell(ctx, nodeName)
if err != nil {
return err
}
defer closeOrLogError(sshClient)
sshSession, err := sshClient.NewSession()
if err != nil {
return err
}
defer closeOrLogError(sshSession)
script := strings.Join(commands, " && \\\n")
output, err := sshSession.CombinedOutput(
fmt.Sprintf(
"/bin/bash -c '%s'",
strings.ReplaceAll(script, "'", "'\"'\"'"),
),
)
if err != nil {
return errors.New(string(output))
}
return nil
}
// GetNodeIP retrieves the IP address of a node.
func (c *VirtualEnvironmentClient) GetNodeIP(
ctx context.Context,
nodeName string,
) (*string, error) {
networkDevices, err := c.ListNodeNetworkDevices(ctx, nodeName)
if err != nil {
return nil, err
}
nodeAddress := ""
for _, d := range networkDevices {
if d.Address != nil {
nodeAddress = *d.Address
break
}
}
if nodeAddress == "" {
return nil, fmt.Errorf("failed to determine the IP address of node \"%s\"", nodeName)
}
nodeAddressParts := strings.Split(nodeAddress, "/")
return &nodeAddressParts[0], nil
}
// GetNodeTime retrieves the time information for a node.
func (c *VirtualEnvironmentClient) GetNodeTime(
ctx context.Context,
nodeName string,
) (*VirtualEnvironmentNodeGetTimeResponseData, error) {
resBody := &VirtualEnvironmentNodeGetTimeResponseBody{}
err := c.DoRequest(
ctx,
http.MethodGet,
fmt.Sprintf("nodes/%s/time", url.PathEscape(nodeName)),
nil,
resBody,
)
if err != nil {
return nil, err
}
if resBody.Data == nil {
return nil, errors.New("the server did not include a data object in the response")
}
return resBody.Data, nil
}
// GetNodeTaskStatus retrieves the status of a node task.
func (c *VirtualEnvironmentClient) GetNodeTaskStatus(
ctx context.Context,
nodeName string,
upid string,
) (*VirtualEnvironmentNodeGetTaskStatusResponseData, error) {
resBody := &VirtualEnvironmentNodeGetTaskStatusResponseBody{}
err := c.DoRequest(
ctx,
http.MethodGet,
fmt.Sprintf("nodes/%s/tasks/%s/status", url.PathEscape(nodeName), url.PathEscape(upid)),
nil,
resBody,
)
if err != nil {
return nil, err
}
if resBody.Data == nil {
return nil, errors.New("the server did not include a data object in the response")
}
return resBody.Data, nil
}
// ListNodeNetworkDevices retrieves a list of network devices for a specific nodes.
func (c *VirtualEnvironmentClient) ListNodeNetworkDevices(
ctx context.Context,
nodeName string,
) ([]*VirtualEnvironmentNodeNetworkDeviceListResponseData, error) {
resBody := &VirtualEnvironmentNodeNetworkDeviceListResponseBody{}
err := c.DoRequest(
ctx,
http.MethodGet,
fmt.Sprintf("nodes/%s/network", url.PathEscape(nodeName)),
nil,
resBody,
)
if err != nil {
return nil, err
}
if resBody.Data == nil {
return nil, errors.New("the server did not include a data object in the response")
}
sort.Slice(resBody.Data, func(i, j int) bool {
return resBody.Data[i].Priority < resBody.Data[j].Priority
})
return resBody.Data, nil
}
// ListNodes retrieves a list of nodes.
func (c *VirtualEnvironmentClient) ListNodes(
ctx context.Context,
) ([]*VirtualEnvironmentNodeListResponseData, error) {
resBody := &VirtualEnvironmentNodeListResponseBody{}
err := c.DoRequest(ctx, http.MethodGet, "nodes", nil, resBody)
if err != nil {
return nil, err
}
if resBody.Data == nil {
return nil, errors.New("the server did not include a data object in the response")
}
sort.Slice(resBody.Data, func(i, j int) bool {
return resBody.Data[i].Name < resBody.Data[j].Name
})
return resBody.Data, nil
}
// OpenNodeShell establishes a new SSH connection to a node.
func (c *VirtualEnvironmentClient) OpenNodeShell(
ctx context.Context,
nodeName string,
) (*ssh.Client, error) {
nodeAddress, err := c.GetNodeIP(ctx, nodeName)
if err != nil {
return nil, err
}
homeDir, err := os.UserHomeDir()
if err != nil {
return nil, fmt.Errorf("failed to determine the home directory: %w", err)
}
sshHost := fmt.Sprintf("%s:22", *nodeAddress)
sshPath := path.Join(homeDir, ".ssh")
if _, err = os.Stat(sshPath); os.IsNotExist(err) {
e := os.Mkdir(sshPath, 0o700)
if e != nil {
return nil, fmt.Errorf("failed to create %s: %w", sshPath, e)
}
}
khPath := path.Join(sshPath, "known_hosts")
if _, err = os.Stat(khPath); os.IsNotExist(err) {
e := os.WriteFile(khPath, []byte{}, 0o600)
if e != nil {
return nil, fmt.Errorf("failed to create %s: %w", khPath, e)
}
}
kh, err := knownhosts.New(khPath)
if err != nil {
return nil, fmt.Errorf("failed to read %s: %w", khPath, err)
}
// Create a custom permissive hostkey callback which still errors on hosts
// with changed keys, but allows unknown hosts and adds them to known_hosts
cb := ssh.HostKeyCallback(func(hostname string, remote net.Addr, key ssh.PublicKey) error {
err := kh(hostname, remote, key)
if knownhosts.IsHostKeyChanged(err) {
return fmt.Errorf("REMOTE HOST IDENTIFICATION HAS CHANGED for host %s! This may indicate a MitM attack", hostname)
}
if knownhosts.IsHostUnknown(err) {
f, ferr := os.OpenFile(khPath, os.O_APPEND|os.O_WRONLY, 0o600)
if ferr == nil {
defer CloseOrLogError(ctx)(f)
ferr = knownhosts.WriteKnownHost(f, hostname, remote, key)
}
if ferr == nil {
tflog.Info(ctx, fmt.Sprintf("Added host %s to known_hosts", hostname))
} else {
tflog.Error(ctx, fmt.Sprintf("Failed to add host %s to known_hosts", hostname), map[string]interface{}{
"error": err,
})
}
return nil
}
return err
})
sshConfig := &ssh.ClientConfig{
User: c.SSHUsername,
Auth: []ssh.AuthMethod{ssh.Password(c.SSHPassword)},
HostKeyCallback: cb,
HostKeyAlgorithms: kh.HostKeyAlgorithms(sshHost),
}
tflog.Info(ctx, fmt.Sprintf("Agent is set to %t", c.SSHAgent))
if c.SSHAgent {
sshClient, err := c.CreateSSHClientAgent(ctx, cb, kh, sshHost)
if err != nil {
tflog.Error(ctx, "Failed ssh connection through agent, "+
"falling back to password authentication",
map[string]interface{}{
"error": err,
})
} else {
return sshClient, nil
}
}
sshClient, err := ssh.Dial("tcp", sshHost, sshConfig)
if err != nil {
return nil, fmt.Errorf("failed to dial %s: %w", sshHost, err)
}
tflog.Debug(ctx, "SSH connection established", map[string]interface{}{
"host": sshHost,
"user": c.SSHUsername,
})
return sshClient, nil
}
// CreateSSHClientAgent establishes an ssh connection through the agent authentication mechanism
func (c *VirtualEnvironmentClient) CreateSSHClientAgent(
ctx context.Context,
cb ssh.HostKeyCallback,
kh knownhosts.HostKeyCallback,
sshHost string,
) (*ssh.Client, error) {
if c.SSHAgentSocket == "" {
return nil, errors.New("failed connecting to SSH agent socket: the socket file is not defined, " +
"authentication will fall back to password")
}
conn, err := net.Dial("unix", c.SSHAgentSocket)
if err != nil {
return nil, fmt.Errorf("failed connecting to SSH auth socket '%s': %w", c.SSHAgentSocket, err)
}
ag := agent.NewClient(conn)
sshConfig := &ssh.ClientConfig{
User: c.SSHUsername,
Auth: []ssh.AuthMethod{ssh.PublicKeysCallback(ag.Signers), ssh.Password(c.SSHPassword)},
HostKeyCallback: cb,
HostKeyAlgorithms: kh.HostKeyAlgorithms(sshHost),
}
sshClient, err := ssh.Dial("tcp", sshHost, sshConfig)
if err != nil {
return nil, fmt.Errorf("failed to dial %s: %w", sshHost, err)
}
tflog.Debug(ctx, "SSH connection established", map[string]interface{}{
"host": sshHost,
"user": c.SSHUsername,
})
return sshClient, nil
}
// UpdateNodeTime updates the time on a node.
func (c *VirtualEnvironmentClient) UpdateNodeTime(
ctx context.Context,
nodeName string,
d *VirtualEnvironmentNodeUpdateTimeRequestBody,
) error {
return c.DoRequest(ctx, http.MethodPut, fmt.Sprintf("nodes/%s/time", url.PathEscape(nodeName)), d, nil)
}
// WaitForNodeTask waits for a specific node task to complete.
func (c *VirtualEnvironmentClient) WaitForNodeTask(
ctx context.Context,
nodeName string,
upid string,
timeout int,
delay int,
) error {
timeDelay := int64(delay)
timeMax := float64(timeout)
timeStart := time.Now()
timeElapsed := timeStart.Sub(timeStart)
for timeElapsed.Seconds() < timeMax {
if int64(timeElapsed.Seconds())%timeDelay == 0 {
status, err := c.GetNodeTaskStatus(ctx, nodeName, upid)
if err != nil {
return err
}
if status.Status != "running" {
if status.ExitCode != "OK" {
return fmt.Errorf(
"task \"%s\" on node \"%s\" failed to complete with error: %s",
upid,
nodeName,
status.ExitCode,
)
}
return nil
}
time.Sleep(1 * time.Second)
}
time.Sleep(200 * time.Millisecond)
timeElapsed = time.Since(timeStart)
if ctx.Err() != nil {
return ctx.Err()
}
}
return fmt.Errorf(
"timeout while waiting for task \"%s\" on node \"%s\" to complete",
upid,
nodeName,
)
}

View File

@ -1,104 +0,0 @@
/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at https://mozilla.org/MPL/2.0/. */
package proxmox
import (
"encoding/json"
"net/url"
"github.com/bpg/terraform-provider-proxmox/proxmox/types"
)
// CustomNodeCommands contains an array of commands to execute.
type CustomNodeCommands []string
// VirtualEnvironmentNodeExecuteRequestBody contains the data for a node execute request.
type VirtualEnvironmentNodeExecuteRequestBody struct {
Commands CustomNodeCommands `json:"commands" url:"commands"`
}
// VirtualEnvironmentNodeGetTimeResponseBody contains the body from a node time zone get response.
type VirtualEnvironmentNodeGetTimeResponseBody struct {
Data *VirtualEnvironmentNodeGetTimeResponseData `json:"data,omitempty"`
}
// VirtualEnvironmentNodeGetTimeResponseData contains the data from a node list response.
type VirtualEnvironmentNodeGetTimeResponseData struct {
LocalTime types.CustomTimestamp `json:"localtime"`
TimeZone string `json:"timezone"`
UTCTime types.CustomTimestamp `json:"time"`
}
// VirtualEnvironmentNodeGetTaskStatusResponseBody contains the body from a node get task status response.
type VirtualEnvironmentNodeGetTaskStatusResponseBody struct {
Data *VirtualEnvironmentNodeGetTaskStatusResponseData `json:"data,omitempty"`
}
// VirtualEnvironmentNodeGetTaskStatusResponseData contains the data from a node get task status response.
type VirtualEnvironmentNodeGetTaskStatusResponseData struct {
PID int `json:"pid,omitempty"`
Status string `json:"status,omitempty"`
ExitCode string `json:"exitstatus,omitempty"`
}
// VirtualEnvironmentNodeListResponseBody contains the body from a node list response.
type VirtualEnvironmentNodeListResponseBody struct {
Data []*VirtualEnvironmentNodeListResponseData `json:"data,omitempty"`
}
// VirtualEnvironmentNodeListResponseData contains the data from a node list response.
type VirtualEnvironmentNodeListResponseData struct {
CPUCount *int `json:"maxcpu,omitempty"`
CPUUtilization *float64 `json:"cpu,omitempty"`
MemoryAvailable *int `json:"maxmem,omitempty"`
MemoryUsed *int `json:"mem,omitempty"`
Name string `json:"node"`
SSLFingerprint *string `json:"ssl_fingerprint,omitempty"`
Status *string `json:"status"`
SupportLevel *string `json:"level,omitempty"`
Uptime *int `json:"uptime"`
}
// VirtualEnvironmentNodeNetworkDeviceListResponseBody contains the body from a node network device list response.
type VirtualEnvironmentNodeNetworkDeviceListResponseBody struct {
Data []*VirtualEnvironmentNodeNetworkDeviceListResponseData `json:"data,omitempty"`
}
// VirtualEnvironmentNodeNetworkDeviceListResponseData contains the data from a node network device list response.
type VirtualEnvironmentNodeNetworkDeviceListResponseData struct {
Active *types.CustomBool `json:"active,omitempty"`
Address *string `json:"address,omitempty"`
Autostart *types.CustomBool `json:"autostart,omitempty"`
BridgeFD *string `json:"bridge_fd,omitempty"`
BridgePorts *string `json:"bridge_ports,omitempty"`
BridgeSTP *string `json:"bridge_stp,omitempty"`
CIDR *string `json:"cidr,omitempty"`
Exists *types.CustomBool `json:"exists,omitempty"`
Families *[]string `json:"families,omitempty"`
Gateway *string `json:"gateway,omitempty"`
Iface string `json:"iface"`
MethodIPv4 *string `json:"method,omitempty"`
MethodIPv6 *string `json:"method6,omitempty"`
Netmask *string `json:"netmask,omitempty"`
Priority int `json:"priority"`
Type string `json:"type"`
}
// VirtualEnvironmentNodeUpdateTimeRequestBody contains the body for a node time update request.
type VirtualEnvironmentNodeUpdateTimeRequestBody struct {
TimeZone string `json:"timezone" url:"timezone"`
}
// EncodeValues converts a CustomNodeCommands array to a JSON encoded URL vlaue.
func (r CustomNodeCommands) EncodeValues(key string, v *url.Values) error {
jsonArrayBytes, err := json.Marshal(r)
if err != nil {
return err
}
v.Add(key, string(jsonArrayBytes))
return nil
}

View File

@ -1,79 +0,0 @@
/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at https://mozilla.org/MPL/2.0/. */
package proxmox
import (
"context"
"errors"
"fmt"
"net/http"
"net/url"
"sort"
)
// CreatePool creates a pool.
func (c *VirtualEnvironmentClient) CreatePool(
ctx context.Context,
d *VirtualEnvironmentPoolCreateRequestBody,
) error {
return c.DoRequest(ctx, http.MethodPost, "pools", d, nil)
}
// DeletePool deletes a pool.
func (c *VirtualEnvironmentClient) DeletePool(ctx context.Context, id string) error {
return c.DoRequest(ctx, http.MethodDelete, fmt.Sprintf("pools/%s", url.PathEscape(id)), nil, nil)
}
// GetPool retrieves a pool.
func (c *VirtualEnvironmentClient) GetPool(
ctx context.Context,
id string,
) (*VirtualEnvironmentPoolGetResponseData, error) {
resBody := &VirtualEnvironmentPoolGetResponseBody{}
err := c.DoRequest(ctx, http.MethodGet, fmt.Sprintf("pools/%s", url.PathEscape(id)), nil, resBody)
if err != nil {
return nil, err
}
if resBody.Data == nil {
return nil, errors.New("the server did not include a data object in the response")
}
sort.Slice(resBody.Data.Members, func(i, j int) bool {
return resBody.Data.Members[i].ID < resBody.Data.Members[j].ID
})
return resBody.Data, nil
}
// ListPools retrieves a list of pools.
func (c *VirtualEnvironmentClient) ListPools(
ctx context.Context,
) ([]*VirtualEnvironmentPoolListResponseData, error) {
resBody := &VirtualEnvironmentPoolListResponseBody{}
err := c.DoRequest(ctx, http.MethodGet, "pools", nil, resBody)
if err != nil {
return nil, err
}
if resBody.Data == nil {
return nil, errors.New("the server did not include a data object in the response")
}
sort.Slice(resBody.Data, func(i, j int) bool {
return resBody.Data[i].ID < resBody.Data[j].ID
})
return resBody.Data, nil
}
// UpdatePool updates a pool.
func (c *VirtualEnvironmentClient) UpdatePool(
ctx context.Context,
id string,
d *VirtualEnvironmentPoolUpdateRequestBody,
) error {
return c.DoRequest(ctx, http.MethodPut, fmt.Sprintf("pools/%s", url.PathEscape(id)), d, nil)
}

View File

@ -1,47 +0,0 @@
/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at https://mozilla.org/MPL/2.0/. */
package proxmox
// VirtualEnvironmentPoolCreateRequestBody contains the data for an pool create request.
type VirtualEnvironmentPoolCreateRequestBody struct {
Comment *string `json:"comment,omitempty" url:"comment,omitempty"`
ID string `json:"groupid" url:"poolid"`
}
// VirtualEnvironmentPoolGetResponseBody contains the body from an pool get response.
type VirtualEnvironmentPoolGetResponseBody struct {
Data *VirtualEnvironmentPoolGetResponseData `json:"data,omitempty"`
}
// VirtualEnvironmentPoolGetResponseData contains the data from an pool get response.
type VirtualEnvironmentPoolGetResponseData struct {
Comment *string `json:"comment,omitempty"`
Members []VirtualEnvironmentPoolGetResponseMembers `json:"members,omitempty"`
}
// VirtualEnvironmentPoolGetResponseMembers contains the members data from an pool get response.
type VirtualEnvironmentPoolGetResponseMembers struct {
ID string `json:"id"`
Node string `json:"node"`
DatastoreID *string `json:"storage,omitempty"`
Type string `json:"type"`
VMID *int `json:"vmid"`
}
// VirtualEnvironmentPoolListResponseBody contains the body from an pool list response.
type VirtualEnvironmentPoolListResponseBody struct {
Data []*VirtualEnvironmentPoolListResponseData `json:"data,omitempty"`
}
// VirtualEnvironmentPoolListResponseData contains the data from an pool list response.
type VirtualEnvironmentPoolListResponseData struct {
Comment *string `json:"comment,omitempty"`
ID string `json:"poolid"`
}
// VirtualEnvironmentPoolUpdateRequestBody contains the data for an pool update request.
type VirtualEnvironmentPoolUpdateRequestBody struct {
Comment *string `json:"comment,omitempty" url:"comment,omitempty"`
}

View File

@ -1,85 +0,0 @@
/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at https://mozilla.org/MPL/2.0/. */
package proxmox
import (
"context"
"errors"
"fmt"
"net/http"
"net/url"
"sort"
"github.com/bpg/terraform-provider-proxmox/proxmox/types"
)
// CreateRole creates an access role.
func (c *VirtualEnvironmentClient) CreateRole(
ctx context.Context,
d *VirtualEnvironmentRoleCreateRequestBody,
) error {
return c.DoRequest(ctx, http.MethodPost, "access/roles", d, nil)
}
// DeleteRole deletes an access role.
func (c *VirtualEnvironmentClient) DeleteRole(ctx context.Context, id string) error {
return c.DoRequest(ctx, http.MethodDelete, fmt.Sprintf("access/roles/%s", url.PathEscape(id)), nil, nil)
}
// GetRole retrieves an access role.
func (c *VirtualEnvironmentClient) GetRole(
ctx context.Context,
id string,
) (*types.CustomPrivileges, error) {
resBody := &VirtualEnvironmentRoleGetResponseBody{}
err := c.DoRequest(ctx, http.MethodGet, fmt.Sprintf("access/roles/%s", url.PathEscape(id)), nil, resBody)
if err != nil {
return nil, err
}
if resBody.Data == nil {
return nil, errors.New("the server did not include a data object in the response")
}
sort.Strings(*resBody.Data)
return resBody.Data, nil
}
// ListRoles retrieves a list of access roles.
func (c *VirtualEnvironmentClient) ListRoles(
ctx context.Context,
) ([]*VirtualEnvironmentRoleListResponseData, error) {
resBody := &VirtualEnvironmentRoleListResponseBody{}
err := c.DoRequest(ctx, http.MethodGet, "access/roles", nil, resBody)
if err != nil {
return nil, err
}
if resBody.Data == nil {
return nil, errors.New("the server did not include a data object in the response")
}
sort.Slice(resBody.Data, func(i, j int) bool {
return resBody.Data[i].ID < resBody.Data[j].ID
})
for i := range resBody.Data {
if resBody.Data[i].Privileges != nil {
sort.Strings(*resBody.Data[i].Privileges)
}
}
return resBody.Data, nil
}
// UpdateRole updates an access role.
func (c *VirtualEnvironmentClient) UpdateRole(
ctx context.Context,
id string,
d *VirtualEnvironmentRoleUpdateRequestBody,
) error {
return c.DoRequest(ctx, http.MethodPut, fmt.Sprintf("access/roles/%s", url.PathEscape(id)), d, nil)
}

View File

@ -1,35 +0,0 @@
/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at https://mozilla.org/MPL/2.0/. */
package proxmox
import "github.com/bpg/terraform-provider-proxmox/proxmox/types"
// VirtualEnvironmentRoleCreateRequestBody contains the data for an access group create request.
type VirtualEnvironmentRoleCreateRequestBody struct {
ID string `json:"roleid" url:"roleid"`
Privileges types.CustomPrivileges `json:"privs" url:"privs,comma"`
}
// VirtualEnvironmentRoleGetResponseBody contains the body from an access group get response.
type VirtualEnvironmentRoleGetResponseBody struct {
Data *types.CustomPrivileges `json:"data,omitempty"`
}
// VirtualEnvironmentRoleListResponseBody contains the body from an access group list response.
type VirtualEnvironmentRoleListResponseBody struct {
Data []*VirtualEnvironmentRoleListResponseData `json:"data,omitempty"`
}
// VirtualEnvironmentRoleListResponseData contains the data from an access group list response.
type VirtualEnvironmentRoleListResponseData struct {
ID string `json:"roleid"`
Privileges *types.CustomPrivileges `json:"privs,omitempty"`
Special *types.CustomBool `json:"special,omitempty"`
}
// VirtualEnvironmentRoleUpdateRequestBody contains the data for an access group update request.
type VirtualEnvironmentRoleUpdateRequestBody struct {
Privileges types.CustomPrivileges `json:"privs" url:"privs,comma"`
}

View File

@ -1,111 +0,0 @@
/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at https://mozilla.org/MPL/2.0/. */
package proxmox
import (
"context"
"errors"
"fmt"
"net/http"
"net/url"
"sort"
"time"
"github.com/bpg/terraform-provider-proxmox/proxmox/types"
)
// ChangeUserPassword changes a user's password.
func (c *VirtualEnvironmentClient) ChangeUserPassword(
ctx context.Context,
id, password string,
) error {
d := VirtualEnvironmentUserChangePasswordRequestBody{
ID: id,
Password: password,
}
return c.DoRequest(ctx, http.MethodPut, "access/password", d, nil)
}
// CreateUser creates a user.
func (c *VirtualEnvironmentClient) CreateUser(
ctx context.Context,
d *VirtualEnvironmentUserCreateRequestBody,
) error {
return c.DoRequest(ctx, http.MethodPost, "access/users", d, nil)
}
// DeleteUser deletes an user.
func (c *VirtualEnvironmentClient) DeleteUser(ctx context.Context, id string) error {
return c.DoRequest(ctx, http.MethodDelete, fmt.Sprintf("access/users/%s", url.PathEscape(id)), nil, nil)
}
// GetUser retrieves a user.
func (c *VirtualEnvironmentClient) GetUser(
ctx context.Context,
id string,
) (*VirtualEnvironmentUserGetResponseData, error) {
resBody := &VirtualEnvironmentUserGetResponseBody{}
err := c.DoRequest(ctx, http.MethodGet, fmt.Sprintf("access/users/%s", url.PathEscape(id)), nil, resBody)
if err != nil {
return nil, err
}
if resBody.Data == nil {
return nil, errors.New("the server did not include a data object in the response")
}
if resBody.Data.ExpirationDate != nil {
expirationDate := types.CustomTimestamp(time.Time(*resBody.Data.ExpirationDate).UTC())
resBody.Data.ExpirationDate = &expirationDate
}
if resBody.Data.Groups != nil {
sort.Strings(*resBody.Data.Groups)
}
return resBody.Data, nil
}
// ListUsers retrieves a list of users.
func (c *VirtualEnvironmentClient) ListUsers(
ctx context.Context,
) ([]*VirtualEnvironmentUserListResponseData, error) {
resBody := &VirtualEnvironmentUserListResponseBody{}
err := c.DoRequest(ctx, http.MethodGet, "access/users", nil, resBody)
if err != nil {
return nil, err
}
if resBody.Data == nil {
return nil, errors.New("the server did not include a data object in the response")
}
sort.Slice(resBody.Data, func(i, j int) bool {
return resBody.Data[i].ID < resBody.Data[j].ID
})
for i := range resBody.Data {
if resBody.Data[i].ExpirationDate != nil {
expirationDate := types.CustomTimestamp(time.Time(*resBody.Data[i].ExpirationDate).UTC())
resBody.Data[i].ExpirationDate = &expirationDate
}
if resBody.Data[i].Groups != nil {
sort.Strings(*resBody.Data[i].Groups)
}
}
return resBody.Data, nil
}
// UpdateUser updates a user.
func (c *VirtualEnvironmentClient) UpdateUser(
ctx context.Context,
id string,
d *VirtualEnvironmentUserUpdateRequestBody,
) error {
return c.DoRequest(ctx, http.MethodPut, fmt.Sprintf("access/users/%s", url.PathEscape(id)), d, nil)
}

View File

@ -1,28 +0,0 @@
/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at https://mozilla.org/MPL/2.0/. */
package proxmox
import (
"context"
"errors"
"net/http"
)
// Version retrieves the version information.
func (c *VirtualEnvironmentClient) Version(
ctx context.Context,
) (*VirtualEnvironmentVersionResponseData, error) {
resBody := &VirtualEnvironmentVersionResponseBody{}
err := c.DoRequest(ctx, http.MethodGet, "version", nil, resBody)
if err != nil {
return nil, err
}
if resBody.Data == nil {
return nil, errors.New("the server did not include a data object in the response")
}
return resBody.Data, nil
}

View File

@ -1,18 +0,0 @@
/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at https://mozilla.org/MPL/2.0/. */
package proxmox
// VirtualEnvironmentVersionResponseBody contains the body from a version response.
type VirtualEnvironmentVersionResponseBody struct {
Data *VirtualEnvironmentVersionResponseData `json:"data,omitempty"`
}
// VirtualEnvironmentVersionResponseData contains the data from a version response.
type VirtualEnvironmentVersionResponseData struct {
Keyboard string `json:"keyboard"`
Release string `json:"release"`
RepositoryID string `json:"repoid"`
Version string `json:"version"`
}

View File

@ -1,837 +0,0 @@
/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at https://mozilla.org/MPL/2.0/. */
package proxmox
import (
"context"
"errors"
"fmt"
"net"
"net/http"
"net/url"
"strings"
"sync"
"time"
"github.com/hashicorp/terraform-plugin-log/tflog"
)
const (
getVMIDStep = 1
)
var (
getVMIDCounter = -1
getVMIDCounterMutex = &sync.Mutex{}
)
// CloneVM clones a virtual machine.
func (c *VirtualEnvironmentClient) CloneVM(
ctx context.Context,
nodeName string,
vmID int,
retries int,
d *VirtualEnvironmentVMCloneRequestBody,
timeout int,
) error {
resBody := &VirtualEnvironmentVMMoveDiskResponseBody{}
var err error
// just a guard in case someone sets retries to 0 unknowingly
if retries <= 0 {
retries = 1
}
for i := 0; i < retries; i++ {
err = c.DoRequest(
ctx,
http.MethodPost,
fmt.Sprintf("nodes/%s/qemu/%d/clone", url.PathEscape(nodeName), vmID),
d,
resBody,
)
if err != nil {
return err
}
if resBody.Data == nil {
return errors.New("the server did not include a data object in the response")
}
err = c.WaitForNodeTask(ctx, nodeName, *resBody.Data, timeout, 5)
if err == nil {
return nil
}
time.Sleep(10 * time.Second)
}
return err
}
// CreateVM creates a virtual machine.
func (c *VirtualEnvironmentClient) CreateVM(
ctx context.Context,
nodeName string,
d *VirtualEnvironmentVMCreateRequestBody,
timeout int,
) error {
taskID, err := c.CreateVMAsync(ctx, nodeName, d)
if err != nil {
return err
}
err = c.WaitForNodeTask(ctx, nodeName, *taskID, timeout, 1)
if err != nil {
return fmt.Errorf("error waiting for VM creation: %w", err)
}
return nil
}
// CreateVMAsync creates a virtual machine asynchronously.
func (c *VirtualEnvironmentClient) CreateVMAsync(
ctx context.Context,
nodeName string,
d *VirtualEnvironmentVMCreateRequestBody,
) (*string, error) {
resBody := &VirtualEnvironmentVMCreateResponseBody{}
err := c.DoRequest(
ctx,
http.MethodPost,
fmt.Sprintf("nodes/%s/qemu", url.PathEscape(nodeName)),
d,
resBody,
)
if err != nil {
return nil, err
}
if resBody.Data == nil {
return nil, errors.New("the server did not include a data object in the response")
}
return resBody.Data, nil
}
// DeleteVM deletes a virtual machine.
func (c *VirtualEnvironmentClient) DeleteVM(ctx context.Context, nodeName string, vmID int) error {
return c.DoRequest(
ctx,
http.MethodDelete,
fmt.Sprintf(
"nodes/%s/qemu/%d?destroy-unreferenced-disks=1&purge=1",
url.PathEscape(nodeName),
vmID,
),
nil,
nil,
)
}
// GetVM retrieves a virtual machine.
func (c *VirtualEnvironmentClient) GetVM(
ctx context.Context,
nodeName string,
vmID int,
) (*VirtualEnvironmentVMGetResponseData, error) {
resBody := &VirtualEnvironmentVMGetResponseBody{}
err := c.DoRequest(
ctx,
http.MethodGet,
fmt.Sprintf("nodes/%s/qemu/%d/config", url.PathEscape(nodeName), vmID),
nil,
resBody,
)
if err != nil {
return nil, err
}
if resBody.Data == nil {
return nil, errors.New("the server did not include a data object in the response")
}
return resBody.Data, nil
}
// GetVMID retrieves the next available VM identifier.
func (c *VirtualEnvironmentClient) GetVMID(ctx context.Context) (*int, error) {
getVMIDCounterMutex.Lock()
defer getVMIDCounterMutex.Unlock()
if getVMIDCounter < 0 {
nextVMID, err := c.API().Cluster().GetNextID(ctx, nil)
if err != nil {
return nil, err
}
if nextVMID == nil {
return nil, errors.New("unable to retrieve the next available VM identifier")
}
getVMIDCounter = *nextVMID + getVMIDStep
tflog.Debug(ctx, "next VM identifier", map[string]interface{}{
"id": *nextVMID,
})
return nextVMID, nil
}
vmID := getVMIDCounter
for vmID <= 2147483637 {
_, err := c.API().Cluster().GetNextID(ctx, &vmID)
if err != nil {
vmID += getVMIDStep
continue
}
getVMIDCounter = vmID + getVMIDStep
tflog.Debug(ctx, "next VM identifier", map[string]interface{}{
"id": vmID,
})
return &vmID, nil
}
return nil, errors.New("unable to determine the next available VM identifier")
}
// GetVMNetworkInterfacesFromAgent retrieves the network interfaces reported by the QEMU agent.
func (c *VirtualEnvironmentClient) GetVMNetworkInterfacesFromAgent(
ctx context.Context,
nodeName string,
vmID int,
) (*VirtualEnvironmentVMGetQEMUNetworkInterfacesResponseData, error) {
resBody := &VirtualEnvironmentVMGetQEMUNetworkInterfacesResponseBody{}
err := c.DoRequest(
ctx,
http.MethodGet,
fmt.Sprintf(
"nodes/%s/qemu/%d/agent/network-get-interfaces",
url.PathEscape(nodeName),
vmID,
),
nil,
resBody,
)
if err != nil {
return nil, err
}
if resBody.Data == nil {
return nil, errors.New("the server did not include a data object in the response")
}
return resBody.Data, nil
}
// GetVMStatus retrieves the status for a virtual machine.
func (c *VirtualEnvironmentClient) GetVMStatus(
ctx context.Context,
nodeName string,
vmID int,
) (*VirtualEnvironmentVMGetStatusResponseData, error) {
resBody := &VirtualEnvironmentVMGetStatusResponseBody{}
err := c.DoRequest(
ctx,
http.MethodGet,
fmt.Sprintf("nodes/%s/qemu/%d/status/current", url.PathEscape(nodeName), vmID),
nil,
resBody,
)
if err != nil {
return nil, err
}
if resBody.Data == nil {
return nil, errors.New("the server did not include a data object in the response")
}
return resBody.Data, nil
}
// MigrateVM migrates a virtual machine.
func (c *VirtualEnvironmentClient) MigrateVM(
ctx context.Context,
nodeName string,
vmID int,
d *VirtualEnvironmentVMMigrateRequestBody,
timeout int,
) error {
taskID, err := c.MigrateVMAsync(ctx, nodeName, vmID, d)
if err != nil {
return err
}
err = c.WaitForNodeTask(ctx, nodeName, *taskID, timeout, 5)
if err != nil {
return err
}
return nil
}
// MigrateVMAsync migrates a virtual machine asynchronously.
func (c *VirtualEnvironmentClient) MigrateVMAsync(
ctx context.Context,
nodeName string,
vmID int,
d *VirtualEnvironmentVMMigrateRequestBody,
) (*string, error) {
resBody := &VirtualEnvironmentVMMigrateResponseBody{}
err := c.DoRequest(
ctx,
http.MethodPost,
fmt.Sprintf("nodes/%s/qemu/%d/migrate", url.PathEscape(nodeName), vmID),
d,
resBody,
)
if err != nil {
return nil, err
}
if resBody.Data == nil {
return nil, errors.New("the server did not include a data object in the response")
}
return resBody.Data, nil
}
// MoveVMDisk moves a virtual machine disk.
func (c *VirtualEnvironmentClient) MoveVMDisk(
ctx context.Context,
nodeName string,
vmID int,
d *VirtualEnvironmentVMMoveDiskRequestBody,
timeout int,
) error {
taskID, err := c.MoveVMDiskAsync(ctx, nodeName, vmID, d)
if err != nil {
if strings.Contains(err.Error(), "you can't move to the same storage with same format") {
// if someone tries to move to the same storage, the move is considered to be successful
return nil
}
return err
}
err = c.WaitForNodeTask(ctx, nodeName, *taskID, timeout, 5)
if err != nil {
return err
}
return nil
}
// MoveVMDiskAsync moves a virtual machine disk asynchronously.
func (c *VirtualEnvironmentClient) MoveVMDiskAsync(
ctx context.Context,
nodeName string,
vmID int,
d *VirtualEnvironmentVMMoveDiskRequestBody,
) (*string, error) {
resBody := &VirtualEnvironmentVMMoveDiskResponseBody{}
err := c.DoRequest(
ctx,
http.MethodPost,
fmt.Sprintf("nodes/%s/qemu/%d/move_disk", url.PathEscape(nodeName), vmID),
d,
resBody,
)
if err != nil {
return nil, err
}
if resBody.Data == nil {
return nil, errors.New("the server did not include a data object in the response")
}
return resBody.Data, nil
}
// ListVMs retrieves a list of virtual machines.
func (c *VirtualEnvironmentClient) ListVMs(
ctx context.Context,
nodeName string,
) ([]*VirtualEnvironmentVMListResponseData, error) {
resBody := &VirtualEnvironmentVMListResponseBody{}
err := c.DoRequest(
ctx,
http.MethodGet,
fmt.Sprintf("nodes/%s/qemu", url.PathEscape(nodeName)),
nil,
resBody,
)
if err != nil {
return nil, err
}
if resBody.Data == nil {
return nil, errors.New("the server did not include a data object in the response")
}
return resBody.Data, nil
}
// RebootVM reboots a virtual machine.
func (c *VirtualEnvironmentClient) RebootVM(
ctx context.Context,
nodeName string,
vmID int,
d *VirtualEnvironmentVMRebootRequestBody,
timeout int,
) error {
taskID, err := c.RebootVMAsync(ctx, nodeName, vmID, d)
if err != nil {
return err
}
err = c.WaitForNodeTask(ctx, nodeName, *taskID, timeout, 5)
if err != nil {
return err
}
return nil
}
// RebootVMAsync reboots a virtual machine asynchronously.
func (c *VirtualEnvironmentClient) RebootVMAsync(
ctx context.Context,
nodeName string,
vmID int,
d *VirtualEnvironmentVMRebootRequestBody,
) (*string, error) {
resBody := &VirtualEnvironmentVMRebootResponseBody{}
err := c.DoRequest(
ctx,
http.MethodPost,
fmt.Sprintf("nodes/%s/qemu/%d/status/reboot", url.PathEscape(nodeName), vmID),
d,
resBody,
)
if err != nil {
return nil, err
}
if resBody.Data == nil {
return nil, errors.New("the server did not include a data object in the response")
}
return resBody.Data, nil
}
// ResizeVMDisk resizes a virtual machine disk.
func (c *VirtualEnvironmentClient) ResizeVMDisk(
ctx context.Context,
nodeName string,
vmID int,
d *VirtualEnvironmentVMResizeDiskRequestBody,
) error {
var err error
tflog.Debug(ctx, "resize disk", map[string]interface{}{
"disk": d.Disk,
"size": d.Size,
})
for i := 0; i < 5; i++ {
err = c.DoRequest(
ctx,
http.MethodPut,
fmt.Sprintf("nodes/%s/qemu/%d/resize", url.PathEscape(nodeName), vmID),
d,
nil,
)
if err == nil {
return nil
}
tflog.Debug(ctx, "resize disk failed", map[string]interface{}{
"retry": i,
})
time.Sleep(5 * time.Second)
if ctx.Err() != nil {
return ctx.Err()
}
}
return err
}
// ShutdownVM shuts down a virtual machine.
func (c *VirtualEnvironmentClient) ShutdownVM(
ctx context.Context,
nodeName string,
vmID int,
d *VirtualEnvironmentVMShutdownRequestBody,
timeout int,
) error {
taskID, err := c.ShutdownVMAsync(ctx, nodeName, vmID, d)
if err != nil {
return err
}
err = c.WaitForNodeTask(ctx, nodeName, *taskID, timeout, 5)
if err != nil {
return err
}
return nil
}
// ShutdownVMAsync shuts down a virtual machine asynchronously.
func (c *VirtualEnvironmentClient) ShutdownVMAsync(
ctx context.Context,
nodeName string,
vmID int,
d *VirtualEnvironmentVMShutdownRequestBody,
) (*string, error) {
resBody := &VirtualEnvironmentVMShutdownResponseBody{}
err := c.DoRequest(
ctx,
http.MethodPost,
fmt.Sprintf("nodes/%s/qemu/%d/status/shutdown", url.PathEscape(nodeName), vmID),
d,
resBody,
)
if err != nil {
return nil, err
}
if resBody.Data == nil {
return nil, errors.New("the server did not include a data object in the response")
}
return resBody.Data, nil
}
// StartVM starts a virtual machine.
func (c *VirtualEnvironmentClient) StartVM(
ctx context.Context,
nodeName string,
vmID int,
timeout int,
) error {
taskID, err := c.StartVMAsync(ctx, nodeName, vmID)
if err != nil {
return err
}
err = c.WaitForNodeTask(ctx, nodeName, *taskID, timeout, 5)
if err != nil {
return err
}
return nil
}
// StartVMAsync starts a virtual machine asynchronously.
func (c *VirtualEnvironmentClient) StartVMAsync(
ctx context.Context,
nodeName string,
vmID int,
) (*string, error) {
resBody := &VirtualEnvironmentVMStartResponseBody{}
err := c.DoRequest(
ctx,
http.MethodPost,
fmt.Sprintf("nodes/%s/qemu/%d/status/start", url.PathEscape(nodeName), vmID),
nil,
resBody,
)
if err != nil {
return nil, err
}
if resBody.Data == nil {
return nil, errors.New("the server did not include a data object in the response")
}
return resBody.Data, nil
}
// StopVM stops a virtual machine.
func (c *VirtualEnvironmentClient) StopVM(
ctx context.Context,
nodeName string,
vmID int,
timeout int,
) error {
taskID, err := c.StopVMAsync(ctx, nodeName, vmID)
if err != nil {
return err
}
err = c.WaitForNodeTask(ctx, nodeName, *taskID, timeout, 5)
if err != nil {
return err
}
return nil
}
// StopVMAsync stops a virtual machine asynchronously.
func (c *VirtualEnvironmentClient) StopVMAsync(
ctx context.Context,
nodeName string,
vmID int,
) (*string, error) {
resBody := &VirtualEnvironmentVMStopResponseBody{}
err := c.DoRequest(
ctx,
http.MethodPost,
fmt.Sprintf("nodes/%s/qemu/%d/status/stop", url.PathEscape(nodeName), vmID),
nil,
resBody,
)
if err != nil {
return nil, err
}
if resBody.Data == nil {
return nil, errors.New("the server did not include a data object in the response")
}
return resBody.Data, nil
}
// UpdateVM updates a virtual machine.
func (c *VirtualEnvironmentClient) UpdateVM(
ctx context.Context,
nodeName string,
vmID int,
d *VirtualEnvironmentVMUpdateRequestBody,
) error {
return c.DoRequest(
ctx,
http.MethodPut,
fmt.Sprintf("nodes/%s/qemu/%d/config", url.PathEscape(nodeName), vmID),
d,
nil,
)
}
// UpdateVMAsync updates a virtual machine asynchronously.
func (c *VirtualEnvironmentClient) UpdateVMAsync(
ctx context.Context,
nodeName string,
vmID int,
d *VirtualEnvironmentVMUpdateRequestBody,
) (*string, error) {
resBody := &VirtualEnvironmentVMUpdateAsyncResponseBody{}
err := c.DoRequest(
ctx,
http.MethodPost,
fmt.Sprintf("nodes/%s/qemu/%d/config", url.PathEscape(nodeName), vmID),
d,
resBody,
)
if err != nil {
return nil, err
}
if resBody.Data == nil {
return nil, errors.New("the server did not include a data object in the response")
}
return resBody.Data, nil
}
// WaitForNetworkInterfacesFromVMAgent waits for a virtual machine's QEMU agent to publish the network interfaces.
func (c *VirtualEnvironmentClient) WaitForNetworkInterfacesFromVMAgent(
ctx context.Context,
nodeName string,
vmID int,
timeout int,
delay int,
waitForIP bool,
) (*VirtualEnvironmentVMGetQEMUNetworkInterfacesResponseData, error) {
timeDelay := int64(delay)
timeMax := float64(timeout)
timeStart := time.Now()
timeElapsed := timeStart.Sub(timeStart)
for timeElapsed.Seconds() < timeMax {
if int64(timeElapsed.Seconds())%timeDelay == 0 {
data, err := c.GetVMNetworkInterfacesFromAgent(ctx, nodeName, vmID)
if err == nil && data != nil && data.Result != nil {
hasAnyGlobalUnicast := false
if waitForIP {
for _, nic := range *data.Result {
if nic.Name == "lo" {
continue
}
if nic.IPAddresses == nil ||
(nic.IPAddresses != nil && len(*nic.IPAddresses) == 0) {
break
}
for _, addr := range *nic.IPAddresses {
if ip := net.ParseIP(addr.Address); ip != nil && ip.IsGlobalUnicast() {
hasAnyGlobalUnicast = true
}
}
}
}
if hasAnyGlobalUnicast {
return data, err
}
}
time.Sleep(1 * time.Second)
}
time.Sleep(200 * time.Millisecond)
timeElapsed = time.Since(timeStart)
if ctx.Err() != nil {
return nil, ctx.Err()
}
}
return nil, fmt.Errorf(
"timeout while waiting for the QEMU agent on VM \"%d\" to publish the network interfaces",
vmID,
)
}
// WaitForNoNetworkInterfacesFromVMAgent waits for a virtual machine's QEMU agent to unpublish the network interfaces.
func (c *VirtualEnvironmentClient) WaitForNoNetworkInterfacesFromVMAgent(
ctx context.Context,
nodeName string,
vmID int,
timeout int,
delay int,
) error {
timeDelay := int64(delay)
timeMax := float64(timeout)
timeStart := time.Now()
timeElapsed := timeStart.Sub(timeStart)
for timeElapsed.Seconds() < timeMax {
if int64(timeElapsed.Seconds())%timeDelay == 0 {
_, err := c.GetVMNetworkInterfacesFromAgent(ctx, nodeName, vmID)
if err != nil {
return nil
}
time.Sleep(1 * time.Second)
}
time.Sleep(200 * time.Millisecond)
timeElapsed = time.Since(timeStart)
if ctx.Err() != nil {
return ctx.Err()
}
}
return fmt.Errorf(
"timeout while waiting for the QEMU agent on VM \"%d\" to unpublish the network interfaces",
vmID,
)
}
// WaitForVMConfigUnlock waits for a virtual machine configuration to become unlocked.
//
//nolint:dupl
func (c *VirtualEnvironmentClient) WaitForVMConfigUnlock(
ctx context.Context,
nodeName string,
vmID int,
timeout int,
delay int,
ignoreErrorResponse bool,
) error {
timeDelay := int64(delay)
timeMax := float64(timeout)
timeStart := time.Now()
timeElapsed := timeStart.Sub(timeStart)
for timeElapsed.Seconds() < timeMax {
if int64(timeElapsed.Seconds())%timeDelay == 0 {
data, err := c.GetVMStatus(ctx, nodeName, vmID)
if err != nil {
if !ignoreErrorResponse {
return err
}
} else if data.Lock == nil || *data.Lock == "" {
return nil
}
time.Sleep(1 * time.Second)
}
time.Sleep(200 * time.Millisecond)
timeElapsed = time.Since(timeStart)
if ctx.Err() != nil {
return ctx.Err()
}
}
return fmt.Errorf("timeout while waiting for VM \"%d\" configuration to become unlocked", vmID)
}
// WaitForVMState waits for a virtual machine to reach a specific state.
//
//nolint:dupl
func (c *VirtualEnvironmentClient) WaitForVMState(
ctx context.Context,
nodeName string,
vmID int,
state string,
timeout int,
delay int,
) error {
state = strings.ToLower(state)
timeDelay := int64(delay)
timeMax := float64(timeout)
timeStart := time.Now()
timeElapsed := timeStart.Sub(timeStart)
for timeElapsed.Seconds() < timeMax {
if int64(timeElapsed.Seconds())%timeDelay == 0 {
data, err := c.GetVMStatus(ctx, nodeName, vmID)
if err != nil {
return err
}
if data.Status == state {
return nil
}
time.Sleep(1 * time.Second)
}
time.Sleep(200 * time.Millisecond)
timeElapsed = time.Since(timeStart)
if ctx.Err() != nil {
return ctx.Err()
}
}
return fmt.Errorf("timeout while waiting for VM \"%d\" to enter the state \"%s\"", vmID, state)
}

View File

@ -1,32 +0,0 @@
/*
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
*/
package vm
import (
"fmt"
"net/url"
"github.com/bpg/terraform-provider-proxmox/proxmox/firewall"
"github.com/bpg/terraform-provider-proxmox/proxmox/types"
vmfirewall "github.com/bpg/terraform-provider-proxmox/proxmox/vm/firewall"
)
type Client struct {
types.Client
NodeName string
VMID int
}
func (c *Client) ExpandPath(path string) string {
return fmt.Sprintf("nodes/%s/qemu/%d/%s", url.PathEscape(c.NodeName), c.VMID, path)
}
func (c *Client) Firewall() firewall.API {
return &vmfirewall.Client{
Client: firewall.Client{Client: c},
}
}

View File

@ -10,24 +10,40 @@ import (
"errors"
"github.com/bpg/terraform-provider-proxmox/proxmox"
"github.com/bpg/terraform-provider-proxmox/proxmox/api"
"github.com/bpg/terraform-provider-proxmox/proxmox/ssh"
)
// ProviderConfiguration is the configuration for the provider.
type ProviderConfiguration struct {
veClient *proxmox.VirtualEnvironmentClient
apiClient api.Client
sshClient ssh.Client
}
func NewProviderConfiguration(veClient *proxmox.VirtualEnvironmentClient) ProviderConfiguration {
// NewProviderConfiguration creates a new provider configuration.
func NewProviderConfiguration(
apiClient api.Client,
sshClient ssh.Client,
) ProviderConfiguration {
return ProviderConfiguration{
veClient: veClient,
apiClient: apiClient,
sshClient: sshClient,
}
}
func (c *ProviderConfiguration) GetVEClient() (*proxmox.VirtualEnvironmentClient, error) {
if c.veClient == nil {
// GetClient returns the Proxmox API client.
func (c *ProviderConfiguration) GetClient() (proxmox.Client, error) {
if c.apiClient == nil {
return nil, errors.New(
"you must specify the virtual environment details in the provider configuration",
"you must specify the API access details in the provider configuration",
)
}
return c.veClient, nil
if c.sshClient == nil {
return nil, errors.New(
"you must specify the SSH access details in the provider configuration",
)
}
return proxmox.NewClient(c.apiClient, c.sshClient), nil
}

View File

@ -17,6 +17,7 @@ import (
"github.com/bpg/terraform-provider-proxmox/proxmoxtf/datasource/firewall"
)
// FirewallAlias returns a resource that represents a single firewall alias.
func FirewallAlias() *schema.Resource {
return &schema.Resource{
Schema: firewall.AliasSchema(),
@ -24,6 +25,7 @@ func FirewallAlias() *schema.Resource {
}
}
// FirewallAliases returns a resource that represents firewall aliases.
func FirewallAliases() *schema.Resource {
return &schema.Resource{
Schema: firewall.AliasesSchema(),
@ -31,6 +33,7 @@ func FirewallAliases() *schema.Resource {
}
}
// FirewallIPSet returns a resource that represents a single firewall IP set.
func FirewallIPSet() *schema.Resource {
return &schema.Resource{
Schema: firewall.IPSetSchema(),
@ -38,6 +41,7 @@ func FirewallIPSet() *schema.Resource {
}
}
// FirewallIPSets returns a resource that represents firewall IP sets.
func FirewallIPSets() *schema.Resource {
return &schema.Resource{
Schema: firewall.IPSetsSchema(),
@ -45,30 +49,17 @@ func FirewallIPSets() *schema.Resource {
}
}
// func FirewallSecurityGroup() *schema.Resource {
// return &schema.Resource{
// Schema: firewall.SecurityGroupSchema(),
// ReadContext: invokeFirewallAPI(firewall.SecurityGroupRead),
// }
// }
//
// func FirewallSecurityGroups() *schema.Resource {
// return &schema.Resource{
// Schema: firewall.SecurityGroupsSchema(),
// ReadContext: invokeFirewallAPI(firewall.SecurityGroupsRead),
// }
// }
func invokeFirewallAPI(
f func(context.Context, fw.API, *schema.ResourceData) diag.Diagnostics,
) func(context.Context, *schema.ResourceData, interface{}) diag.Diagnostics {
return func(ctx context.Context, d *schema.ResourceData, m interface{}) diag.Diagnostics {
config := m.(proxmoxtf.ProviderConfiguration)
veClient, err := config.GetVEClient()
api, err := config.GetClient()
if err != nil {
return diag.FromErr(err)
}
return f(ctx, veClient.API().Cluster().Firewall(), d)
return f(ctx, api.Cluster().Firewall(), d)
}
}

View File

@ -30,6 +30,7 @@ const (
mkDataSourceVirtualEnvironmentDatastoresTypes = "types"
)
// Datastores returns a resource for the Proxmox data store.
func Datastores() *schema.Resource {
return &schema.Resource{
Schema: map[string]*schema.Schema{
@ -104,13 +105,13 @@ func datastoresRead(ctx context.Context, d *schema.ResourceData, m interface{})
var diags diag.Diagnostics
config := m.(proxmoxtf.ProviderConfiguration)
veClient, err := config.GetVEClient()
api, err := config.GetClient()
if err != nil {
return diag.FromErr(err)
}
nodeName := d.Get(mkDataSourceVirtualEnvironmentDatastoresNodeName).(string)
list, err := veClient.ListDatastores(ctx, nodeName, nil)
list, err := api.Node(nodeName).ListDatastores(ctx, nil)
if err != nil {
return diag.FromErr(err)
}

View File

@ -17,8 +17,8 @@ import (
// TestDatastoresInstantiation tests whether the Datastores instance can be instantiated.
func TestDatastoresInstantiation(t *testing.T) {
t.Parallel()
s := Datastores()
s := Datastores()
if s == nil {
t.Fatalf("Cannot instantiate Datastores")
}
@ -27,6 +27,7 @@ func TestDatastoresInstantiation(t *testing.T) {
// TestDatastoresSchema tests the Datastores schema.
func TestDatastoresSchema(t *testing.T) {
t.Parallel()
s := Datastores()
test.AssertRequiredArguments(t, s, []string{

View File

@ -22,6 +22,7 @@ const (
mkDataSourceVirtualEnvironmentDNSServers = "servers"
)
// DNS returns a resource for DNS settings on a node.
func DNS() *schema.Resource {
return &schema.Resource{
Schema: map[string]*schema.Schema{
@ -50,13 +51,14 @@ func dnsRead(ctx context.Context, d *schema.ResourceData, m interface{}) diag.Di
var diags diag.Diagnostics
config := m.(proxmoxtf.ProviderConfiguration)
veClient, err := config.GetVEClient()
api, err := config.GetClient()
if err != nil {
return diag.FromErr(err)
}
nodeName := d.Get(mkDataSourceVirtualEnvironmentDNSNodeName).(string)
dns, err := veClient.GetDNS(ctx, nodeName)
dns, err := api.Node(nodeName).GetDNS(ctx)
if err != nil {
return diag.FromErr(err)
}

View File

@ -17,8 +17,8 @@ import (
// TestDNSInstantiation tests whether the DNS instance can be instantiated.
func TestDNSInstantiation(t *testing.T) {
t.Parallel()
s := DNS()
s := DNS()
if s == nil {
t.Fatalf("Cannot instantiate DNS")
}
@ -27,6 +27,7 @@ func TestDNSInstantiation(t *testing.T) {
// TestDNSSchema tests the DNS schema.
func TestDNSSchema(t *testing.T) {
t.Parallel()
s := DNS()
test.AssertRequiredArguments(t, s, []string{

View File

@ -23,6 +23,7 @@ const (
mkAliasComment = "comment"
)
// AliasSchema defines the schema for the alias.
func AliasSchema() map[string]*schema.Schema {
return map[string]*schema.Schema{
mkAliasName: {
@ -43,10 +44,12 @@ func AliasSchema() map[string]*schema.Schema {
}
}
// AliasRead reads the alias.
func AliasRead(ctx context.Context, fw firewall.API, d *schema.ResourceData) diag.Diagnostics {
var diags diag.Diagnostics
aliasName := d.Get(mkAliasName).(string)
alias, err := fw.GetAlias(ctx, aliasName)
if err != nil {
return diag.FromErr(err)
@ -62,6 +65,7 @@ func AliasRead(ctx context.Context, fw firewall.API, d *schema.ResourceData) dia
} else {
err = d.Set(mkAliasComment, dvAliasComment)
}
diags = append(diags, diag.FromErr(err)...)
return diags

View File

@ -24,6 +24,7 @@ func TestAliasSchemaInstantiation(t *testing.T) {
// TestAliasSchema tests the AliasSchema.
func TestAliasSchema(t *testing.T) {
t.Parallel()
s := AliasSchema()
structure.AssertRequiredArguments(t, s, []string{

View File

@ -20,6 +20,7 @@ const (
mkAliasesAliasNames = "alias_names"
)
// AliasesSchema defines the schema for the Aliases data source.
func AliasesSchema() map[string]*schema.Schema {
return map[string]*schema.Schema{
mkAliasesAliasNames: {
@ -31,6 +32,7 @@ func AliasesSchema() map[string]*schema.Schema {
}
}
// AliasesRead reads the aliases.
func AliasesRead(ctx context.Context, fw firewall.API, d *schema.ResourceData) diag.Diagnostics {
list, err := fw.ListAliases(ctx)
if err != nil {

View File

@ -24,6 +24,7 @@ func TestAliasesSchemaInstantiation(t *testing.T) {
// TestAliasesSchema tests the AliasesSchema.
func TestAliasesSchema(t *testing.T) {
t.Parallel()
s := AliasesSchema()
structure.AssertComputedAttributes(t, s, []string{

Some files were not shown because too many files have changed in this diff Show More