diff --git a/.golangci.yml b/.golangci.yml index c151ea4c..add5ff0a 100644 --- a/.golangci.yml +++ b/.golangci.yml @@ -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 diff --git a/proxmox/access/acl.go b/proxmox/access/acl.go new file mode 100644 index 00000000..24df34f0 --- /dev/null +++ b/proxmox/access/acl.go @@ -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 +} diff --git a/proxmox/virtual_environment_acl_types.go b/proxmox/access/acl_types.go similarity index 57% rename from proxmox/virtual_environment_acl_types.go rename to proxmox/access/acl_types.go index ec2f5ec9..db7eba15 100644 --- a/proxmox/virtual_environment_acl_types.go +++ b/proxmox/access/acl_types.go @@ -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"` diff --git a/proxmox/access/client.go b/proxmox/access/client.go new file mode 100644 index 00000000..e625e1f5 --- /dev/null +++ b/proxmox/access/client.go @@ -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) +} diff --git a/proxmox/access/groups.go b/proxmox/access/groups.go new file mode 100644 index 00000000..7b91b837 --- /dev/null +++ b/proxmox/access/groups.go @@ -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 +} diff --git a/proxmox/access/groups_types.go b/proxmox/access/groups_types.go new file mode 100644 index 00000000..98d5e87d --- /dev/null +++ b/proxmox/access/groups_types.go @@ -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"` +} diff --git a/proxmox/access/roles.go b/proxmox/access/roles.go new file mode 100644 index 00000000..9bc3266f --- /dev/null +++ b/proxmox/access/roles.go @@ -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 +} diff --git a/proxmox/access/roles_types.go b/proxmox/access/roles_types.go new file mode 100644 index 00000000..36cc6f76 --- /dev/null +++ b/proxmox/access/roles_types.go @@ -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"` +} diff --git a/proxmox/access/users.go b/proxmox/access/users.go new file mode 100644 index 00000000..d7fed0c3 --- /dev/null +++ b/proxmox/access/users.go @@ -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 +} diff --git a/proxmox/virtual_environment_users_types.go b/proxmox/access/users_types.go similarity index 70% rename from proxmox/virtual_environment_users_types.go rename to proxmox/access/users_types.go index b35ed9b1..26a1f380 100644 --- a/proxmox/virtual_environment_users_types.go +++ b/proxmox/access/users_types.go @@ -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"` diff --git a/proxmox/virtual_environment_authentication.go b/proxmox/api/authentication.go similarity index 71% rename from proxmox/virtual_environment_authentication.go rename to proxmox/api/authentication.go index f13ee581..b752ef0f 100644 --- a/proxmox/virtual_environment_authentication.go +++ b/proxmox/api/authentication.go @@ -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) } diff --git a/proxmox/api/authentication_types.go b/proxmox/api/authentication_types.go new file mode 100644 index 00000000..6fae4162 --- /dev/null +++ b/proxmox/api/authentication_types.go @@ -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"` +} diff --git a/proxmox/virtual_environment_client.go b/proxmox/api/client.go similarity index 76% rename from proxmox/virtual_environment_client.go rename to proxmox/api/client.go index e1e9ccc7..e76502f6 100644 --- a/proxmox/virtual_environment_client.go +++ b/proxmox/api/client.go @@ -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 { diff --git a/proxmox/api/client_types.go b/proxmox/api/client_types.go new file mode 100644 index 00000000..e0f6ab55 --- /dev/null +++ b/proxmox/api/client_types.go @@ -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//qemu//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 +} diff --git a/proxmox/client.go b/proxmox/client.go new file mode 100644 index 00000000..fe437144 --- /dev/null +++ b/proxmox/client.go @@ -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 +} diff --git a/proxmox/cluster/client.go b/proxmox/cluster/client.go index fb12902b..7b2278ae 100644 --- a/proxmox/cluster/client.go +++ b/proxmox/cluster/client.go @@ -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}, diff --git a/proxmox/cluster/cluster.go b/proxmox/cluster/cluster.go index fe606dec..b3de0db1 100644 --- a/proxmox/cluster/cluster.go +++ b/proxmox/cluster/cluster.go @@ -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") +} diff --git a/proxmox/cluster/firewall/client.go b/proxmox/cluster/firewall/client.go index 45f5f875..a5b67f2b 100644 --- a/proxmox/cluster/firewall/client.go +++ b/proxmox/cluster/firewall/client.go @@ -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 diff --git a/proxmox/cluster/firewall/options.go b/proxmox/cluster/firewall/options.go index c7a1db7d..63440234 100644 --- a/proxmox/cluster/firewall/options.go +++ b/proxmox/cluster/firewall/options.go @@ -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 diff --git a/proxmox/cluster/firewall/options_types.go b/proxmox/cluster/firewall/options_types.go new file mode 100644 index 00000000..102391dc --- /dev/null +++ b/proxmox/cluster/firewall/options_types.go @@ -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 +} diff --git a/proxmox/cluster/firewall/security_groups.go b/proxmox/cluster/firewall/security_groups.go index 5ecf17eb..438363b4 100644 --- a/proxmox/cluster/firewall/security_groups.go +++ b/proxmox/cluster/firewall/security_groups.go @@ -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 } diff --git a/proxmox/cluster/firewall/security_groups_types.go b/proxmox/cluster/firewall/security_groups_types.go new file mode 100644 index 00000000..a84fd090 --- /dev/null +++ b/proxmox/cluster/firewall/security_groups_types.go @@ -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"` +} diff --git a/proxmox/datastores.go b/proxmox/datastores.go deleted file mode 100644 index 9f46457c..00000000 --- a/proxmox/datastores.go +++ /dev/null @@ -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 - } -} diff --git a/proxmox/firewall/aliases.go b/proxmox/firewall/aliases.go index 0993a3b7..afd14512 100644 --- a/proxmox/firewall/aliases.go +++ b/proxmox/firewall/aliases.go @@ -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 } diff --git a/proxmox/firewall/aliases_types.go b/proxmox/firewall/aliases_types.go new file mode 100644 index 00000000..4a1927d7 --- /dev/null +++ b/proxmox/firewall/aliases_types.go @@ -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"` +} diff --git a/proxmox/firewall/api.go b/proxmox/firewall/client.go similarity index 60% rename from proxmox/firewall/api.go rename to proxmox/firewall/client.go index 1d0fad3d..a7332611 100644 --- a/proxmox/firewall/api.go +++ b/proxmox/firewall/client.go @@ -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 } diff --git a/proxmox/firewall/ipset.go b/proxmox/firewall/ipset.go index 9c818b0a..a79f6a5b 100644 --- a/proxmox/firewall/ipset.go +++ b/proxmox/firewall/ipset.go @@ -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 { diff --git a/proxmox/firewall/ipset_types.go b/proxmox/firewall/ipset_types.go new file mode 100644 index 00000000..425c3a83 --- /dev/null +++ b/proxmox/firewall/ipset_types.go @@ -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 diff --git a/proxmox/firewall/options.go b/proxmox/firewall/options.go index c903dbcf..16a7a19b 100644 --- a/proxmox/firewall/options.go +++ b/proxmox/firewall/options.go @@ -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 diff --git a/proxmox/firewall/options_types.go b/proxmox/firewall/options_types.go new file mode 100644 index 00000000..967231c0 --- /dev/null +++ b/proxmox/firewall/options_types.go @@ -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"` +} diff --git a/proxmox/firewall/rules.go b/proxmox/firewall/rules.go index b3821230..b6bb62d3 100644 --- a/proxmox/firewall/rules.go +++ b/proxmox/firewall/rules.go @@ -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 } diff --git a/proxmox/firewall/rules_types.go b/proxmox/firewall/rules_types.go new file mode 100644 index 00000000..927c9595 --- /dev/null +++ b/proxmox/firewall/rules_types.go @@ -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"` +} diff --git a/proxmox/nodes/certificate.go b/proxmox/nodes/certificate.go new file mode 100644 index 00000000..3e2c8dc0 --- /dev/null +++ b/proxmox/nodes/certificate.go @@ -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 +} diff --git a/proxmox/virtual_environment_certificate_types.go b/proxmox/nodes/certificate_types.go similarity index 60% rename from proxmox/virtual_environment_certificate_types.go rename to proxmox/nodes/certificate_types.go index 360f1e92..aff0484d 100644 --- a/proxmox/virtual_environment_certificate_types.go +++ b/proxmox/nodes/certificate_types.go @@ -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"` diff --git a/proxmox/nodes/client.go b/proxmox/nodes/client.go new file mode 100644 index 00000000..58c88231 --- /dev/null +++ b/proxmox/nodes/client.go @@ -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, + } +} diff --git a/proxmox/container/client.go b/proxmox/nodes/containers/client.go similarity index 53% rename from proxmox/container/client.go rename to proxmox/nodes/containers/client.go index 0c525b57..4f474996 100644 --- a/proxmox/container/client.go +++ b/proxmox/nodes/containers/client.go @@ -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}, diff --git a/proxmox/nodes/containers/container.go b/proxmox/nodes/containers/container.go new file mode 100644 index 00000000..386e6489 --- /dev/null +++ b/proxmox/nodes/containers/container.go @@ -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) +} diff --git a/proxmox/nodes/containers/container_types.go b/proxmox/nodes/containers/container_types.go new file mode 100644 index 00000000..c95eba45 --- /dev/null +++ b/proxmox/nodes/containers/container_types.go @@ -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 +} diff --git a/proxmox/container/firewall/client.go b/proxmox/nodes/containers/firewall/client.go similarity index 81% rename from proxmox/container/firewall/client.go rename to proxmox/nodes/containers/firewall/client.go index 943843e3..24e7e880 100644 --- a/proxmox/container/firewall/client.go +++ b/proxmox/nodes/containers/firewall/client.go @@ -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 } diff --git a/proxmox/nodes/dns.go b/proxmox/nodes/dns.go new file mode 100644 index 00000000..2b5cb754 --- /dev/null +++ b/proxmox/nodes/dns.go @@ -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 +} diff --git a/proxmox/virtual_environment_dns_types.go b/proxmox/nodes/dns_types.go similarity index 50% rename from proxmox/virtual_environment_dns_types.go rename to proxmox/nodes/dns_types.go index 5c9627d0..2b4abb4d 100644 --- a/proxmox/virtual_environment_dns_types.go +++ b/proxmox/nodes/dns_types.go @@ -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"` diff --git a/proxmox/nodes/hosts.go b/proxmox/nodes/hosts.go new file mode 100644 index 00000000..ab738879 --- /dev/null +++ b/proxmox/nodes/hosts.go @@ -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 +} diff --git a/proxmox/nodes/hosts_types.go b/proxmox/nodes/hosts_types.go new file mode 100644 index 00000000..019736a8 --- /dev/null +++ b/proxmox/nodes/hosts_types.go @@ -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"` +} diff --git a/proxmox/nodes/nodes.go b/proxmox/nodes/nodes.go new file mode 100644 index 00000000..ce211bad --- /dev/null +++ b/proxmox/nodes/nodes.go @@ -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 +} diff --git a/proxmox/nodes/nodes_types.go b/proxmox/nodes/nodes_types.go new file mode 100644 index 00000000..2d31bbef --- /dev/null +++ b/proxmox/nodes/nodes_types.go @@ -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 +} diff --git a/proxmox/nodes/storage.go b/proxmox/nodes/storage.go new file mode 100644 index 00000000..23437214 --- /dev/null +++ b/proxmox/nodes/storage.go @@ -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 +} diff --git a/proxmox/datastores_types.go b/proxmox/nodes/storage_types.go similarity index 74% rename from proxmox/datastores_types.go rename to proxmox/nodes/storage_types.go index 1084bc38..c381bf50 100644 --- a/proxmox/datastores_types.go +++ b/proxmox/nodes/storage_types.go @@ -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"` diff --git a/proxmox/nodes/tasks/client.go b/proxmox/nodes/tasks/client.go new file mode 100644 index 00000000..b48105f4 --- /dev/null +++ b/proxmox/nodes/tasks/client.go @@ -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), + ) +} diff --git a/proxmox/nodes/tasks/tasks.go b/proxmox/nodes/tasks/tasks.go new file mode 100644 index 00000000..ce25ca5f --- /dev/null +++ b/proxmox/nodes/tasks/tasks.go @@ -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, + ) +} diff --git a/proxmox/nodes/tasks/tasks_types.go b/proxmox/nodes/tasks/tasks_types.go new file mode 100644 index 00000000..419b76eb --- /dev/null +++ b/proxmox/nodes/tasks/tasks_types.go @@ -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"` +} diff --git a/proxmox/nodes/vms/client.go b/proxmox/nodes/vms/client.go new file mode 100644 index 00000000..ff45ce23 --- /dev/null +++ b/proxmox/nodes/vms/client.go @@ -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}, + } +} diff --git a/proxmox/vm/firewall/client.go b/proxmox/nodes/vms/firewall/client.go similarity index 83% rename from proxmox/vm/firewall/client.go rename to proxmox/nodes/vms/firewall/client.go index 943843e3..442d8007 100644 --- a/proxmox/vm/firewall/client.go +++ b/proxmox/nodes/vms/firewall/client.go @@ -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 } diff --git a/proxmox/nodes/vms/vms.go b/proxmox/nodes/vms/vms.go new file mode 100644 index 00000000..7735f6a6 --- /dev/null +++ b/proxmox/nodes/vms/vms.go @@ -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) +} diff --git a/proxmox/virtual_environment_vm_types.go b/proxmox/nodes/vms/vms_types.go similarity index 88% rename from proxmox/virtual_environment_vm_types.go rename to proxmox/nodes/vms/vms_types.go index 9e393474..6186b26c 100644 --- a/proxmox/virtual_environment_vm_types.go +++ b/proxmox/nodes/vms/vms_types.go @@ -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 == "" { diff --git a/proxmox/virtual_environment_vm_types_test.go b/proxmox/nodes/vms/vms_types_test.go similarity index 99% rename from proxmox/virtual_environment_vm_types_test.go rename to proxmox/nodes/vms/vms_types_test.go index c6a069c7..beda96f4 100644 --- a/proxmox/virtual_environment_vm_types_test.go +++ b/proxmox/nodes/vms/vms_types_test.go @@ -4,7 +4,7 @@ * file, You can obtain one at https://mozilla.org/MPL/2.0/. */ -package proxmox +package vms import ( "testing" diff --git a/proxmox/pools/client.go b/proxmox/pools/client.go new file mode 100644 index 00000000..a98b914d --- /dev/null +++ b/proxmox/pools/client.go @@ -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 +} diff --git a/proxmox/pools/pool.go b/proxmox/pools/pool.go new file mode 100644 index 00000000..71931d35 --- /dev/null +++ b/proxmox/pools/pool.go @@ -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 +} diff --git a/proxmox/pools/pool_types.go b/proxmox/pools/pool_types.go new file mode 100644 index 00000000..625934ef --- /dev/null +++ b/proxmox/pools/pool_types.go @@ -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"` +} diff --git a/proxmox/ssh/client.go b/proxmox/ssh/client.go new file mode 100644 index 00000000..23047796 --- /dev/null +++ b/proxmox/ssh/client.go @@ -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 +} diff --git a/proxmox/ssh/client_types.go b/proxmox/ssh/client_types.go new file mode 100644 index 00000000..a033b101 --- /dev/null +++ b/proxmox/ssh/client_types.go @@ -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 +} diff --git a/proxmox/storage/client.go b/proxmox/storage/client.go new file mode 100644 index 00000000..e66b2585 --- /dev/null +++ b/proxmox/storage/client.go @@ -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 +} diff --git a/proxmox/storage/storage.go b/proxmox/storage/storage.go new file mode 100644 index 00000000..c988c593 --- /dev/null +++ b/proxmox/storage/storage.go @@ -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 +} diff --git a/proxmox/storage/storage_types.go b/proxmox/storage/storage_types.go new file mode 100644 index 00000000..bf223172 --- /dev/null +++ b/proxmox/storage/storage_types.go @@ -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"` +} diff --git a/proxmox/types/client.go b/proxmox/types/client.go deleted file mode 100644 index 8b245d3f..00000000 --- a/proxmox/types/client.go +++ /dev/null @@ -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//qemu//firewall/options". - ExpandPath(path string) string -} diff --git a/proxmox/types/disk_size.go b/proxmox/types/disk_size.go index 6efa5062..0bd54865 100644 --- a/proxmox/types/disk_size.go +++ b/proxmox/types/disk_size.go @@ -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 diff --git a/proxmox/version/client.go b/proxmox/version/client.go new file mode 100644 index 00000000..b35fb237 --- /dev/null +++ b/proxmox/version/client.go @@ -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 +} diff --git a/proxmox/version/version.go b/proxmox/version/version.go new file mode 100644 index 00000000..2be02189 --- /dev/null +++ b/proxmox/version/version.go @@ -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 +} diff --git a/proxmox/version/version_types.go b/proxmox/version/version_types.go new file mode 100644 index 00000000..f908d552 --- /dev/null +++ b/proxmox/version/version_types.go @@ -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"` +} diff --git a/proxmox/virtual_environment_acl.go b/proxmox/virtual_environment_acl.go deleted file mode 100644 index a28ca082..00000000 --- a/proxmox/virtual_environment_acl.go +++ /dev/null @@ -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) -} diff --git a/proxmox/virtual_environment_authentication_types.go b/proxmox/virtual_environment_authentication_types.go deleted file mode 100644 index 4b458498..00000000 --- a/proxmox/virtual_environment_authentication_types.go +++ /dev/null @@ -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"` -} diff --git a/proxmox/virtual_environment_certificate.go b/proxmox/virtual_environment_certificate.go deleted file mode 100644 index db756884..00000000 --- a/proxmox/virtual_environment_certificate.go +++ /dev/null @@ -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, - ) -} diff --git a/proxmox/virtual_environment_client_types.go b/proxmox/virtual_environment_client_types.go deleted file mode 100644 index 8f37b238..00000000 --- a/proxmox/virtual_environment_client_types.go +++ /dev/null @@ -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} -} diff --git a/proxmox/virtual_environment_container.go b/proxmox/virtual_environment_container.go deleted file mode 100644 index a31aa697..00000000 --- a/proxmox/virtual_environment_container.go +++ /dev/null @@ -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) -} diff --git a/proxmox/virtual_environment_container_types.go b/proxmox/virtual_environment_container_types.go deleted file mode 100644 index 36e3a670..00000000 --- a/proxmox/virtual_environment_container_types.go +++ /dev/null @@ -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 -} diff --git a/proxmox/virtual_environment_dns.go b/proxmox/virtual_environment_dns.go deleted file mode 100644 index 0356b7ef..00000000 --- a/proxmox/virtual_environment_dns.go +++ /dev/null @@ -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) -} diff --git a/proxmox/virtual_environment_groups.go b/proxmox/virtual_environment_groups.go deleted file mode 100644 index e98dc0d0..00000000 --- a/proxmox/virtual_environment_groups.go +++ /dev/null @@ -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) -} diff --git a/proxmox/virtual_environment_groups_types.go b/proxmox/virtual_environment_groups_types.go deleted file mode 100644 index daea3e24..00000000 --- a/proxmox/virtual_environment_groups_types.go +++ /dev/null @@ -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"` -} diff --git a/proxmox/virtual_environment_hosts.go b/proxmox/virtual_environment_hosts.go deleted file mode 100644 index a198a0f2..00000000 --- a/proxmox/virtual_environment_hosts.go +++ /dev/null @@ -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) -} diff --git a/proxmox/virtual_environment_hosts_types.go b/proxmox/virtual_environment_hosts_types.go deleted file mode 100644 index 6cc2a8ae..00000000 --- a/proxmox/virtual_environment_hosts_types.go +++ /dev/null @@ -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"` -} diff --git a/proxmox/virtual_environment_nodes.go b/proxmox/virtual_environment_nodes.go deleted file mode 100644 index cacd16d9..00000000 --- a/proxmox/virtual_environment_nodes.go +++ /dev/null @@ -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, - ) -} diff --git a/proxmox/virtual_environment_nodes_types.go b/proxmox/virtual_environment_nodes_types.go deleted file mode 100644 index 552aa872..00000000 --- a/proxmox/virtual_environment_nodes_types.go +++ /dev/null @@ -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 -} diff --git a/proxmox/virtual_environment_pools.go b/proxmox/virtual_environment_pools.go deleted file mode 100644 index e30e9098..00000000 --- a/proxmox/virtual_environment_pools.go +++ /dev/null @@ -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) -} diff --git a/proxmox/virtual_environment_pools_types.go b/proxmox/virtual_environment_pools_types.go deleted file mode 100644 index 11919030..00000000 --- a/proxmox/virtual_environment_pools_types.go +++ /dev/null @@ -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"` -} diff --git a/proxmox/virtual_environment_roles.go b/proxmox/virtual_environment_roles.go deleted file mode 100644 index af68ff8f..00000000 --- a/proxmox/virtual_environment_roles.go +++ /dev/null @@ -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) -} diff --git a/proxmox/virtual_environment_roles_types.go b/proxmox/virtual_environment_roles_types.go deleted file mode 100644 index ab2e9a40..00000000 --- a/proxmox/virtual_environment_roles_types.go +++ /dev/null @@ -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"` -} diff --git a/proxmox/virtual_environment_users.go b/proxmox/virtual_environment_users.go deleted file mode 100644 index f8b6f719..00000000 --- a/proxmox/virtual_environment_users.go +++ /dev/null @@ -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) -} diff --git a/proxmox/virtual_environment_version.go b/proxmox/virtual_environment_version.go deleted file mode 100644 index 05ecec5d..00000000 --- a/proxmox/virtual_environment_version.go +++ /dev/null @@ -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 -} diff --git a/proxmox/virtual_environment_version_types.go b/proxmox/virtual_environment_version_types.go deleted file mode 100644 index c42fe5ec..00000000 --- a/proxmox/virtual_environment_version_types.go +++ /dev/null @@ -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"` -} diff --git a/proxmox/virtual_environment_vm.go b/proxmox/virtual_environment_vm.go deleted file mode 100644 index 59b1ecd5..00000000 --- a/proxmox/virtual_environment_vm.go +++ /dev/null @@ -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) -} diff --git a/proxmox/vm/client.go b/proxmox/vm/client.go deleted file mode 100644 index cb75c412..00000000 --- a/proxmox/vm/client.go +++ /dev/null @@ -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}, - } -} diff --git a/proxmoxtf/config.go b/proxmoxtf/config.go index 109ec70c..6dde876c 100644 --- a/proxmoxtf/config.go +++ b/proxmoxtf/config.go @@ -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 } diff --git a/proxmoxtf/datasource/cluster/firewall.go b/proxmoxtf/datasource/cluster/firewall.go index 9f0d8dc9..ad49d54f 100644 --- a/proxmoxtf/datasource/cluster/firewall.go +++ b/proxmoxtf/datasource/cluster/firewall.go @@ -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) } } diff --git a/proxmoxtf/datasource/datastores.go b/proxmoxtf/datasource/datastores.go index b48dfb2d..1eb0a0ee 100644 --- a/proxmoxtf/datasource/datastores.go +++ b/proxmoxtf/datasource/datastores.go @@ -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) } diff --git a/proxmoxtf/datasource/datastores_test.go b/proxmoxtf/datasource/datastores_test.go index a2fa5656..3ea13fde 100644 --- a/proxmoxtf/datasource/datastores_test.go +++ b/proxmoxtf/datasource/datastores_test.go @@ -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{ diff --git a/proxmoxtf/datasource/dns.go b/proxmoxtf/datasource/dns.go index 05b0b8b7..ec9c4e3c 100644 --- a/proxmoxtf/datasource/dns.go +++ b/proxmoxtf/datasource/dns.go @@ -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) } diff --git a/proxmoxtf/datasource/dns_test.go b/proxmoxtf/datasource/dns_test.go index e923889f..f8fb75e8 100644 --- a/proxmoxtf/datasource/dns_test.go +++ b/proxmoxtf/datasource/dns_test.go @@ -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{ diff --git a/proxmoxtf/datasource/firewall/alias.go b/proxmoxtf/datasource/firewall/alias.go index aa8dca15..abf144df 100644 --- a/proxmoxtf/datasource/firewall/alias.go +++ b/proxmoxtf/datasource/firewall/alias.go @@ -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 diff --git a/proxmoxtf/datasource/firewall/alias_test.go b/proxmoxtf/datasource/firewall/alias_test.go index 0ba643ad..7f2e8a3f 100644 --- a/proxmoxtf/datasource/firewall/alias_test.go +++ b/proxmoxtf/datasource/firewall/alias_test.go @@ -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{ diff --git a/proxmoxtf/datasource/firewall/aliases.go b/proxmoxtf/datasource/firewall/aliases.go index 4f727d4d..6e850ac0 100644 --- a/proxmoxtf/datasource/firewall/aliases.go +++ b/proxmoxtf/datasource/firewall/aliases.go @@ -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 { diff --git a/proxmoxtf/datasource/firewall/aliases_test.go b/proxmoxtf/datasource/firewall/aliases_test.go index 867c9e9d..17640d18 100644 --- a/proxmoxtf/datasource/firewall/aliases_test.go +++ b/proxmoxtf/datasource/firewall/aliases_test.go @@ -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{ diff --git a/proxmoxtf/datasource/firewall/ipset.go b/proxmoxtf/datasource/firewall/ipset.go index 8e0a380d..63b9b82f 100644 --- a/proxmoxtf/datasource/firewall/ipset.go +++ b/proxmoxtf/datasource/firewall/ipset.go @@ -26,6 +26,7 @@ const ( mkIPSetCIDRNoMatch = "nomatch" ) +// IPSetSchema defines the schema for the IPSet. func IPSetSchema() map[string]*schema.Schema { return map[string]*schema.Schema{ mkIPSetName: { @@ -65,6 +66,7 @@ func IPSetSchema() map[string]*schema.Schema { } } +// IPSetRead reads the IPSet. func IPSetRead(ctx context.Context, fw firewall.API, d *schema.ResourceData) diag.Diagnostics { var diags diag.Diagnostics @@ -75,6 +77,7 @@ func IPSetRead(ctx context.Context, fw firewall.API, d *schema.ResourceData) dia if err != nil { return diag.FromErr(err) } + for _, ipSet := range ipSetList { if ipSet.Name == ipSetName { if ipSet.Comment != nil { @@ -82,7 +85,9 @@ func IPSetRead(ctx context.Context, fw firewall.API, d *schema.ResourceData) dia } else { err = d.Set(mkIPSetCIDRComment, dvIPSetCIDRComment) } + diags = append(diags, diag.FromErr(err)...) + break } } diff --git a/proxmoxtf/datasource/firewall/ipset_test.go b/proxmoxtf/datasource/firewall/ipset_test.go index 64451ad5..1dcf25b9 100644 --- a/proxmoxtf/datasource/firewall/ipset_test.go +++ b/proxmoxtf/datasource/firewall/ipset_test.go @@ -24,6 +24,7 @@ func TestIPSetSchemaInstantiation(t *testing.T) { // TestIPSetSchema tests the IPSetSchema. func TestIPSetSchema(t *testing.T) { t.Parallel() + s := IPSetSchema() structure.AssertRequiredArguments(t, s, []string{ diff --git a/proxmoxtf/datasource/firewall/ipsets.go b/proxmoxtf/datasource/firewall/ipsets.go index 7bb2f727..c25e5d6c 100644 --- a/proxmoxtf/datasource/firewall/ipsets.go +++ b/proxmoxtf/datasource/firewall/ipsets.go @@ -20,6 +20,7 @@ const ( mkIPSetsIPSetNames = "ipset_names" ) +// IPSetsSchema defines the schema for the IP sets. func IPSetsSchema() map[string]*schema.Schema { return map[string]*schema.Schema{ mkIPSetsIPSetNames: { @@ -31,6 +32,7 @@ func IPSetsSchema() map[string]*schema.Schema { } } +// IPSetsRead reads the IP sets. func IPSetsRead(ctx context.Context, fw firewall.API, d *schema.ResourceData) diag.Diagnostics { list, err := fw.ListIPSets(ctx) if err != nil { diff --git a/proxmoxtf/datasource/firewall/ipsets_test.go b/proxmoxtf/datasource/firewall/ipsets_test.go index ddc78b3a..03446766 100644 --- a/proxmoxtf/datasource/firewall/ipsets_test.go +++ b/proxmoxtf/datasource/firewall/ipsets_test.go @@ -24,6 +24,7 @@ func TestIPSetsSchemaInstantiation(t *testing.T) { // TestIPSetsSchema tests the IPSetsSchema. func TestIPSetsSchema(t *testing.T) { t.Parallel() + s := IPSetsSchema() structure.AssertComputedAttributes(t, s, []string{ diff --git a/proxmoxtf/datasource/firewall/rule.go b/proxmoxtf/datasource/firewall/rule.go index 285e69cc..de592572 100644 --- a/proxmoxtf/datasource/firewall/rule.go +++ b/proxmoxtf/datasource/firewall/rule.go @@ -26,6 +26,7 @@ const ( mkRuleType = "type" ) +// RuleSchema returns the schema for the firewall rule data source. func RuleSchema() map[string]*schema.Schema { return map[string]*schema.Schema{ mkRulePos: { diff --git a/proxmoxtf/datasource/firewall/rule_test.go b/proxmoxtf/datasource/firewall/rule_test.go index 69d50425..c137d5f7 100644 --- a/proxmoxtf/datasource/firewall/rule_test.go +++ b/proxmoxtf/datasource/firewall/rule_test.go @@ -24,6 +24,7 @@ func TestRuleSchemaInstantiation(t *testing.T) { // TestRuleSchema tests the RuleSchema. func TestRuleSchema(t *testing.T) { t.Parallel() + ruleSchema := RuleSchema() structure.AssertRequiredArguments(t, ruleSchema, []string{ diff --git a/proxmoxtf/datasource/firewall/security_group.go b/proxmoxtf/datasource/firewall/security_group.go index 9335c096..aaecd4c2 100644 --- a/proxmoxtf/datasource/firewall/security_group.go +++ b/proxmoxtf/datasource/firewall/security_group.go @@ -21,6 +21,7 @@ const ( mkRules = "rules" ) +// SecurityGroupSchema defines the schema for the security group data source. func SecurityGroupSchema() map[string]*schema.Schema { return map[string]*schema.Schema{ mkSecurityGroupName: { @@ -42,6 +43,7 @@ func SecurityGroupSchema() map[string]*schema.Schema { } } +// SecurityGroupRead reads the security group. func SecurityGroupRead(ctx context.Context, api firewall.SecurityGroup, d *schema.ResourceData) diag.Diagnostics { var diags diag.Diagnostics @@ -58,6 +60,7 @@ func SecurityGroupRead(ctx context.Context, api firewall.SecurityGroup, d *schem diags = append(diags, diag.FromErr(err)...) err = d.Set(mkSecurityGroupComment, v.Comment) diags = append(diags, diag.FromErr(err)...) + break } } diff --git a/proxmoxtf/datasource/firewall/security_group_test.go b/proxmoxtf/datasource/firewall/security_group_test.go index 6d32b9b8..1066c0a0 100644 --- a/proxmoxtf/datasource/firewall/security_group_test.go +++ b/proxmoxtf/datasource/firewall/security_group_test.go @@ -24,6 +24,7 @@ func TestSecurityGroupSchemaInstantiation(t *testing.T) { // TestSecurityGroupSchema tests the SecurityGroupSchema. func TestSecurityGroupSchema(t *testing.T) { t.Parallel() + s := SecurityGroupSchema() structure.AssertRequiredArguments(t, s, []string{ diff --git a/proxmoxtf/datasource/firewall/security_groups.go b/proxmoxtf/datasource/firewall/security_groups.go index fc89f0ae..ea8beb05 100644 --- a/proxmoxtf/datasource/firewall/security_groups.go +++ b/proxmoxtf/datasource/firewall/security_groups.go @@ -20,6 +20,7 @@ const ( mkSecurityGroupsSecurityGroupNames = "security_group_names" ) +// SecurityGroupsSchema defines the schema for the security groups. func SecurityGroupsSchema() map[string]*schema.Schema { return map[string]*schema.Schema{ mkSecurityGroupsSecurityGroupNames: { @@ -31,6 +32,7 @@ func SecurityGroupsSchema() map[string]*schema.Schema { } } +// SecurityGroupsRead reads the security groups. func SecurityGroupsRead(ctx context.Context, api firewall.SecurityGroup, d *schema.ResourceData) diag.Diagnostics { groups, err := api.ListGroups(ctx) if err != nil { diff --git a/proxmoxtf/datasource/firewall/security_groups_test.go b/proxmoxtf/datasource/firewall/security_groups_test.go index 2ce5735c..d3995905 100644 --- a/proxmoxtf/datasource/firewall/security_groups_test.go +++ b/proxmoxtf/datasource/firewall/security_groups_test.go @@ -24,6 +24,7 @@ func TestSecurityGroupsSchemaInstantiation(t *testing.T) { // TestSecurityGroupsSchema tests the SecurityGroupsSchema. func TestSecurityGroupsSchema(t *testing.T) { t.Parallel() + s := SecurityGroupsSchema() structure.AssertComputedAttributes(t, s, []string{ diff --git a/proxmoxtf/datasource/group.go b/proxmoxtf/datasource/group.go index 0f6cdd8e..9ebd637e 100644 --- a/proxmoxtf/datasource/group.go +++ b/proxmoxtf/datasource/group.go @@ -25,6 +25,7 @@ const ( mkDataSourceVirtualEnvironmentGroupMembers = "members" ) +// Group returns a resource for the Proxmox user group. func Group() *schema.Resource { return &schema.Resource{ Schema: map[string]*schema.Schema{ @@ -77,18 +78,18 @@ func groupRead(ctx context.Context, d *schema.ResourceData, m interface{}) diag. var diags diag.Diagnostics config := m.(proxmoxtf.ProviderConfiguration) - veClient, err := config.GetVEClient() + api, err := config.GetClient() if err != nil { return diag.FromErr(err) } groupID := d.Get(mkDataSourceVirtualEnvironmentGroupID).(string) - group, err := veClient.GetGroup(ctx, groupID) + group, err := api.Access().GetGroup(ctx, groupID) if err != nil { return diag.FromErr(err) } - acl, err := veClient.GetACL(ctx) + acl, err := api.Access().GetACL(ctx) if err != nil { return diag.FromErr(err) } diff --git a/proxmoxtf/datasource/group_test.go b/proxmoxtf/datasource/group_test.go index 7c62770e..66c93c67 100644 --- a/proxmoxtf/datasource/group_test.go +++ b/proxmoxtf/datasource/group_test.go @@ -17,8 +17,8 @@ import ( // TestGroupInstantiation tests whether the Group instance can be instantiated. func TestGroupInstantiation(t *testing.T) { t.Parallel() - s := Group() + s := Group() if s == nil { t.Fatalf("Cannot instantiate Group") } @@ -27,6 +27,7 @@ func TestGroupInstantiation(t *testing.T) { // TestGroupSchema tests the Group schema. func TestGroupSchema(t *testing.T) { t.Parallel() + s := Group() test.AssertRequiredArguments(t, s, []string{ diff --git a/proxmoxtf/datasource/groups.go b/proxmoxtf/datasource/groups.go index e4cdcea8..1b5418d4 100644 --- a/proxmoxtf/datasource/groups.go +++ b/proxmoxtf/datasource/groups.go @@ -20,6 +20,7 @@ const ( mkDataSourceVirtualEnvironmentGroupsGroupIDs = "group_ids" ) +// Groups returns a resource for the Proxmox user groups. func Groups() *schema.Resource { return &schema.Resource{ Schema: map[string]*schema.Schema{ @@ -44,12 +45,12 @@ func groupsRead(ctx context.Context, d *schema.ResourceData, m interface{}) diag var diags diag.Diagnostics config := m.(proxmoxtf.ProviderConfiguration) - veClient, err := config.GetVEClient() + api, err := config.GetClient() if err != nil { return diag.FromErr(err) } - list, err := veClient.ListGroups(ctx) + list, err := api.Access().ListGroups(ctx) if err != nil { return diag.FromErr(err) } diff --git a/proxmoxtf/datasource/groups_test.go b/proxmoxtf/datasource/groups_test.go index 0e5fd9a6..529212a7 100644 --- a/proxmoxtf/datasource/groups_test.go +++ b/proxmoxtf/datasource/groups_test.go @@ -17,8 +17,8 @@ import ( // TestGroupsInstantiation tests whether the Groups instance can be instantiated. func TestGroupsInstantiation(t *testing.T) { t.Parallel() - s := Groups() + s := Groups() if s == nil { t.Fatalf("Cannot instantiate Groups") } @@ -27,6 +27,7 @@ func TestGroupsInstantiation(t *testing.T) { // TestGroupsSchema tests the Groups schema. func TestGroupsSchema(t *testing.T) { t.Parallel() + s := Groups() test.AssertComputedAttributes(t, s, []string{ diff --git a/proxmoxtf/datasource/hosts.go b/proxmoxtf/datasource/hosts.go index d0418edf..16088e80 100644 --- a/proxmoxtf/datasource/hosts.go +++ b/proxmoxtf/datasource/hosts.go @@ -27,6 +27,7 @@ const ( mkDataSourceVirtualEnvironmentHostsNodeName = "node_name" ) +// Hosts returns a resource for the Proxmox hosts. func Hosts() *schema.Resource { return &schema.Resource{ Schema: map[string]*schema.Schema{ @@ -84,13 +85,14 @@ func hostsRead(ctx context.Context, d *schema.ResourceData, m interface{}) diag. 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(mkDataSourceVirtualEnvironmentHostsNodeName).(string) - hosts, err := veClient.GetHosts(ctx, nodeName) + + hosts, err := api.Node(nodeName).GetHosts(ctx) if err != nil { return diag.FromErr(err) } diff --git a/proxmoxtf/datasource/hosts_test.go b/proxmoxtf/datasource/hosts_test.go index 9d9b1b22..f3b09292 100644 --- a/proxmoxtf/datasource/hosts_test.go +++ b/proxmoxtf/datasource/hosts_test.go @@ -17,8 +17,8 @@ import ( // TestHostsInstantiation tests whether the Hosts instance can be instantiated. func TestHostsInstantiation(t *testing.T) { t.Parallel() - s := Hosts() + s := Hosts() if s == nil { t.Fatalf("Cannot instantiate Hosts") } @@ -27,6 +27,7 @@ func TestHostsInstantiation(t *testing.T) { // TestHostsSchema tests the Hosts schema. func TestHostsSchema(t *testing.T) { t.Parallel() + s := Hosts() test.AssertRequiredArguments(t, s, []string{ diff --git a/proxmoxtf/datasource/nodes.go b/proxmoxtf/datasource/nodes.go index 572bd910..3337f5dc 100644 --- a/proxmoxtf/datasource/nodes.go +++ b/proxmoxtf/datasource/nodes.go @@ -28,6 +28,7 @@ const ( mkDataSourceVirtualEnvironmentNodesUptime = "uptime" ) +// Nodes returns a resource for the Proxmox nodes. func Nodes() *schema.Resource { return &schema.Resource{ Schema: map[string]*schema.Schema{ @@ -94,12 +95,12 @@ func nodesRead(ctx context.Context, d *schema.ResourceData, m interface{}) diag. var diags diag.Diagnostics config := m.(proxmoxtf.ProviderConfiguration) - veClient, err := config.GetVEClient() + api, err := config.GetClient() if err != nil { return diag.FromErr(err) } - list, err := veClient.ListNodes(ctx) + list, err := api.Node("").ListNodes(ctx) if err != nil { return diag.FromErr(err) } diff --git a/proxmoxtf/datasource/nodes_test.go b/proxmoxtf/datasource/nodes_test.go index 03482372..17fe3a2d 100644 --- a/proxmoxtf/datasource/nodes_test.go +++ b/proxmoxtf/datasource/nodes_test.go @@ -17,8 +17,8 @@ import ( // TestNodesInstantiation tests whether the Nodes instance can be instantiated. func TestNodesInstantiation(t *testing.T) { t.Parallel() - s := Nodes() + s := Nodes() if s == nil { t.Fatalf("Cannot instantiate Nodes") } @@ -27,6 +27,7 @@ func TestNodesInstantiation(t *testing.T) { // TestNodesSchema tests the Nodes schema. func TestNodesSchema(t *testing.T) { t.Parallel() + s := Nodes() test.AssertComputedAttributes(t, s, []string{ diff --git a/proxmoxtf/datasource/pool.go b/proxmoxtf/datasource/pool.go index d9cc8a91..b9697a74 100644 --- a/proxmoxtf/datasource/pool.go +++ b/proxmoxtf/datasource/pool.go @@ -26,6 +26,7 @@ const ( mkDataSourceVirtualEnvironmentPoolPoolID = "pool_id" ) +// Pool returns a resource for a single Proxmox pool. func Pool() *schema.Resource { return &schema.Resource{ Schema: map[string]*schema.Schema{ @@ -82,13 +83,13 @@ func poolRead(ctx context.Context, d *schema.ResourceData, m interface{}) diag.D var diags diag.Diagnostics config := m.(proxmoxtf.ProviderConfiguration) - veClient, err := config.GetVEClient() + api, err := config.GetClient() if err != nil { return diag.FromErr(err) } poolID := d.Get(mkDataSourceVirtualEnvironmentPoolPoolID).(string) - pool, err := veClient.GetPool(ctx, poolID) + pool, err := api.Pool().GetPool(ctx, poolID) if err != nil { return diag.FromErr(err) } diff --git a/proxmoxtf/datasource/pool_test.go b/proxmoxtf/datasource/pool_test.go index 0f95c41c..a4812b9b 100644 --- a/proxmoxtf/datasource/pool_test.go +++ b/proxmoxtf/datasource/pool_test.go @@ -17,8 +17,8 @@ import ( // TestPoolInstantiation tests whether the Pool instance can be instantiated. func TestPoolInstantiation(t *testing.T) { t.Parallel() - s := Pool() + s := Pool() if s == nil { t.Fatalf("Cannot instantiate Pool") } @@ -27,6 +27,7 @@ func TestPoolInstantiation(t *testing.T) { // TestPoolSchema tests the Pool schema. func TestPoolSchema(t *testing.T) { t.Parallel() + s := Pool() test.AssertRequiredArguments(t, s, []string{ diff --git a/proxmoxtf/datasource/pools.go b/proxmoxtf/datasource/pools.go index af8f5ebb..834675d0 100644 --- a/proxmoxtf/datasource/pools.go +++ b/proxmoxtf/datasource/pools.go @@ -19,6 +19,7 @@ const ( mkDataSourceVirtualEnvironmentPoolsPoolIDs = "pool_ids" ) +// Pools returns a resource for the Proxmox pools. func Pools() *schema.Resource { return &schema.Resource{ Schema: map[string]*schema.Schema{ @@ -35,12 +36,12 @@ func Pools() *schema.Resource { func poolsRead(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) } - list, err := veClient.ListPools(ctx) + list, err := api.Pool().ListPools(ctx) if err != nil { return diag.FromErr(err) } diff --git a/proxmoxtf/datasource/pools_test.go b/proxmoxtf/datasource/pools_test.go index e06dfd67..70721077 100644 --- a/proxmoxtf/datasource/pools_test.go +++ b/proxmoxtf/datasource/pools_test.go @@ -17,8 +17,8 @@ import ( // TestPoolsInstantiation tests whether the Pools instance can be instantiated. func TestPoolsInstantiation(t *testing.T) { t.Parallel() - s := Pools() + s := Pools() if s == nil { t.Fatalf("Cannot instantiate Pools") } @@ -27,6 +27,7 @@ func TestPoolsInstantiation(t *testing.T) { // TestPoolsSchema tests the Pools schema. func TestPoolsSchema(t *testing.T) { t.Parallel() + s := Pools() test.AssertComputedAttributes(t, s, []string{ diff --git a/proxmoxtf/datasource/role.go b/proxmoxtf/datasource/role.go index bb31048e..b84a2592 100644 --- a/proxmoxtf/datasource/role.go +++ b/proxmoxtf/datasource/role.go @@ -20,6 +20,7 @@ const ( mkDataSourceVirtualEnvironmentRolePrivileges = "privileges" ) +// Role returns a resource for a single user role. func Role() *schema.Resource { return &schema.Resource{ Schema: map[string]*schema.Schema{ @@ -41,13 +42,13 @@ func Role() *schema.Resource { func roleRead(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) } roleID := d.Get(mkDataSourceVirtualEnvironmentRoleID).(string) - accessRole, err := veClient.GetRole(ctx, roleID) + accessRole, err := api.Access().GetRole(ctx, roleID) if err != nil { return diag.FromErr(err) } diff --git a/proxmoxtf/datasource/role_test.go b/proxmoxtf/datasource/role_test.go index ee61af59..221f83f0 100644 --- a/proxmoxtf/datasource/role_test.go +++ b/proxmoxtf/datasource/role_test.go @@ -17,8 +17,8 @@ import ( // TestRoleInstantiation tests whether the Role instance can be instantiated. func TestRoleInstantiation(t *testing.T) { t.Parallel() - s := Role() + s := Role() if s == nil { t.Fatalf("Cannot instantiate Role") } @@ -27,6 +27,7 @@ func TestRoleInstantiation(t *testing.T) { // TestRoleSchema tests the Role schema. func TestRoleSchema(t *testing.T) { t.Parallel() + s := Role() test.AssertRequiredArguments(t, s, []string{ diff --git a/proxmoxtf/datasource/roles.go b/proxmoxtf/datasource/roles.go index 95d48fa3..78b1f0e2 100644 --- a/proxmoxtf/datasource/roles.go +++ b/proxmoxtf/datasource/roles.go @@ -21,6 +21,7 @@ const ( mkDataSourceVirtualEnvironmentRolesSpecial = "special" ) +// Roles returns a resource for the user roles. func Roles() *schema.Resource { return &schema.Resource{ Schema: map[string]*schema.Schema{ @@ -54,12 +55,12 @@ func rolesRead(ctx context.Context, d *schema.ResourceData, m interface{}) diag. var diags diag.Diagnostics config := m.(proxmoxtf.ProviderConfiguration) - veClient, err := config.GetVEClient() + api, err := config.GetClient() if err != nil { return diag.FromErr(err) } - list, err := veClient.ListRoles(ctx) + list, err := api.Access().ListRoles(ctx) if err != nil { return diag.FromErr(err) } diff --git a/proxmoxtf/datasource/roles_test.go b/proxmoxtf/datasource/roles_test.go index b08b483b..272a2269 100644 --- a/proxmoxtf/datasource/roles_test.go +++ b/proxmoxtf/datasource/roles_test.go @@ -17,8 +17,8 @@ import ( // TestRolesInstantiation tests whether the Roles instance can be instantiated. func TestRolesInstantiation(t *testing.T) { t.Parallel() - s := Roles() + s := Roles() if s == nil { t.Fatalf("Cannot instantiate Roles") } @@ -27,6 +27,7 @@ func TestRolesInstantiation(t *testing.T) { // TestRolesSchema tests the Roles schema. func TestRolesSchema(t *testing.T) { t.Parallel() + s := Roles() test.AssertComputedAttributes(t, s, []string{ diff --git a/proxmoxtf/datasource/time.go b/proxmoxtf/datasource/time.go index e9249d58..3a6918dd 100644 --- a/proxmoxtf/datasource/time.go +++ b/proxmoxtf/datasource/time.go @@ -24,6 +24,7 @@ const ( mkDataSourceVirtualEnvironmentTimeUTCTime = "utc_time" ) +// Time returns a resource for the Proxmox time. func Time() *schema.Resource { return &schema.Resource{ Schema: map[string]*schema.Schema{ @@ -56,13 +57,13 @@ func timeRead(ctx context.Context, d *schema.ResourceData, m interface{}) diag.D 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(mkDataSourceVirtualEnvironmentTimeNodeName).(string) - nodeTime, err := veClient.GetNodeTime(ctx, nodeName) + nodeTime, err := api.Node(nodeName).GetTime(ctx) if err != nil { return diag.FromErr(err) } diff --git a/proxmoxtf/datasource/time_test.go b/proxmoxtf/datasource/time_test.go index 7de3296d..f3cbbcf0 100644 --- a/proxmoxtf/datasource/time_test.go +++ b/proxmoxtf/datasource/time_test.go @@ -17,8 +17,8 @@ import ( // TestTimeInstantiation tests whether the Roles instance can be instantiated. func TestTimeInstantiation(t *testing.T) { t.Parallel() - s := Time() + s := Time() if s == nil { t.Fatalf("Cannot instantiate Time") } @@ -27,6 +27,7 @@ func TestTimeInstantiation(t *testing.T) { // TestTimeSchema tests the Time schema. func TestTimeSchema(t *testing.T) { t.Parallel() + s := Time() test.AssertRequiredArguments(t, s, []string{ diff --git a/proxmoxtf/datasource/user.go b/proxmoxtf/datasource/user.go index 7373433d..08c5bf38 100644 --- a/proxmoxtf/datasource/user.go +++ b/proxmoxtf/datasource/user.go @@ -32,6 +32,7 @@ const ( mkDataSourceVirtualEnvironmentUserUserID = "user_id" ) +// User returns a resource for a single Proxmox user. func User() *schema.Resource { return &schema.Resource{ Schema: map[string]*schema.Schema{ @@ -114,18 +115,18 @@ func userRead(ctx context.Context, d *schema.ResourceData, m interface{}) diag.D var diags diag.Diagnostics config := m.(proxmoxtf.ProviderConfiguration) - veClient, err := config.GetVEClient() + api, err := config.GetClient() if err != nil { return diag.FromErr(err) } userID := d.Get(mkDataSourceVirtualEnvironmentUserUserID).(string) - v, err := veClient.GetUser(ctx, userID) + v, err := api.Access().GetUser(ctx, userID) if err != nil { return diag.FromErr(err) } - acl, err := veClient.GetACL(ctx) + acl, err := api.Access().GetACL(ctx) if err != nil { return diag.FromErr(err) } diff --git a/proxmoxtf/datasource/user_test.go b/proxmoxtf/datasource/user_test.go index 4131e0af..c7c3ab41 100644 --- a/proxmoxtf/datasource/user_test.go +++ b/proxmoxtf/datasource/user_test.go @@ -17,8 +17,8 @@ import ( // TestUserInstantiation tests whether the User instance can be instantiated. func TestUserInstantiation(t *testing.T) { t.Parallel() - s := User() + s := User() if s == nil { t.Fatalf("Cannot instantiate User") } @@ -27,6 +27,7 @@ func TestUserInstantiation(t *testing.T) { // TestUserSchema tests the User schema. func TestUserSchema(t *testing.T) { t.Parallel() + s := User() test.AssertRequiredArguments(t, s, []string{ diff --git a/proxmoxtf/datasource/users.go b/proxmoxtf/datasource/users.go index 6e178c2e..4f799fd8 100644 --- a/proxmoxtf/datasource/users.go +++ b/proxmoxtf/datasource/users.go @@ -28,6 +28,7 @@ const ( mkDataSourceVirtualEnvironmentUsersUserIDs = "user_ids" ) +// Users returns a resource for the Proxmox users. func Users() *schema.Resource { return &schema.Resource{ Schema: map[string]*schema.Schema{ @@ -97,12 +98,12 @@ func usersRead(ctx context.Context, d *schema.ResourceData, m interface{}) diag. var diags diag.Diagnostics config := m.(proxmoxtf.ProviderConfiguration) - veClient, err := config.GetVEClient() + api, err := config.GetClient() if err != nil { return diag.FromErr(err) } - list, err := veClient.ListUsers(ctx) + list, err := api.Access().ListUsers(ctx) if err != nil { return diag.FromErr(err) } diff --git a/proxmoxtf/datasource/users_test.go b/proxmoxtf/datasource/users_test.go index 503fb488..f8d91dbb 100644 --- a/proxmoxtf/datasource/users_test.go +++ b/proxmoxtf/datasource/users_test.go @@ -17,8 +17,8 @@ import ( // TestUsersInstantiation tests whether the Users instance can be instantiated. func TestUsersInstantiation(t *testing.T) { t.Parallel() - s := Users() + s := Users() if s == nil { t.Fatalf("Cannot instantiate Users") } @@ -27,6 +27,7 @@ func TestUsersInstantiation(t *testing.T) { // TestUsersSchema tests the Users schema. func TestUsersSchema(t *testing.T) { t.Parallel() + s := Users() test.AssertComputedAttributes(t, s, []string{ diff --git a/proxmoxtf/datasource/version.go b/proxmoxtf/datasource/version.go index b9cd9f7c..ef5ebe73 100644 --- a/proxmoxtf/datasource/version.go +++ b/proxmoxtf/datasource/version.go @@ -22,6 +22,7 @@ const ( mkDataSourceVirtualEnvironmentVersionVersion = "version" ) +// Version returns a resource for the Proxmox version. func Version() *schema.Resource { return &schema.Resource{ Schema: map[string]*schema.Schema{ @@ -58,12 +59,12 @@ func versionRead(ctx context.Context, d *schema.ResourceData, m interface{}) dia var diags diag.Diagnostics config := m.(proxmoxtf.ProviderConfiguration) - veClient, err := config.GetVEClient() + api, err := config.GetClient() if err != nil { return diag.FromErr(err) } - version, err := veClient.Version(ctx) + version, err := api.Version().Version(ctx) if err != nil { return diag.FromErr(err) } diff --git a/proxmoxtf/datasource/version_test.go b/proxmoxtf/datasource/version_test.go index f088b103..5bd21582 100644 --- a/proxmoxtf/datasource/version_test.go +++ b/proxmoxtf/datasource/version_test.go @@ -17,8 +17,8 @@ import ( // TestVersionInstantiation tests whether the Version instance can be instantiated. func TestVersionInstantiation(t *testing.T) { t.Parallel() - s := Version() + s := Version() if s == nil { t.Fatalf("Cannot instantiate Version") } @@ -27,6 +27,7 @@ func TestVersionInstantiation(t *testing.T) { // TestVersionSchema tests the Version schema. func TestVersionSchema(t *testing.T) { t.Parallel() + s := Version() test.AssertComputedAttributes(t, s, []string{ diff --git a/proxmoxtf/datasource/vm.go b/proxmoxtf/datasource/vm.go index aff7ca34..1292a4c4 100644 --- a/proxmoxtf/datasource/vm.go +++ b/proxmoxtf/datasource/vm.go @@ -25,6 +25,7 @@ const ( mkDataSourceVirtualEnvironmentVMVMID = "vm_id" ) +// VM returns a resource for a single Proxmox VM. func VM() *schema.Resource { return &schema.Resource{ Schema: map[string]*schema.Schema{ @@ -59,7 +60,8 @@ func vmRead(ctx context.Context, d *schema.ResourceData, m interface{}) diag.Dia var diags diag.Diagnostics config := m.(proxmoxtf.ProviderConfiguration) - veClient, err := config.GetVEClient() + + api, err := config.GetClient() if err != nil { return diag.FromErr(err) } @@ -67,7 +69,7 @@ func vmRead(ctx context.Context, d *schema.ResourceData, m interface{}) diag.Dia nodeName := d.Get(mkDataSourceVirtualEnvironmentVMNodeName).(string) vmID := d.Get(mkDataSourceVirtualEnvironmentVMVMID).(int) - vmStatus, err := veClient.GetVMStatus(ctx, nodeName, vmID) + vmStatus, err := api.Node(nodeName).VM(vmID).GetVMStatus(ctx) if err != nil { if strings.Contains(err.Error(), "HTTP 404") || (strings.Contains(err.Error(), "HTTP 500") && strings.Contains(err.Error(), "does not exist")) { @@ -84,9 +86,11 @@ func vmRead(ctx context.Context, d *schema.ResourceData, m interface{}) diag.Dia } else { err = d.Set(mkDataSourceVirtualEnvironmentVMName, "") } + diags = append(diags, diag.FromErr(err)...) var tags []string + if vmStatus.Tags != nil { for _, tag := range strings.Split(*vmStatus.Tags, ";") { t := strings.TrimSpace(tag) @@ -94,8 +98,10 @@ func vmRead(ctx context.Context, d *schema.ResourceData, m interface{}) diag.Dia tags = append(tags, t) } } + sort.Strings(tags) } + err = d.Set(mkDataSourceVirtualEnvironmentVMTags, tags) diags = append(diags, diag.FromErr(err)...) diff --git a/proxmoxtf/datasource/vms.go b/proxmoxtf/datasource/vms.go index 400d2569..5bbcd204 100644 --- a/proxmoxtf/datasource/vms.go +++ b/proxmoxtf/datasource/vms.go @@ -25,6 +25,7 @@ const ( mkDataSourceVirtualEnvironmentVMs = "vms" ) +// VMs returns a resource for the Proxmox VMs. func VMs() *schema.Resource { return &schema.Resource{ Schema: map[string]*schema.Schema{ @@ -57,18 +58,20 @@ func vmsRead(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) } - nodeNames, err := getNodeNames(ctx, d, veClient) + nodeNames, err := getNodeNames(ctx, d, api) if err != nil { return diag.FromErr(err) } + var filterTags []string + tagsData := d.Get(mkDataSourceVirtualEnvironmentVMTags).([]interface{}) - var filterTags []string for i := 0; i < len(tagsData); i++ { tag := strings.TrimSpace(tagsData[i].(string)) if len(tag) > 0 { @@ -78,10 +81,11 @@ func vmsRead(ctx context.Context, d *schema.ResourceData, m interface{}) diag.Di sort.Strings(filterTags) var vms []interface{} + for _, nodeName := range nodeNames { - listData, err := veClient.ListVMs(ctx, nodeName) - if err != nil { - diags = append(diags, diag.FromErr(err)...) + listData, e := api.Node(nodeName).VM(0).ListVMs(ctx) + if e != nil { + diags = append(diags, diag.FromErr(e)...) } sort.Slice(listData, func(i, j int) bool { @@ -109,12 +113,14 @@ func vmsRead(ctx context.Context, d *schema.ResourceData, m interface{}) diag.Di if len(filterTags) > 0 { match := true + for _, tag := range filterTags { if !slices.Contains(tags, tag) { match = false break } } + if !match { continue } @@ -132,17 +138,14 @@ func vmsRead(ctx context.Context, d *schema.ResourceData, m interface{}) diag.Di return diags } -func getNodeNames( - ctx context.Context, - d *schema.ResourceData, - veClient *proxmox.VirtualEnvironmentClient, -) ([]string, error) { +func getNodeNames(ctx context.Context, d *schema.ResourceData, api proxmox.Client) ([]string, error) { var nodeNames []string + nodeName := d.Get(mkDataSourceVirtualEnvironmentVMNodeName).(string) if nodeName != "" { nodeNames = append(nodeNames, nodeName) } else { - nodes, err := veClient.ListNodes(ctx) + nodes, err := api.Node(nodeName).ListNodes(ctx) if err != nil { return nil, fmt.Errorf("error listing nodes: %w", err) } @@ -153,5 +156,6 @@ func getNodeNames( } sort.Strings(nodeNames) + return nodeNames, nil } diff --git a/proxmoxtf/provider/provider.go b/proxmoxtf/provider/provider.go index 60a667d0..1d8b03ca 100644 --- a/proxmoxtf/provider/provider.go +++ b/proxmoxtf/provider/provider.go @@ -8,11 +8,13 @@ package provider import ( "context" + "strings" "github.com/hashicorp/terraform-plugin-sdk/v2/diag" "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" - "github.com/bpg/terraform-provider-proxmox/proxmox" + "github.com/bpg/terraform-provider-proxmox/proxmox/api" + "github.com/bpg/terraform-provider-proxmox/proxmox/ssh" "github.com/bpg/terraform-provider-proxmox/proxmoxtf" ) @@ -43,56 +45,79 @@ func ProxmoxVirtualEnvironment() *schema.Provider { func providerConfigure(_ context.Context, d *schema.ResourceData) (interface{}, diag.Diagnostics) { var err error - var veClient *proxmox.VirtualEnvironmentClient + + var apiClient api.Client + + var sshClient ssh.Client + + var username, password string // Legacy configuration, wrapped in the deprecated `virtual_environment` block veConfigBlock := d.Get(mkProviderVirtualEnvironment).([]interface{}) if len(veConfigBlock) > 0 { veConfig := veConfigBlock[0].(map[string]interface{}) - veSSHConfig := veConfig[mkProviderSSH].(map[string]interface{}) - veClient, err = proxmox.NewVirtualEnvironmentClient( + username = veConfig[mkProviderUsername].(string) + password = veConfig[mkProviderPassword].(string) + + apiClient, err = api.NewClient( veConfig[mkProviderEndpoint].(string), - veConfig[mkProviderUsername].(string), - veConfig[mkProviderSSH].(map[string]interface{})[mkProviderSSHUsername].(string), - veConfig[mkProviderPassword].(string), + username, + password, + veConfig[mkProviderOTP].(string), veConfig[mkProviderInsecure].(bool), - veSSHConfig[mkProviderSSHUsername].(string), - veSSHConfig[mkProviderSSHPassword].(string), - veSSHConfig[mkProviderSSHAgent].(bool), - veSSHConfig[mkProviderSSHAgentSocket].(string), ) } else { - sshconf := map[string]interface{}{ - mkProviderSSHUsername: "", - mkProviderSSHPassword: "", - mkProviderSSHAgent: false, - mkProviderSSHAgentSocket: "", - } + username = d.Get(mkProviderUsername).(string) + password = d.Get(mkProviderPassword).(string) - sshBlock, sshSet := d.GetOk(mkProviderSSH) - if sshSet { - sshconf = sshBlock.(*schema.Set).List()[0].(map[string]interface{}) - } - - veClient, err = proxmox.NewVirtualEnvironmentClient( + apiClient, err = api.NewClient( d.Get(mkProviderEndpoint).(string), - d.Get(mkProviderUsername).(string), - d.Get(mkProviderPassword).(string), + username, + password, d.Get(mkProviderOTP).(string), d.Get(mkProviderInsecure).(bool), - sshconf[mkProviderSSHUsername].(string), - sshconf[mkProviderSSHPassword].(string), - sshconf[mkProviderSSHAgent].(bool), - sshconf[mkProviderSSHAgentSocket].(string), ) } if err != nil { - return nil, diag.FromErr(err) + return nil, diag.Errorf("error creating virtual environment client: %s", err) } - config := proxmoxtf.NewProviderConfiguration(veClient) + sshConf := map[string]interface{}{} + + sshBlock := d.Get(mkProviderSSH).([]interface{}) + if len(sshBlock) > 0 { + sshConf = sshBlock[0].(map[string]interface{}) + } + + if v, ok := sshConf[mkProviderSSHUsername]; !ok || v.(string) == "" { + sshConf[mkProviderSSHUsername] = strings.Split(username, "@")[0] + } + + if v, ok := sshConf[mkProviderSSHPassword]; !ok || v.(string) == "" { + sshConf[mkProviderSSHPassword] = password + } + + if _, ok := sshConf[mkProviderSSHAgent]; !ok { + sshConf[mkProviderSSHAgent] = false + } + + if _, ok := sshConf[mkProviderSSHAgentSocket]; !ok { + sshConf[mkProviderSSHAgentSocket] = "" + } + + sshClient, err = ssh.NewClient( + sshConf[mkProviderSSHUsername].(string), + sshConf[mkProviderSSHPassword].(string), + sshConf[mkProviderSSHAgent].(bool), + sshConf[mkProviderSSHAgentSocket].(string), + ) + if err != nil { + return nil, diag.Errorf("error creating SSH client: %s", err) + } + + config := proxmoxtf.NewProviderConfiguration(apiClient, sshClient) return config, nil } diff --git a/proxmoxtf/provider/provider_test.go b/proxmoxtf/provider/provider_test.go index ea98576e..eb04f6ec 100644 --- a/proxmoxtf/provider/provider_test.go +++ b/proxmoxtf/provider/provider_test.go @@ -17,8 +17,8 @@ import ( // TestProviderInstantiation() tests whether the ProxmoxVirtualEnvironment instance can be instantiated. func TestProviderInstantiation(t *testing.T) { t.Parallel() - s := ProxmoxVirtualEnvironment() + s := ProxmoxVirtualEnvironment() if s == nil { t.Fatalf("Cannot instantiate ProxmoxVirtualEnvironment") } @@ -27,6 +27,7 @@ func TestProviderInstantiation(t *testing.T) { // TestProviderSchema() tests the ProxmoxVirtualEnvironment schema. func TestProviderSchema(t *testing.T) { t.Parallel() + s := &schema.Resource{ Schema: ProxmoxVirtualEnvironment().Schema, } diff --git a/proxmoxtf/provider/schema.go b/proxmoxtf/provider/schema.go index 551e3ec0..fcdf11eb 100644 --- a/proxmoxtf/provider/schema.go +++ b/proxmoxtf/provider/schema.go @@ -99,7 +99,7 @@ func nestedProviderSchema() map[string]*schema.Schema { ValidateFunc: validation.StringIsNotEmpty, }, mkProviderSSH: { - Type: schema.TypeSet, + Type: schema.TypeList, Optional: true, MaxItems: 1, Description: "The SSH connection configuration to a Proxmox node", diff --git a/proxmoxtf/resource/certificate.go b/proxmoxtf/resource/certificate.go index b968db06..927d45d0 100644 --- a/proxmoxtf/resource/certificate.go +++ b/proxmoxtf/resource/certificate.go @@ -15,7 +15,7 @@ import ( "github.com/hashicorp/terraform-plugin-sdk/v2/diag" "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" - "github.com/bpg/terraform-provider-proxmox/proxmox" + "github.com/bpg/terraform-provider-proxmox/proxmox/nodes" "github.com/bpg/terraform-provider-proxmox/proxmox/types" "github.com/bpg/terraform-provider-proxmox/proxmoxtf" ) @@ -40,6 +40,7 @@ const ( mkResourceVirtualEnvironmentCertificateSubjectAlternativeNames = "subject_alternative_names" ) +// Certificate returns a resource that manages a certificate. func Certificate() *schema.Resource { return &schema.Resource{ Schema: map[string]*schema.Schema{ @@ -139,7 +140,7 @@ func certificateCreate(ctx context.Context, d *schema.ResourceData, m interface{ return nil } -func certificateGetUpdateBody(d *schema.ResourceData) *proxmox.VirtualEnvironmentCertificateUpdateRequestBody { +func certificateGetUpdateBody(d *schema.ResourceData) *nodes.CertificateUpdateRequestBody { certificate := d.Get(mkResourceVirtualEnvironmentCertificateCertificate).(string) certificateChain := d.Get(mkResourceVirtualEnvironmentCertificateCertificateChain).(string) overwrite := types.CustomBool(d.Get(mkResourceVirtualEnvironmentCertificateOverwrite).(bool)) @@ -159,7 +160,7 @@ func certificateGetUpdateBody(d *schema.ResourceData) *proxmox.VirtualEnvironmen restart := types.CustomBool(true) - body := &proxmox.VirtualEnvironmentCertificateUpdateRequestBody{ + body := &nodes.CertificateUpdateRequestBody{ Certificates: combinedCertificates, Force: &force, PrivateKey: &privateKey, @@ -173,13 +174,14 @@ func certificateRead(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(mkResourceVirtualEnvironmentCertificateNodeName).(string) - list, err := veClient.ListCertificates(ctx, nodeName) + + list, err := api.Node(nodeName).ListCertificates(ctx) if err != nil { return diag.FromErr(err) } @@ -301,7 +303,7 @@ func certificateRead(ctx context.Context, d *schema.ResourceData, m interface{}) func certificateUpdate(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) } @@ -310,7 +312,7 @@ func certificateUpdate(ctx context.Context, d *schema.ResourceData, m interface{ body := certificateGetUpdateBody(d) - err = veClient.UpdateCertificate(ctx, nodeName, body) + err = api.Node(nodeName).UpdateCertificate(ctx, body) if err != nil { return diag.FromErr(err) } @@ -320,18 +322,18 @@ func certificateUpdate(ctx context.Context, d *schema.ResourceData, m interface{ func certificateDelete(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) } nodeName := d.Get(mkResourceVirtualEnvironmentCertificateNodeName).(string) + restart := types.CustomBool(true) - err = veClient.DeleteCertificate( + err = api.Node(nodeName).DeleteCertificate( ctx, - nodeName, - &proxmox.VirtualEnvironmentCertificateDeleteRequestBody{ + &nodes.CertificateDeleteRequestBody{ Restart: &restart, }, ) diff --git a/proxmoxtf/resource/certificate_test.go b/proxmoxtf/resource/certificate_test.go index 6595e929..f4fe32ce 100644 --- a/proxmoxtf/resource/certificate_test.go +++ b/proxmoxtf/resource/certificate_test.go @@ -17,8 +17,8 @@ import ( // TestCertificateInstantiation tests whether the Certificate instance can be instantiated. func TestCertificateInstantiation(t *testing.T) { t.Parallel() - s := Certificate() + s := Certificate() if s == nil { t.Fatalf("Cannot instantiate Certificate") } @@ -27,6 +27,7 @@ func TestCertificateInstantiation(t *testing.T) { // TestCertificateSchema tests the Certificate schema. func TestCertificateSchema(t *testing.T) { t.Parallel() + s := Certificate() test.AssertRequiredArguments(t, s, []string{ diff --git a/proxmoxtf/resource/cluster/firewall/firewall.go b/proxmoxtf/resource/cluster/firewall/firewall.go index 506a1b96..06fa1d09 100644 --- a/proxmoxtf/resource/cluster/firewall/firewall.go +++ b/proxmoxtf/resource/cluster/firewall/firewall.go @@ -35,6 +35,7 @@ const ( mkPolicyOut = "output_policy" ) +// Firewall returns a resource to manage firewall options. func Firewall() *schema.Resource { return &schema.Resource{ Schema: map[string]*schema.Schema{ @@ -217,13 +218,12 @@ func selectFirewallAPI( ) 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) } - api := veClient.API().Cluster().Firewall() - - return f(ctx, api, d) + return f(ctx, api.Cluster().Firewall(), d) } } diff --git a/proxmoxtf/resource/cluster/firewall/security_group.go b/proxmoxtf/resource/cluster/firewall/security_group.go index 07ab12a9..200b10d3 100644 --- a/proxmoxtf/resource/cluster/firewall/security_group.go +++ b/proxmoxtf/resource/cluster/firewall/security_group.go @@ -25,6 +25,7 @@ const ( mkSecurityGroupComment = "comment" ) +// SecurityGroup returns a resource to manage security groups. func SecurityGroup() *schema.Resource { s := map[string]*schema.Schema{ mkSecurityGroupName: { @@ -52,6 +53,7 @@ func SecurityGroup() *schema.Resource { } } +// SecurityGroupCreate creates a new security group. func SecurityGroupCreate(ctx context.Context, api clusterfirewall.API, d *schema.ResourceData) diag.Diagnostics { comment := d.Get(mkSecurityGroupComment).(string) name := d.Get(mkSecurityGroupName).(string) @@ -76,6 +78,7 @@ func SecurityGroupCreate(ctx context.Context, api clusterfirewall.API, d *schema return SecurityGroupRead(ctx, api, d) } +// SecurityGroupRead reads the security group from the API and updates the state. func SecurityGroupRead(ctx context.Context, api clusterfirewall.API, d *schema.ResourceData) diag.Diagnostics { var diags diag.Diagnostics @@ -92,6 +95,7 @@ func SecurityGroupRead(ctx context.Context, api clusterfirewall.API, d *schema.R diags = append(diags, diag.FromErr(err)...) err = d.Set(mkSecurityGroupComment, v.Comment) diags = append(diags, diag.FromErr(err)...) + break } } @@ -103,6 +107,7 @@ func SecurityGroupRead(ctx context.Context, api clusterfirewall.API, d *schema.R return firewall.RulesRead(ctx, api.SecurityGroup(name), d) } +// SecurityGroupUpdate updates a security group. func SecurityGroupUpdate(ctx context.Context, api clusterfirewall.API, d *schema.ResourceData) diag.Diagnostics { comment := d.Get(mkSecurityGroupComment).(string) newName := d.Get(mkSecurityGroupName).(string) @@ -129,6 +134,7 @@ func SecurityGroupUpdate(ctx context.Context, api clusterfirewall.API, d *schema return SecurityGroupRead(ctx, api, d) } +// SecurityGroupDelete deletes a security group. func SecurityGroupDelete(ctx context.Context, api clusterfirewall.API, d *schema.ResourceData) diag.Diagnostics { group := d.Id() @@ -143,6 +149,7 @@ func SecurityGroupDelete(ctx context.Context, api clusterfirewall.API, d *schema d.SetId("") return nil } + return diag.FromErr(err) } diff --git a/proxmoxtf/resource/cluster/firewall/security_group_test.go b/proxmoxtf/resource/cluster/firewall/security_group_test.go index 7578dd0a..0c8e599c 100644 --- a/proxmoxtf/resource/cluster/firewall/security_group_test.go +++ b/proxmoxtf/resource/cluster/firewall/security_group_test.go @@ -25,6 +25,7 @@ func TestSecurityGroupInstantiation(t *testing.T) { // TestSecurityGroupSchema tests the SecurityGroup Schema. func TestSecurityGroupSchema(t *testing.T) { t.Parallel() + s := SecurityGroup().Schema structure.AssertRequiredArguments(t, s, []string{ diff --git a/proxmoxtf/resource/container.go b/proxmoxtf/resource/container.go index 8ae4138c..ac632da4 100644 --- a/proxmoxtf/resource/container.go +++ b/proxmoxtf/resource/container.go @@ -17,7 +17,7 @@ import ( "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" "github.com/hashicorp/terraform-plugin-sdk/v2/helper/validation" - "github.com/bpg/terraform-provider-proxmox/proxmox" + "github.com/bpg/terraform-provider-proxmox/proxmox/nodes/containers" "github.com/bpg/terraform-provider-proxmox/proxmox/types" "github.com/bpg/terraform-provider-proxmox/proxmoxtf" ) @@ -117,6 +117,7 @@ const ( mkResourceVirtualEnvironmentContainerVMID = "vm_id" ) +// Container returns a resource that manages a container. func Container() *schema.Resource { return &schema.Resource{ Schema: map[string]*schema.Schema{ @@ -657,7 +658,7 @@ func containerCreate(ctx context.Context, d *schema.ResourceData, m interface{}) func containerCreateClone(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) } @@ -684,9 +685,9 @@ func containerCreateClone(ctx context.Context, d *schema.ResourceData, m interfa vmID := d.Get(mkResourceVirtualEnvironmentContainerVMID).(int) if vmID == -1 { - vmIDNew, err := veClient.GetVMID(ctx) - if err != nil { - return diag.FromErr(err) + vmIDNew, e := api.Cluster().GetVMID(ctx) + if e != nil { + return diag.FromErr(e) } vmID = *vmIDNew @@ -694,7 +695,7 @@ func containerCreateClone(ctx context.Context, d *schema.ResourceData, m interfa fullCopy := types.CustomBool(true) - cloneBody := &proxmox.VirtualEnvironmentContainerCloneRequestBody{ + cloneBody := &containers.CloneRequestBody{ FullCopy: &fullCopy, VMIDNew: vmID, } @@ -718,9 +719,9 @@ func containerCreateClone(ctx context.Context, d *schema.ResourceData, m interfa if cloneNodeName != "" && cloneNodeName != nodeName { cloneBody.TargetNodeName = &nodeName - err = veClient.CloneContainer(ctx, cloneNodeName, cloneVMID, cloneBody) + err = api.Node(cloneNodeName).Container(cloneVMID).CloneContainer(ctx, cloneBody) } else { - err = veClient.CloneContainer(ctx, nodeName, cloneVMID, cloneBody) + err = api.Node(nodeName).Container(cloneVMID).CloneContainer(ctx, cloneBody) } if err != nil { @@ -729,14 +730,16 @@ func containerCreateClone(ctx context.Context, d *schema.ResourceData, m interfa d.SetId(strconv.Itoa(vmID)) + containerAPI := api.Node(nodeName).Container(vmID) + // Wait for the container to be created and its configuration lock to be released. - err = veClient.WaitForContainerLock(ctx, nodeName, vmID, 600, 5, true) + err = containerAPI.WaitForContainerLock(ctx, 600, 5, true) if err != nil { return diag.FromErr(err) } // Now that the virtual machine has been cloned, we need to perform some modifications. - updateBody := &proxmox.VirtualEnvironmentContainerUpdateRequestBody{} + updateBody := &containers.UpdateRequestBody{} console := d.Get(mkResourceVirtualEnvironmentContainerConsole).([]interface{}) @@ -805,6 +808,7 @@ func containerCreateClone(ctx context.Context, d *schema.ResourceData, m interfa initializationIPConfigIPv4Address, ipv4Block[mkResourceVirtualEnvironmentContainerInitializationIPConfigIPv4Address].(string), ) + initializationIPConfigIPv4Gateway = append( initializationIPConfigIPv4Gateway, ipv4Block[mkResourceVirtualEnvironmentContainerInitializationIPConfigIPv4Gateway].(string), @@ -823,6 +827,7 @@ func containerCreateClone(ctx context.Context, d *schema.ResourceData, m interfa initializationIPConfigIPv6Address, ipv6Block[mkResourceVirtualEnvironmentContainerInitializationIPConfigIPv6Address].(string), ) + initializationIPConfigIPv6Gateway = append( initializationIPConfigIPv6Gateway, ipv6Block[mkResourceVirtualEnvironmentContainerInitializationIPConfigIPv6Gateway].(string), @@ -841,7 +846,7 @@ func containerCreateClone(ctx context.Context, d *schema.ResourceData, m interfa if len(keys) > 0 { initializationUserAccountKeys := make( - proxmox.VirtualEnvironmentContainerCustomSSHKeys, + containers.CustomSSHKeys, len(keys), ) @@ -879,25 +884,20 @@ func containerCreateClone(ctx context.Context, d *schema.ResourceData, m interfa networkInterface := d.Get(mkResourceVirtualEnvironmentContainerNetworkInterface).([]interface{}) if len(networkInterface) == 0 { - networkInterface, err = containerGetExistingNetworkInterface( - ctx, - veClient, - nodeName, - vmID, - ) + networkInterface, err = containerGetExistingNetworkInterface(ctx, containerAPI) if err != nil { return diag.FromErr(err) } } networkInterfaceArray := make( - proxmox.VirtualEnvironmentContainerCustomNetworkInterfaceArray, + containers.CustomNetworkInterfaceArray, len(networkInterface), ) for ni, nv := range networkInterface { networkInterfaceMap := nv.(map[string]interface{}) - networkInterfaceObject := proxmox.VirtualEnvironmentContainerCustomNetworkInterface{} + networkInterfaceObject := containers.CustomNetworkInterface{} bridge := networkInterfaceMap[mkResourceVirtualEnvironmentContainerNetworkInterfaceBridge].(string) enabled := networkInterfaceMap[mkResourceVirtualEnvironmentContainerNetworkInterfaceEnabled].(bool) @@ -992,13 +992,13 @@ func containerCreateClone(ctx context.Context, d *schema.ResourceData, m interfa updateBody.Template = &template } - err = veClient.UpdateContainer(ctx, nodeName, vmID, updateBody) + err = containerAPI.UpdateContainer(ctx, updateBody) if err != nil { return diag.FromErr(err) } // Wait for the container's lock to be released. - err = veClient.WaitForContainerLock(ctx, nodeName, vmID, 600, 5, true) + err = containerAPI.WaitForContainerLock(ctx, 600, 5, true) if err != nil { return diag.FromErr(err) } @@ -1008,7 +1008,7 @@ func containerCreateClone(ctx context.Context, d *schema.ResourceData, m interfa func containerCreateCustom(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) } @@ -1063,12 +1063,13 @@ func containerCreateCustom(ctx context.Context, d *schema.ResourceData, m interf diskDatastoreID := diskBlock[mkResourceVirtualEnvironmentContainerDiskDatastoreID].(string) - var rootFS *proxmox.VirtualEnvironmentContainerCustomRootFS + var rootFS *containers.CustomRootFS + diskSize := diskBlock[mkResourceVirtualEnvironmentContainerDiskSize].(int) if diskSize != dvResourceVirtualEnvironmentContainerDiskSize && diskDatastoreID != "" { // This is a special case where the rootfs size is set to a non-default value at creation time. // see https://pve.proxmox.com/pve-docs/chapter-pct.html#_storage_backed_mount_points - rootFS = &proxmox.VirtualEnvironmentContainerCustomRootFS{ + rootFS = &containers.CustomRootFS{ Volume: fmt.Sprintf("%s:%d", diskDatastoreID, diskSize), } } @@ -1085,7 +1086,7 @@ func containerCreateCustom(ctx context.Context, d *schema.ResourceData, m interf } nesting := types.CustomBool(featuresBlock[mkResourceVirtualEnvironmentContainerFeaturesNesting].(bool)) - features := proxmox.VirtualEnvironmentContainerCustomFeatures{ + features := containers.CustomFeatures{ Nesting: &nesting, } @@ -1097,7 +1098,8 @@ func containerCreateCustom(ctx context.Context, d *schema.ResourceData, m interf var initializationIPConfigIPv4Gateway []string var initializationIPConfigIPv6Address []string var initializationIPConfigIPv6Gateway []string - initializationUserAccountKeys := proxmox.VirtualEnvironmentContainerCustomSSHKeys{} + + initializationUserAccountKeys := containers.CustomSSHKeys{} initializationUserAccountPassword := dvResourceVirtualEnvironmentContainerInitializationUserAccountPassword if len(initialization) > 0 { @@ -1124,6 +1126,7 @@ func containerCreateCustom(ctx context.Context, d *schema.ResourceData, m interf initializationIPConfigIPv4Address, ipv4Block[mkResourceVirtualEnvironmentContainerInitializationIPConfigIPv4Address].(string), ) + initializationIPConfigIPv4Gateway = append( initializationIPConfigIPv4Gateway, ipv4Block[mkResourceVirtualEnvironmentContainerInitializationIPConfigIPv4Gateway].(string), @@ -1142,6 +1145,7 @@ func containerCreateCustom(ctx context.Context, d *schema.ResourceData, m interf initializationIPConfigIPv6Address, ipv6Block[mkResourceVirtualEnvironmentContainerInitializationIPConfigIPv6Address].(string), ) + initializationIPConfigIPv6Gateway = append( initializationIPConfigIPv6Gateway, ipv6Block[mkResourceVirtualEnvironmentContainerInitializationIPConfigIPv6Gateway].(string), @@ -1159,7 +1163,7 @@ func containerCreateCustom(ctx context.Context, d *schema.ResourceData, m interf keys := initializationUserAccountBlock[mkResourceVirtualEnvironmentContainerInitializationUserAccountKeys].([]interface{}) initializationUserAccountKeys = make( - proxmox.VirtualEnvironmentContainerCustomSSHKeys, + containers.CustomSSHKeys, len(keys), ) @@ -1187,13 +1191,13 @@ func containerCreateCustom(ctx context.Context, d *schema.ResourceData, m interf networkInterface := d.Get(mkResourceVirtualEnvironmentContainerNetworkInterface).([]interface{}) networkInterfaceArray := make( - proxmox.VirtualEnvironmentContainerCustomNetworkInterfaceArray, + containers.CustomNetworkInterfaceArray, len(networkInterface), ) for ni, nv := range networkInterface { networkInterfaceMap := nv.(map[string]interface{}) - networkInterfaceObject := proxmox.VirtualEnvironmentContainerCustomNetworkInterface{} + networkInterfaceObject := containers.CustomNetworkInterface{} bridge := networkInterfaceMap[mkResourceVirtualEnvironmentContainerNetworkInterfaceBridge].(string) enabled := networkInterfaceMap[mkResourceVirtualEnvironmentContainerNetworkInterfaceEnabled].(bool) @@ -1268,16 +1272,16 @@ func containerCreateCustom(ctx context.Context, d *schema.ResourceData, m interf vmID := d.Get(mkResourceVirtualEnvironmentContainerVMID).(int) if vmID == -1 { - vmIDNew, err := veClient.GetVMID(ctx) - if err != nil { - return diag.FromErr(err) + vmIDNew, e := api.Cluster().GetVMID(ctx) + if e != nil { + return diag.FromErr(e) } vmID = *vmIDNew } // Attempt to create the resource using the retrieved values. - createBody := proxmox.VirtualEnvironmentContainerCreateRequestBody{ + createBody := containers.CreateRequestBody{ ConsoleEnabled: &consoleEnabled, ConsoleMode: &consoleMode, CPUArchitecture: &cpuArchitecture, @@ -1331,7 +1335,7 @@ func containerCreateCustom(ctx context.Context, d *schema.ResourceData, m interf createBody.Tags = &tagsString } - err = veClient.CreateContainer(ctx, nodeName, &createBody) + err = api.Node(nodeName).Container(0).CreateContainer(ctx, &createBody) if err != nil { return diag.FromErr(err) } @@ -1339,7 +1343,7 @@ func containerCreateCustom(ctx context.Context, d *schema.ResourceData, m interf d.SetId(strconv.Itoa(vmID)) // Wait for the container's lock to be released. - err = veClient.WaitForContainerLock(ctx, nodeName, vmID, 600, 5, true) + err = api.Node(nodeName).Container(vmID).WaitForContainerLock(ctx, 600, 5, true) if err != nil { return diag.FromErr(err) } @@ -1356,7 +1360,7 @@ func containerCreateStart(ctx context.Context, d *schema.ResourceData, m interfa } config := m.(proxmoxtf.ProviderConfiguration) - veClient, err := config.GetVEClient() + api, err := config.GetClient() if err != nil { return diag.FromErr(err) } @@ -1367,13 +1371,15 @@ func containerCreateStart(ctx context.Context, d *schema.ResourceData, m interfa return diag.FromErr(err) } + containerAPI := api.Node(nodeName).Container(vmID) + // Start the container and wait for it to reach a running state before continuing. - err = veClient.StartContainer(ctx, nodeName, vmID) + err = containerAPI.StartContainer(ctx) if err != nil { return diag.FromErr(err) } - err = veClient.WaitForContainerState(ctx, nodeName, vmID, "running", 120, 5) + err = containerAPI.WaitForContainerState(ctx, "running", 120, 5) if err != nil { return diag.FromErr(err) } @@ -1400,17 +1406,16 @@ func containerGetCPUArchitectureValidator() schema.SchemaValidateDiagFunc { func containerGetExistingNetworkInterface( ctx context.Context, - client *proxmox.VirtualEnvironmentClient, - nodeName string, - vmID int, + containerAPI *containers.Client, ) ([]interface{}, error) { - containerInfo, err := client.GetContainer(ctx, nodeName, vmID) + containerInfo, err := containerAPI.GetContainer(ctx) if err != nil { return []interface{}{}, err } var networkInterfaces []interface{} - networkInterfaceArray := []*proxmox.VirtualEnvironmentContainerCustomNetworkInterface{ + + networkInterfaceArray := []*containers.CustomNetworkInterface{ containerInfo.NetworkInterface0, containerInfo.NetworkInterface1, containerInfo.NetworkInterface2, @@ -1488,15 +1493,18 @@ func containerGetOperatingSystemTypeValidator() schema.SchemaValidateDiagFunc { } func containerGetTagsString(d *schema.ResourceData) string { - tags := d.Get(mkResourceVirtualEnvironmentContainerTags).([]interface{}) var sanitizedTags []string + + tags := d.Get(mkResourceVirtualEnvironmentContainerTags).([]interface{}) for i := 0; i < len(tags); i++ { tag := strings.TrimSpace(tags[i].(string)) if len(tag) > 0 { sanitizedTags = append(sanitizedTags, tag) } } + sort.Strings(sanitizedTags) + return strings.Join(sanitizedTags, ";") } @@ -1504,27 +1512,32 @@ func containerRead(ctx context.Context, d *schema.ResourceData, m interface{}) d var diags diag.Diagnostics config := m.(proxmoxtf.ProviderConfiguration) - veClient, err := config.GetVEClient() - if err != nil { - return diag.FromErr(err) + + api, e := config.GetClient() + if e != nil { + return diag.FromErr(e) } nodeName := d.Get(mkResourceVirtualEnvironmentContainerNodeName).(string) - vmID, err := strconv.Atoi(d.Id()) - if err != nil { - return diag.FromErr(err) + + vmID, e := strconv.Atoi(d.Id()) + if e != nil { + return diag.FromErr(e) } + containerAPI := api.Node(nodeName).Container(vmID) + // Retrieve the entire configuration in order to compare it to the state. - containerConfig, err := veClient.GetContainer(ctx, nodeName, vmID) - if err != nil { - if strings.Contains(err.Error(), "HTTP 404") || - (strings.Contains(err.Error(), "HTTP 500") && strings.Contains(err.Error(), "does not exist")) { + containerConfig, e := containerAPI.GetContainer(ctx) + if e != nil { + if strings.Contains(e.Error(), "HTTP 404") || + (strings.Contains(e.Error(), "HTTP 500") && strings.Contains(e.Error(), "does not exist")) { d.SetId("") return nil } - return diag.FromErr(err) + + return diag.FromErr(e) } clone := d.Get(mkResourceVirtualEnvironmentContainerClone).([]interface{}) @@ -1534,14 +1547,15 @@ func containerRead(ctx context.Context, d *schema.ResourceData, m interface{}) d if len(clone) == 0 || currentDescription != dvResourceVirtualEnvironmentContainerDescription { if containerConfig.Description != nil { - err = d.Set( + e = d.Set( mkResourceVirtualEnvironmentContainerDescription, strings.TrimSpace(*containerConfig.Description), ) } else { - err = d.Set(mkResourceVirtualEnvironmentContainerDescription, "") + e = d.Set(mkResourceVirtualEnvironmentContainerDescription, "") } - diags = append(diags, diag.FromErr(err)...) + + diags = append(diags, diag.FromErr(e)...) } // Compare the console configuration to the one stored in the state. @@ -1714,7 +1728,8 @@ func containerRead(ctx context.Context, d *schema.ResourceData, m interface{}) d } var ipConfigList []interface{} - networkInterfaceArray := []*proxmox.VirtualEnvironmentContainerCustomNetworkInterface{ + + networkInterfaceArray := []*containers.CustomNetworkInterface{ containerConfig.NetworkInterface0, containerConfig.NetworkInterface1, containerConfig.NetworkInterface2, @@ -1724,6 +1739,7 @@ func containerRead(ctx context.Context, d *schema.ResourceData, m interface{}) d containerConfig.NetworkInterface6, containerConfig.NetworkInterface7, } + var networkInterfaceList []interface{} for _, nv := range networkInterfaceArray { @@ -1860,14 +1876,15 @@ func containerRead(ctx context.Context, d *schema.ResourceData, m interface{}) d } if len(initialization) > 0 { - err = d.Set( + e = d.Set( mkResourceVirtualEnvironmentContainerInitialization, []interface{}{initialization}, ) } else { - err = d.Set(mkResourceVirtualEnvironmentContainerInitialization, []interface{}{}) + e = d.Set(mkResourceVirtualEnvironmentContainerInitialization, []interface{}{}) } - diags = append(diags, diag.FromErr(err)...) + + diags = append(diags, diag.FromErr(e)...) } currentNetworkInterface := d.Get(mkResourceVirtualEnvironmentContainerNetworkInterface).([]interface{}) @@ -1881,11 +1898,11 @@ func containerRead(ctx context.Context, d *schema.ResourceData, m interface{}) d } } else { if len(initialization) > 0 { - err = d.Set(mkResourceVirtualEnvironmentContainerInitialization, []interface{}{initialization}) + e = d.Set(mkResourceVirtualEnvironmentContainerInitialization, []interface{}{initialization}) } else { - err = d.Set(mkResourceVirtualEnvironmentContainerInitialization, []interface{}{}) + e = d.Set(mkResourceVirtualEnvironmentContainerInitialization, []interface{}{}) } - diags = append(diags, diag.FromErr(err)...) + diags = append(diags, diag.FromErr(e)...) err := d.Set(mkResourceVirtualEnvironmentContainerNetworkInterface, networkInterfaceList) diags = append(diags, diag.FromErr(err)...) @@ -1927,6 +1944,7 @@ func containerRead(ctx context.Context, d *schema.ResourceData, m interface{}) d if len(clone) == 0 || len(currentTags) > 0 { var tags []string + if containerConfig.Tags != nil { for _, tag := range strings.Split(*containerConfig.Tags, ";") { t := strings.TrimSpace(tag) @@ -1934,10 +1952,12 @@ func containerRead(ctx context.Context, d *schema.ResourceData, m interface{}) d tags = append(tags, t) } } + sort.Strings(tags) } - err = d.Set(mkResourceVirtualEnvironmentContainerTags, tags) - diags = append(diags, diag.FromErr(err)...) + + e = d.Set(mkResourceVirtualEnvironmentContainerTags, tags) + diags = append(diags, diag.FromErr(e)...) } currentTemplate := d.Get(mkResourceVirtualEnvironmentContainerTemplate).(bool) @@ -1945,43 +1965,48 @@ func containerRead(ctx context.Context, d *schema.ResourceData, m interface{}) d //nolint:gosimple if len(clone) == 0 || currentTemplate != dvResourceVirtualEnvironmentContainerTemplate { if containerConfig.Template != nil { - err = d.Set( + e = d.Set( mkResourceVirtualEnvironmentContainerTemplate, bool(*containerConfig.Template), ) } else { - err = d.Set(mkResourceVirtualEnvironmentContainerTemplate, false) + e = d.Set(mkResourceVirtualEnvironmentContainerTemplate, false) } - diags = append(diags, diag.FromErr(err)...) + + diags = append(diags, diag.FromErr(e)...) } // Determine the state of the container in order to update the "started" argument. - status, err := veClient.GetContainerStatus(ctx, nodeName, vmID) - if err != nil { - return diag.FromErr(err) + status, e := containerAPI.GetContainerStatus(ctx) + if e != nil { + return diag.FromErr(e) } - err = d.Set(mkResourceVirtualEnvironmentContainerStarted, status.Status == "running") - diags = append(diags, diag.FromErr(err)...) + e = d.Set(mkResourceVirtualEnvironmentContainerStarted, status.Status == "running") + diags = append(diags, diag.FromErr(e)...) return diags } func containerUpdate(ctx context.Context, d *schema.ResourceData, m interface{}) diag.Diagnostics { config := m.(proxmoxtf.ProviderConfiguration) - veClient, err := config.GetVEClient() - if err != nil { - return diag.FromErr(err) + + api, e := config.GetClient() + if e != nil { + return diag.FromErr(e) } nodeName := d.Get(mkResourceVirtualEnvironmentContainerNodeName).(string) - vmID, err := strconv.Atoi(d.Id()) - if err != nil { - return diag.FromErr(err) + + vmID, e := strconv.Atoi(d.Id()) + if e != nil { + return diag.FromErr(e) } + containerAPI := api.Node(nodeName).Container(vmID) + // Prepare the new request object. - updateBody := proxmox.VirtualEnvironmentContainerUpdateRequestBody{ + updateBody := containers.UpdateRequestBody{ Delete: []string{}, } @@ -2085,6 +2110,7 @@ func containerUpdate(ctx context.Context, d *schema.ResourceData, m interface{}) initializationIPConfigIPv4Address, ipv4Block[mkResourceVirtualEnvironmentContainerInitializationIPConfigIPv4Address].(string), ) + initializationIPConfigIPv4Gateway = append( initializationIPConfigIPv4Gateway, ipv4Block[mkResourceVirtualEnvironmentContainerInitializationIPConfigIPv4Gateway].(string), @@ -2103,6 +2129,7 @@ func containerUpdate(ctx context.Context, d *schema.ResourceData, m interface{}) initializationIPConfigIPv6Address, ipv6Block[mkResourceVirtualEnvironmentContainerInitializationIPConfigIPv6Address].(string), ) + initializationIPConfigIPv6Gateway = append( initializationIPConfigIPv6Gateway, ipv6Block[mkResourceVirtualEnvironmentContainerInitializationIPConfigIPv6Gateway].(string), @@ -2148,14 +2175,9 @@ func containerUpdate(ctx context.Context, d *schema.ResourceData, m interface{}) networkInterface := d.Get(mkResourceVirtualEnvironmentContainerNetworkInterface).([]interface{}) if len(networkInterface) == 0 && len(clone) > 0 { - networkInterface, err = containerGetExistingNetworkInterface( - ctx, - veClient, - nodeName, - vmID, - ) - if err != nil { - return diag.FromErr(err) + networkInterface, e = containerGetExistingNetworkInterface(ctx, containerAPI) + if e != nil { + return diag.FromErr(e) } } @@ -2163,13 +2185,13 @@ func containerUpdate(ctx context.Context, d *schema.ResourceData, m interface{}) if d.HasChange(mkResourceVirtualEnvironmentContainerInitialization) || d.HasChange(mkResourceVirtualEnvironmentContainerNetworkInterface) { networkInterfaceArray := make( - proxmox.VirtualEnvironmentContainerCustomNetworkInterfaceArray, + containers.CustomNetworkInterfaceArray, len(networkInterface), ) for ni, nv := range networkInterface { networkInterfaceMap := nv.(map[string]interface{}) - networkInterfaceObject := proxmox.VirtualEnvironmentContainerCustomNetworkInterface{} + networkInterfaceObject := containers.CustomNetworkInterface{} bridge := networkInterfaceMap[mkResourceVirtualEnvironmentContainerNetworkInterfaceBridge].(string) enabled := networkInterfaceMap[mkResourceVirtualEnvironmentContainerNetworkInterfaceEnabled].(bool) @@ -2269,9 +2291,9 @@ func containerUpdate(ctx context.Context, d *schema.ResourceData, m interface{}) } // Update the configuration now that everything has been prepared. - err = veClient.UpdateContainer(ctx, nodeName, vmID, &updateBody) - if err != nil { - return diag.FromErr(err) + e = containerAPI.UpdateContainer(ctx, &updateBody) + if e != nil { + return diag.FromErr(e) } // Determine if the state of the container needs to be changed. @@ -2279,30 +2301,30 @@ func containerUpdate(ctx context.Context, d *schema.ResourceData, m interface{}) if d.HasChange(mkResourceVirtualEnvironmentContainerStarted) && !bool(template) { if started { - err = veClient.StartContainer(ctx, nodeName, vmID) - if err != nil { - return diag.FromErr(err) + e = containerAPI.StartContainer(ctx) + if e != nil { + return diag.FromErr(e) } - err = veClient.WaitForContainerState(ctx, nodeName, vmID, "running", 300, 5) - if err != nil { - return diag.FromErr(err) + e = containerAPI.WaitForContainerState(ctx, "running", 300, 5) + if e != nil { + return diag.FromErr(e) } } else { forceStop := types.CustomBool(true) shutdownTimeout := 300 - err = veClient.ShutdownContainer(ctx, nodeName, vmID, &proxmox.VirtualEnvironmentContainerShutdownRequestBody{ + e = containerAPI.ShutdownContainer(ctx, &containers.ShutdownRequestBody{ ForceStop: &forceStop, Timeout: &shutdownTimeout, }) - if err != nil { - return diag.FromErr(err) + if e != nil { + return diag.FromErr(e) } - err = veClient.WaitForContainerState(ctx, nodeName, vmID, "stopped", 300, 5) - if err != nil { - return diag.FromErr(err) + e = containerAPI.WaitForContainerState(ctx, "stopped", 300, 5) + if e != nil { + return diag.FromErr(e) } rebootRequired = false @@ -2313,16 +2335,14 @@ func containerUpdate(ctx context.Context, d *schema.ResourceData, m interface{}) if !bool(template) && rebootRequired { rebootTimeout := 300 - err = veClient.RebootContainer( + e = containerAPI.RebootContainer( ctx, - nodeName, - vmID, - &proxmox.VirtualEnvironmentContainerRebootRequestBody{ + &containers.RebootRequestBody{ Timeout: &rebootTimeout, }, ) - if err != nil { - return diag.FromErr(err) + if e != nil { + return diag.FromErr(e) } } @@ -2331,7 +2351,7 @@ func containerUpdate(ctx context.Context, d *schema.ResourceData, m interface{}) func containerDelete(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) } @@ -2342,8 +2362,10 @@ func containerDelete(ctx context.Context, d *schema.ResourceData, m interface{}) return diag.FromErr(err) } + containerAPI := api.Node(nodeName).Container(vmID) + // Shut down the container before deleting it. - status, err := veClient.GetContainerStatus(ctx, nodeName, vmID) + status, err := containerAPI.GetContainerStatus(ctx) if err != nil { return diag.FromErr(err) } @@ -2352,11 +2374,9 @@ func containerDelete(ctx context.Context, d *schema.ResourceData, m interface{}) forceStop := types.CustomBool(true) shutdownTimeout := 300 - err = veClient.ShutdownContainer( + err = containerAPI.ShutdownContainer( ctx, - nodeName, - vmID, - &proxmox.VirtualEnvironmentContainerShutdownRequestBody{ + &containers.ShutdownRequestBody{ ForceStop: &forceStop, Timeout: &shutdownTimeout, }, @@ -2365,13 +2385,13 @@ func containerDelete(ctx context.Context, d *schema.ResourceData, m interface{}) return diag.FromErr(err) } - err = veClient.WaitForContainerState(ctx, nodeName, vmID, "stopped", 30, 5) + err = containerAPI.WaitForContainerState(ctx, "stopped", 30, 5) if err != nil { return diag.FromErr(err) } } - err = veClient.DeleteContainer(ctx, nodeName, vmID) + err = containerAPI.DeleteContainer(ctx) if err != nil { if strings.Contains(err.Error(), "HTTP 404") { @@ -2383,7 +2403,7 @@ func containerDelete(ctx context.Context, d *schema.ResourceData, m interface{}) } // Wait for the state to become unavailable as that clearly indicates the destruction of the container. - err = veClient.WaitForContainerState(ctx, nodeName, vmID, "", 60, 2) + err = containerAPI.WaitForContainerState(ctx, "", 60, 2) if err == nil { return diag.Errorf("failed to delete container \"%d\"", vmID) } diff --git a/proxmoxtf/resource/container_test.go b/proxmoxtf/resource/container_test.go index 149c8fa2..0ecfcf4f 100644 --- a/proxmoxtf/resource/container_test.go +++ b/proxmoxtf/resource/container_test.go @@ -17,8 +17,8 @@ import ( // TestContainerInstantiation tests whether the Container instance can be instantiated. func TestContainerInstantiation(t *testing.T) { t.Parallel() - s := Container() + s := Container() if s == nil { t.Fatalf("Cannot instantiate Container") } @@ -27,6 +27,7 @@ func TestContainerInstantiation(t *testing.T) { // TestContainerSchema tests the Container schema. func TestContainerSchema(t *testing.T) { t.Parallel() + s := Container() test.AssertRequiredArguments(t, s, []string{ diff --git a/proxmoxtf/resource/dns.go b/proxmoxtf/resource/dns.go index 2d0114d0..dc7d3a5f 100644 --- a/proxmoxtf/resource/dns.go +++ b/proxmoxtf/resource/dns.go @@ -13,7 +13,7 @@ import ( "github.com/hashicorp/terraform-plugin-sdk/v2/diag" "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" - "github.com/bpg/terraform-provider-proxmox/proxmox" + "github.com/bpg/terraform-provider-proxmox/proxmox/nodes" "github.com/bpg/terraform-provider-proxmox/proxmoxtf" ) @@ -23,6 +23,7 @@ const ( mkResourceVirtualEnvironmentDNSServers = "servers" ) +// DNS returns a resource that manages DNS settings for a node. func DNS() *schema.Resource { return &schema.Resource{ Schema: map[string]*schema.Schema{ @@ -69,11 +70,11 @@ func dnsCreate(ctx context.Context, d *schema.ResourceData, m interface{}) diag. return nil } -func dnsGetUpdateBody(d *schema.ResourceData) *proxmox.VirtualEnvironmentDNSUpdateRequestBody { +func dnsGetUpdateBody(d *schema.ResourceData) *nodes.DNSUpdateRequestBody { domain := d.Get(mkResourceVirtualEnvironmentDNSDomain).(string) servers := d.Get(mkResourceVirtualEnvironmentDNSServers).([]interface{}) - body := &proxmox.VirtualEnvironmentDNSUpdateRequestBody{ + body := &nodes.DNSUpdateRequestBody{ SearchDomain: &domain, } @@ -97,13 +98,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(mkResourceVirtualEnvironmentDNSNodeName).(string) - dns, err := veClient.GetDNS(ctx, nodeName) + + dns, err := api.Node(nodeName).GetDNS(ctx) if err != nil { return diag.FromErr(err) } @@ -137,7 +139,7 @@ func dnsRead(ctx context.Context, d *schema.ResourceData, m interface{}) diag.Di func dnsUpdate(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) } @@ -146,7 +148,7 @@ func dnsUpdate(ctx context.Context, d *schema.ResourceData, m interface{}) diag. body := dnsGetUpdateBody(d) - err = veClient.UpdateDNS(ctx, nodeName, body) + err = api.Node(nodeName).UpdateDNS(ctx, body) if err != nil { return diag.FromErr(err) } diff --git a/proxmoxtf/resource/dns_test.go b/proxmoxtf/resource/dns_test.go index 2337a4bd..6eb765c5 100644 --- a/proxmoxtf/resource/dns_test.go +++ b/proxmoxtf/resource/dns_test.go @@ -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{ diff --git a/proxmoxtf/resource/file.go b/proxmoxtf/resource/file.go index 90ef3478..2ecea457 100644 --- a/proxmoxtf/resource/file.go +++ b/proxmoxtf/resource/file.go @@ -25,8 +25,9 @@ import ( "github.com/hashicorp/terraform-plugin-sdk/v2/diag" "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" - "github.com/bpg/terraform-provider-proxmox/proxmox" + "github.com/bpg/terraform-provider-proxmox/proxmox/api" "github.com/bpg/terraform-provider-proxmox/proxmoxtf" + "github.com/bpg/terraform-provider-proxmox/utils" ) const ( @@ -56,6 +57,7 @@ const ( mkResourceVirtualEnvironmentFileSourceRawResize = "resize" ) +// File returns a resource that manages files on a node. func File() *schema.Resource { return &schema.Resource{ Schema: map[string]*schema.Schema{ @@ -195,12 +197,6 @@ func File() *schema.Resource { func fileCreate(ctx context.Context, d *schema.ResourceData, m interface{}) diag.Diagnostics { var diags diag.Diagnostics - config := m.(proxmoxtf.ProviderConfiguration) - veClient, err := config.GetVEClient() - if err != nil { - return diag.FromErr(err) - } - contentType, dg := fileGetContentType(d) diags = append(diags, dg...) @@ -254,7 +250,8 @@ func fileCreate(ctx context.Context, d *schema.ResourceData, m interface{}) diag if err != nil { return diag.FromErr(err) } - defer proxmox.CloseOrLogError(ctx)(res.Body) + + defer utils.CloseOrLogError(ctx)(res.Body) tempDownloadedFile, err := os.CreateTemp("", "download") if err != nil { @@ -378,15 +375,44 @@ func fileCreate(ctx context.Context, d *schema.ResourceData, m interface{}) diag } }(file) - body := &proxmox.DatastoreUploadRequestBody{ - ContentType: *contentType, - DatastoreID: datastoreID, - FileName: *fileName, - File: file, - NodeName: nodeName, + config := m.(proxmoxtf.ProviderConfiguration) + + capi, err := config.GetClient() + if err != nil { + return diag.FromErr(err) + } + + request := &api.FileUploadRequest{ + ContentType: *contentType, + FileName: *fileName, + File: file, + } + + switch *contentType { + case "iso", "vztmpl": + _, err = capi.Node(nodeName).APIUpload(ctx, datastoreID, request) + default: + // For all other content types, we need to upload the file to the node's + // datastore using SFTP. + nodeAddress, err2 := capi.Node(nodeName).GetIP(ctx) + if err2 != nil { + return diag.Errorf("failed to get node IP: %s", err2) + } + + datastore, err2 := capi.Storage().GetDatastore(ctx, datastoreID) + if err2 != nil { + return diag.Errorf("failed to get datastore: %s", err2) + } + + if datastore.Path == nil || *datastore.Path == "" { + return diag.Errorf("failed to determine the datastore path") + } + + remoteFileDir := *datastore.Path + + err = capi.SSH().NodeUpload(ctx, nodeAddress, remoteFileDir, request) } - _, err = veClient.UploadFileToDatastore(ctx, body) if err != nil { return diag.FromErr(err) } @@ -530,7 +556,7 @@ func fileIsURL(d *schema.ResourceData) bool { func fileRead(ctx context.Context, d *schema.ResourceData, m interface{}) diag.Diagnostics { config := m.(proxmoxtf.ProviderConfiguration) - veClient, err := config.GetVEClient() + capi, err := config.GetClient() if err != nil { return diag.FromErr(err) } @@ -547,7 +573,7 @@ func fileRead(ctx context.Context, d *schema.ResourceData, m interface{}) diag.D sourceFileBlock := sourceFile[0].(map[string]interface{}) sourceFilePath = sourceFileBlock[mkResourceVirtualEnvironmentFileSourceFilePath].(string) - list, err := veClient.ListDatastoreFiles(ctx, nodeName, datastoreID) + list, err := capi.Node(nodeName).ListDatastoreFiles(ctx, datastoreID) if err != nil { return diag.FromErr(err) } @@ -591,6 +617,7 @@ func fileRead(ctx context.Context, d *schema.ResourceData, m interface{}) diag.D if lastFileMD != "" && lastFileSize != 0 && lastFileTag != "" { changed = lastFileMD != fileModificationDate || lastFileSize != fileSize || lastFileTag != fileTag } + sourceFileBlock[mkResourceVirtualEnvironmentFileSourceFileChanged] = changed err = d.Set(mkResourceVirtualEnvironmentFileSourceFile, sourceFile) diags = append(diags, diag.FromErr(err)...) @@ -607,6 +634,7 @@ func fileRead(ctx context.Context, d *schema.ResourceData, m interface{}) diag.D return nil } +//nolint:nonamedreturns func readFile( ctx context.Context, sourceFilePath string, @@ -617,10 +645,10 @@ func readFile( } defer func(f *os.File) { - err := f.Close() - if err != nil { + e := f.Close() + if e != nil { tflog.Error(ctx, "failed to close the file", map[string]interface{}{ - "error": err.Error(), + "error": e.Error(), }) } }(f) @@ -637,6 +665,7 @@ func readFile( return fileModificationDate, fileSize, fileTag, nil } +//nolint:nonamedreturns func readURL( ctx context.Context, d *schema.ResourceData, @@ -647,7 +676,7 @@ func readURL( return } - defer proxmox.CloseOrLogError(ctx)(res.Body) + defer utils.CloseOrLogError(ctx)(res.Body) fileSize = res.ContentLength httpLastModified := res.Header.Get("Last-Modified") @@ -690,7 +719,7 @@ func readURL( func fileDelete(ctx context.Context, d *schema.ResourceData, m interface{}) diag.Diagnostics { config := m.(proxmoxtf.ProviderConfiguration) - veClient, err := config.GetVEClient() + capi, err := config.GetClient() if err != nil { return diag.FromErr(err) } @@ -698,7 +727,7 @@ func fileDelete(ctx context.Context, d *schema.ResourceData, m interface{}) diag datastoreID := d.Get(mkResourceVirtualEnvironmentFileDatastoreID).(string) nodeName := d.Get(mkResourceVirtualEnvironmentFileNodeName).(string) - err = veClient.DeleteDatastoreFile(ctx, nodeName, datastoreID, d.Id()) + err = capi.Node(nodeName).DeleteDatastoreFile(ctx, datastoreID, d.Id()) if err != nil { if strings.Contains(err.Error(), "HTTP 404") { diff --git a/proxmoxtf/resource/file_test.go b/proxmoxtf/resource/file_test.go index 91f0fe0d..81edc426 100644 --- a/proxmoxtf/resource/file_test.go +++ b/proxmoxtf/resource/file_test.go @@ -17,8 +17,8 @@ import ( // TestFileInstantiation tests whether the File instance can be instantiated. func TestFileInstantiation(t *testing.T) { t.Parallel() - s := File() + s := File() if s == nil { t.Fatalf("Cannot instantiate File") } @@ -27,6 +27,7 @@ func TestFileInstantiation(t *testing.T) { // TestFileSchema tests the File schema. func TestFileSchema(t *testing.T) { t.Parallel() + s := File() test.AssertRequiredArguments(t, s, []string{ diff --git a/proxmoxtf/resource/firewall/alias.go b/proxmoxtf/resource/firewall/alias.go index a5e069e2..45ced202 100644 --- a/proxmoxtf/resource/firewall/alias.go +++ b/proxmoxtf/resource/firewall/alias.go @@ -23,6 +23,7 @@ const ( mkAliasComment = "comment" ) +// Alias returns a resource to manage aliases. func Alias() *schema.Resource { s := map[string]*schema.Schema{ mkAliasName: { @@ -77,12 +78,14 @@ func aliasCreate(ctx context.Context, api firewall.API, d *schema.ResourceData) func aliasRead(ctx context.Context, api firewall.API, d *schema.ResourceData) diag.Diagnostics { name := d.Id() + alias, err := api.GetAlias(ctx, name) if err != nil { if strings.Contains(err.Error(), "no such alias") { d.SetId("") return nil } + return diag.FromErr(err) } @@ -126,12 +129,14 @@ func aliasUpdate(ctx context.Context, api firewall.API, d *schema.ResourceData) func aliasDelete(ctx context.Context, api firewall.API, d *schema.ResourceData) diag.Diagnostics { name := d.Id() + err := api.DeleteAlias(ctx, name) if err != nil { if strings.Contains(err.Error(), "no such alias") { d.SetId("") return nil } + return diag.FromErr(err) } diff --git a/proxmoxtf/resource/firewall/alias_test.go b/proxmoxtf/resource/firewall/alias_test.go index 8e655fa9..0f3392d7 100644 --- a/proxmoxtf/resource/firewall/alias_test.go +++ b/proxmoxtf/resource/firewall/alias_test.go @@ -24,6 +24,7 @@ func TestAliasInstantiation(t *testing.T) { // TestAliasSchema tests the Alias Schema. func TestAliasSchema(t *testing.T) { t.Parallel() + s := Alias().Schema structure.AssertRequiredArguments(t, s, []string{ diff --git a/proxmoxtf/resource/firewall/ipset.go b/proxmoxtf/resource/firewall/ipset.go index 8a62a93f..b7373e94 100644 --- a/proxmoxtf/resource/firewall/ipset.go +++ b/proxmoxtf/resource/firewall/ipset.go @@ -29,6 +29,7 @@ const ( mkIPSetCIDRNoMatch = "nomatch" ) +// IPSet returns a resource to manage IP sets. func IPSet() *schema.Resource { s := map[string]*schema.Schema{ mkIPSetName: { @@ -104,11 +105,12 @@ func ipSetCreate(ctx context.Context, api firewall.API, d *schema.ResourceData) cidr := ipSetMap[mkIPSetCIDRName].(string) noMatch := ipSetMap[mkIPSetCIDRNoMatch].(bool) - comm := ipSetMap[mkIPSetCIDRComment].(string) + comm := ipSetMap[mkIPSetCIDRComment].(string) if comm != "" { ipSetObject.Comment = &comm } + ipSetObject.CIDR = cidr if noMatch { @@ -157,6 +159,7 @@ func ipSetRead(ctx context.Context, api firewall.API, d *schema.ResourceData) di diags = append(diags, diag.FromErr(err)...) err = d.Set(mkIPSetCIDRComment, v.Comment) diags = append(diags, diag.FromErr(err)...) + break } } @@ -167,12 +170,15 @@ func ipSetRead(ctx context.Context, api firewall.API, d *schema.ResourceData) di d.SetId("") return nil } + diags = append(diags, diag.FromErr(err)...) + return diags } //nolint:prealloc var entries []interface{} + for key := range ipSet { entry := map[string]interface{}{} diff --git a/proxmoxtf/resource/firewall/ipset_test.go b/proxmoxtf/resource/firewall/ipset_test.go index 51ffdcf9..8f2b0137 100644 --- a/proxmoxtf/resource/firewall/ipset_test.go +++ b/proxmoxtf/resource/firewall/ipset_test.go @@ -25,6 +25,7 @@ func TestIPSetInstantiation(t *testing.T) { // TestIPSetSchema tests the IPSet Schema. func TestIPSetSchema(t *testing.T) { t.Parallel() + s := IPSet().Schema structure.AssertRequiredArguments(t, s, []string{ diff --git a/proxmoxtf/resource/firewall/options.go b/proxmoxtf/resource/firewall/options.go index e2722db8..77b2f9c7 100644 --- a/proxmoxtf/resource/firewall/options.go +++ b/proxmoxtf/resource/firewall/options.go @@ -41,6 +41,7 @@ const ( mkRadv = "radv" ) +// Options returns a resource to manage firewall options. func Options() *schema.Resource { s := map[string]*schema.Schema{ mkDHCP: { diff --git a/proxmoxtf/resource/firewall/options_test.go b/proxmoxtf/resource/firewall/options_test.go index 83ecfd94..179236e6 100644 --- a/proxmoxtf/resource/firewall/options_test.go +++ b/proxmoxtf/resource/firewall/options_test.go @@ -24,6 +24,7 @@ func TestOptionsInstantiation(t *testing.T) { // TestOptionsSchema tests the Options Schema. func TestOptionsSchema(t *testing.T) { t.Parallel() + s := Options().Schema structure.AssertOptionalArguments(t, s, []string{ diff --git a/proxmoxtf/resource/firewall/rules.go b/proxmoxtf/resource/firewall/rules.go index db8a91b6..c3d79b53 100644 --- a/proxmoxtf/resource/firewall/rules.go +++ b/proxmoxtf/resource/firewall/rules.go @@ -34,6 +34,7 @@ const ( dvRuleSPort = "" dvRuleSource = "" + // MkRule defines the name of the rule resource in the schema. MkRule = "rule" mkSecurityGroup = "security_group" @@ -53,6 +54,7 @@ const ( mkRuleType = "type" ) +// Rules returns a resource that manages firewall rules. func Rules() *schema.Resource { rule := map[string]*schema.Schema{ mkRulePos: { @@ -176,14 +178,17 @@ func Rules() *schema.Resource { } } +// RulesCreate creates new firewall rules. func RulesCreate(ctx context.Context, api firewall.Rule, d *schema.ResourceData) diag.Diagnostics { var diags diag.Diagnostics rules := d.Get(MkRule).([]interface{}) + for i := len(rules) - 1; i >= 0; i-- { - rule := rules[i].(map[string]interface{}) var ruleBody firewall.RuleCreateRequestBody + rule := rules[i].(map[string]interface{}) + sg := rule[mkSecurityGroup].(string) if sg != "" { // this is a special case of security group insertion @@ -226,6 +231,7 @@ func RulesCreate(ctx context.Context, api firewall.Rule, d *schema.ResourceData) return RulesRead(ctx, api, d) } +// RulesRead reads rules from the API and updates the state. func RulesRead(ctx context.Context, api firewall.Rule, d *schema.ResourceData) diag.Diagnostics { var diags diag.Diagnostics @@ -287,6 +293,7 @@ func RulesRead(ctx context.Context, api firewall.Rule, d *schema.ResourceData) d return diags } +// RulesUpdate updates rules. func RulesUpdate(ctx context.Context, api firewall.Rule, d *schema.ResourceData) diag.Diagnostics { var diags diag.Diagnostics @@ -302,10 +309,12 @@ func RulesUpdate(ctx context.Context, api firewall.Rule, d *schema.ResourceData) if pos >= 0 { ruleBody.Pos = &pos } + action := rule[mkRuleAction].(string) if action != "" { ruleBody.Action = &action } + rType := rule[mkRuleType].(string) if rType != "" { ruleBody.Type = &rType @@ -324,6 +333,7 @@ func RulesUpdate(ctx context.Context, api firewall.Rule, d *schema.ResourceData) return RulesRead(ctx, api, d) } +// RulesDelete deletes all rules. func RulesDelete(ctx context.Context, api firewall.Rule, d *schema.ResourceData) diag.Diagnostics { var diags diag.Diagnostics @@ -351,14 +361,17 @@ func mapToBaseRule(rule map[string]interface{}) *firewall.BaseRule { if comment != "" { baseRule.Comment = &comment } + dest := rule[mkRuleDest].(string) if dest != "" { baseRule.Dest = &dest } + dport := rule[mkRuleDPort].(string) if dport != "" { baseRule.DPort = &dport } + enableBool := types.CustomBool(rule[mkRuleEnabled].(bool)) baseRule.Enable = &enableBool @@ -366,22 +379,27 @@ func mapToBaseRule(rule map[string]interface{}) *firewall.BaseRule { if iface != "" { baseRule.IFace = &iface } + log := rule[mkRuleLog].(string) if log != "" { baseRule.Log = &log } + macro := rule[mkRuleMacro].(string) if macro != "" { baseRule.Macro = ¯o } + proto := rule[mkRuleProto].(string) if proto != "" { baseRule.Proto = &proto } + source := rule[mkRuleSource].(string) if source != "" { baseRule.Source = &source } + sport := rule[mkRuleSPort].(string) if sport != "" { baseRule.SPort = &sport @@ -413,30 +431,39 @@ func baseRuleToMap(baseRule *firewall.BaseRule, rule map[string]interface{}) { if baseRule.Comment != nil { rule[mkRuleComment] = *baseRule.Comment } + if baseRule.Dest != nil { rule[mkRuleDest] = *baseRule.Dest } + if baseRule.DPort != nil { rule[mkRuleDPort] = *baseRule.DPort } + if baseRule.Enable != nil { rule[mkRuleEnabled] = *baseRule.Enable } + if baseRule.IFace != nil { rule[mkRuleIFace] = *baseRule.IFace } + if baseRule.Log != nil { rule[mkRuleLog] = *baseRule.Log } + if baseRule.Macro != nil { rule[mkRuleMacro] = *baseRule.Macro } + if baseRule.Proto != nil { rule[mkRuleProto] = *baseRule.Proto } + if baseRule.Source != nil { rule[mkRuleSource] = *baseRule.Source } + if baseRule.SPort != nil { rule[mkRuleSPort] = *baseRule.SPort } @@ -446,9 +473,11 @@ func securityGroupBaseRuleToMap(baseRule *firewall.BaseRule, rule map[string]int if baseRule.Comment != nil { rule[mkRuleComment] = *baseRule.Comment } + if baseRule.Enable != nil { rule[mkRuleEnabled] = *baseRule.Enable } + if baseRule.IFace != nil { rule[mkRuleIFace] = *baseRule.IFace } diff --git a/proxmoxtf/resource/firewall/rules_test.go b/proxmoxtf/resource/firewall/rules_test.go index 2ce128d2..a66a765d 100644 --- a/proxmoxtf/resource/firewall/rules_test.go +++ b/proxmoxtf/resource/firewall/rules_test.go @@ -24,6 +24,7 @@ func TestRuleInstantiation(t *testing.T) { // TestRuleSchema tests the Rules Schema. func TestRuleSchema(t *testing.T) { t.Parallel() + rulesSchema := Rules().Schema structure.AssertRequiredArguments(t, rulesSchema, []string{ diff --git a/proxmoxtf/resource/firewall/selector.go b/proxmoxtf/resource/firewall/selector.go index 4eafb23a..911fade5 100644 --- a/proxmoxtf/resource/firewall/selector.go +++ b/proxmoxtf/resource/firewall/selector.go @@ -46,6 +46,7 @@ func selectorSchemaMandatory() map[string]*schema.Schema { s := selectorSchema() s[mkSelectorNodeName].Optional = false s[mkSelectorNodeName].Required = true + return s } @@ -54,21 +55,25 @@ func selectFirewallAPI( ) 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) } - var api firewall.API = veClient.API().Cluster().Firewall() + var fwAPI firewall.API = api.Cluster().Firewall() + if nn, ok := d.GetOk(mkSelectorNodeName); ok { nodeName := nn.(string) + nodeAPI := api.Node(nodeName) + if v, ok := d.GetOk(mkSelectorVMID); ok { - api = veClient.API().VM(nodeName, v.(int)).Firewall() + fwAPI = nodeAPI.VM(v.(int)).Firewall() } else if v, ok := d.GetOk(mkSelectorContainerID); ok { - api = veClient.API().Container(nodeName, v.(int)).Firewall() + fwAPI = nodeAPI.Container(v.(int)).Firewall() } } - return f(ctx, api, d) + return f(ctx, fwAPI, d) } } diff --git a/proxmoxtf/resource/group.go b/proxmoxtf/resource/group.go index 64c5fc7a..61453cae 100644 --- a/proxmoxtf/resource/group.go +++ b/proxmoxtf/resource/group.go @@ -13,7 +13,7 @@ import ( "github.com/hashicorp/terraform-plugin-sdk/v2/diag" "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" - "github.com/bpg/terraform-provider-proxmox/proxmox" + "github.com/bpg/terraform-provider-proxmox/proxmox/access" "github.com/bpg/terraform-provider-proxmox/proxmox/types" "github.com/bpg/terraform-provider-proxmox/proxmoxtf" ) @@ -30,6 +30,7 @@ const ( mkResourceVirtualEnvironmentGroupMembers = "members" ) +// Group returns a resource that manages a group in the Proxmox VE access control list. func Group() *schema.Resource { return &schema.Resource{ Schema: map[string]*schema.Schema{ @@ -89,7 +90,7 @@ func Group() *schema.Resource { func groupCreate(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) } @@ -97,12 +98,12 @@ func groupCreate(ctx context.Context, d *schema.ResourceData, m interface{}) dia comment := d.Get(mkResourceVirtualEnvironmentGroupComment).(string) groupID := d.Get(mkResourceVirtualEnvironmentGroupID).(string) - body := &proxmox.VirtualEnvironmentGroupCreateRequestBody{ + body := &access.GroupCreateRequestBody{ Comment: &comment, ID: groupID, } - err = veClient.CreateGroup(ctx, body) + err = api.Access().CreateGroup(ctx, body) if err != nil { return diag.FromErr(err) } @@ -118,7 +119,7 @@ func groupCreate(ctx context.Context, d *schema.ResourceData, m interface{}) dia aclEntry[mkResourceVirtualEnvironmentGroupACLPropagate].(bool), ) - aclBody := &proxmox.VirtualEnvironmentACLUpdateRequestBody{ + aclBody := &access.ACLUpdateRequestBody{ Delete: &aclDelete, Groups: []string{groupID}, Path: aclEntry[mkResourceVirtualEnvironmentGroupACLPath].(string), @@ -126,7 +127,7 @@ func groupCreate(ctx context.Context, d *schema.ResourceData, m interface{}) dia Roles: []string{aclEntry[mkResourceVirtualEnvironmentGroupACLRoleID].(string)}, } - err := veClient.UpdateACL(ctx, aclBody) + err := api.Access().UpdateACL(ctx, aclBody) if err != nil { return diag.FromErr(err) } @@ -139,13 +140,13 @@ func groupRead(ctx context.Context, d *schema.ResourceData, m interface{}) diag. var diags diag.Diagnostics config := m.(proxmoxtf.ProviderConfiguration) - veClient, err := config.GetVEClient() + api, err := config.GetClient() if err != nil { return diag.FromErr(err) } groupID := d.Id() - group, err := veClient.GetGroup(ctx, groupID) + group, err := api.Access().GetGroup(ctx, groupID) if err != nil { if strings.Contains(err.Error(), "HTTP 404") { d.SetId("") @@ -155,7 +156,7 @@ func groupRead(ctx context.Context, d *schema.ResourceData, m interface{}) diag. return diag.FromErr(err) } - acl, err := veClient.GetACL(ctx) + acl, err := api.Access().GetACL(ctx) if err != nil { return diag.FromErr(err) } @@ -198,7 +199,7 @@ func groupRead(ctx context.Context, d *schema.ResourceData, m interface{}) diag. func groupUpdate(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) } @@ -206,11 +207,11 @@ func groupUpdate(ctx context.Context, d *schema.ResourceData, m interface{}) dia comment := d.Get(mkResourceVirtualEnvironmentGroupComment).(string) groupID := d.Id() - body := &proxmox.VirtualEnvironmentGroupUpdateRequestBody{ + body := &access.GroupUpdateRequestBody{ Comment: &comment, } - err = veClient.UpdateGroup(ctx, groupID, body) + err = api.Access().UpdateGroup(ctx, groupID, body) if err != nil { return diag.FromErr(err) } @@ -225,7 +226,7 @@ func groupUpdate(ctx context.Context, d *schema.ResourceData, m interface{}) dia aclEntry[mkResourceVirtualEnvironmentGroupACLPropagate].(bool), ) - aclBody := &proxmox.VirtualEnvironmentACLUpdateRequestBody{ + aclBody := &access.ACLUpdateRequestBody{ Delete: &aclDelete, Groups: []string{groupID}, Path: aclEntry[mkResourceVirtualEnvironmentGroupACLPath].(string), @@ -233,7 +234,7 @@ func groupUpdate(ctx context.Context, d *schema.ResourceData, m interface{}) dia Roles: []string{aclEntry[mkResourceVirtualEnvironmentGroupACLRoleID].(string)}, } - err := veClient.UpdateACL(ctx, aclBody) + err := api.Access().UpdateACL(ctx, aclBody) if err != nil { return diag.FromErr(err) } @@ -248,7 +249,7 @@ func groupUpdate(ctx context.Context, d *schema.ResourceData, m interface{}) dia aclEntry[mkResourceVirtualEnvironmentGroupACLPropagate].(bool), ) - aclBody := &proxmox.VirtualEnvironmentACLUpdateRequestBody{ + aclBody := &access.ACLUpdateRequestBody{ Delete: &aclDelete, Groups: []string{groupID}, Path: aclEntry[mkResourceVirtualEnvironmentGroupACLPath].(string), @@ -256,7 +257,7 @@ func groupUpdate(ctx context.Context, d *schema.ResourceData, m interface{}) dia Roles: []string{aclEntry[mkResourceVirtualEnvironmentGroupACLRoleID].(string)}, } - err := veClient.UpdateACL(ctx, aclBody) + err := api.Access().UpdateACL(ctx, aclBody) if err != nil { return diag.FromErr(err) } @@ -267,7 +268,7 @@ func groupUpdate(ctx context.Context, d *schema.ResourceData, m interface{}) dia func groupDelete(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) } @@ -282,7 +283,7 @@ func groupDelete(ctx context.Context, d *schema.ResourceData, m interface{}) dia aclEntry[mkResourceVirtualEnvironmentGroupACLPropagate].(bool), ) - aclBody := &proxmox.VirtualEnvironmentACLUpdateRequestBody{ + aclBody := &access.ACLUpdateRequestBody{ Delete: &aclDelete, Groups: []string{groupID}, Path: aclEntry[mkResourceVirtualEnvironmentGroupACLPath].(string), @@ -290,13 +291,13 @@ func groupDelete(ctx context.Context, d *schema.ResourceData, m interface{}) dia Roles: []string{aclEntry[mkResourceVirtualEnvironmentGroupACLRoleID].(string)}, } - err := veClient.UpdateACL(ctx, aclBody) + err = api.Access().UpdateACL(ctx, aclBody) if err != nil { return diag.FromErr(err) } } - err = veClient.DeleteGroup(ctx, groupID) + err = api.Access().DeleteGroup(ctx, groupID) if err != nil { if strings.Contains(err.Error(), "HTTP 404") { diff --git a/proxmoxtf/resource/group_test.go b/proxmoxtf/resource/group_test.go index fe5f17a7..2293ba46 100644 --- a/proxmoxtf/resource/group_test.go +++ b/proxmoxtf/resource/group_test.go @@ -17,8 +17,8 @@ import ( // TestGroupInstantiation tests whether the Group instance can be instantiated. func TestGroupInstantiation(t *testing.T) { t.Parallel() - s := Group() + s := Group() if s == nil { t.Fatalf("Cannot instantiate Group") } @@ -27,6 +27,7 @@ func TestGroupInstantiation(t *testing.T) { // TestGroupSchema tests the Group schema. func TestGroupSchema(t *testing.T) { t.Parallel() + s := Group() test.AssertRequiredArguments(t, s, []string{ diff --git a/proxmoxtf/resource/hosts.go b/proxmoxtf/resource/hosts.go index 7572812c..5e56f368 100644 --- a/proxmoxtf/resource/hosts.go +++ b/proxmoxtf/resource/hosts.go @@ -14,7 +14,7 @@ import ( "github.com/hashicorp/terraform-plugin-sdk/v2/diag" "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" - "github.com/bpg/terraform-provider-proxmox/proxmox" + "github.com/bpg/terraform-provider-proxmox/proxmox/nodes" "github.com/bpg/terraform-provider-proxmox/proxmoxtf" ) @@ -31,6 +31,7 @@ const ( mkResourceVirtualEnvironmentHostsNodeName = "node_name" ) +// Hosts returns a resource that manages hosts settings for a node. func Hosts() *schema.Resource { return &schema.Resource{ Schema: map[string]*schema.Schema{ @@ -126,13 +127,14 @@ func hostsRead(ctx context.Context, d *schema.ResourceData, m interface{}) diag. 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(mkResourceVirtualEnvironmentHostsNodeName).(string) - hosts, err := veClient.GetHosts(ctx, nodeName) + + hosts, err := api.Node(nodeName).GetHosts(ctx) if err != nil { return diag.FromErr(err) } @@ -194,7 +196,7 @@ func hostsRead(ctx context.Context, d *schema.ResourceData, m interface{}) diag. func hostsUpdate(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) } @@ -203,7 +205,7 @@ func hostsUpdate(ctx context.Context, d *schema.ResourceData, m interface{}) dia nodeName := d.Get(mkResourceVirtualEnvironmentHostsNodeName).(string) // Generate the data for the hosts file based on the specified entries. - body := proxmox.VirtualEnvironmentHostsUpdateRequestBody{ + body := nodes.HostsUpdateRequestBody{ Data: "", } @@ -223,7 +225,7 @@ func hostsUpdate(ctx context.Context, d *schema.ResourceData, m interface{}) dia body.Data += "\n" } - err = veClient.UpdateHosts(ctx, nodeName, &body) + err = api.Node(nodeName).UpdateHosts(ctx, &body) if err != nil { return diag.FromErr(err) } diff --git a/proxmoxtf/resource/hosts_test.go b/proxmoxtf/resource/hosts_test.go index e571a415..bc650bad 100644 --- a/proxmoxtf/resource/hosts_test.go +++ b/proxmoxtf/resource/hosts_test.go @@ -17,8 +17,8 @@ import ( // TestHostsInstantiation tests whether the Hosts instance can be instantiated. func TestHostsInstantiation(t *testing.T) { t.Parallel() - s := Hosts() + s := Hosts() if s == nil { t.Fatalf("Cannot instantiate Hosts") } @@ -27,6 +27,7 @@ func TestHostsInstantiation(t *testing.T) { // TestHostsSchema tests the Hosts schema. func TestHostsSchema(t *testing.T) { t.Parallel() + s := Hosts() test.AssertRequiredArguments(t, s, []string{ diff --git a/proxmoxtf/resource/pool.go b/proxmoxtf/resource/pool.go index 542ef9c7..a7147060 100644 --- a/proxmoxtf/resource/pool.go +++ b/proxmoxtf/resource/pool.go @@ -13,7 +13,7 @@ import ( "github.com/hashicorp/terraform-plugin-sdk/v2/diag" "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" - "github.com/bpg/terraform-provider-proxmox/proxmox" + "github.com/bpg/terraform-provider-proxmox/proxmox/pools" "github.com/bpg/terraform-provider-proxmox/proxmoxtf" ) @@ -30,6 +30,7 @@ const ( mkResourceVirtualEnvironmentPoolPoolID = "pool_id" ) +// Pool returns a resource that manages pools. func Pool() *schema.Resource { return &schema.Resource{ Schema: map[string]*schema.Schema{ @@ -89,7 +90,7 @@ func Pool() *schema.Resource { func poolCreate(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) } @@ -97,12 +98,12 @@ func poolCreate(ctx context.Context, d *schema.ResourceData, m interface{}) diag comment := d.Get(mkResourceVirtualEnvironmentPoolComment).(string) poolID := d.Get(mkResourceVirtualEnvironmentPoolPoolID).(string) - body := &proxmox.VirtualEnvironmentPoolCreateRequestBody{ + body := &pools.PoolCreateRequestBody{ Comment: &comment, ID: poolID, } - err = veClient.CreatePool(ctx, body) + err = api.Pool().CreatePool(ctx, body) if err != nil { return diag.FromErr(err) } @@ -116,13 +117,13 @@ func poolRead(ctx context.Context, d *schema.ResourceData, m interface{}) diag.D var diags diag.Diagnostics config := m.(proxmoxtf.ProviderConfiguration) - veClient, err := config.GetVEClient() + api, err := config.GetClient() if err != nil { return diag.FromErr(err) } poolID := d.Id() - pool, err := veClient.GetPool(ctx, poolID) + pool, err := api.Pool().GetPool(ctx, poolID) if err != nil { if strings.Contains(err.Error(), "HTTP 404") { d.SetId("") @@ -171,7 +172,7 @@ func poolRead(ctx context.Context, d *schema.ResourceData, m interface{}) diag.D func poolUpdate(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) } @@ -179,11 +180,11 @@ func poolUpdate(ctx context.Context, d *schema.ResourceData, m interface{}) diag comment := d.Get(mkResourceVirtualEnvironmentPoolComment).(string) poolID := d.Id() - body := &proxmox.VirtualEnvironmentPoolUpdateRequestBody{ + body := &pools.PoolUpdateRequestBody{ Comment: &comment, } - err = veClient.UpdatePool(ctx, poolID, body) + err = api.Pool().UpdatePool(ctx, poolID, body) if err != nil { return diag.FromErr(err) } @@ -193,13 +194,13 @@ func poolUpdate(ctx context.Context, d *schema.ResourceData, m interface{}) diag func poolDelete(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) } poolID := d.Id() - err = veClient.DeletePool(ctx, poolID) + err = api.Pool().DeletePool(ctx, poolID) if err != nil { if strings.Contains(err.Error(), "HTTP 404") { diff --git a/proxmoxtf/resource/pool_test.go b/proxmoxtf/resource/pool_test.go index 2d08775d..f952de5e 100644 --- a/proxmoxtf/resource/pool_test.go +++ b/proxmoxtf/resource/pool_test.go @@ -17,8 +17,8 @@ import ( // TestPoolInstantiation tests whether the Pool instance can be instantiated. func TestPoolInstantiation(t *testing.T) { t.Parallel() - s := Pool() + s := Pool() if s == nil { t.Fatalf("Cannot instantiate Pool") } @@ -27,6 +27,7 @@ func TestPoolInstantiation(t *testing.T) { // TestPoolSchema tests the Pool schema. func TestPoolSchema(t *testing.T) { t.Parallel() + s := Pool() test.AssertRequiredArguments(t, s, []string{ diff --git a/proxmoxtf/resource/role.go b/proxmoxtf/resource/role.go index 3f659423..0a4ec053 100644 --- a/proxmoxtf/resource/role.go +++ b/proxmoxtf/resource/role.go @@ -13,7 +13,7 @@ import ( "github.com/hashicorp/terraform-plugin-sdk/v2/diag" "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" - "github.com/bpg/terraform-provider-proxmox/proxmox" + "github.com/bpg/terraform-provider-proxmox/proxmox/access" "github.com/bpg/terraform-provider-proxmox/proxmox/types" "github.com/bpg/terraform-provider-proxmox/proxmoxtf" ) @@ -23,6 +23,7 @@ const ( mkResourceVirtualEnvironmentRoleRoleID = "role_id" ) +// Role returns a resource that manages roles. func Role() *schema.Resource { return &schema.Resource{ Schema: map[string]*schema.Schema{ @@ -48,7 +49,7 @@ func Role() *schema.Resource { func roleCreate(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) } @@ -61,12 +62,12 @@ func roleCreate(ctx context.Context, d *schema.ResourceData, m interface{}) diag customPrivileges[i] = v.(string) } - body := &proxmox.VirtualEnvironmentRoleCreateRequestBody{ + body := &access.RoleCreateRequestBody{ ID: roleID, Privileges: customPrivileges, } - err = veClient.CreateRole(ctx, body) + err = api.Access().CreateRole(ctx, body) if err != nil { return diag.FromErr(err) } @@ -78,13 +79,13 @@ func roleCreate(ctx context.Context, d *schema.ResourceData, m interface{}) diag func roleRead(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) } roleID := d.Id() - role, err := veClient.GetRole(ctx, roleID) + role, err := api.Access().GetRole(ctx, roleID) if err != nil { if strings.Contains(err.Error(), "HTTP 404") { d.SetId("") @@ -108,7 +109,7 @@ func roleRead(ctx context.Context, d *schema.ResourceData, m interface{}) diag.D func roleUpdate(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) } @@ -121,11 +122,11 @@ func roleUpdate(ctx context.Context, d *schema.ResourceData, m interface{}) diag customPrivileges[i] = v.(string) } - body := &proxmox.VirtualEnvironmentRoleUpdateRequestBody{ + body := &access.RoleUpdateRequestBody{ Privileges: customPrivileges, } - err = veClient.UpdateRole(ctx, roleID, body) + err = api.Access().UpdateRole(ctx, roleID, body) if err != nil { return diag.FromErr(err) } @@ -135,13 +136,13 @@ func roleUpdate(ctx context.Context, d *schema.ResourceData, m interface{}) diag func roleDelete(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) } roleID := d.Id() - err = veClient.DeleteRole(ctx, roleID) + err = api.Access().DeleteRole(ctx, roleID) if err != nil { if strings.Contains(err.Error(), "HTTP 404") { diff --git a/proxmoxtf/resource/role_test.go b/proxmoxtf/resource/role_test.go index 58ddf066..75b2016b 100644 --- a/proxmoxtf/resource/role_test.go +++ b/proxmoxtf/resource/role_test.go @@ -17,8 +17,8 @@ import ( // TestRoleInstantiation tests whether the Role instance can be instantiated. func TestRoleInstantiation(t *testing.T) { t.Parallel() - s := Role() + s := Role() if s == nil { t.Fatalf("Cannot instantiate Role") } @@ -27,6 +27,7 @@ func TestRoleInstantiation(t *testing.T) { // TestRoleSchema tests the Role schema. func TestRoleSchema(t *testing.T) { t.Parallel() + s := Role() test.AssertRequiredArguments(t, s, []string{ diff --git a/proxmoxtf/resource/time.go b/proxmoxtf/resource/time.go index 7c9e8df4..c4b469b5 100644 --- a/proxmoxtf/resource/time.go +++ b/proxmoxtf/resource/time.go @@ -14,7 +14,7 @@ import ( "github.com/hashicorp/terraform-plugin-sdk/v2/diag" "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" - "github.com/bpg/terraform-provider-proxmox/proxmox" + "github.com/bpg/terraform-provider-proxmox/proxmox/nodes" "github.com/bpg/terraform-provider-proxmox/proxmoxtf" ) @@ -25,6 +25,7 @@ const ( mkResourceVirtualEnvironmentTimeUTCTime = "utc_time" ) +// Time returns a resource that manages time settings for a node. func Time() *schema.Resource { return &schema.Resource{ Schema: map[string]*schema.Schema{ @@ -73,13 +74,13 @@ func timeRead(ctx context.Context, d *schema.ResourceData, m interface{}) diag.D 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(mkResourceVirtualEnvironmentTimeNodeName).(string) - nodeTime, err := veClient.GetNodeTime(ctx, nodeName) + nodeTime, err := api.Node(nodeName).GetTime(ctx) if err != nil { return diag.FromErr(err) } @@ -109,7 +110,7 @@ func timeRead(ctx context.Context, d *schema.ResourceData, m interface{}) diag.D func timeUpdate(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) } @@ -117,10 +118,9 @@ func timeUpdate(ctx context.Context, d *schema.ResourceData, m interface{}) diag nodeName := d.Get(mkResourceVirtualEnvironmentTimeNodeName).(string) timeZone := d.Get(mkResourceVirtualEnvironmentTimeTimeZone).(string) - err = veClient.UpdateNodeTime( + err = api.Node(nodeName).UpdateTime( ctx, - nodeName, - &proxmox.VirtualEnvironmentNodeUpdateTimeRequestBody{ + &nodes.UpdateTimeRequestBody{ TimeZone: timeZone, }, ) diff --git a/proxmoxtf/resource/time_test.go b/proxmoxtf/resource/time_test.go index 8db4e47f..6ee516b2 100644 --- a/proxmoxtf/resource/time_test.go +++ b/proxmoxtf/resource/time_test.go @@ -17,8 +17,8 @@ import ( // TestTimeInstantiation tests whether the Time instance can be instantiated. func TestTimeInstantiation(t *testing.T) { t.Parallel() - s := Time() + s := Time() if s == nil { t.Fatalf("Cannot instantiate Time") } @@ -27,6 +27,7 @@ func TestTimeInstantiation(t *testing.T) { // TestTimeSchema tests the Time schema. func TestTimeSchema(t *testing.T) { t.Parallel() + s := Time() test.AssertRequiredArguments(t, s, []string{ diff --git a/proxmoxtf/resource/user.go b/proxmoxtf/resource/user.go index da9346ba..78d3f891 100644 --- a/proxmoxtf/resource/user.go +++ b/proxmoxtf/resource/user.go @@ -15,7 +15,7 @@ import ( "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" "github.com/hashicorp/terraform-plugin-sdk/v2/helper/validation" - "github.com/bpg/terraform-provider-proxmox/proxmox" + "github.com/bpg/terraform-provider-proxmox/proxmox/access" "github.com/bpg/terraform-provider-proxmox/proxmox/types" "github.com/bpg/terraform-provider-proxmox/proxmoxtf" ) @@ -44,6 +44,7 @@ const ( mkResourceVirtualEnvironmentUserUserID = "user_id" ) +// User returns a resource that manages a user in the Proxmox VE access control list. func User() *schema.Resource { return &schema.Resource{ Schema: map[string]*schema.Schema{ @@ -148,7 +149,7 @@ func User() *schema.Resource { func userCreate(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) } @@ -178,7 +179,7 @@ func userCreate(ctx context.Context, d *schema.ResourceData, m interface{}) diag password := d.Get(mkResourceVirtualEnvironmentUserPassword).(string) userID := d.Get(mkResourceVirtualEnvironmentUserUserID).(string) - body := &proxmox.VirtualEnvironmentUserCreateRequestBody{ + body := &access.UserCreateRequestBody{ Comment: &comment, Email: &email, Enabled: &enabled, @@ -191,7 +192,7 @@ func userCreate(ctx context.Context, d *schema.ResourceData, m interface{}) diag Password: password, } - err = veClient.CreateUser(ctx, body) + err = api.Access().CreateUser(ctx, body) if err != nil { return diag.FromErr(err) } @@ -207,7 +208,7 @@ func userCreate(ctx context.Context, d *schema.ResourceData, m interface{}) diag aclEntry[mkResourceVirtualEnvironmentUserACLPropagate].(bool), ) - aclBody := &proxmox.VirtualEnvironmentACLUpdateRequestBody{ + aclBody := &access.ACLUpdateRequestBody{ Delete: &aclDelete, Path: aclEntry[mkResourceVirtualEnvironmentUserACLPath].(string), Propagate: &aclPropagate, @@ -215,7 +216,7 @@ func userCreate(ctx context.Context, d *schema.ResourceData, m interface{}) diag Users: []string{userID}, } - err := veClient.UpdateACL(ctx, aclBody) + err := api.Access().UpdateACL(ctx, aclBody) if err != nil { return diag.FromErr(err) } @@ -226,13 +227,13 @@ func userCreate(ctx context.Context, d *schema.ResourceData, m interface{}) diag func userRead(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) } userID := d.Id() - user, err := veClient.GetUser(ctx, userID) + user, err := api.Access().GetUser(ctx, userID) if err != nil { if strings.Contains(err.Error(), "HTTP 404") { d.SetId("") @@ -242,7 +243,7 @@ func userRead(ctx context.Context, d *schema.ResourceData, m interface{}) diag.D return diag.FromErr(err) } - acl, err := veClient.GetACL(ctx) + acl, err := api.Access().GetACL(ctx) if err != nil { return diag.FromErr(err) } @@ -340,7 +341,7 @@ func userRead(ctx context.Context, d *schema.ResourceData, m interface{}) diag.D func userUpdate(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) } @@ -368,7 +369,7 @@ func userUpdate(ctx context.Context, d *schema.ResourceData, m interface{}) diag keys := d.Get(mkResourceVirtualEnvironmentUserKeys).(string) lastName := d.Get(mkResourceVirtualEnvironmentUserLastName).(string) - body := &proxmox.VirtualEnvironmentUserUpdateRequestBody{ + body := &access.UserUpdateRequestBody{ Comment: &comment, Email: &email, Enabled: &enabled, @@ -380,14 +381,14 @@ func userUpdate(ctx context.Context, d *schema.ResourceData, m interface{}) diag } userID := d.Id() - err = veClient.UpdateUser(ctx, userID, body) + err = api.Access().UpdateUser(ctx, userID, body) if err != nil { return diag.FromErr(err) } if d.HasChange(mkResourceVirtualEnvironmentUserPassword) { password := d.Get(mkResourceVirtualEnvironmentUserPassword).(string) - err = veClient.ChangeUserPassword(ctx, userID, password) + err = api.Access().ChangeUserPassword(ctx, userID, password) if err != nil { return diag.FromErr(err) } @@ -403,7 +404,7 @@ func userUpdate(ctx context.Context, d *schema.ResourceData, m interface{}) diag aclEntry[mkResourceVirtualEnvironmentUserACLPropagate].(bool), ) - aclBody := &proxmox.VirtualEnvironmentACLUpdateRequestBody{ + aclBody := &access.ACLUpdateRequestBody{ Delete: &aclDelete, Path: aclEntry[mkResourceVirtualEnvironmentUserACLPath].(string), Propagate: &aclPropagate, @@ -411,7 +412,7 @@ func userUpdate(ctx context.Context, d *schema.ResourceData, m interface{}) diag Users: []string{userID}, } - err := veClient.UpdateACL(ctx, aclBody) + err := api.Access().UpdateACL(ctx, aclBody) if err != nil { return diag.FromErr(err) } @@ -426,7 +427,7 @@ func userUpdate(ctx context.Context, d *schema.ResourceData, m interface{}) diag aclEntry[mkResourceVirtualEnvironmentUserACLPropagate].(bool), ) - aclBody := &proxmox.VirtualEnvironmentACLUpdateRequestBody{ + aclBody := &access.ACLUpdateRequestBody{ Delete: &aclDelete, Path: aclEntry[mkResourceVirtualEnvironmentUserACLPath].(string), Propagate: &aclPropagate, @@ -434,7 +435,7 @@ func userUpdate(ctx context.Context, d *schema.ResourceData, m interface{}) diag Users: []string{userID}, } - err := veClient.UpdateACL(ctx, aclBody) + err := api.Access().UpdateACL(ctx, aclBody) if err != nil { return diag.FromErr(err) } @@ -445,7 +446,7 @@ func userUpdate(ctx context.Context, d *schema.ResourceData, m interface{}) diag func userDelete(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) } @@ -460,7 +461,7 @@ func userDelete(ctx context.Context, d *schema.ResourceData, m interface{}) diag aclEntry[mkResourceVirtualEnvironmentUserACLPropagate].(bool), ) - aclBody := &proxmox.VirtualEnvironmentACLUpdateRequestBody{ + aclBody := &access.ACLUpdateRequestBody{ Delete: &aclDelete, Path: aclEntry[mkResourceVirtualEnvironmentUserACLPath].(string), Propagate: &aclPropagate, @@ -468,13 +469,13 @@ func userDelete(ctx context.Context, d *schema.ResourceData, m interface{}) diag Users: []string{userID}, } - err := veClient.UpdateACL(ctx, aclBody) + err = api.Access().UpdateACL(ctx, aclBody) if err != nil { return diag.FromErr(err) } } - err = veClient.DeleteUser(ctx, userID) + err = api.Access().DeleteUser(ctx, userID) if err != nil { if strings.Contains(err.Error(), "HTTP 404") { diff --git a/proxmoxtf/resource/user_test.go b/proxmoxtf/resource/user_test.go index e951af00..f570d889 100644 --- a/proxmoxtf/resource/user_test.go +++ b/proxmoxtf/resource/user_test.go @@ -17,8 +17,8 @@ import ( // TestUserInstantiation tests whether the User instance can be instantiated. func TestUserInstantiation(t *testing.T) { t.Parallel() - s := User() + s := User() if s == nil { t.Fatalf("Cannot instantiate User") } @@ -27,6 +27,7 @@ func TestUserInstantiation(t *testing.T) { // TestUserSchema tests the User schema. func TestUserSchema(t *testing.T) { t.Parallel() + s := User() test.AssertRequiredArguments(t, s, []string{ diff --git a/proxmoxtf/resource/utils.go b/proxmoxtf/resource/utils.go index 179ba0c0..8cb97308 100644 --- a/proxmoxtf/resource/utils.go +++ b/proxmoxtf/resource/utils.go @@ -22,7 +22,7 @@ import ( "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" "github.com/hashicorp/terraform-plugin-sdk/v2/helper/validation" - "github.com/bpg/terraform-provider-proxmox/proxmox" + "github.com/bpg/terraform-provider-proxmox/proxmox/nodes/vms" ) func getBIOSValidator() schema.SchemaValidateDiagFunc { @@ -420,31 +420,37 @@ func suppressIfListsAreEqualIgnoringOrder(key, _, _ string, d *schema.ResourceDa if lastDotIndex != -1 { key = key[:lastDotIndex] } + oldData, newData := d.GetChange(key) if oldData == nil || newData == nil { return false } + oldArray := oldData.([]interface{}) newArray := newData.([]interface{}) + if len(oldArray) != len(newArray) { return false } oldEvents := make([]string, len(oldArray)) newEvents := make([]string, len(newArray)) + for i, oldEvt := range oldArray { oldEvents[i] = fmt.Sprint(oldEvt) } + for j, newEvt := range newArray { newEvents[j] = fmt.Sprint(newEvt) } sort.Strings(oldEvents) sort.Strings(newEvents) + return reflect.DeepEqual(oldEvents, newEvents) } -func getDiskInfo(vm *proxmox.VirtualEnvironmentVMGetResponseData, d *schema.ResourceData) map[string]*proxmox.CustomStorageDevice { +func getDiskInfo(resp *vms.GetResponseData, d *schema.ResourceData) map[string]*vms.CustomStorageDevice { currentDisk := d.Get(mkResourceVirtualEnvironmentVMDisk) currentDiskList := currentDisk.([]interface{}) @@ -457,50 +463,50 @@ func getDiskInfo(vm *proxmox.VirtualEnvironmentVMGetResponseData, d *schema.Reso currentDiskMap[diskInterface] = diskMap } - storageDevices := map[string]*proxmox.CustomStorageDevice{} + storageDevices := map[string]*vms.CustomStorageDevice{} - storageDevices["ide0"] = vm.IDEDevice0 - storageDevices["ide1"] = vm.IDEDevice1 - storageDevices["ide2"] = vm.IDEDevice2 + storageDevices["ide0"] = resp.IDEDevice0 + storageDevices["ide1"] = resp.IDEDevice1 + storageDevices["ide2"] = resp.IDEDevice2 - storageDevices["sata0"] = vm.SATADevice0 - storageDevices["sata1"] = vm.SATADevice1 - storageDevices["sata2"] = vm.SATADevice2 - storageDevices["sata3"] = vm.SATADevice3 - storageDevices["sata4"] = vm.SATADevice4 - storageDevices["sata5"] = vm.SATADevice5 + storageDevices["sata0"] = resp.SATADevice0 + storageDevices["sata1"] = resp.SATADevice1 + storageDevices["sata2"] = resp.SATADevice2 + storageDevices["sata3"] = resp.SATADevice3 + storageDevices["sata4"] = resp.SATADevice4 + storageDevices["sata5"] = resp.SATADevice5 - storageDevices["scsi0"] = vm.SCSIDevice0 - storageDevices["scsi1"] = vm.SCSIDevice1 - storageDevices["scsi2"] = vm.SCSIDevice2 - storageDevices["scsi3"] = vm.SCSIDevice3 - storageDevices["scsi4"] = vm.SCSIDevice4 - storageDevices["scsi5"] = vm.SCSIDevice5 - storageDevices["scsi6"] = vm.SCSIDevice6 - storageDevices["scsi7"] = vm.SCSIDevice7 - storageDevices["scsi8"] = vm.SCSIDevice8 - storageDevices["scsi9"] = vm.SCSIDevice9 - storageDevices["scsi10"] = vm.SCSIDevice10 - storageDevices["scsi11"] = vm.SCSIDevice11 - storageDevices["scsi12"] = vm.SCSIDevice12 - storageDevices["scsi13"] = vm.SCSIDevice13 + storageDevices["scsi0"] = resp.SCSIDevice0 + storageDevices["scsi1"] = resp.SCSIDevice1 + storageDevices["scsi2"] = resp.SCSIDevice2 + storageDevices["scsi3"] = resp.SCSIDevice3 + storageDevices["scsi4"] = resp.SCSIDevice4 + storageDevices["scsi5"] = resp.SCSIDevice5 + storageDevices["scsi6"] = resp.SCSIDevice6 + storageDevices["scsi7"] = resp.SCSIDevice7 + storageDevices["scsi8"] = resp.SCSIDevice8 + storageDevices["scsi9"] = resp.SCSIDevice9 + storageDevices["scsi10"] = resp.SCSIDevice10 + storageDevices["scsi11"] = resp.SCSIDevice11 + storageDevices["scsi12"] = resp.SCSIDevice12 + storageDevices["scsi13"] = resp.SCSIDevice13 - storageDevices["virtio0"] = vm.VirtualIODevice0 - storageDevices["virtio1"] = vm.VirtualIODevice1 - storageDevices["virtio2"] = vm.VirtualIODevice2 - storageDevices["virtio3"] = vm.VirtualIODevice3 - storageDevices["virtio4"] = vm.VirtualIODevice4 - storageDevices["virtio5"] = vm.VirtualIODevice5 - storageDevices["virtio6"] = vm.VirtualIODevice6 - storageDevices["virtio7"] = vm.VirtualIODevice7 - storageDevices["virtio8"] = vm.VirtualIODevice8 - storageDevices["virtio9"] = vm.VirtualIODevice9 - storageDevices["virtio10"] = vm.VirtualIODevice10 - storageDevices["virtio11"] = vm.VirtualIODevice11 - storageDevices["virtio12"] = vm.VirtualIODevice12 - storageDevices["virtio13"] = vm.VirtualIODevice13 - storageDevices["virtio14"] = vm.VirtualIODevice14 - storageDevices["virtio15"] = vm.VirtualIODevice15 + storageDevices["virtio0"] = resp.VirtualIODevice0 + storageDevices["virtio1"] = resp.VirtualIODevice1 + storageDevices["virtio2"] = resp.VirtualIODevice2 + storageDevices["virtio3"] = resp.VirtualIODevice3 + storageDevices["virtio4"] = resp.VirtualIODevice4 + storageDevices["virtio5"] = resp.VirtualIODevice5 + storageDevices["virtio6"] = resp.VirtualIODevice6 + storageDevices["virtio7"] = resp.VirtualIODevice7 + storageDevices["virtio8"] = resp.VirtualIODevice8 + storageDevices["virtio9"] = resp.VirtualIODevice9 + storageDevices["virtio10"] = resp.VirtualIODevice10 + storageDevices["virtio11"] = resp.VirtualIODevice11 + storageDevices["virtio12"] = resp.VirtualIODevice12 + storageDevices["virtio13"] = resp.VirtualIODevice13 + storageDevices["virtio14"] = resp.VirtualIODevice14 + storageDevices["virtio15"] = resp.VirtualIODevice15 for k, v := range storageDevices { if v != nil { @@ -519,7 +525,7 @@ func getDiskInfo(vm *proxmox.VirtualEnvironmentVMGetResponseData, d *schema.Reso } // getDiskDatastores returns a list of the used datastores in a VM -func getDiskDatastores(vm *proxmox.VirtualEnvironmentVMGetResponseData, d *schema.ResourceData) []string { +func getDiskDatastores(vm *vms.GetResponseData, d *schema.ResourceData) []string { storageDevices := getDiskInfo(vm, d) datastoresSet := map[string]int{} @@ -540,13 +546,13 @@ func getDiskDatastores(vm *proxmox.VirtualEnvironmentVMGetResponseData, d *schem return datastores } -func getPCIInfo(vm *proxmox.VirtualEnvironmentVMGetResponseData, d *schema.ResourceData) map[string]*proxmox.CustomPCIDevice { - pciDevices := map[string]*proxmox.CustomPCIDevice{} +func getPCIInfo(resp *vms.GetResponseData, _ *schema.ResourceData) map[string]*vms.CustomPCIDevice { + pciDevices := map[string]*vms.CustomPCIDevice{} - pciDevices["hostpci0"] = vm.PCIDevice0 - pciDevices["hostpci1"] = vm.PCIDevice1 - pciDevices["hostpci2"] = vm.PCIDevice2 - pciDevices["hostpci3"] = vm.PCIDevice3 + pciDevices["hostpci0"] = resp.PCIDevice0 + pciDevices["hostpci1"] = resp.PCIDevice1 + pciDevices["hostpci2"] = resp.PCIDevice2 + pciDevices["hostpci3"] = resp.PCIDevice3 return pciDevices } diff --git a/proxmoxtf/resource/utils_test.go b/proxmoxtf/resource/utils_test.go index 84e578ed..48f4dc12 100644 --- a/proxmoxtf/resource/utils_test.go +++ b/proxmoxtf/resource/utils_test.go @@ -12,6 +12,7 @@ import ( func Test_getCPUTypeValidator(t *testing.T) { t.Parallel() + tests := []struct { name string value string @@ -23,6 +24,7 @@ func Test_getCPUTypeValidator(t *testing.T) { {"valid", "qemu64", true}, {"valid", "custom-abc", true}, } + for _, tt := range tests { tt := tt t.Run(tt.name, func(t *testing.T) { diff --git a/proxmoxtf/resource/validator/firewall.go b/proxmoxtf/resource/validator/firewall.go index 251ea4d8..49bb3dfd 100644 --- a/proxmoxtf/resource/validator/firewall.go +++ b/proxmoxtf/resource/validator/firewall.go @@ -18,6 +18,7 @@ var ( ifaceExpression = regexp.MustCompile(`net\d+`) ) +// FirewallRate returns a schema validation function for a firewall rate. func FirewallRate() schema.SchemaValidateDiagFunc { return validation.ToDiagFunc(validation.StringMatch( rateExpression, @@ -25,6 +26,7 @@ func FirewallRate() schema.SchemaValidateDiagFunc { )) } +// FirewallIFace returns a schema validation function for a firewall iface. func FirewallIFace() schema.SchemaValidateDiagFunc { return validation.ToDiagFunc(validation.StringMatch( ifaceExpression, @@ -32,6 +34,7 @@ func FirewallIFace() schema.SchemaValidateDiagFunc { )) } +// FirewallPolicy returns a schema validation function for a firewall policy. func FirewallPolicy() schema.SchemaValidateDiagFunc { return validation.ToDiagFunc(validation.StringInSlice( []string{"ACCEPT", "REJECT", "DROP"}, @@ -39,6 +42,7 @@ func FirewallPolicy() schema.SchemaValidateDiagFunc { )) } +// FirewallLogLevel returns a schema validation function for a firewall log level. func FirewallLogLevel() schema.SchemaValidateDiagFunc { return validation.ToDiagFunc(validation.StringInSlice( []string{"emerg", "alert", "crit", "err", "warning", "notice", "info", "debug", "nolog"}, diff --git a/proxmoxtf/resource/vm.go b/proxmoxtf/resource/vm.go index b7f7a4da..570b1906 100644 --- a/proxmoxtf/resource/vm.go +++ b/proxmoxtf/resource/vm.go @@ -20,7 +20,7 @@ import ( "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" "github.com/hashicorp/terraform-plugin-sdk/v2/helper/validation" - "github.com/bpg/terraform-provider-proxmox/proxmox" + "github.com/bpg/terraform-provider-proxmox/proxmox/nodes/vms" "github.com/bpg/terraform-provider-proxmox/proxmox/types" "github.com/bpg/terraform-provider-proxmox/proxmoxtf" ) @@ -232,6 +232,7 @@ const ( vmCreateTimeoutSeconds = 10 ) +// VM returns a resource that manages VMs. func VM() *schema.Resource { return &schema.Resource{ Schema: map[string]*schema.Schema{ @@ -1268,9 +1269,10 @@ func vmCreate(ctx context.Context, d *schema.ResourceData, m interface{}) diag.D func vmCreateClone(ctx context.Context, d *schema.ResourceData, m interface{}) diag.Diagnostics { config := m.(proxmoxtf.ProviderConfiguration) - veClient, err := config.GetVEClient() - if err != nil { - return diag.FromErr(err) + + api, e := config.GetClient() + if e != nil { + return diag.FromErr(e) } clone := d.Get(mkResourceVirtualEnvironmentVMClone).([]interface{}) @@ -1289,7 +1291,7 @@ func vmCreateClone(ctx context.Context, d *schema.ResourceData, m interface{}) d vmID := d.Get(mkResourceVirtualEnvironmentVMVMID).(int) if vmID == -1 { - vmIDNew, err := veClient.GetVMID(ctx) + vmIDNew, err := api.Cluster().GetVMID(ctx) if err != nil { return diag.FromErr(err) } @@ -1298,7 +1300,7 @@ func vmCreateClone(ctx context.Context, d *schema.ResourceData, m interface{}) d fullCopy := types.CustomBool(cloneFull) - cloneBody := &proxmox.VirtualEnvironmentVMCloneRequestBody{ + cloneBody := &vms.CloneRequestBody{ FullCopy: &fullCopy, VMIDNew: vmID, } @@ -1323,7 +1325,7 @@ func vmCreateClone(ctx context.Context, d *schema.ResourceData, m interface{}) d if cloneNodeName != "" && cloneNodeName != nodeName { // Check if any used datastores of the source VM are not shared - vmConfig, err := veClient.GetVM(ctx, cloneNodeName, cloneVMID) + vmConfig, err := api.Node(cloneNodeName).VM(cloneVMID).GetVM(ctx) if err != nil { return diag.FromErr(err) } @@ -1332,13 +1334,12 @@ func vmCreateClone(ctx context.Context, d *schema.ResourceData, m interface{}) d onlySharedDatastores := true for _, datastore := range datastores { - datastoreStatus, err := veClient.GetDatastoreStatus(ctx, cloneNodeName, datastore) - if err != nil { - return diag.FromErr(err) + datastoreStatus, err2 := api.Node(cloneNodeName).GetDatastoreStatus(ctx, datastore) + if err2 != nil { + return diag.FromErr(err2) } - if datastoreStatus.Shared != nil && - *datastoreStatus.Shared == types.CustomBool(false) { + if datastoreStatus.Shared != nil && !*datastoreStatus.Shared { onlySharedDatastores = false break } @@ -1349,10 +1350,8 @@ func vmCreateClone(ctx context.Context, d *schema.ResourceData, m interface{}) d // all used datastores in the source VM are shared. Directly cloning to non-shared storage // on a different node is currently not supported by proxmox. cloneBody.TargetNodeName = &nodeName - err = veClient.CloneVM( + err = api.Node(cloneNodeName).VM(cloneVMID).CloneVM( ctx, - cloneNodeName, - cloneVMID, cloneRetries, cloneBody, cloneTimeout, @@ -1367,20 +1366,21 @@ func vmCreateClone(ctx context.Context, d *schema.ResourceData, m interface{}) d // https://forum.proxmox.com/threads/500-cant-clone-to-non-shared-storage-local.49078/#post-229727 // Temporarily clone to local node - err = veClient.CloneVM(ctx, cloneNodeName, cloneVMID, cloneRetries, cloneBody, cloneTimeout) + err = api.Node(cloneNodeName).VM(cloneVMID).CloneVM(ctx, cloneRetries, cloneBody, cloneTimeout) if err != nil { return diag.FromErr(err) } // Wait for the virtual machine to be created and its configuration lock to be released before migrating. - err = veClient.WaitForVMConfigUnlock(ctx, cloneNodeName, vmID, 600, 5, true) + + err = api.Node(cloneNodeName).VM(vmID).WaitForVMConfigUnlock(ctx, 600, 5, true) if err != nil { return diag.FromErr(err) } // Migrate to target node withLocalDisks := types.CustomBool(true) - migrateBody := &proxmox.VirtualEnvironmentVMMigrateRequestBody{ + migrateBody := &vms.MigrateRequestBody{ TargetNode: nodeName, WithLocalDisks: &withLocalDisks, } @@ -1389,24 +1389,27 @@ func vmCreateClone(ctx context.Context, d *schema.ResourceData, m interface{}) d migrateBody.TargetStorage = &cloneDatastoreID } - err = veClient.MigrateVM(ctx, cloneNodeName, vmID, migrateBody, cloneTimeout) + err = api.Node(cloneNodeName).VM(vmID).MigrateVM(ctx, migrateBody, cloneTimeout) if err != nil { return diag.FromErr(err) } } } else { - err = veClient.CloneVM(ctx, nodeName, cloneVMID, cloneRetries, cloneBody, cloneTimeout) + e = api.Node(nodeName).VM(cloneVMID).CloneVM(ctx, cloneRetries, cloneBody, cloneTimeout) } - if err != nil { - return diag.FromErr(err) + + if e != nil { + return diag.FromErr(e) } d.SetId(strconv.Itoa(vmID)) + vmAPI := api.Node(nodeName).VM(vmID) + // Wait for the virtual machine to be created and its configuration lock to be released. - err = veClient.WaitForVMConfigUnlock(ctx, nodeName, vmID, 600, 5, true) - if err != nil { - return diag.FromErr(err) + e = vmAPI.WaitForVMConfigUnlock(ctx, 600, 5, true) + if e != nil { + return diag.FromErr(e) } // Now that the virtual machine has been cloned, we need to perform some modifications. @@ -1431,11 +1434,11 @@ func vmCreateClone(ctx context.Context, d *schema.ResourceData, m interface{}) d template := types.CustomBool(d.Get(mkResourceVirtualEnvironmentVMTemplate).(bool)) vga := d.Get(mkResourceVirtualEnvironmentVMVGA).([]interface{}) - updateBody := &proxmox.VirtualEnvironmentVMUpdateRequestBody{ + updateBody := &vms.UpdateRequestBody{ AudioDevices: audioDevices, } - ideDevices := proxmox.CustomStorageDevices{} + ideDevices := vms.CustomStorageDevices{} var del []string @@ -1453,7 +1456,7 @@ func vmCreateClone(ctx context.Context, d *schema.ResourceData, m interface{}) d agentTrim := types.CustomBool(agentBlock[mkResourceVirtualEnvironmentVMAgentTrim].(bool)) agentType := agentBlock[mkResourceVirtualEnvironmentVMAgentType].(string) - updateBody.Agent = &proxmox.CustomAgent{ + updateBody.Agent = &vms.CustomAgent{ Enabled: &agentEnabled, TrimClonedDisks: &agentTrim, Type: &agentType, @@ -1473,17 +1476,17 @@ func vmCreateClone(ctx context.Context, d *schema.ResourceData, m interface{}) d } if len(cdrom) > 0 || len(initialization) > 0 { - ideDevices = proxmox.CustomStorageDevices{ - "ide0": proxmox.CustomStorageDevice{ + ideDevices = vms.CustomStorageDevices{ + "ide0": vms.CustomStorageDevice{ Enabled: false, }, - "ide1": proxmox.CustomStorageDevice{ + "ide1": vms.CustomStorageDevice{ Enabled: false, }, - "ide2": proxmox.CustomStorageDevice{ + "ide2": vms.CustomStorageDevice{ Enabled: false, }, - "ide3": proxmox.CustomStorageDevice{ + "ide3": vms.CustomStorageDevice{ Enabled: false, }, } @@ -1501,8 +1504,8 @@ func vmCreateClone(ctx context.Context, d *schema.ResourceData, m interface{}) d cdromMedia := "cdrom" - ideDevices = proxmox.CustomStorageDevices{ - "ide3": proxmox.CustomStorageDevice{ + ideDevices = vms.CustomStorageDevices{ + "ide3": vms.CustomStorageDevice{ Enabled: cdromEnabled, FileVolume: cdromFileID, Media: &cdromMedia, @@ -1528,13 +1531,13 @@ func vmCreateClone(ctx context.Context, d *schema.ResourceData, m interface{}) d } // Only the root account is allowed to change the CPU architecture, which makes this check necessary. - if veClient.Username == proxmox.DefaultRootAccount || + if api.API().IsRoot() || cpuArchitecture != dvResourceVirtualEnvironmentVMCPUArchitecture { updateBody.CPUArchitecture = &cpuArchitecture } updateBody.CPUCores = &cpuCores - updateBody.CPUEmulation = &proxmox.CustomCPUEmulation{ + updateBody.CPUEmulation = &vms.CustomCPUEmulation{ Flags: &cpuFlagsConverted, Type: cpuType, } @@ -1554,22 +1557,22 @@ func vmCreateClone(ctx context.Context, d *schema.ResourceData, m interface{}) d cdromCloudInitFileID := fmt.Sprintf("%s:cloudinit", initializationDatastoreID) cdromCloudInitMedia := "cdrom" - ideDevices = proxmox.CustomStorageDevices{ - "ide2": proxmox.CustomStorageDevice{ + ideDevices = vms.CustomStorageDevices{ + "ide2": vms.CustomStorageDevice{ Enabled: cdromCloudInitEnabled, FileVolume: cdromCloudInitFileID, Media: &cdromCloudInitMedia, }, } - ciUpdateBody := &proxmox.VirtualEnvironmentVMUpdateRequestBody{} + ciUpdateBody := &vms.UpdateRequestBody{} ciUpdateBody.Delete = append(ciUpdateBody.Delete, "ide2") - err = veClient.UpdateVM(ctx, nodeName, vmID, ciUpdateBody) - if err != nil { - return diag.FromErr(err) - } - initializationConfig := vmGetCloudInitConfig(d) - updateBody.CloudInitConfig = initializationConfig + e = vmAPI.UpdateVM(ctx, ciUpdateBody) + if e != nil { + return diag.FromErr(e) + } + + updateBody.CloudInitConfig = vmGetCloudInitConfig(d) } if len(hostPCI) > 0 { @@ -1597,7 +1600,7 @@ func vmCreateClone(ctx context.Context, d *schema.ResourceData, m interface{}) d if memoryShared > 0 { memorySharedName := fmt.Sprintf("vm-%d-ivshmem", vmID) - updateBody.SharedMemory = &proxmox.CustomSharedMemory{ + updateBody.SharedMemory = &vms.CustomSharedMemory{ Name: &memorySharedName, Size: memoryShared, } @@ -1661,28 +1664,30 @@ func vmCreateClone(ctx context.Context, d *schema.ResourceData, m interface{}) d updateBody.Delete = del - err = veClient.UpdateVM(ctx, nodeName, vmID, updateBody) - if err != nil { - return diag.FromErr(err) + e = vmAPI.UpdateVM(ctx, updateBody) + if e != nil { + return diag.FromErr(e) } disk := d.Get(mkResourceVirtualEnvironmentVMDisk).([]interface{}) - vmConfig, err := veClient.GetVM(ctx, nodeName, vmID) - if err != nil { - if strings.Contains(err.Error(), "HTTP 404") || - (strings.Contains(err.Error(), "HTTP 500") && strings.Contains(err.Error(), "does not exist")) { + vmConfig, e := vmAPI.GetVM(ctx) + if e != nil { + if strings.Contains(e.Error(), "HTTP 404") || + (strings.Contains(e.Error(), "HTTP 500") && strings.Contains(e.Error(), "does not exist")) { d.SetId("") return nil } - return diag.FromErr(err) + + return diag.FromErr(e) } allDiskInfo := getDiskInfo(vmConfig, d) - diskDeviceObjects, err := vmGetDiskDeviceObjects(d, nil) - if err != nil { - return diag.FromErr(err) + + diskDeviceObjects, e := vmGetDiskDeviceObjects(d, nil) + if e != nil { + return diag.FromErr(e) } for i := range disk { @@ -1694,30 +1699,30 @@ func vmCreateClone(ctx context.Context, d *schema.ResourceData, m interface{}) d currentDiskInfo := allDiskInfo[diskInterface] if currentDiskInfo == nil { - diskUpdateBody := &proxmox.VirtualEnvironmentVMUpdateRequestBody{} + diskUpdateBody := &vms.UpdateRequestBody{} prefix := diskDigitPrefix(diskInterface) switch prefix { case "virtio": if diskUpdateBody.VirtualIODevices == nil { - diskUpdateBody.VirtualIODevices = proxmox.CustomStorageDevices{} + diskUpdateBody.VirtualIODevices = vms.CustomStorageDevices{} } diskUpdateBody.VirtualIODevices[diskInterface] = diskDeviceObjects[prefix][diskInterface] case "sata": if diskUpdateBody.SATADevices == nil { - diskUpdateBody.SATADevices = proxmox.CustomStorageDevices{} + diskUpdateBody.SATADevices = vms.CustomStorageDevices{} } diskUpdateBody.SATADevices[diskInterface] = diskDeviceObjects[prefix][diskInterface] case "scsi": if diskUpdateBody.SCSIDevices == nil { - diskUpdateBody.SCSIDevices = proxmox.CustomStorageDevices{} + diskUpdateBody.SCSIDevices = vms.CustomStorageDevices{} } diskUpdateBody.SCSIDevices[diskInterface] = diskDeviceObjects[prefix][diskInterface] } - err = veClient.UpdateVM(ctx, nodeName, vmID, diskUpdateBody) - if err != nil { - return diag.FromErr(err) + e = vmAPI.UpdateVM(ctx, diskUpdateBody) + if e != nil { + return diag.FromErr(e) } continue @@ -1733,13 +1738,13 @@ func vmCreateClone(ctx context.Context, d *schema.ResourceData, m interface{}) d deleteOriginalDisk := types.CustomBool(true) - diskMoveBody := &proxmox.VirtualEnvironmentVMMoveDiskRequestBody{ + diskMoveBody := &vms.MoveDiskRequestBody{ DeleteOriginalDisk: &deleteOriginalDisk, Disk: diskInterface, TargetStorage: dataStoreID, } - diskResizeBody := &proxmox.VirtualEnvironmentVMResizeDiskRequestBody{ + diskResizeBody := &vms.ResizeDiskRequestBody{ Disk: diskInterface, Size: types.DiskSizeFromGigabytes(diskSize), } @@ -1757,16 +1762,17 @@ func vmCreateClone(ctx context.Context, d *schema.ResourceData, m interface{}) d if moveDisk { moveDiskTimeout := d.Get(mkResourceVirtualEnvironmentVMTimeoutMoveDisk).(int) - err = veClient.MoveVMDisk(ctx, nodeName, vmID, diskMoveBody, moveDiskTimeout) - if err != nil { - return diag.FromErr(err) + + e = vmAPI.MoveVMDisk(ctx, diskMoveBody, moveDiskTimeout) + if e != nil { + return diag.FromErr(e) } } if diskSize > currentDiskInfo.Size.InGigabytes() { - err = veClient.ResizeVMDisk(ctx, nodeName, vmID, diskResizeBody) - if err != nil { - return diag.FromErr(err) + e = vmAPI.ResizeVMDisk(ctx, diskResizeBody) + if e != nil { + return diag.FromErr(e) } } } @@ -1776,7 +1782,7 @@ func vmCreateClone(ctx context.Context, d *schema.ResourceData, m interface{}) d func vmCreateCustom(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) } @@ -1925,32 +1931,36 @@ func vmCreateCustom(ctx context.Context, d *schema.ResourceData, m interface{}) vmID := d.Get(mkResourceVirtualEnvironmentVMVMID).(int) if vmID == -1 { - vmIDNew, err := veClient.GetVMID(ctx) - if err != nil { - return diag.FromErr(err) + vmIDNew, e := api.Cluster().GetVMID(ctx) + if e != nil { + return diag.FromErr(e) } vmID = *vmIDNew } - var memorySharedObject *proxmox.CustomSharedMemory + var memorySharedObject *vms.CustomSharedMemory var bootOrderConverted []string if cdromEnabled { bootOrderConverted = []string{"ide3"} } + bootOrder := d.Get(mkResourceVirtualEnvironmentVMBootOrder).([]interface{}) //nolint:nestif if len(bootOrder) == 0 { if sataDeviceObjects != nil { bootOrderConverted = append(bootOrderConverted, "sata0") } + if scsiDeviceObjects != nil { bootOrderConverted = append(bootOrderConverted, "scsi0") } + if virtioDeviceObjects != nil { bootOrderConverted = append(bootOrderConverted, "virtio0") } + if networkDeviceObjects != nil { bootOrderConverted = append(bootOrderConverted, "net0") } @@ -1967,13 +1977,13 @@ func vmCreateCustom(ctx context.Context, d *schema.ResourceData, m interface{}) } ideDevice2Media := "cdrom" - ideDevices := proxmox.CustomStorageDevices{ - "ide2": proxmox.CustomStorageDevice{ + ideDevices := vms.CustomStorageDevices{ + "ide2": vms.CustomStorageDevice{ Enabled: cdromCloudInitEnabled, FileVolume: cdromCloudInitFileID, Media: &ideDevice2Media, }, - "ide3": proxmox.CustomStorageDevice{ + "ide3": vms.CustomStorageDevice{ Enabled: cdromEnabled, FileVolume: cdromFileID, Media: &ideDevice2Media, @@ -1982,7 +1992,7 @@ func vmCreateCustom(ctx context.Context, d *schema.ResourceData, m interface{}) if memoryShared > 0 { memorySharedName := fmt.Sprintf("vm-%d-ivshmem", vmID) - memorySharedObject = &proxmox.CustomSharedMemory{ + memorySharedObject = &vms.CustomSharedMemory{ Name: &memorySharedName, Size: memoryShared, } @@ -1990,21 +2000,21 @@ func vmCreateCustom(ctx context.Context, d *schema.ResourceData, m interface{}) scsiHardware := d.Get(mkResourceVirtualEnvironmentVMSCSIHardware).(string) - createBody := &proxmox.VirtualEnvironmentVMCreateRequestBody{ + createBody := &vms.CreateRequestBody{ ACPI: &acpi, - Agent: &proxmox.CustomAgent{ + Agent: &vms.CustomAgent{ Enabled: &agentEnabled, TrimClonedDisks: &agentTrim, Type: &agentType, }, AudioDevices: audioDevices, BIOS: &bios, - Boot: &proxmox.CustomBoot{ + Boot: &vms.CustomBoot{ Order: &bootOrderConverted, }, CloudInitConfig: initializationConfig, CPUCores: &cpuCores, - CPUEmulation: &proxmox.CustomCPUEmulation{ + CPUEmulation: &vms.CustomCPUEmulation{ Flags: &cpuFlagsConverted, Type: cpuType, }, @@ -2040,7 +2050,7 @@ func vmCreateCustom(ctx context.Context, d *schema.ResourceData, m interface{}) } // Only the root account is allowed to change the CPU architecture, which makes this check necessary. - if veClient.Username == proxmox.DefaultRootAccount || + if api.API().IsRoot() || cpuArchitecture != dvResourceVirtualEnvironmentVMCPUArchitecture { createBody.CPUArchitecture = &cpuArchitecture } @@ -2074,7 +2084,7 @@ func vmCreateCustom(ctx context.Context, d *schema.ResourceData, m interface{}) createBody.PoolID = &poolID } - err = veClient.CreateVM(ctx, nodeName, createBody, vmCreateTimeoutSeconds) + err = api.Node(nodeName).VM(0).CreateVM(ctx, createBody, vmCreateTimeoutSeconds) if err != nil { return diag.FromErr(err) } @@ -2085,13 +2095,6 @@ func vmCreateCustom(ctx context.Context, d *schema.ResourceData, m interface{}) } func vmCreateCustomDisks(ctx context.Context, d *schema.ResourceData, m interface{}) diag.Diagnostics { - config := m.(proxmoxtf.ProviderConfiguration) - veClient, err := config.GetVEClient() - if err != nil { - return diag.FromErr(err) - } - - nodeName := d.Get(mkResourceVirtualEnvironmentVMNodeName).(string) vmID, err := strconv.Atoi(d.Id()) if err != nil { return diag.FromErr(err) @@ -2221,7 +2224,21 @@ func vmCreateCustomDisks(ctx context.Context, d *schema.ResourceData, m interfac // Execute the commands on the node and wait for the result. // This is a highly experimental approach to disk imports and is not recommended by Proxmox. if len(commands) > 0 { - err = veClient.ExecuteNodeCommands(ctx, nodeName, commands) + config := m.(proxmoxtf.ProviderConfiguration) + + api, err := config.GetClient() + if err != nil { + return diag.FromErr(err) + } + + nodeName := d.Get(mkResourceVirtualEnvironmentVMNodeName).(string) + + nodeAddress, err := api.Node(nodeName).GetIP(ctx) + if err != nil { + return diag.FromErr(err) + } + + err = api.SSH().ExecuteNodeCommands(ctx, nodeAddress, commands) if err != nil { return diag.FromErr(err) } @@ -2240,7 +2257,7 @@ func vmCreateStart(ctx context.Context, d *schema.ResourceData, m interface{}) d } config := m.(proxmoxtf.ProviderConfiguration) - veClient, err := config.GetVEClient() + api, err := config.GetClient() if err != nil { return diag.FromErr(err) } @@ -2251,9 +2268,11 @@ func vmCreateStart(ctx context.Context, d *schema.ResourceData, m interface{}) d return diag.FromErr(err) } + vmAPI := api.Node(nodeName).VM(vmID) + // Start the virtual machine and wait for it to reach a running state before continuing. startVMTimeout := d.Get(mkResourceVirtualEnvironmentVMTimeoutStartVM).(int) - err = veClient.StartVM(ctx, nodeName, vmID, startVMTimeout) + err = vmAPI.StartVM(ctx, startVMTimeout) if err != nil { return diag.FromErr(err) } @@ -2261,11 +2280,9 @@ func vmCreateStart(ctx context.Context, d *schema.ResourceData, m interface{}) d if reboot { rebootTimeout := d.Get(mkResourceVirtualEnvironmentVMTimeoutReboot).(int) - err := veClient.RebootVM( + err := vmAPI.RebootVM( ctx, - nodeName, - vmID, - &proxmox.VirtualEnvironmentVMRebootRequestBody{ + &vms.RebootRequestBody{ Timeout: &rebootTimeout, }, rebootTimeout+30, @@ -2278,9 +2295,9 @@ func vmCreateStart(ctx context.Context, d *schema.ResourceData, m interface{}) d return vmRead(ctx, d, m) } -func vmGetAudioDeviceList(d *schema.ResourceData) proxmox.CustomAudioDevices { +func vmGetAudioDeviceList(d *schema.ResourceData) vms.CustomAudioDevices { devices := d.Get(mkResourceVirtualEnvironmentVMAudioDevice).([]interface{}) - list := make(proxmox.CustomAudioDevices, len(devices)) + list := make(vms.CustomAudioDevices, len(devices)) for i, v := range devices { block := v.(map[string]interface{}) @@ -2311,14 +2328,14 @@ func vmGetAudioDriverValidator() schema.SchemaValidateDiagFunc { }, false)) } -func vmGetCloudInitConfig(d *schema.ResourceData) *proxmox.CustomCloudInitConfig { - var initializationConfig *proxmox.CustomCloudInitConfig +func vmGetCloudInitConfig(d *schema.ResourceData) *vms.CustomCloudInitConfig { + var initializationConfig *vms.CustomCloudInitConfig initialization := d.Get(mkResourceVirtualEnvironmentVMInitialization).([]interface{}) if len(initialization) > 0 { initializationBlock := initialization[0].(map[string]interface{}) - initializationConfig = &proxmox.CustomCloudInitConfig{} + initializationConfig = &vms.CustomCloudInitConfig{} initializationDNS := initializationBlock[mkResourceVirtualEnvironmentVMInitializationDNS].([]interface{}) if len(initializationDNS) > 0 { @@ -2338,7 +2355,7 @@ func vmGetCloudInitConfig(d *schema.ResourceData) *proxmox.CustomCloudInitConfig initializationIPConfig := initializationBlock[mkResourceVirtualEnvironmentVMInitializationIPConfig].([]interface{}) initializationConfig.IPConfig = make( - []proxmox.CustomCloudInitIPConfig, + []vms.CustomCloudInitIPConfig, len(initializationIPConfig), ) @@ -2386,7 +2403,7 @@ func vmGetCloudInitConfig(d *schema.ResourceData) *proxmox.CustomCloudInitConfig keys := initializationUserAccountBlock[mkResourceVirtualEnvironmentVMInitializationUserAccountKeys].([]interface{}) if len(keys) > 0 { - sshKeys := make(proxmox.CustomCloudInitSSHKeys, len(keys)) + sshKeys := make(vms.CustomCloudInitSSHKeys, len(keys)) for i, k := range keys { sshKeys[i] = k.(string) @@ -2409,7 +2426,7 @@ func vmGetCloudInitConfig(d *schema.ResourceData) *proxmox.CustomCloudInitConfig initializationUserDataFileID := initializationBlock[mkResourceVirtualEnvironmentVMInitializationUserDataFileID].(string) if initializationUserDataFileID != "" { - initializationConfig.Files = &proxmox.CustomCloudInitFiles{ + initializationConfig.Files = &vms.CustomCloudInitFiles{ UserVolume: &initializationUserDataFileID, } } @@ -2418,7 +2435,7 @@ func vmGetCloudInitConfig(d *schema.ResourceData) *proxmox.CustomCloudInitConfig if initializationVendorDataFileID != "" { if initializationConfig.Files == nil { - initializationConfig.Files = &proxmox.CustomCloudInitFiles{} + initializationConfig.Files = &vms.CustomCloudInitFiles{} } initializationConfig.Files.VendorVolume = &initializationVendorDataFileID } @@ -2427,7 +2444,7 @@ func vmGetCloudInitConfig(d *schema.ResourceData) *proxmox.CustomCloudInitConfig if initializationNetworkDataFileID != "" { if initializationConfig.Files == nil { - initializationConfig.Files = &proxmox.CustomCloudInitFiles{} + initializationConfig.Files = &vms.CustomCloudInitFiles{} } initializationConfig.Files.NetworkVolume = &initializationNetworkDataFileID } @@ -2452,7 +2469,7 @@ func vmGetCPUArchitectureValidator() schema.SchemaValidateDiagFunc { func vmGetDiskDeviceObjects( d *schema.ResourceData, disks []interface{}, -) (map[string]map[string]proxmox.CustomStorageDevice, error) { +) (map[string]map[string]vms.CustomStorageDevice, error) { var diskDevice []interface{} if disks != nil { @@ -2461,11 +2478,11 @@ func vmGetDiskDeviceObjects( diskDevice = d.Get(mkResourceVirtualEnvironmentVMDisk).([]interface{}) } - diskDeviceObjects := map[string]map[string]proxmox.CustomStorageDevice{} + diskDeviceObjects := map[string]map[string]vms.CustomStorageDevice{} resource := VM() for _, diskEntry := range diskDevice { - diskDevice := proxmox.CustomStorageDevice{ + diskDevice := vms.CustomStorageDevice{ Enabled: true, } @@ -2548,7 +2565,7 @@ func vmGetDiskDeviceObjects( } if _, present := diskDeviceObjects[baseDiskInterface]; !present { - diskDeviceObjects[baseDiskInterface] = map[string]proxmox.CustomStorageDevice{} + diskDeviceObjects[baseDiskInterface] = map[string]vms.CustomStorageDevice{} } diskDeviceObjects[baseDiskInterface][diskInterface] = diskDevice @@ -2557,9 +2574,9 @@ func vmGetDiskDeviceObjects( return diskDeviceObjects, nil } -func vmGetHostPCIDeviceObjects(d *schema.ResourceData) proxmox.CustomPCIDevices { +func vmGetHostPCIDeviceObjects(d *schema.ResourceData) vms.CustomPCIDevices { pciDevice := d.Get(mkResourceVirtualEnvironmentVMHostPCI).([]interface{}) - pciDeviceObjects := make(proxmox.CustomPCIDevices, len(pciDevice)) + pciDeviceObjects := make(vms.CustomPCIDevices, len(pciDevice)) for i, pciDeviceEntry := range pciDevice { block := pciDeviceEntry.(map[string]interface{}) @@ -2573,7 +2590,7 @@ func vmGetHostPCIDeviceObjects(d *schema.ResourceData) proxmox.CustomPCIDevices romfile, _ := block[mkResourceVirtualEnvironmentVMHostPCIDeviceROMFile].(string) xvga := types.CustomBool(block[mkResourceVirtualEnvironmentVMHostPCIDeviceXVGA].(bool)) - device := proxmox.CustomPCIDevice{ + device := vms.CustomPCIDevice{ DeviceIDs: strings.Split(ids, ";"), PCIExpress: &pcie, ROMBAR: &rombar, @@ -2597,9 +2614,9 @@ func vmGetHostPCIDeviceObjects(d *schema.ResourceData) proxmox.CustomPCIDevices return pciDeviceObjects } -func vmGetNetworkDeviceObjects(d *schema.ResourceData) proxmox.CustomNetworkDevices { +func vmGetNetworkDeviceObjects(d *schema.ResourceData) vms.CustomNetworkDevices { networkDevice := d.Get(mkResourceVirtualEnvironmentVMNetworkDevice).([]interface{}) - networkDeviceObjects := make(proxmox.CustomNetworkDevices, len(networkDevice)) + networkDeviceObjects := make(vms.CustomNetworkDevices, len(networkDevice)) for i, networkDeviceEntry := range networkDevice { block := networkDeviceEntry.(map[string]interface{}) @@ -2613,7 +2630,7 @@ func vmGetNetworkDeviceObjects(d *schema.ResourceData) proxmox.CustomNetworkDevi vlanID := block[mkResourceVirtualEnvironmentVMNetworkDeviceVLANID].(int) mtu := block[mkResourceVirtualEnvironmentVMNetworkDeviceMTU].(int) - device := proxmox.CustomNetworkDevice{ + device := vms.CustomNetworkDevice{ Enabled: enabled, Firewall: &firewall, Model: model, @@ -2662,9 +2679,9 @@ func vmGetOperatingSystemTypeValidator() schema.SchemaValidateDiagFunc { }, false)) } -func vmGetSerialDeviceList(d *schema.ResourceData) proxmox.CustomSerialDevices { +func vmGetSerialDeviceList(d *schema.ResourceData) vms.CustomSerialDevices { device := d.Get(mkResourceVirtualEnvironmentVMSerialDevice).([]interface{}) - list := make(proxmox.CustomSerialDevices, len(device)) + list := make(vms.CustomSerialDevices, len(device)) for i, v := range device { block := v.(map[string]interface{}) @@ -2708,7 +2725,7 @@ func vmGetSerialDeviceValidator() schema.SchemaValidateDiagFunc { }) } -func vmGetVGADeviceObject(d *schema.ResourceData) (*proxmox.CustomVGADevice, error) { +func vmGetVGADeviceObject(d *schema.ResourceData) (*vms.CustomVGADevice, error) { resource := VM() vgaBlock, err := getSchemaBlock( @@ -2726,7 +2743,7 @@ func vmGetVGADeviceObject(d *schema.ResourceData) (*proxmox.CustomVGADevice, err vgaMemory := vgaBlock[mkResourceVirtualEnvironmentVMVGAMemory].(int) vgaType := vgaBlock[mkResourceVirtualEnvironmentVMVGAType].(string) - vgaDevice := &proxmox.CustomVGADevice{} + vgaDevice := &vms.CustomVGADevice{} if vgaEnabled { if vgaMemory > 0 { @@ -2737,7 +2754,7 @@ func vmGetVGADeviceObject(d *schema.ResourceData) (*proxmox.CustomVGADevice, err } else { vgaType = "none" - vgaDevice = &proxmox.CustomVGADevice{ + vgaDevice = &vms.CustomVGADevice{ Type: &vgaType, } } @@ -2747,7 +2764,7 @@ func vmGetVGADeviceObject(d *schema.ResourceData) (*proxmox.CustomVGADevice, err func vmRead(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) } @@ -2758,8 +2775,10 @@ func vmRead(ctx context.Context, d *schema.ResourceData, m interface{}) diag.Dia return diag.FromErr(err) } + vmAPI := api.Node(nodeName).VM(vmID) + // Retrieve the entire configuration in order to compare it to the state. - vmConfig, err := veClient.GetVM(ctx, nodeName, vmID) + vmConfig, err := vmAPI.GetVM(ctx) if err != nil { if strings.Contains(err.Error(), "HTTP 404") || (strings.Contains(err.Error(), "HTTP 500") && strings.Contains(err.Error(), "does not exist")) { @@ -2771,7 +2790,7 @@ func vmRead(ctx context.Context, d *schema.ResourceData, m interface{}) diag.Dia return diag.FromErr(err) } - vmStatus, err := veClient.GetVMStatus(ctx, nodeName, vmID) + vmStatus, err := vmAPI.GetVMStatus(ctx) if err != nil { return diag.FromErr(err) } @@ -2784,11 +2803,11 @@ func vmReadCustom( d *schema.ResourceData, m interface{}, vmID int, - vmConfig *proxmox.VirtualEnvironmentVMGetResponseData, - vmStatus *proxmox.VirtualEnvironmentVMGetStatusResponseData, + vmConfig *vms.GetResponseData, + vmStatus *vms.GetStatusResponseData, ) diag.Diagnostics { config := m.(proxmoxtf.ProviderConfiguration) - veClient, err := config.GetVEClient() + api, err := config.GetClient() if err != nil { return diag.FromErr(err) } @@ -2869,7 +2888,7 @@ func vmReadCustom( currentAudioDevice := d.Get(mkResourceVirtualEnvironmentVMAudioDevice).([]interface{}) audioDevices := make([]interface{}, 1) - audioDevicesArray := []*proxmox.CustomAudioDevice{ + audioDevicesArray := []*vms.CustomAudioDevice{ vmConfig.AudioDevice, } audioDevicesCount := 0 @@ -2903,7 +2922,7 @@ func vmReadCustom( diags = append(diags, diag.FromErr(err)...) } - // Compare the IDE devices to the CDROM and cloud-init configurations stored in the state. + // Compare the IDE devices to the CD-ROM and cloud-init configurations stored in the state. if vmConfig.IDEDevice3 != nil { cdrom := make([]interface{}, 1) cdromBlock := map[string]interface{}{} @@ -2944,7 +2963,7 @@ func vmReadCustom( } else { // Default value of "arch" is "" according to the API documentation. // However, assume the provider's default value as a workaround when the root account is not being used. - if veClient.Username != proxmox.DefaultRootAccount { + if !api.API().IsRoot() { cpu[mkResourceVirtualEnvironmentVMCPUArchitecture] = dvResourceVirtualEnvironmentVMCPUArchitecture } else { cpu[mkResourceVirtualEnvironmentVMCPUArchitecture] = "" @@ -3038,11 +3057,12 @@ func vmReadCustom( disk[mkResourceVirtualEnvironmentVMDiskFileFormat] = dvResourceVirtualEnvironmentVMDiskFileFormat // disk format may not be returned by config API if it is default for the storage, and that may be different // from the default qcow2, so we need to read it from the storage API to make sure we have the correct value - files, err := veClient.ListDatastoreFiles(ctx, nodeName, fileIDParts[0]) + files, err := api.Node(nodeName).ListDatastoreFiles(ctx, fileIDParts[0]) if err != nil { diags = append(diags, diag.FromErr(err)...) continue } + for _, v := range files { if v.VolumeID == dd.FileVolume { disk[mkResourceVirtualEnvironmentVMDiskFileFormat] = v.FileFormat @@ -3243,7 +3263,7 @@ func vmReadCustom( } ipConfigLast := -1 - ipConfigObjects := []*proxmox.CustomCloudInitIPConfig{ + ipConfigObjects := []*vms.CustomCloudInitIPConfig{ vmConfig.IPConfig0, vmConfig.IPConfig1, vmConfig.IPConfig2, @@ -3448,7 +3468,7 @@ func vmReadCustom( macAddresses := make([]interface{}, 8) networkDeviceLast := -1 networkDeviceList := make([]interface{}, 8) - networkDeviceObjects := []*proxmox.CustomNetworkDevice{ + networkDeviceObjects := []*vms.CustomNetworkDevice{ vmConfig.NetworkDevice0, vmConfig.NetworkDevice1, vmConfig.NetworkDevice2, @@ -3674,17 +3694,21 @@ func vmReadNetworkValues( d *schema.ResourceData, m interface{}, vmID int, - vmConfig *proxmox.VirtualEnvironmentVMGetResponseData, + vmConfig *vms.GetResponseData, ) diag.Diagnostics { var diags diag.Diagnostics config := m.(proxmoxtf.ProviderConfiguration) - veClient, err := config.GetVEClient() - if err != nil { - return diag.FromErr(err) + + api, e := config.GetClient() + if e != nil { + return diag.FromErr(e) } nodeName := d.Get(mkResourceVirtualEnvironmentVMNodeName).(string) + + vmAPI := api.Node(nodeName).VM(vmID) + started := d.Get(mkResourceVirtualEnvironmentVMStarted).(bool) var ipv4Addresses []interface{} @@ -3713,15 +3737,8 @@ func vmReadNetworkValues( } var macAddresses []interface{} - networkInterfaces, err := veClient.WaitForNetworkInterfacesFromVMAgent( - ctx, - nodeName, - vmID, - int(agentTimeout.Seconds()), - 5, - true, - ) + networkInterfaces, err := vmAPI.WaitForNetworkInterfacesFromVMAgent(ctx, int(agentTimeout.Seconds()), 5, true) if err == nil && networkInterfaces.Result != nil { ipv4Addresses = make([]interface{}, len(*networkInterfaces.Result)) ipv6Addresses = make([]interface{}, len(*networkInterfaces.Result)) @@ -3755,20 +3772,20 @@ func vmReadNetworkValues( } } - err = d.Set(mkResourceVirtualEnvironmentVMIPv4Addresses, ipv4Addresses) - diags = append(diags, diag.FromErr(err)...) - err = d.Set(mkResourceVirtualEnvironmentVMIPv6Addresses, ipv6Addresses) - diags = append(diags, diag.FromErr(err)...) - err = d.Set(mkResourceVirtualEnvironmentVMNetworkInterfaceNames, networkInterfaceNames) - diags = append(diags, diag.FromErr(err)...) + e = d.Set(mkResourceVirtualEnvironmentVMIPv4Addresses, ipv4Addresses) + diags = append(diags, diag.FromErr(e)...) + e = d.Set(mkResourceVirtualEnvironmentVMIPv6Addresses, ipv6Addresses) + diags = append(diags, diag.FromErr(e)...) + e = d.Set(mkResourceVirtualEnvironmentVMNetworkInterfaceNames, networkInterfaceNames) + diags = append(diags, diag.FromErr(e)...) return diags } func vmReadPrimitiveValues( d *schema.ResourceData, - vmConfig *proxmox.VirtualEnvironmentVMGetResponseData, - vmStatus *proxmox.VirtualEnvironmentVMGetStatusResponseData, + vmConfig *vms.GetResponseData, + vmStatus *vms.GetStatusResponseData, ) diag.Diagnostics { var diags diag.Diagnostics var err error @@ -3797,6 +3814,7 @@ func vmReadPrimitiveValues( // Default value of "args" is "" according to the API documentation. err = d.Set(mkResourceVirtualEnvironmentVMKVMArguments, "") } + diags = append(diags, diag.FromErr(err)...) } @@ -3915,43 +3933,47 @@ func vmReadPrimitiveValues( func vmUpdate(ctx context.Context, d *schema.ResourceData, m interface{}) diag.Diagnostics { config := m.(proxmoxtf.ProviderConfiguration) - veClient, err := config.GetVEClient() - if err != nil { - return diag.FromErr(err) + + api, e := config.GetClient() + if e != nil { + return diag.FromErr(e) } nodeName := d.Get(mkResourceVirtualEnvironmentVMNodeName).(string) rebootRequired := false - vmID, err := strconv.Atoi(d.Id()) - if err != nil { - return diag.FromErr(err) + vmID, e := strconv.Atoi(d.Id()) + if e != nil { + return diag.FromErr(e) } - updateBody := &proxmox.VirtualEnvironmentVMUpdateRequestBody{ - IDEDevices: proxmox.CustomStorageDevices{ - "ide0": proxmox.CustomStorageDevice{ + vmAPI := api.Node(nodeName).VM(vmID) + + updateBody := &vms.UpdateRequestBody{ + IDEDevices: vms.CustomStorageDevices{ + "ide0": vms.CustomStorageDevice{ Enabled: false, }, - "ide1": proxmox.CustomStorageDevice{ + "ide1": vms.CustomStorageDevice{ Enabled: false, }, - "ide2": proxmox.CustomStorageDevice{ + "ide2": vms.CustomStorageDevice{ Enabled: false, }, - "ide3": proxmox.CustomStorageDevice{ + "ide3": vms.CustomStorageDevice{ Enabled: false, }, }, } var del []string + resource := VM() // Retrieve the entire configuration as we need to process certain values. - vmConfig, err := veClient.GetVM(ctx, nodeName, vmID) - if err != nil { - return diag.FromErr(err) + vmConfig, e := vmAPI.GetVM(ctx) + if e != nil { + return diag.FromErr(e) } // Prepare the new primitive configuration values. @@ -4040,7 +4062,7 @@ func vmUpdate(ctx context.Context, d *schema.ResourceData, m interface{}) diag.D agentTrim := types.CustomBool(agentBlock[mkResourceVirtualEnvironmentVMAgentTrim].(bool)) agentType := agentBlock[mkResourceVirtualEnvironmentVMAgentType].(string) - updateBody.Agent = &proxmox.CustomAgent{ + updateBody.Agent = &vms.CustomAgent{ Enabled: &agentEnabled, TrimClonedDisks: &agentTrim, Type: &agentType, @@ -4070,16 +4092,18 @@ func vmUpdate(ctx context.Context, d *schema.ResourceData, m interface{}) diag.D if d.HasChange(mkResourceVirtualEnvironmentVMBootOrder) { bootOrder := d.Get(mkResourceVirtualEnvironmentVMBootOrder).([]interface{}) bootOrderConverted := make([]string, len(bootOrder)) + for i, device := range bootOrder { bootOrderConverted[i] = device.(string) } - updateBody.Boot = &proxmox.CustomBoot{ + + updateBody.Boot = &vms.CustomBoot{ Order: &bootOrderConverted, } rebootRequired = true } - // Prepare the new CDROM configuration. + // Prepare the new CD-ROM configuration. if d.HasChange(mkResourceVirtualEnvironmentVMCDROM) { cdromBlock, err := getSchemaBlock( resource, @@ -4105,7 +4129,7 @@ func vmUpdate(ctx context.Context, d *schema.ResourceData, m interface{}) diag.D cdromMedia := "cdrom" - updateBody.IDEDevices["ide3"] = proxmox.CustomStorageDevice{ + updateBody.IDEDevices["ide3"] = vms.CustomStorageDevice{ Enabled: cdromEnabled, FileVolume: cdromFileID, Media: &cdromMedia, @@ -4134,7 +4158,7 @@ func vmUpdate(ctx context.Context, d *schema.ResourceData, m interface{}) diag.D cpuUnits := cpuBlock[mkResourceVirtualEnvironmentVMCPUUnits].(int) // Only the root account is allowed to change the CPU architecture, which makes this check necessary. - if veClient.Username == proxmox.DefaultRootAccount || + if api.API().IsRoot() || cpuArchitecture != dvResourceVirtualEnvironmentVMCPUArchitecture { updateBody.CPUArchitecture = &cpuArchitecture } @@ -4155,7 +4179,7 @@ func vmUpdate(ctx context.Context, d *schema.ResourceData, m interface{}) diag.D cpuFlagsConverted[fi] = flag.(string) } - updateBody.CPUEmulation = &proxmox.CustomCPUEmulation{ + updateBody.CPUEmulation = &vms.CustomCPUEmulation{ Flags: &cpuFlagsConverted, Type: cpuType, } @@ -4192,7 +4216,7 @@ func vmUpdate(ctx context.Context, d *schema.ResourceData, m interface{}) diag.D case "virtio": { if updateBody.VirtualIODevices == nil { - updateBody.VirtualIODevices = proxmox.CustomStorageDevices{} + updateBody.VirtualIODevices = vms.CustomStorageDevices{} } updateBody.VirtualIODevices[key] = tmp @@ -4200,7 +4224,7 @@ func vmUpdate(ctx context.Context, d *schema.ResourceData, m interface{}) diag.D case "sata": { if updateBody.SATADevices == nil { - updateBody.SATADevices = proxmox.CustomStorageDevices{} + updateBody.SATADevices = vms.CustomStorageDevices{} } updateBody.SATADevices[key] = tmp @@ -4208,7 +4232,7 @@ func vmUpdate(ctx context.Context, d *schema.ResourceData, m interface{}) diag.D case "scsi": { if updateBody.SCSIDevices == nil { - updateBody.SCSIDevices = proxmox.CustomStorageDevices{} + updateBody.SCSIDevices = vms.CustomStorageDevices{} } updateBody.SCSIDevices[key] = tmp @@ -4237,7 +4261,7 @@ func vmUpdate(ctx context.Context, d *schema.ResourceData, m interface{}) diag.D cdromMedia := "cdrom" - updateBody.IDEDevices["ide2"] = proxmox.CustomStorageDevice{ + updateBody.IDEDevices["ide2"] = vms.CustomStorageDevice{ Enabled: true, FileVolume: fmt.Sprintf("%s:cloudinit", initializationDatastoreID), Media: &cdromMedia, @@ -4261,10 +4285,6 @@ func vmUpdate(ctx context.Context, d *schema.ResourceData, m interface{}) diag.D // Prepare the new hostpci devices configuration. if d.HasChange(mkResourceVirtualEnvironmentVMHostPCI) { updateBody.PCIDevices = vmGetHostPCIDeviceObjects(d) - if err != nil { - return diag.FromErr(err) - } - rebootRequired = true } @@ -4291,7 +4311,7 @@ func vmUpdate(ctx context.Context, d *schema.ResourceData, m interface{}) diag.D if memoryShared > 0 { memorySharedName := fmt.Sprintf("vm-%d-ivshmem", vmID) - updateBody.SharedMemory = &proxmox.CustomSharedMemory{ + updateBody.SharedMemory = &vms.CustomSharedMemory{ Name: &memorySharedName, Size: memoryShared, } @@ -4350,9 +4370,9 @@ func vmUpdate(ctx context.Context, d *schema.ResourceData, m interface{}) diag.D // Prepare the new VGA configuration. if d.HasChange(mkResourceVirtualEnvironmentVMVGA) { - updateBody.VGADevice, err = vmGetVGADeviceObject(d) - if err != nil { - return diag.FromErr(err) + updateBody.VGADevice, e = vmGetVGADeviceObject(d) + if e != nil { + return diag.FromErr(e) } rebootRequired = true @@ -4369,9 +4389,9 @@ func vmUpdate(ctx context.Context, d *schema.ResourceData, m interface{}) diag.D // Update the configuration now that everything has been prepared. updateBody.Delete = del - err = veClient.UpdateVM(ctx, nodeName, vmID, updateBody) - if err != nil { - return diag.FromErr(err) + e = vmAPI.UpdateVM(ctx, updateBody) + if e != nil { + return diag.FromErr(e) } // Determine if the state of the virtual machine state needs to be changed. @@ -4380,20 +4400,21 @@ func vmUpdate(ctx context.Context, d *schema.ResourceData, m interface{}) diag.D if d.HasChange(mkResourceVirtualEnvironmentVMStarted) && !bool(template) { if started { startVMTimeout := d.Get(mkResourceVirtualEnvironmentVMTimeoutStartVM).(int) - err = veClient.StartVM(ctx, nodeName, vmID, startVMTimeout) - if err != nil { - return diag.FromErr(err) + + e = vmAPI.StartVM(ctx, startVMTimeout) + if e != nil { + return diag.FromErr(e) } } else { forceStop := types.CustomBool(true) shutdownTimeout := d.Get(mkResourceVirtualEnvironmentVMTimeoutShutdownVM).(int) - err = veClient.ShutdownVM(ctx, nodeName, vmID, &proxmox.VirtualEnvironmentVMShutdownRequestBody{ + e = vmAPI.ShutdownVM(ctx, &vms.ShutdownRequestBody{ ForceStop: &forceStop, Timeout: &shutdownTimeout, }, shutdownTimeout+30) - if err != nil { - return diag.FromErr(err) + if e != nil { + return diag.FromErr(e) } rebootRequired = false @@ -4416,7 +4437,7 @@ func vmUpdateDiskLocationAndSize( reboot bool, ) diag.Diagnostics { config := m.(proxmoxtf.ProviderConfiguration) - veClient, err := config.GetVEClient() + api, err := config.GetClient() if err != nil { return diag.FromErr(err) } @@ -4429,6 +4450,8 @@ func vmUpdateDiskLocationAndSize( return diag.FromErr(err) } + vmAPI := api.Node(nodeName).VM(vmID) + // Determine if any of the disks are changing location and/or size, and initiate the necessary actions. if d.HasChange(mkResourceVirtualEnvironmentVMDisk) { diskOld, diskNew := d.GetChange(mkResourceVirtualEnvironmentVMDisk) @@ -4449,8 +4472,9 @@ func vmUpdateDiskLocationAndSize( return diag.FromErr(err) } - var diskMoveBodies []*proxmox.VirtualEnvironmentVMMoveDiskRequestBody - var diskResizeBodies []*proxmox.VirtualEnvironmentVMResizeDiskRequestBody + var diskMoveBodies []*vms.MoveDiskRequestBody + + var diskResizeBodies []*vms.ResizeDiskRequestBody shutdownForDisksRequired := false @@ -4468,7 +4492,7 @@ func vmUpdateDiskLocationAndSize( diskMoveBodies = append( diskMoveBodies, - &proxmox.VirtualEnvironmentVMMoveDiskRequestBody{ + &vms.MoveDiskRequestBody{ DeleteOriginalDisk: &deleteOriginalDisk, Disk: *oldDisk.Interface, TargetStorage: *diskNewEntries[prefix][oldKey].ID, @@ -4482,7 +4506,7 @@ func vmUpdateDiskLocationAndSize( if *oldDisk.SizeInt <= *diskNewEntries[prefix][oldKey].SizeInt { diskResizeBodies = append( diskResizeBodies, - &proxmox.VirtualEnvironmentVMResizeDiskRequestBody{ + &vms.ResizeDiskRequestBody{ Disk: *oldDisk.Interface, Size: *diskNewEntries[prefix][oldKey].Size, }, @@ -4495,11 +4519,9 @@ func vmUpdateDiskLocationAndSize( forceStop := types.CustomBool(true) shutdownTimeout := d.Get(mkResourceVirtualEnvironmentVMTimeoutShutdownVM).(int) - err = veClient.ShutdownVM( + err = vmAPI.ShutdownVM( ctx, - nodeName, - vmID, - &proxmox.VirtualEnvironmentVMShutdownRequestBody{ + &vms.ShutdownRequestBody{ ForceStop: &forceStop, Timeout: &shutdownTimeout, }, @@ -4512,14 +4534,14 @@ func vmUpdateDiskLocationAndSize( for _, reqBody := range diskMoveBodies { moveDiskTimeout := d.Get(mkResourceVirtualEnvironmentVMTimeoutMoveDisk).(int) - err = veClient.MoveVMDisk(ctx, nodeName, vmID, reqBody, moveDiskTimeout) + err = vmAPI.MoveVMDisk(ctx, reqBody, moveDiskTimeout) if err != nil { return diag.FromErr(err) } } for _, reqBody := range diskResizeBodies { - err = veClient.ResizeVMDisk(ctx, nodeName, vmID, reqBody) + err = vmAPI.ResizeVMDisk(ctx, reqBody) if err != nil { return diag.FromErr(err) } @@ -4527,7 +4549,7 @@ func vmUpdateDiskLocationAndSize( if shutdownForDisksRequired && started && !template { startVMTimeout := d.Get(mkResourceVirtualEnvironmentVMTimeoutStartVM).(int) - err = veClient.StartVM(ctx, nodeName, vmID, startVMTimeout) + err = vmAPI.StartVM(ctx, startVMTimeout) if err != nil { return diag.FromErr(err) } @@ -4541,11 +4563,9 @@ func vmUpdateDiskLocationAndSize( if reboot { rebootTimeout := d.Get(mkResourceVirtualEnvironmentVMTimeoutReboot).(int) - err := veClient.RebootVM( + err := vmAPI.RebootVM( ctx, - nodeName, - vmID, - &proxmox.VirtualEnvironmentVMRebootRequestBody{ + &vms.RebootRequestBody{ Timeout: &rebootTimeout, }, rebootTimeout+30, @@ -4560,7 +4580,7 @@ func vmUpdateDiskLocationAndSize( func vmDelete(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) } @@ -4571,8 +4591,10 @@ func vmDelete(ctx context.Context, d *schema.ResourceData, m interface{}) diag.D return diag.FromErr(err) } + vmAPI := api.Node(nodeName).VM(vmID) + // Shut down the virtual machine before deleting it. - status, err := veClient.GetVMStatus(ctx, nodeName, vmID) + status, err := vmAPI.GetVMStatus(ctx) if err != nil { return diag.FromErr(err) } @@ -4581,11 +4603,9 @@ func vmDelete(ctx context.Context, d *schema.ResourceData, m interface{}) diag.D forceStop := types.CustomBool(true) shutdownTimeout := d.Get(mkResourceVirtualEnvironmentVMTimeoutShutdownVM).(int) - err = veClient.ShutdownVM( + err = vmAPI.ShutdownVM( ctx, - nodeName, - vmID, - &proxmox.VirtualEnvironmentVMShutdownRequestBody{ + &vms.ShutdownRequestBody{ ForceStop: &forceStop, Timeout: &shutdownTimeout, }, @@ -4596,7 +4616,7 @@ func vmDelete(ctx context.Context, d *schema.ResourceData, m interface{}) diag.D } } - err = veClient.DeleteVM(ctx, nodeName, vmID) + err = vmAPI.DeleteVM(ctx) if err != nil { if strings.Contains(err.Error(), "HTTP 404") || @@ -4609,7 +4629,7 @@ func vmDelete(ctx context.Context, d *schema.ResourceData, m interface{}) diag.D } // Wait for the state to become unavailable as that clearly indicates the destruction of the VM. - err = veClient.WaitForVMState(ctx, nodeName, vmID, "", 60, 2) + err = vmAPI.WaitForVMState(ctx, "", 60, 2) if err == nil { return diag.Errorf("failed to delete VM \"%d\"", vmID) } diff --git a/proxmoxtf/resource/vm_test.go b/proxmoxtf/resource/vm_test.go index 649c5493..e8230598 100644 --- a/proxmoxtf/resource/vm_test.go +++ b/proxmoxtf/resource/vm_test.go @@ -17,8 +17,8 @@ import ( // TestVMInstantiation tests whether the VM instance can be instantiated. func TestVMInstantiation(t *testing.T) { t.Parallel() - s := VM() + s := VM() if s == nil { t.Fatalf("Cannot instantiate VM") } @@ -27,6 +27,7 @@ func TestVMInstantiation(t *testing.T) { // TestVMSchema tests the VM schema. func TestVMSchema(t *testing.T) { t.Parallel() + s := VM() test.AssertRequiredArguments(t, s, []string{ diff --git a/proxmoxtf/structure/test.go b/proxmoxtf/structure/test.go index 9af495cc..8b0bad08 100644 --- a/proxmoxtf/structure/test.go +++ b/proxmoxtf/structure/test.go @@ -14,14 +14,20 @@ import ( "github.com/stretchr/testify/require" ) +// AssertComputedAttributes asserts that the given keys are present in the schema and are computed. func AssertComputedAttributes(t *testing.T, s map[string]*schema.Schema, keys []string) { + t.Helper() + for _, v := range keys { require.NotNil(t, s[v], "Error in Schema: Missing definition for \"%s\"", v) assert.True(t, s[v].Computed, "Error in Schema: Attribute \"%s\" is not computed", v) } } +// AssertNestedSchemaExistence asserts that the given key is present in the schema and is a nested schema. func AssertNestedSchemaExistence(t *testing.T, s map[string]*schema.Schema, key string) *schema.Resource { + t.Helper() + sh, ok := s[key].Elem.(*schema.Resource) if !ok { @@ -33,21 +39,30 @@ func AssertNestedSchemaExistence(t *testing.T, s map[string]*schema.Schema, key return sh } +// AssertOptionalArguments asserts that the given keys are present in the schema and are optional. func AssertOptionalArguments(t *testing.T, s map[string]*schema.Schema, keys []string) { + t.Helper() + for _, v := range keys { require.NotNil(t, s[v], "Error in Schema: Missing definition for \"%s\"", v) assert.True(t, s[v].Optional, "Error in Schema: Argument \"%s\" is not optional", v) } } +// AssertRequiredArguments asserts that the given keys are present in the schema and are required. func AssertRequiredArguments(t *testing.T, s map[string]*schema.Schema, keys []string) { + t.Helper() + for _, v := range keys { require.NotNil(t, s[v], "Error in Schema: Missing definition for \"%s\"", v) assert.True(t, s[v].Required, "Error in Schema: Argument \"%s\" is not required", v) } } +// AssertValueTypes asserts that the given keys are present in the schema and are of the given type. func AssertValueTypes(t *testing.T, s map[string]*schema.Schema, f map[string]schema.ValueType) { + t.Helper() + for fn, ft := range f { require.NotNil(t, s[fn], "Error in Schema: Missing definition for \"%s\"", fn) assert.Equal(t, ft, s[fn].Type, "Error in Schema: Argument or attribute \"%s\" is not of type \"%v\"", fn, ft) diff --git a/proxmoxtf/structure/tools.go b/proxmoxtf/structure/tools.go index 05249295..d89c58c5 100644 --- a/proxmoxtf/structure/tools.go +++ b/proxmoxtf/structure/tools.go @@ -13,12 +13,13 @@ import ( ) // MergeSchema merges the map[string]*schema.Schema from src into dst. Safety -// against conflicts is enforced by panicing. +// against conflicts is enforced by panicking. func MergeSchema(dst, src map[string]*schema.Schema) { for k, v := range src { if _, ok := dst[k]; ok { panic(fmt.Errorf("conflicting schema key: %s", k)) } + dst[k] = v } } diff --git a/proxmoxtf/test/test_utils.go b/proxmoxtf/test/test_utils.go index 2b4a1271..81de76e0 100644 --- a/proxmoxtf/test/test_utils.go +++ b/proxmoxtf/test/test_utils.go @@ -14,14 +14,20 @@ import ( "github.com/stretchr/testify/require" ) +// AssertComputedAttributes checks that the given schema has the given computed attributes. func AssertComputedAttributes(t *testing.T, s *schema.Resource, keys []string) { + t.Helper() + for _, v := range keys { require.NotNil(t, s.Schema[v], "Error in Schema: Missing definition for \"%s\"", v) assert.True(t, s.Schema[v].Computed, "Error in Schema: Attribute \"%s\" is not computed", v) } } +// AssertNestedSchemaExistence checks that the given schema has a nested schema for the given key. func AssertNestedSchemaExistence(t *testing.T, s *schema.Resource, key string) *schema.Resource { + t.Helper() + sh, ok := s.Schema[key].Elem.(*schema.Resource) if !ok { @@ -33,21 +39,30 @@ func AssertNestedSchemaExistence(t *testing.T, s *schema.Resource, key string) * return sh } +// AssertOptionalArguments checks that the given schema has the given optional arguments. func AssertOptionalArguments(t *testing.T, s *schema.Resource, keys []string) { + t.Helper() + for _, v := range keys { require.NotNil(t, s.Schema[v], "Error in Schema: Missing definition for \"%s\"", v) assert.True(t, s.Schema[v].Optional, "Error in Schema: Argument \"%s\" is not optional", v) } } +// AssertRequiredArguments checks that the given schema has the given required arguments. func AssertRequiredArguments(t *testing.T, s *schema.Resource, keys []string) { + t.Helper() + for _, v := range keys { require.NotNil(t, s.Schema[v], "Error in Schema: Missing definition for \"%s\"", v) assert.True(t, s.Schema[v].Required, "Error in Schema: Argument \"%s\" is not required", v) } } +// AssertValueTypes checks that the given schema has the given value types for the given fields. func AssertValueTypes(t *testing.T, s *schema.Resource, f map[string]schema.ValueType) { + t.Helper() + for fn, ft := range f { require.NotNil(t, s.Schema[fn], "Error in Schema: Missing definition for \"%s\"", fn) assert.Equal(t, ft, s.Schema[fn].Type, "Error in Schema: Argument or attribute \"%s\" is not of type \"%v\"", fn, ft) diff --git a/proxmox/utils.go b/utils/io.go similarity index 86% rename from proxmox/utils.go rename to utils/io.go index ac71e07c..735c431a 100644 --- a/proxmox/utils.go +++ b/utils/io.go @@ -4,7 +4,7 @@ * file, You can obtain one at https://mozilla.org/MPL/2.0/. */ -package proxmox +package utils import ( "context" @@ -13,6 +13,7 @@ import ( "github.com/hashicorp/terraform-plugin-log/tflog" ) +// CloseOrLogError closes the given io.Closer and logs any error. func CloseOrLogError(ctx context.Context) func(io.Closer) { return func(c io.Closer) { if err := c.Close(); err != nil { diff --git a/proxmox/utils_test.go b/utils/io_test.go similarity index 97% rename from proxmox/utils_test.go rename to utils/io_test.go index 8244ade3..268e020d 100644 --- a/proxmox/utils_test.go +++ b/utils/io_test.go @@ -4,7 +4,7 @@ * file, You can obtain one at https://mozilla.org/MPL/2.0/. */ -package proxmox +package utils import ( "context" @@ -16,10 +16,12 @@ import ( func TestCloseOrLogError(t *testing.T) { t.Parallel() + f := CloseOrLogError(context.Background()) c := &testCloser{} b := &badCloser{} + func() { defer f(c) defer f(b)