diff --git a/decort/constants.go b/decort/constants.go index 37c1e0f..e4ddc67 100644 --- a/decort/constants.go +++ b/decort/constants.go @@ -34,4 +34,10 @@ const MaxSshKeysPerCompute=12 const MaxExtraDisksPerCompute=12 // MaxNetworksPerCompute sets maximum number of vNICs per compute -const MaxNetworksPerCompute=8 \ No newline at end of file +const MaxNetworksPerCompute=8 + +// MaxCpusPerCompute sets maximum number of vCPUs per compute +const MaxCpusPerCompute=128 + +// MinRamPerCompute sets minimum amount of RAM per compute in MB +const MinRamPerCompute=128 \ No newline at end of file diff --git a/decort/data_source_compute.go b/decort/data_source_compute.go index 39cad9a..7fd2cc1 100644 --- a/decort/data_source_compute.go +++ b/decort/data_source_compute.go @@ -136,6 +136,21 @@ func parseBootDiskSize(disks []DiskRecord) int { return 0 } +func parseBootDiskId(disks []DiskRecord) uint { + // this return value will be used to d.Set("boot_disk_id",) item of dataSourceCompute schema + if len(disks) == 0 { + return 0 + } + + for _, value := range disks { + if value.Type == "B" { + return value.ID + } + } + + return 0 +} + // Parse the list of interfaces from compute/get response into a list of networks // attached to this compute func parseComputeInterfacesToNetworks(ifaces []InterfaceRecord) []interface{} { @@ -157,6 +172,7 @@ func parseComputeInterfacesToNetworks(ifaces []InterfaceRecord) []interface{} { elem["net_id"] = value.NetID elem["net_type"] = value.NetType elem["ip_address"] = value.IPAddress + elem["mac"] = value.MAC result[i] = elem } @@ -233,6 +249,7 @@ func flattenCompute(d *schema.ResourceData, compFacts string) error { d.Set("ram", model.Ram) // d.Set("boot_disk_size", model.BootDiskSize) - bootdiskSize key in API compute/get is always zero, so we set boot_disk_size in another way d.Set("boot_disk_size", parseBootDiskSize(model.Disks)) + d.Set("boot_disk_id", parseBootDiskId(model.Disks)) // we may need boot disk ID in resize operations d.Set("image_id", model.ImageID) d.Set("description", model.Desc) d.Set("status", model.Status) @@ -360,6 +377,12 @@ func dataSourceCompute() *schema.Resource { Description: "This compute instance boot disk size in GB.", }, + "boot_disk_id": { + Type: schema.TypeInt, + Computed: true, + Description: "This compute instance boot disk ID.", + }, + "extra_disks": { Type: schema.TypeList, Computed: true, diff --git a/decort/models_api.go b/decort/models_api.go index 1de82f4..ced4e52 100644 --- a/decort/models_api.go +++ b/decort/models_api.go @@ -282,6 +282,8 @@ const ComputeListAPI = "/restmachine/cloudapi/compute/list" type ComputeListResp []ComputeRecord +const ComputeResizeAPI = "/restmachine/cloudapi/compute/resize" + // // structures related to /cloudapi/compute/get // @@ -462,6 +464,12 @@ const ComputePfwDelAPI = "/restmachine/cloudapi/compute/pfwDel" // // structures related to /cloudapi/compute/net Attach/Detach API // +type ComputeNetMgmtRecord struct { // used to "cache" network specs when preparing to manage compute networks + ID int + Type string + IPAddress string + MAC string +} const ComputeNetAttachAPI = "/restmachine/cloudapi/compute/netAttach" const ComputeNetDetachAPI = "/restmachine/cloudapi/compute/netDetach" diff --git a/decort/network_subresource.go b/decort/network_subresource.go index 3d1edb7..984e6f1 100644 --- a/decort/network_subresource.go +++ b/decort/network_subresource.go @@ -51,6 +51,12 @@ func networkSubresourceSchemaMake() map[string]*schema.Schema { Description: "Optional IP address to assign to this connection. This IP should belong to the selected network and free for use.", }, + "mac": { + Type: schema.TypeString, + Computed: true, + Description: "MAC address associated with this connection. MAC address is assigned automatically.", + }, + } return rets } diff --git a/decort/resource_compute.go b/decort/resource_compute.go index e91b526..ea32207 100644 --- a/decort/resource_compute.go +++ b/decort/resource_compute.go @@ -1,5 +1,5 @@ /* -Copyright (c) 2019-2020 Digital Energy Cloud Solutions LLC. All Rights Reserved. +Copyright (c) 2019-2021 Digital Energy Cloud Solutions LLC. All Rights Reserved. Author: Sergey Shubin, , Licensed under the Apache License, Version 2.0 (the "License"); @@ -168,10 +168,77 @@ func resourceComputeRead(d *schema.ResourceData, m interface{}) error { } func resourceComputeUpdate(d *schema.ResourceData, m interface{}) error { - log.Debugf("resourceComputeUpdate: called for Compute name %s, RGID %d", - d.Get("name").(string), d.Get("rg_id").(int)) + log.Debugf("resourceComputeUpdate: called for Compute ID %s / name %s, RGID %d", + d.Id(), d.Get("name").(string), d.Get("rg_id").(int)) - log.Printf("resourceComputeUpdate: NOT IMPLEMENTED YET!") + controller := m.(*ControllerCfg) + + /* + 1. Resize CPU/RAM + 2. Resize (grow) boot disk + 3. Update extra disks + 4. Update networks + 5. Update port forwards + */ + + // 1. Resize CPU/RAM + params := &url.Values{} + doUpdate := false + params.Add("computeId", d.Id()) + + oldCpu, newCpu := d.GetChange("cpu") + if oldCpu.(int) != newCpu.(int) { + params.Add("cpu", fmt.Sprintf("%d", newCpu.(int))) + doUpdate = true + } else { + params.Add("cpu", "0") // no change to CPU allocation + } + + oldRam, newRam := d.GetChange("ram") + if oldRam.(int) != newRam.(int) { + params.Add("ram", fmt.Sprintf("%d", newRam.(int))) + doUpdate = true + } else { + params.Add("ram", "0") + } + + if doUpdate { + log.Debugf("resourceComputeUpdate: changing CPU %d -> %d and/or RAM %d -> %d", + oldCpu.(int), newCpu.(int), + oldRam.(int), newRam.(int)) + _, err := controller.decortAPICall("POST", ComputeResizeAPI, params) + if err != nil { + return err + } + } + + // 2. Resize (grow) Boot disk + oldSize, newSize := d.GetChange("boot_disk_size") + if oldSize.(int) < newSize.(int) { + bdsParams := &url.Values{} + bdsParams.Add("diskId", fmt.Sprintf("%d", d.Get("boot_disk_id").(int))) + bdsParams.Add("size", fmt.Sprintf("%d", newSize.(int))) + log.Debugf("resourceComputeUpdate: compute ID %s, boot disk ID %d resize %d -> %d", + d.Id(), d.Get("boot_disk_id").(int), oldSize.(int), newSize.(int)) + _, err := controller.decortAPICall("POST", DisksResizeAPI, params) + if err != nil { + return err + } + } else if oldSize.(int) > newSize.(int) { + log.Warnf("resourceComputeUpdate: compute ID %d - shrinking boot disk is not allowed", d.Id()) + } + + // 3. Calculate and apply changes to data disks + err := controller.utilityComputeExtraDisksConfigure(d, true) // pass do_delta = true to apply changes, if any + if err != nil { + return err + } + + // 4. Calculate and apply changes to network connections + err = controller.utilityComputeNetworksConfigure(d, true) // pass do_delta = true to apply changes, if any + if err != nil { + return err + } // we may reuse dataSourceComputeRead here as we maintain similarity // between Compute resource and Compute data source schemas @@ -267,14 +334,14 @@ func resourceCompute() *schema.Resource { "cpu": { Type: schema.TypeInt, Required: true, - ValidateFunc: validation.IntBetween(1, 64), + ValidateFunc: validation.IntBetween(1, MaxCpusPerCompute), Description: "Number of CPUs to allocate to this compute instance.", }, "ram": { Type: schema.TypeInt, Required: true, - ValidateFunc: validation.IntAtLeast(512), + ValidateFunc: validation.IntAtLeast(MinRamPerCompute), Description: "Amount of RAM in MB to allocate to this compute instance.", }, @@ -346,6 +413,12 @@ func resourceCompute() *schema.Resource { Description: "Name of the account this compute instance belongs to.", }, + "boot_disk_id": { + Type: schema.TypeInt, + Computed: true, + Description: "This compute instance boot disk ID.", + }, + /* "disks": { Type: schema.TypeList, diff --git a/decort/utility_compute.go b/decort/utility_compute.go index 34844b9..7490061 100644 --- a/decort/utility_compute.go +++ b/decort/utility_compute.go @@ -149,7 +149,7 @@ func (ctrl *ControllerCfg) utilityComputeExtraDisksConfigure(d *schema.ResourceD } if apiErrCount > 0 { - log.Errorf("utilityComputeExtraDisksConfigure: there were %d error(s) when managing disks on Compute ID %s. Last error was: %s", + log.Errorf("utilityComputeExtraDisksConfigure: there were %d error(s) when managing disks of Compute ID %s. Last error was: %s", apiErrCount, d.Id(), lastSavedError) return lastSavedError } @@ -162,29 +162,145 @@ func (ctrl *ControllerCfg) utilityComputeNetworksConfigure(d *schema.ResourceDat // "d" is filled with data according to computeResource schema, so extra networks config is retrieved via "network" key // If do_delta is true, this function will identify changes between new and existing specs for network and try to // update compute configuration accordingly + + /* argVal, argSet := d.GetOk("network") if !argSet || len(argVal.([]interface{})) < 1 { return nil } - net_list := argVal.([]interface{}) // network is ar array of maps; for keys see func networkSubresourceSchemaMake() definition + */ + + old_set, new_set := d.GetChange("network") + + oldNets := make([]interface{},0,0) + if old_set != nil { + oldNets = old_set.([]interface{}) // network is ar array of maps; for keys see func networkSubresourceSchemaMake() definition + } + + newNets := make([]interface{},0,0) + if new_set != nil { + newNets = new_set.([]interface{}) // network is ar array of maps; for keys see func networkSubresourceSchemaMake() definition + } - for _, net := range net_list { + apiErrCount := 0 + var lastSavedError error + + if !do_delta { + for _, net := range newNets { + urlValues := &url.Values{} + net_data := net.(map[string]interface{}) + urlValues.Add("computeId", d.Id()) + urlValues.Add("netType", net_data["net_type"].(string)) + urlValues.Add("netId", fmt.Sprintf("%d", net_data["net_id"].(int))) + ipaddr, ipSet := net_data["ip_address"] // "ip_address" key is optional + if ipSet { + urlValues.Add("ipAddr", ipaddr.(string)) + } + _, err := ctrl.decortAPICall("POST", ComputeNetAttachAPI, urlValues) + if err != nil { + // failed to attach network - partial resource update + apiErrCount++ + lastSavedError = err + } + } + + if apiErrCount > 0 { + log.Errorf("utilityComputeNetworksConfigure: there were %d error(s) when managing networks of Compute ID %s. Last error was: %s", + apiErrCount, d.Id(), lastSavedError) + return lastSavedError + } + return nil + } + + attachList := make([]ComputeNetMgmtRecord, 0, MaxNetworksPerCompute) + detachList := make([]ComputeNetMgmtRecord, 0, MaxNetworksPerCompute) + attIdx := 0 + detIdx := 0 + match := false + + for _, oRunner := range oldNets { + match = false + oSpecs := oRunner.(map[string]interface{}) + for _, nRunner := range newNets { + nSpecs := nRunner.(map[string]interface{}) + if oSpecs["net_id"].(int) == nSpecs["net_id"].(int) && oSpecs["net_type"].(string) == nSpecs["net_type"].(string) { + match = true + break + } + } + if !match { + detachList[attIdx].ID = oSpecs["net_id"].(int) + detachList[detIdx].Type = oSpecs["net_type"].(string) + detachList[detIdx].IPAddress = oSpecs["ip_address"].(string) + detachList[detIdx].MAC = oSpecs["mac"].(string) + detIdx++ + } + } + log.Debugf("utilityComputeNetworksConfigure: detach list has %d items for Compute ID %s", len(detachList), d.Id()) + + for _, nRunner := range newNets { + match = false + nSpecs := nRunner.(map[string]interface{}) + for _, oRunner := range oldNets { + oSpecs := oRunner.(map[string]interface{}) + if nSpecs["net_id"].(int) == oSpecs["net_id"].(int) && nSpecs["net_type"].(string) == oSpecs["net_type"].(string) { + match = true + break + } + } + if !match { + attachList[attIdx].ID = nSpecs["net_id"].(int) + attachList[detIdx].Type = nSpecs["net_type"].(string) + if nSpecs["ip_address"] != nil { + attachList[detIdx].IPAddress = nSpecs["ip_address"].(string) + } else { + attachList[detIdx].IPAddress = "" // make sure it is empty, if not coming from the schema + } + attIdx++ + } + } + log.Debugf("utilityComputeNetworksConfigure: attach list has %d items for Compute ID %s", len(attachList), d.Id()) + + for _, netRec := range detachList { + urlValues := &url.Values{} + urlValues.Add("computeId", d.Id()) + urlValues.Add("ipAddr", netRec.IPAddress) + urlValues.Add("mac", netRec.MAC) + _, err := ctrl.decortAPICall("POST", ComputeNetDetachAPI, urlValues) + if err != nil { + // failed to detach this network - there will be partial resource update + log.Debugf("utilityComputeNetworksConfigure: failed to detach net ID %d of type %s from Compute ID %s: %s", + netRec.ID, netRec.Type, d.Id(), err) + apiErrCount++ + lastSavedError = err + } + } + + for _, netRec := range attachList { urlValues := &url.Values{} - net_data := net.(map[string]interface{}) urlValues.Add("computeId", d.Id()) - urlValues.Add("netType", net_data["net_type"].(string)) - urlValues.Add("netId", fmt.Sprintf("%d", net_data["net_id"].(int))) - ipaddr, ipSet := net_data["ip_address"] // "ip_address" key is optional - if ipSet { - urlValues.Add("ipAddr", ipaddr.(string)) + urlValues.Add("netId", fmt.Sprintf("%d",netRec.ID)) + urlValues.Add("netType", netRec.Type) + if netRec.IPAddress != "" { + urlValues.Add("ipAddr", netRec.IPAddress) } _, err := ctrl.decortAPICall("POST", ComputeNetAttachAPI, urlValues) if err != nil { - // failed to attach network - partial resource update - return err + // failed to attach this network - there will be partial resource update + log.Debugf("utilityComputeNetworksConfigure: failed to attach net ID %d of type %s from Compute ID %s: %s", + netRec.ID, netRec.Type, d.Id(), err) + apiErrCount++ + lastSavedError = err } } + + if apiErrCount > 0 { + log.Errorf("utilityComputeNetworksConfigure: there were %d error(s) when managing networks of Compute ID %s. Last error was: %s", + apiErrCount, d.Id(), lastSavedError) + return lastSavedError + } + return nil }