Skip to content

Commit b0e2a4b

Browse files
committed
Refactor BMC ID Mapping and add deterministic generated XNAMEs
Signed-off-by: Eric Lund <77127214+erl-hpe@users.noreply.github.com>
1 parent 9069de8 commit b0e2a4b

9 files changed

Lines changed: 315 additions & 120 deletions

File tree

README.md

Lines changed: 7 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -140,7 +140,7 @@ This should return a JSON response with general information. The output below ha
140140

141141
### BMC ID Mapping
142142

143-
While the `magellan collect` command collects data from RedFish servers and can produce node data suitable for use with SMD, the RedFish data has no defined way to provide BMC IDs that SMD can use. SMD consumes BMC IDs in the form of XNAMEs, which are names that provide both unique identification within a cluster and the topographical location information needed to phyically identify a BMC. Since RedFish does not provide a way to communicate BMC XNAMEs or other meaningful BMC IDs, `magellan` provides a mechanism to generate meaningful BMC IDs based on an external mapping. The `--bmc-id-map` (`-m`) option to `magellan` provides this mapping either in the form of a command line JSON string or in the form of a JSON or YAML file (by prepending the path to the file with an `@` sign). The YAML form of the mapping data is as follows:
143+
While the `magellan collect` command collects data from RedFish servers and can produce node data suitable for use with SMD, the RedFish data has no defined way to provide BMC IDs that have the are "meaningful" with respect to location semantics that some consumers of SMD data require. SMD consumes BMC IDs in the form of XNAMEs, which are names that provide both unique identification within a cluster and the topographical location information needed to phyically identify a BMC. Since RedFish does not provide a way to communicate BMC XNAMEs or other meaningful BMC IDs, `magellan` provides a mechanism to generate meaningful BMC IDs based on an external mapping. The `--bmc-id-map` (`-m`) option to `magellan` provides this mapping either in the form of a command line JSON string or in the form of a JSON or YAML file (by prepending the path to the file with an `@` sign). The YAML form of the mapping data is as follows:
144144

145145
```yaml
146146
map_key: bmc-ip-addr
@@ -159,7 +159,7 @@ x<cabinet>c<chassis>s<shelf>b<blade>
159159

160160
where `<cabinet>` is a cabinet number in the cluster, `<chassis>` is a chassis within the cabinet, `<shelf>` is the shelf within the chassis and `<blade>` is the blade within a shelf where the BMC is located. The above mapping file (minus the elipsis) will work with the example described in the [Starting the Emulator](#starting-the-emulator) section.
161161

162-
If you are using `magellan` within a system deployed using RIE in the [Quickstart Deployment Recipe](https://github.com/OpenCHAMI/deployment-recipes/blob/main/quickstart/README.md) you will need to generate a BMC ID Map from the RIE instances running under `docker-compose`. You can do this outside of the docker containers by running this script:
162+
If you are using `magellan` within a system deployed using RIE in the [Quickstart Deployment Recipe](https://github.com/OpenCHAMI/deployment-recipes/blob/main/quickstart/README.md) you can generate a BMC ID Map with XNAMEs that match the RIE configured XNAMEs from the RIE instances running under `docker-compose`. You can do this outside of the docker containers by running this script:
163163

164164
```bash
165165
#! /bin/sh
@@ -184,12 +184,14 @@ directing the output into a ID mapping file, then copying the ID mapping file in
184184
magellan collect --bmc-id-map @my_bmc_id_map.yaml -o nodes.yaml
185185
```
186186

187-
If you have real BMCs present in your system in addition to those presented by RIE, you will need to add their IP Address to XNAME mapping to the BMC ID Map. How you do that for any given configuration is beyond the scope of this README.
187+
If you have real BMCs present in your system in addition to those presented by RIE, and you want their XNAMEs to be meaningful, you will need to add each IP Address to XNAME mapping to the BMC ID Map. How you do that for any given configuration is beyond the scope of this README.
188+
189+
If you do not care about the meaning of XNAMEs produced by `collect` you can omit the `--bmc-id-map` (`-m`) option entirely and `collect` will generate XNAMEs algorithmically based on the IPv4 address of each BMC.
188190

189191
If you are using `magellan` in an application that is not OpenCHAMI and have a need for a different BMC ID mapping, you can construct a different kind of mapping file by replacing the XNAMEs with whatever your IDs should be.
190192

191193
> [!NOTE]
192-
> If you do not specify a BMC ID Map, the IPv4 address of the BMC will be used in the ID field of all BMC data produced. If you do provide a BMC ID Map but some of the BMC map keys (IPv4 addresses) don't match anything in the file, those BMCs will be suppressed in the resulting data. This reflects the fact that `collect` does not know how to map those BMCs to valid IDs.
194+
> If you do provide a BMC ID Map but some of the BMC map keys (IPv4 addresses) don't match anything in the map, those BMCs will be suppressed in the resulting data. This reflects the fact that `collect` does not know how to map those BMCs to meaningful IDs.
193195
194196
### Running the Tool
195197

@@ -260,14 +262,11 @@ We can then save the output and make a request with the `send` subcommand or pip
260262
--password $PASSWORD \
261263
--format yaml \
262264
--output-file nodes.yaml \
263-
--cacert cacert.pem \
264-
--bmc-id-map @my_bmc_id_map.yaml
265+
--cacert cacert.pem
265266
```
266267

