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  }