diff --git a/decort/data_source_compute.go b/decort/data_source_compute.go index ea92243..c7570db 100644 --- a/decort/data_source_compute.go +++ b/decort/data_source_compute.go @@ -299,6 +299,8 @@ func dataSourceCompute() *schema.Resource { Description: "Name of this compute instance. NOTE: this parameter is case sensitive.", }, + // TODO: consider removing compute_id from the schema, as it not practical to call this data provider if + // corresponding compute ID is already known "compute_id": { Type: schema.TypeInt, Optional: true, diff --git a/decort/data_source_vins.go b/decort/data_source_vins.go index 91a4051..efa9d04 100644 --- a/decort/data_source_vins.go +++ b/decort/data_source_vins.go @@ -51,12 +51,14 @@ func flattenVins(d *schema.ResourceData, vins_facts string) error { vinsRecord.Name, vinsRecord.ID, vinsRecord.AccountID, vinsRecord.RgID) d.SetId(fmt.Sprintf("%d", vinsRecord.ID)) + d.Set("name", vinsRecord.Name) d.Set("account_id", vinsRecord.AccountID) d.Set("account_name", vinsRecord.AccountName) err = d.Set("rg_id", vinsRecord.RgID) d.Set("description", vinsRecord.Desc) d.Set("ipcidr", vinsRecord.IPCidr) + noExtNetConnection := true for _, value := range vinsRecord.VNFs { if value.Type == "GW" { log.Debugf("flattenVins: discovered GW VNF ID %d in ViNS ID %d", value.ID, vinsRecord.ID) @@ -69,10 +71,16 @@ func flattenVins(d *schema.ResourceData, vins_facts string) error { } else { return fmt.Errorf("Failed to unmarshal VNF GW Config - structure is invalid.") } + noExtNetConnection = false break } } + if noExtNetConnection { + d.Set("ext_ip_addr", "") + d.Set("ext_net_id", -1) + } + log.Debugf("flattenVins: EXTRA CHECK - schema rg_id=%d, ext_net_id=%d", d.Get("rg_id").(int), d.Get("ext_net_id").(int)) return nil diff --git a/decort/models_api.go b/decort/models_api.go index 0b3da84..349e49f 100644 --- a/decort/models_api.go +++ b/decort/models_api.go @@ -562,7 +562,12 @@ type VinsRecord struct { // represents part of the response from API vins/get const VinsGetAPI = "/restmachine/cloudapi/vins/get" -const VinsCreateAPI = "/restmachine/cloudapi/vins/create" +const VinsCreateInAccountAPI = "/restmachine/cloudapi/vins/createInAccount" +const VinsCreateInRgAPI = "/restmachine/cloudapi/vins/createInRG" + +const VinsExtNetConnect = "/restmachine/cloudapi/vins/extNetConnect" +const VinsExtNetDisconnect = "/restmachine/cloudapi/vins/extNetDisconnect" + const VinsDeleteAPI = "/restmachine/cloudapi/vins/delete" // diff --git a/decort/provider.go b/decort/provider.go index 54be5d0..0dc8876 100644 --- a/decort/provider.go +++ b/decort/provider.go @@ -102,7 +102,7 @@ func Provider() *schema.Provider { "decort_resgroup": resourceResgroup(), "decort_kvmvm": resourceCompute(), "decort_disk": resourceDisk(), - // "decort_vins": resourceVins(), + "decort_vins": resourceVins(), // "decort_pfw": resourcePfw(), }, diff --git a/decort/resource_compute.go b/decort/resource_compute.go index c821cef..050e9bc 100644 --- a/decort/resource_compute.go +++ b/decort/resource_compute.go @@ -303,7 +303,7 @@ func resourceComputeDelete(d *schema.ResourceData, m interface{}) error { params := &url.Values{} params.Add("computeId", d.Id()) - params.Add("permanently", "true") + params.Add("permanently", "1") controller := m.(*ControllerCfg) _, err = controller.decortAPICall("POST", ComputeDeleteAPI, params) @@ -435,7 +435,7 @@ func resourceCompute() *schema.Resource { "description": { Type: schema.TypeString, Optional: true, - Description: "Description of this compute instance.", + Description: "Optional text description of this compute instance.", }, diff --git a/decort/resource_disk.go b/decort/resource_disk.go index a38631a..cac103e 100644 --- a/decort/resource_disk.go +++ b/decort/resource_disk.go @@ -157,14 +157,14 @@ func resourceDiskDelete(d *schema.ResourceData, m interface{}) error { params := &url.Values{} params.Add("diskId", d.Id()) - // NOTE: we are not force-detaching disk from a compute (if attached) this protecting + // NOTE: we are not force-detaching disk from a compute (if attached) thus protecting // data that may be on that disk from destruction. // However, this may change in the future, as TF state management logic may want // to delete disk resource BEFORE it is detached from compute instance, and, while // perfectly OK from data preservation viewpoint, this is breaking expected TF workflow // in the eyes of an experienced TF user - params.Add("detach", "false") - params.Add("permanently", "true") + params.Add("detach", "0") + params.Add("permanently", "1") controller := m.(*ControllerCfg) _, err = controller.decortAPICall("POST", DisksDeleteAPI, params) diff --git a/decort/resource_rg.go b/decort/resource_rg.go index 1856381..8daceaf 100644 --- a/decort/resource_rg.go +++ b/decort/resource_rg.go @@ -237,8 +237,8 @@ func resourceResgroupDelete(d *schema.ResourceData, m interface{}) error { url_values := &url.Values{} url_values.Add("rgId", d.Id()) - url_values.Add("force", "true") - url_values.Add("permanently", "true") + url_values.Add("force", "1") + url_values.Add("permanently", "1") url_values.Add("reason", "Destroyed by DECORT Terraform provider") controller := m.(*ControllerCfg) diff --git a/decort/resource_vins.go b/decort/resource_vins.go new file mode 100644 index 0000000..1da5e93 --- /dev/null +++ b/decort/resource_vins.go @@ -0,0 +1,299 @@ +/* +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"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +/* +This file is part of Terraform (by Hashicorp) provider for Digital Energy Cloud Orchestration +Technology platfom. + +Visit https://github.com/rudecs/terraform-provider-decort for full source code package and updates. +*/ + +package decort + +import ( + // "encoding/json" + "fmt" + "net/url" + "strconv" + + log "github.com/sirupsen/logrus" + + "github.com/hashicorp/terraform-plugin-sdk/helper/schema" + // "github.com/hashicorp/terraform-plugin-sdk/helper/validation" +) + +func ipcidrDiffSupperss(key, oldVal, newVal string, d *schema.ResourceData) bool { + if oldVal == "" && newVal != "" { + // if old value for "ipcidr" resource is empty string, it means that we are creating new ViNS + // and there is a chance that the user will want specific IP address range for this ViNS - + // check if "ipcidr" is explicitly set in TF file to a non-empty string. + log.Debugf("ipcidrDiffSupperss: key=%s, oldVal=%q, newVal=%q -> suppress=FALSE", key, oldVal, newVal) + return false // there is a difference between stored and new value + } + log.Debugf("ipcidrDiffSupperss: key=%s, oldVal=%q, newVal=%q -> suppress=TRUE", key, oldVal, newVal) + return true // suppress difference +} + +func resourceVinsCreate(d *schema.ResourceData, m interface{}) error { + log.Debugf("resourceVinsCreate: called for ViNS name %s, Account ID %d, RG ID %d", + d.Get("name").(string), d.Get("account_id").(int), d.Get("rg_id").(int)) + + apiToCall := VinsCreateInAccountAPI + + controller := m.(*ControllerCfg) + urlValues := &url.Values{} + + urlValues.Add("name", d.Get("name").(string)) + + argVal, argSet := d.GetOk("rg_id") + if argSet && argVal.(int) > 0 { + apiToCall = VinsCreateInRgAPI + urlValues.Add("rgId", fmt.Sprintf("%d", argVal.(int))) + } else { + // RG ID either not set at all or set to 0 - user may want ViNS at account level + argVal, argSet = d.GetOk("account_id") + if !argSet || argVal.(int) <= 0 { + // No valid Account ID (and no RG ID either) - cannot create ViNS + return fmt.Errorf("resourceVinsCreate: ViNS name %s - no valid account and/or resource group ID specified", d.Id()) + } + urlValues.Add("accountId", fmt.Sprintf("%d", argVal.(int))) + } + + argVal, argSet = d.GetOk("ext_net_id") // NB: even if ext_net_id value is explicitly set to 0, argSet = false anyway + if argSet { + if argVal.(int) > 0 { + // connect to specific external network + urlValues.Add("extNetId", fmt.Sprintf("%d", argVal.(int))) + // in case of specific ext net connection user may also want a particular IP address + argVal, argSet = d.GetOk("ext_net_ip") + if argSet && argVal.(string) != "" { + urlValues.Add("extIp", argVal.(string)) + } + } else { + // ext_net_id is set to a negative value - connect to default external network + // no particular IP address selection in this case + urlValues.Add("extNetId", "0") + } + } + + argVal, argSet = d.GetOk("ipcidr") + if argSet && argVal.(string) != "" { + log.Debugf("resourceVinsCreate: ipcidr is set to %s", argVal.(string)) + urlValues.Add("ipcidr", argVal.(string)) + } + + argVal, argSet = d.GetOk("description") + if argSet { + urlValues.Add("desc", argVal.(string)) + } + + apiResp, err := controller.decortAPICall("POST", apiToCall, urlValues) + if err != nil { + return err + } + + d.SetId(apiResp) // update ID of the resource to tell Terraform that the ViNS resource exists + vinsId, _ := strconv.Atoi(apiResp) + + log.Debugf("resourceVinsCreate: new ViNS ID / name %d / %s creation sequence complete", vinsId, d.Get("name").(string)) + + // We may reuse dataSourceVinsRead here as we maintain similarity + // between ViNS resource and ViNS data source schemas + // ViNS resource read function will also update resource ID on success, so that Terraform + // will know the resource exists (however, we already did it a few lines before) + return dataSourceVinsRead(d, m) +} + +func resourceVinsRead(d *schema.ResourceData, m interface{}) error { + vinsFacts, err := utilityVinsCheckPresence(d, m) + if vinsFacts == "" { + // if empty string is returned from utilityVinsCheckPresence then there is no + // such ViNS and err tells so - just return it to the calling party + d.SetId("") // ensure ID is empty + return err + } + + return flattenVins(d, vinsFacts) +} + +func resourceVinsUpdate(d *schema.ResourceData, m interface{}) error { + + return fmt.Errorf("resourceVinsUpdate: method not implemnted yet - ViNS ID %s", d.Id()) + + /* + log.Debugf("resourceVinsUpdate: called for ViNS ID / name %s / %s, Account ID %d", + d.Id(), d.Get("name").(string), d.Get("account_id").(int)) + + d.Partial(true) + + controller := m.(*ControllerCfg) + + + oldName, newName := d.GetChange("name") + if oldName.(string) != newName.(string) { + log.Debugf("resourceVinsUpdate: renaming ViNS ID %d - %s -> %s", + d.Get("disk_id").(int), oldName.(string), newName.(string)) + renameParams := &url.Values{} + renameParams.Add("vinsId", d.Id()) + renameParams.Add("name", newName.(string)) + _, err := controller.decortAPICall("POST", VinsRenameAPI, renameParams) + if err != nil { + return err + } + d.SetPartial("name") + } + + d.Partial(false) + */ + + // we may reuse dataSourceVinsRead here as we maintain similarity + // between Compute resource and Compute data source schemas + // return dataSourceVinsRead(d, m) +} + +func resourceVinsDelete(d *schema.ResourceData, m interface{}) error { + log.Debugf("resourceVinsDelete: called for ViNS ID / name %s / %s, Account ID %d, RG ID %d", + d.Id(), d.Get("name").(string), d.Get("account_id").(int), d.Get("rg_id").(int)) + + vinsFacts, err := utilityVinsCheckPresence(d, m) + if vinsFacts == "" { + // the specified ViNS does not exist - in this case according to Terraform best practice + // we exit from Destroy method without error + return nil + } + + params := &url.Values{} + params.Add("vinsId", d.Id()) + params.Add("force", "1") // disconnect all computes before deleting ViNS + params.Add("permanently", "1") // delete ViNS immediately bypassing recycle bin + + controller := m.(*ControllerCfg) + _, err = controller.decortAPICall("POST", VinsDeleteAPI, params) + if err != nil { + return err + } + + return nil +} + +func resourceVinsExists(d *schema.ResourceData, m interface{}) (bool, error) { + // Reminder: according to Terraform rules, this function should not modify its ResourceData argument + log.Debugf("resourceVinsExists: called for ViNS name %s, Account ID %d, RG ID %d", + d.Get("name").(string), d.Get("account_id").(int), d.Get("rg_id").(int)) + + vinsFacts, err := utilityVinsCheckPresence(d, m) + if vinsFacts == "" { + if err != nil { + return false, err + } + return false, nil + } + return true, nil +} + +func resourceVinsSchemaMake() map[string]*schema.Schema { + rets := map[string]*schema.Schema{ + "name": { + Type: schema.TypeString, + Required: true, + Description: "Name of the ViNS. Names are case sensitive and unique within the context of an account or resource group.", + }, + + /* we do not need ViNS ID as an argument because if we already know this ID, it is not practical to call resource provider. + Resource Import will work anyway, as it obtains the ID of ViNS to be imported through another mechanism. + "vins_id": { + Type: schema.TypeInt, + Optional: true, + Description: "Unique ID of the ViNS. If ViNS ID is specified, then ViNS name, rg_id and account_id are ignored.", + }, + */ + + "rg_id": { + Type: schema.TypeInt, + Optional: true, + Description: "ID of the resource group, where this ViNS belongs to. Non-zero for ViNS created at resource group level, 0 otherwise.", + }, + + "account_id": { + Type: schema.TypeInt, + Required: true, + Description: "ID of the account, which this ViNS belongs to. For ViNS created at account level, resource group ID is 0.", + }, + + "description": { + Type: schema.TypeString, + Optional: true, + Default: "", + Description: "Optional user-defined text description of this ViNS.", + }, + + "ext_net_id": { + Type: schema.TypeInt, + Optional: true, + Description: "ID of the external network this ViNS is connected to (set to 0 if no external connection required, -1 to connect to default external network).", + }, + + "ext_ip_addr": { + Type: schema.TypeString, + Optional: true, + Description: "IP address of the external connection (valid for ViNS connected to external network, ignored otherwise).", + }, + + "ipcidr": { + Type: schema.TypeString, + Optional: true, + DiffSuppressFunc: ipcidrDiffSupperss, + Description: "Network address to use by this ViNS.", + }, + + // the rest of attributes are computed + "account_name": { + Type: schema.TypeString, + Computed: true, + Description: "Name of the account, which this ViNS belongs to.", + }, + + } + + return rets +} + +func resourceVins() *schema.Resource { + return &schema.Resource{ + SchemaVersion: 1, + + Create: resourceVinsCreate, + Read: resourceVinsRead, + Update: resourceVinsUpdate, + Delete: resourceVinsDelete, + Exists: resourceVinsExists, + + Importer: &schema.ResourceImporter{ + State: schema.ImportStatePassthrough, + }, + + Timeouts: &schema.ResourceTimeout{ + Create: &Timeout180s, + Read: &Timeout30s, + Update: &Timeout180s, + Delete: &Timeout60s, + Default: &Timeout60s, + }, + + Schema: resourceVinsSchemaMake(), + } +} diff --git a/decort/utility_vins.go b/decort/utility_vins.go index 22c3680..f64cc79 100644 --- a/decort/utility_vins.go +++ b/decort/utility_vins.go @@ -28,6 +28,7 @@ import ( "encoding/json" "fmt" "net/url" + "strconv" log "github.com/sirupsen/logrus" @@ -75,6 +76,34 @@ func utilityVinsCheckPresence(d *schema.ResourceData, m interface{}) (string, er controller := m.(*ControllerCfg) urlValues := &url.Values{} + // make it possible to use "read" & "check presence" functions with ViNS ID set so + // that Import of ViNS resource is possible + idSet := false + theId, err := strconv.Atoi(d.Id()) + if err != nil || theId <= 0 { + vinsId, argSet := d.GetOk("vins_id") // NB: vins_id is NOT present in vinsResource schema! + if argSet { + theId = vinsId.(int) + idSet = true + } + } else { + idSet = true + } + + if idSet { + // ViNS ID is specified, try to get compute instance straight by this ID + log.Debugf("utilityVinsCheckPresence: locating ViNS by its ID %d", theId) + urlValues.Add("vinsId", fmt.Sprintf("%d", theId)) + vinsFacts, err := controller.decortAPICall("POST", VinsGetAPI, urlValues) + if err != nil { + return "", err + } + return vinsFacts, nil + } + + // ID was not set in the schema upon entering this function - work through ViNS name + // and Account / RG ID + vinsName, argSet := d.GetOk("name") if !argSet { // if ViNS name is not set. then we cannot locate ViNS @@ -82,11 +111,11 @@ func utilityVinsCheckPresence(d *schema.ResourceData, m interface{}) (string, er } urlValues.Add("name", vinsName.(string)) urlValues.Add("show_all", "false") - log.Debugf("utilityVinsCheckPresence: locating ViNS %s", vinsName.(string)) + log.Debugf("utilityVinsCheckPresence: preparing to locate ViNS name %s", vinsName.(string)) rgId, rgSet := d.GetOk("rg_id") if rgSet { - log.Debugf("utilityVinsCheckPresence: limiting ViNS t search to RG ID %d", rgId.(int)) + log.Debugf("utilityVinsCheckPresence: limiting ViNS search to RG ID %d", rgId.(int)) urlValues.Add("rgId", fmt.Sprintf("%d", rgId.(int))) } @@ -133,5 +162,5 @@ func utilityVinsCheckPresence(d *schema.ResourceData, m interface{}) (string, er } } - return "", fmt.Errorf("Cannot find ViNS name %s. Check name and/or RG ID & Account ID", vinsName.(string)) + return "", fmt.Errorf("Cannot find ViNS name %s. Check name and/or RG ID & Account ID and your access rights", vinsName.(string)) }