Initial code injection, no testing or debugging yet

rc-1.0
Sergey Shubin svs1370 4 years ago
parent 25467751c5
commit 132ed6d65e

@ -1,2 +1,49 @@
# terraform-provider-decort # terraform-provider-decort
Terraform provider for DECORT platform (API 3.5.x) Terraform provider for Digital Energy Cloud Orchestration Technology (DECORT)
NOTE: this provider is designed for DECORT API 3.5.x. For API versions prior to 3.5.x please use
Terraform DECS provider (https://github.com/rudecs/terraform-provider-decs).
With this provider you can manage Computes and resource groups in DECORT platform, as well as query the platform for
information about existing resources.
See user guide at https://github.com/rudecs/terraform-provider-decort/wiki
For a quick start follow these steps.
1. Obtain the latest GO compiler. As of November 2019 it is recommended to use v.1.13.x but as new Terraform versions are released newer Go compiler may be required, so check official Terraform repository regularly for more information.
```
cd /tmp
wget https://dl.google.com/go/go1.13.3.linux-amd64.tar.gz
tar xvf ./go1.13.3.linux-amd64.tar.gz
sudo mv go /usr/local
# add the following environment variables' declaration to shell startup
export GOPATH=/opt/gopkg:~/
export GOROOT=/usr/local/go
export PATH=$PATH:$GOROOT/bin
```
2. Clone Terraform framework repository to $GOPKG/src/github.com/hashicorp/terraform
```
mkdir -p $GOPKG/src/github.com/hashicorp
cd $GOPKG/src/github.com/hashicorp
git clone https://github.com/hashicorp/terraform.git
```
3. Clone jwt-go package repository to $GOPKG/src/github.com/dgrijalva/jwt-go:
```
mkdir -p $GOPKG/src/github.com/dgrijalva
cd $GOPKG/src/github.com/dgrijalva
git clone https://github.com/dgrijalva/jwt-go.git
```
4. Clone terraform-decs-provider repository to $GOPKG/src/github.com/terraform-provider-decort
```
cd $GOPKG/src/github.com
git clone https://github.com/rudecs/terraform-provider-decort.git
```
5. Build Terraform DECS provider:
```
cd $GOPKG/src/github.com/terraform-provider-decort
go build -o terraform-provider-decort
```

@ -0,0 +1,407 @@
/*
Copyright (c) 2019-2021 Digital Energy Cloud Solutions LLC. All Rights Reserved.
Author: Sergey Shubin, <sergey.shubin@digitalenergy.online>, <svs1370@gmail.com>
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 (
"bytes"
"crypto/tls"
"fmt"
"io/ioutil"
"log"
"net/http"
"net/url"
"strconv"
"strings"
// "time"
"github.com/dgrijalva/jwt-go"
"github.com/hashicorp/terraform/helper/schema"
// "github.com/hashicorp/terraform/terraform"
)
// enumerated constants that define authentication modes
const (
MODE_UNDEF = iota // this is the invalid mode - it should never be seen
MODE_LEGACY = iota
MODE_OAUTH2 = iota
MODE_JWT = iota
)
type ControllerCfg struct {
controller_url string // always required
auth_mode_code int // always required
auth_mode_txt string // always required, it is a text representation of auth mode
legacy_user string // required for legacy mode
legacy_password string // required for legacy mode
legacy_sid string // obtained from DECORT controller on successful login in legacy mode
jwt string // obtained from Outh2 provider on successful login in oauth2 mode, required in jwt mode
app_id string // required for oauth2 mode
app_secret string // required for oauth2 mode
oauth2_url string // always required
decort_username string // assigned to either legacy_user (legacy mode) or Oauth2 user (oauth2 mode) upon successful verification
cc_client *http.Client // assigned when all initial check successfully passed
}
func ControllerConfigure(d *schema.ResourceData) (*ControllerCfg, error) {
// This function first will check that all required provider parameters for the
// selected authenticator mode are set correctly and initialize ControllerCfg structure
// based on the provided parameters.
//
// Next, it will check for validity of supplied credentials by initiating connection to the specified
// DECORT controller URL and, if succeeded, completes ControllerCfg structure with the rest of computed
// parameters (e.g. JWT, session ID and Oauth2 user name).
//
// The structure created by this function should be used with subsequent calls to decortAPICall() method,
// which is a DECORT authentication mode aware wrapper around standard HTTP requests.
ret_config := &ControllerCfg{
controller_url: d.Get("controller_url").(string),
auth_mode_code: MODE_UNDEF,
legacy_user: d.Get("user").(string),
legacy_password: d.Get("password").(string),
legacy_sid: "",
jwt: d.Get("jwt").(string),
app_id: d.Get("app_id").(string),
app_secret: d.Get("app_secret").(string),
oauth2_url: d.Get("oauth2_url").(string),
decort_username: "",
}
var allow_unverified_ssl bool
allow_unverified_ssl = d.Get("allow_unverified_ssl").(bool)
if ret_config.controller_url == "" {
return nil, fmt.Errorf("Empty DECORT cloud controller URL provided.")
}
// this should have already been done by StateFunc defined in Schema, but we want to be sure
ret_config.auth_mode_txt = strings.ToLower(d.Get("authenticator").(string))
switch ret_config.auth_mode_txt {
case "jwt":
if ret_config.jwt == "" {
return nil, fmt.Errorf("Authenticator mode 'jwt' specified but no JWT provided.")
}
ret_config.auth_mode_code = MODE_JWT
case "oauth2":
if ret_config.oauth2_url == "" {
return nil, fmt.Errorf("Authenticator mode 'oauth2' specified but no OAuth2 URL provided.")
}
if ret_config.app_id == "" {
return nil, fmt.Errorf("Authenticator mode 'oauth2' specified but no Application ID provided.")
}
if ret_config.app_secret == "" {
return nil, fmt.Errorf("Authenticator mode 'oauth2' specified but no Secret ID provided.")
}
ret_config.auth_mode_code = MODE_OAUTH2
case "legacy":
//
ret_config.legacy_user = d.Get("user").(string)
if ret_config.legacy_user == "" {
return nil, fmt.Errorf("Authenticator mode 'legacy' specified but no user provided.")
}
ret_config.legacy_password = d.Get("password").(string)
if ret_config.legacy_password == "" {
return nil, fmt.Errorf("Authenticator mode 'legacy' specified but no password provided.")
}
ret_config.auth_mode_code = MODE_LEGACY
default:
return nil, fmt.Errorf("Unknown authenticator mode %q provided.", ret_config.auth_mode_txt)
}
if allow_unverified_ssl {
log.Printf("ControllerConfigure: allow_unverified_ssl is set - will not check certificates!")
transCfg := &http.Transport{TLSClientConfig: &tls.Config{InsecureSkipVerify: true},}
ret_config.cc_client = &http.Client{
Transport: transCfg,
Timeout: Timeout180s,
}
} else {
ret_config.cc_client = &http.Client{
Timeout: Timeout180s, // time.Second * 30,
}
}
switch ret_config.auth_mode_code {
case MODE_LEGACY:
ok, err := ret_config.validateLegacyUser()
if !ok {
return nil, err
}
ret_config.decort_username = ret_config.legacy_user
case MODE_JWT:
//
ok, err := ret_config.validateJWT("")
if !ok {
return nil, err
}
case MODE_OAUTH2:
// on success getOAuth2JWT will set config.jwt to the obtained JWT, so there is no
// need to set it once again here
_, err := ret_config.getOAuth2JWT()
if err != nil {
return nil, err
}
// we are not verifying the JWT when parsing because actual verification is done on the
// OVC controller side. Here we do parsing solely to extract Oauth2 user name (claim "user")
// and JWT issuer name (claim "iss")
parser := jwt.Parser{}
token, _, err := parser.ParseUnverified(ret_config.jwt, jwt.MapClaims{})
if err != nil {
return nil, err
}
if claims, ok := token.Claims.(jwt.MapClaims); ok {
var tbuf bytes.Buffer
tbuf.WriteString(claims["username"].(string))
tbuf.WriteString("@")
tbuf.WriteString(claims["iss"].(string))
ret_config.decort_username = tbuf.String()
} else {
return nil, fmt.Errorf("Failed to extract user and iss fields from JWT token in oauth2 mode.")
}
default:
// FYI, this should never happen due to all above checks, but we want to be fool proof
return nil, fmt.Errorf("Unknown authenticator mode code %d provided.", ret_config.auth_mode_code)
}
// All checks passed successfully, credentials corresponding to the selected authenticator mode
// obtained and validated.
return ret_config, nil
}
func (config *ControllerCfg) getDecsUsername() (string) {
return config.decort_username
}
func (config *ControllerCfg) getOAuth2JWT() (string, error) {
// Obtain JWT from the Oauth2 provider using application ID and application secret provided in config.
if config.auth_mode_code == MODE_UNDEF {
return "", fmt.Errorf("getOAuth2JWT method called for undefined authorization mode.")
}
if config.auth_mode_code != MODE_OAUTH2 {
return "", fmt.Errorf("getOAuth2JWT method called for incompatible authorization mode %q.", config.auth_mode_txt)
}
params := url.Values{}
params.Add("grant_type", "client_credentials")
params.Add("client_id", config.app_id)
params.Add("client_secret", config.app_secret)
params.Add("response_type", "id_token")
params.Add("validity", "3600")
params_str := params.Encode()
req, err := http.NewRequest("POST", config.oauth2_url + "/v1/oauth/access_token", strings.NewReader(params_str))
if err != nil {
return "", err
}
req.Header.Set("Content-Type", "application/x-www-form-urlencoded")
req.Header.Set("Content-Length", strconv.Itoa(len(params_str)))
resp, err := config.cc_client.Do(req)
if err != nil {
return "", err
}
if resp.StatusCode != http.StatusOK {
// fmt.Println("response Status:", resp.Status)
// fmt.Println("response Headers:", resp.Header)
// fmt.Println("response Headers:", req.URL)
return "", fmt.Errorf("getOauth2JWT: unexpected status code %d when obtaining JWT from %q for APP_ID %q, request Body %q",
resp.StatusCode, req.URL, config.app_id, params_str)
}
defer resp.Body.Close()
responseData, err := ioutil.ReadAll(resp.Body)
if err != nil {
return "", err
}
// validation successful - store JWT in the corresponding field of the ControllerCfg structure
config.jwt = strings.TrimSpace(string(responseData))
return config.jwt, nil
}
func (config *ControllerCfg) validateJWT(jwt string) (bool, error) {
/*
Validate JWT against DECORT controller. JWT can be supplied as argument to this method. If empty string supplied as
argument, JWT will be taken from config attribute.
DECORT controller URL will always be taken from the config attribute assigned at instantiation.
Validation is accomplished by attempting API call that lists accounts for the invoking user.
*/
if jwt == "" {
if config.jwt == "" {
return false, fmt.Errorf("validateJWT method called, but no meaningful JWT provided.")
}
jwt = config.jwt
}
if config.oauth2_url == "" {
return false, fmt.Errorf("validateJWT method called, but no OAuth2 URL provided.")
}
req, err := http.NewRequest("POST", config.controller_url + "/restmachine/cloudapi/accounts/list", nil)
if err != nil {
return false, err
}
req.Header.Set("Authorization", fmt.Sprintf("bearer %s", jwt))
// req.Header.Set("Content-Type", "application/x-www-form-urlencoded")
// req.Header.Set("Content-Length", strconv.Itoa(0))
resp, err := config.cc_client.Do(req)
if err != nil {
return false, err
}
if resp.StatusCode != http.StatusOK {
return false, fmt.Errorf("validateJWT: unexpected status code %d when validating JWT against %q.",
resp.StatusCode, req.URL)
}
defer resp.Body.Close()
return true, nil
}
func (config *ControllerCfg) validateLegacyUser() (bool, error) {
/*
Validate legacy user by obtaining a session key, which will be used for authenticating subsequent API calls
to DECORT controller.
If successful, the session key is stored in config.legacy_sid and true is returned. If unsuccessful for any
reason, the method will return false and error.
*/
if config.auth_mode_code == MODE_UNDEF {
return false, fmt.Errorf("validateLegacyUser method called for undefined authorization mode.")
}
if config.auth_mode_code != MODE_LEGACY {
return false, fmt.Errorf("validateLegacyUser method called for incompatible authorization mode %q.", config.auth_mode_txt)
}
params := url.Values{}
params.Add("username", config.legacy_user)
params.Add("password", config.legacy_password)
params_str := params.Encode()
req, err := http.NewRequest("POST", config.controller_url + "/restmachine/cloudapi/users/authenticate", strings.NewReader(params_str))
if err != nil {
return false, err
}
req.Header.Set("Content-Type", "application/x-www-form-urlencoded")
req.Header.Set("Content-Length", strconv.Itoa(len(params_str)))
resp, err := config.cc_client.Do(req)
if err != nil {
return false, err
}
if resp.StatusCode != http.StatusOK {
return false, fmt.Errorf("validateLegacyUser: unexpected status code %d when validating legacy user %q against %q.",
resp.StatusCode, config.legacy_user, config.controller_url)
}
defer resp.Body.Close()
responseData, err := ioutil.ReadAll(resp.Body)
if err != nil {
log.Fatal(err)
}
// validation successful - keep session ID for future use
config.legacy_sid = string(responseData)
return true, nil
}
func (config *ControllerCfg) decortAPICall(method string, api_name string, url_values *url.Values) (json_resp string, err error) {
// This is a convenience wrapper around standard HTTP request methods that is aware of the
// authorization mode for which the provider was initialized and compiles request accordingly.
if config.cc_client == nil {
// this should never happen if ClientConfig was properly called prior to decortAPICall
return "", fmt.Errorf("decortAPICall method called with unconfigured DECORT cloud controller HTTP client.")
}
// Example: to create api_params, one would generally do the following:
//
// data := []byte(`{"machineId": "2638"}`)
// api_params := bytes.NewBuffer(data))
//
// Or:
//
// params := url.Values{}
// params.Add("machineId", "2638")
// params.Add("username", "u")
// params.Add("password", "b")
// req, _ := http.NewRequest(method, url, strings.NewReader(params.Encode()))
//
if config.auth_mode_code == MODE_UNDEF {
return "", fmt.Errorf("decortAPICall method called for unknown authorization mode.")
}
if config.auth_mode_code == MODE_LEGACY {
url_values.Add("authkey", config.legacy_sid)
}
params_str := url_values.Encode()
req, err := http.NewRequest(method, config.controller_url + api_name, strings.NewReader(params_str))
if err != nil {
return "", err
}
req.Header.Set("Content-Type", "application/x-www-form-urlencoded")
req.Header.Set("Content-Length", strconv.Itoa(len(params_str)))
if config.auth_mode_code == MODE_OAUTH2 || config.auth_mode_code == MODE_JWT {
req.Header.Set("Authorization", fmt.Sprintf("bearer %s", config.jwt))
}
resp, err := config.cc_client.Do(req)
if err != nil {
return "", err
}
defer resp.Body.Close()
if resp.StatusCode == http.StatusOK {
tmp_body, err := ioutil.ReadAll(resp.Body)
if err != nil {
return "", err
}
json_resp := Jo2JSON(string(tmp_body))
log.Printf("decortAPICall:\n %s", json_resp)
return json_resp, nil
} else {
return "", fmt.Errorf("decortAPICall: unexpected status code %d when calling API %q with request Body %q",
resp.StatusCode, req.URL, params_str)
}
/*
if resp.StatusCode == StatusServiceUnavailable {
return nil, fmt.Errorf("decortAPICall method called for incompatible authorization mode %q.", config.auth_mode_txt)
}
*/
return "", err
}

