free-range 0.1.3 → 0.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/lib/free-range.rb +113 -26
- 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: a0252edadec09077b1c95f148f2e5dec18a0faf393f7c9b225fc0ca0962b06af
|
4
|
+
data.tar.gz: f45851705405c80dd7ec484c439711716146317e0b19efdba3e4d756f5fce2fa
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 76e14e7c6e5a94c2f4a88fe987477e8ca960aab236fb2d63b24e7655a694d5e0da57a0b08aee141c4f6b4ecd4e1ec2caf64d671703033ab626a725de30c4a9a5
|
7
|
+
data.tar.gz: 879632d0abb8539b61b99e49af8bf427b786163be9e669e0a5542f2286234666d9c183a360fe21cac97bc9debb1ad97e816b15f18bd1cb494adfb205252c43ad
|
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|
|
@@ -466,7 +520,12 @@ module FreeRange
|
|
466
520
|
def self.run
|
467
521
|
options = {}
|
468
522
|
OptionParser.new do |opts|
|
469
|
-
opts.banner =
|
523
|
+
opts.banner = <<~BANNER
|
524
|
+
Використання: free-range <IP-адреса або hostname> [опції]
|
525
|
+
|
526
|
+
Аналізує розподіл VLAN на мережевих пристроях, генеруючи таблиці або PNG-зображення.
|
527
|
+
BANNER
|
528
|
+
opts.on("-h", "--help", "Показати цю довідку") { puts opts; exit 0 }
|
470
529
|
opts.on("-u", "--username USERNAME", "Ім'я користувача для SSH") { |u| options[:username] = u }
|
471
530
|
opts.on("-p", "--password PASSWORD", "Пароль для SSH") { |p| options[:password] = p }
|
472
531
|
opts.on("-n", "--no-color", "Вимкнути кольоровий вивід") { options[:no_color] = true }
|
@@ -474,44 +533,72 @@ module FreeRange
|
|
474
533
|
opts.on("-t", "--table", "Вивести діаграму розподілу VLAN-ів") { options[:table] = true }
|
475
534
|
opts.on("-g", "--table-png PATH", "Зберегти діаграму розподілу VLAN-ів як PNG") { |path| options[:table_png] = path }
|
476
535
|
opts.on("-i", "--interface INTERFACE", "Назва інтерфейсу або 'all'") { |i| options[:interface] = i }
|
536
|
+
opts.on("-c", "--config CONFIG_FILE", "Шлях до конфігураційного файлу") { |c| options[:config_file] = c }
|
477
537
|
end.parse!
|
478
538
|
|
479
539
|
if ARGV.empty?
|
480
540
|
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."
|
541
|
+
puts "Використовуйте: free-range --help для довідки."
|
490
542
|
exit 1
|
491
543
|
end
|
492
544
|
|
545
|
+
# Визначаємо змінні з опцій
|
493
546
|
use_color = !options[:no_color] && ENV['TERM'] && ENV['TERM'] != 'dumb'
|
494
547
|
debug = options[:debug]
|
495
548
|
table_mode = options[:table]
|
496
549
|
table_png_mode = options[:table_png]
|
497
550
|
interface = options[:interface]
|
551
|
+
config_file = options[:config_file]
|
552
|
+
|
553
|
+
# Ініціалізуємо config з порожнім login
|
554
|
+
config = Config.new({ target: ARGV[0], username: nil, password: nil })
|
555
|
+
|
556
|
+
# Завантажуємо конфігураційний файл, якщо він вказаний
|
557
|
+
if config_file
|
558
|
+
begin
|
559
|
+
# Виконуємо конфігураційний файл у контексті існуючого об’єкта config
|
560
|
+
config.instance_eval(File.read(config_file), config_file)
|
561
|
+
rescue LoadError, Errno::ENOENT
|
562
|
+
puts "Помилка: неможливо завантажити конфігураційний файл '#{config_file}'."
|
563
|
+
exit 1
|
564
|
+
rescue ArgumentError => e
|
565
|
+
puts "Помилка в конфігураційному файлі '#{config_file}': #{e.message}"
|
566
|
+
exit 1
|
567
|
+
rescue StandardError => e
|
568
|
+
puts "Помилка в конфігураційному файлі '#{config_file}': #{e.message}"
|
569
|
+
exit 1
|
570
|
+
end
|
571
|
+
end
|
572
|
+
|
573
|
+
# Визначаємо username і password з пріоритетом: аргументи > config > ENV
|
574
|
+
username = options[:username] || config.username || ENV['WHOAMI']
|
575
|
+
password = options[:password] || config.password || ENV['WHATISMYPASSWD']
|
576
|
+
|
577
|
+
if username.nil? || password.nil?
|
578
|
+
puts "Помилка: необхідно вказати ім'я користувача та пароль."
|
579
|
+
puts "Використовуйте опції -u/--username і -p/--password, конфігураційний файл або змінні оточення WHOAMI і WHATISMYPASSWD."
|
580
|
+
exit 1
|
581
|
+
end
|
498
582
|
|
499
583
|
login = { target: ARGV[0], username: username, password: password }
|
500
584
|
target = ARGV[0].split('.')[0]
|
501
585
|
puts "Connecting to device: #{login[:target]}"
|
502
586
|
|
503
|
-
|
504
|
-
|
587
|
+
# Оновлюємо config з актуальними login даними
|
588
|
+
config = Config.new(login) { |c|
|
589
|
+
c.username = config.username if config.username
|
590
|
+
c.password = config.password if config.password
|
591
|
+
}
|
505
592
|
|
506
|
-
subscribers_result = `#{subscribers_command}`.strip
|
593
|
+
subscribers_result = `#{config.subscribers_command}`.strip
|
507
594
|
if subscribers_result.empty?
|
508
595
|
puts "Помилка: результат subscribers_command порожній. Перевір шлях або доступ."
|
509
596
|
exit 1
|
510
597
|
end
|
511
598
|
|
512
599
|
if interface == "all"
|
513
|
-
|
514
|
-
|
600
|
+
full_cmd = "#{config.ssh_command} '#{config.command_interfaces}'"
|
601
|
+
puts "[DEBUG] Executing command: #{full_cmd}" if debug
|
515
602
|
result = `#{full_cmd}`.strip
|
516
603
|
if result.empty?
|
517
604
|
puts "Помилка: результат команди порожній. Перевір підключення або команду."
|
@@ -534,7 +621,7 @@ module FreeRange
|
|
534
621
|
end
|
535
622
|
end
|
536
623
|
|
537
|
-
process_interface(
|
624
|
+
process_interface(config, intf, ranges, debug)
|
538
625
|
if debug
|
539
626
|
puts "\nІнтерфейс: #{intf}"
|
540
627
|
Print.ranged(ranges)
|
@@ -564,7 +651,7 @@ module FreeRange
|
|
564
651
|
end
|
565
652
|
end
|
566
653
|
|
567
|
-
process_interface(
|
654
|
+
process_interface(config, interface, ranges, debug)
|
568
655
|
if debug
|
569
656
|
puts "\nІнтерфейс: #{interface}" if interface
|
570
657
|
Print.ranged(ranges)
|
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.
|
4
|
+
version: 0.2.0
|
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:
|