diff --git a/pixiecore/README.md b/pixiecore/README.md index 989b2a0..e23083d 100644 --- a/pixiecore/README.md +++ b/pixiecore/README.md @@ -6,6 +6,8 @@ boots, or as a building block of machine management infrastructure. [](https://github.com/google/netboot/blob/master/LICENSE) [](https://travis-ci.org/google/netboot)   [](https://godoc.org/go.universe.tf/netboot/pixiecore) + + ## Why? Booting a Linux system over the network is quite tedious. You have to diff --git a/pixiecore/cli/cli.go b/pixiecore/cli/cli.go index 072eeb4..9236b78 100644 --- a/pixiecore/cli/cli.go +++ b/pixiecore/cli/cli.go @@ -72,10 +72,15 @@ func serverConfigFlags(cmd *cobra.Command) { cmd.Flags().BoolP("log-timestamps", "t", false, "Add a timestamp to each log line") 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().Int("status-port", 0, "HTTP port for status information (can be the same as --port)") 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") cmd.Flags().String("ipxe-efi32", "", "Path to an iPXE binary for 32-bit UEFI") cmd.Flags().String("ipxe-efi64", "", "Path to an iPXE binary for 64-bit UEFI") + + // Development flags, hidden from normal use. + cmd.Flags().String("ui-assets-dir", "", "UI assets directory (used for development)") + cmd.Flags().MarkHidden("ui-assets-dir") } func mustFile(path string) []byte { @@ -104,6 +109,10 @@ func serverFromFlags(cmd *cobra.Command) *pixiecore.Server { if err != nil { fatalf("Error reading flag: %s", err) } + httpStatusPort, err := cmd.Flags().GetInt("status-port") + if err != nil { + fatalf("Error reading flag: %s", err) + } dhcpNoBind, err := cmd.Flags().GetBool("dhcp-no-bind") if err != nil { fatalf("Error reading flag: %s", err) @@ -120,16 +129,22 @@ func serverFromFlags(cmd *cobra.Command) *pixiecore.Server { if err != nil { fatalf("Error reading flag: %s", err) } + uiAssetsDir, err := cmd.Flags().GetString("ui-assets-dir") + if err != nil { + fatalf("Error reading flag: %s", err) + } if httpPort <= 0 { fatalf("HTTP port must be >0") } ret := &pixiecore.Server{ - Ipxe: map[pixiecore.Firmware][]byte{}, - Log: logWithStdFmt, - HTTPPort: httpPort, - DHCPNoBind: dhcpNoBind, + Ipxe: map[pixiecore.Firmware][]byte{}, + Log: logWithStdFmt, + HTTPPort: httpPort, + HTTPStatusPort: httpStatusPort, + DHCPNoBind: dhcpNoBind, + UIAssetsDir: uiAssetsDir, } for fwtype, bs := range Ipxe { ret.Ipxe[fwtype] = bs diff --git a/pixiecore/dhcp.go b/pixiecore/dhcp.go index 96d49e9..27c6450 100644 --- a/pixiecore/dhcp.go +++ b/pixiecore/dhcp.go @@ -49,10 +49,16 @@ func (s *Server) serveDHCP(conn *dhcp4.Conn) error { } if spec == nil { s.debug("DHCP", "No boot spec for %s, ignoring boot request", pkt.HardwareAddr) + s.machineEvent(pkt.HardwareAddr, machineStateIgnored, "Machine should not netboot") continue } s.log("DHCP", "Offering to boot %s", pkt.HardwareAddr) + if isIpxe { + s.machineEvent(pkt.HardwareAddr, machineStateProxyDHCPIpxe, "Offering to boot iPXE") + } else { + s.machineEvent(pkt.HardwareAddr, machineStateProxyDHCP, "Offering to boot") + } // Machine should be booted. serverIP, err := interfaceIP(intf) @@ -157,7 +163,7 @@ func (s *Server) offerDHCP(pkt *dhcp4.Packet, mach Machine, serverIP net.IP, isI resp.BootFilename = fmt.Sprintf("http://%s:%d/_/ipxe?arch=%d&mac=%s", serverIP, s.HTTPPort, mach.Arch, mach.MAC) } else { resp.BootServerName = serverIP.String() - resp.BootFilename = fmt.Sprintf("%d", fwtype) + resp.BootFilename = fmt.Sprintf("%s/%d", mach.MAC, fwtype) } if fwtype == FirmwareX86PC { diff --git a/pixiecore/http.go b/pixiecore/http.go index aa080b2..82ebde7 100644 --- a/pixiecore/http.go +++ b/pixiecore/http.go @@ -26,16 +26,23 @@ import ( "text/template" ) -func (s *Server) serveHTTP(l net.Listener) error { - httpMux := http.NewServeMux() - httpMux.HandleFunc("/_/ipxe", s.handleIpxe) - httpMux.HandleFunc("/_/file", s.handleFile) - if err := http.Serve(l, httpMux); err != nil { +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) { macStr := r.URL.Query().Get("mac") if macStr == "" { @@ -89,7 +96,7 @@ func (s *Server) handleIpxe(w http.ResponseWriter, r *http.Request) { http.Error(w, "you don't netboot", http.StatusNotFound) return } - script, err := ipxeScript(spec, r.Host) + script, err := ipxeScript(mach, spec, r.Host) 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) @@ -97,6 +104,7 @@ func (s *Server) handleIpxe(w http.ResponseWriter, r *http.Request) { } s.log("HTTP", "Sending ipxe boot script to %s", r.RemoteAddr) + s.machineEvent(mac, machineStateIpxeScript, "Sent iPXE boot script") w.Header().Set("Content-Type", "text/plain") w.Write(script) } @@ -125,20 +133,55 @@ func (s *Server) handleFile(w http.ResponseWriter, r *http.Request) { return } s.log("HTTP", "Sent file %q to %s", name, r.RemoteAddr) + + 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 + } + switch r.URL.Query().Get("type") { + case "kernel": + s.machineEvent(mac, machineStateKernel, "Sent kernel %q", name) + case "initrd": + s.machineEvent(mac, machineStateInitrd, "Sent initrd %q", name) + } } -func ipxeScript(spec *Spec, serverHost string) ([]byte, error) { +func (s *Server) handleBooting(w http.ResponseWriter, r *http.Request) { + // This handler always errors out. + http.Error(w, "", http.StatusInternalServerError) + + 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.Kernel == "" { return nil, errors.New("spec is missing Kernel") } - urlPrefix := fmt.Sprintf("http://%s/_/file?name=", serverHost) + urlTemplate := fmt.Sprintf("http://%s/_/file?name=%%s&type=%%s&mac=%%s", serverHost) var b bytes.Buffer b.WriteString("#!ipxe\n") - fmt.Fprintf(&b, "kernel --name kernel %s%s\n", urlPrefix, url.QueryEscape(string(spec.Kernel))) + 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 { - fmt.Fprintf(&b, "initrd --name initrd%d %s%s\n", i, urlPrefix, url.QueryEscape(string(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) @@ -152,7 +195,7 @@ func ipxeScript(spec *Spec, serverHost string) ([]byte, error) { return nil, fmt.Errorf("expanding cmdline %q: %s", spec.Cmdline, err) } b.WriteString(cmdline) - b.WriteByte('\n') + return b.Bytes(), nil } diff --git a/pixiecore/http_test.go b/pixiecore/http_test.go index 7b99d05..2fc6a38 100644 --- a/pixiecore/http_test.go +++ b/pixiecore/http_test.go @@ -53,6 +53,7 @@ func TestIpxe(t *testing.T) { Booter: booterFunc(booter), Log: log, Debug: log, + events: make(map[string][]machineEvent), } // Successful boot @@ -69,9 +70,11 @@ func TestIpxe(t *testing.T) { } expected := `#!ipxe -kernel --name kernel http://localhost:1234/_/file?name=k-01%3A02%3A03%3A04%3A05%3A06-0 -initrd --name initrd0 http://localhost:1234/_/file?name=i1-01%3A02%3A03%3A04%3A05%3A06-0 -initrd --name initrd1 http://localhost:1234/_/file?name=i2-01%3A02%3A03%3A04%3A05%3A06-0 +kernel --name kernel http://localhost:1234/_/file?name=k-01%3A02%3A03%3A04%3A05%3A06-0&type=kernel&mac=01%3A02%3A03%3A04%3A05%3A06 +initrd --name initrd0 http://localhost:1234/_/file?name=i1-01%3A02%3A03%3A04%3A05%3A06-0&type=initrd&mac=01%3A02%3A03%3A04%3A05%3A06 +initrd --name initrd1 http://localhost:1234/_/file?name=i2-01%3A02%3A03%3A04%3A05%3A06-0&type=initrd&mac=01%3A02%3A03%3A04%3A05%3A06 +imgfetch --name ready http://localhost:1234/_/booting?mac=01%3A02%3A03%3A04%3A05%3A06 || +imgfree ready || boot kernel initrd=initrd0 initrd=initrd1 thing=http://localhost:1234/_/file?name=f-01%3A02%3A03%3A04%3A05%3A06-0 foo=bar ` if rr.Body.String() != expected { @@ -92,9 +95,11 @@ boot kernel initrd=initrd0 initrd=initrd1 thing=http://localhost:1234/_/file?nam } expected = `#!ipxe -kernel --name kernel http://localhost:1234/_/file?name=k-fe%3Afe%3Afe%3Afe%3Afe%3Afe-1 -initrd --name initrd0 http://localhost:1234/_/file?name=i1-fe%3Afe%3Afe%3Afe%3Afe%3Afe-1 -initrd --name initrd1 http://localhost:1234/_/file?name=i2-fe%3Afe%3Afe%3Afe%3Afe%3Afe-1 +kernel --name kernel http://localhost:1234/_/file?name=k-fe%3Afe%3Afe%3Afe%3Afe%3Afe-1&type=kernel&mac=fe%3Afe%3Afe%3Afe%3Afe%3Afe +initrd --name initrd0 http://localhost:1234/_/file?name=i1-fe%3Afe%3Afe%3Afe%3Afe%3Afe-1&type=initrd&mac=fe%3Afe%3Afe%3Afe%3Afe%3Afe +initrd --name initrd1 http://localhost:1234/_/file?name=i2-fe%3Afe%3Afe%3Afe%3Afe%3Afe-1&type=initrd&mac=fe%3Afe%3Afe%3Afe%3Afe%3Afe +imgfetch --name ready http://localhost:1234/_/booting?mac=fe%3Afe%3Afe%3Afe%3Afe%3Afe || +imgfree ready || boot kernel initrd=initrd0 initrd=initrd1 thing=http://localhost:1234/_/file?name=f-fe%3Afe%3Afe%3Afe%3Afe%3Afe-1 foo=bar ` if rr.Body.String() != expected { diff --git a/pixiecore/logging.go b/pixiecore/logging.go index f6ac580..e77214a 100644 --- a/pixiecore/logging.go +++ b/pixiecore/logging.go @@ -17,8 +17,77 @@ package pixiecore import ( "encoding/base64" "fmt" + "net" + "time" ) +const savedEventsPerMachine = 10 + +type machineState int + +func (m machineState) String() string { + switch m { + case machineStateProxyDHCP: + return "Made boot offer (ProxyDHCP)" + case machineStatePXE: + return "Made boot offer (PXE)" + case machineStateTFTP: + return "Sent iPXE binary (TFTP)" + case machineStateProxyDHCPIpxe: + return "Made iPXE boot offer (ProxyDHCP)" + case machineStateIpxeScript: + return "Sent iPXE script (HTTP)" + case machineStateKernel: + return "Sent kernel (HTTP)" + case machineStateInitrd: + return "Sent initrd(s) (HTTP)" + case machineStateBooted: + return "Booted machine" + default: + return "Unknown" + } +} + +func (m machineState) Progress() string { + return fmt.Sprintf("%.0f%%", float32(m)/float32(machineStateBooted)*100) +} + +const ( + machineStateUnknown machineState = iota + machineStateProxyDHCP + machineStatePXE + machineStateTFTP + machineStateProxyDHCPIpxe + machineStateIpxeScript + machineStateKernel + machineStateInitrd + machineStateBooted + + machineStateIgnored +) + +type machineEvent struct { + Timestamp time.Time + State machineState + Message string +} + +func (s *Server) machineEvent(mac net.HardwareAddr, state machineState, format string, args ...interface{}) { + evt := machineEvent{ + Timestamp: time.Now(), + State: state, + Message: fmt.Sprintf(format, args...), + } + k := mac.String() + + s.eventsMu.Lock() + defer s.eventsMu.Unlock() + s.events[k] = append(s.events[k], evt) + if len(s.events[k]) > savedEventsPerMachine { + s.events[k] = s.events[k][len(s.events[k])-savedEventsPerMachine:] + } +} + func (s *Server) log(subsystem, format string, args ...interface{}) { if s.Log == nil { return diff --git a/pixiecore/pixiecore-ui.png b/pixiecore/pixiecore-ui.png new file mode 100644 index 0000000..fae32d3 Binary files /dev/null and b/pixiecore/pixiecore-ui.png differ diff --git a/pixiecore/pixiecore.go b/pixiecore/pixiecore.go index 854759c..1ab9360 100644 --- a/pixiecore/pixiecore.go +++ b/pixiecore/pixiecore.go @@ -20,6 +20,7 @@ import ( "io" "net" "strings" + "sync" "text/template" "go.universe.tf/netboot/dhcp4" @@ -138,8 +139,12 @@ type Server struct { Booter Booter // Address to listen on, or empty for all interfaces. - Address string + Address string + // HTTP port for boot services. HTTPPort int + // HTTP port for human-readable information. Can be the same as + // HTTPPort. + HTTPStatusPort int // Ipxe lists the supported bootable Firmwares, and their // associated ipxe binary. @@ -165,7 +170,14 @@ type Server struct { // Currently only supported on Linux. DHCPNoBind bool + // Read UI assets from this path, rather than use the builtin UI + // assets. Used for development of Pixiecore. + UIAssetsDir string + errs chan error + + eventsMu sync.Mutex + events map[string][]machineEvent } // Serve listens for machines attempting to boot, and uses Booter to @@ -211,20 +223,39 @@ func (s *Server) Serve() error { pxe.Close() return err } + var statusHTTP net.Listener + if s.HTTPStatusPort != 0 && s.HTTPStatusPort != s.HTTPPort { + statusHTTP, err = net.Listen("tcp", fmt.Sprintf("%s:%d", s.Address, s.HTTPStatusPort)) + if err != nil { + dhcp.Close() + tftp.Close() + pxe.Close() + http.Close() + return err + } + } + s.events = make(map[string][]machineEvent) // 5 buffer slots, one for each goroutine, plus one for // Shutdown(). We only ever pull the first error out, but shutdown // will likely generate some spurious errors from the other // goroutines, and we want them to be able to dump them without // blocking. - s.errs = make(chan error, 5) + s.errs = make(chan error, 6) 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) }() - go func() { s.errs <- s.serveHTTP(http) }() + if statusHTTP != nil { + go func() { s.errs <- serveHTTP(http, s.serveHTTP) }() + go func() { s.errs <- serveHTTP(statusHTTP, s.serveUI) }() + } else if s.HTTPStatusPort == s.HTTPPort { + go func() { s.errs <- serveHTTP(http, s.serveHTTP, s.serveUI) }() + } else { + go func() { s.errs <- serveHTTP(http, s.serveHTTP) }() + } // Wait for either a fatal error, or Shutdown(). err = <-s.errs @@ -232,6 +263,9 @@ func (s *Server) Serve() error { tftp.Close() pxe.Close() http.Close() + if statusHTTP != nil { + statusHTTP.Close() + } return err } diff --git a/pixiecore/pxe.go b/pixiecore/pxe.go index b17f693..4929c8c 100644 --- a/pixiecore/pxe.go +++ b/pixiecore/pxe.go @@ -69,6 +69,8 @@ func (s *Server) servePXE(conn net.PacketConn) error { continue } + s.machineEvent(pkt.HardwareAddr, machineStatePXE, "Sent PXE configuration") + resp, err := s.offerPXE(pkt, serverIP, fwtype) if err != nil { s.log("PXE", "Failed to construct PXE offer for %s (%s): %s", pkt.HardwareAddr, addr, err) @@ -124,7 +126,7 @@ func (s *Server) offerPXE(pkt *dhcp4.Packet, serverIP net.IP, fwtype Firmware) ( RelayAddr: pkt.RelayAddr, ServerAddr: serverIP, BootServerName: serverIP.String(), - BootFilename: fmt.Sprintf("%d", fwtype), + BootFilename: fmt.Sprintf("%s/%d", pkt.HardwareAddr, fwtype), Options: dhcp4.Options{ dhcp4.OptServerIdentifier: serverIP, dhcp4.OptVendorIdentifier: []byte("PXEClient"), diff --git a/pixiecore/tftp.go b/pixiecore/tftp.go index 327d365..651e710 100644 --- a/pixiecore/tftp.go +++ b/pixiecore/tftp.go @@ -22,6 +22,7 @@ import ( "io/ioutil" "net" "strconv" + "strings" "go.universe.tf/netboot/tftp" ) @@ -39,18 +40,44 @@ func (s *Server) serveTFTP(l net.PacketConn) error { return nil } +func extractInfo(path string) (net.HardwareAddr, int, error) { + pathElements := strings.Split(path, "/") + if len(pathElements) != 2 { + return nil, 0, errors.New("not found") + } + + mac, err := net.ParseMAC(pathElements[0]) + if err != nil { + return nil, 0, fmt.Errorf("invalid MAC address %q", pathElements[0]) + } + + i, err := strconv.Atoi(pathElements[1]) + if err != nil { + return nil, 0, errors.New("not found") + } + + return mac, i, nil +} + func (s *Server) logTFTPTransfer(clientAddr net.Addr, path string, err error) { + mac, _, err := extractInfo(path) + if err != nil { + // Unknown path, nothing to log. + return + } + if err != nil { s.log("TFTP", "Send of %q to %s failed: %s", path, clientAddr, err) } else { s.log("TFTP", "Sent %q to %s", path, clientAddr) + s.machineEvent(mac, machineStateTFTP, "Sent iPXE to %s", clientAddr) } } func (s *Server) handleTFTP(path string, clientAddr net.Addr) (io.ReadCloser, int64, error) { - i, err := strconv.Atoi(path) + _, i, err := extractInfo(path) if err != nil { - return nil, 0, errors.New("not found") + return nil, 0, fmt.Errorf("unknown path %q", path) } bs, ok := s.Ipxe[Firmware(i)] diff --git a/pixiecore/ui.go b/pixiecore/ui.go new file mode 100644 index 0000000..a584dd3 --- /dev/null +++ b/pixiecore/ui.go @@ -0,0 +1,96 @@ +// 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" + "html/template" + "io/ioutil" + "mime" + "net/http" + "path/filepath" + "strings" + "time" + + "github.com/cockroachdb/cockroach/ui" +) + +//go:generate go-bindata -o ui/autogen.go -ignore autogen.go -pkg ui -nometadata -nomemcopy -prefix ui/ ui/ + +const assetsPath = "/_/assets/" + +func (s *Server) serveUI(mux *http.ServeMux) { + mux.HandleFunc("/", s.handleUI) + mux.HandleFunc(assetsPath, s.handleUI) +} + +func (s *Server) handleUI(w http.ResponseWriter, r *http.Request) { + if r.URL.Path == "/" { + r.URL.Path = assetsPath + "index.html" + } + if !strings.HasPrefix(r.URL.Path, assetsPath) { + http.NotFound(w, r) + return + } + // Sticking a / in front of the path before cleaning it will strip + // out any "../" attempts at path traversal. Then we remove the + // leading /, and we end up with a path we can filepath.Join or + // fetch from asset data. + path := filepath.Clean("/" + r.URL.Path[len(assetsPath):])[1:] + t, err := s.getTemplate(path) + if err != nil { + s.log("UI", "Failed to parse template for %q: %s", path, err) + http.Error(w, "internal server error", http.StatusInternalServerError) + return + } + var b bytes.Buffer + s.eventsMu.Lock() + defer s.eventsMu.Unlock() + if err = t.Execute(&b, s.events); err != nil { + s.log("UI", "Failed to expand template for %q: %s", path, err) + http.Error(w, "internal server error", http.StatusInternalServerError) + return + } + mimetype := mime.TypeByExtension(filepath.Ext(path)) + if mimetype != "" { + w.Header().Set("Content-Type", mimetype) + } + + b.WriteTo(w) +} + +func (s *Server) getTemplate(name string) (*template.Template, error) { + var ( + bs []byte + err error + ) + if s.UIAssetsDir != "" { + bs, err = ioutil.ReadFile(filepath.Join(s.UIAssetsDir, name)) + } else { + bs, err = ui.Asset(name) + } + if err != nil { + return nil, err + } + funcs := template.FuncMap{ + "dec": func(i int) int { + return i - 1 + }, + "timestamp_millis": func(t time.Time) int64 { + return t.UnixNano() / int64(time.Millisecond) + }, + } + return template.New(name).Funcs(funcs).Parse(string(bs)) +} diff --git a/pixiecore/ui/autogen.go b/pixiecore/ui/autogen.go new file mode 100644 index 0000000..4f1c455 --- /dev/null +++ b/pixiecore/ui/autogen.go @@ -0,0 +1,281 @@ +// Code generated by go-bindata. +// sources: +// ui/index.html +// ui/style.css +// ui/ui.js +// DO NOT EDIT! + +package ui + +import ( + "bytes" + "compress/gzip" + "fmt" + "io" + "io/ioutil" + "os" + "path/filepath" + "strings" + "time" +) + +func bindataRead(data, name string) ([]byte, error) { + gz, err := gzip.NewReader(strings.NewReader(data)) + if err != nil { + return nil, fmt.Errorf("Read %q: %v", name, err) + } + + var buf bytes.Buffer + _, err = io.Copy(&buf, gz) + clErr := gz.Close() + + if err != nil { + return nil, fmt.Errorf("Read %q: %v", name, err) + } + if clErr != nil { + return nil, err + } + + return buf.Bytes(), nil +} + +type asset struct { + bytes []byte + info os.FileInfo +} + +type bindataFileInfo struct { + name string + size int64 + mode os.FileMode + modTime time.Time +} + +func (fi bindataFileInfo) Name() string { + return fi.name +} +func (fi bindataFileInfo) Size() int64 { + return fi.size +} +func (fi bindataFileInfo) Mode() os.FileMode { + return fi.mode +} +func (fi bindataFileInfo) ModTime() time.Time { + return fi.modTime +} +func (fi bindataFileInfo) IsDir() bool { + return false +} +func (fi bindataFileInfo) Sys() interface{} { + return nil +} + +var _indexHtml = "\x1f\x8b\x08\x00\x00\x09\x6e\x88\x00\xff\x8c\x54\x4d\x6f\xdb\x30\x0c\xbd\xf7\x57\x70\x46\x0f\x0d\x10\x7f\xa4\x58\x87\x21\xb0\x73\x19\xb0\x5b\x81\x02\xcd\xce\x85\x26\x31\xb1\x36\x49\x36\x24\x25\x4d\x10\xf4\xbf\x8f\x96\x6c\x47\xe9\x69\xa7\x80\x7c\x8f\x8f\x7c\xa4\x9c\xfa\x8b\xe8\xb8\x3f\xf7\x08\xad\xd7\x6a\x73\x57\x4f\x3f\xc8\xc4\xe6\x0e\xa0\xd6\xe8\x19\xf0\x96\x59\x87\xbe\xc9\x0e\x7e\x97\x7f\xcf\xae\x80\x61\x1a\x9b\xec\x28\xf1\xbd\xef\xac\xcf\x80\x77\xc6\xa3\x21\xe2\xbb\x14\xbe\x6d\x04\x1e\x25\xc7\x3c\x04\x4b\x2d\x8d\xd4\x07\x9d\x3b\xce\x14\x36\xab\x25\x85\x5e\x32\x35\xc5\x51\xd5\x4b\xaf\x70\xf3\x22\x4f\x12\x79\x67\xb1\x2e\x63\x62\x80\x94\x34\x7f\xc1\xa2\x6a\x32\xe7\xcf\x0a\x5d\x8b\x48\x1d\x5b\x8b\xbb\x26\x2b\xdf\x4a\xe6\x68\x42\x57\x06\xac\xe0\xce\x45\x3d\xc7\xad\xec\x3d\x08\xdc\xa1\x05\x67\x79\x4a\x3d\xc8\xe2\x0f\xd1\xea\x32\x92\xc8\x75\x19\x6d\xd7\xbf\x3b\x71\x0e\xe5\xed\x2a\x9d\x85\xa2\x21\xd9\x9f\x72\xcd\x78\x2b\x0d\xe6\x4a\x3a\x3f\xe4\x00\x2e\x97\x1c\x2c\x33\x7b\x84\x7b\x02\x97\x70\x8f\x47\x5a\x84\x83\x75\x03\x05\x7c\x7c\x04\x4e\x52\x09\x52\x34\xd9\xe5\x12\xc8\x04\x67\x51\x84\x28\x42\x1e\x81\x2b\x9a\xb0\xc9\xba\x23\xda\x61\xb5\x33\x38\x28\x4c\x20\xd5\xe5\x4c\x08\x8b\x83\xd3\xab\x50\x5d\xf6\x09\x3b\x11\xeb\x6d\xb7\x8f\xe4\x19\x1e\x09\x61\x63\xe3\xc5\xd6\xe4\x03\x1e\xa4\x11\x78\x9a\x1d\x3c\x08\xe4\xf0\xa0\xd0\x4c\x99\xc5\x62\x51\xbc\x7a\xe6\xb1\x78\x19\x45\x83\x83\xba\x24\xb5\xa4\xf9\xa7\x70\x9e\xdc\x51\xe9\x21\x0e\xfd\xdf\x9d\x6e\x9d\xdd\x48\xa7\x26\x63\x55\x62\x31\xb9\x4a\xc0\x86\x73\x4c\xdd\xc6\xa3\x8c\xc3\x79\xa9\x91\x26\xd3\x7d\x38\xcb\x1c\xbd\x69\xa9\xe8\xc8\x63\x51\xb1\x9d\xf2\xe9\xcd\x82\x82\xeb\x99\x99\xc6\x98\xab\xe3\x65\x3e\x95\x16\xbf\xb6\x3f\x8a\x9f\x9d\xd5\xcc\x43\xf6\x58\x55\xdf\xf2\x6a\x95\x57\x8f\xb0\x7a\x5a\x57\x5f\xd7\xd5\x13\x3c\xbf\x6e\xb3\x60\x78\xd0\x4c\x9b\x5c\xc5\x9e\x69\xe9\x6c\x8f\x37\x1e\xd2\xcb\x0f\xbe\xd1\x88\x2b\x9e\xac\x8c\x88\xf3\x33\xbc\xbe\xdd\x99\x9d\xc2\xe3\xfb\xae\xcb\xf8\x41\xd0\x17\x10\xfe\x1d\xfe\x05\x00\x00\xff\xff\x97\xa6\x98\xc4\x35\x04\x00\x00" + +func indexHtmlBytes() ([]byte, error) { + return bindataRead( + _indexHtml, + "index.html", + ) +} + +func indexHtml() (*asset, error) { + bytes, err := indexHtmlBytes() + if err != nil { + return nil, err + } + + info := bindataFileInfo{name: "index.html", size: 0, mode: os.FileMode(0), modTime: time.Unix(0, 0)} + a := &asset{bytes: bytes, info: info} + return a, nil +} + +var _styleCss = "\x1f\x8b\x08\x00\x00\x09\x6e\x88\x00\xff\xb4\x54\xd1\x6e\x9b\x30\x14\x7d\xcf\x57\x58\x9d\x26\xb5\x52\x5c\xd1\xb4\xe9\x5a\xfa\xb2\x69\x2f\x7b\xda\x3f\x18\xfb\x02\x77\x35\x36\xb2\x4d\xa0\x9b\xf2\xef\xb3\x81\x50\xa0\x90\xf6\x61\x0b\x0a\xc2\x36\x5c\x9f\x73\xee\xf1\xc9\x5d\x21\xb7\x24\xd1\xe2\x85\xfc\xd9\x10\xff\xab\x51\xb8\x3c\x26\x37\x51\xf4\xf9\xa9\x9d\xc8\x01\xb3\xdc\x8d\x67\x0a\x66\x32\x54\x31\x89\xba\x61\xc9\x84\x40\x95\x0d\xe3\x84\xf1\xe7\xcc\xe8\x4a\x89\x98\x7c\x82\xc7\x70\x75\x0b\xa9\x56\x8e\xa6\xac\x40\xf9\x12\x93\x6f\x06\x99\xdf\xf9\x07\xc8\x03\x38\xe4\x6c\x4b\x2c\x53\x96\x5a\x30\x98\x3e\x6d\x8e\x9b\x4d\x7e\xd3\x23\x72\xd0\x38\xca\x24\x66\x7e\x4b\x0e\xca\x81\x19\x95\xb3\xf8\x1b\x62\x72\x1f\x95\xee\x43\xd8\xba\x65\x9a\x68\xe7\x74\x11\x93\x5d\x54\x36\x4f\xcb\xb4\x27\x2c\xee\x38\x4b\xf7\x51\x0b\xab\x6c\x68\xc1\x78\x8e\x0a\xa8\x44\xeb\x7a\x8c\x02\x6d\x29\x99\xa7\x95\x4a\xe8\x2b\x86\x27\x2a\xd0\x00\x77\xa8\x03\x74\x2d\xab\x42\x75\x6b\x2d\x1b\x8a\x0e\x0a\x1b\x13\xeb\x0c\x38\x9e\x77\x2b\xbf\x2a\xeb\x30\x7d\xa1\xdc\x93\xf3\x5c\xa7\x8c\x0b\xd6\xd0\x1e\xe8\x43\xb4\x0e\xfd\xa4\x01\xab\x9c\x6e\x31\x5f\xeb\x03\x98\x03\x42\xbd\xd6\xe3\x44\x37\xd4\xe6\x4c\xe8\x3a\x26\xb7\x65\x33\xfc\x4d\x96\xb0\xcb\x68\xdb\x5e\xd7\xbb\xab\xb7\xca\xd4\xb9\x27\xb1\x2c\x6d\x80\x77\x56\x9b\x54\x86\xed\x8c\xae\x49\x6d\x58\xb9\x20\xcc\x98\xfa\x59\x5d\x86\x2e\xef\xc3\xa6\x13\xc2\x65\x4f\x79\xcd\x17\xfb\x93\x8a\x6f\x6d\xbe\xe4\xbb\x50\xdb\xb7\x9f\xfa\xcf\x0d\x58\xdb\x17\x9f\x18\xfb\xe2\xbb\xae\x0c\x82\x21\x3f\xa1\xbe\xd8\x92\x7e\xb4\x25\x85\x56\xda\x96\x8c\xc3\xc8\xbd\x75\xbf\x69\xa2\xa5\xe8\x8a\x97\x46\x67\xa3\xca\x7d\xa7\x76\xaf\xdd\x1e\x70\x0e\xc8\x97\x3d\x3e\xa2\x36\xbc\x70\x73\x9a\x49\xb4\x11\x60\xa8\x61\x02\x2b\x2f\xf4\x97\x61\x7e\x6c\x7a\xfe\x00\xf7\xfc\x71\x06\x4b\xe0\x61\x6e\xf9\x44\x6a\xfe\xfc\x2f\xb2\xe2\x74\xca\xc2\x42\x68\x60\x67\x90\x1c\x85\x00\xb5\x8e\x3b\xc0\xb3\x8e\xb9\x2a\x68\x16\x06\x70\xf0\xcd\x9a\x09\xf8\xf8\x7e\x9a\xbd\x9e\x96\xbe\xc0\x5b\xeb\x2c\xd8\xe6\x7f\x9d\x9c\x09\x0e\x6f\x1b\x35\xb6\xda\xd4\x36\xa3\x12\xe6\x44\xae\x6c\x42\x85\xaf\x05\x08\x64\xfe\x50\x49\xc2\x94\x20\x97\xf3\xfc\xb8\xea\x8b\x0e\x51\x3b\x8b\xd5\xbb\x21\x56\x8f\xed\xbd\xbd\x2d\xf8\x7f\x31\x53\x8e\xdd\xe9\x9f\x3b\x7a\xfa\xee\x49\xc6\xe3\x3b\x70\xf7\x1f\x81\x7b\x3b\xc0\x3d\xa3\xd4\x98\xc9\x2c\x16\x87\x64\x5a\x49\xed\xf1\xa7\xeb\xac\x76\x53\x56\x7f\x03\x00\x00\xff\xff\x84\x47\x3d\xe2\x61\x07\x00\x00" + +func styleCssBytes() ([]byte, error) { + return bindataRead( + _styleCss, + "style.css", + ) +} + +func styleCss() (*asset, error) { + bytes, err := styleCssBytes() + if err != nil { + return nil, err + } + + info := bindataFileInfo{name: "style.css", size: 0, mode: os.FileMode(0), modTime: time.Unix(0, 0)} + a := &asset{bytes: bytes, info: info} + return a, nil +} + +var _uiJs = "\x1f\x8b\x08\x00\x00\x09\x6e\x88\x00\xff\xb4\x8f\xb1\x0a\xc2\x40\x10\x44\xfb\xfb\x8a\xb3\xba\x04\x4c\x7e\x20\x95\x88\x60\x91\x74\xfe\xc0\xb9\xb7\xc4\xe0\xe6\x4e\xb2\x1b\x08\x48\xfe\xdd\x8d\x06\x14\x2b\x1b\xb7\xdc\x99\x37\xc3\xb8\x91\xd1\xb2\x0c\x1d\x88\xab\x8c\x01\xf2\xcc\xb6\xf1\x70\xe9\x22\xd6\x1d\x8b\xc5\x49\x30\x06\xb6\xc7\x53\x53\x1f\x08\x7b\x8c\x62\xef\xc6\xea\x79\x11\xf5\x61\xd8\x7b\xa2\xb3\x87\x6b\x96\xaf\xc2\x72\x90\x22\x27\xc2\x92\x52\x9b\xb9\xdd\xd8\x2e\x1c\x86\x8d\xcb\xab\xa7\x65\x36\xf3\x57\xd9\x3f\x8b\x42\x82\x71\xf9\x97\x03\xb6\xba\x09\x87\x35\x3f\x73\xb7\xa9\xe8\x5f\xfd\x05\xa9\xe2\xb6\x9f\xdb\x35\xe2\x17\xf2\x0d\x29\xf0\x08\x00\x00\xff\xff\x36\xf4\x16\xc4\x50\x01\x00\x00" + +func uiJsBytes() ([]byte, error) { + return bindataRead( + _uiJs, + "ui.js", + ) +} + +func uiJs() (*asset, error) { + bytes, err := uiJsBytes() + if err != nil { + return nil, err + } + + info := bindataFileInfo{name: "ui.js", size: 0, mode: os.FileMode(0), modTime: time.Unix(0, 0)} + a := &asset{bytes: bytes, info: info} + return a, nil +} + +// Asset loads and returns the asset for the given name. +// It returns an error if the asset could not be found or +// could not be loaded. +func Asset(name string) ([]byte, error) { + cannonicalName := strings.Replace(name, "\\", "/", -1) + if f, ok := _bindata[cannonicalName]; ok { + a, err := f() + if err != nil { + return nil, fmt.Errorf("Asset %s can't read by error: %v", name, err) + } + return a.bytes, nil + } + return nil, fmt.Errorf("Asset %s not found", name) +} + +// MustAsset is like Asset but panics when Asset would return an error. +// It simplifies safe initialization of global variables. +func MustAsset(name string) []byte { + a, err := Asset(name) + if err != nil { + panic("asset: Asset(" + name + "): " + err.Error()) + } + + return a +} + +// AssetInfo loads and returns the asset info for the given name. +// It returns an error if the asset could not be found or +// could not be loaded. +func AssetInfo(name string) (os.FileInfo, error) { + cannonicalName := strings.Replace(name, "\\", "/", -1) + if f, ok := _bindata[cannonicalName]; ok { + a, err := f() + if err != nil { + return nil, fmt.Errorf("AssetInfo %s can't read by error: %v", name, err) + } + return a.info, nil + } + return nil, fmt.Errorf("AssetInfo %s not found", name) +} + +// AssetNames returns the names of the assets. +func AssetNames() []string { + names := make([]string, 0, len(_bindata)) + for name := range _bindata { + names = append(names, name) + } + return names +} + +// _bindata is a table, holding each asset generator, mapped to its name. +var _bindata = map[string]func() (*asset, error){ + "index.html": indexHtml, + "style.css": styleCss, + "ui.js": uiJs, +} + +// AssetDir returns the file names below a certain +// directory embedded in the file by go-bindata. +// For example if you run go-bindata on data/... and data contains the +// following hierarchy: +// data/ +// foo.txt +// img/ +// a.png +// b.png +// then AssetDir("data") would return []string{"foo.txt", "img"} +// AssetDir("data/img") would return []string{"a.png", "b.png"} +// AssetDir("foo.txt") and AssetDir("notexist") would return an error +// AssetDir("") will return []string{"data"}. +func AssetDir(name string) ([]string, error) { + node := _bintree + if len(name) != 0 { + cannonicalName := strings.Replace(name, "\\", "/", -1) + pathList := strings.Split(cannonicalName, "/") + for _, p := range pathList { + node = node.Children[p] + if node == nil { + return nil, fmt.Errorf("Asset %s not found", name) + } + } + } + if node.Func != nil { + return nil, fmt.Errorf("Asset %s not found", name) + } + rv := make([]string, 0, len(node.Children)) + for childName := range node.Children { + rv = append(rv, childName) + } + return rv, nil +} + +type bintree struct { + Func func() (*asset, error) + Children map[string]*bintree +} +var _bintree = &bintree{nil, map[string]*bintree{ + "index.html": &bintree{indexHtml, map[string]*bintree{}}, + "style.css": &bintree{styleCss, map[string]*bintree{}}, + "ui.js": &bintree{uiJs, map[string]*bintree{}}, +}} + +// RestoreAsset restores an asset under the given directory +func RestoreAsset(dir, name string) error { + data, err := Asset(name) + if err != nil { + return err + } + info, err := AssetInfo(name) + if err != nil { + return err + } + err = os.MkdirAll(_filePath(dir, filepath.Dir(name)), os.FileMode(0755)) + if err != nil { + return err + } + err = ioutil.WriteFile(_filePath(dir, name), data, info.Mode()) + if err != nil { + return err + } + err = os.Chtimes(_filePath(dir, name), info.ModTime(), info.ModTime()) + if err != nil { + return err + } + return nil +} + +// RestoreAssets restores an asset under the given directory recursively +func RestoreAssets(dir, name string) error { + children, err := AssetDir(name) + // File + if err != nil { + return RestoreAsset(dir, name) + } + // Dir + for _, child := range children { + err = RestoreAssets(dir, filepath.Join(name, child)) + if err != nil { + return err + } + } + return nil +} + +func _filePath(dir, name string) string { + cannonicalName := strings.Replace(name, "\\", "/", -1) + return filepath.Join(append([]string{dir}, strings.Split(cannonicalName, "/")...)...) +} + diff --git a/pixiecore/ui/index.html b/pixiecore/ui/index.html new file mode 100644 index 0000000..b19d603 --- /dev/null +++ b/pixiecore/ui/index.html @@ -0,0 +1,34 @@ + + +
+ + +{{ $mac }}
+{{ (index $events (dec (len $events))).State }}
++ + {{ $event.Message }} +
+ {{- end }} +