free-range 0.1.0 → 0.1.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.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 76578fcf399be31962cc3e611e74c8cdb408f5a75277977e9a5aabb11b01ea41
4
- data.tar.gz: 6d6e57d1e254f4745cd8b6afbc8524b7bd356f50f9ef049d9a6e36de72377ec6
3
+ metadata.gz: 1d9b08207790b2176f72f34bd8c8a1284fcaa4029f67b01c5f82b7f87ebeab87
4
+ data.tar.gz: 20632cd0aa6ae42cf7c0804bdd138fc0ae7cabc0d0d01c2f8e08ec1dbf12e094
5
5
  SHA512:
6
- metadata.gz: fed9b90bb0bc8eb9cc5244fe549f2d7d84cca82592e16d3081471c355e4fb238bd831d9e34657656a1010d50e7c6218206c7037e811bdd1bc45554ea7ac9e5a9
7
- data.tar.gz: 3f7372c1bbbbbb51b6a4da6b4794713b10e3baa79b47f1025896a087fee7cca6e53fae28ce91890200c09720a1953a250651423c3c88e93e19f423318790428f
6
+ metadata.gz: ed55ef454cba43a4c4354f7243316219c965859c9bded0b807f0ee5d421e94701ce1c1f6d4ed7c2756f800d2dfac9cd53ca23ddb4451654b951e01a814199434
7
+ data.tar.gz: bb79ea6e1f81c238fcc507757ee241e12b7dda0d40e4b0bbc999928b8e57de25107c9e82ef169ef4ce468d0b19ae358afc7abe94490a1017e6f48d0b64d21b47
data/bin/free-range CHANGED
@@ -1,6 +1,4 @@
1
1
  #!/usr/bin/env ruby
2
- require 'free-range'
2
+ require 'free_range'
3
3
 
4
- # Виклик основної логіки, якщо потрібно (зазвичай код уже запускається в lib/free_range.rb)
5
-
6
- FreeRange.main if defined?(FreeRange.main)
4
+ FreeRange.run
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 CHANGED
@@ -1,68 +1,17 @@
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']
1
+ require 'optparse'
2
+ require 'rmagick'
3
+ require 'fileutils'
51
4
 
52
- # Перевіряємо наявність облікових даних
53
- if username.nil? || password.nil?
54
- puts "Помилка: необхідно вказати ім'я користувача та пароль."
55
- puts "Використовуйте опції -u|--username і -p|--password або змінні оточення WHOAMI і WHATISMYPASSWD."
5
+ module FreeRange
6
+ # Перевірка наявності ImageMagick
7
+ begin
8
+ require 'rmagick'
9
+ rescue LoadError
10
+ puts "Помилка: бібліотека rmagick не встановлена або ImageMagick недоступний."
11
+ puts "Встановіть ImageMagick і виконайте: gem install rmagick"
56
12
  exit 1
57
13
  end
58
14
 
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
15
  # Абстрактний клас для роботи з VLAN
67
16
  class VlanContainer
68
17
  def initialize
@@ -173,8 +122,6 @@ module FreeRange
173
122
  end
174
123
 
175
124
  all_vlans, _status_counts = build_vlan_statuses(ranges, vlans)
176
-
177
- # Формуємо діапазони з урахуванням статусів
178
125
  result = []
179
126
  sorted_vlans = all_vlans.keys.sort
180
127
  start = sorted_vlans.first
@@ -183,14 +130,12 @@ module FreeRange
183
130
 
184
131
  sorted_vlans[1..-1].each do |vlan|
185
132
  unless vlan == prev + 1 && all_vlans[vlan] == status
186
- # Завершуємо попередній діапазон
187
133
  result << format_range(start, prev, status, use_color)
188
134
  start = vlan
189
135
  status = all_vlans[vlan]
190
136
  end
191
137
  prev = vlan
192
138
  end
193
- # Додаємо останній діапазон
194
139
  result << format_range(start, prev, status, use_color)
195
140
 
196
141
  puts result.join(',')
