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