mirror of
				https://github.com/danderson/netboot.git
				synced 2025-11-04 02:11:20 +01:00 
			
		
		
		
	The feature is heavily laced with warnings and a complete lack of promises around future compatibility. But within those parameters, it might allow some people to do cool things. Fixes #60.
		
			
				
	
	
		
			222 lines
		
	
	
		
			7.2 KiB
		
	
	
	
		
			Go
		
	
	
	
	
	
			
		
		
	
	
			222 lines
		
	
	
		
			7.2 KiB
		
	
	
	
		
			Go
		
	
	
	
	
	
// 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 (
 | 
						|
	"bytes"
 | 
						|
	"errors"
 | 
						|
	"fmt"
 | 
						|
	"io"
 | 
						|
	"net"
 | 
						|
	"net/http"
 | 
						|
	"net/url"
 | 
						|
	"strconv"
 | 
						|
	"text/template"
 | 
						|
	"time"
 | 
						|
)
 | 
						|
 | 
						|
func serveHTTP(l net.Listener, handlers ...func(*http.ServeMux)) error {
 | 
						|
	mux := http.NewServeMux()
 | 
						|
	for _, h := range handlers {
 | 
						|
		h(mux)
 | 
						|
	}
 | 
						|
	if err := http.Serve(l, mux); err != nil {
 | 
						|
		return fmt.Errorf("HTTP server shut down: %s", err)
 | 
						|
	}
 | 
						|
	return nil
 | 
						|
}
 | 
						|
 | 
						|
func (s *Server) serveHTTP(mux *http.ServeMux) {
 | 
						|
	mux.HandleFunc("/_/ipxe", s.handleIpxe)
 | 
						|
	mux.HandleFunc("/_/file", s.handleFile)
 | 
						|
	mux.HandleFunc("/_/booting", s.handleBooting)
 | 
						|
}
 | 
						|
 | 
						|
