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 => ( +