@@ -200,7 +145,6 @@ module FreeRange
200
145
  puts "VLAN Distribution for #{target}#{interface ? " (#{interface})" : ''}"
201
146
  all_vlans, status_counts = build_vlan_statuses(ranges, vlans)
202
147
 
203
- # Виводимо заголовок
204
148
  puts " 0 1 2 3 4 5 6 7 8 9 "
205
149
  (0..40).each do |h|
206
150
  start_vlan = h * 100
@@ -209,7 +153,6 @@ module FreeRange
209
153
  puts "#{format("%4d", start_vlan)} #{row}"
210
154
  end
211
155
 
212
- # Виводимо легенду
213
156
  legend_parts = [
214
157
  ["Legend: ", nil],
215
158
  ["f", 'f'], ["=free", nil], [", ", nil],
@@ -228,7 +171,6 @@ module FreeRange
228
171
  end.join
229
172
  puts "\n#{legend_text}"
230
173
 
231
- # Виводимо підсумок
232
174
  summary_parts = [
233
175
  ["Total: ", nil],
234
176
  ["f", 'f'], ["=#{status_counts['f']}", nil], [", ", nil],
@@ -250,8 +192,6 @@ module FreeRange
250
192
 
251
193
  def self.table_png(ranges, vlans, path, target, interface = nil)
252
194
  all_vlans, status_counts = build_vlan_statuses(ranges, vlans)
253
-
254
- # Налаштування розмірів і стилів
255
195
  cell_width = 12
256
196
  cell_height = 20
257
197
  rows = 41
@@ -259,42 +199,35 @@ module FreeRange
259
199
  header_height = 60
260
200
  label_width = 50
261
201
  width = label_width + cols * cell_width + 10
262
- height = header_height + rows * cell_height + 20 + 50 # Вистачає для легенди і підсумку
202
+ height = header_height + rows * cell_height + 20 + 50
263
203
  font_size = 14
264
- title_font_size = 18 # Більший шрифт для заголовка
204
+ title_font_size = 18
265
205
  font = 'Courier'
266
206
 
267
- # Створюємо полотно
268
207
  canvas = Magick::Image.new(width, height) { |options| options.background_color = 'white' }
269
208
  gc = Magick::Draw.new
270
209
  gc.font = font
271
210
  gc.pointsize = font_size
272
211
  gc.text_antialias = true
273
212
 
274
- # Малюємо заголовок із назвою пристрою
275
213
  gc.fill('black')
276
214
  gc.pointsize = title_font_size
277
215
  gc.text(10, 25, "VLAN Distribution for #{target}#{interface ? " (#{interface})" : ''}")
278
- gc.pointsize = font_size # Повертаємо стандартний розмір шрифту
216
+ gc.pointsize = font_size
279
217
 
280
- # Малюємо заголовок (0 1 2 ... 9)
281
218
  (0..9).each do |i|
282
219
  x = label_width + i * 10 * cell_width - 3
283
220
  gc.fill('black')
284
221
  gc.text(x + 5, header_height - 5, i.to_s)
285
222
  end
286
223
 
287
- # Малюємо таблицю
288
224
  (0..40).each do |h|
289
225
  start_vlan = h * 100
290
226
  end_vlan = [start_vlan + 99, 4094].min
291
227
  y = header_height + h * cell_height
292
-
293
- # Малюємо номер рядка
294
228
  gc.fill('black')
295
229
  gc.text(5, y + font_size, format("%4d", start_vlan))
296
230
 
297
- # Малюємо клітинки
298
231
  (start_vlan..end_vlan).each_with_index do |vlan, i|
299
232
  status = all_vlans[vlan] || ' '
300
233
  x = label_width + i * cell_width
@@ -314,7 +247,6 @@ module FreeRange
314
247
  end
315
248
  end
316
249
 
317
- # Малюємо легенду з кольоровими фонами
318
250
  legend_y = height - 50
319
251
  x = 10
320
252
  legend_parts = [
@@ -336,11 +268,10 @@ module FreeRange
336
268
  else
337
269
  gc.fill('black')
338
270
  gc.text(x, legend_y, text)
339
- x += text.length * 8 # Приблизно 8 пікселів на символ
271
+ x += text.length * 8
340
272
  end
341
273
  end
342
274
 
343
- # Малюємо підсумок VLAN-ів з кольоровими фонами
344
275
  summary_y = height - 30
345
276
  x = 10
346
277
  summary_parts = [
@@ -362,11 +293,10 @@ module FreeRange
362
293
  else
363
294
  gc.fill('black')
364
295
  gc.text(x, summary_y, text)
365
- x += text.length * 8 # Приблизно 8 пікселів на символ
296
+ x += text.length * 8
366
297
  end
367
298
  end
368
299
 
369
- # Зберігаємо зображення
370
300
  gc.draw(canvas)
371
301
  FileUtils.mkdir_p(path) unless Dir.exist?(path)
372
302
  filename = File.join(path, "free-range-#{target}#{interface ? "-#{interface.tr('/', '-')}" : ''}.png")
@@ -378,17 +308,12 @@ module FreeRange
378
308
 
379
309
  def self.build_vlan_statuses(ranges, vlans)
380
310
  all_vlans = {}
381
- # Позначаємо VLAN із діапазонів як free
382
311
  ranges.ranges.each do |start, finish|
383
312
  (start..finish).each { |vlan| all_vlans[vlan] = 'f' }
384
313
  end
385
- # Оновлюємо статуси для зайнятих VLAN
386
314
  vlans.vlans.uniq.each { |vlan| all_vlans[vlan] = all_vlans.key?(vlan) ? 'b' : 'e' }
387
- # Позначаємо всі інші VLAN як unused
388
315
  (1..4094).each { |vlan| all_vlans[vlan] = 'u' unless all_vlans.key?(vlan) }
389
- # Оновлюємо статуси для "інших" VLAN
390
316
  ranges.another_in_ranges.each { |vlan| all_vlans[vlan] = all_vlans.key?(vlan) && all_vlans[vlan] != 'u' ? 'c' : 'a' }
391
- # Підраховуємо статуси
392
317
  status_counts = { 'f' => 0, 'b' => 0, 'e' => 0, 'c' => 0, 'a' => 0, 'u' => 0 }
393
318
  all_vlans.each_value { |status| status_counts[status] += 1 }
394
319
 
@@ -398,23 +323,15 @@ module FreeRange
398
323
  def self.format_range(start, finish, status, use_color)
399
324
  range_text = start == finish ? "#{start}" : "#{start}-#{finish}"
400
325
  range_text_with_status = "#{range_text}(#{status})"
401
-
402
326
  if use_color
403
327
  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 # Без кольору для інших статусів
328
+ when 'f' then "\e[32m#{range_text}\e[0m" # Зелений для free
329
+ when 'b' then "\e[33m#{range_text}\e[0m" # Жовтий для busy
330
+ when 'e' then "\e[31m#{range_text}\e[0m" # Червоний для error
331
+ when 'c' then "\e[35m#{range_text}\e[0m" # Фіолетовий для configured
332
+ when 'a' then "\e[34m#{range_text}\e[0m" # Синій для another
333
+ when 'u' then "\e[90m#{range_text}\e[0m" # Темно-сірий для unused
334
+ else range_text # Без кольору для інших статусів
418
335
  end
419
336
  else
420
337
  range_text_with_status # Текстовий вивід зі статусами
@@ -424,20 +341,13 @@ module FreeRange
424
341
  def self.format_table_char(status, use_color)
425
342
  if use_color
426
343
  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 # Без кольору для інших статусів
344
+ when 'f' then "\e[48;5;2m\e[30m#{status}\e[0m" # Зелений фон, чорний текст
345
+ when 'b' then "\e[48;5;3m\e[30m#{status}\e[0m" # Жовтий фон, чорний текст
346
+ when 'e' then "\e[48;5;1m\e[30m#{status}\e[0m" # Червоний фон, чорний текст
347
+ when 'c' then "\e[48;5;5m\e[30m#{status}\e[0m" # Фіолетовий фон, чорний текст
348
+ when 'a' then "\e[48;5;4m\e[30m#{status}\e[0m" # Синій фон, чорний текст
349
+ when 'u' then "\e[48;5;8m\e[30m#{status}\e[0m" # Темно-сірий фон, чорний текст
350
+ else status # Без кольору для інших статусів
441
351
  end
442
352
  else
443
353
  status # Текстовий вивід без кольорів
@@ -445,25 +355,14 @@ module FreeRange
445
355
  end
446
356
  end
447
357
 
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)
358
+ # Метод для заповнення Ranges для одного інтерфейсу
359
+ def self.process_interface(ssh_command, interface, ranges)
459
360
  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
361
  command_demux = interface ? "show configuration interfaces #{interface} | display set | match unnumbered-address" : 'show configuration interfaces | display set | match unnumbered-address'
461
362
  command_another = interface ? "show configuration interfaces #{interface} | display set | match vlan-id" : 'show configuration interfaces | display set | match vlan-id'
462
363
 
463
- # Виконуємо команду для отримання діапазонів
464
364
  full_cmd = "#{ssh_command} '#{command_ranges}'"
465
365
  result = `#{full_cmd}`.strip
466
-
467
366
  unless result.empty?
468
367
  result.each_line do |line|
469
368
  if line =~ /ranges (\d+)(?:-(\d+))?/
@@ -474,10 +373,8 @@ module FreeRange
474
373
  end
475
374
  end
476
375
 
477
- # Виконуємо команду для отримання unnumbered-address інтерфейсів (demux-source)
478
376
  full_cmd = "#{ssh_command} '#{command_demux}'"
479
377
  result = `#{full_cmd}`.strip
480
-
481
378
  unless result.empty?
482
379
  result.each_line do |line|
483
380
  if line =~ /unit (\d+)/
@@ -490,10 +387,8 @@ module FreeRange
490
387
  end
491
388
  end
492
389
 
493
- # Виконуємо команду для отримання vlan-ів усіх наявних інтерфейсів
494
390
  full_cmd = "#{ssh_command} '#{command_another}'"
495
391
  result = `#{full_cmd}`.strip
496
-
497
392
  unless result.empty?
498
393
  result.each_line do |line|
499
394
  if line =~ /vlan-id (\d+)/
@@ -507,94 +402,129 @@ module FreeRange
507
402
  end
508
403
  end
509
404
 
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
405
+ # Основна логіка виконання
406
+ def self.run
407
+ options = {}
408
+ OptionParser.new do |opts|
409
+ opts.banner = "Використання: free-range <IP-адреса або hostname> [опції]"
410
+ opts.on("-u", "--username USERNAME", "Ім'я користувача для SSH") { |u| options[:username] = u }
411
+ opts.on("-p", "--password PASSWORD", "Пароль для SSH") { |p| options[:password] = p }
412
+ opts.on("-n", "--no-color", "Вимкнути кольоровий вивід") { options[:no_color] = true }
413
+ opts.on("-d", "--debug", "Увімкнути дебаг-режим") { options[:debug] = true }
414
+ opts.on("-t", "--table", "Вивести діаграму розподілу VLAN-ів") { options[:table] = true }
415
+ opts.on("-g", "--table-png PATH", "Зберегти діаграму розподілу VLAN-ів як PNG") { |path| options[:table_png] = path }
416
+ opts.on("-i", "--interface INTERFACE", "Назва інтерфейсу або 'all'") { |i| options[:interface] = i }
417
+ end.parse!
418
+
419
+ if ARGV.empty?
420
+ puts "Помилка: потрібно вказати IP-адресу або hostname роутера."
421
+ puts "Використання: free-range <IP-адреса або hostname> [-u|--username USERNAME] [-p|--password PASSWORD] [-n|--no-color] [-d|--debug] [-t|--table] [--table-png PATH] [-i|--interface INTERFACE]"
422
+ exit 1
423
+ end
523
424
 
524
- if result.empty?
525
- puts "Помилка: результат команди порожній. Перевір підключення або команду."
425
+ username = options[:username] || ENV['WHOAMI']
426
+ password = options[:password] || ENV['WHATISMYPASSWD']
427
+ if username.nil? || password.nil?
428
+ puts "Помилка: необхідно вказати ім'я користувача та пароль."
429
+ puts "Використовуйте опції -u|--username і -p|--password або змінні оточення WHOAMI і WHATISMYPASSWD."
526
430
  exit 1
527
431
  end
528
432
 
529
- interfaces = result.each_line.map { |line| line.split[2] }.uniq
530
- if interfaces.empty?
531
- puts "Помилка: не знайдено інтерфейсів із діапазонами."
433
+ use_color = !options[:no_color] && ENV['TERM'] && ENV['TERM'] != 'dumb'
434
+ debug = options[:debug]
435
+ table_mode = options[:table]
436
+ table_png_mode = options[:table_png]
437
+ interface = options[:interface]
438
+
439
+ # # Перевірка формату інтерфейсу
440
+ # if interface && interface != "all" && interface !~ /^[a-z]+-\d+\/\d+\/\d+$/
441
+ # puts "Помилка: некоректна назва інтерфейсу. Використовуйте формат 'xe-0/0/2' або 'all'."
442
+ # exit 1
443
+ # end
444
+
445
+ login = { target: ARGV[0], username: username, password: password }
446
+ target = ARGV[0].split('.')[0]
447
+ puts "Connecting to device: #{login[:target]}"
448
+
449
+ ssh_command = "sshpass -p \"#{login[:password]}\" ssh -C -x -4 -o StrictHostKeyChecking=no #{login[:username]}@#{login[:target]}"
450
+ subscribers_command = "ssh -C -x roffice /usr/local/share/noc/bin/radius-subscribers"
451
+
452
+ subscribers_result = `#{subscribers_command}`.strip
453
+ if subscribers_result.empty?
454
+ puts "Помилка: результат subscribers_command порожній. Перевір шлях або доступ."
532
455
  exit 1
533
456
  end
534
457
 
535
- interfaces.each do |intf|
458
+ if interface == "all"
459
+ command_interfaces = 'show configuration interfaces | no-more | display set | match dynamic-profile | match "ranges ([0-9]+(-[0-9]+)?)"'
460
+ full_cmd = "#{ssh_command} '#{command_interfaces}'"
461
+ result = `#{full_cmd}`.strip
462
+ if result.empty?
463
+ puts "Помилка: результат команди порожній. Перевір підключення або команду."
464
+ exit 1
465
+ end
466
+
467
+ interfaces = result.each_line.map { |line| line.split[2] }.uniq
468
+ if interfaces.empty?
469
+ puts "Помилка: не знайдено інтерфейсів із діапазонами."
470
+ exit 1
471
+ end
472
+
473
+ interfaces.each do |intf|
474
+ ranges = Ranges.new
475
+ vlans = Vlans.new
476
+ subscribers_result.each_line do |line|
477
+ if line.split.first =~ /dhcp(?:_[0-9a-fA-F.]+)?_([^:]+):(\d+)@#{Regexp.escape(target)}$/
478
+ subscriber_interface, vlan = $1, $2.to_i
479
+ vlans.add_vlan(vlan) if subscriber_interface == intf && vlan > 0
480
+ end
481
+ end
482
+
483
+ process_interface(ssh_command, intf, ranges)
484
+ if debug
485
+ puts "\nІнтерфейс: #{intf}"
486
+ Print.ranged(ranges)
487
+ Print.vlans(vlans)
488
+ Print.vlan_ranges(vlans)
489
+ puts
490
+ end
491
+ if table_png_mode
492
+ Print.table_png(ranges, vlans, table_png_mode, target, intf)
493
+ elsif table_mode
494
+ Print.table(ranges, vlans, use_color, target, intf)
495
+ else
496
+ Print.combined_ranges(ranges, vlans, use_color, target, intf)
497
+ end
498
+ end
499
+ else
536
500
  ranges = Ranges.new
537
501
  vlans = Vlans.new
538
-
539
- # Заповнюємо VLAN-и, фільтруючи за інтерфейсом
540
502
  subscribers_result.each_line do |line|
541
503
  if line.split.first =~ /dhcp(?:_[0-9a-fA-F.]+)?_([^:]+):(\d+)@#{Regexp.escape(target)}$/
542
504
  subscriber_interface, vlan = $1, $2.to_i
543
- vlans.add_vlan(vlan) if subscriber_interface == intf && vlan > 0
505
+ if interface
506
+ vlans.add_vlan(vlan) if subscriber_interface == interface && vlan > 0
507
+ else
508
+ vlans.add_vlan(vlan) if vlan > 0
509
+ end
544
510
  end
545
511
  end
546
512
 
547
- process_interface(ssh_command, intf, ranges)
548
-
549
- # Виводимо результати
513
+ process_interface(ssh_command, interface, ranges)
550
514
  if debug
551
- puts "\nІнтерфейс: #{intf}"
515
+ puts "\nІнтерфейс: #{interface}" if interface
552
516
  Print.ranged(ranges)
553
517
  Print.vlans(vlans)
554
518
  Print.vlan_ranges(vlans)
555
519
  puts
556
520
  end
557
521
  if table_png_mode
558
- Print.table_png(ranges, vlans, table_png_mode, target, intf)
522
+ Print.table_png(ranges, vlans, table_png_mode, target, interface)
559
523
  elsif table_mode
560
- Print.table(ranges, vlans, use_color, target, intf)
524
+ Print.table(ranges, vlans, use_color, target, interface)
561
525
  else
562
- Print.combined_ranges(ranges, vlans, use_color, target, intf)
526
+ Print.combined_ranges(ranges, vlans, use_color, target, interface)
563
527
  end
564
528
  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
529
  end
599
-
600
530
  end
@@ -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 CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: free-range
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.1.0
4
+ version: 0.1.1
5
5
  platform: ruby
6
6
  authors:
7
7
  - Oleksandr Russkikh //aka Olden Gremlin
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2025-09-26 00:00:00.000000000 Z
11
+ date: 2025-09-29 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: rmagick
@@ -61,7 +61,9 @@ extensions: []
61
61
  extra_rdoc_files: []
62
62
  files:
63
63
  - bin/free-range
64
+ - bin/free-range.~
64
65
  - lib/free-range.rb
66
+ - lib/free-range.rb.~
65
67
  homepage: https://github.com/oldengremlin/free-range
66
68
  licenses:
67
69
  - Apache-2.0