netbox-client-ruby 0.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (72) hide show
  1. checksums.yaml +7 -0
  2. data/.gitignore +13 -0
  3. data/.rspec +2 -0
  4. data/.ruby-version +1 -0
  5. data/.travis.yml +8 -0
  6. data/Dockerfile +12 -0
  7. data/Gemfile +4 -0
  8. data/Gemfile.lock +75 -0
  9. data/LICENSE.txt +21 -0
  10. data/README.md +138 -0
  11. data/Rakefile +6 -0
  12. data/VERSION +1 -0
  13. data/bin/console +14 -0
  14. data/bin/setup +8 -0
  15. data/docker-compose.test.yml +5 -0
  16. data/docker-compose.yml +37 -0
  17. data/docker/start.sh +3 -0
  18. data/docker/start.test.sh +3 -0
  19. data/lib/netbox-client-ruby.rb +1 -0
  20. data/lib/netbox_client_ruby.rb +25 -0
  21. data/lib/netbox_client_ruby/api.rb +18 -0
  22. data/lib/netbox_client_ruby/api/dcim.rb +55 -0
  23. data/lib/netbox_client_ruby/api/dcim/device.rb +31 -0
  24. data/lib/netbox_client_ruby/api/dcim/device_role.rb +12 -0
  25. data/lib/netbox_client_ruby/api/dcim/device_roles.rb +19 -0
  26. data/lib/netbox_client_ruby/api/dcim/device_type.rb +26 -0
  27. data/lib/netbox_client_ruby/api/dcim/device_types.rb +19 -0
  28. data/lib/netbox_client_ruby/api/dcim/devices.rb +19 -0
  29. data/lib/netbox_client_ruby/api/dcim/interface.rb +14 -0
  30. data/lib/netbox_client_ruby/api/dcim/interfaces.rb +19 -0
  31. data/lib/netbox_client_ruby/api/dcim/manufacturer.rb +12 -0
  32. data/lib/netbox_client_ruby/api/dcim/manufacturers.rb +19 -0
  33. data/lib/netbox_client_ruby/api/dcim/platform.rb +12 -0
  34. data/lib/netbox_client_ruby/api/dcim/platforms.rb +19 -0
  35. data/lib/netbox_client_ruby/api/dcim/rack.rb +12 -0
  36. data/lib/netbox_client_ruby/api/dcim/racks.rb +19 -0
  37. data/lib/netbox_client_ruby/api/dcim/region.rb +13 -0
  38. data/lib/netbox_client_ruby/api/dcim/regions.rb +19 -0
  39. data/lib/netbox_client_ruby/api/dcim/site.rb +15 -0
  40. data/lib/netbox_client_ruby/api/dcim/sites.rb +19 -0
  41. data/lib/netbox_client_ruby/api/ipam.rb +53 -0
  42. data/lib/netbox_client_ruby/api/ipam/aggregate.rb +14 -0
  43. data/lib/netbox_client_ruby/api/ipam/aggregates.rb +19 -0
  44. data/lib/netbox_client_ruby/api/ipam/ip_address.rb +42 -0
  45. data/lib/netbox_client_ruby/api/ipam/ip_addresses.rb +19 -0
  46. data/lib/netbox_client_ruby/api/ipam/prefix.rb +47 -0
  47. data/lib/netbox_client_ruby/api/ipam/prefixes.rb +19 -0
  48. data/lib/netbox_client_ruby/api/ipam/rir.rb +12 -0
  49. data/lib/netbox_client_ruby/api/ipam/rirs.rb +19 -0
  50. data/lib/netbox_client_ruby/api/ipam/role.rb +12 -0
  51. data/lib/netbox_client_ruby/api/ipam/roles.rb +19 -0
  52. data/lib/netbox_client_ruby/api/ipam/vlan.rb +40 -0
  53. data/lib/netbox_client_ruby/api/ipam/vlan_group.rb +14 -0
  54. data/lib/netbox_client_ruby/api/ipam/vlan_groups.rb +19 -0
  55. data/lib/netbox_client_ruby/api/ipam/vlans.rb +19 -0
  56. data/lib/netbox_client_ruby/api/ipam/vrf.rb +14 -0
  57. data/lib/netbox_client_ruby/api/ipam/vrfs.rb +19 -0
  58. data/lib/netbox_client_ruby/api/tenancy.rb +25 -0
  59. data/lib/netbox_client_ruby/api/tenancy/tenant.rb +14 -0
  60. data/lib/netbox_client_ruby/api/tenancy/tenant_group.rb +12 -0
  61. data/lib/netbox_client_ruby/api/tenancy/tenant_groups.rb +19 -0
  62. data/lib/netbox_client_ruby/api/tenancy/tenants.rb +19 -0
  63. data/lib/netbox_client_ruby/communication.rb +74 -0
  64. data/lib/netbox_client_ruby/connection.rb +33 -0
  65. data/lib/netbox_client_ruby/entities.rb +200 -0
  66. data/lib/netbox_client_ruby/entity.rb +345 -0
  67. data/lib/netbox_client_ruby/error/client_error.rb +4 -0
  68. data/lib/netbox_client_ruby/error/local_error.rb +4 -0
  69. data/lib/netbox_client_ruby/error/remote_error.rb +4 -0
  70. data/netbox-client-ruby.gemspec +43 -0
  71. data/netbox.env +17 -0
  72. metadata +255 -0
