Skip to content

Commit ca04705

Browse files
committed
wip: add support for node extraction -> cluster metadata
Problem: we need a extract metadata for nodes and then parse into a cluster graph Solution: create a compspec create nodes subcommand. In this PR I am adding a ClusterGraph, which still needs work to improve the output to easily map into a JGF (right now it has elements that can support any type that need further parsing). I am also generalizing the idea of plugins more, so we will have extractors and converters (that run create) but I need to finalize the design for the latter, right now the create commands are very separate. I am opening the PR sooner than later in case my computer explodes. A few problems I have run into is that NFD does not have cpu counts, let along physical vs. logical. This information is in /proc/cpuinfo for x86 but not arm. We also do not have a way to get socket -> core mapping. So likely we do need to add the hwloc extractor, and provide an automated build for doing that since it requires hwloc on the system. I will put some thought into this. Signed-off-by: vsoch <[email protected]>
1 parent 51ffdbe commit ca04705

25 files changed

+918
-338
lines changed

cmd/compspec/compspec.go

+29-9
Original file line numberDiff line numberDiff line change
@@ -54,12 +54,21 @@ func main() {
5454
cachePath := matchCmd.String("", "cache", &argparse.Options{Help: "A path to a cache for artifacts"})
5555
saveGraph := matchCmd.String("", "cache-graph", &argparse.Options{Help: "Load or use a cached graph"})
5656

57-
// Create arguments
58-
options := createCmd.StringList("a", "append", &argparse.Options{Help: "Append one or more custom metadata fields to append"})
59-
specname := createCmd.String("i", "in", &argparse.Options{Required: true, Help: "Input yaml that contains spec for creation"})
60-
specfile := createCmd.String("o", "out", &argparse.Options{Help: "Save compatibility json artifact to this file"})
61-
mediaType := createCmd.String("m", "media-type", &argparse.Options{Help: "The expected media-type for the compatibility artifact"})
62-
allowFailCreate := createCmd.Flag("f", "allow-fail", &argparse.Options{Help: "Allow any specific extractor to fail (and continue extraction)"})
57+
// Create subcommands - note that "nodes" could be cluster, but could want to make a subset of one
58+
artifactCmd := createCmd.NewCommand("artifact", "Create a new artifact")
59+
nodesCmd := createCmd.NewCommand("nodes", "Create nodes in Json Graph format from extraction data")
60+
61+
// Artifaction creation arguments
62+
options := artifactCmd.StringList("a", "append", &argparse.Options{Help: "Append one or more custom metadata fields to append"})
63+
specname := artifactCmd.String("i", "in", &argparse.Options{Required: true, Help: "Input yaml that contains spec for creation"})
64+
specfile := artifactCmd.String("o", "out", &argparse.Options{Help: "Save compatibility json artifact to this file"})
65+
mediaType := artifactCmd.String("m", "media-type", &argparse.Options{Help: "The expected media-type for the compatibility artifact"})
66+
allowFailCreate := artifactCmd.Flag("f", "allow-fail", &argparse.Options{Help: "Allow any specific extractor to fail (and continue extraction)"})
67+
68+
// Nodes creation arguments
69+
nodesOutFile := nodesCmd.String("", "nodes-output", &argparse.Options{Help: "Output json file for cluster nodes"})
70+
nodesDir := nodesCmd.String("", "node-dir", &argparse.Options{Required: true, Help: "Input directory with extraction data for nodes"})
71+
clusterName := nodesCmd.String("", "cluster-name", &argparse.Options{Required: true, Help: "Cluster name to describe in graph"})
6372

6473
// Now parse the arguments
6574
err := parser.Parse(os.Args)
@@ -75,10 +84,21 @@ func main() {
7584
log.Fatalf("Issue with extraction: %s\n", err)
7685
}
7786
} else if createCmd.Happened() {
78-
err := create.Run(*specname, *options, *specfile, *allowFailCreate)
79-
if err != nil {
80-
log.Fatal(err.Error())
87+
if artifactCmd.Happened() {
88+
err := create.Artifact(*specname, *options, *specfile, *allowFailCreate)
89+
if err != nil {
90+
log.Fatal(err.Error())
91+
}
92+
} else if nodesCmd.Happened() {
93+
err := create.Nodes(*nodesDir, *clusterName, *nodesOutFile)
94+
if err != nil {
95+
log.Fatal(err.Error())
96+
}
97+
} else {
98+
fmt.Println(Header)
99+
fmt.Println("Please provide a --node-dir and (optionally) --nodes-output (json file to write)")
81100
}
101+
82102
} else if matchCmd.Happened() {
83103
err := match.Run(
84104
*manifestFile,

cmd/compspec/create/artifact.go

+127
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,127 @@
1+
package create
2+
3+
import (
4+
"fmt"
5+
"os"
6+
7+
"github.com/compspec/compspec-go/pkg/types"
8+
ep "github.com/compspec/compspec-go/plugins/extractors"
9+
10+
p "github.com/compspec/compspec-go/plugins"
11+
)
12+
13+
// Artifact will create a compatibility artifact based on a request in YAML
14+
// TODO likely want to refactor this into a proper create plugin
15+
func Artifact(specname string, fields []string, saveto string, allowFail bool) error {
16+
17+
// Cut out early if a spec not provided
18+
if specname == "" {
19+
return fmt.Errorf("a spec input -i/--input is required")
20+
}
21+
request, err := loadRequest(specname)
22+
if err != nil {
23+
return err
24+
}
25+
26+
// Right now we only know about extractors, when we define subfields
27+
// we can further filter here.
28+
extractors := request.GetExtractors()
29+
plugins, err := ep.GetPlugins(extractors)
30+
if err != nil {
31+
return err
32+
}
33+
34+
// Finally, add custom fields and extract metadata
35+
result, err := plugins.Extract(allowFail)
36+
if err != nil {
37+
return err
38+
}
39+
40+
// Update with custom fields (either new or overwrite)
41+
result.AddCustomFields(fields)
42+
43+
// The compspec returned is the populated Compatibility request!
44+
compspec, err := PopulateExtractors(&result, request)
45+
if err != nil {
46+
return err
47+
}
48+
49+
output, err := compspec.ToJson()
50+
if err != nil {
51+
return err
52+
}
53+
if saveto == "" {
54+
fmt.Println(string(output))
55+
} else {
56+
err = os.WriteFile(saveto, output, 0644)
57+
if err != nil {
58+
return err
59+
}
60+
}
61+
return nil
62+
}
63+
64+
// LoadExtractors loads a compatibility result into a compatibility request
65+
// After this we can save the populated thing into an artifact (json DUMP)
66+
func PopulateExtractors(result *ep.Result, request *types.CompatibilityRequest) (*types.CompatibilityRequest, error) {
67+
68+
// Every metadata attribute must be known under a schema
69+
schemas := request.Metadata.Schemas
70+
if len(schemas) == 0 {
71+
return nil, fmt.Errorf("the request must have one or more schemas")
72+
}
73+
for i, compat := range request.Compatibilities {
74+
75+
// The compatibility section name is a schema, and must be defined
76+
url, ok := schemas[compat.Name]
77+
if !ok {
78+
return nil, fmt.Errorf("%s is missing a schema", compat.Name)
79+
}
80+
if url == "" {
81+
return nil, fmt.Errorf("%s has an empty schema", compat.Name)
82+
}
83+
84+
for key, extractorKey := range compat.Attributes {
85+
86+
// Get the extractor, section, and subfield from the extractor lookup key
87+
f, err := p.ParseField(extractorKey)
88+
if err != nil {
89+
fmt.Printf("warning: cannot parse %s: %s, setting to empty\n", key, extractorKey)
90+
compat.Attributes[key] = ""
91+
continue
92+
}
93+
94+
// If we get here, we can parse it and look it up in our result metadata
95+
extractor, ok := result.Results[f.Extractor]
96+
if !ok {
97+
fmt.Printf("warning: extractor %s is unknown, setting to empty\n", f.Extractor)
98+
compat.Attributes[key] = ""
99+
continue
100+
}
101+
102+
// Now get the section
103+
section, ok := extractor.Sections[f.Section]
104+
if !ok {
105+
fmt.Printf("warning: section %s.%s is unknown, setting to empty\n", f.Extractor, f.Section)
106+
compat.Attributes[key] = ""
107+
continue
108+
}
109+
110+
// Now get the value!
111+
value, ok := section[f.Field]
112+
if !ok {
113+
fmt.Printf("warning: field %s.%s.%s is unknown, setting to empty\n", f.Extractor, f.Section, f.Field)
114+
compat.Attributes[key] = ""
115+
continue
116+
}
117+
118+
// If we get here - we found it! Hooray!
119+
compat.Attributes[key] = value
120+
}
121+
122+
// Update the compatibiity
123+
request.Compatibilities[i] = compat
124+
}
125+
126+
return request, nil
127+
}

cmd/compspec/create/create.go

-117
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,9 @@
11
package create
22

33
import (
4-
"fmt"
54
"os"
65

76
"github.com/compspec/compspec-go/pkg/types"
8-
p "github.com/compspec/compspec-go/plugins"
97
"sigs.k8s.io/yaml"
108
)
119

@@ -23,118 +21,3 @@ func loadRequest(filename string) (*types.CompatibilityRequest, error) {
2321
}
2422
return &request, nil
2523
}
26-
27-
// Run will create a compatibility artifact based on a request in YAML
28-
func Run(specname string, fields []string, saveto string, allowFail bool) error {
29-
30-
// Cut out early if a spec not provided
31-
if specname == "" {
32-
return fmt.Errorf("A spec input -i/--input is required")
33-
}
34-
request, err := loadRequest(specname)
35-
if err != nil {
36-
return err
37-
}
38-
39-
// Right now we only know about extractors, when we define subfields
40-
// we can further filter here.
41-
extractors := request.GetExtractors()
42-
plugins, err := p.GetPlugins(extractors)
43-
if err != nil {
44-
return err
45-
}
46-
47-
// Finally, add custom fields and extract metadata
48-
result, err := plugins.Extract(allowFail)
49-
if err != nil {
50-
return err
51-
}
52-
53-
// Update with custom fields (either new or overwrite)
54-
result.AddCustomFields(fields)
55-
56-
// The compspec returned is the populated Compatibility request!
57-
compspec, err := PopulateExtractors(&result, request)
58-
if err != nil {
59-
return err
60-
}
61-
62-
output, err := compspec.ToJson()
63-
if err != nil {
64-
return err
65-
}
66-
if saveto == "" {
67-
fmt.Println(string(output))
68-
} else {
69-
err = os.WriteFile(saveto, output, 0644)
70-
if err != nil {
71-
return err
72-
}
73-
}
74-
return nil
75-
}
76-
77-
// LoadExtractors loads a compatibility result into a compatibility request
78-
// After this we can save the populated thing into an artifact (json DUMP)
79-
func PopulateExtractors(result *p.Result, request *types.CompatibilityRequest) (*types.CompatibilityRequest, error) {
80-
81-
// Every metadata attribute must be known under a schema
82-
schemas := request.Metadata.Schemas
83-
if len(schemas) == 0 {
84-
return nil, fmt.Errorf("the request must have one or more schemas")
85-
}
86-
for i, compat := range request.Compatibilities {
87-
88-
// The compatibility section name is a schema, and must be defined
89-
url, ok := schemas[compat.Name]
90-
if !ok {
91-
return nil, fmt.Errorf("%s is missing a schema", compat.Name)
92-
}
93-
if url == "" {
94-
return nil, fmt.Errorf("%s has an empty schema", compat.Name)
95-
}
96-
97-
for key, extractorKey := range compat.Attributes {
98-
99-
// Get the extractor, section, and subfield from the extractor lookup key
100-
f, err := p.ParseField(extractorKey)
101-
if err != nil {
102-
fmt.Printf("warning: cannot parse %s: %s, setting to empty\n", key, extractorKey)
103-
compat.Attributes[key] = ""
104-
continue
105-
}
106-
107-
// If we get here, we can parse it and look it up in our result metadata
108-
extractor, ok := result.Results[f.Extractor]
109-
if !ok {
110-
fmt.Printf("warning: extractor %s is unknown, setting to empty\n", f.Extractor)
111-
compat.Attributes[key] = ""
112-
continue
113-
}
114-
115-
// Now get the section
116-
section, ok := extractor.Sections[f.Section]
117-
if !ok {
118-
fmt.Printf("warning: section %s.%s is unknown, setting to empty\n", f.Extractor, f.Section)
119-
compat.Attributes[key] = ""
120-
continue
121-
}
122-
123-
// Now get the value!
124-
value, ok := section[f.Field]
125-
if !ok {
126-
fmt.Printf("warning: field %s.%s.%s is unknown, setting to empty\n", f.Extractor, f.Section, f.Field)
127-
compat.Attributes[key] = ""
128-
continue
129-
}
130-
131-
// If we get here - we found it! Hooray!
132-
compat.Attributes[key] = value
133-
}
134-
135-
// Update the compatibiity
136-
request.Compatibilities[i] = compat
137-
}
138-
139-
return request, nil
140-
}

0 commit comments

Comments
 (0)