@ -0,0 +1,245 @@
/*
Copyright (c) 2019-2021 Digital Energy Cloud Solutions LLC. All Rights Reserved.
Author: Sergey Shubin, <sergey.shubin@digitalenergy.online>, <svs1370@gmail.com>
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"
"log"
// "net/url"
"github.com/hashicorp/terraform/helper/schema"
"github.com/hashicorp/terraform/helper/validation"
)
func flattenCompute(d *schema.ResourceData, comp_facts string) error {
// NOTE: this function modifies ResourceData argument - as such it should never be called
// from resourceComputeExists(...) method
model := MachinesGetResp{}
log.Printf("flattenCompute: ready to unmarshal string %q", comp_facts)
err := json.Unmarshal([]byte(comp_facts), &model)
if err != nil {
return err
}
log.Printf("flattenCompute: model.ID %d, model.ResGroupID %d", model.ID, model.ResGroupID)
d.SetId(fmt.Sprintf("%d", model.ID))
d.Set("name", model.Name)
d.Set("rgid", model.ResGroupID)
d.Set("cpu", model.Cpu)
d.Set("ram", model.Ram)
// d.Set("boot_disk", model.BootDisk)
d.Set("image_id", model.ImageID)
d.Set("description", model.Description)
bootdisk_map := make(map[string]interface{})
bootdisk_map["size"] = model.BootDisk
bootdisk_map["label"] = "boot"
bootdisk_map["pool"] = "default"
bootdisk_map["provider"] = "default"
if err = d.Set("boot_disk", []interface{}{bootdisk_map}); err != nil {
return err
}
if len(model.DataDisks) > 0 {
log.Printf("flattenCompute: calling flattenDataDisks")
if err = d.Set("data_disks", flattenDataDisks(model.DataDisks)); err != nil {
return err
}
}
if len(model.NICs) > 0 {
log.Printf("flattenCompute: calling flattenNICs")
if err = d.Set("nics", flattenNICs(model.NICs)); err != nil {
return err
}
log.Printf("flattenCompute: calling flattenNetworks")
if err = d.Set("networks", flattenNetworks(model.NICs)); err != nil {
return err
}
}
if len(model.GuestLogins) > 0 {
log.Printf("flattenCompute: calling flattenGuestLogins")
guest_logins := flattenGuestLogins(model.GuestLogins)
if err = d.Set("guest_logins", guest_logins); err != nil {
return err
}
default_login := guest_logins[0].(map[string]interface{})
// set user & password attributes to the corresponding values of the 1st item in the list
if err = d.Set("user", default_login["login"]); err != nil {
return err
}
if err = d.Set("password", default_login["password"]); err != nil {
return err
}
}
return nil
}
func dataSourceComputeRead(d *schema.ResourceData, m interface{}) error {
comp_facts, err := utilityComputeCheckPresence(d, m)
if comp_facts == "" {
// if empty string is returned from utilityComputeCheckPresence then there is no
// such Compute and err tells so - just return it to the calling party
d.SetId("") // ensure ID is empty
return err
}
return flattenCompute(d, comp_facts)
}
func dataSourceCompute() *schema.Resource {
return &schema.Resource {
SchemaVersion: 1,
Read: dataSourceComputeRead,
Timeouts: &schema.ResourceTimeout {
Read: &Timeout30s,
Default: &Timeout60s,
},
Schema: map[string]*schema.Schema {
"name": {
Type: schema.TypeString,
Required: true,
Description: "Name of this virtual machine. This parameter is case sensitive.",
},
"rgid": {
Type: schema.TypeInt,
Required: true,
ValidateFunc: validation.IntAtLeast(1),
Description: "ID of the resource group where this virtual machine is located.",
},
/*
"internal_ip": {
Type: schema.TypeString,
Computed: true,
Description: "Internal IP address of this Compute.",
},
*/
"cpu": {
Type: schema.TypeInt,
Computed: true,
Description: "Number of CPUs allocated for this virtual machine.",
},
"ram": {
Type: schema.TypeInt,
Computed: true,
Description: "Amount of RAM in MB allocated for this virtual machine.",
},
"image_id": {
Type: schema.TypeInt,
Computed: true,
Description: "ID of the OS image this virtual machine is based on.",
},
/*
"image_name": {
Type: schema.TypeString,
Computed: true,
Description: "Name of the OS image this virtual machine is based on.",
},
*/
"boot_disk": {
Type: schema.TypeList,
Computed: true,
MinItems: 1,
Elem: &schema.Resource {
Schema: diskSubresourceSchema(),
},
Description: "Specification for a boot disk on this virtual machine.",
},
"data_disks": {
Type: schema.TypeList,
Computed: true,
Elem: &schema.Resource {
Schema: diskSubresourceSchema(),
},
Description: "Specification for data disks on this virtual machine.",
},
"guest_logins": {
Type: schema.TypeList,
Computed: true,
Elem: &schema.Resource {
Schema: loginsSubresourceSchema(),
},
Description: "Specification for guest logins on this virtual machine.",
},
"networks": {
Type: schema.TypeList,
Computed: true,
Elem: &schema.Resource {
Schema: networkSubresourceSchema(),
},
Description: "Specification for the networks to connect this virtual machine to.",
},
"nics": {
Type: schema.TypeList,
Computed: true,
Elem: &schema.Resource {
Schema: nicSubresourceSchema(),
},
Description: "Specification for the virutal NICs allocated to this virtual machine.",
},
"description": {
Type: schema.TypeString,
Computed: true,
Description: "Description of this virtual machine.",
},
"user": {
Type: schema.TypeString,
Computed: true,
Description: "Default login name for the guest OS on this virtual machine.",
},
"password": {
Type: schema.TypeString,
Computed: true,
Sensitive: true,
Description: "Default password for the guest OS login on this virtual machine.",
},
},
}
}

