diff --git a/CHANGELOG.md b/CHANGELOG.md index e3210dd7..19b946f8 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,7 @@ FEATURES: +* **New Data Source:** `proxmox_virtual_environment_datastores` * **New Data Source:** `proxmox_virtual_environment_group` * **New Data Source:** `proxmox_virtual_environment_groups` * **New Data Source:** `proxmox_virtual_environment_nodes` @@ -12,7 +13,6 @@ 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_group` * **New Resource:** `proxmox_virtual_environment_pool` * **New Resource:** `proxmox_virtual_environment_role` diff --git a/README.md b/README.md index b0f97e10..44b1a4dc 100644 --- a/README.md +++ b/README.md @@ -37,6 +37,22 @@ If you're building the provider, follow the instructions to [install it as a plu #### Virtual Environment +##### Datastores (proxmox_virtual_environment_datastores) + +###### Arguments +* `node_name` - (Required) A node name + +###### Attributes +* `active` - Whether the datastore is active +* `content_types` - The allowed content types +* `datastore_ids` - The datastore ids +* `enabled` - Whether the datastore is enabled +* `shared` - Whether the datastore is shared +* `space_available` - The available space in bytes +* `space_total` - The total space in bytes +* `space_used` - The used space in bytes +* `types` - The storage types + ##### Group (proxmox_virtual_environment_group) ###### Arguments diff --git a/data_source_virtual_environment_datastores.go b/data_source_virtual_environment_datastores.go new file mode 100644 index 00000000..1bf1966c --- /dev/null +++ b/data_source_virtual_environment_datastores.go @@ -0,0 +1,181 @@ +/* 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" + + "github.com/hashicorp/terraform/helper/schema" +) + +const ( + mkDataSourceVirtualEnvironmentDatastoresActive = "active" + mkDataSourceVirtualEnvironmentDatastoresContentTypes = "content_types" + mkDataSourceVirtualEnvironmentDatastoresDatastoreIDs = "datastore_ids" + mkDataSourceVirtualEnvironmentDatastoresEnabled = "enabled" + mkDataSourceVirtualEnvironmentDatastoresNodeName = "node_name" + mkDataSourceVirtualEnvironmentDatastoresShared = "shared" + mkDataSourceVirtualEnvironmentDatastoresSpaceAvailable = "space_available" + mkDataSourceVirtualEnvironmentDatastoresSpaceTotal = "space_total" + mkDataSourceVirtualEnvironmentDatastoresSpaceUsed = "space_used" + mkDataSourceVirtualEnvironmentDatastoresTypes = "types" +) + +func dataSourceVirtualEnvironmentDatastores() *schema.Resource { + return &schema.Resource{ + Schema: map[string]*schema.Schema{ + mkDataSourceVirtualEnvironmentDatastoresActive: &schema.Schema{ + Type: schema.TypeList, + Description: "Whether a datastore is active", + Computed: true, + Elem: &schema.Schema{Type: schema.TypeBool}, + }, + mkDataSourceVirtualEnvironmentDatastoresContentTypes: &schema.Schema{ + Type: schema.TypeList, + Description: "The allowed content types", + Computed: true, + Elem: &schema.Schema{ + Type: schema.TypeList, + Elem: &schema.Schema{Type: schema.TypeString}, + }, + }, + mkDataSourceVirtualEnvironmentDatastoresDatastoreIDs: &schema.Schema{ + Type: schema.TypeString, + Description: "The datastore id", + Computed: true, + }, + mkDataSourceVirtualEnvironmentDatastoresEnabled: &schema.Schema{ + Type: schema.TypeList, + Description: "Whether a datastore is enabled", + Computed: true, + Elem: &schema.Schema{Type: schema.TypeBool}, + }, + mkDataSourceVirtualEnvironmentDatastoresNodeName: &schema.Schema{ + Type: schema.TypeString, + Description: "The node id", + Required: true, + }, + mkDataSourceVirtualEnvironmentDatastoresShared: &schema.Schema{ + Type: schema.TypeList, + Description: "Whether a datastore is shared", + Computed: true, + Elem: &schema.Schema{Type: schema.TypeBool}, + }, + mkDataSourceVirtualEnvironmentDatastoresSpaceAvailable: &schema.Schema{ + Type: schema.TypeList, + Description: "The available space in bytes", + Computed: true, + Elem: &schema.Schema{Type: schema.TypeInt}, + }, + mkDataSourceVirtualEnvironmentDatastoresSpaceTotal: &schema.Schema{ + Type: schema.TypeList, + Description: "The total space in bytes", + Computed: true, + Elem: &schema.Schema{Type: schema.TypeInt}, + }, + mkDataSourceVirtualEnvironmentDatastoresSpaceUsed: &schema.Schema{ + Type: schema.TypeList, + Description: "The used space in bytes", + Computed: true, + Elem: &schema.Schema{Type: schema.TypeInt}, + }, + mkDataSourceVirtualEnvironmentDatastoresTypes: &schema.Schema{ + Type: schema.TypeList, + Description: "The storage type", + Computed: true, + Elem: &schema.Schema{Type: schema.TypeInt}, + }, + }, + Read: dataSourceVirtualEnvironmentDatastoresRead, + } +} + +func dataSourceVirtualEnvironmentDatastoresRead(d *schema.ResourceData, m interface{}) error { + config := m.(providerConfiguration) + veClient, err := config.GetVEClient() + + if err != nil { + return err + } + + nodeName := d.Get(mkDataSourceVirtualEnvironmentDatastoresNodeName).(string) + list, err := veClient.ListDatastores(nodeName, nil) + + if err != nil { + return err + } + + active := make([]interface{}, len(list)) + contentTypes := make([]interface{}, len(list)) + datastoreIDs := make([]interface{}, len(list)) + enabled := make([]interface{}, len(list)) + shared := make([]interface{}, len(list)) + spaceAvailable := make([]interface{}, len(list)) + spaceTotal := make([]interface{}, len(list)) + spaceUsed := make([]interface{}, len(list)) + types := make([]interface{}, len(list)) + + for i, v := range list { + if v.Active != nil { + active[i] = bool(*v.Active) + } else { + active[i] = true + } + + if v.ContentTypes != nil { + contentTypes[i] = []string(*v.ContentTypes) + } else { + contentTypes[i] = []string{} + } + + datastoreIDs[i] = v.ID + + if v.Enabled != nil { + enabled[i] = bool(*v.Enabled) + } else { + enabled[i] = true + } + + if v.Shared != nil { + shared[i] = bool(*v.Shared) + } else { + shared[i] = true + } + + if v.SpaceAvailable != nil { + spaceAvailable[i] = int(*v.SpaceAvailable) + } else { + spaceAvailable[i] = 0 + } + + if v.SpaceTotal != nil { + spaceTotal[i] = int(*v.SpaceTotal) + } else { + spaceTotal[i] = 0 + } + + if v.SpaceUsed != nil { + spaceUsed[i] = int(*v.SpaceUsed) + } else { + spaceUsed[i] = 0 + } + + types[i] = v.Type + } + + d.SetId(fmt.Sprintf("%s_datastores", nodeName)) + + d.Set(mkDataSourceVirtualEnvironmentDatastoresActive, active) + d.Set(mkDataSourceVirtualEnvironmentDatastoresContentTypes, contentTypes) + d.Set(mkDataSourceVirtualEnvironmentDatastoresDatastoreIDs, datastoreIDs) + d.Set(mkDataSourceVirtualEnvironmentDatastoresEnabled, enabled) + d.Set(mkDataSourceVirtualEnvironmentDatastoresShared, shared) + d.Set(mkDataSourceVirtualEnvironmentDatastoresSpaceAvailable, spaceAvailable) + d.Set(mkDataSourceVirtualEnvironmentDatastoresSpaceTotal, spaceTotal) + d.Set(mkDataSourceVirtualEnvironmentDatastoresSpaceUsed, spaceUsed) + d.Set(mkDataSourceVirtualEnvironmentDatastoresTypes, types) + + return nil +} diff --git a/data_source_virtual_environment_datastores_test.go b/data_source_virtual_environment_datastores_test.go new file mode 100644 index 00000000..3b314931 --- /dev/null +++ b/data_source_virtual_environment_datastores_test.go @@ -0,0 +1,45 @@ +/* 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" +) + +// TestDataSourceVirtualEnvironmentDatastoresInstantiation tests whether the DataSourceVirtualEnvironmentDatastores instance can be instantiated. +func TestDataSourceVirtualEnvironmentDatastoresInstantiation(t *testing.T) { + s := dataSourceVirtualEnvironmentDatastores() + + if s == nil { + t.Fatalf("Cannot instantiate dataSourceVirtualEnvironmentDatastores") + } +} + +// TestDataSourceVirtualEnvironmentDatastoresSchema tests the dataSourceVirtualEnvironmentDatastores schema. +func TestDataSourceVirtualEnvironmentDatastoresSchema(t *testing.T) { + s := dataSourceVirtualEnvironmentDatastores() + + attributeKeys := []string{ + mkDataSourceVirtualEnvironmentDatastoresActive, + mkDataSourceVirtualEnvironmentDatastoresContentTypes, + mkDataSourceVirtualEnvironmentDatastoresDatastoreIDs, + mkDataSourceVirtualEnvironmentDatastoresEnabled, + mkDataSourceVirtualEnvironmentDatastoresShared, + mkDataSourceVirtualEnvironmentDatastoresSpaceAvailable, + mkDataSourceVirtualEnvironmentDatastoresSpaceTotal, + mkDataSourceVirtualEnvironmentDatastoresSpaceUsed, + mkDataSourceVirtualEnvironmentDatastoresTypes, + } + + for _, v := range attributeKeys { + if s.Schema[v] == nil { + t.Fatalf("Error in dataSourceVirtualEnvironmentDatastores.Schema: Missing attribute \"%s\"", v) + } + + if s.Schema[v].Computed != true { + t.Fatalf("Error in dataSourceVirtualEnvironmentDatastores.Schema: Attribute \"%s\" is not computed", v) + } + } +} diff --git a/example/data_source_virtual_environment_datastores.tf b/example/data_source_virtual_environment_datastores.tf new file mode 100644 index 00000000..db9a52bd --- /dev/null +++ b/example/data_source_virtual_environment_datastores.tf @@ -0,0 +1,43 @@ +data "proxmox_virtual_environment_datastores" "example" { + node_name = "${data.proxmox_virtual_environment_nodes.example.names[0]}" +} + +output "data_proxmox_virtual_environment_datastores_example_active" { + value = "${data.proxmox_virtual_environment_datastores.example.active}" +} + +output "data_proxmox_virtual_environment_datastores_example_content_types" { + value = "${data.proxmox_virtual_environment_datastores.example.content_types}" +} + +output "data_proxmox_virtual_environment_datastores_example_datastore_ids" { + value = "${data.proxmox_virtual_environment_datastores.example.datastore_ids}" +} + +output "data_proxmox_virtual_environment_datastores_example_enabled" { + value = "${data.proxmox_virtual_environment_datastores.example.enabled}" +} + +output "data_proxmox_virtual_environment_datastores_example_node_name" { + value = "${data.proxmox_virtual_environment_datastores.example.node_name}" +} + +output "data_proxmox_virtual_environment_datastores_example_shared" { + value = "${data.proxmox_virtual_environment_datastores.example.shared}" +} + +output "data_proxmox_virtual_environment_datastores_example_space_available" { + value = "${data.proxmox_virtual_environment_datastores.example.space_available}" +} + +output "data_proxmox_virtual_environment_datastores_example_space_total" { + value = "${data.proxmox_virtual_environment_datastores.example.space_total}" +} + +output "data_proxmox_virtual_environment_datastores_example_space_used" { + value = "${data.proxmox_virtual_environment_datastores.example.space_used}" +} + +output "data_proxmox_virtual_environment_datastores_example_types" { + value = "${data.proxmox_virtual_environment_datastores.example.types}" +} diff --git a/provider.go b/provider.go index 6443aa7f..1d21c969 100644 --- a/provider.go +++ b/provider.go @@ -28,16 +28,17 @@ func Provider() *schema.Provider { return &schema.Provider{ ConfigureFunc: providerConfigure, DataSourcesMap: map[string]*schema.Resource{ - "proxmox_virtual_environment_group": dataSourceVirtualEnvironmentGroup(), - "proxmox_virtual_environment_groups": dataSourceVirtualEnvironmentGroups(), - "proxmox_virtual_environment_nodes": dataSourceVirtualEnvironmentNodes(), - "proxmox_virtual_environment_pool": dataSourceVirtualEnvironmentPool(), - "proxmox_virtual_environment_pools": dataSourceVirtualEnvironmentPools(), - "proxmox_virtual_environment_role": dataSourceVirtualEnvironmentRole(), - "proxmox_virtual_environment_roles": dataSourceVirtualEnvironmentRoles(), - "proxmox_virtual_environment_user": dataSourceVirtualEnvironmentUser(), - "proxmox_virtual_environment_users": dataSourceVirtualEnvironmentUsers(), - "proxmox_virtual_environment_version": dataSourceVirtualEnvironmentVersion(), + "proxmox_virtual_environment_datastores": dataSourceVirtualEnvironmentDatastores(), + "proxmox_virtual_environment_group": dataSourceVirtualEnvironmentGroup(), + "proxmox_virtual_environment_groups": dataSourceVirtualEnvironmentGroups(), + "proxmox_virtual_environment_nodes": dataSourceVirtualEnvironmentNodes(), + "proxmox_virtual_environment_pool": dataSourceVirtualEnvironmentPool(), + "proxmox_virtual_environment_pools": dataSourceVirtualEnvironmentPools(), + "proxmox_virtual_environment_role": dataSourceVirtualEnvironmentRole(), + "proxmox_virtual_environment_roles": dataSourceVirtualEnvironmentRoles(), + "proxmox_virtual_environment_user": dataSourceVirtualEnvironmentUser(), + "proxmox_virtual_environment_users": dataSourceVirtualEnvironmentUsers(), + "proxmox_virtual_environment_version": dataSourceVirtualEnvironmentVersion(), }, ResourcesMap: map[string]*schema.Resource{ "proxmox_virtual_environment_group": resourceVirtualEnvironmentGroup(), diff --git a/proxmox/common_types.go b/proxmox/common_types.go index 58c155b0..864058d4 100644 --- a/proxmox/common_types.go +++ b/proxmox/common_types.go @@ -18,6 +18,9 @@ type CustomBool bool // CustomPrivileges allows a JSON object of privileges to also be a string array. type CustomPrivileges []string +// CustomCommaSeparatedList allows a JSON string to also be a string array. +type CustomCommaSeparatedList []string + // CustomTimestamp allows a JSON boolean value to also be a unix timestamp. type CustomTimestamp time.Time @@ -42,6 +45,28 @@ func (r *CustomBool) UnmarshalJSON(b []byte) error { return nil } +// MarshalJSON converts a boolean to a JSON value. +func (r *CustomCommaSeparatedList) MarshalJSON() ([]byte, error) { + s := strings.Join(*r, ",") + + return json.Marshal(s) +} + +// UnmarshalJSON converts a JSON value to a boolean. +func (r *CustomCommaSeparatedList) UnmarshalJSON(b []byte) error { + var s string + + err := json.Unmarshal(b, &s) + + if err != nil { + return err + } + + *r = strings.Split(s, ",") + + return nil +} + // MarshalJSON converts a boolean to a JSON value. func (r *CustomPrivileges) MarshalJSON() ([]byte, error) { var privileges map[string]CustomBool diff --git a/proxmox/virtual_environment.go b/proxmox/virtual_environment.go index 500d8a72..ebe50461 100644 --- a/proxmox/virtual_environment.go +++ b/proxmox/virtual_environment.go @@ -79,13 +79,10 @@ func NewVirtualEnvironmentClient(endpoint, username, password string, insecure b func (c *VirtualEnvironmentClient) DoRequest(method, path string, requestBody interface{}, responseBody interface{}) error { log.Printf("[DEBUG] Performing HTTP %s request (path: %s)", method, path) + modifiedPath := path urlEncodedRequestBody := new(bytes.Buffer) if requestBody != nil { - if method == hmGET || method == hmHEAD { - return fmt.Errorf("A request body must not be specified for %s/%s requests", hmGET, hmHEAD) - } - v, err := query.Values(requestBody) if err != nil { @@ -93,12 +90,21 @@ func (c *VirtualEnvironmentClient) DoRequest(method, path string, requestBody in } encodedValues := v.Encode() - urlEncodedRequestBody = bytes.NewBufferString(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 { + urlEncodedRequestBody = bytes.NewBufferString(encodedValues) + } log.Printf("[DEBUG] Added request body to HTTP %s request (path: %s) - Body: %s", method, path, encodedValues) } - req, err := http.NewRequest(method, fmt.Sprintf("%s/%s/%s", c.Endpoint, basePathJSONAPI, path), urlEncodedRequestBody) + req, err := http.NewRequest(method, fmt.Sprintf("%s/%s/%s", c.Endpoint, basePathJSONAPI, modifiedPath), urlEncodedRequestBody) if err != nil { return fmt.Errorf("Failed to create HTTP %s request (path: %s) - Reason: %s", method, path, err.Error()) diff --git a/proxmox/virtual_environment_datastores.go b/proxmox/virtual_environment_datastores.go new file mode 100644 index 00000000..24b283dd --- /dev/null +++ b/proxmox/virtual_environment_datastores.go @@ -0,0 +1,59 @@ +/* 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 ( + "errors" + "fmt" + "sort" +) + +// VirtualEnvironmentDatastoreListRequestBody contains the body for a datastore list request. +type VirtualEnvironmentDatastoreListRequestBody struct { + ContentTypes CustomCommaSeparatedList `json:"content,omitempty" url:"content,omitempty,comma"` + Enabled *CustomBool `json:"enabled,omitempty" url:"enabled,omitempty,int"` + Format *CustomBool `json:"format,omitempty" url:"format,omitempty,int"` + ID *string `json:"storage,omitempty" url:"storage,omitempty"` + Target *string `json:"target,omitempty" url:"target,omitempty"` +} + +// VirtualEnvironmentDatastoreListResponseBody contains the body from a datastore list response. +type VirtualEnvironmentDatastoreListResponseBody struct { + Data []*VirtualEnvironmentDatastoreListResponseData `json:"data,omitempty"` +} + +// VirtualEnvironmentDatastoreListResponseData contains the data from a node list response. +type VirtualEnvironmentDatastoreListResponseData struct { + Active *CustomBool `json:"active,omitempty"` + ContentTypes *CustomCommaSeparatedList `json:"content,omitempty"` + Enabled *CustomBool `json:"enabled,omitempty"` + ID string `json:"storage,omitempty"` + Shared *CustomBool `json:"shared,omitempty"` + SpaceAvailable *int `json:"avail,omitempty"` + SpaceTotal *int `json:"total,omitempty"` + SpaceUsed *int `json:"used,omitempty"` + SpaceUsedPercentage *float64 `json:"used_fraction,omitempty"` + Type string `json:"type,omitempty"` +} + +// ListDatastores retrieves a list of nodes. +func (c *VirtualEnvironmentClient) ListDatastores(nodeName string, d *VirtualEnvironmentDatastoreListRequestBody) ([]*VirtualEnvironmentDatastoreListResponseData, error) { + resBody := &VirtualEnvironmentDatastoreListResponseBody{} + err := c.DoRequest(hmGET, fmt.Sprintf("nodes/%s/storage", 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 +}