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