diff options
author | Mike Crute <mike@crute.us> | 2022-12-23 23:05:47 -0800 |
---|---|---|
committer | Mike Crute <mike@crute.us> | 2022-12-23 23:05:47 -0800 |
commit | 85c67dd133605d2b1125a1b0e8c28f991223aa75 (patch) | |
tree | 89e04d1ee2603431d265335942d46c5108b06260 | |
parent | 15ef537afcded7561f59d1890df14af7f8277752 (diff) | |
download | golib-85c67dd133605d2b1125a1b0e8c28f991223aa75.tar.bz2 golib-85c67dd133605d2b1125a1b0e8c28f991223aa75.tar.xz golib-85c67dd133605d2b1125a1b0e8c28f991223aa75.zip |
clients/netbox: add config file clientclients/netbox/v3.2.0
-rw-r--r-- | clients/netbox/config_file_client.go | 151 | ||||
-rw-r--r-- | clients/netbox/config_file_client_test.go | 165 | ||||
-rw-r--r-- | clients/netbox/go.mod | 12 | ||||
-rw-r--r-- | clients/netbox/go.sum | 21 |
4 files changed, 349 insertions, 0 deletions
diff --git a/clients/netbox/config_file_client.go b/clients/netbox/config_file_client.go new file mode 100644 index 0000000..6023916 --- /dev/null +++ b/clients/netbox/config_file_client.go | |||
@@ -0,0 +1,151 @@ | |||
1 | package netbox | ||
2 | |||
3 | import ( | ||
4 | "context" | ||
5 | "encoding/json" | ||
6 | "fmt" | ||
7 | "io/fs" | ||
8 | "net" | ||
9 | "path/filepath" | ||
10 | |||
11 | "github.com/mitchellh/mapstructure" | ||
12 | "gopkg.in/yaml.v2" | ||
13 | ) | ||
14 | |||
15 | type ConfigFileNetboxClient struct { | ||
16 | cfg map[string]any | ||
17 | } | ||
18 | |||
19 | var _ NetboxClient = (*ConfigFileNetboxClient)(nil) | ||
20 | |||
21 | // NewConfigFileClient creates a new ConfigFileNetboxClient by loading a | ||
22 | // named config file from a filesystem and unmarshalling it. The config | ||
23 | // file can be in JSON or YAML format, determined by a .json, .yaml, or | ||
24 | // .yml extension. The configuration must be nested within a key in that | ||
25 | // file to support sharing the file with other subsystems. | ||
26 | // | ||
27 | // See method docs for the extected format of this file. | ||
28 | func NewConfigFileClient(filesystem fs.FS, name, key string) (NetboxClient, error) { | ||
29 | fp, err := filesystem.Open(name) | ||
30 | if err != nil { | ||
31 | return nil, err | ||
32 | } | ||
33 | defer fp.Close() | ||
34 | |||
35 | fc := map[string]any{} | ||
36 | |||
37 | switch e := filepath.Ext(name); e { | ||
38 | case ".yaml", ".yml": | ||
39 | if err := yaml.NewDecoder(fp).Decode(&fc); err != nil { | ||
40 | return nil, err | ||
41 | } | ||
42 | case ".json": | ||
43 | if err := json.NewDecoder(fp).Decode(&fc); err != nil { | ||
44 | return nil, err | ||
45 | } | ||
46 | default: | ||
47 | return nil, fmt.Errorf("Config files with extension %s are not supported", e) | ||
48 | } | ||
49 | |||
50 | ck, ok := fc[key] | ||
51 | if !ok { | ||
52 | return nil, fmt.Errorf("Key %s does not exist in config file", key) | ||
53 | } | ||
54 | |||
55 | cfg := map[string]any{} | ||
56 | if err := mapstructure.Decode(ck, &cfg); err != nil { | ||
57 | return nil, fmt.Errorf("Config file key was not of correct type: %w", err) | ||
58 | } | ||
59 | |||
60 | return &ConfigFileNetboxClient{ | ||
61 | cfg: cfg, | ||
62 | }, nil | ||
63 | } | ||
64 | |||
65 | // GetSitePrefixesWithTag gets a list of IP prefixes for a site based on | ||
66 | // some tag. | ||
67 | // | ||
68 | // The data format of the config file should be, under the key "sites": | ||
69 | // | ||
70 | // map[string]map[string][]string{ | ||
71 | // "site_name": map[string][]string{ | ||
72 | // "tag_name": []string{ | ||
73 | // "127.0.0.1/24", | ||
74 | // }, | ||
75 | // }, | ||
76 | // } | ||
77 | // | ||
78 | // In JSON format: | ||
79 | // | ||
80 | // { "sites": { | ||
81 | // "site": { | ||
82 | // "tag_name": [ "127.0.0.1/24" ] | ||
83 | // } | ||
84 | // } | ||
85 | func (c *ConfigFileNetboxClient) GetSitePrefixesWithTag(ctx context.Context, site string, tag string) ([]*net.IPNet, error) { | ||
86 | s, ok := c.cfg["sites"] | ||
87 | if !ok { | ||
88 | return nil, fmt.Errorf("No key 'sites' in config file") | ||
89 | } | ||
90 | |||
91 | sites := map[string]map[string][]string{} | ||
92 | if err := mapstructure.Decode(s, &sites); err != nil { | ||
93 | return nil, fmt.Errorf("Key 'sites' not in correct format: %w", err) | ||
94 | } | ||
95 | |||
96 | tags, ok := sites[site] | ||
97 | if !ok { | ||
98 | return nil, fmt.Errorf("Site %s not in sites", site) | ||
99 | } | ||
100 | |||
101 | values, ok := tags[tag] | ||
102 | if !ok { | ||
103 | return nil, fmt.Errorf("Tag %s not in tags", tag) | ||
104 | } | ||
105 | |||
106 | return parseValues(values) | ||
107 | } | ||
108 | |||
109 | // GetPrefixesWithTag gets a list fo IP prefixes based on some tag. | ||
110 | // | ||
111 | // The data format of the config file should be, under the key "tags": | ||
112 | // | ||
113 | // map[string][]string{ | ||
114 | // "tag_name": []string{ | ||
115 | // "127.0.0.1/24", | ||
116 | // }, | ||
117 | // } | ||
118 | // | ||
119 | // In JSON format: | ||
120 | // | ||
121 | // { "tags": { "tag_name": [ "127.0.0.1/24" ] } } | ||
122 | func (c *ConfigFileNetboxClient) GetPrefixesWithTag(ctx context.Context, tag string) ([]*net.IPNet, error) { | ||
123 | t, ok := c.cfg["tags"] | ||
124 | if !ok { | ||
125 | return nil, fmt.Errorf("No key 'tags' in config file") | ||
126 | } | ||
127 | |||
128 | tags := map[string][]string{} | ||
129 | if err := mapstructure.Decode(t, &tags); err != nil { | ||
130 | return nil, fmt.Errorf("Key 'tags' not in correct format: %w", err) | ||
131 | } | ||
132 | |||
133 | values, ok := tags[tag] | ||
134 | if !ok { | ||
135 | return nil, fmt.Errorf("Tag %s not in tags", tag) | ||
136 | } | ||
137 | |||
138 | return parseValues(values) | ||
139 | } | ||
140 | |||
141 | func parseValues(v []string) ([]*net.IPNet, error) { | ||
142 | out := make([]*net.IPNet, len(v)) | ||
143 | for i, n := range v { | ||
144 | _, in, err := net.ParseCIDR(n) | ||
145 | if err != nil { | ||
146 | return nil, err | ||
147 | } | ||
148 | out[i] = in | ||
149 | } | ||
150 | return out, nil | ||
151 | } | ||
diff --git a/clients/netbox/config_file_client_test.go b/clients/netbox/config_file_client_test.go new file mode 100644 index 0000000..bce5165 --- /dev/null +++ b/clients/netbox/config_file_client_test.go | |||
@@ -0,0 +1,165 @@ | |||
1 | package netbox | ||
2 | |||
3 | import ( | ||
4 | "context" | ||
5 | "net" | ||
6 | "testing" | ||
7 | "testing/fstest" | ||
8 | |||
9 | "github.com/stretchr/testify/assert" | ||
10 | "github.com/stretchr/testify/suite" | ||
11 | ) | ||
12 | |||
13 | var ( | ||
14 | ipNet1 *net.IPNet | ||
15 | ipNet2 *net.IPNet | ||
16 | ) | ||
17 | |||
18 | func init() { | ||
19 | _, ipNet1, _ = net.ParseCIDR("127.0.0.1/8") | ||
20 | _, ipNet2, _ = net.ParseCIDR("10.0.10.0/24") | ||
21 | } | ||
22 | |||
23 | var testFs fstest.MapFS | ||
24 | |||
25 | var jsonMissingTopSiteTags = []byte(`{ | ||
26 | "netbox": { } | ||
27 | }`) | ||
28 | |||
29 | var jsonInvalidFormat = []byte(`{ | ||
30 | "netbox": { | ||
31 | "sites": { | ||
32 | "site1": { | ||
33 | "invalid-tag": { "foo": "bar" } | ||
34 | } | ||
35 | }, | ||
36 | "tags": { | ||
37 | "invalid-tag": { "foo": "bar" } | ||
38 | } | ||
39 | } | ||
40 | }`) | ||
41 | |||
42 | var jsonTestConfig = []byte(`{ | ||
43 | "netbox": { | ||
44 | "sites": { | ||
45 | "site1": { | ||
46 | "tag1": [ "127.0.0.1/8", "10.0.10.0/24" ] | ||
47 | } | ||
48 | }, | ||
49 | "tags": { | ||
50 | "tag1": [ "127.0.0.1/8", "10.0.10.0/24" ] | ||
51 | } | ||
52 | } | ||
53 | }`) | ||
54 | |||
55 | var yamlTestConfig = []byte(` | ||
56 | netbox: | ||
57 | sites: | ||
58 | site1: | ||
59 | tag1: [ "127.0.0.1/8", "10.0.10.0/24" ] | ||
60 | tags: | ||
61 | tag1: [ "127.0.0.1/8", "10.0.10.0/24" ] | ||
62 | `) | ||
63 | |||
64 | func init() { | ||
65 | testFs = fstest.MapFS{} | ||
66 | testFs["netbox.foo"] = &fstest.MapFile{Data: jsonTestConfig} | ||
67 | testFs["netbox.json"] = &fstest.MapFile{Data: jsonTestConfig} | ||
68 | testFs["netbox.yaml"] = &fstest.MapFile{Data: yamlTestConfig} | ||
69 | testFs["netbox.yml"] = &fstest.MapFile{Data: yamlTestConfig} | ||
70 | testFs["no-site-tags.json"] = &fstest.MapFile{Data: jsonMissingTopSiteTags} | ||
71 | testFs["invalid-format.json"] = &fstest.MapFile{Data: jsonInvalidFormat} | ||
72 | } | ||
73 | |||
74 | type ConfigFileNetboxClientSuite struct { | ||
75 | suite.Suite | ||
76 | c NetboxClient | ||
77 | nst NetboxClient | ||
78 | inv NetboxClient | ||
79 | } | ||
80 | |||
81 | func (s *ConfigFileNetboxClientSuite) SetupTest() { | ||
82 | var err error | ||
83 | s.c, err = NewConfigFileClient(testFs, "netbox.yaml", "netbox") | ||
84 | assert.NoError(s.T(), err) | ||
85 | |||
86 | s.nst, err = NewConfigFileClient(testFs, "no-site-tags.json", "netbox") | ||
87 | assert.NoError(s.T(), err) | ||
88 | |||
89 | s.inv, err = NewConfigFileClient(testFs, "invalid-format.json", "netbox") | ||
90 | assert.NoError(s.T(), err) | ||
91 | } | ||
92 | |||
93 | func (s *ConfigFileNetboxClientSuite) TestFromJSON() { | ||
94 | _, err := NewConfigFileClient(testFs, "netbox.json", "netbox") | ||
95 | assert.NoError(s.T(), err) | ||
96 | } | ||
97 | |||
98 | func (s *ConfigFileNetboxClientSuite) TestFromYAML() { | ||
99 | _, err := NewConfigFileClient(testFs, "netbox.yaml", "netbox") | ||
100 | assert.NoError(s.T(), err) | ||
101 | _, err = NewConfigFileClient(testFs, "netbox.yml", "netbox") | ||
102 | assert.NoError(s.T(), err) | ||
103 | } | ||
104 | |||
105 | func (s *ConfigFileNetboxClientSuite) TestFromInvalidExtension() { | ||
106 | _, err := NewConfigFileClient(testFs, "netbox.foo", "netbox") | ||
107 | assert.ErrorContains(s.T(), err, "extension .foo are not supported") | ||
108 | } | ||
109 | |||
110 | func (s *ConfigFileNetboxClientSuite) TestFromInvalidFile() { | ||
111 | _, err := NewConfigFileClient(testFs, "netbox.foo-", "netbox") | ||
112 | assert.ErrorContains(s.T(), err, "file does not exist") | ||
113 | } | ||
114 | |||
115 | func (s *ConfigFileNetboxClientSuite) TestFromInvalidKey() { | ||
116 | _, err := NewConfigFileClient(testFs, "netbox.json", "netboks") | ||
117 | assert.ErrorContains(s.T(), err, "Key netboks does not exist") | ||
118 | } | ||
119 | |||
120 | func (s *ConfigFileNetboxClientSuite) TestGetSitePrefixesWithTag() { | ||
121 | ips, err := s.c.GetSitePrefixesWithTag(context.TODO(), "site1", "tag1") | ||
122 | assert.NoError(s.T(), err) | ||
123 | assert.Equal(s.T(), ipNet1, ips[0]) | ||
124 | assert.Equal(s.T(), ipNet2, ips[1]) | ||
125 | } | ||
126 | |||
127 | func (s *ConfigFileNetboxClientSuite) TestGetSitePrefixesWithTagMissing() { | ||
128 | _, err := s.nst.GetSitePrefixesWithTag(context.TODO(), "site1", "tag1") | ||
129 | assert.ErrorContains(s.T(), err, "No key 'sites'") | ||
130 | |||
131 | _, err = s.c.GetSitePrefixesWithTag(context.TODO(), "site2", "tag1") | ||
132 | assert.ErrorContains(s.T(), err, "Site site2 not in sites") | ||
133 | |||
134 | _, err = s.c.GetSitePrefixesWithTag(context.TODO(), "site1", "tag2") | ||
135 | assert.ErrorContains(s.T(), err, "Tag tag2 not in tags") | ||
136 | } | ||
137 | |||
138 | func (s *ConfigFileNetboxClientSuite) TestGetSitePrefixesWithTagInvalid() { | ||
139 | _, err := s.inv.GetSitePrefixesWithTag(context.TODO(), "site1", "invalid-tag") | ||
140 | assert.ErrorContains(s.T(), err, "Key 'sites' not in correct format") | ||
141 | } | ||
142 | |||
143 | func (s *ConfigFileNetboxClientSuite) TestGetPrefixesWithTag() { | ||
144 | ips, err := s.c.GetPrefixesWithTag(context.TODO(), "tag1") | ||
145 | assert.NoError(s.T(), err) | ||
146 | assert.Equal(s.T(), ipNet1, ips[0]) | ||
147 | assert.Equal(s.T(), ipNet2, ips[1]) | ||
148 | } | ||
149 | |||
150 | func (s *ConfigFileNetboxClientSuite) TestGetPrefixesWithTagMissingTags() { | ||
151 | _, err := s.nst.GetPrefixesWithTag(context.TODO(), "tag1") | ||
152 | assert.ErrorContains(s.T(), err, "No key 'tags'") | ||
153 | |||
154 | _, err = s.c.GetPrefixesWithTag(context.TODO(), "tag2") | ||
155 | assert.ErrorContains(s.T(), err, "Tag tag2 not in tags") | ||
156 | } | ||
157 | |||
158 | func (s *ConfigFileNetboxClientSuite) TestGetPrefixesWithTagMissingInvalid() { | ||
159 | _, err := s.inv.GetPrefixesWithTag(context.TODO(), "invalid-tag") | ||
160 | assert.ErrorContains(s.T(), err, "Key 'tags' not in correct format") | ||
161 | } | ||
162 | |||
163 | func TestConfigFileNetboxClientSuite(t *testing.T) { | ||
164 | suite.Run(t, &ConfigFileNetboxClientSuite{}) | ||
165 | } | ||
diff --git a/clients/netbox/go.mod b/clients/netbox/go.mod index b1a9bf4..1a3ea97 100644 --- a/clients/netbox/go.mod +++ b/clients/netbox/go.mod | |||
@@ -1,3 +1,15 @@ | |||
1 | module code.crute.us/mcrute/golib/clients/netbox/v3 | 1 | module code.crute.us/mcrute/golib/clients/netbox/v3 |
2 | 2 | ||
3 | go 1.18 | 3 | go 1.18 |
4 | |||
5 | require ( | ||
6 | github.com/mitchellh/mapstructure v1.5.0 | ||
7 | github.com/stretchr/testify v1.8.1 | ||
8 | gopkg.in/yaml.v2 v2.4.0 | ||
9 | ) | ||
10 | |||
11 | require ( | ||
12 | github.com/davecgh/go-spew v1.1.1 // indirect | ||
13 | github.com/pmezard/go-difflib v1.0.0 // indirect | ||
14 | gopkg.in/yaml.v3 v3.0.1 // indirect | ||
15 | ) | ||
diff --git a/clients/netbox/go.sum b/clients/netbox/go.sum index e69de29..0468562 100644 --- a/clients/netbox/go.sum +++ b/clients/netbox/go.sum | |||
@@ -0,0 +1,21 @@ | |||
1 | github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= | ||
2 | github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= | ||
3 | github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= | ||
4 | github.com/mitchellh/mapstructure v1.5.0 h1:jeMsZIYE/09sWLaz43PL7Gy6RuMjD2eJVyuac5Z2hdY= | ||
5 | github.com/mitchellh/mapstructure v1.5.0/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo= | ||
6 | github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= | ||
7 | github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= | ||
8 | github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= | ||
9 | github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= | ||
10 | github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= | ||
11 | github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= | ||
12 | github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= | ||
13 | github.com/stretchr/testify v1.8.1 h1:w7B6lhMri9wdJUVmEZPGGhZzrYTPvgJArz7wNPgYKsk= | ||
14 | github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= | ||
15 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= | ||
16 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= | ||
17 | gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY= | ||
18 | gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ= | ||
19 | gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= | ||
20 | gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= | ||
21 | gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= | ||