free-range 0.1.0 → 0.1.2

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: 8cef8634d2f2d3e9e90289e4098b5bac3341872844764b0db4bca9e0b3bdaf0e
4
+ data.tar.gz: ca753e285e70bce1f8ca7d947c56d76683901666c103765de05a6f58a36c879b
5
5
  SHA512:
6
- metadata.gz: fed9b90bb0bc8eb9cc5244fe549f2d7d84cca82592e16d3081471c355e4fb238bd831d9e34657656a1010d50e7c6218206c7037e811bdd1bc45554ea7ac9e5a9
7
- data.tar.gz: 3f7372c1bbbbbb51b6a4da6b4794713b10e3baa79b47f1025896a087fee7cca6e53fae28ce91890200c09720a1953a250651423c3c88e93e19f423318790428f
6
+ metadata.gz: ac8287096aa861556171e3b7ad393640ff4193e16de0d062666f0b576b80b6768aaa96fa47e511ab8b94ce4e0473cec28c7f402b89b5c76f691c72d62e017934
7
+ data.tar.gz: 9a2778e40dd3a4e189ebe32bd5e56b0676d08be1bb05f13f9c3b08378d9c9a65638b5f0742bff33991f2e2521dd4549a2b9f112a526226f71889c1fde8116616
data/bin/free-range CHANGED
@@ -1,6 +1,4 @@
1
1
  #!/usr/bin/env ruby
2
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,18 @@
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
1
+ require 'optparse'
2
+ require 'rmagick'
3
+ require 'fileutils'
23
4
 
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']
5
+ module FreeRange
51
6
 
52
- # Перевіряємо наявність облікових даних
53
- if username.nil? || password.nil?
54
- puts "Помилка: необхідно вказати ім'я користувача та пароль."
55
- puts "Використовуйте опції -u|--username і -p|--password або змінні оточення WHOAMI і WHATISMYPASSWD."
7
+ # Перевірка наявності ImageMagick
8
+ begin
9
+ require 'rmagick'
10
+ rescue LoadError
11
+ puts "Помилка: бібліотека rmagick не встановлена або ImageMagick недоступний."
12
+ puts "Встановіть ImageMagick і виконайте: gem install rmagick"
56
13
  exit 1
57
14
  end
58
15
 
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
16
  # Абстрактний клас для роботи з VLAN
67
17
  class VlanContainer
68
18
  def initialize
@@ -173,8 +123,6 @@ module FreeRange
173
123
  end
174
124
 
175
125
  all_vlans, _status_counts = build_vlan_statuses(ranges, vlans)
176
-
177
- # Формуємо діапазони з урахуванням статусів
178
126
  result = []
179
127
  sorted_vlans = all_vlans.keys.sort
180
128
  start = sorted_vlans.first
@@ -183,14 +131,12 @@ module FreeRange
183
131
 
184
132
  sorted_vlans[1..-1].each do |vlan|
185
133
  unless vlan == prev + 1 && all_vlans[vlan] == status
186
- # Завершуємо попередній діапазон
187
134
  result << format_range(start, prev, status, use_color)
188
135
  start = vlan
189
136
  status = all_vlans[vlan]
190
137
  end
191
138
  prev = vlan
192
139
  end
193
- # Додаємо останній діапазон
194
140
  result << format_range(start, prev, status, use_color)
195
141
 
196
142
  puts result.join(',')
