Esempio n. 1
0
func (ds *DataSource) Users() ([]*User, error) {
	ctx, cancel := context.WithTimeout(context.Background(), 3*time.Second)
	defer cancel()
	response, err := ds.keysAPI.Get(ctx, fmt.Sprintf("/%s/users", ds.etcdDir), nil)
	if err != nil {
		return nil, err
	}

	var users []*User

	errCount := 0
	for i := range response.Node.Nodes {
		u, e := userFromNodeValue(response.Node.Nodes[i].Value)
		if e != nil {
			errCount += 1
			logging.Debug(debugTag, "Error while userFromNodeValue: %s", e)
		} else {
			users = append(users, u)
		}
	}

	if errCount > 0 {
		return nil, fmt.Errorf("Errors happened while trying to unmarshal %d user(s)", errCount)
	}

	return users, nil
}
Esempio n. 2
0
func (b *HTTPBooter) fileHandler(w http.ResponseWriter, r *http.Request) {
	splitPath := strings.SplitN(r.URL.Path, "/", 4)
	version := splitPath[2]
	id := splitPath[3]

	logging.Debug("HTTPBOOTER", "Got request for %s", r.URL.Path)

	var (
		f   io.ReadCloser
		err error
	)

	f, err = b.coreOS(version, id)

	if err != nil {
		logging.Log("HTTPBOOTER", "Couldn't get byte stream for %q from %s: %s", r.URL, r.RemoteAddr, err)
		http.Error(w, "Couldn't get byte stream", http.StatusInternalServerError)
		return
	}
	defer f.Close()

	w.Header().Set("Content-Type", "application/octet-stream")
	written, err := io.Copy(w, f)
	if err != nil {
		logging.Log("HTTPBOOTER", "Error serving %s to %s: %s", id, r.RemoteAddr, err)
		return
	}
	logging.Log("HTTPBOOTER", "Sent %s to %s (%d bytes)", id, r.RemoteAddr, written)
}
Esempio n. 3
0
func (ds *DataSource) Groups() ([]*Group, error) {
	ctx, cancel := context.WithTimeout(context.Background(), 3*time.Second)
	defer cancel()
	response, err := ds.keysAPI.Get(ctx, fmt.Sprintf("/%s/groups", ds.etcdDir), nil)
	if err != nil {
		return nil, err
	}

	var groups []*Group

	errCount := 0
	for i := range response.Node.Nodes {
		g, e := groupFromNodeValue(response.Node.Nodes[i].Value)
		if e != nil {
			errCount += 1
			logging.Debug(debugTag, "Error while groupFromNodeValue: %s", e)
		} else {
			groups = append(groups, g)
		}
	}

	if errCount > 0 {
		return nil, fmt.Errorf("Errors happened while trying to unmarshal %d group(s)", errCount)
	}

	return groups, nil
}
Esempio n. 4
0
func ServeDHCP(settings *DHCPSetting, leasePool *LeasePool) error {
	handler, err := newDHCPHandler(settings, leasePool)
	if err != nil {
		logging.Debug("DHCP", "Error in connecting etcd - %s", err.Error())
		return err
	}
	logging.Log("DHCP", "Listening on :67 - with server IP %s", settings.ServerIP.String())
	if settings.IFName != "" {
		err = dhcp4.ListenAndServeIf(settings.IFName, handler)
	} else {
		err = dhcp4.ListenAndServe(handler)
	}
	if err != nil {
		logging.Debug("DHCP", "Error in server - %s", err.Error())
	}
	return err
}
Esempio n. 5
0
func logln(level int, s string) {
	if level == 2 {
		log.Fatalf(s)
	} else if level == 1 {
		logging.Log(debugTag, s)
	} else {
		logging.Debug(debugTag, s)
	}
}
Esempio n. 6
0
// Get the contents of a blob mentioned in a previously issued
// BootSpec. Additionally returns a pretty name for the blob for
// logging purposes.
func (b *HTTPBooter) coreOS(version string, id string) (io.ReadCloser, error) {
	imagePath := filepath.Join(b.runtimeConfig.WorkspacePath, "images")
	switch id {
	case "kernel":
		path := filepath.Join(imagePath, version, "coreos_production_pxe.vmlinuz")
		logging.Debug("HTTPBOOTER", "path=<%q>", path)
		return os.Open(path)
	case "initrd":
		return os.Open(filepath.Join(imagePath, version, "coreos_production_pxe_image.cpio.gz"))
	}
	return nil, fmt.Errorf("id=<%q> wasn't expected", id)
}
Esempio n. 7
0
func ServeDHCP(settings *DHCPSetting, datasource datasource.DataSource) error {
	handler, err := newDHCPHandler(settings, datasource)
	if err != nil {
		logging.Debug("DHCP", "Error in connecting etcd - %s", err.Error())
		return err
	}
	logging.Log("DHCP", "Listening on %s:67 (interface: %s)",
		settings.ServerIP.String(), settings.IFName)
	if settings.IFName != "" {
		err = dhcp4.ListenAndServeIf(settings.IFName, handler)
	} else {
		err = dhcp4.ListenAndServe(handler)
	}
	if err != nil {
		logging.Debug("DHCP", "Error in server - %s", err.Error())
	}

	rand.Seed(time.Now().UTC().UnixNano())

	return err
}
Esempio n. 8
0
func ServeTFTP(listenAddr net.UDPAddr) error {
	pxelinuxDir := FS(false)
	pxelinux, err := pxelinuxDir.Open("/pxelinux/lpxelinux.0")
	if err != nil {
		return err
	}
	tftp.Log = func(msg string, args ...interface{}) { logging.Log("TFTP", msg, args...) }
	tftp.Debug = func(msg string, args ...interface{}) { logging.Debug("TFTP", msg, args...) }

	handler := func(string, net.Addr) (io.ReadCloser, error) {
		return pxelinux, nil
	}

	return tftp.ListenAndServe("udp4", listenAddr.String(), handler)
}
Esempio n. 9
0
func (ds *DataSource) StoreUser(u *User) error {
	userJSON, err := json.Marshal(u)
	if err != nil {
		return err
	}

	logging.Debug(debugTag, "Setting %s", userJSON)

	ctx, cancel := context.WithTimeout(context.Background(), 3*time.Second)
	defer cancel()
	_, err = ds.keysAPI.Set(ctx, fmt.Sprintf("/%s/users/%s", ds.etcdDir, u.Email), string(userJSON[:]), nil)
	if err != nil {
		return err
	}
	return nil
}
Esempio n. 10
0
func (ds *DataSource) StoreGroup(g *Group) error {
	groupJSON, err := json.Marshal(g)
	if err != nil {
		return err
	}

	logging.Debug(debugTag, "Setting %s", groupJSON)

	ctx, cancel := context.WithTimeout(context.Background(), 3*time.Second)
	defer cancel()
	_, err = ds.keysAPI.Set(ctx, fmt.Sprintf("/%s/groups/%s", ds.etcdDir, g.Email), string(groupJSON[:]), nil)
	if err != nil {
		return err
	}
	return nil
}
Esempio n. 11
0
func ServePXE(listenAddr net.UDPAddr, serverIP net.IP, httpAddr net.TCPAddr) error {
	conn, err := net.ListenPacket("udp4", listenAddr.String())
	if err != nil {
		return err
	}
	defer conn.Close()
	l := ipv4.NewPacketConn(conn)
	if err = l.SetControlMessage(ipv4.FlagInterface, true); err != nil {
		return err
	}

	logging.Log("PXE", "Listening on %s", listenAddr.String())
	buf := make([]byte, 1024)
	for {
		n, msg, addr, err := l.ReadFrom(buf)
		if err != nil {
			logging.Log("PXE", "Error reading from socket: %s", err)
			continue
		}

		req, err := ParsePXE(buf[:n])
		if err != nil {
			logging.Debug("PXE", "ParsePXE: %s", err)
			continue
		}

		req.ServerIP = serverIP
		req.HTTPServer = fmt.Sprintf("http://%s/", httpAddr.String())

		logging.Log("PXE", "Chainloading %s (%s) to pxelinux (via %s)", req.MAC, req.ClientIP, req.ServerIP)

		if _, err := l.WriteTo(ReplyPXE(req), &ipv4.ControlMessage{
			IfIndex: msg.IfIndex,
		}, addr); err != nil {
			logging.Log("PXE", "Responding to %s: %s", req.MAC, err)
			continue
		}
	}
}
Esempio n. 12
0
func (u *User) AcceptsPassword(plainPassword string, salt []byte) bool {
	encodedInputPassword, err := u.encodePassword(plainPassword, salt)
	if err != nil {
		logging.Debug(debugTag, "Error while encodePassword: %s", err)
		return false
	}

	userPassword, err := base64.StdEncoding.DecodeString(u.Password)
	if err != nil {
		return false
	}
	if userPassword == nil || encodedInputPassword == nil {
		return false
	}
	if len(userPassword) != len(encodedInputPassword) {
		return false
	}
	for i := range userPassword {
		if userPassword[i] != encodedInputPassword[i] {
			return false
		}
	}
	return true
}
Esempio n. 13
0
func (b *HTTPBooter) pxelinuxConfig(w http.ResponseWriter, r *http.Request) {
	w.Header().Set("Content-Type", "text/plain")

	macStr := filepath.Base(r.URL.Path)
	errStr := fmt.Sprintf("%s requested a pxelinux config from URL %q, which does not include a MAC address", r.RemoteAddr, r.URL)
	if !strings.HasPrefix(macStr, "01-") {
		logging.Debug("HTTPBOOTER", errStr)
		http.Error(w, "Missing MAC address in request", http.StatusBadRequest)
		return
	}
	mac, err := net.ParseMAC(macStr[3:])
	if err != nil {
		logging.Debug("HTTPBOOTER", errStr)
		http.Error(w, "Malformed MAC address in request", http.StatusBadRequest)
		return
	}

	if _, _, err := net.SplitHostPort(r.Host); err != nil {
		r.Host = fmt.Sprintf("%s:%d", r.Host, b.listenAddr.Port)
	}

	// TODO: Ask dataSource about the mac
	// We have a machine sitting in pxelinux, but the Booter says
	// we shouldn't be netbooting. So, give it a config that tells
	// pxelinux to shut down PXE booting and continue with the
	// next local boot method.

	coreOSVersion, _ := b.runtimeConfig.GetCoreOSVersion()

	KernelURL := "http://" + r.Host + "/f/" + coreOSVersion + "/kernel"
	InitrdURL := "http://" + r.Host + "/f/" + coreOSVersion + "/initrd"

	host, _, err := net.SplitHostPort(r.Host)
	if err != nil {
		logging.Log("HTTPBOOTER", "error in parsing host and port")
		http.Error(w, "error in parsing host and port", 500)
		return
	}

	// generate bootparams config
	params, err := b.bootParamsRepo.GenerateConfig(&cloudconfig.ConfigContext{
		MacAddr: strings.Replace(mac.String(), ":", "", -1),
		IP:      "",
	})
	if err != nil {
		logging.Log("HTTPBOOTER", "error in bootparam template - %s", err.Error())
		http.Error(w, "error in bootparam template", 500)
		return
	}
	params = strings.Replace(params, "\n", " ", -1)

	// FIXME: 8001 is hardcoded
	Cmdline := fmt.Sprintf(
		"cloud-config-url=http://%s:8001/cloud/%s "+
			"coreos.config.url=http://%s:8001/ignition/%s %s",
		host, strings.Replace(mac.String(), ":", "", -1),
		host, strings.Replace(mac.String(), ":", "", -1), params)
	bootMessage := strings.Replace(bootMessageTemplate, "$MAC", macStr, -1)
	cfg := fmt.Sprintf(`
SAY %s
DEFAULT linux
LABEL linux
LINUX %s
APPEND initrd=%s %s
`, strings.Replace(bootMessage, "\n", "\nSAY ", -1), KernelURL, InitrdURL, Cmdline)
	w.Write([]byte(cfg))
	logging.Log("HTTPBOOTER", "Sent pxelinux config to %s (%s)", mac, r.RemoteAddr)
}
Esempio n. 14
0
func (h *DHCPHandler) ServeDHCP(p dhcp4.Packet, msgType dhcp4.MessageType, options dhcp4.Options) (d dhcp4.Packet) {
	var macAddress string = strings.Join(strings.Split(p.CHAddr().String(), ":"), "")
	switch msgType {
	case dhcp4.Discover:
		ip, err := h.leasePool.Assign(p.CHAddr().String())
		if err != nil {
			logging.Debug("DHCP", "err in lease pool - %s", err.Error())
			return nil // pool is full
		}
		replyOptions := h.dhcpOptions.SelectOrderOrAll(options[dhcp4.OptionParameterRequestList])
		packet := dhcp4.ReplyPacket(p, dhcp4.Offer, h.settings.ServerIP, ip, h.settings.LeaseDuration, replyOptions)
		// this is a pxe request
		guidVal, is_pxe := options[97]
		if is_pxe {
			logging.Log("DHCP", "dhcp discover with PXE - CHADDR %s - IP %s - our ip %s", p.CHAddr().String(), ip.String(), h.settings.ServerIP.String())
			guid := guidVal[1:]
			packet.AddOption(60, []byte("PXEClient"))
			packet.AddOption(97, guid)
			packet.AddOption(43, h.fillPXE())
		} else {
			logging.Log("DHCP", "dhcp discover - CHADDR %s - IP %s", p.CHAddr().String(), ip.String())
		}
		return packet
	case dhcp4.Request:
		if server, ok := options[dhcp4.OptionServerIdentifier]; ok && !net.IP(server).Equal(h.settings.ServerIP) {
			return nil // this message is not ours
		}
		requestedIP := net.IP(options[dhcp4.OptionRequestedIPAddress])
		if requestedIP == nil {
			requestedIP = net.IP(p.CIAddr())
		}
		if len(requestedIP) != 4 || requestedIP.Equal(net.IPv4zero) {
			logging.Debug("DHCP", "dhcp request - CHADDR %s - bad request", p.CHAddr().String())
			return nil
		}
		_, err := h.leasePool.Request(p.CHAddr().String(), requestedIP)
		if err != nil {
			logging.Debug("DHCP", "dhcp request - CHADDR %s - Requested IP %s - NO MATCH", p.CHAddr().String(), requestedIP.String())
			return dhcp4.ReplyPacket(p, dhcp4.NAK, h.settings.ServerIP, nil, 0, nil)
		}

		replyOptions := h.dhcpOptions.SelectOrderOrAll(options[dhcp4.OptionParameterRequestList])
		packet := dhcp4.ReplyPacket(p, dhcp4.ACK, h.settings.ServerIP, requestedIP, h.settings.LeaseDuration, replyOptions)
		// this is a pxe request
		guidVal, is_pxe := options[97]
		if is_pxe {
			logging.Log("DHCP", "dhcp request with PXE - CHADDR %s - Requested IP %s - our ip %s - ACCEPTED", p.CHAddr().String(), requestedIP.String(), h.settings.ServerIP.String())
			guid := guidVal[1:]
			packet.AddOption(60, []byte("PXEClient"))
			packet.AddOption(97, guid)
			packet.AddOption(43, h.fillPXE())
		} else {
			logging.Log("DHCP", "dhcp request - CHADDR %s - Requested IP %s - ACCEPTED", p.CHAddr().String(), requestedIP.String())
		}
		packet.AddOption(12, []byte("node"+macAddress)) // host name option
		return packet
	case dhcp4.Release, dhcp4.Decline:
		return nil
	}
	return nil
}
Esempio n. 15
0
func main() {
	var err error
	flag.Parse()

	fmt.Printf("Blacksmith (%s)\n", version)
	fmt.Printf("  Commit:        %s\n", commit)
	fmt.Printf("  Build Time:    %s\n", buildTime)

	if *versionFlag {
		os.Exit(0)
	}

	// etcd config
	if etcdFlag == nil || clusterNameFlag == nil {
		fmt.Fprint(os.Stderr, "\nPlease specify the etcd endpoints\n")
		os.Exit(1)
	}

	// finding interface by interface name
	var dhcpIF *net.Interface
	if *listenIFFlag != "" {
		dhcpIF, err = net.InterfaceByName(*listenIFFlag)
		if err != nil {
			fmt.Fprintf(os.Stderr, "\nError while trying to get the interface (%s)\n", *listenIFFlag)
			os.Exit(1)
		}
	} else {
		fmt.Fprint(os.Stderr, "\nPlease specify an interface\n")
		os.Exit(1)
	}

	serverIP, err := interfaceIP(dhcpIF)
	if err != nil {
		fmt.Fprintf(os.Stderr, "\nError while trying to get the ip from the interface (%s)\n", dhcpIF)
		os.Exit(1)
	}

	webAddr := net.TCPAddr{IP: serverIP, Port: 8000}

	if *httpListenFlag != httpListenFlagDefaultTCPAddress {
		splitAddress := strings.Split(*httpListenFlag, ":")
		if len(splitAddress) > 2 {
			fmt.Printf("Incorrect tcp address provided: %s", httpListenFlag)
			os.Exit(1)
		}
		if len(splitAddress) == 1 {
			splitAddress = append(splitAddress, "8000")
		}

		webAddr.IP = net.ParseIP(splitAddress[0])
		port, err := strconv.ParseInt(splitAddress[1], 10, 64)

		if err != nil {
			fmt.Printf("Incorrect tcp address provided: %s", httpListenFlag)
			os.Exit(1)
		}
		webAddr.Port = int(port)

	}

	// component ports
	// web api is exposed to requests from `webIP', 0.0.0.0 by default

	// other services are exposed just through the given interface
	var httpBooterAddr = net.TCPAddr{IP: serverIP, Port: 70}
	var tftpAddr = net.UDPAddr{IP: serverIP, Port: 69}
	var pxeAddr = net.UDPAddr{IP: serverIP, Port: 4011}
	// 67 -> dhcp

	// dhcp setting
	leaseStart := net.ParseIP(*leaseStartFlag)
	leaseRange := *leaseRangeFlag
	leaseSubnet := net.ParseIP(*leaseSubnetFlag)
	leaseRouter := net.ParseIP(*leaseRouterFlag)

	dnsIPStrings := strings.Split(*dnsAddressesFlag, ",")
	if len(dnsIPStrings) == 0 {
		fmt.Fprint(os.Stderr, "\nPlease specify an DNS server\n")
		os.Exit(1)
	}
	for _, ipString := range dnsIPStrings {
		ip := net.ParseIP(ipString)
		if ip == nil {
			fmt.Fprintf(os.Stderr, "\nInvalid dns ip: %s\n", ipString)
			os.Exit(1)
		}
	}

	if leaseStart == nil {
		fmt.Fprint(os.Stderr, "\nPlease specify the lease start ip\n")
		os.Exit(1)
	}
	if leaseRange <= 1 {
		fmt.Fprint(os.Stderr, "\nLease range should be greater that 1\n")
		os.Exit(1)
	}
	if leaseSubnet == nil {
		fmt.Fprint(os.Stderr, "\nPlease specify the lease subnet\n")
		os.Exit(1)
	}
	if leaseRouter == nil {
		fmt.Fprint(os.Stderr, "\nNo network router is defined.\n")
	}

	fmt.Printf("Interface IP:    %s\n", serverIP.String())
	fmt.Printf("Interface Name:  %s\n", dhcpIF.Name)

	// datasources
	etcdClient, err := etcd.New(etcd.Config{
		Endpoints:               strings.Split(*etcdFlag, ","),
		HeaderTimeoutPerRequest: 5 * time.Second,
	})
	if err != nil {
		fmt.Fprintf(os.Stderr, "\nCouldn't create etcd connection: %s\n", err)
		os.Exit(1)
	}
	kapi := etcd.NewKeysAPI(etcdClient)

	v := datasource.BlacksmithVersion{
		Version:   version,
		Commit:    commit,
		BuildTime: buildTime,
	}
	etcdDataSource, err := datasource.NewEtcdDataSource(kapi, etcdClient,
		leaseStart, leaseRange, *clusterNameFlag, *workspacePathFlag,
		serverIP, dnsIPStrings, v)
	if err != nil {
		fmt.Fprintf(os.Stderr, "\nCouldn't create runtime configuration: %s\n", err)
		os.Exit(1)
	}

	go func() {
		logging.RecordLogs(log.New(os.Stderr, "", log.LstdFlags), *debugFlag)
	}()

	// serving api
	go func() {
		err := web.ServeWeb(etcdDataSource, webAddr)
		log.Fatalf("\nError while serving api: %s\n", err)
	}()

	c := make(chan os.Signal, 1)
	signal.Notify(c, syscall.SIGHUP, syscall.SIGINT, syscall.SIGTERM)
	go func() {
		for _ = range c {
			gracefulShutdown(etcdDataSource)
		}
	}()

	// waiting til we're officially the master instance
	for !etcdDataSource.IsMaster() {
		logging.Debug(debugTag, "Not master, waiting to be promoted...")
		time.Sleep(datasource.StandbyMasterUpdateTime)
	}

	logging.Debug(debugTag, "Now we're the master instance. Starting the services...")

	// serving http booter
	go func() {
		err := pxe.ServeHTTPBooter(httpBooterAddr, etcdDataSource, webAddr.Port)
		log.Fatalf("\nError while serving http booter: %s\n", err)
	}()

	// serving tftp
	go func() {
		err := pxe.ServeTFTP(tftpAddr)
		log.Fatalf("\nError while serving tftp: %s\n", err)
	}()

	// pxe protocol
	go func() {
		err := pxe.ServePXE(pxeAddr, serverIP, httpBooterAddr)
		log.Fatalf("\nError while serving pxe: %s\n", err)
	}()

	// serving dhcp
	go func() {
		err := dhcp.ServeDHCP(&dhcp.DHCPSetting{
			IFName:     dhcpIF.Name,
			ServerIP:   serverIP,
			RouterAddr: leaseRouter,
			SubnetMask: leaseSubnet,
		}, etcdDataSource)
		log.Fatalf("\nError while serving dhcp: %s\n", err)
	}()

	for etcdDataSource.IsMaster() {
		time.Sleep(datasource.ActiveMasterUpdateTime)
	}

	logging.Debug(debugTag, "Now we're NOT the master. Terminating. Hoping to be restarted by the service manager.")

	gracefulShutdown(etcdDataSource)
}
Esempio n. 16
0
func (b *HTTPBooter) pxelinuxConfig(w http.ResponseWriter, r *http.Request) {
	w.Header().Set("Content-Type", "text/plain")

	macStr := filepath.Base(r.URL.Path)
	errStr := fmt.Sprintf("%s requested a pxelinux config from URL %q, which does not include a correct MAC address", r.RemoteAddr, r.URL)
	if !strings.HasPrefix(macStr, "01-") {
		logging.Debug("HTTPBOOTER", errStr)
		http.Error(w, "Missing MAC address in request", http.StatusBadRequest)
		return
	}
	mac, err := net.ParseMAC(macStr[3:])
	if err != nil {
		logging.Debug("HTTPBOOTER", errStr)
		http.Error(w, "Malformed MAC address in request", http.StatusBadRequest)
		return
	}

	machine, exist := b.datasource.GetMachine(mac)
	if !exist {
		logging.Debug("HTTPBOOTER", "Machine not found. mac=%s", mac)
		http.Error(w, "Machine not found", http.StatusNotFound)
		return
	}

	if _, _, err := net.SplitHostPort(r.Host); err != nil {
		r.Host = fmt.Sprintf("%s:%d", r.Host, b.listenAddr.Port)
	}

	coreOSVersion, _ := b.datasource.CoreOSVersion()

	KernelURL := "http://" + r.Host + "/f/" + coreOSVersion + "/kernel"
	InitrdURL := "http://" + r.Host + "/f/" + coreOSVersion + "/initrd"

	host, _, err := net.SplitHostPort(r.Host)
	if err != nil {
		logging.Log("HTTPBOOTER", "error in parsing host and port")
		http.Error(w, "error in parsing host and port", 500)
		return
	}

	params, err := templating.ExecuteTemplateFolder(
		path.Join(b.datasource.WorkspacePath(), "config", "bootparams"), machine, r.Host)
	if err != nil {
		logging.Log("HTTPBOOTER", `Error while executing the template: %q`, err)
		http.Error(w, fmt.Sprintf(`Error while executing the template: %q`, err),
			http.StatusInternalServerError)
		return
	}

	if err != nil {
		logging.Log("HTTPBOOTER", "error in bootparam template - %s", err.Error())
		http.Error(w, "error in bootparam template", 500)
		return
	}
	params = strings.Replace(params, "\n", " ", -1)

	Cmdline := fmt.Sprintf(
		"cloud-config-url=http://%s:%d/t/cc/%s "+
			"coreos.config.url=http://%s:%d/t/ig/%s %s",
		host, b.webPort, mac.String(),
		host, b.webPort, mac.String(), params)
	bootMessage := strings.Replace(b.bootMessageTemplate, "$MAC", macStr, -1)
	cfg := fmt.Sprintf(`
SAY %s
DEFAULT linux
LABEL linux
LINUX %s
APPEND initrd=%s %s
`, strings.Replace(bootMessage, "\n", "\nSAY ", -1), KernelURL, InitrdURL, Cmdline)
	w.Write([]byte(cfg))
	logging.Log("HTTPBOOTER", "Sent pxelinux config to %s (%s)", mac, r.RemoteAddr)
}