diff --git a/pixiecore/booters.go b/pixiecore/booters.go index da01a43..42bc16c 100644 --- a/pixiecore/booters.go +++ b/pixiecore/booters.go @@ -71,22 +71,31 @@ func (s *staticBooter) BootSpec(m Machine) (*Spec, error) { return s.spec, nil } -func (s *staticBooter) serveFile(path string) (io.ReadCloser, error) { +func (s *staticBooter) serveFile(path string) (io.ReadCloser, int64, error) { if strings.HasPrefix(path, "http://") || strings.HasPrefix(path, "https://") { resp, err := http.Get(path) if err != nil { - return nil, err + return nil, -1, err } if resp.StatusCode != http.StatusOK { resp.Body.Close() - return nil, fmt.Errorf("%s: %s", path, http.StatusText(resp.StatusCode)) + return nil, -1, fmt.Errorf("%s: %s", path, http.StatusText(resp.StatusCode)) } - return resp.Body, nil + return resp.Body, resp.ContentLength, nil } - return os.Open(path) + f, err := os.Open(path) + if err != nil { + return nil, -1, err + } + fi, err := f.Stat() + if err != nil { + f.Close() + return nil, -1, err + } + return f, fi.Size(), nil } -func (s *staticBooter) ReadBootFile(id ID) (io.ReadCloser, error) { +func (s *staticBooter) ReadBootFile(id ID) (io.ReadCloser, int64, error) { path := string(id) switch { case path == "kernel": @@ -95,19 +104,19 @@ func (s *staticBooter) ReadBootFile(id ID) (io.ReadCloser, error) { case strings.HasPrefix(path, "initrd-"): i, err := strconv.Atoi(string(path[7:])) if err != nil || i < 0 || i >= len(s.initrd) { - return nil, fmt.Errorf("no file with ID %q", id) + return nil, -1, fmt.Errorf("no file with ID %q", id) } return s.serveFile(s.initrd[i]) case strings.HasPrefix(path, "other-"): i, err := strconv.Atoi(string(path[6:])) if err != nil || i < 0 || i >= len(s.otherIDs) { - return nil, fmt.Errorf("no file with ID %q", id) + return nil, -1, fmt.Errorf("no file with ID %q", id) } return s.serveFile(s.otherIDs[i]) } - return nil, fmt.Errorf("no file with ID %q", id) + return nil, -1, fmt.Errorf("no file with ID %q", id) } func (s *staticBooter) WriteBootFile(ID, io.Reader) error { @@ -229,18 +238,22 @@ func (b *apibooter) BootSpec(m Machine) (*Spec, error) { return &ret, nil } -func (b *apibooter) ReadBootFile(id ID) (io.ReadCloser, error) { +func (b *apibooter) ReadBootFile(id ID) (io.ReadCloser, int64, error) { urlStr, err := getURL(id, &b.key) if err != nil { - return nil, err + return nil, -1, err } u, err := url.Parse(urlStr) if err != nil { - return nil, fmt.Errorf("%q is not an URL", urlStr) + return nil, -1, fmt.Errorf("%q is not an URL", urlStr) } - var ret io.ReadCloser + var ( + ret io.ReadCloser + sz int64 = -1 + ) if u.Scheme == "file" { + // TODO serveFile ret, err = os.Open(u.Path) } else { // urlStr will get reparsed by http.Get, which is mildly @@ -248,18 +261,18 @@ func (b *apibooter) ReadBootFile(id ID) (io.ReadCloser, error) { // Request. resp, err := http.Get(urlStr) if err != nil { - return nil, err + return nil, -1, err } if resp.StatusCode != 200 { - return nil, fmt.Errorf("GET %q failed: %s", urlStr, resp.Status) + return nil, -1, fmt.Errorf("GET %q failed: %s", urlStr, resp.Status) } - ret, err = resp.Body, nil + ret, sz, err = resp.Body, resp.ContentLength, nil } if err != nil { - return nil, err + return nil, -1, err } - return ret, nil + return ret, sz, nil } func (b *apibooter) WriteBootFile(id ID, body io.Reader) error { diff --git a/pixiecore/booters_test.go b/pixiecore/booters_test.go index 74ddf64..f8a4dfb 100644 --- a/pixiecore/booters_test.go +++ b/pixiecore/booters_test.go @@ -43,7 +43,7 @@ func mustWrite(dir, path, contents string) { } } -func mustRead(f io.ReadCloser, err error) string { +func mustRead(f io.ReadCloser, sz int64, err error) string { if err != nil { panic(err) } @@ -52,6 +52,9 @@ func mustRead(f io.ReadCloser, err error) string { if err != nil { panic(err) } + if sz >= 0 && int64(len(bs)) != sz { + panic(fmt.Errorf("sz = %d, but ReadCloser has %d bytes", sz, len(bs))) + } return string(bs) } diff --git a/pixiecore/cli/cli.go b/pixiecore/cli/cli.go index 5a47a3b..072eeb4 100644 --- a/pixiecore/cli/cli.go +++ b/pixiecore/cli/cli.go @@ -18,7 +18,6 @@ package cli // import "go.universe.tf/netboot/pixiecore/cli" import ( "fmt" "io/ioutil" - "net" "os" "github.com/spf13/cobra" @@ -71,7 +70,7 @@ func todo(msg string, args ...interface{}) { func serverConfigFlags(cmd *cobra.Command) { cmd.Flags().BoolP("debug", "d", false, "Log more things that aren't directly related to booting a recognized client") cmd.Flags().BoolP("log-timestamps", "t", false, "Add a timestamp to each log line") - cmd.Flags().IPP("listen-addr", "l", net.IPv4zero, "IPv4 address to listen on") + cmd.Flags().StringP("listen-addr", "l", "", "IPv4 address to listen on") cmd.Flags().IntP("port", "p", 80, "Port to listen on for HTTP") cmd.Flags().Bool("dhcp-no-bind", false, "Handle DHCP traffic without binding to the DHCP server port") cmd.Flags().String("ipxe-bios", "", "Path to an iPXE binary for BIOS/UNDI") @@ -97,7 +96,7 @@ func serverFromFlags(cmd *cobra.Command) *pixiecore.Server { if err != nil { fatalf("Error reading flag: %s", err) } - addr, err := cmd.Flags().GetIP("listen-addr") + addr, err := cmd.Flags().GetString("listen-addr") if err != nil { fatalf("Error reading flag: %s", err) } @@ -122,9 +121,6 @@ func serverFromFlags(cmd *cobra.Command) *pixiecore.Server { fatalf("Error reading flag: %s", err) } - if addr != nil && addr.To4() == nil { - fatalf("Listen address must be IPv4") - } if httpPort <= 0 { fatalf("HTTP port must be >0") } @@ -154,8 +150,8 @@ func serverFromFlags(cmd *cobra.Command) *pixiecore.Server { if debug { ret.Debug = ret.Log } - if addr != nil { - ret.Address = addr.String() + if addr != "" { + ret.Address = addr } return ret diff --git a/pixiecore/http.go b/pixiecore/http.go index e1730eb..0397447 100644 --- a/pixiecore/http.go +++ b/pixiecore/http.go @@ -108,13 +108,16 @@ func (s *Server) handleFile(w http.ResponseWriter, r *http.Request) { http.Error(w, "missing filename", http.StatusBadRequest) } - f, err := s.Booter.ReadBootFile(ID(name)) + f, sz, err := s.Booter.ReadBootFile(ID(name)) if err != nil { s.log("HTTP", "Error getting file %q (query %q from %s): %s", name, r.URL, r.RemoteAddr, err) http.Error(w, "couldn't get file", http.StatusInternalServerError) return } defer f.Close() + if sz >= 0 { + w.Header().Set("Content-Length", strconv.FormatInt(sz, 10)) + } if _, err = io.Copy(w, f); err != nil { s.log("HTTP", "Copy of %q to %s (query %q) failed: %s", name, r.RemoteAddr, r.URL, err) return diff --git a/pixiecore/http_test.go b/pixiecore/http_test.go index e5e94d9..7b99d05 100644 --- a/pixiecore/http_test.go +++ b/pixiecore/http_test.go @@ -28,9 +28,11 @@ import ( type booterFunc func(Machine) (*Spec, error) -func (b booterFunc) BootSpec(m Machine) (*Spec, error) { return b(m) } -func (b booterFunc) ReadBootFile(id ID) (io.ReadCloser, error) { return nil, errors.New("no") } -func (b booterFunc) WriteBootFile(id ID, r io.Reader) error { return errors.New("no") } +func (b booterFunc) BootSpec(m Machine) (*Spec, error) { return b(m) } +func (b booterFunc) ReadBootFile(id ID) (io.ReadCloser, int64, error) { + return nil, -1, errors.New("no") +} +func (b booterFunc) WriteBootFile(id ID, r io.Reader) error { return errors.New("no") } var logSync sync.Mutex @@ -149,8 +151,9 @@ boot kernel initrd=initrd0 initrd=initrd1 thing=http://localhost:1234/_/file?nam type readBootFile string func (b readBootFile) BootSpec(m Machine) (*Spec, error) { return nil, nil } -func (b readBootFile) ReadBootFile(id ID) (io.ReadCloser, error) { - return ioutil.NopCloser(bytes.NewBuffer([]byte(fmt.Sprintf("%s %s", id, b)))), nil +func (b readBootFile) ReadBootFile(id ID) (io.ReadCloser, int64, error) { + d := fmt.Sprintf("%s %s", id, b) + return ioutil.NopCloser(bytes.NewBuffer([]byte(d))), int64(len(d)), nil } func (b readBootFile) WriteBootFile(id ID, r io.Reader) error { return errors.New("no") } diff --git a/pixiecore/pixiecore.go b/pixiecore/pixiecore.go index 510da8b..854759c 100644 --- a/pixiecore/pixiecore.go +++ b/pixiecore/pixiecore.go @@ -102,7 +102,12 @@ type Booter interface { // the client machine's request. BootSpec(m Machine) (*Spec, error) // Get the bytes corresponding to an ID given in Spec. - ReadBootFile(id ID) (io.ReadCloser, error) + // + // Additionally returns the total number of bytes in the + // ReadCloser, or -1 if the size is unknown. Be warned, returning + // -1 will make the boot process orders of magnitude slower due to + // poor ipxe behavior. + ReadBootFile(id ID) (io.ReadCloser, int64, error) // Write the given Reader to an ID given in Spec. WriteBootFile(id ID, body io.Reader) error } @@ -214,6 +219,8 @@ func (s *Server) Serve() error { // blocking. s.errs = make(chan error, 5) + s.debug("Init", "Starting Pixiecore goroutines") + go func() { s.errs <- s.serveDHCP(dhcp) }() go func() { s.errs <- s.servePXE(pxe) }() go func() { s.errs <- s.serveTFTP(tftp) }()