free-range 0.1.2 → 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
data/lib/free-range.rb.~ CHANGED
@@ -1,68 +1,60 @@
1
- module FreeRange
2
-
3
- require 'optparse'
4
- require 'rmagick'
5
- require 'fileutils'
1
+ require 'optparse'
2
+ require 'rmagick'
3
+ require 'fileutils'
6
4
 
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
5
+ module FreeRange
6
+ # Перевірка наявності ImageMagick
7
+ begin
8
+ require 'rmagick'
9
+ rescue LoadError
10
+ puts "Помилка: бібліотека rmagick не встановлена або ImageMagick недоступний."
11
+ puts "Встановіть ImageMagick і виконайте: gem install rmagick"
12
+ exit 1
13
+ end
15
14
 
16
- opts.on("-p", "--password PASSWORD", "Пароль для SSH") do |p|
17
- options[:password] = p
18
- end
15
+ # Клас для зберігання конфігураційних команд
16
+ class Config
17
+ attr_accessor :ssh_command, :subscribers_command
19
18
 
20
- opts.on("-n", "--no-color", "Вимкнути кольоровий вивід") do
21
- options[:no_color] = true
19
+ # Ініціалізує об’єкт конфігурації з значеннями за замовчуванням
20
+ # @param login [Hash] Hash with target, username, and password
21
+ # @yield [self] Yields self for block-based configuration
22
+ def initialize(login)
23
+ @login = login
24
+ # Значення за замовчуванням
25
+ @ssh_command = "sshpass -p \"#{@login[:password]}\" ssh -C -x -4 -o StrictHostKeyChecking=no #{@login[:username]}@#{@login[:target]}"
26
+ @subscribers_command = "ssh -C -x roffice /usr/local/share/noc/bin/radius-subscribers"
27
+ yield self if block_given?
22
28
  end
23
29
 
24
- opts.on("-d", "--debug", "Увімкнути дебаг-режим") do
25
- options[:debug] = true
30
+ # Повертає команду для отримання списку інтерфейсів
31
+ # @return [String] Command to fetch interfaces
32
+ def command_interfaces
33
+ 'show configuration interfaces | no-more | display set | match dynamic-profile | match "ranges ([0-9]+(-[0-9]+)?)"'
26
34
  end
27
35
 
28
- opts.on("-t", "--table", "Вивести діаграму розподілу VLAN-ів") do
29
- options[:table] = true
36
+ # Повертає команду для отримання діапазонів VLAN
37
+ # @param interface [String, nil] Interface name or nil for all interfaces
38
+ # @return [String] Command to fetch VLAN ranges
39
+ def command_ranges(interface)
40
+ interface ? "show configuration interfaces #{interface} | no-more | display set | match dynamic-profile | match \"ranges ([0-9]+(-[0-9]+)?)\"" : 'show configuration interfaces | no-more | display set | match dynamic-profile | match "ranges ([0-9]+(-[0-9]+)?)"'
30
41
  end
31
42
 
32
- opts.on("-g", "--table-png PATH", "Зберегти діаграму розподілу VLAN-ів як PNG у вказаний каталог") do |path|
33
- options[:table_png] = path
43
+ # Повертає команду для отримання демультиплексорних VLAN
44
+ # @param interface [String, nil] Interface name or nil for all interfaces
45
+ # @return [String] Command to fetch demux VLANs
46
+ def command_demux(interface)
47
+ interface ? "show configuration interfaces #{interface} | display set | match unnumbered-address" : 'show configuration interfaces | display set | match unnumbered-address'
34
48
  end
35
49
 
36
- opts.on("-i", "--interface INTERFACE", "Назва інтерфейсу або 'all'") do |i|
37
- options[:interface] = i
50
+ # Повертає команду для отримання інших VLAN
51
+ # @param interface [String, nil] Interface name or nil for all interfaces
52
+ # @return [String] Command to fetch other VLANs
53
+ def command_another(interface)
54
+ interface ? "show configuration interfaces #{interface} | display set | match vlan-id" : 'show configuration interfaces | display set | match vlan-id'
38
55
  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
56
  end
58
57
 
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
58
  # Абстрактний клас для роботи з VLAN
