diff --git a/CHANGELOG.md b/CHANGELOG.md index edd8a90..838dbe7 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,13 @@ All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). +## [0.1.1] - 2024-07-19 + +### Added + +- Supports `/cloud-init[-secure]/{user,meta,vendor}-data` endpoints, which auto-detect the querying node's IP address and look up the corresponding xname in SMD + - This is in contrast to the existing MAC-based endpoints, which remain functional + ## [0.1.0] - 2024-07-17 ### Added diff --git a/cmd/cloud-init-server/handlers.go b/cmd/cloud-init-server/handlers.go index e188939..8348341 100644 --- a/cmd/cloud-init-server/handlers.go +++ b/cmd/cloud-init-server/handlers.go @@ -4,7 +4,9 @@ import ( "encoding/json" "fmt" "io" + "log" "net/http" + "strings" "github.com/OpenCHAMI/cloud-init/internal/memstore" "github.com/OpenCHAMI/cloud-init/internal/smdclient" @@ -26,6 +28,15 @@ func NewCiHandler(s ciStore, c *smdclient.SMDClient) *CiHandler { } } +// Enumeration for cloud-init data categories +type ciDataKind uint +// Takes advantage of implicit repetition and iota's auto-incrementing +const ( + UserData ciDataKind = iota + MetaData + VendorData +) + // ListEntries godoc // @Summary List all cloud-init entries // @Description List all cloud-init entries @@ -95,50 +106,69 @@ func (h CiHandler) GetEntry(w http.ResponseWriter, r *http.Request) { } } -func (h CiHandler) GetUserData(w http.ResponseWriter, r *http.Request) { - id := chi.URLParam(r, "id") - - ci, err := h.store.Get(id, h.sm) - if err != nil { - http.Error(w, err.Error(), http.StatusNotFound) - } - ud, err := yaml.Marshal(ci.CIData.UserData) - if err != nil { - fmt.Print(err) +func (h CiHandler) GetDataByMAC(dataKind ciDataKind) func(w http.ResponseWriter, r *http.Request) { + return func(w http.ResponseWriter, r *http.Request) { + id := chi.URLParam(r, "id") + // Retrieve the node's xname based on MAC address + name, err := h.sm.IDfromMAC(id) + if err != nil { + log.Print(err) + name = id // Fall back to using the given name as-is + } else { + log.Printf("xname %s with mac %s found\n", name, id) + } + // Actually respond with the data + h.getData(name, dataKind, w) } - s := fmt.Sprintf("#cloud-config\n%s", string(ud[:])) - w.Header().Set("Content-Type", "text/yaml") - w.Write([]byte(s)) } -func (h CiHandler) GetMetaData(w http.ResponseWriter, r *http.Request) { - id := chi.URLParam(r, "id") - - ci, err := h.store.Get(id, h.sm) - if err != nil { - http.Error(w, err.Error(), http.StatusNotFound) - } - md, err := yaml.Marshal(ci.CIData.MetaData) - if err != nil { - fmt.Print(err) +func (h CiHandler) GetDataByIP(dataKind ciDataKind) func(w http.ResponseWriter, r *http.Request) { + return func(w http.ResponseWriter, r *http.Request) { + // Strip port number from RemoteAddr to obtain raw IP + portIndex := strings.LastIndex(r.RemoteAddr, ":") + var ip string + if portIndex > 0 { + ip = r.RemoteAddr[:portIndex] + } else { + ip = r.RemoteAddr + } + // Retrieve the node's xname based on IP address + name, err := h.sm.IDfromIP(ip) + if err != nil { + log.Print(err) + w.WriteHeader(http.StatusUnprocessableEntity) + return + } else { + log.Printf("xname %s with ip %s found\n", name, ip) + } + // Actually respond with the data + h.getData(name, dataKind, w) } - w.Header().Set("Content-Type", "text/yaml") - w.Write([]byte(md)) } -func (h CiHandler) GetVendorData(w http.ResponseWriter, r *http.Request) { - id := chi.URLParam(r, "id") - +func (h CiHandler) getData(id string, dataKind ciDataKind, w http.ResponseWriter) { ci, err := h.store.Get(id, h.sm) if err != nil { http.Error(w, err.Error(), http.StatusNotFound) } - md, err := yaml.Marshal(ci.CIData.VendorData) + + var data *map[string]interface{} + switch dataKind { + case UserData: + w.Write([]byte("#cloud-config\n")) + data = &ci.CIData.UserData + case MetaData: + data = &ci.CIData.MetaData + case VendorData: + data = &ci.CIData.VendorData + } + + ydata, err := yaml.Marshal(data) if err != nil { fmt.Print(err) } w.Header().Set("Content-Type", "text/yaml") - w.Write([]byte(md)) + w.Write([]byte(ydata)) } func (h CiHandler) UpdateEntry(w http.ResponseWriter, r *http.Request) { diff --git a/cmd/cloud-init-server/main.go b/cmd/cloud-init-server/main.go index ec20954..636d50f 100644 --- a/cmd/cloud-init-server/main.go +++ b/cmd/cloud-init-server/main.go @@ -83,10 +83,13 @@ func initCiRouter(router chi.Router, handler *CiHandler) { // Add cloud-init endpoints to router router.Get("/", handler.ListEntries) router.Post("/", handler.AddEntry) + router.Get("/user-data", handler.GetDataByIP(UserData)) + router.Get("/meta-data", handler.GetDataByIP(MetaData)) + router.Get("/vendor-data", handler.GetDataByIP(VendorData)) router.Get("/{id}", handler.GetEntry) - router.Get("/{id}/user-data", handler.GetUserData) - router.Get("/{id}/meta-data", handler.GetMetaData) - router.Get("/{id}/vendor-data", handler.GetVendorData) + router.Get("/{id}/user-data", handler.GetDataByMAC(UserData)) + router.Get("/{id}/meta-data", handler.GetDataByMAC(MetaData)) + router.Get("/{id}/vendor-data", handler.GetDataByMAC(VendorData)) router.Put("/{id}", handler.UpdateEntry) router.Delete("/{id}", handler.DeleteEntry) } diff --git a/internal/memstore/ciMemStore.go b/internal/memstore/ciMemStore.go index 6566c76..0e215ee 100644 --- a/internal/memstore/ciMemStore.go +++ b/internal/memstore/ciMemStore.go @@ -60,19 +60,11 @@ func (m MemStore) Get(name string, sm *smdclient.SMDClient) (citypes.CI, error) ci_merged := new(citypes.CI) - id, err := sm.IDfromMAC(name) - if err != nil { - log.Print(err) - id = name // Fall back to using the given name as an ID - } else { - log.Printf("xname %s with mac %s found\n", id, name) - } - - gl, err := sm.GroupMembership(id) + gl, err := sm.GroupMembership(name) if err != nil { log.Print(err) } else if len(gl) > 0 { - log.Printf("xname %s is a member of these groups: %s\n", id, gl) + log.Printf("Node %s is a member of these groups: %s\n", name, gl) for g := 0; g < len(gl); g++ { if val, ok := m.list[gl[g]]; ok { @@ -82,15 +74,15 @@ func (m MemStore) Get(name string, sm *smdclient.SMDClient) (citypes.CI, error) } } } else { - log.Printf("ID %s is not a member of any groups\n", id) + log.Printf("Node %s is not a member of any groups\n", name) } - if val, ok := m.list[id]; ok { + if val, ok := m.list[name]; ok { ci_merged.CIData.UserData = lo.Assign(ci_merged.CIData.UserData, val.CIData.UserData) ci_merged.CIData.VendorData = lo.Assign(ci_merged.CIData.VendorData, val.CIData.VendorData) ci_merged.CIData.MetaData = lo.Assign(ci_merged.CIData.MetaData, val.CIData.MetaData) } else { - log.Printf("ID %s has no specific configuration\n", id) + log.Printf("Node %s has no specific configuration\n", name) } if len(ci_merged.CIData.UserData) == 0 && diff --git a/internal/smdclient/SMDclient.go b/internal/smdclient/SMDclient.go index 63174ea..105ff9c 100644 --- a/internal/smdclient/SMDclient.go +++ b/internal/smdclient/SMDclient.go @@ -35,10 +35,10 @@ type SMDClient struct { func NewSMDClient(baseurl string, jwtURL string) *SMDClient { c := &http.Client{Timeout: 2 * time.Second} return &SMDClient{ - smdClient: c, - smdBaseURL: baseurl, + smdClient: c, + smdBaseURL: baseurl, tokenEndpoint: jwtURL, - accessToken: "", + accessToken: "", } } @@ -87,20 +87,32 @@ func (s *SMDClient) getSMD(ep string, smd interface{}) error { // IDfromMAC returns the ID of the xname that has the MAC address func (s *SMDClient) IDfromMAC(mac string) (string, error) { - endpointData := new(sm.ComponentEndpointArray) - ep := "/hsm/v2/Inventory/ComponentEndpoints/" - s.getSMD(ep, endpointData) + var ethIfaceArray []sm.CompEthInterfaceV2 + ep := "/hsm/v2/Inventory/EthernetInterfaces/" + s.getSMD(ep, ðIfaceArray) - for _, ep := range endpointData.ComponentEndpoints { - id := ep.ID - nics := ep.RedfishSystemInfo.EthNICInfo - for _, v := range nics { - if strings.EqualFold(mac, v.MACAddress) { - return id, nil + for _, ep := range ethIfaceArray { + if strings.EqualFold(mac, ep.MACAddr) { + return ep.CompID, nil + } + } + return "", errors.New("MAC " + mac + " not found for an xname in EthernetInterfaces") +} + +// IDfromIP returns the ID of the xname that has the IP address +func (s *SMDClient) IDfromIP(ipaddr string) (string, error) { + var ethIfaceArray []sm.CompEthInterfaceV2 + ep := "/hsm/v2/Inventory/EthernetInterfaces/" + s.getSMD(ep, ðIfaceArray) + + for _, ep := range ethIfaceArray { + for _, v := range ep.IPAddrs { + if strings.EqualFold(ipaddr, v.IPAddr) { + return ep.CompID, nil } } } - return "", errors.New("MAC " + mac + " not found for an xname in ComponentEndpoints") + return "", errors.New("IP address " + ipaddr + " not found for an xname in EthernetInterfaces") } // GroupMembership returns the group labels for the xname with the given ID