From be36a2159e20bf2be72349b64be9c9b046640ced Mon Sep 17 00:00:00 2001 From: Marin Salinas Date: Mon, 28 Jan 2019 15:24:31 -0600 Subject: [PATCH] feature: add run config to bsusurrogate config struct --- builder/osc/bsusurrogate/builder.go | 1 + builder/osc/common/run_config.go | 218 ++++++++++++++++++++++++ builder/osc/common/run_config_test.go | 232 ++++++++++++++++++++++++++ 3 files changed, 451 insertions(+) create mode 100644 builder/osc/common/run_config.go create mode 100644 builder/osc/common/run_config_test.go diff --git a/builder/osc/bsusurrogate/builder.go b/builder/osc/bsusurrogate/builder.go index 62868b653..94de49b76 100644 --- a/builder/osc/bsusurrogate/builder.go +++ b/builder/osc/bsusurrogate/builder.go @@ -17,6 +17,7 @@ const BuilderId = "digitalonus.osc.bsusurrogate" type Config struct { common.PackerConfig `mapstructure:",squash"` osccommon.AccessConfig `mapstructure:",squash"` + osccommon.RunConfig `mapstructure:",squash"` ctx interpolate.Context } diff --git a/builder/osc/common/run_config.go b/builder/osc/common/run_config.go new file mode 100644 index 000000000..44b64b01e --- /dev/null +++ b/builder/osc/common/run_config.go @@ -0,0 +1,218 @@ +package common + +import ( + "fmt" + "net" + "os" + "regexp" + "strings" + "time" + + "github.com/hashicorp/packer/common/uuid" + "github.com/hashicorp/packer/helper/communicator" + "github.com/hashicorp/packer/template/interpolate" +) + +var reShutdownBehavior = regexp.MustCompile("^(stop|terminate)$") + +type OmiFilterOptions struct { + Filters map[*string]*string + Owners []*string + MostRecent bool `mapstructure:"most_recent"` +} + +func (d *OmiFilterOptions) Empty() bool { + return len(d.Owners) == 0 && len(d.Filters) == 0 +} + +func (d *OmiFilterOptions) NoOwner() bool { + return len(d.Owners) == 0 +} + +type SubnetFilterOptions struct { + Filters map[*string]*string + MostFree bool `mapstructure:"most_free"` + Random bool `mapstructure:"random"` +} + +func (d *SubnetFilterOptions) Empty() bool { + return len(d.Filters) == 0 +} + +type NetFilterOptions struct { + Filters map[*string]*string +} + +func (d *NetFilterOptions) Empty() bool { + return len(d.Filters) == 0 +} + +type SecurityGroupFilterOptions struct { + Filters map[*string]*string +} + +func (d *SecurityGroupFilterOptions) Empty() bool { + return len(d.Filters) == 0 +} + +// RunConfig contains configuration for running an vm from a source +// AMI and details on how to access that launched image. +type RunConfig struct { + AssociatePublicIpAddress bool `mapstructure:"associate_public_ip_address"` + Subregion string `mapstructure:"availability_zone"` + BlockDurationMinutes int64 `mapstructure:"block_duration_minutes"` + DisableStopVm bool `mapstructure:"disable_stop_vm"` + BsuOptimized bool `mapstructure:"ebs_optimized"` + EnableT2Unlimited bool `mapstructure:"enable_t2_unlimited"` + IamVmProfile string `mapstructure:"iam_vm_profile"` + VmInitiatedShutdownBehavior string `mapstructure:"shutdown_behavior"` + VmType string `mapstructure:"vm_type"` + SecurityGroupFilter SecurityGroupFilterOptions `mapstructure:"security_group_filter"` + RunTags map[string]string `mapstructure:"run_tags"` + SecurityGroupId string `mapstructure:"security_group_id"` + SecurityGroupIds []string `mapstructure:"security_group_ids"` + SourceOmi string `mapstructure:"source_omi"` + SourceOmiFilter OmiFilterOptions `mapstructure:"source_omi_filter"` + SpotPrice string `mapstructure:"spot_price"` + SpotPriceAutoProduct string `mapstructure:"spot_price_auto_product"` + SpotTags map[string]string `mapstructure:"spot_tags"` + SubnetFilter SubnetFilterOptions `mapstructure:"subnet_filter"` + SubnetId string `mapstructure:"subnet_id"` + TemporaryKeyPairName string `mapstructure:"temporary_key_pair_name"` + TemporarySGSourceCidr string `mapstructure:"temporary_security_group_source_cidr"` + UserData string `mapstructure:"user_data"` + UserDataFile string `mapstructure:"user_data_file"` + NetFilter NetFilterOptions `mapstructure:"net_filter"` + NetId string `mapstructure:"net_id"` + WindowsPasswordTimeout time.Duration `mapstructure:"windows_password_timeout"` + + // Communicator settings + Comm communicator.Config `mapstructure:",squash"` +} + +func (c *RunConfig) Prepare(ctx *interpolate.Context) []error { + // If we are not given an explicit ssh_keypair_name or + // ssh_private_key_file, then create a temporary one, but only if the + // temporary_key_pair_name has not been provided and we are not using + // ssh_password. + if c.Comm.SSHKeyPairName == "" && c.Comm.SSHTemporaryKeyPairName == "" && + c.Comm.SSHPrivateKeyFile == "" && c.Comm.SSHPassword == "" { + + c.Comm.SSHTemporaryKeyPairName = fmt.Sprintf("packer_%s", uuid.TimeOrderedUUID()) + } + + if c.WindowsPasswordTimeout == 0 { + c.WindowsPasswordTimeout = 20 * time.Minute + } + + if c.RunTags == nil { + c.RunTags = make(map[string]string) + } + + // Validation + errs := c.Comm.Prepare(ctx) + + // Validating ssh_interface + if c.Comm.SSHInterface != "public_ip" && + c.Comm.SSHInterface != "private_ip" && + c.Comm.SSHInterface != "public_dns" && + c.Comm.SSHInterface != "private_dns" && + c.Comm.SSHInterface != "" { + errs = append(errs, fmt.Errorf("Unknown interface type: %s", c.Comm.SSHInterface)) + } + + if c.Comm.SSHKeyPairName != "" { + if c.Comm.Type == "winrm" && c.Comm.WinRMPassword == "" && c.Comm.SSHPrivateKeyFile == "" { + errs = append(errs, fmt.Errorf("ssh_private_key_file must be provided to retrieve the winrm password when using ssh_keypair_name.")) + } else if c.Comm.SSHPrivateKeyFile == "" && !c.Comm.SSHAgentAuth { + errs = append(errs, fmt.Errorf("ssh_private_key_file must be provided or ssh_agent_auth enabled when ssh_keypair_name is specified.")) + } + } + + if c.SourceOmi == "" && c.SourceOmiFilter.Empty() { + errs = append(errs, fmt.Errorf("A source_omi or source_omi_filter must be specified")) + } + + if c.SourceOmi == "" && c.SourceOmiFilter.NoOwner() { + errs = append(errs, fmt.Errorf("For security reasons, your source AMI filter must declare an owner.")) + } + + if c.VmType == "" { + errs = append(errs, fmt.Errorf("An vm_type must be specified")) + } + + if c.BlockDurationMinutes%60 != 0 { + errs = append(errs, fmt.Errorf( + "block_duration_minutes must be multiple of 60")) + } + + if c.SpotPrice == "auto" { + if c.SpotPriceAutoProduct == "" { + errs = append(errs, fmt.Errorf( + "spot_price_auto_product must be specified when spot_price is auto")) + } + } + + if c.SpotPriceAutoProduct != "" { + if c.SpotPrice != "auto" { + errs = append(errs, fmt.Errorf( + "spot_price should be set to auto when spot_price_auto_product is specified")) + } + } + + if c.SpotTags != nil { + if c.SpotPrice == "" || c.SpotPrice == "0" { + errs = append(errs, fmt.Errorf( + "spot_tags should not be set when not requesting a spot vm")) + } + } + + if c.UserData != "" && c.UserDataFile != "" { + errs = append(errs, fmt.Errorf("Only one of user_data or user_data_file can be specified.")) + } else if c.UserDataFile != "" { + if _, err := os.Stat(c.UserDataFile); err != nil { + errs = append(errs, fmt.Errorf("user_data_file not found: %s", c.UserDataFile)) + } + } + + if c.SecurityGroupId != "" { + if len(c.SecurityGroupIds) > 0 { + errs = append(errs, fmt.Errorf("Only one of security_group_id or security_group_ids can be specified.")) + } else { + c.SecurityGroupIds = []string{c.SecurityGroupId} + c.SecurityGroupId = "" + } + } + + if c.TemporarySGSourceCidr == "" { + c.TemporarySGSourceCidr = "0.0.0.0/0" + } else { + if _, _, err := net.ParseCIDR(c.TemporarySGSourceCidr); err != nil { + errs = append(errs, fmt.Errorf("Error parsing temporary_security_group_source_cidr: %s", err.Error())) + } + } + + if c.VmInitiatedShutdownBehavior == "" { + c.VmInitiatedShutdownBehavior = "stop" + } else if !reShutdownBehavior.MatchString(c.VmInitiatedShutdownBehavior) { + errs = append(errs, fmt.Errorf("shutdown_behavior only accepts 'stop' or 'terminate' values.")) + } + + if c.EnableT2Unlimited { + if c.SpotPrice != "" { + errs = append(errs, fmt.Errorf("Error: T2 Unlimited cannot be used in conjuction with Spot Vms")) + } + firstDotIndex := strings.Index(c.VmType, ".") + if firstDotIndex == -1 { + errs = append(errs, fmt.Errorf("Error determining main Vm Type from: %s", c.VmType)) + } else if c.VmType[0:firstDotIndex] != "t2" { + errs = append(errs, fmt.Errorf("Error: T2 Unlimited enabled with a non-T2 Vm Type: %s", c.VmType)) + } + } + + return errs +} + +func (c *RunConfig) IsSpotVm() bool { + return c.SpotPrice != "" && c.SpotPrice != "0" +} diff --git a/builder/osc/common/run_config_test.go b/builder/osc/common/run_config_test.go new file mode 100644 index 000000000..5bdc977b7 --- /dev/null +++ b/builder/osc/common/run_config_test.go @@ -0,0 +1,232 @@ +package common + +import ( + "io/ioutil" + "os" + "regexp" + "testing" + + "github.com/hashicorp/packer/helper/communicator" +) + +func init() { + // Clear out the OUTSCALE access key env vars so they don't + // affect our tests. + os.Setenv("OUTSCALE_ACCESS_KEY_ID", "") + os.Setenv("OUTSCALE_ACCESS_KEY", "") + os.Setenv("OUTSCALE_SECRET_ACCESS_KEY", "") + os.Setenv("OUTSCALE_SECRET_KEY", "") +} + +func testConfig() *RunConfig { + return &RunConfig{ + SourceOmi: "abcd", + VmType: "m1.small", + + Comm: communicator.Config{ + SSHUsername: "foo", + }, + } +} + +func testConfigFilter() *RunConfig { + config := testConfig() + config.SourceOmi = "" + config.SourceOmiFilter = OmiFilterOptions{} + return config +} + +func TestRunConfigPrepare(t *testing.T) { + c := testConfig() + err := c.Prepare(nil) + if len(err) > 0 { + t.Fatalf("err: %s", err) + } +} + +func TestRunConfigPrepare_VmType(t *testing.T) { + c := testConfig() + c.VmType = "" + if err := c.Prepare(nil); len(err) != 1 { + t.Fatalf("Should error if an instance_type is not specified") + } +} + +func TestRunConfigPrepare_SourceOmi(t *testing.T) { + c := testConfig() + c.SourceOmi = "" + if err := c.Prepare(nil); len(err) != 2 { + t.Fatalf("Should error if a source_ami (or source_ami_filter) is not specified") + } +} + +func TestRunConfigPrepare_SourceOmiFilterBlank(t *testing.T) { + c := testConfigFilter() + if err := c.Prepare(nil); len(err) != 2 { + t.Fatalf("Should error if source_ami_filter is empty or not specified (and source_ami is not specified)") + } +} + +func TestRunConfigPrepare_SourceOmiFilterOwnersBlank(t *testing.T) { + c := testConfigFilter() + filter_key := "name" + filter_value := "foo" + c.SourceOmiFilter = OmiFilterOptions{Filters: map[*string]*string{&filter_key: &filter_value}} + if err := c.Prepare(nil); len(err) != 1 { + t.Fatalf("Should error if Owners is not specified)") + } +} + +func TestRunConfigPrepare_SourceOmiFilterGood(t *testing.T) { + c := testConfigFilter() + owner := "123" + filter_key := "name" + filter_value := "foo" + goodFilter := OmiFilterOptions{Owners: []*string{&owner}, Filters: map[*string]*string{&filter_key: &filter_value}} + c.SourceOmiFilter = goodFilter + if err := c.Prepare(nil); len(err) != 0 { + t.Fatalf("err: %s", err) + } +} + +func TestRunConfigPrepare_EnableT2UnlimitedGood(t *testing.T) { + c := testConfig() + // Must have a T2 instance type if T2 Unlimited is enabled + c.VmType = "t2.micro" + c.EnableT2Unlimited = true + err := c.Prepare(nil) + if len(err) > 0 { + t.Fatalf("err: %s", err) + } +} + +func TestRunConfigPrepare_EnableT2UnlimitedBadVmType(t *testing.T) { + c := testConfig() + // T2 Unlimited cannot be used with instance types other than T2 + c.VmType = "m5.large" + c.EnableT2Unlimited = true + err := c.Prepare(nil) + if len(err) != 1 { + t.Fatalf("Should error if T2 Unlimited is enabled with non-T2 instance_type") + } +} + +func TestRunConfigPrepare_EnableT2UnlimitedBadWithSpotInstanceRequest(t *testing.T) { + c := testConfig() + // T2 Unlimited cannot be used with Spot Instances + c.VmType = "t2.micro" + c.EnableT2Unlimited = true + c.SpotPrice = "auto" + c.SpotPriceAutoProduct = "Linux/UNIX" + err := c.Prepare(nil) + if len(err) != 1 { + t.Fatalf("Should error if T2 Unlimited has been used in conjuntion with a Spot Price request") + } +} + +func TestRunConfigPrepare_SpotAuto(t *testing.T) { + c := testConfig() + c.SpotPrice = "auto" + if err := c.Prepare(nil); len(err) != 1 { + t.Fatalf("Should error if spot_price_auto_product is not set and spot_price is set to auto") + } + + // Good - SpotPrice and SpotPriceAutoProduct are correctly set + c.SpotPriceAutoProduct = "foo" + if err := c.Prepare(nil); len(err) != 0 { + t.Fatalf("err: %s", err) + } + + c.SpotPrice = "" + if err := c.Prepare(nil); len(err) != 1 { + t.Fatalf("Should error if spot_price is not set to auto and spot_price_auto_product is set") + } +} + +func TestRunConfigPrepare_SSHPort(t *testing.T) { + c := testConfig() + c.Comm.SSHPort = 0 + if err := c.Prepare(nil); len(err) != 0 { + t.Fatalf("err: %s", err) + } + + if c.Comm.SSHPort != 22 { + t.Fatalf("invalid value: %d", c.Comm.SSHPort) + } + + c.Comm.SSHPort = 44 + if err := c.Prepare(nil); len(err) != 0 { + t.Fatalf("err: %s", err) + } + + if c.Comm.SSHPort != 44 { + t.Fatalf("invalid value: %d", c.Comm.SSHPort) + } +} + +func TestRunConfigPrepare_UserData(t *testing.T) { + c := testConfig() + tf, err := ioutil.TempFile("", "packer") + if err != nil { + t.Fatalf("err: %s", err) + } + defer os.Remove(tf.Name()) + defer tf.Close() + + c.UserData = "foo" + c.UserDataFile = tf.Name() + if err := c.Prepare(nil); len(err) != 1 { + t.Fatalf("Should error if user_data string and user_data_file have both been specified") + } +} + +func TestRunConfigPrepare_UserDataFile(t *testing.T) { + c := testConfig() + if err := c.Prepare(nil); len(err) != 0 { + t.Fatalf("err: %s", err) + } + + c.UserDataFile = "idontexistidontthink" + if err := c.Prepare(nil); len(err) != 1 { + t.Fatalf("Should error if the file specified by user_data_file does not exist") + } + + tf, err := ioutil.TempFile("", "packer") + if err != nil { + t.Fatalf("err: %s", err) + } + defer os.Remove(tf.Name()) + defer tf.Close() + + c.UserDataFile = tf.Name() + if err := c.Prepare(nil); len(err) != 0 { + t.Fatalf("err: %s", err) + } +} + +func TestRunConfigPrepare_TemporaryKeyPairName(t *testing.T) { + c := testConfig() + c.Comm.SSHTemporaryKeyPairName = "" + if err := c.Prepare(nil); len(err) != 0 { + t.Fatalf("err: %s", err) + } + + if c.Comm.SSHTemporaryKeyPairName == "" { + t.Fatal("keypair name is empty") + } + + // Match prefix and UUID, e.g. "packer_5790d491-a0b8-c84c-c9d2-2aea55086550". + r := regexp.MustCompile(`\Apacker_(?:(?i)[a-f\d]{8}(?:-[a-f\d]{4}){3}-[a-f\d]{12}?)\z`) + if !r.MatchString(c.Comm.SSHTemporaryKeyPairName) { + t.Fatal("keypair name is not valid") + } + + c.Comm.SSHTemporaryKeyPairName = "ssh-key-123" + if err := c.Prepare(nil); len(err) != 0 { + t.Fatalf("err: %s", err) + } + + if c.Comm.SSHTemporaryKeyPairName != "ssh-key-123" { + t.Fatal("keypair name does not match") + } +}