diff --git a/builder/cloudstack/builder.go b/builder/cloudstack/builder.go index 8e562354e..f905d4a9e 100644 --- a/builder/cloudstack/builder.go +++ b/builder/cloudstack/builder.go @@ -56,7 +56,14 @@ func (b *Builder) Run(ui packer.Ui, hook packer.Hook, cache packer.Cache) (packe // Build the steps. steps := []multistep.Step{ &stepPrepareConfig{}, - &stepCreateInstance{}, + &common.StepHTTPServer{ + HTTPDir: b.config.HTTPDir, + HTTPPortMin: b.config.HTTPPortMin, + HTTPPortMax: b.config.HTTPPortMax, + }, + &stepCreateInstance{ + Ctx: b.config.ctx, + }, &stepSetupNetworking{}, &communicator.StepConnect{ Config: &b.config.Comm, diff --git a/builder/cloudstack/config.go b/builder/cloudstack/config.go index 5394d3c9c..4f87c06eb 100644 --- a/builder/cloudstack/config.go +++ b/builder/cloudstack/config.go @@ -17,6 +17,7 @@ import ( // Config holds all the details needed to configure the builder. type Config struct { common.PackerConfig `mapstructure:",squash"` + common.HTTPConfig `mapstructure:",squash"` Comm communicator.Config `mapstructure:",squash"` APIURL string `mapstructure:"api_url"` @@ -64,6 +65,11 @@ func NewConfig(raws ...interface{}) (*Config, error) { err := config.Decode(c, &config.DecodeOpts{ Interpolate: true, InterpolateContext: &c.ctx, + InterpolateFilter: &interpolate.RenderFilter{ + Exclude: []string{ + "user_data", + }, + }, }, raws...) if err != nil { return nil, err diff --git a/builder/cloudstack/step_create_instance.go b/builder/cloudstack/step_create_instance.go index c1caa9f36..cf22f3c08 100644 --- a/builder/cloudstack/step_create_instance.go +++ b/builder/cloudstack/step_create_instance.go @@ -2,16 +2,27 @@ package cloudstack import ( "encoding/base64" + "errors" "fmt" + "net" "strings" "github.com/hashicorp/packer/packer" + "github.com/hashicorp/packer/template/interpolate" "github.com/mitchellh/multistep" "github.com/xanzy/go-cloudstack/cloudstack" ) +// userDataTemplateData represents variables for user_data interpolation +type userDataTemplateData struct { + HTTPIP string + HTTPPort uint +} + // stepCreateInstance represents a Packer build step that creates CloudStack instances. -type stepCreateInstance struct{} +type stepCreateInstance struct { + Ctx interpolate.Context +} // Run executes the Packer build step that creates a CloudStack instance. func (s *stepCreateInstance) Run(state multistep.StateBag) multistep.StepAction { @@ -65,7 +76,26 @@ func (s *stepCreateInstance) Run(state multistep.StateBag) multistep.StepAction } if config.UserData != "" { - ud, err := getUserData(config.UserData, config.HTTPGetOnly) + httpPort := state.Get("http_port").(uint) + hostIp, err := hostIP() + if err != nil { + ui.Error(err.Error()) + return multistep.ActionHalt + } + + s.Ctx.Data = &userDataTemplateData{ + hostIp, + httpPort, + } + + renderedUserData, err := interpolate.Render(config.UserData, &s.Ctx) + if err != nil { + err := fmt.Errorf("Error rendering user_data: %s", err) + ui.Error(err.Error()) + return multistep.ActionHalt + } + + ud, err := getUserData(renderedUserData, config.HTTPGetOnly) if err != nil { ui.Error(err.Error()) return multistep.ActionHalt @@ -159,3 +189,20 @@ func getUserData(userData string, httpGETOnly bool) (string, error) { 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") +} diff --git a/website/source/docs/builders/cloudstack.html.md b/website/source/docs/builders/cloudstack.html.md index 8c2d4d3b6..22665cb2f 100644 --- a/website/source/docs/builders/cloudstack.html.md +++ b/website/source/docs/builders/cloudstack.html.md @@ -83,10 +83,24 @@ builder. - `disk_size` (int) - The size (in GB) of the root disk of the new instance. This option is only available when using `source_template`. +- `http_directory` (string) - Path to a directory to serve using an + HTTP server. The files in this directory will be available over HTTP that + will be requestable from the virtual machine. This is useful for hosting + kickstart files and so on. By default this is "", which means no HTTP server + will be started. The address and port of the HTTP server will be available + as variables in `user_data`. This is covered in more detail below. + - `http_get_only` (boolean) - Some cloud providers only allow HTTP GET calls to their CloudStack API. If using such a provider, you need to set this to `true` in order for the provider to only make GET calls and no POST calls. +- `http_port_min` and `http_port_max` (integer) - These are the minimum and + maximum port to use for the HTTP server started to serve the + `http_directory`. Because Packer often runs in parallel, Packer will choose + a randomly available port in this range to run the HTTP server. If you want + to force the HTTP server to be on one port, make this minimum and maximum + port the same. By default the values are 8000 and 9000, respectively. + - `hypervisor` (string) - The target hypervisor (e.g. `XenServer`, `KVM`) for the new template. This option is required when using `source_iso`. @@ -123,6 +137,15 @@ builder. - `use_local_ip_address` (boolean) - Set to `true` to indicate that the provisioners should connect to the local IP address of the instance. +## User Data + +The available variables are: + +- `HTTPIP` and `HTTPPort` - The IP and port, respectively of an HTTP server + that is started serving the directory specified by the `http_directory` + configuration parameter. If `http_directory` isn't specified, these will be + blank! + ## Basic Example Here is a basic example.