@ -0,0 +1,111 @@
/*
Copyright (c) 2019-2020 Digital Energy Cloud Solutions LLC. All Rights Reserved.
Author: Sergey Shubin, <sergey.shubin@digitalenergy.online>, <svs1370@gmail.com>
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"
"log"
"net/url"
"github.com/hashicorp/terraform/helper/schema"
"github.com/hashicorp/terraform/helper/validation"
)
func dataSourceImageRead(d *schema.ResourceData, m interface{}) error {
name := d.Get("name").(string)
rgid, rgid_set := d.GetOk("rgid")
tenant_id, tenant_set := d.GetOk("tenant_id")
controller := m.(*ControllerCfg)
url_values := &url.Values{}
if tenant_set {
url_values.Add("accountId", fmt.Sprintf("%d",tenant_id.(int)))
}
if rgid_set {
url_values.Add("cloudspaceId", fmt.Sprintf("%d",rgid.(int)))
}
body_string, err := controller.decortAPICall("POST", ImagesListAPI, url_values)
if err != nil {
return err
}
log.Printf("dataSourceImageRead: ready to decode response body")
model := ImagesListResp{}
err = json.Unmarshal([]byte(body_string), &model)
if err != nil {
return err
}
log.Printf("%#v", model)
log.Printf("dataSourceImageRead: traversing decoded JSON of length %d", len(model))
for index, item := range model {
// need to match VM by name
if item.Name == name {
log.Printf("dataSourceImageRead: index %d, matched name %q", index, item.Name)
d.SetId(fmt.Sprintf("%d", model[index].ID))
// d.Set("field_name", value)
return nil
}
}
return fmt.Errorf("Cannot find OS Image name %q", name)
}
func dataSourceImage() *schema.Resource {
return &schema.Resource {
SchemaVersion: 1,
Read: dataSourceImageRead,
Timeouts: &schema.ResourceTimeout {
Read: &Timeout30s,
Default: &Timeout60s,
},
Schema: map[string]*schema.Schema {
"name": {
Type: schema.TypeString,
Required: true,
Description: "Name of the OS image to locate. This parameter is case sensitive.",
},
"tenant_id": {
Type: schema.TypeInt,
Optional: true,
ValidateFunc: validation.IntAtLeast(1),
Description: "ID of the tenant to limit image search to.",
},
"rgid": {
Type: schema.TypeInt,
Optional: true,
ValidateFunc: validation.IntAtLeast(1),
Description: "ID of the resource group to limit image search to.",
},
},
}
}

@ -0,0 +1,139 @@
/*
Copyright (c) 2019-2020 Digital Energy Cloud Solutions LLC. All Rights Reserved.
Author: Sergey Shubin, <sergey.shubin@digitalenergy.online>, <svs1370@gmail.com>
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"
"log"
// "net/url"
"github.com/hashicorp/terraform/helper/schema"
// "github.com/hashicorp/terraform/helper/validation"
)
func flattenResgroup(d *schema.ResourceData, rg_facts string) error {
// NOTE: this function modifies ResourceData argument - as such it should never be called
// from resourceRsgroupExists(...) method
log.Printf("%s", rg_facts)
log.Printf("flattenResgroup: ready to decode response body from %q", CloudspacesGetAPI)
details := CloudspacesGetResp{}
err := json.Unmarshal([]byte(rg_facts), &details)
if err != nil {
return err
}
log.Printf("flattenResgroup: decoded ResGroup name %q / ID %d, tenant ID %d, public IP %q",
details.Name, details.ID, details.TenantID, details.PublicIP)
d.SetId(fmt.Sprintf("%d", details.ID))
d.Set("name", details.Name)
d.Set("tenant_id", details.TenantID)
d.Set("grid_id", details.GridID)
d.Set("public_ip", details.PublicIP) // legacy field - this may be obsoleted when new network segments are implemented
log.Printf("flattenResgroup: calling flattenQuota()")
if err = d.Set("quotas", flattenQuota(details.Quotas)); err != nil {
return err
}
return nil
}
func dataSourceResgroupRead(d *schema.ResourceData, m interface{}) error {
rg_facts, err := utilityResgroupCheckPresence(d, m)
if rg_facts == "" {
// if empty string is returned from utilityResgroupCheckPresence then there is no
// such resource group and err tells so - just return it to the calling party
d.SetId("") // ensure ID is empty
return err
}
return flattenResgroup(d, rg_facts)
}
func dataSourceResgroup() *schema.Resource {
return &schema.Resource {
SchemaVersion: 1,
Read: dataSourceResgroupRead,
Timeouts: &schema.ResourceTimeout {
Read: &Timeout30s,
Default: &Timeout60s,
},
Schema: map[string]*schema.Schema {
"name": {
Type: schema.TypeString,
Required: true,
Description: "Name of this resource group. Names are case sensitive and unique within the context of a tenant.",
},
"tenant": &schema.Schema {
Type: schema.TypeString,
Required: true,
Description: "Name of the tenant, which this resource group belongs to.",
},
"tenant_id": &schema.Schema {
Type: schema.TypeInt,
Computed: true,
Description: "Unique ID of the tenant, which this resource group belongs to.",
},
"grid_id": &schema.Schema {
Type: schema.TypeInt,
Computed: true,
Description: "Unique ID of the grid, where this resource group is deployed.",
},
"location": {
Type: schema.TypeString,
Computed: true,
Description: "Location of this resource group.",
},
"public_ip": { // this may be obsoleted as new network segments and true resource groups are implemented
Type: schema.TypeString,
Computed: true,
Description: "Public IP address of this resource group (if any).",
},
"quotas": {
Type: schema.TypeList,
Optional: true,
MaxItems: 1,
Elem: &schema.Resource {
Schema: quotasSubresourceSchema(),
},
Description: "Quotas on the resources for this resource group.",
},
},
}
}

@ -0,0 +1,130 @@
/*
Copyright (c) 2019 Digital Energy Cloud Solutions LLC. All Rights Reserved.
Author: Sergey Shubin, <sergey.shubin@digitalenergy.online>, <svs1370@gmail.com>
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 decs
import (
"log"
"github.com/hashicorp/terraform/helper/schema"
"github.com/hashicorp/terraform/helper/validation"
)
func makeDisksConfig(arg_list []interface{}) (disks []DiskConfig, count int) {
count = len(arg_list)
if count < 1 {
return nil, 0
}
// allocate DataDisks list and fill it
disks = make([]DiskConfig, count)
var subres_data map[string]interface{}
for index, value := range arg_list {
subres_data = value.(map[string]interface{})
disks[index].Label = subres_data["label"].(string)
disks[index].Size = subres_data["size"].(int)
disks[index].Pool = subres_data["pool"].(string)
disks[index].Provider = subres_data["provider"].(string)
}
return disks, count
}
func flattenDataDisks(disks []DataDiskRecord) []interface{} {
var length = 0
for _, value := range disks {
if value.DiskType == "D" {
length += 1
}
}
log.Printf("flattenDataDisks: found %d disks with D type", length)
result := make([]interface{}, length)
if length == 0 {
return result
}
elem := make(map[string]interface{})
var subindex = 0
for _, value := range disks {
if value.DiskType == "D" {
elem["label"] = value.Label
elem["size"] = value.SizeMax
elem["disk_id"] = value.ID
elem["pool"] = "default"
elem["provider"] = "default"
result[subindex] = elem
subindex += 1
}
}
return result
}
/*
func makeDataDisksArgString(disks []DiskConfig) string {
// Prepare a string with the sizes of data disks for the virtual machine.
// It is designed to be passed as "datadisks" argument of virtual machine create API call.
if len(disks) < 1 {
return ""
}
return ""
}
*/
func diskSubresourceSchema() map[string]*schema.Schema {
rets := map[string]*schema.Schema {
"label": {
Type: schema.TypeString,
Required: true,
Description: "Unique label to identify this disk among other disks connected to this VM.",
},
"size": {
Type: schema.TypeInt,
Required: true,
ValidateFunc: validation.IntAtLeast(1),
Description: "Size of the disk in GB.",
},
"pool": {
Type: schema.TypeString,
Optional: true,
Default: "default",
Description: "Pool from which this disk should be provisioned.",
},
"provider": {
Type: schema.TypeString,
Optional: true,
Default: "default",
Description: "Storage provider (storage technology type) by which this disk should be served.",
},
"disk_id": {
Type: schema.TypeInt,
Computed: true,
Description: "ID of this disk resource.",
},
}
return rets
}

