From b895f561c87f8d24e04b7997afa0af6d952bb6eb Mon Sep 17 00:00:00 2001 From: Mike Mason Date: Tue, 10 Aug 2021 16:08:50 -0500 Subject: [PATCH] allow custom_ipxe to handle both paths for booting an ipxe script --- cmd/boots/main.go | 1 - installers/{ipxe => custom_ipxe}/errors.go | 2 +- installers/custom_ipxe/ipxe_script_test.go | 2 +- installers/custom_ipxe/main.go | 81 ++++++- installers/custom_ipxe/main_test.go | 235 +++++++++++++++++++ installers/ipxe/main.go | 82 ------- installers/ipxe/main_test.go | 254 --------------------- 7 files changed, 313 insertions(+), 344 deletions(-) rename installers/{ipxe => custom_ipxe}/errors.go (83%) delete mode 100644 installers/ipxe/main.go delete mode 100644 installers/ipxe/main_test.go diff --git a/cmd/boots/main.go b/cmd/boots/main.go index 59350f558..a872c2716 100644 --- a/cmd/boots/main.go +++ b/cmd/boots/main.go @@ -19,7 +19,6 @@ import ( _ "github.com/tinkerbell/boots/installers/coreos" _ "github.com/tinkerbell/boots/installers/custom_ipxe" - _ "github.com/tinkerbell/boots/installers/ipxe" _ "github.com/tinkerbell/boots/installers/nixos" _ "github.com/tinkerbell/boots/installers/osie" _ "github.com/tinkerbell/boots/installers/rancher" diff --git a/installers/ipxe/errors.go b/installers/custom_ipxe/errors.go similarity index 83% rename from installers/ipxe/errors.go rename to installers/custom_ipxe/errors.go index f9779e7d2..e2cd57fa0 100644 --- a/installers/ipxe/errors.go +++ b/installers/custom_ipxe/errors.go @@ -1,4 +1,4 @@ -package ipxe +package custom_ipxe import "errors" diff --git a/installers/custom_ipxe/ipxe_script_test.go b/installers/custom_ipxe/ipxe_script_test.go index b759f7244..4d4a62389 100644 --- a/installers/custom_ipxe/ipxe_script_test.go +++ b/installers/custom_ipxe/ipxe_script_test.go @@ -30,7 +30,7 @@ func TestScript(t *testing.T) { s.Set("tinkerbell", "http://127.0.0.1") s.Set("ipxe_cloud_config", "packet") - bootScript(m.Job(), &s) + ipxeScript(m.Job(), &s) got := string(s.Bytes()) if script != got { t.Fatalf("%s bad iPXE script:\n%v", typ, diff.LineDiff(script, got)) diff --git a/installers/custom_ipxe/main.go b/installers/custom_ipxe/main.go index ab2eab430..aef3722e9 100644 --- a/installers/custom_ipxe/main.go +++ b/installers/custom_ipxe/main.go @@ -1,24 +1,95 @@ package custom_ipxe import ( + "encoding/json" "strings" + "github.com/packethost/pkg/log" + "github.com/tinkerbell/boots/installers" "github.com/tinkerbell/boots/ipxe" "github.com/tinkerbell/boots/job" ) func init() { - job.RegisterSlug("custom_ipxe", bootScript) + job.RegisterInstaller("ipxe", ipxeScript) } -func bootScript(j job.Job, s *ipxe.Script) { +func ipxeScript(j job.Job, s *ipxe.Script) { + logger := installers.Logger("ipxe") + if j.InstanceID() != "" { + logger = logger.With("instance.id", j.InstanceID()) + } + + var cfg *Config + var err error + + if j.OperatingSystem().Installer == "ipxe" { + cfg, err = ipxeConfigFromJob(j) + if err != nil { + s.Echo("Failed to decode installer data") + s.Shell() + logger.Error(err, "decoding installer data") + + return + } + } else { + cfg = &Config{} + + if strings.HasPrefix(j.UserData(), "#!ipxe") { + cfg.Script = j.UserData() + } else { + cfg.Chain = j.IPXEScriptURL() + } + } + + IpxeScriptFromConfig(logger, cfg, j, s) +} + +func IpxeScriptFromConfig(logger log.Logger, cfg *Config, j job.Job, s *ipxe.Script) { + if err := cfg.validate(); err != nil { + s.Echo("Invalid ipxe configuration") + s.Shell() + logger.Error(err, "validating ipxe config") + + return + } + s.PhoneHome("provisioning.104.01") s.Set("packet_facility", j.FacilityCode()) s.Set("packet_plan", j.PlanSlug()) - if strings.HasPrefix(j.UserData(), "#!ipxe") { - s.AppendString(strings.TrimPrefix(j.UserData(), "#!ipxe")) + if cfg.Chain != "" { + s.Chain(cfg.Chain) + } else if cfg.Script != "" { + s.AppendString(strings.TrimPrefix(cfg.Script, "#!ipxe")) } else { - s.Chain(j.IPXEScriptURL()) + s.Echo("Unknown ipxe config path") + s.Shell() } } + +func ipxeConfigFromJob(j job.Job) (*Config, error) { + data := j.OperatingSystem().InstallerData + + cfg := &Config{} + + err := json.NewDecoder(strings.NewReader(data)).Decode(&cfg) + if err != nil { + return nil, err + } + + return cfg, nil +} + +type Config struct { + Chain string `json:"chain,omitempty"` + Script string `json:"script,omitempty"` +} + +func (c *Config) validate() error { + if c.Chain == "" && c.Script == "" { + return ErrEmptyIpxeConfig + } + + return nil +} diff --git a/installers/custom_ipxe/main_test.go b/installers/custom_ipxe/main_test.go index afcd7bdfe..65073cfc3 100644 --- a/installers/custom_ipxe/main_test.go +++ b/installers/custom_ipxe/main_test.go @@ -2,12 +2,20 @@ package custom_ipxe import ( "os" + "regexp" "testing" l "github.com/packethost/pkg/log" + "github.com/stretchr/testify/require" + "github.com/tinkerbell/boots/installers" + "github.com/tinkerbell/boots/ipxe" "github.com/tinkerbell/boots/job" ) +var ( + testLogger l.Logger +) + func TestMain(m *testing.M) { os.Setenv("PACKET_ENV", "test") os.Setenv("PACKET_VERSION", "0") @@ -16,5 +24,232 @@ func TestMain(m *testing.M) { logger, _ := l.Init("github.com/tinkerbell/boots") job.Init(logger) + installers.Init(logger) + testLogger = logger os.Exit(m.Run()) } + +func TestIpxeScript(t *testing.T) { + var testCases = []struct { + name string + installerData string + want string + }{ + { + "invalid config", + "", + `#!ipxe + + echo Failed to decode installer data + shell + `, + }, + { + "valid config", + `{"chain": "http://url/path.ipxe"}`, + `#!ipxe + + + params + param body Device connected to DHCP system + param type provisioning.104.01 + imgfetch ${tinkerbell}/phone-home##params + imgfree + + set packet_facility test.facility + set packet_plan test.slug + chain --autofree http://url/path.ipxe + `, + }, + } + + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + assert := require.New(t) + mockJob := job.NewMock(t, "test.slug", "test.facility") + script := ipxe.NewScript() + + mockJob.SetOSInstaller("ipxe") + mockJob.SetOSInstallerData(tc.installerData) + + ipxeScript(mockJob.Job(), script) + + assert.Equal(dedent(tc.want), string(script.Bytes())) + }) + } +} + +func TestIpxeScriptFromConfig(t *testing.T) { + var testCases = []struct { + name string + config *Config + want string + }{ + { + "invalid config", + &Config{}, + `#!ipxe + + echo Invalid ipxe configuration + shell + `, + }, + { + "valid chain", + &Config{Chain: "http://url/path.ipxe"}, + `#!ipxe + + + params + param body Device connected to DHCP system + param type provisioning.104.01 + imgfetch ${tinkerbell}/phone-home##params + imgfree + + set packet_facility test.facility + set packet_plan test.slug + chain --autofree http://url/path.ipxe + `, + }, + { + "valid script", + &Config{Script: "echo my test script"}, + `#!ipxe + + + params + param body Device connected to DHCP system + param type provisioning.104.01 + imgfetch ${tinkerbell}/phone-home##params + imgfree + + set packet_facility test.facility + set packet_plan test.slug + echo my test script + `, + }, + { + "valid script with header", + &Config{Script: "#!ipxe\necho my test script"}, + `#!ipxe + + + params + param body Device connected to DHCP system + param type provisioning.104.01 + imgfetch ${tinkerbell}/phone-home##params + imgfree + + set packet_facility test.facility + set packet_plan test.slug + + echo my test script + `, + }, + } + + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + assert := require.New(t) + mockJob := job.NewMock(t, "test.slug", "test.facility") + script := ipxe.NewScript() + + IpxeScriptFromConfig(testLogger, tc.config, mockJob.Job(), script) + + assert.Equal(dedent(tc.want), string(script.Bytes())) + }) + } +} + +func TestIpxeConfigFromJob(t *testing.T) { + var testCases = []struct { + name string + installerData string + want *Config + expectError string + }{ + { + "valid chain", + `{"chain": "http://url/path.ipxe"}`, + &Config{Chain: "http://url/path.ipxe"}, + "", + }, + { + "valid script", + `{"script": "echo script"}`, + &Config{Script: "echo script"}, + "", + }, + { + "empty json error", + ``, + nil, + "EOF", + }, + { + "invalid json error", + `{"error"`, + nil, + "unexpected EOF", + }, + } + + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + assert := require.New(t) + + mockJob := job.NewMock(t, "test.slug", "test.facility") + + mockJob.SetOSInstallerData(tc.installerData) + + cfg, err := ipxeConfigFromJob(mockJob.Job()) + + if tc.expectError == "" { + assert.Nil(err) + } else { + assert.EqualError(err, tc.expectError) + } + + assert.Equal(tc.want, cfg) + }) + } +} + +func TestConfigValidate(t *testing.T) { + var testCases = []struct { + name string + chain string + script string + want string + }{ + {"error when empty", "", "", "ipxe config URL or Script must be defined"}, + {"using chain", "http://chain.url/script.ipxe", "", ""}, + {"using script", "", "#!ipxe\necho ipxe script", ""}, + {"using both", "http://path", "ipxe script", ""}, + } + + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + assert := require.New(t) + + cfg := &Config{ + Chain: tc.chain, + Script: tc.script, + } + + got := cfg.validate() + + if tc.want == "" { + assert.Nil(got) + } else { + assert.EqualError(got, tc.want) + } + }) + } +} + +var dedentRegexp = regexp.MustCompile(`(?m)^[^\S\n]+`) + +func dedent(s string) string { + return dedentRegexp.ReplaceAllString(s, "") +} diff --git a/installers/ipxe/main.go b/installers/ipxe/main.go deleted file mode 100644 index 62f8593f4..000000000 --- a/installers/ipxe/main.go +++ /dev/null @@ -1,82 +0,0 @@ -package ipxe - -import ( - "encoding/json" - "strings" - - "github.com/packethost/pkg/log" - "github.com/tinkerbell/boots/installers" - "github.com/tinkerbell/boots/ipxe" - "github.com/tinkerbell/boots/job" -) - -func init() { - job.RegisterInstaller("ipxe", ipxeScript) -} - -func ipxeScript(j job.Job, s *ipxe.Script) { - logger := installers.Logger("ipxe") - if j.InstanceID() != "" { - logger = logger.With("instance.id", j.InstanceID()) - } - - cfg, err := ipxeConfigFromJob(j) - if err != nil { - s.Echo("Failed to decode installer data") - s.Shell() - logger.Error(err, "decoding installer data") - - return - } - - IpxeScriptFromConfig(logger, cfg, j, s) -} - -func IpxeScriptFromConfig(logger log.Logger, cfg *Config, j job.Job, s *ipxe.Script) { - if err := cfg.validate(); err != nil { - s.Echo("Invalid ipxe configuration") - s.Shell() - logger.Error(err, "validating ipxe config") - - return - } - - s.PhoneHome("provisioning.104.01") - s.Set("packet_facility", j.FacilityCode()) - s.Set("packet_plan", j.PlanSlug()) - - if cfg.Chain != "" { - s.Chain(cfg.Chain) - } else if cfg.Script != "" { - s.AppendString(strings.TrimPrefix(cfg.Script, "#!ipxe")) - } else { - s.Echo("Unknown ipxe config path") - s.Shell() - } -} - -func ipxeConfigFromJob(j job.Job) (*Config, error) { - data := j.OperatingSystem().InstallerData - - cfg := &Config{} - - err := json.NewDecoder(strings.NewReader(data)).Decode(&cfg) - if err != nil { - return nil, err - } - - return cfg, nil -} - -type Config struct { - Chain string `json:"chain,omitempty"` - Script string `json:"script,omitempty"` -} - -func (c *Config) validate() error { - if c.Chain == "" && c.Script == "" { - return ErrEmptyIpxeConfig - } - - return nil -} diff --git a/installers/ipxe/main_test.go b/installers/ipxe/main_test.go deleted file mode 100644 index 3e37a9e0c..000000000 --- a/installers/ipxe/main_test.go +++ /dev/null @@ -1,254 +0,0 @@ -package ipxe - -import ( - "os" - "regexp" - "testing" - - l "github.com/packethost/pkg/log" - "github.com/stretchr/testify/require" - "github.com/tinkerbell/boots/installers" - "github.com/tinkerbell/boots/ipxe" - "github.com/tinkerbell/boots/job" -) - -var ( - testLogger l.Logger -) - -func TestMain(m *testing.M) { - os.Setenv("PACKET_ENV", "test") - os.Setenv("PACKET_VERSION", "0") - os.Setenv("ROLLBAR_DISABLE", "1") - os.Setenv("ROLLBAR_TOKEN", "1") - - logger, _ := l.Init("github.com/tinkerbell/boots") - job.Init(logger) - installers.Init(logger) - testLogger = logger - os.Exit(m.Run()) -} - -func TestIpxeScript(t *testing.T) { - var testCases = []struct { - name string - installerData string - want string - }{ - { - "invalid config", - "", - `#!ipxe - - echo Failed to decode installer data - shell - `, - }, - { - "valid config", - `{"chain": "http://url/path.ipxe"}`, - `#!ipxe - - - params - param body Device connected to DHCP system - param type provisioning.104.01 - imgfetch ${tinkerbell}/phone-home##params - imgfree - - set packet_facility test.facility - set packet_plan test.slug - chain --autofree http://url/path.ipxe - `, - }, - } - - for _, tc := range testCases { - t.Run(tc.name, func(t *testing.T) { - assert := require.New(t) - mockJob := job.NewMock(t, "test.slug", "test.facility") - script := ipxe.NewScript() - - mockJob.SetOSInstallerData(tc.installerData) - - ipxeScript(mockJob.Job(), script) - - assert.Equal(dedent(tc.want), string(script.Bytes())) - }) - } -} - -func TestIpxeScriptFromConfig(t *testing.T) { - var testCases = []struct { - name string - config *Config - want string - }{ - { - "invalid config", - &Config{}, - `#!ipxe - - echo Invalid ipxe configuration - shell - `, - }, - { - "valid chain", - &Config{Chain: "http://url/path.ipxe"}, - `#!ipxe - - - params - param body Device connected to DHCP system - param type provisioning.104.01 - imgfetch ${tinkerbell}/phone-home##params - imgfree - - set packet_facility test.facility - set packet_plan test.slug - chain --autofree http://url/path.ipxe - `, - }, - { - "valid script", - &Config{Script: "echo my test script"}, - `#!ipxe - - - params - param body Device connected to DHCP system - param type provisioning.104.01 - imgfetch ${tinkerbell}/phone-home##params - imgfree - - set packet_facility test.facility - set packet_plan test.slug - echo my test script - `, - }, - { - "valid script with header", - &Config{Script: "#!ipxe\necho my test script"}, - `#!ipxe - - - params - param body Device connected to DHCP system - param type provisioning.104.01 - imgfetch ${tinkerbell}/phone-home##params - imgfree - - set packet_facility test.facility - set packet_plan test.slug - - echo my test script - `, - }, - } - - for _, tc := range testCases { - t.Run(tc.name, func(t *testing.T) { - assert := require.New(t) - mockJob := job.NewMock(t, "test.slug", "test.facility") - script := ipxe.NewScript() - - IpxeScriptFromConfig(testLogger, tc.config, mockJob.Job(), script) - - assert.Equal(dedent(tc.want), string(script.Bytes())) - }) - } -} - -func TestIpxeConfigFromJob(t *testing.T) { - var testCases = []struct { - name string - installerData string - want *Config - expectError string - }{ - { - "valid chain", - `{"chain": "http://url/path.ipxe"}`, - &Config{Chain: "http://url/path.ipxe"}, - "", - }, - { - "valid script", - `{"script": "echo script"}`, - &Config{Script: "echo script"}, - "", - }, - { - "empty json error", - ``, - nil, - "EOF", - }, - { - "invalid json error", - `{"error"`, - nil, - "unexpected EOF", - }, - } - - for _, tc := range testCases { - t.Run(tc.name, func(t *testing.T) { - assert := require.New(t) - - mockJob := job.NewMock(t, "test.slug", "test.facility") - - mockJob.SetOSInstallerData(tc.installerData) - - cfg, err := ipxeConfigFromJob(mockJob.Job()) - - if tc.expectError == "" { - assert.Nil(err) - } else { - assert.EqualError(err, tc.expectError) - } - - assert.Equal(tc.want, cfg) - }) - } -} - -func TestConfigValidate(t *testing.T) { - var testCases = []struct { - name string - chain string - script string - want string - }{ - {"error when empty", "", "", "ipxe config URL or Script must be defined"}, - {"using chain", "http://chain.url/script.ipxe", "", ""}, - {"using script", "", "#!ipxe\necho ipxe script", ""}, - {"using both", "http://path", "ipxe script", ""}, - } - - for _, tc := range testCases { - t.Run(tc.name, func(t *testing.T) { - assert := require.New(t) - - cfg := &Config{ - Chain: tc.chain, - Script: tc.script, - } - - got := cfg.validate() - - if tc.want == "" { - assert.Nil(got) - } else { - assert.EqualError(got, tc.want) - } - }) - } -} - -var dedentRegexp = regexp.MustCompile(`(?m)^[^\S\n]+`) - -func dedent(s string) string { - return dedentRegexp.ReplaceAllString(s, "") -}