github.com/StackPointCloud/packer@v0.10.2-0.20180716202532-b28098e0f79b/builder/cloudstack/step_create_instance.go (about) 1 package cloudstack 2 3 import ( 4 "context" 5 "encoding/base64" 6 "errors" 7 "fmt" 8 "net" 9 "strings" 10 11 "github.com/hashicorp/packer/common" 12 "github.com/hashicorp/packer/helper/multistep" 13 "github.com/hashicorp/packer/packer" 14 "github.com/hashicorp/packer/template/interpolate" 15 "github.com/xanzy/go-cloudstack/cloudstack" 16 ) 17 18 // userDataTemplateData represents variables for user_data interpolation 19 type userDataTemplateData struct { 20 HTTPIP string 21 HTTPPort uint 22 } 23 24 // stepCreateInstance represents a Packer build step that creates CloudStack instances. 25 type stepCreateInstance struct { 26 Debug bool 27 Ctx interpolate.Context 28 } 29 30 // Run executes the Packer build step that creates a CloudStack instance. 31 func (s *stepCreateInstance) Run(_ context.Context, state multistep.StateBag) multistep.StepAction { 32 client := state.Get("client").(*cloudstack.CloudStackClient) 33 config := state.Get("config").(*Config) 34 ui := state.Get("ui").(packer.Ui) 35 36 ui.Say("Creating instance...") 37 38 // Create a new parameter struct. 39 p := client.VirtualMachine.NewDeployVirtualMachineParams( 40 config.ServiceOffering, 41 state.Get("source").(string), 42 config.Zone, 43 ) 44 45 // Configure the instance. 46 p.SetName(config.InstanceName) 47 p.SetDisplayname("Created by Packer") 48 49 if keypair, ok := state.GetOk("keypair"); ok { 50 kp := keypair.(string) 51 ui.Message(fmt.Sprintf("Using keypair: %s", kp)) 52 p.SetKeypair(kp) 53 } 54 55 if securitygroups, ok := state.GetOk("security_groups"); ok { 56 p.SetSecuritygroupids(securitygroups.([]string)) 57 } 58 59 // If we use an ISO, configure the disk offering. 60 if config.SourceISO != "" { 61 p.SetDiskofferingid(config.DiskOffering) 62 p.SetHypervisor(config.Hypervisor) 63 } 64 65 // If we use a template, set the root disk size. 66 if config.SourceTemplate != "" && config.DiskSize > 0 { 67 p.SetRootdisksize(config.DiskSize) 68 } 69 70 // Retrieve the zone object. 71 zone, _, err := client.Zone.GetZoneByID(config.Zone) 72 if err != nil { 73 err := fmt.Errorf("Failed to get zone %s by ID: %s", config.Zone, err) 74 state.Put("error", err) 75 ui.Error(err.Error()) 76 return multistep.ActionHalt 77 } 78 79 if zone.Networktype == "Advanced" { 80 // Set the network ID's. 81 p.SetNetworkids([]string{config.Network}) 82 } 83 84 // If there is a project supplied, set the project id. 85 if config.Project != "" { 86 p.SetProjectid(config.Project) 87 } 88 89 if config.UserData != "" { 90 httpPort := state.Get("http_port").(uint) 91 httpIP, err := hostIP() 92 if err != nil { 93 err := fmt.Errorf("Failed to determine host IP: %s", err) 94 state.Put("error", err) 95 ui.Error(err.Error()) 96 return multistep.ActionHalt 97 } 98 common.SetHTTPIP(httpIP) 99 100 s.Ctx.Data = &userDataTemplateData{ 101 httpIP, 102 httpPort, 103 } 104 105 ud, err := s.generateUserData(config.UserData, config.HTTPGetOnly) 106 if err != nil { 107 state.Put("error", err) 108 ui.Error(err.Error()) 109 return multistep.ActionHalt 110 } 111 112 p.SetUserdata(ud) 113 } 114 115 // Create the new instance. 116 instance, err := client.VirtualMachine.DeployVirtualMachine(p) 117 if err != nil { 118 err := fmt.Errorf("Error creating new instance %s: %s", config.InstanceName, err) 119 state.Put("error", err) 120 ui.Error(err.Error()) 121 return multistep.ActionHalt 122 } 123 124 ui.Message("Instance has been created!") 125 ui.Message(fmt.Sprintf("Instance ID: %s", instance.Id)) 126 127 // In debug-mode, we output the password 128 if s.Debug { 129 ui.Message(fmt.Sprintf( 130 "Password (since debug is enabled) \"%s\"", instance.Password)) 131 } 132 133 // Set the auto generated password if a password was not explicitly configured. 134 switch config.Comm.Type { 135 case "ssh": 136 if config.Comm.SSHPassword == "" { 137 config.Comm.SSHPassword = instance.Password 138 } 139 case "winrm": 140 if config.Comm.WinRMPassword == "" { 141 config.Comm.WinRMPassword = instance.Password 142 } 143 } 144 145 // Set the host address when using the local IP address to connect. 146 if config.UseLocalIPAddress { 147 state.Put("ipaddress", instance.Nic[0].Ipaddress) 148 } 149 150 // Store the instance ID so we can remove it later. 151 state.Put("instance_id", instance.Id) 152 153 return multistep.ActionContinue 154 } 155 156 // Cleanup any resources that may have been created during the Run phase. 157 func (s *stepCreateInstance) Cleanup(state multistep.StateBag) { 158 client := state.Get("client").(*cloudstack.CloudStackClient) 159 config := state.Get("config").(*Config) 160 ui := state.Get("ui").(packer.Ui) 161 162 instanceID, ok := state.Get("instance_id").(string) 163 if !ok || instanceID == "" { 164 return 165 } 166 167 // Create a new parameter struct. 168 p := client.VirtualMachine.NewDestroyVirtualMachineParams(instanceID) 169 170 ui.Say("Deleting instance...") 171 if _, err := client.VirtualMachine.DestroyVirtualMachine(p); err != nil { 172 // This is a very poor way to be told the ID does no longer exist :( 173 if strings.Contains(err.Error(), fmt.Sprintf( 174 "Invalid parameter id value=%s due to incorrect long value format, "+ 175 "or entity does not exist", instanceID)) { 176 return 177 } 178 179 ui.Error(fmt.Sprintf("Error destroying instance. Please destroy it manually.\n\n"+ 180 "\tName: %s\n"+ 181 "\tError: %s", config.InstanceName, err)) 182 return 183 } 184 185 // We could expunge the VM while destroying it, but if the user doesn't have 186 // rights that single call could error out leaving the VM running. So but 187 // splitting these calls we make sure the VM is always deleted, even when the 188 // expunge fails. 189 if config.Expunge { 190 // Create a new parameter struct. 191 p := client.VirtualMachine.NewExpungeVirtualMachineParams(instanceID) 192 193 ui.Say("Expunging instance...") 194 if _, err := client.VirtualMachine.ExpungeVirtualMachine(p); err != nil { 195 // This is a very poor way to be told the ID does no longer exist :( 196 if strings.Contains(err.Error(), fmt.Sprintf( 197 "Invalid parameter id value=%s due to incorrect long value format, "+ 198 "or entity does not exist", instanceID)) { 199 return 200 } 201 202 ui.Error(fmt.Sprintf("Error expunging instance. Please expunge it manually.\n\n"+ 203 "\tName: %s\n"+ 204 "\tError: %s", config.InstanceName, err)) 205 return 206 } 207 } 208 209 ui.Message("Instance has been deleted!") 210 return 211 } 212 213 // generateUserData returns the user data as a base64 encoded string. 214 func (s *stepCreateInstance) generateUserData(userData string, httpGETOnly bool) (string, error) { 215 renderedUserData, err := interpolate.Render(userData, &s.Ctx) 216 if err != nil { 217 return "", fmt.Errorf("Error rendering user_data: %s", err) 218 } 219 220 ud := base64.StdEncoding.EncodeToString([]byte(renderedUserData)) 221 222 // DeployVirtualMachine uses POST by default which allows 32K of 223 // userdata. If using GET instead the userdata is limited to 2K. 224 maxUD := 32768 225 if httpGETOnly { 226 maxUD = 2048 227 } 228 229 if len(ud) > maxUD { 230 return "", fmt.Errorf( 231 "The supplied user_data contains %d bytes after encoding, "+ 232 "this exceeds the limit of %d bytes", len(ud), maxUD) 233 } 234 235 return ud, nil 236 } 237 238 func hostIP() (string, error) { 239 addrs, err := net.InterfaceAddrs() 240 if err != nil { 241 return "", err 242 } 243 244 for _, addr := range addrs { 245 if ipnet, ok := addr.(*net.IPNet); ok && !ipnet.IP.IsLoopback() { 246 if ipnet.IP.To4() != nil { 247 return ipnet.IP.String(), nil 248 } 249 } 250 } 251 252 return "", errors.New("No host IP found") 253 }