logstash-input-omada 0.1.1

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA256:
3
+ metadata.gz: 20f7bf2814a235d57e440942bacce366f8fb7f537c4082279d678661d2daaed6
4
+ data.tar.gz: 675681f2fe1026c2c199bc00e2e7d0bfff5e866c2989dd8502499ad813f909a7
5
+ SHA512:
6
+ metadata.gz: f5f4edc7e563108ecffd4e32ce2fb3dcac36da7f823deb08c075e5be5f76bbf41354173d77173ebc27edd8928216cbb23ceb41e15b0005c12c2a6663d7560ae1
7
+ data.tar.gz: f844d8c2fbf5777e06d027246b055f38a4645c81db35c6eafd7934b439feddfeb69023bc2d36ca0372e433fc4df9be8488187f6a6de5ea12ba155b64480e3f26
data/CHANGELOG.md ADDED
@@ -0,0 +1,2 @@
1
+ ## 0.1.0
2
+ - Plugin created with the logstash plugin generator
data/CONTRIBUTORS ADDED
@@ -0,0 +1,10 @@
1
+ The following is a list of people who have contributed ideas, code, bug
2
+ reports, or in general have helped logstash along its way.
3
+
4
+ Contributors:
5
+ * Matthew Haugen - mhaugen@haugenapplications.com
6
+
7
+ Note: If you've sent us patches, bug reports, or otherwise contributed to
8
+ Logstash, and you aren't on the list above and want to be, please let us know
9
+ and we'll make sure you're here. Contributions from folks like you are what make
10
+ open source awesome.
data/DEVELOPER.md ADDED
@@ -0,0 +1,2 @@
1
+ # logstash-input-omada
2
+ Example input plugin. This should help bootstrap your effort to write your own input plugin!
data/Gemfile ADDED
@@ -0,0 +1,2 @@
1
+ source 'https://rubygems.org'
2
+ gemspec
data/LICENSE ADDED
@@ -0,0 +1,11 @@
1
+ Licensed under the Apache License, Version 2.0 (the "License");
2
+ you may not use this file except in compliance with the License.
3
+ You may obtain a copy of the License at
4
+
5
+ http://www.apache.org/licenses/LICENSE-2.0
6
+
7
+ Unless required by applicable law or agreed to in writing, software
8
+ distributed under the License is distributed on an "AS IS" BASIS,
9
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
10
+ See the License for the specific language governing permissions and
11
+ limitations under the License.
data/README.md ADDED
@@ -0,0 +1,86 @@
1
+ # Logstash Plugin
2
+
3
+ This is a plugin for [Logstash](https://github.com/elastic/logstash).
4
+
5
+ It is fully free and fully open source. The license is Apache 2.0, meaning you are pretty much free to use it however you want in whatever way.
6
+
7
+ ## Documentation
8
+
9
+ Logstash provides infrastructure to automatically generate documentation for this plugin. We use the asciidoc format to write documentation so any comments in the source code will be first converted into asciidoc and then into html. All plugin documentation are placed under one [central location](http://www.elastic.co/guide/en/logstash/current/).
10
+
11
+ - For formatting code or config example, you can use the asciidoc `[source,ruby]` directive
12
+ - For more asciidoc formatting tips, see the excellent reference here https://github.com/elastic/docs#asciidoc-guide
13
+
14
+ ## Need Help?
15
+
16
+ Need help? Try #logstash on freenode IRC or the https://discuss.elastic.co/c/logstash discussion forum.
17
+
18
+ ## Developing
19
+
20
+ ### 1. Plugin Developement and Testing
21
+
22
+ #### Code
23
+ - To get started, you'll need JRuby with the Bundler gem installed.
24
+
25
+ - Create a new plugin or clone and existing from the GitHub [logstash-plugins](https://github.com/logstash-plugins) organization. We also provide [example plugins](https://github.com/logstash-plugins?query=example).
26
+
27
+ - Install dependencies
28
+ ```sh
29
+ bundle install
30
+ ```
31
+
32
+ #### Test
33
+
34
+ - Update your dependencies
35
+
36
+ ```sh
37
+ bundle install
38
+ ```
39
+
40
+ - Run tests
41
+
42
+ ```sh
43
+ bundle exec rspec
44
+ ```
45
+
46
+ ### 2. Running your unpublished Plugin in Logstash
47
+
48
+ #### 2.1 Run in a local Logstash clone
49
+
50
+ - Edit Logstash `Gemfile` and add the local plugin path, for example:
51
+ ```ruby
52
+ gem "logstash-filter-awesome", :path => "/your/local/logstash-filter-awesome"
53
+ ```
54
+ - Install plugin
55
+ ```sh
56
+ bin/logstash-plugin install --no-verify
57
+ ```
58
+ - Run Logstash with your plugin
59
+ ```sh
60
+ bin/logstash -e 'filter {awesome {}}'
61
+ ```
62
+ At this point any modifications to the plugin code will be applied to this local Logstash setup. After modifying the plugin, simply rerun Logstash.
63
+
64
+ #### 2.2 Run in an installed Logstash
65
+
66
+ You can use the same **2.1** method to run your plugin in an installed Logstash by editing its `Gemfile` and pointing the `:path` to your local plugin development directory or you can build the gem and install it using:
67
+
68
+ - Build your plugin gem
69
+ ```sh
70
+ gem build logstash-filter-awesome.gemspec
71
+ ```
72
+ - Install the plugin from the Logstash home
73
+ ```sh
74
+ bin/logstash-plugin install /your/local/plugin/logstash-filter-awesome.gem
75
+ ```
76
+ - Start Logstash and proceed to test the plugin
77
+
78
+ ## Contributing
79
+
80
+ All contributions are welcome: ideas, patches, documentation, bug reports, complaints, and even something you drew up on a napkin.
81
+
82
+ Programming is not a required skill. Whatever you've seen about open source and maintainers or community members saying "send patches or die" - you will not see that here.
83
+
84
+ It is more important to the community that you are able to contribute.
85
+
86
+ For more information about contributing, see the [CONTRIBUTING](https://github.com/elastic/logstash/blob/main/CONTRIBUTING.md) file.
@@ -0,0 +1,456 @@
1
+ # encoding: utf-8
2
+ require "logstash/inputs/base"
3
+ require "stud/interval"
4
+ require "socket" # for Socket.gethostname
5
+ require "json"
6
+ require "date"
7
+
8
+ # Generate a repeating message.
9
+ #
10
+ # This plugin is intented only as an example.
11
+
12
+ OpenSSL::SSL::VERIFY_PEER = OpenSSL::SSL::VERIFY_NONE
13
+
14
+ class OmadaInstance
15
+ def initialize(host, ssl, username, password)
16
+ if ssl
17
+ @session = Net::HTTP.start(host, 443, use_ssl: true)
18
+ else
19
+ @session = Net::HTTP.start(host, 80)
20
+ end
21
+
22
+ @cookies = nil
23
+
24
+ @username = username
25
+ @password = password
26
+
27
+ @omadacId = nil
28
+ @token = nil
29
+ end
30
+
31
+ def get_omadacid
32
+ unless @omadacId
33
+ info = self.get_info
34
+ @omadacId = info["omadacId"]
35
+ end
36
+
37
+ return @omadacId
38
+ end
39
+
40
+ def get_token
41
+ unless @token
42
+ omadacid = self.get_omadacid
43
+
44
+ url = "/#{omadacid}/api/v2/login"
45
+
46
+ req = Net::HTTP::Post.new(url)
47
+ req.body = JSON.generate({
48
+ username: @username,
49
+ password: @password.value
50
+ })
51
+ req["Content-Type"] = "application/json"
52
+
53
+ resp = @session.request(req)
54
+
55
+ @cookies = resp["Set-Cookie"]
56
+
57
+ resp_json = JSON.parse(resp.body)
58
+
59
+ @token = resp_json["result"]["token"]
60
+ end
61
+
62
+ return @token
63
+ end
64
+
65
+ def get_url(path, query_parameters = nil)
66
+ unless path.start_with?("/")
67
+ path = "/" + path
68
+ end
69
+
70
+ omadacid = self.get_omadacid
71
+
72
+ url = URI("/" + omadacid + path)
73
+
74
+ if query_parameters
75
+ url.query = URI.encode_www_form(query_parameters)
76
+ end
77
+
78
+ return url.to_s
79
+ end
80
+
81
+ def send_request(req)
82
+ req["Accept"] = "application/json"
83
+ req["Csrf-Token"] = self.get_token
84
+ req["Cookie"] = @cookies
85
+
86
+ resp = @session.request(req)
87
+
88
+ resp_json = JSON.parse(resp.body)
89
+
90
+ error_code = resp_json["errorCode"]
91
+
92
+ unless error_code == 0
93
+ raise "invalid response from '#{req.uri}': " + resp.body
94
+ end
95
+
96
+ return resp_json["result"]
97
+ end
98
+
99
+ def enumerate_pages(path)
100
+ max_page = 1
101
+ per_page = 100
102
+
103
+ results = []
104
+
105
+ page = 0
106
+ while page < max_page
107
+ page = page + 1
108
+
109
+ query_parameters = {
110
+ "currentPage": page,
111
+ "currentPageSize": per_page
112
+ }
113
+
114
+ result = self.send_request(Net::HTTP::Get.new(self.get_url(path, query_parameters)))
115
+
116
+ total_rows = result["totalRows"]
117
+ max_page = (total_rows / per_page).to_int + 1
118
+
119
+ results.concat(result["data"])
120
+ end
121
+
122
+ return results
123
+ end
124
+
125
+ def get_info
126
+
127
+ req = Net::HTTP::Get.new("/api/info")
128
+
129
+ resp = @session.request(req)
130
+
131
+ resp_json = JSON.parse(resp.body)
132
+
133
+ return resp_json["result"]
134
+ end
135
+
136
+ def get_current_user
137
+ resp = self.send_request(Net::HTTP::Get.new(self.get_url("/api/v2/users/current")))
138
+
139
+ return resp
140
+ end
141
+
142
+ def get_sites
143
+ current_user = self.get_current_user
144
+
145
+ current_user["privilege"]["sites"].map { |site|
146
+ OmadaSite.new(self, site["key"], site["name"])
147
+ }
148
+ end
149
+
150
+ def dispose
151
+ @session.finish
152
+ end
153
+ end
154
+
155
+ class OmadaSite
156
+ def initialize(omada_instance, site_key, site_name)
157
+ @omada_instance = omada_instance
158
+ @site_key = site_key
159
+ @site_name = site_name
160
+ end
161
+
162
+ def get_site_name
163
+ return @site_name
164
+ end
165
+
166
+ def get_site_key
167
+ return @site_key
168
+ end
169
+
170
+ def get_url(subpath, query_parameters = nil)
171
+ unless subpath.start_with?("/")
172
+ subpath = "/" + subpath
173
+ end
174
+
175
+ return @omada_instance.get_url("/api/v2/sites/" + @site_key + subpath, query_parameters)
176
+ end
177
+
178
+ def send_request(req)
179
+ resp = @omada_instance.send_request(req)
180
+
181
+ return resp
182
+ end
183
+
184
+ def enumerate_pages(subpath)
185
+ unless subpath.start_with?("/")
186
+ subpath = "/" + subpath
187
+ end
188
+
189
+ return @omada_instance.enumerate_pages("/api/v2/sites/" + @site_key + subpath)
190
+ end
191
+
192
+ def get_all_clients
193
+ return self.enumerate_pages("/clients")
194
+ end
195
+
196
+ def get_all_devices
197
+ result = self.send_request(Net::HTTP::Get.new(self.get_url("/devices")))
198
+
199
+ return result
200
+ end
201
+
202
+ def get_all_events
203
+ return self.enumerate_pages("/events")
204
+ end
205
+
206
+ def get_client_distribution
207
+ return self.send_request(Net::HTTP::Get.new(self.get_url("/dashboard/clientsFreqDistribution")))
208
+ end
209
+
210
+ def get_association_failure_statistics
211
+ return self.send_request(Net::HTTP::Get.new(self.get_url("/dashboard/associationFailures")))
212
+ end
213
+
214
+ def get_latest_isp_load(to = nil, from = nil)
215
+ unless to
216
+ to = DateTime.now
217
+ end
218
+
219
+ unless from
220
+ from = to - 1
221
+ end
222
+
223
+ resp = self.send_request(Net::HTTP::Get.new(self.get_url("/dashboard/ispLoad", {
224
+ start: from.to_time.to_i,
225
+ end: to.to_time.to_i
226
+ })))
227
+
228
+ ret = []
229
+ resp.each { |port|
230
+ last = port["data"].last
231
+
232
+ if last
233
+ ret.append({
234
+ port: {
235
+ id: port["portId"],
236
+ name: port["portName"]
237
+ },
238
+ totalRate: last["totalRate"],
239
+ latency: last["latency"],
240
+ time: last["time"]
241
+ })
242
+ end
243
+ }
244
+
245
+ return ret
246
+ end
247
+
248
+ def get_latest_speed_tests(to = nil, from = nil)
249
+ unless to
250
+ to = DateTime.now
251
+ end
252
+
253
+ unless from
254
+ from = to - 1
255
+ end
256
+
257
+ req = Net::HTTP::Post.new(self.get_url("/stat/wanSpeeds"))
258
+ req.body = JSON.generate({
259
+ start: from.to_time.to_i,
260
+ end: to.to_time.to_i
261
+ })
262
+ req["Content-Type"] = "application/json"
263
+
264
+ resp = self.send_request(req)
265
+
266
+ last = resp.last
267
+
268
+ ret = []
269
+
270
+ if last
271
+ last["ports"].each { |port|
272
+ ret.append({
273
+ time: last["time"],
274
+ port: {
275
+ id: port["portId"],
276
+ name: port["name"]
277
+ },
278
+ latencyMs: port["latency"],
279
+ downloadBandwidthMbps: port["down"],
280
+ uploadBandwidthMbps: port["up"]
281
+ })
282
+ }
283
+ end
284
+
285
+ return ret
286
+ end
287
+ end
288
+
289
+ class LogStash::Inputs::Omada < LogStash::Inputs::Base
290
+ config_name "omada"
291
+
292
+ # If undefined, Logstash will complain, even if codec is unused.
293
+ default :codec, "plain"
294
+
295
+ # The host string to use in the event.
296
+ config :server, :validate => :string
297
+ config :ssl, :validate => :boolean
298
+
299
+ config :username, :validate => :string
300
+ config :password, :validate => :password
301
+
302
+ # Set how frequently messages should be sent.
303
+ #
304
+ # The default, `60`, checks once a minute.
305
+ config :interval, :validate => :number, :default => 60
306
+
307
+ private
308
+ def get_site_hash(site)
309
+ return {
310
+ name: site.get_site_name,
311
+ key: site.get_site_key
312
+ }
313
+ end
314
+
315
+ def apply_association_failure_statistics(site, queue)
316
+ association_failure_statistics = site.get_association_failure_statistics
317
+
318
+ site_hash = get_site_hash(site)
319
+
320
+ omada = {
321
+ site: site_hash,
322
+ associationFailureStatistics: association_failure_statistics
323
+ }
324
+
325
+ event = LogStash::Event.new("omada" => omada, "tags" => ["omada.association_failure_statistics"])
326
+ decorate(event)
327
+ queue << event
328
+ end
329
+
330
+ def apply_latest_isp_load(site, queue)
331
+ now = DateTime.now
332
+
333
+ isp_load_ports = site.get_latest_isp_load(now, @next_isp_load_since)
334
+
335
+ unless isp_load_ports.empty?
336
+ @next_isp_load_since = now
337
+
338
+ site_hash = get_site_hash(site)
339
+
340
+ isp_load_ports.each { |isp_load_port|
341
+ omada = {
342
+ site: site_hash,
343
+ ispLoad: isp_load_port
344
+ }
345
+
346
+ timestamp = Time.at(isp_load_port.delete(:time)).utc.iso8601
347
+
348
+ event = LogStash::Event.new("@timestamp" => timestamp, "omada" => omada, "tags" => ["omada.isp_load"])
349
+ decorate(event)
350
+ queue << event
351
+ }
352
+ end
353
+ end
354
+
355
+ def apply_latest_speed_tests(site, queue)
356
+ now = DateTime.now
357
+
358
+ speed_test_ports = site.get_latest_speed_tests(now, @next_speed_tests_since)
359
+
360
+ unless speed_test_ports.empty?
361
+ @next_speed_tests_since = now
362
+
363
+ site_hash = get_site_hash(site)
364
+
365
+ speed_test_ports.each { |speed_test_port|
366
+ omada = {
367
+ site: site_hash,
368
+ speedTest: speed_test_port
369
+ }
370
+
371
+ timestamp = Time.at(speed_test_port.delete(:time)).utc.iso8601
372
+
373
+ event = LogStash::Event.new("@timestamp" => timestamp, "omada" => omada, "tags" => ["omada.speed_test"])
374
+ decorate(event)
375
+ queue << event
376
+ }
377
+ end
378
+ end
379
+
380
+ def apply_client_distribution(site, queue)
381
+ site_hash = get_site_hash(site)
382
+
383
+ client_distribution = site.get_client_distribution
384
+
385
+ omada = {
386
+ site: site_hash,
387
+ clientDistribution: client_distribution
388
+ }
389
+
390
+ event = LogStash::Event.new("omada" => omada, "tags" => ["omada.client_distribution"])
391
+ decorate(event)
392
+ queue << event
393
+ end
394
+
395
+ def apply_devices(site, queue)
396
+ site_hash = get_site_hash(site)
397
+
398
+ site.get_all_devices.each { |device|
399
+ omada = {
400
+ site: site_hash,
401
+ device: device
402
+ }
403
+
404
+ event = LogStash::Event.new("omada" => omada, "tags" => ["omada.device"])
405
+ decorate(event)
406
+ queue << event
407
+ }
408
+ end
409
+
410
+ def apply_clients(site, queue)
411
+ site_hash = get_site_hash(site)
412
+
413
+ site.get_all_clients.each { |client|
414
+ omada = {
415
+ site: site_hash,
416
+ client: client
417
+ }
418
+
419
+ event = LogStash::Event.new("omada" => omada, "tags" => ["omada.client"])
420
+ decorate(event)
421
+ queue << event
422
+ }
423
+ end
424
+
425
+ public
426
+ def register
427
+ @host = Socket.gethostname
428
+
429
+ @logger.info("Connecting to Omada API", :server => @server)
430
+ @omada_instance = OmadaInstance.new(@server, @ssl, @username, @password)
431
+ end # def register
432
+
433
+ def run(queue)
434
+ # we can abort the loop if stop? becomes true
435
+ while !stop?
436
+ @omada_instance.get_sites.each { |site|
437
+ apply_clients(site, queue)
438
+ apply_devices(site, queue)
439
+ apply_client_distribution(site, queue)
440
+ apply_association_failure_statistics(site, queue)
441
+ apply_latest_isp_load(site, queue)
442
+ apply_latest_speed_tests(site, queue)
443
+ }
444
+
445
+ # because the sleep interval can be big, when shutdown happens
446
+ # we want to be able to abort the sleep
447
+ # Stud.stoppable_sleep will frequently evaluate the given block
448
+ # and abort the sleep(@interval) if the return value is true
449
+ Stud.stoppable_sleep(@interval) { stop? }
450
+ end # loop
451
+ end # def run
452
+
453
+ def stop
454
+ @omada_instance.dispose
455
+ end
456
+ end # class LogStash::Inputs::Omada
@@ -0,0 +1,25 @@
1
+ Gem::Specification.new do |s|
2
+ s.name = 'logstash-input-omada'
3
+ s.version = '0.1.1'
4
+ s.licenses = ['Apache-2.0']
5
+ s.summary = 'Poll Omada API for clients, devices, and statistics.'
6
+ s.description = 'Poll Omada API for clients, devices, and statistics.'
7
+ s.homepage = 'https://www.haugenapplications.com'
8
+ s.authors = ['Matthew Haugen']
9
+ s.email = 'mhaugen@haugenapplications.com'
10
+ s.require_paths = ['lib']
11
+
12
+ # Files
13
+ s.files = Dir['lib/**/*','spec/**/*','vendor/**/*','*.gemspec','*.md','CONTRIBUTORS','Gemfile','LICENSE','NOTICE.TXT']
14
+ # Tests
15
+ s.test_files = s.files.grep(%r{^(test|spec|features)/})
16
+
17
+ # Special flag to let us know this is actually a logstash plugin
18
+ s.metadata = { "logstash_plugin" => "true", "logstash_group" => "input" }
19
+
20
+ # Gem dependencies
21
+ s.add_runtime_dependency "logstash-core-plugin-api", "~> 2.0"
22
+ s.add_runtime_dependency 'logstash-codec-plain'
23
+ s.add_runtime_dependency 'stud', '>= 0.0.22'
24
+ s.add_development_dependency 'logstash-devutils', '>= 0.0.16'
25
+ end
@@ -0,0 +1,11 @@
1
+ # encoding: utf-8
2
+ require "logstash/devutils/rspec/spec_helper"
3
+ require "logstash/inputs/omada"
4
+
5
+ describe LogStash::Inputs::Omada do
6
+
7
+ it_behaves_like "an interruptible input plugin" do
8
+ let(:config) { { "interval" => 100 } }
9
+ end
10
+
11
+ end
metadata ADDED
@@ -0,0 +1,110 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: logstash-input-omada
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.1.1
5
+ platform: ruby
6
+ authors:
7
+ - Matthew Haugen
8
+ autorequire:
9
+ bindir: bin
10
+ cert_chain: []
11
+ date: 2022-07-15 00:00:00.000000000 Z
12
+ dependencies:
13
+ - !ruby/object:Gem::Dependency
14
+ name: logstash-core-plugin-api
15
+ requirement: !ruby/object:Gem::Requirement
16
+ requirements:
17
+ - - "~>"
18
+ - !ruby/object:Gem::Version
19
+ version: '2.0'
20
+ type: :runtime
21
+ prerelease: false
22
+ version_requirements: !ruby/object:Gem::Requirement
23
+ requirements:
24
+ - - "~>"
25
+ - !ruby/object:Gem::Version
26
+ version: '2.0'
27
+ - !ruby/object:Gem::Dependency
28
+ name: logstash-codec-plain
29
+ requirement: !ruby/object:Gem::Requirement
30
+ requirements:
31
+ - - ">="
32
+ - !ruby/object:Gem::Version
33
+ version: '0'
34
+ type: :runtime
35
+ prerelease: false
36
+ version_requirements: !ruby/object:Gem::Requirement
37
+ requirements:
38
+ - - ">="
39
+ - !ruby/object:Gem::Version
40
+ version: '0'
41
+ - !ruby/object:Gem::Dependency
42
+ name: stud
43
+ requirement: !ruby/object:Gem::Requirement
44
+ requirements:
45
+ - - ">="
46
+ - !ruby/object:Gem::Version
47
+ version: 0.0.22
48
+ type: :runtime
49
+ prerelease: false
50
+ version_requirements: !ruby/object:Gem::Requirement
51
+ requirements:
52
+ - - ">="
53
+ - !ruby/object:Gem::Version
54
+ version: 0.0.22
55
+ - !ruby/object:Gem::Dependency
56
+ name: logstash-devutils
57
+ requirement: !ruby/object:Gem::Requirement
58
+ requirements:
59
+ - - ">="
60
+ - !ruby/object:Gem::Version
61
+ version: 0.0.16
62
+ type: :development
63
+ prerelease: false
64
+ version_requirements: !ruby/object:Gem::Requirement
65
+ requirements:
66
+ - - ">="
67
+ - !ruby/object:Gem::Version
68
+ version: 0.0.16
69
+ description: Poll Omada API for clients, devices, and statistics.
70
+ email: mhaugen@haugenapplications.com
71
+ executables: []
72
+ extensions: []
73
+ extra_rdoc_files: []
74
+ files:
75
+ - CHANGELOG.md
76
+ - CONTRIBUTORS
77
+ - DEVELOPER.md
78
+ - Gemfile
79
+ - LICENSE
80
+ - README.md
81
+ - lib/logstash/inputs/omada.rb
82
+ - logstash-input-omada.gemspec
83
+ - spec/inputs/omada_spec.rb
84
+ homepage: https://www.haugenapplications.com
85
+ licenses:
86
+ - Apache-2.0
87
+ metadata:
88
+ logstash_plugin: 'true'
89
+ logstash_group: input
90
+ post_install_message:
91
+ rdoc_options: []
92
+ require_paths:
93
+ - lib
94
+ required_ruby_version: !ruby/object:Gem::Requirement
95
+ requirements:
96
+ - - ">="
97
+ - !ruby/object:Gem::Version
98
+ version: '0'
99
+ required_rubygems_version: !ruby/object:Gem::Requirement
100
+ requirements:
101
+ - - ">="
102
+ - !ruby/object:Gem::Version
103
+ version: '0'
104
+ requirements: []
105
+ rubygems_version: 3.0.3.1
106
+ signing_key:
107
+ specification_version: 4
108
+ summary: Poll Omada API for clients, devices, and statistics.
109
+ test_files:
110
+ - spec/inputs/omada_spec.rb