Skip to content

Commit

Permalink
Merge pull request #55 from ubccr/v0.0.13
Browse files Browse the repository at this point in the history
V0.0.13
  • Loading branch information
aebruno authored Aug 27, 2024
2 parents 8673135 + 388d8cc commit 1ae1243
Show file tree
Hide file tree
Showing 26 changed files with 755 additions and 223 deletions.
2 changes: 1 addition & 1 deletion .goreleaser.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -60,7 +60,7 @@ nfpms:
signature:
key_file: key.gpg
contents:
- src: ./scripts/nfpm/grendel.toml.default
- src: ./grendel.toml.sample
dst: /etc/grendel/grendel.toml
type: "config|noreplace"
- src: ./scripts/nfpm/grendel.service
Expand Down
13 changes: 12 additions & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,15 @@
# Grendel Changelog

## [0.0.13] - 2024-08-26

- Update retryablehttp, gjson, ipxe deps
- Feat: frontend - added CSV export with GO template support
- Feat: tors - added SONiC queries
- Feat: tors - added Arista EOS queries
- Fix: frontend - prevent initial admin user from needing to relog after registration
- Fix: bmc - concurrent map writes on BMC queries
- Fix: nfpm - use updated sample toml file rather than .default file

## [0.0.12] - 2024-08-05

- Update echo, fiber deps
Expand Down Expand Up @@ -143,4 +153,5 @@
[0.0.10]: https://github.com/ubccr/grendel/releases/tag/v0.0.10
[0.0.11]: https://github.com/ubccr/grendel/releases/tag/v0.0.11
[0.0.12]: https://github.com/ubccr/grendel/releases/tag/v0.0.12
[Unreleased]: https://github.com/ubccr/grendel/compare/v0.0.12...HEAD
[0.0.13]: https://github.com/ubccr/grendel/releases/tag/v0.0.13
[Unreleased]: https://github.com/ubccr/grendel/compare/v0.0.13...HEAD
10 changes: 3 additions & 7 deletions bmc/client.go
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
package bmc

import (
"github.com/spf13/viper"
"github.com/stmcginnis/gofish"
)

