Skip to content

Commit

Permalink
feat: implement basic pxe serving
Browse files Browse the repository at this point in the history
  • Loading branch information
teodor-pripoae committed Nov 30, 2024
1 parent ef544db commit 00ff399
Show file tree
Hide file tree
Showing 10 changed files with 234 additions and 4 deletions.
2 changes: 1 addition & 1 deletion MODULE.bazel
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ go_sdk.download(version = "1.23.3")

go_deps = use_extension("@bazel_gazelle//:extensions.bzl", "go_deps")
go_deps.from_file(go_mod = "//:go.mod")
use_repo(go_deps, "com_github_pin_tftp_v3", "com_github_sirupsen_logrus", "com_github_spf13_cobra")
use_repo(go_deps, "com_github_gorilla_mux", "com_github_pin_tftp_v3", "com_github_sirupsen_logrus", "com_github_spf13_cobra")

###############################################################################
# RPM
Expand Down
1 change: 1 addition & 0 deletions go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ module toni.systems/goisoboot
go 1.22.0

require (
github.com/gorilla/mux v1.8.1
github.com/pin/tftp/v3 v3.1.0
github.com/sirupsen/logrus v1.9.3
github.com/spf13/cobra v1.8.1
Expand Down
2 changes: 2 additions & 0 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@ github.com/cpuguy83/go-md2man/v2 v2.0.4/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46t
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/gorilla/mux v1.8.1 h1:TuBL49tXwgrFYWhqrNgrUNEY92u81SPhu7sTdzQEiWY=
github.com/gorilla/mux v1.8.1/go.mod h1:AKf9I4AEqPTmMytcMc0KkNouC66V3BtZ4qD5fmWSiMQ=
github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8=
github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw=
github.com/pin/tftp/v3 v3.1.0 h1:rQaxd4pGwcAJnpId8zC+O2NX3B2/NscjDZQaqEjuE7c=
Expand Down
1 change: 1 addition & 0 deletions pkg/cli/BUILD.bazel
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ go_library(
importpath = "toni.systems/goisoboot/pkg/cli",
visibility = ["//visibility:public"],
deps = [
"//pkg/ipxe",
"//pkg/tftp",
"@com_github_spf13_cobra//:cobra",
],
Expand Down
48 changes: 45 additions & 3 deletions pkg/cli/server.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,15 +5,56 @@ import (
"os"

"github.com/spf13/cobra"
"toni.systems/goisoboot/pkg/ipxe"
"toni.systems/goisoboot/pkg/tftp"
)

func quit(message string) {
fmt.Printf("Error: %s\n", message)
os.Exit(1)
}

func server(cmd *cobra.Command, args []string) {
serverIP, err := cmd.Flags().GetString("server-ip")
if err != nil {
quit(err.Error())
} else if serverIP == "" {
quit("server-ip is required")
}

httpPort, err := cmd.Flags().GetInt("http-port")
if err != nil {
quit(err.Error())
}

tftp := tftp.New()
server, err := ipxe.New(
ipxe.WithIP(serverIP),
ipxe.WithPort(httpPort),
)

errChan := make(chan error)
done := make(chan struct{})

go func() {
if err := tftp.Run(); err != nil {
errChan <- err
}
done <- struct{}{}
}()

go func() {
if err := server.Run(); err != nil {
errChan <- err
}
done <- struct{}{}
}()

if err := tftp.Run(); err != nil {
fmt.Printf("Error: %v\n", err)
os.Exit(1)
select {
case err := <-errChan:
quit(err.Error())
case <-done:
fmt.Println("Server stopped")
}
}

Expand All @@ -25,6 +66,7 @@ func NewServerCmd() *cobra.Command {
Run: server,
}

cmd.Flags().StringP("server-ip", "i", "", "Server IP")
cmd.Flags().IntP("http-port", "p", 8080, "HTTP Port")

return cmd
Expand Down
17 changes: 17 additions & 0 deletions pkg/ipxe/BUILD.bazel
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
load("@rules_go//go:def.bzl", "go_library")

go_library(
name = "ipxe",
srcs = [
"handlers.go",
"options.go",
"routes.go",
"server.go",
],
importpath = "toni.systems/goisoboot/pkg/ipxe",
visibility = ["//visibility:public"],
deps = [
"@com_github_gorilla_mux//:mux",
"@com_github_sirupsen_logrus//:logrus",
],
)
53 changes: 53 additions & 0 deletions pkg/ipxe/handlers.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
package ipxe

import (
"net/http"
"text/template"

"github.com/gorilla/mux"
)

var (
ipxeTemplate = `#!ipxe
kernel http://{{ .serverIP }}/linux/{{ .name }}/vmlinuz ip=dhcp initrd=initrd
initrd http://{{ .serverIP }}/linux/{{ .name }}/initrd
boot`
)

func (s *server) IPXE(w http.ResponseWriter, r *http.Request) {
tmpl, err := template.New("ipxe").Parse(ipxeTemplate)
if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
}

vars := s.getIPXEVars(r)

err = tmpl.Execute(w, vars)
if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
}
}

func (s *server) Kernel(w http.ResponseWriter, r *http.Request) {
http.ServeFile(w, r, "./linux/"+mux.Vars(r)["name"]+"/vmlinuz")
}

func (s *server) Initrd(w http.ResponseWriter, r *http.Request) {
http.ServeFile(w, r, "./linux/"+mux.Vars(r)["name"]+"/initrd")
}

func (s *server) Squashfs(w http.ResponseWriter, r *http.Request) {
http.ServeFile(w, r, "./linux/"+mux.Vars(r)["name"]+"/squashfs")
}

func (s *server) getIPXEVars(r *http.Request) map[string]string {
name := "example"

vars := make(map[string]string)

vars["ip"] = r.RemoteAddr
vars["serverIP"] = s.ip
vars["name"] = name

return vars
}
15 changes: 15 additions & 0 deletions pkg/ipxe/options.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
package ipxe

type Option func(*server)

func WithPort(port int) Option {
return func(s *server) {
s.port = port
}
}

func WithIP(ip string) Option {
return func(s *server) {
s.ip = ip
}
}
15 changes: 15 additions & 0 deletions pkg/ipxe/routes.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
package ipxe

import (
"github.com/gorilla/mux"
)

func router(server Server) *mux.Router {
r := mux.NewRouter()
r.HandleFunc("/ipxe", server.IPXE).Methods("GET")
r.HandleFunc("/linux/{name}/vmlinuz", server.Kernel).Methods("GET")
r.HandleFunc("/linux/{name}/initrd", server.Initrd).Methods("GET")
r.HandleFunc("/linux/{name}/squashfs", server.Squashfs).Methods("GET")

return r
}
84 changes: 84 additions & 0 deletions pkg/ipxe/server.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,84 @@
package ipxe

import (
"bytes"
"errors"
"fmt"
"net/http"
"text/template"
"time"

"github.com/gorilla/mux"
log "github.com/sirupsen/logrus"
)

var (
ErrMissingIP = errors.New("missing IP")
ErrMissingPort = errors.New("missing port")

dhcpTemplate = `
if exists user-class and option user-class = "iPXE" {
filename "http://{{ .serverIP }}/ipxe";
} else {
filename "ipxe.efi";
}`
)

type Server interface {
Run() error
IPXE(w http.ResponseWriter, r *http.Request)
Kernel(w http.ResponseWriter, r *http.Request)
Initrd(w http.ResponseWriter, r *http.Request)
Squashfs(w http.ResponseWriter, r *http.Request)
}

type server struct {
ip string
port int
router *mux.Router
}

func New(options ...Option) (Server, error) {
s := &server{}

s.router = router(s)

for _, o := range options {
o(s)
}

if s.ip == "" {
return nil, ErrMissingIP
}
if s.port == 0 {
return nil, ErrMissingPort
}

return s, nil
}

func (s *server) Run() error {
addr := fmt.Sprintf(":%d", s.port)

srv := &http.Server{
Addr: addr,
WriteTimeout: time.Second * 15,
ReadTimeout: time.Second * 15,
IdleTimeout: time.Second * 60,
Handler: s.router,
}

log.Infof("Starting HTTP server on %s", addr)
s.printInfo()

return srv.ListenAndServe()
}

func (s *server) printInfo() {
log.Infof("Setup ISC DHCP Server with the following configuration:")
tmpl, _ := template.New("dhcp").Parse(dhcpTemplate)
var buff bytes.Buffer
_ = tmpl.Execute(&buff, map[string]string{"serverIP": s.ip})

log.Infof(buff.String())
}

0 comments on commit 00ff399

Please sign in to comment.