func (s *Server) handleIpxe(w http.ResponseWriter, r *http.Request) {
 | 
						|
	overallStart := time.Now()
 | 
						|
	macStr := r.URL.Query().Get("mac")
 | 
						|
	if macStr == "" {
 | 
						|
		s.debug("HTTP", "Bad request %q from %s, missing MAC address", r.URL, r.RemoteAddr)
 | 
						|
		http.Error(w, "missing MAC address parameter", http.StatusBadRequest)
 | 
						|
		return
 | 
						|
	}
 | 
						|
	archStr := r.URL.Query().Get("arch")
 | 
						|
	if archStr == "" {
 | 
						|
		s.debug("HTTP", "Bad request %q from %s, missing architecture", r.URL, r.RemoteAddr)
 | 
						|
		http.Error(w, "missing architecture parameter", http.StatusBadRequest)
 | 
						|
		return
 | 
						|
	}
 | 
						|
 | 
						|
	mac, err := net.ParseMAC(macStr)
 | 
						|
	if err != nil {
 | 
						|
		s.debug("HTTP", "Bad request %q from %s, invalid MAC address %q (%s)", r.URL, r.RemoteAddr, macStr, err)
 | 
						|
		http.Error(w, "invalid MAC address", http.StatusBadRequest)
 | 
						|
		return
 | 
						|
	}
 | 
						|
 | 
						|
	i, err := strconv.Atoi(archStr)
 | 
						|
	if err != nil {
 | 
						|
		s.debug("HTTP", "Bad request %q from %s, invalid architecture %q (%s)", r.URL, r.RemoteAddr, archStr, err)
 | 
						|
		http.Error(w, "invalid architecture", http.StatusBadRequest)
 | 
						|
		return
 | 
						|
	}
 | 
						|
	arch := Architecture(i)
 | 
						|
	switch arch {
 | 
						|
	case ArchIA32, ArchX64:
 | 
						|
	default:
 | 
						|
		s.debug("HTTP", "Bad request %q from %s, unknown architecture %q", r.URL, r.RemoteAddr, arch)
 | 
						|
		http.Error(w, "unknown architecture", http.StatusBadRequest)
 | 
						|
		return
 | 
						|
	}
 | 
						|
 | 
						|
	mach := Machine{
 | 
						|
		MAC:  mac,
 | 
						|
		Arch: arch,
 | 
						|
	}
 | 
						|
	start := time.Now()
 | 
						|
	spec, err := s.Booter.BootSpec(mach)
 | 
						|
	s.debug("HTTP", "Get bootspec for %s took %s", mac, time.Since(start))
 | 
						|
	if err != nil {
 | 
						|
		s.log("HTTP", "Couldn't get a bootspec for %s (query %q from %s): %s", mac, r.URL, r.RemoteAddr, err)
 | 
						|
		http.Error(w, "couldn't get a bootspec", http.StatusInternalServerError)
 | 
						|
		return
 | 
						|
	}
 | 
						|
	if spec == nil {
 | 
						|
		// TODO: make ipxe abort netbooting so it can fall through to
 | 
						|
		// other boot options - unsure if that's possible.
 | 
						|
		s.debug("HTTP", "No boot spec for %s (query %q from %s), ignoring boot request", mac, r.URL, r.RemoteAddr)
 | 
						|
		http.Error(w, "you don't netboot", http.StatusNotFound)
 | 
						|
		return
 | 
						|
	}
 | 
						|
	start = time.Now()
 | 
						|
	script, err := ipxeScript(mach, spec, r.Host)
 | 
						|
	s.debug("HTTP", "Construct ipxe script for %s took %s", mac, time.Since(start))
 | 
						|
	if err != nil {
 | 
						|
		s.log("HTTP", "Failed to assemble ipxe script for %s (query %q from %s): %s", mac, r.URL, r.RemoteAddr, err)
 | 
						|
		http.Error(w, "couldn't get a boot script", http.StatusInternalServerError)
 | 
						|
		return
 | 
						|
	}
 | 
						|
 | 
						|
	s.log("HTTP", "Sending ipxe boot script to %s", r.RemoteAddr)
 | 
						|
	start = time.Now()
 | 
						|
	s.machineEvent(mac, machineStateIpxeScript, "Sent iPXE boot script")
 | 
						|
	w.Header().Set("Content-Type", "text/plain")
 | 
						|
	w.Write(script)
 | 
						|
	s.debug("HTTP", "Writing ipxe script to %s took %s", mac, time.Since(start))
 | 
						|
	s.debug("HTTP", "handleIpxe for %s took %s", mac, time.Since(overallStart))
 | 
						|
}
 | 
						|
 | 
						|
func (s *Server) handleFile(w http.ResponseWriter, r *http.Request) {
 | 
						|
	name := r.URL.Query().Get("name")
 | 
						|
	if name == "" {
 | 
						|
		s.debug("HTTP", "Bad request %q from %s, missing filename", r.URL, r.RemoteAddr)
 | 
						|
		http.Error(w, "missing filename", http.StatusBadRequest)
 | 
						|
	}
 | 
						|
 | 
						|
	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))
 | 
						|
	} else {
 | 
						|
		s.log("HTTP", "Unknown file size for %q, boot will be VERY slow (can your Booter provide file sizes?)", name)
 | 
						|
	}
 | 
						|
	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
 | 
						|
	}
 | 
						|
	s.log("HTTP", "Sent file %q to %s", name, r.RemoteAddr)
 | 
						|
 | 
						|
	switch r.URL.Query().Get("type") {
 | 
						|
	case "kernel":
 | 
						|
		mac, err := net.ParseMAC(r.URL.Query().Get("mac"))
 | 
						|
		if err != nil {
 | 
						|
			s.log("HTTP", "File fetch provided invalid MAC address %q", r.URL.Query().Get("mac"))
 | 
						|
			return
 | 
						|
		}
 | 
						|
		s.machineEvent(mac, machineStateKernel, "Sent kernel %q", name)
 | 
						|
	case "initrd":
 | 
						|
		mac, err := net.ParseMAC(r.URL.Query().Get("mac"))
 | 
						|
		if err != nil {
 | 
						|
			s.log("HTTP", "File fetch provided invalid MAC address %q", r.URL.Query().Get("mac"))
 | 
						|
			return
 | 
						|
		}
 | 
						|
		s.machineEvent(mac, machineStateInitrd, "Sent initrd %q", name)
 | 
						|
	}
 | 
						|
}
 | 
						|
 | 
						|
