266 lines
7.3 KiB
Go
266 lines
7.3 KiB
Go
package cloudstack
|
|
|
|
import (
|
|
"context"
|
|
"encoding/base64"
|
|
"errors"
|
|
"fmt"
|
|
"net"
|
|
"strings"
|
|
|
|
"github.com/hashicorp/packer-plugin-sdk/multistep"
|
|
packersdk "github.com/hashicorp/packer-plugin-sdk/packer"
|
|
"github.com/hashicorp/packer-plugin-sdk/template/interpolate"
|
|
"github.com/xanzy/go-cloudstack/cloudstack"
|
|
)
|
|
|
|
// userDataTemplateData represents variables for user_data interpolation
|
|
type userDataTemplateData struct {
|
|
HTTPIP string
|
|
HTTPPort int
|
|
}
|
|
|
|
// stepCreateInstance represents a Packer build step that creates CloudStack instances.
|
|
type stepCreateInstance struct {
|
|
Debug bool
|
|
Ctx interpolate.Context
|
|
}
|
|
|
|
// Run executes the Packer build step that creates a CloudStack instance.
|
|
func (s *stepCreateInstance) Run(ctx context.Context, state multistep.StateBag) multistep.StepAction {
|
|
client := state.Get("client").(*cloudstack.CloudStackClient)
|
|
config := state.Get("config").(*Config)
|
|
ui := state.Get("ui").(packersdk.Ui)
|
|
|
|
ui.Say("Creating instance...")
|
|
|
|
// Create a new parameter struct.
|
|
p := client.VirtualMachine.NewDeployVirtualMachineParams(
|
|
config.ServiceOffering,
|
|
state.Get("source").(string),
|
|
config.Zone,
|
|
)
|
|
|
|
// Configure the instance.
|
|
p.SetName(config.InstanceName)
|
|
p.SetDisplayname(config.InstanceDisplayName)
|
|
|
|
if len(config.Comm.SSHKeyPairName) != 0 {
|
|
ui.Message(fmt.Sprintf("Using keypair: %s", config.Comm.SSHKeyPairName))
|
|
p.SetKeypair(config.Comm.SSHKeyPairName)
|
|
}
|
|
|
|
if securitygroups, ok := state.GetOk("security_groups"); ok {
|
|
p.SetSecuritygroupids(securitygroups.([]string))
|
|
}
|
|
|
|
// If we use an ISO, configure the disk offering.
|
|
if config.SourceISO != "" {
|
|
p.SetDiskofferingid(config.DiskOffering)
|
|
p.SetHypervisor(config.Hypervisor)
|
|
}
|
|
|
|
// If we use a template, set the root disk size.
|
|
if config.SourceTemplate != "" && config.DiskSize > 0 {
|
|
p.SetRootdisksize(config.DiskSize)
|
|
}
|
|
|
|
// Retrieve the zone object.
|
|
zone, _, err := client.Zone.GetZoneByID(config.Zone)
|
|
if err != nil {
|
|
err := fmt.Errorf("Failed to get zone %s by ID: %s", config.Zone, err)
|
|
state.Put("error", err)
|
|
ui.Error(err.Error())
|
|
return multistep.ActionHalt
|
|
}
|
|
|
|
if zone.Networktype == "Advanced" {
|
|
// Set the network ID's.
|
|
p.SetNetworkids([]string{config.Network})
|
|
}
|
|
|
|
// If there is a project supplied, set the project id.
|
|
if config.Project != "" {
|
|
p.SetProjectid(config.Project)
|
|
}
|
|
|
|
if config.UserData != "" {
|
|
httpPort := state.Get("http_port").(int)
|
|
httpIP, err := hostIP()
|
|
if err != nil {
|
|
err := fmt.Errorf("Failed to determine host IP: %s", err)
|
|
state.Put("error", err)
|
|
ui.Error(err.Error())
|
|
return multistep.ActionHalt
|
|
}
|
|
state.Put("http_ip", httpIP)
|
|
|
|
s.Ctx.Data = &userDataTemplateData{
|
|
httpIP,
|
|
httpPort,
|
|
}
|
|
|
|
ud, err := s.generateUserData(config.UserData, config.HTTPGetOnly)
|
|
if err != nil {
|
|
state.Put("error", err)
|
|
ui.Error(err.Error())
|
|
return multistep.ActionHalt
|
|
}
|
|
|
|
p.SetUserdata(ud)
|
|
}
|
|
|
|
// Create the new instance.
|
|
instance, err := client.VirtualMachine.DeployVirtualMachine(p)
|
|
if err != nil {
|
|
err := fmt.Errorf("Error creating new instance %s: %s", config.InstanceName, err)
|
|
state.Put("error", err)
|
|
ui.Error(err.Error())
|
|
return multistep.ActionHalt
|
|
}
|
|
|
|
ui.Message("Instance has been created!")
|
|
ui.Message(fmt.Sprintf("Instance ID: %s", instance.Id))
|
|
|
|
// In debug-mode, we output the password
|
|
if s.Debug {
|
|
ui.Message(fmt.Sprintf(
|
|
"Password (since debug is enabled) \"%s\"", instance.Password))
|
|
}
|
|
|
|
// Set the auto generated password if a password was not explicitly configured.
|
|
switch config.Comm.Type {
|
|
case "ssh":
|
|
if config.Comm.SSHPassword == "" {
|
|
config.Comm.SSHPassword = instance.Password
|
|
}
|
|
case "winrm":
|
|
if config.Comm.WinRMPassword == "" {
|
|
config.Comm.WinRMPassword = instance.Password
|
|
}
|
|
}
|
|
|
|
// Set the host address when using the local IP address to connect.
|
|
if config.UseLocalIPAddress {
|
|
state.Put("ipaddress", instance.Nic[0].Ipaddress)
|
|
}
|
|
|
|
// Store the instance ID so we can remove it later.
|
|
state.Put("instance_id", instance.Id)
|
|
|
|
// Set instance tags
|
|
if len(config.Tags) > 0 {
|
|
resourceID := []string{instance.Id}
|
|
tp := client.Resourcetags.NewCreateTagsParams(resourceID, "UserVm", config.Tags)
|
|
|
|
_, err = client.Resourcetags.CreateTags(tp)
|
|
|
|
if err != nil {
|
|
state.Put("error", err)
|
|
ui.Error(err.Error())
|
|
return multistep.ActionHalt
|
|
}
|
|
}
|
|
|
|
return multistep.ActionContinue
|
|
}
|
|
|
|
// Cleanup any resources that may have been created during the Run phase.
|
|
func (s *stepCreateInstance) Cleanup(state multistep.StateBag) {
|
|
client := state.Get("client").(*cloudstack.CloudStackClient)
|
|
config := state.Get("config").(*Config)
|
|
ui := state.Get("ui").(packersdk.Ui)
|
|
|
|
instanceID, ok := state.Get("instance_id").(string)
|
|
if !ok || instanceID == "" {
|
|
return
|
|
}
|
|
|
|
// Create a new parameter struct.
|
|
p := client.VirtualMachine.NewDestroyVirtualMachineParams(instanceID)
|
|
|
|
ui.Say("Deleting instance...")
|
|
if _, err := client.VirtualMachine.DestroyVirtualMachine(p); err != nil {
|
|
// This is a very poor way to be told the ID does no longer exist :(
|
|
if strings.Contains(err.Error(), fmt.Sprintf(
|
|
"Invalid parameter id value=%s due to incorrect long value format, "+
|
|
"or entity does not exist", instanceID)) {
|
|
return
|
|
}
|
|
|
|
ui.Error(fmt.Sprintf("Error destroying instance. Please destroy it manually.\n\n"+
|
|
"\tName: %s\n"+
|
|
"\tError: %s", config.InstanceName, err))
|
|
return
|
|
}
|
|
|
|
// We could expunge the VM while destroying it, but if the user doesn't have
|
|
// rights that single call could error out leaving the VM running. So but
|
|
// splitting these calls we make sure the VM is always deleted, even when the
|
|
// expunge fails.
|
|
if config.Expunge {
|
|
// Create a new parameter struct.
|
|
p := client.VirtualMachine.NewExpungeVirtualMachineParams(instanceID)
|
|
|
|
ui.Say("Expunging instance...")
|
|
if _, err := client.VirtualMachine.ExpungeVirtualMachine(p); err != nil {
|
|
// This is a very poor way to be told the ID does no longer exist :(
|
|
if strings.Contains(err.Error(), fmt.Sprintf(
|
|
"Invalid parameter id value=%s due to incorrect long value format, "+
|
|
"or entity does not exist", instanceID)) {
|
|
return
|
|
}
|
|
|
|
ui.Error(fmt.Sprintf("Error expunging instance. Please expunge it manually.\n\n"+
|
|
"\tName: %s\n"+
|
|
"\tError: %s", config.InstanceName, err))
|
|
return
|
|
}
|
|
}
|
|
|
|
ui.Message("Instance has been deleted!")
|
|
return
|
|
}
|
|
|
|
// generateUserData returns the user data as a base64 encoded string.
|
|
func (s *stepCreateInstance) generateUserData(userData string, httpGETOnly bool) (string, error) {
|
|
renderedUserData, err := interpolate.Render(userData, &s.Ctx)
|
|
if err != nil {
|
|
return "", fmt.Errorf("Error rendering user_data: %s", err)
|
|
}
|
|
|
|
ud := base64.StdEncoding.EncodeToString([]byte(renderedUserData))
|
|
|
|
// DeployVirtualMachine uses POST by default which allows 32K of
|
|
// userdata. If using GET instead the userdata is limited to 2K.
|
|
maxUD := 32768
|
|
if httpGETOnly {
|
|
maxUD = 2048
|
|
}
|
|
|
|
if len(ud) > maxUD {
|
|
return "", fmt.Errorf(
|
|
"The supplied user_data contains %d bytes after encoding, "+
|
|
"this exceeds the limit of %d bytes", len(ud), maxUD)
|
|
}
|
|
|
|
return ud, nil
|
|
}
|
|
|
|
func hostIP() (string, error) {
|
|
addrs, err := net.InterfaceAddrs()
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
|
|
for _, addr := range addrs {
|
|
if ipnet, ok := addr.(*net.IPNet); ok && !ipnet.IP.IsLoopback() {
|
|
if ipnet.IP.To4() != nil {
|
|
return ipnet.IP.String(), nil
|
|
}
|
|
}
|
|
}
|
|
|
|
return "", errors.New("No host IP found")
|
|
}
|