master
go 180 lines 3.82 KB
Raw
1 // SPDX-License-Identifier: GPL-3.0-or-later
2
3 package vnodes
4
5 import (
6 _ "embed"
7 "fmt"
8 "io"
9 "io/fs"
10 "log/slog"
11 "maps"
12 "os"
13 "path/filepath"
14
15 "github.com/google/uuid"
16 "gopkg.in/yaml.v2"
17
18 "github.com/netdata/netdata/go/plugins/logger"
19 "github.com/netdata/netdata/go/plugins/pkg/pluginconfig"
20 )
21
22 //go:embed "config_schema.json"
23 var ConfigSchema string
24
25 var log = logger.New().With(
26 slog.String("component", "vnodes"),
27 )
28
29 func Load(dir string) map[string]*VirtualNode {
30 return readConfDir(dir)
31 }
32
33 type VirtualNode struct {
34 Name string `yaml:"name" json:"name"`
35 Hostname string `yaml:"hostname" json:"hostname"`
36 GUID string `yaml:"guid" json:"guid"`
37 Labels map[string]string `yaml:"labels,omitempty" json:"labels"`
38
39 Source string `yaml:"-" json:"-"`
40 SourceType string `yaml:"-" json:"-"`
41 }
42
43 func (v *VirtualNode) Copy() *VirtualNode {
44 if v == nil {
45 return nil
46 }
47
48 labels := make(map[string]string, len(v.Labels))
49 maps.Copy(labels, v.Labels)
50
51 return &VirtualNode{
52 Name: v.Name,
53 Hostname: v.Hostname,
54 GUID: v.GUID,
55 Source: v.Source,
56 SourceType: v.SourceType,
57 Labels: labels,
58 }
59 }
60
61 func (v *VirtualNode) Equal(vn *VirtualNode) bool {
62 return v.Name == vn.Name &&
63 v.Hostname == vn.Hostname &&
64 v.GUID == vn.GUID &&
65 maps.Equal(v.Labels, vn.Labels)
66 }
67
68 func readConfDir(dir string) map[string]*VirtualNode {
69 vnodes := make(map[string]*VirtualNode)
70
71 _ = filepath.WalkDir(dir, func(path string, d fs.DirEntry, err error) error {
72 if err != nil {
73 log.Warning(err)
74 return nil
75 }
76
77 if d.Type()&os.ModeSymlink != 0 {
78 dst, err := os.Readlink(path)
79 if err != nil {
80 log.Warningf("failed to resolve symlink '%s': %v", path, err)
81 return nil
82 }
83
84 if !filepath.IsAbs(dst) {
85 dst = filepath.Join(filepath.Dir(path), filepath.Clean(dst))
86 }
87
88 fi, err := os.Stat(dst)
89 if err != nil {
90 log.Warningf("failed to stat resolved path '%s': %v", dst, err)
91 return nil
92 }
93 if !fi.Mode().IsRegular() {
94 log.Debugf("'%s' is not a regular file, skipping it", dst)
95 return nil
96 }
97 path = dst
98 } else if !d.Type().IsRegular() {
99 log.Debugf("'%s' is not a regular file, skipping it", path)
100 return nil
101 }
102
103 if !isConfigFile(path) {
104 log.Debugf("'%s' is not a config file (wrong extension), skipping it", path)
105 return nil
106 }
107
108 var cfg []VirtualNode
109
110 if err := loadConfigFile(&cfg, path); err != nil {
111 log.Warning(err)
112 return nil
113 }
114
115 for _, v := range cfg {
116 if v.Hostname == "" || v.GUID == "" {
117 log.Warningf("skipping virtual node '%+v': required fields are missing (%s)", v, path)
118 continue
119 }
120 if err := uuid.Validate(v.GUID); err != nil {
121 log.Warningf("skipping virtual node '%+v': invalid GUID: %v (%s)", v, err, path)
122 continue
123 }
124 if _, ok := vnodes[v.Hostname]; ok {
125 log.Warningf("skipping virtual node '%+v': duplicate node (%s)", v, path)
126 continue
127 }
128
129 v := v
130
131 if v.Name != "" && v.Name != v.Hostname {
132 log.Warningf(
133 "ignoring virtual node name '%s' for hostname '%s'; file-based vnode identity uses hostname",
134 v.Name, v.Hostname,
135 )
136 }
137 v.Name = v.Hostname
138 v.Source = fmt.Sprintf("file=%s", path)
139 if isStockConfig(path) {
140 v.SourceType = "stock"
141 } else {
142 v.SourceType = "user"
143 }
144
145 log.Debugf("adding virtual node'%+v' (%s)", v, path)
146 vnodes[v.Hostname] = &v
147 }
148
149 return nil
150 })
151
152 return vnodes
153 }
154
155 func isConfigFile(path string) bool {
156 switch filepath.Ext(path) {
157 case ".yaml", ".yml", ".conf":
158 return true
159 default:
160 return false
161 }
162 }
163
164 func loadConfigFile(conf any, path string) error {
165 f, err := os.Open(path)
166 if err != nil {
167 return err
168 }
169 defer func() { _ = f.Close() }()
170
171 if err := yaml.NewDecoder(f).Decode(conf); err != nil && err != io.EOF {
172 return err
173 }
174
175 return nil
176 }
177
178 func isStockConfig(path string) bool {
179 return pluginconfig.IsStock(path)
180 }