@ -0,0 +1,71 @@
/*
Copyright (c) 2019-2021 Digital Energy Cloud Solutions LLC. All Rights Reserved.
Author: Sergey Shubin, <sergey.shubin@digitalenergy.online>, <svs1370@gmail.com>
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 decs
import (
"log"
"github.com/hashicorp/terraform/helper/schema"
// "github.com/hashicorp/terraform/helper/validation"
)
func flattenGuestLogins(logins []GuestLoginRecord) []interface{} {
var result = make([]interface{}, len(logins))
elem := make(map[string]interface{})
for index, value := range logins {
elem["guid"] = value.Guid
elem["login"] = value.Login
elem["password"] = value.Password
result[index] = elem
log.Printf("flattenGuestLogins: parsed element %d - login %q",
index, value.Login)
}
return result
}
func loginsSubresourceSchema() map[string]*schema.Schema {
rets := map[string]*schema.Schema {
"guid": {
Type: schema.TypeString,
Optional: true,
Default: "",
Description: "GUID of this guest user.",
},
"login": {
Type: schema.TypeString,
Optional: true,
Default: "",
Description: "Login name of this guest user.",
},
"password": {
Type: schema.TypeString,
Optional: true,
Default: "",
Sensitive: true,
Description: "Password of this guest user.",
},
}
return rets
}

@ -0,0 +1,538 @@
/*
Copyright (c) 2019-2020 Digital Energy Cloud Solutions LLC. All Rights Reserved.
Author: Sergey Shubin, <sergey.shubin@digitalenergy.online>, <svs1370@gmail.com>
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 (
"time"
)
//
// timeouts for API calls from CRUD functions of Terraform plugin
var Timeout30s = time.Second * 30
var Timeout60s = time.Second * 60
var Timeout180s = time.Second * 180
//
// structures related to /cloudapi/rg/list API
//
type UserAclRecord struct {
IsExplicit bool `json:"explicit"`
Rights string `json:"right"`
Status string `json:"status"`
Type string `json:"type"`
UgroupID string `json:"userGroupId"`
// CanBeDeleted bool `json:"canBeDeleted"`
}
type AccountAclRecord struct {
IsExplicit bool `json:"explicit"`
Guid string `json:"guid"`
Rights string `json:"right"`
Status string `json:"status"`
Type string `json:"type"`
UgroupID string `json:"userGroupId"`
}
type ResgroupRecord struct {
ACLs []UserAclRecord `json:"ACLs"`
Owner AccountAclRecord `json:"accountAcl"`
TenantID int `json:"accountId"`
TenantName string `json:"accountName"`
CreatedBy string `json:"createdBy"`
CreatedTime uint64 `json:"createdTime"`
DefaultNetID int `json:"def_net_id"`
DefaultNetType string `json:"def_net_type"`
Decsription string `json:"desc"`
GridID int `json:"gid"`
ID uint `json:"id"`
LockStatus string `json:"lockStatus"`
Name string `json:"name"`
Status string `json:"status"`
UpdatedBy string `json:"updatedBy"`
UpdatedTime uint64 `json:"updatedTime"`
Vins []int `json:"vins"`
Computes []int `json:"vms"`
}
const ResgroupListAPI = "/restmachine/cloudapi/rg/list"
type ResgroupListResp []ResgroupRecord
//
// structures related to /cloudapi/rg/create API call
//
const ResgroupCreateAPI= "/restmachine/cloudapi/rg/create"
type ResgroupCreateParam struct {
TenantID int `json:"accountId"`
GridId int `json:"gid"`
Name string `json:"name"`
Ram int `json:"maxMemoryCapacity"`
Disk int `json:"maxVDiskCapacity"`
Cpu int `json:"maxCPUCapacity"`
NetTraffic int `json:"maxNetworkPeerTransfer"`
ExtIPs int `json:"maxNumPublicIP"`
Owner string `json:"owner"`
DefNet string `json:"def_net"`
IPCidr string `json:"ipcidr"`
Desc string `json:"decs"`
Reason string `json:"reason"`
ExtNetID int `json:"extNetId"`
ExtIP string `json:"extIp"`
}
//
// structures related to /cloudapi/rg/update API call
//
const ResgroupUpdateAPI= "/restmachine/cloudapi/rg/update"
type ResgroupUpdateParam struct {
RgId int `json:"rgId"`
Name string `json:"name"`
Desc string `json:"decs"`
Ram int `json:"maxMemoryCapacity"`
Disk int `json:"maxVDiskCapacity"`
Cpu int `json:"maxCPUCapacity"`
NetTraffic int `json:"maxNetworkPeerTransfer"`
Reason string `json:"reason"`
}
//
// structures related to /cloudapi/rg/get API call
//
type ResourceRecord struct {
Cpu int `json:"cpu"`
Disk int `json:"disksize"`
ExtIPs int `json:"extips"`
ExtTraffic int `json:"exttraffic"`
Gpu int `json:"gpu"`
Ram int `json:"ram"`
}
type UsageRecord struct {
Current ResourceRecord `json:"Current"`
Reserved ResourceRecord `json:"Reserved"`
}
const ResgroupGetAPI= "/restmachine/cloudapi/rg/get"
type ResgroupGetResp struct {
ACLs []UserAclRecord `json:"ACLs"`
Usage UsageRecord `json:"Resources"`
TenantID int `json:"accountId"`
TenantName string `json:"accountName"`
CreatedBy string `json:"createdBy"`
CreatedTime uint64 `json:"createdTime"`
DefaultNetID int `json:"def_net_id"`
DefaultNetType string `json:"def_net_type"`
DeletedBy string `json:"deletedBy"`
DeletedTime uint64 `json:"deletedTime"`
Decsription string `json:"desc"`
ID uint `json:"id"`
LockStatus string `json:"lockStatus"`
Name string `json:"name"`
Quotas QuotaRecord `json:"resourceLimits"`
Status string `json:"status"`
UpdatedBy string `json:"updatedBy"`
UpdatedTime uint64 `json:"updatedTime"`
Vins []int `json:"vins"`
Computes []int `json:"vms"`
Ignored map[string]interface{} `json:"-"`
}
//
// structures related to /cloudapi/rg/update API
//
const ResgroupUpdateAPI = "/restmachine/cloudapi/rg/update"
type ResgroupUpdateParam struct {
ID uint `json:"rgId"`
Name string `json:"name"`
Decsription string `json:"desc"`
Cpu int `json:"maxCPUCapacity"`
Ram int `json:"maxMemoryCapacity"`
Disk int `json:"maxVDiskCapacity"`
NetTraffic int `json:"maxNetworkPeerTransfer"`
ExtIPs int `json:"maxNumPublicIP"`
Reason string `json:"reason"`
}
//
// structures related to /cloudapi/rg/delete API
//
const ResgroupDeleteAPI = "/restmachine/cloudapi/rg/delete"
type ResgroupDeleteParam struct {
ID uint `json:"rgId"`
Force bool `json:"force"`
Reason string `json:"reason"`
}
//
// structures related to /cloudapi/kvmXXX/create APIs
//
const KvmX86CreateAPI = "/restmachine/cloudapi/kvmx86/create"
const KvmPPCCreateAPI = "/restmachine/cloudapi/kvmppc/create"
type KvmXXXCreateParam struct { // this is unified structure for both x86 and PPC based VMs creation
RgID uint `json:"rgId"`
Name string `json:"name"`
Cpu int `json:"cpu"`
Ram int `json:"ram"`
ImageID int `json:"imageId"`
BootDisk int `json:"bootDisk"`
NetType string `json:"netType"`
NetId int `json:"netId"`
IPAddr string `json:"ipAddr"`
UserData string `json:"userdata"`
Description string `json:"desc"`
Start bool `json:"start"`
}
// structures related to cloudapi/compute/delete API
const ComputeDeleteAPI = "/restmachine/cloudapi/compute/delete"
type ComputeDeleteParam struct {
ComputeID int `json:"computeId"`
Permanently bool `json:"permanently"`
}
//
// structures related to /cloudapi/compute/list API
//
type InterfaceRecord struct {
ConnID int `json:"connId"`
ConnType string `json:"connType"`
DefaultGW string `json:"defGw"`
Guid string `json:"guid"`
IPAddress string `json:"ipAddress"` // without trailing network mask, i.e. "192.168.1.3"
MAC string `json:"mac"`
Name string `json:"name"`
NetID int `json:"netId"`
NetMaks int `json:"netMask"`
NetType string `json:"netType"`
PciSlot int `json:"pciSlot"`
Target string `json:"target"`
Type string `json:"type"`
VNFs []int `json:"vnfs"`
}
type SnapSetRecord struct {
Disks []int `json:"disks"`
Guid string `json:"guid"`
Label string `json:"label"`
TimeStamp uint64 `json:"timestamp"`
}
type ComputeRecord struct {
TenantID int `json:"accountId"`
TenantName string `json:"accountName"`
ACLs []UserAclRecord `json:"acl"`
Arch string `json:"arch"`
BootDiskSize int `json:"bootdiskSize"`
CloneReference int `json:"cloneReference"`
Clones []int `json:"clones"`
Cpus int `json:"cpus"`
CreatedBy string `json:"createdBy"`
CreatedTime uint64 `json:"createdTime"`
DeletedBy string `json:"deletedBy"`
DeletedTime uint64 `json:"deletedTime"`
Desc string `json:"desc"`
Disks []int `json:"disks"`
GridID int `json:"gid"`
ID uint `json:"id"`
ImageID int `json:"imageId"`
Interfaces []InterfaceRecord `json:"interfaces`
LockStatus string `json:"lockStatus"`
ManagerID int `json:"managerId"`
Name string `json:"name"`
Ram int `json:"ram"`
RgID int `json:"rgId"`
RgName string `json:"rgName"`
SnapSets []SnapSetRecord `json:"snapSets"`
Status string `json:"status"`
Tags []string `json:"tags"`
TechStatus string `json:"techStatus"`
TotalDiskSize int `json:"totalDiskSize"`
UpdatedBy string `json:"updatedBy"`
UpdateTime uint64 `json:"updateTime"`
UserManaged bool `json:"userManaged"`
Vgpus []int `json:"vgpus"`
VinsConnected int `json:"vinsConnected"`
VirtualImageID int `json:"virtualImageId"`
}
const ComputeListAPI = "/restmachine/cloudapi/compute/list"
type ComputeListParam struct {
IncludeDeleted bool `json:"includedeleted"`
}
type ComputeListResp []ComputeRecord
//
// structures related to /cloudapi/compute/get
//
type SnapshotRecord struct {
Guid string `json:"guid"`
Label string `json:"label"`
SnapSetGuid string `json:"snapSetGuid"`
SnapSetTime uint64 `json:"snapSetTime"`
TimeStamp uint64 `json:"timestamp"`
}
type DiskRecord struct {
// ACLs `json:"ACL"` - it is a dictionary, special parsing required
// was - Acl map[string]string `json:"acl"`
TenantID int `json:"accountId"`
BootPartition int `json:"bootPartition"`
CreatedTime uint64 `json:"creationTime"`
DeletedTime uint64 `json:"deletionTime"`
Description string `json:"descr"`
DestructionTime uint64 `json:"destructionTime"`
DiskPath string `json:"diskPath"`
GridID int `json:"gid"`
ID uint `json:"id"`
ImageID int `json:"imageId"`
Images []int `json:"images"`
// IOTune 'json:"iotune" - it is a dictionary
Name string `json:"name"`
ParentId int `json:"parentId"`
PciSlot int `json:"pciSlot"`
// ResID string `json:"resId"`
// ResName string `json:"resName"`
// Params string `json:"params"`
Pool string `json:"pool"`
PurgeTime uint64 `json:"purgeTime"`
// Role string `json:"role"`
SepType string `json:"sepType"`
SepID int `json:"sepid"`
SizeMax int `json:"sizeMax"`
SizeUsed int `json:"sizeUsed"`
Snapshots []SnapshotRecord `json:"snapshots"`
Status string `json:"status"`
TechStatus string `json:"techStatus"`
Type string `json:"type"`
ComputeID int `json:"vmId"`
}
type OsUserRecord struct {
Guid string `json:"guid"`
Login string `json:"login"`
Password string `json:"password"`
PubKey string `json:"pubkey"`
}
const ComputeGetAPI = "/restmachine/cloudapi/compute/get"
type ComputeGetParam struct {
ComputeID int `json:"computeId"`
}
type ComputeGetResp struct {
// ACLs `json:"ACL"` - it is a dictionary, special parsing required
TenantID int `json:"accountId"`
TenantName string `json:"accountName"`
Arch string `json:"arch"`
BootDiskSize int `json:"bootdiskSize"`
CloneReference int `json:"cloneReference"`
Clones []int `json:"clones"`
Cpus int `json:"cpus"`
Desc string `json:"desc"`
Disks []DiskRecord `json:"disks"`
GridID int `json:"gid"`
ID uint `json:"id"`
ImageID int `json:"imageId"`
ImageName string `json:"imageName"`
Interfaces []InterfaceRecord `json:"interfaces`
LockStatus string `json:"lockStatus"`
ManagerID int `json:"managerId"`
ManagerType string `json:"manageType"`
Name string `json:"name"`
NatableVinsID int `json:"natableVinsId"`
NatableVinsIP string `json:"natableVinsIp"`
NatableVinsName string `json:"natableVinsName"`
NatableVinsNet string `json:"natableVinsNetwork"`
NatableVinsNetName string `json:"natableVinsNetworkName"`
OsUsers []OsUserRecord `json:"osUsers"`
Ram int `json:"ram"`
RgID int `json:"rgId"`
RgName string `json:"rgName"`
SnapSets []SnapSetRecord `json:"snapSets"`
Status string `json:"status"`
Tags []string `json:"tags"`
TechStatus string `json:"techStatus"`
TotalDiskSize int `json:"totalDiskSize"`
UpdatedBy string `json:"updatedBy"`
UpdateTime uint64 `json:"updateTime"`
UserManaged bool `json:"userManaged"`
Vgpus []int `json:"vgpus"`
VinsConnected int `json:"vinsConnected"`
VirtualImageID int `json:"virtualImageId"`
}
//
// structures related to /restmachine/cloudapi/images/list API
//
type ImageRecord struct {
TenantID uint `json:"accountId"`
Arch string `json:"architecture`
BootType string `json:"bootType"`
IsBootable boo `json:"bootable"`
IsCdrom bool `json:"cdrom"`
Desc string `json:"description"`
IsHotResize bool `json:"hotResize"`
ID uint `json:"id"`
Name string `json:"name"`
Pool string `json:"pool"`
SepID int `json:"sepid"`
Size int `json:"size"`
Status string `json:"status"`
Type string `json:"type"`
Username string `json:"username"`
IsVirtual bool `json:"virtual"`
}
const ImagesListAPI = "/restmachine/cloudapi/images/list"
type ImagesListParam struct {
TenantID int `json:"accountId"`
}
type ImagesListResp []ImageRecord
//
// structures related to /cloudapi/extnet/list API
//
type ExtNetRecord struct {
Name string `json:"name"`
ID uint `json:"id"`
IPCIDR string `json:"ipcidr"`
}
const ExtNetListAPI = "/restmachine/cloudapi/extnet/list"
type ExtNetListParam struct {
TenantID int `json:"accountId"`
}
type ExtNetListResp []ExtNetRecord
//
// structures related to /cloudapi/accounts/list API
//
type TenantRecord struct {
ACLs []UserAclRecord `json:"acl"`
CreatedTime uint64 `json:"creationTime"`
DeletedTime uint64 `json:"deletionTime"`
ID int `json:"id"`
Name string `json:"name"`
Status string `json:"status"`
UpdatedTime uint64 `json:"updateTime"`
}
const TenantsListAPI = "/restmachine/cloudapi/accounts/list"
type TenantsListResp []TenantRecord
//
// structures related to /cloudapi/portforwarding/list API
//
type PfwRecord struct {
ID int `json:"id"`
LocalIP string `json:"localIp`
LocalPort int `json:"localPort"`
Protocol string `json:"protocol"`
PublicPortEnd int `json:"publicPortEnd"`
PublicPortStart int `json:"publicPortStart"`
ComputeID int `json:"vmId"`
}
const ComputePfwListAPI = "/restmachine/cloudapi/compute/pfwList"
type ComputePfwListResp []PfwRecord
type ComputePfwAddParam struct {
ComputeID int `json:"computeId"`
PublicPortStart int `json:"publicPortStart"`
PublicPortEnd int `json:"publicPortEnd"`
LocalBasePort int `json:"localBasePort"`
Protocol string `json:"proto"`
}
const ComputePfwAddAPI = "/restmachine/cloudapi/compute/pfwAdd"
type ComputePfwDelParam struct {
ComputeID int `json:"computeId"`
RuleID int `json:"ruleId"`
PublicPortStart int `json:"publicPortStart"`
PublicPortEnd int `json:"publicPortEnd"`
LocalBasePort int `json:"localBasePort"`
Protocol string `json:"proto"`
}
const ComputePfwDelAPI = "/restmachine/cloudapi/compute/pfwDel"
//
// structures related to /cloudapi/compute/net Attach/Detach API
//
type ComputeNetAttachParam struct {
ComputeID int `json:"computeId"`
NetType string `json:"netType"`
NetID int `json:"netId"`
IPAddr string `json:"apAddr"`
}
const ComputeNetAttachAPI = "/restmachine/cloudapi/compute/netAttach"
type ComputeNetDetachParam struct {
ComputeID int `json:"computeId"`
IPAddr string `json:"apAddr"`
MAC string `json:"mac"`
}
const ComputeNetDetachAPI = "/restmachine/cloudapi/compute/netDetach"
//
// structures related to /cloudapi/compute/disk Attach/Detach API
//
type ComputeDiskManipulationParam struct {
ComputeID int `json:"computeId"`
DiskID int `json:"diskId"`
}
const ComputeDiskAttachAPI = "/restmachine/cloudapi/compute/diskAttach"
const ComputeDiskDetachAPI = "/restmachine/cloudapi/compute/diskDetach"
//
// structures related to /cloudapi/disks/create
//
type DiskCreateParam struct {
TenantID int `json:"accountId`
GridID int `json:"gid"`
Name string `json:"string"`
Description string `json:"description"`
Size int `json:"size"`
Type string `json:"type"`
SepID int `json:"sep_id"`
Pool string `json:"pool"`
}
const DiskCreateAPI = "/restmachine/cloudapi/disks/create"
//
// structures related to /cloudapi/disks/get
//
type DisksGetParam struct {
DiskID int `json:"diskId`
}
const DisksCreateAPI = "/restmachine/cloudapi/disks/create"
const DisksGetAPI = "/restmachine/cloudapi/disks/get" // Returns single DiskRecord on success

@ -0,0 +1,93 @@
/*
Copyright (c) 2019-2020 Digital Energy Cloud Solutions LLC. All Rights Reserved.
Author: Sergey Shubin, <sergey.shubin@digitalenergy.online>, <svs1370@gmail.com>
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
type DiskConfig struct {
Label string
Size int
Pool string
Provider string
ID int
}
type NetworkConfig struct {
Label string
NetworkID int
}
type PortforwardConfig struct {
Label string
ExtPort int
IntPort int
Proto string
}
type SshKeyConfig struct {
User string
SshKey string
UserShell string
}
type ComputeConfig struct {
ResGroupID int
Name string
ID int
Cpu int
Ram int
ImageID int
BootDisk DiskConfig
DataDisks []DiskConfig
Networks []NetworkConfig
PortForwards []PortforwardConfig
SshKeys []SshKeyConfig
Description string
// The following two parameters are required to create data disks by
// a separate disks/create API call
TenantID int
GridID int
// The following one paratmeter is required to create port forwards
// it will be obsoleted when we implement true Resource Groups
ExtIP string
}
type ResgroupQuotaConfig struct {
Cpu int
Ram float32 // NOTE: it is float32! However, int would be enough here
Disk int
NetTraffic int
ExtIPs int
}
type ResgroupConfig struct {
TenantID int
TenantName string
Location string
Name string
ID int
GridID int
ExtIP string // legacy field for VDC - this will eventually become obsoleted by true Resource Groups
Quota ResgroupQuotaConfig
Network NetworkConfig
}

@ -0,0 +1,278 @@
/*
Copyright (c) 2019 Digital Energy Cloud Solutions LLC. All Rights Reserved.
Author: Sergey Shubin, <sergey.shubin@digitalenergy.online>, <svs1370@gmail.com>
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 decs
import (
"log"
"strconv"
"strings"
"github.com/hashicorp/terraform/helper/schema"
"github.com/hashicorp/terraform/helper/validation"
)
func makeNetworksConfig(arg_list []interface{}) (nets []NetworkConfig, count int) {
count = len(arg_list)
if count < 1 {
return nil, 0
}
// allocate Networks list and fill it
nets = make([]NetworkConfig, count)
var subres_data map[string]interface{}
for index, value := range arg_list {
subres_data = value.(map[string]interface{})
// nets[index].Label = subres_data["label"].(string)
nets[index].NetworkID = subres_data["network_id"].(int)
}
return nets, count
}
func flattenNetworks(nets []NicRecord) []interface{} {
// this function expects an array of NicRecord as returned by machines/get API call
// NOTE: it does NOT expect a strucutre as returned by externalnetwork/list
var length = 0
var strarray []string
for _, value := range nets {
if value.NicType == "PUBLIC" {
length += 1
}
}
log.Printf("flattenNetworks: found %d NICs with PUBLIC type", length)
result := make([]interface{}, length)
if length == 0 {
return result
}
elem := make(map[string]interface{})
var subindex = 0
for index, value := range nets {
if value.NicType == "PUBLIC" {
// this will be changed as network segments entity
// value.Params for ext net comes in a form "gateway:176.118.165.1 externalnetworkId:6"
// for network_id we need to extract from this string
strarray = strings.Split(value.Params, " ")
substr := strings.Split(strarray[1], ":")
elem["network_id"], _ = strconv.Atoi(substr[1])
elem["ip_range"] = value.IPAddress
// elem["label"] = ... - should be uncommented for the future release
log.Printf("flattenNetworks: parsed element %d - network_id %d, ip_range %q",
index, elem["network_id"].(int), value.IPAddress)
result[subindex] = elem
subindex += 1
}
}
return result
}
func networkSubresourceSchema() map[string]*schema.Schema {
rets := map[string]*schema.Schema {
"network_id": {
Type: schema.TypeInt,
Required: true,
ValidateFunc: validation.IntAtLeast(1),
Description: "ID of the network to attach to this VM.",
},
/* should be uncommented for the future release
"label": {
Type: schema.TypeString,
Required: true,
Description: "Unique label of this network connection to identify it among other connections for this VM.",
},
*/
"ip_range": {
Type: schema.TypeString,
Computed: true,
Description: "Range of IP addresses defined for this network.",
},
"mac": {
Type: schema.TypeString,
Computed: true,
Description: "MAC address of the interface connected to this network.",
},
}
return rets
}
func makePortforwardsConfig(arg_list []interface{}) (pfws []PortforwardConfig, count int) {
count = len(arg_list)
if count < 1 {
return nil, 0
}
pfws = make([]PortforwardConfig, count)
var subres_data map[string]interface{}
for index, value := range arg_list {
subres_data = value.(map[string]interface{})
// pfws[index].Label = subres_data["label"].(string) - should be uncommented for future release
pfws[index].ExtPort = subres_data["ext_port"].(int)
pfws[index].IntPort = subres_data["int_port"].(int)
pfws[index].Proto = subres_data["proto"].(string)
}
return pfws, count
}
func flattenPortforwards(pfws []PortforwardRecord) []interface{} {
result := make([]interface{}, len(pfws))
elem := make(map[string]interface{})
var port_num int
for index, value := range pfws {
// elem["label"] = ... - should be uncommented for the future release
// external port field is of TypeInt in the portforwardSubresourceSchema, but string is returned
// by portforwards/list API, so we need conversion here
port_num, _ = strconv.Atoi(value.ExtPort)
elem["ext_port"] = port_num
// internal port field is of TypeInt in the portforwardSubresourceSchema, but string is returned
// by portforwards/list API, so we need conversion here
port_num, _ = strconv.Atoi(value.IntPort)
elem["int_port"] = port_num
elem["proto"] = value.Proto
elem["ext_ip"] = value.ExtIP
elem["int_ip"] = value.IntIP
result[index] = elem
}
return result
}
func portforwardSubresourceSchema() map[string]*schema.Schema {
rets := map[string]*schema.Schema {
/* this should be uncommented for the future release
"label": {
Type: schema.TypeString,
Required: true,
Description: "Unique label of this network connection to identify it amnong other connections for this VM.",
},
*/
"ext_port": {
Type: schema.TypeInt,
Required: true,
ValidateFunc: validation.IntBetween(1, 65535),
Description: "External port number for this port forwarding rule.",
},
"int_port": {
Type: schema.TypeInt,
Required: true,
ValidateFunc: validation.IntBetween(1, 65535),
Description: "Internal port number for this port forwarding rule.",
},
"proto": {
Type: schema.TypeString,
Required: true,
// ValidateFunc: validation.IntBetween(1, ),
Description: "Protocol type for this port forwarding rule. Should be either 'tcp' or 'udp'.",
},
"ext_ip": {
Type: schema.TypeString,
Computed: true,
Description: ".",
},
"int_ip": {
Type: schema.TypeString,
Computed: true,
Description: ".",
},
}
return rets
}
func flattenNICs(nics []NicRecord) []interface{} {
var result = make([]interface{}, len(nics))
elem := make(map[string]interface{})
for index, value := range nics {
elem["status"] = value.Status
elem["type"] = value.NicType
elem["mac"] = value.MacAddress
elem["ip_address"] = value.IPAddress
elem["parameters"] = value.Params
elem["reference_id"] = value.ReferenceID
elem["network_id"] = value.NetworkID
result[index] = elem
}
return result
}
func nicSubresourceSchema() map[string]*schema.Schema {
rets := map[string]*schema.Schema {
"status": {
Type: schema.TypeString,
Computed: true,
Description: "Current status of this NIC.",
},
"type": {
Type: schema.TypeString,
Computed: true,
Description: "Type of this NIC.",
},
"mac": {
Type: schema.TypeString,
Computed: true,
Description: "MAC address assigned to this NIC.",
},
"ip_address": {
Type: schema.TypeString,
Computed: true,
Description: "IP address assigned to this NIC.",
},
"parameters": {
Type: schema.TypeString,
Computed: true,
Description: "Additional NIC parameters.",
},
"reference_id": {
Type: schema.TypeString,
Computed: true,
Description: "Reference ID of this NIC.",
},
"network_id": {
Type: schema.TypeInt,
Computed: true,
Description: "Network ID which this NIC is connected to.",
},
}
return rets
}

