diff --git a/.gitignore b/.gitignore index decb026..7f822f3 100644 --- a/.gitignore +++ b/.gitignore @@ -1,4 +1,5 @@ .idea +examples/*_override.tf *~* *.bak diff --git a/examples/lxc_example.tf b/examples/lxc_example.tf new file mode 100644 index 0000000..4829f18 --- /dev/null +++ b/examples/lxc_example.tf @@ -0,0 +1,26 @@ +provider "proxmox" { + pm_tls_insecure = true + pm_api_url = "https://proxmox.org/api2/json" + pm_password = "supersecret" + pm_user = "terraform-user@pve" +} + +resource "proxmox_lxc" "lxc-test" { + features { + nesting = true + } + hostname = "terraform-new-container" + network { + id = 0 + name = "eth0" + bridge = "vmbr0" + ip = "dhcp" + ip6 = "dhcp" + } + ostemplate = "shared:vztmpl/centos-7-default_20171212_amd64.tar.xz" + password = "rootroot" + pool = "terraform" + storage = "local-lvm" + target_node = "node-01" + unprivileged = true +} diff --git a/proxmox/provider.go b/proxmox/provider.go index 24bcb30..c829ddc 100644 --- a/proxmox/provider.go +++ b/proxmox/provider.go @@ -58,6 +58,7 @@ func Provider() *schema.Provider { ResourcesMap: map[string]*schema.Resource{ "proxmox_vm_qemu": resourceVmQemu(), + "proxmox_lxc": resourceLxc(), // TODO - storage_iso // TODO - bridge // TODO - vm_qemu_template diff --git a/proxmox/resource_lxc.go b/proxmox/resource_lxc.go new file mode 100644 index 0000000..2be6037 --- /dev/null +++ b/proxmox/resource_lxc.go @@ -0,0 +1,564 @@ +package proxmox + +import ( + pxapi "github.com/Telmate/proxmox-api-go/proxmox" + "github.com/hashicorp/terraform/helper/schema" +) + +func resourceLxc() *schema.Resource { + *pxapi.Debug = true + return &schema.Resource{ + Create: resourceLxcCreate, + Read: resourceLxcRead, + Update: resourceLxcUpdate, + Delete: resourceVmQemuDelete, + Importer: &schema.ResourceImporter{ + State: schema.ImportStatePassthrough, + }, + + Schema: map[string]*schema.Schema{ + "ostemplate": { + Type: schema.TypeString, + Optional: true, + }, + "arch": { + Type: schema.TypeString, + Optional: true, + Default: "amd64", + }, + "bwlimit": { + Type: schema.TypeInt, + Optional: true, + }, + "cmode": { + Type: schema.TypeString, + Optional: true, + Default: "tty", + }, + "console": { + Type: schema.TypeBool, + Optional: true, + Default: true, + }, + "cores": { + Type: schema.TypeInt, + Optional: true, + }, + "cpulimit": { + Type: schema.TypeInt, + Optional: true, + Default: 0, + }, + "cpuunits": { + Type: schema.TypeInt, + Optional: true, + Default: 1024, + }, + "description": { + Type: schema.TypeString, + Optional: true, + }, + "features": { + Type: schema.TypeSet, + Optional: true, + Elem: &schema.Resource{ + Schema: map[string]*schema.Schema{ + "fuse": { + Type: schema.TypeBool, + Optional: true, + }, + "keyctl": { + Type: schema.TypeBool, + Optional: true, + }, + "mount": { + Type: schema.TypeString, + Optional: true, + }, + "nesting": { + Type: schema.TypeBool, + Optional: true, + }, + }, + }, + }, + "force": { + Type: schema.TypeBool, + Optional: true, + }, + "hookscript": { + Type: schema.TypeString, + Optional: true, + }, + "hostname": { + Type: schema.TypeString, + Optional: true, + }, + "ignore_unpack_errors": { + Type: schema.TypeBool, + Optional: true, + }, + "lock": { + Type: schema.TypeString, + Optional: true, + }, + "memory": { + Type: schema.TypeInt, + Optional: true, + Default: 512, + }, + "mountpoint": { + Type: schema.TypeSet, + Optional: true, + Elem: &schema.Resource{ + Schema: map[string]*schema.Schema{ + "id": { + Type: schema.TypeInt, + Required: true, + }, + "volume": { + Type: schema.TypeString, + Required: true, + }, + "mp": { + Type: schema.TypeString, + Required: true, + }, + "acl": { + Type: schema.TypeBool, + Optional: true, + }, + "backup": { + Type: schema.TypeBool, + Optional: true, + }, + "quota": { + Type: schema.TypeBool, + Optional: true, + }, + "replicate": { + Type: schema.TypeBool, + Optional: true, + }, + "shared": { + Type: schema.TypeBool, + Optional: true, + }, + "size": { + Type: schema.TypeInt, + Optional: true, + }, + }, + }, + }, + "nameserver": { + Type: schema.TypeString, + Optional: true, + }, + "network": { + Type: schema.TypeSet, + Optional: true, + Elem: &schema.Resource{ + Schema: map[string]*schema.Schema{ + "id": { + Type: schema.TypeInt, + Required: true, + }, + "name": { + Type: schema.TypeString, + Required: true, + }, + "bridge": { + Type: schema.TypeString, + Optional: true, + }, + "firewall": { + Type: schema.TypeBool, + Optional: true, + }, + "gw": { + Type: schema.TypeBool, + Optional: true, + }, + "gw6": { + Type: schema.TypeBool, + Optional: true, + }, + "hwaddr": { + Type: schema.TypeBool, + Optional: true, + }, + "ip": { + Type: schema.TypeString, + Optional: true, + }, + "ip6": { + Type: schema.TypeString, + Optional: true, + }, + "mtu": { + Type: schema.TypeString, + Optional: true, + }, + "rate": { + Type: schema.TypeInt, + Optional: true, + }, + "tag": { + Type: schema.TypeInt, + Optional: true, + }, + "trunks": { + Type: schema.TypeString, + Optional: true, + }, + "type": { + Type: schema.TypeString, + Optional: true, + }, + }, + }, + }, + "onboot": { + Type: schema.TypeBool, + Optional: true, + Default: false, + }, + "ostype": { + Type: schema.TypeString, + Optional: true, + }, + "password": { + Type: schema.TypeString, + Optional: true, + }, + "pool": { + Type: schema.TypeString, + Optional: true, + }, + "protection": { + Type: schema.TypeBool, + Optional: true, + Default: false, + }, + "restore": { + Type: schema.TypeBool, + Optional: true, + }, + "rootfs": { + Type: schema.TypeString, + Optional: true, + }, + "searchdomain": { + Type: schema.TypeString, + Optional: true, + }, + "ssh_public_keys": { + Type: schema.TypeString, + Optional: true, + }, + "start": { + Type: schema.TypeBool, + Optional: true, + Default: false, + }, + "startup": { + Type: schema.TypeString, + Optional: true, + }, + "storage": { + Type: schema.TypeString, + Optional: true, + Default: "local", + }, + "swap": { + Type: schema.TypeInt, + Optional: true, + Default: 512, + }, + "template": { + Type: schema.TypeBool, + Optional: true, + }, + "tty": { + Type: schema.TypeInt, + Optional: true, + Default: 2, + }, + "unique": { + Type: schema.TypeBool, + Optional: true, + }, + "unprivileged": { + Type: schema.TypeBool, + Optional: true, + Default: false, + }, + "unused": { + Type: schema.TypeList, + Optional: true, + Elem: &schema.Schema{ + Type: schema.TypeString, + }, + }, + "target_node": { + Type: schema.TypeString, + Required: true, + ForceNew: true, + }, + }, + } +} + +func resourceLxcCreate(d *schema.ResourceData, meta interface{}) error { + pconf := meta.(*providerConfiguration) + pmParallelBegin(pconf) + client := pconf.Client + vmName := d.Get("hostname").(string) + + config := pxapi.NewConfigLxc() + config.Ostemplate = d.Get("ostemplate").(string) + config.Arch = d.Get("arch").(string) + config.BWLimit = d.Get("bwlimit").(int) + config.CMode = d.Get("cmode").(string) + config.Console = d.Get("console").(bool) + config.Cores = d.Get("cores").(int) + config.CPULimit = d.Get("cpulimit").(int) + config.CPUUnits = d.Get("cpuunits").(int) + config.Description = d.Get("description").(string) + features := d.Get("features").(*schema.Set) + featureSetList := features.List() + if len(featureSetList) > 0 { + // only apply the first feature set, + // because proxmox api only allows one feature set + config.Features = featureSetList[0].(map[string]interface{}) + } + config.Force = d.Get("force").(bool) + config.Hookscript = d.Get("hookscript").(string) + config.Hostname = vmName + config.IgnoreUnpackErrors = d.Get("ignore_unpack_errors").(bool) + config.Lock = d.Get("lock").(string) + config.Memory = d.Get("memory").(int) + // proxmox api allows multiple mountpoint sets, + // having a unique 'id' parameter foreach set + mountpoints := d.Get("mountpoint").(*schema.Set) + lxcMountpoints := DevicesSetToMap(mountpoints) + config.Mountpoints = lxcMountpoints + config.Nameserver = d.Get("nameserver").(string) + // proxmox api allows multiple network sets, + // having a unique 'id' parameter foreach set + networks := d.Get("network").(*schema.Set) + lxcNetworks := DevicesSetToMap(networks) + config.Networks = lxcNetworks + config.OnBoot = d.Get("onboot").(bool) + config.OsType = d.Get("ostype").(string) + config.Password = d.Get("password").(string) + config.Pool = d.Get("pool").(string) + config.Protection = d.Get("protection").(bool) + config.Restore = d.Get("restore").(bool) + config.RootFs = d.Get("rootfs").(string) + config.SearchDomain = d.Get("searchdomain").(string) + config.SSHPublicKeys = d.Get("ssh_public_keys").(string) + config.Start = d.Get("start").(bool) + config.Startup = d.Get("startup").(string) + config.Storage = d.Get("storage").(string) + config.Swap = d.Get("swap").(int) + config.Template = d.Get("template").(bool) + config.Tty = d.Get("tty").(int) + config.Unique = d.Get("unique").(bool) + config.Unprivileged = d.Get("unprivileged").(bool) + // proxmox api allows to specify unused volumes + // even if it is recommended not to change them manually + unusedVolumes := d.Get("unused").([]interface{}) + var volumes []string + for _, v := range unusedVolumes { + volumes = append(volumes, v.(string)) + } + config.Unused = volumes + + targetNode := d.Get("target_node").(string) + //vmr, _ := client.GetVmRefByName(vmName) + + // get unique id + nextid, err := nextVmId(pconf) + if err != nil { + pmParallelEnd(pconf) + return err + } + vmr := pxapi.NewVmRef(nextid) + vmr.SetNode(targetNode) + err = config.CreateLxc(vmr, client) + if err != nil { + pmParallelEnd(pconf) + return err + } + + // The existence of a non-blank ID is what tells Terraform that a resource was created + d.SetId(resourceId(targetNode, "lxc", vmr.VmId())) + + return resourceLxcRead(d, meta) +} + +func resourceLxcUpdate(d *schema.ResourceData, meta interface{}) error { + pconf := meta.(*providerConfiguration) + pmParallelBegin(pconf) + client := pconf.Client + + _, _, vmID, err := parseResourceId(d.Id()) + if err != nil { + pmParallelEnd(pconf) + return err + } + vmr := pxapi.NewVmRef(vmID) + _, err = client.GetVmInfo(vmr) + if err != nil { + pmParallelEnd(pconf) + return err + } + + config := pxapi.NewConfigLxc() + config.Ostemplate = d.Get("ostemplate").(string) + config.Arch = d.Get("arch").(string) + config.BWLimit = d.Get("bwlimit").(int) + config.CMode = d.Get("cmode").(string) + config.Console = d.Get("console").(bool) + config.Cores = d.Get("cores").(int) + config.CPULimit = d.Get("cpulimit").(int) + config.CPUUnits = d.Get("cpuunits").(int) + config.Description = d.Get("description").(string) + features := d.Get("features").(*schema.Set) + featureSetList := features.List() + if len(featureSetList) > 0 { + // only apply the first feature set, + // because proxmox api only allows one feature set + config.Features = featureSetList[0].(map[string]interface{}) + } + config.Force = d.Get("force").(bool) + config.Hookscript = d.Get("hookscript").(string) + config.Hostname = d.Get("hostname").(string) + config.IgnoreUnpackErrors = d.Get("ignore_unpack_errors").(bool) + config.Lock = d.Get("lock").(string) + config.Memory = d.Get("memory").(int) + // proxmox api allows multiple mountpoint sets, + // having a unique 'id' parameter foreach set + mountpoints := d.Get("mountpoint").(*schema.Set) + lxcMountpoints := DevicesSetToMap(mountpoints) + config.Mountpoints = lxcMountpoints + config.Nameserver = d.Get("nameserver").(string) + // proxmox api allows multiple network sets, + // having a unique 'id' parameter foreach set + networks := d.Get("network").(*schema.Set) + lxcNetworks := DevicesSetToMap(networks) + config.Networks = lxcNetworks + config.OnBoot = d.Get("onboot").(bool) + config.OsType = d.Get("ostype").(string) + config.Password = d.Get("password").(string) + config.Pool = d.Get("pool").(string) + config.Protection = d.Get("protection").(bool) + config.Restore = d.Get("restore").(bool) + config.RootFs = d.Get("rootfs").(string) + config.SearchDomain = d.Get("searchdomain").(string) + config.SSHPublicKeys = d.Get("ssh_public_keys").(string) + config.Start = d.Get("start").(bool) + config.Startup = d.Get("startup").(string) + config.Storage = d.Get("storage").(string) + config.Swap = d.Get("swap").(int) + config.Template = d.Get("template").(bool) + config.Tty = d.Get("tty").(int) + config.Unique = d.Get("unique").(bool) + config.Unprivileged = d.Get("unprivileged").(bool) + // proxmox api allows to specify unused volumes + // even if it is recommended not to change them manually + unusedVolumes := d.Get("unused").([]interface{}) + var volumes []string + for _, v := range unusedVolumes { + volumes = append(volumes, v.(string)) + } + config.Unused = volumes + + err = config.UpdateConfig(vmr, client) + if err != nil { + pmParallelEnd(pconf) + return err + } + + return nil +} + +func resourceLxcRead(d *schema.ResourceData, meta interface{}) error { + pconf := meta.(*providerConfiguration) + pmParallelBegin(pconf) + client := pconf.Client + _, _, vmID, err := parseResourceId(d.Id()) + if err != nil { + pmParallelEnd(pconf) + d.SetId("") + return err + } + vmr := pxapi.NewVmRef(vmID) + _, err = client.GetVmInfo(vmr) + if err != nil { + pmParallelEnd(pconf) + return err + } + config, err := pxapi.NewConfigLxcFromApi(vmr, client) + if err != nil { + pmParallelEnd(pconf) + return err + } + d.SetId(resourceId(vmr.Node(), "lxc", vmr.VmId())) + d.Set("target_node", vmr.Node()) + + d.Set("arch", config.Arch) + d.Set("bwlimit", config.BWLimit) + d.Set("cmode", config.CMode) + d.Set("console", config.Console) + d.Set("cores", config.Cores) + d.Set("cpulimit", config.CPULimit) + d.Set("cpuunits", config.CPUUnits) + d.Set("description", config.Description) + + defaultFeatures := d.Get("features").(*schema.Set) + featuresWithDefaults := UpdateDeviceConfDefaults(config.Features, defaultFeatures) + d.Set("features", featuresWithDefaults) + + d.Set("force", config.Force) + d.Set("hookscript", config.Hookscript) + d.Set("hostname", config.Hostname) + d.Set("ignore_unpack_errors", config.IgnoreUnpackErrors) + d.Set("lock", config.Lock) + d.Set("memory", config.Memory) + + configMountpointSet := d.Get("mountpoint").(*schema.Set) + activeMountpointSet := UpdateDevicesSet(configMountpointSet, config.Mountpoints) + d.Set("mountpoint", activeMountpointSet) + + d.Set("nameserver", config.Nameserver) + + configNetworksSet := d.Get("network").(*schema.Set) + activeNetworksSet := UpdateDevicesSet(configNetworksSet, config.Networks) + d.Set("network", activeNetworksSet) + + d.Set("onboot", config.OnBoot) + d.Set("ostemplate", config.Ostemplate) + d.Set("ostype", config.OsType) + d.Set("password", config.Password) + d.Set("pool", config.Pool) + d.Set("protection", config.Protection) + d.Set("restore", config.Restore) + d.Set("rootfs", config.RootFs) + d.Set("searchdomain", config.SearchDomain) + d.Set("ssh_public_keys", config.SSHPublicKeys) + d.Set("start", config.Start) + d.Set("startup", config.Startup) + d.Set("storage", config.Storage) + d.Set("swap", config.Swap) + d.Set("template", config.Template) + d.Set("tty", config.Tty) + d.Set("unique", config.Unique) + d.Set("unprivileged", config.Unprivileged) + d.Set("unused", config.Unused) + + pmParallelEnd(pconf) + return nil +} diff --git a/proxmox/resource_vm_qemu.go b/proxmox/resource_vm_qemu.go index 183168b..5fa93a2 100644 --- a/proxmox/resource_vm_qemu.go +++ b/proxmox/resource_vm_qemu.go @@ -358,9 +358,9 @@ func resourceVmQemuCreate(d *schema.ResourceData, meta interface{}) error { client := pconf.Client vmName := d.Get("name").(string) networks := d.Get("network").(*schema.Set) - qemuNetworks := devicesSetToMap(networks) + qemuNetworks := DevicesSetToMap(networks) disks := d.Get("disk").(*schema.Set) - qemuDisks := devicesSetToMap(disks) + qemuDisks := DevicesSetToMap(disks) config := pxapi.ConfigQemu{ Name: vmName, @@ -514,9 +514,9 @@ func resourceVmQemuUpdate(d *schema.ResourceData, meta interface{}) error { return err } configDisksSet := d.Get("disk").(*schema.Set) - qemuDisks := devicesSetToMap(configDisksSet) + qemuDisks := DevicesSetToMap(configDisksSet) configNetworksSet := d.Get("network").(*schema.Set) - qemuNetworks := devicesSetToMap(configNetworksSet) + qemuNetworks := DevicesSetToMap(configNetworksSet) config := pxapi.ConfigQemu{ Name: d.Get("name").(string), @@ -625,11 +625,11 @@ func resourceVmQemuRead(d *schema.ResourceData, meta interface{}) error { d.Set("ipconfig1", config.Ipconfig1) // Disks. configDisksSet := d.Get("disk").(*schema.Set) - activeDisksSet := updateDevicesSet(configDisksSet, config.QemuDisks) + activeDisksSet := UpdateDevicesSet(configDisksSet, config.QemuDisks) d.Set("disk", activeDisksSet) // Networks. configNetworksSet := d.Get("network").(*schema.Set) - activeNetworksSet := updateDevicesSet(configNetworksSet, config.QemuNetworks) + activeNetworksSet := UpdateDevicesSet(configNetworksSet, config.QemuNetworks) d.Set("network", activeNetworksSet) // Deprecated single disk config. d.Set("storage", config.Storage) @@ -717,7 +717,7 @@ func diskSizeGB(dcSize interface{}) float64 { // 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 { +func DevicesSetToMap(devicesSet *schema.Set) pxapi.QemuDevices { devicesMap := pxapi.QemuDevices{} @@ -733,12 +733,13 @@ func devicesSetToMap(devicesSet *schema.Set) pxapi.QemuDevices { // 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( +func UpdateDevicesSet( devicesSet *schema.Set, devicesMap pxapi.QemuDevices, ) *schema.Set { - configDevicesMap := devicesSetToMap(devicesSet) + configDevicesMap := DevicesSetToMap(devicesSet) + activeDevicesMap := updateDevicesDefaults(devicesMap, configDevicesMap) for _, setConf := range devicesSet.List() { diff --git a/proxmox/util.go b/proxmox/util.go new file mode 100644 index 0000000..9f00069 --- /dev/null +++ b/proxmox/util.go @@ -0,0 +1,33 @@ +package proxmox + +import ( + "strconv" + pxapi "github.com/Telmate/proxmox-api-go/proxmox" + "github.com/hashicorp/terraform/helper/schema" +) + +func UpdateDeviceConfDefaults( + activeDeviceConf pxapi.QemuDevice, + defaultDeviceConf *schema.Set, +) *schema.Set { + defaultDeviceConfMap := defaultDeviceConf.List()[0].(map[string]interface{}) + for key, _ := range defaultDeviceConfMap { + if deviceConfigValue, ok := activeDeviceConf[key]; ok { + defaultDeviceConfMap[key] = deviceConfigValue + switch deviceConfigValue.(type) { + case int: + sValue := strconv.Itoa(deviceConfigValue.(int)) + bValue, err := strconv.ParseBool(sValue) + if err == nil { + defaultDeviceConfMap[key] = bValue + } + default: + defaultDeviceConfMap[key] = deviceConfigValue + } + } + } + defaultDeviceConf.Remove(defaultDeviceConf.List()[0]) + defaultDeviceConf.Add(defaultDeviceConfMap) + return defaultDeviceConf +} +