Expand All @@ -13,9 +12,11 @@ type Redfish struct {

type System struct {
Name string `json:"name"`
HostName string `json:"host_name"`
BIOSVersion string `json:"bios_version"`
SerialNumber string `json:"serial_number"`
Manufacturer string `json:"manufacturer"`
Model string `json:"model"`
PowerStatus string `json:"power_status"`
Health string `json:"health"`
TotalMemory float32 `json:"total_memory"`
Expand All @@ -24,12 +25,7 @@ type System struct {
BootOrder []string `json:"boot_order"`
}

func NewRedfishClient(ip string) (*Redfish, error) {
user := viper.GetString("bmc.user")
pass := viper.GetString("bmc.password")
viper.SetDefault("bmc.insecure", true)
insecure := viper.GetBool("bmc.insecure")

func NewRedfishClient(ip, user, pass string, insecure bool) (*Redfish, error) {
endpoint := "https://" + ip

config := gofish.ClientConfig{
Expand Down
3 changes: 2 additions & 1 deletion bmc/redfish.go
Original file line number Diff line number Diff line change
Expand Up @@ -107,10 +107,11 @@ func (r *Redfish) GetSystem() (*System, error) {
sys := ss[0]

system := &System{
Name: sys.HostName,
HostName: sys.HostName,
BIOSVersion: sys.BIOSVersion,
SerialNumber: sys.SKU,
Manufacturer: sys.Manufacturer,
Model: sys.Model,
PowerStatus: string(sys.PowerState),
Health: string(sys.Status.Health),
TotalMemory: sys.MemorySummary.TotalSystemMemoryGiB,
Expand Down
2 changes: 1 addition & 1 deletion bmc/redfish_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,7 @@ func TestRedfish(t *testing.T) {
t.Skip("Skipping BMC test. Missing env vars")
}

r, err := NewRedfishClient(endpoint)
r, err := NewRedfishClient(endpoint, user, pass, true)
assert.Nil(t, err)
defer r.client.Logout()

Expand Down
86 changes: 67 additions & 19 deletions bmc/runner.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,11 +5,15 @@ import (
"fmt"

"github.com/korovkin/limiter"
"github.com/spf13/viper"
"github.com/ubccr/grendel/model"
)

type jobRunner struct {
limit *limiter.ConcurrencyLimiter
limit *limiter.ConcurrencyLimiter
user string
pass string
insecure bool
}

type JobMessage struct {
Expand All @@ -20,7 +24,16 @@ type JobMessage struct {
}

func newJobRunner(j *Job) *jobRunner {
return &jobRunner{limit: limiter.NewConcurrencyLimiter(j.fanout)}
user := viper.GetString("bmc.user")
pass := viper.GetString("bmc.password")
insecure := viper.GetBool("bmc.insecure")

return &jobRunner{
limit: limiter.NewConcurrencyLimiter(j.fanout),
user: user,
pass: pass,
insecure: insecure,
}
}

func (r *jobRunner) Wait() {
Expand All @@ -32,8 +45,12 @@ func (r *jobRunner) RunPowerCycle(host *model.Host, ch chan JobMessage, bootOver
m := JobMessage{Status: "error", Host: host.Name}
defer func() { ch <- m }()

ip := host.InterfaceBMC().AddrString()
r, err := NewRedfishClient(ip)
bmc := host.InterfaceBMC()
ip := ""
if bmc != nil {
ip = bmc.AddrString()
}
r, err := NewRedfishClient(ip, r.user, r.pass, r.insecure)
if err != nil {
m.Msg = fmt.Sprintf("%s", err)
return
Expand All @@ -57,8 +74,12 @@ func (r *jobRunner) RunPowerOn(host *model.Host, ch chan JobMessage, bootOverrid
m := JobMessage{Status: "error", Host: host.Name}
defer func() { ch <- m }()

ip := host.InterfaceBMC().AddrString()
r, err := NewRedfishClient(ip)
bmc := host.InterfaceBMC()
ip := ""
if bmc != nil {
ip = bmc.AddrString()
}
r, err := NewRedfishClient(ip, r.user, r.pass, r.insecure)
if err != nil {
m.Msg = fmt.Sprintf("%s", err)
return
Expand All @@ -82,8 +103,12 @@ func (r *jobRunner) RunPowerOff(host *model.Host, ch chan JobMessage) {
m := JobMessage{Status: "error", Host: host.Name}
defer func() { ch <- m }()

ip := host.InterfaceBMC().AddrString()
r, err := NewRedfishClient(ip)
bmc := host.InterfaceBMC()
ip := ""
if bmc != nil {
ip = bmc.AddrString()
}
r, err := NewRedfishClient(ip, r.user, r.pass, r.insecure)
if err != nil {
m.Msg = fmt.Sprintf("%s", err)
return
Expand All @@ -108,8 +133,12 @@ func (r *jobRunner) RunBmcStatus(host *model.Host, ch chan JobMessage) {
defer func() { ch <- m }()

data := &System{}
ip := host.InterfaceBMC().AddrString()
r, err := NewRedfishClient(ip)
bmc := host.InterfaceBMC()
ip := ""
if bmc != nil {
ip = bmc.AddrString()
}
r, err := NewRedfishClient(ip, r.user, r.pass, r.insecure)
if err != nil {
m.Msg = fmt.Sprintf("%s", err)
return
Expand All @@ -123,6 +152,7 @@ func (r *jobRunner) RunBmcStatus(host *model.Host, ch chan JobMessage) {
return
}

data.Name = host.Name
output, err := json.Marshal(data)
if err != nil {
m.Msg = fmt.Sprintf("%s", err)
Expand All @@ -139,8 +169,12 @@ func (r *jobRunner) RunPowerCycleBmc(host *model.Host, ch chan JobMessage) {
m := JobMessage{Status: "error", Host: host.Name}
defer func() { ch <- m }()

ip := host.InterfaceBMC().AddrString()
r, err := NewRedfishClient(ip)
bmc := host.InterfaceBMC()
ip := ""
if bmc != nil {
ip = bmc.AddrString()
}
r, err := NewRedfishClient(ip, r.user, r.pass, r.insecure)
if err != nil {
m.Msg = fmt.Sprintf("%s", err)
return
Expand All @@ -163,8 +197,12 @@ func (r *jobRunner) RunClearSel(host *model.Host, ch chan JobMessage) {
m := JobMessage{Status: "error", Host: host.Name}
defer func() { ch <- m }()

ip := host.InterfaceBMC().AddrString()
r, err := NewRedfishClient(ip)
bmc := host.InterfaceBMC()
ip := ""
if bmc != nil {
ip = bmc.AddrString()
}
r, err := NewRedfishClient(ip, r.user, r.pass, r.insecure)
if err != nil {
m.Msg = fmt.Sprintf("%s", err)
return
Expand All @@ -187,8 +225,12 @@ func (r *jobRunner) RunBmcAutoConfigure(host *model.Host, ch chan JobMessage) {
m := JobMessage{Status: "error", Host: host.Name}
defer func() { ch <- m }()

ip := host.InterfaceBMC().AddrString()
r, err := NewRedfishClient(ip)
bmc := host.InterfaceBMC()
ip := ""
if bmc != nil {
ip = bmc.AddrString()
}
r, err := NewRedfishClient(ip, r.user, r.pass, r.insecure)
if err != nil {
m.Msg = fmt.Sprintf("%s", err)
return
Expand All @@ -211,14 +253,20 @@ func (r *jobRunner) RunBmcImportConfiguration(host *model.Host, ch chan JobMessa
m := JobMessage{Status: "error", Host: host.Name}
defer func() { ch <- m }()

ip := host.InterfaceBMC().AddrString()
token, err := model.NewBootToken(host.ID.String(), host.InterfaceBMC().MAC.String())
bmc := host.InterfaceBMC()
mac := ""
ip := ""
if bmc != nil {
mac = bmc.MAC.String()
ip = bmc.AddrString()
}
token, err := model.NewBootToken(host.ID.String(), mac)
if err != nil {
m.Msg = fmt.Sprintf("%s", err)
}
path := fmt.Sprintf("/boot/%s/bmc", token)

r, err := NewRedfishClient(ip)
r, err := NewRedfishClient(ip, r.user, r.pass, r.insecure)
if err != nil {
m.Msg = fmt.Sprintf("%s", err)
return
Expand Down
2 changes: 1 addition & 1 deletion firmware/ipxe
Submodule ipxe updated 698 files
72 changes: 70 additions & 2 deletions frontend/api.go
Original file line number Diff line number Diff line change
@@ -1,12 +1,16 @@
package frontend

import (
"bytes"
"encoding/json"
"fmt"
"net"
"net/netip"
"slices"
"strconv"
"strings"
"text/template"
"time"

"github.com/gofiber/fiber/v2"
"github.com/segmentio/ksuid"
Expand Down Expand Up @@ -91,7 +95,7 @@ func (h *Handler) RegisterUser(f *fiber.Ctx) error {
return ToastError(f, nil, "Failed to register: Password must not contain spaces or unicode characters")
}

err := h.DB.StoreUser(su, sp)
role, err := h.DB.StoreUser(su, sp)
if err != nil {
if err.Error() == fmt.Sprintf("User %s already exists", su) {
msg = "Failed to register: Username already exists"
Expand All @@ -110,7 +114,7 @@ func (h *Handler) RegisterUser(f *fiber.Ctx) error {

sess.Set("authenticated", true)
sess.Set("user", su)
sess.Set("role", "disabled")
sess.Set("role", role)

err = sess.Save()
if err != nil {
Expand Down Expand Up @@ -468,6 +472,70 @@ func (h *Handler) exportHosts(f *fiber.Ctx) error {
return f.SendString(string(o))
}

func (h *Handler) exportInventory(f *fiber.Ctx) error {
hosts := f.Params("hosts")
filename := f.Query("filename")
templateString := f.Query("template")

tmpl, err := template.New("test").Parse(templateString)
if err != nil {
return ToastError(f, err, "Failed to parse template")
}

ns, err := nodeset.NewNodeSet(hosts)
if err != nil {
return ToastError(f, err, "Failed to parse node set")
}

hostList, err := h.DB.FindHosts(ns)
if err != nil {
return ToastError(f, err, "Failed to find nodes")
}

job := bmc.NewJob()
jobStatus, err := job.BmcStatus(hostList)
if err != nil {
return err
}

type templateData struct {
Hosts []bmc.System
Date string
}

td := templateData{
Hosts: jobStatus,
Date: time.Now().Format(time.DateOnly),
}

var buf bytes.Buffer
err = tmpl.Execute(&buf, td)
if err != nil {
return ToastError(f, err, "Failed to execute template")
}

it := ns.Iterator()

failed := []string{}
for it.Next() {
if !slices.ContainsFunc(jobStatus, func(x bmc.System) bool { return x.Name == it.Value() }) {
failed = append(failed, it.Value())
}
}

if len(hostList) != len(jobStatus) {
buf.WriteString(fmt.Sprintf("\nWARNING: Some BMC queries have failed. Their entries will not be listed:\n%s", strings.Join(failed, "\n")))
}

if filename != "" {
f.Set("HX-Redirect", fmt.Sprintf("/api/hosts/inventory/%s?filename=%s", hosts, filename))
f.Set("Content-Type", "application/force-download")
f.Set("Content-Disposition", fmt.Sprintf("attachment; filename=%s", filename))
}

return f.SendString(buf.String())
}

func (h *Handler) bmcConfigureAuto(f *fiber.Ctx) error {
hosts := f.FormValue("hosts")
ns, err := nodeset.NewNodeSet(hosts)
Expand Down
7 changes: 5 additions & 2 deletions frontend/fragments.go
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import (

"github.com/gofiber/fiber/v2"
"github.com/spf13/viper"
"github.com/ubccr/grendel/bmc"
"github.com/ubccr/grendel/model"
"github.com/ubccr/grendel/nodeset"
"github.com/ubccr/grendel/tors"
Expand Down Expand Up @@ -141,8 +142,10 @@ func (h *Handler) actions(f *fiber.Ctx) error {

nodeset := ns.String()
return f.Render("fragments/actions", fiber.Map{
"Hosts": nodeset,
"BootImages": h.getBootImages(),
"Hosts": nodeset,
"BmcSystem": bmc.System{},
"ExportCSVDefaultTemplate": viper.GetString("frontend.export_csv_default_template"),
"BootImages": h.getBootImages(),
}, "")
}

Expand Down
1 change: 1 addition & 0 deletions frontend/handler.go
Original file line number Diff line number Diff line change
Expand Up @@ -131,6 +131,7 @@ func (h *Handler) SetupRoutes(app *fiber.App) {
api.Patch("/hosts/tags", auth, h.tagHosts)
api.Patch("/hosts/image", auth, h.imageHosts)
api.Get("/hosts/export/:hosts", auth, h.exportHosts)
api.Get("/hosts/inventory/:hosts", auth, h.exportInventory)

app.Get("/users", admin, h.Users)
api.Post("/users", admin, h.usersPost)
Expand Down
Loading

0 comments on commit 1ae1243

Please sign in to comment.