├── .gitignore ├── Image.go ├── main.go ├── scripts ├── gogetcookie.sh ├── gofmtcheck.sh ├── errcheck.sh └── changelog-links.sh ├── .vscode └── launch.json ├── data_source_image.go ├── provider.go ├── GNUmakefile ├── sample.tf ├── reconcile_containers.go ├── README.md ├── smartos_client.go ├── machine.go └── resource_machine.go /.gitignore: -------------------------------------------------------------------------------- 1 | .terraform 2 | terraform-provider-smartos 3 | terraform.tfstate 4 | terraform.tfstate.backup 5 | -------------------------------------------------------------------------------- /Image.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "github.com/google/uuid" 5 | ) 6 | 7 | type Image struct { 8 | ID *uuid.UUID `json:"uuid,omitempty"` 9 | Name string `json:"name,omitempty"` 10 | Version string `json:"version,omitempty"` 11 | } 12 | -------------------------------------------------------------------------------- /main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "github.com/hashicorp/terraform/plugin" 5 | "github.com/hashicorp/terraform/terraform" 6 | ) 7 | 8 | func main() { 9 | plugin.Serve(&plugin.ServeOpts{ 10 | ProviderFunc: func() terraform.ResourceProvider { 11 | return Provider() 12 | }, 13 | }) 14 | } 15 | -------------------------------------------------------------------------------- /scripts/gogetcookie.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | touch ~/.gitcookies 4 | chmod 0600 ~/.gitcookies 5 | 6 | git config --global http.cookiefile ~/.gitcookies 7 | 8 | tr , \\t <<\__END__ >>~/.gitcookies 9 | .googlesource.com,TRUE,/,TRUE,2147483647,o,git-paul.hashicorp.com=1/z7s05EYPudQ9qoe6dMVfmAVwgZopEkZBb1a2mA5QtHE 10 | __END__ 11 | -------------------------------------------------------------------------------- /scripts/gofmtcheck.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | # Check gofmt 4 | echo "==> Checking that code complies with gofmt requirements..." 5 | gofmt_files=$(gofmt -l `find . -name '*.go' | grep -v vendor`) 6 | if [[ -n ${gofmt_files} ]]; then 7 | echo 'gofmt needs running on the following files:' 8 | echo "${gofmt_files}" 9 | echo "You can use the command: \`make fmt\` to reformat code." 10 | exit 1 11 | fi 12 | 13 | exit 0 14 | 15 | -------------------------------------------------------------------------------- /.vscode/launch.json: -------------------------------------------------------------------------------- 1 | { 2 | // Use IntelliSense to learn about possible attributes. 3 | // Hover to view descriptions of existing attributes. 4 | // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387 5 | "version": "0.2.0", 6 | "configurations": [ 7 | { 8 | "name": "Launch Terraform", 9 | "type": "go", 10 | "request": "launch", 11 | "mode": "exec", 12 | "program": "/usr/local/Cellar/terraform/0.11.13/bin/terraform", 13 | "env": {}, 14 | "args": ["apply"] 15 | } 16 | ] 17 | } 18 | -------------------------------------------------------------------------------- /scripts/errcheck.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | # Check gofmt 4 | echo "==> Checking for unchecked errors..." 5 | 6 | if ! which errcheck > /dev/null; then 7 | echo "==> Installing errcheck..." 8 | go get -u github.com/kisielk/errcheck 9 | fi 10 | 11 | err_files=$(errcheck -ignoretests \ 12 | -ignore 'github.com/hashicorp/terraform/helper/schema:Set' \ 13 | -ignore 'bytes:.*' \ 14 | -ignore 'io:Close|Write' \ 15 | $(go list ./...| grep -v /vendor/)) 16 | 17 | if [[ -n ${err_files} ]]; then 18 | echo 'Unchecked errors found in the following places:' 19 | echo "${err_files}" 20 | echo "Please handle returned errors. You can check directly with \`make errcheck\`" 21 | exit 1 22 | fi 23 | 24 | exit 0 25 | -------------------------------------------------------------------------------- /scripts/changelog-links.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | # This script rewrites [GH-nnnn]-style references in the CHANGELOG.md file to 4 | # be Markdown links to the given github issues. 5 | # 6 | # This is run during releases so that the issue references in all of the 7 | # released items are presented as clickable links, but we can just use the 8 | # easy [GH-nnnn] shorthand for quickly adding items to the "Unrelease" section 9 | # while merging things between releases. 10 | 11 | set -e 12 | 13 | if [[ ! -f CHANGELOG.md ]]; then 14 | echo "ERROR: CHANGELOG.md not found in pwd." 15 | echo "Please run this from the root of the terraform provider repository" 16 | exit 1 17 | fi 18 | 19 | if [[ `uname` == "Darwin" ]]; then 20 | echo "Using BSD sed" 21 | SED="sed -i.bak -E -e" 22 | else 23 | echo "Using GNU sed" 24 | SED="sed -i.bak -r -e" 25 | fi 26 | 27 | PROVIDER_URL="https:\/\/github.com\/john-terrell\/terraform-provider-smartos\/issues" 28 | 29 | $SED "s/GH-([0-9]+)/\[#\1\]\($PROVIDER_URL\/\1\)/g" -e 's/\[\[#(.+)([0-9])\)]$/(\[#\1\2))/g' CHANGELOG.md 30 | 31 | rm CHANGELOG.md.bak 32 | -------------------------------------------------------------------------------- /data_source_image.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "fmt" 5 | "log" 6 | 7 | "github.com/hashicorp/terraform/helper/schema" 8 | ) 9 | 10 | func datasourceImage() *schema.Resource { 11 | return &schema.Resource{ 12 | SchemaVersion: 1, 13 | Read: datasourceImageReadRunc, 14 | Schema: map[string]*schema.Schema{ 15 | "name": &schema.Schema{ 16 | Type: schema.TypeString, 17 | Required: true, 18 | }, 19 | "version": &schema.Schema{ 20 | Type: schema.TypeString, 21 | Required: true, 22 | }, 23 | }, 24 | } 25 | } 26 | 27 | func datasourceImageReadRunc(d *schema.ResourceData, m interface{}) error { 28 | client := m.(*SmartOSClient) 29 | 30 | name := d.Get("name").(string) 31 | version := d.Get("version").(string) 32 | 33 | var image *Image 34 | var err error 35 | 36 | image, err = client.GetLocalImage(name, version) 37 | if err != nil { 38 | return err 39 | } 40 | 41 | if image == nil { 42 | image, err = client.FindRemoteImage(name, version) 43 | if err == nil && image == nil { 44 | return fmt.Errorf("Image not found") 45 | } 46 | } 47 | 48 | if err != nil { 49 | log.Printf("Failed to retrieve image with name: %s, version: %s. Error: %s", name, version, err) 50 | return err 51 | } 52 | 53 | d.SetId(image.ID.String()) 54 | 55 | return nil 56 | } 57 | -------------------------------------------------------------------------------- /provider.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "net" 5 | "os" 6 | 7 | "github.com/hashicorp/terraform/helper/schema" 8 | "golang.org/x/crypto/ssh" 9 | "golang.org/x/crypto/ssh/agent" 10 | ) 11 | 12 | func Provider() *schema.Provider { 13 | return &schema.Provider{ 14 | Schema: providerSchema(), 15 | ResourcesMap: providerResources(), 16 | DataSourcesMap: providerDataSources(), 17 | ConfigureFunc: providerConfigure, 18 | } 19 | } 20 | 21 | // List of supported configuration fields for the provider. 22 | // More info in https://github.com/hashicorp/terraform/blob/v0.6.6/helper/schema/schema.go#L29-L142 23 | func providerSchema() map[string]*schema.Schema { 24 | return map[string]*schema.Schema{ 25 | "host": &schema.Schema{ 26 | Type: schema.TypeString, 27 | Required: true, 28 | Description: "Host address of the SmartOS global zone.", 29 | }, 30 | "user": &schema.Schema{ 31 | Type: schema.TypeString, 32 | Required: true, 33 | Description: "User to authenticate with.", 34 | }, 35 | } 36 | } 37 | 38 | func providerResources() map[string]*schema.Resource { 39 | return map[string]*schema.Resource{ 40 | "smartos_machine": resourceMachine(), 41 | } 42 | } 43 | 44 | func providerDataSources() map[string]*schema.Resource { 45 | return map[string]*schema.Resource{ 46 | "smartos_image": datasourceImage(), 47 | } 48 | } 49 | 50 | func providerConfigure(d *schema.ResourceData) (interface{}, error) { 51 | sshSocket := os.Getenv("SSH_AUTH_SOCK") 52 | agentConnection, err := net.Dial("unix", sshSocket) 53 | if err != nil { 54 | return nil, err 55 | } 56 | 57 | authMethods := []ssh.AuthMethod{} 58 | authMethods = append(authMethods, ssh.PublicKeysCallback(agent.NewClient(agentConnection).Signers)) 59 | 60 | client := SmartOSClient{ 61 | host: d.Get("host").(string), 62 | user: d.Get("user").(string), 63 | agentConnection: agentConnection, 64 | authMethods: authMethods, 65 | } 66 | 67 | return &client, nil 68 | } 69 | -------------------------------------------------------------------------------- /GNUmakefile: -------------------------------------------------------------------------------- 1 | TEST?=$$(go list ./... |grep -v 'vendor') 2 | GOFMT_FILES?=$$(find . -name '*.go' |grep -v vendor) 3 | WEBSITE_REPO=github.com/hashicorp/terraform-website 4 | PKG_NAME=smartos 5 | VERSION=$(shell git describe --tags --always) 6 | 7 | default: build 8 | 9 | build: fmtcheck 10 | go install -ldflags="-X github.com/john-terrell/terraform-provider-smartos/version.ProviderVersion=$(VERSION)" 11 | 12 | test: fmtcheck 13 | go test -i $(TEST) || exit 1 14 | echo $(TEST) | \ 15 | xargs -t -n4 go test $(TESTARGS) -timeout=30s -parallel=4 16 | 17 | testacc: fmtcheck 18 | TF_ACC=1 go test $(TEST) -v $(TESTARGS) -timeout 120m 19 | 20 | vet: 21 | @echo "go vet ." 22 | @go vet $$(go list ./... | grep -v vendor/) ; if [ $$? -eq 1 ]; then \ 23 | echo ""; \ 24 | echo "Vet found suspicious constructs. Please check the reported constructs"; \ 25 | echo "and fix them if necessary before submitting the code for review."; \ 26 | exit 1; \ 27 | fi 28 | 29 | fmt: 30 | gofmt -w $(GOFMT_FILES) 31 | 32 | fmtcheck: 33 | @sh -c "'$(CURDIR)/scripts/gofmtcheck.sh'" 34 | 35 | errcheck: 36 | @sh -c "'$(CURDIR)/scripts/errcheck.sh'" 37 | 38 | test-compile: 39 | @if [ "$(TEST)" = "./..." ]; then \ 40 | echo "ERROR: Set TEST to a specific package. For example,"; \ 41 | echo " make test-compile TEST=./$(PKG_NAME)"; \ 42 | exit 1; \ 43 | fi 44 | go test -c $(TEST) $(TESTARGS) 45 | 46 | website: 47 | ifeq (,$(wildcard $(GOPATH)/src/$(WEBSITE_REPO))) 48 | echo "$(WEBSITE_REPO) not found in your GOPATH (necessary for layouts and assets), get-ting..." 49 | git clone https://$(WEBSITE_REPO) $(GOPATH)/src/$(WEBSITE_REPO) 50 | endif 51 | @$(MAKE) -C $(GOPATH)/src/$(WEBSITE_REPO) website-provider PROVIDER_PATH=$(shell pwd) PROVIDER_NAME=$(PKG_NAME) 52 | 53 | website-test: 54 | ifeq (,$(wildcard $(GOPATH)/src/$(WEBSITE_REPO))) 55 | echo "$(WEBSITE_REPO) not found in your GOPATH (necessary for layouts and assets), get-ting..." 56 | git clone https://$(WEBSITE_REPO) $(GOPATH)/src/$(WEBSITE_REPO) 57 | endif 58 | @$(MAKE) -C $(GOPATH)/src/$(WEBSITE_REPO) website-provider-test PROVIDER_PATH=$(shell pwd) PROVIDER_NAME=$(PKG_NAME) 59 | 60 | .PHONY: build test testacc vet fmt fmtcheck errcheck test-compile website website-test 61 | 62 | -------------------------------------------------------------------------------- /sample.tf: -------------------------------------------------------------------------------- 1 | provider "smartos" { 2 | "host" = "10.99.50.60:22" 3 | "user" = "root" 4 | } 5 | 6 | data "smartos_image" "illumos" { 7 | "name" = "base-64-lts" 8 | "version" = "18.4.0" 9 | } 10 | 11 | data "smartos_image" "linux" { 12 | "name" = "ubuntu-16.04" 13 | "version" = "20170403" 14 | } 15 | 16 | data "smartos_image" "linux_kvm" { 17 | "name" = "ubuntu-certified-16.04" 18 | "version" = "20190212" 19 | } 20 | 21 | resource "smartos_machine" "illumos" { 22 | "alias" = "provider-test-illumos" 23 | "brand" = "joyent" 24 | "cpu_cap" = 100 25 | 26 | "customer_metadata" = { 27 | # Note: this is my public SSH key...use your own. :-) 28 | "root_authorized_keys" = "ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABAQDYIHv3DoXnAMn+dggUup1a+jjSqpZiIU5ThgljXHG9KM+iy1W3zo9qshUE7vBj/l7l5aHzRKyXsmWb6EdmtlVBnYl7SH5IMGaEFlB6n7T+yoMRl7VczZZxvP+VSAac2HeLPvdrCDeJCckfkHeTg9E3rt2PcAz0REKDCm34lpsedgM4QrVh8D54NgqLCdpT+QpidEBwE1T5wGMId4OwBB+r1VJZyn+lstJreQ0mu67qn3TFKu5AxZoTdDj6BSDqqHEos5KirS4pz3zt3r5IbC3mv8vDm9+o6O5M2f7R6RRNfD9IPJANmO0k2Ajf529I0bGgAGgIpIXb8OaI6G+L48dR john@Johns-MacBook-Pro.local" 29 | "user-script" = "/usr/sbin/mdata-get root_authorized_keys > ~root/.ssh/authorized_keys" 30 | } 31 | 32 | "image_uuid" = "${data.smartos_image.illumos.id}" 33 | "maintain_resolvers" = true 34 | "max_physical_memory" = 512 35 | "nics" = [ 36 | { 37 | "nic_tag" = "external" 38 | "ips" = ["10.0.222.222/16"] 39 | "gateways" = ["10.0.0.1"] 40 | "interface" = "net4" 41 | } 42 | ] 43 | "quota" = 25 44 | 45 | "resolvers" = ["1.1.1.1", "1.0.0.1"] 46 | 47 | provisioner "remote-exec" { 48 | inline = [ 49 | "pkgin -y update", 50 | "pkgin -y in htop", 51 | ] 52 | } 53 | } 54 | 55 | resource "smartos_machine" "linux" { 56 | "alias" = "provider-test-linux" 57 | "brand" = "lx" 58 | "kernel_version" = "3.16.0" 59 | "cpu_cap" = 100 60 | 61 | "customer_metadata" = { 62 | # Note: this is my public SSH key...use your own. :-) 63 | "root_authorized_keys" = "ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABAQDYIHv3DoXnAMn+dggUup1a+jjSqpZiIU5ThgljXHG9KM+iy1W3zo9qshUE7vBj/l7l5aHzRKyXsmWb6EdmtlVBnYl7SH5IMGaEFlB6n7T+yoMRl7VczZZxvP+VSAac2HeLPvdrCDeJCckfkHeTg9E3rt2PcAz0REKDCm34lpsedgM4QrVh8D54NgqLCdpT+QpidEBwE1T5wGMId4OwBB+r1VJZyn+lstJreQ0mu67qn3TFKu5AxZoTdDj6BSDqqHEos5KirS4pz3zt3r5IbC3mv8vDm9+o6O5M2f7R6RRNfD9IPJANmO0k2Ajf529I0bGgAGgIpIXb8OaI6G+L48dR john@Johns-MacBook-Pro.local" 64 | "user-script" = "/usr/sbin/mdata-get root_authorized_keys > ~root/.ssh/authorized_keys" 65 | } 66 | 67 | "image_uuid" = "${data.smartos_image.linux.id}" 68 | "maintain_resolvers" = true 69 | "max_physical_memory" = 512 70 | "nics" = [ 71 | { 72 | "nic_tag" = "external" 73 | "ips" = ["10.0.222.223/16"] 74 | "gateways" = ["10.0.0.1"] 75 | "interface" = "net5" 76 | } 77 | ] 78 | "quota" = 25 79 | 80 | "resolvers" = ["1.1.1.1", "1.0.0.1"] 81 | 82 | provisioner "remote-exec" { 83 | inline = [ 84 | "apt-get update", 85 | "apt-get -y install htop", 86 | ] 87 | } 88 | } 89 | 90 | resource "smartos_machine" "linux-kvm" { 91 | "alias" = "provider-test-linux-kvm" 92 | "brand" = "kvm" 93 | "kernel_version" = "3.16.0" 94 | "vcpus" = 2 95 | 96 | "customer_metadata" = { 97 | # Note: this is my public SSH key...use your own. :-) 98 | "root_authorized_keys" = "ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABAQDYIHv3DoXnAMn+dggUup1a+jjSqpZiIU5ThgljXHG9KM+iy1W3zo9qshUE7vBj/l7l5aHzRKyXsmWb6EdmtlVBnYl7SH5IMGaEFlB6n7T+yoMRl7VczZZxvP+VSAac2HeLPvdrCDeJCckfkHeTg9E3rt2PcAz0REKDCm34lpsedgM4QrVh8D54NgqLCdpT+QpidEBwE1T5wGMId4OwBB+r1VJZyn+lstJreQ0mu67qn3TFKu5AxZoTdDj6BSDqqHEos5KirS4pz3zt3r5IbC3mv8vDm9+o6O5M2f7R6RRNfD9IPJANmO0k2Ajf529I0bGgAGgIpIXb8OaI6G+L48dR john@Johns-MacBook-Pro.local" 99 | } 100 | 101 | "maintain_resolvers" = true 102 | "ram" = 512 103 | "nics" = [ 104 | { 105 | "nic_tag" = "external" 106 | "ips" = ["10.0.222.224/16"] 107 | "gateways" = ["10.0.0.1"] 108 | "interface" = "net0" 109 | "model" = "virtio" 110 | } 111 | ] 112 | "quota" = 25 113 | 114 | "resolvers" = ["1.1.1.1", "1.0.0.1"] 115 | 116 | "disks" = [ 117 | { 118 | "boot" = true 119 | "image_uuid" = "${data.smartos_image.linux_kvm.id}" 120 | "compression" = "lz4" 121 | "model" = "virtio" 122 | } 123 | ] 124 | 125 | provisioner "remote-exec" { 126 | inline = [ 127 | "apt-get update", 128 | "apt-get -y install htop", 129 | ] 130 | } 131 | } 132 | -------------------------------------------------------------------------------- /reconcile_containers.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "log" 5 | "sort" 6 | ) 7 | 8 | type AddItemFunc func(string, interface{}) 9 | type UpdateItemFunc func(string, interface{}) 10 | type ItemEqualFunc func(interface{}, interface{}) bool 11 | type RemoveItemFunc func(string) 12 | 13 | // ReconcileMaps compares two maps; one with old data and one with new data and calls the various argument functions reflecting what operations are necessary to transform the old map into the new. 14 | func ReconcileMaps(oldMap map[string]interface{}, newMap map[string]interface{}, addItem AddItemFunc, updateItem UpdateItemFunc, removeItem RemoveItemFunc, itemsAreEqual ItemEqualFunc) bool { 15 | changesMade := false 16 | 17 | var oldKeys []string 18 | var newKeys []string 19 | 20 | for key := range oldMap { 21 | oldKeys = append(oldKeys, key) 22 | } 23 | sort.Strings(oldKeys) 24 | 25 | for key := range newMap { 26 | newKeys = append(newKeys, key) 27 | } 28 | sort.Strings(newKeys) 29 | 30 | newKeyIndex := 0 31 | oldKeyIndex := 0 32 | 33 | for { 34 | // if there are no more old keys, the remaining new keys need to be added 35 | if oldKeyIndex >= len(oldKeys) { 36 | break 37 | } 38 | 39 | // If there are no more new keys, the remaining old keys need to be removed. 40 | if newKeyIndex >= len(newKeys) { 41 | break 42 | } 43 | 44 | oldKey := oldKeys[oldKeyIndex] 45 | newKey := newKeys[newKeyIndex] 46 | 47 | log.Printf("COMPARING old key '%s' with new key '%s'\n", oldKey, newKey) 48 | 49 | if oldKey == newKey { 50 | // Keys are the same, see if the values are the same 51 | oldValue := oldMap[oldKey] 52 | newValue := newMap[oldKey] 53 | 54 | if !itemsAreEqual(oldValue, newValue) { 55 | // Value changed 56 | updateItem(oldKey, newValue) 57 | log.Printf("COMPARE: Updating [%s] = %s", oldKey, newValue.(string)) 58 | changesMade = true 59 | } else { 60 | log.Printf("COMPARE: No change to [%s] = %s", oldKey, newValue.(string)) 61 | } 62 | newKeyIndex++ 63 | oldKeyIndex++ 64 | } else if oldKey < newKey { 65 | // 'oldKey' was removed 66 | oldValue := oldMap[oldKey].(string) 67 | removeItem(oldKey) 68 | log.Printf("COMPARE: Removing [%s] = %s", oldKey, oldValue) 69 | changesMade = true 70 | 71 | oldKeyIndex++ 72 | } else { 73 | // 'newKey' was added 74 | newValue := newMap[newKey] 75 | 76 | addItem(newKey, newValue) 77 | log.Printf("COMPARE: Adding [%s] = %s", newKey, newValue.(string)) 78 | 79 | newKeyIndex++ 80 | changesMade = true 81 | } 82 | } 83 | 84 | // Remove any remaining old keys 85 | for ; oldKeyIndex < len(oldKeys); oldKeyIndex++ { 86 | oldKey := oldKeys[oldKeyIndex] 87 | oldValue := oldMap[oldKey].(string) 88 | removeItem(oldKey) 89 | log.Printf("COMPARE: Removing at end [%s] = %s", oldKey, oldValue) 90 | changesMade = true 91 | } 92 | 93 | // Add any remaining new keys 94 | for ; newKeyIndex < len(newKeys); newKeyIndex++ { 95 | newKey := newKeys[newKeyIndex] 96 | newValue := newMap[newKey] 97 | 98 | addItem(newKey, newValue) 99 | log.Printf("COMPARE: Adding at end [%s] = %s", newKey, newValue.(string)) 100 | changesMade = true 101 | } 102 | 103 | return changesMade 104 | } 105 | 106 | type AddSliceItemFunc func(string) 107 | type RemoveSliceItemFunc func(string) 108 | 109 | // ReconcileSlices compares two slices; one with old data and one with new data and calls the various argument functions reflecting what operations are necessary to transform the old slice into the new. 110 | func ReconcileSlices(oldSlice []string, newSlice []string, addItem AddSliceItemFunc, removeItem RemoveSliceItemFunc) bool { 111 | changesMade := false 112 | 113 | sort.Strings(oldSlice) 114 | sort.Strings(newSlice) 115 | 116 | newSliceIndex := 0 117 | oldSliceIndex := 0 118 | 119 | for { 120 | // if there are no more old values, the remaining new values need to be added 121 | if oldSliceIndex >= len(oldSlice) { 122 | break 123 | } 124 | 125 | // If there are no more new values, the remaining old values need to be removed. 126 | if newSliceIndex >= len(newSlice) { 127 | break 128 | } 129 | 130 | oldValue := oldSlice[oldSliceIndex] 131 | newValue := newSlice[newSliceIndex] 132 | 133 | log.Printf("COMPARING old value '%s' with new value '%s'\n", oldValue, newValue) 134 | 135 | if oldValue == newValue { 136 | newSliceIndex++ 137 | oldSliceIndex++ 138 | } else if oldValue < newValue { 139 | // 'oldValue' was removed 140 | removeItem(oldValue) 141 | log.Printf("COMPARE: Removing [%s]", oldValue) 142 | changesMade = true 143 | 144 | oldSliceIndex++ 145 | } else { 146 | // 'newValue' was added 147 | addItem(newValue) 148 | log.Printf("COMPARE: Adding [%s]", newValue) 149 | 150 | newSliceIndex++ 151 | changesMade = true 152 | } 153 | } 154 | 155 | // Remove any remaining old values 156 | for ; oldSliceIndex < len(oldSlice); oldSliceIndex++ { 157 | oldValue := oldSlice[oldSliceIndex] 158 | removeItem(oldValue) 159 | log.Printf("COMPARE: Removing [%s]", oldValue) 160 | changesMade = true 161 | } 162 | 163 | // Add any remaining new values 164 | for ; newSliceIndex < len(newSlice); newSliceIndex++ { 165 | newValue := newSlice[newSliceIndex] 166 | addItem(newValue) 167 | log.Printf("COMPARE: Adding at end [%s]", newValue) 168 | changesMade = true 169 | } 170 | 171 | return changesMade 172 | } 173 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | NOTE: This provider has been superceded by: 2 | 3 | https://github.com/CoolpeopleNetworks/terraform-provider-smartos.git 4 | 5 | This repository will no longer be updated. 6 | 7 | 8 | SmartOS Terraform Provider 9 | ========================= 10 | 11 | 12 | 13 | Requirements 14 | ------------ 15 | 16 | - [Terraform](https://www.terraform.io/downloads.html) 0.10.x 17 | - [Go](https://golang.org/doc/install) 1.9 (to build the provider plugin) 18 | 19 | Using the provider 20 | ------------------ 21 | 22 | This provider can be used to provision machines with a SmartOS host via SSH. SSH public keys are expected to already be installed on the SmartOS host in order for this provider to work. 23 | 24 | NOTE: Currently, this provider only supports a subset of properties for SmartOS virtual machines. 25 | 26 | ### Setup ### 27 | 28 | ```hcl 29 | provider "smartos" { 30 | "host" = "10.99.50.60:22" 31 | "user" = "root" 32 | } 33 | ``` 34 | 35 | The following arguments are supported. 36 | 37 | - `host` - (Required) This is the address of the global zone on the SmartOS host. 38 | - `user` - (Required) This is the authenticated SSH user which will run provisioning commands. Normally this is 'root'. 39 | 40 | ### Resources and Data Providers ### 41 | 42 | Currently, the following data and resources are provided: 43 | 44 | - smartos_image (Data source - the images will be imported on first use by a smartos_machine stanza.) 45 | - smartos_machine (Resource) 46 | 47 | NOTE: The property names supported by this provider match (as much as possible) those defined by Joyent for use with their 'vmadm' utility. See the man page (specifically the PROPERTIES section) for that utility for more info: 48 | 49 | https://smartos.org/man/1m/vmadm 50 | 51 | Many of the properties defined in the man page are not yet supported by the provider. 52 | 53 | ### Example ### 54 | 55 | The following example shows you how to configure two simple zones - one running Illumos (base-64-lts from Joyent) and the other running Ubuntu 16.04. 56 | 57 | (See the included sample.tf) 58 | 59 | ```hcl 60 | provider "smartos" { 61 | "host" = "10.99.50.60:22" 62 | "user" = "root" 63 | } 64 | 65 | data "smartos_image" "illumos" { 66 | "name" = "base-64-lts" 67 | "version" = "18.4.0" 68 | } 69 | 70 | data "smartos_image" "linux" { 71 | "name" = "ubuntu-16.04" 72 | "version" = "20170403" 73 | } 74 | 75 | data "smartos_image" "linux_kvm" { 76 | "name" = "ubuntu-certified-16.04" 77 | "version" = "20190212" 78 | } 79 | 80 | resource "smartos_machine" "illumos" { 81 | "alias" = "illumos" 82 | "brand" = "joyent" 83 | "cpu_cap" = 100 84 | 85 | # These fields are required in order for provisioning (below) to function. 86 | "customer_metadata" = { 87 | "root_authorized_keys" = "... copy this from your ~/.ssh/id_rsa.pub ..." 88 | "user-script" = "/usr/sbin/mdata-get root_authorized_keys > ~root/.ssh/authorized_keys" 89 | } 90 | 91 | "image_uuid" = "${data.smartos_image.illumos.id}" 92 | "maintain_resolvers" = true 93 | "max_physical_memory" = 512 94 | "nics" = [ 95 | { 96 | "nic_tag" = "external" 97 | "ips" = ["10.0.222.222/16"] 98 | "gateways" = ["10.0.0.1"] 99 | "interface" = "net4" 100 | } 101 | ] 102 | "quota" = 25 103 | 104 | "resolvers" = ["1.1.1.1", "1.0.0.1"] 105 | 106 | provisioner "remote-exec" { 107 | inline = [ 108 | "pkgin -y update", 109 | "pkgin -y in htop", 110 | ] 111 | } 112 | } 113 | 114 | resource "smartos_machine" "linux" { 115 | "alias" = "provider-test-linux" 116 | "brand" = "lx" 117 | "kernel_version" = "3.16.0" 118 | "cpu_cap" = 100 119 | 120 | "customer_metadata" = { 121 | # Note: this is my public SSH key...use your own. :-) 122 | "root_authorized_keys" = "... copy this from your ~/.ssh/id_rsa.pub ..." 123 | "user-script" = "/usr/sbin/mdata-get root_authorized_keys > ~root/.ssh/authorized_keys" 124 | } 125 | 126 | "image_uuid" = "${data.smartos_image.linux.id}" 127 | "maintain_resolvers" = true 128 | "max_physical_memory" = 512 129 | "nics" = [ 130 | { 131 | "nic_tag" = "external" 132 | "ips" = ["10.0.222.223/16"] 133 | "gateways" = ["10.0.0.1"] 134 | "interface" = "net5" 135 | } 136 | ] 137 | "quota" = 25 138 | 139 | "resolvers" = ["1.1.1.1", "1.0.0.1"] 140 | 141 | provisioner "remote-exec" { 142 | inline = [ 143 | "apt-get update", 144 | "apt-get -y install htop", 145 | ] 146 | } 147 | } 148 | 149 | resource "smartos_machine" "linux-kvm" { 150 | "alias" = "provider-test-linux-kvm" 151 | "brand" = "kvm" 152 | "kernel_version" = "3.16.0" 153 | "vcpus" = 2 154 | 155 | "customer_metadata" = { 156 | # Note: this is my public SSH key...use your own. :-) 157 | "root_authorized_keys" = "... copy this from your ~/.ssh/id_rsa.pub ..." 158 | } 159 | 160 | "maintain_resolvers" = true 161 | "ram" = 512 162 | "nics" = [ 163 | { 164 | "nic_tag" = "external" 165 | "ips" = ["10.0.222.224/16"] 166 | "gateways" = ["10.0.0.1"] 167 | "interface" = "net0" 168 | "model" = "virtio" 169 | } 170 | ] 171 | "quota" = 25 172 | 173 | "resolvers" = ["1.1.1.1", "1.0.0.1"] 174 | 175 | "disks" = [ 176 | { 177 | "boot" = true 178 | "image_uuid" = "${data.smartos_image.linux_kvm.id}" 179 | "compression" = "lz4" 180 | "model" = "virtio" 181 | } 182 | ] 183 | 184 | provisioner "remote-exec" { 185 | inline = [ 186 | "apt-get update", 187 | "apt-get -y install htop", 188 | ] 189 | } 190 | } 191 | 192 | ``` 193 | -------------------------------------------------------------------------------- /smartos_client.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "bytes" 5 | "encoding/json" 6 | "fmt" 7 | "log" 8 | "net" 9 | "regexp" 10 | 11 | "github.com/google/uuid" 12 | "golang.org/x/crypto/ssh" 13 | ) 14 | 15 | type SmartOSClient struct { 16 | host string 17 | user string 18 | client *ssh.Client 19 | agentConnection net.Conn 20 | authMethods []ssh.AuthMethod 21 | } 22 | 23 | func (c *SmartOSClient) Connect() error { 24 | var err error = nil 25 | 26 | if c.client != nil { 27 | return nil 28 | } 29 | 30 | config := &ssh.ClientConfig{ 31 | User: c.user, 32 | Auth: c.authMethods, 33 | HostKeyCallback: ssh.InsecureIgnoreHostKey(), 34 | } 35 | 36 | log.Println("SSH: Connecting to host: ", c.host) 37 | c.client, err = ssh.Dial("tcp", c.host, config) 38 | if err != nil { 39 | log.Println("SSH: Connection failed: ", err.Error()) 40 | return err 41 | } 42 | 43 | log.Println("SSH: Connected successfully") 44 | return nil 45 | } 46 | 47 | func (c *SmartOSClient) Close() { 48 | if c.client != nil { 49 | c.client.Close() 50 | c.client = nil 51 | } 52 | } 53 | 54 | func (c *SmartOSClient) CreateMachine(machine *Machine) (*uuid.UUID, error) { 55 | err := c.Connect() 56 | if err != nil { 57 | return nil, err 58 | } 59 | 60 | session, err := c.client.NewSession() 61 | if err != nil { 62 | return nil, err 63 | } 64 | 65 | defer session.Close() 66 | 67 | // Ensure the image has been imported 68 | if machine.ImageUUID != nil && *machine.ImageUUID != uuid.Nil { 69 | log.Printf("Ensuring image with UUID %s has been imported", machine.ImageUUID.String()) 70 | err = c.ImportRemoteImage(*machine.ImageUUID) 71 | if err != nil { 72 | log.Fatalln("Failed to import image for machine. Error: ", err.Error()) 73 | return nil, err 74 | } 75 | } else if machine.Brand == "joyent" || machine.Brand == "lx" { 76 | log.Fatalln("No image specifiec for OS VM.") 77 | return nil, fmt.Errorf("No image specifiec for OS VM.") 78 | } 79 | 80 | // Ensure any disk images are imported 81 | for _, disk := range machine.Disks { 82 | if disk.ImageUUID != nil && *disk.ImageUUID != uuid.Nil { 83 | err = c.ImportRemoteImage(*disk.ImageUUID) 84 | if err != nil { 85 | log.Fatalf("Failed to import disk image: %s (Error: %s)", disk.ImageUUID.String(), err.Error()) 86 | return nil, err 87 | } 88 | } 89 | } 90 | 91 | json, err := json.Marshal(machine) 92 | if err != nil { 93 | log.Fatalln("Failed to create JSON for machine. Error: ", err.Error()) 94 | return nil, err 95 | } 96 | 97 | log.Println("JSON: ", string(json)) 98 | 99 | session.Stdin = bytes.NewReader(json) 100 | 101 | var b bytes.Buffer 102 | session.Stderr = &b 103 | 104 | log.Println("SSH execute: vmadm create") 105 | err = session.Run("vmadm create") 106 | if err != nil { 107 | return nil, fmt.Errorf("Remote command vmadm failed. Error: %s (%s)\n", err, b.String()) 108 | } 109 | 110 | output := b.String() 111 | log.Printf("Returned data: %s", output) 112 | 113 | re := regexp.MustCompile("Successfully created VM ([0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12})") 114 | matches := re.FindStringSubmatch(output) 115 | 116 | if len(matches) != 2 { 117 | return nil, fmt.Errorf("Unrecognized response from vmadm: %s", output) 118 | } 119 | 120 | log.Println("Matched regex: ", matches[1]) 121 | uuid, err := uuid.Parse(matches[1]) 122 | if err != nil { 123 | return nil, err 124 | } 125 | 126 | return &uuid, nil 127 | } 128 | 129 | func (c *SmartOSClient) GetMachine(id uuid.UUID) (*Machine, error) { 130 | err := c.Connect() 131 | if err != nil { 132 | return nil, err 133 | } 134 | 135 | session, err := c.client.NewSession() 136 | if err != nil { 137 | return nil, err 138 | } 139 | 140 | defer session.Close() 141 | 142 | var b bytes.Buffer 143 | session.Stdout = &b 144 | 145 | var stderr bytes.Buffer 146 | session.Stderr = &stderr 147 | 148 | log.Println("SSH execute: vmadm get", id.String()) 149 | err = session.Run("vmadm get " + id.String()) 150 | if err != nil { 151 | return nil, fmt.Errorf("Remote command vmadm failed. Error: %s (%s)\n", err, stderr.String()) 152 | } 153 | 154 | outputBytes := b.Bytes() 155 | 156 | output := string(outputBytes) 157 | log.Printf("Returned data: %s", output) 158 | 159 | var machine Machine 160 | err = json.Unmarshal(outputBytes, &machine) 161 | if err != nil { 162 | log.Printf("Failed to parse returned JSON: %s", err) 163 | return nil, err 164 | } 165 | 166 | machine.UpdatePrimaryIP() 167 | machine.UpdateMetadata() 168 | 169 | return &machine, nil 170 | } 171 | 172 | func (c *SmartOSClient) UpdateMachine(machine *Machine) error { 173 | err := c.Connect() 174 | if err != nil { 175 | return err 176 | } 177 | 178 | session, err := c.client.NewSession() 179 | if err != nil { 180 | return err 181 | } 182 | 183 | defer session.Close() 184 | 185 | json, err := json.Marshal(machine) 186 | if err != nil { 187 | log.Fatalln("Failed to create JSON for machine. Error: ", err.Error()) 188 | } 189 | 190 | log.Println("JSON: ", string(json)) 191 | 192 | session.Stdin = bytes.NewReader(json) 193 | 194 | var b bytes.Buffer 195 | session.Stderr = &b 196 | 197 | log.Println("SSH execute: vmadm update" + machine.ID.String()) 198 | err = session.Run("vmadm update " + machine.ID.String()) 199 | if err != nil { 200 | return fmt.Errorf("Remote command vmadm failed. Error: %s (%s)\n", err, b.String()) 201 | } 202 | 203 | output := b.String() 204 | log.Printf("Returned data: %s", output) 205 | 206 | return nil 207 | } 208 | 209 | func (c *SmartOSClient) DeleteMachine(id uuid.UUID) error { 210 | err := c.Connect() 211 | if err != nil { 212 | return err 213 | } 214 | 215 | session, err := c.client.NewSession() 216 | if err != nil { 217 | return err 218 | } 219 | 220 | defer session.Close() 221 | 222 | var b bytes.Buffer 223 | session.Stderr = &b 224 | 225 | log.Println("SSH execute: vmadm delete ", id.String()) 226 | err = session.Run("vmadm delete " + id.String()) 227 | if err != nil { 228 | return fmt.Errorf("Remote command vmadm failed. Error: %s\n", err) 229 | } 230 | 231 | output := b.String() 232 | log.Printf("Returned data: %s", output) 233 | 234 | re := regexp.MustCompile("Successfully deleted VM ([0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12})") 235 | matches := re.FindStringSubmatch(output) 236 | 237 | if len(matches) != 2 { 238 | return fmt.Errorf("Unrecognized response from vmadm: %s", output) 239 | } 240 | 241 | return nil 242 | } 243 | 244 | func (c *SmartOSClient) GetLocalImage(name string, version string) (*Image, error) { 245 | err := c.Connect() 246 | if err != nil { 247 | return nil, err 248 | } 249 | 250 | session, err := c.client.NewSession() 251 | if err != nil { 252 | return nil, err 253 | } 254 | 255 | defer session.Close() 256 | 257 | var b bytes.Buffer 258 | session.Stdout = &b 259 | 260 | var stderr bytes.Buffer 261 | session.Stderr = &stderr 262 | 263 | command := fmt.Sprintf("imgadm list -j name=%s version=%s", name, version) 264 | err = session.Run(command) 265 | if err != nil { 266 | return nil, fmt.Errorf("Remote command vmadm failed. Error: %s (%s)\n", err, stderr.String()) 267 | } 268 | 269 | outputBytes := b.Bytes() 270 | 271 | output := string(outputBytes) 272 | log.Printf("Returned data: %s", output) 273 | 274 | var images []map[string]interface{} 275 | err = json.Unmarshal(outputBytes, &images) 276 | if err != nil { 277 | log.Printf("Failed to parse returned JSON: %s", err) 278 | return nil, err 279 | } 280 | 281 | if len(images) == 0 { 282 | return nil, nil 283 | } 284 | 285 | imageInfo := images[0] 286 | manifest := imageInfo["manifest"].(map[string]interface{}) 287 | 288 | image := Image{} 289 | image.Name = manifest["name"].(string) 290 | image.Version = manifest["version"].(string) 291 | 292 | imageID, err := uuid.Parse(manifest["uuid"].(string)) 293 | image.ID = &imageID 294 | 295 | return &image, nil 296 | } 297 | 298 | func (c *SmartOSClient) FindRemoteImage(name string, version string) (*Image, error) { 299 | err := c.Connect() 300 | if err != nil { 301 | return nil, err 302 | } 303 | 304 | session, err := c.client.NewSession() 305 | if err != nil { 306 | return nil, err 307 | } 308 | 309 | defer session.Close() 310 | 311 | var b bytes.Buffer 312 | session.Stdout = &b 313 | 314 | var stderr bytes.Buffer 315 | session.Stderr = &stderr 316 | 317 | command := fmt.Sprintf("imgadm avail -j name=%s version=%s", name, version) 318 | err = session.Run(command) 319 | if err != nil { 320 | return nil, fmt.Errorf("Remote command vmadm failed. Error: %s (%s)\n", err, stderr.String()) 321 | } 322 | 323 | outputBytes := b.Bytes() 324 | 325 | output := string(outputBytes) 326 | log.Printf("Returned data: %s", output) 327 | 328 | var images []map[string]interface{} 329 | err = json.Unmarshal(outputBytes, &images) 330 | if err != nil { 331 | log.Printf("Failed to parse returned JSON: %s", err) 332 | return nil, err 333 | } 334 | 335 | if len(images) == 0 { 336 | return nil, nil 337 | } 338 | 339 | imageInfo := images[0] 340 | manifest := imageInfo["manifest"].(map[string]interface{}) 341 | 342 | image := Image{} 343 | image.Name = manifest["name"].(string) 344 | image.Version = manifest["version"].(string) 345 | 346 | imageID, err := uuid.Parse(manifest["uuid"].(string)) 347 | image.ID = &imageID 348 | 349 | return &image, nil 350 | } 351 | 352 | func (c *SmartOSClient) ImportRemoteImage(uuid uuid.UUID) error { 353 | err := c.Connect() 354 | if err != nil { 355 | return err 356 | } 357 | 358 | session, err := c.client.NewSession() 359 | if err != nil { 360 | return err 361 | } 362 | 363 | defer session.Close() 364 | 365 | var b bytes.Buffer 366 | session.Stdout = &b 367 | 368 | var stderr bytes.Buffer 369 | session.Stderr = &stderr 370 | 371 | log.Printf("Importing image with UUID: %s\n", uuid.String()) 372 | 373 | command := fmt.Sprintf("imgadm import %s", uuid.String()) 374 | err = session.Run(command) 375 | if err != nil { 376 | return fmt.Errorf("Remote command vmadm failed. Error: %s (%s)\n", err, stderr.String()) 377 | } 378 | 379 | outputBytes := b.Bytes() 380 | 381 | output := string(outputBytes) 382 | log.Printf("Returned data: %s", output) 383 | 384 | return nil 385 | } 386 | 387 | func (c *SmartOSClient) GetImage(name string, version string) (*Image, error) { 388 | image, err := c.GetLocalImage(name, version) 389 | if err != nil { 390 | return nil, err 391 | } 392 | 393 | if image == nil { 394 | image, err = c.FindRemoteImage(name, version) 395 | if err != nil { 396 | return nil, err 397 | } 398 | 399 | err = c.ImportRemoteImage(*image.ID) 400 | if err != nil { 401 | return nil, err 402 | } 403 | } 404 | 405 | return image, nil 406 | } 407 | -------------------------------------------------------------------------------- /machine.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "strings" 5 | 6 | "github.com/google/uuid" 7 | "github.com/hashicorp/terraform/helper/schema" 8 | ) 9 | 10 | type Machine struct { 11 | ID *uuid.UUID `json:"uuid,omitempty"` 12 | Alias string `json:"alias,omitempty"` 13 | Autoboot *bool `json:"autoboot,omitempty"` 14 | Brand string `json:"brand,omitempty"` 15 | CPUCap *uint32 `json:"cpu_cap,omitempty"` 16 | /* 17 | CPUShares uint32 `json:"cpu_shares,omitempty"` 18 | */ 19 | CustomerMetadata map[string]string `json:"customer_metadata,omitempty"` 20 | SetCustomerMetadata map[string]string `json:"set_customer_metadata,omitempty"` // for updates 21 | RemoveCustomerMetadata []string `json:"remove_customer_metadata,omitempty"` // for updates 22 | 23 | Disks []Disk `json:"disks,omitempty"` 24 | 25 | /* 26 | DelegateDataset bool `json:"delegate_dataset,omitempty"` 27 | DNSDomain string `json:"dns_domain,omitempty"` 28 | FirewallEnabled bool `json:"firewall_enabled,omitempty"` 29 | Hostname string `json:"hostname,omitempty"` 30 | */ 31 | ImageUUID *uuid.UUID `json:"image_uuid,omitempty"` 32 | /* 33 | InternalMetadata map[string]string `json:"internal_metadat,omitempty"` 34 | InternalMetadataNamespaces map[string]string `json:"internal_metadata_namespaces,omitempty"` 35 | IndestructableDelegated bool `json:"indestructible_delegated,omitempty"` 36 | IndestructableZoneRoot bool `json:"indestructible_zoneroot,omitempty"` 37 | */ 38 | KernelVersion string `json:"kernel_version,omitempty"` 39 | MaintainResolvers *bool `json:"maintain_resolvers,omitempty"` 40 | MaxPhysicalMemory *uint32 `json:"max_physical_memory,omitempty"` 41 | /* 42 | MaxSwap uint32 `json:"max_swap,omitempty"` 43 | */ 44 | NetworkInterfaces []NetworkInterface `json:"nics,omitempty"` 45 | Quota *uint32 `json:"quota,omitempty"` 46 | RAM *uint32 `json:"ram,omitempty"` 47 | Resolvers []string `json:"resolvers,omitempty"` 48 | VirtualCPUCount *uint32 `json:"vcpus,omitempty"` 49 | State string `json:"state,omitempty"` 50 | PrimaryIP string `json:"-"` 51 | 52 | Metadata map[string]string `json:"-"` 53 | } 54 | 55 | func (m *Machine) UpdatePrimaryIP() { 56 | m.PrimaryIP = "" 57 | for _, networkInterface := range m.NetworkInterfaces { 58 | if networkInterface.IsPrimary != nil { 59 | m.PrimaryIP = networkInterface.IPAddress 60 | break 61 | } 62 | } 63 | } 64 | 65 | func (m *Machine) UpdateMetadata() { 66 | metadata := map[string]string{} 67 | prefix := "terraform:" 68 | for k, v := range m.CustomerMetadata { 69 | if strings.HasPrefix(k, prefix) { 70 | metadata[strings.TrimPrefix(k, prefix)] = v 71 | delete(m.CustomerMetadata, k) 72 | } 73 | } 74 | m.Metadata = metadata 75 | } 76 | 77 | func newBool(value bool) *bool { 78 | n := value 79 | return &n 80 | } 81 | 82 | func newUint32(value uint32) *uint32 { 83 | n := value 84 | return &n 85 | } 86 | 87 | func newStringMap() *map[string]string { 88 | var n map[string]string 89 | return &n 90 | } 91 | 92 | func (m *Machine) LoadFromSchema(d *schema.ResourceData) error { 93 | 94 | m.Alias = d.Get("alias").(string) 95 | m.Brand = d.Get("brand").(string) 96 | 97 | if iid, ok := d.GetOk("image_uuid"); ok { 98 | uuid, _ := uuid.Parse(iid.(string)) 99 | m.ImageUUID = &uuid 100 | } 101 | 102 | if autoboot, ok := d.GetOk("autoboot"); ok { 103 | m.Autoboot = newBool(autoboot.(bool)) 104 | } 105 | 106 | if cpuCap, ok := d.GetOk("cpu_cap"); ok { 107 | m.CPUCap = newUint32(uint32(cpuCap.(int))) 108 | } 109 | 110 | customerMetaData := map[string]string{} 111 | for k, v := range d.Get("customer_metadata").(map[string]interface{}) { 112 | customerMetaData[k] = v.(string) 113 | } 114 | m.CustomerMetadata = customerMetaData 115 | 116 | metadata := map[string]string{} 117 | for k, v := range d.Get("metadata").(map[string]interface{}) { 118 | metadata[k] = v.(string) 119 | } 120 | m.Metadata = metadata 121 | 122 | if disks, ok := d.GetOk("disks"); ok { 123 | m.Disks, _ = getDisks(disks) 124 | } 125 | 126 | if kernelVersion, ok := d.GetOk("kernel_version"); ok { 127 | m.KernelVersion = kernelVersion.(string) 128 | } 129 | 130 | if maxPhysicalMemory, ok := d.GetOk("max_physical_memory"); ok { 131 | m.MaxPhysicalMemory = newUint32(uint32(maxPhysicalMemory.(int))) 132 | } 133 | 134 | if maintainResolvers, ok := d.GetOk("maintain_resolvers"); ok { 135 | m.MaintainResolvers = newBool(maintainResolvers.(bool)) 136 | } 137 | 138 | if nics, ok := d.GetOk("nics"); ok { 139 | m.NetworkInterfaces, _ = getNetworkInterfaces(nics) 140 | } 141 | 142 | if quota, ok := d.GetOk("quota"); ok { 143 | m.Quota = newUint32(uint32(quota.(int))) 144 | } 145 | 146 | if ram, ok := d.GetOk("ram"); ok { 147 | m.RAM = newUint32(uint32(ram.(int))) 148 | } 149 | 150 | if vcpus, ok := d.GetOk("vcpus"); ok { 151 | m.VirtualCPUCount = newUint32(uint32(vcpus.(int))) 152 | } 153 | 154 | var resolvers []string 155 | for _, resolver := range d.Get("resolvers").([]interface{}) { 156 | resolvers = append(resolvers, resolver.(string)) 157 | } 158 | m.Resolvers = resolvers 159 | 160 | return nil 161 | } 162 | 163 | func (m *Machine) SaveToSchema(d *schema.ResourceData) error { 164 | d.Set("primary_ip", m.PrimaryIP) 165 | d.Set("id", m.ID.String()) 166 | 167 | // We update the metadata in case machine provisioning pushed data there. 168 | d.Set("metadata", m.Metadata) 169 | 170 | if m.PrimaryIP != "" { 171 | d.SetConnInfo(map[string]string{ 172 | "type": "ssh", 173 | "host": m.PrimaryIP, 174 | }) 175 | } 176 | 177 | return nil 178 | } 179 | 180 | func (m *Machine) setCustomerMetadata(key string, value interface{}) { 181 | if m.SetCustomerMetadata == nil { 182 | m.SetCustomerMetadata = make(map[string]string) 183 | } 184 | 185 | m.SetCustomerMetadata[key] = value.(string) 186 | } 187 | 188 | func (m *Machine) removeCustomerMetadata(key string) { 189 | m.RemoveCustomerMetadata = append(m.RemoveCustomerMetadata, key) 190 | } 191 | 192 | func stringsAreEqual(a interface{}, b interface{}) bool { 193 | return a.(string) == b.(string) 194 | } 195 | 196 | type NetworkInterface struct { 197 | /* 198 | AllowDHCPSpoofing bool `json:"allow_dhcp_spoofing,omitempty"` 199 | */ 200 | AllowIPSpoofing bool `json:"allow_ip_spoofing"` 201 | AllowMACSpoofing bool `json:"allow_mac_spoofing"` 202 | AllowRestrictedTraffic bool `json:"allow_restricted_traffic"` 203 | /* 204 | BlockedOutgoingPorts []uint16 `json:"blocked_outgoing_ports,omitempty"` 205 | */ 206 | Gateways []string `json:"gateways,omitempty"` 207 | Interface string `json:"interface,omitempty"` 208 | IPAddresses []string `json:"ips,omitempty"` 209 | IPAddress string `json:"ip,omitempty"` 210 | /* 211 | HardwareAddress string `json:"mac,omitempty"` 212 | */ 213 | Model string `json:"model,omitempty"` 214 | Tag string `json:"nic_tag,omitempty"` 215 | IsPrimary *bool `json:"primary,omitempty"` 216 | VirtualLANID uint16 `json:"vlan_id,omitempty"` 217 | } 218 | 219 | func getNetworkInterfaces(d interface{}) ([]NetworkInterface, error) { 220 | networkInterfaceDefinitions := d.([]interface{}) 221 | 222 | var networkInterfaces []NetworkInterface 223 | 224 | for _, nid := range networkInterfaceDefinitions { 225 | networkInterfaceDefinition := nid.(map[string]interface{}) 226 | 227 | allowRestrictedTraffic := false 228 | if value, ok := networkInterfaceDefinition["allow_restricted_traffic"].(bool); ok { 229 | allowRestrictedTraffic = value 230 | } 231 | 232 | allowIPSpoofing := false 233 | if value, ok := networkInterfaceDefinition["allow_ip_spoofing"].(bool); ok { 234 | allowIPSpoofing = value 235 | } 236 | 237 | allowMACSpoofing := false 238 | if value, ok := networkInterfaceDefinition["allow_mac_spoofing"].(bool); ok { 239 | allowMACSpoofing = value 240 | } 241 | 242 | var gateways []string 243 | for _, gateway := range networkInterfaceDefinition["gateways"].([]interface{}) { 244 | gateways = append(gateways, gateway.(string)) 245 | } 246 | 247 | interfaceName := networkInterfaceDefinition["interface"].(string) 248 | 249 | var ips []string 250 | for _, ip := range networkInterfaceDefinition["ips"].([]interface{}) { 251 | ips = append(ips, ip.(string)) 252 | } 253 | 254 | nicTag := networkInterfaceDefinition["nic_tag"].(string) 255 | 256 | var vlanID uint16 257 | if vlanIDCheck, ok := networkInterfaceDefinition["vlan_id"].(int); ok { 258 | vlanID = uint16(vlanIDCheck) 259 | } 260 | 261 | model := "" 262 | if m, ok := networkInterfaceDefinition["model"].(string); ok { 263 | model = m 264 | } 265 | 266 | networkInterface := NetworkInterface{ 267 | AllowRestrictedTraffic: allowRestrictedTraffic, 268 | AllowIPSpoofing: allowIPSpoofing, 269 | AllowMACSpoofing: allowMACSpoofing, 270 | Interface: interfaceName, 271 | IPAddresses: ips, 272 | Tag: nicTag, 273 | Gateways: gateways, 274 | VirtualLANID: vlanID, 275 | Model: model, 276 | } 277 | 278 | networkInterfaces = append(networkInterfaces, networkInterface) 279 | } 280 | 281 | return networkInterfaces, nil 282 | } 283 | 284 | type Disk struct { 285 | Boot bool `json:"boot,omitempty"` 286 | Compression string `json:"compression,omitempty"` 287 | ImageUUID *uuid.UUID `json:"image_uuid,omitempty"` 288 | ImageSize uint32 `json:"image_size,omitempty"` 289 | Model string `json:"model,omitempty"` 290 | Size *uint32 `json:"size,omitempty"` 291 | } 292 | 293 | func getDisks(d interface{}) ([]Disk, error) { 294 | diskDefinitions := d.([]interface{}) 295 | 296 | var disks []Disk 297 | 298 | for _, dd := range diskDefinitions { 299 | diskDefinition := dd.(map[string]interface{}) 300 | disk := Disk{} 301 | 302 | if b, ok := diskDefinition["boot"]; ok { 303 | disk.Boot = b.(bool) 304 | } 305 | 306 | if c, ok := diskDefinition["compression"]; ok { 307 | disk.Compression = c.(string) 308 | } 309 | 310 | if iid, ok := diskDefinition["image_uuid"]; ok { 311 | iid2, _ := uuid.Parse(iid.(string)) 312 | if iid2 != uuid.Nil { 313 | disk.ImageUUID = &iid2 314 | } else { 315 | disk.ImageUUID = nil 316 | } 317 | } 318 | 319 | if is, ok := diskDefinition["image_size"]; ok { 320 | disk.ImageSize = uint32(is.(int)) 321 | } 322 | 323 | if m, ok := diskDefinition["model"]; ok { 324 | disk.Model = m.(string) 325 | } 326 | 327 | if sz, ok := diskDefinition["size"]; ok { 328 | size := uint32(sz.(int)) 329 | if size > 0 { 330 | disk.Size = &size 331 | } 332 | } 333 | 334 | disks = append(disks, disk) 335 | } 336 | 337 | return disks, nil 338 | } 339 | -------------------------------------------------------------------------------- /resource_machine.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "log" 5 | 6 | "github.com/google/uuid" 7 | "github.com/hashicorp/terraform/helper/schema" 8 | ) 9 | 10 | func resourceMachine() *schema.Resource { 11 | return &schema.Resource{ 12 | Create: resourceMachineCreate, 13 | Read: resourceMachineRead, 14 | Update: resourceMachineUpdate, 15 | Delete: resourceMachineDelete, 16 | 17 | Schema: map[string]*schema.Schema{ 18 | "alias": &schema.Schema{ 19 | Type: schema.TypeString, 20 | Required: true, 21 | }, 22 | /* 23 | "archive_on_delete": &schema.Schema{ 24 | Type: schema.TypeBool, 25 | Optional: true, 26 | }, 27 | */ 28 | "autoboot": &schema.Schema{ 29 | Type: schema.TypeBool, 30 | Optional: true, 31 | }, 32 | /* 33 | "billing_id": &schema.Schema{ 34 | Type: schema.TypeString, 35 | Optional: true, 36 | }, 37 | "bhyve_extra_opts": &schema.Schema{ 38 | Type: schema.TypeString, 39 | Optional: true, 40 | ForceNew: true, 41 | }, 42 | "boot": &schema.Schema{ 43 | Type: schema.TypeString, 44 | Optional: true, 45 | }, 46 | "bootrom": &schema.Schema{ 47 | Type: schema.TypeString, 48 | Optional: true, 49 | ForceNew: true, 50 | }, 51 | */ 52 | "brand": &schema.Schema{ 53 | Type: schema.TypeString, 54 | Required: true, 55 | ForceNew: true, 56 | }, 57 | "cpu_cap": &schema.Schema{ 58 | Type: schema.TypeInt, 59 | Optional: true, 60 | }, 61 | /* 62 | "cpu_shares": &schema.Schema{ 63 | Type: schema.TypeInt, 64 | Optional: true, 65 | }, 66 | "cpu_type": &schema.Schema{ 67 | Type: schema.TypeString, 68 | Optional: true, 69 | ForceNew: true, 70 | }, 71 | */ 72 | "customer_metadata": &schema.Schema{ 73 | Type: schema.TypeMap, 74 | Optional: true, 75 | }, 76 | "metadata": &schema.Schema{ 77 | Type: schema.TypeMap, 78 | Computed: true, 79 | }, 80 | /* 81 | "delegate_dataset": &schema.Schema{ 82 | Type: schema.TypeBool, 83 | Optional: true, 84 | }, 85 | */ 86 | "disks": { 87 | Type: schema.TypeList, 88 | Optional: true, 89 | ForceNew: true, 90 | Elem: &schema.Resource{ 91 | Schema: map[string]*schema.Schema{ 92 | "boot": &schema.Schema{ 93 | Type: schema.TypeBool, 94 | Optional: true, 95 | ForceNew: true, 96 | }, 97 | "compression": &schema.Schema{ 98 | Type: schema.TypeString, 99 | Optional: true, 100 | ForceNew: true, 101 | }, 102 | "image_uuid": &schema.Schema{ 103 | Type: schema.TypeString, 104 | Optional: true, 105 | ForceNew: true, 106 | }, 107 | "image_size": &schema.Schema{ // in MiB 108 | Type: schema.TypeInt, 109 | Optional: true, 110 | ForceNew: true, 111 | }, 112 | "model": &schema.Schema{ 113 | Type: schema.TypeString, 114 | Optional: true, 115 | ForceNew: true, 116 | }, 117 | "size": &schema.Schema{ // in MiB 118 | Type: schema.TypeInt, 119 | Optional: true, 120 | ForceNew: true, 121 | }, 122 | }, 123 | }, 124 | }, 125 | /* 126 | "disk_driver": &schema.Schema{ 127 | Type: schema.TypeString, 128 | Optional: true, 129 | ForceNew: true, 130 | }, 131 | "do_not_inventory": &schema.Schema{ 132 | Type: schema.TypeBool, 133 | Optional: true, 134 | }, 135 | "dns_domain": &schema.Schema{ 136 | Type: schema.TypeString, 137 | Optional: true, 138 | }, 139 | // "filesystems.*" 140 | "firewall_enabled": &schema.Schema{ 141 | Type: schema.TypeBool, 142 | Optional: true, 143 | }, 144 | "flexible_disk_size": &schema.Schema{ 145 | Type: schema.TypeInt, 146 | Optional: true, 147 | }, 148 | "fs_allowed": &schema.Schema{ 149 | Type: schema.TypeString, 150 | Optional: true, 151 | }, 152 | "hostname": &schema.Schema{ 153 | Type: schema.TypeString, 154 | Optional: true, 155 | }, 156 | */ 157 | "image_uuid": &schema.Schema{ 158 | Type: schema.TypeString, 159 | Optional: true, 160 | ForceNew: true, 161 | }, 162 | /* 163 | "internal_metadata": &schema.Schema{ 164 | Type: schema.TypeList, 165 | Optional: true, 166 | Elem: &schema.Schema{ 167 | Type: schema.TypeString, 168 | }, 169 | }, 170 | "internal_metadata_namespaces": &schema.Schema{ 171 | Type: schema.TypeList, 172 | Optional: true, 173 | Elem: &schema.Schema{ 174 | Type: schema.TypeString, 175 | }, 176 | }, 177 | "indestructible_delegated": &schema.Schema{ 178 | Type: schema.TypeBool, 179 | Optional: true, 180 | }, 181 | "indestructible_zoneroot": &schema.Schema{ 182 | Type: schema.TypeBool, 183 | Optional: true, 184 | }, 185 | */ 186 | "kernel_version": &schema.Schema{ 187 | Type: schema.TypeString, 188 | Optional: true, 189 | ForceNew: true, 190 | }, 191 | /* 192 | "limit_priv": &schema.Schema{ 193 | Type: schema.TypeString, 194 | Optional: true, 195 | }, 196 | */ 197 | "maintain_resolvers": &schema.Schema{ 198 | Type: schema.TypeBool, 199 | Optional: true, 200 | }, 201 | /* 202 | "max_locked_memory": &schema.Schema{ 203 | Type: schema.TypeInt, 204 | Optional: true, 205 | }, 206 | "max_lwps": &schema.Schema{ 207 | Type: schema.TypeInt, 208 | Optional: true, 209 | }, 210 | */ 211 | "max_physical_memory": &schema.Schema{ 212 | Type: schema.TypeInt, 213 | Optional: true, 214 | }, 215 | /* 216 | "max_swap": &schema.Schema{ 217 | Type: schema.TypeInt, 218 | Optional: true, 219 | }, 220 | "mdata_exec_timeout": &schema.Schema{ 221 | Type: schema.TypeInt, 222 | Optional: true, 223 | }, 224 | */ 225 | "nics": &schema.Schema{ 226 | Type: schema.TypeList, 227 | Optional: true, 228 | ForceNew: true, 229 | Elem: &schema.Resource{ 230 | Schema: map[string]*schema.Schema{ 231 | "allow_restricted_traffic": &schema.Schema{ 232 | Type: schema.TypeBool, 233 | Optional: true, 234 | ForceNew: true, 235 | }, 236 | "allow_ip_spoofing": &schema.Schema{ 237 | Type: schema.TypeBool, 238 | Optional: true, 239 | ForceNew: true, 240 | }, 241 | "allow_mac_spoofing": &schema.Schema{ 242 | Type: schema.TypeBool, 243 | Optional: true, 244 | ForceNew: true, 245 | }, 246 | "gateways": &schema.Schema{ 247 | Type: schema.TypeList, 248 | Optional: true, 249 | ForceNew: true, 250 | Elem: &schema.Schema{ 251 | Type: schema.TypeString, 252 | }, 253 | }, 254 | "interface": &schema.Schema{ 255 | Type: schema.TypeString, 256 | Required: true, 257 | ForceNew: true, 258 | }, 259 | "ips": &schema.Schema{ 260 | Type: schema.TypeList, 261 | Required: true, 262 | ForceNew: true, 263 | Elem: &schema.Schema{ 264 | Type: schema.TypeString, 265 | }, 266 | }, 267 | "nic_tag": &schema.Schema{ 268 | Type: schema.TypeString, 269 | Required: true, 270 | ForceNew: true, 271 | }, 272 | "model": &schema.Schema{ 273 | Type: schema.TypeString, 274 | Optional: true, 275 | ForceNew: true, 276 | }, 277 | }, 278 | }, 279 | }, 280 | /* 281 | "nic_driver": &schema.Schema{ 282 | Type: schema.TypeString, 283 | Optional: true, 284 | }, 285 | "nowait": &schema.Schema{ 286 | Type: schema.TypeBool, 287 | Optional: true, 288 | }, 289 | "owner_uuid": &schema.Schema{ 290 | Type: schema.TypeString, 291 | Optional: true, 292 | }, 293 | "qemu_opts": &schema.Schema{ 294 | Type: schema.TypeString, 295 | Optional: true, 296 | }, 297 | "qemu_extra_opts": &schema.Schema{ 298 | Type: schema.TypeString, 299 | Optional: true, 300 | }, 301 | */ 302 | "primary_ip": &schema.Schema{ 303 | Type: schema.TypeString, 304 | Computed: true, 305 | }, 306 | "quota": &schema.Schema{ 307 | Type: schema.TypeInt, 308 | Optional: true, 309 | }, 310 | "ram": &schema.Schema{ 311 | Type: schema.TypeInt, 312 | Optional: true, 313 | ForceNew: true, 314 | }, 315 | "resolvers": &schema.Schema{ 316 | Type: schema.TypeList, 317 | Optional: true, 318 | Elem: &schema.Schema{ 319 | Type: schema.TypeString, 320 | }, 321 | }, 322 | // "routes.*" - object 323 | /* 324 | "spice_opts": &schema.Schema{ 325 | Type: schema.TypeString, 326 | Optional: true, 327 | }, 328 | "spice_password": &schema.Schema{ 329 | Type: schema.TypeString, 330 | Optional: true, 331 | }, 332 | "spice_port": &schema.Schema{ 333 | Type: schema.TypeString, 334 | Optional: true, 335 | }, 336 | "tmpfs": &schema.Schema{ 337 | Type: schema.TypeInt, 338 | Optional: true, 339 | }, 340 | "uuid": &schema.Schema{ 341 | Type: schema.TypeString, 342 | Optional: true, 343 | }, 344 | */ 345 | "vcpus": &schema.Schema{ 346 | Type: schema.TypeInt, 347 | Optional: true, 348 | ForceNew: true, 349 | }, 350 | /* 351 | "vga": &schema.Schema{ 352 | Type: schema.TypeString, 353 | Optional: true, 354 | }, 355 | "virtio_txburst": &schema.Schema{ 356 | Type: schema.TypeInt, 357 | Optional: true, 358 | }, 359 | "virtio_txtimer": &schema.Schema{ 360 | Type: schema.TypeInt, 361 | Optional: true, 362 | }, 363 | "vnc_password": &schema.Schema{ 364 | Type: schema.TypeString, 365 | Optional: true, 366 | }, 367 | "vnc_port": &schema.Schema{ 368 | Type: schema.TypeInt, 369 | Optional: true, 370 | }, 371 | "zfs_data_compression": &schema.Schema{ 372 | Type: schema.TypeString, 373 | Optional: true, 374 | }, 375 | "zfs_data_recsize": &schema.Schema{ 376 | Type: schema.TypeInt, 377 | Optional: true, 378 | }, 379 | "zfs_filesystem_limit": &schema.Schema{ 380 | Type: schema.TypeInt, 381 | Optional: true, 382 | }, 383 | "zfs_io_priority": &schema.Schema{ 384 | Type: schema.TypeInt, 385 | Optional: true, 386 | }, 387 | "zfs_root_compression": &schema.Schema{ 388 | Type: schema.TypeString, 389 | Optional: true, 390 | }, 391 | "zfs_root_recsize": &schema.Schema{ 392 | Type: schema.TypeInt, 393 | Optional: true, 394 | }, 395 | "zfs_snapshot_limit": &schema.Schema{ 396 | Type: schema.TypeInt, 397 | Optional: true, 398 | }, 399 | "zlog_max_size": &schema.Schema{ 400 | Type: schema.TypeInt, 401 | Optional: true, 402 | }, 403 | "zpool": &schema.Schema{ 404 | Type: schema.TypeString, 405 | Optional: true, 406 | }, 407 | */ 408 | }, 409 | } 410 | } 411 | 412 | func resourceMachineCreate(d *schema.ResourceData, m interface{}) error { 413 | log.Printf("---------------- MachineCreate") 414 | d.SetId("") 415 | 416 | client := m.(*SmartOSClient) 417 | machine := Machine{} 418 | err := machine.LoadFromSchema(d) 419 | if err != nil { 420 | return err 421 | } 422 | 423 | uuid, err := client.CreateMachine(&machine) 424 | if err != nil { 425 | return err 426 | } 427 | 428 | d.SetId(uuid.String()) 429 | 430 | err = resourceMachineRead(d, m) 431 | log.Printf("---------------- MachineCreate (COMPLETE)") 432 | return err 433 | } 434 | 435 | func resourceMachineRead(d *schema.ResourceData, m interface{}) error { 436 | log.Printf("---------------- MachineRead") 437 | client := m.(*SmartOSClient) 438 | uuid, err := uuid.Parse(d.Id()) 439 | if err != nil { 440 | log.Printf("Failed to parse incoming ID: %s", err) 441 | return err 442 | } 443 | 444 | machine, err := client.GetMachine(uuid) 445 | if err != nil { 446 | log.Printf("Failed to retrieve machine with ID %s. Error: %s", d.Id(), err) 447 | return err 448 | } 449 | 450 | err = machine.SaveToSchema(d) 451 | log.Printf("---------------- MachineRead (COMPLETE)") 452 | return err 453 | } 454 | 455 | func resourceMachineUpdate(d *schema.ResourceData, m interface{}) error { 456 | log.Printf("---------------- MachineUpdate") 457 | machineId, err := uuid.Parse(d.Id()) 458 | if err != nil { 459 | return err 460 | } 461 | 462 | d.Partial(true) 463 | 464 | machineUpdate := Machine{ 465 | ID: &machineId, 466 | } 467 | 468 | updatesRequired := false 469 | 470 | if d.HasChange("alias") && !d.IsNewResource() { 471 | _, newValue := d.GetChange("alias") 472 | 473 | machineUpdate.Alias = newValue.(string) 474 | updatesRequired = true 475 | } 476 | 477 | if d.HasChange("autoboot") && !d.IsNewResource() { 478 | _, newValue := d.GetChange("autoboot") 479 | 480 | machineUpdate.Autoboot = newBool(newValue.(bool)) 481 | updatesRequired = true 482 | } 483 | 484 | if d.HasChange("cpu_cap") && !d.IsNewResource() { 485 | _, newValue := d.GetChange("cpu_cap") 486 | 487 | machineUpdate.CPUCap = newUint32(uint32(newValue.(int))) 488 | updatesRequired = true 489 | } 490 | 491 | if d.HasChange("customer_metadata") && !d.IsNewResource() { 492 | oldSchemaValue, newSchemaValue := d.GetChange("customer_metadata") 493 | oldMap := oldSchemaValue.(map[string]interface{}) 494 | newMap := newSchemaValue.(map[string]interface{}) 495 | 496 | var addItem func(key string, value interface{}) = machineUpdate.setCustomerMetadata 497 | var removeItem func(key string) = machineUpdate.removeCustomerMetadata 498 | 499 | if ReconcileMaps(oldMap, newMap, addItem, addItem, removeItem, stringsAreEqual) { 500 | updatesRequired = true 501 | } 502 | } 503 | 504 | if d.HasChange("maintain_resolvers") && !d.IsNewResource() { 505 | _, newValue := d.GetChange("maintain_resolvers") 506 | 507 | machineUpdate.MaintainResolvers = newBool(newValue.(bool)) 508 | updatesRequired = true 509 | } 510 | 511 | if d.HasChange("max_physical_memory") && !d.IsNewResource() { 512 | _, newValue := d.GetChange("max_physical_memory") 513 | 514 | machineUpdate.MaxPhysicalMemory = newUint32(uint32(newValue.(int))) 515 | updatesRequired = true 516 | } 517 | 518 | if d.HasChange("quota") && !d.IsNewResource() { 519 | _, newValue := d.GetChange("quota") 520 | 521 | machineUpdate.Quota = newUint32(uint32(newValue.(int))) 522 | updatesRequired = true 523 | } 524 | 525 | if d.HasChange("resolvers") && !d.IsNewResource() { 526 | _, newSchemaValue := d.GetChange("resolvers") 527 | 528 | var resolvers []string 529 | for _, resolver := range newSchemaValue.([]interface{}) { 530 | resolvers = append(resolvers, resolver.(string)) 531 | } 532 | machineUpdate.Resolvers = resolvers 533 | updatesRequired = true 534 | } 535 | 536 | if d.HasChange("nics") && !d.IsNewResource() { 537 | _, newSchemaValue := d.GetChange("nics") 538 | 539 | var nics []NetworkInterface 540 | for _, nic := range newSchemaValue.([]interface{}) { 541 | nics = append(nics, nic.(NetworkInterface)) 542 | } 543 | machineUpdate.NetworkInterfaces = nics 544 | updatesRequired = true 545 | } 546 | 547 | if updatesRequired { 548 | client := m.(*SmartOSClient) 549 | 550 | err = client.UpdateMachine(&machineUpdate) 551 | if err != nil { 552 | return err 553 | } 554 | } 555 | 556 | d.Partial(false) 557 | err = resourceMachineRead(d, m) 558 | log.Printf("---------------- MachineUpdate (COMPLETE)") 559 | return err 560 | } 561 | 562 | func resourceMachineDelete(d *schema.ResourceData, m interface{}) error { 563 | log.Printf("Request to delete machine with ID: %s\n", d.Id()) 564 | 565 | client := m.(*SmartOSClient) 566 | machineId, err := uuid.Parse(d.Id()) 567 | if err != nil { 568 | return err 569 | } 570 | 571 | return client.DeleteMachine(machineId) 572 | } 573 | --------------------------------------------------------------------------------