@@ -200,7 +146,6 @@ module FreeRange
200
146
  puts "VLAN Distribution for #{target}#{interface ? " (#{interface})" : ''}"
201
147
  all_vlans, status_counts = build_vlan_statuses(ranges, vlans)
202
148
 
203
- # Виводимо заголовок
204
149
  puts " 0 1 2 3 4 5 6 7 8 9 "
205
150
  (0..40).each do |h|
206
151
  start_vlan = h * 100
@@ -209,7 +154,6 @@ module FreeRange
209
154
  puts "#{format("%4d", start_vlan)} #{row}"
210
155
  end
211
156
 
212
- # Виводимо легенду
213
157
  legend_parts = [
214
158
  ["Legend: ", nil],
215
159
  ["f", 'f'], ["=free", nil], [", ", nil],
@@ -228,7 +172,6 @@ module FreeRange
228
172
  end.join
229
173
  puts "\n#{legend_text}"
230
174
 
231
- # Виводимо підсумок
232
175
  summary_parts = [
233
176
  ["Total: ", nil],
234
177
  ["f", 'f'], ["=#{status_counts['f']}", nil], [", ", nil],
@@ -250,8 +193,6 @@ module FreeRange
250
193
 
251
194
  def self.table_png(ranges, vlans, path, target, interface = nil)
252
195
  all_vlans, status_counts = build_vlan_statuses(ranges, vlans)
253
-
254
- # Налаштування розмірів і стилів
255
196
  cell_width = 12
256
197
  cell_height = 20
257
198
  rows = 41
@@ -259,42 +200,35 @@ module FreeRange
259
200
  header_height = 60
260
201
  label_width = 50
261
202
  width = label_width + cols * cell_width + 10
262
- height = header_height + rows * cell_height + 20 + 50 # Вистачає для легенди і підсумку
203
+ height = header_height + rows * cell_height + 20 + 50
263
204
  font_size = 14
264
- title_font_size = 18 # Більший шрифт для заголовка
205
+ title_font_size = 18
265
206
  font = 'Courier'
266
207
 
267
- # Створюємо полотно
268
208
  canvas = Magick::Image.new(width, height) { |options| options.background_color = 'white' }
269
209
  gc = Magick::Draw.new
270
210
  gc.font = font
271
211
  gc.pointsize = font_size
272
212
  gc.text_antialias = true
273
213
 
274
- # Малюємо заголовок із назвою пристрою
275
214
  gc.fill('black')
276
215
  gc.pointsize = title_font_size
277
216
  gc.text(10, 25, "VLAN Distribution for #{target}#{interface ? " (#{interface})" : ''}")
278
- gc.pointsize = font_size # Повертаємо стандартний розмір шрифту
217
+ gc.pointsize = font_size
279
218
 
280
- # Малюємо заголовок (0 1 2 ... 9)
281
219
  (0..9).each do |i|
282
220
  x = label_width + i * 10 * cell_width - 3
283
221
  gc.fill('black')
284
222
  gc.text(x + 5, header_height - 5, i.to_s)
285
223
  end
286
224
 
287
- # Малюємо таблицю
288
225
  (0..40).each do |h|
289
226
  start_vlan = h * 100
290
227
  end_vlan = [start_vlan + 99, 4094].min
291
228
  y = header_height + h * cell_height
292
-
293
- # Малюємо номер рядка
294
229
  gc.fill('black')
295
230
  gc.text(5, y + font_size, format("%4d", start_vlan))
296
231
 
297
- # Малюємо клітинки
298
232
  (start_vlan..end_vlan).each_with_index do |vlan, i|
299
233
  status = all_vlans[vlan] || ' '
300
234
  x = label_width + i * cell_width
@@ -314,7 +248,6 @@ module FreeRange
314
248
  end
315
249
  end
316
250
 
317
- # Малюємо легенду з кольоровими фонами
318
251
  legend_y = height - 50
319
252
  x = 10
320
253
  legend_parts = [
@@ -336,11 +269,10 @@ module FreeRange
336
269
  else
337
270
  gc.fill('black')
338
271
  gc.text(x, legend_y, text)
339
- x += text.length * 8 # Приблизно 8 пікселів на символ
272
+ x += text.length * 8
340
273
  end
341
274
  end
342
275
 
343
- # Малюємо підсумок VLAN-ів з кольоровими фонами
344
276
  summary_y = height - 30
345
277
  x = 10
346
278
  summary_parts = [
@@ -362,11 +294,10 @@ module FreeRange
362
294
  else
363
295
  gc.fill('black')
364
296
  gc.text(x, summary_y, text)
365
- x += text.length * 8 # Приблизно 8 пікселів на символ
297
+ x += text.length * 8
366
298
  end
367
299
  end
368
300
 
369
- # Зберігаємо зображення
370
301
  gc.draw(canvas)
371
302
  FileUtils.mkdir_p(path) unless Dir.exist?(path)
372
303
  filename = File.join(path, "free-range-#{target}#{interface ? "-#{interface.tr('/', '-')}" : ''}.png")
@@ -378,17 +309,12 @@ module FreeRange
378
309
 
379
310
  def self.build_vlan_statuses(ranges, vlans)
380
311
  all_vlans = {}
381
- # Позначаємо VLAN із діапазонів як free
382
312
  ranges.ranges.each do |start, finish|
383
313
  (start..finish).each { |vlan| all_vlans[vlan] = 'f' }
384
314
  end
385
- # Оновлюємо статуси для зайнятих VLAN
386
315
  vlans.vlans.uniq.each { |vlan| all_vlans[vlan] = all_vlans.key?(vlan) ? 'b' : 'e' }
387
- # Позначаємо всі інші VLAN як unused
388
316
  (1..4094).each { |vlan| all_vlans[vlan] = 'u' unless all_vlans.key?(vlan) }
389
- # Оновлюємо статуси для "інших" VLAN
390
317
  ranges.another_in_ranges.each { |vlan| all_vlans[vlan] = all_vlans.key?(vlan) && all_vlans[vlan] != 'u' ? 'c' : 'a' }
391
- # Підраховуємо статуси
392
318
  status_counts = { 'f' => 0, 'b' => 0, 'e' => 0, 'c' => 0, 'a' => 0, 'u' => 0 }
393
319
  all_vlans.each_value { |status| status_counts[status] += 1 }
394
320
 
@@ -398,23 +324,15 @@ module FreeRange
398
324
  def self.format_range(start, finish, status, use_color)
399
325
  range_text = start == finish ? "#{start}" : "#{start}-#{finish}"
400
326
  range_text_with_status = "#{range_text}(#{status})"
401
-
402
327
  if use_color
403
328
  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 # Без кольору для інших статусів
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 # Без кольору для інших статусів
418
336
  end
419
337
  else
420
338
  range_text_with_status # Текстовий вивід зі статусами
@@ -424,20 +342,13 @@ module FreeRange
424
342
  def self.format_table_char(status, use_color)
425
343
  if use_color
426
344
  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 # Без кольору для інших статусів
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 # Без кольору для інших статусів
441
352
  end
442
353
  else
443
354
  status # Текстовий вивід без кольорів
@@ -445,25 +356,14 @@ module FreeRange
445
356
  end
446
357
  end
447
358
 
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)
359
+ # Метод для заповнення Ranges для одного інтерфейсу
360
+ def self.process_interface(ssh_command, interface, ranges)
459
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]+)?)"'
460
362
  command_demux = interface ? "show configuration interfaces #{interface} | display set | match unnumbered-address" : 'show configuration interfaces | display set | match unnumbered-address'
461
363
  command_another = interface ? "show configuration interfaces #{interface} | display set | match vlan-id" : 'show configuration interfaces | display set | match vlan-id'
462
364
 
463
- # Виконуємо команду для отримання діапазонів
464
365
  full_cmd = "#{ssh_command} '#{command_ranges}'"
465
366
  result = `#{full_cmd}`.strip
466
-
467
367
  unless result.empty?
468
368
  result.each_line do |line|
469
369
  if line =~ /ranges (\d+)(?:-(\d+))?/
@@ -474,10 +374,8 @@ module FreeRange
474
374
  end
475
375
  end
476
376
 
477
- # Виконуємо команду для отримання unnumbered-address інтерфейсів (demux-source)
478
377
  full_cmd = "#{ssh_command} '#{command_demux}'"
479
378
  result = `#{full_cmd}`.strip
480
-
481
379
  unless result.empty?
482
380
  result.each_line do |line|
483
381
  if line =~ /unit (\d+)/
@@ -490,10 +388,8 @@ module FreeRange
490
388
  end
491
389
  end
492
390
 
493
- # Виконуємо команду для отримання vlan-ів усіх наявних інтерфейсів
494
391
  full_cmd = "#{ssh_command} '#{command_another}'"
495
392
  result = `#{full_cmd}`.strip
496
-
497
393
  unless result.empty?
498
394
  result.each_line do |line|
499
395
  if line =~ /vlan-id (\d+)/
@@ -507,94 +403,129 @@ module FreeRange
507
403
  end
508
404
  end
509
405
 
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
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
523
425
 
524
- if result.empty?
525
- puts "Помилка: результат команди порожній. Перевір підключення або команду."
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."
526
431
  exit 1
527
432
  end
528
433
 
529
- interfaces = result.each_line.map { |line| line.split[2] }.uniq
530
- if interfaces.empty?
531
- puts "Помилка: не знайдено інтерфейсів із діапазонами."
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 порожній. Перевір шлях або доступ."
532
456
  exit 1
533
457
  end
534
458
 
535
- interfaces.each do |intf|
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
536
501
  ranges = Ranges.new
537
502
  vlans = Vlans.new
538
-
539
- # Заповнюємо VLAN-и, фільтруючи за інтерфейсом
540
503
  subscribers_result.each_line do |line|
541
504
  if line.split.first =~ /dhcp(?:_[0-9a-fA-F.]+)?_([^:]+):(\d+)@#{Regexp.escape(target)}$/
542
505
  subscriber_interface, vlan = $1, $2.to_i
543
- vlans.add_vlan(vlan) if subscriber_interface == intf && vlan > 0
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
544
511
  end
545
512
  end
546
513
 
547
- process_interface(ssh_command, intf, ranges)
548
-
549
- # Виводимо результати
514
+ process_interface(ssh_command, interface, ranges)
550
515
  if debug
551
- puts "\nІнтерфейс: #{intf}"
516
+ puts "\nІнтерфейс: #{interface}" if interface
552
517
  Print.ranged(ranges)
553
518
  Print.vlans(vlans)
554
519
  Print.vlan_ranges(vlans)
555
520
  puts
556
521
  end
557
522
  if table_png_mode
558
- Print.table_png(ranges, vlans, table_png_mode, target, intf)
523
+ Print.table_png(ranges, vlans, table_png_mode, target, interface)
559
524
  elsif table_mode
560
- Print.table(ranges, vlans, use_color, target, intf)
525
+ Print.table(ranges, vlans, use_color, target, interface)
561
526
  else
562
- Print.combined_ranges(ranges, vlans, use_color, target, intf)
527
+ Print.combined_ranges(ranges, vlans, use_color, target, interface)
563
528
  end
564
529
  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
530
  end
599
-
600
531
  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.2
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