diff --git a/CHANGELOG.md b/CHANGELOG.md index b59e8ecfb..83d4ee84b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,7 @@ FEATURES: * resources/opennebula_virtual_machine: Change 'image_id' disk attribute from Required to Optional ([#71](https://github.com/OpenNebula/terraform-provider-opennebula/issues/71)) * **New Resource**: `opennebula_service`: First implementation ([oneflow](http://docs.opennebula.io/5.12/integration/system_interfaces/appflow_api.html#service)), * **New Resource**: `opennebula_service_template`: First implementation ([oneflow-template](http://docs.opennebula.io/5.12/integration/system_interfaces/appflow_api.html#service-template)), +* resources/opennebula_virtual_machine: Enable VM NIC update ([#72](https://github.com/OpenNebula/terraform-provider-opennebula/issues/72)) BUG FIXES: * resources/opennebula_virtual_network: Fix Hold IPs crash ([#67](https://github.com/OpenNebula/terraform-provider-opennebula/issues/67)) diff --git a/opennebula/helpers.go b/opennebula/helpers.go index 375059bec..9874d3120 100644 --- a/opennebula/helpers.go +++ b/opennebula/helpers.go @@ -7,6 +7,8 @@ import ( "github.com/OpenNebula/one/src/oca/go/src/goca/errors" "github.com/OpenNebula/one/src/oca/go/src/goca/schemas/shared" + + "github.com/hashicorp/terraform/helper/schema" ) func inArray(val string, array []string) (index int) { @@ -92,3 +94,38 @@ func NoExists(err error) bool { return false } + +// returns the diff of two lists of schemas, making diff on attrNames only +func diffListConfig(refVecs, vecs []interface{}, s *schema.Resource, attrNames ...string) ([]interface{}, []interface{}) { + + refSet := schema.NewSet(schema.HashResource(s), []interface{}{}) + for _, iface := range refVecs { + sc := iface.(map[string]interface{}) + + // keep only attrNames values + filteredSc := make(map[string]interface{}) + for _, name := range attrNames { + filteredSc[name] = sc[name] + } + + refSet.Add(filteredSc) + } + + set := schema.NewSet(schema.HashResource(s), []interface{}{}) + for _, iface := range vecs { + sc := iface.(map[string]interface{}) + + // keep only attrNames values + filteredSc := make(map[string]interface{}) + for _, name := range attrNames { + filteredSc[name] = sc[name] + } + + set.Add(filteredSc) + } + + pSet := refSet.Difference(set) + mSet := set.Difference(refSet) + + return mSet.List(), pSet.List() +} diff --git a/opennebula/helpers_vm.go b/opennebula/helpers_vm.go index 745de86ae..43dc89060 100644 --- a/opennebula/helpers_vm.go +++ b/opennebula/helpers_vm.go @@ -10,45 +10,12 @@ import ( vmk "github.com/OpenNebula/one/src/oca/go/src/goca/schemas/vm/keys" ) -// return disk configuration with image_id that only appear on refDisks side -func disksConfigDiff(refDisks, disks []interface{}) []map[string]interface{} { - - // get the list of disks ID to detach - diffConfig := make([]map[string]interface{}, 0) - - for _, refDisk := range refDisks { - refDiskConfig := refDisk.(map[string]interface{}) - refImageID := refDiskConfig["image_id"].(int) - // If the disk exists with Default ID, skip it - if refImageID < 0 { - continue - } - - diff := true - for _, disk := range disks { - diskConfig := disk.(map[string]interface{}) - diskImageID := diskConfig["image_id"].(int) - - if refImageID == diskImageID { - diff = false - break - } - } - - if diff { - diffConfig = append(diffConfig, refDiskConfig) - } - } - - return diffConfig -} - // vmDiskAttach is an helper that synchronously attach a disk func vmDiskAttach(vmc *goca.VMController, timeout int, diskTpl *shared.Disk) error { imageID, err := diskTpl.GetI(shared.ImageID) if err != nil { - return fmt.Errorf("disk template doesn't have and image ID") + return fmt.Errorf("disk template doesn't have an image ID") } log.Printf("[DEBUG] Attach image (ID:%d) as disk", imageID) @@ -128,3 +95,89 @@ func vmDiskDetach(vmc *goca.VMController, timeout int, diskID int) error { return nil } + +// vmNICAttach is an helper that synchronously attach a nic +func vmNICAttach(vmc *goca.VMController, timeout int, nicTpl *shared.NIC) error { + + networkID, err := nicTpl.GetI(shared.NetworkID) + if err != nil { + return fmt.Errorf("NIC template doesn't have a network ID") + } + + log.Printf("[DEBUG] Attach NIC to network (ID:%d)", networkID) + + err = vmc.AttachNIC(nicTpl.String()) + if err != nil { + return fmt.Errorf("can't attach network with ID:%d: %s\n", networkID, err) + } + + // wait before checking NIC + _, err = waitForVMState(vmc, timeout, vmNICUpdateReadyStates...) + if err != nil { + return fmt.Errorf( + "waiting for virtual machine (ID:%d) to be in state %s: %s", vmc.ID, strings.Join(vmNICUpdateReadyStates, " "), err) + } + + // Check that NIC is attached + vm, err := vmc.Info(false) + if err != nil { + return err + } + + for _, attachedNic := range vm.Template.GetNICs() { + + attachedNicNetworkID, _ := attachedNic.GetI(shared.NetworkID) + if attachedNicNetworkID == networkID { + return nil + } + } + + // If NIC not attached, retrieve error message + vmerr, _ := vm.UserTemplate.Get(vmk.Error) + + return fmt.Errorf("network ID %d: %s", networkID, vmerr) +} + +// vmNICDetach is an helper that synchronously detach a NIC +func vmNICDetach(vmc *goca.VMController, timeout int, nicID int) error { + + log.Printf("[DEBUG] Detach NIC %d", nicID) + + err := vmc.DetachNIC(nicID) + if err != nil { + return fmt.Errorf("can't detach NIC %d: %s\n", nicID, err) + } + + // wait before checking NIC + _, err = waitForVMState(vmc, timeout, vmNICUpdateReadyStates...) + if err != nil { + return fmt.Errorf( + "waiting for virtual machine (ID:%d) to be in state %s: %s", vmc.ID, strings.Join(vmNICUpdateReadyStates, " "), err) + } + + // Check that NIC is detached + vm, err := vmc.Info(false) + if err != nil { + return err + } + + detached := true + for _, attachedNIC := range vm.Template.GetNICs() { + + attachedNICID, _ := attachedNIC.ID() + if attachedNICID == nicID { + detached = false + break + } + + } + + if !detached { + // If NIC still attached, retrieve error message + vmerr, _ := vm.UserTemplate.Get(vmk.Error) + + return fmt.Errorf("NIC %d: %s", nicID, vmerr) + } + + return nil +} diff --git a/opennebula/resource_opennebula_template.go b/opennebula/resource_opennebula_template.go index afe9e8f41..b959f0102 100644 --- a/opennebula/resource_opennebula_template.go +++ b/opennebula/resource_opennebula_template.go @@ -239,6 +239,38 @@ func resourceOpennebulaTemplateRead(d *schema.ResourceData, meta interface{}) er return err } + // Nics + nics := tpl.Template.GetNICs() + nicList := make([]interface{}, 0, len(nics)) + + // Set Nics to resource + for _, nic := range nics { + nicList = append(nicList, flattenNIC(nic)) + } + + if len(nicList) > 0 { + err = d.Set("nic", nicList) + if err != nil { + return err + } + } + + // Set Disks to resource + disks := tpl.Template.GetDisks() + diskList := make([]interface{}, 0, len(disks)) + + // Set Disks to resource + for _, disk := range disks { + diskList = append(diskList, flattenDisk(disk)) + } + + if len(diskList) > 0 { + err = d.Set("disk", diskList) + if err != nil { + return err + } + } + err = flattenTemplate(d, &tpl.Template, true) if err != nil { return err diff --git a/opennebula/resource_opennebula_virtual_machine.go b/opennebula/resource_opennebula_virtual_machine.go index 257cd8e03..a2d4930e5 100644 --- a/opennebula/resource_opennebula_virtual_machine.go +++ b/opennebula/resource_opennebula_virtual_machine.go @@ -12,11 +12,15 @@ import ( "github.com/OpenNebula/one/src/oca/go/src/goca" dyn "github.com/OpenNebula/one/src/oca/go/src/goca/dynamic" + "github.com/OpenNebula/one/src/oca/go/src/goca/schemas/shared" "github.com/OpenNebula/one/src/oca/go/src/goca/schemas/vm" vmk "github.com/OpenNebula/one/src/oca/go/src/goca/schemas/vm/keys" ) -var vmDiskUpdateReadyStates = []string{"RUNNING", "POWEROFF"} +var ( + vmDiskUpdateReadyStates = []string{"RUNNING", "POWEROFF"} + vmNICUpdateReadyStates = vmDiskUpdateReadyStates +) func resourceOpennebulaVirtualMachine() *schema.Resource { return &schema.Resource{ @@ -122,9 +126,9 @@ func resourceOpennebulaVirtualMachine() *schema.Resource { "vcpu": vcpuSchema(), "memory": memorySchema(), "context": contextSchema(), - "disk": diskSchema(), + "disk": diskVMSchema(), "graphics": graphicsSchema(), - "nic": nicSchema(), + "nic": nicVMSchema(), "os": osSchema(), "vmgroup": vmGroupSchema(), "tags": tagsSchema(), @@ -142,6 +146,77 @@ func resourceOpennebulaVirtualMachine() *schema.Resource { } } +func nicVMFields() *schema.Resource { + return nicFields(map[string]*schema.Schema{ + "nic_id": { + Type: schema.TypeInt, + Computed: true, + }, + "computed_ip": { + Type: schema.TypeString, + Computed: true, + }, + "computed_mac": { + Type: schema.TypeString, + Computed: true, + }, + "computed_model": { + Type: schema.TypeString, + Computed: true, + }, + "computed_physical_device": { + Type: schema.TypeString, + Computed: true, + }, + "computed_security_groups": { + Type: schema.TypeList, + Computed: true, + Elem: &schema.Schema{ + Type: schema.TypeInt, + }, + }, + }) +} + +func nicVMSchema() *schema.Schema { + return &schema.Schema{ + Type: schema.TypeList, + Optional: true, + Description: "Definition of network adapter(s) assigned to the Virtual Machine", + Elem: nicVMFields(), + } +} + +func diskVMFields() *schema.Resource { + return diskFields(map[string]*schema.Schema{ + "disk_id": { + Type: schema.TypeInt, + Computed: true, + }, + "computed_size": { + Type: schema.TypeInt, + Computed: true, + }, + "computed_target": { + Type: schema.TypeString, + Computed: true, + }, + "computed_driver": { + Type: schema.TypeString, + Computed: true, + }, + }) +} + +func diskVMSchema() *schema.Schema { + return &schema.Schema{ + Type: schema.TypeList, + Optional: true, + Description: "Definition of disks assigned to the Virtual Machine", + Elem: diskVMFields(), + } +} + func getVirtualMachineController(d *schema.ResourceData, meta interface{}, args ...int) (*goca.VMController, error) { controller := meta.(*goca.Controller) var vmc *goca.VMController @@ -318,6 +393,16 @@ func resourceOpennebulaVirtualMachineRead(d *schema.ResourceData, meta interface return err } + err = flattenVMDisk(d, &vm.Template) + if err != nil { + return err + } + + err = flattenVMNIC(d, &vm.Template) + if err != nil { + return err + } + err = flattenTemplate(d, &vm.Template, false) if err != nil { return err @@ -331,6 +416,132 @@ func resourceOpennebulaVirtualMachineRead(d *schema.ResourceData, meta interface return nil } +// flattenVMDisk is similar to flattenDisk but deal with computed_* attributes +// this is a temporary solution until we can use nested attributes marked computed and optional +func flattenVMDisk(d *schema.ResourceData, vmTemplate *vm.Template) error { + + // Set disks to resource + disks := vmTemplate.GetDisks() + diskList := make([]interface{}, 0, len(disks)) + + for _, disk := range disks { + + size, _ := disk.GetI(shared.Size) + driver, _ := disk.Get(shared.Driver) + target, _ := disk.Get(shared.TargetDisk) + imageID, _ := disk.GetI(shared.ImageID) + diskID, _ := disk.GetI(shared.DiskID) + + diskRead := map[string]interface{}{ + "image_id": imageID, + "disk_id": diskID, + "computed_size": size, + "computed_target": target, + "computed_driver": driver, + } + + // copy disk config values + diskConfigs := d.Get("disk").([]interface{}) + for j := 0; j < len(diskConfigs); j++ { + diskConfig := diskConfigs[j].(map[string]interface{}) + + if diskConfig["image_id"] != imageID { + continue + } + + diskRead["size"] = diskConfig["size"] + diskRead["target"] = diskConfig["target"] + diskRead["driver"] = diskConfig["driver"] + break + + } + + diskList = append(diskList, diskRead) + } + + if len(diskList) > 0 { + err := d.Set("disk", diskList) + if err != nil { + return err + } + } + + return nil +} + +// flattenVMNIC is similar to flattenNIC but deal with computed_* attributes +// this is a temporary solution until we can use nested attributes marked computed and optional +func flattenVMNIC(d *schema.ResourceData, vmTemplate *vm.Template) error { + + // Set Nics to resource + nics := vmTemplate.GetNICs() + nicList := make([]interface{}, 0, len(nics)) + + for i, nic := range nics { + + nicID, _ := nic.ID() + sg := make([]int, 0) + ip, _ := nic.Get(shared.IP) + mac, _ := nic.Get(shared.MAC) + physicalDevice, _ := nic.GetStr("PHYDEV") + network, _ := nic.Get(shared.Network) + + model, _ := nic.Get(shared.Model) + networkId, _ := nic.GetI(shared.NetworkID) + securityGroupsArray, _ := nic.Get(shared.SecurityGroups) + + sgString := strings.Split(securityGroupsArray, ",") + for _, s := range sgString { + sgInt, _ := strconv.ParseInt(s, 10, 32) + sg = append(sg, int(sgInt)) + } + + nicRead := map[string]interface{}{ + "nic_id": nicID, + "network_id": networkId, + "network": network, + "computed_ip": ip, + "computed_mac": mac, + "computed_physical_device": physicalDevice, + "computed_model": model, + "computed_security_groups": sg, + } + + // copy nic config values + nicsConfigs := d.Get("nic").([]interface{}) + for j := 0; j < len(nicsConfigs); j++ { + nicConfig := nicsConfigs[j].(map[string]interface{}) + + if nicConfig["network_id"] != networkId { + continue + } + + nicRead["ip"] = nicConfig["ip"] + nicRead["mac"] = nicConfig["mac"] + nicRead["model"] = nicConfig["model"] + nicRead["physical_device"] = nicConfig["physical_device"] + nicRead["security_groups"] = nicConfig["security_groups"] + + break + + } + + nicList = append(nicList, nicRead) + + if i == 0 { + d.Set("ip", nicRead["computed_ip"]) + } + } + + if len(nicList) > 0 { + err := d.Set("nic", nicList) + if err != nil { + return err + } + } + return nil +} + func flattenTags(d *schema.ResourceData, vmUserTpl *vm.UserTemplate) error { tags := make(map[string]interface{}) @@ -371,9 +582,6 @@ func resourceOpennebulaVirtualMachineExists(d *schema.ResourceData, meta interfa func resourceOpennebulaVirtualMachineUpdate(d *schema.ResourceData, meta interface{}) error { - // Enable partial state mode - d.Partial(true) - //Get VM vmc, err := getVirtualMachineController(d, meta) if err != nil { @@ -395,7 +603,6 @@ func resourceOpennebulaVirtualMachineUpdate(d *schema.ResourceData, meta interfa // TODO: fix it after 5.10 release // Force the "decrypt" bool to false to keep ONE 5.8 behavior vm, err := vmc.Info(false) - d.SetPartial("name") log.Printf("[INFO] Successfully updated name (%s) for VM ID %x\n", vm.Name, vm.ID) } @@ -406,7 +613,6 @@ func resourceOpennebulaVirtualMachineUpdate(d *schema.ResourceData, meta interfa return err } } - d.SetPartial("permissions") log.Printf("[INFO] Successfully updated Permissions VM %s\n", vm.Name) } @@ -441,20 +647,32 @@ func resourceOpennebulaVirtualMachineUpdate(d *schema.ResourceData, meta interfa timeout := d.Get("timeout").(int) - // wait for the VM to be ready for attach operations - _, err = waitForVMState(vmc, timeout, vmDiskUpdateReadyStates...) - if err != nil { - return fmt.Errorf( - "waiting for virtual machine (ID:%d) to be in state %s: %s", vmc.ID, strings.Join(vmDiskUpdateReadyStates, " "), err) - } - - // get the list of disks ID to detach - toDetach := disksConfigDiff(attachedDisksCfg, newDisksCfg) + // get unique elements of each list of configs + toDetach, toAttach := diffListConfig(newDisksCfg, attachedDisksCfg, + diskVMFields(), + "image_id", + "target", + "driver") // Detach the disks - for _, diskConfig := range toDetach { + var diskID int + for _, diskIf := range toDetach { + diskConfig := diskIf.(map[string]interface{}) - diskID := diskConfig["disk_id"].(int) + imageID := diskConfig["image_id"].(int) + if imageID == -1 { + continue + } + + // retrieve the the disk_id + for _, d := range attachedDisksCfg { + cfg := d.(map[string]interface{}) + if cfg["image_id"].(int) != diskConfig["image_id"].(int) { + continue + } + diskID = cfg["disk_id"].(int) + break + } err := vmDiskDetach(vmc, timeout, diskID) if err != nil { @@ -463,17 +681,16 @@ func resourceOpennebulaVirtualMachineUpdate(d *schema.ResourceData, meta interfa } } - // get the list of disks to attach - toAttach := disksConfigDiff(newDisksCfg, attachedDisksCfg) - // Attach the disks - for _, diskConfig := range toAttach { + for _, diskIf := range toAttach { + diskConfig := diskIf.(map[string]interface{}) imageID := diskConfig["image_id"].(int) - diskTpl := makeDiskVector(map[string]interface{}{ - "image_id": imageID, - "target": diskConfig["target"], - }) + if imageID == -1 { + continue + } + + diskTpl := makeDiskVector(diskConfig) err := vmDiskAttach(vmc, timeout, diskTpl) if err != nil { @@ -482,9 +699,59 @@ func resourceOpennebulaVirtualMachineUpdate(d *schema.ResourceData, meta interfa } } - // We succeeded, disable partial mode. This causes Terraform to save - // save all fields again. - d.Partial(false) + if d.HasChange("nic") { + + log.Printf("[INFO] Update NIC configuration") + + old, new := d.GetChange("nic") + attachedNicsCfg := old.([]interface{}) + newNicsCfg := new.([]interface{}) + + timeout := d.Get("timeout").(int) + + // get unique elements of each list of configs + toDetach, toAttach := diffListConfig(newNicsCfg, attachedNicsCfg, + nicVMFields(), + "network_id", + "ip", + "mac", + "security_groups", + "physical_device") + + // Detach the nics + var nicID int + for _, nicIf := range toDetach { + nicConfig := nicIf.(map[string]interface{}) + + // retrieve the the nic_id + for _, d := range attachedNicsCfg { + cfg := d.(map[string]interface{}) + if cfg["network_id"].(int) != nicConfig["network_id"].(int) { + continue + } + nicID = cfg["nic_id"].(int) + break + } + + err := vmNICDetach(vmc, timeout, nicID) + if err != nil { + return fmt.Errorf("vm nic detach: %s", err) + + } + } + + // Attach the nics + for _, nicIf := range toAttach { + nicConfig := nicIf.(map[string]interface{}) + + nicTpl := makeNICVector(nicConfig) + + err := vmNICAttach(vmc, timeout, nicTpl) + if err != nil { + return fmt.Errorf("vm nic attach: %s", err) + } + } + } return nil } diff --git a/opennebula/resource_opennebula_virtual_machine_test.go b/opennebula/resource_opennebula_virtual_machine_test.go index 2e3d8afe7..0c325f243 100644 --- a/opennebula/resource_opennebula_virtual_machine_test.go +++ b/opennebula/resource_opennebula_virtual_machine_test.go @@ -123,6 +123,14 @@ func TestAccVirtualMachineDiskUpdate(t *testing.T) { resource.TestCheckResourceAttr("opennebula_virtual_machine.test", "disk.0.target", "vdb"), ), }, + { + Config: testAccVirtualMachineTemplateConfigDiskTargetUpdate, + Check: resource.ComposeTestCheckFunc( + resource.TestCheckResourceAttr("opennebula_virtual_machine.test", "name", "test-virtual_machine"), + resource.TestCheckResourceAttr("opennebula_virtual_machine.test", "disk.#", "1"), + resource.TestCheckResourceAttr("opennebula_virtual_machine.test", "disk.0.target", "vdc"), + ), + }, { Config: testAccVirtualMachineTemplateConfigDiskDetached, Check: resource.ComposeTestCheckFunc( @@ -134,6 +142,55 @@ func TestAccVirtualMachineDiskUpdate(t *testing.T) { }) } +func TestAccVirtualMachineNICUpdate(t *testing.T) { + resource.Test(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t) }, + Providers: testAccProviders, + CheckDestroy: testAccCheckVirtualMachineDestroy, + Steps: []resource.TestStep{ + { + Config: testAccVirtualMachineTemplateConfigBasic, + Check: resource.ComposeTestCheckFunc( + testAccSetDSdummy(), + resource.TestCheckResourceAttr("opennebula_virtual_machine.test", "name", "test-virtual_machine"), + resource.TestCheckResourceAttr("opennebula_virtual_machine.test", "nic.#", "0"), + ), + }, + { + Config: testAccVirtualMachineTemplateConfigNIC, + Check: resource.ComposeTestCheckFunc( + resource.TestCheckResourceAttr("opennebula_virtual_machine.test", "name", "test-virtual_machine"), + resource.TestCheckResourceAttr("opennebula_virtual_machine.test", "nic.#", "1"), + resource.TestCheckResourceAttr("opennebula_virtual_machine.test", "nic.0.ip", "172.16.100.131"), + ), + }, + { + Config: testAccVirtualMachineTemplateConfigNICUpdate, + Check: resource.ComposeTestCheckFunc( + resource.TestCheckResourceAttr("opennebula_virtual_machine.test", "name", "test-virtual_machine"), + resource.TestCheckResourceAttr("opennebula_virtual_machine.test", "nic.#", "1"), + resource.TestCheckResourceAttr("opennebula_virtual_machine.test", "nic.0.ip", "172.16.100.111"), + ), + }, + { + Config: testAccVirtualMachineTemplateConfigNICIPUpdate, + Check: resource.ComposeTestCheckFunc( + resource.TestCheckResourceAttr("opennebula_virtual_machine.test", "name", "test-virtual_machine"), + resource.TestCheckResourceAttr("opennebula_virtual_machine.test", "nic.#", "1"), + resource.TestCheckResourceAttr("opennebula_virtual_machine.test", "nic.0.ip", "172.16.100.112"), + ), + }, + { + Config: testAccVirtualMachineTemplateConfigNICDetached, + Check: resource.ComposeTestCheckFunc( + resource.TestCheckResourceAttr("opennebula_virtual_machine.test", "name", "test-virtual_machine"), + resource.TestCheckResourceAttr("opennebula_virtual_machine.test", "nic.#", "0"), + ), + }, + }, + }) +} + func TestAccVirtualMachinePending(t *testing.T) { resource.Test(t, resource.TestCase{ PreCheck: func() { testAccPreCheck(t) }, @@ -458,6 +515,63 @@ resource "opennebula_image" "img1" { } ` +var testAccVirtualMachineTemplateConfigDiskTargetUpdate = ` + +resource "opennebula_image" "img1" { + name = "image1" + type = "DATABLOCK" + size = "16" + datastore_id = 1 + persistent = false + permissions = "660" + } + + resource "opennebula_image" "img2" { + name = "image2" + type = "DATABLOCK" + size = "8" + datastore_id = 1 + persistent = false + permissions = "660" + } + + resource "opennebula_virtual_machine" "test" { + name = "test-virtual_machine" + group = "oneadmin" + permissions = "642" + memory = 128 + cpu = 0.1 + + context = { + NETWORK = "YES" + SET_HOSTNAME = "$NAME" + } + + graphics { + type = "VNC" + listen = "0.0.0.0" + keymap = "en-us" + } + + os { + arch = "x86_64" + boot = "" + } + + tags = { + env = "prod" + customer = "test" + } + + disk { + image_id = opennebula_image.img1.id + target = "vdc" + } + + timeout = 5 +} +` + var testAccVirtualMachineTemplateConfigDiskDetached = ` resource "opennebula_image" "img1" { @@ -509,3 +623,283 @@ resource "opennebula_virtual_machine" "test" { timeout = 5 } ` + +var testAccVirtualMachineTemplateConfigNIC = ` + +resource "opennebula_virtual_network" "net1" { + name = "test-net1" + type = "dummy" + bridge = "onebr" + mtu = 1500 + ar { + ar_type = "IP4" + size = 12 + ip4 = "172.16.100.130" + } + permissions = "642" + group = "oneadmin" + security_groups = [0] + clusters = [0] + } + + resource "opennebula_virtual_network" "net2" { + name = "test-net2" + type = "dummy" + bridge = "onebr" + mtu = 1500 + ar { + ar_type = "IP4" + size = 16 + ip4 = "172.16.100.110" + } + permissions = "642" + group = "oneadmin" + security_groups = [0] + clusters = [0] + } + + +resource "opennebula_virtual_machine" "test" { + name = "test-virtual_machine" + group = "oneadmin" + permissions = "642" + memory = 128 + cpu = 0.1 + + context = { + NETWORK = "YES" + SET_HOSTNAME = "$NAME" + } + + graphics { + type = "VNC" + listen = "0.0.0.0" + keymap = "en-us" + } + + os { + arch = "x86_64" + boot = "" + } + + tags = { + env = "prod" + customer = "test" + } + + nic { + network_id = opennebula_virtual_network.net1.id + ip = "172.16.100.131" + } + + timeout = 5 +} +` + +var testAccVirtualMachineTemplateConfigNICUpdate = ` + +resource "opennebula_virtual_network" "net1" { + name = "test-net1" + type = "dummy" + bridge = "onebr" + mtu = 1500 + ar { + ar_type = "IP4" + size = 12 + ip4 = "172.16.100.130" + } + permissions = "642" + group = "oneadmin" + security_groups = [0] + clusters = [0] + } + + resource "opennebula_virtual_network" "net2" { + name = "test-net2" + type = "dummy" + bridge = "onebr" + mtu = 1500 + ar { + ar_type = "IP4" + size = 16 + ip4 = "172.16.100.110" + } + permissions = "642" + group = "oneadmin" + security_groups = [0] + clusters = [0] + } + + resource "opennebula_virtual_machine" "test" { + name = "test-virtual_machine" + group = "oneadmin" + permissions = "642" + memory = 128 + cpu = 0.1 + + context = { + NETWORK = "YES" + SET_HOSTNAME = "$NAME" + } + + graphics { + type = "VNC" + listen = "0.0.0.0" + keymap = "en-us" + } + + os { + arch = "x86_64" + boot = "" + } + + tags = { + env = "prod" + customer = "test" + } + + nic { + network_id = opennebula_virtual_network.net2.id + ip = "172.16.100.111" + } + + timeout = 5 +} +` + +var testAccVirtualMachineTemplateConfigNICIPUpdate = ` + +resource "opennebula_virtual_network" "net1" { + name = "test-net1" + type = "dummy" + bridge = "onebr" + mtu = 1500 + ar { + ar_type = "IP4" + size = 12 + ip4 = "172.16.100.130" + } + permissions = "642" + group = "oneadmin" + security_groups = [0] + clusters = [0] + } + + resource "opennebula_virtual_network" "net2" { + name = "test-net2" + type = "dummy" + bridge = "onebr" + mtu = 1500 + ar { + ar_type = "IP4" + size = 16 + ip4 = "172.16.100.110" + } + permissions = "642" + group = "oneadmin" + security_groups = [0] + clusters = [0] + } + + resource "opennebula_virtual_machine" "test" { + name = "test-virtual_machine" + group = "oneadmin" + permissions = "642" + memory = 128 + cpu = 0.1 + + context = { + NETWORK = "YES" + SET_HOSTNAME = "$NAME" + } + + graphics { + type = "VNC" + listen = "0.0.0.0" + keymap = "en-us" + } + + os { + arch = "x86_64" + boot = "" + } + + tags = { + env = "prod" + customer = "test" + } + + nic { + network_id = opennebula_virtual_network.net2.id + ip = "172.16.100.112" + } + + timeout = 5 +} +` + +var testAccVirtualMachineTemplateConfigNICDetached = ` + +resource "opennebula_virtual_network" "net1" { + name = "test-net1" + type = "dummy" + bridge = "onebr" + mtu = 1500 + ar { + ar_type = "IP4" + size = 12 + ip4 = "172.16.100.130" + } + permissions = "642" + group = "oneadmin" + security_groups = [0] + clusters = [0] + } + + resource "opennebula_virtual_network" "net2" { + name = "test-net2" + type = "dummy" + bridge = "onebr" + mtu = 1500 + ar { + ar_type = "IP4" + size = 16 + ip4 = "172.16.100.110" + } + permissions = "642" + group = "oneadmin" + security_groups = [0] + clusters = [0] + } + +resource "opennebula_virtual_machine" "test" { + name = "test-virtual_machine" + group = "oneadmin" + permissions = "642" + memory = 128 + cpu = 0.1 + + context = { + NETWORK = "YES" + SET_HOSTNAME = "$NAME" + } + + graphics { + type = "VNC" + listen = "0.0.0.0" + keymap = "en-us" + } + + os { + arch = "x86_64" + boot = "" + } + + tags = { + env = "prod" + customer = "test" + } + + timeout = 5 +} +` diff --git a/opennebula/resource_opennebula_virtual_network.go b/opennebula/resource_opennebula_virtual_network.go index ea8776e56..f1813e030 100644 --- a/opennebula/resource_opennebula_virtual_network.go +++ b/opennebula/resource_opennebula_virtual_network.go @@ -245,7 +245,6 @@ func resourceOpennebulaVirtualNetwork() *schema.Resource { "hold_ips": { Type: schema.TypeList, Optional: true, - Computed: true, Description: "List of IPs to be held the VNET", ConflictsWith: []string{"reservation_vnet", "reservation_size"}, Elem: &schema.Schema{ diff --git a/opennebula/shared_schemas.go b/opennebula/shared_schemas.go index d827714cf..cf5586d8a 100644 --- a/opennebula/shared_schemas.go +++ b/opennebula/shared_schemas.go @@ -13,92 +13,101 @@ import ( vmk "github.com/OpenNebula/one/src/oca/go/src/goca/schemas/vm/keys" ) +func nicFields(customFields ...map[string]*schema.Schema) *schema.Resource { + + fields := map[string]*schema.Schema{ + "ip": { + Type: schema.TypeString, + Optional: true, + }, + "mac": { + Type: schema.TypeString, + Optional: true, + }, + "model": { + Type: schema.TypeString, + Optional: true, + }, + "network_id": { + Type: schema.TypeInt, + Required: true, + }, + "network": { + Type: schema.TypeString, + Computed: true, + }, + "physical_device": { + Type: schema.TypeString, + Optional: true, + }, + "security_groups": { + Type: schema.TypeList, + Optional: true, + Elem: &schema.Schema{ + Type: schema.TypeInt, + }, + }, + } + + for _, m := range customFields { + for k, v := range m { + fields[k] = v + } + } + + return &schema.Resource{ + Schema: fields, + } +} + func nicSchema() *schema.Schema { return &schema.Schema{ Type: schema.TypeList, Optional: true, - Computed: true, Description: "Definition of network adapter(s) assigned to the Virtual Machine", - Elem: &schema.Resource{ - Schema: map[string]*schema.Schema{ - "ip": { - Type: schema.TypeString, - Computed: true, - Optional: true, - }, - "mac": { - Type: schema.TypeString, - Computed: true, - Optional: true, - }, - "model": { - Type: schema.TypeString, - Computed: true, - Optional: true, - }, - "network_id": { - Type: schema.TypeInt, - Required: true, - }, - "network": { - Type: schema.TypeString, - Computed: true, - }, - "physical_device": { - Type: schema.TypeString, - Computed: true, - Optional: true, - }, - "security_groups": { - Type: schema.TypeList, - Optional: true, - Computed: true, - Elem: &schema.Schema{ - Type: schema.TypeInt, - }, - }, - "nic_id": { - Type: schema.TypeInt, - Computed: true, - }, - }, + Elem: nicFields(), + } +} + +func diskFields(customFields ...map[string]*schema.Schema) *schema.Resource { + fields := map[string]*schema.Schema{ + "image_id": { + Type: schema.TypeInt, + Default: -1, + Optional: true, + Description: "Image Id of the image to attach to the VM. Defaults to -1: no image attached.", + }, + "size": { + Type: schema.TypeInt, + Optional: true, + }, + "target": { + Type: schema.TypeString, + Optional: true, + }, + "driver": { + Type: schema.TypeString, + Optional: true, }, } + + for _, m := range customFields { + for k, v := range m { + fields[k] = v + } + } + + return &schema.Resource{ + Schema: fields, + } } -func diskSchema() *schema.Schema { +func diskSchema(customFields ...map[string]*schema.Schema) *schema.Schema { return &schema.Schema{ Type: schema.TypeList, Optional: true, Description: "Definition of disks assigned to the Virtual Machine", - Elem: &schema.Resource{ - Schema: map[string]*schema.Schema{ - "image_id": { - Type: schema.TypeInt, - Default: -1, - Optional: true, - Description: "Image Id of the image to attach to the VM. Defaults to -1: no image attached.", - }, - "disk_id": { - Type: schema.TypeInt, - Computed: true, - }, - "size": { - Type: schema.TypeInt, - Computed: true, - }, - "target": { - Type: schema.TypeString, - Computed: true, - Optional: true, - }, - "driver": { - Type: schema.TypeString, - Computed: true, - Optional: true, - }, - }, - }, + Elem: diskFields(), } } @@ -249,6 +258,38 @@ func makeDiskVector(diskConfig map[string]interface{}) *shared.Disk { return disk } +func makeNICVector(nicConfig map[string]interface{}) *shared.NIC { + nic := shared.NewNIC() + + for k, v := range nicConfig { + + if k == "network_id" { + nic.Add(shared.NetworkID, strconv.Itoa(v.(int))) + continue + } + + if isEmptyValue(reflect.ValueOf(v)) { + continue + } + + switch k { + case "ip": + nic.Add(shared.IP, v.(string)) + case "mac": + nic.Add(shared.MAC, v.(string)) + case "model": + nic.Add(shared.Model, v.(string)) + case "physical_device": + nic.Add("PHYDEV", v.(string)) + case "security_groups": + nicSecGroups := ArrayToString(v.([]interface{}), ",") + nic.Add(shared.SecurityGroups, nicSecGroups) + } + } + + return nic +} + func generateVMTemplate(d *schema.ResourceData, tpl *vm.Template) { //Generate NIC definition @@ -257,33 +298,9 @@ func generateVMTemplate(d *schema.ResourceData, tpl *vm.Template) { for i := 0; i < len(nics); i++ { nicconfig := nics[i].(map[string]interface{}) - nic := tpl.AddNIC() - for k, v := range nicconfig { - - if k == "network_id" { - nic.Add(shared.NetworkID, strconv.Itoa(v.(int))) - continue - } - - if isEmptyValue(reflect.ValueOf(v)) { - continue - } - - switch k { - case "ip": - nic.Add(shared.IP, v.(string)) - case "mac": - nic.Add(shared.MAC, v.(string)) - case "model": - nic.Add(shared.Model, v.(string)) - case "physical_device": - nic.Add("PHYDEV", v.(string)) - case "security_groups": - nicsecgroups := ArrayToString(v.([]interface{}), ",") - nic.Add(shared.SecurityGroups, nicsecgroups) - } - } + nic := makeNICVector(nicconfig) + tpl.Elements = append(tpl.Elements, nic) } @@ -365,6 +382,50 @@ func generateVMTemplate(d *schema.ResourceData, tpl *vm.Template) { } +func flattenNIC(nic shared.NIC) map[string]interface{} { + + sg := make([]int, 0) + ip, _ := nic.Get(shared.IP) + mac, _ := nic.Get(shared.MAC) + physicalDevice, _ := nic.GetStr("PHYDEV") + network, _ := nic.Get(shared.Network) + + model, _ := nic.Get(shared.Model) + networkId, _ := nic.GetI(shared.NetworkID) + securityGroupsArray, _ := nic.Get(shared.SecurityGroups) + + sgString := strings.Split(securityGroupsArray, ",") + for _, s := range sgString { + sgInt, _ := strconv.ParseInt(s, 10, 32) + sg = append(sg, int(sgInt)) + } + + return map[string]interface{}{ + "ip": ip, + "mac": mac, + "network_id": networkId, + "physical_device": physicalDevice, + "network": network, + "model": model, + "security_groups": sg, + } +} + +func flattenDisk(disk shared.Disk) map[string]interface{} { + + size, _ := disk.GetI(shared.Size) + driver, _ := disk.Get(shared.Driver) + target, _ := disk.Get(shared.TargetDisk) + imageID, _ := disk.GetI(shared.ImageID) + + return map[string]interface{}{ + "image_id": imageID, + "size": size, + "target": target, + "driver": driver, + } +} + func flattenTemplate(d *schema.ResourceData, vmTemplate *vm.Template, tplTags bool) error { var err error @@ -387,12 +448,6 @@ func flattenTemplate(d *schema.ResourceData, vmTemplate *vm.Template, tplTags bo t, _ := vmTemplate.GetIOGraphic(vmk.GraphicType) keymap, _ := vmTemplate.GetIOGraphic(vmk.Keymap) - // Disks - diskList := make([]interface{}, 0, 1) - - // Nics - nicList := make([]interface{}, 0, 1) - // Set VM Group to resource if vmgIdStr != "" { vmgMap = append(vmgMap, map[string]interface{}{ @@ -431,71 +486,6 @@ func flattenTemplate(d *schema.ResourceData, vmTemplate *vm.Template, tplTags bo } } - // Set Disks to Resource - for _, disk := range vmTemplate.GetDisks() { - size, _ := disk.GetI(shared.Size) - driver, _ := disk.Get(shared.Driver) - target, _ := disk.Get(shared.TargetDisk) - imageID, _ := disk.GetI(shared.ImageID) - diskID, _ := disk.GetI(shared.DiskID) - - diskList = append(diskList, map[string]interface{}{ - "image_id": imageID, - "disk_id": diskID, - "size": size, - "target": target, - "driver": driver, - }) - } - - if len(diskList) > 0 { - err = d.Set("disk", diskList) - if err != nil { - return err - } - } - - // Set Nics to resource - for i, nic := range vmTemplate.GetNICs() { - sg := make([]int, 0) - ip, _ := nic.Get(shared.IP) - mac, _ := nic.Get(shared.MAC) - physicalDevice, _ := nic.GetStr("PHYDEV") - network, _ := nic.Get(shared.Network) - nicId, _ := nic.ID() - - model, _ := nic.Get(shared.Model) - networkId, _ := nic.GetI(shared.NetworkID) - securityGroupsArray, _ := nic.Get(shared.SecurityGroups) - - sgString := strings.Split(securityGroupsArray, ",") - for _, s := range sgString { - sgInt, _ := strconv.ParseInt(s, 10, 32) - sg = append(sg, int(sgInt)) - } - - nicList = append(nicList, map[string]interface{}{ - "ip": ip, - "mac": mac, - "network_id": networkId, - "physical_device": physicalDevice, - "network": network, - "nic_id": nicId, - "model": model, - "security_groups": sg, - }) - if i == 0 { - d.Set("ip", ip) - } - } - - if len(nicList) > 0 { - err = d.Set("nic", nicList) - if err != nil { - return err - } - } - if tplTags { tags := make(map[string]interface{}) // Get only tags from userTemplate diff --git a/website/docs/r/virtual_machine.html.markdown b/website/docs/r/virtual_machine.html.markdown index 1f2d722f8..e87d26c66 100644 --- a/website/docs/r/virtual_machine.html.markdown +++ b/website/docs/r/virtual_machine.html.markdown @@ -117,6 +117,8 @@ The following arguments are supported: Minimum 1 item. Maximum 8 items. +A disk update will be triggered in adding or removing a `disk` section, or by a modification of any of these parameters: `image_id`, `target`, `driver` + ### NIC parameters `nic` supports the following arguments @@ -130,6 +132,8 @@ Minimum 1 item. Maximum 8 items. Minimum 1 item. Maximum 8 items. +A NIC update will be triggered in adding or removing a `nic` section, or by a modification of any of these parameters: `network_id`, `ip`, `mac`, `security_groups`, `physical_device` + ### VM group parameters `vmgroup` supports the following arguments: @@ -150,6 +154,24 @@ The following attribute are exported: * `state` - State of the virtual machine. * `lcmstate` - LCM State of the virtual machine. + +### NIC + +* `nic_id` - nic attachment identifier +* `network` - network name +* `computed_ip` - IP of the virtual machine on this network. +* `computed_mac` - MAC of the virtual machine on this network. +* `computed_model` - Nic model driver. +* `computed_physical_device` - Physical device hosting the virtual network. +* `computed_security_groups` - List of security group IDs to use on the virtual. + +### Disk + +* `disk_id` - disk attachment identifier +* `computed_size` - Size (in MB) of the image attached to the virtual machine. Not possible to change a cloned image size. +* `computed_target` - Target name device on the virtual machine. Depends of the image `dev_prefix`. +* `computed_driver` - OpenNebula image driver. + ## Instantiate from a template When the attribute `template_id` is set, here is the behavior: