diff --git a/softlayer/resource_softlayer_bare_metal.go b/softlayer/resource_softlayer_bare_metal.go index 34c1f2a71..86b6100a2 100644 --- a/softlayer/resource_softlayer_bare_metal.go +++ b/softlayer/resource_softlayer_bare_metal.go @@ -78,7 +78,7 @@ func resourceSoftLayerBareMetal() *schema.Resource { "datacenter": { Type: schema.TypeString, - Required: true, + Optional: true, ForceNew: true, }, @@ -154,7 +154,13 @@ func resourceSoftLayerBareMetal() *schema.Resource { "fixed_config_preset": { Type: schema.TypeString, - Required: true, + Optional: true, + ForceNew: true, + }, + + "quote_id": { + Type: schema.TypeInt, + Optional: true, ForceNew: true, }, @@ -193,10 +199,12 @@ func getBareMetalOrderFromResourceData(d *schema.ResourceData, meta interface{}) NetworkComponents: []datatypes.Network_Component{networkComponent}, PostInstallScriptUri: sl.String(d.Get("post_install_script_uri").(string)), BareMetalInstanceFlag: sl.Int(1), + } - FixedConfigurationPreset: &datatypes.Product_Package_Preset{ - KeyName: sl.String(d.Get("fixed_config_preset").(string)), - }, + if fixed_config_preset, ok := d.GetOk("fixed_config_preset"); ok { + hardware.FixedConfigurationPreset = &datatypes.Product_Package_Preset{ + KeyName: sl.String(fixed_config_preset.(string)), + } } if operatingSystemReferenceCode, ok := d.GetOk("os_reference_code"); ok { @@ -267,23 +275,48 @@ func resourceSoftLayerBareMetalCreate(d *schema.ResourceData, meta interface{}) return err } - order, err := hwService.GenerateOrderTemplate(&hardware) - if err != nil { - return fmt.Errorf( - "Encountered problem trying to get the bare metal order template: %s", err) - } + quote_id := d.Get("quote_id").(int) + if quote_id > 0 { + quoteService := services.GetBillingOrderQuoteService(meta.(ProviderConfig).SoftLayerSession()) + order, err := quoteService.Id(quote_id).GetRecalculatedOrderContainer(nil, sl.Bool(false)) + if err != nil { + return fmt.Errorf( + "Encountered problem trying to get the bare metal order template from quote: %s", err) + } + order.Quantity = sl.Int(1) + order.PresetId = nil + order.Hardware = make([]datatypes.Hardware, 0, 1) + order.Hardware = append( + order.Hardware, + datatypes.Hardware{ + Hostname: hardware.Hostname, + Domain: hardware.Domain, + }, + ) - // Set image template id if it exists - if rawImageTemplateId, ok := d.GetOk("image_template_id"); ok { - imageTemplateId := rawImageTemplateId.(int) - order.ImageTemplateId = sl.Int(imageTemplateId) - } + log.Println("[INFO] Ordering bare metal server") + _, err = orderService.PlaceOrder(&order, sl.Bool(false)) + if err != nil { + return fmt.Errorf("Error ordering bare metal server: %s\n%+v\n", err, order) + } + } else { + order, err := hwService.GenerateOrderTemplate(&hardware) + if err != nil { + return fmt.Errorf( + "Encountered problem trying to get the bare metal order template: %s", err) + } - log.Println("[INFO] Ordering bare metal server") + // Set image template id if it exists + if rawImageTemplateId, ok := d.GetOk("image_template_id"); ok { + imageTemplateId := rawImageTemplateId.(int) + order.ImageTemplateId = sl.Int(imageTemplateId) + } - _, err = orderService.PlaceOrder(&order, sl.Bool(false)) - if err != nil { - return fmt.Errorf("Error ordering bare metal server: %s", err) + log.Println("[INFO] Ordering bare metal server") + _, err = orderService.PlaceOrder(&order, sl.Bool(false)) + if err != nil { + return fmt.Errorf("Error ordering bare metal server: %s\n%+v\n", err, order) + } } log.Printf("[INFO] Bare Metal Server ID: %s", d.Id()) diff --git a/softlayer/resource_softlayer_bare_metal_quote.go b/softlayer/resource_softlayer_bare_metal_quote.go new file mode 100644 index 000000000..cd46af34e --- /dev/null +++ b/softlayer/resource_softlayer_bare_metal_quote.go @@ -0,0 +1,397 @@ +package softlayer + +import ( + "fmt" + "log" + "strconv" + "strings" + "time" + + "github.com/hashicorp/terraform/helper/resource" + "github.com/hashicorp/terraform/helper/schema" + "github.com/softlayer/softlayer-go/datatypes" + "github.com/softlayer/softlayer-go/filter" + "github.com/softlayer/softlayer-go/services" + "github.com/softlayer/softlayer-go/sl" +) + +func resourceSoftLayerBareMetalQuote() *schema.Resource { + return &schema.Resource{ + Create: resourceSoftLayerBareMetalQuoteCreate, + Read: resourceSoftLayerBareMetalQuoteRead, + Update: resourceSoftLayerBareMetalQuoteUpdate, + Delete: resourceSoftLayerBareMetalQuoteDelete, + Exists: resourceSoftLayerBareMetalQuoteExists, + Importer: &schema.ResourceImporter{}, + + Schema: map[string]*schema.Schema{ + "id": { + Type: schema.TypeInt, + Computed: true, + }, + + "hostname": { + Type: schema.TypeString, + Optional: true, + ForceNew: true, + DefaultFunc: genId, + DiffSuppressFunc: func(k, o, n string, d *schema.ResourceData) bool { + // FIXME: Work around another bug in terraform. + // When a default function is used with an optional property, + // terraform will always execute it on apply, even when the property + // already has a value in the state for it. This causes a false diff. + // Making the property Computed:true does not make a difference. + if strings.HasPrefix(o, "terraformed-") && strings.HasPrefix(n, "terraformed-") { + return true + } + + return o == n + }, + }, + + "domain": { + Type: schema.TypeString, + Required: true, + ForceNew: true, + }, + + "public_vlan_id": { + Type: schema.TypeInt, + Optional: true, + ForceNew: true, + Computed: true, + }, + + "public_subnet": { + Type: schema.TypeString, + Optional: true, + ForceNew: true, + Computed: true, + }, + + "private_vlan_id": { + Type: schema.TypeInt, + Optional: true, + ForceNew: true, + Computed: true, + }, + + "private_subnet": { + Type: schema.TypeString, + Optional: true, + ForceNew: true, + Computed: true, + }, + + "public_ipv4_address": { + Type: schema.TypeString, + Computed: true, + }, + + "private_ipv4_address": { + Type: schema.TypeString, + Computed: true, + }, + + "ssh_key_ids": { + Type: schema.TypeList, + Optional: true, + Elem: &schema.Schema{Type: schema.TypeInt}, + ForceNew: true, + }, + + "user_metadata": { + Type: schema.TypeString, + Optional: true, + ForceNew: true, + }, + + "notes": { + Type: schema.TypeString, + Optional: true, + }, + + "post_install_script_uri": { + Type: schema.TypeString, + Optional: true, + Default: nil, + ForceNew: true, + }, + + "quote_id": { + Type: schema.TypeInt, + Required: true, + ForceNew: true, + }, + + "tags": { + Type: schema.TypeSet, + Optional: true, + Elem: &schema.Schema{Type: schema.TypeString}, + Set: schema.HashString, + }, + }, + } +} + +func resourceSoftLayerBareMetalQuoteCreate(d *schema.ResourceData, meta interface{}) error { + sess := meta.(ProviderConfig).SoftLayerSession() + orderService := services.GetProductOrderService(sess) + quoteService := services.GetBillingOrderQuoteService(sess) + + order, err := quoteService.Id(d.Get("quote_id").(int)).GetRecalculatedOrderContainer(nil, sl.Bool(false)) + if err != nil { + return fmt.Errorf( + "Encountered problem trying to get the bare metal order template from quote: %s", err) + } + + // Set additional parameters + order.Quantity = sl.Int(1) + order.PresetId = nil + order.Hardware = make([]datatypes.Hardware, 0, 1) + order.Hardware = append( + order.Hardware, + datatypes.Hardware{ + Hostname: sl.String(d.Get("hostname").(string)), + Domain: sl.String(d.Get("domain").(string)), + }, + ) + hardware := datatypes.Hardware{ + Hostname: sl.String(d.Get("hostname").(string)), + Domain: sl.String(d.Get("domain").(string)), + } + + log.Println("[INFO] Ordering bare metal server") + + _, err = orderService.PlaceOrder(&order, sl.Bool(false)) + if err != nil { + return fmt.Errorf("Error ordering bare metal server: %s", err) + } + + log.Printf("[INFO] Bare Metal Server ID: %s", d.Id()) + + // wait for machine availability + bm, err := waitForBareMetalProvision(&hardware, meta) + if err != nil { + return fmt.Errorf( + "Error waiting for bare metal server (%s) to become ready: %s", d.Id(), err) + } + + id := *bm.(datatypes.Hardware).Id + d.SetId(fmt.Sprintf("%d", id)) + + // Set tags + err = setHardwareTags(id, d, meta) + if err != nil { + return err + } + + // Set notes + if d.Get("notes").(string) != "" { + err = setHardwareNotes(id, d, meta) + if err != nil { + return err + } + } + + return resourceSoftLayerBareMetalRead(d, meta) +} + +func resourceSoftLayerBareMetalQuoteRead(d *schema.ResourceData, meta interface{}) error { + service := services.GetHardwareService(meta.(ProviderConfig).SoftLayerSession()) + + id, err := strconv.Atoi(d.Id()) + if err != nil { + return fmt.Errorf("Not a valid ID, must be an integer: %s", err) + } + + result, err := service.Id(id).Mask( + "hostname,domain," + + "primaryIpAddress,primaryBackendIpAddress,privateNetworkOnlyFlag," + + "notes,userData[value],tagReferences[id,tag[name]]," + + "hourlyBillingFlag," + + "datacenter[id,name,longName]," + + "primaryNetworkComponent[networkVlan[id,primaryRouter,vlanNumber],maxSpeed]," + + "primaryBackendNetworkComponent[networkVlan[id,primaryRouter,vlanNumber],maxSpeed]", + ).GetObject() + + if err != nil { + return fmt.Errorf("Error retrieving bare metal server: %s", err) + } + + d.Set("hostname", *result.Hostname) + d.Set("domain", *result.Domain) + + if result.PrimaryIpAddress != nil { + d.Set("public_ipv4_address", *result.PrimaryIpAddress) + } + d.Set("private_ipv4_address", *result.PrimaryBackendIpAddress) + + if result.PrimaryNetworkComponent.NetworkVlan != nil { + d.Set("public_vlan_id", *result.PrimaryNetworkComponent.NetworkVlan.Id) + } + + if result.PrimaryBackendNetworkComponent.NetworkVlan != nil { + d.Set("private_vlan_id", *result.PrimaryBackendNetworkComponent.NetworkVlan.Id) + } + + userData := result.UserData + if len(userData) > 0 && userData[0].Value != nil { + d.Set("user_metadata", *userData[0].Value) + } + + d.Set("notes", sl.Get(result.Notes, nil)) + + tagReferences := result.TagReferences + tagReferencesLen := len(tagReferences) + if tagReferencesLen > 0 { + tags := make([]string, 0, tagReferencesLen) + for _, tagRef := range tagReferences { + tags = append(tags, *tagRef.Tag.Name) + } + d.Set("tags", tags) + } + + connInfo := map[string]string{"type": "ssh"} + connInfo["host"] = *result.PrimaryBackendIpAddress + d.SetConnInfo(connInfo) + + return nil +} + +func resourceSoftLayerBareMetalQuoteUpdate(d *schema.ResourceData, meta interface{}) error { + id, _ := strconv.Atoi(d.Id()) + + if d.HasChange("tags") { + err := setHardwareTags(id, d, meta) + if err != nil { + return err + } + } + + if d.HasChange("notes") { + err := setHardwareNotes(id, d, meta) + if err != nil { + return err + } + } + + return nil +} + +func resourceSoftLayerBareMetalQuoteDelete(d *schema.ResourceData, meta interface{}) error { + sess := meta.(ProviderConfig).SoftLayerSession() + service := services.GetHardwareService(sess) + + id, err := strconv.Atoi(d.Id()) + if err != nil { + return fmt.Errorf("Not a valid ID, must be an integer: %s", err) + } + + _, err = waitForNoBareMetalActiveTransactions(id, meta) + if err != nil { + return fmt.Errorf("Error deleting bare metal server while waiting for zero active transactions: %s", err) + } + + billingItem, err := service.Id(id).GetBillingItem() + if err != nil { + return fmt.Errorf("Error getting billing item for bare metal server: %s", err) + } + + billingItemService := services.GetBillingItemService(sess) + _, err = billingItemService.Id(*billingItem.Id).CancelItem( + sl.Bool(false), sl.Bool(true), sl.String("No longer required"), sl.String("Please cancel this server"), + ) + if err != nil { + return fmt.Errorf("Error canceling the bare metal server (%d): %s", id, err) + } + + return nil +} + +func resourceSoftLayerBareMetalQuoteExists(d *schema.ResourceData, meta interface{}) (bool, error) { + service := services.GetHardwareService(meta.(ProviderConfig).SoftLayerSession()) + + id, err := strconv.Atoi(d.Id()) + if err != nil { + return false, fmt.Errorf("Not a valid ID, must be an integer: %s", err) + } + + result, err := service.Id(id).GetObject() + if err != nil { + if apiErr, ok := err.(sl.Error); !ok || apiErr.StatusCode != 404 { + return false, fmt.Errorf("Error trying to retrieve the Bare Metal server: %s", err) + } + } + + return err == nil && result.Id != nil && *result.Id == id, nil +} + +// Bare metal creation does not return a bare metal object with an Id. +// Have to wait on provision date to become available on server that matches +// hostname and domain. +// http://sldn.softlayer.com/blog/bpotter/ordering-bare-metal-servers-using-softlayer-api +func waitForBareMetalQuoteProvision(d *datatypes.Hardware, meta interface{}) (interface{}, error) { + hostname := *d.Hostname + domain := *d.Domain + log.Printf("Waiting for server (%s.%s) to have to be provisioned", hostname, domain) + + stateConf := &resource.StateChangeConf{ + Pending: []string{"retry", "pending"}, + Target: []string{"provisioned"}, + Refresh: func() (interface{}, string, error) { + service := services.GetAccountService(meta.(ProviderConfig).SoftLayerSession()) + bms, err := service.Filter( + filter.Build( + filter.Path("hardware.hostname").Eq(hostname), + filter.Path("hardware.domain").Eq(domain), + ), + ).Mask("id,provisionDate").GetHardware() + if err != nil { + return false, "retry", nil + } + + if len(bms) == 0 || bms[0].ProvisionDate == nil { + return datatypes.Hardware{}, "pending", nil + } else { + return bms[0], "provisioned", nil + } + }, + Timeout: 4 * time.Hour, + Delay: 30 * time.Second, + MinTimeout: 2 * time.Minute, + } + + return stateConf.WaitForState() +} + +func waitForNoBareMetalQuoteActiveTransactions(id int, meta interface{}) (interface{}, error) { + log.Printf("Waiting for server (%d) to have zero active transactions", id) + service := services.GetHardwareServerService(meta.(ProviderConfig).SoftLayerSession()) + + stateConf := &resource.StateChangeConf{ + Pending: []string{"retry", "active"}, + Target: []string{"idle"}, + Refresh: func() (interface{}, string, error) { + bm, err := service.Id(id).Mask("id,activeTransactionCount").GetObject() + if err != nil { + return false, "retry", nil + } + + if bm.ActiveTransactionCount != nil && *bm.ActiveTransactionCount == 0 { + return bm, "idle", nil + } else { + return bm, "active", nil + } + }, + Timeout: 4 * time.Hour, + Delay: 5 * time.Second, + MinTimeout: 1 * time.Minute, + } + + return stateConf.WaitForState() +} + +// Depends on ressource_softlayer_bare_metal.go setHardwareTags + +// Depends on ressource_softlayer_bare_metal.go setHardwareNotes diff --git a/softlayer/resource_softlayer_bare_metal_quote_test.go b/softlayer/resource_softlayer_bare_metal_quote_test.go new file mode 100644 index 000000000..57ec459fa --- /dev/null +++ b/softlayer/resource_softlayer_bare_metal_quote_test.go @@ -0,0 +1,138 @@ +package softlayer + +import ( + "errors" + "fmt" + "strconv" + "testing" + + "github.com/hashicorp/terraform/helper/resource" + "github.com/hashicorp/terraform/terraform" + "github.com/softlayer/softlayer-go/datatypes" + "github.com/softlayer/softlayer-go/services" + "github.com/softlayer/softlayer-go/sl" +) + +func TestAccSoftLayerBareMetalQuote_Basic(t *testing.T) { + var bareMetal datatypes.Hardware + + resource.Test(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t) }, + Providers: testAccProviders, + CheckDestroy: testAccCheckSoftLayerBareMetalQuoteDestroy, + Steps: []resource.TestStep{ + { + Config: testAccCheckSoftLayerBareMetalQuoteConfig_basic, + Destroy: false, + Check: resource.ComposeTestCheckFunc( + testAccCheckSoftLayerBareMetalQuoteExists("softlayer_bare_metal_quote.terraform-acceptance-test-1", &bareMetal), + resource.TestCheckResourceAttr( + "softlayer_bare_metal_quote.terraform-acceptance-test-1", "hostname", "terraform-test"), + resource.TestCheckResourceAttr( + "softlayer_bare_metal_quote.terraform-acceptance-test-1", "domain", "bar.example.com"), + resource.TestCheckResourceAttr( + "softlayer_bare_metal_quote.terraform-acceptance-test-1", "user_metadata", "{\"value\":\"newvalue\"}"), + resource.TestCheckResourceAttr( + "softlayer_bare_metal_quote.terraform-acceptance-test-1", "quote_id", "2179879"), + CheckStringSet( + "softlayer_bare_metal_quote.terraform-acceptance-test-1", + "tags", []string{"collectd"}, + ), + ), + }, + + { + Config: testAccCheckSoftLayerBareMetalQuoteConfig_update, + Destroy: false, + Check: resource.ComposeTestCheckFunc( + testAccCheckSoftLayerBareMetalQuoteExists("softlayer_bare_metal_quote.terraform-acceptance-test-1", &bareMetal), + CheckStringSet( + "softlayer_bare_metal_quote.terraform-acceptance-test-1", + "tags", []string{"mesos-master"}, + ), + ), + }, + }, + }) +} + +func testAccCheckSoftLayerBareMetalQuoteDestroy(s *terraform.State) error { + service := services.GetHardwareService(testAccProvider.Meta().(ProviderConfig).SoftLayerSession()) + + for _, rs := range s.RootModule().Resources { + if rs.Type != "softlayer_bare_metal_quote" { + continue + } + + id, _ := strconv.Atoi(rs.Primary.ID) + + // Try to find the bare metal + _, err := service.Id(id).GetObject() + + // Wait + if err != nil { + if apiErr, ok := err.(sl.Error); !ok || apiErr.StatusCode != 404 { + return fmt.Errorf( + "Error waiting for bare metal (%d) to be destroyed: %s", + id, err) + } + } + } + + return nil +} + +func testAccCheckSoftLayerBareMetalQuoteExists(n string, bareMetal *datatypes.Hardware) resource.TestCheckFunc { + return func(s *terraform.State) error { + rs, ok := s.RootModule().Resources[n] + if !ok { + return fmt.Errorf("Not found: %s", n) + } + + if rs.Primary.ID == "" { + return errors.New("No bare metal ID is set") + } + + id, err := strconv.Atoi(rs.Primary.ID) + + if err != nil { + return err + } + + service := services.GetHardwareService(testAccProvider.Meta().(ProviderConfig).SoftLayerSession()) + bm, err := service.Id(id).GetObject() + if err != nil { + return err + } + + fmt.Printf("The ID is %d", *bm.Id) + + if *bm.Id != id { + return errors.New("Bare metal not found") + } + + *bareMetal = bm + + return nil + } +} + +const testAccCheckSoftLayerBareMetalQuoteConfig_basic = ` +resource "softlayer_bare_metal_quote" "terraform-acceptance-test-1" { + hostname = "terraform-test" + domain = "bar.example.com" + user_metadata = "{\"value\":\"newvalue\"}" + quote_id = 2179879 + tags = ["collectd"] +} +` + +const testAccCheckSoftLayerBareMetalQuoteConfig_update = ` +resource "softlayer_bare_metal_quote" "terraform-acceptance-test-1" { + hostname = "terraform-test" + domain = "bar.example.com" + user_metadata = "{\"value\":\"newvalue\"}" + quote_id = 2179879 + tags = ["mesos-master"] +} +`