267268
This will initiate a crawler to fetch inventory data from the specified BMC host. The data can be saved, viewed, or modified from standard output by setting the `-v/--verbose` flag. Similarly, this output can also be saved by using the `-o/--output-file` flag and providing a path argument.
268269

269-
To prepare a BMC ID Map (`my_bmc_id_map.yaml`) see the [BMC ID Mapping](#bmc-id-mapping).
270-
271270
To make a request with the `collect` output, we specify the `-d/--data` flag for `send`. For files, use the `@` symbol before the file path. Make sure that you set the correct input format with `-F/--format`. Finally, specify the host as a positional argument.
272271

273272
```bash
@@ -400,9 +399,6 @@ magellan collect -o node_info.json
400399
401400
This example should work just like running on real hardware, and produce a `node-info.json` output file that contains the collected data.
402401
403-
> [!NOTE]
404-
> The output from the above `magellan collect` command will not be compatible with SMD because it contains IP addresses instead of XNAMEs as BMC IDs. To generate correct XNAME BMC IDs, you must supply a BMC ID Mapping. See [BMC ID Mapping](#bmc-id-mapping) for details on how to create and specify a BMC ID Mapping when running `magellan collect`.
405-
406402
### Updating Firmware
407403
408404
The `magellan` tool is capable of updating firmware with using the `update` subcommand via the Redfish API. This may sometimes necessary if some of the `collect` output is missing or is not including what is expected. The subcommand expects to find a running HTTP/HTTPS server that has an accessible URL path to the firmware download. Specify the URL with the `--firmware-path` flag and the firmware type with the `--component` flag (optional) with all the other usual arguments like in the example below:

cmd/collect.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -129,7 +129,7 @@ var CollectCmd = &cobra.Command{
129129
ForceUpdate: forceUpdate,
130130
AccessToken: accessToken,
131131
SecretStore: store,
132-
BMCIdMap: idMap,
132+
BMCIDMap: idMap,
133133
}
134134

135135
// show all of the 'collect' parameters being set from CLI if verbose

go.mod

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,8 @@
11
module github.com/OpenCHAMI/magellan
22

3-
go 1.21
3+
go 1.23
4+
5+
toolchain go1.24.5
46

57
require (
68
github.com/cznic/mathutil v0.0.0-20181122101859-297441e03548
@@ -17,6 +19,7 @@ require (
1719

1820
require (
1921
dario.cat/mergo v1.0.2
22+
github.com/Cray-HPE/hms-xname v1.4.0
2023
github.com/rs/zerolog v1.33.0
2124
golang.org/x/crypto v0.32.0
2225
gopkg.in/yaml.v3 v3.0.1

go.sum

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,8 @@ dario.cat/mergo v1.0.2 h1:85+piFYR1tMbRrLcDwR18y4UKJ3aH1Tbzi24VRW1TK8=
22
dario.cat/mergo v1.0.2/go.mod h1:E/hbnu0NxMFBjpMIE34DRGLWqDy0g5FuKDhCb31ngxA=
33
filippo.io/edwards25519 v1.1.0 h1:FNf4tywRC1HmFuKW5xopWpigGjJKiJSV0Cqo0cJWDaA=
44
filippo.io/edwards25519 v1.1.0/go.mod h1:BxyFTGdWcka3PhytdK4V28tE5sGfRvvvRV7EaN4VDT4=
5+
github.com/Cray-HPE/hms-xname v1.4.0 h1:i47YmE8rbSfJ64simKCCC6ZVcGid3rDIX6/jfVbISAM=
6+
github.com/Cray-HPE/hms-xname v1.4.0/go.mod h1:wH7t1UXYck0VdHSWjrMsxZmaCK5W1lmwgNnsYAFPTus=
57
github.com/coreos/go-systemd/v22 v22.5.0/go.mod h1:Y58oyj3AT4RCenI/lSvhwexgC+NSVTIJ3seZv2GcEnc=
68
github.com/cpuguy83/go-md2man/v2 v2.0.4/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o=
79
github.com/cznic/mathutil v0.0.0-20181122101859-297441e03548 h1:iwZdTE0PVqJCos1vaoKsclOGD3ADKpshg3SRtYBbwso=

internal/util/net.go

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
package util
2+
3+
import (
4+
"fmt"
5+
"net"
6+
)
7+
8+
func IPAddrStrToInt(ipStr string)(int, error) {
9+
// Generate an integer from an IP address. This is not
10+
// sensitive to byte ordering, so the integer produced on
11+
// different systems may be different. It will be consistent
12+
// for any specific architecture.
13+
ip := net.ParseIP(ipStr).To4()
14+
if ip == nil {
15+
return 0, fmt.Errorf("cannot convert invalid IPv4 address string '%s' to integer", ipStr)
16+
}
17+
return (int(ip[0]) * (1 << 24)) + (int(ip[1]) * (1 << 16)) + (int(ip[2]) * (1 << 8)) + int(ip[3]), nil
18+
}

pkg/collect.go

Lines changed: 15 additions & 107 deletions
Original file line numberDiff line numberDiff line change
@@ -39,19 +39,10 @@ type CollectParams struct {
3939
Format string // set the output format
4040
ForceUpdate bool // set whether to force updating SMD with 'force-update' flag
4141
AccessToken string // set the access token to include in request with 'access-token' flag
42-
BMCIdMap string // Set the path to the BMC ID mapping YAML or JSON file (if any)
42+
BMCIDMap string // Set the path to the BMC ID mapping YAML or JSON data or file name (if any)
4343
SecretStore secrets.SecretStore // set BMC credentials
4444
}
4545

46-
// BMCIdMap contains the mapping of host address strings to BMC Identifiers
47-
// supplied by the --bmc-id-map option to collect. IdMap is the mapping itself,
48-
// MapKey specifies what string to use as the key to the map. For now, that is
49-
// always 'bmc-ip-addr'. In the future other options may be available.
50-
type BMCIdMap struct {
51-
IdMap map[string]string `json:"id_map" yaml:"id_map"`
52-
MapKey string `json:"map_key" yaml:"map_key"`
53-
}
54-
5546
// This is the main function used to collect information from the BMC nodes via Redfish.
5647
// The results of the collect are stored in a cache specified with the `--cache` flag.
5748
// The function expects a list of hosts found using the `ScanForAssets()` function.
@@ -74,27 +65,9 @@ func CollectInventory(assets *[]RemoteAsset, params *CollectParams) ([]map[strin
7465
found = make([]string, 0, len(*assets))
7566
done = make(chan struct{}, params.Concurrency+1)
7667
chanAssets = make(chan RemoteAsset, params.Concurrency+1)
77-
bmcIdMap *BMCIdMap
68+
mapper idMapper = pickIDMapper(params)
7869
err error
7970
)
80-
// Get the host to BMC ID mapping
81-
bmcIdMap, err = getBMCIdMap(params.BMCIdMap, params.Format)
82-
if err != nil {
83-
return nil, err
84-
}
85-
// Validate the MapKey field in the ID Map if a map was found
86-
// (the only value currently allowed is 'bmc-ip-addr', but
87-
// this is where any other legal values would be added).
88-
if bmcIdMap != nil {
89-
switch bmcIdMap.MapKey {
90-
case "bmc-ip-addr":
91-
break
92-
default:
93-
return nil, fmt.Errorf("invalid 'map_key' field '%s' in BMC ID Map a valid value is 'bmc-ip-addr", bmcIdMap.MapKey)
94-
}
95-
} else {
96-
log.Warn().Msg("no BMC ID Map (--bmc-id-map string option) provided, BMC IDs will be IP addresses which are incompatible with SMD")
97-
}
9871

9972
// set the client's params from CLI
10073
wg.Add(params.Concurrency)
@@ -109,13 +82,18 @@ func CollectInventory(assets *[]RemoteAsset, params *CollectParams) ([]map[strin
10982

11083
trimmedHost := strings.TrimPrefix(sr.Host, "https://")
11184
uri := fmt.Sprintf("%s:%d", sr.Host, sr.Port)
112-
bmcId := getBMCId(bmcIdMap, trimmedHost)
113-
114-
// If bmcId is empty, skip this BMC. Empty means that there
115-
// is a valid mapping, but there was no match for this host
116-
// in the mapping, meaning that the BMC is unrecognized. Skip
117-
// this BMC.
118-
if bmcId == "" {
85+
keys := &idMapperKeys {
86+
IPv4Addr: trimmedHost,
87+
}
88+
bmcID := mapper.getMappedID(keys)
89+
90+
// If bmcID is empty, skip this
91+
// BMC. Empty means that there is a
92+
// valid mapping, but there was no
93+
// match for this host in the mapping,
94+
// meaning that the BMC is
95+
// unrecognized. Skip this BMC.
96+
if bmcID == "" {
11997
continue
12098
}
12199

@@ -154,7 +132,7 @@ func CollectInventory(assets *[]RemoteAsset, params *CollectParams) ([]map[strin
154132

155133
// data to be sent to smd
156134
data := map[string]any{
157-
"ID": bmcId,
135+
"ID": bmcID,
158136
"Type": "",
159137
"Name": "",
160138
"FQDN": strings.TrimPrefix(sr.Host, "https://"),
@@ -359,73 +337,3 @@ func FindMACAddressWithIP(config crawler.CrawlerConfig, targetIP net.IP) (string
359337
// no matches found, so return an empty string
360338
return "", fmt.Errorf("no ethernet interfaces found with IP address")
361339
}
362-
363-
func getBMCIdMap(data string, format string)(*BMCIdMap, error) {
364-
// If no mapping is provided, there is no error, but there is
365-
// also no mapping, just return nil with no error and let the
366-
// caller pass that around.
367-
if data == "" {
368-
return nil, nil
369-
}
370-
371-
var bmcIdMap BMCIdMap
372-
// First, check whether 'data' specifies a file (i.e. starts
373-
// with '@'). If not, it should be a JSON string containing the
374-
// map data. Otherwise, strip the '@' and fall through.
375-
if data[0] != '@' {
376-
err := json.Unmarshal([]byte(data), &bmcIdMap)
377-
if err != nil {
378-
return nil, err
379-
}
380-
return &bmcIdMap, nil
381-
}
382-
383-
// The map data is in a file. Get the path from what comes
384-
// after the '@' and process it.
385-
path := data[1:]
386-
387-
// Read in the contents of the map file, since we are going to
388-
// do that no matter what type it is...
389-
input, err := os.ReadFile(path)
390-
if err != nil {
391-
return nil, fmt.Errorf("error reading BMC ID mapping file '%s': %v", path, err)
392-
}
393-
394-
// Decode the file based on the appropriate format.
395-
switch util.DataFormatFromFileExt(path, format) {
396-
case util.FORMAT_JSON:
397-
// Read in JSON file
398-
err := json.Unmarshal(input, &bmcIdMap)
399-
if err != nil {
400-
return nil, err
401-
}
402-
case util.FORMAT_YAML:
403-
// Read in YAML file
404-
err := yaml.Unmarshal(input, &bmcIdMap)
405-
if err != nil {
406-
return nil, err
407-
}
408-
}
409-
return &bmcIdMap, nil
410-
}
411-
412-
// Generate a BMC ID string associated with 'selector' in the provided
413-
// 'BMCIdMap'. If there is no map, then return the selector string
414-
// itself. If the map is present but the host is not present in the
415-
// map, then log a warning and return an empty string indicating that
416-
// the BMC ID was not composed.
417-
func getBMCId(bmcIdMap *BMCIdMap, selector string) (string) {
418-
if bmcIdMap == nil {
419-
return selector
420-
}
421-
// Go does not error out on string map references that do not
422-
// match the selector, it simply produces an empty
423-
// string. Recognize that case and log it, then return an
424-
// empty string.
425-
bmcId := bmcIdMap.IdMap[selector]
426-
if bmcId == "" {
427-
log.Warn().Msgf("no mapping found from host selector '%v' to a BMC ID", selector)
428-
return ""
429-
}
430-
return bmcId
431-
}

pkg/idmap.go

Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,62 @@
1+
// Package magellan implements the core routines for the tools.
2+
package magellan
3+
4+
import "github.com/rs/zerolog/log"
5+
6+
// This file is the top-level of the BMC ID Mapping infrastructure. It
7+
// defines the interface into all BMC ID Mappers, the structure of the
8+
// parameters for ID Mapping, and a function for picking a BMC ID
9+
// Mapper to use.
10+
//
11+
// Implementations of specific ID mappers are found in files with the
12+
// prefix 'idmap_' in this directory. To extend the BMC ID Mapping
13+
// capability, create a new mapper in its own file, and then add the
14+
// mapper selection logic for your new mapper to pickIDMapper() here.
15+
16+
// The structure passed into the GetMappedID() function as the key
17+
// options for a mapper. Currently only contains the IPv4 address of
18+
// the BMC in question.
19+
type idMapperKeys struct {
20+
IPv4Addr string
21+
}
22+
23+
type idMapper interface {
24+
initialize()(idMapper, error)
25+
getMappedID(keys *idMapperKeys)(string)
26+
}
27+
28+
// Select the correct BMC ID Mapper based on the parameters to
29+
// 'collect'.
30+
func pickIDMapper(params *CollectParams)(idMapper) {
31+
// If the parameters contain a BMC ID Map (user defined
32+
// mapping of a key to a BMC ID) then we use the userProvidedMapper
33+
// implementaiton of an ID Mapper. Until other BMC ID schemes
34+
// are implemented, the other case is simply to use the
35+
// generated XNAME mapper, generatedXNAMEMapper.
36+
var (
37+
mapper idMapper
38+
mapperName string
39+
err error
40+
)
41+
42+
if params.BMCIDMap != "" {
43+
// Always use the user provided mapper if a user
44+
// provided map is present.
45+
mapperName = "userProvidedMapper"
46+
mapper = userProvidedMapper {
47+
IDMapStr: params.BMCIDMap,
48+
IDMapFormat: params.Format,
49+
}
50+
} else {
51+
// Currently the only other mapper is the generated
52+
// XNAMEs mapper, since no user provided mapper was
53+
// offered, use that instead.
54+
mapperName = "generatedXNAMEMapper"
55+
mapper = generatedXNAMEMapper{}
56+
}
57+
mapper, err = mapper.initialize()
58+
if err != nil {
59+
log.Error().Err(err).Str("Mapper Name", mapperName).Msg("failed to initialized BMC ID Mapper")
60+
}
61+
return mapper
62+
}

0 commit comments

Comments
 (0)