67
59
  class VlanContainer
68
60
  def initialize
@@ -70,16 +62,20 @@ module FreeRange
70
62
  end
71
63
 
72
64
  # Додаємо VLAN до списку (віртуальний метод, може бути перевизначений)
65
+ # @param vlan [Integer] VLAN ID to add
66
+ # @return [void]
73
67
  def add_vlan(vlan)
74
68
  @vlans << vlan
75
69
  end
76
70
 
77
71
  # Повертаємо масив VLAN
72
+ # @return [Array<Integer>] Sorted array of unique VLAN IDs
78
73
  def vlans
79
74
  @vlans.uniq.sort
80
75
  end
81
76
 
82
77
  # Створюємо хеш діапазонів із VLAN
78
+ # @return [Hash{Integer => Integer}] Hash mapping range start to range end
83
79
  def ranges
84
80
  return {} if @vlans.empty?
85
81
 
@@ -109,16 +105,23 @@ module FreeRange
109
105
  end
110
106
 
111
107
  # Додаємо діапазон до списку VLAN-ів
108
+ # @param start [Integer] Start of VLAN range
109
+ # @param finish [Integer] End of VLAN range
110
+ # @return [void]
112
111
  def add_range(start, finish)
113
112
  (start..finish).each { |vlan| @vlans << vlan }
114
113
  end
115
114
 
116
115
  # Додаємо діапазон до списку "інших" VLAN-ів
116
+ # @param start [Integer] Start of another VLAN range
117
+ # @param finish [Integer] End of another VLAN range
118
+ # @return [void]
117
119
  def add_another_range(start, finish)
118
120
  (start..finish).each { |vlan| @another_in_ranges << vlan }
119
121
  end
120
122
 
121
123
  # Повертаємо масив "інших" VLAN-ів
124
+ # @return [Array<Integer>] Sorted array of unique "another" VLAN IDs
122
125
  def another_in_ranges
123
126
  @another_in_ranges.uniq.sort
124
127
  end
@@ -134,6 +137,9 @@ module FreeRange
134
137
 
135
138
  # Клас для виводу даних
136
139
  class Print
140
+ # Виводить хеш діапазонів VLAN у порядку зростання
141
+ # @param ranges [Ranges] Object containing VLAN ranges
142
+ # @return [void]
137
143
  def self.ranged(ranges)
138
144
  puts "\nСформований хеш діапазонів (в порядку зростання):"
139
145
  if ranges.ranges.empty?
@@ -145,6 +151,9 @@ module FreeRange
145
151
  end
146
152
  end
147
153
 
154
+ # Виводить зайняті VLAN-и в межах діапазонів у порядку зростання
155
+ # @param vlans [Vlans] Object containing VLANs
156
+ # @return [void]
148
157
  def self.vlans(vlans)
149
158
  puts "\nЗайняті VLAN-и в межах діапазонів (в порядку зростання):"
150
159
  if vlans.vlans.empty?
@@ -154,6 +163,9 @@ module FreeRange
154
163
  end
155
164
  end
156
165
 
166
+ # Виводить діапазони VLAN-ів у порядку зростання
167
+ # @param vlans [Vlans] Object containing VLANs
168
+ # @return [void]
157
169
  def self.vlan_ranges(vlans)
158
170
  puts "\nДіапазони VLAN-ів (в порядку зростання):"
159
171
  vlan_ranges = vlans.vlan_ranges
@@ -166,6 +178,13 @@ module FreeRange
166
178
  end
167
179
  end
168
180
 
181
+ # Виводить комбіновані діапазони VLAN зі статусами
182
+ # @param ranges [Ranges] Object containing VLAN ranges
183
+ # @param vlans [Vlans] Object containing VLANs
184
+ # @param use_color [Boolean] Enable colored output
185
+ # @param target [String] Target device hostname
186
+ # @param interface [String, nil] Interface name or nil
187
+ # @return [void]
169
188
  def self.combined_ranges(ranges, vlans, use_color, target, interface = nil)
170
189
  if ranges.ranges.empty?
171
190
  puts "Не знайдено діапазонів."
@@ -173,8 +192,6 @@ module FreeRange
173
192
  end
174
193
 
