diff --git a/.gitignore b/.gitignore
index a18673c..175e579 100644
--- a/.gitignore
+++ b/.gitignore
@@ -1,3 +1,5 @@
ipxeblue
vendors
-.podman-data
\ No newline at end of file
+.podman-data
+!tftp/.gitkeep
+tftp/
diff --git a/Dockerfile b/Dockerfile
index 39fd29e..95fb5d5 100644
--- a/Dockerfile
+++ b/Dockerfile
@@ -13,7 +13,7 @@ RUN CGO_ENABLED=0 GOOS=linux go build -ldflags="-w -s" -o /go/bin/ipxeblue -mod
############################
# STEP 2 build webui
############################
-FROM node:lts-buster as builderui
+FROM node:16-bullseye as builderui
WORKDIR /webui/
diff --git a/README.md b/README.md
index 38d910e..0c7fa6a 100644
--- a/README.md
+++ b/README.md
@@ -32,6 +32,12 @@ Supported environment variables
- `MINIO_SECURE`
- `MINIO_BUCKETNAME`
- default: `ipxeblue`
+- `GRUB_SUPPORT_ENABLED`
+ - default: `False`
+- `TFTP_ENABLED`
+ - default: `False`
+- `DEFAULT_BOOTENTRY_NAME`
+ - default: ``
## DHCP or ipxe config for connection to ipxeblue
@@ -44,7 +50,7 @@ set crosscert http://ca.ipxe.org/auto
chain https://USERNAME:PASSWORD@FQDN/?asset=${asset}&buildarch=${buildarch}&hostname=${hostname}&mac=${mac:hexhyp}&ip=${ip}&manufacturer=${manufacturer}&platform=${platform}&product=${product}&serial=${serial}&uuid=${uuid}&version=${version}
```
-For isc-dhcp-server
+### For isc-dhcp-server
you need to set `iPXE-specific options` see https://ipxe.org/howto/dhcpd
@@ -64,6 +70,61 @@ you need to set `iPXE-specific options` see https://ipxe.org/howto/dhcpd
next-server 10.123.123.123;
```
+### For kea-dhcp-server
+```text
+"Dhcp4": {
+ ...
+ "option-def": [
+ { "space": "dhcp4", "name": "ipxe-encap-opts", "code": 175, "type": "empty", "array": false, "record-types": "", "encapsulate": "ipxe" },
+ { "space": "ipxe", "name": "crosscert", "code": 93, "type": "string" },
+ { "space": "ipxe", "name": "username", "code": 190, "type": "string" },
+ { "space": "ipxe", "name": "password", "code": 191, "type": "string" }
+ ],
+ "client-classes": [
+ {
+ "name": "XClient_iPXE",
+ "test": "substring(option[77].hex,0,4) == 'iPXE'",
+ "boot-file-name": "ipxeblue.ipxe",
+ "option-data": [
+ { "space": "dhcp4", "name": "ipxe-encap-opts", "code": 175 },
+ { "space": "ipxe", "name": "crosscert", "data": "http://ca.ipxe.org/auto" },
+ { "space": "ipxe", "name": "username", "data": "demo" },
+ { "space": "ipxe", "name": "password", "data": "demo" }
+ ]
+ },
+ {
+ "name": "UEFI-64",
+ "test": "substring(option[60].hex,0,20) == 'PXEClient:Arch:00007'",
+ "boot-file-name": "snponly.efi"
+ },
+ {
+ "name": "Legacy",
+ "test": "substring(option[60].hex,0,20) == 'PXEClient:Arch:00000'",
+ "boot-file-name": "undionly.kpxe"
+ }
+ ],
+ "subnet4": [
+ {
+ ...
+ "next-server": "10.123.123.123",
+ ...
+ }
+ ]
+ ...
+}
+```
+
+### Grub over PXE:
+> Secure Boot supported with signed binaries
+
+/srv/tftp/bootx64.efi (sha256sum 8c885fa9886ab668da267142c7226b8ce475e682b99e4f4afc1093c5f77ce275)
+/srv/tftp/grubx64.efi (sha256sum d0d6d85f44a0ffe07d6a856ad5a1871850c31af17b7779086b0b9384785d5449)
+/srv/tftp/grub/grub.cfg
+```text
+insmod http
+source (http,192.168.32.7)/grub/
+```
+
## screenshots
![Computer List](docs/images/computer-list.png?raw=true "Computer List")
diff --git a/controllers/grub.go b/controllers/grub.go
new file mode 100644
index 0000000..92af8a5
--- /dev/null
+++ b/controllers/grub.go
@@ -0,0 +1,132 @@
+package controllers
+
+import (
+ "bytes"
+ "fmt"
+ "github.com/aarnaud/ipxeblue/models"
+ "github.com/aarnaud/ipxeblue/utils"
+ "github.com/gin-gonic/gin"
+ "github.com/google/uuid"
+ "github.com/jackc/pgtype"
+ "gorm.io/gorm"
+ "net/http"
+ "text/template"
+)
+
+func GrubScript(c *gin.Context) {
+ config := c.MustGet("config").(*utils.Config)
+
+ // basic check or reply with ipxe chain
+ _, uuidExist := c.GetQuery("uuid")
+ _, macExist := c.GetQuery("mac")
+ _, ipExist := c.GetQuery("ip")
+ if !uuidExist || !macExist || !ipExist {
+ baseURL := *config.BaseURL
+ // use the same scheme from request to generate URL
+ if schem := c.Request.Header.Get("X-Forwarded-Proto"); schem != "" {
+ baseURL.Scheme = schem
+ }
+ c.HTML(http.StatusOK, "grub_index.gohtml", gin.H{
+ "BaseURL": config.BaseURL.String(),
+ "Scheme": config.BaseURL.Scheme,
+ "Host": config.BaseURL.Host,
+ })
+ return
+ }
+
+ // process query params
+ db := c.MustGet("db").(*gorm.DB)
+ id, err := uuid.Parse(c.Query("uuid"))
+ if err != nil {
+ c.AbortWithStatusJSON(http.StatusNotAcceptable, gin.H{
+ "error": err.Error(),
+ })
+ return
+ }
+
+ mac := pgtype.Macaddr{}
+ err = mac.DecodeText(nil, []byte(c.Query("mac")))
+ if err != nil {
+ c.AbortWithStatusJSON(http.StatusNotAcceptable, gin.H{
+ "error": err.Error(),
+ })
+ return
+ }
+
+ ip := pgtype.Inet{}
+ err = ip.DecodeText(nil, []byte(c.Query("ip")))
+ if err != nil {
+ c.AbortWithStatusJSON(http.StatusNotAcceptable, gin.H{
+ "error": err.Error(),
+ })
+ return
+ }
+
+ computer := updateOrCreateComputer(c, id, mac, ip)
+ // Add computer in gin context to use it in template function
+ c.Set("computer", &computer)
+
+ c.Header("Content-Type", "text/plain; charset=utf-8")
+ bootorder := models.Bootorder{}
+ result := db.Preload("Bootentry").Preload("Bootentry.Files").
+ Where("computer_uuid = ?", computer.Uuid).Order("bootorders.order").First(&bootorder)
+ if result.RowsAffected == 0 {
+ c.HTML(http.StatusOK, "grub_empty.gohtml", gin.H{})
+ return
+ }
+ bootentry := bootorder.Bootentry
+
+ // Create template name by the uuid
+ tpl := template.New(bootentry.Uuid.String())
+ // provide a func in the FuncMap which can access tpl to be able to look up templates
+ tpl.Funcs(utils.GetCustomFunctions(c, tpl))
+
+ tpl, err = tpl.Parse(bootentry.GrupScript)
+ if err != nil {
+ c.AbortWithStatusJSON(http.StatusInternalServerError, gin.H{
+ "error": err.Error(),
+ })
+ return
+ }
+
+ writer := bytes.NewBuffer([]byte{})
+ writer.Write([]byte("set timeout=2\n"))
+ writer.Write([]byte(fmt.Sprintf("set prefix=(http,%s)\n", config.BaseURL.Host)))
+ writer.Write([]byte(fmt.Sprintf("echo 'Booting %s'\n", bootentry.Description)))
+
+ // if bootentry selected is menu load all bootentries as template
+ if bootentry.Name == "menu" {
+ bootentries := make([]models.Bootentry, 0)
+ db.Preload("Files").Where("name != 'menu'").Find(&bootentries)
+ for _, be := range bootentries {
+ // test if empty
+ tpl.New(be.Uuid.String()).Parse(be.GrupScript)
+ }
+ err = tpl.ExecuteTemplate(writer, bootentry.Uuid.String(), gin.H{
+ "Computer": computer,
+ "Bootentry": bootentry,
+ "Bootentries": bootentries,
+ })
+ if err != nil {
+ c.AbortWithStatusJSON(http.StatusInternalServerError, gin.H{
+ "error": err.Error(),
+ })
+ return
+ }
+ } else {
+ err = tpl.ExecuteTemplate(writer, bootentry.Uuid.String(), bootentry)
+ if err != nil {
+ c.AbortWithStatusJSON(http.StatusInternalServerError, gin.H{
+ "error": err.Error(),
+ })
+ return
+ }
+ }
+
+ // reset bootentry if not persistent
+ if !*bootentry.Persistent {
+ db.Model(&bootorder).Delete(&bootorder)
+ }
+
+ c.Data(http.StatusOK, "text/plain", writer.Bytes())
+}
diff --git a/controllers/ipxescript.go b/controllers/ipxescript.go
index 0d270fa..87e0ae6 100644
--- a/controllers/ipxescript.go
+++ b/controllers/ipxescript.go
@@ -21,34 +21,67 @@ import (
)
func updateOrCreateComputer(c *gin.Context, id uuid.UUID, mac pgtype.Macaddr, ip pgtype.Inet) models.Computer {
+ config := c.MustGet("config").(*utils.Config)
var computer models.Computer
var err error
db := c.MustGet("db").(*gorm.DB)
+ /*
+ a = asset
+ m = manufacturer
+ p = product
+ f = family
+ sn = serial
+ uuid = uuid
+ c = cpu_arch
+ t = platform
+ h = hostname
+ v = version
+ */
+
// auto set name based on hostname or asset for new computer
name := c.DefaultQuery("hostname", "")
if name == "" {
- name = c.DefaultQuery("asset", "")
+ name = c.DefaultQuery("asset", c.DefaultQuery("a", ""))
+ }
+ if name == "" {
+ name = c.DefaultQuery("serial", c.DefaultQuery("sn", ""))
}
computer, err = searchComputer(db, id, mac)
+ var accountID *string
+ if value, ok := c.Get("account"); ok {
+ accountID = &value.(*models.Ipxeaccount).Username
+ }
if err != nil {
+ // Default bootentry for new computer
+ bootorder := make([]*models.Bootorder, 0)
+ if config.DefaultBootentryName != "" {
+ defaultBootentry := &models.Bootentry{}
+ result := db.Where("name = ?", config.DefaultBootentryName).Find(&defaultBootentry)
+ if result.RowsAffected != 0 {
+ bootorder = append(bootorder, &models.Bootorder{
+ BootentryUuid: defaultBootentry.Uuid,
+ })
+ }
+ }
computer = models.Computer{
Name: name,
- Asset: c.DefaultQuery("asset", ""),
- BuildArch: c.DefaultQuery("buildarch", ""),
- Hostname: c.DefaultQuery("hostname", ""),
+ Asset: c.DefaultQuery("asset", c.DefaultQuery("a", "")),
+ BuildArch: c.DefaultQuery("buildarch", c.DefaultQuery("c", "")),
+ Hostname: c.DefaultQuery("hostname", c.DefaultQuery("h", "")),
LastSeen: time.Now(),
Mac: mac,
IP: ip,
- Manufacturer: c.DefaultQuery("manufacturer", ""),
- Platform: c.DefaultQuery("platform", ""),
- Product: c.DefaultQuery("product", ""),
- Serial: c.DefaultQuery("serial", ""),
+ Manufacturer: c.DefaultQuery("manufacturer", c.DefaultQuery("m", "")),
+ Platform: c.DefaultQuery("platform", c.DefaultQuery("t", "")),
+ Product: c.DefaultQuery("product", c.DefaultQuery("p", "")),
+ Serial: c.DefaultQuery("serial", c.DefaultQuery("sn", "")),
Uuid: id,
- Version: c.DefaultQuery("version", ""),
- LastIpxeaccountID: c.MustGet("account").(*models.Ipxeaccount).Username,
+ Version: c.DefaultQuery("version", c.DefaultQuery("v", "")),
+ LastIpxeaccountID: accountID,
+ Bootorder: bootorder,
}
db.FirstOrCreate(&computer)
}
@@ -59,18 +92,18 @@ func updateOrCreateComputer(c *gin.Context, id uuid.UUID, mac pgtype.Macaddr, ip
}
if time.Now().Sub(computer.LastSeen).Seconds() > 10 {
- computer.Asset = c.DefaultQuery("asset", "")
- computer.BuildArch = c.DefaultQuery("buildarch", "")
+ computer.Asset = c.DefaultQuery("asset", c.DefaultQuery("a", ""))
+ computer.BuildArch = c.DefaultQuery("buildarch", c.DefaultQuery("c", ""))
computer.Hostname = c.DefaultQuery("hostname", "")
computer.LastSeen = time.Now()
computer.Mac = mac
computer.IP = ip
- computer.Manufacturer = c.DefaultQuery("manufacturer", "")
- computer.Platform = c.DefaultQuery("platform", "")
- computer.Product = c.DefaultQuery("product", "")
- computer.Serial = c.DefaultQuery("serial", "")
- computer.Version = c.DefaultQuery("version", "")
- computer.LastIpxeaccountID = c.MustGet("account").(*models.Ipxeaccount).Username
+ computer.Manufacturer = c.DefaultQuery("manufacturer", c.DefaultQuery("m", ""))
+ computer.Platform = c.DefaultQuery("platform", c.DefaultQuery("t", ""))
+ computer.Product = c.DefaultQuery("product", c.DefaultQuery("p", ""))
+ computer.Serial = c.DefaultQuery("serial", c.DefaultQuery("sn", ""))
+ computer.Version = c.DefaultQuery("version", c.DefaultQuery("v", ""))
+ computer.LastIpxeaccountID = accountID
db.Save(computer)
}
diff --git a/controllers/tftp.go b/controllers/tftp.go
new file mode 100644
index 0000000..b55732e
--- /dev/null
+++ b/controllers/tftp.go
@@ -0,0 +1,110 @@
+package controllers
+
+import (
+ "errors"
+ "fmt"
+ "github.com/aarnaud/ipxeblue/utils"
+ "github.com/pin/tftp/v3"
+ "github.com/rs/zerolog/log"
+ "gorm.io/gorm"
+ "io"
+ "net/http"
+ "net/url"
+ "os"
+ "path/filepath"
+ "strings"
+)
+
+func GetTFTPReader(config *utils.Config, db *gorm.DB) func(filename string, rf io.ReaderFrom) error {
+ folder := "tftp"
+ return func(filename string, rf io.ReaderFrom) error {
+ filename = strings.TrimRight(filename, "�")
+ raddr := rf.(tftp.OutgoingTransfer).RemoteAddr()
+ log.Info().Msgf("RRQ from %s filename %s", raddr.String(), filename)
+
+ path := filepath.Join(folder, filename)
+
+ // if file doesn't exist and path start by /grub/ use grubTFTP2HTTP
+ if _, err := os.Stat(path); errors.Is(err, os.ErrNotExist) && strings.HasPrefix(filename, "/grub/") {
+ return grubTFTP2HTTP(config, db, filename, rf)
+ }
+
+ file, err := os.Open(path)
+ if err != nil {
+ log.Error().Err(err)
+ return err
+ }
+ stat, err := file.Stat()
+ if err != nil {
+ log.Error().Err(err)
+ return err
+ }
+ rf.(tftp.OutgoingTransfer).SetSize(stat.Size())
+ _, err = rf.ReadFrom(file)
+ if err != nil {
+ log.Error().Err(err)
+ return err
+ }
+ return nil
+ }
+}
+
+func GetTFTPWriter(config *utils.Config) func(filename string, wt io.WriterTo) error {
+ return func(filename string, wt io.WriterTo) error {
+ return nil
+ }
+}
+
+func grubTFTP2HTTP(config *utils.Config, db *gorm.DB, filename string, rf io.ReaderFrom) error {
+ gruburl, _ := url.Parse(config.BaseURL.String())
+ if filename == "/grub/grub.cfg" {
+ gruburl = gruburl.JoinPath("/grub/")
+ resp, err := http.Get(gruburl.String())
+ if err != nil {
+ return err
+ }
+ defer resp.Body.Close()
+ b, err := io.ReadAll(resp.Body)
+ if err != nil {
+ return err
+ }
+ reader := strings.NewReader(string(b))
+ _, err = rf.ReadFrom(reader)
+ if err != nil {
+ return err
+ }
+ return nil
+ }
+
+ paths := strings.Split(filename, "/")
+ if len(paths) < 11 {
+ return fmt.Errorf("invalid path")
+ }
+ gruburl = gruburl.JoinPath("/grub/")
+ query := gruburl.Query()
+ query.Add("mac", paths[2])
+ query.Add("ip", paths[3])
+ query.Add("uuid", paths[4])
+ query.Add("asset", strings.TrimSpace(strings.TrimLeft(paths[5], "-")))
+ query.Add("manufacturer", strings.TrimLeft(paths[6], "-"))
+ query.Add("serial", strings.TrimLeft(paths[7], "-"))
+ query.Add("product", strings.TrimLeft(paths[8], "-"))
+ query.Add("buildarch", strings.TrimLeft(paths[9], "-"))
+ query.Add("platform", strings.TrimLeft(paths[10], "-"))
+ gruburl.RawQuery = query.Encode()
+ resp, err := http.Get(gruburl.String())
+ if err != nil {
+ return err
+ }
+ defer resp.Body.Close()
+ b, err := io.ReadAll(resp.Body)
+ if err != nil {
+ return err
+ }
+ reader := strings.NewReader(string(b))
+ _, err = rf.ReadFrom(reader)
+ if err != nil {
+ return err
+ }
+ return nil
+}
diff --git a/go.mod b/go.mod
index 02de7d4..3a335ba 100644
--- a/go.mod
+++ b/go.mod
@@ -11,6 +11,7 @@ require (
github.com/google/uuid v1.1.2
github.com/jackc/pgtype v1.5.0
github.com/minio/minio-go/v7 v7.0.6
+ github.com/pin/tftp/v3 v3.0.0
github.com/pkg/errors v0.9.1 // indirect
github.com/rs/zerolog v1.16.0
github.com/spf13/viper v1.7.1
diff --git a/go.sum b/go.sum
index f06effe..e4259c0 100644
--- a/go.sum
+++ b/go.sum
@@ -306,6 +306,8 @@ github.com/oklog/ulid v1.3.1/go.mod h1:CirwcVhetQ6Lv90oh/F+FBtV6XMibvdAFo93nm5qn
github.com/pascaldekloe/goe v0.0.0-20180627143212-57f6aae5913c/go.mod h1:lzWF7FIEvWOWxwDKqyGYQf6ZUaNfKdP144TG7ZOy1lc=
github.com/pelletier/go-toml v1.2.0 h1:T5zMGML61Wp+FlcbWjRDT7yAxhJNAiPPLOFECq181zc=
github.com/pelletier/go-toml v1.2.0/go.mod h1:5z9KED0ma1S8pY6P1sdut58dfprrGBbd/94hg7ilaic=
+github.com/pin/tftp/v3 v3.0.0 h1:o9cQpmWBSbgiaYXuN+qJAB12XBIv4dT7OuOONucn2l0=
+github.com/pin/tftp/v3 v3.0.0/go.mod h1:xwQaN4viYL019tM4i8iecm++5cGxSqen6AJEOEyEI0w=
github.com/pkg/errors v0.8.0/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
@@ -453,6 +455,7 @@ golang.org/x/net v0.0.0-20190611141213-3f473d35a33a/go.mod h1:z5CRVTTTmAJ677TzLL
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20190813141303-74dc4d7220e7/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20190827160401-ba9fcec4b297/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
+golang.org/x/net v0.0.0-20200202094626-16171245cfb2/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20200707034311-ab3426394381/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA=
golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU=
golang.org/x/net v0.0.0-20201110031124-69a78807bb2b/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU=
diff --git a/main.go b/main.go
index 1257021..1cb1686 100644
--- a/main.go
+++ b/main.go
@@ -9,11 +9,13 @@ import (
"github.com/gin-contrib/cors"
"github.com/gin-contrib/logger"
"github.com/gin-gonic/gin"
+ "github.com/pin/tftp/v3"
"github.com/rs/zerolog"
"github.com/rs/zerolog/log"
swaggerFiles "github.com/swaggo/files"
ginSwagger "github.com/swaggo/gin-swagger"
"net/http"
+ "os"
"time"
)
@@ -82,6 +84,26 @@ func main() {
c.JSON(http.StatusOK, gin.H{})
})
+ if appconf.GrubSupportEnabled {
+ // Grub request without auth
+ log.Info().Msg("enabling grub http endpoint")
+ router.GET("/grub/", controllers.GrubScript)
+ }
+
+ // grub don't support long http queries
+ // using TFTP to pass positional metadata in tftp path
+ if appconf.TFTPEnabled {
+ s := tftp.NewServer(controllers.GetTFTPReader(appconf, db), controllers.GetTFTPWriter(appconf))
+ s.SetTimeout(30 * time.Second)
+ go func() {
+ log.Info().Msg("starting tftp server")
+ err := s.ListenAndServe(":69")
+ if err != nil {
+ fmt.Fprintf(os.Stdout, "server: %v\n", err)
+ }
+ }()
+ }
+
// iPXE request with auth
ipxeroute := router.Group("/", midlewares.BasicAuthIpxeAccount(false))
ipxeroute.GET("/", controllers.IpxeScript)
@@ -122,5 +144,6 @@ func main() {
v1.POST("/bootentries/:uuid/files/:name", controllers.UploadBootentryFile)
v1.GET("/bootentries/:uuid/files/:name", controllers.DownloadBootentryFile)
+ log.Info().Msg("starting http server")
router.Run(fmt.Sprintf(":%d", appconf.Port))
}
diff --git a/models/bootentry.go b/models/bootentry.go
index cd16671..447a7bc 100644
--- a/models/bootentry.go
+++ b/models/bootentry.go
@@ -14,6 +14,7 @@ type Bootentry struct {
Description string `json:"description"`
Persistent *bool `gorm:"not null;default:FALSE" json:"persistent"`
IpxeScript string `json:"ipxe_script"`
+ GrupScript string `json:"grub_script"`
Files []BootentryFile `gorm:"foreignkey:bootentry_uuid;constraint:OnUpdate:CASCADE,OnDelete:CASCADE;" json:"files"`
Bootorder []*Bootorder `gorm:"foreignKey:bootentry_uuid;constraint:OnUpdate:CASCADE,OnDelete:CASCADE;" json:"-"`
CreatedAt time.Time `json:"created_at"`
diff --git a/models/computer.go b/models/computer.go
index 936bcff..da40406 100644
--- a/models/computer.go
+++ b/models/computer.go
@@ -23,7 +23,7 @@ type Computer struct {
Version string `json:"version"`
Tags []*Tag `gorm:"foreignkey:computer_uuid;constraint:OnUpdate:CASCADE,OnDelete:CASCADE;" json:"tags"`
Bootorder []*Bootorder `gorm:"foreignKey:computer_uuid;constraint:OnUpdate:CASCADE,OnDelete:CASCADE;" json:"-"`
- LastIpxeaccountID string `json:"last_ipxeaccount"`
+ LastIpxeaccountID *string `json:"last_ipxeaccount"`
LastIpxeaccount *Ipxeaccount `gorm:"foreignkey:last_ipxeaccount_id;References:Username;constraint:OnUpdate:CASCADE,OnDelete:SET NULL;default:NULL" json:"-"`
CreatedAt time.Time `json:"created_at"`
UpdatedAt time.Time `json:"updated_at"`
diff --git a/templates/grub_empty.gohtml b/templates/grub_empty.gohtml
new file mode 100644
index 0000000..0dd977e
--- /dev/null
+++ b/templates/grub_empty.gohtml
@@ -0,0 +1,3 @@
+echo "INFO: empty bootorder on this computer, boot on next device"
+sleep 1
+exit 0
\ No newline at end of file
diff --git a/templates/grub_index.gohtml b/templates/grub_index.gohtml
new file mode 100644
index 0000000..787ea3c
--- /dev/null
+++ b/templates/grub_index.gohtml
@@ -0,0 +1,24 @@
+echo
+echo
+echo .###.########.##.....#.#######.########.##......##.....#.########
+echo ..##.##.....#..##...##.##......##.....#.##......##.....#.##......
+echo ..##.##.....#...##.##..##......##.....#.##......##.....#.##......
+echo ..##.########....###...######..########.##......##.....#.######..
+echo ..##.##.........##.##..##......##.....#.##......##.....#.##......
+echo ..##.##........##...##.##......##.....#.##......##.....#.##......
+echo .###.##.......##.....#.#######.########.#######..#######.########
+echo
+echo
+
+echo "loading ipxe from {{ .BaseURL }}"
+sleep 2
+
+smbios --type 1 --get-string 4 --set smbios_manufacturer
+smbios --type 1 --get-string 5 --set smbios_product
+smbios --type 1 --get-string 7 --set smbios_serial
+smbios --type 1 --get-uuid 8 --set smbios_uuid
+smbios --type 2 --get-string 8 --set smbios_asset
+insmod http
+
+echo "(tftp)/grub/$net_default_mac/$net_default_ip/$smbios_uuid/-$smbios_asset/-$smbios_manufacturer/-$smbios_serial/-$smbios_product/$grub_cpu/$grub_platform/grub.cfg"
+source "(tftp)/grub/$net_default_mac/$net_default_ip/$smbios_uuid/-$smbios_asset/-$smbios_manufacturer/-$smbios_serial/-$smbios_product/$grub_cpu/$grub_platform/grub.cfg"
\ No newline at end of file
diff --git a/tftp/.gitkeep b/tftp/.gitkeep
new file mode 100644
index 0000000..e69de29
diff --git a/utils/config.go b/utils/config.go
index 9770bfe..46ea270 100644
--- a/utils/config.go
+++ b/utils/config.go
@@ -15,10 +15,13 @@ type MinioConfig struct {
}
type Config struct {
- Port int
- EnableAPIAuth bool
- MinioConfig MinioConfig
- BaseURL *url.URL
+ Port int
+ EnableAPIAuth bool
+ MinioConfig MinioConfig
+ BaseURL *url.URL
+ GrubSupportEnabled bool
+ TFTPEnabled bool
+ DefaultBootentryName string
}
func GetConfig() *Config {
@@ -32,6 +35,8 @@ func GetConfig() *Config {
Endpoint: "127.0.0.1:9000",
BucketName: "ipxeblue",
},
+ GrubSupportEnabled: false,
+ TFTPEnabled: false,
}
if p := viper.GetInt("PORT"); p != 0 {
@@ -65,5 +70,9 @@ func GetConfig() *Config {
}
config.BaseURL = u
+ config.GrubSupportEnabled = viper.GetBool("GRUB_SUPPORT_ENABLED")
+ config.TFTPEnabled = viper.GetBool("TFTP_ENABLED")
+ config.DefaultBootentryName = viper.GetString("DEFAULT_BOOTENTRY_NAME")
+
return &config
}
diff --git a/utils/templatefunctions.go b/utils/templatefunctions.go
index 5b91dc6..2dfe21b 100644
--- a/utils/templatefunctions.go
+++ b/utils/templatefunctions.go
@@ -42,6 +42,19 @@ func GetCustomFunctions(c *gin.Context, tpl *template.Template) template.FuncMap
}
return fmt.Sprintf("%s%s", baseURL.String(), path), err
},
+ "GetGrubDownloadPath": func(bootentry models.Bootentry, filename string) (ret string, err error) {
+ file := bootentry.GetFile(filename)
+ if file == nil {
+ return fmt.Sprintf("%s not found in bootentry %s", filename, bootentry.Uuid), err
+ }
+ path, token := file.GetDownloadPath()
+ if token != nil {
+ // Get computer in gin context to add it in token, to used it in file template.
+ token.Computer = *c.MustGet("computer").(*models.Computer)
+ db.Create(&token)
+ }
+ return fmt.Sprintf("(http,%s)%s", baseURL.Host, path), err
+ },
"GetDownloadBaseURL": func(bootentry models.Bootentry) (ret string, err error) {
path, token := bootentry.GetDownloadBasePath()
if token != nil {
diff --git a/webui/.eslintcache b/webui/.eslintcache
index b772871..11aa1aa 100644
--- a/webui/.eslintcache
+++ b/webui/.eslintcache
@@ -1 +1 @@
-[{"/webui/src/index.js":"1","/webui/src/App.js":"2","/webui/src/models/computers.js":"3","/webui/src/models/ipxeaccount.js":"4","/webui/src/models/bootentry.js":"5"},{"size":219,"mtime":1609957302467,"results":"6","hashOfConfig":"7"},{"size":3151,"mtime":1616005052196,"results":"8","hashOfConfig":"7"},{"size":4258,"mtime":1651116847046,"results":"9","hashOfConfig":"10"},{"size":2405,"mtime":1623124607631,"results":"11","hashOfConfig":"10"},{"size":3158,"mtime":1623124607631,"results":"12","hashOfConfig":"10"},{"filePath":"13","messages":"14","errorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0},"79ytzx",{"filePath":"15","messages":"16","errorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0},{"filePath":"17","messages":"18","errorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0},"asp27a",{"filePath":"19","messages":"20","errorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0},{"filePath":"21","messages":"22","errorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0},"/webui/src/index.js",[],"/webui/src/App.js",[],"/webui/src/models/computers.js",[],"/webui/src/models/ipxeaccount.js",[],"/webui/src/models/bootentry.js",[]]
\ No newline at end of file
+[{"/webui/src/index.js":"1","/webui/src/App.js":"2","/webui/src/models/computers.js":"3","/webui/src/models/ipxeaccount.js":"4","/webui/src/models/bootentry.js":"5"},{"size":219,"mtime":1609957302467,"results":"6","hashOfConfig":"7"},{"size":3151,"mtime":1616005052196,"results":"8","hashOfConfig":"9"},{"size":4258,"mtime":1684558334905,"results":"10","hashOfConfig":"9"},{"size":2405,"mtime":1623124607631,"results":"11","hashOfConfig":"9"},{"size":3398,"mtime":1684559659877,"results":"12","hashOfConfig":"9"},{"filePath":"13","messages":"14","errorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0},"79ytzx",{"filePath":"15","messages":"16","errorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0},"asp27a",{"filePath":"17","messages":"18","errorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0},{"filePath":"19","messages":"20","errorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0},{"filePath":"21","messages":"22","errorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0},"/webui/src/index.js",[],"/webui/src/App.js",[],"/webui/src/models/computers.js",[],"/webui/src/models/ipxeaccount.js",[],"/webui/src/models/bootentry.js",[]]
\ No newline at end of file
diff --git a/webui/src/models/bootentry.js b/webui/src/models/bootentry.js
index 76b0316..d25c482 100644
--- a/webui/src/models/bootentry.js
+++ b/webui/src/models/bootentry.js
@@ -56,6 +56,7 @@ export const BootentryCreate = props => (
+
);
@@ -70,6 +71,7 @@ export const BootentryEdit = props => (
+