Skip to content

Commit

Permalink
Merge pull request #11 from OpenCHAMI/lritzdorf/autodetect-node
Browse files Browse the repository at this point in the history
Support node autodetection via IP address
  • Loading branch information
LRitzdorf authored Jul 22, 2024
2 parents e99542a + 217abab commit 51d12f8
Show file tree
Hide file tree
Showing 5 changed files with 103 additions and 59 deletions.
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

0 comments on commit 51d12f8

Please sign in to comment.