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