0
0
mirror of https://github.com/bpg/terraform-provider-proxmox.git synced 2025-06-30 18:42:58 +00:00

Initial work on file resource

This commit is contained in:
Dan Petersen 2019-12-12 00:32:09 +01:00
parent 1921fc0531
commit e2541de215
10 changed files with 312 additions and 25 deletions

View File

@ -13,6 +13,7 @@ FEATURES:
* **New Data Source:** `proxmox_virtual_environment_user` * **New Data Source:** `proxmox_virtual_environment_user`
* **New Data Source:** `proxmox_virtual_environment_users` * **New Data Source:** `proxmox_virtual_environment_users`
* **New Data Source:** `proxmox_virtual_environment_version` * **New Data Source:** `proxmox_virtual_environment_version`
* **New Resource:** `proxmox_virtual_environment_file`
* **New Resource:** `proxmox_virtual_environment_group` * **New Resource:** `proxmox_virtual_environment_group`
* **New Resource:** `proxmox_virtual_environment_pool` * **New Resource:** `proxmox_virtual_environment_pool`
* **New Resource:** `proxmox_virtual_environment_role` * **New Resource:** `proxmox_virtual_environment_role`

View File

@ -181,6 +181,18 @@ This data source doesn't accept arguments.
#### Virtual Environment #### Virtual Environment
##### File (proxmox_virtual_environment_file)
###### Arguments
* `datastore_id` - (Required) The datastore id
* `file_name` - (Optional) The file name to use in the datastore (leave undefined to use source file name)
* `node_name` - (Required) The node name
* `source` - (Required) A path to a file
* `template` - (Required) Whether this is a container template
###### Attributes
This resource doesn't expose any additional attributes.
##### Group (proxmox_virtual_environment_group) ##### Group (proxmox_virtual_environment_group)
###### Arguments ###### Arguments

View File

