free-range 0.1.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 +7 -0
- data/bin/free-range +6 -0
- data/lib/free-range.rb +600 -0
- metadata +88 -0
checksums.yaml
ADDED
@@ -0,0 +1,7 @@
|
|
1
|
+
---
|
2
|
+
SHA256:
|
3
|
+
metadata.gz: 76578fcf399be31962cc3e611e74c8cdb408f5a75277977e9a5aabb11b01ea41
|
4
|
+
data.tar.gz: 6d6e57d1e254f4745cd8b6afbc8524b7bd356f50f9ef049d9a6e36de72377ec6
|
5
|
+
SHA512:
|
6
|
+
metadata.gz: fed9b90bb0bc8eb9cc5244fe549f2d7d84cca82592e16d3081471c355e4fb238bd831d9e34657656a1010d50e7c6218206c7037e811bdd1bc45554ea7ac9e5a9
|
7
|
+
data.tar.gz: 3f7372c1bbbbbb51b6a4da6b4794713b10e3baa79b47f1025896a087fee7cca6e53fae28ce91890200c09720a1953a250651423c3c88e93e19f423318790428f
|
data/bin/free-range
ADDED
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
ADDED
@@ -0,0 +1,88 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: free-range
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 0.1.0
|
5
|
+
platform: ruby
|
6
|
+
authors:
|
7
|
+
- Oleksandr Russkikh //aka Olden Gremlin
|
8
|
+
autorequire:
|
9
|
+
bindir: bin
|
10
|
+
cert_chain: []
|
11
|
+
date: 2025-09-26 00:00:00.000000000 Z
|
12
|
+
dependencies:
|
13
|
+
- !ruby/object:Gem::Dependency
|
14
|
+
name: rmagick
|
15
|
+
requirement: !ruby/object:Gem::Requirement
|
16
|
+
requirements:
|
17
|
+
- - "~>"
|
18
|
+
- !ruby/object:Gem::Version
|
19
|
+
version: '5.3'
|
20
|
+
type: :runtime
|
21
|
+
prerelease: false
|
22
|
+
version_requirements: !ruby/object:Gem::Requirement
|
23
|
+
requirements:
|
24
|
+
- - "~>"
|
25
|
+
- !ruby/object:Gem::Version
|
26
|
+
version: '5.3'
|
27
|
+
- !ruby/object:Gem::Dependency
|
28
|
+
name: bundler
|
29
|
+
requirement: !ruby/object:Gem::Requirement
|
30
|
+
requirements:
|
31
|
+
- - "~>"
|
32
|
+
- !ruby/object:Gem::Version
|
33
|
+
version: '2.0'
|
34
|
+
type: :development
|
35
|
+
prerelease: false
|
36
|
+
version_requirements: !ruby/object:Gem::Requirement
|
37
|
+
requirements:
|
38
|
+
- - "~>"
|
39
|
+
- !ruby/object:Gem::Version
|
40
|
+
version: '2.0'
|
41
|
+
- !ruby/object:Gem::Dependency
|
42
|
+
name: rake
|
43
|
+
requirement: !ruby/object:Gem::Requirement
|
44
|
+
requirements:
|
45
|
+
- - "~>"
|
46
|
+
- !ruby/object:Gem::Version
|
47
|
+
version: '13.0'
|
48
|
+
type: :development
|
49
|
+
prerelease: false
|
50
|
+
version_requirements: !ruby/object:Gem::Requirement
|
51
|
+
requirements:
|
52
|
+
- - "~>"
|
53
|
+
- !ruby/object:Gem::Version
|
54
|
+
version: '13.0'
|
55
|
+
description: A Ruby script to analyze VLAN distribution on network devices, generating
|
56
|
+
tables or PNG images.
|
57
|
+
email: olden@ukr-com.net
|
58
|
+
executables:
|
59
|
+
- free-range
|
60
|
+
extensions: []
|
61
|
+
extra_rdoc_files: []
|
62
|
+
files:
|
63
|
+
- bin/free-range
|
64
|
+
- lib/free-range.rb
|
65
|
+
homepage: https://github.com/oldengremlin/free-range
|
66
|
+
licenses:
|
67
|
+
- Apache-2.0
|
68
|
+
metadata: {}
|
69
|
+
post_install_message:
|
70
|
+
rdoc_options: []
|
71
|
+
require_paths:
|
72
|
+
- lib
|
73
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
74
|
+
requirements:
|
75
|
+
- - ">="
|
76
|
+
- !ruby/object:Gem::Version
|
77
|
+
version: '0'
|
78
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
79
|
+
requirements:
|
80
|
+
- - ">="
|
81
|
+
- !ruby/object:Gem::Version
|
82
|
+
version: '0'
|
83
|
+
requirements: []
|
84
|
+
rubygems_version: 3.3.15
|
85
|
+
signing_key:
|
86
|
+
specification_version: 4
|
87
|
+
summary: VLAN distribution analysis tool
|
88
|
+
test_files: []
|