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