func (s *Server) handleBooting(w http.ResponseWriter, r *http.Request) {
 | 
						|
	// Return a no-op boot script, to satisfy iPXE. It won't get used,
 | 
						|
	// the boot script deletes this image immediately after
 | 
						|
	// downloading.
 | 
						|
	fmt.Fprintf(w, "# Booting")
 | 
						|
 | 
						|
	macStr := r.URL.Query().Get("mac")
 | 
						|
	if macStr == "" {
 | 
						|
		s.debug("HTTP", "Bad request %q from %s, missing MAC address", r.URL, r.RemoteAddr)
 | 
						|
		return
 | 
						|
	}
 | 
						|
	mac, err := net.ParseMAC(macStr)
 | 
						|
	if err != nil {
 | 
						|
		s.debug("HTTP", "Bad request %q from %s, invalid MAC address %q (%s)", r.URL, r.RemoteAddr, macStr, err)
 | 
						|
		return
 | 
						|
	}
 | 
						|
	s.machineEvent(mac, machineStateBooted, "Booting into OS")
 | 
						|
}
 | 
						|
 | 
						|
func ipxeScript(mach Machine, spec *Spec, serverHost string) ([]byte, error) {
 | 
						|
	if spec.IpxeScript != "" {
 | 
						|
		return []byte(spec.IpxeScript), nil
 | 
						|
	}
 | 
						|
 | 
						|
	if spec.Kernel == "" {
 | 
						|
		return nil, errors.New("spec is missing Kernel")
 | 
						|
	}
 | 
						|
 | 
						|
	urlTemplate := fmt.Sprintf("http://%s/_/file?name=%%s&type=%%s&mac=%%s", serverHost)
 | 
						|
	var b bytes.Buffer
 | 
						|
	b.WriteString("#!ipxe\n")
 | 
						|
	u := fmt.Sprintf(urlTemplate, url.QueryEscape(string(spec.Kernel)), "kernel", url.QueryEscape(mach.MAC.String()))
 | 
						|
	fmt.Fprintf(&b, "kernel --name kernel %s\n", u)
 | 
						|
	for i, initrd := range spec.Initrd {
 | 
						|
		u = fmt.Sprintf(urlTemplate, url.QueryEscape(string(initrd)), "initrd", url.QueryEscape(mach.MAC.String()))
 | 
						|
		fmt.Fprintf(&b, "initrd --name initrd%d %s\n", i, u)
 | 
						|
	}
 | 
						|
 | 
						|
	fmt.Fprintf(&b, "imgfetch --name ready http://%s/_/booting?mac=%s ||\n", serverHost, url.QueryEscape(mach.MAC.String()))
 | 
						|
	b.WriteString("imgfree ready ||\n")
 | 
						|
 | 
						|
	b.WriteString("boot kernel ")
 | 
						|
	for i := range spec.Initrd {
 | 
						|
		fmt.Fprintf(&b, "initrd=initrd%d ", i)
 | 
						|
	}
 | 
						|
 | 
						|
	f := func(id string) string {
 | 
						|
		return fmt.Sprintf("http://%s/_/file?name=%s", serverHost, url.QueryEscape(id))
 | 
						|
	}
 | 
						|
	cmdline, err := expandCmdline(spec.Cmdline, template.FuncMap{"ID": f})
 | 
						|
	if err != nil {
 | 
						|
		return nil, fmt.Errorf("expanding cmdline %q: %s", spec.Cmdline, err)
 | 
						|
	}
 | 
						|
	b.WriteString(cmdline)
 | 
						|
	b.WriteByte('\n')
 | 
						|
 | 
						|
	return b.Bytes(), nil
 | 
						|
}
 |