free-range 0.1.0 → 0.1.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/bin/free-range +2 -4
- data/bin/free-range.~ +6 -0
- data/lib/free-range.rb +129 -199
- 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: 1d9b08207790b2176f72f34bd8c8a1284fcaa4029f67b01c5f82b7f87ebeab87
|
4
|
+
data.tar.gz: 20632cd0aa6ae42cf7c0804bdd138fc0ae7cabc0d0d01c2f8e08ec1dbf12e094
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: ed55ef454cba43a4c4354f7243316219c965859c9bded0b807f0ee5d421e94701ce1c1f6d4ed7c2756f800d2dfac9cd53ca23ddb4451654b951e01a814199434
|
7
|
+
data.tar.gz: bb79ea6e1f81c238fcc507757ee241e12b7dda0d40e4b0bbc999928b8e57de25107c9e82ef169ef4ce468d0b19ae358afc7abe94490a1017e6f48d0b64d21b47
|
data/bin/free-range
CHANGED
data/bin/free-range.~
ADDED
data/lib/free-range.rb
CHANGED
@@ -1,68 +1,17 @@
|
|
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
|
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']
|
1
|
+
require 'optparse'
|
2
|
+
require 'rmagick'
|
3
|
+
require 'fileutils'
|
51
4
|
|
52
|
-
|
53
|
-
|
54
|
-
|
55
|
-
|
5
|
+
module FreeRange
|
6
|
+
# Перевірка наявності ImageMagick
|
7
|
+
begin
|
8
|
+
require 'rmagick'
|
9
|
+
rescue LoadError
|
10
|
+
puts "Помилка: бібліотека rmagick не встановлена або ImageMagick недоступний."
|
11
|
+
puts "Встановіть ImageMagick і виконайте: gem install rmagick"
|
56
12
|
exit 1
|
57
13
|
end
|
58
14
|
|
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
15
|
# Абстрактний клас для роботи з VLAN
|
67
16
|
class VlanContainer
|
68
17
|
def initialize
|
@@ -173,8 +122,6 @@ module FreeRange
|
|
173
122
|
end
|
174
123
|
|
175
124
|
all_vlans, _status_counts = build_vlan_statuses(ranges, vlans)
|
176
|
-
|
177
|
-
# Формуємо діапазони з урахуванням статусів
|
178
125
|
result = []
|
179
126
|
sorted_vlans = all_vlans.keys.sort
|
180
127
|
start = sorted_vlans.first
|
@@ -183,14 +130,12 @@ module FreeRange
|
|
183
130
|
|
184
131
|
sorted_vlans[1..-1].each do |vlan|
|
185
132
|
unless vlan == prev + 1 && all_vlans[vlan] == status
|
186
|
-
# Завершуємо попередній діапазон
|
187
133
|
result << format_range(start, prev, status, use_color)
|
188
134
|
start = vlan
|
189
135
|
status = all_vlans[vlan]
|
190
136
|
end
|
191
137
|
prev = vlan
|
192
138
|
end
|
193
|
-
# Додаємо останній діапазон
|
194
139
|
result << format_range(start, prev, status, use_color)
|
195
140
|
|
196
141
|
puts result.join(',')
|
@@ -200,7 +145,6 @@ module FreeRange
|
|
200
145
|
puts "VLAN Distribution for #{target}#{interface ? " (#{interface})" : ''}"
|
201
146
|
all_vlans, status_counts = build_vlan_statuses(ranges, vlans)
|
202
147
|
|
203
|
-
# Виводимо заголовок
|
204
148
|
puts " 0 1 2 3 4 5 6 7 8 9 "
|
205
149
|
(0..40).each do |h|
|
206
150
|
start_vlan = h * 100
|
@@ -209,7 +153,6 @@ module FreeRange
|
|
209
153
|
puts "#{format("%4d", start_vlan)} #{row}"
|
210
154
|
end
|
211
155
|
|
212
|
-
# Виводимо легенду
|
213
156
|
legend_parts = [
|
214
157
|
["Legend: ", nil],
|
215
158
|
["f", 'f'], ["=free", nil], [", ", nil],
|
@@ -228,7 +171,6 @@ module FreeRange
|
|
228
171
|
end.join
|
229
172
|
puts "\n#{legend_text}"
|
230
173
|
|
231
|
-
# Виводимо підсумок
|
232
174
|
summary_parts = [
|
233
175
|
["Total: ", nil],
|
234
176
|
["f", 'f'], ["=#{status_counts['f']}", nil], [", ", nil],
|
@@ -250,8 +192,6 @@ module FreeRange
|
|
250
192
|
|
251
193
|
def self.table_png(ranges, vlans, path, target, interface = nil)
|
252
194
|
all_vlans, status_counts = build_vlan_statuses(ranges, vlans)
|
253
|
-
|
254
|
-
# Налаштування розмірів і стилів
|
255
195
|
cell_width = 12
|
256
196
|
cell_height = 20
|
257
197
|
rows = 41
|
@@ -259,42 +199,35 @@ module FreeRange
|
|
259
199
|
header_height = 60
|
260
200
|
label_width = 50
|
261
201
|
width = label_width + cols * cell_width + 10
|
262
|
-
height = header_height + rows * cell_height + 20 + 50
|
202
|
+
height = header_height + rows * cell_height + 20 + 50
|
263
203
|
font_size = 14
|
264
|
-
title_font_size = 18
|
204
|
+
title_font_size = 18
|
265
205
|
font = 'Courier'
|
266
206
|
|
267
|
-
# Створюємо полотно
|
268
207
|
canvas = Magick::Image.new(width, height) { |options| options.background_color = 'white' }
|
269
208
|
gc = Magick::Draw.new
|
270
209
|
gc.font = font
|
271
210
|
gc.pointsize = font_size
|
272
211
|
gc.text_antialias = true
|
273
212
|
|
274
|
-
# Малюємо заголовок із назвою пристрою
|
275
213
|
gc.fill('black')
|
276
214
|
gc.pointsize = title_font_size
|
277
215
|
gc.text(10, 25, "VLAN Distribution for #{target}#{interface ? " (#{interface})" : ''}")
|
278
|
-
gc.pointsize = font_size
|
216
|
+
gc.pointsize = font_size
|
279
217
|
|
280
|
-
# Малюємо заголовок (0 1 2 ... 9)
|
281
218
|
(0..9).each do |i|
|
282
219
|
x = label_width + i * 10 * cell_width - 3
|
283
220
|
gc.fill('black')
|
284
221
|
gc.text(x + 5, header_height - 5, i.to_s)
|
285
222
|
end
|
286
223
|
|
287
|
-
# Малюємо таблицю
|
288
224
|
(0..40).each do |h|
|
289
225
|
start_vlan = h * 100
|
290
226
|
end_vlan = [start_vlan + 99, 4094].min
|
291
227
|
y = header_height + h * cell_height
|
292
|
-
|
293
|
-
# Малюємо номер рядка
|
294
228
|
gc.fill('black')
|
295
229
|
gc.text(5, y + font_size, format("%4d", start_vlan))
|
296
230
|
|
297
|
-
# Малюємо клітинки
|
298
231
|
(start_vlan..end_vlan).each_with_index do |vlan, i|
|
299
232
|
status = all_vlans[vlan] || ' '
|
300
233
|
x = label_width + i * cell_width
|
@@ -314,7 +247,6 @@ module FreeRange
|
|
314
247
|
end
|
315
248
|
end
|
316
249
|
|
317
|
-
# Малюємо легенду з кольоровими фонами
|
318
250
|
legend_y = height - 50
|
319
251
|
x = 10
|
320
252
|
legend_parts = [
|
@@ -336,11 +268,10 @@ module FreeRange
|
|
336
268
|
else
|
337
269
|
gc.fill('black')
|
338
270
|
gc.text(x, legend_y, text)
|
339
|
-
x += text.length * 8
|
271
|
+
x += text.length * 8
|
340
272
|
end
|
341
273
|
end
|
342
274
|
|
343
|
-
# Малюємо підсумок VLAN-ів з кольоровими фонами
|
344
275
|
summary_y = height - 30
|
345
276
|
x = 10
|
346
277
|
summary_parts = [
|
@@ -362,11 +293,10 @@ module FreeRange
|
|
362
293
|
else
|
363
294
|
gc.fill('black')
|
364
295
|
gc.text(x, summary_y, text)
|
365
|
-
x += text.length * 8
|
296
|
+
x += text.length * 8
|
366
297
|
end
|
367
298
|
end
|
368
299
|
|
369
|
-
# Зберігаємо зображення
|
370
300
|
gc.draw(canvas)
|
371
301
|
FileUtils.mkdir_p(path) unless Dir.exist?(path)
|
372
302
|
filename = File.join(path, "free-range-#{target}#{interface ? "-#{interface.tr('/', '-')}" : ''}.png")
|
@@ -378,17 +308,12 @@ module FreeRange
|
|
378
308
|
|
379
309
|
def self.build_vlan_statuses(ranges, vlans)
|
380
310
|
all_vlans = {}
|
381
|
-
# Позначаємо VLAN із діапазонів як free
|
382
311
|
ranges.ranges.each do |start, finish|
|
383
312
|
(start..finish).each { |vlan| all_vlans[vlan] = 'f' }
|
384
313
|
end
|
385
|
-
# Оновлюємо статуси для зайнятих VLAN
|
386
314
|
vlans.vlans.uniq.each { |vlan| all_vlans[vlan] = all_vlans.key?(vlan) ? 'b' : 'e' }
|
387
|
-
# Позначаємо всі інші VLAN як unused
|
388
315
|
(1..4094).each { |vlan| all_vlans[vlan] = 'u' unless all_vlans.key?(vlan) }
|
389
|
-
# Оновлюємо статуси для "інших" VLAN
|
390
316
|
ranges.another_in_ranges.each { |vlan| all_vlans[vlan] = all_vlans.key?(vlan) && all_vlans[vlan] != 'u' ? 'c' : 'a' }
|
391
|
-
# Підраховуємо статуси
|
392
317
|
status_counts = { 'f' => 0, 'b' => 0, 'e' => 0, 'c' => 0, 'a' => 0, 'u' => 0 }
|
393
318
|
all_vlans.each_value { |status| status_counts[status] += 1 }
|
394
319
|
|
@@ -398,23 +323,15 @@ module FreeRange
|
|
398
323
|
def self.format_range(start, finish, status, use_color)
|
399
324
|
range_text = start == finish ? "#{start}" : "#{start}-#{finish}"
|
400
325
|
range_text_with_status = "#{range_text}(#{status})"
|
401
|
-
|
402
326
|
if use_color
|
403
327
|
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 # Без кольору для інших статусів
|
328
|
+
when 'f' then "\e[32m#{range_text}\e[0m" # Зелений для free
|
329
|
+
when 'b' then "\e[33m#{range_text}\e[0m" # Жовтий для busy
|
330
|
+
when 'e' then "\e[31m#{range_text}\e[0m" # Червоний для error
|
331
|
+
when 'c' then "\e[35m#{range_text}\e[0m" # Фіолетовий для configured
|
332
|
+
when 'a' then "\e[34m#{range_text}\e[0m" # Синій для another
|
333
|
+
when 'u' then "\e[90m#{range_text}\e[0m" # Темно-сірий для unused
|
334
|
+
else range_text # Без кольору для інших статусів
|
418
335
|
end
|
419
336
|
else
|
420
337
|
range_text_with_status # Текстовий вивід зі статусами
|
@@ -424,20 +341,13 @@ module FreeRange
|
|
424
341
|
def self.format_table_char(status, use_color)
|
425
342
|
if use_color
|
426
343
|
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 # Без кольору для інших статусів
|
344
|
+
when 'f' then "\e[48;5;2m\e[30m#{status}\e[0m" # Зелений фон, чорний текст
|
345
|
+
when 'b' then "\e[48;5;3m\e[30m#{status}\e[0m" # Жовтий фон, чорний текст
|
346
|
+
when 'e' then "\e[48;5;1m\e[30m#{status}\e[0m" # Червоний фон, чорний текст
|
347
|
+
when 'c' then "\e[48;5;5m\e[30m#{status}\e[0m" # Фіолетовий фон, чорний текст
|
348
|
+
when 'a' then "\e[48;5;4m\e[30m#{status}\e[0m" # Синій фон, чорний текст
|
349
|
+
when 'u' then "\e[48;5;8m\e[30m#{status}\e[0m" # Темно-сірий фон, чорний текст
|
350
|
+
else status # Без кольору для інших статусів
|
441
351
|
end
|
442
352
|
else
|
443
353
|
status # Текстовий вивід без кольорів
|
@@ -445,25 +355,14 @@ module FreeRange
|
|
445
355
|
end
|
446
356
|
end
|
447
357
|
|
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)
|
358
|
+
# Метод для заповнення Ranges для одного інтерфейсу
|
359
|
+
def self.process_interface(ssh_command, interface, ranges)
|
459
360
|
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
361
|
command_demux = interface ? "show configuration interfaces #{interface} | display set | match unnumbered-address" : 'show configuration interfaces | display set | match unnumbered-address'
|
461
362
|
command_another = interface ? "show configuration interfaces #{interface} | display set | match vlan-id" : 'show configuration interfaces | display set | match vlan-id'
|
462
363
|
|
463
|
-
# Виконуємо команду для отримання діапазонів
|
464
364
|
full_cmd = "#{ssh_command} '#{command_ranges}'"
|
465
365
|
result = `#{full_cmd}`.strip
|
466
|
-
|
467
366
|
unless result.empty?
|
468
367
|
result.each_line do |line|
|
469
368
|
if line =~ /ranges (\d+)(?:-(\d+))?/
|
@@ -474,10 +373,8 @@ module FreeRange
|
|
474
373
|
end
|
475
374
|
end
|
476
375
|
|
477
|
-
# Виконуємо команду для отримання unnumbered-address інтерфейсів (demux-source)
|
478
376
|
full_cmd = "#{ssh_command} '#{command_demux}'"
|
479
377
|
result = `#{full_cmd}`.strip
|
480
|
-
|
481
378
|
unless result.empty?
|
482
379
|
result.each_line do |line|
|
483
380
|
if line =~ /unit (\d+)/
|
@@ -490,10 +387,8 @@ module FreeRange
|
|
490
387
|
end
|
491
388
|
end
|
492
389
|
|
493
|
-
# Виконуємо команду для отримання vlan-ів усіх наявних інтерфейсів
|
494
390
|
full_cmd = "#{ssh_command} '#{command_another}'"
|
495
391
|
result = `#{full_cmd}`.strip
|
496
|
-
|
497
392
|
unless result.empty?
|
498
393
|
result.each_line do |line|
|
499
394
|
if line =~ /vlan-id (\d+)/
|
@@ -507,94 +402,129 @@ module FreeRange
|
|
507
402
|
end
|
508
403
|
end
|
509
404
|
|
510
|
-
#
|
511
|
-
|
512
|
-
|
513
|
-
|
514
|
-
|
515
|
-
|
516
|
-
|
517
|
-
|
518
|
-
|
519
|
-
|
520
|
-
|
521
|
-
|
522
|
-
|
405
|
+
# Основна логіка виконання
|
406
|
+
def self.run
|
407
|
+
options = {}
|
408
|
+
OptionParser.new do |opts|
|
409
|
+
opts.banner = "Використання: free-range <IP-адреса або hostname> [опції]"
|
410
|
+
opts.on("-u", "--username USERNAME", "Ім'я користувача для SSH") { |u| options[:username] = u }
|
411
|
+
opts.on("-p", "--password PASSWORD", "Пароль для SSH") { |p| options[:password] = p }
|
412
|
+
opts.on("-n", "--no-color", "Вимкнути кольоровий вивід") { options[:no_color] = true }
|
413
|
+
opts.on("-d", "--debug", "Увімкнути дебаг-режим") { options[:debug] = true }
|
414
|
+
opts.on("-t", "--table", "Вивести діаграму розподілу VLAN-ів") { options[:table] = true }
|
415
|
+
opts.on("-g", "--table-png PATH", "Зберегти діаграму розподілу VLAN-ів як PNG") { |path| options[:table_png] = path }
|
416
|
+
opts.on("-i", "--interface INTERFACE", "Назва інтерфейсу або 'all'") { |i| options[:interface] = i }
|
417
|
+
end.parse!
|
418
|
+
|
419
|
+
if ARGV.empty?
|
420
|
+
puts "Помилка: потрібно вказати IP-адресу або hostname роутера."
|
421
|
+
puts "Використання: free-range <IP-адреса або hostname> [-u|--username USERNAME] [-p|--password PASSWORD] [-n|--no-color] [-d|--debug] [-t|--table] [--table-png PATH] [-i|--interface INTERFACE]"
|
422
|
+
exit 1
|
423
|
+
end
|
523
424
|
|
524
|
-
|
525
|
-
|
425
|
+
username = options[:username] || ENV['WHOAMI']
|
426
|
+
password = options[:password] || ENV['WHATISMYPASSWD']
|
427
|
+
if username.nil? || password.nil?
|
428
|
+
puts "Помилка: необхідно вказати ім'я користувача та пароль."
|
429
|
+
puts "Використовуйте опції -u|--username і -p|--password або змінні оточення WHOAMI і WHATISMYPASSWD."
|
526
430
|
exit 1
|
527
431
|
end
|
528
432
|
|
529
|
-
|
530
|
-
|
531
|
-
|
433
|
+
use_color = !options[:no_color] && ENV['TERM'] && ENV['TERM'] != 'dumb'
|
434
|
+
debug = options[:debug]
|
435
|
+
table_mode = options[:table]
|
436
|
+
table_png_mode = options[:table_png]
|
437
|
+
interface = options[:interface]
|
438
|
+
|
439
|
+
# # Перевірка формату інтерфейсу
|
440
|
+
# if interface && interface != "all" && interface !~ /^[a-z]+-\d+\/\d+\/\d+$/
|
441
|
+
# puts "Помилка: некоректна назва інтерфейсу. Використовуйте формат 'xe-0/0/2' або 'all'."
|
442
|
+
# exit 1
|
443
|
+
# end
|
444
|
+
|
445
|
+
login = { target: ARGV[0], username: username, password: password }
|
446
|
+
target = ARGV[0].split('.')[0]
|
447
|
+
puts "Connecting to device: #{login[:target]}"
|
448
|
+
|
449
|
+
ssh_command = "sshpass -p \"#{login[:password]}\" ssh -C -x -4 -o StrictHostKeyChecking=no #{login[:username]}@#{login[:target]}"
|
450
|
+
subscribers_command = "ssh -C -x roffice /usr/local/share/noc/bin/radius-subscribers"
|
451
|
+
|
452
|
+
subscribers_result = `#{subscribers_command}`.strip
|
453
|
+
if subscribers_result.empty?
|
454
|
+
puts "Помилка: результат subscribers_command порожній. Перевір шлях або доступ."
|
532
455
|
exit 1
|
533
456
|
end
|
534
457
|
|
535
|
-
|
458
|
+
if interface == "all"
|
459
|
+
command_interfaces = 'show configuration interfaces | no-more | display set | match dynamic-profile | match "ranges ([0-9]+(-[0-9]+)?)"'
|
460
|
+
full_cmd = "#{ssh_command} '#{command_interfaces}'"
|
461
|
+
result = `#{full_cmd}`.strip
|
462
|
+
if result.empty?
|
463
|
+
puts "Помилка: результат команди порожній. Перевір підключення або команду."
|
464
|
+
exit 1
|
465
|
+
end
|
466
|
+
|
467
|
+
interfaces = result.each_line.map { |line| line.split[2] }.uniq
|
468
|
+
if interfaces.empty?
|
469
|
+
puts "Помилка: не знайдено інтерфейсів із діапазонами."
|
470
|
+
exit 1
|
471
|
+
end
|
472
|
+
|
473
|
+
interfaces.each do |intf|
|
474
|
+
ranges = Ranges.new
|
475
|
+
vlans = Vlans.new
|
476
|
+
subscribers_result.each_line do |line|
|
477
|
+
if line.split.first =~ /dhcp(?:_[0-9a-fA-F.]+)?_([^:]+):(\d+)@#{Regexp.escape(target)}$/
|
478
|
+
subscriber_interface, vlan = $1, $2.to_i
|
479
|
+
vlans.add_vlan(vlan) if subscriber_interface == intf && vlan > 0
|
480
|
+
end
|
481
|
+
end
|
482
|
+
|
483
|
+
process_interface(ssh_command, intf, ranges)
|
484
|
+
if debug
|
485
|
+
puts "\nІнтерфейс: #{intf}"
|
486
|
+
Print.ranged(ranges)
|
487
|
+
Print.vlans(vlans)
|
488
|
+
Print.vlan_ranges(vlans)
|
489
|
+
puts
|
490
|
+
end
|
491
|
+
if table_png_mode
|
492
|
+
Print.table_png(ranges, vlans, table_png_mode, target, intf)
|
493
|
+
elsif table_mode
|
494
|
+
Print.table(ranges, vlans, use_color, target, intf)
|
495
|
+
else
|
496
|
+
Print.combined_ranges(ranges, vlans, use_color, target, intf)
|
497
|
+
end
|
498
|
+
end
|
499
|
+
else
|
536
500
|
ranges = Ranges.new
|
537
501
|
vlans = Vlans.new
|
538
|
-
|
539
|
-
# Заповнюємо VLAN-и, фільтруючи за інтерфейсом
|
540
502
|
subscribers_result.each_line do |line|
|
541
503
|
if line.split.first =~ /dhcp(?:_[0-9a-fA-F.]+)?_([^:]+):(\d+)@#{Regexp.escape(target)}$/
|
542
504
|
subscriber_interface, vlan = $1, $2.to_i
|
543
|
-
|
505
|
+
if interface
|
506
|
+
vlans.add_vlan(vlan) if subscriber_interface == interface && vlan > 0
|
507
|
+
else
|
508
|
+
vlans.add_vlan(vlan) if vlan > 0
|
509
|
+
end
|
544
510
|
end
|
545
511
|
end
|
546
512
|
|
547
|
-
process_interface(ssh_command,
|
548
|
-
|
549
|
-
# Виводимо результати
|
513
|
+
process_interface(ssh_command, interface, ranges)
|
550
514
|
if debug
|
551
|
-
puts "\nІнтерфейс: #{
|
515
|
+
puts "\nІнтерфейс: #{interface}" if interface
|
552
516
|
Print.ranged(ranges)
|
553
517
|
Print.vlans(vlans)
|
554
518
|
Print.vlan_ranges(vlans)
|
555
519
|
puts
|
556
520
|
end
|
557
521
|
if table_png_mode
|
558
|
-
Print.table_png(ranges, vlans, table_png_mode, target,
|
522
|
+
Print.table_png(ranges, vlans, table_png_mode, target, interface)
|
559
523
|
elsif table_mode
|
560
|
-
Print.table(ranges, vlans, use_color, target,
|
524
|
+
Print.table(ranges, vlans, use_color, target, interface)
|
561
525
|
else
|
562
|
-
Print.combined_ranges(ranges, vlans, use_color, target,
|
526
|
+
Print.combined_ranges(ranges, vlans, use_color, target, interface)
|
563
527
|
end
|
564
528
|
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
529
|
end
|
599
|
-
|
600
530
|
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.1
|
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
|