175
194
  all_vlans, _status_counts = build_vlan_statuses(ranges, vlans)
176
-
177
- # Формуємо діапазони з урахуванням статусів
178
195
  result = []
179
196
  sorted_vlans = all_vlans.keys.sort
180
197
  start = sorted_vlans.first
@@ -183,24 +200,28 @@ module FreeRange
183
200
 
184
201
  sorted_vlans[1..-1].each do |vlan|
185
202
  unless vlan == prev + 1 && all_vlans[vlan] == status
186
- # Завершуємо попередній діапазон
187
203
  result << format_range(start, prev, status, use_color)
188
204
  start = vlan
189
205
  status = all_vlans[vlan]
190
206
  end
191
207
  prev = vlan
192
208
  end
193
- # Додаємо останній діапазон
194
209
  result << format_range(start, prev, status, use_color)
195
210
 
196
211
  puts result.join(',')
197
212
  end
198
213
 
214
+ # Виводить таблицю розподілу VLAN
215
+ # @param ranges [Ranges] Object containing VLAN ranges
216
+ # @param vlans [Vlans] Object containing VLANs
217
+ # @param use_color [Boolean] Enable colored output
218
+ # @param target [String] Target device hostname
219
+ # @param interface [String, nil] Interface name or nil
220
+ # @return [void]
199
221
  def self.table(ranges, vlans, use_color, target, interface = nil)