@ -42,9 +42,10 @@ func dataSourceVirtualEnvironmentDatastores() *schema.Resource {
}, },
}, },
mkDataSourceVirtualEnvironmentDatastoresDatastoreIDs: &schema.Schema{ mkDataSourceVirtualEnvironmentDatastoresDatastoreIDs: &schema.Schema{
Type: schema.TypeString, Type: schema.TypeList,
Description: "The datastore id", Description: "The datastore id",
Computed: true, Computed: true,
Elem: &schema.Schema{Type: schema.TypeString},
}, },
mkDataSourceVirtualEnvironmentDatastoresEnabled: &schema.Schema{ mkDataSourceVirtualEnvironmentDatastoresEnabled: &schema.Schema{
Type: schema.TypeList, Type: schema.TypeList,
@ -54,7 +55,7 @@ func dataSourceVirtualEnvironmentDatastores() *schema.Resource {
}, },
mkDataSourceVirtualEnvironmentDatastoresNodeName: &schema.Schema{ mkDataSourceVirtualEnvironmentDatastoresNodeName: &schema.Schema{
Type: schema.TypeString, Type: schema.TypeString,
Description: "The node id", Description: "The node name",
Required: true, Required: true,
}, },
mkDataSourceVirtualEnvironmentDatastoresShared: &schema.Schema{ mkDataSourceVirtualEnvironmentDatastoresShared: &schema.Schema{

View File

@ -0,0 +1,6 @@
resource "proxmox_virtual_environment_file" "alpine_template" {
datastore_id = "${element(data.proxmox_virtual_environment_datastores.example.datastore_ids, index(data.proxmox_virtual_environment_datastores.example.datastore_ids, "local"))}"
node_name = "${data.proxmox_virtual_environment_datastores.example.node_name}"
source = "${path.module}/assets/alpine-3.10-default_20190626_amd64.tar.xz"
template = true
}

View File

@ -41,6 +41,7 @@ func Provider() *schema.Provider {
"proxmox_virtual_environment_version": dataSourceVirtualEnvironmentVersion(), "proxmox_virtual_environment_version": dataSourceVirtualEnvironmentVersion(),
}, },
ResourcesMap: map[string]*schema.Resource{ ResourcesMap: map[string]*schema.Resource{
"proxmox_virtual_environment_file": resourceVirtualEnvironmentFile(),
"proxmox_virtual_environment_group": resourceVirtualEnvironmentGroup(), "proxmox_virtual_environment_group": resourceVirtualEnvironmentGroup(),
"proxmox_virtual_environment_pool": resourceVirtualEnvironmentPool(), "proxmox_virtual_environment_pool": resourceVirtualEnvironmentPool(),
"proxmox_virtual_environment_role": resourceVirtualEnvironmentRole(), "proxmox_virtual_environment_role": resourceVirtualEnvironmentRole(),

View File

@ -10,6 +10,7 @@ import (
"encoding/json" "encoding/json"
"errors" "errors"
"fmt" "fmt"
"io"
"log" "log"
"net/http" "net/http"
"net/url" "net/url"
@ -38,6 +39,12 @@ type VirtualEnvironmentClient struct {
httpClient *http.Client httpClient *http.Client
} }
// VirtualEnvironmentMultiPartData enables multipart uploads in DoRequest.
type VirtualEnvironmentMultiPartData struct {
Boundary string
Reader io.Reader
}
// NewVirtualEnvironmentClient creates and initializes a VirtualEnvironmentClient instance. // NewVirtualEnvironmentClient creates and initializes a VirtualEnvironmentClient instance.
func NewVirtualEnvironmentClient(endpoint, username, password string, insecure bool) (*VirtualEnvironmentClient, error) { func NewVirtualEnvironmentClient(endpoint, username, password string, insecure bool) (*VirtualEnvironmentClient, error) {
url, err := url.ParseRequestURI(endpoint) url, err := url.ParseRequestURI(endpoint)
@ -77,20 +84,36 @@ func NewVirtualEnvironmentClient(endpoint, username, password string, insecure b
// DoRequest performs a HTTP request against a JSON API endpoint. // DoRequest performs a HTTP request against a JSON API endpoint.
func (c *VirtualEnvironmentClient) DoRequest(method, path string, requestBody interface{}, responseBody interface{}) error { func (c *VirtualEnvironmentClient) DoRequest(method, path string, requestBody interface{}, responseBody interface{}) error {
var reqBodyReader io.Reader
log.Printf("[DEBUG] Performing HTTP %s request (path: %s)", method, path) log.Printf("[DEBUG] Performing HTTP %s request (path: %s)", method, path)
modifiedPath := path modifiedPath := path
urlEncodedRequestBody := new(bytes.Buffer) reqBodyType := ""
if requestBody != nil { if requestBody != nil {
multipartData, multipart := requestBody.(*VirtualEnvironmentMultiPartData)
pipedBodyReader, pipedBody := requestBody.(*io.PipeReader)
if multipart {
reqBodyReader = multipartData.Reader
reqBodyType = fmt.Sprintf("multipart/form-data; boundary=%s", multipartData.Boundary)
log.Printf("[DEBUG] Added multipart request body to HTTP %s request (path: %s)", method, modifiedPath)
} else if pipedBody {
reqBodyReader = pipedBodyReader
log.Printf("[DEBUG] Added piped request body to HTTP %s request (path: %s)", method, modifiedPath)
} else {
v, err := query.Values(requestBody) v, err := query.Values(requestBody)
if err != nil { if err != nil {
return fmt.Errorf("Failed to encode HTTP %s request (path: %s) - Reason: %s", method, path, err.Error()) return fmt.Errorf("Failed to encode HTTP %s request (path: %s) - Reason: %s", method, modifiedPath, err.Error())
} }
encodedValues := v.Encode() encodedValues := v.Encode()
if encodedValues != "" {
if method == hmGET || method == hmHEAD { if method == hmGET || method == hmHEAD {
if !strings.Contains(modifiedPath, "?") { if !strings.Contains(modifiedPath, "?") {
modifiedPath = fmt.Sprintf("%s?%s", modifiedPath, encodedValues) modifiedPath = fmt.Sprintf("%s?%s", modifiedPath, encodedValues)
@ -98,22 +121,27 @@ func (c *VirtualEnvironmentClient) DoRequest(method, path string, requestBody in
modifiedPath = fmt.Sprintf("%s&%s", modifiedPath, encodedValues) modifiedPath = fmt.Sprintf("%s&%s", modifiedPath, encodedValues)
} }
} else { } else {
urlEncodedRequestBody = bytes.NewBufferString(encodedValues) reqBodyReader = bytes.NewBufferString(encodedValues)
reqBodyType = "application/x-www-form-urlencoded"
} }
log.Printf("[DEBUG] Added request body to HTTP %s request (path: %s) - Body: %s", method, path, encodedValues) log.Printf("[DEBUG] Added request body to HTTP %s request (path: %s) - Body: %s", method, modifiedPath, encodedValues)
}
}
} else {
reqBodyReader = new(bytes.Buffer)
} }
req, err := http.NewRequest(method, fmt.Sprintf("%s/%s/%s", c.Endpoint, basePathJSONAPI, modifiedPath), urlEncodedRequestBody) req, err := http.NewRequest(method, fmt.Sprintf("%s/%s/%s", c.Endpoint, basePathJSONAPI, modifiedPath), reqBodyReader)
if err != nil { if err != nil {
return fmt.Errorf("Failed to create HTTP %s request (path: %s) - Reason: %s", method, path, err.Error()) return fmt.Errorf("Failed to create HTTP %s request (path: %s) - Reason: %s", method, modifiedPath, err.Error())
} }
req.Header.Add("Accept", "application/json") req.Header.Add("Accept", "application/json")
if req.Method != hmGET && req.Method != hmHEAD { if reqBodyType != "" {
req.Header.Add("Content-Type", "application/x-www-form-urlencoded") req.Header.Add("Content-Type", reqBodyType)
} }
err = c.AuthenticateRequest(req) err = c.AuthenticateRequest(req)
@ -125,7 +153,7 @@ func (c *VirtualEnvironmentClient) DoRequest(method, path string, requestBody in
res, err := c.httpClient.Do(req) res, err := c.httpClient.Do(req)
if err != nil { if err != nil {
return fmt.Errorf("Failed to perform HTTP %s request (path: %s) - Reason: %s", method, path, err.Error()) return fmt.Errorf("Failed to perform HTTP %s request (path: %s) - Reason: %s", method, modifiedPath, err.Error())
} }
err = c.ValidateResponseCode(res) err = c.ValidateResponseCode(res)
@ -138,7 +166,7 @@ func (c *VirtualEnvironmentClient) DoRequest(method, path string, requestBody in
err = json.NewDecoder(res.Body).Decode(responseBody) err = json.NewDecoder(res.Body).Decode(responseBody)
if err != nil { if err != nil {
return fmt.Errorf("Failed to decode HTTP %s response (path: %s) - Reason: %s", method, path, err.Error()) return fmt.Errorf("Failed to decode HTTP %s response (path: %s) - Reason: %s", method, modifiedPath, err.Error())
} }
} }
@ -150,7 +178,7 @@ func (c *VirtualEnvironmentClient) ValidateResponseCode(res *http.Response) erro
if res.StatusCode < 200 || res.StatusCode >= 300 { if res.StatusCode < 200 || res.StatusCode >= 300 {
switch res.StatusCode { switch res.StatusCode {
case 400: case 400:
return fmt.Errorf("Received a HTTP %d response - This is most likely caused by a bug in the code, so please create a new issue on https://github.com/danitso/terraform-provider-proxmox/issues", res.StatusCode) return fmt.Errorf("Received a HTTP %d response - Reason: %s", res.StatusCode, res.Status)
case 401: case 401:
return fmt.Errorf("Received a HTTP %d response - Please verify that the specified credentials are valid", res.StatusCode) return fmt.Errorf("Received a HTTP %d response - Please verify that the specified credentials are valid", res.StatusCode)
case 403: case 403:

View File

@ -5,8 +5,11 @@
package proxmox package proxmox
import ( import (
"bytes"
"errors" "errors"
"fmt" "fmt"
"io"
"mime/multipart"
"sort" "sort"
) )
@ -24,7 +27,7 @@ type VirtualEnvironmentDatastoreListResponseBody struct {
Data []*VirtualEnvironmentDatastoreListResponseData `json:"data,omitempty"` Data []*VirtualEnvironmentDatastoreListResponseData `json:"data,omitempty"`
} }
// VirtualEnvironmentDatastoreListResponseData contains the data from a node list response. // VirtualEnvironmentDatastoreListResponseData contains the data from a datastore list response.
type VirtualEnvironmentDatastoreListResponseData struct { type VirtualEnvironmentDatastoreListResponseData struct {
Active *CustomBool `json:"active,omitempty"` Active *CustomBool `json:"active,omitempty"`
ContentTypes *CustomCommaSeparatedList `json:"content,omitempty"` ContentTypes *CustomCommaSeparatedList `json:"content,omitempty"`
@ -38,6 +41,20 @@ type VirtualEnvironmentDatastoreListResponseData struct {
Type string `json:"type,omitempty"` Type string `json:"type,omitempty"`
} }
// VirtualEnvironmentDatastoreUploadRequestBody contains the body for a datastore upload request.
type VirtualEnvironmentDatastoreUploadRequestBody struct {
ContentType string `json:"content,omitempty"`
DatastoreID string `json:"storage,omitempty"`
FileName string `json:"filename,omitempty"`
FileReader io.Reader `json:"-"`
NodeName string `json:"node,omitempty"`
}
// VirtualEnvironmentDatastoreUploadResponseBody contains the body from a datastore upload response.
type VirtualEnvironmentDatastoreUploadResponseBody struct {
UploadID *string `json:"data,omitempty"`
}
// ListDatastores retrieves a list of nodes. // ListDatastores retrieves a list of nodes.
func (c *VirtualEnvironmentClient) ListDatastores(nodeName string, d *VirtualEnvironmentDatastoreListRequestBody) ([]*VirtualEnvironmentDatastoreListResponseData, error) { func (c *VirtualEnvironmentClient) ListDatastores(nodeName string, d *VirtualEnvironmentDatastoreListRequestBody) ([]*VirtualEnvironmentDatastoreListResponseData, error) {
resBody := &VirtualEnvironmentDatastoreListResponseBody{} resBody := &VirtualEnvironmentDatastoreListResponseBody{}
@ -57,3 +74,47 @@ func (c *VirtualEnvironmentClient) ListDatastores(nodeName string, d *VirtualEnv
return resBody.Data, nil return resBody.Data, nil
} }
// UploadFileToDatastore uploads a file to a datastore.
func (c *VirtualEnvironmentClient) UploadFileToDatastore(d *VirtualEnvironmentDatastoreUploadRequestBody) (*VirtualEnvironmentDatastoreUploadResponseBody, error) {
r, w := io.Pipe()
m := multipart.NewWriter(w)
go func() {
defer w.Close()
defer m.Close()
m.WriteField("content", d.ContentType)
part, err := m.CreateFormFile("filename", d.FileName)
if err != nil {
return
}
_, err = io.Copy(part, d.FileReader)
if err != nil {
return
}
}()
// Due to Proxmox VE not supporting chunked transfers, we sadly need to load the file into memory.
// This is not optimal for large files but there's no alternative right now.
workaroundReader := new(bytes.Buffer)
workaroundReader.ReadFrom(r)
reqBody := &VirtualEnvironmentMultiPartData{
Boundary: m.Boundary(),
Reader: workaroundReader,
}
resBody := &VirtualEnvironmentDatastoreUploadResponseBody{}
err := c.DoRequest(hmPOST, fmt.Sprintf("nodes/%s/storage/%s/upload", d.NodeName, d.DatastoreID), reqBody, resBody)
if err != nil {
return nil, err
}
return resBody, nil
}

View File

@ -0,0 +1,142 @@
/* 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 main
import (
"fmt"
"os"
"path/filepath"
"github.com/danitso/terraform-provider-proxmox/proxmox"
"github.com/hashicorp/terraform/helper/schema"
)
const (
mkResourceVirtualEnvironmentFileDatastoreID = "datastore_id"
mkResourceVirtualEnvironmentFileFileName = "file_name"
mkResourceVirtualEnvironmentFileNodeName = "node_name"
mkResourceVirtualEnvironmentFileSource = "source"
mkResourceVirtualEnvironmentFileTemplate = "template"
)
func resourceVirtualEnvironmentFile() *schema.Resource {
return &schema.Resource{
Schema: map[string]*schema.Schema{
mkResourceVirtualEnvironmentFileDatastoreID: &schema.Schema{
Type: schema.TypeString,
Description: "The datastore id",
Required: true,
ForceNew: true,
},
mkResourceVirtualEnvironmentFileFileName: &schema.Schema{
Type: schema.TypeString,
Description: "The file name to use in the datastore",
Optional: true,
ForceNew: true,
Default: "",
},
mkResourceVirtualEnvironmentFileNodeName: &schema.Schema{
Type: schema.TypeString,
Description: "The node name",
Required: true,
ForceNew: true,
},
mkResourceVirtualEnvironmentFileSource: &schema.Schema{
Type: schema.TypeString,
Description: "The path to a file",
Required: true,
ForceNew: true,
},
mkResourceVirtualEnvironmentFileTemplate: &schema.Schema{
Type: schema.TypeBool,
Description: "Whether this is a container template",
Required: true,
ForceNew: true,
},
},
Create: resourceVirtualEnvironmentFileCreate,
Read: resourceVirtualEnvironmentFileRead,
Delete: resourceVirtualEnvironmentFileDelete,
}
}
func resourceVirtualEnvironmentFileCreate(d *schema.ResourceData, m interface{}) error {
config := m.(providerConfiguration)
veClient, err := config.GetVEClient()
if err != nil {
return err
}
datastoreID := d.Get(mkResourceVirtualEnvironmentFileDatastoreID).(string)
fileName := d.Get(mkResourceVirtualEnvironmentFileFileName).(string)
nodeName := d.Get(mkResourceVirtualEnvironmentFileNodeName).(string)
source := d.Get(mkResourceVirtualEnvironmentFileSource).(string)
template := d.Get(mkResourceVirtualEnvironmentFileTemplate).(bool)
if fileName == "" {
fileName = filepath.Base(source)
}
file, err := os.Open(source)
if err != nil {
return err
}
defer file.Close()
contentType := "iso"
if template {
contentType = "vztmpl"
}
body := &proxmox.VirtualEnvironmentDatastoreUploadRequestBody{
ContentType: contentType,
DatastoreID: datastoreID,
FileName: fileName,
FileReader: file,
NodeName: nodeName,
}
_, err = veClient.UploadFileToDatastore(body)
if err != nil {
return err
}
d.SetId(fmt.Sprintf("%s:%s:%s", nodeName, datastoreID, fileName))
return resourceVirtualEnvironmentFileRead(d, m)
}
func resourceVirtualEnvironmentFileRead(d *schema.ResourceData, m interface{}) error {
/*
config := m.(providerConfiguration)
veClient, err := config.GetVEClient()
if err != nil {
return err
}
*/
return nil
}
func resourceVirtualEnvironmentFileDelete(d *schema.ResourceData, m interface{}) error {
/*
config := m.(providerConfiguration)
veClient, err := config.GetVEClient()
if err != nil {
return err
}
*/
d.SetId("")
return nil
}

View File

@ -0,0 +1,35 @@
/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at https://mozilla.org/MPL/2.0/. */
package main
import (
"testing"
)
// TestResourceVirtualEnvironmentFileInstantiation tests whether the ResourceVirtualEnvironmentFile instance can be instantiated.
func TestResourceVirtualEnvironmentFileInstantiation(t *testing.T) {
s := resourceVirtualEnvironmentFile()
if s == nil {
t.Fatalf("Cannot instantiate resourceVirtualEnvironmentFile")
}
}
// TestResourceVirtualEnvironmentFileSchema tests the resourceVirtualEnvironmentFile schema.
func TestResourceVirtualEnvironmentFileSchema(t *testing.T) {
s := resourceVirtualEnvironmentFile()
attributeKeys := []string{}
for _, v := range attributeKeys {
if s.Schema[v] == nil {
t.Fatalf("Error in resourceVirtualEnvironmentFile.Schema: Missing attribute \"%s\"", v)
}
if s.Schema[v].Computed != true {
t.Fatalf("Error in resourceVirtualEnvironmentFile.Schema: Attribute \"%s\" is not computed", v)
}
}
}