Merge pull request #13 from AAbouZaid/multi_device

[WIP] Add support for multi device.
This commit is contained in:
Grant Gongaware 2018-11-12 14:26:04 -08:00 committed by GitHub
commit 95d52ed4d6
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
3 changed files with 426 additions and 168 deletions

View File

@ -1,4 +1,4 @@
# Proxmox 4 Terraform
# Proxmox 4 Terraform
Terraform provider plugin for proxmox
@ -78,13 +78,27 @@ resource "proxmox_vm_qemu" "prepprovision-test" {
target_node = "proxmox1-xx"
clone = "terraform-ubuntu1404-template"
storage = "local"
cores = 3
sockets = 1
memory = 2560
disk_gb = 4
nic = "virtio"
bridge = "vmbr1"
network {
id = 0
model = "virtio"
}
network {
id = 1
model = "virtio"
bridge = "vmbr1"
}
disk {
id = 0
type = virtio
storage = local-lvm
storage_type = lvm
size = 4G
backup = true
}
preprovision = true
ssh_forward_ip = "10.0.0.1"
ssh_user = "terraform"
ssh_private_key = <<EOF
@ -117,7 +131,7 @@ You can start from either an ISO or clone an existing VM.
Optimally, you could create a VM resource you will use a clone base with an ISO, and make the rest of the VM resources depend on that base "template" and clone it.
Interesting parameters:
**preprovision** - to enable or disable internal pre-provisioning (e.g. if you already have another way to provision VMs). Conflicts with: `ssh_forward_ip`, `ssh_user`, `ssh_private_key`, `os_type`, `os_network_config`.
**os_type** -
* cloud-init - from Proxmox 5.2
* ubuntu -(https://github.com/Telmate/terraform-ubuntu-proxmox-iso)

View File

@ -61,8 +61,9 @@ func applyFn(ctx context.Context) error {
return err
}
time.Sleep(10 * time.Second)
var vmParams map[string]interface{}
vmParams["net1"] = data.Get("net1").(string)
vmParams := map[string]interface{}{
"net1": data.Get("net1").(string),
}
_, err = client.SetVmConfig(vmr, vmParams)
time.Sleep(10 * time.Second)
return err

View File

@ -42,9 +42,10 @@ func resourceVmQemu() *schema.Resource {
Required: true,
ForceNew: true,
},
"ssh_forward_ip": {
Type: schema.TypeString,
"onboot": {
Type: schema.TypeBool,
Optional: true,
Default: true,
},
"iso": {
Type: schema.TypeString,
@ -56,31 +57,10 @@ func resourceVmQemu() *schema.Resource {
Optional: true,
ForceNew: true,
},
"storage": {
Type: schema.TypeString,
Required: true,
},
"storage_type": {
Type: schema.TypeString,
Optional: true,
ForceNew: false,
DiffSuppressFunc: func(k, old, new string, d *schema.ResourceData) bool {
if new == "" {
return true // empty template ok
}
return strings.TrimSpace(old) == strings.TrimSpace(new)
},
},
"qemu_os": {
Type: schema.TypeString,
Optional: true,
Default: "l26",
DiffSuppressFunc: func(k, old, new string, d *schema.ResourceData) bool {
if old == "" {
return true // reading empty ok
}
return strings.TrimSpace(old) == strings.TrimSpace(new)
},
},
"memory": {
Type: schema.TypeInt,
@ -94,9 +74,120 @@ func resourceVmQemu() *schema.Resource {
Type: schema.TypeInt,
Required: true,
},
"network": &schema.Schema{
Type: schema.TypeSet,
Optional: true,
ConflictsWith: []string{"nic", "bridge", "vlan", "mac"},
Elem: &schema.Resource{
Schema: map[string]*schema.Schema{
"id": &schema.Schema{
Type: schema.TypeInt,
Required: true,
},
"model": &schema.Schema{
Type: schema.TypeString,
Required: true,
},
"macaddr": &schema.Schema{
// TODO: Find a way to set MAC address in .tf config.
Type: schema.TypeString,
Computed: true,
},
"bridge": &schema.Schema{
Type: schema.TypeString,
Optional: true,
Default: "nat",
},
"tag": &schema.Schema{
Type: schema.TypeInt,
Optional: true,
Description: "VLAN tag.",
Default: -1,
},
"firewall": &schema.Schema{
Type: schema.TypeBool,
Optional: true,
Default: false,
},
"rate": &schema.Schema{
Type: schema.TypeInt,
Optional: true,
Default: -1,
},
"queues": &schema.Schema{
Type: schema.TypeInt,
Optional: true,
Default: -1,
},
"link_down": &schema.Schema{
Type: schema.TypeBool,
Optional: true,
Default: false,
},
},
},
},
"disk": &schema.Schema{
Type: schema.TypeSet,
Optional: true,
ConflictsWith: []string{"disk_gb", "storage", "storage_type"},
Elem: &schema.Resource{
Schema: map[string]*schema.Schema{
"id": &schema.Schema{
Type: schema.TypeInt,
Required: true,
},
"type": &schema.Schema{
Type: schema.TypeString,
Required: true,
},
"storage": &schema.Schema{
Type: schema.TypeString,
Required: true,
},
"storage_type": &schema.Schema{
Type: schema.TypeString,
Optional: true,
Default: "dir",
Description: "One of PVE types as described: https://pve.proxmox.com/wiki/Storage",
},
"size": &schema.Schema{
Type: schema.TypeString,
Required: true,
},
"format": &schema.Schema{
Type: schema.TypeString,
Optional: true,
Default: "raw",
},
"cache": &schema.Schema{
Type: schema.TypeString,
Optional: true,
Default: "none",
},
"backup": &schema.Schema{
Type: schema.TypeBool,
Optional: true,
Default: false,
},
"iothread": &schema.Schema{
Type: schema.TypeBool,
Optional: true,
Default: false,
},
"replicate": &schema.Schema{
Type: schema.TypeBool,
Optional: true,
Default: false,
},
},
},
},
// Deprecated single disk config.
"disk_gb": {
Type: schema.TypeFloat,
Required: true,
Type: schema.TypeFloat,
Deprecated: "Use `disk.size` instead",
Optional: true,
DiffSuppressFunc: func(k, old, new string, d *schema.ResourceData) bool {
// bigger ok
oldf, _ := strconv.ParseFloat(old, 64)
@ -104,18 +195,50 @@ func resourceVmQemu() *schema.Resource {
return oldf >= newf
},
},
"storage": {
Type: schema.TypeString,
Deprecated: "Use `disk.storage` instead",
Optional: true,
},
"storage_type": {
Type: schema.TypeString,
Deprecated: "Use `disk.type` instead",
Optional: true,
ForceNew: false,
DiffSuppressFunc: func(k, old, new string, d *schema.ResourceData) bool {
if new == "" {
return true // empty template ok
}
return strings.TrimSpace(old) == strings.TrimSpace(new)
},
},
// Deprecated single nic config.
"nic": {
Type: schema.TypeString,
Required: true,
Type: schema.TypeString,
Deprecated: "Use `network` instead",
Optional: true,
},
"bridge": {
Type: schema.TypeString,
Required: true,
Type: schema.TypeString,
Deprecated: "Use `network.bridge` instead",
Optional: true,
},
"vlan": {
Type: schema.TypeInt,
Optional: true,
Default: -1,
Type: schema.TypeInt,
Deprecated: "Use `network.tag` instead",
Optional: true,
Default: -1,
},
"mac": {
Type: schema.TypeString,
Deprecated: "Use `network.macaddr` to access the auto generated MAC address",
Optional: true,
DiffSuppressFunc: func(k, old, new string, d *schema.ResourceData) bool {
if new == "" {
return true // macaddr auto-generates and its ok
}
return strings.TrimSpace(old) == strings.TrimSpace(new)
},
},
"os_type": {
Type: schema.TypeString,
@ -129,6 +252,10 @@ func resourceVmQemu() *schema.Resource {
return strings.TrimSpace(old) == strings.TrimSpace(new)
},
},
"ssh_forward_ip": {
Type: schema.TypeString,
Optional: true,
},
"ssh_user": {
Type: schema.TypeString,
Optional: true,
@ -147,16 +274,6 @@ func resourceVmQemu() *schema.Resource {
Optional: true,
Default: false,
},
"mac": {
Type: schema.TypeString,
Optional: true,
DiffSuppressFunc: func(k, old, new string, d *schema.ResourceData) bool {
if new == "" {
return true // macaddr auto-generates and its ok
}
return strings.TrimSpace(old) == strings.TrimSpace(new)
},
},
"ci_wait": { // how long to wait before provision
Type: schema.TypeInt,
Optional: true,
@ -199,6 +316,12 @@ func resourceVmQemu() *schema.Resource {
Type: schema.TypeString,
Optional: true,
},
"preprovision": {
Type: schema.TypeBool,
Optional: true,
Default: true,
ConflictsWith: []string{"ssh_forward_ip", "ssh_user", "ssh_private_key", "os_type", "os_network_config"},
},
},
}
}
@ -210,20 +333,22 @@ func resourceVmQemuCreate(d *schema.ResourceData, meta interface{}) error {
pmParallelBegin(pconf)
client := pconf.Client
vmName := d.Get("name").(string)
diskGB := d.Get("disk_gb").(float64)
networks := d.Get("network").(*schema.Set)
qemuNetworks := devicesSetToMap(networks)
disks := d.Get("disk").(*schema.Set)
qemuDisks := devicesSetToMap(disks)
config := pxapi.ConfigQemu{
Name: vmName,
Description: d.Get("desc").(string),
Storage: d.Get("storage").(string),
Onboot: d.Get("onboot").(bool),
Memory: d.Get("memory").(int),
QemuCores: d.Get("cores").(int),
QemuSockets: d.Get("sockets").(int),
DiskSize: diskGB,
QemuOs: d.Get("qemu_os").(string),
QemuNicModel: d.Get("nic").(string),
QemuBrige: d.Get("bridge").(string),
QemuVlanTag: d.Get("vlan").(int),
QemuMacAddr: d.Get("mac").(string),
QemuNetworks: qemuNetworks,
QemuDisks: qemuDisks,
// Cloud-init.
CIuser: d.Get("ciuser").(string),
CIpassword: d.Get("cipassword").(string),
Searchdomain: d.Get("searchdomain").(string),
@ -231,6 +356,14 @@ func resourceVmQemuCreate(d *schema.ResourceData, meta interface{}) error {
Sshkeys: d.Get("sshkeys").(string),
Ipconfig0: d.Get("ipconfig0").(string),
Ipconfig1: d.Get("ipconfig1").(string),
// Deprecated single disk config.
Storage: d.Get("storage").(string),
DiskSize: d.Get("disk_gb").(float64),
// Deprecated single nic config.
QemuNicModel: d.Get("nic").(string),
QemuBrige: d.Get("bridge").(string),
QemuVlanTag: d.Get("vlan").(int),
QemuMacAddr: d.Get("mac").(string),
}
log.Print("[DEBUG] checking for duplicate name")
dupVmr, _ := client.GetVmRefByName(vmName)
@ -275,7 +408,7 @@ func resourceVmQemuCreate(d *schema.ResourceData, meta interface{}) error {
// give sometime to proxmox to catchup
time.Sleep(5 * time.Second)
err = prepareDiskSize(client, vmr, diskGB)
err = prepareDiskSize(client, vmr, qemuDisks)
if err != nil {
pmParallelEnd(pconf)
return err
@ -303,7 +436,7 @@ func resourceVmQemuCreate(d *schema.ResourceData, meta interface{}) error {
// give sometime to proxmox to catchup
time.Sleep(5 * time.Second)
err = prepareDiskSize(client, vmr, diskGB)
err = prepareDiskSize(client, vmr, qemuDisks)
if err != nil {
pmParallelEnd(pconf)
return err
@ -321,75 +454,8 @@ func resourceVmQemuCreate(d *schema.ResourceData, meta interface{}) error {
return err
}
sshPort := "22"
sshHost := ""
if config.HasCloudInit() {
if d.Get("ssh_forward_ip") != nil {
sshHost = d.Get("ssh_forward_ip").(string)
}
if sshHost == "" {
// parse IP address out of ipconfig0
ipMatch := rxIPconfig.FindStringSubmatch(d.Get("ipconfig0").(string))
sshHost = ipMatch[1]
}
// Check if we got a speficied port
if strings.Contains(sshHost, ":") {
sshParts := strings.Split(sshHost, ":")
sshHost = sshParts[0]
sshPort = sshParts[1]
}
} else {
log.Print("[DEBUG] setting up SSH forward")
sshPort, err = pxapi.SshForwardUsernet(vmr, client)
if err != nil {
pmParallelEnd(pconf)
return err
}
sshHost = d.Get("ssh_forward_ip").(string)
}
// Done with proxmox API, end parallel and do the SSH things
pmParallelEnd(pconf)
d.SetConnInfo(map[string]string{
"type": "ssh",
"host": sshHost,
"port": sshPort,
"user": d.Get("ssh_user").(string),
"private_key": d.Get("ssh_private_key").(string),
"pm_api_url": client.ApiUrl,
"pm_user": client.Username,
"pm_password": client.Password,
"pm_tls_insecure": "true", // TODO - pass pm_tls_insecure state around, but if we made it this far, default insecure
})
switch d.Get("os_type").(string) {
case "ubuntu":
// give sometime to bootup
time.Sleep(9 * time.Second)
err = preProvisionUbuntu(d)
if err != nil {
return err
}
case "centos":
// give sometime to bootup
time.Sleep(9 * time.Second)
err = preProvisionCentos(d)
if err != nil {
return err
}
case "cloud-init":
// wait for OS too boot awhile...
log.Print("[DEBUG] sleeping for OS bootup...")
time.Sleep(time.Duration(d.Get("ci_wait").(int)) * time.Second)
default:
return fmt.Errorf("Unknown os_type: %s", d.Get("os_type").(string))
}
// Apply pre-provision if enabled.
preprovision(d, pconf, client, vmr, true)
return nil
}
@ -404,20 +470,22 @@ func resourceVmQemuUpdate(d *schema.ResourceData, meta interface{}) error {
return err
}
vmName := d.Get("name").(string)
diskGB := d.Get("disk_gb").(float64)
configDisksSet := d.Get("disk").(*schema.Set)
qemuDisks := devicesSetToMap(configDisksSet)
configNetworksSet := d.Get("network").(*schema.Set)
qemuNetworks := devicesSetToMap(configNetworksSet)
config := pxapi.ConfigQemu{
Name: vmName,
Description: d.Get("desc").(string),
Storage: d.Get("storage").(string),
Onboot: d.Get("onboot").(bool),
Memory: d.Get("memory").(int),
QemuCores: d.Get("cores").(int),
QemuSockets: d.Get("sockets").(int),
DiskSize: diskGB,
QemuOs: d.Get("qemu_os").(string),
QemuNicModel: d.Get("nic").(string),
QemuBrige: d.Get("bridge").(string),
QemuVlanTag: d.Get("vlan").(int),
QemuMacAddr: d.Get("mac").(string),
QemuNetworks: qemuNetworks,
QemuDisks: qemuDisks,
// Cloud-init.
CIuser: d.Get("ciuser").(string),
CIpassword: d.Get("cipassword").(string),
Searchdomain: d.Get("searchdomain").(string),
@ -425,6 +493,14 @@ func resourceVmQemuUpdate(d *schema.ResourceData, meta interface{}) error {
Sshkeys: d.Get("sshkeys").(string),
Ipconfig0: d.Get("ipconfig0").(string),
Ipconfig1: d.Get("ipconfig1").(string),
// Deprecated single disk config.
Storage: d.Get("storage").(string),
DiskSize: d.Get("disk_gb").(float64),
// Deprecated single nic config.
QemuNicModel: d.Get("nic").(string),
QemuBrige: d.Get("bridge").(string),
QemuVlanTag: d.Get("vlan").(int),
QemuMacAddr: d.Get("mac").(string),
}
err = config.UpdateConfig(vmr, client)
@ -436,31 +512,24 @@ func resourceVmQemuUpdate(d *schema.ResourceData, meta interface{}) error {
// give sometime to proxmox to catchup
time.Sleep(5 * time.Second)
prepareDiskSize(client, vmr, diskGB)
prepareDiskSize(client, vmr, qemuDisks)
// give sometime to proxmox to catchup
time.Sleep(5 * time.Second)
log.Print("[DEBUG] starting VM")
_, err = client.StartVm(vmr)
if err != nil {
// Start VM only if it wasn't running.
vmState, err := client.GetVmState(vmr)
if err == nil && vmState["status"] == "stopped" {
log.Print("[DEBUG] starting VM")
_, err = client.StartVm(vmr)
} else if err != nil {
pmParallelEnd(pconf)
return err
}
sshPort, err := pxapi.SshForwardUsernet(vmr, client)
if err != nil {
pmParallelEnd(pconf)
return err
}
d.SetConnInfo(map[string]string{
"type": "ssh",
"host": d.Get("ssh_forward_ip").(string),
"port": sshPort,
"user": d.Get("ssh_user").(string),
"private_key": d.Get("ssh_private_key").(string),
})
// Apply pre-provision if enabled.
preprovision(d, pconf, client, vmr, false)
pmParallelEnd(pconf)
// give sometime to bootup
@ -485,18 +554,12 @@ func resourceVmQemuRead(d *schema.ResourceData, meta interface{}) error {
d.Set("target_node", vmr.Node())
d.Set("name", config.Name)
d.Set("desc", config.Description)
d.Set("storage", config.Storage)
d.Set("storage_type", config.StorageType)
d.Set("onboot", config.Onboot)
d.Set("memory", config.Memory)
d.Set("cores", config.QemuCores)
d.Set("sockets", config.QemuSockets)
d.Set("disk_gb", config.DiskSize)
d.Set("qemu_os", config.QemuOs)
d.Set("nic", config.QemuNicModel)
d.Set("bridge", config.QemuBrige)
d.Set("vlan", config.QemuVlanTag)
d.Set("mac", config.QemuMacAddr)
// Cloud-init.
d.Set("ciuser", config.CIuser)
d.Set("cipassword", config.CIpassword)
d.Set("searchdomain", config.Searchdomain)
@ -504,6 +567,23 @@ func resourceVmQemuRead(d *schema.ResourceData, meta interface{}) error {
d.Set("sshkeys", config.Sshkeys)
d.Set("ipconfig0", config.Ipconfig0)
d.Set("ipconfig1", config.Ipconfig1)
// Disks.
configDisksSet := d.Get("disk").(*schema.Set)
activeDisksSet := updateDevicesSet(configDisksSet, config.QemuDisks)
d.Set("disk", activeDisksSet)
// Networks.
configNetworksSet := d.Get("network").(*schema.Set)
activeNetworksSet := updateDevicesSet(configNetworksSet, config.QemuNetworks)
d.Set("network", activeNetworksSet)
// Deprecated single disk config.
d.Set("storage", config.Storage)
d.Set("disk_gb", config.DiskSize)
d.Set("storage_type", config.StorageType)
// Deprecated single nic config.
d.Set("nic", config.QemuNicModel)
d.Set("bridge", config.QemuBrige)
d.Set("vlan", config.QemuVlanTag)
d.Set("mac", config.QemuMacAddr)
pmParallelEnd(pconf)
return nil
@ -533,18 +613,181 @@ func resourceVmQemuDelete(d *schema.ResourceData, meta interface{}) error {
return err
}
func prepareDiskSize(client *pxapi.Client, vmr *pxapi.VmRef, diskGB float64) error {
// Increase disk size if original disk was smaller than new disk.
func prepareDiskSize(
client *pxapi.Client,
vmr *pxapi.VmRef,
diskConfMap pxapi.QemuDevices,
) error {
clonedConfig, err := pxapi.NewConfigQemuFromApi(vmr, client)
if err != nil {
return err
}
if diskGB > clonedConfig.DiskSize {
log.Print("[DEBUG] resizing disk " + clonedConfig.StorageType)
diffSize := int(math.Ceil(diskGB - clonedConfig.DiskSize))
_, err = client.ResizeQemuDisk(vmr, clonedConfig.StorageType+"0", diffSize)
for _, diskConf := range diskConfMap {
diskID := diskConf["id"].(int)
diskName := fmt.Sprintf("%v%v", diskConf["type"], diskID)
diskSizeGB := diskConf["size"].(string)
diskSize, _ := strconv.ParseFloat(strings.Trim(diskSizeGB, "G"), 64)
if err != nil {
return err
}
if _, diskExists := clonedConfig.QemuDisks[diskID]; !diskExists {
return err
}
clonedDiskSizeGB := clonedConfig.QemuDisks[diskID]["size"].(string)
clonedDiskSize, _ := strconv.ParseFloat(strings.Trim(clonedDiskSizeGB, "G"), 64)
if err != nil {
return err
}
diffSize := int(math.Ceil(diskSize - clonedDiskSize))
if diskSize > clonedDiskSize {
log.Print("[DEBUG] resizing disk " + diskName)
_, err = client.ResizeQemuDisk(vmr, diskName, diffSize)
if err != nil {
return err
}
}
}
return nil
}
// Converting from schema.TypeSet to map of id and conf for each device,
// which will be sent to Proxmox API.
func devicesSetToMap(devicesSet *schema.Set) pxapi.QemuDevices {
devicesMap := pxapi.QemuDevices{}
for _, set := range devicesSet.List() {
setMap, isMap := set.(map[string]interface{})
if isMap {
setID := setMap["id"].(int)
devicesMap[setID] = setMap
}
}
return devicesMap
}
// Update schema.TypeSet with new values comes from Proxmox API.
// TODO: Maybe it's better to create a new Set instead add to current one.
func updateDevicesSet(
devicesSet *schema.Set,
devicesMap pxapi.QemuDevices,
) *schema.Set {
configDevicesMap := devicesSetToMap(devicesSet)
activeDevicesMap := updateDevicesDefaults(devicesMap, configDevicesMap)
for _, setConf := range devicesSet.List() {
devicesSet.Remove(setConf)
setConfMap := setConf.(map[string]interface{})
deviceID := setConfMap["id"].(int)
// Value type should be one of types allowed by Terraform schema types.
for key, value := range activeDevicesMap[deviceID] {
// This nested switch is used for nested config like in `net[n]`,
// where Proxmox uses `key=<0|1>` in string" at the same time
// a boolean could be used in ".tf" files.
switch setConfMap[key].(type) {
case bool:
switch value.(type) {
// If the key is bool and value is int (which comes from Proxmox API),
// should be converted to bool (as in ".tf" conf).
case int:
sValue := strconv.Itoa(value.(int))
bValue, err := strconv.ParseBool(sValue)
if err == nil {
setConfMap[key] = bValue
}
// If value is bool, which comes from Terraform conf, add it directly.
case bool:
setConfMap[key] = value
}
// Anything else will be added as it is.
default:
setConfMap[key] = value
}
devicesSet.Add(setConfMap)
}
}
return devicesSet
}
// Because default values are not stored in Proxmox, so the API returns only active values.
// So to prevent Terraform doing unnecessary diffs, this function reads default values
// from Terraform itself, and fill empty fields.
func updateDevicesDefaults(
activeDevicesMap pxapi.QemuDevices,
configDevicesMap pxapi.QemuDevices,
) pxapi.QemuDevices {
for deviceID, deviceConf := range configDevicesMap {
if _, ok := activeDevicesMap[deviceID]; !ok {
activeDevicesMap[deviceID] = configDevicesMap[deviceID]
}
for key, value := range deviceConf {
if _, ok := activeDevicesMap[deviceID][key]; !ok {
activeDevicesMap[deviceID][key] = value
}
}
}
return activeDevicesMap
}
// Internal pre-provision.
func preprovision(
d *schema.ResourceData,
pconf *providerConfiguration,
client *pxapi.Client,
vmr *pxapi.VmRef,
systemPreProvision bool,
) error {
if d.Get("preprovision").(bool) {
log.Print("[DEBUG] setting up SSH forward")
sshPort, err := pxapi.SshForwardUsernet(vmr, client)
if err != nil {
pmParallelEnd(pconf)
return err
}
// Done with proxmox API, end parallel and do the SSH things
pmParallelEnd(pconf)
d.SetConnInfo(map[string]string{
"type": "ssh",
"host": d.Get("ssh_forward_ip").(string),
"port": sshPort,
"user": d.Get("ssh_user").(string),
"private_key": d.Get("ssh_private_key").(string),
"pm_api_url": client.ApiUrl,
"pm_user": client.Username,
"pm_password": client.Password,
})
if systemPreProvision {
switch d.Get("os_type").(string) {
case "ubuntu":
// give sometime to bootup
time.Sleep(9 * time.Second)
err = preProvisionUbuntu(d)
if err != nil {
return err
}
case "centos":
// give sometime to bootup
time.Sleep(9 * time.Second)
err = preProvisionCentos(d)
if err != nil {
return err
}
default:
return fmt.Errorf("Unknown os_type: %s", d.Get("os_type").(string))
}
}
}
return nil
}