diff --git a/pixiecore/booters.go b/pixiecore/booters.go index 5c3ee2f..cc860bd 100644 --- a/pixiecore/booters.go +++ b/pixiecore/booters.go @@ -197,6 +197,7 @@ func (b *apibooter) BootSpec(m Machine) (*Spec, error) { if r.Cmdline != nil { switch c := r.Cmdline.(type) { case string: + ret.Cmdline = c case map[string]interface{}: ret.Cmdline, err = b.constructCmdline(c) if err != nil { @@ -208,13 +209,20 @@ func (b *apibooter) BootSpec(m Machine) (*Spec, error) { } f := func(u string) (string, error) { - id, err := signURL(u, &b.key) + urlStr, err := b.makeURLAbsolute(u) + if err != nil { + return "", fmt.Errorf("invalid url %q for cmdline: %s", urlStr, err) + } + id, err := signURL(urlStr, &b.key) if err != nil { return "", err } return fmt.Sprintf("{{ ID %q }}", id), nil } ret.Cmdline, err = expandCmdline(ret.Cmdline, template.FuncMap{"URL": f}) + if err != nil { + return nil, err + } return &ret, nil } @@ -303,10 +311,6 @@ func (b *apibooter) constructCmdline(m map[string]interface{}) (string, error) { if !ok { return "", fmt.Errorf("cmdline key %q has object value with no 'url' attribute", k) } - urlStr, err := b.makeURLAbsolute(urlStr) - if err != nil { - return "", fmt.Errorf("invalid url for cmdline key %q: %s", k, err) - } ret = append(ret, fmt.Sprintf("%s={{ URL %q }}", k, urlStr)) default: return "", fmt.Errorf("unsupported value kind %T for cmdline key %q", m[k], k) diff --git a/pixiecore/booters_test.go b/pixiecore/booters_test.go new file mode 100644 index 0000000..74ddf64 --- /dev/null +++ b/pixiecore/booters_test.go @@ -0,0 +1,196 @@ +// Copyright 2016 Google Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package pixiecore + +import ( + "fmt" + "io" + "io/ioutil" + "log" + "net" + "net/http" + "os" + "path/filepath" + "reflect" + "strings" + "testing" + "time" +) + +func mustMAC(s string) net.HardwareAddr { + m, err := net.ParseMAC(s) + if err != nil { + panic(err) + } + return m +} + +func mustWrite(dir, path, contents string) { + if err := ioutil.WriteFile(filepath.Join(dir, path), []byte(contents), 0644); err != nil { + panic(err) + } +} + +func mustRead(f io.ReadCloser, err error) string { + if err != nil { + panic(err) + } + defer f.Close() + bs, err := ioutil.ReadAll(f) + if err != nil { + panic(err) + } + return string(bs) +} + +func TestStaticBooter(t *testing.T) { + dir, err := ioutil.TempDir("", "pixiecore-static-booter-test") + if err != nil { + log.Fatal(err) + } + defer os.RemoveAll(dir) + mustWrite(dir, "foo", "foo file") + mustWrite(dir, "bar", "bar file") + mustWrite(dir, "baz", "baz file") + mustWrite(dir, "quux", "quux file") + + s := &Spec{ + Kernel: ID(filepath.Join(dir, "foo")), + Initrd: []ID{ + ID(filepath.Join(dir, "bar")), + ID(filepath.Join(dir, "baz")), + }, + Cmdline: fmt.Sprintf(`test={{ ID "%s" }} thing=other`, filepath.Join(dir, "quux")), + Message: "Hello from testing world!", + } + + b, err := StaticBooter(s) + if err != nil { + t.Fatalf("Constructing StaticBooter: %s", err) + } + + m := Machine{ + MAC: mustMAC("01:02:03:04:05:06"), + Arch: ArchIA32, + } + + spec, err := b.BootSpec(m) + if err != nil { + t.Fatalf("Getting bootspec: %s", err) + } + + expected := &Spec{ + Kernel: ID("kernel"), + Initrd: []ID{"initrd-0", "initrd-1"}, + Cmdline: `test={{ ID "other-0" }} thing=other`, + Message: "Hello from testing world!", + } + + if !reflect.DeepEqual(spec, expected) { + t.Fatalf("Expected equal specs, but they differed:\nwant: %#v\ngot: %#v", expected, spec) + } + + // Different machine gets the same spec + m.MAC = mustMAC("02:03:04:05:06:07") + m.Arch = ArchX64 + + spec, err = b.BootSpec(m) + if err != nil { + t.Fatalf("Getting bootspec: %s", err) + } + if !reflect.DeepEqual(spec, expected) { + t.Fatalf("Expected equal specs, but they differed:\nwant: %#v\ngot: %#v", expected, spec) + } + + // Check that the referenced files exist + fs := map[ID]string{ + "kernel": "foo file", + "initrd-0": "bar file", + "initrd-1": "baz file", + "other-0": "quux file", + } + for id, contents := range fs { + v := mustRead(b.ReadBootFile(id)) + if v != contents { + t.Fatalf("Wrong file contents for %q: wanted %q, got %q", id, contents, v) + } + } +} + +func TestAPIBooter(t *testing.T) { + // Set up an HTTP server to act as a (terrible) API server + l, err := net.Listen("tcp", "127.0.0.1:0") + if err != nil { + t.Fatalf("Couldn't get a listener for HTTP: %s", err) + } + + http.HandleFunc("/v1/boot/01:02:03:04:05:06", func(w http.ResponseWriter, r *http.Request) { + w.Write([]byte(`{ + "kernel": "/foo", + "initrd": ["/bar", "/baz"], + "cmdline": "test={{ URL \"/quux\" }} other=thing", + "message": "Hello from test world!" +}`)) + }) + http.HandleFunc("/foo", func(w http.ResponseWriter, r *http.Request) { w.Write([]byte(`foo file`)) }) + http.HandleFunc("/bar", func(w http.ResponseWriter, r *http.Request) { w.Write([]byte(`bar file`)) }) + http.HandleFunc("/baz", func(w http.ResponseWriter, r *http.Request) { w.Write([]byte(`baz file`)) }) + http.HandleFunc("/quux", func(w http.ResponseWriter, r *http.Request) { w.Write([]byte(`quux file`)) }) + go http.Serve(l, nil) + + // Finally, build an APIBooter and test it. + b, err := APIBooter(fmt.Sprintf("http://%s/", l.Addr()), 100*time.Millisecond) + if err != nil { + t.Fatalf("Constructing APIBooter: %s", err) + } + + m := Machine{ + MAC: mustMAC("01:02:03:04:05:06"), + Arch: ArchIA32, + } + + spec, err := b.BootSpec(m) + if err != nil { + t.Fatalf("Getting bootspec: %s", err) + } + + // Unlike StaticBooter, the IDs are variable here because the + // server address isn't deterministic (also we don't make + // rand.Reader deterministic). Let's do as much checking as we + // can, and then just fetch the IDs we got back to check the rest. + if spec.Message != "Hello from test world!" { + t.Fatalf("Wrong message %q", spec.Message) + } + if len(spec.Initrd) != 2 { + t.Fatalf("Wrong number of initrds: %d", len(spec.Initrd)) + } + if !strings.HasPrefix(spec.Cmdline, `test={{ ID "`) || !strings.HasSuffix(spec.Cmdline, `" }} other=thing`) { + t.Fatalf("Wrong cmdline %q", spec.Cmdline) + } + + quuxID := ID(spec.Cmdline[12 : len(spec.Cmdline)-16]) + fs := map[ID]string{ + spec.Kernel: "foo file", + spec.Initrd[0]: "bar file", + spec.Initrd[1]: "baz file", + quuxID: "quux file", + } + for id, contents := range fs { + v := mustRead(b.ReadBootFile(id)) + if v != contents { + t.Fatalf("Wrong file contents for %q: wanted %q, got %q", id, contents, v) + } + } +}