@ -0,0 +1,127 @@
/*
Copyright (c) 2019 Digital Energy Cloud Solutions LLC. All Rights Reserved.
Author: Sergey Shubin, <sergey.shubin@digitalenergy.online>, <svs1370@gmail.com>
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 decs
import (
"strings"
"github.com/hashicorp/terraform/helper/schema"
"github.com/hashicorp/terraform/helper/validation"
// "github.com/hashicorp/terraform/terraform"
)
var decsController *ControllerCfg
func Provider() *schema.Provider {
return &schema.Provider {
Schema: map[string]*schema.Schema {
"authenticator": {
Type: schema.TypeString,
Required: true,
StateFunc: stateFuncToLower,
ValidateFunc: validation.StringInSlice([]string{"oauth2", "legacy", "jwt"}, true), // ignore case while validating
Description: "Authentication mode to use when connecting to DECS cloud API. Should be one of 'oauth2', 'legacy' or 'jwt'.",
},
"oauth2_url": {
Type: schema.TypeString,
Optional: true,
StateFunc: stateFuncToLower,
DefaultFunc: schema.EnvDefaultFunc("DECS_OAUTH2_URL", nil),
Description: "The Oauth2 application URL in 'oauth2' authentication mode.",
},
"controller_url": {
Type: schema.TypeString,
Required: true,
ForceNew: true,
StateFunc: stateFuncToLower,
Description: "The URL of DECS Cloud controller to use. API calls will be directed to this URL.",
},
"user": {
Type: schema.TypeString,
Optional: true,
DefaultFunc: schema.EnvDefaultFunc("DECS_USER", nil),
Description: "The user name for DECS cloud API operations in 'legacy' authentication mode.",
},
"password": {
Type: schema.TypeString,
Optional: true,
DefaultFunc: schema.EnvDefaultFunc("DECS_PASSWORD", nil),
Description: "The user password for DECS cloud API operations in 'legacy' authentication mode.",
},
"app_id": {
Type: schema.TypeString,
Optional: true,
DefaultFunc: schema.EnvDefaultFunc("DECS_APP_ID", nil),
Description: "Application ID to access DECS cloud API in 'oauth2' authentication mode.",
},
"app_secret": {
Type: schema.TypeString,
Optional: true,
DefaultFunc: schema.EnvDefaultFunc("DECS_APP_SECRET", nil),
Description: "Application secret to access DECS cloud API in 'oauth2' authentication mode.",
},
"jwt": {
Type: schema.TypeString,
Optional: true,
DefaultFunc: schema.EnvDefaultFunc("DECS_JWT", nil),
Description: "JWT to access DECS cloud API in 'jwt' authentication mode.",
},
"allow_unverified_ssl": {
Type: schema.TypeBool,
Optional: true,
Default: false,
Description: "If set, DECS API will allow unverifiable SSL certificates.",
},
},
ResourcesMap: map[string]*schema.Resource {
"decs_resgroup": resourceResgroup(),
"decs_vm": resourceVm(),
},
DataSourcesMap: map[string]*schema.Resource {
"decs_resgroup": dataSourceResgroup(),
"decs_vm": dataSourceVm(),
"decs_image": dataSourceImage(),
},
ConfigureFunc: providerConfigure,
}
}
func stateFuncToLower(argval interface{}) string {
return strings.ToLower(argval.(string))
}
func providerConfigure(d *schema.ResourceData) (interface{}, error) {
decsController, err := ControllerConfigure(d)
if err != nil {
return nil, err
}
return decsController, nil
}

@ -0,0 +1,119 @@
/*
Copyright (c) 2019 Digital Energy Cloud Solutions LLC. All Rights Reserved.
Author: Sergey Shubin, <sergey.shubin@digitalenergy.online>, <svs1370@gmail.com>
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 decs
import (
// "encoding/json"
// "fmt"
// "log"
// "net/url"
"github.com/hashicorp/terraform/helper/schema"
// "github.com/hashicorp/terraform/helper/validation"
)
func makeQuotaConfig(arg_list []interface{}) (ResgroupQuotaConfig, int) {
quota := ResgroupQuotaConfig{
Cpu: -1,
Ram: -1,
Disk: -1,
NetTraffic: -1,
ExtIPs: -1,
}
subres_data := arg_list[0].(map[string]interface{})
if subres_data["cpu"].(int) > 0 {
quota.Cpu = subres_data["cpu"].(int)
}
if subres_data["disk"].(int) > 0 {
quota.Disk = subres_data["disk"].(int)
}
if subres_data["ram"].(int) > 0 {
ram_limit := subres_data["ram"].(int)
quota.Ram = float32(ram_limit) // /1024 // legacy fix - this can be obsoleted once redmine FR #1465 is implemented
}
if subres_data["net_traffic"].(int) > 0 {
quota.NetTraffic = subres_data["net_traffic"].(int)
}
if subres_data["ext_ips"].(int) > 0 {
quota.ExtIPs = subres_data["ext_ips"].(int)
}
return quota, 1
}
func flattenQuota(quotas QuotaRecord) []interface{} {
quotas_map := make(map[string]interface{})
quotas_map["cpu"] = quotas.Cpu
quotas_map["ram"] = int(quotas.Ram)
quotas_map["disk"] = quotas.Disk
quotas_map["net_traffic"] = quotas.NetTraffic
quotas_map["ext_ips"] = quotas.ExtIPs
result := make([]interface{}, 1)
result[0] = quotas_map
return result
}
func quotasSubresourceSchema() map[string]*schema.Schema {
rets := map[string]*schema.Schema {
"cpu": &schema.Schema {
Type: schema.TypeInt,
Optional: true,
Default: -1,
Description: "The quota on the total number of CPUs in this resource group.",
},
"ram": &schema.Schema {
Type: schema.TypeInt, // NB: API expects and returns this as float! This may be changed in the future.
Optional: true,
Default: -1,
Description: "The quota on the total amount of RAM in this resource group, specified in GB (Gigabytes!).",
},
"disk": &schema.Schema {
Type: schema.TypeInt,
Optional: true,
Default: -1,
Description: "The quota on the total volume of storage resources in this resource group, specified in GB.",
},
"net_traffic": &schema.Schema {
Type: schema.TypeInt,
Optional: true,
Default: -1,
Description: "The quota on the total ingress network traffic for this resource group, specified in GB.",
},
"ext_ips": &schema.Schema {
Type: schema.TypeInt,
Optional: true,
Default: -1,
Description: "The quota on the total number of external IP addresses this resource group can use.",
},
}
return rets
}

@ -0,0 +1,485 @@
/*
Copyright (c) 2019-2020 Digital Energy Cloud Solutions LLC. All Rights Reserved.
Author: Sergey Shubin, <sergey.shubin@digitalenergy.online>, <svs1370@gmail.com>
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"
"log"
"net/url"
"strconv"
"github.com/hashicorp/terraform/helper/schema"
"github.com/hashicorp/terraform/helper/validation"
)
func resourceComputeCreate(d *schema.ResourceData, m interface{}) error {
machine := &MachineConfig{
ResGroupID: d.Get("rgid").(int),
Name: d.Get("name").(string),
Cpu: d.Get("cpu").(int),
Ram: d.Get("ram").(int),
ImageID: d.Get("image_id").(int),
Description: d.Get("description").(string),
}
// BootDisk
// DataDisks
// Networks
// PortForwards
// SshKeyData string
log.Printf("resourceComputeCreate: called for VM name %q, ResGroupID %d", machine.Name, machine.ResGroupID)
var subres_list []interface{}
var subres_data map[string]interface{}
var arg_value interface{}
var arg_set bool
// boot disk list is a required argument and has only one element,
// which is of type diskSubresourceSchema
subres_list = d.Get("boot_disk").([]interface{})
subres_data = subres_list[0].(map[string]interface{})
machine.BootDisk.Label = subres_data["label"].(string)
machine.BootDisk.Size = subres_data["size"].(int)
machine.BootDisk.Pool = subres_data["pool"].(string)
machine.BootDisk.Provider = subres_data["provider"].(string)
arg_value, arg_set = d.GetOk("data_disks")
if arg_set {
log.Printf("resourceComputeCreate: calling makeDisksConfig")
machine.DataDisks, _ = makeDisksConfig(arg_value.([]interface{}))
}
arg_value, arg_set = d.GetOk("networks")
if arg_set {
log.Printf("resourceComputeCreate: calling makeNetworksConfig")
machine.Networks, _ = makeNetworksConfig(arg_value.([]interface{}))
}
arg_value, arg_set = d.GetOk("port_forwards")
if arg_set {
log.Printf("resourceComputeCreate: calling makePortforwardsConfig")
machine.PortForwards, _ = makePortforwardsConfig(arg_value.([]interface{}))
}
arg_value, arg_set = d.GetOk("ssh_keys")
if arg_set {
log.Printf("resourceComputeCreate: calling makeSshKeysConfig")
machine.SshKeys, _ = makeSshKeysConfig(arg_value.([]interface{}))
}
// create basic VM (i.e. without port forwards and ext network connections - those will be done
// by separate API calls)
d.Partial(true)
controller := m.(*ControllerCfg)
url_values := &url.Values{}
url_values.Add("cloudspaceId", fmt.Sprintf("%d", machine.ResGroupID))
url_values.Add("name", machine.Name)
url_values.Add("description", machine.Description)
url_values.Add("vcpus", fmt.Sprintf("%d", machine.Cpu))
url_values.Add("memory", fmt.Sprintf("%d", machine.Ram))
url_values.Add("imageId", fmt.Sprintf("%d", machine.ImageID))
url_values.Add("disksize", fmt.Sprintf("%d", machine.BootDisk.Size))
if len(machine.SshKeys) > 0 {
url_values.Add("userdata", makeSshKeysArgString(machine.SshKeys))
}
api_resp, err := controller.decortAPICall("POST", MachineCreateAPI, url_values)
if err != nil {
return err
}
d.SetId(api_resp) // machines/create API plainly returns ID of the new VM on success
machine.ID, _ = strconv.Atoi(api_resp)
d.SetPartial("name")
d.SetPartial("description")
d.SetPartial("cpu")
d.SetPartial("ram")
d.SetPartial("image_id")
d.SetPartial("boot_disk")
if len(machine.SshKeys) > 0 {
d.SetPartial("ssh_keys")
}
log.Printf("resourceComputeCreate: new VM ID %d, name %q created", machine.ID, machine.Name)
if len(machine.DataDisks) > 0 || len(machine.PortForwards) > 0 {
// for data disk or port foreards provisioning we have to know Tenant ID
// and Grid ID so we call utilityResgroupConfigGet method to populate these
// fields in the machine structure that will be passed to provisionVmDisks or
// provisionVmPortforwards
log.Printf("resourceComputeCreate: calling utilityResgroupConfigGet")
resgroup, err := controller.utilityResgroupConfigGet(machine.ResGroupID)
if err == nil {
machine.TenantID = resgroup.TenantID
machine.GridID = resgroup.GridID
machine.ExtIP = resgroup.ExtIP
log.Printf("resourceComputeCreate: tenant ID %d, GridID %d, ExtIP %q",
machine.TenantID, machine.GridID, machine.ExtIP)
}
}
//
// Configure data disks
disks_ok := true
if len(machine.DataDisks) > 0 {
log.Printf("resourceComputeCreate: calling utilityVmDisksProvision for disk count %d", len(machine.DataDisks))
if machine.TenantID == 0 {
// if TenantID is still 0 it means that we failed to get Resgroup Facts by
// a previous call to utilityResgroupGetFacts,
// hence we do not have technical ability to provision data disks
disks_ok = false
} else {
// provisionVmDisks accomplishes two steps for each data disk specification
// 1) creates the disks
// 2) attaches them to the VM
err = controller.utilityVmDisksProvision(machine)
if err != nil {
disks_ok = false
}
}
}
if disks_ok {
d.SetPartial("data_disks")
}
//
// Configure port forward rules
pfws_ok := true
if len(machine.PortForwards) > 0 {
log.Printf("resourceComputeCreate: calling utilityVmPortforwardsProvision for pfw rules count %d", len(machine.PortForwards))
if machine.ExtIP == "" {
// if ExtIP is still empty it means that we failed to get Resgroup Facts by
// a previous call to utilityResgroupGetFacts,
// hence we do not have technical ability to provision port forwards
pfws_ok = false
} else {
err := controller.utilityVmPortforwardsProvision(machine)
if err != nil {
pfws_ok = false
}
}
}
if pfws_ok {
// there were no errors reported when configuring port forwards
d.SetPartial("port_forwards")
}
//
// Configure external networks
// NOTE: currently only one external network can be attached to each VM, so in the current
// implementation we ignore all but the 1st network definition
nets_ok := true
if len(machine.Networks) > 0 {
log.Printf("resourceComputeCreate: calling utilityVmNetworksProvision for networks count %d", len(machine.Networks))
err := controller.utilityVmNetworksProvision(machine)
if err != nil {
nets_ok = false
}
}
if nets_ok {
// there were no errors reported when configuring networks
d.SetPartial("networks")
}
if ( disks_ok && nets_ok && pfws_ok ) {
// if there were no errors in setting any of the subresources, we may leave Partial mode
d.Partial(false)
}
// resourceComputeRead will also update resource ID on success, so that Terraform will know
// that resource exists
return resourceComputeRead(d, m)
}
func resourceComputeRead(d *schema.ResourceData, m interface{}) error {
log.Printf("resourceComputeRead: called for VM name %q, ResGroupID %d",
d.Get("name").(string), d.Get("rgid").(int))
comp_facts, err := utilityComputeCheckPresence(d, m)
if comp_facts == "" {
if err != nil {
return err
}
// VM was not found
return nil
}
if err = flattenCompute(d, comp_facts); err != nil {
return err
}
log.Printf("resourceComputeRead: after flattenCompute: VM ID %s, VM name %q, ResGroupID %d",
d.Id(), d.Get("name").(string), d.Get("rgid").(int))
// Not all parameters, that we may need, are returned by machines/get API
// Continue with further reading of VM subresource parameters:
controller := m.(*ControllerCfg)
url_values := &url.Values{}
/*
// Obtain information on external networks
url_values.Add("machineId", d.Id())
body_string, err := controller.decortAPICall("POST", VmExtNetworksListAPI, url_values)
if err != nil {
return err
}
net_list := ExtNetworksResp{}
err = json.Unmarshal([]byte(body_string), &net_list)
if err != nil {
return err
}
if len(net_list) > 0 {
if err = d.Set("networks", flattenNetworks(net_list)); err != nil {
return err
}
}
*/
/*
// Ext networks flattening is now done inside flattenCompute because it is currently based
// on data read into NICs component by machine/get API call
if err = d.Set("networks", flattenNetworks()); err != nil {
return err
}
*/
//
// Obtain information on port forwards
url_values.Add("cloudspaceId", fmt.Sprintf("%d",d.Get("rgid")))
url_values.Add("machineId", d.Id())
pfw_list := PortforwardsResp{}
body_string, err := controller.decortAPICall("POST", PortforwardsListAPI, url_values)
if err != nil {
return err
}
err = json.Unmarshal([]byte(body_string), &pfw_list)
if err != nil {
return err
}
if len(pfw_list) > 0 {
if err = d.Set("port_forwards", flattenPortforwards(pfw_list)); err != nil {
return err
}
}
return nil
}
func resourceComputeUpdate(d *schema.ResourceData, m interface{}) error {
log.Printf("resourceComputeUpdate: called for VM name %q, ResGroupID %d",
d.Get("name").(string), d.Get("rgid").(int))
return resourceComputeRead(d, m)
}
func resourceComputeDelete(d *schema.ResourceData, m interface{}) error {
// NOTE: this method destroys target VM with flag "permanently", so there is no way to
// restore destroyed VM
log.Printf("resourceComputeDelete: called for VM name %q, ResGroupID %d",
d.Get("name").(string), d.Get("rgid").(int))
comp_facts, err := utilityComputeCheckPresence(d, m)
if comp_facts == "" {
// the target VM 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("machineId", d.Id())
params.Add("permanently", "true")
controller := m.(*ControllerCfg)
comp_facts, err = controller.decortAPICall("POST", MachineDeleteAPI, params)
if err != nil {
return err
}
return nil
}
func resourceComputeExists(d *schema.ResourceData, m interface{}) (bool, error) {
// Reminder: according to Terraform rules, this function should not modify its ResourceData argument
log.Printf("resourceComputeExist: called for VM name %q, ResGroupID %d",
d.Get("name").(string), d.Get("rgid").(int))
comp_facts, err := utilityComputeCheckPresence(d, m)
if comp_facts == "" {
if err != nil {
return false, err
}
return false, nil
}
return true, nil
}
func resourceCompute() *schema.Resource {
return &schema.Resource {
SchemaVersion: 1,
Create: resourceComputeCreate,
Read: resourceComputeRead,
Update: resourceComputeUpdate,
Delete: resourceComputeDelete,
Exists: resourceComputeExists,
Timeouts: &schema.ResourceTimeout {
Create: &Timeout180s,
Read: &Timeout30s,
Update: &Timeout180s,
Delete: &Timeout60s,
Default: &Timeout60s,
},
Schema: map[string]*schema.Schema {
"name": {
Type: schema.TypeString,
Required: true,
Description: "Name of this virtual machine. This parameter is case sensitive.",
},
"rgid": {
Type: schema.TypeInt,
Required: true,
ValidateFunc: validation.IntAtLeast(1),
Description: "ID of the resource group where this virtual machine should be deployed.",
},
"cpu": {
Type: schema.TypeInt,
Required: true,
ValidateFunc: validation.IntBetween(1, 64),
Description: "Number of CPUs to allocate to this virtual machine.",
},
"ram": {
Type: schema.TypeInt,
Required: true,
ValidateFunc: validation.IntAtLeast(512),
Description: "Amount of RAM in MB to allocate to this virtual machine.",
},
"image_id": {
Type: schema.TypeInt,
Required: true,
ForceNew: true,
Description: "ID of the OS image to base this virtual machine on.",
},
"boot_disk": {
Type: schema.TypeList,
Required: true,
MaxItems: 1,
Elem: &schema.Resource {
Schema: diskSubresourceSchema(),
},
Description: "Specification for a boot disk on this virtual machine.",
},
"data_disks": {
Type: schema.TypeList,
Optional: true,
MaxItems: 12,
Elem: &schema.Resource {
Schema: diskSubresourceSchema(),
},
Description: "Specification for data disks on this virtual machine.",
},
"guest_logins": {
Type: schema.TypeList,
Computed: true,
Elem: &schema.Resource {
Schema: loginsSubresourceSchema(),
},
Description: "Specification for guest logins on this virtual machine.",
},
"networks": {
Type: schema.TypeList,
Optional: true,
MaxItems: 8,
Elem: &schema.Resource {
Schema: networkSubresourceSchema(),
},
Description: "Specification for the networks to connect this virtual machine to.",
},
"nics": {
Type: schema.TypeList,
Computed: true,
MaxItems: 8,
Elem: &schema.Resource {
Schema: nicSubresourceSchema(),
},
Description: "Specification for the virutal NICs allocated to this virtual machine.",
},
"ssh_keys": {
Type: schema.TypeList,
Optional: true,
MaxItems: 12,
Elem: &schema.Resource {
Schema: sshSubresourceSchema(),
},
Description: "SSH keys to authorize on this virtual machine.",
},
"port_forwards": {
Type: schema.TypeList,
Optional: true,
MaxItems: 12,
Elem: &schema.Resource {
Schema: portforwardSubresourceSchema(),
},
Description: "Specification for the port forwards to configure for this virtual machine.",
},
"description": {
Type: schema.TypeString,
Optional: true,
Description: "Description of this virtual machine.",
},
"user": {
Type: schema.TypeString,
Computed: true,
Description: "Default login name for the guest OS on this virtual machine.",
},
"password": {
Type: schema.TypeString,
Computed: true,
Sensitive: true,
Description: "Default password for the guest OS login on this virtual machine.",
},
},
}
}

@ -0,0 +1,297 @@
/*
Copyright (c) 2019-2020 Digital Energy Cloud Solutions. All Rights Reserved.
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 (
"fmt"
"log"
"net/url"
"strconv"
"github.com/hashicorp/terraform/helper/schema"
)
func resourceResgroupCreate(d *schema.ResourceData, m interface{}) error {
log.Printf("resourceResgroupCreate: called for res group name %q, tenant name %q",
d.Get("name").(string), d.Get("tenant").(string))
rg := &ResgroupConfig{
Name: d.Get("name").(string),
TenantName: d.Get("tenant").(string),
}
// validate that we have all parameters required to create the new Resource Group
// location code is required to create new resource group
arg_value, arg_set := d.GetOk("location")
if arg_set {
rg.Location = arg_value.(string)
} else {
return fmt.Errorf("Cannot create new resource group %q for tenant %q: missing location parameter.",
rg.Name, rg.TenantName)
}
// tenant ID is required to create new resource group
// obtain Tenant ID by tenant name - it should not be zero on success
tenant_id, err := utilityGetTenantIdByName(rg.TenantName, m)
if err != nil {
return err
}
rg.TenantID = tenant_id
set_quotas := false
arg_value, arg_set = d.GetOk("quotas")
if arg_set {
log.Printf("resourceResgroupCreate: calling makeQuotaConfig")
rg.Quota, _ = makeQuotaConfig(arg_value.([]interface{}))
set_quotas = true
}
controller := m.(*ControllerCfg)
log.Printf("resourceResgroupCreate: called by user %q for Resource group name %q, for tenant %q / ID %d, location %q",
controller.getdecortUsername(),
rg.Name, d.Get("tenant"), rg.TenantID, rg.Location)
url_values := &url.Values{}
url_values.Add("accountId", fmt.Sprintf("%d", rg.TenantID))
url_values.Add("name", rg.Name)
url_values.Add("location", rg.Location)
url_values.Add("access", controller.getdecortUsername())
// pass quota values as set
if set_quotas {
url_values.Add("maxCPUCapacity", fmt.Sprintf("%d", rg.Quota.Cpu))
url_values.Add("maxVDiskCapacity", fmt.Sprintf("%d", rg.Quota.Disk))
url_values.Add("maxMemoryCapacity", fmt.Sprintf("%f", rg.Quota.Ram))
url_values.Add("maxNetworkPeerTransfer", fmt.Sprintf("%d", rg.Quota.NetTraffic))
url_values.Add("maxNumPublicIP", fmt.Sprintf("%d", rg.Quota.ExtIPs))
}
// pass externalnetworkid if set
arg_value, arg_set = d.GetOk("extnet_id")
if arg_set {
url_values.Add("externalnetworkid", fmt.Sprintf("%d", arg_value))
}
api_resp, err := controller.decortAPICall("POST", ResgroupCreateAPI, url_values)
if err != nil {
return err
}
d.SetId(api_resp) // cloudspaces/create API plainly returns ID of the newly creted resource group on success
rg.ID, _ = strconv.Atoi(api_resp)
return resourceResgroupRead(d, m)
}
func resourceResgroupRead(d *schema.ResourceData, m interface{}) error {
log.Printf("resourceResgroupRead: called for res group name %q, tenant name %q",
d.Get("name").(string), d.Get("tenant").(string))
rg_facts, err := utilityResgroupCheckPresence(d, m)
if rg_facts == "" {
// if empty string is returned from utilityResgroupCheckPresence then there is no
// such resource group and err tells so - just return it to the calling party
d.SetId("") // ensure ID is empty
return err
}
return flattenResgroup(d, rg_facts)
}
func resourceResgroupUpdate(d *schema.ResourceData, m interface{}) error {
// this method will only update quotas, if any are set
log.Printf("resourceResgroupUpdate: called for res group name %q, tenant name %q",
d.Get("name").(string), d.Get("tenant").(string))
quota_value, arg_set := d.GetOk("quotas")
if !arg_set {
// if there are no quotas set explicitly in the resource configuration - no change will be done
log.Printf("resourceResgroupUpdate: quotas are not set in the resource config - no update on this resource will be done")
return resourceResgroupRead(d, m)
}
quotaconfig_new, _ := makeQuotaConfig(quota_value.([]interface{}))
quota_value, _ = d.GetChange("quotas") // returns old as 1st, new as 2nd argument
quotaconfig_old, _ := makeQuotaConfig(quota_value.([]interface{}))
controller := m.(*ControllerCfg)
url_values := &url.Values{}
url_values.Add("cloudspaceId", d.Id())
url_values.Add("name", d.Get("name").(string))
do_update := false
if quotaconfig_new.Cpu != quotaconfig_old.Cpu {
do_update = true
log.Printf("resourceResgroupUpdate: Cpu diff %d <- %d", quotaconfig_new.Cpu, quotaconfig_old.Cpu)
url_values.Add("maxCPUCapacity", fmt.Sprintf("%d", quotaconfig_new.Cpu))
}
if quotaconfig_new.Disk != quotaconfig_old.Disk {
do_update = true
log.Printf("resourceResgroupUpdate: Disk diff %d <- %d", quotaconfig_new.Disk, quotaconfig_old.Disk)
url_values.Add("maxVDiskCapacity", fmt.Sprintf("%d", quotaconfig_new.Disk))
}
if quotaconfig_new.Ram != quotaconfig_old.Ram {
do_update = true
log.Printf("resourceResgroupUpdate: Ram diff %f <- %f", quotaconfig_new.Ram, quotaconfig_old.Ram)
url_values.Add("maxMemoryCapacity", fmt.Sprintf("%f", quotaconfig_new.Ram))
}
if quotaconfig_new.NetTraffic != quotaconfig_old.NetTraffic {
do_update = true
log.Printf("resourceResgroupUpdate: NetTraffic diff %d <- %d", quotaconfig_new.NetTraffic, quotaconfig_old.NetTraffic)
url_values.Add("maxNetworkPeerTransfer", fmt.Sprintf("%d", quotaconfig_new.NetTraffic))
}
if quotaconfig_new.ExtIPs != quotaconfig_old.ExtIPs {
do_update = true
log.Printf("resourceResgroupUpdate: ExtIPs diff %d <- %d", quotaconfig_new.ExtIPs, quotaconfig_old.ExtIPs)
url_values.Add("maxNumPublicIP", fmt.Sprintf("%d", quotaconfig_new.ExtIPs))
}
if do_update {
log.Printf("resourceResgroupUpdate: some new quotas are set - updating the resource")
_, err := controller.decortAPICall("POST", ResgroupUpdateAPI, url_values)
if err != nil {
return err
}
} else {
log.Printf("resourceResgroupUpdate: no difference in quotas between old and new state - no update on this resource will be done")
}
return resourceResgroupRead(d, m)
}
func resourceResgroupDelete(d *schema.ResourceData, m interface{}) error {
// NOTE: this method destroys target resource group with flag "permanently", so there is no way to
// restore the destroyed resource group as well all VMs that existed in it
log.Printf("resourceResgroupDelete: called for res group name %q, tenant name %q",
d.Get("name").(string), d.Get("tenant").(string))
vm_facts, err := utilityResgroupCheckPresence(d, m)
if vm_facts == "" {
// the target VM 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("cloudspaceId", d.Id())
params.Add("permanently", "true")
controller := m.(*ControllerCfg)
vm_facts, err = controller.decortAPICall("POST", CloudspacesDeleteAPI, params)
if err != nil {
return err
}
return nil
}
func resourceResgroupExists(d *schema.ResourceData, m interface{}) (bool, error) {
// Reminder: according to Terraform rules, this function should not modify ResourceData argument
rg_facts, err := utilityResgroupCheckPresence(d, m)
if rg_facts == "" {
if err != nil {
return false, err
}
return false, nil
}
return true, nil
}
func resourceResgroup() *schema.Resource {
return &schema.Resource {
SchemaVersion: 1,
Create: resourceResgroupCreate,
Read: resourceResgroupRead,
Update: resourceResgroupUpdate,
Delete: resourceResgroupDelete,
Exists: resourceResgroupExists,
Timeouts: &schema.ResourceTimeout {
Create: &Timeout180s,
Read: &Timeout30s,
Update: &Timeout180s,
Delete: &Timeout60s,
Default: &Timeout60s,
},
Schema: map[string]*schema.Schema {
"name": &schema.Schema {
Type: schema.TypeString,
Required: true,
Description: "Name of this resource group. Names are case sensitive and unique within the context of a tenant.",
},
"tenant": &schema.Schema {
Type: schema.TypeString,
Required: true,
Description: "Name of the tenant, which this resource group belongs to.",
},
"extnet_id": &schema.Schema {
Type: schema.TypeInt,
Optional: true,
Description: "ID of the external network, which this resource group will be connected to by default.",
},
"tenant_id": &schema.Schema {
Type: schema.TypeInt,
Computed: true,
Description: "Unique ID of the tenant, which this resource group belongs to.",
},
"grid_id": &schema.Schema {
Type: schema.TypeInt,
Computed: true,
Description: "Unique ID of the grid, where this resource group is deployed.",
},
"location": &schema.Schema {
Type: schema.TypeString,
Optional: true, // note that it is a REQUIRED parameter when creating new resource group
ForceNew: true,
Description: "Name of the location where this resource group should exist.",
},
"public_ip": { // this may be obsoleted as new network segments and true resource groups are implemented
Type: schema.TypeString,
Computed: true,
Description: "Public IP address of this resource group (if any).",
},
"quotas": {
Type: schema.TypeList,
Optional: true,
MaxItems: 1,
Elem: &schema.Resource {
Schema: quotasSubresourceSchema(),
},
Description: "Quotas on the resources for this resource group.",
},
},
}
}

@ -0,0 +1,97 @@
/*
Copyright (c) 2019 Digital Energy Cloud Solutions LLC. All Rights Reserved.
Author: Sergey Shubin, <sergey.shubin@digitalenergy.online>, <svs1370@gmail.com>
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 decs
import (
"fmt"
"github.com/hashicorp/terraform/helper/schema"
// "github.com/hashicorp/terraform/helper/validation"
)
func makeSshKeysConfig(arg_list []interface{}) (sshkeys []SshKeyConfig, count int) {
count = len(arg_list)
if count < 1 {
return nil, 0
}
sshkeys = make([]SshKeyConfig, count)
var subres_data map[string]interface{}
for index, value := range arg_list {
subres_data = value.(map[string]interface{})
sshkeys[index].User = subres_data["user"].(string)
sshkeys[index].SshKey = subres_data["public_key"].(string)
sshkeys[index].UserShell = subres_data["shell"].(string)
}
return sshkeys, count
}
func makeSshKeysArgString(sshkeys []SshKeyConfig) string {
// Prepare a string with username and public ssh key value in a format recognized by cloud-init utility.
// It is designed to be passed as "userdata" argument of virtual machine create API call.
// The following format is expected:
// '{"users": [{"ssh-authorized-keys": ["SSH_PUBCIC_KEY_VALUE"], "shell": "SHELL_VALUE", "name": "USERNAME_VALUE"}, {...}, ]}'
/*
`%s\n
- name: %s\n
ssh-authorized-keys:
- %s\n
shell: /bin/bash`
*/
if len(sshkeys) < 1 {
return ""
}
out := `{"users": [`
const UserdataTemplate = `%s{"ssh-authorized-keys": ["%s"], "shell": "%s", "name": "%s"}, `
const out_suffix = `]}`
for _, elem := range sshkeys {
out = fmt.Sprintf(UserdataTemplate, out, elem.SshKey, elem.UserShell, elem.User)
}
out = fmt.Sprintf("%s %s", out, out_suffix)
return out
}
func sshSubresourceSchema() map[string]*schema.Schema {
rets := map[string]*schema.Schema {
"user": {
Type: schema.TypeString,
Required: true,
Description: "Name of the user on the guest OS of the new VM, for which the following SSH key will be authorized.",
},
"public_key": {
Type: schema.TypeString,
Required: true,
Description: "Public part of SSH key to authorize to the specified user on the VM being created.",
},
"shell": {
Type: schema.TypeString,
Optional: true,
Default: "/bin/bash",
Description: "Guest user shell. This parameter is optional, default is /bin/bash.",
},
}
return rets
}

