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