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:

+ +
+
+
+
+
+ + + +
+
+
+ + +
+ +
+
+
+
+
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 {