@ -0,0 +1,48 @@
/*
Copyright (c) 2019-2021 Digital Energy Cloud Solutions LLC. All Rights Reserved.
Author: Sergey Shubin, <sergey.shubin@digitalenergy.online>, <svs1370@gmail.com>
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 (
"strings"
)
func Jo2JSON(arg_str string) string {
// DECS API historically returns response in the form of Python dictionary, which generally
// looks like JSON, but does not comply with JSON syntax.
// For Golang JSON Unmarshal to work properly we need to pre-process API response as follows:
ret_string := strings.Replace(string(arg_str), "u'", "\"", -1)
ret_string = strings.Replace(ret_string, "'", "\"", -1)
ret_string = strings.Replace(ret_string, ": False", ": false", -1)
ret_string = strings.Replace(ret_string, ": True", ": true", -1)
ret_string = strings.Replace(ret_string, "null", "\"\"", -1)
ret_string = strings.Replace(ret_string, "None", "\"\"", -1)
// fix for incorrect handling of usage info
// ret_string = strings.Replace(ret_string, "<", "\"", -1)
// ret_string = strings.Replace(ret_string, ">", "\"", -1)
return ret_string
}

@ -0,0 +1,146 @@
/*
Copyright (c) 2019-2020 Digital Energy Cloud Solutions LLC. All Rights Reserved.
Author: Sergey Shubin, <sergey.shubin@digitalenergy.online>, <svs1370@gmail.com>
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"
"log"
"net/url"
// "strconv"
"github.com/hashicorp/terraform/helper/schema"
// "github.com/hashicorp/terraform/helper/validation"
)
func (ctrl *ControllerCfg) utilityResgroupConfigGet(rgid int) (*ResgroupConfig, error) {
url_values := &url.Values{}
url_values.Add("cloudspaceId", fmt.Sprintf("%d", rgid))
resgroup_facts, err := ctrl.decortAPICall("POST", CloudspacesGetAPI, url_values)
if err != nil {
return nil, err
}
log.Printf("utilityResgroupConfigGet: ready to unmarshal string %q", resgroup_facts)
model := CloudspacesGetResp{}
err = json.Unmarshal([]byte(resgroup_facts), &model)
if err != nil {
return nil, err
}
ret := &ResgroupConfig{}
ret.TenantID = model.TenantID
ret.Location = model.Location
ret.Name = model.Name
ret.ID = rgid
ret.GridID = model.GridID
ret.ExtIP = model.ExtIP // legacy field for VDC - this will eventually become obsoleted by true Resource Groups
// Quota ResgroupQuotaConfig
// Network NetworkConfig
log.Printf("utilityResgroupConfigGet: tenant ID %d, GridID %d, ExtIP %q",
model.TenantID, model.GridID, model.ExtIP)
return ret, nil
}
func utilityResgroupCheckPresence(d *schema.ResourceData, m interface{}) (string, error) {
// This function tries to locate resource group by its name and tenant name.
// If succeeded, it returns non empty string that contains JSON formatted facts about the
// resource group as returned by cloudspaces/get API call.
// Otherwise it returns empty string and meaningful error.
//
// This function does not modify its ResourceData argument, so it is safe to use it as core
// method for the resource's Exists method.
//
name := d.Get("name").(string)
tenant_name := d.Get("tenant").(string)
controller := m.(*ControllerCfg)
url_values := &url.Values{}
url_values.Add("includedeleted", "false")
body_string, err := controller.decortAPICall("POST", CloudspacesListAPI, url_values)
if err != nil {
return "", err
}
log.Printf("%s", body_string)
log.Printf("utilityResgroupCheckPresence: ready to decode response body from %q", CloudspacesListAPI)
model := CloudspacesListResp{}
err = json.Unmarshal([]byte(body_string), &model)
if err != nil {
return "", err
}
log.Printf("utilityResgroupCheckPresence: traversing decoded Json of length %d", len(model))
for index, item := range model {
// need to match VDC by name & tenant name
if item.Name == name && item.TenantName == tenant_name {
log.Printf("utilityResgroupCheckPresence: match ResGroup name %q / ID %d, tenant %q at index %d",
item.Name, item.ID, item.TenantName, index)
// not all required information is returned by cloudspaces/list API, so we need to initiate one more
// call to cloudspaces/get to obtain extra data to complete Resource population.
// Namely, we need to extract resource quota settings
req_values := &url.Values{}
req_values.Add("cloudspaceId", fmt.Sprintf("%d", item.ID))
body_string, err := controller.decortAPICall("POST", CloudspacesGetAPI, req_values)
if err != nil {
return "", err
}
return body_string, nil
}
}
return "", fmt.Errorf("Cannot find resource group name %q owned by tenant %q", name, tenant_name)
}
func utilityGetTenantIdByName(tenant_name string, m interface{}) (int, error) {
controller := m.(*ControllerCfg)
url_values := &url.Values{}
body_string, err := controller.decortAPICall("POST", TenantsListAPI, url_values)
if err != nil {
return 0, err
}
model := TenantsListResp{}
err = json.Unmarshal([]byte(body_string), &model)
if err != nil {
return 0, err
}
log.Printf("utilityGetTenantIdByName: traversing decoded Json of length %d", len(model))
for index, item := range model {
// need to match Tenant by name
if item.Name == tenant_name {
log.Printf("utilityGetTenantIdByName: match Tenant name %q / ID %d at index %d",
item.Name, item.ID, index)
return item.ID, nil
}
}
return 0, fmt.Errorf("Cannot find tenant %q for the current user. Check tenant value and your access rights", tenant_name)
}

@ -0,0 +1,159 @@
/*
Copyright (c) 2019-2021 Digital Energy Cloud Solutions LLC. All Rights Reserved.
Author: Sergey Shubin, <sergey.shubin@digitalenergy.online>, <svs1370@gmail.com>
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"
"github.com/hashicorp/terraform/helper/schema"
// "github.com/hashicorp/terraform/helper/validation"
)
func (ctrl *ControllerCfg) utilityVmDisksProvision(mcfg *MachineConfig) error {
for index, disk := range mcfg.DataDisks {
url_values := &url.Values{}
// url_values.Add("machineId", fmt.Sprintf("%d", mcfg.ID))
url_values.Add("accountId", fmt.Sprintf("%d", mcfg.TenantID))
url_values.Add("gid", fmt.Sprintf("%d", mcfg.GridID))
url_values.Add("name", fmt.Sprintf("%s", disk.Label))
url_values.Add("description", fmt.Sprintf("Data disk for VM ID %d / VM Name: %s", mcfg.ID, mcfg.Name))
url_values.Add("size", fmt.Sprintf("%d", disk.Size))
url_values.Add("type", "D")
// url_values.Add("iops", )
disk_id_resp, err := ctrl.decortAPICall("POST", DiskCreateAPI, url_values)
if err != nil {
// failed to create disk - partial resource update
return err
}
// disk created - API call returns disk ID as a string - use it to update
// disk ID in the corresponding MachineConfig.DiskConfig record
mcfg.DataDisks[index].ID, err = strconv.Atoi(disk_id_resp)
if err != nil {
// failed to convert disk ID into proper integer value - partial resource update
return err
}
// now that we have disk created and stored its ID in the mcfg.DataDisks[index].ID
// we can attempt attaching the disk to the VM
url_values = &url.Values{}
// url_values.Add("machineId", fmt.Sprintf("%d", mcfg.ID))
url_values.Add("machineId", fmt.Sprintf("%d", mcfg.ID))
url_values.Add("diskId", disk_id_resp)
_, err = ctrl.decortAPICall("POST", DiskAttachAPI, url_values)
if err != nil {
// failed to attach disk - partial resource update
return err
}
}
return nil
}
func (ctrl *ControllerCfg) utilityVmPortforwardsProvision(mcfg *MachineConfig) error {
for _, rule := range mcfg.PortForwards {
url_values := &url.Values{}
url_values.Add("machineId", fmt.Sprintf("%d", mcfg.ID))
url_values.Add("cloudspaceId", fmt.Sprintf("%d", mcfg.ResGroupID))
url_values.Add("publicIp", mcfg.ExtIP) // this may be obsoleted by Resource group implementation
url_values.Add("publicPort", fmt.Sprintf("%d", rule.ExtPort))
url_values.Add("localPort", fmt.Sprintf("%d", rule.IntPort))
url_values.Add("protocol", rule.Proto)
_, err := ctrl.decortAPICall("POST", PortforwardingCreateAPI, url_values)
if err != nil {
// failed to create port forward rule - partial resource update
return err
}
}
return nil
}
func (ctrl *ControllerCfg) utilityVmNetworksProvision(mcfg *MachineConfig) error {
for _, net := range mcfg.Networks {
url_values := &url.Values{}
url_values.Add("machineId", fmt.Sprintf("%d", mcfg.ID))
url_values.Add("externalNetworkId", fmt.Sprintf("%d", net.NetworkID))
_, err := ctrl.decortAPICall("POST", AttachExternalNetworkAPI, url_values)
if err != nil {
// failed to attach network - partial resource update
return err
}
}
return nil
}
func utilityVmCheckPresence(d *schema.ResourceData, m interface{}) (string, error) {
// This function tries to locate VM by its name and resource group ID
// if succeeded, it returns non empty string that contains JSON formatted facts about the VM
// as returned by machines/get API call.
// Otherwise it returns empty string and meaningful error.
//
// This function does not modify its ResourceData argument, so it is safe to use it as core
// method for resource's Exists method.
//
name := d.Get("name").(string)
rgid := d.Get("rgid").(int)
controller := m.(*ControllerCfg)
list_url_values := &url.Values{}
list_url_values.Add("cloudspaceId", fmt.Sprintf("%d",rgid))
body_string, err := controller.decortAPICall("POST", MachinesListAPI, list_url_values)
if err != nil {
return "", err
}
// log.Printf("%s", body_string)
// log.Printf("dataSourceVmRead: ready to decode mashines/list response body")
vm_list := MachinesListResp{}
err = json.Unmarshal([]byte(body_string), &vm_list)
if err != nil {
return "", err
}
// log.Printf("%#v", vm_list)
// log.Printf("dataSourceVmRead: traversing decoded JSON of length %d", len(vm_list))
for _, item := range vm_list {
// need to match VM by name, skip VMs with the same name in DESTROYED satus
if item.Name == name && item.Status != "DESTROYED" {
// log.Printf("dataSourceVmRead: index %d, matched name %q", index, item.Name)
// we found the VM we need - not get detailed information via API call to cloudapi/machines/get
get_url_values := &url.Values{}
get_url_values.Add("machineId", fmt.Sprintf("%d", item.ID))
body_string, err = controller.decortAPICall("POST", MachinesGetAPI, get_url_values)
if err != nil {
return "", err
}
return body_string, nil
}
}
return "", nil // there should be no error if VM does not exist
// return "", fmt.Errorf("Cannot find VM name %q in resource group ID %d", name, rgid)
}

@ -0,0 +1,46 @@
/*
Copyright (c) 2019-2021 Digital Energy Cloud Solutions LLC. All Rights Reserved.
Author: Sergey Shubin, <sergey.shubin@digitalenergy.online>, <svs1370@gmail.com>
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.
*/
/*
Terraform DECORT provider - manage resources provided by DECORT (Digital Energy Cloud
Orchestration Technology) with Terraform by Hashicorp.
Source code: https://github.com/rudecs/terraform-provider-decort
Please see README.md to learn where to place source code so that it
builds seamlessly.
Documentation: https://github.com/rudecs/terraform-provider-decort/wiki
*/
package main
import (
"github.com/hashicorp/terraform/plugin"
"github.com/hashicorp/terraform/terraform"
"github.com/terraform-provider-decort/decort"
)
func main() {
plugin.Serve(&plugin.ServeOpts{
ProviderFunc: func() terraform.ResourceProvider {
return decort.Provider()
},
})
}
Loading…
Cancel
Save