Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Support node autodetection via IP address #11

Merged
merged 8 commits into from
Jul 22, 2024
7 changes: 7 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
90 changes: 60 additions & 30 deletions cmd/cloud-init-server/handlers.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand All @@ -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
Expand Down Expand Up @@ -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) {
Expand Down
9 changes: 6 additions & 3 deletions cmd/cloud-init-server/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -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)
}
18 changes: 5 additions & 13 deletions internal/memstore/ciMemStore.go
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand All @@ -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 &&
Expand Down
38 changes: 25 additions & 13 deletions internal/smdclient/SMDclient.go
Original file line number Diff line number Diff line change
Expand Up @@ -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: "",
}
}

Expand Down Expand Up @@ -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, &ethIfaceArray)

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, &ethIfaceArray)

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
Expand Down