diff --git a/.goreleaser.yaml b/.goreleaser.yaml
index ff1880d..5589d50 100644
--- a/.goreleaser.yaml
+++ b/.goreleaser.yaml
@@ -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
diff --git a/CHANGELOG.md b/CHANGELOG.md
index a42d88a..246b452 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -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
@@ -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
diff --git a/bmc/client.go b/bmc/client.go
index c3bf76b..42ca7ad 100644
--- a/bmc/client.go
+++ b/bmc/client.go
@@ -1,7 +1,6 @@
package bmc
import (
- "github.com/spf13/viper"
"github.com/stmcginnis/gofish"
)
@@ -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"`
@@ -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{
diff --git a/bmc/redfish.go b/bmc/redfish.go
index fbc6864..a95d962 100644
--- a/bmc/redfish.go
+++ b/bmc/redfish.go
@@ -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,
diff --git a/bmc/redfish_test.go b/bmc/redfish_test.go
index b84c1a8..c1c2a37 100644
--- a/bmc/redfish_test.go
+++ b/bmc/redfish_test.go
@@ -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()
diff --git a/bmc/runner.go b/bmc/runner.go
index 5187035..d1a5f1a 100644
--- a/bmc/runner.go
+++ b/bmc/runner.go
@@ -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 {
@@ -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() {
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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)
@@ -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
@@ -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
@@ -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
@@ -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
diff --git a/firmware/ipxe b/firmware/ipxe
index d72c8fd..bdb5b4a 160000
--- a/firmware/ipxe
+++ b/firmware/ipxe
@@ -1 +1 @@
-Subproject commit d72c8fdc902bc5d605fef081a18f6fe84f3d0512
+Subproject commit bdb5b4aef46ed34b47094652f3eefc7d0463d166
diff --git a/frontend/api.go b/frontend/api.go
index a323c5a..f331474 100644
--- a/frontend/api.go
+++ b/frontend/api.go
@@ -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"
@@ -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"
@@ -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 {
@@ -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)
diff --git a/frontend/fragments.go b/frontend/fragments.go
index 4a607ac..286611f 100644
--- a/frontend/fragments.go
+++ b/frontend/fragments.go
@@ -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"
@@ -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(),
}, "")
}
diff --git a/frontend/handler.go b/frontend/handler.go
index fd2d8f1..5d75aa3 100644
--- a/frontend/handler.go
+++ b/frontend/handler.go
@@ -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)
diff --git a/frontend/notifications.go b/frontend/notifications.go
index 29e58d2..0080d8e 100644
--- a/frontend/notifications.go
+++ b/frontend/notifications.go
@@ -12,7 +12,9 @@ func ToastSuccess(context *fiber.Ctx, msg string, appendTrigger string) error {
return context.Send(nil)
}
func ToastError(context *fiber.Ctx, err error, msg string) error {
- log.Error(err)
+ if err != nil {
+ log.Error(err)
+ }
context.Response().Header.Add("HX-Trigger", fmt.Sprintf(`{"toast-error": "%s"}`, msg))
context.Response().Header.Add("HX-Reswap", "none")
return context.SendString(msg)
diff --git a/frontend/public/tailwind.css b/frontend/public/tailwind.css
index e7d2a3d..c86f3ed 100644
--- a/frontend/public/tailwind.css
+++ b/frontend/public/tailwind.css
@@ -940,6 +940,10 @@ video {
white-space: nowrap;
}
+.whitespace-pre {
+ white-space: pre;
+}
+
.rounded-full {
border-radius: 9999px;
}
@@ -1150,10 +1154,6 @@ video {
padding-bottom: 0.75rem;
}
-.pb-6 {
- padding-bottom: 1.5rem;
-}
-
.pt-0 {
padding-top: 0px;
}
diff --git a/frontend/switch.go b/frontend/switch.go
index 5c8eed5..dc3717b 100644
--- a/frontend/switch.go
+++ b/frontend/switch.go
@@ -1,9 +1,8 @@
package frontend
import (
- "fmt"
+ "errors"
- "github.com/spf13/viper"
"github.com/ubccr/grendel/nodeset"
"github.com/ubccr/grendel/tors"
)
@@ -13,17 +12,19 @@ func (h *Handler) getMacAddress(switchName string) (tors.MACTable, error) {
if err != nil {
return nil, err
}
- host, err := h.DB.FindHosts(nodeset)
+ hosts, err := h.DB.FindHosts(nodeset)
if err != nil {
return nil, err
}
+ if len(hosts) != 1 {
+ return nil, errors.New("failed to load switch from DB")
+ }
+ host := hosts[0]
- endpoint := fmt.Sprintf("https://%s", host[0].InterfaceBMC().ToStdAddr().String())
- sw, err := tors.NewDellOS10(endpoint, "admin", viper.GetString("bmc.switch_admin_password"), "", true)
+ sw, err := tors.NewNetworkSwitch(host)
if err != nil {
return nil, err
}
-
macTable, err := sw.GetMACTable()
if err != nil {
return nil, err
diff --git a/frontend/views/fragments/actions.gohtml b/frontend/views/fragments/actions.gohtml
index 4b4a6ce..296321f 100644
--- a/frontend/views/fragments/actions.gohtml
+++ b/frontend/views/fragments/actions.gohtml
@@ -332,6 +332,69 @@
+
+
+
Export CSV:
+
+ Outputs a CSV for inventory management
+
+
+
+
diff --git a/go.mod b/go.mod
index 3677e72..9903470 100644
--- a/go.mod
+++ b/go.mod
@@ -8,14 +8,15 @@ require (
github.com/GehirnInc/crypt v0.0.0-20230320061759-8cc1b52080c5
github.com/Pallinder/go-randomdata v1.2.0
github.com/alouca/gosnmp v0.0.0-20170620005048-04d83944c9ab
+ github.com/aristanetworks/goeapi v1.0.0
github.com/bits-and-blooms/bitset v1.5.0
github.com/bluele/factory-go v0.0.0-20181130035244-e6e8633dd3fe
github.com/coreos/butane v0.14.1-0.20220513204719-6cd92788076e
github.com/dustin/go-humanize v1.0.1
- github.com/fatih/color v1.13.0
+ github.com/fatih/color v1.16.0
github.com/go-playground/validator/v10 v10.0.1
github.com/hako/branca v0.0.0-20191227164554-3b9970524189
- github.com/hashicorp/go-retryablehttp v0.6.6
+ github.com/hashicorp/go-retryablehttp v0.7.7
github.com/insomniacslk/dhcp v0.0.0-20200922210017-67c425063dca
github.com/korovkin/limiter v0.0.0-20190919045942-dac5a6b2a536
github.com/labstack/echo/v4 v4.12.0
@@ -33,7 +34,7 @@ require (
github.com/stmcginnis/gofish v0.15.0
github.com/stretchr/testify v1.8.4
github.com/tidwall/buntdb v1.3.0
- github.com/tidwall/gjson v1.14.4
+ github.com/tidwall/gjson v1.17.3
github.com/tidwall/sjson v1.2.5
github.com/ubccr/go-dhcpd-leases v0.1.1-0.20191206204522-601ab01835fb
go4.org/netipx v0.0.0-20230125063823-8449b0a6169f
@@ -50,6 +51,7 @@ require (
github.com/gofiber/utils v1.1.0 // indirect
github.com/mattn/go-sqlite3 v1.14.17 // indirect
github.com/redis/go-redis/v9 v9.0.2 // indirect
+ github.com/vaughan0/go-ini v0.0.0-20130923145212-a98ad7ee00ec // indirect
golang.org/x/sync v0.7.0 // indirect
)
diff --git a/go.sum b/go.sum
index c188fc3..bbf2d5f 100644
--- a/go.sum
+++ b/go.sum
@@ -50,6 +50,8 @@ github.com/alouca/gosnmp v0.0.0-20170620005048-04d83944c9ab h1:pfx9N/EMDxIwVzGu9
github.com/alouca/gosnmp v0.0.0-20170620005048-04d83944c9ab/go.mod h1:kEcj+iUROrUCr7AIrul5NutI2kWv0ns9BL0ezVp1h/Y=
github.com/andybalholm/brotli v1.1.0 h1:eLKJA0d02Lf0mVpIDgYnqXcUn0GqVmEFny3VuID1U3M=
github.com/andybalholm/brotli v1.1.0/go.mod h1:sms7XGricyQI9K10gOSf56VKKWS4oLer58Q+mhRPtnY=
+github.com/aristanetworks/goeapi v1.0.0 h1:FjckkjOY32SkmKrqDyBqYu6hN7DaIJuxcii9LLdZqtQ=
+github.com/aristanetworks/goeapi v1.0.0/go.mod h1:DcgIvssM+qcRRVICDky/ecT/Gqpx40UQDTYY8Lu/iJ0=
github.com/aws/aws-sdk-go v1.30.28/go.mod h1:5zCpMtNQVjRREroY7sYe8lOMRSxkhG6MZveU8YkpAk0=
github.com/aws/aws-sdk-go v1.44.19 h1:dhI6p4l6kisnA7gBAM8sP5YIk0bZ9HNAj7yrK7kcfdU=
github.com/aws/aws-sdk-go v1.44.19/go.mod h1:y4AeaBuwd2Lk+GepC1E9v0qOiTws0MIWAX4oIKwKHZo=
@@ -110,8 +112,8 @@ github.com/envoyproxy/go-control-plane v0.9.7/go.mod h1:cwu0lG7PUMfa9snN8LXBig5y
github.com/envoyproxy/go-control-plane v0.9.9-0.20201210154907-fd9021fe5dad/go.mod h1:cXg6YxExXjJnVBQHBLXeUAgxn2UodCpnH306RInaBQk=
github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c=
github.com/fanliao/go-promise v0.0.0-20141029170127-1890db352a72/go.mod h1:PjfxuH4FZdUyfMdtBio2lsRr1AKEaVPwelzuHuh8Lqc=
-github.com/fatih/color v1.13.0 h1:8LOYc1KYPPmyKMuN8QV2DNRWNbLo6LZ0iLs8+mlH53w=
-github.com/fatih/color v1.13.0/go.mod h1:kLAiJbzzSOZDVNGyDpeOxJ47H46qBXwg5ILebYFFOfk=
+github.com/fatih/color v1.16.0 h1:zmkK9Ngbjj+K0yRhTVONQh1p/HknKYSlNT+vZCzyokM=
+github.com/fatih/color v1.16.0/go.mod h1:fL2Sau1YI5c0pdGEVCbKQbLXB6edEj1ZgiY4NijnWvE=
github.com/frankban/quicktest v1.14.3 h1:FJKSZTDHjyhriyC81FLQ0LY93eSai0ZyR/ZIkd3ZUKE=
github.com/frankban/quicktest v1.14.3/go.mod h1:mgiwOwqx65TmIk1wJ6Q7wvnVMocbUorkibMOrVTHZps=
github.com/fsnotify/fsnotify v1.6.0 h1:n+5WquG0fcWoWp6xPWfHdbskMCQaFnG6PfBrh1Ky4HY=
@@ -214,14 +216,12 @@ github.com/googleapis/google-cloud-go-testing v0.0.0-20200911160855-bcd43fbb19e8
github.com/gopherjs/gopherjs v0.0.0-20181017120253-0766667cb4d1/go.mod h1:wJfORRmW1u3UXTncJ5qlYoELFm8eSnnEO6hX4iZ3EWY=
github.com/hako/branca v0.0.0-20191227164554-3b9970524189 h1:qnw4Yi3Wp0gJF5JOF2uHA/wl2zq1FOxhtXYt0Z/h1rk=
github.com/hako/branca v0.0.0-20191227164554-3b9970524189/go.mod h1:rg2Mhi85BDi/JlegTSj3hgLPNJ0iNvWgDrnM306nbWQ=
-github.com/hashicorp/go-cleanhttp v0.5.1/go.mod h1:JpRdi6/HCYpAwUzNwuwqhbovhLtngrth3wmdIIUrZ80=
github.com/hashicorp/go-cleanhttp v0.5.2 h1:035FKYIWjmULyFRBKPs8TBQoi0x6d9G4xc9neXJWAZQ=
github.com/hashicorp/go-cleanhttp v0.5.2/go.mod h1:kO/YDlP8L1346E6Sodw+PrpBSV4/SoxCXGY6BqNFT48=
-github.com/hashicorp/go-hclog v0.9.2/go.mod h1:5CU+agLiy3J7N7QjHK5d05KxGsuXiQLrjA0H7acj2lQ=
-github.com/hashicorp/go-hclog v1.2.0 h1:La19f8d7WIlm4ogzNHB0JGqs5AUDAZ2UfCY4sJXcJdM=
-github.com/hashicorp/go-hclog v1.2.0/go.mod h1:whpDNt7SSdeAju8AWKIWsul05p54N/39EeqMAyrmvFQ=
-github.com/hashicorp/go-retryablehttp v0.6.6 h1:HJunrbHTDDbBb/ay4kxa1n+dLmttUlnP3V9oNE4hmsM=
-github.com/hashicorp/go-retryablehttp v0.6.6/go.mod h1:vAew36LZh98gCBJNLH42IQ1ER/9wtLZZ8meHqQvEYWY=
+github.com/hashicorp/go-hclog v1.6.3 h1:Qr2kF+eVWjTiYmU7Y31tYlP1h0q/X3Nl3tPGdaB11/k=
+github.com/hashicorp/go-hclog v1.6.3/go.mod h1:W4Qnvbt70Wk/zYJryRzDRU/4r0kIg0PVHBcfoyhpF5M=
+github.com/hashicorp/go-retryablehttp v0.7.7 h1:C8hUCYzor8PIfXHa4UrZkU4VvK8o9ISHxT2Q8+VepXU=
+github.com/hashicorp/go-retryablehttp v0.7.7/go.mod h1:pkQpWZeYWskR+D1tR2O5OcBFOxfA7DoAO6xtkuQnHTk=
github.com/hashicorp/golang-lru v0.5.0/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8=
github.com/hashicorp/golang-lru v0.5.1/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8=
github.com/hashicorp/hcl v1.0.0 h1:0Anlzjpi4vEasTeNFn2mLJgTSwt0+6sfsiTG8qcWGx4=
@@ -266,11 +266,8 @@ github.com/leodido/go-urn v1.2.0 h1:hpXL4XnriNwQ/ABnpepYM/1vCLWNDfUNts8dX3xTG6Y=
github.com/leodido/go-urn v1.2.0/go.mod h1:+8+nEpDfqqsY+g338gtMEUOtuK+4dEMhiQEgxpxOKII=
github.com/magiconair/properties v1.8.7 h1:IeQXZAiQcpL9mgcAe1Nu6cX9LLw6ExEHKjN0VQdvPDY=
github.com/magiconair/properties v1.8.7/go.mod h1:Dhd985XPs7jluiymwWYZ0G4Z61jb3vdS329zhj2hYo0=
-github.com/mattn/go-colorable v0.1.9/go.mod h1:u6P/XSegPjTcexA+o6vUJrdnUu04hMope9wVRipJSqc=
github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA=
github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg=
-github.com/mattn/go-isatty v0.0.12/go.mod h1:cbi8OIDigv2wuxKPP5vlRcQ1OAZbq2CE4Kysco4FUpU=
-github.com/mattn/go-isatty v0.0.14/go.mod h1:7GGIvUiUoEMVVmxf/4nioHXj79iQHKdU27kJ6hsGG94=
github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM=
github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
@@ -361,8 +358,8 @@ github.com/tidwall/buntdb v1.3.0 h1:gdhWO+/YwoB2qZMeAU9JcWWsHSYU3OvcieYgFRS0zwA=
github.com/tidwall/buntdb v1.3.0/go.mod h1:lZZrZUWzlyDJKlLQ6DKAy53LnG7m5kHyrEHvvcDmBpU=
github.com/tidwall/gjson v1.12.1/go.mod h1:/wbyibRr2FHMks5tjHJ5F8dMZh3AcwJEMf5vlfC0lxk=
github.com/tidwall/gjson v1.14.2/go.mod h1:/wbyibRr2FHMks5tjHJ5F8dMZh3AcwJEMf5vlfC0lxk=
-github.com/tidwall/gjson v1.14.4 h1:uo0p8EbA09J7RQaflQ1aBRffTR7xedD2bcIVSYxLnkM=
-github.com/tidwall/gjson v1.14.4/go.mod h1:/wbyibRr2FHMks5tjHJ5F8dMZh3AcwJEMf5vlfC0lxk=
+github.com/tidwall/gjson v1.17.3 h1:bwWLZU7icoKRG+C+0PNwIKC6FCJO/Q3p2pZvuP0jN94=
+github.com/tidwall/gjson v1.17.3/go.mod h1:/wbyibRr2FHMks5tjHJ5F8dMZh3AcwJEMf5vlfC0lxk=
github.com/tidwall/grect v0.1.4 h1:dA3oIgNgWdSspFzn1kS4S/RDpZFLrIxAZOdJKjYapOg=
github.com/tidwall/grect v0.1.4/go.mod h1:9FBsaYRaR0Tcy4UwefBX/UDcDcDy9V5jUcxHzv2jd5Q=
github.com/tidwall/lotsa v1.0.2 h1:dNVBH5MErdaQ/xd9s769R31/n2dXavsQ0Yf4TMEHHw8=
@@ -390,6 +387,8 @@ github.com/valyala/fasttemplate v1.2.2 h1:lxLXG0uE3Qnshl9QyaK6XJxMXlQZELvChBOCmQ
github.com/valyala/fasttemplate v1.2.2/go.mod h1:KHLXt3tVN2HBp8eijSv/kGJopbvo7S+qRAEEKiv+SiQ=
github.com/valyala/tcplisten v1.0.0 h1:rBHj/Xf+E1tRGZyWIWwJDiRY0zc1Js+CV5DqwacVSA8=
github.com/valyala/tcplisten v1.0.0/go.mod h1:T0xQ8SeCZGxckz9qRXTfG43PvQ/mcWh7FwZEA7Ioqkc=
+github.com/vaughan0/go-ini v0.0.0-20130923145212-a98ad7ee00ec h1:DGmKwyZwEB8dI7tbLt/I/gQuP559o/0FrAkHKlQM/Ks=
+github.com/vaughan0/go-ini v0.0.0-20130923145212-a98ad7ee00ec/go.mod h1:owBmyHYMLkxyrugmfwE/DLJyW8Ro9mkphwuVErQ0iUw=
github.com/vincent-petithory/dataurl v1.0.0 h1:cXw+kPto8NLuJtlMsI152irrVw9fRDX8AbShPRpg2CI=
github.com/vincent-petithory/dataurl v1.0.0/go.mod h1:FHafX5vmDzyP+1CQATJn7WFKc9CvnvxyvZy6I1MrG/U=
github.com/vmware/vmw-guestinfo v0.0.0-20220317130741-510905f0efa3/go.mod h1:CSBTxrhePCm0cmXNKDGeu+6bOQzpaEklfCqEpn89JWk=
@@ -535,7 +534,6 @@ golang.org/x/sys v0.0.0-20191008105621-543471e840be/go.mod h1:h1NjWce9XRLGQEsW7w
golang.org/x/sys v0.0.0-20191204072324-ce4227a45e2e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20191228213918-04cbcbbfeed8/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200113162924-86b910548bc1/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
-golang.org/x/sys v0.0.0-20200116001909-b77594299b42/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200122134326-e047566fdf82/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200202164722-d101bd2416d5/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200212091648-12a6c2dcc1e4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
diff --git a/grendel.toml.sample b/grendel.toml.sample
index ef0abc2..37db9c8 100644
--- a/grendel.toml.sample
+++ b/grendel.toml.sample
@@ -1,5 +1,5 @@
#------------------------------------------------------------------------------
-# Sample Grendel Config
+# Grendel Config
#------------------------------------------------------------------------------
#------------------------------------------------------------------------------
@@ -152,9 +152,17 @@ insecure = false
[bmc]
user = ""
password = ""
+switch_admin_username = "admin"
+switch_admin_password = ""
-# Allow unsigned https certs for redfish queries? Default=true
-#insecure = true
+# Allow unsigned https certs for redfish queries
+insecure = true
+
+# bmc runner settings:
+# sets number of active queries allowed at one time
+fanout = 20
+# number of seconds after a query completes to wait before sending another
+delay = 1
# IP sent to the BMC for import system config, should be an IP of the provision server which is reachable by the BMCs
#config_share_ip = "0.0.0.0"
@@ -193,6 +201,9 @@ cols_end = 5
rack_min = 3
rack_max = 42
+# Export CSV default template
+#export_csv_default_template = """"""
+
# Session storage driver. Valid options: redis, sqlite3, memory.
session_storage = "memory"
diff --git a/model/buntstore.go b/model/buntstore.go
index f8816c9..a25cbfb 100644
--- a/model/buntstore.go
+++ b/model/buntstore.go
@@ -76,7 +76,7 @@ type UserValue struct {
}
// StoreUser stores the User in the data store
-func (s *BuntStore) StoreUser(username, password string) error {
+func (s *BuntStore) StoreUser(username, password string) (string, error) {
d := true
role := "disabled"
@@ -92,10 +92,10 @@ func (s *BuntStore) StoreUser(username, password string) error {
})
if err != nil {
- return err
+ return role, err
}
if d {
- return fmt.Errorf("user %s already exists", username)
+ return role, fmt.Errorf("user %s already exists", username)
}
// Set role to admin if this is the first user
@@ -107,14 +107,14 @@ func (s *BuntStore) StoreUser(username, password string) error {
})
})
if err != nil {
- return err
+ return role, err
}
if count == 0 {
role = "admin"
}
hashed, _ := bcrypt.GenerateFromPassword([]byte(password), 8)
- return s.db.Update(func(tx *buntdb.Tx) error {
+ return role, s.db.Update(func(tx *buntdb.Tx) error {
user := UserValue{
Hash: hashed,
Role: role,
diff --git a/model/buntstore_test.go b/model/buntstore_test.go
index a82491a..de0f6dc 100644
--- a/model/buntstore_test.go
+++ b/model/buntstore_test.go
@@ -40,6 +40,53 @@ func tempfile() string {
return name.Name()
}
+func TestBuntStoreUser(t *testing.T) {
+ adminUsername := "admin"
+ adminPassword := "SuperSecureAdminPassword1234!@#$"
+ userUsername := "user"
+ userPassword := "1234"
+ assert := assert.New(t)
+
+ store, err := model.NewBuntStore(":memory:")
+ assert.NoError(err)
+ defer store.Close()
+
+ role, err := store.StoreUser(adminUsername, adminPassword)
+ assert.NoError(err)
+ assert.Equal(role, "admin")
+ role, err = store.StoreUser(userUsername, userPassword)
+ assert.NoError(err)
+ assert.Equal(role, "disabled")
+
+ authenticated, role, err := store.VerifyUser("admin", adminPassword)
+ assert.NoError(err)
+ assert.Equal(role, "admin")
+ assert.Equal(authenticated, true)
+ authenticated, role, err = store.VerifyUser("user", userPassword)
+ assert.NoError(err)
+ assert.Equal(role, "disabled")
+ assert.Equal(authenticated, true)
+
+ users, err := store.GetUsers()
+ assert.NoError(err)
+ assert.Equal(users[0].Username, "admin")
+ assert.Contains(users[1].Username, "user")
+
+ err = store.UpdateUser("user", "user")
+ assert.NoError(err)
+ authenticated, role, err = store.VerifyUser("user", userPassword)
+ assert.NoError(err)
+ assert.Equal(role, "user")
+ assert.Equal(authenticated, true)
+
+ err = store.DeleteUser("user")
+ assert.NoError(err)
+ users, err = store.GetUsers()
+ assert.NoError(err)
+ assert.Equal(len(users), 1)
+
+}
+
func TestBuntStoreHost(t *testing.T) {
assert := assert.New(t)
diff --git a/model/datastore.go b/model/datastore.go
index bd8e271..377d418 100644
--- a/model/datastore.go
+++ b/model/datastore.go
@@ -43,7 +43,7 @@ var (
// DataStore
type DataStore interface {
// StoreUser stores the User in the data store
- StoreUser(username, password string) error
+ StoreUser(username, password string) (string, error)
// VerifyUser checks if the given username exists in the data store
VerifyUser(username, password string) (bool, string, error)
diff --git a/scripts/nfpm/grendel.toml.default b/scripts/nfpm/grendel.toml.default
deleted file mode 100644
index 37c87f1..0000000
--- a/scripts/nfpm/grendel.toml.default
+++ /dev/null
@@ -1,151 +0,0 @@
-#------------------------------------------------------------------------------
-# Grendel Config
-#------------------------------------------------------------------------------
-
-#------------------------------------------------------------------------------
-# General
-#------------------------------------------------------------------------------
-
-#
-# Path database file. Defaults to ":memory:" which uses in-memory store. Change
-# this to a filepath for persisent storage.
-#
-dbpath = "/var/lib/grendel/grendel.db"
-
-#
-# By default, all loggers are on. You can turn off logging for specific
-# services here.
-#
-loggers = {cli="on", tftp="off", dhcp="on", dns="off", provision="on", api="on", pxe="off"}
-
-#------------------------------------------------------------------------------
-# HTTP Provision Server
-#------------------------------------------------------------------------------
-[provision]
-
-# For provisioning with http
-listen = "0.0.0.0:80"
-scheme = "http"
-
-# For provisioning with https
-#
-#listen = "0.0.0.0:443"
-#
-#scheme = "https"
-#
-# hostname for grendel, should also be the hostname for the SSL certificate
-#hostname = "my.host.name"
-#
-# Path to ssl cert (.crt file)
-#cert = "/path/to/cert/file/hostname.crt"
-#
-# Path to ssl key (.key file)
-#key = "/path/to/cert/file/hostname.key"
-#
-
-# TTL in seconds for provision tokens. Defaults to 1 hour
-token_ttl = 3600
-
-# Can generate secret with `openssl rand -hex 16`
-#secret = "_provisioning_secret_here_"
-
-# Hashed root password used in kickstart template
-root_password = ""
-
-# Default OS image name
-default_image = ""
-
-# Path to repo directory
-repo_dir = "/var/lib/grendel/repo"
-
-#------------------------------------------------------------------------------
-# DHCP Server
-#------------------------------------------------------------------------------
-[dhcp]
-listen = "0.0.0.0:67"
-
-# Default lease time
-lease_time = "24h"
-
-# List of DNS servers
-dns_servers = []
-
-# List of DNS search domains
-domain_search = []
-
-# Default MTU
-mtu = 1500
-
-# Dynamic router configuration. Grendel will generate the router option 3 for
-# DHCP responses based on the hosts IP address, netmask, and router_octet4. For
-# example, if all subnets in your data center have routers 10.x.x.254 you can
-# set router_octet4 = 254. If a host ip address is 10.104.13.10, Grendel will
-# set the router option in the dhcp response to 10.104.13.254. Note setting
-# this option will set the netmask to 24. Off by default.
-router_octet4 = 0
-
-# Hard code a static router. Not set by default.
-#router = ""
-
-# Default netmask example: 8, 16, 24, etc.
-netmask = 24
-
-# Only run DHCP Proxy server
-proxy_only = false
-
-#------------------------------------------------------------------------------
-# DNS Server
-#------------------------------------------------------------------------------
-[dns]
-# Change this to 0.0.0.0:53 for production deployments
-listen = "0.0.0.0:8553"
-
-# Default TTL for dns responses
-ttl = 86400
-
-#------------------------------------------------------------------------------
-# TFTP Server
-#------------------------------------------------------------------------------
-[tftp]
-listen = "0.0.0.0:69"
-
-#------------------------------------------------------------------------------
-# PXE Server
-#------------------------------------------------------------------------------
-[pxe]
-listen = "0.0.0.0:4011"
-
-#------------------------------------------------------------------------------
-# API Server
-#------------------------------------------------------------------------------
-[api]
-# Can generate secret with `openssl rand -hex 16`
-#secret = "_api_secret_here_"
-
-# Path to unix socket
-socket_path = "/var/lib/grendel/grendel-api.socket"
-
-#------------------------------------------------------------------------------
-# API Client Config
-#------------------------------------------------------------------------------
-[client]
-# Grendel API endpoint
-api_endpoint = "/var/lib/grendel/grendel-api.socket"
-
-# Verify ssl certs? false (yes) true (no)
-insecure = false
-
-#------------------------------------------------------------------------------
-# Global BMC Config
-#------------------------------------------------------------------------------
-[bmc]
-user = ""
-password = ""
-
-#------------------------------------------------------------------------------
-# Automatic Host Discovery Config
-#------------------------------------------------------------------------------
-[discovery]
-user = ""
-password = ""
-domain = ""
diff --git a/tors/arista.go b/tors/arista.go
new file mode 100644
index 0000000..7c7bfae
--- /dev/null
+++ b/tors/arista.go
@@ -0,0 +1,63 @@
+package tors
+
+import (
+ "net"
+ "strconv"
+ "strings"
+
+ "github.com/aristanetworks/goeapi"
+ "github.com/aristanetworks/goeapi/module"
+)
+
+const (
+ ARISTA_MACTABLE = "show mac address-table"
+)
+
+type Arista struct {
+ client *goeapi.Node
+}
+
+func NewArista(host string, username, password string) (*Arista, error) {
+ node, err := goeapi.Connect("https", host, username, password, 443)
+ if err != nil {
+ return nil, err
+ }
+
+ return &Arista{client: node}, nil
+}
+
+func (a *Arista) GetMACTable() (MACTable, error) {
+ macTable := make(MACTable, 0)
+ show := module.Show(a.client)
+ macRes, err := show.ShowMACAddressTable()
+ if err != nil {
+ return nil, err
+ }
+
+ // Should we also loop over the Multicast table??
+ for _, m := range macRes.UnicastTable.TableEntries {
+ if !strings.HasPrefix(m.Interface, "Ethernet") {
+ continue
+ }
+ portStr := strings.Replace(m.Interface, "Ethernet", "", 1)
+ port, err := strconv.Atoi(portStr)
+ if err != nil {
+ log.Debugf("failed to parse mac address table port on interface: %s", m.Interface)
+ continue
+ }
+ mac, err := net.ParseMAC(m.MACAddress)
+ if err != nil {
+ log.Debugf("failed to parse mac address on interface: %s, mac: %s", m.Interface, m.MACAddress)
+ continue
+ }
+ macTable[m.MACAddress] = &MACTableEntry{
+ Ifname: m.Interface,
+ VLAN: strconv.Itoa(m.VlanID),
+ MAC: mac,
+ Type: m.EntryType,
+ Port: port,
+ }
+ }
+
+ return macTable, nil
+}
diff --git a/tors/arista_test.go b/tors/arista_test.go
new file mode 100644
index 0000000..8d52f3a
--- /dev/null
+++ b/tors/arista_test.go
@@ -0,0 +1,52 @@
+// Copyright 2019 Grendel Authors. All rights reserved.
+//
+// This file is part of Grendel.
+//
+// Grendel is free software: you can redistribute it and/or modify
+// it under the terms of the GNU General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// Grendel is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+// GNU General Public License for more details.
+//
+// You should have received a copy of the GNU General Public License
+// along with Grendel. If not, see .
+
+package tors
+
+import (
+ "fmt"
+ "os"
+ "testing"
+)
+
+func TestArista(t *testing.T) {
+ endpoint := os.Getenv("GRENDEL_ARISTA_ENDPOINT")
+ user := os.Getenv("GRENDEL_ARISTA_USER")
+ pass := os.Getenv("GRENDEL_ARISTA_PASS")
+
+ if endpoint == "" || user == "" || pass == "" {
+ t.Skip("Skipping Arista test. Missing env vars")
+ }
+
+ client, err := NewArista(endpoint, user, pass)
+ if err != nil {
+ t.Fatal(err)
+ }
+
+ macTable, err := client.GetMACTable()
+ if err != nil {
+ t.Fatal(err)
+ }
+
+ if len(macTable) == 0 {
+ t.Errorf("No mac table entries returned from api")
+ }
+
+ for _, entry := range macTable {
+ fmt.Printf("%s - %d\n", entry.MAC, entry.Port)
+ }
+}
diff --git a/tors/sonic.go b/tors/sonic.go
new file mode 100644
index 0000000..cf10be0
--- /dev/null
+++ b/tors/sonic.go
@@ -0,0 +1,216 @@
+package tors
+
+import (
+ "crypto/tls"
+ "crypto/x509"
+ "encoding/json"
+ "fmt"
+ "io"
+ "net"
+ "net/http"
+ "os"
+ "strconv"
+ "strings"
+ "time"
+)
+
+const (
+ SONIC_RESTCONF_MACTABLE = "/restconf/data/openconfig-network-instance:network-instances/network-instance=default/fdb/mac-table/entries"
+ SONIC_RESTCONF_LLDP = "/restconf/data/openconfig-lldp:lldp/interfaces"
+)
+
+type Sonic struct {
+ username string
+ password string
+ baseUrl string
+ client *http.Client
+}
+
+type sonicMacTable struct {
+ Root struct {
+ Entry []sonicMacTableEntry `json:"entry"`
+ } `json:"openconfig-network-instance:entries"`
+}
+
+type sonicMacTableEntry struct {
+ MacAddress string `json:"mac-address"`
+ Vlan int `json:"vlan"`
+ State struct {
+ EntryType string `json:"entry-type"`
+ MacAddress string `json:"mac-address"`
+ Vlan int `json:"vlan"`
+ } `json:"state"`
+ Interface struct {
+ InterfaceRef struct {
+ State struct {
+ Interface string `json:"interface"`
+ } `json:"state"`
+ } `json:"interface-ref"`
+ } `json:"interface"`
+}
+
+func NewSonic(baseUrl, user, password, cacert string, insecure bool) (*Sonic, error) {
+ tr := &http.Transport{TLSClientConfig: &tls.Config{InsecureSkipVerify: insecure}}
+
+ pem, err := os.ReadFile(cacert)
+ if err == nil {
+ certPool := x509.NewCertPool()
+ if !certPool.AppendCertsFromPEM(pem) {
+ return nil, fmt.Errorf("Failed to read cacert: %s", cacert)
+ }
+
+ tr = &http.Transport{TLSClientConfig: &tls.Config{RootCAs: certPool, InsecureSkipVerify: false}}
+ }
+
+ d := &Sonic{
+ username: user,
+ password: password,
+ baseUrl: "https://" + baseUrl,
+ client: &http.Client{Timeout: time.Second * 20, Transport: tr},
+ }
+
+ return d, nil
+}
+
+func (d *Sonic) URL(resource string) string {
+ return fmt.Sprintf("%s%s", d.baseUrl, resource)
+}
+
+func (d *Sonic) getRequest(url string) (*http.Request, error) {
+ req, err := http.NewRequest(http.MethodGet, url, nil)
+ if err != nil {
+ return nil, err
+ }
+
+ req.Header.Set("Accept", "application/json")
+ if d.username != "" && d.password != "" {
+ req.SetBasicAuth(d.username, d.password)
+ }
+
+ return req, nil
+}
+
+func (d *Sonic) GetMACTable() (MACTable, error) {
+ url := d.URL(SONIC_RESTCONF_MACTABLE)
+ log.Infof("Requesting MAC table: %s", url)
+
+ req, err := d.getRequest(url)
+ if err != nil {
+ return nil, err
+ }
+ res, err := d.client.Do(req)
+ if err != nil {
+ return nil, err
+ }
+ defer res.Body.Close()
+
+ if res.StatusCode == 500 {
+ return nil, fmt.Errorf("failed to fetch mac table with HTTP status code: %d", res.StatusCode)
+ }
+
+ rawJson, err := io.ReadAll(res.Body)
+ if err != nil {
+ return nil, err
+ }
+
+ log.Debugf("Sonic json response: %s", rawJson)
+
+ var sMACTable *sonicMacTable
+ err = json.Unmarshal(rawJson, &sMACTable)
+ if err != nil {
+ return nil, err
+ }
+
+ macTable := make(MACTable, 0)
+
+ for _, entry := range sMACTable.Root.Entry {
+ // Parse port number from interface.
+ // Format is: Eth1/16 (in standard naming mode)
+ iface := entry.Interface.InterfaceRef.State.Interface
+ if !strings.HasPrefix(iface, "Eth1/") {
+ continue
+ }
+ portStr := strings.Replace(iface, "Eth1/", "", 1)
+ port, err := strconv.Atoi(portStr)
+ if err != nil {
+ log.Debugf("failed to parse mac address table port on interface: %s", iface)
+ continue
+ }
+
+ mac, err := net.ParseMAC(entry.MacAddress)
+ if err != nil {
+ log.Errorf("Invalid mac address entry %s: %v", entry.MacAddress, err)
+ continue
+ }
+
+ macTable[entry.MacAddress] = &MACTableEntry{
+ Ifname: iface,
+ Port: port,
+ VLAN: strconv.Itoa(entry.Vlan),
+ Type: entry.State.EntryType,
+ MAC: mac,
+ }
+ }
+
+ log.Infof("Received %d entries", len(macTable))
+ return macTable, nil
+
+}
+
+type sonicLLDP struct {
+ Root struct {
+ iface []struct {
+ name string
+ neighbors []struct {
+ neighbor struct {
+ id string
+ state struct {
+ ChassisId string `json:"chassis-id"`
+ ChassisidType string `json:"chassis-id-type"`
+ Id string
+ ManagementAddress string `json:"management-address"`
+ PortDescription string `json:"port-description"`
+ PortId string `json:"port-id"`
+ PortIdType string `json:"port-id-type"`
+ SystemDescription string `json:"system-description"`
+ SystemName string `json:"system-name"`
+ Ttl int
+ }
+ }
+ }
+ }
+ } `json:"openconfig-lldp:interfaces"`
+}
+
+// TODO:
+// func (s *Sonic) GetLLDPNeighbors() (LLDPNeighbors, error) {
+// var lldpRaw *sonicLLDP
+// res, err := s.getRequest(SONIC_RESTCONF_LLDP)
+// if err != nil {
+// return nil, err
+// }
+// err = json.Unmarshal(res, &lldpRaw)
+// if err != nil {
+// return nil, err
+// }
+
+// o := make(LLDPNeighbors, 0)
+
+// for _, iface := range lldpRaw.Root.iface {
+// for _, n := range iface.neighbors {
+// state := n.neighbor.state
+// o[iface.name] = &LLDP{
+// ChassisId: state.ChassisId,
+// ChassisIdType: state.ChassisidType,
+// ManagementAddress: state.ManagementAddress,
+// PortDescription: state.PortDescription,
+// PortId: state.PortId,
+// PortIdType: state.PortIdType,
+// SystemDescription: state.SystemDescription,
+// SystemName: state.SystemName,
+// }
+// }
+// }
+
+// return nil, nil
+// }
diff --git a/tors/sonic_test.go b/tors/sonic_test.go
new file mode 100644
index 0000000..b0bc75a
--- /dev/null
+++ b/tors/sonic_test.go
@@ -0,0 +1,52 @@
+// Copyright 2019 Grendel Authors. All rights reserved.
+//
+// This file is part of Grendel.
+//
+// Grendel is free software: you can redistribute it and/or modify
+// it under the terms of the GNU General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// Grendel is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+// GNU General Public License for more details.
+//
+// You should have received a copy of the GNU General Public License
+// along with Grendel. If not, see .
+
+package tors
+
+import (
+ "fmt"
+ "os"
+ "testing"
+)
+
+func TestSonic(t *testing.T) {
+ endpoint := os.Getenv("GRENDEL_SONIC_ENDPOINT")
+ user := os.Getenv("GRENDEL_SONIC_USER")
+ pass := os.Getenv("GRENDEL_SONIC_PASS")
+
+ if endpoint == "" || user == "" || pass == "" {
+ t.Skip("Skipping SONiC test. Missing env vars")
+ }
+
+ client, err := NewSonic(endpoint, user, pass, "", true)
+ if err != nil {
+ t.Fatal(err)
+ }
+
+ macTable, err := client.GetMACTable()
+ if err != nil {
+ t.Fatal(err)
+ }
+
+ if len(macTable) == 0 {
+ t.Errorf("No mac table entries returned from api")
+ }
+
+ for _, entry := range macTable {
+ fmt.Printf("%s - %d\n", entry.MAC, entry.Port)
+ }
+}
diff --git a/tors/switch.go b/tors/switch.go
index 695d65c..25d7f1c 100644
--- a/tors/switch.go
+++ b/tors/switch.go
@@ -19,9 +19,12 @@ package tors
import (
"encoding/json"
+ "errors"
"net"
+ "github.com/spf13/viper"
"github.com/ubccr/grendel/logger"
+ "github.com/ubccr/grendel/model"
)
var log = logger.GetLogger("SWITCH")
@@ -34,10 +37,55 @@ type MACTableEntry struct {
MAC net.HardwareAddr `json:"mac-addr"`
}
+type LLDP struct {
+ ChassisIdType string
+ ChassisId string
+ SystemName string
+ SystemDescription string
+ ManagementAddress string
+ PortDescription string
+ PortId string
+ PortIdType string
+}
+
type MACTable map[string]*MACTableEntry
+type LLDPNeighbors map[string]*LLDP
+
type NetworkSwitch interface {
GetMACTable() (MACTable, error)
+ // GetLLDPNeighbors() (*LLDPNeighbors, error)
+}
+
+func NewNetworkSwitch(host *model.Host) (NetworkSwitch, error) {
+ username := viper.GetString("bmc.switch_admin_username")
+ password := viper.GetString("bmc.switch_admin_password")
+
+ if username == "" || password == "" {
+ log.Warn("Please set both bmc.switch_admin_username and bmc.switch_admin_password in your toml configuration file in order to query network switches")
+ return nil, errors.New("failed to get switch credentials from config file")
+ }
+
+ var sw NetworkSwitch
+ var err error
+
+ bmc := host.InterfaceBMC()
+ ip := ""
+ if bmc != nil {
+ ip = bmc.AddrString()
+ }
+ // TODO: automatically determine NOS
+ if host.HasTags("arista") {
+ sw, err = NewArista(ip, username, password)
+ } else if host.HasTags("sonic") {
+ sw, err = NewSonic(ip, username, password, "", true)
+ } else if host.HasTags("os10") {
+ sw, err = NewDellOS10("https://"+ip, username, password, "", true)
+ } else {
+ return nil, errors.New("failed to determine switch NOS")
+ }
+
+ return sw, err
}
func (mt MACTable) Port(port int) []*MACTableEntry {