free-range 0.1.2 → 0.2.0

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.
@@ -0,0 +1,531 @@
1
+ require 'optparse'
2
+ require 'rmagick'
3
+ require 'fileutils'
4
+
5
+ module FreeRange
6
+
7
+ # Перевірка наявності ImageMagick
8
+ begin
9
+ require 'rmagick'
10
+ rescue LoadError
11
+ puts "Помилка: бібліотека rmagick не встановлена або ImageMagick недоступний."
12
+ puts "Встановіть ImageMagick і виконайте: gem install rmagick"
13
+ exit 1
14
+ end
15
+
16
+ # Абстрактний клас для роботи з VLAN
17
+ class VlanContainer
18
+ def initialize
19
+ @vlans = []
20
+ end
21
+
22
+ # Додаємо VLAN до списку (віртуальний метод, може бути перевизначений)
23
+ def add_vlan(vlan)
24
+ @vlans << vlan
25
+ end
26
+
27
+ # Повертаємо масив VLAN
28
+ def vlans
29
+ @vlans.uniq.sort
30
+ end
31
+
32
+ # Створюємо хеш діапазонів із VLAN
33
+ def ranges
34
+ return {} if @vlans.empty?
35
+
36
+ vlan_ranges_hash = {}
37
+ sorted_vlans = vlans
38
+ start = sorted_vlans.first
39
+ prev = start
40
+
41
+ sorted_vlans[1..-1].each do |vlan|
42
+ unless vlan == prev + 1
43
+ vlan_ranges_hash[start] = prev
44
+ start = vlan
45
+ end
46
+ prev = vlan
47
+ end
48
+ vlan_ranges_hash[start] = prev
49
+
50
+ vlan_ranges_hash
51
+ end
52
+ end
53
+
54
+ # Клас для зберігання та обробки діапазонів
55
+ class Ranges < VlanContainer
56
+ def initialize
57
+ super
58
+ @another_in_ranges = []
59
+ end
60
+
61
+ # Додаємо діапазон до списку VLAN-ів
62
+ def add_range(start, finish)
63
+ (start..finish).each { |vlan| @vlans << vlan }
64
+ end
65
+
66
+ # Додаємо діапазон до списку "інших" VLAN-ів
67
+ def add_another_range(start, finish)
68
+ (start..finish).each { |vlan| @another_in_ranges << vlan }
69
+ end
70
+
71
+ # Повертаємо масив "інших" VLAN-ів
72
+ def another_in_ranges
73
+ @another_in_ranges.uniq.sort
74
+ end
75
+ end
76
+
77
+ # Клас для зберігання та обробки VLAN-ів
78
+ class Vlans < VlanContainer
79
+ # Метод add_vlan уже успадковано від VlanContainer
80
+ # Метод vlans уже успадковано
81
+ # Метод ranges уже успадковано, але перейменуємо для зрозумілості
82
+ alias vlan_ranges ranges
83
+ end
84
+
85
+ # Клас для виводу даних
86
+ class Print
87
+ def self.ranged(ranges)
88
+ puts "\nСформований хеш діапазонів (в порядку зростання):"
89
+ if ranges.ranges.empty?
90
+ puts "Не знайдено діапазонів."
91
+ else
92
+ ranges.ranges.sort_by { |k, _| k }.each do |start, end_val|
93
+ puts "range[#{start}]=#{end_val}"
94
+ end
95
+ end
96
+ end
97
+
98
+ def self.vlans(vlans)
99
+ puts "\nЗайняті VLAN-и в межах діапазонів (в порядку зростання):"
100
+ if vlans.vlans.empty?
101
+ puts "Не знайдено VLAN-ів у межах діапазонів."
102
+ else
103
+ puts vlans.vlans.uniq.sort.join(", ")
104
+ end
105
+ end
106
+
107
+ def self.vlan_ranges(vlans)
108
+ puts "\nДіапазони VLAN-ів (в порядку зростання):"
109
+ vlan_ranges = vlans.vlan_ranges
110
+ if vlan_ranges.empty?
111
+ puts "Не знайдено діапазонів VLAN-ів."
112
+ else
113
+ vlan_ranges.sort_by { |k, _| k }.each do |start, end_val|
114
+ puts "range[#{start}]=#{end_val}"
115
+ end
116
+ end
117
+ end
118
+
119
+ def self.combined_ranges(ranges, vlans, use_color, target, interface = nil)
120
+ if ranges.ranges.empty?
121
+ puts "Не знайдено діапазонів."
122
+ return
123
+ end
124
+
125
+ all_vlans, _status_counts = build_vlan_statuses(ranges, vlans)
126
+ result = []
127
+ sorted_vlans = all_vlans.keys.sort
128
+ start = sorted_vlans.first
129
+ prev = start
130
+ status = all_vlans[start]
131
+
132
+ sorted_vlans[1..-1].each do |vlan|
133
+ unless vlan == prev + 1 && all_vlans[vlan] == status
134
+ result << format_range(start, prev, status, use_color)
135
+ start = vlan
136
+ status = all_vlans[vlan]
137
+ end
138
+ prev = vlan
139
+ end
140
+ result << format_range(start, prev, status, use_color)
141
+
142
+ puts result.join(',')
143
+ end
144
+
145
+ def self.table(ranges, vlans, use_color, target, interface = nil)
146
+ puts "VLAN Distribution for #{target}#{interface ? " (#{interface})" : ''}"
147
+ all_vlans, status_counts = build_vlan_statuses(ranges, vlans)
148
+
149
+ puts " 0 1 2 3 4 5 6 7 8 9 "
150
+ (0..40).each do |h|
151
+ start_vlan = h * 100
152
+ end_vlan = [start_vlan + 99, 4094].min
153
+ row = (start_vlan..end_vlan).map { |vlan| format_table_char(all_vlans[vlan] || ' ', use_color) }.join
154
+ puts "#{format("%4d", start_vlan)} #{row}"
155
+ end
156
+
157
+ legend_parts = [
158
+ ["Legend: ", nil],
159
+ ["f", 'f'], ["=free", nil], [", ", nil],
160
+ ["b", 'b'], ["=busy", nil], [", ", nil],
161
+ ["e", 'e'], ["=error", nil], [", ", nil],
162
+ ["c", 'c'], ["=configured", nil], [", ", nil],
163
+ ["a", 'a'], ["=another", nil], [", ", nil],
164
+ ["u", 'u'], ["=unused", nil]
165
+ ]
166
+ legend_text = legend_parts.map do |text, status|
167
+ if status && use_color
168
+ format_table_char(status, use_color)
169
+ else
170
+ text
171
+ end
172
+ end.join
173
+ puts "\n#{legend_text}"
174
+
175
+ summary_parts = [
176
+ ["Total: ", nil],
177
+ ["f", 'f'], ["=#{status_counts['f']}", nil], [", ", nil],
178
+ ["b", 'b'], ["=#{status_counts['b']}", nil], [", ", nil],
179
+ ["e", 'e'], ["=#{status_counts['e']}", nil], [", ", nil],
180
+ ["c", 'c'], ["=#{status_counts['c']}", nil], [", ", nil],
181
+ ["a", 'a'], ["=#{status_counts['a']}", nil], [", ", nil],
182
+ ["u", 'u'], ["=#{status_counts['u']}", nil]
183
+ ]
184
+ summary_text = summary_parts.map do |text, status|
185
+ if status && use_color
186
+ format_table_char(status, use_color)
187
+ else
188
+ text
189
+ end
190
+ end.join
191
+ puts summary_text
192
+ end
193
+
194
+ def self.table_png(ranges, vlans, path, target, interface = nil)
195
+ all_vlans, status_counts = build_vlan_statuses(ranges, vlans)
196
+ cell_width = 12
197
+ cell_height = 20
198
+ rows = 41
199
+ cols = 100
200
+ header_height = 60
201
+ label_width = 50
202
+ width = label_width + cols * cell_width + 10
203
+ height = header_height + rows * cell_height + 20 + 50
204
+ font_size = 14
205
+ title_font_size = 18
206
+ font = 'Courier'
207
+
208
+ canvas = Magick::Image.new(width, height) { |options| options.background_color = 'white' }
209
+ gc = Magick::Draw.new
210
+ gc.font = font
211
+ gc.pointsize = font_size
212
+ gc.text_antialias = true
213
+
214
+ gc.fill('black')
215
+ gc.pointsize = title_font_size
216
+ gc.text(10, 25, "VLAN Distribution for #{target}#{interface ? " (#{interface})" : ''}")
217
+ gc.pointsize = font_size
218
+
219
+ (0..9).each do |i|
220
+ x = label_width + i * 10 * cell_width - 3
221
+ gc.fill('black')
222
+ gc.text(x + 5, header_height - 5, i.to_s)
223
+ end
224
+
225
+ (0..40).each do |h|
226
+ start_vlan = h * 100
227
+ end_vlan = [start_vlan + 99, 4094].min
228
+ y = header_height + h * cell_height
229
+ gc.fill('black')
230
+ gc.text(5, y + font_size, format("%4d", start_vlan))
231
+
232
+ (start_vlan..end_vlan).each_with_index do |vlan, i|
233
+ status = all_vlans[vlan] || ' '
234
+ x = label_width + i * cell_width
235
+ color = case status
236
+ when 'f' then '#00FF00' # Зелений
237
+ when 'b' then '#FFFF00' # Жовтий
238
+ when 'e' then '#FF0000' # Червоний
239
+ when 'c' then '#FF00FF' # Фіолетовий
240
+ when 'a' then '#0000FF' # Синій
241
+ when 'u' then '#555555' # Темно-сірий
242
+ else 'white' # Пробіл
243
+ end
244
+ gc.fill(color)
245
+ gc.rectangle(x, y, x + cell_width - 1, y + cell_height - 1)
246
+ gc.fill('black')
247
+ gc.text(x + 2, y + font_size, status) unless status == ' '
248
+ end
249
+ end
250
+
251
+ legend_y = height - 50
252
+ x = 10
253
+ legend_parts = [
254
+ ["Legend: ", nil],
255
+ ["f", '#00FF00'], ["=free", nil], [", ", nil],
256
+ ["b", '#FFFF00'], ["=busy", nil], [", ", nil],
257
+ ["e", '#FF0000'], ["=error", nil], [", ", nil],
258
+ ["c", '#FF00FF'], ["=configured", nil], [", ", nil],
259
+ ["a", '#0000FF'], ["=another", nil], [", ", nil],
260
+ ["u", '#555555'], ["=unused", nil]
261
+ ]
262
+ legend_parts.each do |text, color|
263
+ if color
264
+ gc.fill(color)
265
+ gc.rectangle(x, legend_y - font_size + 2, x + 10, legend_y + 2)
266
+ gc.fill('black')
267
+ gc.text(x + 2, legend_y, text)
268
+ x += 12
269
+ else
270
+ gc.fill('black')
271
+ gc.text(x, legend_y, text)
272
+ x += text.length * 8
273
+ end
274
+ end
275
+
276
+ summary_y = height - 30
277
+ x = 10
278
+ summary_parts = [
279
+ ["Total: ", nil],
280
+ ["f", '#00FF00'], ["=#{status_counts['f']}", nil], [", ", nil],
281
+ ["b", '#FFFF00'], ["=#{status_counts['b']}", nil], [", ", nil],
282
+ ["e", '#FF0000'], ["=#{status_counts['e']}", nil], [", ", nil],
283
+ ["c", '#FF00FF'], ["=#{status_counts['c']}", nil], [", ", nil],
284
+ ["a", '#0000FF'], ["=#{status_counts['a']}", nil], [", ", nil],
285
+ ["u", '#555555'], ["=#{status_counts['u']}", nil]
286
+ ]
287
+ summary_parts.each do |text, color|
288
+ if color
289
+ gc.fill(color)
290
+ gc.rectangle(x, summary_y - font_size + 2, x + 10, summary_y + 2)
291
+ gc.fill('black')
292
+ gc.text(x + 2, summary_y, text)
293
+ x += 12
294
+ else
295
+ gc.fill('black')
296
+ gc.text(x, summary_y, text)
297
+ x += text.length * 8
298
+ end
299
+ end
300
+
301
+ gc.draw(canvas)
302
+ FileUtils.mkdir_p(path) unless Dir.exist?(path)
303
+ filename = File.join(path, "free-range-#{target}#{interface ? "-#{interface.tr('/', '-')}" : ''}.png")
304
+ canvas.write(filename)
305
+ puts "Зображення збережено: #{filename}"
306
+ end
307
+
308
+ private
309
+
310
+ def self.build_vlan_statuses(ranges, vlans)
311
+ all_vlans = {}
312
+ ranges.ranges.each do |start, finish|
313
+ (start..finish).each { |vlan| all_vlans[vlan] = 'f' }
314
+ end
315
+ vlans.vlans.uniq.each { |vlan| all_vlans[vlan] = all_vlans.key?(vlan) ? 'b' : 'e' }
316
+ (1..4094).each { |vlan| all_vlans[vlan] = 'u' unless all_vlans.key?(vlan) }
317
+ ranges.another_in_ranges.each { |vlan| all_vlans[vlan] = all_vlans.key?(vlan) && all_vlans[vlan] != 'u' ? 'c' : 'a' }
318
+ status_counts = { 'f' => 0, 'b' => 0, 'e' => 0, 'c' => 0, 'a' => 0, 'u' => 0 }
319
+ all_vlans.each_value { |status| status_counts[status] += 1 }
320
+
321
+ [all_vlans, status_counts]
322
+ end
323
+
324
+ def self.format_range(start, finish, status, use_color)
325
+ range_text = start == finish ? "#{start}" : "#{start}-#{finish}"
326
+ range_text_with_status = "#{range_text}(#{status})"
327
+ if use_color
328
+ case status
329
+ when 'f' then "\e[32m#{range_text}\e[0m" # Зелений для free
330
+ when 'b' then "\e[33m#{range_text}\e[0m" # Жовтий для busy
331
+ when 'e' then "\e[31m#{range_text}\e[0m" # Червоний для error
332
+ when 'c' then "\e[35m#{range_text}\e[0m" # Фіолетовий для configured
333
+ when 'a' then "\e[34m#{range_text}\e[0m" # Синій для another
334
+ when 'u' then "\e[90m#{range_text}\e[0m" # Темно-сірий для unused
335
+ else range_text # Без кольору для інших статусів
336
+ end
337
+ else
338
+ range_text_with_status # Текстовий вивід зі статусами
339
+ end
340
+ end
341
+
342
+ def self.format_table_char(status, use_color)
343
+ if use_color
344
+ case status
345
+ when 'f' then "\e[48;5;2m\e[30m#{status}\e[0m" # Зелений фон, чорний текст
346
+ when 'b' then "\e[48;5;3m\e[30m#{status}\e[0m" # Жовтий фон, чорний текст
347
+ when 'e' then "\e[48;5;1m\e[30m#{status}\e[0m" # Червоний фон, чорний текст
348
+ when 'c' then "\e[48;5;5m\e[30m#{status}\e[0m" # Фіолетовий фон, чорний текст
349
+ when 'a' then "\e[48;5;4m\e[30m#{status}\e[0m" # Синій фон, чорний текст
350
+ when 'u' then "\e[48;5;8m\e[30m#{status}\e[0m" # Темно-сірий фон, чорний текст
351
+ else status # Без кольору для інших статусів
352
+ end
353
+ else
354
+ status # Текстовий вивід без кольорів
355
+ end
356
+ end
357
+ end
358
+
359
+ # Метод для заповнення Ranges для одного інтерфейсу
360
+ def self.process_interface(ssh_command, interface, ranges)
361
+ command_ranges = interface ? "show configuration interfaces #{interface} | no-more | display set | match dynamic-profile | match \"ranges ([0-9]+(-[0-9]+)?)\"" : 'show configuration interfaces | no-more | display set | match dynamic-profile | match "ranges ([0-9]+(-[0-9]+)?)"'
362
+ command_demux = interface ? "show configuration interfaces #{interface} | display set | match unnumbered-address" : 'show configuration interfaces | display set | match unnumbered-address'
363
+ command_another = interface ? "show configuration interfaces #{interface} | display set | match vlan-id" : 'show configuration interfaces | display set | match vlan-id'
364
+
365
+ full_cmd = "#{ssh_command} '#{command_ranges}'"
366
+ result = `#{full_cmd}`.strip
367
+ unless result.empty?
368
+ result.each_line do |line|
369
+ if line =~ /ranges (\d+)(?:-(\d+))?/
370
+ start_range = $1.to_i
371
+ end_range = $2 ? $2.to_i : $1.to_i
372
+ ranges.add_range(start_range, end_range)
373
+ end
374
+ end
375
+ end
376
+
377
+ full_cmd = "#{ssh_command} '#{command_demux}'"
378
+ result = `#{full_cmd}`.strip
379
+ unless result.empty?
380
+ result.each_line do |line|
381
+ if line =~ /unit (\d+)/
382
+ start_range = $1.to_i
383
+ if start_range > 0
384
+ end_range = start_range
385
+ ranges.add_range(start_range, end_range)
386
+ end
387
+ end
388
+ end
389
+ end
390
+
391
+ full_cmd = "#{ssh_command} '#{command_another}'"
392
+ result = `#{full_cmd}`.strip
393
+ unless result.empty?
394
+ result.each_line do |line|
395
+ if line =~ /vlan-id (\d+)/
396
+ start_range = $1.to_i
397
+ if start_range > 0
398
+ end_range = start_range
399
+ ranges.add_another_range(start_range, end_range)
400
+ end
401
+ end
402
+ end
403
+ end
404
+ end
405
+
406
+ # Основна логіка виконання
407
+ def self.run
408
+ options = {}
409
+ OptionParser.new do |opts|
410
+ opts.banner = "Використання: free-range <IP-адреса або hostname> [опції]"
411
+ opts.on("-u", "--username USERNAME", "Ім'я користувача для SSH") { |u| options[:username] = u }
412
+ opts.on("-p", "--password PASSWORD", "Пароль для SSH") { |p| options[:password] = p }
413
+ opts.on("-n", "--no-color", "Вимкнути кольоровий вивід") { options[:no_color] = true }
414
+ opts.on("-d", "--debug", "Увімкнути дебаг-режим") { options[:debug] = true }
415
+ opts.on("-t", "--table", "Вивести діаграму розподілу VLAN-ів") { options[:table] = true }
416
+ opts.on("-g", "--table-png PATH", "Зберегти діаграму розподілу VLAN-ів як PNG") { |path| options[:table_png] = path }
417
+ opts.on("-i", "--interface INTERFACE", "Назва інтерфейсу або 'all'") { |i| options[:interface] = i }
418
+ end.parse!
419
+
420
+ if ARGV.empty?
421
+ puts "Помилка: потрібно вказати IP-адресу або hostname роутера."
422
+ puts "Використання: free-range <IP-адреса або hostname> [-u|--username USERNAME] [-p|--password PASSWORD] [-n|--no-color] [-d|--debug] [-t|--table] [--table-png PATH] [-i|--interface INTERFACE]"
423
+ exit 1
424
+ end
425
+
426
+ username = options[:username] || ENV['WHOAMI']
427
+ password = options[:password] || ENV['WHATISMYPASSWD']
428
+ if username.nil? || password.nil?
429
+ puts "Помилка: необхідно вказати ім'я користувача та пароль."
430
+ puts "Використовуйте опції -u|--username і -p|--password або змінні оточення WHOAMI і WHATISMYPASSWD."
431
+ exit 1
432
+ end
433
+
434
+ use_color = !options[:no_color] && ENV['TERM'] && ENV['TERM'] != 'dumb'
435
+ debug = options[:debug]
436
+ table_mode = options[:table]
437
+ table_png_mode = options[:table_png]
438
+ interface = options[:interface]
439
+
440
+ # # Перевірка формату інтерфейсу
441
+ # if interface && interface != "all" && interface !~ /^[a-z]+-\d+\/\d+\/\d+$/
442
+ # puts "Помилка: некоректна назва інтерфейсу. Використовуйте формат 'xe-0/0/2' або 'all'."
443
+ # exit 1
444
+ # end
445
+
446
+ login = { target: ARGV[0], username: username, password: password }
447
+ target = ARGV[0].split('.')[0]
448
+ puts "Connecting to device: #{login[:target]}"
449
+
450
+ ssh_command = "sshpass -p \"#{login[:password]}\" ssh -C -x -4 -o StrictHostKeyChecking=no #{login[:username]}@#{login[:target]}"
451
+ subscribers_command = "ssh -C -x roffice /usr/local/share/noc/bin/radius-subscribers"
452
+
453
+ subscribers_result = `#{subscribers_command}`.strip
454
+ if subscribers_result.empty?
455
+ puts "Помилка: результат subscribers_command порожній. Перевір шлях або доступ."
456
+ exit 1
457
+ end
458
+
459
+ if interface == "all"
460
+ command_interfaces = 'show configuration interfaces | no-more | display set | match dynamic-profile | match "ranges ([0-9]+(-[0-9]+)?)"'
461
+ full_cmd = "#{ssh_command} '#{command_interfaces}'"
462
+ result = `#{full_cmd}`.strip
463
+ if result.empty?
464
+ puts "Помилка: результат команди порожній. Перевір підключення або команду."
465
+ exit 1
466
+ end
467
+
468
+ interfaces = result.each_line.map { |line| line.split[2] }.uniq
469
+ if interfaces.empty?
470
+ puts "Помилка: не знайдено інтерфейсів із діапазонами."
471
+ exit 1
472
+ end
473
+
474
+ interfaces.each do |intf|
475
+ ranges = Ranges.new
476
+ vlans = Vlans.new
477
+ subscribers_result.each_line do |line|
478
+ if line.split.first =~ /dhcp(?:_[0-9a-fA-F.]+)?_([^:]+):(\d+)@#{Regexp.escape(target)}$/
479
+ subscriber_interface, vlan = $1, $2.to_i
480
+ vlans.add_vlan(vlan) if subscriber_interface == intf && vlan > 0
481
+ end
482
+ end
483
+
484
+ process_interface(ssh_command, intf, ranges)
485
+ if debug
486
+ puts "\nІнтерфейс: #{intf}"
487
+ Print.ranged(ranges)
488
+ Print.vlans(vlans)
489
+ Print.vlan_ranges(vlans)
490
+ puts
491
+ end
492
+ if table_png_mode
493
+ Print.table_png(ranges, vlans, table_png_mode, target, intf)
494
+ elsif table_mode
495
+ Print.table(ranges, vlans, use_color, target, intf)
496
+ else
497
+ Print.combined_ranges(ranges, vlans, use_color, target, intf)
498
+ end
499
+ end
500
+ else
501
+ ranges = Ranges.new
502
+ vlans = Vlans.new
503
+ subscribers_result.each_line do |line|
504
+ if line.split.first =~ /dhcp(?:_[0-9a-fA-F.]+)?_([^:]+):(\d+)@#{Regexp.escape(target)}$/
505
+ subscriber_interface, vlan = $1, $2.to_i
506
+ if interface
507
+ vlans.add_vlan(vlan) if subscriber_interface == interface && vlan > 0
508
+ else
509
+ vlans.add_vlan(vlan) if vlan > 0
510
+ end
511
+ end
512
+ end
513
+
514
+ process_interface(ssh_command, interface, ranges)
515
+ if debug
516
+ puts "\nІнтерфейс: #{interface}" if interface
517
+ Print.ranged(ranges)
518
+ Print.vlans(vlans)
519
+ Print.vlan_ranges(vlans)
520
+ puts
521
+ end
522
+ if table_png_mode
523
+ Print.table_png(ranges, vlans, table_png_mode, target, interface)
524
+ elsif table_mode
525
+ Print.table(ranges, vlans, use_color, target, interface)
526
+ else
527
+ Print.combined_ranges(ranges, vlans, use_color, target, interface)
528
+ end
529
+ end
530
+ end
531
+ end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: free-range
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.1.2
4
+ version: 0.2.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Oleksandr Russkikh //aka Olden Gremlin
@@ -52,6 +52,20 @@ dependencies:
52
52
  - - "~>"
53
53
  - !ruby/object:Gem::Version
54
54
  version: '13.0'
55
+ - !ruby/object:Gem::Dependency
56
+ name: yard
57
+ requirement: !ruby/object:Gem::Requirement
58
+ requirements:
59
+ - - "~>"
60
+ - !ruby/object:Gem::Version
61
+ version: '0.9'
62
+ type: :development
63
+ prerelease: false
64
+ version_requirements: !ruby/object:Gem::Requirement
65
+ requirements:
66
+ - - "~>"
67
+ - !ruby/object:Gem::Version
68
+ version: '0.9'
55
69
  description: A Ruby script to analyze VLAN distribution on network devices, generating
56
70
  tables or PNG images.
57
71
  email: olden@ukr-com.net
@@ -61,9 +75,9 @@ extensions: []
61
75
  extra_rdoc_files: []
62
76
  files:
63
77
  - bin/free-range
64
- - bin/free-range.~
65
78
  - lib/free-range.rb
66
79
  - lib/free-range.rb.~
80
+ - lib/free-range.rb~
67
81
  homepage: https://github.com/oldengremlin/free-range
68
82
  licenses:
69
83
  - Apache-2.0
data/bin/free-range.~ DELETED
@@ -1,6 +0,0 @@
1
- #!/usr/bin/env ruby
2
- require 'free-range'
3
-
4
- # Виклик основної логіки, якщо потрібно (зазвичай код уже запускається в lib/free_range.rb)
5
-
6
- FreeRange.main if defined?(FreeRange.main)