diff --git a/CHANGELOG.md b/CHANGELOG.md index 19b946f8..02c51bf6 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -13,6 +13,7 @@ FEATURES: * **New Data Source:** `proxmox_virtual_environment_user` * **New Data Source:** `proxmox_virtual_environment_users` * **New Data Source:** `proxmox_virtual_environment_version` +* **New Resource:** `proxmox_virtual_environment_file` * **New Resource:** `proxmox_virtual_environment_group` * **New Resource:** `proxmox_virtual_environment_pool` * **New Resource:** `proxmox_virtual_environment_role` diff --git a/README.md b/README.md index 44b1a4dc..fcc16eb2 100644 --- a/README.md +++ b/README.md @@ -181,6 +181,18 @@ This data source doesn't accept arguments. #### 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) ###### Arguments diff --git a/data_source_virtual_environment_datastores.go b/data_source_virtual_environment_datastores.go index 1bf1966c..9c3db556 100644 --- a/data_source_virtual_environment_datastores.go +++ b/data_source_virtual_environment_datastores.go @@ -42,9 +42,10 @@ func dataSourceVirtualEnvironmentDatastores() *schema.Resource { }, }, mkDataSourceVirtualEnvironmentDatastoresDatastoreIDs: &schema.Schema{ - Type: schema.TypeString, + Type: schema.TypeList, Description: "The datastore id", Computed: true, + Elem: &schema.Schema{Type: schema.TypeString}, }, mkDataSourceVirtualEnvironmentDatastoresEnabled: &schema.Schema{ Type: schema.TypeList, @@ -54,7 +55,7 @@ func dataSourceVirtualEnvironmentDatastores() *schema.Resource { }, mkDataSourceVirtualEnvironmentDatastoresNodeName: &schema.Schema{ Type: schema.TypeString, - Description: "The node id", + Description: "The node name", Required: true, }, mkDataSourceVirtualEnvironmentDatastoresShared: &schema.Schema{ diff --git a/example/assets/alpine-3.10-default_20190626_amd64.tar.xz b/example/assets/alpine-3.10-default_20190626_amd64.tar.xz new file mode 100644 index 00000000..c5f9fe38 Binary files /dev/null and b/example/assets/alpine-3.10-default_20190626_amd64.tar.xz differ diff --git a/example/resource_virtual_environment_file.tf b/example/resource_virtual_environment_file.tf new file mode 100644 index 00000000..3883cf1e --- /dev/null +++ b/example/resource_virtual_environment_file.tf @@ -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 +} diff --git a/provider.go b/provider.go index 1d21c969..3f1be9cc 100644 --- a/provider.go +++ b/provider.go @@ -41,6 +41,7 @@ func Provider() *schema.Provider { "proxmox_virtual_environment_version": dataSourceVirtualEnvironmentVersion(), }, ResourcesMap: map[string]*schema.Resource{ + "proxmox_virtual_environment_file": resourceVirtualEnvironmentFile(), "proxmox_virtual_environment_group": resourceVirtualEnvironmentGroup(), "proxmox_virtual_environment_pool": resourceVirtualEnvironmentPool(), "proxmox_virtual_environment_role": resourceVirtualEnvironmentRole(), diff --git a/proxmox/virtual_environment.go b/proxmox/virtual_environment.go index ebe50461..6701e605 100644 --- a/proxmox/virtual_environment.go +++ b/proxmox/virtual_environment.go @@ -10,6 +10,7 @@ import ( "encoding/json" "errors" "fmt" + "io" "log" "net/http" "net/url" @@ -38,6 +39,12 @@ type VirtualEnvironmentClient struct { httpClient *http.Client } +// VirtualEnvironmentMultiPartData enables multipart uploads in DoRequest. +type VirtualEnvironmentMultiPartData struct { + Boundary string + Reader io.Reader +} + // NewVirtualEnvironmentClient creates and initializes a VirtualEnvironmentClient instance. func NewVirtualEnvironmentClient(endpoint, username, password string, insecure bool) (*VirtualEnvironmentClient, error) { url, err := url.ParseRequestURI(endpoint) @@ -77,43 +84,64 @@ func NewVirtualEnvironmentClient(endpoint, username, password string, insecure b // DoRequest performs a HTTP request against a JSON API endpoint. 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) modifiedPath := path - urlEncodedRequestBody := new(bytes.Buffer) + reqBodyType := "" if requestBody != nil { - v, err := query.Values(requestBody) + multipartData, multipart := requestBody.(*VirtualEnvironmentMultiPartData) + pipedBodyReader, pipedBody := requestBody.(*io.PipeReader) - if err != nil { - return fmt.Errorf("Failed to encode HTTP %s request (path: %s) - Reason: %s", method, path, err.Error()) - } + if multipart { + reqBodyReader = multipartData.Reader + reqBodyType = fmt.Sprintf("multipart/form-data; boundary=%s", multipartData.Boundary) - encodedValues := v.Encode() + log.Printf("[DEBUG] Added multipart request body to HTTP %s request (path: %s)", method, modifiedPath) + } else if pipedBody { + reqBodyReader = pipedBodyReader - if method == hmGET || method == hmHEAD { - if !strings.Contains(modifiedPath, "?") { - modifiedPath = fmt.Sprintf("%s?%s", modifiedPath, encodedValues) - } else { - modifiedPath = fmt.Sprintf("%s&%s", modifiedPath, encodedValues) - } + log.Printf("[DEBUG] Added piped request body to HTTP %s request (path: %s)", method, modifiedPath) } else { - urlEncodedRequestBody = bytes.NewBufferString(encodedValues) - } + v, err := query.Values(requestBody) - log.Printf("[DEBUG] Added request body to HTTP %s request (path: %s) - Body: %s", method, path, encodedValues) + if err != nil { + return fmt.Errorf("Failed to encode HTTP %s request (path: %s) - Reason: %s", method, modifiedPath, err.Error()) + } + + encodedValues := v.Encode() + + if encodedValues != "" { + if method == hmGET || method == hmHEAD { + if !strings.Contains(modifiedPath, "?") { + modifiedPath = fmt.Sprintf("%s?%s", modifiedPath, encodedValues) + } else { + modifiedPath = fmt.Sprintf("%s&%s", modifiedPath, encodedValues) + } + } else { + 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, 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 { - 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") - if req.Method != hmGET && req.Method != hmHEAD { - req.Header.Add("Content-Type", "application/x-www-form-urlencoded") + if reqBodyType != "" { + req.Header.Add("Content-Type", reqBodyType) } err = c.AuthenticateRequest(req) @@ -125,7 +153,7 @@ func (c *VirtualEnvironmentClient) DoRequest(method, path string, requestBody in res, err := c.httpClient.Do(req) 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) @@ -138,7 +166,7 @@ func (c *VirtualEnvironmentClient) DoRequest(method, path string, requestBody in err = json.NewDecoder(res.Body).Decode(responseBody) 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 { switch res.StatusCode { 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: return fmt.Errorf("Received a HTTP %d response - Please verify that the specified credentials are valid", res.StatusCode) case 403: diff --git a/proxmox/virtual_environment_datastores.go b/proxmox/virtual_environment_datastores.go index 24b283dd..0d3f1b39 100644 --- a/proxmox/virtual_environment_datastores.go +++ b/proxmox/virtual_environment_datastores.go @@ -5,8 +5,11 @@ package proxmox import ( + "bytes" "errors" "fmt" + "io" + "mime/multipart" "sort" ) @@ -24,7 +27,7 @@ type VirtualEnvironmentDatastoreListResponseBody struct { 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 { Active *CustomBool `json:"active,omitempty"` ContentTypes *CustomCommaSeparatedList `json:"content,omitempty"` @@ -38,6 +41,20 @@ type VirtualEnvironmentDatastoreListResponseData struct { 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. func (c *VirtualEnvironmentClient) ListDatastores(nodeName string, d *VirtualEnvironmentDatastoreListRequestBody) ([]*VirtualEnvironmentDatastoreListResponseData, error) { resBody := &VirtualEnvironmentDatastoreListResponseBody{} @@ -57,3 +74,47 @@ func (c *VirtualEnvironmentClient) ListDatastores(nodeName string, d *VirtualEnv 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 +} diff --git a/resource_virtual_environment_file.go b/resource_virtual_environment_file.go new file mode 100644 index 00000000..6e9ac882 --- /dev/null +++ b/resource_virtual_environment_file.go @@ -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 +} diff --git a/resource_virtual_environment_file_test.go b/resource_virtual_environment_file_test.go new file mode 100644 index 00000000..5bce87ce --- /dev/null +++ b/resource_virtual_environment_file_test.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 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) + } + } +}