200
222
  puts "VLAN Distribution for #{target}#{interface ? " (#{interface})" : ''}"
201
223
  all_vlans, status_counts = build_vlan_statuses(ranges, vlans)
202
224
 
203
- # Виводимо заголовок
204
225
  puts " 0 1 2 3 4 5 6 7 8 9 "
205
226
  (0..40).each do |h|
206
227
  start_vlan = h * 100
@@ -209,7 +230,6 @@ module FreeRange
209
230
  puts "#{format("%4d", start_vlan)} #{row}"
210
231
  end
211
232
 
212
- # Виводимо легенду
213
233
  legend_parts = [
214
234
  ["Legend: ", nil],
215
235
  ["f", 'f'], ["=free", nil], [", ", nil],
@@ -228,7 +248,6 @@ module FreeRange
228
248
  end.join
229
249
  puts "\n#{legend_text}"
230
250
 
231
- # Виводимо підсумок
232
251
  summary_parts = [
233
252
  ["Total: ", nil],
234
253
  ["f", 'f'], ["=#{status_counts['f']}", nil], [", ", nil],
@@ -248,10 +267,15 @@ module FreeRange
248
267
  puts summary_text
249
268
  end
250
269
 
270
+ # Зберігає таблицю розподілу VLAN як PNG-зображення
271
+ # @param ranges [Ranges] Object containing VLAN ranges
272
+ # @param vlans [Vlans] Object containing VLANs
273
+ # @param path [String] Directory path to save the PNG
274
+ # @param target [String] Target device hostname
275
+ # @param interface [String, nil] Interface name or nil
276
+ # @return [void]
251
277
  def self.table_png(ranges, vlans, path, target, interface = nil)
252
278
  all_vlans, status_counts = build_vlan_statuses(ranges, vlans)
253
-
254
- # Налаштування розмірів і стилів
255
279
  cell_width = 12
256
280
  cell_height = 20
257
281
  rows = 41
@@ -259,42 +283,35 @@ module FreeRange
259
283
  header_height = 60
260
284
  label_width = 50
261
285
  width = label_width + cols * cell_width + 10
262
- height = header_height + rows * cell_height + 20 + 50 # Вистачає для легенди і підсумку
286
+ height = header_height + rows * cell_height + 20 + 50
263
287
  font_size = 14
264
- title_font_size = 18 # Більший шрифт для заголовка
288
+ title_font_size = 18
265
289
  font = 'Courier'
266
290
 
267
- # Створюємо полотно
268
291
  canvas = Magick::Image.new(width, height) { |options| options.background_color = 'white' }
269
292
  gc = Magick::Draw.new
270
293
  gc.font = font
271
294
  gc.pointsize = font_size
272
295
  gc.text_antialias = true
273
296
 
274
- # Малюємо заголовок із назвою пристрою
275
297
  gc.fill('black')
276
298
  gc.pointsize = title_font_size
277
299
  gc.text(10, 25, "VLAN Distribution for #{target}#{interface ? " (#{interface})" : ''}")
278
- gc.pointsize = font_size # Повертаємо стандартний розмір шрифту
300
+ gc.pointsize = font_size
279
301
 
280
- # Малюємо заголовок (0 1 2 ... 9)
281
302
  (0..9).each do |i|
282
303
  x = label_width + i * 10 * cell_width - 3
283
304
  gc.fill('black')
284
305
  gc.text(x + 5, header_height - 5, i.to_s)
285
306
  end
286
307
 
287
- # Малюємо таблицю
288
308
  (0..40).each do |h|
289
309
  start_vlan = h * 100
290
310
  end_vlan = [start_vlan + 99, 4094].min
291
311
  y = header_height + h * cell_height
292
-
293
- # Малюємо номер рядка
294
312
  gc.fill('black')
295
313
  gc.text(5, y + font_size, format("%4d", start_vlan))
296
314
 
297
- # Малюємо клітинки
298
315
  (start_vlan..end_vlan).each_with_index do |vlan, i|
299
316
  status = all_vlans[vlan] || ' '
300
317
  x = label_width + i * cell_width
@@ -314,7 +331,6 @@ module FreeRange
314
331
  end
315
332
  end
316
333
 
317
- # Малюємо легенду з кольоровими фонами
318
334
  legend_y = height - 50
319
335
  x = 10
320
336
  legend_parts = [
@@ -336,11 +352,10 @@ module FreeRange
336
352
  else
337
353
  gc.fill('black')
338
354
  gc.text(x, legend_y, text)
339
- x += text.length * 8 # Приблизно 8 пікселів на символ
355
+ x += text.length * 8
340
356
  end
341
357
  end
342
358
 
343
- # Малюємо підсумок VLAN-ів з кольоровими фонами
344
359
  summary_y = height - 30
345
360
  x = 10
346
361
  summary_parts = [
@@ -362,11 +377,10 @@ module FreeRange
362
377
  else
363
378
  gc.fill('black')
364
379
  gc.text(x, summary_y, text)
365
- x += text.length * 8 # Приблизно 8 пікселів на символ
380
+ x += text.length * 8
366
381
  end
367
382
  end
368
383
 
369
- # Зберігаємо зображення
370
384
  gc.draw(canvas)
371
385
  FileUtils.mkdir_p(path) unless Dir.exist?(path)
372
386
  filename = File.join(path, "free-range-#{target}#{interface ? "-#{interface.tr('/', '-')}" : ''}.png")
@@ -376,68 +390,62 @@ module FreeRange
376
390
 
377
391
  private
378
392
 
393
+ # Будує хеш статусів VLAN і підраховує кількість кожного статусу
394
+ # @param ranges [Ranges] Object containing VLAN ranges
395
+ # @param vlans [Vlans] Object containing VLANs
396
+ # @return [Array<Hash, Hash>] Hash of VLAN statuses and status counts
379
397
  def self.build_vlan_statuses(ranges, vlans)
380
398
  all_vlans = {}
381
- # Позначаємо VLAN із діапазонів як free
382
399
  ranges.ranges.each do |start, finish|
383
400
  (start..finish).each { |vlan| all_vlans[vlan] = 'f' }
384
401
  end
385
- # Оновлюємо статуси для зайнятих VLAN
386
402
  vlans.vlans.uniq.each { |vlan| all_vlans[vlan] = all_vlans.key?(vlan) ? 'b' : 'e' }
387
- # Позначаємо всі інші VLAN як unused
388
403
  (1..4094).each { |vlan| all_vlans[vlan] = 'u' unless all_vlans.key?(vlan) }
389
- # Оновлюємо статуси для "інших" VLAN
390
404
  ranges.another_in_ranges.each { |vlan| all_vlans[vlan] = all_vlans.key?(vlan) && all_vlans[vlan] != 'u' ? 'c' : 'a' }
391
- # Підраховуємо статуси
392
405
  status_counts = { 'f' => 0, 'b' => 0, 'e' => 0, 'c' => 0, 'a' => 0, 'u' => 0 }
393
406
  all_vlans.each_value { |status| status_counts[status] += 1 }
394
407
 
395
408
  [all_vlans, status_counts]
396
409
  end
397
410
 
411
+ # Форматує діапазон VLAN зі статусом для виводу
412
+ # @param start [Integer] Start of VLAN range
413
+ # @param finish [Integer] End of VLAN range
414
+ # @param status [String] VLAN status ('f', 'b', 'e', 'c', 'a', 'u')
415
+ # @param use_color [Boolean] Enable colored output
416
+ # @return [String] Formatted range string
398
417
  def self.format_range(start, finish, status, use_color)
399
418
  range_text = start == finish ? "#{start}" : "#{start}-#{finish}"
400
419
  range_text_with_status = "#{range_text}(#{status})"
401
-
402
420
  if use_color
403
421
  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 # Без кольору для інших статусів
422
+ when 'f' then "\e[32m#{range_text}\e[0m" # Зелений для free
423
+ when 'b' then "\e[33m#{range_text}\e[0m" # Жовтий для busy
424
+ when 'e' then "\e[31m#{range_text}\e[0m" # Червоний для error
425
+ when 'c' then "\e[35m#{range_text}\e[0m" # Фіолетовий для configured
426
+ when 'a' then "\e[34m#{range_text}\e[0m" # Синій для another
427
+ when 'u' then "\e[90m#{range_text}\e[0m" # Темно-сірий для unused
428
+ else range_text # Без кольору для інших статусів
418
429
  end
419
430
  else
420
431
  range_text_with_status # Текстовий вивід зі статусами
421
432
  end
422
433
  end
423
434
 
435
+ # Форматує символ для таблиці VLAN
436
+ # @param status [String] VLAN status ('f', 'b', 'e', 'c', 'a', 'u', or ' ')
437
+ # @param use_color [Boolean] Enable colored output
438
+ # @return [String] Formatted character for table display
424
439
  def self.format_table_char(status, use_color)
425
440
  if use_color
426
441
  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 # Без кольору для інших статусів
442
+ when 'f' then "\e[48;5;2m\e[30m#{status}\e[0m" # Зелений фон, чорний текст
443
+ when 'b' then "\e[48;5;3m\e[30m#{status}\e[0m" # Жовтий фон, чорний текст
444
+ when 'e' then "\e[48;5;1m\e[30m#{status}\e[0m" # Червоний фон, чорний текст
445
+ when 'c' then "\e[48;5;5m\e[30m#{status}\e[0m" # Фіолетовий фон, чорний текст
446
+ when 'a' then "\e[48;5;4m\e[30m#{status}\e[0m" # Синій фон, чорний текст
447
+ when 'u' then "\e[48;5;8m\e[30m#{status}\e[0m" # Темно-сірий фон, чорний текст
448
+ else status # Без кольору для інших статусів
441
449
  end
442
450
  else
443
451
  status # Текстовий вивід без кольорів
@@ -445,25 +453,14 @@ module FreeRange
445
453
  end
446
454
  end
447
455
 
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}'"
456
+ # Метод для заповнення Ranges для одного інтерфейсу
457
+ # @param config [Config] Configuration object with commands
458
+ # @param interface [String, nil] Interface name or nil for all interfaces
459
+ # @param ranges [Ranges] Object to store VLAN ranges
460
+ # @return [void]
461
+ def self.process_interface(config, interface, ranges)
462
+ full_cmd = "#{config.ssh_command} '#{config.command_ranges(interface)}'"
465
463
  result = `#{full_cmd}`.strip
466
-
467
464
  unless result.empty?
468
465
  result.each_line do |line|
469
466
  if line =~ /ranges (\d+)(?:-(\d+))?/
@@ -474,10 +471,8 @@ module FreeRange
474
471
  end
475
472
  end
476
473
 
477
- # Виконуємо команду для отримання unnumbered-address інтерфейсів (demux-source)
478
- full_cmd = "#{ssh_command} '#{command_demux}'"
474
+ full_cmd = "#{config.ssh_command} '#{config.command_demux(interface)}'"
479
475
  result = `#{full_cmd}`.strip
480
-
481
476
  unless result.empty?
482
477
  result.each_line do |line|
483
478
  if line =~ /unit (\d+)/
@@ -490,10 +485,8 @@ module FreeRange
490
485
  end
491
486
  end
492
487
 
493
- # Виконуємо команду для отримання vlan-ів усіх наявних інтерфейсів
494
- full_cmd = "#{ssh_command} '#{command_another}'"
488
+ full_cmd = "#{config.ssh_command} '#{config.command_another(interface)}'"
495
489
  result = `#{full_cmd}`.strip
496
-
497
490
  unless result.empty?
498
491
  result.each_line do |line|
499
492
  if line =~ /vlan-id (\d+)/
@@ -507,94 +500,142 @@ module FreeRange
507
500
  end
508
501
  end
509
502
 
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
503
+ # Основна логіка виконання
504
+ # @return [void]
505
+ def self.run
506
+ options = {}
507
+ OptionParser.new do |opts|
508
+ opts.banner = <<~BANNER
509
+ Використання: free-range <IP-адреса або hostname> [опції]
510
+
511
+ Аналізує розподіл VLAN на мережевих пристроях, генеруючи таблиці або PNG-зображення.
512
+ BANNER
513
+ opts.on("-h", "--help", "Показати цю довідку") { puts opts; exit 0 }
514
+ opts.on("-u", "--username USERNAME", "Ім'я користувача для SSH") { |u| options[:username] = u }
515
+ opts.on("-p", "--password PASSWORD", "Пароль для SSH") { |p| options[:password] = p }
516
+ opts.on("-n", "--no-color", "Вимкнути кольоровий вивід") { options[:no_color] = true }
517
+ opts.on("-d", "--debug", "Увімкнути дебаг-режим") { options[:debug] = true }
518
+ opts.on("-t", "--table", "Вивести діаграму розподілу VLAN-ів") { options[:table] = true }
519
+ opts.on("-g", "--table-png PATH", "Зберегти діаграму розподілу VLAN-ів як PNG") { |path| options[:table_png] = path }
520
+ opts.on("-i", "--interface INTERFACE", "Назва інтерфейсу або 'all'") { |i| options[:interface] = i }
521
+ opts.on("-c", "--config CONFIG_FILE", "Шлях до конфігураційного файлу") { |c| options[:config_file] = c }
522
+ end.parse!
523
+
524
+ if ARGV.empty?
525
+ puts "Помилка: потрібно вказати IP-адресу або hostname роутера."
526
+ puts "Використовуйте: free-range --help для довідки."
527
+ exit 1
528
+ end
523
529
 
524
- if result.empty?
525
- puts "Помилка: результат команди порожній. Перевір підключення або команду."
530
+ username = options[:username] || ENV['WHOAMI']
531
+ password = options[:password] || ENV['WHATISMYPASSWD']
532
+ if username.nil? || password.nil?
533
+ puts "Помилка: необхідно вказати ім'я користувача та пароль."
534
+ puts "Використовуйте опції -u/--username і -p/--password або змінні оточення WHOAMI і WHATISMYPASSWD."
526
535
  exit 1
527
536
  end
528
537
 
529
- interfaces = result.each_line.map { |line| line.split[2] }.uniq
530
- if interfaces.empty?
531
- puts "Помилка: не знайдено інтерфейсів із діапазонами."
538
+ use_color = !options[:no_color] && ENV['TERM'] && ENV['TERM'] != 'dumb'
539
+ debug = options[:debug]
540
+ table_mode = options[:table]
541
+ table_png_mode = options[:table_png]
542
+ interface = options[:interface]
543
+ config_file = options[:config_file]
544
+
545
+ login = { target: ARGV[0], username: username, password: password }
546
+ target = ARGV[0].split('.')[0]
547
+ puts "Connecting to device: #{login[:target]}"
548
+
549
+ # Ініціалізація конфігурації
550
+ config = Config.new(login)
551
+ if config_file
552
+ begin
553
+ load config_file
554
+ config = FreeRange::Config.new(login) { |c| eval(File.read(config_file), binding, config_file) }
555
+ rescue LoadError, Errno::ENOENT
556
+ puts "Помилка: неможливо завантажити конфігураційний файл '#{config_file}'."
557
+ exit 1
558
+ rescue StandardError => e
559
+ puts "Помилка в конфігураційному файлі '#{config_file}': #{e.message}"
560
+ exit 1
561
+ end
562
+ end
563
+
564
+ subscribers_result = `#{config.subscribers_command}`.strip
565
+ if subscribers_result.empty?
566
+ puts "Помилка: результат subscribers_command порожній. Перевір шлях або доступ."
532
567
  exit 1
533
568
  end
534
569
 
535
- interfaces.each do |intf|
570
+ if interface == "all"
571
+ full_cmd = "#{config.ssh_command} '#{config.command_interfaces}'"
572
+ result = `#{full_cmd}`.strip
573
+ if result.empty?
574
+ puts "Помилка: результат команди порожній. Перевір підключення або команду."
575
+ exit 1
576
+ end
577
+
578
+ interfaces = result.each_line.map { |line| line.split[2] }.uniq
579
+ if interfaces.empty?
580
+ puts "Помилка: не знайдено інтерфейсів із діапазонами."
581
+ exit 1
582
+ end
583
+
584
+ interfaces.each do |intf|
585
+ ranges = Ranges.new
586
+ vlans = Vlans.new
587
+ subscribers_result.each_line do |line|
588
+ if line.split.first =~ /dhcp(?:_[0-9a-fA-F.]+)?_([^:]+):(\d+)@#{Regexp.escape(target)}$/
589
+ subscriber_interface, vlan = $1, $2.to_i
590
+ vlans.add_vlan(vlan) if subscriber_interface == intf && vlan > 0
591
+ end
592
+ end
593
+
594
+ process_interface(config, intf, ranges)
595
+ if debug
596
+ puts "\nІнтерфейс: #{intf}"
597
+ Print.ranged(ranges)
598
+ Print.vlans(vlans)
599
+ Print.vlan_ranges(vlans)
600
+ puts
601
+ end
602
+ if table_png_mode
603
+ Print.table_png(ranges, vlans, table_png_mode, target, intf)
604
+ elsif table_mode
605
+ Print.table(ranges, vlans, use_color, target, intf)
606
+ else
607
+ Print.combined_ranges(ranges, vlans, use_color, target, intf)
608
+ end
609
+ end
610
+ else
536
611
  ranges = Ranges.new
537
612
  vlans = Vlans.new
538
-
539
- # Заповнюємо VLAN-и, фільтруючи за інтерфейсом
540
613
  subscribers_result.each_line do |line|
541
614
  if line.split.first =~ /dhcp(?:_[0-9a-fA-F.]+)?_([^:]+):(\d+)@#{Regexp.escape(target)}$/
542
615
  subscriber_interface, vlan = $1, $2.to_i
543
- vlans.add_vlan(vlan) if subscriber_interface == intf && vlan > 0
616
+ if interface
617
+ vlans.add_vlan(vlan) if subscriber_interface == interface && vlan > 0
618
+ else
619
+ vlans.add_vlan(vlan) if vlan > 0
620
+ end
544
621
  end
545
622
  end
546
623
 
547
- process_interface(ssh_command, intf, ranges)
548
-
549
- # Виводимо результати
624
+ process_interface(config, interface, ranges)
550
625
  if debug
551
- puts "\nІнтерфейс: #{intf}"
626
+ puts "\nІнтерфейс: #{interface}" if interface
552
627
  Print.ranged(ranges)
553
628
  Print.vlans(vlans)
554
629
  Print.vlan_ranges(vlans)
555
630
  puts
556
631
  end
557
632
  if table_png_mode
558
- Print.table_png(ranges, vlans, table_png_mode, target, intf)
633
+ Print.table_png(ranges, vlans, table_png_mode, target, interface)
559
634
  elsif table_mode
560
- Print.table(ranges, vlans, use_color, target, intf)
635
+ Print.table(ranges, vlans, use_color, target, interface)
561
636
  else
562
- Print.combined_ranges(ranges, vlans, use_color, target, intf)
637
+ Print.combined_ranges(ranges, vlans, use_color, target, interface)
563
638
  end
564
639
  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
640
  end
599
-
600
641
  end