diff --git a/README.md b/README.md index 9e6eec6..5379110 100644 --- a/README.md +++ b/README.md @@ -1,7 +1,7 @@ # terraform-provider-decort Terraform provider for Digital Energy Cloud Orchestration Technology (DECORT) platform -NOTE: provider rc-1.25 is designed for DECORT API 3.7.x. For older API versions please use: +NOTE: provider rc-1.30 is designed for DECORT API 3.7.x. For older API versions please use: - DECORT API 3.6.x versions - provider version rc-1.10 - DECORT API versions prior to 3.6.0 - Terraform DECS provider (https://github.com/rudecs/terraform-provider-decs) diff --git a/decort/data_source_compute.go b/decort/data_source_compute.go index 1492200..546dd0b 100644 --- a/decort/data_source_compute.go +++ b/decort/data_source_compute.go @@ -28,6 +28,7 @@ import ( "encoding/json" "fmt" // "net/url" + // "strconv" log "github.com/sirupsen/logrus" @@ -151,7 +152,7 @@ func parseBootDiskId(disks []DiskRecord) uint { // Parse the list of interfaces from compute/get response into a list of networks // attached to this compute -func parseComputeInterfacesToNetworks(ifaces []InterfaceRecord) []interface{} { +func parseComputeInterfacesToNetworks(ifaces []InterfaceRecord, pfwVinsID int, pfwRules []map[string]interface{}) []interface{} { // return value will be used to d.Set("network") item of dataSourceCompute schema length := len(ifaces) log.Debugf("parseComputeInterfacesToNetworks: called for %d ifaces", length) @@ -167,6 +168,14 @@ func parseComputeInterfacesToNetworks(ifaces []InterfaceRecord) []interface{} { elem["ip_address"] = value.IPAddress elem["mac"] = value.MAC + if value.NetType == "VINS" && len(pfwRules) > 0 && pfwVinsID == value.NetID { + // we have non-empty port forward rules that seem to be relevant to the current + // network segment - set "pfw_rule" element accordingly + log.Debugf("parseComputeInterfacesToNetworks: setting pfw_rule attributes on network block for ViNS ID %d", + value.NetID) + elem["pfw_rule"] = pfwRules + } + // log.Debugf(" element %d: net_id=%d, net_type=%s", i, value.NetID, value.NetType) result = append(result, elem) @@ -240,7 +249,7 @@ func parseComputeInterfaces(ifaces []InterfaceRecord) []map[string]interface{} { return result } -func flattenCompute(d *schema.ResourceData, compFacts string) error { +func flattenCompute(d *schema.ResourceData, compFacts string, pfwVinsID int, pfwRules []map[string]interface{}) error { // This function expects that compFacts string contains response from API compute/get, // i.e. detailed information about compute instance. // @@ -283,7 +292,7 @@ func flattenCompute(d *schema.ResourceData, compFacts string) error { if len(model.Interfaces) > 0 { log.Debugf("flattenCompute: calling parseComputeInterfacesToNetworks for %d interfaces", len(model.Interfaces)) - if err = d.Set("network", parseComputeInterfacesToNetworks(model.Interfaces)); err != nil { + if err = d.Set("network", parseComputeInterfacesToNetworks(model.Interfaces, pfwVinsID, pfwRules)); err != nil { return err } } @@ -299,15 +308,23 @@ func flattenCompute(d *schema.ResourceData, compFacts string) error { } func dataSourceComputeRead(d *schema.ResourceData, m interface{}) error { - compFacts, err := utilityComputeCheckPresence(d, m) + compID, compFacts, err := utilityComputeCheckPresence(d, m) if compFacts == "" { - // if empty string is returned from utilityComputeCheckPresence then there is no - // such Compute and err tells so - just return it to the calling party + // if empty compFacts is returned from utilityComputeCheckPresence and err=nil + // it means that there is no such Compute; + // In any other case non-nil error will be reported. d.SetId("") // ensure ID is empty return err } - return flattenCompute(d, compFacts) + vinsID, pfwRules, err := utilityComputePfwGet(compID, m) + if err != nil { + log.Errorf("dataSourceComputeRead: there was error calling utilityComputePfwGet for compute ID %s: %s", + d.Id(), err) + return err + } + + return flattenCompute(d, compFacts, vinsID, pfwRules) } func dataSourceCompute() *schema.Resource { diff --git a/decort/models_api.go b/decort/models_api.go index cd44e58..1dc3ba7 100644 --- a/decort/models_api.go +++ b/decort/models_api.go @@ -444,9 +444,17 @@ const AccountsListAPI = "/restmachine/cloudapi/account/list" // returns list of type AccountsListResp []AccountRecord // -// structures related to /cloudapi/portforwarding/list API +// structures related to /cloudapi/compute/pfwlLst API // -type PfwRecord struct { + +// Note that if there are port forwarding rules for compute, then compute/pfwList response +// will contain a list which starts with prefix (see PfwPrefixRecord) and then contains +// one or more rule records (see PfwRuleRecord) +type PfwPrefixRecord struct { + VinsID int `json:"vinsId"` + VinsName string `json:"vinsName"` +} +type PfwRuleRecord struct { ID int `json:"id"` LocalIP string `json:"localIp"` LocalPort int `json:"localPort"` @@ -458,8 +466,6 @@ type PfwRecord struct { const ComputePfwListAPI = "/restmachine/cloudapi/compute/pfwList" -type ComputePfwListResp []PfwRecord - const ComputePfwAddAPI = "/restmachine/cloudapi/compute/pfwAdd" const ComputePfwDelAPI = "/restmachine/cloudapi/compute/pfwDel" @@ -538,6 +544,8 @@ type VnfRecord struct { AccountID int `json:"accountId"` Type string `json:"type"` // "DHCP", "NAT", "GW" etc Config map[string]interface{} `json:"config"` // NOTE: VNF specs vary by VNF type + Status string `json:"status"` + TechStatus string `json:"techStatus"` } type VnfGwConfigRecord struct { // describes GW VNF config structure inside ViNS, as returned by API vins/get @@ -546,6 +554,14 @@ type VnfGwConfigRecord struct { // describes GW VNF config structure inside ViNS ExtNetMask int `json:"ext_net_mask"` DefaultGW string `json:"default_gw"` } + +type NatRuleRecord struct { // describes one NAT rule, a list of such rules is maintained inside VNF NAT Config +} +type VnfNatConfigRecord struct { // describes NAT VNF config structure inside ViNS, as returned by API vins/get + Netmask int `json:"netmask"` + Network string `json:"network"` // just network address, no mask, e.g. "192.168.1.0" + Rules []NatRuleRecord `json:"rules"` +} type VinsRecord struct { // represents part of the response from API vins/get ID int `json:"id"` Name string `json:"name"` diff --git a/decort/network_subresource.go b/decort/network_subresource.go index 2e8ca3d..a1e9a2a 100644 --- a/decort/network_subresource.go +++ b/decort/network_subresource.go @@ -129,7 +129,7 @@ func networkSubresourceSchemaMake() map[string]*schema.Schema { Optional: true, Computed: true, DiffSuppressFunc: networkSubresIPAddreDiffSupperss, - Description: "Optional IP address to assign to this connection. This IP should belong to the selected network and free for use.", + Description: "Optional IP address to assign to this connection. This IP should belong to the selected network and available for use.", }, "mac": { @@ -138,6 +138,15 @@ func networkSubresourceSchemaMake() map[string]*schema.Schema { Description: "MAC address associated with this connection. MAC address is assigned automatically.", }, + "pfw_rule": { + Type: schema.TypeSet, + Optional: true, + Elem: &schema.Resource{ + Schema: pfwSubresourceSchemaMake(), + }, + Description: "Port forwarding rule to setup for this connection. You may specify several such blocks, one for each rule.", + }, + } return rets } diff --git a/decort/pfw_subresource.go b/decort/pfw_subresource.go new file mode 100644 index 0000000..483b4c9 --- /dev/null +++ b/decort/pfw_subresource.go @@ -0,0 +1,70 @@ +/* +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. +*/ + +package decort + +import ( + + // "encoding/json" + // "fmt" + // "bytes" + // log "github.com/sirupsen/logrus" + // "net/url" + + "github.com/hashicorp/terraform-plugin-sdk/helper/schema" + "github.com/hashicorp/terraform-plugin-sdk/helper/validation" +) + +// This is subresource of network subresource of compute resource used +// when creating/managing port forwarding rules for a compute connected +// to the corresponding network +// It only applies to a ViNS connection AND to a ViNS with external network connection + +func pfwSubresourceSchemaMake() map[string]*schema.Schema { + rets := map[string]*schema.Schema{ + "pub_port_start": { + Type: schema.TypeInt, + Required: true, + ValidateFunc: validation.IntBetween(1, 65535), + Description: "Port number on the external interface. For a ranged rule it set the starting port number.", + }, + + "pub_port_end": { + Type: schema.TypeInt, + Required: true, + ValidateFunc: validation.IntBetween(1, 65535), + Description: "End port number on the external interface for a ranged rule. Set it equal to start port for a single port rule.", + }, + + "local_port": { + Type: schema.TypeInt, + Required: true, + ValidateFunc: validation.IntBetween(1, 65535), + Description: "Port number on the local interface.", + }, + + "proto": { + Type: schema.TypeString, + Required: true, + StateFunc: stateFuncToLower, + ValidateFunc: validation.StringInSlice([]string{"tcp", "udp"}, false), + Description: "Protocol for this rule. Could be either tcp or udp.", + }, + + } + return rets +} diff --git a/decort/provider.go b/decort/provider.go index 0dc8876..28b1a18 100644 --- a/decort/provider.go +++ b/decort/provider.go @@ -103,7 +103,7 @@ func Provider() *schema.Provider { "decort_kvmvm": resourceCompute(), "decort_disk": resourceDisk(), "decort_vins": resourceVins(), - // "decort_pfw": resourcePfw(), + // "decort_k8s": resourceK8s(), }, DataSourcesMap: map[string]*schema.Resource{ @@ -113,7 +113,8 @@ func Provider() *schema.Provider { "decort_image": dataSourceImage(), "decort_disk": dataSourceDisk(), "decort_vins": dataSourceVins(), - // "decort_pfw": dataSourcePfw(), + // "decort_k8ci": dataSourceK8ci(), + // "decort_k8s": dataSourceK8s(), }, ConfigureFunc: providerConfigure, diff --git a/decort/resource_compute.go b/decort/resource_compute.go index a27f16f..68e7786 100644 --- a/decort/resource_compute.go +++ b/decort/resource_compute.go @@ -184,7 +184,7 @@ func resourceComputeRead(d *schema.ResourceData, m interface{}) error { log.Debugf("resourceComputeRead: called for Compute name %s, RG ID %d", d.Get("name").(string), d.Get("rg_id").(int)) - compFacts, err := utilityComputeCheckPresence(d, m) + compID, compFacts, err := utilityComputeCheckPresence(d, m) if compFacts == "" { if err != nil { return err @@ -193,7 +193,14 @@ func resourceComputeRead(d *schema.ResourceData, m interface{}) error { return nil } - if err = flattenCompute(d, compFacts); err != nil { + vinsID, pfwRules, err := utilityComputePfwGet(compID, m) + if err != nil { + log.Errorf("resourceComputeRead: there was error calling utilityComputePfwGet for compute ID %s: %s", + d.Id(), err) + return err + } + + if err = flattenCompute(d, compFacts, vinsID, pfwRules); err != nil { return err } @@ -299,7 +306,7 @@ func resourceComputeDelete(d *schema.ResourceData, m interface{}) error { log.Debugf("resourceComputeDelete: called for Compute name %s, RG ID %d", d.Get("name").(string), d.Get("rg_id").(int)) - compFacts, err := utilityComputeCheckPresence(d, m) + _, compFacts, err := utilityComputeCheckPresence(d, m) if compFacts == "" { // the target Compute does not exist - in this case according to Terraform best practice // we exit from Destroy method without error @@ -352,7 +359,7 @@ func resourceComputeExists(d *schema.ResourceData, m interface{}) (bool, error) log.Debugf("resourceComputeExist: called for Compute name %s, RG ID %d", d.Get("name").(string), d.Get("rg_id").(int)) - compFacts, err := utilityComputeCheckPresence(d, m) + _, compFacts, err := utilityComputeCheckPresence(d, m) if compFacts == "" { if err != nil { return false, err diff --git a/decort/utility_compute.go b/decort/utility_compute.go index 8767a25..a56e64a 100644 --- a/decort/utility_compute.go +++ b/decort/utility_compute.go @@ -29,6 +29,7 @@ import ( "fmt" "net/url" "strconv" + "strings" log "github.com/sirupsen/logrus" @@ -145,11 +146,48 @@ func (ctrl *ControllerCfg) utilityComputeNetworksConfigure(d *schema.ResourceDat if ipSet { urlValues.Add("ipAddr", ipaddr.(string)) } + log.Debugf("utilityComputeNetworksConfigure: ready to add network type %s ID %d for Compute ID %s", + net_data["net_type"].(string), net_data["net_id"].(int), d.Id()) _, err := ctrl.decortAPICall("POST", ComputeNetAttachAPI, urlValues) if err != nil { // failed to attach network - partial resource update apiErrCount++ lastSavedError = err + continue + } + + if pfw_rules, ok := net_data["pfw_rule"]; ok { + // fool-proof - port forwarding is applicable to VINS type networks only! And only to + // those ViNSes that have active GW VNF, but here we check for VINS type only, the rest + // will be validated by the cloud platform + if net_data["net_type"].(string) != "VINS" { + log.Errorf("utilityComputeNetworksConfigure: encountered port forward rules specs in network block of type %s for Compute ID %s", + net_data["net_type"].(string), d.Id()) + apiErrCount++ + lastSavedError = err + continue + } + + log.Debugf("utilityComputeNetworksConfigure: found port forward rules specs in network block ID %d for Compute ID %s", + net_data["net_id"].(int), d.Id()) + for _, rule_runner := range pfw_rules.(*schema.Set).List() { + pfwValues := &url.Values{} + rule := rule_runner.(map[string]interface{}) + pfwValues.Add("computeId", d.Id()) + pfwValues.Add("publicPortStart", fmt.Sprintf("%d", rule["pub_port_start"].(int))) + pfwValues.Add("publicPortEnd", fmt.Sprintf("%d", rule["pub_port_end"].(int))) + pfwValues.Add("localBasePort", fmt.Sprintf("%d", rule["local_port"].(int))) + pfwValues.Add("proto", rule["proto"].(string)) + log.Debugf("utilityComputeNetworksConfigure: ready to add pfw rule %d:%d -> %d proto %s for Compute ID %s", + rule["pub_port_start"].(int), rule["pub_port_end"].(int), + rule["proto"].(string), d.Id()) + _, err := ctrl.decortAPICall("POST", ComputePfwAddAPI, pfwValues) + if err != nil { + // failed to add port forward rule - partial resource update + apiErrCount++ + lastSavedError = err + } + } } } @@ -209,7 +247,11 @@ func (ctrl *ControllerCfg) utilityComputeNetworksConfigure(d *schema.ResourceDat return nil } -func utilityComputeCheckPresence(d *schema.ResourceData, m interface{}) (string, error) { + +//func (ctrl *ControllerCfg) utilityComputePfwConfigure(d *schema.ResourceData, do_delta bool) error { +//} + +func utilityComputeCheckPresence(d *schema.ResourceData, m interface{}) (int, string, error) { // This function tries to locate Compute by one of the following approaches: // - if compute_id is specified - locate by compute ID // - if compute_name is specified - locate by a combination of compute name and resource @@ -246,27 +288,27 @@ func utilityComputeCheckPresence(d *schema.ResourceData, m interface{}) (string, urlValues.Add("computeId", fmt.Sprintf("%d", theId)) computeFacts, err := controller.decortAPICall("POST", ComputeGetAPI, urlValues) if err != nil { - return "", err + return 0, "", err } - return computeFacts, nil + return theId, computeFacts, nil } // ID was not set in the schema upon entering this function - work through Compute name // and RG ID computeName, argSet := d.GetOk("name") if !argSet { - return "", fmt.Errorf("Cannot locate compute instance if name is empty and no compute ID specified") + return 0, "", fmt.Errorf("Cannot locate compute instance if name is empty and no compute ID specified") } rgId, argSet := d.GetOk("rg_id") if !argSet { - return "", fmt.Errorf("Cannot locate compute by name %s if no resource group ID is set", computeName.(string)) + return 0, "", fmt.Errorf("Cannot locate compute by name %s if no resource group ID is set", computeName.(string)) } urlValues.Add("rgId", fmt.Sprintf("%d", rgId)) apiResp, err := controller.decortAPICall("POST", RgListComputesAPI, urlValues) if err != nil { - return "", err + return 0, "", err } log.Debugf("utilityComputeCheckPresence: ready to unmarshal string %s", apiResp) @@ -274,7 +316,7 @@ func utilityComputeCheckPresence(d *schema.ResourceData, m interface{}) (string, computeList := RgListComputesResp{} err = json.Unmarshal([]byte(apiResp), &computeList) if err != nil { - return "", err + return 0, "", err } // log.Printf("%#v", computeList) @@ -288,11 +330,83 @@ func utilityComputeCheckPresence(d *schema.ResourceData, m interface{}) (string, cgetValues.Add("computeId", fmt.Sprintf("%d", item.ID)) apiResp, err = controller.decortAPICall("POST", ComputeGetAPI, cgetValues) if err != nil { - return "", err + return 0, "", err } - return apiResp, nil + // NOTE: compute ID is unsigned int in the platform. Here we convert it to int, which may have + // unwanted side effects when the number of compute instances grows + return int(item.ID), apiResp, nil + } + } + + return 0, "", nil // there should be no error if Compute does not exist +} + +// This function reads port forwards from a specified compute and returns them (if any) in a +// form of a list of maps of interfaces suitable to be used for d.Set("pfw_rule") on the +// network block, corresponding to the ViNS these rules belong to. To simlify this network +// block identification among multiple blocks of the same compute this function also +// returns the ID of the ViNS associated with listed rules. +func utilityComputePfwGet(compId int, m interface{}) (int, []map[string]interface{}, error) { + // If there is an error either reading portforward rules from the cloud or parsing them, error is + // returned. + // In case there are no portforwarding rules for this compute, err = nil and rule record list is empty. + // Otherwise, both prefix record and rule record list contain meaningful data. + controller := m.(*ControllerCfg) + urlValues := &url.Values{} + + pfwPrefix := PfwPrefixRecord{} + pfwRules := []PfwRuleRecord{} + pfwRulesList := []map[string]interface{}{} + + urlValues.Add("computeId", fmt.Sprintf("%d", compId)) + apiResp, err := controller.decortAPICall("POST", ComputePfwListAPI, urlValues) + if err != nil { + return 0, pfwRulesList, err + } + + if apiResp == "" { + // No port forward rules defined for this compute + return 0, pfwRulesList, nil + } + + log.Debugf("utilityComputePfwGet: ready to split API response string %s", apiResp) + + twoParts := strings.SplitN(apiResp, "},", 2) + if len(twoParts) != 2 { + log.Errorf("utilityComputePfwGet: non-empty pfwList response for compute ID %d failed to split into 2 fragments (got %d)", compId, len(twoParts)) + return 0, pfwRulesList, fmt.Errorf("Non-empty pfwList response failed to split into 2 fragments") + } + + prefixResp := strings.TrimSuffix(strings.TrimPrefix(twoParts[0], "["), ",") + "}" + log.Debugf("utilityComputePfwGet: ready to unmarshal prefix part %s", prefixResp) + err = json.Unmarshal([]byte(prefixResp), &pfwPrefix) + if err != nil { + log.Errorf("utilityComputePfwGet: failed to unmarshal prefix part of API response: %s", err) + return 0, pfwRulesList, err + } + + rulesResp := "[" + twoParts[1] + log.Debugf("utilityComputePfwGet: ready to unmarshal rules part %s", rulesResp) + err = json.Unmarshal([]byte(rulesResp), &pfwRules) + if err != nil { + log.Errorf("utilityComputePfwGet: failed to unmarshal rules part of API response: %s", err) + return 0, pfwRulesList, err + } + + log.Debugf("utilityComputePfwGet: successfully read %d port forward rules for Compute ID %d, ViNS ID %d", + len(pfwRules), compId, pfwPrefix.VinsID) + + for _, runner := range pfwRules { + rule := map[string]interface{}{ + "pub_port_start": runner.PublicPortStart, + "pub_port_end": runner.PublicPortEnd, + "local_port": runner.LocalPort, + "proto": runner.Protocol, } + pfwRulesList = append(pfwRulesList, rule) } - return "", nil // there should be no error if Compute does not exist + return pfwPrefix.VinsID, pfwRulesList, nil } + +