| 1 | // SPDX-License-Identifier: GPL-3.0-or-later |
| 2 | |
| 3 | package match |
| 4 | |
| 5 | import ( |
| 6 | "fmt" |
| 7 | "strings" |
| 8 | |
| 9 | "github.com/netdata/netdata/go/plugins/pkg/matcher" |
| 10 | rs "github.com/netdata/netdata/go/plugins/plugin/go.d/collector/vsphere/resources" |
| 11 | ) |
| 12 | |
| 13 | type HostMatcher interface { |
| 14 | Match(*rs.Host) bool |
| 15 | } |
| 16 | |
| 17 | type VMMatcher interface { |
| 18 | Match(*rs.VM) bool |
| 19 | } |
| 20 | |
| 21 | type DatastoreMatcher interface { |
| 22 | Match(*rs.Datastore) bool |
| 23 | } |
| 24 | |
| 25 | type ClusterMatcher interface { |
| 26 | Match(*rs.Cluster) bool |
| 27 | } |
| 28 | |
| 29 | type DatastoreClusterMatcher interface { |
| 30 | Match(*rs.StoragePod) bool |
| 31 | } |
| 32 | |
| 33 | type VSANClusterMatcher interface { |
| 34 | Match(*rs.Cluster) bool |
| 35 | } |
| 36 | |
| 37 | type VSANHostMatcher interface { |
| 38 | Match(*rs.Host) bool |
| 39 | } |
| 40 | |
| 41 | type VSANVMMatcher interface { |
| 42 | Match(*rs.VM) bool |
| 43 | } |
| 44 | |
| 45 | type ( |
| 46 | resourceMatcher[T any] interface { |
| 47 | Match(*T) bool |
| 48 | } |
| 49 | fieldMatcher[T any] struct { |
| 50 | m matcher.Matcher |
| 51 | get func(*T) string |
| 52 | } |
| 53 | orMatcher[T any] struct{ lhs, rhs resourceMatcher[T] } |
| 54 | andMatcher[T any] struct{ lhs, rhs resourceMatcher[T] } |
| 55 | ) |
| 56 | |
| 57 | func (m fieldMatcher[T]) Match(v *T) bool { |
| 58 | return m.m.MatchString(m.get(v)) |
| 59 | } |
| 60 | |
| 61 | func (m orMatcher[T]) Match(v *T) bool { |
| 62 | return m.lhs.Match(v) || m.rhs.Match(v) |
| 63 | } |
| 64 | |
| 65 | func (m andMatcher[T]) Match(v *T) bool { |
| 66 | return m.lhs.Match(v) && m.rhs.Match(v) |
| 67 | } |
| 68 | |
| 69 | func chainAnd[T any](ms []resourceMatcher[T]) resourceMatcher[T] { |
| 70 | switch len(ms) { |
| 71 | case 0: |
| 72 | return nil |
| 73 | case 1: |
| 74 | return ms[0] |
| 75 | } |
| 76 | m := andMatcher[T]{lhs: ms[0], rhs: ms[1]} |
| 77 | for _, next := range ms[2:] { |
| 78 | m = andMatcher[T]{lhs: m, rhs: next} |
| 79 | } |
| 80 | return m |
| 81 | } |
| 82 | |
| 83 | func chainOr[T any](ms []resourceMatcher[T]) resourceMatcher[T] { |
| 84 | switch len(ms) { |
| 85 | case 0: |
| 86 | return nil |
| 87 | case 1: |
| 88 | return ms[0] |
| 89 | } |
| 90 | m := orMatcher[T]{lhs: ms[0], rhs: ms[1]} |
| 91 | for _, next := range ms[2:] { |
| 92 | m = orMatcher[T]{lhs: m, rhs: next} |
| 93 | } |
| 94 | return m |
| 95 | } |
| 96 | |
| 97 | type ( |
| 98 | VMIncludes []string |
| 99 | HostIncludes []string |
| 100 | DatastoreIncludes []string |
| 101 | ClusterIncludes []string |
| 102 | |
| 103 | DatastoreClusterIncludes []string |
| 104 | VSANClusterIncludes []string |
| 105 | VSANHostIncludes []string |
| 106 | VSANVMIncludes []string |
| 107 | ) |
| 108 | |
| 109 | type ( |
| 110 | datastoreClusterMatcher struct{ m matcher.Matcher } |
| 111 | vsanClusterMatcher struct{ m matcher.Matcher } |
| 112 | vsanHostMatcher struct{ m matcher.Matcher } |
| 113 | vsanVMMatcher struct{ m matcher.Matcher } |
| 114 | ) |
| 115 | |
| 116 | // NewPatternListMatcher parses ordered glob patterns with optional !-prefixed exclusions. |
| 117 | func NewPatternListMatcher(name string, patterns []string) (matcher.Matcher, error) { |
| 118 | m, err := matcher.NewSimplePatternListMatcher(patterns) |
| 119 | if err != nil { |
| 120 | return nil, patternListError(name, err) |
| 121 | } |
| 122 | return m, nil |
| 123 | } |
| 124 | |
| 125 | func patternListError(name string, err error) error { |
| 126 | if err.Error() == "invalid empty negative pattern" || strings.HasPrefix(err.Error(), "invalid pattern") { |
| 127 | return fmt.Errorf("%s has %w", name, err) |
| 128 | } |
| 129 | return fmt.Errorf("%s %w", name, err) |
| 130 | } |
| 131 | |
| 132 | func (m datastoreClusterMatcher) Match(pod *rs.StoragePod) bool { |
| 133 | return m.m.MatchString(datastoreClusterPath(pod)) || |
| 134 | m.m.MatchString(pod.Name) || |
| 135 | m.m.MatchString(pod.ID) |
| 136 | } |
| 137 | |
| 138 | func (m vsanClusterMatcher) Match(cluster *rs.Cluster) bool { |
| 139 | return m.m.MatchString(vsanClusterPath(cluster)) || |
| 140 | m.m.MatchString(cluster.Name) || |
| 141 | m.m.MatchString(cluster.ID) || |
| 142 | m.m.MatchString("vsan_uuid:"+cluster.VSANUUID) |
| 143 | } |
| 144 | |
| 145 | func (m vsanHostMatcher) Match(host *rs.Host) bool { |
| 146 | return m.m.MatchString(vsanHostPath(host)) || |
| 147 | m.m.MatchString(host.Name) || |
| 148 | m.m.MatchString(host.ID) || |
| 149 | m.m.MatchString("vsan_node_uuid:"+host.VSANNodeUUID) |
| 150 | } |
| 151 | |
| 152 | func (m vsanVMMatcher) Match(vm *rs.VM) bool { |
| 153 | return m.m.MatchString(vsanVMPath(vm)) || |
| 154 | m.m.MatchString(vm.Name) || |
| 155 | m.m.MatchString(vm.ID) || |
| 156 | m.m.MatchString("instance_uuid:"+vm.InstanceUUID) |
| 157 | } |
| 158 | |
| 159 | func datastoreClusterPath(pod *rs.StoragePod) string { |
| 160 | if pod.Hier.DC.Name == "" { |
| 161 | return "/" + pod.Name |
| 162 | } |
| 163 | return "/" + pod.Hier.DC.Name + "/" + pod.Name |
| 164 | } |
| 165 | |
| 166 | func vsanClusterPath(cluster *rs.Cluster) string { |
| 167 | if cluster.Hier.DC.Name == "" { |
| 168 | return "/" + cluster.Name |
| 169 | } |
| 170 | return "/" + cluster.Hier.DC.Name + "/" + cluster.Name |
| 171 | } |
| 172 | |
| 173 | func vsanHostPath(host *rs.Host) string { |
| 174 | return "/" + host.Hier.DC.Name + "/" + host.Hier.Cluster.Name + "/" + host.Name |
| 175 | } |
| 176 | |
| 177 | func vsanVMPath(vm *rs.VM) string { |
| 178 | return "/" + vm.Hier.DC.Name + "/" + vm.Hier.Cluster.Name + "/" + vm.Hier.Host.Name + "/" + vm.Name |
| 179 | } |
| 180 | |
| 181 | func (dci DatastoreClusterIncludes) Parse() (DatastoreClusterMatcher, error) { |
| 182 | m, err := NewPatternListMatcher("datastore_cluster_include", []string(dci)) |
| 183 | if err != nil { |
| 184 | return nil, err |
| 185 | } |
| 186 | return datastoreClusterMatcher{m}, nil |
| 187 | } |
| 188 | |
| 189 | func (vci VSANClusterIncludes) Parse() (VSANClusterMatcher, error) { |
| 190 | m, err := NewPatternListMatcher("vsan_cluster_include", []string(vci)) |
| 191 | if err != nil { |
| 192 | return nil, err |
| 193 | } |
| 194 | return vsanClusterMatcher{m}, nil |
| 195 | } |
| 196 | |
| 197 | func (vhi VSANHostIncludes) Parse() (VSANHostMatcher, error) { |
| 198 | m, err := NewPatternListMatcher("vsan_host_include", []string(vhi)) |
| 199 | if err != nil { |
| 200 | return nil, err |
| 201 | } |
| 202 | return vsanHostMatcher{m}, nil |
| 203 | } |
| 204 | |
| 205 | func (vvi VSANVMIncludes) Parse() (VSANVMMatcher, error) { |
| 206 | m, err := NewPatternListMatcher("vsan_vm_include", []string(vvi)) |
| 207 | if err != nil { |
| 208 | return nil, err |
| 209 | } |
| 210 | return vsanVMMatcher{m}, nil |
| 211 | } |
| 212 | |
| 213 | func (vi VMIncludes) Parse() (VMMatcher, error) { |
| 214 | return parseIncludes[rs.VM]("VM include", []string(vi), parseVMInclude) |
| 215 | } |
| 216 | |
| 217 | func (hi HostIncludes) Parse() (HostMatcher, error) { |
| 218 | return parseIncludes[rs.Host]("host include", []string(hi), parseHostInclude) |
| 219 | } |
| 220 | |
| 221 | func (di DatastoreIncludes) Parse() (DatastoreMatcher, error) { |
| 222 | return parseIncludes[rs.Datastore]("datastore include", []string(di), parseDatastoreInclude) |
| 223 | } |
| 224 | |
| 225 | func cleanInclude(include string) string { |
| 226 | return strings.Trim(include, "/") |
| 227 | } |
| 228 | |
| 229 | func parseIncludes[T any](name string, includes []string, parse func(string) (resourceMatcher[T], error)) (resourceMatcher[T], error) { |
| 230 | var ms []resourceMatcher[T] |
| 231 | for _, v := range includes { |
| 232 | m, err := parse(v) |
| 233 | if err != nil { |
| 234 | return nil, fmt.Errorf("parse %s %q: %w", name, v, err) |
| 235 | } |
| 236 | if m == nil { |
| 237 | continue |
| 238 | } |
| 239 | ms = append(ms, m) |
| 240 | } |
| 241 | return chainOr(ms), nil |
| 242 | } |
| 243 | |
| 244 | func parsePathInclude[T any](include, name, expected string, segments []func(*T) string) (resourceMatcher[T], error) { |
| 245 | if !isIncludeFormatValid(include) { |
| 246 | return nil, fmt.Errorf("bad %s include format %q: expected %s", name, include, expected) |
| 247 | } |
| 248 | |
| 249 | include = cleanInclude(include) |
| 250 | parts := strings.Split(include, "/") |
| 251 | ms := make([]resourceMatcher[T], 0, min(len(parts), len(segments))) |
| 252 | |
| 253 | for i, v := range parts { |
| 254 | m, err := parseSubInclude(v) |
| 255 | if err != nil { |
| 256 | return nil, fmt.Errorf("parse %s include segment index=%d value=%q: %w", name, i, v, err) |
| 257 | } |
| 258 | if i >= len(segments) { |
| 259 | continue |
| 260 | } |
| 261 | ms = append(ms, fieldMatcher[T]{m: m, get: segments[i]}) |
| 262 | } |
| 263 | |
| 264 | return chainAnd(ms), nil |
| 265 | } |
| 266 | |
| 267 | var hostPathSegments = []func(*rs.Host) string{ |
| 268 | func(host *rs.Host) string { return host.Hier.DC.Name }, |
| 269 | func(host *rs.Host) string { return host.Hier.Cluster.Name }, |
| 270 | func(host *rs.Host) string { return host.Name }, |
| 271 | } |
| 272 | |
| 273 | var vmPathSegments = []func(*rs.VM) string{ |
| 274 | func(vm *rs.VM) string { return vm.Hier.DC.Name }, |
| 275 | func(vm *rs.VM) string { return vm.Hier.Cluster.Name }, |
| 276 | func(vm *rs.VM) string { return vm.Hier.Host.Name }, |
| 277 | func(vm *rs.VM) string { return vm.Name }, |
| 278 | } |
| 279 | |
| 280 | var clusterPathSegments = []func(*rs.Cluster) string{ |
| 281 | func(cluster *rs.Cluster) string { return cluster.Hier.DC.Name }, |
| 282 | func(cluster *rs.Cluster) string { return cluster.Name }, |
| 283 | } |
| 284 | |
| 285 | var datastorePathSegments = []func(*rs.Datastore) string{ |
| 286 | func(ds *rs.Datastore) string { return ds.Hier.DC.Name }, |
| 287 | func(ds *rs.Datastore) string { return ds.Name }, |
| 288 | } |
| 289 | |
| 290 | func parseHostInclude(include string) (resourceMatcher[rs.Host], error) { |
| 291 | return parsePathInclude(include, "host", "/<datacenter>/<cluster>/<host>", hostPathSegments) |
| 292 | } |
| 293 | |
| 294 | func parseVMInclude(include string) (resourceMatcher[rs.VM], error) { |
| 295 | return parsePathInclude(include, "VM", "/<datacenter>/<cluster>/<host>/<vm>", vmPathSegments) |
| 296 | } |
| 297 | |
| 298 | func parseClusterInclude(include string) (resourceMatcher[rs.Cluster], error) { |
| 299 | return parsePathInclude(include, "cluster", "/<datacenter>/<cluster>", clusterPathSegments) |
| 300 | } |
| 301 | |
| 302 | func parseDatastoreInclude(include string) (resourceMatcher[rs.Datastore], error) { |
| 303 | return parsePathInclude(include, "datastore", "/<datacenter>/<datastore>", datastorePathSegments) |
| 304 | } |
| 305 | |
| 306 | func parseSubInclude(sub string) (matcher.Matcher, error) { |
| 307 | sub = strings.TrimSpace(sub) |
| 308 | if sub == "" || sub == "!*" { |
| 309 | return matcher.FALSE(), nil |
| 310 | } |
| 311 | if sub == "*" { |
| 312 | return matcher.TRUE(), nil |
| 313 | } |
| 314 | return matcher.NewSimplePatternsMatcher(sub) |
| 315 | } |
| 316 | |
| 317 | func isIncludeFormatValid(line string) bool { |
| 318 | return strings.HasPrefix(line, "/") |
| 319 | } |
| 320 | |
| 321 | func (ci ClusterIncludes) Parse() (ClusterMatcher, error) { |
| 322 | return parseIncludes[rs.Cluster]("cluster include", []string(ci), parseClusterInclude) |
| 323 | } |