@@ -0,0 +1,33 @@
1
+ require 'faraday'
2
+ require 'faraday_middleware'
3
+ require 'faraday/detailed_logger'
4
+ require 'netbox_client_ruby/error/local_error'
5
+
6
+ module NetboxClientRuby
7
+ class Connection
8
+ def self.new
9
+ build_faraday
10
+ end
11
+
12
+ def self.headers
13
+ headers = {}
14
+ netbox_auth_config = NetboxClientRuby.config.netbox.auth
15
+
16
+ raise LocalError, 'The authorization_token has not been configured.' unless netbox_auth_config.token
17
+
18
+ headers['Authorization'] = "Token #{netbox_auth_config.token}"
19
+ headers
20
+ end
21
+
22
+ private_class_method def self.build_faraday
23
+ config = NetboxClientRuby.config
24
+ Faraday.new(url: config.netbox.api_base_url, headers: headers) do |faraday|
25
+ faraday.request :json
26
+ faraday.response config.faraday.logger if config.faraday.logger
27
+ faraday.response :json, content_type: /\bjson$/
28
+ faraday.adapter config.faraday.adapter || Faraday.default_adapter
29
+ faraday.options.merge NetboxClientRuby.config.faraday.request_options
30
+ end
31
+ end
32
+ end
33
+ end
@@ -0,0 +1,200 @@
1
+ require 'netbox_client_ruby/entity'
2
+ require 'netbox_client_ruby/error/local_error'
3
+ require 'pry'
4
+
5
+ module NetboxClientRuby
6
+ module Entities
7
+ include NetboxClientRuby::Communication
8
+
9
+ def self.included(other_klass)
10
+ other_klass.extend ClassMethods
11
+ end
12
+
13
+ module ClassMethods
14
+ def self.extended(other_klass)
15
+ @other_klass = other_klass
16
+ end
17
+
18
+ def data_key(key = 'data')
19
+ @data_key ||= key
20
+ end
21
+
22
+ def count_key(key = 'count')
23
+ @count_key ||= key
24
+ end
25
+
26
+ def limit(limit = nil)
27
+ return @limit if @limit
28
+
29
+ @limit = limit || NetboxClientRuby.config.netbox.pagination.default_limit
30
+
31
+ if @limit.nil?
32
+ raise ArgumentError,
33
+ "Whether 'limit' nor 'default_limit' are defined."
34
+ end
35
+
36
+ check_limit @limit
37
+
38
+ @limit
39
+ end
40
+
41
+ def check_limit(limit)
42
+ max_limit = NetboxClientRuby.config.netbox.pagination.max_limit
43
+ if limit.nil?
44
+ raise ArgumentError,
45
+ "'limit' can not be nil."
46
+ elsif !limit.is_a?(Numeric)
47
+ raise ArgumentError,
48
+ "The limit '#{limit}' is not numeric but it has to be."
49
+ elsif !limit.integer?
50
+ raise ArgumentError,
51
+ "The limit '#{limit}' is not integer but it has to be."
52
+ elsif limit.negative?
53
+ raise ArgumentError,
54
+ "The limit '#{limit}' is below zero, but it should be zero or bigger."
55
+ elsif limit > max_limit
56
+ raise ArgumentError,
57
+ "The limit '#{limit}' is bigger than the configured limit value ('#{max_limit}')."
58
+ end
59
+ end
60
+
61
+ def entity_creator(creator = nil)
62
+ return @entity_creator if @entity_creator
63
+
64
+ raise ArgumentError, '"entity_creator" has not been defined.' unless creator
65
+
66
+ @entity_creator = creator
67
+ end
68
+
69
+ def path(path = nil)
70
+ return @path if @path
71
+
72
+ raise ArgumentError, '"path" has not been defined.' unless path
73
+
74
+ @path = path
75
+ end
76
+ end
77
+
78
+ def filter(filter)
79
+ raise ArgumentError, '"filter" expects a hash' unless filter.is_a? Hash
80
+
81
+ @filter = filter
82
+ reset
83
+ self
84
+ end
85
+
86
+ def all
87
+ @instance_limit = NetboxClientRuby.config.netbox.pagination.max_limit
88
+ reset
89
+ self
90
+ end
91
+
92
+ def limit(limit)
93
+ self.class.check_limit limit unless limit.nil?
94
+
95
+ @instance_limit = limit
96
+ reset
97
+ self
98
+ end
99
+
100
+ def offset(offset)
101
+ raise ArgumentError, "The offset '#{offset}' is not numeric." unless offset.is_a? Numeric
102
+ raise ArgumentError, "The offset '#{offset}' must not be negative." if offset.negative?
103
+
104
+ @offset = offset
105
+ reset
106
+ self
107
+ end
108
+
109
+ def page(page)
110
+ raise ArgumentError, "The offset '#{page}' is not numeric but has to be." unless page.is_a? Numeric
111
+ raise ArgumentError, "The offset '#{page}' must be integer but isn't." unless page.integer?
112
+ raise ArgumentError, "The offset '#{page}' must not be negative but is." if page.negative?
113
+
114
+ limit = @instance_limit || self.class.limit
115
+ offset(limit * page)
116
+ end
117
+
118
+ def [](index)
119
+ return nil if length <= index
120
+
121
+ as_entity raw_data_array[index]
122
+ end
123
+
124
+ def as_array
125
+ raw_data_array.map { |raw_entity| as_entity raw_entity }
126
+ end
127
+
128
+ ##
129
+ # The number of entities that have been fetched
130
+ def length
131
+ raw_data_array.length
132
+ end
133
+
134
+ ##
135
+ # The total number of available entities for that query
136
+ def total
137
+ data[self.class.count_key]
138
+ end
139
+
140
+ def reload
141
+ @data = get
142
+ self
143
+ end
144
+
145
+ def raw_data!
146
+ data
147
+ end
148
+
149
+ alias get! reload
150
+ alias size length
151
+ alias count total
152
+
153
+ private
154
+
155
+ def reset
156
+ @data = nil
157
+ end
158
+
159
+ def raw_data_array
160
+ data[self.class.data_key] || []
161
+ end
162
+
163
+ def data
164
+ @data ||= get
165
+ end
166
+
167
+ def get
168
+ response connection.get path_with_parameters
169
+ end
170
+
171
+ def path_with_parameters
172
+ self.class.path + path_parameters
173
+ end
174
+
175
+ def path_parameters
176
+ params = []
177
+
178
+ params << @filter
179
+ params << { limit: @instance_limit || self.class.limit }
180
+ params << { offset: @offset } if @offset
181
+
182
+ join_path_parameters(params)
183
+ end
184
+
185
+ def join_path_parameters(params)
186
+ return '' if params.empty?
187
+
188
+ '?' + params.compact.map do |param_obj|
189
+ URI.encode_www_form param_obj
190
+ end.join('&')
191
+ end
192
+
193
+ def as_entity(raw_entity)
194
+ entity_creator_method = method self.class.entity_creator
195
+ entity = entity_creator_method.call raw_entity
196
+ entity.data = raw_entity
197
+ entity
198
+ end
199
+ end
200
+ end
@@ -0,0 +1,345 @@
1
+ require 'netbox_client_ruby/communication'
2
+ require 'netbox_client_ruby/error/local_error'
3
+
4
+ module NetboxClientRuby
5
+ module Entity
6
+ include NetboxClientRuby::Communication
7
+
8
+ def self.included(other_klass)
9
+ other_klass.extend ClassMethods
10
+ end
11
+
12
+ module ClassMethods
13
+ ##
14
+ # Expects ids in the following format:
15
+ # id 'an_id_field'
16
+ # id :an_id_field
17
+ # id 'an_id_field', 'another_id_field'
18
+ # id :an_id_field, :another_id_field
19
+ # id an_id_field: 'id_field_in_data'
20
+ # id an_id_field: :id_field_in_data
21
+ # id 'an_id_field' => :id_field_in_data
22
+ # id 'an_id_field' => 'id_field_in_data'
23
+ # id an_id_field: 'id_field_in_data', :another_id_field: 'id_field2_in_data'
24
+ #
25
+ def id(*fields)
26
+ return @id_fields if @id_fields
27
+
28
+ raise ArgumentError, "No 'id' was defined, but one is expected." if fields.empty?
29
+
30
+ @id_fields = {}
31
+ if fields.first.is_a?(Hash)
32
+ fields.first.each { |key, value| @id_fields[key.to_s] = value.to_s }
33
+ else
34
+ fields.map(&:to_s).each do |field|
35
+ field_as_string = field.to_s
36
+ @id_fields[field_as_string] = field_as_string
37
+ end
38
+ end
39
+
40
+ @id_fields.keys.each do |field|
41
+ define_method(field) { instance_variable_get "@#{field}" }
42
+ end
43
+
44
+ @id_fields
45
+ end
46
+
47
+ def readonly_fields(*fields)
48
+ return @readonly_fields if @readonly_fields
49
+
50
+ @readonly_fields = fields.map(&:to_s)
51
+ end
52
+
53
+ def deletable(deletable = false)
54
+ @deletable ||= deletable
55
+ end
56
+
57
+ def path(path = nil)
58
+ @path ||= path
59
+
60
+ return @path if @path
61
+
62
+ raise ArgumentError, "No argument to 'path' was given."
63
+ end
64
+
65
+ def creation_path(creation_path = nil)
66
+ @creation_path ||= creation_path
67
+
68
+ return @creation_path if @creation_path
69
+
70
+ raise ArgumentError, "No argument to 'creation_path' was given."
71
+ end
72
+
73
+ # allowed values:
74
+ # <code>
75
+ # object_fields :field_a, :field_b, :field_c, 'fieldname_as_string'
76
+ # # next is ame as previous
77
+ # object_fields field_a: nil, field_b: nil, field_c: nil, 'fieldname_as_string': nil
78
+ # # next defines the types of the objects
79
+ # object_fields field_a: Klass, field_b: proc { |data| Klass.new data }, field_c: nil, 'fieldname_as_string'
80
+ # </code>
81
+ def object_fields(*fields_to_class_map)
82
+ return @object_fields if @object_fields
83
+
84
+ if fields_to_class_map.nil? || fields_to_class_map.empty?
85
+ @object_fields = {}
86
+ else
87
+ fields_map = sanitize_mapping(fields_to_class_map)
88
+ @object_fields = fields_map
89
+ end
90
+ end
91
+
92
+ def sanitize_mapping(fields_to_class_map)
93
+ fields_map = {}
94
+ fields_to_class_map.each do |field_definition|
95
+ if field_definition.is_a?(Hash)
96
+ fields_to_class_map[0].each do |field, klass_or_proc|
97
+ fields_map[field.to_s] = klass_or_proc
98
+ end
99
+ else
100
+ fields_map[field_definition.to_s] = nil
101
+ end
102
+ end
103
+ fields_map
104
+ end
105
+ end
106
+
107
+ def initialize(given_ids = nil)
108
+ return self if given_ids.nil?
109
+
110
+ if id_fields.count == 1 && !given_ids.is_a?(Hash)
111
+ instance_variable_set("@#{id_fields.keys.first}", given_ids)
112
+ return self
113
+ end
114
+
115
+ check_given_ids(given_ids)
116
+
117
+ given_ids.each { |id_field, id_value| instance_variable_set "@#{id_field}", id_value }
118
+
119
+ self
120
+ end
121
+
122
+ def revert
123
+ dirty_data.clear
124
+ self
125
+ end
126
+
127
+ def reload
128
+ @data = get
129
+ revert
130
+ self
131
+ end
132
+
133
+ def save
134
+ return post unless ids_set?
135
+ patch
136
+ end
137
+
138
+ def create(raw_data)
139
+ raise LocalError, "Can't 'create', this object already exists" if ids_set?
140
+
141
+ @dirty_data = raw_data
142
+ post
143
+ end
144
+
145
+ def delete
146
+ raise NetboxClientRuby::LocalError, "Can't delete unless deletable=true" unless deletable
147
+ return self if @deleted
148
+
149
+ @data = response connection.delete path
150
+ @deleted = true
151
+ revert
152
+ self
153
+ end
154
+
155
+ def update(updated_fields)
156
+ checked_updated_fields = {}
157
+ updated_fields.each do |key, values|
158
+ s_key = key.to_s
159
+
160
+ checked_updated_fields[s_key] = values unless readonly_fields.include? s_key
161
+ end
162
+
163
+ dirty_data.merge! checked_updated_fields
164
+ patch
165
+ end
166
+
167
+ def raw_data!
168
+ data
169
+ end
170
+
171
+ def [](name)
172
+ s_name = name.to_s
173
+ dirty_data[s_name] || data[s_name]
174
+ end
175
+
176
+ def []=(name, value)
177
+ dirty_data[name.to_s] = value
178
+ end
179
+
180
+ def method_missing(name_as_symbol, *args, &block)
181
+ name = name_as_symbol.to_s
182
+
183
+ if name.end_with?('=')
184
+ is_readonly_field = readonly_fields.include?(name[0..-2])
185
+ is_instance_variable = instance_variables.include?("@#{name[0..-2]}".to_sym)
186
+ not_this_classes_business = is_readonly_field || is_instance_variable
187
+
188
+ if not_this_classes_business
189
+ super
190
+ else
191
+ dirty_data[name[0..-2]] = args[0]
192
+ return args[0]
193
+ end
194
+ end
195
+
196
+ return objectify(name, object_fields[name]) if object_fields.keys.include? name
197
+
198
+ return dirty_data[name] if dirty_data.keys.include? name
199
+ return data[name] if data.is_a?(Hash) && data.keys.include?(name)
200
+
201
+ super
202
+ end
203
+
204
+ def respond_to_missing?(name_as_symbol, *args)
205
+ name = name_as_symbol.to_s
206
+
207
+ return false if name.end_with?('=') && readonly_fields.include?(name[0..-2])
208
+ return false if name.end_with?('=') && instance_variables.include?(name[0..-2])
209
+
210
+ return true if object_fields.keys.include? name
211
+
212
+ return true if dirty_data.keys.include? name
213
+ return true if data.is_a?(Hash) && data.keys.include?(name)
214
+
215
+ super
216
+ end
217
+
218
+ ##
219
+ # :internal: Used to pre-populate an entity
220
+ def data=(data)
221
+ @data = data
222
+ end
223
+
224
+ alias get! reload
225
+ alias remove delete
226
+
227
+ private
228
+
229
+ def post
230
+ return self if dirty_data.empty?
231
+
232
+ @data = response connection.post creation_path, dirty_data
233
+ extract_ids
234
+ revert
235
+ self
236
+ end
237
+
238
+ def patch
239
+ return self if dirty_data.empty?
240
+
241
+ @data ||= {}
242
+ response_data = response connection.patch(path, dirty_data)
243
+ @data.merge! response_data
244
+ revert
245
+ self
246
+ end
247
+
248
+ def check_given_ids(given_ids)
249
+ if !given_ids.is_a? Hash
250
+ raise ArgumentError, "'#{self.class}.new' expects the argument to be a Hash when multiple ids are defined"
251
+ elsif given_ids.empty?
252
+ raise ArgumentError, "'#{self.class}.new' expects a the argument to not be empty."
253
+ end
254
+
255
+ missing_ids = id_fields.keys - given_ids.keys.map(&:to_s)
256
+
257
+ unless missing_ids.empty?
258
+ raise ArgumentError,
259
+ "'#{self.class}.new' expects a value for all defined IDs. The following values are missing: '#{missing_ids.join(', ')}'"
260
+ end
261
+ end
262
+
263
+ def data
264
+ @data ||= get
265
+ end
266
+
267
+ def get
268
+ response connection.get path unless @deleted
269
+ end
270
+
271
+ def readonly_fields
272
+ self.class.readonly_fields
273
+ end
274
+
275
+ def deletable
276
+ self.class.deletable
277
+ end
278
+
279
+ def object_fields
280
+ self.class.object_fields
281
+ end
282
+
283
+ def data_to_obj(raw_data, klass_or_proc = nil)
284
+ if klass_or_proc.nil?
285
+ hash_to_object raw_data
286
+ elsif klass_or_proc.is_a? Proc
287
+ klass_or_proc.call raw_data
288
+ else
289
+ klass_or_proc.new raw_data
290
+ end
291
+ end
292
+
293
+ def objectify(name, klass_or_proc = nil)
294
+ raw_data = data[name]
295
+
296
+ return nil if raw_data.nil?
297
+
298
+ return data_to_obj(raw_data, klass_or_proc) unless raw_data.is_a? Array
299
+
300
+ data[name].map do |raw_data_entry|
301
+ data_to_obj(raw_data_entry, klass_or_proc)
302
+ end
303
+ end
304
+
305
+ def creation_path
306
+ @creation_path ||= replace_path_variables_in self.class.creation_path
307
+ end
308
+
309
+ def path
310
+ @path ||= replace_path_variables_in self.class.path
311
+ end
312
+
313
+ def replace_path_variables_in(path)
314
+ interpreted_path = path.clone
315
+ path.scan(/:([a-zA-Z_][a-zA-Z0-9_]+[!?=]?)/) do |match, *|
316
+ path_variable_value = send(match)
317
+ return interpreted_path.gsub! ":#{match}", path_variable_value.to_s unless path_variable_value.nil?
318
+ raise LocalError, "Received 'nil' while replacing ':#{match}' in '#{path}' with a value."
319
+ end
320
+ interpreted_path
321
+ end
322
+
323
+ def dirty_data
324
+ @dirty_data ||= {}
325
+ end
326
+
327
+ def id_fields
328
+ self.class.id
329
+ end
330
+
331
+ def extract_ids
332
+ id_fields.each do |id_attr, id_field|
333
+ unless data.key?(id_field)
334
+ raise LocalError, "Can't find the id field '#{id_field}' in the received data."
335
+ end
336
+
337
+ instance_variable_set("@#{id_attr}", data[id_field])
338
+ end
339
+ end
340
+
341
+ def ids_set?
342
+ id_fields.map { |id_attr, _| instance_variable_get("@#{id_attr}") }.all?
343
+ end
344
+ end
345
+ end